From 4d6d4daadd05165961464878c92533bc92a18578 Mon Sep 17 00:00:00 2001 From: anhduy-tech Date: Wed, 7 Jan 2026 16:01:06 +0700 Subject: [PATCH] changes --- api/__pycache__/settings.cpython-313.pyc | Bin 3451 -> 3451 bytes .../document_generator.cpython-313.pyc | Bin 21828 -> 30974 bytes app/document_generator.py | 340 +++++++++++++++--- .../1. Phiếu xác lập thỏa thuận ca nhan.docx | Bin 0 -> 16742 bytes 4 files changed, 300 insertions(+), 40 deletions(-) create mode 100644 static/contract/1. Phiếu xác lập thỏa thuận ca nhan.docx diff --git a/api/__pycache__/settings.cpython-313.pyc b/api/__pycache__/settings.cpython-313.pyc index 23f6b64ed81572569fda93f8c9a140eb8db57ece..a9a5fcb19e63980d32bf9c154d76bb6e8ae5475f 100644 GIT binary patch delta 20 acmew@^;?SjGcPX}0}vdPi`&RuzzYCHWd*|k delta 20 acmew@^;?SjGcPX}0}y<^7Q2zVfENHu4hF0M diff --git a/app/__pycache__/document_generator.cpython-313.pyc b/app/__pycache__/document_generator.cpython-313.pyc index b141b67ce7672f624f8be5c9a37c047ed1a1b58f..3fd3fcf495b6b7936b85fca59286b4ff4451ff7c 100644 GIT binary patch delta 8989 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 B-5me` delta 4998 zcmb7I4^)%a75`q6FNA~yFbN3|A(0da2ox*=0wM~c0{$VUx<(o!`M_xY(D!{P>KwM) zaki@)_ML9HZWY)4an-ZgvUYCmcARsY-P}r+wn@L^IkQ{O&OF=MX=l$?&;H)~MZnta z?DQS|@!oy+-gn=<-@Es{{OetE`B`GRX*B9NY=i#rrS&IFcGZ{2bCm~3MZVWq#O)z; zvDZ|@(Gokig!ASWao)T}mBP=_(h=fWG_zHGSVhKWe$8iVNG`jUy`6YiqyB|^#6`e% zgFC{_7B@bxIb%Mx?&)>UYi@}eAU|R+A~*gGjn!8?;77)9avlw}LPsWHnNT@)dN7&T*Gt*k^BLtob55~3QqA6}vvs3JR@Q{hpJ_~bp6#FHayB4JUYOBCTf z@o-Gm42NSO>H-2avtQ=afCK)X(@F|hlW`+i%#IqXw52$?guQ57uT^%@&Un)bBF?Nb z)vId2HqGYFp6-p%(FKHv5EWvQtR9a>s0A%Re>AEIrzUMs>}9aoBX%E9-mte6;|^s5 z=ok-3@xW+kn2rKvC+eE1Dtws0zRwl)ptAQCKeg1ei0*(XI*2fW5JKodSckA4VJ(80 zy;Rbt3q#kFOZ!=FX?5O4q;EpljIfk#D6L%+hpw0lploshGP^Ab$3%g`KBK`AArKsv z1RB^Y9M~VHA^Hdi9Y^Vzmr9$!P&;Qd zMz4{YMzTG5a(-8QXbj`mBg6z6l;ZTpd5)FWKWH^39xa_yY z&wh1ZDLKymd|v~xF;`=a>W3WL+_)4}9B$l3VE*;xPgrp!I)(5VgeMUg`*>3cNIuon z4w7#+tx;X#SV{BSWRj^{HkfrB7t{LqA+XQM`FJxE^&kld(ju8AAq|-pWfuFUk4n`uLd|5>WI>oNG zE+u8`R%<UN_JGL_@mynZ+xgPz9esMH=bAs32=WoiHTPGSuh*@EGWM>_3J)x@d58 zG#ncdGxOiUi7z939YOKd6Ue6>4oBd-q2GmpCyO3ud7XC4C7j8L#N&I%Mg#kTkuiZj z!`eEFz@OVXYgGT>W{!4h2!!G@tIJ6-exK;h`35fLFg@LrX0EPzQB8isTx$#2mwL*< zC&4x5nRL%XDsq`^*x-OLj%@IfH`z-Y{E%~Py{jgaExZ8Ilp%ZzK;~(DEEbaa;Yd6v z(InD?2zY>KH*)A8kqt}Xs6a6-WHyN=(O=`NCKNm%(zDpj!W0LgqNj28+sdo}Tl83D zHT;pa>fxwFFR*L99opX^%UR~?Ycs^Ki>DDhw7YLHIO1sEI%1u9weJUnoSZqnaXp!6 z<1V%2_1d@}=i7T5xGQbVeMZ$QEi3!VxSux@#5QZMMg0piL9|(W>(sxfC5R2yKE3*C zHbFF+`kegLQi52f?`u+DZQucE1*{E);~f^e znd>3z{sre!1J*naf0gFb`W>uu zO<{jC2yg>f@9t{b*Ln@O+}!mcmIZvSyO?0OeV;c6`Fxs2HA{fjRjabr4E%4ccj(L7 zW%z&}vl?&5WE073t~0Ya$nwhOey?mU;~XP8Cj`6de;3zoCzZENLzcDNf9lH_sK8}w^6vOV^xpg$5S~*xZw~J>4COkwo?wr<6SK^R zB~X1wLOPA}|3s)la3KV_2qY+@lAvT>isKqra02&&X;})&2Ia=KJ05{cL}n#SWK;(Z zqACP9;lPW_$@)>a*+oG>5=0}A7j8t*gGBa;kxtVH$cQoc){k&+=h@k-BYP)o#a}pd z;!w&~{kpCCSWntgJk^u1xQ}(G&9;=;oiMvo=E{V*@{BcUt~=IsJ-1+T-=h=LmC4*H z;5gBiuBbgT{#r%zlp)>HI;Bb3Jqf$#Om)(}{9QFy+47dfk*;ny-<+s!OI5E0A#oJ$;Fu?Jt`z_+BoWeQ@UocyAiHrQPJ6JZ@3pRKbbT z>9w!tSEVQHj#K(+)g+&Gl%485*?Y!teoNBPG?{&^uw?4M8-}Y`$>cWLLVj@%-*YZAYrMJ5k&HMr}{RUNxmpm*1NzZ%&jqUnso1<_fve`3nEz z4axFN;G5#olWpntj;XGcqbA{~IkPw6Sn;OAeXV%np6TIpp;Ym*MDen;yE5hONVq#L zYOZWgti3&p+!?h1W?_okJrTSwmGxuUASc(@Y#QmPa-{N(2vUQ%#;bDB9Ll|+ptfqlhg zXR{3!<}q8?@%!|qnA(p8P-#%gpdK~ial5Pe^X3RX^?Yv7+$~AW?4d4)X+9g9AemX2 zD_XS3rG~-o$y;%TzJdrrlm1YAh5?!m-3I z3)3&Pu=BM#!(4WSn=f0{V|9W6@6#gs@ti8nzhtgA;ASWM4izNp=VH}{kK_QacHRM5 zci3DB3M~b`EN_-q*P?QMXgg*9#jD%tyeceM)lfMsMc(XrVzwXahSH$I684Sw7IK!| zir3WXeNZKp%1bf@0LGb0OuR;uNf zv9cFNZQ0pgQ?14g?>F}PXxYS85TTHB;ME_Hb#VETXmCi{>LIdD7>8GOAikISkmmsM zz;6N(Aut-0cGE-37;mnk6pW6-74HO6G_hb*!1s7Wpxc1pDWO;44>e&zd<;2^bBPf3 zuvJt)vGDkGD-57e3J(Q_GnsV+#rNQZPKbrX{b6Z0+?q!tkXOeg<;f;%m8-r;k0C2w zypc{;|6h~&6QZa51W1cF0FH2JUC!rnAI*JDR|vh7&Xv%)UelGmqvcA= zSW?`k{i~MCDZb8nq~(i&Sm-#VJE@ztq+In0SAELWoNzTKT`Q9IR`#@X*zmTgVAkeI znyQYeS@GDDRhS+;e5U*;k4mM`DUQIJNa3Obzr1YR2;r8;mX9d!lh5MBqX-KRtJ64C zBBfM9C0rgbU$zDUx8o6%BpTi|1{Ew2P~L4yZp!>9@+lNYz start_idx: + runs_to_modify.append(run) + + if not runs_to_modify: + return + + first_run = runs_to_modify[0] + first_run_index = next(i for i, r in enumerate(runs) if r is first_run) + + local_start = start_idx - sum(len(runs[i].text) for i in range(first_run_index)) + + remaining_old = old_text + + for i, run in enumerate(runs_to_modify): + run_text = run.text + if i == 0: + prefix = run_text[:local_start] + remove_len = min(len(remaining_old), len(run_text) - local_start) + suffix = run_text[local_start + remove_len:] + run.text = prefix + suffix + remaining_old = remaining_old[remove_len:] + else: + remove_len = min(len(remaining_old), len(run_text)) + suffix = run_text[remove_len:] + run.text = suffix + remaining_old = remaining_old[remove_len:] + + first_run = runs_to_modify[0] + first_run.text = first_run.text[:local_start] + new_text + first_run.text[local_start:] + + replace_in_paragraph(para) + + for para in doc.paragraphs: + replace_in_paragraph(para) + + for table in doc.tables: + for row in table.rows: + for cell in row.cells: + for para in cell.paragraphs: + replace_in_paragraph(para) + + for section in doc.sections: + footer = section.footer + for para in footer.paragraphs: + if any("PAGE" in run._element.xml for run in para.runs): + continue + replace_in_paragraph(para) + + +def docx_to_pdf(input_path, output_dir=None): + """Converts a .docx file to .pdf using LibreOffice, handling non-zero exit codes gracefully.""" + if output_dir is None: + output_dir = os.path.dirname(os.path.abspath(input_path)) + + pdf_path = os.path.join(output_dir, os.path.basename(input_path).replace(".docx", ".pdf")) + + try: + result = subprocess.run( + [ + "libreoffice", + "--headless", + "--convert-to", + "pdf", + "--outdir", + output_dir, + input_path, + ], + timeout=60, + capture_output=True, + text=True, + ) + + if result.returncode != 0: + # Log the warning/error from LibreOffice + print(f"WARNING: libreoffice command returned non-zero exit code ({result.returncode}) for {input_path}.") + print(f" STDOUT: {result.stdout}") + print(f" STDERR: {result.stderr}") + + # Check if the PDF was created anyway + if not os.path.exists(pdf_path) or os.path.getsize(pdf_path) == 0: + # This is a real failure + raise Exception(f"PDF conversion failed and output file was not created. STDERR: {result.stderr}") + else: + print(f"INFO: PDF file was created successfully despite the non-zero exit code.") + + except FileNotFoundError: + print("ERROR: libreoffice command not found. Please ensure it is installed and in your PATH.") + raise + except Exception as e: + # Re-raise other exceptions (like timeout) + print(f"ERROR: An unexpected error occurred during PDF conversion for {input_path}. Error: {e}") + raise + + +def insert_image_after_keyword(doc, keywords, image_path, full_name, time): + """Finds a keyword in a table and inserts an image and text after it.""" + if not os.path.exists(image_path): + print(f"==INSERT IMAGE ERROR== File not found: {image_path}") + return + + try: + for table in doc.tables: + for row in table.rows: + for cell in row.cells: + for para in cell.paragraphs: + for keyword in keywords: + if keyword in para.text: + p_img = cell.add_paragraph() + p_img.alignment = WD_ALIGN_PARAGRAPH.CENTER + p_img.add_run().add_picture(image_path, width=Inches(1.5)) + + p_name = cell.add_paragraph() + p_name.alignment = WD_ALIGN_PARAGRAPH.CENTER + run_name = p_name.add_run(full_name) + run_name.bold = True + + p_time = cell.add_paragraph() + p_time.alignment = WD_ALIGN_PARAGRAPH.CENTER + p_time.add_run(time) + return + except Exception as e: + print(f"==INSERT IMAGE ERROR== {e}") + + +# ============================================================================= +# Document Generator Class +# ============================================================================= + + class DocumentGenerator: def __init__(self, document_code, context_pks: dict): self.document_code = document_code @@ -356,67 +532,153 @@ class DocumentGenerator: val = apply_format(val, cur_fmt, obj) return str(val) - def prepare_replacements(self): - # Set base date replacements + def _scan_placeholders_in_doc(self, doc): + """Scans the entire document and returns a set of unique placeholders.""" + placeholders = set() + pattern = re.compile(r'\[([^\[\]]+)\]') + + def scan_paragraph(para): + full_text = ''.join(run.text for run in para.runs) + for match in pattern.finditer(full_text): + placeholders.add(f"[{match.group(1)}]") + + for para in doc.paragraphs: + scan_paragraph(para) + + for table in doc.tables: + for row in table.rows: + for cell in row.cells: + for para in cell.paragraphs: + scan_paragraph(para) + + for section in doc.sections: + footer = section.footer + for para in footer.paragraphs: + if any("PAGE" in run._element.xml for run in para.runs): + continue + scan_paragraph(para) + + return placeholders + + def _parse_format_args(self, args_string): + """Parses a string like 'lang:vi, type:number_to_words' into a dictionary.""" + if not args_string: + return {} + format_config = {} + args = args_string.split(',') + for arg in args: + if ':' in arg: + key, value = arg.split(':', 1) + format_config[key.strip()] = value.strip() + return format_config + + def prepare_replacements(self, doc): + """ + Prepares all replacements by implementing a hybrid approach: + 1. Prioritizes manual configuration from 'fields'. + 2. Automatically handles any remaining dynamic placeholders. + """ today = datetime.now() self.replacements['[day]'] = str(today.day) self.replacements['[month]'] = str(today.month) self.replacements['[year]'] = str(today.year) self.replacements['[date]'] = today.strftime("%d/%m/%Y") - for mapping in self.config.mappings: - alias = mapping["alias"] - data = self.data_context.get(alias) + placeholders_in_doc = self._scan_placeholders_in_doc(doc) - if mapping["type"] == "object": - if data is None: - for placeholder in mapping["fields"]: - self.replacements[placeholder] = "" + # PASS 1: Handle manual/explicit configuration (backward compatibility) + if isinstance(self.config.mappings, list): + for mapping in self.config.mappings: + if "fields" not in mapping: continue - for placeholder, config in mapping["fields"].items(): - if isinstance(config, dict): - value = self._get_value_from_object(data, config["source"]) - self.replacements[placeholder] = self._format_value(value, config["format"], data) - else: - value = self._get_value_from_object(data, config) - self.replacements[placeholder] = str(value) if value is not None else "" - elif mapping["type"] == "list": - items = data or [] - max_items = mapping.get("max_items", 4) - for i in range(max_items): - item = items[i] if i < len(items) else None - for p_template, config in mapping["fields"].items(): - placeholder = p_template.replace("{index}", str(i + 1)) - if item is None: - self.replacements[placeholder] = "" - continue - if isinstance(config, dict): - value = self._get_value_from_object(item, config["source"]) - self.replacements[placeholder] = self._format_value(value, config["format"], item) - else: - value = self._get_value_from_object(item, config) - self.replacements[placeholder] = str(value) if value is not None else "" + alias = mapping["alias"] + data = self.data_context.get(alias) + + if mapping["type"] == "list": + items = data or [] + max_items = mapping.get("max_items", 4) + for i in range(max_items): + item = items[i] if i < len(items) else None + for p_template, config in mapping["fields"].items(): + placeholder = p_template.replace("{index}", str(i + 1)) + if placeholder in placeholders_in_doc: + if item is None: + self.replacements[placeholder] = "" + else: + if isinstance(config, dict): + value = self._get_value_from_object(item, config["source"]) + self.replacements[placeholder] = self._format_value(value, config["format"], item) + else: + value = self._get_value_from_object(item, config) + self.replacements[placeholder] = str(value) if value is not None else "" + placeholders_in_doc.discard(placeholder) + + elif mapping["type"] == "object": + if data is None: + for placeholder in mapping["fields"]: + if placeholder in placeholders_in_doc: + self.replacements[placeholder] = "" + placeholders_in_doc.discard(placeholder) + continue + + for placeholder, config in mapping["fields"].items(): + if placeholder in placeholders_in_doc: + if isinstance(config, dict): + value = self._get_value_from_object(data, config["source"]) + self.replacements[placeholder] = self._format_value(value, config["format"], data) + else: + value = self._get_value_from_object(data, config) + self.replacements[placeholder] = str(value) if value is not None else "" + placeholders_in_doc.discard(placeholder) + + # PASS 2: Handle remaining dynamic placeholders + dynamic_pattern = re.compile(r'\[([a-zA-Z0-9_]+\.[a-zA-Z0-9_.]*)(?:\((.*?)\))?\]') + + for placeholder in list(placeholders_in_doc): + match = dynamic_pattern.fullmatch(placeholder) + if not match: + continue + + data_path, format_args_str = match.groups() + + try: + alias, field_path = data_path.split('.', 1) + + if alias not in self.data_context: + self.replacements[placeholder] = f"[ALIAS_NOT_FOUND: {alias}]" + continue + + source_object = self.data_context.get(alias) + value = self._get_value_from_object(source_object, field_path) + + if format_args_str: + format_config = self._parse_format_args(format_args_str) + self.replacements[placeholder] = self._format_value(value, format_config, source_object) + else: + self.replacements[placeholder] = str(value) if value is not None else "" + + except Exception as e: + self.replacements[placeholder] = f"[ERROR: {e}]" def generate(self, signature_info=None, output_filename=None): self.fetch_data() - self.prepare_replacements() - # Remove leading slashes from template_path to prevent os.path.join issues clean_template_path = self.config.template_path.lstrip('/') template_full_path = os.path.join(static_folder, clean_template_path) if not os.path.exists(template_full_path): raise FileNotFoundError(f"Template file not found at: {template_full_path}") - # --- FILENAME LOGIC --- + doc = Document(template_full_path) + + self.prepare_replacements(doc) + if output_filename: - # Use user-provided filename, ensure it has the correct extension if not output_filename.endswith(".docx"): base_name = os.path.splitext(output_filename)[0] output_filename = f"{base_name}.docx" else: - # Use a more descriptive output filename (original logic) - pk_values = "_".join(self.context_pks.values()) + pk_values = "_".join(str(v) for v in self.context_pks.values()) output_filename = f"{self.document_code}_{pk_values}_{int(datetime.now().timestamp())}.docx" output_dir = os.path.join(static_folder, "contract") @@ -424,8 +686,6 @@ class DocumentGenerator: output_path = os.path.join(output_dir, output_filename) pdf_filename = output_filename.replace(".docx", ".pdf") - doc = Document(template_full_path) - for old_text, new_text in self.replacements.items(): replace_text_in_doc(doc, old_text, new_text) diff --git a/static/contract/1. Phiếu xác lập thỏa thuận ca nhan.docx b/static/contract/1. Phiếu xác lập thỏa thuận ca nhan.docx new file mode 100644 index 0000000000000000000000000000000000000000..9c06f3d031471057889f9474ee1849f4783cbe5d GIT binary patch literal 16742 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 literal 0 HcmV?d00001