diff --git a/child_compassion/models/compassion_hold.py b/child_compassion/models/compassion_hold.py index 79b27c82c..efbb1d702 100644 --- a/child_compassion/models/compassion_hold.py +++ b/child_compassion/models/compassion_hold.py @@ -486,24 +486,28 @@ def postpone_no_money_hold(self, additional_text=None): } hold.write(hold_vals) - body = ( - "The no money hold for child {local_id} was expiring on " - "{old_expiration} and was extended to {new_expiration} " - "({extension_description} extension).{additional_text}" - ) - hold.message_post( - body=_(body.format(**values)), - subject=_("No money hold extension"), - subtype_xmlid="mail.mt_comment", - ) - - else: - body = _( - "The no money hold for child {local_id} is expiring on " - "{old_expiration} and will not be extended since " - "no sponsorship exists for this child." - ) - hold.message_post(body=body.format(**values)) + # ---------------------------------------------------- + # NOTIFICATION (Only for NO_MONEY_HOLD) + # ---------------------------------------------------- + if hold.type == HoldType.NO_MONEY_HOLD.value: + if hold.child_id.sponsor_id: + body = ( + "The no money hold for child {local_id} was expiring on " + "{old_expiration} and was extended to {new_expiration} " + "({extension_description} extension).{additional_text}" + ) + hold.message_post( + body=_(body.format(**values)), + subject=_("No money hold extension"), + subtype_xmlid="mail.mt_comment", + ) + else: + body = _( + "The no money hold for child {local_id} is expiring on " + "{old_expiration} and will not be extended since " + "no sponsorship exists for this child." + ) + hold.message_post(body=body.format(**values)) ########################################################################## # Mapping METHOD # diff --git a/partner_communication_compassion/__manifest__.py b/partner_communication_compassion/__manifest__.py index fcf26d0a4..a28824250 100644 --- a/partner_communication_compassion/__manifest__.py +++ b/partner_communication_compassion/__manifest__.py @@ -29,7 +29,7 @@ # pylint: disable=C8101 { "name": "Compassion Partner Communications", - "version": "14.0.1.1.0", + "version": "14.0.1.1.1", "category": "Other", "author": "Compassion CH", "license": "AGPL-3", @@ -49,6 +49,7 @@ "data/depart_communications.xml", "data/communication_config.xml", "data/utm_data.xml", + "data/ir_cron_data.xml", "views/communication_job_view.xml", "views/disaster_alert_view.xml", "views/partner_compassion_view.xml", diff --git a/partner_communication_compassion/data/communication_config.xml b/partner_communication_compassion/data/communication_config.xml index 6ed7e455d..5486d918f 100644 --- a/partner_communication_compassion/data/communication_config.xml +++ b/partner_communication_compassion/data/communication_config.xml @@ -256,7 +256,7 @@ - Project Suspension + Project Suspension Announcement True - - Project Suspension Extension 1 - - - digital - True - - - - Project Suspension Extension 2 - + + Project Suspension Follow Up - digital - True - - - - Project Reactivation - + + + + Project: 2-Month Suspension Annoucement/Follow-Up + + code + model._cron_suspension_communication() + + 1 + days + -1 + + + + + diff --git a/partner_communication_compassion/data/project_lifecycle_emails.xml b/partner_communication_compassion/data/project_lifecycle_emails.xml index b534559ca..ed452294c 100644 --- a/partner_communication_compassion/data/project_lifecycle_emails.xml +++ b/partner_communication_compassion/data/project_lifecycle_emails.xml @@ -73,134 +73,6 @@ - - Project Suspension Extension 1 - - ${object.partner_id and object.partner_id.email and object.partner_id.id or False } - Extension of the project suspension - "${object.user_id.company_id.name}" <${object.user_id.company_id.email}> - -
- % set child = object.get_objects() - % set project = child.mapped('project_id')[0] - % set suspension = project.lifecycle_ids[0] - % set reasons = suspension.suspension_reason_ids - % set details = suspension.suspension_detail -
-

- ${object.partner_id.salutation}, -
-
- We informed you some time ago about the project suspension of the center ${project.fcp_id}. -
-
- We received the information that the suspension was extended for 3 months - % if suspension.extension_1_reason_ids: - because of ${suspension.get_list('extension_1_reason_ids.value')}. - % else: - in order to put back the project on a sustainable process. - % endif -
-
- You will receive updated information as soon as we get them. -
-
- Yours sincerely -

-
-
- - - Project Suspension Extension 2 - - ${object.partner_id and object.partner_id.email and object.partner_id.id or False } - Extension of the project suspension - "${object.user_id.company_id.name}" <${object.user_id.company_id.email}> - -
- % set child = object.get_objects() - % set project = child.mapped('project_id')[0] - % set suspension = project.lifecycle_ids[0] - % set reasons = suspension.suspension_reason_ids - % set details = suspension.suspension_detail -
-

- ${object.partner_id.salutation}, -
-
- We informed you some time ago about the extension of the project suspension ${project.fcp_id}. -
-
- We received the information that the suspension was again extended for 3 months - % if suspension.extension_2_reason_ids: - because of ${suspension.get_list('extension_2_reason_ids.value')}. - % else: - in order to put back the project on a sustainable process. - % endif - This is the last chance for the center to correct their processes or the project will be closed. -
-
- You will receive updated information as soon as we get them. -
-
- Yours sincerely -

-
-
- - - Project Reactivation - - ${object.partner_id and object.partner_id.email and object.partner_id.id or False } - Project Reactivation - "${object.user_id.company_id.name}" <${object.user_id.company_id.email}> - -
- % set child = object.get_objects() - % set project = child.mapped('project_id')[0] - % set reactivation = project.lifecycle_ids[0] - % set details = reactivation.fcp_improvement_desc -
-

- ${object.partner_id.salutation}, -
-
- A few time ago the project ${project.fcp_id} where ${child.get('your sponsored child')} ${child.get('is')} registered was on suspension. - We are happy to inform you that the problem is resolved and that the project is again sane and active. - The project will do regular tuitions and spiritual activites to keep the situation healthy. -
-
- % if details: - ${details} -
-
- % endif - Yours sincerely -

-
-
- Project Transition diff --git a/partner_communication_compassion/migrations/14.0.1.1.1/pre-migration.py b/partner_communication_compassion/migrations/14.0.1.1.1/pre-migration.py new file mode 100644 index 000000000..3dee5edd2 --- /dev/null +++ b/partner_communication_compassion/migrations/14.0.1.1.1/pre-migration.py @@ -0,0 +1,42 @@ +from openupgradelib import openupgrade + +from odoo import SUPERUSER_ID, api + + +def migrate(cr, version): + env = api.Environment(cr, SUPERUSER_ID, {}) + + # List of XML IDs to be removed + obsolete_xml_ids = [ + "project_suspension_e1", + "project_suspension_e2", + "project_reactivation", + ] + + for xml_id in obsolete_xml_ids: + # ref() finds the database record using the XML ID + record = env.ref( + f"partner_communication_compassion.{xml_id}", raise_if_not_found=False + ) + + if record: + # 1. Find and delete all communication logs linked to this config + logs = env["partner.communication.job"].search( + [("config_id", "=", record.id)] + ) + if logs: + logs.unlink() + + # 2. Delete the config record itself + record.unlink() + + # Define XML IDs for the communication configs "FCP Suspension Follow up" + cr = env.cr + openupgrade.add_xmlid( + cr, + "partner_communication_compassion", + "email_project_suspension_follow_up", + "mail.template", + 292, + noupdate=False, + ) diff --git a/partner_communication_compassion/models/compassion_project.py b/partner_communication_compassion/models/compassion_project.py index 6a99a102a..ef4203a8f 100644 --- a/partner_communication_compassion/models/compassion_project.py +++ b/partner_communication_compassion/models/compassion_project.py @@ -1,4 +1,6 @@ -from odoo import fields, models +from dateutil.relativedelta import relativedelta + +from odoo import api, fields, models class CompassionProject(models.Model): @@ -10,3 +12,79 @@ class CompassionProject(models.Model): "communication.snippet", string="Caption to use for pictures, or prayer shared by fcp, or else", ) + + last_suspension_communication_date = fields.Date( + string="Date of the last suspension communication", + help="Records the date of the last suspension communication.", + ) + + def reactivate_project(self): + """ + Inherit reactivation to trigger 'FCP Suspension Follow Up' + """ + # 1. Execute original logic + res = super(CompassionProject, self).reactivate_project() + + # 2. Trigger the communication + self._trigger_communication("project_suspension_follow_up") + + return res + + def _trigger_communication(self, config_xml_id): + """ + Finds all active sponsors for children in the project and + sends communication to the sponsor. + """ + # 1. Get the communication configuration record + comm_config = self.env.ref(f"partner_communication_compassion.{config_xml_id}") + + for project in self: + # 2. Find all active sponsorships for children in this project + contracts = self.env["recurring.contract"].search( + [("child_id.project_id", "=", project.id), ("state", "=", "active")] + ) + + # 3. Send communication to each sponsor + for contract in contracts: + contract.with_context( + default_object_ids=contract.child_id.id + ).send_communication(communication=comm_config, correspondent=True) + + # 4. Update the last suspension communication date + project.last_suspension_communication_date = fields.Date.today() + + @api.model + def _cron_suspension_communication(self): + """ + Finds all projects suspended for more than 2 months without follow-up + communication and triggers the appropriate communication. + """ + today = fields.Date.today() + two_months_ago = today - relativedelta(months=2) + + # Find projects suspended for > 2 months + # AND (no follow-up yet OR last follow-up was > 2 months ago) + projects = self.search( + [ + ("suspension", "in", ["suspended", "fund-suspended"]), + ("last_lifecycle_id.type", "=", "Suspension"), + ("last_lifecycle_id.date", "<=", two_months_ago), + "|", + ("last_suspension_communication_date", "=", False), + ("last_suspension_communication_date", "<=", two_months_ago), + ] + ) + + for project in projects: + last_lifecycle_date = project.last_lifecycle_id.date + + if ( + not project.last_suspension_communication_date + or project.last_suspension_communication_date < last_lifecycle_date + ): + # 1. First time for this lifecycle event: Announcement + project._trigger_communication("project_suspension") + + else: + # 2. We already announced it, but 2 months have passed: Follow-up + project._trigger_communication("project_suspension_follow_up") diff --git a/sbc_compassion/models/correspondence_s2b_generator.py b/sbc_compassion/models/correspondence_s2b_generator.py index bb7dcbc51..aec2f9b44 100644 --- a/sbc_compassion/models/correspondence_s2b_generator.py +++ b/sbc_compassion/models/correspondence_s2b_generator.py @@ -80,6 +80,22 @@ class CorrespondenceS2bGenerator(models.Model): preview_pdf = fields.Binary(readonly=True) filename = fields.Char(compute="_compute_filename") month = fields.Selection("_get_months") + generation_status = fields.Selection( + [ + ("creating_task", "creating_task"), + ("apply_template", "apply_template"), + ("apply_text", "apply_text"), + ("apply_images", "apply_images"), + ("generate_pdf", "generate_pdf"), + ("done", "done"), + ("failed", "failed"), + ("finalizing", "finalizing"), + ], + default="creating_task", + string="Generation Status", + ) + generation_error_message = fields.Text(string="Generation Message") + MAX_PAGE_COUNT = 15 # Maximum number of pages allowed in a letter def _compute_nb_letters(self): for generator in self: @@ -130,50 +146,63 @@ def onchange_month(self): def preview(self): """Generate a picture for preview.""" - pdf = self._get_pdf(self.sponsorship_ids[:1])[0] - if self.template_id.layout == "CH-A-3S01-1": - # Read page 2 - in_pdf = PdfFileReader(BytesIO(pdf)) - output_pdf = PdfFileWriter() - out_data = BytesIO() - output_pdf.addPage(in_pdf.getPage(1)) - output_pdf.write(out_data) - out_data.seek(0) - pdf = out_data.read() - try: + pdf = self._get_pdf(self.sponsorship_ids[:1])[0] + + if self.template_id.layout == "CH-A-3S01-1": + in_pdf = PdfFileReader(BytesIO(pdf)) + output_pdf = PdfFileWriter() + output_pdf.addPage(in_pdf.getPage(1)) + out_data = BytesIO() + output_pdf.write(out_data) + pdf = out_data.getvalue() + + n_pages = PdfFileReader(BytesIO(pdf)).getNumPages() + if n_pages > self.MAX_PAGE_COUNT: + msg = _("Oops your letter has %d pages. The limit is %d.") % ( + n_pages, + self.MAX_PAGE_COUNT, + ) + + raise UserError(msg) with Image(blob=pdf, resolution=96) as pdf_image: preview = base64.b64encode(pdf_image.make_blob(format="jpeg")) - except PolicyError as error: - _logger.error( - "ImageMagick policy error. Please add following line to " - "/etc/Image-Magick-/policy.xml: " - '', - ) - raise UserError( - _( - "Please allow ImageMagick to write PDF files." - " Ask an IT admin for help." + + return self.isolated_write( + { + "state": "preview", + "generation_status": "done", + "generation_error_message": False, + "preview_image": preview, + "preview_pdf": base64.b64encode(pdf), + } ) - ) from error - except TypeError as error: - raise UserError( + + except (PolicyError, TypeError, UserError, Exception) as error: + error_message = ( _( + "Unfortunately the server cannot generate PDF documents " + "at the moment. Our IT team is informed and will fix this issue " + "as soon as possible." + ) + if isinstance(error, PolicyError) + else str(error) + if isinstance(error, UserError) + else _( "There was an error while generating the PDF of the letter. " "Please check FPDF logs for more information." ) - ) from error - - pdf_image = base64.b64encode(pdf) - - return self.write( - { - "state": "preview", - "preview_image": preview, - "preview_pdf": pdf_image, - } - ) + ) + _logger.error("Unable to generate PDF", exc_info=True) + if self.env.context.get("raise_error"): + raise UserError(error_message) from error + self.env.cr.rollback() + self.isolated_write( + { + "generation_status": "failed", + "generation_error_message": error_message, + } + ) def edit(self): """Generate a picture for preview.""" @@ -184,6 +213,7 @@ def generate_letters(self): Launch S2B Creation job :return: True """ + self.generation_status = "finalizing" self.with_delay( identity_key="s2b_generator." + str(self.ids) ).generate_letters_job() @@ -217,7 +247,7 @@ def generate_letters_job(self): "res_model": letters._name, }, ) - for atchmt in self.image_ids + for atchmt in self.image_ids.sorted(reverse=True) ] letters += letters.create(vals) @@ -227,11 +257,26 @@ def generate_letters_job(self): # If the operation succeeds, notify the user message = "Letters have been successfully generated." self.env.user.notify_success(message=message) - return self.write({"state": "done", "date": fields.Datetime.now()}) + + return self.isolated_write( + { + "state": "done", + "date": fields.Datetime.now(), + "generation_status": "done", + "generation_error_message": False, + } + ) except Exception as error: # If the operation fails, notify the user with the error message + self.env.cr.rollback() error_message = str(error) + self.isolated_write( + { + "generation_status": "failed", + "generation_error_message": error_message, + } + ) _logger.error(error_message, exc_info=True) self.env.user.notify_danger(message=error_message) @@ -257,8 +302,8 @@ def _get_text(self, sponsorship): keywords = { "%child%": child.preferred_name, "%age%": str(child.age), - "%firstname%": sponsor.firstname or sponsor.name, - "%lastname%": sponsor.firstname and sponsor.lastname or "", + "%firstname%": sponsor.preferred_name or sponsor.firstname or sponsor.name, + "%lastname%": sponsor.lastname or "", } text = self.body for keyword, replacement in list(keywords.items()): @@ -285,7 +330,19 @@ def _get_pdf(self, sponsorship): sponsorship.display_name, (header, ""), # Headers (front/back) {"Original": [text]}, # Text - self.mapped("image_ids.datas"), # Images + self.mapped("image_ids").sorted(reverse=True).mapped("datas"), # Images + s2b_generator=self, ), text, ) + + def isolated_write(self, vals): + """Use a separate transaction to update the letter_generator.""" + if len(self) != 1: + return False + + with self.env.registry.cursor() as new_cr: + new_env = self.env(cr=new_cr) + new_s2b_generator = new_env[self._name].browse(self.id) + new_s2b_generator.write(vals) + new_cr.commit() diff --git a/sbc_compassion/models/correspondence_template.py b/sbc_compassion/models/correspondence_template.py index 7e09255cd..167ceb58a 100644 --- a/sbc_compassion/models/correspondence_template.py +++ b/sbc_compassion/models/correspondence_template.py @@ -112,13 +112,24 @@ def _compute_template_image(self): ########################################################################## # PUBLIC METHODS # ########################################################################## - def generate_pdf(self, pdf_name, header, text, image_data, background_list=None): + # ruff: noqa: C901 (Yes this function is complex... it will be removed in v17) + def generate_pdf( + self, + pdf_name, + header, + text, + image_data, + background_list=None, + s2b_generator=None, + ): """ Generate a pdf file This function is nearly as generic as it should be to be implemented directly to generate PDF for any template, text and image We save every text to a temp txt file to avoid having to escape all characters that could potentially be problematic + :param s2b_generator: a s2b.generator record to update + the generation status :param pdf_name: path and name of the pdf file to write on :param header: tuple of text for the headers to display (first value is for front pages, second for back pages) @@ -130,129 +141,170 @@ def generate_pdf(self, pdf_name, header, text, image_data, background_list=None) the template. """ self.ensure_one() + if s2b_generator is None: + s2b_generator = self.env["correspondence.s2b.generator"] - # Images stored on disk for FPDF processing. We keep them in these lists - # to make sure we properly remove the files at the end of the process. - temp_img = [] - - if background_list is None: - background_list = [] - overflow_template = False - - pages = self.mapped("page_ids") - self.additional_page_id - template_list, header_data, image_boxes = self._generate_template_list( - pages, header, background_list, temp_img - ) - image_list = [] - - if background_list: - # An original document is provided. We want - # to complete the PDF document with the remaining pages - # and provide an overflow template in case the text is longer - # and we should add pages for additional translation. - if len(background_list) > len(pages): - for i in range(len(pages), len(background_list)): + # Keep file objects alive to prevent auto-deletion + temp_files = [] + std_err_file = None + pdf_file = None + + try: + if background_list is None: + background_list = [] + overflow_template = False + + pages = self.mapped("page_ids") - self.additional_page_id + template_list, header_data, image_boxes = self._generate_template_list( + pages, header, background_list, temp_files + ) + image_list = [] + + s2b_generator.isolated_write({"generation_status": "apply_template"}) + + if background_list: + # An original document is provided. We want + # to complete the PDF document with the remaining pages + # and provide an overflow template in case the text is longer + # and we should add pages for additional translation. + if len(background_list) > len(pages): + for i in range(len(pages), len(background_list)): + bf = tempfile.NamedTemporaryFile(prefix="img_", suffix=".jpg") + bf.write(base64.b64decode(background_list[i])) + bf.flush() + temp_files.append(bf) + template_list.append([bf.name, [], [], []]) + additional_page = self.env.ref("sbc_compassion.b2s_additional_page") + bf_name = False + if additional_page.background: bf = tempfile.NamedTemporaryFile(prefix="img_", suffix=".jpg") - bf.write(base64.b64decode(background_list[i])) + bf.write(base64.b64decode(additional_page.background)) bf.flush() - temp_img.append(bf) - template_list.append([bf.name, [], [], []]) - additional_page = self.env.ref("sbc_compassion.b2s_additional_page") - bf_name = False - if additional_page.background: - bf = tempfile.NamedTemporaryFile(prefix="img_", suffix=".jpg") - bf.write(base64.b64decode(additional_page.background)) - bf.flush() - bf_name = bf.name - temp_img.append(bf) - text_list = [] - for text_box in additional_page.text_box_ids: - text_list.append(text_box.get_json_repr()) - overflow_template = [bf_name, [], text_list, image_boxes] - elif self.additional_page_id: - # We are generating a new PDF (S2B case). We provide - # an overflow template using the template - add_background = tempfile.NamedTemporaryFile(prefix="img_", suffix=".jpg") - add_background.write(base64.b64decode(self.additional_page_id.background)) - add_background.flush() - temp_img.append(add_background) + bf_name = bf.name + temp_files.append(bf) + text_list = [] + for text_box in additional_page.text_box_ids: + text_list.append(text_box.get_json_repr()) + overflow_template = [bf_name, [], text_list, image_boxes] + elif self.additional_page_id: + # We are generating a new PDF (S2B case). We provide + # an overflow template using the template + add_background = tempfile.NamedTemporaryFile( + prefix="img_", suffix=".jpg" + ) + add_background.write( + base64.b64decode(self.additional_page_id.background) + ) + add_background.flush() + temp_files.append(add_background) + text_list = [] + for text_box in self.additional_page_id.text_box_ids: + text_list.append(text_box.get_json_repr()) + overflow_template = [add_background.name, header_data, text_list, []] + + s2b_generator.isolated_write({"generation_status": "apply_text"}) + text_list = [] - for text_box in self.additional_page_id.text_box_ids: - text_list.append(text_box.get_json_repr()) - overflow_template = [add_background.name, header_data, text_list, []] + for t_type, t_boxes in list(text.items()): + for txt in t_boxes: + txt_file = tempfile.NamedTemporaryFile( + "w", prefix=t_type + "_", suffix=".txt", encoding="utf-8" + ) + txt_file.write(txt) + txt_file.flush() + temp_files.append(txt_file) + text_list.append([txt_file.name, t_type]) - text_list = [] - for t_type, t_boxes in list(text.items()): - for txt in t_boxes: - txt_file = tempfile.NamedTemporaryFile( - "w", prefix=t_type + "_", suffix=".txt", encoding="utf-8" - ) - txt_file.write(txt) - txt_file.flush() - temp_img.append(txt_file) - text_list.append([txt_file.name, t_type]) - - for image in image_data: - ifile = tempfile.NamedTemporaryFile(prefix="img_", suffix=".jpg") - ifile.write(base64.b64decode(image)) - ifile.flush() - image_list.append(ifile.name) - temp_img.append(ifile) - - generated_json = { - "images": image_list, - "templates": template_list, - "texts": text_list, - # The output should at least contain 2 pages - "original_size": max(2, len(background_list)), - "overflow_template": overflow_template, - "lang": self.env.lang, - "prevent_overflow": self.type == "b2s", - } - - json_val = json.dumps(generated_json).replace(" ", "") - - std_err_file_path = self.path_to("stderr.txt") - std_err_file = open(std_err_file_path, "w", encoding="utf-8") - - php_command_args = ["php", self.path_to("pdf.php"), pdf_name, json_val] - if config.get("php_debug"): - # Allow php debugging with Xend - os.environ["XDEBUG_CONFIG"] = "PHPSTORM" - php_command_args.extend( - [ - "-dxdebug.remote_enable=1", - "-dxdebug.remote_mode=req", - "-dxdebug.remote_port=9000", - "-dxdebug.remote_host=127.0.0.1", - ] - ) - proc = subprocess.Popen(php_command_args, stderr=std_err_file) - proc.communicate() - - if proc.returncode != 0: - with open(std_err_file_path, "r", encoding="utf-8") as stderr: - _logger.error( - "FPDF returned nonzero exit code %d. stderr:\n%s", - proc.returncode, - stderr.read(), + s2b_generator.isolated_write({"generation_status": "apply_images"}) + + for image in image_data: + ifile = tempfile.NamedTemporaryFile(prefix="img_", suffix=".jpg") + ifile.write(base64.b64decode(image)) + ifile.flush() + image_list.append(ifile.name) + temp_files.append(ifile) + + generated_json = { + "images": image_list, + "templates": template_list, + "texts": text_list, + # The output should at least contain 2 pages + "original_size": max(2, len(background_list)), + "overflow_template": overflow_template, + "lang": self.env.lang, + "prevent_overflow": self.type == "b2s", + } + + json_val = json.dumps(generated_json).replace(" ", "") + + std_err_file_path = self.path_to("stderr.txt") + std_err_file = open(std_err_file_path, "w", encoding="utf-8") + + s2b_generator.isolated_write({"generation_status": "generate_pdf"}) + + php_command_args = ["php", self.path_to("pdf.php"), pdf_name, json_val] + if config.get("php_debug"): + # Allow php debugging with Xend + os.environ["XDEBUG_CONFIG"] = "PHPSTORM" + php_command_args.extend( + [ + "-dxdebug.remote_enable=1", + "-dxdebug.remote_mode=req", + "-dxdebug.remote_port=9000", + "-dxdebug.remote_host=127.0.0.1", + ] ) + proc = subprocess.Popen(php_command_args, stderr=std_err_file) + proc.communicate() - # Clean temp files - for img in temp_img: - img.close() - std_err_file.close() + if proc.returncode != 0: + with open(std_err_file_path, "r", encoding="utf-8") as stderr: + _logger.error( + "FPDF returned nonzero exit code %d. stderr:\n%s", + proc.returncode, + stderr.read(), + ) - # Read and return output - try: + # Read and return output pdf_file = open(pdf_name, "rb") res = pdf_file.read() pdf_file.close() - os.remove(pdf_file.name) - except FileNotFoundError: - _logger.error("Cannot read PDF made by FPDF.") - res = False - return res + pdf_file = None + os.remove(pdf_name) + + return res + + except Exception: + _logger.error("Cannot read PDF made by FPDF.", exc_info=True) + return False + + finally: + # Clean up all temporary files (they will be auto-deleted when closed) + for temp_file in temp_files: + try: + temp_file.close() + except Exception as e: + _logger.warning("Failed to close temp file: %s", str(e)) + + # Clean up stderr file + if std_err_file: + try: + std_err_file.close() + except Exception: + pass + try: + std_err_file_path = self.path_to("stderr.txt") + if os.path.exists(std_err_file_path): + os.remove(std_err_file_path) + except Exception as e: + _logger.warning("Failed to clean up stderr file: %s", str(e)) + + # Clean up PDF file if still open + if pdf_file: + try: + pdf_file.close() + except Exception: + pass # path of the FPDF folder _absolute_path = os.path.join( diff --git a/sbc_compassion/views/correspondence_s2b_generator_view.xml b/sbc_compassion/views/correspondence_s2b_generator_view.xml index 57985cf03..649a0fb16 100644 --- a/sbc_compassion/views/correspondence_s2b_generator_view.xml +++ b/sbc_compassion/views/correspondence_s2b_generator_view.xml @@ -25,6 +25,7 @@ icon="fa-search-plus" class="oe_stat_button" states="draft" + context="{'raise_error': True}" />