From d3759b397160ec343fc3085db0a37552c78b28c0 Mon Sep 17 00:00:00 2001 From: Gursheen Anand Date: Sat, 15 Jul 2023 19:32:56 +0530 Subject: [PATCH 001/101] fix: make offsetting entry for acc dimensions --- .../accounting_dimension_detail.json | 22 +++++++++- .../doctype/journal_entry/journal_entry.py | 44 +++++++++++++++++++ 2 files changed, 65 insertions(+), 1 deletion(-) diff --git a/erpnext/accounts/doctype/accounting_dimension_detail/accounting_dimension_detail.json b/erpnext/accounts/doctype/accounting_dimension_detail/accounting_dimension_detail.json index e9e1f43f99..7b6120a583 100644 --- a/erpnext/accounts/doctype/accounting_dimension_detail/accounting_dimension_detail.json +++ b/erpnext/accounts/doctype/accounting_dimension_detail/accounting_dimension_detail.json @@ -8,7 +8,10 @@ "reference_document", "default_dimension", "mandatory_for_bs", - "mandatory_for_pl" + "mandatory_for_pl", + "column_break_lqns", + "automatically_post_balancing_accounting_entry", + "offsetting_account" ], "fields": [ { @@ -50,6 +53,23 @@ "fieldtype": "Check", "in_list_view": 1, "label": "Mandatory For Profit and Loss Account" + }, + { + "default": "0", + "fieldname": "automatically_post_balancing_accounting_entry", + "fieldtype": "Check", + "label": "Automatically post balancing accounting entry" + }, + { + "fieldname": "offsetting_account", + "fieldtype": "Link", + "label": "Offsetting Account", + "mandatory_depends_on": "eval: doc.automatically_post_balancing_accounting_entry", + "options": "Account" + }, + { + "fieldname": "column_break_lqns", + "fieldtype": "Column Break" } ], "istable": 1, diff --git a/erpnext/accounts/doctype/journal_entry/journal_entry.py b/erpnext/accounts/doctype/journal_entry/journal_entry.py index 83312dbd22..ed6122c57d 100644 --- a/erpnext/accounts/doctype/journal_entry/journal_entry.py +++ b/erpnext/accounts/doctype/journal_entry/journal_entry.py @@ -905,8 +905,52 @@ class JournalEntry(AccountsController): item=d, ) ) + + self.make_acc_dimensions_offsetting_entry(gl_map, d) + return gl_map + def make_acc_dimensions_offsetting_entry(self, gl_map, d): + accounting_dimensions = frappe.db.get_list("Accounting Dimension", {"disabled": 0}, pluck="name") + for dimension in accounting_dimensions: + dimension_details = frappe.db.get_values( + "Accounting Dimension Detail", + {"parent": dimension}, + ["automatically_post_balancing_accounting_entry", "offsetting_account"], + )[0] + if dimension_details[0] == 1: + offsetting_account = dimension_details[1] + gl_map.append( + self.get_gl_dict( + { + "account": offsetting_account, + "party_type": d.party_type, + "due_date": self.due_date, + "party": d.party, + "against": d.against_account, + "debit": flt(d.credit, d.precision("credit")), + "credit": flt(d.debit, d.precision("credit")), + "account_currency": d.account_currency, + "debit_in_account_currency": flt( + d.credit_in_account_currency, d.precision("credit_in_account_currency") + ), + "credit_in_account_currency": flt( + d.debit_in_account_currency, d.precision("debit_in_account_currency") + ), + "against_voucher_type": d.reference_type, + "against_voucher": d.reference_name, + "remarks": _( + "Offsetting for Accounting Dimension - {dimension}".format(dimension=dimension) + ), + "voucher_detail_no": d.reference_detail_no, + "cost_center": d.cost_center, + "project": d.project, + "finance_book": self.finance_book, + }, + item=d, + ) + ) + def make_gl_entries(self, cancel=0, adv_adj=0): from erpnext.accounts.general_ledger import make_gl_entries From 4e09de4db2d13fc251d36378cc9aaff775ff910c Mon Sep 17 00:00:00 2001 From: Gursheen Anand Date: Mon, 17 Jul 2023 11:47:33 +0530 Subject: [PATCH 002/101] fix: fetch accounting dimension details specific to company --- .../accounts/doctype/journal_entry/journal_entry.py | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/erpnext/accounts/doctype/journal_entry/journal_entry.py b/erpnext/accounts/doctype/journal_entry/journal_entry.py index ed6122c57d..334e8b002c 100644 --- a/erpnext/accounts/doctype/journal_entry/journal_entry.py +++ b/erpnext/accounts/doctype/journal_entry/journal_entry.py @@ -915,10 +915,11 @@ class JournalEntry(AccountsController): for dimension in accounting_dimensions: dimension_details = frappe.db.get_values( "Accounting Dimension Detail", - {"parent": dimension}, + {"parent": dimension, "company": self.company}, ["automatically_post_balancing_accounting_entry", "offsetting_account"], - )[0] - if dimension_details[0] == 1: + ) + dimension_details = dimension_details[0] if len(dimension_details) > 0 else None + if dimension_details and dimension_details[0] == 1: offsetting_account = dimension_details[1] gl_map.append( self.get_gl_dict( @@ -939,9 +940,7 @@ class JournalEntry(AccountsController): ), "against_voucher_type": d.reference_type, "against_voucher": d.reference_name, - "remarks": _( - "Offsetting for Accounting Dimension - {dimension}".format(dimension=dimension) - ), + "remarks": _("Offsetting for Accounting Dimension") + " - {0}".format(dimension), "voucher_detail_no": d.reference_detail_no, "cost_center": d.cost_center, "project": d.project, From 22ba12172fc1143efd778aee4cd296cf6970d736 Mon Sep 17 00:00:00 2001 From: Gursheen Anand Date: Mon, 17 Jul 2023 15:17:53 +0530 Subject: [PATCH 003/101] fix: make offsetting entry for all doctypes --- .../doctype/journal_entry/journal_entry.py | 43 ------------------ erpnext/accounts/general_ledger.py | 45 +++++++++++++++++++ 2 files changed, 45 insertions(+), 43 deletions(-) diff --git a/erpnext/accounts/doctype/journal_entry/journal_entry.py b/erpnext/accounts/doctype/journal_entry/journal_entry.py index 334e8b002c..83312dbd22 100644 --- a/erpnext/accounts/doctype/journal_entry/journal_entry.py +++ b/erpnext/accounts/doctype/journal_entry/journal_entry.py @@ -905,51 +905,8 @@ class JournalEntry(AccountsController): item=d, ) ) - - self.make_acc_dimensions_offsetting_entry(gl_map, d) - return gl_map - def make_acc_dimensions_offsetting_entry(self, gl_map, d): - accounting_dimensions = frappe.db.get_list("Accounting Dimension", {"disabled": 0}, pluck="name") - for dimension in accounting_dimensions: - dimension_details = frappe.db.get_values( - "Accounting Dimension Detail", - {"parent": dimension, "company": self.company}, - ["automatically_post_balancing_accounting_entry", "offsetting_account"], - ) - dimension_details = dimension_details[0] if len(dimension_details) > 0 else None - if dimension_details and dimension_details[0] == 1: - offsetting_account = dimension_details[1] - gl_map.append( - self.get_gl_dict( - { - "account": offsetting_account, - "party_type": d.party_type, - "due_date": self.due_date, - "party": d.party, - "against": d.against_account, - "debit": flt(d.credit, d.precision("credit")), - "credit": flt(d.debit, d.precision("credit")), - "account_currency": d.account_currency, - "debit_in_account_currency": flt( - d.credit_in_account_currency, d.precision("credit_in_account_currency") - ), - "credit_in_account_currency": flt( - d.debit_in_account_currency, d.precision("debit_in_account_currency") - ), - "against_voucher_type": d.reference_type, - "against_voucher": d.reference_name, - "remarks": _("Offsetting for Accounting Dimension") + " - {0}".format(dimension), - "voucher_detail_no": d.reference_detail_no, - "cost_center": d.cost_center, - "project": d.project, - "finance_book": self.finance_book, - }, - item=d, - ) - ) - def make_gl_entries(self, cancel=0, adv_adj=0): from erpnext.accounts.general_ledger import make_gl_entries diff --git a/erpnext/accounts/general_ledger.py b/erpnext/accounts/general_ledger.py index f1dad875fa..1299c44307 100644 --- a/erpnext/accounts/general_ledger.py +++ b/erpnext/accounts/general_ledger.py @@ -30,6 +30,7 @@ def make_gl_entries( from_repost=False, ): if gl_map: + make_acc_dimensions_offsetting_entry(gl_map) if not cancel: validate_accounting_period(gl_map) validate_disabled_accounts(gl_map) @@ -54,6 +55,50 @@ def make_gl_entries( make_reverse_gl_entries(gl_map, adv_adj=adv_adj, update_outstanding=update_outstanding) +def make_acc_dimensions_offsetting_entry(gl_map): + accounting_dimensions_to_offset = get_accounting_dimensions_for_offsetting_entry(gl_map) + if len(accounting_dimensions_to_offset) == 0: + return + + offsetting_entries = [] + for gle in gl_map: + for dimension in accounting_dimensions_to_offset: + dimension_details = frappe.db.get_values( + "Accounting Dimension Detail", + {"parent": dimension, "company": gle.company}, + ["automatically_post_balancing_accounting_entry", "offsetting_account"], + ) + dimension_details = dimension_details[0] if len(dimension_details) > 0 else None + if dimension_details and dimension_details[0] == 1: + offsetting_account = dimension_details[1] + offsetting_entry = gle.copy() + offsetting_entry.update( + { + "account": offsetting_account, + "debit": flt(gle.credit), + "credit": flt(gle.debit), + "debit_in_account_currency": flt(gle.credit_in_account_currency), + "credit_in_account_currency": flt(gle.debit_in_account_currency), + "remarks": _("Offsetting for Accounting Dimension") + " - {0}".format(dimension), + "against_voucher": None, + } + ) + offsetting_entry["against_voucher_type"] = None + offsetting_entries.append(offsetting_entry) + gl_map += offsetting_entries + + +def get_accounting_dimensions_for_offsetting_entry(gl_map): + acc_dimensions = frappe.db.get_list("Accounting Dimension", {"disabled": 0}, pluck="name") + accounting_dimensions_to_offset = [] + for acc_dimension in acc_dimensions: + fieldname = acc_dimension.lower().replace(" ", "_") + values = set([entry[fieldname] for entry in gl_map]) + if len(values) > 1: + accounting_dimensions_to_offset.append(acc_dimension) + return accounting_dimensions_to_offset + + def validate_disabled_accounts(gl_map): accounts = [d.account for d in gl_map if d.account] From 4004427892aedf332e2b5ae3c47c4debcdf6ae05 Mon Sep 17 00:00:00 2001 From: Gursheen Anand Date: Mon, 17 Jul 2023 17:57:03 +0530 Subject: [PATCH 004/101] test: TB report balanced whenfiltered using acc dimension --- .../trial_balance/test_trial_balance.py | 111 ++++++++++++++++++ 1 file changed, 111 insertions(+) create mode 100644 erpnext/accounts/report/trial_balance/test_trial_balance.py diff --git a/erpnext/accounts/report/trial_balance/test_trial_balance.py b/erpnext/accounts/report/trial_balance/test_trial_balance.py new file mode 100644 index 0000000000..d4d055464f --- /dev/null +++ b/erpnext/accounts/report/trial_balance/test_trial_balance.py @@ -0,0 +1,111 @@ +# Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and Contributors +# MIT License. See license.txt + +import frappe +from frappe.tests.utils import FrappeTestCase +from frappe.utils import today + +from erpnext.accounts.report.trial_balance.trial_balance import execute + + +class TestTrialBalance(FrappeTestCase): + def setUp(self): + from erpnext.accounts.doctype.account.test_account import create_account + from erpnext.accounts.doctype.cost_center.test_cost_center import create_cost_center + from erpnext.accounts.utils import get_fiscal_year + + self.company = create_company() + create_cost_center( + cost_center_name="Test Cost Center", + company="Trial Balance Company", + parent_cost_center="Trial Balance Company - TBC", + ) + create_account( + account_name="Offsetting", + company="Trial Balance Company", + parent_account="Temporary Accounts - TBC", + ) + self.fiscal_year = get_fiscal_year(today(), company="Trial Balance Company")[0] + create_accounting_dimension() + + def test_offsetting_entries_for_accounting_dimensions(self): + """ + Checks if Trial Balance Report is balanced when filtered using a particular Accounting Dimension + """ + from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice + + frappe.db.sql("delete from `tabSales Invoice` where company='Trial Balance Company'") + frappe.db.sql("delete from `tabGL Entry` where company='Trial Balance Company'") + + branch1 = frappe.new_doc("Branch") + branch1.branch = "Location 1" + branch1.insert() + branch2 = frappe.new_doc("Branch") + branch2.branch = "Location 2" + branch2.insert() + + si = create_sales_invoice( + company=self.company, + debit_to="Debtors - TBC", + cost_center="Test Cost Center - TBC", + income_account="Sales - TBC", + do_not_submit=1, + ) + si.branch = "Location 1" + si.items[0].branch = "Location 2" + si.save() + si.submit() + + filters = frappe._dict( + {"company": self.company, "fiscal_year": self.fiscal_year, "branch": ["Location 1"]} + ) + total_row = execute(filters)[1][-1] + self.assertEqual(total_row["debit"], total_row["credit"]) + + def tearDown(self): + disable_dimension() + + +def create_company(**args): + args = frappe._dict(args) + company = frappe.get_doc( + { + "doctype": "Company", + "company_name": args.company_name or "Trial Balance Company", + "country": args.country or "India", + "default_currency": args.currency or "INR", + } + ) + company.insert(ignore_if_duplicate=True) + return company.name + + +def create_accounting_dimension(**args): + args = frappe._dict(args) + document_type = args.document_type or "Branch" + if not frappe.db.exists("Accounting Dimension", {"document_type": "Branch"}): + accounting_dimension = frappe.get_doc( + {"doctype": "Accounting Dimension", "document_type": document_type} + ).insert() + else: + accounting_dimension = frappe.get_doc("Accounting Dimension", document_type) + accounting_dimension.disabled = 0 + + accounting_dimension.append( + "dimension_defaults", + { + "company": args.company or "Trial Balance Company", + "automatically_post_balancing_accounting_entry": 1, + "offsetting_account": args.offsetting_account or "Offsetting - TBC", + }, + ) + accounting_dimension.save() + return accounting_dimension.name + + +def disable_dimension(**args): + args = frappe._dict(args) + document_type = args.document_type or "Branch" + dimension = frappe.get_doc("Accounting Dimension", document_type) + dimension.disabled = 1 + dimension.save() From ed3bef1840ac7482f673a69f01dfdb7608037955 Mon Sep 17 00:00:00 2001 From: Gursheen Anand Date: Mon, 17 Jul 2023 18:40:52 +0530 Subject: [PATCH 005/101] fix: dict value for dimension for gl entries defined without the dimension --- erpnext/accounts/general_ledger.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/accounts/general_ledger.py b/erpnext/accounts/general_ledger.py index 1299c44307..e4f30160df 100644 --- a/erpnext/accounts/general_ledger.py +++ b/erpnext/accounts/general_ledger.py @@ -93,7 +93,7 @@ def get_accounting_dimensions_for_offsetting_entry(gl_map): accounting_dimensions_to_offset = [] for acc_dimension in acc_dimensions: fieldname = acc_dimension.lower().replace(" ", "_") - values = set([entry[fieldname] for entry in gl_map]) + values = set([entry.get(fieldname) for entry in gl_map]) if len(values) > 1: accounting_dimensions_to_offset.append(acc_dimension) return accounting_dimensions_to_offset From ecaf0aba3c8ccda3a5fa2771e1dd467b9f3b712e Mon Sep 17 00:00:00 2001 From: barredterra <14891507+barredterra@users.noreply.github.com> Date: Mon, 17 Jul 2023 16:45:11 +0200 Subject: [PATCH 006/101] fix: rounding of percentage fields Always round with precision of 2 --- .../doctype/purchase_order/purchase_order.js | 14 +++++++------- erpnext/controllers/website_list_for_contact.py | 6 ++++-- erpnext/patches/v12_0/update_bom_in_so_mr.py | 4 +++- erpnext/selling/doctype/sales_order/sales_order.js | 14 +++++++------- .../doctype/sales_order/sales_order_list.js | 10 +++++----- erpnext/templates/form_grid/item_grid.html | 2 +- 6 files changed, 27 insertions(+), 23 deletions(-) diff --git a/erpnext/buying/doctype/purchase_order/purchase_order.js b/erpnext/buying/doctype/purchase_order/purchase_order.js index 8fa8f30554..147b447e80 100644 --- a/erpnext/buying/doctype/purchase_order/purchase_order.js +++ b/erpnext/buying/doctype/purchase_order/purchase_order.js @@ -62,7 +62,7 @@ frappe.ui.form.on("Purchase Order", { get_materials_from_supplier: function(frm) { let po_details = []; - if (frm.doc.supplied_items && (frm.doc.per_received == 100 || frm.doc.status === 'Closed')) { + if (frm.doc.supplied_items && (flt(frm.doc.per_received, 2) == 100 || frm.doc.status === 'Closed')) { frm.doc.supplied_items.forEach(d => { if (d.total_supplied_qty && d.total_supplied_qty != d.consumed_qty) { po_details.push(d.name) @@ -181,7 +181,7 @@ erpnext.buying.PurchaseOrderController = class PurchaseOrderController extends e } if(!in_list(["Closed", "Delivered"], doc.status)) { - if(this.frm.doc.status !== 'Closed' && flt(this.frm.doc.per_received) < 100 && flt(this.frm.doc.per_billed) < 100) { + if(this.frm.doc.status !== 'Closed' && flt(this.frm.doc.per_received, 2) < 100 && flt(this.frm.doc.per_billed, 2) < 100) { // Don't add Update Items button if the PO is following the new subcontracting flow. if (!(this.frm.doc.is_subcontracted && !this.frm.doc.is_old_subcontracting_flow)) { this.frm.add_custom_button(__('Update Items'), () => { @@ -195,7 +195,7 @@ erpnext.buying.PurchaseOrderController = class PurchaseOrderController extends e } } if (this.frm.has_perm("submit")) { - if(flt(doc.per_billed, 6) < 100 || flt(doc.per_received, 6) < 100) { + if(flt(doc.per_billed, 2) < 100 || flt(doc.per_received, 2) < 100) { if (doc.status != "On Hold") { this.frm.add_custom_button(__('Hold'), () => this.hold_purchase_order(), __("Status")); } else{ @@ -218,7 +218,7 @@ erpnext.buying.PurchaseOrderController = class PurchaseOrderController extends e } if(doc.status != "Closed") { if (doc.status != "On Hold") { - if(flt(doc.per_received) < 100 && allow_receipt) { + if(flt(doc.per_received, 2) < 100 && allow_receipt) { cur_frm.add_custom_button(__('Purchase Receipt'), this.make_purchase_receipt, __('Create')); if (doc.is_subcontracted) { if (doc.is_old_subcontracting_flow) { @@ -231,11 +231,11 @@ erpnext.buying.PurchaseOrderController = class PurchaseOrderController extends e } } } - if(flt(doc.per_billed) < 100) + if(flt(doc.per_billed, 2) < 100) cur_frm.add_custom_button(__('Purchase Invoice'), this.make_purchase_invoice, __('Create')); - if(flt(doc.per_billed) < 100 && doc.status != "Delivered") { + if(flt(doc.per_billed, 2) < 100 && doc.status != "Delivered") { this.frm.add_custom_button( __('Payment'), () => this.make_payment_entry(), @@ -243,7 +243,7 @@ erpnext.buying.PurchaseOrderController = class PurchaseOrderController extends e ); } - if(flt(doc.per_billed) < 100) { + if(flt(doc.per_billed, 2) < 100) { this.frm.add_custom_button(__('Payment Request'), function() { me.make_payment_request() }, __('Create')); } diff --git a/erpnext/controllers/website_list_for_contact.py b/erpnext/controllers/website_list_for_contact.py index 642722ae6b..01b6f5ceba 100644 --- a/erpnext/controllers/website_list_for_contact.py +++ b/erpnext/controllers/website_list_for_contact.py @@ -206,9 +206,11 @@ def post_process(doctype, data): ) if doc.get("per_delivered"): - doc.status_percent += flt(doc.per_delivered) + doc.status_percent += flt(doc.per_delivered, 2) doc.status_display.append( - _("Delivered") if doc.per_delivered == 100 else _("{0}% Delivered").format(doc.per_delivered) + _("Delivered") + if flt(doc.per_delivered, 2) == 100 + else _("{0}% Delivered").format(doc.per_delivered) ) if hasattr(doc, "set_indicator"): diff --git a/erpnext/patches/v12_0/update_bom_in_so_mr.py b/erpnext/patches/v12_0/update_bom_in_so_mr.py index 114f65d100..d35b4bcdd6 100644 --- a/erpnext/patches/v12_0/update_bom_in_so_mr.py +++ b/erpnext/patches/v12_0/update_bom_in_so_mr.py @@ -6,7 +6,9 @@ def execute(): frappe.reload_doc("selling", "doctype", "sales_order_item") for doctype in ["Sales Order", "Material Request"]: - condition = " and child_doc.stock_qty > child_doc.produced_qty and doc.per_delivered < 100" + condition = ( + " and child_doc.stock_qty > child_doc.produced_qty and ROUND(doc.per_delivered, 2) < 100" + ) if doctype == "Material Request": condition = " and doc.per_ordered < 100 and doc.material_request_type = 'Manufacture'" diff --git a/erpnext/selling/doctype/sales_order/sales_order.js b/erpnext/selling/doctype/sales_order/sales_order.js index 5d43a07d96..f1a68af8d9 100644 --- a/erpnext/selling/doctype/sales_order/sales_order.js +++ b/erpnext/selling/doctype/sales_order/sales_order.js @@ -50,7 +50,7 @@ frappe.ui.form.on("Sales Order", { refresh: function(frm) { if(frm.doc.docstatus === 1) { - if (frm.doc.status !== 'Closed' && flt(frm.doc.per_delivered, 6) < 100 && flt(frm.doc.per_billed, 6) < 100) { + if (frm.doc.status !== 'Closed' && flt(frm.doc.per_delivered, 2) < 100 && flt(frm.doc.per_billed, 2) < 100) { frm.add_custom_button(__('Update Items'), () => { erpnext.utils.update_child_items({ frm: frm, @@ -307,7 +307,7 @@ erpnext.selling.SalesOrderController = class SalesOrderController extends erpnex me.frm.cscript.update_status('Resume', 'Draft') }, __("Status")); - if(flt(doc.per_delivered, 6) < 100 || flt(doc.per_billed) < 100) { + if(flt(doc.per_delivered, 2) < 100 || flt(doc.per_billed, 2) < 100) { // close this.frm.add_custom_button(__('Close'), () => this.close_sales_order(), __("Status")) } @@ -325,7 +325,7 @@ erpnext.selling.SalesOrderController = class SalesOrderController extends erpnex && !this.frm.doc.skip_delivery_note if (this.frm.has_perm("submit")) { - if(flt(doc.per_delivered, 6) < 100 || flt(doc.per_billed) < 100) { + if(flt(doc.per_delivered, 2) < 100 || flt(doc.per_billed, 2) < 100) { // hold this.frm.add_custom_button(__('Hold'), () => this.hold_sales_order(), __("Status")) // close @@ -333,7 +333,7 @@ erpnext.selling.SalesOrderController = class SalesOrderController extends erpnex } } - if (flt(doc.per_picked, 6) < 100 && flt(doc.per_delivered, 6) < 100) { + if (flt(doc.per_picked, 2) < 100 && flt(doc.per_delivered, 2) < 100) { this.frm.add_custom_button(__('Pick List'), () => this.create_pick_list(), __('Create')); } @@ -343,18 +343,18 @@ erpnext.selling.SalesOrderController = class SalesOrderController extends erpnex const order_is_a_custom_sale = ["Sales", "Shopping Cart", "Maintenance"].indexOf(doc.order_type) === -1; // delivery note - if(flt(doc.per_delivered, 6) < 100 && (order_is_a_sale || order_is_a_custom_sale) && allow_delivery) { + if(flt(doc.per_delivered, 2) < 100 && (order_is_a_sale || order_is_a_custom_sale) && allow_delivery) { this.frm.add_custom_button(__('Delivery Note'), () => this.make_delivery_note_based_on_delivery_date(), __('Create')); this.frm.add_custom_button(__('Work Order'), () => this.make_work_order(), __('Create')); } // sales invoice - if(flt(doc.per_billed, 6) < 100) { + if(flt(doc.per_billed, 2) < 100) { this.frm.add_custom_button(__('Sales Invoice'), () => me.make_sales_invoice(), __('Create')); } // material request - if(!doc.order_type || (order_is_a_sale || order_is_a_custom_sale) && flt(doc.per_delivered, 6) < 100) { + if(!doc.order_type || (order_is_a_sale || order_is_a_custom_sale) && flt(doc.per_delivered, 2) < 100) { this.frm.add_custom_button(__('Material Request'), () => this.make_material_request(), __('Create')); this.frm.add_custom_button(__('Request for Raw Materials'), () => this.make_raw_material_request(), __('Create')); } diff --git a/erpnext/selling/doctype/sales_order/sales_order_list.js b/erpnext/selling/doctype/sales_order/sales_order_list.js index 64c58ef5d7..518f018726 100644 --- a/erpnext/selling/doctype/sales_order/sales_order_list.js +++ b/erpnext/selling/doctype/sales_order/sales_order_list.js @@ -10,7 +10,7 @@ frappe.listview_settings['Sales Order'] = { return [__("On Hold"), "orange", "status,=,On Hold"]; } else if (doc.status === "Completed") { return [__("Completed"), "green", "status,=,Completed"]; - } else if (!doc.skip_delivery_note && flt(doc.per_delivered, 6) < 100) { + } else if (!doc.skip_delivery_note && flt(doc.per_delivered, 2) < 100) { if (frappe.datetime.get_diff(doc.delivery_date) < 0) { // not delivered & overdue return [__("Overdue"), "red", @@ -19,7 +19,7 @@ frappe.listview_settings['Sales Order'] = { // not delivered (zeroount order) return [__("To Deliver"), "orange", "per_delivered,<,100|grand_total,=,0|status,!=,Closed"]; - } else if (flt(doc.per_billed, 6) < 100) { + } else if (flt(doc.per_billed, 2) < 100) { // not delivered & not billed return [__("To Deliver and Bill"), "orange", "per_delivered,<,100|per_billed,<,100|status,!=,Closed"]; @@ -28,12 +28,12 @@ frappe.listview_settings['Sales Order'] = { return [__("To Deliver"), "orange", "per_delivered,<,100|per_billed,=,100|status,!=,Closed"]; } - } else if ((flt(doc.per_delivered, 6) === 100) && flt(doc.grand_total) !== 0 - && flt(doc.per_billed, 6) < 100) { + } else if ((flt(doc.per_delivered, 2) === 100) && flt(doc.grand_total) !== 0 + && flt(doc.per_billed, 2) < 100) { // to bill return [__("To Bill"), "orange", "per_delivered,=,100|per_billed,<,100|status,!=,Closed"]; - } else if (doc.skip_delivery_note && flt(doc.per_billed, 6) < 100){ + } else if (doc.skip_delivery_note && flt(doc.per_billed, 2) < 100){ return [__("To Bill"), "orange", "per_billed,<,100|status,!=,Closed"]; } }, diff --git a/erpnext/templates/form_grid/item_grid.html b/erpnext/templates/form_grid/item_grid.html index c596890aa3..027046fd92 100644 --- a/erpnext/templates/form_grid/item_grid.html +++ b/erpnext/templates/form_grid/item_grid.html @@ -17,7 +17,7 @@ title = "Warehouse", actual_qty = (frm.doc.doctype==="Sales Order" ? doc.projected_qty : doc.actual_qty); - if(flt(frm.doc.per_delivered) < 100 + if(flt(frm.doc.per_delivered, 2) < 100 && in_list(["Sales Order Item", "Delivery Note Item"], doc.doctype)) { if(actual_qty != undefined) { if(actual_qty >= doc.qty) { From 1e1e4b93c17e7c30f14a59237ffa54cfe093594c Mon Sep 17 00:00:00 2001 From: Gursheen Anand Date: Tue, 18 Jul 2023 12:12:24 +0530 Subject: [PATCH 007/101] fix: divide offsetting amount for multiple dimensions --- erpnext/accounts/general_ledger.py | 11 ++++++----- .../accounts/report/trial_balance/trial_balance.py | 2 +- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/erpnext/accounts/general_ledger.py b/erpnext/accounts/general_ledger.py index e4f30160df..a26812c7a6 100644 --- a/erpnext/accounts/general_ledger.py +++ b/erpnext/accounts/general_ledger.py @@ -57,7 +57,8 @@ def make_gl_entries( def make_acc_dimensions_offsetting_entry(gl_map): accounting_dimensions_to_offset = get_accounting_dimensions_for_offsetting_entry(gl_map) - if len(accounting_dimensions_to_offset) == 0: + no_of_dimensions = len(accounting_dimensions_to_offset) + if no_of_dimensions == 0: return offsetting_entries = [] @@ -75,10 +76,10 @@ def make_acc_dimensions_offsetting_entry(gl_map): offsetting_entry.update( { "account": offsetting_account, - "debit": flt(gle.credit), - "credit": flt(gle.debit), - "debit_in_account_currency": flt(gle.credit_in_account_currency), - "credit_in_account_currency": flt(gle.debit_in_account_currency), + "debit": flt(gle.credit) / no_of_dimensions if gle.credit != 0 else 0, + "credit": flt(gle.debit) / no_of_dimensions if gle.debit != 0 else 0, + "debit_in_account_currency": flt(gle.credit) / no_of_dimensions if gle.credit != 0 else 0, + "credit_in_account_currency": flt(gle.debit) / no_of_dimensions if gle.debit != 0 else 0, "remarks": _("Offsetting for Accounting Dimension") + " - {0}".format(dimension), "against_voucher": None, } diff --git a/erpnext/accounts/report/trial_balance/trial_balance.py b/erpnext/accounts/report/trial_balance/trial_balance.py index d51c4c4acb..5a9b51b080 100644 --- a/erpnext/accounts/report/trial_balance/trial_balance.py +++ b/erpnext/accounts/report/trial_balance/trial_balance.py @@ -241,7 +241,7 @@ def get_opening_balance( lft, rgt = frappe.db.get_value("Cost Center", filters.cost_center, ["lft", "rgt"]) cost_center = frappe.qb.DocType("Cost Center") opening_balance = opening_balance.where( - closing_balance.cost_center.in_( + closing_balance.cost_center.isin( frappe.qb.from_(cost_center) .select("name") .where((cost_center.lft >= lft) & (cost_center.rgt <= rgt)) From 3a3ffa23076f368c4c24fbec1777205a12c76146 Mon Sep 17 00:00:00 2001 From: Gursheen Anand Date: Tue, 18 Jul 2023 12:51:09 +0530 Subject: [PATCH 008/101] fix: divide offsetting amount only when account exists --- erpnext/accounts/general_ledger.py | 57 +++++++++++++++++------------- 1 file changed, 32 insertions(+), 25 deletions(-) diff --git a/erpnext/accounts/general_ledger.py b/erpnext/accounts/general_ledger.py index a26812c7a6..994c7c30aa 100644 --- a/erpnext/accounts/general_ledger.py +++ b/erpnext/accounts/general_ledger.py @@ -56,7 +56,9 @@ def make_gl_entries( def make_acc_dimensions_offsetting_entry(gl_map): - accounting_dimensions_to_offset = get_accounting_dimensions_for_offsetting_entry(gl_map) + accounting_dimensions_to_offset = get_accounting_dimensions_for_offsetting_entry( + gl_map, gl_map[0].company + ) no_of_dimensions = len(accounting_dimensions_to_offset) if no_of_dimensions == 0: return @@ -64,36 +66,41 @@ def make_acc_dimensions_offsetting_entry(gl_map): offsetting_entries = [] for gle in gl_map: for dimension in accounting_dimensions_to_offset: - dimension_details = frappe.db.get_values( - "Accounting Dimension Detail", - {"parent": dimension, "company": gle.company}, - ["automatically_post_balancing_accounting_entry", "offsetting_account"], + offsetting_account = dimension.offsetting_account + offsetting_entry = gle.copy() + offsetting_entry.update( + { + "account": offsetting_account, + "debit": flt(gle.credit) / no_of_dimensions if gle.credit != 0 else 0, + "credit": flt(gle.debit) / no_of_dimensions if gle.debit != 0 else 0, + "debit_in_account_currency": flt(gle.credit) / no_of_dimensions if gle.credit != 0 else 0, + "credit_in_account_currency": flt(gle.debit) / no_of_dimensions if gle.debit != 0 else 0, + "remarks": _("Offsetting for Accounting Dimension") + " - {0}".format(dimension.name), + "against_voucher": None, + } ) - dimension_details = dimension_details[0] if len(dimension_details) > 0 else None - if dimension_details and dimension_details[0] == 1: - offsetting_account = dimension_details[1] - offsetting_entry = gle.copy() - offsetting_entry.update( - { - "account": offsetting_account, - "debit": flt(gle.credit) / no_of_dimensions if gle.credit != 0 else 0, - "credit": flt(gle.debit) / no_of_dimensions if gle.debit != 0 else 0, - "debit_in_account_currency": flt(gle.credit) / no_of_dimensions if gle.credit != 0 else 0, - "credit_in_account_currency": flt(gle.debit) / no_of_dimensions if gle.debit != 0 else 0, - "remarks": _("Offsetting for Accounting Dimension") + " - {0}".format(dimension), - "against_voucher": None, - } - ) - offsetting_entry["against_voucher_type"] = None - offsetting_entries.append(offsetting_entry) + offsetting_entry["against_voucher_type"] = None + offsetting_entries.append(offsetting_entry) gl_map += offsetting_entries -def get_accounting_dimensions_for_offsetting_entry(gl_map): - acc_dimensions = frappe.db.get_list("Accounting Dimension", {"disabled": 0}, pluck="name") +def get_accounting_dimensions_for_offsetting_entry(gl_map, company): + acc_dimension = frappe.qb.DocType("Accounting Dimension") + dimension_detail = frappe.qb.DocType("Accounting Dimension Detail") + acc_dimensions = ( + frappe.qb.from_(acc_dimension) + .inner_join(dimension_detail) + .on(acc_dimension.name == dimension_detail.parent) + .select(acc_dimension.name, dimension_detail.offsetting_account) + .where( + (acc_dimension.disabled == 0) + & (dimension_detail.company == company) + & (dimension_detail.automatically_post_balancing_accounting_entry == 1) + ) + ).run(as_dict=True) accounting_dimensions_to_offset = [] for acc_dimension in acc_dimensions: - fieldname = acc_dimension.lower().replace(" ", "_") + fieldname = acc_dimension.name.lower().replace(" ", "_") values = set([entry.get(fieldname) for entry in gl_map]) if len(values) > 1: accounting_dimensions_to_offset.append(acc_dimension) From 77deac4fb904a6a727afbd0eb174c2abbaed8834 Mon Sep 17 00:00:00 2001 From: Gursheen Anand Date: Tue, 18 Jul 2023 15:51:01 +0530 Subject: [PATCH 009/101] test: PI offsetting entry for accounting dimension --- .../purchase_invoice/test_purchase_invoice.py | 85 ++++++++++++++++++- 1 file changed, 82 insertions(+), 3 deletions(-) diff --git a/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py b/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py index 8c96480478..4aaed4ccec 100644 --- a/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py +++ b/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py @@ -1736,6 +1736,72 @@ class TestPurchaseInvoice(unittest.TestCase, StockTestMixin): rate = flt(sle.stock_value_difference) / flt(sle.actual_qty) self.assertAlmostEqual(returned_inv.items[0].rate, rate) + def test_offsetting_entries_for_accounting_dimensions(self): + from erpnext.accounts.doctype.account.test_account import create_account + + create_account( + account_name="Offsetting", + company="_Test Company", + parent_account="Temporary Accounts - _TC", + ) + + clear_dimension_defaults("Branch") + accounting_dimension = frappe.get_doc("Accounting Dimension", "Branch") + accounting_dimension.disabled = 0 + accounting_dimension.append( + "dimension_defaults", + { + "company": "_Test Company", + "automatically_post_balancing_accounting_entry": 1, + "offsetting_account": "Offsetting - _TC", + }, + ) + accounting_dimension.save() + + branch1 = frappe.new_doc("Branch") + branch1.branch = "Location 1" + branch1.insert(ignore_if_duplicate=True) + branch2 = frappe.new_doc("Branch") + branch2.branch = "Location 2" + branch2.insert(ignore_if_duplicate=True) + + pi = make_purchase_invoice( + company="_Test Company", + customer="_Test Supplier", + do_not_save=True, + do_not_submit=True, + rate=1000, + price_list_rate=1000, + qty=1, + ) + pi.branch = branch1.branch + pi.items[0].branch = branch2.branch + pi.save() + pi.submit() + + expected_gle = [ + ["_Test Account Cost for Goods Sold - _TC", 1000, 0.0, nowdate(), {"branch": branch2.branch}], + ["Creditors - _TC", 0.0, 1000, nowdate(), {"branch": branch1.branch}], + ["Offsetting - _TC", 1000, 0.0, nowdate(), {"branch": branch1.branch}], + ["Offsetting - _TC", 0.0, 1000, nowdate(), {"branch": branch2.branch}], + ] + + check_gl_entries( + self, + pi.name, + expected_gle, + nowdate(), + voucher_type="Purchase Invoice", + check_acc_dimensions=True, + ) + clear_dimension_defaults("Branch") + + +def clear_dimension_defaults(dimension_name): + accounting_dimension = frappe.get_doc("Accounting Dimension", dimension_name) + accounting_dimension.dimension_defaults = [] + accounting_dimension.save() + def set_advance_flag(company, flag, default_account): frappe.db.set_value( @@ -1748,9 +1814,16 @@ def set_advance_flag(company, flag, default_account): ) -def check_gl_entries(doc, voucher_no, expected_gle, posting_date, voucher_type="Purchase Invoice"): +def check_gl_entries( + doc, + voucher_no, + expected_gle, + posting_date, + voucher_type="Purchase Invoice", + check_acc_dimensions=False, +): gl = frappe.qb.DocType("GL Entry") - q = ( + query = ( frappe.qb.from_(gl) .select(gl.account, gl.debit, gl.credit, gl.posting_date) .where( @@ -1761,13 +1834,19 @@ def check_gl_entries(doc, voucher_no, expected_gle, posting_date, voucher_type=" ) .orderby(gl.posting_date, gl.account, gl.creation) ) - gl_entries = q.run(as_dict=True) + if check_acc_dimensions: + for col in list(expected_gle[0][4].keys()): + query = query.select(col) + gl_entries = query.run(as_dict=True) for i, gle in enumerate(gl_entries): doc.assertEqual(expected_gle[i][0], gle.account) doc.assertEqual(expected_gle[i][1], gle.debit) doc.assertEqual(expected_gle[i][2], gle.credit) doc.assertEqual(getdate(expected_gle[i][3]), gle.posting_date) + if check_acc_dimensions: + for acc_dimension in expected_gle[i][4]: + doc.assertEqual(expected_gle[i][4][acc_dimension], gle[acc_dimension]) def create_tax_witholding_category(category_name, company, account): From b3f6d991b5fd8347311ef05506237f409c1b327b Mon Sep 17 00:00:00 2001 From: Gursheen Anand Date: Wed, 19 Jul 2023 12:02:26 +0530 Subject: [PATCH 010/101] fix: duplicate acc dimension in test --- .../purchase_invoice/test_purchase_invoice.py | 23 ++++--------------- .../trial_balance/test_trial_balance.py | 12 ++++++++-- 2 files changed, 15 insertions(+), 20 deletions(-) diff --git a/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py b/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py index 4aaed4ccec..846b8bd47c 100644 --- a/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py +++ b/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py @@ -1738,6 +1738,10 @@ class TestPurchaseInvoice(unittest.TestCase, StockTestMixin): def test_offsetting_entries_for_accounting_dimensions(self): from erpnext.accounts.doctype.account.test_account import create_account + from erpnext.accounts.report.trial_balance.test_trial_balance import ( + clear_dimension_defaults, + create_accounting_dimension, + ) create_account( account_name="Offsetting", @@ -1745,18 +1749,7 @@ class TestPurchaseInvoice(unittest.TestCase, StockTestMixin): parent_account="Temporary Accounts - _TC", ) - clear_dimension_defaults("Branch") - accounting_dimension = frappe.get_doc("Accounting Dimension", "Branch") - accounting_dimension.disabled = 0 - accounting_dimension.append( - "dimension_defaults", - { - "company": "_Test Company", - "automatically_post_balancing_accounting_entry": 1, - "offsetting_account": "Offsetting - _TC", - }, - ) - accounting_dimension.save() + create_accounting_dimension() branch1 = frappe.new_doc("Branch") branch1.branch = "Location 1" @@ -1797,12 +1790,6 @@ class TestPurchaseInvoice(unittest.TestCase, StockTestMixin): clear_dimension_defaults("Branch") -def clear_dimension_defaults(dimension_name): - accounting_dimension = frappe.get_doc("Accounting Dimension", dimension_name) - accounting_dimension.dimension_defaults = [] - accounting_dimension.save() - - def set_advance_flag(company, flag, default_account): frappe.db.set_value( "Company", diff --git a/erpnext/accounts/report/trial_balance/test_trial_balance.py b/erpnext/accounts/report/trial_balance/test_trial_balance.py index d4d055464f..1a29e9f692 100644 --- a/erpnext/accounts/report/trial_balance/test_trial_balance.py +++ b/erpnext/accounts/report/trial_balance/test_trial_balance.py @@ -39,10 +39,10 @@ class TestTrialBalance(FrappeTestCase): branch1 = frappe.new_doc("Branch") branch1.branch = "Location 1" - branch1.insert() + branch1.insert(ignore_if_duplicate=True) branch2 = frappe.new_doc("Branch") branch2.branch = "Location 2" - branch2.insert() + branch2.insert(ignore_if_duplicate=True) si = create_sales_invoice( company=self.company, @@ -91,6 +91,8 @@ def create_accounting_dimension(**args): accounting_dimension = frappe.get_doc("Accounting Dimension", document_type) accounting_dimension.disabled = 0 + clear_dimension_defaults(document_type) + accounting_dimension.load_from_db() accounting_dimension.append( "dimension_defaults", { @@ -109,3 +111,9 @@ def disable_dimension(**args): dimension = frappe.get_doc("Accounting Dimension", document_type) dimension.disabled = 1 dimension.save() + + +def clear_dimension_defaults(dimension_name): + accounting_dimension = frappe.get_doc("Accounting Dimension", dimension_name) + accounting_dimension.dimension_defaults = [] + accounting_dimension.save() From e19a6f5dcba50f70ad1378a9b19236be4e1d7ab1 Mon Sep 17 00:00:00 2001 From: Gursheen Anand Date: Wed, 19 Jul 2023 12:26:57 +0530 Subject: [PATCH 011/101] fix: fetch acc dimensions correctly when fieldname is different from name --- erpnext/accounts/general_ledger.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/erpnext/accounts/general_ledger.py b/erpnext/accounts/general_ledger.py index 994c7c30aa..eacb318d04 100644 --- a/erpnext/accounts/general_ledger.py +++ b/erpnext/accounts/general_ledger.py @@ -91,7 +91,7 @@ def get_accounting_dimensions_for_offsetting_entry(gl_map, company): frappe.qb.from_(acc_dimension) .inner_join(dimension_detail) .on(acc_dimension.name == dimension_detail.parent) - .select(acc_dimension.name, dimension_detail.offsetting_account) + .select(acc_dimension.fieldname, dimension_detail.offsetting_account) .where( (acc_dimension.disabled == 0) & (dimension_detail.company == company) @@ -100,8 +100,7 @@ def get_accounting_dimensions_for_offsetting_entry(gl_map, company): ).run(as_dict=True) accounting_dimensions_to_offset = [] for acc_dimension in acc_dimensions: - fieldname = acc_dimension.name.lower().replace(" ", "_") - values = set([entry.get(fieldname) for entry in gl_map]) + values = set([entry.get(acc_dimension.fieldname) for entry in gl_map]) if len(values) > 1: accounting_dimensions_to_offset.append(acc_dimension) return accounting_dimensions_to_offset From 23e56d3ec18f0c8006bf9709520ee48847529bcf Mon Sep 17 00:00:00 2001 From: Gursheen Anand Date: Wed, 19 Jul 2023 16:07:32 +0530 Subject: [PATCH 012/101] fix: clear dimension defaults after test --- .../accounts/doctype/purchase_invoice/test_purchase_invoice.py | 2 ++ erpnext/accounts/report/trial_balance/test_trial_balance.py | 3 +-- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py b/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py index 846b8bd47c..4180beff8a 100644 --- a/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py +++ b/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py @@ -1741,6 +1741,7 @@ class TestPurchaseInvoice(unittest.TestCase, StockTestMixin): from erpnext.accounts.report.trial_balance.test_trial_balance import ( clear_dimension_defaults, create_accounting_dimension, + disable_dimension, ) create_account( @@ -1788,6 +1789,7 @@ class TestPurchaseInvoice(unittest.TestCase, StockTestMixin): check_acc_dimensions=True, ) clear_dimension_defaults("Branch") + disable_dimension() def set_advance_flag(company, flag, default_account): diff --git a/erpnext/accounts/report/trial_balance/test_trial_balance.py b/erpnext/accounts/report/trial_balance/test_trial_balance.py index 1a29e9f692..732035dc9f 100644 --- a/erpnext/accounts/report/trial_balance/test_trial_balance.py +++ b/erpnext/accounts/report/trial_balance/test_trial_balance.py @@ -63,6 +63,7 @@ class TestTrialBalance(FrappeTestCase): self.assertEqual(total_row["debit"], total_row["credit"]) def tearDown(self): + clear_dimension_defaults("Branch") disable_dimension() @@ -91,8 +92,6 @@ def create_accounting_dimension(**args): accounting_dimension = frappe.get_doc("Accounting Dimension", document_type) accounting_dimension.disabled = 0 - clear_dimension_defaults(document_type) - accounting_dimension.load_from_db() accounting_dimension.append( "dimension_defaults", { From 3f5afb9cac22a8399c2157bca32a1793f6b31e8b Mon Sep 17 00:00:00 2001 From: Gursheen Anand Date: Wed, 19 Jul 2023 18:15:59 +0530 Subject: [PATCH 013/101] fix: reset dimension defaults when company changedin test --- .../purchase_invoice/test_purchase_invoice.py | 2 +- .../report/trial_balance/test_trial_balance.py | 13 ++++++------- 2 files changed, 7 insertions(+), 8 deletions(-) diff --git a/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py b/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py index 4180beff8a..d2f19a1b25 100644 --- a/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py +++ b/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py @@ -1750,7 +1750,7 @@ class TestPurchaseInvoice(unittest.TestCase, StockTestMixin): parent_account="Temporary Accounts - _TC", ) - create_accounting_dimension() + create_accounting_dimension(company="_Test Company", offsetting_account="Offsetting - _TC") branch1 = frappe.new_doc("Branch") branch1.branch = "Location 1" diff --git a/erpnext/accounts/report/trial_balance/test_trial_balance.py b/erpnext/accounts/report/trial_balance/test_trial_balance.py index 732035dc9f..cd0429be4c 100644 --- a/erpnext/accounts/report/trial_balance/test_trial_balance.py +++ b/erpnext/accounts/report/trial_balance/test_trial_balance.py @@ -84,14 +84,14 @@ def create_company(**args): def create_accounting_dimension(**args): args = frappe._dict(args) document_type = args.document_type or "Branch" - if not frappe.db.exists("Accounting Dimension", {"document_type": "Branch"}): - accounting_dimension = frappe.get_doc( - {"doctype": "Accounting Dimension", "document_type": document_type} - ).insert() - else: + if frappe.db.exists("Accounting Dimension", document_type): accounting_dimension = frappe.get_doc("Accounting Dimension", document_type) accounting_dimension.disabled = 0 - + else: + accounting_dimension = frappe.new_doc("Accounting Dimension") + accounting_dimension.document_type = document_type + accounting_dimension.insert() + accounting_dimension.save() accounting_dimension.append( "dimension_defaults", { @@ -101,7 +101,6 @@ def create_accounting_dimension(**args): }, ) accounting_dimension.save() - return accounting_dimension.name def disable_dimension(**args): From 4b4d828260b058db1db8df0eb1f61d2da5a26be7 Mon Sep 17 00:00:00 2001 From: HarryPaulo Date: Wed, 19 Jul 2023 13:02:55 -0300 Subject: [PATCH 014/101] fix: inserting of items with pricing rule with qty range --- erpnext/selling/page/point_of_sale/pos_controller.js | 1 - 1 file changed, 1 deletion(-) diff --git a/erpnext/selling/page/point_of_sale/pos_controller.js b/erpnext/selling/page/point_of_sale/pos_controller.js index 016ebf0cd1..d9efd12202 100644 --- a/erpnext/selling/page/point_of_sale/pos_controller.js +++ b/erpnext/selling/page/point_of_sale/pos_controller.js @@ -605,7 +605,6 @@ erpnext.PointOfSale.Controller = class { i => i.item_code === item_code && (!has_batch_no || (has_batch_no && i.batch_no === batch_no)) && (i.uom === uom) - && (i.rate == rate) ); } From 3558c3d24e3a6dbc10a2d20a03a50a894f359b00 Mon Sep 17 00:00:00 2001 From: barredterra <14891507+barredterra@users.noreply.github.com> Date: Tue, 25 Jul 2023 21:42:01 +0200 Subject: [PATCH 015/101] fix: german translations --- erpnext/translations/de.csv | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/erpnext/translations/de.csv b/erpnext/translations/de.csv index 8efa94df5a..fc48755179 100644 --- a/erpnext/translations/de.csv +++ b/erpnext/translations/de.csv @@ -1145,6 +1145,7 @@ Get Items from Prescriptions,Holen Sie sich Artikel aus Verordnungen, Get Items from Product Bundle,Artikel aus dem Produkt-Bundle übernehmen, Get Suppliers,Holen Sie sich Lieferanten, Get Suppliers By,Holen Sie sich Lieferanten durch, +Get Supplier Group Details,Werte aus Lieferantengruppe übernehmen, Get Updates,Newsletter abonnieren, Get customers from,Holen Sie Kunden von, Get from Patient Encounter,Von der Patientenbegegnung erhalten, @@ -5018,6 +5019,7 @@ Credit To,Gutschreiben auf, Party Account Currency,Währung des Kontos der Partei, Against Expense Account,Zu Aufwandskonto, Inter Company Invoice Reference,Unternehmensübergreifende Rechnungsreferenz, +Internal Supplier,Interner Lieferant, Is Internal Supplier,Ist interner Lieferant, Start date of current invoice's period,Startdatum der laufenden Rechnungsperiode, End date of current invoice's period,Schlußdatum der laufenden Eingangsrechnungsperiode, @@ -5466,6 +5468,8 @@ Tracking,Verfolgung, Ref SQ,Ref-SQ, Inter Company Order Reference,Inter Company Bestellreferenz, Supplier Part Number,Lieferanten-Artikelnummer, +Supplier Primary Contact,Hauptkontakt des Lieferanten, +Supplier Primary Address,Hauptadresse des Lieferanten, Billed Amt,Rechnungsbetrag, Warehouse and Reference,Lager und Referenz, To be delivered to customer,Zur Auslieferung an den Kunden, @@ -7532,7 +7536,7 @@ Accounts Manager,Buchhalter, Allow Sales Invoice Creation Without Sales Order,Ermöglichen Sie die Erstellung von Kundenrechnungen ohne Auftrag, Allow Sales Invoice Creation Without Delivery Note,Ermöglichen Sie die Erstellung einer Ausgangsrechnung ohne Lieferschein, Default Price List,Standardpreisliste, -Primary Address and Contact Detail,Primäre Adresse und Kontaktdetails, +Primary Address and Contact,Hauptadresse und -kontakt, "Select, to make the customer searchable with these fields","Wählen Sie, um den Kunden mit diesen Feldern durchsuchbar zu machen", Customer Primary Contact,Hauptkontakt des Kunden, "Reselect, if the chosen contact is edited after save","Wählen Sie erneut, wenn der ausgewählte Kontakt nach dem Speichern bearbeitet wird", From c47a37c3ab1d4b4e1ebb1c27579cf2a9320db1fb Mon Sep 17 00:00:00 2001 From: Gursheen Anand Date: Wed, 26 Jul 2023 16:42:06 +0530 Subject: [PATCH 016/101] fix: fetch ple with party type employee in AP --- .../accounts_receivable.py | 21 +++++++++++++++---- 1 file changed, 17 insertions(+), 4 deletions(-) diff --git a/erpnext/accounts/report/accounts_receivable/accounts_receivable.py b/erpnext/accounts/report/accounts_receivable/accounts_receivable.py index 30f7fb38c5..93c3fb3340 100755 --- a/erpnext/accounts/report/accounts_receivable/accounts_receivable.py +++ b/erpnext/accounts/report/accounts_receivable/accounts_receivable.py @@ -421,6 +421,10 @@ class ReceivablePayableReport(object): # customer / supplier name party_details = self.get_party_details(row.party) or {} row.update(party_details) + if row.voucher_type == "Expense Claim": + row.party_type = "Employee" + else: + row.party_type = self.party_type if self.filters.get(scrub(self.filters.party_type)): row.currency = row.account_currency else: @@ -747,7 +751,10 @@ class ReceivablePayableReport(object): def prepare_conditions(self): self.qb_selection_filter = [] party_type_field = scrub(self.party_type) - self.qb_selection_filter.append(self.ple.party_type == self.party_type) + if self.party_type == "Supplier": + self.qb_selection_filter.append(self.ple.party_type.isin([self.party_type, "Employee"])) + else: + self.qb_selection_filter.append(self.ple.party_type == self.party_type) self.add_common_filters(party_type_field=party_type_field) @@ -901,10 +908,16 @@ class ReceivablePayableReport(object): self.columns = [] self.add_column("Posting Date", fieldtype="Date") self.add_column( - label=_(self.party_type), + label="Party Type", + fieldname="party_type", + fieldtype="Data", + width=100, + ) + self.add_column( + label="Party", fieldname="party", - fieldtype="Link", - options=self.party_type, + fieldtype="Dynamic Link", + options="party_type", width=180, ) self.add_column( From 81cd7873d343d893a6e2a3b41107f17e03eedcd8 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Thu, 27 Apr 2023 09:46:54 +0530 Subject: [PATCH 017/101] refactor: book exchange gain/loss through journal --- .../doctype/payment_entry/payment_entry.js | 2 +- .../doctype/payment_entry/payment_entry.py | 19 +- .../payment_reconciliation.py | 10 +- .../doctype/sales_invoice/sales_invoice.py | 3 +- erpnext/accounts/utils.py | 34 +++- erpnext/controllers/accounts_controller.py | 170 ++++++++++++------ 6 files changed, 171 insertions(+), 67 deletions(-) diff --git a/erpnext/accounts/doctype/payment_entry/payment_entry.js b/erpnext/accounts/doctype/payment_entry/payment_entry.js index ed18feaf57..105c4767fa 100644 --- a/erpnext/accounts/doctype/payment_entry/payment_entry.js +++ b/erpnext/accounts/doctype/payment_entry/payment_entry.js @@ -9,7 +9,7 @@ erpnext.accounts.taxes.setup_tax_filters("Advance Taxes and Charges"); frappe.ui.form.on('Payment Entry', { onload: function(frm) { - frm.ignore_doctypes_on_cancel_all = ['Sales Invoice', 'Purchase Invoice', "Repost Payment Ledger"]; + frm.ignore_doctypes_on_cancel_all = ['Sales Invoice', 'Purchase Invoice', "Journal Entry", "Repost Payment Ledger"]; if(frm.doc.__islocal) { if (!frm.doc.paid_from) frm.set_value("paid_from_account_currency", null); diff --git a/erpnext/accounts/doctype/payment_entry/payment_entry.py b/erpnext/accounts/doctype/payment_entry/payment_entry.py index 38d8b8fcad..eea7f4d650 100644 --- a/erpnext/accounts/doctype/payment_entry/payment_entry.py +++ b/erpnext/accounts/doctype/payment_entry/payment_entry.py @@ -143,6 +143,7 @@ class PaymentEntry(AccountsController): "Repost Payment Ledger", "Repost Payment Ledger Items", ) + super(PaymentEntry, self).on_cancel() self.make_gl_entries(cancel=1) self.make_advance_gl_entries(cancel=1) self.update_outstanding_amounts() @@ -808,10 +809,25 @@ class PaymentEntry(AccountsController): flt(d.allocated_amount) * flt(exchange_rate), self.precision("base_paid_amount") ) else: + + # Use source/target exchange rate, so no difference amount is calculated. + # then update exchange gain/loss amount in refernece table + # if there is an amount, submit a JE for that + + exchange_rate = 1 + if self.payment_type == "Receive": + exchange_rate = self.source_exchange_rate + elif self.payment_type == "Pay": + exchange_rate = self.target_exchange_rate + base_allocated_amount += flt( - flt(d.allocated_amount) * flt(d.exchange_rate), self.precision("base_paid_amount") + flt(d.allocated_amount) * flt(exchange_rate), self.precision("base_paid_amount") ) + allocated_amount_in_pe_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 return base_allocated_amount def set_total_allocated_amount(self): @@ -1002,6 +1018,7 @@ class PaymentEntry(AccountsController): gl_entries = self.build_gl_map() gl_entries = process_gl_map(gl_entries) make_gl_entries(gl_entries, cancel=cancel, adv_adj=adv_adj) + self.make_exchange_gain_loss_journal() def add_party_gl_entries(self, gl_entries): if self.party_account: diff --git a/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py b/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py index 25d94c55d3..df777f03be 100644 --- a/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py +++ b/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py @@ -363,11 +363,11 @@ class PaymentReconciliation(Document): payment_details = self.get_payment_details(row, dr_or_cr) reconciled_entry.append(payment_details) - if payment_details.difference_amount and row.reference_type not in [ - "Sales Invoice", - "Purchase Invoice", - ]: - self.make_difference_entry(payment_details) + # if payment_details.difference_amount and row.reference_type not in [ + # "Sales Invoice", + # "Purchase Invoice", + # ]: + # self.make_difference_entry(payment_details) if entry_list: reconcile_against_document(entry_list, skip_ref_details_update_for_pe) diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py index 974a876429..fa18d8fc0e 100644 --- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py +++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py @@ -1029,6 +1029,8 @@ class SalesInvoice(SellingController): merge_entries=False, from_repost=from_repost, ) + + self.make_exchange_gain_loss_journal() elif self.docstatus == 2: make_reverse_gl_entries(voucher_type=self.doctype, voucher_no=self.name) @@ -1054,7 +1056,6 @@ class SalesInvoice(SellingController): self.make_customer_gl_entry(gl_entries) self.make_tax_gl_entries(gl_entries) - self.make_exchange_gain_loss_gl_entries(gl_entries) self.make_internal_transfer_gl_entries(gl_entries) self.make_item_gl_entries(gl_entries) diff --git a/erpnext/accounts/utils.py b/erpnext/accounts/utils.py index e354663151..0b3f45ad76 100644 --- a/erpnext/accounts/utils.py +++ b/erpnext/accounts/utils.py @@ -614,9 +614,7 @@ def update_reference_in_payment_entry( "total_amount": d.grand_total, "outstanding_amount": d.outstanding_amount, "allocated_amount": d.allocated_amount, - "exchange_rate": d.exchange_rate - if not d.exchange_gain_loss - else payment_entry.get_exchange_rate(), + "exchange_rate": d.exchange_rate if d.exchange_gain_loss else payment_entry.get_exchange_rate(), "exchange_gain_loss": d.exchange_gain_loss, # only populated from invoice in case of advance allocation "account": d.account, } @@ -655,11 +653,41 @@ def update_reference_in_payment_entry( if not skip_ref_details_update_for_pe: payment_entry.set_missing_ref_details() payment_entry.set_amounts() + payment_entry.make_exchange_gain_loss_journal() if not do_not_save: payment_entry.save(ignore_permissions=True) +def cancel_exchange_gain_loss_journal(parent_doc: dict | object) -> None: + """ + Cancel Exchange Gain/Loss for Sales/Purchase Invoice, if they have any. + """ + if parent_doc.doctype in ["Sales Invoice", "Purchase Invoice", "Payment Entry"]: + journals = frappe.db.get_all( + "Journal Entry Account", + filters={ + "reference_type": parent_doc.doctype, + "reference_name": parent_doc.name, + "docstatus": 1, + }, + fields=["parent"], + as_list=1, + ) + if journals: + exchange_journals = frappe.db.get_all( + "Journal Entry", + filters={ + "name": ["in", [x[0] for x in journals]], + "voucher_type": "Exchange Gain Or Loss", + "docstatus": 1, + }, + as_list=1, + ) + for doc in exchange_journals: + frappe.get_doc("Journal Entry", doc[0]).cancel() + + def unlink_ref_doc_from_payment_entries(ref_doc): remove_ref_doc_link_from_jv(ref_doc.doctype, ref_doc.name) remove_ref_doc_link_from_pe(ref_doc.doctype, ref_doc.name) diff --git a/erpnext/controllers/accounts_controller.py b/erpnext/controllers/accounts_controller.py index 79404894cd..ee7dfb737a 100644 --- a/erpnext/controllers/accounts_controller.py +++ b/erpnext/controllers/accounts_controller.py @@ -5,7 +5,7 @@ import json import frappe -from frappe import _, bold, throw +from frappe import _, bold, qb, throw from frappe.model.workflow import get_workflow_name, is_transition_condition_satisfied from frappe.query_builder.custom import ConstantColumn from frappe.query_builder.functions import Abs, Sum @@ -968,67 +968,119 @@ class AccountsController(TransactionBase): d.exchange_gain_loss = difference - def make_exchange_gain_loss_gl_entries(self, gl_entries): - if self.get("doctype") in ["Purchase Invoice", "Sales Invoice"]: - for d in self.get("advances"): - if d.exchange_gain_loss: - is_purchase_invoice = self.get("doctype") == "Purchase Invoice" - party = self.supplier if is_purchase_invoice else self.customer - party_account = self.credit_to if is_purchase_invoice else self.debit_to - party_type = "Supplier" if is_purchase_invoice else "Customer" - - gain_loss_account = frappe.get_cached_value( - "Company", self.company, "exchange_gain_loss_account" - ) - if not gain_loss_account: - frappe.throw( - _("Please set default Exchange Gain/Loss Account in Company {}").format(self.get("company")) - ) - account_currency = get_account_currency(gain_loss_account) - if account_currency != self.company_currency: - frappe.throw( - _("Currency for {0} must be {1}").format(gain_loss_account, self.company_currency) - ) - - # for purchase - dr_or_cr = "debit" if d.exchange_gain_loss > 0 else "credit" - if not is_purchase_invoice: - # just reverse for sales? - dr_or_cr = "debit" if dr_or_cr == "credit" else "credit" - - gl_entries.append( - self.get_gl_dict( - { - "account": gain_loss_account, - "account_currency": account_currency, - "against": party, - dr_or_cr + "_in_account_currency": abs(d.exchange_gain_loss), - dr_or_cr: abs(d.exchange_gain_loss), - "cost_center": self.cost_center or erpnext.get_default_cost_center(self.company), - "project": self.project, - }, - item=d, + def make_exchange_gain_loss_journal(self) -> None: + """ + Make Exchange Gain/Loss journal for Invoices and Payments + """ + # Cancelling is existing exchange gain/loss journals is handled in on_cancel event + if self.docstatus == 1: + if self.get("doctype") == "Payment Entry": + gain_loss_to_book = [x for x in self.references if x.exchange_gain_loss != 0] + booked = [] + if gain_loss_to_book: + vtypes = [x.reference_doctype for x in gain_loss_to_book] + vnames = [x.reference_name for x in gain_loss_to_book] + je = qb.DocType("Journal Entry") + jea = qb.DocType("Journal Entry Account") + parents = ( + qb.from_(jea) + .select(jea.parent) + .where( + (jea.reference_type == "Payment Entry") + & (jea.reference_name == self.name) + & (jea.docstatus == 1) ) + .run() ) - dr_or_cr = "debit" if dr_or_cr == "credit" else "credit" + booked = [] + if parents: + booked = ( + qb.from_(je) + .inner_join(jea) + .on(je.name == jea.parent) + .select(jea.reference_type, jea.reference_name, jea.reference_detail_no) + .where( + (je.docstatus == 1) + & (je.name.isin(parents)) + & (je.voucher_type == "Exchange Gain or Loss") + ) + .run() + ) - gl_entries.append( - self.get_gl_dict( + for d in gain_loss_to_book: + if d.exchange_gain_loss and ( + (d.reference_doctype, d.reference_name, str(d.idx)) not in booked + ): + journal_entry = frappe.new_doc("Journal Entry") + journal_entry.voucher_type = "Exchange Gain Or Loss" + journal_entry.company = self.company + journal_entry.posting_date = nowdate() + journal_entry.multi_currency = 1 + + if self.payment_type == "Receive": + party_account = self.paid_from + elif self.payment_type == "Pay": + party_account = self.paid_to + + party_account_currency = frappe.get_cached_value( + "Account", party_account, "account_currency" + ) + dr_or_cr = "debit" if d.exchange_gain_loss > 0 else "credit" + reverse_dr_or_cr = "debit" if dr_or_cr == "credit" else "credit" + + gain_loss_account = frappe.get_cached_value( + "Company", self.company, "exchange_gain_loss_account" + ) + if not gain_loss_account: + frappe.throw( + _("Please set default Exchange Gain/Loss Account in Company {}").format( + self.get("company") + ) + ) + gain_loss_account_currency = get_account_currency(gain_loss_account) + if gain_loss_account_currency != self.company_currency: + frappe.throw( + _("Currency for {0} must be {1}").format(gain_loss_account, self.company_currency) + ) + + journal_account = frappe._dict( { "account": party_account, - "party_type": party_type, - "party": party, - "against": gain_loss_account, - dr_or_cr + "_in_account_currency": flt(abs(d.exchange_gain_loss) / self.conversion_rate), + "party_type": self.party_type, + "party": self.party, + "account_currency": party_account_currency, + "exchange_rate": 0, + "cost_center": erpnext.get_default_cost_center(self.company), + "reference_type": d.reference_doctype, + "reference_name": d.reference_name, + "reference_detail_no": d.idx, dr_or_cr: abs(d.exchange_gain_loss), - "cost_center": self.cost_center, - "project": self.project, - }, - self.party_account_currency, - item=self, + dr_or_cr + "_in_account_currency": 0, + } ) - ) + + journal_entry.append("accounts", journal_account) + + journal_account = frappe._dict( + { + "account": gain_loss_account, + "account_currency": gain_loss_account_currency, + "exchange_rate": 1, + "cost_center": erpnext.get_default_cost_center(self.company), + "reference_type": self.doctype, + "reference_name": self.name, + "reference_detail_no": d.idx, + reverse_dr_or_cr + "_in_account_currency": abs(d.exchange_gain_loss), + reverse_dr_or_cr: abs(d.exchange_gain_loss), + } + ) + + journal_entry.append("accounts", journal_account) + + journal_entry.save() + journal_entry.submit() + # frappe.throw("stopping...") def update_against_document_in_jv(self): """ @@ -1090,9 +1142,15 @@ class AccountsController(TransactionBase): reconcile_against_document(lst) def on_cancel(self): - from erpnext.accounts.utils import unlink_ref_doc_from_payment_entries + from erpnext.accounts.utils import ( + cancel_exchange_gain_loss_journal, + unlink_ref_doc_from_payment_entries, + ) + + if self.doctype in ["Sales Invoice", "Purchase Invoice", "Payment Entry"]: + # Cancel Exchange Gain/Loss Journal before unlinking + cancel_exchange_gain_loss_journal(self) - if self.doctype in ["Sales Invoice", "Purchase Invoice"]: if frappe.db.get_single_value("Accounts Settings", "unlink_payment_on_cancellation_of_invoice"): unlink_ref_doc_from_payment_entries(self) From 5e1cd1f22701b7675422b05b3616253d9a3a28db Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Thu, 15 Jun 2023 16:55:56 +0530 Subject: [PATCH 018/101] test: different scenarios for exchange booking --- .../tests/test_accounts_controller.py | 501 ++++++++++++++++++ 1 file changed, 501 insertions(+) create mode 100644 erpnext/controllers/tests/test_accounts_controller.py diff --git a/erpnext/controllers/tests/test_accounts_controller.py b/erpnext/controllers/tests/test_accounts_controller.py new file mode 100644 index 0000000000..31aa857c8f --- /dev/null +++ b/erpnext/controllers/tests/test_accounts_controller.py @@ -0,0 +1,501 @@ +# Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and contributors +# For license information, please see license.txt + +import unittest + +import frappe +from frappe import qb +from frappe.tests.utils import FrappeTestCase, change_settings +from frappe.utils import add_days, flt, nowdate + +from erpnext import get_default_cost_center +from erpnext.accounts.doctype.payment_entry.payment_entry import get_payment_entry +from erpnext.accounts.doctype.payment_entry.test_payment_entry import create_payment_entry +from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice +from erpnext.accounts.party import get_party_account +from erpnext.stock.doctype.item.test_item import create_item + + +def make_customer(customer_name, currency=None): + if not frappe.db.exists("Customer", customer_name): + customer = frappe.new_doc("Customer") + customer.customer_name = customer_name + customer.type = "Individual" + + if currency: + customer.default_currency = currency + customer.save() + return customer.name + else: + return customer_name + + +class TestAccountsController(FrappeTestCase): + """ + Test Exchange Gain/Loss booking on various scenarios + """ + + def setUp(self): + self.create_company() + self.create_account() + self.create_item() + self.create_customer() + self.clear_old_entries() + + def tearDown(self): + frappe.db.rollback() + + def create_company(self): + company_name = "_Test Company MC" + self.company_abbr = abbr = "_CM" + if frappe.db.exists("Company", company_name): + company = frappe.get_doc("Company", company_name) + else: + company = frappe.get_doc( + { + "doctype": "Company", + "company_name": company_name, + "country": "India", + "default_currency": "INR", + "create_chart_of_accounts_based_on": "Standard Template", + "chart_of_accounts": "Standard", + } + ) + company = company.save() + + self.company = company.name + self.cost_center = company.cost_center + self.warehouse = "Stores - " + abbr + self.finished_warehouse = "Finished Goods - " + abbr + self.income_account = "Sales - " + abbr + self.expense_account = "Cost of Goods Sold - " + abbr + self.debit_to = "Debtors - " + abbr + self.debit_usd = "Debtors USD - " + abbr + self.cash = "Cash - " + abbr + self.creditors = "Creditors - " + abbr + + def create_item(self): + item = create_item( + item_code="_Test Notebook", is_stock_item=0, company=self.company, warehouse=self.warehouse + ) + self.item = item if isinstance(item, str) else item.item_code + + def create_customer(self): + self.customer = make_customer("_Test MC Customer USD", "USD") + + def create_account(self): + account_name = "Debtors USD" + if not frappe.db.get_value( + "Account", filters={"account_name": account_name, "company": self.company} + ): + acc = frappe.new_doc("Account") + acc.account_name = account_name + acc.parent_account = "Accounts Receivable - " + self.company_abbr + acc.company = self.company + acc.account_currency = "USD" + acc.account_type = "Receivable" + acc.insert() + else: + name = frappe.db.get_value( + "Account", + filters={"account_name": account_name, "company": self.company}, + fieldname="name", + pluck=True, + ) + acc = frappe.get_doc("Account", name) + self.debtors_usd = acc.name + + def create_sales_invoice( + self, qty=1, rate=1, posting_date=nowdate(), do_not_save=False, do_not_submit=False + ): + """ + Helper function to populate default values in sales invoice + """ + sinv = create_sales_invoice( + qty=qty, + rate=rate, + company=self.company, + customer=self.customer, + item_code=self.item, + item_name=self.item, + cost_center=self.cost_center, + warehouse=self.warehouse, + debit_to=self.debit_usd, + parent_cost_center=self.cost_center, + update_stock=0, + currency="USD", + conversion_rate=80, + is_pos=0, + is_return=0, + return_against=None, + income_account=self.income_account, + expense_account=self.expense_account, + do_not_save=do_not_save, + do_not_submit=do_not_submit, + ) + return sinv + + def create_payment_entry( + self, amount=1, source_exc_rate=75, posting_date=nowdate(), customer=None + ): + """ + Helper function to populate default values in payment entry + """ + payment = create_payment_entry( + company=self.company, + payment_type="Receive", + party_type="Customer", + party=customer or self.customer, + paid_from=self.debit_usd, + paid_to=self.cash, + paid_amount=amount, + ) + payment.source_exchange_rate = source_exc_rate + payment.received_amount = source_exc_rate * amount + payment.posting_date = posting_date + return payment + + def clear_old_entries(self): + doctype_list = [ + "GL Entry", + "Payment Ledger Entry", + "Sales Invoice", + "Purchase Invoice", + "Payment Entry", + "Journal Entry", + ] + for doctype in doctype_list: + qb.from_(qb.DocType(doctype)).delete().where(qb.DocType(doctype).company == self.company).run() + + def create_payment_reconciliation(self): + pr = frappe.new_doc("Payment Reconciliation") + pr.company = self.company + pr.party_type = "Customer" + pr.party = self.customer + pr.receivable_payable_account = get_party_account(pr.party_type, pr.party, pr.company) + pr.from_invoice_date = pr.to_invoice_date = pr.from_payment_date = pr.to_payment_date = nowdate() + return pr + + def create_journal_entry( + self, acc1=None, acc2=None, amount=0, posting_date=None, cost_center=None + ): + je = frappe.new_doc("Journal Entry") + je.posting_date = posting_date or nowdate() + je.company = self.company + je.user_remark = "test" + if not cost_center: + cost_center = self.cost_center + je.set( + "accounts", + [ + { + "account": acc1, + "cost_center": cost_center, + "debit_in_account_currency": amount if amount > 0 else 0, + "credit_in_account_currency": abs(amount) if amount < 0 else 0, + }, + { + "account": acc2, + "cost_center": cost_center, + "credit_in_account_currency": amount if amount > 0 else 0, + "debit_in_account_currency": abs(amount) if amount < 0 else 0, + }, + ], + ) + return je + + def get_journals_for(self, voucher_type: str, voucher_no: str) -> list: + journals = [] + if voucher_type and voucher_no: + journals = frappe.db.get_all( + "Journal Entry Account", + filters={"reference_type": voucher_type, "reference_name": voucher_no, "docstatus": 1}, + fields=["parent"], + ) + return journals + + def test_01_payment_against_invoice(self): + # Invoice in Foreign Currency + si = self.create_sales_invoice(qty=1, rate=1) + # Payment + pe = self.create_payment_entry(amount=1, source_exc_rate=75).save() + pe.append( + "references", + {"reference_doctype": si.doctype, "reference_name": si.name, "allocated_amount": 1}, + ) + pe = pe.save().submit() + + si.reload() + self.assertEqual(si.outstanding_amount, 0) + + # Exchange Gain/Loss Journal should've been created. + exc_je_for_si = self.get_journals_for(si.doctype, si.name) + exc_je_for_pe = self.get_journals_for(pe.doctype, pe.name) + + self.assertNotEqual(exc_je_for_si, []) + self.assertEqual(len(exc_je_for_si), 1) + self.assertEqual(len(exc_je_for_pe), 1) + self.assertEqual(exc_je_for_si[0], exc_je_for_pe[0]) + + # Cancel Payment + pe.cancel() + + si.reload() + self.assertEqual(si.outstanding_amount, 1) + + # Exchange Gain/Loss Journal should've been cancelled + exc_je_for_si = self.get_journals_for(si.doctype, si.name) + exc_je_for_pe = self.get_journals_for(pe.doctype, pe.name) + + self.assertEqual(exc_je_for_si, []) + self.assertEqual(exc_je_for_pe, []) + + def test_02_advance_against_invoice(self): + # Advance Payment + adv = self.create_payment_entry(amount=1, source_exc_rate=85).save().submit() + adv.reload() + + # Invoice in Foreign Currency + si = self.create_sales_invoice(qty=1, rate=1, do_not_submit=True) + si.append( + "advances", + { + "doctype": "Sales Invoice Advance", + "reference_type": adv.doctype, + "reference_name": adv.name, + "advance_amount": 1, + "allocated_amount": 1, + "ref_exchange_rate": 85, + "remarks": "Test", + }, + ) + si = si.save() + si = si.submit() + + adv.reload() + self.assertEqual(si.outstanding_amount, 0) + + # Exchange Gain/Loss Journal should've been created. + exc_je_for_si = self.get_journals_for(si.doctype, si.name) + exc_je_for_adv = self.get_journals_for(adv.doctype, adv.name) + + self.assertNotEqual(exc_je_for_si, []) + self.assertEqual(len(exc_je_for_si), 1) + self.assertEqual(len(exc_je_for_adv), 1) + self.assertEqual(exc_je_for_si, exc_je_for_adv) + + # Cancel Invoice + si.cancel() + + # Exchange Gain/Loss Journal should've been cancelled + exc_je_for_si = self.get_journals_for(si.doctype, si.name) + exc_je_for_adv = self.get_journals_for(adv.doctype, adv.name) + + self.assertEqual(exc_je_for_si, []) + self.assertEqual(exc_je_for_adv, []) + + def test_03_partial_advance_and_payment_for_invoice(self): + """ + Invoice with partial advance payment, and a normal payment + """ + # Partial Advance + adv = self.create_payment_entry(amount=1, source_exc_rate=85).save().submit() + adv.reload() + + # Invoice in Foreign Currency linked with advance + si = self.create_sales_invoice(qty=2, rate=1, do_not_submit=True) + si.append( + "advances", + { + "doctype": "Sales Invoice Advance", + "reference_type": adv.doctype, + "reference_name": adv.name, + "advance_amount": 1, + "allocated_amount": 1, + "ref_exchange_rate": 85, + "remarks": "Test", + }, + ) + si = si.save() + si = si.submit() + + si.reload() + self.assertEqual(si.outstanding_amount, 1) + + # Exchange Gain/Loss Journal should've been created for the partial advance + exc_je_for_si = self.get_journals_for(si.doctype, si.name) + exc_je_for_adv = self.get_journals_for(adv.doctype, adv.name) + + self.assertNotEqual(exc_je_for_si, []) + self.assertEqual(len(exc_je_for_si), 1) + self.assertEqual(len(exc_je_for_adv), 1) + self.assertEqual(exc_je_for_si, exc_je_for_adv) + + # Payment + pe = self.create_payment_entry(amount=1, source_exc_rate=75).save() + pe.append( + "references", + {"reference_doctype": si.doctype, "reference_name": si.name, "allocated_amount": 1}, + ) + pe = pe.save().submit() + + si.reload() + self.assertEqual(si.outstanding_amount, 0) + + # Exchange Gain/Loss Journal should've been created for the payment + exc_je_for_si = self.get_journals_for(si.doctype, si.name) + exc_je_for_pe = self.get_journals_for(pe.doctype, pe.name) + + self.assertNotEqual(exc_je_for_si, []) + # There should be 2 JE's now. One for the advance and one for the payment + self.assertEqual(len(exc_je_for_si), 2) + self.assertEqual(len(exc_je_for_pe), 1) + self.assertEqual(exc_je_for_si, exc_je_for_pe + exc_je_for_adv) + + # Cancel Invoice + si.reload() + si.cancel() + + # Exchange Gain/Loss Journal should been cancelled + exc_je_for_si = self.get_journals_for(si.doctype, si.name) + exc_je_for_pe = self.get_journals_for(pe.doctype, pe.name) + exc_je_for_adv = self.get_journals_for(adv.doctype, adv.name) + + self.assertEqual(exc_je_for_si, []) + self.assertEqual(exc_je_for_pe, []) + self.assertEqual(exc_je_for_adv, []) + + def test_04_partial_advance_and_payment_for_invoice_with_cancellation(self): + """ + Invoice with partial advance payment, and a normal payment. Cancel advance and payment. + """ + # Partial Advance + adv = self.create_payment_entry(amount=1, source_exc_rate=85).save().submit() + adv.reload() + + # Invoice in Foreign Currency linked with advance + si = self.create_sales_invoice(qty=2, rate=1, do_not_submit=True) + si.append( + "advances", + { + "doctype": "Sales Invoice Advance", + "reference_type": adv.doctype, + "reference_name": adv.name, + "advance_amount": 1, + "allocated_amount": 1, + "ref_exchange_rate": 85, + "remarks": "Test", + }, + ) + si = si.save() + si = si.submit() + + si.reload() + self.assertEqual(si.outstanding_amount, 1) + + # Exchange Gain/Loss Journal should've been created for the partial advance + exc_je_for_si = self.get_journals_for(si.doctype, si.name) + exc_je_for_adv = self.get_journals_for(adv.doctype, adv.name) + + self.assertNotEqual(exc_je_for_si, []) + self.assertEqual(len(exc_je_for_si), 1) + self.assertEqual(len(exc_je_for_adv), 1) + self.assertEqual(exc_je_for_si, exc_je_for_adv) + + # Payment + pe = self.create_payment_entry(amount=1, source_exc_rate=75).save() + pe.append( + "references", + {"reference_doctype": si.doctype, "reference_name": si.name, "allocated_amount": 1}, + ) + pe = pe.save().submit() + + si.reload() + self.assertEqual(si.outstanding_amount, 0) + + # Exchange Gain/Loss Journal should've been created for the payment + exc_je_for_si = self.get_journals_for(si.doctype, si.name) + exc_je_for_pe = self.get_journals_for(pe.doctype, pe.name) + + self.assertNotEqual(exc_je_for_si, []) + # There should be 2 JE's now. One for the advance and one for the payment + self.assertEqual(len(exc_je_for_si), 2) + self.assertEqual(len(exc_je_for_pe), 1) + self.assertEqual(exc_je_for_si, exc_je_for_pe + exc_je_for_adv) + + adv.reload() + adv.cancel() + + si.reload() + self.assertEqual(si.outstanding_amount, 1) + + exc_je_for_si = self.get_journals_for(si.doctype, si.name) + exc_je_for_pe = self.get_journals_for(pe.doctype, pe.name) + exc_je_for_adv = self.get_journals_for(adv.doctype, adv.name) + + # Exchange Gain/Loss Journal for advance should been cancelled + self.assertEqual(len(exc_je_for_si), 1) + self.assertEqual(len(exc_je_for_pe), 1) + self.assertEqual(exc_je_for_adv, []) + + def test_05_same_payment_split_against_invoice(self): + # Invoice in Foreign Currency + si = self.create_sales_invoice(qty=2, rate=1) + # Payment + pe = self.create_payment_entry(amount=2, source_exc_rate=75).save() + pe.append( + "references", + {"reference_doctype": si.doctype, "reference_name": si.name, "allocated_amount": 1}, + ) + pe = pe.save().submit() + + si.reload() + self.assertEqual(si.outstanding_amount, 1) + + # Exchange Gain/Loss Journal should've been created. + exc_je_for_si = self.get_journals_for(si.doctype, si.name) + exc_je_for_pe = self.get_journals_for(pe.doctype, pe.name) + + self.assertNotEqual(exc_je_for_si, []) + self.assertEqual(len(exc_je_for_si), 1) + self.assertEqual(len(exc_je_for_pe), 1) + self.assertEqual(exc_je_for_si[0], exc_je_for_pe[0]) + + # Reconcile the remaining amount + pr = frappe.get_doc("Payment Reconciliation") + pr.company = self.company + pr.party_type = "Customer" + pr.party = self.customer + pr.receivable_payable_account = self.debit_usd + + pr.get_unreconciled_entries() + self.assertEqual(len(pr.invoices), 1) + self.assertEqual(len(pr.payments), 1) + + # Test exact payment allocation + invoices = [x.as_dict() for x in pr.invoices] + payments = [x.as_dict() for x in pr.payments] + pr.allocate_entries(frappe._dict({"invoices": invoices, "payments": payments})) + + pr.reconcile() + self.assertEqual(len(pr.invoices), 0) + self.assertEqual(len(pr.payments), 0) + + exc_je_for_si = self.get_journals_for(si.doctype, si.name) + exc_je_for_pe = self.get_journals_for(pe.doctype, pe.name) + self.assertEqual(len(exc_je_for_si), 2) + self.assertEqual(len(exc_je_for_pe), 2) + self.assertEqual(exc_je_for_si, exc_je_for_pe) + + # Cancel Payment + pe.reload() + pe.cancel() + + si.reload() + self.assertEqual(si.outstanding_amount, 2) + + # Exchange Gain/Loss Journal should've been cancelled + exc_je_for_si = self.get_journals_for(si.doctype, si.name) + exc_je_for_pe = self.get_journals_for(pe.doctype, pe.name) + self.assertEqual(exc_je_for_si, []) + self.assertEqual(exc_je_for_pe, []) From 7e94a1c51b428202820858f72a7e4a864cde0e9c Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Fri, 16 Jun 2023 11:34:11 +0530 Subject: [PATCH 019/101] refactor: replace with new method in purchase invoice --- erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py index 230a8b3c58..1f9555a3c3 100644 --- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py +++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py @@ -536,6 +536,7 @@ class PurchaseInvoice(BuyingController): merge_entries=False, from_repost=from_repost, ) + self.make_exchange_gain_loss_journal() elif self.docstatus == 2: provisional_entries = [a for a in gl_entries if a.voucher_type == "Purchase Receipt"] make_reverse_gl_entries(voucher_type=self.doctype, voucher_no=self.name) @@ -580,7 +581,6 @@ class PurchaseInvoice(BuyingController): self.get_asset_gl_entry(gl_entries) self.make_tax_gl_entries(gl_entries) - self.make_exchange_gain_loss_gl_entries(gl_entries) self.make_internal_transfer_gl_entries(gl_entries) gl_entries = make_regional_gl_entries(gl_entries, self) From 0587338435a6ffeeb59669ff20dbd9779b9ac740 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Fri, 16 Jun 2023 12:32:21 +0530 Subject: [PATCH 020/101] chore: patch to update property setter for Journal Entry Accounts --- erpnext/patches.txt | 1 + ...eference_type_in_journal_entry_accounts.py | 22 +++++++++++++++++++ 2 files changed, 23 insertions(+) create mode 100644 erpnext/patches/v14_0/update_reference_type_in_journal_entry_accounts.py diff --git a/erpnext/patches.txt b/erpnext/patches.txt index 0f4238c16b..641d7550e3 100644 --- a/erpnext/patches.txt +++ b/erpnext/patches.txt @@ -320,6 +320,7 @@ erpnext.patches.v15_0.update_gpa_and_ndb_for_assdeprsch erpnext.patches.v14_0.create_accounting_dimensions_for_closing_balance erpnext.patches.v14_0.update_closing_balances #14-07-2023 execute:frappe.db.set_single_value("Accounts Settings", "merge_similar_account_heads", 0) +erpnext.patches.v14_0.update_reference_type_in_journal_entry_accounts # below migration patches should always run last erpnext.patches.v14_0.migrate_gl_to_payment_ledger execute:frappe.delete_doc_if_exists("Report", "Tax Detail") diff --git a/erpnext/patches/v14_0/update_reference_type_in_journal_entry_accounts.py b/erpnext/patches/v14_0/update_reference_type_in_journal_entry_accounts.py new file mode 100644 index 0000000000..48b6bcf755 --- /dev/null +++ b/erpnext/patches/v14_0/update_reference_type_in_journal_entry_accounts.py @@ -0,0 +1,22 @@ +import frappe + + +def execute(): + """ + Update Propery Setters for Journal Entry with new 'Entry Type' + """ + new_reference_type = "Payment Entry" + prop_setter = frappe.db.get_list( + "Property Setter", + filters={ + "doc_type": "Journal Entry Account", + "field_name": "reference_type", + "property": "options", + }, + ) + if prop_setter: + property_setter_doc = frappe.get_doc("Property Setter", prop_setter[0].get("name")) + + if new_reference_type not in property_setter_doc.value.split("\n"): + property_setter_doc.value += "\n" + new_reference_type + property_setter_doc.save() From 13febcac811507c7c61bc116ca797857d0b5baf5 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Fri, 16 Jun 2023 14:01:48 +0530 Subject: [PATCH 021/101] refactor: add new reference type in journal entry account --- .../doctype/journal_entry_account/journal_entry_account.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/erpnext/accounts/doctype/journal_entry_account/journal_entry_account.json b/erpnext/accounts/doctype/journal_entry_account/journal_entry_account.json index 47ad19e0f9..3ba8cea94b 100644 --- a/erpnext/accounts/doctype/journal_entry_account/journal_entry_account.json +++ b/erpnext/accounts/doctype/journal_entry_account/journal_entry_account.json @@ -203,7 +203,7 @@ "fieldtype": "Select", "label": "Reference Type", "no_copy": 1, - "options": "\nSales Invoice\nPurchase Invoice\nJournal Entry\nSales Order\nPurchase Order\nExpense Claim\nAsset\nLoan\nPayroll Entry\nEmployee Advance\nExchange Rate Revaluation\nInvoice Discounting\nFees\nFull and Final Statement" + "options": "\nSales Invoice\nPurchase Invoice\nJournal Entry\nSales Order\nPurchase Order\nExpense Claim\nAsset\nLoan\nPayroll Entry\nEmployee Advance\nExchange Rate Revaluation\nInvoice Discounting\nFees\nFull and Final Statement\nPayment Entry" }, { "fieldname": "reference_name", @@ -284,7 +284,7 @@ "idx": 1, "istable": 1, "links": [], - "modified": "2022-10-26 20:03:10.906259", + "modified": "2023-06-16 14:11:13.507807", "modified_by": "Administrator", "module": "Accounts", "name": "Journal Entry Account", From 34b5e849a290ee02d9b653286dfbe6590d35a800 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Fri, 16 Jun 2023 14:07:44 +0530 Subject: [PATCH 022/101] chore: fix logic for purchase invoice and some typos --- erpnext/accounts/doctype/payment_entry/payment_entry.py | 4 ++-- erpnext/controllers/accounts_controller.py | 7 +++++-- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/erpnext/accounts/doctype/payment_entry/payment_entry.py b/erpnext/accounts/doctype/payment_entry/payment_entry.py index eea7f4d650..44e3e898d2 100644 --- a/erpnext/accounts/doctype/payment_entry/payment_entry.py +++ b/erpnext/accounts/doctype/payment_entry/payment_entry.py @@ -811,8 +811,8 @@ class PaymentEntry(AccountsController): else: # Use source/target exchange rate, so no difference amount is calculated. - # then update exchange gain/loss amount in refernece table - # if there is an amount, submit a JE for that + # then update exchange gain/loss amount in reference table + # if there is an exchange gain/loss amount in reference table, submit a JE for that exchange_rate = 1 if self.payment_type == "Receive": diff --git a/erpnext/controllers/accounts_controller.py b/erpnext/controllers/accounts_controller.py index ee7dfb737a..e60719c5c0 100644 --- a/erpnext/controllers/accounts_controller.py +++ b/erpnext/controllers/accounts_controller.py @@ -972,7 +972,7 @@ class AccountsController(TransactionBase): """ Make Exchange Gain/Loss journal for Invoices and Payments """ - # Cancelling is existing exchange gain/loss journals is handled in on_cancel event + # Cancelling existing exchange gain/loss journals is handled in on_cancel event in accounts/utils.py if self.docstatus == 1: if self.get("doctype") == "Payment Entry": gain_loss_to_book = [x for x in self.references if x.exchange_gain_loss != 0] @@ -1027,6 +1027,10 @@ class AccountsController(TransactionBase): "Account", party_account, "account_currency" ) dr_or_cr = "debit" if d.exchange_gain_loss > 0 else "credit" + + if d.reference_doctype == "Purchase Invoice": + dr_or_cr = "debit" if dr_or_cr == "credit" else "credit" + reverse_dr_or_cr = "debit" if dr_or_cr == "credit" else "credit" gain_loss_account = frappe.get_cached_value( @@ -1080,7 +1084,6 @@ class AccountsController(TransactionBase): journal_entry.save() journal_entry.submit() - # frappe.throw("stopping...") def update_against_document_in_jv(self): """ From c1184585eda2e37b74718b95d541fa0419511bd9 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Mon, 19 Jun 2023 17:51:05 +0530 Subject: [PATCH 023/101] refactor: helper method --- .../doctype/payment_entry/test_payment_entry.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/erpnext/accounts/doctype/payment_entry/test_payment_entry.py b/erpnext/accounts/doctype/payment_entry/test_payment_entry.py index c6e93f3f7a..afd03c6bd4 100644 --- a/erpnext/accounts/doctype/payment_entry/test_payment_entry.py +++ b/erpnext/accounts/doctype/payment_entry/test_payment_entry.py @@ -31,6 +31,16 @@ class TestPaymentEntry(FrappeTestCase): def tearDown(self): frappe.db.rollback() + def get_journals_for(self, voucher_type: str, voucher_no: str) -> list: + journals = [] + if voucher_type and voucher_no: + journals = frappe.db.get_all( + "Journal Entry Account", + filters={"reference_type": voucher_type, "reference_name": voucher_no, "docstatus": 1}, + fields=["parent"], + ) + return journals + def test_payment_entry_against_order(self): so = make_sales_order() pe = get_payment_entry("Sales Order", so.name, bank_account="_Test Cash - _TC") From 92ae9c220110ddcb32d90a1bb89f0b85e72ff7d0 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Mon, 19 Jun 2023 09:58:18 +0530 Subject: [PATCH 024/101] refactor: remove unused variable, pe should pull in parent exc rate 1. 'reference_doc' variable is never set. Hence, removing. 2. set_exchange_rate() relies on ref_doc, which was never set due to point [1]. Replacing it with 'doc'. 3. Sales/Purchase Invoice has 'conversion_rate' field for tracking exchange rate. Added a get statement for them as well. --- erpnext/accounts/doctype/payment_entry/payment_entry.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/erpnext/accounts/doctype/payment_entry/payment_entry.py b/erpnext/accounts/doctype/payment_entry/payment_entry.py index 44e3e898d2..89241ebfe0 100644 --- a/erpnext/accounts/doctype/payment_entry/payment_entry.py +++ b/erpnext/accounts/doctype/payment_entry/payment_entry.py @@ -400,7 +400,7 @@ class PaymentEntry(AccountsController): else: if ref_doc: if self.paid_from_account_currency == ref_doc.currency: - self.source_exchange_rate = ref_doc.get("exchange_rate") + self.source_exchange_rate = ref_doc.get("exchange_rate") or ref_doc.get("conversion_rate") if not self.source_exchange_rate: self.source_exchange_rate = get_exchange_rate( @@ -413,7 +413,7 @@ class PaymentEntry(AccountsController): elif self.paid_to and not self.target_exchange_rate: if ref_doc: if self.paid_to_account_currency == ref_doc.currency: - self.target_exchange_rate = ref_doc.get("exchange_rate") + self.target_exchange_rate = ref_doc.get("exchange_rate") or ref_doc.get("conversion_rate") if not self.target_exchange_rate: self.target_exchange_rate = get_exchange_rate( @@ -2005,7 +2005,6 @@ def get_payment_entry( payment_type=None, reference_date=None, ): - reference_doc = None doc = frappe.get_doc(dt, dn) over_billing_allowance = frappe.db.get_single_value("Accounts Settings", "over_billing_allowance") if dt in ("Sales Order", "Purchase Order") and flt(doc.per_billed, 2) >= ( @@ -2145,7 +2144,7 @@ def get_payment_entry( update_accounting_dimensions(pe, doc) if party_account and bank: - pe.set_exchange_rate(ref_doc=reference_doc) + pe.set_exchange_rate(ref_doc=doc) pe.set_amounts() if discount_amount: From 4ff53e106271e0562e2ba5604802a41c24c999c4 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Mon, 19 Jun 2023 10:23:23 +0530 Subject: [PATCH 025/101] refactor: assert exchange gain/loss amount in reference table --- .../doctype/payment_entry/test_payment_entry.py | 14 ++++---------- 1 file changed, 4 insertions(+), 10 deletions(-) diff --git a/erpnext/accounts/doctype/payment_entry/test_payment_entry.py b/erpnext/accounts/doctype/payment_entry/test_payment_entry.py index afd03c6bd4..997d52bed6 100644 --- a/erpnext/accounts/doctype/payment_entry/test_payment_entry.py +++ b/erpnext/accounts/doctype/payment_entry/test_payment_entry.py @@ -601,21 +601,15 @@ class TestPaymentEntry(FrappeTestCase): pe.target_exchange_rate = 45.263 pe.reference_no = "1" pe.reference_date = "2016-01-01" - - pe.append( - "deductions", - { - "account": "_Test Exchange Gain/Loss - _TC", - "cost_center": "_Test Cost Center - _TC", - "amount": 94.80, - }, - ) - pe.save() self.assertEqual(flt(pe.difference_amount, 2), 0.0) self.assertEqual(flt(pe.unallocated_amount, 2), 0.0) + # the exchange gain/loss amount is captured in reference table and a separate Journal will be submitted for them + # payment entry will not be generating difference amount + self.assertEqual(flt(pe.references[0].exchange_gain_loss, 2), -94.74) + def test_payment_entry_retrieves_last_exchange_rate(self): from erpnext.setup.doctype.currency_exchange.test_currency_exchange import ( save_new_records, From 00a2e42a47fb064afbb31b27653a54d12b6c8097 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Mon, 19 Jun 2023 11:26:49 +0530 Subject: [PATCH 026/101] refactor(test): exc gain/loss booked through journal --- .../payment_entry/test_payment_entry.py | 19 +++++++------------ 1 file changed, 7 insertions(+), 12 deletions(-) diff --git a/erpnext/accounts/doctype/payment_entry/test_payment_entry.py b/erpnext/accounts/doctype/payment_entry/test_payment_entry.py index 997d52bed6..5f3267427a 100644 --- a/erpnext/accounts/doctype/payment_entry/test_payment_entry.py +++ b/erpnext/accounts/doctype/payment_entry/test_payment_entry.py @@ -796,33 +796,28 @@ class TestPaymentEntry(FrappeTestCase): pe.reference_no = "1" pe.reference_date = "2016-01-01" pe.source_exchange_rate = 55 - - pe.append( - "deductions", - { - "account": "_Test Exchange Gain/Loss - _TC", - "cost_center": "_Test Cost Center - _TC", - "amount": -500, - }, - ) pe.save() self.assertEqual(pe.unallocated_amount, 0) self.assertEqual(pe.difference_amount, 0) - + self.assertEqual(pe.references[0].exchange_gain_loss, 500) pe.submit() expected_gle = dict( (d[0], d) for d in [ - ["_Test Receivable USD - _TC", 0, 5000, si.name], + ["_Test Receivable USD - _TC", 0, 5500, si.name], ["_Test Bank USD - _TC", 5500, 0, None], - ["_Test Exchange Gain/Loss - _TC", 0, 500, None], ] ) self.validate_gl_entries(pe.name, expected_gle) + # Exchange gain/loss should have been posted through a journal + exc_je_for_si = self.get_journals_for(si.doctype, si.name) + exc_je_for_pe = self.get_journals_for(pe.doctype, pe.name) + + self.assertEqual(exc_je_for_si, exc_je_for_pe) outstanding_amount = flt(frappe.db.get_value("Sales Invoice", si.name, "outstanding_amount")) self.assertEqual(outstanding_amount, 0) From 7b516f84636e7219ac17d972c80a8286c385e954 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Mon, 26 Jun 2023 17:34:28 +0530 Subject: [PATCH 027/101] refactor: exc booking logic for Journal Entry --- erpnext/accounts/utils.py | 3 + erpnext/controllers/accounts_controller.py | 78 +++++++++++++++++++++- 2 files changed, 80 insertions(+), 1 deletion(-) diff --git a/erpnext/accounts/utils.py b/erpnext/accounts/utils.py index 0b3f45ad76..cfd0133700 100644 --- a/erpnext/accounts/utils.py +++ b/erpnext/accounts/utils.py @@ -459,6 +459,9 @@ def reconcile_against_document(args, skip_ref_details_update_for_pe=False): # n # update ref in advance entry if voucher_type == "Journal Entry": update_reference_in_journal_entry(entry, doc, do_not_save=True) + # advance section in sales/purchase invoice and reconciliation tool,both pass on exchange gain/loss + # amount and account in args + doc.make_exchange_gain_loss_journal(args) else: update_reference_in_payment_entry( entry, doc, do_not_save=True, skip_ref_details_update_for_pe=skip_ref_details_update_for_pe diff --git a/erpnext/controllers/accounts_controller.py b/erpnext/controllers/accounts_controller.py index e60719c5c0..5597b515f6 100644 --- a/erpnext/controllers/accounts_controller.py +++ b/erpnext/controllers/accounts_controller.py @@ -968,13 +968,89 @@ class AccountsController(TransactionBase): d.exchange_gain_loss = difference - def make_exchange_gain_loss_journal(self) -> None: + def make_exchange_gain_loss_journal(self, args=None) -> None: """ Make Exchange Gain/Loss journal for Invoices and Payments """ # Cancelling existing exchange gain/loss journals is handled in on_cancel event in accounts/utils.py if self.docstatus == 1: + if self.get("doctype") == "Journal Entry": + if args: + for arg in args: + print(arg) + if arg.get("difference_amount") != 0 and arg.get("difference_account"): + journal_entry = frappe.new_doc("Journal Entry") + journal_entry.voucher_type = "Exchange Gain Or Loss" + journal_entry.company = self.company + journal_entry.posting_date = nowdate() + journal_entry.multi_currency = 1 + + party_account = arg.account + party_account_currency = frappe.get_cached_value( + "Account", party_account, "account_currency" + ) + dr_or_cr = "debit" if arg.difference_amount > 0 else "credit" + + if arg.reference_doctype == "Purchase Invoice": + dr_or_cr = "debit" if dr_or_cr == "credit" else "credit" + + reverse_dr_or_cr = "debit" if dr_or_cr == "credit" else "credit" + + gain_loss_account = arg.difference_account + + if not gain_loss_account: + frappe.throw( + _("Please set default Exchange Gain/Loss Account in Company {}").format( + self.get("company") + ) + ) + + gain_loss_account_currency = get_account_currency(gain_loss_account) + if gain_loss_account_currency != self.company_currency: + frappe.throw( + _("Currency for {0} must be {1}").format(gain_loss_account, self.company_currency) + ) + + journal_account = frappe._dict( + { + "account": party_account, + "party_type": arg.party_type, + "party": arg.party, + "account_currency": party_account_currency, + "exchange_rate": 0, + "cost_center": erpnext.get_default_cost_center(self.company), + "reference_type": arg.against_voucher_type, + "reference_name": arg.against_voucher, + "reference_detail_no": arg.idx, + dr_or_cr: abs(arg.difference_amount), + dr_or_cr + "_in_account_currency": 0, + } + ) + + journal_entry.append("accounts", journal_account) + + journal_account = frappe._dict( + { + "account": gain_loss_account, + "account_currency": gain_loss_account_currency, + "exchange_rate": 1, + "cost_center": erpnext.get_default_cost_center(self.company), + # TODO: figure out a way to pass reference + # "reference_type": self.doctype, + # "reference_name": self.name, + # "reference_detail_no": arg.idx, + reverse_dr_or_cr + "_in_account_currency": abs(arg.difference_amount), + reverse_dr_or_cr: abs(arg.difference_amount), + } + ) + + journal_entry.append("accounts", journal_account) + + journal_entry.save() + journal_entry.submit() + if self.get("doctype") == "Payment Entry": + # For Payment Entry, exchange_gain_loss field in the `reference` table is the trigger for journal creation gain_loss_to_book = [x for x in self.references if x.exchange_gain_loss != 0] booked = [] if gain_loss_to_book: From ee3ce82ea82df9dd2910e4d29a5c2c4f885be393 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Mon, 26 Jun 2023 21:43:20 +0530 Subject: [PATCH 028/101] chore: remove debugging statements and fixing failing unit tests --- erpnext/controllers/accounts_controller.py | 23 +++++++++++----------- 1 file changed, 11 insertions(+), 12 deletions(-) diff --git a/erpnext/controllers/accounts_controller.py b/erpnext/controllers/accounts_controller.py index 5597b515f6..2548bdc760 100644 --- a/erpnext/controllers/accounts_controller.py +++ b/erpnext/controllers/accounts_controller.py @@ -977,26 +977,25 @@ class AccountsController(TransactionBase): if self.get("doctype") == "Journal Entry": if args: for arg in args: - print(arg) - if arg.get("difference_amount") != 0 and arg.get("difference_account"): + if arg.get("difference_amount", 0) != 0 and arg.get("difference_account"): journal_entry = frappe.new_doc("Journal Entry") journal_entry.voucher_type = "Exchange Gain Or Loss" journal_entry.company = self.company journal_entry.posting_date = nowdate() journal_entry.multi_currency = 1 - party_account = arg.account + party_account = arg.get("account") party_account_currency = frappe.get_cached_value( "Account", party_account, "account_currency" ) - dr_or_cr = "debit" if arg.difference_amount > 0 else "credit" + dr_or_cr = "debit" if arg.get("difference_amount") > 0 else "credit" if arg.reference_doctype == "Purchase Invoice": dr_or_cr = "debit" if dr_or_cr == "credit" else "credit" reverse_dr_or_cr = "debit" if dr_or_cr == "credit" else "credit" - gain_loss_account = arg.difference_account + gain_loss_account = arg.get("difference_account") if not gain_loss_account: frappe.throw( @@ -1014,14 +1013,14 @@ class AccountsController(TransactionBase): journal_account = frappe._dict( { "account": party_account, - "party_type": arg.party_type, - "party": arg.party, + "party_type": arg.get("party_type"), + "party": arg.get("party"), "account_currency": party_account_currency, "exchange_rate": 0, "cost_center": erpnext.get_default_cost_center(self.company), - "reference_type": arg.against_voucher_type, - "reference_name": arg.against_voucher, - "reference_detail_no": arg.idx, + "reference_type": arg.get("against_voucher_type"), + "reference_name": arg.get("against_voucher"), + "reference_detail_no": arg.get("idx"), dr_or_cr: abs(arg.difference_amount), dr_or_cr + "_in_account_currency": 0, } @@ -1039,8 +1038,8 @@ class AccountsController(TransactionBase): # "reference_type": self.doctype, # "reference_name": self.name, # "reference_detail_no": arg.idx, - reverse_dr_or_cr + "_in_account_currency": abs(arg.difference_amount), - reverse_dr_or_cr: abs(arg.difference_amount), + reverse_dr_or_cr + "_in_account_currency": abs(arg.get("difference_amount")), + reverse_dr_or_cr: abs(arg.get("difference_amount")), } ) From 389cadf15715b1483986297b38ec2dbb268d2b26 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Tue, 27 Jun 2023 11:16:52 +0530 Subject: [PATCH 029/101] refactor(test): assert Exc journal when reconciling Journa to invoic --- .../test_payment_reconciliation.py | 16 +++++++++++++--- erpnext/controllers/accounts_controller.py | 2 +- 2 files changed, 14 insertions(+), 4 deletions(-) diff --git a/erpnext/accounts/doctype/payment_reconciliation/test_payment_reconciliation.py b/erpnext/accounts/doctype/payment_reconciliation/test_payment_reconciliation.py index 2ac7df0e39..1d843abde1 100644 --- a/erpnext/accounts/doctype/payment_reconciliation/test_payment_reconciliation.py +++ b/erpnext/accounts/doctype/payment_reconciliation/test_payment_reconciliation.py @@ -686,14 +686,24 @@ class TestPaymentReconciliation(FrappeTestCase): # Check if difference journal entry gets generated for difference amount after reconciliation pr.reconcile() - total_debit_amount = frappe.db.get_all( + total_credit_amount = frappe.db.get_all( "Journal Entry Account", {"account": self.debtors_eur, "docstatus": 1, "reference_name": si.name}, - "sum(debit) as amount", + "sum(credit) as amount", group_by="reference_name", )[0].amount - self.assertEqual(flt(total_debit_amount, 2), -500) + # total credit includes the exchange gain/loss amount + self.assertEqual(flt(total_credit_amount, 2), 8500) + + jea_parent = frappe.db.get_all( + "Journal Entry Account", + filters={"account": self.debtors_eur, "docstatus": 1, "reference_name": si.name, "credit": 500}, + fields=["parent"], + )[0] + self.assertEqual( + frappe.db.get_value("Journal Entry", jea_parent.parent, "voucher_type"), "Exchange Gain Or Loss" + ) def test_difference_amount_via_payment_entry(self): # Make Sale Invoice diff --git a/erpnext/controllers/accounts_controller.py b/erpnext/controllers/accounts_controller.py index 2548bdc760..5abff417bf 100644 --- a/erpnext/controllers/accounts_controller.py +++ b/erpnext/controllers/accounts_controller.py @@ -1038,8 +1038,8 @@ class AccountsController(TransactionBase): # "reference_type": self.doctype, # "reference_name": self.name, # "reference_detail_no": arg.idx, - reverse_dr_or_cr + "_in_account_currency": abs(arg.get("difference_amount")), reverse_dr_or_cr: abs(arg.get("difference_amount")), + reverse_dr_or_cr + "_in_account_currency": 0, } ) From ee2d1fa36e24326aa9f5b11877139857ed3a6f21 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Tue, 27 Jun 2023 11:41:14 +0530 Subject: [PATCH 030/101] refactor(test): payment will have same exch rate - no gain/loss while making payment entry using reference to sales/purchase invoice, it herits the parent docs exchange rate. so, there will be no exchange gain/loss --- .../accounts/doctype/payment_request/test_payment_request.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/erpnext/accounts/doctype/payment_request/test_payment_request.py b/erpnext/accounts/doctype/payment_request/test_payment_request.py index e17a846dd8..feb2fdffc9 100644 --- a/erpnext/accounts/doctype/payment_request/test_payment_request.py +++ b/erpnext/accounts/doctype/payment_request/test_payment_request.py @@ -144,8 +144,7 @@ class TestPaymentRequest(unittest.TestCase): (d[0], d) for d in [ ["_Test Receivable USD - _TC", 0, 5000, si_usd.name], - [pr.payment_account, 6290.0, 0, None], - ["_Test Exchange Gain/Loss - _TC", 0, 1290, None], + [pr.payment_account, 5000.0, 0, None], ] ) From 78bc712756bc9d8966c22cbbce68e5058daa87db Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Tue, 27 Jun 2023 12:29:02 +0530 Subject: [PATCH 031/101] refactor: only post on base currency for exchange gain/loss --- erpnext/controllers/accounts_controller.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/controllers/accounts_controller.py b/erpnext/controllers/accounts_controller.py index 5abff417bf..f72ae81a53 100644 --- a/erpnext/controllers/accounts_controller.py +++ b/erpnext/controllers/accounts_controller.py @@ -1150,7 +1150,7 @@ class AccountsController(TransactionBase): "reference_type": self.doctype, "reference_name": self.name, "reference_detail_no": d.idx, - reverse_dr_or_cr + "_in_account_currency": abs(d.exchange_gain_loss), + reverse_dr_or_cr + "_in_account_currency": 0, reverse_dr_or_cr: abs(d.exchange_gain_loss), } ) From 5b06bd1af4197b0c6ab8714c65d8f7a578499163 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Tue, 27 Jun 2023 12:42:07 +0530 Subject: [PATCH 032/101] refactor(test): exc gain/loss journal for advance in purchase invoice --- .../purchase_invoice/test_purchase_invoice.py | 63 +++++++++++++++++-- 1 file changed, 59 insertions(+), 4 deletions(-) diff --git a/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py b/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py index 8c96480478..974c881306 100644 --- a/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py +++ b/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py @@ -1273,10 +1273,11 @@ class TestPurchaseInvoice(unittest.TestCase, StockTestMixin): pi.save() pi.submit() + creditors_account = pi.credit_to + expected_gle = [ ["_Test Account Cost for Goods Sold - _TC", 37500.0], - ["_Test Payable USD - _TC", -35000.0], - ["Exchange Gain/Loss - _TC", -2500.0], + ["_Test Payable USD - _TC", -37500.0], ] gl_entries = frappe.db.sql( @@ -1293,6 +1294,31 @@ class TestPurchaseInvoice(unittest.TestCase, StockTestMixin): self.assertEqual(expected_gle[i][0], gle.account) self.assertEqual(expected_gle[i][1], gle.balance) + pi.reload() + self.assertEqual(pi.outstanding_amount, 0) + + total_debit_amount = frappe.db.get_all( + "Journal Entry Account", + {"account": creditors_account, "docstatus": 1, "reference_name": pi.name}, + "sum(debit) as amount", + group_by="reference_name", + )[0].amount + self.assertEqual(flt(total_debit_amount, 2), 2500) + jea_parent = frappe.db.get_all( + "Journal Entry Account", + filters={ + "account": creditors_account, + "docstatus": 1, + "reference_name": pi.name, + "debit": 2500, + "debit_in_account_currency": 0, + }, + fields=["parent"], + )[0] + self.assertEqual( + frappe.db.get_value("Journal Entry", jea_parent.parent, "voucher_type"), "Exchange Gain Or Loss" + ) + pi_2 = make_purchase_invoice( supplier="_Test Supplier USD", currency="USD", @@ -1317,10 +1343,12 @@ class TestPurchaseInvoice(unittest.TestCase, StockTestMixin): pi_2.save() pi_2.submit() + pi_2.reload() + self.assertEqual(pi_2.outstanding_amount, 0) + expected_gle = [ ["_Test Account Cost for Goods Sold - _TC", 36500.0], - ["_Test Payable USD - _TC", -35000.0], - ["Exchange Gain/Loss - _TC", -1500.0], + ["_Test Payable USD - _TC", -36500.0], ] gl_entries = frappe.db.sql( @@ -1351,12 +1379,39 @@ class TestPurchaseInvoice(unittest.TestCase, StockTestMixin): self.assertEqual(expected_gle[i][0], gle.account) self.assertEqual(expected_gle[i][1], gle.balance) + total_debit_amount = frappe.db.get_all( + "Journal Entry Account", + {"account": creditors_account, "docstatus": 1, "reference_name": pi_2.name}, + "sum(debit) as amount", + group_by="reference_name", + )[0].amount + self.assertEqual(flt(total_debit_amount, 2), 1500) + jea_parent_2 = frappe.db.get_all( + "Journal Entry Account", + filters={ + "account": creditors_account, + "docstatus": 1, + "reference_name": pi_2.name, + "debit": 1500, + "debit_in_account_currency": 0, + }, + fields=["parent"], + )[0] + self.assertEqual( + frappe.db.get_value("Journal Entry", jea_parent_2.parent, "voucher_type"), + "Exchange Gain Or Loss", + ) + pi.reload() pi.cancel() + self.assertEqual(frappe.db.get_value("Journal Entry", jea_parent.parent, "docstatus"), 2) + pi_2.reload() pi_2.cancel() + self.assertEqual(frappe.db.get_value("Journal Entry", jea_parent_2.parent, "docstatus"), 2) + pay.reload() pay.cancel() From 72bc5b3a11528611db8a322d68c0ecc422b570c6 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Tue, 27 Jun 2023 16:11:03 +0530 Subject: [PATCH 033/101] refactor(test): difference amount no updated for exchange gain/loss --- erpnext/accounts/test/test_utils.py | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/erpnext/accounts/test/test_utils.py b/erpnext/accounts/test/test_utils.py index 3aca60eae5..3cb5e42e7a 100644 --- a/erpnext/accounts/test/test_utils.py +++ b/erpnext/accounts/test/test_utils.py @@ -80,18 +80,27 @@ class TestUtils(unittest.TestCase): item = make_item().name purchase_invoice = make_purchase_invoice( - item=item, supplier="_Test Supplier USD", currency="USD", conversion_rate=82.32 + item=item, supplier="_Test Supplier USD", currency="USD", conversion_rate=82.32, do_not_submit=1 ) + purchase_invoice.credit_to = "_Test Payable USD - _TC" purchase_invoice.submit() payment_entry = get_payment_entry(purchase_invoice.doctype, purchase_invoice.name) - payment_entry.target_exchange_rate = 62.9 payment_entry.paid_amount = 15725 payment_entry.deductions = [] - payment_entry.insert() + payment_entry.save() + + # below is the difference between base_received_amount and base_paid_amount + self.assertEqual(payment_entry.difference_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) - self.assertEqual(payment_entry.difference_amount, -4855.00) payment_entry.references = [] + self.assertEqual(payment_entry.difference_amount, 0.0) payment_entry.submit() payment_reconciliation = frappe.new_doc("Payment Reconciliation") From 1bcb728c850c67f3e479eb402ce1296dc215496b Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Tue, 27 Jun 2023 16:57:38 +0530 Subject: [PATCH 034/101] refactor: remove call for setting deductions in payment entry --- erpnext/accounts/utils.py | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/erpnext/accounts/utils.py b/erpnext/accounts/utils.py index cfd0133700..49a6367784 100644 --- a/erpnext/accounts/utils.py +++ b/erpnext/accounts/utils.py @@ -639,17 +639,6 @@ def update_reference_in_payment_entry( new_row.docstatus = 1 new_row.update(reference_details) - if d.difference_amount and d.difference_account: - account_details = { - "account": d.difference_account, - "cost_center": payment_entry.cost_center - or frappe.get_cached_value("Company", payment_entry.company, "cost_center"), - } - if d.difference_amount: - account_details["amount"] = d.difference_amount - - payment_entry.set_gain_or_loss(account_details=account_details) - payment_entry.flags.ignore_validate_update_after_submit = True payment_entry.setup_party_account_field() payment_entry.set_missing_values() From cd42b268391113d5d5b10d75a6e2562736e43aae Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Mon, 10 Jul 2023 15:28:10 +0530 Subject: [PATCH 035/101] chore: code cleanup --- .../payment_reconciliation/payment_reconciliation.py | 6 ------ erpnext/controllers/accounts_controller.py | 8 ++++++-- 2 files changed, 6 insertions(+), 8 deletions(-) diff --git a/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py b/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py index df777f03be..d574cd79b8 100644 --- a/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py +++ b/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py @@ -363,12 +363,6 @@ class PaymentReconciliation(Document): payment_details = self.get_payment_details(row, dr_or_cr) reconciled_entry.append(payment_details) - # if payment_details.difference_amount and row.reference_type not in [ - # "Sales Invoice", - # "Purchase Invoice", - # ]: - # self.make_difference_entry(payment_details) - if entry_list: reconcile_against_document(entry_list, skip_ref_details_update_for_pe) diff --git a/erpnext/controllers/accounts_controller.py b/erpnext/controllers/accounts_controller.py index f72ae81a53..611eca621e 100644 --- a/erpnext/controllers/accounts_controller.py +++ b/erpnext/controllers/accounts_controller.py @@ -972,9 +972,12 @@ class AccountsController(TransactionBase): """ Make Exchange Gain/Loss journal for Invoices and Payments """ - # Cancelling existing exchange gain/loss journals is handled in on_cancel event in accounts/utils.py + # Cancelling existing exchange gain/loss journals is handled during the `on_cancel` event. + # see accounts/utils.py:cancel_exchange_gain_loss_journal() if self.docstatus == 1: if self.get("doctype") == "Journal Entry": + # 'args' is populated with exchange gain/loss account and the amount to be booked. + # These are generated by Sales/Purchase Invoice during reconciliation and advance allocation. if args: for arg in args: if arg.get("difference_amount", 0) != 0 and arg.get("difference_account"): @@ -1035,6 +1038,7 @@ class AccountsController(TransactionBase): "exchange_rate": 1, "cost_center": erpnext.get_default_cost_center(self.company), # TODO: figure out a way to pass reference + # throws 'Journal Entry doesn't have {account} or doesn't have matched account' # "reference_type": self.doctype, # "reference_name": self.name, # "reference_detail_no": arg.idx, @@ -1049,7 +1053,7 @@ class AccountsController(TransactionBase): journal_entry.submit() if self.get("doctype") == "Payment Entry": - # For Payment Entry, exchange_gain_loss field in the `reference` table is the trigger for journal creation + # For Payment Entry, exchange_gain_loss field in the `references` table is the trigger for journal creation gain_loss_to_book = [x for x in self.references if x.exchange_gain_loss != 0] booked = [] if gain_loss_to_book: From f119a1e11553a0357f937bd23a397757f3f5b54f Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Tue, 11 Jul 2023 12:04:13 +0530 Subject: [PATCH 036/101] refactor: linkage between journal as payment and gain/loss journal --- erpnext/accounts/doctype/gl_entry/gl_entry.py | 7 +++++++ .../accounts/doctype/journal_entry/journal_entry.py | 11 ++++++----- erpnext/controllers/accounts_controller.py | 6 +++--- 3 files changed, 16 insertions(+), 8 deletions(-) diff --git a/erpnext/accounts/doctype/gl_entry/gl_entry.py b/erpnext/accounts/doctype/gl_entry/gl_entry.py index f07a4fa3bc..7af40c46cb 100644 --- a/erpnext/accounts/doctype/gl_entry/gl_entry.py +++ b/erpnext/accounts/doctype/gl_entry/gl_entry.py @@ -58,6 +58,13 @@ class GLEntry(Document): validate_balance_type(self.account, adv_adj) validate_frozen_account(self.account, adv_adj) + if ( + self.voucher_type == "Journal Entry" + and frappe.get_cached_value("Journal Entry", self.voucher_no, "voucher_type") + == "Exchange Gain Or Loss" + ): + return + if frappe.get_cached_value("Account", self.account, "account_type") not in [ "Receivable", "Payable", diff --git a/erpnext/accounts/doctype/journal_entry/journal_entry.py b/erpnext/accounts/doctype/journal_entry/journal_entry.py index ea4a2d4b19..0115fd7f7a 100644 --- a/erpnext/accounts/doctype/journal_entry/journal_entry.py +++ b/erpnext/accounts/doctype/journal_entry/journal_entry.py @@ -499,11 +499,12 @@ class JournalEntry(AccountsController): ) if not against_entries: - frappe.throw( - _( - "Journal Entry {0} does not have account {1} or already matched against other voucher" - ).format(d.reference_name, d.account) - ) + if self.voucher_type != "Exchange Gain Or Loss": + frappe.throw( + _( + "Journal Entry {0} does not have account {1} or already matched against other voucher" + ).format(d.reference_name, d.account) + ) else: dr_or_cr = "debit" if d.credit > 0 else "credit" valid = False diff --git a/erpnext/controllers/accounts_controller.py b/erpnext/controllers/accounts_controller.py index 611eca621e..0b3b41de9f 100644 --- a/erpnext/controllers/accounts_controller.py +++ b/erpnext/controllers/accounts_controller.py @@ -1039,9 +1039,9 @@ class AccountsController(TransactionBase): "cost_center": erpnext.get_default_cost_center(self.company), # TODO: figure out a way to pass reference # throws 'Journal Entry doesn't have {account} or doesn't have matched account' - # "reference_type": self.doctype, - # "reference_name": self.name, - # "reference_detail_no": arg.idx, + "reference_type": self.doctype, + "reference_name": self.name, + "reference_detail_no": arg.idx, reverse_dr_or_cr: abs(arg.get("difference_amount")), reverse_dr_or_cr + "_in_account_currency": 0, } From 6e18bb6456b3a7a2cbad89b86dcc124978337e4d Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Tue, 11 Jul 2023 12:21:10 +0530 Subject: [PATCH 037/101] refactor: cancel gain/loss JE on Journal as payment cancellation --- .../doctype/journal_entry/journal_entry.py | 5 ++- erpnext/accounts/utils.py | 2 +- erpnext/controllers/accounts_controller.py | 11 +++--- .../tests/test_accounts_controller.py | 34 ++++++++++++++++--- 4 files changed, 39 insertions(+), 13 deletions(-) diff --git a/erpnext/accounts/doctype/journal_entry/journal_entry.py b/erpnext/accounts/doctype/journal_entry/journal_entry.py index 0115fd7f7a..e6b8b5d281 100644 --- a/erpnext/accounts/doctype/journal_entry/journal_entry.py +++ b/erpnext/accounts/doctype/journal_entry/journal_entry.py @@ -87,9 +87,8 @@ class JournalEntry(AccountsController): self.update_invoice_discounting() def on_cancel(self): - from erpnext.accounts.utils import unlink_ref_doc_from_payment_entries - - unlink_ref_doc_from_payment_entries(self) + # References for this Journal are removed on the `on_cancel` event in accounts_controller + super(JournalEntry, self).on_cancel() self.ignore_linked_doctypes = ( "GL Entry", "Stock Ledger Entry", diff --git a/erpnext/accounts/utils.py b/erpnext/accounts/utils.py index 49a6367784..53d9e21c35 100644 --- a/erpnext/accounts/utils.py +++ b/erpnext/accounts/utils.py @@ -655,7 +655,7 @@ def cancel_exchange_gain_loss_journal(parent_doc: dict | object) -> None: """ Cancel Exchange Gain/Loss for Sales/Purchase Invoice, if they have any. """ - if parent_doc.doctype in ["Sales Invoice", "Purchase Invoice", "Payment Entry"]: + if parent_doc.doctype in ["Sales Invoice", "Purchase Invoice", "Payment Entry", "Journal Entry"]: journals = frappe.db.get_all( "Journal Entry Account", filters={ diff --git a/erpnext/controllers/accounts_controller.py b/erpnext/controllers/accounts_controller.py index 0b3b41de9f..a126dfe6b3 100644 --- a/erpnext/controllers/accounts_controller.py +++ b/erpnext/controllers/accounts_controller.py @@ -991,10 +991,11 @@ class AccountsController(TransactionBase): party_account_currency = frappe.get_cached_value( "Account", party_account, "account_currency" ) - dr_or_cr = "debit" if arg.get("difference_amount") > 0 else "credit" - if arg.reference_doctype == "Purchase Invoice": - dr_or_cr = "debit" if dr_or_cr == "credit" else "credit" + dr_or_cr = "debit" if arg.get("party_type") == "Customer" else "credit" + + # if arg.reference_doctype == "Purchase Invoice": + # dr_or_cr = "debit" if dr_or_cr == "credit" else "credit" reverse_dr_or_cr = "debit" if dr_or_cr == "credit" else "credit" @@ -1038,6 +1039,7 @@ class AccountsController(TransactionBase): "exchange_rate": 1, "cost_center": erpnext.get_default_cost_center(self.company), # TODO: figure out a way to pass reference + # TODO: add reference_detail_no field in payment ledger # throws 'Journal Entry doesn't have {account} or doesn't have matched account' "reference_type": self.doctype, "reference_name": self.name, @@ -1163,6 +1165,7 @@ class AccountsController(TransactionBase): journal_entry.save() journal_entry.submit() + # frappe.throw("stopping...") def update_against_document_in_jv(self): """ @@ -1229,7 +1232,7 @@ class AccountsController(TransactionBase): unlink_ref_doc_from_payment_entries, ) - if self.doctype in ["Sales Invoice", "Purchase Invoice", "Payment Entry"]: + if self.doctype in ["Sales Invoice", "Purchase Invoice", "Payment Entry", "Journal Entry"]: # Cancel Exchange Gain/Loss Journal before unlinking cancel_exchange_gain_loss_journal(self) diff --git a/erpnext/controllers/tests/test_accounts_controller.py b/erpnext/controllers/tests/test_accounts_controller.py index 31aa857c8f..28a569b524 100644 --- a/erpnext/controllers/tests/test_accounts_controller.py +++ b/erpnext/controllers/tests/test_accounts_controller.py @@ -11,6 +11,7 @@ from frappe.utils import add_days, flt, nowdate from erpnext import get_default_cost_center from erpnext.accounts.doctype.payment_entry.payment_entry import get_payment_entry from erpnext.accounts.doctype.payment_entry.test_payment_entry import create_payment_entry +from erpnext.accounts.doctype.purchase_invoice.test_purchase_invoice import make_purchase_invoice from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice from erpnext.accounts.party import get_party_account from erpnext.stock.doctype.item.test_item import create_item @@ -20,7 +21,7 @@ def make_customer(customer_name, currency=None): if not frappe.db.exists("Customer", customer_name): customer = frappe.new_doc("Customer") customer.customer_name = customer_name - customer.type = "Individual" + customer.customer_type = "Individual" if currency: customer.default_currency = currency @@ -30,7 +31,22 @@ def make_customer(customer_name, currency=None): return customer_name -class TestAccountsController(FrappeTestCase): +def make_supplier(supplier_name, currency=None): + if not frappe.db.exists("Supplier", supplier_name): + supplier = frappe.new_doc("Supplier") + supplier.supplier_name = supplier_name + supplier.supplier_type = "Individual" + + if currency: + supplier.default_currency = currency + supplier.save() + return supplier.name + else: + return supplier_name + + +# class TestAccountsController(FrappeTestCase): +class TestAccountsController(unittest.TestCase): """ Test Exchange Gain/Loss booking on various scenarios """ @@ -39,11 +55,12 @@ class TestAccountsController(FrappeTestCase): self.create_company() self.create_account() self.create_item() - self.create_customer() + self.create_parties() self.clear_old_entries() def tearDown(self): - frappe.db.rollback() + # frappe.db.rollback() + pass def create_company(self): company_name = "_Test Company MC" @@ -80,9 +97,16 @@ class TestAccountsController(FrappeTestCase): ) self.item = item if isinstance(item, str) else item.item_code + def create_parties(self): + self.create_customer() + self.create_supplier() + def create_customer(self): self.customer = make_customer("_Test MC Customer USD", "USD") + def create_supplier(self): + self.supplier = make_supplier("_Test MC Supplier USD", "USD") + def create_account(self): account_name = "Debtors USD" if not frappe.db.get_value( @@ -215,7 +239,7 @@ class TestAccountsController(FrappeTestCase): return journals def test_01_payment_against_invoice(self): - # Invoice in Foreign Currency + # Sales Invoice in Foreign Currency si = self.create_sales_invoice(qty=1, rate=1) # Payment pe = self.create_payment_entry(amount=1, source_exc_rate=75).save() From 73cc1ba654f39d81b7e1d9769ef6a0a8ceb689fe Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Tue, 11 Jul 2023 16:34:20 +0530 Subject: [PATCH 038/101] refactor: assert payment ledger outstanding in both currencies --- .../tests/test_accounts_controller.py | 249 +++++++++++------- 1 file changed, 158 insertions(+), 91 deletions(-) diff --git a/erpnext/controllers/tests/test_accounts_controller.py b/erpnext/controllers/tests/test_accounts_controller.py index 28a569b524..fc30c4b8cd 100644 --- a/erpnext/controllers/tests/test_accounts_controller.py +++ b/erpnext/controllers/tests/test_accounts_controller.py @@ -5,6 +5,7 @@ import unittest import frappe from frappe import qb +from frappe.query_builder.functions import Sum from frappe.tests.utils import FrappeTestCase, change_settings from frappe.utils import add_days, flt, nowdate @@ -48,7 +49,15 @@ def make_supplier(supplier_name, currency=None): # class TestAccountsController(FrappeTestCase): class TestAccountsController(unittest.TestCase): """ - Test Exchange Gain/Loss booking on various scenarios + Test Exchange Gain/Loss booking on various scenarios. + Test Cases are numbered for better readbility + + 10 series - Sales Invoice against Payment Entries + 20 series - Sales Invoice against Journals + 30 series - Sales Invoice against Credit Notes + 40 series - Purchase Invoice against Payment Entries + 50 series - Purchase Invoice against Journals + 60 series - Purchase Invoice against Debit Notes """ def setUp(self): @@ -130,7 +139,13 @@ class TestAccountsController(unittest.TestCase): self.debtors_usd = acc.name def create_sales_invoice( - self, qty=1, rate=1, posting_date=nowdate(), do_not_save=False, do_not_submit=False + self, + qty=1, + rate=1, + conversion_rate=80, + posting_date=nowdate(), + do_not_save=False, + do_not_submit=False, ): """ Helper function to populate default values in sales invoice @@ -148,7 +163,7 @@ class TestAccountsController(unittest.TestCase): parent_cost_center=self.cost_center, update_stock=0, currency="USD", - conversion_rate=80, + conversion_rate=conversion_rate, is_pos=0, is_return=0, return_against=None, @@ -238,96 +253,140 @@ class TestAccountsController(unittest.TestCase): ) return journals - def test_01_payment_against_invoice(self): - # Sales Invoice in Foreign Currency - si = self.create_sales_invoice(qty=1, rate=1) - # Payment - pe = self.create_payment_entry(amount=1, source_exc_rate=75).save() - pe.append( - "references", - {"reference_doctype": si.doctype, "reference_name": si.name, "allocated_amount": 1}, + def assert_ledger_outstanding( + self, + voucher_type: str, + voucher_no: str, + outstanding: float, + outstanding_in_account_currency: float, + ) -> None: + """ + Assert outstanding amount based on ledger on both company/base currency and account currency + """ + + ple = qb.DocType("Payment Ledger Entry") + current_outstanding = ( + qb.from_(ple) + .select( + Sum(ple.amount).as_("outstanding"), + Sum(ple.amount_in_account_currency).as_("outstanding_in_account_currency"), + ) + .where( + (ple.against_voucher_type == voucher_type) + & (ple.against_voucher_no == voucher_no) + & (ple.delinked == 0) + ) + .run(as_dict=True)[0] + ) + self.assertEqual(outstanding, current_outstanding.outstanding) + self.assertEqual( + outstanding_in_account_currency, current_outstanding.outstanding_in_account_currency ) - pe = pe.save().submit() - si.reload() - self.assertEqual(si.outstanding_amount, 0) + def test_10_payment_against_sales_invoice(self): + # Sales Invoice in Foreign Currency + rate = 80 + rate_in_account_currency = 1 - # Exchange Gain/Loss Journal should've been created. - exc_je_for_si = self.get_journals_for(si.doctype, si.name) - exc_je_for_pe = self.get_journals_for(pe.doctype, pe.name) + si = self.create_sales_invoice(qty=1, rate=rate_in_account_currency) - self.assertNotEqual(exc_je_for_si, []) - self.assertEqual(len(exc_je_for_si), 1) - self.assertEqual(len(exc_je_for_pe), 1) - self.assertEqual(exc_je_for_si[0], exc_je_for_pe[0]) + # Test payments with different exchange rates + for exc_rate in [75.9, 83.1, 80.01]: + with self.subTest(exc_rate=exc_rate): + pe = self.create_payment_entry(amount=1, source_exc_rate=exc_rate).save() + pe.append( + "references", + {"reference_doctype": si.doctype, "reference_name": si.name, "allocated_amount": 1}, + ) + pe = pe.save().submit() - # Cancel Payment - pe.cancel() + # Outstanding in both currencies should be '0' + si.reload() + self.assertEqual(si.outstanding_amount, 0) + self.assert_ledger_outstanding(si.doctype, si.name, 0.0, 0.0) - si.reload() - self.assertEqual(si.outstanding_amount, 1) + # Exchange Gain/Loss Journal should've been created. + exc_je_for_si = self.get_journals_for(si.doctype, si.name) + exc_je_for_pe = self.get_journals_for(pe.doctype, pe.name) + self.assertNotEqual(exc_je_for_si, []) + self.assertEqual(len(exc_je_for_si), 1) + self.assertEqual(len(exc_je_for_pe), 1) + self.assertEqual(exc_je_for_si[0], exc_je_for_pe[0]) - # Exchange Gain/Loss Journal should've been cancelled - exc_je_for_si = self.get_journals_for(si.doctype, si.name) - exc_je_for_pe = self.get_journals_for(pe.doctype, pe.name) + # Cancel Payment + pe.cancel() - self.assertEqual(exc_je_for_si, []) - self.assertEqual(exc_je_for_pe, []) + # outstanding should be same as grand total + si.reload() + self.assertEqual(si.outstanding_amount, rate_in_account_currency) + self.assert_ledger_outstanding(si.doctype, si.name, rate, rate_in_account_currency) - def test_02_advance_against_invoice(self): + # Exchange Gain/Loss Journal should've been cancelled + exc_je_for_si = self.get_journals_for(si.doctype, si.name) + exc_je_for_pe = self.get_journals_for(pe.doctype, pe.name) + self.assertEqual(exc_je_for_si, []) + self.assertEqual(exc_je_for_pe, []) + + def test_11_advance_against_sales_invoice(self): # Advance Payment adv = self.create_payment_entry(amount=1, source_exc_rate=85).save().submit() adv.reload() - # Invoice in Foreign Currency - si = self.create_sales_invoice(qty=1, rate=1, do_not_submit=True) - si.append( - "advances", - { - "doctype": "Sales Invoice Advance", - "reference_type": adv.doctype, - "reference_name": adv.name, - "advance_amount": 1, - "allocated_amount": 1, - "ref_exchange_rate": 85, - "remarks": "Test", - }, - ) - si = si.save() - si = si.submit() + # Sales Invoices in different exchange rates + for exc_rate in [75.9, 83.1, 80.01]: + with self.subTest(exc_rate=exc_rate): + si = self.create_sales_invoice(qty=1, conversion_rate=exc_rate, rate=1, do_not_submit=True) + si.append( + "advances", + { + "doctype": "Sales Invoice Advance", + "reference_type": adv.doctype, + "reference_name": adv.name, + "advance_amount": 1, + "allocated_amount": 1, + "ref_exchange_rate": 85, + "remarks": "Test", + }, + ) + si = si.save() + si = si.submit() - adv.reload() - self.assertEqual(si.outstanding_amount, 0) + # Outstanding in both currencies should be '0' + adv.reload() + self.assertEqual(si.outstanding_amount, 0) + self.assert_ledger_outstanding(si.doctype, si.name, 0.0, 0.0) - # Exchange Gain/Loss Journal should've been created. - exc_je_for_si = self.get_journals_for(si.doctype, si.name) - exc_je_for_adv = self.get_journals_for(adv.doctype, adv.name) + # Exchange Gain/Loss Journal should've been created. + exc_je_for_si = self.get_journals_for(si.doctype, si.name) + exc_je_for_adv = self.get_journals_for(adv.doctype, adv.name) + self.assertNotEqual(exc_je_for_si, []) + self.assertEqual(len(exc_je_for_si), 1) + self.assertEqual(len(exc_je_for_adv), 1) + self.assertEqual(exc_je_for_si, exc_je_for_adv) - self.assertNotEqual(exc_je_for_si, []) - self.assertEqual(len(exc_je_for_si), 1) - self.assertEqual(len(exc_je_for_adv), 1) - self.assertEqual(exc_je_for_si, exc_je_for_adv) + # Cancel Invoice + si.cancel() - # Cancel Invoice - si.cancel() + # Exchange Gain/Loss Journal should've been cancelled + exc_je_for_si = self.get_journals_for(si.doctype, si.name) + exc_je_for_adv = self.get_journals_for(adv.doctype, adv.name) + self.assertEqual(exc_je_for_si, []) + self.assertEqual(exc_je_for_adv, []) - # Exchange Gain/Loss Journal should've been cancelled - exc_je_for_si = self.get_journals_for(si.doctype, si.name) - exc_je_for_adv = self.get_journals_for(adv.doctype, adv.name) - - self.assertEqual(exc_je_for_si, []) - self.assertEqual(exc_je_for_adv, []) - - def test_03_partial_advance_and_payment_for_invoice(self): + def test_12_partial_advance_and_payment_for_sales_invoice(self): """ - Invoice with partial advance payment, and a normal payment + Sales invoice with partial advance payment, and a normal payment reconciled """ # Partial Advance adv = self.create_payment_entry(amount=1, source_exc_rate=85).save().submit() adv.reload() - # Invoice in Foreign Currency linked with advance - si = self.create_sales_invoice(qty=2, rate=1, do_not_submit=True) + # sales invoice with advance(partial amount) + rate = 80 + rate_in_account_currency = 1 + si = self.create_sales_invoice( + qty=2, conversion_rate=80, rate=rate_in_account_currency, do_not_submit=True + ) si.append( "advances", { @@ -343,19 +402,20 @@ class TestAccountsController(unittest.TestCase): si = si.save() si = si.submit() + # Outstanding should be there in both currencies si.reload() - self.assertEqual(si.outstanding_amount, 1) + self.assertEqual(si.outstanding_amount, 1) # account currency + self.assert_ledger_outstanding(si.doctype, si.name, 80.0, 1.0) # Exchange Gain/Loss Journal should've been created for the partial advance exc_je_for_si = self.get_journals_for(si.doctype, si.name) exc_je_for_adv = self.get_journals_for(adv.doctype, adv.name) - self.assertNotEqual(exc_je_for_si, []) self.assertEqual(len(exc_je_for_si), 1) self.assertEqual(len(exc_je_for_adv), 1) self.assertEqual(exc_je_for_si, exc_je_for_adv) - # Payment + # Payment for remaining amount pe = self.create_payment_entry(amount=1, source_exc_rate=75).save() pe.append( "references", @@ -363,13 +423,14 @@ class TestAccountsController(unittest.TestCase): ) pe = pe.save().submit() + # Outstanding in both currencies should be '0' si.reload() self.assertEqual(si.outstanding_amount, 0) + self.assert_ledger_outstanding(si.doctype, si.name, 0.0, 0.0) # Exchange Gain/Loss Journal should've been created for the payment exc_je_for_si = self.get_journals_for(si.doctype, si.name) exc_je_for_pe = self.get_journals_for(pe.doctype, pe.name) - self.assertNotEqual(exc_je_for_si, []) # There should be 2 JE's now. One for the advance and one for the payment self.assertEqual(len(exc_je_for_si), 2) @@ -384,21 +445,20 @@ class TestAccountsController(unittest.TestCase): exc_je_for_si = self.get_journals_for(si.doctype, si.name) exc_je_for_pe = self.get_journals_for(pe.doctype, pe.name) exc_je_for_adv = self.get_journals_for(adv.doctype, adv.name) - self.assertEqual(exc_je_for_si, []) self.assertEqual(exc_je_for_pe, []) self.assertEqual(exc_je_for_adv, []) - def test_04_partial_advance_and_payment_for_invoice_with_cancellation(self): + def test_13_partial_advance_and_payment_for_invoice_with_cancellation(self): """ - Invoice with partial advance payment, and a normal payment. Cancel advance and payment. + Invoice with partial advance payment, and a normal payment. Then cancel advance and payment. """ # Partial Advance adv = self.create_payment_entry(amount=1, source_exc_rate=85).save().submit() adv.reload() - # Invoice in Foreign Currency linked with advance - si = self.create_sales_invoice(qty=2, rate=1, do_not_submit=True) + # invoice with advance(partial amount) + si = self.create_sales_invoice(qty=2, conversion_rate=80, rate=1, do_not_submit=True) si.append( "advances", { @@ -414,19 +474,20 @@ class TestAccountsController(unittest.TestCase): si = si.save() si = si.submit() + # Outstanding should be there in both currencies si.reload() - self.assertEqual(si.outstanding_amount, 1) + self.assertEqual(si.outstanding_amount, 1) # account currency + self.assert_ledger_outstanding(si.doctype, si.name, 80.0, 1.0) # Exchange Gain/Loss Journal should've been created for the partial advance exc_je_for_si = self.get_journals_for(si.doctype, si.name) exc_je_for_adv = self.get_journals_for(adv.doctype, adv.name) - self.assertNotEqual(exc_je_for_si, []) self.assertEqual(len(exc_je_for_si), 1) self.assertEqual(len(exc_je_for_adv), 1) self.assertEqual(exc_je_for_si, exc_je_for_adv) - # Payment + # Payment(remaining amount) pe = self.create_payment_entry(amount=1, source_exc_rate=75).save() pe.append( "references", @@ -434,13 +495,14 @@ class TestAccountsController(unittest.TestCase): ) pe = pe.save().submit() + # Outstanding should be '0' in both currencies si.reload() self.assertEqual(si.outstanding_amount, 0) + self.assert_ledger_outstanding(si.doctype, si.name, 0.0, 0.0) # Exchange Gain/Loss Journal should've been created for the payment exc_je_for_si = self.get_journals_for(si.doctype, si.name) exc_je_for_pe = self.get_journals_for(pe.doctype, pe.name) - self.assertNotEqual(exc_je_for_si, []) # There should be 2 JE's now. One for the advance and one for the payment self.assertEqual(len(exc_je_for_si), 2) @@ -450,21 +512,22 @@ class TestAccountsController(unittest.TestCase): adv.reload() adv.cancel() + # Outstanding should be there in both currencies, since advance is cancelled. si.reload() - self.assertEqual(si.outstanding_amount, 1) + self.assertEqual(si.outstanding_amount, 1) # account currency + self.assert_ledger_outstanding(si.doctype, si.name, 80.0, 1.0) exc_je_for_si = self.get_journals_for(si.doctype, si.name) exc_je_for_pe = self.get_journals_for(pe.doctype, pe.name) exc_je_for_adv = self.get_journals_for(adv.doctype, adv.name) - # Exchange Gain/Loss Journal for advance should been cancelled self.assertEqual(len(exc_je_for_si), 1) self.assertEqual(len(exc_je_for_pe), 1) self.assertEqual(exc_je_for_adv, []) - def test_05_same_payment_split_against_invoice(self): + def test_14_same_payment_split_against_invoice(self): # Invoice in Foreign Currency - si = self.create_sales_invoice(qty=2, rate=1) + si = self.create_sales_invoice(qty=2, conversion_rate=80, rate=1) # Payment pe = self.create_payment_entry(amount=2, source_exc_rate=75).save() pe.append( @@ -473,13 +536,14 @@ class TestAccountsController(unittest.TestCase): ) pe = pe.save().submit() + # There should be outstanding in both currencies si.reload() self.assertEqual(si.outstanding_amount, 1) + self.assert_ledger_outstanding(si.doctype, si.name, 80.0, 1.0) # Exchange Gain/Loss Journal should've been created. exc_je_for_si = self.get_journals_for(si.doctype, si.name) exc_je_for_pe = self.get_journals_for(pe.doctype, pe.name) - self.assertNotEqual(exc_je_for_si, []) self.assertEqual(len(exc_je_for_si), 1) self.assertEqual(len(exc_je_for_pe), 1) @@ -491,32 +555,35 @@ class TestAccountsController(unittest.TestCase): pr.party_type = "Customer" pr.party = self.customer pr.receivable_payable_account = self.debit_usd - pr.get_unreconciled_entries() self.assertEqual(len(pr.invoices), 1) self.assertEqual(len(pr.payments), 1) - - # Test exact payment allocation invoices = [x.as_dict() for x in pr.invoices] payments = [x.as_dict() for x in pr.payments] pr.allocate_entries(frappe._dict({"invoices": invoices, "payments": payments})) - pr.reconcile() self.assertEqual(len(pr.invoices), 0) self.assertEqual(len(pr.payments), 0) + # Exc gain/loss journal should have been creaetd for the reconciled amount exc_je_for_si = self.get_journals_for(si.doctype, si.name) exc_je_for_pe = self.get_journals_for(pe.doctype, pe.name) self.assertEqual(len(exc_je_for_si), 2) self.assertEqual(len(exc_je_for_pe), 2) self.assertEqual(exc_je_for_si, exc_je_for_pe) + # There should be no outstanding + si.reload() + self.assertEqual(si.outstanding_amount, 0) + self.assert_ledger_outstanding(si.doctype, si.name, 0.0, 0.0) + # Cancel Payment pe.reload() pe.cancel() si.reload() self.assertEqual(si.outstanding_amount, 2) + self.assert_ledger_outstanding(si.doctype, si.name, 160.0, 2.0) # Exchange Gain/Loss Journal should've been cancelled exc_je_for_si = self.get_journals_for(si.doctype, si.name) From 056724377206f9bdc3e7ac9894fabb0d93bd3176 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Wed, 12 Jul 2023 06:14:17 +0530 Subject: [PATCH 039/101] refactor: dr/cr logic for journals as payments --- erpnext/controllers/accounts_controller.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/erpnext/controllers/accounts_controller.py b/erpnext/controllers/accounts_controller.py index a126dfe6b3..dfc3114fa0 100644 --- a/erpnext/controllers/accounts_controller.py +++ b/erpnext/controllers/accounts_controller.py @@ -992,15 +992,14 @@ class AccountsController(TransactionBase): "Account", party_account, "account_currency" ) - dr_or_cr = "debit" if arg.get("party_type") == "Customer" else "credit" - - # if arg.reference_doctype == "Purchase Invoice": - # dr_or_cr = "debit" if dr_or_cr == "credit" else "credit" + if arg.get("difference_amount") > 0: + dr_or_cr = "debit" if arg.get("party_type") == "Customer" else "credit" + else: + dr_or_cr = "credit" if arg.get("party_type") == "Customer" else "debit" reverse_dr_or_cr = "debit" if dr_or_cr == "credit" else "credit" gain_loss_account = arg.get("difference_account") - if not gain_loss_account: frappe.throw( _("Please set default Exchange Gain/Loss Account in Company {}").format( From 5695d6a5a62e634536c51a22e045eb8281a29d9d Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Wed, 12 Jul 2023 06:46:59 +0530 Subject: [PATCH 040/101] refactor: unit tests for journals --- .../tests/test_accounts_controller.py | 83 +++++++++++++++++-- 1 file changed, 78 insertions(+), 5 deletions(-) diff --git a/erpnext/controllers/tests/test_accounts_controller.py b/erpnext/controllers/tests/test_accounts_controller.py index fc30c4b8cd..9e857f04c3 100644 --- a/erpnext/controllers/tests/test_accounts_controller.py +++ b/erpnext/controllers/tests/test_accounts_controller.py @@ -216,12 +216,21 @@ class TestAccountsController(unittest.TestCase): return pr def create_journal_entry( - self, acc1=None, acc2=None, amount=0, posting_date=None, cost_center=None + self, + acc1=None, + acc1_exc_rate=None, + acc2_exc_rate=None, + acc2=None, + acc1_amount=0, + acc2_amount=0, + posting_date=None, + cost_center=None, ): je = frappe.new_doc("Journal Entry") je.posting_date = posting_date or nowdate() je.company = self.company je.user_remark = "test" + je.multi_currency = True if not cost_center: cost_center = self.cost_center je.set( @@ -229,15 +238,21 @@ class TestAccountsController(unittest.TestCase): [ { "account": acc1, + "exchange_rate": acc1_exc_rate or 1, "cost_center": cost_center, - "debit_in_account_currency": amount if amount > 0 else 0, - "credit_in_account_currency": abs(amount) if amount < 0 else 0, + "debit_in_account_currency": acc1_amount if acc1_amount > 0 else 0, + "credit_in_account_currency": abs(acc1_amount) if acc1_amount < 0 else 0, + "debit": acc1_amount * acc1_exc_rate if acc1_amount > 0 else 0, + "credit": abs(acc1_amount * acc1_exc_rate) if acc1_amount < 0 else 0, }, { "account": acc2, + "exchange_rate": acc2_exc_rate or 1, "cost_center": cost_center, - "credit_in_account_currency": amount if amount > 0 else 0, - "debit_in_account_currency": abs(amount) if amount < 0 else 0, + "credit_in_account_currency": acc2_amount if acc2_amount > 0 else 0, + "debit_in_account_currency": abs(acc2_amount) if acc2_amount < 0 else 0, + "credit": acc2_amount * acc2_exc_rate if acc2_amount > 0 else 0, + "debit": abs(acc2_amount * acc2_exc_rate) if acc2_amount < 0 else 0, }, ], ) @@ -590,3 +605,61 @@ class TestAccountsController(unittest.TestCase): exc_je_for_pe = self.get_journals_for(pe.doctype, pe.name) self.assertEqual(exc_je_for_si, []) self.assertEqual(exc_je_for_pe, []) + + def test_21_journal_against_sales_invoice(self): + # Invoice in Foreign Currency + si = self.create_sales_invoice(qty=1, conversion_rate=80, rate=1) + # Payment + je = self.create_journal_entry( + acc1=self.debit_usd, + acc1_exc_rate=75, + acc2=self.cash, + acc1_amount=-1, + acc2_amount=-75, + acc2_exc_rate=1, + ) + je.accounts[0].party_type = "Customer" + je.accounts[0].party = self.customer + je = je.save().submit() + + # Reconcile the remaining amount + pr = self.create_payment_reconciliation() + # pr.receivable_payable_account = self.debit_usd + pr.get_unreconciled_entries() + self.assertEqual(len(pr.invoices), 1) + self.assertEqual(len(pr.payments), 1) + invoices = [x.as_dict() for x in pr.invoices] + payments = [x.as_dict() for x in pr.payments] + pr.allocate_entries(frappe._dict({"invoices": invoices, "payments": payments})) + pr.reconcile() + self.assertEqual(len(pr.invoices), 0) + self.assertEqual(len(pr.payments), 0) + + # There should be no outstanding in both currencies + si.reload() + self.assertEqual(si.outstanding_amount, 0) + self.assert_ledger_outstanding(si.doctype, si.name, 0.0, 0.0) + + # Exchange Gain/Loss Journal should've been created. + exc_je_for_si = self.get_journals_for(si.doctype, si.name) + exc_je_for_je = self.get_journals_for(je.doctype, je.name) + self.assertNotEqual(exc_je_for_si, []) + self.assertEqual( + len(exc_je_for_si), 2 + ) # payment also has reference. so, there are 2 journals referencing invoice + self.assertEqual(len(exc_je_for_je), 1) + self.assertIn(exc_je_for_je[0], exc_je_for_si) + + # Cancel Payment + je.reload() + je.cancel() + + si.reload() + self.assertEqual(si.outstanding_amount, 1) + self.assert_ledger_outstanding(si.doctype, si.name, 80.0, 1.0) + + # Exchange Gain/Loss Journal should've been cancelled + exc_je_for_si = self.get_journals_for(si.doctype, si.name) + exc_je_for_je = self.get_journals_for(je.doctype, je.name) + self.assertEqual(exc_je_for_si, []) + self.assertEqual(exc_je_for_je, []) From f4a65cccc48bd15fd732973030451c93629bc84b Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Fri, 14 Jul 2023 16:51:42 +0530 Subject: [PATCH 041/101] refactor: handle diff amount in various names --- erpnext/controllers/accounts_controller.py | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/erpnext/controllers/accounts_controller.py b/erpnext/controllers/accounts_controller.py index dfc3114fa0..2ce1eb8c15 100644 --- a/erpnext/controllers/accounts_controller.py +++ b/erpnext/controllers/accounts_controller.py @@ -980,7 +980,10 @@ class AccountsController(TransactionBase): # These are generated by Sales/Purchase Invoice during reconciliation and advance allocation. if args: for arg in args: - if arg.get("difference_amount", 0) != 0 and arg.get("difference_account"): + # Advance section uses `exchange_gain_loss` and reconciliation uses `difference_amount` + if ( + arg.get("difference_amount", 0) != 0 or arg.get("exchange_gain_loss", 0) != 0 + ) and arg.get("difference_account"): journal_entry = frappe.new_doc("Journal Entry") journal_entry.voucher_type = "Exchange Gain Or Loss" journal_entry.company = self.company @@ -992,7 +995,8 @@ class AccountsController(TransactionBase): "Account", party_account, "account_currency" ) - if arg.get("difference_amount") > 0: + difference_amount = arg.get("difference_amount") or arg.get("exchange_gain_loss") + if difference_amount > 0: dr_or_cr = "debit" if arg.get("party_type") == "Customer" else "credit" else: dr_or_cr = "credit" if arg.get("party_type") == "Customer" else "debit" @@ -1024,7 +1028,7 @@ class AccountsController(TransactionBase): "reference_type": arg.get("against_voucher_type"), "reference_name": arg.get("against_voucher"), "reference_detail_no": arg.get("idx"), - dr_or_cr: abs(arg.difference_amount), + dr_or_cr: abs(difference_amount), dr_or_cr + "_in_account_currency": 0, } ) @@ -1043,7 +1047,7 @@ class AccountsController(TransactionBase): "reference_type": self.doctype, "reference_name": self.name, "reference_detail_no": arg.idx, - reverse_dr_or_cr: abs(arg.get("difference_amount")), + reverse_dr_or_cr: abs(difference_amount), reverse_dr_or_cr + "_in_account_currency": 0, } ) From f3363e813a353363696169602bae5b0a36ae0376 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Sun, 16 Jul 2023 21:29:19 +0530 Subject: [PATCH 042/101] test: journals against sales invoice --- .../tests/test_accounts_controller.py | 264 +++++++++++++++++- 1 file changed, 263 insertions(+), 1 deletion(-) diff --git a/erpnext/controllers/tests/test_accounts_controller.py b/erpnext/controllers/tests/test_accounts_controller.py index 9e857f04c3..9a7326ea29 100644 --- a/erpnext/controllers/tests/test_accounts_controller.py +++ b/erpnext/controllers/tests/test_accounts_controller.py @@ -606,7 +606,7 @@ class TestAccountsController(unittest.TestCase): self.assertEqual(exc_je_for_si, []) self.assertEqual(exc_je_for_pe, []) - def test_21_journal_against_sales_invoice(self): + def test_20_journal_against_sales_invoice(self): # Invoice in Foreign Currency si = self.create_sales_invoice(qty=1, conversion_rate=80, rate=1) # Payment @@ -663,3 +663,265 @@ class TestAccountsController(unittest.TestCase): exc_je_for_je = self.get_journals_for(je.doctype, je.name) self.assertEqual(exc_je_for_si, []) self.assertEqual(exc_je_for_je, []) + + def test_21_advance_journal_against_sales_invoice(self): + # Advance Payment + adv_exc_rate = 80 + adv = self.create_journal_entry( + acc1=self.debit_usd, + acc1_exc_rate=adv_exc_rate, + acc2=self.cash, + acc1_amount=-1, + acc2_amount=adv_exc_rate * -1, + acc2_exc_rate=1, + ) + adv.accounts[0].party_type = "Customer" + adv.accounts[0].party = self.customer + adv.accounts[0].is_advance = "Yes" + adv = adv.save().submit() + adv.reload() + + # Sales Invoices in different exchange rates + for exc_rate in [75.9, 83.1, 80.01]: + with self.subTest(exc_rate=exc_rate): + si = self.create_sales_invoice(qty=1, conversion_rate=exc_rate, rate=1, do_not_submit=True) + si.append( + "advances", + { + "doctype": "Sales Invoice Advance", + "reference_type": adv.doctype, + "reference_name": adv.name, + "reference_row": adv.accounts[0].name, + "advance_amount": 1, + "allocated_amount": 1, + "ref_exchange_rate": adv_exc_rate, + "remarks": "Test", + }, + ) + si = si.save() + si = si.submit() + + # Outstanding in both currencies should be '0' + adv.reload() + self.assertEqual(si.outstanding_amount, 0) + self.assert_ledger_outstanding(si.doctype, si.name, 0.0, 0.0) + + # Exchange Gain/Loss Journal should've been created. + exc_je_for_si = [x for x in self.get_journals_for(si.doctype, si.name) if x.parent != adv.name] + exc_je_for_adv = self.get_journals_for(adv.doctype, adv.name) + self.assertNotEqual(exc_je_for_si, []) + self.assertEqual(len(exc_je_for_si), 1) + self.assertEqual(len(exc_je_for_adv), 1) + self.assertEqual(exc_je_for_si, exc_je_for_adv) + + # Cancel Invoice + si.cancel() + + # Exchange Gain/Loss Journal should've been cancelled + exc_je_for_si = self.get_journals_for(si.doctype, si.name) + exc_je_for_adv = self.get_journals_for(adv.doctype, adv.name) + self.assertEqual(exc_je_for_si, []) + self.assertEqual(exc_je_for_adv, []) + + def test_22_partial_advance_and_payment_for_invoice_with_cancellation(self): + """ + Invoice with partial advance payment as Journal, and a normal payment. Then cancel advance and payment. + """ + # Partial Advance + adv_exc_rate = 75 + adv = self.create_journal_entry( + acc1=self.debit_usd, + acc1_exc_rate=adv_exc_rate, + acc2=self.cash, + acc1_amount=-1, + acc2_amount=adv_exc_rate * -1, + acc2_exc_rate=1, + ) + adv.accounts[0].party_type = "Customer" + adv.accounts[0].party = self.customer + adv.accounts[0].is_advance = "Yes" + adv = adv.save().submit() + adv.reload() + + # invoice with advance(partial amount) + si = self.create_sales_invoice(qty=3, conversion_rate=80, rate=1, do_not_submit=True) + si.append( + "advances", + { + "doctype": "Sales Invoice Advance", + "reference_type": adv.doctype, + "reference_name": adv.name, + "reference_row": adv.accounts[0].name, + "advance_amount": 1, + "allocated_amount": 1, + "ref_exchange_rate": adv_exc_rate, + "remarks": "Test", + }, + ) + si = si.save() + si = si.submit() + + # Outstanding should be there in both currencies + si.reload() + self.assertEqual(si.outstanding_amount, 2) # account currency + self.assert_ledger_outstanding(si.doctype, si.name, 160.0, 2.0) + + # Exchange Gain/Loss Journal should've been created for the partial advance + exc_je_for_si = [x for x in self.get_journals_for(si.doctype, si.name) if x.parent != adv.name] + exc_je_for_adv = self.get_journals_for(adv.doctype, adv.name) + self.assertNotEqual(exc_je_for_si, []) + self.assertEqual(len(exc_je_for_si), 1) + self.assertEqual(len(exc_je_for_adv), 1) + self.assertEqual(exc_je_for_si, exc_je_for_adv) + + # Payment + adv2_exc_rate = 83 + pay = self.create_journal_entry( + acc1=self.debit_usd, + acc1_exc_rate=adv2_exc_rate, + acc2=self.cash, + acc1_amount=-2, + acc2_amount=adv2_exc_rate * -2, + acc2_exc_rate=1, + ) + pay.accounts[0].party_type = "Customer" + pay.accounts[0].party = self.customer + pay.accounts[0].is_advance = "Yes" + pay = pay.save().submit() + pay.reload() + + # Reconcile the remaining amount + pr = self.create_payment_reconciliation() + # pr.receivable_payable_account = self.debit_usd + pr.get_unreconciled_entries() + self.assertEqual(len(pr.invoices), 1) + self.assertEqual(len(pr.payments), 1) + invoices = [x.as_dict() for x in pr.invoices] + payments = [x.as_dict() for x in pr.payments] + pr.allocate_entries(frappe._dict({"invoices": invoices, "payments": payments})) + pr.reconcile() + self.assertEqual(len(pr.invoices), 0) + self.assertEqual(len(pr.payments), 0) + + # Outstanding should be '0' in both currencies + si.reload() + self.assertEqual(si.outstanding_amount, 0) + self.assert_ledger_outstanding(si.doctype, si.name, 0.0, 0.0) + + # Exchange Gain/Loss Journal should've been created for the payment + exc_je_for_si = [ + x + for x in self.get_journals_for(si.doctype, si.name) + if x.parent != adv.name and x.parent != pay.name + ] + exc_je_for_pe = self.get_journals_for(pay.doctype, pay.name) + self.assertNotEqual(exc_je_for_si, []) + # There should be 2 JE's now. One for the advance and one for the payment + self.assertEqual(len(exc_je_for_si), 2) + self.assertEqual(len(exc_je_for_pe), 1) + self.assertEqual(exc_je_for_si, exc_je_for_pe + exc_je_for_adv) + + adv.reload() + adv.cancel() + + # Outstanding should be there in both currencies, since advance is cancelled. + si.reload() + self.assertEqual(si.outstanding_amount, 1) # account currency + self.assert_ledger_outstanding(si.doctype, si.name, 80.0, 1.0) + + exc_je_for_si = [ + x + for x in self.get_journals_for(si.doctype, si.name) + if x.parent != adv.name and x.parent != pay.name + ] + exc_je_for_pe = self.get_journals_for(pay.doctype, pay.name) + exc_je_for_adv = self.get_journals_for(adv.doctype, adv.name) + # Exchange Gain/Loss Journal for advance should been cancelled + self.assertEqual(len(exc_je_for_si), 1) + self.assertEqual(len(exc_je_for_pe), 1) + self.assertEqual(exc_je_for_adv, []) + + def test_23_same_journal_split_against_single_invoice(self): + # Invoice in Foreign Currency + si = self.create_sales_invoice(qty=2, conversion_rate=80, rate=1) + # Payment + je = self.create_journal_entry( + acc1=self.debit_usd, + acc1_exc_rate=75, + acc2=self.cash, + acc1_amount=-2, + acc2_amount=-150, + acc2_exc_rate=1, + ) + je.accounts[0].party_type = "Customer" + je.accounts[0].party = self.customer + je = je.save().submit() + + # Reconcile the first half + pr = self.create_payment_reconciliation() + pr.get_unreconciled_entries() + self.assertEqual(len(pr.invoices), 1) + self.assertEqual(len(pr.payments), 1) + invoices = [x.as_dict() for x in pr.invoices] + payments = [x.as_dict() for x in pr.payments] + pr.allocate_entries(frappe._dict({"invoices": invoices, "payments": payments})) + difference_amount = pr.calculate_difference_on_allocation_change( + [x.as_dict() for x in pr.payments], [x.as_dict() for x in pr.invoices], 1 + ) + pr.allocation[0].allocated_amount = 1 + pr.allocation[0].difference_amount = difference_amount + pr.reconcile() + self.assertEqual(len(pr.invoices), 1) + self.assertEqual(len(pr.payments), 1) + + # There should be outstanding in both currencies + si.reload() + self.assertEqual(si.outstanding_amount, 1) + self.assert_ledger_outstanding(si.doctype, si.name, 80.0, 1.0) + + # Exchange Gain/Loss Journal should've been created. + exc_je_for_si = [x for x in self.get_journals_for(si.doctype, si.name) if x.parent != je.name] + exc_je_for_je = self.get_journals_for(je.doctype, je.name) + self.assertNotEqual(exc_je_for_si, []) + self.assertEqual(len(exc_je_for_si), 1) + self.assertEqual(len(exc_je_for_je), 1) + self.assertIn(exc_je_for_je[0], exc_je_for_si) + + # reconcile remaining half + pr.get_unreconciled_entries() + self.assertEqual(len(pr.invoices), 1) + self.assertEqual(len(pr.payments), 1) + invoices = [x.as_dict() for x in pr.invoices] + payments = [x.as_dict() for x in pr.payments] + pr.allocate_entries(frappe._dict({"invoices": invoices, "payments": payments})) + pr.allocation[0].allocated_amount = 1 + pr.allocation[0].difference_amount = difference_amount + pr.reconcile() + self.assertEqual(len(pr.invoices), 0) + self.assertEqual(len(pr.payments), 0) + + # Exchange Gain/Loss Journal should've been created. + exc_je_for_si = [x for x in self.get_journals_for(si.doctype, si.name) if x.parent != je.name] + exc_je_for_je = self.get_journals_for(je.doctype, je.name) + self.assertNotEqual(exc_je_for_si, []) + self.assertEqual(len(exc_je_for_si), 2) + self.assertEqual(len(exc_je_for_je), 2) + self.assertIn(exc_je_for_je[0], exc_je_for_si) + + si.reload() + self.assertEqual(si.outstanding_amount, 0) + self.assert_ledger_outstanding(si.doctype, si.name, 0.0, 0.0) + + # Cancel Payment + je.reload() + je.cancel() + + si.reload() + self.assertEqual(si.outstanding_amount, 2) + self.assert_ledger_outstanding(si.doctype, si.name, 160.0, 2.0) + + # Exchange Gain/Loss Journal should've been cancelled + exc_je_for_si = self.get_journals_for(si.doctype, si.name) + exc_je_for_je = self.get_journals_for(je.doctype, je.name) + self.assertEqual(exc_je_for_si, []) + self.assertEqual(exc_je_for_je, []) From 70dd9d0671e1d77d50c814885c6a6f59508c4f62 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Mon, 17 Jul 2023 12:29:42 +0530 Subject: [PATCH 043/101] chore(test): fix broken unit test --- .../doctype/sales_invoice/test_sales_invoice.py | 11 +---------- erpnext/controllers/tests/test_accounts_controller.py | 4 +--- 2 files changed, 2 insertions(+), 13 deletions(-) diff --git a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py index 41e55546a8..6ddf305228 100644 --- a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py +++ b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py @@ -3213,15 +3213,10 @@ class TestSalesInvoice(unittest.TestCase): account.disabled = 0 account.save() + @change_settings("Accounts Settings", {"unlink_payment_on_cancel_of_invoice": 1}) def test_gain_loss_with_advance_entry(self): from erpnext.accounts.doctype.journal_entry.test_journal_entry import make_journal_entry - unlink_enabled = frappe.db.get_value( - "Accounts Settings", "Accounts Settings", "unlink_payment_on_cancel_of_invoice" - ) - - frappe.db.set_single_value("Accounts Settings", "unlink_payment_on_cancel_of_invoice", 1) - jv = make_journal_entry("_Test Receivable USD - _TC", "_Test Bank - _TC", -7000, save=False) jv.accounts[0].exchange_rate = 70 @@ -3264,10 +3259,6 @@ class TestSalesInvoice(unittest.TestCase): check_gl_entries(self, si.name, expected_gle, nowdate()) - frappe.db.set_single_value( - "Accounts Settings", "unlink_payment_on_cancel_of_invoice", unlink_enabled - ) - def test_batch_expiry_for_sales_invoice_return(self): from erpnext.controllers.sales_and_purchase_return import make_return_doc from erpnext.stock.doctype.item.test_item import make_item diff --git a/erpnext/controllers/tests/test_accounts_controller.py b/erpnext/controllers/tests/test_accounts_controller.py index 9a7326ea29..eefe202e47 100644 --- a/erpnext/controllers/tests/test_accounts_controller.py +++ b/erpnext/controllers/tests/test_accounts_controller.py @@ -37,6 +37,7 @@ def make_supplier(supplier_name, currency=None): supplier = frappe.new_doc("Supplier") supplier.supplier_name = supplier_name supplier.supplier_type = "Individual" + supplier.supplier_group = "All Supplier Groups" if currency: supplier.default_currency = currency @@ -55,9 +56,6 @@ class TestAccountsController(unittest.TestCase): 10 series - Sales Invoice against Payment Entries 20 series - Sales Invoice against Journals 30 series - Sales Invoice against Credit Notes - 40 series - Purchase Invoice against Payment Entries - 50 series - Purchase Invoice against Journals - 60 series - Purchase Invoice against Debit Notes """ def setUp(self): From 37895a361cdf7be4704f376eb6ec749af0ab3c90 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Mon, 24 Jul 2023 20:41:05 +0530 Subject: [PATCH 044/101] chore(test): fix broken test case --- erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py index 6ddf305228..2f193972a7 100644 --- a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py +++ b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py @@ -3213,7 +3213,7 @@ class TestSalesInvoice(unittest.TestCase): account.disabled = 0 account.save() - @change_settings("Accounts Settings", {"unlink_payment_on_cancel_of_invoice": 1}) + @change_settings("Accounts Settings", {"unlink_payment_on_cancellation_of_invoice": 1}) def test_gain_loss_with_advance_entry(self): from erpnext.accounts.doctype.journal_entry.test_journal_entry import make_journal_entry From 6628632fbb15ddcc80f5af201d15976337141fc6 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Tue, 25 Jul 2023 10:30:08 +0530 Subject: [PATCH 045/101] chore: type info --- erpnext/controllers/accounts_controller.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/controllers/accounts_controller.py b/erpnext/controllers/accounts_controller.py index 2ce1eb8c15..40432f70f9 100644 --- a/erpnext/controllers/accounts_controller.py +++ b/erpnext/controllers/accounts_controller.py @@ -968,7 +968,7 @@ class AccountsController(TransactionBase): d.exchange_gain_loss = difference - def make_exchange_gain_loss_journal(self, args=None) -> None: + def make_exchange_gain_loss_journal(self, args: dict = None) -> None: """ Make Exchange Gain/Loss journal for Invoices and Payments """ From c87332d5da638c43ff6d0560bf3c26dde81e21cf Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Tue, 25 Jul 2023 10:30:49 +0530 Subject: [PATCH 046/101] refactor: cr/dr note will be on single exchange rate --- .../doctype/journal_entry/journal_entry.py | 29 +++++++++++-------- .../payment_reconciliation.py | 7 +++-- 2 files changed, 21 insertions(+), 15 deletions(-) diff --git a/erpnext/accounts/doctype/journal_entry/journal_entry.py b/erpnext/accounts/doctype/journal_entry/journal_entry.py index e6b8b5d281..0c23d772d5 100644 --- a/erpnext/accounts/doctype/journal_entry/journal_entry.py +++ b/erpnext/accounts/doctype/journal_entry/journal_entry.py @@ -768,18 +768,23 @@ class JournalEntry(AccountsController): ) ): - # Modified to include the posting date for which to retreive the exchange rate - d.exchange_rate = get_exchange_rate( - self.posting_date, - d.account, - d.account_currency, - self.company, - d.reference_type, - d.reference_name, - d.debit, - d.credit, - d.exchange_rate, - ) + ignore_exchange_rate = False + if self.get("flags") and self.flags.get("ignore_exchange_rate"): + ignore_exchange_rate = True + + if not ignore_exchange_rate: + # Modified to include the posting date for which to retreive the exchange rate + d.exchange_rate = get_exchange_rate( + self.posting_date, + d.account, + d.account_currency, + self.company, + d.reference_type, + d.reference_name, + d.debit, + d.credit, + d.exchange_rate, + ) if not d.exchange_rate: frappe.throw(_("Row {0}: Exchange Rate is mandatory").format(d.idx)) diff --git a/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py b/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py index d574cd79b8..2c11ef5120 100644 --- a/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py +++ b/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py @@ -650,6 +650,7 @@ def reconcile_dr_cr_note(dr_cr_notes, company): "reference_type": inv.against_voucher_type, "reference_name": inv.against_voucher, "cost_center": erpnext.get_default_cost_center(company), + "exchange_rate": inv.exchange_rate, }, { "account": inv.account, @@ -663,13 +664,13 @@ def reconcile_dr_cr_note(dr_cr_notes, company): "reference_type": inv.voucher_type, "reference_name": inv.voucher_no, "cost_center": erpnext.get_default_cost_center(company), + "exchange_rate": inv.exchange_rate, }, ], } ) - if difference_entry := get_difference_row(inv): - jv.append("accounts", difference_entry) - jv.flags.ignore_mandatory = True + jv.flags.ignore_exchange_rate = True jv.submit() + jv.make_exchange_gain_loss_journal(args=[inv]) From c0b3b069b587cff11969112b01fff08c8df7adf0 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Tue, 25 Jul 2023 10:51:58 +0530 Subject: [PATCH 047/101] refactor: split make_exchage_gain_loss_journal into smaller function --- erpnext/controllers/accounts_controller.py | 219 ++++++++++----------- 1 file changed, 103 insertions(+), 116 deletions(-) diff --git a/erpnext/controllers/accounts_controller.py b/erpnext/controllers/accounts_controller.py index 40432f70f9..6bf9d299d2 100644 --- a/erpnext/controllers/accounts_controller.py +++ b/erpnext/controllers/accounts_controller.py @@ -968,6 +968,78 @@ class AccountsController(TransactionBase): d.exchange_gain_loss = difference + def create_gain_loss_journal( + self, + party_type, + party, + party_account, + gain_loss_account, + exc_gain_loss, + dr_or_cr, + reverse_dr_or_cr, + ref1_dt, + ref1_dn, + ref1_detail_no, + ref2_dt, + ref2_dn, + ref2_detail_no, + ) -> str: + journal_entry = frappe.new_doc("Journal Entry") + journal_entry.voucher_type = "Exchange Gain Or Loss" + journal_entry.company = self.company + journal_entry.posting_date = nowdate() + journal_entry.multi_currency = 1 + + party_account_currency = frappe.get_cached_value("Account", party_account, "account_currency") + + if not gain_loss_account: + frappe.throw( + _("Please set default Exchange Gain/Loss Account in Company {}").format(self.get("company")) + ) + gain_loss_account_currency = get_account_currency(gain_loss_account) + company_currency = frappe.get_cached_value("Company", self.company, "default_currency") + + if gain_loss_account_currency != self.company_currency: + frappe.throw(_("Currency for {0} must be {1}").format(gain_loss_account, company_currency)) + + journal_account = frappe._dict( + { + "account": party_account, + "party_type": party_type, + "party": party, + "account_currency": party_account_currency, + "exchange_rate": 0, + "cost_center": erpnext.get_default_cost_center(self.company), + "reference_type": ref1_dt, + "reference_name": ref1_dn, + "reference_detail_no": ref1_detail_no, + dr_or_cr: abs(exc_gain_loss), + dr_or_cr + "_in_account_currency": 0, + } + ) + + journal_entry.append("accounts", journal_account) + + journal_account = frappe._dict( + { + "account": gain_loss_account, + "account_currency": gain_loss_account_currency, + "exchange_rate": 1, + "cost_center": erpnext.get_default_cost_center(self.company), + "reference_type": ref2_dt, + "reference_name": ref2_dn, + "reference_detail_no": ref2_detail_no, + reverse_dr_or_cr + "_in_account_currency": 0, + reverse_dr_or_cr: abs(exc_gain_loss), + } + ) + + journal_entry.append("accounts", journal_account) + + journal_entry.save() + journal_entry.submit() + return journal_entry.name + def make_exchange_gain_loss_journal(self, args: dict = None) -> None: """ Make Exchange Gain/Loss journal for Invoices and Payments @@ -978,23 +1050,16 @@ class AccountsController(TransactionBase): if self.get("doctype") == "Journal Entry": # 'args' is populated with exchange gain/loss account and the amount to be booked. # These are generated by Sales/Purchase Invoice during reconciliation and advance allocation. + # and below logic is only for such scenarios if args: for arg in args: # Advance section uses `exchange_gain_loss` and reconciliation uses `difference_amount` if ( arg.get("difference_amount", 0) != 0 or arg.get("exchange_gain_loss", 0) != 0 ) and arg.get("difference_account"): - journal_entry = frappe.new_doc("Journal Entry") - journal_entry.voucher_type = "Exchange Gain Or Loss" - journal_entry.company = self.company - journal_entry.posting_date = nowdate() - journal_entry.multi_currency = 1 party_account = arg.get("account") - party_account_currency = frappe.get_cached_value( - "Account", party_account, "account_currency" - ) - + gain_loss_account = arg.get("difference_account") difference_amount = arg.get("difference_amount") or arg.get("exchange_gain_loss") if difference_amount > 0: dr_or_cr = "debit" if arg.get("party_type") == "Customer" else "credit" @@ -1003,60 +1068,22 @@ class AccountsController(TransactionBase): reverse_dr_or_cr = "debit" if dr_or_cr == "credit" else "credit" - gain_loss_account = arg.get("difference_account") - if not gain_loss_account: - frappe.throw( - _("Please set default Exchange Gain/Loss Account in Company {}").format( - self.get("company") - ) - ) - - gain_loss_account_currency = get_account_currency(gain_loss_account) - if gain_loss_account_currency != self.company_currency: - frappe.throw( - _("Currency for {0} must be {1}").format(gain_loss_account, self.company_currency) - ) - - journal_account = frappe._dict( - { - "account": party_account, - "party_type": arg.get("party_type"), - "party": arg.get("party"), - "account_currency": party_account_currency, - "exchange_rate": 0, - "cost_center": erpnext.get_default_cost_center(self.company), - "reference_type": arg.get("against_voucher_type"), - "reference_name": arg.get("against_voucher"), - "reference_detail_no": arg.get("idx"), - dr_or_cr: abs(difference_amount), - dr_or_cr + "_in_account_currency": 0, - } + self.create_gain_loss_journal( + arg.get("party_type"), + arg.get("party"), + party_account, + gain_loss_account, + difference_amount, + dr_or_cr, + reverse_dr_or_cr, + arg.get("against_voucher_type"), + arg.get("against_voucher"), + arg.get("idx"), + self.doctype, + self.name, + arg.get("idx"), ) - journal_entry.append("accounts", journal_account) - - journal_account = frappe._dict( - { - "account": gain_loss_account, - "account_currency": gain_loss_account_currency, - "exchange_rate": 1, - "cost_center": erpnext.get_default_cost_center(self.company), - # TODO: figure out a way to pass reference - # TODO: add reference_detail_no field in payment ledger - # throws 'Journal Entry doesn't have {account} or doesn't have matched account' - "reference_type": self.doctype, - "reference_name": self.name, - "reference_detail_no": arg.idx, - reverse_dr_or_cr: abs(difference_amount), - reverse_dr_or_cr + "_in_account_currency": 0, - } - ) - - journal_entry.append("accounts", journal_account) - - journal_entry.save() - journal_entry.submit() - if self.get("doctype") == "Payment Entry": # For Payment Entry, exchange_gain_loss field in the `references` table is the trigger for journal creation gain_loss_to_book = [x for x in self.references if x.exchange_gain_loss != 0] @@ -1093,23 +1120,15 @@ class AccountsController(TransactionBase): ) for d in gain_loss_to_book: + # Filter out References for which Gain/Loss is already booked if d.exchange_gain_loss and ( (d.reference_doctype, d.reference_name, str(d.idx)) not in booked ): - journal_entry = frappe.new_doc("Journal Entry") - journal_entry.voucher_type = "Exchange Gain Or Loss" - journal_entry.company = self.company - journal_entry.posting_date = nowdate() - journal_entry.multi_currency = 1 - if self.payment_type == "Receive": party_account = self.paid_from elif self.payment_type == "Pay": party_account = self.paid_to - party_account_currency = frappe.get_cached_value( - "Account", party_account, "account_currency" - ) dr_or_cr = "debit" if d.exchange_gain_loss > 0 else "credit" if d.reference_doctype == "Purchase Invoice": @@ -1120,54 +1139,22 @@ class AccountsController(TransactionBase): gain_loss_account = frappe.get_cached_value( "Company", self.company, "exchange_gain_loss_account" ) - if not gain_loss_account: - frappe.throw( - _("Please set default Exchange Gain/Loss Account in Company {}").format( - self.get("company") - ) - ) - gain_loss_account_currency = get_account_currency(gain_loss_account) - if gain_loss_account_currency != self.company_currency: - frappe.throw( - _("Currency for {0} must be {1}").format(gain_loss_account, self.company_currency) - ) - journal_account = frappe._dict( - { - "account": party_account, - "party_type": self.party_type, - "party": self.party, - "account_currency": party_account_currency, - "exchange_rate": 0, - "cost_center": erpnext.get_default_cost_center(self.company), - "reference_type": d.reference_doctype, - "reference_name": d.reference_name, - "reference_detail_no": d.idx, - dr_or_cr: abs(d.exchange_gain_loss), - dr_or_cr + "_in_account_currency": 0, - } + self.create_gain_loss_journal( + self.party_type, + self.party, + party_account, + gain_loss_account, + d.exchange_gain_loss, + dr_or_cr, + reverse_dr_or_cr, + d.reference_doctype, + d.reference_name, + d.idx, + self.doctype, + self.name, + d.idx, ) - - journal_entry.append("accounts", journal_account) - - journal_account = frappe._dict( - { - "account": gain_loss_account, - "account_currency": gain_loss_account_currency, - "exchange_rate": 1, - "cost_center": erpnext.get_default_cost_center(self.company), - "reference_type": self.doctype, - "reference_name": self.name, - "reference_detail_no": d.idx, - reverse_dr_or_cr + "_in_account_currency": 0, - reverse_dr_or_cr: abs(d.exchange_gain_loss), - } - ) - - journal_entry.append("accounts", journal_account) - - journal_entry.save() - journal_entry.submit() # frappe.throw("stopping...") def update_against_document_in_jv(self): From 1ea1bfebc4a2407961d93a6d0c4c6c9f43202689 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Wed, 26 Jul 2023 16:19:38 +0530 Subject: [PATCH 048/101] refactor: convert class method to standalone function --- erpnext/accounts/utils.py | 71 ++++++++++++++++++ erpnext/controllers/accounts_controller.py | 85 +++------------------- 2 files changed, 81 insertions(+), 75 deletions(-) diff --git a/erpnext/accounts/utils.py b/erpnext/accounts/utils.py index 53d9e21c35..fa889c0f65 100644 --- a/erpnext/accounts/utils.py +++ b/erpnext/accounts/utils.py @@ -1836,3 +1836,74 @@ class QueryPaymentLedger(object): self.query_for_outstanding() return self.voucher_outstandings + + +def create_gain_loss_journal( + company, + party_type, + party, + party_account, + gain_loss_account, + exc_gain_loss, + dr_or_cr, + reverse_dr_or_cr, + ref1_dt, + ref1_dn, + ref1_detail_no, + ref2_dt, + ref2_dn, + ref2_detail_no, +) -> str: + journal_entry = frappe.new_doc("Journal Entry") + journal_entry.voucher_type = "Exchange Gain Or Loss" + journal_entry.company = company + journal_entry.posting_date = nowdate() + journal_entry.multi_currency = 1 + + party_account_currency = frappe.get_cached_value("Account", party_account, "account_currency") + + if not gain_loss_account: + frappe.throw(_("Please set default Exchange Gain/Loss Account in Company {}").format(company)) + gain_loss_account_currency = get_account_currency(gain_loss_account) + company_currency = frappe.get_cached_value("Company", company, "default_currency") + + if gain_loss_account_currency != company_currency: + frappe.throw(_("Currency for {0} must be {1}").format(gain_loss_account, company_currency)) + + journal_account = frappe._dict( + { + "account": party_account, + "party_type": party_type, + "party": party, + "account_currency": party_account_currency, + "exchange_rate": 0, + "cost_center": erpnext.get_default_cost_center(company), + "reference_type": ref1_dt, + "reference_name": ref1_dn, + "reference_detail_no": ref1_detail_no, + dr_or_cr: abs(exc_gain_loss), + dr_or_cr + "_in_account_currency": 0, + } + ) + + journal_entry.append("accounts", journal_account) + + journal_account = frappe._dict( + { + "account": gain_loss_account, + "account_currency": gain_loss_account_currency, + "exchange_rate": 1, + "cost_center": erpnext.get_default_cost_center(company), + "reference_type": ref2_dt, + "reference_name": ref2_dn, + "reference_detail_no": ref2_detail_no, + reverse_dr_or_cr + "_in_account_currency": 0, + reverse_dr_or_cr: abs(exc_gain_loss), + } + ) + + journal_entry.append("accounts", journal_account) + + journal_entry.save() + journal_entry.submit() + return journal_entry.name diff --git a/erpnext/controllers/accounts_controller.py b/erpnext/controllers/accounts_controller.py index 6bf9d299d2..b9fc0826cc 100644 --- a/erpnext/controllers/accounts_controller.py +++ b/erpnext/controllers/accounts_controller.py @@ -38,7 +38,12 @@ from erpnext.accounts.party import ( get_party_gle_currency, validate_party_frozen_disabled, ) -from erpnext.accounts.utils import get_account_currency, get_fiscal_years, validate_fiscal_year +from erpnext.accounts.utils import ( + create_gain_loss_journal, + get_account_currency, + get_fiscal_years, + validate_fiscal_year, +) from erpnext.buying.utils import update_last_purchase_rate from erpnext.controllers.print_settings import ( set_print_templates_for_item_table, @@ -968,78 +973,6 @@ class AccountsController(TransactionBase): d.exchange_gain_loss = difference - def create_gain_loss_journal( - self, - party_type, - party, - party_account, - gain_loss_account, - exc_gain_loss, - dr_or_cr, - reverse_dr_or_cr, - ref1_dt, - ref1_dn, - ref1_detail_no, - ref2_dt, - ref2_dn, - ref2_detail_no, - ) -> str: - journal_entry = frappe.new_doc("Journal Entry") - journal_entry.voucher_type = "Exchange Gain Or Loss" - journal_entry.company = self.company - journal_entry.posting_date = nowdate() - journal_entry.multi_currency = 1 - - party_account_currency = frappe.get_cached_value("Account", party_account, "account_currency") - - if not gain_loss_account: - frappe.throw( - _("Please set default Exchange Gain/Loss Account in Company {}").format(self.get("company")) - ) - gain_loss_account_currency = get_account_currency(gain_loss_account) - company_currency = frappe.get_cached_value("Company", self.company, "default_currency") - - if gain_loss_account_currency != self.company_currency: - frappe.throw(_("Currency for {0} must be {1}").format(gain_loss_account, company_currency)) - - journal_account = frappe._dict( - { - "account": party_account, - "party_type": party_type, - "party": party, - "account_currency": party_account_currency, - "exchange_rate": 0, - "cost_center": erpnext.get_default_cost_center(self.company), - "reference_type": ref1_dt, - "reference_name": ref1_dn, - "reference_detail_no": ref1_detail_no, - dr_or_cr: abs(exc_gain_loss), - dr_or_cr + "_in_account_currency": 0, - } - ) - - journal_entry.append("accounts", journal_account) - - journal_account = frappe._dict( - { - "account": gain_loss_account, - "account_currency": gain_loss_account_currency, - "exchange_rate": 1, - "cost_center": erpnext.get_default_cost_center(self.company), - "reference_type": ref2_dt, - "reference_name": ref2_dn, - "reference_detail_no": ref2_detail_no, - reverse_dr_or_cr + "_in_account_currency": 0, - reverse_dr_or_cr: abs(exc_gain_loss), - } - ) - - journal_entry.append("accounts", journal_account) - - journal_entry.save() - journal_entry.submit() - return journal_entry.name - def make_exchange_gain_loss_journal(self, args: dict = None) -> None: """ Make Exchange Gain/Loss journal for Invoices and Payments @@ -1068,7 +1001,8 @@ class AccountsController(TransactionBase): reverse_dr_or_cr = "debit" if dr_or_cr == "credit" else "credit" - self.create_gain_loss_journal( + create_gain_loss_journal( + self.company, arg.get("party_type"), arg.get("party"), party_account, @@ -1140,7 +1074,8 @@ class AccountsController(TransactionBase): "Company", self.company, "exchange_gain_loss_account" ) - self.create_gain_loss_journal( + create_gain_loss_journal( + self.company, self.party_type, self.party, party_account, From ba1f065765db6fc36358281fb4e4d775f1c1dcb1 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Wed, 26 Jul 2023 16:46:50 +0530 Subject: [PATCH 049/101] refactor: create gain/loss on Cr/Dr notes with different exc rates --- .../doctype/journal_entry/journal_entry.py | 4 ++- .../payment_reconciliation.py | 27 ++++++++++++++++++- 2 files changed, 29 insertions(+), 2 deletions(-) diff --git a/erpnext/accounts/doctype/journal_entry/journal_entry.py b/erpnext/accounts/doctype/journal_entry/journal_entry.py index 0c23d772d5..daa6355a44 100644 --- a/erpnext/accounts/doctype/journal_entry/journal_entry.py +++ b/erpnext/accounts/doctype/journal_entry/journal_entry.py @@ -586,7 +586,9 @@ class JournalEntry(AccountsController): else: party_account = against_voucher[1] - if against_voucher[0] != cstr(d.party) or party_account != d.account: + if ( + against_voucher[0] != cstr(d.party) or party_account != d.account + ) and self.voucher_type != "Exchange Gain Or Loss": frappe.throw( _("Row {0}: Party / Account does not match with {1} / {2} in {3} {4}").format( d.idx, diff --git a/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py b/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py index 2c11ef5120..5937a29ff5 100644 --- a/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py +++ b/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py @@ -14,6 +14,7 @@ from erpnext.accounts.doctype.process_payment_reconciliation.process_payment_rec ) from erpnext.accounts.utils import ( QueryPaymentLedger, + create_gain_loss_journal, get_outstanding_invoices, reconcile_against_document, ) @@ -673,4 +674,28 @@ def reconcile_dr_cr_note(dr_cr_notes, company): jv.flags.ignore_mandatory = True jv.flags.ignore_exchange_rate = True jv.submit() - jv.make_exchange_gain_loss_journal(args=[inv]) + + # make gain/loss journal + if inv.party_type == "Customer": + dr_or_cr = "credit" if inv.difference_amount < 0 else "debit" + else: + dr_or_cr = "debit" if inv.difference_amount < 0 else "credit" + + reverse_dr_or_cr = "debit" if dr_or_cr == "credit" else "credit" + + create_gain_loss_journal( + company, + inv.party_type, + inv.party, + inv.account, + inv.difference_account, + inv.difference_amount, + dr_or_cr, + reverse_dr_or_cr, + inv.against_voucher_type, + inv.against_voucher, + None, + inv.voucher_type, + inv.voucher_no, + None, + ) From 506a5775f9937fc893ae02b287ecd7303487363c Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Wed, 26 Jul 2023 20:53:07 +0530 Subject: [PATCH 050/101] fix: incorrect gain/loss on allocation change on reconciliation tool --- .../doctype/payment_reconciliation/payment_reconciliation.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py b/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py index 5937a29ff5..36f362210d 100644 --- a/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py +++ b/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py @@ -277,6 +277,11 @@ class PaymentReconciliation(Document): def calculate_difference_on_allocation_change(self, payment_entry, invoice, allocated_amount): invoice_exchange_map = self.get_invoice_exchange_map(invoice, payment_entry) invoice[0]["exchange_rate"] = invoice_exchange_map.get(invoice[0].get("invoice_number")) + if payment_entry[0].get("reference_type") in ["Sales Invoice", "Purchase Invoice"]: + payment_entry[0]["exchange_rate"] = invoice_exchange_map.get( + payment_entry[0].get("reference_name") + ) + new_difference_amount = self.get_difference_amount( payment_entry[0], invoice[0], allocated_amount ) From e3d2a2c5bdd94364f22828acc40854f6834b66ce Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Wed, 26 Jul 2023 21:12:14 +0530 Subject: [PATCH 051/101] test: cr notes against invoice --- .../tests/test_accounts_controller.py | 43 ++++++++++++++++++- 1 file changed, 42 insertions(+), 1 deletion(-) diff --git a/erpnext/controllers/tests/test_accounts_controller.py b/erpnext/controllers/tests/test_accounts_controller.py index eefe202e47..fc4fb9fe9b 100644 --- a/erpnext/controllers/tests/test_accounts_controller.py +++ b/erpnext/controllers/tests/test_accounts_controller.py @@ -51,7 +51,7 @@ def make_supplier(supplier_name, currency=None): class TestAccountsController(unittest.TestCase): """ Test Exchange Gain/Loss booking on various scenarios. - Test Cases are numbered for better readbility + Test Cases are numbered for better organization 10 series - Sales Invoice against Payment Entries 20 series - Sales Invoice against Journals @@ -923,3 +923,44 @@ class TestAccountsController(unittest.TestCase): exc_je_for_je = self.get_journals_for(je.doctype, je.name) self.assertEqual(exc_je_for_si, []) self.assertEqual(exc_je_for_je, []) + + def test_30_cr_note_against_sales_invoice(self): + """ + Reconciling Cr Note against Sales Invoice, both having different exchange rates + """ + # Invoice in Foreign currency + si = self.create_sales_invoice(qty=2, conversion_rate=80, rate=1) + + # Cr Note in Foreign currency of different exchange rate + cr_note = self.create_sales_invoice(qty=-2, conversion_rate=75, rate=1, do_not_save=True) + cr_note.is_return = 1 + cr_note.save().submit() + + # Reconcile the first half + pr = self.create_payment_reconciliation() + pr.get_unreconciled_entries() + self.assertEqual(len(pr.invoices), 1) + self.assertEqual(len(pr.payments), 1) + invoices = [x.as_dict() for x in pr.invoices] + payments = [x.as_dict() for x in pr.payments] + pr.allocate_entries(frappe._dict({"invoices": invoices, "payments": payments})) + difference_amount = pr.calculate_difference_on_allocation_change( + [x.as_dict() for x in pr.payments], [x.as_dict() for x in pr.invoices], 1 + ) + pr.allocation[0].allocated_amount = 1 + pr.allocation[0].difference_amount = difference_amount + pr.reconcile() + self.assertEqual(len(pr.invoices), 1) + self.assertEqual(len(pr.payments), 1) + + # Exchange Gain/Loss Journal should've been created. + exc_je_for_si = self.get_journals_for(si.doctype, si.name) + exc_je_for_cr = self.get_journals_for(cr_note.doctype, cr_note.name) + self.assertNotEqual(exc_je_for_si, []) + self.assertEqual(len(exc_je_for_si), 2) + self.assertEqual(len(exc_je_for_cr), 2) + self.assertEqual(exc_je_for_cr, exc_je_for_si) + + si.reload() + self.assertEqual(si.outstanding_amount, 1) + self.assert_ledger_outstanding(si.doctype, si.name, 80.0, 1.0) From 95543225cf402e9f17e05efee80f2dfb199aa4d9 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Wed, 26 Jul 2023 21:15:48 +0530 Subject: [PATCH 052/101] fix: cr/dr note should be posted for exc gain/loss --- .../payment_reconciliation/payment_reconciliation.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py b/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py index 36f362210d..59abecd0b9 100644 --- a/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py +++ b/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py @@ -697,10 +697,10 @@ def reconcile_dr_cr_note(dr_cr_notes, company): inv.difference_amount, dr_or_cr, reverse_dr_or_cr, - inv.against_voucher_type, - inv.against_voucher, - None, inv.voucher_type, inv.voucher_no, None, + inv.against_voucher_type, + inv.against_voucher, + None, ) From ae424fdfedb49e6018d957eebb11eb4e03d9d410 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Wed, 26 Jul 2023 21:24:08 +0530 Subject: [PATCH 053/101] test: assert ledger after cr note cancellation --- .../tests/test_accounts_controller.py | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/erpnext/controllers/tests/test_accounts_controller.py b/erpnext/controllers/tests/test_accounts_controller.py index fc4fb9fe9b..415e1734a9 100644 --- a/erpnext/controllers/tests/test_accounts_controller.py +++ b/erpnext/controllers/tests/test_accounts_controller.py @@ -964,3 +964,19 @@ class TestAccountsController(unittest.TestCase): si.reload() self.assertEqual(si.outstanding_amount, 1) self.assert_ledger_outstanding(si.doctype, si.name, 80.0, 1.0) + + cr_note.reload() + cr_note.cancel() + + # Exchange Gain/Loss Journal should've been created. + exc_je_for_si = self.get_journals_for(si.doctype, si.name) + exc_je_for_cr = self.get_journals_for(cr_note.doctype, cr_note.name) + self.assertNotEqual(exc_je_for_si, []) + self.assertEqual(len(exc_je_for_si), 1) + self.assertEqual(len(exc_je_for_cr), 0) + + # The Credit Note JE is still active and is referencing the sales invoice + # So, outstanding stays the same + si.reload() + self.assertEqual(si.outstanding_amount, 1) + self.assert_ledger_outstanding(si.doctype, si.name, 80.0, 1.0) From bfa54d533572f33ea5bc83794489293d36949e5d Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Wed, 26 Jul 2023 21:54:23 +0530 Subject: [PATCH 054/101] fix(test): test case breakage in Github Actions --- erpnext/accounts/doctype/journal_entry/test_journal_entry.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/erpnext/accounts/doctype/journal_entry/test_journal_entry.py b/erpnext/accounts/doctype/journal_entry/test_journal_entry.py index e7aca79d08..a6e920b7ef 100644 --- a/erpnext/accounts/doctype/journal_entry/test_journal_entry.py +++ b/erpnext/accounts/doctype/journal_entry/test_journal_entry.py @@ -5,6 +5,7 @@ import unittest import frappe +from frappe.tests.utils import change_settings from frappe.utils import flt, nowdate from erpnext.accounts.doctype.account.test_account import get_inventory_account @@ -13,6 +14,7 @@ from erpnext.exceptions import InvalidAccountCurrency class TestJournalEntry(unittest.TestCase): + @change_settings("Accounts Settings", {"unlink_payment_on_cancellation_of_invoice": 1}) def test_journal_entry_with_against_jv(self): jv_invoice = frappe.copy_doc(test_records[2]) base_jv = frappe.copy_doc(test_records[0]) From 025091161e47bd2ad77beec068d5263567605425 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Wed, 26 Jul 2023 22:32:59 +0530 Subject: [PATCH 055/101] refactor(test): assert ledger outstanding --- .../sales_invoice/test_sales_invoice.py | 22 +++++++++++++++---- 1 file changed, 18 insertions(+), 4 deletions(-) diff --git a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py index 2f193972a7..0de43bab2d 100644 --- a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py +++ b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py @@ -3249,16 +3249,30 @@ class TestSalesInvoice(unittest.TestCase): ) si.save() si.submit() - expected_gle = [ - ["_Test Exchange Gain/Loss - _TC", 500.0, 0.0, nowdate()], ["_Test Receivable USD - _TC", 7500.0, 0.0, nowdate()], - ["_Test Receivable USD - _TC", 0.0, 500.0, nowdate()], ["Sales - _TC", 0.0, 7500.0, nowdate()], ] - check_gl_entries(self, si.name, expected_gle, nowdate()) + si.reload() + self.assertEqual(si.outstanding_amount, 0) + journals = frappe.db.get_all( + "Journal Entry Account", + filters={"reference_type": "Sales Invoice", "reference_name": si.name, "docstatus": 1}, + pluck="parent", + ) + journals = [x for x in journals if x != jv.name] + self.assertEqual(len(journals), 1) + je_type = frappe.get_cached_value("Journal Entry", journals[0], "voucher_type") + self.assertEqual(je_type, "Exchange Gain Or Loss") + ledger_outstanding = frappe.db.get_all( + "Payment Ledger Entry", + filters={"against_voucher_no": si.name, "delinked": 0}, + fields=["sum(amount), sum(amount_in_account_currency)"], + as_list=1, + ) + def test_batch_expiry_for_sales_invoice_return(self): from erpnext.controllers.sales_and_purchase_return import make_return_doc from erpnext.stock.doctype.item.test_item import make_item From 47bbb37291eba1e2bb8217837417a072f73f5634 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Thu, 27 Jul 2023 05:54:13 +0530 Subject: [PATCH 056/101] chore: use frappetestcase --- erpnext/controllers/tests/test_accounts_controller.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/erpnext/controllers/tests/test_accounts_controller.py b/erpnext/controllers/tests/test_accounts_controller.py index 415e1734a9..acda12bf59 100644 --- a/erpnext/controllers/tests/test_accounts_controller.py +++ b/erpnext/controllers/tests/test_accounts_controller.py @@ -47,8 +47,7 @@ def make_supplier(supplier_name, currency=None): return supplier_name -# class TestAccountsController(FrappeTestCase): -class TestAccountsController(unittest.TestCase): +class TestAccountsController(FrappeTestCase): """ Test Exchange Gain/Loss booking on various scenarios. Test Cases are numbered for better organization @@ -66,8 +65,7 @@ class TestAccountsController(unittest.TestCase): self.clear_old_entries() def tearDown(self): - # frappe.db.rollback() - pass + frappe.db.rollback() def create_company(self): company_name = "_Test Company MC" From acc7322874b97830a838d066925aec99b01af129 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Thu, 27 Jul 2023 07:52:01 +0530 Subject: [PATCH 057/101] chore: add msgprint for exc JE --- erpnext/controllers/accounts_controller.py | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/erpnext/controllers/accounts_controller.py b/erpnext/controllers/accounts_controller.py index b9fc0826cc..37a18d80e9 100644 --- a/erpnext/controllers/accounts_controller.py +++ b/erpnext/controllers/accounts_controller.py @@ -1001,7 +1001,7 @@ class AccountsController(TransactionBase): reverse_dr_or_cr = "debit" if dr_or_cr == "credit" else "credit" - create_gain_loss_journal( + je = create_gain_loss_journal( self.company, arg.get("party_type"), arg.get("party"), @@ -1017,6 +1017,11 @@ class AccountsController(TransactionBase): self.name, arg.get("idx"), ) + frappe.msgprint( + _("Exchange Gain/Loss amount has been booked through {0}").format( + get_link_to_form("Journal Entry", je) + ) + ) if self.get("doctype") == "Payment Entry": # For Payment Entry, exchange_gain_loss field in the `references` table is the trigger for journal creation @@ -1074,7 +1079,7 @@ class AccountsController(TransactionBase): "Company", self.company, "exchange_gain_loss_account" ) - create_gain_loss_journal( + je = create_gain_loss_journal( self.company, self.party_type, self.party, @@ -1090,7 +1095,11 @@ class AccountsController(TransactionBase): self.name, d.idx, ) - # frappe.throw("stopping...") + frappe.msgprint( + _("Exchange Gain/Loss amount has been booked through {0}").format( + get_link_to_form("Journal Entry", je) + ) + ) def update_against_document_in_jv(self): """ From d9d685615335778cd36734b1d1bd0c2b4189b690 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Thu, 27 Jul 2023 08:02:46 +0530 Subject: [PATCH 058/101] chore: rename some internal variables --- erpnext/accounts/utils.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/erpnext/accounts/utils.py b/erpnext/accounts/utils.py index fa889c0f65..61359a6671 100644 --- a/erpnext/accounts/utils.py +++ b/erpnext/accounts/utils.py @@ -666,8 +666,9 @@ def cancel_exchange_gain_loss_journal(parent_doc: dict | object) -> None: fields=["parent"], as_list=1, ) + if journals: - exchange_journals = frappe.db.get_all( + gain_loss_journals = frappe.db.get_all( "Journal Entry", filters={ "name": ["in", [x[0] for x in journals]], @@ -676,7 +677,7 @@ def cancel_exchange_gain_loss_journal(parent_doc: dict | object) -> None: }, as_list=1, ) - for doc in exchange_journals: + for doc in gain_loss_journals: frappe.get_doc("Journal Entry", doc[0]).cancel() From 804afaa647b5727c37206fc4207c203652c10d53 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Thu, 27 Jul 2023 09:30:38 +0530 Subject: [PATCH 059/101] chore(test): use existing company for unit test --- erpnext/controllers/tests/test_accounts_controller.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/erpnext/controllers/tests/test_accounts_controller.py b/erpnext/controllers/tests/test_accounts_controller.py index acda12bf59..8e5f813d97 100644 --- a/erpnext/controllers/tests/test_accounts_controller.py +++ b/erpnext/controllers/tests/test_accounts_controller.py @@ -68,8 +68,8 @@ class TestAccountsController(FrappeTestCase): frappe.db.rollback() def create_company(self): - company_name = "_Test Company MC" - self.company_abbr = abbr = "_CM" + company_name = "_Test Company" + self.company_abbr = abbr = "_TC" if frappe.db.exists("Company", company_name): company = frappe.get_doc("Company", company_name) else: From 4f9242d699e8aa5c5595a12cafe763737978b01b Mon Sep 17 00:00:00 2001 From: Gursheen Anand Date: Thu, 27 Jul 2023 15:45:48 +0530 Subject: [PATCH 060/101] fix: dimension name in remark --- erpnext/accounts/general_ledger.py | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/erpnext/accounts/general_ledger.py b/erpnext/accounts/general_ledger.py index eacb318d04..a348911f03 100644 --- a/erpnext/accounts/general_ledger.py +++ b/erpnext/accounts/general_ledger.py @@ -66,15 +66,16 @@ def make_acc_dimensions_offsetting_entry(gl_map): offsetting_entries = [] for gle in gl_map: for dimension in accounting_dimensions_to_offset: - offsetting_account = dimension.offsetting_account offsetting_entry = gle.copy() + debit = flt(gle.credit) / no_of_dimensions if gle.credit != 0 else 0 + credit = flt(gle.debit) / no_of_dimensions if gle.debit != 0 else 0 offsetting_entry.update( { - "account": offsetting_account, - "debit": flt(gle.credit) / no_of_dimensions if gle.credit != 0 else 0, - "credit": flt(gle.debit) / no_of_dimensions if gle.debit != 0 else 0, - "debit_in_account_currency": flt(gle.credit) / no_of_dimensions if gle.credit != 0 else 0, - "credit_in_account_currency": flt(gle.debit) / no_of_dimensions if gle.debit != 0 else 0, + "account": dimension.offsetting_account, + "debit": debit, + "credit": credit, + "debit_in_account_currency": debit, + "credit_in_account_currency": credit, "remarks": _("Offsetting for Accounting Dimension") + " - {0}".format(dimension.name), "against_voucher": None, } @@ -91,7 +92,7 @@ def get_accounting_dimensions_for_offsetting_entry(gl_map, company): frappe.qb.from_(acc_dimension) .inner_join(dimension_detail) .on(acc_dimension.name == dimension_detail.parent) - .select(acc_dimension.fieldname, dimension_detail.offsetting_account) + .select(acc_dimension.fieldname, acc_dimension.name, dimension_detail.offsetting_account) .where( (acc_dimension.disabled == 0) & (dimension_detail.company == company) From 567c0ce1e85a42056d76cfc399b3468df32a576a Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Fri, 28 Jul 2023 08:12:44 +0530 Subject: [PATCH 061/101] chore: don't make gain/loss journal for base currency transactions --- .../payment_reconciliation.py | 45 ++++++++++--------- 1 file changed, 23 insertions(+), 22 deletions(-) diff --git a/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py b/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py index 59abecd0b9..ea06e0ec9a 100644 --- a/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py +++ b/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py @@ -680,27 +680,28 @@ def reconcile_dr_cr_note(dr_cr_notes, company): jv.flags.ignore_exchange_rate = True jv.submit() - # make gain/loss journal - if inv.party_type == "Customer": - dr_or_cr = "credit" if inv.difference_amount < 0 else "debit" - else: - dr_or_cr = "debit" if inv.difference_amount < 0 else "credit" + if inv.difference_amount != 0: + # make gain/loss journal + if inv.party_type == "Customer": + dr_or_cr = "credit" if inv.difference_amount < 0 else "debit" + else: + dr_or_cr = "debit" if inv.difference_amount < 0 else "credit" - reverse_dr_or_cr = "debit" if dr_or_cr == "credit" else "credit" + reverse_dr_or_cr = "debit" if dr_or_cr == "credit" else "credit" - create_gain_loss_journal( - company, - inv.party_type, - inv.party, - inv.account, - inv.difference_account, - inv.difference_amount, - dr_or_cr, - reverse_dr_or_cr, - inv.voucher_type, - inv.voucher_no, - None, - inv.against_voucher_type, - inv.against_voucher, - None, - ) + create_gain_loss_journal( + company, + inv.party_type, + inv.party, + inv.account, + inv.difference_account, + inv.difference_amount, + dr_or_cr, + reverse_dr_or_cr, + inv.voucher_type, + inv.voucher_no, + None, + inv.against_voucher_type, + inv.against_voucher, + None, + ) From 46ea81440066af74a3b98f4ab9d5006839a17a4b Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Fri, 28 Jul 2023 08:29:19 +0530 Subject: [PATCH 062/101] chore: cancel gain/loss je while posting reverse gl --- .../accounts/doctype/journal_entry/journal_entry.py | 3 +++ .../accounts/doctype/payment_entry/payment_entry.py | 12 ++++++++++-- .../accounts/doctype/sales_invoice/sales_invoice.py | 3 ++- erpnext/controllers/stock_controller.py | 3 ++- 4 files changed, 17 insertions(+), 4 deletions(-) diff --git a/erpnext/accounts/doctype/journal_entry/journal_entry.py b/erpnext/accounts/doctype/journal_entry/journal_entry.py index daa6355a44..1e1b3ba642 100644 --- a/erpnext/accounts/doctype/journal_entry/journal_entry.py +++ b/erpnext/accounts/doctype/journal_entry/journal_entry.py @@ -18,6 +18,7 @@ from erpnext.accounts.doctype.tax_withholding_category.tax_withholding_category ) from erpnext.accounts.party import get_party_account from erpnext.accounts.utils import ( + cancel_exchange_gain_loss_journal, get_account_currency, get_balance_on, get_stock_accounts, @@ -942,6 +943,8 @@ class JournalEntry(AccountsController): merge_entries=merge_entries, update_outstanding=update_outstanding, ) + if cancel: + cancel_exchange_gain_loss_journal(frappe._dict(doctype=self.doctype, name=self.name)) @frappe.whitelist() def get_balance(self, difference_account=None): diff --git a/erpnext/accounts/doctype/payment_entry/payment_entry.py b/erpnext/accounts/doctype/payment_entry/payment_entry.py index 89241ebfe0..dec7f2b777 100644 --- a/erpnext/accounts/doctype/payment_entry/payment_entry.py +++ b/erpnext/accounts/doctype/payment_entry/payment_entry.py @@ -28,7 +28,12 @@ from erpnext.accounts.general_ledger import ( process_gl_map, ) from erpnext.accounts.party import get_party_account -from erpnext.accounts.utils import get_account_currency, get_balance_on, get_outstanding_invoices +from erpnext.accounts.utils import ( + cancel_exchange_gain_loss_journal, + get_account_currency, + get_balance_on, + get_outstanding_invoices, +) from erpnext.controllers.accounts_controller import ( AccountsController, get_supplier_block_status, @@ -1018,7 +1023,10 @@ class PaymentEntry(AccountsController): gl_entries = self.build_gl_map() gl_entries = process_gl_map(gl_entries) make_gl_entries(gl_entries, cancel=cancel, adv_adj=adv_adj) - self.make_exchange_gain_loss_journal() + if cancel: + cancel_exchange_gain_loss_journal(frappe._dict(doctype=self.doctype, name=self.name)) + else: + self.make_exchange_gain_loss_journal() def add_party_gl_entries(self, gl_entries): if self.party_account: diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py index fa18d8fc0e..e6bec93050 100644 --- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py +++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py @@ -23,7 +23,7 @@ from erpnext.accounts.doctype.tax_withholding_category.tax_withholding_category ) from erpnext.accounts.general_ledger import get_round_off_account_and_cost_center from erpnext.accounts.party import get_due_date, get_party_account, get_party_details -from erpnext.accounts.utils import get_account_currency +from erpnext.accounts.utils import cancel_exchange_gain_loss_journal, get_account_currency from erpnext.assets.doctype.asset.depreciation import ( depreciate_asset, get_disposal_account_and_cost_center, @@ -1032,6 +1032,7 @@ class SalesInvoice(SellingController): self.make_exchange_gain_loss_journal() elif self.docstatus == 2: + cancel_exchange_gain_loss_journal(frappe._dict(doctype=self.doctype, name=self.name)) make_reverse_gl_entries(voucher_type=self.doctype, voucher_no=self.name) if update_outstanding == "No": diff --git a/erpnext/controllers/stock_controller.py b/erpnext/controllers/stock_controller.py index caf4b6f18b..d669abe910 100644 --- a/erpnext/controllers/stock_controller.py +++ b/erpnext/controllers/stock_controller.py @@ -15,7 +15,7 @@ from erpnext.accounts.general_ledger import ( make_reverse_gl_entries, process_gl_map, ) -from erpnext.accounts.utils import get_fiscal_year +from erpnext.accounts.utils import cancel_exchange_gain_loss_journal, get_fiscal_year from erpnext.controllers.accounts_controller import AccountsController from erpnext.stock import get_warehouse_account_map from erpnext.stock.doctype.inventory_dimension.inventory_dimension import ( @@ -534,6 +534,7 @@ class StockController(AccountsController): make_sl_entries(sl_entries, allow_negative_stock, via_landed_cost_voucher) def make_gl_entries_on_cancel(self): + cancel_exchange_gain_loss_journal(frappe._dict(doctype=self.doctype, name=self.name)) if frappe.db.sql( """select name from `tabGL Entry` where voucher_type=%s and voucher_no=%s""", From f9fa34ff4058316af87fc03eb4131e5f58524408 Mon Sep 17 00:00:00 2001 From: Ashish Shah Date: Fri, 28 Jul 2023 11:10:51 +0530 Subject: [PATCH 063/101] fix: in payment_entry 'Unallocated Amount' cal is broken --- erpnext/accounts/doctype/payment_entry/payment_entry.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/erpnext/accounts/doctype/payment_entry/payment_entry.js b/erpnext/accounts/doctype/payment_entry/payment_entry.js index ed18feaf57..44474d976c 100644 --- a/erpnext/accounts/doctype/payment_entry/payment_entry.js +++ b/erpnext/accounts/doctype/payment_entry/payment_entry.js @@ -903,12 +903,12 @@ frappe.ui.form.on('Payment Entry', { 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)) { - unallocated_amount = (frm.doc.base_received_amount + total_deductions + frm.doc.base_total_taxes_and_charges + 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.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)) { - unallocated_amount = (frm.doc.base_paid_amount + frm.doc.base_total_taxes_and_charges - (total_deductions + 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.target_exchange_rate; } } From fd5c4e0a64a4a8972bf70fd1358767ab1fb86785 Mon Sep 17 00:00:00 2001 From: Gursheen Anand Date: Fri, 28 Jul 2023 11:41:03 +0530 Subject: [PATCH 064/101] fix: fetch ple for all party types --- .../accounts_payable/accounts_payable.py | 2 +- .../accounts_receivable.py | 98 +++++++++++-------- 2 files changed, 56 insertions(+), 44 deletions(-) diff --git a/erpnext/accounts/report/accounts_payable/accounts_payable.py b/erpnext/accounts/report/accounts_payable/accounts_payable.py index 7b19994911..8279afbc2b 100644 --- a/erpnext/accounts/report/accounts_payable/accounts_payable.py +++ b/erpnext/accounts/report/accounts_payable/accounts_payable.py @@ -7,7 +7,7 @@ from erpnext.accounts.report.accounts_receivable.accounts_receivable import Rece def execute(filters=None): args = { - "party_type": "Supplier", + "account_type": "Payable", "naming_by": ["Buying Settings", "supp_master_name"], } return ReceivablePayableReport(filters).run(args) diff --git a/erpnext/accounts/report/accounts_receivable/accounts_receivable.py b/erpnext/accounts/report/accounts_receivable/accounts_receivable.py index 93c3fb3340..5b92dcd717 100755 --- a/erpnext/accounts/report/accounts_receivable/accounts_receivable.py +++ b/erpnext/accounts/report/accounts_receivable/accounts_receivable.py @@ -34,7 +34,7 @@ from erpnext.accounts.utils import get_currency_precision def execute(filters=None): args = { - "party_type": "Customer", + "account_type": "Receivable", "naming_by": ["Selling Settings", "cust_master_name"], } return ReceivablePayableReport(filters).run(args) @@ -70,8 +70,11 @@ class ReceivablePayableReport(object): "Company", self.filters.get("company"), "default_currency" ) self.currency_precision = get_currency_precision() or 2 - self.dr_or_cr = "debit" if self.filters.party_type == "Customer" else "credit" - self.party_type = self.filters.party_type + self.dr_or_cr = "debit" if self.filters.account_type == "Receivable" else "credit" + self.account_type = self.filters.account_type + self.party_type = frappe.db.get_all( + "Party Type", {"account_type": self.account_type}, pluck="name" + ) self.party_details = {} self.invoices = set() self.skip_total_row = 0 @@ -197,6 +200,7 @@ class ReceivablePayableReport(object): # no invoice, this is an invoice / stand-alone payment / credit note row = self.voucher_balance.get((ple.voucher_type, ple.voucher_no, ple.party)) + row.party_type = ple.party_type return row def update_voucher_balance(self, ple): @@ -207,8 +211,9 @@ class ReceivablePayableReport(object): return # amount in "Party Currency", if its supplied. If not, amount in company currency - if self.filters.get(scrub(self.party_type)): - amount = ple.amount_in_account_currency + for party_type in self.party_type: + if self.filters.get(scrub(party_type)): + amount = ple.amount_in_account_currency else: amount = ple.amount amount_in_account_currency = ple.amount_in_account_currency @@ -362,7 +367,7 @@ class ReceivablePayableReport(object): def get_invoice_details(self): self.invoice_details = frappe._dict() - if self.party_type == "Customer": + if self.account_type == "Receivable": si_list = frappe.db.sql( """ select name, due_date, po_no @@ -390,7 +395,7 @@ class ReceivablePayableReport(object): d.sales_person ) - if self.party_type == "Supplier": + if self.account_type == "Payable": for pi in frappe.db.sql( """ select name, due_date, bill_no, bill_date @@ -421,12 +426,10 @@ class ReceivablePayableReport(object): # customer / supplier name party_details = self.get_party_details(row.party) or {} row.update(party_details) - if row.voucher_type == "Expense Claim": - row.party_type = "Employee" - else: - row.party_type = self.party_type - if self.filters.get(scrub(self.filters.party_type)): - row.currency = row.account_currency + for party_type in self.party_type: + if self.filters.get(scrub(party_type)): + row.currency = row.account_currency + break else: row.currency = self.company_currency @@ -552,7 +555,7 @@ class ReceivablePayableReport(object): where payment_entry.docstatus < 2 and payment_entry.posting_date > %s - and payment_entry.party_type = %s + and payment_entry.party_type in %s """, (self.filters.report_date, self.party_type), as_dict=1, @@ -562,11 +565,11 @@ class ReceivablePayableReport(object): if self.filters.get("party"): amount_field = ( "jea.debit_in_account_currency - jea.credit_in_account_currency" - if self.party_type == "Supplier" + if self.account_type == "Payable" else "jea.credit_in_account_currency - jea.debit_in_account_currency" ) else: - amount_field = "jea.debit - " if self.party_type == "Supplier" else "jea.credit" + amount_field = "jea.debit - " if self.account_type == "Payable" else "jea.credit" return frappe.db.sql( """ @@ -584,7 +587,7 @@ class ReceivablePayableReport(object): where je.docstatus < 2 and je.posting_date > %s - and jea.party_type = %s + and jea.party_type in %s and jea.reference_name is not null and jea.reference_name != '' group by je.name, jea.reference_name having future_amount > 0 @@ -623,13 +626,17 @@ class ReceivablePayableReport(object): row.future_ref = ", ".join(row.future_ref) def get_return_entries(self): - doctype = "Sales Invoice" if self.party_type == "Customer" else "Purchase Invoice" + doctype = "Sales Invoice" if self.account_type == "Receivable" else "Purchase Invoice" filters = {"is_return": 1, "docstatus": 1, "company": self.filters.company} - party_field = scrub(self.filters.party_type) - if self.filters.get(party_field): - filters.update({party_field: self.filters.get(party_field)}) + or_filters = {} + for party_type in self.party_type: + party_field = scrub(party_type) + if self.filters.get(party_field): + or_filters.update({party_field: self.filters.get(party_field)}) self.return_entries = frappe._dict( - frappe.get_all(doctype, filters, ["name", "return_against"], as_list=1) + frappe.get_all( + doctype, filters=filters, or_filters=or_filters, fields=["name", "return_against"], as_list=1 + ) ) def set_ageing(self, row): @@ -720,6 +727,7 @@ class ReceivablePayableReport(object): ) .where(ple.delinked == 0) .where(Criterion.all(self.qb_selection_filter)) + .where(Criterion.any(self.or_filters)) ) if self.filters.get("group_by_party"): @@ -750,19 +758,18 @@ class ReceivablePayableReport(object): def prepare_conditions(self): self.qb_selection_filter = [] - party_type_field = scrub(self.party_type) - if self.party_type == "Supplier": - self.qb_selection_filter.append(self.ple.party_type.isin([self.party_type, "Employee"])) - else: - self.qb_selection_filter.append(self.ple.party_type == self.party_type) + self.or_filters = [] + for party_type in self.party_type: + party_type_field = scrub(party_type) + self.or_filters.append(self.ple.party_type == party_type) - self.add_common_filters(party_type_field=party_type_field) + self.add_common_filters(party_type_field=party_type_field) - if party_type_field == "customer": - self.add_customer_filters() + if party_type_field == "customer": + self.add_customer_filters() - elif party_type_field == "supplier": - self.add_supplier_filters() + elif party_type_field == "supplier": + self.add_supplier_filters() if self.filters.cost_center: self.get_cost_center_conditions() @@ -791,11 +798,10 @@ class ReceivablePayableReport(object): self.qb_selection_filter.append(self.ple.account == self.filters.party_account) else: # get GL with "receivable" or "payable" account_type - account_type = "Receivable" if self.party_type == "Customer" else "Payable" accounts = [ d.name for d in frappe.get_all( - "Account", filters={"account_type": account_type, "company": self.filters.company} + "Account", filters={"account_type": self.account_type, "company": self.filters.company} ) ] @@ -885,7 +891,7 @@ class ReceivablePayableReport(object): def get_party_details(self, party): if not party in self.party_details: - if self.party_type == "Customer": + if self.account_type == "Receivable": fields = ["customer_name", "territory", "customer_group", "customer_primary_contact"] if self.filters.get("sales_partner"): @@ -921,7 +927,7 @@ class ReceivablePayableReport(object): width=180, ) self.add_column( - label="Receivable Account" if self.party_type == "Customer" else "Payable Account", + label=self.account_type + " Account", fieldname="party_account", fieldtype="Link", options="Account", @@ -929,13 +935,19 @@ class ReceivablePayableReport(object): ) if self.party_naming_by == "Naming Series": + if self.account_type == "Payable": + label = "Supplier Name" + fieldname = "supplier_name" + else: + label = "Customer Name" + fieldname = "customer_name" self.add_column( - _("{0} Name").format(self.party_type), - fieldname=scrub(self.party_type) + "_name", + label=label, + fieldname=fieldname, fieldtype="Data", ) - if self.party_type == "Customer": + if self.account_type == "Receivable": self.add_column( _("Customer Contact"), fieldname="customer_primary_contact", @@ -955,7 +967,7 @@ class ReceivablePayableReport(object): self.add_column(label="Due Date", fieldtype="Date") - if self.party_type == "Supplier": + if self.account_type == "Payable": self.add_column(label=_("Bill No"), fieldname="bill_no", fieldtype="Data") self.add_column(label=_("Bill Date"), fieldname="bill_date", fieldtype="Date") @@ -965,7 +977,7 @@ class ReceivablePayableReport(object): self.add_column(_("Invoiced Amount"), fieldname="invoiced") self.add_column(_("Paid Amount"), fieldname="paid") - if self.party_type == "Customer": + if self.account_type == "Receivable": self.add_column(_("Credit Note"), fieldname="credit_note") else: # note: fieldname is still `credit_note` @@ -983,7 +995,7 @@ class ReceivablePayableReport(object): self.add_column(label=_("Future Payment Amount"), fieldname="future_amount") self.add_column(label=_("Remaining Balance"), fieldname="remaining_balance") - if self.filters.party_type == "Customer": + if self.filters.account_type == "Receivable": self.add_column(label=_("Customer LPO"), fieldname="po_no", fieldtype="Data") # comma separated list of linked delivery notes @@ -1004,7 +1016,7 @@ class ReceivablePayableReport(object): if self.filters.sales_partner: self.add_column(label=_("Sales Partner"), fieldname="default_sales_partner", fieldtype="Data") - if self.filters.party_type == "Supplier": + if self.filters.account_type == "Payable": self.add_column( label=_("Supplier Group"), fieldname="supplier_group", From 49981fecc72acb387642689a47a2813d72543bc7 Mon Sep 17 00:00:00 2001 From: ramonalmato Date: Fri, 28 Jul 2023 10:57:31 +0200 Subject: [PATCH 065/101] fix: Job Card validation fixed when displaying total completed quantity --- erpnext/manufacturing/doctype/job_card/job_card.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/erpnext/manufacturing/doctype/job_card/job_card.py b/erpnext/manufacturing/doctype/job_card/job_card.py index 80bdfd5329..db6bc80838 100644 --- a/erpnext/manufacturing/doctype/job_card/job_card.py +++ b/erpnext/manufacturing/doctype/job_card/job_card.py @@ -544,12 +544,12 @@ class JobCard(Document): if self.for_quantity and flt(total_completed_qty, precision) != flt( self.for_quantity, precision ): - total_completed_qty = bold(_("Total Completed Qty")) + total_completed_qty_label = bold(_("Total Completed Qty")) qty_to_manufacture = bold(_("Qty to Manufacture")) frappe.throw( _("The {0} ({1}) must be equal to {2} ({3})").format( - total_completed_qty, + total_completed_qty_label, bold(flt(total_completed_qty, precision)), qty_to_manufacture, bold(self.for_quantity), From e355dea4b550fcf64450876652f852f6a6c529fd Mon Sep 17 00:00:00 2001 From: Gursheen Anand Date: Fri, 28 Jul 2023 14:51:28 +0530 Subject: [PATCH 066/101] fix: AP and AR summary --- erpnext/accounts/party.py | 38 ++++++++------- .../accounts_payable_summary.py | 2 +- .../accounts_receivable_summary.py | 48 ++++++++++++++----- 3 files changed, 58 insertions(+), 30 deletions(-) diff --git a/erpnext/accounts/party.py b/erpnext/accounts/party.py index 4996203635..d5f8634a7e 100644 --- a/erpnext/accounts/party.py +++ b/erpnext/accounts/party.py @@ -14,6 +14,7 @@ from frappe.contacts.doctype.address.address import ( from frappe.contacts.doctype.contact.contact import get_contact_details from frappe.core.doctype.user_permission.user_permission import get_permitted_documents from frappe.model.utils import get_fetch_values +from frappe.query_builder.functions import Date, Sum from frappe.utils import ( add_days, add_months, @@ -920,32 +921,35 @@ def get_party_shipping_address(doctype: str, name: str) -> Optional[str]: def get_partywise_advanced_payment_amount( - party_type, posting_date=None, future_payment=0, company=None, party=None + party_type, posting_date=None, future_payment=0, company=None, party=None, account_type=None ): - cond = "1=1" + gle = frappe.qb.DocType("GL Entry") + query = ( + frappe.qb.from_(gle) + .select(gle.party) + .where( + (gle.party_type.isin(party_type)) & (gle.against_voucher == None) & (gle.is_cancelled == 0) + ) + .groupby(gle.party) + ) + if account_type == "Receivable": + query = query.select(Sum(gle.credit).as_("amount")) + else: + query = query.select(Sum(gle.debit).as_("amount")) + if posting_date: if future_payment: - cond = "(posting_date <= '{0}' OR DATE(creation) <= '{0}')" "".format(posting_date) + query = query.where((gle.posting_date <= posting_date) | (Date(gle.creation) <= posting_date)) else: - cond = "posting_date <= '{0}'".format(posting_date) + query = query.where(gle.posting_date <= posting_date) if company: - cond += "and company = {0}".format(frappe.db.escape(company)) + query = query.where(gle.company == company) if party: - cond += "and party = {0}".format(frappe.db.escape(party)) + query = query.where(gle.party == party) - data = frappe.db.sql( - """ SELECT party, sum({0}) as amount - FROM `tabGL Entry` - WHERE - party_type = %s and against_voucher is null - and is_cancelled = 0 - and {1} GROUP BY party""".format( - ("credit") if party_type == "Customer" else "debit", cond - ), - party_type, - ) + data = query.run(as_dict=True) if data: return frappe._dict(data) diff --git a/erpnext/accounts/report/accounts_payable_summary/accounts_payable_summary.py b/erpnext/accounts/report/accounts_payable_summary/accounts_payable_summary.py index 65fe1de568..834c83c38e 100644 --- a/erpnext/accounts/report/accounts_payable_summary/accounts_payable_summary.py +++ b/erpnext/accounts/report/accounts_payable_summary/accounts_payable_summary.py @@ -9,7 +9,7 @@ from erpnext.accounts.report.accounts_receivable_summary.accounts_receivable_sum def execute(filters=None): args = { - "party_type": "Supplier", + "account_type": "Payable", "naming_by": ["Buying Settings", "supp_master_name"], } return AccountsReceivableSummary(filters).run(args) diff --git a/erpnext/accounts/report/accounts_receivable_summary/accounts_receivable_summary.py b/erpnext/accounts/report/accounts_receivable_summary/accounts_receivable_summary.py index 9c01b1a498..3aa1ae7104 100644 --- a/erpnext/accounts/report/accounts_receivable_summary/accounts_receivable_summary.py +++ b/erpnext/accounts/report/accounts_receivable_summary/accounts_receivable_summary.py @@ -12,7 +12,7 @@ from erpnext.accounts.report.accounts_receivable.accounts_receivable import Rece def execute(filters=None): args = { - "party_type": "Customer", + "account_type": "Receivable", "naming_by": ["Selling Settings", "cust_master_name"], } @@ -21,7 +21,10 @@ def execute(filters=None): class AccountsReceivableSummary(ReceivablePayableReport): def run(self, args): - self.party_type = args.get("party_type") + self.account_type = args.get("account_type") + self.party_type = frappe.db.get_all( + "Party Type", {"account_type": self.account_type}, pluck="name" + ) self.party_naming_by = frappe.db.get_value( args.get("naming_by")[0], None, args.get("naming_by")[1] ) @@ -35,13 +38,19 @@ class AccountsReceivableSummary(ReceivablePayableReport): self.get_party_total(args) + party = None + for party_type in self.party_type: + if self.filters.get(scrub(party_type)): + party = self.filters.get(scrub(party_type)) + party_advance_amount = ( get_partywise_advanced_payment_amount( self.party_type, self.filters.report_date, self.filters.show_future_payments, self.filters.company, - party=self.filters.get(scrub(self.party_type)), + party=party, + account_type=self.account_type, ) or {} ) @@ -57,9 +66,13 @@ class AccountsReceivableSummary(ReceivablePayableReport): row.party = party if self.party_naming_by == "Naming Series": - row.party_name = frappe.get_cached_value( - self.party_type, party, scrub(self.party_type) + "_name" - ) + if self.account_type == "Payable": + doctype = "Supplier" + fieldname = "supplier_name" + else: + doctype = "Customer" + fieldname = "customer_name" + row.party_name = frappe.get_cached_value(doctype, party, fieldname) row.update(party_dict) @@ -93,6 +106,7 @@ class AccountsReceivableSummary(ReceivablePayableReport): # set territory, customer_group, sales person etc self.set_party_details(d) + self.party_total[d.party].update({"party_type": d.party_type}) def init_party_total(self, row): self.party_total.setdefault( @@ -131,17 +145,27 @@ class AccountsReceivableSummary(ReceivablePayableReport): def get_columns(self): self.columns = [] self.add_column( - label=_(self.party_type), + label="Party Type", + fieldname="party_type", + fieldtype="Data", + width=100, + ) + self.add_column( + label="Party", fieldname="party", - fieldtype="Link", - options=self.party_type, + fieldtype="Dynamic Link", + options="party_type", width=180, ) if self.party_naming_by == "Naming Series": - self.add_column(_("{0} Name").format(self.party_type), fieldname="party_name", fieldtype="Data") + self.add_column( + label="Supplier Name" if self.account_type == "Payable" else "Customer Name", + fieldname="party_name", + fieldtype="Data", + ) - credit_debit_label = "Credit Note" if self.party_type == "Customer" else "Debit Note" + credit_debit_label = "Credit Note" if self.account_type == "Receivable" else "Debit Note" self.add_column(_("Advance Amount"), fieldname="advance") self.add_column(_("Invoiced Amount"), fieldname="invoiced") @@ -159,7 +183,7 @@ class AccountsReceivableSummary(ReceivablePayableReport): self.add_column(label=_("Future Payment Amount"), fieldname="future_amount") self.add_column(label=_("Remaining Balance"), fieldname="remaining_balance") - if self.party_type == "Customer": + if self.account_type == "Receivable": self.add_column( label=_("Territory"), fieldname="territory", fieldtype="Link", options="Territory" ) From 1c2148b637c9bd02bec718a3abb298a89881601b Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Fri, 28 Jul 2023 15:17:15 +0530 Subject: [PATCH 067/101] fix: multiple issues related to Production Plan --- .../production_plan/production_plan.js | 79 +++++++----- .../production_plan/production_plan.json | 10 +- .../production_plan/production_plan.py | 122 +++++++++++++++++- .../production_plan/test_production_plan.py | 96 ++++++++++++++ .../sales_order_item/sales_order_item.json | 10 +- 5 files changed, 277 insertions(+), 40 deletions(-) diff --git a/erpnext/manufacturing/doctype/production_plan/production_plan.js b/erpnext/manufacturing/doctype/production_plan/production_plan.js index 48986910b0..46c554c1e8 100644 --- a/erpnext/manufacturing/doctype/production_plan/production_plan.js +++ b/erpnext/manufacturing/doctype/production_plan/production_plan.js @@ -9,19 +9,25 @@ frappe.ui.form.on('Production Plan', { item.temporary_name = item.name; }); }, + setup(frm) { + frm.trigger("setup_queries"); + frm.custom_make_buttons = { 'Work Order': 'Work Order / Subcontract PO', 'Material Request': 'Material Request', }; + }, - frm.fields_dict['po_items'].grid.get_field('warehouse').get_query = function(doc) { + setup_queries(frm) { + frm.set_query("sales_order", "sales_orders", () => { return { + query: "erpnext.manufacturing.doctype.production_plan.production_plan.sales_order_query", filters: { - company: doc.company + company: frm.doc.company, } } - } + }); frm.set_query('for_warehouse', function(doc) { return { @@ -42,32 +48,40 @@ frappe.ui.form.on('Production Plan', { }; }); - frm.fields_dict['po_items'].grid.get_field('item_code').get_query = function(doc) { + frm.set_query("item_code", "po_items", (doc, cdt, cdn) => { return { query: "erpnext.controllers.queries.item_query", filters:{ 'is_stock_item': 1, } } - } + }); - frm.fields_dict['po_items'].grid.get_field('bom_no').get_query = function(doc, cdt, cdn) { + frm.set_query("bom_no", "po_items", (doc, cdt, cdn) => { var d = locals[cdt][cdn]; if (d.item_code) { return { query: "erpnext.controllers.queries.bom", - filters:{'item': cstr(d.item_code), 'docstatus': 1} + filters:{'item': d.item_code, 'docstatus': 1} } } else frappe.msgprint(__("Please enter Item first")); - } + }); - frm.fields_dict['mr_items'].grid.get_field('warehouse').get_query = function(doc) { + frm.set_query("warehouse", "mr_items", (doc) => { return { filters: { company: doc.company } } - } + }); + + frm.set_query("warehouse", "po_items", (doc) => { + return { + filters: { + company: doc.company + } + } + }); }, refresh(frm) { @@ -436,7 +450,7 @@ frappe.ui.form.on("Production Plan Item", { } }); } - } + }, }); frappe.ui.form.on("Material Request Plan Item", { @@ -467,31 +481,36 @@ frappe.ui.form.on("Material Request Plan Item", { frappe.ui.form.on("Production Plan Sales Order", { sales_order(frm, cdt, cdn) { - const { sales_order } = locals[cdt][cdn]; + let row = locals[cdt][cdn]; + const sales_order = row.sales_order; if (!sales_order) { return; } - frappe.call({ - method: "erpnext.manufacturing.doctype.production_plan.production_plan.get_so_details", - args: { sales_order }, - callback(r) { - const {transaction_date, customer, grand_total} = r.message; - frappe.model.set_value(cdt, cdn, 'sales_order_date', transaction_date); - frappe.model.set_value(cdt, cdn, 'customer', customer); - frappe.model.set_value(cdt, cdn, 'grand_total', grand_total); - } - }); + + if (row.sales_order) { + frm.call({ + method: "validate_sales_orders", + doc: frm.doc, + args: { + sales_order: row.sales_order, + }, + callback(r) { + frappe.call({ + method: "erpnext.manufacturing.doctype.production_plan.production_plan.get_so_details", + args: { sales_order }, + callback(r) { + const {transaction_date, customer, grand_total} = r.message; + frappe.model.set_value(cdt, cdn, 'sales_order_date', transaction_date); + frappe.model.set_value(cdt, cdn, 'customer', customer); + frappe.model.set_value(cdt, cdn, 'grand_total', grand_total); + } + }); + } + }); + } } }); -cur_frm.fields_dict['sales_orders'].grid.get_field("sales_order").get_query = function() { - return{ - filters: [ - ['Sales Order','docstatus', '=' ,1] - ] - } -}; - frappe.tour['Production Plan'] = [ { fieldname: "get_items_from", diff --git a/erpnext/manufacturing/doctype/production_plan/production_plan.json b/erpnext/manufacturing/doctype/production_plan/production_plan.json index 232f1cb2c4..0d0fd5e270 100644 --- a/erpnext/manufacturing/doctype/production_plan/production_plan.json +++ b/erpnext/manufacturing/doctype/production_plan/production_plan.json @@ -228,10 +228,10 @@ }, { "default": "0", - "description": "To know more about projected quantity, click here.", + "description": "If enabled, the system won't create material requests for the available items.", "fieldname": "ignore_existing_ordered_qty", "fieldtype": "Check", - "label": "Ignore Existing Projected Quantity" + "label": "Ignore Available Stock" }, { "fieldname": "column_break_25", @@ -339,7 +339,7 @@ "depends_on": "eval:doc.get_items_from == 'Sales Order'", "fieldname": "combine_items", "fieldtype": "Check", - "label": "Consolidate Items" + "label": "Consolidate Sales Order Items" }, { "fieldname": "section_break_25", @@ -399,7 +399,7 @@ }, { "default": "0", - "description": "System consider the projected quantity to check available or will be available sub-assembly items ", + "description": "If this checkbox is enabled, then the system won\u2019t run the MRP for the available sub-assembly items.", "fieldname": "skip_available_sub_assembly_item", "fieldtype": "Check", "label": "Skip Available Sub Assembly Items" @@ -422,7 +422,7 @@ "index_web_pages_for_search": 1, "is_submittable": 1, "links": [], - "modified": "2023-05-22 23:36:31.770517", + "modified": "2023-07-28 13:37:43.926686", "modified_by": "Administrator", "module": "Manufacturing", "name": "Production Plan", diff --git a/erpnext/manufacturing/doctype/production_plan/production_plan.py b/erpnext/manufacturing/doctype/production_plan/production_plan.py index d8cc8f6d39..261aa76b70 100644 --- a/erpnext/manufacturing/doctype/production_plan/production_plan.py +++ b/erpnext/manufacturing/doctype/production_plan/production_plan.py @@ -39,6 +39,36 @@ class ProductionPlan(Document): self.set_status() self._rename_temporary_references() validate_uom_is_integer(self, "stock_uom", "planned_qty") + self.validate_sales_orders() + + @frappe.whitelist() + def validate_sales_orders(self, sales_order=None): + sales_orders = [] + + if sales_order: + sales_orders.append(sales_order) + else: + sales_orders = [row.sales_order for row in self.sales_orders if row.sales_order] + + data = sales_order_query(filters={"company": self.company, "sales_orders": sales_orders}) + + title = _("Production Plan Already Submitted") + if not data: + msg = _("No items are available in the sales order {0} for production").format(sales_orders[0]) + if len(sales_orders) > 1: + sales_orders = ", ".join(sales_orders) + msg = _("No items are available in sales orders {0} for production").format(sales_orders) + + frappe.throw(msg, title=title) + + data = [d[0] for d in data] + + for sales_order in sales_orders: + if sales_order not in data: + frappe.throw( + _("No items are available in the sales order {0} for production").format(sales_order), + title=title, + ) def set_pending_qty_in_row_without_reference(self): "Set Pending Qty in independent rows (not from SO or MR)." @@ -205,6 +235,7 @@ class ProductionPlan(Document): ).as_("pending_qty"), so_item.description, so_item.name, + so_item.bom_no, ) .distinct() .where( @@ -342,7 +373,7 @@ class ProductionPlan(Document): "item_code": data.item_code, "description": data.description or item_details.description, "stock_uom": item_details and item_details.stock_uom or "", - "bom_no": item_details and item_details.bom_no or "", + "bom_no": data.bom_no or item_details and item_details.bom_no or "", "planned_qty": data.pending_qty, "pending_qty": data.pending_qty, "planned_start_date": now_datetime(), @@ -401,11 +432,50 @@ class ProductionPlan(Document): def on_submit(self): self.update_bin_qty() + self.update_sales_order() def on_cancel(self): self.db_set("status", "Cancelled") self.delete_draft_work_order() self.update_bin_qty() + self.update_sales_order() + + def update_sales_order(self): + sales_orders = [row.sales_order for row in self.po_items if row.sales_order] + if sales_orders: + so_wise_planned_qty = self.get_so_wise_planned_qty(sales_orders) + + for row in self.po_items: + if not row.sales_order and not row.sales_order_item: + continue + + key = (row.sales_order, row.sales_order_item) + frappe.db.set_value( + "Sales Order Item", + row.sales_order_item, + "production_plan_qty", + flt(so_wise_planned_qty.get(key)), + ) + + @staticmethod + def get_so_wise_planned_qty(sales_orders): + so_wise_planned_qty = frappe._dict() + data = frappe.get_all( + "Production Plan Item", + fields=["sales_order", "sales_order_item", "SUM(planned_qty) as qty"], + filters={ + "sales_order": ("in", sales_orders), + "docstatus": 1, + "sales_order_item": ("is", "set"), + }, + group_by="sales_order, sales_order_item", + ) + + for row in data: + key = (row.sales_order, row.sales_order_item) + so_wise_planned_qty[key] = row.qty + + return so_wise_planned_qty def update_bin_qty(self): for d in self.mr_items: @@ -719,6 +789,9 @@ class ProductionPlan(Document): sub_assembly_items_store = [] # temporary store to process all subassembly items for row in self.po_items: + if self.skip_available_sub_assembly_item and not row.warehouse: + frappe.throw(_("Row #{0}: Please select the FG Warehouse in Assembly Items").format(row.idx)) + if not row.item_code: frappe.throw(_("Row #{0}: Please select Item Code in Assembly Items").format(row.idx)) @@ -1142,7 +1215,7 @@ def get_sales_orders(self): & (so.docstatus == 1) & (so.status.notin(["Stopped", "Closed"])) & (so.company == self.company) - & (so_item.qty > so_item.work_order_qty) + & (so_item.qty > so_item.production_plan_qty) ) ) @@ -1566,7 +1639,6 @@ def get_reserved_qty_for_production_plan(item_code, warehouse): def get_raw_materials_of_sub_assembly_items( item_details, company, bom_no, include_non_stock_items, sub_assembly_items, planned_qty=1 ): - bei = frappe.qb.DocType("BOM Item") bom = frappe.qb.DocType("BOM") item = frappe.qb.DocType("Item") @@ -1609,7 +1681,10 @@ def get_raw_materials_of_sub_assembly_items( for item in items: key = (item.item_code, item.bom_no) - if item.bom_no and key in sub_assembly_items: + if item.bom_no and key not in sub_assembly_items: + continue + + if item.bom_no: planned_qty = flt(sub_assembly_items[key]) get_raw_materials_of_sub_assembly_items( item_details, @@ -1626,3 +1701,42 @@ def get_raw_materials_of_sub_assembly_items( item_details.setdefault(item.get("item_code"), item) return item_details + + +@frappe.whitelist() +def sales_order_query( + doctype=None, txt=None, searchfield=None, start=None, page_len=None, filters=None +): + frappe.has_permission("Production Plan", throw=True) + + if not filters: + filters = {} + + so_table = frappe.qb.DocType("Sales Order") + table = frappe.qb.DocType("Sales Order Item") + + query = ( + frappe.qb.from_(so_table) + .join(table) + .on(table.parent == so_table.name) + .select(table.parent) + .distinct() + .where((table.qty > table.production_plan_qty) & (table.docstatus == 1)) + ) + + if filters.get("company"): + query = query.where(so_table.company == filters.get("company")) + + if filters.get("sales_orders"): + query = query.where(so_table.name.isin(filters.get("sales_orders"))) + + if txt: + query = query.where(table.item_code.like(f"{txt}%")) + + if page_len: + query = query.limit(page_len) + + if start: + query = query.offset(start) + + return query.run() diff --git a/erpnext/manufacturing/doctype/production_plan/test_production_plan.py b/erpnext/manufacturing/doctype/production_plan/test_production_plan.py index f60dbfc3f5..2871a29d76 100644 --- a/erpnext/manufacturing/doctype/production_plan/test_production_plan.py +++ b/erpnext/manufacturing/doctype/production_plan/test_production_plan.py @@ -225,6 +225,102 @@ class TestProductionPlan(FrappeTestCase): self.assertEqual(sales_orders, []) + def test_donot_allow_to_make_multiple_pp_against_same_so(self): + item = "Test SO Production Item 1" + create_item(item) + + raw_material = "Test SO RM Production Item 1" + create_item(raw_material) + + if not frappe.db.get_value("BOM", {"item": item}): + make_bom(item=item, raw_materials=[raw_material]) + + so = make_sales_order(item_code=item, qty=4) + pln = frappe.new_doc("Production Plan") + pln.company = so.company + pln.get_items_from = "Sales Order" + + pln.append( + "sales_orders", + { + "sales_order": so.name, + "sales_order_date": so.transaction_date, + "customer": so.customer, + "grand_total": so.grand_total, + }, + ) + + pln.get_so_items() + pln.submit() + + pln = frappe.new_doc("Production Plan") + pln.company = so.company + pln.get_items_from = "Sales Order" + + pln.append( + "sales_orders", + { + "sales_order": so.name, + "sales_order_date": so.transaction_date, + "customer": so.customer, + "grand_total": so.grand_total, + }, + ) + + pln.get_so_items() + self.assertRaises(frappe.ValidationError, pln.save) + + def test_so_based_bill_of_material(self): + item = "Test SO Production Item 1" + create_item(item) + + raw_material = "Test SO RM Production Item 1" + create_item(raw_material) + + bom1 = make_bom(item=item, raw_materials=[raw_material]) + + so = make_sales_order(item_code=item, qty=4) + + # Create new BOM and assign to new sales order + bom2 = make_bom(item=item, raw_materials=[raw_material]) + so2 = make_sales_order(item_code=item, qty=4) + + pln1 = frappe.new_doc("Production Plan") + pln1.company = so.company + pln1.get_items_from = "Sales Order" + + pln1.append( + "sales_orders", + { + "sales_order": so.name, + "sales_order_date": so.transaction_date, + "customer": so.customer, + "grand_total": so.grand_total, + }, + ) + + pln1.get_so_items() + + self.assertEqual(pln1.po_items[0].bom_no, bom1.name) + + pln2 = frappe.new_doc("Production Plan") + pln2.company = so2.company + pln2.get_items_from = "Sales Order" + + pln2.append( + "sales_orders", + { + "sales_order": so2.name, + "sales_order_date": so2.transaction_date, + "customer": so2.customer, + "grand_total": so2.grand_total, + }, + ) + + pln2.get_so_items() + + self.assertEqual(pln2.po_items[0].bom_no, bom2.name) + def test_production_plan_combine_items(self): "Test combining FG items in Production Plan." item = "Test Production Item 1" diff --git a/erpnext/selling/doctype/sales_order_item/sales_order_item.json b/erpnext/selling/doctype/sales_order_item/sales_order_item.json index 5c7e10a232..07565c3928 100644 --- a/erpnext/selling/doctype/sales_order_item/sales_order_item.json +++ b/erpnext/selling/doctype/sales_order_item/sales_order_item.json @@ -84,6 +84,7 @@ "actual_qty", "ordered_qty", "planned_qty", + "production_plan_qty", "column_break_69", "work_order_qty", "delivered_qty", @@ -882,12 +883,19 @@ "print_hide": 1, "read_only": 1, "report_hide": 1 + }, + { + "fieldname": "production_plan_qty", + "fieldtype": "Float", + "label": "Production Plan Qty", + "no_copy": 1, + "read_only": 1 } ], "idx": 1, "istable": 1, "links": [], - "modified": "2023-04-04 10:44:05.707488", + "modified": "2023-07-28 14:56:42.031636", "modified_by": "Administrator", "module": "Selling", "name": "Sales Order Item", From f5761e79657f744aff6b1ed964f9ffdf6e7d5a9f Mon Sep 17 00:00:00 2001 From: Gursheen Anand Date: Fri, 28 Jul 2023 16:01:30 +0530 Subject: [PATCH 068/101] refactor: future payments query --- erpnext/accounts/party.py | 2 +- .../accounts_receivable.py | 110 +++++++++--------- 2 files changed, 57 insertions(+), 55 deletions(-) diff --git a/erpnext/accounts/party.py b/erpnext/accounts/party.py index d5f8634a7e..895c314510 100644 --- a/erpnext/accounts/party.py +++ b/erpnext/accounts/party.py @@ -928,7 +928,7 @@ def get_partywise_advanced_payment_amount( frappe.qb.from_(gle) .select(gle.party) .where( - (gle.party_type.isin(party_type)) & (gle.against_voucher == None) & (gle.is_cancelled == 0) + (gle.party_type.isin(party_type)) & (gle.against_voucher.isnull()) & (gle.is_cancelled == 0) ) .groupby(gle.party) ) diff --git a/erpnext/accounts/report/accounts_receivable/accounts_receivable.py b/erpnext/accounts/report/accounts_receivable/accounts_receivable.py index 5b92dcd717..11bbb6f1e4 100755 --- a/erpnext/accounts/report/accounts_receivable/accounts_receivable.py +++ b/erpnext/accounts/report/accounts_receivable/accounts_receivable.py @@ -7,7 +7,7 @@ from collections import OrderedDict import frappe from frappe import _, qb, scrub from frappe.query_builder import Criterion -from frappe.query_builder.functions import Date +from frappe.query_builder.functions import Date, Sum from frappe.utils import cint, cstr, flt, getdate, nowdate from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import ( @@ -539,65 +539,67 @@ class ReceivablePayableReport(object): self.future_payments.setdefault((d.invoice_no, d.party), []).append(d) def get_future_payments_from_payment_entry(self): - return frappe.db.sql( - """ - select - ref.reference_name as invoice_no, - payment_entry.party, - payment_entry.party_type, - payment_entry.posting_date as future_date, - ref.allocated_amount as future_amount, - payment_entry.reference_no as future_ref - from - `tabPayment Entry` as payment_entry inner join `tabPayment Entry Reference` as ref - on - (ref.parent = payment_entry.name) - where - payment_entry.docstatus < 2 - and payment_entry.posting_date > %s - and payment_entry.party_type in %s - """, - (self.filters.report_date, self.party_type), - as_dict=1, - ) + pe = frappe.qb.DocType("Payment Entry") + pe_ref = frappe.qb.DocType("Payment Entry Reference") + return ( + frappe.qb.from_(pe) + .inner_join(pe_ref) + .on(pe_ref.parent == pe.name) + .select( + (pe_ref.reference_name).as_("invoice_no"), + pe.party, + pe.party_type, + (pe.posting_date).as_("future_date"), + (pe_ref.allocated_amount).as_("future_amount"), + (pe.reference_no).as_("future_ref"), + ) + .where( + (pe.docstatus < 2) + & (pe.posting_date > self.filters.report_date) + & (pe.party_type.isin(self.party_type)) + ) + ).run(as_dict=True) def get_future_payments_from_journal_entry(self): - if self.filters.get("party"): - amount_field = ( - "jea.debit_in_account_currency - jea.credit_in_account_currency" - if self.account_type == "Payable" - else "jea.credit_in_account_currency - jea.debit_in_account_currency" - ) - else: - amount_field = "jea.debit - " if self.account_type == "Payable" else "jea.credit" - - return frappe.db.sql( - """ - select - jea.reference_name as invoice_no, + je = frappe.qb.DocType("Journal Entry") + jea = frappe.qb.DocType("Journal Entry Account") + query = ( + frappe.qb.from_(je) + .inner_join(jea) + .on(jea.parent == je.name) + .select( + jea.reference_name.as_("invoice_no"), jea.party, jea.party_type, - je.posting_date as future_date, - sum('{0}') as future_amount, - je.cheque_no as future_ref - from - `tabJournal Entry` as je inner join `tabJournal Entry Account` as jea - on - (jea.parent = je.name) - where - je.docstatus < 2 - and je.posting_date > %s - and jea.party_type in %s - and jea.reference_name is not null and jea.reference_name != '' - group by je.name, jea.reference_name - having future_amount > 0 - """.format( - amount_field - ), - (self.filters.report_date, self.party_type), - as_dict=1, + je.posting_date.as_("future_date"), + je.cheque_no.as_("future_ref"), + ) + .where( + (je.docstatus < 2) + & (je.posting_date > self.filters.report_date) + & (jea.party_type.isin(self.party_type)) + & (jea.reference_name.isnotnull()) + & (jea.reference_name != "") + ) ) + if self.filters.get("party"): + if self.account_type == "Payable": + query = query.select( + Sum(jea.debit_in_account_currency - jea.credit_in_account_currency).as_("future_amount") + ) + else: + query = query.select( + Sum(jea.credit_in_account_currency - jea.debit_in_account_currency).as_("future_amount") + ) + else: + query = query.select( + Sum(jea.debit if self.account_type == "Payable" else jea.credit).as_("future_amount") + ) + + query = query.having(qb.Field("future_amount") > 0) + return query.run(as_dict=True) + def allocate_future_payments(self, row): # future payments are captured in additional columns # this method allocates pending future payments against a voucher to From 58d867503b38fb72c850a2008ec5f33455e643d0 Mon Sep 17 00:00:00 2001 From: s-aga-r Date: Fri, 28 Jul 2023 15:53:54 +0530 Subject: [PATCH 069/101] perf: use `LEFT JOIN` instead of `NOT EXISTS` --- .../bom_update_log/bom_updation_utils.py | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/erpnext/manufacturing/doctype/bom_update_log/bom_updation_utils.py b/erpnext/manufacturing/doctype/bom_update_log/bom_updation_utils.py index b90cfd942f..a2919b79b8 100644 --- a/erpnext/manufacturing/doctype/bom_update_log/bom_updation_utils.py +++ b/erpnext/manufacturing/doctype/bom_update_log/bom_updation_utils.py @@ -157,12 +157,19 @@ def get_next_higher_level_boms( def get_leaf_boms() -> List[str]: "Get BOMs that have no dependencies." - return frappe.db.sql_list( - """select name from `tabBOM` bom - where docstatus=1 and is_active=1 - and not exists(select bom_no from `tabBOM Item` - where parent=bom.name and bom_no !='')""" - ) + bom = frappe.qb.DocType("BOM") + bom_item = frappe.qb.DocType("BOM Item") + + boms = ( + frappe.qb.from_(bom) + .left_join(bom_item) + .on((bom.name == bom_item.parent) & (bom_item.bom_no != "")) + .select(bom.name) + .where((bom.docstatus == 1) & (bom.is_active == 1) & (bom_item.bom_no.isnull())) + .distinct() + ).run(pluck=True) + + return boms def _generate_dependence_map() -> defaultdict: From 148d466ae53b4754b5ea7209d9d67f6731f6a2d3 Mon Sep 17 00:00:00 2001 From: s-aga-r Date: Fri, 28 Jul 2023 17:05:17 +0530 Subject: [PATCH 070/101] fix: long queue `process_boms_cost_level_wise` --- erpnext/manufacturing/doctype/bom_update_log/bom_update_log.py | 1 + 1 file changed, 1 insertion(+) diff --git a/erpnext/manufacturing/doctype/bom_update_log/bom_update_log.py b/erpnext/manufacturing/doctype/bom_update_log/bom_update_log.py index 17b5aae966..e9867468f9 100644 --- a/erpnext/manufacturing/doctype/bom_update_log/bom_update_log.py +++ b/erpnext/manufacturing/doctype/bom_update_log/bom_update_log.py @@ -93,6 +93,7 @@ class BOMUpdateLog(Document): else: frappe.enqueue( method="erpnext.manufacturing.doctype.bom_update_log.bom_update_log.process_boms_cost_level_wise", + queue="long", update_doc=self, now=frappe.flags.in_test, enqueue_after_commit=True, From c82cb379a56d73c9ae1f847f746fd6e106655b7c Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Fri, 28 Jul 2023 17:21:05 +0530 Subject: [PATCH 071/101] fix: change fieldtype from Currency to Float for the valuation rate in the stock report --- erpnext/stock/report/stock_balance/stock_balance.py | 3 +-- erpnext/stock/report/stock_ledger/stock_ledger.py | 4 ++-- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/erpnext/stock/report/stock_balance/stock_balance.py b/erpnext/stock/report/stock_balance/stock_balance.py index f2c2e27cd9..d60e9b57ab 100644 --- a/erpnext/stock/report/stock_balance/stock_balance.py +++ b/erpnext/stock/report/stock_balance/stock_balance.py @@ -446,10 +446,9 @@ class StockBalanceReport(object): { "label": _("Valuation Rate"), "fieldname": "val_rate", - "fieldtype": "Currency", + "fieldtype": "Float", "width": 90, "convertible": "rate", - "options": "currency", }, { "label": _("Reserved Stock"), diff --git a/erpnext/stock/report/stock_ledger/stock_ledger.py b/erpnext/stock/report/stock_ledger/stock_ledger.py index 77bc4e004d..ed28ed3ee4 100644 --- a/erpnext/stock/report/stock_ledger/stock_ledger.py +++ b/erpnext/stock/report/stock_ledger/stock_ledger.py @@ -196,7 +196,7 @@ def get_columns(filters): { "label": _("Avg Rate (Balance Stock)"), "fieldname": "valuation_rate", - "fieldtype": "Currency", + "fieldtype": "Float", "width": 180, "options": "Company:company:default_currency", "convertible": "rate", @@ -204,7 +204,7 @@ def get_columns(filters): { "label": _("Valuation Rate"), "fieldname": "in_out_rate", - "fieldtype": "Currency", + "fieldtype": "Float", "width": 140, "options": "Company:company:default_currency", "convertible": "rate", From 1c687a4afd43591e561cde29672a4d680c37d888 Mon Sep 17 00:00:00 2001 From: xdlumertz Date: Thu, 27 Jul 2023 18:44:18 -0300 Subject: [PATCH 072/101] fix: removed "fetch_from" * fix: removed ("fetch_from": "goal.objective") The field ended up being disabled because of this. --- .../quality_goal_objective/quality_goal_objective.json | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/erpnext/quality_management/doctype/quality_goal_objective/quality_goal_objective.json b/erpnext/quality_management/doctype/quality_goal_objective/quality_goal_objective.json index e3dbd660b5..010888dd31 100644 --- a/erpnext/quality_management/doctype/quality_goal_objective/quality_goal_objective.json +++ b/erpnext/quality_management/doctype/quality_goal_objective/quality_goal_objective.json @@ -1,4 +1,5 @@ { + "actions": [], "autoname": "format:{####}", "creation": "2019-05-26 15:03:43.996455", "doctype": "DocType", @@ -12,7 +13,6 @@ ], "fields": [ { - "fetch_from": "goal.objective", "fieldname": "objective", "fieldtype": "Text", "in_list_view": 1, @@ -38,14 +38,17 @@ } ], "istable": 1, - "modified": "2019-05-26 16:12:54.832058", + "links": [], + "modified": "2023-07-28 18:10:23.351246", "modified_by": "Administrator", "module": "Quality Management", "name": "Quality Goal Objective", + "naming_rule": "Expression", "owner": "Administrator", "permissions": [], "quick_entry": 1, "sort_field": "modified", "sort_order": "DESC", + "states": [], "track_changes": 1 } \ No newline at end of file From bc470591ac615552bad5a5d370045ed0b68937ba Mon Sep 17 00:00:00 2001 From: xdlumertz Date: Fri, 28 Jul 2023 12:31:29 -0300 Subject: [PATCH 073/101] fix: translate fix: translate --- erpnext/accounts/doctype/sales_invoice/sales_invoice.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py index 974a876429..f5ee2285d2 100644 --- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py +++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py @@ -1833,7 +1833,7 @@ def validate_inter_company_party(doctype, party, company, inter_company_referenc doc = frappe.get_doc(ref_doc, inter_company_reference) ref_party = doc.supplier if doctype in ["Sales Invoice", "Sales Order"] else doc.customer if not frappe.db.get_value(partytype, {"represents_company": doc.company}, "name") == party: - frappe.throw(_("Invalid {0} for Inter Company Transaction.").format(partytype)) + frappe.throw(_("Invalid {0} for Inter Company Transaction.").format(_(partytype))) if not frappe.get_cached_value(ref_partytype, ref_party, "represents_company") == company: frappe.throw(_("Invalid Company for Inter Company Transaction.")) @@ -1847,7 +1847,7 @@ def validate_inter_company_party(doctype, party, company, inter_company_referenc if not company in companies: frappe.throw( _("{0} not allowed to transact with {1}. Please change the Company.").format( - partytype, company + _(partytype), company ) ) From 3b5805541017ae3abc2bafa17dcc698fe6922a73 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Fri, 28 Jul 2023 21:02:21 +0530 Subject: [PATCH 074/101] refactor(test): introduce and make use of mixins in unit tests (#36382) * refactor(test): create and use test mixin * chore(test): replace get_user_default with variable --- .../test_deferred_revenue_and_expense.py | 194 ++++++------------ erpnext/accounts/test/accounts_mixin.py | 80 ++++++++ 2 files changed, 144 insertions(+), 130 deletions(-) create mode 100644 erpnext/accounts/test/accounts_mixin.py diff --git a/erpnext/accounts/report/deferred_revenue_and_expense/test_deferred_revenue_and_expense.py b/erpnext/accounts/report/deferred_revenue_and_expense/test_deferred_revenue_and_expense.py index c84b843f1f..28d0c20a91 100644 --- a/erpnext/accounts/report/deferred_revenue_and_expense/test_deferred_revenue_and_expense.py +++ b/erpnext/accounts/report/deferred_revenue_and_expense/test_deferred_revenue_and_expense.py @@ -2,6 +2,7 @@ import unittest import frappe from frappe import qb +from frappe.tests.utils import FrappeTestCase, change_settings from frappe.utils import nowdate from erpnext.accounts.doctype.account.test_account import create_account @@ -10,16 +11,15 @@ from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sal from erpnext.accounts.report.deferred_revenue_and_expense.deferred_revenue_and_expense import ( Deferred_Revenue_and_Expense_Report, ) +from erpnext.accounts.test.accounts_mixin import AccountsTestMixin from erpnext.accounts.utils import get_fiscal_year from erpnext.buying.doctype.supplier.test_supplier import create_supplier from erpnext.stock.doctype.item.test_item import create_item -class TestDeferredRevenueAndExpense(unittest.TestCase): +class TestDeferredRevenueAndExpense(FrappeTestCase, AccountsTestMixin): @classmethod def setUpClass(self): - clear_accounts_and_items() - create_company() self.maxDiff = None def clear_old_entries(self): @@ -51,55 +51,58 @@ class TestDeferredRevenueAndExpense(unittest.TestCase): if deferred_invoices: qb.from_(pinv).delete().where(pinv.name.isin(deferred_invoices)).run() - def test_deferred_revenue(self): - self.clear_old_entries() + def setup_deferred_accounts_and_items(self): + # created deferred expense accounts, if not found + self.deferred_revenue_account = create_account( + account_name="Deferred Revenue", + parent_account="Current Liabilities - " + self.company_abbr, + company=self.company, + ) # created deferred expense accounts, if not found - deferred_revenue_account = create_account( - account_name="Deferred Revenue", - parent_account="Current Liabilities - _CD", - company="_Test Company DR", + self.deferred_expense_account = create_account( + account_name="Deferred Expense", + parent_account="Current Assets - " + self.company_abbr, + company=self.company, ) - acc_settings = frappe.get_doc("Accounts Settings", "Accounts Settings") - acc_settings.book_deferred_entries_based_on = "Months" - acc_settings.save() + def setUp(self): + self.create_company() + self.create_customer("_Test Customer") + self.create_supplier("_Test Furniture Supplier") + self.setup_deferred_accounts_and_items() + self.clear_old_entries() - customer = frappe.new_doc("Customer") - customer.customer_name = "_Test Customer DR" - customer.type = "Individual" - customer.insert() + def tearDown(self): + frappe.db.rollback() - item = create_item( - "_Test Internet Subscription", - is_stock_item=0, - warehouse="All Warehouses - _CD", - company="_Test Company DR", - ) + @change_settings("Accounts Settings", {"book_deferred_entries_based_on": "Months"}) + def test_deferred_revenue(self): + self.create_item("_Test Internet Subscription", 0, self.warehouse, self.company) + item = frappe.get_doc("Item", self.item) item.enable_deferred_revenue = 1 - item.deferred_revenue_account = deferred_revenue_account + item.deferred_revenue_account = self.deferred_revenue_account item.no_of_months = 3 item.save() si = create_sales_invoice( - item=item.name, - company="_Test Company DR", - customer="_Test Customer DR", - debit_to="Debtors - _CD", + item=self.item, + company=self.company, + customer=self.customer, + debit_to=self.debit_to, posting_date="2021-05-01", - parent_cost_center="Main - _CD", - cost_center="Main - _CD", + parent_cost_center=self.cost_center, + cost_center=self.cost_center, do_not_save=True, rate=300, price_list_rate=300, ) - si.items[0].income_account = "Sales - _CD" + si.items[0].income_account = self.income_account si.items[0].enable_deferred_revenue = 1 si.items[0].service_start_date = "2021-05-01" si.items[0].service_end_date = "2021-08-01" - si.items[0].deferred_revenue_account = deferred_revenue_account - si.items[0].income_account = "Sales - _CD" + si.items[0].deferred_revenue_account = self.deferred_revenue_account si.save() si.submit() @@ -110,7 +113,7 @@ class TestDeferredRevenueAndExpense(unittest.TestCase): start_date="2021-05-01", end_date="2021-08-01", type="Income", - company="_Test Company DR", + company=self.company, ) ) pda.insert() @@ -120,7 +123,7 @@ class TestDeferredRevenueAndExpense(unittest.TestCase): fiscal_year = frappe.get_doc("Fiscal Year", get_fiscal_year(date="2021-05-01")) self.filters = frappe._dict( { - "company": frappe.defaults.get_user_default("Company"), + "company": self.company, "filter_based_on": "Date Range", "period_start_date": "2021-05-01", "period_end_date": "2021-08-01", @@ -142,57 +145,36 @@ class TestDeferredRevenueAndExpense(unittest.TestCase): ] self.assertEqual(report.period_total, expected) + @change_settings("Accounts Settings", {"book_deferred_entries_based_on": "Months"}) def test_deferred_expense(self): - self.clear_old_entries() - - # created deferred expense accounts, if not found - deferred_expense_account = create_account( - account_name="Deferred Expense", - parent_account="Current Assets - _CD", - company="_Test Company DR", - ) - - acc_settings = frappe.get_doc("Accounts Settings", "Accounts Settings") - acc_settings.book_deferred_entries_based_on = "Months" - acc_settings.save() - - supplier = create_supplier( - supplier_name="_Test Furniture Supplier", supplier_group="Local", supplier_type="Company" - ) - supplier.save() - - item = create_item( - "_Test Office Desk", - is_stock_item=0, - warehouse="All Warehouses - _CD", - company="_Test Company DR", - ) + self.create_item("_Test Office Desk", 0, self.warehouse, self.company) + item = frappe.get_doc("Item", self.item) item.enable_deferred_expense = 1 - item.deferred_expense_account = deferred_expense_account + item.deferred_expense_account = self.deferred_expense_account item.no_of_months_exp = 3 item.save() pi = make_purchase_invoice( - item=item.name, - company="_Test Company DR", - supplier="_Test Furniture Supplier", + item=self.item, + company=self.company, + supplier=self.supplier, is_return=False, update_stock=False, posting_date=frappe.utils.datetime.date(2021, 5, 1), - parent_cost_center="Main - _CD", - cost_center="Main - _CD", + parent_cost_center=self.cost_center, + cost_center=self.cost_center, do_not_save=True, rate=300, price_list_rate=300, - warehouse="All Warehouses - _CD", + warehouse=self.warehouse, qty=1, ) pi.set_posting_time = True pi.items[0].enable_deferred_expense = 1 pi.items[0].service_start_date = "2021-05-01" pi.items[0].service_end_date = "2021-08-01" - pi.items[0].deferred_expense_account = deferred_expense_account - pi.items[0].expense_account = "Office Maintenance Expenses - _CD" + pi.items[0].deferred_expense_account = self.deferred_expense_account + pi.items[0].expense_account = self.expense_account pi.save() pi.submit() @@ -203,7 +185,7 @@ class TestDeferredRevenueAndExpense(unittest.TestCase): start_date="2021-05-01", end_date="2021-08-01", type="Expense", - company="_Test Company DR", + company=self.company, ) ) pda.insert() @@ -213,7 +195,7 @@ class TestDeferredRevenueAndExpense(unittest.TestCase): fiscal_year = frappe.get_doc("Fiscal Year", get_fiscal_year(date="2021-05-01")) self.filters = frappe._dict( { - "company": frappe.defaults.get_user_default("Company"), + "company": self.company, "filter_based_on": "Date Range", "period_start_date": "2021-05-01", "period_end_date": "2021-08-01", @@ -235,52 +217,31 @@ class TestDeferredRevenueAndExpense(unittest.TestCase): ] self.assertEqual(report.period_total, expected) + @change_settings("Accounts Settings", {"book_deferred_entries_based_on": "Months"}) def test_zero_months(self): - self.clear_old_entries() - # created deferred expense accounts, if not found - deferred_revenue_account = create_account( - account_name="Deferred Revenue", - parent_account="Current Liabilities - _CD", - company="_Test Company DR", - ) - - acc_settings = frappe.get_doc("Accounts Settings", "Accounts Settings") - acc_settings.book_deferred_entries_based_on = "Months" - acc_settings.save() - - customer = frappe.new_doc("Customer") - customer.customer_name = "_Test Customer DR" - customer.type = "Individual" - customer.insert() - - item = create_item( - "_Test Internet Subscription", - is_stock_item=0, - warehouse="All Warehouses - _CD", - company="_Test Company DR", - ) + self.create_item("_Test Internet Subscription", 0, self.warehouse, self.company) + item = frappe.get_doc("Item", self.item) item.enable_deferred_revenue = 1 - item.deferred_revenue_account = deferred_revenue_account + item.deferred_revenue_account = self.deferred_revenue_account item.no_of_months = 0 item.save() si = create_sales_invoice( item=item.name, - company="_Test Company DR", - customer="_Test Customer DR", - debit_to="Debtors - _CD", + company=self.company, + customer=self.customer, + debit_to=self.debit_to, posting_date="2021-05-01", - parent_cost_center="Main - _CD", - cost_center="Main - _CD", + parent_cost_center=self.cost_center, + cost_center=self.cost_center, do_not_save=True, rate=300, price_list_rate=300, ) si.items[0].enable_deferred_revenue = 1 - si.items[0].income_account = "Sales - _CD" - si.items[0].deferred_revenue_account = deferred_revenue_account - si.items[0].income_account = "Sales - _CD" + si.items[0].income_account = self.income_account + si.items[0].deferred_revenue_account = self.deferred_revenue_account si.save() si.submit() @@ -291,7 +252,7 @@ class TestDeferredRevenueAndExpense(unittest.TestCase): start_date="2021-05-01", end_date="2021-08-01", type="Income", - company="_Test Company DR", + company=self.company, ) ) pda.insert() @@ -301,7 +262,7 @@ class TestDeferredRevenueAndExpense(unittest.TestCase): fiscal_year = frappe.get_doc("Fiscal Year", get_fiscal_year(date="2021-05-01")) self.filters = frappe._dict( { - "company": frappe.defaults.get_user_default("Company"), + "company": self.company, "filter_based_on": "Date Range", "period_start_date": "2021-05-01", "period_end_date": "2021-08-01", @@ -322,30 +283,3 @@ class TestDeferredRevenueAndExpense(unittest.TestCase): {"key": "aug_2021", "total": 0, "actual": 0}, ] self.assertEqual(report.period_total, expected) - - -def create_company(): - company = frappe.db.exists("Company", "_Test Company DR") - if not company: - company = frappe.new_doc("Company") - company.company_name = "_Test Company DR" - company.default_currency = "INR" - company.chart_of_accounts = "Standard" - company.insert() - - -def clear_accounts_and_items(): - item = qb.DocType("Item") - account = qb.DocType("Account") - customer = qb.DocType("Customer") - supplier = qb.DocType("Supplier") - - qb.from_(account).delete().where( - (account.account_name == "Deferred Revenue") - | (account.account_name == "Deferred Expense") & (account.company == "_Test Company DR") - ).run() - qb.from_(item).delete().where( - (item.item_code == "_Test Internet Subscription") | (item.item_code == "_Test Office Rent") - ).run() - qb.from_(customer).delete().where(customer.customer_name == "_Test Customer DR").run() - qb.from_(supplier).delete().where(supplier.supplier_name == "_Test Furniture Supplier").run() diff --git a/erpnext/accounts/test/accounts_mixin.py b/erpnext/accounts/test/accounts_mixin.py new file mode 100644 index 0000000000..c82164ef64 --- /dev/null +++ b/erpnext/accounts/test/accounts_mixin.py @@ -0,0 +1,80 @@ +import frappe + +from erpnext.stock.doctype.item.test_item import create_item + + +class AccountsTestMixin: + def create_customer(self, customer_name, currency=None): + if not frappe.db.exists("Customer", customer_name): + customer = frappe.new_doc("Customer") + customer.customer_name = customer_name + customer.type = "Individual" + + if currency: + customer.default_currency = currency + customer.save() + self.customer = customer.name + else: + self.customer = customer_name + + def create_supplier(self, supplier_name, currency=None): + if not frappe.db.exists("Supplier", supplier_name): + supplier = frappe.new_doc("Supplier") + supplier.supplier_name = supplier_name + supplier.supplier_type = "Individual" + supplier.supplier_group = "Local" + + if currency: + supplier.default_currency = currency + supplier.save() + self.supplier = supplier.name + else: + self.supplier = supplier_name + + def create_item(self, item_name, is_stock=0, warehouse=None, company=None): + item = create_item(item_name, is_stock_item=is_stock, warehouse=warehouse, company=company) + self.item = item.name + + def create_company(self, company_name="_Test Company", abbr="_TC"): + self.company_abbr = abbr + if frappe.db.exists("Company", company_name): + company = frappe.get_doc("Company", company_name) + else: + company = frappe.get_doc( + { + "doctype": "Company", + "company_name": company_name, + "country": "India", + "default_currency": "INR", + "create_chart_of_accounts_based_on": "Standard Template", + "chart_of_accounts": "Standard", + } + ) + company = company.save() + + self.company = company.name + self.cost_center = company.cost_center + self.warehouse = "Stores - " + abbr + self.finished_warehouse = "Finished Goods - " + abbr + self.income_account = "Sales - " + abbr + self.expense_account = "Cost of Goods Sold - " + abbr + self.debit_to = "Debtors - " + abbr + self.debit_usd = "Debtors USD - " + abbr + self.cash = "Cash - " + abbr + self.creditors = "Creditors - " + abbr + + # create bank account + bank_account = "HDFC - " + abbr + if frappe.db.exists("Account", bank_account): + self.bank = bank_account + else: + bank_acc = frappe.get_doc( + { + "doctype": "Account", + "account_name": "HDFC", + "parent_account": "Bank Accounts - " + abbr, + "company": self.company, + } + ) + bank_acc.save() + self.bank = bank_acc.name From ecca9cb023f91c7504e53b4f709b01622f156fd9 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Sat, 29 Jul 2023 11:52:54 +0530 Subject: [PATCH 075/101] fix: Add company filters for account --- .../accounting_dimension.js | 11 +++++++ .../accounting_dimension.py | 10 +++++++ .../purchase_invoice/test_purchase_invoice.py | 29 +++++++++++-------- erpnext/accounts/general_ledger.py | 7 ++++- .../trial_balance/test_trial_balance.py | 3 +- 5 files changed, 46 insertions(+), 14 deletions(-) diff --git a/erpnext/accounts/doctype/accounting_dimension/accounting_dimension.js b/erpnext/accounts/doctype/accounting_dimension/accounting_dimension.js index 2fa1d53c60..2f53f7b640 100644 --- a/erpnext/accounts/doctype/accounting_dimension/accounting_dimension.js +++ b/erpnext/accounts/doctype/accounting_dimension/accounting_dimension.js @@ -15,6 +15,17 @@ frappe.ui.form.on('Accounting Dimension', { }; }); + frm.set_query("offsetting_account", "dimension_defaults", function(doc, cdt, cdn) { + let d = locals[cdt][cdn]; + return { + filters: { + company: d.company, + root_type: ["in", ["Asset", "Liability"]], + is_group: 0 + } + } + }); + if (!frm.is_new()) { frm.add_custom_button(__('Show {0}', [frm.doc.document_type]), function () { frappe.set_route("List", frm.doc.document_type); diff --git a/erpnext/accounts/doctype/accounting_dimension/accounting_dimension.py b/erpnext/accounts/doctype/accounting_dimension/accounting_dimension.py index 81ff6a52db..ea44734e75 100644 --- a/erpnext/accounts/doctype/accounting_dimension/accounting_dimension.py +++ b/erpnext/accounts/doctype/accounting_dimension/accounting_dimension.py @@ -39,6 +39,8 @@ class AccountingDimension(Document): if not self.is_new(): self.validate_document_type_change() + self.validate_dimension_defaults() + def validate_document_type_change(self): doctype_before_save = frappe.db.get_value("Accounting Dimension", self.name, "document_type") if doctype_before_save != self.document_type: @@ -46,6 +48,14 @@ class AccountingDimension(Document): message += _("Please create a new Accounting Dimension if required.") frappe.throw(message) + def validate_dimension_defaults(self): + companies = [] + for default in self.get("dimension_defaults"): + if default.company not in companies: + companies.append(default.company) + else: + frappe.throw(_("Company {0} is added more than once").format(frappe.bold(default.company))) + def after_insert(self): if frappe.flags.in_test: make_dimension_in_accounting_doctypes(doc=self) diff --git a/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py b/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py index d2f19a1b25..486e01e00f 100644 --- a/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py +++ b/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py @@ -1774,10 +1774,10 @@ class TestPurchaseInvoice(unittest.TestCase, StockTestMixin): pi.submit() expected_gle = [ - ["_Test Account Cost for Goods Sold - _TC", 1000, 0.0, nowdate(), {"branch": branch2.branch}], - ["Creditors - _TC", 0.0, 1000, nowdate(), {"branch": branch1.branch}], - ["Offsetting - _TC", 1000, 0.0, nowdate(), {"branch": branch1.branch}], - ["Offsetting - _TC", 0.0, 1000, nowdate(), {"branch": branch2.branch}], + ["_Test Account Cost for Goods Sold - _TC", 1000, 0.0, nowdate(), branch2.branch], + ["Creditors - _TC", 0.0, 1000, nowdate(), branch1.branch], + ["Offsetting - _TC", 1000, 0.0, nowdate(), branch1.branch], + ["Offsetting - _TC", 0.0, 1000, nowdate(), branch2.branch], ] check_gl_entries( @@ -1786,7 +1786,7 @@ class TestPurchaseInvoice(unittest.TestCase, StockTestMixin): expected_gle, nowdate(), voucher_type="Purchase Invoice", - check_acc_dimensions=True, + additional_columns=["branch"], ) clear_dimension_defaults("Branch") disable_dimension() @@ -1809,7 +1809,7 @@ def check_gl_entries( expected_gle, posting_date, voucher_type="Purchase Invoice", - check_acc_dimensions=False, + additional_columns=None, ): gl = frappe.qb.DocType("GL Entry") query = ( @@ -1823,9 +1823,11 @@ def check_gl_entries( ) .orderby(gl.posting_date, gl.account, gl.creation) ) - if check_acc_dimensions: - for col in list(expected_gle[0][4].keys()): - query = query.select(col) + + if additional_columns: + for col in additional_columns: + query = query.select(gl[col]) + gl_entries = query.run(as_dict=True) for i, gle in enumerate(gl_entries): @@ -1833,9 +1835,12 @@ def check_gl_entries( doc.assertEqual(expected_gle[i][1], gle.debit) doc.assertEqual(expected_gle[i][2], gle.credit) doc.assertEqual(getdate(expected_gle[i][3]), gle.posting_date) - if check_acc_dimensions: - for acc_dimension in expected_gle[i][4]: - doc.assertEqual(expected_gle[i][4][acc_dimension], gle[acc_dimension]) + + if additional_columns: + j = 4 + for col in additional_columns: + doc.assertEqual(expected_gle[i][j], gle[col]) + j += 1 def create_tax_witholding_category(category_name, company, account): diff --git a/erpnext/accounts/general_ledger.py b/erpnext/accounts/general_ledger.py index a348911f03..addc9d5754 100644 --- a/erpnext/accounts/general_ledger.py +++ b/erpnext/accounts/general_ledger.py @@ -30,8 +30,8 @@ def make_gl_entries( from_repost=False, ): if gl_map: - make_acc_dimensions_offsetting_entry(gl_map) if not cancel: + make_acc_dimensions_offsetting_entry(gl_map) validate_accounting_period(gl_map) validate_disabled_accounts(gl_map) gl_map = process_gl_map(gl_map, merge_entries) @@ -64,6 +64,7 @@ def make_acc_dimensions_offsetting_entry(gl_map): return offsetting_entries = [] + for gle in gl_map: for dimension in accounting_dimensions_to_offset: offsetting_entry = gle.copy() @@ -82,12 +83,14 @@ def make_acc_dimensions_offsetting_entry(gl_map): ) offsetting_entry["against_voucher_type"] = None offsetting_entries.append(offsetting_entry) + gl_map += offsetting_entries def get_accounting_dimensions_for_offsetting_entry(gl_map, company): acc_dimension = frappe.qb.DocType("Accounting Dimension") dimension_detail = frappe.qb.DocType("Accounting Dimension Detail") + acc_dimensions = ( frappe.qb.from_(acc_dimension) .inner_join(dimension_detail) @@ -99,11 +102,13 @@ def get_accounting_dimensions_for_offsetting_entry(gl_map, company): & (dimension_detail.automatically_post_balancing_accounting_entry == 1) ) ).run(as_dict=True) + accounting_dimensions_to_offset = [] for acc_dimension in acc_dimensions: values = set([entry.get(acc_dimension.fieldname) for entry in gl_map]) if len(values) > 1: accounting_dimensions_to_offset.append(acc_dimension) + return accounting_dimensions_to_offset diff --git a/erpnext/accounts/report/trial_balance/test_trial_balance.py b/erpnext/accounts/report/trial_balance/test_trial_balance.py index cd0429be4c..4682ac4500 100644 --- a/erpnext/accounts/report/trial_balance/test_trial_balance.py +++ b/erpnext/accounts/report/trial_balance/test_trial_balance.py @@ -91,7 +91,8 @@ def create_accounting_dimension(**args): accounting_dimension = frappe.new_doc("Accounting Dimension") accounting_dimension.document_type = document_type accounting_dimension.insert() - accounting_dimension.save() + + accounting_dimension.set("dimension_defaults", []) accounting_dimension.append( "dimension_defaults", { From c0642cf528e2054192f5d16dd9d5c1a8162069cf Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Sat, 29 Jul 2023 15:02:11 +0530 Subject: [PATCH 076/101] fix: only publish repost progress to doc subscriber (#36400) Huge size of string gets blasted to everyone on site. Due to some memory leak (cause unknown) till sockets are open the strings are also in process' memory. related https://github.com/frappe/frappe/issues/21863 --- erpnext/stock/stock_ledger.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/erpnext/stock/stock_ledger.py b/erpnext/stock/stock_ledger.py index 5abb8e827f..248b7056a0 100644 --- a/erpnext/stock/stock_ledger.py +++ b/erpnext/stock/stock_ledger.py @@ -358,6 +358,8 @@ def update_args_in_repost_item_valuation( "current_index": index, "total_reposting_count": len(args), }, + doctype=doc.doctype, + docname=doc.name, ) From 1ddfaa7605f710b2fd12ccdfa38824cd086f576d Mon Sep 17 00:00:00 2001 From: Gursheen Kaur Anand <40693548+GursheenK@users.noreply.github.com> Date: Mon, 31 Jul 2023 14:29:20 +0530 Subject: [PATCH 077/101] fix: ignore cancelled gle in voucher-wise balance report (#36417) fix: ignore cancelled gle --- .../accounts/report/voucher_wise_balance/voucher_wise_balance.py | 1 + 1 file changed, 1 insertion(+) diff --git a/erpnext/accounts/report/voucher_wise_balance/voucher_wise_balance.py b/erpnext/accounts/report/voucher_wise_balance/voucher_wise_balance.py index 5ab3611b9a..bd9e9fccad 100644 --- a/erpnext/accounts/report/voucher_wise_balance/voucher_wise_balance.py +++ b/erpnext/accounts/report/voucher_wise_balance/voucher_wise_balance.py @@ -46,6 +46,7 @@ def get_data(filters): .select( gle.voucher_type, gle.voucher_no, Sum(gle.debit).as_("debit"), Sum(gle.credit).as_("credit") ) + .where(gle.is_cancelled == 0) .groupby(gle.voucher_no) ) query = apply_filters(query, filters, gle) From 05b07e098a7f7e5b11f0a18bd62005dc098c5995 Mon Sep 17 00:00:00 2001 From: xdlumertz Date: Mon, 31 Jul 2023 09:04:55 -0300 Subject: [PATCH 078/101] fix: process_owner is not link User (#36420) -Changed "fetch from" since field is not a binding field -Change field "full_name" from Hidden to Read Only --- .../doctype/non_conformance/non_conformance.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/erpnext/quality_management/doctype/non_conformance/non_conformance.json b/erpnext/quality_management/doctype/non_conformance/non_conformance.json index 8dfe2d6859..e6b87449ce 100644 --- a/erpnext/quality_management/doctype/non_conformance/non_conformance.json +++ b/erpnext/quality_management/doctype/non_conformance/non_conformance.json @@ -62,10 +62,10 @@ "fieldtype": "Column Break" }, { - "fetch_from": "process_owner.full_name", + "fetch_from": "procedure.process_owner_full_name", "fieldname": "full_name", "fieldtype": "Data", - "hidden": 1, + "read_only": 1, "label": "Full Name" }, { @@ -81,7 +81,7 @@ ], "index_web_pages_for_search": 1, "links": [], - "modified": "2021-02-26 15:27:47.247814", + "modified": "2023-07-31 08:10:47.247814", "modified_by": "Administrator", "module": "Quality Management", "name": "Non Conformance", From ce36d1f668425539a4937dfca4cefa03dc96648d Mon Sep 17 00:00:00 2001 From: Vimal Date: Mon, 31 Jul 2023 13:21:31 +0100 Subject: [PATCH 079/101] fix: job card suggest holiday as start date (#35958) --- erpnext/manufacturing/doctype/workstation/workstation.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/manufacturing/doctype/workstation/workstation.py b/erpnext/manufacturing/doctype/workstation/workstation.py index d5b6d37d67..ac271b7144 100644 --- a/erpnext/manufacturing/doctype/workstation/workstation.py +++ b/erpnext/manufacturing/doctype/workstation/workstation.py @@ -114,7 +114,7 @@ class Workstation(Document): if schedule_date in tuple(get_holidays(self.holiday_list)): schedule_date = add_days(schedule_date, 1) - self.validate_workstation_holiday(schedule_date, skip_holiday_list_check=True) + return self.validate_workstation_holiday(schedule_date, skip_holiday_list_check=True) return schedule_date From 652398fad25d7d6f457ef49667a09d91a8abd8e4 Mon Sep 17 00:00:00 2001 From: xdlumertz Date: Mon, 31 Jul 2023 09:34:33 -0300 Subject: [PATCH 080/101] fix: Defined "Open" Status as default (#36421) Defined "Open" Status as default of the child doctype (Quality Review Objective), because without it the main doctype (Quality Review) has "Passed" status. This happens because in the "set_status" function, the status is updated according to the status of the child records. --- .../quality_review_objective/quality_review_objective.json | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/erpnext/quality_management/doctype/quality_review_objective/quality_review_objective.json b/erpnext/quality_management/doctype/quality_review_objective/quality_review_objective.json index 3a750c21d6..5ddf0f2a0b 100644 --- a/erpnext/quality_management/doctype/quality_review_objective/quality_review_objective.json +++ b/erpnext/quality_management/doctype/quality_review_objective/quality_review_objective.json @@ -56,6 +56,7 @@ "fieldtype": "Column Break" }, { + "default": "Open", "columns": 2, "fieldname": "status", "fieldtype": "Select", @@ -67,7 +68,7 @@ "index_web_pages_for_search": 1, "istable": 1, "links": [], - "modified": "2020-10-27 16:28:20.908637", + "modified": "2023-07-31 09:20:20.908637", "modified_by": "Administrator", "module": "Quality Management", "name": "Quality Review Objective", @@ -76,4 +77,4 @@ "sort_field": "modified", "sort_order": "DESC", "track_changes": 1 -} \ No newline at end of file +} From e8eeeb16e26b9885f5ff88d136e8174c3fbd8203 Mon Sep 17 00:00:00 2001 From: Devin Slauenwhite Date: Mon, 31 Jul 2023 08:47:14 -0400 Subject: [PATCH 081/101] fix: group item reorder by (warehouse, material_request_type) (#35818) * fix: group item reorder by (warehouse, material_request_type) * fix: update reorder error message * chore: linter * fix: correct error message Co-authored-by: s-aga-r * chore: linter --------- Co-authored-by: s-aga-r --- erpnext/stock/doctype/item/item.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/erpnext/stock/doctype/item/item.py b/erpnext/stock/doctype/item/item.py index ef4155e48a..aff958738a 100644 --- a/erpnext/stock/doctype/item/item.py +++ b/erpnext/stock/doctype/item/item.py @@ -395,16 +395,16 @@ class Item(Document): def validate_warehouse_for_reorder(self): """Validate Reorder level table for duplicate and conditional mandatory""" - warehouse = [] + warehouse_material_request_type: list[tuple[str, str]] = [] for d in self.get("reorder_levels"): if not d.warehouse_group: d.warehouse_group = d.warehouse - if d.get("warehouse") and d.get("warehouse") not in warehouse: - warehouse += [d.get("warehouse")] + if (d.get("warehouse"), d.get("material_request_type")) not in warehouse_material_request_type: + warehouse_material_request_type += [(d.get("warehouse"), d.get("material_request_type"))] else: frappe.throw( - _("Row {0}: An Reorder entry already exists for this warehouse {1}").format( - d.idx, d.warehouse + _("Row #{0}: A reorder entry already exists for warehouse {1} with reorder type {2}.").format( + d.idx, d.warehouse, d.material_request_type ), DuplicateReorderRows, ) From f83a100a8d41de0c539599394d9bf7260fef847b Mon Sep 17 00:00:00 2001 From: rohitwaghchaure Date: Mon, 31 Jul 2023 19:13:23 +0530 Subject: [PATCH 082/101] fix: not able to make material request (#36416) --- .../doctype/sales_order/sales_order.py | 4 ++-- .../doctype/sales_order/test_sales_order.py | 20 +++++++++++++++++++ 2 files changed, 22 insertions(+), 2 deletions(-) diff --git a/erpnext/selling/doctype/sales_order/sales_order.py b/erpnext/selling/doctype/sales_order/sales_order.py index 624dadbc4d..45b3f1ddfe 100755 --- a/erpnext/selling/doctype/sales_order/sales_order.py +++ b/erpnext/selling/doctype/sales_order/sales_order.py @@ -733,7 +733,7 @@ def make_material_request(source_name, target_doc=None): # qty is for packed items, because packed items don't have stock_qty field qty = source.get("qty") target.project = source_parent.project - target.qty = qty - requested_item_qty.get(source.name, 0) - source.delivered_qty + target.qty = qty - requested_item_qty.get(source.name, 0) - flt(source.get("delivered_qty")) target.stock_qty = flt(target.qty) * flt(target.conversion_factor) args = target.as_dict().copy() @@ -767,7 +767,7 @@ def make_material_request(source_name, target_doc=None): "doctype": "Material Request Item", "field_map": {"name": "sales_order_item", "parent": "sales_order"}, "condition": lambda doc: not frappe.db.exists("Product Bundle", doc.item_code) - and (doc.stock_qty - doc.delivered_qty) > requested_item_qty.get(doc.name, 0), + and (doc.stock_qty - flt(doc.get("delivered_qty"))) > requested_item_qty.get(doc.name, 0), "postprocess": update_item, }, }, diff --git a/erpnext/selling/doctype/sales_order/test_sales_order.py b/erpnext/selling/doctype/sales_order/test_sales_order.py index 796e2588ff..c85a4fb2f0 100644 --- a/erpnext/selling/doctype/sales_order/test_sales_order.py +++ b/erpnext/selling/doctype/sales_order/test_sales_order.py @@ -549,6 +549,26 @@ class TestSalesOrder(FrappeTestCase): workflow.is_active = 0 workflow.save() + def test_material_request_for_product_bundle(self): + # Create the Material Request from the sales order for the Packing Items + # Check whether the material request has the correct packing item or not. + if not frappe.db.exists("Item", "_Test Product Bundle Item New 1"): + bundle_item = make_item("_Test Product Bundle Item New 1", {"is_stock_item": 0}) + bundle_item.append( + "item_defaults", {"company": "_Test Company", "default_warehouse": "_Test Warehouse - _TC"} + ) + bundle_item.save(ignore_permissions=True) + + make_item("_Packed Item New 2", {"is_stock_item": 1}) + make_product_bundle("_Test Product Bundle Item New 1", ["_Packed Item New 2"], 2) + + so = make_sales_order( + item_code="_Test Product Bundle Item New 1", + ) + + mr = make_material_request(so.name) + self.assertEqual(mr.items[0].item_code, "_Packed Item New 2") + def test_bin_details_of_packed_item(self): # test Update Items with product bundle if not frappe.db.exists("Item", "_Test Product Bundle Item New"): From f31d07554d05f5b325d8770b90e70e9ee214844b Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Mon, 31 Jul 2023 22:13:47 +0530 Subject: [PATCH 083/101] perf: avoid full table scan in sle count check (#36428) --- .../batch_wise_balance_history/batch_wise_balance_history.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/stock/report/batch_wise_balance_history/batch_wise_balance_history.py b/erpnext/stock/report/batch_wise_balance_history/batch_wise_balance_history.py index bdc9d742c0..176a21566a 100644 --- a/erpnext/stock/report/batch_wise_balance_history/batch_wise_balance_history.py +++ b/erpnext/stock/report/batch_wise_balance_history/batch_wise_balance_history.py @@ -17,7 +17,7 @@ def execute(filters=None): if not filters: filters = {} - sle_count = frappe.db.count("Stock Ledger Entry", {"is_cancelled": 0}) + sle_count = frappe.db.count("Stock Ledger Entry") if sle_count > SLE_COUNT_LIMIT and not filters.get("item_code") and not filters.get("warehouse"): frappe.throw(_("Please select either the Item or Warehouse filter to generate the report.")) From 11bd15e58059e2b2d65619437683f030914996f9 Mon Sep 17 00:00:00 2001 From: Gursheen Kaur Anand <40693548+GursheenK@users.noreply.github.com> Date: Mon, 31 Jul 2023 23:27:16 +0530 Subject: [PATCH 084/101] fix: root type in account map for balance sheet (#36303) * fix: root type in account map * fix: fetch gle by root type in consolidated financial statement * refactor: consolidated financial statement gle query * fix: filter accounts by root type --- .../consolidated_financial_statement.py | 75 ++++++++++++------- .../accounts/report/financial_statements.py | 18 ++++- 2 files changed, 64 insertions(+), 29 deletions(-) diff --git a/erpnext/accounts/report/consolidated_financial_statement/consolidated_financial_statement.py b/erpnext/accounts/report/consolidated_financial_statement/consolidated_financial_statement.py index 0b583a1ec6..7c2ebe1d20 100644 --- a/erpnext/accounts/report/consolidated_financial_statement/consolidated_financial_statement.py +++ b/erpnext/accounts/report/consolidated_financial_statement/consolidated_financial_statement.py @@ -6,6 +6,7 @@ from collections import defaultdict import frappe from frappe import _ +from frappe.query_builder import Criterion from frappe.utils import flt, getdate import erpnext @@ -359,6 +360,7 @@ def get_data( accounts_by_name, accounts, ignore_closing_entries=False, + root_type=root_type, ) calculate_values(accounts_by_name, gl_entries_by_account, companies, filters, fiscal_year) @@ -603,6 +605,7 @@ def set_gl_entries_by_account( accounts_by_name, accounts, ignore_closing_entries=False, + root_type=None, ): """Returns a dict like { "account": [gl entries], ... }""" @@ -610,7 +613,6 @@ def set_gl_entries_by_account( "Company", filters.get("company"), ["lft", "rgt"] ) - additional_conditions = get_additional_conditions(from_date, ignore_closing_entries, filters) companies = frappe.db.sql( """ select name, default_currency from `tabCompany` where lft >= %(company_lft)s and rgt <= %(company_rgt)s""", @@ -626,27 +628,42 @@ def set_gl_entries_by_account( ) for d in companies: - gl_entries = frappe.db.sql( - """select gl.posting_date, gl.account, gl.debit, gl.credit, gl.is_opening, gl.company, - gl.fiscal_year, gl.debit_in_account_currency, gl.credit_in_account_currency, gl.account_currency, - acc.account_name, acc.account_number - from `tabGL Entry` gl, `tabAccount` acc where acc.name = gl.account and gl.company = %(company)s and gl.is_cancelled = 0 - {additional_conditions} and gl.posting_date <= %(to_date)s and acc.lft >= %(lft)s and acc.rgt <= %(rgt)s - order by gl.account, gl.posting_date""".format( - additional_conditions=additional_conditions - ), - { - "from_date": from_date, - "to_date": to_date, - "lft": root_lft, - "rgt": root_rgt, - "company": d.name, - "finance_book": filters.get("finance_book"), - "company_fb": frappe.get_cached_value("Company", d.name, "default_finance_book"), - }, - as_dict=True, + gle = frappe.qb.DocType("GL Entry") + account = frappe.qb.DocType("Account") + query = ( + frappe.qb.from_(gle) + .inner_join(account) + .on(account.name == gle.account) + .select( + gle.posting_date, + gle.account, + gle.debit, + gle.credit, + gle.is_opening, + gle.company, + gle.fiscal_year, + gle.debit_in_account_currency, + gle.credit_in_account_currency, + gle.account_currency, + account.account_name, + account.account_number, + ) + .where( + (gle.company == d.name) + & (gle.is_cancelled == 0) + & (gle.posting_date <= to_date) + & (account.lft >= root_lft) + & (account.rgt <= root_rgt) + & (account.root_type <= root_type) + ) + .orderby(gle.account, gle.posting_date) ) + additional_conditions = get_additional_conditions(from_date, ignore_closing_entries, filters, d) + if additional_conditions: + query = query.where(Criterion.all(additional_conditions)) + gl_entries = query.run(as_dict=True) + if filters and filters.get("presentation_currency") != d.default_currency: currency_info["company"] = d.name currency_info["company_currency"] = d.default_currency @@ -716,23 +733,25 @@ def validate_entries(key, entry, accounts_by_name, accounts): accounts.insert(idx + 1, args) -def get_additional_conditions(from_date, ignore_closing_entries, filters): +def get_additional_conditions(from_date, ignore_closing_entries, filters, d): + gle = frappe.qb.DocType("GL Entry") additional_conditions = [] if ignore_closing_entries: - additional_conditions.append("gl.voucher_type != 'Period Closing Voucher'") + additional_conditions.append((gle.voucher_type != "Period Closing Voucher")) if from_date: - additional_conditions.append("gl.posting_date >= %(from_date)s") + additional_conditions.append(gle.posting_date >= from_date) + + finance_book = filters.get("finance_book") + company_fb = frappe.get_cached_value("Company", d.name, "default_finance_book") if filters.get("include_default_book_entries"): - additional_conditions.append( - "(finance_book in (%(finance_book)s, %(company_fb)s, '') OR finance_book IS NULL)" - ) + additional_conditions.append((gle.finance_book.isin([finance_book, company_fb, "", None]))) else: - additional_conditions.append("(finance_book in (%(finance_book)s, '') OR finance_book IS NULL)") + additional_conditions.append((gle.finance_book.isin([finance_book, "", None]))) - return " and {}".format(" and ".join(additional_conditions)) if additional_conditions else "" + return additional_conditions def add_total_row(out, root_type, balance_must_be, companies, company_currency): diff --git a/erpnext/accounts/report/financial_statements.py b/erpnext/accounts/report/financial_statements.py index 26bf315b19..a76dea6a52 100644 --- a/erpnext/accounts/report/financial_statements.py +++ b/erpnext/accounts/report/financial_statements.py @@ -188,6 +188,7 @@ def get_data( filters, gl_entries_by_account, ignore_closing_entries=ignore_closing_entries, + root_type=root_type, ) calculate_values( @@ -417,13 +418,28 @@ def set_gl_entries_by_account( gl_entries_by_account, ignore_closing_entries=False, ignore_opening_entries=False, + root_type=None, ): """Returns a dict like { "account": [gl entries], ... }""" gl_entries = [] + account_filters = { + "company": company, + "is_group": 0, + "lft": (">=", root_lft), + "rgt": ("<=", root_rgt), + } + + if root_type: + account_filters.update( + { + "root_type": root_type, + } + ) + accounts_list = frappe.db.get_all( "Account", - filters={"company": company, "is_group": 0, "lft": (">=", root_lft), "rgt": ("<=", root_rgt)}, + filters=account_filters, pluck="name", ) From bc8d05da0fb856f055e43162747749c7df0825cc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bernd=20Oliver=20S=C3=BCnderhauf?= Date: Sun, 30 Jul 2023 19:04:03 +0200 Subject: [PATCH 085/101] feat: Reallow customizing company abbreviation on setup. --- erpnext/public/js/setup_wizard.js | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/erpnext/public/js/setup_wizard.js b/erpnext/public/js/setup_wizard.js index a913844e18..934fd1f88a 100644 --- a/erpnext/public/js/setup_wizard.js +++ b/erpnext/public/js/setup_wizard.js @@ -24,12 +24,14 @@ erpnext.setup.slides_settings = [ fieldtype: 'Data', reqd: 1 }, + { fieldtype: "Column Break" }, { fieldname: 'company_abbr', label: __('Company Abbreviation'), fieldtype: 'Data', - hidden: 1 + reqd: 1 }, + { fieldtype: "Section Break" }, { fieldname: 'chart_of_accounts', label: __('Chart of Accounts'), options: "", fieldtype: 'Select' @@ -134,18 +136,20 @@ erpnext.setup.slides_settings = [ me.charts_modal(slide, chart_template); }); - slide.get_input("company_name").on("change", function () { + slide.get_input("company_name").on("input", function () { let parts = slide.get_input("company_name").val().split(" "); let abbr = $.map(parts, function (p) { return p ? p.substr(0, 1) : null }).join(""); slide.get_field("company_abbr").set_value(abbr.slice(0, 10).toUpperCase()); }).val(frappe.boot.sysdefaults.company_name || "").trigger("change"); slide.get_input("company_abbr").on("change", function () { - if (slide.get_input("company_abbr").val().length > 10) { + let abbr = slide.get_input("company_abbr").val(); + if (abbr.length > 10) { frappe.msgprint(__("Company Abbreviation cannot have more than 5 characters")); - slide.get_field("company_abbr").set_value(""); + abbr = abbr.slice(0, 10); } - }); + slide.get_field("company_abbr").set_value(abbr); + }).val(frappe.boot.sysdefaults.company_abbr || "").trigger("change"); }, charts_modal: function(slide, chart_template) { From ba15810639577e49ba2a56c2dddb7790ce25ef4e Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Tue, 1 Aug 2023 07:58:09 +0530 Subject: [PATCH 086/101] fix: incorrect usage `get_cached_value` on single doctypes --- erpnext/accounts/deferred_revenue.py | 2 +- erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py | 2 +- erpnext/selling/doctype/sales_order/sales_order.py | 2 +- erpnext/stock/doctype/price_list/price_list.py | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/erpnext/accounts/deferred_revenue.py b/erpnext/accounts/deferred_revenue.py index fb49ef3a42..d0940c7df2 100644 --- a/erpnext/accounts/deferred_revenue.py +++ b/erpnext/accounts/deferred_revenue.py @@ -341,7 +341,7 @@ def book_deferred_income_or_expense(doc, deferred_process, posting_date=None): "enable_deferred_revenue" if doc.doctype == "Sales Invoice" else "enable_deferred_expense" ) - accounts_frozen_upto = frappe.get_cached_value("Accounts Settings", "None", "acc_frozen_upto") + accounts_frozen_upto = frappe.db.get_single_value("Accounts Settings", "acc_frozen_upto") def _book_deferred_revenue_or_expense( item, diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py index 230a8b3c58..96ba78378b 100644 --- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py +++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py @@ -229,7 +229,7 @@ class PurchaseInvoice(BuyingController): ) if ( - cint(frappe.get_cached_value("Buying Settings", "None", "maintain_same_rate")) + cint(frappe.db.get_single_value("Buying Settings", "maintain_same_rate")) and not self.is_return and not self.is_internal_supplier ): diff --git a/erpnext/selling/doctype/sales_order/sales_order.py b/erpnext/selling/doctype/sales_order/sales_order.py index 624dadbc4d..0fb4860731 100755 --- a/erpnext/selling/doctype/sales_order/sales_order.py +++ b/erpnext/selling/doctype/sales_order/sales_order.py @@ -50,7 +50,7 @@ class SalesOrder(SellingController): super(SalesOrder, self).__init__(*args, **kwargs) def onload(self) -> None: - if frappe.get_cached_value("Stock Settings", None, "enable_stock_reservation"): + if frappe.db.get_single_value("Stock Settings", "enable_stock_reservation"): if self.has_unreserved_stock(): self.set_onload("has_unreserved_stock", True) diff --git a/erpnext/stock/doctype/price_list/price_list.py b/erpnext/stock/doctype/price_list/price_list.py index 554055fd83..e77d53a367 100644 --- a/erpnext/stock/doctype/price_list/price_list.py +++ b/erpnext/stock/doctype/price_list/price_list.py @@ -45,7 +45,7 @@ class PriceList(Document): doc_before_save = self.get_doc_before_save() currency_changed = self.currency != doc_before_save.currency - affects_cart = self.name == frappe.get_cached_value("E Commerce Settings", None, "price_list") + affects_cart = self.name == frappe.db.get_single_value("E Commerce Settings", "price_list") if currency_changed and affects_cart: validate_cart_settings() From 4f473eb090de0e69bd581a1d44edce3eb0d66adc Mon Sep 17 00:00:00 2001 From: abdosaeed95 <118386543+abdosaeed95@users.noreply.github.com> Date: Tue, 1 Aug 2023 07:31:01 +0300 Subject: [PATCH 087/101] fix: typo in loyalty program throw message (#36432) --- erpnext/accounts/doctype/loyalty_program/loyalty_program.py | 2 +- erpnext/translations/af.csv | 2 +- erpnext/translations/am.csv | 2 +- erpnext/translations/ar.csv | 2 +- erpnext/translations/bg.csv | 2 +- erpnext/translations/bn.csv | 2 +- erpnext/translations/bs.csv | 2 +- erpnext/translations/ca.csv | 2 +- erpnext/translations/cs.csv | 2 +- erpnext/translations/da.csv | 2 +- erpnext/translations/de.csv | 2 +- erpnext/translations/el.csv | 2 +- erpnext/translations/es.csv | 2 +- erpnext/translations/et.csv | 2 +- erpnext/translations/fa.csv | 2 +- erpnext/translations/fi.csv | 2 +- erpnext/translations/fr.csv | 2 +- erpnext/translations/gu.csv | 2 +- erpnext/translations/he.csv | 2 +- erpnext/translations/hi.csv | 2 +- erpnext/translations/hr.csv | 2 +- erpnext/translations/hu.csv | 2 +- erpnext/translations/id.csv | 2 +- erpnext/translations/is.csv | 2 +- erpnext/translations/it.csv | 2 +- erpnext/translations/ja.csv | 2 +- erpnext/translations/km.csv | 2 +- erpnext/translations/kn.csv | 2 +- erpnext/translations/ko.csv | 2 +- erpnext/translations/ku.csv | 2 +- erpnext/translations/lo.csv | 2 +- erpnext/translations/lt.csv | 2 +- erpnext/translations/lv.csv | 2 +- erpnext/translations/mk.csv | 2 +- erpnext/translations/ml.csv | 2 +- erpnext/translations/mr.csv | 2 +- erpnext/translations/ms.csv | 2 +- erpnext/translations/my.csv | 2 +- erpnext/translations/nl.csv | 2 +- erpnext/translations/no.csv | 2 +- erpnext/translations/pl.csv | 2 +- erpnext/translations/ps.csv | 2 +- erpnext/translations/pt-BR.csv | 2 +- erpnext/translations/pt.csv | 2 +- erpnext/translations/ro.csv | 2 +- erpnext/translations/ru.csv | 2 +- erpnext/translations/rw.csv | 2 +- erpnext/translations/si.csv | 2 +- erpnext/translations/sk.csv | 2 +- erpnext/translations/sl.csv | 2 +- erpnext/translations/sq.csv | 2 +- erpnext/translations/sr-SP.csv | 2 +- erpnext/translations/sr.csv | 2 +- erpnext/translations/sr_sp.csv | 2 +- erpnext/translations/sv.csv | 2 +- erpnext/translations/sw.csv | 2 +- erpnext/translations/ta.csv | 2 +- erpnext/translations/te.csv | 2 +- erpnext/translations/th.csv | 2 +- erpnext/translations/tr.csv | 2 +- erpnext/translations/uk.csv | 2 +- erpnext/translations/ur.csv | 2 +- erpnext/translations/uz.csv | 2 +- erpnext/translations/vi.csv | 2 +- erpnext/translations/zh-TW.csv | 2 +- erpnext/translations/zh.csv | 2 +- erpnext/translations/zh_tw.csv | 2 +- 67 files changed, 67 insertions(+), 67 deletions(-) diff --git a/erpnext/accounts/doctype/loyalty_program/loyalty_program.py b/erpnext/accounts/doctype/loyalty_program/loyalty_program.py index 48a25ad6b8..a134f74663 100644 --- a/erpnext/accounts/doctype/loyalty_program/loyalty_program.py +++ b/erpnext/accounts/doctype/loyalty_program/loyalty_program.py @@ -141,7 +141,7 @@ def validate_loyalty_points(ref_doc, points_to_redeem): ) if points_to_redeem > loyalty_program_details.loyalty_points: - frappe.throw(_("You don't have enought Loyalty Points to redeem")) + frappe.throw(_("You don't have enough Loyalty Points to redeem")) loyalty_amount = flt(points_to_redeem * loyalty_program_details.conversion_factor) diff --git a/erpnext/translations/af.csv b/erpnext/translations/af.csv index 35ccbb6b75..417f1ecfff 100644 --- a/erpnext/translations/af.csv +++ b/erpnext/translations/af.csv @@ -3094,7 +3094,7 @@ You cannot delete Fiscal Year {0}. Fiscal Year {0} is set as default in Global S You cannot delete Project Type 'External',Jy kan nie projektipe 'eksterne' uitvee nie, You cannot edit root node.,U kan nie wortelknoop wysig nie., You cannot restart a Subscription that is not cancelled.,U kan nie 'n intekening herlaai wat nie gekanselleer is nie., -You don't have enought Loyalty Points to redeem,U het nie genoeg lojaliteitspunte om te verkoop nie, +You don't have enough Loyalty Points to redeem,U het nie genoeg lojaliteitspunte om te verkoop nie, You have already assessed for the assessment criteria {}.,U het reeds geassesseer vir die assesseringskriteria ()., You have already selected items from {0} {1},Jy het reeds items gekies van {0} {1}, You have been invited to collaborate on the project: {0},U is genooi om saam te werk aan die projek: {0}, diff --git a/erpnext/translations/am.csv b/erpnext/translations/am.csv index da865b8797..b5abbbf373 100644 --- a/erpnext/translations/am.csv +++ b/erpnext/translations/am.csv @@ -3094,7 +3094,7 @@ You cannot delete Fiscal Year {0}. Fiscal Year {0} is set as default in Global S You cannot delete Project Type 'External',የፕሮጀክት አይነት «ውጫዊ» ን መሰረዝ አይችሉም., You cannot edit root node.,የስር ሥፍራ ማረም አይችሉም., You cannot restart a Subscription that is not cancelled.,የማይሰረዝ የደንበኝነት ምዝገባን ዳግም ማስጀመር አይችሉም., -You don't have enought Loyalty Points to redeem,ለማስመለስ በቂ የታማኝነት ነጥቦች የሉዎትም, +You don't have enough Loyalty Points to redeem,ለማስመለስ በቂ የታማኝነት ነጥቦች የሉዎትም, You have already assessed for the assessment criteria {}.,ቀድሞውንም ግምገማ መስፈርት ከገመገምን {}., You have already selected items from {0} {1},ከዚህ ቀደም ከ ንጥሎች ተመርጠዋል ሊሆን {0} {1}, You have been invited to collaborate on the project: {0},እርስዎ ፕሮጀክት ላይ ተባበር ተጋብዘዋል: {0}, diff --git a/erpnext/translations/ar.csv b/erpnext/translations/ar.csv index 17d4386873..550b5f2761 100644 --- a/erpnext/translations/ar.csv +++ b/erpnext/translations/ar.csv @@ -3094,7 +3094,7 @@ You cannot delete Fiscal Year {0}. Fiscal Year {0} is set as default in Global S You cannot delete Project Type 'External',لا يمكنك حذف مشروع من نوع 'خارجي', You cannot edit root node.,لا يمكنك تحرير عقدة الجذر., You cannot restart a Subscription that is not cancelled.,لا يمكنك إعادة تشغيل اشتراك غير ملغى., -You don't have enought Loyalty Points to redeem,ليس لديك ما يكفي من نقاط الولاء لاستردادها, +You don't have enough Loyalty Points to redeem,ليس لديك ما يكفي من نقاط الولاء لاستردادها, You have already assessed for the assessment criteria {}.,لقد سبق أن قيمت معايير التقييم {}., You have already selected items from {0} {1},لقد حددت العناصر من {0} {1}, You have been invited to collaborate on the project: {0},لقد وجهت الدعوة إلى التعاون في هذا المشروع: {0}, diff --git a/erpnext/translations/bg.csv b/erpnext/translations/bg.csv index 5fc10c4e66..baee5263fa 100644 --- a/erpnext/translations/bg.csv +++ b/erpnext/translations/bg.csv @@ -3094,7 +3094,7 @@ You cannot delete Fiscal Year {0}. Fiscal Year {0} is set as default in Global S You cannot delete Project Type 'External',Не можете да изтриете Тип на проекта "Външен", You cannot edit root node.,Не можете да редактирате корен възел., You cannot restart a Subscription that is not cancelled.,"Не можете да рестартирате абонамент, който не е анулиран.", -You don't have enought Loyalty Points to redeem,"Нямате достатъчно точки за лоялност, за да осребрите", +You don't have enough Loyalty Points to redeem,"Нямате достатъчно точки за лоялност, за да осребрите", You have already assessed for the assessment criteria {}.,Вече оценихте критериите за оценка {}., You have already selected items from {0} {1},Вие вече сте избрали елементи от {0} {1}, You have been invited to collaborate on the project: {0},Вие сте били поканени да си сътрудничат по проекта: {0}, diff --git a/erpnext/translations/bn.csv b/erpnext/translations/bn.csv index 1da9bb6fcb..266bd160ef 100644 --- a/erpnext/translations/bn.csv +++ b/erpnext/translations/bn.csv @@ -3094,7 +3094,7 @@ You cannot delete Fiscal Year {0}. Fiscal Year {0} is set as default in Global S You cannot delete Project Type 'External',আপনি প্রকল্প প্রকার 'বহিরাগত' মুছে ফেলতে পারবেন না, You cannot edit root node.,আপনি রুট নোড সম্পাদনা করতে পারবেন না।, You cannot restart a Subscription that is not cancelled.,আপনি সাবস্ক্রিপশনটি বাতিল না করা পুনরায় শুরু করতে পারবেন না, -You don't have enought Loyalty Points to redeem,আপনি বিক্রি করার জন্য আনুগত্য পয়েন্ট enought না, +You don't have enough Loyalty Points to redeem,আপনি বিক্রি করার জন্য আনুগত্য পয়েন্ট enough না, You have already assessed for the assessment criteria {}.,"আপনি ইতিমধ্যে মূল্যায়ন মানদণ্ডের জন্য মূল্যায়ন করে নিলে, {}।", You have already selected items from {0} {1},আপনি ইতিমধ্যে থেকে আইটেম নির্বাচন করা আছে {0} {1}, You have been invited to collaborate on the project: {0},আপনি প্রকল্পের সহযোগীতা করার জন্য আমন্ত্রণ জানানো হয়েছে: {0}, diff --git a/erpnext/translations/bs.csv b/erpnext/translations/bs.csv index cab9c83217..53e9d93bce 100644 --- a/erpnext/translations/bs.csv +++ b/erpnext/translations/bs.csv @@ -3094,7 +3094,7 @@ You cannot delete Fiscal Year {0}. Fiscal Year {0} is set as default in Global S You cannot delete Project Type 'External',Ne možete obrisati tip projekta 'Spoljni', You cannot edit root node.,Ne možete uređivati root čvor., You cannot restart a Subscription that is not cancelled.,Ne možete ponovo pokrenuti pretplatu koja nije otkazana., -You don't have enought Loyalty Points to redeem,Ne iskoristite Loyalty Points za otkup, +You don't have enough Loyalty Points to redeem,Ne iskoristite Loyalty Points za otkup, You have already assessed for the assessment criteria {}.,Ste već ocijenili za kriterije procjene {}., You have already selected items from {0} {1},Vi ste već odabrane stavke iz {0} {1}, You have been invited to collaborate on the project: {0},Vi ste pozvani da surađuju na projektu: {0}, diff --git a/erpnext/translations/ca.csv b/erpnext/translations/ca.csv index 0e16a74f37..4ca1435415 100644 --- a/erpnext/translations/ca.csv +++ b/erpnext/translations/ca.csv @@ -3094,7 +3094,7 @@ You cannot delete Fiscal Year {0}. Fiscal Year {0} is set as default in Global S You cannot delete Project Type 'External',No es pot eliminar el tipus de projecte 'Extern', You cannot edit root node.,No podeu editar el node arrel., You cannot restart a Subscription that is not cancelled.,No podeu reiniciar una subscripció que no es cancel·la., -You don't have enought Loyalty Points to redeem,No teniu punts de fidelització previstos per bescanviar, +You don't have enough Loyalty Points to redeem,No teniu punts de fidelització previstos per bescanviar, You have already assessed for the assessment criteria {}.,Vostè ja ha avaluat pels criteris d'avaluació {}., You have already selected items from {0} {1},Ja ha seleccionat articles de {0} {1}, You have been invited to collaborate on the project: {0},Se li ha convidat a col·laborar en el projecte: {0}, diff --git a/erpnext/translations/cs.csv b/erpnext/translations/cs.csv index 3cef0dedef..26b8bf18bb 100644 --- a/erpnext/translations/cs.csv +++ b/erpnext/translations/cs.csv @@ -3094,7 +3094,7 @@ You cannot delete Fiscal Year {0}. Fiscal Year {0} is set as default in Global S You cannot delete Project Type 'External',Nelze odstranit typ projektu "Externí", You cannot edit root node.,Nelze upravit kořenový uzel., You cannot restart a Subscription that is not cancelled.,"Nelze znovu spustit odběr, který není zrušen.", -You don't have enought Loyalty Points to redeem,Nemáte dostatečné věrnostní body k uplatnění, +You don't have enough Loyalty Points to redeem,Nemáte dostatečné věrnostní body k uplatnění, You have already assessed for the assessment criteria {}.,Již jste hodnotili kritéria hodnocení {}., You have already selected items from {0} {1},Již jste vybrané položky z {0} {1}, You have been invited to collaborate on the project: {0},Byli jste pozváni ke spolupráci na projektu: {0}, diff --git a/erpnext/translations/da.csv b/erpnext/translations/da.csv index c58065a97e..09aaa15735 100644 --- a/erpnext/translations/da.csv +++ b/erpnext/translations/da.csv @@ -3094,7 +3094,7 @@ You cannot delete Fiscal Year {0}. Fiscal Year {0} is set as default in Global S You cannot delete Project Type 'External',Du kan ikke slette Project Type 'Ekstern', You cannot edit root node.,Du kan ikke redigere root node., You cannot restart a Subscription that is not cancelled.,"Du kan ikke genstarte en abonnement, der ikke annulleres.", -You don't have enought Loyalty Points to redeem,Du har ikke nok loyalitetspoint til at indløse, +You don't have enough Loyalty Points to redeem,Du har ikke nok loyalitetspoint til at indløse, You have already assessed for the assessment criteria {}.,Du har allerede vurderet for bedømmelseskriterierne {}., You have already selected items from {0} {1},Du har allerede valgt elementer fra {0} {1}, You have been invited to collaborate on the project: {0},Du er blevet inviteret til at samarbejde om sag: {0}, diff --git a/erpnext/translations/de.csv b/erpnext/translations/de.csv index 04e317a041..e2c746782d 100644 --- a/erpnext/translations/de.csv +++ b/erpnext/translations/de.csv @@ -3107,7 +3107,7 @@ You cannot delete Fiscal Year {0}. Fiscal Year {0} is set as default in Global S You cannot delete Project Type 'External',Sie können den Projekttyp 'Extern' nicht löschen, You cannot edit root node.,Sie können den Stammknoten nicht bearbeiten., You cannot restart a Subscription that is not cancelled.,Sie können ein nicht abgebrochenes Abonnement nicht neu starten., -You don't have enought Loyalty Points to redeem,Sie haben nicht genügend Treuepunkte zum Einlösen, +You don't have enough Loyalty Points to redeem,Sie haben nicht genügend Treuepunkte zum Einlösen, You have already assessed for the assessment criteria {}.,Sie haben bereits für die Bewertungskriterien beurteilt., You have already selected items from {0} {1},Sie haben bereits Elemente aus {0} {1} gewählt, You have been invited to collaborate on the project: {0},Sie wurden zur Zusammenarbeit für das Projekt {0} eingeladen., diff --git a/erpnext/translations/el.csv b/erpnext/translations/el.csv index 9d15e61a5b..ae72c26f74 100644 --- a/erpnext/translations/el.csv +++ b/erpnext/translations/el.csv @@ -3094,7 +3094,7 @@ You cannot delete Fiscal Year {0}. Fiscal Year {0} is set as default in Global S You cannot delete Project Type 'External',Δεν μπορείτε να διαγράψετε τον τύπο έργου 'Εξωτερικό', You cannot edit root node.,Δεν μπορείτε να επεξεργαστείτε τον κόμβο ρίζας., You cannot restart a Subscription that is not cancelled.,Δεν μπορείτε να κάνετε επανεκκίνηση μιας συνδρομής που δεν ακυρώνεται., -You don't have enought Loyalty Points to redeem,Δεν διαθέτετε σημεία αφοσίωσης για εξαργύρωση, +You don't have enough Loyalty Points to redeem,Δεν διαθέτετε σημεία αφοσίωσης για εξαργύρωση, You have already assessed for the assessment criteria {}.,Έχετε ήδη αξιολογήσει τα κριτήρια αξιολόγησης {}., You have already selected items from {0} {1},Έχετε ήδη επιλεγμένα αντικείμενα από {0} {1}, You have been invited to collaborate on the project: {0},Έχετε προσκληθεί να συνεργαστούν για το έργο: {0}, diff --git a/erpnext/translations/es.csv b/erpnext/translations/es.csv index 50074d2a7b..d931429624 100644 --- a/erpnext/translations/es.csv +++ b/erpnext/translations/es.csv @@ -3094,7 +3094,7 @@ You cannot delete Fiscal Year {0}. Fiscal Year {0} is set as default in Global S You cannot delete Project Type 'External',No puede eliminar Tipo de proyecto 'Externo', You cannot edit root node.,No puedes editar el nodo raíz., You cannot restart a Subscription that is not cancelled.,No puede reiniciar una suscripción que no está cancelada., -You don't have enought Loyalty Points to redeem,No tienes suficientes puntos de lealtad para canjear, +You don't have enough Loyalty Points to redeem,No tienes suficientes puntos de lealtad para canjear, You have already assessed for the assessment criteria {}.,Ya ha evaluado los criterios de evaluación {}., You have already selected items from {0} {1},Ya ha seleccionado artículos de {0} {1}, You have been invited to collaborate on the project: {0},Se le ha invitado a colaborar en el proyecto: {0}, diff --git a/erpnext/translations/et.csv b/erpnext/translations/et.csv index a9f6c6c5b0..29e599b259 100644 --- a/erpnext/translations/et.csv +++ b/erpnext/translations/et.csv @@ -3094,7 +3094,7 @@ You cannot delete Fiscal Year {0}. Fiscal Year {0} is set as default in Global S You cannot delete Project Type 'External',Te ei saa projekti tüübi "Väline" kustutada, You cannot edit root node.,Sa ei saa redigeerida juursõlme., You cannot restart a Subscription that is not cancelled.,Te ei saa tellimust uuesti katkestada., -You don't have enought Loyalty Points to redeem,"Teil pole lojaalsuspunkte, mida soovite lunastada", +You don't have enough Loyalty Points to redeem,"Teil pole lojaalsuspunkte, mida soovite lunastada", You have already assessed for the assessment criteria {}.,Olete juba hinnanud hindamise kriteeriumid {}., You have already selected items from {0} {1},Olete juba valitud objektide {0} {1}, You have been invited to collaborate on the project: {0},Sind on kutsutud koostööd projekti: {0}, diff --git a/erpnext/translations/fa.csv b/erpnext/translations/fa.csv index 35a42e8027..4c5ab8098c 100644 --- a/erpnext/translations/fa.csv +++ b/erpnext/translations/fa.csv @@ -3094,7 +3094,7 @@ You cannot delete Fiscal Year {0}. Fiscal Year {0} is set as default in Global S You cannot delete Project Type 'External',شما نمیتوانید نوع پروژه «خارجی» را حذف کنید, You cannot edit root node.,نمی توانید گره ریشه را ویرایش کنید, You cannot restart a Subscription that is not cancelled.,شما نمی توانید اشتراک را لغو کنید., -You don't have enought Loyalty Points to redeem,شما نمیتوانید امتیازات وفاداری خود را به دست آورید, +You don't have enough Loyalty Points to redeem,شما نمیتوانید امتیازات وفاداری خود را به دست آورید, You have already assessed for the assessment criteria {}.,شما در حال حاضر برای معیارهای ارزیابی ارزیابی {}., You have already selected items from {0} {1},شما در حال حاضر اقلام از انتخاب {0} {1}, You have been invited to collaborate on the project: {0},از شما دعوت شده برای همکاری در این پروژه: {0}, diff --git a/erpnext/translations/fi.csv b/erpnext/translations/fi.csv index 9d7bf8b5d9..c26441b611 100644 --- a/erpnext/translations/fi.csv +++ b/erpnext/translations/fi.csv @@ -3094,7 +3094,7 @@ You cannot delete Fiscal Year {0}. Fiscal Year {0} is set as default in Global S You cannot delete Project Type 'External',Et voi poistaa projektityyppiä "Ulkoinen", You cannot edit root node.,Et voi muokata juurisolmua., You cannot restart a Subscription that is not cancelled.,"Et voi uudelleenkäynnistää tilausta, jota ei peruuteta.", -You don't have enought Loyalty Points to redeem,Sinulla ei ole tarpeeksi Loyalty Pointsia lunastettavaksi, +You don't have enough Loyalty Points to redeem,Sinulla ei ole tarpeeksi Loyalty Pointsia lunastettavaksi, You have already assessed for the assessment criteria {}.,Olet jo arvioitu arviointikriteerit {}., You have already selected items from {0} {1},Olet jo valitut kohteet {0} {1}, You have been invited to collaborate on the project: {0},Sinut on kutsuttu yhteistyöhön projektissa {0}, diff --git a/erpnext/translations/fr.csv b/erpnext/translations/fr.csv index 6d5505b6e1..801604a4d8 100644 --- a/erpnext/translations/fr.csv +++ b/erpnext/translations/fr.csv @@ -3093,7 +3093,7 @@ You cannot delete Fiscal Year {0}. Fiscal Year {0} is set as default in Global S You cannot delete Project Type 'External',Vous ne pouvez pas supprimer le Type de Projet 'Externe', You cannot edit root node.,Vous ne pouvez pas modifier le nœud racine., You cannot restart a Subscription that is not cancelled.,Vous ne pouvez pas redémarrer un abonnement qui n'est pas annulé., -You don't have enought Loyalty Points to redeem,Vous n'avez pas assez de points de fidélité à échanger, +You don't have enough Loyalty Points to redeem,Vous n'avez pas assez de points de fidélité à échanger, You have already assessed for the assessment criteria {}.,Vous avez déjà évalué les critères d'évaluation {}., You have already selected items from {0} {1},Vous avez déjà choisi des articles de {0} {1}, You have been invited to collaborate on the project: {0},Vous avez été invité à collaborer sur le projet : {0}, diff --git a/erpnext/translations/gu.csv b/erpnext/translations/gu.csv index 025ec89134..569159794e 100644 --- a/erpnext/translations/gu.csv +++ b/erpnext/translations/gu.csv @@ -3094,7 +3094,7 @@ You cannot delete Fiscal Year {0}. Fiscal Year {0} is set as default in Global S You cannot delete Project Type 'External',તમે 'બાહ્ય' પ્રોજેક્ટ પ્રકારને કાઢી શકતા નથી, You cannot edit root node.,તમે રૂટ નોડને સંપાદિત કરી શકતા નથી., You cannot restart a Subscription that is not cancelled.,તમે સબ્સ્ક્રિપ્શન ફરીથી શરૂ કરી શકતા નથી કે જે રદ કરવામાં આવી નથી., -You don't have enought Loyalty Points to redeem,તમારી પાસે રિડીમ કરવા માટે વફાદારીના પોઇંટ્સ નથી, +You don't have enough Loyalty Points to redeem,તમારી પાસે રિડીમ કરવા માટે વફાદારીના પોઇંટ્સ નથી, You have already assessed for the assessment criteria {}.,જો તમે પહેલાથી જ આકારણી માપદંડ માટે આકારણી છે {}., You have already selected items from {0} {1},જો તમે પહેલાથી જ વસ્તુઓ પસંદ કરેલ {0} {1}, You have been invited to collaborate on the project: {0},તમે આ પ્રોજેક્ટ પર સહયોગ કરવા માટે આમંત્રિત કરવામાં આવ્યા છે: {0}, diff --git a/erpnext/translations/he.csv b/erpnext/translations/he.csv index 43bac41194..9aa152c6bb 100644 --- a/erpnext/translations/he.csv +++ b/erpnext/translations/he.csv @@ -3094,7 +3094,7 @@ You cannot delete Fiscal Year {0}. Fiscal Year {0} is set as default in Global S You cannot delete Project Type 'External',אינך יכול למחוק את סוג הפרויקט 'חיצוני', You cannot edit root node.,אינך יכול לערוך צומת שורש., You cannot restart a Subscription that is not cancelled.,אינך יכול להפעיל מחדש מנוי שאינו מבוטל., -You don't have enought Loyalty Points to redeem,אין לך מספיק נקודות נאמנות למימוש, +You don't have enough Loyalty Points to redeem,אין לך מספיק נקודות נאמנות למימוש, You have already assessed for the assessment criteria {}.,כבר הערכת את קריטריוני ההערכה {}., You have already selected items from {0} {1},בחרת כבר פריטים מ- {0} {1}, You have been invited to collaborate on the project: {0},הוזמנת לשתף פעולה על הפרויקט: {0}, diff --git a/erpnext/translations/hi.csv b/erpnext/translations/hi.csv index 00747d45cf..d56dcec8bb 100644 --- a/erpnext/translations/hi.csv +++ b/erpnext/translations/hi.csv @@ -3094,7 +3094,7 @@ You cannot delete Fiscal Year {0}. Fiscal Year {0} is set as default in Global S You cannot delete Project Type 'External',आप परियोजना प्रकार 'बाहरी' को नहीं हटा सकते, You cannot edit root node.,आप रूट नोड संपादित नहीं कर सकते हैं।, You cannot restart a Subscription that is not cancelled.,आप एक सदस्यता को पुनरारंभ नहीं कर सकते जो रद्द नहीं किया गया है।, -You don't have enought Loyalty Points to redeem,आपने रिडीम करने के लिए वफादारी अंक नहीं खरीदे हैं, +You don't have enough Loyalty Points to redeem,आपने रिडीम करने के लिए वफादारी अंक नहीं खरीदे हैं, You have already assessed for the assessment criteria {}.,आप मूल्यांकन मानदंड के लिए पहले से ही मूल्यांकन कर चुके हैं {}, You have already selected items from {0} {1},आप पहले से ही से आइटम का चयन किया है {0} {1}, You have been invited to collaborate on the project: {0},आप इस परियोजना पर सहयोग करने के लिए आमंत्रित किया गया है: {0}, diff --git a/erpnext/translations/hr.csv b/erpnext/translations/hr.csv index ec24026432..827ae2c778 100644 --- a/erpnext/translations/hr.csv +++ b/erpnext/translations/hr.csv @@ -3094,7 +3094,7 @@ You cannot delete Fiscal Year {0}. Fiscal Year {0} is set as default in Global S You cannot delete Project Type 'External',Ne možete izbrisati vrstu projekta 'Vanjski', You cannot edit root node.,Ne možete uređivati root čvor., You cannot restart a Subscription that is not cancelled.,Ne možete ponovo pokrenuti pretplatu koja nije otkazana., -You don't have enought Loyalty Points to redeem,Nemate dovoljno bodova lojalnosti za otkup, +You don't have enough Loyalty Points to redeem,Nemate dovoljno bodova lojalnosti za otkup, You have already assessed for the assessment criteria {}.,Već ste ocijenili kriterije procjene {}., You have already selected items from {0} {1},Već ste odabrali stavke iz {0} {1}, You have been invited to collaborate on the project: {0},Pozvani ste za suradnju na projektu: {0}, diff --git a/erpnext/translations/hu.csv b/erpnext/translations/hu.csv index f92946a8bd..e68b56f952 100644 --- a/erpnext/translations/hu.csv +++ b/erpnext/translations/hu.csv @@ -3094,7 +3094,7 @@ You cannot delete Fiscal Year {0}. Fiscal Year {0} is set as default in Global S You cannot delete Project Type 'External',"A ""Külső"" projekttípust nem törölheti", You cannot edit root node.,Nem szerkesztheti a fő csomópontot., You cannot restart a Subscription that is not cancelled.,"Nem indíthatja el az Előfizetést, amelyet nem zárt le.", -You don't have enought Loyalty Points to redeem,Nincs elegendő hűségpontjaid megváltáshoz, +You don't have enough Loyalty Points to redeem,Nincs elegendő hűségpontjaid megváltáshoz, You have already assessed for the assessment criteria {}.,Már értékelte ezekkel az értékelési kritériumokkal: {}., You have already selected items from {0} {1},Már választott ki elemeket innen {0} {1}, You have been invited to collaborate on the project: {0},Ön meghívást kapott ennek a projeknek a közreműködéséhez: {0}, diff --git a/erpnext/translations/id.csv b/erpnext/translations/id.csv index 8ba9e66454..67311a1cac 100644 --- a/erpnext/translations/id.csv +++ b/erpnext/translations/id.csv @@ -3094,7 +3094,7 @@ You cannot delete Fiscal Year {0}. Fiscal Year {0} is set as default in Global S You cannot delete Project Type 'External',Anda tidak bisa menghapus Jenis Proyek 'External', You cannot edit root node.,Anda tidak dapat mengedit simpul root., You cannot restart a Subscription that is not cancelled.,Anda tidak dapat memulai ulang Langganan yang tidak dibatalkan., -You don't have enought Loyalty Points to redeem,Anda tidak memiliki Poin Loyalitas yang cukup untuk ditukarkan, +You don't have enough Loyalty Points to redeem,Anda tidak memiliki Poin Loyalitas yang cukup untuk ditukarkan, You have already assessed for the assessment criteria {}.,Anda telah memberikan penilaian terhadap kriteria penilaian {}., You have already selected items from {0} {1},Anda sudah memilih item dari {0} {1}, You have been invited to collaborate on the project: {0},Anda telah diundang untuk berkolaborasi pada proyek: {0}, diff --git a/erpnext/translations/is.csv b/erpnext/translations/is.csv index 9802592e9b..1bf6b4c791 100644 --- a/erpnext/translations/is.csv +++ b/erpnext/translations/is.csv @@ -3094,7 +3094,7 @@ You cannot delete Fiscal Year {0}. Fiscal Year {0} is set as default in Global S You cannot delete Project Type 'External',Þú getur ekki eytt verkefnisgerðinni 'ytri', You cannot edit root node.,Þú getur ekki breytt rótarkóði., You cannot restart a Subscription that is not cancelled.,Þú getur ekki endurræst áskrift sem ekki er lokað., -You don't have enought Loyalty Points to redeem,Þú hefur ekki nóg hollusta stig til að innleysa, +You don't have enough Loyalty Points to redeem,Þú hefur ekki nóg hollusta stig til að innleysa, You have already assessed for the assessment criteria {}.,Þú hefur nú þegar metið mat á viðmiðunum {}., You have already selected items from {0} {1},Þú hefur nú þegar valið hluti úr {0} {1}, You have been invited to collaborate on the project: {0},Þér hefur verið boðið að vinna að verkefninu: {0}, diff --git a/erpnext/translations/it.csv b/erpnext/translations/it.csv index 064f16b232..f96b1aacdf 100644 --- a/erpnext/translations/it.csv +++ b/erpnext/translations/it.csv @@ -3094,7 +3094,7 @@ You cannot delete Fiscal Year {0}. Fiscal Year {0} is set as default in Global S You cannot delete Project Type 'External',Non è possibile eliminare il tipo di progetto 'Esterno', You cannot edit root node.,Non è possibile modificare il nodo principale., You cannot restart a Subscription that is not cancelled.,Non è possibile riavviare una sottoscrizione che non è stata annullata., -You don't have enought Loyalty Points to redeem,Non hai abbastanza Punti fedeltà da riscattare, +You don't have enough Loyalty Points to redeem,Non hai abbastanza Punti fedeltà da riscattare, You have already assessed for the assessment criteria {}.,Hai già valutato i criteri di valutazione {}., You have already selected items from {0} {1},Hai già selezionato elementi da {0} {1}, You have been invited to collaborate on the project: {0},Sei stato invitato a collaborare al progetto: {0}, diff --git a/erpnext/translations/ja.csv b/erpnext/translations/ja.csv index b5064f65b2..e5ebeb3411 100644 --- a/erpnext/translations/ja.csv +++ b/erpnext/translations/ja.csv @@ -3094,7 +3094,7 @@ You cannot delete Fiscal Year {0}. Fiscal Year {0} is set as default in Global S You cannot delete Project Type 'External',プロジェクトタイプ「外部」を削除することはできません, You cannot edit root node.,ルートノードは編集できません。, You cannot restart a Subscription that is not cancelled.,キャンセルされていないサブスクリプションを再起動することはできません。, -You don't have enought Loyalty Points to redeem,あなたは交換するのに十分なロイヤリティポイントがありません, +You don't have enough Loyalty Points to redeem,あなたは交換するのに十分なロイヤリティポイントがありません, You have already assessed for the assessment criteria {}.,評価基準{}は評価済です。, You have already selected items from {0} {1},項目を選択済みです {0} {1}, You have been invited to collaborate on the project: {0},プロジェクト:{0} の共同作業に招待されました, diff --git a/erpnext/translations/km.csv b/erpnext/translations/km.csv index 31dcdc0293..0dbecca1f8 100644 --- a/erpnext/translations/km.csv +++ b/erpnext/translations/km.csv @@ -3094,7 +3094,7 @@ You cannot delete Fiscal Year {0}. Fiscal Year {0} is set as default in Global S You cannot delete Project Type 'External',អ្នកមិនអាចលុបប្រភេទគម្រោង 'ខាងក្រៅ', You cannot edit root node.,អ្នកមិនអាចកែថ្នាំង root បានទេ។, You cannot restart a Subscription that is not cancelled.,អ្នកមិនអាចចាប់ផ្តើមឡើងវិញនូវការជាវដែលមិនត្រូវបានលុបចោលទេ។, -You don't have enought Loyalty Points to redeem,អ្នកមិនមានពិន្ទុភាពស្មោះត្រង់គ្រប់គ្រាន់ដើម្បីលោះទេ, +You don't have enough Loyalty Points to redeem,អ្នកមិនមានពិន្ទុភាពស្មោះត្រង់គ្រប់គ្រាន់ដើម្បីលោះទេ, You have already assessed for the assessment criteria {}.,អ្នកបានវាយតម្លែរួចទៅហើយសម្រាប់លក្ខណៈវិនិច្ឆ័យវាយតម្លៃនេះ {} ។, You have already selected items from {0} {1},អ្នកបានជ្រើសរួចហើយចេញពីធាតុ {0} {1}, You have been invited to collaborate on the project: {0},អ្នកបានត្រូវអញ្ជើញដើម្បីសហការគ្នាលើគម្រោងនេះ: {0}, diff --git a/erpnext/translations/kn.csv b/erpnext/translations/kn.csv index f01e386f41..b929e2982b 100644 --- a/erpnext/translations/kn.csv +++ b/erpnext/translations/kn.csv @@ -3094,7 +3094,7 @@ You cannot delete Fiscal Year {0}. Fiscal Year {0} is set as default in Global S You cannot delete Project Type 'External',ನೀವು ಪ್ರಾಜೆಕ್ಟ್ ಕೌಟುಂಬಿಕತೆ 'ಬಾಹ್ಯ' ಅನ್ನು ಅಳಿಸಲು ಸಾಧ್ಯವಿಲ್ಲ, You cannot edit root node.,ನೀವು ರೂಟ್ ನೋಡ್ ಅನ್ನು ಸಂಪಾದಿಸಲಾಗುವುದಿಲ್ಲ., You cannot restart a Subscription that is not cancelled.,ರದ್ದುಪಡಿಸದ ಚಂದಾದಾರಿಕೆಯನ್ನು ನೀವು ಮರುಪ್ರಾರಂಭಿಸಬಾರದು., -You don't have enought Loyalty Points to redeem,ರಿಡೀಮ್ ಮಾಡಲು ನೀವು ಲಾಯಲ್ಟಿ ಪಾಯಿಂಟುಗಳನ್ನು ಹೊಂದಿದ್ದೀರಿ, +You don't have enough Loyalty Points to redeem,ರಿಡೀಮ್ ಮಾಡಲು ನೀವು ಲಾಯಲ್ಟಿ ಪಾಯಿಂಟುಗಳನ್ನು ಹೊಂದಿದ್ದೀರಿ, You have already assessed for the assessment criteria {}.,ನೀವು ಈಗಾಗಲೇ ಮೌಲ್ಯಮಾಪನ ಮಾನದಂಡದ ನಿರ್ಣಯಿಸುವ {}., You have already selected items from {0} {1},ನೀವು ಈಗಾಗಲೇ ಆಯ್ಕೆ ಐಟಂಗಳನ್ನು ಎಂದು {0} {1}, You have been invited to collaborate on the project: {0},ನೀವು ಯೋಜನೆಯ ಸಹಯೋಗಿಸಲು ಆಮಂತ್ರಿಸಲಾಗಿದೆ: {0}, diff --git a/erpnext/translations/ko.csv b/erpnext/translations/ko.csv index dffcaa8093..1c8020fbb9 100644 --- a/erpnext/translations/ko.csv +++ b/erpnext/translations/ko.csv @@ -3094,7 +3094,7 @@ You cannot delete Fiscal Year {0}. Fiscal Year {0} is set as default in Global S You cannot delete Project Type 'External',프로젝트 유형 '외부'를 삭제할 수 없습니다., You cannot edit root node.,루트 노드는 편집 할 수 없습니다., You cannot restart a Subscription that is not cancelled.,취소되지 않은 구독은 다시 시작할 수 없습니다., -You don't have enought Loyalty Points to redeem,사용하기에 충성도 포인트가 충분하지 않습니다., +You don't have enough Loyalty Points to redeem,사용하기에 충성도 포인트가 충분하지 않습니다., You have already assessed for the assessment criteria {}.,이미 평가 기준 {}을 (를) 평가했습니다., You have already selected items from {0} {1},이미에서 항목을 선택한 {0} {1}, You have been invited to collaborate on the project: {0},당신은 프로젝트 공동 작업에 초대되었습니다 : {0}, diff --git a/erpnext/translations/ku.csv b/erpnext/translations/ku.csv index 047ee893e2..f4d1197361 100644 --- a/erpnext/translations/ku.csv +++ b/erpnext/translations/ku.csv @@ -3094,7 +3094,7 @@ You cannot delete Fiscal Year {0}. Fiscal Year {0} is set as default in Global S You cannot delete Project Type 'External',Hûn nikarin jêbirinê hilbijêre 'External', You cannot edit root node.,Hûn nikarin node root root biguherînin., You cannot restart a Subscription that is not cancelled.,Hûn nikarin endamê peymana ku destûr nabe., -You don't have enought Loyalty Points to redeem,Hûn pisporên dilsozî ne ku hûn bistînin, +You don't have enough Loyalty Points to redeem,Hûn pisporên dilsozî ne ku hûn bistînin, You have already assessed for the assessment criteria {}.,Tu niha ji bo nirxandina nirxandin {}., You have already selected items from {0} {1},Jixwe te tomar ji hilbijartî {0} {1}, You have been invited to collaborate on the project: {0},Hûn hatine vexwendin ji bo hevkariyê li ser vê projeyê: {0}, diff --git a/erpnext/translations/lo.csv b/erpnext/translations/lo.csv index c94bc253fc..9e77b51b8a 100644 --- a/erpnext/translations/lo.csv +++ b/erpnext/translations/lo.csv @@ -3094,7 +3094,7 @@ You cannot delete Fiscal Year {0}. Fiscal Year {0} is set as default in Global S You cannot delete Project Type 'External',ທ່ານບໍ່ສາມາດລຶບປະເພດໂຄງການ 'ພາຍນອກ', You cannot edit root node.,ທ່ານບໍ່ສາມາດແກ້ໄຂຮາກຮາກ., You cannot restart a Subscription that is not cancelled.,ທ່ານບໍ່ສາມາດເລີ່ມຕົ້ນລະບົບຈອງໃຫມ່ທີ່ບໍ່ໄດ້ຖືກຍົກເລີກ., -You don't have enought Loyalty Points to redeem,ທ່ານບໍ່ມີຈຸດປະສົງອັນຄົບຖ້ວນພໍທີ່ຈະຊື້, +You don't have enough Loyalty Points to redeem,ທ່ານບໍ່ມີຈຸດປະສົງອັນຄົບຖ້ວນພໍທີ່ຈະຊື້, You have already assessed for the assessment criteria {}.,ທ່ານໄດ້ປະເມີນແລ້ວສໍາລັບມາດຕະຖານການປະເມີນຜົນ {}., You have already selected items from {0} {1},ທ່ານໄດ້ຄັດເລືອກເອົາແລ້ວລາຍການຈາກ {0} {1}, You have been invited to collaborate on the project: {0},ທ່ານໄດ້ຖືກເຊື້ອເຊີນເພື່ອເຮັດວຽກຮ່ວມກັນກ່ຽວກັບໂຄງການ: {0}, diff --git a/erpnext/translations/lt.csv b/erpnext/translations/lt.csv index 731638cf1f..66215c19b3 100644 --- a/erpnext/translations/lt.csv +++ b/erpnext/translations/lt.csv @@ -3094,7 +3094,7 @@ You cannot delete Fiscal Year {0}. Fiscal Year {0} is set as default in Global S You cannot delete Project Type 'External',Negalite ištrinti projekto tipo "Išorinis", You cannot edit root node.,Jūs negalite redaguoti šakninis mazgas., You cannot restart a Subscription that is not cancelled.,"Jūs negalite iš naujo paleisti Prenumeratos, kuri nėra atšaukta.", -You don't have enought Loyalty Points to redeem,Jūs neturite nusipirkti lojalumo taškų išpirkti, +You don't have enough Loyalty Points to redeem,Jūs neturite nusipirkti lojalumo taškų išpirkti, You have already assessed for the assessment criteria {}.,Jūs jau įvertintas vertinimo kriterijus {}., You have already selected items from {0} {1},Jūs jau pasirinkote elementus iš {0} {1}, You have been invited to collaborate on the project: {0},Jūs buvote pakviestas bendradarbiauti su projektu: {0}, diff --git a/erpnext/translations/lv.csv b/erpnext/translations/lv.csv index 71b51f4f62..1409719f36 100644 --- a/erpnext/translations/lv.csv +++ b/erpnext/translations/lv.csv @@ -3094,7 +3094,7 @@ You cannot delete Fiscal Year {0}. Fiscal Year {0} is set as default in Global S You cannot delete Project Type 'External',Jūs nevarat izdzēst projekta veidu "Ārējais", You cannot edit root node.,Jūs nevarat rediģēt saknes mezglu., You cannot restart a Subscription that is not cancelled.,"Jūs nevarat atsākt Abonementu, kas nav atcelts.", -You don't have enought Loyalty Points to redeem,Jums nav lojalitātes punktu atpirkt, +You don't have enough Loyalty Points to redeem,Jums nav lojalitātes punktu atpirkt, You have already assessed for the assessment criteria {}.,Jūs jau izvērtēta vērtēšanas kritērijiem {}., You have already selected items from {0} {1},Jūs jau atsevišķus posteņus {0} {1}, You have been invited to collaborate on the project: {0},Jūs esat uzaicināts sadarboties projektam: {0}, diff --git a/erpnext/translations/mk.csv b/erpnext/translations/mk.csv index 2dcef0c848..06d7e6540a 100644 --- a/erpnext/translations/mk.csv +++ b/erpnext/translations/mk.csv @@ -3094,7 +3094,7 @@ You cannot delete Fiscal Year {0}. Fiscal Year {0} is set as default in Global S You cannot delete Project Type 'External',Не можете да го избришете Типот на проектот 'External', You cannot edit root node.,Не можете да уредувате корен јазол., You cannot restart a Subscription that is not cancelled.,Не можете да ја рестартирате претплатата која не е откажана., -You don't have enought Loyalty Points to redeem,Вие не сте донеле лојални точки за откуп, +You don't have enough Loyalty Points to redeem,Вие не сте донеле лојални точки за откуп, You have already assessed for the assessment criteria {}.,Веќе сте се проценува за критериумите за оценување {}., You have already selected items from {0} {1},Веќе сте одбрале предмети од {0} {1}, You have been invited to collaborate on the project: {0},Вие сте поканети да соработуваат на проектот: {0}, diff --git a/erpnext/translations/ml.csv b/erpnext/translations/ml.csv index 46629a3532..22684b64d1 100644 --- a/erpnext/translations/ml.csv +++ b/erpnext/translations/ml.csv @@ -3094,7 +3094,7 @@ You cannot delete Fiscal Year {0}. Fiscal Year {0} is set as default in Global S You cannot delete Project Type 'External',നിങ്ങൾക്ക് പദ്ധതി തരം 'ബാഹ്യ' ഇല്ലാതാക്കാൻ കഴിയില്ല, You cannot edit root node.,നിങ്ങൾക്ക് റൂട്ട് നോഡ് എഡിറ്റുചെയ്യാൻ കഴിയില്ല., You cannot restart a Subscription that is not cancelled.,നിങ്ങൾക്ക് റദ്ദാക്കാത്ത ഒരു സബ്സ്ക്രിപ്ഷൻ പുനഃരാരംഭിക്കാൻ കഴിയില്ല., -You don't have enought Loyalty Points to redeem,നിങ്ങൾക്ക് വീണ്ടെടുക്കാനുള്ള വിശ്വസ്ത ടയറുകൾ ആവശ്യമില്ല, +You don't have enough Loyalty Points to redeem,നിങ്ങൾക്ക് വീണ്ടെടുക്കാനുള്ള വിശ്വസ്ത ടയറുകൾ ആവശ്യമില്ല, You have already assessed for the assessment criteria {}.,ഇതിനകം നിങ്ങൾ വിലയിരുത്തൽ മാനദണ്ഡങ്ങൾ {} വേണ്ടി വിലയിരുത്തി ചെയ്തു., You have already selected items from {0} {1},നിങ്ങൾ ഇതിനകം നിന്ന് {0} {1} ഇനങ്ങൾ തിരഞ്ഞെടുത്തു, You have been invited to collaborate on the project: {0},നിങ്ങൾ പദ്ധതി സഹകരിക്കുക ക്ഷണിച്ചു: {0}, diff --git a/erpnext/translations/mr.csv b/erpnext/translations/mr.csv index 7f1c5e28f5..87e0fdd066 100644 --- a/erpnext/translations/mr.csv +++ b/erpnext/translations/mr.csv @@ -3094,7 +3094,7 @@ You cannot delete Fiscal Year {0}. Fiscal Year {0} is set as default in Global S You cannot delete Project Type 'External',आपण प्रोजेक्ट प्रकार 'बाह्य' हटवू शकत नाही, You cannot edit root node.,आपण मूळ नोड संपादित करू शकत नाही., You cannot restart a Subscription that is not cancelled.,आपण रद्द न केलेली सबस्क्रिप्शन पुन्हा सुरू करू शकत नाही., -You don't have enought Loyalty Points to redeem,आपण परत विकत घेण्यासाठी निष्ठावान बिंदू नाहीत, +You don't have enough Loyalty Points to redeem,आपण परत विकत घेण्यासाठी निष्ठावान बिंदू नाहीत, You have already assessed for the assessment criteria {}.,आपण मूल्यांकन निकष आधीच मूल्यमापन आहे {}., You have already selected items from {0} {1},आपण आधीच आयटम निवडले आहेत {0} {1}, You have been invited to collaborate on the project: {0},आपण प्रकल्प सहयोग करण्यासाठी आमंत्रित आहेत: {0}, diff --git a/erpnext/translations/ms.csv b/erpnext/translations/ms.csv index 0dfc55bf99..8e1ac51d72 100644 --- a/erpnext/translations/ms.csv +++ b/erpnext/translations/ms.csv @@ -3094,7 +3094,7 @@ You cannot delete Fiscal Year {0}. Fiscal Year {0} is set as default in Global S You cannot delete Project Type 'External',Anda tidak boleh memadam Jenis Projek 'Luar', You cannot edit root node.,Anda tidak boleh mengedit nod akar., You cannot restart a Subscription that is not cancelled.,Anda tidak boleh memulakan semula Langganan yang tidak dibatalkan., -You don't have enought Loyalty Points to redeem,Anda tidak mempunyai mata Kesetiaan yang cukup untuk menebusnya, +You don't have enough Loyalty Points to redeem,Anda tidak mempunyai mata Kesetiaan yang cukup untuk menebusnya, You have already assessed for the assessment criteria {}.,Anda telah pun dinilai untuk kriteria penilaian {}., You have already selected items from {0} {1},Anda telah memilih barangan dari {0} {1}, You have been invited to collaborate on the project: {0},Anda telah dijemput untuk bekerjasama dalam projek: {0}, diff --git a/erpnext/translations/my.csv b/erpnext/translations/my.csv index c0c0c453ae..6da58603bd 100644 --- a/erpnext/translations/my.csv +++ b/erpnext/translations/my.csv @@ -3094,7 +3094,7 @@ You cannot delete Fiscal Year {0}. Fiscal Year {0} is set as default in Global S You cannot delete Project Type 'External',သငျသညျစီမံကိန်းအမျိုးအစား '' ပြင်ပ '' မဖျက်နိုင်ပါ, You cannot edit root node.,သငျသညျအမြစ် node ကိုတည်းဖြတ်မရနိုင်ပါ။, You cannot restart a Subscription that is not cancelled.,သငျသညျဖျက်သိမ်းမပေးကြောင်းတစ် Subscription ပြန်လည်စတင်ရန်လို့မရပါဘူး။, -You don't have enought Loyalty Points to redeem,သငျသညျကိုရှေးနှုတျမှ enought သစ္စာရှိမှုအမှတ်ရှိသည်မဟုတ်ကြဘူး, +You don't have enough Loyalty Points to redeem,သငျသညျကိုရှေးနှုတျမှ enough သစ္စာရှိမှုအမှတ်ရှိသည်မဟုတ်ကြဘူး, You have already assessed for the assessment criteria {}.,သငျသညျပြီးသား {} အဆိုပါအကဲဖြတ်သတ်မှတ်ချက်အဘို့အအကဲဖြတ်ပါပြီ။, You have already selected items from {0} {1},သငျသညျပြီးသား {0} {1} ကနေပစ္စည်းကိုရှေးခယျြခဲ့ကြ, You have been invited to collaborate on the project: {0},သငျသညျစီမံကိန်းကိုအပေါ်ပူးပေါင်းဖို့ဖိတ်ခေါ်ခဲ့ကြ: {0}, diff --git a/erpnext/translations/nl.csv b/erpnext/translations/nl.csv index 4d810954f1..96d1770461 100644 --- a/erpnext/translations/nl.csv +++ b/erpnext/translations/nl.csv @@ -3094,7 +3094,7 @@ You cannot delete Fiscal Year {0}. Fiscal Year {0} is set as default in Global S You cannot delete Project Type 'External',U kunt projecttype 'extern' niet verwijderen, You cannot edit root node.,U kunt het basisknooppunt niet bewerken., You cannot restart a Subscription that is not cancelled.,U kunt een Abonnement dat niet is geannuleerd niet opnieuw opstarten., -You don't have enought Loyalty Points to redeem,Je hebt geen genoeg loyaliteitspunten om in te wisselen, +You don't have enough Loyalty Points to redeem,Je hebt geen genoeg loyaliteitspunten om in te wisselen, You have already assessed for the assessment criteria {}.,U heeft al beoordeeld op de beoordelingscriteria {}., You have already selected items from {0} {1},U heeft reeds geselecteerde items uit {0} {1}, You have been invited to collaborate on the project: {0},U bent uitgenodigd om mee te werken aan het project: {0}, diff --git a/erpnext/translations/no.csv b/erpnext/translations/no.csv index 0ee6ed6bc6..f285e487d1 100644 --- a/erpnext/translations/no.csv +++ b/erpnext/translations/no.csv @@ -3094,7 +3094,7 @@ You cannot delete Fiscal Year {0}. Fiscal Year {0} is set as default in Global S You cannot delete Project Type 'External',Du kan ikke slette Project Type 'External', You cannot edit root node.,Du kan ikke redigere rotknutepunktet., You cannot restart a Subscription that is not cancelled.,Du kan ikke starte en abonnement som ikke er kansellert., -You don't have enought Loyalty Points to redeem,Du har ikke nok lojalitetspoeng til å innløse, +You don't have enough Loyalty Points to redeem,Du har ikke nok lojalitetspoeng til å innløse, You have already assessed for the assessment criteria {}.,Du har allerede vurdert for vurderingskriteriene {}., You have already selected items from {0} {1},Du har allerede valgt elementer fra {0} {1}, You have been invited to collaborate on the project: {0},Du har blitt invitert til å samarbeide om prosjektet: {0}, diff --git a/erpnext/translations/pl.csv b/erpnext/translations/pl.csv index e0ecec5b86..82cc64d09f 100644 --- a/erpnext/translations/pl.csv +++ b/erpnext/translations/pl.csv @@ -3069,7 +3069,7 @@ You cannot delete Fiscal Year {0}. Fiscal Year {0} is set as default in Global S You cannot delete Project Type 'External',Nie można usunąć typu projektu "zewnętrzny", You cannot edit root node.,Nie można edytować węzła głównego., You cannot restart a Subscription that is not cancelled.,"Nie można ponownie uruchomić subskrypcji, która nie zostanie anulowana.", -You don't have enought Loyalty Points to redeem,"Nie masz wystarczającej liczby Punktów Lojalnościowych, aby je wykorzystać", +You don't have enough Loyalty Points to redeem,"Nie masz wystarczającej liczby Punktów Lojalnościowych, aby je wykorzystać", You have already assessed for the assessment criteria {}.,Oceniałeś już kryteria oceny {}., You have already selected items from {0} {1},Już wybrane pozycje z {0} {1}, You have been invited to collaborate on the project: {0},Zostałeś zaproszony do współpracy przy projekcie: {0}, diff --git a/erpnext/translations/ps.csv b/erpnext/translations/ps.csv index 8788bcb42b..27df03ceb2 100644 --- a/erpnext/translations/ps.csv +++ b/erpnext/translations/ps.csv @@ -3094,7 +3094,7 @@ You cannot delete Fiscal Year {0}. Fiscal Year {0} is set as default in Global S You cannot delete Project Type 'External',تاسو د پروژې ډول 'بهرني' نه ړنګولی شئ, You cannot edit root node.,تاسو د ریډ نوډ سمون نشو کولی., You cannot restart a Subscription that is not cancelled.,تاسو نشي کولی هغه یو بل ریکارډ بیا پیل کړئ چې رد شوی نه وي., -You don't have enought Loyalty Points to redeem,تاسو د ژغورلو لپاره د وفادارۍ ټکي نلرئ, +You don't have enough Loyalty Points to redeem,تاسو د ژغورلو لپاره د وفادارۍ ټکي نلرئ, You have already assessed for the assessment criteria {}.,تاسو مخکې د ارزونې معیارونه ارزول {}., You have already selected items from {0} {1},تاسو وخته ټاکل څخه توکي {0} د {1}, You have been invited to collaborate on the project: {0},تاسو ته په دغه پروژه کې همکاري بلل شوي دي: {0}, diff --git a/erpnext/translations/pt-BR.csv b/erpnext/translations/pt-BR.csv index 3aa00bad9c..c07082ec03 100644 --- a/erpnext/translations/pt-BR.csv +++ b/erpnext/translations/pt-BR.csv @@ -3094,7 +3094,7 @@ You cannot delete Fiscal Year {0}. Fiscal Year {0} is set as default in Global S You cannot delete Project Type 'External',Você não pode excluir o Tipo de Projeto ';Externo';, You cannot edit root node.,Você não pode editar o nó raiz., You cannot restart a Subscription that is not cancelled.,Você não pode reiniciar uma Assinatura que não seja cancelada., -You don't have enought Loyalty Points to redeem,Você não tem suficientes pontos de lealdade para resgatar, +You don't have enough Loyalty Points to redeem,Você não tem suficientes pontos de lealdade para resgatar, You have already assessed for the assessment criteria {}.,Você já avaliou os critérios de avaliação {}., You have already selected items from {0} {1},Já selecionou itens de {0} {1}, You have been invited to collaborate on the project: {0},Você foi convidado para colaborar com o projeto: {0}, diff --git a/erpnext/translations/pt.csv b/erpnext/translations/pt.csv index f52ed55147..9b7a854d04 100644 --- a/erpnext/translations/pt.csv +++ b/erpnext/translations/pt.csv @@ -3094,7 +3094,7 @@ You cannot delete Fiscal Year {0}. Fiscal Year {0} is set as default in Global S You cannot delete Project Type 'External',Você não pode excluir o Tipo de Projeto 'Externo', You cannot edit root node.,Você não pode editar o nó raiz., You cannot restart a Subscription that is not cancelled.,Você não pode reiniciar uma Assinatura que não seja cancelada., -You don't have enought Loyalty Points to redeem,Você não tem suficientes pontos de lealdade para resgatar, +You don't have enough Loyalty Points to redeem,Você não tem suficientes pontos de lealdade para resgatar, You have already assessed for the assessment criteria {}.,Você já avaliou os critérios de avaliação {}., You have already selected items from {0} {1},Já selecionou itens de {0} {1}, You have been invited to collaborate on the project: {0},Foi convidado para colaborar com o projeto: {0}, diff --git a/erpnext/translations/ro.csv b/erpnext/translations/ro.csv index d3e2685ecf..6c814194c0 100644 --- a/erpnext/translations/ro.csv +++ b/erpnext/translations/ro.csv @@ -3093,7 +3093,7 @@ You cannot delete Fiscal Year {0}. Fiscal Year {0} is set as default in Global S You cannot delete Project Type 'External',Nu puteți șterge tipul de proiect "extern", You cannot edit root node.,Nu puteți edita nodul rădăcină., You cannot restart a Subscription that is not cancelled.,Nu puteți reporni o abonament care nu este anulat., -You don't have enought Loyalty Points to redeem,Nu aveți puncte de loialitate pentru a răscumpăra, +You don't have enough Loyalty Points to redeem,Nu aveți puncte de loialitate pentru a răscumpăra, You have already assessed for the assessment criteria {}.,Ați evaluat deja criteriile de evaluare {}., You have already selected items from {0} {1},Ați selectat deja un produs de la {0} {1}, You have been invited to collaborate on the project: {0},Ați fost invitat să colaboreze la proiect: {0}, diff --git a/erpnext/translations/ru.csv b/erpnext/translations/ru.csv index 405ce4627a..92442cda1a 100644 --- a/erpnext/translations/ru.csv +++ b/erpnext/translations/ru.csv @@ -3092,7 +3092,7 @@ You cannot delete Fiscal Year {0}. Fiscal Year {0} is set as default in Global S You cannot delete Project Type 'External',"Вы не можете удалить проект типа ""Внешний""", You cannot edit root node.,Вы не можете редактировать корневой узел., You cannot restart a Subscription that is not cancelled.,"Вы не можете перезапустить подписку, которая не отменена.", -You don't have enought Loyalty Points to redeem,У вас недостаточно очков лояльности для выкупа, +You don't have enough Loyalty Points to redeem,У вас недостаточно очков лояльности для выкупа, You have already assessed for the assessment criteria {}.,Вы уже оценили критерии оценки {}., You have already selected items from {0} {1},Вы уже выбрали продукты из {0} {1}, You have been invited to collaborate on the project: {0},Вы были приглашены для совместной работы над проектом: {0}, diff --git a/erpnext/translations/rw.csv b/erpnext/translations/rw.csv index ecad4f5efc..55b79fe73f 100644 --- a/erpnext/translations/rw.csv +++ b/erpnext/translations/rw.csv @@ -3094,7 +3094,7 @@ You cannot delete Fiscal Year {0}. Fiscal Year {0} is set as default in Global S You cannot delete Project Type 'External',Ntushobora gusiba Ubwoko bwumushinga 'Hanze', You cannot edit root node.,Ntushobora guhindura imizi., You cannot restart a Subscription that is not cancelled.,Ntushobora gutangira Kwiyandikisha bidahagaritswe., -You don't have enought Loyalty Points to redeem,Ntabwo ufite amanota ahagije yo gucungura, +You don't have enough Loyalty Points to redeem,Ntabwo ufite amanota ahagije yo gucungura, You have already assessed for the assessment criteria {}.,Mumaze gusuzuma ibipimo ngenderwaho {}., You have already selected items from {0} {1},Mumaze guhitamo ibintu kuva {0} {1}, You have been invited to collaborate on the project: {0},Watumiwe gufatanya kumushinga: {0}, diff --git a/erpnext/translations/si.csv b/erpnext/translations/si.csv index 568f8927ef..b43af743af 100644 --- a/erpnext/translations/si.csv +++ b/erpnext/translations/si.csv @@ -3094,7 +3094,7 @@ You cannot delete Fiscal Year {0}. Fiscal Year {0} is set as default in Global S You cannot delete Project Type 'External',ඔබට ව්යාපෘති වර්ගය 'බාහිර', You cannot edit root node.,ඔබට root node සංස්කරණය කළ නොහැක., You cannot restart a Subscription that is not cancelled.,අවලංගු නොකළ දායකත්ව නැවත ආරම්භ කළ නොහැක., -You don't have enought Loyalty Points to redeem,ඔබ මුදා හැරීමට පක්ෂපාතීත්වයේ පොත්වලට ඔබ කැමති නැත, +You don't have enough Loyalty Points to redeem,ඔබ මුදා හැරීමට පක්ෂපාතීත්වයේ පොත්වලට ඔබ කැමති නැත, You have already assessed for the assessment criteria {}.,තක්සේරු නිර්ණායක {} සඳහා ඔබ දැනටමත් තක්සේරු කර ඇත., You have already selected items from {0} {1},ඔබ මේ වන විටත් {0} {1} සිට භාණ්ඩ තෝරාගෙන ඇති, You have been invited to collaborate on the project: {0},ඔබ මෙම ව්යාපෘතිය පිළිබඳව සහයෝගයෙන් කටයුතු කිරීමට ආරාධනා කර ඇත: {0}, diff --git a/erpnext/translations/sk.csv b/erpnext/translations/sk.csv index a97f6c01df..451b88207a 100644 --- a/erpnext/translations/sk.csv +++ b/erpnext/translations/sk.csv @@ -3094,7 +3094,7 @@ You cannot delete Fiscal Year {0}. Fiscal Year {0} is set as default in Global S You cannot delete Project Type 'External',Nemôžete odstrániť typ projektu "Externé", You cannot edit root node.,Nemôžete upraviť koreňový uzol., You cannot restart a Subscription that is not cancelled.,"Predplatné, ktoré nie je zrušené, nemôžete reštartovať.", -You don't have enought Loyalty Points to redeem,Nemáte dostatok vernostných bodov na vykúpenie, +You don't have enough Loyalty Points to redeem,Nemáte dostatok vernostných bodov na vykúpenie, You have already assessed for the assessment criteria {}.,Vyhodnotili ste kritériá hodnotenia {}., You have already selected items from {0} {1},Už ste vybrané položky z {0} {1}, You have been invited to collaborate on the project: {0},Boli ste pozvaní k spolupráci na projekte: {0}, diff --git a/erpnext/translations/sl.csv b/erpnext/translations/sl.csv index b653990cff..0cb7a6fcc1 100644 --- a/erpnext/translations/sl.csv +++ b/erpnext/translations/sl.csv @@ -3094,7 +3094,7 @@ You cannot delete Fiscal Year {0}. Fiscal Year {0} is set as default in Global S You cannot delete Project Type 'External',Ne morete izbrisati vrste projekta "Zunanji", You cannot edit root node.,Rootnega vozlišča ne morete urejati., You cannot restart a Subscription that is not cancelled.,"Naročnino, ki ni preklican, ne morete znova zagnati.", -You don't have enought Loyalty Points to redeem,Za unovčevanje niste prejeli točk za zvestobo, +You don't have enough Loyalty Points to redeem,Za unovčevanje niste prejeli točk za zvestobo, You have already assessed for the assessment criteria {}.,Ste že ocenili za ocenjevalnih meril {}., You have already selected items from {0} {1},Ste že izbrane postavke iz {0} {1}, You have been invited to collaborate on the project: {0},Ti so bili povabljeni k sodelovanju na projektu: {0}, diff --git a/erpnext/translations/sq.csv b/erpnext/translations/sq.csv index 964c840eec..742dfccf40 100644 --- a/erpnext/translations/sq.csv +++ b/erpnext/translations/sq.csv @@ -3094,7 +3094,7 @@ You cannot delete Fiscal Year {0}. Fiscal Year {0} is set as default in Global S You cannot delete Project Type 'External',Ju nuk mund të fshini llojin e projektit 'Jashtë', You cannot edit root node.,Nuk mund të ndryshosh nyjen e rrënjës., You cannot restart a Subscription that is not cancelled.,Nuk mund të rifilloni një Abonimi që nuk anulohet., -You don't have enought Loyalty Points to redeem,Ju nuk keni shumë pikat e Besnikërisë për të shpenguar, +You don't have enough Loyalty Points to redeem,Ju nuk keni shumë pikat e Besnikërisë për të shpenguar, You have already assessed for the assessment criteria {}.,Ju kanë vlerësuar tashmë me kriteret e vlerësimit {}., You have already selected items from {0} {1},Ju keni zgjedhur tashmë artikuj nga {0} {1}, You have been invited to collaborate on the project: {0},Ju keni qenë të ftuar për të bashkëpunuar në këtë projekt: {0}, diff --git a/erpnext/translations/sr-SP.csv b/erpnext/translations/sr-SP.csv index 25223db05a..bb283539e7 100644 --- a/erpnext/translations/sr-SP.csv +++ b/erpnext/translations/sr-SP.csv @@ -503,7 +503,7 @@ Mobile,Mobilni, Price List Rate,Cijena, Discount Amount,Vrijednost popusta, Sales Invoice Trends,Trendovi faktura prodaje, -You don't have enought Loyalty Points to redeem,Немате довољно Бодова Лојалности. +You don't have enough Loyalty Points to redeem,Немате довољно Бодова Лојалности. Tax Breakup,Porez po pozicijama, Task,Zadatak, Add / Edit Prices,Dodaj / Izmijeni cijene, diff --git a/erpnext/translations/sr.csv b/erpnext/translations/sr.csv index b8428d010a..c5662ad3c5 100644 --- a/erpnext/translations/sr.csv +++ b/erpnext/translations/sr.csv @@ -3094,7 +3094,7 @@ You cannot delete Fiscal Year {0}. Fiscal Year {0} is set as default in Global S You cannot delete Project Type 'External',Не можете обрисати тип пројекта 'Спољни', You cannot edit root node.,Не можете уређивати роот чвор., You cannot restart a Subscription that is not cancelled.,Не можете поново покренути претплату која није отказана., -You don't have enought Loyalty Points to redeem,Не искористите Лоиалти Поинтс за откуп, +You don't have enough Loyalty Points to redeem,Не искористите Лоиалти Поинтс за откуп, You have already assessed for the assessment criteria {}.,Већ сте оцијенили за критеријуми за оцењивање {}., You have already selected items from {0} {1},Који сте изабрали ставке из {0} {1}, You have been invited to collaborate on the project: {0},Позвани сте да сарађују на пројекту: {0}, diff --git a/erpnext/translations/sr_sp.csv b/erpnext/translations/sr_sp.csv index c121e6a6ed..2383c6ec0b 100644 --- a/erpnext/translations/sr_sp.csv +++ b/erpnext/translations/sr_sp.csv @@ -545,7 +545,7 @@ You cannot credit and debit same account at the same time,Не можете кр You cannot delete Fiscal Year {0}. Fiscal Year {0} is set as default in Global Settings,Ne možete obrisati fiskalnu godinu {0}. Fiskalna {0} godina je označena kao trenutna u globalnim podešavanjima., You cannot delete Project Type 'External',"Не можете обрисати ""Спољни"" тип пројекта.", You cannot edit root node.,Не можете уређивати коренски чвор., -You don't have enought Loyalty Points to redeem,Немате довољно Бодова Лојалности., +You don't have enough Loyalty Points to redeem,Немате довољно Бодова Лојалности., You have already assessed for the assessment criteria {}.,Већ сте оценили за критеријум оцењивања {}., You have already selected items from {0} {1},Већ сте изабрали ставке из {0} {1}, You have been invited to collaborate on the project: {0},Позвани сте да сарађујете на пројекту: {0}, diff --git a/erpnext/translations/sv.csv b/erpnext/translations/sv.csv index f4ac1d5151..abdbc6bb7d 100644 --- a/erpnext/translations/sv.csv +++ b/erpnext/translations/sv.csv @@ -3094,7 +3094,7 @@ You cannot delete Fiscal Year {0}. Fiscal Year {0} is set as default in Global S You cannot delete Project Type 'External',Du kan inte ta bort Project Type 'External', You cannot edit root node.,Du kan inte redigera rotknutpunkt., You cannot restart a Subscription that is not cancelled.,Du kan inte starta om en prenumeration som inte avbryts., -You don't have enought Loyalty Points to redeem,Du har inte tillräckligt med lojalitetspoäng för att lösa in, +You don't have enough Loyalty Points to redeem,Du har inte tillräckligt med lojalitetspoäng för att lösa in, You have already assessed for the assessment criteria {}.,Du har redan bedömt för bedömningskriterierna {}., You have already selected items from {0} {1},Du har redan valt objekt från {0} {1}, You have been invited to collaborate on the project: {0},Du har blivit inbjuden att samarbeta i projektet: {0}, diff --git a/erpnext/translations/sw.csv b/erpnext/translations/sw.csv index 9f2504eefa..5f29c3fb43 100644 --- a/erpnext/translations/sw.csv +++ b/erpnext/translations/sw.csv @@ -3094,7 +3094,7 @@ You cannot delete Fiscal Year {0}. Fiscal Year {0} is set as default in Global S You cannot delete Project Type 'External',Huwezi kufuta Aina ya Mradi 'Nje', You cannot edit root node.,Huwezi kubadilisha node ya mizizi., You cannot restart a Subscription that is not cancelled.,Huwezi kuanzisha upya Usajili ambao haujahairiwa., -You don't have enought Loyalty Points to redeem,Huna ushawishi wa Pole ya Uaminifu ili ukomboe, +You don't have enough Loyalty Points to redeem,Huna ushawishi wa Pole ya Uaminifu ili ukomboe, You have already assessed for the assessment criteria {}.,Tayari umehakikishia vigezo vya tathmini {}., You have already selected items from {0} {1},Tayari umechagua vitu kutoka {0} {1}, You have been invited to collaborate on the project: {0},Umealikwa kushirikiana kwenye mradi: {0}, diff --git a/erpnext/translations/ta.csv b/erpnext/translations/ta.csv index cb8b83a75a..d36f47ce33 100644 --- a/erpnext/translations/ta.csv +++ b/erpnext/translations/ta.csv @@ -3094,7 +3094,7 @@ You cannot delete Fiscal Year {0}. Fiscal Year {0} is set as default in Global S You cannot delete Project Type 'External',நீங்கள் திட்டம் வகை 'வெளிப்புற' நீக்க முடியாது, You cannot edit root node.,ரூட் முனையை நீங்கள் திருத்த முடியாது., You cannot restart a Subscription that is not cancelled.,ரத்துசெய்யப்படாத சந்தாவை மறுதொடக்கம் செய்ய முடியாது., -You don't have enought Loyalty Points to redeem,நீங்கள் மீட்கும் விசுவாச புள்ளிகளைப் பெறுவீர்கள், +You don't have enough Loyalty Points to redeem,நீங்கள் மீட்கும் விசுவாச புள்ளிகளைப் பெறுவீர்கள், You have already assessed for the assessment criteria {}.,ஏற்கனவே மதிப்பீட்டிற்குத் தகுதி மதிப்பீடு செய்யப்பட்டதன் {}., You have already selected items from {0} {1},நீங்கள் ஏற்கனவே இருந்து பொருட்களை தேர்ந்தெடுத்த {0} {1}, You have been invited to collaborate on the project: {0},நீங்கள் திட்டம் இணைந்து அழைக்கப்பட்டுள்ளனர்: {0}, diff --git a/erpnext/translations/te.csv b/erpnext/translations/te.csv index a2f49606b5..83d9c8c0da 100644 --- a/erpnext/translations/te.csv +++ b/erpnext/translations/te.csv @@ -3094,7 +3094,7 @@ You cannot delete Fiscal Year {0}. Fiscal Year {0} is set as default in Global S You cannot delete Project Type 'External',మీరు ప్రాజెక్ట్ రకం 'బాహ్య' తొలగించలేరు, You cannot edit root node.,మీరు రూట్ నోడ్ను సవరించలేరు., You cannot restart a Subscription that is not cancelled.,మీరు రద్దు చేయని సభ్యత్వాన్ని పునఃప్రారంభించలేరు., -You don't have enought Loyalty Points to redeem,మీరు విమోచన చేయడానికి లాయల్టీ పాయింట్స్ను కలిగి ఉండరు, +You don't have enough Loyalty Points to redeem,మీరు విమోచన చేయడానికి లాయల్టీ పాయింట్స్ను కలిగి ఉండరు, You have already assessed for the assessment criteria {}.,మీరు ఇప్పటికే అంచనా ప్రమాణం కోసం అంచనా {}., You have already selected items from {0} {1},మీరు ఇప్పటికే ఎంపిక నుండి అంశాలను రోజులో {0} {1}, You have been invited to collaborate on the project: {0},మీరు ప్రాజెక్ట్ సహకరించడానికి ఆహ్వానించబడ్డారు: {0}, diff --git a/erpnext/translations/th.csv b/erpnext/translations/th.csv index af272fac18..e49285653e 100644 --- a/erpnext/translations/th.csv +++ b/erpnext/translations/th.csv @@ -3094,7 +3094,7 @@ You cannot delete Fiscal Year {0}. Fiscal Year {0} is set as default in Global S You cannot delete Project Type 'External',คุณไม่สามารถลบประเภทโครงการ 'ภายนอก', You cannot edit root node.,คุณไม่สามารถแก้ไขโหนดรากได้, You cannot restart a Subscription that is not cancelled.,คุณไม่สามารถรีสตาร์ทการสมัครสมาชิกที่ไม่ได้ยกเลิกได้, -You don't have enought Loyalty Points to redeem,คุณไม่มีจุดภักดีเพียงพอที่จะไถ่ถอน, +You don't have enough Loyalty Points to redeem,คุณไม่มีจุดภักดีเพียงพอที่จะไถ่ถอน, You have already assessed for the assessment criteria {}.,คุณได้รับการประเมินเกณฑ์การประเมินแล้ว {}, You have already selected items from {0} {1},คุณได้เลือกแล้วรายการจาก {0} {1}, You have been invited to collaborate on the project: {0},คุณได้รับเชิญที่จะทำงานร่วมกันในโครงการ: {0}, diff --git a/erpnext/translations/tr.csv b/erpnext/translations/tr.csv index 9c8fb3fa7b..e0034c09b2 100644 --- a/erpnext/translations/tr.csv +++ b/erpnext/translations/tr.csv @@ -3094,7 +3094,7 @@ You cannot delete Fiscal Year {0}. Fiscal Year {0} is set as default in Global S You cannot delete Project Type 'External','Dış' Proje Türünü silemezsiniz., You cannot edit root node.,Kök düğümünü düzenleyemezsiniz., You cannot restart a Subscription that is not cancelled.,İptal edilmeyen bir Aboneliği başlatamazsınız., -You don't have enought Loyalty Points to redeem,Kullanılması gereken sadakat puanlarına sahip olabilirsiniz, +You don't have enough Loyalty Points to redeem,Kullanılması gereken sadakat puanlarına sahip olabilirsiniz, You have already assessed for the assessment criteria {}.,Zaten değerlendirme kriteri {} için değerlendirdiniz., You have already selected items from {0} {1},Zaten öğelerinizi seçtiniz {0} {1}, You have been invited to collaborate on the project: {0},{0} projesine davet edilmek için davet edildiniz, diff --git a/erpnext/translations/uk.csv b/erpnext/translations/uk.csv index 1b78f9647d..f4faedc21e 100644 --- a/erpnext/translations/uk.csv +++ b/erpnext/translations/uk.csv @@ -3094,7 +3094,7 @@ You cannot delete Fiscal Year {0}. Fiscal Year {0} is set as default in Global S You cannot delete Project Type 'External',Ви не можете видалити тип проекту "Зовнішній", You cannot edit root node.,Ви не можете редагувати кореневий вузол., You cannot restart a Subscription that is not cancelled.,"Ви не можете перезапустити підписку, яку не скасовано.", -You don't have enought Loyalty Points to redeem,"Ви не маєте впевнених точок лояльності, щоб викупити", +You don't have enough Loyalty Points to redeem,"Ви не маєте впевнених точок лояльності, щоб викупити", You have already assessed for the assessment criteria {}.,Ви вже оцінили за критеріями оцінки {}., You have already selected items from {0} {1},Ви вже вибрали елементи з {0} {1}, You have been invited to collaborate on the project: {0},Ви були запрошені для спільної роботи над проектом: {0}, diff --git a/erpnext/translations/ur.csv b/erpnext/translations/ur.csv index e32e59427d..e958e573a6 100644 --- a/erpnext/translations/ur.csv +++ b/erpnext/translations/ur.csv @@ -3094,7 +3094,7 @@ You cannot delete Fiscal Year {0}. Fiscal Year {0} is set as default in Global S You cannot delete Project Type 'External',آپ پراجیکٹ کی قسم کو خارج نہیں کرسکتے ہیں 'بیرونی', You cannot edit root node.,آپ جڑ نوڈ میں ترمیم نہیں کر سکتے ہیں., You cannot restart a Subscription that is not cancelled.,آپ ایک سبسکرپشن کو دوبارہ شروع نہیں کرسکتے جو منسوخ نہیں ہوسکتا., -You don't have enought Loyalty Points to redeem,آپ کو بہت زیادہ وفادار پوائنٹس حاصل کرنے کے لئے نہیں ہے, +You don't have enough Loyalty Points to redeem,آپ کو بہت زیادہ وفادار پوائنٹس حاصل کرنے کے لئے نہیں ہے, You have already assessed for the assessment criteria {}.,آپ نے پہلے ہی تشخیص کے معیار کے تعین کی ہے {}., You have already selected items from {0} {1},آپ نے پہلے ہی سے اشیاء کو منتخب کیا ہے {0} {1}, You have been invited to collaborate on the project: {0},آپ کو منصوبے پر تعاون کرنے کیلئے مدعو کیا گیا ہے: {0}, diff --git a/erpnext/translations/uz.csv b/erpnext/translations/uz.csv index 30535596ba..6cdb23c60b 100644 --- a/erpnext/translations/uz.csv +++ b/erpnext/translations/uz.csv @@ -3094,7 +3094,7 @@ You cannot delete Fiscal Year {0}. Fiscal Year {0} is set as default in Global S You cannot delete Project Type 'External',Siz "Tashqi" loyiha turini o'chira olmaysiz, You cannot edit root node.,Ildiz tugunni tahrirlay olmaysiz., You cannot restart a Subscription that is not cancelled.,Bekor qilinmagan obunani qayta boshlash mumkin emas., -You don't have enought Loyalty Points to redeem,Siz sotib olish uchun sodiqlik nuqtalari yo'q, +You don't have enough Loyalty Points to redeem,Siz sotib olish uchun sodiqlik nuqtalari yo'q, You have already assessed for the assessment criteria {}.,Siz allaqachon baholash mezonlari uchun baholagansiz {}., You have already selected items from {0} {1},{0} {1} dan tanlangan elementlarni tanladingiz, You have been invited to collaborate on the project: {0},Siz loyihada hamkorlik qilish uchun taklif qilingan: {0}, diff --git a/erpnext/translations/vi.csv b/erpnext/translations/vi.csv index 5c74694245..650ac287cc 100644 --- a/erpnext/translations/vi.csv +++ b/erpnext/translations/vi.csv @@ -3094,7 +3094,7 @@ You cannot delete Fiscal Year {0}. Fiscal Year {0} is set as default in Global S You cannot delete Project Type 'External',Bạn không thể xóa Loại dự án 'Bên ngoài', You cannot edit root node.,Bạn không thể chỉnh sửa nút gốc., You cannot restart a Subscription that is not cancelled.,Bạn không thể khởi động lại Đăng ký không bị hủy., -You don't have enought Loyalty Points to redeem,Bạn không có Điểm trung thành đủ để đổi, +You don't have enough Loyalty Points to redeem,Bạn không có Điểm trung thành đủ để đổi, You have already assessed for the assessment criteria {}.,Bạn đã đánh giá các tiêu chí đánh giá {}., You have already selected items from {0} {1},Bạn đã chọn các mục từ {0} {1}, You have been invited to collaborate on the project: {0},Bạn được lời mời cộng tác trong dự án: {0}, diff --git a/erpnext/translations/zh-TW.csv b/erpnext/translations/zh-TW.csv index 25204f14e6..0209f441db 100644 --- a/erpnext/translations/zh-TW.csv +++ b/erpnext/translations/zh-TW.csv @@ -1542,7 +1542,7 @@ Payables,應付帳款 Course Intro,課程介紹 MWS Auth Token,MWS Auth Token, Stock Entry {0} created,庫存輸入{0}創建 -You don't have enought Loyalty Points to redeem,您沒有獲得忠誠度積分兌換 +You don't have enough Loyalty Points to redeem,您沒有獲得忠誠度積分兌換 Please set associated account in Tax Withholding Category {0} against Company {1},請在針對公司{1}的預扣稅分類{0}中設置關聯帳戶 Row #{0}: Rejected Qty can not be entered in Purchase Return,行#{0}:駁回採購退貨數量不能進入 Changing Customer Group for the selected Customer is not allowed.,不允許更改所選客戶的客戶組。 diff --git a/erpnext/translations/zh.csv b/erpnext/translations/zh.csv index 9b2fbf04af..d51bf6b98b 100644 --- a/erpnext/translations/zh.csv +++ b/erpnext/translations/zh.csv @@ -3094,7 +3094,7 @@ You cannot delete Fiscal Year {0}. Fiscal Year {0} is set as default in Global S You cannot delete Project Type 'External',您不能删除“外部”类型的项目, You cannot edit root node.,您不能编辑根节点。, You cannot restart a Subscription that is not cancelled.,您无法重新启动未取消的订阅。, -You don't have enought Loyalty Points to redeem,您没有获得忠诚度积分兑换, +You don't have enough Loyalty Points to redeem,您没有获得忠诚度积分兑换, You have already assessed for the assessment criteria {}.,您已经评估了评估标准{}。, You have already selected items from {0} {1},您已经选择从项目{0} {1}, You have been invited to collaborate on the project: {0},您已被邀请在项目上进行合作:{0}, diff --git a/erpnext/translations/zh_tw.csv b/erpnext/translations/zh_tw.csv index 54eb86a2fb..75157f02fc 100644 --- a/erpnext/translations/zh_tw.csv +++ b/erpnext/translations/zh_tw.csv @@ -3127,7 +3127,7 @@ You cannot delete Fiscal Year {0}. Fiscal Year {0} is set as default in Global S You cannot delete Project Type 'External',您不能刪除項目類型“外部”, You cannot edit root node.,您不能編輯根節點。, You cannot restart a Subscription that is not cancelled.,您無法重新啟動未取消的訂閱。, -You don't have enought Loyalty Points to redeem,您沒有獲得忠誠度積分兌換, +You don't have enough Loyalty Points to redeem,您沒有獲得忠誠度積分兌換, You have already assessed for the assessment criteria {}.,您已經評估了評估標準{}。, You have already selected items from {0} {1},您已經選擇從項目{0} {1}, You have been invited to collaborate on the project: {0},您已被邀請在項目上進行合作:{0}, From 3f09f811bfe51cc022aa01b4f606a33024e2d0e0 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Tue, 1 Aug 2023 11:20:04 +0530 Subject: [PATCH 088/101] fix: allow fully depreciated existing assets (copy #36378) (#36379) * fix: allow fully depreciated existing assets (cherry picked from commit 9489cba275525a93d92c00f62569888a219a8912) # Conflicts: # erpnext/assets/doctype/asset/asset.json # erpnext/assets/doctype/asset/depreciation.py * chore: fix conflicts in asset.json * chore: fix conflicts in depreciation.py --------- Co-authored-by: anandbaburajan --- erpnext/assets/doctype/asset/asset.json | 21 +++++++++++++------- erpnext/assets/doctype/asset/asset.py | 13 ++++++++---- erpnext/assets/doctype/asset/depreciation.py | 9 +++++++++ 3 files changed, 32 insertions(+), 11 deletions(-) diff --git a/erpnext/assets/doctype/asset/asset.json b/erpnext/assets/doctype/asset/asset.json index 698fc787e8..2eb5f3dc3f 100644 --- a/erpnext/assets/doctype/asset/asset.json +++ b/erpnext/assets/doctype/asset/asset.json @@ -43,6 +43,7 @@ "column_break_33", "opening_accumulated_depreciation", "number_of_depreciations_booked", + "is_fully_depreciated", "section_break_36", "finance_books", "section_break_33", @@ -205,6 +206,7 @@ "fieldname": "disposal_date", "fieldtype": "Date", "label": "Disposal Date", + "no_copy": 1, "read_only": 1 }, { @@ -244,19 +246,17 @@ "label": "Is Existing Asset" }, { - "depends_on": "is_existing_asset", + "depends_on": "eval:(doc.is_existing_asset)", "fieldname": "opening_accumulated_depreciation", "fieldtype": "Currency", "label": "Opening Accumulated Depreciation", - "no_copy": 1, "options": "Company:company:default_currency" }, { - "depends_on": "eval:(doc.is_existing_asset && doc.opening_accumulated_depreciation)", + "depends_on": "eval:(doc.is_existing_asset)", "fieldname": "number_of_depreciations_booked", "fieldtype": "Int", - "label": "Number of Depreciations Booked", - "no_copy": 1 + "label": "Number of Depreciations Booked" }, { "collapsible": 1, @@ -500,6 +500,13 @@ "fieldtype": "HTML", "hidden": 1, "label": "Depreciation Schedule View" + }, + { + "default": "0", + "depends_on": "eval:(doc.is_existing_asset)", + "fieldname": "is_fully_depreciated", + "fieldtype": "Check", + "label": "Is Fully Depreciated" } ], "idx": 72, @@ -533,7 +540,7 @@ "table_fieldname": "accounts" } ], - "modified": "2023-07-26 13:33:36.821534", + "modified": "2023-07-28 15:47:01.137996", "modified_by": "Administrator", "module": "Assets", "name": "Asset", @@ -577,4 +584,4 @@ "states": [], "title_field": "asset_name", "track_changes": 1 -} \ No newline at end of file +} diff --git a/erpnext/assets/doctype/asset/asset.py b/erpnext/assets/doctype/asset/asset.py index 5d35808413..9efa18ba9d 100644 --- a/erpnext/assets/doctype/asset/asset.py +++ b/erpnext/assets/doctype/asset/asset.py @@ -194,8 +194,11 @@ class Asset(AccountsController): if not self.calculate_depreciation: return - elif not self.finance_books: - frappe.throw(_("Enter depreciation details")) + else: + if not self.finance_books: + frappe.throw(_("Enter depreciation details")) + if self.is_fully_depreciated: + frappe.throw(_("Depreciation cannot be calculated for fully depreciated assets")) if self.is_existing_asset: return @@ -276,7 +279,7 @@ class Asset(AccountsController): depreciable_amount = flt(self.gross_purchase_amount) - flt(row.expected_value_after_useful_life) if flt(self.opening_accumulated_depreciation) > depreciable_amount: frappe.throw( - _("Opening Accumulated Depreciation must be less than equal to {0}").format( + _("Opening Accumulated Depreciation must be less than or equal to {0}").format( depreciable_amount ) ) @@ -412,7 +415,9 @@ class Asset(AccountsController): expected_value_after_useful_life = self.finance_books[idx].expected_value_after_useful_life value_after_depreciation = self.finance_books[idx].value_after_depreciation - if flt(value_after_depreciation) <= expected_value_after_useful_life: + if ( + flt(value_after_depreciation) <= expected_value_after_useful_life or self.is_fully_depreciated + ): status = "Fully Depreciated" elif flt(value_after_depreciation) < flt(self.gross_purchase_amount): status = "Partially Depreciated" diff --git a/erpnext/assets/doctype/asset/depreciation.py b/erpnext/assets/doctype/asset/depreciation.py index e1431eae17..a311bc699a 100644 --- a/erpnext/assets/doctype/asset/depreciation.py +++ b/erpnext/assets/doctype/asset/depreciation.py @@ -398,6 +398,15 @@ def reverse_depreciation_entry_made_after_disposal(asset, date): reverse_journal_entry = make_reverse_journal_entry(schedule.journal_entry) reverse_journal_entry.posting_date = nowdate() + + for account in reverse_journal_entry.accounts: + account.update( + { + "reference_type": "Asset", + "reference_name": asset.name, + } + ) + frappe.flags.is_reverse_depr_entry = True reverse_journal_entry.submit() From 2ab3d752742d818ab5b6750591ed8d75cf62dec7 Mon Sep 17 00:00:00 2001 From: Anand Baburajan Date: Tue, 1 Aug 2023 12:00:24 +0530 Subject: [PATCH 089/101] feat: asset activity (#36391) * feat: asset activity * chore: add more actions to asset activity * chore: fix failing test due to timestamp mismatch error * chore: rewriting asset activity messages * chore: add report and add it to workspace * chore: show user in list view --- .../doctype/sales_invoice/sales_invoice.py | 5 +- erpnext/assets/doctype/asset/asset.json | 7 +- erpnext/assets/doctype/asset/asset.py | 33 +++++- erpnext/assets/doctype/asset/depreciation.py | 5 + .../assets/doctype/asset_activity/__init__.py | 0 .../doctype/asset_activity/asset_activity.js | 8 ++ .../asset_activity/asset_activity.json | 109 ++++++++++++++++++ .../doctype/asset_activity/asset_activity.py | 20 ++++ .../asset_activity/test_asset_activity.py | 9 ++ .../asset_capitalization.py | 31 ++++- .../doctype/asset_movement/asset_movement.py | 26 ++++- .../doctype/asset_repair/asset_repair.py | 23 +++- .../asset_value_adjustment.py | 17 +++ .../assets/report/asset_activity/__init__.py | 0 .../report/asset_activity/asset_activity.json | 33 ++++++ erpnext/assets/workspace/assets/assets.json | 11 ++ 16 files changed, 329 insertions(+), 8 deletions(-) create mode 100644 erpnext/assets/doctype/asset_activity/__init__.py create mode 100644 erpnext/assets/doctype/asset_activity/asset_activity.js create mode 100644 erpnext/assets/doctype/asset_activity/asset_activity.json create mode 100644 erpnext/assets/doctype/asset_activity/asset_activity.py create mode 100644 erpnext/assets/doctype/asset_activity/test_asset_activity.py create mode 100644 erpnext/assets/report/asset_activity/__init__.py create mode 100644 erpnext/assets/report/asset_activity/asset_activity.json diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py index f5ee2285d2..b0cc8ca29f 100644 --- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py +++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py @@ -32,6 +32,7 @@ from erpnext.assets.doctype.asset.depreciation import ( reset_depreciation_schedule, reverse_depreciation_entry_made_after_disposal, ) +from erpnext.assets.doctype.asset_activity.asset_activity import add_asset_activity from erpnext.controllers.accounts_controller import validate_account_head from erpnext.controllers.selling_controller import SellingController from erpnext.projects.doctype.timesheet.timesheet import get_projectwise_timesheet_data @@ -1176,12 +1177,13 @@ class SalesInvoice(SellingController): self.get("posting_date"), ) asset.db_set("disposal_date", None) + add_asset_activity(asset.name, _("Asset returned")) if asset.calculate_depreciation: posting_date = frappe.db.get_value("Sales Invoice", self.return_against, "posting_date") reverse_depreciation_entry_made_after_disposal(asset, posting_date) notes = _( - "This schedule was created when Asset {0} was returned after being sold through Sales Invoice {1}." + "This schedule was created when Asset {0} was returned through Sales Invoice {1}." ).format( get_link_to_form(asset.doctype, asset.name), get_link_to_form(self.doctype, self.get("name")), @@ -1209,6 +1211,7 @@ class SalesInvoice(SellingController): self.get("posting_date"), ) asset.db_set("disposal_date", self.posting_date) + add_asset_activity(asset.name, _("Asset sold")) for gle in fixed_asset_gl_entries: gle["against"] = self.customer diff --git a/erpnext/assets/doctype/asset/asset.json b/erpnext/assets/doctype/asset/asset.json index 2eb5f3dc3f..befb5248d5 100644 --- a/erpnext/assets/doctype/asset/asset.json +++ b/erpnext/assets/doctype/asset/asset.json @@ -533,6 +533,11 @@ "link_doctype": "Asset Depreciation Schedule", "link_fieldname": "asset" }, + { + "group": "Activity", + "link_doctype": "Asset Activity", + "link_fieldname": "asset" + }, { "group": "Journal Entry", "link_doctype": "Journal Entry", @@ -540,7 +545,7 @@ "table_fieldname": "accounts" } ], - "modified": "2023-07-28 15:47:01.137996", + "modified": "2023-07-28 20:12:44.819616", "modified_by": "Administrator", "module": "Assets", "name": "Asset", diff --git a/erpnext/assets/doctype/asset/asset.py b/erpnext/assets/doctype/asset/asset.py index 9efa18ba9d..252a3dd63f 100644 --- a/erpnext/assets/doctype/asset/asset.py +++ b/erpnext/assets/doctype/asset/asset.py @@ -25,6 +25,7 @@ from erpnext.assets.doctype.asset.depreciation import ( get_depreciation_accounts, get_disposal_account_and_cost_center, ) +from erpnext.assets.doctype.asset_activity.asset_activity import add_asset_activity from erpnext.assets.doctype.asset_category.asset_category import get_asset_category_account from erpnext.assets.doctype.asset_depreciation_schedule.asset_depreciation_schedule import ( cancel_asset_depr_schedules, @@ -59,7 +60,7 @@ class Asset(AccountsController): self.make_asset_movement() if not self.booked_fixed_asset and self.validate_make_gl_entry(): self.make_gl_entries() - if not self.split_from: + if self.calculate_depreciation and not self.split_from: asset_depr_schedules_names = make_draft_asset_depr_schedules_if_not_present(self) convert_draft_asset_depr_schedules_into_active(self) if asset_depr_schedules_names: @@ -71,6 +72,7 @@ class Asset(AccountsController): "Asset Depreciation Schedules created:
{0}

Please check, edit if needed, and submit the Asset." ).format(asset_depr_schedules_links) ) + add_asset_activity(self.name, _("Asset submitted")) def on_cancel(self): self.validate_cancellation() @@ -81,9 +83,10 @@ class Asset(AccountsController): self.ignore_linked_doctypes = ("GL Entry", "Stock Ledger Entry") make_reverse_gl_entries(voucher_type="Asset", voucher_no=self.name) self.db_set("booked_fixed_asset", 0) + add_asset_activity(self.name, _("Asset cancelled")) def after_insert(self): - if not self.split_from: + if self.calculate_depreciation and not self.split_from: asset_depr_schedules_names = make_draft_asset_depr_schedules(self) asset_depr_schedules_links = get_comma_separated_links( asset_depr_schedules_names, "Asset Depreciation Schedule" @@ -93,6 +96,16 @@ class Asset(AccountsController): "Asset Depreciation Schedules created:
{0}

Please check, edit if needed, and submit the Asset." ).format(asset_depr_schedules_links) ) + if not frappe.db.exists( + { + "doctype": "Asset Activity", + "asset": self.name, + } + ): + add_asset_activity(self.name, _("Asset created")) + + def after_delete(self): + add_asset_activity(self.name, _("Asset deleted")) def validate_asset_and_reference(self): if self.purchase_invoice or self.purchase_receipt: @@ -903,6 +916,13 @@ def update_existing_asset(asset, remaining_qty, new_asset_name): }, ) + add_asset_activity( + asset.name, + _("Asset updated after being split into Asset {0}").format( + get_link_to_form("Asset", new_asset_name) + ), + ) + for row in asset.get("finance_books"): value_after_depreciation = flt( (row.value_after_depreciation * remaining_qty) / asset.asset_quantity @@ -970,6 +990,15 @@ def create_new_asset_after_split(asset, split_qty): (row.expected_value_after_useful_life * split_qty) / asset.asset_quantity ) + new_asset.insert() + + add_asset_activity( + new_asset.name, + _("Asset created after being split from Asset {0}").format( + get_link_to_form("Asset", asset.name) + ), + ) + new_asset.submit() new_asset.set_status() diff --git a/erpnext/assets/doctype/asset/depreciation.py b/erpnext/assets/doctype/asset/depreciation.py index a311bc699a..0588065d39 100644 --- a/erpnext/assets/doctype/asset/depreciation.py +++ b/erpnext/assets/doctype/asset/depreciation.py @@ -21,6 +21,7 @@ from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import ( get_checks_for_pl_and_bs_accounts, ) from erpnext.accounts.doctype.journal_entry.journal_entry import make_reverse_journal_entry +from erpnext.assets.doctype.asset_activity.asset_activity import add_asset_activity from erpnext.assets.doctype.asset_depreciation_schedule.asset_depreciation_schedule import ( get_asset_depr_schedule_doc, get_asset_depr_schedule_name, @@ -325,6 +326,8 @@ def scrap_asset(asset_name): frappe.db.set_value("Asset", asset_name, "journal_entry_for_scrap", je.name) asset.set_status("Scrapped") + add_asset_activity(asset_name, _("Asset scrapped")) + frappe.msgprint(_("Asset scrapped via Journal Entry {0}").format(je.name)) @@ -349,6 +352,8 @@ def restore_asset(asset_name): asset.set_status() + add_asset_activity(asset_name, _("Asset restored")) + def depreciate_asset(asset_doc, date, notes): asset_doc.flags.ignore_validate_update_after_submit = True diff --git a/erpnext/assets/doctype/asset_activity/__init__.py b/erpnext/assets/doctype/asset_activity/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/erpnext/assets/doctype/asset_activity/asset_activity.js b/erpnext/assets/doctype/asset_activity/asset_activity.js new file mode 100644 index 0000000000..38d3434746 --- /dev/null +++ b/erpnext/assets/doctype/asset_activity/asset_activity.js @@ -0,0 +1,8 @@ +// Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and contributors +// For license information, please see license.txt + +// frappe.ui.form.on("Asset Activity", { +// refresh(frm) { + +// }, +// }); diff --git a/erpnext/assets/doctype/asset_activity/asset_activity.json b/erpnext/assets/doctype/asset_activity/asset_activity.json new file mode 100644 index 0000000000..476fb2732e --- /dev/null +++ b/erpnext/assets/doctype/asset_activity/asset_activity.json @@ -0,0 +1,109 @@ +{ + "actions": [], + "creation": "2023-07-28 12:41:13.232505", + "default_view": "List", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "asset", + "column_break_vkdy", + "date", + "column_break_kkxv", + "user", + "section_break_romx", + "subject" + ], + "fields": [ + { + "fieldname": "asset", + "fieldtype": "Link", + "in_list_view": 1, + "in_standard_filter": 1, + "label": "Asset", + "options": "Asset", + "print_width": "165", + "read_only": 1, + "reqd": 1, + "width": "165" + }, + { + "fieldname": "column_break_vkdy", + "fieldtype": "Column Break" + }, + { + "fieldname": "section_break_romx", + "fieldtype": "Section Break" + }, + { + "fieldname": "subject", + "fieldtype": "Small Text", + "in_list_view": 1, + "label": "Subject", + "print_width": "518", + "read_only": 1, + "reqd": 1, + "width": "518" + }, + { + "default": "now", + "fieldname": "date", + "fieldtype": "Datetime", + "in_list_view": 1, + "label": "Date", + "print_width": "158", + "read_only": 1, + "reqd": 1, + "width": "158" + }, + { + "fieldname": "user", + "fieldtype": "Link", + "in_list_view": 1, + "label": "User", + "options": "User", + "print_width": "150", + "read_only": 1, + "reqd": 1, + "width": "150" + }, + { + "fieldname": "column_break_kkxv", + "fieldtype": "Column Break" + } + ], + "in_create": 1, + "index_web_pages_for_search": 1, + "links": [], + "modified": "2023-08-01 11:09:52.584482", + "modified_by": "Administrator", + "module": "Assets", + "name": "Asset Activity", + "owner": "Administrator", + "permissions": [ + { + "email": 1, + "read": 1, + "report": 1, + "role": "System Manager", + "share": 1 + }, + { + "email": 1, + "read": 1, + "report": 1, + "role": "Accounts User", + "share": 1 + }, + { + "email": 1, + "read": 1, + "report": 1, + "role": "Quality Manager", + "share": 1 + } + ], + "sort_field": "modified", + "sort_order": "DESC", + "states": [] +} \ No newline at end of file diff --git a/erpnext/assets/doctype/asset_activity/asset_activity.py b/erpnext/assets/doctype/asset_activity/asset_activity.py new file mode 100644 index 0000000000..28e1b3e32a --- /dev/null +++ b/erpnext/assets/doctype/asset_activity/asset_activity.py @@ -0,0 +1,20 @@ +# Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and contributors +# For license information, please see license.txt + +import frappe +from frappe.model.document import Document + + +class AssetActivity(Document): + pass + + +def add_asset_activity(asset, subject): + frappe.get_doc( + { + "doctype": "Asset Activity", + "asset": asset, + "subject": subject, + "user": frappe.session.user, + } + ).insert(ignore_permissions=True, ignore_links=True) diff --git a/erpnext/assets/doctype/asset_activity/test_asset_activity.py b/erpnext/assets/doctype/asset_activity/test_asset_activity.py new file mode 100644 index 0000000000..7a21559c52 --- /dev/null +++ b/erpnext/assets/doctype/asset_activity/test_asset_activity.py @@ -0,0 +1,9 @@ +# Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and Contributors +# See license.txt + +# import frappe +from frappe.tests.utils import FrappeTestCase + + +class TestAssetActivity(FrappeTestCase): + pass diff --git a/erpnext/assets/doctype/asset_capitalization/asset_capitalization.py b/erpnext/assets/doctype/asset_capitalization/asset_capitalization.py index a883bec71b..858c1db43c 100644 --- a/erpnext/assets/doctype/asset_capitalization/asset_capitalization.py +++ b/erpnext/assets/doctype/asset_capitalization/asset_capitalization.py @@ -18,6 +18,7 @@ from erpnext.assets.doctype.asset.depreciation import ( reset_depreciation_schedule, reverse_depreciation_entry_made_after_disposal, ) +from erpnext.assets.doctype.asset_activity.asset_activity import add_asset_activity from erpnext.assets.doctype.asset_category.asset_category import get_asset_category_account from erpnext.controllers.stock_controller import StockController from erpnext.setup.doctype.brand.brand import get_brand_defaults @@ -519,6 +520,13 @@ class AssetCapitalization(StockController): "fixed_asset_account", item=self.target_item_code, company=asset_doc.company ) + add_asset_activity( + asset_doc.name, + _("Asset created after Asset Capitalization {0} was submitted").format( + get_link_to_form("Asset Capitalization", self.name) + ), + ) + frappe.msgprint( _( "Asset {0} has been created. Please set the depreciation details if any and submit it." @@ -542,9 +550,30 @@ class AssetCapitalization(StockController): def set_consumed_asset_status(self, asset): if self.docstatus == 1: - asset.set_status("Capitalized" if self.target_is_fixed_asset else "Decapitalized") + if self.target_is_fixed_asset: + asset.set_status("Capitalized") + add_asset_activity( + asset.name, + _("Asset capitalized after Asset Capitalization {0} was submitted").format( + get_link_to_form("Asset Capitalization", self.name) + ), + ) + else: + asset.set_status("Decapitalized") + add_asset_activity( + asset.name, + _("Asset decapitalized after Asset Capitalization {0} was submitted").format( + get_link_to_form("Asset Capitalization", self.name) + ), + ) else: asset.set_status() + add_asset_activity( + asset.name, + _("Asset restored after Asset Capitalization {0} was cancelled").format( + get_link_to_form("Asset Capitalization", self.name) + ), + ) @frappe.whitelist() diff --git a/erpnext/assets/doctype/asset_movement/asset_movement.py b/erpnext/assets/doctype/asset_movement/asset_movement.py index b85f7194f9..620aad80ed 100644 --- a/erpnext/assets/doctype/asset_movement/asset_movement.py +++ b/erpnext/assets/doctype/asset_movement/asset_movement.py @@ -5,6 +5,9 @@ import frappe from frappe import _ from frappe.model.document import Document +from frappe.utils import get_link_to_form + +from erpnext.assets.doctype.asset_activity.asset_activity import add_asset_activity class AssetMovement(Document): @@ -128,5 +131,24 @@ class AssetMovement(Document): current_location = latest_movement_entry[0][0] current_employee = latest_movement_entry[0][1] - frappe.db.set_value("Asset", d.asset, "location", current_location) - frappe.db.set_value("Asset", d.asset, "custodian", current_employee) + frappe.db.set_value("Asset", d.asset, "location", current_location, update_modified=False) + frappe.db.set_value("Asset", d.asset, "custodian", current_employee, update_modified=False) + + if current_location and current_employee: + add_asset_activity( + d.asset, + _("Asset received at Location {0} and issued to Employee {1}").format( + get_link_to_form("Location", current_location), + get_link_to_form("Employee", current_employee), + ), + ) + elif current_location: + add_asset_activity( + d.asset, + _("Asset transferred to Location {0}").format(get_link_to_form("Location", current_location)), + ) + elif current_employee: + add_asset_activity( + d.asset, + _("Asset issued to Employee {0}").format(get_link_to_form("Employee", current_employee)), + ) diff --git a/erpnext/assets/doctype/asset_repair/asset_repair.py b/erpnext/assets/doctype/asset_repair/asset_repair.py index f649e510f9..7e95cb2a1b 100644 --- a/erpnext/assets/doctype/asset_repair/asset_repair.py +++ b/erpnext/assets/doctype/asset_repair/asset_repair.py @@ -8,6 +8,7 @@ from frappe.utils import add_months, cint, flt, get_link_to_form, getdate, time_ import erpnext from erpnext.accounts.general_ledger import make_gl_entries from erpnext.assets.doctype.asset.asset import get_asset_account +from erpnext.assets.doctype.asset_activity.asset_activity import add_asset_activity from erpnext.assets.doctype.asset_depreciation_schedule.asset_depreciation_schedule import ( get_depr_schedule, make_new_active_asset_depr_schedules_and_cancel_current_ones, @@ -25,8 +26,14 @@ class AssetRepair(AccountsController): self.calculate_total_repair_cost() def update_status(self): - if self.repair_status == "Pending": + if self.repair_status == "Pending" and self.asset_doc.status != "Out of Order": frappe.db.set_value("Asset", self.asset, "status", "Out of Order") + add_asset_activity( + self.asset, + _("Asset out of order due to Asset Repair {0}").format( + get_link_to_form("Asset Repair", self.name) + ), + ) else: self.asset_doc.set_status() @@ -68,6 +75,13 @@ class AssetRepair(AccountsController): make_new_active_asset_depr_schedules_and_cancel_current_ones(self.asset_doc, notes) self.asset_doc.save() + add_asset_activity( + self.asset, + _("Asset updated after completion of Asset Repair {0}").format( + get_link_to_form("Asset Repair", self.name) + ), + ) + def before_cancel(self): self.asset_doc = frappe.get_doc("Asset", self.asset) @@ -95,6 +109,13 @@ class AssetRepair(AccountsController): make_new_active_asset_depr_schedules_and_cancel_current_ones(self.asset_doc, notes) self.asset_doc.save() + add_asset_activity( + self.asset, + _("Asset updated after cancellation of Asset Repair {0}").format( + get_link_to_form("Asset Repair", self.name) + ), + ) + def after_delete(self): frappe.get_doc("Asset", self.asset).set_status() diff --git a/erpnext/assets/doctype/asset_value_adjustment/asset_value_adjustment.py b/erpnext/assets/doctype/asset_value_adjustment/asset_value_adjustment.py index 8426ed43ff..a1f047352c 100644 --- a/erpnext/assets/doctype/asset_value_adjustment/asset_value_adjustment.py +++ b/erpnext/assets/doctype/asset_value_adjustment/asset_value_adjustment.py @@ -12,6 +12,7 @@ from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import ( ) from erpnext.assets.doctype.asset.asset import get_asset_value_after_depreciation from erpnext.assets.doctype.asset.depreciation import get_depreciation_accounts +from erpnext.assets.doctype.asset_activity.asset_activity import add_asset_activity from erpnext.assets.doctype.asset_depreciation_schedule.asset_depreciation_schedule import ( get_asset_depr_schedule_doc, get_depreciation_amount, @@ -27,9 +28,21 @@ class AssetValueAdjustment(Document): def on_submit(self): self.make_depreciation_entry() self.reschedule_depreciations(self.new_asset_value) + add_asset_activity( + self.asset, + _("Asset's value adjusted after submission of Asset Value Adjustment {0}").format( + get_link_to_form("Asset Value Adjustment", self.name) + ), + ) def on_cancel(self): self.reschedule_depreciations(self.current_asset_value) + add_asset_activity( + self.asset, + _("Asset's value adjusted after cancellation of Asset Value Adjustment {0}").format( + get_link_to_form("Asset Value Adjustment", self.name) + ), + ) def validate_date(self): asset_purchase_date = frappe.db.get_value("Asset", self.asset, "purchase_date") @@ -74,12 +87,16 @@ class AssetValueAdjustment(Document): "account": accumulated_depreciation_account, "credit_in_account_currency": self.difference_amount, "cost_center": depreciation_cost_center or self.cost_center, + "reference_type": "Asset", + "reference_name": self.asset, } debit_entry = { "account": depreciation_expense_account, "debit_in_account_currency": self.difference_amount, "cost_center": depreciation_cost_center or self.cost_center, + "reference_type": "Asset", + "reference_name": self.asset, } accounting_dimensions = get_checks_for_pl_and_bs_accounts() diff --git a/erpnext/assets/report/asset_activity/__init__.py b/erpnext/assets/report/asset_activity/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/erpnext/assets/report/asset_activity/asset_activity.json b/erpnext/assets/report/asset_activity/asset_activity.json new file mode 100644 index 0000000000..cc46775197 --- /dev/null +++ b/erpnext/assets/report/asset_activity/asset_activity.json @@ -0,0 +1,33 @@ +{ + "add_total_row": 0, + "columns": [], + "creation": "2023-08-01 11:14:46.581234", + "disabled": 0, + "docstatus": 0, + "doctype": "Report", + "filters": [], + "idx": 0, + "is_standard": "Yes", + "json": "{}", + "letterhead": null, + "modified": "2023-08-01 11:14:46.581234", + "modified_by": "Administrator", + "module": "Assets", + "name": "Asset Activity", + "owner": "Administrator", + "prepared_report": 0, + "ref_doctype": "Asset Activity", + "report_name": "Asset Activity", + "report_type": "Report Builder", + "roles": [ + { + "role": "System Manager" + }, + { + "role": "Accounts User" + }, + { + "role": "Quality Manager" + } + ] +} \ No newline at end of file diff --git a/erpnext/assets/workspace/assets/assets.json b/erpnext/assets/workspace/assets/assets.json index d810effda0..c6b321e9c1 100644 --- a/erpnext/assets/workspace/assets/assets.json +++ b/erpnext/assets/workspace/assets/assets.json @@ -183,6 +183,17 @@ "link_type": "Report", "onboard": 0, "type": "Link" + }, + { + "dependencies": "Asset Activity", + "hidden": 0, + "is_query_report": 0, + "label": "Asset Activity", + "link_count": 0, + "link_to": "Asset Activity", + "link_type": "Report", + "onboard": 0, + "type": "Link" } ], "modified": "2023-05-24 14:47:20.243146", From ab933df5bbce6aa576b0762a221933734bb5f719 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Tue, 1 Aug 2023 13:12:16 +0530 Subject: [PATCH 090/101] fix: overallocation validation misfire on normal invoices (#36349) * fix: overallocation validation misfire on normal invoices * test: assert misfire doesn't happen --- .../doctype/payment_entry/payment_entry.py | 16 ++++--- .../payment_entry/test_payment_entry.py | 46 +++++++++++++++++++ 2 files changed, 56 insertions(+), 6 deletions(-) diff --git a/erpnext/accounts/doctype/payment_entry/payment_entry.py b/erpnext/accounts/doctype/payment_entry/payment_entry.py index 21adb2759b..29b52729cd 100644 --- a/erpnext/accounts/doctype/payment_entry/payment_entry.py +++ b/erpnext/accounts/doctype/payment_entry/payment_entry.py @@ -277,12 +277,13 @@ class PaymentEntry(AccountsController): fail_message = _("Row #{0}: Allocated Amount cannot be greater than outstanding amount.") - if (flt(d.allocated_amount)) > 0 and flt(d.allocated_amount) > flt(latest.outstanding_amount): - frappe.throw(fail_message.format(d.idx)) - - if d.payment_term and ( - (flt(d.allocated_amount)) > 0 - and flt(d.allocated_amount) > flt(latest.payment_term_outstanding) + if ( + d.payment_term + and ( + (flt(d.allocated_amount)) > 0 + and flt(d.allocated_amount) > flt(latest.payment_term_outstanding) + ) + and self.term_based_allocation_enabled_for_reference(d.reference_doctype, d.reference_name) ): frappe.throw( _( @@ -292,6 +293,9 @@ class PaymentEntry(AccountsController): ) ) + if (flt(d.allocated_amount)) > 0 and flt(d.allocated_amount) > flt(latest.outstanding_amount): + frappe.throw(fail_message.format(d.idx)) + # Check for negative outstanding invoices as well if flt(d.allocated_amount) < 0 and flt(d.allocated_amount) < flt(latest.outstanding_amount): frappe.throw(fail_message.format(d.idx)) diff --git a/erpnext/accounts/doctype/payment_entry/test_payment_entry.py b/erpnext/accounts/doctype/payment_entry/test_payment_entry.py index c6e93f3f7a..dc44fc359c 100644 --- a/erpnext/accounts/doctype/payment_entry/test_payment_entry.py +++ b/erpnext/accounts/doctype/payment_entry/test_payment_entry.py @@ -1156,6 +1156,52 @@ class TestPaymentEntry(FrappeTestCase): si3.cancel() si3.delete() + @change_settings( + "Accounts Settings", + { + "unlink_payment_on_cancellation_of_invoice": 1, + "delete_linked_ledger_entries": 1, + "allow_multi_currency_invoices_against_single_party_account": 1, + }, + ) + def test_overallocation_validation_shouldnt_misfire(self): + """ + Overallocation validation shouldn't fire for Template without "Allocate Payment based on Payment Terms" enabled + + """ + customer = create_customer() + create_payment_terms_template() + + template = frappe.get_doc("Payment Terms Template", "Test Receivable Template") + template.allocate_payment_based_on_payment_terms = 0 + template.save() + + # Validate allocation on base/company currency + si = create_sales_invoice(do_not_save=1, qty=1, rate=200) + si.payment_terms_template = "Test Receivable Template" + si.save().submit() + + si.reload() + pe = get_payment_entry(si.doctype, si.name).save() + # There will no term based allocation + self.assertEqual(len(pe.references), 1) + self.assertEqual(pe.references[0].payment_term, None) + self.assertEqual(flt(pe.references[0].allocated_amount), flt(si.grand_total)) + pe.save() + + # specify a term + pe.references[0].payment_term = template.terms[0].payment_term + # no validation error should be thrown + pe.save() + + pe.paid_amount = si.grand_total + 1 + pe.references[0].allocated_amount = si.grand_total + 1 + self.assertRaises(frappe.ValidationError, pe.save) + + template = frappe.get_doc("Payment Terms Template", "Test Receivable Template") + template.allocate_payment_based_on_payment_terms = 1 + template.save() + def create_payment_entry(**args): payment_entry = frappe.new_doc("Payment Entry") From bd3fc7c4342195ce22cd860cba83e287aaac15b5 Mon Sep 17 00:00:00 2001 From: Corentin Flr <10946971+cogk@users.noreply.github.com> Date: Tue, 1 Aug 2023 14:35:11 +0200 Subject: [PATCH 091/101] fix: Fix query for financial statement report --- .../consolidated_financial_statement.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/accounts/report/consolidated_financial_statement/consolidated_financial_statement.py b/erpnext/accounts/report/consolidated_financial_statement/consolidated_financial_statement.py index 7c2ebe1d20..f1e665a68f 100644 --- a/erpnext/accounts/report/consolidated_financial_statement/consolidated_financial_statement.py +++ b/erpnext/accounts/report/consolidated_financial_statement/consolidated_financial_statement.py @@ -654,7 +654,7 @@ def set_gl_entries_by_account( & (gle.posting_date <= to_date) & (account.lft >= root_lft) & (account.rgt <= root_rgt) - & (account.root_type <= root_type) + & (account.root_type == root_type) ) .orderby(gle.account, gle.posting_date) ) From a8df875820daee7d87e4297445a756c875eb1e6d Mon Sep 17 00:00:00 2001 From: Anand Baburajan Date: Tue, 1 Aug 2023 21:14:27 +0530 Subject: [PATCH 092/101] chore: use datatable for asset depr sch table view (#36449) * chore: use datatable for asset depr sch table view * chore: remove unnecessary code --- erpnext/assets/doctype/asset/asset.js | 49 +++++++++++++++------------ 1 file changed, 27 insertions(+), 22 deletions(-) diff --git a/erpnext/assets/doctype/asset/asset.js b/erpnext/assets/doctype/asset/asset.js index a97ea735da..0a2f61d23b 100644 --- a/erpnext/assets/doctype/asset/asset.js +++ b/erpnext/assets/doctype/asset/asset.js @@ -207,34 +207,39 @@ frappe.ui.form.on('Asset', { }, render_depreciation_schedule_view: function(frm, depr_schedule) { - var wrapper = $(frm.fields_dict["depreciation_schedule_view"].wrapper).empty(); + let wrapper = $(frm.fields_dict["depreciation_schedule_view"].wrapper).empty(); - let table = $(` - - - - - - - - - - -
${__("No.")}${__("Schedule Date")}${__("Depreciation Amount")}${__("Accumulated Depreciation Amount")}${__("Journal Entry")}
`); + let data = []; depr_schedule.forEach((sch) => { - const row = $(` - ${sch['idx']} - ${frappe.format(sch['schedule_date'], { fieldtype: 'Date' })} - ${frappe.format(sch['depreciation_amount'], { fieldtype: 'Currency' })} - ${frappe.format(sch['accumulated_depreciation_amount'], { fieldtype: 'Currency' })} - ${sch['journal_entry'] || ''} - `); - table.find("tbody").append(row); + const row = [ + sch['idx'], + frappe.format(sch['schedule_date'], { fieldtype: 'Date' }), + frappe.format(sch['depreciation_amount'], { fieldtype: 'Currency' }), + frappe.format(sch['accumulated_depreciation_amount'], { fieldtype: 'Currency' }), + sch['journal_entry'] || '' + ]; + data.push(row); }); - wrapper.append(table); + let datatable = new frappe.DataTable(wrapper.get(0), { + columns: [ + {name: __("No."), editable: false, resizable: false, format: value => value, width: 60}, + {name: __("Schedule Date"), editable: false, resizable: false, width: 270}, + {name: __("Depreciation Amount"), editable: false, resizable: false, width: 164}, + {name: __("Accumulated Depreciation Amount"), editable: false, resizable: false, width: 164}, + {name: __("Journal Entry"), editable: false, resizable: false, format: value => `${value}`, width: 312} + ], + data: data, + serialNoColumn: false, + checkboxColumn: true, + cellHeight: 35 + }); + datatable.style.setStyle(`.dt-scrollable`, {'font-size': '0.75rem', 'margin-bottom': '1rem'}); + datatable.style.setStyle(`.dt-cell--col-1`, {'text-align': 'center'}); + datatable.style.setStyle(`.dt-cell--col-2`, {'font-weight': 600}); + datatable.style.setStyle(`.dt-cell--col-3`, {'font-weight': 600}); }, setup_chart_and_depr_schedule_view: async function(frm) { From cd98be6088acc0a9d6170c88da7bdf440306f425 Mon Sep 17 00:00:00 2001 From: Gursheen Anand Date: Tue, 1 Aug 2023 23:22:49 +0530 Subject: [PATCH 093/101] fix: check root type only when not none --- .../consolidated_financial_statement.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/erpnext/accounts/report/consolidated_financial_statement/consolidated_financial_statement.py b/erpnext/accounts/report/consolidated_financial_statement/consolidated_financial_statement.py index f1e665a68f..080e45a798 100644 --- a/erpnext/accounts/report/consolidated_financial_statement/consolidated_financial_statement.py +++ b/erpnext/accounts/report/consolidated_financial_statement/consolidated_financial_statement.py @@ -654,11 +654,12 @@ def set_gl_entries_by_account( & (gle.posting_date <= to_date) & (account.lft >= root_lft) & (account.rgt <= root_rgt) - & (account.root_type == root_type) ) .orderby(gle.account, gle.posting_date) ) + if root_type: + query = query.where(account.root_type == root_type) additional_conditions = get_additional_conditions(from_date, ignore_closing_entries, filters, d) if additional_conditions: query = query.where(Criterion.all(additional_conditions)) From 002bf77314a71c02ad164e328a3a9cc9ec9714e4 Mon Sep 17 00:00:00 2001 From: Gursheen Anand Date: Tue, 1 Aug 2023 23:24:18 +0530 Subject: [PATCH 094/101] test: balance sheet report --- .../balance_sheet/test_balance_sheet.py | 51 +++++++++++++++++++ 1 file changed, 51 insertions(+) create mode 100644 erpnext/accounts/report/balance_sheet/test_balance_sheet.py diff --git a/erpnext/accounts/report/balance_sheet/test_balance_sheet.py b/erpnext/accounts/report/balance_sheet/test_balance_sheet.py new file mode 100644 index 0000000000..3cb6efebee --- /dev/null +++ b/erpnext/accounts/report/balance_sheet/test_balance_sheet.py @@ -0,0 +1,51 @@ +# Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and Contributors +# MIT License. See license.txt + +import frappe +from frappe.tests.utils import FrappeTestCase +from frappe.utils import today + +from erpnext.accounts.report.balance_sheet.balance_sheet import execute + + +class TestBalanceSheet(FrappeTestCase): + def test_balance_sheet(self): + from erpnext.accounts.doctype.purchase_invoice.test_purchase_invoice import make_purchase_invoice + from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import ( + create_sales_invoice, + make_sales_invoice, + ) + from erpnext.accounts.utils import get_fiscal_year + + frappe.db.sql("delete from `tabPurchase Invoice` where company='_Test Company 6'") + frappe.db.sql("delete from `tabSales Invoice` where company='_Test Company 6'") + frappe.db.sql("delete from `tabGL Entry` where company='_Test Company 6'") + + pi = make_purchase_invoice( + company="_Test Company 6", + warehouse="Finished Goods - _TC6", + expense_account="Cost of Goods Sold - _TC6", + cost_center="Main - _TC6", + qty=10, + rate=100, + ) + si = create_sales_invoice( + company="_Test Company 6", + debit_to="Debtors - _TC6", + income_account="Sales - _TC6", + cost_center="Main - _TC6", + qty=5, + rate=110, + ) + filters = frappe._dict( + company="_Test Company 6", + period_start_date=today(), + period_end_date=today(), + periodicity="Yearly", + ) + result = execute(filters)[1] + for account_dict in result: + if account_dict.get("account") == "Current Liabilities - _TC6": + self.assertEqual(account_dict.total, 1000) + if account_dict.get("account") == "Current Assets - _TC6": + self.assertEqual(account_dict.total, 550) From dedf24b86db824f84dd48cf2b470272aa90ab636 Mon Sep 17 00:00:00 2001 From: Devin Slauenwhite Date: Wed, 2 Aug 2023 06:56:55 -0400 Subject: [PATCH 095/101] fix: don't allow negative rates (#36027) * fix: don't allow negative rate * test: don't allow negative rate * fix: only check for -rate on items child table --- .../accounts/doctype/sales_invoice/test_sales_invoice.py | 7 +++++++ erpnext/controllers/status_updater.py | 3 +++ 2 files changed, 10 insertions(+) diff --git a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py index 41e55546a8..e8445aa82a 100644 --- a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py +++ b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py @@ -3371,6 +3371,13 @@ class TestSalesInvoice(unittest.TestCase): set_advance_flag(company="_Test Company", flag=0, default_account="") + def test_sales_return_negative_rate(self): + si = create_sales_invoice(is_return=1, qty=-2, rate=-10, do_not_save=True) + self.assertRaises(frappe.ValidationError, si.save) + + si.items[0].rate = 10 + si.save() + def set_advance_flag(company, flag, default_account): frappe.db.set_value( diff --git a/erpnext/controllers/status_updater.py b/erpnext/controllers/status_updater.py index 58cab147a4..a4bc4a9c69 100644 --- a/erpnext/controllers/status_updater.py +++ b/erpnext/controllers/status_updater.py @@ -233,6 +233,9 @@ class StatusUpdater(Document): if hasattr(d, "qty") and d.qty > 0 and self.get("is_return"): frappe.throw(_("For an item {0}, quantity must be negative number").format(d.item_code)) + if hasattr(d, "item_code") and hasattr(d, "rate") and d.rate < 0: + frappe.throw(_("For an item {0}, rate must be a positive number").format(d.item_code)) + if d.doctype == args["source_dt"] and d.get(args["join_field"]): args["name"] = d.get(args["join_field"]) From 27ebf14f9d516ec555ed702a2edd78c6e65d517f Mon Sep 17 00:00:00 2001 From: Husam Hammad <85282854+husamhammad@users.noreply.github.com> Date: Wed, 2 Aug 2023 13:58:05 +0300 Subject: [PATCH 096/101] fix: handle None value in payment_term_outstanding * Fix payment entry bug: Handle None value in payment_term_outstanding * fix: Handle None value in payment_term_outstanding V2 fix linting issue --- erpnext/accounts/doctype/payment_entry/payment_entry.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/erpnext/accounts/doctype/payment_entry/payment_entry.py b/erpnext/accounts/doctype/payment_entry/payment_entry.py index 29b52729cd..c3018cdfd4 100644 --- a/erpnext/accounts/doctype/payment_entry/payment_entry.py +++ b/erpnext/accounts/doctype/payment_entry/payment_entry.py @@ -281,7 +281,8 @@ class PaymentEntry(AccountsController): d.payment_term and ( (flt(d.allocated_amount)) > 0 - and flt(d.allocated_amount) > flt(latest.payment_term_outstanding) + and latest.payment_term_outstanding + and (flt(d.allocated_amount) > flt(latest.payment_term_outstanding)) ) and self.term_based_allocation_enabled_for_reference(d.reference_doctype, d.reference_name) ): From 38a612c62e2943a4a84f678bfd2804671e966b46 Mon Sep 17 00:00:00 2001 From: Anand Baburajan Date: Thu, 3 Aug 2023 16:37:05 +0530 Subject: [PATCH 097/101] chore: better cost center validation for assets (#36477) --- erpnext/assets/doctype/asset/asset.py | 36 +++++++++++++++++++-------- 1 file changed, 26 insertions(+), 10 deletions(-) diff --git a/erpnext/assets/doctype/asset/asset.py b/erpnext/assets/doctype/asset/asset.py index 252a3dd63f..04ec7be3cd 100644 --- a/erpnext/assets/doctype/asset/asset.py +++ b/erpnext/assets/doctype/asset/asset.py @@ -148,17 +148,33 @@ class Asset(AccountsController): frappe.throw(_("Item {0} must be a non-stock item").format(self.item_code)) def validate_cost_center(self): - if not self.cost_center: - return - - cost_center_company = frappe.db.get_value("Cost Center", self.cost_center, "company") - if cost_center_company != self.company: - frappe.throw( - _("Selected Cost Center {} doesn't belongs to {}").format( - frappe.bold(self.cost_center), frappe.bold(self.company) - ), - title=_("Invalid Cost Center"), + if self.cost_center: + cost_center_company, cost_center_is_group = frappe.db.get_value( + "Cost Center", self.cost_center, ["company", "is_group"] ) + if cost_center_company != self.company: + frappe.throw( + _("Cost Center {} doesn't belong to Company {}").format( + frappe.bold(self.cost_center), frappe.bold(self.company) + ), + title=_("Invalid Cost Center"), + ) + if cost_center_is_group: + frappe.throw( + _( + "Cost Center {} is a group cost center and group cost centers cannot be used in transactions" + ).format(frappe.bold(self.cost_center)), + title=_("Invalid Cost Center"), + ) + + else: + if not frappe.get_cached_value("Company", self.company, "depreciation_cost_center"): + frappe.throw( + _( + "Please set a Cost Center for the Asset or set an Asset Depreciation Cost Center for the Company {}" + ).format(frappe.bold(self.company)), + title=_("Missing Cost Center"), + ) def validate_in_use_date(self): if not self.available_for_use_date: From 49be7407369f33419513475f00b1ca8da9efea17 Mon Sep 17 00:00:00 2001 From: Sumit Jain <59503001+sumitjain236@users.noreply.github.com> Date: Fri, 4 Aug 2023 17:45:16 +0530 Subject: [PATCH 098/101] fix: Contact Doctype doesn't have any field called `job_title` fix: Contact Doctype doesn't have any field called `job_title` --- erpnext/crm/doctype/lead/lead.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/crm/doctype/lead/lead.py b/erpnext/crm/doctype/lead/lead.py index a98886c648..105c58d110 100644 --- a/erpnext/crm/doctype/lead/lead.py +++ b/erpnext/crm/doctype/lead/lead.py @@ -182,7 +182,7 @@ class Lead(SellingController, CRMNote): "last_name": self.last_name, "salutation": self.salutation, "gender": self.gender, - "job_title": self.job_title, + "designation": self.job_title, "company_name": self.company_name, } ) From edbefee10ca779f1d81153110c6085dd04d9c769 Mon Sep 17 00:00:00 2001 From: Gursheen Kaur Anand <40693548+GursheenK@users.noreply.github.com> Date: Fri, 4 Aug 2023 17:49:17 +0530 Subject: [PATCH 099/101] fix: payment allocation in invoice payment schedule (#36440) * fix: payment allocation in invoice payment schedule * test: payment allocation for payment terms * chore: linting issues --- .../purchase_invoice/test_purchase_invoice.py | 46 +++++++++++++++++++ erpnext/controllers/accounts_controller.py | 5 ++ 2 files changed, 51 insertions(+) diff --git a/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py b/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py index 55d0203c7a..ce7ada3b09 100644 --- a/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py +++ b/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py @@ -1791,6 +1791,52 @@ class TestPurchaseInvoice(unittest.TestCase, StockTestMixin): rate = flt(sle.stock_value_difference) / flt(sle.actual_qty) self.assertAlmostEqual(returned_inv.items[0].rate, rate) + def test_payment_allocation_for_payment_terms(self): + from erpnext.buying.doctype.purchase_order.test_purchase_order import ( + create_pr_against_po, + create_purchase_order, + ) + from erpnext.selling.doctype.sales_order.test_sales_order import ( + automatically_fetch_payment_terms, + ) + from erpnext.stock.doctype.purchase_receipt.purchase_receipt import ( + make_purchase_invoice as make_pi_from_pr, + ) + + automatically_fetch_payment_terms() + frappe.db.set_value( + "Payment Terms Template", + "_Test Payment Term Template", + "allocate_payment_based_on_payment_terms", + 0, + ) + + po = create_purchase_order(do_not_save=1) + po.payment_terms_template = "_Test Payment Term Template" + po.save() + po.submit() + + pr = create_pr_against_po(po.name, received_qty=4) + pi = make_pi_from_pr(pr.name) + self.assertEqual(pi.payment_schedule[0].payment_amount, 1000) + + frappe.db.set_value( + "Payment Terms Template", + "_Test Payment Term Template", + "allocate_payment_based_on_payment_terms", + 1, + ) + pi = make_pi_from_pr(pr.name) + self.assertEqual(pi.payment_schedule[0].payment_amount, 2500) + + automatically_fetch_payment_terms(enable=0) + frappe.db.set_value( + "Payment Terms Template", + "_Test Payment Term Template", + "allocate_payment_based_on_payment_terms", + 0, + ) + def test_offsetting_entries_for_accounting_dimensions(self): from erpnext.accounts.doctype.account.test_account import create_account from erpnext.accounts.report.trial_balance.test_trial_balance import ( diff --git a/erpnext/controllers/accounts_controller.py b/erpnext/controllers/accounts_controller.py index 37a18d80e9..b2cfc39be9 100644 --- a/erpnext/controllers/accounts_controller.py +++ b/erpnext/controllers/accounts_controller.py @@ -1756,8 +1756,13 @@ class AccountsController(TransactionBase): ) self.append("payment_schedule", data) + allocate_payment_based_on_payment_terms = frappe.db.get_value( + "Payment Terms Template", self.payment_terms_template, "allocate_payment_based_on_payment_terms" + ) + if not ( automatically_fetch_payment_terms + and allocate_payment_based_on_payment_terms and self.linked_order_has_payment_terms(po_or_so, fieldname, doctype) ): for d in self.get("payment_schedule"): From b86afb2964d404322ce4541f1e60d650be37a1a0 Mon Sep 17 00:00:00 2001 From: RitvikSardana <65544983+RitvikSardana@users.noreply.github.com> Date: Fri, 4 Aug 2023 22:05:30 +0530 Subject: [PATCH 100/101] feat: Financial Ratio Report (#36130) * feat: Financial Ratio report added * fix: Made columns dynamic * fix: Changed fieldtype of year column * fix: Added Financial Ratios for all Fiscal Years * fix: Added Validation of only Parent Having account_type of Direct Income, Indirect Income, Current Asset and Current Liability * fix: Added 4 more ratios * fix: added a function for repeated code * fix: added account_type in accounts utils and cleaned report code * fix: created function for avg_ratio_values * fix: cleaning code * fix: basic ratios completed * fix: cleaned the code * chore: code cleanup * chore: remove comments * chore: code cleanup * chore: cleanup account query * chore: Remove unused variables --------- Co-authored-by: Ritvik Sardana Co-authored-by: Deepesh Garg --- erpnext/accounts/doctype/account/account.js | 185 ++++++----- erpnext/accounts/doctype/account/account.json | 5 +- erpnext/accounts/doctype/account/account.py | 15 + .../report/balance_sheet/balance_sheet.js | 26 +- .../report/financial_ratios/__init__.py | 0 .../financial_ratios/financial_ratios.js | 72 +++++ .../financial_ratios/financial_ratios.json | 37 +++ .../financial_ratios/financial_ratios.py | 296 ++++++++++++++++++ .../profit_and_loss_statement.js | 27 +- erpnext/accounts/utils.py | 20 +- 10 files changed, 575 insertions(+), 108 deletions(-) create mode 100644 erpnext/accounts/report/financial_ratios/__init__.py create mode 100644 erpnext/accounts/report/financial_ratios/financial_ratios.js create mode 100644 erpnext/accounts/report/financial_ratios/financial_ratios.json create mode 100644 erpnext/accounts/report/financial_ratios/financial_ratios.py diff --git a/erpnext/accounts/doctype/account/account.js b/erpnext/accounts/doctype/account/account.js index f033b54dd0..3c0eb85701 100644 --- a/erpnext/accounts/doctype/account/account.js +++ b/erpnext/accounts/doctype/account/account.js @@ -1,67 +1,83 @@ // Copyright (c) 2017, Frappe Technologies Pvt. Ltd. and Contributors // License: GNU General Public License v3. See license.txt -frappe.ui.form.on('Account', { - setup: function(frm) { - frm.add_fetch('parent_account', 'report_type', 'report_type'); - frm.add_fetch('parent_account', 'root_type', 'root_type'); +frappe.ui.form.on("Account", { + setup: function (frm) { + frm.add_fetch("parent_account", "report_type", "report_type"); + frm.add_fetch("parent_account", "root_type", "root_type"); }, - onload: function(frm) { - frm.set_query('parent_account', function(doc) { + onload: function (frm) { + frm.set_query("parent_account", function (doc) { return { filters: { - "is_group": 1, - "company": doc.company - } + is_group: 1, + company: doc.company, + }, }; }); }, - refresh: function(frm) { - frm.toggle_display('account_name', frm.is_new()); + refresh: function (frm) { + frm.toggle_display("account_name", frm.is_new()); // hide fields if group - frm.toggle_display(['account_type', 'tax_rate'], cint(frm.doc.is_group) == 0); + frm.toggle_display(["tax_rate"], cint(frm.doc.is_group) == 0); // disable fields - frm.toggle_enable(['is_group', 'company'], false); + frm.toggle_enable(["is_group", "company"], false); if (cint(frm.doc.is_group) == 0) { - frm.toggle_display('freeze_account', frm.doc.__onload - && frm.doc.__onload.can_freeze_account); + frm.toggle_display( + "freeze_account", + frm.doc.__onload && frm.doc.__onload.can_freeze_account + ); } // read-only for root accounts if (!frm.is_new()) { if (!frm.doc.parent_account) { frm.set_read_only(); - frm.set_intro(__("This is a root account and cannot be edited.")); + frm.set_intro( + __("This is a root account and cannot be edited.") + ); } else { // credit days and type if customer or supplier frm.set_intro(null); - frm.trigger('account_type'); + frm.trigger("account_type"); // show / hide convert buttons - frm.trigger('add_toolbar_buttons'); + frm.trigger("add_toolbar_buttons"); } - if (frm.has_perm('write')) { - frm.add_custom_button(__('Merge Account'), function () { - frm.trigger("merge_account"); - }, __('Actions')); - frm.add_custom_button(__('Update Account Name / Number'), function () { - frm.trigger("update_account_number"); - }, __('Actions')); + if (frm.has_perm("write")) { + frm.add_custom_button( + __("Merge Account"), + function () { + frm.trigger("merge_account"); + }, + __("Actions") + ); + frm.add_custom_button( + __("Update Account Name / Number"), + function () { + frm.trigger("update_account_number"); + }, + __("Actions") + ); } } }, account_type: function (frm) { if (frm.doc.is_group == 0) { - frm.toggle_display(['tax_rate'], frm.doc.account_type == 'Tax'); - frm.toggle_display('warehouse', frm.doc.account_type == 'Stock'); + frm.toggle_display(["tax_rate"], frm.doc.account_type == "Tax"); + frm.toggle_display("warehouse", frm.doc.account_type == "Stock"); } }, - add_toolbar_buttons: function(frm) { - frm.add_custom_button(__('Chart of Accounts'), () => { - frappe.set_route("Tree", "Account"); - }, __('View')); + add_toolbar_buttons: function (frm) { + frm.add_custom_button( + __("Chart of Accounts"), + () => { + frappe.set_route("Tree", "Account"); + }, + __("View") + ); if (frm.doc.is_group == 1) { frm.add_custom_button(__('Convert to Non-Group'), function () { @@ -86,31 +102,35 @@ frappe.ui.form.on('Account', { frappe.set_route("query-report", "General Ledger"); }, __('View')); - frm.add_custom_button(__('Convert to Group'), function () { - return frappe.call({ - doc: frm.doc, - method: 'convert_ledger_to_group', - callback: function() { - frm.refresh(); - } - }); - }, __('Actions')); + frm.add_custom_button( + __("Convert to Group"), + function () { + return frappe.call({ + doc: frm.doc, + method: "convert_ledger_to_group", + callback: function () { + frm.refresh(); + }, + }); + }, + __("Actions") + ); } }, - merge_account: function(frm) { + merge_account: function (frm) { var d = new frappe.ui.Dialog({ - title: __('Merge with Existing Account'), + title: __("Merge with Existing Account"), fields: [ { - "label" : "Name", - "fieldname": "name", - "fieldtype": "Data", - "reqd": 1, - "default": frm.doc.name - } + label: "Name", + fieldname: "name", + fieldtype: "Data", + reqd: 1, + default: frm.doc.name, + }, ], - primary_action: function() { + primary_action: function () { var data = d.get_values(); frappe.call({ method: "erpnext.accounts.doctype.account.account.merge_account", @@ -119,44 +139,47 @@ frappe.ui.form.on('Account', { new: data.name, is_group: frm.doc.is_group, root_type: frm.doc.root_type, - company: frm.doc.company + company: frm.doc.company, }, - callback: function(r) { - if(!r.exc) { - if(r.message) { + callback: function (r) { + if (!r.exc) { + if (r.message) { frappe.set_route("Form", "Account", r.message); } d.hide(); } - } + }, }); }, - primary_action_label: __('Merge') + primary_action_label: __("Merge"), }); d.show(); }, - update_account_number: function(frm) { + update_account_number: function (frm) { var d = new frappe.ui.Dialog({ - title: __('Update Account Number / Name'), + title: __("Update Account Number / Name"), fields: [ { - "label": "Account Name", - "fieldname": "account_name", - "fieldtype": "Data", - "reqd": 1, - "default": frm.doc.account_name + label: "Account Name", + fieldname: "account_name", + fieldtype: "Data", + reqd: 1, + default: frm.doc.account_name, }, { - "label": "Account Number", - "fieldname": "account_number", - "fieldtype": "Data", - "default": frm.doc.account_number - } + label: "Account Number", + fieldname: "account_number", + fieldtype: "Data", + default: frm.doc.account_number, + }, ], - primary_action: function() { + primary_action: function () { var data = d.get_values(); - if(data.account_number === frm.doc.account_number && data.account_name === frm.doc.account_name) { + if ( + data.account_number === frm.doc.account_number && + data.account_name === frm.doc.account_name + ) { d.hide(); return; } @@ -166,23 +189,29 @@ frappe.ui.form.on('Account', { args: { account_number: data.account_number, account_name: data.account_name, - name: frm.doc.name + name: frm.doc.name, }, - callback: function(r) { - if(!r.exc) { - if(r.message) { + callback: function (r) { + if (!r.exc) { + if (r.message) { frappe.set_route("Form", "Account", r.message); } else { - frm.set_value("account_number", data.account_number); - frm.set_value("account_name", data.account_name); + frm.set_value( + "account_number", + data.account_number + ); + frm.set_value( + "account_name", + data.account_name + ); } d.hide(); } - } + }, }); }, - primary_action_label: __('Update') + primary_action_label: __("Update"), }); d.show(); - } + }, }); diff --git a/erpnext/accounts/doctype/account/account.json b/erpnext/accounts/doctype/account/account.json index e79fb66062..78f73efff1 100644 --- a/erpnext/accounts/doctype/account/account.json +++ b/erpnext/accounts/doctype/account/account.json @@ -123,7 +123,7 @@ "label": "Account Type", "oldfieldname": "account_type", "oldfieldtype": "Select", - "options": "\nAccumulated Depreciation\nAsset Received But Not Billed\nBank\nCash\nChargeable\nCapital Work in Progress\nCost of Goods Sold\nDepreciation\nEquity\nExpense Account\nExpenses Included In Asset Valuation\nExpenses Included In Valuation\nFixed Asset\nIncome Account\nPayable\nReceivable\nRound Off\nStock\nStock Adjustment\nStock Received But Not Billed\nService Received But Not Billed\nTax\nTemporary" + "options": "\nAccumulated Depreciation\nAsset Received But Not Billed\nBank\nCash\nChargeable\nCapital Work in Progress\nCost of Goods Sold\nCurrent Asset\nCurrent Liability\nDepreciation\nDirect Expense\nDirect Income\nEquity\nExpense Account\nExpenses Included In Asset Valuation\nExpenses Included In Valuation\nFixed Asset\nIncome Account\nIndirect Expense\nIndirect Income\nLiability\nPayable\nReceivable\nRound Off\nStock\nStock Adjustment\nStock Received But Not Billed\nService Received But Not Billed\nTax\nTemporary" }, { "description": "Rate at which this tax is applied", @@ -192,7 +192,7 @@ "idx": 1, "is_tree": 1, "links": [], - "modified": "2023-04-11 16:08:46.983677", + "modified": "2023-07-20 18:18:44.405723", "modified_by": "Administrator", "module": "Accounts", "name": "Account", @@ -243,7 +243,6 @@ "read": 1, "report": 1, "role": "Accounts Manager", - "set_user_permissions": 1, "share": 1, "write": 1 } diff --git a/erpnext/accounts/doctype/account/account.py b/erpnext/accounts/doctype/account/account.py index e94b7cf4c2..c1eca721b6 100644 --- a/erpnext/accounts/doctype/account/account.py +++ b/erpnext/accounts/doctype/account/account.py @@ -45,6 +45,7 @@ class Account(NestedSet): if frappe.local.flags.allow_unverified_charts: return self.validate_parent() + self.validate_parent_child_account_type() self.validate_root_details() validate_field_number("Account", self.name, self.account_number, self.company, "account_number") self.validate_group_or_ledger() @@ -55,6 +56,20 @@ class Account(NestedSet): self.validate_account_currency() self.validate_root_company_and_sync_account_to_children() + def validate_parent_child_account_type(self): + if self.parent_account: + if self.account_type in [ + "Direct Income", + "Indirect Income", + "Current Asset", + "Current Liability", + "Direct Expense", + "Indirect Expense", + ]: + parent_account_type = frappe.db.get_value("Account", self.parent_account, ["account_type"]) + if parent_account_type == self.account_type: + throw(_("Only Parent can be of type {0}").format(self.account_type)) + def validate_parent(self): """Fetch Parent Details and validate parent account""" if self.parent_account: diff --git a/erpnext/accounts/report/balance_sheet/balance_sheet.js b/erpnext/accounts/report/balance_sheet/balance_sheet.js index 4a4ad4d71c..c65b9e8ccc 100644 --- a/erpnext/accounts/report/balance_sheet/balance_sheet.js +++ b/erpnext/accounts/report/balance_sheet/balance_sheet.js @@ -1,22 +1,26 @@ // Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors // License: GNU General Public License v3. See license.txt -frappe.require("assets/erpnext/js/financial_statements.js", function() { - frappe.query_reports["Balance Sheet"] = $.extend({}, erpnext.financial_statements); +frappe.require("assets/erpnext/js/financial_statements.js", function () { + frappe.query_reports["Balance Sheet"] = $.extend( + {}, + erpnext.financial_statements + ); - erpnext.utils.add_dimensions('Balance Sheet', 10); + erpnext.utils.add_dimensions("Balance Sheet", 10); frappe.query_reports["Balance Sheet"]["filters"].push({ - "fieldname": "accumulated_values", - "label": __("Accumulated Values"), - "fieldtype": "Check", - "default": 1 + fieldname: "accumulated_values", + label: __("Accumulated Values"), + fieldtype: "Check", + default: 1, }); + console.log(frappe.query_reports["Balance Sheet"]["filters"]); frappe.query_reports["Balance Sheet"]["filters"].push({ - "fieldname": "include_default_book_entries", - "label": __("Include Default Book Entries"), - "fieldtype": "Check", - "default": 1 + fieldname: "include_default_book_entries", + label: __("Include Default Book Entries"), + fieldtype: "Check", + default: 1, }); }); diff --git a/erpnext/accounts/report/financial_ratios/__init__.py b/erpnext/accounts/report/financial_ratios/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/erpnext/accounts/report/financial_ratios/financial_ratios.js b/erpnext/accounts/report/financial_ratios/financial_ratios.js new file mode 100644 index 0000000000..643423d865 --- /dev/null +++ b/erpnext/accounts/report/financial_ratios/financial_ratios.js @@ -0,0 +1,72 @@ +// Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and contributors +// For license information, please see license.txt +/* eslint-disable */ + +frappe.query_reports["Financial Ratios"] = { + filters: [ + { + fieldname: "company", + label: __("Company"), + fieldtype: "Link", + options: "Company", + default: frappe.defaults.get_user_default("Company"), + reqd: 1, + }, + { + fieldname: "from_fiscal_year", + label: __("Start Year"), + fieldtype: "Link", + options: "Fiscal Year", + default: erpnext.utils.get_fiscal_year(frappe.datetime.get_today()), + reqd: 1, + }, + { + fieldname: "to_fiscal_year", + label: __("End Year"), + fieldtype: "Link", + options: "Fiscal Year", + default: erpnext.utils.get_fiscal_year(frappe.datetime.get_today()), + reqd: 1, + }, + { + fieldname: "periodicity", + label: __("Periodicity"), + fieldtype: "Data", + default: "Yearly", + reqd: 1, + hidden: 1, + }, + { + fieldname: "period_start_date", + label: __("From Date"), + fieldtype: "Date", + default: erpnext.utils.get_fiscal_year(frappe.datetime.get_today(), true)[1], + hidden: 1, + }, + { + fieldname: "period_end_date", + label: __("To Date"), + fieldtype: "Date", + default: erpnext.utils.get_fiscal_year(frappe.datetime.get_today(), true)[2], + hidden: 1, + }, + ], + "formatter": function(value, row, column, data, default_formatter) { + + let heading_ratios = ["Liquidity Ratios", "Solvency Ratios","Turnover Ratios"] + + if (heading_ratios.includes(value)) { + value = $(`${value}`); + let $value = $(value).css("font-weight", "bold"); + value = $value.wrap("

").parent().html(); + } + + if (heading_ratios.includes(row[1].content) && column.fieldtype == "Float") { + column.fieldtype = "Data"; + } + + value = default_formatter(value, row, column, data); + + return value; + }, +}; diff --git a/erpnext/accounts/report/financial_ratios/financial_ratios.json b/erpnext/accounts/report/financial_ratios/financial_ratios.json new file mode 100644 index 0000000000..1a2e56bad1 --- /dev/null +++ b/erpnext/accounts/report/financial_ratios/financial_ratios.json @@ -0,0 +1,37 @@ +{ + "add_total_row": 0, + "columns": [], + "creation": "2023-07-13 16:11:11.925096", + "disabled": 0, + "docstatus": 0, + "doctype": "Report", + "filters": [], + "idx": 0, + "is_standard": "Yes", + "modified": "2023-07-13 16:11:11.925096", + "modified_by": "Administrator", + "module": "Accounts", + "name": "Financial Ratios", + "owner": "Administrator", + "prepared_report": 0, + "ref_doctype": "Account", + "report_name": "Financial Ratios", + "report_type": "Script Report", + "roles": [ + { + "role": "Accounts User" + }, + { + "role": "Auditor" + }, + { + "role": "Sales User" + }, + { + "role": "Purchase User" + }, + { + "role": "Accounts Manager" + } + ] +} \ No newline at end of file diff --git a/erpnext/accounts/report/financial_ratios/financial_ratios.py b/erpnext/accounts/report/financial_ratios/financial_ratios.py new file mode 100644 index 0000000000..57421ebcb0 --- /dev/null +++ b/erpnext/accounts/report/financial_ratios/financial_ratios.py @@ -0,0 +1,296 @@ +# Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and contributors +# For license information, please see license.txt + +import frappe +from frappe import _ +from frappe.utils import add_days, flt + +from erpnext.accounts.report.financial_statements import get_data, get_period_list +from erpnext.accounts.utils import get_balance_on, get_fiscal_year + + +def execute(filters=None): + filters["filter_based_on"] = "Fiscal Year" + columns, data = [], [] + + setup_filters(filters) + + period_list = get_period_list( + filters.from_fiscal_year, + filters.to_fiscal_year, + filters.period_start_date, + filters.period_end_date, + filters.filter_based_on, + filters.periodicity, + company=filters.company, + ) + + columns, years = get_columns(period_list) + data = get_ratios_data(filters, period_list, years) + + return columns, data + + +def setup_filters(filters): + if not filters.get("period_start_date"): + period_start_date = get_fiscal_year(fiscal_year=filters.from_fiscal_year)[1] + filters["period_start_date"] = period_start_date + + if not filters.get("period_end_date"): + period_end_date = get_fiscal_year(fiscal_year=filters.to_fiscal_year)[2] + filters["period_end_date"] = period_end_date + + +def get_columns(period_list): + years = [] + columns = [ + { + "label": _("Ratios"), + "fieldname": "ratio", + "fieldtype": "Data", + "width": 200, + }, + ] + + for period in period_list: + columns.append( + { + "fieldname": period.key, + "label": period.label, + "fieldtype": "Float", + "width": 150, + } + ) + years.append(period.key) + + return columns, years + + +def get_ratios_data(filters, period_list, years): + + data = [] + assets, liabilities, income, expense = get_gl_data(filters, period_list, years) + + current_asset, total_asset = {}, {} + current_liability, total_liability = {}, {} + net_sales, total_income = {}, {} + cogs, total_expense = {}, {} + quick_asset = {} + direct_expense = {} + + for year in years: + total_quick_asset = 0 + total_net_sales = 0 + total_cogs = 0 + + for d in [ + [ + current_asset, + total_asset, + "Current Asset", + year, + assets, + "Asset", + quick_asset, + total_quick_asset, + ], + [ + current_liability, + total_liability, + "Current Liability", + year, + liabilities, + "Liability", + {}, + 0, + ], + [cogs, total_expense, "Cost of Goods Sold", year, expense, "Expense", {}, total_cogs], + [direct_expense, direct_expense, "Direct Expense", year, expense, "Expense", {}, 0], + [net_sales, total_income, "Direct Income", year, income, "Income", {}, total_net_sales], + ]: + update_balances(d[0], d[1], d[2], d[3], d[4], d[5], d[6], d[7]) + add_liquidity_ratios(data, years, current_asset, current_liability, quick_asset) + add_solvency_ratios( + data, years, total_asset, total_liability, net_sales, cogs, total_income, total_expense + ) + add_turnover_ratios( + data, years, period_list, filters, total_asset, net_sales, cogs, direct_expense + ) + + return data + + +def get_gl_data(filters, period_list, years): + data = {} + + for d in [ + ["Asset", "Debit"], + ["Liability", "Credit"], + ["Income", "Credit"], + ["Expense", "Debit"], + ]: + data[frappe.scrub(d[0])] = get_data( + filters.company, + d[0], + d[1], + period_list, + only_current_fiscal_year=False, + filters=filters, + ) + + assets, liabilities, income, expense = ( + data.get("asset"), + data.get("liability"), + data.get("income"), + data.get("expense"), + ) + + return assets, liabilities, income, expense + + +def add_liquidity_ratios(data, years, current_asset, current_liability, quick_asset): + precision = frappe.db.get_single_value("System Settings", "float_precision") + data.append({"ratio": "Liquidity Ratios"}) + + ratio_data = [["Current Ratio", current_asset], ["Quick Ratio", quick_asset]] + + for d in ratio_data: + row = { + "ratio": d[0], + } + for year in years: + row[year] = calculate_ratio(d[1].get(year, 0), current_liability.get(year, 0), precision) + + data.append(row) + + +def add_solvency_ratios( + data, years, total_asset, total_liability, net_sales, cogs, total_income, total_expense +): + precision = frappe.db.get_single_value("System Settings", "float_precision") + data.append({"ratio": "Solvency Ratios"}) + + debt_equity_ratio = {"ratio": "Debt Equity Ratio"} + gross_profit_ratio = {"ratio": "Gross Profit Ratio"} + net_profit_ratio = {"ratio": "Net Profit Ratio"} + return_on_asset_ratio = {"ratio": "Return on Asset Ratio"} + return_on_equity_ratio = {"ratio": "Return on Equity Ratio"} + + for year in years: + profit_after_tax = total_income[year] + total_expense[year] + share_holder_fund = total_asset[year] - total_liability[year] + + debt_equity_ratio[year] = calculate_ratio( + total_liability.get(year), share_holder_fund, precision + ) + return_on_equity_ratio[year] = calculate_ratio(profit_after_tax, share_holder_fund, precision) + + net_profit_ratio[year] = calculate_ratio(profit_after_tax, net_sales.get(year), precision) + gross_profit_ratio[year] = calculate_ratio( + net_sales.get(year, 0) - cogs.get(year, 0), net_sales.get(year), precision + ) + return_on_asset_ratio[year] = calculate_ratio(profit_after_tax, total_asset.get(year), precision) + + data.append(debt_equity_ratio) + data.append(gross_profit_ratio) + data.append(net_profit_ratio) + data.append(return_on_asset_ratio) + data.append(return_on_equity_ratio) + + +def add_turnover_ratios( + data, years, period_list, filters, total_asset, net_sales, cogs, direct_expense +): + precision = frappe.db.get_single_value("System Settings", "float_precision") + data.append({"ratio": "Turnover Ratios"}) + + avg_data = {} + for d in ["Receivable", "Payable", "Stock"]: + avg_data[frappe.scrub(d)] = avg_ratio_balance("Receivable", period_list, precision, filters) + + avg_debtors, avg_creditors, avg_stock = ( + avg_data.get("receivable"), + avg_data.get("payable"), + avg_data.get("stock"), + ) + + ratio_data = [ + ["Fixed Asset Turnover Ratio", net_sales, total_asset], + ["Debtor Turnover Ratio", net_sales, avg_debtors], + ["Creditor Turnover Ratio", direct_expense, avg_creditors], + ["Inventory Turnover Ratio", cogs, avg_stock], + ] + for ratio in ratio_data: + row = { + "ratio": ratio[0], + } + for year in years: + row[year] = calculate_ratio(ratio[1].get(year, 0), ratio[2].get(year, 0), precision) + + data.append(row) + + +def update_balances( + ratio_dict, + total_dict, + account_type, + year, + root_type_data, + root_type, + net_dict=None, + total_net=0, +): + + for entry in root_type_data: + if not entry.get("parent_account") and entry.get("is_group"): + total_dict[year] = entry[year] + if account_type == "Direct Expense": + total_dict[year] = entry[year] * -1 + + if root_type in ("Asset", "Liability"): + if entry.get("account_type") == account_type and entry.get("is_group"): + ratio_dict[year] = entry.get(year) + if entry.get("account_type") in ["Bank", "Cash", "Receivable"] and not entry.get("is_group"): + total_net += entry.get(year) + net_dict[year] = total_net + + elif root_type == "Income": + if entry.get("account_type") == account_type and entry.get("is_group"): + total_net += entry.get(year) + ratio_dict[year] = total_net + elif root_type == "Expense" and account_type == "Cost of Goods Sold": + if entry.get("account_type") == account_type: + total_net += entry.get(year) + ratio_dict[year] = total_net + else: + if entry.get("account_type") == account_type and entry.get("is_group"): + ratio_dict[year] = entry.get(year) + + +def avg_ratio_balance(account_type, period_list, precision, filters): + avg_ratio = {} + for period in period_list: + opening_date = add_days(period["from_date"], -1) + closing_date = period["to_date"] + + closing_balance = get_balance_on( + date=closing_date, + company=filters.company, + account_type=account_type, + ) + opening_balance = get_balance_on( + date=opening_date, + company=filters.company, + account_type=account_type, + ) + avg_ratio[period["key"]] = flt( + (flt(closing_balance) + flt(opening_balance)) / 2, precision=precision + ) + + return avg_ratio + + +def calculate_ratio(value, denominator, precision): + if flt(denominator): + return flt(flt(value) / denominator, precision) + return 0 diff --git a/erpnext/accounts/report/profit_and_loss_statement/profit_and_loss_statement.js b/erpnext/accounts/report/profit_and_loss_statement/profit_and_loss_statement.js index e794f270c2..9fe93b9772 100644 --- a/erpnext/accounts/report/profit_and_loss_statement/profit_and_loss_statement.js +++ b/erpnext/accounts/report/profit_and_loss_statement/profit_and_loss_statement.js @@ -1,19 +1,18 @@ // Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors // License: GNU General Public License v3. See license.txt - -frappe.require("assets/erpnext/js/financial_statements.js", function() { - frappe.query_reports["Profit and Loss Statement"] = $.extend({}, - erpnext.financial_statements); - - erpnext.utils.add_dimensions('Profit and Loss Statement', 10); - - frappe.query_reports["Profit and Loss Statement"]["filters"].push( - { - "fieldname": "include_default_book_entries", - "label": __("Include Default Book Entries"), - "fieldtype": "Check", - "default": 1 - } +frappe.require("assets/erpnext/js/financial_statements.js", function () { + frappe.query_reports["Profit and Loss Statement"] = $.extend( + {}, + erpnext.financial_statements ); + + erpnext.utils.add_dimensions("Profit and Loss Statement", 10); + + frappe.query_reports["Profit and Loss Statement"]["filters"].push({ + fieldname: "accumulated_values", + label: __("Accumulated Values"), + fieldtype: "Check", + default: 1, + }); }); diff --git a/erpnext/accounts/utils.py b/erpnext/accounts/utils.py index 961f41ccef..c24442e2c3 100644 --- a/erpnext/accounts/utils.py +++ b/erpnext/accounts/utils.py @@ -179,6 +179,7 @@ def get_balance_on( in_account_currency=True, cost_center=None, ignore_account_permission=False, + account_type=None, ): if not account and frappe.form_dict.get("account"): account = frappe.form_dict.get("account") @@ -254,6 +255,21 @@ def get_balance_on( else: cond.append("""gle.account = %s """ % (frappe.db.escape(account, percent=False),)) + if account_type: + accounts = frappe.db.get_all( + "Account", + filters={"company": company, "account_type": account_type, "is_group": 0}, + pluck="name", + order_by="lft", + ) + + cond.append( + """ + gle.account in (%s) + """ + % (", ".join([frappe.db.escape(account) for account in accounts])) + ) + if party_type and party: cond.append( """gle.party_type = %s and gle.party = %s """ @@ -263,7 +279,8 @@ def get_balance_on( if company: cond.append("""gle.company = %s """ % (frappe.db.escape(company, percent=False))) - if account or (party_type and party): + if account or (party_type and party) or account_type: + if in_account_currency: select_field = "sum(debit_in_account_currency) - sum(credit_in_account_currency)" else: @@ -276,7 +293,6 @@ def get_balance_on( select_field, " and ".join(cond) ) )[0][0] - # if bal is None, return 0 return flt(bal) From b65ee6c2db5c495bcadece2538915002312d83f6 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Sat, 5 Aug 2023 10:57:23 +0530 Subject: [PATCH 101/101] fix: cross connect delivery note and sales invoice (backport #36183) (#36457) fix: cross connect delivery note and sales invoice (#36183) * fix: cross connect delivery note and sales invoice * chore: remove unnecessary non_standard_fieldname (cherry picked from commit 8501a1182ae8323d91438da30ddc8d93cf8c2789) Co-authored-by: Anand Baburajan --- .../accounts/doctype/sales_invoice/sales_invoice_dashboard.py | 1 + erpnext/stock/doctype/delivery_note/delivery_note_dashboard.py | 1 + 2 files changed, 2 insertions(+) diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice_dashboard.py b/erpnext/accounts/doctype/sales_invoice/sales_invoice_dashboard.py index 0a765f3f46..6fdcf263a5 100644 --- a/erpnext/accounts/doctype/sales_invoice/sales_invoice_dashboard.py +++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice_dashboard.py @@ -15,6 +15,7 @@ def get_data(): }, "internal_links": { "Sales Order": ["items", "sales_order"], + "Delivery Note": ["items", "delivery_note"], "Timesheet": ["timesheets", "time_sheet"], }, "transactions": [ diff --git a/erpnext/stock/doctype/delivery_note/delivery_note_dashboard.py b/erpnext/stock/doctype/delivery_note/delivery_note_dashboard.py index b6b5ff4296..e66c23324d 100644 --- a/erpnext/stock/doctype/delivery_note/delivery_note_dashboard.py +++ b/erpnext/stock/doctype/delivery_note/delivery_note_dashboard.py @@ -11,6 +11,7 @@ def get_data(): }, "internal_links": { "Sales Order": ["items", "against_sales_order"], + "Sales Invoice": ["items", "against_sales_invoice"], "Material Request": ["items", "material_request"], "Purchase Order": ["items", "purchase_order"], },