From 65539d44b8d6f6f0b0308e89c697a2b70ae62a6e Mon Sep 17 00:00:00 2001 From: Mihir Kandoi Date: Mon, 29 Jun 2026 21:33:15 +0530 Subject: [PATCH] fix(regional): survive a failed invoice during Import Supplier Invoice on Postgres create_purchase_invoice caught its own failure and then ran frappe.db.set_value + log_error in the SAME transaction. On Postgres a failed insert/save aborts the whole transaction, so the error-marking died with InFailedSqlTransaction and the failure cascaded through prepare_data_for_import's per-file loop, killing the entire import; MariaDB recovers per-statement and continues. Let create_purchase_invoice raise, and wrap each call in prepare_data_for_import in frappe.db.savepoint + rollback(save_point=...). On failure the savepoint rollback un-poisons the transaction, the error is logged, and the per-file status is set to Error and committed (self.db_set(commit=True), matching the existing process_file_data status commit) so an interrupted import durably reflects Error instead of staying at the already-committed Processing File Data; the loop then continues to the next file. The savepoint is taken after create_supplier/create_address so those are preserved exactly as before. Behaviour change (MariaDB): a failed invoice's partially-created draft Purchase Invoice is now rolled back on BOTH engines instead of being left as an orphan draft on MariaDB. Deliberate and more correct - a failed import should not leave a partial invoice; release-note worthy. --- .../import_supplier_invoice.py | 96 ++++++++++--------- 1 file changed, 51 insertions(+), 45 deletions(-) diff --git a/erpnext/regional/doctype/import_supplier_invoice/import_supplier_invoice.py b/erpnext/regional/doctype/import_supplier_invoice/import_supplier_invoice.py index b9748750b54..dbcaa2ef2a7 100644 --- a/erpnext/regional/doctype/import_supplier_invoice/import_supplier_invoice.py +++ b/erpnext/regional/doctype/import_supplier_invoice/import_supplier_invoice.py @@ -95,20 +95,31 @@ class ImportSupplierInvoice(Document): supplier_name = create_supplier(self.supplier_group, supp_dict) create_address(supplier_name, supp_dict) - pi_name = create_purchase_invoice(supplier_name, file_name, invoices_args, self.name) - self.file_count += 1 - if pi_name: - self.purchase_invoices_count += 1 - file_doc = frappe.new_doc("File") - file_doc.file_name = file_name - file_doc.attached_to_doctype = "Purchase Invoice" - file_doc.attached_to_name = pi_name - file_doc.content = encoded_content - file_doc.decode = False - file_doc.is_private = False - file_doc.insert(ignore_permissions=True) + frappe.db.savepoint("import_invoice") + try: + pi_name = create_purchase_invoice(supplier_name, file_name, invoices_args, self.name) + except Exception: + frappe.db.rollback(save_point="import_invoice") + frappe.log_error( + "Unable to create Purchase Invoice", + reference_doctype=self.doctype, + reference_name=self.name, + ) + self.db_set("status", "Error", commit=True) + continue + + self.purchase_invoices_count += 1 + + file_doc = frappe.new_doc("File") + file_doc.file_name = file_name + file_doc.attached_to_doctype = "Purchase Invoice" + file_doc.attached_to_name = pi_name + file_doc.content = encoded_content + file_doc.decode = False + file_doc.is_private = False + file_doc.insert(ignore_permissions=True) def prepare_items_for_invoice(self, file_content, invoices_args): qty = 1 @@ -374,41 +385,36 @@ def create_purchase_invoice(supplier_name, file_name, args, name): } ) - try: - pi.set_missing_values() - pi.insert(ignore_mandatory=True) + pi.set_missing_values() + pi.insert(ignore_mandatory=True) - # if discount exists in file, apply any discount on grand total - if args.total_discount > 0: - pi.apply_discount_on = "Grand Total" - pi.discount_amount = args.total_discount - pi.save() - # adjust payment amount to match with grand total calculated - calc_total = 0 - adj = 0 - for term in args.terms: - calc_total += flt(term["payment_amount"]) - if flt(calc_total - flt(pi.grand_total)) != 0: - adj = calc_total - flt(pi.grand_total) - pi.payment_schedule = [] - for term in args.terms: - pi.append( - "payment_schedule", - { - "mode_of_payment_code": term["mode_of_payment_code"], - "bank_account_iban": term["bank_account_iban"], - "due_date": term["due_date"], - "payment_amount": flt(term["payment_amount"]) - adj, - }, - ) - adj = 0 - pi.imported_grand_total = calc_total + # if discount exists in file, apply any discount on grand total + if args.total_discount > 0: + pi.apply_discount_on = "Grand Total" + pi.discount_amount = args.total_discount pi.save() - return pi.name - except Exception: - frappe.db.set_value("Import Supplier Invoice", name, "status", "Error") - pi.log_error("Unable to create Puchase Invoice") - return None + # adjust payment amount to match with grand total calculated + calc_total = 0 + adj = 0 + for term in args.terms: + calc_total += flt(term["payment_amount"]) + if flt(calc_total - flt(pi.grand_total)) != 0: + adj = calc_total - flt(pi.grand_total) + pi.payment_schedule = [] + for term in args.terms: + pi.append( + "payment_schedule", + { + "mode_of_payment_code": term["mode_of_payment_code"], + "bank_account_iban": term["bank_account_iban"], + "due_date": term["due_date"], + "payment_amount": flt(term["payment_amount"]) - adj, + }, + ) + adj = 0 + pi.imported_grand_total = calc_total + pi.save() + return pi.name def get_country(code):