From 9b24940059c5a762cdc60af638d479f2bec346d5 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Wed, 21 Dec 2022 12:11:49 +0530 Subject: [PATCH 01/30] fix: typerror on multi warehouse in Packed Items DN(with bundled item with varying warehouses)-> Sales Invoice. (cherry picked from commit e684eb32d0cf62f67f2b1de30ec7368d36708321) --- erpnext/accounts/report/gross_profit/gross_profit.py | 1 + 1 file changed, 1 insertion(+) diff --git a/erpnext/accounts/report/gross_profit/gross_profit.py b/erpnext/accounts/report/gross_profit/gross_profit.py index dacc809da0d..ba947f392f8 100644 --- a/erpnext/accounts/report/gross_profit/gross_profit.py +++ b/erpnext/accounts/report/gross_profit/gross_profit.py @@ -607,6 +607,7 @@ class GrossProfitGenerator(object): return abs(previous_stock_value - flt(sle.stock_value)) * flt(row.qty) / abs(flt(sle.qty)) else: return flt(row.qty) * self.get_average_buying_rate(row, item_code) + return 0.0 def get_buying_amount(self, row, item_code): # IMP NOTE From 007a1118473d61edf37714dac0b2d34ab20d64d3 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Wed, 21 Dec 2022 12:37:13 +0530 Subject: [PATCH 02/30] test: type error on bundled products with different warehouses (cherry picked from commit 5918bb03f7db388a27cb9319530b56c383304242) --- .../report/gross_profit/test_gross_profit.py | 95 +++++++++++++++++++ 1 file changed, 95 insertions(+) diff --git a/erpnext/accounts/report/gross_profit/test_gross_profit.py b/erpnext/accounts/report/gross_profit/test_gross_profit.py index 0ea6b5c8a44..fa11a41df4a 100644 --- a/erpnext/accounts/report/gross_profit/test_gross_profit.py +++ b/erpnext/accounts/report/gross_profit/test_gross_profit.py @@ -6,6 +6,8 @@ from frappe.utils import add_days, flt, nowdate from erpnext.accounts.doctype.sales_invoice.sales_invoice import make_delivery_note from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice from erpnext.accounts.report.gross_profit.gross_profit import execute +from erpnext.stock.doctype.delivery_note.delivery_note import make_sales_invoice +from erpnext.stock.doctype.delivery_note.test_delivery_note import create_delivery_note from erpnext.stock.doctype.item.test_item import create_item from erpnext.stock.doctype.stock_entry.stock_entry_utils import make_stock_entry @@ -14,6 +16,7 @@ class TestGrossProfit(FrappeTestCase): def setUp(self): self.create_company() self.create_item() + self.create_bundle() self.create_customer() self.create_sales_invoice() self.clear_old_entries() @@ -42,6 +45,7 @@ class TestGrossProfit(FrappeTestCase): 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 @@ -53,6 +57,23 @@ class TestGrossProfit(FrappeTestCase): ) self.item = item if isinstance(item, str) else item.item_code + def create_bundle(self): + from erpnext.selling.doctype.product_bundle.test_product_bundle import make_product_bundle + + item2 = create_item( + item_code="_Test GP Item 2", is_stock_item=1, company=self.company, warehouse=self.warehouse + ) + self.item2 = item2 if isinstance(item2, str) else item2.item_code + + # This will be parent item + bundle = create_item( + item_code="_Test GP bundle", is_stock_item=0, company=self.company, warehouse=self.warehouse + ) + self.bundle = bundle if isinstance(bundle, str) else bundle.item_code + + # Create Product Bundle + self.product_bundle = make_product_bundle(parent=self.bundle, items=[self.item, self.item2]) + def create_customer(self): name = "_Test GP Customer" if frappe.db.exists("Customer", name): @@ -93,6 +114,28 @@ class TestGrossProfit(FrappeTestCase): ) return sinv + def create_delivery_note( + self, item=None, qty=1, rate=100, posting_date=nowdate(), do_not_save=False, do_not_submit=False + ): + """ + Helper function to populate default values in Delivery Note + """ + dnote = create_delivery_note( + company=self.company, + customer=self.customer, + currency="INR", + item=item or self.item, + qty=qty, + rate=rate, + cost_center=self.cost_center, + warehouse=self.warehouse, + return_against=None, + expense_account=self.expense_account, + do_not_save=do_not_save, + do_not_submit=do_not_submit, + ) + return dnote + def clear_old_entries(self): doctype_list = [ "Sales Invoice", @@ -207,3 +250,55 @@ class TestGrossProfit(FrappeTestCase): } gp_entry = [x for x in data if x.parent_invoice == sinv.name] self.assertDictContainsSubset(expected_entry_with_dn, gp_entry[0]) + + def test_bundled_delivery_note_with_different_warehouses(self): + """ + Test Delivery Note with bundled item. Packed Item from the bundle having different warehouses + """ + se = make_stock_entry( + company=self.company, + item_code=self.item, + target=self.warehouse, + qty=1, + basic_rate=100, + do_not_submit=True, + ) + item = se.items[0] + se.append( + "items", + { + "item_code": self.item2, + "s_warehouse": "", + "t_warehouse": self.finished_warehouse, + "qty": 1, + "basic_rate": 100, + "conversion_factor": item.conversion_factor or 1.0, + "transfer_qty": flt(item.qty) * (flt(item.conversion_factor) or 1.0), + "serial_no": item.serial_no, + "batch_no": item.batch_no, + "cost_center": item.cost_center, + "expense_account": item.expense_account, + }, + ) + se = se.save().submit() + + # Make a Delivery note with Product bundle + # Packed Items will have different warehouses + dnote = self.create_delivery_note(item=self.bundle, qty=1, rate=200, do_not_submit=True) + dnote.packed_items[1].warehouse = self.finished_warehouse + dnote = dnote.submit() + + # make Sales Invoice for above delivery note + sinv = make_sales_invoice(dnote.name) + sinv = sinv.save().submit() + + filters = frappe._dict( + company=self.company, + from_date=nowdate(), + to_date=nowdate(), + group_by="Invoice", + sales_invoice=sinv.name, + ) + + columns, data = execute(filters=filters) + self.assertGreater(len(data), 0) From 0f6790be11d9c191cc8640d4ab5827cd86072173 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Thu, 22 Dec 2022 10:55:22 +0530 Subject: [PATCH 03/30] fix: payment terms and sales partner filter issue in AR/AP report (cherry picked from commit 13c4420f42bb483bbbed7eddf168f4cb62554ab6) --- .../accounts_receivable/accounts_receivable.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/erpnext/accounts/report/accounts_receivable/accounts_receivable.py b/erpnext/accounts/report/accounts_receivable/accounts_receivable.py index a195c575866..e73d5ba390d 100755 --- a/erpnext/accounts/report/accounts_receivable/accounts_receivable.py +++ b/erpnext/accounts/report/accounts_receivable/accounts_receivable.py @@ -794,19 +794,19 @@ class ReceivablePayableReport(object): if self.filters.get("payment_terms_template"): self.qb_selection_filter.append( - self.ple.party_isin( - qb.from_(self.customer).where( - self.customer.payment_terms == self.filters.get("payment_terms_template") - ) + self.ple.party.isin( + qb.from_(self.customer) + .select(self.customer.name) + .where(self.customer.payment_terms == self.filters.get("payment_terms_template")) ) ) if self.filters.get("sales_partner"): self.qb_selection_filter.append( - self.ple.party_isin( - qb.from_(self.customer).where( - self.customer.default_sales_partner == self.filters.get("payment_terms_template") - ) + self.ple.party.isin( + qb.from_(self.customer) + .select(self.customer.name) + .where(self.customer.default_sales_partner == self.filters.get("payment_terms_template")) ) ) From f30f77cde693d3df259257b0c9ad8ee836d82310 Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Thu, 22 Dec 2022 10:24:04 +0530 Subject: [PATCH 04/30] fix: timeout error while submitting stock entry Co-authored-by: Ankush Menat (cherry picked from commit a05c47e49990ad00dc1b33b5d58688bca4b6b021) --- erpnext/stock/doctype/bin/bin.py | 1 + erpnext/stock/stock_ledger.py | 23 ++++++++++++++++++----- 2 files changed, 19 insertions(+), 5 deletions(-) diff --git a/erpnext/stock/doctype/bin/bin.py b/erpnext/stock/doctype/bin/bin.py index c28f45aed41..9f409d4b96a 100644 --- a/erpnext/stock/doctype/bin/bin.py +++ b/erpnext/stock/doctype/bin/bin.py @@ -162,6 +162,7 @@ def update_qty(bin_name, args): .where((sle.item_code == args.get("item_code")) & (sle.warehouse == args.get("warehouse"))) .orderby(CombineDatetime(sle.posting_date, sle.posting_time), order=Order.desc) .orderby(sle.creation, order=Order.desc) + .limit(1) .run() ) diff --git a/erpnext/stock/stock_ledger.py b/erpnext/stock/stock_ledger.py index e7f55e9b35d..55a11a18671 100644 --- a/erpnext/stock/stock_ledger.py +++ b/erpnext/stock/stock_ledger.py @@ -470,8 +470,10 @@ class update_entries_after(object): item_code = %(item_code)s and warehouse = %(warehouse)s and is_cancelled = 0 - and timestamp(posting_date, time_format(posting_time, %(time_format)s)) = timestamp(%(posting_date)s, time_format(%(posting_time)s, %(time_format)s)) - + and ( + posting_date = %(posting_date)s and + time_format(posting_time, %(time_format)s) = time_format(%(posting_time)s, %(time_format)s) + ) order by creation ASC for update @@ -1070,7 +1072,13 @@ def get_previous_sle_of_current_voucher(args, exclude_current_voucher=False): and warehouse = %(warehouse)s and is_cancelled = 0 {voucher_condition} - and timestamp(posting_date, time_format(posting_time, %(time_format)s)) < timestamp(%(posting_date)s, time_format(%(posting_time)s, %(time_format)s)) + and ( + posting_date < %(posting_date)s or + ( + posting_date = %(posting_date)s and + time_format(posting_time, %(time_format)s) < time_format(%(posting_time)s, %(time_format)s) + ) + ) order by timestamp(posting_date, posting_time) desc, creation desc limit 1 for update""".format( @@ -1355,8 +1363,13 @@ def update_qty_in_future_sle(args, allow_negative_stock=False): and warehouse = %(warehouse)s and voucher_no != %(voucher_no)s and is_cancelled = 0 - and timestamp(posting_date, time_format(posting_time, %(time_format)s)) - > timestamp(%(posting_date)s, time_format(%(posting_time)s, %(time_format)s)) + and ( + posting_date > %(posting_date)s or + ( + posting_date = %(posting_date)s and + time_format(posting_time, %(time_format)s) > time_format(%(posting_time)s, %(time_format)s) + ) + ) {datetime_limit_condition} """, args, From 448fbe55826c06fd85fc95c84b2c678ca19fb61f Mon Sep 17 00:00:00 2001 From: s-aga-r Date: Sat, 24 Dec 2022 18:11:04 +0530 Subject: [PATCH 05/30] fix: `shipping_address` in PO (cherry picked from commit 7e1b6b3c2aa114331ad9085cfefee340b8ca2ad0) --- .../doctype/purchase_order/purchase_order.json | 16 ++++++++-------- .../selling/doctype/sales_order/sales_order.py | 6 +++--- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/erpnext/buying/doctype/purchase_order/purchase_order.json b/erpnext/buying/doctype/purchase_order/purchase_order.json index ce7de874c56..e1dd6797815 100644 --- a/erpnext/buying/doctype/purchase_order/purchase_order.json +++ b/erpnext/buying/doctype/purchase_order/purchase_order.json @@ -108,7 +108,7 @@ "contact_display", "contact_mobile", "contact_email", - "company_shipping_address_section", + "shipping_address_section", "shipping_address", "column_break_99", "shipping_address_display", @@ -385,7 +385,7 @@ { "fieldname": "shipping_address", "fieldtype": "Link", - "label": "Company Shipping Address", + "label": "Shipping Address", "options": "Address", "print_hide": 1 }, @@ -1207,11 +1207,6 @@ "fieldtype": "Tab Break", "label": "Address & Contact" }, - { - "fieldname": "company_shipping_address_section", - "fieldtype": "Section Break", - "label": "Company Shipping Address" - }, { "fieldname": "company_billing_address_section", "fieldtype": "Section Break", @@ -1263,13 +1258,18 @@ "fieldname": "named_place", "fieldtype": "Data", "label": "Named Place" + }, + { + "fieldname": "shipping_address_section", + "fieldtype": "Section Break", + "label": "Shipping Address" } ], "icon": "fa fa-file-text", "idx": 105, "is_submittable": 1, "links": [], - "modified": "2022-12-12 18:36:37.455134", + "modified": "2022-12-25 18:08:59.074182", "modified_by": "Administrator", "module": "Buying", "name": "Purchase Order", diff --git a/erpnext/selling/doctype/sales_order/sales_order.py b/erpnext/selling/doctype/sales_order/sales_order.py index 0013c95032f..efe1f8eb171 100755 --- a/erpnext/selling/doctype/sales_order/sales_order.py +++ b/erpnext/selling/doctype/sales_order/sales_order.py @@ -1031,8 +1031,6 @@ def make_purchase_order(source_name, selected_items=None, target_doc=None): target.discount_amount = 0.0 target.inter_company_order_reference = "" target.shipping_rule = "" - target.customer = "" - target.customer_name = "" target.run_method("set_missing_values") target.run_method("calculate_taxes_and_totals") @@ -1059,9 +1057,11 @@ def make_purchase_order(source_name, selected_items=None, target_doc=None): "contact_email", "contact_person", "taxes_and_charges", - "shipping_address", "terms", ], + "field_map": [ + ["shipping_address_name", "shipping_address"], + ], "validation": {"docstatus": ["=", 1]}, }, "Sales Order Item": { From 7a98ece7f124049fb7703680d712a0926ec58850 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Mon, 26 Dec 2022 10:12:12 +0530 Subject: [PATCH 06/30] refactor: Customer and Supplier Ledger summary will have hidden fields for better handling of user permission (#33433) --- .../customer_ledger_summary.py | 72 +++++++++++++++++++ .../supplier_ledger_summary.js | 18 ----- .../customer_group/customer_group.json | 14 +++- .../supplier_group/supplier_group.json | 16 ++++- .../setup/doctype/territory/territory.json | 14 +++- 5 files changed, 110 insertions(+), 24 deletions(-) diff --git a/erpnext/accounts/report/customer_ledger_summary/customer_ledger_summary.py b/erpnext/accounts/report/customer_ledger_summary/customer_ledger_summary.py index cafe95b3603..d870a1aaf83 100644 --- a/erpnext/accounts/report/customer_ledger_summary/customer_ledger_summary.py +++ b/erpnext/accounts/report/customer_ledger_summary/customer_ledger_summary.py @@ -26,6 +26,7 @@ class PartyLedgerSummaryReport(object): ) self.get_gl_entries() + self.get_additional_columns() self.get_return_invoices() self.get_party_adjustment_amounts() @@ -33,6 +34,42 @@ class PartyLedgerSummaryReport(object): data = self.get_data() return columns, data + def get_additional_columns(self): + """ + Additional Columns for 'User Permission' based access control + """ + from frappe import qb + + if self.filters.party_type == "Customer": + self.territories = frappe._dict({}) + self.customer_group = frappe._dict({}) + + customer = qb.DocType("Customer") + result = ( + frappe.qb.from_(customer) + .select( + customer.name, customer.territory, customer.customer_group, customer.default_sales_partner + ) + .where((customer.disabled == 0)) + .run(as_dict=True) + ) + + for x in result: + self.territories[x.name] = x.territory + self.customer_group[x.name] = x.customer_group + else: + self.supplier_group = frappe._dict({}) + supplier = qb.DocType("Supplier") + result = ( + frappe.qb.from_(supplier) + .select(supplier.name, supplier.supplier_group) + .where((supplier.disabled == 0)) + .run(as_dict=True) + ) + + for x in result: + self.supplier_group[x.name] = x.supplier_group + def get_columns(self): columns = [ { @@ -116,6 +153,35 @@ class PartyLedgerSummaryReport(object): }, ] + # Hidden columns for handling 'User Permissions' + if self.filters.party_type == "Customer": + columns += [ + { + "label": _("Territory"), + "fieldname": "territory", + "fieldtype": "Link", + "options": "Territory", + "hidden": 1, + }, + { + "label": _("Customer Group"), + "fieldname": "customer_group", + "fieldtype": "Link", + "options": "Customer Group", + "hidden": 1, + }, + ] + else: + columns += [ + { + "label": _("Supplier Group"), + "fieldname": "supplier_group", + "fieldtype": "Link", + "options": "Supplier Group", + "hidden": 1, + } + ] + return columns def get_data(self): @@ -143,6 +209,12 @@ class PartyLedgerSummaryReport(object): ), ) + if self.filters.party_type == "Customer": + self.party_data[gle.party].update({"territory": self.territories.get(gle.party)}) + self.party_data[gle.party].update({"customer_group": self.customer_group.get(gle.party)}) + else: + self.party_data[gle.party].update({"supplier_group": self.supplier_group.get(gle.party)}) + amount = gle.get(invoice_dr_or_cr) - gle.get(reverse_dr_or_cr) self.party_data[gle.party].closing_balance += amount diff --git a/erpnext/accounts/report/supplier_ledger_summary/supplier_ledger_summary.js b/erpnext/accounts/report/supplier_ledger_summary/supplier_ledger_summary.js index f81297760ed..5dc4c3d1c15 100644 --- a/erpnext/accounts/report/supplier_ledger_summary/supplier_ledger_summary.js +++ b/erpnext/accounts/report/supplier_ledger_summary/supplier_ledger_summary.js @@ -63,24 +63,6 @@ frappe.query_reports["Supplier Ledger Summary"] = { "fieldtype": "Link", "options": "Payment Terms Template" }, - { - "fieldname":"territory", - "label": __("Territory"), - "fieldtype": "Link", - "options": "Territory" - }, - { - "fieldname":"sales_partner", - "label": __("Sales Partner"), - "fieldtype": "Link", - "options": "Sales Partner" - }, - { - "fieldname":"sales_person", - "label": __("Sales Person"), - "fieldtype": "Link", - "options": "Sales Person" - }, { "fieldname":"tax_id", "label": __("Tax Id"), diff --git a/erpnext/setup/doctype/customer_group/customer_group.json b/erpnext/setup/doctype/customer_group/customer_group.json index 0e2ed9efcf8..d6a431ea616 100644 --- a/erpnext/setup/doctype/customer_group/customer_group.json +++ b/erpnext/setup/doctype/customer_group/customer_group.json @@ -139,10 +139,11 @@ "idx": 1, "is_tree": 1, "links": [], - "modified": "2021-02-08 17:01:52.162202", + "modified": "2022-12-24 11:15:17.142746", "modified_by": "Administrator", "module": "Setup", "name": "Customer Group", + "naming_rule": "By fieldname", "nsm_parent_field": "parent_customer_group", "owner": "Administrator", "permissions": [ @@ -198,10 +199,19 @@ "role": "Customer", "select": 1, "share": 1 + }, + { + "email": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "Accounts User", + "share": 1 } ], "search_fields": "parent_customer_group", "show_name_in_global_search": 1, "sort_field": "modified", - "sort_order": "DESC" + "sort_order": "DESC", + "states": [] } \ No newline at end of file diff --git a/erpnext/setup/doctype/supplier_group/supplier_group.json b/erpnext/setup/doctype/supplier_group/supplier_group.json index 9119bb947cb..b3ed608cd03 100644 --- a/erpnext/setup/doctype/supplier_group/supplier_group.json +++ b/erpnext/setup/doctype/supplier_group/supplier_group.json @@ -6,6 +6,7 @@ "creation": "2013-01-10 16:34:24", "doctype": "DocType", "document_type": "Setup", + "engine": "InnoDB", "field_order": [ "supplier_group_name", "parent_supplier_group", @@ -106,10 +107,11 @@ "idx": 1, "is_tree": 1, "links": [], - "modified": "2020-03-18 18:10:49.228407", + "modified": "2022-12-24 11:16:12.486719", "modified_by": "Administrator", "module": "Setup", "name": "Supplier Group", + "naming_rule": "By fieldname", "nsm_parent_field": "parent_supplier_group", "owner": "Administrator", "permissions": [ @@ -156,8 +158,18 @@ "permlevel": 1, "read": 1, "role": "Purchase User" + }, + { + "email": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "Accounts User", + "share": 1 } ], "show_name_in_global_search": 1, - "sort_order": "ASC" + "sort_field": "modified", + "sort_order": "ASC", + "states": [] } \ No newline at end of file diff --git a/erpnext/setup/doctype/territory/territory.json b/erpnext/setup/doctype/territory/territory.json index a25bda054b9..c3a49933746 100644 --- a/erpnext/setup/doctype/territory/territory.json +++ b/erpnext/setup/doctype/territory/territory.json @@ -123,11 +123,12 @@ "idx": 1, "is_tree": 1, "links": [], - "modified": "2021-02-08 17:10:03.767426", + "modified": "2022-12-24 11:16:39.964956", "modified_by": "Administrator", "module": "Setup", "name": "Territory", "name_case": "Title Case", + "naming_rule": "By fieldname", "nsm_parent_field": "parent_territory", "owner": "Administrator", "permissions": [ @@ -175,10 +176,19 @@ "role": "Customer", "select": 1, "share": 1 + }, + { + "email": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "Accounts User", + "share": 1 } ], "search_fields": "parent_territory,territory_manager", "show_name_in_global_search": 1, "sort_field": "modified", - "sort_order": "DESC" + "sort_order": "DESC", + "states": [] } \ No newline at end of file From e7704b2321c6c4fdaf4850ba9b37554daf092304 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Mon, 26 Dec 2022 11:34:47 +0530 Subject: [PATCH 07/30] feat: Accounting Dimension updation in Payment Request and Entry (#33411) --- .../doctype/payment_entry/payment_entry.py | 14 +++++++++ .../payment_request/payment_request.json | 28 ++++++++++++++++- .../payment_request/payment_request.py | 25 +++++++++++++++ erpnext/patches.txt | 3 +- ...counting_dimensions_for_payment_request.py | 31 +++++++++++++++++++ 5 files changed, 99 insertions(+), 2 deletions(-) create mode 100644 erpnext/patches/v14_0/create_accounting_dimensions_for_payment_request.py diff --git a/erpnext/accounts/doctype/payment_entry/payment_entry.py b/erpnext/accounts/doctype/payment_entry/payment_entry.py index 1a761b424ad..5dbc91654f9 100644 --- a/erpnext/accounts/doctype/payment_entry/payment_entry.py +++ b/erpnext/accounts/doctype/payment_entry/payment_entry.py @@ -1758,6 +1758,8 @@ def get_payment_entry( pe.setup_party_account_field() pe.set_missing_values() + update_accounting_dimensions(pe, doc) + if party_account and bank: pe.set_exchange_rate(ref_doc=reference_doc) pe.set_amounts() @@ -1775,6 +1777,18 @@ def get_payment_entry( return pe +def update_accounting_dimensions(pe, doc): + """ + Updates accounting dimensions in Payment Entry based on the accounting dimensions in the reference document + """ + from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import ( + get_accounting_dimensions, + ) + + for dimension in get_accounting_dimensions(): + pe.set(dimension, doc.get(dimension)) + + def get_bank_cash_account(doc, bank_account): bank = get_default_bank_cash_account( doc.company, "Bank", mode_of_payment=doc.get("mode_of_payment"), account=bank_account diff --git a/erpnext/accounts/doctype/payment_request/payment_request.json b/erpnext/accounts/doctype/payment_request/payment_request.json index 2f3516e135a..381f3fb531a 100644 --- a/erpnext/accounts/doctype/payment_request/payment_request.json +++ b/erpnext/accounts/doctype/payment_request/payment_request.json @@ -32,6 +32,10 @@ "iban", "branch_code", "swift_number", + "accounting_dimensions_section", + "cost_center", + "dimension_col_break", + "project", "recipient_and_message", "print_format", "email_to", @@ -362,13 +366,35 @@ "label": "Payment Channel", "options": "\nEmail\nPhone", "read_only": 1 + }, + { + "collapsible": 1, + "fieldname": "accounting_dimensions_section", + "fieldtype": "Section Break", + "label": "Accounting Dimensions" + }, + { + "fieldname": "cost_center", + "fieldtype": "Link", + "label": "Cost Center", + "options": "Cost Center" + }, + { + "fieldname": "dimension_col_break", + "fieldtype": "Column Break" + }, + { + "fieldname": "project", + "fieldtype": "Link", + "label": "Project", + "options": "Project" } ], "in_create": 1, "index_web_pages_for_search": 1, "is_submittable": 1, "links": [], - "modified": "2022-09-30 16:19:43.680025", + "modified": "2022-12-21 16:56:40.115737", "modified_by": "Administrator", "module": "Accounts", "name": "Payment Request", diff --git a/erpnext/accounts/doctype/payment_request/payment_request.py b/erpnext/accounts/doctype/payment_request/payment_request.py index e09da678071..f63fba1b716 100644 --- a/erpnext/accounts/doctype/payment_request/payment_request.py +++ b/erpnext/accounts/doctype/payment_request/payment_request.py @@ -11,6 +11,9 @@ from frappe.utils import flt, get_url, nowdate from frappe.utils.background_jobs import enqueue from payments.utils import get_payment_gateway_controller +from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import ( + get_accounting_dimensions, +) from erpnext.accounts.doctype.payment_entry.payment_entry import ( get_company_defaults, get_payment_entry, @@ -263,6 +266,17 @@ class PaymentRequest(Document): } ) + # Update dimensions + payment_entry.update( + { + "cost_center": self.get("cost_center"), + "project": self.get("project"), + } + ) + + for dimension in get_accounting_dimensions(): + payment_entry.update({dimension: self.get(dimension)}) + if payment_entry.difference_amount: company_details = get_company_defaults(ref_doc.company) @@ -442,6 +456,17 @@ def make_payment_request(**args): } ) + # Update dimensions + pr.update( + { + "cost_center": ref_doc.get("cost_center"), + "project": ref_doc.get("project"), + } + ) + + for dimension in get_accounting_dimensions(): + pr.update({dimension: ref_doc.get(dimension)}) + if args.order_type == "Shopping Cart" or args.mute_email: pr.flags.mute_email = True diff --git a/erpnext/patches.txt b/erpnext/patches.txt index 5e83f837614..f7d2dedb1b3 100644 --- a/erpnext/patches.txt +++ b/erpnext/patches.txt @@ -319,4 +319,5 @@ erpnext.patches.v14_0.create_accounting_dimensions_for_asset_capitalization erpnext.patches.v13_0.update_schedule_type_in_loans erpnext.patches.v14_0.update_partial_tds_fields erpnext.patches.v14_0.create_incoterms_and_migrate_shipment -erpnext.patches.v14_0.setup_clear_repost_logs \ No newline at end of file +erpnext.patches.v14_0.setup_clear_repost_logs +erpnext.patches.v14_0.create_accounting_dimensions_for_payment_request \ No newline at end of file diff --git a/erpnext/patches/v14_0/create_accounting_dimensions_for_payment_request.py b/erpnext/patches/v14_0/create_accounting_dimensions_for_payment_request.py new file mode 100644 index 00000000000..bede419ad29 --- /dev/null +++ b/erpnext/patches/v14_0/create_accounting_dimensions_for_payment_request.py @@ -0,0 +1,31 @@ +import frappe +from frappe.custom.doctype.custom_field.custom_field import create_custom_field + + +def execute(): + accounting_dimensions = frappe.db.get_all( + "Accounting Dimension", fields=["fieldname", "label", "document_type", "disabled"] + ) + + if not accounting_dimensions: + return + + doctype = "Payment Request" + + for d in accounting_dimensions: + field = frappe.db.get_value("Custom Field", {"dt": doctype, "fieldname": d.fieldname}) + + if field: + continue + + df = { + "fieldname": d.fieldname, + "label": d.label, + "fieldtype": "Link", + "options": d.document_type, + "insert_after": "accounting_dimensions_section", + } + + create_custom_field(doctype, df, ignore_validate=True) + + frappe.clear_cache(doctype=doctype) From a7a3654541b75854bdc69a822845e23e4088dee5 Mon Sep 17 00:00:00 2001 From: s-aga-r Date: Mon, 26 Dec 2022 11:39:58 +0530 Subject: [PATCH 08/30] fix: `shipping_address` for non-drop shipping item (cherry picked from commit 67a7ccf3cead654e66a3d1b5ccb253d90b19c43b) --- .../doctype/sales_order/sales_order.py | 21 ++++++++++++++++--- 1 file changed, 18 insertions(+), 3 deletions(-) diff --git a/erpnext/selling/doctype/sales_order/sales_order.py b/erpnext/selling/doctype/sales_order/sales_order.py index efe1f8eb171..7c0601e3dd5 100755 --- a/erpnext/selling/doctype/sales_order/sales_order.py +++ b/erpnext/selling/doctype/sales_order/sales_order.py @@ -1024,6 +1024,15 @@ def make_purchase_order(source_name, selected_items=None, target_doc=None): ] items_to_map = list(set(items_to_map)) + def is_drop_ship_order(target): + drop_ship = True + for item in target.items: + if not item.delivered_by_supplier: + drop_ship = False + break + + return drop_ship + def set_missing_values(source, target): target.supplier = "" target.apply_discount_on = "" @@ -1031,6 +1040,14 @@ def make_purchase_order(source_name, selected_items=None, target_doc=None): target.discount_amount = 0.0 target.inter_company_order_reference = "" target.shipping_rule = "" + + if is_drop_ship_order(target): + target.customer = source.customer + target.customer_name = source.customer_name + target.shipping_address = source.shipping_address_name + else: + target.customer = target.customer_name = target.shipping_address = None + target.run_method("set_missing_values") target.run_method("calculate_taxes_and_totals") @@ -1057,11 +1074,9 @@ def make_purchase_order(source_name, selected_items=None, target_doc=None): "contact_email", "contact_person", "taxes_and_charges", + "shipping_address", "terms", ], - "field_map": [ - ["shipping_address_name", "shipping_address"], - ], "validation": {"docstatus": ["=", 1]}, }, "Sales Order Item": { From 1edde9c9e05868166b0ba929b373f4968238442d Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Tue, 27 Dec 2022 15:01:31 +0530 Subject: [PATCH 09/30] fix: Random behaviour while picking items using picklist (backport #33449) (#33450) fix: Random behaviour while picking items using picklist (#33449) (cherry picked from commit 8263bf9a9a716f7651386633fd6f26686f2008bd) Co-authored-by: Deepesh Garg --- erpnext/stock/doctype/pick_list/pick_list.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/stock/doctype/pick_list/pick_list.py b/erpnext/stock/doctype/pick_list/pick_list.py index aff5e0539c7..8704b6718b9 100644 --- a/erpnext/stock/doctype/pick_list/pick_list.py +++ b/erpnext/stock/doctype/pick_list/pick_list.py @@ -441,7 +441,7 @@ def get_available_item_locations_for_batched_item( sle.`batch_no`, sle.`item_code` HAVING `qty` > 0 - ORDER BY IFNULL(batch.`expiry_date`, '2200-01-01'), batch.`creation` + ORDER BY IFNULL(batch.`expiry_date`, '2200-01-01'), batch.`creation`, sle.`batch_no`, sle.`warehouse` """.format( warehouse_condition=warehouse_condition ), From a166a7676847cc8fe34bb879bfcbf59adeb85e47 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Tue, 27 Dec 2022 17:49:22 +0530 Subject: [PATCH 10/30] fix: Multiple rows for same warehouse and batches in pick list (backport #33456) (#33458) fix: Multiple rows for same warehouse and batches in pick list (#33456) (cherry picked from commit d2686ce75bb39bffff4fd1b56ad4880444efb72e) Co-authored-by: Deepesh Garg --- erpnext/stock/doctype/pick_list/pick_list.js | 10 ++++++++- erpnext/stock/doctype/pick_list/pick_list.py | 22 +++++++++++++++++++- 2 files changed, 30 insertions(+), 2 deletions(-) diff --git a/erpnext/stock/doctype/pick_list/pick_list.js b/erpnext/stock/doctype/pick_list/pick_list.js index 799406cd79e..8213adb89bf 100644 --- a/erpnext/stock/doctype/pick_list/pick_list.js +++ b/erpnext/stock/doctype/pick_list/pick_list.js @@ -51,7 +51,15 @@ frappe.ui.form.on('Pick List', { if (!(frm.doc.locations && frm.doc.locations.length)) { frappe.msgprint(__('Add items in the Item Locations table')); } else { - frm.call('set_item_locations', {save: save}); + frappe.call({ + method: "set_item_locations", + doc: frm.doc, + args: { + "save": save, + }, + freeze: 1, + freeze_message: __("Setting Item Locations..."), + }); } }, get_item_locations: (frm) => { diff --git a/erpnext/stock/doctype/pick_list/pick_list.py b/erpnext/stock/doctype/pick_list/pick_list.py index 8704b6718b9..953fca7419c 100644 --- a/erpnext/stock/doctype/pick_list/pick_list.py +++ b/erpnext/stock/doctype/pick_list/pick_list.py @@ -135,6 +135,7 @@ class PickList(Document): # reset self.delete_key("locations") + updated_locations = frappe._dict() for item_doc in items: item_code = item_doc.item_code @@ -155,7 +156,26 @@ class PickList(Document): for row in locations: location = item_doc.as_dict() location.update(row) - self.append("locations", location) + key = ( + location.item_code, + location.warehouse, + location.uom, + location.batch_no, + location.serial_no, + location.sales_order_item or location.material_request_item, + ) + + if key not in updated_locations: + updated_locations.setdefault(key, location) + else: + updated_locations[key].qty += location.qty + updated_locations[key].stock_qty += location.stock_qty + + for location in updated_locations.values(): + if location.picked_qty > location.stock_qty: + location.picked_qty = location.stock_qty + + self.append("locations", location) # If table is empty on update after submit, set stock_qty, picked_qty to 0 so that indicator is red # and give feedback to the user. This is to avoid empty Pick Lists. From a332b229cfb09a2f83d96f1cdb74cc862516b0f4 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Tue, 27 Dec 2022 17:56:05 +0530 Subject: [PATCH 11/30] fix: Default dimensions on fetching items from BOM (backport #33439) (#33459) fix: Default dimensions on fetching items from BOM (#33439) (cherry picked from commit 0b75aa53907e67d440884a2ea0084044265994a5) Co-authored-by: Deepesh Garg --- erpnext/stock/doctype/stock_entry/stock_entry.js | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.js b/erpnext/stock/doctype/stock_entry/stock_entry.js index b9102445e01..d4b4efa4cdd 100644 --- a/erpnext/stock/doctype/stock_entry/stock_entry.js +++ b/erpnext/stock/doctype/stock_entry/stock_entry.js @@ -112,6 +112,10 @@ frappe.ui.form.on('Stock Entry', { } }); attach_bom_items(frm.doc.bom_no); + + if(!check_should_not_attach_bom_items(frm.doc.bom_no)) { + erpnext.accounts.dimensions.update_dimension(frm, frm.doctype); + } }, setup_quality_inspection: function(frm) { @@ -326,7 +330,11 @@ frappe.ui.form.on('Stock Entry', { } frm.trigger("setup_quality_inspection"); - attach_bom_items(frm.doc.bom_no) + attach_bom_items(frm.doc.bom_no); + + if(!check_should_not_attach_bom_items(frm.doc.bom_no)) { + erpnext.accounts.dimensions.update_dimension(frm, frm.doctype); + } }, before_save: function(frm) { @@ -939,7 +947,10 @@ erpnext.stock.StockEntry = class StockEntry extends erpnext.stock.StockControlle method: "get_items", callback: function(r) { if(!r.exc) refresh_field("items"); - if(me.frm.doc.bom_no) attach_bom_items(me.frm.doc.bom_no) + if(me.frm.doc.bom_no) { + attach_bom_items(me.frm.doc.bom_no); + erpnext.accounts.dimensions.update_dimension(me.frm, me.frm.doctype); + } } }); } From 73c9820e825e30cb88495c6735339882699a18f7 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Tue, 27 Dec 2022 18:28:36 +0530 Subject: [PATCH 12/30] fix: Customer Primary Contact (backport #33424) (#33440) fix: Customer Primary Contact (#33424) Co-authored-by: Nihantra C. Patel (cherry picked from commit 7d9f3f23dde6efd6fc3f218d48a3828dd9aa1133) Co-authored-by: Solufyin <34390782+Solufyin@users.noreply.github.com> --- erpnext/selling/doctype/customer/customer.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/selling/doctype/customer/customer.py b/erpnext/selling/doctype/customer/customer.py index 60c33567bef..146ebbd7dfe 100644 --- a/erpnext/selling/doctype/customer/customer.py +++ b/erpnext/selling/doctype/customer/customer.py @@ -740,7 +740,7 @@ def get_customer_primary_contact(doctype, txt, searchfield, start, page_len, fil qb.from_(con) .join(dlink) .on(con.name == dlink.parent) - .select(con.name, con.full_name, con.email_id) + .select(con.name, con.email_id) .where((dlink.link_name == customer) & (con.name.like(f"%{txt}%"))) .run() ) From 823b352c57af2e2e698c5b2debe615b4605ff651 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Fri, 16 Dec 2022 21:58:22 +0530 Subject: [PATCH 13/30] fix: ERR journals reported in AR/AP Exchange Rate Revaluation on Receivable/Payable will included in AR/AP report (cherry picked from commit b09eade3e4b41833e507410410f63b93d8d17de7) --- .../accounts_receivable.py | 20 ++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/erpnext/accounts/report/accounts_receivable/accounts_receivable.py b/erpnext/accounts/report/accounts_receivable/accounts_receivable.py index e73d5ba390d..fb2e444abd1 100755 --- a/erpnext/accounts/report/accounts_receivable/accounts_receivable.py +++ b/erpnext/accounts/report/accounts_receivable/accounts_receivable.py @@ -99,6 +99,9 @@ class ReceivablePayableReport(object): # Get return entries self.get_return_entries() + # Get Exchange Rate Revaluations + self.get_exchange_rate_revaluations() + self.data = [] for ple in self.ple_entries: @@ -251,7 +254,8 @@ class ReceivablePayableReport(object): row.invoice_grand_total = row.invoiced if (abs(row.outstanding) > 1.0 / 10**self.currency_precision) and ( - abs(row.outstanding_in_account_currency) > 1.0 / 10**self.currency_precision + (abs(row.outstanding_in_account_currency) > 1.0 / 10**self.currency_precision) + or (row.voucher_no in self.err_journals) ): # non-zero oustanding, we must consider this row @@ -1028,3 +1032,17 @@ class ReceivablePayableReport(object): "data": {"labels": self.ageing_column_labels, "datasets": rows}, "type": "percentage", } + + def get_exchange_rate_revaluations(self): + je = qb.DocType("Journal Entry") + results = ( + qb.from_(je) + .select(je.name) + .where( + (je.company == self.filters.company) + & (je.posting_date.lte(self.filters.report_date)) + & (je.voucher_type == "Exchange Rate Revaluation") + ) + .run() + ) + self.err_journals = [x[0] for x in results] if results else [] From 1f40d9be93056a4bd4a0bce0734ac98081f26067 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Sun, 18 Dec 2022 12:39:54 +0530 Subject: [PATCH 14/30] test: err for party should be in AR/AP report (cherry picked from commit 2ed86760d7696b0129a0d01cc4287cfe001f3a66) --- .../test_accounts_receivable.py | 134 +++++++++++++++--- 1 file changed, 116 insertions(+), 18 deletions(-) diff --git a/erpnext/accounts/report/accounts_receivable/test_accounts_receivable.py b/erpnext/accounts/report/accounts_receivable/test_accounts_receivable.py index bac8beed2e2..97a9c15fc76 100644 --- a/erpnext/accounts/report/accounts_receivable/test_accounts_receivable.py +++ b/erpnext/accounts/report/accounts_receivable/test_accounts_receivable.py @@ -1,9 +1,10 @@ import unittest import frappe -from frappe.tests.utils import FrappeTestCase -from frappe.utils import add_days, getdate, today +from frappe.tests.utils import FrappeTestCase, change_settings +from frappe.utils import add_days, flt, getdate, today +from erpnext import get_default_cost_center from erpnext.accounts.doctype.payment_entry.payment_entry import get_payment_entry from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice from erpnext.accounts.report.accounts_receivable.accounts_receivable import execute @@ -17,10 +18,37 @@ class TestAccountsReceivable(FrappeTestCase): frappe.db.sql("delete from `tabPayment Entry` where company='_Test Company 2'") frappe.db.sql("delete from `tabGL Entry` where company='_Test Company 2'") frappe.db.sql("delete from `tabPayment Ledger Entry` where company='_Test Company 2'") + frappe.db.sql("delete from `tabJournal Entry` where company='_Test Company 2'") + frappe.db.sql("delete from `tabExchange Rate Revaluation` where company='_Test Company 2'") + + self.create_usd_account() def tearDown(self): frappe.db.rollback() + def create_usd_account(self): + name = "Debtors USD" + exists = frappe.db.get_list( + "Account", filters={"company": "_Test Company 2", "account_name": "Debtors USD"} + ) + if exists: + self.debtors_usd = exists[0].name + else: + debtors = frappe.get_doc( + "Account", + frappe.db.get_list( + "Account", filters={"company": "_Test Company 2", "account_name": "Debtors"} + )[0].name, + ) + + debtors_usd = frappe.new_doc("Account") + debtors_usd.company = debtors.company + debtors_usd.account_name = "Debtors USD" + debtors_usd.account_currency = "USD" + debtors_usd.parent_account = debtors.parent_account + debtors_usd.account_type = debtors.account_type + self.debtors_usd = debtors_usd.save().name + def test_accounts_receivable(self): filters = { "company": "_Test Company 2", @@ -33,7 +61,7 @@ class TestAccountsReceivable(FrappeTestCase): } # check invoice grand total and invoiced column's value for 3 payment terms - name = make_sales_invoice() + name = make_sales_invoice().name report = execute(filters) expected_data = [[100, 30], [100, 50], [100, 20]] @@ -118,8 +146,74 @@ class TestAccountsReceivable(FrappeTestCase): ], ) + @change_settings( + "Accounts Settings", {"allow_multi_currency_invoices_against_single_party_account": 1} + ) + def test_exchange_revaluation_for_party(self): + """ + Exchange Revaluation for party on Receivable/Payable shoule be included + """ -def make_sales_invoice(): + company = "_Test Company 2" + customer = "_Test Customer 2" + + # Using Exchange Gain/Loss account for unrealized as well. + company_doc = frappe.get_doc("Company", company) + company_doc.unrealized_exchange_gain_loss_account = company_doc.exchange_gain_loss_account + company_doc.save() + + si = make_sales_invoice(no_payment_schedule=True, do_not_submit=True) + si.currency = "USD" + si.conversion_rate = 0.90 + si.debit_to = self.debtors_usd + si = si.save().submit() + + # Exchange Revaluation + err = frappe.new_doc("Exchange Rate Revaluation") + err.company = company + err.posting_date = today() + accounts = err.get_accounts_data() + err.extend("accounts", accounts) + err.accounts[0].new_exchange_rate = 0.95 + row = err.accounts[0] + row.new_balance_in_base_currency = flt( + row.new_exchange_rate * flt(row.balance_in_account_currency) + ) + row.gain_loss = row.new_balance_in_base_currency - flt(row.balance_in_base_currency) + err.set_total_gain_loss() + err = err.save().submit() + + # Submit JV for ERR + jv = frappe.get_doc(err.make_jv_entry()) + jv = jv.save() + for x in jv.accounts: + x.cost_center = get_default_cost_center(jv.company) + jv.submit() + + filters = { + "company": company, + "report_date": today(), + "range1": 30, + "range2": 60, + "range3": 90, + "range4": 120, + } + report = execute(filters) + + expected_data_for_err = [0, -5, 0, 5] + row = [x for x in report[1] if x.voucher_type == jv.doctype and x.voucher_no == jv.name][0] + self.assertEqual( + expected_data_for_err, + [ + row.invoiced, + row.paid, + row.credit_note, + row.outstanding, + ], + ) + + +def make_sales_invoice(no_payment_schedule=False, do_not_submit=False): frappe.set_user("Administrator") si = create_sales_invoice( @@ -134,22 +228,26 @@ def make_sales_invoice(): do_not_save=1, ) - si.append( - "payment_schedule", - dict(due_date=getdate(add_days(today(), 30)), invoice_portion=30.00, payment_amount=30), - ) - si.append( - "payment_schedule", - dict(due_date=getdate(add_days(today(), 60)), invoice_portion=50.00, payment_amount=50), - ) - si.append( - "payment_schedule", - dict(due_date=getdate(add_days(today(), 90)), invoice_portion=20.00, payment_amount=20), - ) + if not no_payment_schedule: + si.append( + "payment_schedule", + dict(due_date=getdate(add_days(today(), 30)), invoice_portion=30.00, payment_amount=30), + ) + si.append( + "payment_schedule", + dict(due_date=getdate(add_days(today(), 60)), invoice_portion=50.00, payment_amount=50), + ) + si.append( + "payment_schedule", + dict(due_date=getdate(add_days(today(), 90)), invoice_portion=20.00, payment_amount=20), + ) - si.submit() + si = si.save() - return si.name + if not do_not_submit: + si = si.submit() + + return si def make_payment(docname): From 6e15331fd4c906223f9d4605a12373aef4a5f061 Mon Sep 17 00:00:00 2001 From: s-aga-r Date: Tue, 27 Dec 2022 14:54:23 +0530 Subject: [PATCH 15/30] fix: `fg_item_qty` in non-subcontracted PO (cherry picked from commit 6f5824cb213cdda10e225322dd417e0258e0a3ce) --- .../doctype/purchase_order/purchase_order.py | 53 ++++++++++--------- 1 file changed, 29 insertions(+), 24 deletions(-) diff --git a/erpnext/buying/doctype/purchase_order/purchase_order.py b/erpnext/buying/doctype/purchase_order/purchase_order.py index 4c10b4812e7..5a4168a573e 100644 --- a/erpnext/buying/doctype/purchase_order/purchase_order.py +++ b/erpnext/buying/doctype/purchase_order/purchase_order.py @@ -207,31 +207,36 @@ class PurchaseOrder(BuyingController): ) def validate_fg_item_for_subcontracting(self): - if self.is_subcontracted and not self.is_old_subcontracting_flow: + if self.is_subcontracted: + if not self.is_old_subcontracting_flow: + for item in self.items: + if not item.fg_item: + frappe.throw( + _("Row #{0}: Finished Good Item is not specified for service item {1}").format( + item.idx, item.item_code + ) + ) + else: + if not frappe.get_value("Item", item.fg_item, "is_sub_contracted_item"): + frappe.throw( + _( + "Row #{0}: Finished Good Item {1} must be a sub-contracted item for service item {2}" + ).format(item.idx, item.fg_item, item.item_code) + ) + elif not frappe.get_value("Item", item.fg_item, "default_bom"): + frappe.throw( + _("Row #{0}: Default BOM not found for FG Item {1}").format(item.idx, item.fg_item) + ) + if not item.fg_item_qty: + frappe.throw( + _("Row #{0}: Finished Good Item Qty is not specified for service item {0}").format( + item.idx, item.item_code + ) + ) + else: for item in self.items: - if not item.fg_item: - frappe.throw( - _("Row #{0}: Finished Good Item is not specified for service item {1}").format( - item.idx, item.item_code - ) - ) - else: - if not frappe.get_value("Item", item.fg_item, "is_sub_contracted_item"): - frappe.throw( - _( - "Row #{0}: Finished Good Item {1} must be a sub-contracted item for service item {2}" - ).format(item.idx, item.fg_item, item.item_code) - ) - elif not frappe.get_value("Item", item.fg_item, "default_bom"): - frappe.throw( - _("Row #{0}: Default BOM not found for FG Item {1}").format(item.idx, item.fg_item) - ) - if not item.fg_item_qty: - frappe.throw( - _("Row #{0}: Finished Good Item Qty is not specified for service item {0}").format( - item.idx, item.item_code - ) - ) + item.set("fg_item", None) + item.set("fg_item_qty", 0) def get_schedule_dates(self): for d in self.get("items"): From 3bceb475427ed13fc2cf57ebe8c6adfc8820fa57 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Thu, 29 Dec 2022 09:33:59 +0530 Subject: [PATCH 16/30] fix(pricing rule): consider child tables in condition (backport #33469) (#33470) fix(pricing rule): consider child tables in condition (#33469) (cherry picked from commit cabaed9ed2526e2649d173c806f6987d3377b0c3) Co-authored-by: Dany Robert --- erpnext/accounts/doctype/pricing_rule/utils.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/erpnext/accounts/doctype/pricing_rule/utils.py b/erpnext/accounts/doctype/pricing_rule/utils.py index ab1d7385f78..199766e9a8d 100644 --- a/erpnext/accounts/doctype/pricing_rule/utils.py +++ b/erpnext/accounts/doctype/pricing_rule/utils.py @@ -246,10 +246,15 @@ def get_other_conditions(conditions, values, args): if args.get("doctype") in [ "Quotation", + "Quotation Item", "Sales Order", + "Sales Order Item", "Delivery Note", + "Delivery Note Item", "Sales Invoice", + "Sales Invoice Item", "POS Invoice", + "POS Invoice Item", ]: conditions += """ and ifnull(`tabPricing Rule`.selling, 0) = 1""" else: From 88ca7806af440d7ba09382cd5c197c18b0af603a Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Thu, 29 Dec 2022 09:34:51 +0530 Subject: [PATCH 17/30] fix: use base_net_amount in case of missing stock qty (#33457) (cherry picked from commit e3a0ce5d6328d2ef68fb00419775ce113889c133) --- .../item_wise_purchase_register.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/erpnext/accounts/report/item_wise_purchase_register/item_wise_purchase_register.py b/erpnext/accounts/report/item_wise_purchase_register/item_wise_purchase_register.py index c04b9c71252..d34c21348c8 100644 --- a/erpnext/accounts/report/item_wise_purchase_register/item_wise_purchase_register.py +++ b/erpnext/accounts/report/item_wise_purchase_register/item_wise_purchase_register.py @@ -53,9 +53,6 @@ def _execute(filters=None, additional_table_columns=None, additional_query_colum item_details = get_item_details() for d in item_list: - if not d.stock_qty: - continue - item_record = item_details.get(d.item_code) purchase_receipt = None @@ -94,7 +91,7 @@ def _execute(filters=None, additional_table_columns=None, additional_query_colum "expense_account": expense_account, "stock_qty": d.stock_qty, "stock_uom": d.stock_uom, - "rate": d.base_net_amount / d.stock_qty, + "rate": d.base_net_amount / d.stock_qty if d.stock_qty else d.base_net_amount, "amount": d.base_net_amount, } ) From a7901c2b9cf0c97cd4a96546cb4d97cd000afa37 Mon Sep 17 00:00:00 2001 From: rohitwaghchaure Date: Fri, 30 Dec 2022 11:08:31 +0530 Subject: [PATCH 18/30] Revert "fix: daily scheduler to identify and fix stock transfer entries having incorrect valuation" (cherry picked from commit 728dc1acf4db979e1dde7d950279d8335028e1a7) --- erpnext/hooks.py | 1 - .../stock/doctype/stock_entry/stock_entry.py | 73 +------------------ .../doctype/stock_entry/test_stock_entry.py | 42 +---------- 3 files changed, 2 insertions(+), 114 deletions(-) diff --git a/erpnext/hooks.py b/erpnext/hooks.py index a2f87cb2ad8..6bc17a3675a 100644 --- a/erpnext/hooks.py +++ b/erpnext/hooks.py @@ -427,7 +427,6 @@ scheduler_events = { "erpnext.loan_management.doctype.process_loan_security_shortfall.process_loan_security_shortfall.create_process_loan_security_shortfall", "erpnext.loan_management.doctype.process_loan_interest_accrual.process_loan_interest_accrual.process_loan_interest_accrual_for_term_loans", "erpnext.crm.utils.open_leads_opportunities_based_on_todays_event", - "erpnext.stock.doctype.stock_entry.stock_entry.audit_incorrect_valuation_entries", ], "monthly_long": [ "erpnext.accounts.deferred_revenue.process_deferred_accounting", diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.py b/erpnext/stock/doctype/stock_entry/stock_entry.py index 0d8675a1f53..fc3a50ededb 100644 --- a/erpnext/stock/doctype/stock_entry/stock_entry.py +++ b/erpnext/stock/doctype/stock_entry/stock_entry.py @@ -4,24 +4,12 @@ import json from collections import defaultdict -from typing import Dict import frappe from frappe import _ from frappe.model.mapper import get_mapped_doc from frappe.query_builder.functions import Sum -from frappe.utils import ( - add_days, - cint, - comma_or, - cstr, - flt, - format_time, - formatdate, - getdate, - nowdate, - today, -) +from frappe.utils import cint, comma_or, cstr, flt, format_time, formatdate, getdate, nowdate import erpnext from erpnext.accounts.general_ledger import process_gl_map @@ -2715,62 +2703,3 @@ def get_stock_entry_data(work_order): ) .orderby(stock_entry.creation, stock_entry_detail.item_code, stock_entry_detail.idx) ).run(as_dict=1) - - -def audit_incorrect_valuation_entries(): - # Audit of stock transfer entries having incorrect valuation - from erpnext.controllers.stock_controller import create_repost_item_valuation_entry - - stock_entries = get_incorrect_stock_entries() - - for stock_entry, values in stock_entries.items(): - reposting_data = frappe._dict( - { - "posting_date": values.posting_date, - "posting_time": values.posting_time, - "voucher_type": "Stock Entry", - "voucher_no": stock_entry, - "company": values.company, - } - ) - - create_repost_item_valuation_entry(reposting_data) - - -def get_incorrect_stock_entries() -> Dict: - stock_entry = frappe.qb.DocType("Stock Entry") - stock_ledger_entry = frappe.qb.DocType("Stock Ledger Entry") - transfer_purposes = [ - "Material Transfer", - "Material Transfer for Manufacture", - "Send to Subcontractor", - ] - - query = ( - frappe.qb.from_(stock_entry) - .inner_join(stock_ledger_entry) - .on(stock_entry.name == stock_ledger_entry.voucher_no) - .select( - stock_entry.name, - stock_entry.company, - stock_entry.posting_date, - stock_entry.posting_time, - Sum(stock_ledger_entry.stock_value_difference).as_("stock_value"), - ) - .where( - (stock_entry.docstatus == 1) - & (stock_entry.purpose.isin(transfer_purposes)) - & (stock_ledger_entry.modified > add_days(today(), -2)) - ) - .groupby(stock_ledger_entry.voucher_detail_no) - .having(Sum(stock_ledger_entry.stock_value_difference) != 0) - ) - - data = query.run(as_dict=True) - stock_entries = {} - - for row in data: - if abs(row.stock_value) > 0.1 and row.name not in stock_entries: - stock_entries.setdefault(row.name, row) - - return stock_entries diff --git a/erpnext/stock/doctype/stock_entry/test_stock_entry.py b/erpnext/stock/doctype/stock_entry/test_stock_entry.py index 680d209735e..b574b718fe1 100644 --- a/erpnext/stock/doctype/stock_entry/test_stock_entry.py +++ b/erpnext/stock/doctype/stock_entry/test_stock_entry.py @@ -5,7 +5,7 @@ import frappe from frappe.permissions import add_user_permission, remove_user_permission from frappe.tests.utils import FrappeTestCase, change_settings -from frappe.utils import add_days, flt, now, nowdate, nowtime, today +from frappe.utils import add_days, flt, nowdate, nowtime, today from erpnext.accounts.doctype.account.test_account import get_inventory_account from erpnext.stock.doctype.item.test_item import ( @@ -17,8 +17,6 @@ from erpnext.stock.doctype.item.test_item import ( from erpnext.stock.doctype.serial_no.serial_no import * # noqa from erpnext.stock.doctype.stock_entry.stock_entry import ( FinishedGoodError, - audit_incorrect_valuation_entries, - get_incorrect_stock_entries, move_sample_to_retention_warehouse, ) from erpnext.stock.doctype.stock_entry.stock_entry_utils import make_stock_entry @@ -1616,44 +1614,6 @@ class TestStockEntry(FrappeTestCase): self.assertRaises(BatchExpiredError, se.save) - def test_audit_incorrect_stock_entries(self): - item_code = "Test Incorrect Valuation Rate Item - 001" - create_item(item_code=item_code, is_stock_item=1) - - make_stock_entry( - item_code=item_code, - purpose="Material Receipt", - posting_date=add_days(nowdate(), -10), - qty=2, - rate=500, - to_warehouse="_Test Warehouse - _TC", - ) - - transfer_entry = make_stock_entry( - item_code=item_code, - purpose="Material Transfer", - qty=2, - rate=500, - from_warehouse="_Test Warehouse - _TC", - to_warehouse="_Test Warehouse 1 - _TC", - ) - - sle_name = frappe.db.get_value( - "Stock Ledger Entry", {"voucher_no": transfer_entry.name, "actual_qty": (">", 0)}, "name" - ) - - frappe.db.set_value( - "Stock Ledger Entry", sle_name, {"modified": add_days(now(), -1), "stock_value_difference": 10} - ) - - stock_entries = get_incorrect_stock_entries() - self.assertTrue(transfer_entry.name in stock_entries) - - audit_incorrect_valuation_entries() - - stock_entries = get_incorrect_stock_entries() - self.assertFalse(transfer_entry.name in stock_entries) - def make_serialized_item(**args): args = frappe._dict(args) From ba5a149a6b75280ba0984a1435dc92a7af327c39 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Fri, 30 Dec 2022 16:00:45 +0530 Subject: [PATCH 19/30] fix: Conversion factor error for invoices without item code (petty expenses) (#32714) fix: Conversion factor error for invoices without item code (petty expenses) (#32714) * fix: Set default uom conversion factor to 1 for invoices * chore: set default conversion_factor as 1 * chore: remove print statements (cherry picked from commit 617518389ac0c6459197e27f94efbcb14d409dbf) Co-authored-by: Deepesh Garg --- .../doctype/sales_invoice_item/sales_invoice_item.json | 2 +- erpnext/controllers/accounts_controller.py | 7 ++++++- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/erpnext/accounts/doctype/sales_invoice_item/sales_invoice_item.json b/erpnext/accounts/doctype/sales_invoice_item/sales_invoice_item.json index 77055f94455..f4f083068ee 100644 --- a/erpnext/accounts/doctype/sales_invoice_item/sales_invoice_item.json +++ b/erpnext/accounts/doctype/sales_invoice_item/sales_invoice_item.json @@ -886,7 +886,7 @@ "idx": 1, "istable": 1, "links": [], - "modified": "2022-11-02 12:53:12.693217", + "modified": "2022-12-28 16:17:33.484531", "modified_by": "Administrator", "module": "Accounts", "name": "Sales Invoice Item", diff --git a/erpnext/controllers/accounts_controller.py b/erpnext/controllers/accounts_controller.py index 334a2d806d6..788dc4982e5 100644 --- a/erpnext/controllers/accounts_controller.py +++ b/erpnext/controllers/accounts_controller.py @@ -584,7 +584,12 @@ class AccountsController(TransactionBase): if bool(uom) != bool(stock_uom): # xor item.stock_uom = item.uom = uom or stock_uom - item.conversion_factor = get_uom_conv_factor(item.get("uom"), item.get("stock_uom")) + # UOM cannot be zero so substitute as 1 + item.conversion_factor = ( + get_uom_conv_factor(item.get("uom"), item.get("stock_uom")) + or item.get("conversion_factor") + or 1 + ) if self.doctype == "Purchase Invoice": self.set_expense_account(for_validate) From d03085259d1f8e7f21256451be076ccd9134718f Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Mon, 2 Jan 2023 08:52:23 +0530 Subject: [PATCH 20/30] fix: Multi-currency issues in Bank Reconciliation Tool (#33488) fix: Multi-currency issues in Bank Recociliation Tool (cherry picked from commit ad53ecf2b48a808dc98f2834dbbfb9c5ebf73c72) Co-authored-by: Deepesh Garg --- .../bank_reconciliation_tool.py | 2 +- .../doctype/bank_transaction/bank_transaction.py | 2 +- .../js/bank_reconciliation_tool/dialog_manager.js | 12 ++++++++++++ 3 files changed, 14 insertions(+), 2 deletions(-) diff --git a/erpnext/accounts/doctype/bank_reconciliation_tool/bank_reconciliation_tool.py b/erpnext/accounts/doctype/bank_reconciliation_tool/bank_reconciliation_tool.py index cc3727ce837..8b496e5ea33 100644 --- a/erpnext/accounts/doctype/bank_reconciliation_tool/bank_reconciliation_tool.py +++ b/erpnext/accounts/doctype/bank_reconciliation_tool/bank_reconciliation_tool.py @@ -298,7 +298,7 @@ def reconcile_vouchers(bank_transaction_name, vouchers): dict( account=account, voucher_type=voucher["payment_doctype"], voucher_no=voucher["payment_name"] ), - ["credit", "debit"], + ["credit_in_account_currency as credit", "debit_in_account_currency as debit"], as_dict=1, ) gl_amount, transaction_amount = ( diff --git a/erpnext/accounts/doctype/bank_transaction/bank_transaction.py b/erpnext/accounts/doctype/bank_transaction/bank_transaction.py index a7885143353..9b36c93a0f3 100644 --- a/erpnext/accounts/doctype/bank_transaction/bank_transaction.py +++ b/erpnext/accounts/doctype/bank_transaction/bank_transaction.py @@ -137,7 +137,7 @@ def get_paid_amount(payment_entry, currency, bank_account): ) elif doc.payment_type == "Pay": paid_amount_field = ( - "paid_amount" if doc.paid_to_account_currency == currency else "base_paid_amount" + "paid_amount" if doc.paid_from_account_currency == currency else "base_paid_amount" ) return frappe.db.get_value( diff --git a/erpnext/public/js/bank_reconciliation_tool/dialog_manager.js b/erpnext/public/js/bank_reconciliation_tool/dialog_manager.js index ca01f68140c..b5e6ab871d1 100644 --- a/erpnext/public/js/bank_reconciliation_tool/dialog_manager.js +++ b/erpnext/public/js/bank_reconciliation_tool/dialog_manager.js @@ -355,12 +355,14 @@ erpnext.accounts.bank_reconciliation.DialogManager = class DialogManager { fieldname: "deposit", fieldtype: "Currency", label: "Deposit", + options: "currency", read_only: 1, }, { fieldname: "withdrawal", fieldtype: "Currency", label: "Withdrawal", + options: "currency", read_only: 1, }, { @@ -378,6 +380,7 @@ erpnext.accounts.bank_reconciliation.DialogManager = class DialogManager { fieldname: "allocated_amount", fieldtype: "Currency", label: "Allocated Amount", + options: "Currency", read_only: 1, }, @@ -385,8 +388,17 @@ erpnext.accounts.bank_reconciliation.DialogManager = class DialogManager { fieldname: "unallocated_amount", fieldtype: "Currency", label: "Unallocated Amount", + options: "Currency", read_only: 1, }, + { + fieldname: "currency", + fieldtype: "Link", + label: "Currency", + options: "Currency", + read_only: 1, + hidden: 1, + } ]; } From 55e8e45d52970cefb5d7ce63d6cbbc16b73d46c8 Mon Sep 17 00:00:00 2001 From: Devin Slauenwhite Date: Sat, 31 Dec 2022 14:14:25 -0500 Subject: [PATCH 21/30] fix: add missing 'ordered_qty' to get_bin_details (cherry picked from commit 8d62cdfd5f683c20c8ac0bd06e2b40b86579fd56) --- erpnext/stock/get_item_details.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/erpnext/stock/get_item_details.py b/erpnext/stock/get_item_details.py index 1741d654601..02456f3d295 100644 --- a/erpnext/stock/get_item_details.py +++ b/erpnext/stock/get_item_details.py @@ -1175,10 +1175,10 @@ def get_bin_details(item_code, warehouse, company=None): bin_details = frappe.db.get_value( "Bin", {"item_code": item_code, "warehouse": warehouse}, - ["projected_qty", "actual_qty", "reserved_qty"], + ["projected_qty", "actual_qty", "reserved_qty", "ordered_qty"], as_dict=True, cache=True, - ) or {"projected_qty": 0, "actual_qty": 0, "reserved_qty": 0} + ) or {"projected_qty": 0, "actual_qty": 0, "reserved_qty": 0, "ordered_qty": 0} if company: bin_details["company_total_stock"] = get_company_total_stock(item_code, company) return bin_details From 6ec8088eb32b48e488d49ae743f78c7627e93fd1 Mon Sep 17 00:00:00 2001 From: Devin Slauenwhite Date: Sat, 31 Dec 2022 15:41:07 -0500 Subject: [PATCH 22/30] test: get_item_details contains bin details (cherry picked from commit 239a5f8bf4c6dda9a955dc6472d9387d80afe619) --- erpnext/stock/doctype/item/test_item.py | 20 +++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/erpnext/stock/doctype/item/test_item.py b/erpnext/stock/doctype/item/test_item.py index e1ee9389de9..7e426ae4af8 100644 --- a/erpnext/stock/doctype/item/test_item.py +++ b/erpnext/stock/doctype/item/test_item.py @@ -83,6 +83,7 @@ class TestItem(FrappeTestCase): def test_get_item_details(self): # delete modified item price record and make as per test_records frappe.db.sql("""delete from `tabItem Price`""") + frappe.db.sql("""delete from `tabBin`""") to_check = { "item_code": "_Test Item", @@ -103,9 +104,26 @@ class TestItem(FrappeTestCase): "batch_no": None, "uom": "_Test UOM", "conversion_factor": 1.0, + "reserved_qty": 1, + "actual_qty": 5, + "ordered_qty": 10, + "projected_qty": 14, } make_test_objects("Item Price") + make_test_objects( + "Bin", + [ + { + "item_code": "_Test Item", + "warehouse": "_Test Warehouse - _TC", + "reserved_qty": 1, + "actual_qty": 5, + "ordered_qty": 10, + "projected_qty": 14, + } + ], + ) company = "_Test Company" currency = frappe.get_cached_value("Company", company, "default_currency") @@ -129,7 +147,7 @@ class TestItem(FrappeTestCase): ) for key, value in to_check.items(): - self.assertEqual(value, details.get(key)) + self.assertEqual(value, details.get(key), key) def test_item_tax_template(self): expected_item_tax_template = [ From e2964088b76bbd6dbbd6c016d4dad4850d33b322 Mon Sep 17 00:00:00 2001 From: s-aga-r Date: Thu, 29 Dec 2022 12:34:18 +0530 Subject: [PATCH 23/30] fix: consider child nodes while getting bin details (cherry picked from commit c716dcc01e1b29779dac28b4d531b3fa3ee27143) --- erpnext/controllers/selling_controller.py | 2 +- erpnext/public/js/controllers/buying.js | 3 +- erpnext/stock/get_item_details.py | 38 ++++++++++++++++------- 3 files changed, 30 insertions(+), 13 deletions(-) diff --git a/erpnext/controllers/selling_controller.py b/erpnext/controllers/selling_controller.py index 0ebc8d4b4de..dec3b750ebc 100644 --- a/erpnext/controllers/selling_controller.py +++ b/erpnext/controllers/selling_controller.py @@ -26,7 +26,7 @@ class SellingController(StockController): super(SellingController, self).onload() if self.doctype in ("Sales Order", "Delivery Note", "Sales Invoice"): for item in self.get("items"): - item.update(get_bin_details(item.item_code, item.warehouse)) + item.update(get_bin_details(item.item_code, item.warehouse, include_child_warehouses=True)) def validate(self): super(SellingController, self).validate() diff --git a/erpnext/public/js/controllers/buying.js b/erpnext/public/js/controllers/buying.js index 09779d89ec1..b0e08cc6f26 100644 --- a/erpnext/public/js/controllers/buying.js +++ b/erpnext/public/js/controllers/buying.js @@ -225,7 +225,8 @@ erpnext.buying.BuyingController = class BuyingController extends erpnext.Transac args: { item_code: item.item_code, warehouse: item.warehouse, - company: doc.company + company: doc.company, + include_child_warehouses: true } }); } diff --git a/erpnext/stock/get_item_details.py b/erpnext/stock/get_item_details.py index 1741d654601..dc00999b463 100644 --- a/erpnext/stock/get_item_details.py +++ b/erpnext/stock/get_item_details.py @@ -102,9 +102,11 @@ def get_item_details(args, doc=None, for_validate=False, overwrite_warehouse=Tru elif out.get("warehouse"): if doc and doc.get("doctype") == "Purchase Order": # calculate company_total_stock only for po - bin_details = get_bin_details(args.item_code, out.warehouse, args.company) + bin_details = get_bin_details( + args.item_code, out.warehouse, args.company, include_child_warehouses=True + ) else: - bin_details = get_bin_details(args.item_code, out.warehouse) + bin_details = get_bin_details(args.item_code, out.warehouse, include_child_warehouses=True) out.update(bin_details) @@ -1060,7 +1062,9 @@ def get_pos_profile_item_details(company, args, pos_profile=None, update_data=Fa res[fieldname] = pos_profile.get(fieldname) if res.get("warehouse"): - res.actual_qty = get_bin_details(args.item_code, res.warehouse).get("actual_qty") + res.actual_qty = get_bin_details( + args.item_code, res.warehouse, include_child_warehouses=True + ).get("actual_qty") return res @@ -1171,14 +1175,26 @@ def get_projected_qty(item_code, warehouse): @frappe.whitelist() -def get_bin_details(item_code, warehouse, company=None): - bin_details = frappe.db.get_value( - "Bin", - {"item_code": item_code, "warehouse": warehouse}, - ["projected_qty", "actual_qty", "reserved_qty"], - as_dict=True, - cache=True, - ) or {"projected_qty": 0, "actual_qty": 0, "reserved_qty": 0} +def get_bin_details(item_code, warehouse, company=None, include_child_warehouses=False): + bin_details = {"projected_qty": 0, "actual_qty": 0, "reserved_qty": 0} + + if warehouse: + from erpnext.stock.doctype.warehouse.warehouse import get_child_warehouses + + warehouses = get_child_warehouses(warehouse) if include_child_warehouses else [warehouse] + bin_details = frappe.db.get_value( + "Bin", + filters={"item_code": item_code, "warehouse": ["in", warehouses]}, + fieldname=[ + "sum(projected_qty) as projected_qty", + "sum(actual_qty) as actual_qty", + "sum(reserved_qty) as reserved_qty", + ], + as_dict=True, + cache=True, + ) + bin_details = {k: 0 if not v else v for k, v in bin_details.items()} + if company: bin_details["company_total_stock"] = get_company_total_stock(item_code, company) return bin_details From ec538b27fe89df5828aef8dae056c88c7d47b0ce Mon Sep 17 00:00:00 2001 From: s-aga-r Date: Thu, 29 Dec 2022 16:38:08 +0530 Subject: [PATCH 24/30] chore: use `frappe.qb` instead of `frappe.db.get_value` (cherry picked from commit c3911a592a51a390c0b426707f7ec777686cb621) --- erpnext/stock/get_item_details.py | 25 +++++++++++++------------ 1 file changed, 13 insertions(+), 12 deletions(-) diff --git a/erpnext/stock/get_item_details.py b/erpnext/stock/get_item_details.py index dc00999b463..61528809857 100644 --- a/erpnext/stock/get_item_details.py +++ b/erpnext/stock/get_item_details.py @@ -1179,21 +1179,22 @@ def get_bin_details(item_code, warehouse, company=None, include_child_warehouses bin_details = {"projected_qty": 0, "actual_qty": 0, "reserved_qty": 0} if warehouse: + from frappe.query_builder.functions import Coalesce, Sum + from erpnext.stock.doctype.warehouse.warehouse import get_child_warehouses warehouses = get_child_warehouses(warehouse) if include_child_warehouses else [warehouse] - bin_details = frappe.db.get_value( - "Bin", - filters={"item_code": item_code, "warehouse": ["in", warehouses]}, - fieldname=[ - "sum(projected_qty) as projected_qty", - "sum(actual_qty) as actual_qty", - "sum(reserved_qty) as reserved_qty", - ], - as_dict=True, - cache=True, - ) - bin_details = {k: 0 if not v else v for k, v in bin_details.items()} + + bin = frappe.qb.DocType("Bin") + bin_details = ( + frappe.qb.from_(bin) + .select( + Coalesce(Sum(bin.projected_qty), 0).as_("projected_qty"), + Coalesce(Sum(bin.actual_qty), 0).as_("actual_qty"), + Coalesce(Sum(bin.reserved_qty), 0).as_("reserved_qty"), + ) + .where((bin.item_code == item_code) & (bin.warehouse.isin(warehouses))) + ).run(as_dict=True)[0] if company: bin_details["company_total_stock"] = get_company_total_stock(item_code, company) From 648e979eb2bf7deca120716376f088b901016b33 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Mon, 2 Jan 2023 16:37:53 +0530 Subject: [PATCH 25/30] refactor: Exchange rate revaluation to handle accounts with zero account balance (#33165) refactor: Exchange rate revaluation to handle accounts with zero account balance (#33165) * refactor: new type for JE - Exchange Gain or Loss * refactor: skip few validations for Exchanage Gain Or Loss type Jour * refactor: ERR create 2 journals for handling zero and non-zero compa 1. Additional check box accounts table to identify accounts with zero balance 2. Accounts with zero balance only in either of the 2 currencies will be handled on separate Journal * refactor: skips few validation for allowing 0 debit/credit * fix: General Ledger presentaion currency * test: fix test case in general ledger * test: fix failing test case in AR report (cherry picked from commit 914b23038ce6e0c75f90e87647246a5c0dd99568) Co-authored-by: ruthra kumar --- .../exchange_rate_revaluation.js | 18 +- .../exchange_rate_revaluation.json | 38 +- .../exchange_rate_revaluation.py | 513 ++++++++++++++---- .../exchange_rate_revaluation_account.json | 51 +- erpnext/accounts/doctype/gl_entry/gl_entry.py | 10 +- .../doctype/journal_entry/journal_entry.json | 4 +- .../doctype/journal_entry/journal_entry.py | 37 +- erpnext/accounts/general_ledger.py | 24 +- .../test_accounts_receivable.py | 10 +- .../general_ledger/test_general_ledger.py | 3 +- erpnext/accounts/report/utils.py | 7 +- 11 files changed, 552 insertions(+), 163 deletions(-) diff --git a/erpnext/accounts/doctype/exchange_rate_revaluation/exchange_rate_revaluation.js b/erpnext/accounts/doctype/exchange_rate_revaluation/exchange_rate_revaluation.js index 926a442f808..f72ecc9e501 100644 --- a/erpnext/accounts/doctype/exchange_rate_revaluation/exchange_rate_revaluation.js +++ b/erpnext/accounts/doctype/exchange_rate_revaluation/exchange_rate_revaluation.js @@ -26,7 +26,7 @@ frappe.ui.form.on('Exchange Rate Revaluation', { doc: frm.doc, callback: function(r) { if (r.message) { - frm.add_custom_button(__('Journal Entry'), function() { + frm.add_custom_button(__('Journal Entries'), function() { return frm.events.make_jv(frm); }, __('Create')); } @@ -35,10 +35,11 @@ frappe.ui.form.on('Exchange Rate Revaluation', { } }, - get_entries: function(frm) { + get_entries: function(frm, account) { frappe.call({ method: "get_accounts_data", doc: cur_frm.doc, + account: account, callback: function(r){ frappe.model.clear_table(frm.doc, "accounts"); if(r.message) { @@ -57,7 +58,6 @@ frappe.ui.form.on('Exchange Rate Revaluation', { let total_gain_loss = 0; frm.doc.accounts.forEach((d) => { - d.gain_loss = flt(d.new_balance_in_base_currency, precision("new_balance_in_base_currency", d)) - flt(d.balance_in_base_currency, precision("balance_in_base_currency", d)); total_gain_loss += flt(d.gain_loss, precision("gain_loss", d)); }); @@ -66,13 +66,19 @@ frappe.ui.form.on('Exchange Rate Revaluation', { }, make_jv : function(frm) { + let revaluation_journal = null; + let zero_balance_journal = null; frappe.call({ - method: "make_jv_entry", + method: "make_jv_entries", doc: frm.doc, + freeze: true, + freeze_message: "Making Journal Entries...", callback: function(r){ if (r.message) { - var doc = frappe.model.sync(r.message)[0]; - frappe.set_route("Form", doc.doctype, doc.name); + let response = r.message; + if(response['revaluation_jv'] || response['zero_balance_jv']) { + frappe.msgprint(__("Journals have been created")); + } } } }); diff --git a/erpnext/accounts/doctype/exchange_rate_revaluation/exchange_rate_revaluation.json b/erpnext/accounts/doctype/exchange_rate_revaluation/exchange_rate_revaluation.json index e00b17e5a53..0d198ca1201 100644 --- a/erpnext/accounts/doctype/exchange_rate_revaluation/exchange_rate_revaluation.json +++ b/erpnext/accounts/doctype/exchange_rate_revaluation/exchange_rate_revaluation.json @@ -14,6 +14,9 @@ "get_entries", "accounts", "section_break_6", + "gain_loss_unbooked", + "gain_loss_booked", + "column_break_10", "total_gain_loss", "amended_from" ], @@ -59,13 +62,6 @@ "fieldname": "section_break_6", "fieldtype": "Section Break" }, - { - "fieldname": "total_gain_loss", - "fieldtype": "Currency", - "label": "Total Gain/Loss", - "options": "Company:company:default_currency", - "read_only": 1 - }, { "fieldname": "amended_from", "fieldtype": "Link", @@ -74,11 +70,37 @@ "options": "Exchange Rate Revaluation", "print_hide": 1, "read_only": 1 + }, + { + "fieldname": "gain_loss_unbooked", + "fieldtype": "Currency", + "label": "Gain/Loss from Revaluation", + "options": "Company:company:default_currency", + "read_only": 1 + }, + { + "description": "Gain/Loss accumulated in foreign currency account. Accounts with '0' balance in either Base or Account currency", + "fieldname": "gain_loss_booked", + "fieldtype": "Currency", + "label": "Gain/Loss already booked", + "options": "Company:company:default_currency", + "read_only": 1 + }, + { + "fieldname": "total_gain_loss", + "fieldtype": "Currency", + "label": "Total Gain/Loss", + "options": "Company:company:default_currency", + "read_only": 1 + }, + { + "fieldname": "column_break_10", + "fieldtype": "Column Break" } ], "is_submittable": 1, "links": [], - "modified": "2022-11-17 10:28:03.911554", + "modified": "2022-12-29 19:38:24.416529", "modified_by": "Administrator", "module": "Accounts", "name": "Exchange Rate Revaluation", diff --git a/erpnext/accounts/doctype/exchange_rate_revaluation/exchange_rate_revaluation.py b/erpnext/accounts/doctype/exchange_rate_revaluation/exchange_rate_revaluation.py index c907a25e14d..d67d59b5d45 100644 --- a/erpnext/accounts/doctype/exchange_rate_revaluation/exchange_rate_revaluation.py +++ b/erpnext/accounts/doctype/exchange_rate_revaluation/exchange_rate_revaluation.py @@ -3,10 +3,12 @@ import frappe -from frappe import _ +from frappe import _, qb from frappe.model.document import Document from frappe.model.meta import get_field_precision -from frappe.utils import flt +from frappe.query_builder import Criterion, Order +from frappe.query_builder.functions import NullIf, Sum +from frappe.utils import flt, get_link_to_form import erpnext from erpnext.accounts.doctype.journal_entry.journal_entry import get_balance_on @@ -19,11 +21,25 @@ class ExchangeRateRevaluation(Document): def set_total_gain_loss(self): total_gain_loss = 0 + + gain_loss_booked = 0 + gain_loss_unbooked = 0 + for d in self.accounts: - d.gain_loss = flt( - d.new_balance_in_base_currency, d.precision("new_balance_in_base_currency") - ) - flt(d.balance_in_base_currency, d.precision("balance_in_base_currency")) + if not d.zero_balance: + d.gain_loss = flt( + d.new_balance_in_base_currency, d.precision("new_balance_in_base_currency") + ) - flt(d.balance_in_base_currency, d.precision("balance_in_base_currency")) + + if d.zero_balance: + gain_loss_booked += flt(d.gain_loss, d.precision("gain_loss")) + else: + gain_loss_unbooked += flt(d.gain_loss, d.precision("gain_loss")) + total_gain_loss += flt(d.gain_loss, d.precision("gain_loss")) + + self.gain_loss_booked = gain_loss_booked + self.gain_loss_unbooked = gain_loss_unbooked self.total_gain_loss = flt(total_gain_loss, self.precision("total_gain_loss")) def validate_mandatory(self): @@ -35,98 +51,206 @@ class ExchangeRateRevaluation(Document): @frappe.whitelist() def check_journal_entry_condition(self): - total_debit = frappe.db.get_value( - "Journal Entry Account", - {"reference_type": "Exchange Rate Revaluation", "reference_name": self.name, "docstatus": 1}, - "sum(debit) as sum", + exchange_gain_loss_account = self.get_for_unrealized_gain_loss_account() + + jea = qb.DocType("Journal Entry Account") + journals = ( + qb.from_(jea) + .select(jea.parent) + .distinct() + .where( + (jea.reference_type == "Exchange Rate Revaluation") + & (jea.reference_name == self.name) + & (jea.docstatus == 1) + ) + .run() ) - total_amt = 0 - for d in self.accounts: - total_amt = total_amt + d.new_balance_in_base_currency + if journals: + gle = qb.DocType("GL Entry") + total_amt = ( + qb.from_(gle) + .select((Sum(gle.credit) - Sum(gle.debit)).as_("total_amount")) + .where( + (gle.voucher_type == "Journal Entry") + & (gle.voucher_no.isin(journals)) + & (gle.account == exchange_gain_loss_account) + & (gle.is_cancelled == 0) + ) + .run() + ) - if total_amt != total_debit: - return True + if total_amt and total_amt[0][0] != self.total_gain_loss: + return True + else: + return False - return False + return True @frappe.whitelist() - def get_accounts_data(self, account=None): - accounts = [] + def get_accounts_data(self): self.validate_mandatory() - company_currency = erpnext.get_company_currency(self.company) + account_details = self.get_account_balance_from_gle( + company=self.company, posting_date=self.posting_date, account=None, party_type=None, party=None + ) + accounts_with_new_balance = self.calculate_new_account_balance( + self.company, self.posting_date, account_details + ) + + if not accounts_with_new_balance: + self.throw_invalid_response_message(account_details) + + return accounts_with_new_balance + + @staticmethod + def get_account_balance_from_gle(company, posting_date, account, party_type, party): + account_details = [] + + if company and posting_date: + company_currency = erpnext.get_company_currency(company) + + acc = qb.DocType("Account") + if account: + accounts = [account] + else: + res = ( + qb.from_(acc) + .select(acc.name) + .where( + (acc.is_group == 0) + & (acc.report_type == "Balance Sheet") + & (acc.root_type.isin(["Asset", "Liability", "Equity"])) + & (acc.account_type != "Stock") + & (acc.company == company) + & (acc.account_currency != company_currency) + ) + .orderby(acc.name) + .run(as_list=True) + ) + accounts = [x[0] for x in res] + + if accounts: + having_clause = (qb.Field("balance") != qb.Field("balance_in_account_currency")) & ( + (qb.Field("balance_in_account_currency") != 0) | (qb.Field("balance") != 0) + ) + + gle = qb.DocType("GL Entry") + + # conditions + conditions = [] + conditions.append(gle.account.isin(accounts)) + conditions.append(gle.posting_date.lte(posting_date)) + conditions.append(gle.is_cancelled == 0) + + if party_type: + conditions.append(gle.party_type == party_type) + if party: + conditions.append(gle.party == party) + + account_details = ( + qb.from_(gle) + .select( + gle.account, + gle.party_type, + gle.party, + gle.account_currency, + (Sum(gle.debit_in_account_currency) - Sum(gle.credit_in_account_currency)).as_( + "balance_in_account_currency" + ), + (Sum(gle.debit) - Sum(gle.credit)).as_("balance"), + (Sum(gle.debit) - Sum(gle.credit) == 0) + ^ (Sum(gle.debit_in_account_currency) - Sum(gle.credit_in_account_currency) == 0).as_( + "zero_balance" + ), + ) + .where(Criterion.all(conditions)) + .groupby(gle.account, NullIf(gle.party_type, ""), NullIf(gle.party, "")) + .having(having_clause) + .orderby(gle.account) + .run(as_dict=True) + ) + + return account_details + + @staticmethod + def calculate_new_account_balance(company, posting_date, account_details): + accounts = [] + company_currency = erpnext.get_company_currency(company) precision = get_field_precision( frappe.get_meta("Exchange Rate Revaluation Account").get_field("new_balance_in_base_currency"), company_currency, ) - account_details = self.get_accounts_from_gle() - for d in account_details: - current_exchange_rate = ( - d.balance / d.balance_in_account_currency if d.balance_in_account_currency else 0 - ) - new_exchange_rate = get_exchange_rate(d.account_currency, company_currency, self.posting_date) - new_balance_in_base_currency = flt(d.balance_in_account_currency * new_exchange_rate) - gain_loss = flt(new_balance_in_base_currency, precision) - flt(d.balance, precision) - if gain_loss: - accounts.append( - { - "account": d.account, - "party_type": d.party_type, - "party": d.party, - "account_currency": d.account_currency, - "balance_in_base_currency": d.balance, - "balance_in_account_currency": d.balance_in_account_currency, - "current_exchange_rate": current_exchange_rate, - "new_exchange_rate": new_exchange_rate, - "new_balance_in_base_currency": new_balance_in_base_currency, - } + if account_details: + # Handle Accounts with balance in both Account/Base Currency + for d in [x for x in account_details if not x.zero_balance]: + current_exchange_rate = ( + d.balance / d.balance_in_account_currency if d.balance_in_account_currency else 0 ) + new_exchange_rate = get_exchange_rate(d.account_currency, company_currency, posting_date) + new_balance_in_base_currency = flt(d.balance_in_account_currency * new_exchange_rate) + gain_loss = flt(new_balance_in_base_currency, precision) - flt(d.balance, precision) + if gain_loss: + accounts.append( + { + "account": d.account, + "party_type": d.party_type, + "party": d.party, + "account_currency": d.account_currency, + "balance_in_base_currency": d.balance, + "balance_in_account_currency": d.balance_in_account_currency, + "zero_balance": d.zero_balance, + "current_exchange_rate": current_exchange_rate, + "new_exchange_rate": new_exchange_rate, + "new_balance_in_base_currency": new_balance_in_base_currency, + "new_balance_in_account_currency": d.balance_in_account_currency, + "gain_loss": gain_loss, + } + ) - if not accounts: - self.throw_invalid_response_message(account_details) + # Handle Accounts with '0' balance in Account/Base Currency + for d in [x for x in account_details if x.zero_balance]: + + # TODO: Set new balance in Base/Account currency + if d.balance > 0: + current_exchange_rate = new_exchange_rate = 0 + + new_balance_in_account_currency = 0 # this will be '0' + new_balance_in_base_currency = 0 # this will be '0' + gain_loss = flt(new_balance_in_base_currency, precision) - flt(d.balance, precision) + else: + new_exchange_rate = 0 + new_balance_in_base_currency = 0 + new_balance_in_account_currency = 0 + + current_exchange_rate = calculate_exchange_rate_using_last_gle( + company, d.account, d.party_type, d.party + ) + + gain_loss = new_balance_in_account_currency - ( + current_exchange_rate * d.balance_in_account_currency + ) + + if gain_loss: + accounts.append( + { + "account": d.account, + "party_type": d.party_type, + "party": d.party, + "account_currency": d.account_currency, + "balance_in_base_currency": d.balance, + "balance_in_account_currency": d.balance_in_account_currency, + "zero_balance": d.zero_balance, + "current_exchange_rate": current_exchange_rate, + "new_exchange_rate": new_exchange_rate, + "new_balance_in_base_currency": new_balance_in_base_currency, + "new_balance_in_account_currency": new_balance_in_account_currency, + "gain_loss": gain_loss, + } + ) return accounts - def get_accounts_from_gle(self): - company_currency = erpnext.get_company_currency(self.company) - accounts = frappe.db.sql_list( - """ - select name - from tabAccount - where is_group = 0 - and report_type = 'Balance Sheet' - and root_type in ('Asset', 'Liability', 'Equity') - and account_type != 'Stock' - and company=%s - and account_currency != %s - order by name""", - (self.company, company_currency), - ) - - account_details = [] - if accounts: - account_details = frappe.db.sql( - """ - select - account, party_type, party, account_currency, - sum(debit_in_account_currency) - sum(credit_in_account_currency) as balance_in_account_currency, - sum(debit) - sum(credit) as balance - from `tabGL Entry` - where account in (%s) - and posting_date <= %s - and is_cancelled = 0 - group by account, NULLIF(party_type,''), NULLIF(party,'') - having sum(debit) != sum(credit) - order by account - """ - % (", ".join(["%s"] * len(accounts)), "%s"), - tuple(accounts + [self.posting_date]), - as_dict=1, - ) - - return account_details - def throw_invalid_response_message(self, account_details): if account_details: message = _("No outstanding invoices require exchange rate revaluation") @@ -134,11 +258,7 @@ class ExchangeRateRevaluation(Document): message = _("No outstanding invoices found") frappe.msgprint(message) - @frappe.whitelist() - def make_jv_entry(self): - if self.total_gain_loss == 0: - return - + def get_for_unrealized_gain_loss_account(self): unrealized_exchange_gain_loss_account = frappe.get_cached_value( "Company", self.company, "unrealized_exchange_gain_loss_account" ) @@ -146,6 +266,130 @@ class ExchangeRateRevaluation(Document): frappe.throw( _("Please set Unrealized Exchange Gain/Loss Account in Company {0}").format(self.company) ) + return unrealized_exchange_gain_loss_account + + @frappe.whitelist() + def make_jv_entries(self): + zero_balance_jv = self.make_jv_for_zero_balance() + if zero_balance_jv: + frappe.msgprint( + f"Zero Balance Journal: {get_link_to_form('Journal Entry', zero_balance_jv.name)}" + ) + + revaluation_jv = self.make_jv_for_revaluation() + if revaluation_jv: + frappe.msgprint( + f"Revaluation Journal: {get_link_to_form('Journal Entry', revaluation_jv.name)}" + ) + + return { + "revaluation_jv": revaluation_jv.name if revaluation_jv else None, + "zero_balance_jv": zero_balance_jv.name if zero_balance_jv else None, + } + + def make_jv_for_zero_balance(self): + if self.gain_loss_booked == 0: + return + + accounts = [x for x in self.accounts if x.zero_balance] + + if not accounts: + return + + unrealized_exchange_gain_loss_account = self.get_for_unrealized_gain_loss_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 = self.posting_date + journal_entry.multi_currency = 1 + + journal_entry_accounts = [] + for d in accounts: + journal_account = frappe._dict( + { + "account": d.get("account"), + "party_type": d.get("party_type"), + "party": d.get("party"), + "account_currency": d.get("account_currency"), + "balance": flt( + d.get("balance_in_account_currency"), d.precision("balance_in_account_currency") + ), + "exchange_rate": 0, + "cost_center": erpnext.get_default_cost_center(self.company), + "reference_type": "Exchange Rate Revaluation", + "reference_name": self.name, + } + ) + + # Account Currency has balance + if d.get("balance_in_account_currency") and not d.get("new_balance_in_account_currency"): + dr_or_cr = ( + "credit_in_account_currency" + if d.get("balance_in_account_currency") > 0 + else "debit_in_account_currency" + ) + reverse_dr_or_cr = ( + "debit_in_account_currency" + if dr_or_cr == "credit_in_account_currency" + else "credit_in_account_currency" + ) + journal_account.update( + { + dr_or_cr: flt( + abs(d.get("balance_in_account_currency")), d.precision("balance_in_account_currency") + ), + reverse_dr_or_cr: 0, + "debit": 0, + "credit": 0, + } + ) + elif d.get("balance_in_base_currency") and not d.get("new_balance_in_base_currency"): + # Base currency has balance + dr_or_cr = "credit" if d.get("balance_in_base_currency") > 0 else "debit" + reverse_dr_or_cr = "debit" if dr_or_cr == "credit" else "credit" + journal_account.update( + { + dr_or_cr: flt( + abs(d.get("balance_in_base_currency")), d.precision("balance_in_base_currency") + ), + reverse_dr_or_cr: 0, + "debit_in_account_currency": 0, + "credit_in_account_currency": 0, + } + ) + + journal_entry_accounts.append(journal_account) + + journal_entry_accounts.append( + { + "account": unrealized_exchange_gain_loss_account, + "balance": get_balance_on(unrealized_exchange_gain_loss_account), + "debit": abs(self.gain_loss_booked) if self.gain_loss_booked < 0 else 0, + "credit": abs(self.gain_loss_booked) if self.gain_loss_booked > 0 else 0, + "debit_in_account_currency": abs(self.gain_loss_booked) if self.gain_loss_booked < 0 else 0, + "credit_in_account_currency": self.gain_loss_booked if self.gain_loss_booked > 0 else 0, + "cost_center": erpnext.get_default_cost_center(self.company), + "exchange_rate": 1, + "reference_type": "Exchange Rate Revaluation", + "reference_name": self.name, + } + ) + + journal_entry.set("accounts", journal_entry_accounts) + journal_entry.set_total_debit_credit() + journal_entry.save() + return journal_entry + + def make_jv_for_revaluation(self): + if self.gain_loss_unbooked == 0: + return + + accounts = [x for x in self.accounts if not x.zero_balance] + if not accounts: + return + + unrealized_exchange_gain_loss_account = self.get_for_unrealized_gain_loss_account() journal_entry = frappe.new_doc("Journal Entry") journal_entry.voucher_type = "Exchange Rate Revaluation" @@ -154,7 +398,7 @@ class ExchangeRateRevaluation(Document): journal_entry.multi_currency = 1 journal_entry_accounts = [] - for d in self.accounts: + for d in accounts: dr_or_cr = ( "debit_in_account_currency" if d.get("balance_in_account_currency") > 0 @@ -179,6 +423,7 @@ class ExchangeRateRevaluation(Document): dr_or_cr: flt( abs(d.get("balance_in_account_currency")), d.precision("balance_in_account_currency") ), + "cost_center": erpnext.get_default_cost_center(self.company), "exchange_rate": flt(d.get("new_exchange_rate"), d.precision("new_exchange_rate")), "reference_type": "Exchange Rate Revaluation", "reference_name": self.name, @@ -196,6 +441,7 @@ class ExchangeRateRevaluation(Document): reverse_dr_or_cr: flt( abs(d.get("balance_in_account_currency")), d.precision("balance_in_account_currency") ), + "cost_center": erpnext.get_default_cost_center(self.company), "exchange_rate": flt(d.get("current_exchange_rate"), d.precision("current_exchange_rate")), "reference_type": "Exchange Rate Revaluation", "reference_name": self.name, @@ -206,8 +452,11 @@ class ExchangeRateRevaluation(Document): { "account": unrealized_exchange_gain_loss_account, "balance": get_balance_on(unrealized_exchange_gain_loss_account), - "debit_in_account_currency": abs(self.total_gain_loss) if self.total_gain_loss < 0 else 0, - "credit_in_account_currency": self.total_gain_loss if self.total_gain_loss > 0 else 0, + "debit_in_account_currency": abs(self.gain_loss_unbooked) + if self.gain_loss_unbooked < 0 + else 0, + "credit_in_account_currency": self.gain_loss_unbooked if self.gain_loss_unbooked > 0 else 0, + "cost_center": erpnext.get_default_cost_center(self.company), "exchange_rate": 1, "reference_type": "Exchange Rate Revaluation", "reference_name": self.name, @@ -217,42 +466,90 @@ class ExchangeRateRevaluation(Document): journal_entry.set("accounts", journal_entry_accounts) journal_entry.set_amounts_in_company_currency() journal_entry.set_total_debit_credit() - return journal_entry.as_dict() + journal_entry.save() + return journal_entry + + +def calculate_exchange_rate_using_last_gle(company, account, party_type, party): + """ + Use last GL entry to calculate exchange rate + """ + last_exchange_rate = None + if company and account: + gl = qb.DocType("GL Entry") + + # build conditions + conditions = [] + conditions.append(gl.company == company) + conditions.append(gl.account == account) + conditions.append(gl.is_cancelled == 0) + if party_type: + conditions.append(gl.party_type == party_type) + if party: + conditions.append(gl.party == party) + + voucher_type, voucher_no = ( + qb.from_(gl) + .select(gl.voucher_type, gl.voucher_no) + .where(Criterion.all(conditions)) + .orderby(gl.posting_date, order=Order.desc) + .limit(1) + .run()[0] + ) + + last_exchange_rate = ( + qb.from_(gl) + .select((gl.debit - gl.credit) / (gl.debit_in_account_currency - gl.credit_in_account_currency)) + .where( + (gl.voucher_type == voucher_type) & (gl.voucher_no == voucher_no) & (gl.account == account) + ) + .orderby(gl.posting_date, order=Order.desc) + .limit(1) + .run()[0][0] + ) + + return last_exchange_rate @frappe.whitelist() -def get_account_details(account, company, posting_date, party_type=None, party=None): - account_currency, account_type = frappe.db.get_value( +def get_account_details(company, posting_date, account, party_type=None, party=None): + if not (company and posting_date): + frappe.throw(_("Company and Posting Date is mandatory")) + + account_currency, account_type = frappe.get_cached_value( "Account", account, ["account_currency", "account_type"] ) + if account_type in ["Receivable", "Payable"] and not (party_type and party): frappe.throw(_("Party Type and Party is mandatory for {0} account").format(account_type)) account_details = {} company_currency = erpnext.get_company_currency(company) - balance = get_balance_on( - account, date=posting_date, party_type=party_type, party=party, in_account_currency=False - ) + account_details = { "account_currency": account_currency, } + account_balance = ExchangeRateRevaluation.get_account_balance_from_gle( + company=company, posting_date=posting_date, account=account, party_type=party_type, party=party + ) - if balance: - balance_in_account_currency = get_balance_on( - account, date=posting_date, party_type=party_type, party=party + if account_balance and ( + account_balance[0].balance or account_balance[0].balance_in_account_currency + ): + account_with_new_balance = ExchangeRateRevaluation.calculate_new_account_balance( + company, posting_date, account_balance ) - current_exchange_rate = ( - balance / balance_in_account_currency if balance_in_account_currency else 0 - ) - new_exchange_rate = get_exchange_rate(account_currency, company_currency, posting_date) - new_balance_in_base_currency = balance_in_account_currency * new_exchange_rate - account_details = account_details.update( + row = account_with_new_balance[0] + account_details.update( { - "balance_in_base_currency": balance, - "balance_in_account_currency": balance_in_account_currency, - "current_exchange_rate": current_exchange_rate, - "new_exchange_rate": new_exchange_rate, - "new_balance_in_base_currency": new_balance_in_base_currency, + "balance_in_base_currency": row["balance_in_base_currency"], + "balance_in_account_currency": row["balance_in_account_currency"], + "current_exchange_rate": row["current_exchange_rate"], + "new_exchange_rate": row["new_exchange_rate"], + "new_balance_in_base_currency": row["new_balance_in_base_currency"], + "new_balance_in_account_currency": row["new_balance_in_account_currency"], + "zero_balance": row["zero_balance"], + "gain_loss": row["gain_loss"], } ) diff --git a/erpnext/accounts/doctype/exchange_rate_revaluation_account/exchange_rate_revaluation_account.json b/erpnext/accounts/doctype/exchange_rate_revaluation_account/exchange_rate_revaluation_account.json index 80e972bbdf2..2968359a0d0 100644 --- a/erpnext/accounts/doctype/exchange_rate_revaluation_account/exchange_rate_revaluation_account.json +++ b/erpnext/accounts/doctype/exchange_rate_revaluation_account/exchange_rate_revaluation_account.json @@ -10,14 +10,21 @@ "party", "column_break_2", "account_currency", + "account_balances", "balance_in_account_currency", + "column_break_46yz", + "new_balance_in_account_currency", "balances", "current_exchange_rate", - "balance_in_base_currency", - "column_break_9", + "column_break_xown", "new_exchange_rate", + "column_break_9", + "balance_in_base_currency", + "column_break_ukce", "new_balance_in_base_currency", - "gain_loss" + "section_break_ngrs", + "gain_loss", + "zero_balance" ], "fields": [ { @@ -78,7 +85,7 @@ }, { "fieldname": "column_break_9", - "fieldtype": "Column Break" + "fieldtype": "Section Break" }, { "fieldname": "new_exchange_rate", @@ -102,11 +109,45 @@ "label": "Gain/Loss", "options": "Company:company:default_currency", "read_only": 1 + }, + { + "default": "0", + "description": "This Account has '0' balance in either Base Currency or Account Currency", + "fieldname": "zero_balance", + "fieldtype": "Check", + "label": "Zero Balance" + }, + { + "fieldname": "new_balance_in_account_currency", + "fieldtype": "Currency", + "label": "New Balance In Account Currency", + "options": "account_currency", + "read_only": 1 + }, + { + "fieldname": "account_balances", + "fieldtype": "Section Break" + }, + { + "fieldname": "column_break_46yz", + "fieldtype": "Column Break" + }, + { + "fieldname": "column_break_xown", + "fieldtype": "Column Break" + }, + { + "fieldname": "column_break_ukce", + "fieldtype": "Column Break" + }, + { + "fieldname": "section_break_ngrs", + "fieldtype": "Section Break" } ], "istable": 1, "links": [], - "modified": "2022-11-17 10:26:18.302728", + "modified": "2022-12-29 19:38:52.915295", "modified_by": "Administrator", "module": "Accounts", "name": "Exchange Rate Revaluation Account", diff --git a/erpnext/accounts/doctype/gl_entry/gl_entry.py b/erpnext/accounts/doctype/gl_entry/gl_entry.py index 7227b95818f..fa4a66aaacf 100644 --- a/erpnext/accounts/doctype/gl_entry/gl_entry.py +++ b/erpnext/accounts/doctype/gl_entry/gl_entry.py @@ -95,7 +95,15 @@ class GLEntry(Document): ) # Zero value transaction is not allowed - if not (flt(self.debit, self.precision("debit")) or flt(self.credit, self.precision("credit"))): + if not ( + flt(self.debit, self.precision("debit")) + or flt(self.credit, self.precision("credit")) + or ( + self.voucher_type == "Journal Entry" + and frappe.get_cached_value("Journal Entry", self.voucher_no, "voucher_type") + == "Exchange Gain Or Loss" + ) + ): frappe.throw( _("{0} {1}: Either debit or credit amount is required for {2}").format( self.voucher_type, self.voucher_no, self.account diff --git a/erpnext/accounts/doctype/journal_entry/journal_entry.json b/erpnext/accounts/doctype/journal_entry/journal_entry.json index 8e5ba3718f7..3f69d5c7cd8 100644 --- a/erpnext/accounts/doctype/journal_entry/journal_entry.json +++ b/erpnext/accounts/doctype/journal_entry/journal_entry.json @@ -88,7 +88,7 @@ "label": "Entry Type", "oldfieldname": "voucher_type", "oldfieldtype": "Select", - "options": "Journal Entry\nInter Company Journal Entry\nBank Entry\nCash Entry\nCredit Card Entry\nDebit Note\nCredit Note\nContra Entry\nExcise Entry\nWrite Off Entry\nOpening Entry\nDepreciation Entry\nExchange Rate Revaluation\nDeferred Revenue\nDeferred Expense", + "options": "Journal Entry\nInter Company Journal Entry\nBank Entry\nCash Entry\nCredit Card Entry\nDebit Note\nCredit Note\nContra Entry\nExcise Entry\nWrite Off Entry\nOpening Entry\nDepreciation Entry\nExchange Rate Revaluation\nExchange Gain Or Loss\nDeferred Revenue\nDeferred Expense", "reqd": 1, "search_index": 1 }, @@ -539,7 +539,7 @@ "idx": 176, "is_submittable": 1, "links": [], - "modified": "2022-06-23 22:01:32.348337", + "modified": "2022-11-28 17:40:01.241908", "modified_by": "Administrator", "module": "Accounts", "name": "Journal Entry", diff --git a/erpnext/accounts/doctype/journal_entry/journal_entry.py b/erpnext/accounts/doctype/journal_entry/journal_entry.py index de012b28ec8..5a1b6ba1712 100644 --- a/erpnext/accounts/doctype/journal_entry/journal_entry.py +++ b/erpnext/accounts/doctype/journal_entry/journal_entry.py @@ -604,16 +604,18 @@ class JournalEntry(AccountsController): d.against_account = ", ".join(list(set(accounts_debited))) def validate_debit_credit_amount(self): - for d in self.get("accounts"): - if not flt(d.debit) and not flt(d.credit): - frappe.throw(_("Row {0}: Both Debit and Credit values cannot be zero").format(d.idx)) + if not (self.voucher_type == "Exchange Gain Or Loss" and self.multi_currency): + for d in self.get("accounts"): + if not flt(d.debit) and not flt(d.credit): + frappe.throw(_("Row {0}: Both Debit and Credit values cannot be zero").format(d.idx)) def validate_total_debit_and_credit(self): self.set_total_debit_credit() - if self.difference: - frappe.throw( - _("Total Debit must be equal to Total Credit. The difference is {0}").format(self.difference) - ) + if not (self.voucher_type == "Exchange Gain Or Loss" and self.multi_currency): + if self.difference: + frappe.throw( + _("Total Debit must be equal to Total Credit. The difference is {0}").format(self.difference) + ) def set_total_debit_credit(self): self.total_debit, self.total_credit, self.difference = 0, 0, 0 @@ -651,16 +653,17 @@ class JournalEntry(AccountsController): self.set_exchange_rate() def set_amounts_in_company_currency(self): - for d in self.get("accounts"): - d.debit_in_account_currency = flt( - d.debit_in_account_currency, d.precision("debit_in_account_currency") - ) - d.credit_in_account_currency = flt( - d.credit_in_account_currency, d.precision("credit_in_account_currency") - ) + if not (self.voucher_type == "Exchange Gain Or Loss" and self.multi_currency): + for d in self.get("accounts"): + d.debit_in_account_currency = flt( + d.debit_in_account_currency, d.precision("debit_in_account_currency") + ) + d.credit_in_account_currency = flt( + d.credit_in_account_currency, d.precision("credit_in_account_currency") + ) - d.debit = flt(d.debit_in_account_currency * flt(d.exchange_rate), d.precision("debit")) - d.credit = flt(d.credit_in_account_currency * flt(d.exchange_rate), d.precision("credit")) + d.debit = flt(d.debit_in_account_currency * flt(d.exchange_rate), d.precision("debit")) + d.credit = flt(d.credit_in_account_currency * flt(d.exchange_rate), d.precision("credit")) def set_exchange_rate(self): for d in self.get("accounts"): @@ -789,7 +792,7 @@ class JournalEntry(AccountsController): def build_gl_map(self): gl_map = [] for d in self.get("accounts"): - if d.debit or d.credit: + if d.debit or d.credit or (self.voucher_type == "Exchange Gain Or Loss"): r = [d.user_remark, self.remark] r = [x for x in r if x] remarks = "\n".join(r) diff --git a/erpnext/accounts/general_ledger.py b/erpnext/accounts/general_ledger.py index c757057437b..41fdb6a97f8 100644 --- a/erpnext/accounts/general_ledger.py +++ b/erpnext/accounts/general_ledger.py @@ -199,7 +199,14 @@ def merge_similar_entries(gl_map, precision=None): # filter zero debit and credit entries merged_gl_map = filter( - lambda x: flt(x.debit, precision) != 0 or flt(x.credit, precision) != 0, merged_gl_map + lambda x: flt(x.debit, precision) != 0 + or flt(x.credit, precision) != 0 + or ( + x.voucher_type == "Journal Entry" + and frappe.get_cached_value("Journal Entry", x.voucher_no, "voucher_type") + == "Exchange Gain Or Loss" + ), + merged_gl_map, ) merged_gl_map = list(merged_gl_map) @@ -350,15 +357,26 @@ def process_debit_credit_difference(gl_map): allowance = get_debit_credit_allowance(voucher_type, precision) debit_credit_diff = get_debit_credit_difference(gl_map, precision) + if abs(debit_credit_diff) > allowance: - raise_debit_credit_not_equal_error(debit_credit_diff, voucher_type, voucher_no) + if not ( + voucher_type == "Journal Entry" + and frappe.get_cached_value("Journal Entry", voucher_no, "voucher_type") + == "Exchange Gain Or Loss" + ): + raise_debit_credit_not_equal_error(debit_credit_diff, voucher_type, voucher_no) elif abs(debit_credit_diff) >= (1.0 / (10**precision)): make_round_off_gle(gl_map, debit_credit_diff, precision) debit_credit_diff = get_debit_credit_difference(gl_map, precision) if abs(debit_credit_diff) > allowance: - raise_debit_credit_not_equal_error(debit_credit_diff, voucher_type, voucher_no) + if not ( + voucher_type == "Journal Entry" + and frappe.get_cached_value("Journal Entry", voucher_no, "voucher_type") + == "Exchange Gain Or Loss" + ): + raise_debit_credit_not_equal_error(debit_credit_diff, voucher_type, voucher_no) def get_debit_credit_difference(gl_map, precision): diff --git a/erpnext/accounts/report/accounts_receivable/test_accounts_receivable.py b/erpnext/accounts/report/accounts_receivable/test_accounts_receivable.py index 97a9c15fc76..afd02a006e6 100644 --- a/erpnext/accounts/report/accounts_receivable/test_accounts_receivable.py +++ b/erpnext/accounts/report/accounts_receivable/test_accounts_receivable.py @@ -184,11 +184,9 @@ class TestAccountsReceivable(FrappeTestCase): err = err.save().submit() # Submit JV for ERR - jv = frappe.get_doc(err.make_jv_entry()) - jv = jv.save() - for x in jv.accounts: - x.cost_center = get_default_cost_center(jv.company) - jv.submit() + err_journals = err.make_jv_entries() + je = frappe.get_doc("Journal Entry", err_journals.get("revaluation_jv")) + je = je.submit() filters = { "company": company, @@ -201,7 +199,7 @@ class TestAccountsReceivable(FrappeTestCase): report = execute(filters) expected_data_for_err = [0, -5, 0, 5] - row = [x for x in report[1] if x.voucher_type == jv.doctype and x.voucher_no == jv.name][0] + row = [x for x in report[1] if x.voucher_type == je.doctype and x.voucher_no == je.name][0] self.assertEqual( expected_data_for_err, [ diff --git a/erpnext/accounts/report/general_ledger/test_general_ledger.py b/erpnext/accounts/report/general_ledger/test_general_ledger.py index b10e7696187..c5637857636 100644 --- a/erpnext/accounts/report/general_ledger/test_general_ledger.py +++ b/erpnext/accounts/report/general_ledger/test_general_ledger.py @@ -109,8 +109,7 @@ class TestGeneralLedger(FrappeTestCase): frappe.db.set_value( "Company", company, "unrealized_exchange_gain_loss_account", "_Test Exchange Gain/Loss - _TC" ) - revaluation_jv = revaluation.make_jv_entry() - revaluation_jv = frappe.get_doc(revaluation_jv) + revaluation_jv = revaluation.make_jv_for_revaluation() revaluation_jv.cost_center = "_Test Cost Center - _TC" for acc in revaluation_jv.get("accounts"): acc.cost_center = "_Test Cost Center - _TC" diff --git a/erpnext/accounts/report/utils.py b/erpnext/accounts/report/utils.py index d3cd29013f2..97cc1c4a130 100644 --- a/erpnext/accounts/report/utils.py +++ b/erpnext/accounts/report/utils.py @@ -101,11 +101,8 @@ def convert_to_presentation_currency(gl_entries, currency_info, company): account_currency = entry["account_currency"] if len(account_currencies) == 1 and account_currency == presentation_currency: - if debit_in_account_currency: - entry["debit"] = debit_in_account_currency - - if credit_in_account_currency: - entry["credit"] = credit_in_account_currency + entry["debit"] = debit_in_account_currency + entry["credit"] = credit_in_account_currency else: date = currency_info["report_date"] converted_debit_value = convert(debit, presentation_currency, company_currency, date) From e7254fd161c42dee123e4f998340b55c35c4ecb6 Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Mon, 2 Jan 2023 18:46:22 +0530 Subject: [PATCH 26/30] fix: [concurrency issue] incorrect picked qty in sales order (cherry picked from commit aba83849a69a4fc419f5b7cdf1d237e7098f8860) --- erpnext/stock/doctype/pick_list/pick_list.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/erpnext/stock/doctype/pick_list/pick_list.py b/erpnext/stock/doctype/pick_list/pick_list.py index 953fca7419c..65a792fb46b 100644 --- a/erpnext/stock/doctype/pick_list/pick_list.py +++ b/erpnext/stock/doctype/pick_list/pick_list.py @@ -100,6 +100,7 @@ class PickList(Document): item_table, item.sales_order_item, ["picked_qty", stock_qty_field], + for_update=True, ) if self.docstatus == 1: @@ -118,7 +119,7 @@ class PickList(Document): def update_sales_order_picking_status(sales_orders: Set[str]) -> None: for sales_order in sales_orders: if sales_order: - frappe.get_doc("Sales Order", sales_order).update_picking_status() + frappe.get_doc("Sales Order", sales_order, for_update=True).update_picking_status() @frappe.whitelist() def set_item_locations(self, save=False): @@ -262,7 +263,7 @@ class PickList(Document): for so_row, item_code in product_bundles.items(): picked_qty = self._compute_picked_qty_for_bundle(so_row, product_bundle_qty_map[item_code]) item_table = "Sales Order Item" - already_picked = frappe.db.get_value(item_table, so_row, "picked_qty") + already_picked = frappe.db.get_value(item_table, so_row, "picked_qty", for_update=True) frappe.db.set_value( item_table, so_row, From 027510b629f612887c20479e7b04fde4ddd83d97 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Tue, 3 Jan 2023 18:39:19 +0530 Subject: [PATCH 27/30] fix: Deferred revenue date comparison (#33515) fix: Deferred revenue date comparison (#33515) (cherry picked from commit a3ab8f973a887c83ec19513b6eb4219de22bf0e0) Co-authored-by: Deepesh Garg --- erpnext/accounts/deferred_revenue.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/accounts/deferred_revenue.py b/erpnext/accounts/deferred_revenue.py index f319003876b..45e04ee6b0f 100644 --- a/erpnext/accounts/deferred_revenue.py +++ b/erpnext/accounts/deferred_revenue.py @@ -378,7 +378,7 @@ def book_deferred_income_or_expense(doc, deferred_process, posting_date=None): return # check if books nor frozen till endate: - if accounts_frozen_upto and (end_date) <= getdate(accounts_frozen_upto): + if accounts_frozen_upto and getdate(end_date) <= getdate(accounts_frozen_upto): end_date = get_last_day(add_days(accounts_frozen_upto, 1)) if via_journal_entry: From 06e13b64a429af702c79e0e5a4d7cf0e2668a18c Mon Sep 17 00:00:00 2001 From: s-aga-r Date: Sat, 31 Dec 2022 17:17:22 +0530 Subject: [PATCH 28/30] fix: set `supplier` details while mapping SE(Send to Subcontractor) (cherry picked from commit 751bdc98ed8cda3b523d7bc54c61770c9da70865) --- erpnext/controllers/subcontracting_controller.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/erpnext/controllers/subcontracting_controller.py b/erpnext/controllers/subcontracting_controller.py index 8d67e300a30..335d92f43f3 100644 --- a/erpnext/controllers/subcontracting_controller.py +++ b/erpnext/controllers/subcontracting_controller.py @@ -829,6 +829,9 @@ def make_rm_stock_entry( order_doctype: { "doctype": "Stock Entry", "field_map": { + "supplier": "supplier", + "supplier_name": "supplier_name", + "supplier_address": "supplier_address", "to_warehouse": "supplier_warehouse", }, "field_no_map": [field_no_map], From fcf052d3c48c64ee46a0a9f8d7391186b15db0d0 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Tue, 3 Jan 2023 22:09:50 +0530 Subject: [PATCH 29/30] fix: Get payment entry button not visible in Bank Clearance doc (backport #33518) (#33525) fix: Get payment entry button not visible in Bank Clearance doc (#33518) (cherry picked from commit 1a83a67d41b33b57d1651093cb7145a59d451889) Co-authored-by: Deepesh Garg --- .../accounts/doctype/bank_clearance/bank_clearance.js | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/erpnext/accounts/doctype/bank_clearance/bank_clearance.js b/erpnext/accounts/doctype/bank_clearance/bank_clearance.js index ceba99a56a8..71f2dcca1b2 100644 --- a/erpnext/accounts/doctype/bank_clearance/bank_clearance.js +++ b/erpnext/accounts/doctype/bank_clearance/bank_clearance.js @@ -37,14 +37,11 @@ frappe.ui.form.on("Bank Clearance", { refresh: function(frm) { frm.disable_save(); + frm.add_custom_button(__('Get Payment Entries'), () => + frm.trigger("get_payment_entries") + ); - if (frm.doc.account && frm.doc.from_date && frm.doc.to_date) { - frm.add_custom_button(__('Get Payment Entries'), () => - frm.trigger("get_payment_entries") - ); - - frm.change_custom_button_type('Get Payment Entries', null, 'primary'); - } + frm.change_custom_button_type('Get Payment Entries', null, 'primary'); }, update_clearance_date: function(frm) { From 8e375db0b248952e3292069c17bd639c2b0a893c Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Tue, 3 Jan 2023 22:13:29 +0530 Subject: [PATCH 30/30] fix: Missing opening entry in general ledger (backport #33519) (#33526) fix: Missing opening entry in general ledger (#33519) (cherry picked from commit c78399c618ddc81cddba76caa7b8ab5563cc6a4b) Co-authored-by: Deepesh Garg --- erpnext/accounts/report/general_ledger/general_ledger.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/accounts/report/general_ledger/general_ledger.py b/erpnext/accounts/report/general_ledger/general_ledger.py index af559d4c1ab..ab93893861b 100644 --- a/erpnext/accounts/report/general_ledger/general_ledger.py +++ b/erpnext/accounts/report/general_ledger/general_ledger.py @@ -239,7 +239,7 @@ def get_conditions(filters): ): conditions.append("(posting_date >=%(from_date)s or is_opening = 'Yes')") - conditions.append("(posting_date <=%(to_date)s)") + conditions.append("(posting_date <=%(to_date)s or is_opening = 'Yes')") if filters.get("project"): conditions.append("project in %(project)s")