From f7de825e891154d1df2ae0dd5b832067f87bef35 Mon Sep 17 00:00:00 2001 From: Smit Vora Date: Tue, 19 Sep 2023 20:47:21 +0530 Subject: [PATCH 01/72] chore: add regional support for getting payment entries (#37119) chore: add regional support for get payment entries (cherry picked from commit 3e282bfbceb6bbf3cfb015f92790b534335f97da) --- .../payment_reconciliation/payment_reconciliation.py | 10 ++++++++-- erpnext/controllers/accounts_controller.py | 7 ++++++- 2 files changed, 14 insertions(+), 3 deletions(-) diff --git a/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py b/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py index 7d294e873d4..08923e74266 100644 --- a/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py +++ b/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py @@ -19,7 +19,7 @@ from erpnext.accounts.utils import ( get_outstanding_invoices, reconcile_against_document, ) -from erpnext.controllers.accounts_controller import get_advance_payment_entries +from erpnext.controllers.accounts_controller import get_advance_payment_entries_for_regional class PaymentReconciliation(Document): @@ -62,7 +62,7 @@ class PaymentReconciliation(Document): if self.payment_name: condition += "name like '%%{0}%%'".format(self.payment_name) - payment_entries = get_advance_payment_entries( + payment_entries = get_advance_payment_entries_for_regional( self.party_type, self.party, self.receivable_payable_account, @@ -350,6 +350,7 @@ class PaymentReconciliation(Document): ) def reconcile_allocations(self, skip_ref_details_update_for_pe=False): + adjust_allocations_for_taxes(self) dr_or_cr = ( "credit_in_account_currency" if erpnext.get_party_account_type(self.party_type) == "Receivable" @@ -650,3 +651,8 @@ def reconcile_dr_cr_note(dr_cr_notes, company): None, inv.cost_center, ) + + +@erpnext.allow_regional +def adjust_allocations_for_taxes(doc): + pass diff --git a/erpnext/controllers/accounts_controller.py b/erpnext/controllers/accounts_controller.py index c7e21638bc7..257223234a8 100644 --- a/erpnext/controllers/accounts_controller.py +++ b/erpnext/controllers/accounts_controller.py @@ -909,7 +909,7 @@ class AccountsController(TransactionBase): party_type, party, party_account, amount_field, order_doctype, order_list, include_unallocated ) - payment_entries = get_advance_payment_entries( + payment_entries = get_advance_payment_entries_for_regional( party_type, party, party_account, order_doctype, order_list, include_unallocated ) @@ -2349,6 +2349,11 @@ def get_advance_journal_entries( return list(journal_entries) +@erpnext.allow_regional +def get_advance_payment_entries_for_regional(*args, **kwargs): + return get_advance_payment_entries(*args, **kwargs) + + def get_advance_payment_entries( party_type, party, From 0ecd7d2bf5eb522315776bae5633486d89791e00 Mon Sep 17 00:00:00 2001 From: Anand Baburajan Date: Wed, 4 Oct 2023 10:29:14 +0530 Subject: [PATCH 02/72] feat: composite WIP asset (#37352) feat: wip composite asset --- .../purchase_invoice/purchase_invoice.js | 6 + .../purchase_invoice_item.json | 11 +- erpnext/assets/doctype/asset/asset.js | 44 ++++- erpnext/assets/doctype/asset/asset.json | 29 +++- erpnext/assets/doctype/asset/asset.py | 11 +- erpnext/assets/doctype/asset/test_asset.py | 1 + .../asset_capitalization.js | 72 ++++++-- .../asset_capitalization.json | 36 ++-- .../asset_capitalization.py | 159 ++++++++++++++++-- .../test_asset_capitalization.py | 74 ++++++++ .../purchase_receipt/purchase_receipt.js | 6 + .../purchase_receipt_item.json | 11 +- 12 files changed, 406 insertions(+), 54 deletions(-) diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js index 5c82cf99438..ceb8204bd5d 100644 --- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js +++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js @@ -460,6 +460,12 @@ cur_frm.set_query("expense_account", "items", function(doc) { } }); +cur_frm.set_query("wip_composite_asset", "items", function() { + return { + filters: {'is_composite_asset': 1, 'docstatus': 0 } + } +}); + cur_frm.cscript.expense_account = function(doc, cdt, cdn){ var d = locals[cdt][cdn]; if(d.idx == 1 && d.expense_account){ diff --git a/erpnext/accounts/doctype/purchase_invoice_item/purchase_invoice_item.json b/erpnext/accounts/doctype/purchase_invoice_item/purchase_invoice_item.json index 92db6b61bb9..04f28beda9e 100644 --- a/erpnext/accounts/doctype/purchase_invoice_item/purchase_invoice_item.json +++ b/erpnext/accounts/doctype/purchase_invoice_item/purchase_invoice_item.json @@ -75,6 +75,7 @@ "manufacturer_part_no", "accounting", "expense_account", + "wip_composite_asset", "col_break5", "is_fixed_asset", "asset_location", @@ -877,12 +878,18 @@ "fieldname": "apply_tds", "fieldtype": "Check", "label": "Apply TDS" + }, + { + "fieldname": "wip_composite_asset", + "fieldtype": "Link", + "label": "WIP Composite Asset", + "options": "Asset" } ], "idx": 1, "istable": 1, "links": [], - "modified": "2023-07-04 17:22:21.501152", + "modified": "2023-10-03 21:01:01.824892", "modified_by": "Administrator", "module": "Accounts", "name": "Purchase Invoice Item", @@ -892,4 +899,4 @@ "sort_field": "modified", "sort_order": "DESC", "states": [] -} +} \ No newline at end of file diff --git a/erpnext/assets/doctype/asset/asset.js b/erpnext/assets/doctype/asset/asset.js index 5c1da91fdf9..0605189fec0 100644 --- a/erpnext/assets/doctype/asset/asset.js +++ b/erpnext/assets/doctype/asset/asset.js @@ -147,6 +147,15 @@ frappe.ui.form.on('Asset', { if (frm.doc.docstatus == 0) { frm.toggle_reqd("finance_books", frm.doc.calculate_depreciation); + + if (frm.doc.is_composite_asset && !frm.doc.capitalized_in) { + $('.primary-action').prop('hidden', true); + $('.form-message').text('Capitalize this asset to confirm'); + + frm.add_custom_button(__("Capitalize Asset"), function() { + frm.trigger("create_asset_capitalization"); + }); + } } }, @@ -168,7 +177,7 @@ frappe.ui.form.on('Asset', { frm.set_df_property('purchase_invoice', 'read_only', 1); frm.set_df_property('purchase_receipt', 'read_only', 1); } - else if (frm.doc.is_existing_asset) { + else if (frm.doc.is_existing_asset || frm.doc.is_composite_asset) { frm.toggle_reqd('purchase_receipt', 0); frm.toggle_reqd('purchase_invoice', 0); } @@ -300,7 +309,17 @@ frappe.ui.form.on('Asset', { is_existing_asset: function(frm) { frm.trigger("toggle_reference_doc"); - // frm.toggle_reqd("next_depreciation_date", (!frm.doc.is_existing_asset && frm.doc.calculate_depreciation)); + }, + + is_composite_asset: function(frm) { + if(frm.doc.is_composite_asset) { + frm.set_value('gross_purchase_amount', 0); + frm.set_df_property('gross_purchase_amount', 'read_only', 1); + } else { + frm.set_df_property('gross_purchase_amount', 'read_only', 0); + } + + frm.trigger("toggle_reference_doc"); }, make_schedules_editable: function(frm) { @@ -361,6 +380,19 @@ frappe.ui.form.on('Asset', { }); }, + create_asset_capitalization: function(frm) { + frappe.call({ + args: { + "asset": frm.doc.name, + }, + method: "erpnext.assets.doctype.asset.asset.create_asset_capitalization", + callback: function(r) { + var doclist = frappe.model.sync(r.message); + frappe.set_route("Form", doclist[0].doctype, doclist[0].name); + } + }); + }, + split_asset: function(frm) { const title = __('Split Asset'); @@ -424,9 +456,11 @@ frappe.ui.form.on('Asset', { }, gross_purchase_amount: function(frm) { - frm.doc.finance_books.forEach(d => { - frm.events.set_depreciation_rate(frm, d); - }) + if (frm.doc.finance_books) { + frm.doc.finance_books.forEach(d => { + frm.events.set_depreciation_rate(frm, d); + }) + } }, purchase_receipt: (frm) => { diff --git a/erpnext/assets/doctype/asset/asset.json b/erpnext/assets/doctype/asset/asset.json index 060d991945b..1da3edcc60e 100644 --- a/erpnext/assets/doctype/asset/asset.json +++ b/erpnext/assets/doctype/asset/asset.json @@ -14,6 +14,7 @@ "asset_owner", "asset_owner_company", "is_existing_asset", + "is_composite_asset", "supplier", "customer", "image", @@ -72,7 +73,8 @@ "purchase_receipt_amount", "default_finance_book", "depr_entry_posting_status", - "amended_from" + "amended_from", + "capitalized_in" ], "fields": [ { @@ -199,7 +201,7 @@ "fieldtype": "Date", "label": "Purchase Date", "read_only": 1, - "read_only_depends_on": "eval:!doc.is_existing_asset", + "read_only_depends_on": "eval:!doc.is_existing_asset && !doc.is_composite_asset", "reqd": 1 }, { @@ -237,10 +239,12 @@ "default": "0", "fieldname": "calculate_depreciation", "fieldtype": "Check", - "label": "Calculate Depreciation" + "label": "Calculate Depreciation", + "read_only_depends_on": "eval:doc.is_composite_asset && !doc.gross_purchase_amount" }, { "default": "0", + "depends_on": "eval:!doc.is_composite_asset", "fieldname": "is_existing_asset", "fieldtype": "Check", "label": "Is Existing Asset" @@ -492,7 +496,7 @@ "fieldname": "asset_quantity", "fieldtype": "Int", "label": "Asset Quantity", - "read_only_depends_on": "eval:!doc.is_existing_asset" + "read_only_depends_on": "eval:!doc.is_existing_asset && !doc.is_composite_asset" }, { "fieldname": "depr_entry_posting_status", @@ -510,6 +514,21 @@ "fieldname": "is_fully_depreciated", "fieldtype": "Check", "label": "Is Fully Depreciated" + }, + { + "default": "0", + "depends_on": "eval:!doc.is_existing_asset", + "fieldname": "is_composite_asset", + "fieldtype": "Check", + "label": "Is Composite Asset" + }, + { + "fieldname": "capitalized_in", + "fieldtype": "Link", + "hidden": 1, + "label": "Capitalized In", + "options": "Asset Capitalization", + "read_only": 1 } ], "idx": 72, @@ -538,7 +557,7 @@ "table_fieldname": "accounts" } ], - "modified": "2023-08-10 20:25:09.913073", + "modified": "2023-10-03 23:28:26.732269", "modified_by": "Administrator", "module": "Assets", "name": "Asset", diff --git a/erpnext/assets/doctype/asset/asset.py b/erpnext/assets/doctype/asset/asset.py index 81a35ad8f93..d54d15afaf6 100644 --- a/erpnext/assets/doctype/asset/asset.py +++ b/erpnext/assets/doctype/asset/asset.py @@ -234,7 +234,7 @@ class Asset(AccountsController): if not self.asset_category: self.asset_category = frappe.get_cached_value("Item", self.item_code, "asset_category") - if not flt(self.gross_purchase_amount): + if not flt(self.gross_purchase_amount) and not self.is_composite_asset: frappe.throw(_("Gross Purchase Amount is mandatory"), frappe.MandatoryError) if is_cwip_accounting_enabled(self.asset_category): @@ -1166,6 +1166,15 @@ def create_asset_repair(asset, asset_name): return asset_repair +@frappe.whitelist() +def create_asset_capitalization(asset): + asset_capitalization = frappe.new_doc("Asset Capitalization") + asset_capitalization.update( + {"target_asset": asset, "capitalization_method": "Choose a WIP composite asset"} + ) + return asset_capitalization + + @frappe.whitelist() def create_asset_value_adjustment(asset, asset_category, company): asset_value_adjustment = frappe.new_doc("Asset Value Adjustment") diff --git a/erpnext/assets/doctype/asset/test_asset.py b/erpnext/assets/doctype/asset/test_asset.py index 1adbeed65a5..fc36df8aec5 100644 --- a/erpnext/assets/doctype/asset/test_asset.py +++ b/erpnext/assets/doctype/asset/test_asset.py @@ -1686,6 +1686,7 @@ def create_asset(**args): "location": args.location or "Test Location", "asset_owner": args.asset_owner or "Company", "is_existing_asset": args.is_existing_asset or 1, + "is_composite_asset": args.is_composite_asset or 0, "asset_quantity": args.get("asset_quantity") or 1, "depr_entry_posting_status": args.depr_entry_posting_status or "", } diff --git a/erpnext/assets/doctype/asset_capitalization/asset_capitalization.js b/erpnext/assets/doctype/asset_capitalization/asset_capitalization.js index b312f93d319..304bdf26dee 100644 --- a/erpnext/assets/doctype/asset_capitalization/asset_capitalization.js +++ b/erpnext/assets/doctype/asset_capitalization/asset_capitalization.js @@ -15,9 +15,15 @@ erpnext.assets.AssetCapitalization = class AssetCapitalization extends erpnext.s refresh() { this.show_general_ledger(); + if ((this.frm.doc.stock_items && this.frm.doc.stock_items.length) || !this.frm.doc.target_is_fixed_asset) { this.show_stock_ledger(); } + + if (this.frm.doc.stock_items && !this.frm.doc.stock_items.length && this.frm.doc.target_asset && this.frm.doc.capitalization_method === "Choose a WIP composite asset") { + this.set_consumed_stock_items_tagged_to_wip_composite_asset(this.frm.doc.target_asset); + this.get_target_asset_details(); + } } setup_queries() { @@ -34,18 +40,9 @@ erpnext.assets.AssetCapitalization = class AssetCapitalization extends erpnext.s }); me.frm.set_query("target_asset", function() { - var filters = {}; - - if (me.frm.doc.target_item_code) { - filters['item_code'] = me.frm.doc.target_item_code; - } - - filters['status'] = ["not in", ["Draft", "Scrapped", "Sold", "Capitalized", "Decapitalized"]]; - filters['docstatus'] = 1; - return { - filters: filters - }; + filters: {'is_composite_asset': 1, 'docstatus': 0 } + } }); me.frm.set_query("asset", "asset_items", function() { @@ -104,6 +101,39 @@ erpnext.assets.AssetCapitalization = class AssetCapitalization extends erpnext.s return this.get_target_item_details(); } + target_asset() { + if (this.frm.doc.target_asset && this.frm.doc.capitalization_method === "Choose a WIP composite asset") { + this.set_consumed_stock_items_tagged_to_wip_composite_asset(this.frm.doc.target_asset); + this.get_target_asset_details(); + } + } + + set_consumed_stock_items_tagged_to_wip_composite_asset(asset) { + var me = this; + + if (asset) { + return me.frm.call({ + method: "erpnext.assets.doctype.asset_capitalization.asset_capitalization.get_items_tagged_to_wip_composite_asset", + args: { + asset: asset, + }, + callback: function (r) { + if (!r.exc && r.message) { + me.frm.clear_table("stock_items"); + + for (let item of r.message) { + me.frm.add_child("stock_items", item); + } + + refresh_field("stock_items"); + + me.calculate_totals(); + } + } + }); + } + } + item_code(doc, cdt, cdn) { var row = frappe.get_doc(cdt, cdn); if (cdt === "Asset Capitalization Stock Item") { @@ -218,6 +248,26 @@ erpnext.assets.AssetCapitalization = class AssetCapitalization extends erpnext.s } } + get_target_asset_details() { + var me = this; + + if (me.frm.doc.target_asset) { + return me.frm.call({ + method: "erpnext.assets.doctype.asset_capitalization.asset_capitalization.get_target_asset_details", + child: me.frm.doc, + args: { + asset: me.frm.doc.target_asset, + company: me.frm.doc.company, + }, + callback: function (r) { + if (!r.exc) { + me.frm.refresh_fields(); + } + } + }); + } + } + get_consumed_stock_item_details(row) { var me = this; diff --git a/erpnext/assets/doctype/asset_capitalization/asset_capitalization.json b/erpnext/assets/doctype/asset_capitalization/asset_capitalization.json index 04b0c4e5132..9ddc44212f6 100644 --- a/erpnext/assets/doctype/asset_capitalization/asset_capitalization.json +++ b/erpnext/assets/doctype/asset_capitalization/asset_capitalization.json @@ -8,24 +8,25 @@ "engine": "InnoDB", "field_order": [ "title", + "company", "naming_series", "entry_type", - "target_item_code", - "target_asset", "target_item_name", "target_is_fixed_asset", "target_has_batch_no", "target_has_serial_no", "column_break_9", - "target_asset_name", + "capitalization_method", + "target_item_code", "target_asset_location", + "target_asset", + "target_asset_name", "target_warehouse", "target_qty", "target_stock_uom", "target_batch_no", "target_serial_no", "column_break_5", - "company", "finance_book", "posting_date", "posting_time", @@ -57,12 +58,13 @@ "label": "Title" }, { + "depends_on": "eval:(doc.target_item_code && !doc.__islocal && doc.capitalization_method !== 'Choose a WIP composite asset') || ((doc.entry_type=='Capitalization' && doc.capitalization_method=='Create a new composite asset') || doc.entry_type=='Decapitalization')", "fieldname": "target_item_code", "fieldtype": "Link", "in_standard_filter": 1, "label": "Target Item Code", - "options": "Item", - "reqd": 1 + "mandatory_depends_on": "eval:(doc.entry_type=='Capitalization' && doc.capitalization_method=='Create a new composite asset') || doc.entry_type=='Decapitalization'", + "options": "Item" }, { "depends_on": "eval:doc.target_item_code && doc.target_item_name != doc.target_item_code", @@ -86,16 +88,18 @@ "fieldtype": "Column Break" }, { + "depends_on": "eval:(doc.target_asset && !doc.__islocal) || (doc.entry_type=='Capitalization' && doc.capitalization_method=='Choose a WIP composite asset')", "fieldname": "target_asset", "fieldtype": "Link", "in_standard_filter": 1, "label": "Target Asset", + "mandatory_depends_on": "eval:doc.entry_type=='Capitalization' && doc.capitalization_method=='Choose a WIP composite asset'", "no_copy": 1, "options": "Asset", - "read_only": 1 + "read_only_depends_on": "eval:(doc.entry_type=='Decapitalization') || (doc.entry_type=='Capitalization' && doc.capitalization_method=='Create a new composite asset')" }, { - "depends_on": "eval:doc.entry_type=='Capitalization'", + "depends_on": "eval:(doc.target_asset_name && !doc.__islocal) || (doc.target_asset && doc.entry_type=='Capitalization' && doc.capitalization_method=='Choose a WIP composite asset')", "fetch_from": "target_asset.asset_name", "fieldname": "target_asset_name", "fieldtype": "Data", @@ -186,12 +190,14 @@ }, { "default": "1", + "depends_on": "eval:doc.entry_type=='Decapitalization'", "fieldname": "target_qty", "fieldtype": "Float", "label": "Target Qty", "read_only_depends_on": "eval:doc.entry_type=='Capitalization'" }, { + "depends_on": "eval:doc.entry_type=='Decapitalization'", "fetch_from": "target_item_code.stock_uom", "fieldname": "target_stock_uom", "fieldtype": "Link", @@ -331,18 +337,26 @@ "read_only": 1 }, { - "depends_on": "eval:doc.entry_type=='Capitalization'", + "depends_on": "eval:doc.entry_type=='Capitalization' && doc.capitalization_method=='Create a new composite asset'", "fieldname": "target_asset_location", "fieldtype": "Link", "label": "Target Asset Location", - "mandatory_depends_on": "eval:doc.entry_type=='Capitalization'", + "mandatory_depends_on": "eval:doc.entry_type=='Capitalization' && doc.capitalization_method=='Create a new composite asset'", "options": "Location" + }, + { + "depends_on": "eval:doc.entry_type=='Capitalization'", + "fieldname": "capitalization_method", + "fieldtype": "Select", + "label": "Capitalization Method", + "mandatory_depends_on": "eval:doc.entry_type=='Capitalization'", + "options": "\nCreate a new composite asset\nChoose a WIP composite asset" } ], "index_web_pages_for_search": 1, "is_submittable": 1, "links": [], - "modified": "2023-06-22 14:17:07.995120", + "modified": "2023-10-03 22:55:59.461456", "modified_by": "Administrator", "module": "Assets", "name": "Asset Capitalization", diff --git a/erpnext/assets/doctype/asset_capitalization/asset_capitalization.py b/erpnext/assets/doctype/asset_capitalization/asset_capitalization.py index 44db6920c13..04654104c7a 100644 --- a/erpnext/assets/doctype/asset_capitalization/asset_capitalization.py +++ b/erpnext/assets/doctype/asset_capitalization/asset_capitalization.py @@ -53,6 +53,7 @@ class AssetCapitalization(StockController): self.validate_posting_time() self.set_missing_values(for_validate=True) self.validate_target_item() + self.validate_target_asset() self.validate_consumed_stock_item() self.validate_consumed_asset_item() self.validate_service_item() @@ -63,12 +64,12 @@ class AssetCapitalization(StockController): def before_submit(self): self.validate_source_mandatory() - if self.entry_type == "Capitalization": - self.create_target_asset() + self.create_target_asset() def on_submit(self): self.update_stock_ledger() self.make_gl_entries() + self.update_target_asset() def on_cancel(self): self.ignore_linked_doctypes = ("GL Entry", "Stock Ledger Entry", "Repost Item Valuation") @@ -85,6 +86,11 @@ class AssetCapitalization(StockController): if self.meta.has_field(k) and (not self.get(k) or k in force_fields): self.set(k, v) + target_asset_details = get_target_asset_details(self.target_asset, self.company) + for k, v in target_asset_details.items(): + if self.meta.has_field(k) and (not self.get(k) or k in force_fields): + self.set(k, v) + for d in self.stock_items: args = self.as_dict() args.update(d.as_dict()) @@ -146,6 +152,33 @@ class AssetCapitalization(StockController): self.validate_item(target_item) + def validate_target_asset(self): + if self.target_asset: + target_asset = self.get_asset_for_validation(self.target_asset) + + if not target_asset.is_composite_asset: + frappe.throw(_("Target Asset {0} needs to be composite asset").format(target_asset.name)) + + if target_asset.item_code != self.target_item_code: + frappe.throw( + _("Asset {0} does not belong to Item {1}").format(self.target_asset, self.target_item_code) + ) + + if target_asset.status in ("Scrapped", "Sold", "Capitalized", "Decapitalized"): + frappe.throw( + _("Target Asset {0} cannot be {1}").format(target_asset.name, target_asset.status) + ) + + if target_asset.docstatus == 1: + frappe.throw(_("Target Asset {0} cannot be submitted").format(target_asset.name)) + elif target_asset.docstatus == 2: + frappe.throw(_("Target Asset {0} cannot be cancelled").format(target_asset.name)) + + if target_asset.company != self.company: + frappe.throw( + _("Target Asset {0} does not belong to company {1}").format(target_asset.name, self.company) + ) + def validate_consumed_stock_item(self): for d in self.stock_items: if d.item_code: @@ -170,7 +203,23 @@ class AssetCapitalization(StockController): ) asset = self.get_asset_for_validation(d.asset) - self.validate_asset(asset) + + if asset.status in ("Draft", "Scrapped", "Sold", "Capitalized", "Decapitalized"): + frappe.throw( + _("Row #{0}: Consumed Asset {1} cannot be {2}").format(d.idx, asset.name, asset.status) + ) + + if asset.docstatus == 0: + frappe.throw(_("Row #{0}: Consumed Asset {1} cannot be Draft").format(d.idx, asset.name)) + elif asset.docstatus == 2: + frappe.throw(_("Row #{0}: Consumed Asset {1} cannot be cancelled").format(d.idx, asset.name)) + + if asset.company != self.company: + frappe.throw( + _("Row #{0}: Consumed Asset {1} does not belong to company {2}").format( + d.idx, asset.name, self.company + ) + ) def validate_service_item(self): for d in self.service_items: @@ -205,21 +254,12 @@ class AssetCapitalization(StockController): def get_asset_for_validation(self, asset): return frappe.db.get_value( - "Asset", asset, ["name", "item_code", "company", "status", "docstatus"], as_dict=1 + "Asset", + asset, + ["name", "item_code", "company", "status", "docstatus", "is_composite_asset"], + as_dict=1, ) - def validate_asset(self, asset): - if asset.status in ("Draft", "Scrapped", "Sold", "Capitalized", "Decapitalized"): - frappe.throw(_("Asset {0} is {1}").format(asset.name, asset.status)) - - if asset.docstatus == 0: - frappe.throw(_("Asset {0} is Draft").format(asset.name)) - if asset.docstatus == 2: - frappe.throw(_("Asset {0} is cancelled").format(asset.name)) - - if asset.company != self.company: - frappe.throw(_("Asset {0} does not belong to company {1}").format(asset.name, self.company)) - @frappe.whitelist() def set_warehouse_details(self): for d in self.get("stock_items"): @@ -485,16 +525,25 @@ class AssetCapitalization(StockController): ) def create_target_asset(self): + if ( + self.entry_type != "Capitalization" + or self.capitalization_method != "Create a new composite asset" + ): + return + total_target_asset_value = flt(self.total_value, self.precision("total_value")) + asset_doc = frappe.new_doc("Asset") asset_doc.company = self.company asset_doc.item_code = self.target_item_code - asset_doc.is_existing_asset = 1 + asset_doc.is_composite_asset = 1 asset_doc.location = self.target_asset_location asset_doc.available_for_use_date = self.posting_date asset_doc.purchase_date = self.posting_date asset_doc.gross_purchase_amount = total_target_asset_value asset_doc.purchase_receipt_amount = total_target_asset_value + asset_doc.purchase_receipt_amount = total_target_asset_value + asset_doc.capitalized_in = self.name asset_doc.flags.ignore_validate = True asset_doc.insert() @@ -510,6 +559,28 @@ class AssetCapitalization(StockController): ).format(get_link_to_form("Asset", asset_doc.name)) ) + def update_target_asset(self): + if ( + self.entry_type != "Capitalization" + or self.capitalization_method != "Choose a WIP composite asset" + ): + return + + total_target_asset_value = flt(self.total_value, self.precision("total_value")) + + asset_doc = frappe.get_doc("Asset", self.target_asset) + asset_doc.gross_purchase_amount = total_target_asset_value + asset_doc.purchase_receipt_amount = total_target_asset_value + asset_doc.capitalized_in = self.name + asset_doc.flags.ignore_validate = True + asset_doc.save() + + frappe.msgprint( + _( + "Asset {0} has been updated. Please set the depreciation details if any and submit it." + ).format(get_link_to_form("Asset", asset_doc.name)) + ) + def restore_consumed_asset_items(self): for item in self.asset_items: asset = frappe.get_doc("Asset", item.asset) @@ -568,6 +639,33 @@ def get_target_item_details(item_code=None, company=None): return out +@frappe.whitelist() +def get_target_asset_details(asset=None, company=None): + out = frappe._dict() + + # Get Asset Details + asset_details = frappe._dict() + if asset: + asset_details = frappe.db.get_value("Asset", asset, ["asset_name", "item_code"], as_dict=1) + if not asset_details: + frappe.throw(_("Asset {0} does not exist").format(asset)) + + # Re-set item code from Asset + out.target_item_code = asset_details.item_code + + # Set Asset Details + out.asset_name = asset_details.asset_name + + if asset_details.item_code: + out.target_fixed_asset_account = get_asset_category_account( + "fixed_asset_account", item=asset_details.item_code, company=company + ) + else: + out.target_fixed_asset_account = None + + return out + + @frappe.whitelist() def get_consumed_stock_item_details(args): if isinstance(args, string_types): @@ -716,3 +814,30 @@ def get_service_item_details(args): ) return out + + +@frappe.whitelist() +def get_items_tagged_to_wip_composite_asset(asset): + fields = [ + "item_code", + "item_name", + "batch_no", + "serial_no", + "stock_qty", + "stock_uom", + "warehouse", + "cost_center", + "qty", + "valuation_rate", + "amount", + ] + + pi_items = frappe.get_all( + "Purchase Invoice Item", filters={"wip_composite_asset": asset}, fields=fields + ) + + pr_items = frappe.get_all( + "Purchase Receipt Item", filters={"wip_composite_asset": asset}, fields=fields + ) + + return pi_items + pr_items diff --git a/erpnext/assets/doctype/asset_capitalization/test_asset_capitalization.py b/erpnext/assets/doctype/asset_capitalization/test_asset_capitalization.py index ead7abbf340..60c62d49afa 100644 --- a/erpnext/assets/doctype/asset_capitalization/test_asset_capitalization.py +++ b/erpnext/assets/doctype/asset_capitalization/test_asset_capitalization.py @@ -50,6 +50,7 @@ class TestAssetCapitalization(unittest.TestCase): # Create and submit Asset Captitalization asset_capitalization = create_asset_capitalization( entry_type="Capitalization", + capitalization_method="Create a new composite asset", target_item_code="Macbook Pro", target_asset_location="Test Location", stock_qty=stock_qty, @@ -139,6 +140,7 @@ class TestAssetCapitalization(unittest.TestCase): # Create and submit Asset Captitalization asset_capitalization = create_asset_capitalization( entry_type="Capitalization", + capitalization_method="Create a new composite asset", target_item_code="Macbook Pro", target_asset_location="Test Location", stock_qty=stock_qty, @@ -203,6 +205,77 @@ class TestAssetCapitalization(unittest.TestCase): self.assertFalse(get_actual_gle_dict(asset_capitalization.name)) self.assertFalse(get_actual_sle_dict(asset_capitalization.name)) + def test_capitalization_with_wip_composite_asset(self): + company = "_Test Company with perpetual inventory" + set_depreciation_settings_in_company(company=company) + + stock_rate = 1000 + stock_qty = 2 + stock_amount = 2000 + + total_amount = 2000 + + wip_composite_asset = create_asset( + asset_name="Asset Capitalization WIP Composite Asset", + is_composite_asset=1, + warehouse="Stores - TCP1", + company=company, + ) + + # Create and submit Asset Captitalization + asset_capitalization = create_asset_capitalization( + entry_type="Capitalization", + capitalization_method="Choose a WIP composite asset", + target_asset=wip_composite_asset, + target_asset_location="Test Location", + stock_qty=stock_qty, + stock_rate=stock_rate, + service_expense_account="Expenses Included In Asset Valuation - TCP1", + company=company, + submit=1, + ) + + # Test Asset Capitalization values + self.assertEqual(asset_capitalization.entry_type, "Capitalization") + self.assertEqual(asset_capitalization.capitalization_method, "Choose a WIP composite asset") + self.assertEqual(asset_capitalization.target_qty, 1) + + self.assertEqual(asset_capitalization.stock_items[0].valuation_rate, stock_rate) + self.assertEqual(asset_capitalization.stock_items[0].amount, stock_amount) + self.assertEqual(asset_capitalization.stock_items_total, stock_amount) + + self.assertEqual(asset_capitalization.total_value, total_amount) + self.assertEqual(asset_capitalization.target_incoming_rate, total_amount) + + # Test Target Asset values + target_asset = frappe.get_doc("Asset", asset_capitalization.target_asset) + self.assertEqual(target_asset.gross_purchase_amount, total_amount) + self.assertEqual(target_asset.purchase_receipt_amount, total_amount) + + # Test General Ledger Entries + expected_gle = { + "_Test Fixed Asset - TCP1": 2000, + "_Test Warehouse - TCP1": -2000, + } + actual_gle = get_actual_gle_dict(asset_capitalization.name) + + self.assertEqual(actual_gle, expected_gle) + + # Test Stock Ledger Entries + expected_sle = { + ("Capitalization Source Stock Item", "_Test Warehouse - TCP1"): { + "actual_qty": -stock_qty, + "stock_value_difference": -stock_amount, + } + } + actual_sle = get_actual_sle_dict(asset_capitalization.name) + self.assertEqual(actual_sle, expected_sle) + + # Cancel Asset Capitalization and make test entries and status are reversed + asset_capitalization.cancel() + self.assertFalse(get_actual_gle_dict(asset_capitalization.name)) + self.assertFalse(get_actual_sle_dict(asset_capitalization.name)) + def test_decapitalization_with_depreciation(self): # Variables purchase_date = "2020-01-01" @@ -326,6 +399,7 @@ def create_asset_capitalization(**args): asset_capitalization.update( { "entry_type": args.entry_type or "Capitalization", + "capitalization_method": args.capitalization_method or None, "company": company, "posting_date": args.posting_date or now.strftime("%Y-%m-%d"), "posting_time": args.posting_time or now.strftime("%H:%M:%S.%f"), diff --git a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.js b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.js index 312c166f8b7..8966fbcbb3c 100644 --- a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.js +++ b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.js @@ -35,6 +35,12 @@ frappe.ui.form.on("Purchase Receipt", { } }); + frm.set_query("wip_composite_asset", "items", function() { + return { + filters: {'is_composite_asset': 1, 'docstatus': 0 } + } + }); + frm.set_query("taxes_and_charges", function() { return { filters: {'company': frm.doc.company } diff --git a/erpnext/stock/doctype/purchase_receipt_item/purchase_receipt_item.json b/erpnext/stock/doctype/purchase_receipt_item/purchase_receipt_item.json index 76f476edf8a..4911523e7ed 100644 --- a/erpnext/stock/doctype/purchase_receipt_item/purchase_receipt_item.json +++ b/erpnext/stock/doctype/purchase_receipt_item/purchase_receipt_item.json @@ -110,6 +110,7 @@ "manufacturer_part_no", "accounting_details_section", "expense_account", + "wip_composite_asset", "column_break_102", "provisional_expense_account", "accounting_dimensions_section", @@ -1018,12 +1019,18 @@ "no_copy": 1, "print_hide": 1, "read_only": 1 + }, + { + "fieldname": "wip_composite_asset", + "fieldtype": "Link", + "label": "WIP Composite Asset", + "options": "Asset" } ], "idx": 1, "istable": 1, "links": [], - "modified": "2023-07-04 17:22:02.830029", + "modified": "2023-10-03 21:11:50.547261", "modified_by": "Administrator", "module": "Stock", "name": "Purchase Receipt Item", @@ -1034,4 +1041,4 @@ "sort_field": "modified", "sort_order": "DESC", "states": [] -} +} \ No newline at end of file From 2b38b780bab39d8e3cce62a77cae6fd0e971652b Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Wed, 4 Oct 2023 14:29:45 +0530 Subject: [PATCH 03/72] fix(Employee): enable `no_copy` for `relieving_date` (backport #37344) (#37358) Co-authored-by: Rucha Mahabal Co-authored-by: Jignesh (GreyCube Technologies) fix(Employee): enable `no_copy` for `relieving_date` (#37344) --- erpnext/setup/doctype/employee/employee.json | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/erpnext/setup/doctype/employee/employee.json b/erpnext/setup/doctype/employee/employee.json index 6cb4292226c..1143ccb7b10 100644 --- a/erpnext/setup/doctype/employee/employee.json +++ b/erpnext/setup/doctype/employee/employee.json @@ -616,6 +616,7 @@ "fieldname": "relieving_date", "fieldtype": "Date", "label": "Relieving Date", + "no_copy": 1, "mandatory_depends_on": "eval:doc.status == \"Left\"", "oldfieldname": "relieving_date", "oldfieldtype": "Date" @@ -822,7 +823,7 @@ "idx": 24, "image_field": "image", "links": [], - "modified": "2023-03-30 15:57:05.174592", + "modified": "2023-10-04 10:57:05.174592", "modified_by": "Administrator", "module": "Setup", "name": "Employee", @@ -870,4 +871,4 @@ "sort_order": "DESC", "states": [], "title_field": "employee_name" -} \ No newline at end of file +} From 67a43c353c3ec49be8c6422c47c55c97fe0d8ad1 Mon Sep 17 00:00:00 2001 From: anandbaburajan Date: Wed, 4 Oct 2023 22:11:15 +0530 Subject: [PATCH 04/72] test: fixing test_capitalization_with_wip_composite_asset (cherry picked from commit 9468513d7cf692624127d235129589b94a58d4b5) --- .../doctype/asset_capitalization/test_asset_capitalization.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/assets/doctype/asset_capitalization/test_asset_capitalization.py b/erpnext/assets/doctype/asset_capitalization/test_asset_capitalization.py index 60c62d49afa..59b65ec3fd0 100644 --- a/erpnext/assets/doctype/asset_capitalization/test_asset_capitalization.py +++ b/erpnext/assets/doctype/asset_capitalization/test_asset_capitalization.py @@ -226,7 +226,7 @@ class TestAssetCapitalization(unittest.TestCase): asset_capitalization = create_asset_capitalization( entry_type="Capitalization", capitalization_method="Choose a WIP composite asset", - target_asset=wip_composite_asset, + target_asset=wip_composite_asset.name, target_asset_location="Test Location", stock_qty=stock_qty, stock_rate=stock_rate, From 587a965bdf1ba0fc37e9015a7d2e5da7c47db29d Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Fri, 29 Sep 2023 13:40:59 +0530 Subject: [PATCH 05/72] refactor: block Payment Entry as ref in JE from UI (cherry picked from commit d391e8150557004164a12666a5b5b251e7756042) --- .../accounts/doctype/journal_entry/journal_entry.js | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/erpnext/accounts/doctype/journal_entry/journal_entry.js b/erpnext/accounts/doctype/journal_entry/journal_entry.js index 199068529d4..7719f0e7c90 100644 --- a/erpnext/accounts/doctype/journal_entry/journal_entry.js +++ b/erpnext/accounts/doctype/journal_entry/journal_entry.js @@ -51,7 +51,15 @@ frappe.ui.form.on("Journal Entry", { }, __('Make')); } }, - + before_save: function(frm) { + if ((frm.doc.docstatus == 0) && (!frm.doc.is_system_generated)) { + let payment_entry_references = frm.doc.accounts.filter(elem => (elem.reference_type == "Payment Entry")); + if (payment_entry_references.length > 0) { + let rows = payment_entry_references.map(x => "#"+x.idx); + frappe.throw(__("Rows: {0} have 'Payment Entry' as reference_type. This should not be set manually.", [frappe.utils.comma_and(rows)])); + } + } + }, make_inter_company_journal_entry: function(frm) { var d = new frappe.ui.Dialog({ title: __("Select Company"), From 98a9007e9f629ead141e05261011fbefb0f17918 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Wed, 4 Oct 2023 15:23:29 +0530 Subject: [PATCH 06/72] refactor: introduce access_key field (cherry picked from commit 81591a34c29977f78825a687a6cf206a4eb7855a) # Conflicts: # erpnext/accounts/doctype/currency_exchange_settings/currency_exchange_settings.json --- .../currency_exchange_settings.json | 20 +++++++++++++++++++ .../currency_exchange_settings.py | 10 ++++++++++ 2 files changed, 30 insertions(+) diff --git a/erpnext/accounts/doctype/currency_exchange_settings/currency_exchange_settings.json b/erpnext/accounts/doctype/currency_exchange_settings/currency_exchange_settings.json index 7921fcc2b96..dede6bd033d 100644 --- a/erpnext/accounts/doctype/currency_exchange_settings/currency_exchange_settings.json +++ b/erpnext/accounts/doctype/currency_exchange_settings/currency_exchange_settings.json @@ -8,6 +8,7 @@ "api_details_section", "service_provider", "api_endpoint", + "access_key", "url", "column_break_3", "help", @@ -77,12 +78,31 @@ "label": "Service Provider", "options": "frankfurter.app\nexchangerate.host\nCustom", "reqd": 1 +<<<<<<< HEAD +======= + }, + { + "default": "0", + "fieldname": "disabled", + "fieldtype": "Check", + "label": "Disabled" + }, + { + "depends_on": "eval:doc.service_provider == 'exchangerate.host';", + "fieldname": "access_key", + "fieldtype": "Data", + "label": "Access Key" +>>>>>>> 81591a34c2 (refactor: introduce access_key field) } ], "index_web_pages_for_search": 1, "issingle": 1, "links": [], +<<<<<<< HEAD "modified": "2022-01-10 15:51:14.521174", +======= + "modified": "2023-10-04 15:30:25.333860", +>>>>>>> 81591a34c2 (refactor: introduce access_key field) "modified_by": "Administrator", "module": "Accounts", "name": "Currency Exchange Settings", diff --git a/erpnext/accounts/doctype/currency_exchange_settings/currency_exchange_settings.py b/erpnext/accounts/doctype/currency_exchange_settings/currency_exchange_settings.py index d618c5ca117..117d5ff21e8 100644 --- a/erpnext/accounts/doctype/currency_exchange_settings/currency_exchange_settings.py +++ b/erpnext/accounts/doctype/currency_exchange_settings/currency_exchange_settings.py @@ -18,11 +18,21 @@ class CurrencyExchangeSettings(Document): def set_parameters_and_result(self): if self.service_provider == "exchangerate.host": + + if not self.access_key: + frappe.throw( + _("Access Key is required for Service Provider: {0}").format( + frappe.bold(self.service_provider) + ) + ) + self.set("result_key", []) self.set("req_params", []) self.api_endpoint = "https://api.exchangerate.host/convert" self.append("result_key", {"key": "result"}) + self.append("req_params", {"key": "access_key", "value": self.access_key}) + self.append("req_params", {"key": "amount", "value": "1"}) self.append("req_params", {"key": "date", "value": "{transaction_date}"}) self.append("req_params", {"key": "from", "value": "{from_currency}"}) self.append("req_params", {"key": "to", "value": "{to_currency}"}) From 04b8527ba8d2104078a5031d59eab5d9ce9b8198 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Wed, 4 Oct 2023 17:18:26 +0530 Subject: [PATCH 07/72] chore: refactor test case for exchangerate.host provider (cherry picked from commit c8e3dc6c4c6dbf248670fd553b39f6ba69232c2c) --- .../setup/doctype/currency_exchange/test_currency_exchange.py | 1 + 1 file changed, 1 insertion(+) diff --git a/erpnext/setup/doctype/currency_exchange/test_currency_exchange.py b/erpnext/setup/doctype/currency_exchange/test_currency_exchange.py index e3d281a5645..d4defdf88de 100644 --- a/erpnext/setup/doctype/currency_exchange/test_currency_exchange.py +++ b/erpnext/setup/doctype/currency_exchange/test_currency_exchange.py @@ -121,6 +121,7 @@ class TestCurrencyExchange(unittest.TestCase): # Update Currency Exchange Rate settings = frappe.get_single("Currency Exchange Settings") settings.service_provider = "exchangerate.host" + settings.access_key = "12345667890" settings.save() # Update exchange From 1ca0516fe549320838620f14dc0e7611a43350e7 Mon Sep 17 00:00:00 2001 From: Devin Slauenwhite Date: Mon, 16 Jan 2023 10:30:10 -0500 Subject: [PATCH 08/72] feat: disable currency exchange api. (#33593) (cherry picked from commit 179a31ed5e0688d90e8b95da44d980a8cb7c0323) --- .../currency_exchange_settings.json | 9 ++++++++- erpnext/setup/utils.py | 5 +++++ 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/erpnext/accounts/doctype/currency_exchange_settings/currency_exchange_settings.json b/erpnext/accounts/doctype/currency_exchange_settings/currency_exchange_settings.json index 7921fcc2b96..c62b711f2c2 100644 --- a/erpnext/accounts/doctype/currency_exchange_settings/currency_exchange_settings.json +++ b/erpnext/accounts/doctype/currency_exchange_settings/currency_exchange_settings.json @@ -6,6 +6,7 @@ "engine": "InnoDB", "field_order": [ "api_details_section", + "disabled", "service_provider", "api_endpoint", "url", @@ -77,12 +78,18 @@ "label": "Service Provider", "options": "frankfurter.app\nexchangerate.host\nCustom", "reqd": 1 + }, + { + "default": "0", + "fieldname": "disabled", + "fieldtype": "Check", + "label": "Disabled" } ], "index_web_pages_for_search": 1, "issingle": 1, "links": [], - "modified": "2022-01-10 15:51:14.521174", + "modified": "2023-01-09 12:19:03.955906", "modified_by": "Administrator", "module": "Accounts", "name": "Currency Exchange Settings", diff --git a/erpnext/setup/utils.py b/erpnext/setup/utils.py index 54bd8c355d6..bab57fe267a 100644 --- a/erpnext/setup/utils.py +++ b/erpnext/setup/utils.py @@ -81,6 +81,11 @@ def get_exchange_rate(from_currency, to_currency, transaction_date=None, args=No if entries: return flt(entries[0].exchange_rate) + if frappe.get_cached_value( + "Currency Exchange Settings", "Currency Exchange Settings", "disabled" + ): + return 0.00 + try: cache = frappe.cache() key = "currency_exchange_rate_{0}:{1}:{2}".format(transaction_date, from_currency, to_currency) From c1d40a6bfa74c48bc10d493a464a1f2bdd8a9bbd Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Thu, 5 Oct 2023 13:13:11 +0530 Subject: [PATCH 09/72] fix: fetch company details for Lead based quotation (cherry picked from commit f388864fd5a3ce95e0349d7fd37fb3878834262c) --- erpnext/crm/doctype/lead/lead.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/crm/doctype/lead/lead.py b/erpnext/crm/doctype/lead/lead.py index 460974972c5..294c41b9341 100644 --- a/erpnext/crm/doctype/lead/lead.py +++ b/erpnext/crm/doctype/lead/lead.py @@ -382,7 +382,7 @@ def get_lead_details(lead, posting_date=None, company=None): } ) - set_address_details(out, lead, "Lead") + set_address_details(out, lead, "Lead", company=company) taxes_and_charges = set_taxes( None, From 63f45739e05d728e8844320ff0c7dbafa8660acf Mon Sep 17 00:00:00 2001 From: rohitwaghchaure Date: Fri, 6 Oct 2023 11:58:15 +0530 Subject: [PATCH 10/72] fix: incorrect status of the returned purchase receipt (#37300) --- erpnext/controllers/taxes_and_totals.py | 4 ++- .../public/js/controllers/taxes_and_totals.js | 10 ++++++- .../purchase_receipt/purchase_receipt.py | 4 +++ .../purchase_receipt/test_purchase_receipt.py | 26 +++++++++++++++++++ 4 files changed, 42 insertions(+), 2 deletions(-) diff --git a/erpnext/controllers/taxes_and_totals.py b/erpnext/controllers/taxes_and_totals.py index 62d4c538682..95bf0e4688e 100644 --- a/erpnext/controllers/taxes_and_totals.py +++ b/erpnext/controllers/taxes_and_totals.py @@ -190,7 +190,9 @@ class calculate_taxes_and_totals(object): item.net_rate = item.rate - if not item.qty and self.doc.get("is_return"): + if ( + not item.qty and self.doc.get("is_return") and self.doc.get("doctype") != "Purchase Receipt" + ): item.amount = flt(-1 * item.rate, item.precision("amount")) elif not item.qty and self.doc.get("is_debit_note"): item.amount = flt(item.rate, item.precision("amount")) diff --git a/erpnext/public/js/controllers/taxes_and_totals.js b/erpnext/public/js/controllers/taxes_and_totals.js index 6f4e602abb6..b7ed22346b4 100644 --- a/erpnext/public/js/controllers/taxes_and_totals.js +++ b/erpnext/public/js/controllers/taxes_and_totals.js @@ -135,7 +135,15 @@ erpnext.taxes_and_totals = class TaxesAndTotals extends erpnext.payments { } else { // allow for '0' qty on Credit/Debit notes - let qty = item.qty || (me.frm.doc.is_debit_note ? 1 : -1); + let qty = flt(item.qty); + if (!qty) { + qty = (me.frm.doc.is_debit_note ? 1 : -1); + if (me.frm.doc.doctype !== "Purchase Receipt" && me.frm.doc.is_return === 1) { + // In case of Purchase Receipt, qty can be 0 if all items are rejected + qty = flt(item.qty); + } + } + item.net_amount = item.amount = flt(item.rate * qty, precision("amount", item)); } diff --git a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py index 1873efc711a..4a651cd0d18 100644 --- a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py +++ b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py @@ -958,6 +958,10 @@ def update_billing_percentage(pr_doc, update_modified=True, adjust_incoming_rate total_amount += total_billable_amount total_billed_amount += flt(item.billed_amt) + + if pr_doc.get("is_return") and not total_amount and total_billed_amount: + total_amount = total_billed_amount + if adjust_incoming_rate: adjusted_amt = 0.0 if item.billed_amt and item.amount: diff --git a/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py b/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py index 2f46809f49d..82694a0b192 100644 --- a/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py +++ b/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py @@ -2067,6 +2067,32 @@ class TestPurchaseReceipt(FrappeTestCase): company.enable_provisional_accounting_for_non_stock_items = 0 company.save() + def test_purchase_return_status_with_debit_note(self): + pr = make_purchase_receipt(rejected_qty=10, received_qty=10, rate=100, do_not_save=1) + pr.items[0].qty = 0 + pr.items[0].stock_qty = 0 + pr.submit() + + return_pr = make_purchase_receipt( + is_return=1, + return_against=pr.name, + qty=0, + rejected_qty=10 * -1, + received_qty=10 * -1, + do_not_save=1, + ) + return_pr.items[0].qty = 0.0 + return_pr.items[0].stock_qty = 0.0 + return_pr.submit() + + self.assertEqual(return_pr.status, "To Bill") + + pi = make_purchase_invoice(return_pr.name) + pi.submit() + + return_pr.reload() + self.assertEqual(return_pr.status, "Completed") + def prepare_data_for_internal_transfer(): from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_internal_supplier From 4c337a6f44127e474503862f3ff0d718c4eeffad Mon Sep 17 00:00:00 2001 From: rohitwaghchaure Date: Fri, 6 Oct 2023 13:06:13 +0530 Subject: [PATCH 11/72] fix: added validation for the batch on stock reco (#37174) --- .../stock_reconciliation.py | 8 ++++ .../test_stock_reconciliation.py | 44 ++++++++++++++++++- 2 files changed, 50 insertions(+), 2 deletions(-) diff --git a/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py b/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py index bb1a9b36214..9f2cab233af 100644 --- a/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py +++ b/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py @@ -167,6 +167,14 @@ class StockReconciliation(StockController): if flt(row.valuation_rate) < 0: self.validation_messages.append(_get_msg(row_num, _("Negative Valuation Rate is not allowed"))) + if row.batch_no and frappe.get_cached_value("Batch", row.batch_no, "item") != row.item_code: + self.validation_messages.append( + _get_msg( + row_num, + _("Batch {0} does not belong to item {1}").format(bold(row.batch_no), bold(row.item_code)), + ) + ) + if row.qty and row.valuation_rate in ["", None]: row.valuation_rate = get_stock_balance( row.item_code, row.warehouse, self.posting_date, self.posting_time, with_valuation_rate=True diff --git a/erpnext/stock/doctype/stock_reconciliation/test_stock_reconciliation.py b/erpnext/stock/doctype/stock_reconciliation/test_stock_reconciliation.py index df6777bbe4c..d1e5c5d345f 100644 --- a/erpnext/stock/doctype/stock_reconciliation/test_stock_reconciliation.py +++ b/erpnext/stock/doctype/stock_reconciliation/test_stock_reconciliation.py @@ -604,9 +604,9 @@ class TestStockReconciliation(FrappeTestCase, StockTestMixin): create_batch_item_with_batch("Testing Batch Item 1", "001") create_batch_item_with_batch("Testing Batch Item 2", "002") sr = create_stock_reconciliation( - item_code="Testing Batch Item 1", qty=1, rate=100, batch_no="002", do_not_submit=True + item_code="Testing Batch Item 1", qty=1, rate=100, batch_no="002", do_not_save=True ) - self.assertRaises(frappe.ValidationError, sr.submit) + self.assertRaises(frappe.ValidationError, sr.save) def test_serial_no_cancellation(self): from erpnext.stock.doctype.stock_entry.test_stock_entry import make_stock_entry @@ -916,6 +916,46 @@ class TestStockReconciliation(FrappeTestCase, StockTestMixin): # Check if Negative Stock is blocked self.assertRaises(frappe.ValidationError, sr.submit) + def test_batch_item_validation(self): + from erpnext.stock.doctype.stock_entry.test_stock_entry import make_stock_entry + + item_code = self.make_item( + "Test Batch Item Original", + { + "is_stock_item": 1, + "has_batch_no": 1, + "batch_number_series": "BNS9.####", + "create_new_batch": 1, + }, + ).name + + sr = make_stock_entry( + item_code=item_code, + target="_Test Warehouse - _TC", + qty=100, + basic_rate=100, + posting_date=nowdate(), + ) + + new_item_code = self.make_item( + "Test Batch Item New 1", + { + "is_stock_item": 1, + "has_batch_no": 1, + }, + ).name + + sr = create_stock_reconciliation( + item_code=new_item_code, + warehouse="_Test Warehouse - _TC", + qty=10, + rate=100, + batch_no=sr.items[0].batch_no, + do_not_save=True, + ) + + self.assertRaises(frappe.ValidationError, sr.save) + def create_batch_item_with_batch(item_name, batch_id): batch_item_doc = create_item(item_name, is_stock_item=1) From 1480acabb0faeae61c7c055bb7d1e81877b87cfb Mon Sep 17 00:00:00 2001 From: rohitwaghchaure Date: Fri, 6 Oct 2023 17:55:32 +0530 Subject: [PATCH 12/72] feat: validate negative stock for inventory dimension (#37373) * feat: validate negative stock for inventory dimension * test: test case for validate negative stock for inv dimension --- .../inventory_dimension.js | 2 +- .../inventory_dimension.json | 14 +++- .../inventory_dimension.py | 5 +- .../test_inventory_dimension.py | 67 ++++++++++++++++++ .../stock_ledger_entry/stock_ledger_entry.py | 69 ++++++++++++++++++- .../stock_reconciliation.py | 39 ++++++++++- erpnext/stock/stock_ledger.py | 20 +++++- erpnext/stock/utils.py | 9 ++- 8 files changed, 215 insertions(+), 10 deletions(-) diff --git a/erpnext/stock/doctype/inventory_dimension/inventory_dimension.js b/erpnext/stock/doctype/inventory_dimension/inventory_dimension.js index 0310682a2c1..35d1c02719c 100644 --- a/erpnext/stock/doctype/inventory_dimension/inventory_dimension.js +++ b/erpnext/stock/doctype/inventory_dimension/inventory_dimension.js @@ -37,7 +37,7 @@ frappe.ui.form.on('Inventory Dimension', { if (frm.doc.__onload && frm.doc.__onload.has_stock_ledger && frm.doc.__onload.has_stock_ledger.length) { let allow_to_edit_fields = ['disabled', 'fetch_from_parent', - 'type_of_transaction', 'condition', 'mandatory_depends_on']; + 'type_of_transaction', 'condition', 'mandatory_depends_on', 'validate_negative_stock']; frm.fields.forEach((field) => { if (!in_list(allow_to_edit_fields, field.df.fieldname)) { diff --git a/erpnext/stock/doctype/inventory_dimension/inventory_dimension.json b/erpnext/stock/doctype/inventory_dimension/inventory_dimension.json index eb6102a436e..0e4055251f0 100644 --- a/erpnext/stock/doctype/inventory_dimension/inventory_dimension.json +++ b/erpnext/stock/doctype/inventory_dimension/inventory_dimension.json @@ -17,6 +17,8 @@ "target_fieldname", "applicable_for_documents_tab", "apply_to_all_doctypes", + "column_break_niy2u", + "validate_negative_stock", "column_break_13", "document_type", "type_of_transaction", @@ -173,11 +175,21 @@ "fieldname": "reqd", "fieldtype": "Check", "label": "Mandatory" + }, + { + "fieldname": "column_break_niy2u", + "fieldtype": "Column Break" + }, + { + "default": "0", + "fieldname": "validate_negative_stock", + "fieldtype": "Check", + "label": "Validate Negative Stock" } ], "index_web_pages_for_search": 1, "links": [], - "modified": "2023-01-31 13:44:38.507698", + "modified": "2023-10-05 12:52:18.705431", "modified_by": "Administrator", "module": "Stock", "name": "Inventory Dimension", diff --git a/erpnext/stock/doctype/inventory_dimension/inventory_dimension.py b/erpnext/stock/doctype/inventory_dimension/inventory_dimension.py index 8bff4d51470..257d18fc33a 100644 --- a/erpnext/stock/doctype/inventory_dimension/inventory_dimension.py +++ b/erpnext/stock/doctype/inventory_dimension/inventory_dimension.py @@ -60,6 +60,7 @@ class InventoryDimension(Document): "fetch_from_parent", "type_of_transaction", "condition", + "validate_negative_stock", ] for field in frappe.get_meta("Inventory Dimension").fields: @@ -160,6 +161,7 @@ class InventoryDimension(Document): insert_after="inventory_dimension", options=self.reference_document, label=label, + search_index=1, reqd=self.reqd, mandatory_depends_on=self.mandatory_depends_on, ), @@ -255,7 +257,7 @@ def field_exists(doctype, fieldname) -> str or None: def get_inventory_documents( doctype=None, txt=None, searchfield=None, start=None, page_len=None, filters=None ): - and_filters = [["DocField", "parent", "not in", ["Batch", "Serial No"]]] + and_filters = [["DocField", "parent", "not in", ["Batch", "Serial No", "Item Price"]]] or_filters = [ ["DocField", "options", "in", ["Batch", "Serial No"]], ["DocField", "parent", "in", ["Putaway Rule"]], @@ -340,6 +342,7 @@ def get_inventory_dimensions(): fields=[ "distinct target_fieldname as fieldname", "reference_document as doctype", + "validate_negative_stock", ], filters={"disabled": 0}, ) diff --git a/erpnext/stock/doctype/inventory_dimension/test_inventory_dimension.py b/erpnext/stock/doctype/inventory_dimension/test_inventory_dimension.py index b1d7f8f00c6..531bc3f109f 100644 --- a/erpnext/stock/doctype/inventory_dimension/test_inventory_dimension.py +++ b/erpnext/stock/doctype/inventory_dimension/test_inventory_dimension.py @@ -414,6 +414,53 @@ class TestInventoryDimension(FrappeTestCase): else: self.assertEqual(d.store, "Inter Transfer Store 2") + def test_validate_negative_stock_for_inventory_dimension(self): + frappe.local.inventory_dimensions = {} + item_code = "Test Negative Inventory Dimension Item" + frappe.db.set_single_value("Stock Settings", "allow_negative_stock", 1) + create_item(item_code) + + inv_dimension = create_inventory_dimension( + apply_to_all_doctypes=1, + dimension_name="Inv Site", + reference_document="Inv Site", + document_type="Inv Site", + validate_negative_stock=1, + ) + + warehouse = create_warehouse("Negative Stock Warehouse") + doc = make_stock_entry(item_code=item_code, target=warehouse, qty=10, do_not_submit=True) + + doc.items[0].to_inv_site = "Site 1" + doc.submit() + + site_name = frappe.get_all( + "Stock Ledger Entry", filters={"voucher_no": doc.name, "is_cancelled": 0}, fields=["inv_site"] + )[0].inv_site + + self.assertEqual(site_name, "Site 1") + + doc = make_stock_entry(item_code=item_code, source=warehouse, qty=100, do_not_submit=True) + + doc.items[0].inv_site = "Site 1" + self.assertRaises(frappe.ValidationError, doc.submit) + + inv_dimension.reload() + inv_dimension.db_set("validate_negative_stock", 0) + frappe.local.inventory_dimensions = {} + + doc = make_stock_entry(item_code=item_code, source=warehouse, qty=100, do_not_submit=True) + + doc.items[0].inv_site = "Site 1" + doc.submit() + self.assertEqual(doc.docstatus, 1) + + site_name = frappe.get_all( + "Stock Ledger Entry", filters={"voucher_no": doc.name, "is_cancelled": 0}, fields=["inv_site"] + )[0].inv_site + + self.assertEqual(site_name, "Site 1") + def get_voucher_sl_entries(voucher_no, fields): return frappe.get_all( @@ -504,6 +551,26 @@ def prepare_test_data(): } ).insert(ignore_permissions=True) + if not frappe.db.exists("DocType", "Inv Site"): + frappe.get_doc( + { + "doctype": "DocType", + "name": "Inv Site", + "module": "Stock", + "custom": 1, + "naming_rule": "By fieldname", + "autoname": "field:site_name", + "fields": [{"label": "Site Name", "fieldname": "site_name", "fieldtype": "Data"}], + "permissions": [ + {"role": "System Manager", "permlevel": 0, "read": 1, "write": 1, "create": 1, "delete": 1} + ], + } + ).insert(ignore_permissions=True) + + for site in ["Site 1", "Site 2"]: + if not frappe.db.exists("Inv Site", site): + frappe.get_doc({"doctype": "Inv Site", "site_name": site}).insert(ignore_permissions=True) + def create_inventory_dimension(**args): args = frappe._dict(args) diff --git a/erpnext/stock/doctype/stock_ledger_entry/stock_ledger_entry.py b/erpnext/stock/doctype/stock_ledger_entry/stock_ledger_entry.py index 052f7781c13..921b04aab8c 100644 --- a/erpnext/stock/doctype/stock_ledger_entry/stock_ledger_entry.py +++ b/erpnext/stock/doctype/stock_ledger_entry/stock_ledger_entry.py @@ -5,13 +5,15 @@ from datetime import date import frappe -from frappe import _ +from frappe import _, bold from frappe.core.doctype.role.role import get_users from frappe.model.document import Document -from frappe.utils import add_days, cint, formatdate, get_datetime, getdate +from frappe.utils import add_days, cint, flt, formatdate, get_datetime, getdate from erpnext.accounts.utils import get_fiscal_year from erpnext.controllers.item_variant import ItemTemplateCannotHaveStock +from erpnext.stock.doctype.inventory_dimension.inventory_dimension import get_inventory_dimensions +from erpnext.stock.stock_ledger import get_previous_sle class StockFreezeError(frappe.ValidationError): @@ -48,6 +50,69 @@ class StockLedgerEntry(Document): self.validate_and_set_fiscal_year() self.block_transactions_against_group_warehouse() self.validate_with_last_transaction_posting_time() + self.validate_inventory_dimension_negative_stock() + + def validate_inventory_dimension_negative_stock(self): + extra_cond = "" + kwargs = {} + + dimensions = self._get_inventory_dimensions() + if not dimensions: + return + + for dimension, values in dimensions.items(): + kwargs[dimension] = values.get("value") + extra_cond += f" and {dimension} = %({dimension})s" + + kwargs.update( + { + "item_code": self.item_code, + "warehouse": self.warehouse, + "posting_date": self.posting_date, + "posting_time": self.posting_time, + "company": self.company, + } + ) + + sle = get_previous_sle(kwargs, extra_cond=extra_cond) + if sle: + flt_precision = cint(frappe.db.get_default("float_precision")) or 2 + diff = sle.qty_after_transaction + flt(self.actual_qty) + diff = flt(diff, flt_precision) + if diff < 0 and abs(diff) > 0.0001: + self.throw_validation_error(diff, dimensions) + + def throw_validation_error(self, diff, dimensions): + dimension_msg = _(", with the inventory {0}: {1}").format( + "dimensions" if len(dimensions) > 1 else "dimension", + ", ".join(f"{bold(d.doctype)} ({d.value})" for k, d in dimensions.items()), + ) + + msg = _( + "{0} units of {1} are required in {2}{3}, on {4} {5} for {6} to complete the transaction." + ).format( + abs(diff), + frappe.get_desk_link("Item", self.item_code), + frappe.get_desk_link("Warehouse", self.warehouse), + dimension_msg, + self.posting_date, + self.posting_time, + frappe.get_desk_link(self.voucher_type, self.voucher_no), + ) + + frappe.throw(msg, title=_("Inventory Dimension Negative Stock")) + + def _get_inventory_dimensions(self): + inv_dimensions = get_inventory_dimensions() + inv_dimension_dict = {} + for dimension in inv_dimensions: + if not dimension.get("validate_negative_stock") or not self.get(dimension.fieldname): + continue + + dimension["value"] = self.get(dimension.fieldname) + inv_dimension_dict.setdefault(dimension.fieldname, dimension) + + return inv_dimension_dict def on_submit(self): self.check_stock_frozen_date() diff --git a/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py b/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py index 9f2cab233af..e469291eac9 100644 --- a/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py +++ b/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py @@ -12,6 +12,7 @@ import erpnext from erpnext.accounts.utils import get_company_default from erpnext.controllers.stock_controller import StockController from erpnext.stock.doctype.batch.batch import get_batch_qty +from erpnext.stock.doctype.inventory_dimension.inventory_dimension import get_inventory_dimensions from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos from erpnext.stock.utils import get_stock_balance @@ -45,10 +46,22 @@ class StockReconciliation(StockController): self.clean_serial_nos() self.set_total_qty_and_amount() self.validate_putaway_capacity() + self.validate_inventory_dimension() if self._action == "submit": self.make_batches("warehouse") + def validate_inventory_dimension(self): + dimensions = get_inventory_dimensions() + for dimension in dimensions: + for row in self.items: + if not row.batch_no and row.current_qty and row.get(dimension.get("fieldname")): + frappe.throw( + _( + "Row #{0}: You cannot use the inventory dimension '{1}' in Stock Reconciliation to modify the quantity or valuation rate. Stock reconciliation with inventory dimensions is intended solely for performing opening entries." + ).format(row.idx, bold(dimension.get("doctype"))) + ) + def on_submit(self): self.update_stock_ledger() self.make_gl_entries() @@ -70,8 +83,19 @@ class StockReconciliation(StockController): self.difference_amount = 0.0 def _changed(item): + inventory_dimensions_dict = {} + if not item.batch_no and not item.serial_no: + for dimension in get_inventory_dimensions(): + if item.get(dimension.get("fieldname")): + inventory_dimensions_dict[dimension.get("fieldname")] = item.get(dimension.get("fieldname")) + item_dict = get_stock_balance_for( - item.item_code, item.warehouse, self.posting_date, self.posting_time, batch_no=item.batch_no + item.item_code, + item.warehouse, + self.posting_date, + self.posting_time, + batch_no=item.batch_no, + inventory_dimensions_dict=inventory_dimensions_dict, ) if ( @@ -423,6 +447,12 @@ class StockReconciliation(StockController): if not row.batch_no: data.qty_after_transaction = flt(row.qty, row.precision("qty")) + dimensions = get_inventory_dimensions() + has_dimensions = False + for dimension in dimensions: + if row.get(dimension.get("fieldname")): + has_dimensions = True + if self.docstatus == 2 and not row.batch_no: if row.current_qty: data.actual_qty = -1 * row.current_qty @@ -437,6 +467,11 @@ class StockReconciliation(StockController): data.valuation_rate = flt(row.valuation_rate) data.stock_value_difference = -1 * flt(row.amount_difference) + elif self.docstatus == 1 and has_dimensions and not row.batch_no: + data.actual_qty = row.qty + data.qty_after_transaction = 0.0 + data.incoming_rate = flt(row.valuation_rate) + self.update_inventory_dimensions(row, data) return data @@ -825,6 +860,7 @@ def get_stock_balance_for( posting_time, batch_no: Optional[str] = None, with_valuation_rate: bool = True, + inventory_dimensions_dict=None, ): frappe.has_permission("Stock Reconciliation", "write", throw=True) @@ -853,6 +889,7 @@ def get_stock_balance_for( posting_time, with_valuation_rate=with_valuation_rate, with_serial_no=has_serial_no, + inventory_dimensions_dict=inventory_dimensions_dict, ) if has_serial_no: diff --git a/erpnext/stock/stock_ledger.py b/erpnext/stock/stock_ledger.py index 8cd6325c9d3..108d36a490d 100644 --- a/erpnext/stock/stock_ledger.py +++ b/erpnext/stock/stock_ledger.py @@ -13,6 +13,7 @@ from frappe.utils import cint, cstr, flt, get_link_to_form, getdate, now, nowdat import erpnext from erpnext.stock.doctype.bin.bin import update_qty as update_bin_qty +from erpnext.stock.doctype.inventory_dimension.inventory_dimension import get_inventory_dimensions from erpnext.stock.utils import ( get_incoming_outgoing_rate_for_cancel, get_or_make_bin, @@ -582,6 +583,13 @@ class update_entries_after(object): ): sle.outgoing_rate = get_incoming_rate_for_inter_company_transfer(sle) + dimensions = get_inventory_dimensions() + has_dimensions = False + if dimensions: + for dimension in dimensions: + if sle.get(dimension.get("fieldname")): + has_dimensions = True + if get_serial_nos(sle.serial_no): self.get_serialized_values(sle) self.wh_data.qty_after_transaction += flt(sle.actual_qty) @@ -596,7 +604,7 @@ class update_entries_after(object): ): self.update_batched_values(sle) else: - if sle.voucher_type == "Stock Reconciliation" and not sle.batch_no: + if sle.voucher_type == "Stock Reconciliation" and not sle.batch_no and not has_dimensions: # assert self.wh_data.valuation_rate = sle.valuation_rate self.wh_data.qty_after_transaction = sle.qty_after_transaction @@ -1186,7 +1194,7 @@ def get_previous_sle_of_current_voucher(args, operator="<", exclude_current_vouc return sle[0] if sle else frappe._dict() -def get_previous_sle(args, for_update=False): +def get_previous_sle(args, for_update=False, extra_cond=None): """ get the last sle on or before the current time-bucket, to get actual qty before transaction, this function @@ -1201,7 +1209,9 @@ def get_previous_sle(args, for_update=False): } """ args["name"] = args.get("sle", None) or "" - sle = get_stock_ledger_entries(args, "<=", "desc", "limit 1", for_update=for_update) + sle = get_stock_ledger_entries( + args, "<=", "desc", "limit 1", for_update=for_update, extra_cond=extra_cond + ) return sle and sle[0] or {} @@ -1213,6 +1223,7 @@ def get_stock_ledger_entries( for_update=False, debug=False, check_serial_no=True, + extra_cond=None, ): """get stock ledger entries filtered by specific posting datetime conditions""" conditions = " and timestamp(posting_date, posting_time) {0} timestamp(%(posting_date)s, %(posting_time)s)".format( @@ -1250,6 +1261,9 @@ def get_stock_ledger_entries( if operator in (">", "<=") and previous_sle.get("name"): conditions += " and name!=%(name)s" + if extra_cond: + conditions += f"{extra_cond}" + return frappe.db.sql( """ select *, timestamp(posting_date, posting_time) as "timestamp" diff --git a/erpnext/stock/utils.py b/erpnext/stock/utils.py index a7e37d5961a..9f654fc6632 100644 --- a/erpnext/stock/utils.py +++ b/erpnext/stock/utils.py @@ -94,6 +94,7 @@ def get_stock_balance( posting_time=None, with_valuation_rate=False, with_serial_no=False, + inventory_dimensions_dict=None, ): """Returns stock balance quantity at given warehouse on given posting date or current date. @@ -113,7 +114,13 @@ def get_stock_balance( "posting_time": posting_time, } - last_entry = get_previous_sle(args) + extra_cond = "" + if inventory_dimensions_dict: + for field, value in inventory_dimensions_dict.items(): + args[field] = value + extra_cond += f" and {field} = %({field})s" + + last_entry = get_previous_sle(args, extra_cond=extra_cond) if with_valuation_rate: if with_serial_no: From 15d2024b8e8349562cae0f979d1d483d4d969d32 Mon Sep 17 00:00:00 2001 From: Gursheen Anand Date: Fri, 29 Sep 2023 11:12:19 +0530 Subject: [PATCH 13/72] fix: ageing summary in AR (cherry picked from commit d9eb44e62d9307f4100ca8e8b500309ca6da6922) --- .../process_statement_of_accounts.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.py b/erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.py index 3f8731afe65..1352c892a56 100644 --- a/erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.py +++ b/erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.py @@ -100,7 +100,8 @@ def set_ageing(doc, entry): "range2": 60, "range3": 90, "range4": 120, - "customer": entry.customer, + "party_type": "Customer", + "party": entry.customer, } ) col1, ageing = get_ageing(ageing_filters) From c29eab12dfe19cc3adfb41d9a0ffe287bd7e1164 Mon Sep 17 00:00:00 2001 From: Gursheen Anand Date: Fri, 29 Sep 2023 14:34:43 +0530 Subject: [PATCH 14/72] refactor: separate function for statement dict (cherry picked from commit 67f878ff8c49f0f2e3704aa7264b35fb3418a36e) # Conflicts: # erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.py --- .../process_statement_of_accounts.py | 24 +++++++++++++++++-- 1 file changed, 22 insertions(+), 2 deletions(-) diff --git a/erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.py b/erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.py index 1352c892a56..cc6a00c1954 100644 --- a/erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.py +++ b/erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.py @@ -47,6 +47,20 @@ class ProcessStatementOfAccounts(Document): def get_report_pdf(doc, consolidated=True): + statement_dict = get_statement_dict(doc) + if not bool(statement_dict): + return False + elif consolidated: + delimiter = '
' if doc.include_break else "" + result = delimiter.join(list(statement_dict.values())) + return get_pdf(result, {"orientation": doc.orientation}) + else: + for customer, statement_html in statement_dict.items(): + statement_dict[customer] = get_pdf(statement_html, {"orientation": doc.orientation}) + return statement_dict + + +def get_statement_dict(doc, get_statement_dict=False): statement_dict = {} ageing = "" @@ -77,8 +91,11 @@ def get_report_pdf(doc, consolidated=True): if not res: continue - statement_dict[entry.customer] = get_html(doc, filters, entry, col, res, ageing) + statement_dict[entry.customer] = ( + [res, ageing] if get_statement_dict else get_html(doc, filters, entry, col, res, ageing) + ) +<<<<<<< HEAD if not bool(statement_dict): return False elif consolidated: @@ -88,6 +105,9 @@ def get_report_pdf(doc, consolidated=True): for customer, statement_html in statement_dict.items(): statement_dict[customer] = get_pdf(statement_html, {"orientation": doc.orientation}) return statement_dict +======= + return statement_dict +>>>>>>> 67f878ff8c (refactor: separate function for statement dict) def set_ageing(doc, entry): @@ -101,7 +121,7 @@ def set_ageing(doc, entry): "range3": 90, "range4": 120, "party_type": "Customer", - "party": entry.customer, + "party": [entry.customer], } ) col1, ageing = get_ageing(ageing_filters) From 24b1100c8f007bb03e9fd89010ffc7487d30b71c Mon Sep 17 00:00:00 2001 From: Gursheen Anand Date: Fri, 29 Sep 2023 14:35:43 +0530 Subject: [PATCH 15/72] test: process soa for gl and ar (cherry picked from commit 644e25e587affcf2a5fb821f148e468b154c261a) --- .../test_process_statement_of_accounts.py | 100 +++++++++++++++--- 1 file changed, 84 insertions(+), 16 deletions(-) diff --git a/erpnext/accounts/doctype/process_statement_of_accounts/test_process_statement_of_accounts.py b/erpnext/accounts/doctype/process_statement_of_accounts/test_process_statement_of_accounts.py index fb0d8d152f0..a3a74df4029 100644 --- a/erpnext/accounts/doctype/process_statement_of_accounts/test_process_statement_of_accounts.py +++ b/erpnext/accounts/doctype/process_statement_of_accounts/test_process_statement_of_accounts.py @@ -4,39 +4,107 @@ import unittest import frappe +from frappe.tests.utils import FrappeTestCase from frappe.utils import add_days, getdate, today from erpnext.accounts.doctype.process_statement_of_accounts.process_statement_of_accounts import ( + get_statement_dict, send_emails, ) from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice +from erpnext.accounts.test.accounts_mixin import AccountsTestMixin -class TestProcessStatementOfAccounts(unittest.TestCase): +class TestProcessStatementOfAccounts(AccountsTestMixin, FrappeTestCase): def setUp(self): + self.create_company() + self.create_customer() + self.create_customer(customer_name="Other Customer") + self.clear_old_entries() self.si = create_sales_invoice() - self.process_soa = create_process_soa() + create_sales_invoice(customer="Other Customer") + + def test_process_soa_for_gl(self): + """Tests the utils for Statement of Accounts(General Ledger)""" + process_soa = create_process_soa( + name="_Test Process SOA for GL", + customers=[{"customer": "_Test Customer"}, {"customer": "Other Customer"}], + ) + statement_dict = get_statement_dict(process_soa, get_statement_dict=True) + + # Checks if the statements are filtered based on the Customer + self.assertIn("Other Customer", statement_dict) + self.assertIn("_Test Customer", statement_dict) + + # Checks if the correct number of receivable entries exist + # 3 rows for opening and closing and 1 row for SI + receivable_entries = statement_dict["_Test Customer"][0] + self.assertEqual(len(receivable_entries), 4) + + # Checks the amount for the receivable entry + self.assertEqual(receivable_entries[1].voucher_no, self.si.name) + self.assertEqual(receivable_entries[1].balance, 100) + + def test_process_soa_for_ar(self): + """Tests the utils for Statement of Accounts(Accounts Receivable)""" + process_soa = create_process_soa(name="_Test Process SOA for AR", report="Accounts Receivable") + statement_dict = get_statement_dict(process_soa, get_statement_dict=True) + + # Checks if the statements are filtered based on the Customer + self.assertNotIn("Other Customer", statement_dict) + self.assertIn("_Test Customer", statement_dict) + + # Checks if the correct number of receivable entries exist + receivable_entries = statement_dict["_Test Customer"][0] + self.assertEqual(len(receivable_entries), 1) + + # Checks the amount for the receivable entry + self.assertEqual(receivable_entries[0].voucher_no, self.si.name) + self.assertEqual(receivable_entries[0].total_due, 100) + + # Checks the ageing summary for AR + ageing_summary = statement_dict["_Test Customer"][1][0] + expected_summary = frappe._dict( + range1=100, + range2=0, + range3=0, + range4=0, + range5=0, + ) + self.check_ageing_summary(ageing_summary, expected_summary) def test_auto_email_for_process_soa_ar(self): - send_emails(self.process_soa.name, from_scheduler=True) - self.process_soa.load_from_db() - self.assertEqual(self.process_soa.posting_date, getdate(add_days(today(), 7))) + process_soa = create_process_soa( + name="_Test Process SOA", enable_auto_email=1, report="Accounts Receivable" + ) + send_emails(process_soa.name, from_scheduler=True) + process_soa.load_from_db() + self.assertEqual(process_soa.posting_date, getdate(add_days(today(), 7))) + + def check_ageing_summary(self, ageing, expected_ageing): + for age_range in expected_ageing: + self.assertEqual(expected_ageing[age_range], ageing.get(age_range)) def tearDown(self): - frappe.delete_doc_if_exists("Process Statement Of Accounts", "Test Process SOA") + frappe.db.rollback() -def create_process_soa(): - frappe.delete_doc_if_exists("Process Statement Of Accounts", "Test Process SOA") +def create_process_soa(**args): + args = frappe._dict(args) + frappe.delete_doc_if_exists("Process Statement Of Accounts", args.name) process_soa = frappe.new_doc("Process Statement Of Accounts") - soa_dict = { - "name": "Test Process SOA", - "company": "_Test Company", - } + soa_dict = frappe._dict( + name=args.name, + company=args.company or "_Test Company", + customers=args.customers or [{"customer": "_Test Customer"}], + enable_auto_email=1 if args.enable_auto_email else 0, + frequency=args.frequency or "Weekly", + report=args.report or "General Ledger", + from_date=args.from_date or getdate(today()), + to_date=args.to_date or getdate(today()), + posting_date=args.posting_date or getdate(today()), + include_ageing=1, + ) process_soa.update(soa_dict) - process_soa.set("customers", [{"customer": "_Test Customer"}]) - process_soa.enable_auto_email = 1 - process_soa.frequency = "Weekly" - process_soa.report = "Accounts Receivable" process_soa.save() return process_soa From 3f59518d01bb4fa81869fb10580f4da5bcf4bf19 Mon Sep 17 00:00:00 2001 From: Gursheen Kaur Anand <40693548+GursheenK@users.noreply.github.com> Date: Sat, 7 Oct 2023 15:21:50 +0530 Subject: [PATCH 16/72] chore: resolve conflicts --- .../process_statement_of_accounts.py | 14 +------------- 1 file changed, 1 insertion(+), 13 deletions(-) diff --git a/erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.py b/erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.py index cc6a00c1954..16ca7d457fb 100644 --- a/erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.py +++ b/erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.py @@ -94,20 +94,8 @@ def get_statement_dict(doc, get_statement_dict=False): statement_dict[entry.customer] = ( [res, ageing] if get_statement_dict else get_html(doc, filters, entry, col, res, ageing) ) - -<<<<<<< HEAD - if not bool(statement_dict): - return False - elif consolidated: - result = "".join(list(statement_dict.values())) - return get_pdf(result, {"orientation": doc.orientation}) - else: - for customer, statement_html in statement_dict.items(): - statement_dict[customer] = get_pdf(statement_html, {"orientation": doc.orientation}) - return statement_dict -======= + return statement_dict ->>>>>>> 67f878ff8c (refactor: separate function for statement dict) def set_ageing(doc, entry): From 77d719af6e146ff243c264fabf1cc329c393c293 Mon Sep 17 00:00:00 2001 From: Gursheen Kaur Anand <40693548+GursheenK@users.noreply.github.com> Date: Sat, 7 Oct 2023 15:40:27 +0530 Subject: [PATCH 17/72] chore: linting issues --- .../process_statement_of_accounts.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.py b/erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.py index 16ca7d457fb..b7d6827f64c 100644 --- a/erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.py +++ b/erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.py @@ -94,7 +94,7 @@ def get_statement_dict(doc, get_statement_dict=False): statement_dict[entry.customer] = ( [res, ageing] if get_statement_dict else get_html(doc, filters, entry, col, res, ageing) ) - + return statement_dict From e58b3b11e9aee1da3e8610d19112cdb30801a9fb Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Sun, 8 Oct 2023 13:16:28 +0530 Subject: [PATCH 18/72] fix: exception on exporting errored rows (cherry picked from commit d3c6000904387f64605fdb316412f1ee1948bd86) --- .../doctype/bank_statement_import/bank_statement_import.js | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/erpnext/accounts/doctype/bank_statement_import/bank_statement_import.js b/erpnext/accounts/doctype/bank_statement_import/bank_statement_import.js index 04af32346bb..a70af7a90e3 100644 --- a/erpnext/accounts/doctype/bank_statement_import/bank_statement_import.js +++ b/erpnext/accounts/doctype/bank_statement_import/bank_statement_import.js @@ -352,10 +352,11 @@ frappe.ui.form.on("Bank Statement Import", { export_errored_rows(frm) { open_url_post( - "/api/method/frappe.core.doctype.data_import.data_import.download_errored_template", + "/api/method/erpnext.accounts.doctype.bank_statement_import.bank_statement_import.download_errored_template", { data_import_name: frm.doc.name, - } + }, + true ); }, From ae8355c95391d0b4223a8c17a1f0fa786f45ed96 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Tue, 22 Aug 2023 10:26:42 +0530 Subject: [PATCH 19/72] feat: introduce unreconcile doctype (cherry picked from commit dc7162329594337ee5b869f42d44fb28d8eaf2e3) --- .../unreconcile_payment_entries/__init__.py | 0 .../unreconcile_payment_entries.json | 71 ++++++++++++++++++ .../unreconcile_payment_entries.py | 9 +++ .../doctype/unreconcile_payments/__init__.py | 0 .../test_unreconcile_payments.py | 9 +++ .../unreconcile_payments.js | 8 +++ .../unreconcile_payments.json | 72 +++++++++++++++++++ .../unreconcile_payments.py | 9 +++ 8 files changed, 178 insertions(+) create mode 100644 erpnext/accounts/doctype/unreconcile_payment_entries/__init__.py create mode 100644 erpnext/accounts/doctype/unreconcile_payment_entries/unreconcile_payment_entries.json create mode 100644 erpnext/accounts/doctype/unreconcile_payment_entries/unreconcile_payment_entries.py create mode 100644 erpnext/accounts/doctype/unreconcile_payments/__init__.py create mode 100644 erpnext/accounts/doctype/unreconcile_payments/test_unreconcile_payments.py create mode 100644 erpnext/accounts/doctype/unreconcile_payments/unreconcile_payments.js create mode 100644 erpnext/accounts/doctype/unreconcile_payments/unreconcile_payments.json create mode 100644 erpnext/accounts/doctype/unreconcile_payments/unreconcile_payments.py diff --git a/erpnext/accounts/doctype/unreconcile_payment_entries/__init__.py b/erpnext/accounts/doctype/unreconcile_payment_entries/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/erpnext/accounts/doctype/unreconcile_payment_entries/unreconcile_payment_entries.json b/erpnext/accounts/doctype/unreconcile_payment_entries/unreconcile_payment_entries.json new file mode 100644 index 00000000000..5beb39d0342 --- /dev/null +++ b/erpnext/accounts/doctype/unreconcile_payment_entries/unreconcile_payment_entries.json @@ -0,0 +1,71 @@ +{ + "actions": [], + "allow_rename": 1, + "creation": "2023-08-22 10:28:10.196712", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "voucher_type", + "voucher_no", + "reference_type", + "reference_name", + "allocated_amount", + "unlinked" + ], + "fields": [ + { + "fieldname": "voucher_type", + "fieldtype": "Link", + "in_list_view": 1, + "label": "Voucher Type", + "options": "DocType" + }, + { + "fieldname": "voucher_no", + "fieldtype": "Dynamic Link", + "in_list_view": 1, + "label": "Voucher No", + "options": "voucher_type" + }, + { + "fieldname": "reference_type", + "fieldtype": "Link", + "in_list_view": 1, + "label": "Reference Type", + "options": "DocType" + }, + { + "fieldname": "reference_name", + "fieldtype": "Dynamic Link", + "in_list_view": 1, + "label": "Reference Name", + "options": "reference_type" + }, + { + "fieldname": "allocated_amount", + "fieldtype": "Int", + "in_list_view": 1, + "label": "Allocated Amount" + }, + { + "default": "0", + "fieldname": "unlinked", + "fieldtype": "Check", + "in_list_view": 1, + "label": "Unlinked" + } + ], + "index_web_pages_for_search": 1, + "istable": 1, + "links": [], + "modified": "2023-08-22 11:22:20.381079", + "modified_by": "Administrator", + "module": "Accounts", + "name": "Unreconcile Payment Entries", + "owner": "Administrator", + "permissions": [], + "sort_field": "modified", + "sort_order": "DESC", + "states": [] +} \ No newline at end of file diff --git a/erpnext/accounts/doctype/unreconcile_payment_entries/unreconcile_payment_entries.py b/erpnext/accounts/doctype/unreconcile_payment_entries/unreconcile_payment_entries.py new file mode 100644 index 00000000000..c41545c2685 --- /dev/null +++ b/erpnext/accounts/doctype/unreconcile_payment_entries/unreconcile_payment_entries.py @@ -0,0 +1,9 @@ +# Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and contributors +# For license information, please see license.txt + +# import frappe +from frappe.model.document import Document + + +class UnreconcilePaymentEntries(Document): + pass diff --git a/erpnext/accounts/doctype/unreconcile_payments/__init__.py b/erpnext/accounts/doctype/unreconcile_payments/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/erpnext/accounts/doctype/unreconcile_payments/test_unreconcile_payments.py b/erpnext/accounts/doctype/unreconcile_payments/test_unreconcile_payments.py new file mode 100644 index 00000000000..85af5211aef --- /dev/null +++ b/erpnext/accounts/doctype/unreconcile_payments/test_unreconcile_payments.py @@ -0,0 +1,9 @@ +# Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and Contributors +# See license.txt + +# import frappe +from frappe.tests.utils import FrappeTestCase + + +class TestUnreconcilePayments(FrappeTestCase): + pass diff --git a/erpnext/accounts/doctype/unreconcile_payments/unreconcile_payments.js b/erpnext/accounts/doctype/unreconcile_payments/unreconcile_payments.js new file mode 100644 index 00000000000..d6670037d46 --- /dev/null +++ b/erpnext/accounts/doctype/unreconcile_payments/unreconcile_payments.js @@ -0,0 +1,8 @@ +// Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and contributors +// For license information, please see license.txt + +// frappe.ui.form.on("Unreconcile Payments", { +// refresh(frm) { + +// }, +// }); diff --git a/erpnext/accounts/doctype/unreconcile_payments/unreconcile_payments.json b/erpnext/accounts/doctype/unreconcile_payments/unreconcile_payments.json new file mode 100644 index 00000000000..c182a63b654 --- /dev/null +++ b/erpnext/accounts/doctype/unreconcile_payments/unreconcile_payments.json @@ -0,0 +1,72 @@ +{ + "actions": [], + "allow_rename": 1, + "autoname": "format:UNREC-{#####}", + "creation": "2023-08-22 10:26:34.421423", + "default_view": "List", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "company", + "entries", + "amended_from" + ], + "fields": [ + { + "fieldname": "amended_from", + "fieldtype": "Link", + "label": "Amended From", + "no_copy": 1, + "options": "Unreconcile Payments", + "print_hide": 1, + "read_only": 1 + }, + { + "fieldname": "company", + "fieldtype": "Link", + "label": "Company", + "options": "Company" + }, + { + "fieldname": "entries", + "fieldtype": "Table", + "label": "Entries", + "options": "Unreconcile Payment Entries" + } + ], + "index_web_pages_for_search": 1, + "is_submittable": 1, + "links": [], + "modified": "2023-08-22 11:07:03.854434", + "modified_by": "Administrator", + "module": "Accounts", + "name": "Unreconcile Payments", + "naming_rule": "Expression", + "owner": "Administrator", + "permissions": [ + { + "create": 1, + "delete": 1, + "read": 1, + "role": "Accounts Manager", + "select": 1, + "share": 1, + "submit": 1, + "write": 1 + }, + { + "create": 1, + "delete": 1, + "read": 1, + "role": "Accounts User", + "select": 1, + "share": 1, + "submit": 1, + "write": 1 + } + ], + "sort_field": "modified", + "sort_order": "DESC", + "states": [] +} \ No newline at end of file diff --git a/erpnext/accounts/doctype/unreconcile_payments/unreconcile_payments.py b/erpnext/accounts/doctype/unreconcile_payments/unreconcile_payments.py new file mode 100644 index 00000000000..96bcc009170 --- /dev/null +++ b/erpnext/accounts/doctype/unreconcile_payments/unreconcile_payments.py @@ -0,0 +1,9 @@ +# Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and contributors +# For license information, please see license.txt + +# import frappe +from frappe.model.document import Document + + +class UnreconcilePayments(Document): + pass From 77fa0f68df25d17b2ff711ce28e88a51950a06c4 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Tue, 22 Aug 2023 15:01:14 +0530 Subject: [PATCH 20/72] chore: working state on barebones functions (cherry picked from commit e48a90efe69f36dc455df3fefa8131384903e422) --- .../unreconcile_payment_entries.json | 36 ++++++------------- .../unreconcile_payments.js | 25 ++++++++++--- .../unreconcile_payments.json | 22 +++++++++--- .../unreconcile_payments.py | 33 +++++++++++++++-- 4 files changed, 80 insertions(+), 36 deletions(-) diff --git a/erpnext/accounts/doctype/unreconcile_payment_entries/unreconcile_payment_entries.json b/erpnext/accounts/doctype/unreconcile_payment_entries/unreconcile_payment_entries.json index 5beb39d0342..f70f4db2a8e 100644 --- a/erpnext/accounts/doctype/unreconcile_payment_entries/unreconcile_payment_entries.json +++ b/erpnext/accounts/doctype/unreconcile_payment_entries/unreconcile_payment_entries.json @@ -6,41 +6,18 @@ "editable_grid": 1, "engine": "InnoDB", "field_order": [ - "voucher_type", - "voucher_no", - "reference_type", + "reference_doctype", "reference_name", "allocated_amount", "unlinked" ], "fields": [ - { - "fieldname": "voucher_type", - "fieldtype": "Link", - "in_list_view": 1, - "label": "Voucher Type", - "options": "DocType" - }, - { - "fieldname": "voucher_no", - "fieldtype": "Dynamic Link", - "in_list_view": 1, - "label": "Voucher No", - "options": "voucher_type" - }, - { - "fieldname": "reference_type", - "fieldtype": "Link", - "in_list_view": 1, - "label": "Reference Type", - "options": "DocType" - }, { "fieldname": "reference_name", "fieldtype": "Dynamic Link", "in_list_view": 1, "label": "Reference Name", - "options": "reference_type" + "options": "reference_doctype" }, { "fieldname": "allocated_amount", @@ -54,12 +31,19 @@ "fieldtype": "Check", "in_list_view": 1, "label": "Unlinked" + }, + { + "fieldname": "reference_doctype", + "fieldtype": "Link", + "in_list_view": 1, + "label": "Reference Type", + "options": "DocType" } ], "index_web_pages_for_search": 1, "istable": 1, "links": [], - "modified": "2023-08-22 11:22:20.381079", + "modified": "2023-08-22 15:00:33.203161", "modified_by": "Administrator", "module": "Accounts", "name": "Unreconcile Payment Entries", diff --git a/erpnext/accounts/doctype/unreconcile_payments/unreconcile_payments.js b/erpnext/accounts/doctype/unreconcile_payments/unreconcile_payments.js index d6670037d46..03a8253dd2f 100644 --- a/erpnext/accounts/doctype/unreconcile_payments/unreconcile_payments.js +++ b/erpnext/accounts/doctype/unreconcile_payments/unreconcile_payments.js @@ -1,8 +1,25 @@ // Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and contributors // For license information, please see license.txt -// frappe.ui.form.on("Unreconcile Payments", { -// refresh(frm) { +frappe.ui.form.on("Unreconcile Payments", { + refresh(frm) { + frm.set_query("voucher_type", function() { + return { + filters: { + name: "Payment Entry" + } + } + }); -// }, -// }); + + frm.set_query("voucher_no", function(doc) { + return { + filters: { + company: doc.company, + docstatus: 1 + } + } + }); + + }, +}); diff --git a/erpnext/accounts/doctype/unreconcile_payments/unreconcile_payments.json b/erpnext/accounts/doctype/unreconcile_payments/unreconcile_payments.json index c182a63b654..f4b3cd70901 100644 --- a/erpnext/accounts/doctype/unreconcile_payments/unreconcile_payments.json +++ b/erpnext/accounts/doctype/unreconcile_payments/unreconcile_payments.json @@ -9,7 +9,9 @@ "engine": "InnoDB", "field_order": [ "company", - "entries", + "voucher_type", + "voucher_no", + "references", "amended_from" ], "fields": [ @@ -29,16 +31,28 @@ "options": "Company" }, { - "fieldname": "entries", + "fieldname": "voucher_type", + "fieldtype": "Link", + "label": "Voucher Type", + "options": "DocType" + }, + { + "fieldname": "voucher_no", + "fieldtype": "Dynamic Link", + "label": "Voucher No", + "options": "voucher_type" + }, + { + "fieldname": "references", "fieldtype": "Table", - "label": "Entries", + "label": "References", "options": "Unreconcile Payment Entries" } ], "index_web_pages_for_search": 1, "is_submittable": 1, "links": [], - "modified": "2023-08-22 11:07:03.854434", + "modified": "2023-08-22 14:11:13.073414", "modified_by": "Administrator", "module": "Accounts", "name": "Unreconcile Payments", diff --git a/erpnext/accounts/doctype/unreconcile_payments/unreconcile_payments.py b/erpnext/accounts/doctype/unreconcile_payments/unreconcile_payments.py index 96bcc009170..df08d79f01f 100644 --- a/erpnext/accounts/doctype/unreconcile_payments/unreconcile_payments.py +++ b/erpnext/accounts/doctype/unreconcile_payments/unreconcile_payments.py @@ -1,9 +1,38 @@ # Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and contributors # For license information, please see license.txt -# import frappe +import frappe from frappe.model.document import Document +from erpnext.accounts.utils import unlink_ref_doc_from_payment_entries, update_voucher_outstanding + class UnreconcilePayments(Document): - pass + def before_save(self): + if self.voucher_type == "Payment Entry": + references = frappe.db.get_all( + "Payment Entry Reference", + filters={"docstatus": 1, "parent": self.voucher_no}, + fields=["reference_doctype", "reference_name", "allocated_amount"], + ) + + self.set("references", []) + for ref in references: + self.append("references", ref) + + def on_submit(self): + payment_type, paid_from, paid_to, party_type, party = frappe.db.get_all( + self.voucher_type, + filters={"name": self.voucher_no}, + fields=["payment_type", "paid_from", "paid_to", "party_type", "party"], + as_list=1, + )[0] + account = paid_from if payment_type == "Receive" else paid_to + + for ref in self.references: + doc = frappe.get_doc(ref.reference_doctype, ref.reference_name) + unlink_ref_doc_from_payment_entries(doc) + update_voucher_outstanding( + ref.reference_doctype, ref.reference_name, account, party_type, party + ) + frappe.db.set_value("Unreconcile Payment Entries", ref.name, "unlinked", True) From b9647ac0a4974ed310f652e9af20acfef2ec2b28 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Thu, 24 Aug 2023 14:52:26 +0530 Subject: [PATCH 21/72] refactor: adding 'Get Allocations' button (cherry picked from commit 5114a9580db961a006d9b2f3c4dc08f207f374c7) --- .../unreconcile_payment_entries.json | 5 +- .../unreconcile_payments.js | 16 +++++++ .../unreconcile_payments.json | 14 ++++-- .../unreconcile_payments.py | 47 +++++++++++++------ 4 files changed, 62 insertions(+), 20 deletions(-) diff --git a/erpnext/accounts/doctype/unreconcile_payment_entries/unreconcile_payment_entries.json b/erpnext/accounts/doctype/unreconcile_payment_entries/unreconcile_payment_entries.json index f70f4db2a8e..c4afaa8bcac 100644 --- a/erpnext/accounts/doctype/unreconcile_payment_entries/unreconcile_payment_entries.json +++ b/erpnext/accounts/doctype/unreconcile_payment_entries/unreconcile_payment_entries.json @@ -30,7 +30,8 @@ "fieldname": "unlinked", "fieldtype": "Check", "in_list_view": 1, - "label": "Unlinked" + "label": "Unlinked", + "read_only": 1 }, { "fieldname": "reference_doctype", @@ -43,7 +44,7 @@ "index_web_pages_for_search": 1, "istable": 1, "links": [], - "modified": "2023-08-22 15:00:33.203161", + "modified": "2023-08-24 14:48:10.018574", "modified_by": "Administrator", "module": "Accounts", "name": "Unreconcile Payment Entries", diff --git a/erpnext/accounts/doctype/unreconcile_payments/unreconcile_payments.js b/erpnext/accounts/doctype/unreconcile_payments/unreconcile_payments.js index 03a8253dd2f..ef7c958113c 100644 --- a/erpnext/accounts/doctype/unreconcile_payments/unreconcile_payments.js +++ b/erpnext/accounts/doctype/unreconcile_payments/unreconcile_payments.js @@ -22,4 +22,20 @@ frappe.ui.form.on("Unreconcile Payments", { }); }, + get_allocations: function(frm) { + frm.clear_table("allocations"); + frappe.call({ + method: "get_allocations_from_payment", + doc: frm.doc, + callback: function(r) { + if (r.message) { + r.message.forEach(x => { + frm.add_child("allocations", x) + }) + frm.refresh_fields(); + } + } + }) + + } }); diff --git a/erpnext/accounts/doctype/unreconcile_payments/unreconcile_payments.json b/erpnext/accounts/doctype/unreconcile_payments/unreconcile_payments.json index f4b3cd70901..68af5dcc12a 100644 --- a/erpnext/accounts/doctype/unreconcile_payments/unreconcile_payments.json +++ b/erpnext/accounts/doctype/unreconcile_payments/unreconcile_payments.json @@ -11,7 +11,8 @@ "company", "voucher_type", "voucher_no", - "references", + "get_allocations", + "allocations", "amended_from" ], "fields": [ @@ -43,16 +44,21 @@ "options": "voucher_type" }, { - "fieldname": "references", + "fieldname": "get_allocations", + "fieldtype": "Button", + "label": "Get Allocations" + }, + { + "fieldname": "allocations", "fieldtype": "Table", - "label": "References", + "label": "Allocations", "options": "Unreconcile Payment Entries" } ], "index_web_pages_for_search": 1, "is_submittable": 1, "links": [], - "modified": "2023-08-22 14:11:13.073414", + "modified": "2023-08-24 16:53:50.767700", "modified_by": "Administrator", "module": "Accounts", "name": "Unreconcile Payments", diff --git a/erpnext/accounts/doctype/unreconcile_payments/unreconcile_payments.py b/erpnext/accounts/doctype/unreconcile_payments/unreconcile_payments.py index df08d79f01f..ab2cc718ada 100644 --- a/erpnext/accounts/doctype/unreconcile_payments/unreconcile_payments.py +++ b/erpnext/accounts/doctype/unreconcile_payments/unreconcile_payments.py @@ -2,25 +2,44 @@ # For license information, please see license.txt import frappe +from frappe import qb from frappe.model.document import Document +from frappe.query_builder.functions import Sum from erpnext.accounts.utils import unlink_ref_doc_from_payment_entries, update_voucher_outstanding class UnreconcilePayments(Document): - def before_save(self): - if self.voucher_type == "Payment Entry": - references = frappe.db.get_all( - "Payment Entry Reference", - filters={"docstatus": 1, "parent": self.voucher_no}, - fields=["reference_doctype", "reference_name", "allocated_amount"], - ) + # def validate(self): + # parent = set([alloc.parent for alloc in self.allocations]) + # if len(parent) != 1: + # pass - self.set("references", []) - for ref in references: - self.append("references", ref) + @frappe.whitelist() + def get_allocations_from_payment(self): + if self.voucher_type == "Payment Entry": + per = qb.DocType("Payment Entry Reference") + allocated_references = ( + qb.from_(per) + .select( + per.reference_doctype, per.reference_name, Sum(per.allocated_amount).as_("allocated_amount") + ) + .where((per.docstatus == 1) & (per.parent == self.voucher_no)) + .groupby(per.reference_name) + .run(as_dict=True) + ) + return allocated_references + + def add_references(self): + allocations = self.get_allocations_from_payment() + + for alloc in allocations: + self.append("allocations", alloc) def on_submit(self): + # todo: add more granular unlinking + # different amounts for same invoice should be individually unlinkable + payment_type, paid_from, paid_to, party_type, party = frappe.db.get_all( self.voucher_type, filters={"name": self.voucher_no}, @@ -29,10 +48,10 @@ class UnreconcilePayments(Document): )[0] account = paid_from if payment_type == "Receive" else paid_to - for ref in self.references: - doc = frappe.get_doc(ref.reference_doctype, ref.reference_name) + for alloc in self.allocations: + doc = frappe.get_doc(alloc.reference_doctype, alloc.reference_name) unlink_ref_doc_from_payment_entries(doc) update_voucher_outstanding( - ref.reference_doctype, ref.reference_name, account, party_type, party + alloc.reference_doctype, alloc.reference_name, account, party_type, party ) - frappe.db.set_value("Unreconcile Payment Entries", ref.name, "unlinked", True) + frappe.db.set_value("Unreconcile Payment Entries", alloc.name, "unlinked", True) From fb41f5f88c0e3d78bc0deb0069cdd7c8d8783dc4 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Thu, 24 Aug 2023 17:55:02 +0530 Subject: [PATCH 22/72] test: basic unreconcile function (cherry picked from commit 0faffaa8db495f94d8bbd673faac5d4acdcc58a4) --- .../test_unreconcile_payments.py | 98 ++++++++++++++++++- 1 file changed, 95 insertions(+), 3 deletions(-) diff --git a/erpnext/accounts/doctype/unreconcile_payments/test_unreconcile_payments.py b/erpnext/accounts/doctype/unreconcile_payments/test_unreconcile_payments.py index 85af5211aef..2bb8a54c350 100644 --- a/erpnext/accounts/doctype/unreconcile_payments/test_unreconcile_payments.py +++ b/erpnext/accounts/doctype/unreconcile_payments/test_unreconcile_payments.py @@ -1,9 +1,101 @@ # Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and Contributors # See license.txt -# import frappe +import frappe from frappe.tests.utils import FrappeTestCase +from frappe.utils import today + +from erpnext.accounts.doctype.payment_entry.test_payment_entry import create_payment_entry +from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice +from erpnext.accounts.test.accounts_mixin import AccountsTestMixin -class TestUnreconcilePayments(FrappeTestCase): - pass +class TestUnreconcilePayments(AccountsTestMixin, FrappeTestCase): + def setUp(self): + self.create_company() + self.create_customer() + self.create_item() + self.clear_old_entries() + + def tearDown(self): + frappe.db.rollback() + + def test_01_unreconcile_invoice(self): + si1 = create_sales_invoice( + item=self.item, + company=self.company, + customer=self.customer, + debit_to=self.debit_to, + posting_date=today(), + parent_cost_center=self.cost_center, + cost_center=self.cost_center, + rate=100, + price_list_rate=100, + ) + + si2 = create_sales_invoice( + item=self.item, + company=self.company, + customer=self.customer, + debit_to=self.debit_to, + posting_date=today(), + parent_cost_center=self.cost_center, + cost_center=self.cost_center, + rate=100, + price_list_rate=100, + ) + + pe = create_payment_entry( + company=self.company, + payment_type="Receive", + party_type="Customer", + party=self.customer, + paid_from=self.debit_to, + paid_to=self.cash, + paid_amount=200, + save=True, + ) + + pe.append( + "references", + {"reference_doctype": si1.doctype, "reference_name": si1.name, "allocated_amount": 100}, + ) + pe.append( + "references", + {"reference_doctype": si2.doctype, "reference_name": si2.name, "allocated_amount": 100}, + ) + # Allocation payment against both invoices + pe.save().submit() + + # Assert outstanding + si1.reload() + si2.reload() + self.assertEqual(si1.outstanding_amount, 0) + self.assertEqual(si2.outstanding_amount, 0) + + unreconcile = frappe.get_doc( + { + "doctype": "Unreconcile Payments", + "company": self.company, + "voucher_type": pe.doctype, + "voucher_no": pe.name, + } + ) + unreconcile.add_references() + self.assertEqual(len(unreconcile.allocations), 2) + allocations = [x.reference_name for x in unreconcile.allocations] + self.assertEquals([si1.name, si2.name], allocations) + # unreconcile si1 + for x in unreconcile.allocations: + if x.reference_name != si1.name: + unreconcile.remove(x) + unreconcile.save().submit() + + # Assert outstanding + si1.reload() + si2.reload() + self.assertEqual(si1.outstanding_amount, 100) + self.assertEqual(si2.outstanding_amount, 0) + + pe.reload() + self.assertEqual(len(pe.references), 1) From 9531a45b941e9481c5c1d0f5520c3fef4d6b1b09 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Sat, 26 Aug 2023 20:29:50 +0530 Subject: [PATCH 23/72] feat: UI for unreconcile (cherry picked from commit fc6be5bfb9a2cf1a79d0150fc5867ba0cb988f64) --- .../doctype/sales_invoice/sales_invoice.js | 44 +++++++++++++++++ .../unreconcile_payments.py | 49 ++++++++++++++++++- 2 files changed, 92 insertions(+), 1 deletion(-) diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.js b/erpnext/accounts/doctype/sales_invoice/sales_invoice.js index d6977d39a9f..7c41070580d 100644 --- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.js +++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.js @@ -177,6 +177,50 @@ erpnext.accounts.SalesInvoiceController = class SalesInvoiceController extends e }, __('Create')); } } + + if (doc.docstatus == 1) { + frappe.call({ + "method": "erpnext.accounts.doctype.unreconcile_payments.unreconcile_payments.doc_has_payments", + "args": { + "doctype": this.frm.doc.doctype, + "docname": this.frm.doc.name + }, + callback: function(r) { + if (r.message) { + me.frm.add_custom_button(__("Un-Reconcile"), function() { + me.unreconcile_prompt(); + }); + } + } + }); + } + } + + unreconcile_prompt() { + // get linked payments + let query_args = { + query:"erpnext.accounts.doctype.unreconcile_payments.unreconcile_payments.get_linked_payments_for_doc", + filters: { + doctype: this.frm.doc.doctype, + docname: this.frm.doc.name + } + } + + new frappe.ui.form.MultiSelectDialog({ + doctype: "Payment Ledger Entry", + target: this.cur_frm, + setters: { }, + add_filters_group: 0, + date_field: "posting_date", + columns: ["voucher_type", "voucher_no", "allocated_amount"], + get_query() { + return query_args; + }, + action(selections) { + console.log(selections); + } + }); + } make_maintenance_schedule() { diff --git a/erpnext/accounts/doctype/unreconcile_payments/unreconcile_payments.py b/erpnext/accounts/doctype/unreconcile_payments/unreconcile_payments.py index ab2cc718ada..ed978cbc376 100644 --- a/erpnext/accounts/doctype/unreconcile_payments/unreconcile_payments.py +++ b/erpnext/accounts/doctype/unreconcile_payments/unreconcile_payments.py @@ -4,7 +4,7 @@ import frappe from frappe import qb from frappe.model.document import Document -from frappe.query_builder.functions import Sum +from frappe.query_builder.functions import Abs, Sum from erpnext.accounts.utils import unlink_ref_doc_from_payment_entries, update_voucher_outstanding @@ -55,3 +55,50 @@ class UnreconcilePayments(Document): alloc.reference_doctype, alloc.reference_name, account, party_type, party ) frappe.db.set_value("Unreconcile Payment Entries", alloc.name, "unlinked", True) + + +@frappe.whitelist() +def doc_has_payments(doctype, docname): + if doctype in ["Sales Invoice", "Purchase Invoice"]: + return frappe.db.count( + "Payment Ledger Entry", + filters={"delinked": 0, "against_voucher_no": docname, "amount": ["<", 0]}, + ) + else: + return frappe.db.count( + "Payment Ledger Entry", + filters={"delinked": 0, "voucher_no": docname, "against_voucher_no": ["!=", docname]}, + ) + + +@frappe.whitelist() +def get_linked_payments_for_doc(doctype, txt, searchfield, start, page_len, filters): + if filters.get("doctype") and filters.get("docname"): + _dt = filters.get("doctype") + _dn = filters.get("docname") + ple = qb.DocType("Payment Ledger Entry") + if _dt in ["Sales Invoice", "Purchase Invoice"]: + res = ( + qb.from_(ple) + .select( + ple.voucher_type, + ple.voucher_no, + Abs(Sum(ple.amount_in_account_currency)).as_("allocated_amount"), + ) + .where((ple.delinked == 0) & (ple.against_voucher_no == _dn) & (ple.amount < 0)) + .groupby(ple.against_voucher_no) + .run(as_dict=True) + ) + return res + else: + return frappe.db.get_all( + "Payment Ledger Entry", + filters={ + "delinked": 0, + "voucher_no": _dn, + "against_voucher_no": ["!=", _dn], + "amount": ["<", 0], + }, + group_by="against_voucher_no", + fields=["against_voucher_type", "against_voucher_no", "Sum(amount_in_account_currency)"], + ) From cb35218eec8954c7419863c659d3176805a6f79d Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Sat, 26 Aug 2023 20:45:18 +0530 Subject: [PATCH 24/72] feat: filter on voucher no (cherry picked from commit 41eb2c9f5a2aeadab0fd2401cca86bd8302f7eb2) --- erpnext/accounts/doctype/sales_invoice/sales_invoice.js | 2 ++ .../doctype/unreconcile_payments/unreconcile_payments.py | 8 +++++++- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.js b/erpnext/accounts/doctype/sales_invoice/sales_invoice.js index 7c41070580d..850ca840fed 100644 --- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.js +++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.js @@ -213,6 +213,8 @@ erpnext.accounts.SalesInvoiceController = class SalesInvoiceController extends e add_filters_group: 0, date_field: "posting_date", columns: ["voucher_type", "voucher_no", "allocated_amount"], + primary_action_label: "Un-Reconcile", + title: "Un-Reconcile Payments", get_query() { return query_args; }, diff --git a/erpnext/accounts/doctype/unreconcile_payments/unreconcile_payments.py b/erpnext/accounts/doctype/unreconcile_payments/unreconcile_payments.py index ed978cbc376..dfd2d29e0f4 100644 --- a/erpnext/accounts/doctype/unreconcile_payments/unreconcile_payments.py +++ b/erpnext/accounts/doctype/unreconcile_payments/unreconcile_payments.py @@ -4,6 +4,7 @@ import frappe from frappe import qb from frappe.model.document import Document +from frappe.query_builder import Criterion from frappe.query_builder.functions import Abs, Sum from erpnext.accounts.utils import unlink_ref_doc_from_payment_entries, update_voucher_outstanding @@ -78,6 +79,11 @@ def get_linked_payments_for_doc(doctype, txt, searchfield, start, page_len, filt _dn = filters.get("docname") ple = qb.DocType("Payment Ledger Entry") if _dt in ["Sales Invoice", "Purchase Invoice"]: + criteria = [(ple.delinked == 0), (ple.against_voucher_no == _dn), (ple.amount < 0)] + + if txt: + criteria.append(ple.voucher_no.like(f"%{txt}%")) + res = ( qb.from_(ple) .select( @@ -85,7 +91,7 @@ def get_linked_payments_for_doc(doctype, txt, searchfield, start, page_len, filt ple.voucher_no, Abs(Sum(ple.amount_in_account_currency)).as_("allocated_amount"), ) - .where((ple.delinked == 0) & (ple.against_voucher_no == _dn) & (ple.amount < 0)) + .where(Criterion.all(criteria)) .groupby(ple.against_voucher_no) .run(as_dict=True) ) From 37fc82cd114181f62e5850095222fa963e719fee Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Mon, 28 Aug 2023 16:27:29 +0530 Subject: [PATCH 25/72] chore: delete references upon parent deletion (cherry picked from commit fbdfb8151c1f79fcc9b835a4ccc5c954e65b743f) --- erpnext/controllers/accounts_controller.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/erpnext/controllers/accounts_controller.py b/erpnext/controllers/accounts_controller.py index c7e21638bc7..d830ab7b414 100644 --- a/erpnext/controllers/accounts_controller.py +++ b/erpnext/controllers/accounts_controller.py @@ -218,6 +218,11 @@ class AccountsController(TransactionBase): (rpi.voucher_type == self.doctype) & (rpi.voucher_no == self.name) ).run() + upe = frappe.qb.DocType("UnReconcile Payment Entries") + frappe.qb.from_(upe).delete().where( + (upe.reference_doctype == self.doctype) & (upe.reference_name == self.name) + ).run() + # delete sl and gl entries on deletion of transaction if frappe.db.get_single_value("Accounts Settings", "delete_linked_ledger_entries"): ple = frappe.qb.DocType("Payment Ledger Entry") From 9422422dcc6ee0a71bc9539fa2cb91dd231feeb4 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Mon, 28 Aug 2023 17:36:12 +0530 Subject: [PATCH 26/72] refactor: remove references using framework (cherry picked from commit 42df0d3d6729a57953bc9cb0ef8622eae34829a0) --- .../doctype/sales_invoice/sales_invoice.js | 2 +- .../doctype/sales_invoice/sales_invoice.py | 2 ++ erpnext/controllers/accounts_controller.py | 27 ++++++++++++++++--- 3 files changed, 26 insertions(+), 5 deletions(-) diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.js b/erpnext/accounts/doctype/sales_invoice/sales_invoice.js index 850ca840fed..8b6b65a9fe7 100644 --- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.js +++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.js @@ -34,7 +34,7 @@ erpnext.accounts.SalesInvoiceController = class SalesInvoiceController extends e super.onload(); this.frm.ignore_doctypes_on_cancel_all = ['POS Invoice', 'Timesheet', 'POS Invoice Merge Log', - 'POS Closing Entry', 'Journal Entry', 'Payment Entry', "Repost Payment Ledger", "Repost Accounting Ledger"]; + 'POS Closing Entry', 'Journal Entry', 'Payment Entry', "Repost Payment Ledger", "Repost Accounting Ledger", "Unreconcile Payments", "Unreconcile Payment Entries"]; if(!this.frm.doc.__islocal && !this.frm.doc.customer && this.frm.doc.debit_to) { // show debit_to in print format diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py index 3f9fe0441d1..7d431b4205b 100644 --- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py +++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py @@ -401,6 +401,8 @@ class SalesInvoice(SellingController): "Repost Payment Ledger Items", "Repost Accounting Ledger", "Repost Accounting Ledger Items", + "Unreconcile Payments", + "Unreconcile Payment Entries", "Payment Ledger Entry", ) diff --git a/erpnext/controllers/accounts_controller.py b/erpnext/controllers/accounts_controller.py index d830ab7b414..792557b2b2a 100644 --- a/erpnext/controllers/accounts_controller.py +++ b/erpnext/controllers/accounts_controller.py @@ -211,6 +211,28 @@ class AccountsController(TransactionBase): def before_cancel(self): validate_einvoice_fields(self) + def _remove_references_in_unreconcile(self): + upe = frappe.qb.DocType("UnReconcile Payment Entries") + rows = ( + frappe.qb.from_(upe) + .select(upe.name, upe.parent) + .where((upe.reference_doctype == self.doctype) & (upe.reference_name == self.name)) + .run(as_dict=True) + ) + + references_map = frappe._dict() + for x in rows: + references_map.setdefault(x.parent, []).append(x.name) + + for doc, rows in references_map.items(): + unreconcile_doc = frappe.get_doc("Unreconcile Payments", doc) + for row in rows: + unreconcile_doc.remove(unreconcile_doc.get("allocations", {"name": row})[0]) + + unreconcile_doc.flags.ignore_validate_update_after_submit = True + unreconcile_doc.flags.ignore_links = True + unreconcile_doc.save(ignore_permissions=True) + def on_trash(self): # delete references in 'Repost Payment Ledger' rpi = frappe.qb.DocType("Repost Payment Ledger Items") @@ -218,10 +240,7 @@ class AccountsController(TransactionBase): (rpi.voucher_type == self.doctype) & (rpi.voucher_no == self.name) ).run() - upe = frappe.qb.DocType("UnReconcile Payment Entries") - frappe.qb.from_(upe).delete().where( - (upe.reference_doctype == self.doctype) & (upe.reference_name == self.name) - ).run() + self._remove_references_in_unreconcile() # delete sl and gl entries on deletion of transaction if frappe.db.get_single_value("Accounts Settings", "delete_linked_ledger_entries"): From 2fd500ce2642bd4caf0c93e6da3cccc7caeeab97 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Mon, 28 Aug 2023 17:43:05 +0530 Subject: [PATCH 27/72] chore: track changes (cherry picked from commit 489a545bbb1a814cb18164321e10a0d17042272c) --- .../doctype/unreconcile_payments/unreconcile_payments.json | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/erpnext/accounts/doctype/unreconcile_payments/unreconcile_payments.json b/erpnext/accounts/doctype/unreconcile_payments/unreconcile_payments.json index 68af5dcc12a..f29e61b6ef6 100644 --- a/erpnext/accounts/doctype/unreconcile_payments/unreconcile_payments.json +++ b/erpnext/accounts/doctype/unreconcile_payments/unreconcile_payments.json @@ -58,7 +58,7 @@ "index_web_pages_for_search": 1, "is_submittable": 1, "links": [], - "modified": "2023-08-24 16:53:50.767700", + "modified": "2023-08-28 17:42:50.261377", "modified_by": "Administrator", "module": "Accounts", "name": "Unreconcile Payments", @@ -88,5 +88,6 @@ ], "sort_field": "modified", "sort_order": "DESC", - "states": [] + "states": [], + "track_changes": 1 } \ No newline at end of file From 3a670264b24c0d57070c5530f039b56ed837a599 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Mon, 28 Aug 2023 17:49:09 +0530 Subject: [PATCH 28/72] chore: delete unreoncile doc upon parent doc deletion (cherry picked from commit 6bbe47c6714546114d77e34cbefbc7f30227050f) --- .../doctype/payment_entry/payment_entry.js | 2 +- .../doctype/payment_entry/payment_entry.py | 2 ++ erpnext/controllers/accounts_controller.py | 29 ++++++++++++------- 3 files changed, 22 insertions(+), 11 deletions(-) diff --git a/erpnext/accounts/doctype/payment_entry/payment_entry.js b/erpnext/accounts/doctype/payment_entry/payment_entry.js index d43a057db02..9f495e9fd8a 100644 --- a/erpnext/accounts/doctype/payment_entry/payment_entry.js +++ b/erpnext/accounts/doctype/payment_entry/payment_entry.js @@ -7,7 +7,7 @@ cur_frm.cscript.tax_table = "Advance Taxes and Charges"; frappe.ui.form.on('Payment Entry', { onload: function(frm) { - frm.ignore_doctypes_on_cancel_all = ['Sales Invoice', 'Purchase Invoice', 'Journal Entry', 'Repost Payment Ledger','Repost Accounting Ledger']; + frm.ignore_doctypes_on_cancel_all = ['Sales Invoice', 'Purchase Invoice', 'Journal Entry', 'Repost Payment Ledger','Repost Accounting Ledger', 'Unreconcile Payments', 'Unreconcile Payment Entries']; if(frm.doc.__islocal) { if (!frm.doc.paid_from) frm.set_value("paid_from_account_currency", null); diff --git a/erpnext/accounts/doctype/payment_entry/payment_entry.py b/erpnext/accounts/doctype/payment_entry/payment_entry.py index c5501a58306..35ab729339d 100644 --- a/erpnext/accounts/doctype/payment_entry/payment_entry.py +++ b/erpnext/accounts/doctype/payment_entry/payment_entry.py @@ -107,6 +107,8 @@ class PaymentEntry(AccountsController): "Repost Payment Ledger Items", "Repost Accounting Ledger", "Repost Accounting Ledger Items", + "Unreconcile Payments", + "Unreconcile Payment Entries", ) super(PaymentEntry, self).on_cancel() self.make_gl_entries(cancel=1) diff --git a/erpnext/controllers/accounts_controller.py b/erpnext/controllers/accounts_controller.py index 792557b2b2a..ffd8870553b 100644 --- a/erpnext/controllers/accounts_controller.py +++ b/erpnext/controllers/accounts_controller.py @@ -220,18 +220,27 @@ class AccountsController(TransactionBase): .run(as_dict=True) ) - references_map = frappe._dict() - for x in rows: - references_map.setdefault(x.parent, []).append(x.name) + if rows: + references_map = frappe._dict() + for x in rows: + references_map.setdefault(x.parent, []).append(x.name) - for doc, rows in references_map.items(): - unreconcile_doc = frappe.get_doc("Unreconcile Payments", doc) - for row in rows: - unreconcile_doc.remove(unreconcile_doc.get("allocations", {"name": row})[0]) + for doc, rows in references_map.items(): + unreconcile_doc = frappe.get_doc("Unreconcile Payments", doc) + for row in rows: + unreconcile_doc.remove(unreconcile_doc.get("allocations", {"name": row})[0]) - unreconcile_doc.flags.ignore_validate_update_after_submit = True - unreconcile_doc.flags.ignore_links = True - unreconcile_doc.save(ignore_permissions=True) + unreconcile_doc.flags.ignore_validate_update_after_submit = True + unreconcile_doc.flags.ignore_links = True + unreconcile_doc.save(ignore_permissions=True) + + # delete docs upon parent doc deletion + unreconcile_docs = frappe.db.get_all("Unreconcile Payments", filters={"voucher_no": self.name}) + for x in unreconcile_docs: + _doc = frappe.get_doc("Unreconcile Payments", x.name) + if _doc.docstatus == 1: + _doc.cancel() + _doc.delete() def on_trash(self): # delete references in 'Repost Payment Ledger' From b8865896572b2e50f402039338175653d1cf2c7a Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Tue, 29 Aug 2023 11:27:16 +0530 Subject: [PATCH 29/72] refactor: add UI elements (cherry picked from commit 58dc0e52e197e89653f0778fc434d86611616808) --- .../doctype/sales_invoice/sales_invoice.js | 71 +++++++++++++------ .../unreconcile_payments.py | 37 +++++++--- 2 files changed, 77 insertions(+), 31 deletions(-) diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.js b/erpnext/accounts/doctype/sales_invoice/sales_invoice.js index 8b6b65a9fe7..b682e910c15 100644 --- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.js +++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.js @@ -197,32 +197,57 @@ erpnext.accounts.SalesInvoiceController = class SalesInvoiceController extends e } unreconcile_prompt() { - // get linked payments - let query_args = { - query:"erpnext.accounts.doctype.unreconcile_payments.unreconcile_payments.get_linked_payments_for_doc", - filters: { - doctype: this.frm.doc.doctype, - docname: this.frm.doc.name - } - } - - new frappe.ui.form.MultiSelectDialog({ - doctype: "Payment Ledger Entry", - target: this.cur_frm, - setters: { }, - add_filters_group: 0, - date_field: "posting_date", - columns: ["voucher_type", "voucher_no", "allocated_amount"], - primary_action_label: "Un-Reconcile", - title: "Un-Reconcile Payments", - get_query() { - return query_args; + let child_table_fields = [ + { label: __("Voucher Type"), fieldname: "voucher_type", fieldtype: "Dynamic Link", options: "DocType", in_list_view: 1, read_only: 1}, + { label: __("Voucher No"), fieldname: "voucher_no", fieldtype: "Link", options: "voucher_type", in_list_view: 1, read_only: 1 }, + { label: __("Allocated Amount"), fieldname: "allocated_amount", fieldtype: "Float", in_list_view: 1, read_only: 1 }, + ] + let unreconcile_dialog_fields = [ + { + label: __('Allocations'), + fieldname: 'allocations', + fieldtype: 'Table', + read_only: 1, + fields: child_table_fields, }, - action(selections) { - console.log(selections); + ]; + + // get linked payments + frappe.call({ + "method": "erpnext.accounts.doctype.unreconcile_payments.unreconcile_payments.get_linked_payments_for_doc", + "args": { + "company": this.frm.doc.company, + "doctype": this.frm.doc.doctype, + "docname": this.frm.doc.name + }, + callback: function(r) { + if (r.message) { + // populate child table with allocations + unreconcile_dialog_fields[0].data = r.message; + unreconcile_dialog_fields[0].get_data = function(){ return r.message}; + + let d = new frappe.ui.Dialog({ + title: 'Un-Reconcile Allocations', + fields: unreconcile_dialog_fields, + size: 'large', + cannot_add_rows: 1, + primary_action_label: 'Un-Reconcile', + primary_action(values) { + + let selected_allocations = values.allocations.filter(x=>x.__checked); + if (selected_allocations.length > 0) { + // assuming each row is an individual voucher + // pass this to server side method that created unreconcile doc for row + } else { + frappe.msgprint("No Selection"); + } + } + }); + + d.show(); + } } }); - } make_maintenance_schedule() { diff --git a/erpnext/accounts/doctype/unreconcile_payments/unreconcile_payments.py b/erpnext/accounts/doctype/unreconcile_payments/unreconcile_payments.py index dfd2d29e0f4..cced2b3de49 100644 --- a/erpnext/accounts/doctype/unreconcile_payments/unreconcile_payments.py +++ b/erpnext/accounts/doctype/unreconcile_payments/unreconcile_payments.py @@ -73,20 +73,25 @@ def doc_has_payments(doctype, docname): @frappe.whitelist() -def get_linked_payments_for_doc(doctype, txt, searchfield, start, page_len, filters): - if filters.get("doctype") and filters.get("docname"): - _dt = filters.get("doctype") - _dn = filters.get("docname") +def get_linked_payments_for_doc( + company: str = None, doctype: str = None, docname: str = None +) -> list: + if company and doctype and docname: + _dt = doctype + _dn = docname ple = qb.DocType("Payment Ledger Entry") if _dt in ["Sales Invoice", "Purchase Invoice"]: - criteria = [(ple.delinked == 0), (ple.against_voucher_no == _dn), (ple.amount < 0)] - - if txt: - criteria.append(ple.voucher_no.like(f"%{txt}%")) + criteria = [ + (ple.delinked == 0), + (ple.against_voucher_no == _dn), + (ple.amount < 0), + (ple.company == company), + ] res = ( qb.from_(ple) .select( + ple.company, ple.voucher_type, ple.voucher_no, Abs(Sum(ple.amount_in_account_currency)).as_("allocated_amount"), @@ -108,3 +113,19 @@ def get_linked_payments_for_doc(doctype, txt, searchfield, start, page_len, filt group_by="against_voucher_no", fields=["against_voucher_type", "against_voucher_no", "Sum(amount_in_account_currency)"], ) + return [] + + +@frappe.whitelist() +def create_unreconcile_doc_for_selection( + company: str = None, dt: str = None, dn: str = None, selections: list = None +): + if selections: + # assuming each row is a unique voucher + for row in selections: + unrecon = frappe.new_doc("Unreconcile Payments") + unrecon.company = company + unrecon.voucher_type = dt + unrecon.voucher_type = dn + unrecon.add_references() + # remove unselected references From 1e93d0bcc4b0c7d75fb4b41855aa4909eaa59d42 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Tue, 29 Aug 2023 13:19:26 +0530 Subject: [PATCH 30/72] chore: move dialog building function to `utils.js` file (cherry picked from commit 5981c7e0ad1d80236654ceb4214b97a178fe5a05) --- .../doctype/sales_invoice/sales_invoice.js | 55 +----------------- erpnext/public/js/utils.js | 56 +++++++++++++++++++ 2 files changed, 57 insertions(+), 54 deletions(-) diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.js b/erpnext/accounts/doctype/sales_invoice/sales_invoice.js index b682e910c15..90ee7613929 100644 --- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.js +++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.js @@ -188,7 +188,7 @@ erpnext.accounts.SalesInvoiceController = class SalesInvoiceController extends e callback: function(r) { if (r.message) { me.frm.add_custom_button(__("Un-Reconcile"), function() { - me.unreconcile_prompt(); + erpnext.utils.build_unreconcile_dialog(cur_frm); }); } } @@ -196,59 +196,6 @@ erpnext.accounts.SalesInvoiceController = class SalesInvoiceController extends e } } - unreconcile_prompt() { - let child_table_fields = [ - { label: __("Voucher Type"), fieldname: "voucher_type", fieldtype: "Dynamic Link", options: "DocType", in_list_view: 1, read_only: 1}, - { label: __("Voucher No"), fieldname: "voucher_no", fieldtype: "Link", options: "voucher_type", in_list_view: 1, read_only: 1 }, - { label: __("Allocated Amount"), fieldname: "allocated_amount", fieldtype: "Float", in_list_view: 1, read_only: 1 }, - ] - let unreconcile_dialog_fields = [ - { - label: __('Allocations'), - fieldname: 'allocations', - fieldtype: 'Table', - read_only: 1, - fields: child_table_fields, - }, - ]; - - // get linked payments - frappe.call({ - "method": "erpnext.accounts.doctype.unreconcile_payments.unreconcile_payments.get_linked_payments_for_doc", - "args": { - "company": this.frm.doc.company, - "doctype": this.frm.doc.doctype, - "docname": this.frm.doc.name - }, - callback: function(r) { - if (r.message) { - // populate child table with allocations - unreconcile_dialog_fields[0].data = r.message; - unreconcile_dialog_fields[0].get_data = function(){ return r.message}; - - let d = new frappe.ui.Dialog({ - title: 'Un-Reconcile Allocations', - fields: unreconcile_dialog_fields, - size: 'large', - cannot_add_rows: 1, - primary_action_label: 'Un-Reconcile', - primary_action(values) { - - let selected_allocations = values.allocations.filter(x=>x.__checked); - if (selected_allocations.length > 0) { - // assuming each row is an individual voucher - // pass this to server side method that created unreconcile doc for row - } else { - frappe.msgprint("No Selection"); - } - } - }); - - d.show(); - } - } - }); - } make_maintenance_schedule() { frappe.model.open_mapped_doc({ diff --git a/erpnext/public/js/utils.js b/erpnext/public/js/utils.js index a6b4ea12bbe..14532286696 100755 --- a/erpnext/public/js/utils.js +++ b/erpnext/public/js/utils.js @@ -666,6 +666,62 @@ erpnext.utils.update_child_items = function(opts) { }).show(); } +erpnext.utils.build_unreconcile_dialog = function(frm) { + if (['Sales Invoice', 'Purchase Invoice', 'Payment Entry', 'Journal Entry'].includes(frm.doc.doctype)) { + let child_table_fields = [ + { label: __("Voucher Type"), fieldname: "voucher_type", fieldtype: "Dynamic Link", options: "DocType", in_list_view: 1, read_only: 1}, + { label: __("Voucher No"), fieldname: "voucher_no", fieldtype: "Link", options: "voucher_type", in_list_view: 1, read_only: 1 }, + { label: __("Allocated Amount"), fieldname: "allocated_amount", fieldtype: "Float", in_list_view: 1, read_only: 1 }, + ] + let unreconcile_dialog_fields = [ + { + label: __('Allocations'), + fieldname: 'allocations', + fieldtype: 'Table', + read_only: 1, + fields: child_table_fields, + }, + ]; + + // get linked payments + frappe.call({ + "method": "erpnext.accounts.doctype.unreconcile_payments.unreconcile_payments.get_linked_payments_for_doc", + "args": { + "company": frm.doc.company, + "doctype": frm.doc.doctype, + "docname": frm.doc.name + }, + callback: function(r) { + if (r.message) { + // populate child table with allocations + unreconcile_dialog_fields[0].data = r.message; + unreconcile_dialog_fields[0].get_data = function(){ return r.message}; + + let d = new frappe.ui.Dialog({ + title: 'Un-Reconcile Allocations', + fields: unreconcile_dialog_fields, + size: 'large', + cannot_add_rows: 1, + primary_action_label: 'Un-Reconcile', + primary_action(values) { + + let selected_allocations = values.allocations.filter(x=>x.__checked); + if (selected_allocations.length > 0) { + // assuming each row is an individual voucher + // pass this to server side method that created unreconcile doc for row + } else { + frappe.msgprint("No Selection"); + } + } + }); + + d.show(); + } + } + }); + } +} + erpnext.utils.map_current_doc = function(opts) { function _map() { if($.isArray(cur_frm.doc.items) && cur_frm.doc.items.length > 0) { From e464f5e419df0e9370d3581f8a95b7451facd001 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Tue, 29 Aug 2023 13:46:29 +0530 Subject: [PATCH 31/72] chore: move functions to a separate file in utils (cherry picked from commit 25fe75218578a44302f1335f8db0caa17d4d7608) # Conflicts: # erpnext/accounts/doctype/payment_entry/payment_entry.js # erpnext/public/js/erpnext.bundle.js --- .../doctype/payment_entry/payment_entry.js | 5 + .../doctype/sales_invoice/sales_invoice.js | 17 +-- .../unreconcile_payments.py | 19 +++- erpnext/public/js/erpnext.bundle.js | 5 + erpnext/public/js/utils.js | 53 --------- erpnext/public/js/utils/unreconcile.js | 106 ++++++++++++++++++ 6 files changed, 130 insertions(+), 75 deletions(-) create mode 100644 erpnext/public/js/utils/unreconcile.js diff --git a/erpnext/accounts/doctype/payment_entry/payment_entry.js b/erpnext/accounts/doctype/payment_entry/payment_entry.js index 9f495e9fd8a..48a18e95b7a 100644 --- a/erpnext/accounts/doctype/payment_entry/payment_entry.js +++ b/erpnext/accounts/doctype/payment_entry/payment_entry.js @@ -152,6 +152,11 @@ frappe.ui.form.on('Payment Entry', { frm.events.hide_unhide_fields(frm); frm.events.set_dynamic_labels(frm); frm.events.show_general_ledger(frm); +<<<<<<< HEAD +======= + erpnext.accounts.ledger_preview.show_accounting_ledger_preview(frm); + erpnext.accounts.unreconcile_payments.add_unreconcile_btn(frm); +>>>>>>> 25fe752185 (chore: move functions to a separate file in utils) }, validate_company: (frm) => { diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.js b/erpnext/accounts/doctype/sales_invoice/sales_invoice.js index 90ee7613929..a411889fbdd 100644 --- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.js +++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.js @@ -178,22 +178,7 @@ erpnext.accounts.SalesInvoiceController = class SalesInvoiceController extends e } } - if (doc.docstatus == 1) { - frappe.call({ - "method": "erpnext.accounts.doctype.unreconcile_payments.unreconcile_payments.doc_has_payments", - "args": { - "doctype": this.frm.doc.doctype, - "docname": this.frm.doc.name - }, - callback: function(r) { - if (r.message) { - me.frm.add_custom_button(__("Un-Reconcile"), function() { - erpnext.utils.build_unreconcile_dialog(cur_frm); - }); - } - } - }); - } + erpnext.accounts.unreconcile_payments.add_unreconcile_btn(me.frm); } diff --git a/erpnext/accounts/doctype/unreconcile_payments/unreconcile_payments.py b/erpnext/accounts/doctype/unreconcile_payments/unreconcile_payments.py index cced2b3de49..c80365b0ef0 100644 --- a/erpnext/accounts/doctype/unreconcile_payments/unreconcile_payments.py +++ b/erpnext/accounts/doctype/unreconcile_payments/unreconcile_payments.py @@ -117,15 +117,22 @@ def get_linked_payments_for_doc( @frappe.whitelist() -def create_unreconcile_doc_for_selection( - company: str = None, dt: str = None, dn: str = None, selections: list = None -): +def create_unreconcile_doc_for_selection(selections=None): if selections: + selections = frappe.json.loads(selections) # assuming each row is a unique voucher for row in selections: unrecon = frappe.new_doc("Unreconcile Payments") - unrecon.company = company - unrecon.voucher_type = dt - unrecon.voucher_type = dn + unrecon.company = row.get("company") + unrecon.voucher_type = row.get("voucher_type") + unrecon.voucher_no = row.get("voucher_no") unrecon.add_references() + # remove unselected references + unrecon.allocations = [ + x + for x in unrecon.allocations + if x.reference_doctype == row.get("against_voucher_type") + and x.reference_name == row.get("against_voucher_no") + ] + unrecon.save().submit() diff --git a/erpnext/public/js/erpnext.bundle.js b/erpnext/public/js/erpnext.bundle.js index 7b230af2699..5df6318a1cd 100644 --- a/erpnext/public/js/erpnext.bundle.js +++ b/erpnext/public/js/erpnext.bundle.js @@ -18,6 +18,11 @@ import "./utils/customer_quick_entry"; import "./utils/supplier_quick_entry"; import "./call_popup/call_popup"; import "./utils/dimension_tree_filter"; +<<<<<<< HEAD +======= +import "./utils/ledger_preview.js"; +import "./utils/unreconcile.js"; +>>>>>>> 25fe752185 (chore: move functions to a separate file in utils) import "./utils/barcode_scanner"; import "./telephony"; import "./templates/call_link.html"; diff --git a/erpnext/public/js/utils.js b/erpnext/public/js/utils.js index 14532286696..eafc1ed70e6 100755 --- a/erpnext/public/js/utils.js +++ b/erpnext/public/js/utils.js @@ -666,61 +666,8 @@ erpnext.utils.update_child_items = function(opts) { }).show(); } -erpnext.utils.build_unreconcile_dialog = function(frm) { - if (['Sales Invoice', 'Purchase Invoice', 'Payment Entry', 'Journal Entry'].includes(frm.doc.doctype)) { - let child_table_fields = [ - { label: __("Voucher Type"), fieldname: "voucher_type", fieldtype: "Dynamic Link", options: "DocType", in_list_view: 1, read_only: 1}, - { label: __("Voucher No"), fieldname: "voucher_no", fieldtype: "Link", options: "voucher_type", in_list_view: 1, read_only: 1 }, - { label: __("Allocated Amount"), fieldname: "allocated_amount", fieldtype: "Float", in_list_view: 1, read_only: 1 }, - ] - let unreconcile_dialog_fields = [ - { - label: __('Allocations'), - fieldname: 'allocations', - fieldtype: 'Table', - read_only: 1, - fields: child_table_fields, - }, - ]; - // get linked payments - frappe.call({ - "method": "erpnext.accounts.doctype.unreconcile_payments.unreconcile_payments.get_linked_payments_for_doc", - "args": { - "company": frm.doc.company, - "doctype": frm.doc.doctype, - "docname": frm.doc.name - }, - callback: function(r) { - if (r.message) { - // populate child table with allocations - unreconcile_dialog_fields[0].data = r.message; - unreconcile_dialog_fields[0].get_data = function(){ return r.message}; - let d = new frappe.ui.Dialog({ - title: 'Un-Reconcile Allocations', - fields: unreconcile_dialog_fields, - size: 'large', - cannot_add_rows: 1, - primary_action_label: 'Un-Reconcile', - primary_action(values) { - - let selected_allocations = values.allocations.filter(x=>x.__checked); - if (selected_allocations.length > 0) { - // assuming each row is an individual voucher - // pass this to server side method that created unreconcile doc for row - } else { - frappe.msgprint("No Selection"); - } - } - }); - - d.show(); - } - } - }); - } -} erpnext.utils.map_current_doc = function(opts) { function _map() { diff --git a/erpnext/public/js/utils/unreconcile.js b/erpnext/public/js/utils/unreconcile.js new file mode 100644 index 00000000000..509cd394100 --- /dev/null +++ b/erpnext/public/js/utils/unreconcile.js @@ -0,0 +1,106 @@ +frappe.provide('erpnext.accounts'); + +erpnext.accounts.unreconcile_payments = { + add_unreconcile_btn(frm) { + if (frm.doc.docstatus == 1) { + frappe.call({ + "method": "erpnext.accounts.doctype.unreconcile_payments.unreconcile_payments.doc_has_payments", + "args": { + "doctype": frm.doc.doctype, + "docname": frm.doc.name + }, + callback: function(r) { + if (r.message) { + frm.add_custom_button(__("Un-Reconcile"), function() { + erpnext.accounts.unreconcile_payments.build_unreconcile_dialog(frm); + }); + } + } + }); + } + }, + + build_unreconcile_dialog(frm) { + if (['Sales Invoice', 'Purchase Invoice', 'Payment Entry', 'Journal Entry'].includes(frm.doc.doctype)) { + let child_table_fields = [ + { label: __("Voucher Type"), fieldname: "voucher_type", fieldtype: "Dynamic Link", options: "DocType", in_list_view: 1, read_only: 1}, + { label: __("Voucher No"), fieldname: "voucher_no", fieldtype: "Link", options: "voucher_type", in_list_view: 1, read_only: 1 }, + { label: __("Allocated Amount"), fieldname: "allocated_amount", fieldtype: "Float", in_list_view: 1, read_only: 1 }, + ] + let unreconcile_dialog_fields = [ + { + label: __('Allocations'), + fieldname: 'allocations', + fieldtype: 'Table', + read_only: 1, + fields: child_table_fields, + }, + ]; + + // get linked payments + frappe.call({ + "method": "erpnext.accounts.doctype.unreconcile_payments.unreconcile_payments.get_linked_payments_for_doc", + "args": { + "company": frm.doc.company, + "doctype": frm.doc.doctype, + "docname": frm.doc.name + }, + callback: function(r) { + if (r.message) { + // populate child table with allocations + unreconcile_dialog_fields[0].data = r.message; + unreconcile_dialog_fields[0].get_data = function(){ return r.message}; + + let d = new frappe.ui.Dialog({ + title: 'Un-Reconcile Allocations', + fields: unreconcile_dialog_fields, + size: 'large', + cannot_add_rows: 1, + primary_action_label: 'Un-Reconcile', + primary_action(values) { + + let selected_allocations = values.allocations.filter(x=>x.__checked); + if (selected_allocations.length > 0) { + // assuming each row is an individual voucher + // pass this to server side method that creates unreconcile doc for each row + if (['Sales Invoice', 'Purchase Invoice'].includes(frm.doc.doctype)) { + let selection_map = selected_allocations.map(function(elem) { + return { + company: elem.company, + voucher_type: elem.voucher_type, + voucher_no: elem.voucher_no, + against_voucher_type: frm.doc.doctype, + against_voucher_no: frm.doc.name + }; + + }); + + erpnext.utils.create_unreconcile_docs(selection_map); + d.hide(); + } + + } else { + frappe.msgprint("No Selection"); + } + } + }); + + d.show(); + } + } + }); + } + }, + + create_unreconcile_docs(selection_map) { + frappe.call({ + "method": "erpnext.accounts.doctype.unreconcile_payments.unreconcile_payments.create_unreconcile_doc_for_selection", + "args": { + "selections": selection_map + }, + }); + } + + + +} From 7651ecbc2bb9672456c10dc79dd3f85a7abc34c5 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Tue, 29 Aug 2023 15:15:14 +0530 Subject: [PATCH 32/72] chore: fetch logic for payment entry (cherry picked from commit 1981f3837a10b5c0c2298a682190e0e6689a8b19) --- .../unreconcile_payments.py | 31 +++++++----- erpnext/public/js/utils/unreconcile.js | 48 ++++++++++++------- 2 files changed, 51 insertions(+), 28 deletions(-) diff --git a/erpnext/accounts/doctype/unreconcile_payments/unreconcile_payments.py b/erpnext/accounts/doctype/unreconcile_payments/unreconcile_payments.py index c80365b0ef0..b6dd363cea5 100644 --- a/erpnext/accounts/doctype/unreconcile_payments/unreconcile_payments.py +++ b/erpnext/accounts/doctype/unreconcile_payments/unreconcile_payments.py @@ -82,10 +82,10 @@ def get_linked_payments_for_doc( ple = qb.DocType("Payment Ledger Entry") if _dt in ["Sales Invoice", "Purchase Invoice"]: criteria = [ + (ple.company == company), (ple.delinked == 0), (ple.against_voucher_no == _dn), (ple.amount < 0), - (ple.company == company), ] res = ( @@ -102,17 +102,26 @@ def get_linked_payments_for_doc( ) return res else: - return frappe.db.get_all( - "Payment Ledger Entry", - filters={ - "delinked": 0, - "voucher_no": _dn, - "against_voucher_no": ["!=", _dn], - "amount": ["<", 0], - }, - group_by="against_voucher_no", - fields=["against_voucher_type", "against_voucher_no", "Sum(amount_in_account_currency)"], + criteria = [ + (ple.company == company), + (ple.delinked == 0), + (ple.voucher_no == _dn), + (ple.against_voucher_no != _dn), + ] + + query = ( + qb.from_(ple) + .select( + ple.company, + ple.against_voucher_type.as_("voucher_type"), + ple.against_voucher_no.as_("voucher_no"), + Abs(Sum(ple.amount_in_account_currency)).as_("allocated_amount"), + ) + .where(Criterion.all(criteria)) + .groupby(ple.against_voucher_no) ) + res = query.run(as_dict=True) + return res return [] diff --git a/erpnext/public/js/utils/unreconcile.js b/erpnext/public/js/utils/unreconcile.js index 509cd394100..46555fe2a2b 100644 --- a/erpnext/public/js/utils/unreconcile.js +++ b/erpnext/public/js/utils/unreconcile.js @@ -20,6 +20,34 @@ erpnext.accounts.unreconcile_payments = { } }, + build_selection_map(frm, selections) { + // assuming each row is an individual voucher + // pass this to server side method that creates unreconcile doc for each row + let selection_map = []; + if (['Sales Invoice', 'Purchase Invoice'].includes(frm.doc.doctype)) { + selection_map = selections.map(function(elem) { + return { + company: elem.company, + voucher_type: elem.voucher_type, + voucher_no: elem.voucher_no, + against_voucher_type: frm.doc.doctype, + against_voucher_no: frm.doc.name + }; + }); + } else if (['Payment Entry', 'Journal Entry'].includes(frm.doc.doctype)) { + selection_map = selections.map(function(elem) { + return { + company: elem.company, + voucher_type: frm.doc.doctype, + voucher_no: frm.doc.name, + against_voucher_type: elem.voucher_type, + against_voucher_no: elem.voucher_no, + }; + }); + } + return selection_map; + }, + build_unreconcile_dialog(frm) { if (['Sales Invoice', 'Purchase Invoice', 'Payment Entry', 'Journal Entry'].includes(frm.doc.doctype)) { let child_table_fields = [ @@ -61,23 +89,9 @@ erpnext.accounts.unreconcile_payments = { let selected_allocations = values.allocations.filter(x=>x.__checked); if (selected_allocations.length > 0) { - // assuming each row is an individual voucher - // pass this to server side method that creates unreconcile doc for each row - if (['Sales Invoice', 'Purchase Invoice'].includes(frm.doc.doctype)) { - let selection_map = selected_allocations.map(function(elem) { - return { - company: elem.company, - voucher_type: elem.voucher_type, - voucher_no: elem.voucher_no, - against_voucher_type: frm.doc.doctype, - against_voucher_no: frm.doc.name - }; - - }); - - erpnext.utils.create_unreconcile_docs(selection_map); - d.hide(); - } + let selection_map = erpnext.accounts.unreconcile_payments.build_selection_map(frm, selected_allocations); + erpnext.accounts.unreconcile_payments.create_unreconcile_docs(selection_map); + d.hide(); } else { frappe.msgprint("No Selection"); From f4e1959cc7d69d4872b31b530f62d4157bb33063 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Tue, 29 Aug 2023 21:45:17 +0530 Subject: [PATCH 33/72] chore: code cleanup (cherry picked from commit 69683776a5e46c32ba16664264f3aa9bc09a03f5) --- .../doctype/unreconcile_payments/unreconcile_payments.py | 5 ----- 1 file changed, 5 deletions(-) diff --git a/erpnext/accounts/doctype/unreconcile_payments/unreconcile_payments.py b/erpnext/accounts/doctype/unreconcile_payments/unreconcile_payments.py index b6dd363cea5..01f910e5646 100644 --- a/erpnext/accounts/doctype/unreconcile_payments/unreconcile_payments.py +++ b/erpnext/accounts/doctype/unreconcile_payments/unreconcile_payments.py @@ -11,11 +11,6 @@ from erpnext.accounts.utils import unlink_ref_doc_from_payment_entries, update_v class UnreconcilePayments(Document): - # def validate(self): - # parent = set([alloc.parent for alloc in self.allocations]) - # if len(parent) != 1: - # pass - @frappe.whitelist() def get_allocations_from_payment(self): if self.voucher_type == "Payment Entry": From 84e4a2509cc08e59c5c5a857cffb8754e90688f6 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Tue, 29 Aug 2023 21:49:21 +0530 Subject: [PATCH 34/72] chore: rename and add trigger in journal entry (cherry picked from commit 0ccb6d8242c8bcb44457745553124629e4dc5434) --- erpnext/accounts/doctype/journal_entry/journal_entry.js | 2 ++ .../doctype/unreconcile_payments/unreconcile_payments.py | 2 +- erpnext/public/js/utils/unreconcile.js | 2 +- 3 files changed, 4 insertions(+), 2 deletions(-) diff --git a/erpnext/accounts/doctype/journal_entry/journal_entry.js b/erpnext/accounts/doctype/journal_entry/journal_entry.js index 7719f0e7c90..fe570a5ba8a 100644 --- a/erpnext/accounts/doctype/journal_entry/journal_entry.js +++ b/erpnext/accounts/doctype/journal_entry/journal_entry.js @@ -50,6 +50,8 @@ frappe.ui.form.on("Journal Entry", { frm.trigger("make_inter_company_journal_entry"); }, __('Make')); } + + erpnext.accounts.unreconcile_payments.add_unreconcile_btn(frm); }, before_save: function(frm) { if ((frm.doc.docstatus == 0) && (!frm.doc.is_system_generated)) { diff --git a/erpnext/accounts/doctype/unreconcile_payments/unreconcile_payments.py b/erpnext/accounts/doctype/unreconcile_payments/unreconcile_payments.py index 01f910e5646..9b80c0a3f85 100644 --- a/erpnext/accounts/doctype/unreconcile_payments/unreconcile_payments.py +++ b/erpnext/accounts/doctype/unreconcile_payments/unreconcile_payments.py @@ -54,7 +54,7 @@ class UnreconcilePayments(Document): @frappe.whitelist() -def doc_has_payments(doctype, docname): +def doc_has_references(doctype, docname): if doctype in ["Sales Invoice", "Purchase Invoice"]: return frappe.db.count( "Payment Ledger Entry", diff --git a/erpnext/public/js/utils/unreconcile.js b/erpnext/public/js/utils/unreconcile.js index 46555fe2a2b..df07643bb7c 100644 --- a/erpnext/public/js/utils/unreconcile.js +++ b/erpnext/public/js/utils/unreconcile.js @@ -4,7 +4,7 @@ erpnext.accounts.unreconcile_payments = { add_unreconcile_btn(frm) { if (frm.doc.docstatus == 1) { frappe.call({ - "method": "erpnext.accounts.doctype.unreconcile_payments.unreconcile_payments.doc_has_payments", + "method": "erpnext.accounts.doctype.unreconcile_payments.unreconcile_payments.doc_has_references", "args": { "doctype": frm.doc.doctype, "docname": frm.doc.name From 1a69db0f80b3d287e95bf1d34c8d6554718c5f7a Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Wed, 30 Aug 2023 10:02:47 +0530 Subject: [PATCH 35/72] refactor: modularisation and group by voucher_no (cherry picked from commit cce96669f0b651795522ac350319993df1d482e9) --- .../unreconcile_payments.py | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/erpnext/accounts/doctype/unreconcile_payments/unreconcile_payments.py b/erpnext/accounts/doctype/unreconcile_payments/unreconcile_payments.py index 9b80c0a3f85..8aef772ad58 100644 --- a/erpnext/accounts/doctype/unreconcile_payments/unreconcile_payments.py +++ b/erpnext/accounts/doctype/unreconcile_payments/unreconcile_payments.py @@ -13,6 +13,7 @@ from erpnext.accounts.utils import unlink_ref_doc_from_payment_entries, update_v class UnreconcilePayments(Document): @frappe.whitelist() def get_allocations_from_payment(self): + allocated_references = [] if self.voucher_type == "Payment Entry": per = qb.DocType("Payment Entry Reference") allocated_references = ( @@ -24,7 +25,19 @@ class UnreconcilePayments(Document): .groupby(per.reference_name) .run(as_dict=True) ) - return allocated_references + elif self.voucher_type == "Journal Entry": + jea = qb.DocType("Journal Entry Account") + allocated_references = ( + qb.from_(jea) + .select( + jea.reference_type, jea.reference_name, Sum(jea.allocated_amount).as_("allocated_amount") + ) + .where((jea.docstatus == 1) & (jea.parent == self.voucher_no)) + .groupby(jea.reference_name) + .run(as_dict=True) + ) + + return allocated_references def add_references(self): allocations = self.get_allocations_from_payment() @@ -92,7 +105,7 @@ def get_linked_payments_for_doc( Abs(Sum(ple.amount_in_account_currency)).as_("allocated_amount"), ) .where(Criterion.all(criteria)) - .groupby(ple.against_voucher_no) + .groupby(ple.voucher_no, ple.against_voucher_no) .run(as_dict=True) ) return res From cd2d335256ede5980a0757a80fabc5140adc3d10 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Wed, 30 Aug 2023 10:43:00 +0530 Subject: [PATCH 36/72] feat: unreconcile support for journal entry (cherry picked from commit 285963acdba73bfdb0f9e5d7f4fac3d765b282d6) --- .../unreconcile_payments.js | 2 +- .../unreconcile_payments.py | 28 +++++++++++++++---- 2 files changed, 23 insertions(+), 7 deletions(-) diff --git a/erpnext/accounts/doctype/unreconcile_payments/unreconcile_payments.js b/erpnext/accounts/doctype/unreconcile_payments/unreconcile_payments.js index ef7c958113c..c522567637f 100644 --- a/erpnext/accounts/doctype/unreconcile_payments/unreconcile_payments.js +++ b/erpnext/accounts/doctype/unreconcile_payments/unreconcile_payments.js @@ -6,7 +6,7 @@ frappe.ui.form.on("Unreconcile Payments", { frm.set_query("voucher_type", function() { return { filters: { - name: "Payment Entry" + name: ["in", ["Payment Entry", "Journal Entry"]] } } }); diff --git a/erpnext/accounts/doctype/unreconcile_payments/unreconcile_payments.py b/erpnext/accounts/doctype/unreconcile_payments/unreconcile_payments.py index 8aef772ad58..a32313f4a5c 100644 --- a/erpnext/accounts/doctype/unreconcile_payments/unreconcile_payments.py +++ b/erpnext/accounts/doctype/unreconcile_payments/unreconcile_payments.py @@ -2,15 +2,21 @@ # For license information, please see license.txt import frappe -from frappe import qb +from frappe import _, qb from frappe.model.document import Document from frappe.query_builder import Criterion from frappe.query_builder.functions import Abs, Sum +from frappe.utils.data import comma_and from erpnext.accounts.utils import unlink_ref_doc_from_payment_entries, update_voucher_outstanding class UnreconcilePayments(Document): + def validate(self): + self.supported_types = ["Payment Entry", "Journal Entry"] + if not self.voucher_type in self.supported_types: + frappe.throw(_("Only {0} are supported").format(comma_and(self.supported_types))) + @frappe.whitelist() def get_allocations_from_payment(self): allocated_references = [] @@ -26,14 +32,24 @@ class UnreconcilePayments(Document): .run(as_dict=True) ) elif self.voucher_type == "Journal Entry": - jea = qb.DocType("Journal Entry Account") + # for journals, using payment ledger to fetch allocation. + # this way we can avoid vaildating account type and reference details individually on child table + + ple = qb.DocType("Payment Ledger Entry") allocated_references = ( - qb.from_(jea) + qb.from_(ple) .select( - jea.reference_type, jea.reference_name, Sum(jea.allocated_amount).as_("allocated_amount") + ple.against_voucher_type.as_("reference_doctype"), + ple.against_voucher_no.as_("reference_name"), + Abs(Sum(ple.amount_in_account_currency)).as_("allocated_amount"), ) - .where((jea.docstatus == 1) & (jea.parent == self.voucher_no)) - .groupby(jea.reference_name) + .where( + (ple.docstatus == 1) + & (ple.voucher_type == self.voucher_type) + & (ple.voucher_no == self.voucher_no) + & (ple.voucher_no != ple.against_voucher_no) + ) + .groupby(ple.against_voucher_type, ple.against_voucher_no) .run(as_dict=True) ) From 335cb5fd283149884dad53c208b6dd50940df77e Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Wed, 30 Aug 2023 11:02:03 +0530 Subject: [PATCH 37/72] refactor: single fetch and unlinking logic for JE and PE (cherry picked from commit de910ab152801dcfa18fe72d45853680716630b6) --- .../unreconcile_payment_entries.json | 20 ++++++- .../unreconcile_payments.py | 58 ++++++------------- 2 files changed, 38 insertions(+), 40 deletions(-) diff --git a/erpnext/accounts/doctype/unreconcile_payment_entries/unreconcile_payment_entries.json b/erpnext/accounts/doctype/unreconcile_payment_entries/unreconcile_payment_entries.json index c4afaa8bcac..955c3bbe031 100644 --- a/erpnext/accounts/doctype/unreconcile_payment_entries/unreconcile_payment_entries.json +++ b/erpnext/accounts/doctype/unreconcile_payment_entries/unreconcile_payment_entries.json @@ -6,6 +6,9 @@ "editable_grid": 1, "engine": "InnoDB", "field_order": [ + "account", + "party_type", + "party", "reference_doctype", "reference_name", "allocated_amount", @@ -39,12 +42,27 @@ "in_list_view": 1, "label": "Reference Type", "options": "DocType" + }, + { + "fieldname": "account", + "fieldtype": "Data", + "label": "Account" + }, + { + "fieldname": "party_type", + "fieldtype": "Data", + "label": "Party Type" + }, + { + "fieldname": "party", + "fieldtype": "Data", + "label": "Party" } ], "index_web_pages_for_search": 1, "istable": 1, "links": [], - "modified": "2023-08-24 14:48:10.018574", + "modified": "2023-08-30 10:58:45.322668", "modified_by": "Administrator", "module": "Accounts", "name": "Unreconcile Payment Entries", diff --git a/erpnext/accounts/doctype/unreconcile_payments/unreconcile_payments.py b/erpnext/accounts/doctype/unreconcile_payments/unreconcile_payments.py index a32313f4a5c..1688b6e4984 100644 --- a/erpnext/accounts/doctype/unreconcile_payments/unreconcile_payments.py +++ b/erpnext/accounts/doctype/unreconcile_payments/unreconcile_payments.py @@ -20,38 +20,26 @@ class UnreconcilePayments(Document): @frappe.whitelist() def get_allocations_from_payment(self): allocated_references = [] - if self.voucher_type == "Payment Entry": - per = qb.DocType("Payment Entry Reference") - allocated_references = ( - qb.from_(per) - .select( - per.reference_doctype, per.reference_name, Sum(per.allocated_amount).as_("allocated_amount") - ) - .where((per.docstatus == 1) & (per.parent == self.voucher_no)) - .groupby(per.reference_name) - .run(as_dict=True) + ple = qb.DocType("Payment Ledger Entry") + allocated_references = ( + qb.from_(ple) + .select( + ple.account, + ple.party_type, + ple.party, + ple.against_voucher_type.as_("reference_doctype"), + ple.against_voucher_no.as_("reference_name"), + Abs(Sum(ple.amount_in_account_currency)).as_("allocated_amount"), ) - elif self.voucher_type == "Journal Entry": - # for journals, using payment ledger to fetch allocation. - # this way we can avoid vaildating account type and reference details individually on child table - - ple = qb.DocType("Payment Ledger Entry") - allocated_references = ( - qb.from_(ple) - .select( - ple.against_voucher_type.as_("reference_doctype"), - ple.against_voucher_no.as_("reference_name"), - Abs(Sum(ple.amount_in_account_currency)).as_("allocated_amount"), - ) - .where( - (ple.docstatus == 1) - & (ple.voucher_type == self.voucher_type) - & (ple.voucher_no == self.voucher_no) - & (ple.voucher_no != ple.against_voucher_no) - ) - .groupby(ple.against_voucher_type, ple.against_voucher_no) - .run(as_dict=True) + .where( + (ple.docstatus == 1) + & (ple.voucher_type == self.voucher_type) + & (ple.voucher_no == self.voucher_no) + & (ple.voucher_no != ple.against_voucher_no) ) + .groupby(ple.against_voucher_type, ple.against_voucher_no) + .run(as_dict=True) + ) return allocated_references @@ -65,19 +53,11 @@ class UnreconcilePayments(Document): # todo: add more granular unlinking # different amounts for same invoice should be individually unlinkable - payment_type, paid_from, paid_to, party_type, party = frappe.db.get_all( - self.voucher_type, - filters={"name": self.voucher_no}, - fields=["payment_type", "paid_from", "paid_to", "party_type", "party"], - as_list=1, - )[0] - account = paid_from if payment_type == "Receive" else paid_to - for alloc in self.allocations: doc = frappe.get_doc(alloc.reference_doctype, alloc.reference_name) unlink_ref_doc_from_payment_entries(doc) update_voucher_outstanding( - alloc.reference_doctype, alloc.reference_name, account, party_type, party + alloc.reference_doctype, alloc.reference_name, alloc.account, alloc.party_type, alloc.party ) frappe.db.set_value("Unreconcile Payment Entries", alloc.name, "unlinked", True) From 3cbaea389b90f6602c3fe1a4e54244adc6d0df3b Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Wed, 30 Aug 2023 13:25:23 +0530 Subject: [PATCH 38/72] refactor: convert raw sql to query_builder (cherry picked from commit 0130aea2aa1ac93cd790af3652ea7b43871c23c8) --- erpnext/accounts/utils.py | 119 ++++++++++++++++++++------------------ 1 file changed, 64 insertions(+), 55 deletions(-) diff --git a/erpnext/accounts/utils.py b/erpnext/accounts/utils.py index 971932e415a..9777fd53402 100644 --- a/erpnext/accounts/utils.py +++ b/erpnext/accounts/utils.py @@ -693,72 +693,87 @@ def cancel_exchange_gain_loss_journal(parent_doc: dict | object) -> None: frappe.get_doc("Journal Entry", doc[0]).cancel() -def unlink_ref_doc_from_payment_entries(ref_doc): - remove_ref_doc_link_from_jv(ref_doc.doctype, ref_doc.name) - remove_ref_doc_link_from_pe(ref_doc.doctype, ref_doc.name) - - frappe.db.sql( - """update `tabGL Entry` - set against_voucher_type=null, against_voucher=null, - modified=%s, modified_by=%s - where against_voucher_type=%s and against_voucher=%s - and voucher_no != ifnull(against_voucher, '')""", - (now(), frappe.session.user, ref_doc.doctype, ref_doc.name), - ) +def update_accounting_ledgers_after_reference_removal(ref_type: str = None, ref_no: str = None): + # General Ledger + gle = qb.DocType("GL Entry") + qb.update(gle).set(gle.against_voucher_type, None).set(gle.against_voucher, None).set( + gle.modified, now() + ).set(gle.modified_by, frappe.session.user).where( + (gle.against_voucher_type == ref_type) & (gle.against_voucher == ref_no) + ).run() + # Payment Ledger ple = qb.DocType("Payment Ledger Entry") - qb.update(ple).set(ple.against_voucher_type, ple.voucher_type).set( ple.against_voucher_no, ple.voucher_no ).set(ple.modified, now()).set(ple.modified_by, frappe.session.user).where( - (ple.against_voucher_type == ref_doc.doctype) - & (ple.against_voucher_no == ref_doc.name) - & (ple.delinked == 0) + (ple.against_voucher_type == ref_type) & (ple.against_voucher_no == ref_no) & (ple.delinked == 0) ).run() + +def remove_ref_from_advance_section(ref_doc: object = None): if ref_doc.doctype in ("Sales Invoice", "Purchase Invoice"): ref_doc.set("advances", []) + adv_type = qb.DocType(f"{ref_doc.doctype} Advance") + qb.from_(adv_type).delete().where(adv_type.parent == ref_doc.name).run() - frappe.db.sql( - """delete from `tab{0} Advance` where parent = %s""".format(ref_doc.doctype), ref_doc.name - ) + +def unlink_ref_doc_from_payment_entries(ref_doc): + remove_ref_doc_link_from_jv(ref_doc.doctype, ref_doc.name) + remove_ref_doc_link_from_pe(ref_doc.doctype, ref_doc.name) + update_accounting_ledgers_after_reference_removal(ref_doc.doctype, ref_doc.name) def remove_ref_doc_link_from_jv(ref_type, ref_no): - linked_jv = frappe.db.sql_list( - """select parent from `tabJournal Entry Account` - where reference_type=%s and reference_name=%s and docstatus < 2""", - (ref_type, ref_no), + jea = qb.DocType("Journal Entry Account") + + linked_jv = ( + qb.from_(jea) + .select(jea.parent) + .select( + (jea.reference_type == ref_type) & (jea.reference_name == ref_no) & (jea.docstatus.lt(2)) + ) + .run(as_list=1) ) + linked_jv = convert_to_list(linked_jv) if linked_jv: - frappe.db.sql( - """update `tabJournal Entry Account` - set reference_type=null, reference_name = null, - modified=%s, modified_by=%s - where reference_type=%s and reference_name=%s - and docstatus < 2""", - (now(), frappe.session.user, ref_type, ref_no), - ) + qb.update(jea).set(jea.reference_type, None).set(jea.reference_name, None).set( + jea.modified, now() + ).set(jea.modified_by, frappe.session.user).where( + (jea.reference_type == ref_type) & (jea.reference_name == ref_no) + ).run() frappe.msgprint(_("Journal Entries {0} are un-linked").format("\n".join(linked_jv))) +def convert_to_list(result): + """ + Convert tuple to list + """ + return [x[0] for x in result] + + def remove_ref_doc_link_from_pe(ref_type, ref_no): - linked_pe = frappe.db.sql_list( - """select parent from `tabPayment Entry Reference` - where reference_doctype=%s and reference_name=%s and docstatus < 2""", - (ref_type, ref_no), + per = qb.DocType("Payment Entry Reference") + pay = qb.DocType("Payment Entry") + + linked_pe = ( + qb.from_(per) + .select(per.parent) + .where( + (per.reference_doctype == ref_type) & (per.reference_name == ref_no) & (per.docstatus.lt(2)) + ) + .run(as_list=1) ) + linked_pe = convert_to_list(linked_pe) if linked_pe: - frappe.db.sql( - """update `tabPayment Entry Reference` - set allocated_amount=0, modified=%s, modified_by=%s - where reference_doctype=%s and reference_name=%s - and docstatus < 2""", - (now(), frappe.session.user, ref_type, ref_no), - ) + qb.update(per).set(per.allocated_amount, 0).set(per.modified, now()).set( + per.modified_by, frappe.session.user + ).where( + (per.docstatus.lt(2) & (per.reference_doctype == ref_type) & (per.reference_name == ref_no)) + ).run() for pe in linked_pe: try: @@ -772,19 +787,13 @@ def remove_ref_doc_link_from_pe(ref_type, ref_no): msg += _("Please cancel payment entry manually first") frappe.throw(msg, exc=PaymentEntryUnlinkError, title=_("Payment Unlink Error")) - frappe.db.sql( - """update `tabPayment Entry` set total_allocated_amount=%s, - base_total_allocated_amount=%s, unallocated_amount=%s, modified=%s, modified_by=%s - where name=%s""", - ( - pe_doc.total_allocated_amount, - pe_doc.base_total_allocated_amount, - pe_doc.unallocated_amount, - now(), - frappe.session.user, - pe, - ), - ) + qb.update(pay).set(pay.total_allocated_amount, pe_doc.total_allocated_amount).set( + pay.base_total_allocated_amount, pe_doc.base_total_allocated_amount + ).set(pay.unallocated_amount, pe_doc.unallocated_amount).set(pay.modified, now()).set( + pay.modified_by, frappe.session.user + ).where( + pay.name == pe + ).run() frappe.msgprint(_("Payment Entries {0} are un-linked").format("\n".join(linked_pe))) From 8954bd77590ce766635bff24f569a523a735c3b2 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Wed, 30 Aug 2023 16:30:14 +0530 Subject: [PATCH 39/72] chore: type info (cherry picked from commit b4dc2bdf28bf9c1c7043750a4c91d786c230ea6a) --- .../doctype/unreconcile_payments/unreconcile_payments.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/accounts/doctype/unreconcile_payments/unreconcile_payments.py b/erpnext/accounts/doctype/unreconcile_payments/unreconcile_payments.py index 1688b6e4984..304ccccb089 100644 --- a/erpnext/accounts/doctype/unreconcile_payments/unreconcile_payments.py +++ b/erpnext/accounts/doctype/unreconcile_payments/unreconcile_payments.py @@ -63,7 +63,7 @@ class UnreconcilePayments(Document): @frappe.whitelist() -def doc_has_references(doctype, docname): +def doc_has_references(doctype: str = None, docname: str = None): if doctype in ["Sales Invoice", "Purchase Invoice"]: return frappe.db.count( "Payment Ledger Entry", From f5718390b74e1430d73947305c1d0e3898f10799 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Wed, 30 Aug 2023 16:44:25 +0530 Subject: [PATCH 40/72] refactor: unlink individual vouchers from payments (cherry picked from commit 9b6eac23b6ad38323c68f42fdcf1a2f8916705a9) --- .../unreconcile_payments.py | 2 +- erpnext/accounts/utils.py | 101 +++++++++++++----- 2 files changed, 75 insertions(+), 28 deletions(-) diff --git a/erpnext/accounts/doctype/unreconcile_payments/unreconcile_payments.py b/erpnext/accounts/doctype/unreconcile_payments/unreconcile_payments.py index 304ccccb089..5161a928237 100644 --- a/erpnext/accounts/doctype/unreconcile_payments/unreconcile_payments.py +++ b/erpnext/accounts/doctype/unreconcile_payments/unreconcile_payments.py @@ -55,7 +55,7 @@ class UnreconcilePayments(Document): for alloc in self.allocations: doc = frappe.get_doc(alloc.reference_doctype, alloc.reference_name) - unlink_ref_doc_from_payment_entries(doc) + unlink_ref_doc_from_payment_entries(doc, self.voucher_no) update_voucher_outstanding( alloc.reference_doctype, alloc.reference_name, alloc.account, alloc.party_type, alloc.party ) diff --git a/erpnext/accounts/utils.py b/erpnext/accounts/utils.py index 9777fd53402..77ac3023ca8 100644 --- a/erpnext/accounts/utils.py +++ b/erpnext/accounts/utils.py @@ -693,38 +693,62 @@ def cancel_exchange_gain_loss_journal(parent_doc: dict | object) -> None: frappe.get_doc("Journal Entry", doc[0]).cancel() -def update_accounting_ledgers_after_reference_removal(ref_type: str = None, ref_no: str = None): +def update_accounting_ledgers_after_reference_removal( + ref_type: str = None, ref_no: str = None, payment_name: str = None +): # General Ledger gle = qb.DocType("GL Entry") - qb.update(gle).set(gle.against_voucher_type, None).set(gle.against_voucher, None).set( - gle.modified, now() - ).set(gle.modified_by, frappe.session.user).where( - (gle.against_voucher_type == ref_type) & (gle.against_voucher == ref_no) - ).run() + gle_update_query = ( + qb.update(gle) + .set(gle.against_voucher_type, None) + .set(gle.against_voucher, None) + .set(gle.modified, now()) + .set(gle.modified_by, frappe.session.user) + .where((gle.against_voucher_type == ref_type) & (gle.against_voucher == ref_no)) + ) + + if payment_name: + gle_update_query = gle_update_query.where(gle.voucher_no == payment_name) + gle_update_query.run() # Payment Ledger ple = qb.DocType("Payment Ledger Entry") - qb.update(ple).set(ple.against_voucher_type, ple.voucher_type).set( - ple.against_voucher_no, ple.voucher_no - ).set(ple.modified, now()).set(ple.modified_by, frappe.session.user).where( - (ple.against_voucher_type == ref_type) & (ple.against_voucher_no == ref_no) & (ple.delinked == 0) - ).run() + ple_update_query = ( + qb.update(ple) + .set(ple.against_voucher_type, ple.voucher_type) + .set(ple.against_voucher_no, ple.voucher_no) + .set(ple.modified, now()) + .set(ple.modified_by, frappe.session.user) + .where( + (ple.against_voucher_type == ref_type) + & (ple.against_voucher_no == ref_no) + & (ple.delinked == 0) + ) + ) + + if payment_name: + ple_update_query = ple_update_query.where(ple.voucher_no == payment_name) + ple_update_query.run() def remove_ref_from_advance_section(ref_doc: object = None): + # TODO: this might need some testing if ref_doc.doctype in ("Sales Invoice", "Purchase Invoice"): ref_doc.set("advances", []) adv_type = qb.DocType(f"{ref_doc.doctype} Advance") qb.from_(adv_type).delete().where(adv_type.parent == ref_doc.name).run() -def unlink_ref_doc_from_payment_entries(ref_doc): - remove_ref_doc_link_from_jv(ref_doc.doctype, ref_doc.name) - remove_ref_doc_link_from_pe(ref_doc.doctype, ref_doc.name) - update_accounting_ledgers_after_reference_removal(ref_doc.doctype, ref_doc.name) +def unlink_ref_doc_from_payment_entries(ref_doc: object = None, payment_name: str = None): + remove_ref_doc_link_from_jv(ref_doc.doctype, ref_doc.name, payment_name) + remove_ref_doc_link_from_pe(ref_doc.doctype, ref_doc.name, payment_name) + update_accounting_ledgers_after_reference_removal(ref_doc.doctype, ref_doc.name, payment_name) + remove_ref_from_advance_section(ref_doc) -def remove_ref_doc_link_from_jv(ref_type, ref_no): +def remove_ref_doc_link_from_jv( + ref_type: str = None, ref_no: str = None, payment_name: str = None +): jea = qb.DocType("Journal Entry Account") linked_jv = ( @@ -736,13 +760,23 @@ def remove_ref_doc_link_from_jv(ref_type, ref_no): .run(as_list=1) ) linked_jv = convert_to_list(linked_jv) + # remove reference only from specified payment + linked_jv = [x for x in linked_jv if x == payment_name] if payment_name else linked_jv if linked_jv: - qb.update(jea).set(jea.reference_type, None).set(jea.reference_name, None).set( - jea.modified, now() - ).set(jea.modified_by, frappe.session.user).where( - (jea.reference_type == ref_type) & (jea.reference_name == ref_no) - ).run() + update_query = ( + qb.update(jea) + .set(jea.reference_type, None) + .set(jea.reference_name, None) + .set(jea.modified, now()) + .set(jea.modified_by, frappe.session.user) + .where((jea.reference_type == ref_type) & (jea.reference_name == ref_no)) + ) + + if payment_name: + update_query = update_query.where(jea.parent == payment_name) + + update_query.run() frappe.msgprint(_("Journal Entries {0} are un-linked").format("\n".join(linked_jv))) @@ -754,7 +788,9 @@ def convert_to_list(result): return [x[0] for x in result] -def remove_ref_doc_link_from_pe(ref_type, ref_no): +def remove_ref_doc_link_from_pe( + ref_type: str = None, ref_no: str = None, payment_name: str = None +): per = qb.DocType("Payment Entry Reference") pay = qb.DocType("Payment Entry") @@ -767,13 +803,24 @@ def remove_ref_doc_link_from_pe(ref_type, ref_no): .run(as_list=1) ) linked_pe = convert_to_list(linked_pe) + # remove reference only from specified payment + linked_pe = [x for x in linked_pe if x == payment_name] if payment_name else linked_pe if linked_pe: - qb.update(per).set(per.allocated_amount, 0).set(per.modified, now()).set( - per.modified_by, frappe.session.user - ).where( - (per.docstatus.lt(2) & (per.reference_doctype == ref_type) & (per.reference_name == ref_no)) - ).run() + update_query = ( + qb.update(per) + .set(per.allocated_amount, 0) + .set(per.modified, now()) + .set(per.modified_by, frappe.session.user) + .where( + (per.docstatus.lt(2) & (per.reference_doctype == ref_type) & (per.reference_name == ref_no)) + ) + ) + + if payment_name: + update_query = update_query.where(per.parent == payment_name) + + update_query.run() for pe in linked_pe: try: From cf308912a17829499dee2f68ed9c15dca92b6910 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Wed, 30 Aug 2023 17:03:02 +0530 Subject: [PATCH 41/72] test: more granular unreconciliation (cherry picked from commit 67980188a7a673d42848005b5b1ebbad9a6c98df) --- .../test_unreconcile_payments.py | 109 ++++++++++++++++++ 1 file changed, 109 insertions(+) diff --git a/erpnext/accounts/doctype/unreconcile_payments/test_unreconcile_payments.py b/erpnext/accounts/doctype/unreconcile_payments/test_unreconcile_payments.py index 2bb8a54c350..924a950c4fe 100644 --- a/erpnext/accounts/doctype/unreconcile_payments/test_unreconcile_payments.py +++ b/erpnext/accounts/doctype/unreconcile_payments/test_unreconcile_payments.py @@ -99,3 +99,112 @@ class TestUnreconcilePayments(AccountsTestMixin, FrappeTestCase): pe.reload() self.assertEqual(len(pe.references), 1) + self.assertEqual(pe.unallocated_amount, 100) + + def test_02_unreconcile_one_payment_from_multi_payments(self): + """ + Scenario: 2 payments, both split against 2 invoices + Unreconcile only one payment from one invoice + """ + si1 = create_sales_invoice( + item=self.item, + company=self.company, + customer=self.customer, + debit_to=self.debit_to, + posting_date=today(), + parent_cost_center=self.cost_center, + cost_center=self.cost_center, + rate=100, + price_list_rate=100, + ) + + si2 = create_sales_invoice( + item=self.item, + company=self.company, + customer=self.customer, + debit_to=self.debit_to, + posting_date=today(), + parent_cost_center=self.cost_center, + cost_center=self.cost_center, + rate=100, + price_list_rate=100, + ) + + pe1 = create_payment_entry( + company=self.company, + payment_type="Receive", + party_type="Customer", + party=self.customer, + paid_from=self.debit_to, + paid_to=self.cash, + paid_amount=100, + save=True, + ) + pe1.append( + "references", + {"reference_doctype": si1.doctype, "reference_name": si1.name, "allocated_amount": 50}, + ) + pe1.append( + "references", + {"reference_doctype": si2.doctype, "reference_name": si2.name, "allocated_amount": 50}, + ) + # Allocation payment against both invoices + pe1.save().submit() + + pe2 = create_payment_entry( + company=self.company, + payment_type="Receive", + party_type="Customer", + party=self.customer, + paid_from=self.debit_to, + paid_to=self.cash, + paid_amount=100, + save=True, + ) + pe2.append( + "references", + {"reference_doctype": si1.doctype, "reference_name": si1.name, "allocated_amount": 50}, + ) + pe2.append( + "references", + {"reference_doctype": si2.doctype, "reference_name": si2.name, "allocated_amount": 50}, + ) + # Allocation payment against both invoices + pe2.save().submit() + + # Assert outstanding + si1.reload() + si2.reload() + self.assertEqual(si1.outstanding_amount, 0) + self.assertEqual(si2.outstanding_amount, 0) + + unreconcile = frappe.get_doc( + { + "doctype": "Unreconcile Payments", + "company": self.company, + "voucher_type": pe2.doctype, + "voucher_no": pe2.name, + } + ) + unreconcile.add_references() + self.assertEqual(len(unreconcile.allocations), 2) + allocations = [x.reference_name for x in unreconcile.allocations] + self.assertEquals([si1.name, si2.name], allocations) + # unreconcile si1 from pe2 + for x in unreconcile.allocations: + if x.reference_name != si1.name: + unreconcile.remove(x) + unreconcile.save().submit() + + # Assert outstanding + si1.reload() + si2.reload() + self.assertEqual(si1.outstanding_amount, 50) + self.assertEqual(si2.outstanding_amount, 0) + + pe1.reload() + pe2.reload() + self.assertEqual(len(pe1.references), 2) + self.assertEqual(len(pe2.references), 1) + self.assertEqual(pe1.unallocated_amount, 0) + self.assertEqual(pe2.unallocated_amount, 50) From 606c99e57cdd36951ef8472df58dcf54e862e476 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Wed, 30 Aug 2023 20:50:16 +0530 Subject: [PATCH 42/72] fix: typo in doctype name and qb (cherry picked from commit 9a1588f1cccc5336ca7a7f45be66f84ab3dc1e06) --- erpnext/accounts/utils.py | 4 +--- erpnext/controllers/accounts_controller.py | 2 +- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/erpnext/accounts/utils.py b/erpnext/accounts/utils.py index 77ac3023ca8..a78ef2e9384 100644 --- a/erpnext/accounts/utils.py +++ b/erpnext/accounts/utils.py @@ -754,9 +754,7 @@ def remove_ref_doc_link_from_jv( linked_jv = ( qb.from_(jea) .select(jea.parent) - .select( - (jea.reference_type == ref_type) & (jea.reference_name == ref_no) & (jea.docstatus.lt(2)) - ) + .where((jea.reference_type == ref_type) & (jea.reference_name == ref_no) & (jea.docstatus.lt(2))) .run(as_list=1) ) linked_jv = convert_to_list(linked_jv) diff --git a/erpnext/controllers/accounts_controller.py b/erpnext/controllers/accounts_controller.py index ffd8870553b..2c919b2288d 100644 --- a/erpnext/controllers/accounts_controller.py +++ b/erpnext/controllers/accounts_controller.py @@ -212,7 +212,7 @@ class AccountsController(TransactionBase): validate_einvoice_fields(self) def _remove_references_in_unreconcile(self): - upe = frappe.qb.DocType("UnReconcile Payment Entries") + upe = frappe.qb.DocType("Unreconcile Payment Entries") rows = ( frappe.qb.from_(upe) .select(upe.name, upe.parent) From 669d692844e1a0d3f66d96988081886b5616ce50 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Tue, 5 Sep 2023 08:53:10 +0530 Subject: [PATCH 43/72] refactor: display allocated amount in account currency with symbol (cherry picked from commit 6fd1c1bca263b05cf035c97fcf56d209c137b61b) --- .../unreconcile_payment_entries.json | 15 ++++++++++++--- .../unreconcile_payments/unreconcile_payments.py | 2 ++ erpnext/public/js/utils/unreconcile.js | 5 +++-- 3 files changed, 17 insertions(+), 5 deletions(-) diff --git a/erpnext/accounts/doctype/unreconcile_payment_entries/unreconcile_payment_entries.json b/erpnext/accounts/doctype/unreconcile_payment_entries/unreconcile_payment_entries.json index 955c3bbe031..42da669e650 100644 --- a/erpnext/accounts/doctype/unreconcile_payment_entries/unreconcile_payment_entries.json +++ b/erpnext/accounts/doctype/unreconcile_payment_entries/unreconcile_payment_entries.json @@ -12,6 +12,7 @@ "reference_doctype", "reference_name", "allocated_amount", + "account_currency", "unlinked" ], "fields": [ @@ -24,9 +25,10 @@ }, { "fieldname": "allocated_amount", - "fieldtype": "Int", + "fieldtype": "Currency", "in_list_view": 1, - "label": "Allocated Amount" + "label": "Allocated Amount", + "options": "account_currency" }, { "default": "0", @@ -57,12 +59,19 @@ "fieldname": "party", "fieldtype": "Data", "label": "Party" + }, + { + "fieldname": "account_currency", + "fieldtype": "Link", + "label": "Account Currency", + "options": "Currency", + "read_only": 1 } ], "index_web_pages_for_search": 1, "istable": 1, "links": [], - "modified": "2023-08-30 10:58:45.322668", + "modified": "2023-09-05 09:33:28.620149", "modified_by": "Administrator", "module": "Accounts", "name": "Unreconcile Payment Entries", diff --git a/erpnext/accounts/doctype/unreconcile_payments/unreconcile_payments.py b/erpnext/accounts/doctype/unreconcile_payments/unreconcile_payments.py index 5161a928237..25f85db71f7 100644 --- a/erpnext/accounts/doctype/unreconcile_payments/unreconcile_payments.py +++ b/erpnext/accounts/doctype/unreconcile_payments/unreconcile_payments.py @@ -30,6 +30,7 @@ class UnreconcilePayments(Document): ple.against_voucher_type.as_("reference_doctype"), ple.against_voucher_no.as_("reference_name"), Abs(Sum(ple.amount_in_account_currency)).as_("allocated_amount"), + ple.account_currency, ) .where( (ple.docstatus == 1) @@ -99,6 +100,7 @@ def get_linked_payments_for_doc( ple.voucher_type, ple.voucher_no, Abs(Sum(ple.amount_in_account_currency)).as_("allocated_amount"), + ple.account_currency, ) .where(Criterion.all(criteria)) .groupby(ple.voucher_no, ple.against_voucher_no) diff --git a/erpnext/public/js/utils/unreconcile.js b/erpnext/public/js/utils/unreconcile.js index df07643bb7c..cd44f3578b0 100644 --- a/erpnext/public/js/utils/unreconcile.js +++ b/erpnext/public/js/utils/unreconcile.js @@ -53,7 +53,8 @@ erpnext.accounts.unreconcile_payments = { let child_table_fields = [ { label: __("Voucher Type"), fieldname: "voucher_type", fieldtype: "Dynamic Link", options: "DocType", in_list_view: 1, read_only: 1}, { label: __("Voucher No"), fieldname: "voucher_no", fieldtype: "Link", options: "voucher_type", in_list_view: 1, read_only: 1 }, - { label: __("Allocated Amount"), fieldname: "allocated_amount", fieldtype: "Float", in_list_view: 1, read_only: 1 }, + { label: __("Allocated Amount"), fieldname: "allocated_amount", fieldtype: "Currency", in_list_view: 1, read_only: 1 , options: "account_currency"}, + { label: __("Currency"), fieldname: "account_currency", fieldtype: "Currency", read_only: 1}, ] let unreconcile_dialog_fields = [ { @@ -83,7 +84,7 @@ erpnext.accounts.unreconcile_payments = { title: 'Un-Reconcile Allocations', fields: unreconcile_dialog_fields, size: 'large', - cannot_add_rows: 1, + cannot_add_rows: true, primary_action_label: 'Un-Reconcile', primary_action(values) { From 75d3093aea6c286230c6323077622211d610f7e0 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Tue, 5 Sep 2023 09:52:36 +0530 Subject: [PATCH 44/72] refactor: only cancel specific gain/loss je (cherry picked from commit 5dbcf7d2b94dae06ef7fc31b3142606d65611bff) --- erpnext/accounts/utils.py | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/erpnext/accounts/utils.py b/erpnext/accounts/utils.py index a78ef2e9384..76339713a22 100644 --- a/erpnext/accounts/utils.py +++ b/erpnext/accounts/utils.py @@ -663,7 +663,9 @@ def update_reference_in_payment_entry( payment_entry.save(ignore_permissions=True) -def cancel_exchange_gain_loss_journal(parent_doc: dict | object) -> None: +def cancel_exchange_gain_loss_journal( + parent_doc: dict | object, referenced_dt: str = None, referenced_dn: str = None +) -> None: """ Cancel Exchange Gain/Loss for Sales/Purchase Invoice, if they have any. """ @@ -690,7 +692,18 @@ def cancel_exchange_gain_loss_journal(parent_doc: dict | object) -> None: as_list=1, ) for doc in gain_loss_journals: - frappe.get_doc("Journal Entry", doc[0]).cancel() + gain_loss_je = frappe.get_doc("Journal Entry", doc[0]) + if referenced_dt and referenced_dn: + references = [(x.reference_type, x.reference_name) for x in gain_loss_je.accounts] + if ( + len(references) == 2 + and (referenced_dt, referenced_dn) in references + and (parent_doc.doctype, parent_doc.name) in references + ): + # only cancel JE generated against parent_doc and referenced_dn + gain_loss_je.cancel() + else: + gain_loss_je.cancel() def update_accounting_ledgers_after_reference_removal( From 4bd83b5058d439d7aedc3589806118f9d0e8c22d Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Tue, 5 Sep 2023 09:59:34 +0530 Subject: [PATCH 45/72] refactor: cancel gain/loss JE on multi currency transactions (cherry picked from commit 1d93d66c30e69bfcb277123462bf822aa3c3c1d4) --- .../unreconcile_payments/unreconcile_payments.py | 13 +++++++++---- erpnext/public/js/utils/unreconcile.js | 6 ++++++ 2 files changed, 15 insertions(+), 4 deletions(-) diff --git a/erpnext/accounts/doctype/unreconcile_payments/unreconcile_payments.py b/erpnext/accounts/doctype/unreconcile_payments/unreconcile_payments.py index 25f85db71f7..4f9fb50d463 100644 --- a/erpnext/accounts/doctype/unreconcile_payments/unreconcile_payments.py +++ b/erpnext/accounts/doctype/unreconcile_payments/unreconcile_payments.py @@ -8,7 +8,11 @@ from frappe.query_builder import Criterion from frappe.query_builder.functions import Abs, Sum from frappe.utils.data import comma_and -from erpnext.accounts.utils import unlink_ref_doc_from_payment_entries, update_voucher_outstanding +from erpnext.accounts.utils import ( + cancel_exchange_gain_loss_journal, + unlink_ref_doc_from_payment_entries, + update_voucher_outstanding, +) class UnreconcilePayments(Document): @@ -51,12 +55,11 @@ class UnreconcilePayments(Document): self.append("allocations", alloc) def on_submit(self): - # todo: add more granular unlinking - # different amounts for same invoice should be individually unlinkable - + # todo: more granular unreconciliation for alloc in self.allocations: doc = frappe.get_doc(alloc.reference_doctype, alloc.reference_name) unlink_ref_doc_from_payment_entries(doc, self.voucher_no) + cancel_exchange_gain_loss_journal(doc, self.voucher_type, self.voucher_no) update_voucher_outstanding( alloc.reference_doctype, alloc.reference_name, alloc.account, alloc.party_type, alloc.party ) @@ -104,6 +107,7 @@ def get_linked_payments_for_doc( ) .where(Criterion.all(criteria)) .groupby(ple.voucher_no, ple.against_voucher_no) + .having(qb.Field("allocated_amount") > 0) .run(as_dict=True) ) return res @@ -122,6 +126,7 @@ def get_linked_payments_for_doc( ple.against_voucher_type.as_("voucher_type"), ple.against_voucher_no.as_("voucher_no"), Abs(Sum(ple.amount_in_account_currency)).as_("allocated_amount"), + ple.account_currency, ) .where(Criterion.all(criteria)) .groupby(ple.against_voucher_no) diff --git a/erpnext/public/js/utils/unreconcile.js b/erpnext/public/js/utils/unreconcile.js index cd44f3578b0..acc77a64b01 100644 --- a/erpnext/public/js/utils/unreconcile.js +++ b/erpnext/public/js/utils/unreconcile.js @@ -3,6 +3,12 @@ frappe.provide('erpnext.accounts'); erpnext.accounts.unreconcile_payments = { add_unreconcile_btn(frm) { if (frm.doc.docstatus == 1) { + if(((frm.doc.doctype == "Journal Entry") && (frm.doc.voucher_type != "Journal Entry")) + || !["Purchase Invoice", "Sales Invoice", "Journal Entry", "Payment Entry"].includes(frm.doc.doctype) + ) { + return; + } + frappe.call({ "method": "erpnext.accounts.doctype.unreconcile_payments.unreconcile_payments.doc_has_references", "args": { From aba51ee352d9f16ce6f9ae16a7538b4e43dbfd58 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Fri, 8 Sep 2023 21:43:23 +0530 Subject: [PATCH 46/72] refactor(test): more modularization (cherry picked from commit 5c09fdf9419e301ffbf3787db02d689758f4757e) --- .../test_unreconcile_payments.py | 108 +++++------------- 1 file changed, 30 insertions(+), 78 deletions(-) diff --git a/erpnext/accounts/doctype/unreconcile_payments/test_unreconcile_payments.py b/erpnext/accounts/doctype/unreconcile_payments/test_unreconcile_payments.py index 924a950c4fe..3d7c6cbe321 100644 --- a/erpnext/accounts/doctype/unreconcile_payments/test_unreconcile_payments.py +++ b/erpnext/accounts/doctype/unreconcile_payments/test_unreconcile_payments.py @@ -20,20 +20,8 @@ class TestUnreconcilePayments(AccountsTestMixin, FrappeTestCase): def tearDown(self): frappe.db.rollback() - def test_01_unreconcile_invoice(self): - si1 = create_sales_invoice( - item=self.item, - company=self.company, - customer=self.customer, - debit_to=self.debit_to, - posting_date=today(), - parent_cost_center=self.cost_center, - cost_center=self.cost_center, - rate=100, - price_list_rate=100, - ) - - si2 = create_sales_invoice( + def create_sales_invoice(self): + si = create_sales_invoice( item=self.item, company=self.company, customer=self.customer, @@ -44,7 +32,9 @@ class TestUnreconcilePayments(AccountsTestMixin, FrappeTestCase): rate=100, price_list_rate=100, ) + return si + def create_payment_entry(self): pe = create_payment_entry( company=self.company, payment_type="Receive", @@ -55,7 +45,13 @@ class TestUnreconcilePayments(AccountsTestMixin, FrappeTestCase): paid_amount=200, save=True, ) + return pe + def test_01_unreconcile_invoice(self): + si1 = self.create_sales_invoice() + si2 = self.create_sales_invoice() + + pe = self.create_payment_entry() pe.append( "references", {"reference_doctype": si1.doctype, "reference_name": si1.name, "allocated_amount": 100}, @@ -68,10 +64,10 @@ class TestUnreconcilePayments(AccountsTestMixin, FrappeTestCase): pe.save().submit() # Assert outstanding - si1.reload() - si2.reload() + [doc.reload() for doc in [si1, si2, pe]] self.assertEqual(si1.outstanding_amount, 0) self.assertEqual(si2.outstanding_amount, 0) + self.assertEqual(pe.unallocated_amount, 0) unreconcile = frappe.get_doc( { @@ -92,54 +88,22 @@ class TestUnreconcilePayments(AccountsTestMixin, FrappeTestCase): unreconcile.save().submit() # Assert outstanding - si1.reload() - si2.reload() + [doc.reload() for doc in [si1, si2, pe]] self.assertEqual(si1.outstanding_amount, 100) self.assertEqual(si2.outstanding_amount, 0) - - pe.reload() self.assertEqual(len(pe.references), 1) self.assertEqual(pe.unallocated_amount, 100) def test_02_unreconcile_one_payment_from_multi_payments(self): """ - Scenario: 2 payments, both split against 2 invoices + Scenario: 2 payments, both split against 2 different invoices Unreconcile only one payment from one invoice """ - si1 = create_sales_invoice( - item=self.item, - company=self.company, - customer=self.customer, - debit_to=self.debit_to, - posting_date=today(), - parent_cost_center=self.cost_center, - cost_center=self.cost_center, - rate=100, - price_list_rate=100, - ) - - si2 = create_sales_invoice( - item=self.item, - company=self.company, - customer=self.customer, - debit_to=self.debit_to, - posting_date=today(), - parent_cost_center=self.cost_center, - cost_center=self.cost_center, - rate=100, - price_list_rate=100, - ) - - pe1 = create_payment_entry( - company=self.company, - payment_type="Receive", - party_type="Customer", - party=self.customer, - paid_from=self.debit_to, - paid_to=self.cash, - paid_amount=100, - save=True, - ) + si1 = self.create_sales_invoice() + si2 = self.create_sales_invoice() + pe1 = self.create_payment_entry() + pe1.paid_amount = 100 + # Allocate payment against both invoices pe1.append( "references", {"reference_doctype": si1.doctype, "reference_name": si1.name, "allocated_amount": 50}, @@ -148,19 +112,11 @@ class TestUnreconcilePayments(AccountsTestMixin, FrappeTestCase): "references", {"reference_doctype": si2.doctype, "reference_name": si2.name, "allocated_amount": 50}, ) - # Allocation payment against both invoices pe1.save().submit() - pe2 = create_payment_entry( - company=self.company, - payment_type="Receive", - party_type="Customer", - party=self.customer, - paid_from=self.debit_to, - paid_to=self.cash, - paid_amount=100, - save=True, - ) + pe2 = self.create_payment_entry() + pe2.paid_amount = 100 + # Allocate payment against both invoices pe2.append( "references", {"reference_doctype": si1.doctype, "reference_name": si1.name, "allocated_amount": 50}, @@ -169,14 +125,14 @@ class TestUnreconcilePayments(AccountsTestMixin, FrappeTestCase): "references", {"reference_doctype": si2.doctype, "reference_name": si2.name, "allocated_amount": 50}, ) - # Allocation payment against both invoices pe2.save().submit() - # Assert outstanding - si1.reload() - si2.reload() - self.assertEqual(si1.outstanding_amount, 0) - self.assertEqual(si2.outstanding_amount, 0) + # Assert outstanding and unallocated + [doc.reload() for doc in [si1, si2, pe1, pe2]] + self.assertEqual(si1.outstanding_amount, 0.0) + self.assertEqual(si2.outstanding_amount, 0.0) + self.assertEqual(pe1.unallocated_amount, 0.0) + self.assertEqual(pe2.unallocated_amount, 0.0) unreconcile = frappe.get_doc( { @@ -196,14 +152,10 @@ class TestUnreconcilePayments(AccountsTestMixin, FrappeTestCase): unreconcile.remove(x) unreconcile.save().submit() - # Assert outstanding - si1.reload() - si2.reload() + # Assert outstanding and unallocated + [doc.reload() for doc in [si1, si2, pe1, pe2]] self.assertEqual(si1.outstanding_amount, 50) self.assertEqual(si2.outstanding_amount, 0) - - pe1.reload() - pe2.reload() self.assertEqual(len(pe1.references), 2) self.assertEqual(len(pe2.references), 1) self.assertEqual(pe1.unallocated_amount, 0) From 63274527d20c8a84d0aec855599a2b4cb2016b28 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Sat, 9 Sep 2023 07:24:56 +0530 Subject: [PATCH 47/72] test: multi currency invoice unreconciliation exchange gain/loss associated with the unreconcile invoice should be cancelled as well (cherry picked from commit d3987757151949a6ad37e906f3a14d0863307b97) --- .../test_unreconcile_payments.py | 156 +++++++++++++++++- 1 file changed, 155 insertions(+), 1 deletion(-) diff --git a/erpnext/accounts/doctype/unreconcile_payments/test_unreconcile_payments.py b/erpnext/accounts/doctype/unreconcile_payments/test_unreconcile_payments.py index 3d7c6cbe321..78e04bff819 100644 --- a/erpnext/accounts/doctype/unreconcile_payments/test_unreconcile_payments.py +++ b/erpnext/accounts/doctype/unreconcile_payments/test_unreconcile_payments.py @@ -14,13 +14,14 @@ class TestUnreconcilePayments(AccountsTestMixin, FrappeTestCase): def setUp(self): self.create_company() self.create_customer() + self.create_usd_receivable_account() self.create_item() self.clear_old_entries() def tearDown(self): frappe.db.rollback() - def create_sales_invoice(self): + def create_sales_invoice(self, do_not_submit=False): si = create_sales_invoice( item=self.item, company=self.company, @@ -31,6 +32,7 @@ class TestUnreconcilePayments(AccountsTestMixin, FrappeTestCase): cost_center=self.cost_center, rate=100, price_list_rate=100, + do_not_submit=do_not_submit, ) return si @@ -160,3 +162,155 @@ class TestUnreconcilePayments(AccountsTestMixin, FrappeTestCase): self.assertEqual(len(pe2.references), 1) self.assertEqual(pe1.unallocated_amount, 0) self.assertEqual(pe2.unallocated_amount, 50) + + def test_03_unreconciliation_on_multi_currency_invoice(self): + self.create_customer("_Test MC Customer USD", "USD") + si1 = self.create_sales_invoice(do_not_submit=True) + si1.currency = "USD" + si1.debit_to = self.debtors_usd + si1.conversion_rate = 80 + si1.save().submit() + + si2 = self.create_sales_invoice(do_not_submit=True) + si2.currency = "USD" + si2.debit_to = self.debtors_usd + si2.conversion_rate = 80 + si2.save().submit() + + pe = self.create_payment_entry() + pe.paid_from = self.debtors_usd + pe.paid_from_account_currency = "USD" + pe.source_exchange_rate = 75 + pe.received_amount = 75 * 200 + pe.save() + # Allocate payment against both invoices + pe.append( + "references", + {"reference_doctype": si1.doctype, "reference_name": si1.name, "allocated_amount": 100}, + ) + pe.append( + "references", + {"reference_doctype": si2.doctype, "reference_name": si2.name, "allocated_amount": 100}, + ) + pe.save().submit() + + unreconcile = frappe.get_doc( + { + "doctype": "Unreconcile Payments", + "company": self.company, + "voucher_type": pe.doctype, + "voucher_no": pe.name, + } + ) + unreconcile.add_references() + self.assertEqual(len(unreconcile.allocations), 2) + allocations = [x.reference_name for x in unreconcile.allocations] + self.assertEquals([si1.name, si2.name], allocations) + # unreconcile si1 from pe + for x in unreconcile.allocations: + if x.reference_name != si1.name: + unreconcile.remove(x) + unreconcile.save().submit() + + # Assert outstanding and unallocated + [doc.reload() for doc in [si1, si2, pe]] + self.assertEqual(si1.outstanding_amount, 100) + self.assertEqual(si2.outstanding_amount, 0) + self.assertEqual(len(pe.references), 1) + self.assertEqual(pe.unallocated_amount, 100) + + # Exc gain/loss JE should've been cancelled as well + self.assertEqual( + frappe.db.count( + "Journal Entry Account", + filters={"reference_type": si1.doctype, "reference_name": si1.name, "docstatus": 1}, + ), + 0, + ) + + def test_04_unreconciliation_on_multi_currency_invoice(self): + """ + 2 payments split against 2 foreign currency invoices + """ + self.create_customer("_Test MC Customer USD", "USD") + si1 = self.create_sales_invoice(do_not_submit=True) + si1.currency = "USD" + si1.debit_to = self.debtors_usd + si1.conversion_rate = 80 + si1.save().submit() + + si2 = self.create_sales_invoice(do_not_submit=True) + si2.currency = "USD" + si2.debit_to = self.debtors_usd + si2.conversion_rate = 80 + si2.save().submit() + + pe1 = self.create_payment_entry() + pe1.paid_from = self.debtors_usd + pe1.paid_from_account_currency = "USD" + pe1.source_exchange_rate = 75 + pe1.received_amount = 75 * 100 + pe1.save() + # Allocate payment against both invoices + pe1.append( + "references", + {"reference_doctype": si1.doctype, "reference_name": si1.name, "allocated_amount": 50}, + ) + pe1.append( + "references", + {"reference_doctype": si2.doctype, "reference_name": si2.name, "allocated_amount": 50}, + ) + pe1.save().submit() + + pe2 = self.create_payment_entry() + pe2.paid_from = self.debtors_usd + pe2.paid_from_account_currency = "USD" + pe2.source_exchange_rate = 75 + pe2.received_amount = 75 * 100 + pe2.save() + # Allocate payment against both invoices + pe2.append( + "references", + {"reference_doctype": si1.doctype, "reference_name": si1.name, "allocated_amount": 50}, + ) + pe2.append( + "references", + {"reference_doctype": si2.doctype, "reference_name": si2.name, "allocated_amount": 50}, + ) + pe2.save().submit() + + unreconcile = frappe.get_doc( + { + "doctype": "Unreconcile Payments", + "company": self.company, + "voucher_type": pe2.doctype, + "voucher_no": pe2.name, + } + ) + unreconcile.add_references() + self.assertEqual(len(unreconcile.allocations), 2) + allocations = [x.reference_name for x in unreconcile.allocations] + self.assertEquals([si1.name, si2.name], allocations) + # unreconcile si1 from pe2 + for x in unreconcile.allocations: + if x.reference_name != si1.name: + unreconcile.remove(x) + unreconcile.save().submit() + + # Assert outstanding and unallocated + [doc.reload() for doc in [si1, si2, pe1, pe2]] + self.assertEqual(si1.outstanding_amount, 50) + self.assertEqual(si2.outstanding_amount, 0) + self.assertEqual(len(pe1.references), 2) + self.assertEqual(len(pe2.references), 1) + self.assertEqual(pe1.unallocated_amount, 0) + self.assertEqual(pe2.unallocated_amount, 50) + + # Exc gain/loss JE from PE1 should be available + self.assertEqual( + frappe.db.count( + "Journal Entry Account", + filters={"reference_type": si1.doctype, "reference_name": si1.name, "docstatus": 1}, + ), + 1, + ) From 8fc705ea6a92de51abeffc73f5c2cd2d787e981b Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Mon, 9 Oct 2023 15:17:15 +0530 Subject: [PATCH 48/72] chore: resolve conflicts --- erpnext/accounts/doctype/payment_entry/payment_entry.js | 4 ---- erpnext/public/js/erpnext.bundle.js | 4 ---- 2 files changed, 8 deletions(-) diff --git a/erpnext/accounts/doctype/payment_entry/payment_entry.js b/erpnext/accounts/doctype/payment_entry/payment_entry.js index 48a18e95b7a..d5b047389ed 100644 --- a/erpnext/accounts/doctype/payment_entry/payment_entry.js +++ b/erpnext/accounts/doctype/payment_entry/payment_entry.js @@ -152,11 +152,7 @@ frappe.ui.form.on('Payment Entry', { frm.events.hide_unhide_fields(frm); frm.events.set_dynamic_labels(frm); frm.events.show_general_ledger(frm); -<<<<<<< HEAD -======= - erpnext.accounts.ledger_preview.show_accounting_ledger_preview(frm); erpnext.accounts.unreconcile_payments.add_unreconcile_btn(frm); ->>>>>>> 25fe752185 (chore: move functions to a separate file in utils) }, validate_company: (frm) => { diff --git a/erpnext/public/js/erpnext.bundle.js b/erpnext/public/js/erpnext.bundle.js index 5df6318a1cd..730ee23173d 100644 --- a/erpnext/public/js/erpnext.bundle.js +++ b/erpnext/public/js/erpnext.bundle.js @@ -18,11 +18,7 @@ import "./utils/customer_quick_entry"; import "./utils/supplier_quick_entry"; import "./call_popup/call_popup"; import "./utils/dimension_tree_filter"; -<<<<<<< HEAD -======= -import "./utils/ledger_preview.js"; import "./utils/unreconcile.js"; ->>>>>>> 25fe752185 (chore: move functions to a separate file in utils) import "./utils/barcode_scanner"; import "./telephony"; import "./templates/call_link.html"; From 24852e46c17a0274de4b222004ecc838fb468831 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Mon, 9 Oct 2023 20:56:43 +0530 Subject: [PATCH 49/72] chore: rewrite query using query builder (backport #37310) (#37415) * chore: rewrite query using query builder (cherry picked from commit 25718f5cc758a30b6641e7c9be12f4386dc81a28) * chore: fix shopping cart tests (cherry picked from commit fb51cae88b20274a836165a9b7aa70fe1a436ff4) --------- Co-authored-by: Deepesh Garg --- .../crm/report/lead_details/lead_details.py | 77 ++++++------- .../shopping_cart/test_shopping_cart.py | 103 +++++++++++++----- 2 files changed, 110 insertions(+), 70 deletions(-) diff --git a/erpnext/crm/report/lead_details/lead_details.py b/erpnext/crm/report/lead_details/lead_details.py index 7b8c43b2d65..98dfbec18be 100644 --- a/erpnext/crm/report/lead_details/lead_details.py +++ b/erpnext/crm/report/lead_details/lead_details.py @@ -4,6 +4,7 @@ import frappe from frappe import _ +from frappe.query_builder.functions import Concat_ws, Date def execute(filters=None): @@ -69,53 +70,41 @@ def get_columns(): def get_data(filters): - return frappe.db.sql( - """ - SELECT - `tabLead`.name, - `tabLead`.lead_name, - `tabLead`.status, - `tabLead`.lead_owner, - `tabLead`.territory, - `tabLead`.source, - `tabLead`.email_id, - `tabLead`.mobile_no, - `tabLead`.phone, - `tabLead`.owner, - `tabLead`.company, - concat_ws(', ', - trim(',' from `tabAddress`.address_line1), - trim(',' from tabAddress.address_line2) - ) AS address, - `tabAddress`.state, - `tabAddress`.pincode, - `tabAddress`.country - FROM - `tabLead` left join `tabDynamic Link` on ( - `tabLead`.name = `tabDynamic Link`.link_name and - `tabDynamic Link`.parenttype = 'Address') - left join `tabAddress` on ( - `tabAddress`.name=`tabDynamic Link`.parent) - WHERE - company = %(company)s - AND DATE(`tabLead`.creation) BETWEEN %(from_date)s AND %(to_date)s - {conditions} - ORDER BY - `tabLead`.creation asc """.format( - conditions=get_conditions(filters) - ), - filters, - as_dict=1, + lead = frappe.qb.DocType("Lead") + address = frappe.qb.DocType("Address") + dynamic_link = frappe.qb.DocType("Dynamic Link") + + query = ( + frappe.qb.from_(lead) + .left_join(dynamic_link) + .on((lead.name == dynamic_link.link_name) & (dynamic_link.parenttype == "Address")) + .left_join(address) + .on(address.name == dynamic_link.parent) + .select( + lead.name, + lead.lead_name, + lead.status, + lead.lead_owner, + lead.territory, + lead.source, + lead.email_id, + lead.mobile_no, + lead.phone, + lead.owner, + lead.company, + (Concat_ws(", ", address.address_line1, address.address_line2)).as_("address"), + address.state, + address.pincode, + address.country, + ) + .where(lead.company == filters.company) + .where(Date(lead.creation).between(filters.from_date, filters.to_date)) ) - -def get_conditions(filters): - conditions = [] - if filters.get("territory"): - conditions.append(" and `tabLead`.territory=%(territory)s") + query = query.where(lead.territory == filters.get("territory")) if filters.get("status"): - conditions.append(" and `tabLead`.status=%(status)s") + query = query.where(lead.status == filters.get("status")) - return " ".join(conditions) if conditions else "" + return query.run(as_dict=1) diff --git a/erpnext/e_commerce/shopping_cart/test_shopping_cart.py b/erpnext/e_commerce/shopping_cart/test_shopping_cart.py index f44f8fe2984..363a80545b0 100644 --- a/erpnext/e_commerce/shopping_cart/test_shopping_cart.py +++ b/erpnext/e_commerce/shopping_cart/test_shopping_cart.py @@ -17,7 +17,6 @@ from erpnext.e_commerce.shopping_cart.cart import ( request_for_quotation, update_cart, ) -from erpnext.tests.utils import create_test_contact_and_address class TestShoppingCart(unittest.TestCase): @@ -28,7 +27,6 @@ class TestShoppingCart(unittest.TestCase): def setUp(self): frappe.set_user("Administrator") - create_test_contact_and_address() self.enable_shopping_cart() if not frappe.db.exists("Website Item", {"item_code": "_Test Item"}): make_website_item(frappe.get_cached_doc("Item", "_Test Item")) @@ -46,48 +44,57 @@ class TestShoppingCart(unittest.TestCase): frappe.db.sql("delete from `tabTax Rule`") def test_get_cart_new_user(self): - self.login_as_new_user() - + self.login_as_customer( + "test_contact_two_customer@example.com", "_Test Contact 2 For _Test Customer" + ) + create_address_and_contact( + address_title="_Test Address for Customer 2", + first_name="_Test Contact for Customer 2", + email="test_contact_two_customer@example.com", + customer="_Test Customer 2", + ) # test if lead is created and quotation with new lead is fetched - quotation = _get_cart_quotation() + customer = frappe.get_doc("Customer", "_Test Customer 2") + quotation = _get_cart_quotation(party=customer) self.assertEqual(quotation.quotation_to, "Customer") self.assertEqual( quotation.contact_person, - frappe.db.get_value("Contact", dict(email_id="test_cart_user@example.com")), + frappe.db.get_value("Contact", dict(email_id="test_contact_two_customer@example.com")), ) self.assertEqual(quotation.contact_email, frappe.session.user) return quotation - def test_get_cart_customer(self): - def validate_quotation(): + def test_get_cart_customer(self, customer="_Test Customer 2"): + def validate_quotation(customer_name): # test if quotation with customer is fetched - quotation = _get_cart_quotation() + party = frappe.get_doc("Customer", customer_name) + quotation = _get_cart_quotation(party=party) self.assertEqual(quotation.quotation_to, "Customer") - self.assertEqual(quotation.party_name, "_Test Customer") + self.assertEqual(quotation.party_name, customer_name) self.assertEqual(quotation.contact_email, frappe.session.user) return quotation - self.login_as_customer( - "test_contact_two_customer@example.com", "_Test Contact 2 For _Test Customer" - ) - validate_quotation() - - self.login_as_customer() - quotation = validate_quotation() - + quotation = validate_quotation(customer) return quotation def test_add_to_cart(self): - self.login_as_customer() - + self.login_as_customer( + "test_contact_two_customer@example.com", "_Test Contact 2 For _Test Customer" + ) + create_address_and_contact( + address_title="_Test Address for Customer 2", + first_name="_Test Contact for Customer 2", + email="test_contact_two_customer@example.com", + customer="_Test Customer 2", + ) # clear existing quotations self.clear_existing_quotations() # add first item update_cart("_Test Item", 1) - quotation = self.test_get_cart_customer() + quotation = self.test_get_cart_customer("_Test Customer 2") self.assertEqual(quotation.get("items")[0].item_code, "_Test Item") self.assertEqual(quotation.get("items")[0].qty, 1) @@ -95,7 +102,7 @@ class TestShoppingCart(unittest.TestCase): # add second item update_cart("_Test Item 2", 1) - quotation = self.test_get_cart_customer() + quotation = self.test_get_cart_customer("_Test Customer 2") self.assertEqual(quotation.get("items")[1].item_code, "_Test Item 2") self.assertEqual(quotation.get("items")[1].qty, 1) self.assertEqual(quotation.get("items")[1].amount, 20) @@ -108,7 +115,7 @@ class TestShoppingCart(unittest.TestCase): # update first item update_cart("_Test Item", 5) - quotation = self.test_get_cart_customer() + quotation = self.test_get_cart_customer("_Test Customer 2") self.assertEqual(quotation.get("items")[0].item_code, "_Test Item") self.assertEqual(quotation.get("items")[0].qty, 5) self.assertEqual(quotation.get("items")[0].amount, 50) @@ -121,7 +128,7 @@ class TestShoppingCart(unittest.TestCase): # remove first item update_cart("_Test Item", 0) - quotation = self.test_get_cart_customer() + quotation = self.test_get_cart_customer("_Test Customer 2") self.assertEqual(quotation.get("items")[0].item_code, "_Test Item 2") self.assertEqual(quotation.get("items")[0].qty, 1) @@ -132,7 +139,17 @@ class TestShoppingCart(unittest.TestCase): @unittest.skip("Flaky in CI") def test_tax_rule(self): self.create_tax_rule() - self.login_as_customer() + + self.login_as_customer( + "test_contact_two_customer@example.com", "_Test Contact 2 For _Test Customer" + ) + create_address_and_contact( + address_title="_Test Address for Customer 2", + first_name="_Test Contact for Customer 2", + email="test_contact_two_customer@example.com", + customer="_Test Customer 2", + ) + quotation = self.create_quotation() from erpnext.accounts.party import set_taxes @@ -320,7 +337,7 @@ class TestShoppingCart(unittest.TestCase): if frappe.db.exists("User", email): return - frappe.get_doc( + user = frappe.get_doc( { "doctype": "User", "user_type": "Website User", @@ -330,6 +347,40 @@ class TestShoppingCart(unittest.TestCase): } ).insert(ignore_permissions=True) + user.add_roles("Customer") + + +def create_address_and_contact(**kwargs): + if not frappe.db.get_value("Address", {"address_title": kwargs.get("address_title")}): + frappe.get_doc( + { + "doctype": "Address", + "address_title": kwargs.get("address_title"), + "address_type": kwargs.get("address_type") or "Office", + "address_line1": kwargs.get("address_line1") or "Station Road", + "city": kwargs.get("city") or "_Test City", + "state": kwargs.get("state") or "Test State", + "country": kwargs.get("country") or "India", + "links": [ + {"link_doctype": "Customer", "link_name": kwargs.get("customer") or "_Test Customer"} + ], + } + ).insert() + + if not frappe.db.get_value("Contact", {"first_name": kwargs.get("first_name")}): + contact = frappe.get_doc( + { + "doctype": "Contact", + "first_name": kwargs.get("first_name"), + "links": [ + {"link_doctype": "Customer", "link_name": kwargs.get("customer") or "_Test Customer"} + ], + } + ) + contact.add_email(kwargs.get("email") or "test_contact_customer@example.com", is_primary=True) + contact.add_phone(kwargs.get("phone") or "+91 0000000000", is_primary_phone=True) + contact.insert() + test_dependencies = [ "Sales Taxes and Charges Template", From eed58634ba09ff78bf041d93c63c25146b25ecb9 Mon Sep 17 00:00:00 2001 From: David Arnold Date: Sun, 17 Sep 2023 20:17:40 +0200 Subject: [PATCH 50/72] fix: payment request rounding in multi-currency and on status update (cherry picked from commit 6e1ad4c5bded8193be96a3e5bbc1df1aa0e3f956) --- erpnext/accounts/doctype/payment_request/payment_request.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/accounts/doctype/payment_request/payment_request.py b/erpnext/accounts/doctype/payment_request/payment_request.py index 0955664d98b..f6653f87f0f 100644 --- a/erpnext/accounts/doctype/payment_request/payment_request.py +++ b/erpnext/accounts/doctype/payment_request/payment_request.py @@ -249,7 +249,7 @@ class PaymentRequest(Document): if ( party_account_currency == ref_doc.company_currency and party_account_currency != self.currency ): - party_amount = ref_doc.base_grand_total + party_amount = ref_doc.get("base_rounded_total") or ref_doc.get("base_grand_total") else: party_amount = self.grand_total From b22ac137f50b979331eca4349702343f9af4714f Mon Sep 17 00:00:00 2001 From: Gursheen Anand Date: Fri, 22 Sep 2023 12:41:17 +0530 Subject: [PATCH 51/72] fix: allocate amt for payment term invoices (cherry picked from commit ac28a5b372d96badd7ba808308a3f1270943f892) --- .../doctype/payment_entry/payment_entry.js | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/erpnext/accounts/doctype/payment_entry/payment_entry.js b/erpnext/accounts/doctype/payment_entry/payment_entry.js index d43a057db02..c72df4224ee 100644 --- a/erpnext/accounts/doctype/payment_entry/payment_entry.js +++ b/erpnext/accounts/doctype/payment_entry/payment_entry.js @@ -858,20 +858,25 @@ frappe.ui.form.on('Payment Entry', { } } + let outstanding_amount; $.each(frm.doc.references || [], function(i, row) { if (frappe.flags.allocate_payment_amount == 0) { //If allocate payment amount checkbox is unchecked, set zero to allocate amount row.allocated_amount = 0; - } else if (frappe.flags.allocate_payment_amount != 0 && (!row.allocated_amount || paid_amount_change)) { - if (row.outstanding_amount > 0 && allocated_positive_outstanding >= 0) { - row.allocated_amount = (row.outstanding_amount >= allocated_positive_outstanding) ? - allocated_positive_outstanding : row.outstanding_amount; + } else if (frappe.flags.allocate_payment_amount != 0 && (row.payment_term || !row.allocated_amount || paid_amount_change)) { + if(row.payment_term) + outstanding_amount = row.allocated_amount; + else + outstanding_amount = row.outstanding_amount; + if (outstanding_amount > 0 && allocated_positive_outstanding >= 0) { + row.allocated_amount = (outstanding_amount >= allocated_positive_outstanding) ? + allocated_positive_outstanding : outstanding_amount; allocated_positive_outstanding -= flt(row.allocated_amount); - } else if (row.outstanding_amount < 0 && allocated_negative_outstanding) { - row.allocated_amount = (Math.abs(row.outstanding_amount) >= allocated_negative_outstanding) ? - -1*allocated_negative_outstanding : row.outstanding_amount; + } else if (outstanding_amount < 0 && allocated_negative_outstanding) { + row.allocated_amount = (Math.abs(outstanding_amount) >= allocated_negative_outstanding) ? + -1*allocated_negative_outstanding : outstanding_amount; allocated_negative_outstanding -= Math.abs(flt(row.allocated_amount)); } } From 06b04770fc878c00e1073ca07af1c09286f92a08 Mon Sep 17 00:00:00 2001 From: Gursheen Anand Date: Sat, 23 Sep 2023 14:57:53 +0530 Subject: [PATCH 52/72] fix: split inv allocated amt on server side (cherry picked from commit b3aa201eb570167c44225438031438065eef9728) --- .../doctype/payment_entry/payment_entry.js | 18 +++++++----------- .../doctype/payment_entry/payment_entry.py | 15 ++++++++------- 2 files changed, 15 insertions(+), 18 deletions(-) diff --git a/erpnext/accounts/doctype/payment_entry/payment_entry.js b/erpnext/accounts/doctype/payment_entry/payment_entry.js index c72df4224ee..a6ea140e74d 100644 --- a/erpnext/accounts/doctype/payment_entry/payment_entry.js +++ b/erpnext/accounts/doctype/payment_entry/payment_entry.js @@ -864,19 +864,15 @@ frappe.ui.form.on('Payment Entry', { //If allocate payment amount checkbox is unchecked, set zero to allocate amount row.allocated_amount = 0; - } else if (frappe.flags.allocate_payment_amount != 0 && (row.payment_term || !row.allocated_amount || paid_amount_change)) { - if(row.payment_term) - outstanding_amount = row.allocated_amount; - else - outstanding_amount = row.outstanding_amount; - if (outstanding_amount > 0 && allocated_positive_outstanding >= 0) { - row.allocated_amount = (outstanding_amount >= allocated_positive_outstanding) ? - allocated_positive_outstanding : outstanding_amount; + } else if (frappe.flags.allocate_payment_amount != 0 && (!row.allocated_amount || paid_amount_change)) { + if (row.outstanding_amount > 0 && allocated_positive_outstanding >= 0) { + row.allocated_amount = (row.outstanding_amount >= allocated_positive_outstanding) ? + allocated_positive_outstanding : row.outstanding_amount; allocated_positive_outstanding -= flt(row.allocated_amount); - } else if (outstanding_amount < 0 && allocated_negative_outstanding) { - row.allocated_amount = (Math.abs(outstanding_amount) >= allocated_negative_outstanding) ? - -1*allocated_negative_outstanding : outstanding_amount; + } else if (row.outstanding_amount < 0 && allocated_negative_outstanding) { + row.allocated_amount = (Math.abs(row.outstanding_amount) >= allocated_negative_outstanding) ? + -1*allocated_negative_outstanding : row.outstanding_amount; allocated_negative_outstanding -= Math.abs(flt(row.allocated_amount)); } } diff --git a/erpnext/accounts/doctype/payment_entry/payment_entry.py b/erpnext/accounts/doctype/payment_entry/payment_entry.py index c5501a58306..9c22deadb2f 100644 --- a/erpnext/accounts/doctype/payment_entry/payment_entry.py +++ b/erpnext/accounts/doctype/payment_entry/payment_entry.py @@ -227,16 +227,18 @@ class PaymentEntry(AccountsController): # if no payment template is used by invoice and has a custom term(no `payment_term`), then invoice outstanding will be in 'None' key latest = latest.get(d.payment_term) or latest.get(None) - # The reference has already been fully paid if not latest: frappe.throw( _("{0} {1} has already been fully paid.").format(_(d.reference_doctype), d.reference_name) ) # The reference has already been partly paid - elif latest.outstanding_amount < latest.invoice_amount and flt( - d.outstanding_amount, d.precision("outstanding_amount") - ) != flt(latest.outstanding_amount, d.precision("outstanding_amount")): + elif ( + latest.outstanding_amount < latest.invoice_amount + and flt(d.outstanding_amount, d.precision("outstanding_amount")) + != flt(latest.outstanding_amount, d.precision("outstanding_amount")) + and d.payment_term == "" + ): frappe.throw( _( "{0} {1} has already been partly paid. Please use the 'Get Outstanding Invoice' or the 'Get Outstanding Orders' button to get the latest outstanding amounts." @@ -1600,11 +1602,10 @@ def split_invoices_based_on_payment_terms(outstanding_invoices, company): "voucher_type": d.voucher_type, "posting_date": d.posting_date, "invoice_amount": flt(d.invoice_amount), - "outstanding_amount": flt(d.outstanding_amount), - "payment_term_outstanding": payment_term_outstanding, - "allocated_amount": payment_term_outstanding + "outstanding_amount": payment_term_outstanding if payment_term_outstanding else d.outstanding_amount, + "payment_term_outstanding": payment_term_outstanding, "payment_amount": payment_term.payment_amount, "payment_term": payment_term.payment_term, } From bc0db696c9cae33a2ea8ca105f3f2c32cb7c879c Mon Sep 17 00:00:00 2001 From: Gursheen Anand Date: Sat, 23 Sep 2023 14:59:12 +0530 Subject: [PATCH 53/72] chore: remove unused variable (cherry picked from commit 545f2ccdf1a66ccdcea04de532e637fa502b7e67) --- erpnext/accounts/doctype/payment_entry/payment_entry.js | 1 - 1 file changed, 1 deletion(-) diff --git a/erpnext/accounts/doctype/payment_entry/payment_entry.js b/erpnext/accounts/doctype/payment_entry/payment_entry.js index a6ea140e74d..d43a057db02 100644 --- a/erpnext/accounts/doctype/payment_entry/payment_entry.js +++ b/erpnext/accounts/doctype/payment_entry/payment_entry.js @@ -858,7 +858,6 @@ frappe.ui.form.on('Payment Entry', { } } - let outstanding_amount; $.each(frm.doc.references || [], function(i, row) { if (frappe.flags.allocate_payment_amount == 0) { //If allocate payment amount checkbox is unchecked, set zero to allocate amount From f5245f6b3fdf8f89702458fd7bb2e04e4eacd927 Mon Sep 17 00:00:00 2001 From: Gursheen Anand Date: Thu, 21 Sep 2023 17:26:20 +0530 Subject: [PATCH 54/72] feat: allow on submit fields (cherry picked from commit e922ec60eb44196255a1e4b9cce839d6f7f05493) # Conflicts: # erpnext/accounts/doctype/purchase_invoice/purchase_invoice.json --- .../purchase_invoice/purchase_invoice.json | 25 ++++++++++++++----- .../purchase_invoice_item.json | 1 + .../purchase_taxes_and_charges.json | 2 ++ 3 files changed, 22 insertions(+), 6 deletions(-) diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.json b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.json index 7e848a0b4e8..864f0b81890 100644 --- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.json +++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.json @@ -166,6 +166,7 @@ "against_expense_account", "column_break_63", "unrealized_profit_loss_account", + "repost_required", "subscription_section", "auto_repeat", "update_auto_repeat_reference", @@ -190,8 +191,7 @@ "inter_company_invoice_reference", "is_old_subcontracting_flow", "remarks", - "connections_tab", - "column_break_38" + "connections_tab" ], "fields": [ { @@ -987,6 +987,7 @@ "print_hide": 1 }, { + "allow_on_submit": 1, "fieldname": "cash_bank_account", "fieldtype": "Link", "label": "Cash/Bank Account", @@ -1050,6 +1051,7 @@ "fieldtype": "Column Break" }, { + "allow_on_submit": 1, "depends_on": "eval:flt(doc.write_off_amount)!=0", "fieldname": "write_off_account", "fieldtype": "Link", @@ -1213,6 +1215,7 @@ "read_only": 1 }, { + "allow_on_submit": 1, "default": "No", "fieldname": "is_opening", "fieldtype": "Select", @@ -1345,6 +1348,7 @@ "options": "Project" }, { + "allow_on_submit": 1, "depends_on": "eval:doc.is_internal_supplier", "description": "Unrealized Profit/Loss account for intra-company transfers", "fieldname": "unrealized_profit_loss_account", @@ -1495,10 +1499,6 @@ "fieldname": "column_break_6", "fieldtype": "Column Break" }, - { - "fieldname": "column_break_38", - "fieldtype": "Column Break" - }, { "fieldname": "column_break_50", "fieldtype": "Column Break" @@ -1569,13 +1569,26 @@ "fieldname": "use_company_roundoff_cost_center", "fieldtype": "Check", "label": "Use Company Default Round Off Cost Center" + }, + { + "default": "0", + "fieldname": "repost_required", + "fieldtype": "Check", + "hidden": 1, + "label": "Repost Required", + "options": "Account", + "read_only": 1 } ], "icon": "fa fa-file-text", "idx": 204, "is_submittable": 1, "links": [], +<<<<<<< HEAD "modified": "2023-10-01 21:01:47.282533", +======= + "modified": "2023-09-21 12:22:04.545106", +>>>>>>> e922ec60eb (feat: allow on submit fields) "modified_by": "Administrator", "module": "Accounts", "name": "Purchase Invoice", diff --git a/erpnext/accounts/doctype/purchase_invoice_item/purchase_invoice_item.json b/erpnext/accounts/doctype/purchase_invoice_item/purchase_invoice_item.json index 04f28beda9e..c7357360ec0 100644 --- a/erpnext/accounts/doctype/purchase_invoice_item/purchase_invoice_item.json +++ b/erpnext/accounts/doctype/purchase_invoice_item/purchase_invoice_item.json @@ -468,6 +468,7 @@ "label": "Accounting" }, { + "allow_on_submit": 1, "fieldname": "expense_account", "fieldtype": "Link", "label": "Expense Head", diff --git a/erpnext/accounts/doctype/purchase_taxes_and_charges/purchase_taxes_and_charges.json b/erpnext/accounts/doctype/purchase_taxes_and_charges/purchase_taxes_and_charges.json index d86abade924..347cae05b72 100644 --- a/erpnext/accounts/doctype/purchase_taxes_and_charges/purchase_taxes_and_charges.json +++ b/erpnext/accounts/doctype/purchase_taxes_and_charges/purchase_taxes_and_charges.json @@ -86,6 +86,7 @@ "fieldtype": "Column Break" }, { + "allow_on_submit": 1, "columns": 2, "fieldname": "account_head", "fieldtype": "Link", @@ -97,6 +98,7 @@ "reqd": 1 }, { + "allow_on_submit": 1, "default": ":Company", "fieldname": "cost_center", "fieldtype": "Link", From 79e414cb9747570e2ca747d1a9ab7dd18a453b3b Mon Sep 17 00:00:00 2001 From: Gursheen Anand Date: Thu, 21 Sep 2023 17:28:07 +0530 Subject: [PATCH 55/72] refactor: move reposting logic to common controller (cherry picked from commit 68effd93bdb1a91a8625d983cd9b8afeb7b1eb3b) # Conflicts: # erpnext/accounts/doctype/sales_invoice/sales_invoice.py --- .../doctype/sales_invoice/sales_invoice.py | 72 +++++++------------ erpnext/controllers/accounts_controller.py | 55 ++++++++++++++ 2 files changed, 80 insertions(+), 47 deletions(-) diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py index 7d431b4205b..a8525522df3 100644 --- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py +++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py @@ -11,9 +11,6 @@ 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, @@ -529,54 +526,32 @@ class SalesInvoice(SellingController): 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 - + fields_to_check = [ + "additional_discount_account", + "cash_bank_account", + "account_for_change_amount", + "write_off_account", + "loyalty_redemption_account", + "unrealized_profit_loss_account", + ] + child_tables = { + "items": ("income_account", "expense_account", "discount_account"), + "taxes": ("account_head",), + } + self.needs_repost = self.check_if_fields_updated(fields_to_check, child_tables) + self.validate_deferred_accounting_before_repost() self.validate_accounts() + self.db_set("repost_required", self.needs_repost) +<<<<<<< HEAD # validate if deferred revenue is enabled for any item # Don't allow to update the invoice if deferred revenue is enabled +======= + def validate_deferred_accounting_before_repost(self): + # validate if deferred revenue is enabled for any item + # Don't allow to update the invoice if deferred revenue is enabled + if self.needs_repost: +>>>>>>> 68effd93bd (refactor: move reposting logic to common controller) for item in self.get("items"): if item.enable_deferred_revenue: frappe.throw( @@ -584,6 +559,7 @@ class SalesInvoice(SellingController): "Deferred Revenue is enabled for item {0}. You cannot update the invoice after submission." ).format(item.item_code) ) +<<<<<<< HEAD self.db_set("repost_required", needs_repost) @@ -601,6 +577,8 @@ class SalesInvoice(SellingController): return True return False +======= +>>>>>>> 68effd93bd (refactor: move reposting logic to common controller) @frappe.whitelist() def repost_accounting_entries(self): diff --git a/erpnext/controllers/accounts_controller.py b/erpnext/controllers/accounts_controller.py index 534119762af..78f171089b3 100644 --- a/erpnext/controllers/accounts_controller.py +++ b/erpnext/controllers/accounts_controller.py @@ -2159,6 +2159,44 @@ class AccountsController(TransactionBase): _("Select finance book for the item {0} at row {1}").format(item.item_code, item.idx) ) + def check_if_fields_updated(self, fields_to_check, child_tables): + # 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 + needs_repost = doc_before_update.get("is_opening") != self.is_opening + + if not needs_repost: + # Parent Level Accounts excluding party account + fields_to_check += accounting_dimensions + for field in fields_to_check: + if doc_before_update.get(field) != self.get(field): + needs_repost = 1 + break + + if not needs_repost: + # Check for child tables + for table in child_tables: + needs_repost = check_if_child_table_updated( + doc_before_update.get(table), self.get(table), child_tables[table] + ) + if needs_repost: + break + + return needs_repost + + @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")) + @frappe.whitelist() def get_tax_rate(account_head): @@ -3044,6 +3082,23 @@ def update_child_qty_rate(parent_doctype, trans_items, parent_doctype_name, chil parent.set_status() +def check_if_child_table_updated( + child_table_before_update, child_table_after_update, fields_to_check +): + accounting_dimensions = get_accounting_dimensions() + ["cost_center", "project"] + # Check if any field affecting accounting entry is altered + for index, item in enumerate(child_table_after_update): + for field in fields_to_check: + if child_table_before_update[index].get(field) != item.get(field): + return True + + for dimension in accounting_dimensions: + if child_table_before_update[index].get(dimension) != item.get(dimension): + return True + + return False + + @erpnext.allow_regional def validate_regional(doc): pass From cde848dc7fa755201391fd024e94e0a6935c632e Mon Sep 17 00:00:00 2001 From: Gursheen Anand Date: Thu, 21 Sep 2023 17:29:14 +0530 Subject: [PATCH 56/72] feat: add repost btn in invoice (cherry picked from commit e77814fbc0b15a47c8ecde579fc0d2a9e200a476) --- .../purchase_invoice/purchase_invoice.js | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js index ceb8204bd5d..d8f02c19bee 100644 --- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js +++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js @@ -59,6 +59,25 @@ erpnext.accounts.PurchaseInvoice = class PurchaseInvoice extends erpnext.buying. this.show_stock_ledger(); } + 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(!doc.is_return && doc.docstatus == 1 && doc.outstanding_amount != 0){ if(doc.on_hold) { this.frm.add_custom_button( From 2d13dda49c59784fee4257ff56ebf679617713f9 Mon Sep 17 00:00:00 2001 From: Gursheen Anand Date: Thu, 21 Sep 2023 17:30:53 +0530 Subject: [PATCH 57/72] feat: allow repost for pi (cherry picked from commit 23470bf52dd8f7b5fee127e5bc506938ae9632f3) --- .../purchase_invoice/purchase_invoice.py | 25 +++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py index 9737ee2c53e..1842ad903b9 100644 --- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py +++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py @@ -529,6 +529,31 @@ class PurchaseInvoice(BuyingController): self.process_common_party_accounting() + def on_update_after_submit(self): + if hasattr(self, "repost_required"): + fields_to_check = [ + "cash_bank_account", + "write_off_account" "unrealized_profit_loss_account", + ] + child_tables = {"items": ("expense_account",), "taxes": ("account_head",)} + self.needs_repost = self.check_if_fields_updated(fields_to_check, child_tables) + self.validate_deferred_accounting_before_repost() + self.validate_write_off_account() + self.validate_expense_account() + self.db_set("repost_required", self.needs_repost) + + def validate_deferred_accounting_before_repost(self): + # validate if deferred expense is enabled for any item + # Don't allow to update the invoice if deferred expense is enabled + if self.needs_repost: + for item in self.get("items"): + if item.enable_deferred_expense: + frappe.throw( + _( + "Deferred Expense is enabled for item {0}. You cannot update the invoice after submission." + ).format(item.item_code) + ) + def make_gl_entries(self, gl_entries=None, from_repost=False): if not gl_entries: gl_entries = self.get_gl_entries() From 6c8a65e03b9ab4b57f5c254566ea9a57c79dd2e8 Mon Sep 17 00:00:00 2001 From: Gursheen Anand Date: Thu, 21 Sep 2023 17:41:59 +0530 Subject: [PATCH 58/72] fix: linting issues (cherry picked from commit c88f6d1fa7f28453d81cabf6726bf8eb1b5d8969) --- erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py index 1842ad903b9..cde6221da49 100644 --- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py +++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py @@ -533,7 +533,8 @@ class PurchaseInvoice(BuyingController): if hasattr(self, "repost_required"): fields_to_check = [ "cash_bank_account", - "write_off_account" "unrealized_profit_loss_account", + "write_off_account", + "unrealized_profit_loss_account", ] child_tables = {"items": ("expense_account",), "taxes": ("account_head",)} self.needs_repost = self.check_if_fields_updated(fields_to_check, child_tables) From a512d27dbb53999556b0194dd4f69e17147995d1 Mon Sep 17 00:00:00 2001 From: Gursheen Anand Date: Fri, 22 Sep 2023 11:22:25 +0530 Subject: [PATCH 59/72] test: reposted acc entries for pi (cherry picked from commit c66c4385759b25243f946b708055bccc12561809) # Conflicts: # erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py --- .../purchase_invoice/purchase_invoice.js | 4 +- .../purchase_invoice/test_purchase_invoice.py | 86 ++++++++++++++++++- 2 files changed, 87 insertions(+), 3 deletions(-) diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js index d8f02c19bee..ee5a50af058 100644 --- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js +++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js @@ -60,7 +60,7 @@ erpnext.accounts.PurchaseInvoice = class PurchaseInvoice extends erpnext.buying. } 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.set_intro(__("Accounting entries for this invoice need to be reposted. Please click on 'Repost' button to update.")); this.frm.add_custom_button(__('Repost Accounting Entries'), () => { this.frm.call({ @@ -70,7 +70,7 @@ erpnext.accounts.PurchaseInvoice = class PurchaseInvoice extends erpnext.buying. freeze_message: __('Reposting...'), callback: (r) => { if (!r.exc) { - frappe.msgprint(__('Accounting Entries are reposted')); + frappe.msgprint(__('Accounting Entries are reposted.')); me.frm.refresh(); } } diff --git a/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py b/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py index 30265aeb50e..51208885044 100644 --- a/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py +++ b/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py @@ -1710,6 +1710,65 @@ class TestPurchaseInvoice(unittest.TestCase, StockTestMixin): self.assertTrue(return_pi.docstatus == 1) +<<<<<<< HEAD +======= + def test_advance_entries_as_asset(self): + from erpnext.accounts.doctype.payment_entry.test_payment_entry import create_payment_entry + + account = create_account( + parent_account="Current Assets - _TC", + account_name="Advances Paid", + company="_Test Company", + account_type="Receivable", + ) + + set_advance_flag(company="_Test Company", flag=1, default_account=account) + + pe = create_payment_entry( + company="_Test Company", + payment_type="Pay", + party_type="Supplier", + party="_Test Supplier", + paid_from="Cash - _TC", + paid_to="Creditors - _TC", + paid_amount=500, + ) + pe.submit() + + pi = make_purchase_invoice( + company="_Test Company", + do_not_save=True, + do_not_submit=True, + rate=1000, + price_list_rate=1000, + qty=1, + ) + pi.base_grand_total = 1000 + pi.grand_total = 1000 + pi.set_advances() + for advance in pi.advances: + advance.allocated_amount = 500 if advance.reference_name == pe.name else 0 + pi.save() + pi.submit() + + self.assertEqual(pi.advances[0].allocated_amount, 500) + + # Check GL Entry against payment doctype + expected_gle = [ + ["Advances Paid - _TC", 0.0, 500, nowdate()], + ["Cash - _TC", 0.0, 500, nowdate()], + ["Creditors - _TC", 500, 0.0, nowdate()], + ["Creditors - _TC", 500, 0.0, nowdate()], + ] + + check_gl_entries(self, pe.name, expected_gle, nowdate(), voucher_type="Payment Entry") + + pi.load_from_db() + self.assertEqual(pi.outstanding_amount, 500) + + set_advance_flag(company="_Test Company", flag=0, default_account="") + +>>>>>>> c66c438575 (test: reposted acc entries for pi) def test_gl_entries_for_standalone_debit_note(self): make_purchase_invoice(qty=5, rate=500, update_stock=True) @@ -1796,7 +1855,6 @@ class TestPurchaseInvoice(unittest.TestCase, StockTestMixin): pi = make_purchase_invoice( company="_Test Company", - customer="_Test Supplier", do_not_save=True, do_not_submit=True, rate=1000, @@ -1826,6 +1884,32 @@ class TestPurchaseInvoice(unittest.TestCase, StockTestMixin): clear_dimension_defaults("Branch") disable_dimension() + def test_repost_accounting_entries(self): + pi = make_purchase_invoice( + rate=1000, + price_list_rate=1000, + qty=1, + ) + expected_gle = [ + ["_Test Account Cost for Goods Sold - _TC", 1000, 0.0, nowdate()], + ["Creditors - _TC", 0.0, 1000, nowdate()], + ] + check_gl_entries(self, pi.name, expected_gle, nowdate()) + + pi.items[0].expense_account = "Service - _TC" + pi.save() + pi.load_from_db() + self.assertTrue(pi.repost_required) + pi.repost_accounting_entries() + + expected_gle = [ + ["Creditors - _TC", 0.0, 1000, nowdate()], + ["Service - _TC", 1000, 0.0, nowdate()], + ] + check_gl_entries(self, pi.name, expected_gle, nowdate()) + pi.load_from_db() + self.assertFalse(pi.repost_required) + def check_gl_entries( doc, From 8c83bbc0962fbad193ef91cd7ee5158018712c21 Mon Sep 17 00:00:00 2001 From: Gursheen Anand Date: Sat, 23 Sep 2023 19:11:46 +0530 Subject: [PATCH 60/72] refactor: remove unused method (cherry picked from commit ba7212c98b81a43e5f63d010eadf99635c95e349) --- .../accounts/doctype/sales_invoice/sales_invoice.py | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py index a8525522df3..361e5769df5 100644 --- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py +++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py @@ -580,17 +580,6 @@ class SalesInvoice(SellingController): ======= >>>>>>> 68effd93bd (refactor: move reposting logic to common controller) - @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 From 677525b2cf5f9c9b422c6e145b1ec3cc1354813c Mon Sep 17 00:00:00 2001 From: Gursheen Anand Date: Sat, 23 Sep 2023 19:13:33 +0530 Subject: [PATCH 61/72] refactor: use repost accounting legder (cherry picked from commit 7ebf0836833d9226ca9e0d2d231b35be4f438842) --- erpnext/controllers/accounts_controller.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/erpnext/controllers/accounts_controller.py b/erpnext/controllers/accounts_controller.py index 78f171089b3..072537dcbb4 100644 --- a/erpnext/controllers/accounts_controller.py +++ b/erpnext/controllers/accounts_controller.py @@ -2189,10 +2189,11 @@ class AccountsController(TransactionBase): @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() + repost_ledger = frappe.new_doc("Repost Accounting Ledger") + repost_ledger.company = self.company + repost_ledger.append("vouchers", {"voucher_type": self.doctype, "voucher_no": self.name}) + repost_ledger.insert() + repost_ledger.submit() self.db_set("repost_required", 0) else: frappe.throw(_("No updates pending for reposting")) From c9bcf79e83d16e17627fd116c45e1ea2abfd21dc Mon Sep 17 00:00:00 2001 From: Gursheen Anand Date: Sat, 23 Sep 2023 19:18:06 +0530 Subject: [PATCH 62/72] refactor: remove repeated validation for voucher (cherry picked from commit a856091ff4756462b6cf0f02493f6fb6ddf59921) # Conflicts: # erpnext/accounts/doctype/sales_invoice/sales_invoice.py --- .../doctype/purchase_invoice/purchase_invoice.py | 13 ------------- .../accounts/doctype/sales_invoice/sales_invoice.py | 4 +++- 2 files changed, 3 insertions(+), 14 deletions(-) diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py index cde6221da49..c17062c943a 100644 --- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py +++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py @@ -538,23 +538,10 @@ class PurchaseInvoice(BuyingController): ] child_tables = {"items": ("expense_account",), "taxes": ("account_head",)} self.needs_repost = self.check_if_fields_updated(fields_to_check, child_tables) - self.validate_deferred_accounting_before_repost() self.validate_write_off_account() self.validate_expense_account() self.db_set("repost_required", self.needs_repost) - def validate_deferred_accounting_before_repost(self): - # validate if deferred expense is enabled for any item - # Don't allow to update the invoice if deferred expense is enabled - if self.needs_repost: - for item in self.get("items"): - if item.enable_deferred_expense: - frappe.throw( - _( - "Deferred Expense is enabled for item {0}. You cannot update the invoice after submission." - ).format(item.item_code) - ) - def make_gl_entries(self, gl_entries=None, from_repost=False): if not gl_entries: gl_entries = self.get_gl_entries() diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py index 361e5769df5..6b8baa8b2c9 100644 --- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py +++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py @@ -539,10 +539,10 @@ class SalesInvoice(SellingController): "taxes": ("account_head",), } self.needs_repost = self.check_if_fields_updated(fields_to_check, child_tables) - self.validate_deferred_accounting_before_repost() self.validate_accounts() self.db_set("repost_required", self.needs_repost) +<<<<<<< HEAD <<<<<<< HEAD # validate if deferred revenue is enabled for any item # Don't allow to update the invoice if deferred revenue is enabled @@ -580,6 +580,8 @@ class SalesInvoice(SellingController): ======= >>>>>>> 68effd93bd (refactor: move reposting logic to common controller) +======= +>>>>>>> a856091ff4 (refactor: remove repeated validation for voucher) def set_paid_amount(self): paid_amount = 0.0 base_paid_amount = 0.0 From 4123e7b2447d7ed5de4957ac48ea1deff5a50649 Mon Sep 17 00:00:00 2001 From: Gursheen Anand Date: Sat, 23 Sep 2023 19:30:42 +0530 Subject: [PATCH 63/72] fix: do not run bg job for single doc (cherry picked from commit 1856050ef9022d5688bcd98ea4edb146946b8b7a) --- .../repost_accounting_ledger.py | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/erpnext/accounts/doctype/repost_accounting_ledger/repost_accounting_ledger.py b/erpnext/accounts/doctype/repost_accounting_ledger/repost_accounting_ledger.py index 4cf2ed2f46c..e533fed35b4 100644 --- a/erpnext/accounts/doctype/repost_accounting_ledger/repost_accounting_ledger.py +++ b/erpnext/accounts/doctype/repost_accounting_ledger/repost_accounting_ledger.py @@ -139,14 +139,17 @@ class RepostAccountingLedger(Document): return rendered_page def on_submit(self): - job_name = "repost_accounting_ledger_" + self.name - frappe.enqueue( - method="erpnext.accounts.doctype.repost_accounting_ledger.repost_accounting_ledger.start_repost", - account_repost_doc=self.name, - is_async=True, - job_name=job_name, - ) - frappe.msgprint(_("Repost has started in the background")) + if len(self.vouchers) > 1: + job_name = "repost_accounting_ledger_" + self.name + frappe.enqueue( + method="erpnext.accounts.doctype.repost_accounting_ledger.repost_accounting_ledger.start_repost", + account_repost_doc=self.name, + is_async=True, + job_name=job_name, + ) + frappe.msgprint(_("Repost has started in the background")) + else: + start_repost(self.name) @frappe.whitelist() From bec3e8ed96f24ea6b048f88d23d3dbc6bc457e0f Mon Sep 17 00:00:00 2001 From: Gursheen Anand Date: Sat, 23 Sep 2023 20:29:38 +0530 Subject: [PATCH 64/72] fix: call validate before setting repost flag (cherry picked from commit 8ef0d8870830b1a4f848a91d201fb740e7f47aa0) --- .../purchase_invoice/purchase_invoice.py | 11 ++++- .../repost_accounting_ledger.py | 46 ++++++++++--------- 2 files changed, 33 insertions(+), 24 deletions(-) diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py index c17062c943a..f6ec446ef35 100644 --- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py +++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py @@ -11,6 +11,9 @@ from frappe.utils import cint, cstr, flt, formatdate, get_link_to_form, getdate, import erpnext from erpnext.accounts.deferred_revenue import validate_service_stop_date from erpnext.accounts.doctype.gl_entry.gl_entry import update_outstanding_amt +from erpnext.accounts.doctype.repost_accounting_ledger.repost_accounting_ledger import ( + validate_docs_for_deferred_accounting, +) from erpnext.accounts.doctype.sales_invoice.sales_invoice import ( check_if_return_invoice_linked_with_payment_entry, get_total_in_party_account_currency, @@ -487,6 +490,11 @@ class PurchaseInvoice(BuyingController): _("Stock cannot be updated against Purchase Receipt {0}").format(item.purchase_receipt) ) + def validate_for_repost(self): + self.validate_write_off_account() + self.validate_expense_account() + validate_docs_for_deferred_accounting([], [self.name]) + def on_submit(self): super(PurchaseInvoice, self).on_submit() @@ -538,8 +546,7 @@ class PurchaseInvoice(BuyingController): ] child_tables = {"items": ("expense_account",), "taxes": ("account_head",)} self.needs_repost = self.check_if_fields_updated(fields_to_check, child_tables) - self.validate_write_off_account() - self.validate_expense_account() + self.validate_for_repost() self.db_set("repost_required", self.needs_repost) def make_gl_entries(self, gl_entries=None, from_repost=False): diff --git a/erpnext/accounts/doctype/repost_accounting_ledger/repost_accounting_ledger.py b/erpnext/accounts/doctype/repost_accounting_ledger/repost_accounting_ledger.py index e533fed35b4..dbb0971fdea 100644 --- a/erpnext/accounts/doctype/repost_accounting_ledger/repost_accounting_ledger.py +++ b/erpnext/accounts/doctype/repost_accounting_ledger/repost_accounting_ledger.py @@ -21,29 +21,8 @@ class RepostAccountingLedger(Document): def validate_for_deferred_accounting(self): sales_docs = [x.voucher_no for x in self.vouchers if x.voucher_type == "Sales Invoice"] - docs_with_deferred_revenue = frappe.db.get_all( - "Sales Invoice Item", - filters={"parent": ["in", sales_docs], "docstatus": 1, "enable_deferred_revenue": True}, - fields=["parent"], - as_list=1, - ) - purchase_docs = [x.voucher_no for x in self.vouchers if x.voucher_type == "Purchase Invoice"] - docs_with_deferred_expense = frappe.db.get_all( - "Purchase Invoice Item", - filters={"parent": ["in", purchase_docs], "docstatus": 1, "enable_deferred_expense": 1}, - fields=["parent"], - as_list=1, - ) - - if docs_with_deferred_revenue or docs_with_deferred_expense: - frappe.throw( - _("Documents: {0} have deferred revenue/expense enabled for them. Cannot repost.").format( - frappe.bold( - comma_and([x[0] for x in docs_with_deferred_expense + docs_with_deferred_revenue]) - ) - ) - ) + validate_docs_for_deferred_accounting(sales_docs, purchase_docs) def validate_for_closed_fiscal_year(self): if self.vouchers: @@ -184,3 +163,26 @@ def start_repost(account_repost_doc=str) -> None: doc.make_gl_entries() frappe.db.commit() + + +def validate_docs_for_deferred_accounting(sales_docs, purchase_docs): + docs_with_deferred_revenue = frappe.db.get_all( + "Sales Invoice Item", + filters={"parent": ["in", sales_docs], "docstatus": 1, "enable_deferred_revenue": True}, + fields=["parent"], + as_list=1, + ) + + docs_with_deferred_expense = frappe.db.get_all( + "Purchase Invoice Item", + filters={"parent": ["in", purchase_docs], "docstatus": 1, "enable_deferred_expense": 1}, + fields=["parent"], + as_list=1, + ) + + if docs_with_deferred_revenue or docs_with_deferred_expense: + frappe.throw( + _("Documents: {0} have deferred revenue/expense enabled for them. Cannot repost.").format( + frappe.bold(comma_and([x[0] for x in docs_with_deferred_expense + docs_with_deferred_revenue])) + ) + ) From 3dc68e3b002ec0f072809d9d94a175b16ea98e8d Mon Sep 17 00:00:00 2001 From: Gursheen Anand Date: Sat, 23 Sep 2023 20:30:50 +0530 Subject: [PATCH 65/72] fix: validation for si (cherry picked from commit 61c6ebbb95b0e7ecbb86312646fe862b9a471d95) --- .../accounts/doctype/sales_invoice/sales_invoice.py | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py index 6b8baa8b2c9..c26d3c0283b 100644 --- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py +++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py @@ -15,6 +15,9 @@ from erpnext.accounts.doctype.loyalty_program.loyalty_program import ( get_loyalty_program_details_with_points, validate_loyalty_points, ) +from erpnext.accounts.doctype.repost_accounting_ledger.repost_accounting_ledger import ( + validate_docs_for_deferred_accounting, +) from erpnext.accounts.doctype.tax_withholding_category.tax_withholding_category import ( get_party_tax_withholding_details, ) @@ -173,6 +176,12 @@ class SalesInvoice(SellingController): self.validate_account_for_change_amount() self.validate_income_account() + def validate_for_repost(self): + self.validate_write_off_account() + self.validate_account_for_change_amount() + self.validate_income_account() + validate_docs_for_deferred_accounting([self.name], []) + 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: @@ -539,7 +548,7 @@ class SalesInvoice(SellingController): "taxes": ("account_head",), } self.needs_repost = self.check_if_fields_updated(fields_to_check, child_tables) - self.validate_accounts() + self.validate_for_repost() self.db_set("repost_required", self.needs_repost) <<<<<<< HEAD From debfbc4761085ade3e2c826e18fef9cecd4dfd5e Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Tue, 26 Sep 2023 14:23:21 +0530 Subject: [PATCH 66/72] refactor: remove references in repost doctypes upon parent doc delet (cherry picked from commit ed7f67b1a85a1c2ef47bba77267e20cf6bbced71) --- .../repost_accounting_ledger.json | 5 ++- .../repost_payment_ledger.json | 5 ++- erpnext/controllers/accounts_controller.py | 37 ++++++++++++++++--- 3 files changed, 37 insertions(+), 10 deletions(-) diff --git a/erpnext/accounts/doctype/repost_accounting_ledger/repost_accounting_ledger.json b/erpnext/accounts/doctype/repost_accounting_ledger/repost_accounting_ledger.json index 8d56c9bb11d..5b7cd2b0b20 100644 --- a/erpnext/accounts/doctype/repost_accounting_ledger/repost_accounting_ledger.json +++ b/erpnext/accounts/doctype/repost_accounting_ledger/repost_accounting_ledger.json @@ -55,7 +55,7 @@ "index_web_pages_for_search": 1, "is_submittable": 1, "links": [], - "modified": "2023-07-27 15:47:58.975034", + "modified": "2023-09-26 14:21:27.362567", "modified_by": "Administrator", "module": "Accounts", "name": "Repost Accounting Ledger", @@ -77,5 +77,6 @@ ], "sort_field": "modified", "sort_order": "DESC", - "states": [] + "states": [], + "track_changes": 1 } \ No newline at end of file diff --git a/erpnext/accounts/doctype/repost_payment_ledger/repost_payment_ledger.json b/erpnext/accounts/doctype/repost_payment_ledger/repost_payment_ledger.json index 5175fd169ff..ed8d395a0ec 100644 --- a/erpnext/accounts/doctype/repost_payment_ledger/repost_payment_ledger.json +++ b/erpnext/accounts/doctype/repost_payment_ledger/repost_payment_ledger.json @@ -99,7 +99,7 @@ "index_web_pages_for_search": 1, "is_submittable": 1, "links": [], - "modified": "2022-11-08 07:38:40.079038", + "modified": "2023-09-26 14:21:35.719727", "modified_by": "Administrator", "module": "Accounts", "name": "Repost Payment Ledger", @@ -155,5 +155,6 @@ ], "sort_field": "modified", "sort_order": "DESC", - "states": [] + "states": [], + "track_changes": 1 } \ No newline at end of file diff --git a/erpnext/controllers/accounts_controller.py b/erpnext/controllers/accounts_controller.py index 072537dcbb4..7207743e095 100644 --- a/erpnext/controllers/accounts_controller.py +++ b/erpnext/controllers/accounts_controller.py @@ -242,13 +242,38 @@ class AccountsController(TransactionBase): _doc.cancel() _doc.delete() - def on_trash(self): - # delete references in 'Repost Payment Ledger' - rpi = frappe.qb.DocType("Repost Payment Ledger Items") - frappe.qb.from_(rpi).delete().where( - (rpi.voucher_type == self.doctype) & (rpi.voucher_no == self.name) - ).run() + def _remove_references_in_repost_doctypes(self): + repost_doctypes = ["Repost Payment Ledger Items", "Repost Accounting Ledger Items"] + for _doctype in repost_doctypes: + dt = frappe.qb.DocType(_doctype) + rows = ( + frappe.qb.from_(dt) + .select(dt.name, dt.parent, dt.parenttype) + .where((dt.voucher_type == self.doctype) & (dt.voucher_no == self.name)) + .run(as_dict=True) + ) + + if rows: + references_map = frappe._dict() + for x in rows: + references_map.setdefault((x.parenttype, x.parent), []).append(x.name) + + for doc, rows in references_map.items(): + repost_doc = frappe.get_doc(doc[0], doc[1]) + + for row in rows: + if _doctype == "Repost Payment Ledger Items": + repost_doc.remove(repost_doc.get("repost_vouchers", {"name": row})[0]) + else: + repost_doc.remove(repost_doc.get("vouchers", {"name": row})[0]) + + repost_doc.flags.ignore_validate_update_after_submit = True + repost_doc.flags.ignore_links = True + repost_doc.save(ignore_permissions=True) + + def on_trash(self): + self._remove_references_in_repost_doctypes() self._remove_references_in_unreconcile() # delete sl and gl entries on deletion of transaction From 0aad942312da44584f30b44249baeeed15145926 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Tue, 10 Oct 2023 11:06:33 +0530 Subject: [PATCH 67/72] fix: fetch dependent task subject and project (backport #37401) (#37421) fix: fetch dependent task subject and project (#37401) (cherry picked from commit 78eaf5d0356e00f4a07b84a3dc5ee7916871efaf) Co-authored-by: s-aga-r --- .../task_depends_on/task_depends_on.json | 192 ++++-------------- 1 file changed, 44 insertions(+), 148 deletions(-) diff --git a/erpnext/projects/doctype/task_depends_on/task_depends_on.json b/erpnext/projects/doctype/task_depends_on/task_depends_on.json index dbbe9d3c7b5..5102986f00d 100644 --- a/erpnext/projects/doctype/task_depends_on/task_depends_on.json +++ b/erpnext/projects/doctype/task_depends_on/task_depends_on.json @@ -1,156 +1,52 @@ { - "allow_copy": 0, - "allow_import": 0, - "allow_rename": 0, - "beta": 0, - "creation": "2015-04-29 04:52:48.868079", - "custom": 0, - "docstatus": 0, - "doctype": "DocType", - "document_type": "", - "editable_grid": 1, + "actions": [], + "creation": "2015-04-29 04:52:48.868079", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "task", + "column_break_2", + "subject", + "project" + ], "fields": [ { - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "task", - "fieldtype": "Link", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 1, - "in_standard_filter": 0, - "label": "Task", - "length": 0, - "no_copy": 0, - "options": "Task", - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "unique": 0 - }, + "fieldname": "task", + "fieldtype": "Link", + "in_list_view": 1, + "label": "Task", + "options": "Task" + }, { - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "column_break_2", - "fieldtype": "Column Break", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "unique": 0 - }, + "fieldname": "column_break_2", + "fieldtype": "Column Break" + }, { - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "subject", - "fieldtype": "Text", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 1, - "in_standard_filter": 0, - "label": "Subject", - "length": 0, - "no_copy": 0, - "options": "", - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 1, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "unique": 0 - }, + "fetch_from": "task.subject", + "fieldname": "subject", + "fieldtype": "Text", + "in_list_view": 1, + "label": "Subject", + "read_only": 1 + }, { - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "project", - "fieldtype": "Text", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Project", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 1, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "unique": 0 + "fetch_from": "task.project", + "fieldname": "project", + "fieldtype": "Text", + "label": "Project", + "read_only": 1 } - ], - "hide_heading": 0, - "hide_toolbar": 0, - "idx": 0, - "image_view": 0, - "in_create": 0, - - "is_submittable": 0, - "issingle": 0, - "istable": 1, - "max_attachments": 0, - "modified": "2017-02-24 04:56:04.862502", - "modified_by": "Administrator", - "module": "Projects", - "name": "Task Depends On", - "name_case": "", - "owner": "Administrator", - "permissions": [], - "quick_entry": 0, - "read_only": 0, - "read_only_onload": 0, - "show_name_in_global_search": 0, - "sort_field": "modified", - "sort_order": "DESC", - "track_changes": 0, - "track_seen": 0 + ], + "istable": 1, + "links": [], + "modified": "2023-10-09 11:34:14.335853", + "modified_by": "Administrator", + "module": "Projects", + "name": "Task Depends On", + "owner": "Administrator", + "permissions": [], + "sort_field": "modified", + "sort_order": "DESC", + "states": [] } \ No newline at end of file From b0ac0973275a58628d0236071ec921eac4781c9e Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Tue, 10 Oct 2023 11:16:12 +0530 Subject: [PATCH 68/72] chore: resolve conflicts --- .../purchase_invoice/purchase_invoice.json | 4 -- .../purchase_invoice/test_purchase_invoice.py | 59 ------------------- .../doctype/sales_invoice/sales_invoice.py | 40 ------------- 3 files changed, 103 deletions(-) diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.json b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.json index 864f0b81890..1f3b17ee147 100644 --- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.json +++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.json @@ -1584,11 +1584,7 @@ "idx": 204, "is_submittable": 1, "links": [], -<<<<<<< HEAD "modified": "2023-10-01 21:01:47.282533", -======= - "modified": "2023-09-21 12:22:04.545106", ->>>>>>> e922ec60eb (feat: allow on submit fields) "modified_by": "Administrator", "module": "Accounts", "name": "Purchase Invoice", diff --git a/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py b/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py index 51208885044..47126d3846f 100644 --- a/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py +++ b/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py @@ -1710,65 +1710,6 @@ class TestPurchaseInvoice(unittest.TestCase, StockTestMixin): self.assertTrue(return_pi.docstatus == 1) -<<<<<<< HEAD -======= - def test_advance_entries_as_asset(self): - from erpnext.accounts.doctype.payment_entry.test_payment_entry import create_payment_entry - - account = create_account( - parent_account="Current Assets - _TC", - account_name="Advances Paid", - company="_Test Company", - account_type="Receivable", - ) - - set_advance_flag(company="_Test Company", flag=1, default_account=account) - - pe = create_payment_entry( - company="_Test Company", - payment_type="Pay", - party_type="Supplier", - party="_Test Supplier", - paid_from="Cash - _TC", - paid_to="Creditors - _TC", - paid_amount=500, - ) - pe.submit() - - pi = make_purchase_invoice( - company="_Test Company", - do_not_save=True, - do_not_submit=True, - rate=1000, - price_list_rate=1000, - qty=1, - ) - pi.base_grand_total = 1000 - pi.grand_total = 1000 - pi.set_advances() - for advance in pi.advances: - advance.allocated_amount = 500 if advance.reference_name == pe.name else 0 - pi.save() - pi.submit() - - self.assertEqual(pi.advances[0].allocated_amount, 500) - - # Check GL Entry against payment doctype - expected_gle = [ - ["Advances Paid - _TC", 0.0, 500, nowdate()], - ["Cash - _TC", 0.0, 500, nowdate()], - ["Creditors - _TC", 500, 0.0, nowdate()], - ["Creditors - _TC", 500, 0.0, nowdate()], - ] - - check_gl_entries(self, pe.name, expected_gle, nowdate(), voucher_type="Payment Entry") - - pi.load_from_db() - self.assertEqual(pi.outstanding_amount, 500) - - set_advance_flag(company="_Test Company", flag=0, default_account="") - ->>>>>>> c66c438575 (test: reposted acc entries for pi) def test_gl_entries_for_standalone_debit_note(self): make_purchase_invoice(qty=5, rate=500, update_stock=True) diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py index c26d3c0283b..03aca8ad588 100644 --- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py +++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py @@ -551,46 +551,6 @@ class SalesInvoice(SellingController): self.validate_for_repost() self.db_set("repost_required", self.needs_repost) -<<<<<<< HEAD -<<<<<<< HEAD - # validate if deferred revenue is enabled for any item - # Don't allow to update the invoice if deferred revenue is enabled -======= - def validate_deferred_accounting_before_repost(self): - # validate if deferred revenue is enabled for any item - # Don't allow to update the invoice if deferred revenue is enabled - if self.needs_repost: ->>>>>>> 68effd93bd (refactor: move reposting logic to common controller) - 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) - ) -<<<<<<< HEAD - - 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 -======= ->>>>>>> 68effd93bd (refactor: move reposting logic to common controller) - -======= ->>>>>>> a856091ff4 (refactor: remove repeated validation for voucher) def set_paid_amount(self): paid_amount = 0.0 base_paid_amount = 0.0 From ee1255a716451015da11a8afe62b4c0af2193e0c Mon Sep 17 00:00:00 2001 From: Gursheen Kaur Anand <40693548+GursheenK@users.noreply.github.com> Date: Mon, 31 Jul 2023 14:29:20 +0530 Subject: [PATCH 69/72] fix: ignore cancelled gle in voucher-wise balance report (#36417) fix: ignore cancelled gle (cherry picked from commit 1ddfaa7605f710b2fd12ccdfa38824cd086f576d) --- .../accounts/report/voucher_wise_balance/voucher_wise_balance.py | 1 + 1 file changed, 1 insertion(+) diff --git a/erpnext/accounts/report/voucher_wise_balance/voucher_wise_balance.py b/erpnext/accounts/report/voucher_wise_balance/voucher_wise_balance.py index 5ab3611b9af..bd9e9fccadc 100644 --- a/erpnext/accounts/report/voucher_wise_balance/voucher_wise_balance.py +++ b/erpnext/accounts/report/voucher_wise_balance/voucher_wise_balance.py @@ -46,6 +46,7 @@ def get_data(filters): .select( gle.voucher_type, gle.voucher_no, Sum(gle.debit).as_("debit"), Sum(gle.credit).as_("credit") ) + .where(gle.is_cancelled == 0) .groupby(gle.voucher_no) ) query = apply_filters(query, filters, gle) From fb0b426fe435357ad49513ae15e2b12a845864e0 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Wed, 11 Oct 2023 13:51:29 +0530 Subject: [PATCH 70/72] fix(ux): allow MR to Stop until fully received (backport #37452) (#37456) fix(ux): allow MR to Stop until fully received (cherry picked from commit 0d7a0f393deed1f3bec5d5925bd5f9bb4eab99c5) Co-authored-by: s-aga-r --- .../doctype/material_request/material_request.js | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/erpnext/stock/doctype/material_request/material_request.js b/erpnext/stock/doctype/material_request/material_request.js index 2632501b718..ec075bb6bad 100644 --- a/erpnext/stock/doctype/material_request/material_request.js +++ b/erpnext/stock/doctype/material_request/material_request.js @@ -102,6 +102,12 @@ frappe.ui.form.on('Material Request', { if (frm.doc.docstatus == 1 && frm.doc.status != 'Stopped') { let precision = frappe.defaults.get_default("float_precision"); + + if (flt(frm.doc.per_received, precision) < 100) { + frm.add_custom_button(__('Stop'), + () => frm.events.update_status(frm, 'Stopped')); + } + if (flt(frm.doc.per_ordered, precision) < 100) { let add_create_pick_list_button = () => { frm.add_custom_button(__('Pick List'), @@ -148,11 +154,6 @@ frappe.ui.form.on('Material Request', { } frm.page.set_inner_btn_group_as_primary(__('Create')); - - // stop - frm.add_custom_button(__('Stop'), - () => frm.events.update_status(frm, 'Stopped')); - } } From 573b159541b6aebbdcc86202576c38c69c947b1e Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Wed, 11 Oct 2023 14:41:32 +0530 Subject: [PATCH 71/72] fix: production plan reserved qty incorrect calculation (backport #37400) (#37458) fix: production plan reserved qty incorrect calculation (#37400) (cherry picked from commit f3238f910509813a24fbc7ebfb726f42f6addd6f) Co-authored-by: rohitwaghchaure --- .../production_plan/production_plan.py | 23 ++++++------------- .../production_plan/test_production_plan.py | 6 ++--- .../doctype/work_order/work_order.py | 19 ++++++++------- 3 files changed, 21 insertions(+), 27 deletions(-) diff --git a/erpnext/manufacturing/doctype/production_plan/production_plan.py b/erpnext/manufacturing/doctype/production_plan/production_plan.py index f1817e3305b..5fc764fb6f7 100644 --- a/erpnext/manufacturing/doctype/production_plan/production_plan.py +++ b/erpnext/manufacturing/doctype/production_plan/production_plan.py @@ -8,7 +8,6 @@ import json import frappe from frappe import _, msgprint from frappe.model.document import Document -from frappe.query_builder import Case from frappe.query_builder.functions import IfNull, Sum from frappe.utils import ( add_days, @@ -1617,21 +1616,13 @@ def get_reserved_qty_for_production_plan(item_code, warehouse): table = frappe.qb.DocType("Production Plan") child = frappe.qb.DocType("Material Request Plan Item") - completed_production_plans = get_completed_production_plans() + non_completed_production_plans = get_non_completed_production_plans() - case = Case() query = ( frappe.qb.from_(table) .inner_join(child) .on(table.name == child.parent) - .select( - Sum( - child.quantity - * IfNull( - case.when(child.material_request_type == "Purchase", child.conversion_factor).else_(1.0), 1.0 - ) - ) - ) + .select(Sum(child.required_bom_qty)) .where( (table.docstatus == 1) & (child.item_code == item_code) @@ -1640,8 +1631,8 @@ def get_reserved_qty_for_production_plan(item_code, warehouse): ) ) - if completed_production_plans: - query = query.where(table.name.notin(completed_production_plans)) + if non_completed_production_plans: + query = query.where(table.name.isin(non_completed_production_plans)) query = query.run() @@ -1652,7 +1643,7 @@ def get_reserved_qty_for_production_plan(item_code, warehouse): reserved_qty_for_production = flt( get_reserved_qty_for_production( - item_code, warehouse, completed_production_plans, check_production_plan=True + item_code, warehouse, non_completed_production_plans, check_production_plan=True ) ) @@ -1662,7 +1653,7 @@ def get_reserved_qty_for_production_plan(item_code, warehouse): return reserved_qty_for_production_plan - reserved_qty_for_production -def get_completed_production_plans(): +def get_non_completed_production_plans(): table = frappe.qb.DocType("Production Plan") child = frappe.qb.DocType("Production Plan Item") @@ -1674,7 +1665,7 @@ def get_completed_production_plans(): .where( (table.docstatus == 1) & (table.status.notin(["Completed", "Closed"])) - & (child.ordered_qty >= child.planned_qty) + & (child.planned_qty > child.ordered_qty) ) ).run(as_dict=True) diff --git a/erpnext/manufacturing/doctype/production_plan/test_production_plan.py b/erpnext/manufacturing/doctype/production_plan/test_production_plan.py index 55122f7069c..dbd3083ab58 100644 --- a/erpnext/manufacturing/doctype/production_plan/test_production_plan.py +++ b/erpnext/manufacturing/doctype/production_plan/test_production_plan.py @@ -6,8 +6,8 @@ from frappe.utils import add_to_date, flt, getdate, now_datetime, nowdate from erpnext.controllers.item_variant import create_variant from erpnext.manufacturing.doctype.production_plan.production_plan import ( - get_completed_production_plans, get_items_for_material_requests, + get_non_completed_production_plans, get_sales_orders, get_warehouse_list, ) @@ -1132,9 +1132,9 @@ class TestProductionPlan(FrappeTestCase): self.assertEqual(after_qty, before_qty) - completed_plans = get_completed_production_plans() + completed_plans = get_non_completed_production_plans() for plan in plans: - self.assertTrue(plan in completed_plans) + self.assertFalse(plan in completed_plans) def test_resered_qty_for_production_plan_for_material_requests_with_multi_UOM(self): from erpnext.stock.utils import get_or_make_bin diff --git a/erpnext/manufacturing/doctype/work_order/work_order.py b/erpnext/manufacturing/doctype/work_order/work_order.py index a491501d4b7..93d015dc93b 100644 --- a/erpnext/manufacturing/doctype/work_order/work_order.py +++ b/erpnext/manufacturing/doctype/work_order/work_order.py @@ -1493,7 +1493,7 @@ def create_pick_list(source_name, target_doc=None, for_qty=None): def get_reserved_qty_for_production( item_code: str, warehouse: str, - completed_production_plans: list = None, + non_completed_production_plans: list = None, check_production_plan: bool = False, ) -> float: """Get total reserved quantity for any item in specified warehouse""" @@ -1516,19 +1516,22 @@ def get_reserved_qty_for_production( & (wo_item.parent == wo.name) & (wo.docstatus == 1) & (wo_item.source_warehouse == warehouse) - & (wo.status.notin(["Stopped", "Completed", "Closed"])) - & ( - (wo_item.required_qty > wo_item.transferred_qty) - | (wo_item.required_qty > wo_item.consumed_qty) - ) ) ) if check_production_plan: query = query.where(wo.production_plan.isnotnull()) + else: + query = query.where( + (wo.status.notin(["Stopped", "Completed", "Closed"])) + & ( + (wo_item.required_qty > wo_item.transferred_qty) + | (wo_item.required_qty > wo_item.consumed_qty) + ) + ) - if completed_production_plans: - query = query.where(wo.production_plan.notin(completed_production_plans)) + if non_completed_production_plans: + query = query.where(wo.production_plan.isin(non_completed_production_plans)) return query.run()[0][0] or 0.0 From 26ad6885845962130cfb178fcbdbe2c4e75ce194 Mon Sep 17 00:00:00 2001 From: s-aga-r Date: Wed, 11 Oct 2023 18:44:32 +0530 Subject: [PATCH 72/72] fix: negative valuation rate in PR return (#37424) * fix: negative valuation rate in PR return * test: add test case for PR return --- erpnext/controllers/buying_controller.py | 20 +++++-- .../purchase_receipt/test_purchase_receipt.py | 54 +++++++++++++++++++ erpnext/stock/stock_ledger.py | 18 ++++--- 3 files changed, 80 insertions(+), 12 deletions(-) diff --git a/erpnext/controllers/buying_controller.py b/erpnext/controllers/buying_controller.py index 01990a3a268..a38905c7e2b 100644 --- a/erpnext/controllers/buying_controller.py +++ b/erpnext/controllers/buying_controller.py @@ -14,7 +14,8 @@ from erpnext.buying.utils import update_last_purchase_rate, validate_for_items from erpnext.controllers.sales_and_purchase_return import get_rate_for_return from erpnext.controllers.subcontracting_controller import SubcontractingController from erpnext.stock.get_item_details import get_conversion_factor -from erpnext.stock.utils import get_incoming_rate +from erpnext.stock.stock_ledger import get_previous_sle +from erpnext.stock.utils import get_incoming_rate, get_valuation_method class QtyMismatchError(ValidationError): @@ -514,9 +515,20 @@ class BuyingController(SubcontractingController): ) if self.is_return: - outgoing_rate = get_rate_for_return( - self.doctype, self.name, d.item_code, self.return_against, item_row=d - ) + if get_valuation_method(d.item_code) == "Moving Average": + previous_sle = get_previous_sle( + { + "item_code": d.item_code, + "warehouse": d.warehouse, + "posting_date": self.posting_date, + "posting_time": self.posting_time, + } + ) + outgoing_rate = flt(previous_sle.get("valuation_rate")) + else: + outgoing_rate = get_rate_for_return( + self.doctype, self.name, d.item_code, self.return_against, item_row=d + ) sle.update({"outgoing_rate": outgoing_rate, "recalculate_rate": 1}) if d.from_warehouse: diff --git a/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py b/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py index 82694a0b192..463353e2549 100644 --- a/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py +++ b/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py @@ -2093,6 +2093,60 @@ class TestPurchaseReceipt(FrappeTestCase): return_pr.reload() self.assertEqual(return_pr.status, "Completed") + def test_valuation_rate_in_return_purchase_receipt_for_moving_average(self): + from erpnext.stock.doctype.stock_entry.test_stock_entry import make_stock_entry + from erpnext.stock.stock_ledger import get_previous_sle + + # Step - 1: Create an Item (Valuation Method = Moving Average) + item_code = make_item(properties={"is_stock_item": 1, "valuation_method": "Moving Average"}).name + + # Step - 2: Create a Purchase Receipt (Qty = 10, Rate = 100) + pr = make_purchase_receipt(qty=10, rate=100, item_code=item_code) + + # Step - 3: Create a Material Receipt Stock Entry (Qty = 100, Basic Rate = 10) + warehouse = "_Test Warehouse - _TC" + make_stock_entry( + purpose="Material Receipt", + item_code=item_code, + to_warehouse=warehouse, + qty=100, + rate=10, + ) + + # Step - 4: Create a Material Issue Stock Entry (Qty = 100, Basic Rate = 18.18 [Auto Fetched]) + make_stock_entry( + purpose="Material Issue", item_code=item_code, from_warehouse=warehouse, qty=100 + ) + + # Step - 5: Create a Return Purchase Return (Qty = -8, Rate = 100 [Auto fetched]) + return_pr = make_purchase_receipt( + is_return=1, + return_against=pr.name, + item_code=item_code, + qty=-8, + ) + + sle = frappe.db.get_value( + "Stock Ledger Entry", + {"voucher_no": return_pr.name, "voucher_detail_no": return_pr.items[0].name}, + ["posting_date", "posting_time", "outgoing_rate", "valuation_rate"], + as_dict=1, + ) + previous_sle_valuation_rate = get_previous_sle( + { + "item_code": item_code, + "warehouse": warehouse, + "posting_date": sle.posting_date, + "posting_time": sle.posting_time, + } + ).get("valuation_rate") + + # Test - 1: Valuation Rate should be equal to Outgoing Rate + self.assertEqual(flt(sle.outgoing_rate, 2), flt(sle.valuation_rate, 2)) + + # Test - 2: Valuation Rate should be equal to Previous SLE Valuation Rate + self.assertEqual(flt(sle.valuation_rate, 2), flt(previous_sle_valuation_rate, 2)) + def prepare_data_for_internal_transfer(): from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_internal_supplier diff --git a/erpnext/stock/stock_ledger.py b/erpnext/stock/stock_ledger.py index 108d36a490d..13bbe1f5c08 100644 --- a/erpnext/stock/stock_ledger.py +++ b/erpnext/stock/stock_ledger.py @@ -698,14 +698,16 @@ class update_entries_after(object): get_rate_for_return, # don't move this import to top ) - rate = get_rate_for_return( - sle.voucher_type, - sle.voucher_no, - sle.item_code, - voucher_detail_no=sle.voucher_detail_no, - sle=sle, - ) - + if self.valuation_method == "Moving Average": + rate = self.data[self.args.warehouse].previous_sle.valuation_rate + else: + rate = get_rate_for_return( + sle.voucher_type, + sle.voucher_no, + sle.item_code, + voucher_detail_no=sle.voucher_detail_no, + sle=sle, + ) elif ( sle.voucher_type in ["Purchase Receipt", "Purchase Invoice"] and sle.voucher_detail_no