From 00eb6329a7727425d504cc36494f4affa89eb44b Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Tue, 21 Feb 2023 14:21:36 +0530 Subject: [PATCH] feat: Editable Sales Invoice (#32625) * feat: Editable Sales Invoice (cherry picked from commit 30da6ab2c1b870047d78b0f52196c906c6d3533d) # Conflicts: # erpnext/accounts/doctype/sales_invoice/sales_invoice.json * chore: Update allow on submit for Sales Invoice fields (cherry picked from commit e626107d3de7f95f15627a266e652cdfedf020a1) # Conflicts: # erpnext/accounts/doctype/sales_invoice_item/sales_invoice_item.json * chore: Break into smaller functions (cherry picked from commit 42e4c37f15e2a0613be9104d390bfdc3032f85b7) * chore: Update allow on submit fields (cherry picked from commit 1105e520315cbee4915e696706ecc3bfa1f0af8f) # Conflicts: # erpnext/accounts/doctype/sales_invoice/sales_invoice.json * test: Add unit tests (cherry picked from commit ed98015a5639889086a7662256bf743dfe387ffc) * chore: Update tests (cherry picked from commit 1a980123a2c83c669735a87d1d59815666de6170) * chore: Reset repost_required_flag on cancel (cherry picked from commit 0966867c0835d1066edf4146156e18d7f91da2f1) # Conflicts: # erpnext/accounts/doctype/sales_invoice/sales_invoice.py * chore: Enable no-copy for repost required field (cherry picked from commit 5fe55176ecc7b148380dd9940c5c34dda2aa9cd9) # Conflicts: # erpnext/accounts/doctype/sales_invoice/sales_invoice.json * chore: Validate for deferred revenue invoices (cherry picked from commit e29f756146b42c203d6e2a456c5e1ebf13d2ee19) * chore: Resolve conflicts --------- Co-authored-by: Deepesh Garg --- .../doctype/sales_invoice/sales_invoice.js | 19 ++++ .../doctype/sales_invoice/sales_invoice.json | 19 +++- .../doctype/sales_invoice/sales_invoice.py | 99 ++++++++++++++++++- .../sales_invoice/test_sales_invoice.py | 26 +++++ .../sales_invoice_item.json | 8 +- .../sales_taxes_and_charges.json | 7 +- erpnext/public/js/controllers/accounts.js | 6 ++ 7 files changed, 176 insertions(+), 8 deletions(-) diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.js b/erpnext/accounts/doctype/sales_invoice/sales_invoice.js index 89647da6e10..37f42c114cc 100644 --- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.js +++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.js @@ -64,6 +64,25 @@ erpnext.accounts.SalesInvoiceController = class SalesInvoiceController extends e this.frm.toggle_reqd("due_date", !this.frm.doc.is_return); + if (this.frm.doc.repost_required && this.frm.doc.docstatus===1) { + this.frm.set_intro(__("Accounting entries for this invoice needs to be reposted. Please click on 'Repost' button to update.")); + this.frm.add_custom_button(__('Repost Accounting Entries'), + () => { + this.frm.call({ + doc: this.frm.doc, + method: 'repost_accounting_entries', + freeze: true, + freeze_message: __('Reposting...'), + callback: (r) => { + if (!r.exc) { + frappe.msgprint(__('Accounting Entries are reposted')); + me.frm.refresh(); + } + } + }); + }).removeClass('btn-default').addClass('btn-warning'); + } + if (this.frm.doc.is_return) { this.frm.return_print_format = "Sales Invoice Return"; } diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.json b/erpnext/accounts/doctype/sales_invoice/sales_invoice.json index 0c8ddeb2bae..f18fef5a148 100644 --- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.json +++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.json @@ -209,6 +209,7 @@ "is_internal_customer", "is_discounted", "remarks", + "repost_required", "connections_tab" ], "fields": [ @@ -1038,6 +1039,7 @@ "read_only": 1 }, { + "allow_on_submit": 1, "depends_on": "redeem_loyalty_points", "fieldname": "loyalty_redemption_account", "fieldtype": "Link", @@ -1336,6 +1338,7 @@ "options": "fa fa-money" }, { + "allow_on_submit": 1, "depends_on": "is_pos", "fieldname": "cash_bank_account", "fieldtype": "Link", @@ -1435,6 +1438,7 @@ "print_hide": 1 }, { + "allow_on_submit": 1, "depends_on": "is_pos", "fieldname": "account_for_change_amount", "fieldtype": "Link", @@ -1483,6 +1487,7 @@ "hide_seconds": 1 }, { + "allow_on_submit": 1, "fieldname": "write_off_account", "fieldtype": "Link", "hide_days": 1, @@ -1706,6 +1711,7 @@ "read_only": 1 }, { + "allow_on_submit": 1, "default": "No", "fieldname": "is_opening", "fieldtype": "Select", @@ -1922,6 +1928,7 @@ "read_only": 1 }, { + "allow_on_submit": 1, "depends_on": "eval:doc.is_internal_customer", "description": "Unrealized Profit / Loss account for intra-company transfers", "fieldname": "unrealized_profit_loss_account", @@ -1964,6 +1971,7 @@ "label": "Disable Rounded Total" }, { + "allow_on_submit": 1, "fieldname": "additional_discount_account", "fieldtype": "Link", "label": "Discount Account", @@ -2114,6 +2122,15 @@ "fieldname": "named_place", "fieldtype": "Data", "label": "Named Place" + }, + { + "default": "0", + "fieldname": "repost_required", + "fieldtype": "Check", + "hidden": 1, + "label": "Repost Required", + "no_copy": 1, + "read_only": 1 } ], "icon": "fa fa-file-text", @@ -2126,7 +2143,7 @@ "link_fieldname": "consolidated_invoice" } ], - "modified": "2023-01-28 19:45:47.538163", + "modified": "2022-11-07 16:02:07.972258", "modified_by": "Administrator", "module": "Accounts", "name": "Sales Invoice", diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py index a56a2cf56ba..f5be4c7a3f3 100644 --- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py +++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py @@ -11,6 +11,9 @@ from frappe.utils import add_days, cint, cstr, flt, formatdate, get_link_to_form import erpnext from erpnext.accounts.deferred_revenue import validate_service_stop_date +from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import ( + get_accounting_dimensions, +) from erpnext.accounts.doctype.loyalty_program.loyalty_program import ( get_loyalty_program_details_with_points, validate_loyalty_points, @@ -100,13 +103,11 @@ class SalesInvoice(SellingController): self.validate_debit_to_acc() self.clear_unallocated_advances("Sales Invoice Advance", "advances") self.add_remarks() - self.validate_write_off_account() - self.validate_account_for_change_amount() self.validate_fixed_asset() self.set_income_account_for_fixed_assets() self.validate_item_cost_centers() - self.validate_income_account() self.check_conversion_rate() + self.validate_accounts() validate_inter_company_party( self.doctype, self.customer, self.company, self.inter_company_invoice_reference @@ -170,6 +171,11 @@ class SalesInvoice(SellingController): self.reset_default_field_value("set_warehouse", "items", "warehouse") + def validate_accounts(self): + self.validate_write_off_account() + self.validate_account_for_change_amount() + self.validate_income_account() + def validate_fixed_asset(self): for d in self.get("items"): if d.is_fixed_asset and d.meta.get_field("asset") and d.asset: @@ -368,6 +374,7 @@ class SalesInvoice(SellingController): self.repost_future_sle_and_gle() self.db_set("status", "Cancelled") + self.db_set("repost_required", 0) if ( frappe.db.get_single_value("Selling Settings", "sales_update_frequency") == "Each Transaction" @@ -516,6 +523,92 @@ class SalesInvoice(SellingController): def on_update(self): self.set_paid_amount() + def on_update_after_submit(self): + if hasattr(self, "repost_required"): + needs_repost = 0 + + # Check if any field affecting accounting entry is altered + doc_before_update = self.get_doc_before_save() + accounting_dimensions = get_accounting_dimensions() + ["cost_center", "project"] + + # Check if opening entry check updated + if doc_before_update.get("is_opening") != self.is_opening: + needs_repost = 1 + + if not needs_repost: + # Parent Level Accounts excluding party account + for field in ( + "additional_discount_account", + "cash_bank_account", + "account_for_change_amount", + "write_off_account", + "loyalty_redemption_account", + "unrealized_profit_loss_account", + ): + if doc_before_update.get(field) != self.get(field): + needs_repost = 1 + break + + # Check for parent accounting dimensions + for dimension in accounting_dimensions: + if doc_before_update.get(dimension) != self.get(dimension): + needs_repost = 1 + break + + # Check for child tables + if self.check_if_child_table_updated( + "items", + doc_before_update, + ("income_account", "expense_account", "discount_account"), + accounting_dimensions, + ): + needs_repost = 1 + + if self.check_if_child_table_updated( + "taxes", doc_before_update, ("account_head",), accounting_dimensions + ): + needs_repost = 1 + + self.validate_accounts() + + # validate if deferred revenue is enabled for any item + # Don't allow to update the invoice if deferred revenue is enabled + for item in self.get("items"): + if item.enable_deferred_revenue: + frappe.throw( + _( + "Deferred Revenue is enabled for item {0}. You cannot update the invoice after submission." + ).format(item.item_code) + ) + + self.db_set("repost_required", needs_repost) + + def check_if_child_table_updated( + self, child_table, doc_before_update, fields_to_check, accounting_dimensions + ): + # Check if any field affecting accounting entry is altered + for index, item in enumerate(self.get(child_table)): + for field in fields_to_check: + if doc_before_update.get(child_table)[index].get(field) != item.get(field): + return True + + for dimension in accounting_dimensions: + if doc_before_update.get(child_table)[index].get(dimension) != item.get(dimension): + return True + + return False + + @frappe.whitelist() + def repost_accounting_entries(self): + if self.repost_required: + self.docstatus = 2 + self.make_gl_entries_on_cancel() + self.docstatus = 1 + self.make_gl_entries() + self.db_set("repost_required", 0) + else: + frappe.throw(_("No updates pending for reposting")) + def set_paid_amount(self): paid_amount = 0.0 base_paid_amount = 0.0 diff --git a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py index df3cfec24bc..f9fb6232ac9 100644 --- a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py +++ b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py @@ -2769,6 +2769,31 @@ class TestSalesInvoice(unittest.TestCase): check_gl_entries(self, si.name, expected_gle, add_days(nowdate(), -1)) + # Update Invoice post submit and then check GL Entries again + + si.load_from_db() + si.items[0].income_account = "Service - _TC" + si.additional_discount_account = "_Test Account Sales - _TC" + si.taxes[0].account_head = "TDS Payable - _TC" + si.save() + + si.load_from_db() + self.assertTrue(si.repost_required) + + si.repost_accounting_entries() + + expected_gle = [ + ["_Test Account Sales - _TC", 22.0, 0.0, nowdate()], + ["Debtors - _TC", 88, 0.0, nowdate()], + ["Service - _TC", 0.0, 100.0, nowdate()], + ["TDS Payable - _TC", 0.0, 10.0, nowdate()], + ] + + check_gl_entries(self, si.name, expected_gle, add_days(nowdate(), -1)) + + si.load_from_db() + self.assertFalse(si.repost_required) + def test_asset_depreciation_on_sale_with_pro_rata(self): """ Tests if an Asset set to depreciate yearly on June 30, that gets sold on Sept 30, creates an additional depreciation entry on its date of sale. @@ -3326,6 +3351,7 @@ def check_gl_entries(doc, voucher_no, expected_gle, posting_date): """select account, debit, credit, posting_date from `tabGL Entry` where voucher_type='Sales Invoice' and voucher_no=%s and posting_date > %s + and is_cancelled = 0 order by posting_date asc, account asc""", (voucher_no, posting_date), as_dict=1, diff --git a/erpnext/accounts/doctype/sales_invoice_item/sales_invoice_item.json b/erpnext/accounts/doctype/sales_invoice_item/sales_invoice_item.json index f4f083068ee..cfda48e5c75 100644 --- a/erpnext/accounts/doctype/sales_invoice_item/sales_invoice_item.json +++ b/erpnext/accounts/doctype/sales_invoice_item/sales_invoice_item.json @@ -438,6 +438,7 @@ "label": "Accounting Details" }, { + "allow_on_submit": 1, "fieldname": "income_account", "fieldtype": "Link", "label": "Income Account", @@ -450,6 +451,7 @@ "width": "120px" }, { + "allow_on_submit": 1, "fieldname": "expense_account", "fieldtype": "Link", "label": "Expense Account", @@ -469,6 +471,7 @@ "print_hide": 1 }, { + "allow_on_submit": 1, "default": ":Company", "fieldname": "cost_center", "fieldtype": "Link", @@ -800,6 +803,7 @@ "options": "Finance Book" }, { + "allow_on_submit": 1, "fieldname": "project", "fieldtype": "Link", "label": "Project", @@ -822,7 +826,6 @@ "label": "Incoming Rate (Costing)", "no_copy": 1, "options": "Company:company:default_currency", - "precision": "6", "print_hide": 1 }, { @@ -835,6 +838,7 @@ "read_only": 1 }, { + "allow_on_submit": 1, "fieldname": "discount_account", "fieldtype": "Link", "label": "Discount Account", @@ -886,7 +890,7 @@ "idx": 1, "istable": 1, "links": [], - "modified": "2022-12-28 16:17:33.484531", + "modified": "2022-10-17 12:51:44.825398", "modified_by": "Administrator", "module": "Accounts", "name": "Sales Invoice Item", diff --git a/erpnext/accounts/doctype/sales_taxes_and_charges/sales_taxes_and_charges.json b/erpnext/accounts/doctype/sales_taxes_and_charges/sales_taxes_and_charges.json index 3a871bfcede..e236577e118 100644 --- a/erpnext/accounts/doctype/sales_taxes_and_charges/sales_taxes_and_charges.json +++ b/erpnext/accounts/doctype/sales_taxes_and_charges/sales_taxes_and_charges.json @@ -51,6 +51,7 @@ "oldfieldtype": "Data" }, { + "allow_on_submit": 1, "columns": 2, "fieldname": "account_head", "fieldtype": "Link", @@ -63,6 +64,7 @@ "search_index": 1 }, { + "allow_on_submit": 1, "default": ":Company", "fieldname": "cost_center", "fieldtype": "Link", @@ -216,12 +218,13 @@ "index_web_pages_for_search": 1, "istable": 1, "links": [], - "modified": "2021-08-05 20:04:01.726867", + "modified": "2022-10-17 13:08:17.776528", "modified_by": "Administrator", "module": "Accounts", "name": "Sales Taxes and Charges", "owner": "Administrator", "permissions": [], "sort_field": "modified", - "sort_order": "ASC" + "sort_order": "ASC", + "states": [] } \ No newline at end of file diff --git a/erpnext/public/js/controllers/accounts.js b/erpnext/public/js/controllers/accounts.js index c1fe72bb488..a07f75d1c5d 100644 --- a/erpnext/public/js/controllers/accounts.js +++ b/erpnext/public/js/controllers/accounts.js @@ -143,6 +143,12 @@ var get_payment_mode_account = function(frm, mode_of_payment, callback) { cur_frm.cscript.account_head = function(doc, cdt, cdn) { var d = locals[cdt][cdn]; + + if (doc.docstatus == 1) { + // Should not trigger any changes on change post submit + return; + } + if(!d.charge_type && d.account_head){ frappe.msgprint(__("Please select Charge Type first")); frappe.model.set_value(cdt, cdn, "account_head", "");