feat: Editable Sales Invoice
diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.js b/erpnext/accounts/doctype/sales_invoice/sales_invoice.js
index 73ec051..7a5d392 100644
--- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.js
+++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.js
@@ -64,6 +64,27 @@
this.frm.toggle_reqd("due_date", !this.frm.doc.is_return);
+ if (this.frm.doc.repost_required) {
+ 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'));
+ this.frm.trigger('refresh');
+ }
+ }
+ });
+ });
+
+ $(`["${encodeURIComponent("Repost Accounting Entries")}"]`).css('color', 'red');
+ }
+
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 97e5f40..b98cd3a 100644
--- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.json
+++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.json
@@ -207,6 +207,7 @@
"is_internal_customer",
"is_discounted",
"remarks",
+ "repost_required",
"connections_tab"
],
"fields": [
@@ -1703,6 +1704,7 @@
"read_only": 1
},
{
+ "allow_on_submit": 1,
"default": "No",
"fieldname": "is_opening",
"fieldtype": "Select",
@@ -2097,6 +2099,14 @@
"hide_seconds": 1,
"label": "Write Off",
"width": "50%"
+ },
+ {
+ "default": "0",
+ "fieldname": "repost_required",
+ "fieldtype": "Check",
+ "hidden": 1,
+ "label": "Repost Required",
+ "read_only": 1
}
],
"icon": "fa fa-file-text",
@@ -2109,7 +2119,7 @@
"link_fieldname": "consolidated_invoice"
}
],
- "modified": "2022-10-11 13:07:36.488095",
+ "modified": "2022-10-15 19:15:49.526529",
"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 afd5a59..4c38883 100644
--- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py
+++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py
@@ -11,6 +11,9 @@
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 @@
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 @@
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:
@@ -514,6 +520,64 @@
def on_update(self):
self.set_paid_amount()
+ def on_update_after_submit(self):
+ 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()
+
+ # 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 parent level
+ for index, item in enumerate(self.get("items")):
+ for field in (
+ "income_account",
+ "expense_account",
+ "discount_account",
+ "deferred_revenue_account",
+ ):
+ if doc_before_update.get("items")[index].get(field) != item.get(field):
+ needs_repost = 1
+ break
+
+ for dimension in accounting_dimensions:
+ if doc_before_update.get("items")[index].get(dimension) != item.get(dimension):
+ needs_repost = 1
+ break
+
+ self.validate_accounts()
+ self.db_set("repost_required", needs_repost)
+
+ @frappe.whitelist()
+ def repost_accounting_entries(self):
+ self.docstatus = 2
+ self.make_gl_entries_on_cancel()
+ self.docstatus = 1
+ self.make_gl_entries()
+ self.db_set("repost_required", 0)
+
def set_paid_amount(self):
paid_amount = 0.0
base_paid_amount = 0.0