feat: Editable Sales Invoice (#32625)

* feat: Editable Sales Invoice

(cherry picked from commit 30da6ab2c1)

# Conflicts:
#	erpnext/accounts/doctype/sales_invoice/sales_invoice.json

* chore: Update allow on submit for Sales Invoice fields

(cherry picked from commit e626107d3d)

# Conflicts:
#	erpnext/accounts/doctype/sales_invoice_item/sales_invoice_item.json

* chore: Break into smaller functions

(cherry picked from commit 42e4c37f15)

* chore: Update allow on submit fields

(cherry picked from commit 1105e52031)

# Conflicts:
#	erpnext/accounts/doctype/sales_invoice/sales_invoice.json

* test: Add unit tests

(cherry picked from commit ed98015a56)

* chore: Update tests

(cherry picked from commit 1a980123a2)

* chore: Reset repost_required_flag on cancel

(cherry picked from commit 0966867c08)

# Conflicts:
#	erpnext/accounts/doctype/sales_invoice/sales_invoice.py

* chore: Enable no-copy for repost required field

(cherry picked from commit 5fe55176ec)

# Conflicts:
#	erpnext/accounts/doctype/sales_invoice/sales_invoice.json

* chore: Validate for deferred revenue invoices

(cherry picked from commit e29f756146)

* chore: Resolve conflicts

---------

Co-authored-by: Deepesh Garg <deepeshgarg6@gmail.com>
This commit is contained in:
mergify[bot]
2023-02-21 14:21:36 +05:30
committed by GitHub
parent ce817e97eb
commit 00eb6329a7
7 changed files with 176 additions and 8 deletions

View File

@@ -64,6 +64,25 @@ erpnext.accounts.SalesInvoiceController = class SalesInvoiceController extends e
this.frm.toggle_reqd("due_date", !this.frm.doc.is_return); 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) { if (this.frm.doc.is_return) {
this.frm.return_print_format = "Sales Invoice Return"; this.frm.return_print_format = "Sales Invoice Return";
} }

View File

@@ -209,6 +209,7 @@
"is_internal_customer", "is_internal_customer",
"is_discounted", "is_discounted",
"remarks", "remarks",
"repost_required",
"connections_tab" "connections_tab"
], ],
"fields": [ "fields": [
@@ -1038,6 +1039,7 @@
"read_only": 1 "read_only": 1
}, },
{ {
"allow_on_submit": 1,
"depends_on": "redeem_loyalty_points", "depends_on": "redeem_loyalty_points",
"fieldname": "loyalty_redemption_account", "fieldname": "loyalty_redemption_account",
"fieldtype": "Link", "fieldtype": "Link",
@@ -1336,6 +1338,7 @@
"options": "fa fa-money" "options": "fa fa-money"
}, },
{ {
"allow_on_submit": 1,
"depends_on": "is_pos", "depends_on": "is_pos",
"fieldname": "cash_bank_account", "fieldname": "cash_bank_account",
"fieldtype": "Link", "fieldtype": "Link",
@@ -1435,6 +1438,7 @@
"print_hide": 1 "print_hide": 1
}, },
{ {
"allow_on_submit": 1,
"depends_on": "is_pos", "depends_on": "is_pos",
"fieldname": "account_for_change_amount", "fieldname": "account_for_change_amount",
"fieldtype": "Link", "fieldtype": "Link",
@@ -1483,6 +1487,7 @@
"hide_seconds": 1 "hide_seconds": 1
}, },
{ {
"allow_on_submit": 1,
"fieldname": "write_off_account", "fieldname": "write_off_account",
"fieldtype": "Link", "fieldtype": "Link",
"hide_days": 1, "hide_days": 1,
@@ -1706,6 +1711,7 @@
"read_only": 1 "read_only": 1
}, },
{ {
"allow_on_submit": 1,
"default": "No", "default": "No",
"fieldname": "is_opening", "fieldname": "is_opening",
"fieldtype": "Select", "fieldtype": "Select",
@@ -1922,6 +1928,7 @@
"read_only": 1 "read_only": 1
}, },
{ {
"allow_on_submit": 1,
"depends_on": "eval:doc.is_internal_customer", "depends_on": "eval:doc.is_internal_customer",
"description": "Unrealized Profit / Loss account for intra-company transfers", "description": "Unrealized Profit / Loss account for intra-company transfers",
"fieldname": "unrealized_profit_loss_account", "fieldname": "unrealized_profit_loss_account",
@@ -1964,6 +1971,7 @@
"label": "Disable Rounded Total" "label": "Disable Rounded Total"
}, },
{ {
"allow_on_submit": 1,
"fieldname": "additional_discount_account", "fieldname": "additional_discount_account",
"fieldtype": "Link", "fieldtype": "Link",
"label": "Discount Account", "label": "Discount Account",
@@ -2114,6 +2122,15 @@
"fieldname": "named_place", "fieldname": "named_place",
"fieldtype": "Data", "fieldtype": "Data",
"label": "Named Place" "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", "icon": "fa fa-file-text",
@@ -2126,7 +2143,7 @@
"link_fieldname": "consolidated_invoice" "link_fieldname": "consolidated_invoice"
} }
], ],
"modified": "2023-01-28 19:45:47.538163", "modified": "2022-11-07 16:02:07.972258",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Accounts", "module": "Accounts",
"name": "Sales Invoice", "name": "Sales Invoice",

View File

@@ -11,6 +11,9 @@ from frappe.utils import add_days, cint, cstr, flt, formatdate, get_link_to_form
import erpnext import erpnext
from erpnext.accounts.deferred_revenue import validate_service_stop_date 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 ( from erpnext.accounts.doctype.loyalty_program.loyalty_program import (
get_loyalty_program_details_with_points, get_loyalty_program_details_with_points,
validate_loyalty_points, validate_loyalty_points,
@@ -100,13 +103,11 @@ class SalesInvoice(SellingController):
self.validate_debit_to_acc() self.validate_debit_to_acc()
self.clear_unallocated_advances("Sales Invoice Advance", "advances") self.clear_unallocated_advances("Sales Invoice Advance", "advances")
self.add_remarks() self.add_remarks()
self.validate_write_off_account()
self.validate_account_for_change_amount()
self.validate_fixed_asset() self.validate_fixed_asset()
self.set_income_account_for_fixed_assets() self.set_income_account_for_fixed_assets()
self.validate_item_cost_centers() self.validate_item_cost_centers()
self.validate_income_account()
self.check_conversion_rate() self.check_conversion_rate()
self.validate_accounts()
validate_inter_company_party( validate_inter_company_party(
self.doctype, self.customer, self.company, self.inter_company_invoice_reference 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") 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): def validate_fixed_asset(self):
for d in self.get("items"): for d in self.get("items"):
if d.is_fixed_asset and d.meta.get_field("asset") and d.asset: 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.repost_future_sle_and_gle()
self.db_set("status", "Cancelled") self.db_set("status", "Cancelled")
self.db_set("repost_required", 0)
if ( if (
frappe.db.get_single_value("Selling Settings", "sales_update_frequency") == "Each Transaction" frappe.db.get_single_value("Selling Settings", "sales_update_frequency") == "Each Transaction"
@@ -516,6 +523,92 @@ class SalesInvoice(SellingController):
def on_update(self): def on_update(self):
self.set_paid_amount() 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): def set_paid_amount(self):
paid_amount = 0.0 paid_amount = 0.0
base_paid_amount = 0.0 base_paid_amount = 0.0

View File

@@ -2769,6 +2769,31 @@ class TestSalesInvoice(unittest.TestCase):
check_gl_entries(self, si.name, expected_gle, add_days(nowdate(), -1)) 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): 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. 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 """select account, debit, credit, posting_date
from `tabGL Entry` from `tabGL Entry`
where voucher_type='Sales Invoice' and voucher_no=%s and posting_date > %s where voucher_type='Sales Invoice' and voucher_no=%s and posting_date > %s
and is_cancelled = 0
order by posting_date asc, account asc""", order by posting_date asc, account asc""",
(voucher_no, posting_date), (voucher_no, posting_date),
as_dict=1, as_dict=1,

View File

@@ -438,6 +438,7 @@
"label": "Accounting Details" "label": "Accounting Details"
}, },
{ {
"allow_on_submit": 1,
"fieldname": "income_account", "fieldname": "income_account",
"fieldtype": "Link", "fieldtype": "Link",
"label": "Income Account", "label": "Income Account",
@@ -450,6 +451,7 @@
"width": "120px" "width": "120px"
}, },
{ {
"allow_on_submit": 1,
"fieldname": "expense_account", "fieldname": "expense_account",
"fieldtype": "Link", "fieldtype": "Link",
"label": "Expense Account", "label": "Expense Account",
@@ -469,6 +471,7 @@
"print_hide": 1 "print_hide": 1
}, },
{ {
"allow_on_submit": 1,
"default": ":Company", "default": ":Company",
"fieldname": "cost_center", "fieldname": "cost_center",
"fieldtype": "Link", "fieldtype": "Link",
@@ -800,6 +803,7 @@
"options": "Finance Book" "options": "Finance Book"
}, },
{ {
"allow_on_submit": 1,
"fieldname": "project", "fieldname": "project",
"fieldtype": "Link", "fieldtype": "Link",
"label": "Project", "label": "Project",
@@ -822,7 +826,6 @@
"label": "Incoming Rate (Costing)", "label": "Incoming Rate (Costing)",
"no_copy": 1, "no_copy": 1,
"options": "Company:company:default_currency", "options": "Company:company:default_currency",
"precision": "6",
"print_hide": 1 "print_hide": 1
}, },
{ {
@@ -835,6 +838,7 @@
"read_only": 1 "read_only": 1
}, },
{ {
"allow_on_submit": 1,
"fieldname": "discount_account", "fieldname": "discount_account",
"fieldtype": "Link", "fieldtype": "Link",
"label": "Discount Account", "label": "Discount Account",
@@ -886,7 +890,7 @@
"idx": 1, "idx": 1,
"istable": 1, "istable": 1,
"links": [], "links": [],
"modified": "2022-12-28 16:17:33.484531", "modified": "2022-10-17 12:51:44.825398",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Accounts", "module": "Accounts",
"name": "Sales Invoice Item", "name": "Sales Invoice Item",

View File

@@ -51,6 +51,7 @@
"oldfieldtype": "Data" "oldfieldtype": "Data"
}, },
{ {
"allow_on_submit": 1,
"columns": 2, "columns": 2,
"fieldname": "account_head", "fieldname": "account_head",
"fieldtype": "Link", "fieldtype": "Link",
@@ -63,6 +64,7 @@
"search_index": 1 "search_index": 1
}, },
{ {
"allow_on_submit": 1,
"default": ":Company", "default": ":Company",
"fieldname": "cost_center", "fieldname": "cost_center",
"fieldtype": "Link", "fieldtype": "Link",
@@ -216,12 +218,13 @@
"index_web_pages_for_search": 1, "index_web_pages_for_search": 1,
"istable": 1, "istable": 1,
"links": [], "links": [],
"modified": "2021-08-05 20:04:01.726867", "modified": "2022-10-17 13:08:17.776528",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Accounts", "module": "Accounts",
"name": "Sales Taxes and Charges", "name": "Sales Taxes and Charges",
"owner": "Administrator", "owner": "Administrator",
"permissions": [], "permissions": [],
"sort_field": "modified", "sort_field": "modified",
"sort_order": "ASC" "sort_order": "ASC",
"states": []
} }

View File

@@ -143,6 +143,12 @@ var get_payment_mode_account = function(frm, mode_of_payment, callback) {
cur_frm.cscript.account_head = function(doc, cdt, cdn) { cur_frm.cscript.account_head = function(doc, cdt, cdn) {
var d = locals[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){ if(!d.charge_type && d.account_head){
frappe.msgprint(__("Please select Charge Type first")); frappe.msgprint(__("Please select Charge Type first"));
frappe.model.set_value(cdt, cdn, "account_head", ""); frappe.model.set_value(cdt, cdn, "account_head", "");