From e0cb5f9ba8dc8a5b23af958b5c988b3499ed4853 Mon Sep 17 00:00:00 2001 From: Raffael Meyer <14891507+barredterra@users.noreply.github.com> Date: Tue, 26 Nov 2024 21:10:34 +0100 Subject: [PATCH 01/44] feat(Dunning): separate tab "Address & Contact" (#44363) (cherry picked from commit e094473c65b63942dae8a422107452fa7e72d1ed) # Conflicts: # erpnext/accounts/doctype/dunning/dunning.json --- erpnext/accounts/doctype/dunning/dunning.json | 51 ++++++++++++++----- 1 file changed, 37 insertions(+), 14 deletions(-) diff --git a/erpnext/accounts/doctype/dunning/dunning.json b/erpnext/accounts/doctype/dunning/dunning.json index b7e8aeaaafd6..85ce6d555d30 100644 --- a/erpnext/accounts/doctype/dunning/dunning.json +++ b/erpnext/accounts/doctype/dunning/dunning.json @@ -19,16 +19,6 @@ "currency", "column_break_11", "conversion_rate", - "address_and_contact_section", - "customer_address", - "address_display", - "contact_person", - "contact_display", - "column_break_16", - "company_address", - "company_address_display", - "contact_mobile", - "contact_email", "section_break_6", "dunning_type", "column_break_8", @@ -56,7 +46,21 @@ "income_account", "column_break_48", "cost_center", - "amended_from" + "amended_from", + "address_and_contact_tab", + "address_and_contact_section", + "customer_address", + "address_display", + "column_break_vodj", + "contact_person", + "contact_display", + "contact_mobile", + "contact_email", + "section_break_xban", + "column_break_16", + "company_address", + "company_address_display", + "column_break_lqmf" ], "fields": [ { @@ -178,10 +182,8 @@ "label": "Rate of Interest (%) Yearly" }, { - "collapsible": 1, "fieldname": "address_and_contact_section", - "fieldtype": "Section Break", - "label": "Address and Contact" + "fieldtype": "Section Break" }, { "fieldname": "address_display", @@ -377,11 +379,32 @@ { "fieldname": "column_break_48", "fieldtype": "Column Break" + }, + { + "fieldname": "address_and_contact_tab", + "fieldtype": "Tab Break", + "label": "Address & Contact" + }, + { + "fieldname": "column_break_vodj", + "fieldtype": "Column Break" + }, + { + "fieldname": "section_break_xban", + "fieldtype": "Section Break" + }, + { + "fieldname": "column_break_lqmf", + "fieldtype": "Column Break" } ], "is_submittable": 1, "links": [], +<<<<<<< HEAD "modified": "2023-06-15 15:46:53.865712", +======= + "modified": "2024-11-26 13:46:07.760867", +>>>>>>> e094473c65 (feat(Dunning): separate tab "Address & Contact" (#44363)) "modified_by": "Administrator", "module": "Accounts", "name": "Dunning", From c20def5d59eb0213c492d85e356b88f94fc91672 Mon Sep 17 00:00:00 2001 From: Raffael Meyer <14891507+barredterra@users.noreply.github.com> Date: Tue, 26 Nov 2024 21:12:39 +0100 Subject: [PATCH 02/44] chore: resolve conflicts --- erpnext/accounts/doctype/dunning/dunning.json | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/erpnext/accounts/doctype/dunning/dunning.json b/erpnext/accounts/doctype/dunning/dunning.json index 85ce6d555d30..496097417bad 100644 --- a/erpnext/accounts/doctype/dunning/dunning.json +++ b/erpnext/accounts/doctype/dunning/dunning.json @@ -400,11 +400,7 @@ ], "is_submittable": 1, "links": [], -<<<<<<< HEAD - "modified": "2023-06-15 15:46:53.865712", -======= "modified": "2024-11-26 13:46:07.760867", ->>>>>>> e094473c65 (feat(Dunning): separate tab "Address & Contact" (#44363)) "modified_by": "Administrator", "module": "Accounts", "name": "Dunning", @@ -458,4 +454,4 @@ "states": [], "title_field": "customer_name", "track_changes": 1 -} \ No newline at end of file +} From 0e1f5ff391b7a6f277149e5a14203f4cde7fac6c Mon Sep 17 00:00:00 2001 From: Abdeali Chharchhoda Date: Wed, 27 Nov 2024 16:53:50 +0530 Subject: [PATCH 03/44] fix: Add translation for showing mandatory fields in error msg (cherry picked from commit f42ec6a124d272dec0ac9671b7eef13f55d81de2) --- erpnext/selling/doctype/quotation/quotation.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/selling/doctype/quotation/quotation.py b/erpnext/selling/doctype/quotation/quotation.py index 8e560b8d0ab8..2bc6ef8399e4 100644 --- a/erpnext/selling/doctype/quotation/quotation.py +++ b/erpnext/selling/doctype/quotation/quotation.py @@ -568,7 +568,7 @@ def handle_mandatory_error(e, customer, lead_name): from frappe.utils import get_link_to_form mandatory_fields = e.args[0].split(":")[1].split(",") - mandatory_fields = [customer.meta.get_label(field.strip()) for field in mandatory_fields] + mandatory_fields = [_(customer.meta.get_label(field.strip())) for field in mandatory_fields] frappe.local.message_log = [] message = _("Could not auto create Customer due to the following missing mandatory field(s):") + "
" From 70b5b08d588ea4e9132f2ab51846a16d4da04241 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Wed, 27 Nov 2024 18:55:43 +0100 Subject: [PATCH 04/44] feat: add Company Contact Person in selling transactions (backport #44362) (#44398) * feat: add Company Contact Person in selling transactions (#44362) (cherry picked from commit f6776c7d6b37ce2766244dfeaf5939bda1a183ba) * chore: resolve merge conflicts --------- Co-authored-by: Raffael Meyer <14891507+barredterra@users.noreply.github.com> --- .../accounts/doctype/pos_invoice/pos_invoice.json | 10 +++++++++- .../accounts/doctype/pos_invoice/pos_invoice.py | 9 +++------ .../doctype/sales_invoice/sales_invoice.json | 14 +++++++++++--- .../doctype/sales_invoice/sales_invoice.py | 1 + erpnext/public/js/queries.js | 11 +++++++++++ erpnext/public/js/utils/sales_common.js | 1 + erpnext/selling/doctype/quotation/quotation.json | 12 ++++++++++-- erpnext/selling/doctype/quotation/quotation.py | 1 + .../selling/doctype/sales_order/sales_order.json | 12 ++++++++++-- erpnext/selling/doctype/sales_order/sales_order.py | 1 + .../stock/doctype/delivery_note/delivery_note.json | 12 ++++++++++-- .../stock/doctype/delivery_note/delivery_note.py | 1 + 12 files changed, 69 insertions(+), 16 deletions(-) diff --git a/erpnext/accounts/doctype/pos_invoice/pos_invoice.json b/erpnext/accounts/doctype/pos_invoice/pos_invoice.json index d7b173667ecb..428611404941 100644 --- a/erpnext/accounts/doctype/pos_invoice/pos_invoice.json +++ b/erpnext/accounts/doctype/pos_invoice/pos_invoice.json @@ -48,6 +48,7 @@ "shipping_address", "company_address", "company_address_display", + "company_contact_person", "currency_and_price_list", "currency", "conversion_rate", @@ -1558,12 +1559,19 @@ "fieldname": "update_billed_amount_in_delivery_note", "fieldtype": "Check", "label": "Update Billed Amount in Delivery Note" + }, + { + "fieldname": "company_contact_person", + "fieldtype": "Link", + "label": "Company Contact Person", + "options": "Contact", + "print_hide": 1 } ], "icon": "fa fa-file-text", "is_submittable": 1, "links": [], - "modified": "2024-03-20 16:00:34.268756", + "modified": "2024-11-26 13:10:50.309570", "modified_by": "Administrator", "module": "Accounts", "name": "POS Invoice", diff --git a/erpnext/accounts/doctype/pos_invoice/pos_invoice.py b/erpnext/accounts/doctype/pos_invoice/pos_invoice.py index 20316e3394b2..ab5a4092c338 100644 --- a/erpnext/accounts/doctype/pos_invoice/pos_invoice.py +++ b/erpnext/accounts/doctype/pos_invoice/pos_invoice.py @@ -32,12 +32,8 @@ class POSInvoice(SalesInvoice): from erpnext.accounts.doctype.payment_schedule.payment_schedule import PaymentSchedule from erpnext.accounts.doctype.pos_invoice_item.pos_invoice_item import POSInvoiceItem from erpnext.accounts.doctype.pricing_rule_detail.pricing_rule_detail import PricingRuleDetail - from erpnext.accounts.doctype.sales_invoice_advance.sales_invoice_advance import ( - SalesInvoiceAdvance, - ) - from erpnext.accounts.doctype.sales_invoice_payment.sales_invoice_payment import ( - SalesInvoicePayment, - ) + from erpnext.accounts.doctype.sales_invoice_advance.sales_invoice_advance import SalesInvoiceAdvance + from erpnext.accounts.doctype.sales_invoice_payment.sales_invoice_payment import SalesInvoicePayment from erpnext.accounts.doctype.sales_invoice_timesheet.sales_invoice_timesheet import ( SalesInvoiceTimesheet, ) @@ -75,6 +71,7 @@ class POSInvoice(SalesInvoice): company: DF.Link company_address: DF.Link | None company_address_display: DF.SmallText | None + company_contact_person: DF.Link | None consolidated_invoice: DF.Link | None contact_display: DF.SmallText | None contact_email: DF.Data | None diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.json b/erpnext/accounts/doctype/sales_invoice/sales_invoice.json index c44afd555e05..cb861e68cdc7 100644 --- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.json +++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.json @@ -159,8 +159,9 @@ "dispatch_address", "company_address_section", "company_address", - "company_addr_col_break", "company_address_display", + "company_addr_col_break", + "company_contact_person", "terms_tab", "payment_schedule_section", "ignore_default_payment_terms_template", @@ -2166,6 +2167,13 @@ "label": "Update Outstanding for Self", "no_copy": 1, "print_hide": 1 + }, + { + "fieldname": "company_contact_person", + "fieldtype": "Link", + "label": "Company Contact Person", + "options": "Contact", + "print_hide": 1 } ], "icon": "fa fa-file-text", @@ -2178,7 +2186,7 @@ "link_fieldname": "consolidated_invoice" } ], - "modified": "2024-07-18 15:30:39.428519", + "modified": "2024-11-26 12:34:09.110690", "modified_by": "Administrator", "module": "Accounts", "name": "Sales Invoice", @@ -2233,4 +2241,4 @@ "title_field": "title", "track_changes": 1, "track_seen": 1 -} +} \ No newline at end of file diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py index 8baa36475da1..410e934ab711 100644 --- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py +++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py @@ -96,6 +96,7 @@ class SalesInvoice(SellingController): company: DF.Link company_address: DF.Link | None company_address_display: DF.SmallText | None + company_contact_person: DF.Link | None company_tax_id: DF.Data | None contact_display: DF.SmallText | None contact_email: DF.Data | None diff --git a/erpnext/public/js/queries.js b/erpnext/public/js/queries.js index 5350f91746c1..fd5b7603844c 100644 --- a/erpnext/public/js/queries.js +++ b/erpnext/public/js/queries.js @@ -56,6 +56,17 @@ $.extend(erpnext.queries, { } }, + company_contact_query: function (doc) { + if (!doc.company) { + frappe.throw(__("Please set {0}", [__(frappe.meta.get_label(doc.doctype, "company", doc.name))])); + } + + return { + query: "frappe.contacts.doctype.contact.contact.contact_query", + filters: { link_doctype: "Company", link_name: doc.company }, + }; + }, + address_query: function (doc) { if (frappe.dynamic_link) { if (!doc[frappe.dynamic_link.fieldname]) { diff --git a/erpnext/public/js/utils/sales_common.js b/erpnext/public/js/utils/sales_common.js index 373bf3d2115e..ca2bed20c7f1 100644 --- a/erpnext/public/js/utils/sales_common.js +++ b/erpnext/public/js/utils/sales_common.js @@ -49,6 +49,7 @@ erpnext.sales_common = { ); me.frm.set_query("contact_person", erpnext.queries.contact_query); + me.frm.set_query("company_contact_person", erpnext.queries.company_contact_query); me.frm.set_query("customer_address", erpnext.queries.address_query); me.frm.set_query("shipping_address_name", erpnext.queries.address_query); me.frm.set_query("dispatch_address_name", erpnext.queries.dispatch_address_query); diff --git a/erpnext/selling/doctype/quotation/quotation.json b/erpnext/selling/doctype/quotation/quotation.json index d6ee87b5dee7..4d257ff69e7e 100644 --- a/erpnext/selling/doctype/quotation/quotation.json +++ b/erpnext/selling/doctype/quotation/quotation.json @@ -96,8 +96,9 @@ "shipping_address", "company_address_section", "company_address", - "column_break_87", "company_address_display", + "column_break_87", + "company_contact_person", "terms_tab", "payment_schedule_section", "payment_terms_template", @@ -1076,13 +1077,20 @@ "fieldname": "disable_rounded_total", "fieldtype": "Check", "label": "Disable Rounded Total" + }, + { + "fieldname": "company_contact_person", + "fieldtype": "Link", + "label": "Company Contact Person", + "options": "Contact", + "print_hide": 1 } ], "icon": "fa fa-shopping-cart", "idx": 82, "is_submittable": 1, "links": [], - "modified": "2024-11-07 18:37:11.715189", + "modified": "2024-11-26 12:43:29.293637", "modified_by": "Administrator", "module": "Selling", "name": "Quotation", diff --git a/erpnext/selling/doctype/quotation/quotation.py b/erpnext/selling/doctype/quotation/quotation.py index 8e560b8d0ab8..1922d6584ee6 100644 --- a/erpnext/selling/doctype/quotation/quotation.py +++ b/erpnext/selling/doctype/quotation/quotation.py @@ -50,6 +50,7 @@ class Quotation(SellingController): company: DF.Link company_address: DF.Link | None company_address_display: DF.SmallText | None + company_contact_person: DF.Link | None competitors: DF.TableMultiSelect[CompetitorDetail] contact_display: DF.SmallText | None contact_email: DF.Data | None diff --git a/erpnext/selling/doctype/sales_order/sales_order.json b/erpnext/selling/doctype/sales_order/sales_order.json index 0695c3fd9c4a..1525b9632de5 100644 --- a/erpnext/selling/doctype/sales_order/sales_order.json +++ b/erpnext/selling/doctype/sales_order/sales_order.json @@ -113,8 +113,9 @@ "dispatch_address", "col_break46", "company_address", - "column_break_92", "company_address_display", + "column_break_92", + "company_contact_person", "payment_schedule_section", "payment_terms_section", "payment_terms_template", @@ -1640,13 +1641,20 @@ "no_copy": 1, "print_hide": 1, "report_hide": 1 + }, + { + "fieldname": "company_contact_person", + "fieldtype": "Link", + "label": "Company Contact Person", + "options": "Contact", + "print_hide": 1 } ], "icon": "fa fa-file-text", "idx": 105, "is_submittable": 1, "links": [], - "modified": "2024-05-23 16:35:54.905804", + "modified": "2024-11-26 12:42:06.872527", "modified_by": "Administrator", "module": "Selling", "name": "Sales Order", diff --git a/erpnext/selling/doctype/sales_order/sales_order.py b/erpnext/selling/doctype/sales_order/sales_order.py index d8b3f3c6dcf6..374c37f99bf4 100755 --- a/erpnext/selling/doctype/sales_order/sales_order.py +++ b/erpnext/selling/doctype/sales_order/sales_order.py @@ -85,6 +85,7 @@ class SalesOrder(SellingController): company: DF.Link company_address: DF.Link | None company_address_display: DF.SmallText | None + company_contact_person: DF.Link | None contact_display: DF.SmallText | None contact_email: DF.Data | None contact_mobile: DF.SmallText | None diff --git a/erpnext/stock/doctype/delivery_note/delivery_note.json b/erpnext/stock/doctype/delivery_note/delivery_note.json index 87c333370b24..4a0580f0e94a 100644 --- a/erpnext/stock/doctype/delivery_note/delivery_note.json +++ b/erpnext/stock/doctype/delivery_note/delivery_note.json @@ -108,8 +108,9 @@ "dispatch_address", "company_address_section", "company_address", - "column_break_101", "company_address_display", + "column_break_101", + "company_contact_person", "terms_tab", "tc_name", "terms", @@ -1391,13 +1392,20 @@ "fieldname": "named_place", "fieldtype": "Data", "label": "Named Place" + }, + { + "fieldname": "company_contact_person", + "fieldtype": "Link", + "label": "Company Contact Person", + "options": "Contact", + "print_hide": 1 } ], "icon": "fa fa-truck", "idx": 146, "is_submittable": 1, "links": [], - "modified": "2024-03-20 16:05:02.854990", + "modified": "2024-11-26 12:44:28.258215", "modified_by": "Administrator", "module": "Stock", "name": "Delivery Note", diff --git a/erpnext/stock/doctype/delivery_note/delivery_note.py b/erpnext/stock/doctype/delivery_note/delivery_note.py index 68eb07d3a77b..0bc6b28fe681 100644 --- a/erpnext/stock/doctype/delivery_note/delivery_note.py +++ b/erpnext/stock/doctype/delivery_note/delivery_note.py @@ -53,6 +53,7 @@ class DeliveryNote(SellingController): company: DF.Link company_address: DF.Link | None company_address_display: DF.SmallText | None + company_contact_person: DF.Link | None contact_display: DF.SmallText | None contact_email: DF.Data | None contact_mobile: DF.SmallText | None From 0fbc60a20e0958d93c935446f0271d9ee626ecb5 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Wed, 27 Nov 2024 23:52:16 +0100 Subject: [PATCH 05/44] fix: show "Send SMS" only when enabled (backport #43941) (#43970) fix: show "Send SMS" only when enabled (#43941) (cherry picked from commit 65088cbb1b7cf54858805b5a5bd1b38ff3e0e29d) Co-authored-by: Raffael Meyer <14891507+barredterra@users.noreply.github.com> --- erpnext/public/js/controllers/transaction.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/public/js/controllers/transaction.js b/erpnext/public/js/controllers/transaction.js index 77db3292eb78..b342f38ac404 100644 --- a/erpnext/public/js/controllers/transaction.js +++ b/erpnext/public/js/controllers/transaction.js @@ -486,7 +486,7 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe setup_sms() { var me = this; let blacklist = ['Purchase Invoice', 'BOM']; - if(this.frm.doc.docstatus===1 && !["Lost", "Stopped", "Closed"].includes(this.frm.doc.status) + if(frappe.boot.sms_gateway_enabled && this.frm.doc.docstatus===1 && !["Lost", "Stopped", "Closed"].includes(this.frm.doc.status) && !blacklist.includes(this.frm.doctype)) { this.frm.page.add_menu_item(__('Send SMS'), function() { me.send_sms(); }); } From c3bc724523aab30585554e24f7e3907ae48f6ea2 Mon Sep 17 00:00:00 2001 From: Khushi Rawat <142375893+khushi8112@users.noreply.github.com> Date: Thu, 28 Nov 2024 11:36:05 +0530 Subject: [PATCH 06/44] fix: IndexError in Asset Depreciation Ledger when query result is empty (cherry picked from commit 7c393e5aa01f0c1e05470e1073adf42e89540601) --- .../asset_depreciation_ledger/asset_depreciation_ledger.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/erpnext/accounts/report/asset_depreciation_ledger/asset_depreciation_ledger.py b/erpnext/accounts/report/asset_depreciation_ledger/asset_depreciation_ledger.py index e1545bdcd879..a1ed6e1caa19 100644 --- a/erpnext/accounts/report/asset_depreciation_ledger/asset_depreciation_ledger.py +++ b/erpnext/accounts/report/asset_depreciation_ledger/asset_depreciation_ledger.py @@ -71,6 +71,7 @@ def get_data(filters): assets = [d.against_voucher for d in gl_entries] assets_details = get_assets_details(assets) + print(gl_entries) for d in gl_entries: asset_data = assets_details.get(d.against_voucher) @@ -89,7 +90,9 @@ def get_data(filters): & (DepreciationSchedule.schedule_date == d.posting_date) ) ).run(as_dict=True) - asset_data.accumulated_depreciation_amount = query[0]["accumulated_depreciation_amount"] + asset_data.accumulated_depreciation_amount = ( + query[0]["accumulated_depreciation_amount"] if query else 0 + ) else: asset_data.accumulated_depreciation_amount += d.debit From 5bbef90f08abfcf35feb8ef9101735cc906078f8 Mon Sep 17 00:00:00 2001 From: Khushi Rawat <142375893+khushi8112@users.noreply.github.com> Date: Thu, 28 Nov 2024 11:41:00 +0530 Subject: [PATCH 07/44] chore: removed print statement (cherry picked from commit 1737de7c10d96eaf8b6f8430202977336d969e00) --- .../asset_depreciation_ledger/asset_depreciation_ledger.py | 1 - 1 file changed, 1 deletion(-) diff --git a/erpnext/accounts/report/asset_depreciation_ledger/asset_depreciation_ledger.py b/erpnext/accounts/report/asset_depreciation_ledger/asset_depreciation_ledger.py index a1ed6e1caa19..a21103c719de 100644 --- a/erpnext/accounts/report/asset_depreciation_ledger/asset_depreciation_ledger.py +++ b/erpnext/accounts/report/asset_depreciation_ledger/asset_depreciation_ledger.py @@ -71,7 +71,6 @@ def get_data(filters): assets = [d.against_voucher for d in gl_entries] assets_details = get_assets_details(assets) - print(gl_entries) for d in gl_entries: asset_data = assets_details.get(d.against_voucher) From 173d60fb7d9aaab62e7bcd7e1e6ae5ec5f261518 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Thu, 28 Nov 2024 14:41:26 +0530 Subject: [PATCH 08/44] fix: typeerror on transaction.js (cherry picked from commit 46ce8780f2c7284ae6149a9b4a82e58fa1b017d7) --- erpnext/public/js/controllers/transaction.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/public/js/controllers/transaction.js b/erpnext/public/js/controllers/transaction.js index b342f38ac404..18cddd7f7a1c 100644 --- a/erpnext/public/js/controllers/transaction.js +++ b/erpnext/public/js/controllers/transaction.js @@ -1124,7 +1124,7 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe apply_discount_on_item(doc, cdt, cdn, field) { var item = frappe.get_doc(cdt, cdn); - if(!item?.price_list_rate) { + if(item && !item.price_list_rate) { item[field] = 0.0; } else { this.price_list_rate(doc, cdt, cdn); From 8ab9fc7f55b7759033f540cd275630a30ace8e5b Mon Sep 17 00:00:00 2001 From: Ninad Parikh <109862100+Ninad1306@users.noreply.github.com> Date: Thu, 28 Nov 2024 15:59:52 +0530 Subject: [PATCH 09/44] fix: Data Should be Computed in Backend to Maintain Consistent Behaviour (#44195) (cherry picked from commit 69bd90b038e08b2348a100445acefb0d7fe7fecc) --- .../report/balance_sheet/balance_sheet.py | 4 ++ .../accounts/report/financial_statements.py | 65 +++++++++++++++++++ .../profit_and_loss_statement.py | 8 +++ erpnext/public/js/financial_statements.js | 39 ++++------- 4 files changed, 91 insertions(+), 25 deletions(-) diff --git a/erpnext/accounts/report/balance_sheet/balance_sheet.py b/erpnext/accounts/report/balance_sheet/balance_sheet.py index ab2f45d4f8bd..fc19c40f8f98 100644 --- a/erpnext/accounts/report/balance_sheet/balance_sheet.py +++ b/erpnext/accounts/report/balance_sheet/balance_sheet.py @@ -7,6 +7,7 @@ from frappe.utils import cint, flt from erpnext.accounts.report.financial_statements import ( + compute_growth_view_data, get_columns, get_data, get_filtered_list_for_consolidated_report, @@ -101,6 +102,9 @@ def execute(filters=None): period_list, asset, liability, equity, provisional_profit_loss, currency, filters ) + if filters.get("selected_view") == "Growth": + compute_growth_view_data(data, period_list) + return columns, data, message, chart, report_summary, primitive_summary diff --git a/erpnext/accounts/report/financial_statements.py b/erpnext/accounts/report/financial_statements.py index c233f3c7e2b6..ca9472557883 100644 --- a/erpnext/accounts/report/financial_statements.py +++ b/erpnext/accounts/report/financial_statements.py @@ -2,6 +2,7 @@ # License: GNU General Public License v3. See license.txt +import copy import functools import math import re @@ -668,3 +669,67 @@ def get_filtered_list_for_consolidated_report(filters, period_list): filtered_summary_list.append(period) return filtered_summary_list + + +def compute_growth_view_data(data, columns): + data_copy = copy.deepcopy(data) + + for row_idx in range(len(data_copy)): + for column_idx in range(1, len(columns)): + previous_period_key = columns[column_idx - 1].get("key") + current_period_key = columns[column_idx].get("key") + current_period_value = data_copy[row_idx].get(current_period_key) + previous_period_value = data_copy[row_idx].get(previous_period_key) + annual_growth = 0 + + if current_period_value is None: + data[row_idx][current_period_key] = None + continue + + if previous_period_value == 0 and current_period_value > 0: + annual_growth = 1 + + elif previous_period_value > 0: + annual_growth = (current_period_value - previous_period_value) / previous_period_value + + growth_percent = round(annual_growth * 100, 2) + + data[row_idx][current_period_key] = growth_percent + + +def compute_margin_view_data(data, columns, accumulated_values): + if not columns: + return + + if not accumulated_values: + columns.append({"key": "total"}) + + data_copy = copy.deepcopy(data) + + base_row = None + for row in data_copy: + if row.get("account_name") == _("Income"): + base_row = row + break + + if not base_row: + return + + for row_idx in range(len(data_copy)): + # Taking the total income from each column (for all the financial years) as the base (100%) + row = data_copy[row_idx] + if not row: + continue + + for column in columns: + curr_period = column.get("key") + base_value = base_row[curr_period] + curr_value = row[curr_period] + + if curr_value is None or base_value <= 0: + data[row_idx][curr_period] = None + continue + + margin_percent = round((curr_value / base_value) * 100, 2) + + data[row_idx][curr_period] = margin_percent diff --git a/erpnext/accounts/report/profit_and_loss_statement/profit_and_loss_statement.py b/erpnext/accounts/report/profit_and_loss_statement/profit_and_loss_statement.py index 35453f2ec442..2b6280c74b51 100644 --- a/erpnext/accounts/report/profit_and_loss_statement/profit_and_loss_statement.py +++ b/erpnext/accounts/report/profit_and_loss_statement/profit_and_loss_statement.py @@ -7,6 +7,8 @@ from frappe.utils import flt from erpnext.accounts.report.financial_statements import ( + compute_growth_view_data, + compute_margin_view_data, get_columns, get_data, get_filtered_list_for_consolidated_report, @@ -68,6 +70,12 @@ def execute(filters=None): period_list, filters.periodicity, income, expense, net_profit_loss, currency, filters ) + if filters.get("selected_view") == "Growth": + compute_growth_view_data(data, period_list) + + if filters.get("selected_view") == "Margin": + compute_margin_view_data(data, period_list, filters.accumulated_values) + return columns, data, None, chart, report_summary, primitive_summary diff --git a/erpnext/public/js/financial_statements.js b/erpnext/public/js/financial_statements.js index bd80c2548897..a58eb9f013a9 100644 --- a/erpnext/public/js/financial_statements.js +++ b/erpnext/public/js/financial_statements.js @@ -9,40 +9,29 @@ erpnext.financial_statements = { data && column.colIndex >= 3 ) { - //Assuming that the first three columns are s.no, account name and the very first year of the accounting values, to calculate the relative percentage values of the successive columns. - const lastAnnualValue = row[column.colIndex - 1].content; - const currentAnnualvalue = data[column.fieldname]; - if (currentAnnualvalue == undefined) return "NA"; //making this not applicable for undefined/null values - let annualGrowth = 0; - if (lastAnnualValue == 0 && currentAnnualvalue > 0) { - //If the previous year value is 0 and the current value is greater than 0 - annualGrowth = 1; - } else if (lastAnnualValue > 0) { - annualGrowth = (currentAnnualvalue - lastAnnualValue) / lastAnnualValue; - } + const growthPercent = data[column.fieldname]; - const growthPercent = Math.round(annualGrowth * 10000) / 100; //calculating the rounded off percentage + if (growthPercent == undefined) return "NA"; //making this not applicable for undefined/null values - value = $(`${(growthPercent >= 0 ? "+" : "") + growthPercent + "%"}`); - if (growthPercent < 0) { - value = $(value).addClass("text-danger"); + if (column.fieldname === "total") { + value = $(`${growthPercent}`); } else { - value = $(value).addClass("text-success"); + value = $(`${(growthPercent >= 0 ? "+" : "") + growthPercent + "%"}`); + + if (growthPercent < 0) { + value = $(value).addClass("text-danger"); + } else { + value = $(value).addClass("text-success"); + } } value = $(value).wrap("

").parent().html(); return value; } else if (frappe.query_report.get_filter_value("selected_view") == "Margin" && data) { - if (column.fieldname == "account" && data.account_name == __("Income")) { - //Taking the total income from each column (for all the financial years) as the base (100%) - this.baseData = row; - } if (column.colIndex >= 2) { - //Assuming that the first two columns are s.no and account name, to calculate the relative percentage values of the successive columns. - const currentAnnualvalue = data[column.fieldname]; - const baseValue = this.baseData[column.colIndex].content; - if (currentAnnualvalue == undefined || baseValue <= 0) return "NA"; - const marginPercent = Math.round((currentAnnualvalue / baseValue) * 10000) / 100; + const marginPercent = data[column.fieldname]; + + if (marginPercent == undefined) return "NA"; //making this not applicable for undefined/null values value = $(`${marginPercent + "%"}`); if (marginPercent < 0) value = $(value).addClass("text-danger"); From 633be8d06bcbba0a7b08ba3e9172f67a7562343b Mon Sep 17 00:00:00 2001 From: David Date: Mon, 29 Jul 2024 20:01:22 +0200 Subject: [PATCH 10/44] fix: link cash flow rows and fix summary linking (cherry picked from commit b94af285875e23ce0d983831a7831f00934c67c2) # Conflicts: # erpnext/public/js/financial_statements.js --- .../accounts/report/cash_flow/cash_flow.js | 5 +- .../accounts/report/cash_flow/cash_flow.py | 58 +++++++++++-------- .../accounts/report/financial_statements.py | 10 ++-- erpnext/public/js/financial_statements.js | 20 +++++-- 4 files changed, 57 insertions(+), 36 deletions(-) diff --git a/erpnext/accounts/report/cash_flow/cash_flow.js b/erpnext/accounts/report/cash_flow/cash_flow.js index c824f0d9f384..bc76ee0a114d 100644 --- a/erpnext/accounts/report/cash_flow/cash_flow.js +++ b/erpnext/accounts/report/cash_flow/cash_flow.js @@ -1,7 +1,10 @@ // Copyright (c) 2013, Frappe Technologies Pvt. Ltd. and contributors // For license information, please see license.txt -frappe.query_reports["Cash Flow"] = $.extend({}, erpnext.financial_statements); +frappe.query_reports["Cash Flow"] = $.extend(erpnext.financial_statements, { + name_field: "section", + parent_field: "parent_section", +}); erpnext.utils.add_dimensions("Cash Flow", 10); diff --git a/erpnext/accounts/report/cash_flow/cash_flow.py b/erpnext/accounts/report/cash_flow/cash_flow.py index 9b4416714158..562ac5efb818 100644 --- a/erpnext/accounts/report/cash_flow/cash_flow.py +++ b/erpnext/accounts/report/cash_flow/cash_flow.py @@ -30,7 +30,7 @@ def execute(filters=None): company=filters.company, ) - cash_flow_accounts = get_cash_flow_accounts() + cash_flow_sections = get_cash_flow_accounts() # compute net profit / loss income = get_data( @@ -60,14 +60,14 @@ def execute(filters=None): summary_data = {} company_currency = frappe.get_cached_value("Company", filters.company, "default_currency") - for cash_flow_account in cash_flow_accounts: + for cash_flow_section in cash_flow_sections: section_data = [] data.append( { - "account_name": cash_flow_account["section_header"], - "parent_account": None, + "section_name": "'" + cash_flow_section["section_header"] + "'", + "parent_section": None, "indent": 0.0, - "account": cash_flow_account["section_header"], + "section": cash_flow_section["section_header"], } ) @@ -75,31 +75,40 @@ def execute(filters=None): # add first net income in operations section if net_profit_loss: net_profit_loss.update( - {"indent": 1, "parent_account": cash_flow_accounts[0]["section_header"]} + {"indent": 1, "parent_section": cash_flow_sections[0]["section_header"]} ) data.append(net_profit_loss) section_data.append(net_profit_loss) - for account in cash_flow_account["account_types"]: - account_data = get_account_type_based_data( - filters.company, account["account_type"], period_list, filters.accumulated_values, filters + for row in cash_flow_section["account_types"]: + row_data = get_account_type_based_data( + filters.company, row["account_type"], period_list, filters.accumulated_values, filters ) - account_data.update( + accounts = frappe.get_all( + "Account", + filters={ + "account_type": row["account_type"], + "is_group": 0, + }, + pluck="name", + ) + row_data.update( { - "account_name": account["label"], - "account": account["label"], + "section_name": row["label"], + "section": row["label"], "indent": 1, - "parent_account": cash_flow_account["section_header"], + "accounts": accounts, + "parent_section": cash_flow_section["section_header"], "currency": company_currency, } ) - data.append(account_data) - section_data.append(account_data) + data.append(row_data) + section_data.append(row_data) add_total_row_account( data, section_data, - cash_flow_account["section_footer"], + cash_flow_section["section_footer"], period_list, company_currency, summary_data, @@ -109,7 +118,7 @@ def execute(filters=None): add_total_row_account( data, data, _("Net Change in Cash"), period_list, company_currency, summary_data, filters ) - columns = get_columns(filters.periodicity, period_list, filters.accumulated_values, filters.company) + columns = get_columns(filters.periodicity, period_list, filters.accumulated_values, filters.company, True) chart = get_chart_data(columns, data, company_currency) @@ -217,8 +226,8 @@ def get_start_date(period, accumulated_values, company): def add_total_row_account(out, data, label, period_list, currency, summary_data, filters, consolidated=False): total_row = { - "account_name": "'" + _("{0}").format(label) + "'", - "account": "'" + _("{0}").format(label) + "'", + "section_name": "'" + _("{0}").format(label) + "'", + "section": "'" + _("{0}").format(label) + "'", "currency": currency, } @@ -229,7 +238,7 @@ def add_total_row_account(out, data, label, period_list, currency, summary_data, period_list = get_filtered_list_for_consolidated_report(filters, period_list) for row in data: - if row.get("parent_account"): + if row.get("parent_section"): for period in period_list: key = period if consolidated else period["key"] total_row.setdefault(key, 0.0) @@ -254,13 +263,14 @@ def get_report_summary(summary_data, currency): def get_chart_data(columns, data, currency): labels = [d.get("label") for d in columns[2:]] + print(data) datasets = [ { - "name": account.get("account").replace("'", ""), - "values": [account.get(d.get("fieldname")) for d in columns[2:]], + "name": section.get("section").replace("'", ""), + "values": [section.get(d.get("fieldname")) for d in columns[2:]], } - for account in data - if account.get("parent_account") is None and account.get("currency") + for section in data + if section.get("parent_section") is None and section.get("currency") ] datasets = datasets[:-1] diff --git a/erpnext/accounts/report/financial_statements.py b/erpnext/accounts/report/financial_statements.py index ca9472557883..76b3dfc65c8b 100644 --- a/erpnext/accounts/report/financial_statements.py +++ b/erpnext/accounts/report/financial_statements.py @@ -335,8 +335,8 @@ def filter_out_zero_value_rows(data, parent_children_map, show_zero_values=False def add_total_row(out, root_type, balance_must_be, period_list, company_currency): total_row = { - "account_name": _("Total {0} ({1})").format(_(root_type), _(balance_must_be)), - "account": _("Total {0} ({1})").format(_(root_type), _(balance_must_be)), + "account_name": "'" + _("Total {0} ({1})").format(_(root_type), _(balance_must_be)) + "'", + "account": "'" + _("Total {0} ({1})").format(_(root_type), _(balance_must_be)) + "'", "currency": company_currency, "opening_balance": 0.0, } @@ -617,11 +617,11 @@ def get_cost_centers_with_children(cost_centers): return list(set(all_cost_centers)) -def get_columns(periodicity, period_list, accumulated_values=1, company=None): +def get_columns(periodicity, period_list, accumulated_values=1, company=None, cash_flow=False): columns = [ { - "fieldname": "account", - "label": _("Account"), + "fieldname": "stub", + "label": _("Account") if not cash_flow else _("Section"), "fieldtype": "Link", "options": "Account", "width": 300, diff --git a/erpnext/public/js/financial_statements.js b/erpnext/public/js/financial_statements.js index a58eb9f013a9..b14b5cdcf37d 100644 --- a/erpnext/public/js/financial_statements.js +++ b/erpnext/public/js/financial_statements.js @@ -28,6 +28,13 @@ erpnext.financial_statements = { return value; } else if (frappe.query_report.get_filter_value("selected_view") == "Margin" && data) { +<<<<<<< HEAD +======= + if (column.fieldname == "stub" && data.account_name == __("Income")) { + //Taking the total income from each column (for all the financial years) as the base (100%) + this.baseData = row; + } +>>>>>>> b94af28587 (fix: link cash flow rows and fix summary linking) if (column.colIndex >= 2) { const marginPercent = data[column.fieldname]; @@ -41,8 +48,9 @@ erpnext.financial_statements = { } } - if (data && column.fieldname == "account") { - value = data.account_name || value; + if (data && column.fieldname == "stub") { + // first column + value = data.section_name || data.account_name || value; if (filter && filter?.text && filter?.type == "contains") { if (!value.toLowerCase().includes(filter.text)) { @@ -50,7 +58,7 @@ erpnext.financial_statements = { } } - if (data.account) { + if (data.account || data.accounts) { column.link_onclick = "erpnext.financial_statements.open_general_ledger(" + JSON.stringify(data) + ")"; } @@ -59,7 +67,7 @@ erpnext.financial_statements = { value = default_formatter(value, row, column, data); - if (data && !data.parent_account) { + if (data && !data.parent_account && !data.parent_section) { value = $(`${value}`); var $value = $(value).css("font-weight", "bold"); @@ -73,13 +81,13 @@ erpnext.financial_statements = { return value; }, open_general_ledger: function (data) { - if (!data.account) return; + if (!data.account && !data.accounts) return; let project = $.grep(frappe.query_report.filters, function (e) { return e.df.fieldname == "project"; }); frappe.route_options = { - account: data.account, + account: data.account || data.accounts, company: frappe.query_report.get_filter_value("company"), from_date: data.from_date || data.year_start_date, to_date: data.to_date || data.year_end_date, From 48d6fcaab8e4fbc92c2cb8474c8cb9437e70449a Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Fri, 29 Nov 2024 10:43:36 +0530 Subject: [PATCH 11/44] chore: resolve conflict --- erpnext/public/js/financial_statements.js | 3 --- 1 file changed, 3 deletions(-) diff --git a/erpnext/public/js/financial_statements.js b/erpnext/public/js/financial_statements.js index b14b5cdcf37d..cf3efae6d523 100644 --- a/erpnext/public/js/financial_statements.js +++ b/erpnext/public/js/financial_statements.js @@ -28,13 +28,10 @@ erpnext.financial_statements = { return value; } else if (frappe.query_report.get_filter_value("selected_view") == "Margin" && data) { -<<<<<<< HEAD -======= if (column.fieldname == "stub" && data.account_name == __("Income")) { //Taking the total income from each column (for all the financial years) as the base (100%) this.baseData = row; } ->>>>>>> b94af28587 (fix: link cash flow rows and fix summary linking) if (column.colIndex >= 2) { const marginPercent = data[column.fieldname]; From ae81bb3c1bca02f4da1fb32ee74f2c8e2f7129a2 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Fri, 29 Nov 2024 10:52:38 +0530 Subject: [PATCH 12/44] chore: revert 'stub' --- erpnext/accounts/report/financial_statements.py | 2 +- erpnext/public/js/financial_statements.js | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/erpnext/accounts/report/financial_statements.py b/erpnext/accounts/report/financial_statements.py index 76b3dfc65c8b..73e49983fb20 100644 --- a/erpnext/accounts/report/financial_statements.py +++ b/erpnext/accounts/report/financial_statements.py @@ -620,7 +620,7 @@ def get_cost_centers_with_children(cost_centers): def get_columns(periodicity, period_list, accumulated_values=1, company=None, cash_flow=False): columns = [ { - "fieldname": "stub", + "fieldname": "account", "label": _("Account") if not cash_flow else _("Section"), "fieldtype": "Link", "options": "Account", diff --git a/erpnext/public/js/financial_statements.js b/erpnext/public/js/financial_statements.js index cf3efae6d523..7a3877b9c460 100644 --- a/erpnext/public/js/financial_statements.js +++ b/erpnext/public/js/financial_statements.js @@ -28,7 +28,7 @@ erpnext.financial_statements = { return value; } else if (frappe.query_report.get_filter_value("selected_view") == "Margin" && data) { - if (column.fieldname == "stub" && data.account_name == __("Income")) { + if (column.fieldname == "account" && data.account_name == __("Income")) { //Taking the total income from each column (for all the financial years) as the base (100%) this.baseData = row; } @@ -45,7 +45,7 @@ erpnext.financial_statements = { } } - if (data && column.fieldname == "stub") { + if (data && column.fieldname == "account") { // first column value = data.section_name || data.account_name || value; From 5f785ede16341ab9e8db12db21b08d70c140de1a Mon Sep 17 00:00:00 2001 From: Abdeali Chharchhoda Date: Sat, 23 Nov 2024 14:47:31 +0530 Subject: [PATCH 13/44] refactor: Used object to get payment request status indicator (cherry picked from commit e1c4d6e1e666ee539caf746ed303003ac89f8024) # Conflicts: # erpnext/accounts/doctype/payment_request/payment_request_list.js --- .../payment_request/payment_request_list.js | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/erpnext/accounts/doctype/payment_request/payment_request_list.js b/erpnext/accounts/doctype/payment_request/payment_request_list.js index 183ca7c45849..a1e1549e9d9d 100644 --- a/erpnext/accounts/doctype/payment_request/payment_request_list.js +++ b/erpnext/accounts/doctype/payment_request/payment_request_list.js @@ -1,6 +1,17 @@ +const INDICATORS = { + "Partially Paid": "orange", + Cancelled: "red", + Draft: "gray", + Failed: "red", + Initiated: "green", + Paid: "blue", + Requested: "green", +}; + frappe.listview_settings["Payment Request"] = { add_fields: ["status"], get_indicator: function (doc) { +<<<<<<< HEAD if (doc.status == "Draft") { return [__("Draft"), "gray", "status,=,Draft"]; } @@ -15,5 +26,8 @@ frappe.listview_settings["Payment Request"] = { } else if (doc.status == "Cancelled") { return [__("Cancelled"), "red", "status,=,Cancelled"]; } +======= + return [__(doc.status), INDICATORS[doc.status] || "gray", `status,=,${doc.status}`]; +>>>>>>> e1c4d6e1e6 (refactor: Used object to get payment request status indicator) }, }; From 0d67c62f43758d453bc59ea0595046f48afa78ab Mon Sep 17 00:00:00 2001 From: Abdeali Chharchhoda Date: Sat, 23 Nov 2024 15:10:23 +0530 Subject: [PATCH 14/44] fix: Dashboard for `Payment Request` (cherry picked from commit 91955e27c38338ff72ae4d19d8f7e26880c5eb3a) --- .../payment_request/payment_request_dashboard.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) create mode 100644 erpnext/accounts/doctype/payment_request/payment_request_dashboard.py diff --git a/erpnext/accounts/doctype/payment_request/payment_request_dashboard.py b/erpnext/accounts/doctype/payment_request/payment_request_dashboard.py new file mode 100644 index 000000000000..02ad5684792b --- /dev/null +++ b/erpnext/accounts/doctype/payment_request/payment_request_dashboard.py @@ -0,0 +1,14 @@ +from frappe import _ + + +def get_data(): + return { + "fieldname": "payment_request", + "internal_links": { + "Payment Entry": ["references", "payment_request"], + "Payment Order": ["references", "payment_order"], + }, + "transactions": [ + {"label": _("Payment"), "items": ["Payment Entry", "Payment Order"]}, + ], + } From 9c4b5814a65b9bed086326aa11538f5a9db99938 Mon Sep 17 00:00:00 2001 From: Abdeali Chharchhoda Date: Tue, 26 Nov 2024 12:10:05 +0530 Subject: [PATCH 15/44] revert: remove default `Payment Request` indicator color (cherry picked from commit 37ceb09955c4e5a12714d6422c6092a271382e4f) # Conflicts: # erpnext/accounts/doctype/payment_request/payment_request_list.js --- .../doctype/payment_request/payment_request_list.js | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/erpnext/accounts/doctype/payment_request/payment_request_list.js b/erpnext/accounts/doctype/payment_request/payment_request_list.js index a1e1549e9d9d..61dae1451f96 100644 --- a/erpnext/accounts/doctype/payment_request/payment_request_list.js +++ b/erpnext/accounts/doctype/payment_request/payment_request_list.js @@ -11,6 +11,7 @@ const INDICATORS = { frappe.listview_settings["Payment Request"] = { add_fields: ["status"], get_indicator: function (doc) { +<<<<<<< HEAD <<<<<<< HEAD if (doc.status == "Draft") { return [__("Draft"), "gray", "status,=,Draft"]; @@ -29,5 +30,10 @@ frappe.listview_settings["Payment Request"] = { ======= return [__(doc.status), INDICATORS[doc.status] || "gray", `status,=,${doc.status}`]; >>>>>>> e1c4d6e1e6 (refactor: Used object to get payment request status indicator) +======= + if (!doc.status || !INDICATORS[doc.status]) return; + + return [__(doc.status), INDICATORS[doc.status], `status,=,${doc.status}`]; +>>>>>>> 37ceb09955 (revert: remove default `Payment Request` indicator color) }, }; From 4b046160f8f9d43c34c1f9e093d052d4f9ca2e27 Mon Sep 17 00:00:00 2001 From: Abdeali Chharchhoda Date: Wed, 27 Nov 2024 15:52:45 +0530 Subject: [PATCH 16/44] refactor: Move `PR` link filters to client side (cherry picked from commit 2db2c8bce1c1f453818e7e693ded0c0eec8053ec) --- erpnext/accounts/doctype/payment_entry/payment_entry.js | 4 ++++ .../accounts/doctype/payment_request/payment_request.py | 7 +------ 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/erpnext/accounts/doctype/payment_entry/payment_entry.js b/erpnext/accounts/doctype/payment_entry/payment_entry.js index a377aa04db2d..2d27cccfff86 100644 --- a/erpnext/accounts/doctype/payment_entry/payment_entry.js +++ b/erpnext/accounts/doctype/payment_entry/payment_entry.js @@ -185,6 +185,10 @@ frappe.ui.form.on("Payment Entry", { filters: { reference_doctype: row.reference_doctype, reference_name: row.reference_name, + company: doc.company, + status: ["!=", "Paid"], + outstanding_amount: [">", 0], // for compatibility with old data + docstatus: 1, }, }; }); diff --git a/erpnext/accounts/doctype/payment_request/payment_request.py b/erpnext/accounts/doctype/payment_request/payment_request.py index 1d31fd4b67b7..27e3aa830926 100644 --- a/erpnext/accounts/doctype/payment_request/payment_request.py +++ b/erpnext/accounts/doctype/payment_request/payment_request.py @@ -987,12 +987,7 @@ def get_open_payment_requests_query(doctype, txt, searchfield, start, page_len, open_payment_requests = frappe.get_list( "Payment Request", - filters={ - **filters, - "status": ["!=", "Paid"], - "outstanding_amount": ["!=", 0], # for compatibility with old data - "docstatus": 1, - }, + filters=filters, fields=["name", "grand_total", "outstanding_amount"], order_by="transaction_date ASC,creation ASC", ) From 5999a8e24f908dc84bce88969bdb192c03c2cd08 Mon Sep 17 00:00:00 2001 From: Abdeali Chharchhoda Date: Wed, 27 Nov 2024 17:18:10 +0530 Subject: [PATCH 17/44] fix: Add filter for `outstanding_amount` to fetch open PRs (cherry picked from commit 214dfab2697ec13e70262cb4af92ca812a2dcb80) --- erpnext/accounts/doctype/payment_entry/payment_entry.py | 1 + 1 file changed, 1 insertion(+) diff --git a/erpnext/accounts/doctype/payment_entry/payment_entry.py b/erpnext/accounts/doctype/payment_entry/payment_entry.py index 89f52b466799..5ebe1dd87549 100644 --- a/erpnext/accounts/doctype/payment_entry/payment_entry.py +++ b/erpnext/accounts/doctype/payment_entry/payment_entry.py @@ -2877,6 +2877,7 @@ def get_open_payment_requests_for_references(references=None): .where(Tuple(PR.reference_doctype, PR.reference_name).isin(list(refs))) .where(PR.status != "Paid") .where(PR.docstatus == 1) + .where(PR.outstanding_amount > 0) # to avoid old PRs with 0 outstanding amount .orderby(Coalesce(PR.transaction_date, PR.creation), order=frappe.qb.asc) ).run(as_dict=True) From 1c50111371eea3eb1abffb73babb550013f748b5 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Fri, 29 Nov 2024 14:50:41 +0530 Subject: [PATCH 18/44] chore: resolve conflict --- .../payment_request/payment_request_list.js | 21 ------------------- 1 file changed, 21 deletions(-) diff --git a/erpnext/accounts/doctype/payment_request/payment_request_list.js b/erpnext/accounts/doctype/payment_request/payment_request_list.js index 61dae1451f96..1027385aaaf1 100644 --- a/erpnext/accounts/doctype/payment_request/payment_request_list.js +++ b/erpnext/accounts/doctype/payment_request/payment_request_list.js @@ -11,29 +11,8 @@ const INDICATORS = { frappe.listview_settings["Payment Request"] = { add_fields: ["status"], get_indicator: function (doc) { -<<<<<<< HEAD -<<<<<<< HEAD - if (doc.status == "Draft") { - return [__("Draft"), "gray", "status,=,Draft"]; - } - if (doc.status == "Requested") { - return [__("Requested"), "green", "status,=,Requested"]; - } else if (doc.status == "Initiated") { - return [__("Initiated"), "green", "status,=,Initiated"]; - } else if (doc.status == "Partially Paid") { - return [__("Partially Paid"), "orange", "status,=,Partially Paid"]; - } else if (doc.status == "Paid") { - return [__("Paid"), "blue", "status,=,Paid"]; - } else if (doc.status == "Cancelled") { - return [__("Cancelled"), "red", "status,=,Cancelled"]; - } -======= - return [__(doc.status), INDICATORS[doc.status] || "gray", `status,=,${doc.status}`]; ->>>>>>> e1c4d6e1e6 (refactor: Used object to get payment request status indicator) -======= if (!doc.status || !INDICATORS[doc.status]) return; return [__(doc.status), INDICATORS[doc.status], `status,=,${doc.status}`]; ->>>>>>> 37ceb09955 (revert: remove default `Payment Request` indicator color) }, }; From e607795baed7544bcaedac031ed595b3a34eec4d Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Fri, 29 Nov 2024 15:49:18 +0530 Subject: [PATCH 19/44] fix: do not validate stock during inward (backport #44417) (#44427) fix: do not validate stock during inward (#44417) (cherry picked from commit d37d7b9811b8bd51f577004cfb89aefcab6d41cd) Co-authored-by: rohitwaghchaure --- .../doctype/serial_and_batch_bundle/serial_and_batch_bundle.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.py b/erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.py index 68c47b0d577b..dc30039a8c09 100644 --- a/erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.py +++ b/erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.py @@ -972,6 +972,9 @@ def validate_batch_inventory(self): ): return + if self.voucher_type in ["Sales Invoice", "Delivery Note"] and self.type_of_transaction == "Inward": + return + if not self.has_batch_no: return From 0e39aa349eda503339299acae94009c6e54b54cb Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Fri, 29 Nov 2024 17:00:59 +0530 Subject: [PATCH 20/44] fix: SABB print for packed items (backport #44413) (#44428) fix: SABB print for packed items (#44413) (cherry picked from commit 5266f236b70290bfcf32e51d86fb0a9a673f9576) Co-authored-by: rohitwaghchaure --- erpnext/controllers/print_settings.py | 8 +++++++- erpnext/stock/serial_batch_bundle.py | 2 +- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/erpnext/controllers/print_settings.py b/erpnext/controllers/print_settings.py index f99711631ffa..f05ef67c3087 100644 --- a/erpnext/controllers/print_settings.py +++ b/erpnext/controllers/print_settings.py @@ -11,7 +11,13 @@ def set_print_templates_for_item_table(doc, settings): "items": { "qty": "templates/print_formats/includes/item_table_qty.html", "serial_and_batch_bundle": "templates/print_formats/includes/serial_and_batch_bundle.html", - } + }, + "packed_items": { + "serial_and_batch_bundle": "templates/print_formats/includes/serial_and_batch_bundle.html", + }, + "supplied_items": { + "serial_and_batch_bundle": "templates/print_formats/includes/serial_and_batch_bundle.html", + }, } doc.flags.compact_item_fields = ["description", "qty", "rate", "amount"] diff --git a/erpnext/stock/serial_batch_bundle.py b/erpnext/stock/serial_batch_bundle.py index 3fed0195d693..dd459bb30bc4 100644 --- a/erpnext/stock/serial_batch_bundle.py +++ b/erpnext/stock/serial_batch_bundle.py @@ -478,7 +478,7 @@ def get_serial_or_batch_nos(bundle): html = "" for d in data: if d.serial_no: - html += f"" + html += f"" else: html += f"" From 0d41c23383961a332802de46c2919981e8250a3c Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Fri, 29 Nov 2024 18:12:24 +0530 Subject: [PATCH 21/44] fix: precision calculation causing 0.1 discrepancy (backport #44431) (#44436) fix: precision calculation causing 0.1 discrepancy (#44431) (cherry picked from commit 7f7564b581b8bc2e373b1bf6fc668b54f375d00c) Co-authored-by: rohitwaghchaure --- .../serial_and_batch_bundle/serial_and_batch_bundle.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.py b/erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.py index dc30039a8c09..9b07c1e4d191 100644 --- a/erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.py +++ b/erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.py @@ -432,8 +432,6 @@ def set_incoming_rate_for_inward_transaction(self, row=None, save=False): valuation_field = "rate" child_table = "Subcontracting Receipt Item" - precision = frappe.get_precision(child_table, valuation_field) or 2 - if not rate and self.voucher_detail_no and self.voucher_no: rate = frappe.db.get_value(child_table, self.voucher_detail_no, valuation_field) @@ -443,9 +441,9 @@ def set_incoming_rate_for_inward_transaction(self, row=None, save=False): elif (d.incoming_rate == rate) and d.qty and d.stock_value_difference: continue - d.incoming_rate = flt(rate, precision) + d.incoming_rate = rate if d.qty: - d.stock_value_difference = flt(d.qty) * flt(d.incoming_rate) + d.stock_value_difference = d.qty * d.incoming_rate if save: d.db_set( From c81b5e3d9cb8acb6d15b294bb0545f713ba5748d Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Fri, 29 Nov 2024 18:13:05 +0530 Subject: [PATCH 22/44] fix: source warehouse not set in required items of WO (backport #44426) (#44434) fix: source warehouse not set in required items of WO (#44426) fix: source warehouse not set in required items of WO on data import (cherry picked from commit 4050ea07eb7c45b3a9babe9472faf2fa89f47a9c) Co-authored-by: rohitwaghchaure --- erpnext/manufacturing/doctype/work_order/work_order.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/erpnext/manufacturing/doctype/work_order/work_order.py b/erpnext/manufacturing/doctype/work_order/work_order.py index 1ebcde753661..1d0df26800df 100644 --- a/erpnext/manufacturing/doctype/work_order/work_order.py +++ b/erpnext/manufacturing/doctype/work_order/work_order.py @@ -160,10 +160,18 @@ def validate(self): self.validate_workstation_type() self.reset_use_multi_level_bom() + if self.source_warehouse: + self.set_warehouses() + validate_uom_is_integer(self, "stock_uom", ["qty", "produced_qty"]) self.set_required_items(reset_only_qty=len(self.get("required_items"))) + def set_warehouses(self): + for row in self.required_items: + if not row.source_warehouse: + row.source_warehouse = self.source_warehouse + def reset_use_multi_level_bom(self): if self.is_new(): return From 1f9797905973ef8adc74867adc3a1f052cf5e83b Mon Sep 17 00:00:00 2001 From: Sagar Vora Date: Fri, 29 Nov 2024 22:51:32 +0530 Subject: [PATCH 23/44] perf: cache product bundle items at document level (#44440) (cherry picked from commit 6de7320ef40f218cf6aa3b01d0dc1c0c431e68ed) --- erpnext/controllers/selling_controller.py | 32 ++++++++++++++++++----- 1 file changed, 26 insertions(+), 6 deletions(-) diff --git a/erpnext/controllers/selling_controller.py b/erpnext/controllers/selling_controller.py index bb59166d3f87..459cf36bf449 100644 --- a/erpnext/controllers/selling_controller.py +++ b/erpnext/controllers/selling_controller.py @@ -381,12 +381,32 @@ def get_item_list(self): return il def has_product_bundle(self, item_code): - product_bundle = frappe.qb.DocType("Product Bundle") - return ( - frappe.qb.from_(product_bundle) - .select(product_bundle.name) - .where((product_bundle.new_item_code == item_code) & (product_bundle.disabled == 0)) - ).run() + product_bundle_items = getattr(self, "_product_bundle_items", None) + if product_bundle_items is None: + self._product_bundle_items = product_bundle_items = {} + + if item_code not in product_bundle_items: + self._fetch_product_bundle_items(item_code) + + return product_bundle_items[item_code] + + def _fetch_product_bundle_items(self, item_code): + product_bundle_items = self._product_bundle_items + items_to_fetch = {row.item_code for row in self.items if row.item_code not in product_bundle_items} + # fetch for requisite item_code even if it is not in items + items_to_fetch.add(item_code) + + items_with_product_bundle = { + row.new_item_code + for row in frappe.get_all( + "Product Bundle", + filters={"new_item_code": ("in", items_to_fetch), "disabled": 0}, + fields="new_item_code", + ) + } + + for item_code in items_to_fetch: + product_bundle_items[item_code] = item_code in items_with_product_bundle def get_already_delivered_qty(self, current_docname, so, so_detail): delivered_via_dn = frappe.db.sql( From 17c2734042ed0ed2f226ff2adb1ef70dfd63cfd9 Mon Sep 17 00:00:00 2001 From: Abdeali Chharchhoda Date: Fri, 29 Nov 2024 18:13:03 +0530 Subject: [PATCH 24/44] fix: added fieldname to avoid fieldname to translate (cherry picked from commit b80022133c0043d2ccfdcfef8770c2970adc9f0c) --- .../report/accounts_receivable/accounts_receivable.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/erpnext/accounts/report/accounts_receivable/accounts_receivable.py b/erpnext/accounts/report/accounts_receivable/accounts_receivable.py index a58d4ab72ca0..86e3dbc5b782 100644 --- a/erpnext/accounts/report/accounts_receivable/accounts_receivable.py +++ b/erpnext/accounts/report/accounts_receivable/accounts_receivable.py @@ -1013,7 +1013,7 @@ def get_party_details(self, party): def get_columns(self): self.columns = [] - self.add_column(_("Posting Date"), fieldtype="Date") + self.add_column(_("Posting Date"), fieldname="posting_date", fieldtype="Date") self.add_column( label=_("Party Type"), fieldname="party_type", @@ -1066,7 +1066,7 @@ def get_columns(self): width=180, ) - self.add_column(label=_("Due Date"), fieldtype="Date") + self.add_column(label=_("Due Date"), fieldname="due_date", fieldtype="Date") if self.account_type == "Payable": self.add_column(label=_("Bill No"), fieldname="bill_no", fieldtype="Data") From 48059a7c74422fa2c462aabb041fb642b01a9ae2 Mon Sep 17 00:00:00 2001 From: Sagar Vora Date: Sat, 30 Nov 2024 00:11:03 +0530 Subject: [PATCH 25/44] perf: reduce queries during transaction save (cherry picked from commit b6b8a06fda0ffaffe3e163af77772520d8c3be1c) --- erpnext/accounts/party.py | 14 +++++++------- erpnext/controllers/selling_controller.py | 8 +------- erpnext/utilities/transaction_base.py | 10 +++++----- 3 files changed, 13 insertions(+), 19 deletions(-) diff --git a/erpnext/accounts/party.py b/erpnext/accounts/party.py index 65054aec53f9..5be80872db8b 100644 --- a/erpnext/accounts/party.py +++ b/erpnext/accounts/party.py @@ -29,6 +29,12 @@ from erpnext.exceptions import InvalidAccountCurrency, PartyDisabled, PartyFrozen from erpnext.utilities.regional import temporary_flag +try: + from frappe.contacts.doctype.address.address import render_address as _render_address +except ImportError: + # Older frappe versions where this function is not available + from frappe.contacts.doctype.address.address import get_address_display as _render_address + PURCHASE_TRANSACTION_TYPES = { "Supplier Quotation", "Purchase Order", @@ -982,10 +988,4 @@ def add_party_account(party_type, party, company, account): def render_address(address, check_permissions=True): - try: - from frappe.contacts.doctype.address.address import render_address as _render - except ImportError: - # Older frappe versions where this function is not available - from frappe.contacts.doctype.address.address import get_address_display as _render - - return frappe.call(_render, address, check_permissions=check_permissions) + return frappe.call(_render_address, address, check_permissions=check_permissions) diff --git a/erpnext/controllers/selling_controller.py b/erpnext/controllers/selling_controller.py index 459cf36bf449..b704cb30791d 100644 --- a/erpnext/controllers/selling_controller.py +++ b/erpnext/controllers/selling_controller.py @@ -74,19 +74,13 @@ def set_missing_lead_customer_details(self, for_validate=False): if customer: from erpnext.accounts.party import _get_party_details - fetch_payment_terms_template = False - if self.get("__islocal") or self.company != frappe.db.get_value( - self.doctype, self.name, "company" - ): - fetch_payment_terms_template = True - party_details = _get_party_details( customer, ignore_permissions=self.flags.ignore_permissions, doctype=self.doctype, company=self.company, posting_date=self.get("posting_date"), - fetch_payment_terms_template=fetch_payment_terms_template, + fetch_payment_terms_template=self.has_value_changed("company"), party_address=self.customer_address, shipping_address=self.shipping_address_name, company_address=self.get("company_address"), diff --git a/erpnext/utilities/transaction_base.py b/erpnext/utilities/transaction_base.py index 6fab5380c382..2e4bdac6aab4 100644 --- a/erpnext/utilities/transaction_base.py +++ b/erpnext/utilities/transaction_base.py @@ -257,11 +257,11 @@ def validate_uom_is_integer(doc, uom_field, qty_fields, child_dt=None): if isinstance(qty_fields, str): qty_fields = [qty_fields] - distinct_uoms = list(set(d.get(uom_field) for d in doc.get_all_children())) - integer_uoms = list( - filter( - lambda uom: frappe.db.get_value("UOM", uom, "must_be_whole_number", cache=True) or None, - distinct_uoms, + distinct_uoms = tuple(set(uom for uom in (d.get(uom_field) for d in doc.get_all_children()) if uom)) + integer_uoms = set( + d[0] + for d in frappe.db.get_values( + "UOM", (("name", "in", distinct_uoms), ("must_be_whole_number", "=", 1)), cache=True ) ) From 579d8e293eb898b0bae0da31ca53b8aa46955884 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Sat, 30 Nov 2024 13:42:54 +0530 Subject: [PATCH 26/44] fix: number format in the message (backport #44435) (#44438) fix: number format in the message (#44435) (cherry picked from commit 810c72a30c6f06b19ae1f606b22c0df8fa0dc7b2) Co-authored-by: rohitwaghchaure --- .../serial_and_batch_bundle/serial_and_batch_bundle.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.py b/erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.py index 9b07c1e4d191..fff9cbfc07d1 100644 --- a/erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.py +++ b/erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.py @@ -607,8 +607,10 @@ def validate_quantity(self, row, qty_field=None): precision = row.precision if abs(abs(flt(self.total_qty, precision)) - abs(flt(qty, precision))) > 0.01: + total_qty = frappe.format_value(abs(flt(self.total_qty)), "Float", row) + set_qty = frappe.format_value(abs(flt(row.get(qty_field))), "Float", row) self.throw_error_message( - f"Total quantity {abs(flt(self.total_qty))} in the Serial and Batch Bundle {bold(self.name)} does not match with the quantity {abs(flt(row.get(qty_field)))} for the Item {bold(self.item_code)} in the {self.voucher_type} # {self.voucher_no}" + f"Total quantity {total_qty} in the Serial and Batch Bundle {bold(self.name)} does not match with the quantity {set_qty} for the Item {bold(self.item_code)} in the {self.voucher_type} # {self.voucher_no}" ) def get_qty_field(self, row, qty_field=None) -> str: From eb4a485df6e0953c1552549ffda74caea644ed60 Mon Sep 17 00:00:00 2001 From: Abdeali Chharchhoda Date: Fri, 29 Nov 2024 17:42:11 +0530 Subject: [PATCH 27/44] fix: Added translation for `Account` column (cherry picked from commit de6cbd382f7ff93adff53ad598c2d76361fab4a5) --- .../accounts/report/accounts_receivable/accounts_receivable.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/accounts/report/accounts_receivable/accounts_receivable.py b/erpnext/accounts/report/accounts_receivable/accounts_receivable.py index 86e3dbc5b782..ad6dd096b581 100644 --- a/erpnext/accounts/report/accounts_receivable/accounts_receivable.py +++ b/erpnext/accounts/report/accounts_receivable/accounts_receivable.py @@ -1028,7 +1028,7 @@ def get_columns(self): width=180, ) self.add_column( - label=self.account_type + " Account", + label=_(self.account_type + " Account"), fieldname="party_account", fieldtype="Link", options="Account", From fdda86455a93a25bb2216893da69cc8efe9a07d2 Mon Sep 17 00:00:00 2001 From: Abdeali Chharchhoda Date: Mon, 2 Dec 2024 11:23:10 +0530 Subject: [PATCH 28/44] fix: Translate `Party Account` column label (cherry picked from commit a4f8315602ad5ebfd3805b986f7186648d05313c) --- .../report/accounts_receivable/accounts_receivable.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/erpnext/accounts/report/accounts_receivable/accounts_receivable.py b/erpnext/accounts/report/accounts_receivable/accounts_receivable.py index ad6dd096b581..49dce0e299b5 100644 --- a/erpnext/accounts/report/accounts_receivable/accounts_receivable.py +++ b/erpnext/accounts/report/accounts_receivable/accounts_receivable.py @@ -1027,8 +1027,15 @@ def get_columns(self): options="party_type", width=180, ) + if self.account_type == "Receivable": + label = _("Receivable Account") + elif self.account_type == "Payable": + label = _("Payable Account") + else: + label = _("Party Account") + self.add_column( - label=_(self.account_type + " Account"), + label=label, fieldname="party_account", fieldtype="Link", options="Account", From c8e2c9aa2535b35a9c2a803762ef26a55ef804e2 Mon Sep 17 00:00:00 2001 From: ljain112 Date: Thu, 28 Nov 2024 19:50:01 +0530 Subject: [PATCH 29/44] fix: handle multi currency in common party journal entry (cherry picked from commit e371f68d66700a1641bb36da533385682feac076) # Conflicts: # erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py --- .../sales_invoice/test_sales_invoice.py | 194 ++++++++++++++++++ erpnext/controllers/accounts_controller.py | 67 +++++- 2 files changed, 251 insertions(+), 10 deletions(-) diff --git a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py index 90bec0182573..1d7359b811f6 100644 --- a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py +++ b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py @@ -4135,6 +4135,200 @@ def test_ledger_entries_on_opening_invoice_with_rounding_loss_by_inclusive_tax(s self.assertEqual(len(actual), 4) self.assertEqual(expected, actual) +<<<<<<< HEAD +======= + @IntegrationTestCase.change_settings("Accounts Settings", {"enable_common_party_accounting": True}) + def test_common_party_with_foreign_currency_jv(self): + from erpnext.accounts.doctype.account.test_account import create_account + from erpnext.accounts.doctype.opening_invoice_creation_tool.test_opening_invoice_creation_tool import ( + make_customer, + ) + from erpnext.accounts.doctype.party_link.party_link import create_party_link + from erpnext.buying.doctype.supplier.test_supplier import create_supplier + from erpnext.setup.utils import get_exchange_rate + + creditors = create_account( + account_name="Creditors USD", + parent_account="Accounts Payable - _TC", + company="_Test Company", + account_currency="USD", + account_type="Payable", + ) + debtors = create_account( + account_name="Debtors USD", + parent_account="Accounts Receivable - _TC", + company="_Test Company", + account_currency="USD", + account_type="Receivable", + ) + + # create a customer + customer = make_customer(customer="_Test Common Party USD") + cust_doc = frappe.get_doc("Customer", customer) + cust_doc.default_currency = "USD" + test_account_details = { + "company": "_Test Company", + "account": debtors, + } + cust_doc.append("accounts", test_account_details) + cust_doc.save() + + # create a supplier + supplier = create_supplier(supplier_name="_Test Common Party USD").name + supp_doc = frappe.get_doc("Supplier", supplier) + supp_doc.default_currency = "USD" + test_account_details = { + "company": "_Test Company", + "account": creditors, + } + supp_doc.append("accounts", test_account_details) + supp_doc.save() + + # create a party link between customer & supplier + create_party_link("Supplier", supplier, customer) + + # create a sales invoice + si = create_sales_invoice( + customer=customer, + currency="USD", + conversion_rate=get_exchange_rate("USD", "INR"), + debit_to=debtors, + do_not_save=1, + ) + si.party_account_currency = "USD" + si.save() + si.submit() + + # check outstanding of sales invoice + si.reload() + self.assertEqual(si.status, "Paid") + self.assertEqual(flt(si.outstanding_amount), 0.0) + + # check creation of journal entry + jv = frappe.get_all( + "Journal Entry Account", + { + "account": si.debit_to, + "party_type": "Customer", + "party": si.customer, + "reference_type": si.doctype, + "reference_name": si.name, + }, + pluck="credit_in_account_currency", + ) + self.assertTrue(jv) + self.assertEqual(jv[0], si.grand_total) + + @IntegrationTestCase.change_settings("Accounts Settings", {"enable_common_party_accounting": True}) + def test_common_party_with_different_currency_in_debtor_and_creditor(self): + from erpnext.accounts.doctype.account.test_account import create_account + from erpnext.accounts.doctype.opening_invoice_creation_tool.test_opening_invoice_creation_tool import ( + make_customer, + ) + from erpnext.accounts.doctype.party_link.party_link import create_party_link + from erpnext.buying.doctype.supplier.test_supplier import create_supplier + from erpnext.setup.utils import get_exchange_rate + + creditors = create_account( + account_name="Creditors INR", + parent_account="Accounts Payable - _TC", + company="_Test Company", + account_currency="INR", + account_type="Payable", + ) + debtors = create_account( + account_name="Debtors USD", + parent_account="Accounts Receivable - _TC", + company="_Test Company", + account_currency="USD", + account_type="Receivable", + ) + + # create a customer + customer = make_customer(customer="_Test Common Party USD") + cust_doc = frappe.get_doc("Customer", customer) + cust_doc.default_currency = "USD" + test_account_details = { + "company": "_Test Company", + "account": debtors, + } + cust_doc.append("accounts", test_account_details) + cust_doc.save() + + # create a supplier + supplier = create_supplier(supplier_name="_Test Common Party INR").name + supp_doc = frappe.get_doc("Supplier", supplier) + supp_doc.default_currency = "INR" + test_account_details = { + "company": "_Test Company", + "account": creditors, + } + supp_doc.append("accounts", test_account_details) + supp_doc.save() + + # create a party link between customer & supplier + create_party_link("Supplier", supplier, customer) + + # create a sales invoice + si = create_sales_invoice( + customer=customer, + currency="USD", + conversion_rate=get_exchange_rate("USD", "INR"), + debit_to=debtors, + do_not_save=1, + ) + si.party_account_currency = "USD" + si.save() + si.submit() + + # check outstanding of sales invoice + si.reload() + self.assertEqual(si.status, "Paid") + self.assertEqual(flt(si.outstanding_amount), 0.0) + + # check creation of journal entry + jv = frappe.get_all( + "Journal Entry Account", + { + "account": si.debit_to, + "party_type": "Customer", + "party": si.customer, + "reference_type": si.doctype, + "reference_name": si.name, + }, + pluck="credit_in_account_currency", + ) + self.assertTrue(jv) + self.assertEqual(jv[0], si.grand_total) + + def test_invoice_remarks(self): + si = frappe.copy_doc(self.globalTestRecords["Sales Invoice"][0]) + si.po_no = "Test PO" + si.po_date = nowdate() + si.save() + si.submit() + self.assertEqual(si.remarks, f"Against Customer Order Test PO dated {format_date(nowdate())}") + + def test_gl_voucher_subtype(self): + si = create_sales_invoice() + gl_entries = frappe.get_all( + "GL Entry", + filters={"voucher_type": "Sales Invoice", "voucher_no": si.name}, + pluck="voucher_subtype", + ) + + self.assertTrue(all([x == "Sales Invoice" for x in gl_entries])) + + si = create_sales_invoice(is_return=1, qty=-1) + gl_entries = frappe.get_all( + "GL Entry", + filters={"voucher_type": "Sales Invoice", "voucher_no": si.name}, + pluck="voucher_subtype", + ) + + self.assertTrue(all([x == "Credit Note" for x in gl_entries])) + +>>>>>>> e371f68d66 (fix: handle multi currency in common party journal entry) def set_advance_flag(company, flag, default_account): frappe.db.set_value( diff --git a/erpnext/controllers/accounts_controller.py b/erpnext/controllers/accounts_controller.py index b4b23dd5f4ce..bc0cdde94dee 100644 --- a/erpnext/controllers/accounts_controller.py +++ b/erpnext/controllers/accounts_controller.py @@ -2465,6 +2465,12 @@ def create_advance_and_reconcile(self, party_link): secondary_account = get_party_account(secondary_party_type, secondary_party, self.company) primary_account_currency = get_account_currency(primary_account) secondary_account_currency = get_account_currency(secondary_account) + default_currency = erpnext.get_company_currency(self.company) + + # Determine if multi-currency journal entry is needed + multi_currency = ( + primary_account_currency != default_currency or secondary_account_currency != default_currency + ) jv = frappe.new_doc("Journal Entry") jv.voucher_type = "Journal Entry" @@ -2489,7 +2495,7 @@ def create_advance_and_reconcile(self, party_link): advance_entry.cost_center = self.cost_center or erpnext.get_default_cost_center(self.company) advance_entry.is_advance = "Yes" - # update dimesions + # Update dimensions dimensions_dict = frappe._dict() active_dimensions = get_dimensions()[0] for dim in active_dimensions: @@ -2498,17 +2504,58 @@ def create_advance_and_reconcile(self, party_link): reconcilation_entry.update(dimensions_dict) advance_entry.update(dimensions_dict) - if self.doctype == "Sales Invoice": - reconcilation_entry.credit_in_account_currency = self.outstanding_amount - advance_entry.debit_in_account_currency = self.outstanding_amount - else: - advance_entry.credit_in_account_currency = self.outstanding_amount - reconcilation_entry.debit_in_account_currency = self.outstanding_amount + # Calculate exchange rates if necessary + if multi_currency: + # Exchange rates for primary and secondary accounts + exc_rate_primary_to_default = ( + 1 + if primary_account_currency == default_currency + else get_exchange_rate(primary_account_currency, default_currency, self.posting_date) + ) + exc_rate_secondary_to_default = ( + 1 + if secondary_account_currency == default_currency + else get_exchange_rate(secondary_account_currency, default_currency, self.posting_date) + ) + exc_rate_secondary_to_primary = ( + 1 + if secondary_account_currency == primary_account_currency + else get_exchange_rate( + secondary_account_currency, primary_account_currency, self.posting_date + ) + ) - default_currency = erpnext.get_company_currency(self.company) - if primary_account_currency != default_currency or secondary_account_currency != default_currency: - jv.multi_currency = 1 + # Convert outstanding amount from secondary to primary account currency, if needed + + os_in_default_currency = self.outstanding_amount * exc_rate_secondary_to_default + os_in_primary_currency = self.outstanding_amount * exc_rate_secondary_to_primary + + if self.doctype == "Sales Invoice": + # Calculate credit and debit values for reconciliation and advance entries + reconcilation_entry.credit_in_account_currency = self.outstanding_amount + reconcilation_entry.credit = os_in_default_currency + + advance_entry.debit_in_account_currency = os_in_primary_currency + advance_entry.debit = os_in_default_currency + else: + advance_entry.credit_in_account_currency = os_in_primary_currency + advance_entry.credit = os_in_default_currency + + reconcilation_entry.debit_in_account_currency = self.outstanding_amount + reconcilation_entry.debit = os_in_default_currency + + # Set exchange rates for entries + reconcilation_entry.exchange_rate = exc_rate_secondary_to_default + advance_entry.exchange_rate = exc_rate_primary_to_default + else: + if self.doctype == "Sales Invoice": + reconcilation_entry.credit_in_account_currency = self.outstanding_amount + advance_entry.debit_in_account_currency = self.outstanding_amount + else: + advance_entry.credit_in_account_currency = self.outstanding_amount + reconcilation_entry.debit_in_account_currency = self.outstanding_amount + jv.multi_currency = multi_currency jv.append("accounts", reconcilation_entry) jv.append("accounts", advance_entry) From 4c5570ae7d5d04fd3254f3c68e08877530a21997 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Mon, 2 Dec 2024 13:46:39 +0530 Subject: [PATCH 30/44] chore: resolve conflict --- .../sales_invoice/test_sales_invoice.py | 114 +----------------- 1 file changed, 1 insertion(+), 113 deletions(-) diff --git a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py index 1d7359b811f6..d2b8882743aa 100644 --- a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py +++ b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py @@ -4135,91 +4135,7 @@ def test_ledger_entries_on_opening_invoice_with_rounding_loss_by_inclusive_tax(s self.assertEqual(len(actual), 4) self.assertEqual(expected, actual) -<<<<<<< HEAD -======= - @IntegrationTestCase.change_settings("Accounts Settings", {"enable_common_party_accounting": True}) - def test_common_party_with_foreign_currency_jv(self): - from erpnext.accounts.doctype.account.test_account import create_account - from erpnext.accounts.doctype.opening_invoice_creation_tool.test_opening_invoice_creation_tool import ( - make_customer, - ) - from erpnext.accounts.doctype.party_link.party_link import create_party_link - from erpnext.buying.doctype.supplier.test_supplier import create_supplier - from erpnext.setup.utils import get_exchange_rate - - creditors = create_account( - account_name="Creditors USD", - parent_account="Accounts Payable - _TC", - company="_Test Company", - account_currency="USD", - account_type="Payable", - ) - debtors = create_account( - account_name="Debtors USD", - parent_account="Accounts Receivable - _TC", - company="_Test Company", - account_currency="USD", - account_type="Receivable", - ) - - # create a customer - customer = make_customer(customer="_Test Common Party USD") - cust_doc = frappe.get_doc("Customer", customer) - cust_doc.default_currency = "USD" - test_account_details = { - "company": "_Test Company", - "account": debtors, - } - cust_doc.append("accounts", test_account_details) - cust_doc.save() - - # create a supplier - supplier = create_supplier(supplier_name="_Test Common Party USD").name - supp_doc = frappe.get_doc("Supplier", supplier) - supp_doc.default_currency = "USD" - test_account_details = { - "company": "_Test Company", - "account": creditors, - } - supp_doc.append("accounts", test_account_details) - supp_doc.save() - - # create a party link between customer & supplier - create_party_link("Supplier", supplier, customer) - - # create a sales invoice - si = create_sales_invoice( - customer=customer, - currency="USD", - conversion_rate=get_exchange_rate("USD", "INR"), - debit_to=debtors, - do_not_save=1, - ) - si.party_account_currency = "USD" - si.save() - si.submit() - - # check outstanding of sales invoice - si.reload() - self.assertEqual(si.status, "Paid") - self.assertEqual(flt(si.outstanding_amount), 0.0) - - # check creation of journal entry - jv = frappe.get_all( - "Journal Entry Account", - { - "account": si.debit_to, - "party_type": "Customer", - "party": si.customer, - "reference_type": si.doctype, - "reference_name": si.name, - }, - pluck="credit_in_account_currency", - ) - self.assertTrue(jv) - self.assertEqual(jv[0], si.grand_total) - - @IntegrationTestCase.change_settings("Accounts Settings", {"enable_common_party_accounting": True}) + @change_settings("Accounts Settings", {"enable_common_party_accounting": True}) def test_common_party_with_different_currency_in_debtor_and_creditor(self): from erpnext.accounts.doctype.account.test_account import create_account from erpnext.accounts.doctype.opening_invoice_creation_tool.test_opening_invoice_creation_tool import ( @@ -4301,34 +4217,6 @@ def test_common_party_with_different_currency_in_debtor_and_creditor(self): self.assertTrue(jv) self.assertEqual(jv[0], si.grand_total) - def test_invoice_remarks(self): - si = frappe.copy_doc(self.globalTestRecords["Sales Invoice"][0]) - si.po_no = "Test PO" - si.po_date = nowdate() - si.save() - si.submit() - self.assertEqual(si.remarks, f"Against Customer Order Test PO dated {format_date(nowdate())}") - - def test_gl_voucher_subtype(self): - si = create_sales_invoice() - gl_entries = frappe.get_all( - "GL Entry", - filters={"voucher_type": "Sales Invoice", "voucher_no": si.name}, - pluck="voucher_subtype", - ) - - self.assertTrue(all([x == "Sales Invoice" for x in gl_entries])) - - si = create_sales_invoice(is_return=1, qty=-1) - gl_entries = frappe.get_all( - "GL Entry", - filters={"voucher_type": "Sales Invoice", "voucher_no": si.name}, - pluck="voucher_subtype", - ) - - self.assertTrue(all([x == "Credit Note" for x in gl_entries])) - ->>>>>>> e371f68d66 (fix: handle multi currency in common party journal entry) def set_advance_flag(company, flag, default_account): frappe.db.set_value( From ae93f7f967e2b4da1c76552a794bce118827b57f Mon Sep 17 00:00:00 2001 From: Sagar Vora Date: Mon, 2 Dec 2024 14:54:29 +0530 Subject: [PATCH 31/44] fix: set correct unallocated amount in Payment Entry (#43958) * fix: set correct unallocated amount in Payment Entry * fix: add checkbox and other logic fix * fix: patch to set is_exchange_gain_loss in Payment Entry deductions * fix: consider deductions except exch. gain/loss * fix: set exchange gain loss in payment entry * fix: separate function to set exchange gain loss * fix: failing test cases * fix: add cash disc. row first * fix: review changes * fix: changes as per review * fix: failing test cases * fix: review * fix: wait for request to complete before updating exchange gain loss * fix: review --------- Co-authored-by: vishakhdesai Co-authored-by: ruthra kumar (cherry picked from commit 7cc111f7907b47a177dffc4bc40992281d8946b3) # Conflicts: # erpnext/accounts/doctype/payment_entry_deduction/payment_entry_deduction.json # erpnext/patches.txt --- .../test_exchange_rate_revaluation.py | 4 +- .../doctype/payment_entry/payment_entry.js | 232 +++++++++++------- .../doctype/payment_entry/payment_entry.json | 8 +- .../doctype/payment_entry/payment_entry.py | 135 ++++++---- .../payment_entry/test_payment_entry.py | 38 +-- .../payment_entry_deduction.json | 13 + .../payment_entry_deduction.py | 1 + .../test_unreconcile_payment.py | 2 + erpnext/accounts/test/test_utils.py | 8 +- erpnext/patches.txt | 6 + ...e_gain_loss_in_payment_entry_deductions.py | 22 ++ 11 files changed, 299 insertions(+), 170 deletions(-) create mode 100644 erpnext/patches/v15_0/set_is_exchange_gain_loss_in_payment_entry_deductions.py diff --git a/erpnext/accounts/doctype/exchange_rate_revaluation/test_exchange_rate_revaluation.py b/erpnext/accounts/doctype/exchange_rate_revaluation/test_exchange_rate_revaluation.py index 51053f1f68c7..3eef6ab3832d 100644 --- a/erpnext/accounts/doctype/exchange_rate_revaluation/test_exchange_rate_revaluation.py +++ b/erpnext/accounts/doctype/exchange_rate_revaluation/test_exchange_rate_revaluation.py @@ -188,7 +188,7 @@ def test_03_accounts_only_with_account_currency_balance(self): pe = get_payment_entry(si.doctype, si.name) pe.paid_amount = 95 - pe.source_exchange_rate = 84.211 + pe.source_exchange_rate = 84.2105 pe.received_amount = 8000 pe.references = [] pe.save().submit() @@ -229,7 +229,7 @@ def test_03_accounts_only_with_account_currency_balance(self): row = next(x for x in je.accounts if x.account == self.debtors_usd) self.assertEqual(flt(row.credit_in_account_currency, precision), 5.0) # in USD row = next(x for x in je.accounts if x.account != self.debtors_usd) - self.assertEqual(flt(row.debit_in_account_currency, precision), 421.06) # in INR + self.assertEqual(flt(row.debit_in_account_currency, precision), 421.05) # in INR # total_debit and total_credit will be 0.0, as JV is posting only to account currency fields self.assertEqual(flt(je.total_debit, precision), 0.0) diff --git a/erpnext/accounts/doctype/payment_entry/payment_entry.js b/erpnext/accounts/doctype/payment_entry/payment_entry.js index 2d27cccfff86..f2d11ba9ff3c 100644 --- a/erpnext/accounts/doctype/payment_entry/payment_entry.js +++ b/erpnext/accounts/doctype/payment_entry/payment_entry.js @@ -324,11 +324,6 @@ frappe.ui.form.on("Payment Entry", { "write_off_difference_amount", frm.doc.difference_amount && frm.doc.party && frm.doc.total_allocated_amount > party_amount ); - - frm.toggle_display( - "set_exchange_gain_loss", - frm.doc.paid_amount && frm.doc.received_amount && frm.doc.difference_amount - ); }, set_dynamic_labels: function (frm) { @@ -1119,36 +1114,34 @@ frappe.ui.form.on("Payment Entry", { }, set_unallocated_amount: function (frm) { - var unallocated_amount = 0; - var total_deductions = frappe.utils.sum( - $.map(frm.doc.deductions || [], function (d) { - return flt(d.amount); - }) - ); + let unallocated_amount = 0; + let deductions_to_consider = 0; + + for (const row of frm.doc.deductions || []) { + if (!row.is_exchange_gain_loss) deductions_to_consider += flt(row.amount); + } + const included_taxes = get_included_taxes(frm); if (frm.doc.party) { if ( frm.doc.payment_type == "Receive" && - frm.doc.base_total_allocated_amount < frm.doc.base_received_amount + total_deductions && - frm.doc.total_allocated_amount < - frm.doc.paid_amount + total_deductions / frm.doc.source_exchange_rate + frm.doc.base_total_allocated_amount < frm.doc.base_paid_amount + deductions_to_consider ) { unallocated_amount = - (frm.doc.base_received_amount + - total_deductions - - flt(frm.doc.base_total_taxes_and_charges) - - frm.doc.base_total_allocated_amount) / + (frm.doc.base_paid_amount + + deductions_to_consider - + frm.doc.base_total_allocated_amount - + included_taxes) / frm.doc.source_exchange_rate; } else if ( frm.doc.payment_type == "Pay" && - frm.doc.base_total_allocated_amount < frm.doc.base_paid_amount - total_deductions && - frm.doc.total_allocated_amount < - frm.doc.received_amount + total_deductions / frm.doc.target_exchange_rate + frm.doc.base_total_allocated_amount < frm.doc.base_received_amount - deductions_to_consider ) { unallocated_amount = - (frm.doc.base_paid_amount + - flt(frm.doc.base_total_taxes_and_charges) - - (total_deductions + frm.doc.base_total_allocated_amount)) / + (frm.doc.base_received_amount - + deductions_to_consider - + frm.doc.base_total_allocated_amount - + included_taxes) / frm.doc.target_exchange_rate; } } @@ -1242,77 +1235,85 @@ frappe.ui.form.on("Payment Entry", { }, write_off_difference_amount: function (frm) { - frm.events.set_deductions_entry(frm, "write_off_account"); + frm.events.set_write_off_deduction(frm); }, - set_exchange_gain_loss: function (frm) { - frm.events.set_deductions_entry(frm, "exchange_gain_loss_account"); + base_paid_amount: function (frm) { + frm.events.set_exchange_gain_loss_deduction(frm); }, - set_deductions_entry: function (frm, account) { - if (frm.doc.difference_amount) { - frappe.call({ - method: "erpnext.accounts.doctype.payment_entry.payment_entry.get_company_defaults", - args: { - company: frm.doc.company, - }, - callback: function (r, rt) { - if (r.message) { - const write_off_row = $.map(frm.doc["deductions"] || [], function (t) { - return t.account == r.message[account] ? t : null; - }); + base_received_amount: function (frm) { + frm.events.set_exchange_gain_loss_deduction(frm); + }, - const difference_amount = flt( - frm.doc.difference_amount, - precision("difference_amount") - ); - - const add_deductions = (details) => { - let row = null; - if (!write_off_row.length && difference_amount) { - row = frm.add_child("deductions"); - row.account = details[account]; - row.cost_center = details["cost_center"]; - } else { - row = write_off_row[0]; - } + set_exchange_gain_loss_deduction: async function (frm) { + // wait for allocate_party_amount_against_ref_docs to finish + await frappe.after_ajax(); + const base_paid_amount = frm.doc.base_paid_amount || 0; + const base_received_amount = frm.doc.base_received_amount || 0; + const exchange_gain_loss = flt( + base_paid_amount - base_received_amount, + get_deduction_amount_precision() + ); - if (row) { - row.amount = flt(row.amount) + difference_amount; - } else { - frappe.msgprint(__("No gain or loss in the exchange rate")); - } - refresh_field("deductions"); - }; - - if (!r.message[account]) { - frappe.prompt( - { - label: __("Please Specify Account"), - fieldname: account, - fieldtype: "Link", - options: "Account", - get_query: () => ({ - filters: { - company: frm.doc.company, - }, - }), - }, - (values) => { - const details = Object.assign({}, r.message, values); - add_deductions(details); - }, - __(frappe.unscrub(account)) - ); - } else { - add_deductions(r.message); - } + if (!exchange_gain_loss) { + frm.events.delete_exchange_gain_loss(frm); + return; + } - frm.events.set_unallocated_amount(frm); - } - }, - }); + const account_fieldname = "exchange_gain_loss_account"; + let row = (frm.doc.deductions || []).find((t) => t.is_exchange_gain_loss); + + if (!row) { + const response = await get_company_defaults(frm.doc.company); + + const account = + response.message?.[account_fieldname] || + (await prompt_for_missing_account(frm, account_fieldname)); + + row = frm.add_child("deductions"); + row.account = account; + row.cost_center = response.message?.cost_center; + row.is_exchange_gain_loss = 1; + } + + row.amount = exchange_gain_loss; + frm.refresh_field("deductions"); + frm.events.set_unallocated_amount(frm); + }, + + delete_exchange_gain_loss: function (frm) { + const exchange_gain_loss_row = (frm.doc.deductions || []).find((row) => row.is_exchange_gain_loss); + + if (!exchange_gain_loss_row) return; + + exchange_gain_loss_row.amount = 0; + frm.get_field("deductions").grid.grid_rows[exchange_gain_loss_row.idx - 1].remove(); + frm.refresh_field("deductions"); + }, + + set_write_off_deduction: async function (frm) { + const difference_amount = flt(frm.doc.difference_amount, get_deduction_amount_precision()); + if (!difference_amount) return; + + const account_fieldname = "write_off_account"; + const response = await get_company_defaults(frm.doc.company); + const write_off_account = + response.message?.[account_fieldname] || + (await prompt_for_missing_account(frm, account_fieldname)); + + if (!write_off_account) return; + + let row = (frm.doc["deductions"] || []).find((t) => t.account == write_off_account); + if (!row) { + row = frm.add_child("deductions"); + row.account = write_off_account; + row.cost_center = response.message?.cost_center; } + + row.amount = flt(row.amount) + difference_amount; + frm.refresh_field("deductions"); + frm.events.set_unallocated_amount(frm); }, bank_account: function (frm) { @@ -1778,6 +1779,13 @@ frappe.ui.form.on("Advance Taxes and Charges", { }); frappe.ui.form.on("Payment Entry Deduction", { + before_deductions_remove: function (doc, cdt, cdn) { + const row = frappe.get_doc(cdt, cdn); + if (row.is_exchange_gain_loss && row.amount) { + frappe.throw(__("Cannot delete Exchange Gain/Loss row")); + } + }, + amount: function (frm) { frm.events.set_unallocated_amount(frm); }, @@ -1799,3 +1807,53 @@ function set_default_party_type(frm) { if (party_type) frm.set_value("party_type", party_type); } + +function get_included_taxes(frm) { + let included_taxes = 0; + for (const tax of frm.doc.taxes) { + if (!tax.included_in_paid_amount) continue; + + if (tax.add_deduct_tax == "Add") { + included_taxes += tax.base_tax_amount; + } else { + included_taxes -= tax.base_tax_amount; + } + } + + return included_taxes; +} + +function get_company_defaults(company) { + return frappe.call({ + method: "erpnext.accounts.doctype.payment_entry.payment_entry.get_company_defaults", + args: { + company: company, + }, + }); +} + +function prompt_for_missing_account(frm, account) { + return new Promise((resolve) => { + const dialog = frappe.prompt( + { + label: __(frappe.unscrub(account)), + fieldname: account, + fieldtype: "Link", + options: "Account", + get_query: () => ({ + filters: { + company: frm.doc.company, + }, + }), + }, + (values) => resolve(values?.[account]), + __("Please Specify Account") + ); + + dialog.on_hide = () => resolve(""); + }); +} + +function get_deduction_amount_precision() { + return frappe.meta.get_field_precision(frappe.meta.get_field("Payment Entry Deduction", "amount")); +} diff --git a/erpnext/accounts/doctype/payment_entry/payment_entry.json b/erpnext/accounts/doctype/payment_entry/payment_entry.json index d420bcca3422..69debbec5c7c 100644 --- a/erpnext/accounts/doctype/payment_entry/payment_entry.json +++ b/erpnext/accounts/doctype/payment_entry/payment_entry.json @@ -56,7 +56,6 @@ "section_break_34", "total_allocated_amount", "base_total_allocated_amount", - "set_exchange_gain_loss", "column_break_36", "unallocated_amount", "difference_amount", @@ -390,11 +389,6 @@ "print_hide": 1, "read_only": 1 }, - { - "fieldname": "set_exchange_gain_loss", - "fieldtype": "Button", - "label": "Set Exchange Gain / Loss" - }, { "fieldname": "column_break_36", "fieldtype": "Column Break" @@ -801,7 +795,7 @@ "table_fieldname": "payment_entries" } ], - "modified": "2024-05-31 17:07:06.197249", + "modified": "2024-11-07 11:19:19.320883", "modified_by": "Administrator", "module": "Accounts", "name": "Payment Entry", diff --git a/erpnext/accounts/doctype/payment_entry/payment_entry.py b/erpnext/accounts/doctype/payment_entry/payment_entry.py index 5ebe1dd87549..7e3d8a5833bb 100644 --- a/erpnext/accounts/doctype/payment_entry/payment_entry.py +++ b/erpnext/accounts/doctype/payment_entry/payment_entry.py @@ -893,6 +893,7 @@ def set_amounts(self): self.set_amounts_in_company_currency() self.set_total_allocated_amount() self.set_unallocated_amount() + self.set_exchange_gain_loss() self.set_difference_amount() def validate_amounts(self): @@ -988,10 +989,10 @@ def calculate_base_allocated_amount_for_reference(self, d) -> float: if d.exchange_rate is None: d.exchange_rate = 1 - allocated_amount_in_pe_exchange_rate = flt( + allocated_amount_in_ref_exchange_rate = flt( flt(d.allocated_amount) * flt(d.exchange_rate), self.precision("base_paid_amount") ) - d.exchange_gain_loss = base_allocated_amount - allocated_amount_in_pe_exchange_rate + d.exchange_gain_loss = base_allocated_amount - allocated_amount_in_ref_exchange_rate return base_allocated_amount def set_total_allocated_amount(self): @@ -1009,29 +1010,80 @@ def set_total_allocated_amount(self): def set_unallocated_amount(self): self.unallocated_amount = 0 - if self.party: - total_deductions = sum(flt(d.amount) for d in self.get("deductions")) - included_taxes = self.get_included_taxes() - if ( - self.payment_type == "Receive" - and self.base_total_allocated_amount < self.base_received_amount + total_deductions - and self.total_allocated_amount - < flt(self.paid_amount) + (total_deductions / self.source_exchange_rate) - ): - self.unallocated_amount = ( - self.base_received_amount + total_deductions - self.base_total_allocated_amount - ) / self.source_exchange_rate - self.unallocated_amount -= included_taxes - elif ( - self.payment_type == "Pay" - and self.base_total_allocated_amount < (self.base_paid_amount - total_deductions) - and self.total_allocated_amount - < flt(self.received_amount) + (total_deductions / self.target_exchange_rate) - ): - self.unallocated_amount = ( - self.base_paid_amount - (total_deductions + self.base_total_allocated_amount) - ) / self.target_exchange_rate - self.unallocated_amount -= included_taxes + if not self.party: + return + + deductions_to_consider = sum( + flt(d.amount) for d in self.get("deductions") if not d.is_exchange_gain_loss + ) + included_taxes = self.get_included_taxes() + + if self.payment_type == "Receive" and self.base_total_allocated_amount < ( + self.base_paid_amount + deductions_to_consider + ): + self.unallocated_amount = ( + self.base_paid_amount + + deductions_to_consider + - self.base_total_allocated_amount + - included_taxes + ) / self.source_exchange_rate + elif self.payment_type == "Pay" and self.base_total_allocated_amount < ( + self.base_received_amount - deductions_to_consider + ): + self.unallocated_amount = ( + self.base_received_amount + - deductions_to_consider + - self.base_total_allocated_amount + - included_taxes + ) / self.target_exchange_rate + + def set_exchange_gain_loss(self): + exchange_gain_loss = flt( + self.base_paid_amount - self.base_received_amount, + self.precision("amount", "deductions"), + ) + + exchange_gain_loss_rows = [row for row in self.get("deductions") if row.is_exchange_gain_loss] + exchange_gain_loss_row = exchange_gain_loss_rows.pop(0) if exchange_gain_loss_rows else None + + for row in exchange_gain_loss_rows: + self.remove(row) + + if not exchange_gain_loss: + if exchange_gain_loss_row: + self.remove(exchange_gain_loss_row) + + return + + if not exchange_gain_loss_row: + values = frappe.get_cached_value( + "Company", self.company, ("exchange_gain_loss_account", "cost_center"), as_dict=True + ) + + for fieldname, value in values.items(): + if value: + continue + + label = _(frappe.get_meta("Company").get_label(fieldname)) + return frappe.msgprint( + _("Please set {0} in Company {1} to account for Exchange Gain / Loss").format( + label, get_link_to_form("Company", self.company) + ), + title=_("Missing Default in Company"), + indicator="red" if self.docstatus.is_submitted() else "yellow", + raise_exception=self.docstatus.is_submitted(), + ) + + exchange_gain_loss_row = self.append( + "deductions", + { + "account": values.exchange_gain_loss_account, + "cost_center": values.cost_center, + "is_exchange_gain_loss": 1, + }, + ) + + exchange_gain_loss_row.amount = exchange_gain_loss def set_difference_amount(self): base_unallocated_amount = flt(self.unallocated_amount) * ( @@ -1059,11 +1111,13 @@ def set_difference_amount(self): def get_included_taxes(self): included_taxes = 0 for tax in self.get("taxes"): - if tax.included_in_paid_amount: - if tax.add_deduct_tax == "Add": - included_taxes += tax.base_tax_amount - else: - included_taxes -= tax.base_tax_amount + if not tax.included_in_paid_amount: + continue + + if tax.add_deduct_tax == "Add": + included_taxes += tax.base_tax_amount + else: + included_taxes -= tax.base_tax_amount return included_taxes @@ -1912,8 +1966,8 @@ def set_matched_payment_requests(self, matched_payment_requests): def get_matched_payment_request_of_references(references=None): """ Get those `Payment Requests` which are matched with `References`.\n - - Amount must be same. - - Only single `Payment Request` available for this amount. + - Amount must be same. + - Only single `Payment Request` available for this amount. Example: [(reference_doctype, reference_name, allocated_amount, payment_request), ...] """ @@ -2015,7 +2069,7 @@ def get_outstanding_of_references_with_payment_term(references=None): def get_outstanding_of_references_with_no_payment_term(references): """ Fetch outstanding amount of `References` which have no `Payment Term` set.\n - - Fetch outstanding amount from `References` it self. + - Fetch outstanding amount from `References` it self. Note: `None` is used for allocation of `Payment Request` Example: {(reference_doctype, reference_name, None): outstanding_amount, ...} @@ -2829,9 +2883,6 @@ def get_payment_entry( update_accounting_dimensions(pe, doc) if party_account and bank: - pe.set_exchange_rate(ref_doc=doc) - pe.set_amounts() - if discount_amount: base_total_discount_loss = 0 if frappe.db.get_single_value("Accounts Settings", "book_tax_discount_loss"): @@ -2841,7 +2892,8 @@ def get_payment_entry( pe, doc, discount_amount, base_total_discount_loss, party_account_currency ) - pe.set_difference_amount() + pe.set_exchange_rate(ref_doc=doc) + pe.set_amounts() # If PE is created from PR directly, then no need to find open PRs for the references if not created_from_payment_request: @@ -2853,7 +2905,7 @@ def get_payment_entry( def get_open_payment_requests_for_references(references=None): """ Fetch all unpaid Payment Requests for the references. \n - - Each reference can have multiple Payment Requests. \n + - Each reference can have multiple Payment Requests. \n Example: {("Sales Invoice", "SINV-00001"): {"PREQ-00001": 1000, "PREQ-00002": 2000}} """ @@ -3188,13 +3240,14 @@ def set_pending_discount_loss(pe, doc, discount_amount, base_total_discount_loss book_tax_loss = frappe.db.get_single_value("Accounts Settings", "book_tax_discount_loss") account_type = "round_off_account" if book_tax_loss else "default_discount_account" - pe.set_gain_or_loss( - account_details={ + pe.append( + "deductions", + { "account": frappe.get_cached_value("Company", pe.company, account_type), "cost_center": pe.cost_center or frappe.get_cached_value("Company", pe.company, "cost_center"), "amount": discount_amount * positive_negative, - } + }, ) diff --git a/erpnext/accounts/doctype/payment_entry/test_payment_entry.py b/erpnext/accounts/doctype/payment_entry/test_payment_entry.py index 8758110534f2..312628d9f97e 100644 --- a/erpnext/accounts/doctype/payment_entry/test_payment_entry.py +++ b/erpnext/accounts/doctype/payment_entry/test_payment_entry.py @@ -479,16 +479,9 @@ def test_payment_entry_multicurrency_accounting_si_with_early_payment_discount(s self.assertEqual(pe.deductions[0].account, "Write Off - _TC") # Exchange loss - self.assertEqual(pe.difference_amount, 300.0) - - pe.append( - "deductions", - { - "account": "_Test Exchange Gain/Loss - _TC", - "cost_center": "_Test Cost Center - _TC", - "amount": 300.0, - }, - ) + self.assertEqual(pe.deductions[-1].amount, 300.0) + pe.deductions[-1].account = "_Test Exchange Gain/Loss - _TC" + pe.deductions[-1].cost_center = "_Test Cost Center - _TC" pe.insert() pe.submit() @@ -552,16 +545,10 @@ def test_payment_entry_against_si_usd_to_inr(self): pe.reference_no = "1" pe.reference_date = "2016-01-01" - self.assertEqual(pe.difference_amount, 100) + self.assertEqual(pe.deductions[0].amount, 100) + pe.deductions[0].account = "_Test Exchange Gain/Loss - _TC" + pe.deductions[0].cost_center = "_Test Cost Center - _TC" - pe.append( - "deductions", - { - "account": "_Test Exchange Gain/Loss - _TC", - "cost_center": "_Test Cost Center - _TC", - "amount": 100, - }, - ) pe.insert() pe.submit() @@ -654,16 +641,9 @@ def test_internal_transfer_usd_to_inr(self): pe.set_exchange_rate() pe.set_amounts() - self.assertEqual(pe.difference_amount, 500) - - pe.append( - "deductions", - { - "account": "_Test Exchange Gain/Loss - _TC", - "cost_center": "_Test Cost Center - _TC", - "amount": 500, - }, - ) + self.assertEqual(pe.deductions[0].amount, 500) + pe.deductions[0].account = "_Test Exchange Gain/Loss - _TC" + pe.deductions[0].cost_center = "_Test Cost Center - _TC" pe.insert() pe.submit() diff --git a/erpnext/accounts/doctype/payment_entry_deduction/payment_entry_deduction.json b/erpnext/accounts/doctype/payment_entry_deduction/payment_entry_deduction.json index 1c31829f0ea6..82e871d0fffe 100644 --- a/erpnext/accounts/doctype/payment_entry_deduction/payment_entry_deduction.json +++ b/erpnext/accounts/doctype/payment_entry_deduction/payment_entry_deduction.json @@ -9,6 +9,7 @@ "cost_center", "amount", "column_break_2", + "is_exchange_gain_loss", "description" ], "fields": [ @@ -45,12 +46,24 @@ "fieldname": "description", "fieldtype": "Small Text", "label": "Description" + }, + { + "default": "0", + "depends_on": "eval:doc.is_exchange_gain_loss", + "fieldname": "is_exchange_gain_loss", + "fieldtype": "Check", + "label": "Is Exchange Gain / Loss?", + "read_only": 1 } ], "index_web_pages_for_search": 1, "istable": 1, "links": [], +<<<<<<< HEAD "modified": "2023-03-06 07:11:57.739619", +======= + "modified": "2024-11-05 16:07:47.307971", +>>>>>>> 7cc111f790 (fix: set correct unallocated amount in Payment Entry (#43958)) "modified_by": "Administrator", "module": "Accounts", "name": "Payment Entry Deduction", diff --git a/erpnext/accounts/doctype/payment_entry_deduction/payment_entry_deduction.py b/erpnext/accounts/doctype/payment_entry_deduction/payment_entry_deduction.py index fc67c526b28b..ae4134fc27a3 100644 --- a/erpnext/accounts/doctype/payment_entry_deduction/payment_entry_deduction.py +++ b/erpnext/accounts/doctype/payment_entry_deduction/payment_entry_deduction.py @@ -18,6 +18,7 @@ class PaymentEntryDeduction(Document): amount: DF.Currency cost_center: DF.Link description: DF.SmallText | None + is_exchange_gain_loss: DF.Check parent: DF.Data parentfield: DF.Data parenttype: DF.Data diff --git a/erpnext/accounts/doctype/unreconcile_payment/test_unreconcile_payment.py b/erpnext/accounts/doctype/unreconcile_payment/test_unreconcile_payment.py index 3d222b22ff8a..c058dbfa0b85 100644 --- a/erpnext/accounts/doctype/unreconcile_payment/test_unreconcile_payment.py +++ b/erpnext/accounts/doctype/unreconcile_payment/test_unreconcile_payment.py @@ -262,6 +262,7 @@ def test_04_unreconciliation_on_multi_currency_invoice(self): pe1.paid_from = self.debtors_usd pe1.paid_from_account_currency = "USD" pe1.source_exchange_rate = 75 + pe1.paid_amount = 100 pe1.received_amount = 75 * 100 pe1.save() # Allocate payment against both invoices @@ -279,6 +280,7 @@ def test_04_unreconciliation_on_multi_currency_invoice(self): pe2.paid_from = self.debtors_usd pe2.paid_from_account_currency = "USD" pe2.source_exchange_rate = 75 + pe2.paid_amount = 100 pe2.received_amount = 75 * 100 pe2.save() # Allocate payment against both invoices diff --git a/erpnext/accounts/test/test_utils.py b/erpnext/accounts/test/test_utils.py index 59cbc11794f4..5e108dee9b50 100644 --- a/erpnext/accounts/test/test_utils.py +++ b/erpnext/accounts/test/test_utils.py @@ -92,14 +92,14 @@ def test_update_reference_in_payment_entry(self): payment_entry.deductions = [] payment_entry.save() - # below is the difference between base_received_amount and base_paid_amount - self.assertEqual(payment_entry.difference_amount, -4855.0) + # below is the difference between base_paid_amount and base_received_amount (exchange gain) + self.assertEqual(payment_entry.deductions[0].amount, -4855.0) payment_entry.target_exchange_rate = 62.9 payment_entry.save() - # below is due to change in exchange rate - self.assertEqual(payment_entry.references[0].exchange_gain_loss, -4855.0) + # after changing the exchange rate, there is no exchange gain / loss + self.assertEqual(payment_entry.deductions, []) payment_entry.references = [] self.assertEqual(payment_entry.difference_amount, 0.0) diff --git a/erpnext/patches.txt b/erpnext/patches.txt index 2716aa9883bd..1828cfa031fb 100644 --- a/erpnext/patches.txt +++ b/erpnext/patches.txt @@ -380,6 +380,12 @@ erpnext.patches.v15_0.add_disassembly_order_stock_entry_type #1 erpnext.patches.v15_0.set_standard_stock_entry_type erpnext.patches.v15_0.link_purchase_item_to_asset_doc erpnext.patches.v14_0.update_currency_exchange_settings_for_frankfurter +<<<<<<< HEAD erpnext.patches.v15_0.update_task_assignee_email_field_in_asset_maintenance_log erpnext.patches.v15_0.update_sub_voucher_type_in_gl_entries erpnext.patches.v14_0.update_stock_uom_in_work_order_item +======= +erpnext.patches.v15_0.migrate_old_item_wise_tax_detail_data_format +erpnext.patches.v15_0.set_is_exchange_gain_loss_in_payment_entry_deductions +erpnext.patches.v14_0.update_stock_uom_in_work_order_item +>>>>>>> 7cc111f790 (fix: set correct unallocated amount in Payment Entry (#43958)) diff --git a/erpnext/patches/v15_0/set_is_exchange_gain_loss_in_payment_entry_deductions.py b/erpnext/patches/v15_0/set_is_exchange_gain_loss_in_payment_entry_deductions.py new file mode 100644 index 000000000000..9ffe272fd5e0 --- /dev/null +++ b/erpnext/patches/v15_0/set_is_exchange_gain_loss_in_payment_entry_deductions.py @@ -0,0 +1,22 @@ +import frappe + + +def execute(): + default_exchange_gain_loss_accounts = frappe.get_all( + "Company", + filters={"exchange_gain_loss_account": ["!=", ""]}, + pluck="exchange_gain_loss_account", + ) + + if not default_exchange_gain_loss_accounts: + return + + payment_entry = frappe.qb.DocType("Payment Entry") + payment_entry_deduction = frappe.qb.DocType("Payment Entry Deduction") + + frappe.qb.update(payment_entry_deduction).set(payment_entry_deduction.is_exchange_gain_loss, 1).join( + payment_entry, + ).on(payment_entry.name == payment_entry_deduction.parent).where( + (payment_entry.paid_to_account_currency != payment_entry.paid_from_account_currency) + & (payment_entry_deduction.account.isin(default_exchange_gain_loss_accounts)) + ).run() From c1579789121d8fb93e352a55f45c80aa1c6771b6 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Mon, 2 Dec 2024 14:57:54 +0530 Subject: [PATCH 32/44] chore: resolve conflicts --- .../payment_entry_deduction/payment_entry_deduction.json | 4 ---- erpnext/patches.txt | 5 ----- 2 files changed, 9 deletions(-) diff --git a/erpnext/accounts/doctype/payment_entry_deduction/payment_entry_deduction.json b/erpnext/accounts/doctype/payment_entry_deduction/payment_entry_deduction.json index 82e871d0fffe..e47b51ae028f 100644 --- a/erpnext/accounts/doctype/payment_entry_deduction/payment_entry_deduction.json +++ b/erpnext/accounts/doctype/payment_entry_deduction/payment_entry_deduction.json @@ -59,11 +59,7 @@ "index_web_pages_for_search": 1, "istable": 1, "links": [], -<<<<<<< HEAD - "modified": "2023-03-06 07:11:57.739619", -======= "modified": "2024-11-05 16:07:47.307971", ->>>>>>> 7cc111f790 (fix: set correct unallocated amount in Payment Entry (#43958)) "modified_by": "Administrator", "module": "Accounts", "name": "Payment Entry Deduction", diff --git a/erpnext/patches.txt b/erpnext/patches.txt index 1828cfa031fb..f53769155bde 100644 --- a/erpnext/patches.txt +++ b/erpnext/patches.txt @@ -380,12 +380,7 @@ erpnext.patches.v15_0.add_disassembly_order_stock_entry_type #1 erpnext.patches.v15_0.set_standard_stock_entry_type erpnext.patches.v15_0.link_purchase_item_to_asset_doc erpnext.patches.v14_0.update_currency_exchange_settings_for_frankfurter -<<<<<<< HEAD erpnext.patches.v15_0.update_task_assignee_email_field_in_asset_maintenance_log erpnext.patches.v15_0.update_sub_voucher_type_in_gl_entries erpnext.patches.v14_0.update_stock_uom_in_work_order_item -======= -erpnext.patches.v15_0.migrate_old_item_wise_tax_detail_data_format erpnext.patches.v15_0.set_is_exchange_gain_loss_in_payment_entry_deductions -erpnext.patches.v14_0.update_stock_uom_in_work_order_item ->>>>>>> 7cc111f790 (fix: set correct unallocated amount in Payment Entry (#43958)) From 5c6d9c98122ab0d2a9278455e97c776cf49e8a66 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Mon, 2 Dec 2024 15:37:38 +0530 Subject: [PATCH 33/44] refactor(UI): Rearranging fields under new sections (cherry picked from commit 7244754d28762c7c6347812529b8e28ab24475cc) --- erpnext/setup/doctype/company/company.json | 53 ++++++++++++++++++---- 1 file changed, 43 insertions(+), 10 deletions(-) diff --git a/erpnext/setup/doctype/company/company.json b/erpnext/setup/doctype/company/company.json index 4b07037ad3e2..271b440fbda6 100644 --- a/erpnext/setup/doctype/company/company.json +++ b/erpnext/setup/doctype/company/company.json @@ -48,24 +48,30 @@ "default_bank_account", "default_cash_account", "default_receivable_account", - "round_off_account", - "round_off_for_opening", - "round_off_cost_center", + "default_payable_account", "write_off_account", - "exchange_gain_loss_account", - "unrealized_exchange_gain_loss_account", "unrealized_profit_loss_account", "column_break0", "allow_account_creation_against_child_company", - "default_payable_account", "default_expense_account", "default_income_account", - "default_deferred_revenue_account", - "default_deferred_expense_account", "default_discount_account", "payment_terms", "cost_center", "default_finance_book", + "exchange_gain__loss_section", + "exchange_gain_loss_account", + "column_break_sttp", + "unrealized_exchange_gain_loss_account", + "round_off_section", + "round_off_account", + "round_off_cost_center", + "column_break_jqfo", + "round_off_for_opening", + "deferred_accounting_section", + "default_deferred_revenue_account", + "column_break_dcdl", + "default_deferred_expense_account", "advance_payments_section", "book_advance_payments_in_separate_party_account", "reconcile_on_advance_payment_date", @@ -287,7 +293,7 @@ { "fieldname": "default_settings", "fieldtype": "Section Break", - "label": "Accounts Settings", + "label": "Default Accounts", "oldfieldtype": "Section Break" }, { @@ -808,6 +814,33 @@ "fieldtype": "Link", "label": "Round Off for Opening", "options": "Account" + }, + { + "fieldname": "exchange_gain__loss_section", + "fieldtype": "Section Break", + "label": "Exchange Gain / Loss" + }, + { + "fieldname": "round_off_section", + "fieldtype": "Section Break", + "label": "Round Off" + }, + { + "fieldname": "deferred_accounting_section", + "fieldtype": "Section Break", + "label": "Deferred Accounting" + }, + { + "fieldname": "column_break_sttp", + "fieldtype": "Column Break" + }, + { + "fieldname": "column_break_jqfo", + "fieldtype": "Column Break" + }, + { + "fieldname": "column_break_dcdl", + "fieldtype": "Column Break" } ], "icon": "fa fa-building", @@ -815,7 +848,7 @@ "image_field": "company_logo", "is_tree": 1, "links": [], - "modified": "2024-08-02 11:34:46.785377", + "modified": "2024-12-02 15:37:32.723176", "modified_by": "Administrator", "module": "Setup", "name": "Company", From 05795af4716990fb4e3c104dbf4a6721fb7f7afa Mon Sep 17 00:00:00 2001 From: Ninad1306 Date: Wed, 27 Nov 2024 12:05:39 +0530 Subject: [PATCH 34/44] fix: always set sales incoming rate for internal transfers (cherry picked from commit d049c978843ebf3e974aea8559d12d8024fd6ca8) --- erpnext/controllers/buying_controller.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/erpnext/controllers/buying_controller.py b/erpnext/controllers/buying_controller.py index e9e7ef626702..6020dce07615 100644 --- a/erpnext/controllers/buying_controller.py +++ b/erpnext/controllers/buying_controller.py @@ -356,14 +356,14 @@ def set_incoming_rate(self): if not self.is_internal_transfer(): return + self.set_sales_incoming_rate_for_internal_transfer() + allow_at_arms_length_price = frappe.get_cached_value( "Stock Settings", None, "allow_internal_transfer_at_arms_length_price" ) if allow_at_arms_length_price: return - self.set_sales_incoming_rate_for_internal_transfer() - for d in self.get("items"): d.discount_percentage = 0.0 d.discount_amount = 0.0 From 558d49b3d3c1044a255e0c26d89954d7083484f7 Mon Sep 17 00:00:00 2001 From: Ninad1306 Date: Wed, 27 Nov 2024 12:06:11 +0530 Subject: [PATCH 35/44] test: validate buying workflow (cherry picked from commit 94d3fc9fde78220b7924d14b858204785480018a) --- .../tests/test_accounts_controller.py | 26 +++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/erpnext/controllers/tests/test_accounts_controller.py b/erpnext/controllers/tests/test_accounts_controller.py index b2f8fce3d312..289a955f9801 100644 --- a/erpnext/controllers/tests/test_accounts_controller.py +++ b/erpnext/controllers/tests/test_accounts_controller.py @@ -807,6 +807,7 @@ def test_15_gain_loss_on_different_posting_date(self): @change_settings("Stock Settings", {"allow_internal_transfer_at_arms_length_price": 1}) def test_16_internal_transfer_at_arms_length_price(self): + from erpnext.accounts.doctype.sales_invoice.sales_invoice import make_inter_company_purchase_invoice from erpnext.stock.doctype.warehouse.test_warehouse import create_warehouse prepare_data_for_internal_transfer() @@ -840,6 +841,31 @@ def test_16_internal_transfer_at_arms_length_price(self): # rate should reset to incoming rate self.assertEqual(si.items[0].rate, 100) + si.update_stock = 0 + si.save() + si.submit() + + pi = make_inter_company_purchase_invoice(si.name) + pi.update_stock = 1 + pi.items[0].rate = arms_length_price + pi.items[0].warehouse = target_warehouse + pi.items[0].from_warehouse = warehouse + pi.save() + + self.assertEqual(pi.items[0].rate, 100) + self.assertEqual(pi.items[0].valuation_rate, 100) + + frappe.db.set_single_value("Stock Settings", "allow_internal_transfer_at_arms_length_price", 1) + pi = make_inter_company_purchase_invoice(si.name) + pi.update_stock = 1 + pi.items[0].rate = arms_length_price + pi.items[0].warehouse = target_warehouse + pi.items[0].from_warehouse = warehouse + pi.save() + + self.assertEqual(pi.items[0].rate, arms_length_price) + self.assertEqual(pi.items[0].valuation_rate, 100) + def test_20_journal_against_sales_invoice(self): # Invoice in Foreign Currency si = self.create_sales_invoice(qty=1, conversion_rate=80, rate=1) From 435280d626e542f64e7265733f6dd478339ffcc5 Mon Sep 17 00:00:00 2001 From: ljain112 Date: Thu, 28 Nov 2024 20:45:17 +0530 Subject: [PATCH 36/44] fix: adjusted incoming rate for zero rated item in purchase receipt (cherry picked from commit 3182c6981c7bb2107b8c2d59b7f0e474f94c0cea) --- .../purchase_invoice/test_purchase_invoice.py | 24 +++++++++++++++++++ .../purchase_receipt/purchase_receipt.py | 2 +- 2 files changed, 25 insertions(+), 1 deletion(-) diff --git a/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py b/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py index f0b51c32c05d..e353435661c4 100644 --- a/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py +++ b/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py @@ -1735,6 +1735,30 @@ def test_adjust_incoming_rate(self): frappe.db.set_single_value("Buying Settings", "set_landed_cost_based_on_purchase_invoice_rate", 1) + # Cost of Item is zero in Purchase Receipt + pr = make_purchase_receipt(qty=1, rate=0) + + stock_value_difference = frappe.db.get_value( + "Stock Ledger Entry", + {"voucher_type": "Purchase Receipt", "voucher_no": pr.name}, + "stock_value_difference", + ) + self.assertEqual(stock_value_difference, 0) + + pi = create_purchase_invoice_from_receipt(pr.name) + for row in pi.items: + row.rate = 150 + + pi.save() + pi.submit() + + stock_value_difference = frappe.db.get_value( + "Stock Ledger Entry", + {"voucher_type": "Purchase Receipt", "voucher_no": pr.name}, + "stock_value_difference", + ) + self.assertEqual(stock_value_difference, 150) + # Increase the cost of the item pr = make_purchase_receipt(qty=1, rate=100) diff --git a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py index 4c2b6891cea3..76ecf0fd596a 100644 --- a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py +++ b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py @@ -1085,7 +1085,7 @@ def update_billing_percentage(pr_doc, update_modified=True, adjust_incoming_rate if adjust_incoming_rate: adjusted_amt = 0.0 - if item.billed_amt and item.amount: + if item.billed_amt is not None and item.amount is not None: adjusted_amt = flt(item.billed_amt) - flt(item.amount) adjusted_amt = adjusted_amt * flt(pr_doc.conversion_rate) From ebdacc094c47aa334282b5f75cc194e6c25d48ad Mon Sep 17 00:00:00 2001 From: Sugesh393 Date: Fri, 29 Nov 2024 17:39:40 +0530 Subject: [PATCH 37/44] fix: calculate submitted payment entry as paid amount (cherry picked from commit 561a159aec1d71125fa0b1779df5ac1ff111b0da) --- erpnext/accounts/doctype/payment_request/payment_request.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/erpnext/accounts/doctype/payment_request/payment_request.py b/erpnext/accounts/doctype/payment_request/payment_request.py index 27e3aa830926..61bb2932d2b4 100644 --- a/erpnext/accounts/doctype/payment_request/payment_request.py +++ b/erpnext/accounts/doctype/payment_request/payment_request.py @@ -778,6 +778,8 @@ def get_existing_paid_amount(doctype, name): .where(PL.against_voucher_type.eq(doctype)) .where(PL.against_voucher_no.eq(name)) .where(PL.amount < 0) + .where(PL.delinked == 0) + .where(PER.docstatus == 1) .where(PER.payment_request.isnull()) ) response = query.run() From aa090beae0d0a7d3730634c6525e43f681f74653 Mon Sep 17 00:00:00 2001 From: Sugesh393 Date: Fri, 29 Nov 2024 17:44:22 +0530 Subject: [PATCH 38/44] test: add new unit test to validate paid amount in payment request (cherry picked from commit 9bee2d430cb05f21b911d0af5f9c0d1a66615ffc) --- .../payment_request/test_payment_request.py | 27 +++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/erpnext/accounts/doctype/payment_request/test_payment_request.py b/erpnext/accounts/doctype/payment_request/test_payment_request.py index 4442dbdd7eac..eadb714baa30 100644 --- a/erpnext/accounts/doctype/payment_request/test_payment_request.py +++ b/erpnext/accounts/doctype/payment_request/test_payment_request.py @@ -543,3 +543,30 @@ def test_partial_paid_invoice_with_payment_request(self): pr = make_payment_request(dt="Sales Invoice", dn=si.name, mute_email=1) self.assertEqual(pr.grand_total, si.outstanding_amount) + + +def test_partial_paid_invoice_with_submitted_payment_entry(self): + pi = make_purchase_invoice(currency="INR", qty=1, rate=5000) + pi.save() + pi.submit() + + pe = get_payment_entry("Purchase Invoice", pi.name, bank_account="_Test Bank - _TC") + pe.reference_no = "PURINV0001" + pe.reference_date = frappe.utils.nowdate() + pe.paid_amount = 2500 + pe.references[0].allocated_amount = 2500 + pe.save() + pe.submit() + pe.cancel() + + pe = get_payment_entry("Purchase Invoice", pi.name, bank_account="_Test Bank - _TC") + pe.reference_no = "PURINV0002" + pe.reference_date = frappe.utils.nowdate() + pe.paid_amount = 2500 + pe.references[0].allocated_amount = 2500 + pe.save() + pe.submit() + + pi.load_from_db() + pr = make_payment_request(dt="Purchase Invoice", dn=pi.name, mute_email=1) + self.assertEqual(pr.grand_total, pi.outstanding_amount) From 0a9c92fce940ed74dbd1b22ecfbebfc6612ba61a Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Tue, 3 Dec 2024 15:09:09 +0530 Subject: [PATCH 39/44] fix: incorrect Gross Margin on project (backport #44461) (#44468) * fix: incorrect Gross Margin on project (#44461) (cherry picked from commit 7de9c14a2ce329ea6134fddf5c13424bc5191657) # Conflicts: # erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py * chore: resolve conflict --------- Co-authored-by: rohitwaghchaure Co-authored-by: ruthra kumar --- .../doctype/sales_invoice/sales_invoice.py | 3 +++ .../doctype/sales_invoice/test_sales_invoice.py | 14 ++++++++++++++ 2 files changed, 17 insertions(+) diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py index 410e934ab711..1a7ffc3c339b 100644 --- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py +++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py @@ -1755,6 +1755,9 @@ def validate_serial_against_delivery_note(self): def update_project(self): unique_projects = list(set([d.project for d in self.get("items") if d.project])) + if self.project and self.project not in unique_projects: + unique_projects.append(self.project) + for p in unique_projects: project = frappe.get_doc("Project", p) project.update_billed_amount() diff --git a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py index d2b8882743aa..57eb84caaa49 100644 --- a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py +++ b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py @@ -4217,6 +4217,20 @@ def test_common_party_with_different_currency_in_debtor_and_creditor(self): self.assertTrue(jv) self.assertEqual(jv[0], si.grand_total) + def test_total_billed_amount(self): + si = create_sales_invoice(do_not_submit=True) + + project = frappe.new_doc("Project") + project.project_name = "Test Total Billed Amount" + project.save() + + si.project = project.name + si.save() + si.submit() + + doc = frappe.get_doc("Project", project.name) + self.assertEqual(doc.total_billed_amount, si.grand_total) + def set_advance_flag(company, flag, default_account): frappe.db.set_value( From ea57f2b2921f5fb6b06a0be1bac0cac4ddaf248a Mon Sep 17 00:00:00 2001 From: ljain112 Date: Mon, 2 Dec 2024 14:08:21 +0530 Subject: [PATCH 40/44] fix: remove queries (cherry picked from commit a86b223aed6e978154dbe6feb3581bcdacff2d60) --- .../report/gross_profit/gross_profit.py | 29 +++++++------------ 1 file changed, 10 insertions(+), 19 deletions(-) diff --git a/erpnext/accounts/report/gross_profit/gross_profit.py b/erpnext/accounts/report/gross_profit/gross_profit.py index 5ba1e41b6244..852d33c6356a 100644 --- a/erpnext/accounts/report/gross_profit/gross_profit.py +++ b/erpnext/accounts/report/gross_profit/gross_profit.py @@ -421,10 +421,10 @@ def __init__(self, filters=None): self.load_invoice_items() self.get_delivery_notes() + self.load_product_bundle() if filters.group_by == "Invoice": self.group_items_by_invoice() - self.load_product_bundle() self.load_non_stock_items() self.get_returned_invoice_items() self.process() @@ -851,6 +851,7 @@ def load_invoice_items(self): `tabSales Invoice`.project, `tabSales Invoice`.update_stock, `tabSales Invoice`.customer, `tabSales Invoice`.customer_group, `tabSales Invoice`.territory, `tabSales Invoice Item`.item_code, + `tabSales Invoice`.base_net_total as "invoice_base_net_total", `tabSales Invoice Item`.item_name, `tabSales Invoice Item`.description, `tabSales Invoice Item`.warehouse, `tabSales Invoice Item`.item_group, `tabSales Invoice Item`.brand, `tabSales Invoice Item`.so_detail, @@ -911,6 +912,7 @@ def group_items_by_invoice(self): """ grouped = OrderedDict() + product_bundels = self.product_bundles.get("Sales Invoice", {}) for row in self.si_list: # initialize list with a header row for each new parent @@ -921,8 +923,7 @@ def group_items_by_invoice(self): ) # if item is a bundle, add it's components as seperate rows - if frappe.db.exists("Product Bundle", row.item_code): - bundled_items = self.get_bundle_items(row) + if bundled_items := product_bundels.get(row.parent, {}).get(row.item_code): for x in bundled_items: bundle_item = self.get_bundle_item_row(row, x) grouped.get(row.parent).append(bundle_item) @@ -958,18 +959,11 @@ def get_invoice_row(self, row): "item_row": None, "is_return": row.is_return, "cost_center": row.cost_center, - "base_net_amount": frappe.db.get_value("Sales Invoice", row.parent, "base_net_total"), + "base_net_amount": row.invoice_base_net_total, } ) - def get_bundle_items(self, product_bundle): - return frappe.get_all( - "Product Bundle Item", filters={"parent": product_bundle.item_code}, fields=["item_code", "qty"] - ) - def get_bundle_item_row(self, product_bundle, item): - item_name, description, item_group, brand = self.get_bundle_item_details(item.item_code) - return frappe._dict( { "parent_invoice": product_bundle.item_code, @@ -982,23 +976,20 @@ def get_bundle_item_row(self, product_bundle, item): "customer": product_bundle.customer, "customer_group": product_bundle.customer_group, "item_code": item.item_code, - "item_name": item_name, - "description": description, + "item_name": item.item_name, + "description": item.description, "warehouse": product_bundle.warehouse, - "item_group": item_group, - "brand": brand, + "item_group": "", + "brand": "", "dn_detail": product_bundle.dn_detail, "delivery_note": product_bundle.delivery_note, - "qty": (flt(product_bundle.qty) * flt(item.qty)), + "qty": item.total_qty * -1, "item_row": None, "is_return": product_bundle.is_return, "cost_center": product_bundle.cost_center, } ) - def get_bundle_item_details(self, item_code): - return frappe.db.get_value("Item", item_code, ["item_name", "description", "item_group", "brand"]) - def get_stock_ledger_entries(self, item_code, warehouse): if item_code and warehouse: if (item_code, warehouse) not in self.sle: From f165e1732b2160cf6cc65ca52ee2ce0d2a833667 Mon Sep 17 00:00:00 2001 From: ljain112 Date: Mon, 2 Dec 2024 15:20:20 +0530 Subject: [PATCH 41/44] fix: correct buying amount for product bundel (cherry picked from commit 4e6a5893e7a26da863920c782f2719861b5aaecd) --- .../report/gross_profit/gross_profit.py | 36 +++++++++++-------- 1 file changed, 21 insertions(+), 15 deletions(-) diff --git a/erpnext/accounts/report/gross_profit/gross_profit.py b/erpnext/accounts/report/gross_profit/gross_profit.py index 852d33c6356a..1191f72cb3fa 100644 --- a/erpnext/accounts/report/gross_profit/gross_profit.py +++ b/erpnext/accounts/report/gross_profit/gross_profit.py @@ -636,6 +636,7 @@ def get_buying_amount_from_product_bundle(self, row, product_bundle): if packed_item.get("parent_detail_docname") == row.item_row: packed_item_row = row.copy() packed_item_row.warehouse = packed_item.warehouse + packed_item_row.qty = packed_item.total_qty * -1 buying_amount += self.get_buying_amount(packed_item_row, packed_item.item_code) return flt(buying_amount, self.currency_precision) @@ -668,7 +669,9 @@ def get_buying_amount(self, row, item_code): else: my_sle = self.get_stock_ledger_entries(item_code, row.warehouse) if (row.update_stock or row.dn_detail) and my_sle: - parenttype, parent = row.parenttype, row.parent + parenttype = row.parenttype + parent = row.invoice or row.parent + if row.dn_detail: parenttype, parent = "Delivery Note", row.delivery_note @@ -963,30 +966,33 @@ def get_invoice_row(self, row): } ) - def get_bundle_item_row(self, product_bundle, item): + def get_bundle_item_row(self, row, item): return frappe._dict( { - "parent_invoice": product_bundle.item_code, - "indent": product_bundle.indent + 1, + "parent_invoice": row.item_code, + "parenttype": row.parenttype, + "indent": row.indent + 1, "parent": None, "invoice_or_item": item.item_code, - "posting_date": product_bundle.posting_date, - "posting_time": product_bundle.posting_time, - "project": product_bundle.project, - "customer": product_bundle.customer, - "customer_group": product_bundle.customer_group, + "posting_date": row.posting_date, + "posting_time": row.posting_time, + "project": row.project, + "customer": row.customer, + "customer_group": row.customer_group, "item_code": item.item_code, "item_name": item.item_name, "description": item.description, - "warehouse": product_bundle.warehouse, + "warehouse": item.warehouse or row.warehouse, + "update_stock": row.update_stock, "item_group": "", "brand": "", - "dn_detail": product_bundle.dn_detail, - "delivery_note": product_bundle.delivery_note, + "dn_detail": row.dn_detail, + "delivery_note": row.delivery_note, "qty": item.total_qty * -1, - "item_row": None, - "is_return": product_bundle.is_return, - "cost_center": product_bundle.cost_center, + "item_row": row.item_row, + "is_return": row.is_return, + "cost_center": row.cost_center, + "invoice": row.parent, } ) From 4a713f6b5e40472166718cc67b5f975b47bc9de3 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Tue, 3 Dec 2024 15:39:17 +0530 Subject: [PATCH 42/44] chore: fix typo (cherry picked from commit fc0122ce760f8c2f14de931bf8f082de8cefa02d) --- erpnext/accounts/report/gross_profit/gross_profit.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/erpnext/accounts/report/gross_profit/gross_profit.py b/erpnext/accounts/report/gross_profit/gross_profit.py index 1191f72cb3fa..c59a3bd2a7ae 100644 --- a/erpnext/accounts/report/gross_profit/gross_profit.py +++ b/erpnext/accounts/report/gross_profit/gross_profit.py @@ -915,7 +915,7 @@ def group_items_by_invoice(self): """ grouped = OrderedDict() - product_bundels = self.product_bundles.get("Sales Invoice", {}) + product_bundles = self.product_bundles.get("Sales Invoice", {}) for row in self.si_list: # initialize list with a header row for each new parent @@ -926,7 +926,7 @@ def group_items_by_invoice(self): ) # if item is a bundle, add it's components as seperate rows - if bundled_items := product_bundels.get(row.parent, {}).get(row.item_code): + if bundled_items := product_bundles.get(row.parent, {}).get(row.item_code): for x in bundled_items: bundle_item = self.get_bundle_item_row(row, x) grouped.get(row.parent).append(bundle_item) From 63de576be61ec2ec30c79cec055b4c8afa2dfd07 Mon Sep 17 00:00:00 2001 From: vishakhdesai Date: Tue, 26 Nov 2024 14:26:02 +0530 Subject: [PATCH 43/44] fix: move validate_total_debit_and_credit from validate to on_submit in Journal Entry (cherry picked from commit 8b5d4c023654157d58b40fce81ab6787d95bc734) --- erpnext/accounts/doctype/journal_entry/journal_entry.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/erpnext/accounts/doctype/journal_entry/journal_entry.py b/erpnext/accounts/doctype/journal_entry/journal_entry.py index aeaadae0b30b..34a4d14fbbf4 100644 --- a/erpnext/accounts/doctype/journal_entry/journal_entry.py +++ b/erpnext/accounts/doctype/journal_entry/journal_entry.py @@ -127,9 +127,6 @@ def validate(self): self.set_amounts_in_company_currency() self.validate_debit_credit_amount() self.set_total_debit_credit() - # Do not validate while importing via data import - if not frappe.flags.in_import: - self.validate_total_debit_and_credit() if not frappe.flags.is_reverse_depr_entry: self.validate_against_jv() @@ -185,6 +182,10 @@ def cancel(self): return self._cancel() def on_submit(self): + # Do not validate while importing via data import + if not frappe.flags.in_import: + self.validate_total_debit_and_credit() + self.validate_cheque_info() self.check_credit_limit() self.make_gl_entries() From 16d0d42afe2f1282c5291cfcb88495348910c015 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Tue, 3 Dec 2024 17:42:11 +0530 Subject: [PATCH 44/44] refactor: validate debit and credit on before_submit (cherry picked from commit c3ace82db83dcf2dbb2afabeda2a82aa37d9e5ab) --- erpnext/accounts/doctype/journal_entry/journal_entry.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/erpnext/accounts/doctype/journal_entry/journal_entry.py b/erpnext/accounts/doctype/journal_entry/journal_entry.py index 34a4d14fbbf4..ef2388a7eaab 100644 --- a/erpnext/accounts/doctype/journal_entry/journal_entry.py +++ b/erpnext/accounts/doctype/journal_entry/journal_entry.py @@ -181,11 +181,12 @@ def cancel(self): else: return self._cancel() - def on_submit(self): + def before_submit(self): # Do not validate while importing via data import if not frappe.flags.in_import: self.validate_total_debit_and_credit() + def on_submit(self): self.validate_cheque_info() self.check_credit_limit() self.make_gl_entries()
{d.batch_no}{d.serial_no}{abs(d.qty)}
{d.batch_no}{d.serial_no}{abs(d.qty)}
{d.batch_no}{abs(d.qty)}