From c9daa85985e998f96a885100b7763c7ea7a5b49f Mon Sep 17 00:00:00 2001 From: Gursheen Anand Date: Tue, 25 Jul 2023 16:50:46 +0530 Subject: [PATCH 001/501] fix: cost center filter for fetching payments --- erpnext/controllers/accounts_controller.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/erpnext/controllers/accounts_controller.py b/erpnext/controllers/accounts_controller.py index b70ea36dcde8..11358a482bab 100644 --- a/erpnext/controllers/accounts_controller.py +++ b/erpnext/controllers/accounts_controller.py @@ -2223,15 +2223,15 @@ def get_advance_payment_entries( 'Payment Entry' as reference_type, t1.name as reference_name, t1.remarks, t2.allocated_amount as amount, t2.name as reference_row, t2.reference_name as against_order, t1.posting_date, - t1.{0} as currency, t1.{4} as exchange_rate + t1.{0} as currency, t1.{5} as exchange_rate from `tabPayment Entry` t1, `tabPayment Entry Reference` t2 where t1.name = t2.parent and t1.{1} = %s and t1.payment_type = %s and t1.party_type = %s and t1.party = %s and t1.docstatus = 1 - and t2.reference_doctype = %s {2} - order by t1.posting_date {3} + and t2.reference_doctype = %s {2} {3} + order by t1.posting_date {4} """.format( - currency_field, party_account_field, reference_condition, limit_cond, exchange_rate_field + currency_field, party_account_field, reference_condition, condition, limit_cond, exchange_rate_field ), [party_account, payment_type, party_type, party, order_doctype] + order_list, as_dict=1, From c3b21a6c30dfcc3a6f6fd52479333b294b900e76 Mon Sep 17 00:00:00 2001 From: Gursheen Anand Date: Tue, 25 Jul 2023 17:04:03 +0530 Subject: [PATCH 002/501] chore: linting issues --- erpnext/controllers/accounts_controller.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/erpnext/controllers/accounts_controller.py b/erpnext/controllers/accounts_controller.py index 11358a482bab..7fec63102079 100644 --- a/erpnext/controllers/accounts_controller.py +++ b/erpnext/controllers/accounts_controller.py @@ -2231,7 +2231,12 @@ def get_advance_payment_entries( and t2.reference_doctype = %s {2} {3} order by t1.posting_date {4} """.format( - currency_field, party_account_field, reference_condition, condition, limit_cond, exchange_rate_field + currency_field, + party_account_field, + reference_condition, + condition, + limit_cond, + exchange_rate_field ), [party_account, payment_type, party_type, party, order_doctype] + order_list, as_dict=1, From f07b87c5eb652b4ec3714923e542d23965bc6238 Mon Sep 17 00:00:00 2001 From: barredterra <14891507+barredterra@users.noreply.github.com> Date: Tue, 25 Jul 2023 18:05:41 +0200 Subject: [PATCH 003/501] chore(Item Group): remove redundant autoname (cherry picked from commit 1691eee26e1b619ac56feae21df82c37a9c699ad) --- erpnext/setup/doctype/item_group/item_group.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/erpnext/setup/doctype/item_group/item_group.py b/erpnext/setup/doctype/item_group/item_group.py index f5432c182582..e96c8541f402 100644 --- a/erpnext/setup/doctype/item_group/item_group.py +++ b/erpnext/setup/doctype/item_group/item_group.py @@ -24,9 +24,6 @@ class ItemGroup(NestedSet, WebsiteGenerator): no_breadcrumbs=1, ) - def autoname(self): - self.name = self.item_group_name - def validate(self): super(ItemGroup, self).validate() From 479cab0336cd024477367bd8f7e535ce638f7251 Mon Sep 17 00:00:00 2001 From: Gursheen Anand Date: Wed, 26 Jul 2023 09:56:48 +0530 Subject: [PATCH 004/501] fix: test with None conditon in PE --- erpnext/controllers/accounts_controller.py | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/erpnext/controllers/accounts_controller.py b/erpnext/controllers/accounts_controller.py index 7fec63102079..f4b6e01cc4bd 100644 --- a/erpnext/controllers/accounts_controller.py +++ b/erpnext/controllers/accounts_controller.py @@ -2217,6 +2217,9 @@ def get_advance_payment_entries( reference_condition = "" order_list = [] + if not condition: + condition = "" + payment_entries_against_order = frappe.db.sql( """ select @@ -2231,12 +2234,12 @@ def get_advance_payment_entries( and t2.reference_doctype = %s {2} {3} order by t1.posting_date {4} """.format( - currency_field, - party_account_field, - reference_condition, - condition, - limit_cond, - exchange_rate_field + currency_field, + party_account_field, + reference_condition, + condition, + limit_cond, + exchange_rate_field, ), [party_account, payment_type, party_type, party, order_doctype] + order_list, as_dict=1, From 02428b446d959784c546f30c176d6af10e6ed250 Mon Sep 17 00:00:00 2001 From: Gursheen Anand Date: Wed, 26 Jul 2023 11:04:46 +0530 Subject: [PATCH 005/501] fix: filter by cost center in trial balance --- erpnext/accounts/report/trial_balance/trial_balance.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/accounts/report/trial_balance/trial_balance.py b/erpnext/accounts/report/trial_balance/trial_balance.py index 599c8a312a5d..3f0e971be6fc 100644 --- a/erpnext/accounts/report/trial_balance/trial_balance.py +++ b/erpnext/accounts/report/trial_balance/trial_balance.py @@ -253,7 +253,7 @@ def get_opening_balance( lft, rgt = frappe.db.get_value("Cost Center", filters.cost_center, ["lft", "rgt"]) cost_center = frappe.qb.DocType("Cost Center") opening_balance = opening_balance.where( - closing_balance.cost_center.in_( + closing_balance.cost_center.isin( frappe.qb.from_(cost_center) .select("name") .where((cost_center.lft >= lft) & (cost_center.rgt <= rgt)) From 32fea643cd6c0cd8759c24d4a51299abe74279d5 Mon Sep 17 00:00:00 2001 From: Gursheen Anand Date: Wed, 26 Jul 2023 11:34:27 +0530 Subject: [PATCH 006/501] feat: add party type filter --- .../tds_payable_monthly.js | 26 ++++++++++++++++--- 1 file changed, 22 insertions(+), 4 deletions(-) diff --git a/erpnext/accounts/report/tds_payable_monthly/tds_payable_monthly.js b/erpnext/accounts/report/tds_payable_monthly/tds_payable_monthly.js index ff2aa306017b..3df21e87185a 100644 --- a/erpnext/accounts/report/tds_payable_monthly/tds_payable_monthly.js +++ b/erpnext/accounts/report/tds_payable_monthly/tds_payable_monthly.js @@ -12,10 +12,28 @@ frappe.query_reports["TDS Payable Monthly"] = { "default": frappe.defaults.get_default('company') }, { - "fieldname":"supplier", - "label": __("Supplier"), - "fieldtype": "Link", - "options": "Supplier", + "fieldname":"party_type", + "label": __("Party Type"), + "fieldtype": "Select", + "options": ["Supplier", "Customer"], + "reqd": 1, + "default": "Supplier", + "on_change": function(){ + frappe.query_report.set_filter_value("party", ""); + } + }, + { + "fieldname":"party", + "label": __("Party"), + "fieldtype": "Dynamic Link", + "get_options": function() { + var party_type = frappe.query_report.get_filter_value('party_type'); + var party = frappe.query_report.get_filter_value('party'); + if(party && !party_type) { + frappe.throw(__("Please select Party Type first")); + } + return party_type; + } }, { "fieldname":"from_date", From 619b0feb5f7f5827f5a0ec4e140dbda4da8fe262 Mon Sep 17 00:00:00 2001 From: Gursheen Anand Date: Wed, 26 Jul 2023 11:35:02 +0530 Subject: [PATCH 007/501] fix: show tds & tcs separately --- .../tds_payable_monthly.py | 121 +++++++++++------- 1 file changed, 78 insertions(+), 43 deletions(-) diff --git a/erpnext/accounts/report/tds_payable_monthly/tds_payable_monthly.py b/erpnext/accounts/report/tds_payable_monthly/tds_payable_monthly.py index 98838907be1f..ddd049a11517 100644 --- a/erpnext/accounts/report/tds_payable_monthly/tds_payable_monthly.py +++ b/erpnext/accounts/report/tds_payable_monthly/tds_payable_monthly.py @@ -33,77 +33,94 @@ def validate_filters(filters): def get_result( filters, tds_docs, tds_accounts, tax_category_map, journal_entry_party_map, invoice_net_total_map ): - supplier_map = get_supplier_pan_map() + party_map = get_party_pan_map(filters.get("party_type")) tax_rate_map = get_tax_rate_map(filters) gle_map = get_gle_map(tds_docs) out = [] for name, details in gle_map.items(): - tds_deducted, total_amount_credited = 0, 0 + tax_amount, total_amount = 0, 0 tax_withholding_category = tax_category_map.get(name) rate = tax_rate_map.get(tax_withholding_category) for entry in details: - supplier = entry.party or entry.against + party = entry.party or entry.against posting_date = entry.posting_date voucher_type = entry.voucher_type if voucher_type == "Journal Entry": - suppliers = journal_entry_party_map.get(name) - if suppliers: - supplier = suppliers[0] + party_list = journal_entry_party_map.get(name) + if party_list: + party = party_list[0] if not tax_withholding_category: - tax_withholding_category = supplier_map.get(supplier, {}).get("tax_withholding_category") + tax_withholding_category = party_map.get(party, {}).get("tax_withholding_category") rate = tax_rate_map.get(tax_withholding_category) if entry.account in tds_accounts: - tds_deducted += entry.credit - entry.debit + tax_amount += entry.credit - entry.debit if invoice_net_total_map.get(name): - total_amount_credited = invoice_net_total_map.get(name) + total_amount = invoice_net_total_map.get(name) else: - total_amount_credited += entry.credit + total_amount += entry.credit + + if tax_amount: + if party_map.get(party, {}).get("party_type") == "Supplier": + party_name = "supplier_name" + party_type = "supplier_type" + table_name = "Supplier" + else: + party_name = "customer_name" + party_type = "customer_type" + table_name = "Customer" - if tds_deducted: row = { "pan" - if frappe.db.has_column("Supplier", "pan") - else "tax_id": supplier_map.get(supplier, {}).get("pan"), - "supplier": supplier_map.get(supplier, {}).get("name"), + if frappe.db.has_column(table_name, "pan") + else "tax_id": party_map.get(party, {}).get("pan"), + "party": party_map.get(party, {}).get("name"), } if filters.naming_series == "Naming Series": - row.update({"supplier_name": supplier_map.get(supplier, {}).get("supplier_name")}) + row.update({"party_name": party_map.get(party, {}).get(party_name)}) row.update( { "section_code": tax_withholding_category, - "entity_type": supplier_map.get(supplier, {}).get("supplier_type"), - "tds_rate": rate, - "total_amount_credited": total_amount_credited, - "tds_deducted": tds_deducted, + "entity_type": party_map.get(party, {}).get(party_type), + "rate": rate, + "total_amount": total_amount, + "tax_amount": tax_amount, "transaction_date": posting_date, "transaction_type": voucher_type, "ref_no": name, } ) - out.append(row) return out -def get_supplier_pan_map(): - supplier_map = frappe._dict() - suppliers = frappe.db.get_all( - "Supplier", fields=["name", "pan", "supplier_type", "supplier_name", "tax_withholding_category"] - ) +def get_party_pan_map(party_type): + party_map = frappe._dict() + + fields = ["name", "tax_withholding_category"] + if party_type == "Supplier": + fields += ["supplier_type", "supplier_name"] + else: + fields += ["customer_type", "customer_name"] + + if frappe.db.has_column(party_type, "pan"): + fields.append("pan") + + party_details = frappe.db.get_all(party_type, fields=fields) - for d in suppliers: - supplier_map[d.name] = d + for party in party_details: + party.party_type = party_type + party_map[party.name] = party - return supplier_map + return party_map def get_gle_map(documents): @@ -131,17 +148,17 @@ def get_columns(filters): columns = [ {"label": _(frappe.unscrub(pan)), "fieldname": pan, "fieldtype": "Data", "width": 90}, { - "label": _("Supplier"), - "options": "Supplier", - "fieldname": "supplier", - "fieldtype": "Link", + "label": _(filters.get("party_type")), + "fieldname": "party", + "fieldtype": "Dynamic Link", + "options": "party_type", "width": 180, }, ] if filters.naming_series == "Naming Series": columns.append( - {"label": _("Supplier Name"), "fieldname": "supplier_name", "fieldtype": "Data", "width": 180} + {"label": _("Party Name"), "fieldname": "party_name", "fieldtype": "Data", "width": 180} ) columns.extend( @@ -153,17 +170,22 @@ def get_columns(filters): "fieldtype": "Link", "width": 180, }, - {"label": _("Entity Type"), "fieldname": "entity_type", "fieldtype": "Data", "width": 180}, - {"label": _("TDS Rate %"), "fieldname": "tds_rate", "fieldtype": "Percent", "width": 90}, + {"label": _("Entity Type"), "fieldname": "entity_type", "fieldtype": "Data", "width": 120}, { - "label": _("Total Amount Credited"), - "fieldname": "total_amount_credited", + "label": _("TDS Rate %") if filters.get("party_type") == "Supplier" else _("TCS Rate %"), + "fieldname": "rate", + "fieldtype": "Percent", + "width": 90, + }, + { + "label": _("Total Amount"), + "fieldname": "total_amount", "fieldtype": "Float", "width": 90, }, { - "label": _("Amount of TDS Deducted"), - "fieldname": "tds_deducted", + "label": _("TDS Amount") if filters.get("party_type") == "Supplier" else _("TCS Amount"), + "fieldname": "tax_amount", "fieldtype": "Float", "width": 90, }, @@ -173,13 +195,13 @@ def get_columns(filters): "fieldtype": "Date", "width": 90, }, - {"label": _("Transaction Type"), "fieldname": "transaction_type", "width": 90}, + {"label": _("Transaction Type"), "fieldname": "transaction_type", "width": 100}, { "label": _("Reference No."), "fieldname": "ref_no", "fieldtype": "Dynamic Link", "options": "transaction_type", - "width": 90, + "width": 180, }, ] ) @@ -190,6 +212,7 @@ def get_columns(filters): def get_tds_docs(filters): tds_documents = [] purchase_invoices = [] + sales_invoices = [] payment_entries = [] journal_entries = [] tax_category_map = frappe._dict() @@ -209,10 +232,13 @@ def get_tds_docs(filters): "against": ("not in", bank_accounts), } - if filters.get("supplier"): + party = frappe.get_all(filters.get("party_type"), pluck="name") + query_filters.update({"against": ("in", party)}) + + if filters.get("party"): del query_filters["account"] del query_filters["against"] - or_filters = {"against": filters.get("supplier"), "party": filters.get("supplier")} + or_filters = {"against": filters.get("party"), "party": filters.get("party")} tds_docs = frappe.get_all( "GL Entry", @@ -224,6 +250,8 @@ def get_tds_docs(filters): for d in tds_docs: if d.voucher_type == "Purchase Invoice": purchase_invoices.append(d.voucher_no) + if d.voucher_type == "Sales Invoice": + sales_invoices.append(d.voucher_no) elif d.voucher_type == "Payment Entry": payment_entries.append(d.voucher_no) elif d.voucher_type == "Journal Entry": @@ -234,6 +262,9 @@ def get_tds_docs(filters): if purchase_invoices: get_doc_info(purchase_invoices, "Purchase Invoice", tax_category_map, invoice_net_total_map) + if sales_invoices: + get_doc_info(sales_invoices, "Sales Invoice", tax_category_map, invoice_net_total_map) + if payment_entries: get_doc_info(payment_entries, "Payment Entry", tax_category_map) @@ -267,6 +298,8 @@ def get_journal_entry_party_map(journal_entries): def get_doc_info(vouchers, doctype, tax_category_map, invoice_net_total_map=None): if doctype == "Purchase Invoice": fields = ["name", "tax_withholding_category", "base_tax_withholding_net_total"] + if doctype == "Sales Invoice": + fields = ["name", "base_net_total"] else: fields = ["name", "tax_withholding_category"] @@ -276,6 +309,8 @@ def get_doc_info(vouchers, doctype, tax_category_map, invoice_net_total_map=None tax_category_map.update({entry.name: entry.tax_withholding_category}) if doctype == "Purchase Invoice": invoice_net_total_map.update({entry.name: entry.base_tax_withholding_net_total}) + if doctype == "Sales Invoice": + invoice_net_total_map.update({entry.name: entry.base_net_total}) def get_tax_rate_map(filters): From e802f0c3526c7529039683bd9ff176eab7afcb4f Mon Sep 17 00:00:00 2001 From: gouravengineer <63018500+gouravengineer@users.noreply.github.com> Date: Tue, 25 Jul 2023 16:23:36 +0530 Subject: [PATCH 008/501] fix: show invoices name instead of object address comma_and function in expecting a list but it gets a tuple so it is returning a object instead of a string (cherry picked from commit cf93714a7cce465d241bf4e713ee7492c5c01bfa) --- erpnext/accounts/doctype/payment_entry/payment_entry.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/accounts/doctype/payment_entry/payment_entry.py b/erpnext/accounts/doctype/payment_entry/payment_entry.py index 411ec6d02c62..83736bd68e91 100644 --- a/erpnext/accounts/doctype/payment_entry/payment_entry.py +++ b/erpnext/accounts/doctype/payment_entry/payment_entry.py @@ -468,7 +468,7 @@ def validate_paid_invoices(self): _( "References {0} of type {1} had no outstanding amount left before submitting the Payment Entry. Now they have a negative outstanding amount." ).format( - frappe.bold(comma_and((d.reference_name for d in references))), + frappe.bold(comma_and([d.reference_name for d in references])), _(reference_doctype), ) + "

" From b105ba11b89f7cd57bb03e300d026322cfa97be4 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Wed, 26 Jul 2023 22:25:39 +0530 Subject: [PATCH 009/501] chore: adding totals in asset reports (backport #36334) (#36335) chore: adding totals in asset reports (#36334) (cherry picked from commit 5e7b05e566d305831ab089bd6d6c45ddf65f789c) Co-authored-by: Anand Baburajan --- .../asset_depreciation_ledger.json | 35 ++++++++++--------- .../asset_depreciations_and_balances.json | 35 ++++++++++--------- .../fixed_asset_register.json | 8 +++-- 3 files changed, 43 insertions(+), 35 deletions(-) diff --git a/erpnext/accounts/report/asset_depreciation_ledger/asset_depreciation_ledger.json b/erpnext/accounts/report/asset_depreciation_ledger/asset_depreciation_ledger.json index bee2829c87ab..0ef9d858dd5b 100644 --- a/erpnext/accounts/report/asset_depreciation_ledger/asset_depreciation_ledger.json +++ b/erpnext/accounts/report/asset_depreciation_ledger/asset_depreciation_ledger.json @@ -1,20 +1,23 @@ { - "add_total_row": 0, - "apply_user_permissions": 1, - "creation": "2016-04-08 14:49:58.133098", - "disabled": 0, - "docstatus": 0, - "doctype": "Report", - "idx": 2, - "is_standard": "Yes", - "modified": "2017-02-24 20:08:26.084484", - "modified_by": "Administrator", - "module": "Accounts", - "name": "Asset Depreciation Ledger", - "owner": "Administrator", - "ref_doctype": "Asset", - "report_name": "Asset Depreciation Ledger", - "report_type": "Script Report", + "add_total_row": 1, + "columns": [], + "creation": "2016-04-08 14:49:58.133098", + "disabled": 0, + "docstatus": 0, + "doctype": "Report", + "filters": [], + "idx": 2, + "is_standard": "Yes", + "letterhead": null, + "modified": "2023-07-26 21:05:33.554778", + "modified_by": "Administrator", + "module": "Accounts", + "name": "Asset Depreciation Ledger", + "owner": "Administrator", + "prepared_report": 0, + "ref_doctype": "Asset", + "report_name": "Asset Depreciation Ledger", + "report_type": "Script Report", "roles": [ { "role": "Accounts User" diff --git a/erpnext/accounts/report/asset_depreciations_and_balances/asset_depreciations_and_balances.json b/erpnext/accounts/report/asset_depreciations_and_balances/asset_depreciations_and_balances.json index eab95fc73b3f..2ea9af223e5d 100644 --- a/erpnext/accounts/report/asset_depreciations_and_balances/asset_depreciations_and_balances.json +++ b/erpnext/accounts/report/asset_depreciations_and_balances/asset_depreciations_and_balances.json @@ -1,20 +1,23 @@ { - "add_total_row": 0, - "apply_user_permissions": 1, - "creation": "2016-04-08 14:56:37.235981", - "disabled": 0, - "docstatus": 0, - "doctype": "Report", - "idx": 2, - "is_standard": "Yes", - "modified": "2017-02-24 20:08:18.660476", - "modified_by": "Administrator", - "module": "Accounts", - "name": "Asset Depreciations and Balances", - "owner": "Administrator", - "ref_doctype": "Asset", - "report_name": "Asset Depreciations and Balances", - "report_type": "Script Report", + "add_total_row": 1, + "columns": [], + "creation": "2016-04-08 14:56:37.235981", + "disabled": 0, + "docstatus": 0, + "doctype": "Report", + "filters": [], + "idx": 2, + "is_standard": "Yes", + "letterhead": null, + "modified": "2023-07-26 21:04:54.751077", + "modified_by": "Administrator", + "module": "Accounts", + "name": "Asset Depreciations and Balances", + "owner": "Administrator", + "prepared_report": 0, + "ref_doctype": "Asset", + "report_name": "Asset Depreciations and Balances", + "report_type": "Script Report", "roles": [ { "role": "Accounts User" diff --git a/erpnext/assets/report/fixed_asset_register/fixed_asset_register.json b/erpnext/assets/report/fixed_asset_register/fixed_asset_register.json index b40243cb75a3..9074ba1cc164 100644 --- a/erpnext/assets/report/fixed_asset_register/fixed_asset_register.json +++ b/erpnext/assets/report/fixed_asset_register/fixed_asset_register.json @@ -1,13 +1,15 @@ { - "add_total_row": 0, + "add_total_row": 1, + "columns": [], "creation": "2019-09-23 16:35:02.836134", - "disable_prepared_report": 1, "disabled": 0, "docstatus": 0, "doctype": "Report", + "filters": [], "idx": 0, "is_standard": "Yes", - "modified": "2019-10-22 13:00:31.539726", + "letterhead": null, + "modified": "2023-07-26 21:03:20.722628", "modified_by": "Administrator", "module": "Assets", "name": "Fixed Asset Register", From ade13e6d36a17777d3547c4bf7c279c4be0e9a28 Mon Sep 17 00:00:00 2001 From: Nabin Hait Date: Wed, 26 Jul 2023 13:03:29 +0530 Subject: [PATCH 010/501] fix: GL Entries should not be splitted based on cost center allocation in PCV (cherry picked from commit 666d96187557e0dd26bad99d07aa5b1899b9bc62) --- erpnext/accounts/general_ledger.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/erpnext/accounts/general_ledger.py b/erpnext/accounts/general_ledger.py index 16b61236dbaf..5d4035ee65e9 100644 --- a/erpnext/accounts/general_ledger.py +++ b/erpnext/accounts/general_ledger.py @@ -105,7 +105,8 @@ def process_gl_map(gl_map, merge_entries=True, precision=None): if not gl_map: return [] - gl_map = distribute_gl_based_on_cost_center_allocation(gl_map, precision) + if gl_map[0].voucher_type != "Period Closing Voucher": + gl_map = distribute_gl_based_on_cost_center_allocation(gl_map, precision) if merge_entries: gl_map = merge_similar_entries(gl_map, precision) From 800417eeed04e29be372f30f08b9acce74adb6fc Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Thu, 27 Jul 2023 15:40:36 +0530 Subject: [PATCH 011/501] fix: Ignore account closing balance for financial statement (cherry picked from commit ccf1920a78d87afd8c25d0ed711afa24dc6e7181) # Conflicts: # erpnext/accounts/doctype/accounts_settings/accounts_settings.json --- .../accounts_settings/accounts_settings.json | 12 +++++++ .../accounts_settings/accounts_settings.py | 33 ++++++++++++------- .../accounts/report/financial_statements.py | 12 +++++-- .../report/trial_balance/trial_balance.py | 18 ++++++---- 4 files changed, 55 insertions(+), 20 deletions(-) diff --git a/erpnext/accounts/doctype/accounts_settings/accounts_settings.json b/erpnext/accounts/doctype/accounts_settings/accounts_settings.json index 47c4396e102b..7184a3ba26e6 100644 --- a/erpnext/accounts/doctype/accounts_settings/accounts_settings.json +++ b/erpnext/accounts/doctype/accounts_settings/accounts_settings.json @@ -60,6 +60,7 @@ "closing_settings_tab", "period_closing_settings_section", "acc_frozen_upto", + "ignore_account_closing_balance", "column_break_25", "frozen_accounts_modifier", "report_settings_sb", @@ -408,6 +409,13 @@ "fieldname": "enable_fuzzy_matching", "fieldtype": "Check", "label": "Enable Fuzzy Matching" + }, + { + "default": "0", + "description": "Financial reports will be generated using GL Entry doctypes (should be enabled if Period Closing Voucher is not posted for all years sequentially or missing) ", + "fieldname": "ignore_account_closing_balance", + "fieldtype": "Check", + "label": "Ignore Account Closing Balance" } ], "icon": "icon-cog", @@ -415,7 +423,11 @@ "index_web_pages_for_search": 1, "issingle": 1, "links": [], +<<<<<<< HEAD "modified": "2023-06-15 18:47:46.430291", +======= + "modified": "2023-07-27 15:05:34.000264", +>>>>>>> ccf1920a78 (fix: Ignore account closing balance for financial statement) "modified_by": "Administrator", "module": "Accounts", "name": "Accounts Settings", diff --git a/erpnext/accounts/doctype/accounts_settings/accounts_settings.py b/erpnext/accounts/doctype/accounts_settings/accounts_settings.py index 3b125a29862f..ac3d44bb5e70 100644 --- a/erpnext/accounts/doctype/accounts_settings/accounts_settings.py +++ b/erpnext/accounts/doctype/accounts_settings/accounts_settings.py @@ -14,21 +14,32 @@ class AccountsSettings(Document): - def on_update(self): - frappe.clear_cache() - def validate(self): - frappe.db.set_default( - "add_taxes_from_item_tax_template", self.get("add_taxes_from_item_tax_template", 0) - ) + old_doc = self.get_doc_before_save() + clear_cache = False + + if old_doc.add_taxes_from_item_tax_template != self.add_taxes_from_item_tax_template: + frappe.db.set_default( + "add_taxes_from_item_tax_template", self.get("add_taxes_from_item_tax_template", 0) + ) + clear_cache = True - frappe.db.set_default( - "enable_common_party_accounting", self.get("enable_common_party_accounting", 0) - ) + if old_doc.enable_common_party_accounting != self.enable_common_party_accounting: + frappe.db.set_default( + "enable_common_party_accounting", self.get("enable_common_party_accounting", 0) + ) + clear_cache = True self.validate_stale_days() - self.enable_payment_schedule_in_print() - self.validate_pending_reposts() + + if old_doc.show_payment_schedule_in_print != self.show_payment_schedule_in_print: + self.enable_payment_schedule_in_print() + + if old_doc.acc_frozen_upto != self.acc_frozen_upto: + self.validate_pending_reposts() + + if clear_cache: + frappe.clear_cache() def validate_stale_days(self): if not self.allow_stale and cint(self.stale_days) <= 0: diff --git a/erpnext/accounts/report/financial_statements.py b/erpnext/accounts/report/financial_statements.py index db9609debe61..26bf315b1927 100644 --- a/erpnext/accounts/report/financial_statements.py +++ b/erpnext/accounts/report/financial_statements.py @@ -429,11 +429,17 @@ def set_gl_entries_by_account( if accounts_list: # For balance sheet - if not from_date: - from_date = filters["period_start_date"] + ignore_closing_balances = frappe.db.get_single_value( + "Accounts Settings", "ignore_account_closing_balance" + ) + if not from_date and not ignore_closing_balances: last_period_closing_voucher = frappe.db.get_all( "Period Closing Voucher", - filters={"docstatus": 1, "company": filters.company, "posting_date": ("<", from_date)}, + filters={ + "docstatus": 1, + "company": filters.company, + "posting_date": ("<", filters["period_start_date"]), + }, fields=["posting_date", "name"], order_by="posting_date desc", limit=1, diff --git a/erpnext/accounts/report/trial_balance/trial_balance.py b/erpnext/accounts/report/trial_balance/trial_balance.py index 3f0e971be6fc..376571f03461 100644 --- a/erpnext/accounts/report/trial_balance/trial_balance.py +++ b/erpnext/accounts/report/trial_balance/trial_balance.py @@ -142,14 +142,20 @@ def get_opening_balances(filters): def get_rootwise_opening_balances(filters, report_type): gle = [] - last_period_closing_voucher = frappe.db.get_all( - "Period Closing Voucher", - filters={"docstatus": 1, "company": filters.company, "posting_date": ("<", filters.from_date)}, - fields=["posting_date", "name"], - order_by="posting_date desc", - limit=1, + last_period_closing_voucher = "" + ignore_closing_balances = frappe.db.get_single_value( + "Accounts Settings", "ignore_account_closing_balance" ) + if not ignore_closing_balances: + last_period_closing_voucher = frappe.db.get_all( + "Period Closing Voucher", + filters={"docstatus": 1, "company": filters.company, "posting_date": ("<", filters.from_date)}, + fields=["posting_date", "name"], + order_by="posting_date desc", + limit=1, + ) + accounting_dimensions = get_accounting_dimensions(as_list=False) if last_period_closing_voucher: From be9440233873ebd6bea47aacd48d343d04dbe577 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Thu, 27 Jul 2023 21:12:58 +0530 Subject: [PATCH 012/501] chore: resolve conflicts --- .../accounts/doctype/accounts_settings/accounts_settings.json | 4 ---- 1 file changed, 4 deletions(-) diff --git a/erpnext/accounts/doctype/accounts_settings/accounts_settings.json b/erpnext/accounts/doctype/accounts_settings/accounts_settings.json index 7184a3ba26e6..467c68f10218 100644 --- a/erpnext/accounts/doctype/accounts_settings/accounts_settings.json +++ b/erpnext/accounts/doctype/accounts_settings/accounts_settings.json @@ -423,11 +423,7 @@ "index_web_pages_for_search": 1, "issingle": 1, "links": [], -<<<<<<< HEAD - "modified": "2023-06-15 18:47:46.430291", -======= "modified": "2023-07-27 15:05:34.000264", ->>>>>>> ccf1920a78 (fix: Ignore account closing balance for financial statement) "modified_by": "Administrator", "module": "Accounts", "name": "Accounts Settings", From 1a6be5e19bf5d0aea9e456d18e51b19b43013690 Mon Sep 17 00:00:00 2001 From: barredterra <14891507+barredterra@users.noreply.github.com> Date: Tue, 25 Jul 2023 17:01:57 +0200 Subject: [PATCH 013/501] fix(Item Group): allow root deletion It was not possible to delete an empty, unused Item Group without any children, if it was one of possibly multiple roots of the Item Group tree. This fix allows deleting a root Item Group. (cherry picked from commit fd2c272bed6f1b59b5e7cbb3d381ae1e66168046) --- erpnext/setup/doctype/item_group/item_group.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/setup/doctype/item_group/item_group.py b/erpnext/setup/doctype/item_group/item_group.py index e96c8541f402..cc67c696b437 100644 --- a/erpnext/setup/doctype/item_group/item_group.py +++ b/erpnext/setup/doctype/item_group/item_group.py @@ -73,7 +73,7 @@ def make_route(self): return self.route def on_trash(self): - NestedSet.on_trash(self) + NestedSet.on_trash(self, allow_root_deletion=True) WebsiteGenerator.on_trash(self) self.delete_child_item_groups_key() From 50ef35845a216a8e13def39e06e95fd821caec55 Mon Sep 17 00:00:00 2001 From: HarryPaulo Date: Tue, 11 Jul 2023 11:35:37 -0300 Subject: [PATCH 014/501] fix: paid_amount when the group is mode of payment (cherry picked from commit 2268f7db43bbf12910efe090975026652e16c0ff) --- erpnext/accounts/report/pos_register/pos_register.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/erpnext/accounts/report/pos_register/pos_register.py b/erpnext/accounts/report/pos_register/pos_register.py index 9c0aba332efe..488bb9957c96 100644 --- a/erpnext/accounts/report/pos_register/pos_register.py +++ b/erpnext/accounts/report/pos_register/pos_register.py @@ -50,20 +50,20 @@ def get_pos_entries(filters, group_by_field): order_by = "p.posting_date" select_mop_field, from_sales_invoice_payment, group_by_mop_condition = "", "", "" if group_by_field == "mode_of_payment": - select_mop_field = ", sip.mode_of_payment" + select_mop_field = ", sip.mode_of_payment, sip.base_amount - IF(sip.type='Cash', p.change_amount, 0) as paid_amount" from_sales_invoice_payment = ", `tabSales Invoice Payment` sip" - group_by_mop_condition = "sip.parent = p.name AND ifnull(sip.base_amount, 0) != 0 AND" + group_by_mop_condition = "sip.parent = p.name AND ifnull(sip.base_amount - IF(sip.type='Cash', p.change_amount, 0), 0) != 0 AND" order_by += ", sip.mode_of_payment" elif group_by_field: order_by += ", p.{}".format(group_by_field) + select_mop_field = ", p.base_paid_amount - p.change_amount as paid_amount " return frappe.db.sql( """ SELECT p.posting_date, p.name as pos_invoice, p.pos_profile, - p.owner, p.base_grand_total as grand_total, p.base_paid_amount - p.change_amount as paid_amount, - p.customer, p.is_return {select_mop_field} + p.owner, p.customer, p.is_return, p.base_grand_total as grand_total {select_mop_field} FROM `tabPOS Invoice` p {from_sales_invoice_payment} WHERE From 4ac60cd73bf953c9eac218e170d3fe09e1790a81 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Fri, 28 Jul 2023 12:54:39 +0530 Subject: [PATCH 015/501] perf: move project status reminder to hourly (backport #36372) (#36373) perf: move project status reminder to hourly (#36372) Only used for sending daily/weekly/bi-daily [skip ci] (cherry picked from commit e36c8ce5be4d8b7e89bf21a722fa223d74a70298) Co-authored-by: Ankush Menat --- erpnext/hooks.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/hooks.py b/erpnext/hooks.py index 16c488d90476..6b7d2dc26a23 100644 --- a/erpnext/hooks.py +++ b/erpnext/hooks.py @@ -413,11 +413,11 @@ ], }, "all": [ - "erpnext.projects.doctype.project.project.project_status_update_reminder", "erpnext.crm.doctype.social_media_post.social_media_post.process_scheduled_social_media_posts", ], "hourly": [ "erpnext.erpnext_integrations.doctype.plaid_settings.plaid_settings.automatic_synchronization", + "erpnext.projects.doctype.project.project.project_status_update_reminder", "erpnext.projects.doctype.project.project.hourly_reminder", "erpnext.projects.doctype.project.project.collect_project_status", ], From 6d051f57322f9404718815341b8a61f7bb37d5cb Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Fri, 28 Jul 2023 18:15:06 +0530 Subject: [PATCH 016/501] fix: removed "fetch_from" (backport #36365) (#36386) fix: removed "fetch_from" * fix: removed ("fetch_from": "goal.objective") The field ended up being disabled because of this. (cherry picked from commit 1c687a4afd43591e561cde29672a4d680c37d888) Co-authored-by: xdlumertz --- .../quality_goal_objective/quality_goal_objective.json | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/erpnext/quality_management/doctype/quality_goal_objective/quality_goal_objective.json b/erpnext/quality_management/doctype/quality_goal_objective/quality_goal_objective.json index e3dbd660b543..010888dd31fc 100644 --- a/erpnext/quality_management/doctype/quality_goal_objective/quality_goal_objective.json +++ b/erpnext/quality_management/doctype/quality_goal_objective/quality_goal_objective.json @@ -1,4 +1,5 @@ { + "actions": [], "autoname": "format:{####}", "creation": "2019-05-26 15:03:43.996455", "doctype": "DocType", @@ -12,7 +13,6 @@ ], "fields": [ { - "fetch_from": "goal.objective", "fieldname": "objective", "fieldtype": "Text", "in_list_view": 1, @@ -38,14 +38,17 @@ } ], "istable": 1, - "modified": "2019-05-26 16:12:54.832058", + "links": [], + "modified": "2023-07-28 18:10:23.351246", "modified_by": "Administrator", "module": "Quality Management", "name": "Quality Goal Objective", + "naming_rule": "Expression", "owner": "Administrator", "permissions": [], "quick_entry": 1, "sort_field": "modified", "sort_order": "DESC", + "states": [], "track_changes": 1 } \ No newline at end of file From 93bd4c7ff342a329ffa02d4f22bc642750311428 Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Fri, 28 Jul 2023 17:21:05 +0530 Subject: [PATCH 017/501] fix: change fieldtype from Currency to Float for the valuation rate in the stock report (cherry picked from commit c82cb379a56d73c9ae1f847f746fd6e106655b7c) --- erpnext/stock/report/stock_balance/stock_balance.py | 3 +-- erpnext/stock/report/stock_ledger/stock_ledger.py | 4 ++-- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/erpnext/stock/report/stock_balance/stock_balance.py b/erpnext/stock/report/stock_balance/stock_balance.py index 7c821700df3d..d362d818ebf9 100644 --- a/erpnext/stock/report/stock_balance/stock_balance.py +++ b/erpnext/stock/report/stock_balance/stock_balance.py @@ -430,10 +430,9 @@ def get_columns(self): { "label": _("Valuation Rate"), "fieldname": "val_rate", - "fieldtype": "Currency", + "fieldtype": "Float", "width": 90, "convertible": "rate", - "options": "currency", }, { "label": _("Company"), diff --git a/erpnext/stock/report/stock_ledger/stock_ledger.py b/erpnext/stock/report/stock_ledger/stock_ledger.py index 77bc4e004de7..ed28ed3ee46f 100644 --- a/erpnext/stock/report/stock_ledger/stock_ledger.py +++ b/erpnext/stock/report/stock_ledger/stock_ledger.py @@ -196,7 +196,7 @@ def get_columns(filters): { "label": _("Avg Rate (Balance Stock)"), "fieldname": "valuation_rate", - "fieldtype": "Currency", + "fieldtype": "Float", "width": 180, "options": "Company:company:default_currency", "convertible": "rate", @@ -204,7 +204,7 @@ def get_columns(filters): { "label": _("Valuation Rate"), "fieldname": "in_out_rate", - "fieldtype": "Currency", + "fieldtype": "Float", "width": 140, "options": "Company:company:default_currency", "convertible": "rate", From 2844d849e09ddba500efdb42008c98ae7277c3ce Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Fri, 28 Jul 2023 21:02:21 +0530 Subject: [PATCH 018/501] refactor(test): introduce and make use of mixins in unit tests (#36382) * refactor(test): create and use test mixin * chore(test): replace get_user_default with variable (cherry picked from commit 3b5805541017ae3abc2bafa17dcc698fe6922a73) --- .../test_deferred_revenue_and_expense.py | 194 ++++++------------ erpnext/accounts/test/accounts_mixin.py | 80 ++++++++ 2 files changed, 144 insertions(+), 130 deletions(-) create mode 100644 erpnext/accounts/test/accounts_mixin.py diff --git a/erpnext/accounts/report/deferred_revenue_and_expense/test_deferred_revenue_and_expense.py b/erpnext/accounts/report/deferred_revenue_and_expense/test_deferred_revenue_and_expense.py index c84b843f1fd8..28d0c20a9187 100644 --- a/erpnext/accounts/report/deferred_revenue_and_expense/test_deferred_revenue_and_expense.py +++ b/erpnext/accounts/report/deferred_revenue_and_expense/test_deferred_revenue_and_expense.py @@ -2,6 +2,7 @@ import frappe from frappe import qb +from frappe.tests.utils import FrappeTestCase, change_settings from frappe.utils import nowdate from erpnext.accounts.doctype.account.test_account import create_account @@ -10,16 +11,15 @@ from erpnext.accounts.report.deferred_revenue_and_expense.deferred_revenue_and_expense import ( Deferred_Revenue_and_Expense_Report, ) +from erpnext.accounts.test.accounts_mixin import AccountsTestMixin from erpnext.accounts.utils import get_fiscal_year from erpnext.buying.doctype.supplier.test_supplier import create_supplier from erpnext.stock.doctype.item.test_item import create_item -class TestDeferredRevenueAndExpense(unittest.TestCase): +class TestDeferredRevenueAndExpense(FrappeTestCase, AccountsTestMixin): @classmethod def setUpClass(self): - clear_accounts_and_items() - create_company() self.maxDiff = None def clear_old_entries(self): @@ -51,55 +51,58 @@ def clear_old_entries(self): if deferred_invoices: qb.from_(pinv).delete().where(pinv.name.isin(deferred_invoices)).run() - def test_deferred_revenue(self): - self.clear_old_entries() - + def setup_deferred_accounts_and_items(self): # created deferred expense accounts, if not found - deferred_revenue_account = create_account( + self.deferred_revenue_account = create_account( account_name="Deferred Revenue", - parent_account="Current Liabilities - _CD", - company="_Test Company DR", + parent_account="Current Liabilities - " + self.company_abbr, + company=self.company, ) - acc_settings = frappe.get_doc("Accounts Settings", "Accounts Settings") - acc_settings.book_deferred_entries_based_on = "Months" - acc_settings.save() + # created deferred expense accounts, if not found + self.deferred_expense_account = create_account( + account_name="Deferred Expense", + parent_account="Current Assets - " + self.company_abbr, + company=self.company, + ) + + def setUp(self): + self.create_company() + self.create_customer("_Test Customer") + self.create_supplier("_Test Furniture Supplier") + self.setup_deferred_accounts_and_items() + self.clear_old_entries() - customer = frappe.new_doc("Customer") - customer.customer_name = "_Test Customer DR" - customer.type = "Individual" - customer.insert() + def tearDown(self): + frappe.db.rollback() - item = create_item( - "_Test Internet Subscription", - is_stock_item=0, - warehouse="All Warehouses - _CD", - company="_Test Company DR", - ) + @change_settings("Accounts Settings", {"book_deferred_entries_based_on": "Months"}) + def test_deferred_revenue(self): + self.create_item("_Test Internet Subscription", 0, self.warehouse, self.company) + item = frappe.get_doc("Item", self.item) item.enable_deferred_revenue = 1 - item.deferred_revenue_account = deferred_revenue_account + item.deferred_revenue_account = self.deferred_revenue_account item.no_of_months = 3 item.save() si = create_sales_invoice( - item=item.name, - company="_Test Company DR", - customer="_Test Customer DR", - debit_to="Debtors - _CD", + item=self.item, + company=self.company, + customer=self.customer, + debit_to=self.debit_to, posting_date="2021-05-01", - parent_cost_center="Main - _CD", - cost_center="Main - _CD", + parent_cost_center=self.cost_center, + cost_center=self.cost_center, do_not_save=True, rate=300, price_list_rate=300, ) - si.items[0].income_account = "Sales - _CD" + si.items[0].income_account = self.income_account si.items[0].enable_deferred_revenue = 1 si.items[0].service_start_date = "2021-05-01" si.items[0].service_end_date = "2021-08-01" - si.items[0].deferred_revenue_account = deferred_revenue_account - si.items[0].income_account = "Sales - _CD" + si.items[0].deferred_revenue_account = self.deferred_revenue_account si.save() si.submit() @@ -110,7 +113,7 @@ def test_deferred_revenue(self): start_date="2021-05-01", end_date="2021-08-01", type="Income", - company="_Test Company DR", + company=self.company, ) ) pda.insert() @@ -120,7 +123,7 @@ def test_deferred_revenue(self): fiscal_year = frappe.get_doc("Fiscal Year", get_fiscal_year(date="2021-05-01")) self.filters = frappe._dict( { - "company": frappe.defaults.get_user_default("Company"), + "company": self.company, "filter_based_on": "Date Range", "period_start_date": "2021-05-01", "period_end_date": "2021-08-01", @@ -142,57 +145,36 @@ def test_deferred_revenue(self): ] self.assertEqual(report.period_total, expected) + @change_settings("Accounts Settings", {"book_deferred_entries_based_on": "Months"}) def test_deferred_expense(self): - self.clear_old_entries() - - # created deferred expense accounts, if not found - deferred_expense_account = create_account( - account_name="Deferred Expense", - parent_account="Current Assets - _CD", - company="_Test Company DR", - ) - - acc_settings = frappe.get_doc("Accounts Settings", "Accounts Settings") - acc_settings.book_deferred_entries_based_on = "Months" - acc_settings.save() - - supplier = create_supplier( - supplier_name="_Test Furniture Supplier", supplier_group="Local", supplier_type="Company" - ) - supplier.save() - - item = create_item( - "_Test Office Desk", - is_stock_item=0, - warehouse="All Warehouses - _CD", - company="_Test Company DR", - ) + self.create_item("_Test Office Desk", 0, self.warehouse, self.company) + item = frappe.get_doc("Item", self.item) item.enable_deferred_expense = 1 - item.deferred_expense_account = deferred_expense_account + item.deferred_expense_account = self.deferred_expense_account item.no_of_months_exp = 3 item.save() pi = make_purchase_invoice( - item=item.name, - company="_Test Company DR", - supplier="_Test Furniture Supplier", + item=self.item, + company=self.company, + supplier=self.supplier, is_return=False, update_stock=False, posting_date=frappe.utils.datetime.date(2021, 5, 1), - parent_cost_center="Main - _CD", - cost_center="Main - _CD", + parent_cost_center=self.cost_center, + cost_center=self.cost_center, do_not_save=True, rate=300, price_list_rate=300, - warehouse="All Warehouses - _CD", + warehouse=self.warehouse, qty=1, ) pi.set_posting_time = True pi.items[0].enable_deferred_expense = 1 pi.items[0].service_start_date = "2021-05-01" pi.items[0].service_end_date = "2021-08-01" - pi.items[0].deferred_expense_account = deferred_expense_account - pi.items[0].expense_account = "Office Maintenance Expenses - _CD" + pi.items[0].deferred_expense_account = self.deferred_expense_account + pi.items[0].expense_account = self.expense_account pi.save() pi.submit() @@ -203,7 +185,7 @@ def test_deferred_expense(self): start_date="2021-05-01", end_date="2021-08-01", type="Expense", - company="_Test Company DR", + company=self.company, ) ) pda.insert() @@ -213,7 +195,7 @@ def test_deferred_expense(self): fiscal_year = frappe.get_doc("Fiscal Year", get_fiscal_year(date="2021-05-01")) self.filters = frappe._dict( { - "company": frappe.defaults.get_user_default("Company"), + "company": self.company, "filter_based_on": "Date Range", "period_start_date": "2021-05-01", "period_end_date": "2021-08-01", @@ -235,52 +217,31 @@ def test_deferred_expense(self): ] self.assertEqual(report.period_total, expected) + @change_settings("Accounts Settings", {"book_deferred_entries_based_on": "Months"}) def test_zero_months(self): - self.clear_old_entries() - # created deferred expense accounts, if not found - deferred_revenue_account = create_account( - account_name="Deferred Revenue", - parent_account="Current Liabilities - _CD", - company="_Test Company DR", - ) - - acc_settings = frappe.get_doc("Accounts Settings", "Accounts Settings") - acc_settings.book_deferred_entries_based_on = "Months" - acc_settings.save() - - customer = frappe.new_doc("Customer") - customer.customer_name = "_Test Customer DR" - customer.type = "Individual" - customer.insert() - - item = create_item( - "_Test Internet Subscription", - is_stock_item=0, - warehouse="All Warehouses - _CD", - company="_Test Company DR", - ) + self.create_item("_Test Internet Subscription", 0, self.warehouse, self.company) + item = frappe.get_doc("Item", self.item) item.enable_deferred_revenue = 1 - item.deferred_revenue_account = deferred_revenue_account + item.deferred_revenue_account = self.deferred_revenue_account item.no_of_months = 0 item.save() si = create_sales_invoice( item=item.name, - company="_Test Company DR", - customer="_Test Customer DR", - debit_to="Debtors - _CD", + company=self.company, + customer=self.customer, + debit_to=self.debit_to, posting_date="2021-05-01", - parent_cost_center="Main - _CD", - cost_center="Main - _CD", + parent_cost_center=self.cost_center, + cost_center=self.cost_center, do_not_save=True, rate=300, price_list_rate=300, ) si.items[0].enable_deferred_revenue = 1 - si.items[0].income_account = "Sales - _CD" - si.items[0].deferred_revenue_account = deferred_revenue_account - si.items[0].income_account = "Sales - _CD" + si.items[0].income_account = self.income_account + si.items[0].deferred_revenue_account = self.deferred_revenue_account si.save() si.submit() @@ -291,7 +252,7 @@ def test_zero_months(self): start_date="2021-05-01", end_date="2021-08-01", type="Income", - company="_Test Company DR", + company=self.company, ) ) pda.insert() @@ -301,7 +262,7 @@ def test_zero_months(self): fiscal_year = frappe.get_doc("Fiscal Year", get_fiscal_year(date="2021-05-01")) self.filters = frappe._dict( { - "company": frappe.defaults.get_user_default("Company"), + "company": self.company, "filter_based_on": "Date Range", "period_start_date": "2021-05-01", "period_end_date": "2021-08-01", @@ -322,30 +283,3 @@ def test_zero_months(self): {"key": "aug_2021", "total": 0, "actual": 0}, ] self.assertEqual(report.period_total, expected) - - -def create_company(): - company = frappe.db.exists("Company", "_Test Company DR") - if not company: - company = frappe.new_doc("Company") - company.company_name = "_Test Company DR" - company.default_currency = "INR" - company.chart_of_accounts = "Standard" - company.insert() - - -def clear_accounts_and_items(): - item = qb.DocType("Item") - account = qb.DocType("Account") - customer = qb.DocType("Customer") - supplier = qb.DocType("Supplier") - - qb.from_(account).delete().where( - (account.account_name == "Deferred Revenue") - | (account.account_name == "Deferred Expense") & (account.company == "_Test Company DR") - ).run() - qb.from_(item).delete().where( - (item.item_code == "_Test Internet Subscription") | (item.item_code == "_Test Office Rent") - ).run() - qb.from_(customer).delete().where(customer.customer_name == "_Test Customer DR").run() - qb.from_(supplier).delete().where(supplier.supplier_name == "_Test Furniture Supplier").run() diff --git a/erpnext/accounts/test/accounts_mixin.py b/erpnext/accounts/test/accounts_mixin.py new file mode 100644 index 000000000000..c82164ef6447 --- /dev/null +++ b/erpnext/accounts/test/accounts_mixin.py @@ -0,0 +1,80 @@ +import frappe + +from erpnext.stock.doctype.item.test_item import create_item + + +class AccountsTestMixin: + def create_customer(self, customer_name, currency=None): + if not frappe.db.exists("Customer", customer_name): + customer = frappe.new_doc("Customer") + customer.customer_name = customer_name + customer.type = "Individual" + + if currency: + customer.default_currency = currency + customer.save() + self.customer = customer.name + else: + self.customer = customer_name + + def create_supplier(self, supplier_name, currency=None): + if not frappe.db.exists("Supplier", supplier_name): + supplier = frappe.new_doc("Supplier") + supplier.supplier_name = supplier_name + supplier.supplier_type = "Individual" + supplier.supplier_group = "Local" + + if currency: + supplier.default_currency = currency + supplier.save() + self.supplier = supplier.name + else: + self.supplier = supplier_name + + def create_item(self, item_name, is_stock=0, warehouse=None, company=None): + item = create_item(item_name, is_stock_item=is_stock, warehouse=warehouse, company=company) + self.item = item.name + + def create_company(self, company_name="_Test Company", abbr="_TC"): + self.company_abbr = abbr + if frappe.db.exists("Company", company_name): + company = frappe.get_doc("Company", company_name) + else: + company = frappe.get_doc( + { + "doctype": "Company", + "company_name": company_name, + "country": "India", + "default_currency": "INR", + "create_chart_of_accounts_based_on": "Standard Template", + "chart_of_accounts": "Standard", + } + ) + company = company.save() + + self.company = company.name + self.cost_center = company.cost_center + self.warehouse = "Stores - " + abbr + self.finished_warehouse = "Finished Goods - " + abbr + self.income_account = "Sales - " + abbr + self.expense_account = "Cost of Goods Sold - " + abbr + self.debit_to = "Debtors - " + abbr + self.debit_usd = "Debtors USD - " + abbr + self.cash = "Cash - " + abbr + self.creditors = "Creditors - " + abbr + + # create bank account + bank_account = "HDFC - " + abbr + if frappe.db.exists("Account", bank_account): + self.bank = bank_account + else: + bank_acc = frappe.get_doc( + { + "doctype": "Account", + "account_name": "HDFC", + "parent_account": "Bank Accounts - " + abbr, + "company": self.company, + } + ) + bank_acc.save() + self.bank = bank_acc.name From 7b3bcd3bc4933513daa05b2edad667f1bc30abf7 Mon Sep 17 00:00:00 2001 From: ramonalmato Date: Fri, 28 Jul 2023 10:57:31 +0200 Subject: [PATCH 019/501] fix: Job Card validation fixed when displaying total completed quantity (cherry picked from commit 49981fecc72acb387642689a47a2813d72543bc7) --- erpnext/manufacturing/doctype/job_card/job_card.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/erpnext/manufacturing/doctype/job_card/job_card.py b/erpnext/manufacturing/doctype/job_card/job_card.py index 2c17568d1b4f..d26006f8ce89 100644 --- a/erpnext/manufacturing/doctype/job_card/job_card.py +++ b/erpnext/manufacturing/doctype/job_card/job_card.py @@ -498,12 +498,12 @@ def validate_job_card(self): if self.for_quantity and flt(total_completed_qty, precision) != flt( self.for_quantity, precision ): - total_completed_qty = bold(_("Total Completed Qty")) + total_completed_qty_label = bold(_("Total Completed Qty")) qty_to_manufacture = bold(_("Qty to Manufacture")) frappe.throw( _("The {0} ({1}) must be equal to {2} ({3})").format( - total_completed_qty, + total_completed_qty_label, bold(flt(total_completed_qty, precision)), qty_to_manufacture, bold(self.for_quantity), From 32bdb7cccdf60b49496f778667a8eba03a5d98d4 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Sat, 29 Jul 2023 18:14:51 +0530 Subject: [PATCH 020/501] fix: only publish repost progress to doc subscriber (backport #36400) (#36402) * fix: only publish repost progress to doc subscriber (#36400) Huge size of string gets blasted to everyone on site. Due to some memory leak (cause unknown) till sockets are open the strings are also in process' memory. related https://github.com/frappe/frappe/issues/21863 (cherry picked from commit c0642cf528e2054192f5d16dd9d5c1a8162069cf) # Conflicts: # erpnext/stock/stock_ledger.py * chore: conflicts --------- Co-authored-by: Ankush Menat --- erpnext/stock/stock_ledger.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/erpnext/stock/stock_ledger.py b/erpnext/stock/stock_ledger.py index 4f8f06023deb..3a4191953538 100644 --- a/erpnext/stock/stock_ledger.py +++ b/erpnext/stock/stock_ledger.py @@ -288,6 +288,8 @@ def update_args_in_repost_item_valuation( frappe.publish_realtime( "item_reposting_progress", {"name": doc.name, "items_to_be_repost": json.dumps(args, default=str), "current_index": index}, + doctype=doc.doctype, + docname=doc.name, ) From 57b19a523efd4f6d22da8e25c6370710b8320579 Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Sat, 29 Jul 2023 19:42:23 +0530 Subject: [PATCH 021/501] fix: incorrect qty set in the serial no picker --- .../js/utils/serial_no_batch_selector.js | 23 ++++++++++++++----- 1 file changed, 17 insertions(+), 6 deletions(-) diff --git a/erpnext/public/js/utils/serial_no_batch_selector.js b/erpnext/public/js/utils/serial_no_batch_selector.js index 64c5ee59dc81..22120988ad0f 100644 --- a/erpnext/public/js/utils/serial_no_batch_selector.js +++ b/erpnext/public/js/utils/serial_no_batch_selector.js @@ -510,30 +510,41 @@ erpnext.SerialNoBatchSelector = class SerialNoBatchSelector { if(!list_value) { new_line = ''; } else { - me.serial_list = list_value.replace(/\n/g, ' ').match(/\S+/g) || []; + me.serial_list = list_value.split(/\n/g) || []; } if(!me.serial_list.includes(new_number)) { this.set_new_description(''); serial_no_list_field.set_value(me.serial_list.join('\n') + new_line + new_number); - me.serial_list = serial_no_list_field.get_value().replace(/\n/g, ' ').match(/\S+/g) || []; + me.serial_list = serial_no_list_field.get_value().split(/\n/g) || []; } else { this.set_new_description(new_number + ' is already selected.'); } + me.serial_list = me.serial_list.filter(serial => { + if (serial) { + return true; + } + }); + qty_field.set_input(me.serial_list.length); this.$input.val(""); this.in_local_change = 0; } }, - {fieldtype: 'Column Break'}, + {fieldtype: 'Section Break'}, { fieldname: 'serial_no', - fieldtype: 'Small Text', + fieldtype: 'Text', label: __(me.has_batch && !me.has_serial_no ? 'Selected Batch Numbers' : 'Selected Serial Numbers'), onchange: function() { - me.serial_list = this.get_value() - .replace(/\n/g, ' ').match(/\S+/g) || []; + me.serial_list = this.get_value().split(/\n/g); + me.serial_list = me.serial_list.filter(serial => { + if (serial) { + return true; + } + }); + this.layout.fields_dict.qty.set_input(me.serial_list.length); } } From a799e1b2179114bbde036454ffbd86015cffcbfd Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Sun, 30 Jul 2023 10:56:49 +0530 Subject: [PATCH 022/501] fix: multiple issues related to Production Plan (backport #36377) (#36381) * fix: multiple issues related to Production Plan (cherry picked from commit 1c2148b637c9bd02bec718a3abb298a89881601b) # Conflicts: # erpnext/selling/doctype/sales_order_item/sales_order_item.json * chore: fixed conflicts --------- Co-authored-by: Rohit Waghchaure --- .../production_plan/production_plan.js | 79 +++++++----- .../production_plan/production_plan.json | 10 +- .../production_plan/production_plan.py | 122 +++++++++++++++++- .../production_plan/test_production_plan.py | 96 ++++++++++++++ .../sales_order_item/sales_order_item.json | 10 +- 5 files changed, 277 insertions(+), 40 deletions(-) diff --git a/erpnext/manufacturing/doctype/production_plan/production_plan.js b/erpnext/manufacturing/doctype/production_plan/production_plan.js index 48986910b07c..46c554c1e80d 100644 --- a/erpnext/manufacturing/doctype/production_plan/production_plan.js +++ b/erpnext/manufacturing/doctype/production_plan/production_plan.js @@ -9,19 +9,25 @@ frappe.ui.form.on('Production Plan', { item.temporary_name = item.name; }); }, + setup(frm) { + frm.trigger("setup_queries"); + frm.custom_make_buttons = { 'Work Order': 'Work Order / Subcontract PO', 'Material Request': 'Material Request', }; + }, - frm.fields_dict['po_items'].grid.get_field('warehouse').get_query = function(doc) { + setup_queries(frm) { + frm.set_query("sales_order", "sales_orders", () => { return { + query: "erpnext.manufacturing.doctype.production_plan.production_plan.sales_order_query", filters: { - company: doc.company + company: frm.doc.company, } } - } + }); frm.set_query('for_warehouse', function(doc) { return { @@ -42,32 +48,40 @@ frappe.ui.form.on('Production Plan', { }; }); - frm.fields_dict['po_items'].grid.get_field('item_code').get_query = function(doc) { + frm.set_query("item_code", "po_items", (doc, cdt, cdn) => { return { query: "erpnext.controllers.queries.item_query", filters:{ 'is_stock_item': 1, } } - } + }); - frm.fields_dict['po_items'].grid.get_field('bom_no').get_query = function(doc, cdt, cdn) { + frm.set_query("bom_no", "po_items", (doc, cdt, cdn) => { var d = locals[cdt][cdn]; if (d.item_code) { return { query: "erpnext.controllers.queries.bom", - filters:{'item': cstr(d.item_code), 'docstatus': 1} + filters:{'item': d.item_code, 'docstatus': 1} } } else frappe.msgprint(__("Please enter Item first")); - } + }); - frm.fields_dict['mr_items'].grid.get_field('warehouse').get_query = function(doc) { + frm.set_query("warehouse", "mr_items", (doc) => { return { filters: { company: doc.company } } - } + }); + + frm.set_query("warehouse", "po_items", (doc) => { + return { + filters: { + company: doc.company + } + } + }); }, refresh(frm) { @@ -436,7 +450,7 @@ frappe.ui.form.on("Production Plan Item", { } }); } - } + }, }); frappe.ui.form.on("Material Request Plan Item", { @@ -467,30 +481,35 @@ frappe.ui.form.on("Material Request Plan Item", { frappe.ui.form.on("Production Plan Sales Order", { sales_order(frm, cdt, cdn) { - const { sales_order } = locals[cdt][cdn]; + let row = locals[cdt][cdn]; + const sales_order = row.sales_order; if (!sales_order) { return; } - frappe.call({ - method: "erpnext.manufacturing.doctype.production_plan.production_plan.get_so_details", - args: { sales_order }, - callback(r) { - const {transaction_date, customer, grand_total} = r.message; - frappe.model.set_value(cdt, cdn, 'sales_order_date', transaction_date); - frappe.model.set_value(cdt, cdn, 'customer', customer); - frappe.model.set_value(cdt, cdn, 'grand_total', grand_total); - } - }); - } -}); -cur_frm.fields_dict['sales_orders'].grid.get_field("sales_order").get_query = function() { - return{ - filters: [ - ['Sales Order','docstatus', '=' ,1] - ] + if (row.sales_order) { + frm.call({ + method: "validate_sales_orders", + doc: frm.doc, + args: { + sales_order: row.sales_order, + }, + callback(r) { + frappe.call({ + method: "erpnext.manufacturing.doctype.production_plan.production_plan.get_so_details", + args: { sales_order }, + callback(r) { + const {transaction_date, customer, grand_total} = r.message; + frappe.model.set_value(cdt, cdn, 'sales_order_date', transaction_date); + frappe.model.set_value(cdt, cdn, 'customer', customer); + frappe.model.set_value(cdt, cdn, 'grand_total', grand_total); + } + }); + } + }); + } } -}; +}); frappe.tour['Production Plan'] = [ { diff --git a/erpnext/manufacturing/doctype/production_plan/production_plan.json b/erpnext/manufacturing/doctype/production_plan/production_plan.json index 232f1cb2c446..0d0fd5e27060 100644 --- a/erpnext/manufacturing/doctype/production_plan/production_plan.json +++ b/erpnext/manufacturing/doctype/production_plan/production_plan.json @@ -228,10 +228,10 @@ }, { "default": "0", - "description": "To know more about projected quantity, click here.", + "description": "If enabled, the system won't create material requests for the available items.", "fieldname": "ignore_existing_ordered_qty", "fieldtype": "Check", - "label": "Ignore Existing Projected Quantity" + "label": "Ignore Available Stock" }, { "fieldname": "column_break_25", @@ -339,7 +339,7 @@ "depends_on": "eval:doc.get_items_from == 'Sales Order'", "fieldname": "combine_items", "fieldtype": "Check", - "label": "Consolidate Items" + "label": "Consolidate Sales Order Items" }, { "fieldname": "section_break_25", @@ -399,7 +399,7 @@ }, { "default": "0", - "description": "System consider the projected quantity to check available or will be available sub-assembly items ", + "description": "If this checkbox is enabled, then the system won\u2019t run the MRP for the available sub-assembly items.", "fieldname": "skip_available_sub_assembly_item", "fieldtype": "Check", "label": "Skip Available Sub Assembly Items" @@ -422,7 +422,7 @@ "index_web_pages_for_search": 1, "is_submittable": 1, "links": [], - "modified": "2023-05-22 23:36:31.770517", + "modified": "2023-07-28 13:37:43.926686", "modified_by": "Administrator", "module": "Manufacturing", "name": "Production Plan", diff --git a/erpnext/manufacturing/doctype/production_plan/production_plan.py b/erpnext/manufacturing/doctype/production_plan/production_plan.py index d8cc8f6d3953..261aa76b7067 100644 --- a/erpnext/manufacturing/doctype/production_plan/production_plan.py +++ b/erpnext/manufacturing/doctype/production_plan/production_plan.py @@ -39,6 +39,36 @@ def validate(self): self.set_status() self._rename_temporary_references() validate_uom_is_integer(self, "stock_uom", "planned_qty") + self.validate_sales_orders() + + @frappe.whitelist() + def validate_sales_orders(self, sales_order=None): + sales_orders = [] + + if sales_order: + sales_orders.append(sales_order) + else: + sales_orders = [row.sales_order for row in self.sales_orders if row.sales_order] + + data = sales_order_query(filters={"company": self.company, "sales_orders": sales_orders}) + + title = _("Production Plan Already Submitted") + if not data: + msg = _("No items are available in the sales order {0} for production").format(sales_orders[0]) + if len(sales_orders) > 1: + sales_orders = ", ".join(sales_orders) + msg = _("No items are available in sales orders {0} for production").format(sales_orders) + + frappe.throw(msg, title=title) + + data = [d[0] for d in data] + + for sales_order in sales_orders: + if sales_order not in data: + frappe.throw( + _("No items are available in the sales order {0} for production").format(sales_order), + title=title, + ) def set_pending_qty_in_row_without_reference(self): "Set Pending Qty in independent rows (not from SO or MR)." @@ -205,6 +235,7 @@ def get_so_items(self): ).as_("pending_qty"), so_item.description, so_item.name, + so_item.bom_no, ) .distinct() .where( @@ -342,7 +373,7 @@ def add_items(self, items): "item_code": data.item_code, "description": data.description or item_details.description, "stock_uom": item_details and item_details.stock_uom or "", - "bom_no": item_details and item_details.bom_no or "", + "bom_no": data.bom_no or item_details and item_details.bom_no or "", "planned_qty": data.pending_qty, "pending_qty": data.pending_qty, "planned_start_date": now_datetime(), @@ -401,11 +432,50 @@ def update_produced_pending_qty(self, produced_qty, production_plan_item): def on_submit(self): self.update_bin_qty() + self.update_sales_order() def on_cancel(self): self.db_set("status", "Cancelled") self.delete_draft_work_order() self.update_bin_qty() + self.update_sales_order() + + def update_sales_order(self): + sales_orders = [row.sales_order for row in self.po_items if row.sales_order] + if sales_orders: + so_wise_planned_qty = self.get_so_wise_planned_qty(sales_orders) + + for row in self.po_items: + if not row.sales_order and not row.sales_order_item: + continue + + key = (row.sales_order, row.sales_order_item) + frappe.db.set_value( + "Sales Order Item", + row.sales_order_item, + "production_plan_qty", + flt(so_wise_planned_qty.get(key)), + ) + + @staticmethod + def get_so_wise_planned_qty(sales_orders): + so_wise_planned_qty = frappe._dict() + data = frappe.get_all( + "Production Plan Item", + fields=["sales_order", "sales_order_item", "SUM(planned_qty) as qty"], + filters={ + "sales_order": ("in", sales_orders), + "docstatus": 1, + "sales_order_item": ("is", "set"), + }, + group_by="sales_order, sales_order_item", + ) + + for row in data: + key = (row.sales_order, row.sales_order_item) + so_wise_planned_qty[key] = row.qty + + return so_wise_planned_qty def update_bin_qty(self): for d in self.mr_items: @@ -719,6 +789,9 @@ def get_sub_assembly_items(self, manufacturing_type=None): sub_assembly_items_store = [] # temporary store to process all subassembly items for row in self.po_items: + if self.skip_available_sub_assembly_item and not row.warehouse: + frappe.throw(_("Row #{0}: Please select the FG Warehouse in Assembly Items").format(row.idx)) + if not row.item_code: frappe.throw(_("Row #{0}: Please select Item Code in Assembly Items").format(row.idx)) @@ -1142,7 +1215,7 @@ def get_sales_orders(self): & (so.docstatus == 1) & (so.status.notin(["Stopped", "Closed"])) & (so.company == self.company) - & (so_item.qty > so_item.work_order_qty) + & (so_item.qty > so_item.production_plan_qty) ) ) @@ -1566,7 +1639,6 @@ def get_reserved_qty_for_production_plan(item_code, warehouse): def get_raw_materials_of_sub_assembly_items( item_details, company, bom_no, include_non_stock_items, sub_assembly_items, planned_qty=1 ): - bei = frappe.qb.DocType("BOM Item") bom = frappe.qb.DocType("BOM") item = frappe.qb.DocType("Item") @@ -1609,7 +1681,10 @@ def get_raw_materials_of_sub_assembly_items( for item in items: key = (item.item_code, item.bom_no) - if item.bom_no and key in sub_assembly_items: + if item.bom_no and key not in sub_assembly_items: + continue + + if item.bom_no: planned_qty = flt(sub_assembly_items[key]) get_raw_materials_of_sub_assembly_items( item_details, @@ -1626,3 +1701,42 @@ def get_raw_materials_of_sub_assembly_items( item_details.setdefault(item.get("item_code"), item) return item_details + + +@frappe.whitelist() +def sales_order_query( + doctype=None, txt=None, searchfield=None, start=None, page_len=None, filters=None +): + frappe.has_permission("Production Plan", throw=True) + + if not filters: + filters = {} + + so_table = frappe.qb.DocType("Sales Order") + table = frappe.qb.DocType("Sales Order Item") + + query = ( + frappe.qb.from_(so_table) + .join(table) + .on(table.parent == so_table.name) + .select(table.parent) + .distinct() + .where((table.qty > table.production_plan_qty) & (table.docstatus == 1)) + ) + + if filters.get("company"): + query = query.where(so_table.company == filters.get("company")) + + if filters.get("sales_orders"): + query = query.where(so_table.name.isin(filters.get("sales_orders"))) + + if txt: + query = query.where(table.item_code.like(f"{txt}%")) + + if page_len: + query = query.limit(page_len) + + if start: + query = query.offset(start) + + return query.run() diff --git a/erpnext/manufacturing/doctype/production_plan/test_production_plan.py b/erpnext/manufacturing/doctype/production_plan/test_production_plan.py index f60dbfc3f55b..2871a29d7682 100644 --- a/erpnext/manufacturing/doctype/production_plan/test_production_plan.py +++ b/erpnext/manufacturing/doctype/production_plan/test_production_plan.py @@ -225,6 +225,102 @@ def test_production_plan_sales_orders(self): self.assertEqual(sales_orders, []) + def test_donot_allow_to_make_multiple_pp_against_same_so(self): + item = "Test SO Production Item 1" + create_item(item) + + raw_material = "Test SO RM Production Item 1" + create_item(raw_material) + + if not frappe.db.get_value("BOM", {"item": item}): + make_bom(item=item, raw_materials=[raw_material]) + + so = make_sales_order(item_code=item, qty=4) + pln = frappe.new_doc("Production Plan") + pln.company = so.company + pln.get_items_from = "Sales Order" + + pln.append( + "sales_orders", + { + "sales_order": so.name, + "sales_order_date": so.transaction_date, + "customer": so.customer, + "grand_total": so.grand_total, + }, + ) + + pln.get_so_items() + pln.submit() + + pln = frappe.new_doc("Production Plan") + pln.company = so.company + pln.get_items_from = "Sales Order" + + pln.append( + "sales_orders", + { + "sales_order": so.name, + "sales_order_date": so.transaction_date, + "customer": so.customer, + "grand_total": so.grand_total, + }, + ) + + pln.get_so_items() + self.assertRaises(frappe.ValidationError, pln.save) + + def test_so_based_bill_of_material(self): + item = "Test SO Production Item 1" + create_item(item) + + raw_material = "Test SO RM Production Item 1" + create_item(raw_material) + + bom1 = make_bom(item=item, raw_materials=[raw_material]) + + so = make_sales_order(item_code=item, qty=4) + + # Create new BOM and assign to new sales order + bom2 = make_bom(item=item, raw_materials=[raw_material]) + so2 = make_sales_order(item_code=item, qty=4) + + pln1 = frappe.new_doc("Production Plan") + pln1.company = so.company + pln1.get_items_from = "Sales Order" + + pln1.append( + "sales_orders", + { + "sales_order": so.name, + "sales_order_date": so.transaction_date, + "customer": so.customer, + "grand_total": so.grand_total, + }, + ) + + pln1.get_so_items() + + self.assertEqual(pln1.po_items[0].bom_no, bom1.name) + + pln2 = frappe.new_doc("Production Plan") + pln2.company = so2.company + pln2.get_items_from = "Sales Order" + + pln2.append( + "sales_orders", + { + "sales_order": so2.name, + "sales_order_date": so2.transaction_date, + "customer": so2.customer, + "grand_total": so2.grand_total, + }, + ) + + pln2.get_so_items() + + self.assertEqual(pln2.po_items[0].bom_no, bom2.name) + def test_production_plan_combine_items(self): "Test combining FG items in Production Plan." item = "Test Production Item 1" diff --git a/erpnext/selling/doctype/sales_order_item/sales_order_item.json b/erpnext/selling/doctype/sales_order_item/sales_order_item.json index 50ae3a3f1a92..a3e9977244cf 100644 --- a/erpnext/selling/doctype/sales_order_item/sales_order_item.json +++ b/erpnext/selling/doctype/sales_order_item/sales_order_item.json @@ -82,6 +82,7 @@ "actual_qty", "ordered_qty", "planned_qty", + "production_plan_qty", "column_break_69", "work_order_qty", "delivered_qty", @@ -860,12 +861,19 @@ "fieldname": "material_request_item", "fieldtype": "Data", "label": "Material Request Item" + }, + { + "fieldname": "production_plan_qty", + "fieldtype": "Float", + "label": "Production Plan Qty", + "no_copy": 1, + "read_only": 1 } ], "idx": 1, "istable": 1, "links": [], - "modified": "2022-12-25 02:51:10.247569", + "modified": "2023-07-28 14:56:42.031636", "modified_by": "Administrator", "module": "Selling", "name": "Sales Order Item", From 9c8e2a33e9f418ce3569e584ff05a5b1f7d149ec Mon Sep 17 00:00:00 2001 From: barredterra <14891507+barredterra@users.noreply.github.com> Date: Tue, 25 Jul 2023 21:42:01 +0200 Subject: [PATCH 023/501] fix: german translations (cherry picked from commit 3558c3d24e3a6dbc10a2d20a03a50a894f359b00) --- erpnext/translations/de.csv | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/erpnext/translations/de.csv b/erpnext/translations/de.csv index 840ba6b9d0d3..b40eeca67bfb 100644 --- a/erpnext/translations/de.csv +++ b/erpnext/translations/de.csv @@ -1147,6 +1147,7 @@ Get Items from Prescriptions,Holen Sie sich Artikel aus Verordnungen, Get Items from Product Bundle,Artikel aus dem Produkt-Bundle übernehmen, Get Suppliers,Holen Sie sich Lieferanten, Get Suppliers By,Holen Sie sich Lieferanten durch, +Get Supplier Group Details,Werte aus Lieferantengruppe übernehmen, Get Updates,Newsletter abonnieren, Get customers from,Holen Sie Kunden von, Get from Patient Encounter,Von der Patientenbegegnung erhalten, @@ -5066,6 +5067,7 @@ Credit To,Gutschreiben auf, Party Account Currency,Währung des Kontos der Partei, Against Expense Account,Zu Aufwandskonto, Inter Company Invoice Reference,Unternehmensübergreifende Rechnungsreferenz, +Internal Supplier,Interner Lieferant, Is Internal Supplier,Ist interner Lieferant, Start date of current invoice's period,Startdatum der laufenden Rechnungsperiode, End date of current invoice's period,Schlußdatum der laufenden Eingangsrechnungsperiode, @@ -5514,6 +5516,8 @@ Tracking,Verfolgung, Ref SQ,Ref-SQ, Inter Company Order Reference,Inter Company Bestellreferenz, Supplier Part Number,Lieferanten-Artikelnummer, +Supplier Primary Contact,Hauptkontakt des Lieferanten, +Supplier Primary Address,Hauptadresse des Lieferanten, Billed Amt,Rechnungsbetrag, Warehouse and Reference,Lager und Referenz, To be delivered to customer,Zur Auslieferung an den Kunden, @@ -7672,7 +7676,7 @@ Accounts Manager,Buchhalter, Allow Sales Invoice Creation Without Sales Order,Ermöglichen Sie die Erstellung von Kundenrechnungen ohne Auftrag, Allow Sales Invoice Creation Without Delivery Note,Ermöglichen Sie die Erstellung einer Ausgangsrechnung ohne Lieferschein, Default Price List,Standardpreisliste, -Primary Address and Contact Detail,Primäre Adresse und Kontaktdetails, +Primary Address and Contact,Hauptadresse und -kontakt, "Select, to make the customer searchable with these fields","Wählen Sie, um den Kunden mit diesen Feldern durchsuchbar zu machen", Customer Primary Contact,Hauptkontakt des Kunden, "Reselect, if the chosen contact is edited after save","Wählen Sie erneut, wenn der ausgewählte Kontakt nach dem Speichern bearbeitet wird", From cd3b85ccff227cf18db950d5e2a883d1a681b021 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Sun, 30 Jul 2023 18:43:01 +0530 Subject: [PATCH 024/501] fix: in payment_entry 'Unallocated Amount' cal is broken (#36369) fix: in payment_entry 'Unallocated Amount' cal is broken (cherry picked from commit f9fa34ff4058316af87fc03eb4131e5f58524408) Co-authored-by: Ashish Shah --- erpnext/accounts/doctype/payment_entry/payment_entry.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/erpnext/accounts/doctype/payment_entry/payment_entry.js b/erpnext/accounts/doctype/payment_entry/payment_entry.js index f91d29a6b68b..74a90fe2e8f6 100644 --- a/erpnext/accounts/doctype/payment_entry/payment_entry.js +++ b/erpnext/accounts/doctype/payment_entry/payment_entry.js @@ -894,12 +894,12 @@ frappe.ui.form.on('Payment Entry', { if(frm.doc.payment_type == "Receive" && frm.doc.base_total_allocated_amount < frm.doc.base_received_amount + total_deductions && frm.doc.total_allocated_amount < frm.doc.paid_amount + (total_deductions / frm.doc.source_exchange_rate)) { - unallocated_amount = (frm.doc.base_received_amount + total_deductions + frm.doc.base_total_taxes_and_charges + unallocated_amount = (frm.doc.base_received_amount + total_deductions + flt(frm.doc.base_total_taxes_and_charges) - frm.doc.base_total_allocated_amount) / frm.doc.source_exchange_rate; } else if (frm.doc.payment_type == "Pay" && frm.doc.base_total_allocated_amount < frm.doc.base_paid_amount - total_deductions && frm.doc.total_allocated_amount < frm.doc.received_amount + (total_deductions / frm.doc.target_exchange_rate)) { - unallocated_amount = (frm.doc.base_paid_amount + frm.doc.base_total_taxes_and_charges - (total_deductions + unallocated_amount = (frm.doc.base_paid_amount + flt(frm.doc.base_total_taxes_and_charges) - (total_deductions + frm.doc.base_total_allocated_amount)) / frm.doc.target_exchange_rate; } } From 289dc3669610ee17681aab7cd4436f653fb5bd6a Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Mon, 31 Jul 2023 17:38:02 +0530 Subject: [PATCH 025/501] fix: process_owner is not link User (backport #36420) (#36422) fix: process_owner is not link User (#36420) -Changed "fetch from" since field is not a binding field -Change field "full_name" from Hidden to Read Only (cherry picked from commit 05b07e098a7f7e5b11f0a18bd62005dc098c5995) Co-authored-by: xdlumertz --- .../doctype/non_conformance/non_conformance.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/erpnext/quality_management/doctype/non_conformance/non_conformance.json b/erpnext/quality_management/doctype/non_conformance/non_conformance.json index 8dfe2d6859d4..e6b87449ced3 100644 --- a/erpnext/quality_management/doctype/non_conformance/non_conformance.json +++ b/erpnext/quality_management/doctype/non_conformance/non_conformance.json @@ -62,10 +62,10 @@ "fieldtype": "Column Break" }, { - "fetch_from": "process_owner.full_name", + "fetch_from": "procedure.process_owner_full_name", "fieldname": "full_name", "fieldtype": "Data", - "hidden": 1, + "read_only": 1, "label": "Full Name" }, { @@ -81,7 +81,7 @@ ], "index_web_pages_for_search": 1, "links": [], - "modified": "2021-02-26 15:27:47.247814", + "modified": "2023-07-31 08:10:47.247814", "modified_by": "Administrator", "module": "Quality Management", "name": "Non Conformance", From 26a0b3b3800b06866c9b6c5de76f3c086cd7f1ca Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Mon, 31 Jul 2023 17:39:11 +0530 Subject: [PATCH 026/501] perf: use `LEFT JOIN` instead of `NOT EXISTS` (backport #36221) (#36383) * perf: use `LEFT JOIN` instead of `NOT EXISTS` (cherry picked from commit 58d867503b38fb72c850a2008ec5f33455e643d0) # Conflicts: # erpnext/manufacturing/doctype/bom_update_log/bom_updation_utils.py * fix: long queue `process_boms_cost_level_wise` (cherry picked from commit 148d466ae53b4754b5ea7209d9d67f6731f6a2d3) * chore: `conflicts` --------- Co-authored-by: s-aga-r --- .../doctype/bom_update_log/bom_update_log.py | 1 + .../bom_update_log/bom_updation_utils.py | 19 +++++++++++++------ 2 files changed, 14 insertions(+), 6 deletions(-) diff --git a/erpnext/manufacturing/doctype/bom_update_log/bom_update_log.py b/erpnext/manufacturing/doctype/bom_update_log/bom_update_log.py index 17b5aae96665..e9867468f98f 100644 --- a/erpnext/manufacturing/doctype/bom_update_log/bom_update_log.py +++ b/erpnext/manufacturing/doctype/bom_update_log/bom_update_log.py @@ -93,6 +93,7 @@ def on_submit(self): else: frappe.enqueue( method="erpnext.manufacturing.doctype.bom_update_log.bom_update_log.process_boms_cost_level_wise", + queue="long", update_doc=self, now=frappe.flags.in_test, enqueue_after_commit=True, diff --git a/erpnext/manufacturing/doctype/bom_update_log/bom_updation_utils.py b/erpnext/manufacturing/doctype/bom_update_log/bom_updation_utils.py index af115e3e4210..a2919b79b806 100644 --- a/erpnext/manufacturing/doctype/bom_update_log/bom_updation_utils.py +++ b/erpnext/manufacturing/doctype/bom_update_log/bom_updation_utils.py @@ -157,12 +157,19 @@ def _all_children_are_processed(parent_bom): def get_leaf_boms() -> List[str]: "Get BOMs that have no dependencies." - return frappe.db.sql_list( - """select name from `tabBOM` bom - where docstatus=1 and is_active=1 - and not exists(select bom_no from `tabBOM Item` - where parent=bom.name and ifnull(bom_no, '')!='')""" - ) + bom = frappe.qb.DocType("BOM") + bom_item = frappe.qb.DocType("BOM Item") + + boms = ( + frappe.qb.from_(bom) + .left_join(bom_item) + .on((bom.name == bom_item.parent) & (bom_item.bom_no != "")) + .select(bom.name) + .where((bom.docstatus == 1) & (bom.is_active == 1) & (bom_item.bom_no.isnull())) + .distinct() + ).run(pluck=True) + + return boms def _generate_dependence_map() -> defaultdict: From 29ddd26ba107f770c1ec78dc2ff600376ba97bbd Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Mon, 31 Jul 2023 18:22:44 +0530 Subject: [PATCH 027/501] fix: job card suggest holiday as start date (backport #35958) (#36423) fix: job card suggest holiday as start date (#35958) (cherry picked from commit ce36d1f668425539a4937dfca4cefa03dc96648d) Co-authored-by: Vimal --- erpnext/manufacturing/doctype/workstation/workstation.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/manufacturing/doctype/workstation/workstation.py b/erpnext/manufacturing/doctype/workstation/workstation.py index fabd254d8f65..0eb9906a2ab4 100644 --- a/erpnext/manufacturing/doctype/workstation/workstation.py +++ b/erpnext/manufacturing/doctype/workstation/workstation.py @@ -114,7 +114,7 @@ def validate_workstation_holiday(self, schedule_date, skip_holiday_list_check=Fa if schedule_date in tuple(get_holidays(self.holiday_list)): schedule_date = add_days(schedule_date, 1) - self.validate_workstation_holiday(schedule_date, skip_holiday_list_check=True) + return self.validate_workstation_holiday(schedule_date, skip_holiday_list_check=True) return schedule_date From 33a947726ddf61691f7a36e99bcc75318c59b173 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Mon, 31 Jul 2023 18:32:29 +0530 Subject: [PATCH 028/501] fix: Defined "Open" Status as default (backport #36421) (#36424) fix: Defined "Open" Status as default (#36421) Defined "Open" Status as default of the child doctype (Quality Review Objective), because without it the main doctype (Quality Review) has "Passed" status. This happens because in the "set_status" function, the status is updated according to the status of the child records. (cherry picked from commit 652398fad25d7d6f457ef49667a09d91a8abd8e4) Co-authored-by: xdlumertz --- .../quality_review_objective/quality_review_objective.json | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/erpnext/quality_management/doctype/quality_review_objective/quality_review_objective.json b/erpnext/quality_management/doctype/quality_review_objective/quality_review_objective.json index 3a750c21d6e2..5ddf0f2a0b45 100644 --- a/erpnext/quality_management/doctype/quality_review_objective/quality_review_objective.json +++ b/erpnext/quality_management/doctype/quality_review_objective/quality_review_objective.json @@ -56,6 +56,7 @@ "fieldtype": "Column Break" }, { + "default": "Open", "columns": 2, "fieldname": "status", "fieldtype": "Select", @@ -67,7 +68,7 @@ "index_web_pages_for_search": 1, "istable": 1, "links": [], - "modified": "2020-10-27 16:28:20.908637", + "modified": "2023-07-31 09:20:20.908637", "modified_by": "Administrator", "module": "Quality Management", "name": "Quality Review Objective", @@ -76,4 +77,4 @@ "sort_field": "modified", "sort_order": "DESC", "track_changes": 1 -} \ No newline at end of file +} From 516191bf2ba9cb9f00613dc5c6bf3985caa95a4c Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Mon, 31 Jul 2023 18:36:09 +0530 Subject: [PATCH 029/501] fix: group item reorder by (warehouse, material_request_type) (backport #35818) (#36425) fix: group item reorder by (warehouse, material_request_type) (#35818) * fix: group item reorder by (warehouse, material_request_type) * fix: update reorder error message * chore: linter * fix: correct error message Co-authored-by: s-aga-r * chore: linter --------- Co-authored-by: s-aga-r (cherry picked from commit e8eeeb16e26b9885f5ff88d136e8174c3fbd8203) Co-authored-by: Devin Slauenwhite --- erpnext/stock/doctype/item/item.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/erpnext/stock/doctype/item/item.py b/erpnext/stock/doctype/item/item.py index ba1c04fe27ed..693d33ffb713 100644 --- a/erpnext/stock/doctype/item/item.py +++ b/erpnext/stock/doctype/item/item.py @@ -395,16 +395,16 @@ def validate_barcode(self): def validate_warehouse_for_reorder(self): """Validate Reorder level table for duplicate and conditional mandatory""" - warehouse = [] + warehouse_material_request_type: list[tuple[str, str]] = [] for d in self.get("reorder_levels"): if not d.warehouse_group: d.warehouse_group = d.warehouse - if d.get("warehouse") and d.get("warehouse") not in warehouse: - warehouse += [d.get("warehouse")] + if (d.get("warehouse"), d.get("material_request_type")) not in warehouse_material_request_type: + warehouse_material_request_type += [(d.get("warehouse"), d.get("material_request_type"))] else: frappe.throw( - _("Row {0}: An Reorder entry already exists for this warehouse {1}").format( - d.idx, d.warehouse + _("Row #{0}: A reorder entry already exists for warehouse {1} with reorder type {2}.").format( + d.idx, d.warehouse, d.material_request_type ), DuplicateReorderRows, ) From 99e7406b5b2a27ecd81c1ccb76472ff9c7ca43ff Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Mon, 31 Jul 2023 19:43:29 +0530 Subject: [PATCH 030/501] fix: not able to make material request (backport #36416) (#36426) fix: not able to make material request (#36416) (cherry picked from commit f83a100a8d41de0c539599394d9bf7260fef847b) Co-authored-by: rohitwaghchaure --- .../doctype/sales_order/sales_order.py | 4 ++-- .../doctype/sales_order/test_sales_order.py | 20 +++++++++++++++++++ 2 files changed, 22 insertions(+), 2 deletions(-) diff --git a/erpnext/selling/doctype/sales_order/sales_order.py b/erpnext/selling/doctype/sales_order/sales_order.py index 97cccb136202..da838d1b7952 100755 --- a/erpnext/selling/doctype/sales_order/sales_order.py +++ b/erpnext/selling/doctype/sales_order/sales_order.py @@ -556,7 +556,7 @@ def update_item(source, target, source_parent): # qty is for packed items, because packed items don't have stock_qty field qty = source.get("qty") target.project = source_parent.project - target.qty = qty - requested_item_qty.get(source.name, 0) - source.delivered_qty + target.qty = qty - requested_item_qty.get(source.name, 0) - flt(source.get("delivered_qty")) target.stock_qty = flt(target.qty) * flt(target.conversion_factor) args = target.as_dict().copy() @@ -590,7 +590,7 @@ def update_item(source, target, source_parent): "doctype": "Material Request Item", "field_map": {"name": "sales_order_item", "parent": "sales_order"}, "condition": lambda doc: not frappe.db.exists("Product Bundle", doc.item_code) - and (doc.stock_qty - doc.delivered_qty) > requested_item_qty.get(doc.name, 0), + and (doc.stock_qty - flt(doc.get("delivered_qty"))) > requested_item_qty.get(doc.name, 0), "postprocess": update_item, }, }, diff --git a/erpnext/selling/doctype/sales_order/test_sales_order.py b/erpnext/selling/doctype/sales_order/test_sales_order.py index 9854f159cfef..ced1ac62729a 100644 --- a/erpnext/selling/doctype/sales_order/test_sales_order.py +++ b/erpnext/selling/doctype/sales_order/test_sales_order.py @@ -552,6 +552,26 @@ def test_update_child_qty_rate_with_workflow(self): workflow.is_active = 0 workflow.save() + def test_material_request_for_product_bundle(self): + # Create the Material Request from the sales order for the Packing Items + # Check whether the material request has the correct packing item or not. + if not frappe.db.exists("Item", "_Test Product Bundle Item New 1"): + bundle_item = make_item("_Test Product Bundle Item New 1", {"is_stock_item": 0}) + bundle_item.append( + "item_defaults", {"company": "_Test Company", "default_warehouse": "_Test Warehouse - _TC"} + ) + bundle_item.save(ignore_permissions=True) + + make_item("_Packed Item New 2", {"is_stock_item": 1}) + make_product_bundle("_Test Product Bundle Item New 1", ["_Packed Item New 2"], 2) + + so = make_sales_order( + item_code="_Test Product Bundle Item New 1", + ) + + mr = make_material_request(so.name) + self.assertEqual(mr.items[0].item_code, "_Packed Item New 2") + def test_bin_details_of_packed_item(self): # test Update Items with product bundle if not frappe.db.exists("Item", "_Test Product Bundle Item New"): From 8f72a6814bd8343a88499c21e677b1751a72af36 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Tue, 1 Aug 2023 07:58:09 +0530 Subject: [PATCH 031/501] fix: incorrect usage `get_cached_value` on single doctypes (cherry picked from commit ba15810639577e49ba2a56c2dddb7790ce25ef4e) --- erpnext/accounts/deferred_revenue.py | 2 +- erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py | 2 +- erpnext/stock/doctype/price_list/price_list.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/erpnext/accounts/deferred_revenue.py b/erpnext/accounts/deferred_revenue.py index fb49ef3a4234..d0940c7df213 100644 --- a/erpnext/accounts/deferred_revenue.py +++ b/erpnext/accounts/deferred_revenue.py @@ -341,7 +341,7 @@ def book_deferred_income_or_expense(doc, deferred_process, posting_date=None): "enable_deferred_revenue" if doc.doctype == "Sales Invoice" else "enable_deferred_expense" ) - accounts_frozen_upto = frappe.get_cached_value("Accounts Settings", "None", "acc_frozen_upto") + accounts_frozen_upto = frappe.db.get_single_value("Accounts Settings", "acc_frozen_upto") def _book_deferred_revenue_or_expense( item, diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py index 6d905a37cde3..d791232c0afb 100644 --- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py +++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py @@ -232,7 +232,7 @@ def validate_with_previous_doc(self): ) if ( - cint(frappe.get_cached_value("Buying Settings", "None", "maintain_same_rate")) + cint(frappe.db.get_single_value("Buying Settings", "maintain_same_rate")) and not self.is_return and not self.is_internal_supplier ): diff --git a/erpnext/stock/doctype/price_list/price_list.py b/erpnext/stock/doctype/price_list/price_list.py index 554055fd839a..e77d53a36712 100644 --- a/erpnext/stock/doctype/price_list/price_list.py +++ b/erpnext/stock/doctype/price_list/price_list.py @@ -45,7 +45,7 @@ def check_impact_on_shopping_cart(self): doc_before_save = self.get_doc_before_save() currency_changed = self.currency != doc_before_save.currency - affects_cart = self.name == frappe.get_cached_value("E Commerce Settings", None, "price_list") + affects_cart = self.name == frappe.db.get_single_value("E Commerce Settings", "price_list") if currency_changed and affects_cart: validate_cart_settings() From 5b562961e39de1875028bd50f5fc66e80262c683 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Tue, 1 Aug 2023 10:14:34 +0530 Subject: [PATCH 032/501] fix: root type in account map for balance sheet (#36303) * fix: root type in account map for balance sheet (#36303) * fix: root type in account map * fix: fetch gle by root type in consolidated financial statement * refactor: consolidated financial statement gle query * fix: filter accounts by root type (cherry picked from commit 11bd15e58059e2b2d65619437683f030914996f9) # Conflicts: # erpnext/accounts/report/consolidated_financial_statement/consolidated_financial_statement.py * chore: Resolve conflicts --------- Co-authored-by: Gursheen Kaur Anand <40693548+GursheenK@users.noreply.github.com> Co-authored-by: Deepesh Garg --- .../consolidated_financial_statement.py | 75 ++++++++++++------- .../accounts/report/financial_statements.py | 18 ++++- 2 files changed, 64 insertions(+), 29 deletions(-) diff --git a/erpnext/accounts/report/consolidated_financial_statement/consolidated_financial_statement.py b/erpnext/accounts/report/consolidated_financial_statement/consolidated_financial_statement.py index 5934fd12e859..56d33dd111e5 100644 --- a/erpnext/accounts/report/consolidated_financial_statement/consolidated_financial_statement.py +++ b/erpnext/accounts/report/consolidated_financial_statement/consolidated_financial_statement.py @@ -6,6 +6,7 @@ import frappe from frappe import _ +from frappe.query_builder import Criterion from frappe.utils import cint, flt, getdate import erpnext @@ -364,6 +365,7 @@ def get_data( accounts_by_name, accounts, ignore_closing_entries=False, + root_type=root_type, ) calculate_values(accounts_by_name, gl_entries_by_account, companies, filters, fiscal_year) @@ -608,6 +610,7 @@ def set_gl_entries_by_account( accounts_by_name, accounts, ignore_closing_entries=False, + root_type=None, ): """Returns a dict like { "account": [gl entries], ... }""" @@ -615,7 +618,6 @@ def set_gl_entries_by_account( "Company", filters.get("company"), ["lft", "rgt"] ) - additional_conditions = get_additional_conditions(from_date, ignore_closing_entries, filters) companies = frappe.db.sql( """ select name, default_currency from `tabCompany` where lft >= %(company_lft)s and rgt <= %(company_rgt)s""", @@ -631,27 +633,42 @@ def set_gl_entries_by_account( ) for d in companies: - gl_entries = frappe.db.sql( - """select gl.posting_date, gl.account, gl.debit, gl.credit, gl.is_opening, gl.company, - gl.fiscal_year, gl.debit_in_account_currency, gl.credit_in_account_currency, gl.account_currency, - acc.account_name, acc.account_number - from `tabGL Entry` gl, `tabAccount` acc where acc.name = gl.account and gl.company = %(company)s and gl.is_cancelled = 0 - {additional_conditions} and gl.posting_date <= %(to_date)s and acc.lft >= %(lft)s and acc.rgt <= %(rgt)s - order by gl.account, gl.posting_date""".format( - additional_conditions=additional_conditions - ), - { - "from_date": from_date, - "to_date": to_date, - "lft": root_lft, - "rgt": root_rgt, - "company": d.name, - "finance_book": filters.get("finance_book"), - "company_fb": frappe.db.get_value("Company", d.name, "default_finance_book"), - }, - as_dict=True, + gle = frappe.qb.DocType("GL Entry") + account = frappe.qb.DocType("Account") + query = ( + frappe.qb.from_(gle) + .inner_join(account) + .on(account.name == gle.account) + .select( + gle.posting_date, + gle.account, + gle.debit, + gle.credit, + gle.is_opening, + gle.company, + gle.fiscal_year, + gle.debit_in_account_currency, + gle.credit_in_account_currency, + gle.account_currency, + account.account_name, + account.account_number, + ) + .where( + (gle.company == d.name) + & (gle.is_cancelled == 0) + & (gle.posting_date <= to_date) + & (account.lft >= root_lft) + & (account.rgt <= root_rgt) + & (account.root_type <= root_type) + ) + .orderby(gle.account, gle.posting_date) ) + additional_conditions = get_additional_conditions(from_date, ignore_closing_entries, filters, d) + if additional_conditions: + query = query.where(Criterion.all(additional_conditions)) + gl_entries = query.run(as_dict=True) + if filters and filters.get("presentation_currency") != d.default_currency: currency_info["company"] = d.name currency_info["company_currency"] = d.default_currency @@ -721,23 +738,25 @@ def validate_entries(key, entry, accounts_by_name, accounts): accounts.insert(idx + 1, args) -def get_additional_conditions(from_date, ignore_closing_entries, filters): +def get_additional_conditions(from_date, ignore_closing_entries, filters, d): + gle = frappe.qb.DocType("GL Entry") additional_conditions = [] if ignore_closing_entries: - additional_conditions.append("ifnull(gl.voucher_type, '')!='Period Closing Voucher'") + additional_conditions.append((gle.voucher_type != "Period Closing Voucher")) if from_date: - additional_conditions.append("gl.posting_date >= %(from_date)s") + additional_conditions.append(gle.posting_date >= from_date) + + finance_book = filters.get("finance_book") + company_fb = frappe.get_cached_value("Company", d.name, "default_finance_book") if filters.get("include_default_book_entries"): - additional_conditions.append( - "(finance_book in (%(finance_book)s, %(company_fb)s, '') OR finance_book IS NULL)" - ) + additional_conditions.append((gle.finance_book.isin([finance_book, company_fb, "", None]))) else: - additional_conditions.append("(finance_book in (%(finance_book)s, '') OR finance_book IS NULL)") + additional_conditions.append((gle.finance_book.isin([finance_book, "", None]))) - return " and {}".format(" and ".join(additional_conditions)) if additional_conditions else "" + return additional_conditions def add_total_row(out, root_type, balance_must_be, companies, company_currency): diff --git a/erpnext/accounts/report/financial_statements.py b/erpnext/accounts/report/financial_statements.py index 26bf315b1927..a76dea6a523e 100644 --- a/erpnext/accounts/report/financial_statements.py +++ b/erpnext/accounts/report/financial_statements.py @@ -188,6 +188,7 @@ def get_data( filters, gl_entries_by_account, ignore_closing_entries=ignore_closing_entries, + root_type=root_type, ) calculate_values( @@ -417,13 +418,28 @@ def set_gl_entries_by_account( gl_entries_by_account, ignore_closing_entries=False, ignore_opening_entries=False, + root_type=None, ): """Returns a dict like { "account": [gl entries], ... }""" gl_entries = [] + account_filters = { + "company": company, + "is_group": 0, + "lft": (">=", root_lft), + "rgt": ("<=", root_rgt), + } + + if root_type: + account_filters.update( + { + "root_type": root_type, + } + ) + accounts_list = frappe.db.get_all( "Account", - filters={"company": company, "is_group": 0, "lft": (">=", root_lft), "rgt": ("<=", root_rgt)}, + filters=account_filters, pluck="name", ) From 782a4d1fa83cea6f7b277bab0c36ac33d7dbbb8c Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Tue, 1 Aug 2023 11:19:38 +0530 Subject: [PATCH 033/501] fix: typo in loyalty program throw message (#36432) * fix: typo in loyalty program throw message (#36432) (cherry picked from commit 4f473eb090de0e69bd581a1d44edce3eb0d66adc) # Conflicts: # erpnext/translations/sr-SP.csv # erpnext/translations/tr.csv # erpnext/translations/zh-TW.csv * chore: Resolve conflicts --------- Co-authored-by: abdosaeed95 <118386543+abdosaeed95@users.noreply.github.com> Co-authored-by: Deepesh Garg --- .../loyalty_program/loyalty_program.py | 2 +- erpnext/translations/af.csv | 2 +- erpnext/translations/am.csv | 2 +- erpnext/translations/ar.csv | 2 +- erpnext/translations/bg.csv | 2 +- erpnext/translations/bn.csv | 2 +- erpnext/translations/bs.csv | 2 +- erpnext/translations/ca.csv | 2 +- erpnext/translations/cs.csv | 2 +- erpnext/translations/da.csv | 2 +- erpnext/translations/de.csv | 2 +- erpnext/translations/el.csv | 2 +- erpnext/translations/es.csv | 2 +- erpnext/translations/et.csv | 2 +- erpnext/translations/fa.csv | 2 +- erpnext/translations/fi.csv | 2 +- erpnext/translations/fr.csv | 2 +- erpnext/translations/gu.csv | 2 +- erpnext/translations/he.csv | 2 +- erpnext/translations/hi.csv | 2 +- erpnext/translations/hr.csv | 2 +- erpnext/translations/hu.csv | 2 +- erpnext/translations/id.csv | 2 +- erpnext/translations/is.csv | 2 +- erpnext/translations/it.csv | 2 +- erpnext/translations/ja.csv | 2 +- erpnext/translations/km.csv | 2 +- erpnext/translations/kn.csv | 2 +- erpnext/translations/ko.csv | 2 +- erpnext/translations/ku.csv | 2 +- erpnext/translations/lo.csv | 2 +- erpnext/translations/lt.csv | 2 +- erpnext/translations/lv.csv | 2 +- erpnext/translations/mk.csv | 2 +- erpnext/translations/ml.csv | 2 +- erpnext/translations/mr.csv | 2 +- erpnext/translations/ms.csv | 2 +- erpnext/translations/my.csv | 2 +- erpnext/translations/nl.csv | 2 +- erpnext/translations/no.csv | 2 +- erpnext/translations/pl.csv | 2 +- erpnext/translations/ps.csv | 2 +- erpnext/translations/pt-BR.csv | 2 +- erpnext/translations/pt.csv | 2 +- erpnext/translations/ro.csv | 2 +- erpnext/translations/ru.csv | 2 +- erpnext/translations/rw.csv | 2 +- erpnext/translations/si.csv | 2 +- erpnext/translations/sk.csv | 2 +- erpnext/translations/sl.csv | 2 +- erpnext/translations/sq.csv | 2 +- erpnext/translations/sr-SP.csv | 642 ++++++++++++++++++ erpnext/translations/sr.csv | 2 +- erpnext/translations/sr_sp.csv | 2 +- erpnext/translations/sv.csv | 2 +- erpnext/translations/sw.csv | 2 +- erpnext/translations/ta.csv | 2 +- erpnext/translations/te.csv | 2 +- erpnext/translations/th.csv | 2 +- erpnext/translations/tr.csv | 4 + erpnext/translations/uk.csv | 2 +- erpnext/translations/ur.csv | 2 +- erpnext/translations/uz.csv | 2 +- erpnext/translations/vi.csv | 2 +- erpnext/translations/zh.csv | 2 +- erpnext/translations/zh_tw.csv | 2 +- 66 files changed, 710 insertions(+), 64 deletions(-) diff --git a/erpnext/accounts/doctype/loyalty_program/loyalty_program.py b/erpnext/accounts/doctype/loyalty_program/loyalty_program.py index 48a25ad6b81e..a134f7466351 100644 --- a/erpnext/accounts/doctype/loyalty_program/loyalty_program.py +++ b/erpnext/accounts/doctype/loyalty_program/loyalty_program.py @@ -141,7 +141,7 @@ def validate_loyalty_points(ref_doc, points_to_redeem): ) if points_to_redeem > loyalty_program_details.loyalty_points: - frappe.throw(_("You don't have enought Loyalty Points to redeem")) + frappe.throw(_("You don't have enough Loyalty Points to redeem")) loyalty_amount = flt(points_to_redeem * loyalty_program_details.conversion_factor) diff --git a/erpnext/translations/af.csv b/erpnext/translations/af.csv index f2458e3c9658..10e1cba349e4 100644 --- a/erpnext/translations/af.csv +++ b/erpnext/translations/af.csv @@ -3338,7 +3338,7 @@ You cannot delete Fiscal Year {0}. Fiscal Year {0} is set as default in Global S You cannot delete Project Type 'External',Jy kan nie projektipe 'eksterne' uitvee nie, You cannot edit root node.,U kan nie wortelknoop wysig nie., You cannot restart a Subscription that is not cancelled.,U kan nie 'n intekening herlaai wat nie gekanselleer is nie., -You don't have enought Loyalty Points to redeem,U het nie genoeg lojaliteitspunte om te verkoop nie, +You don't have enough Loyalty Points to redeem,U het nie genoeg lojaliteitspunte om te verkoop nie, You have already assessed for the assessment criteria {}.,U het reeds geassesseer vir die assesseringskriteria ()., You have already selected items from {0} {1},Jy het reeds items gekies van {0} {1}, You have been invited to collaborate on the project: {0},U is genooi om saam te werk aan die projek: {0}, diff --git a/erpnext/translations/am.csv b/erpnext/translations/am.csv index d4db28586716..f1d979205eca 100644 --- a/erpnext/translations/am.csv +++ b/erpnext/translations/am.csv @@ -3338,7 +3338,7 @@ You cannot delete Fiscal Year {0}. Fiscal Year {0} is set as default in Global S You cannot delete Project Type 'External',የፕሮጀክት አይነት «ውጫዊ» ን መሰረዝ አይችሉም., You cannot edit root node.,የስር ሥፍራ ማረም አይችሉም., You cannot restart a Subscription that is not cancelled.,የማይሰረዝ የደንበኝነት ምዝገባን ዳግም ማስጀመር አይችሉም., -You don't have enought Loyalty Points to redeem,ለማስመለስ በቂ የታማኝነት ነጥቦች የሉዎትም, +You don't have enough Loyalty Points to redeem,ለማስመለስ በቂ የታማኝነት ነጥቦች የሉዎትም, You have already assessed for the assessment criteria {}.,ቀድሞውንም ግምገማ መስፈርት ከገመገምን {}., You have already selected items from {0} {1},ከዚህ ቀደም ከ ንጥሎች ተመርጠዋል ሊሆን {0} {1}, You have been invited to collaborate on the project: {0},እርስዎ ፕሮጀክት ላይ ተባበር ተጋብዘዋል: {0}, diff --git a/erpnext/translations/ar.csv b/erpnext/translations/ar.csv index ea2777faf03b..d4df088c778a 100644 --- a/erpnext/translations/ar.csv +++ b/erpnext/translations/ar.csv @@ -3338,7 +3338,7 @@ You cannot delete Fiscal Year {0}. Fiscal Year {0} is set as default in Global S You cannot delete Project Type 'External',لا يمكنك حذف مشروع من نوع 'خارجي', You cannot edit root node.,لا يمكنك تحرير عقدة الجذر., You cannot restart a Subscription that is not cancelled.,لا يمكنك إعادة تشغيل اشتراك غير ملغى., -You don't have enought Loyalty Points to redeem,ليس لديك ما يكفي من نقاط الولاء لاستردادها, +You don't have enough Loyalty Points to redeem,ليس لديك ما يكفي من نقاط الولاء لاستردادها, You have already assessed for the assessment criteria {}.,لقد سبق أن قيمت معايير التقييم {}., You have already selected items from {0} {1},لقد حددت العناصر من {0} {1}, You have been invited to collaborate on the project: {0},لقد وجهت الدعوة إلى التعاون في هذا المشروع: {0}, diff --git a/erpnext/translations/bg.csv b/erpnext/translations/bg.csv index 6839129a319b..24a397210e1d 100644 --- a/erpnext/translations/bg.csv +++ b/erpnext/translations/bg.csv @@ -3338,7 +3338,7 @@ You cannot delete Fiscal Year {0}. Fiscal Year {0} is set as default in Global S You cannot delete Project Type 'External',Не можете да изтриете Тип на проекта "Външен", You cannot edit root node.,Не можете да редактирате корен възел., You cannot restart a Subscription that is not cancelled.,"Не можете да рестартирате абонамент, който не е анулиран.", -You don't have enought Loyalty Points to redeem,"Нямате достатъчно точки за лоялност, за да осребрите", +You don't have enough Loyalty Points to redeem,"Нямате достатъчно точки за лоялност, за да осребрите", You have already assessed for the assessment criteria {}.,Вече оценихте критериите за оценка {}., You have already selected items from {0} {1},Вие вече сте избрали елементи от {0} {1}, You have been invited to collaborate on the project: {0},Вие сте били поканени да си сътрудничат по проекта: {0}, diff --git a/erpnext/translations/bn.csv b/erpnext/translations/bn.csv index a944d99ecbf2..75532418ea40 100644 --- a/erpnext/translations/bn.csv +++ b/erpnext/translations/bn.csv @@ -3338,7 +3338,7 @@ You cannot delete Fiscal Year {0}. Fiscal Year {0} is set as default in Global S You cannot delete Project Type 'External',আপনি প্রকল্প প্রকার 'বহিরাগত' মুছে ফেলতে পারবেন না, You cannot edit root node.,আপনি রুট নোড সম্পাদনা করতে পারবেন না।, You cannot restart a Subscription that is not cancelled.,আপনি সাবস্ক্রিপশনটি বাতিল না করা পুনরায় শুরু করতে পারবেন না, -You don't have enought Loyalty Points to redeem,আপনি বিক্রি করার জন্য আনুগত্য পয়েন্ট enought না, +You don't have enough Loyalty Points to redeem,আপনি বিক্রি করার জন্য আনুগত্য পয়েন্ট enough না, You have already assessed for the assessment criteria {}.,"আপনি ইতিমধ্যে মূল্যায়ন মানদণ্ডের জন্য মূল্যায়ন করে নিলে, {}।", You have already selected items from {0} {1},আপনি ইতিমধ্যে থেকে আইটেম নির্বাচন করা আছে {0} {1}, You have been invited to collaborate on the project: {0},আপনি প্রকল্পের সহযোগীতা করার জন্য আমন্ত্রণ জানানো হয়েছে: {0}, diff --git a/erpnext/translations/bs.csv b/erpnext/translations/bs.csv index 2d9c26d15b12..ab82d0fc1072 100644 --- a/erpnext/translations/bs.csv +++ b/erpnext/translations/bs.csv @@ -3338,7 +3338,7 @@ You cannot delete Fiscal Year {0}. Fiscal Year {0} is set as default in Global S You cannot delete Project Type 'External',Ne možete obrisati tip projekta 'Spoljni', You cannot edit root node.,Ne možete uređivati root čvor., You cannot restart a Subscription that is not cancelled.,Ne možete ponovo pokrenuti pretplatu koja nije otkazana., -You don't have enought Loyalty Points to redeem,Ne iskoristite Loyalty Points za otkup, +You don't have enough Loyalty Points to redeem,Ne iskoristite Loyalty Points za otkup, You have already assessed for the assessment criteria {}.,Ste već ocijenili za kriterije procjene {}., You have already selected items from {0} {1},Vi ste već odabrane stavke iz {0} {1}, You have been invited to collaborate on the project: {0},Vi ste pozvani da surađuju na projektu: {0}, diff --git a/erpnext/translations/ca.csv b/erpnext/translations/ca.csv index 85c62851c8d5..afac395efa94 100644 --- a/erpnext/translations/ca.csv +++ b/erpnext/translations/ca.csv @@ -3338,7 +3338,7 @@ You cannot delete Fiscal Year {0}. Fiscal Year {0} is set as default in Global S You cannot delete Project Type 'External',No es pot eliminar el tipus de projecte 'Extern', You cannot edit root node.,No podeu editar el node arrel., You cannot restart a Subscription that is not cancelled.,No podeu reiniciar una subscripció que no es cancel·la., -You don't have enought Loyalty Points to redeem,No teniu punts de fidelització previstos per bescanviar, +You don't have enough Loyalty Points to redeem,No teniu punts de fidelització previstos per bescanviar, You have already assessed for the assessment criteria {}.,Vostè ja ha avaluat pels criteris d'avaluació {}., You have already selected items from {0} {1},Ja ha seleccionat articles de {0} {1}, You have been invited to collaborate on the project: {0},Se li ha convidat a col·laborar en el projecte: {0}, diff --git a/erpnext/translations/cs.csv b/erpnext/translations/cs.csv index 3fb67e787087..64dec565400b 100644 --- a/erpnext/translations/cs.csv +++ b/erpnext/translations/cs.csv @@ -3338,7 +3338,7 @@ You cannot delete Fiscal Year {0}. Fiscal Year {0} is set as default in Global S You cannot delete Project Type 'External',Nelze odstranit typ projektu "Externí", You cannot edit root node.,Nelze upravit kořenový uzel., You cannot restart a Subscription that is not cancelled.,"Nelze znovu spustit odběr, který není zrušen.", -You don't have enought Loyalty Points to redeem,Nemáte dostatečné věrnostní body k uplatnění, +You don't have enough Loyalty Points to redeem,Nemáte dostatečné věrnostní body k uplatnění, You have already assessed for the assessment criteria {}.,Již jste hodnotili kritéria hodnocení {}., You have already selected items from {0} {1},Již jste vybrané položky z {0} {1}, You have been invited to collaborate on the project: {0},Byli jste pozváni ke spolupráci na projektu: {0}, diff --git a/erpnext/translations/da.csv b/erpnext/translations/da.csv index f0654b998da4..8a735b1e2714 100644 --- a/erpnext/translations/da.csv +++ b/erpnext/translations/da.csv @@ -3338,7 +3338,7 @@ You cannot delete Fiscal Year {0}. Fiscal Year {0} is set as default in Global S You cannot delete Project Type 'External',Du kan ikke slette Project Type 'Ekstern', You cannot edit root node.,Du kan ikke redigere root node., You cannot restart a Subscription that is not cancelled.,"Du kan ikke genstarte en abonnement, der ikke annulleres.", -You don't have enought Loyalty Points to redeem,Du har ikke nok loyalitetspoint til at indløse, +You don't have enough Loyalty Points to redeem,Du har ikke nok loyalitetspoint til at indløse, You have already assessed for the assessment criteria {}.,Du har allerede vurderet for bedømmelseskriterierne {}., You have already selected items from {0} {1},Du har allerede valgt elementer fra {0} {1}, You have been invited to collaborate on the project: {0},Du er blevet inviteret til at samarbejde om sag: {0}, diff --git a/erpnext/translations/de.csv b/erpnext/translations/de.csv index b40eeca67bfb..42d313f4db19 100644 --- a/erpnext/translations/de.csv +++ b/erpnext/translations/de.csv @@ -3352,7 +3352,7 @@ You cannot delete Fiscal Year {0}. Fiscal Year {0} is set as default in Global S You cannot delete Project Type 'External',Sie können den Projekttyp 'Extern' nicht löschen, You cannot edit root node.,Sie können den Stammknoten nicht bearbeiten., You cannot restart a Subscription that is not cancelled.,Sie können ein nicht abgebrochenes Abonnement nicht neu starten., -You don't have enought Loyalty Points to redeem,Sie haben nicht genügend Treuepunkte zum Einlösen, +You don't have enough Loyalty Points to redeem,Sie haben nicht genügend Treuepunkte zum Einlösen, You have already assessed for the assessment criteria {}.,Sie haben bereits für die Bewertungskriterien beurteilt., You have already selected items from {0} {1},Sie haben bereits Elemente aus {0} {1} gewählt, You have been invited to collaborate on the project: {0},Sie wurden zur Zusammenarbeit für das Projekt {0} eingeladen., diff --git a/erpnext/translations/el.csv b/erpnext/translations/el.csv index c241558e31c7..cacce1efadac 100644 --- a/erpnext/translations/el.csv +++ b/erpnext/translations/el.csv @@ -3338,7 +3338,7 @@ You cannot delete Fiscal Year {0}. Fiscal Year {0} is set as default in Global S You cannot delete Project Type 'External',Δεν μπορείτε να διαγράψετε τον τύπο έργου 'Εξωτερικό', You cannot edit root node.,Δεν μπορείτε να επεξεργαστείτε τον κόμβο ρίζας., You cannot restart a Subscription that is not cancelled.,Δεν μπορείτε να κάνετε επανεκκίνηση μιας συνδρομής που δεν ακυρώνεται., -You don't have enought Loyalty Points to redeem,Δεν διαθέτετε σημεία αφοσίωσης για εξαργύρωση, +You don't have enough Loyalty Points to redeem,Δεν διαθέτετε σημεία αφοσίωσης για εξαργύρωση, You have already assessed for the assessment criteria {}.,Έχετε ήδη αξιολογήσει τα κριτήρια αξιολόγησης {}., You have already selected items from {0} {1},Έχετε ήδη επιλεγμένα αντικείμενα από {0} {1}, You have been invited to collaborate on the project: {0},Έχετε προσκληθεί να συνεργαστούν για το έργο: {0}, diff --git a/erpnext/translations/es.csv b/erpnext/translations/es.csv index 9996fe54c15d..8999d90ae876 100644 --- a/erpnext/translations/es.csv +++ b/erpnext/translations/es.csv @@ -3338,7 +3338,7 @@ You cannot delete Fiscal Year {0}. Fiscal Year {0} is set as default in Global S You cannot delete Project Type 'External',No puede eliminar Tipo de proyecto 'Externo', You cannot edit root node.,No puedes editar el nodo raíz., You cannot restart a Subscription that is not cancelled.,No puede reiniciar una suscripción que no está cancelada., -You don't have enought Loyalty Points to redeem,No tienes suficientes puntos de lealtad para canjear, +You don't have enough Loyalty Points to redeem,No tienes suficientes puntos de lealtad para canjear, You have already assessed for the assessment criteria {}.,Ya ha evaluado los criterios de evaluación {}., You have already selected items from {0} {1},Ya ha seleccionado artículos de {0} {1}, You have been invited to collaborate on the project: {0},Se le ha invitado a colaborar en el proyecto: {0}, diff --git a/erpnext/translations/et.csv b/erpnext/translations/et.csv index 6e60809fe7b9..0d509deda1aa 100644 --- a/erpnext/translations/et.csv +++ b/erpnext/translations/et.csv @@ -3338,7 +3338,7 @@ You cannot delete Fiscal Year {0}. Fiscal Year {0} is set as default in Global S You cannot delete Project Type 'External',Te ei saa projekti tüübi "Väline" kustutada, You cannot edit root node.,Sa ei saa redigeerida juursõlme., You cannot restart a Subscription that is not cancelled.,Te ei saa tellimust uuesti katkestada., -You don't have enought Loyalty Points to redeem,"Teil pole lojaalsuspunkte, mida soovite lunastada", +You don't have enough Loyalty Points to redeem,"Teil pole lojaalsuspunkte, mida soovite lunastada", You have already assessed for the assessment criteria {}.,Olete juba hinnanud hindamise kriteeriumid {}., You have already selected items from {0} {1},Olete juba valitud objektide {0} {1}, You have been invited to collaborate on the project: {0},Sind on kutsutud koostööd projekti: {0}, diff --git a/erpnext/translations/fa.csv b/erpnext/translations/fa.csv index 7d18e27ad422..3e054606fc74 100644 --- a/erpnext/translations/fa.csv +++ b/erpnext/translations/fa.csv @@ -3338,7 +3338,7 @@ You cannot delete Fiscal Year {0}. Fiscal Year {0} is set as default in Global S You cannot delete Project Type 'External',شما نمیتوانید نوع پروژه «خارجی» را حذف کنید, You cannot edit root node.,نمی توانید گره ریشه را ویرایش کنید, You cannot restart a Subscription that is not cancelled.,شما نمی توانید اشتراک را لغو کنید., -You don't have enought Loyalty Points to redeem,شما نمیتوانید امتیازات وفاداری خود را به دست آورید, +You don't have enough Loyalty Points to redeem,شما نمیتوانید امتیازات وفاداری خود را به دست آورید, You have already assessed for the assessment criteria {}.,شما در حال حاضر برای معیارهای ارزیابی ارزیابی {}., You have already selected items from {0} {1},شما در حال حاضر اقلام از انتخاب {0} {1}, You have been invited to collaborate on the project: {0},از شما دعوت شده برای همکاری در این پروژه: {0}, diff --git a/erpnext/translations/fi.csv b/erpnext/translations/fi.csv index c700f60d15c0..892261d01784 100644 --- a/erpnext/translations/fi.csv +++ b/erpnext/translations/fi.csv @@ -3338,7 +3338,7 @@ You cannot delete Fiscal Year {0}. Fiscal Year {0} is set as default in Global S You cannot delete Project Type 'External',Et voi poistaa projektityyppiä "Ulkoinen", You cannot edit root node.,Et voi muokata juurisolmua., You cannot restart a Subscription that is not cancelled.,"Et voi uudelleenkäynnistää tilausta, jota ei peruuteta.", -You don't have enought Loyalty Points to redeem,Sinulla ei ole tarpeeksi Loyalty Pointsia lunastettavaksi, +You don't have enough Loyalty Points to redeem,Sinulla ei ole tarpeeksi Loyalty Pointsia lunastettavaksi, You have already assessed for the assessment criteria {}.,Olet jo arvioitu arviointikriteerit {}., You have already selected items from {0} {1},Olet jo valitut kohteet {0} {1}, You have been invited to collaborate on the project: {0},Sinut on kutsuttu yhteistyöhön projektissa {0}, diff --git a/erpnext/translations/fr.csv b/erpnext/translations/fr.csv index ab9bf7d9c147..fde3f57211e0 100644 --- a/erpnext/translations/fr.csv +++ b/erpnext/translations/fr.csv @@ -3338,7 +3338,7 @@ You cannot delete Fiscal Year {0}. Fiscal Year {0} is set as default in Global S You cannot delete Project Type 'External',Vous ne pouvez pas supprimer le Type de Projet 'Externe', You cannot edit root node.,Vous ne pouvez pas modifier le nœud racine., You cannot restart a Subscription that is not cancelled.,Vous ne pouvez pas redémarrer un abonnement qui n'est pas annulé., -You don't have enought Loyalty Points to redeem,Vous n'avez pas assez de points de fidélité à échanger, +You don't have enough Loyalty Points to redeem,Vous n'avez pas assez de points de fidélité à échanger, You have already assessed for the assessment criteria {}.,Vous avez déjà évalué les critères d'évaluation {}., You have already selected items from {0} {1},Vous avez déjà choisi des articles de {0} {1}, You have been invited to collaborate on the project: {0},Vous avez été invité à collaborer sur le projet : {0}, diff --git a/erpnext/translations/gu.csv b/erpnext/translations/gu.csv index b26d2f3d0768..787e29d62192 100644 --- a/erpnext/translations/gu.csv +++ b/erpnext/translations/gu.csv @@ -3338,7 +3338,7 @@ You cannot delete Fiscal Year {0}. Fiscal Year {0} is set as default in Global S You cannot delete Project Type 'External',તમે 'બાહ્ય' પ્રોજેક્ટ પ્રકારને કાઢી શકતા નથી, You cannot edit root node.,તમે રૂટ નોડને સંપાદિત કરી શકતા નથી., You cannot restart a Subscription that is not cancelled.,તમે સબ્સ્ક્રિપ્શન ફરીથી શરૂ કરી શકતા નથી કે જે રદ કરવામાં આવી નથી., -You don't have enought Loyalty Points to redeem,તમારી પાસે રિડીમ કરવા માટે વફાદારીના પોઇંટ્સ નથી, +You don't have enough Loyalty Points to redeem,તમારી પાસે રિડીમ કરવા માટે વફાદારીના પોઇંટ્સ નથી, You have already assessed for the assessment criteria {}.,જો તમે પહેલાથી જ આકારણી માપદંડ માટે આકારણી છે {}., You have already selected items from {0} {1},જો તમે પહેલાથી જ વસ્તુઓ પસંદ કરેલ {0} {1}, You have been invited to collaborate on the project: {0},તમે આ પ્રોજેક્ટ પર સહયોગ કરવા માટે આમંત્રિત કરવામાં આવ્યા છે: {0}, diff --git a/erpnext/translations/he.csv b/erpnext/translations/he.csv index e40b68e504f9..c8a9c7546e10 100644 --- a/erpnext/translations/he.csv +++ b/erpnext/translations/he.csv @@ -3338,7 +3338,7 @@ You cannot delete Fiscal Year {0}. Fiscal Year {0} is set as default in Global S You cannot delete Project Type 'External',אינך יכול למחוק את סוג הפרויקט 'חיצוני', You cannot edit root node.,אינך יכול לערוך צומת שורש., You cannot restart a Subscription that is not cancelled.,אינך יכול להפעיל מחדש מנוי שאינו מבוטל., -You don't have enought Loyalty Points to redeem,אין לך מספיק נקודות נאמנות למימוש, +You don't have enough Loyalty Points to redeem,אין לך מספיק נקודות נאמנות למימוש, You have already assessed for the assessment criteria {}.,כבר הערכת את קריטריוני ההערכה {}., You have already selected items from {0} {1},בחרת כבר פריטים מ- {0} {1}, You have been invited to collaborate on the project: {0},הוזמנת לשתף פעולה על הפרויקט: {0}, diff --git a/erpnext/translations/hi.csv b/erpnext/translations/hi.csv index 78094d735af0..21e659aabec6 100644 --- a/erpnext/translations/hi.csv +++ b/erpnext/translations/hi.csv @@ -3338,7 +3338,7 @@ You cannot delete Fiscal Year {0}. Fiscal Year {0} is set as default in Global S You cannot delete Project Type 'External',आप परियोजना प्रकार 'बाहरी' को नहीं हटा सकते, You cannot edit root node.,आप रूट नोड संपादित नहीं कर सकते हैं।, You cannot restart a Subscription that is not cancelled.,आप एक सदस्यता को पुनरारंभ नहीं कर सकते जो रद्द नहीं किया गया है।, -You don't have enought Loyalty Points to redeem,आपने रिडीम करने के लिए वफादारी अंक नहीं खरीदे हैं, +You don't have enough Loyalty Points to redeem,आपने रिडीम करने के लिए वफादारी अंक नहीं खरीदे हैं, You have already assessed for the assessment criteria {}.,आप मूल्यांकन मानदंड के लिए पहले से ही मूल्यांकन कर चुके हैं {}, You have already selected items from {0} {1},आप पहले से ही से आइटम का चयन किया है {0} {1}, You have been invited to collaborate on the project: {0},आप इस परियोजना पर सहयोग करने के लिए आमंत्रित किया गया है: {0}, diff --git a/erpnext/translations/hr.csv b/erpnext/translations/hr.csv index 232832f3f97d..9d0b9526f797 100644 --- a/erpnext/translations/hr.csv +++ b/erpnext/translations/hr.csv @@ -3338,7 +3338,7 @@ You cannot delete Fiscal Year {0}. Fiscal Year {0} is set as default in Global S You cannot delete Project Type 'External',Ne možete izbrisati vrstu projekta 'Vanjski', You cannot edit root node.,Ne možete uređivati root čvor., You cannot restart a Subscription that is not cancelled.,Ne možete ponovo pokrenuti pretplatu koja nije otkazana., -You don't have enought Loyalty Points to redeem,Nemate dovoljno bodova lojalnosti za otkup, +You don't have enough Loyalty Points to redeem,Nemate dovoljno bodova lojalnosti za otkup, You have already assessed for the assessment criteria {}.,Već ste ocijenili kriterije procjene {}., You have already selected items from {0} {1},Već ste odabrali stavke iz {0} {1}, You have been invited to collaborate on the project: {0},Pozvani ste za suradnju na projektu: {0}, diff --git a/erpnext/translations/hu.csv b/erpnext/translations/hu.csv index e3dcd61fb2f5..c7966b5113e2 100644 --- a/erpnext/translations/hu.csv +++ b/erpnext/translations/hu.csv @@ -3338,7 +3338,7 @@ You cannot delete Fiscal Year {0}. Fiscal Year {0} is set as default in Global S You cannot delete Project Type 'External',"A ""Külső"" projekttípust nem törölheti", You cannot edit root node.,Nem szerkesztheti a fő csomópontot., You cannot restart a Subscription that is not cancelled.,"Nem indíthatja el az Előfizetést, amelyet nem zárt le.", -You don't have enought Loyalty Points to redeem,Nincs elegendő hűségpontjaid megváltáshoz, +You don't have enough Loyalty Points to redeem,Nincs elegendő hűségpontjaid megváltáshoz, You have already assessed for the assessment criteria {}.,Már értékelte ezekkel az értékelési kritériumokkal: {}., You have already selected items from {0} {1},Már választott ki elemeket innen {0} {1}, You have been invited to collaborate on the project: {0},Ön meghívást kapott ennek a projeknek a közreműködéséhez: {0}, diff --git a/erpnext/translations/id.csv b/erpnext/translations/id.csv index ccbb002370f3..de44c6fd7826 100644 --- a/erpnext/translations/id.csv +++ b/erpnext/translations/id.csv @@ -3338,7 +3338,7 @@ You cannot delete Fiscal Year {0}. Fiscal Year {0} is set as default in Global S You cannot delete Project Type 'External',Anda tidak bisa menghapus Jenis Proyek 'External', You cannot edit root node.,Anda tidak dapat mengedit simpul root., You cannot restart a Subscription that is not cancelled.,Anda tidak dapat memulai ulang Langganan yang tidak dibatalkan., -You don't have enought Loyalty Points to redeem,Anda tidak memiliki Poin Loyalitas yang cukup untuk ditukarkan, +You don't have enough Loyalty Points to redeem,Anda tidak memiliki Poin Loyalitas yang cukup untuk ditukarkan, You have already assessed for the assessment criteria {}.,Anda telah memberikan penilaian terhadap kriteria penilaian {}., You have already selected items from {0} {1},Anda sudah memilih item dari {0} {1}, You have been invited to collaborate on the project: {0},Anda telah diundang untuk berkolaborasi pada proyek: {0}, diff --git a/erpnext/translations/is.csv b/erpnext/translations/is.csv index 5f11c63abafd..b9174e26e51f 100644 --- a/erpnext/translations/is.csv +++ b/erpnext/translations/is.csv @@ -3338,7 +3338,7 @@ You cannot delete Fiscal Year {0}. Fiscal Year {0} is set as default in Global S You cannot delete Project Type 'External',Þú getur ekki eytt verkefnisgerðinni 'ytri', You cannot edit root node.,Þú getur ekki breytt rótarkóði., You cannot restart a Subscription that is not cancelled.,Þú getur ekki endurræst áskrift sem ekki er lokað., -You don't have enought Loyalty Points to redeem,Þú hefur ekki nóg hollusta stig til að innleysa, +You don't have enough Loyalty Points to redeem,Þú hefur ekki nóg hollusta stig til að innleysa, You have already assessed for the assessment criteria {}.,Þú hefur nú þegar metið mat á viðmiðunum {}., You have already selected items from {0} {1},Þú hefur nú þegar valið hluti úr {0} {1}, You have been invited to collaborate on the project: {0},Þér hefur verið boðið að vinna að verkefninu: {0}, diff --git a/erpnext/translations/it.csv b/erpnext/translations/it.csv index 0dbde45778a7..d432eaf1e96c 100644 --- a/erpnext/translations/it.csv +++ b/erpnext/translations/it.csv @@ -3338,7 +3338,7 @@ You cannot delete Fiscal Year {0}. Fiscal Year {0} is set as default in Global S You cannot delete Project Type 'External',Non è possibile eliminare il tipo di progetto 'Esterno', You cannot edit root node.,Non è possibile modificare il nodo principale., You cannot restart a Subscription that is not cancelled.,Non è possibile riavviare una sottoscrizione che non è stata annullata., -You don't have enought Loyalty Points to redeem,Non hai abbastanza Punti fedeltà da riscattare, +You don't have enough Loyalty Points to redeem,Non hai abbastanza Punti fedeltà da riscattare, You have already assessed for the assessment criteria {}.,Hai già valutato i criteri di valutazione {}., You have already selected items from {0} {1},Hai già selezionato elementi da {0} {1}, You have been invited to collaborate on the project: {0},Sei stato invitato a collaborare al progetto: {0}, diff --git a/erpnext/translations/ja.csv b/erpnext/translations/ja.csv index 210c78ee5df5..2bf91fbe2515 100644 --- a/erpnext/translations/ja.csv +++ b/erpnext/translations/ja.csv @@ -3338,7 +3338,7 @@ You cannot delete Fiscal Year {0}. Fiscal Year {0} is set as default in Global S You cannot delete Project Type 'External',プロジェクトタイプ「外部」を削除することはできません, You cannot edit root node.,ルートノードは編集できません。, You cannot restart a Subscription that is not cancelled.,キャンセルされていないサブスクリプションを再起動することはできません。, -You don't have enought Loyalty Points to redeem,あなたは交換するのに十分なロイヤリティポイントがありません, +You don't have enough Loyalty Points to redeem,あなたは交換するのに十分なロイヤリティポイントがありません, You have already assessed for the assessment criteria {}.,評価基準{}は評価済です。, You have already selected items from {0} {1},項目を選択済みです {0} {1}, You have been invited to collaborate on the project: {0},プロジェクト:{0} の共同作業に招待されました, diff --git a/erpnext/translations/km.csv b/erpnext/translations/km.csv index 1eb85cca924e..8b25f9983560 100644 --- a/erpnext/translations/km.csv +++ b/erpnext/translations/km.csv @@ -3338,7 +3338,7 @@ You cannot delete Fiscal Year {0}. Fiscal Year {0} is set as default in Global S You cannot delete Project Type 'External',អ្នកមិនអាចលុបប្រភេទគម្រោង 'ខាងក្រៅ', You cannot edit root node.,អ្នកមិនអាចកែថ្នាំង root បានទេ។, You cannot restart a Subscription that is not cancelled.,អ្នកមិនអាចចាប់ផ្តើមឡើងវិញនូវការជាវដែលមិនត្រូវបានលុបចោលទេ។, -You don't have enought Loyalty Points to redeem,អ្នកមិនមានពិន្ទុភាពស្មោះត្រង់គ្រប់គ្រាន់ដើម្បីលោះទេ, +You don't have enough Loyalty Points to redeem,អ្នកមិនមានពិន្ទុភាពស្មោះត្រង់គ្រប់គ្រាន់ដើម្បីលោះទេ, You have already assessed for the assessment criteria {}.,អ្នកបានវាយតម្លែរួចទៅហើយសម្រាប់លក្ខណៈវិនិច្ឆ័យវាយតម្លៃនេះ {} ។, You have already selected items from {0} {1},អ្នកបានជ្រើសរួចហើយចេញពីធាតុ {0} {1}, You have been invited to collaborate on the project: {0},អ្នកបានត្រូវអញ្ជើញដើម្បីសហការគ្នាលើគម្រោងនេះ: {0}, diff --git a/erpnext/translations/kn.csv b/erpnext/translations/kn.csv index 4e40c63e576b..944a59ef3362 100644 --- a/erpnext/translations/kn.csv +++ b/erpnext/translations/kn.csv @@ -3338,7 +3338,7 @@ You cannot delete Fiscal Year {0}. Fiscal Year {0} is set as default in Global S You cannot delete Project Type 'External',ನೀವು ಪ್ರಾಜೆಕ್ಟ್ ಕೌಟುಂಬಿಕತೆ 'ಬಾಹ್ಯ' ಅನ್ನು ಅಳಿಸಲು ಸಾಧ್ಯವಿಲ್ಲ, You cannot edit root node.,ನೀವು ರೂಟ್ ನೋಡ್ ಅನ್ನು ಸಂಪಾದಿಸಲಾಗುವುದಿಲ್ಲ., You cannot restart a Subscription that is not cancelled.,ರದ್ದುಪಡಿಸದ ಚಂದಾದಾರಿಕೆಯನ್ನು ನೀವು ಮರುಪ್ರಾರಂಭಿಸಬಾರದು., -You don't have enought Loyalty Points to redeem,ರಿಡೀಮ್ ಮಾಡಲು ನೀವು ಲಾಯಲ್ಟಿ ಪಾಯಿಂಟುಗಳನ್ನು ಹೊಂದಿದ್ದೀರಿ, +You don't have enough Loyalty Points to redeem,ರಿಡೀಮ್ ಮಾಡಲು ನೀವು ಲಾಯಲ್ಟಿ ಪಾಯಿಂಟುಗಳನ್ನು ಹೊಂದಿದ್ದೀರಿ, You have already assessed for the assessment criteria {}.,ನೀವು ಈಗಾಗಲೇ ಮೌಲ್ಯಮಾಪನ ಮಾನದಂಡದ ನಿರ್ಣಯಿಸುವ {}., You have already selected items from {0} {1},ನೀವು ಈಗಾಗಲೇ ಆಯ್ಕೆ ಐಟಂಗಳನ್ನು ಎಂದು {0} {1}, You have been invited to collaborate on the project: {0},ನೀವು ಯೋಜನೆಯ ಸಹಯೋಗಿಸಲು ಆಮಂತ್ರಿಸಲಾಗಿದೆ: {0}, diff --git a/erpnext/translations/ko.csv b/erpnext/translations/ko.csv index 36ec3affcec6..66b8692afe8c 100644 --- a/erpnext/translations/ko.csv +++ b/erpnext/translations/ko.csv @@ -3338,7 +3338,7 @@ You cannot delete Fiscal Year {0}. Fiscal Year {0} is set as default in Global S You cannot delete Project Type 'External',프로젝트 유형 '외부'를 삭제할 수 없습니다., You cannot edit root node.,루트 노드는 편집 할 수 없습니다., You cannot restart a Subscription that is not cancelled.,취소되지 않은 구독은 다시 시작할 수 없습니다., -You don't have enought Loyalty Points to redeem,사용하기에 충성도 포인트가 충분하지 않습니다., +You don't have enough Loyalty Points to redeem,사용하기에 충성도 포인트가 충분하지 않습니다., You have already assessed for the assessment criteria {}.,이미 평가 기준 {}을 (를) 평가했습니다., You have already selected items from {0} {1},이미에서 항목을 선택한 {0} {1}, You have been invited to collaborate on the project: {0},당신은 프로젝트 공동 작업에 초대되었습니다 : {0}, diff --git a/erpnext/translations/ku.csv b/erpnext/translations/ku.csv index 28927a08d105..325a0a7d2560 100644 --- a/erpnext/translations/ku.csv +++ b/erpnext/translations/ku.csv @@ -3338,7 +3338,7 @@ You cannot delete Fiscal Year {0}. Fiscal Year {0} is set as default in Global S You cannot delete Project Type 'External',Hûn nikarin jêbirinê hilbijêre 'External', You cannot edit root node.,Hûn nikarin node root root biguherînin., You cannot restart a Subscription that is not cancelled.,Hûn nikarin endamê peymana ku destûr nabe., -You don't have enought Loyalty Points to redeem,Hûn pisporên dilsozî ne ku hûn bistînin, +You don't have enough Loyalty Points to redeem,Hûn pisporên dilsozî ne ku hûn bistînin, You have already assessed for the assessment criteria {}.,Tu niha ji bo nirxandina nirxandin {}., You have already selected items from {0} {1},Jixwe te tomar ji hilbijartî {0} {1}, You have been invited to collaborate on the project: {0},Hûn hatine vexwendin ji bo hevkariyê li ser vê projeyê: {0}, diff --git a/erpnext/translations/lo.csv b/erpnext/translations/lo.csv index 3904308af2fd..b8b7a5d623ba 100644 --- a/erpnext/translations/lo.csv +++ b/erpnext/translations/lo.csv @@ -3338,7 +3338,7 @@ You cannot delete Fiscal Year {0}. Fiscal Year {0} is set as default in Global S You cannot delete Project Type 'External',ທ່ານບໍ່ສາມາດລຶບປະເພດໂຄງການ 'ພາຍນອກ', You cannot edit root node.,ທ່ານບໍ່ສາມາດແກ້ໄຂຮາກຮາກ., You cannot restart a Subscription that is not cancelled.,ທ່ານບໍ່ສາມາດເລີ່ມຕົ້ນລະບົບຈອງໃຫມ່ທີ່ບໍ່ໄດ້ຖືກຍົກເລີກ., -You don't have enought Loyalty Points to redeem,ທ່ານບໍ່ມີຈຸດປະສົງອັນຄົບຖ້ວນພໍທີ່ຈະຊື້, +You don't have enough Loyalty Points to redeem,ທ່ານບໍ່ມີຈຸດປະສົງອັນຄົບຖ້ວນພໍທີ່ຈະຊື້, You have already assessed for the assessment criteria {}.,ທ່ານໄດ້ປະເມີນແລ້ວສໍາລັບມາດຕະຖານການປະເມີນຜົນ {}., You have already selected items from {0} {1},ທ່ານໄດ້ຄັດເລືອກເອົາແລ້ວລາຍການຈາກ {0} {1}, You have been invited to collaborate on the project: {0},ທ່ານໄດ້ຖືກເຊື້ອເຊີນເພື່ອເຮັດວຽກຮ່ວມກັນກ່ຽວກັບໂຄງການ: {0}, diff --git a/erpnext/translations/lt.csv b/erpnext/translations/lt.csv index d05688c6537a..5f249a742df3 100644 --- a/erpnext/translations/lt.csv +++ b/erpnext/translations/lt.csv @@ -3338,7 +3338,7 @@ You cannot delete Fiscal Year {0}. Fiscal Year {0} is set as default in Global S You cannot delete Project Type 'External',Negalite ištrinti projekto tipo "Išorinis", You cannot edit root node.,Jūs negalite redaguoti šakninis mazgas., You cannot restart a Subscription that is not cancelled.,"Jūs negalite iš naujo paleisti Prenumeratos, kuri nėra atšaukta.", -You don't have enought Loyalty Points to redeem,Jūs neturite nusipirkti lojalumo taškų išpirkti, +You don't have enough Loyalty Points to redeem,Jūs neturite nusipirkti lojalumo taškų išpirkti, You have already assessed for the assessment criteria {}.,Jūs jau įvertintas vertinimo kriterijus {}., You have already selected items from {0} {1},Jūs jau pasirinkote elementus iš {0} {1}, You have been invited to collaborate on the project: {0},Jūs buvote pakviestas bendradarbiauti su projektu: {0}, diff --git a/erpnext/translations/lv.csv b/erpnext/translations/lv.csv index d5cf852bc6b9..f7a3d356f803 100644 --- a/erpnext/translations/lv.csv +++ b/erpnext/translations/lv.csv @@ -3338,7 +3338,7 @@ You cannot delete Fiscal Year {0}. Fiscal Year {0} is set as default in Global S You cannot delete Project Type 'External',Jūs nevarat izdzēst projekta veidu "Ārējais", You cannot edit root node.,Jūs nevarat rediģēt saknes mezglu., You cannot restart a Subscription that is not cancelled.,"Jūs nevarat atsākt Abonementu, kas nav atcelts.", -You don't have enought Loyalty Points to redeem,Jums nav lojalitātes punktu atpirkt, +You don't have enough Loyalty Points to redeem,Jums nav lojalitātes punktu atpirkt, You have already assessed for the assessment criteria {}.,Jūs jau izvērtēta vērtēšanas kritērijiem {}., You have already selected items from {0} {1},Jūs jau atsevišķus posteņus {0} {1}, You have been invited to collaborate on the project: {0},Jūs esat uzaicināts sadarboties projektam: {0}, diff --git a/erpnext/translations/mk.csv b/erpnext/translations/mk.csv index e01cb70e926d..06c2aff0b442 100644 --- a/erpnext/translations/mk.csv +++ b/erpnext/translations/mk.csv @@ -3338,7 +3338,7 @@ You cannot delete Fiscal Year {0}. Fiscal Year {0} is set as default in Global S You cannot delete Project Type 'External',Не можете да го избришете Типот на проектот 'External', You cannot edit root node.,Не можете да уредувате корен јазол., You cannot restart a Subscription that is not cancelled.,Не можете да ја рестартирате претплатата која не е откажана., -You don't have enought Loyalty Points to redeem,Вие не сте донеле лојални точки за откуп, +You don't have enough Loyalty Points to redeem,Вие не сте донеле лојални точки за откуп, You have already assessed for the assessment criteria {}.,Веќе сте се проценува за критериумите за оценување {}., You have already selected items from {0} {1},Веќе сте одбрале предмети од {0} {1}, You have been invited to collaborate on the project: {0},Вие сте поканети да соработуваат на проектот: {0}, diff --git a/erpnext/translations/ml.csv b/erpnext/translations/ml.csv index c5a98b6d2560..f28ea330402e 100644 --- a/erpnext/translations/ml.csv +++ b/erpnext/translations/ml.csv @@ -3338,7 +3338,7 @@ You cannot delete Fiscal Year {0}. Fiscal Year {0} is set as default in Global S You cannot delete Project Type 'External',നിങ്ങൾക്ക് പദ്ധതി തരം 'ബാഹ്യ' ഇല്ലാതാക്കാൻ കഴിയില്ല, You cannot edit root node.,നിങ്ങൾക്ക് റൂട്ട് നോഡ് എഡിറ്റുചെയ്യാൻ കഴിയില്ല., You cannot restart a Subscription that is not cancelled.,നിങ്ങൾക്ക് റദ്ദാക്കാത്ത ഒരു സബ്സ്ക്രിപ്ഷൻ പുനഃരാരംഭിക്കാൻ കഴിയില്ല., -You don't have enought Loyalty Points to redeem,നിങ്ങൾക്ക് വീണ്ടെടുക്കാനുള്ള വിശ്വസ്ത ടയറുകൾ ആവശ്യമില്ല, +You don't have enough Loyalty Points to redeem,നിങ്ങൾക്ക് വീണ്ടെടുക്കാനുള്ള വിശ്വസ്ത ടയറുകൾ ആവശ്യമില്ല, You have already assessed for the assessment criteria {}.,ഇതിനകം നിങ്ങൾ വിലയിരുത്തൽ മാനദണ്ഡങ്ങൾ {} വേണ്ടി വിലയിരുത്തി ചെയ്തു., You have already selected items from {0} {1},നിങ്ങൾ ഇതിനകം നിന്ന് {0} {1} ഇനങ്ങൾ തിരഞ്ഞെടുത്തു, You have been invited to collaborate on the project: {0},നിങ്ങൾ പദ്ധതി സഹകരിക്കുക ക്ഷണിച്ചു: {0}, diff --git a/erpnext/translations/mr.csv b/erpnext/translations/mr.csv index 21aaa3f8bbf9..64b074882965 100644 --- a/erpnext/translations/mr.csv +++ b/erpnext/translations/mr.csv @@ -3338,7 +3338,7 @@ You cannot delete Fiscal Year {0}. Fiscal Year {0} is set as default in Global S You cannot delete Project Type 'External',आपण प्रोजेक्ट प्रकार 'बाह्य' हटवू शकत नाही, You cannot edit root node.,आपण मूळ नोड संपादित करू शकत नाही., You cannot restart a Subscription that is not cancelled.,आपण रद्द न केलेली सबस्क्रिप्शन पुन्हा सुरू करू शकत नाही., -You don't have enought Loyalty Points to redeem,आपण परत विकत घेण्यासाठी निष्ठावान बिंदू नाहीत, +You don't have enough Loyalty Points to redeem,आपण परत विकत घेण्यासाठी निष्ठावान बिंदू नाहीत, You have already assessed for the assessment criteria {}.,आपण मूल्यांकन निकष आधीच मूल्यमापन आहे {}., You have already selected items from {0} {1},आपण आधीच आयटम निवडले आहेत {0} {1}, You have been invited to collaborate on the project: {0},आपण प्रकल्प सहयोग करण्यासाठी आमंत्रित आहेत: {0}, diff --git a/erpnext/translations/ms.csv b/erpnext/translations/ms.csv index 5a3d986f5b58..5d2c2a890e7a 100644 --- a/erpnext/translations/ms.csv +++ b/erpnext/translations/ms.csv @@ -3338,7 +3338,7 @@ You cannot delete Fiscal Year {0}. Fiscal Year {0} is set as default in Global S You cannot delete Project Type 'External',Anda tidak boleh memadam Jenis Projek 'Luar', You cannot edit root node.,Anda tidak boleh mengedit nod akar., You cannot restart a Subscription that is not cancelled.,Anda tidak boleh memulakan semula Langganan yang tidak dibatalkan., -You don't have enought Loyalty Points to redeem,Anda tidak mempunyai mata Kesetiaan yang cukup untuk menebusnya, +You don't have enough Loyalty Points to redeem,Anda tidak mempunyai mata Kesetiaan yang cukup untuk menebusnya, You have already assessed for the assessment criteria {}.,Anda telah pun dinilai untuk kriteria penilaian {}., You have already selected items from {0} {1},Anda telah memilih barangan dari {0} {1}, You have been invited to collaborate on the project: {0},Anda telah dijemput untuk bekerjasama dalam projek: {0}, diff --git a/erpnext/translations/my.csv b/erpnext/translations/my.csv index 7638e762ba47..0471f66bc55e 100644 --- a/erpnext/translations/my.csv +++ b/erpnext/translations/my.csv @@ -3338,7 +3338,7 @@ You cannot delete Fiscal Year {0}. Fiscal Year {0} is set as default in Global S You cannot delete Project Type 'External',သငျသညျစီမံကိန်းအမျိုးအစား '' ပြင်ပ '' မဖျက်နိုင်ပါ, You cannot edit root node.,သငျသညျအမြစ် node ကိုတည်းဖြတ်မရနိုင်ပါ။, You cannot restart a Subscription that is not cancelled.,သငျသညျဖျက်သိမ်းမပေးကြောင်းတစ် Subscription ပြန်လည်စတင်ရန်လို့မရပါဘူး။, -You don't have enought Loyalty Points to redeem,သငျသညျကိုရှေးနှုတျမှ enought သစ္စာရှိမှုအမှတ်ရှိသည်မဟုတ်ကြဘူး, +You don't have enough Loyalty Points to redeem,သငျသညျကိုရှေးနှုတျမှ enough သစ္စာရှိမှုအမှတ်ရှိသည်မဟုတ်ကြဘူး, You have already assessed for the assessment criteria {}.,သငျသညျပြီးသား {} အဆိုပါအကဲဖြတ်သတ်မှတ်ချက်အဘို့အအကဲဖြတ်ပါပြီ။, You have already selected items from {0} {1},သငျသညျပြီးသား {0} {1} ကနေပစ္စည်းကိုရှေးခယျြခဲ့ကြ, You have been invited to collaborate on the project: {0},သငျသညျစီမံကိန်းကိုအပေါ်ပူးပေါင်းဖို့ဖိတ်ခေါ်ခဲ့ကြ: {0}, diff --git a/erpnext/translations/nl.csv b/erpnext/translations/nl.csv index b65f22334c75..1efc5000b779 100644 --- a/erpnext/translations/nl.csv +++ b/erpnext/translations/nl.csv @@ -3338,7 +3338,7 @@ You cannot delete Fiscal Year {0}. Fiscal Year {0} is set as default in Global S You cannot delete Project Type 'External',U kunt projecttype 'extern' niet verwijderen, You cannot edit root node.,U kunt het basisknooppunt niet bewerken., You cannot restart a Subscription that is not cancelled.,U kunt een Abonnement dat niet is geannuleerd niet opnieuw opstarten., -You don't have enought Loyalty Points to redeem,Je hebt geen genoeg loyaliteitspunten om in te wisselen, +You don't have enough Loyalty Points to redeem,Je hebt geen genoeg loyaliteitspunten om in te wisselen, You have already assessed for the assessment criteria {}.,U heeft al beoordeeld op de beoordelingscriteria {}., You have already selected items from {0} {1},U heeft reeds geselecteerde items uit {0} {1}, You have been invited to collaborate on the project: {0},U bent uitgenodigd om mee te werken aan het project: {0}, diff --git a/erpnext/translations/no.csv b/erpnext/translations/no.csv index 20b8916eaf26..2642a9c70876 100644 --- a/erpnext/translations/no.csv +++ b/erpnext/translations/no.csv @@ -3338,7 +3338,7 @@ You cannot delete Fiscal Year {0}. Fiscal Year {0} is set as default in Global S You cannot delete Project Type 'External',Du kan ikke slette Project Type 'External', You cannot edit root node.,Du kan ikke redigere rotknutepunktet., You cannot restart a Subscription that is not cancelled.,Du kan ikke starte en abonnement som ikke er kansellert., -You don't have enought Loyalty Points to redeem,Du har ikke nok lojalitetspoeng til å innløse, +You don't have enough Loyalty Points to redeem,Du har ikke nok lojalitetspoeng til å innløse, You have already assessed for the assessment criteria {}.,Du har allerede vurdert for vurderingskriteriene {}., You have already selected items from {0} {1},Du har allerede valgt elementer fra {0} {1}, You have been invited to collaborate on the project: {0},Du har blitt invitert til å samarbeide om prosjektet: {0}, diff --git a/erpnext/translations/pl.csv b/erpnext/translations/pl.csv index 4a93d4987567..ca820258f439 100644 --- a/erpnext/translations/pl.csv +++ b/erpnext/translations/pl.csv @@ -3338,7 +3338,7 @@ You cannot delete Fiscal Year {0}. Fiscal Year {0} is set as default in Global S You cannot delete Project Type 'External',Nie można usunąć typu projektu "zewnętrzny", You cannot edit root node.,Nie można edytować węzła głównego., You cannot restart a Subscription that is not cancelled.,"Nie można ponownie uruchomić subskrypcji, która nie zostanie anulowana.", -You don't have enought Loyalty Points to redeem,"Nie masz wystarczającej liczby Punktów Lojalnościowych, aby je wykorzystać", +You don't have enough Loyalty Points to redeem,"Nie masz wystarczającej liczby Punktów Lojalnościowych, aby je wykorzystać", You have already assessed for the assessment criteria {}.,Oceniałeś już kryteria oceny {}., You have already selected items from {0} {1},Już wybrane pozycje z {0} {1}, You have been invited to collaborate on the project: {0},Zostałeś zaproszony do współpracy przy projekcie: {0}, diff --git a/erpnext/translations/ps.csv b/erpnext/translations/ps.csv index 26cd0a9cbcb8..ed15740e3781 100644 --- a/erpnext/translations/ps.csv +++ b/erpnext/translations/ps.csv @@ -3338,7 +3338,7 @@ You cannot delete Fiscal Year {0}. Fiscal Year {0} is set as default in Global S You cannot delete Project Type 'External',تاسو د پروژې ډول 'بهرني' نه ړنګولی شئ, You cannot edit root node.,تاسو د ریډ نوډ سمون نشو کولی., You cannot restart a Subscription that is not cancelled.,تاسو نشي کولی هغه یو بل ریکارډ بیا پیل کړئ چې رد شوی نه وي., -You don't have enought Loyalty Points to redeem,تاسو د ژغورلو لپاره د وفادارۍ ټکي نلرئ, +You don't have enough Loyalty Points to redeem,تاسو د ژغورلو لپاره د وفادارۍ ټکي نلرئ, You have already assessed for the assessment criteria {}.,تاسو مخکې د ارزونې معیارونه ارزول {}., You have already selected items from {0} {1},تاسو وخته ټاکل څخه توکي {0} د {1}, You have been invited to collaborate on the project: {0},تاسو ته په دغه پروژه کې همکاري بلل شوي دي: {0}, diff --git a/erpnext/translations/pt-BR.csv b/erpnext/translations/pt-BR.csv index edaaddd6a78f..503a16f7ebea 100644 --- a/erpnext/translations/pt-BR.csv +++ b/erpnext/translations/pt-BR.csv @@ -3338,7 +3338,7 @@ You cannot delete Fiscal Year {0}. Fiscal Year {0} is set as default in Global S You cannot delete Project Type 'External',Você não pode excluir o Tipo de Projeto ';Externo';, You cannot edit root node.,Você não pode editar o nó raiz., You cannot restart a Subscription that is not cancelled.,Você não pode reiniciar uma Assinatura que não seja cancelada., -You don't have enought Loyalty Points to redeem,Você não tem suficientes pontos de lealdade para resgatar, +You don't have enough Loyalty Points to redeem,Você não tem suficientes pontos de lealdade para resgatar, You have already assessed for the assessment criteria {}.,Você já avaliou os critérios de avaliação {}., You have already selected items from {0} {1},Já selecionou itens de {0} {1}, You have been invited to collaborate on the project: {0},Você foi convidado para colaborar com o projeto: {0}, diff --git a/erpnext/translations/pt.csv b/erpnext/translations/pt.csv index 5cc486d8be2d..3e83df5f6933 100644 --- a/erpnext/translations/pt.csv +++ b/erpnext/translations/pt.csv @@ -3338,7 +3338,7 @@ You cannot delete Fiscal Year {0}. Fiscal Year {0} is set as default in Global S You cannot delete Project Type 'External',Você não pode excluir o Tipo de Projeto 'Externo', You cannot edit root node.,Você não pode editar o nó raiz., You cannot restart a Subscription that is not cancelled.,Você não pode reiniciar uma Assinatura que não seja cancelada., -You don't have enought Loyalty Points to redeem,Você não tem suficientes pontos de lealdade para resgatar, +You don't have enough Loyalty Points to redeem,Você não tem suficientes pontos de lealdade para resgatar, You have already assessed for the assessment criteria {}.,Você já avaliou os critérios de avaliação {}., You have already selected items from {0} {1},Já selecionou itens de {0} {1}, You have been invited to collaborate on the project: {0},Foi convidado para colaborar com o projeto: {0}, diff --git a/erpnext/translations/ro.csv b/erpnext/translations/ro.csv index 8f97d072021d..91c92070758f 100644 --- a/erpnext/translations/ro.csv +++ b/erpnext/translations/ro.csv @@ -3338,7 +3338,7 @@ You cannot delete Fiscal Year {0}. Fiscal Year {0} is set as default in Global S You cannot delete Project Type 'External',Nu puteți șterge tipul de proiect "extern", You cannot edit root node.,Nu puteți edita nodul rădăcină., You cannot restart a Subscription that is not cancelled.,Nu puteți reporni o abonament care nu este anulat., -You don't have enought Loyalty Points to redeem,Nu aveți puncte de loialitate pentru a răscumpăra, +You don't have enough Loyalty Points to redeem,Nu aveți puncte de loialitate pentru a răscumpăra, You have already assessed for the assessment criteria {}.,Ați evaluat deja criteriile de evaluare {}., You have already selected items from {0} {1},Ați selectat deja un produs de la {0} {1}, You have been invited to collaborate on the project: {0},Ați fost invitat să colaboreze la proiect: {0}, diff --git a/erpnext/translations/ru.csv b/erpnext/translations/ru.csv index 1fc37ddedb6b..00641159e064 100644 --- a/erpnext/translations/ru.csv +++ b/erpnext/translations/ru.csv @@ -3336,7 +3336,7 @@ You cannot delete Fiscal Year {0}. Fiscal Year {0} is set as default in Global S You cannot delete Project Type 'External',Вы не можете удалить проект типа "Внешний", You cannot edit root node.,Вы не можете редактировать корневой узел., You cannot restart a Subscription that is not cancelled.,"Вы не можете перезапустить подписку, которая не отменена.", -You don't have enought Loyalty Points to redeem,У вас недостаточно очков лояльности для выкупа, +You don't have enough Loyalty Points to redeem,У вас недостаточно очков лояльности для выкупа, You have already assessed for the assessment criteria {}.,Вы уже оценили критерии оценки {}., You have already selected items from {0} {1},Вы уже выбрали продукты из {0} {1}, You have been invited to collaborate on the project: {0},Вы были приглашены для совместной работы над проектом: {0}, diff --git a/erpnext/translations/rw.csv b/erpnext/translations/rw.csv index 741f11792f0d..2d8c07ee58dd 100644 --- a/erpnext/translations/rw.csv +++ b/erpnext/translations/rw.csv @@ -3338,7 +3338,7 @@ You cannot delete Fiscal Year {0}. Fiscal Year {0} is set as default in Global S You cannot delete Project Type 'External',Ntushobora gusiba Ubwoko bwumushinga 'Hanze', You cannot edit root node.,Ntushobora guhindura imizi., You cannot restart a Subscription that is not cancelled.,Ntushobora gutangira Kwiyandikisha bidahagaritswe., -You don't have enought Loyalty Points to redeem,Ntabwo ufite amanota ahagije yo gucungura, +You don't have enough Loyalty Points to redeem,Ntabwo ufite amanota ahagije yo gucungura, You have already assessed for the assessment criteria {}.,Mumaze gusuzuma ibipimo ngenderwaho {}., You have already selected items from {0} {1},Mumaze guhitamo ibintu kuva {0} {1}, You have been invited to collaborate on the project: {0},Watumiwe gufatanya kumushinga: {0}, diff --git a/erpnext/translations/si.csv b/erpnext/translations/si.csv index e5ea9bff7b1f..67fa6ae51dde 100644 --- a/erpnext/translations/si.csv +++ b/erpnext/translations/si.csv @@ -3338,7 +3338,7 @@ You cannot delete Fiscal Year {0}. Fiscal Year {0} is set as default in Global S You cannot delete Project Type 'External',ඔබට ව්යාපෘති වර්ගය 'බාහිර', You cannot edit root node.,ඔබට root node සංස්කරණය කළ නොහැක., You cannot restart a Subscription that is not cancelled.,අවලංගු නොකළ දායකත්ව නැවත ආරම්භ කළ නොහැක., -You don't have enought Loyalty Points to redeem,ඔබ මුදා හැරීමට පක්ෂපාතීත්වයේ පොත්වලට ඔබ කැමති නැත, +You don't have enough Loyalty Points to redeem,ඔබ මුදා හැරීමට පක්ෂපාතීත්වයේ පොත්වලට ඔබ කැමති නැත, You have already assessed for the assessment criteria {}.,තක්සේරු නිර්ණායක {} සඳහා ඔබ දැනටමත් තක්සේරු කර ඇත., You have already selected items from {0} {1},ඔබ මේ වන විටත් {0} {1} සිට භාණ්ඩ තෝරාගෙන ඇති, You have been invited to collaborate on the project: {0},ඔබ මෙම ව්යාපෘතිය පිළිබඳව සහයෝගයෙන් කටයුතු කිරීමට ආරාධනා කර ඇත: {0}, diff --git a/erpnext/translations/sk.csv b/erpnext/translations/sk.csv index d16c49201a37..7bdfdffbaf95 100644 --- a/erpnext/translations/sk.csv +++ b/erpnext/translations/sk.csv @@ -3338,7 +3338,7 @@ You cannot delete Fiscal Year {0}. Fiscal Year {0} is set as default in Global S You cannot delete Project Type 'External',Nemôžete odstrániť typ projektu "Externé", You cannot edit root node.,Nemôžete upraviť koreňový uzol., You cannot restart a Subscription that is not cancelled.,"Predplatné, ktoré nie je zrušené, nemôžete reštartovať.", -You don't have enought Loyalty Points to redeem,Nemáte dostatok vernostných bodov na vykúpenie, +You don't have enough Loyalty Points to redeem,Nemáte dostatok vernostných bodov na vykúpenie, You have already assessed for the assessment criteria {}.,Vyhodnotili ste kritériá hodnotenia {}., You have already selected items from {0} {1},Už ste vybrané položky z {0} {1}, You have been invited to collaborate on the project: {0},Boli ste pozvaní k spolupráci na projekte: {0}, diff --git a/erpnext/translations/sl.csv b/erpnext/translations/sl.csv index 08901606c4fc..370c3c61ae7a 100644 --- a/erpnext/translations/sl.csv +++ b/erpnext/translations/sl.csv @@ -3338,7 +3338,7 @@ You cannot delete Fiscal Year {0}. Fiscal Year {0} is set as default in Global S You cannot delete Project Type 'External',Ne morete izbrisati vrste projekta "Zunanji", You cannot edit root node.,Rootnega vozlišča ne morete urejati., You cannot restart a Subscription that is not cancelled.,"Naročnino, ki ni preklican, ne morete znova zagnati.", -You don't have enought Loyalty Points to redeem,Za unovčevanje niste prejeli točk za zvestobo, +You don't have enough Loyalty Points to redeem,Za unovčevanje niste prejeli točk za zvestobo, You have already assessed for the assessment criteria {}.,Ste že ocenili za ocenjevalnih meril {}., You have already selected items from {0} {1},Ste že izbrane postavke iz {0} {1}, You have been invited to collaborate on the project: {0},Ti so bili povabljeni k sodelovanju na projektu: {0}, diff --git a/erpnext/translations/sq.csv b/erpnext/translations/sq.csv index 987211ab7a85..f069a059d4e4 100644 --- a/erpnext/translations/sq.csv +++ b/erpnext/translations/sq.csv @@ -3338,7 +3338,7 @@ You cannot delete Fiscal Year {0}. Fiscal Year {0} is set as default in Global S You cannot delete Project Type 'External',Ju nuk mund të fshini llojin e projektit 'Jashtë', You cannot edit root node.,Nuk mund të ndryshosh nyjen e rrënjës., You cannot restart a Subscription that is not cancelled.,Nuk mund të rifilloni një Abonimi që nuk anulohet., -You don't have enought Loyalty Points to redeem,Ju nuk keni shumë pikat e Besnikërisë për të shpenguar, +You don't have enough Loyalty Points to redeem,Ju nuk keni shumë pikat e Besnikërisë për të shpenguar, You have already assessed for the assessment criteria {}.,Ju kanë vlerësuar tashmë me kriteret e vlerësimit {}., You have already selected items from {0} {1},Ju keni zgjedhur tashmë artikuj nga {0} {1}, You have been invited to collaborate on the project: {0},Ju keni qenë të ftuar për të bashkëpunuar në këtë projekt: {0}, diff --git a/erpnext/translations/sr-SP.csv b/erpnext/translations/sr-SP.csv index 27ac9448abb1..64c2f63e98e8 100644 --- a/erpnext/translations/sr-SP.csv +++ b/erpnext/translations/sr-SP.csv @@ -265,6 +265,7 @@ DocType: Supplier,Name and Type,Ime i tip DocType: Customs Tariff Number,Customs Tariff Number,Carinska tarifa apps/erpnext/erpnext/hr/doctype/employee_benefit_application/employee_benefit_application.py +65,"You can claim only an amount of {0}, the rest amount {1} should be in the application \ as pro-rata component","Можете тражити само износ од {0}, остатак од {1} би требао бити у апликацији \ као про-рата компонента." +<<<<<<< HEAD DocType: Crop,Yield UOM,Јединица мере приноса DocType: Item Default,Default Supplier,Podrazumijevani dobavljač apps/erpnext/erpnext/healthcare/page/medical_record/patient_select.html +3,Select Patient,Izaberite pacijenta @@ -904,6 +905,647 @@ apps/erpnext/erpnext/config/selling.py +46,All Addresses.,Sve adrese apps/erpnext/erpnext/utilities/user_progress.py +39,Opening Balances,Početna stanja apps/erpnext/erpnext/config/setup.py +66,Users and Permissions,Korisnici i dozvole apps/erpnext/erpnext/hr/doctype/staffing_plan/staffing_plan.py +68,"You can only plan for upto {0} vacancies and budget {1} \ +======= +Yield UOM,Јединица мере приноса +Default Supplier,Podrazumijevani dobavljač +Select Patient,Izaberite pacijenta, +Opening,Početno stanje, +Customer Groups,Grupe kupaca, +Item Manager,Menadžer artikala, +Fiscal Year Company,Fiskalna godina preduzeća, +Patient Appointment,Zakazivanje pacijenata, +Show In Website,Prikaži na web sajtu, +Paid Amount,Uplaćeno, +Total Paid Amount,Ukupno plaćeno, +Purchase Receipt {0} is not submitted,Prijem robe {0} nije potvrđen, +Items and Pricing,Proizvodi i cijene, +Account Paid From,Račun plaćen preko, +Create customer quotes,Kreirajte bilješke kupca, +Supplier Warehouse,Skladište dobavljača, +Customer is required,Kupac je obavezan podatak, +Manufacturer,Proizvođač +Selling Amount,Prodajni iznos, +Please set the Date Of Joining for employee {0},Molimo podesite datum zasnivanja radnog odnosa {0} +Allow over delivery or receipt upto this percent,Dozvolite isporukuili prijem robe ukoliko ne premaši ovaj procenat, +Orders,Porudžbine, +Stock Transactions,Promjene na zalihama, +You are not authorized to approve leaves on Block Dates,Немате дозволу да одобравате одсуства на Блок Датумима. +Daily Timesheet Summary,Pregled dnevnog potrošenog vremena, +View Timesheet,Pogledaj potrošeno vrijeme, +Rounded Total (Company Currency),Zaokruženi ukupan iznos (valuta preduzeća) +Salary Slip of employee {0} already created for this period,Isplatna lista Zaposlenog {0} kreirana je već za ovaj period, +"If this item has variants, then it cannot be selected in sales orders etc.","Ako ovaj artikal ima varijante, onda ne može biti biran u prodajnom nalogu." +You can only redeem max {0} points in this order.,Можете унети највише {0} поена у овој наруџбини. +No leave record found for employee {0} for {1},Nije nađena evidancija o odsustvu Zaposlenog {0} za {1} +Discount on Price List Rate (%),Popust na cijene iz cjenovnika (%) +Groups,Grupe, +Item Attribute,Atribut artikla, +Amount in customer's currency,Iznos u valuti kupca, +Warehouse is mandatory,Skladište je obavezan podatak, +Stock Ageing,Starost zaliha, +New Sales Orders,Novi prodajni nalozi, +Invoice Created,Kreirana faktura, +Employee Internal Work History,Interna radna istorija Zaposlenog, +Cart is Empty,Korpa je prazna, +Patient Details,Detalji o pacijentu, +Stock Entry {0} is not submitted,Unos zaliha {0} nije potvrđen, +Rest Of The World,Ostatak svijeta, +Additional Operating Cost,Dodatni operativni troškovi, +Rejected Warehouse,Odbijeno skladište, +Manufacturing Manager,Menadžer proizvodnje, +You are not present all day(s) between compensatory leave request days,Нисте присутни свих дана између захтева за компензацијски одмор. +Is Fixed Asset,Artikal je osnovno sredstvo, +POS,POS, +Timesheet {0} is already completed or cancelled,Potrošeno vrijeme {0} je već potvrđeno ili otkazano, + (Half Day),(Pola dana) +Net Weight,Neto težina, +Attendance Record {0} exists against Student {1},Zapis o prisustvu {0} постоји kod studenata {1} +Outstanding,Preostalo, +Discount (%) on Price List Rate with Margin,Popust (%) +Select Shipping Address,Odaberite adresu isporuke, +Amount to Bill,Iznos za fakturisanje, +Make Sales Orders to help you plan your work and deliver on-time,Kreiranje prodajnog naloga će vam pomoći da isplanirate svoje vrijeme i dostavite robu na vrijeme, +Sync Offline Invoices,Sinhronizuj offline fakture, +Manufacturing,Proizvodnja, +{0}% Delivered,{0}% Isporučeno, +Attendance,Prisustvo, +Customer's Purchase Order No,Broj porudžbenice kupca, +Please enter Sales Orders in the above table,U tabelu iznad unesite prodajni nalog, +Report Date,Datum izvještaja, +Item Groups,Vrste artikala, +Discount Percentage,Procenat popusta, +Gross Profit %,Bruto dobit% +"You can define all the tasks which need to carried out for this crop here. The day field is used to mention the day on which the task needs to be carried out, 1 being the 1st day, etc.. ","Овде можете дефинисати све задатке које је потребно извршити за ову жетву. Поље Дан говори дан на који је задатак потребно извршити, 1 је 1. дан, итд." +Payment Request,Upit za plaćanje, +Purchase Analytics,Analiza nabavke, +Tree Details,Detalji stabla, +Upload Attendance,Priloži evidenciju, +Against,Povezano sa, +Requested Amount,Traženi iznos, +"Record of all communications of type email, phone, chat, visit, etc.","Snimanje svih komunikacija tipa email, telefon, poruke, posjete, itd." +Customer Contact Email,Kontakt e-mail kupca, +Primary Address Details,Detalji o primarnoj adresi, +Above,Iznad, +Variant Based On,Varijanta zasnovana na, +Task Weight,Težina zadataka, +Transaction ID,Transakcije, +Allocated,Dodijeljeno, +Add more items or open full form,Dodaj još stavki ili otvori kompletan prozor, +Reserved for sale,Rezervisana za prodaju, +Item Group,Vrste artikala, +Age (Days),Starost (Dani) +Opening (Dr),Početno stanje (Du) +Total Outstanding Amt,Preostalo za plaćanje, +Go to the Desktop and start using ERPNext,Idite na radnu površinu i krenite sa radom u programu, +You can only have Plans with the same billing cycle in a Subscription,Сви Планови у Претплати морају имати исти циклус наплате +Name and Employee ID,Ime i ID Zaposlenog, +Invoice,Faktura, +Invoice Date,Datum fakture, +From Lead,Od Lead-a, +Database of potential customers.,Baza potencijalnih kupaca, +Project Status,Status Projekta, +All Item Groups,Sve vrste artikala, +Serial No {0} does not exist,Serijski broj {0} ne postoji, +No contacts added yet.,Još uvijek nema dodatih kontakata, +Ageing Range 3,Opseg dospijeća 3, +Request for Quotation,Zahtjev za ponudu, +Account Paid To,Račun plaćen u, +Attendance can not be marked for future dates,Učesnik ne može biti označen za buduće datume, +Sales Invoice No,Broj fakture prodaje, +Timesheet,Potrošeno vrijeme, +Don't send Employee Birthday Reminders,Nemojte slati podsjetnik o rođendanima Zaposlenih, +Available Qty at Warehouse,Dostupna količina na skladištu, +Foreign Trade Details,Spoljnotrgovinski detalji, +Minimum Order Qty,Minimalna količina za poručivanje, +No employees for the mentioned criteria,Za traženi kriterijum nema Zaposlenih, +Fiscal Year,Fiskalna godina, +Repack,Prepakovati, +Please select a warehouse,Izaberite skladište, +Received and Accepted,Primio i prihvatio, +Project will be accessible on the website to these users,Projekat će biti dostupan na sajtu sledećim korisnicima, +Upload HTML,Priloži HTML, +Services,Usluge, +Item Cart,Korpa sa artiklima, +Total Paid Amt,Ukupno plaćeno, +Warehouse Detail,Detalji o skldištu, +Quotation Item,Stavka sa ponude, +Employee Advance,Napredak Zaposlenog, +Warehouse and Reference,Skladište i veza, +{0} {1}: Account {2} is inactive,{0} {1}: Nalog {2} je neaktivan, +Fiscal Year {0} not found,Fiskalna godina {0} nije pronađena, +No Remarks,Nema napomene, +Purchase Receipt Message,Poruka u Prijemu robe, +Taxes and Charges Deducted,Umanjeni porezi i naknade, +Include Payment (POS),Uključi POS plaćanje, +Customer PO Details,Pregled porudžbine kupca, +Total Invoiced Amt,Ukupno fakturisano, +Select Brand...,Izaberite brend, +Default Unit of Measure,Podrazumijevana jedinica mjere, +Serial No,Serijski broj, +Supplier Type,Tip dobavljača, +Actual Qty {0} / Waiting Qty {1},Trenutna kol. {0} / Na čekanju {1} +Individual,Fizičko lice, +Partially Ordered,Djelimično poručeno, +Posting Date,Datum dokumenta, +Date Settings,Podešavanje datuma, +Total Allocated Amount (Company Currency),Ukupan povezani iznos (Valuta) +Income,Prihod, +Add Items,Dodaj stavke, +Price List not found or disabled,Cjenovnik nije pronađen ili je zaključan, +Weight (In Kilogram),Težina (u kg) +New Sales Invoice,Nova faktura, +New Company,Novo preduzeće, +Support Team,Tim za podršku, +Valuation Method,Način vrednovanja, +Project Type,Tip Projekta, +Returned Qty,Vraćena kol. +Additional Discount Amount (Company Currency),Iznos dodatnog popusta (valuta preduzeća) +Employee Information,Informacije o Zaposlenom, +'Days Since Last Order' must be greater than or equal to zero,"""Dana od poslednje porudžbine"" mora biti veće ili jednako nuli" +Maintenance,Održavanje, +Multiple Item prices.,Više cijena artikala, +Received From,je primljen od, +Write Off Difference Amount,Otpis razlike u iznosu, +Closing Date,Datum zatvaranja, +Cheque/Reference Date,Datum izvoda, +Planned Qty,Planirana količina, +Payment Date,Datum plaćanja, +Additional Details,Dodatni detalji, +Create Chart Of Accounts Based On,Kreiraj kontni plan prema, +You can not change rate if BOM mentioned agianst any item,Не можете променити цену ако постоји Саставница за било коју ставку. +Open To Do,Otvori To Do, +Average Discount,Prosječan popust, +Material Issue,Reklamacija robe, +Billed Amt,Fakturisani iznos, +Supplier Quotation {0} created,Ponuda dobavljaču {0} је kreirana, +Not allowed to update stock transactions older than {0},Nije dozvoljeno mijenjati Promjene na zalihama starije od {0} +Add Employees,Dodaj Zaposlenog, +Setting up Employees,Podešavanja Zaposlenih, +Warehouse not found in the system,Skladište nije pronađeno u sistemu, +Attendance for employee {0} is already marked for this day,Prisustvo zaposlenog {0} је već označeno za ovaj dan, +Employee relieved on {0} must be set as 'Left',"Zaposleni smijenjen na {0} mora biti označen kao ""Napustio""" + Shipping Bill Number,Broj isporuke, +Lab Test Report,Izvještaj labaratorijskog testa, +You cannot credit and debit same account at the same time,Не можете кредитирати и дебитовати исти налог у исто време. +Customer Name,Naziv kupca, +Current Address,Trenutna adresa, +Upcoming Calendar Events,Predstojeći događaji u kalendaru, +Make Payment via Journal Entry,Kreiraj uplatu kroz knjiženje, +Paid,Plaćeno, +Buying,Nabavka, +Default Item Group,Podrazumijevana vrsta artikala, +In Stock Qty,Na zalihama, +Taxes and Charges Deducted (Company Currency),Umanjeni porezi i naknade (valuta preduzeća) +Additional Costs,Dodatni troškovi, +Pending Review,Čeka provjeru, +Default Selling Cost Center,Podrazumijevani centar troškova, +No Customers yet!,Još uvijek nema kupaca! +Sales Return,Povraćaj prodaje, +No Items added to cart,Nema dodatih artikala na računu, +This is based on transactions against this Customer. See timeline below for details,Ovo je zasnovano na transkcijama ovog klijenta. Pogledajte vremensku liniju ispod za dodatne informacije, +Make Timesheet,Kreiraj potrošeno vrijeme, +Warning: Sales Order {0} already exists against Customer's Purchase Order {1},Upozorenje: Prodajni nalog {0}već postoji veza sa porudžbenicom kupca {1} +Healthcare Settings,Podešavanje klinike, +Accounting Ledger,Analitička kartica, +Total Outgoing Value,Ukupna vrijednost isporuke, +Sales Order {0} is {1},Prodajni nalog {0} је {1} +Automatically Set Serial Nos based on FIFO,Podesi automatski serijski broj da koristi FIFO, +New Customers,Novi kupci, +Pre Sales,Prije prodaje, +POS Customer Group,POS grupa kupaca, +Shopping Cart,Korpa sa sajta, +Reserved for manufacturing,Rezervisana za proizvodnju, +Pricing Rule Help,Pravilnik za cijene pomoć +Ageing Range 2,Opseg dospijeća 2, +Employee Benefits,Primanja Zaposlenih, +POS Item Group,POS Vrsta artikala, +Lead,Lead, +Employee Settings,Podešavanja zaposlenih, +View All Products,Pogledajte sve proizvode, +Patient Medical Record,Medicinski karton pacijenta, +Batch,Serija, +Purchase Receipt,Prijem robe, +Warranty Period (in days),Garantni rok (u danima) +Customer database.,Korisnička baza podataka, +Attendance Date,Datum prisustva, +Notify Employee,Obavijestiti Zaposlenog, +User ID not set for Employee {0},Korisnički ID nije podešen za Zaposlenog {0} +Stock Projected Qty,Projektovana količina na zalihama, +Make Payment,Kreiraj plaćanje, +You cannot delete Fiscal Year {0}. Fiscal Year {0} is set as default in Global Settings,Ne možete obrisati fiskalnu godinu {0}. Fiskalna {0} godina je označena kao trenutna u globalnim podešavanjima. +{0} units of {1} needed in {2} to complete this transaction.,Željenu količinu {0} za artikal {1} je potrebno dodati na {2} da bi dovršili transakciju.. +Item-wise Sales Register,Prodaja po artiklima, +Tax Rate,Poreska stopa, +Remarks,Napomena, +Sales,Prodaja, +Pricing Rule,Pravilnik za cijene, +Products Settings,Podešavanje proizvoda, +Mobile,Mobilni, +Price List Rate,Cijena, +Discount Amount,Vrijednost popusta, +Sales Invoice Trends,Trendovi faktura prodaje, +You don't have enough Loyalty Points to redeem,Немате довољно Бодова Лојалности. +Tax Breakup,Porez po pozicijama, +Task,Zadatak, +Add / Edit Prices,Dodaj / Izmijeni cijene, +Item Prices,Cijene artikala, +Salary Component,Компонента плате +Customer's Purchase Order Date,Datum porudžbenice kupca, +Country of Origin,Zemlja porijekla, +Please select Employee Record first.,Molimo izaberite registar Zaposlenih prvo, +Order Type,Vrsta porudžbine, +Rate & Amount,Cijena i iznos sa rabatom, +For Price List,Za cjenovnik, +Tax ID,Poreski broj, +WIP Warehouse,Wip skladište, +Itemwise Recommended Reorder Level,Pregled preporučenih nivoa dopune, +{0} against Bill {1} dated {2},{0} veza sa računom {1} na datum {2} +You are not authorized to set Frozen value,Немате дозволу да постављате замрзнуту вредност +Requested Items To Be Ordered,Tražene stavke za isporuku, +Unmarked Attendance,Neobilježeno prisustvo, +Sales Order {0} is not submitted,Prodajni nalog {0} nije potvrđen, +Default Material Request Type,Podrazumijevani zahtjev za tip materijala, +Sales Pipeline,Prodajna linija, +Already completed,Već završen, +Ordered Qty,Poručena kol, +Sales Details,Detalji prodaje, +Navigating,Navigacija, +Your Products or Services,Vaši artikli ili usluge, +CRM,CRM, +The Brand,Brend, +Quotation {0} is cancelled,Ponuda {0} je otkazana, +Item Code,Šifra artikla, +Customer Mobile No,Broj telefona kupca, +Reorder Qty,Kol. za dopunu, +Move Item,Premještanje artikala, +Buying Settings,Podešavanja nabavke, +From Employee,Od Zaposlenog, +Fleet Manager,Menadžer transporta, +Stock Levels,Nivoi zalihe, +Rate With Margin (Company Currency),Cijena sa popustom (Valuta preduzeća) +Closing (Cr),Saldo (Po) +Product Bundle,Sastavnica, +Sales and Returns,Prodaja i povraćaji, +Sync Master Data,Sinhronizuj podatke iz centrale, +Sales Person Name,Ime prodajnog agenta, +Purchase Receipts,Prijemi robe, +Customizing Forms,Prilagođavanje formi, +Attendance for employee {0} is already marked,Prisustvo zaposlenog {0} je već označeno, +% Complete Method,% metod vrednovanja završetka projekta, +Overdue,Istekao, +Posting Time,Vrijeme izrade računa, +Purchase Receipt No,Broj prijema robe, +Expected End Date,Očekivani datum završetka, +Expected End Date can not be less than Expected Start Date,Očekivani datum završetka ne može biti manji od očekivanog dana početka, +Customer Primary Contact,Primarni kontakt kupca, +Expected Start Date,Očekivani datum početka, +Credit Limit,Kreditni limit, +Item Tax,Porez, +Selling,Prodaja, +Customer Contact,Kontakt kupca, +Item {0} does not exist,Artikal {0} ne postoji, +Add Users,Dodaj korisnike, +Select Serial Numbers,Izaberite serijske brojeve, +Payment Entry,Uplate, +In Words,Riječima, +Employee record is created using selected field. ,Izvještaj o Zaposlenom se kreira korišćenjem izabranog polja. +Serial No {0} does not belong to Delivery Note {1},Serijski broj {0} ne pripada otpremnici {1} +Support,Podrška, +Get Sales Orders,Pregledaj prodajne naloge, +Stock Ledger Entry,Unos zalihe robe, +Gantt chart of all tasks.,Gantov grafikon svih zadataka, +Price List Rate (Company Currency),Cijena (Valuta preduzeća) +Address Name,Naziv adrese, +Another Sales Person {0} exists with the same Employee id,Postoji još jedan Prodavac {0} sa istim ID zaposlenog, +Item Group Name,Naziv vrste artikala, +A Customer Group exists with same name please change the Customer name or rename the Customer Group,Isto ime grupe kupca već postoji. Promijenite ime kupca ili izmijenite grupu kupca, +Warning: Another {0} # {1} exists against stock entry {2},Upozorenje: Još jedan {0} # {1} postoji u vezanom Unosu zaliha {2} +Suplier,Dobavljač +Has Serial No,Ima serijski broj, +Employee {0} on Half day on {1},Zaposleni {0} na pola radnog vremena {1} +Difference Amount (Company Currency),Razlika u iznosu (Valuta) +Add Serial No,Dodaj serijski broj, +Company and Accounts,Preduzeće i računi, +Current Address Is,Trenutna adresa je, +Unallocated Amount,Nepovezani iznos, +Show zero values,Prikaži vrijednosti sa nulom, +Address and Contact,Adresa i kontakt, +Supplier-Wise Sales Analytics,Analiza Dobavljačeve pametne prodaje, +Payment Entry is already created,Uplata je već kreirana, +Item,Artikal, +Unpaid,Neplaćen, +Net Rate,Neto cijena sa rabatom, +Project User,Projektni user, +Customer Items,Proizvodi kupca, +Item {0} is cancelled,Stavka {0} je otkazana, +Balance Value,Stanje vrijed. +Sales Order required for Item {0},Prodajni nalog je obavezan za artikal {0} +Patient,Pacijent, +Default Target Warehouse,Prijemno skladište, +Voucher No,Br. dokumenta, +Attendance has been marked successfully.,Prisustvo je uspješno obilježeno. +Serial No {0} created,Serijski broj {0} kreiran, +Asset,Osnovna sredstva, +Received Amount,Iznos uplate, +You cannot edit root node.,Не можете уређивати коренски чвор. +Sales Funnel,Prodajni lijevak, +Payment Due Date,Datum dospijeća fakture, +Consultation,Pregled, +Related,Povezan, +Warehouse Name,Naziv skladišta, +Customer / Item Name,Kupac / Naziv proizvoda, +Total Billed Amount,Ukupno fakturisano, +In Value,Prijem vrije. +Employees Email Id,ID email Zaposlenih, +Tree Type,Tip stabla, +Update Rate and Availability,Izmijenite cijenu i dostupnost, +Supplier Quotation,Ponuda dobavljača, +Quantity and Warehouse,Količina i skladište, +Taxes and Charges Added,Porezi i naknade dodate, +Warehouses,Skladišta, +All Customer Contact,Svi kontakti kupca, +Ledger,Skladišni karton, +Quotation Lost Reason,Razlog gubitka ponude, +Return Against Purchase Invoice,Povraćaj u vezi sa Fakturom nabavke, +Brand Name,Naziv brenda, +Stock,Zalihe, +Customer Group Name,Naziv grupe kupca, +Is Sales Item,Da li je prodajni artikal, +Invoiced Amount,Fakturisano, +Edit Posting Date and Time,Izmijeni datum i vrijeme dokumenta, +Inactive Customers,Neaktivni kupci, +Stock Entry Detail,Detalji unosa zaliha, +Accounting Details,Računovodstveni detalji, +Stock Manager,Menadžer zaliha, +As on Date,Na datum, +Is Cancelled,Je otkazan, +Setup Series,Podešavanje tipa dokumenta, +Point of Sale,Kasa, +Invoice No,Broj fakture, +Purchase Receipt Item,Stavka Prijema robe, +Invoices,Fakture, +Task Progress,% završenosti zadataka, +Employee Attendance Tool,Alat za prisustvo Zaposlenih, +Payment Days,Dana za plaćanje, +Recruitment,Zapošljavanje, +Taxes and Charges Calculation,Izračun Poreza, +For Employee,Za Zaposlenog, +Terms and Conditions Template,Uslovi i odredbe šablon, +Change,Kusur, +Stock Entry {0} created,Unos zaliha {0} je kreiran, +Search Item (Ctrl + i),Pretraga artikala (Ctrl + i) +View in Cart,Pogledajte u korpi, +Item Price updated for {0} in Price List {1},Cijena artikla je izmijenjena {0} u cjenovniku {1} +Discount,Popust, +Net Weight UOM,Neto težina JM, +Party Type,Tip partije, +Sales Order Required,Prodajni nalog je obavezan, +Search Item,Pretraži artikal, +Delivered Items To Be Billed,Nefakturisana isporučena roba, +Debit,Duguje, +Date TIme,Datum i vrijeme, +Payment Document,Dokument za plaćanje, +You can not enter current voucher in 'Against Journal Entry' column,"Неможете унети тренутни ваучер у колону ""На основу ставке у журналу""" +In Words (Company Currency),Riječima (valuta kompanije) +Purchase Receipt Trends,Trendovi prijema robe, +Employee Leave Approver,Odobreno odsustvo Zaposlenog, +Fiscal Year {0} does not exist,Fiskalna godina {0} ne postoji, +Accepted Warehouse,Prihvaćeno skladište, +Income Account,Račun prihoda, +Account Balance,Knjigovodstveno stanje, +'Expected Start Date' can not be greater than 'Expected End Date',Očekivani datum početka ne može biti veći od očekivanog datuma završetka, +Employee Emails,Elektronska pošta Zaposlenog, +Opening Qty,Početna količina, +Reorder level based on Warehouse,Nivo dopune u zavisnosti od skladišta, +To Warehouse,U skladište, +Is Group,Je grupa, +Contact Person,Kontakt osoba, +Item Code for Suppliers,Dobavljačeva šifra, +Return / Debit Note,Povraćaj / knjižno zaduženje, +Request for Quotation Supplier,Zahtjev za ponudu dobavljača, +LeaderBoard,Tabla, +Lab Test Groups,Labaratorijske grupe, +Training Result Employee,Rezultati obuke Zaposlenih, +Invoice Details,Detalji fakture, +Banking and Payments,Bakarstvo i plaćanja, +Employee Name,Ime Zaposlenog, +Active Leads / Customers,Активни Леадс / Kupci, +Accounting,Računovodstvo, +Party Name,Ime partije, +Manufacture,Proizvodnja, +New task,Novi zadatak, +Accounts Payable,Obaveze prema dobavljačima, +Shipping Address,Adresa isporuke, +Outstanding Amount,Preostalo za uplatu, +New Warehouse Name,Naziv novog skladišta, +Billed Amount,Fakturisani iznos, +Balance Qty,Stanje zalihe, +Item Shortage Report,Izvještaj o negativnim zalihama, +Transaction reference no {0} dated {1},Broj izvoda {0} na datum {1} +Make Sales Order,Kreiraj prodajni nalog, +Items,Artikli, +Employees working on a holiday,Zaposleni koji rade za vrijeme praznika, +Allocate Payment Amount,Poveži uplaćeni iznos, +Patient ID,ID pacijenta, +Printed On,Datum i vrijeme štampe, +Debit To,Zaduženje za, +Global Settings,Globalna podešavanja, +Make Employee,Keriraj Zaposlenog, +Atleast one warehouse is mandatory,Minimum jedno skladište je obavezno, +Price List Name,Naziv cjenovnika, +Journal Entry for Scrap,Knjiženje rastura i loma, +Website Warehouse,Skladište web sajta, +Customer's Item Code,Šifra kupca, +Supplier,Dobavljači, +Additional Discount Amount,Iznos dodatnog popusta, +Project Start Date,Datum početka projekta, +Student,Student, +Suplier Name,Naziv dobavljača, +In Qty,Prijem količine, +Selling Rate,Prodajna cijena, +Import Successful!,Uvoz uspješan! +Stock cannot be updated against Delivery Note {0},Zaliha se ne može promijeniti jer je vezana sa otpremnicom {0} +You are in offline mode. You will not be able to reload until you have network.,Радите без интернета. Нећете моћи да учитате страницу док се не повежете. +Form View,Prikaži kao formu, +Shortage Qty,Manjak kol. +Hour,Sat, +Item Group Tree,Stablo vrste artikala, +Update Stock,Ažuriraj zalihu, +Target Warehouse,Ciljno skladište, +Delivery Note Trends,Trendovi Otpremnica, +Default Source Warehouse,Izdajno skladište, +"{0}: Employee email not found, hence email not sent","{0}: Email zaposlenog nije pronađena, stoga email nije poslat" +All Warehouses,Sva skladišta, +Difference Amount,Razlika u iznosu, +User Remark,Korisnička napomena, +Quotation Message,Ponuda - poruka, +% Received,% Primljeno, +Stock Entry,Unos zaliha, +Sales Price List,Prodajni cjenovnik, +Avg. Selling Rate,Prosječna prodajna cijena, +End of Life,Kraj proizvodnje, +Payment Type,Vrsta plaćanja, +Default Customer Group,Podrazumijevana grupa kupaca, +Party,Partija, +Total Stock Summary,Ukupan pregled zalihe, +Net Total (Company Currency),Ukupno bez PDV-a (Valuta preduzeća) +Patient Name,Ime pacijenta, +Write Off,Otpisati, +Delivery Note Message,Poruka na otpremnici, +"Cannot delete Serial No {0}, as it is used in stock transactions","Ne može se obrisati serijski broj {0}, dok god se nalazi u dijelu Promjene na zalihama" +Delivery Note {0} is not submitted,Otpremnica {0} nije potvrđena, +New Employee,Novi Zaposleni, +Customers in Queue,Kupci na čekanju, +Price List Currency,Valuta Cjenovnika, +Applicable To (Employee),Primjenljivo na (zaposlene) +Project Manager,Projektni menadzer, +Accounts Receivable,Potraživanja od kupaca, +Rate,Cijena sa popustom, +View Task,Pogledaj zadatak, +Employee Education,Obrazovanje Zaposlenih, +Expense,Rashod, +Newsletters,Newsletter-i, +Select Supplier Address,Izaberite adresu dobavljača, +Price List {0} is disabled or does not exist,Cjenovnik {0} je zaključan ili ne postoji, +Billing Address Name,Naziv adrese za naplatu, +Add Item,Dodaj stavku, +All Customer Groups,Sve grupe kupca, +Employee Birthday,Rođendan Zaposlenih, +Total Billed Amount (via Sales Invoice),Ukupno fakturisano (putem fakture prodaje), +Weight UOM,JM Težina, +Stock Qty,Zaliha, +Return Against Delivery Note,Povraćaj u vezi sa otpremnicom, +Ageing Range 1,Opseg dospijeća 1, +Incoming Rate,Nabavna cijena, +Timesheets,Potrošnja vremena, +Attendance From Date,Datum početka prisustva, +Stock Items,Artikli na zalihama, +New Cart,Nova korpa, +Opening Value,Početna vrijednost, +"Setting Events to {0}, since the Employee attached to the below Sales Persons does not have a User ID{1}","Podešavanje stanja na {0}, pošto Zaposleni koji se priključio Prodavcima nema koririsnički ID {1}" +Import Attendance,Uvoz prisustva, +Analytics,Analitika, +Bank Balance,Stanje na računu, +Employee Number,Broj Zaposlenog, +Rate and Amount,Cijena i vrijednost sa popustom, +'Total','Ukupno bez PDV-a' +Total Taxes and Charges,Porez, +No active or default Salary Structure found for employee {0} for the given dates,Nisu pronađene aktivne ili podrazumjevane strukture plate za Zaposlenog {0} za dati period, +Supplier Part Number,Dobavljačeva šifra, +Project Task,Projektni zadatak, +Parent Item Group,Nadređena Vrsta artikala, +Mark Attendance,Označi prisustvo, +{0} created,Kreirao je korisnik {0} +Advance Paid,Avansno plačanje, +Projected,Projektovana količina na zalihama, +Reorder Level,Nivo dopune, +Customer / Lead Address,Kupac / Adresa lead-a, +Default Buying Price List,Podrazumijevani Cjenovnik, +Qty,Kol, +General,Opšte, +Default Payable Accounts,Podrazumijevani nalog za plaćanje, +Rate: {0},Cijena: {0} +Write Off Amount,Zaokruženi iznos, +Total Outstanding Amount,Preostalo za plaćanje, +Not Paid and Not Delivered,Nije plaćeno i nije isporučeno, +Planned,Planirano, +Total Amount,Ukupan iznos, +Please select Price List,Izaberite cjenovnik, +Item Serial No,Seriski broj artikla, +Customer Service,Usluga kupca, +Working,U toku, +Stock User,Korisnik zaliha, +General Ledger,Glavna knjiga, +Received Date,Datum prijema, +Project master.,Projektni master, +Valid From,Važi od, +Purchase Order Trends,Trendovi kupovina, +In Words will be visible once you save the Quotation.,Sačuvajte Predračun da bi Ispis slovima bio vidljiv, +Projected Qty,Projektovana količina, +Customer Addresses And Contacts,Kontakt i adresa kupca, +Employee name and designation in print,Ime i pozicija Zaposlenog, +For Warehouse,Za skladište, +Purchase Price List,Nabavni cjenovnik, +Accounts Payable Summary,Pregled obaveze prema dobavljačima, +Delivery Notes {0} must be cancelled before cancelling this Sales Order,Otpremnice {0} moraju biti otkazane prije otkazivanja prodajnog naloga, +Total Payment,Ukupno plaćeno, +POS Settings,POS podešavanja, +Buying Amount,Iznos nabavke, +Valuation Rate,Prosječna nab. cijena, +Project Id,ID Projekta, +Invoice Copy,Kopija Fakture, +You have been invited to collaborate on the project: {0},Позвани сте да сарађујете на пројекту: {0} +Purchase Order,Porudžbenica, +Rate With Margin,Cijena sa popustom, +"Search by item code, serial number, batch no or barcode","Pretraga po šifri, serijskom br. ili bar kodu" +Voucher Type,Vrsta dokumenta, +Serial No {0} has already been received,Serijski broj {0} je već primljen, +Data Import and Export,Uvoz i izvoz podataka, +Total advance ({0}) against Order {1} cannot be greater than the Grand Total ({2}),Ukupan avns({0}) na porudžbini {1} ne može biti veći od Ukupnog iznosa ({2}) +% Ordered,% Poručenog, +Price List not selected,Cjenovnik nije odabran, +Apply Discount On,Primijeni popust na, +Total Projected Qty,Ukupna projektovana količina, +Shipping Rule Condition,Uslovi pravila nabavke, +Opening Stock Balance,Početno stanje zalihe, +Customer Credit Balance,Kreditni limit kupca, +No address added yet.,Adresa još nije dodata. +Net Total,Ukupno bez PDV-a, +Total Qty,Ukupna kol. +Return,Povraćaj, +Delivery Warehouse,Skladište dostave, +Total (Company Currency),Ukupno bez PDV-a (Valuta) +Change Amount,Kusur, +Opportunity,Prilika, +Fully Delivered,Kompletno isporučeno, +Leave blank if considered for all employee types,Ostavite prazno ako se podrazumijeva za sve tipove Zaposlenih, +Disc,Popust, +Default Price List,Podrazumijevani cjenovnik, +Journal Entry,Knjiženje, +Apply Additional Discount On,Primijeni dodatni popust na, +This is based on transactions against this Supplier. See timeline below for details,Ovo je zasnovano na transkcijama ovog dobavljača. Pogledajte vremensku liniju ispod za dodatne informacije, +90-Above,Iznad 90 dana, +You have already assessed for the assessment criteria {}.,Већ сте оценили за критеријум оцењивања {}. +Serial Numbers in row {0} does not match with Delivery Note,Serijski broj na poziciji {0} se ne poklapa sa otpremnicom, +New Contact,Novi kontakt, +Returns,Povraćaj, +Delivery To,Isporuka za, +Project Value,Vrijednost Projekta, +Parent Warehouse,Nadređeno skladište, +Make Sales Invoice,Kreiraj fakturu prodaje, +Del,Obriši, +Select Warehouse...,Izaberite skladište... +Invoice/Journal Entry Details,Faktura / Detalji knjiženja, +Projected Quantity as Source,Projektovana izvorna količina, +Manufacturing User,Korisnik u proizvodnji, +Create Users,Kreiraj korisnike, +Price,Cijena, +Out Qty,Izdavanje Kol. +Employee,Zaposleni, +Project activity / task.,Projektna aktivnost / zadatak, +Reserved Warehouse in Sales Order / Finished Goods Warehouse,Rezervisano skladište u Prodajnom nalogu / Skladište gotovog proizvoda, +Physician,Ljekar, +Quantity,Količina, +Purchase Receipt Required,Prijem robe je obavezan, +Currency is required for Price List {0},Valuta je obavezna za Cjenovnik {0} +Out Value,Izdavanje vrije. +Customer Group,Grupa kupaca, +You are not authorized to add or update entries before {0},Немате дозволу да додајете или ажурирате ставке пре {0} +Warehouse can only be changed via Stock Entry / Delivery Note / Purchase Receipt,Skladište se jedino može promijeniti u dijelu Unos zaliha / Otpremnica / Prijem robe, +Request for Quotations,Zahtjev za ponude, +Learn,Naučite, +Employee Detail,Detalji o Zaposlenom, +Ignore Pricing Rule,Zanemari pravilnik o cijenama, +Additional Discount,Dodatni popust, +Cheque/Reference No,Broj izvoda, +Attendance date can not be less than employee's joining date,Datum prisustva ne može biti raniji od datuma ulaska zaposlenog, +Box,Kutija, +Total Allocated Amount,Ukupno povezani iznos, +All Addresses.,Sve adrese, +Opening Balances,Početna stanja, +Users and Permissions,Korisnici i dozvole, +"You can only plan for upto {0} vacancies and budget {1} \ +>>>>>>> 4f473eb090 (fix: typo in loyalty program throw message (#36432)) for {2} as per staffing plan {3} for parent company {4}.",Можете планирати до {0} слободна места и буџетирати {1} \ за {2} по плану особља {3} за матичну компанију {4}. apps/erpnext/erpnext/public/js/event.js +19,Add Customers,Dodaj kupce DocType: Employee External Work History,Employee External Work History,Istorijat o radu van preduzeća za Zaposlenog diff --git a/erpnext/translations/sr.csv b/erpnext/translations/sr.csv index f6cbb57d201d..9558e07797b1 100644 --- a/erpnext/translations/sr.csv +++ b/erpnext/translations/sr.csv @@ -3338,7 +3338,7 @@ You cannot delete Fiscal Year {0}. Fiscal Year {0} is set as default in Global S You cannot delete Project Type 'External',Не можете обрисати тип пројекта 'Спољни', You cannot edit root node.,Не можете уређивати роот чвор., You cannot restart a Subscription that is not cancelled.,Не можете поново покренути претплату која није отказана., -You don't have enought Loyalty Points to redeem,Не искористите Лоиалти Поинтс за откуп, +You don't have enough Loyalty Points to redeem,Не искористите Лоиалти Поинтс за откуп, You have already assessed for the assessment criteria {}.,Већ сте оцијенили за критеријуми за оцењивање {}., You have already selected items from {0} {1},Који сте изабрали ставке из {0} {1}, You have been invited to collaborate on the project: {0},Позвани сте да сарађују на пројекту: {0}, diff --git a/erpnext/translations/sr_sp.csv b/erpnext/translations/sr_sp.csv index 5e7ae79781a7..7c9ed6de3ad4 100644 --- a/erpnext/translations/sr_sp.csv +++ b/erpnext/translations/sr_sp.csv @@ -545,7 +545,7 @@ You cannot credit and debit same account at the same time,Не можете кр You cannot delete Fiscal Year {0}. Fiscal Year {0} is set as default in Global Settings,Ne možete obrisati fiskalnu godinu {0}. Fiskalna {0} godina je označena kao trenutna u globalnim podešavanjima., You cannot delete Project Type 'External',"Не можете обрисати ""Спољни"" тип пројекта.", You cannot edit root node.,Не можете уређивати коренски чвор., -You don't have enought Loyalty Points to redeem,Немате довољно Бодова Лојалности., +You don't have enough Loyalty Points to redeem,Немате довољно Бодова Лојалности., You have already assessed for the assessment criteria {}.,Већ сте оценили за критеријум оцењивања {}., You have already selected items from {0} {1},Већ сте изабрали ставке из {0} {1}, You have been invited to collaborate on the project: {0},Позвани сте да сарађујете на пројекту: {0}, diff --git a/erpnext/translations/sv.csv b/erpnext/translations/sv.csv index ec443e90be65..7d9b70024cd6 100644 --- a/erpnext/translations/sv.csv +++ b/erpnext/translations/sv.csv @@ -3338,7 +3338,7 @@ You cannot delete Fiscal Year {0}. Fiscal Year {0} is set as default in Global S You cannot delete Project Type 'External',Du kan inte ta bort Project Type 'External', You cannot edit root node.,Du kan inte redigera rotknutpunkt., You cannot restart a Subscription that is not cancelled.,Du kan inte starta om en prenumeration som inte avbryts., -You don't have enought Loyalty Points to redeem,Du har inte tillräckligt med lojalitetspoäng för att lösa in, +You don't have enough Loyalty Points to redeem,Du har inte tillräckligt med lojalitetspoäng för att lösa in, You have already assessed for the assessment criteria {}.,Du har redan bedömt för bedömningskriterierna {}., You have already selected items from {0} {1},Du har redan valt objekt från {0} {1}, You have been invited to collaborate on the project: {0},Du har blivit inbjuden att samarbeta i projektet: {0}, diff --git a/erpnext/translations/sw.csv b/erpnext/translations/sw.csv index 989f8b06c88c..6a0ef8cc7c1e 100644 --- a/erpnext/translations/sw.csv +++ b/erpnext/translations/sw.csv @@ -3338,7 +3338,7 @@ You cannot delete Fiscal Year {0}. Fiscal Year {0} is set as default in Global S You cannot delete Project Type 'External',Huwezi kufuta Aina ya Mradi 'Nje', You cannot edit root node.,Huwezi kubadilisha node ya mizizi., You cannot restart a Subscription that is not cancelled.,Huwezi kuanzisha upya Usajili ambao haujahairiwa., -You don't have enought Loyalty Points to redeem,Huna ushawishi wa Pole ya Uaminifu ili ukomboe, +You don't have enough Loyalty Points to redeem,Huna ushawishi wa Pole ya Uaminifu ili ukomboe, You have already assessed for the assessment criteria {}.,Tayari umehakikishia vigezo vya tathmini {}., You have already selected items from {0} {1},Tayari umechagua vitu kutoka {0} {1}, You have been invited to collaborate on the project: {0},Umealikwa kushirikiana kwenye mradi: {0}, diff --git a/erpnext/translations/ta.csv b/erpnext/translations/ta.csv index 7314d4be513d..35cdc92b374b 100644 --- a/erpnext/translations/ta.csv +++ b/erpnext/translations/ta.csv @@ -3338,7 +3338,7 @@ You cannot delete Fiscal Year {0}. Fiscal Year {0} is set as default in Global S You cannot delete Project Type 'External',நீங்கள் திட்டம் வகை 'வெளிப்புற' நீக்க முடியாது, You cannot edit root node.,ரூட் முனையை நீங்கள் திருத்த முடியாது., You cannot restart a Subscription that is not cancelled.,ரத்துசெய்யப்படாத சந்தாவை மறுதொடக்கம் செய்ய முடியாது., -You don't have enought Loyalty Points to redeem,நீங்கள் மீட்கும் விசுவாச புள்ளிகளைப் பெறுவீர்கள், +You don't have enough Loyalty Points to redeem,நீங்கள் மீட்கும் விசுவாச புள்ளிகளைப் பெறுவீர்கள், You have already assessed for the assessment criteria {}.,ஏற்கனவே மதிப்பீட்டிற்குத் தகுதி மதிப்பீடு செய்யப்பட்டதன் {}., You have already selected items from {0} {1},நீங்கள் ஏற்கனவே இருந்து பொருட்களை தேர்ந்தெடுத்த {0} {1}, You have been invited to collaborate on the project: {0},நீங்கள் திட்டம் இணைந்து அழைக்கப்பட்டுள்ளனர்: {0}, diff --git a/erpnext/translations/te.csv b/erpnext/translations/te.csv index 3fb32dc093df..9b8b2d7d1ae1 100644 --- a/erpnext/translations/te.csv +++ b/erpnext/translations/te.csv @@ -3338,7 +3338,7 @@ You cannot delete Fiscal Year {0}. Fiscal Year {0} is set as default in Global S You cannot delete Project Type 'External',మీరు ప్రాజెక్ట్ రకం 'బాహ్య' తొలగించలేరు, You cannot edit root node.,మీరు రూట్ నోడ్ను సవరించలేరు., You cannot restart a Subscription that is not cancelled.,మీరు రద్దు చేయని సభ్యత్వాన్ని పునఃప్రారంభించలేరు., -You don't have enought Loyalty Points to redeem,మీరు విమోచన చేయడానికి లాయల్టీ పాయింట్స్ను కలిగి ఉండరు, +You don't have enough Loyalty Points to redeem,మీరు విమోచన చేయడానికి లాయల్టీ పాయింట్స్ను కలిగి ఉండరు, You have already assessed for the assessment criteria {}.,మీరు ఇప్పటికే అంచనా ప్రమాణం కోసం అంచనా {}., You have already selected items from {0} {1},మీరు ఇప్పటికే ఎంపిక నుండి అంశాలను రోజులో {0} {1}, You have been invited to collaborate on the project: {0},మీరు ప్రాజెక్ట్ సహకరించడానికి ఆహ్వానించబడ్డారు: {0}, diff --git a/erpnext/translations/th.csv b/erpnext/translations/th.csv index e371aed72844..a97be2b8bf06 100644 --- a/erpnext/translations/th.csv +++ b/erpnext/translations/th.csv @@ -3338,7 +3338,7 @@ You cannot delete Fiscal Year {0}. Fiscal Year {0} is set as default in Global S You cannot delete Project Type 'External',คุณไม่สามารถลบประเภทโครงการ 'ภายนอก', You cannot edit root node.,คุณไม่สามารถแก้ไขโหนดรากได้, You cannot restart a Subscription that is not cancelled.,คุณไม่สามารถรีสตาร์ทการสมัครสมาชิกที่ไม่ได้ยกเลิกได้, -You don't have enought Loyalty Points to redeem,คุณไม่มีจุดภักดีเพียงพอที่จะไถ่ถอน, +You don't have enough Loyalty Points to redeem,คุณไม่มีจุดภักดีเพียงพอที่จะไถ่ถอน, You have already assessed for the assessment criteria {}.,คุณได้รับการประเมินเกณฑ์การประเมินแล้ว {}, You have already selected items from {0} {1},คุณได้เลือกแล้วรายการจาก {0} {1}, You have been invited to collaborate on the project: {0},คุณได้รับเชิญที่จะทำงานร่วมกันในโครงการ: {0}, diff --git a/erpnext/translations/tr.csv b/erpnext/translations/tr.csv index 66ea69a1b5b5..3f67c6743827 100644 --- a/erpnext/translations/tr.csv +++ b/erpnext/translations/tr.csv @@ -3338,7 +3338,11 @@ You cannot delete Fiscal Year {0}. Fiscal Year {0} is set as default in Global S You cannot delete Project Type 'External','Dış' Proje Türünü silemezsiniz., You cannot edit root node.,Kök düğümünü düzenleyemezsiniz., You cannot restart a Subscription that is not cancelled.,İptal edilmeyen bir Aboneliği başlatamazsınız., +<<<<<<< HEAD You don't have enought Loyalty Points to redeem,Kullanılması gereken sadakat puanlarına sahip değilsiniz, +======= +You don't have enough Loyalty Points to redeem,Kullanılması gereken sadakat puanlarına sahip olabilirsiniz, +>>>>>>> 4f473eb090 (fix: typo in loyalty program throw message (#36432)) You have already assessed for the assessment criteria {}.,Zaten değerlendirme kriteri {} için değerlendirdiniz., You have already selected items from {0} {1},Zaten öğeleri seçtiniz {0} {1}, You have been invited to collaborate on the project: {0},{0} projesine katkıda bulunmak için davet edildiniz, diff --git a/erpnext/translations/uk.csv b/erpnext/translations/uk.csv index 83c8d41cb70f..d25b242c7d2c 100644 --- a/erpnext/translations/uk.csv +++ b/erpnext/translations/uk.csv @@ -3338,7 +3338,7 @@ You cannot delete Fiscal Year {0}. Fiscal Year {0} is set as default in Global S You cannot delete Project Type 'External',Ви не можете видалити тип проекту "Зовнішній", You cannot edit root node.,Ви не можете редагувати кореневий вузол., You cannot restart a Subscription that is not cancelled.,"Ви не можете перезапустити підписку, яку не скасовано.", -You don't have enought Loyalty Points to redeem,"Ви не маєте впевнених точок лояльності, щоб викупити", +You don't have enough Loyalty Points to redeem,"Ви не маєте впевнених точок лояльності, щоб викупити", You have already assessed for the assessment criteria {}.,Ви вже оцінили за критеріями оцінки {}., You have already selected items from {0} {1},Ви вже вибрали елементи з {0} {1}, You have been invited to collaborate on the project: {0},Ви були запрошені для спільної роботи над проектом: {0}, diff --git a/erpnext/translations/ur.csv b/erpnext/translations/ur.csv index 8cf0707e3656..eb26f7cee278 100644 --- a/erpnext/translations/ur.csv +++ b/erpnext/translations/ur.csv @@ -3338,7 +3338,7 @@ You cannot delete Fiscal Year {0}. Fiscal Year {0} is set as default in Global S You cannot delete Project Type 'External',آپ پراجیکٹ کی قسم کو خارج نہیں کرسکتے ہیں 'بیرونی', You cannot edit root node.,آپ جڑ نوڈ میں ترمیم نہیں کر سکتے ہیں., You cannot restart a Subscription that is not cancelled.,آپ ایک سبسکرپشن کو دوبارہ شروع نہیں کرسکتے جو منسوخ نہیں ہوسکتا., -You don't have enought Loyalty Points to redeem,آپ کو بہت زیادہ وفادار پوائنٹس حاصل کرنے کے لئے نہیں ہے, +You don't have enough Loyalty Points to redeem,آپ کو بہت زیادہ وفادار پوائنٹس حاصل کرنے کے لئے نہیں ہے, You have already assessed for the assessment criteria {}.,آپ نے پہلے ہی تشخیص کے معیار کے تعین کی ہے {}., You have already selected items from {0} {1},آپ نے پہلے ہی سے اشیاء کو منتخب کیا ہے {0} {1}, You have been invited to collaborate on the project: {0},آپ کو منصوبے پر تعاون کرنے کیلئے مدعو کیا گیا ہے: {0}, diff --git a/erpnext/translations/uz.csv b/erpnext/translations/uz.csv index 1e503769cbd2..95141bdb993f 100644 --- a/erpnext/translations/uz.csv +++ b/erpnext/translations/uz.csv @@ -3338,7 +3338,7 @@ You cannot delete Fiscal Year {0}. Fiscal Year {0} is set as default in Global S You cannot delete Project Type 'External',Siz "Tashqi" loyiha turini o'chira olmaysiz, You cannot edit root node.,Ildiz tugunni tahrirlay olmaysiz., You cannot restart a Subscription that is not cancelled.,Bekor qilinmagan obunani qayta boshlash mumkin emas., -You don't have enought Loyalty Points to redeem,Siz sotib olish uchun sodiqlik nuqtalari yo'q, +You don't have enough Loyalty Points to redeem,Siz sotib olish uchun sodiqlik nuqtalari yo'q, You have already assessed for the assessment criteria {}.,Siz allaqachon baholash mezonlari uchun baholagansiz {}., You have already selected items from {0} {1},{0} {1} dan tanlangan elementlarni tanladingiz, You have been invited to collaborate on the project: {0},Siz loyihada hamkorlik qilish uchun taklif qilingan: {0}, diff --git a/erpnext/translations/vi.csv b/erpnext/translations/vi.csv index 5c673afc77a3..de35891698dd 100644 --- a/erpnext/translations/vi.csv +++ b/erpnext/translations/vi.csv @@ -3338,7 +3338,7 @@ You cannot delete Fiscal Year {0}. Fiscal Year {0} is set as default in Global S You cannot delete Project Type 'External',Bạn không thể xóa Loại dự án 'Bên ngoài', You cannot edit root node.,Bạn không thể chỉnh sửa nút gốc., You cannot restart a Subscription that is not cancelled.,Bạn không thể khởi động lại Đăng ký không bị hủy., -You don't have enought Loyalty Points to redeem,Bạn không có Điểm trung thành đủ để đổi, +You don't have enough Loyalty Points to redeem,Bạn không có Điểm trung thành đủ để đổi, You have already assessed for the assessment criteria {}.,Bạn đã đánh giá các tiêu chí đánh giá {}., You have already selected items from {0} {1},Bạn đã chọn các mục từ {0} {1}, You have been invited to collaborate on the project: {0},Bạn được lời mời cộng tác trong dự án: {0}, diff --git a/erpnext/translations/zh.csv b/erpnext/translations/zh.csv index 9ed7420f4630..e2e6899ed5e2 100644 --- a/erpnext/translations/zh.csv +++ b/erpnext/translations/zh.csv @@ -3338,7 +3338,7 @@ You cannot delete Fiscal Year {0}. Fiscal Year {0} is set as default in Global S You cannot delete Project Type 'External',您不能删除“外部”类型的项目, You cannot edit root node.,您不能编辑根节点。, You cannot restart a Subscription that is not cancelled.,您无法重新启动未取消的订阅。, -You don't have enought Loyalty Points to redeem,您没有获得忠诚度积分兑换, +You don't have enough Loyalty Points to redeem,您没有获得忠诚度积分兑换, You have already assessed for the assessment criteria {}.,您已经评估了评估标准{}。, You have already selected items from {0} {1},您已经选择从项目{0} {1}, You have been invited to collaborate on the project: {0},您已被邀请在项目上进行合作:{0}, diff --git a/erpnext/translations/zh_tw.csv b/erpnext/translations/zh_tw.csv index ef4967056773..9fd737bed29f 100644 --- a/erpnext/translations/zh_tw.csv +++ b/erpnext/translations/zh_tw.csv @@ -3127,7 +3127,7 @@ You cannot delete Fiscal Year {0}. Fiscal Year {0} is set as default in Global S You cannot delete Project Type 'External',您不能刪除項目類型“外部”, You cannot edit root node.,您不能編輯根節點。, You cannot restart a Subscription that is not cancelled.,您無法重新啟動未取消的訂閱。, -You don't have enought Loyalty Points to redeem,您沒有獲得忠誠度積分兌換, +You don't have enough Loyalty Points to redeem,您沒有獲得忠誠度積分兌換, You have already assessed for the assessment criteria {}.,您已經評估了評估標準{}。, You have already selected items from {0} {1},您已經選擇從項目{0} {1}, You have been invited to collaborate on the project: {0},您已被邀請在項目上進行合作:{0}, From 04f99150098c033fe2f27ec92c789bae9209be0e Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Tue, 1 Aug 2023 11:20:26 +0530 Subject: [PATCH 034/501] perf: avoid full table scan in sle count check (#36428) perf: avoid full table scan in sle count check (#36428) (cherry picked from commit f31d07554d05f5b325d8770b90e70e9ee214844b) Co-authored-by: Ankush Menat --- .../batch_wise_balance_history/batch_wise_balance_history.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/stock/report/batch_wise_balance_history/batch_wise_balance_history.py b/erpnext/stock/report/batch_wise_balance_history/batch_wise_balance_history.py index 0f319554763a..b38dba8bb17c 100644 --- a/erpnext/stock/report/batch_wise_balance_history/batch_wise_balance_history.py +++ b/erpnext/stock/report/batch_wise_balance_history/batch_wise_balance_history.py @@ -16,7 +16,7 @@ def execute(filters=None): if not filters: filters = {} - sle_count = frappe.db.count("Stock Ledger Entry", {"is_cancelled": 0}) + sle_count = frappe.db.count("Stock Ledger Entry") if sle_count > SLE_COUNT_LIMIT and not filters.get("item_code") and not filters.get("warehouse"): frappe.throw(_("Please select either the Item or Warehouse filter to generate the report.")) From 43b85c571101d3d8d5d2f5a9f041ba176ceb243e Mon Sep 17 00:00:00 2001 From: Anand Baburajan Date: Tue, 1 Aug 2023 11:47:48 +0530 Subject: [PATCH 035/501] fix: allow fully depreciated existing assets (#36378) --- erpnext/assets/doctype/asset/asset.json | 21 +++++++++++++------- erpnext/assets/doctype/asset/asset.py | 13 ++++++++---- erpnext/assets/doctype/asset/depreciation.py | 9 +++++++++ 3 files changed, 32 insertions(+), 11 deletions(-) diff --git a/erpnext/assets/doctype/asset/asset.json b/erpnext/assets/doctype/asset/asset.json index 3e93f0f03e3c..78cbe8621faf 100644 --- a/erpnext/assets/doctype/asset/asset.json +++ b/erpnext/assets/doctype/asset/asset.json @@ -43,6 +43,7 @@ "column_break_33", "opening_accumulated_depreciation", "number_of_depreciations_booked", + "is_fully_depreciated", "section_break_36", "finance_books", "section_break_33", @@ -205,6 +206,7 @@ "fieldname": "disposal_date", "fieldtype": "Date", "label": "Disposal Date", + "no_copy": 1, "read_only": 1 }, { @@ -244,19 +246,17 @@ "label": "Is Existing Asset" }, { - "depends_on": "is_existing_asset", + "depends_on": "eval:(doc.is_existing_asset)", "fieldname": "opening_accumulated_depreciation", "fieldtype": "Currency", "label": "Opening Accumulated Depreciation", - "no_copy": 1, "options": "Company:company:default_currency" }, { - "depends_on": "eval:(doc.is_existing_asset && doc.opening_accumulated_depreciation)", + "depends_on": "eval:(doc.is_existing_asset)", "fieldname": "number_of_depreciations_booked", "fieldtype": "Int", - "label": "Number of Depreciations Booked", - "no_copy": 1 + "label": "Number of Depreciations Booked" }, { "collapsible": 1, @@ -502,6 +502,13 @@ "options": "\nSuccessful\nFailed", "print_hide": 1, "read_only": 1 + }, + { + "default": "0", + "depends_on": "eval:(doc.is_existing_asset)", + "fieldname": "is_fully_depreciated", + "fieldtype": "Check", + "label": "Is Fully Depreciated" } ], "idx": 72, @@ -530,7 +537,7 @@ "table_fieldname": "accounts" } ], - "modified": "2023-03-30 15:07:41.542374", + "modified": "2023-07-28 15:47:01.137996", "modified_by": "Administrator", "module": "Assets", "name": "Asset", @@ -574,4 +581,4 @@ "states": [], "title_field": "asset_name", "track_changes": 1 -} +} \ No newline at end of file diff --git a/erpnext/assets/doctype/asset/asset.py b/erpnext/assets/doctype/asset/asset.py index c6247ea0da3f..b8d8ba5b4865 100644 --- a/erpnext/assets/doctype/asset/asset.py +++ b/erpnext/assets/doctype/asset/asset.py @@ -207,8 +207,11 @@ def validate_asset_values(self): if not self.calculate_depreciation: return - elif not self.finance_books: - frappe.throw(_("Enter depreciation details")) + else: + if not self.finance_books: + frappe.throw(_("Enter depreciation details")) + if self.is_fully_depreciated: + frappe.throw(_("Depreciation cannot be calculated for fully depreciated assets")) if self.is_existing_asset: return @@ -588,7 +591,7 @@ def validate_asset_finance_books(self, row): depreciable_amount = flt(self.gross_purchase_amount) - flt(row.expected_value_after_useful_life) if flt(self.opening_accumulated_depreciation) > depreciable_amount: frappe.throw( - _("Opening Accumulated Depreciation must be less than equal to {0}").format( + _("Opening Accumulated Depreciation must be less than or equal to {0}").format( depreciable_amount ) ) @@ -793,7 +796,9 @@ def get_status(self): expected_value_after_useful_life = self.finance_books[idx].expected_value_after_useful_life value_after_depreciation = self.finance_books[idx].value_after_depreciation - if flt(value_after_depreciation) <= expected_value_after_useful_life: + if ( + flt(value_after_depreciation) <= expected_value_after_useful_life or self.is_fully_depreciated + ): status = "Fully Depreciated" elif flt(value_after_depreciation) < flt(self.gross_purchase_amount): status = "Partially Depreciated" diff --git a/erpnext/assets/doctype/asset/depreciation.py b/erpnext/assets/doctype/asset/depreciation.py index d72c4bd654b6..80262c04d4a9 100644 --- a/erpnext/assets/doctype/asset/depreciation.py +++ b/erpnext/assets/doctype/asset/depreciation.py @@ -369,6 +369,15 @@ def reverse_depreciation_entry_made_after_disposal(asset, date): reverse_journal_entry = make_reverse_journal_entry(schedule.journal_entry) reverse_journal_entry.posting_date = nowdate() + + for account in reverse_journal_entry.accounts: + account.update( + { + "reference_type": "Asset", + "reference_name": asset.name, + } + ) + frappe.flags.is_reverse_depr_entry = True reverse_journal_entry.submit() From 80eb8754dbcfdc27b9ccc03c1a5581df161a2e66 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Thu, 13 Jul 2023 15:04:44 +0530 Subject: [PATCH 036/501] fix: incorrect `idx` on JE's after reconciliation (cherry picked from commit 72f577aad2dee7f691b58cded992a5d86d5efe02) --- erpnext/accounts/utils.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/erpnext/accounts/utils.py b/erpnext/accounts/utils.py index aa861a48eff3..5662e99c5e70 100644 --- a/erpnext/accounts/utils.py +++ b/erpnext/accounts/utils.py @@ -570,7 +570,11 @@ def update_reference_in_journal_entry(d, journal_entry, do_not_save=False): # new row with references new_row = journal_entry.append("accounts") - new_row.update((frappe.copy_doc(jv_detail)).as_dict()) + # Copy field values into new row + [ + new_row.set(field, jv_detail.get(field)) + for field in frappe.get_meta("Journal Entry Account").get_fieldnames_with_value() + ] new_row.set(d["dr_or_cr"], d["allocated_amount"]) new_row.set( From 09af485d549cd8873b81c7e7bb2b2a45b6c21735 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Tue, 1 Aug 2023 13:12:16 +0530 Subject: [PATCH 037/501] fix: overallocation validation misfire on normal invoices (#36349) * fix: overallocation validation misfire on normal invoices * test: assert misfire doesn't happen (cherry picked from commit ab933df5bbce6aa576b0762a221933734bb5f719) --- .../doctype/payment_entry/payment_entry.py | 16 ++++--- .../payment_entry/test_payment_entry.py | 46 +++++++++++++++++++ 2 files changed, 56 insertions(+), 6 deletions(-) diff --git a/erpnext/accounts/doctype/payment_entry/payment_entry.py b/erpnext/accounts/doctype/payment_entry/payment_entry.py index 83736bd68e91..b71e2235612a 100644 --- a/erpnext/accounts/doctype/payment_entry/payment_entry.py +++ b/erpnext/accounts/doctype/payment_entry/payment_entry.py @@ -234,12 +234,13 @@ def validate_allocated_amount_with_latest_data(self): fail_message = _("Row #{0}: Allocated Amount cannot be greater than outstanding amount.") - if (flt(d.allocated_amount)) > 0 and flt(d.allocated_amount) > flt(latest.outstanding_amount): - frappe.throw(fail_message.format(d.idx)) - - if d.payment_term and ( - (flt(d.allocated_amount)) > 0 - and flt(d.allocated_amount) > flt(latest.payment_term_outstanding) + if ( + d.payment_term + and ( + (flt(d.allocated_amount)) > 0 + and flt(d.allocated_amount) > flt(latest.payment_term_outstanding) + ) + and self.term_based_allocation_enabled_for_reference(d.reference_doctype, d.reference_name) ): frappe.throw( _( @@ -249,6 +250,9 @@ def validate_allocated_amount_with_latest_data(self): ) ) + if (flt(d.allocated_amount)) > 0 and flt(d.allocated_amount) > flt(latest.outstanding_amount): + frappe.throw(fail_message.format(d.idx)) + # Check for negative outstanding invoices as well if flt(d.allocated_amount) < 0 and flt(d.allocated_amount) < flt(latest.outstanding_amount): frappe.throw(fail_message.format(d.idx)) diff --git a/erpnext/accounts/doctype/payment_entry/test_payment_entry.py b/erpnext/accounts/doctype/payment_entry/test_payment_entry.py index 785b8a180b14..ca1d317c38ee 100644 --- a/erpnext/accounts/doctype/payment_entry/test_payment_entry.py +++ b/erpnext/accounts/doctype/payment_entry/test_payment_entry.py @@ -1156,6 +1156,52 @@ def test_overallocation_validation_on_payment_terms(self): si3.cancel() si3.delete() + @change_settings( + "Accounts Settings", + { + "unlink_payment_on_cancellation_of_invoice": 1, + "delete_linked_ledger_entries": 1, + "allow_multi_currency_invoices_against_single_party_account": 1, + }, + ) + def test_overallocation_validation_shouldnt_misfire(self): + """ + Overallocation validation shouldn't fire for Template without "Allocate Payment based on Payment Terms" enabled + + """ + customer = create_customer() + create_payment_terms_template() + + template = frappe.get_doc("Payment Terms Template", "Test Receivable Template") + template.allocate_payment_based_on_payment_terms = 0 + template.save() + + # Validate allocation on base/company currency + si = create_sales_invoice(do_not_save=1, qty=1, rate=200) + si.payment_terms_template = "Test Receivable Template" + si.save().submit() + + si.reload() + pe = get_payment_entry(si.doctype, si.name).save() + # There will no term based allocation + self.assertEqual(len(pe.references), 1) + self.assertEqual(pe.references[0].payment_term, None) + self.assertEqual(flt(pe.references[0].allocated_amount), flt(si.grand_total)) + pe.save() + + # specify a term + pe.references[0].payment_term = template.terms[0].payment_term + # no validation error should be thrown + pe.save() + + pe.paid_amount = si.grand_total + 1 + pe.references[0].allocated_amount = si.grand_total + 1 + self.assertRaises(frappe.ValidationError, pe.save) + + template = frappe.get_doc("Payment Terms Template", "Test Receivable Template") + template.allocate_payment_based_on_payment_terms = 1 + template.save() + def create_payment_entry(**args): payment_entry = frappe.new_doc("Payment Entry") From 14dc40360d8c81cb3774f3c182af91277bc4a91a Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Tue, 1 Aug 2023 13:43:28 +0530 Subject: [PATCH 038/501] chore: remove merge conflict in translation file Remove merge conflict in v14-hotfix branch --- erpnext/translations/tr.csv | 4 ---- 1 file changed, 4 deletions(-) diff --git a/erpnext/translations/tr.csv b/erpnext/translations/tr.csv index 3f67c6743827..14d842425e31 100644 --- a/erpnext/translations/tr.csv +++ b/erpnext/translations/tr.csv @@ -3338,11 +3338,7 @@ You cannot delete Fiscal Year {0}. Fiscal Year {0} is set as default in Global S You cannot delete Project Type 'External','Dış' Proje Türünü silemezsiniz., You cannot edit root node.,Kök düğümünü düzenleyemezsiniz., You cannot restart a Subscription that is not cancelled.,İptal edilmeyen bir Aboneliği başlatamazsınız., -<<<<<<< HEAD -You don't have enought Loyalty Points to redeem,Kullanılması gereken sadakat puanlarına sahip değilsiniz, -======= You don't have enough Loyalty Points to redeem,Kullanılması gereken sadakat puanlarına sahip olabilirsiniz, ->>>>>>> 4f473eb090 (fix: typo in loyalty program throw message (#36432)) You have already assessed for the assessment criteria {}.,Zaten değerlendirme kriteri {} için değerlendirdiniz., You have already selected items from {0} {1},Zaten öğeleri seçtiniz {0} {1}, You have been invited to collaborate on the project: {0},{0} projesine katkıda bulunmak için davet edildiniz, From 2dee97aa70d29193a9f6e285f1a5733e7b535c82 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Tue, 1 Aug 2023 14:18:14 +0530 Subject: [PATCH 039/501] chore: remove merge conflict remove merge conflict in sr-SP.csv --- erpnext/translations/sr-SP.csv | 642 --------------------------------- 1 file changed, 642 deletions(-) diff --git a/erpnext/translations/sr-SP.csv b/erpnext/translations/sr-SP.csv index 64c2f63e98e8..afeb651ac278 100644 --- a/erpnext/translations/sr-SP.csv +++ b/erpnext/translations/sr-SP.csv @@ -265,647 +265,6 @@ DocType: Supplier,Name and Type,Ime i tip DocType: Customs Tariff Number,Customs Tariff Number,Carinska tarifa apps/erpnext/erpnext/hr/doctype/employee_benefit_application/employee_benefit_application.py +65,"You can claim only an amount of {0}, the rest amount {1} should be in the application \ as pro-rata component","Можете тражити само износ од {0}, остатак од {1} би требао бити у апликацији \ као про-рата компонента." -<<<<<<< HEAD -DocType: Crop,Yield UOM,Јединица мере приноса -DocType: Item Default,Default Supplier,Podrazumijevani dobavljač -apps/erpnext/erpnext/healthcare/page/medical_record/patient_select.html +3,Select Patient,Izaberite pacijenta -apps/erpnext/erpnext/hr/report/employee_leave_balance/employee_leave_balance.py +27,Opening,Početno stanje -DocType: POS Profile,Customer Groups,Grupe kupaca -DocType: Hub Tracked Item,Item Manager,Menadžer artikala -DocType: Fiscal Year Company,Fiscal Year Company,Fiskalna godina preduzeća -DocType: Patient Appointment,Patient Appointment,Zakazivanje pacijenata -DocType: BOM,Show In Website,Prikaži na web sajtu -DocType: Payment Entry,Paid Amount,Uplaćeno -apps/erpnext/erpnext/accounts/report/accounts_receivable/accounts_receivable.html +137,Total Paid Amount,Ukupno plaćeno -apps/erpnext/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py +267,Purchase Receipt {0} is not submitted,Prijem robe {0} nije potvrđen -apps/erpnext/erpnext/config/selling.py +52,Items and Pricing,Proizvodi i cijene -DocType: Payment Entry,Account Paid From,Račun plaćen preko -apps/erpnext/erpnext/utilities/activation.py +72,Create customer quotes,Kreirajte bilješke kupca -DocType: Purchase Invoice,Supplier Warehouse,Skladište dobavljača -apps/erpnext/erpnext/support/doctype/warranty_claim/warranty_claim.py +20,Customer is required,Kupac je obavezan podatak -DocType: Item,Manufacturer,Proizvođač -apps/erpnext/erpnext/accounts/report/gross_profit/gross_profit.py +69,Selling Amount,Prodajni iznos -apps/erpnext/erpnext/hr/doctype/salary_slip/salary_slip.py +417,Please set the Date Of Joining for employee {0},Molimo podesite datum zasnivanja radnog odnosa {0} -DocType: Item,Allow over delivery or receipt upto this percent,Dozvolite isporukuili prijem robe ukoliko ne premaši ovaj procenat -DocType: Shopping Cart Settings,Orders,Porudžbine -apps/erpnext/erpnext/config/stock.py +7,Stock Transactions,Promjene na zalihama -apps/erpnext/erpnext/hr/doctype/leave_application/leave_application.py +183,You are not authorized to approve leaves on Block Dates,Немате дозволу да одобравате одсуства на Блок Датумима. -,Daily Timesheet Summary,Pregled dnevnog potrošenog vremena -DocType: Project Task,View Timesheet,Pogledaj potrošeno vrijeme -DocType: Purchase Invoice,Rounded Total (Company Currency),Zaokruženi ukupan iznos (valuta preduzeća) -apps/erpnext/erpnext/hr/doctype/salary_slip/salary_slip.py +403,Salary Slip of employee {0} already created for this period,Isplatna lista Zaposlenog {0} kreirana je već za ovaj period -DocType: Item,"If this item has variants, then it cannot be selected in sales orders etc.","Ako ovaj artikal ima varijante, onda ne može biti biran u prodajnom nalogu." -apps/erpnext/erpnext/accounts/doctype/sales_invoice/sales_invoice.js +1517,You can only redeem max {0} points in this order.,Можете унети највише {0} поена у овој наруџбини. -apps/erpnext/erpnext/hr/doctype/attendance/attendance.py +38,No leave record found for employee {0} for {1},Nije nađena evidancija o odsustvu Zaposlenog {0} za {1} -DocType: Pricing Rule,Discount on Price List Rate (%),Popust na cijene iz cjenovnika (%) -apps/erpnext/erpnext/stock/doctype/item/item_dashboard.py +17,Groups,Grupe -DocType: Item,Item Attribute,Atribut artikla -DocType: Payment Request,Amount in customer's currency,Iznos u valuti kupca -apps/erpnext/erpnext/controllers/sales_and_purchase_return.py +105,Warehouse is mandatory,Skladište je obavezan podatak -,Stock Ageing,Starost zaliha -DocType: Email Digest,New Sales Orders,Novi prodajni nalozi -apps/erpnext/erpnext/restaurant/doctype/restaurant_order_entry/restaurant_order_entry.py +64,Invoice Created,Kreirana faktura -DocType: Employee Internal Work History,Employee Internal Work History,Interna radna istorija Zaposlenog -apps/erpnext/erpnext/templates/includes/cart/cart_dropdown.html +25,Cart is Empty,Korpa je prazna -DocType: Patient,Patient Details,Detalji o pacijentu -apps/erpnext/erpnext/accounts/doctype/journal_entry/journal_entry.py +583,Stock Entry {0} is not submitted,Unos zaliha {0} nije potvrđen -apps/erpnext/erpnext/setup/setup_wizard/operations/install_fixtures.py +105,Rest Of The World,Ostatak svijeta -DocType: Work Order,Additional Operating Cost,Dodatni operativni troškovi -DocType: Purchase Invoice,Rejected Warehouse,Odbijeno skladište -DocType: Asset Repair,Manufacturing Manager,Menadžer proizvodnje -apps/erpnext/erpnext/hr/doctype/compensatory_leave_request/compensatory_leave_request.py +39,You are not present all day(s) between compensatory leave request days,Нисте присутни свих дана између захтева за компензацијски одмор. -DocType: Purchase Invoice Item,Is Fixed Asset,Artikal je osnovno sredstvo -,POS,POS -apps/erpnext/erpnext/accounts/doctype/sales_invoice/sales_invoice.py +367,Timesheet {0} is already completed or cancelled,Potrošeno vrijeme {0} je već potvrđeno ili otkazano -apps/erpnext/erpnext/hr/doctype/leave_application/leave_application.py +526, (Half Day),(Pola dana) -DocType: Shipping Rule,Net Weight,Neto težina -apps/erpnext/erpnext/education/doctype/student_attendance/student_attendance.py +56,Attendance Record {0} exists against Student {1},Zapis o prisustvu {0} постоји kod studenata {1} -DocType: Payment Entry Reference,Outstanding,Preostalo -DocType: Sales Invoice Item,Discount (%) on Price List Rate with Margin,Popust (%) -DocType: Purchase Invoice,Select Shipping Address,Odaberite adresu isporuke -apps/erpnext/erpnext/accounts/report/purchase_order_items_to_be_billed/purchase_order_items_to_be_billed.py +20,Amount to Bill,Iznos za fakturisanje -apps/erpnext/erpnext/utilities/activation.py +82,Make Sales Orders to help you plan your work and deliver on-time,Kreiranje prodajnog naloga će vam pomoći da isplanirate svoje vrijeme i dostavite robu na vrijeme -apps/erpnext/erpnext/accounts/page/pos/pos.js +809,Sync Offline Invoices,Sinhronizuj offline fakture -DocType: Blanket Order,Manufacturing,Proizvodnja -apps/erpnext/erpnext/controllers/website_list_for_contact.py +117,{0}% Delivered,{0}% Isporučeno -apps/erpnext/erpnext/education/doctype/course_schedule/course_schedule.js +7,Attendance,Prisustvo -DocType: Delivery Note,Customer's Purchase Order No,Broj porudžbenice kupca -apps/erpnext/erpnext/manufacturing/doctype/production_planning_tool/production_planning_tool.py +125,Please enter Sales Orders in the above table,U tabelu iznad unesite prodajni nalog -DocType: Quality Inspection,Report Date,Datum izvještaja -DocType: POS Profile,Item Groups,Vrste artikala -DocType: Pricing Rule,Discount Percentage,Procenat popusta -apps/erpnext/erpnext/accounts/report/gross_profit/gross_profit.py +72,Gross Profit %,Bruto dobit% -DocType: Crop,"You can define all the tasks which need to carried out for this crop here. The day field is used to mention the day on which the task needs to be carried out, 1 being the 1st day, etc.. ","Овде можете дефинисати све задатке које је потребно извршити за ову жетву. Поље Дан говори дан на који је задатак потребно извршити, 1 је 1. дан, итд." -apps/erpnext/erpnext/accounts/doctype/payment_order/payment_order.js +7,Payment Request,Upit za plaćanje -,Purchase Analytics,Analiza nabavke -DocType: Location,Tree Details,Detalji stabla -DocType: Upload Attendance,Upload Attendance,Priloži evidenciju -DocType: GL Entry,Against,Povezano sa -DocType: Grant Application,Requested Amount,Traženi iznos -apps/erpnext/erpnext/config/crm.py +92,"Record of all communications of type email, phone, chat, visit, etc.","Snimanje svih komunikacija tipa email, telefon, poruke, posjete, itd." -DocType: Purchase Order,Customer Contact Email,Kontakt e-mail kupca -apps/erpnext/erpnext/public/js/utils/customer_quick_entry.js +34,Primary Address Details,Detalji o primarnoj adresi -apps/erpnext/erpnext/accounts/report/accounts_receivable/accounts_receivable.py +98,Above,Iznad -DocType: Item,Variant Based On,Varijanta zasnovana na -DocType: Project,Task Weight,Težina zadataka -DocType: Payment Entry,Transaction ID,Transakcije -DocType: Payment Entry Reference,Allocated,Dodijeljeno -apps/erpnext/erpnext/stock/dashboard/item_dashboard.js +180,Add more items or open full form,Dodaj još stavki ili otvori kompletan prozor -apps/erpnext/erpnext/stock/page/stock_balance/stock_balance.js +49,Reserved for sale,Rezervisana za prodaju -DocType: POS Item Group,Item Group,Vrste artikala -apps/erpnext/erpnext/accounts/report/accounts_receivable/accounts_receivable.py +84,Age (Days),Starost (Dani) -apps/erpnext/erpnext/accounts/report/trial_balance/trial_balance.py +243,Opening (Dr),Početno stanje (Du) -apps/erpnext/erpnext/accounts/report/accounts_receivable_summary/accounts_receivable_summary.py +50,Total Outstanding Amt,Preostalo za plaćanje -apps/erpnext/erpnext/setup/page/welcome_to_erpnext/welcome_to_erpnext.html +23,Go to the Desktop and start using ERPNext,Idite na radnu površinu i krenite sa radom u programu -apps/erpnext/erpnext/accounts/doctype/subscription/subscription.py +71,You can only have Plans with the same billing cycle in a Subscription,Сви Планови у Претплати морају имати исти циклус наплате -DocType: Sales Person,Name and Employee ID,Ime i ID Zaposlenog -DocType: Bank Statement Transaction Invoice Item,Invoice,Faktura -DocType: Bank Statement Transaction Invoice Item,Invoice Date,Datum fakture -DocType: Customer,From Lead,Od Lead-a -apps/erpnext/erpnext/config/crm.py +12,Database of potential customers.,Baza potencijalnih kupaca -apps/erpnext/erpnext/projects/report/project_wise_stock_tracking/project_wise_stock_tracking.py +28,Project Status,Status Projekta -apps/erpnext/erpnext/public/js/pos/pos.html +124,All Item Groups,Sve vrste artikala -apps/erpnext/erpnext/selling/doctype/installation_note/installation_note.py +49,Serial No {0} does not exist,Serijski broj {0} ne postoji -apps/erpnext/erpnext/public/js/templates/contact_list.html +34,No contacts added yet.,Još uvijek nema dodatih kontakata -apps/erpnext/erpnext/accounts/report/accounts_payable/accounts_payable.js +42,Ageing Range 3,Opseg dospijeća 3 -DocType: Request for Quotation,Request for Quotation,Zahtjev za ponudu -DocType: Payment Entry,Account Paid To,Račun plaćen u -apps/erpnext/erpnext/hr/doctype/attendance/attendance.py +44,Attendance can not be marked for future dates,Učesnik ne može biti označen za buduće datume -DocType: Stock Entry,Sales Invoice No,Broj fakture prodaje -apps/erpnext/erpnext/projects/doctype/task/task.js +39,Timesheet,Potrošeno vrijeme -DocType: HR Settings,Don't send Employee Birthday Reminders,Nemojte slati podsjetnik o rođendanima Zaposlenih -DocType: Sales Invoice Item,Available Qty at Warehouse,Dostupna količina na skladištu -DocType: Item,Foreign Trade Details,Spoljnotrgovinski detalji -DocType: Item,Minimum Order Qty,Minimalna količina za poručivanje -apps/erpnext/erpnext/hr/doctype/payroll_entry/payroll_entry.py +68,No employees for the mentioned criteria,Za traženi kriterijum nema Zaposlenih -DocType: Budget,Fiscal Year,Fiskalna godina -DocType: Stock Entry,Repack,Prepakovati -apps/erpnext/erpnext/public/js/utils/serial_no_batch_selector.js +135,Please select a warehouse,Izaberite skladište -DocType: Purchase Receipt Item,Received and Accepted,Primio i prihvatio -DocType: Project,Project will be accessible on the website to these users,Projekat će biti dostupan na sajtu sledećim korisnicima -DocType: Upload Attendance,Upload HTML,Priloži HTML -apps/erpnext/erpnext/public/js/setup_wizard.js +29,Services,Usluge -apps/erpnext/erpnext/public/js/pos/pos.html +4,Item Cart,Korpa sa artiklima -apps/erpnext/erpnext/accounts/report/accounts_receivable_summary/accounts_receivable_summary.py +34,Total Paid Amt,Ukupno plaćeno -DocType: Warehouse,Warehouse Detail,Detalji o skldištu -DocType: Quotation Item,Quotation Item,Stavka sa ponude -DocType: Journal Entry Account,Employee Advance,Napredak Zaposlenog -DocType: Purchase Order Item,Warehouse and Reference,Skladište i veza -apps/erpnext/erpnext/accounts/doctype/gl_entry/gl_entry.py +93,{0} {1}: Account {2} is inactive,{0} {1}: Nalog {2} je neaktivan -apps/erpnext/erpnext/hr/doctype/payroll_entry/payroll_entry.py +467,Fiscal Year {0} not found,Fiskalna godina {0} nije pronađena -apps/erpnext/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py +121,No Remarks,Nema napomene -DocType: Notification Control,Purchase Receipt Message,Poruka u Prijemu robe -DocType: Purchase Invoice,Taxes and Charges Deducted,Umanjeni porezi i naknade -DocType: Sales Invoice,Include Payment (POS),Uključi POS plaćanje -DocType: Sales Invoice,Customer PO Details,Pregled porudžbine kupca -apps/erpnext/erpnext/accounts/report/accounts_receivable_summary/accounts_receivable_summary.py +27,Total Invoiced Amt,Ukupno fakturisano -apps/erpnext/erpnext/public/js/stock_analytics.js +54,Select Brand...,Izaberite brend -DocType: Item,Default Unit of Measure,Podrazumijevana jedinica mjere -DocType: Purchase Invoice Item,Serial No,Serijski broj -DocType: Supplier,Supplier Type,Tip dobavljača -apps/erpnext/erpnext/stock/dashboard/item_dashboard_list.html +25,Actual Qty {0} / Waiting Qty {1},Trenutna kol. {0} / Na čekanju {1} -DocType: Supplier,Individual,Fizičko lice -apps/erpnext/erpnext/stock/doctype/material_request/material_request_list.js +9,Partially Ordered,Djelimično poručeno -DocType: Bank Reconciliation Detail,Posting Date,Datum dokumenta -DocType: Cheque Print Template,Date Settings,Podešavanje datuma -DocType: Payment Entry,Total Allocated Amount (Company Currency),Ukupan povezani iznos (Valuta) -DocType: Account,Income,Prihod -apps/erpnext/erpnext/public/js/utils/item_selector.js +20,Add Items,Dodaj stavke -apps/erpnext/erpnext/accounts/page/pos/pos.js +1731,Price List not found or disabled,Cjenovnik nije pronađen ili je zaključan -DocType: Vital Signs,Weight (In Kilogram),Težina (u kg) -apps/erpnext/erpnext/accounts/page/pos/pos.js +796,New Sales Invoice,Nova faktura -DocType: Employee Transfer,New Company,Novo preduzeće -DocType: Issue,Support Team,Tim za podršku -DocType: Item,Valuation Method,Način vrednovanja -DocType: Project,Project Type,Tip Projekta -DocType: Purchase Order Item,Returned Qty,Vraćena kol. -DocType: Purchase Invoice,Additional Discount Amount (Company Currency),Iznos dodatnog popusta (valuta preduzeća) -,Employee Information,Informacije o Zaposlenom -apps/erpnext/erpnext/selling/report/inactive_customers/inactive_customers.py +16,'Days Since Last Order' must be greater than or equal to zero,"""Dana od poslednje porudžbine"" mora biti veće ili jednako nuli" -DocType: Asset,Maintenance,Održavanje -DocType: Item Price,Multiple Item prices.,Više cijena artikala -apps/erpnext/erpnext/accounts/doctype/payment_entry/payment_entry.py +379,Received From,je primljen od -DocType: Payment Entry,Write Off Difference Amount,Otpis razlike u iznosu -DocType: Task,Closing Date,Datum zatvaranja -DocType: Payment Entry,Cheque/Reference Date,Datum izvoda -DocType: Production Plan Item,Planned Qty,Planirana količina -DocType: Repayment Schedule,Payment Date,Datum plaćanja -DocType: Vehicle,Additional Details,Dodatni detalji -DocType: Company,Create Chart Of Accounts Based On,Kreiraj kontni plan prema -apps/erpnext/erpnext/manufacturing/doctype/bom/bom.js +244,You can not change rate if BOM mentioned agianst any item,Не можете променити цену ако постоји Саставница за било коју ставку. -apps/erpnext/erpnext/setup/doctype/email_digest/templates/default.html +130,Open To Do,Otvori To Do -DocType: Authorization Rule,Average Discount,Prosječan popust -DocType: Item,Material Issue,Reklamacija robe -DocType: Purchase Order Item,Billed Amt,Fakturisani iznos -apps/erpnext/erpnext/buying/doctype/request_for_quotation/request_for_quotation.py +261,Supplier Quotation {0} created,Ponuda dobavljaču {0} је kreirana -apps/erpnext/erpnext/stock/doctype/stock_ledger_entry/stock_ledger_entry.py +101,Not allowed to update stock transactions older than {0},Nije dozvoljeno mijenjati Promjene na zalihama starije od {0} -apps/erpnext/erpnext/public/js/event.js +27,Add Employees,Dodaj Zaposlenog -apps/erpnext/erpnext/config/hr.py +394,Setting up Employees,Podešavanja Zaposlenih -apps/erpnext/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py +96,Warehouse not found in the system,Skladište nije pronađeno u sistemu -apps/erpnext/erpnext/hr/doctype/leave_application/leave_application.py +264,Attendance for employee {0} is already marked for this day,Prisustvo zaposlenog {0} је već označeno za ovaj dan -apps/erpnext/erpnext/hr/doctype/salary_slip/salary_slip.py +349,Employee relieved on {0} must be set as 'Left',"Zaposleni smijenjen na {0} mora biti označen kao ""Napustio""" -DocType: Sales Invoice, Shipping Bill Number,Broj isporuke -,Lab Test Report,Izvještaj labaratorijskog testa -apps/erpnext/erpnext/accounts/doctype/journal_entry/journal_entry.py +331,You cannot credit and debit same account at the same time,Не можете кредитирати и дебитовати исти налог у исто време. -DocType: Sales Invoice,Customer Name,Naziv kupca -DocType: Employee,Current Address,Trenutna adresa -apps/erpnext/erpnext/setup/doctype/email_digest/templates/default.html +97,Upcoming Calendar Events,Predstojeći događaji u kalendaru -DocType: Accounts Settings,Make Payment via Journal Entry,Kreiraj uplatu kroz knjiženje -DocType: Payment Request,Paid,Plaćeno -DocType: Pricing Rule,Buying,Nabavka -DocType: Stock Settings,Default Item Group,Podrazumijevana vrsta artikala -apps/erpnext/erpnext/manufacturing/report/bom_stock_report/bom_stock_report.py +24,In Stock Qty,Na zalihama -DocType: Purchase Invoice,Taxes and Charges Deducted (Company Currency),Umanjeni porezi i naknade (valuta preduzeća) -DocType: Stock Entry,Additional Costs,Dodatni troškovi -DocType: Project Task,Pending Review,Čeka provjeru -DocType: Item Default,Default Selling Cost Center,Podrazumijevani centar troškova -apps/erpnext/erpnext/public/js/pos/pos.html +109,No Customers yet!,Još uvijek nema kupaca! -apps/erpnext/erpnext/stock/doctype/delivery_note/delivery_note.js +909,Sales Return,Povraćaj prodaje -apps/erpnext/erpnext/selling/page/point_of_sale/point_of_sale.js +628,No Items added to cart,Nema dodatih artikala na računu -apps/erpnext/erpnext/selling/doctype/customer/customer_dashboard.py +6,This is based on transactions against this Customer. See timeline below for details,Ovo je zasnovano na transkcijama ovog klijenta. Pogledajte vremensku liniju ispod za dodatne informacije -DocType: Project Task,Make Timesheet,Kreiraj potrošeno vrijeme -apps/erpnext/erpnext/selling/doctype/sales_order/sales_order.py +69,Warning: Sales Order {0} already exists against Customer's Purchase Order {1},Upozorenje: Prodajni nalog {0}već postoji veza sa porudžbenicom kupca {1} -DocType: Healthcare Settings,Healthcare Settings,Podešavanje klinike -apps/erpnext/erpnext/buying/doctype/supplier/supplier.js +39,Accounting Ledger,Analitička kartica -DocType: Stock Entry,Total Outgoing Value,Ukupna vrijednost isporuke -apps/erpnext/erpnext/controllers/selling_controller.py +265,Sales Order {0} is {1},Prodajni nalog {0} је {1} -DocType: Stock Settings,Automatically Set Serial Nos based on FIFO,Podesi automatski serijski broj da koristi FIFO -apps/erpnext/erpnext/selling/report/customer_acquisition_and_loyalty/customer_acquisition_and_loyalty.py +57,New Customers,Novi kupci -apps/erpnext/erpnext/selling/doctype/customer/customer_dashboard.py +10,Pre Sales,Prije prodaje -DocType: POS Customer Group,POS Customer Group,POS grupa kupaca -DocType: Quotation,Shopping Cart,Korpa sa sajta -apps/erpnext/erpnext/stock/page/stock_balance/stock_balance.js +50,Reserved for manufacturing,Rezervisana za proizvodnju -DocType: Pricing Rule,Pricing Rule Help,Pravilnik za cijene pomoć -apps/erpnext/erpnext/accounts/report/accounts_payable/accounts_payable.js +35,Ageing Range 2,Opseg dospijeća 2 -DocType: Employee Benefit Application,Employee Benefits,Primanja Zaposlenih -DocType: POS Item Group,POS Item Group,POS Vrsta artikala -DocType: Lead,Lead,Lead -DocType: HR Settings,Employee Settings,Podešavanja zaposlenih -apps/erpnext/erpnext/templates/pages/home.html +32,View All Products,Pogledajte sve proizvode -DocType: Patient Medical Record,Patient Medical Record,Medicinski karton pacijenta -DocType: Student Attendance Tool,Batch,Serija -apps/erpnext/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js +928,Purchase Receipt,Prijem robe -DocType: Item,Warranty Period (in days),Garantni rok (u danima) -apps/erpnext/erpnext/config/selling.py +28,Customer database.,Korisnička baza podataka -DocType: Attendance,Attendance Date,Datum prisustva -DocType: Supplier Scorecard,Notify Employee,Obavijestiti Zaposlenog -apps/erpnext/erpnext/setup/doctype/sales_person/sales_person.py +46,User ID not set for Employee {0},Korisnički ID nije podešen za Zaposlenog {0} -,Stock Projected Qty,Projektovana količina na zalihama -apps/erpnext/erpnext/accounts/doctype/payment_request/payment_request.py +408,Make Payment,Kreiraj plaćanje -apps/erpnext/erpnext/accounts/doctype/fiscal_year/fiscal_year.py +51,You cannot delete Fiscal Year {0}. Fiscal Year {0} is set as default in Global Settings,Ne možete obrisati fiskalnu godinu {0}. Fiskalna {0} godina je označena kao trenutna u globalnim podešavanjima. -apps/erpnext/erpnext/stock/stock_ledger.py +382,{0} units of {1} needed in {2} to complete this transaction.,Željenu količinu {0} za artikal {1} je potrebno dodati na {2} da bi dovršili transakciju.. -,Item-wise Sales Register,Prodaja po artiklima -DocType: Item Tax,Tax Rate,Poreska stopa -DocType: GL Entry,Remarks,Napomena -DocType: Opening Invoice Creation Tool,Sales,Prodaja -DocType: Pricing Rule,Pricing Rule,Pravilnik za cijene -DocType: Products Settings,Products Settings,Podešavanje proizvoda -DocType: Healthcare Practitioner,Mobile,Mobilni -DocType: Purchase Invoice Item,Price List Rate,Cijena -DocType: Purchase Invoice Item,Discount Amount,Vrijednost popusta -,Sales Invoice Trends,Trendovi faktura prodaje -apps/erpnext/erpnext/accounts/doctype/loyalty_program/loyalty_program.py +110,You don't have enought Loyalty Points to redeem,Немате довољно Бодова Лојалности. -DocType: Purchase Invoice,Tax Breakup,Porez po pozicijama -DocType: Asset Maintenance Log,Task,Zadatak -apps/erpnext/erpnext/stock/doctype/item/item.js +359,Add / Edit Prices,Dodaj / Izmijeni cijene -,Item Prices,Cijene artikala -DocType: Additional Salary,Salary Component,Компонента плате -DocType: Sales Invoice,Customer's Purchase Order Date,Datum porudžbenice kupca -DocType: Item,Country of Origin,Zemlja porijekla -apps/erpnext/erpnext/hr/doctype/department_approver/department_approver.py +17,Please select Employee Record first.,Molimo izaberite registar Zaposlenih prvo -DocType: Blanket Order,Order Type,Vrsta porudžbine -DocType: BOM Item,Rate & Amount,Cijena i iznos sa rabatom -DocType: Pricing Rule,For Price List,Za cjenovnik -DocType: Purchase Invoice,Tax ID,Poreski broj -DocType: Job Card,WIP Warehouse,Wip skladište -,Itemwise Recommended Reorder Level,Pregled preporučenih nivoa dopune -apps/erpnext/erpnext/accounts/doctype/journal_entry/journal_entry.py +408,{0} against Bill {1} dated {2},{0} veza sa računom {1} na datum {2} -apps/erpnext/erpnext/accounts/doctype/account/account.py +113,You are not authorized to set Frozen value,Немате дозволу да постављате замрзнуту вредност -,Requested Items To Be Ordered,Tražene stavke za isporuku -DocType: Employee Attendance Tool,Unmarked Attendance,Neobilježeno prisustvo -apps/erpnext/erpnext/accounts/doctype/sales_invoice/sales_invoice.py +657,Sales Order {0} is not submitted,Prodajni nalog {0} nije potvrđen -DocType: Item,Default Material Request Type,Podrazumijevani zahtjev za tip materijala -apps/erpnext/erpnext/selling/page/sales_funnel/sales_funnel.js +42,Sales Pipeline,Prodajna linija -apps/erpnext/erpnext/manufacturing/doctype/production_order/production_order.py +617,Already completed,Već završen -DocType: Production Plan Item,Ordered Qty,Poručena kol -DocType: Item,Sales Details,Detalji prodaje -apps/erpnext/erpnext/config/learn.py +11,Navigating,Navigacija -apps/erpnext/erpnext/utilities/user_progress.py +138,Your Products or Services,Vaši artikli ili usluge -DocType: Contract,CRM,CRM -apps/erpnext/erpnext/public/js/setup_wizard.js +51,The Brand,Brend -apps/erpnext/erpnext/selling/doctype/sales_order/sales_order.py +165,Quotation {0} is cancelled,Ponuda {0} je otkazana -DocType: Pricing Rule,Item Code,Šifra artikla -DocType: Purchase Order,Customer Mobile No,Broj telefona kupca -apps/erpnext/erpnext/stock/report/stock_balance/stock_balance.py +92,Reorder Qty,Kol. za dopunu -apps/erpnext/erpnext/stock/dashboard/item_dashboard.js +118,Move Item,Premještanje artikala -DocType: Buying Settings,Buying Settings,Podešavanja nabavke -DocType: Asset Movement,From Employee,Od Zaposlenog -DocType: Driver,Fleet Manager,Menadžer transporta -apps/erpnext/erpnext/stock/doctype/batch/batch.js +51,Stock Levels,Nivoi zalihe -DocType: Sales Invoice Item,Rate With Margin (Company Currency),Cijena sa popustom (Valuta preduzeća) -apps/erpnext/erpnext/accounts/report/trial_balance/trial_balance.py +278,Closing (Cr),Saldo (Po) -apps/erpnext/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js +622,Product Bundle,Sastavnica -apps/erpnext/erpnext/accounts/report/sales_payment_summary/sales_payment_summary.py +21,Sales and Returns,Prodaja i povraćaji -apps/erpnext/erpnext/accounts/page/pos/pos.js +801,Sync Master Data,Sinhronizuj podatke iz centrale -DocType: Sales Person,Sales Person Name,Ime prodajnog agenta -DocType: Landed Cost Voucher,Purchase Receipts,Prijemi robe -apps/erpnext/erpnext/config/learn.py +21,Customizing Forms,Prilagođavanje formi -apps/erpnext/erpnext/hr/doctype/attendance/attendance.py +19,Attendance for employee {0} is already marked,Prisustvo zaposlenog {0} je već označeno -DocType: Project,% Complete Method,% metod vrednovanja završetka projekta -DocType: Purchase Invoice,Overdue,Istekao -DocType: Purchase Invoice,Posting Time,Vrijeme izrade računa -DocType: Stock Entry,Purchase Receipt No,Broj prijema robe -DocType: Project,Expected End Date,Očekivani datum završetka -apps/erpnext/erpnext/projects/doctype/project/project.py +84,Expected End Date can not be less than Expected Start Date,Očekivani datum završetka ne može biti manji od očekivanog dana početka -DocType: Customer,Customer Primary Contact,Primarni kontakt kupca -DocType: Project,Expected Start Date,Očekivani datum početka -DocType: Supplier,Credit Limit,Kreditni limit -DocType: Item,Item Tax,Porez -DocType: Pricing Rule,Selling,Prodaja -DocType: Purchase Order,Customer Contact,Kontakt kupca -apps/erpnext/erpnext/stock/doctype/item/item.py +578,Item {0} does not exist,Artikal {0} ne postoji -apps/erpnext/erpnext/utilities/user_progress.py +247,Add Users,Dodaj korisnike -apps/erpnext/erpnext/public/js/utils/serial_no_batch_selector.js +81,Select Serial Numbers,Izaberite serijske brojeve -DocType: Bank Reconciliation Detail,Payment Entry,Uplate -DocType: Purchase Invoice,In Words,Riječima -DocType: HR Settings,Employee record is created using selected field. ,Izvještaj o Zaposlenom se kreira korišćenjem izabranog polja. -apps/erpnext/erpnext/selling/doctype/installation_note/installation_note.py +59,Serial No {0} does not belong to Delivery Note {1},Serijski broj {0} ne pripada otpremnici {1} -DocType: Issue,Support,Podrška -DocType: Production Plan,Get Sales Orders,Pregledaj prodajne naloge -DocType: Stock Ledger Entry,Stock Ledger Entry,Unos zalihe robe -apps/erpnext/erpnext/config/projects.py +36,Gantt chart of all tasks.,Gantov grafikon svih zadataka -DocType: Purchase Invoice Item,Price List Rate (Company Currency),Cijena (Valuta preduzeća) -DocType: Delivery Stop,Address Name,Naziv adrese -apps/erpnext/erpnext/setup/doctype/sales_person/sales_person.py +54,Another Sales Person {0} exists with the same Employee id,Postoji još jedan Prodavac {0} sa istim ID zaposlenog -DocType: Item Group,Item Group Name,Naziv vrste artikala -apps/erpnext/erpnext/selling/doctype/customer/customer.py +175,A Customer Group exists with same name please change the Customer name or rename the Customer Group,Isto ime grupe kupca već postoji. Promijenite ime kupca ili izmijenite grupu kupca -apps/erpnext/erpnext/accounts/doctype/journal_entry/journal_entry.py +586,Warning: Another {0} # {1} exists against stock entry {2},Upozorenje: Još jedan {0} # {1} postoji u vezanom Unosu zaliha {2} -apps/erpnext/erpnext/accounts/report/delivered_items_to_be_billed/delivered_items_to_be_billed.py +18,Suplier,Dobavljač -DocType: Item,Has Serial No,Ima serijski broj -apps/erpnext/erpnext/hr/doctype/attendance/attendance.py +31,Employee {0} on Half day on {1},Zaposleni {0} na pola radnog vremena {1} -DocType: Payment Entry,Difference Amount (Company Currency),Razlika u iznosu (Valuta) -apps/erpnext/erpnext/public/js/utils.js +56,Add Serial No,Dodaj serijski broj -apps/erpnext/erpnext/config/accounts.py +35,Company and Accounts,Preduzeće i računi -DocType: Employee,Current Address Is,Trenutna adresa je -DocType: Payment Entry,Unallocated Amount,Nepovezani iznos -apps/erpnext/erpnext/accounts/report/profitability_analysis/profitability_analysis.js +58,Show zero values,Prikaži vrijednosti sa nulom -DocType: Bank Account,Address and Contact,Adresa i kontakt -,Supplier-Wise Sales Analytics,Analiza Dobavljačeve pametne prodaje -apps/erpnext/erpnext/accounts/doctype/payment_request/payment_request.py +348,Payment Entry is already created,Uplata je već kreirana -DocType: Purchase Invoice Item,Item,Artikal -DocType: Purchase Invoice,Unpaid,Neplaćen -DocType: Purchase Invoice Item,Net Rate,Neto cijena sa rabatom -DocType: Project User,Project User,Projektni user -DocType: Item,Customer Items,Proizvodi kupca -apps/erpnext/erpnext/stock/doctype/item/item.py +840,Item {0} is cancelled,Stavka {0} je otkazana -apps/erpnext/erpnext/stock/report/stock_balance/stock_balance.py +89,Balance Value,Stanje vrijed. -apps/erpnext/erpnext/stock/doctype/delivery_note/delivery_note.py +98,Sales Order required for Item {0},Prodajni nalog je obavezan za artikal {0} -DocType: Clinical Procedure,Patient,Pacijent -DocType: Stock Entry,Default Target Warehouse,Prijemno skladište -DocType: GL Entry,Voucher No,Br. dokumenta -apps/erpnext/erpnext/education/api.py +80,Attendance has been marked successfully.,Prisustvo je uspješno obilježeno. -apps/erpnext/erpnext/stock/doctype/serial_no/serial_no.py +398,Serial No {0} created,Serijski broj {0} kreiran -DocType: Account,Asset,Osnovna sredstva -DocType: Payment Entry,Received Amount,Iznos uplate -apps/erpnext/erpnext/hr/doctype/department/department.js +14,You cannot edit root node.,Не можете уређивати коренски чвор. -,Sales Funnel,Prodajni lijevak -DocType: Sales Invoice,Payment Due Date,Datum dospijeća fakture -apps/erpnext/erpnext/config/healthcare.py +8,Consultation,Pregled -apps/erpnext/erpnext/manufacturing/doctype/production_plan/production_plan_dashboard.py +8,Related,Povezan -DocType: Warehouse,Warehouse Name,Naziv skladišta -DocType: Authorization Rule,Customer / Item Name,Kupac / Naziv proizvoda -DocType: Timesheet,Total Billed Amount,Ukupno fakturisano -apps/erpnext/erpnext/stock/report/stock_balance/stock_balance.py +85,In Value,Prijem vrije. -DocType: Expense Claim,Employees Email Id,ID email Zaposlenih -apps/erpnext/erpnext/healthcare/page/appointment_analytic/appointment_analytic.js +54,Tree Type,Tip stabla -DocType: Stock Entry,Update Rate and Availability,Izmijenite cijenu i dostupnost -apps/erpnext/erpnext/buying/doctype/purchase_order/purchase_order.js +1156,Supplier Quotation,Ponuda dobavljača -DocType: Material Request Item,Quantity and Warehouse,Količina i skladište -DocType: Purchase Invoice,Taxes and Charges Added,Porezi i naknade dodate -DocType: Work Order,Warehouses,Skladišta -DocType: SMS Center,All Customer Contact,Svi kontakti kupca -apps/erpnext/erpnext/accounts/doctype/account/account.js +78,Ledger,Skladišni karton -DocType: Quotation,Quotation Lost Reason,Razlog gubitka ponude -DocType: Purchase Invoice,Return Against Purchase Invoice,Povraćaj u vezi sa Fakturom nabavke -DocType: Sales Invoice Item,Brand Name,Naziv brenda -DocType: Account,Stock,Zalihe -DocType: Customer Group,Customer Group Name,Naziv grupe kupca -DocType: Item,Is Sales Item,Da li je prodajni artikal -apps/erpnext/erpnext/accounts/report/accounts_receivable/accounts_receivable.html +119,Invoiced Amount,Fakturisano -DocType: Purchase Invoice,Edit Posting Date and Time,Izmijeni datum i vrijeme dokumenta -,Inactive Customers,Neaktivni kupci -DocType: Stock Entry Detail,Stock Entry Detail,Detalji unosa zaliha -DocType: Sales Invoice,Accounting Details,Računovodstveni detalji -DocType: Asset Movement,Stock Manager,Menadžer zaliha -apps/erpnext/erpnext/accounts/report/accounts_payable/accounts_payable.js +22,As on Date,Na datum -DocType: Serial No,Is Cancelled,Je otkazan -DocType: Naming Series,Setup Series,Podešavanje tipa dokumenta -,Point of Sale,Kasa -DocType: C-Form Invoice Detail,Invoice No,Broj fakture -DocType: Landed Cost Item,Purchase Receipt Item,Stavka Prijema robe -DocType: Bank Statement Transaction Payment Item,Invoices,Fakture -DocType: Project,Task Progress,% završenosti zadataka -DocType: Employee Attendance Tool,Employee Attendance Tool,Alat za prisustvo Zaposlenih -DocType: Salary Slip,Payment Days,Dana za plaćanje -apps/erpnext/erpnext/config/hr.py +231,Recruitment,Zapošljavanje -DocType: Purchase Invoice,Taxes and Charges Calculation,Izračun Poreza -DocType: Appraisal,For Employee,Za Zaposlenog -apps/erpnext/erpnext/config/selling.py +163,Terms and Conditions Template,Uslovi i odredbe šablon -DocType: Vehicle Service,Change,Kusur -apps/erpnext/erpnext/stock/doctype/batch/batch.js +105,Stock Entry {0} created,Unos zaliha {0} je kreiran -apps/erpnext/erpnext/selling/page/point_of_sale/point_of_sale.js +1181,Search Item (Ctrl + i),Pretraga artikala (Ctrl + i) -apps/erpnext/erpnext/templates/generators/item.html +101,View in Cart,Pogledajte u korpi -apps/erpnext/erpnext/stock/get_item_details.py +405,Item Price updated for {0} in Price List {1},Cijena artikla je izmijenjena {0} u cjenovniku {1} -apps/erpnext/erpnext/selling/page/point_of_sale/point_of_sale.js +623,Discount,Popust -DocType: Packing Slip,Net Weight UOM,Neto težina JM -DocType: Bank Account,Party Type,Tip partije -DocType: Selling Settings,Sales Order Required,Prodajni nalog je obavezan -apps/erpnext/erpnext/accounts/page/pos/pos.js +1108,Search Item,Pretraži artikal -,Delivered Items To Be Billed,Nefakturisana isporučena roba -DocType: Account,Debit,Duguje -DocType: Patient Appointment,Date TIme,Datum i vrijeme -DocType: Bank Reconciliation Detail,Payment Document,Dokument za plaćanje -apps/erpnext/erpnext/accounts/doctype/journal_entry/journal_entry.py +184,You can not enter current voucher in 'Against Journal Entry' column,"Неможете унети тренутни ваучер у колону ""На основу ставке у журналу""" -DocType: Purchase Invoice,In Words (Company Currency),Riječima (valuta kompanije) -,Purchase Receipt Trends,Trendovi prijema robe -DocType: Employee Leave Approver,Employee Leave Approver,Odobreno odsustvo Zaposlenog -apps/erpnext/erpnext/accounts/report/trial_balance/trial_balance.py +25,Fiscal Year {0} does not exist,Fiskalna godina {0} ne postoji -DocType: Purchase Invoice Item,Accepted Warehouse,Prihvaćeno skladište -DocType: Account,Income Account,Račun prihoda -DocType: Journal Entry Account,Account Balance,Knjigovodstveno stanje -apps/erpnext/erpnext/projects/doctype/task/task.py +41,'Expected Start Date' can not be greater than 'Expected End Date',Očekivani datum početka ne može biti veći od očekivanog datuma završetka -DocType: Training Event,Employee Emails,Elektronska pošta Zaposlenog -apps/erpnext/erpnext/stock/report/batch_wise_balance_history/batch_wise_balance_history.py +36,Opening Qty,Početna količina -DocType: Item,Reorder level based on Warehouse,Nivo dopune u zavisnosti od skladišta -apps/erpnext/erpnext/stock/doctype/batch/batch.js +84,To Warehouse,U skladište -DocType: Account,Is Group,Je grupa -DocType: Purchase Invoice,Contact Person,Kontakt osoba -DocType: Item,Item Code for Suppliers,Dobavljačeva šifra -apps/erpnext/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js +893,Return / Debit Note,Povraćaj / knjižno zaduženje -DocType: Request for Quotation Supplier,Request for Quotation Supplier,Zahtjev za ponudu dobavljača -,LeaderBoard,Tabla -DocType: Lab Test Groups,Lab Test Groups,Labaratorijske grupe -DocType: Training Result Employee,Training Result Employee,Rezultati obuke Zaposlenih -DocType: Serial No,Invoice Details,Detalji fakture -apps/erpnext/erpnext/config/accounts.py +130,Banking and Payments,Bakarstvo i plaćanja -DocType: Additional Salary,Employee Name,Ime Zaposlenog -apps/erpnext/erpnext/selling/page/sales_funnel/sales_funnel.py +33,Active Leads / Customers,Активни Леадс / Kupci -DocType: POS Profile,Accounting,Računovodstvo -DocType: Payment Entry,Party Name,Ime partije -DocType: Item,Manufacture,Proizvodnja -apps/erpnext/erpnext/templates/pages/projects.html +27,New task,Novi zadatak -DocType: Journal Entry,Accounts Payable,Obaveze prema dobavljačima -DocType: Purchase Invoice,Shipping Address,Adresa isporuke -DocType: Bank Statement Transaction Invoice Item,Outstanding Amount,Preostalo za uplatu -apps/erpnext/erpnext/stock/doctype/warehouse/warehouse_tree.js +15,New Warehouse Name,Naziv novog skladišta -apps/erpnext/erpnext/accounts/report/delivered_items_to_be_billed/delivered_items_to_be_billed.py +20,Billed Amount,Fakturisani iznos -apps/erpnext/erpnext/stock/report/batch_wise_balance_history/batch_wise_balance_history.py +37,Balance Qty,Stanje zalihe -,Item Shortage Report,Izvještaj o negativnim zalihama -apps/erpnext/erpnext/accounts/doctype/payment_entry/payment_entry.py +383,Transaction reference no {0} dated {1},Broj izvoda {0} na datum {1} -apps/erpnext/erpnext/utilities/activation.py +83,Make Sales Order,Kreiraj prodajni nalog -DocType: Purchase Invoice,Items,Artikli -,Employees working on a holiday,Zaposleni koji rade za vrijeme praznika -DocType: Payment Entry,Allocate Payment Amount,Poveži uplaćeni iznos -DocType: Patient,Patient ID,ID pacijenta -apps/erpnext/erpnext/accounts/report/accounts_receivable/accounts_receivable.html +263,Printed On,Datum i vrijeme štampe -DocType: Sales Invoice,Debit To,Zaduženje za -apps/erpnext/erpnext/config/setup.py +14,Global Settings,Globalna podešavanja -apps/erpnext/erpnext/hr/doctype/job_offer/job_offer.js +18,Make Employee,Keriraj Zaposlenog -apps/erpnext/erpnext/stock/doctype/stock_entry/stock_entry.py +244,Atleast one warehouse is mandatory,Minimum jedno skladište je obavezno -DocType: Price List,Price List Name,Naziv cjenovnika -DocType: Asset,Journal Entry for Scrap,Knjiženje rastura i loma -DocType: Item,Website Warehouse,Skladište web sajta -DocType: Sales Invoice Item,Customer's Item Code,Šifra kupca -DocType: Bank Guarantee,Supplier,Dobavljači -DocType: Purchase Invoice,Additional Discount Amount,Iznos dodatnog popusta -apps/erpnext/erpnext/projects/report/project_wise_stock_tracking/project_wise_stock_tracking.py +30,Project Start Date,Datum početka projekta -DocType: Announcement,Student,Student -apps/erpnext/erpnext/accounts/report/purchase_order_items_to_be_billed/purchase_order_items_to_be_billed.py +18,Suplier Name,Naziv dobavljača -apps/erpnext/erpnext/stock/report/batch_wise_balance_history/batch_wise_balance_history.py +37,In Qty,Prijem količine -apps/erpnext/erpnext/stock/report/item_price_stock/item_price_stock.py +68,Selling Rate,Prodajna cijena -apps/erpnext/erpnext/hr/doctype/upload_attendance/upload_attendance.js +64,Import Successful!,Uvoz uspješan! -apps/erpnext/erpnext/accounts/doctype/sales_invoice/sales_invoice.py +539,Stock cannot be updated against Delivery Note {0},Zaliha se ne može promijeniti jer je vezana sa otpremnicom {0} -apps/erpnext/erpnext/accounts/page/pos/pos.js +742,You are in offline mode. You will not be able to reload until you have network.,Радите без интернета. Нећете моћи да учитате страницу док се не повежете. -apps/erpnext/erpnext/selling/page/point_of_sale/point_of_sale.js +542,Form View,Prikaži kao formu -apps/erpnext/erpnext/stock/report/stock_projected_qty/stock_projected_qty.py +81,Shortage Qty,Manjak kol. -DocType: Drug Prescription,Hour,Sat -apps/erpnext/erpnext/setup/doctype/item_group/item_group.js +55,Item Group Tree,Stablo vrste artikala -DocType: POS Profile,Update Stock,Ažuriraj zalihu -DocType: Crop,Target Warehouse,Ciljno skladište -,Delivery Note Trends,Trendovi Otpremnica -DocType: Stock Entry,Default Source Warehouse,Izdajno skladište -apps/erpnext/erpnext/hr/doctype/salary_slip/salary_slip.py +535,"{0}: Employee email not found, hence email not sent","{0}: Email zaposlenog nije pronađena, stoga email nije poslat" -apps/erpnext/erpnext/patches/v7_0/create_warehouse_nestedset.py +59,All Warehouses,Sva skladišta -DocType: Asset Value Adjustment,Difference Amount,Razlika u iznosu -DocType: Journal Entry,User Remark,Korisnička napomena -DocType: Notification Control,Quotation Message,Ponuda - poruka -DocType: Purchase Order,% Received,% Primljeno -DocType: Journal Entry,Stock Entry,Unos zaliha -apps/erpnext/erpnext/stock/report/item_prices/item_prices.py +40,Sales Price List,Prodajni cjenovnik -apps/erpnext/erpnext/accounts/report/gross_profit/gross_profit.py +67,Avg. Selling Rate,Prosječna prodajna cijena -DocType: Item,End of Life,Kraj proizvodnje -DocType: Payment Entry,Payment Type,Vrsta plaćanja -DocType: Selling Settings,Default Customer Group,Podrazumijevana grupa kupaca -DocType: Bank Account,Party,Partija -,Total Stock Summary,Ukupan pregled zalihe -DocType: Purchase Invoice,Net Total (Company Currency),Ukupno bez PDV-a (Valuta preduzeća) -DocType: Healthcare Settings,Patient Name,Ime pacijenta -apps/erpnext/erpnext/public/js/payment/pos_payment.html +17,Write Off,Otpisati -DocType: Notification Control,Delivery Note Message,Poruka na otpremnici -apps/erpnext/erpnext/stock/doctype/serial_no/serial_no.py +162,"Cannot delete Serial No {0}, as it is used in stock transactions","Ne može se obrisati serijski broj {0}, dok god se nalazi u dijelu Promjene na zalihama" -apps/erpnext/erpnext/accounts/doctype/sales_invoice/sales_invoice.py +660,Delivery Note {0} is not submitted,Otpremnica {0} nije potvrđena -apps/erpnext/erpnext/hr/doctype/employee/employee_tree.js +29,New Employee,Novi Zaposleni -apps/erpnext/erpnext/public/js/pos/pos.html +98,Customers in Queue,Kupci na čekanju -DocType: Purchase Invoice,Price List Currency,Valuta Cjenovnika -DocType: Authorization Rule,Applicable To (Employee),Primjenljivo na (zaposlene) -apps/erpnext/erpnext/setup/setup_wizard/operations/install_fixtures.py +96,Project Manager,Projektni menadzer -DocType: Journal Entry,Accounts Receivable,Potraživanja od kupaca -DocType: Purchase Invoice Item,Rate,Cijena sa popustom -DocType: Project Task,View Task,Pogledaj zadatak -DocType: Employee Education,Employee Education,Obrazovanje Zaposlenih -DocType: Account,Expense,Rashod -apps/erpnext/erpnext/config/learn.py +107,Newsletters,Newsletter-i -DocType: Purchase Invoice,Select Supplier Address,Izaberite adresu dobavljača -apps/erpnext/erpnext/stock/get_item_details.py +521,Price List {0} is disabled or does not exist,Cjenovnik {0} je zaključan ili ne postoji -DocType: Delivery Note,Billing Address Name,Naziv adrese za naplatu -DocType: Restaurant Order Entry,Add Item,Dodaj stavku -apps/erpnext/erpnext/setup/setup_wizard/operations/install_fixtures.py +108,All Customer Groups,Sve grupe kupca -,Employee Birthday,Rođendan Zaposlenih -DocType: Project,Total Billed Amount (via Sales Invoices),Ukupno fakturisano (putem fakture prodaje) -DocType: Purchase Invoice Item,Weight UOM,JM Težina -DocType: Purchase Invoice Item,Stock Qty,Zaliha -DocType: Delivery Note,Return Against Delivery Note,Povraćaj u vezi sa otpremnicom -apps/erpnext/erpnext/accounts/report/accounts_payable/accounts_payable.js +28,Ageing Range 1,Opseg dospijeća 1 -DocType: Serial No,Incoming Rate,Nabavna cijena -DocType: Projects Settings,Timesheets,Potrošnja vremena -DocType: Upload Attendance,Attendance From Date,Datum početka prisustva -apps/erpnext/erpnext/public/js/pos/pos.html +115,Stock Items,Artikli na zalihama -apps/erpnext/erpnext/accounts/page/pos/pos.js +2227,New Cart,Nova korpa -apps/erpnext/erpnext/stock/report/stock_balance/stock_balance.py +83,Opening Value,Početna vrijednost -apps/erpnext/erpnext/maintenance/doctype/maintenance_schedule/maintenance_schedule.py +60,"Setting Events to {0}, since the Employee attached to the below Sales Persons does not have a User ID{1}","Podešavanje stanja na {0}, pošto Zaposleni koji se priključio Prodavcima nema koririsnički ID {1}" -DocType: Upload Attendance,Import Attendance,Uvoz prisustva -apps/erpnext/erpnext/config/selling.py +184,Analytics,Analitika -DocType: Email Digest,Bank Balance,Stanje na računu -DocType: Education Settings,Employee Number,Broj Zaposlenog -DocType: Purchase Receipt Item,Rate and Amount,Cijena i vrijednost sa popustom -apps/erpnext/erpnext/accounts/report/accounts_receivable/accounts_receivable.html +237,'Total','Ukupno bez PDV-a' -DocType: Purchase Invoice,Total Taxes and Charges,Porez -apps/erpnext/erpnext/hr/doctype/salary_slip/salary_slip.py +266,No active or default Salary Structure found for employee {0} for the given dates,Nisu pronađene aktivne ili podrazumjevane strukture plate za Zaposlenog {0} za dati period -DocType: Purchase Order Item,Supplier Part Number,Dobavljačeva šifra -DocType: Project Task,Project Task,Projektni zadatak -DocType: Item Group,Parent Item Group,Nadređena Vrsta artikala -apps/erpnext/erpnext/education/doctype/student_attendance_tool/student_attendance_tool.js +113,Mark Attendance,Označi prisustvo -apps/erpnext/erpnext/manufacturing/doctype/production_planning_tool/production_planning_tool.py +229,{0} created,Kreirao je korisnik {0} -DocType: Purchase Order,Advance Paid,Avansno plačanje -apps/erpnext/erpnext/stock/doctype/item/item.js +41,Projected,Projektovana količina na zalihama -apps/erpnext/erpnext/stock/report/itemwise_recommended_reorder_level/itemwise_recommended_reorder_level.py +43,Reorder Level,Nivo dopune -DocType: Opportunity,Customer / Lead Address,Kupac / Adresa lead-a -DocType: Buying Settings,Default Buying Price List,Podrazumijevani Cjenovnik -DocType: Purchase Invoice Item,Qty,Kol -DocType: Mode of Payment,General,Opšte -DocType: Supplier,Default Payable Accounts,Podrazumijevani nalog za plaćanje -apps/erpnext/erpnext/templates/includes/cart/cart_items.html +29,Rate: {0},Cijena: {0} -apps/erpnext/erpnext/selling/page/point_of_sale/point_of_sale.js +1659,Write Off Amount,Zaokruženi iznos -apps/erpnext/erpnext/accounts/report/accounts_receivable/accounts_receivable.html +139,Total Outstanding Amount,Preostalo za plaćanje -apps/erpnext/erpnext/selling/doctype/sales_order/sales_order.py +359,Not Paid and Not Delivered,Nije plaćeno i nije isporučeno -DocType: Asset Maintenance Log,Planned,Planirano -DocType: Bank Reconciliation,Total Amount,Ukupan iznos -apps/erpnext/erpnext/manufacturing/doctype/bom/bom.py +177,Please select Price List,Izaberite cjenovnik -DocType: Quality Inspection,Item Serial No,Seriski broj artikla -apps/erpnext/erpnext/setup/setup_wizard/operations/install_fixtures.py +388,Customer Service,Usluga kupca -DocType: Project Task,Working,U toku -DocType: Cost Center,Stock User,Korisnik zaliha -apps/erpnext/erpnext/stock/doctype/warehouse/warehouse.js +27,General Ledger,Glavna knjiga -DocType: C-Form,Received Date,Datum prijema -apps/erpnext/erpnext/config/projects.py +13,Project master.,Projektni master -DocType: Item Price,Valid From,Važi od -,Purchase Order Trends,Trendovi kupovina -DocType: Quotation,In Words will be visible once you save the Quotation.,Sačuvajte Predračun da bi Ispis slovima bio vidljiv -apps/erpnext/erpnext/stock/page/stock_balance/stock_balance.js +48,Projected Qty,Projektovana količina -apps/erpnext/erpnext/config/selling.py +234,Customer Addresses And Contacts,Kontakt i adresa kupca -DocType: Healthcare Settings,Employee name and designation in print,Ime i pozicija Zaposlenog -apps/erpnext/erpnext/selling/doctype/sales_order/sales_order.js +1161,For Warehouse,Za skladište -apps/erpnext/erpnext/stock/report/item_prices/item_prices.py +41,Purchase Price List,Nabavni cjenovnik -apps/erpnext/erpnext/accounts/report/accounts_payable/accounts_payable.js +83,Accounts Payable Summary,Pregled obaveze prema dobavljačima -apps/erpnext/erpnext/selling/doctype/sales_order/sales_order.py +224,Delivery Notes {0} must be cancelled before cancelling this Sales Order,Otpremnice {0} moraju biti otkazane prije otkazivanja prodajnog naloga -DocType: Loan,Total Payment,Ukupno plaćeno -DocType: POS Settings,POS Settings,POS podešavanja -apps/erpnext/erpnext/accounts/report/gross_profit/gross_profit.py +70,Buying Amount,Iznos nabavke -DocType: Purchase Invoice Item,Valuation Rate,Prosječna nab. cijena -apps/erpnext/erpnext/projects/report/project_wise_stock_tracking/project_wise_stock_tracking.py +26,Project Id,ID Projekta -DocType: Purchase Invoice,Invoice Copy,Kopija Fakture -apps/erpnext/erpnext/projects/doctype/project/project.py +279,You have been invited to collaborate on the project: {0},Позвани сте да сарађујете на пројекту: {0} -DocType: Journal Entry Account,Purchase Order,Porudžbenica -DocType: Sales Invoice Item,Rate With Margin,Cijena sa popustom -apps/erpnext/erpnext/selling/page/point_of_sale/point_of_sale.js +1182,"Search by item code, serial number, batch no or barcode","Pretraga po šifri, serijskom br. ili bar kodu" -DocType: GL Entry,Voucher Type,Vrsta dokumenta -apps/erpnext/erpnext/stock/doctype/serial_no/serial_no.py +222,Serial No {0} has already been received,Serijski broj {0} je već primljen -apps/erpnext/erpnext/config/setup.py +51,Data Import and Export,Uvoz i izvoz podataka -apps/erpnext/erpnext/controllers/accounts_controller.py +617,Total advance ({0}) against Order {1} cannot be greater than the Grand Total ({2}),Ukupan avns({0}) na porudžbini {1} ne može biti veći od Ukupnog iznosa ({2}) -DocType: Material Request,% Ordered,% Poručenog -apps/erpnext/erpnext/stock/get_item_details.py +523,Price List not selected,Cjenovnik nije odabran -DocType: POS Profile,Apply Discount On,Primijeni popust na -DocType: Item,Total Projected Qty,Ukupna projektovana količina -DocType: Shipping Rule Condition,Shipping Rule Condition,Uslovi pravila nabavke -apps/erpnext/erpnext/config/stock.py +317,Opening Stock Balance,Početno stanje zalihe -,Customer Credit Balance,Kreditni limit kupca -apps/erpnext/erpnext/public/js/templates/address_list.html +20,No address added yet.,Adresa još nije dodata. -DocType: Subscription,Net Total,Ukupno bez PDV-a -DocType: Sales Invoice,Total Qty,Ukupna kol. -DocType: Purchase Invoice,Return,Povraćaj -DocType: Sales Order Item,Delivery Warehouse,Skladište dostave -DocType: Purchase Invoice,Total (Company Currency),Ukupno bez PDV-a (Valuta) -DocType: Sales Invoice,Change Amount,Kusur -apps/erpnext/erpnext/buying/doctype/request_for_quotation/request_for_quotation.js +1083,Opportunity,Prilika -DocType: Sales Order,Fully Delivered,Kompletno isporučeno -DocType: Leave Control Panel,Leave blank if considered for all employee types,Ostavite prazno ako se podrazumijeva za sve tipove Zaposlenih -apps/erpnext/erpnext/selling/page/point_of_sale/point_of_sale.js +594,Disc,Popust -DocType: Customer,Default Price List,Podrazumijevani cjenovnik -DocType: Bank Statement Transaction Invoice Item,Journal Entry,Knjiženje -DocType: Purchase Invoice,Apply Additional Discount On,Primijeni dodatni popust na -apps/erpnext/erpnext/buying/doctype/supplier/supplier_dashboard.py +6,This is based on transactions against this Supplier. See timeline below for details,Ovo je zasnovano na transkcijama ovog dobavljača. Pogledajte vremensku liniju ispod za dodatne informacije -apps/erpnext/erpnext/accounts/report/payment_period_based_on_invoice_date/payment_period_based_on_invoice_date.py +64,90-Above,Iznad 90 dana -apps/erpnext/erpnext/education/doctype/assessment_plan/assessment_plan.py +49,You have already assessed for the assessment criteria {}.,Већ сте оценили за критеријум оцењивања {}. -apps/erpnext/erpnext/accounts/doctype/sales_invoice/sales_invoice.py +994,Serial Numbers in row {0} does not match with Delivery Note,Serijski broj na poziciji {0} se ne poklapa sa otpremnicom -apps/erpnext/erpnext/public/js/templates/contact_list.html +37,New Contact,Novi kontakt -DocType: Cashier Closing,Returns,Povraćaj -DocType: Delivery Note,Delivery To,Isporuka za -apps/erpnext/erpnext/projects/report/project_wise_stock_tracking/project_wise_stock_tracking.py +29,Project Value,Vrijednost Projekta -DocType: Warehouse,Parent Warehouse,Nadređeno skladište -DocType: Payment Request,Make Sales Invoice,Kreiraj fakturu prodaje -apps/erpnext/erpnext/selling/page/point_of_sale/point_of_sale.js +594,Del,Obriši -apps/erpnext/erpnext/public/js/stock_analytics.js +58,Select Warehouse...,Izaberite skladište... -DocType: Payment Reconciliation,Invoice/Journal Entry Details,Faktura / Detalji knjiženja -,Projected Quantity as Source,Projektovana izvorna količina -DocType: Asset Maintenance,Manufacturing User,Korisnik u proizvodnji -apps/erpnext/erpnext/utilities/activation.py +99,Create Users,Kreiraj korisnike -apps/erpnext/erpnext/public/js/pos/pos_selected_item.html +15,Price,Cijena -apps/erpnext/erpnext/stock/report/batch_wise_balance_history/batch_wise_balance_history.py +37,Out Qty,Izdavanje Kol. -DocType: Supplier Scorecard Scoring Standing,Employee,Zaposleni -apps/erpnext/erpnext/config/projects.py +24,Project activity / task.,Projektna aktivnost / zadatak -DocType: Production Plan Item,Reserved Warehouse in Sales Order / Finished Goods Warehouse,Rezervisano skladište u Prodajnom nalogu / Skladište gotovog proizvoda -DocType: Appointment Type,Physician,Ljekar -DocType: Opening Invoice Creation Tool Item,Quantity,Količina -DocType: Buying Settings,Purchase Receipt Required,Prijem robe je obavezan -apps/erpnext/erpnext/shopping_cart/doctype/shopping_cart_settings/shopping_cart_settings.py +38,Currency is required for Price List {0},Valuta je obavezna za Cjenovnik {0} -apps/erpnext/erpnext/stock/report/stock_balance/stock_balance.py +87,Out Value,Izdavanje vrije. -DocType: Loyalty Program,Customer Group,Grupa kupaca -apps/erpnext/erpnext/accounts/doctype/gl_entry/gl_entry.py +161,You are not authorized to add or update entries before {0},Немате дозволу да додајете или ажурирате ставке пре {0} -DocType: Serial No,Warehouse can only be changed via Stock Entry / Delivery Note / Purchase Receipt,Skladište se jedino može promijeniti u dijelu Unos zaliha / Otpremnica / Prijem robe -apps/erpnext/erpnext/hooks.py +148,Request for Quotations,Zahtjev za ponude -apps/erpnext/erpnext/config/desktop.py +159,Learn,Naučite -DocType: Timesheet,Employee Detail,Detalji o Zaposlenom -DocType: POS Profile,Ignore Pricing Rule,Zanemari pravilnik o cijenama -DocType: Purchase Invoice,Additional Discount,Dodatni popust -DocType: Payment Entry,Cheque/Reference No,Broj izvoda -apps/erpnext/erpnext/hr/doctype/attendance/attendance.py +46,Attendance date can not be less than employee's joining date,Datum prisustva ne može biti raniji od datuma ulaska zaposlenog -apps/erpnext/erpnext/utilities/user_progress.py +146,Box,Kutija -DocType: Payment Entry,Total Allocated Amount,Ukupno povezani iznos -apps/erpnext/erpnext/config/selling.py +46,All Addresses.,Sve adrese -apps/erpnext/erpnext/utilities/user_progress.py +39,Opening Balances,Početna stanja -apps/erpnext/erpnext/config/setup.py +66,Users and Permissions,Korisnici i dozvole -apps/erpnext/erpnext/hr/doctype/staffing_plan/staffing_plan.py +68,"You can only plan for upto {0} vacancies and budget {1} \ -======= Yield UOM,Јединица мере приноса Default Supplier,Podrazumijevani dobavljač Select Patient,Izaberite pacijenta, @@ -1545,7 +904,6 @@ All Addresses.,Sve adrese, Opening Balances,Početna stanja, Users and Permissions,Korisnici i dozvole, "You can only plan for upto {0} vacancies and budget {1} \ ->>>>>>> 4f473eb090 (fix: typo in loyalty program throw message (#36432)) for {2} as per staffing plan {3} for parent company {4}.",Можете планирати до {0} слободна места и буџетирати {1} \ за {2} по плану особља {3} за матичну компанију {4}. apps/erpnext/erpnext/public/js/event.js +19,Add Customers,Dodaj kupce DocType: Employee External Work History,Employee External Work History,Istorijat o radu van preduzeća za Zaposlenog From 710ec1808604c9288519c2d858aa831bf8117ec8 Mon Sep 17 00:00:00 2001 From: Frappe PR Bot Date: Tue, 1 Aug 2023 18:06:33 +0000 Subject: [PATCH 040/501] chore(release): Bumped to Version 14.33.0 # [14.33.0](https://github.com/frappe/erpnext/compare/v14.32.1...v14.33.0) (2023-08-01) ### Bug Fixes * allow fully depreciated existing assets ([#36378](https://github.com/frappe/erpnext/issues/36378)) ([43b85c5](https://github.com/frappe/erpnext/commit/43b85c571101d3d8d5d2f5a9f041ba176ceb243e)) * change fieldtype from Currency to Float for the valuation rate in the stock report ([93bd4c7](https://github.com/frappe/erpnext/commit/93bd4c7ff342a329ffa02d4f22bc642750311428)) * cost center filter for fetching payments ([c9daa85](https://github.com/frappe/erpnext/commit/c9daa85985e998f96a885100b7763c7ea7a5b49f)) * Defined "Open" Status as default (backport [#36421](https://github.com/frappe/erpnext/issues/36421)) ([#36424](https://github.com/frappe/erpnext/issues/36424)) ([33a9477](https://github.com/frappe/erpnext/commit/33a947726ddf61691f7a36e99bcc75318c59b173)) * filter by cost center in trial balance ([02428b4](https://github.com/frappe/erpnext/commit/02428b446d959784c546f30c176d6af10e6ed250)) * german translations ([9c8e2a3](https://github.com/frappe/erpnext/commit/9c8e2a33e9f418ce3569e584ff05a5b1f7d149ec)) * GL Entries should not be splitted based on cost center allocation in PCV ([ade13e6](https://github.com/frappe/erpnext/commit/ade13e6d36a17777d3547c4bf7c279c4be0e9a28)) * group item reorder by (warehouse, material_request_type) (backport [#35818](https://github.com/frappe/erpnext/issues/35818)) ([#36425](https://github.com/frappe/erpnext/issues/36425)) ([516191b](https://github.com/frappe/erpnext/commit/516191bf2ba9cb9f00613dc5c6bf3985caa95a4c)) * Ignore account closing balance for financial statement ([800417e](https://github.com/frappe/erpnext/commit/800417eeed04e29be372f30f08b9acce74adb6fc)) * in payment_entry 'Unallocated Amount' cal is broken ([#36369](https://github.com/frappe/erpnext/issues/36369)) ([cd3b85c](https://github.com/frappe/erpnext/commit/cd3b85ccff227cf18db950d5e2a883d1a681b021)) * incorrect `idx` on JE's after reconciliation ([80eb875](https://github.com/frappe/erpnext/commit/80eb8754dbcfdc27b9ccc03c1a5581df161a2e66)) * incorrect qty set in the serial no picker ([57b19a5](https://github.com/frappe/erpnext/commit/57b19a523efd4f6d22da8e25c6370710b8320579)) * incorrect usage `get_cached_value` on single doctypes ([8f72a68](https://github.com/frappe/erpnext/commit/8f72a6814bd8343a88499c21e677b1751a72af36)) * **Item Group:** allow root deletion ([1a6be5e](https://github.com/frappe/erpnext/commit/1a6be5e19bf5d0aea9e456d18e51b19b43013690)) * job card suggest holiday as start date (backport [#35958](https://github.com/frappe/erpnext/issues/35958)) ([#36423](https://github.com/frappe/erpnext/issues/36423)) ([29ddd26](https://github.com/frappe/erpnext/commit/29ddd26ba107f770c1ec78dc2ff600376ba97bbd)) * Job Card validation fixed when displaying total completed quantity ([7b3bcd3](https://github.com/frappe/erpnext/commit/7b3bcd3bc4933513daa05b2edad667f1bc30abf7)) * multiple issues related to Production Plan (backport [#36377](https://github.com/frappe/erpnext/issues/36377)) ([#36381](https://github.com/frappe/erpnext/issues/36381)) ([a799e1b](https://github.com/frappe/erpnext/commit/a799e1b2179114bbde036454ffbd86015cffcbfd)) * not able to make material request (backport [#36416](https://github.com/frappe/erpnext/issues/36416)) ([#36426](https://github.com/frappe/erpnext/issues/36426)) ([99e7406](https://github.com/frappe/erpnext/commit/99e7406b5b2a27ecd81c1ccb76472ff9c7ca43ff)) * only publish repost progress to doc subscriber (backport [#36400](https://github.com/frappe/erpnext/issues/36400)) ([#36402](https://github.com/frappe/erpnext/issues/36402)) ([32bdb7c](https://github.com/frappe/erpnext/commit/32bdb7cccdf60b49496f778667a8eba03a5d98d4)) * overallocation validation misfire on normal invoices ([#36349](https://github.com/frappe/erpnext/issues/36349)) ([09af485](https://github.com/frappe/erpnext/commit/09af485d549cd8873b81c7e7bb2b2a45b6c21735)) * paid_amount when the group is mode of payment ([50ef358](https://github.com/frappe/erpnext/commit/50ef35845a216a8e13def39e06e95fd821caec55)) * process_owner is not link User (backport [#36420](https://github.com/frappe/erpnext/issues/36420)) ([#36422](https://github.com/frappe/erpnext/issues/36422)) ([289dc36](https://github.com/frappe/erpnext/commit/289dc3669610ee17681aab7cd4436f653fb5bd6a)) * removed "fetch_from" (backport [#36365](https://github.com/frappe/erpnext/issues/36365)) ([#36386](https://github.com/frappe/erpnext/issues/36386)) ([6d051f5](https://github.com/frappe/erpnext/commit/6d051f57322f9404718815341b8a61f7bb37d5cb)) * root type in account map for balance sheet ([#36303](https://github.com/frappe/erpnext/issues/36303)) ([5b56296](https://github.com/frappe/erpnext/commit/5b562961e39de1875028bd50f5fc66e80262c683)) * show invoices name instead of object address ([e802f0c](https://github.com/frappe/erpnext/commit/e802f0c3526c7529039683bd9ff176eab7afcb4f)) * show tds & tcs separately ([619b0fe](https://github.com/frappe/erpnext/commit/619b0feb5f7f5827f5a0ec4e140dbda4da8fe262)) * test with None conditon in PE ([479cab0](https://github.com/frappe/erpnext/commit/479cab0336cd024477367bd8f7e535ce638f7251)) * typo in loyalty program throw message ([#36432](https://github.com/frappe/erpnext/issues/36432)) ([782a4d1](https://github.com/frappe/erpnext/commit/782a4d1fa83cea6f7b277bab0c36ac33d7dbbb8c)) ### Features * add party type filter ([32fea64](https://github.com/frappe/erpnext/commit/32fea643cd6c0cd8759c24d4a51299abe74279d5)) ### Performance Improvements * avoid full table scan in sle count check ([#36428](https://github.com/frappe/erpnext/issues/36428)) ([04f9915](https://github.com/frappe/erpnext/commit/04f99150098c033fe2f27ec92c789bae9209be0e)) * move project status reminder to hourly (backport [#36372](https://github.com/frappe/erpnext/issues/36372)) ([#36373](https://github.com/frappe/erpnext/issues/36373)) ([4ac60cd](https://github.com/frappe/erpnext/commit/4ac60cd73bf953c9eac218e170d3fe09e1790a81)) * use `LEFT JOIN` instead of `NOT EXISTS` (backport [#36221](https://github.com/frappe/erpnext/issues/36221)) ([#36383](https://github.com/frappe/erpnext/issues/36383)) ([26a0b3b](https://github.com/frappe/erpnext/commit/26a0b3b3800b06866c9b6c5de76f3c086cd7f1ca)) --- erpnext/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/__init__.py b/erpnext/__init__.py index 67700c52cd4a..0ffff542012a 100644 --- a/erpnext/__init__.py +++ b/erpnext/__init__.py @@ -3,7 +3,7 @@ import frappe -__version__ = "14.32.1" +__version__ = "14.33.0" def get_default_company(user=None): From d1590f266bffc55fa76f799276c1f96fc4cef930 Mon Sep 17 00:00:00 2001 From: Corentin Flr <10946971+cogk@users.noreply.github.com> Date: Tue, 1 Aug 2023 14:35:11 +0200 Subject: [PATCH 041/501] fix: Fix query for financial statement report (cherry picked from commit bd3fc7c4342195ce22cd860cba83e287aaac15b5) --- .../consolidated_financial_statement.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/accounts/report/consolidated_financial_statement/consolidated_financial_statement.py b/erpnext/accounts/report/consolidated_financial_statement/consolidated_financial_statement.py index 56d33dd111e5..ebf43d13b856 100644 --- a/erpnext/accounts/report/consolidated_financial_statement/consolidated_financial_statement.py +++ b/erpnext/accounts/report/consolidated_financial_statement/consolidated_financial_statement.py @@ -659,7 +659,7 @@ def set_gl_entries_by_account( & (gle.posting_date <= to_date) & (account.lft >= root_lft) & (account.rgt <= root_rgt) - & (account.root_type <= root_type) + & (account.root_type == root_type) ) .orderby(gle.account, gle.posting_date) ) From 8501a1182ae8323d91438da30ddc8d93cf8c2789 Mon Sep 17 00:00:00 2001 From: Anand Baburajan Date: Wed, 2 Aug 2023 09:07:04 +0530 Subject: [PATCH 042/501] fix: cross connect delivery note and sales invoice (#36183) * fix: cross connect delivery note and sales invoice * chore: remove unnecessary non_standard_fieldname --- .../accounts/doctype/sales_invoice/sales_invoice_dashboard.py | 1 + erpnext/stock/doctype/delivery_note/delivery_note_dashboard.py | 1 + 2 files changed, 2 insertions(+) diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice_dashboard.py b/erpnext/accounts/doctype/sales_invoice/sales_invoice_dashboard.py index 0a765f3f46fd..6fdcf263a552 100644 --- a/erpnext/accounts/doctype/sales_invoice/sales_invoice_dashboard.py +++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice_dashboard.py @@ -15,6 +15,7 @@ def get_data(): }, "internal_links": { "Sales Order": ["items", "sales_order"], + "Delivery Note": ["items", "delivery_note"], "Timesheet": ["timesheets", "time_sheet"], }, "transactions": [ diff --git a/erpnext/stock/doctype/delivery_note/delivery_note_dashboard.py b/erpnext/stock/doctype/delivery_note/delivery_note_dashboard.py index b6b5ff4296f0..e66c23324da5 100644 --- a/erpnext/stock/doctype/delivery_note/delivery_note_dashboard.py +++ b/erpnext/stock/doctype/delivery_note/delivery_note_dashboard.py @@ -11,6 +11,7 @@ def get_data(): }, "internal_links": { "Sales Order": ["items", "against_sales_order"], + "Sales Invoice": ["items", "against_sales_invoice"], "Material Request": ["items", "material_request"], "Purchase Order": ["items", "purchase_order"], }, From 8c57d56240d2c35ae09350af5d3598b4773e9ac2 Mon Sep 17 00:00:00 2001 From: rohitwaghchaure Date: Wed, 2 Aug 2023 12:47:12 +0530 Subject: [PATCH 043/501] fix: search not working for so in the Production Plan (#36459) fix: search not working for so --- .../manufacturing/doctype/production_plan/production_plan.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/manufacturing/doctype/production_plan/production_plan.py b/erpnext/manufacturing/doctype/production_plan/production_plan.py index 261aa76b7067..8d75c3cb60d3 100644 --- a/erpnext/manufacturing/doctype/production_plan/production_plan.py +++ b/erpnext/manufacturing/doctype/production_plan/production_plan.py @@ -1731,7 +1731,7 @@ def sales_order_query( query = query.where(so_table.name.isin(filters.get("sales_orders"))) if txt: - query = query.where(table.item_code.like(f"{txt}%")) + query = query.where(table.parent.like(f"%{txt}%")) if page_len: query = query.limit(page_len) From 4605bc51ae3d0217a7f06d0e908214fb0c31b168 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Wed, 2 Aug 2023 12:59:31 +0530 Subject: [PATCH 044/501] fix: search not working for so in the Production Plan (backport #36459) (#36460) fix: search not working for so in the Production Plan (#36459) fix: search not working for so (cherry picked from commit 8c57d56240d2c35ae09350af5d3598b4773e9ac2) Co-authored-by: rohitwaghchaure --- .../manufacturing/doctype/production_plan/production_plan.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/manufacturing/doctype/production_plan/production_plan.py b/erpnext/manufacturing/doctype/production_plan/production_plan.py index 261aa76b7067..8d75c3cb60d3 100644 --- a/erpnext/manufacturing/doctype/production_plan/production_plan.py +++ b/erpnext/manufacturing/doctype/production_plan/production_plan.py @@ -1731,7 +1731,7 @@ def sales_order_query( query = query.where(so_table.name.isin(filters.get("sales_orders"))) if txt: - query = query.where(table.item_code.like(f"{txt}%")) + query = query.where(table.parent.like(f"%{txt}%")) if page_len: query = query.limit(page_len) From 75012c17d49856d1a18640f8119df42be8c7c709 Mon Sep 17 00:00:00 2001 From: Frappe PR Bot Date: Wed, 2 Aug 2023 07:31:21 +0000 Subject: [PATCH 045/501] chore(release): Bumped to Version 14.33.1 ## [14.33.1](https://github.com/frappe/erpnext/compare/v14.33.0...v14.33.1) (2023-08-02) ### Bug Fixes * search not working for so in the Production Plan (backport [#36459](https://github.com/frappe/erpnext/issues/36459)) ([#36460](https://github.com/frappe/erpnext/issues/36460)) ([4605bc5](https://github.com/frappe/erpnext/commit/4605bc51ae3d0217a7f06d0e908214fb0c31b168)) --- erpnext/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/__init__.py b/erpnext/__init__.py index 0ffff542012a..cf8d50b72096 100644 --- a/erpnext/__init__.py +++ b/erpnext/__init__.py @@ -3,7 +3,7 @@ import frappe -__version__ = "14.33.0" +__version__ = "14.33.1" def get_default_company(user=None): From b033b3b0d6a7a286e253a24d7432b607eaa14d09 Mon Sep 17 00:00:00 2001 From: Husam Hammad <85282854+husamhammad@users.noreply.github.com> Date: Wed, 2 Aug 2023 13:58:05 +0300 Subject: [PATCH 046/501] fix: handle None value in payment_term_outstanding * Fix payment entry bug: Handle None value in payment_term_outstanding * fix: Handle None value in payment_term_outstanding V2 fix linting issue (cherry picked from commit 27ebf14f9d516ec555ed702a2edd78c6e65d517f) --- erpnext/accounts/doctype/payment_entry/payment_entry.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/erpnext/accounts/doctype/payment_entry/payment_entry.py b/erpnext/accounts/doctype/payment_entry/payment_entry.py index b71e2235612a..bece3f9c2807 100644 --- a/erpnext/accounts/doctype/payment_entry/payment_entry.py +++ b/erpnext/accounts/doctype/payment_entry/payment_entry.py @@ -238,7 +238,8 @@ def validate_allocated_amount_with_latest_data(self): d.payment_term and ( (flt(d.allocated_amount)) > 0 - and flt(d.allocated_amount) > flt(latest.payment_term_outstanding) + and latest.payment_term_outstanding + and (flt(d.allocated_amount) > flt(latest.payment_term_outstanding)) ) and self.term_based_allocation_enabled_for_reference(d.reference_doctype, d.reference_name) ): From caa4f331694937a6768fa3180562e947d6996e7f Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Wed, 2 Aug 2023 17:13:22 +0530 Subject: [PATCH 047/501] fix: don't allow negative rate (backport #36027) (#36465) * fix: don't allow negative rates (#36027) * fix: don't allow negative rate * test: don't allow negative rate * fix: only check for -rate on items child table (cherry picked from commit dedf24b86db824f84dd48cf2b470272aa90ab636) # Conflicts: # erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py * chore: resolve merge conflict --------- Co-authored-by: Devin Slauenwhite Co-authored-by: ruthra kumar --- .../accounts/doctype/sales_invoice/test_sales_invoice.py | 7 +++++++ erpnext/controllers/status_updater.py | 3 +++ 2 files changed, 10 insertions(+) diff --git a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py index 856631ee6576..fd5ca8b1ebae 100644 --- a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py +++ b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py @@ -3316,6 +3316,13 @@ def test_sales_invoice_with_payable_tax_account(self): ) self.assertRaises(frappe.ValidationError, si.submit) + def test_sales_return_negative_rate(self): + si = create_sales_invoice(is_return=1, qty=-2, rate=-10, do_not_save=True) + self.assertRaises(frappe.ValidationError, si.save) + + si.items[0].rate = 10 + si.save() + def get_sales_invoice_for_e_invoice(): si = make_sales_invoice_for_ewaybill() diff --git a/erpnext/controllers/status_updater.py b/erpnext/controllers/status_updater.py index 58cab147a477..a4bc4a9c69e4 100644 --- a/erpnext/controllers/status_updater.py +++ b/erpnext/controllers/status_updater.py @@ -233,6 +233,9 @@ def validate_qty(self): if hasattr(d, "qty") and d.qty > 0 and self.get("is_return"): frappe.throw(_("For an item {0}, quantity must be negative number").format(d.item_code)) + if hasattr(d, "item_code") and hasattr(d, "rate") and d.rate < 0: + frappe.throw(_("For an item {0}, rate must be a positive number").format(d.item_code)) + if d.doctype == args["source_dt"] and d.get(args["join_field"]): args["name"] = d.get(args["join_field"]) From c1819a4b21f09b1775cb20c79a8683c734961ec3 Mon Sep 17 00:00:00 2001 From: rohitwaghchaure Date: Thu, 3 Aug 2023 12:19:25 +0530 Subject: [PATCH 048/501] fix: serial no not able to reject for the internal transfer (#36467) --- erpnext/controllers/buying_controller.py | 21 +++++- .../purchase_receipt/test_purchase_receipt.py | 64 +++++++++++++++++++ erpnext/stock/stock_ledger.py | 2 + 3 files changed, 85 insertions(+), 2 deletions(-) diff --git a/erpnext/controllers/buying_controller.py b/erpnext/controllers/buying_controller.py index fa94a4a88d40..1de399677303 100644 --- a/erpnext/controllers/buying_controller.py +++ b/erpnext/controllers/buying_controller.py @@ -475,6 +475,10 @@ def update_stock_ledger(self, allow_negative_stock=False, via_landed_cost_vouche if d.item_code not in stock_items: continue + rejected_qty = 0.0 + if flt(d.rejected_qty) != 0: + rejected_qty = flt(flt(d.rejected_qty) * flt(d.conversion_factor), d.precision("stock_qty")) + if d.warehouse: pr_qty = flt(flt(d.qty) * flt(d.conversion_factor), d.precision("stock_qty")) @@ -495,6 +499,11 @@ def update_stock_ledger(self, allow_negative_stock=False, via_landed_cost_vouche }, ) + if flt(rejected_qty) != 0: + from_warehouse_sle["actual_qty"] += -1 * rejected_qty + if d.rejected_serial_no: + from_warehouse_sle["serial_no"] += "\n" + cstr(d.rejected_serial_no).strip() + sl_entries.append(from_warehouse_sle) sle = self.get_sl_entries( @@ -520,6 +529,7 @@ def update_stock_ledger(self, allow_negative_stock=False, via_landed_cost_vouche else 0, } ) + sl_entries.append(sle) if d.from_warehouse and ( @@ -530,23 +540,30 @@ def update_stock_ledger(self, allow_negative_stock=False, via_landed_cost_vouche d, {"actual_qty": -1 * pr_qty, "warehouse": d.from_warehouse, "recalculate_rate": 1} ) + if flt(rejected_qty) != 0: + from_warehouse_sle["actual_qty"] += -1 * rejected_qty + if d.rejected_serial_no: + from_warehouse_sle["serial_no"] += "\n" + cstr(d.rejected_serial_no).strip() + sl_entries.append(from_warehouse_sle) - if flt(d.rejected_qty) != 0: + if flt(rejected_qty) != 0: sl_entries.append( self.get_sl_entries( d, { "warehouse": d.rejected_warehouse, - "actual_qty": flt(flt(d.rejected_qty) * flt(d.conversion_factor), d.precision("stock_qty")), + "actual_qty": rejected_qty, "serial_no": cstr(d.rejected_serial_no).strip(), "incoming_rate": 0.0, + "allow_zero_valuation_rate": True, }, ) ) if self.get("is_old_subcontracting_flow"): self.make_sl_entries_for_supplier_warehouse(sl_entries) + self.make_sl_entries( sl_entries, allow_negative_stock=allow_negative_stock, diff --git a/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py b/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py index 585871cf3910..c9433cf51060 100644 --- a/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py +++ b/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py @@ -1258,6 +1258,70 @@ def test_backdated_transaction_for_internal_transfer(self): self.assertEqual(query[0].value, 0) + def test_rejected_qty_for_internal_transfer(self): + from erpnext.stock.doctype.delivery_note.delivery_note import make_inter_company_purchase_receipt + from erpnext.stock.doctype.delivery_note.test_delivery_note import create_delivery_note + + prepare_data_for_internal_transfer() + customer = "_Test Internal Customer 2" + company = "_Test Company with perpetual inventory" + + from_warehouse = create_warehouse("_Test Internal From Warehouse New", company=company) + to_warehouse = create_warehouse("_Test Internal To Warehouse New", company=company) + rejected_warehouse = create_warehouse( + "_Test Rejected Internal To Warehouse New", company=company + ) + item_doc = make_item( + "Test Internal Transfer Item DS", + { + "is_purchase_item": 1, + "is_stock_item": 1, + "has_serial_no": 1, + "serial_no_series": "SBNS.#####", + }, + ) + + target_warehouse = create_warehouse("_Test Internal GIT Warehouse New", company=company) + + pr = make_purchase_receipt( + item_code=item_doc.name, + company=company, + posting_date=add_days(today(), -1), + warehouse=from_warehouse, + qty=2, + rate=100, + ) + + dn1 = create_delivery_note( + item_code=item_doc.name, + company=company, + customer=customer, + serial_no=pr.items[0].serial_no, + cost_center="Main - TCP1", + expense_account="Cost of Goods Sold - TCP1", + qty=2, + rate=500, + warehouse=from_warehouse, + target_warehouse=target_warehouse, + ) + + sns = get_serial_nos(dn1.items[0].serial_no) + + self.assertEqual(len(sns), 2) + + pr1 = make_inter_company_purchase_receipt(dn1.name) + pr1.items[0].qty = 1.0 + pr1.items[0].rejected_qty = 1.0 + pr1.items[0].serial_no = sns[0] + pr1.items[0].rejected_serial_no = sns[1] + pr1.items[0].warehouse = to_warehouse + pr1.items[0].rejected_warehouse = rejected_warehouse + pr1.submit() + + rejected_serial_no_wh = frappe.get_cached_value("Serial No", sns[1], "warehouse") + + self.assertEqual(rejected_warehouse, rejected_serial_no_wh) + def test_backdated_transaction_for_internal_transfer_in_trasit_warehouse_for_purchase_receipt( self, ): diff --git a/erpnext/stock/stock_ledger.py b/erpnext/stock/stock_ledger.py index 3a4191953538..d52d59a0d186 100644 --- a/erpnext/stock/stock_ledger.py +++ b/erpnext/stock/stock_ledger.py @@ -74,6 +74,7 @@ def make_sl_entries(sl_entries, allow_negative_stock=False, via_landed_cost_vouc sle_doc = make_entry(sle, allow_negative_stock, via_landed_cost_voucher) args = sle_doc.as_dict() + args["allow_zero_valuation_rate"] = sle.get("allow_zero_valuation_rate") or False if sle.get("voucher_type") == "Stock Reconciliation": # preserve previous_qty_after_transaction for qty reposting @@ -109,6 +110,7 @@ def repost_current_voucher(args, allow_negative_stock=False, via_landed_cost_vou "sle_id": args.get("name"), "creation": args.get("creation"), }, + allow_zero_rate=args.get("allow_zero_valuation_rate") or False, allow_negative_stock=allow_negative_stock, via_landed_cost_voucher=via_landed_cost_voucher, ) From a09777c406a8c29a2f76926123697ebacb990a5d Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Thu, 3 Aug 2023 13:16:55 +0530 Subject: [PATCH 049/501] fix: serial no not able to reject for the internal transfer (backport #36467) (#36475) fix: serial no not able to reject for the internal transfer (#36467) (cherry picked from commit c1819a4b21f09b1775cb20c79a8683c734961ec3) Co-authored-by: rohitwaghchaure --- erpnext/controllers/buying_controller.py | 21 +++++- .../purchase_receipt/test_purchase_receipt.py | 64 +++++++++++++++++++ erpnext/stock/stock_ledger.py | 2 + 3 files changed, 85 insertions(+), 2 deletions(-) diff --git a/erpnext/controllers/buying_controller.py b/erpnext/controllers/buying_controller.py index fa94a4a88d40..1de399677303 100644 --- a/erpnext/controllers/buying_controller.py +++ b/erpnext/controllers/buying_controller.py @@ -475,6 +475,10 @@ def update_stock_ledger(self, allow_negative_stock=False, via_landed_cost_vouche if d.item_code not in stock_items: continue + rejected_qty = 0.0 + if flt(d.rejected_qty) != 0: + rejected_qty = flt(flt(d.rejected_qty) * flt(d.conversion_factor), d.precision("stock_qty")) + if d.warehouse: pr_qty = flt(flt(d.qty) * flt(d.conversion_factor), d.precision("stock_qty")) @@ -495,6 +499,11 @@ def update_stock_ledger(self, allow_negative_stock=False, via_landed_cost_vouche }, ) + if flt(rejected_qty) != 0: + from_warehouse_sle["actual_qty"] += -1 * rejected_qty + if d.rejected_serial_no: + from_warehouse_sle["serial_no"] += "\n" + cstr(d.rejected_serial_no).strip() + sl_entries.append(from_warehouse_sle) sle = self.get_sl_entries( @@ -520,6 +529,7 @@ def update_stock_ledger(self, allow_negative_stock=False, via_landed_cost_vouche else 0, } ) + sl_entries.append(sle) if d.from_warehouse and ( @@ -530,23 +540,30 @@ def update_stock_ledger(self, allow_negative_stock=False, via_landed_cost_vouche d, {"actual_qty": -1 * pr_qty, "warehouse": d.from_warehouse, "recalculate_rate": 1} ) + if flt(rejected_qty) != 0: + from_warehouse_sle["actual_qty"] += -1 * rejected_qty + if d.rejected_serial_no: + from_warehouse_sle["serial_no"] += "\n" + cstr(d.rejected_serial_no).strip() + sl_entries.append(from_warehouse_sle) - if flt(d.rejected_qty) != 0: + if flt(rejected_qty) != 0: sl_entries.append( self.get_sl_entries( d, { "warehouse": d.rejected_warehouse, - "actual_qty": flt(flt(d.rejected_qty) * flt(d.conversion_factor), d.precision("stock_qty")), + "actual_qty": rejected_qty, "serial_no": cstr(d.rejected_serial_no).strip(), "incoming_rate": 0.0, + "allow_zero_valuation_rate": True, }, ) ) if self.get("is_old_subcontracting_flow"): self.make_sl_entries_for_supplier_warehouse(sl_entries) + self.make_sl_entries( sl_entries, allow_negative_stock=allow_negative_stock, diff --git a/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py b/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py index 585871cf3910..c9433cf51060 100644 --- a/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py +++ b/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py @@ -1258,6 +1258,70 @@ def test_backdated_transaction_for_internal_transfer(self): self.assertEqual(query[0].value, 0) + def test_rejected_qty_for_internal_transfer(self): + from erpnext.stock.doctype.delivery_note.delivery_note import make_inter_company_purchase_receipt + from erpnext.stock.doctype.delivery_note.test_delivery_note import create_delivery_note + + prepare_data_for_internal_transfer() + customer = "_Test Internal Customer 2" + company = "_Test Company with perpetual inventory" + + from_warehouse = create_warehouse("_Test Internal From Warehouse New", company=company) + to_warehouse = create_warehouse("_Test Internal To Warehouse New", company=company) + rejected_warehouse = create_warehouse( + "_Test Rejected Internal To Warehouse New", company=company + ) + item_doc = make_item( + "Test Internal Transfer Item DS", + { + "is_purchase_item": 1, + "is_stock_item": 1, + "has_serial_no": 1, + "serial_no_series": "SBNS.#####", + }, + ) + + target_warehouse = create_warehouse("_Test Internal GIT Warehouse New", company=company) + + pr = make_purchase_receipt( + item_code=item_doc.name, + company=company, + posting_date=add_days(today(), -1), + warehouse=from_warehouse, + qty=2, + rate=100, + ) + + dn1 = create_delivery_note( + item_code=item_doc.name, + company=company, + customer=customer, + serial_no=pr.items[0].serial_no, + cost_center="Main - TCP1", + expense_account="Cost of Goods Sold - TCP1", + qty=2, + rate=500, + warehouse=from_warehouse, + target_warehouse=target_warehouse, + ) + + sns = get_serial_nos(dn1.items[0].serial_no) + + self.assertEqual(len(sns), 2) + + pr1 = make_inter_company_purchase_receipt(dn1.name) + pr1.items[0].qty = 1.0 + pr1.items[0].rejected_qty = 1.0 + pr1.items[0].serial_no = sns[0] + pr1.items[0].rejected_serial_no = sns[1] + pr1.items[0].warehouse = to_warehouse + pr1.items[0].rejected_warehouse = rejected_warehouse + pr1.submit() + + rejected_serial_no_wh = frappe.get_cached_value("Serial No", sns[1], "warehouse") + + self.assertEqual(rejected_warehouse, rejected_serial_no_wh) + def test_backdated_transaction_for_internal_transfer_in_trasit_warehouse_for_purchase_receipt( self, ): diff --git a/erpnext/stock/stock_ledger.py b/erpnext/stock/stock_ledger.py index 3a4191953538..d52d59a0d186 100644 --- a/erpnext/stock/stock_ledger.py +++ b/erpnext/stock/stock_ledger.py @@ -74,6 +74,7 @@ def make_sl_entries(sl_entries, allow_negative_stock=False, via_landed_cost_vouc sle_doc = make_entry(sle, allow_negative_stock, via_landed_cost_voucher) args = sle_doc.as_dict() + args["allow_zero_valuation_rate"] = sle.get("allow_zero_valuation_rate") or False if sle.get("voucher_type") == "Stock Reconciliation": # preserve previous_qty_after_transaction for qty reposting @@ -109,6 +110,7 @@ def repost_current_voucher(args, allow_negative_stock=False, via_landed_cost_vou "sle_id": args.get("name"), "creation": args.get("creation"), }, + allow_zero_rate=args.get("allow_zero_valuation_rate") or False, allow_negative_stock=allow_negative_stock, via_landed_cost_voucher=via_landed_cost_voucher, ) From 2d43ba0c90566be8d6c0b7b5b7520f79f36d1340 Mon Sep 17 00:00:00 2001 From: Frappe PR Bot Date: Thu, 3 Aug 2023 07:48:49 +0000 Subject: [PATCH 050/501] chore(release): Bumped to Version 14.33.2 ## [14.33.2](https://github.com/frappe/erpnext/compare/v14.33.1...v14.33.2) (2023-08-03) ### Bug Fixes * serial no not able to reject for the internal transfer (backport [#36467](https://github.com/frappe/erpnext/issues/36467)) ([#36475](https://github.com/frappe/erpnext/issues/36475)) ([a09777c](https://github.com/frappe/erpnext/commit/a09777c406a8c29a2f76926123697ebacb990a5d)) --- erpnext/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/__init__.py b/erpnext/__init__.py index cf8d50b72096..7b7e2142ee68 100644 --- a/erpnext/__init__.py +++ b/erpnext/__init__.py @@ -3,7 +3,7 @@ import frappe -__version__ = "14.33.1" +__version__ = "14.33.2" def get_default_company(user=None): From dfd356a174149b98246707247a5a78b39965cc13 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Thu, 3 Aug 2023 17:18:56 +0530 Subject: [PATCH 051/501] chore: better cost center validation for assets (backport #36477) (#36479) chore: better cost center validation for assets (#36477) (cherry picked from commit 38a612c62e2943a4a84f678bfd2804671e966b46) Co-authored-by: Anand Baburajan --- erpnext/assets/doctype/asset/asset.py | 36 +++++++++++++++++++-------- 1 file changed, 26 insertions(+), 10 deletions(-) diff --git a/erpnext/assets/doctype/asset/asset.py b/erpnext/assets/doctype/asset/asset.py index b8d8ba5b4865..c97f0ece73c9 100644 --- a/erpnext/assets/doctype/asset/asset.py +++ b/erpnext/assets/doctype/asset/asset.py @@ -148,17 +148,33 @@ def validate_item(self): frappe.throw(_("Item {0} must be a non-stock item").format(self.item_code)) def validate_cost_center(self): - if not self.cost_center: - return - - cost_center_company = frappe.db.get_value("Cost Center", self.cost_center, "company") - if cost_center_company != self.company: - frappe.throw( - _("Selected Cost Center {} doesn't belongs to {}").format( - frappe.bold(self.cost_center), frappe.bold(self.company) - ), - title=_("Invalid Cost Center"), + if self.cost_center: + cost_center_company, cost_center_is_group = frappe.db.get_value( + "Cost Center", self.cost_center, ["company", "is_group"] ) + if cost_center_company != self.company: + frappe.throw( + _("Cost Center {} doesn't belong to Company {}").format( + frappe.bold(self.cost_center), frappe.bold(self.company) + ), + title=_("Invalid Cost Center"), + ) + if cost_center_is_group: + frappe.throw( + _( + "Cost Center {} is a group cost center and group cost centers cannot be used in transactions" + ).format(frappe.bold(self.cost_center)), + title=_("Invalid Cost Center"), + ) + + else: + if not frappe.get_cached_value("Company", self.company, "depreciation_cost_center"): + frappe.throw( + _( + "Please set a Cost Center for the Asset or set an Asset Depreciation Cost Center for the Company {}" + ).format(frappe.bold(self.company)), + title=_("Missing Cost Center"), + ) def validate_in_use_date(self): if not self.available_for_use_date: From 46bb309b8a56edf800cc4ca6b418a3ffc339d99c Mon Sep 17 00:00:00 2001 From: Gursheen Anand Date: Tue, 1 Aug 2023 23:22:49 +0530 Subject: [PATCH 052/501] fix: check root type only when not none (cherry picked from commit cd98be6088acc0a9d6170c88da7bdf440306f425) --- .../consolidated_financial_statement.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/erpnext/accounts/report/consolidated_financial_statement/consolidated_financial_statement.py b/erpnext/accounts/report/consolidated_financial_statement/consolidated_financial_statement.py index ebf43d13b856..cdcf73f66200 100644 --- a/erpnext/accounts/report/consolidated_financial_statement/consolidated_financial_statement.py +++ b/erpnext/accounts/report/consolidated_financial_statement/consolidated_financial_statement.py @@ -659,11 +659,12 @@ def set_gl_entries_by_account( & (gle.posting_date <= to_date) & (account.lft >= root_lft) & (account.rgt <= root_rgt) - & (account.root_type == root_type) ) .orderby(gle.account, gle.posting_date) ) + if root_type: + query = query.where(account.root_type == root_type) additional_conditions = get_additional_conditions(from_date, ignore_closing_entries, filters, d) if additional_conditions: query = query.where(Criterion.all(additional_conditions)) From 47d0e7699948614f3594ac5b810265a627693f06 Mon Sep 17 00:00:00 2001 From: Gursheen Anand Date: Tue, 1 Aug 2023 23:24:18 +0530 Subject: [PATCH 053/501] test: balance sheet report (cherry picked from commit 002bf77314a71c02ad164e328a3a9cc9ec9714e4) --- .../balance_sheet/test_balance_sheet.py | 51 +++++++++++++++++++ 1 file changed, 51 insertions(+) create mode 100644 erpnext/accounts/report/balance_sheet/test_balance_sheet.py diff --git a/erpnext/accounts/report/balance_sheet/test_balance_sheet.py b/erpnext/accounts/report/balance_sheet/test_balance_sheet.py new file mode 100644 index 000000000000..3cb6efebee35 --- /dev/null +++ b/erpnext/accounts/report/balance_sheet/test_balance_sheet.py @@ -0,0 +1,51 @@ +# Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and Contributors +# MIT License. See license.txt + +import frappe +from frappe.tests.utils import FrappeTestCase +from frappe.utils import today + +from erpnext.accounts.report.balance_sheet.balance_sheet import execute + + +class TestBalanceSheet(FrappeTestCase): + def test_balance_sheet(self): + from erpnext.accounts.doctype.purchase_invoice.test_purchase_invoice import make_purchase_invoice + from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import ( + create_sales_invoice, + make_sales_invoice, + ) + from erpnext.accounts.utils import get_fiscal_year + + frappe.db.sql("delete from `tabPurchase Invoice` where company='_Test Company 6'") + frappe.db.sql("delete from `tabSales Invoice` where company='_Test Company 6'") + frappe.db.sql("delete from `tabGL Entry` where company='_Test Company 6'") + + pi = make_purchase_invoice( + company="_Test Company 6", + warehouse="Finished Goods - _TC6", + expense_account="Cost of Goods Sold - _TC6", + cost_center="Main - _TC6", + qty=10, + rate=100, + ) + si = create_sales_invoice( + company="_Test Company 6", + debit_to="Debtors - _TC6", + income_account="Sales - _TC6", + cost_center="Main - _TC6", + qty=5, + rate=110, + ) + filters = frappe._dict( + company="_Test Company 6", + period_start_date=today(), + period_end_date=today(), + periodicity="Yearly", + ) + result = execute(filters)[1] + for account_dict in result: + if account_dict.get("account") == "Current Liabilities - _TC6": + self.assertEqual(account_dict.total, 1000) + if account_dict.get("account") == "Current Assets - _TC6": + self.assertEqual(account_dict.total, 550) From 1ca9aca0d5e2002e1175581df05aa1700bd745ab Mon Sep 17 00:00:00 2001 From: Gursheen Anand Date: Wed, 26 Jul 2023 16:42:06 +0530 Subject: [PATCH 054/501] fix: fetch ple with party type employee in AP (cherry picked from commit c47a37c3ab1d4b4e1ebb1c27579cf2a9320db1fb) --- .../accounts_receivable.py | 21 +++++++++++++++---- 1 file changed, 17 insertions(+), 4 deletions(-) diff --git a/erpnext/accounts/report/accounts_receivable/accounts_receivable.py b/erpnext/accounts/report/accounts_receivable/accounts_receivable.py index 30f7fb38c5f9..93c3fb334035 100755 --- a/erpnext/accounts/report/accounts_receivable/accounts_receivable.py +++ b/erpnext/accounts/report/accounts_receivable/accounts_receivable.py @@ -421,6 +421,10 @@ def set_party_details(self, row): # customer / supplier name party_details = self.get_party_details(row.party) or {} row.update(party_details) + if row.voucher_type == "Expense Claim": + row.party_type = "Employee" + else: + row.party_type = self.party_type if self.filters.get(scrub(self.filters.party_type)): row.currency = row.account_currency else: @@ -747,7 +751,10 @@ def get_sales_invoices_or_customers_based_on_sales_person(self): def prepare_conditions(self): self.qb_selection_filter = [] party_type_field = scrub(self.party_type) - self.qb_selection_filter.append(self.ple.party_type == self.party_type) + if self.party_type == "Supplier": + self.qb_selection_filter.append(self.ple.party_type.isin([self.party_type, "Employee"])) + else: + self.qb_selection_filter.append(self.ple.party_type == self.party_type) self.add_common_filters(party_type_field=party_type_field) @@ -901,10 +908,16 @@ def get_columns(self): self.columns = [] self.add_column("Posting Date", fieldtype="Date") self.add_column( - label=_(self.party_type), + label="Party Type", + fieldname="party_type", + fieldtype="Data", + width=100, + ) + self.add_column( + label="Party", fieldname="party", - fieldtype="Link", - options=self.party_type, + fieldtype="Dynamic Link", + options="party_type", width=180, ) self.add_column( From 674dba8cd7ef97103c706a19066872622f965bf2 Mon Sep 17 00:00:00 2001 From: Gursheen Anand Date: Fri, 28 Jul 2023 11:41:03 +0530 Subject: [PATCH 055/501] fix: fetch ple for all party types (cherry picked from commit fd5c4e0a64a4a8972bf70fd1358767ab1fb86785) --- .../accounts_payable/accounts_payable.py | 2 +- .../accounts_receivable.py | 98 +++++++++++-------- 2 files changed, 56 insertions(+), 44 deletions(-) diff --git a/erpnext/accounts/report/accounts_payable/accounts_payable.py b/erpnext/accounts/report/accounts_payable/accounts_payable.py index 7b1999491130..8279afbc2bc2 100644 --- a/erpnext/accounts/report/accounts_payable/accounts_payable.py +++ b/erpnext/accounts/report/accounts_payable/accounts_payable.py @@ -7,7 +7,7 @@ def execute(filters=None): args = { - "party_type": "Supplier", + "account_type": "Payable", "naming_by": ["Buying Settings", "supp_master_name"], } return ReceivablePayableReport(filters).run(args) diff --git a/erpnext/accounts/report/accounts_receivable/accounts_receivable.py b/erpnext/accounts/report/accounts_receivable/accounts_receivable.py index 93c3fb334035..5b92dcd717f9 100755 --- a/erpnext/accounts/report/accounts_receivable/accounts_receivable.py +++ b/erpnext/accounts/report/accounts_receivable/accounts_receivable.py @@ -34,7 +34,7 @@ def execute(filters=None): args = { - "party_type": "Customer", + "account_type": "Receivable", "naming_by": ["Selling Settings", "cust_master_name"], } return ReceivablePayableReport(filters).run(args) @@ -70,8 +70,11 @@ def set_defaults(self): "Company", self.filters.get("company"), "default_currency" ) self.currency_precision = get_currency_precision() or 2 - self.dr_or_cr = "debit" if self.filters.party_type == "Customer" else "credit" - self.party_type = self.filters.party_type + self.dr_or_cr = "debit" if self.filters.account_type == "Receivable" else "credit" + self.account_type = self.filters.account_type + self.party_type = frappe.db.get_all( + "Party Type", {"account_type": self.account_type}, pluck="name" + ) self.party_details = {} self.invoices = set() self.skip_total_row = 0 @@ -197,6 +200,7 @@ def get_voucher_balance(self, ple): # no invoice, this is an invoice / stand-alone payment / credit note row = self.voucher_balance.get((ple.voucher_type, ple.voucher_no, ple.party)) + row.party_type = ple.party_type return row def update_voucher_balance(self, ple): @@ -207,8 +211,9 @@ def update_voucher_balance(self, ple): return # amount in "Party Currency", if its supplied. If not, amount in company currency - if self.filters.get(scrub(self.party_type)): - amount = ple.amount_in_account_currency + for party_type in self.party_type: + if self.filters.get(scrub(party_type)): + amount = ple.amount_in_account_currency else: amount = ple.amount amount_in_account_currency = ple.amount_in_account_currency @@ -362,7 +367,7 @@ def build_delivery_note_map(self): def get_invoice_details(self): self.invoice_details = frappe._dict() - if self.party_type == "Customer": + if self.account_type == "Receivable": si_list = frappe.db.sql( """ select name, due_date, po_no @@ -390,7 +395,7 @@ def get_invoice_details(self): d.sales_person ) - if self.party_type == "Supplier": + if self.account_type == "Payable": for pi in frappe.db.sql( """ select name, due_date, bill_no, bill_date @@ -421,12 +426,10 @@ def set_party_details(self, row): # customer / supplier name party_details = self.get_party_details(row.party) or {} row.update(party_details) - if row.voucher_type == "Expense Claim": - row.party_type = "Employee" - else: - row.party_type = self.party_type - if self.filters.get(scrub(self.filters.party_type)): - row.currency = row.account_currency + for party_type in self.party_type: + if self.filters.get(scrub(party_type)): + row.currency = row.account_currency + break else: row.currency = self.company_currency @@ -552,7 +555,7 @@ def get_future_payments_from_payment_entry(self): where payment_entry.docstatus < 2 and payment_entry.posting_date > %s - and payment_entry.party_type = %s + and payment_entry.party_type in %s """, (self.filters.report_date, self.party_type), as_dict=1, @@ -562,11 +565,11 @@ def get_future_payments_from_journal_entry(self): if self.filters.get("party"): amount_field = ( "jea.debit_in_account_currency - jea.credit_in_account_currency" - if self.party_type == "Supplier" + if self.account_type == "Payable" else "jea.credit_in_account_currency - jea.debit_in_account_currency" ) else: - amount_field = "jea.debit - " if self.party_type == "Supplier" else "jea.credit" + amount_field = "jea.debit - " if self.account_type == "Payable" else "jea.credit" return frappe.db.sql( """ @@ -584,7 +587,7 @@ def get_future_payments_from_journal_entry(self): where je.docstatus < 2 and je.posting_date > %s - and jea.party_type = %s + and jea.party_type in %s and jea.reference_name is not null and jea.reference_name != '' group by je.name, jea.reference_name having future_amount > 0 @@ -623,13 +626,17 @@ def allocate_future_payments(self, row): row.future_ref = ", ".join(row.future_ref) def get_return_entries(self): - doctype = "Sales Invoice" if self.party_type == "Customer" else "Purchase Invoice" + doctype = "Sales Invoice" if self.account_type == "Receivable" else "Purchase Invoice" filters = {"is_return": 1, "docstatus": 1, "company": self.filters.company} - party_field = scrub(self.filters.party_type) - if self.filters.get(party_field): - filters.update({party_field: self.filters.get(party_field)}) + or_filters = {} + for party_type in self.party_type: + party_field = scrub(party_type) + if self.filters.get(party_field): + or_filters.update({party_field: self.filters.get(party_field)}) self.return_entries = frappe._dict( - frappe.get_all(doctype, filters, ["name", "return_against"], as_list=1) + frappe.get_all( + doctype, filters=filters, or_filters=or_filters, fields=["name", "return_against"], as_list=1 + ) ) def set_ageing(self, row): @@ -720,6 +727,7 @@ def get_ple_entries(self): ) .where(ple.delinked == 0) .where(Criterion.all(self.qb_selection_filter)) + .where(Criterion.any(self.or_filters)) ) if self.filters.get("group_by_party"): @@ -750,19 +758,18 @@ def get_sales_invoices_or_customers_based_on_sales_person(self): def prepare_conditions(self): self.qb_selection_filter = [] - party_type_field = scrub(self.party_type) - if self.party_type == "Supplier": - self.qb_selection_filter.append(self.ple.party_type.isin([self.party_type, "Employee"])) - else: - self.qb_selection_filter.append(self.ple.party_type == self.party_type) + self.or_filters = [] + for party_type in self.party_type: + party_type_field = scrub(party_type) + self.or_filters.append(self.ple.party_type == party_type) - self.add_common_filters(party_type_field=party_type_field) + self.add_common_filters(party_type_field=party_type_field) - if party_type_field == "customer": - self.add_customer_filters() + if party_type_field == "customer": + self.add_customer_filters() - elif party_type_field == "supplier": - self.add_supplier_filters() + elif party_type_field == "supplier": + self.add_supplier_filters() if self.filters.cost_center: self.get_cost_center_conditions() @@ -791,11 +798,10 @@ def add_common_filters(self, party_type_field): self.qb_selection_filter.append(self.ple.account == self.filters.party_account) else: # get GL with "receivable" or "payable" account_type - account_type = "Receivable" if self.party_type == "Customer" else "Payable" accounts = [ d.name for d in frappe.get_all( - "Account", filters={"account_type": account_type, "company": self.filters.company} + "Account", filters={"account_type": self.account_type, "company": self.filters.company} ) ] @@ -885,7 +891,7 @@ def is_invoice(self, ple): def get_party_details(self, party): if not party in self.party_details: - if self.party_type == "Customer": + if self.account_type == "Receivable": fields = ["customer_name", "territory", "customer_group", "customer_primary_contact"] if self.filters.get("sales_partner"): @@ -921,7 +927,7 @@ def get_columns(self): width=180, ) self.add_column( - label="Receivable Account" if self.party_type == "Customer" else "Payable Account", + label=self.account_type + " Account", fieldname="party_account", fieldtype="Link", options="Account", @@ -929,13 +935,19 @@ def get_columns(self): ) if self.party_naming_by == "Naming Series": + if self.account_type == "Payable": + label = "Supplier Name" + fieldname = "supplier_name" + else: + label = "Customer Name" + fieldname = "customer_name" self.add_column( - _("{0} Name").format(self.party_type), - fieldname=scrub(self.party_type) + "_name", + label=label, + fieldname=fieldname, fieldtype="Data", ) - if self.party_type == "Customer": + if self.account_type == "Receivable": self.add_column( _("Customer Contact"), fieldname="customer_primary_contact", @@ -955,7 +967,7 @@ def get_columns(self): self.add_column(label="Due Date", fieldtype="Date") - if self.party_type == "Supplier": + if self.account_type == "Payable": self.add_column(label=_("Bill No"), fieldname="bill_no", fieldtype="Data") self.add_column(label=_("Bill Date"), fieldname="bill_date", fieldtype="Date") @@ -965,7 +977,7 @@ def get_columns(self): self.add_column(_("Invoiced Amount"), fieldname="invoiced") self.add_column(_("Paid Amount"), fieldname="paid") - if self.party_type == "Customer": + if self.account_type == "Receivable": self.add_column(_("Credit Note"), fieldname="credit_note") else: # note: fieldname is still `credit_note` @@ -983,7 +995,7 @@ def get_columns(self): self.add_column(label=_("Future Payment Amount"), fieldname="future_amount") self.add_column(label=_("Remaining Balance"), fieldname="remaining_balance") - if self.filters.party_type == "Customer": + if self.filters.account_type == "Receivable": self.add_column(label=_("Customer LPO"), fieldname="po_no", fieldtype="Data") # comma separated list of linked delivery notes @@ -1004,7 +1016,7 @@ def get_columns(self): if self.filters.sales_partner: self.add_column(label=_("Sales Partner"), fieldname="default_sales_partner", fieldtype="Data") - if self.filters.party_type == "Supplier": + if self.filters.account_type == "Payable": self.add_column( label=_("Supplier Group"), fieldname="supplier_group", From 769d7d7554ff808f0d14b4ed7e276ef44ea15259 Mon Sep 17 00:00:00 2001 From: Gursheen Anand Date: Fri, 28 Jul 2023 14:51:28 +0530 Subject: [PATCH 056/501] fix: AP and AR summary (cherry picked from commit e355dea4b550fcf64450876652f852f6a6c529fd) --- erpnext/accounts/party.py | 40 +++++++++------- .../accounts_payable_summary.py | 2 +- .../accounts_receivable_summary.py | 48 ++++++++++++++----- 3 files changed, 59 insertions(+), 31 deletions(-) diff --git a/erpnext/accounts/party.py b/erpnext/accounts/party.py index 56d33b758b7f..b53e282bfc5f 100644 --- a/erpnext/accounts/party.py +++ b/erpnext/accounts/party.py @@ -14,6 +14,7 @@ from frappe.contacts.doctype.contact.contact import get_contact_details from frappe.core.doctype.user_permission.user_permission import get_permitted_documents from frappe.model.utils import get_fetch_values +from frappe.query_builder.functions import Date, Sum from frappe.utils import ( add_days, add_months, @@ -883,32 +884,35 @@ def get_party_shipping_address(doctype: str, name: str) -> Optional[str]: def get_partywise_advanced_payment_amount( - party_type, posting_date=None, future_payment=0, company=None, party=None + party_type, posting_date=None, future_payment=0, company=None, party=None, account_type=None ): - cond = "1=1" + gle = frappe.qb.DocType("GL Entry") + query = ( + frappe.qb.from_(gle) + .select(gle.party) + .where( + (gle.party_type.isin(party_type)) & (gle.against_voucher == None) & (gle.is_cancelled == 0) + ) + .groupby(gle.party) + ) + if account_type == "Receivable": + query = query.select(Sum(gle.credit).as_("amount")) + else: + query = query.select(Sum(gle.debit).as_("amount")) + if posting_date: if future_payment: - cond = "(posting_date <= '{0}' OR DATE(creation) <= '{0}')" "".format(posting_date) + query = query.where((gle.posting_date <= posting_date) | (Date(gle.creation) <= posting_date)) else: - cond = "posting_date <= '{0}'".format(posting_date) + query = query.where(gle.posting_date <= posting_date) if company: - cond += "and company = {0}".format(frappe.db.escape(company)) + query = query.where(gle.company == company) if party: - cond += "and party = {0}".format(frappe.db.escape(party)) - - data = frappe.db.sql( - """ SELECT party, sum({0}) as amount - FROM `tabGL Entry` - WHERE - party_type = %s and against_voucher is null - and is_cancelled = 0 - and {1} GROUP BY party""".format( - ("credit") if party_type == "Customer" else "debit", cond - ), - party_type, - ) + query = query.where(gle.party == party) + + data = query.run(as_dict=True) if data: return frappe._dict(data) diff --git a/erpnext/accounts/report/accounts_payable_summary/accounts_payable_summary.py b/erpnext/accounts/report/accounts_payable_summary/accounts_payable_summary.py index 65fe1de5689d..834c83c38e9a 100644 --- a/erpnext/accounts/report/accounts_payable_summary/accounts_payable_summary.py +++ b/erpnext/accounts/report/accounts_payable_summary/accounts_payable_summary.py @@ -9,7 +9,7 @@ def execute(filters=None): args = { - "party_type": "Supplier", + "account_type": "Payable", "naming_by": ["Buying Settings", "supp_master_name"], } return AccountsReceivableSummary(filters).run(args) diff --git a/erpnext/accounts/report/accounts_receivable_summary/accounts_receivable_summary.py b/erpnext/accounts/report/accounts_receivable_summary/accounts_receivable_summary.py index 9c01b1a4980e..3aa1ae71045c 100644 --- a/erpnext/accounts/report/accounts_receivable_summary/accounts_receivable_summary.py +++ b/erpnext/accounts/report/accounts_receivable_summary/accounts_receivable_summary.py @@ -12,7 +12,7 @@ def execute(filters=None): args = { - "party_type": "Customer", + "account_type": "Receivable", "naming_by": ["Selling Settings", "cust_master_name"], } @@ -21,7 +21,10 @@ def execute(filters=None): class AccountsReceivableSummary(ReceivablePayableReport): def run(self, args): - self.party_type = args.get("party_type") + self.account_type = args.get("account_type") + self.party_type = frappe.db.get_all( + "Party Type", {"account_type": self.account_type}, pluck="name" + ) self.party_naming_by = frappe.db.get_value( args.get("naming_by")[0], None, args.get("naming_by")[1] ) @@ -35,13 +38,19 @@ def get_data(self, args): self.get_party_total(args) + party = None + for party_type in self.party_type: + if self.filters.get(scrub(party_type)): + party = self.filters.get(scrub(party_type)) + party_advance_amount = ( get_partywise_advanced_payment_amount( self.party_type, self.filters.report_date, self.filters.show_future_payments, self.filters.company, - party=self.filters.get(scrub(self.party_type)), + party=party, + account_type=self.account_type, ) or {} ) @@ -57,9 +66,13 @@ def get_data(self, args): row.party = party if self.party_naming_by == "Naming Series": - row.party_name = frappe.get_cached_value( - self.party_type, party, scrub(self.party_type) + "_name" - ) + if self.account_type == "Payable": + doctype = "Supplier" + fieldname = "supplier_name" + else: + doctype = "Customer" + fieldname = "customer_name" + row.party_name = frappe.get_cached_value(doctype, party, fieldname) row.update(party_dict) @@ -93,6 +106,7 @@ def get_party_total(self, args): # set territory, customer_group, sales person etc self.set_party_details(d) + self.party_total[d.party].update({"party_type": d.party_type}) def init_party_total(self, row): self.party_total.setdefault( @@ -131,17 +145,27 @@ def set_party_details(self, row): def get_columns(self): self.columns = [] self.add_column( - label=_(self.party_type), + label="Party Type", + fieldname="party_type", + fieldtype="Data", + width=100, + ) + self.add_column( + label="Party", fieldname="party", - fieldtype="Link", - options=self.party_type, + fieldtype="Dynamic Link", + options="party_type", width=180, ) if self.party_naming_by == "Naming Series": - self.add_column(_("{0} Name").format(self.party_type), fieldname="party_name", fieldtype="Data") + self.add_column( + label="Supplier Name" if self.account_type == "Payable" else "Customer Name", + fieldname="party_name", + fieldtype="Data", + ) - credit_debit_label = "Credit Note" if self.party_type == "Customer" else "Debit Note" + credit_debit_label = "Credit Note" if self.account_type == "Receivable" else "Debit Note" self.add_column(_("Advance Amount"), fieldname="advance") self.add_column(_("Invoiced Amount"), fieldname="invoiced") @@ -159,7 +183,7 @@ def get_columns(self): self.add_column(label=_("Future Payment Amount"), fieldname="future_amount") self.add_column(label=_("Remaining Balance"), fieldname="remaining_balance") - if self.party_type == "Customer": + if self.account_type == "Receivable": self.add_column( label=_("Territory"), fieldname="territory", fieldtype="Link", options="Territory" ) From a79b30e45f49ea90e246dd9866aee5dcbcc32f34 Mon Sep 17 00:00:00 2001 From: Gursheen Anand Date: Fri, 28 Jul 2023 16:01:30 +0530 Subject: [PATCH 057/501] refactor: future payments query (cherry picked from commit f5761e79657f744aff6b1ed964f9ffdf6e7d5a9f) --- erpnext/accounts/party.py | 2 +- .../accounts_receivable.py | 110 +++++++++--------- 2 files changed, 57 insertions(+), 55 deletions(-) diff --git a/erpnext/accounts/party.py b/erpnext/accounts/party.py index b53e282bfc5f..3aea40316b5a 100644 --- a/erpnext/accounts/party.py +++ b/erpnext/accounts/party.py @@ -891,7 +891,7 @@ def get_partywise_advanced_payment_amount( frappe.qb.from_(gle) .select(gle.party) .where( - (gle.party_type.isin(party_type)) & (gle.against_voucher == None) & (gle.is_cancelled == 0) + (gle.party_type.isin(party_type)) & (gle.against_voucher.isnull()) & (gle.is_cancelled == 0) ) .groupby(gle.party) ) diff --git a/erpnext/accounts/report/accounts_receivable/accounts_receivable.py b/erpnext/accounts/report/accounts_receivable/accounts_receivable.py index 5b92dcd717f9..11bbb6f1e43a 100755 --- a/erpnext/accounts/report/accounts_receivable/accounts_receivable.py +++ b/erpnext/accounts/report/accounts_receivable/accounts_receivable.py @@ -7,7 +7,7 @@ import frappe from frappe import _, qb, scrub from frappe.query_builder import Criterion -from frappe.query_builder.functions import Date +from frappe.query_builder.functions import Date, Sum from frappe.utils import cint, cstr, flt, getdate, nowdate from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import ( @@ -539,65 +539,67 @@ def get_future_payments(self): self.future_payments.setdefault((d.invoice_no, d.party), []).append(d) def get_future_payments_from_payment_entry(self): - return frappe.db.sql( - """ - select - ref.reference_name as invoice_no, - payment_entry.party, - payment_entry.party_type, - payment_entry.posting_date as future_date, - ref.allocated_amount as future_amount, - payment_entry.reference_no as future_ref - from - `tabPayment Entry` as payment_entry inner join `tabPayment Entry Reference` as ref - on - (ref.parent = payment_entry.name) - where - payment_entry.docstatus < 2 - and payment_entry.posting_date > %s - and payment_entry.party_type in %s - """, - (self.filters.report_date, self.party_type), - as_dict=1, - ) - - def get_future_payments_from_journal_entry(self): - if self.filters.get("party"): - amount_field = ( - "jea.debit_in_account_currency - jea.credit_in_account_currency" - if self.account_type == "Payable" - else "jea.credit_in_account_currency - jea.debit_in_account_currency" + pe = frappe.qb.DocType("Payment Entry") + pe_ref = frappe.qb.DocType("Payment Entry Reference") + return ( + frappe.qb.from_(pe) + .inner_join(pe_ref) + .on(pe_ref.parent == pe.name) + .select( + (pe_ref.reference_name).as_("invoice_no"), + pe.party, + pe.party_type, + (pe.posting_date).as_("future_date"), + (pe_ref.allocated_amount).as_("future_amount"), + (pe.reference_no).as_("future_ref"), ) - else: - amount_field = "jea.debit - " if self.account_type == "Payable" else "jea.credit" + .where( + (pe.docstatus < 2) + & (pe.posting_date > self.filters.report_date) + & (pe.party_type.isin(self.party_type)) + ) + ).run(as_dict=True) - return frappe.db.sql( - """ - select - jea.reference_name as invoice_no, + def get_future_payments_from_journal_entry(self): + je = frappe.qb.DocType("Journal Entry") + jea = frappe.qb.DocType("Journal Entry Account") + query = ( + frappe.qb.from_(je) + .inner_join(jea) + .on(jea.parent == je.name) + .select( + jea.reference_name.as_("invoice_no"), jea.party, jea.party_type, - je.posting_date as future_date, - sum('{0}') as future_amount, - je.cheque_no as future_ref - from - `tabJournal Entry` as je inner join `tabJournal Entry Account` as jea - on - (jea.parent = je.name) - where - je.docstatus < 2 - and je.posting_date > %s - and jea.party_type in %s - and jea.reference_name is not null and jea.reference_name != '' - group by je.name, jea.reference_name - having future_amount > 0 - """.format( - amount_field - ), - (self.filters.report_date, self.party_type), - as_dict=1, + je.posting_date.as_("future_date"), + je.cheque_no.as_("future_ref"), + ) + .where( + (je.docstatus < 2) + & (je.posting_date > self.filters.report_date) + & (jea.party_type.isin(self.party_type)) + & (jea.reference_name.isnotnull()) + & (jea.reference_name != "") + ) ) + if self.filters.get("party"): + if self.account_type == "Payable": + query = query.select( + Sum(jea.debit_in_account_currency - jea.credit_in_account_currency).as_("future_amount") + ) + else: + query = query.select( + Sum(jea.credit_in_account_currency - jea.debit_in_account_currency).as_("future_amount") + ) + else: + query = query.select( + Sum(jea.debit if self.account_type == "Payable" else jea.credit).as_("future_amount") + ) + + query = query.having(qb.Field("future_amount") > 0) + return query.run(as_dict=True) + def allocate_future_payments(self, row): # future payments are captured in additional columns # this method allocates pending future payments against a voucher to From cf2a3e2fc50d7822a2588bc01a595b00f53f75ab Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Fri, 4 Aug 2023 17:52:29 +0530 Subject: [PATCH 058/501] Contact Doctype don't have any field `job_title`. (#36156) fix: Contact Doctype doesn't have any field called `job_title` fix: Contact Doctype doesn't have any field called `job_title` (cherry picked from commit 49be7407369f33419513475f00b1ca8da9efea17) Co-authored-by: Sumit Jain <59503001+sumitjain236@users.noreply.github.com> --- erpnext/crm/doctype/lead/lead.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/crm/doctype/lead/lead.py b/erpnext/crm/doctype/lead/lead.py index 08ea4b06e7c3..460974972c58 100644 --- a/erpnext/crm/doctype/lead/lead.py +++ b/erpnext/crm/doctype/lead/lead.py @@ -185,7 +185,7 @@ def create_contact(self): "last_name": self.last_name, "salutation": self.salutation, "gender": self.gender, - "job_title": self.job_title, + "designation": self.job_title, "company_name": self.company_name, } ) From 0d7a4b6ff64dbe5cca0cc6999557c2a9fdbd54fb Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Sat, 5 Aug 2023 19:31:55 +0530 Subject: [PATCH 059/501] fix(ux): add `Ordered Qty` column in Get Items From > MR (backport #36486) (#36505) fix(ux): add `Ordered Qty` column in Get Items From > MR (#36486) (cherry picked from commit e17949976442f921335ab1961928ddec37aeffef) Co-authored-by: s-aga-r --- erpnext/buying/doctype/purchase_order/purchase_order.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/buying/doctype/purchase_order/purchase_order.js b/erpnext/buying/doctype/purchase_order/purchase_order.js index 8fa8f305549d..52a0f4ac2a28 100644 --- a/erpnext/buying/doctype/purchase_order/purchase_order.js +++ b/erpnext/buying/doctype/purchase_order/purchase_order.js @@ -369,7 +369,7 @@ erpnext.buying.PurchaseOrderController = class PurchaseOrderController extends e }, allow_child_item_selection: true, child_fieldname: "items", - child_columns: ["item_code", "qty"] + child_columns: ["item_code", "qty", "ordered_qty"] }) }, __("Get Items From")); From 67393694de3ee4f37e40c8fe1e3a9963acc0ed8b Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Sat, 5 Aug 2023 22:43:44 +0530 Subject: [PATCH 060/501] fix(accounts): Translate columns in AP/AR report (#36503) fix(accounts): Translate columns in AP/AR report (#36503) (cherry picked from commit 559d914c0bffb615b9b53083f6c9ca9fd2ca9a3d) Co-authored-by: Corentin Flr <10946971+cogk@users.noreply.github.com> --- .../accounts_receivable_summary.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/erpnext/accounts/report/accounts_receivable_summary/accounts_receivable_summary.py b/erpnext/accounts/report/accounts_receivable_summary/accounts_receivable_summary.py index 3aa1ae71045c..da4c9dabbf66 100644 --- a/erpnext/accounts/report/accounts_receivable_summary/accounts_receivable_summary.py +++ b/erpnext/accounts/report/accounts_receivable_summary/accounts_receivable_summary.py @@ -145,13 +145,13 @@ def set_party_details(self, row): def get_columns(self): self.columns = [] self.add_column( - label="Party Type", + label=_("Party Type"), fieldname="party_type", fieldtype="Data", width=100, ) self.add_column( - label="Party", + label=_("Party"), fieldname="party", fieldtype="Dynamic Link", options="party_type", @@ -160,7 +160,7 @@ def get_columns(self): if self.party_naming_by == "Naming Series": self.add_column( - label="Supplier Name" if self.account_type == "Payable" else "Customer Name", + label=_("Supplier Name") if self.account_type == "Payable" else _("Customer Name"), fieldname="party_name", fieldtype="Data", ) From 2216875bd6f22f695e19e9af6219628760b42a88 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Sat, 5 Aug 2023 22:44:10 +0530 Subject: [PATCH 061/501] fix: Lower deduction certificate for multi-company (#36491) fix: Lower deduction certificate for multi-company (#36491) (cherry picked from commit 96035b87d566948ac56bec2a49357fed199a59ba) Co-authored-by: Deepesh Garg --- .../lower_deduction_certificate/lower_deduction_certificate.py | 1 + 1 file changed, 1 insertion(+) diff --git a/erpnext/regional/doctype/lower_deduction_certificate/lower_deduction_certificate.py b/erpnext/regional/doctype/lower_deduction_certificate/lower_deduction_certificate.py index cc223e91bc84..6ae04c165c45 100644 --- a/erpnext/regional/doctype/lower_deduction_certificate/lower_deduction_certificate.py +++ b/erpnext/regional/doctype/lower_deduction_certificate/lower_deduction_certificate.py @@ -34,6 +34,7 @@ def validate_supplier_against_tax_category(self): "supplier": self.supplier, "tax_withholding_category": self.tax_withholding_category, "name": ("!=", self.name), + "company": self.company, }, ["name", "valid_from", "valid_upto"], as_dict=True, From a234b8932e34b7f726c54a720c3199ed1e3bfb64 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Sat, 5 Aug 2023 22:58:55 +0530 Subject: [PATCH 062/501] fix: Tax withholding against order via Payment Entry (#36493) fix: Tax withholding against order via Payment Entry (#36493) * fix: Tax withholding against order via Payment Entry * test: Add test case * fix: Nonetype exceptions (cherry picked from commit 93767eb7fcaadd5d7e921c3e255b495ea02b094c) Co-authored-by: Deepesh Garg --- .../doctype/payment_entry/payment_entry.py | 18 ++++++++- .../test_tax_withholding_category.py | 37 +++++++++++++++++++ 2 files changed, 54 insertions(+), 1 deletion(-) diff --git a/erpnext/accounts/doctype/payment_entry/payment_entry.py b/erpnext/accounts/doctype/payment_entry/payment_entry.py index bece3f9c2807..580608d5a372 100644 --- a/erpnext/accounts/doctype/payment_entry/payment_entry.py +++ b/erpnext/accounts/doctype/payment_entry/payment_entry.py @@ -636,7 +636,9 @@ def set_tax_withholding(self): if not self.apply_tax_withholding_amount: return - net_total = self.paid_amount + order_amount = self.get_order_net_total() + + net_total = flt(order_amount) + flt(self.unallocated_amount) # Adding args as purchase invoice to get TDS amount args = frappe._dict( @@ -681,6 +683,20 @@ def set_tax_withholding(self): for d in to_remove: self.remove(d) + def get_order_net_total(self): + if self.party_type == "Supplier": + doctype = "Purchase Order" + else: + doctype = "Sales Order" + + docnames = [d.reference_name for d in self.references if d.reference_doctype == doctype] + + tax_withholding_net_total = frappe.db.get_value( + doctype, {"name": ["in", docnames]}, ["sum(base_tax_withholding_net_total)"] + ) + + return tax_withholding_net_total + def apply_taxes(self): self.initialize_taxes() self.determine_exclusive_rate() diff --git a/erpnext/accounts/doctype/tax_withholding_category/test_tax_withholding_category.py b/erpnext/accounts/doctype/tax_withholding_category/test_tax_withholding_category.py index ac84217e6f97..f8e0e2992f74 100644 --- a/erpnext/accounts/doctype/tax_withholding_category/test_tax_withholding_category.py +++ b/erpnext/accounts/doctype/tax_withholding_category/test_tax_withholding_category.py @@ -321,6 +321,42 @@ def test_tds_calculation_on_net_total_partial_tds(self): for d in reversed(orders): d.cancel() + def test_tds_deduction_for_po_via_payment_entry(self): + from erpnext.accounts.doctype.payment_entry.payment_entry import get_payment_entry + + frappe.db.set_value( + "Supplier", "Test TDS Supplier8", "tax_withholding_category", "Cumulative Threshold TDS" + ) + order = create_purchase_order(supplier="Test TDS Supplier8", rate=40000, do_not_save=True) + + # Add some tax on the order + order.append( + "taxes", + { + "category": "Total", + "charge_type": "Actual", + "account_head": "_Test Account VAT - _TC", + "cost_center": "Main - _TC", + "tax_amount": 8000, + "description": "Test", + "add_deduct_tax": "Add", + }, + ) + + order.save() + + order.apply_tds = 1 + order.tax_withholding_category = "Cumulative Threshold TDS" + order.submit() + + self.assertEqual(order.taxes[0].tax_amount, 4000) + + payment = get_payment_entry(order.doctype, order.name) + payment.apply_tax_withholding_amount = 1 + payment.tax_withholding_category = "Cumulative Threshold TDS" + payment.submit() + self.assertEqual(payment.taxes[0].tax_amount, 4000) + def test_multi_category_single_supplier(self): frappe.db.set_value( "Supplier", "Test TDS Supplier5", "tax_withholding_category", "Test Service Category" @@ -578,6 +614,7 @@ def create_records(): "Test TDS Supplier5", "Test TDS Supplier6", "Test TDS Supplier7", + "Test TDS Supplier8", ]: if frappe.db.exists("Supplier", name): continue From bdfbccd38eefd5c7a3385f3e25bcb3fcb442f2ae Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Sun, 6 Aug 2023 14:51:23 +0530 Subject: [PATCH 063/501] fix: get incoming rate instead of BOM rate (backport #36496) (#36506) * fix: get incoming rate instead of BOM rate (#36496) * fix: get incoming rate instead of BOM rate * test: add test case for SCR rm rate (cherry picked from commit 758b31d895f77ae04b075b65dce3bb427b70975a) # Conflicts: # erpnext/controllers/subcontracting_controller.py * chore: `conflicts` --------- Co-authored-by: s-aga-r --- .../controllers/subcontracting_controller.py | 2 +- .../test_subcontracting_receipt.py | 61 +++++++++++++++++++ 2 files changed, 62 insertions(+), 1 deletion(-) diff --git a/erpnext/controllers/subcontracting_controller.py b/erpnext/controllers/subcontracting_controller.py index 566135d75b93..b01b76d1ec91 100644 --- a/erpnext/controllers/subcontracting_controller.py +++ b/erpnext/controllers/subcontracting_controller.py @@ -460,7 +460,7 @@ def __add_supplied_item(self, item_row, bom_item, qty): "allow_zero_valuation": 1, } ) - rm_obj.rate = bom_item.rate if self.backflush_based_on == "BOM" else get_incoming_rate(args) + rm_obj.rate = get_incoming_rate(args) if self.doctype == self.subcontract_data.order_doctype: rm_obj.required_qty = qty diff --git a/erpnext/subcontracting/doctype/subcontracting_receipt/test_subcontracting_receipt.py b/erpnext/subcontracting/doctype/subcontracting_receipt/test_subcontracting_receipt.py index dfb72c335670..6c962531dfa1 100644 --- a/erpnext/subcontracting/doctype/subcontracting_receipt/test_subcontracting_receipt.py +++ b/erpnext/subcontracting/doctype/subcontracting_receipt/test_subcontracting_receipt.py @@ -594,6 +594,67 @@ def test_supplied_items_cost_after_reposting(self): self.assertNotEqual(scr.supplied_items[0].rate, prev_cost) self.assertEqual(scr.supplied_items[0].rate, sr.items[0].valuation_rate) + def test_subcontracting_receipt_raw_material_rate(self): + from erpnext.manufacturing.doctype.production_plan.test_production_plan import make_bom + + # Step - 1: Set Backflush Based On as "BOM" + set_backflush_based_on("BOM") + + # Step - 2: Create FG and RM Items + fg_item = make_item(properties={"is_stock_item": 1, "is_sub_contracted_item": 1}).name + rm_item1 = make_item(properties={"is_stock_item": 1}).name + rm_item2 = make_item(properties={"is_stock_item": 1}).name + + # Step - 3: Create BOM for FG Item + bom = make_bom(item=fg_item, raw_materials=[rm_item1, rm_item2]) + for rm_item in bom.items: + self.assertEqual(rm_item.rate, 0) + self.assertEqual(rm_item.amount, 0) + bom = bom.name + + # Step - 4: Create PO and SCO + service_items = [ + { + "warehouse": "_Test Warehouse - _TC", + "item_code": "Subcontracted Service Item 1", + "qty": 100, + "rate": 100, + "fg_item": fg_item, + "fg_item_qty": 100, + }, + ] + sco = get_subcontracting_order(service_items=service_items) + for rm_item in sco.supplied_items: + self.assertEqual(rm_item.rate, 0) + self.assertEqual(rm_item.amount, 0) + + # Step - 5: Inward Raw Materials + rm_items = get_rm_items(sco.supplied_items) + for rm_item in rm_items: + rm_item["rate"] = 100 + itemwise_details = make_stock_in_entry(rm_items=rm_items) + + # Step - 6: Transfer RM's to Subcontractor + se = make_stock_transfer_entry( + sco_no=sco.name, + rm_items=rm_items, + itemwise_details=copy.deepcopy(itemwise_details), + ) + for item in se.items: + self.assertEqual(item.qty, 100) + self.assertEqual(item.basic_rate, 100) + self.assertEqual(item.amount, item.qty * item.basic_rate) + + # Step - 7: Create Subcontracting Receipt + scr = make_subcontracting_receipt(sco.name) + scr.save() + scr.submit() + scr.load_from_db() + for rm_item in scr.supplied_items: + self.assertEqual(rm_item.consumed_qty, 100) + self.assertEqual(rm_item.rate, 100) + self.assertEqual(rm_item.amount, rm_item.consumed_qty * rm_item.rate) + def make_return_subcontracting_receipt(**args): args = frappe._dict(args) From 2d7d86039aa5487f841aa63247d2305a1f6d3b0b Mon Sep 17 00:00:00 2001 From: Anand Baburajan Date: Sun, 6 Aug 2023 23:34:02 +0530 Subject: [PATCH 064/501] chore: don't merge asset capitalization gl entries (#36514) --- .../doctype/asset_capitalization/asset_capitalization.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/erpnext/assets/doctype/asset_capitalization/asset_capitalization.py b/erpnext/assets/doctype/asset_capitalization/asset_capitalization.py index 5625fbb523ba..fb480420b97a 100644 --- a/erpnext/assets/doctype/asset_capitalization/asset_capitalization.py +++ b/erpnext/assets/doctype/asset_capitalization/asset_capitalization.py @@ -325,7 +325,7 @@ def make_gl_entries(self, gl_entries=None, from_repost=False): gl_entries = self.get_gl_entries() if gl_entries: - make_gl_entries(gl_entries, from_repost=from_repost) + make_gl_entries(gl_entries, merge_entries=False, from_repost=from_repost) elif self.docstatus == 2: make_reverse_gl_entries(voucher_type=self.doctype, voucher_no=self.name) @@ -355,9 +355,6 @@ def get_gl_entries( gl_entries, target_account, target_against, precision ) - if not self.stock_items and not self.service_items and self.are_all_asset_items_non_depreciable: - return [] - self.get_gl_entries_for_target_item(gl_entries, target_against, precision) return gl_entries From 8770aa59559a7a8235ba1bbe564462606a9e0918 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Mon, 7 Aug 2023 00:20:53 +0530 Subject: [PATCH 065/501] chore: don't merge asset capitalization gl entries (backport #36514) (#36516) chore: don't merge asset capitalization gl entries (#36514) (cherry picked from commit 2d7d86039aa5487f841aa63247d2305a1f6d3b0b) Co-authored-by: Anand Baburajan --- .../doctype/asset_capitalization/asset_capitalization.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/erpnext/assets/doctype/asset_capitalization/asset_capitalization.py b/erpnext/assets/doctype/asset_capitalization/asset_capitalization.py index 5625fbb523ba..fb480420b97a 100644 --- a/erpnext/assets/doctype/asset_capitalization/asset_capitalization.py +++ b/erpnext/assets/doctype/asset_capitalization/asset_capitalization.py @@ -325,7 +325,7 @@ def make_gl_entries(self, gl_entries=None, from_repost=False): gl_entries = self.get_gl_entries() if gl_entries: - make_gl_entries(gl_entries, from_repost=from_repost) + make_gl_entries(gl_entries, merge_entries=False, from_repost=from_repost) elif self.docstatus == 2: make_reverse_gl_entries(voucher_type=self.doctype, voucher_no=self.name) @@ -355,9 +355,6 @@ def get_gl_entries( gl_entries, target_account, target_against, precision ) - if not self.stock_items and not self.service_items and self.are_all_asset_items_non_depreciable: - return [] - self.get_gl_entries_for_target_item(gl_entries, target_against, precision) return gl_entries From f9981d1ff3527057cebc132eede74638b88ebfeb Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Mon, 7 Aug 2023 10:08:59 +0530 Subject: [PATCH 066/501] fix: use correct lang separator for frappe (backport #36519) (#36520) * fix: use correct lang separator for frappe (cherry picked from commit 0218ca538f8dd4342d1c9a989607441dc89fed22) * perf: defer babel import Only required when configuring but will get loaded everywhere (cherry picked from commit f574ac11ea8ee2f7f46916d93c6f5877350bc069) --------- Co-authored-by: Ankush Menat --- erpnext/setup/doctype/holiday_list/holiday_list.py | 5 +++-- .../setup/doctype/holiday_list/test_holiday_list.py | 12 ++++++++++++ 2 files changed, 15 insertions(+), 2 deletions(-) diff --git a/erpnext/setup/doctype/holiday_list/holiday_list.py b/erpnext/setup/doctype/holiday_list/holiday_list.py index 2ef4e655b2d7..6b6c8ba5be8e 100644 --- a/erpnext/setup/doctype/holiday_list/holiday_list.py +++ b/erpnext/setup/doctype/holiday_list/holiday_list.py @@ -6,7 +6,6 @@ from datetime import date import frappe -from babel import Locale from frappe import _, throw from frappe.model.document import Document from frappe.utils import formatdate, getdate, today @@ -169,4 +168,6 @@ def is_holiday(holiday_list, date=None): def local_country_name(country_code: str) -> str: """Return the localized country name for the given country code.""" - return Locale.parse(frappe.local.lang).territories.get(country_code, country_code) + from babel import Locale + + return Locale.parse(frappe.local.lang, sep="-").territories.get(country_code, country_code) diff --git a/erpnext/setup/doctype/holiday_list/test_holiday_list.py b/erpnext/setup/doctype/holiday_list/test_holiday_list.py index 23b08fd11709..7eeb27d864ed 100644 --- a/erpnext/setup/doctype/holiday_list/test_holiday_list.py +++ b/erpnext/setup/doctype/holiday_list/test_holiday_list.py @@ -8,6 +8,8 @@ import frappe from frappe.utils import getdate +from erpnext.setup.doctype.holiday_list.holiday_list import local_country_name + class TestHolidayList(unittest.TestCase): def test_holiday_list(self): @@ -58,6 +60,16 @@ def test_local_holidays(self): self.assertIn(date(2023, 4, 10), holidays) self.assertNotIn(date(2023, 5, 1), holidays) + def test_localized_country_names(self): + lang = frappe.local.lang + frappe.local.lang = "en-gb" + self.assertEqual(local_country_name("IN"), "India") + self.assertEqual(local_country_name("DE"), "Germany") + + frappe.local.lang = "de" + self.assertEqual(local_country_name("DE"), "Deutschland") + frappe.local.lang = lang + def make_holiday_list( name, from_date=getdate() - timedelta(days=10), to_date=getdate(), holiday_dates=None From 7adad4272af06ac3bf50b9dd27affc507f4b931b Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Mon, 7 Aug 2023 10:08:03 +0530 Subject: [PATCH 067/501] perf: defer holiday list imports Only used for configuring but loaded whenever get_doc("holiday list", ...) is done (cherry picked from commit 2eea90a873ca48a351dfda4bcc52a4e1fd57bff6) --- erpnext/setup/doctype/holiday_list/holiday_list.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/erpnext/setup/doctype/holiday_list/holiday_list.py b/erpnext/setup/doctype/holiday_list/holiday_list.py index 6b6c8ba5be8e..526bc2ba4ac2 100644 --- a/erpnext/setup/doctype/holiday_list/holiday_list.py +++ b/erpnext/setup/doctype/holiday_list/holiday_list.py @@ -9,8 +9,6 @@ from frappe import _, throw from frappe.model.document import Document from frappe.utils import formatdate, getdate, today -from holidays import country_holidays -from holidays.utils import list_supported_countries class OverlapError(frappe.ValidationError): @@ -39,6 +37,8 @@ def get_weekly_off_dates(self): @frappe.whitelist() def get_supported_countries(self): + from holidays.utils import list_supported_countries + subdivisions_by_country = list_supported_countries() countries = [ {"value": country, "label": local_country_name(country)} @@ -51,6 +51,8 @@ def get_supported_countries(self): @frappe.whitelist() def get_local_holidays(self): + from holidays import country_holidays + if not self.country: throw(_("Please select a country")) From 07f235cf7d251573a789f014becd0c5956cefc54 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Mon, 7 Aug 2023 11:28:07 +0530 Subject: [PATCH 068/501] feat: ledger comparison report (#36485) * feat: Accounting Ledger comparison report * chore: barebones methods * chore: working state * chore: refactor internal logic * chore: working multi select filter on Account * chore: working voucher no filter * chore: remove debugging statements * chore: report with currency symbol * chore: working start and end date filter * test: basic report function * refactor(test): test all filters (cherry picked from commit b86747c9d4595d37ffeca44c0915a117730ed078) --- .../__init__.py | 0 .../general_and_payment_ledger_comparison.js | 52 +++++ ...general_and_payment_ledger_comparison.json | 32 +++ .../general_and_payment_ledger_comparison.py | 221 ++++++++++++++++++ ...t_general_and_payment_ledger_comparison.py | 100 ++++++++ 5 files changed, 405 insertions(+) create mode 100644 erpnext/accounts/report/general_and_payment_ledger_comparison/__init__.py create mode 100644 erpnext/accounts/report/general_and_payment_ledger_comparison/general_and_payment_ledger_comparison.js create mode 100644 erpnext/accounts/report/general_and_payment_ledger_comparison/general_and_payment_ledger_comparison.json create mode 100644 erpnext/accounts/report/general_and_payment_ledger_comparison/general_and_payment_ledger_comparison.py create mode 100644 erpnext/accounts/report/general_and_payment_ledger_comparison/test_general_and_payment_ledger_comparison.py diff --git a/erpnext/accounts/report/general_and_payment_ledger_comparison/__init__.py b/erpnext/accounts/report/general_and_payment_ledger_comparison/__init__.py new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/erpnext/accounts/report/general_and_payment_ledger_comparison/general_and_payment_ledger_comparison.js b/erpnext/accounts/report/general_and_payment_ledger_comparison/general_and_payment_ledger_comparison.js new file mode 100644 index 000000000000..7e6b0537e875 --- /dev/null +++ b/erpnext/accounts/report/general_and_payment_ledger_comparison/general_and_payment_ledger_comparison.js @@ -0,0 +1,52 @@ +// Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and contributors +// For license information, please see license.txt + +function get_filters() { + let filters = [ + { + "fieldname":"company", + "label": __("Company"), + "fieldtype": "Link", + "options": "Company", + "default": frappe.defaults.get_user_default("Company"), + "reqd": 1 + }, + { + "fieldname":"period_start_date", + "label": __("Start Date"), + "fieldtype": "Date", + "reqd": 1, + "default": frappe.datetime.add_months(frappe.datetime.get_today(), -1) + }, + { + "fieldname":"period_end_date", + "label": __("End Date"), + "fieldtype": "Date", + "reqd": 1, + "default": frappe.datetime.get_today() + }, + { + "fieldname":"account", + "label": __("Account"), + "fieldtype": "MultiSelectList", + "options": "Account", + get_data: function(txt) { + return frappe.db.get_link_options('Account', txt, { + company: frappe.query_report.get_filter_value("company"), + account_type: ['in', ["Receivable", "Payable"]] + }); + } + }, + { + "fieldname":"voucher_no", + "label": __("Voucher No"), + "fieldtype": "Data", + "width": 100, + }, + ] + return filters; +} + +frappe.query_reports["General and Payment Ledger Comparison"] = { + "filters": get_filters() +}; diff --git a/erpnext/accounts/report/general_and_payment_ledger_comparison/general_and_payment_ledger_comparison.json b/erpnext/accounts/report/general_and_payment_ledger_comparison/general_and_payment_ledger_comparison.json new file mode 100644 index 000000000000..1d0d9d134da0 --- /dev/null +++ b/erpnext/accounts/report/general_and_payment_ledger_comparison/general_and_payment_ledger_comparison.json @@ -0,0 +1,32 @@ +{ + "add_total_row": 0, + "columns": [], + "creation": "2023-08-02 17:30:29.494907", + "disabled": 0, + "docstatus": 0, + "doctype": "Report", + "filters": [], + "idx": 0, + "is_standard": "Yes", + "letterhead": null, + "modified": "2023-08-02 17:30:29.494907", + "modified_by": "Administrator", + "module": "Accounts", + "name": "General and Payment Ledger Comparison", + "owner": "Administrator", + "prepared_report": 0, + "ref_doctype": "GL Entry", + "report_name": "General and Payment Ledger Comparison", + "report_type": "Script Report", + "roles": [ + { + "role": "Accounts User" + }, + { + "role": "Accounts Manager" + }, + { + "role": "Auditor" + } + ] +} \ No newline at end of file diff --git a/erpnext/accounts/report/general_and_payment_ledger_comparison/general_and_payment_ledger_comparison.py b/erpnext/accounts/report/general_and_payment_ledger_comparison/general_and_payment_ledger_comparison.py new file mode 100644 index 000000000000..553c137f0247 --- /dev/null +++ b/erpnext/accounts/report/general_and_payment_ledger_comparison/general_and_payment_ledger_comparison.py @@ -0,0 +1,221 @@ +# Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and contributors +# For license information, please see license.txt + +import frappe +from frappe import _, qb +from frappe.query_builder import Criterion +from frappe.query_builder.functions import Sum + + +class General_Payment_Ledger_Comparison(object): + """ + A Utility report to compare Voucher-wise balance between General and Payment Ledger + """ + + def __init__(self, filters=None): + self.filters = filters + self.gle = [] + self.ple = [] + + def get_accounts(self): + receivable_accounts = [ + x[0] + for x in frappe.db.get_all( + "Account", + filters={"company": self.filters.company, "account_type": "Receivable"}, + as_list=True, + ) + ] + payable_accounts = [ + x[0] + for x in frappe.db.get_all( + "Account", filters={"company": self.filters.company, "account_type": "Payable"}, as_list=True + ) + ] + + self.account_types = frappe._dict( + { + "receivable": frappe._dict({"accounts": receivable_accounts, "gle": [], "ple": []}), + "payable": frappe._dict({"accounts": payable_accounts, "gle": [], "ple": []}), + } + ) + + def generate_filters(self): + if self.filters.account: + self.account_types.receivable.accounts = [] + self.account_types.payable.accounts = [] + + for acc in frappe.db.get_all( + "Account", filters={"name": ["in", self.filters.account]}, fields=["name", "account_type"] + ): + if acc.account_type == "Receivable": + self.account_types.receivable.accounts.append(acc.name) + else: + self.account_types.payable.accounts.append(acc.name) + + def get_gle(self): + gle = qb.DocType("GL Entry") + + for acc_type, val in self.account_types.items(): + if val.accounts: + + filter_criterion = [] + if self.filters.voucher_no: + filter_criterion.append((gle.voucher_no == self.filters.voucher_no)) + + if self.filters.period_start_date: + filter_criterion.append(gle.posting_date.gte(self.filters.period_start_date)) + + if self.filters.period_end_date: + filter_criterion.append(gle.posting_date.lte(self.filters.period_end_date)) + + if acc_type == "receivable": + outstanding = (Sum(gle.debit) - Sum(gle.credit)).as_("outstanding") + else: + outstanding = (Sum(gle.credit) - Sum(gle.debit)).as_("outstanding") + + self.account_types[acc_type].gle = ( + qb.from_(gle) + .select( + gle.company, + gle.account, + gle.voucher_no, + gle.party, + outstanding, + ) + .where( + (gle.company == self.filters.company) + & (gle.is_cancelled == 0) + & (gle.account.isin(val.accounts)) + ) + .where(Criterion.all(filter_criterion)) + .groupby(gle.company, gle.account, gle.voucher_no, gle.party) + .run() + ) + + def get_ple(self): + ple = qb.DocType("Payment Ledger Entry") + + for acc_type, val in self.account_types.items(): + if val.accounts: + + filter_criterion = [] + if self.filters.voucher_no: + filter_criterion.append((ple.voucher_no == self.filters.voucher_no)) + + if self.filters.period_start_date: + filter_criterion.append(ple.posting_date.gte(self.filters.period_start_date)) + + if self.filters.period_end_date: + filter_criterion.append(ple.posting_date.lte(self.filters.period_end_date)) + + self.account_types[acc_type].ple = ( + qb.from_(ple) + .select( + ple.company, ple.account, ple.voucher_no, ple.party, Sum(ple.amount).as_("outstanding") + ) + .where( + (ple.company == self.filters.company) + & (ple.delinked == 0) + & (ple.account.isin(val.accounts)) + ) + .where(Criterion.all(filter_criterion)) + .groupby(ple.company, ple.account, ple.voucher_no, ple.party) + .run() + ) + + def compare(self): + self.gle_balances = set() + self.ple_balances = set() + + # consolidate both receivable and payable balances in one set + for acc_type, val in self.account_types.items(): + self.gle_balances = set(val.gle) | self.gle_balances + self.ple_balances = set(val.ple) | self.ple_balances + + self.diff1 = self.gle_balances.difference(self.ple_balances) + self.diff2 = self.ple_balances.difference(self.gle_balances) + self.diff = frappe._dict({}) + + for x in self.diff1: + self.diff[(x[0], x[1], x[2], x[3])] = frappe._dict({"gl_balance": x[4]}) + + for x in self.diff2: + self.diff[(x[0], x[1], x[2], x[3])].update(frappe._dict({"pl_balance": x[4]})) + + def generate_data(self): + self.data = [] + for key, val in self.diff.items(): + self.data.append( + frappe._dict( + { + "voucher_no": key[2], + "party": key[3], + "gl_balance": val.gl_balance, + "pl_balance": val.pl_balance, + } + ) + ) + + def get_columns(self): + self.columns = [] + options = None + self.columns.append( + dict( + label=_("Voucher No"), + fieldname="voucher_no", + fieldtype="Data", + options=options, + width="100", + ) + ) + + self.columns.append( + dict( + label=_("Party"), + fieldname="party", + fieldtype="Data", + options=options, + width="100", + ) + ) + + self.columns.append( + dict( + label=_("GL Balance"), + fieldname="gl_balance", + fieldtype="Currency", + options="Company:company:default_currency", + width="100", + ) + ) + + self.columns.append( + dict( + label=_("Payment Ledger Balance"), + fieldname="pl_balance", + fieldtype="Currency", + options="Company:company:default_currency", + width="100", + ) + ) + + def run(self): + self.get_accounts() + self.generate_filters() + self.get_gle() + self.get_ple() + self.compare() + self.generate_data() + self.get_columns() + + return self.columns, self.data + + +def execute(filters=None): + columns, data = [], [] + + rpt = General_Payment_Ledger_Comparison(filters) + columns, data = rpt.run() + + return columns, data diff --git a/erpnext/accounts/report/general_and_payment_ledger_comparison/test_general_and_payment_ledger_comparison.py b/erpnext/accounts/report/general_and_payment_ledger_comparison/test_general_and_payment_ledger_comparison.py new file mode 100644 index 000000000000..4b0e99d71254 --- /dev/null +++ b/erpnext/accounts/report/general_and_payment_ledger_comparison/test_general_and_payment_ledger_comparison.py @@ -0,0 +1,100 @@ +import unittest + +import frappe +from frappe import qb +from frappe.tests.utils import FrappeTestCase +from frappe.utils import add_days + +from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice +from erpnext.accounts.report.general_and_payment_ledger_comparison.general_and_payment_ledger_comparison import ( + execute, +) +from erpnext.accounts.test.accounts_mixin import AccountsTestMixin + + +class TestGeneralAndPaymentLedger(FrappeTestCase, AccountsTestMixin): + def setUp(self): + self.create_company() + self.cleanup() + + def tearDown(self): + frappe.db.rollback() + + def cleanup(self): + doctypes = [] + doctypes.append(qb.DocType("GL Entry")) + doctypes.append(qb.DocType("Payment Ledger Entry")) + doctypes.append(qb.DocType("Sales Invoice")) + + for doctype in doctypes: + qb.from_(doctype).delete().where(doctype.company == self.company).run() + + def test_01_basic_report_functionality(self): + sinv = create_sales_invoice( + company=self.company, + debit_to=self.debit_to, + expense_account=self.expense_account, + cost_center=self.cost_center, + income_account=self.income_account, + warehouse=self.warehouse, + ) + + # manually edit the payment ledger entry + ple = frappe.db.get_all( + "Payment Ledger Entry", filters={"voucher_no": sinv.name, "delinked": 0} + )[0] + frappe.db.set_value("Payment Ledger Entry", ple.name, "amount", sinv.grand_total - 1) + + filters = frappe._dict({"company": self.company}) + columns, data = execute(filters=filters) + self.assertEqual(len(data), 1) + + expected = { + "voucher_no": sinv.name, + "party": sinv.customer, + "gl_balance": sinv.grand_total, + "pl_balance": sinv.grand_total - 1, + } + self.assertEqual(expected, data[0]) + + # account filter + filters = frappe._dict({"company": self.company, "account": self.debit_to}) + columns, data = execute(filters=filters) + self.assertEqual(len(data), 1) + self.assertEqual(expected, data[0]) + + filters = frappe._dict({"company": self.company, "account": self.creditors}) + columns, data = execute(filters=filters) + self.assertEqual([], data) + + # voucher_no filter + filters = frappe._dict({"company": self.company, "voucher_no": sinv.name}) + columns, data = execute(filters=filters) + self.assertEqual(len(data), 1) + self.assertEqual(expected, data[0]) + + filters = frappe._dict({"company": self.company, "voucher_no": sinv.name + "-1"}) + columns, data = execute(filters=filters) + self.assertEqual([], data) + + # date range filter + filters = frappe._dict( + { + "company": self.company, + "period_start_date": sinv.posting_date, + "period_end_date": sinv.posting_date, + } + ) + columns, data = execute(filters=filters) + self.assertEqual(len(data), 1) + self.assertEqual(expected, data[0]) + + filters = frappe._dict( + { + "company": self.company, + "period_start_date": add_days(sinv.posting_date, -1), + "period_end_date": add_days(sinv.posting_date, -1), + } + ) + columns, data = execute(filters=filters) + self.assertEqual([], data) From 5b047081646ad9131ffbe925d57904597aa454ea Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Mon, 7 Aug 2023 19:11:19 +0530 Subject: [PATCH 069/501] fix: stock entry decimal issue (backport #36530) (#36533) fix: stock entry decimal issue (#36530) (cherry picked from commit 28dfc88789e71edac74556e01e8d49d46e42a35f) Co-authored-by: rohitwaghchaure --- erpnext/stock/doctype/material_request/material_request.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/erpnext/stock/doctype/material_request/material_request.py b/erpnext/stock/doctype/material_request/material_request.py index b430d03d92b8..cf61f9657f4a 100644 --- a/erpnext/stock/doctype/material_request/material_request.py +++ b/erpnext/stock/doctype/material_request/material_request.py @@ -228,7 +228,8 @@ def update_completed_qty(self, mr_items=None, update_modified=True): d.ordered_qty = flt(mr_items_ordered_qty.get(d.name)) if mr_qty_allowance: - allowed_qty = d.qty + (d.qty * (mr_qty_allowance / 100)) + allowed_qty = flt((d.qty + (d.qty * (mr_qty_allowance / 100))), d.precision("ordered_qty")) + if d.ordered_qty and d.ordered_qty > allowed_qty: frappe.throw( _( From 9c108a8ef70ec6f440cddeccc640c363bcf02196 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Mon, 7 Aug 2023 21:03:36 +0530 Subject: [PATCH 070/501] fix: enqueue submit/cancel action for stock entry having more than 50 line items (backport #36532) (#36536) fix: enqueue submit/cancel action for stock entry having more than 50 line items (#36532) (cherry picked from commit ecba6ee1833343d57de15ca546da87e140ab8a55) Co-authored-by: s-aga-r --- erpnext/stock/doctype/stock_entry/stock_entry.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.py b/erpnext/stock/doctype/stock_entry/stock_entry.py index b93ffc437cb5..9560b52f59c7 100644 --- a/erpnext/stock/doctype/stock_entry/stock_entry.py +++ b/erpnext/stock/doctype/stock_entry/stock_entry.py @@ -194,7 +194,7 @@ def is_enqueue_action(self, force=False) -> bool: return False # If line items are more than 100 or record is older than 6 months - if len(self.items) > 100 or month_diff(nowdate(), self.posting_date) > 6: + if len(self.items) > 50 or month_diff(nowdate(), self.posting_date) > 6: return True return False From 5881960001fa55e3d24ddd009e6b897fd4109936 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Mon, 7 Aug 2023 21:17:12 +0530 Subject: [PATCH 071/501] feat(RFQ): make sending attachments configurable (backport #36359) (#36535) * feat(RFQ): make sending attachments configurable (#36359) (cherry picked from commit 8cc3df7c2c77d55ee6cea85b67b045f8c35c9668) # Conflicts: # erpnext/buying/doctype/request_for_quotation/request_for_quotation.json * chore: resolve conflicts --------- Co-authored-by: Raffael Meyer <14891507+barredterra@users.noreply.github.com> --- .../request_for_quotation/request_for_quotation.json | 10 +++++++++- .../request_for_quotation/request_for_quotation.py | 4 +++- 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/erpnext/buying/doctype/request_for_quotation/request_for_quotation.json b/erpnext/buying/doctype/request_for_quotation/request_for_quotation.json index bd65b0c805e8..c16abb2f41b5 100644 --- a/erpnext/buying/doctype/request_for_quotation/request_for_quotation.json +++ b/erpnext/buying/doctype/request_for_quotation/request_for_quotation.json @@ -25,6 +25,7 @@ "col_break_email_1", "email_template", "preview", + "send_attached_files", "sec_break_email_2", "message_for_supplier", "terms_section_break", @@ -285,13 +286,20 @@ "fieldname": "named_place", "fieldtype": "Data", "label": "Named Place" + }, + { + "default": "1", + "description": "If enabled, all files attached to this document will be attached to each email", + "fieldname": "send_attached_files", + "fieldtype": "Check", + "label": "Send Attached Files" } ], "icon": "fa fa-shopping-cart", "index_web_pages_for_search": 1, "is_submittable": 1, "links": [], - "modified": "2023-01-31 23:22:06.684694", + "modified": "2023-07-27 16:41:48.468873", "modified_by": "Administrator", "module": "Buying", "name": "Request for Quotation", diff --git a/erpnext/buying/doctype/request_for_quotation/request_for_quotation.py b/erpnext/buying/doctype/request_for_quotation/request_for_quotation.py index 4590f8c3d937..57bd6bd57052 100644 --- a/erpnext/buying/doctype/request_for_quotation/request_for_quotation.py +++ b/erpnext/buying/doctype/request_for_quotation/request_for_quotation.py @@ -209,7 +209,9 @@ def supplier_rfq_mail(self, data, update_password_link, rfq_link, preview=False) if preview: return message - attachments = self.get_attachments() + attachments = None + if self.send_attached_files: + attachments = self.get_attachments() self.send_email(data, sender, subject, message, attachments) From c26a52d7918f1f1db70ab7958633e6b65c39ffab Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Tue, 8 Aug 2023 14:14:30 +0530 Subject: [PATCH 072/501] refactor: use base_tax_withholding_net_total for treshold validation (#36528) * refactor: use base_tax_withholding_net_total for treshold validation * fix: only for non payment entry doctypes (cherry picked from commit 11d5327d1b5fc1b9198bad77365d5ca269593e76) --- .../tax_withholding_category/tax_withholding_category.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/erpnext/accounts/doctype/tax_withholding_category/tax_withholding_category.py b/erpnext/accounts/doctype/tax_withholding_category/tax_withholding_category.py index 58792d1d8ad9..e66a886bf9a1 100644 --- a/erpnext/accounts/doctype/tax_withholding_category/tax_withholding_category.py +++ b/erpnext/accounts/doctype/tax_withholding_category/tax_withholding_category.py @@ -476,7 +476,12 @@ def get_tds_amount(ldc, parties, inv, tax_details, tax_deducted, vouchers): threshold = tax_details.get("threshold", 0) cumulative_threshold = tax_details.get("cumulative_threshold", 0) - if (threshold and inv.tax_withholding_net_total >= threshold) or ( + if inv.doctype != "Payment Entry": + tax_withholding_net_total = inv.base_tax_withholding_net_total + else: + tax_withholding_net_total = inv.tax_withholding_net_total + + if (threshold and tax_withholding_net_total >= threshold) or ( cumulative_threshold and supp_credit_amt >= cumulative_threshold ): if (cumulative_threshold and supp_credit_amt >= cumulative_threshold) and cint( From cd1c175439dc58ba78224f8a2abd4d9bd8c1a39b Mon Sep 17 00:00:00 2001 From: Anand Baburajan Date: Tue, 8 Aug 2023 15:09:55 +0530 Subject: [PATCH 073/501] perf: asset depreciation entry posting (#36461) * perf: make post depr entries job daily_long * perf: optimise post_depreciation_entries and make_depreciation_entry * chore: more optimisation and dont fail all schedule dates if one date fails * chore: don't post entries before acc_frozen_upto * chore: using get_single_value * refactor: destructure asset object --- erpnext/assets/doctype/asset/asset.py | 8 +- erpnext/assets/doctype/asset/depreciation.py | 306 +++++++++++++----- .../asset_value_adjustment.py | 4 +- erpnext/hooks.py | 2 +- 4 files changed, 233 insertions(+), 87 deletions(-) diff --git a/erpnext/assets/doctype/asset/asset.py b/erpnext/assets/doctype/asset/asset.py index c97f0ece73c9..e6bac31d7d27 100644 --- a/erpnext/assets/doctype/asset/asset.py +++ b/erpnext/assets/doctype/asset/asset.py @@ -962,7 +962,9 @@ def make_gl_entries(self): @frappe.whitelist() def get_manual_depreciation_entries(self): - (_, _, depreciation_expense_account) = get_depreciation_accounts(self) + (_, _, depreciation_expense_account) = get_depreciation_accounts( + self.asset_category, self.company + ) gle = frappe.qb.DocType("GL Entry") @@ -1201,10 +1203,10 @@ def get_asset_account(account_name, asset=None, asset_category=None, company=Non def make_journal_entry(asset_name): asset = frappe.get_doc("Asset", asset_name) ( - fixed_asset_account, + _, accumulated_depreciation_account, depreciation_expense_account, - ) = get_depreciation_accounts(asset) + ) = get_depreciation_accounts(asset.asset_category, asset.company) depreciation_cost_center, depreciation_series = frappe.get_cached_value( "Company", asset.company, ["depreciation_cost_center", "series_for_depreciation_entry"] diff --git a/erpnext/assets/doctype/asset/depreciation.py b/erpnext/assets/doctype/asset/depreciation.py index 80262c04d4a9..f5fd5d60221c 100644 --- a/erpnext/assets/doctype/asset/depreciation.py +++ b/erpnext/assets/doctype/asset/depreciation.py @@ -4,6 +4,8 @@ import frappe from frappe import _ +from frappe.query_builder import Order +from frappe.query_builder.functions import Max, Min from frappe.utils import ( add_months, cint, @@ -36,9 +38,40 @@ def post_depreciation_entries(date=None): failed_asset_names = [] error_log_names = [] - for asset_name in get_depreciable_assets(date): + depreciable_assets = get_depreciable_assets(date) + + credit_and_debit_accounts_for_asset_category_and_company = {} + depreciation_cost_center_and_depreciation_series_for_company = ( + get_depreciation_cost_center_and_depreciation_series_for_company() + ) + + accounting_dimensions = get_checks_for_pl_and_bs_accounts() + + for asset in depreciable_assets: + asset_name, asset_category, asset_company, sch_start_idx, sch_end_idx = asset + + if ( + asset_category, + asset_company, + ) not in credit_and_debit_accounts_for_asset_category_and_company: + credit_and_debit_accounts_for_asset_category_and_company.update( + { + (asset_category, asset_company): get_credit_and_debit_accounts_for_asset_category_and_company( + asset_category, asset_company + ), + } + ) + try: - make_depreciation_entry(asset_name, date) + make_depreciation_entry( + asset_name, + date, + sch_start_idx, + sch_end_idx, + credit_and_debit_accounts_for_asset_category_and_company[(asset_category, asset_company)], + depreciation_cost_center_and_depreciation_series_for_company[asset_company], + accounting_dimensions, + ) frappe.db.commit() except Exception as e: frappe.db.rollback() @@ -54,115 +87,226 @@ def post_depreciation_entries(date=None): def get_depreciable_assets(date): - return frappe.db.sql_list( - """select distinct a.name - from tabAsset a, `tabDepreciation Schedule` ds - where a.name = ds.parent and a.docstatus=1 and ds.schedule_date<=%s and a.calculate_depreciation = 1 - and a.status in ('Submitted', 'Partially Depreciated') - and ifnull(ds.journal_entry, '')=''""", - date, + a = frappe.qb.DocType("Asset") + ds = frappe.qb.DocType("Depreciation Schedule") + + res = ( + frappe.qb.from_(a) + .join(ds) + .on(a.name == ds.parent) + .select(a.name, a.asset_category, a.company, Min(ds.idx) - 1, Max(ds.idx)) + .where(a.calculate_depreciation == 1) + .where(a.docstatus == 1) + .where(a.status.isin(["Submitted", "Partially Depreciated"])) + .where(ds.journal_entry.isnull()) + .where(ds.schedule_date <= date) + .groupby(a.name) + .orderby(a.creation, order=Order.desc) + ) + + acc_frozen_upto = get_acc_frozen_upto() + if acc_frozen_upto: + res = res.where(ds.schedule_date > acc_frozen_upto) + + res = res.run() + + return res + + +def get_acc_frozen_upto(): + acc_frozen_upto = frappe.db.get_single_value("Accounts Settings", "acc_frozen_upto") + + if not acc_frozen_upto: + return + + frozen_accounts_modifier = frappe.db.get_single_value( + "Accounts Settings", "frozen_accounts_modifier" ) + if frozen_accounts_modifier not in frappe.get_roles() or frappe.session.user == "Administrator": + return getdate(acc_frozen_upto) + + return + + +def get_credit_and_debit_accounts_for_asset_category_and_company(asset_category, company): + ( + _, + accumulated_depreciation_account, + depreciation_expense_account, + ) = get_depreciation_accounts(asset_category, company) + + credit_account, debit_account = get_credit_and_debit_accounts( + accumulated_depreciation_account, depreciation_expense_account + ) + + return (credit_account, debit_account) + + +def get_depreciation_cost_center_and_depreciation_series_for_company(): + company_names = frappe.db.get_all("Company", pluck="name") + + res = {} + + for company_name in company_names: + depreciation_cost_center, depreciation_series = frappe.get_cached_value( + "Company", company_name, ["depreciation_cost_center", "series_for_depreciation_entry"] + ) + res.update({company_name: (depreciation_cost_center, depreciation_series)}) + + return res + @frappe.whitelist() -def make_depreciation_entry(asset_name, date=None): +def make_depreciation_entry( + asset_name, + date=None, + sch_start_idx=None, + sch_end_idx=None, + credit_and_debit_accounts=None, + depreciation_cost_center_and_depreciation_series=None, + accounting_dimensions=None, +): frappe.has_permission("Journal Entry", throw=True) if not date: date = today() asset = frappe.get_doc("Asset", asset_name) - ( - fixed_asset_account, - accumulated_depreciation_account, - depreciation_expense_account, - ) = get_depreciation_accounts(asset) - depreciation_cost_center, depreciation_series = frappe.get_cached_value( - "Company", asset.company, ["depreciation_cost_center", "series_for_depreciation_entry"] - ) + if credit_and_debit_accounts: + credit_account, debit_account = credit_and_debit_accounts + else: + credit_account, debit_account = get_credit_and_debit_accounts_for_asset_category_and_company( + asset.asset_category, asset.company + ) + + if depreciation_cost_center_and_depreciation_series: + depreciation_cost_center, depreciation_series = depreciation_cost_center_and_depreciation_series + else: + depreciation_cost_center, depreciation_series = frappe.get_cached_value( + "Company", asset.company, ["depreciation_cost_center", "series_for_depreciation_entry"] + ) depreciation_cost_center = asset.cost_center or depreciation_cost_center - accounting_dimensions = get_checks_for_pl_and_bs_accounts() + if not accounting_dimensions: + accounting_dimensions = get_checks_for_pl_and_bs_accounts() + + depreciation_posting_error = None - for d in asset.get("schedules"): - if not d.journal_entry and getdate(d.schedule_date) <= getdate(date): - je = frappe.new_doc("Journal Entry") - je.voucher_type = "Depreciation Entry" - je.naming_series = depreciation_series - je.posting_date = d.schedule_date - je.company = asset.company - je.finance_book = d.finance_book - je.remark = "Depreciation Entry against {0} worth {1}".format(asset_name, d.depreciation_amount) - - credit_account, debit_account = get_credit_and_debit_accounts( - accumulated_depreciation_account, depreciation_expense_account + for d in asset.get("schedules")[sch_start_idx or 0 : sch_end_idx or len(asset.get("schedules"))]: + try: + _make_journal_entry_for_depreciation( + asset, + date, + d, + sch_start_idx, + sch_end_idx, + depreciation_cost_center, + depreciation_series, + credit_account, + debit_account, + accounting_dimensions, ) + frappe.db.commit() + except Exception as e: + frappe.db.rollback() + depreciation_posting_error = e - credit_entry = { - "account": credit_account, - "credit_in_account_currency": d.depreciation_amount, - "reference_type": "Asset", - "reference_name": asset.name, - "cost_center": depreciation_cost_center, - } + asset.set_status() - debit_entry = { - "account": debit_account, - "debit_in_account_currency": d.depreciation_amount, - "reference_type": "Asset", - "reference_name": asset.name, - "cost_center": depreciation_cost_center, - } + if not depreciation_posting_error: + asset.db_set("depr_entry_posting_status", "Successful") + return asset - for dimension in accounting_dimensions: - if asset.get(dimension["fieldname"]) or dimension.get("mandatory_for_bs"): - credit_entry.update( - { - dimension["fieldname"]: asset.get(dimension["fieldname"]) - or dimension.get("default_dimension") - } - ) + raise depreciation_posting_error - if asset.get(dimension["fieldname"]) or dimension.get("mandatory_for_pl"): - debit_entry.update( - { - dimension["fieldname"]: asset.get(dimension["fieldname"]) - or dimension.get("default_dimension") - } - ) - je.append("accounts", credit_entry) +def _make_journal_entry_for_depreciation( + asset, + date, + depr_schedule, + sch_start_idx, + sch_end_idx, + depreciation_cost_center, + depreciation_series, + credit_account, + debit_account, + accounting_dimensions, +): + if not (sch_start_idx and sch_end_idx) and not ( + not depr_schedule.journal_entry and getdate(depr_schedule.schedule_date) <= getdate(date) + ): + return + + je = frappe.new_doc("Journal Entry") + je.voucher_type = "Depreciation Entry" + je.naming_series = depreciation_series + je.posting_date = depr_schedule.schedule_date + je.company = asset.company + je.finance_book = depr_schedule.finance_book + je.remark = "Depreciation Entry against {0} worth {1}".format( + asset.name, depr_schedule.depreciation_amount + ) - je.append("accounts", debit_entry) + credit_entry = { + "account": credit_account, + "credit_in_account_currency": depr_schedule.depreciation_amount, + "reference_type": "Asset", + "reference_name": asset.name, + "cost_center": depreciation_cost_center, + } + + debit_entry = { + "account": debit_account, + "debit_in_account_currency": depr_schedule.depreciation_amount, + "reference_type": "Asset", + "reference_name": asset.name, + "cost_center": depreciation_cost_center, + } + + for dimension in accounting_dimensions: + if asset.get(dimension["fieldname"]) or dimension.get("mandatory_for_bs"): + credit_entry.update( + { + dimension["fieldname"]: asset.get(dimension["fieldname"]) + or dimension.get("default_dimension") + } + ) - je.flags.ignore_permissions = True - je.flags.planned_depr_entry = True - je.save() + if asset.get(dimension["fieldname"]) or dimension.get("mandatory_for_pl"): + debit_entry.update( + { + dimension["fieldname"]: asset.get(dimension["fieldname"]) + or dimension.get("default_dimension") + } + ) - d.db_set("journal_entry", je.name) + je.append("accounts", credit_entry) - if not je.meta.get_workflow(): - je.submit() - idx = cint(d.finance_book_id) - finance_books = asset.get("finance_books")[idx - 1] - finance_books.value_after_depreciation -= d.depreciation_amount - finance_books.db_update() + je.append("accounts", debit_entry) - asset.db_set("depr_entry_posting_status", "Successful") + je.flags.ignore_permissions = True + je.flags.planned_depr_entry = True + je.save() - asset.set_status() + depr_schedule.db_set("journal_entry", je.name) - return asset + if not je.meta.get_workflow(): + je.submit() + idx = cint(depr_schedule.finance_book_id) + finance_books = asset.get("finance_books")[idx - 1] + finance_books.value_after_depreciation -= depr_schedule.depreciation_amount + finance_books.db_update() -def get_depreciation_accounts(asset): +def get_depreciation_accounts(asset_category, company): fixed_asset_account = accumulated_depreciation_account = depreciation_expense_account = None accounts = frappe.db.get_value( "Asset Category Account", - filters={"parent": asset.asset_category, "company_name": asset.company}, + filters={"parent": asset_category, "company_name": company}, fieldname=[ "fixed_asset_account", "accumulated_depreciation_account", @@ -178,7 +322,7 @@ def get_depreciation_accounts(asset): if not accumulated_depreciation_account or not depreciation_expense_account: accounts = frappe.get_cached_value( - "Company", asset.company, ["accumulated_depreciation_account", "depreciation_expense_account"] + "Company", company, ["accumulated_depreciation_account", "depreciation_expense_account"] ) if not accumulated_depreciation_account: @@ -193,7 +337,7 @@ def get_depreciation_accounts(asset): ): frappe.throw( _("Please set Depreciation related Accounts in Asset Category {0} or Company {1}").format( - asset.asset_category, asset.company + asset_category, company ) ) @@ -533,8 +677,8 @@ def get_gl_entries_on_asset_disposal( def get_asset_details(asset, finance_book=None): - fixed_asset_account, accumulated_depr_account, depr_expense_account = get_depreciation_accounts( - asset + fixed_asset_account, accumulated_depr_account, _ = get_depreciation_accounts( + asset.asset_category, asset.company ) disposal_account, depreciation_cost_center = get_disposal_account_and_cost_center(asset.company) depreciation_cost_center = asset.cost_center or depreciation_cost_center diff --git a/erpnext/assets/doctype/asset_value_adjustment/asset_value_adjustment.py b/erpnext/assets/doctype/asset_value_adjustment/asset_value_adjustment.py index ee75b83af396..9928b2f5f383 100644 --- a/erpnext/assets/doctype/asset_value_adjustment/asset_value_adjustment.py +++ b/erpnext/assets/doctype/asset_value_adjustment/asset_value_adjustment.py @@ -50,10 +50,10 @@ def set_current_asset_value(self): def make_depreciation_entry(self): asset = frappe.get_doc("Asset", self.asset) ( - fixed_asset_account, + _, accumulated_depreciation_account, depreciation_expense_account, - ) = get_depreciation_accounts(asset) + ) = get_depreciation_accounts(asset.asset_category, asset.company) depreciation_cost_center, depreciation_series = frappe.get_cached_value( "Company", asset.company, ["depreciation_cost_center", "series_for_depreciation_entry"] diff --git a/erpnext/hooks.py b/erpnext/hooks.py index 6b7d2dc26a23..b2a76f2038ca 100644 --- a/erpnext/hooks.py +++ b/erpnext/hooks.py @@ -432,7 +432,6 @@ "erpnext.controllers.accounts_controller.update_invoice_status", "erpnext.accounts.doctype.fiscal_year.fiscal_year.auto_create_fiscal_year", "erpnext.projects.doctype.task.task.set_tasks_as_overdue", - "erpnext.assets.doctype.asset.depreciation.post_depreciation_entries", "erpnext.stock.doctype.serial_no.serial_no.update_maintenance_status", "erpnext.buying.doctype.supplier_scorecard.supplier_scorecard.refresh_scorecards", "erpnext.setup.doctype.company.company.cache_companies_monthly_sales_history", @@ -459,6 +458,7 @@ "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.assets.doctype.asset.depreciation.post_depreciation_entries", ], "monthly_long": [ "erpnext.accounts.deferred_revenue.process_deferred_accounting", From 00b9df0bc5199d6c08d14cf7dd4f9f4d01ec93c2 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Tue, 8 Aug 2023 16:04:52 +0530 Subject: [PATCH 074/501] fix: stock reconciliation negative stock error (backport #36544) (#36549) fix: stock reconciliation negative stock error (#36544) fix: stock reco negative stock error (cherry picked from commit 0b36e7d10ec42a4baac2af8f41a2817ed06b711d) Co-authored-by: rohitwaghchaure --- .../stock/doctype/stock_reconciliation/stock_reconciliation.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py b/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py index 3fd4cec5d884..c6c8571b9efa 100644 --- a/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py +++ b/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py @@ -607,7 +607,7 @@ def recalculate_current_qty(self, item_code, batch_no): ) if sl_entries: - self.make_sl_entries(sl_entries) + self.make_sl_entries(sl_entries, allow_negative_stock=True) def get_batch_qty_for_stock_reco( From eb2f68ec987151cc355312947dda4f9ea24e23fe Mon Sep 17 00:00:00 2001 From: barredterra <14891507+barredterra@users.noreply.github.com> Date: Tue, 8 Aug 2023 12:31:01 +0200 Subject: [PATCH 075/501] fix(RFQ): link to supplier portal (cherry picked from commit fd91f2c2e02c3eaa4365baca83c299ee8c6c02eb) --- .../doctype/request_for_quotation/request_for_quotation.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/erpnext/buying/doctype/request_for_quotation/request_for_quotation.py b/erpnext/buying/doctype/request_for_quotation/request_for_quotation.py index 57bd6bd57052..63e393aecd67 100644 --- a/erpnext/buying/doctype/request_for_quotation/request_for_quotation.py +++ b/erpnext/buying/doctype/request_for_quotation/request_for_quotation.py @@ -116,7 +116,10 @@ def get_link(self): route = frappe.db.get_value( "Portal Menu Item", {"reference_doctype": "Request for Quotation"}, ["route"] ) - return get_url("/app/{0}/".format(route) + self.name) + if not route: + frappe.throw(_("Please add Request for Quotation to the sidebar in Portal Settings.")) + + return get_url(f"{route}/{self.name}") def update_supplier_part_no(self, supplier): self.vendor = supplier From d273948d7aaf43715c00808b488c669a1749d08e Mon Sep 17 00:00:00 2001 From: barredterra <14891507+barredterra@users.noreply.github.com> Date: Tue, 8 Aug 2023 12:31:37 +0200 Subject: [PATCH 076/501] test(RFQ): get_link (cherry picked from commit 68ad62f7d082d9c50e558d6834f623dc70516a1e) --- .../test_request_for_quotation.py | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/erpnext/buying/doctype/request_for_quotation/test_request_for_quotation.py b/erpnext/buying/doctype/request_for_quotation/test_request_for_quotation.py index d250e6f18a96..42fa1d923e16 100644 --- a/erpnext/buying/doctype/request_for_quotation/test_request_for_quotation.py +++ b/erpnext/buying/doctype/request_for_quotation/test_request_for_quotation.py @@ -2,11 +2,14 @@ # See license.txt +from urllib.parse import urlparse + import frappe from frappe.tests.utils import FrappeTestCase from frappe.utils import nowdate from erpnext.buying.doctype.request_for_quotation.request_for_quotation import ( + RequestforQuotation, create_supplier_quotation, get_pdf, make_supplier_quotation_from_rfq, @@ -125,13 +128,18 @@ def test_make_rfq_from_opportunity(self): rfq.status = "Draft" rfq.submit() + def test_get_link(self): + rfq = make_request_for_quotation() + parsed_link = urlparse(rfq.get_link()) + self.assertEqual(parsed_link.path, f"/rfq/{rfq.name}") + def test_get_pdf(self): rfq = make_request_for_quotation() get_pdf(rfq.name, rfq.get("suppliers")[0].supplier) self.assertEqual(frappe.local.response.type, "pdf") -def make_request_for_quotation(**args): +def make_request_for_quotation(**args) -> "RequestforQuotation": """ :param supplier_data: List containing supplier data """ From 0e87c86aab93255f57d7b691f7c4843c0274ba70 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Wed, 9 Aug 2023 00:04:08 +0530 Subject: [PATCH 077/501] fix: payment allocation in invoice payment schedule (#36440) * fix: payment allocation in invoice payment schedule (#36440) * fix: payment allocation in invoice payment schedule * test: payment allocation for payment terms * chore: linting issues (cherry picked from commit edbefee10ca779f1d81153110c6085dd04d9c769) # Conflicts: # erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py * chore: resolve conflicts --------- Co-authored-by: Gursheen Kaur Anand <40693548+GursheenK@users.noreply.github.com> Co-authored-by: Deepesh Garg --- .../purchase_invoice/test_purchase_invoice.py | 46 +++++++++++++++++++ erpnext/controllers/accounts_controller.py | 5 ++ 2 files changed, 51 insertions(+) diff --git a/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py b/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py index e8766275f0b1..ab2e3cf103c6 100644 --- a/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py +++ b/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py @@ -1670,6 +1670,52 @@ def test_gl_entries_for_standalone_debit_note(self): rate = flt(sle.stock_value_difference) / flt(sle.actual_qty) self.assertAlmostEqual(returned_inv.items[0].rate, rate) + def test_payment_allocation_for_payment_terms(self): + from erpnext.buying.doctype.purchase_order.test_purchase_order import ( + create_pr_against_po, + create_purchase_order, + ) + from erpnext.selling.doctype.sales_order.test_sales_order import ( + automatically_fetch_payment_terms, + ) + from erpnext.stock.doctype.purchase_receipt.purchase_receipt import ( + make_purchase_invoice as make_pi_from_pr, + ) + + automatically_fetch_payment_terms() + frappe.db.set_value( + "Payment Terms Template", + "_Test Payment Term Template", + "allocate_payment_based_on_payment_terms", + 0, + ) + + po = create_purchase_order(do_not_save=1) + po.payment_terms_template = "_Test Payment Term Template" + po.save() + po.submit() + + pr = create_pr_against_po(po.name, received_qty=4) + pi = make_pi_from_pr(pr.name) + self.assertEqual(pi.payment_schedule[0].payment_amount, 1000) + + frappe.db.set_value( + "Payment Terms Template", + "_Test Payment Term Template", + "allocate_payment_based_on_payment_terms", + 1, + ) + pi = make_pi_from_pr(pr.name) + self.assertEqual(pi.payment_schedule[0].payment_amount, 2500) + + automatically_fetch_payment_terms(enable=0) + frappe.db.set_value( + "Payment Terms Template", + "_Test Payment Term Template", + "allocate_payment_based_on_payment_terms", + 0, + ) + def check_gl_entries(doc, voucher_no, expected_gle, posting_date): gl_entries = frappe.db.sql( diff --git a/erpnext/controllers/accounts_controller.py b/erpnext/controllers/accounts_controller.py index f4b6e01cc4bd..4eb2597c668e 100644 --- a/erpnext/controllers/accounts_controller.py +++ b/erpnext/controllers/accounts_controller.py @@ -1672,8 +1672,13 @@ def set_payment_schedule(self): ) self.append("payment_schedule", data) + allocate_payment_based_on_payment_terms = frappe.db.get_value( + "Payment Terms Template", self.payment_terms_template, "allocate_payment_based_on_payment_terms" + ) + if not ( automatically_fetch_payment_terms + and allocate_payment_based_on_payment_terms and self.linked_order_has_payment_terms(po_or_so, fieldname, doctype) ): for d in self.get("payment_schedule"): From 240d866ef495e09e4da8a3064c33bcccd319cb27 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Wed, 9 Aug 2023 00:04:47 +0530 Subject: [PATCH 078/501] fix: Debit credit difference while submitting Sales Invoice (#36523) * fix: Debit credit difference while submitting Sales Invoice (#36523) * fix: Debit credit difference while submitting Sales Invoice * test(fix): Update gl entry comparison * test(fix): Update gl entry comparison (cherry picked from commit 492ea3bcc820fe14dfde3a0026397d3b5e9e4179) # Conflicts: # erpnext/controllers/accounts_controller.py * chore: resolve conflicts --------- Co-authored-by: Deepesh Garg --- .../purchase_invoice/purchase_invoice.py | 24 -------------- .../doctype/sales_invoice/sales_invoice.py | 1 + .../sales_invoice/test_sales_invoice.py | 32 +++++++++---------- erpnext/controllers/accounts_controller.py | 28 ++++++++++++++++ 4 files changed, 45 insertions(+), 40 deletions(-) diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py index d791232c0afb..5a7ff1c0d1c9 100644 --- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py +++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py @@ -976,30 +976,6 @@ def make_item_gl_entries(self, gl_entries): item.item_tax_amount, item.precision("item_tax_amount") ) - def make_precision_loss_gl_entry(self, gl_entries): - round_off_account, round_off_cost_center = get_round_off_account_and_cost_center( - self.company, "Purchase Invoice", self.name, self.use_company_roundoff_cost_center - ) - - precision_loss = self.get("base_net_total") - flt( - self.get("net_total") * self.conversion_rate, self.precision("net_total") - ) - - if precision_loss: - gl_entries.append( - self.get_gl_dict( - { - "account": round_off_account, - "against": self.supplier, - "credit": precision_loss, - "cost_center": round_off_cost_center - if self.use_company_roundoff_cost_center - else self.cost_center or round_off_cost_center, - "remarks": _("Net total calculation precision loss"), - } - ) - ) - def get_asset_gl_entry(self, gl_entries): arbnb_account = self.get_company_default("asset_received_but_not_billed") eiiav_account = self.get_company_default("expenses_included_in_asset_valuation") diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py index 15774331272e..03c0712d6326 100644 --- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py +++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py @@ -1075,6 +1075,7 @@ def get_gl_entries(self, warehouse_account=None): self.make_internal_transfer_gl_entries(gl_entries) self.make_item_gl_entries(gl_entries) + self.make_precision_loss_gl_entry(gl_entries) self.make_discount_gl_entries(gl_entries) # merge gl entries before adding pos entries diff --git a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py index fd5ca8b1ebae..277e584aeaf6 100644 --- a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py +++ b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py @@ -2049,28 +2049,27 @@ def test_rounding_adjustment_2(self): self.assertEqual(si.total_taxes_and_charges, 228.82) self.assertEqual(si.rounding_adjustment, -0.01) - expected_values = dict( - (d[0], d) - for d in [ - [si.debit_to, 1500, 0.0], - ["_Test Account Service Tax - _TC", 0.0, 114.41], - ["_Test Account VAT - _TC", 0.0, 114.41], - ["Sales - _TC", 0.0, 1271.18], - ] - ) + expected_values = [ + ["_Test Account Service Tax - _TC", 0.0, 114.41], + ["_Test Account VAT - _TC", 0.0, 114.41], + [si.debit_to, 1500, 0.0], + ["Round Off - _TC", 0.01, 0.01], + ["Sales - _TC", 0.0, 1271.18], + ] gl_entries = frappe.db.sql( - """select account, debit, credit + """select account, sum(debit) as debit, sum(credit) as credit from `tabGL Entry` where voucher_type='Sales Invoice' and voucher_no=%s + group by account order by account asc""", si.name, as_dict=1, ) - for gle in gl_entries: - self.assertEqual(expected_values[gle.account][0], gle.account) - self.assertEqual(expected_values[gle.account][1], gle.debit) - self.assertEqual(expected_values[gle.account][2], gle.credit) + for i, gle in enumerate(gl_entries): + self.assertEqual(expected_values[i][0], gle.account) + self.assertEqual(expected_values[i][1], gle.debit) + self.assertEqual(expected_values[i][2], gle.credit) def test_rounding_adjustment_3(self): from erpnext.accounts.doctype.accounting_dimension.test_accounting_dimension import ( @@ -2125,13 +2124,14 @@ def test_rounding_adjustment_3(self): ["_Test Account Service Tax - _TC", 0.0, 240.43], ["_Test Account VAT - _TC", 0.0, 240.43], ["Sales - _TC", 0.0, 4007.15], - ["Round Off - _TC", 0.01, 0], + ["Round Off - _TC", 0.02, 0.01], ] ) gl_entries = frappe.db.sql( - """select account, debit, credit + """select account, sum(debit) as debit, sum(credit) as credit from `tabGL Entry` where voucher_type='Sales Invoice' and voucher_no=%s + group by account order by account asc""", si.name, as_dict=1, diff --git a/erpnext/controllers/accounts_controller.py b/erpnext/controllers/accounts_controller.py index 4eb2597c668e..9912dd47f8b6 100644 --- a/erpnext/controllers/accounts_controller.py +++ b/erpnext/controllers/accounts_controller.py @@ -31,6 +31,7 @@ apply_pricing_rule_on_transaction, get_applied_pricing_rules, ) +from erpnext.accounts.general_ledger import get_round_off_account_and_cost_center from erpnext.accounts.party import ( get_party_account, get_party_account_currency, @@ -1023,6 +1024,33 @@ def make_exchange_gain_loss_gl_entries(self, gl_entries): ) ) + def make_precision_loss_gl_entry(self, gl_entries): + round_off_account, round_off_cost_center = get_round_off_account_and_cost_center( + self.company, "Purchase Invoice", self.name, self.use_company_roundoff_cost_center + ) + + precision_loss = self.get("base_net_total") - flt( + self.get("net_total") * self.conversion_rate, self.precision("net_total") + ) + + credit_or_debit = "credit" if self.doctype == "Purchase Invoice" else "debit" + against = self.supplier if self.doctype == "Purchase Invoice" else self.customer + + if precision_loss: + gl_entries.append( + self.get_gl_dict( + { + "account": round_off_account, + "against": against, + credit_or_debit: precision_loss, + "cost_center": round_off_cost_center + if self.use_company_roundoff_cost_center + else self.cost_center or round_off_cost_center, + "remarks": _("Net total calculation precision loss"), + } + ) + ) + def update_against_document_in_jv(self): """ Links invoice and advance voucher: From 259f3422d5a3ca75a40cc83717847e28fb8aa3e5 Mon Sep 17 00:00:00 2001 From: Frappe PR Bot Date: Wed, 9 Aug 2023 03:06:43 +0000 Subject: [PATCH 079/501] chore(release): Bumped to Version 14.34.0 # [14.34.0](https://github.com/frappe/erpnext/compare/v14.33.2...v14.34.0) (2023-08-09) ### Bug Fixes * **accounts:** Translate columns in AP/AR report ([#36503](https://github.com/frappe/erpnext/issues/36503)) ([6739369](https://github.com/frappe/erpnext/commit/67393694de3ee4f37e40c8fe1e3a9963acc0ed8b)) * AP and AR summary ([769d7d7](https://github.com/frappe/erpnext/commit/769d7d7554ff808f0d14b4ed7e276ef44ea15259)) * check root type only when not none ([46bb309](https://github.com/frappe/erpnext/commit/46bb309b8a56edf800cc4ca6b418a3ffc339d99c)) * cross connect delivery note and sales invoice ([#36183](https://github.com/frappe/erpnext/issues/36183)) ([8501a11](https://github.com/frappe/erpnext/commit/8501a1182ae8323d91438da30ddc8d93cf8c2789)) * Debit credit difference while submitting Sales Invoice ([#36523](https://github.com/frappe/erpnext/issues/36523)) ([240d866](https://github.com/frappe/erpnext/commit/240d866ef495e09e4da8a3064c33bcccd319cb27)) * don't allow negative rate (backport [#36027](https://github.com/frappe/erpnext/issues/36027)) ([#36465](https://github.com/frappe/erpnext/issues/36465)) ([caa4f33](https://github.com/frappe/erpnext/commit/caa4f331694937a6768fa3180562e947d6996e7f)) * enqueue submit/cancel action for stock entry having more than 50 line items (backport [#36532](https://github.com/frappe/erpnext/issues/36532)) ([#36536](https://github.com/frappe/erpnext/issues/36536)) ([9c108a8](https://github.com/frappe/erpnext/commit/9c108a8ef70ec6f440cddeccc640c363bcf02196)) * fetch ple for all party types ([674dba8](https://github.com/frappe/erpnext/commit/674dba8cd7ef97103c706a19066872622f965bf2)) * fetch ple with party type employee in AP ([1ca9aca](https://github.com/frappe/erpnext/commit/1ca9aca0d5e2002e1175581df05aa1700bd745ab)) * Fix query for financial statement report ([d1590f2](https://github.com/frappe/erpnext/commit/d1590f266bffc55fa76f799276c1f96fc4cef930)) * get incoming rate instead of BOM rate (backport [#36496](https://github.com/frappe/erpnext/issues/36496)) ([#36506](https://github.com/frappe/erpnext/issues/36506)) ([bdfbccd](https://github.com/frappe/erpnext/commit/bdfbccd38eefd5c7a3385f3e25bcb3fcb442f2ae)) * handle None value in payment_term_outstanding ([b033b3b](https://github.com/frappe/erpnext/commit/b033b3b0d6a7a286e253a24d7432b607eaa14d09)) * Lower deduction certificate for multi-company ([#36491](https://github.com/frappe/erpnext/issues/36491)) ([2216875](https://github.com/frappe/erpnext/commit/2216875bd6f22f695e19e9af6219628760b42a88)) * payment allocation in invoice payment schedule ([#36440](https://github.com/frappe/erpnext/issues/36440)) ([0e87c86](https://github.com/frappe/erpnext/commit/0e87c86aab93255f57d7b691f7c4843c0274ba70)) * search not working for so in the Production Plan ([#36459](https://github.com/frappe/erpnext/issues/36459)) ([8c57d56](https://github.com/frappe/erpnext/commit/8c57d56240d2c35ae09350af5d3598b4773e9ac2)) * serial no not able to reject for the internal transfer ([#36467](https://github.com/frappe/erpnext/issues/36467)) ([c1819a4](https://github.com/frappe/erpnext/commit/c1819a4b21f09b1775cb20c79a8683c734961ec3)) * stock entry decimal issue (backport [#36530](https://github.com/frappe/erpnext/issues/36530)) ([#36533](https://github.com/frappe/erpnext/issues/36533)) ([5b04708](https://github.com/frappe/erpnext/commit/5b047081646ad9131ffbe925d57904597aa454ea)) * stock reconciliation negative stock error (backport [#36544](https://github.com/frappe/erpnext/issues/36544)) ([#36549](https://github.com/frappe/erpnext/issues/36549)) ([00b9df0](https://github.com/frappe/erpnext/commit/00b9df0bc5199d6c08d14cf7dd4f9f4d01ec93c2)) * Tax withholding against order via Payment Entry ([#36493](https://github.com/frappe/erpnext/issues/36493)) ([a234b89](https://github.com/frappe/erpnext/commit/a234b8932e34b7f726c54a720c3199ed1e3bfb64)) * use correct lang separator for frappe (backport [#36519](https://github.com/frappe/erpnext/issues/36519)) ([#36520](https://github.com/frappe/erpnext/issues/36520)) ([f9981d1](https://github.com/frappe/erpnext/commit/f9981d1ff3527057cebc132eede74638b88ebfeb)) * **ux:** add `Ordered Qty` column in Get Items From > MR (backport [#36486](https://github.com/frappe/erpnext/issues/36486)) ([#36505](https://github.com/frappe/erpnext/issues/36505)) ([0d7a4b6](https://github.com/frappe/erpnext/commit/0d7a4b6ff64dbe5cca0cc6999557c2a9fdbd54fb)) ### Features * ledger comparison report ([#36485](https://github.com/frappe/erpnext/issues/36485)) ([07f235c](https://github.com/frappe/erpnext/commit/07f235cf7d251573a789f014becd0c5956cefc54)) * **RFQ:** make sending attachments configurable (backport [#36359](https://github.com/frappe/erpnext/issues/36359)) ([#36535](https://github.com/frappe/erpnext/issues/36535)) ([5881960](https://github.com/frappe/erpnext/commit/5881960001fa55e3d24ddd009e6b897fd4109936)) ### Performance Improvements * asset depreciation entry posting ([#36461](https://github.com/frappe/erpnext/issues/36461)) ([cd1c175](https://github.com/frappe/erpnext/commit/cd1c175439dc58ba78224f8a2abd4d9bd8c1a39b)) * defer holiday list imports ([7adad42](https://github.com/frappe/erpnext/commit/7adad4272af06ac3bf50b9dd27affc507f4b931b)) --- erpnext/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/__init__.py b/erpnext/__init__.py index 7b7e2142ee68..89ea6e4289b6 100644 --- a/erpnext/__init__.py +++ b/erpnext/__init__.py @@ -3,7 +3,7 @@ import frappe -__version__ = "14.33.2" +__version__ = "14.34.0" def get_default_company(user=None): From fe41be953d301b3a92680b5d8eaa04fb5f8a4d95 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Wed, 9 Aug 2023 14:07:48 +0530 Subject: [PATCH 080/501] perf(invoice): Faster return amount query (backport #36556) (#36557) perf(invoice): Faster return amount query (#36556) perf: Faster return amount query (cherry picked from commit b0c79a0467272bf38553e45d3e067b52285cbfd8) Co-authored-by: Ankush Menat --- erpnext/accounts/doctype/sales_invoice/sales_invoice.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py index 03c0712d6326..c2804d19a345 100644 --- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py +++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py @@ -1665,15 +1665,13 @@ def set_loyalty_program_tier(self): frappe.db.set_value("Customer", self.customer, "loyalty_program_tier", lp_details.tier_name) def get_returned_amount(self): - from frappe.query_builder.functions import Coalesce, Sum + from frappe.query_builder.functions import Sum doc = frappe.qb.DocType(self.doctype) returned_amount = ( frappe.qb.from_(doc) .select(Sum(doc.grand_total)) - .where( - (doc.docstatus == 1) & (doc.is_return == 1) & (Coalesce(doc.return_against, "") == self.name) - ) + .where((doc.docstatus == 1) & (doc.is_return == 1) & (doc.return_against == self.name)) ).run() return abs(returned_amount[0][0]) if returned_amount[0][0] else 0 From 8abc0adb187bd60bf3a83acbabff109da62724d6 Mon Sep 17 00:00:00 2001 From: Anand Baburajan Date: Wed, 9 Aug 2023 20:37:40 +0530 Subject: [PATCH 081/501] chore: add warning for lending separation (#36569) --- erpnext/loan_management/workspace/loans/loans.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/erpnext/loan_management/workspace/loans/loans.json b/erpnext/loan_management/workspace/loans/loans.json index c25f4d35d0be..f431b85aa714 100644 --- a/erpnext/loan_management/workspace/loans/loans.json +++ b/erpnext/loan_management/workspace/loans/loans.json @@ -1,6 +1,6 @@ { "charts": [], - "content": "[{\"id\":\"_38WStznya\",\"type\":\"header\",\"data\":{\"text\":\"Your Shortcuts\",\"col\":12}},{\"id\":\"t7o_K__1jB\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Loan Application\",\"col\":3}},{\"id\":\"IRiNDC6w1p\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Loan\",\"col\":3}},{\"id\":\"xbbo0FYbq0\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Dashboard\",\"col\":3}},{\"id\":\"7ZL4Bro-Vi\",\"type\":\"spacer\",\"data\":{\"col\":12}},{\"id\":\"yhyioTViZ3\",\"type\":\"header\",\"data\":{\"text\":\"Reports & Masters\",\"col\":12}},{\"id\":\"oYFn4b1kSw\",\"type\":\"card\",\"data\":{\"card_name\":\"Loan\",\"col\":4}},{\"id\":\"vZepJF5tl9\",\"type\":\"card\",\"data\":{\"card_name\":\"Loan Processes\",\"col\":4}},{\"id\":\"k-393Mjhqe\",\"type\":\"card\",\"data\":{\"card_name\":\"Disbursement and Repayment\",\"col\":4}},{\"id\":\"6crJ0DBiBJ\",\"type\":\"card\",\"data\":{\"card_name\":\"Loan Security\",\"col\":4}},{\"id\":\"Um5YwxVLRJ\",\"type\":\"card\",\"data\":{\"card_name\":\"Reports\",\"col\":4}}]", + "content": "[{\"id\":\"_38WStznya\",\"type\":\"header\",\"data\":{\"text\":\"Your Shortcuts\",\"col\":12}},{\"id\":\"t7o_K__1jB\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Loan Application\",\"col\":3}},{\"id\":\"IRiNDC6w1p\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Loan\",\"col\":3}},{\"id\":\"xbbo0FYbq0\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Dashboard\",\"col\":3}},{\"id\":\"7ZL4Bro-Vi\",\"type\":\"spacer\",\"data\":{\"col\":12}},{\"id\":\"yhyioTViZ3\",\"type\":\"header\",\"data\":{\"text\":\"Reports & Masters\",\"col\":12}},{\"id\":\"oYFn4b1kSw\",\"type\":\"card\",\"data\":{\"card_name\":\"Loan\",\"col\":4}},{\"id\":\"vZepJF5tl9\",\"type\":\"card\",\"data\":{\"card_name\":\"Loan Processes\",\"col\":4}},{\"id\":\"k-393Mjhqe\",\"type\":\"card\",\"data\":{\"card_name\":\"Disbursement and Repayment\",\"col\":4}},{\"id\":\"6crJ0DBiBJ\",\"type\":\"card\",\"data\":{\"card_name\":\"Loan Security\",\"col\":4}},{\"id\":\"Um5YwxVLRJ\",\"type\":\"card\",\"data\":{\"card_name\":\"Reports\",\"col\":4}},{\"id\":\"g2NbPxffmo\",\"type\":\"spacer\",\"data\":{\"col\":12}},{\"id\":\"UKb6Ko91Ju\",\"type\":\"paragraph\",\"data\":{\"text\":\"Loan Management module will be removed from ERPNext in Version 15. Please install the Lending app to continue using it.\",\"col\":12}}]", "creation": "2020-03-12 16:35:55.299820", "custom_blocks": [], "docstatus": 0, @@ -280,7 +280,7 @@ "type": "Link" } ], - "modified": "2023-05-24 14:47:24.109945", + "modified": "2023-08-09 19:45:02.748408", "modified_by": "Administrator", "module": "Loan Management", "name": "Loans", From 8083c0b59e3e371a39f18280edd092e985e5d0cd Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Thu, 10 Aug 2023 10:02:33 +0530 Subject: [PATCH 082/501] fix: move company rename to long queue (cherry picked from commit 51690060853d719400b5dd020fab59fbf2346872) --- erpnext/setup/doctype/company/company.js | 1 + 1 file changed, 1 insertion(+) diff --git a/erpnext/setup/doctype/company/company.js b/erpnext/setup/doctype/company/company.js index e50ce449e45f..6aa400a53c70 100644 --- a/erpnext/setup/doctype/company/company.js +++ b/erpnext/setup/doctype/company/company.js @@ -18,6 +18,7 @@ frappe.ui.form.on("Company", { }); }, setup: function(frm) { + frm.__rename_queue = "long"; erpnext.company.setup_queries(frm); frm.set_query("parent_company", function() { From 19cfcea78efa065c4ab1daa23f71da240d1e0ec0 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Thu, 10 Aug 2023 11:29:20 +0530 Subject: [PATCH 083/501] fix: don't show disabled items in `Item Shortage Report` (backport #36550) (#36571) fix: don't show disabled items in `Item Shortage Report` (#36550) (cherry picked from commit 4a7fc1506f8f9f5ee1c65c9e073e6e57c137c391) Co-authored-by: s-aga-r --- .../report/item_shortage_report/item_shortage_report.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/erpnext/stock/report/item_shortage_report/item_shortage_report.py b/erpnext/stock/report/item_shortage_report/item_shortage_report.py index 9fafe91c3f96..4bd9a107e2cc 100644 --- a/erpnext/stock/report/item_shortage_report/item_shortage_report.py +++ b/erpnext/stock/report/item_shortage_report/item_shortage_report.py @@ -40,7 +40,12 @@ def get_data(filters): item.item_name, item.description, ) - .where((bin.projected_qty < 0) & (wh.name == bin.warehouse) & (bin.item_code == item.name)) + .where( + (item.disabled == 0) + & (bin.projected_qty < 0) + & (wh.name == bin.warehouse) + & (bin.item_code == item.name) + ) .orderby(bin.projected_qty) ) From a864e07d4ff2cf2d22754ad9d23a2a3718ece770 Mon Sep 17 00:00:00 2001 From: rohitwaghchaure Date: Thu, 10 Aug 2023 13:32:31 +0530 Subject: [PATCH 084/501] fix: precision issue while submitting the stock entry (#36575) fix: precision issue while submmiting the stock entry --- erpnext/stock/doctype/material_request/material_request.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/erpnext/stock/doctype/material_request/material_request.py b/erpnext/stock/doctype/material_request/material_request.py index cf61f9657f4a..159fd32c1230 100644 --- a/erpnext/stock/doctype/material_request/material_request.py +++ b/erpnext/stock/doctype/material_request/material_request.py @@ -223,12 +223,13 @@ def update_completed_qty(self, mr_items=None, update_modified=True): mr_qty_allowance = frappe.db.get_single_value("Stock Settings", "mr_qty_allowance") for d in self.get("items"): + precision = d.precision("ordered_qty") if d.name in mr_items: if self.material_request_type in ("Material Issue", "Material Transfer", "Customer Provided"): d.ordered_qty = flt(mr_items_ordered_qty.get(d.name)) if mr_qty_allowance: - allowed_qty = flt((d.qty + (d.qty * (mr_qty_allowance / 100))), d.precision("ordered_qty")) + allowed_qty = flt((d.qty + (d.qty * (mr_qty_allowance / 100))), precision) if d.ordered_qty and d.ordered_qty > allowed_qty: frappe.throw( @@ -237,11 +238,11 @@ def update_completed_qty(self, mr_items=None, update_modified=True): ).format(d.ordered_qty, d.parent, allowed_qty, d.item_code) ) - elif d.ordered_qty and d.ordered_qty > d.stock_qty: + elif d.ordered_qty and flt(d.ordered_qty, precision) > flt(d.stock_qty, precision): frappe.throw( _( "The total Issue / Transfer quantity {0} in Material Request {1} cannot be greater than requested quantity {2} for Item {3}" - ).format(d.ordered_qty, d.parent, d.qty, d.item_code) + ).format(d.ordered_qty, d.parent, d.stock_qty, d.item_code) ) elif self.material_request_type == "Manufacture": From d6ebd1b6f8e1ceed4245eba75f624a057dece13d Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Thu, 10 Aug 2023 13:34:36 +0530 Subject: [PATCH 085/501] fix: precision issue while submitting the stock entry (backport #36575) (#36576) fix: precision issue while submitting the stock entry (#36575) fix: precision issue while submmiting the stock entry (cherry picked from commit a864e07d4ff2cf2d22754ad9d23a2a3718ece770) Co-authored-by: rohitwaghchaure --- erpnext/stock/doctype/material_request/material_request.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/erpnext/stock/doctype/material_request/material_request.py b/erpnext/stock/doctype/material_request/material_request.py index cf61f9657f4a..159fd32c1230 100644 --- a/erpnext/stock/doctype/material_request/material_request.py +++ b/erpnext/stock/doctype/material_request/material_request.py @@ -223,12 +223,13 @@ def update_completed_qty(self, mr_items=None, update_modified=True): mr_qty_allowance = frappe.db.get_single_value("Stock Settings", "mr_qty_allowance") for d in self.get("items"): + precision = d.precision("ordered_qty") if d.name in mr_items: if self.material_request_type in ("Material Issue", "Material Transfer", "Customer Provided"): d.ordered_qty = flt(mr_items_ordered_qty.get(d.name)) if mr_qty_allowance: - allowed_qty = flt((d.qty + (d.qty * (mr_qty_allowance / 100))), d.precision("ordered_qty")) + allowed_qty = flt((d.qty + (d.qty * (mr_qty_allowance / 100))), precision) if d.ordered_qty and d.ordered_qty > allowed_qty: frappe.throw( @@ -237,11 +238,11 @@ def update_completed_qty(self, mr_items=None, update_modified=True): ).format(d.ordered_qty, d.parent, allowed_qty, d.item_code) ) - elif d.ordered_qty and d.ordered_qty > d.stock_qty: + elif d.ordered_qty and flt(d.ordered_qty, precision) > flt(d.stock_qty, precision): frappe.throw( _( "The total Issue / Transfer quantity {0} in Material Request {1} cannot be greater than requested quantity {2} for Item {3}" - ).format(d.ordered_qty, d.parent, d.qty, d.item_code) + ).format(d.ordered_qty, d.parent, d.stock_qty, d.item_code) ) elif self.material_request_type == "Manufacture": From 10e25972e177518d3cf6932cae374704fdcf074c Mon Sep 17 00:00:00 2001 From: Frappe PR Bot Date: Thu, 10 Aug 2023 08:06:21 +0000 Subject: [PATCH 086/501] chore(release): Bumped to Version 14.34.1 ## [14.34.1](https://github.com/frappe/erpnext/compare/v14.34.0...v14.34.1) (2023-08-10) ### Bug Fixes * precision issue while submitting the stock entry (backport [#36575](https://github.com/frappe/erpnext/issues/36575)) ([#36576](https://github.com/frappe/erpnext/issues/36576)) ([d6ebd1b](https://github.com/frappe/erpnext/commit/d6ebd1b6f8e1ceed4245eba75f624a057dece13d)) --- erpnext/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/__init__.py b/erpnext/__init__.py index 89ea6e4289b6..9db7e24dc399 100644 --- a/erpnext/__init__.py +++ b/erpnext/__init__.py @@ -3,7 +3,7 @@ import frappe -__version__ = "14.34.0" +__version__ = "14.34.1" def get_default_company(user=None): From 5443592d84c180e27168afb624d43e36645f42f8 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Wed, 9 Aug 2023 20:43:51 +0530 Subject: [PATCH 087/501] fix: better remarks on Cr note created by Reconciliation (cherry picked from commit 47cb349362cd6a03cd544abea5483aff23b27e7e) # Conflicts: # erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py --- .../doctype/journal_entry/journal_entry.py | 3 +++ .../payment_reconciliation.py | 18 +++++++++++++++++- 2 files changed, 20 insertions(+), 1 deletion(-) diff --git a/erpnext/accounts/doctype/journal_entry/journal_entry.py b/erpnext/accounts/doctype/journal_entry/journal_entry.py index 594339591f52..8c5cc2c921f7 100644 --- a/erpnext/accounts/doctype/journal_entry/journal_entry.py +++ b/erpnext/accounts/doctype/journal_entry/journal_entry.py @@ -775,6 +775,9 @@ def set_exchange_rate(self): def create_remarks(self): r = [] + if self.flags.skip_remarks_creation: + return + if self.user_remark: r.append(_("Note: {0}").format(self.user_remark)) diff --git a/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py b/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py index 216d4eccac7f..cc9e2bc5a11e 100644 --- a/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py +++ b/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py @@ -6,7 +6,7 @@ from frappe import _, msgprint, qb from frappe.model.document import Document from frappe.query_builder.custom import ConstantColumn -from frappe.utils import flt, get_link_to_form, getdate, nowdate, today +from frappe.utils import flt, fmt_money, get_link_to_form, getdate, nowdate, today import erpnext from erpnext.accounts.doctype.process_payment_reconciliation.process_payment_reconciliation import ( @@ -640,6 +640,11 @@ def get_difference_row(inv): "reference_type": inv.against_voucher_type, "reference_name": inv.against_voucher, "cost_center": erpnext.get_default_cost_center(company), +<<<<<<< HEAD +======= + "exchange_rate": inv.exchange_rate, + "user_remark": f"{fmt_money(flt(inv.allocated_amount), currency=company_currency)} against {inv.against_voucher}", +>>>>>>> 47cb349362 (fix: better remarks on Cr note created by Reconciliation) }, { "account": inv.account, @@ -653,6 +658,11 @@ def get_difference_row(inv): "reference_type": inv.voucher_type, "reference_name": inv.voucher_no, "cost_center": erpnext.get_default_cost_center(company), +<<<<<<< HEAD +======= + "exchange_rate": inv.exchange_rate, + "user_remark": f"{fmt_money(flt(inv.allocated_amount), currency=company_currency)} from {inv.voucher_no}", +>>>>>>> 47cb349362 (fix: better remarks on Cr note created by Reconciliation) }, ], } @@ -662,4 +672,10 @@ def get_difference_row(inv): jv.append("accounts", difference_entry) jv.flags.ignore_mandatory = True +<<<<<<< HEAD +======= + jv.flags.ignore_exchange_rate = True + jv.remark = None + jv.flags.skip_remarks_creation = True +>>>>>>> 47cb349362 (fix: better remarks on Cr note created by Reconciliation) jv.submit() From 00c7dbceaab6a676e31a0214feb4b083f249c019 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Wed, 9 Aug 2023 20:50:11 +0530 Subject: [PATCH 088/501] refactor: add `is_system_generated` field to Journal Entry (cherry picked from commit 3997aa77d40a5e9f6b6fd0554639be54bb977462) # Conflicts: # erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py --- .../doctype/journal_entry/journal_entry.json | 53 +++++-------------- .../payment_reconciliation.py | 4 ++ 2 files changed, 16 insertions(+), 41 deletions(-) diff --git a/erpnext/accounts/doctype/journal_entry/journal_entry.json b/erpnext/accounts/doctype/journal_entry/journal_entry.json index 80e72226d3d3..75c32a501895 100644 --- a/erpnext/accounts/doctype/journal_entry/journal_entry.json +++ b/erpnext/accounts/doctype/journal_entry/journal_entry.json @@ -9,6 +9,7 @@ "engine": "InnoDB", "field_order": [ "entry_type_and_date", + "is_system_generated", "title", "voucher_type", "naming_series", @@ -88,7 +89,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\nExchange Gain Or Loss\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\nReversal Of ITC", "reqd": 1, "search_index": 1 }, @@ -533,57 +534,27 @@ "label": "Process Deferred Accounting", "options": "Process Deferred Accounting", "read_only": 1 + }, + { + "default": "0", + "fieldname": "is_system_generated", + "fieldtype": "Check", + "hidden": 1, + "label": "Is System Generated", + "read_only": 1 } ], "icon": "fa fa-file-text", "idx": 176, "is_submittable": 1, "links": [], - "modified": "2023-03-01 14:58:59.286591", + "modified": "2023-08-09 20:47:27.719809", "modified_by": "Administrator", "module": "Accounts", "name": "Journal Entry", "naming_rule": "By \"Naming Series\" field", "owner": "Administrator", - "permissions": [ - { - "amend": 1, - "cancel": 1, - "create": 1, - "delete": 1, - "email": 1, - "print": 1, - "read": 1, - "report": 1, - "role": "Accounts User", - "share": 1, - "submit": 1, - "write": 1 - }, - { - "amend": 1, - "cancel": 1, - "create": 1, - "delete": 1, - "email": 1, - "export": 1, - "import": 1, - "print": 1, - "read": 1, - "report": 1, - "role": "Accounts Manager", - "share": 1, - "submit": 1, - "write": 1 - }, - { - "email": 1, - "print": 1, - "read": 1, - "report": 1, - "role": "Auditor" - } - ], + "permissions": [], "search_fields": "voucher_type,posting_date, due_date, cheque_no", "sort_field": "modified", "sort_order": "DESC", diff --git a/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py b/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py index cc9e2bc5a11e..91c79ed1f8b7 100644 --- a/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py +++ b/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py @@ -677,5 +677,9 @@ def get_difference_row(inv): jv.flags.ignore_exchange_rate = True jv.remark = None jv.flags.skip_remarks_creation = True +<<<<<<< HEAD >>>>>>> 47cb349362 (fix: better remarks on Cr note created by Reconciliation) +======= + jv.is_system_generated = True +>>>>>>> 3997aa77d4 (refactor: add `is_system_generated` field to Journal Entry) jv.submit() From 9b0d30c56b7ac2e36d63c59ce1156c35a7b2a834 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Thu, 10 Aug 2023 10:05:25 +0530 Subject: [PATCH 089/501] refactor: set flag display condition (cherry picked from commit de17eaef3857f2128303d20ec291dd7aedb61569) --- erpnext/accounts/doctype/journal_entry/journal_entry.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/erpnext/accounts/doctype/journal_entry/journal_entry.json b/erpnext/accounts/doctype/journal_entry/journal_entry.json index 75c32a501895..2812786381c7 100644 --- a/erpnext/accounts/doctype/journal_entry/journal_entry.json +++ b/erpnext/accounts/doctype/journal_entry/journal_entry.json @@ -537,9 +537,9 @@ }, { "default": "0", + "depends_on": "eval:doc.is_system_generated == 1;", "fieldname": "is_system_generated", "fieldtype": "Check", - "hidden": 1, "label": "Is System Generated", "read_only": 1 } @@ -548,7 +548,7 @@ "idx": 176, "is_submittable": 1, "links": [], - "modified": "2023-08-09 20:47:27.719809", + "modified": "2023-08-10 09:54:52.060639", "modified_by": "Administrator", "module": "Accounts", "name": "Journal Entry", From a04471024d79d0124dedc5dbb6b6b41a122baa91 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Thu, 10 Aug 2023 14:32:37 +0530 Subject: [PATCH 090/501] refactor: enable 'no-copy' (cherry picked from commit 4ed4b0240d04b0bbf05afa27711d0370503380af) --- erpnext/accounts/doctype/journal_entry/journal_entry.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/erpnext/accounts/doctype/journal_entry/journal_entry.json b/erpnext/accounts/doctype/journal_entry/journal_entry.json index 2812786381c7..80df0ff0dff1 100644 --- a/erpnext/accounts/doctype/journal_entry/journal_entry.json +++ b/erpnext/accounts/doctype/journal_entry/journal_entry.json @@ -541,6 +541,7 @@ "fieldname": "is_system_generated", "fieldtype": "Check", "label": "Is System Generated", + "no_copy": 1, "read_only": 1 } ], @@ -548,7 +549,7 @@ "idx": 176, "is_submittable": 1, "links": [], - "modified": "2023-08-10 09:54:52.060639", + "modified": "2023-08-10 14:32:22.366895", "modified_by": "Administrator", "module": "Accounts", "name": "Journal Entry", From b49309c160fe20af7a7ee6fdccdc8e07382b4e76 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Thu, 10 Aug 2023 14:40:47 +0530 Subject: [PATCH 091/501] fix: unhide `uom` and `stock_uom` fields in print view (cherry picked from commit 11cd163db72943c54de250b7a217d2ccd44b4c87) --- erpnext/controllers/print_settings.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/erpnext/controllers/print_settings.py b/erpnext/controllers/print_settings.py index c951154a9e04..b906a8a79872 100644 --- a/erpnext/controllers/print_settings.py +++ b/erpnext/controllers/print_settings.py @@ -13,9 +13,6 @@ def set_print_templates_for_item_table(doc, settings): } } - if doc.meta.get_field("items"): - doc.meta.get_field("items").hide_in_print_layout = ["uom", "stock_uom"] - doc.flags.compact_item_fields = ["description", "qty", "rate", "amount"] if settings.compact_item_print: From 2800ad39d2adc40b932b4626a1be4df89916973c Mon Sep 17 00:00:00 2001 From: rohitwaghchaure Date: Thu, 10 Aug 2023 17:22:14 +0530 Subject: [PATCH 092/501] fix: incorrect available qty for backdated stock reco with batch (#36581) --- .../stock_reconciliation.py | 62 +++++++++++------- .../test_stock_reconciliation.py | 63 ++++++++++++++++--- erpnext/stock/stock_ledger.py | 41 +++++------- 3 files changed, 110 insertions(+), 56 deletions(-) diff --git a/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py b/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py index c6c8571b9efa..0d15bd75ad30 100644 --- a/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py +++ b/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py @@ -6,7 +6,7 @@ import frappe from frappe import _, bold, msgprint from frappe.query_builder.functions import CombineDatetime, Sum -from frappe.utils import cint, cstr, flt +from frappe.utils import add_to_date, cint, cstr, flt import erpnext from erpnext.accounts.utils import get_company_default @@ -570,44 +570,58 @@ def cancel(self): else: self._cancel() - def recalculate_current_qty(self, item_code, batch_no): + def recalculate_current_qty(self, voucher_detail_no, sle_creation, add_new_sle=False): from erpnext.stock.stock_ledger import get_valuation_rate sl_entries = [] for row in self.items: - if not (row.item_code == item_code and row.batch_no == batch_no): + if voucher_detail_no != row.name: continue current_qty = get_batch_qty_for_stock_reco( - item_code, row.warehouse, batch_no, self.posting_date, self.posting_time, self.name + row.item_code, row.warehouse, row.batch_no, self.posting_date, self.posting_time, self.name ) precesion = row.precision("current_qty") - if flt(current_qty, precesion) == flt(row.current_qty, precesion): - continue - - val_rate = get_valuation_rate( - item_code, row.warehouse, self.doctype, self.name, company=self.company, batch_no=batch_no - ) + if flt(current_qty, precesion) != flt(row.current_qty, precesion): + val_rate = get_valuation_rate( + row.item_code, + row.warehouse, + self.doctype, + self.name, + company=self.company, + batch_no=row.batch_no, + ) - row.current_valuation_rate = val_rate - if not row.current_qty and current_qty: - sle = self.get_sle_for_items(row) - sle.actual_qty = current_qty * -1 - sle.valuation_rate = val_rate - sl_entries.append(sle) + row.current_valuation_rate = val_rate + row.current_qty = current_qty + row.db_set( + { + "current_qty": row.current_qty, + "current_valuation_rate": row.current_valuation_rate, + "current_amount": flt(row.current_qty * row.current_valuation_rate), + } + ) - row.current_qty = current_qty - row.db_set( - { - "current_qty": row.current_qty, - "current_valuation_rate": row.current_valuation_rate, - "current_amount": flt(row.current_qty * row.current_valuation_rate), - } - ) + if ( + add_new_sle + and not frappe.db.get_value( + "Stock Ledger Entry", + {"voucher_detail_no": row.name, "actual_qty": ("<", 0), "is_cancelled": 0}, + "name", + ) + and current_qty + ): + new_sle = self.get_sle_for_items(row) + new_sle.actual_qty = current_qty * -1 + new_sle.valuation_rate = row.current_valuation_rate + new_sle.creation_time = add_to_date(sle_creation, seconds=-1) + sl_entries.append(new_sle) if sl_entries: self.make_sl_entries(sl_entries, allow_negative_stock=True) + if frappe.db.exists("Repost Item Valuation", {"voucher_no": self.name, "status": "Queued"}): + self.repost_future_sle_and_gle(force=True) def get_batch_qty_for_stock_reco( diff --git a/erpnext/stock/doctype/stock_reconciliation/test_stock_reconciliation.py b/erpnext/stock/doctype/stock_reconciliation/test_stock_reconciliation.py index 88d4e4689772..1d8b72cec9a2 100644 --- a/erpnext/stock/doctype/stock_reconciliation/test_stock_reconciliation.py +++ b/erpnext/stock/doctype/stock_reconciliation/test_stock_reconciliation.py @@ -759,13 +759,6 @@ def test_backdated_stock_reco_entry(self): se2.cancel() - self.assertTrue(frappe.db.exists("Repost Item Valuation", {"voucher_no": stock_reco.name})) - - self.assertEqual( - frappe.db.get_value("Repost Item Valuation", {"voucher_no": stock_reco.name}, "status"), - "Completed", - ) - sle = frappe.get_all( "Stock Ledger Entry", filters={"item_code": item_code, "warehouse": warehouse, "is_cancelled": 0}, @@ -775,6 +768,62 @@ def test_backdated_stock_reco_entry(self): self.assertEqual(flt(sle[0].qty_after_transaction), flt(50.0)) + def test_backdated_stock_reco_entry_with_batch(self): + from erpnext.stock.doctype.stock_entry.test_stock_entry import make_stock_entry + + item_code = self.make_item( + "Test New Batch Item ABCVSD", + { + "is_stock_item": 1, + "has_batch_no": 1, + "batch_number_series": "BNS9.####", + "create_new_batch": 1, + }, + ).name + + warehouse = "_Test Warehouse - _TC" + + # Stock Reco for 100, Balace Qty 100 + stock_reco = create_stock_reconciliation( + item_code=item_code, + posting_date=nowdate(), + posting_time="11:00:00", + warehouse=warehouse, + qty=100, + rate=100, + ) + + sles = frappe.get_all( + "Stock Ledger Entry", + fields=["actual_qty", "batch_no"], + filters={"voucher_no": stock_reco.name, "is_cancelled": 0}, + ) + + self.assertEqual(len(sles), 1) + + # Stock Reco for 100, Balace Qty 100 + create_stock_reconciliation( + item_code=item_code, + posting_date=add_days(nowdate(), -1), + posting_time="11:00:00", + batch_no=sles[0].batch_no, + warehouse=warehouse, + qty=60, + rate=100, + ) + + sles = frappe.get_all( + "Stock Ledger Entry", + fields=["actual_qty"], + filters={"voucher_no": stock_reco.name, "is_cancelled": 0}, + ) + + self.assertEqual(len(sles), 2) + + for row in sles: + if row.actual_qty < 0: + self.assertEqual(row.actual_qty, -60) + def test_update_stock_reconciliation_while_reposting(self): from erpnext.stock.doctype.stock_entry.test_stock_entry import make_stock_entry diff --git a/erpnext/stock/stock_ledger.py b/erpnext/stock/stock_ledger.py index d52d59a0d186..0c3056cc7059 100644 --- a/erpnext/stock/stock_ledger.py +++ b/erpnext/stock/stock_ledger.py @@ -199,6 +199,11 @@ def make_entry(args, allow_negative_stock=False, via_landed_cost_voucher=False): sle.allow_negative_stock = allow_negative_stock sle.via_landed_cost_voucher = via_landed_cost_voucher sle.submit() + + # Added to handle the case when the stock ledger entry is created from the repostig + if args.get("creation_time") and args.get("voucher_type") == "Stock Reconciliation": + sle.db_set("creation", args.get("creation_time")) + return sle @@ -564,12 +569,7 @@ def process_sle(self, sle): if not self.args.get("sle_id"): self.get_dynamic_incoming_outgoing_rate(sle) - if ( - sle.voucher_type == "Stock Reconciliation" - and sle.batch_no - and sle.voucher_detail_no - and sle.actual_qty < 0 - ): + if sle.voucher_type == "Stock Reconciliation" and sle.batch_no and sle.voucher_detail_no: self.reset_actual_qty_for_stock_reco(sle) if ( @@ -634,14 +634,17 @@ def process_sle(self, sle): self.update_outgoing_rate_on_transaction(sle) def reset_actual_qty_for_stock_reco(self, sle): - current_qty = frappe.get_cached_value( - "Stock Reconciliation Item", sle.voucher_detail_no, "current_qty" - ) + doc = frappe.get_cached_doc("Stock Reconciliation", sle.voucher_no) + doc.recalculate_current_qty(sle.voucher_detail_no, sle.creation, sle.actual_qty > 0) - if current_qty: - sle.actual_qty = current_qty * -1 - elif current_qty == 0: - sle.is_cancelled = 1 + if sle.actual_qty < 0: + sle.actual_qty = ( + flt(frappe.db.get_value("Stock Reconciliation Item", sle.voucher_detail_no, "current_qty")) + * -1 + ) + + if abs(sle.actual_qty) == 0.0: + sle.is_cancelled = 1 def validate_negative_stock(self, sle): """ @@ -1433,8 +1436,6 @@ def update_qty_in_future_sle(args, allow_negative_stock=False): next_stock_reco_detail = get_next_stock_reco(args) if next_stock_reco_detail: detail = next_stock_reco_detail[0] - if detail.batch_no: - regenerate_sle_for_batch_stock_reco(detail) # add condition to update SLEs before this date & time datetime_limit_condition = get_datetime_limit_condition(detail) @@ -1463,16 +1464,6 @@ def update_qty_in_future_sle(args, allow_negative_stock=False): validate_negative_qty_in_future_sle(args, allow_negative_stock) -def regenerate_sle_for_batch_stock_reco(detail): - doc = frappe.get_cached_doc("Stock Reconciliation", detail.voucher_no) - doc.recalculate_current_qty(detail.item_code, detail.batch_no) - - if not frappe.db.exists( - "Repost Item Valuation", {"voucher_no": doc.name, "status": "Queued", "docstatus": "1"} - ): - doc.repost_future_sle_and_gle(force=True) - - def get_stock_reco_qty_shift(args): stock_reco_qty_shift = 0 if args.get("is_cancelled"): From bb112eca054e11975265eaaeaf283a052c5d8c45 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Thu, 10 Aug 2023 17:57:17 +0530 Subject: [PATCH 093/501] fix: incorrect available qty for backdated stock reco with batch (backport #36581) (#36585) fix: incorrect available qty for backdated stock reco with batch (#36581) (cherry picked from commit 2800ad39d2adc40b932b4626a1be4df89916973c) Co-authored-by: rohitwaghchaure --- .../stock_reconciliation.py | 62 +++++++++++------- .../test_stock_reconciliation.py | 63 ++++++++++++++++--- erpnext/stock/stock_ledger.py | 41 +++++------- 3 files changed, 110 insertions(+), 56 deletions(-) diff --git a/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py b/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py index c6c8571b9efa..0d15bd75ad30 100644 --- a/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py +++ b/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py @@ -6,7 +6,7 @@ import frappe from frappe import _, bold, msgprint from frappe.query_builder.functions import CombineDatetime, Sum -from frappe.utils import cint, cstr, flt +from frappe.utils import add_to_date, cint, cstr, flt import erpnext from erpnext.accounts.utils import get_company_default @@ -570,44 +570,58 @@ def cancel(self): else: self._cancel() - def recalculate_current_qty(self, item_code, batch_no): + def recalculate_current_qty(self, voucher_detail_no, sle_creation, add_new_sle=False): from erpnext.stock.stock_ledger import get_valuation_rate sl_entries = [] for row in self.items: - if not (row.item_code == item_code and row.batch_no == batch_no): + if voucher_detail_no != row.name: continue current_qty = get_batch_qty_for_stock_reco( - item_code, row.warehouse, batch_no, self.posting_date, self.posting_time, self.name + row.item_code, row.warehouse, row.batch_no, self.posting_date, self.posting_time, self.name ) precesion = row.precision("current_qty") - if flt(current_qty, precesion) == flt(row.current_qty, precesion): - continue - - val_rate = get_valuation_rate( - item_code, row.warehouse, self.doctype, self.name, company=self.company, batch_no=batch_no - ) + if flt(current_qty, precesion) != flt(row.current_qty, precesion): + val_rate = get_valuation_rate( + row.item_code, + row.warehouse, + self.doctype, + self.name, + company=self.company, + batch_no=row.batch_no, + ) - row.current_valuation_rate = val_rate - if not row.current_qty and current_qty: - sle = self.get_sle_for_items(row) - sle.actual_qty = current_qty * -1 - sle.valuation_rate = val_rate - sl_entries.append(sle) + row.current_valuation_rate = val_rate + row.current_qty = current_qty + row.db_set( + { + "current_qty": row.current_qty, + "current_valuation_rate": row.current_valuation_rate, + "current_amount": flt(row.current_qty * row.current_valuation_rate), + } + ) - row.current_qty = current_qty - row.db_set( - { - "current_qty": row.current_qty, - "current_valuation_rate": row.current_valuation_rate, - "current_amount": flt(row.current_qty * row.current_valuation_rate), - } - ) + if ( + add_new_sle + and not frappe.db.get_value( + "Stock Ledger Entry", + {"voucher_detail_no": row.name, "actual_qty": ("<", 0), "is_cancelled": 0}, + "name", + ) + and current_qty + ): + new_sle = self.get_sle_for_items(row) + new_sle.actual_qty = current_qty * -1 + new_sle.valuation_rate = row.current_valuation_rate + new_sle.creation_time = add_to_date(sle_creation, seconds=-1) + sl_entries.append(new_sle) if sl_entries: self.make_sl_entries(sl_entries, allow_negative_stock=True) + if frappe.db.exists("Repost Item Valuation", {"voucher_no": self.name, "status": "Queued"}): + self.repost_future_sle_and_gle(force=True) def get_batch_qty_for_stock_reco( diff --git a/erpnext/stock/doctype/stock_reconciliation/test_stock_reconciliation.py b/erpnext/stock/doctype/stock_reconciliation/test_stock_reconciliation.py index 88d4e4689772..1d8b72cec9a2 100644 --- a/erpnext/stock/doctype/stock_reconciliation/test_stock_reconciliation.py +++ b/erpnext/stock/doctype/stock_reconciliation/test_stock_reconciliation.py @@ -759,13 +759,6 @@ def test_backdated_stock_reco_entry(self): se2.cancel() - self.assertTrue(frappe.db.exists("Repost Item Valuation", {"voucher_no": stock_reco.name})) - - self.assertEqual( - frappe.db.get_value("Repost Item Valuation", {"voucher_no": stock_reco.name}, "status"), - "Completed", - ) - sle = frappe.get_all( "Stock Ledger Entry", filters={"item_code": item_code, "warehouse": warehouse, "is_cancelled": 0}, @@ -775,6 +768,62 @@ def test_backdated_stock_reco_entry(self): self.assertEqual(flt(sle[0].qty_after_transaction), flt(50.0)) + def test_backdated_stock_reco_entry_with_batch(self): + from erpnext.stock.doctype.stock_entry.test_stock_entry import make_stock_entry + + item_code = self.make_item( + "Test New Batch Item ABCVSD", + { + "is_stock_item": 1, + "has_batch_no": 1, + "batch_number_series": "BNS9.####", + "create_new_batch": 1, + }, + ).name + + warehouse = "_Test Warehouse - _TC" + + # Stock Reco for 100, Balace Qty 100 + stock_reco = create_stock_reconciliation( + item_code=item_code, + posting_date=nowdate(), + posting_time="11:00:00", + warehouse=warehouse, + qty=100, + rate=100, + ) + + sles = frappe.get_all( + "Stock Ledger Entry", + fields=["actual_qty", "batch_no"], + filters={"voucher_no": stock_reco.name, "is_cancelled": 0}, + ) + + self.assertEqual(len(sles), 1) + + # Stock Reco for 100, Balace Qty 100 + create_stock_reconciliation( + item_code=item_code, + posting_date=add_days(nowdate(), -1), + posting_time="11:00:00", + batch_no=sles[0].batch_no, + warehouse=warehouse, + qty=60, + rate=100, + ) + + sles = frappe.get_all( + "Stock Ledger Entry", + fields=["actual_qty"], + filters={"voucher_no": stock_reco.name, "is_cancelled": 0}, + ) + + self.assertEqual(len(sles), 2) + + for row in sles: + if row.actual_qty < 0: + self.assertEqual(row.actual_qty, -60) + def test_update_stock_reconciliation_while_reposting(self): from erpnext.stock.doctype.stock_entry.test_stock_entry import make_stock_entry diff --git a/erpnext/stock/stock_ledger.py b/erpnext/stock/stock_ledger.py index d52d59a0d186..0c3056cc7059 100644 --- a/erpnext/stock/stock_ledger.py +++ b/erpnext/stock/stock_ledger.py @@ -199,6 +199,11 @@ def make_entry(args, allow_negative_stock=False, via_landed_cost_voucher=False): sle.allow_negative_stock = allow_negative_stock sle.via_landed_cost_voucher = via_landed_cost_voucher sle.submit() + + # Added to handle the case when the stock ledger entry is created from the repostig + if args.get("creation_time") and args.get("voucher_type") == "Stock Reconciliation": + sle.db_set("creation", args.get("creation_time")) + return sle @@ -564,12 +569,7 @@ def process_sle(self, sle): if not self.args.get("sle_id"): self.get_dynamic_incoming_outgoing_rate(sle) - if ( - sle.voucher_type == "Stock Reconciliation" - and sle.batch_no - and sle.voucher_detail_no - and sle.actual_qty < 0 - ): + if sle.voucher_type == "Stock Reconciliation" and sle.batch_no and sle.voucher_detail_no: self.reset_actual_qty_for_stock_reco(sle) if ( @@ -634,14 +634,17 @@ def process_sle(self, sle): self.update_outgoing_rate_on_transaction(sle) def reset_actual_qty_for_stock_reco(self, sle): - current_qty = frappe.get_cached_value( - "Stock Reconciliation Item", sle.voucher_detail_no, "current_qty" - ) + doc = frappe.get_cached_doc("Stock Reconciliation", sle.voucher_no) + doc.recalculate_current_qty(sle.voucher_detail_no, sle.creation, sle.actual_qty > 0) - if current_qty: - sle.actual_qty = current_qty * -1 - elif current_qty == 0: - sle.is_cancelled = 1 + if sle.actual_qty < 0: + sle.actual_qty = ( + flt(frappe.db.get_value("Stock Reconciliation Item", sle.voucher_detail_no, "current_qty")) + * -1 + ) + + if abs(sle.actual_qty) == 0.0: + sle.is_cancelled = 1 def validate_negative_stock(self, sle): """ @@ -1433,8 +1436,6 @@ def update_qty_in_future_sle(args, allow_negative_stock=False): next_stock_reco_detail = get_next_stock_reco(args) if next_stock_reco_detail: detail = next_stock_reco_detail[0] - if detail.batch_no: - regenerate_sle_for_batch_stock_reco(detail) # add condition to update SLEs before this date & time datetime_limit_condition = get_datetime_limit_condition(detail) @@ -1463,16 +1464,6 @@ def update_qty_in_future_sle(args, allow_negative_stock=False): validate_negative_qty_in_future_sle(args, allow_negative_stock) -def regenerate_sle_for_batch_stock_reco(detail): - doc = frappe.get_cached_doc("Stock Reconciliation", detail.voucher_no) - doc.recalculate_current_qty(detail.item_code, detail.batch_no) - - if not frappe.db.exists( - "Repost Item Valuation", {"voucher_no": doc.name, "status": "Queued", "docstatus": "1"} - ): - doc.repost_future_sle_and_gle(force=True) - - def get_stock_reco_qty_shift(args): stock_reco_qty_shift = 0 if args.get("is_cancelled"): From 35450d7bd93e38d679c65dee8953f7b0f4709ff7 Mon Sep 17 00:00:00 2001 From: Frappe PR Bot Date: Thu, 10 Aug 2023 12:28:54 +0000 Subject: [PATCH 094/501] chore(release): Bumped to Version 14.34.2 ## [14.34.2](https://github.com/frappe/erpnext/compare/v14.34.1...v14.34.2) (2023-08-10) ### Bug Fixes * incorrect available qty for backdated stock reco with batch (backport [#36581](https://github.com/frappe/erpnext/issues/36581)) ([#36585](https://github.com/frappe/erpnext/issues/36585)) ([bb112ec](https://github.com/frappe/erpnext/commit/bb112eca054e11975265eaaeaf283a052c5d8c45)) --- erpnext/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/__init__.py b/erpnext/__init__.py index 9db7e24dc399..2d9400749ffe 100644 --- a/erpnext/__init__.py +++ b/erpnext/__init__.py @@ -3,7 +3,7 @@ import frappe -__version__ = "14.34.1" +__version__ = "14.34.2" def get_default_company(user=None): From 29181274c8591c3028d556e3ded2c6e210c6cf12 Mon Sep 17 00:00:00 2001 From: Anand Baburajan Date: Thu, 10 Aug 2023 22:47:16 +0530 Subject: [PATCH 095/501] feat: daily asset depreciation method (#36587) * feat: daily asset depreciation method * chore: hide depr schedule if no schedules --- erpnext/assets/doctype/asset/asset.json | 3 +- erpnext/assets/doctype/asset/asset.py | 52 ++++++++++++++----- erpnext/assets/doctype/asset/test_asset.py | 35 +++++++++++++ .../asset_finance_book.json | 11 +++- 4 files changed, 87 insertions(+), 14 deletions(-) diff --git a/erpnext/assets/doctype/asset/asset.json b/erpnext/assets/doctype/asset/asset.json index 78cbe8621faf..060d991945b2 100644 --- a/erpnext/assets/doctype/asset/asset.json +++ b/erpnext/assets/doctype/asset/asset.json @@ -318,6 +318,7 @@ "label": "Depreciation Schedule" }, { + "depends_on": "schedules", "fieldname": "schedules", "fieldtype": "Table", "label": "Depreciation Schedule", @@ -537,7 +538,7 @@ "table_fieldname": "accounts" } ], - "modified": "2023-07-28 15:47:01.137996", + "modified": "2023-08-10 20:25:09.913073", "modified_by": "Administrator", "module": "Assets", "name": "Asset", diff --git a/erpnext/assets/doctype/asset/asset.py b/erpnext/assets/doctype/asset/asset.py index e6bac31d7d27..f4a1e3cc190c 100644 --- a/erpnext/assets/doctype/asset/asset.py +++ b/erpnext/assets/doctype/asset/asset.py @@ -1289,29 +1289,38 @@ def get_total_days(date, frequency): return date_diff(date, period_start_date) -@erpnext.allow_regional def get_depreciation_amount( asset, depreciable_value, - row, + fb_row, schedule_idx=0, prev_depreciation_amount=0, has_wdv_or_dd_non_yearly_pro_rata=False, ): - if row.depreciation_method in ("Straight Line", "Manual"): - return get_straight_line_or_manual_depr_amount(asset, row) + frappe.flags.company = asset.company + + if fb_row.depreciation_method in ("Straight Line", "Manual"): + return get_straight_line_or_manual_depr_amount(asset, fb_row, schedule_idx) else: + rate_of_depreciation = get_updated_rate_of_depreciation_for_wdv_and_dd( + asset, depreciable_value, fb_row + ) return get_wdv_or_dd_depr_amount( depreciable_value, - row.rate_of_depreciation, - row.frequency_of_depreciation, + rate_of_depreciation, + fb_row.frequency_of_depreciation, schedule_idx, prev_depreciation_amount, has_wdv_or_dd_non_yearly_pro_rata, ) -def get_straight_line_or_manual_depr_amount(asset, row): +@erpnext.allow_regional +def get_updated_rate_of_depreciation_for_wdv_and_dd(asset, depreciable_value, fb_row): + return fb_row.rate_of_depreciation + + +def get_straight_line_or_manual_depr_amount(asset, row, schedule_idx): # if the Depreciation Schedule is being modified after Asset Repair due to increase in asset life and value if asset.flags.increase_in_asset_life: return (flt(row.value_after_depreciation) - flt(row.expected_value_after_useful_life)) / ( @@ -1324,11 +1333,30 @@ def get_straight_line_or_manual_depr_amount(asset, row): ) # if the Depreciation Schedule is being prepared for the first time else: - return ( - flt(asset.gross_purchase_amount) - - flt(asset.opening_accumulated_depreciation) - - flt(row.expected_value_after_useful_life) - ) / flt(row.total_number_of_depreciations - asset.number_of_depreciations_booked) + if row.daily_depreciation: + daily_depr_amount = ( + flt(asset.gross_purchase_amount) + - flt(asset.opening_accumulated_depreciation) + - flt(row.expected_value_after_useful_life) + ) / date_diff( + add_months( + row.depreciation_start_date, + flt(row.total_number_of_depreciations - asset.number_of_depreciations_booked) + * row.frequency_of_depreciation, + ), + row.depreciation_start_date, + ) + to_date = add_months(row.depreciation_start_date, schedule_idx * row.frequency_of_depreciation) + from_date = add_months( + row.depreciation_start_date, (schedule_idx - 1) * row.frequency_of_depreciation + ) + return daily_depr_amount * date_diff(to_date, from_date) + else: + return ( + flt(asset.gross_purchase_amount) + - flt(asset.opening_accumulated_depreciation) + - flt(row.expected_value_after_useful_life) + ) / flt(row.total_number_of_depreciations - asset.number_of_depreciations_booked) def get_wdv_or_dd_depr_amount( diff --git a/erpnext/assets/doctype/asset/test_asset.py b/erpnext/assets/doctype/asset/test_asset.py index fea6ed3d2bd0..a2826d929b80 100644 --- a/erpnext/assets/doctype/asset/test_asset.py +++ b/erpnext/assets/doctype/asset/test_asset.py @@ -730,6 +730,40 @@ def test_schedule_for_straight_line_method_for_existing_asset(self): self.assertEqual(schedules, expected_schedules) + def test_schedule_for_straight_line_method_with_daily_depreciation(self): + asset = create_asset( + calculate_depreciation=1, + available_for_use_date="2023-01-01", + purchase_date="2023-01-01", + gross_purchase_amount=12000, + depreciation_start_date="2023-01-31", + total_number_of_depreciations=12, + frequency_of_depreciation=1, + daily_depreciation=1, + ) + + expected_schedules = [ + ["2023-01-31", 1019.18, 1019.18], + ["2023-02-28", 920.55, 1939.73], + ["2023-03-31", 1019.18, 2958.91], + ["2023-04-30", 986.3, 3945.21], + ["2023-05-31", 1019.18, 4964.39], + ["2023-06-30", 986.3, 5950.69], + ["2023-07-31", 1019.18, 6969.87], + ["2023-08-31", 1019.18, 7989.05], + ["2023-09-30", 986.3, 8975.35], + ["2023-10-31", 1019.18, 9994.53], + ["2023-11-30", 986.3, 10980.83], + ["2023-12-31", 1019.17, 12000.0], + ] + + schedules = [ + [cstr(d.schedule_date), d.depreciation_amount, d.accumulated_depreciation_amount] + for d in asset.get("schedules") + ] + + self.assertEqual(schedules, expected_schedules) + def test_schedule_for_double_declining_method(self): asset = create_asset( calculate_depreciation=1, @@ -1653,6 +1687,7 @@ def create_asset(**args): "total_number_of_depreciations": args.total_number_of_depreciations or 5, "expected_value_after_useful_life": args.expected_value_after_useful_life or 0, "depreciation_start_date": args.depreciation_start_date, + "daily_depreciation": args.daily_depreciation or 0, }, ) diff --git a/erpnext/assets/doctype/asset_finance_book/asset_finance_book.json b/erpnext/assets/doctype/asset_finance_book/asset_finance_book.json index e5a5f194c1bc..1f80e3a67bda 100644 --- a/erpnext/assets/doctype/asset_finance_book/asset_finance_book.json +++ b/erpnext/assets/doctype/asset_finance_book/asset_finance_book.json @@ -8,6 +8,7 @@ "finance_book", "depreciation_method", "total_number_of_depreciations", + "daily_depreciation", "column_break_5", "frequency_of_depreciation", "depreciation_start_date", @@ -79,12 +80,19 @@ "fieldname": "rate_of_depreciation", "fieldtype": "Percent", "label": "Rate of Depreciation" + }, + { + "default": "0", + "depends_on": "eval:doc.depreciation_method == \"Straight Line\" || doc.depreciation_method == \"Manual\"", + "fieldname": "daily_depreciation", + "fieldtype": "Check", + "label": "Daily Depreciation" } ], "index_web_pages_for_search": 1, "istable": 1, "links": [], - "modified": "2021-06-17 12:59:05.743683", + "modified": "2023-08-10 18:56:09.022246", "modified_by": "Administrator", "module": "Assets", "name": "Asset Finance Book", @@ -93,5 +101,6 @@ "quick_entry": 1, "sort_field": "modified", "sort_order": "DESC", + "states": [], "track_changes": 1 } \ No newline at end of file From 727a379581caf0ef3553f095beb835df73c3ec7b Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Fri, 11 Aug 2023 09:10:10 +0530 Subject: [PATCH 096/501] chore: remove unwanted 'Reversal of ITC' and merge conflicts --- .../doctype/journal_entry/journal_entry.json | 2 +- .../payment_reconciliation.py | 15 --------------- 2 files changed, 1 insertion(+), 16 deletions(-) diff --git a/erpnext/accounts/doctype/journal_entry/journal_entry.json b/erpnext/accounts/doctype/journal_entry/journal_entry.json index 80df0ff0dff1..cff84dc9252c 100644 --- a/erpnext/accounts/doctype/journal_entry/journal_entry.json +++ b/erpnext/accounts/doctype/journal_entry/journal_entry.json @@ -89,7 +89,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\nExchange Gain Or Loss\nDeferred Revenue\nDeferred Expense\nReversal Of ITC", + "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 }, diff --git a/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py b/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py index 91c79ed1f8b7..05623c09932b 100644 --- a/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py +++ b/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py @@ -640,11 +640,7 @@ def get_difference_row(inv): "reference_type": inv.against_voucher_type, "reference_name": inv.against_voucher, "cost_center": erpnext.get_default_cost_center(company), -<<<<<<< HEAD -======= - "exchange_rate": inv.exchange_rate, "user_remark": f"{fmt_money(flt(inv.allocated_amount), currency=company_currency)} against {inv.against_voucher}", ->>>>>>> 47cb349362 (fix: better remarks on Cr note created by Reconciliation) }, { "account": inv.account, @@ -658,11 +654,7 @@ def get_difference_row(inv): "reference_type": inv.voucher_type, "reference_name": inv.voucher_no, "cost_center": erpnext.get_default_cost_center(company), -<<<<<<< HEAD -======= - "exchange_rate": inv.exchange_rate, "user_remark": f"{fmt_money(flt(inv.allocated_amount), currency=company_currency)} from {inv.voucher_no}", ->>>>>>> 47cb349362 (fix: better remarks on Cr note created by Reconciliation) }, ], } @@ -672,14 +664,7 @@ def get_difference_row(inv): jv.append("accounts", difference_entry) jv.flags.ignore_mandatory = True -<<<<<<< HEAD -======= - jv.flags.ignore_exchange_rate = True jv.remark = None jv.flags.skip_remarks_creation = True -<<<<<<< HEAD ->>>>>>> 47cb349362 (fix: better remarks on Cr note created by Reconciliation) -======= jv.is_system_generated = True ->>>>>>> 3997aa77d4 (refactor: add `is_system_generated` field to Journal Entry) jv.submit() From 9821f903507da403111367fb098260db0884c61e Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Fri, 11 Aug 2023 10:55:43 +0530 Subject: [PATCH 097/501] chore: update permissions for Process Payment Reconciliation (cherry picked from commit cd28d15292a77060c2ec9b582e4b0f5635f7b112) --- .../process_payment_reconciliation.json | 22 ++++++++++++++----- 1 file changed, 16 insertions(+), 6 deletions(-) diff --git a/erpnext/accounts/doctype/process_payment_reconciliation/process_payment_reconciliation.json b/erpnext/accounts/doctype/process_payment_reconciliation/process_payment_reconciliation.json index 8bb7092dc502..1a1ab4d800e8 100644 --- a/erpnext/accounts/doctype/process_payment_reconciliation/process_payment_reconciliation.json +++ b/erpnext/accounts/doctype/process_payment_reconciliation/process_payment_reconciliation.json @@ -146,7 +146,7 @@ "index_web_pages_for_search": 1, "is_submittable": 1, "links": [], - "modified": "2023-04-21 17:19:30.912953", + "modified": "2023-08-11 10:56:51.699137", "modified_by": "Administrator", "module": "Accounts", "name": "Process Payment Reconciliation", @@ -154,15 +154,25 @@ "owner": "Administrator", "permissions": [ { + "amend": 1, + "cancel": 1, "create": 1, "delete": 1, - "email": 1, - "export": 1, - "print": 1, "read": 1, - "report": 1, - "role": "System Manager", + "role": "Accounts Manager", "share": 1, + "submit": 1, + "write": 1 + }, + { + "amend": 1, + "cancel": 1, + "create": 1, + "delete": 1, + "read": 1, + "role": "Accounts User", + "share": 1, + "submit": 1, "write": 1 } ], From ee04c6d5c52428f6f8e28fdf1a4e25075bee59ff Mon Sep 17 00:00:00 2001 From: rohitwaghchaure Date: Fri, 11 Aug 2023 13:57:27 +0530 Subject: [PATCH 098/501] fix: allow negative stock condition for batch item (#36586) * fix: allow negative stock condition for batch item * fix: test case --- .../stock_reconciliation.py | 24 +++++---- .../test_stock_reconciliation.py | 54 +++++++++++++++++-- erpnext/stock/stock_ledger.py | 5 +- 3 files changed, 65 insertions(+), 18 deletions(-) diff --git a/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py b/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py index 0d15bd75ad30..bb1a9b362141 100644 --- a/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py +++ b/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py @@ -282,11 +282,7 @@ def update_stock_ledger(self): if has_serial_no: sl_entries = self.merge_similar_item_serial_nos(sl_entries) - allow_negative_stock = False - if has_batch_no: - allow_negative_stock = True - - self.make_sl_entries(sl_entries, allow_negative_stock=allow_negative_stock) + self.make_sl_entries(sl_entries, allow_negative_stock=self.has_negative_stock_allowed()) if has_serial_no and sl_entries: self.update_valuation_rate_for_serial_no() @@ -457,10 +453,7 @@ def make_sle_on_cancel(self): sl_entries = self.merge_similar_item_serial_nos(sl_entries) sl_entries.reverse() - allow_negative_stock = cint( - frappe.db.get_single_value("Stock Settings", "allow_negative_stock") - ) - self.make_sl_entries(sl_entries, allow_negative_stock=allow_negative_stock) + self.make_sl_entries(sl_entries, allow_negative_stock=self.has_negative_stock_allowed()) def merge_similar_item_serial_nos(self, sl_entries): # If user has put the same item in multiple row with different serial no @@ -574,6 +567,7 @@ def recalculate_current_qty(self, voucher_detail_no, sle_creation, add_new_sle=F from erpnext.stock.stock_ledger import get_valuation_rate sl_entries = [] + for row in self.items: if voucher_detail_no != row.name: continue @@ -619,10 +613,18 @@ def recalculate_current_qty(self, voucher_detail_no, sle_creation, add_new_sle=F sl_entries.append(new_sle) if sl_entries: - self.make_sl_entries(sl_entries, allow_negative_stock=True) - if frappe.db.exists("Repost Item Valuation", {"voucher_no": self.name, "status": "Queued"}): + self.make_sl_entries(sl_entries, allow_negative_stock=self.has_negative_stock_allowed()) + if not frappe.db.exists("Repost Item Valuation", {"voucher_no": self.name, "status": "Queued"}): self.repost_future_sle_and_gle(force=True) + def has_negative_stock_allowed(self): + allow_negative_stock = cint(frappe.db.get_single_value("Stock Settings", "allow_negative_stock")) + + if all(d.batch_no and flt(d.qty) == flt(d.current_qty) for d in self.items): + allow_negative_stock = True + + return allow_negative_stock + def get_batch_qty_for_stock_reco( item_code, warehouse, batch_no, posting_date, posting_time, voucher_no diff --git a/erpnext/stock/doctype/stock_reconciliation/test_stock_reconciliation.py b/erpnext/stock/doctype/stock_reconciliation/test_stock_reconciliation.py index 1d8b72cec9a2..df6777bbe4ca 100644 --- a/erpnext/stock/doctype/stock_reconciliation/test_stock_reconciliation.py +++ b/erpnext/stock/doctype/stock_reconciliation/test_stock_reconciliation.py @@ -769,8 +769,6 @@ def test_backdated_stock_reco_entry(self): self.assertEqual(flt(sle[0].qty_after_transaction), flt(50.0)) def test_backdated_stock_reco_entry_with_batch(self): - from erpnext.stock.doctype.stock_entry.test_stock_entry import make_stock_entry - item_code = self.make_item( "Test New Batch Item ABCVSD", { @@ -868,6 +866,56 @@ def test_update_stock_reconciliation_while_reposting(self): sr1.load_from_db() self.assertEqual(sr1.difference_amount, 10000) + @change_settings("Stock Settings", {"allow_negative_stock": 0}) + def test_negative_stock_reco_for_batch(self): + from erpnext.stock.doctype.stock_entry.test_stock_entry import make_stock_entry + + item_code = self.make_item( + "Test New Batch Item ABCVSD", + { + "is_stock_item": 1, + "has_batch_no": 1, + "batch_number_series": "BNS9.####", + "create_new_batch": 1, + }, + ).name + + warehouse = "_Test Warehouse - _TC" + + # Added 100 Qty, Balace Qty 100 + se = make_stock_entry( + item_code=item_code, + target=warehouse, + qty=100, + basic_rate=100, + posting_date=add_days(nowdate(), -2), + ) + + # Removed 100 Qty, Balace Qty 0 + make_stock_entry( + item_code=item_code, + source=warehouse, + qty=100, + batch_no=se.items[0].batch_no, + basic_rate=100, + posting_date=nowdate(), + ) + + # Remove 100 qty, Balace Qty -100 + sr = create_stock_reconciliation( + item_code=item_code, + warehouse=warehouse, + qty=0, + rate=0, + batch_no=se.items[0].batch_no, + posting_date=add_days(nowdate(), -1), + posting_time="11:00:00", + do_not_submit=True, + ) + + # Check if Negative Stock is blocked + self.assertRaises(frappe.ValidationError, sr.submit) + def create_batch_item_with_batch(item_name, batch_id): batch_item_doc = create_item(item_name, is_stock_item=1) @@ -891,7 +939,7 @@ def insert_existing_sle(warehouse, item_code="_Test Item"): posting_time="02:00", item_code=item_code, target=warehouse, - qty=10, + qty=15, basic_rate=700, ) diff --git a/erpnext/stock/stock_ledger.py b/erpnext/stock/stock_ledger.py index 0c3056cc7059..d8284af60473 100644 --- a/erpnext/stock/stock_ledger.py +++ b/erpnext/stock/stock_ledger.py @@ -635,7 +635,7 @@ def process_sle(self, sle): def reset_actual_qty_for_stock_reco(self, sle): doc = frappe.get_cached_doc("Stock Reconciliation", sle.voucher_no) - doc.recalculate_current_qty(sle.voucher_detail_no, sle.creation, sle.actual_qty > 0) + doc.recalculate_current_qty(sle.voucher_detail_no, sle.creation, sle.actual_qty >= 0) if sle.actual_qty < 0: sle.actual_qty = ( @@ -643,9 +643,6 @@ def reset_actual_qty_for_stock_reco(self, sle): * -1 ) - if abs(sle.actual_qty) == 0.0: - sle.is_cancelled = 1 - def validate_negative_stock(self, sle): """ validate negative stock for entries current datetime onwards From 96663d7e28996fa130f5d38bd52981f17820d669 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Fri, 11 Aug 2023 09:19:14 +0000 Subject: [PATCH 099/501] chore: set default filter dates if missing (backport #36597) (#36598) chore: set default filter dates if missing (#36597) (cherry picked from commit 98e82e0d99f4fed6c1a35268ab4dd7324bf1b6c5) Co-authored-by: Anand Baburajan --- .../fixed_asset_register/fixed_asset_register.py | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/erpnext/assets/report/fixed_asset_register/fixed_asset_register.py b/erpnext/assets/report/fixed_asset_register/fixed_asset_register.py index 94c77ea517cd..bf62a8fb39c7 100644 --- a/erpnext/assets/report/fixed_asset_register/fixed_asset_register.py +++ b/erpnext/assets/report/fixed_asset_register/fixed_asset_register.py @@ -7,13 +7,14 @@ import frappe from frappe import _ from frappe.query_builder.functions import IfNull, Sum -from frappe.utils import cstr, flt, formatdate, getdate +from frappe.utils import add_months, cstr, flt, formatdate, getdate, nowdate, today from erpnext.accounts.report.financial_statements import ( get_fiscal_year_data, get_period_list, validate_fiscal_year, ) +from erpnext.accounts.utils import get_fiscal_year from erpnext.assets.doctype.asset.asset import get_asset_value_after_depreciation @@ -37,15 +38,26 @@ def get_conditions(filters): if filters.get("company"): conditions["company"] = filters.company + if filters.filter_based_on == "Date Range": + if not filters.from_date and not filters.to_date: + filters.from_date = add_months(nowdate(), -12) + filters.to_date = nowdate() + conditions[date_field] = ["between", [filters.from_date, filters.to_date]] - if filters.filter_based_on == "Fiscal Year": + elif filters.filter_based_on == "Fiscal Year": + if not filters.from_fiscal_year and not filters.to_fiscal_year: + default_fiscal_year = get_fiscal_year(today())[0] + filters.from_fiscal_year = default_fiscal_year + filters.to_fiscal_year = default_fiscal_year + fiscal_year = get_fiscal_year_data(filters.from_fiscal_year, filters.to_fiscal_year) validate_fiscal_year(fiscal_year, filters.from_fiscal_year, filters.to_fiscal_year) filters.year_start_date = getdate(fiscal_year.year_start_date) filters.year_end_date = getdate(fiscal_year.year_end_date) conditions[date_field] = ["between", [filters.year_start_date, filters.year_end_date]] + if filters.get("only_existing_assets"): conditions["is_existing_asset"] = filters.get("only_existing_assets") if filters.get("asset_category"): From 63c061e5e823cb05c3fb2264c032bec7e90955a4 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Fri, 11 Aug 2023 12:55:25 +0000 Subject: [PATCH 100/501] fix: wrap none type rate under flt (backport #36602) (#36604) fix: wrap none type rate under flt (#36602) (cherry picked from commit 627986efa1a29ccf419f0e845cdca4f49589bac3) Co-authored-by: Anand Baburajan --- erpnext/controllers/status_updater.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/controllers/status_updater.py b/erpnext/controllers/status_updater.py index a4bc4a9c69e4..f3663cc52711 100644 --- a/erpnext/controllers/status_updater.py +++ b/erpnext/controllers/status_updater.py @@ -233,7 +233,7 @@ def validate_qty(self): if hasattr(d, "qty") and d.qty > 0 and self.get("is_return"): frappe.throw(_("For an item {0}, quantity must be negative number").format(d.item_code)) - if hasattr(d, "item_code") and hasattr(d, "rate") and d.rate < 0: + if hasattr(d, "item_code") and hasattr(d, "rate") and flt(d.rate) < 0: frappe.throw(_("For an item {0}, rate must be a positive number").format(d.item_code)) if d.doctype == args["source_dt"] and d.get(args["join_field"]): From e4e1f03bacc1b0093063a57e554cb8e94e0cba27 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Fri, 11 Aug 2023 18:56:29 +0530 Subject: [PATCH 101/501] fix: wrap none type rate under flt (backport #36602) (backport #36604) (#36605) fix: wrap none type rate under flt (backport #36602) (#36604) fix: wrap none type rate under flt (#36602) (cherry picked from commit 627986efa1a29ccf419f0e845cdca4f49589bac3) Co-authored-by: Anand Baburajan (cherry picked from commit 63c061e5e823cb05c3fb2264c032bec7e90955a4) Co-authored-by: mergify[bot] <37929162+mergify[bot]@users.noreply.github.com> --- erpnext/controllers/status_updater.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/controllers/status_updater.py b/erpnext/controllers/status_updater.py index a4bc4a9c69e4..f3663cc52711 100644 --- a/erpnext/controllers/status_updater.py +++ b/erpnext/controllers/status_updater.py @@ -233,7 +233,7 @@ def validate_qty(self): if hasattr(d, "qty") and d.qty > 0 and self.get("is_return"): frappe.throw(_("For an item {0}, quantity must be negative number").format(d.item_code)) - if hasattr(d, "item_code") and hasattr(d, "rate") and d.rate < 0: + if hasattr(d, "item_code") and hasattr(d, "rate") and flt(d.rate) < 0: frappe.throw(_("For an item {0}, rate must be a positive number").format(d.item_code)) if d.doctype == args["source_dt"] and d.get(args["join_field"]): From ddcf555486e550b831e9d2610b7d6ecfaed36492 Mon Sep 17 00:00:00 2001 From: Frappe PR Bot Date: Fri, 11 Aug 2023 13:28:22 +0000 Subject: [PATCH 102/501] chore(release): Bumped to Version 14.34.3 ## [14.34.3](https://github.com/frappe/erpnext/compare/v14.34.2...v14.34.3) (2023-08-11) ### Bug Fixes * wrap none type rate under flt (backport [#36602](https://github.com/frappe/erpnext/issues/36602)) (backport [#36604](https://github.com/frappe/erpnext/issues/36604)) ([#36605](https://github.com/frappe/erpnext/issues/36605)) ([e4e1f03](https://github.com/frappe/erpnext/commit/e4e1f03bacc1b0093063a57e554cb8e94e0cba27)) --- erpnext/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/__init__.py b/erpnext/__init__.py index 2d9400749ffe..24573a20c125 100644 --- a/erpnext/__init__.py +++ b/erpnext/__init__.py @@ -3,7 +3,7 @@ import frappe -__version__ = "14.34.2" +__version__ = "14.34.3" def get_default_company(user=None): From 291264815196fdbec70fcd667db760a9017bb40e Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Thu, 10 Aug 2023 16:09:38 +0530 Subject: [PATCH 103/501] fix: Group Account total not showing in Financial Statements (cherry picked from commit baf5cddd1b189b4b88712f19499f0323fba7c115) --- erpnext/accounts/report/financial_statements.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/erpnext/accounts/report/financial_statements.py b/erpnext/accounts/report/financial_statements.py index a76dea6a523e..6b2341cef13d 100644 --- a/erpnext/accounts/report/financial_statements.py +++ b/erpnext/accounts/report/financial_statements.py @@ -335,12 +335,10 @@ def add_total_row(out, root_type, balance_must_be, period_list, company_currency for period in period_list: total_row.setdefault(period.key, 0.0) total_row[period.key] += row.get(period.key, 0.0) - row[period.key] = row.get(period.key, 0.0) total_row.setdefault("total", 0.0) total_row["total"] += flt(row["total"]) total_row["opening_balance"] += row["opening_balance"] - row["total"] = "" if "total" in total_row: out.append(total_row) From 4ca1f3b9cf69b00a80499b5d7d59a36a00521bf6 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Wed, 9 Aug 2023 15:36:28 +0530 Subject: [PATCH 104/501] fix: Make default sales update frequency as monthly instead of each transaction (cherry picked from commit 32863b492242dc0a761eb70b3e21663e78777aa6) # Conflicts: # erpnext/selling/doctype/selling_settings/selling_settings.json --- .../selling/doctype/selling_settings/selling_settings.json | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/erpnext/selling/doctype/selling_settings/selling_settings.json b/erpnext/selling/doctype/selling_settings/selling_settings.json index 45ad7d95a155..1152d41e87b5 100644 --- a/erpnext/selling/doctype/selling_settings/selling_settings.json +++ b/erpnext/selling/doctype/selling_settings/selling_settings.json @@ -84,7 +84,7 @@ "fieldname": "sales_update_frequency", "fieldtype": "Select", "label": "Sales Update Frequency in Company and Project", - "options": "Each Transaction\nDaily\nMonthly", + "options": "Monthly\nEach Transaction\nDaily", "reqd": 1 }, { @@ -193,7 +193,11 @@ "index_web_pages_for_search": 1, "issingle": 1, "links": [], +<<<<<<< HEAD "modified": "2023-03-03 11:16:54.333615", +======= + "modified": "2023-08-09 15:35:42.914354", +>>>>>>> 32863b4922 (fix: Make default sales update frequency as monthly instead of each transaction) "modified_by": "Administrator", "module": "Selling", "name": "Selling Settings", From 1377cf4cf1bfad982f91c0b65d3ffaf28ed24dd1 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Sat, 12 Aug 2023 20:01:04 +0530 Subject: [PATCH 105/501] fix: Allow backdated repayment cancels for term loans --- .../loan_management/doctype/loan_repayment/loan_repayment.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/erpnext/loan_management/doctype/loan_repayment/loan_repayment.py b/erpnext/loan_management/doctype/loan_repayment/loan_repayment.py index d7e11aafa81c..48086dde93f4 100644 --- a/erpnext/loan_management/doctype/loan_repayment/loan_repayment.py +++ b/erpnext/loan_management/doctype/loan_repayment/loan_repayment.py @@ -246,6 +246,9 @@ def mark_as_unpaid(self): ) def check_future_accruals(self): + if self.is_term_loan: + return + future_accrual_date = frappe.db.get_value( "Loan Interest Accrual", {"posting_date": (">", self.posting_date), "docstatus": 1, "loan": self.against_loan}, From 8b13185c25786afd05df0aa9dc56b2494450bc74 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Sat, 12 Aug 2023 23:25:05 +0530 Subject: [PATCH 106/501] fix: fetch `Stock UOM` from Item if not set (backport #36606) (#36617) fix: fetch `Stock UOM` from Item if not set (#36606) (cherry picked from commit 539cfd08f0b82337a8ef6e3caa3f83b227ecea77) Co-authored-by: s-aga-r --- erpnext/manufacturing/doctype/work_order/work_order.json | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/erpnext/manufacturing/doctype/work_order/work_order.json b/erpnext/manufacturing/doctype/work_order/work_order.json index 38e72533ba0c..fb44dfdffbb6 100644 --- a/erpnext/manufacturing/doctype/work_order/work_order.json +++ b/erpnext/manufacturing/doctype/work_order/work_order.json @@ -405,6 +405,8 @@ "read_only": 1 }, { + "fetch_from": "production_item.stock_uom", + "fetch_if_empty": 1, "fieldname": "stock_uom", "fieldtype": "Link", "label": "Stock UOM", @@ -598,7 +600,7 @@ "image_field": "image", "is_submittable": 1, "links": [], - "modified": "2023-06-09 13:20:09.154362", + "modified": "2023-08-11 18:35:49.852069", "modified_by": "Administrator", "module": "Manufacturing", "name": "Work Order", @@ -618,7 +620,6 @@ "read": 1, "report": 1, "role": "Manufacturing User", - "set_user_permissions": 1, "share": 1, "submit": 1, "write": 1 From cbcdf308405f8e1612a0de6d2f2f8a42c2de6de0 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Fri, 11 Aug 2023 10:55:43 +0530 Subject: [PATCH 107/501] chore: update permissions for Process Payment Reconciliation (cherry picked from commit cd28d15292a77060c2ec9b582e4b0f5635f7b112) --- .../process_payment_reconciliation.json | 22 ++++++++++++++----- 1 file changed, 16 insertions(+), 6 deletions(-) diff --git a/erpnext/accounts/doctype/process_payment_reconciliation/process_payment_reconciliation.json b/erpnext/accounts/doctype/process_payment_reconciliation/process_payment_reconciliation.json index 8bb7092dc502..1a1ab4d800e8 100644 --- a/erpnext/accounts/doctype/process_payment_reconciliation/process_payment_reconciliation.json +++ b/erpnext/accounts/doctype/process_payment_reconciliation/process_payment_reconciliation.json @@ -146,7 +146,7 @@ "index_web_pages_for_search": 1, "is_submittable": 1, "links": [], - "modified": "2023-04-21 17:19:30.912953", + "modified": "2023-08-11 10:56:51.699137", "modified_by": "Administrator", "module": "Accounts", "name": "Process Payment Reconciliation", @@ -154,15 +154,25 @@ "owner": "Administrator", "permissions": [ { + "amend": 1, + "cancel": 1, "create": 1, "delete": 1, - "email": 1, - "export": 1, - "print": 1, "read": 1, - "report": 1, - "role": "System Manager", + "role": "Accounts Manager", "share": 1, + "submit": 1, + "write": 1 + }, + { + "amend": 1, + "cancel": 1, + "create": 1, + "delete": 1, + "read": 1, + "role": "Accounts User", + "share": 1, + "submit": 1, "write": 1 } ], From b901cfdbe25e53b80f53391560a58e6acfdc104c Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Fri, 11 Aug 2023 09:50:43 +0530 Subject: [PATCH 108/501] fix: disallow mulitple SO with same PO No (cherry picked from commit dbd3fdbb415e032d738c0055288c41a12e4cd9b4) --- .../doctype/sales_order/sales_order.py | 28 ++++++++++++------- 1 file changed, 18 insertions(+), 10 deletions(-) diff --git a/erpnext/selling/doctype/sales_order/sales_order.py b/erpnext/selling/doctype/sales_order/sales_order.py index da838d1b7952..485ac60e7440 100755 --- a/erpnext/selling/doctype/sales_order/sales_order.py +++ b/erpnext/selling/doctype/sales_order/sales_order.py @@ -95,18 +95,26 @@ def validate_po(self): and customer = %s", (self.po_no, self.name, self.customer), ) - if ( - so - and so[0][0] - and not cint( + if so and so[0][0]: + if cint( frappe.db.get_single_value("Selling Settings", "allow_against_multiple_purchase_orders") - ) - ): - frappe.msgprint( - _("Warning: Sales Order {0} already exists against Customer's Purchase Order {1}").format( - so[0][0], self.po_no + ): + frappe.msgprint( + _("Warning: Sales Order {0} already exists against Customer's Purchase Order {1}").format( + frappe.bold(so[0][0]), frappe.bold(self.po_no) + ) + ) + else: + frappe.throw( + _( + "Sales Order {0} already exists against Customer's Purchase Order {1}. To allow multiple Sales Orders, Enable {2} in {3}" + ).format( + frappe.bold(so[0][0]), + frappe.bold(self.po_no), + frappe.bold(_("'Allow Multiple Sales Orders Against a Customer's Purchase Order'")), + get_link_to_form("Selling Settings", "Selling Settings"), + ) ) - ) def validate_for_items(self): for d in self.get("items"): From c83f10f63823f1b0fc33e04d5d014ba565a2e0e5 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Fri, 11 Aug 2023 10:45:42 +0530 Subject: [PATCH 109/501] refactor(test): don't set po_no by default (cherry picked from commit 64614cd9152c54fe8a0b59416a0621f9992d41d4) # Conflicts: # erpnext/stock/doctype/delivery_note/test_delivery_note.py --- .../doctype/sales_order/test_sales_order.py | 2 +- .../delivery_note/test_delivery_note.py | 59 +++++++++++++++++-- 2 files changed, 56 insertions(+), 5 deletions(-) diff --git a/erpnext/selling/doctype/sales_order/test_sales_order.py b/erpnext/selling/doctype/sales_order/test_sales_order.py index ced1ac62729a..608e23a82680 100644 --- a/erpnext/selling/doctype/sales_order/test_sales_order.py +++ b/erpnext/selling/doctype/sales_order/test_sales_order.py @@ -2023,7 +2023,7 @@ def make_sales_order(**args): so.company = args.company or "_Test Company" so.customer = args.customer or "_Test Customer" so.currency = args.currency or "INR" - so.po_no = args.po_no or "12345" + so.po_no = args.po_no or "" if args.selling_price_list: so.selling_price_list = args.selling_price_list diff --git a/erpnext/stock/doctype/delivery_note/test_delivery_note.py b/erpnext/stock/doctype/delivery_note/test_delivery_note.py index 5d8efd5d9dc7..4b41692d90cc 100644 --- a/erpnext/stock/doctype/delivery_note/test_delivery_note.py +++ b/erpnext/stock/doctype/delivery_note/test_delivery_note.py @@ -728,7 +728,7 @@ def test_closed_delivery_note(self): def test_dn_billing_status_case1(self): # SO -> DN -> SI - so = make_sales_order() + so = make_sales_order(po_no="12345") dn = create_dn_against_so(so.name, delivered_qty=2) self.assertEqual(dn.status, "To Bill") @@ -755,7 +755,7 @@ def test_dn_billing_status_case2(self): make_sales_invoice, ) - so = make_sales_order() + so = make_sales_order(po_no="12345") si = make_sales_invoice(so.name) si.get("items")[0].qty = 5 @@ -799,7 +799,7 @@ def test_dn_billing_status_case3(self): frappe.db.set_value("Stock Settings", None, "allow_negative_stock", 1) - so = make_sales_order() + so = make_sales_order(po_no="12345") dn1 = make_delivery_note(so.name) dn1.get("items")[0].qty = 2 @@ -845,7 +845,7 @@ def test_dn_billing_status_case4(self): from erpnext.accounts.doctype.sales_invoice.sales_invoice import make_delivery_note from erpnext.selling.doctype.sales_order.sales_order import make_sales_invoice - so = make_sales_order() + so = make_sales_order(po_no="12345") si = make_sales_invoice(so.name) si.submit() @@ -1211,6 +1211,57 @@ def test_batch_expiry_for_delivery_note(self): self.assertTrue(return_dn.docstatus == 1) +<<<<<<< HEAD +======= + def test_reserve_qty_on_sales_return(self): + frappe.db.set_single_value("Selling Settings", "dont_reserve_sales_order_qty_on_sales_return", 0) + self.reserved_qty_check() + + def test_dont_reserve_qty_on_sales_return(self): + frappe.db.set_single_value("Selling Settings", "dont_reserve_sales_order_qty_on_sales_return", 1) + self.reserved_qty_check() + + def reserved_qty_check(self): + from erpnext.controllers.sales_and_purchase_return import make_return_doc + from erpnext.selling.doctype.sales_order.sales_order import make_delivery_note + from erpnext.stock.stock_balance import get_reserved_qty + + dont_reserve_qty = frappe.db.get_single_value( + "Selling Settings", "dont_reserve_sales_order_qty_on_sales_return" + ) + + item = make_item().name + warehouse = "_Test Warehouse - _TC" + qty_to_reserve = 5 + + so = make_sales_order(item_code=item, qty=qty_to_reserve) + + # Make qty avl for test. + make_stock_entry(item_code=item, to_warehouse=warehouse, qty=10, basic_rate=100) + + # Test that item qty has been reserved on submit of sales order. + self.assertEqual(get_reserved_qty(item, warehouse), qty_to_reserve) + + dn = make_delivery_note(so.name) + dn.save().submit() + + # Test that item qty is no longer reserved since qty has been delivered. + self.assertEqual(get_reserved_qty(item, warehouse), 0) + + dn_return = make_return_doc("Delivery Note", dn.name) + dn_return.save().submit() + + returned = frappe.get_doc("Delivery Note", dn_return.name) + returned.update_prevdoc_status() + + # Test that item qty is not reserved on sales return, if selling setting don't reserve qty is checked. + self.assertEqual(get_reserved_qty(item, warehouse), 0 if dont_reserve_qty else qty_to_reserve) + + def tearDown(self): + frappe.db.rollback() + frappe.db.set_single_value("Selling Settings", "dont_reserve_sales_order_qty_on_sales_return", 0) + +>>>>>>> 64614cd915 (refactor(test): don't set po_no by default) def create_delivery_note(**args): dn = frappe.new_doc("Delivery Note") From 21d3fb06250c587b7a2708a45243ea9ba678b65b Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Sun, 13 Aug 2023 08:24:01 +0530 Subject: [PATCH 110/501] chore: resolve merge conflict --- .../delivery_note/test_delivery_note.py | 47 ------------------- 1 file changed, 47 deletions(-) diff --git a/erpnext/stock/doctype/delivery_note/test_delivery_note.py b/erpnext/stock/doctype/delivery_note/test_delivery_note.py index 4b41692d90cc..2565d1b76d19 100644 --- a/erpnext/stock/doctype/delivery_note/test_delivery_note.py +++ b/erpnext/stock/doctype/delivery_note/test_delivery_note.py @@ -1211,57 +1211,10 @@ def test_batch_expiry_for_delivery_note(self): self.assertTrue(return_dn.docstatus == 1) -<<<<<<< HEAD -======= - def test_reserve_qty_on_sales_return(self): - frappe.db.set_single_value("Selling Settings", "dont_reserve_sales_order_qty_on_sales_return", 0) - self.reserved_qty_check() - - def test_dont_reserve_qty_on_sales_return(self): - frappe.db.set_single_value("Selling Settings", "dont_reserve_sales_order_qty_on_sales_return", 1) - self.reserved_qty_check() - - def reserved_qty_check(self): - from erpnext.controllers.sales_and_purchase_return import make_return_doc - from erpnext.selling.doctype.sales_order.sales_order import make_delivery_note - from erpnext.stock.stock_balance import get_reserved_qty - - dont_reserve_qty = frappe.db.get_single_value( - "Selling Settings", "dont_reserve_sales_order_qty_on_sales_return" - ) - - item = make_item().name - warehouse = "_Test Warehouse - _TC" - qty_to_reserve = 5 - - so = make_sales_order(item_code=item, qty=qty_to_reserve) - - # Make qty avl for test. - make_stock_entry(item_code=item, to_warehouse=warehouse, qty=10, basic_rate=100) - - # Test that item qty has been reserved on submit of sales order. - self.assertEqual(get_reserved_qty(item, warehouse), qty_to_reserve) - - dn = make_delivery_note(so.name) - dn.save().submit() - - # Test that item qty is no longer reserved since qty has been delivered. - self.assertEqual(get_reserved_qty(item, warehouse), 0) - - dn_return = make_return_doc("Delivery Note", dn.name) - dn_return.save().submit() - - returned = frappe.get_doc("Delivery Note", dn_return.name) - returned.update_prevdoc_status() - - # Test that item qty is not reserved on sales return, if selling setting don't reserve qty is checked. - self.assertEqual(get_reserved_qty(item, warehouse), 0 if dont_reserve_qty else qty_to_reserve) - def tearDown(self): frappe.db.rollback() frappe.db.set_single_value("Selling Settings", "dont_reserve_sales_order_qty_on_sales_return", 0) ->>>>>>> 64614cd915 (refactor(test): don't set po_no by default) def create_delivery_note(**args): dn = frappe.new_doc("Delivery Note") From ce08f038d276782e58d82155b8122dd5423998dc Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Tue, 25 Jul 2023 14:38:17 +0530 Subject: [PATCH 111/501] fix: validation blocks partial payment for SO and PO (cherry picked from commit cb2bfabb6fb2e9c0656334af9df8104148b291fb) --- erpnext/accounts/doctype/payment_entry/payment_entry.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/accounts/doctype/payment_entry/payment_entry.py b/erpnext/accounts/doctype/payment_entry/payment_entry.py index 580608d5a372..d3f0043fe6a4 100644 --- a/erpnext/accounts/doctype/payment_entry/payment_entry.py +++ b/erpnext/accounts/doctype/payment_entry/payment_entry.py @@ -61,7 +61,7 @@ def setup_party_account_field(self): def validate(self): self.setup_party_account_field() self.set_missing_values() - self.set_missing_ref_details() + self.set_missing_ref_details(force=True) self.validate_payment_type() self.validate_party_details() self.set_exchange_rate() From a3032910a74a6c13a539d5baca83a5a38e93d8f6 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Sun, 13 Aug 2023 11:37:40 +0530 Subject: [PATCH 112/501] chore: resolve conflicts --- .../selling/doctype/selling_settings/selling_settings.json | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/erpnext/selling/doctype/selling_settings/selling_settings.json b/erpnext/selling/doctype/selling_settings/selling_settings.json index 1152d41e87b5..af148c51fb97 100644 --- a/erpnext/selling/doctype/selling_settings/selling_settings.json +++ b/erpnext/selling/doctype/selling_settings/selling_settings.json @@ -193,11 +193,7 @@ "index_web_pages_for_search": 1, "issingle": 1, "links": [], -<<<<<<< HEAD - "modified": "2023-03-03 11:16:54.333615", -======= "modified": "2023-08-09 15:35:42.914354", ->>>>>>> 32863b4922 (fix: Make default sales update frequency as monthly instead of each transaction) "modified_by": "Administrator", "module": "Selling", "name": "Selling Settings", @@ -226,4 +222,4 @@ "sort_order": "DESC", "states": [], "track_changes": 1 -} \ No newline at end of file +} From 3044f46c52a3ea7cf5bcbfb4abc88bc400aea4a4 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Sun, 13 Aug 2023 17:11:47 +0530 Subject: [PATCH 113/501] fix: wrong currency on financial-statement based reports (#36524) fix: wrong currency on financial-statement based reports (#36524) * add missing field options on financial_statement Total field * format financial statement code (cherry picked from commit ce25f9e8c9cf165d03b993f8c3e04caa83d5bb1d) Co-authored-by: Naufal Afif --- erpnext/accounts/report/financial_statements.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/erpnext/accounts/report/financial_statements.py b/erpnext/accounts/report/financial_statements.py index 6b2341cef13d..693725d8f504 100644 --- a/erpnext/accounts/report/financial_statements.py +++ b/erpnext/accounts/report/financial_statements.py @@ -637,7 +637,13 @@ def get_columns(periodicity, period_list, accumulated_values=1, company=None): if periodicity != "Yearly": if not accumulated_values: columns.append( - {"fieldname": "total", "label": _("Total"), "fieldtype": "Currency", "width": 150} + { + "fieldname": "total", + "label": _("Total"), + "fieldtype": "Currency", + "width": 150, + "options": "currency", + } ) return columns From ac0fff7e94598abac128c5ed7e2ed33de98b7be2 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Mon, 14 Aug 2023 09:46:41 +0530 Subject: [PATCH 114/501] fix: AR/AP report based on payment terms (#36574) fix: AR/AP report based on payment terms (#36574) * fix: AR/AP report based on payment terms * fix: AR/AP report based on payment terms (cherry picked from commit fbb5058531278c0fa70f1bd6795c4eba83c66b72) Co-authored-by: Deepesh Garg --- .../accounts_receivable/accounts_receivable.py | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/erpnext/accounts/report/accounts_receivable/accounts_receivable.py b/erpnext/accounts/report/accounts_receivable/accounts_receivable.py index 11bbb6f1e43a..f78a84086a9f 100755 --- a/erpnext/accounts/report/accounts_receivable/accounts_receivable.py +++ b/erpnext/accounts/report/accounts_receivable/accounts_receivable.py @@ -436,12 +436,11 @@ def set_party_details(self, row): def allocate_outstanding_based_on_payment_terms(self, row): self.get_payment_terms(row) for term in row.payment_terms: - - # update "paid" and "oustanding" for this term + # update "paid" and "outstanding" for this term if not term.paid: self.allocate_closing_to_term(row, term, "paid") - # update "credit_note" and "oustanding" for this term + # update "credit_note" and "outstanding" for this term if term.outstanding: self.allocate_closing_to_term(row, term, "credit_note") @@ -453,7 +452,8 @@ def get_payment_terms(self, row): """ select si.name, si.party_account_currency, si.currency, si.conversion_rate, - ps.due_date, ps.payment_term, ps.payment_amount, ps.description, ps.paid_amount, ps.discounted_amount + si.total_advance, ps.due_date, ps.payment_term, ps.payment_amount, ps.base_payment_amount, + ps.description, ps.paid_amount, ps.discounted_amount from `tab{0}` si, `tabPayment Schedule` ps where si.name = ps.parent and @@ -469,6 +469,10 @@ def get_payment_terms(self, row): original_row = frappe._dict(row) row.payment_terms = [] + # Advance allocated during invoicing is not considered in payment terms + # Deduct that from paid amount pre allocation + row.paid -= flt(payment_terms_details[0].total_advance) + # If no or single payment terms, no need to split the row if len(payment_terms_details) <= 1: return @@ -483,7 +487,7 @@ def append_payment_term(self, row, d, term): ) and d.currency == d.party_account_currency: invoiced = d.payment_amount else: - invoiced = flt(flt(d.payment_amount) * flt(d.conversion_rate), self.currency_precision) + invoiced = d.base_payment_amount row.payment_terms.append( term.update( From c06a6bfc099680ea8f1f69b7432927c4597a241f Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Thu, 27 Apr 2023 09:46:54 +0530 Subject: [PATCH 115/501] refactor: book exchange gain/loss through journal (cherry picked from commit 81cd7873d343d893a6e2a3b41107f17e03eedcd8) --- .../doctype/payment_entry/payment_entry.js | 2 +- .../doctype/payment_entry/payment_entry.py | 19 +- .../payment_reconciliation.py | 10 +- .../doctype/sales_invoice/sales_invoice.py | 3 +- erpnext/accounts/utils.py | 34 +++- erpnext/controllers/accounts_controller.py | 162 ++++++++++++------ 6 files changed, 167 insertions(+), 63 deletions(-) diff --git a/erpnext/accounts/doctype/payment_entry/payment_entry.js b/erpnext/accounts/doctype/payment_entry/payment_entry.js index 74a90fe2e8f6..3c2fb1dd0ed0 100644 --- a/erpnext/accounts/doctype/payment_entry/payment_entry.js +++ b/erpnext/accounts/doctype/payment_entry/payment_entry.js @@ -7,7 +7,7 @@ cur_frm.cscript.tax_table = "Advance Taxes and Charges"; frappe.ui.form.on('Payment Entry', { onload: function(frm) { - frm.ignore_doctypes_on_cancel_all = ['Sales Invoice', 'Purchase Invoice', "Repost Payment Ledger"]; + frm.ignore_doctypes_on_cancel_all = ['Sales Invoice', 'Purchase Invoice', "Journal Entry", "Repost Payment Ledger"]; if(frm.doc.__islocal) { if (!frm.doc.paid_from) frm.set_value("paid_from_account_currency", null); diff --git a/erpnext/accounts/doctype/payment_entry/payment_entry.py b/erpnext/accounts/doctype/payment_entry/payment_entry.py index d3f0043fe6a4..f0c36cef47f5 100644 --- a/erpnext/accounts/doctype/payment_entry/payment_entry.py +++ b/erpnext/accounts/doctype/payment_entry/payment_entry.py @@ -101,6 +101,7 @@ def on_cancel(self): "Repost Payment Ledger", "Repost Payment Ledger Items", ) + super(PaymentEntry, self).on_cancel() self.make_gl_entries(cancel=1) self.update_outstanding_amounts() self.update_advance_paid() @@ -783,10 +784,25 @@ def calculate_base_allocated_amount_for_reference(self, d) -> float: flt(d.allocated_amount) * flt(exchange_rate), self.precision("base_paid_amount") ) else: + + # Use source/target exchange rate, so no difference amount is calculated. + # then update exchange gain/loss amount in refernece table + # if there is an amount, submit a JE for that + + exchange_rate = 1 + if self.payment_type == "Receive": + exchange_rate = self.source_exchange_rate + elif self.payment_type == "Pay": + exchange_rate = self.target_exchange_rate + base_allocated_amount += flt( - flt(d.allocated_amount) * flt(d.exchange_rate), self.precision("base_paid_amount") + flt(d.allocated_amount) * flt(exchange_rate), self.precision("base_paid_amount") ) + allocated_amount_in_pe_exchange_rate = flt( + flt(d.allocated_amount) * flt(d.exchange_rate), self.precision("base_paid_amount") + ) + d.exchange_gain_loss = base_allocated_amount - allocated_amount_in_pe_exchange_rate return base_allocated_amount def set_total_allocated_amount(self): @@ -977,6 +993,7 @@ def make_gl_entries(self, cancel=0, adv_adj=0): gl_entries = self.build_gl_map() gl_entries = process_gl_map(gl_entries) make_gl_entries(gl_entries, cancel=cancel, adv_adj=adv_adj) + self.make_exchange_gain_loss_journal() def add_party_gl_entries(self, gl_entries): if self.party_account: diff --git a/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py b/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py index 05623c09932b..ab03699785a2 100644 --- a/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py +++ b/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py @@ -347,11 +347,11 @@ def reconcile_allocations(self, skip_ref_details_update_for_pe=False): payment_details = self.get_payment_details(row, dr_or_cr) reconciled_entry.append(payment_details) - if payment_details.difference_amount and row.reference_type not in [ - "Sales Invoice", - "Purchase Invoice", - ]: - self.make_difference_entry(payment_details) + # if payment_details.difference_amount and row.reference_type not in [ + # "Sales Invoice", + # "Purchase Invoice", + # ]: + # self.make_difference_entry(payment_details) if entry_list: reconcile_against_document(entry_list, skip_ref_details_update_for_pe) diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py index c2804d19a345..9e9cd61e2032 100644 --- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py +++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py @@ -1046,6 +1046,8 @@ def make_gl_entries(self, gl_entries=None, from_repost=False): merge_entries=False, from_repost=from_repost, ) + + self.make_exchange_gain_loss_journal() elif self.docstatus == 2: make_reverse_gl_entries(voucher_type=self.doctype, voucher_no=self.name) @@ -1071,7 +1073,6 @@ def get_gl_entries(self, warehouse_account=None): self.make_customer_gl_entry(gl_entries) self.make_tax_gl_entries(gl_entries) - self.make_exchange_gain_loss_gl_entries(gl_entries) self.make_internal_transfer_gl_entries(gl_entries) self.make_item_gl_entries(gl_entries) diff --git a/erpnext/accounts/utils.py b/erpnext/accounts/utils.py index 5662e99c5e70..adb789e7493a 100644 --- a/erpnext/accounts/utils.py +++ b/erpnext/accounts/utils.py @@ -612,9 +612,7 @@ def update_reference_in_payment_entry( "total_amount": d.grand_total, "outstanding_amount": d.outstanding_amount, "allocated_amount": d.allocated_amount, - "exchange_rate": d.exchange_rate - if not d.exchange_gain_loss - else payment_entry.get_exchange_rate(), + "exchange_rate": d.exchange_rate if d.exchange_gain_loss else payment_entry.get_exchange_rate(), "exchange_gain_loss": d.exchange_gain_loss, # only populated from invoice in case of advance allocation } @@ -657,11 +655,41 @@ def update_reference_in_payment_entry( if not skip_ref_details_update_for_pe: payment_entry.set_missing_ref_details() payment_entry.set_amounts() + payment_entry.make_exchange_gain_loss_journal() if not do_not_save: payment_entry.save(ignore_permissions=True) +def cancel_exchange_gain_loss_journal(parent_doc: dict | object) -> None: + """ + Cancel Exchange Gain/Loss for Sales/Purchase Invoice, if they have any. + """ + if parent_doc.doctype in ["Sales Invoice", "Purchase Invoice", "Payment Entry"]: + journals = frappe.db.get_all( + "Journal Entry Account", + filters={ + "reference_type": parent_doc.doctype, + "reference_name": parent_doc.name, + "docstatus": 1, + }, + fields=["parent"], + as_list=1, + ) + if journals: + exchange_journals = frappe.db.get_all( + "Journal Entry", + filters={ + "name": ["in", [x[0] for x in journals]], + "voucher_type": "Exchange Gain Or Loss", + "docstatus": 1, + }, + as_list=1, + ) + for doc in exchange_journals: + frappe.get_doc("Journal Entry", doc[0]).cancel() + + def unlink_ref_doc_from_payment_entries(ref_doc): remove_ref_doc_link_from_jv(ref_doc.doctype, ref_doc.name) remove_ref_doc_link_from_pe(ref_doc.doctype, ref_doc.name) diff --git a/erpnext/controllers/accounts_controller.py b/erpnext/controllers/accounts_controller.py index 9912dd47f8b6..dcaaff08ade3 100644 --- a/erpnext/controllers/accounts_controller.py +++ b/erpnext/controllers/accounts_controller.py @@ -5,7 +5,7 @@ import json import frappe -from frappe import _, bold, throw +from frappe import _, bold, qb, throw from frappe.model.workflow import get_workflow_name, is_transition_condition_satisfied from frappe.query_builder.functions import Abs, Sum from frappe.utils import ( @@ -962,67 +962,119 @@ def set_advance_gain_or_loss(self): d.exchange_gain_loss = difference - def make_exchange_gain_loss_gl_entries(self, gl_entries): - if self.get("doctype") in ["Purchase Invoice", "Sales Invoice"]: - for d in self.get("advances"): - if d.exchange_gain_loss: - is_purchase_invoice = self.get("doctype") == "Purchase Invoice" - party = self.supplier if is_purchase_invoice else self.customer - party_account = self.credit_to if is_purchase_invoice else self.debit_to - party_type = "Supplier" if is_purchase_invoice else "Customer" - - gain_loss_account = frappe.get_cached_value( - "Company", self.company, "exchange_gain_loss_account" + def make_exchange_gain_loss_journal(self) -> None: + """ + Make Exchange Gain/Loss journal for Invoices and Payments + """ + # Cancelling is existing exchange gain/loss journals is handled in on_cancel event + if self.docstatus == 1: + if self.get("doctype") == "Payment Entry": + gain_loss_to_book = [x for x in self.references if x.exchange_gain_loss != 0] + booked = [] + if gain_loss_to_book: + vtypes = [x.reference_doctype for x in gain_loss_to_book] + vnames = [x.reference_name for x in gain_loss_to_book] + je = qb.DocType("Journal Entry") + jea = qb.DocType("Journal Entry Account") + parents = ( + qb.from_(jea) + .select(jea.parent) + .where( + (jea.reference_type == "Payment Entry") + & (jea.reference_name == self.name) + & (jea.docstatus == 1) + ) + .run() ) - if not gain_loss_account: - frappe.throw( - _("Please set default Exchange Gain/Loss Account in Company {}").format(self.get("company")) + + booked = [] + if parents: + booked = ( + qb.from_(je) + .inner_join(jea) + .on(je.name == jea.parent) + .select(jea.reference_type, jea.reference_name, jea.reference_detail_no) + .where( + (je.docstatus == 1) + & (je.name.isin(parents)) + & (je.voucher_type == "Exchange Gain or Loss") + ) + .run() ) - account_currency = get_account_currency(gain_loss_account) - if account_currency != self.company_currency: - frappe.throw( - _("Currency for {0} must be {1}").format(gain_loss_account, self.company_currency) + + for d in gain_loss_to_book: + if d.exchange_gain_loss and ( + (d.reference_doctype, d.reference_name, str(d.idx)) not in booked + ): + journal_entry = frappe.new_doc("Journal Entry") + journal_entry.voucher_type = "Exchange Gain Or Loss" + journal_entry.company = self.company + journal_entry.posting_date = nowdate() + journal_entry.multi_currency = 1 + + if self.payment_type == "Receive": + party_account = self.paid_from + elif self.payment_type == "Pay": + party_account = self.paid_to + + party_account_currency = frappe.get_cached_value( + "Account", party_account, "account_currency" ) + dr_or_cr = "debit" if d.exchange_gain_loss > 0 else "credit" + reverse_dr_or_cr = "debit" if dr_or_cr == "credit" else "credit" - # for purchase - dr_or_cr = "debit" if d.exchange_gain_loss > 0 else "credit" - if not is_purchase_invoice: - # just reverse for sales? - dr_or_cr = "debit" if dr_or_cr == "credit" else "credit" + gain_loss_account = frappe.get_cached_value( + "Company", self.company, "exchange_gain_loss_account" + ) + if not gain_loss_account: + frappe.throw( + _("Please set default Exchange Gain/Loss Account in Company {}").format( + self.get("company") + ) + ) + gain_loss_account_currency = get_account_currency(gain_loss_account) + if gain_loss_account_currency != self.company_currency: + frappe.throw( + _("Currency for {0} must be {1}").format(gain_loss_account, self.company_currency) + ) - gl_entries.append( - self.get_gl_dict( + journal_account = frappe._dict( { - "account": gain_loss_account, - "account_currency": account_currency, - "against": party, - dr_or_cr + "_in_account_currency": abs(d.exchange_gain_loss), + "account": party_account, + "party_type": self.party_type, + "party": self.party, + "account_currency": party_account_currency, + "exchange_rate": 0, + "cost_center": erpnext.get_default_cost_center(self.company), + "reference_type": d.reference_doctype, + "reference_name": d.reference_name, + "reference_detail_no": d.idx, dr_or_cr: abs(d.exchange_gain_loss), - "cost_center": self.cost_center or erpnext.get_default_cost_center(self.company), - "project": self.project, - }, - item=d, + dr_or_cr + "_in_account_currency": 0, + } ) - ) - dr_or_cr = "debit" if dr_or_cr == "credit" else "credit" + journal_entry.append("accounts", journal_account) - gl_entries.append( - self.get_gl_dict( + journal_account = frappe._dict( { - "account": party_account, - "party_type": party_type, - "party": party, - "against": gain_loss_account, - dr_or_cr + "_in_account_currency": flt(abs(d.exchange_gain_loss) / self.conversion_rate), - dr_or_cr: abs(d.exchange_gain_loss), - "cost_center": self.cost_center, - "project": self.project, - }, - self.party_account_currency, - item=self, + "account": gain_loss_account, + "account_currency": gain_loss_account_currency, + "exchange_rate": 1, + "cost_center": erpnext.get_default_cost_center(self.company), + "reference_type": self.doctype, + "reference_name": self.name, + "reference_detail_no": d.idx, + reverse_dr_or_cr + "_in_account_currency": abs(d.exchange_gain_loss), + reverse_dr_or_cr: abs(d.exchange_gain_loss), + } ) - ) + + journal_entry.append("accounts", journal_account) + + journal_entry.save() + journal_entry.submit() + # frappe.throw("stopping...") def make_precision_loss_gl_entry(self, gl_entries): round_off_account, round_off_cost_center = get_round_off_account_and_cost_center( @@ -1111,9 +1163,15 @@ def update_against_document_in_jv(self): reconcile_against_document(lst) def on_cancel(self): - from erpnext.accounts.utils import unlink_ref_doc_from_payment_entries + from erpnext.accounts.utils import ( + cancel_exchange_gain_loss_journal, + unlink_ref_doc_from_payment_entries, + ) + + if self.doctype in ["Sales Invoice", "Purchase Invoice", "Payment Entry"]: + # Cancel Exchange Gain/Loss Journal before unlinking + cancel_exchange_gain_loss_journal(self) - if self.doctype in ["Sales Invoice", "Purchase Invoice"]: if frappe.db.get_single_value("Accounts Settings", "unlink_payment_on_cancellation_of_invoice"): unlink_ref_doc_from_payment_entries(self) From 44110860b4c0ed5922283c28b32f8a56ff773e2e Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Thu, 15 Jun 2023 16:55:56 +0530 Subject: [PATCH 116/501] test: different scenarios for exchange booking (cherry picked from commit 5e1cd1f22701b7675422b05b3616253d9a3a28db) --- .../tests/test_accounts_controller.py | 501 ++++++++++++++++++ 1 file changed, 501 insertions(+) create mode 100644 erpnext/controllers/tests/test_accounts_controller.py diff --git a/erpnext/controllers/tests/test_accounts_controller.py b/erpnext/controllers/tests/test_accounts_controller.py new file mode 100644 index 000000000000..31aa857c8f55 --- /dev/null +++ b/erpnext/controllers/tests/test_accounts_controller.py @@ -0,0 +1,501 @@ +# Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and contributors +# For license information, please see license.txt + +import unittest + +import frappe +from frappe import qb +from frappe.tests.utils import FrappeTestCase, change_settings +from frappe.utils import add_days, flt, nowdate + +from erpnext import get_default_cost_center +from erpnext.accounts.doctype.payment_entry.payment_entry import get_payment_entry +from erpnext.accounts.doctype.payment_entry.test_payment_entry import create_payment_entry +from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice +from erpnext.accounts.party import get_party_account +from erpnext.stock.doctype.item.test_item import create_item + + +def make_customer(customer_name, currency=None): + if not frappe.db.exists("Customer", customer_name): + customer = frappe.new_doc("Customer") + customer.customer_name = customer_name + customer.type = "Individual" + + if currency: + customer.default_currency = currency + customer.save() + return customer.name + else: + return customer_name + + +class TestAccountsController(FrappeTestCase): + """ + Test Exchange Gain/Loss booking on various scenarios + """ + + def setUp(self): + self.create_company() + self.create_account() + self.create_item() + self.create_customer() + self.clear_old_entries() + + def tearDown(self): + frappe.db.rollback() + + def create_company(self): + company_name = "_Test Company MC" + self.company_abbr = abbr = "_CM" + if frappe.db.exists("Company", company_name): + company = frappe.get_doc("Company", company_name) + else: + company = frappe.get_doc( + { + "doctype": "Company", + "company_name": company_name, + "country": "India", + "default_currency": "INR", + "create_chart_of_accounts_based_on": "Standard Template", + "chart_of_accounts": "Standard", + } + ) + company = company.save() + + self.company = company.name + self.cost_center = company.cost_center + self.warehouse = "Stores - " + abbr + self.finished_warehouse = "Finished Goods - " + abbr + self.income_account = "Sales - " + abbr + self.expense_account = "Cost of Goods Sold - " + abbr + self.debit_to = "Debtors - " + abbr + self.debit_usd = "Debtors USD - " + abbr + self.cash = "Cash - " + abbr + self.creditors = "Creditors - " + abbr + + def create_item(self): + item = create_item( + item_code="_Test Notebook", is_stock_item=0, company=self.company, warehouse=self.warehouse + ) + self.item = item if isinstance(item, str) else item.item_code + + def create_customer(self): + self.customer = make_customer("_Test MC Customer USD", "USD") + + def create_account(self): + account_name = "Debtors USD" + if not frappe.db.get_value( + "Account", filters={"account_name": account_name, "company": self.company} + ): + acc = frappe.new_doc("Account") + acc.account_name = account_name + acc.parent_account = "Accounts Receivable - " + self.company_abbr + acc.company = self.company + acc.account_currency = "USD" + acc.account_type = "Receivable" + acc.insert() + else: + name = frappe.db.get_value( + "Account", + filters={"account_name": account_name, "company": self.company}, + fieldname="name", + pluck=True, + ) + acc = frappe.get_doc("Account", name) + self.debtors_usd = acc.name + + def create_sales_invoice( + self, qty=1, rate=1, posting_date=nowdate(), do_not_save=False, do_not_submit=False + ): + """ + Helper function to populate default values in sales invoice + """ + sinv = create_sales_invoice( + qty=qty, + rate=rate, + company=self.company, + customer=self.customer, + item_code=self.item, + item_name=self.item, + cost_center=self.cost_center, + warehouse=self.warehouse, + debit_to=self.debit_usd, + parent_cost_center=self.cost_center, + update_stock=0, + currency="USD", + conversion_rate=80, + is_pos=0, + is_return=0, + return_against=None, + income_account=self.income_account, + expense_account=self.expense_account, + do_not_save=do_not_save, + do_not_submit=do_not_submit, + ) + return sinv + + def create_payment_entry( + self, amount=1, source_exc_rate=75, posting_date=nowdate(), customer=None + ): + """ + Helper function to populate default values in payment entry + """ + payment = create_payment_entry( + company=self.company, + payment_type="Receive", + party_type="Customer", + party=customer or self.customer, + paid_from=self.debit_usd, + paid_to=self.cash, + paid_amount=amount, + ) + payment.source_exchange_rate = source_exc_rate + payment.received_amount = source_exc_rate * amount + payment.posting_date = posting_date + return payment + + def clear_old_entries(self): + doctype_list = [ + "GL Entry", + "Payment Ledger Entry", + "Sales Invoice", + "Purchase Invoice", + "Payment Entry", + "Journal Entry", + ] + for doctype in doctype_list: + qb.from_(qb.DocType(doctype)).delete().where(qb.DocType(doctype).company == self.company).run() + + def create_payment_reconciliation(self): + pr = frappe.new_doc("Payment Reconciliation") + pr.company = self.company + pr.party_type = "Customer" + pr.party = self.customer + pr.receivable_payable_account = get_party_account(pr.party_type, pr.party, pr.company) + pr.from_invoice_date = pr.to_invoice_date = pr.from_payment_date = pr.to_payment_date = nowdate() + return pr + + def create_journal_entry( + self, acc1=None, acc2=None, amount=0, posting_date=None, cost_center=None + ): + je = frappe.new_doc("Journal Entry") + je.posting_date = posting_date or nowdate() + je.company = self.company + je.user_remark = "test" + if not cost_center: + cost_center = self.cost_center + je.set( + "accounts", + [ + { + "account": acc1, + "cost_center": cost_center, + "debit_in_account_currency": amount if amount > 0 else 0, + "credit_in_account_currency": abs(amount) if amount < 0 else 0, + }, + { + "account": acc2, + "cost_center": cost_center, + "credit_in_account_currency": amount if amount > 0 else 0, + "debit_in_account_currency": abs(amount) if amount < 0 else 0, + }, + ], + ) + return je + + def get_journals_for(self, voucher_type: str, voucher_no: str) -> list: + journals = [] + if voucher_type and voucher_no: + journals = frappe.db.get_all( + "Journal Entry Account", + filters={"reference_type": voucher_type, "reference_name": voucher_no, "docstatus": 1}, + fields=["parent"], + ) + return journals + + def test_01_payment_against_invoice(self): + # Invoice in Foreign Currency + si = self.create_sales_invoice(qty=1, rate=1) + # Payment + pe = self.create_payment_entry(amount=1, source_exc_rate=75).save() + pe.append( + "references", + {"reference_doctype": si.doctype, "reference_name": si.name, "allocated_amount": 1}, + ) + pe = pe.save().submit() + + si.reload() + self.assertEqual(si.outstanding_amount, 0) + + # Exchange Gain/Loss Journal should've been created. + exc_je_for_si = self.get_journals_for(si.doctype, si.name) + exc_je_for_pe = self.get_journals_for(pe.doctype, pe.name) + + self.assertNotEqual(exc_je_for_si, []) + self.assertEqual(len(exc_je_for_si), 1) + self.assertEqual(len(exc_je_for_pe), 1) + self.assertEqual(exc_je_for_si[0], exc_je_for_pe[0]) + + # Cancel Payment + pe.cancel() + + si.reload() + self.assertEqual(si.outstanding_amount, 1) + + # Exchange Gain/Loss Journal should've been cancelled + exc_je_for_si = self.get_journals_for(si.doctype, si.name) + exc_je_for_pe = self.get_journals_for(pe.doctype, pe.name) + + self.assertEqual(exc_je_for_si, []) + self.assertEqual(exc_je_for_pe, []) + + def test_02_advance_against_invoice(self): + # Advance Payment + adv = self.create_payment_entry(amount=1, source_exc_rate=85).save().submit() + adv.reload() + + # Invoice in Foreign Currency + si = self.create_sales_invoice(qty=1, rate=1, do_not_submit=True) + si.append( + "advances", + { + "doctype": "Sales Invoice Advance", + "reference_type": adv.doctype, + "reference_name": adv.name, + "advance_amount": 1, + "allocated_amount": 1, + "ref_exchange_rate": 85, + "remarks": "Test", + }, + ) + si = si.save() + si = si.submit() + + adv.reload() + self.assertEqual(si.outstanding_amount, 0) + + # Exchange Gain/Loss Journal should've been created. + exc_je_for_si = self.get_journals_for(si.doctype, si.name) + exc_je_for_adv = self.get_journals_for(adv.doctype, adv.name) + + self.assertNotEqual(exc_je_for_si, []) + self.assertEqual(len(exc_je_for_si), 1) + self.assertEqual(len(exc_je_for_adv), 1) + self.assertEqual(exc_je_for_si, exc_je_for_adv) + + # Cancel Invoice + si.cancel() + + # Exchange Gain/Loss Journal should've been cancelled + exc_je_for_si = self.get_journals_for(si.doctype, si.name) + exc_je_for_adv = self.get_journals_for(adv.doctype, adv.name) + + self.assertEqual(exc_je_for_si, []) + self.assertEqual(exc_je_for_adv, []) + + def test_03_partial_advance_and_payment_for_invoice(self): + """ + Invoice with partial advance payment, and a normal payment + """ + # Partial Advance + adv = self.create_payment_entry(amount=1, source_exc_rate=85).save().submit() + adv.reload() + + # Invoice in Foreign Currency linked with advance + si = self.create_sales_invoice(qty=2, rate=1, do_not_submit=True) + si.append( + "advances", + { + "doctype": "Sales Invoice Advance", + "reference_type": adv.doctype, + "reference_name": adv.name, + "advance_amount": 1, + "allocated_amount": 1, + "ref_exchange_rate": 85, + "remarks": "Test", + }, + ) + si = si.save() + si = si.submit() + + si.reload() + self.assertEqual(si.outstanding_amount, 1) + + # Exchange Gain/Loss Journal should've been created for the partial advance + exc_je_for_si = self.get_journals_for(si.doctype, si.name) + exc_je_for_adv = self.get_journals_for(adv.doctype, adv.name) + + self.assertNotEqual(exc_je_for_si, []) + self.assertEqual(len(exc_je_for_si), 1) + self.assertEqual(len(exc_je_for_adv), 1) + self.assertEqual(exc_je_for_si, exc_je_for_adv) + + # Payment + pe = self.create_payment_entry(amount=1, source_exc_rate=75).save() + pe.append( + "references", + {"reference_doctype": si.doctype, "reference_name": si.name, "allocated_amount": 1}, + ) + pe = pe.save().submit() + + si.reload() + self.assertEqual(si.outstanding_amount, 0) + + # Exchange Gain/Loss Journal should've been created for the payment + exc_je_for_si = self.get_journals_for(si.doctype, si.name) + exc_je_for_pe = self.get_journals_for(pe.doctype, pe.name) + + self.assertNotEqual(exc_je_for_si, []) + # There should be 2 JE's now. One for the advance and one for the payment + self.assertEqual(len(exc_je_for_si), 2) + self.assertEqual(len(exc_je_for_pe), 1) + self.assertEqual(exc_je_for_si, exc_je_for_pe + exc_je_for_adv) + + # Cancel Invoice + si.reload() + si.cancel() + + # Exchange Gain/Loss Journal should been cancelled + exc_je_for_si = self.get_journals_for(si.doctype, si.name) + exc_je_for_pe = self.get_journals_for(pe.doctype, pe.name) + exc_je_for_adv = self.get_journals_for(adv.doctype, adv.name) + + self.assertEqual(exc_je_for_si, []) + self.assertEqual(exc_je_for_pe, []) + self.assertEqual(exc_je_for_adv, []) + + def test_04_partial_advance_and_payment_for_invoice_with_cancellation(self): + """ + Invoice with partial advance payment, and a normal payment. Cancel advance and payment. + """ + # Partial Advance + adv = self.create_payment_entry(amount=1, source_exc_rate=85).save().submit() + adv.reload() + + # Invoice in Foreign Currency linked with advance + si = self.create_sales_invoice(qty=2, rate=1, do_not_submit=True) + si.append( + "advances", + { + "doctype": "Sales Invoice Advance", + "reference_type": adv.doctype, + "reference_name": adv.name, + "advance_amount": 1, + "allocated_amount": 1, + "ref_exchange_rate": 85, + "remarks": "Test", + }, + ) + si = si.save() + si = si.submit() + + si.reload() + self.assertEqual(si.outstanding_amount, 1) + + # Exchange Gain/Loss Journal should've been created for the partial advance + exc_je_for_si = self.get_journals_for(si.doctype, si.name) + exc_je_for_adv = self.get_journals_for(adv.doctype, adv.name) + + self.assertNotEqual(exc_je_for_si, []) + self.assertEqual(len(exc_je_for_si), 1) + self.assertEqual(len(exc_je_for_adv), 1) + self.assertEqual(exc_je_for_si, exc_je_for_adv) + + # Payment + pe = self.create_payment_entry(amount=1, source_exc_rate=75).save() + pe.append( + "references", + {"reference_doctype": si.doctype, "reference_name": si.name, "allocated_amount": 1}, + ) + pe = pe.save().submit() + + si.reload() + self.assertEqual(si.outstanding_amount, 0) + + # Exchange Gain/Loss Journal should've been created for the payment + exc_je_for_si = self.get_journals_for(si.doctype, si.name) + exc_je_for_pe = self.get_journals_for(pe.doctype, pe.name) + + self.assertNotEqual(exc_je_for_si, []) + # There should be 2 JE's now. One for the advance and one for the payment + self.assertEqual(len(exc_je_for_si), 2) + self.assertEqual(len(exc_je_for_pe), 1) + self.assertEqual(exc_je_for_si, exc_je_for_pe + exc_je_for_adv) + + adv.reload() + adv.cancel() + + si.reload() + self.assertEqual(si.outstanding_amount, 1) + + exc_je_for_si = self.get_journals_for(si.doctype, si.name) + exc_je_for_pe = self.get_journals_for(pe.doctype, pe.name) + exc_je_for_adv = self.get_journals_for(adv.doctype, adv.name) + + # Exchange Gain/Loss Journal for advance should been cancelled + self.assertEqual(len(exc_je_for_si), 1) + self.assertEqual(len(exc_je_for_pe), 1) + self.assertEqual(exc_je_for_adv, []) + + def test_05_same_payment_split_against_invoice(self): + # Invoice in Foreign Currency + si = self.create_sales_invoice(qty=2, rate=1) + # Payment + pe = self.create_payment_entry(amount=2, source_exc_rate=75).save() + pe.append( + "references", + {"reference_doctype": si.doctype, "reference_name": si.name, "allocated_amount": 1}, + ) + pe = pe.save().submit() + + si.reload() + self.assertEqual(si.outstanding_amount, 1) + + # Exchange Gain/Loss Journal should've been created. + exc_je_for_si = self.get_journals_for(si.doctype, si.name) + exc_je_for_pe = self.get_journals_for(pe.doctype, pe.name) + + self.assertNotEqual(exc_je_for_si, []) + self.assertEqual(len(exc_je_for_si), 1) + self.assertEqual(len(exc_je_for_pe), 1) + self.assertEqual(exc_je_for_si[0], exc_je_for_pe[0]) + + # Reconcile the remaining amount + pr = frappe.get_doc("Payment Reconciliation") + pr.company = self.company + pr.party_type = "Customer" + pr.party = self.customer + pr.receivable_payable_account = self.debit_usd + + pr.get_unreconciled_entries() + self.assertEqual(len(pr.invoices), 1) + self.assertEqual(len(pr.payments), 1) + + # Test exact payment allocation + invoices = [x.as_dict() for x in pr.invoices] + payments = [x.as_dict() for x in pr.payments] + pr.allocate_entries(frappe._dict({"invoices": invoices, "payments": payments})) + + pr.reconcile() + self.assertEqual(len(pr.invoices), 0) + self.assertEqual(len(pr.payments), 0) + + exc_je_for_si = self.get_journals_for(si.doctype, si.name) + exc_je_for_pe = self.get_journals_for(pe.doctype, pe.name) + self.assertEqual(len(exc_je_for_si), 2) + self.assertEqual(len(exc_je_for_pe), 2) + self.assertEqual(exc_je_for_si, exc_je_for_pe) + + # Cancel Payment + pe.reload() + pe.cancel() + + si.reload() + self.assertEqual(si.outstanding_amount, 2) + + # Exchange Gain/Loss Journal should've been cancelled + exc_je_for_si = self.get_journals_for(si.doctype, si.name) + exc_je_for_pe = self.get_journals_for(pe.doctype, pe.name) + self.assertEqual(exc_je_for_si, []) + self.assertEqual(exc_je_for_pe, []) From db46987d4b83fda5bf2ba7ea3f28588eb9956623 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Fri, 16 Jun 2023 11:34:11 +0530 Subject: [PATCH 117/501] refactor: replace with new method in purchase invoice (cherry picked from commit 7e94a1c51b428202820858f72a7e4a864cde0e9c) --- erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py index 5a7ff1c0d1c9..cefb502ede19 100644 --- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py +++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py @@ -543,6 +543,7 @@ def make_gl_entries(self, gl_entries=None, from_repost=False): merge_entries=False, from_repost=from_repost, ) + self.make_exchange_gain_loss_journal() elif self.docstatus == 2: provisional_entries = [a for a in gl_entries if a.voucher_type == "Purchase Receipt"] make_reverse_gl_entries(voucher_type=self.doctype, voucher_no=self.name) @@ -587,7 +588,6 @@ def get_gl_entries(self, warehouse_account=None): self.get_asset_gl_entry(gl_entries) self.make_tax_gl_entries(gl_entries) - self.make_exchange_gain_loss_gl_entries(gl_entries) self.make_internal_transfer_gl_entries(gl_entries) gl_entries = make_regional_gl_entries(gl_entries, self) From 287af687cfab88f37eb7289895d794423b35cdd1 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Fri, 16 Jun 2023 12:32:21 +0530 Subject: [PATCH 118/501] chore: patch to update property setter for Journal Entry Accounts (cherry picked from commit 0587338435a6ffeeb59669ff20dbd9779b9ac740) --- erpnext/patches.txt | 1 + ...eference_type_in_journal_entry_accounts.py | 22 +++++++++++++++++++ 2 files changed, 23 insertions(+) create mode 100644 erpnext/patches/v14_0/update_reference_type_in_journal_entry_accounts.py diff --git a/erpnext/patches.txt b/erpnext/patches.txt index 75f728afa88b..4a0c9b3b4102 100644 --- a/erpnext/patches.txt +++ b/erpnext/patches.txt @@ -328,6 +328,7 @@ erpnext.patches.v14_0.set_pick_list_status erpnext.patches.v13_0.update_docs_link erpnext.patches.v14_0.enable_all_leads execute:frappe.db.set_single_value("Accounts Settings", "merge_similar_account_heads", 0) +erpnext.patches.v14_0.update_reference_type_in_journal_entry_accounts # below migration patches should always run last erpnext.patches.v14_0.migrate_gl_to_payment_ledger erpnext.patches.v14_0.update_company_in_ldc diff --git a/erpnext/patches/v14_0/update_reference_type_in_journal_entry_accounts.py b/erpnext/patches/v14_0/update_reference_type_in_journal_entry_accounts.py new file mode 100644 index 000000000000..48b6bcf755f2 --- /dev/null +++ b/erpnext/patches/v14_0/update_reference_type_in_journal_entry_accounts.py @@ -0,0 +1,22 @@ +import frappe + + +def execute(): + """ + Update Propery Setters for Journal Entry with new 'Entry Type' + """ + new_reference_type = "Payment Entry" + prop_setter = frappe.db.get_list( + "Property Setter", + filters={ + "doc_type": "Journal Entry Account", + "field_name": "reference_type", + "property": "options", + }, + ) + if prop_setter: + property_setter_doc = frappe.get_doc("Property Setter", prop_setter[0].get("name")) + + if new_reference_type not in property_setter_doc.value.split("\n"): + property_setter_doc.value += "\n" + new_reference_type + property_setter_doc.save() From 72005bdb3955d7c11e67597e6cc0172d4445fd85 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Fri, 16 Jun 2023 14:01:48 +0530 Subject: [PATCH 119/501] refactor: add new reference type in journal entry account (cherry picked from commit 13febcac811507c7c61bc116ca797857d0b5baf5) --- .../doctype/journal_entry_account/journal_entry_account.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/erpnext/accounts/doctype/journal_entry_account/journal_entry_account.json b/erpnext/accounts/doctype/journal_entry_account/journal_entry_account.json index 47ad19e0f98a..3ba8cea94bbf 100644 --- a/erpnext/accounts/doctype/journal_entry_account/journal_entry_account.json +++ b/erpnext/accounts/doctype/journal_entry_account/journal_entry_account.json @@ -203,7 +203,7 @@ "fieldtype": "Select", "label": "Reference Type", "no_copy": 1, - "options": "\nSales Invoice\nPurchase Invoice\nJournal Entry\nSales Order\nPurchase Order\nExpense Claim\nAsset\nLoan\nPayroll Entry\nEmployee Advance\nExchange Rate Revaluation\nInvoice Discounting\nFees\nFull and Final Statement" + "options": "\nSales Invoice\nPurchase Invoice\nJournal Entry\nSales Order\nPurchase Order\nExpense Claim\nAsset\nLoan\nPayroll Entry\nEmployee Advance\nExchange Rate Revaluation\nInvoice Discounting\nFees\nFull and Final Statement\nPayment Entry" }, { "fieldname": "reference_name", @@ -284,7 +284,7 @@ "idx": 1, "istable": 1, "links": [], - "modified": "2022-10-26 20:03:10.906259", + "modified": "2023-06-16 14:11:13.507807", "modified_by": "Administrator", "module": "Accounts", "name": "Journal Entry Account", From a9da619903e6e4a13cb3ec5319b48a6e56c01137 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Fri, 16 Jun 2023 14:07:44 +0530 Subject: [PATCH 120/501] chore: fix logic for purchase invoice and some typos (cherry picked from commit 34b5e849a290ee02d9b653286dfbe6590d35a800) --- erpnext/accounts/doctype/payment_entry/payment_entry.py | 4 ++-- erpnext/controllers/accounts_controller.py | 7 +++++-- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/erpnext/accounts/doctype/payment_entry/payment_entry.py b/erpnext/accounts/doctype/payment_entry/payment_entry.py index f0c36cef47f5..65c55686bce7 100644 --- a/erpnext/accounts/doctype/payment_entry/payment_entry.py +++ b/erpnext/accounts/doctype/payment_entry/payment_entry.py @@ -786,8 +786,8 @@ def calculate_base_allocated_amount_for_reference(self, d) -> float: else: # Use source/target exchange rate, so no difference amount is calculated. - # then update exchange gain/loss amount in refernece table - # if there is an amount, submit a JE for that + # then update exchange gain/loss amount in reference table + # if there is an exchange gain/loss amount in reference table, submit a JE for that exchange_rate = 1 if self.payment_type == "Receive": diff --git a/erpnext/controllers/accounts_controller.py b/erpnext/controllers/accounts_controller.py index dcaaff08ade3..f9d7b0deb508 100644 --- a/erpnext/controllers/accounts_controller.py +++ b/erpnext/controllers/accounts_controller.py @@ -966,7 +966,7 @@ def make_exchange_gain_loss_journal(self) -> None: """ Make Exchange Gain/Loss journal for Invoices and Payments """ - # Cancelling is existing exchange gain/loss journals is handled in on_cancel event + # Cancelling existing exchange gain/loss journals is handled in on_cancel event in accounts/utils.py if self.docstatus == 1: if self.get("doctype") == "Payment Entry": gain_loss_to_book = [x for x in self.references if x.exchange_gain_loss != 0] @@ -1021,6 +1021,10 @@ def make_exchange_gain_loss_journal(self) -> None: "Account", party_account, "account_currency" ) dr_or_cr = "debit" if d.exchange_gain_loss > 0 else "credit" + + if d.reference_doctype == "Purchase Invoice": + dr_or_cr = "debit" if dr_or_cr == "credit" else "credit" + reverse_dr_or_cr = "debit" if dr_or_cr == "credit" else "credit" gain_loss_account = frappe.get_cached_value( @@ -1074,7 +1078,6 @@ def make_exchange_gain_loss_journal(self) -> None: journal_entry.save() journal_entry.submit() - # frappe.throw("stopping...") def make_precision_loss_gl_entry(self, gl_entries): round_off_account, round_off_cost_center = get_round_off_account_and_cost_center( From 6c6acef78eee6a7228f51a63342ad4edd5987a21 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Mon, 19 Jun 2023 17:51:05 +0530 Subject: [PATCH 121/501] refactor: helper method (cherry picked from commit c1184585eda2e37b74718b95d541fa0419511bd9) --- .../doctype/payment_entry/test_payment_entry.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/erpnext/accounts/doctype/payment_entry/test_payment_entry.py b/erpnext/accounts/doctype/payment_entry/test_payment_entry.py index ca1d317c38ee..47bf6df37d1f 100644 --- a/erpnext/accounts/doctype/payment_entry/test_payment_entry.py +++ b/erpnext/accounts/doctype/payment_entry/test_payment_entry.py @@ -31,6 +31,16 @@ class TestPaymentEntry(FrappeTestCase): def tearDown(self): frappe.db.rollback() + def get_journals_for(self, voucher_type: str, voucher_no: str) -> list: + journals = [] + if voucher_type and voucher_no: + journals = frappe.db.get_all( + "Journal Entry Account", + filters={"reference_type": voucher_type, "reference_name": voucher_no, "docstatus": 1}, + fields=["parent"], + ) + return journals + def test_payment_entry_against_order(self): so = make_sales_order() pe = get_payment_entry("Sales Order", so.name, bank_account="_Test Cash - _TC") From d030f4a1007fcdacede7657721015c83101e875f Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Mon, 19 Jun 2023 09:58:18 +0530 Subject: [PATCH 122/501] refactor: remove unused variable, pe should pull in parent exc rate 1. 'reference_doc' variable is never set. Hence, removing. 2. set_exchange_rate() relies on ref_doc, which was never set due to point [1]. Replacing it with 'doc'. 3. Sales/Purchase Invoice has 'conversion_rate' field for tracking exchange rate. Added a get statement for them as well. (cherry picked from commit 92ae9c220110ddcb32d90a1bb89f0b85e72ff7d0) --- erpnext/accounts/doctype/payment_entry/payment_entry.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/erpnext/accounts/doctype/payment_entry/payment_entry.py b/erpnext/accounts/doctype/payment_entry/payment_entry.py index 65c55686bce7..b4c39f41063a 100644 --- a/erpnext/accounts/doctype/payment_entry/payment_entry.py +++ b/erpnext/accounts/doctype/payment_entry/payment_entry.py @@ -362,7 +362,7 @@ def set_source_exchange_rate(self, ref_doc=None): else: if ref_doc: if self.paid_from_account_currency == ref_doc.currency: - self.source_exchange_rate = ref_doc.get("exchange_rate") + self.source_exchange_rate = ref_doc.get("exchange_rate") or ref_doc.get("conversion_rate") if not self.source_exchange_rate: self.source_exchange_rate = get_exchange_rate( @@ -375,7 +375,7 @@ def set_target_exchange_rate(self, ref_doc=None): elif self.paid_to and not self.target_exchange_rate: if ref_doc: if self.paid_to_account_currency == ref_doc.currency: - self.target_exchange_rate = ref_doc.get("exchange_rate") + self.target_exchange_rate = ref_doc.get("exchange_rate") or ref_doc.get("conversion_rate") if not self.target_exchange_rate: self.target_exchange_rate = get_exchange_rate( @@ -1895,7 +1895,6 @@ def get_payment_entry( payment_type=None, reference_date=None, ): - reference_doc = None doc = frappe.get_doc(dt, dn) over_billing_allowance = frappe.db.get_single_value("Accounts Settings", "over_billing_allowance") if dt in ("Sales Order", "Purchase Order") and flt(doc.per_billed, 2) >= ( @@ -2036,7 +2035,7 @@ def get_payment_entry( update_accounting_dimensions(pe, doc) if party_account and bank: - pe.set_exchange_rate(ref_doc=reference_doc) + pe.set_exchange_rate(ref_doc=doc) pe.set_amounts() if discount_amount: From 59b7e962555da99973c3798629a0eb4ccfcfa941 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Mon, 19 Jun 2023 10:23:23 +0530 Subject: [PATCH 123/501] refactor: assert exchange gain/loss amount in reference table (cherry picked from commit 4ff53e106271e0562e2ba5604802a41c24c999c4) --- .../doctype/payment_entry/test_payment_entry.py | 14 ++++---------- 1 file changed, 4 insertions(+), 10 deletions(-) diff --git a/erpnext/accounts/doctype/payment_entry/test_payment_entry.py b/erpnext/accounts/doctype/payment_entry/test_payment_entry.py index 47bf6df37d1f..5a1dda5c459c 100644 --- a/erpnext/accounts/doctype/payment_entry/test_payment_entry.py +++ b/erpnext/accounts/doctype/payment_entry/test_payment_entry.py @@ -601,21 +601,15 @@ def test_payment_entry_against_si_usd_to_usd_with_deduction_in_base_currency(sel pe.target_exchange_rate = 45.263 pe.reference_no = "1" pe.reference_date = "2016-01-01" - - pe.append( - "deductions", - { - "account": "_Test Exchange Gain/Loss - _TC", - "cost_center": "_Test Cost Center - _TC", - "amount": 94.80, - }, - ) - pe.save() self.assertEqual(flt(pe.difference_amount, 2), 0.0) self.assertEqual(flt(pe.unallocated_amount, 2), 0.0) + # the exchange gain/loss amount is captured in reference table and a separate Journal will be submitted for them + # payment entry will not be generating difference amount + self.assertEqual(flt(pe.references[0].exchange_gain_loss, 2), -94.74) + def test_payment_entry_retrieves_last_exchange_rate(self): from erpnext.setup.doctype.currency_exchange.test_currency_exchange import ( save_new_records, From 1f76dde0256530172e56b970ed5bf88fe116babd Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Mon, 19 Jun 2023 11:26:49 +0530 Subject: [PATCH 124/501] refactor(test): exc gain/loss booked through journal (cherry picked from commit 00a2e42a47fb064afbb31b27653a54d12b6c8097) --- .../payment_entry/test_payment_entry.py | 19 +++++++------------ 1 file changed, 7 insertions(+), 12 deletions(-) diff --git a/erpnext/accounts/doctype/payment_entry/test_payment_entry.py b/erpnext/accounts/doctype/payment_entry/test_payment_entry.py index 5a1dda5c459c..21379458874f 100644 --- a/erpnext/accounts/doctype/payment_entry/test_payment_entry.py +++ b/erpnext/accounts/doctype/payment_entry/test_payment_entry.py @@ -796,33 +796,28 @@ def test_payment_entry_exchange_gain_loss(self): pe.reference_no = "1" pe.reference_date = "2016-01-01" pe.source_exchange_rate = 55 - - pe.append( - "deductions", - { - "account": "_Test Exchange Gain/Loss - _TC", - "cost_center": "_Test Cost Center - _TC", - "amount": -500, - }, - ) pe.save() self.assertEqual(pe.unallocated_amount, 0) self.assertEqual(pe.difference_amount, 0) - + self.assertEqual(pe.references[0].exchange_gain_loss, 500) pe.submit() expected_gle = dict( (d[0], d) for d in [ - ["_Test Receivable USD - _TC", 0, 5000, si.name], + ["_Test Receivable USD - _TC", 0, 5500, si.name], ["_Test Bank USD - _TC", 5500, 0, None], - ["_Test Exchange Gain/Loss - _TC", 0, 500, None], ] ) self.validate_gl_entries(pe.name, expected_gle) + # Exchange gain/loss should have been posted through a journal + exc_je_for_si = self.get_journals_for(si.doctype, si.name) + exc_je_for_pe = self.get_journals_for(pe.doctype, pe.name) + + self.assertEqual(exc_je_for_si, exc_je_for_pe) outstanding_amount = flt(frappe.db.get_value("Sales Invoice", si.name, "outstanding_amount")) self.assertEqual(outstanding_amount, 0) From 220bf245551f84b919040beafa3bcb3d45344ec6 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Mon, 26 Jun 2023 17:34:28 +0530 Subject: [PATCH 125/501] refactor: exc booking logic for Journal Entry (cherry picked from commit 7b516f84636e7219ac17d972c80a8286c385e954) --- erpnext/accounts/utils.py | 3 + erpnext/controllers/accounts_controller.py | 78 +++++++++++++++++++++- 2 files changed, 80 insertions(+), 1 deletion(-) diff --git a/erpnext/accounts/utils.py b/erpnext/accounts/utils.py index adb789e7493a..034a404564f1 100644 --- a/erpnext/accounts/utils.py +++ b/erpnext/accounts/utils.py @@ -459,6 +459,9 @@ def reconcile_against_document(args, skip_ref_details_update_for_pe=False): # n # update ref in advance entry if voucher_type == "Journal Entry": update_reference_in_journal_entry(entry, doc, do_not_save=True) + # advance section in sales/purchase invoice and reconciliation tool,both pass on exchange gain/loss + # amount and account in args + doc.make_exchange_gain_loss_journal(args) else: update_reference_in_payment_entry( entry, doc, do_not_save=True, skip_ref_details_update_for_pe=skip_ref_details_update_for_pe diff --git a/erpnext/controllers/accounts_controller.py b/erpnext/controllers/accounts_controller.py index f9d7b0deb508..a14c571241eb 100644 --- a/erpnext/controllers/accounts_controller.py +++ b/erpnext/controllers/accounts_controller.py @@ -962,13 +962,89 @@ def set_advance_gain_or_loss(self): d.exchange_gain_loss = difference - def make_exchange_gain_loss_journal(self) -> None: + def make_exchange_gain_loss_journal(self, args=None) -> None: """ Make Exchange Gain/Loss journal for Invoices and Payments """ # Cancelling existing exchange gain/loss journals is handled in on_cancel event in accounts/utils.py if self.docstatus == 1: + if self.get("doctype") == "Journal Entry": + if args: + for arg in args: + print(arg) + if arg.get("difference_amount") != 0 and arg.get("difference_account"): + journal_entry = frappe.new_doc("Journal Entry") + journal_entry.voucher_type = "Exchange Gain Or Loss" + journal_entry.company = self.company + journal_entry.posting_date = nowdate() + journal_entry.multi_currency = 1 + + party_account = arg.account + party_account_currency = frappe.get_cached_value( + "Account", party_account, "account_currency" + ) + dr_or_cr = "debit" if arg.difference_amount > 0 else "credit" + + if arg.reference_doctype == "Purchase Invoice": + dr_or_cr = "debit" if dr_or_cr == "credit" else "credit" + + reverse_dr_or_cr = "debit" if dr_or_cr == "credit" else "credit" + + gain_loss_account = arg.difference_account + + if not gain_loss_account: + frappe.throw( + _("Please set default Exchange Gain/Loss Account in Company {}").format( + self.get("company") + ) + ) + + gain_loss_account_currency = get_account_currency(gain_loss_account) + if gain_loss_account_currency != self.company_currency: + frappe.throw( + _("Currency for {0} must be {1}").format(gain_loss_account, self.company_currency) + ) + + journal_account = frappe._dict( + { + "account": party_account, + "party_type": arg.party_type, + "party": arg.party, + "account_currency": party_account_currency, + "exchange_rate": 0, + "cost_center": erpnext.get_default_cost_center(self.company), + "reference_type": arg.against_voucher_type, + "reference_name": arg.against_voucher, + "reference_detail_no": arg.idx, + dr_or_cr: abs(arg.difference_amount), + dr_or_cr + "_in_account_currency": 0, + } + ) + + journal_entry.append("accounts", journal_account) + + journal_account = frappe._dict( + { + "account": gain_loss_account, + "account_currency": gain_loss_account_currency, + "exchange_rate": 1, + "cost_center": erpnext.get_default_cost_center(self.company), + # TODO: figure out a way to pass reference + # "reference_type": self.doctype, + # "reference_name": self.name, + # "reference_detail_no": arg.idx, + reverse_dr_or_cr + "_in_account_currency": abs(arg.difference_amount), + reverse_dr_or_cr: abs(arg.difference_amount), + } + ) + + journal_entry.append("accounts", journal_account) + + journal_entry.save() + journal_entry.submit() + if self.get("doctype") == "Payment Entry": + # For Payment Entry, exchange_gain_loss field in the `reference` table is the trigger for journal creation gain_loss_to_book = [x for x in self.references if x.exchange_gain_loss != 0] booked = [] if gain_loss_to_book: From 72e88d22ede487a93b4b5e525fbe76c6b2ee1a86 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Mon, 26 Jun 2023 21:43:20 +0530 Subject: [PATCH 126/501] chore: remove debugging statements and fixing failing unit tests (cherry picked from commit ee3ce82ea82df9dd2910e4d29a5c2c4f885be393) --- erpnext/controllers/accounts_controller.py | 23 +++++++++++----------- 1 file changed, 11 insertions(+), 12 deletions(-) diff --git a/erpnext/controllers/accounts_controller.py b/erpnext/controllers/accounts_controller.py index a14c571241eb..51c5c83f2b31 100644 --- a/erpnext/controllers/accounts_controller.py +++ b/erpnext/controllers/accounts_controller.py @@ -971,26 +971,25 @@ def make_exchange_gain_loss_journal(self, args=None) -> None: if self.get("doctype") == "Journal Entry": if args: for arg in args: - print(arg) - if arg.get("difference_amount") != 0 and arg.get("difference_account"): + if arg.get("difference_amount", 0) != 0 and arg.get("difference_account"): journal_entry = frappe.new_doc("Journal Entry") journal_entry.voucher_type = "Exchange Gain Or Loss" journal_entry.company = self.company journal_entry.posting_date = nowdate() journal_entry.multi_currency = 1 - party_account = arg.account + party_account = arg.get("account") party_account_currency = frappe.get_cached_value( "Account", party_account, "account_currency" ) - dr_or_cr = "debit" if arg.difference_amount > 0 else "credit" + dr_or_cr = "debit" if arg.get("difference_amount") > 0 else "credit" if arg.reference_doctype == "Purchase Invoice": dr_or_cr = "debit" if dr_or_cr == "credit" else "credit" reverse_dr_or_cr = "debit" if dr_or_cr == "credit" else "credit" - gain_loss_account = arg.difference_account + gain_loss_account = arg.get("difference_account") if not gain_loss_account: frappe.throw( @@ -1008,14 +1007,14 @@ def make_exchange_gain_loss_journal(self, args=None) -> None: journal_account = frappe._dict( { "account": party_account, - "party_type": arg.party_type, - "party": arg.party, + "party_type": arg.get("party_type"), + "party": arg.get("party"), "account_currency": party_account_currency, "exchange_rate": 0, "cost_center": erpnext.get_default_cost_center(self.company), - "reference_type": arg.against_voucher_type, - "reference_name": arg.against_voucher, - "reference_detail_no": arg.idx, + "reference_type": arg.get("against_voucher_type"), + "reference_name": arg.get("against_voucher"), + "reference_detail_no": arg.get("idx"), dr_or_cr: abs(arg.difference_amount), dr_or_cr + "_in_account_currency": 0, } @@ -1033,8 +1032,8 @@ def make_exchange_gain_loss_journal(self, args=None) -> None: # "reference_type": self.doctype, # "reference_name": self.name, # "reference_detail_no": arg.idx, - reverse_dr_or_cr + "_in_account_currency": abs(arg.difference_amount), - reverse_dr_or_cr: abs(arg.difference_amount), + reverse_dr_or_cr + "_in_account_currency": abs(arg.get("difference_amount")), + reverse_dr_or_cr: abs(arg.get("difference_amount")), } ) From e2c35f8c856d9ffab14d616778c49ee739c773c3 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Tue, 27 Jun 2023 11:16:52 +0530 Subject: [PATCH 127/501] refactor(test): assert Exc journal when reconciling Journa to invoic (cherry picked from commit 389cadf15715b1483986297b38ec2dbb268d2b26) --- .../test_payment_reconciliation.py | 16 +++++++++++++--- erpnext/controllers/accounts_controller.py | 2 +- 2 files changed, 14 insertions(+), 4 deletions(-) diff --git a/erpnext/accounts/doctype/payment_reconciliation/test_payment_reconciliation.py b/erpnext/accounts/doctype/payment_reconciliation/test_payment_reconciliation.py index 2ac7df0e39bc..1d843abde1d9 100644 --- a/erpnext/accounts/doctype/payment_reconciliation/test_payment_reconciliation.py +++ b/erpnext/accounts/doctype/payment_reconciliation/test_payment_reconciliation.py @@ -686,14 +686,24 @@ def test_difference_amount_via_journal_entry(self): # Check if difference journal entry gets generated for difference amount after reconciliation pr.reconcile() - total_debit_amount = frappe.db.get_all( + total_credit_amount = frappe.db.get_all( "Journal Entry Account", {"account": self.debtors_eur, "docstatus": 1, "reference_name": si.name}, - "sum(debit) as amount", + "sum(credit) as amount", group_by="reference_name", )[0].amount - self.assertEqual(flt(total_debit_amount, 2), -500) + # total credit includes the exchange gain/loss amount + self.assertEqual(flt(total_credit_amount, 2), 8500) + + jea_parent = frappe.db.get_all( + "Journal Entry Account", + filters={"account": self.debtors_eur, "docstatus": 1, "reference_name": si.name, "credit": 500}, + fields=["parent"], + )[0] + self.assertEqual( + frappe.db.get_value("Journal Entry", jea_parent.parent, "voucher_type"), "Exchange Gain Or Loss" + ) def test_difference_amount_via_payment_entry(self): # Make Sale Invoice diff --git a/erpnext/controllers/accounts_controller.py b/erpnext/controllers/accounts_controller.py index 51c5c83f2b31..fa64b2f22e1e 100644 --- a/erpnext/controllers/accounts_controller.py +++ b/erpnext/controllers/accounts_controller.py @@ -1032,8 +1032,8 @@ def make_exchange_gain_loss_journal(self, args=None) -> None: # "reference_type": self.doctype, # "reference_name": self.name, # "reference_detail_no": arg.idx, - reverse_dr_or_cr + "_in_account_currency": abs(arg.get("difference_amount")), reverse_dr_or_cr: abs(arg.get("difference_amount")), + reverse_dr_or_cr + "_in_account_currency": 0, } ) From 00a26ea6e6ae26de08699682851e2067a0932351 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Tue, 27 Jun 2023 11:41:14 +0530 Subject: [PATCH 128/501] refactor(test): payment will have same exch rate - no gain/loss while making payment entry using reference to sales/purchase invoice, it herits the parent docs exchange rate. so, there will be no exchange gain/loss (cherry picked from commit ee2d1fa36e24326aa9f5b11877139857ed3a6f21) --- .../accounts/doctype/payment_request/test_payment_request.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/erpnext/accounts/doctype/payment_request/test_payment_request.py b/erpnext/accounts/doctype/payment_request/test_payment_request.py index e17a846dd814..feb2fdffc95b 100644 --- a/erpnext/accounts/doctype/payment_request/test_payment_request.py +++ b/erpnext/accounts/doctype/payment_request/test_payment_request.py @@ -144,8 +144,7 @@ def test_payment_entry(self): (d[0], d) for d in [ ["_Test Receivable USD - _TC", 0, 5000, si_usd.name], - [pr.payment_account, 6290.0, 0, None], - ["_Test Exchange Gain/Loss - _TC", 0, 1290, None], + [pr.payment_account, 5000.0, 0, None], ] ) From f6bb6b78db2854c53550f5cf69f82f47f8a7a183 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Tue, 27 Jun 2023 12:29:02 +0530 Subject: [PATCH 129/501] refactor: only post on base currency for exchange gain/loss (cherry picked from commit 78bc712756bc9d8966c22cbbce68e5058daa87db) --- erpnext/controllers/accounts_controller.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/controllers/accounts_controller.py b/erpnext/controllers/accounts_controller.py index fa64b2f22e1e..43813882e58d 100644 --- a/erpnext/controllers/accounts_controller.py +++ b/erpnext/controllers/accounts_controller.py @@ -1144,7 +1144,7 @@ def make_exchange_gain_loss_journal(self, args=None) -> None: "reference_type": self.doctype, "reference_name": self.name, "reference_detail_no": d.idx, - reverse_dr_or_cr + "_in_account_currency": abs(d.exchange_gain_loss), + reverse_dr_or_cr + "_in_account_currency": 0, reverse_dr_or_cr: abs(d.exchange_gain_loss), } ) From 4025442491103d771689a4fe701814f4d737b34e Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Tue, 27 Jun 2023 12:42:07 +0530 Subject: [PATCH 130/501] refactor(test): exc gain/loss journal for advance in purchase invoice (cherry picked from commit 5b06bd1af4197b0c6ab8714c65d8f7a578499163) --- .../purchase_invoice/test_purchase_invoice.py | 63 +++++++++++++++++-- 1 file changed, 59 insertions(+), 4 deletions(-) diff --git a/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py b/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py index ab2e3cf103c6..f60c83dcf5cc 100644 --- a/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py +++ b/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py @@ -1264,10 +1264,11 @@ def test_gain_loss_with_advance_entry(self): pi.save() pi.submit() + creditors_account = pi.credit_to + expected_gle = [ ["_Test Account Cost for Goods Sold - _TC", 37500.0], - ["_Test Payable USD - _TC", -35000.0], - ["Exchange Gain/Loss - _TC", -2500.0], + ["_Test Payable USD - _TC", -37500.0], ] gl_entries = frappe.db.sql( @@ -1284,6 +1285,31 @@ def test_gain_loss_with_advance_entry(self): self.assertEqual(expected_gle[i][0], gle.account) self.assertEqual(expected_gle[i][1], gle.balance) + pi.reload() + self.assertEqual(pi.outstanding_amount, 0) + + total_debit_amount = frappe.db.get_all( + "Journal Entry Account", + {"account": creditors_account, "docstatus": 1, "reference_name": pi.name}, + "sum(debit) as amount", + group_by="reference_name", + )[0].amount + self.assertEqual(flt(total_debit_amount, 2), 2500) + jea_parent = frappe.db.get_all( + "Journal Entry Account", + filters={ + "account": creditors_account, + "docstatus": 1, + "reference_name": pi.name, + "debit": 2500, + "debit_in_account_currency": 0, + }, + fields=["parent"], + )[0] + self.assertEqual( + frappe.db.get_value("Journal Entry", jea_parent.parent, "voucher_type"), "Exchange Gain Or Loss" + ) + pi_2 = make_purchase_invoice( supplier="_Test Supplier USD", currency="USD", @@ -1308,10 +1334,12 @@ def test_gain_loss_with_advance_entry(self): pi_2.save() pi_2.submit() + pi_2.reload() + self.assertEqual(pi_2.outstanding_amount, 0) + expected_gle = [ ["_Test Account Cost for Goods Sold - _TC", 36500.0], - ["_Test Payable USD - _TC", -35000.0], - ["Exchange Gain/Loss - _TC", -1500.0], + ["_Test Payable USD - _TC", -36500.0], ] gl_entries = frappe.db.sql( @@ -1342,12 +1370,39 @@ def test_gain_loss_with_advance_entry(self): self.assertEqual(expected_gle[i][0], gle.account) self.assertEqual(expected_gle[i][1], gle.balance) + total_debit_amount = frappe.db.get_all( + "Journal Entry Account", + {"account": creditors_account, "docstatus": 1, "reference_name": pi_2.name}, + "sum(debit) as amount", + group_by="reference_name", + )[0].amount + self.assertEqual(flt(total_debit_amount, 2), 1500) + jea_parent_2 = frappe.db.get_all( + "Journal Entry Account", + filters={ + "account": creditors_account, + "docstatus": 1, + "reference_name": pi_2.name, + "debit": 1500, + "debit_in_account_currency": 0, + }, + fields=["parent"], + )[0] + self.assertEqual( + frappe.db.get_value("Journal Entry", jea_parent_2.parent, "voucher_type"), + "Exchange Gain Or Loss", + ) + pi.reload() pi.cancel() + self.assertEqual(frappe.db.get_value("Journal Entry", jea_parent.parent, "docstatus"), 2) + pi_2.reload() pi_2.cancel() + self.assertEqual(frappe.db.get_value("Journal Entry", jea_parent_2.parent, "docstatus"), 2) + pay.reload() pay.cancel() From e20b21373778f9f2df3645424387a2def42cd1eb Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Tue, 27 Jun 2023 16:11:03 +0530 Subject: [PATCH 131/501] refactor(test): difference amount no updated for exchange gain/loss (cherry picked from commit 72bc5b3a11528611db8a322d68c0ecc422b570c6) # Conflicts: # erpnext/accounts/test/test_utils.py --- erpnext/accounts/test/test_utils.py | 53 +++++++++++++++++++++++++++++ 1 file changed, 53 insertions(+) diff --git a/erpnext/accounts/test/test_utils.py b/erpnext/accounts/test/test_utils.py index 882cd694a326..f72ac783bd3c 100644 --- a/erpnext/accounts/test/test_utils.py +++ b/erpnext/accounts/test/test_utils.py @@ -73,6 +73,59 @@ def test_stock_voucher_sorting(self): sorted_vouchers = sort_stock_vouchers_by_posting_date(list(reversed(vouchers))) self.assertEqual(sorted_vouchers, vouchers) +<<<<<<< HEAD +======= + def test_update_reference_in_payment_entry(self): + item = make_item().name + + purchase_invoice = make_purchase_invoice( + item=item, supplier="_Test Supplier USD", currency="USD", conversion_rate=82.32, do_not_submit=1 + ) + purchase_invoice.credit_to = "_Test Payable USD - _TC" + purchase_invoice.submit() + + payment_entry = get_payment_entry(purchase_invoice.doctype, purchase_invoice.name) + payment_entry.paid_amount = 15725 + payment_entry.deductions = [] + payment_entry.save() + + # below is the difference between base_received_amount and base_paid_amount + self.assertEqual(payment_entry.difference_amount, -4855.0) + + payment_entry.target_exchange_rate = 62.9 + payment_entry.save() + + # below is due to change in exchange rate + self.assertEqual(payment_entry.references[0].exchange_gain_loss, -4855.0) + + payment_entry.references = [] + self.assertEqual(payment_entry.difference_amount, 0.0) + payment_entry.submit() + + payment_reconciliation = frappe.new_doc("Payment Reconciliation") + payment_reconciliation.company = payment_entry.company + payment_reconciliation.party_type = "Supplier" + payment_reconciliation.party = purchase_invoice.supplier + payment_reconciliation.receivable_payable_account = payment_entry.paid_to + payment_reconciliation.get_unreconciled_entries() + payment_reconciliation.allocate_entries( + { + "payments": [d.__dict__ for d in payment_reconciliation.payments], + "invoices": [d.__dict__ for d in payment_reconciliation.invoices], + } + ) + for d in payment_reconciliation.invoices: + # Reset invoice outstanding_amount because allocate_entries will zero this value out. + d.outstanding_amount = d.amount + for d in payment_reconciliation.allocation: + d.difference_account = "Exchange Gain/Loss - _TC" + payment_reconciliation.reconcile() + + payment_entry.load_from_db() + self.assertEqual(len(payment_entry.references), 1) + self.assertEqual(payment_entry.difference_amount, 0) + +>>>>>>> 72bc5b3a11 (refactor(test): difference amount no updated for exchange gain/loss) ADDRESS_RECORDS = [ { From 86aead3d45bdf88c081039149f1ab034968555da Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Tue, 27 Jun 2023 16:57:38 +0530 Subject: [PATCH 132/501] refactor: remove call for setting deductions in payment entry (cherry picked from commit 1bcb728c850c67f3e479eb402ce1296dc215496b) # Conflicts: # erpnext/accounts/utils.py --- erpnext/accounts/utils.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/erpnext/accounts/utils.py b/erpnext/accounts/utils.py index 034a404564f1..38ba5095b702 100644 --- a/erpnext/accounts/utils.py +++ b/erpnext/accounts/utils.py @@ -636,6 +636,7 @@ def update_reference_in_payment_entry( new_row.docstatus = 1 new_row.update(reference_details) +<<<<<<< HEAD payment_entry.flags.ignore_validate_update_after_submit = True payment_entry.setup_party_account_field() payment_entry.set_missing_values() @@ -652,6 +653,8 @@ def update_reference_in_payment_entry( payment_entry.set_gain_or_loss(account_details=account_details) +======= +>>>>>>> 1bcb728c85 (refactor: remove call for setting deductions in payment entry) payment_entry.flags.ignore_validate_update_after_submit = True payment_entry.setup_party_account_field() payment_entry.set_missing_values() From 075a7dfe2e15f8cf375d01aa324c4e2c6d93aa57 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Mon, 10 Jul 2023 15:28:10 +0530 Subject: [PATCH 133/501] chore: code cleanup (cherry picked from commit cd42b268391113d5d5b10d75a6e2562736e43aae) --- .../payment_reconciliation/payment_reconciliation.py | 6 ------ erpnext/controllers/accounts_controller.py | 8 ++++++-- 2 files changed, 6 insertions(+), 8 deletions(-) diff --git a/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py b/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py index ab03699785a2..1f3d4826adc5 100644 --- a/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py +++ b/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py @@ -347,12 +347,6 @@ def reconcile_allocations(self, skip_ref_details_update_for_pe=False): payment_details = self.get_payment_details(row, dr_or_cr) reconciled_entry.append(payment_details) - # if payment_details.difference_amount and row.reference_type not in [ - # "Sales Invoice", - # "Purchase Invoice", - # ]: - # self.make_difference_entry(payment_details) - if entry_list: reconcile_against_document(entry_list, skip_ref_details_update_for_pe) diff --git a/erpnext/controllers/accounts_controller.py b/erpnext/controllers/accounts_controller.py index 43813882e58d..e50ab0118281 100644 --- a/erpnext/controllers/accounts_controller.py +++ b/erpnext/controllers/accounts_controller.py @@ -966,9 +966,12 @@ def make_exchange_gain_loss_journal(self, args=None) -> None: """ Make Exchange Gain/Loss journal for Invoices and Payments """ - # Cancelling existing exchange gain/loss journals is handled in on_cancel event in accounts/utils.py + # Cancelling existing exchange gain/loss journals is handled during the `on_cancel` event. + # see accounts/utils.py:cancel_exchange_gain_loss_journal() if self.docstatus == 1: if self.get("doctype") == "Journal Entry": + # 'args' is populated with exchange gain/loss account and the amount to be booked. + # These are generated by Sales/Purchase Invoice during reconciliation and advance allocation. if args: for arg in args: if arg.get("difference_amount", 0) != 0 and arg.get("difference_account"): @@ -1029,6 +1032,7 @@ def make_exchange_gain_loss_journal(self, args=None) -> None: "exchange_rate": 1, "cost_center": erpnext.get_default_cost_center(self.company), # TODO: figure out a way to pass reference + # throws 'Journal Entry doesn't have {account} or doesn't have matched account' # "reference_type": self.doctype, # "reference_name": self.name, # "reference_detail_no": arg.idx, @@ -1043,7 +1047,7 @@ def make_exchange_gain_loss_journal(self, args=None) -> None: journal_entry.submit() if self.get("doctype") == "Payment Entry": - # For Payment Entry, exchange_gain_loss field in the `reference` table is the trigger for journal creation + # For Payment Entry, exchange_gain_loss field in the `references` table is the trigger for journal creation gain_loss_to_book = [x for x in self.references if x.exchange_gain_loss != 0] booked = [] if gain_loss_to_book: From 01953bc0e3d4e51b1b2d033857c7384bc94cc184 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Tue, 11 Jul 2023 12:04:13 +0530 Subject: [PATCH 134/501] refactor: linkage between journal as payment and gain/loss journal (cherry picked from commit f119a1e11553a0357f937bd23a397757f3f5b54f) # Conflicts: # erpnext/accounts/doctype/gl_entry/gl_entry.py --- erpnext/accounts/doctype/gl_entry/gl_entry.py | 11 +++++++++++ .../accounts/doctype/journal_entry/journal_entry.py | 11 ++++++----- erpnext/controllers/accounts_controller.py | 6 +++--- 3 files changed, 20 insertions(+), 8 deletions(-) diff --git a/erpnext/accounts/doctype/gl_entry/gl_entry.py b/erpnext/accounts/doctype/gl_entry/gl_entry.py index fa4a66aaacfb..26f4f2ba75f7 100644 --- a/erpnext/accounts/doctype/gl_entry/gl_entry.py +++ b/erpnext/accounts/doctype/gl_entry/gl_entry.py @@ -58,7 +58,18 @@ def on_update(self): validate_balance_type(self.account, adv_adj) validate_frozen_account(self.account, adv_adj) +<<<<<<< HEAD if frappe.db.get_value("Account", self.account, "account_type") not in [ +======= + if ( + self.voucher_type == "Journal Entry" + and frappe.get_cached_value("Journal Entry", self.voucher_no, "voucher_type") + == "Exchange Gain Or Loss" + ): + return + + if frappe.get_cached_value("Account", self.account, "account_type") not in [ +>>>>>>> f119a1e115 (refactor: linkage between journal as payment and gain/loss journal) "Receivable", "Payable", ]: diff --git a/erpnext/accounts/doctype/journal_entry/journal_entry.py b/erpnext/accounts/doctype/journal_entry/journal_entry.py index 8c5cc2c921f7..90f9e820d939 100644 --- a/erpnext/accounts/doctype/journal_entry/journal_entry.py +++ b/erpnext/accounts/doctype/journal_entry/journal_entry.py @@ -487,11 +487,12 @@ def validate_against_jv(self): ) if not against_entries: - frappe.throw( - _( - "Journal Entry {0} does not have account {1} or already matched against other voucher" - ).format(d.reference_name, d.account) - ) + if self.voucher_type != "Exchange Gain Or Loss": + frappe.throw( + _( + "Journal Entry {0} does not have account {1} or already matched against other voucher" + ).format(d.reference_name, d.account) + ) else: dr_or_cr = "debit" if d.credit > 0 else "credit" valid = False diff --git a/erpnext/controllers/accounts_controller.py b/erpnext/controllers/accounts_controller.py index e50ab0118281..647a984d2dee 100644 --- a/erpnext/controllers/accounts_controller.py +++ b/erpnext/controllers/accounts_controller.py @@ -1033,9 +1033,9 @@ def make_exchange_gain_loss_journal(self, args=None) -> None: "cost_center": erpnext.get_default_cost_center(self.company), # TODO: figure out a way to pass reference # throws 'Journal Entry doesn't have {account} or doesn't have matched account' - # "reference_type": self.doctype, - # "reference_name": self.name, - # "reference_detail_no": arg.idx, + "reference_type": self.doctype, + "reference_name": self.name, + "reference_detail_no": arg.idx, reverse_dr_or_cr: abs(arg.get("difference_amount")), reverse_dr_or_cr + "_in_account_currency": 0, } From 7c3fc7eb3bc82942774e161e695de6112b57535f Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Tue, 11 Jul 2023 12:21:10 +0530 Subject: [PATCH 135/501] refactor: cancel gain/loss JE on Journal as payment cancellation (cherry picked from commit 6e18bb6456b3a7a2cbad89b86dcc124978337e4d) --- .../doctype/journal_entry/journal_entry.py | 5 ++- erpnext/accounts/utils.py | 2 +- erpnext/controllers/accounts_controller.py | 11 +++--- .../tests/test_accounts_controller.py | 34 ++++++++++++++++--- 4 files changed, 39 insertions(+), 13 deletions(-) diff --git a/erpnext/accounts/doctype/journal_entry/journal_entry.py b/erpnext/accounts/doctype/journal_entry/journal_entry.py index 90f9e820d939..743496262487 100644 --- a/erpnext/accounts/doctype/journal_entry/journal_entry.py +++ b/erpnext/accounts/doctype/journal_entry/journal_entry.py @@ -87,9 +87,8 @@ def on_submit(self): self.update_invoice_discounting() def on_cancel(self): - from erpnext.accounts.utils import unlink_ref_doc_from_payment_entries - - unlink_ref_doc_from_payment_entries(self) + # References for this Journal are removed on the `on_cancel` event in accounts_controller + super(JournalEntry, self).on_cancel() self.ignore_linked_doctypes = ( "GL Entry", "Stock Ledger Entry", diff --git a/erpnext/accounts/utils.py b/erpnext/accounts/utils.py index 38ba5095b702..9da0d7d3399f 100644 --- a/erpnext/accounts/utils.py +++ b/erpnext/accounts/utils.py @@ -671,7 +671,7 @@ def cancel_exchange_gain_loss_journal(parent_doc: dict | object) -> None: """ Cancel Exchange Gain/Loss for Sales/Purchase Invoice, if they have any. """ - if parent_doc.doctype in ["Sales Invoice", "Purchase Invoice", "Payment Entry"]: + if parent_doc.doctype in ["Sales Invoice", "Purchase Invoice", "Payment Entry", "Journal Entry"]: journals = frappe.db.get_all( "Journal Entry Account", filters={ diff --git a/erpnext/controllers/accounts_controller.py b/erpnext/controllers/accounts_controller.py index 647a984d2dee..c2f723894d4c 100644 --- a/erpnext/controllers/accounts_controller.py +++ b/erpnext/controllers/accounts_controller.py @@ -985,10 +985,11 @@ def make_exchange_gain_loss_journal(self, args=None) -> None: party_account_currency = frappe.get_cached_value( "Account", party_account, "account_currency" ) - dr_or_cr = "debit" if arg.get("difference_amount") > 0 else "credit" - if arg.reference_doctype == "Purchase Invoice": - dr_or_cr = "debit" if dr_or_cr == "credit" else "credit" + dr_or_cr = "debit" if arg.get("party_type") == "Customer" else "credit" + + # if arg.reference_doctype == "Purchase Invoice": + # dr_or_cr = "debit" if dr_or_cr == "credit" else "credit" reverse_dr_or_cr = "debit" if dr_or_cr == "credit" else "credit" @@ -1032,6 +1033,7 @@ def make_exchange_gain_loss_journal(self, args=None) -> None: "exchange_rate": 1, "cost_center": erpnext.get_default_cost_center(self.company), # TODO: figure out a way to pass reference + # TODO: add reference_detail_no field in payment ledger # throws 'Journal Entry doesn't have {account} or doesn't have matched account' "reference_type": self.doctype, "reference_name": self.name, @@ -1157,6 +1159,7 @@ def make_exchange_gain_loss_journal(self, args=None) -> None: journal_entry.save() journal_entry.submit() + # frappe.throw("stopping...") def make_precision_loss_gl_entry(self, gl_entries): round_off_account, round_off_cost_center = get_round_off_account_and_cost_center( @@ -1250,7 +1253,7 @@ def on_cancel(self): unlink_ref_doc_from_payment_entries, ) - if self.doctype in ["Sales Invoice", "Purchase Invoice", "Payment Entry"]: + if self.doctype in ["Sales Invoice", "Purchase Invoice", "Payment Entry", "Journal Entry"]: # Cancel Exchange Gain/Loss Journal before unlinking cancel_exchange_gain_loss_journal(self) diff --git a/erpnext/controllers/tests/test_accounts_controller.py b/erpnext/controllers/tests/test_accounts_controller.py index 31aa857c8f55..28a569b52467 100644 --- a/erpnext/controllers/tests/test_accounts_controller.py +++ b/erpnext/controllers/tests/test_accounts_controller.py @@ -11,6 +11,7 @@ from erpnext import get_default_cost_center from erpnext.accounts.doctype.payment_entry.payment_entry import get_payment_entry from erpnext.accounts.doctype.payment_entry.test_payment_entry import create_payment_entry +from erpnext.accounts.doctype.purchase_invoice.test_purchase_invoice import make_purchase_invoice from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice from erpnext.accounts.party import get_party_account from erpnext.stock.doctype.item.test_item import create_item @@ -20,7 +21,7 @@ def make_customer(customer_name, currency=None): if not frappe.db.exists("Customer", customer_name): customer = frappe.new_doc("Customer") customer.customer_name = customer_name - customer.type = "Individual" + customer.customer_type = "Individual" if currency: customer.default_currency = currency @@ -30,7 +31,22 @@ def make_customer(customer_name, currency=None): return customer_name -class TestAccountsController(FrappeTestCase): +def make_supplier(supplier_name, currency=None): + if not frappe.db.exists("Supplier", supplier_name): + supplier = frappe.new_doc("Supplier") + supplier.supplier_name = supplier_name + supplier.supplier_type = "Individual" + + if currency: + supplier.default_currency = currency + supplier.save() + return supplier.name + else: + return supplier_name + + +# class TestAccountsController(FrappeTestCase): +class TestAccountsController(unittest.TestCase): """ Test Exchange Gain/Loss booking on various scenarios """ @@ -39,11 +55,12 @@ def setUp(self): self.create_company() self.create_account() self.create_item() - self.create_customer() + self.create_parties() self.clear_old_entries() def tearDown(self): - frappe.db.rollback() + # frappe.db.rollback() + pass def create_company(self): company_name = "_Test Company MC" @@ -80,9 +97,16 @@ def create_item(self): ) self.item = item if isinstance(item, str) else item.item_code + def create_parties(self): + self.create_customer() + self.create_supplier() + def create_customer(self): self.customer = make_customer("_Test MC Customer USD", "USD") + def create_supplier(self): + self.supplier = make_supplier("_Test MC Supplier USD", "USD") + def create_account(self): account_name = "Debtors USD" if not frappe.db.get_value( @@ -215,7 +239,7 @@ def get_journals_for(self, voucher_type: str, voucher_no: str) -> list: return journals def test_01_payment_against_invoice(self): - # Invoice in Foreign Currency + # Sales Invoice in Foreign Currency si = self.create_sales_invoice(qty=1, rate=1) # Payment pe = self.create_payment_entry(amount=1, source_exc_rate=75).save() From 197e5881aa1485f971e80ad2be1d0c80ae9e5ad6 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Tue, 11 Jul 2023 16:34:20 +0530 Subject: [PATCH 136/501] refactor: assert payment ledger outstanding in both currencies (cherry picked from commit 73cc1ba654f39d81b7e1d9769ef6a0a8ceb689fe) --- .../tests/test_accounts_controller.py | 273 +++++++++++------- 1 file changed, 170 insertions(+), 103 deletions(-) diff --git a/erpnext/controllers/tests/test_accounts_controller.py b/erpnext/controllers/tests/test_accounts_controller.py index 28a569b52467..fc30c4b8cde8 100644 --- a/erpnext/controllers/tests/test_accounts_controller.py +++ b/erpnext/controllers/tests/test_accounts_controller.py @@ -5,6 +5,7 @@ import frappe from frappe import qb +from frappe.query_builder.functions import Sum from frappe.tests.utils import FrappeTestCase, change_settings from frappe.utils import add_days, flt, nowdate @@ -48,7 +49,15 @@ def make_supplier(supplier_name, currency=None): # class TestAccountsController(FrappeTestCase): class TestAccountsController(unittest.TestCase): """ - Test Exchange Gain/Loss booking on various scenarios + Test Exchange Gain/Loss booking on various scenarios. + Test Cases are numbered for better readbility + + 10 series - Sales Invoice against Payment Entries + 20 series - Sales Invoice against Journals + 30 series - Sales Invoice against Credit Notes + 40 series - Purchase Invoice against Payment Entries + 50 series - Purchase Invoice against Journals + 60 series - Purchase Invoice against Debit Notes """ def setUp(self): @@ -130,7 +139,13 @@ def create_account(self): self.debtors_usd = acc.name def create_sales_invoice( - self, qty=1, rate=1, posting_date=nowdate(), do_not_save=False, do_not_submit=False + self, + qty=1, + rate=1, + conversion_rate=80, + posting_date=nowdate(), + do_not_save=False, + do_not_submit=False, ): """ Helper function to populate default values in sales invoice @@ -148,7 +163,7 @@ def create_sales_invoice( parent_cost_center=self.cost_center, update_stock=0, currency="USD", - conversion_rate=80, + conversion_rate=conversion_rate, is_pos=0, is_return=0, return_against=None, @@ -238,96 +253,140 @@ def get_journals_for(self, voucher_type: str, voucher_no: str) -> list: ) return journals - def test_01_payment_against_invoice(self): - # Sales Invoice in Foreign Currency - si = self.create_sales_invoice(qty=1, rate=1) - # Payment - pe = self.create_payment_entry(amount=1, source_exc_rate=75).save() - pe.append( - "references", - {"reference_doctype": si.doctype, "reference_name": si.name, "allocated_amount": 1}, - ) - pe = pe.save().submit() - - si.reload() - self.assertEqual(si.outstanding_amount, 0) - - # Exchange Gain/Loss Journal should've been created. - exc_je_for_si = self.get_journals_for(si.doctype, si.name) - exc_je_for_pe = self.get_journals_for(pe.doctype, pe.name) - - self.assertNotEqual(exc_je_for_si, []) - self.assertEqual(len(exc_je_for_si), 1) - self.assertEqual(len(exc_je_for_pe), 1) - self.assertEqual(exc_je_for_si[0], exc_je_for_pe[0]) - - # Cancel Payment - pe.cancel() - - si.reload() - self.assertEqual(si.outstanding_amount, 1) - - # Exchange Gain/Loss Journal should've been cancelled - exc_je_for_si = self.get_journals_for(si.doctype, si.name) - exc_je_for_pe = self.get_journals_for(pe.doctype, pe.name) + def assert_ledger_outstanding( + self, + voucher_type: str, + voucher_no: str, + outstanding: float, + outstanding_in_account_currency: float, + ) -> None: + """ + Assert outstanding amount based on ledger on both company/base currency and account currency + """ - self.assertEqual(exc_je_for_si, []) - self.assertEqual(exc_je_for_pe, []) + ple = qb.DocType("Payment Ledger Entry") + current_outstanding = ( + qb.from_(ple) + .select( + Sum(ple.amount).as_("outstanding"), + Sum(ple.amount_in_account_currency).as_("outstanding_in_account_currency"), + ) + .where( + (ple.against_voucher_type == voucher_type) + & (ple.against_voucher_no == voucher_no) + & (ple.delinked == 0) + ) + .run(as_dict=True)[0] + ) + self.assertEqual(outstanding, current_outstanding.outstanding) + self.assertEqual( + outstanding_in_account_currency, current_outstanding.outstanding_in_account_currency + ) - def test_02_advance_against_invoice(self): + def test_10_payment_against_sales_invoice(self): + # Sales Invoice in Foreign Currency + rate = 80 + rate_in_account_currency = 1 + + si = self.create_sales_invoice(qty=1, rate=rate_in_account_currency) + + # Test payments with different exchange rates + for exc_rate in [75.9, 83.1, 80.01]: + with self.subTest(exc_rate=exc_rate): + pe = self.create_payment_entry(amount=1, source_exc_rate=exc_rate).save() + pe.append( + "references", + {"reference_doctype": si.doctype, "reference_name": si.name, "allocated_amount": 1}, + ) + pe = pe.save().submit() + + # Outstanding in both currencies should be '0' + si.reload() + self.assertEqual(si.outstanding_amount, 0) + self.assert_ledger_outstanding(si.doctype, si.name, 0.0, 0.0) + + # Exchange Gain/Loss Journal should've been created. + exc_je_for_si = self.get_journals_for(si.doctype, si.name) + exc_je_for_pe = self.get_journals_for(pe.doctype, pe.name) + self.assertNotEqual(exc_je_for_si, []) + self.assertEqual(len(exc_je_for_si), 1) + self.assertEqual(len(exc_je_for_pe), 1) + self.assertEqual(exc_je_for_si[0], exc_je_for_pe[0]) + + # Cancel Payment + pe.cancel() + + # outstanding should be same as grand total + si.reload() + self.assertEqual(si.outstanding_amount, rate_in_account_currency) + self.assert_ledger_outstanding(si.doctype, si.name, rate, rate_in_account_currency) + + # Exchange Gain/Loss Journal should've been cancelled + exc_je_for_si = self.get_journals_for(si.doctype, si.name) + exc_je_for_pe = self.get_journals_for(pe.doctype, pe.name) + self.assertEqual(exc_je_for_si, []) + self.assertEqual(exc_je_for_pe, []) + + def test_11_advance_against_sales_invoice(self): # Advance Payment adv = self.create_payment_entry(amount=1, source_exc_rate=85).save().submit() adv.reload() - # Invoice in Foreign Currency - si = self.create_sales_invoice(qty=1, rate=1, do_not_submit=True) - si.append( - "advances", - { - "doctype": "Sales Invoice Advance", - "reference_type": adv.doctype, - "reference_name": adv.name, - "advance_amount": 1, - "allocated_amount": 1, - "ref_exchange_rate": 85, - "remarks": "Test", - }, - ) - si = si.save() - si = si.submit() - - adv.reload() - self.assertEqual(si.outstanding_amount, 0) - - # Exchange Gain/Loss Journal should've been created. - exc_je_for_si = self.get_journals_for(si.doctype, si.name) - exc_je_for_adv = self.get_journals_for(adv.doctype, adv.name) - - self.assertNotEqual(exc_je_for_si, []) - self.assertEqual(len(exc_je_for_si), 1) - self.assertEqual(len(exc_je_for_adv), 1) - self.assertEqual(exc_je_for_si, exc_je_for_adv) - - # Cancel Invoice - si.cancel() - - # Exchange Gain/Loss Journal should've been cancelled - exc_je_for_si = self.get_journals_for(si.doctype, si.name) - exc_je_for_adv = self.get_journals_for(adv.doctype, adv.name) - - self.assertEqual(exc_je_for_si, []) - self.assertEqual(exc_je_for_adv, []) - - def test_03_partial_advance_and_payment_for_invoice(self): + # Sales Invoices in different exchange rates + for exc_rate in [75.9, 83.1, 80.01]: + with self.subTest(exc_rate=exc_rate): + si = self.create_sales_invoice(qty=1, conversion_rate=exc_rate, rate=1, do_not_submit=True) + si.append( + "advances", + { + "doctype": "Sales Invoice Advance", + "reference_type": adv.doctype, + "reference_name": adv.name, + "advance_amount": 1, + "allocated_amount": 1, + "ref_exchange_rate": 85, + "remarks": "Test", + }, + ) + si = si.save() + si = si.submit() + + # Outstanding in both currencies should be '0' + adv.reload() + self.assertEqual(si.outstanding_amount, 0) + self.assert_ledger_outstanding(si.doctype, si.name, 0.0, 0.0) + + # Exchange Gain/Loss Journal should've been created. + exc_je_for_si = self.get_journals_for(si.doctype, si.name) + exc_je_for_adv = self.get_journals_for(adv.doctype, adv.name) + self.assertNotEqual(exc_je_for_si, []) + self.assertEqual(len(exc_je_for_si), 1) + self.assertEqual(len(exc_je_for_adv), 1) + self.assertEqual(exc_je_for_si, exc_je_for_adv) + + # Cancel Invoice + si.cancel() + + # Exchange Gain/Loss Journal should've been cancelled + exc_je_for_si = self.get_journals_for(si.doctype, si.name) + exc_je_for_adv = self.get_journals_for(adv.doctype, adv.name) + self.assertEqual(exc_je_for_si, []) + self.assertEqual(exc_je_for_adv, []) + + def test_12_partial_advance_and_payment_for_sales_invoice(self): """ - Invoice with partial advance payment, and a normal payment + Sales invoice with partial advance payment, and a normal payment reconciled """ # Partial Advance adv = self.create_payment_entry(amount=1, source_exc_rate=85).save().submit() adv.reload() - # Invoice in Foreign Currency linked with advance - si = self.create_sales_invoice(qty=2, rate=1, do_not_submit=True) + # sales invoice with advance(partial amount) + rate = 80 + rate_in_account_currency = 1 + si = self.create_sales_invoice( + qty=2, conversion_rate=80, rate=rate_in_account_currency, do_not_submit=True + ) si.append( "advances", { @@ -343,19 +402,20 @@ def test_03_partial_advance_and_payment_for_invoice(self): si = si.save() si = si.submit() + # Outstanding should be there in both currencies si.reload() - self.assertEqual(si.outstanding_amount, 1) + self.assertEqual(si.outstanding_amount, 1) # account currency + self.assert_ledger_outstanding(si.doctype, si.name, 80.0, 1.0) # Exchange Gain/Loss Journal should've been created for the partial advance exc_je_for_si = self.get_journals_for(si.doctype, si.name) exc_je_for_adv = self.get_journals_for(adv.doctype, adv.name) - self.assertNotEqual(exc_je_for_si, []) self.assertEqual(len(exc_je_for_si), 1) self.assertEqual(len(exc_je_for_adv), 1) self.assertEqual(exc_je_for_si, exc_je_for_adv) - # Payment + # Payment for remaining amount pe = self.create_payment_entry(amount=1, source_exc_rate=75).save() pe.append( "references", @@ -363,13 +423,14 @@ def test_03_partial_advance_and_payment_for_invoice(self): ) pe = pe.save().submit() + # Outstanding in both currencies should be '0' si.reload() self.assertEqual(si.outstanding_amount, 0) + self.assert_ledger_outstanding(si.doctype, si.name, 0.0, 0.0) # Exchange Gain/Loss Journal should've been created for the payment exc_je_for_si = self.get_journals_for(si.doctype, si.name) exc_je_for_pe = self.get_journals_for(pe.doctype, pe.name) - self.assertNotEqual(exc_je_for_si, []) # There should be 2 JE's now. One for the advance and one for the payment self.assertEqual(len(exc_je_for_si), 2) @@ -384,21 +445,20 @@ def test_03_partial_advance_and_payment_for_invoice(self): exc_je_for_si = self.get_journals_for(si.doctype, si.name) exc_je_for_pe = self.get_journals_for(pe.doctype, pe.name) exc_je_for_adv = self.get_journals_for(adv.doctype, adv.name) - self.assertEqual(exc_je_for_si, []) self.assertEqual(exc_je_for_pe, []) self.assertEqual(exc_je_for_adv, []) - def test_04_partial_advance_and_payment_for_invoice_with_cancellation(self): + def test_13_partial_advance_and_payment_for_invoice_with_cancellation(self): """ - Invoice with partial advance payment, and a normal payment. Cancel advance and payment. + Invoice with partial advance payment, and a normal payment. Then cancel advance and payment. """ # Partial Advance adv = self.create_payment_entry(amount=1, source_exc_rate=85).save().submit() adv.reload() - # Invoice in Foreign Currency linked with advance - si = self.create_sales_invoice(qty=2, rate=1, do_not_submit=True) + # invoice with advance(partial amount) + si = self.create_sales_invoice(qty=2, conversion_rate=80, rate=1, do_not_submit=True) si.append( "advances", { @@ -414,19 +474,20 @@ def test_04_partial_advance_and_payment_for_invoice_with_cancellation(self): si = si.save() si = si.submit() + # Outstanding should be there in both currencies si.reload() - self.assertEqual(si.outstanding_amount, 1) + self.assertEqual(si.outstanding_amount, 1) # account currency + self.assert_ledger_outstanding(si.doctype, si.name, 80.0, 1.0) # Exchange Gain/Loss Journal should've been created for the partial advance exc_je_for_si = self.get_journals_for(si.doctype, si.name) exc_je_for_adv = self.get_journals_for(adv.doctype, adv.name) - self.assertNotEqual(exc_je_for_si, []) self.assertEqual(len(exc_je_for_si), 1) self.assertEqual(len(exc_je_for_adv), 1) self.assertEqual(exc_je_for_si, exc_je_for_adv) - # Payment + # Payment(remaining amount) pe = self.create_payment_entry(amount=1, source_exc_rate=75).save() pe.append( "references", @@ -434,13 +495,14 @@ def test_04_partial_advance_and_payment_for_invoice_with_cancellation(self): ) pe = pe.save().submit() + # Outstanding should be '0' in both currencies si.reload() self.assertEqual(si.outstanding_amount, 0) + self.assert_ledger_outstanding(si.doctype, si.name, 0.0, 0.0) # Exchange Gain/Loss Journal should've been created for the payment exc_je_for_si = self.get_journals_for(si.doctype, si.name) exc_je_for_pe = self.get_journals_for(pe.doctype, pe.name) - self.assertNotEqual(exc_je_for_si, []) # There should be 2 JE's now. One for the advance and one for the payment self.assertEqual(len(exc_je_for_si), 2) @@ -450,21 +512,22 @@ def test_04_partial_advance_and_payment_for_invoice_with_cancellation(self): adv.reload() adv.cancel() + # Outstanding should be there in both currencies, since advance is cancelled. si.reload() - self.assertEqual(si.outstanding_amount, 1) + self.assertEqual(si.outstanding_amount, 1) # account currency + self.assert_ledger_outstanding(si.doctype, si.name, 80.0, 1.0) exc_je_for_si = self.get_journals_for(si.doctype, si.name) exc_je_for_pe = self.get_journals_for(pe.doctype, pe.name) exc_je_for_adv = self.get_journals_for(adv.doctype, adv.name) - # Exchange Gain/Loss Journal for advance should been cancelled self.assertEqual(len(exc_je_for_si), 1) self.assertEqual(len(exc_je_for_pe), 1) self.assertEqual(exc_je_for_adv, []) - def test_05_same_payment_split_against_invoice(self): + def test_14_same_payment_split_against_invoice(self): # Invoice in Foreign Currency - si = self.create_sales_invoice(qty=2, rate=1) + si = self.create_sales_invoice(qty=2, conversion_rate=80, rate=1) # Payment pe = self.create_payment_entry(amount=2, source_exc_rate=75).save() pe.append( @@ -473,13 +536,14 @@ def test_05_same_payment_split_against_invoice(self): ) pe = pe.save().submit() + # There should be outstanding in both currencies si.reload() self.assertEqual(si.outstanding_amount, 1) + self.assert_ledger_outstanding(si.doctype, si.name, 80.0, 1.0) # Exchange Gain/Loss Journal should've been created. exc_je_for_si = self.get_journals_for(si.doctype, si.name) exc_je_for_pe = self.get_journals_for(pe.doctype, pe.name) - self.assertNotEqual(exc_je_for_si, []) self.assertEqual(len(exc_je_for_si), 1) self.assertEqual(len(exc_je_for_pe), 1) @@ -491,32 +555,35 @@ def test_05_same_payment_split_against_invoice(self): pr.party_type = "Customer" pr.party = self.customer pr.receivable_payable_account = self.debit_usd - pr.get_unreconciled_entries() self.assertEqual(len(pr.invoices), 1) self.assertEqual(len(pr.payments), 1) - - # Test exact payment allocation invoices = [x.as_dict() for x in pr.invoices] payments = [x.as_dict() for x in pr.payments] pr.allocate_entries(frappe._dict({"invoices": invoices, "payments": payments})) - pr.reconcile() self.assertEqual(len(pr.invoices), 0) self.assertEqual(len(pr.payments), 0) + # Exc gain/loss journal should have been creaetd for the reconciled amount exc_je_for_si = self.get_journals_for(si.doctype, si.name) exc_je_for_pe = self.get_journals_for(pe.doctype, pe.name) self.assertEqual(len(exc_je_for_si), 2) self.assertEqual(len(exc_je_for_pe), 2) self.assertEqual(exc_je_for_si, exc_je_for_pe) + # There should be no outstanding + si.reload() + self.assertEqual(si.outstanding_amount, 0) + self.assert_ledger_outstanding(si.doctype, si.name, 0.0, 0.0) + # Cancel Payment pe.reload() pe.cancel() si.reload() self.assertEqual(si.outstanding_amount, 2) + self.assert_ledger_outstanding(si.doctype, si.name, 160.0, 2.0) # Exchange Gain/Loss Journal should've been cancelled exc_je_for_si = self.get_journals_for(si.doctype, si.name) From 8be312b73e155c8f3511e8d798e2562e77545676 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Wed, 12 Jul 2023 06:14:17 +0530 Subject: [PATCH 137/501] refactor: dr/cr logic for journals as payments (cherry picked from commit 056724377206f9bdc3e7ac9894fabb0d93bd3176) --- erpnext/controllers/accounts_controller.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/erpnext/controllers/accounts_controller.py b/erpnext/controllers/accounts_controller.py index c2f723894d4c..a6d7ce43b247 100644 --- a/erpnext/controllers/accounts_controller.py +++ b/erpnext/controllers/accounts_controller.py @@ -986,15 +986,14 @@ def make_exchange_gain_loss_journal(self, args=None) -> None: "Account", party_account, "account_currency" ) - dr_or_cr = "debit" if arg.get("party_type") == "Customer" else "credit" - - # if arg.reference_doctype == "Purchase Invoice": - # dr_or_cr = "debit" if dr_or_cr == "credit" else "credit" + if arg.get("difference_amount") > 0: + dr_or_cr = "debit" if arg.get("party_type") == "Customer" else "credit" + else: + dr_or_cr = "credit" if arg.get("party_type") == "Customer" else "debit" reverse_dr_or_cr = "debit" if dr_or_cr == "credit" else "credit" gain_loss_account = arg.get("difference_account") - if not gain_loss_account: frappe.throw( _("Please set default Exchange Gain/Loss Account in Company {}").format( From 077d98e0fa20574ca9787f237cd3706db9609d4c Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Wed, 12 Jul 2023 06:46:59 +0530 Subject: [PATCH 138/501] refactor: unit tests for journals (cherry picked from commit 5695d6a5a62e634536c51a22e045eb8281a29d9d) --- .../tests/test_accounts_controller.py | 83 +++++++++++++++++-- 1 file changed, 78 insertions(+), 5 deletions(-) diff --git a/erpnext/controllers/tests/test_accounts_controller.py b/erpnext/controllers/tests/test_accounts_controller.py index fc30c4b8cde8..9e857f04c31b 100644 --- a/erpnext/controllers/tests/test_accounts_controller.py +++ b/erpnext/controllers/tests/test_accounts_controller.py @@ -216,12 +216,21 @@ def create_payment_reconciliation(self): return pr def create_journal_entry( - self, acc1=None, acc2=None, amount=0, posting_date=None, cost_center=None + self, + acc1=None, + acc1_exc_rate=None, + acc2_exc_rate=None, + acc2=None, + acc1_amount=0, + acc2_amount=0, + posting_date=None, + cost_center=None, ): je = frappe.new_doc("Journal Entry") je.posting_date = posting_date or nowdate() je.company = self.company je.user_remark = "test" + je.multi_currency = True if not cost_center: cost_center = self.cost_center je.set( @@ -229,15 +238,21 @@ def create_journal_entry( [ { "account": acc1, + "exchange_rate": acc1_exc_rate or 1, "cost_center": cost_center, - "debit_in_account_currency": amount if amount > 0 else 0, - "credit_in_account_currency": abs(amount) if amount < 0 else 0, + "debit_in_account_currency": acc1_amount if acc1_amount > 0 else 0, + "credit_in_account_currency": abs(acc1_amount) if acc1_amount < 0 else 0, + "debit": acc1_amount * acc1_exc_rate if acc1_amount > 0 else 0, + "credit": abs(acc1_amount * acc1_exc_rate) if acc1_amount < 0 else 0, }, { "account": acc2, + "exchange_rate": acc2_exc_rate or 1, "cost_center": cost_center, - "credit_in_account_currency": amount if amount > 0 else 0, - "debit_in_account_currency": abs(amount) if amount < 0 else 0, + "credit_in_account_currency": acc2_amount if acc2_amount > 0 else 0, + "debit_in_account_currency": abs(acc2_amount) if acc2_amount < 0 else 0, + "credit": acc2_amount * acc2_exc_rate if acc2_amount > 0 else 0, + "debit": abs(acc2_amount * acc2_exc_rate) if acc2_amount < 0 else 0, }, ], ) @@ -590,3 +605,61 @@ def test_14_same_payment_split_against_invoice(self): exc_je_for_pe = self.get_journals_for(pe.doctype, pe.name) self.assertEqual(exc_je_for_si, []) self.assertEqual(exc_je_for_pe, []) + + def test_21_journal_against_sales_invoice(self): + # Invoice in Foreign Currency + si = self.create_sales_invoice(qty=1, conversion_rate=80, rate=1) + # Payment + je = self.create_journal_entry( + acc1=self.debit_usd, + acc1_exc_rate=75, + acc2=self.cash, + acc1_amount=-1, + acc2_amount=-75, + acc2_exc_rate=1, + ) + je.accounts[0].party_type = "Customer" + je.accounts[0].party = self.customer + je = je.save().submit() + + # Reconcile the remaining amount + pr = self.create_payment_reconciliation() + # pr.receivable_payable_account = self.debit_usd + pr.get_unreconciled_entries() + self.assertEqual(len(pr.invoices), 1) + self.assertEqual(len(pr.payments), 1) + invoices = [x.as_dict() for x in pr.invoices] + payments = [x.as_dict() for x in pr.payments] + pr.allocate_entries(frappe._dict({"invoices": invoices, "payments": payments})) + pr.reconcile() + self.assertEqual(len(pr.invoices), 0) + self.assertEqual(len(pr.payments), 0) + + # There should be no outstanding in both currencies + si.reload() + self.assertEqual(si.outstanding_amount, 0) + self.assert_ledger_outstanding(si.doctype, si.name, 0.0, 0.0) + + # Exchange Gain/Loss Journal should've been created. + exc_je_for_si = self.get_journals_for(si.doctype, si.name) + exc_je_for_je = self.get_journals_for(je.doctype, je.name) + self.assertNotEqual(exc_je_for_si, []) + self.assertEqual( + len(exc_je_for_si), 2 + ) # payment also has reference. so, there are 2 journals referencing invoice + self.assertEqual(len(exc_je_for_je), 1) + self.assertIn(exc_je_for_je[0], exc_je_for_si) + + # Cancel Payment + je.reload() + je.cancel() + + si.reload() + self.assertEqual(si.outstanding_amount, 1) + self.assert_ledger_outstanding(si.doctype, si.name, 80.0, 1.0) + + # Exchange Gain/Loss Journal should've been cancelled + exc_je_for_si = self.get_journals_for(si.doctype, si.name) + exc_je_for_je = self.get_journals_for(je.doctype, je.name) + self.assertEqual(exc_je_for_si, []) + self.assertEqual(exc_je_for_je, []) From 513721c33836736ddc48a213b787e0c4c7ae9706 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Fri, 14 Jul 2023 16:51:42 +0530 Subject: [PATCH 139/501] refactor: handle diff amount in various names (cherry picked from commit f4a65cccc48bd15fd732973030451c93629bc84b) --- erpnext/controllers/accounts_controller.py | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/erpnext/controllers/accounts_controller.py b/erpnext/controllers/accounts_controller.py index a6d7ce43b247..c5bf808a2f1b 100644 --- a/erpnext/controllers/accounts_controller.py +++ b/erpnext/controllers/accounts_controller.py @@ -974,7 +974,10 @@ def make_exchange_gain_loss_journal(self, args=None) -> None: # These are generated by Sales/Purchase Invoice during reconciliation and advance allocation. if args: for arg in args: - if arg.get("difference_amount", 0) != 0 and arg.get("difference_account"): + # Advance section uses `exchange_gain_loss` and reconciliation uses `difference_amount` + if ( + arg.get("difference_amount", 0) != 0 or arg.get("exchange_gain_loss", 0) != 0 + ) and arg.get("difference_account"): journal_entry = frappe.new_doc("Journal Entry") journal_entry.voucher_type = "Exchange Gain Or Loss" journal_entry.company = self.company @@ -986,7 +989,8 @@ def make_exchange_gain_loss_journal(self, args=None) -> None: "Account", party_account, "account_currency" ) - if arg.get("difference_amount") > 0: + difference_amount = arg.get("difference_amount") or arg.get("exchange_gain_loss") + if difference_amount > 0: dr_or_cr = "debit" if arg.get("party_type") == "Customer" else "credit" else: dr_or_cr = "credit" if arg.get("party_type") == "Customer" else "debit" @@ -1018,7 +1022,7 @@ def make_exchange_gain_loss_journal(self, args=None) -> None: "reference_type": arg.get("against_voucher_type"), "reference_name": arg.get("against_voucher"), "reference_detail_no": arg.get("idx"), - dr_or_cr: abs(arg.difference_amount), + dr_or_cr: abs(difference_amount), dr_or_cr + "_in_account_currency": 0, } ) @@ -1037,7 +1041,7 @@ def make_exchange_gain_loss_journal(self, args=None) -> None: "reference_type": self.doctype, "reference_name": self.name, "reference_detail_no": arg.idx, - reverse_dr_or_cr: abs(arg.get("difference_amount")), + reverse_dr_or_cr: abs(difference_amount), reverse_dr_or_cr + "_in_account_currency": 0, } ) From 4094fbc3e59540cdee2b33771a9914e4139b6190 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Sun, 16 Jul 2023 21:29:19 +0530 Subject: [PATCH 140/501] test: journals against sales invoice (cherry picked from commit f3363e813a353363696169602bae5b0a36ae0376) --- .../tests/test_accounts_controller.py | 264 +++++++++++++++++- 1 file changed, 263 insertions(+), 1 deletion(-) diff --git a/erpnext/controllers/tests/test_accounts_controller.py b/erpnext/controllers/tests/test_accounts_controller.py index 9e857f04c31b..9a7326ea291c 100644 --- a/erpnext/controllers/tests/test_accounts_controller.py +++ b/erpnext/controllers/tests/test_accounts_controller.py @@ -606,7 +606,7 @@ def test_14_same_payment_split_against_invoice(self): self.assertEqual(exc_je_for_si, []) self.assertEqual(exc_je_for_pe, []) - def test_21_journal_against_sales_invoice(self): + def test_20_journal_against_sales_invoice(self): # Invoice in Foreign Currency si = self.create_sales_invoice(qty=1, conversion_rate=80, rate=1) # Payment @@ -663,3 +663,265 @@ def test_21_journal_against_sales_invoice(self): exc_je_for_je = self.get_journals_for(je.doctype, je.name) self.assertEqual(exc_je_for_si, []) self.assertEqual(exc_je_for_je, []) + + def test_21_advance_journal_against_sales_invoice(self): + # Advance Payment + adv_exc_rate = 80 + adv = self.create_journal_entry( + acc1=self.debit_usd, + acc1_exc_rate=adv_exc_rate, + acc2=self.cash, + acc1_amount=-1, + acc2_amount=adv_exc_rate * -1, + acc2_exc_rate=1, + ) + adv.accounts[0].party_type = "Customer" + adv.accounts[0].party = self.customer + adv.accounts[0].is_advance = "Yes" + adv = adv.save().submit() + adv.reload() + + # Sales Invoices in different exchange rates + for exc_rate in [75.9, 83.1, 80.01]: + with self.subTest(exc_rate=exc_rate): + si = self.create_sales_invoice(qty=1, conversion_rate=exc_rate, rate=1, do_not_submit=True) + si.append( + "advances", + { + "doctype": "Sales Invoice Advance", + "reference_type": adv.doctype, + "reference_name": adv.name, + "reference_row": adv.accounts[0].name, + "advance_amount": 1, + "allocated_amount": 1, + "ref_exchange_rate": adv_exc_rate, + "remarks": "Test", + }, + ) + si = si.save() + si = si.submit() + + # Outstanding in both currencies should be '0' + adv.reload() + self.assertEqual(si.outstanding_amount, 0) + self.assert_ledger_outstanding(si.doctype, si.name, 0.0, 0.0) + + # Exchange Gain/Loss Journal should've been created. + exc_je_for_si = [x for x in self.get_journals_for(si.doctype, si.name) if x.parent != adv.name] + exc_je_for_adv = self.get_journals_for(adv.doctype, adv.name) + self.assertNotEqual(exc_je_for_si, []) + self.assertEqual(len(exc_je_for_si), 1) + self.assertEqual(len(exc_je_for_adv), 1) + self.assertEqual(exc_je_for_si, exc_je_for_adv) + + # Cancel Invoice + si.cancel() + + # Exchange Gain/Loss Journal should've been cancelled + exc_je_for_si = self.get_journals_for(si.doctype, si.name) + exc_je_for_adv = self.get_journals_for(adv.doctype, adv.name) + self.assertEqual(exc_je_for_si, []) + self.assertEqual(exc_je_for_adv, []) + + def test_22_partial_advance_and_payment_for_invoice_with_cancellation(self): + """ + Invoice with partial advance payment as Journal, and a normal payment. Then cancel advance and payment. + """ + # Partial Advance + adv_exc_rate = 75 + adv = self.create_journal_entry( + acc1=self.debit_usd, + acc1_exc_rate=adv_exc_rate, + acc2=self.cash, + acc1_amount=-1, + acc2_amount=adv_exc_rate * -1, + acc2_exc_rate=1, + ) + adv.accounts[0].party_type = "Customer" + adv.accounts[0].party = self.customer + adv.accounts[0].is_advance = "Yes" + adv = adv.save().submit() + adv.reload() + + # invoice with advance(partial amount) + si = self.create_sales_invoice(qty=3, conversion_rate=80, rate=1, do_not_submit=True) + si.append( + "advances", + { + "doctype": "Sales Invoice Advance", + "reference_type": adv.doctype, + "reference_name": adv.name, + "reference_row": adv.accounts[0].name, + "advance_amount": 1, + "allocated_amount": 1, + "ref_exchange_rate": adv_exc_rate, + "remarks": "Test", + }, + ) + si = si.save() + si = si.submit() + + # Outstanding should be there in both currencies + si.reload() + self.assertEqual(si.outstanding_amount, 2) # account currency + self.assert_ledger_outstanding(si.doctype, si.name, 160.0, 2.0) + + # Exchange Gain/Loss Journal should've been created for the partial advance + exc_je_for_si = [x for x in self.get_journals_for(si.doctype, si.name) if x.parent != adv.name] + exc_je_for_adv = self.get_journals_for(adv.doctype, adv.name) + self.assertNotEqual(exc_je_for_si, []) + self.assertEqual(len(exc_je_for_si), 1) + self.assertEqual(len(exc_je_for_adv), 1) + self.assertEqual(exc_je_for_si, exc_je_for_adv) + + # Payment + adv2_exc_rate = 83 + pay = self.create_journal_entry( + acc1=self.debit_usd, + acc1_exc_rate=adv2_exc_rate, + acc2=self.cash, + acc1_amount=-2, + acc2_amount=adv2_exc_rate * -2, + acc2_exc_rate=1, + ) + pay.accounts[0].party_type = "Customer" + pay.accounts[0].party = self.customer + pay.accounts[0].is_advance = "Yes" + pay = pay.save().submit() + pay.reload() + + # Reconcile the remaining amount + pr = self.create_payment_reconciliation() + # pr.receivable_payable_account = self.debit_usd + pr.get_unreconciled_entries() + self.assertEqual(len(pr.invoices), 1) + self.assertEqual(len(pr.payments), 1) + invoices = [x.as_dict() for x in pr.invoices] + payments = [x.as_dict() for x in pr.payments] + pr.allocate_entries(frappe._dict({"invoices": invoices, "payments": payments})) + pr.reconcile() + self.assertEqual(len(pr.invoices), 0) + self.assertEqual(len(pr.payments), 0) + + # Outstanding should be '0' in both currencies + si.reload() + self.assertEqual(si.outstanding_amount, 0) + self.assert_ledger_outstanding(si.doctype, si.name, 0.0, 0.0) + + # Exchange Gain/Loss Journal should've been created for the payment + exc_je_for_si = [ + x + for x in self.get_journals_for(si.doctype, si.name) + if x.parent != adv.name and x.parent != pay.name + ] + exc_je_for_pe = self.get_journals_for(pay.doctype, pay.name) + self.assertNotEqual(exc_je_for_si, []) + # There should be 2 JE's now. One for the advance and one for the payment + self.assertEqual(len(exc_je_for_si), 2) + self.assertEqual(len(exc_je_for_pe), 1) + self.assertEqual(exc_je_for_si, exc_je_for_pe + exc_je_for_adv) + + adv.reload() + adv.cancel() + + # Outstanding should be there in both currencies, since advance is cancelled. + si.reload() + self.assertEqual(si.outstanding_amount, 1) # account currency + self.assert_ledger_outstanding(si.doctype, si.name, 80.0, 1.0) + + exc_je_for_si = [ + x + for x in self.get_journals_for(si.doctype, si.name) + if x.parent != adv.name and x.parent != pay.name + ] + exc_je_for_pe = self.get_journals_for(pay.doctype, pay.name) + exc_je_for_adv = self.get_journals_for(adv.doctype, adv.name) + # Exchange Gain/Loss Journal for advance should been cancelled + self.assertEqual(len(exc_je_for_si), 1) + self.assertEqual(len(exc_je_for_pe), 1) + self.assertEqual(exc_je_for_adv, []) + + def test_23_same_journal_split_against_single_invoice(self): + # Invoice in Foreign Currency + si = self.create_sales_invoice(qty=2, conversion_rate=80, rate=1) + # Payment + je = self.create_journal_entry( + acc1=self.debit_usd, + acc1_exc_rate=75, + acc2=self.cash, + acc1_amount=-2, + acc2_amount=-150, + acc2_exc_rate=1, + ) + je.accounts[0].party_type = "Customer" + je.accounts[0].party = self.customer + je = je.save().submit() + + # Reconcile the first half + pr = self.create_payment_reconciliation() + pr.get_unreconciled_entries() + self.assertEqual(len(pr.invoices), 1) + self.assertEqual(len(pr.payments), 1) + invoices = [x.as_dict() for x in pr.invoices] + payments = [x.as_dict() for x in pr.payments] + pr.allocate_entries(frappe._dict({"invoices": invoices, "payments": payments})) + difference_amount = pr.calculate_difference_on_allocation_change( + [x.as_dict() for x in pr.payments], [x.as_dict() for x in pr.invoices], 1 + ) + pr.allocation[0].allocated_amount = 1 + pr.allocation[0].difference_amount = difference_amount + pr.reconcile() + self.assertEqual(len(pr.invoices), 1) + self.assertEqual(len(pr.payments), 1) + + # There should be outstanding in both currencies + si.reload() + self.assertEqual(si.outstanding_amount, 1) + self.assert_ledger_outstanding(si.doctype, si.name, 80.0, 1.0) + + # Exchange Gain/Loss Journal should've been created. + exc_je_for_si = [x for x in self.get_journals_for(si.doctype, si.name) if x.parent != je.name] + exc_je_for_je = self.get_journals_for(je.doctype, je.name) + self.assertNotEqual(exc_je_for_si, []) + self.assertEqual(len(exc_je_for_si), 1) + self.assertEqual(len(exc_je_for_je), 1) + self.assertIn(exc_je_for_je[0], exc_je_for_si) + + # reconcile remaining half + pr.get_unreconciled_entries() + self.assertEqual(len(pr.invoices), 1) + self.assertEqual(len(pr.payments), 1) + invoices = [x.as_dict() for x in pr.invoices] + payments = [x.as_dict() for x in pr.payments] + pr.allocate_entries(frappe._dict({"invoices": invoices, "payments": payments})) + pr.allocation[0].allocated_amount = 1 + pr.allocation[0].difference_amount = difference_amount + pr.reconcile() + self.assertEqual(len(pr.invoices), 0) + self.assertEqual(len(pr.payments), 0) + + # Exchange Gain/Loss Journal should've been created. + exc_je_for_si = [x for x in self.get_journals_for(si.doctype, si.name) if x.parent != je.name] + exc_je_for_je = self.get_journals_for(je.doctype, je.name) + self.assertNotEqual(exc_je_for_si, []) + self.assertEqual(len(exc_je_for_si), 2) + self.assertEqual(len(exc_je_for_je), 2) + self.assertIn(exc_je_for_je[0], exc_je_for_si) + + si.reload() + self.assertEqual(si.outstanding_amount, 0) + self.assert_ledger_outstanding(si.doctype, si.name, 0.0, 0.0) + + # Cancel Payment + je.reload() + je.cancel() + + si.reload() + self.assertEqual(si.outstanding_amount, 2) + self.assert_ledger_outstanding(si.doctype, si.name, 160.0, 2.0) + + # Exchange Gain/Loss Journal should've been cancelled + exc_je_for_si = self.get_journals_for(si.doctype, si.name) + exc_je_for_je = self.get_journals_for(je.doctype, je.name) + self.assertEqual(exc_je_for_si, []) + self.assertEqual(exc_je_for_je, []) From 395f87a0f85a32682e30ca492b78387a009e2474 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Mon, 17 Jul 2023 12:29:42 +0530 Subject: [PATCH 141/501] chore(test): fix broken unit test (cherry picked from commit 70dd9d0671e1d77d50c814885c6a6f59508c4f62) # Conflicts: # erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py --- .../accounts/doctype/sales_invoice/test_sales_invoice.py | 7 +++++++ erpnext/controllers/tests/test_accounts_controller.py | 4 +--- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py index 277e584aeaf6..2e231608588e 100644 --- a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py +++ b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py @@ -3213,9 +3213,11 @@ def test_sales_invoice_with_disabled_account(self): account.disabled = 0 account.save() + @change_settings("Accounts Settings", {"unlink_payment_on_cancel_of_invoice": 1}) def test_gain_loss_with_advance_entry(self): from erpnext.accounts.doctype.journal_entry.test_journal_entry import make_journal_entry +<<<<<<< HEAD unlink_enabled = frappe.db.get_value( "Accounts Settings", "Accounts Settings", "unlink_payment_on_cancel_of_invoice" ) @@ -3224,6 +3226,8 @@ def test_gain_loss_with_advance_entry(self): "Accounts Settings", "Accounts Settings", "unlink_payment_on_cancel_of_invoice", 1 ) +======= +>>>>>>> 70dd9d0671 (chore(test): fix broken unit test) jv = make_journal_entry("_Test Receivable USD - _TC", "_Test Bank - _TC", -7000, save=False) jv.accounts[0].exchange_rate = 70 @@ -3265,10 +3269,13 @@ def test_gain_loss_with_advance_entry(self): check_gl_entries(self, si.name, expected_gle, nowdate()) +<<<<<<< HEAD frappe.db.set_value( "Accounts Settings", "Accounts Settings", "unlink_payment_on_cancel_of_invoice", unlink_enabled ) +======= +>>>>>>> 70dd9d0671 (chore(test): fix broken unit test) def test_batch_expiry_for_sales_invoice_return(self): from erpnext.controllers.sales_and_purchase_return import make_return_doc from erpnext.stock.doctype.item.test_item import make_item diff --git a/erpnext/controllers/tests/test_accounts_controller.py b/erpnext/controllers/tests/test_accounts_controller.py index 9a7326ea291c..eefe202e476a 100644 --- a/erpnext/controllers/tests/test_accounts_controller.py +++ b/erpnext/controllers/tests/test_accounts_controller.py @@ -37,6 +37,7 @@ def make_supplier(supplier_name, currency=None): supplier = frappe.new_doc("Supplier") supplier.supplier_name = supplier_name supplier.supplier_type = "Individual" + supplier.supplier_group = "All Supplier Groups" if currency: supplier.default_currency = currency @@ -55,9 +56,6 @@ class TestAccountsController(unittest.TestCase): 10 series - Sales Invoice against Payment Entries 20 series - Sales Invoice against Journals 30 series - Sales Invoice against Credit Notes - 40 series - Purchase Invoice against Payment Entries - 50 series - Purchase Invoice against Journals - 60 series - Purchase Invoice against Debit Notes """ def setUp(self): From d9219dc7d21a04d50a041c9d1eb6d813afb9b4e7 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Mon, 24 Jul 2023 20:41:05 +0530 Subject: [PATCH 142/501] chore(test): fix broken test case (cherry picked from commit 37895a361cdf7be4704f376eb6ec749af0ab3c90) --- erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py index 2e231608588e..eed33693e9b0 100644 --- a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py +++ b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py @@ -3213,7 +3213,7 @@ def test_sales_invoice_with_disabled_account(self): account.disabled = 0 account.save() - @change_settings("Accounts Settings", {"unlink_payment_on_cancel_of_invoice": 1}) + @change_settings("Accounts Settings", {"unlink_payment_on_cancellation_of_invoice": 1}) def test_gain_loss_with_advance_entry(self): from erpnext.accounts.doctype.journal_entry.test_journal_entry import make_journal_entry From a9faa927965584977ca252c4107dacba09d212a8 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Tue, 25 Jul 2023 10:30:08 +0530 Subject: [PATCH 143/501] chore: type info (cherry picked from commit 6628632fbb15ddcc80f5af201d15976337141fc6) --- erpnext/controllers/accounts_controller.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/controllers/accounts_controller.py b/erpnext/controllers/accounts_controller.py index c5bf808a2f1b..af4c47b56c71 100644 --- a/erpnext/controllers/accounts_controller.py +++ b/erpnext/controllers/accounts_controller.py @@ -962,7 +962,7 @@ def set_advance_gain_or_loss(self): d.exchange_gain_loss = difference - def make_exchange_gain_loss_journal(self, args=None) -> None: + def make_exchange_gain_loss_journal(self, args: dict = None) -> None: """ Make Exchange Gain/Loss journal for Invoices and Payments """ From 57af6d9c21fb8b98d0db29d2cf7e47f1b07122ca Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Tue, 25 Jul 2023 10:30:49 +0530 Subject: [PATCH 144/501] refactor: cr/dr note will be on single exchange rate (cherry picked from commit c87332d5da638c43ff6d0560bf3c26dde81e21cf) # Conflicts: # erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py --- .../doctype/journal_entry/journal_entry.py | 29 +++++++++++-------- .../payment_reconciliation.py | 16 ++++++++-- 2 files changed, 30 insertions(+), 15 deletions(-) diff --git a/erpnext/accounts/doctype/journal_entry/journal_entry.py b/erpnext/accounts/doctype/journal_entry/journal_entry.py index 743496262487..73f0a4c2d459 100644 --- a/erpnext/accounts/doctype/journal_entry/journal_entry.py +++ b/erpnext/accounts/doctype/journal_entry/journal_entry.py @@ -756,18 +756,23 @@ def set_exchange_rate(self): ) ): - # Modified to include the posting date for which to retreive the exchange rate - d.exchange_rate = get_exchange_rate( - self.posting_date, - d.account, - d.account_currency, - self.company, - d.reference_type, - d.reference_name, - d.debit, - d.credit, - d.exchange_rate, - ) + ignore_exchange_rate = False + if self.get("flags") and self.flags.get("ignore_exchange_rate"): + ignore_exchange_rate = True + + if not ignore_exchange_rate: + # Modified to include the posting date for which to retreive the exchange rate + d.exchange_rate = get_exchange_rate( + self.posting_date, + d.account, + d.account_currency, + self.company, + d.reference_type, + d.reference_name, + d.debit, + d.credit, + d.exchange_rate, + ) if not d.exchange_rate: frappe.throw(_("Row {0}: Exchange Rate is mandatory").format(d.idx)) diff --git a/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py b/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py index 1f3d4826adc5..e4e5aeb1593e 100644 --- a/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py +++ b/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py @@ -634,7 +634,11 @@ def get_difference_row(inv): "reference_type": inv.against_voucher_type, "reference_name": inv.against_voucher, "cost_center": erpnext.get_default_cost_center(company), +<<<<<<< HEAD "user_remark": f"{fmt_money(flt(inv.allocated_amount), currency=company_currency)} against {inv.against_voucher}", +======= + "exchange_rate": inv.exchange_rate, +>>>>>>> c87332d5da (refactor: cr/dr note will be on single exchange rate) }, { "account": inv.account, @@ -648,17 +652,23 @@ def get_difference_row(inv): "reference_type": inv.voucher_type, "reference_name": inv.voucher_no, "cost_center": erpnext.get_default_cost_center(company), +<<<<<<< HEAD "user_remark": f"{fmt_money(flt(inv.allocated_amount), currency=company_currency)} from {inv.voucher_no}", +======= + "exchange_rate": inv.exchange_rate, +>>>>>>> c87332d5da (refactor: cr/dr note will be on single exchange rate) }, ], } ) - if difference_entry := get_difference_row(inv): - jv.append("accounts", difference_entry) - jv.flags.ignore_mandatory = True +<<<<<<< HEAD jv.remark = None jv.flags.skip_remarks_creation = True jv.is_system_generated = True +======= + jv.flags.ignore_exchange_rate = True +>>>>>>> c87332d5da (refactor: cr/dr note will be on single exchange rate) jv.submit() + jv.make_exchange_gain_loss_journal(args=[inv]) From 1999132c28acb9df46e6ec7c889ba8fe3855f6e1 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Tue, 25 Jul 2023 10:51:58 +0530 Subject: [PATCH 145/501] refactor: split make_exchage_gain_loss_journal into smaller function (cherry picked from commit c0b3b069b587cff11969112b01fff08c8df7adf0) --- erpnext/controllers/accounts_controller.py | 219 ++++++++++----------- 1 file changed, 103 insertions(+), 116 deletions(-) diff --git a/erpnext/controllers/accounts_controller.py b/erpnext/controllers/accounts_controller.py index af4c47b56c71..3f82312ef74e 100644 --- a/erpnext/controllers/accounts_controller.py +++ b/erpnext/controllers/accounts_controller.py @@ -962,6 +962,78 @@ def set_advance_gain_or_loss(self): d.exchange_gain_loss = difference + def create_gain_loss_journal( + self, + party_type, + party, + party_account, + gain_loss_account, + exc_gain_loss, + dr_or_cr, + reverse_dr_or_cr, + ref1_dt, + ref1_dn, + ref1_detail_no, + ref2_dt, + ref2_dn, + ref2_detail_no, + ) -> str: + journal_entry = frappe.new_doc("Journal Entry") + journal_entry.voucher_type = "Exchange Gain Or Loss" + journal_entry.company = self.company + journal_entry.posting_date = nowdate() + journal_entry.multi_currency = 1 + + party_account_currency = frappe.get_cached_value("Account", party_account, "account_currency") + + if not gain_loss_account: + frappe.throw( + _("Please set default Exchange Gain/Loss Account in Company {}").format(self.get("company")) + ) + gain_loss_account_currency = get_account_currency(gain_loss_account) + company_currency = frappe.get_cached_value("Company", self.company, "default_currency") + + if gain_loss_account_currency != self.company_currency: + frappe.throw(_("Currency for {0} must be {1}").format(gain_loss_account, company_currency)) + + journal_account = frappe._dict( + { + "account": party_account, + "party_type": party_type, + "party": party, + "account_currency": party_account_currency, + "exchange_rate": 0, + "cost_center": erpnext.get_default_cost_center(self.company), + "reference_type": ref1_dt, + "reference_name": ref1_dn, + "reference_detail_no": ref1_detail_no, + dr_or_cr: abs(exc_gain_loss), + dr_or_cr + "_in_account_currency": 0, + } + ) + + journal_entry.append("accounts", journal_account) + + journal_account = frappe._dict( + { + "account": gain_loss_account, + "account_currency": gain_loss_account_currency, + "exchange_rate": 1, + "cost_center": erpnext.get_default_cost_center(self.company), + "reference_type": ref2_dt, + "reference_name": ref2_dn, + "reference_detail_no": ref2_detail_no, + reverse_dr_or_cr + "_in_account_currency": 0, + reverse_dr_or_cr: abs(exc_gain_loss), + } + ) + + journal_entry.append("accounts", journal_account) + + journal_entry.save() + journal_entry.submit() + return journal_entry.name + def make_exchange_gain_loss_journal(self, args: dict = None) -> None: """ Make Exchange Gain/Loss journal for Invoices and Payments @@ -972,23 +1044,16 @@ def make_exchange_gain_loss_journal(self, args: dict = None) -> None: if self.get("doctype") == "Journal Entry": # 'args' is populated with exchange gain/loss account and the amount to be booked. # These are generated by Sales/Purchase Invoice during reconciliation and advance allocation. + # and below logic is only for such scenarios if args: for arg in args: # Advance section uses `exchange_gain_loss` and reconciliation uses `difference_amount` if ( arg.get("difference_amount", 0) != 0 or arg.get("exchange_gain_loss", 0) != 0 ) and arg.get("difference_account"): - journal_entry = frappe.new_doc("Journal Entry") - journal_entry.voucher_type = "Exchange Gain Or Loss" - journal_entry.company = self.company - journal_entry.posting_date = nowdate() - journal_entry.multi_currency = 1 party_account = arg.get("account") - party_account_currency = frappe.get_cached_value( - "Account", party_account, "account_currency" - ) - + gain_loss_account = arg.get("difference_account") difference_amount = arg.get("difference_amount") or arg.get("exchange_gain_loss") if difference_amount > 0: dr_or_cr = "debit" if arg.get("party_type") == "Customer" else "credit" @@ -997,60 +1062,22 @@ def make_exchange_gain_loss_journal(self, args: dict = None) -> None: reverse_dr_or_cr = "debit" if dr_or_cr == "credit" else "credit" - gain_loss_account = arg.get("difference_account") - if not gain_loss_account: - frappe.throw( - _("Please set default Exchange Gain/Loss Account in Company {}").format( - self.get("company") - ) - ) - - gain_loss_account_currency = get_account_currency(gain_loss_account) - if gain_loss_account_currency != self.company_currency: - frappe.throw( - _("Currency for {0} must be {1}").format(gain_loss_account, self.company_currency) - ) - - journal_account = frappe._dict( - { - "account": party_account, - "party_type": arg.get("party_type"), - "party": arg.get("party"), - "account_currency": party_account_currency, - "exchange_rate": 0, - "cost_center": erpnext.get_default_cost_center(self.company), - "reference_type": arg.get("against_voucher_type"), - "reference_name": arg.get("against_voucher"), - "reference_detail_no": arg.get("idx"), - dr_or_cr: abs(difference_amount), - dr_or_cr + "_in_account_currency": 0, - } + self.create_gain_loss_journal( + arg.get("party_type"), + arg.get("party"), + party_account, + gain_loss_account, + difference_amount, + dr_or_cr, + reverse_dr_or_cr, + arg.get("against_voucher_type"), + arg.get("against_voucher"), + arg.get("idx"), + self.doctype, + self.name, + arg.get("idx"), ) - journal_entry.append("accounts", journal_account) - - journal_account = frappe._dict( - { - "account": gain_loss_account, - "account_currency": gain_loss_account_currency, - "exchange_rate": 1, - "cost_center": erpnext.get_default_cost_center(self.company), - # TODO: figure out a way to pass reference - # TODO: add reference_detail_no field in payment ledger - # throws 'Journal Entry doesn't have {account} or doesn't have matched account' - "reference_type": self.doctype, - "reference_name": self.name, - "reference_detail_no": arg.idx, - reverse_dr_or_cr: abs(difference_amount), - reverse_dr_or_cr + "_in_account_currency": 0, - } - ) - - journal_entry.append("accounts", journal_account) - - journal_entry.save() - journal_entry.submit() - if self.get("doctype") == "Payment Entry": # For Payment Entry, exchange_gain_loss field in the `references` table is the trigger for journal creation gain_loss_to_book = [x for x in self.references if x.exchange_gain_loss != 0] @@ -1087,23 +1114,15 @@ def make_exchange_gain_loss_journal(self, args: dict = None) -> None: ) for d in gain_loss_to_book: + # Filter out References for which Gain/Loss is already booked if d.exchange_gain_loss and ( (d.reference_doctype, d.reference_name, str(d.idx)) not in booked ): - journal_entry = frappe.new_doc("Journal Entry") - journal_entry.voucher_type = "Exchange Gain Or Loss" - journal_entry.company = self.company - journal_entry.posting_date = nowdate() - journal_entry.multi_currency = 1 - if self.payment_type == "Receive": party_account = self.paid_from elif self.payment_type == "Pay": party_account = self.paid_to - party_account_currency = frappe.get_cached_value( - "Account", party_account, "account_currency" - ) dr_or_cr = "debit" if d.exchange_gain_loss > 0 else "credit" if d.reference_doctype == "Purchase Invoice": @@ -1114,54 +1133,22 @@ def make_exchange_gain_loss_journal(self, args: dict = None) -> None: gain_loss_account = frappe.get_cached_value( "Company", self.company, "exchange_gain_loss_account" ) - if not gain_loss_account: - frappe.throw( - _("Please set default Exchange Gain/Loss Account in Company {}").format( - self.get("company") - ) - ) - gain_loss_account_currency = get_account_currency(gain_loss_account) - if gain_loss_account_currency != self.company_currency: - frappe.throw( - _("Currency for {0} must be {1}").format(gain_loss_account, self.company_currency) - ) - - journal_account = frappe._dict( - { - "account": party_account, - "party_type": self.party_type, - "party": self.party, - "account_currency": party_account_currency, - "exchange_rate": 0, - "cost_center": erpnext.get_default_cost_center(self.company), - "reference_type": d.reference_doctype, - "reference_name": d.reference_name, - "reference_detail_no": d.idx, - dr_or_cr: abs(d.exchange_gain_loss), - dr_or_cr + "_in_account_currency": 0, - } - ) - journal_entry.append("accounts", journal_account) - - journal_account = frappe._dict( - { - "account": gain_loss_account, - "account_currency": gain_loss_account_currency, - "exchange_rate": 1, - "cost_center": erpnext.get_default_cost_center(self.company), - "reference_type": self.doctype, - "reference_name": self.name, - "reference_detail_no": d.idx, - reverse_dr_or_cr + "_in_account_currency": 0, - reverse_dr_or_cr: abs(d.exchange_gain_loss), - } + self.create_gain_loss_journal( + self.party_type, + self.party, + party_account, + gain_loss_account, + d.exchange_gain_loss, + dr_or_cr, + reverse_dr_or_cr, + d.reference_doctype, + d.reference_name, + d.idx, + self.doctype, + self.name, + d.idx, ) - - journal_entry.append("accounts", journal_account) - - journal_entry.save() - journal_entry.submit() # frappe.throw("stopping...") def make_precision_loss_gl_entry(self, gl_entries): From 22dbe52586c750b9638fe953c5ef899f6250c7f3 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Wed, 26 Jul 2023 16:19:38 +0530 Subject: [PATCH 146/501] refactor: convert class method to standalone function (cherry picked from commit 1ea1bfebc4a2407961d93a6d0c4c6c9f43202689) --- erpnext/accounts/utils.py | 71 ++++++++++++++++++ erpnext/controllers/accounts_controller.py | 85 +++------------------- 2 files changed, 81 insertions(+), 75 deletions(-) diff --git a/erpnext/accounts/utils.py b/erpnext/accounts/utils.py index 9da0d7d3399f..d8235cb256db 100644 --- a/erpnext/accounts/utils.py +++ b/erpnext/accounts/utils.py @@ -1845,3 +1845,74 @@ def get_voucher_outstandings( self.query_for_outstanding() return self.voucher_outstandings + + +def create_gain_loss_journal( + company, + party_type, + party, + party_account, + gain_loss_account, + exc_gain_loss, + dr_or_cr, + reverse_dr_or_cr, + ref1_dt, + ref1_dn, + ref1_detail_no, + ref2_dt, + ref2_dn, + ref2_detail_no, +) -> str: + journal_entry = frappe.new_doc("Journal Entry") + journal_entry.voucher_type = "Exchange Gain Or Loss" + journal_entry.company = company + journal_entry.posting_date = nowdate() + journal_entry.multi_currency = 1 + + party_account_currency = frappe.get_cached_value("Account", party_account, "account_currency") + + if not gain_loss_account: + frappe.throw(_("Please set default Exchange Gain/Loss Account in Company {}").format(company)) + gain_loss_account_currency = get_account_currency(gain_loss_account) + company_currency = frappe.get_cached_value("Company", company, "default_currency") + + if gain_loss_account_currency != company_currency: + frappe.throw(_("Currency for {0} must be {1}").format(gain_loss_account, company_currency)) + + journal_account = frappe._dict( + { + "account": party_account, + "party_type": party_type, + "party": party, + "account_currency": party_account_currency, + "exchange_rate": 0, + "cost_center": erpnext.get_default_cost_center(company), + "reference_type": ref1_dt, + "reference_name": ref1_dn, + "reference_detail_no": ref1_detail_no, + dr_or_cr: abs(exc_gain_loss), + dr_or_cr + "_in_account_currency": 0, + } + ) + + journal_entry.append("accounts", journal_account) + + journal_account = frappe._dict( + { + "account": gain_loss_account, + "account_currency": gain_loss_account_currency, + "exchange_rate": 1, + "cost_center": erpnext.get_default_cost_center(company), + "reference_type": ref2_dt, + "reference_name": ref2_dn, + "reference_detail_no": ref2_detail_no, + reverse_dr_or_cr + "_in_account_currency": 0, + reverse_dr_or_cr: abs(exc_gain_loss), + } + ) + + journal_entry.append("accounts", journal_account) + + journal_entry.save() + journal_entry.submit() + return journal_entry.name diff --git a/erpnext/controllers/accounts_controller.py b/erpnext/controllers/accounts_controller.py index 3f82312ef74e..a61795fd450c 100644 --- a/erpnext/controllers/accounts_controller.py +++ b/erpnext/controllers/accounts_controller.py @@ -38,7 +38,12 @@ get_party_gle_currency, validate_party_frozen_disabled, ) -from erpnext.accounts.utils import get_account_currency, get_fiscal_years, validate_fiscal_year +from erpnext.accounts.utils import ( + create_gain_loss_journal, + get_account_currency, + get_fiscal_years, + validate_fiscal_year, +) from erpnext.buying.utils import update_last_purchase_rate from erpnext.controllers.print_settings import ( set_print_templates_for_item_table, @@ -962,78 +967,6 @@ def set_advance_gain_or_loss(self): d.exchange_gain_loss = difference - def create_gain_loss_journal( - self, - party_type, - party, - party_account, - gain_loss_account, - exc_gain_loss, - dr_or_cr, - reverse_dr_or_cr, - ref1_dt, - ref1_dn, - ref1_detail_no, - ref2_dt, - ref2_dn, - ref2_detail_no, - ) -> str: - journal_entry = frappe.new_doc("Journal Entry") - journal_entry.voucher_type = "Exchange Gain Or Loss" - journal_entry.company = self.company - journal_entry.posting_date = nowdate() - journal_entry.multi_currency = 1 - - party_account_currency = frappe.get_cached_value("Account", party_account, "account_currency") - - if not gain_loss_account: - frappe.throw( - _("Please set default Exchange Gain/Loss Account in Company {}").format(self.get("company")) - ) - gain_loss_account_currency = get_account_currency(gain_loss_account) - company_currency = frappe.get_cached_value("Company", self.company, "default_currency") - - if gain_loss_account_currency != self.company_currency: - frappe.throw(_("Currency for {0} must be {1}").format(gain_loss_account, company_currency)) - - journal_account = frappe._dict( - { - "account": party_account, - "party_type": party_type, - "party": party, - "account_currency": party_account_currency, - "exchange_rate": 0, - "cost_center": erpnext.get_default_cost_center(self.company), - "reference_type": ref1_dt, - "reference_name": ref1_dn, - "reference_detail_no": ref1_detail_no, - dr_or_cr: abs(exc_gain_loss), - dr_or_cr + "_in_account_currency": 0, - } - ) - - journal_entry.append("accounts", journal_account) - - journal_account = frappe._dict( - { - "account": gain_loss_account, - "account_currency": gain_loss_account_currency, - "exchange_rate": 1, - "cost_center": erpnext.get_default_cost_center(self.company), - "reference_type": ref2_dt, - "reference_name": ref2_dn, - "reference_detail_no": ref2_detail_no, - reverse_dr_or_cr + "_in_account_currency": 0, - reverse_dr_or_cr: abs(exc_gain_loss), - } - ) - - journal_entry.append("accounts", journal_account) - - journal_entry.save() - journal_entry.submit() - return journal_entry.name - def make_exchange_gain_loss_journal(self, args: dict = None) -> None: """ Make Exchange Gain/Loss journal for Invoices and Payments @@ -1062,7 +995,8 @@ def make_exchange_gain_loss_journal(self, args: dict = None) -> None: reverse_dr_or_cr = "debit" if dr_or_cr == "credit" else "credit" - self.create_gain_loss_journal( + create_gain_loss_journal( + self.company, arg.get("party_type"), arg.get("party"), party_account, @@ -1134,7 +1068,8 @@ def make_exchange_gain_loss_journal(self, args: dict = None) -> None: "Company", self.company, "exchange_gain_loss_account" ) - self.create_gain_loss_journal( + create_gain_loss_journal( + self.company, self.party_type, self.party, party_account, From 72a507f888aeac4d48701a7bdc7ceb46b3056d58 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Wed, 26 Jul 2023 16:46:50 +0530 Subject: [PATCH 147/501] refactor: create gain/loss on Cr/Dr notes with different exc rates (cherry picked from commit ba1f065765db6fc36358281fb4e4d775f1c1dcb1) --- .../doctype/journal_entry/journal_entry.py | 4 ++- .../payment_reconciliation.py | 27 ++++++++++++++++++- 2 files changed, 29 insertions(+), 2 deletions(-) diff --git a/erpnext/accounts/doctype/journal_entry/journal_entry.py b/erpnext/accounts/doctype/journal_entry/journal_entry.py index 73f0a4c2d459..e20575f31a1f 100644 --- a/erpnext/accounts/doctype/journal_entry/journal_entry.py +++ b/erpnext/accounts/doctype/journal_entry/journal_entry.py @@ -574,7 +574,9 @@ def validate_reference_doc(self): else: party_account = against_voucher[1] - if against_voucher[0] != cstr(d.party) or party_account != d.account: + if ( + against_voucher[0] != cstr(d.party) or party_account != d.account + ) and self.voucher_type != "Exchange Gain Or Loss": frappe.throw( _("Row {0}: Party / Account does not match with {1} / {2} in {3} {4}").format( d.idx, diff --git a/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py b/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py index e4e5aeb1593e..774d674b5eec 100644 --- a/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py +++ b/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py @@ -14,6 +14,7 @@ ) from erpnext.accounts.utils import ( QueryPaymentLedger, + create_gain_loss_journal, get_outstanding_invoices, reconcile_against_document, ) @@ -671,4 +672,28 @@ def get_difference_row(inv): jv.flags.ignore_exchange_rate = True >>>>>>> c87332d5da (refactor: cr/dr note will be on single exchange rate) jv.submit() - jv.make_exchange_gain_loss_journal(args=[inv]) + + # make gain/loss journal + if inv.party_type == "Customer": + dr_or_cr = "credit" if inv.difference_amount < 0 else "debit" + else: + dr_or_cr = "debit" if inv.difference_amount < 0 else "credit" + + reverse_dr_or_cr = "debit" if dr_or_cr == "credit" else "credit" + + create_gain_loss_journal( + company, + inv.party_type, + inv.party, + inv.account, + inv.difference_account, + inv.difference_amount, + dr_or_cr, + reverse_dr_or_cr, + inv.against_voucher_type, + inv.against_voucher, + None, + inv.voucher_type, + inv.voucher_no, + None, + ) From 39c439dc4b8dc7f054644917141ecfd861656e12 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Wed, 26 Jul 2023 20:53:07 +0530 Subject: [PATCH 148/501] fix: incorrect gain/loss on allocation change on reconciliation tool (cherry picked from commit 506a5775f9937fc893ae02b287ecd7303487363c) --- .../doctype/payment_reconciliation/payment_reconciliation.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py b/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py index 774d674b5eec..18d986cb7c4e 100644 --- a/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py +++ b/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py @@ -261,6 +261,11 @@ def is_auto_process_enabled(self): def calculate_difference_on_allocation_change(self, payment_entry, invoice, allocated_amount): invoice_exchange_map = self.get_invoice_exchange_map(invoice, payment_entry) invoice[0]["exchange_rate"] = invoice_exchange_map.get(invoice[0].get("invoice_number")) + if payment_entry[0].get("reference_type") in ["Sales Invoice", "Purchase Invoice"]: + payment_entry[0]["exchange_rate"] = invoice_exchange_map.get( + payment_entry[0].get("reference_name") + ) + new_difference_amount = self.get_difference_amount( payment_entry[0], invoice[0], allocated_amount ) From 09e9b16b932899122eab5f424c95ff42c74ae37f Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Wed, 26 Jul 2023 21:12:14 +0530 Subject: [PATCH 149/501] test: cr notes against invoice (cherry picked from commit e3d2a2c5bdd94364f22828acc40854f6834b66ce) --- .../tests/test_accounts_controller.py | 43 ++++++++++++++++++- 1 file changed, 42 insertions(+), 1 deletion(-) diff --git a/erpnext/controllers/tests/test_accounts_controller.py b/erpnext/controllers/tests/test_accounts_controller.py index eefe202e476a..fc4fb9fe9bfa 100644 --- a/erpnext/controllers/tests/test_accounts_controller.py +++ b/erpnext/controllers/tests/test_accounts_controller.py @@ -51,7 +51,7 @@ def make_supplier(supplier_name, currency=None): class TestAccountsController(unittest.TestCase): """ Test Exchange Gain/Loss booking on various scenarios. - Test Cases are numbered for better readbility + Test Cases are numbered for better organization 10 series - Sales Invoice against Payment Entries 20 series - Sales Invoice against Journals @@ -923,3 +923,44 @@ def test_23_same_journal_split_against_single_invoice(self): exc_je_for_je = self.get_journals_for(je.doctype, je.name) self.assertEqual(exc_je_for_si, []) self.assertEqual(exc_je_for_je, []) + + def test_30_cr_note_against_sales_invoice(self): + """ + Reconciling Cr Note against Sales Invoice, both having different exchange rates + """ + # Invoice in Foreign currency + si = self.create_sales_invoice(qty=2, conversion_rate=80, rate=1) + + # Cr Note in Foreign currency of different exchange rate + cr_note = self.create_sales_invoice(qty=-2, conversion_rate=75, rate=1, do_not_save=True) + cr_note.is_return = 1 + cr_note.save().submit() + + # Reconcile the first half + pr = self.create_payment_reconciliation() + pr.get_unreconciled_entries() + self.assertEqual(len(pr.invoices), 1) + self.assertEqual(len(pr.payments), 1) + invoices = [x.as_dict() for x in pr.invoices] + payments = [x.as_dict() for x in pr.payments] + pr.allocate_entries(frappe._dict({"invoices": invoices, "payments": payments})) + difference_amount = pr.calculate_difference_on_allocation_change( + [x.as_dict() for x in pr.payments], [x.as_dict() for x in pr.invoices], 1 + ) + pr.allocation[0].allocated_amount = 1 + pr.allocation[0].difference_amount = difference_amount + pr.reconcile() + self.assertEqual(len(pr.invoices), 1) + self.assertEqual(len(pr.payments), 1) + + # Exchange Gain/Loss Journal should've been created. + exc_je_for_si = self.get_journals_for(si.doctype, si.name) + exc_je_for_cr = self.get_journals_for(cr_note.doctype, cr_note.name) + self.assertNotEqual(exc_je_for_si, []) + self.assertEqual(len(exc_je_for_si), 2) + self.assertEqual(len(exc_je_for_cr), 2) + self.assertEqual(exc_je_for_cr, exc_je_for_si) + + si.reload() + self.assertEqual(si.outstanding_amount, 1) + self.assert_ledger_outstanding(si.doctype, si.name, 80.0, 1.0) From 349601b4b971010cfc7d260c644c9ec7b95924a3 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Wed, 26 Jul 2023 21:15:48 +0530 Subject: [PATCH 150/501] fix: cr/dr note should be posted for exc gain/loss (cherry picked from commit 95543225cf402e9f17e05efee80f2dfb199aa4d9) --- .../payment_reconciliation/payment_reconciliation.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py b/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py index 18d986cb7c4e..2f55eedd8588 100644 --- a/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py +++ b/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py @@ -695,10 +695,10 @@ def get_difference_row(inv): inv.difference_amount, dr_or_cr, reverse_dr_or_cr, - inv.against_voucher_type, - inv.against_voucher, - None, inv.voucher_type, inv.voucher_no, None, + inv.against_voucher_type, + inv.against_voucher, + None, ) From c5c440b7bc429e04accecb58a946a1ba98a3dc9f Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Wed, 26 Jul 2023 21:24:08 +0530 Subject: [PATCH 151/501] test: assert ledger after cr note cancellation (cherry picked from commit ae424fdfedb49e6018d957eebb11eb4e03d9d410) --- .../tests/test_accounts_controller.py | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/erpnext/controllers/tests/test_accounts_controller.py b/erpnext/controllers/tests/test_accounts_controller.py index fc4fb9fe9bfa..415e1734a93d 100644 --- a/erpnext/controllers/tests/test_accounts_controller.py +++ b/erpnext/controllers/tests/test_accounts_controller.py @@ -964,3 +964,19 @@ def test_30_cr_note_against_sales_invoice(self): si.reload() self.assertEqual(si.outstanding_amount, 1) self.assert_ledger_outstanding(si.doctype, si.name, 80.0, 1.0) + + cr_note.reload() + cr_note.cancel() + + # Exchange Gain/Loss Journal should've been created. + exc_je_for_si = self.get_journals_for(si.doctype, si.name) + exc_je_for_cr = self.get_journals_for(cr_note.doctype, cr_note.name) + self.assertNotEqual(exc_je_for_si, []) + self.assertEqual(len(exc_je_for_si), 1) + self.assertEqual(len(exc_je_for_cr), 0) + + # The Credit Note JE is still active and is referencing the sales invoice + # So, outstanding stays the same + si.reload() + self.assertEqual(si.outstanding_amount, 1) + self.assert_ledger_outstanding(si.doctype, si.name, 80.0, 1.0) From 3542df70f68e76cc9c7b806900b723b4258e5e94 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Wed, 26 Jul 2023 21:54:23 +0530 Subject: [PATCH 152/501] fix(test): test case breakage in Github Actions (cherry picked from commit bfa54d533572f33ea5bc83794489293d36949e5d) --- erpnext/accounts/doctype/journal_entry/test_journal_entry.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/erpnext/accounts/doctype/journal_entry/test_journal_entry.py b/erpnext/accounts/doctype/journal_entry/test_journal_entry.py index f7297d19e0f9..e44ebc6afce3 100644 --- a/erpnext/accounts/doctype/journal_entry/test_journal_entry.py +++ b/erpnext/accounts/doctype/journal_entry/test_journal_entry.py @@ -5,6 +5,7 @@ import unittest import frappe +from frappe.tests.utils import change_settings from frappe.utils import flt, nowdate from erpnext.accounts.doctype.account.test_account import get_inventory_account @@ -13,6 +14,7 @@ class TestJournalEntry(unittest.TestCase): + @change_settings("Accounts Settings", {"unlink_payment_on_cancellation_of_invoice": 1}) def test_journal_entry_with_against_jv(self): jv_invoice = frappe.copy_doc(test_records[2]) base_jv = frappe.copy_doc(test_records[0]) From 052abcb0756156b224fe6a74603df7970c9dfe48 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Wed, 26 Jul 2023 22:32:59 +0530 Subject: [PATCH 153/501] refactor(test): assert ledger outstanding (cherry picked from commit 025091161e47bd2ad77beec068d5263567605425) # Conflicts: # erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py --- .../sales_invoice/test_sales_invoice.py | 28 +++++++++++++++++-- 1 file changed, 26 insertions(+), 2 deletions(-) diff --git a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py index eed33693e9b0..6f05cf36f933 100644 --- a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py +++ b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py @@ -3260,15 +3260,19 @@ def test_gain_loss_with_advance_entry(self): ) si.save() si.submit() - expected_gle = [ +<<<<<<< HEAD ["_Test Receivable USD - _TC", 7500.0, 500], ["Exchange Gain/Loss - _TC", 500.0, 0.0], ["Sales - _TC", 0.0, 7500.0], +======= + ["_Test Receivable USD - _TC", 7500.0, 0.0, nowdate()], + ["Sales - _TC", 0.0, 7500.0, nowdate()], +>>>>>>> 025091161e (refactor(test): assert ledger outstanding) ] - check_gl_entries(self, si.name, expected_gle, nowdate()) +<<<<<<< HEAD <<<<<<< HEAD frappe.db.set_value( "Accounts Settings", "Accounts Settings", "unlink_payment_on_cancel_of_invoice", unlink_enabled @@ -3276,6 +3280,26 @@ def test_gain_loss_with_advance_entry(self): ======= >>>>>>> 70dd9d0671 (chore(test): fix broken unit test) +======= + si.reload() + self.assertEqual(si.outstanding_amount, 0) + journals = frappe.db.get_all( + "Journal Entry Account", + filters={"reference_type": "Sales Invoice", "reference_name": si.name, "docstatus": 1}, + pluck="parent", + ) + journals = [x for x in journals if x != jv.name] + self.assertEqual(len(journals), 1) + je_type = frappe.get_cached_value("Journal Entry", journals[0], "voucher_type") + self.assertEqual(je_type, "Exchange Gain Or Loss") + ledger_outstanding = frappe.db.get_all( + "Payment Ledger Entry", + filters={"against_voucher_no": si.name, "delinked": 0}, + fields=["sum(amount), sum(amount_in_account_currency)"], + as_list=1, + ) + +>>>>>>> 025091161e (refactor(test): assert ledger outstanding) def test_batch_expiry_for_sales_invoice_return(self): from erpnext.controllers.sales_and_purchase_return import make_return_doc from erpnext.stock.doctype.item.test_item import make_item From 2a61d854d34117b6892a7a75d1108d1f67e24df7 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Thu, 27 Jul 2023 05:54:13 +0530 Subject: [PATCH 154/501] chore: use frappetestcase (cherry picked from commit 47bbb37291eba1e2bb8217837417a072f73f5634) --- erpnext/controllers/tests/test_accounts_controller.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/erpnext/controllers/tests/test_accounts_controller.py b/erpnext/controllers/tests/test_accounts_controller.py index 415e1734a93d..acda12bf5957 100644 --- a/erpnext/controllers/tests/test_accounts_controller.py +++ b/erpnext/controllers/tests/test_accounts_controller.py @@ -47,8 +47,7 @@ def make_supplier(supplier_name, currency=None): return supplier_name -# class TestAccountsController(FrappeTestCase): -class TestAccountsController(unittest.TestCase): +class TestAccountsController(FrappeTestCase): """ Test Exchange Gain/Loss booking on various scenarios. Test Cases are numbered for better organization @@ -66,8 +65,7 @@ def setUp(self): self.clear_old_entries() def tearDown(self): - # frappe.db.rollback() - pass + frappe.db.rollback() def create_company(self): company_name = "_Test Company MC" From 4c527d6bba1d5980950ff31cafe24b914b9228f2 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Thu, 27 Jul 2023 07:52:01 +0530 Subject: [PATCH 155/501] chore: add msgprint for exc JE (cherry picked from commit acc7322874b97830a838d066925aec99b01af129) --- erpnext/controllers/accounts_controller.py | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/erpnext/controllers/accounts_controller.py b/erpnext/controllers/accounts_controller.py index a61795fd450c..7afd80b4bcf0 100644 --- a/erpnext/controllers/accounts_controller.py +++ b/erpnext/controllers/accounts_controller.py @@ -995,7 +995,7 @@ def make_exchange_gain_loss_journal(self, args: dict = None) -> None: reverse_dr_or_cr = "debit" if dr_or_cr == "credit" else "credit" - create_gain_loss_journal( + je = create_gain_loss_journal( self.company, arg.get("party_type"), arg.get("party"), @@ -1011,6 +1011,11 @@ def make_exchange_gain_loss_journal(self, args: dict = None) -> None: self.name, arg.get("idx"), ) + frappe.msgprint( + _("Exchange Gain/Loss amount has been booked through {0}").format( + get_link_to_form("Journal Entry", je) + ) + ) if self.get("doctype") == "Payment Entry": # For Payment Entry, exchange_gain_loss field in the `references` table is the trigger for journal creation @@ -1068,7 +1073,7 @@ def make_exchange_gain_loss_journal(self, args: dict = None) -> None: "Company", self.company, "exchange_gain_loss_account" ) - create_gain_loss_journal( + je = create_gain_loss_journal( self.company, self.party_type, self.party, @@ -1084,7 +1089,11 @@ def make_exchange_gain_loss_journal(self, args: dict = None) -> None: self.name, d.idx, ) - # frappe.throw("stopping...") + frappe.msgprint( + _("Exchange Gain/Loss amount has been booked through {0}").format( + get_link_to_form("Journal Entry", je) + ) + ) def make_precision_loss_gl_entry(self, gl_entries): round_off_account, round_off_cost_center = get_round_off_account_and_cost_center( From 8d32a1f4b32531be9f1d50be4d384fad57475951 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Thu, 27 Jul 2023 08:02:46 +0530 Subject: [PATCH 156/501] chore: rename some internal variables (cherry picked from commit d9d685615335778cd36734b1d1bd0c2b4189b690) --- erpnext/accounts/utils.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/erpnext/accounts/utils.py b/erpnext/accounts/utils.py index d8235cb256db..29747b14b7df 100644 --- a/erpnext/accounts/utils.py +++ b/erpnext/accounts/utils.py @@ -682,8 +682,9 @@ def cancel_exchange_gain_loss_journal(parent_doc: dict | object) -> None: fields=["parent"], as_list=1, ) + if journals: - exchange_journals = frappe.db.get_all( + gain_loss_journals = frappe.db.get_all( "Journal Entry", filters={ "name": ["in", [x[0] for x in journals]], @@ -692,7 +693,7 @@ def cancel_exchange_gain_loss_journal(parent_doc: dict | object) -> None: }, as_list=1, ) - for doc in exchange_journals: + for doc in gain_loss_journals: frappe.get_doc("Journal Entry", doc[0]).cancel() From efb293398aae6677b9d5a94a823f681a63fb2c36 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Thu, 27 Jul 2023 09:30:38 +0530 Subject: [PATCH 157/501] chore(test): use existing company for unit test (cherry picked from commit 804afaa647b5727c37206fc4207c203652c10d53) --- erpnext/controllers/tests/test_accounts_controller.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/erpnext/controllers/tests/test_accounts_controller.py b/erpnext/controllers/tests/test_accounts_controller.py index acda12bf5957..8e5f813d97d6 100644 --- a/erpnext/controllers/tests/test_accounts_controller.py +++ b/erpnext/controllers/tests/test_accounts_controller.py @@ -68,8 +68,8 @@ def tearDown(self): frappe.db.rollback() def create_company(self): - company_name = "_Test Company MC" - self.company_abbr = abbr = "_CM" + company_name = "_Test Company" + self.company_abbr = abbr = "_TC" if frappe.db.exists("Company", company_name): company = frappe.get_doc("Company", company_name) else: From ed0881dacb67df9f0ebea5e19a73c0bda88e1861 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Fri, 28 Jul 2023 08:12:44 +0530 Subject: [PATCH 158/501] chore: don't make gain/loss journal for base currency transactions (cherry picked from commit 567c0ce1e85a42056d76cfc399b3468df32a576a) --- .../payment_reconciliation.py | 49 ++++++++++--------- 1 file changed, 25 insertions(+), 24 deletions(-) diff --git a/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py b/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py index 2f55eedd8588..8bdf5dcb7591 100644 --- a/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py +++ b/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py @@ -678,27 +678,28 @@ def get_difference_row(inv): >>>>>>> c87332d5da (refactor: cr/dr note will be on single exchange rate) jv.submit() - # make gain/loss journal - if inv.party_type == "Customer": - dr_or_cr = "credit" if inv.difference_amount < 0 else "debit" - else: - dr_or_cr = "debit" if inv.difference_amount < 0 else "credit" - - reverse_dr_or_cr = "debit" if dr_or_cr == "credit" else "credit" - - create_gain_loss_journal( - company, - inv.party_type, - inv.party, - inv.account, - inv.difference_account, - inv.difference_amount, - dr_or_cr, - reverse_dr_or_cr, - inv.voucher_type, - inv.voucher_no, - None, - inv.against_voucher_type, - inv.against_voucher, - None, - ) + if inv.difference_amount != 0: + # make gain/loss journal + if inv.party_type == "Customer": + dr_or_cr = "credit" if inv.difference_amount < 0 else "debit" + else: + dr_or_cr = "debit" if inv.difference_amount < 0 else "credit" + + reverse_dr_or_cr = "debit" if dr_or_cr == "credit" else "credit" + + create_gain_loss_journal( + company, + inv.party_type, + inv.party, + inv.account, + inv.difference_account, + inv.difference_amount, + dr_or_cr, + reverse_dr_or_cr, + inv.voucher_type, + inv.voucher_no, + None, + inv.against_voucher_type, + inv.against_voucher, + None, + ) From 61afffc908a148316a37286f08f5408860e203ae Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Fri, 28 Jul 2023 08:29:19 +0530 Subject: [PATCH 159/501] chore: cancel gain/loss je while posting reverse gl (cherry picked from commit 46ea81440066af74a3b98f4ab9d5006839a17a4b) --- .../accounts/doctype/journal_entry/journal_entry.py | 3 +++ .../accounts/doctype/payment_entry/payment_entry.py | 12 ++++++++++-- .../accounts/doctype/sales_invoice/sales_invoice.py | 3 ++- erpnext/controllers/stock_controller.py | 3 ++- 4 files changed, 17 insertions(+), 4 deletions(-) diff --git a/erpnext/accounts/doctype/journal_entry/journal_entry.py b/erpnext/accounts/doctype/journal_entry/journal_entry.py index e20575f31a1f..f68980261349 100644 --- a/erpnext/accounts/doctype/journal_entry/journal_entry.py +++ b/erpnext/accounts/doctype/journal_entry/journal_entry.py @@ -18,6 +18,7 @@ ) from erpnext.accounts.party import get_party_account from erpnext.accounts.utils import ( + cancel_exchange_gain_loss_journal, get_account_currency, get_balance_on, get_stock_accounts, @@ -933,6 +934,8 @@ def make_gl_entries(self, cancel=0, adv_adj=0): merge_entries=merge_entries, update_outstanding=update_outstanding, ) + if cancel: + cancel_exchange_gain_loss_journal(frappe._dict(doctype=self.doctype, name=self.name)) @frappe.whitelist() def get_balance(self, difference_account=None): diff --git a/erpnext/accounts/doctype/payment_entry/payment_entry.py b/erpnext/accounts/doctype/payment_entry/payment_entry.py index b4c39f41063a..379903dade3f 100644 --- a/erpnext/accounts/doctype/payment_entry/payment_entry.py +++ b/erpnext/accounts/doctype/payment_entry/payment_entry.py @@ -24,7 +24,12 @@ ) from erpnext.accounts.general_ledger import make_gl_entries, process_gl_map from erpnext.accounts.party import get_party_account -from erpnext.accounts.utils import get_account_currency, get_balance_on, get_outstanding_invoices +from erpnext.accounts.utils import ( + cancel_exchange_gain_loss_journal, + get_account_currency, + get_balance_on, + get_outstanding_invoices, +) from erpnext.controllers.accounts_controller import ( AccountsController, get_supplier_block_status, @@ -993,7 +998,10 @@ def make_gl_entries(self, cancel=0, adv_adj=0): gl_entries = self.build_gl_map() gl_entries = process_gl_map(gl_entries) make_gl_entries(gl_entries, cancel=cancel, adv_adj=adv_adj) - self.make_exchange_gain_loss_journal() + if cancel: + cancel_exchange_gain_loss_journal(frappe._dict(doctype=self.doctype, name=self.name)) + else: + self.make_exchange_gain_loss_journal() def add_party_gl_entries(self, gl_entries): if self.party_account: diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py index 9e9cd61e2032..ab629913cd41 100644 --- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py +++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py @@ -23,7 +23,7 @@ ) from erpnext.accounts.general_ledger import get_round_off_account_and_cost_center from erpnext.accounts.party import get_due_date, get_party_account, get_party_details -from erpnext.accounts.utils import get_account_currency +from erpnext.accounts.utils import cancel_exchange_gain_loss_journal, get_account_currency from erpnext.assets.doctype.asset.depreciation import ( depreciate_asset, get_disposal_account_and_cost_center, @@ -1049,6 +1049,7 @@ def make_gl_entries(self, gl_entries=None, from_repost=False): self.make_exchange_gain_loss_journal() elif self.docstatus == 2: + cancel_exchange_gain_loss_journal(frappe._dict(doctype=self.doctype, name=self.name)) make_reverse_gl_entries(voucher_type=self.doctype, voucher_no=self.name) if update_outstanding == "No": diff --git a/erpnext/controllers/stock_controller.py b/erpnext/controllers/stock_controller.py index 4f0c8a9a54f0..e24d8fb661f6 100644 --- a/erpnext/controllers/stock_controller.py +++ b/erpnext/controllers/stock_controller.py @@ -15,7 +15,7 @@ make_reverse_gl_entries, process_gl_map, ) -from erpnext.accounts.utils import get_fiscal_year +from erpnext.accounts.utils import cancel_exchange_gain_loss_journal, get_fiscal_year from erpnext.controllers.accounts_controller import AccountsController from erpnext.stock import get_warehouse_account_map from erpnext.stock.doctype.inventory_dimension.inventory_dimension import ( @@ -513,6 +513,7 @@ def make_sl_entries(self, sl_entries, allow_negative_stock=False, via_landed_cos make_sl_entries(sl_entries, allow_negative_stock, via_landed_cost_voucher) def make_gl_entries_on_cancel(self): + cancel_exchange_gain_loss_journal(frappe._dict(doctype=self.doctype, name=self.name)) if frappe.db.sql( """select name from `tabGL Entry` where voucher_type=%s and voucher_no=%s""", From 7469018d3e0dd6359ba3dbf2ec3844c32b5c01a9 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Mon, 14 Aug 2023 10:37:56 +0530 Subject: [PATCH 160/501] chore: resolve merge conflict --- erpnext/accounts/doctype/gl_entry/gl_entry.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/erpnext/accounts/doctype/gl_entry/gl_entry.py b/erpnext/accounts/doctype/gl_entry/gl_entry.py index 26f4f2ba75f7..3a564825b55a 100644 --- a/erpnext/accounts/doctype/gl_entry/gl_entry.py +++ b/erpnext/accounts/doctype/gl_entry/gl_entry.py @@ -58,9 +58,6 @@ def on_update(self): validate_balance_type(self.account, adv_adj) validate_frozen_account(self.account, adv_adj) -<<<<<<< HEAD - if frappe.db.get_value("Account", self.account, "account_type") not in [ -======= if ( self.voucher_type == "Journal Entry" and frappe.get_cached_value("Journal Entry", self.voucher_no, "voucher_type") @@ -69,7 +66,6 @@ def on_update(self): return if frappe.get_cached_value("Account", self.account, "account_type") not in [ ->>>>>>> f119a1e115 (refactor: linkage between journal as payment and gain/loss journal) "Receivable", "Payable", ]: From f92453ae456b2f2f2346c20ec75ccceb2bc61277 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Mon, 14 Aug 2023 10:50:31 +0530 Subject: [PATCH 161/501] chore: resolve merge conflict in `accounts/utils.py` and its tests --- erpnext/accounts/test/test_utils.py | 3 --- erpnext/accounts/utils.py | 19 ------------------- 2 files changed, 22 deletions(-) diff --git a/erpnext/accounts/test/test_utils.py b/erpnext/accounts/test/test_utils.py index f72ac783bd3c..0a8c7239861e 100644 --- a/erpnext/accounts/test/test_utils.py +++ b/erpnext/accounts/test/test_utils.py @@ -73,8 +73,6 @@ def test_stock_voucher_sorting(self): sorted_vouchers = sort_stock_vouchers_by_posting_date(list(reversed(vouchers))) self.assertEqual(sorted_vouchers, vouchers) -<<<<<<< HEAD -======= def test_update_reference_in_payment_entry(self): item = make_item().name @@ -125,7 +123,6 @@ def test_update_reference_in_payment_entry(self): self.assertEqual(len(payment_entry.references), 1) self.assertEqual(payment_entry.difference_amount, 0) ->>>>>>> 72bc5b3a11 (refactor(test): difference amount no updated for exchange gain/loss) ADDRESS_RECORDS = [ { diff --git a/erpnext/accounts/utils.py b/erpnext/accounts/utils.py index 29747b14b7df..3e06a36e67ed 100644 --- a/erpnext/accounts/utils.py +++ b/erpnext/accounts/utils.py @@ -636,25 +636,6 @@ def update_reference_in_payment_entry( new_row.docstatus = 1 new_row.update(reference_details) -<<<<<<< HEAD - payment_entry.flags.ignore_validate_update_after_submit = True - payment_entry.setup_party_account_field() - payment_entry.set_missing_values() - payment_entry.set_amounts() - - if d.difference_amount and d.difference_account: - account_details = { - "account": d.difference_account, - "cost_center": payment_entry.cost_center - or frappe.get_cached_value("Company", payment_entry.company, "cost_center"), - } - if d.difference_amount: - account_details["amount"] = d.difference_amount - - payment_entry.set_gain_or_loss(account_details=account_details) - -======= ->>>>>>> 1bcb728c85 (refactor: remove call for setting deductions in payment entry) payment_entry.flags.ignore_validate_update_after_submit = True payment_entry.setup_party_account_field() payment_entry.set_missing_values() From 946aadb0c00368fb4def4b96f1c0f6ff00d13664 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Mon, 14 Aug 2023 10:52:35 +0530 Subject: [PATCH 162/501] chore: resolve conflict in `test_sales_invoice.py` --- .../sales_invoice/test_sales_invoice.py | 27 ------------------- 1 file changed, 27 deletions(-) diff --git a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py index 6f05cf36f933..c36a05ac0998 100644 --- a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py +++ b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py @@ -3217,17 +3217,6 @@ def test_sales_invoice_with_disabled_account(self): def test_gain_loss_with_advance_entry(self): from erpnext.accounts.doctype.journal_entry.test_journal_entry import make_journal_entry -<<<<<<< HEAD - unlink_enabled = frappe.db.get_value( - "Accounts Settings", "Accounts Settings", "unlink_payment_on_cancel_of_invoice" - ) - - frappe.db.set_value( - "Accounts Settings", "Accounts Settings", "unlink_payment_on_cancel_of_invoice", 1 - ) - -======= ->>>>>>> 70dd9d0671 (chore(test): fix broken unit test) jv = make_journal_entry("_Test Receivable USD - _TC", "_Test Bank - _TC", -7000, save=False) jv.accounts[0].exchange_rate = 70 @@ -3261,26 +3250,11 @@ def test_gain_loss_with_advance_entry(self): si.save() si.submit() expected_gle = [ -<<<<<<< HEAD - ["_Test Receivable USD - _TC", 7500.0, 500], - ["Exchange Gain/Loss - _TC", 500.0, 0.0], - ["Sales - _TC", 0.0, 7500.0], -======= ["_Test Receivable USD - _TC", 7500.0, 0.0, nowdate()], ["Sales - _TC", 0.0, 7500.0, nowdate()], ->>>>>>> 025091161e (refactor(test): assert ledger outstanding) ] check_gl_entries(self, si.name, expected_gle, nowdate()) -<<<<<<< HEAD -<<<<<<< HEAD - frappe.db.set_value( - "Accounts Settings", "Accounts Settings", "unlink_payment_on_cancel_of_invoice", unlink_enabled - ) - -======= ->>>>>>> 70dd9d0671 (chore(test): fix broken unit test) -======= si.reload() self.assertEqual(si.outstanding_amount, 0) journals = frappe.db.get_all( @@ -3299,7 +3273,6 @@ def test_gain_loss_with_advance_entry(self): as_list=1, ) ->>>>>>> 025091161e (refactor(test): assert ledger outstanding) def test_batch_expiry_for_sales_invoice_return(self): from erpnext.controllers.sales_and_purchase_return import make_return_doc from erpnext.stock.doctype.item.test_item import make_item From b3f4c14a26bf3fb2e5c15f3291fabe1bc2f6e487 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Mon, 14 Aug 2023 10:53:42 +0530 Subject: [PATCH 163/501] chore: resolve conflict in `payment_reconciliation.py` backport will merge the better remarks PR https://github.com/frappe/erpnext/pull/36573 wil exchange gain/loss booking refactor --- .../payment_reconciliation.py | 13 ++----------- 1 file changed, 2 insertions(+), 11 deletions(-) diff --git a/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py b/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py index 8bdf5dcb7591..b6708ce24b14 100644 --- a/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py +++ b/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py @@ -640,11 +640,8 @@ def get_difference_row(inv): "reference_type": inv.against_voucher_type, "reference_name": inv.against_voucher, "cost_center": erpnext.get_default_cost_center(company), -<<<<<<< HEAD "user_remark": f"{fmt_money(flt(inv.allocated_amount), currency=company_currency)} against {inv.against_voucher}", -======= "exchange_rate": inv.exchange_rate, ->>>>>>> c87332d5da (refactor: cr/dr note will be on single exchange rate) }, { "account": inv.account, @@ -658,24 +655,18 @@ def get_difference_row(inv): "reference_type": inv.voucher_type, "reference_name": inv.voucher_no, "cost_center": erpnext.get_default_cost_center(company), -<<<<<<< HEAD "user_remark": f"{fmt_money(flt(inv.allocated_amount), currency=company_currency)} from {inv.voucher_no}", -======= "exchange_rate": inv.exchange_rate, ->>>>>>> c87332d5da (refactor: cr/dr note will be on single exchange rate) }, ], } ) jv.flags.ignore_mandatory = True -<<<<<<< HEAD - jv.remark = None jv.flags.skip_remarks_creation = True - jv.is_system_generated = True -======= jv.flags.ignore_exchange_rate = True ->>>>>>> c87332d5da (refactor: cr/dr note will be on single exchange rate) + jv.is_system_generated = True + jv.remark = None jv.submit() if inv.difference_amount != 0: From 2e6bfa36de244e2944cd2ff6d42fff4e6ba3cbb8 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Sat, 5 Aug 2023 14:11:57 +0530 Subject: [PATCH 164/501] fix(test): replace hardcoded reference to adv with dynamic one --- .../tests/test_accounts_controller.py | 65 ++++++++++++------- 1 file changed, 42 insertions(+), 23 deletions(-) diff --git a/erpnext/controllers/tests/test_accounts_controller.py b/erpnext/controllers/tests/test_accounts_controller.py index 8e5f813d97d6..0f8e133e0fdc 100644 --- a/erpnext/controllers/tests/test_accounts_controller.py +++ b/erpnext/controllers/tests/test_accounts_controller.py @@ -347,18 +347,23 @@ def test_11_advance_against_sales_invoice(self): for exc_rate in [75.9, 83.1, 80.01]: with self.subTest(exc_rate=exc_rate): si = self.create_sales_invoice(qty=1, conversion_rate=exc_rate, rate=1, do_not_submit=True) + advances = si.get_advance_entries() + self.assertEqual(len(advances), 1) + self.assertEqual(advances[0].reference_name, adv.name) si.append( "advances", { "doctype": "Sales Invoice Advance", - "reference_type": adv.doctype, - "reference_name": adv.name, + "reference_type": advances[0].reference_type, + "reference_name": advances[0].reference_name, + "reference_row": advances[0].reference_row, "advance_amount": 1, "allocated_amount": 1, - "ref_exchange_rate": 85, - "remarks": "Test", + "ref_exchange_rate": advances[0].exchange_rate, + "remarks": advances[0].remarks, }, ) + si = si.save() si = si.submit() @@ -398,16 +403,19 @@ def test_12_partial_advance_and_payment_for_sales_invoice(self): si = self.create_sales_invoice( qty=2, conversion_rate=80, rate=rate_in_account_currency, do_not_submit=True ) + advances = si.get_advance_entries() + self.assertEqual(len(advances), 1) + self.assertEqual(advances[0].reference_name, adv.name) si.append( "advances", { "doctype": "Sales Invoice Advance", - "reference_type": adv.doctype, - "reference_name": adv.name, + "reference_type": advances[0].reference_type, + "reference_name": advances[0].reference_name, "advance_amount": 1, "allocated_amount": 1, - "ref_exchange_rate": 85, - "remarks": "Test", + "ref_exchange_rate": advances[0].exchange_rate, + "remarks": advances[0].remarks, }, ) si = si.save() @@ -470,16 +478,19 @@ def test_13_partial_advance_and_payment_for_invoice_with_cancellation(self): # invoice with advance(partial amount) si = self.create_sales_invoice(qty=2, conversion_rate=80, rate=1, do_not_submit=True) + advances = si.get_advance_entries() + self.assertEqual(len(advances), 1) + self.assertEqual(advances[0].reference_name, adv.name) si.append( "advances", { "doctype": "Sales Invoice Advance", - "reference_type": adv.doctype, - "reference_name": adv.name, + "reference_type": advances[0].reference_type, + "reference_name": advances[0].reference_name, "advance_amount": 1, "allocated_amount": 1, - "ref_exchange_rate": 85, - "remarks": "Test", + "ref_exchange_rate": advances[0].exchange_rate, + "remarks": advances[0].remarks, }, ) si = si.save() @@ -678,22 +689,26 @@ def test_21_advance_journal_against_sales_invoice(self): adv.reload() # Sales Invoices in different exchange rates - for exc_rate in [75.9, 83.1, 80.01]: + for exc_rate in [75.9, 83.1]: with self.subTest(exc_rate=exc_rate): si = self.create_sales_invoice(qty=1, conversion_rate=exc_rate, rate=1, do_not_submit=True) + advances = si.get_advance_entries() + self.assertEqual(len(advances), 1) + self.assertEqual(advances[0].reference_name, adv.name) si.append( "advances", { "doctype": "Sales Invoice Advance", - "reference_type": adv.doctype, - "reference_name": adv.name, - "reference_row": adv.accounts[0].name, + "reference_type": advances[0].reference_type, + "reference_name": advances[0].reference_name, + "reference_row": advances[0].reference_row, "advance_amount": 1, "allocated_amount": 1, - "ref_exchange_rate": adv_exc_rate, - "remarks": "Test", + "ref_exchange_rate": advances[0].exchange_rate, + "remarks": advances[0].remarks, }, ) + si = si.save() si = si.submit() @@ -741,19 +756,23 @@ def test_22_partial_advance_and_payment_for_invoice_with_cancellation(self): # invoice with advance(partial amount) si = self.create_sales_invoice(qty=3, conversion_rate=80, rate=1, do_not_submit=True) + advances = si.get_advance_entries() + self.assertEqual(len(advances), 1) + self.assertEqual(advances[0].reference_name, adv.name) si.append( "advances", { "doctype": "Sales Invoice Advance", - "reference_type": adv.doctype, - "reference_name": adv.name, - "reference_row": adv.accounts[0].name, + "reference_type": advances[0].reference_type, + "reference_name": advances[0].reference_name, + "reference_row": advances[0].reference_row, "advance_amount": 1, "allocated_amount": 1, - "ref_exchange_rate": adv_exc_rate, - "remarks": "Test", + "ref_exchange_rate": advances[0].exchange_rate, + "remarks": advances[0].remarks, }, ) + si = si.save() si = si.submit() From 18cf93d1c8902c9116881fd901c6b0d8afdcacff Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Mon, 14 Aug 2023 11:49:46 +0530 Subject: [PATCH 165/501] refactor(test): import missing functions --- erpnext/accounts/test/test_utils.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/erpnext/accounts/test/test_utils.py b/erpnext/accounts/test/test_utils.py index 0a8c7239861e..3d5e5fc4ec7e 100644 --- a/erpnext/accounts/test/test_utils.py +++ b/erpnext/accounts/test/test_utils.py @@ -3,6 +3,8 @@ import frappe from frappe.test_runner import make_test_objects +from erpnext.accounts.doctype.payment_entry.payment_entry import get_payment_entry +from erpnext.accounts.doctype.purchase_invoice.test_purchase_invoice import make_purchase_invoice from erpnext.accounts.party import get_party_shipping_address from erpnext.accounts.utils import ( get_future_stock_vouchers, From b131f70ed6a8e3b763d59106084440e08a73b815 Mon Sep 17 00:00:00 2001 From: ViralKansodiya <141210323+viralkansodiya@users.noreply.github.com> Date: Mon, 14 Aug 2023 16:20:58 +0530 Subject: [PATCH 166/501] fix: Button Alignment center in hero slider (#36607) fix: speling in CSS (Button alignment center is not working on hero slider)#36561 --- erpnext/e_commerce/web_template/hero_slider/hero_slider.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/e_commerce/web_template/hero_slider/hero_slider.html b/erpnext/e_commerce/web_template/hero_slider/hero_slider.html index e560f4ad7deb..fe4fee375bd8 100644 --- a/erpnext/e_commerce/web_template/hero_slider/hero_slider.html +++ b/erpnext/e_commerce/web_template/hero_slider/hero_slider.html @@ -1,7 +1,7 @@ {%- macro slide(image, title, subtitle, action, label, index, align="Left", theme="Dark") -%} {%- set align_class = resolve_class({ 'text-right': align == 'Right', - 'text-centre': align == 'Centre', + 'text-center': align == 'Centre', 'text-left': align == 'Left', }) -%} From 90b390c2c5554b779da4989e9d4475039681fa9b Mon Sep 17 00:00:00 2001 From: Gursheen Kaur Anand <40693548+GursheenK@users.noreply.github.com> Date: Mon, 14 Aug 2023 18:15:21 +0530 Subject: [PATCH 167/501] feat: add voucher totals in tds payable report (#36568) * feat: voucher totals in tds payable monthly * fix: naming series column in tds payable report * fix: tds computation summary columns --- .../tds_computation_summary.js | 28 ++++- .../tds_computation_summary.py | 85 ++++++++------ .../tds_payable_monthly.js | 9 +- .../tds_payable_monthly.py | 110 ++++++++++++------ 4 files changed, 159 insertions(+), 73 deletions(-) diff --git a/erpnext/accounts/report/tds_computation_summary/tds_computation_summary.js b/erpnext/accounts/report/tds_computation_summary/tds_computation_summary.js index d3d45b353a6b..c42028b61f50 100644 --- a/erpnext/accounts/report/tds_computation_summary/tds_computation_summary.js +++ b/erpnext/accounts/report/tds_computation_summary/tds_computation_summary.js @@ -12,17 +12,35 @@ frappe.query_reports["TDS Computation Summary"] = { "default": frappe.defaults.get_default('company') }, { - "fieldname":"supplier", - "label": __("Supplier"), - "fieldtype": "Link", - "options": "Supplier", + "fieldname":"party_type", + "label": __("Party Type"), + "fieldtype": "Select", + "options": ["Supplier", "Customer"], + "reqd": 1, + "default": "Supplier", + "on_change": function(){ + frappe.query_report.set_filter_value("party", ""); + } + }, + { + "fieldname":"party", + "label": __("Party"), + "fieldtype": "Dynamic Link", + "get_options": function() { + var party_type = frappe.query_report.get_filter_value('party_type'); + var party = frappe.query_report.get_filter_value('party'); + if(party && !party_type) { + frappe.throw(__("Please select Party Type first")); + } + return party_type; + }, "get_query": function() { return { "filters": { "tax_withholding_category": ["!=",""], } } - } + }, }, { "fieldname":"from_date", diff --git a/erpnext/accounts/report/tds_computation_summary/tds_computation_summary.py b/erpnext/accounts/report/tds_computation_summary/tds_computation_summary.py index c6aa21cc8624..82f97f189418 100644 --- a/erpnext/accounts/report/tds_computation_summary/tds_computation_summary.py +++ b/erpnext/accounts/report/tds_computation_summary/tds_computation_summary.py @@ -9,9 +9,14 @@ def execute(filters=None): - validate_filters(filters) + if filters.get("party_type") == "Customer": + party_naming_by = frappe.db.get_single_value("Selling Settings", "cust_master_name") + else: + party_naming_by = frappe.db.get_single_value("Buying Settings", "supp_master_name") + + filters.update({"naming_series": party_naming_by}) - filters.naming_series = frappe.db.get_single_value("Buying Settings", "supp_master_name") + validate_filters(filters) columns = get_columns(filters) ( @@ -25,7 +30,7 @@ def execute(filters=None): res = get_result( filters, tds_docs, tds_accounts, tax_category_map, journal_entry_party_map, invoice_total_map ) - final_result = group_by_supplier_and_category(res) + final_result = group_by_party_and_category(res, filters) return columns, final_result @@ -43,60 +48,67 @@ def validate_filters(filters): filters["fiscal_year"] = from_year -def group_by_supplier_and_category(data): - supplier_category_wise_map = {} +def group_by_party_and_category(data, filters): + party_category_wise_map = {} for row in data: - supplier_category_wise_map.setdefault( - (row.get("supplier"), row.get("section_code")), + party_category_wise_map.setdefault( + (row.get("party"), row.get("section_code")), { "pan": row.get("pan"), - "supplier": row.get("supplier"), - "supplier_name": row.get("supplier_name"), + "tax_id": row.get("tax_id"), + "party": row.get("party"), + "party_name": row.get("party_name"), "section_code": row.get("section_code"), "entity_type": row.get("entity_type"), - "tds_rate": row.get("tds_rate"), - "total_amount_credited": 0.0, - "tds_deducted": 0.0, + "rate": row.get("rate"), + "total_amount": 0.0, + "tax_amount": 0.0, }, ) - supplier_category_wise_map.get((row.get("supplier"), row.get("section_code")))[ - "total_amount_credited" - ] += row.get("total_amount_credited", 0.0) + party_category_wise_map.get((row.get("party"), row.get("section_code")))[ + "total_amount" + ] += row.get("total_amount", 0.0) - supplier_category_wise_map.get((row.get("supplier"), row.get("section_code")))[ - "tds_deducted" - ] += row.get("tds_deducted", 0.0) + party_category_wise_map.get((row.get("party"), row.get("section_code")))[ + "tax_amount" + ] += row.get("tax_amount", 0.0) - final_result = get_final_result(supplier_category_wise_map) + final_result = get_final_result(party_category_wise_map) return final_result -def get_final_result(supplier_category_wise_map): +def get_final_result(party_category_wise_map): out = [] - for key, value in supplier_category_wise_map.items(): + for key, value in party_category_wise_map.items(): out.append(value) return out def get_columns(filters): + pan = "pan" if frappe.db.has_column(filters.party_type, "pan") else "tax_id" columns = [ - {"label": _("PAN"), "fieldname": "pan", "fieldtype": "Data", "width": 90}, + {"label": _(frappe.unscrub(pan)), "fieldname": pan, "fieldtype": "Data", "width": 90}, { - "label": _("Supplier"), - "options": "Supplier", - "fieldname": "supplier", - "fieldtype": "Link", + "label": _(filters.get("party_type")), + "fieldname": "party", + "fieldtype": "Dynamic Link", + "options": "party_type", "width": 180, }, ] if filters.naming_series == "Naming Series": columns.append( - {"label": _("Supplier Name"), "fieldname": "supplier_name", "fieldtype": "Data", "width": 180} + { + "label": _(filters.party_type + " Name"), + "fieldname": "party_name", + "fieldtype": "Data", + "width": 180, + } ) columns.extend( @@ -109,18 +121,23 @@ def get_columns(filters): "width": 180, }, {"label": _("Entity Type"), "fieldname": "entity_type", "fieldtype": "Data", "width": 180}, - {"label": _("TDS Rate %"), "fieldname": "tds_rate", "fieldtype": "Percent", "width": 90}, { - "label": _("Total Amount Credited"), - "fieldname": "total_amount_credited", + "label": _("TDS Rate %") if filters.get("party_type") == "Supplier" else _("TCS Rate %"), + "fieldname": "rate", + "fieldtype": "Percent", + "width": 120, + }, + { + "label": _("Total Amount"), + "fieldname": "total_amount", "fieldtype": "Float", - "width": 90, + "width": 120, }, { - "label": _("Amount of TDS Deducted"), - "fieldname": "tds_deducted", + "label": _("Tax Amount"), + "fieldname": "tax_amount", "fieldtype": "Float", - "width": 90, + "width": 120, }, ] ) diff --git a/erpnext/accounts/report/tds_payable_monthly/tds_payable_monthly.js b/erpnext/accounts/report/tds_payable_monthly/tds_payable_monthly.js index 3df21e87185a..6585ea0a293b 100644 --- a/erpnext/accounts/report/tds_payable_monthly/tds_payable_monthly.js +++ b/erpnext/accounts/report/tds_payable_monthly/tds_payable_monthly.js @@ -33,7 +33,14 @@ frappe.query_reports["TDS Payable Monthly"] = { frappe.throw(__("Please select Party Type first")); } return party_type; - } + }, + "get_query": function() { + return { + "filters": { + "tax_withholding_category": ["!=",""], + } + } + }, }, { "fieldname":"from_date", diff --git a/erpnext/accounts/report/tds_payable_monthly/tds_payable_monthly.py b/erpnext/accounts/report/tds_payable_monthly/tds_payable_monthly.py index ddd049a11517..7d166614722e 100644 --- a/erpnext/accounts/report/tds_payable_monthly/tds_payable_monthly.py +++ b/erpnext/accounts/report/tds_payable_monthly/tds_payable_monthly.py @@ -7,19 +7,26 @@ def execute(filters=None): + if filters.get("party_type") == "Customer": + party_naming_by = frappe.db.get_single_value("Selling Settings", "cust_master_name") + else: + party_naming_by = frappe.db.get_single_value("Buying Settings", "supp_master_name") + + filters.update({"naming_series": party_naming_by}) + validate_filters(filters) ( tds_docs, tds_accounts, tax_category_map, journal_entry_party_map, - invoice_net_total_map, + net_total_map, ) = get_tds_docs(filters) columns = get_columns(filters) res = get_result( - filters, tds_docs, tds_accounts, tax_category_map, journal_entry_party_map, invoice_net_total_map + filters, tds_docs, tds_accounts, tax_category_map, journal_entry_party_map, net_total_map ) return columns, res @@ -31,7 +38,7 @@ def validate_filters(filters): def get_result( - filters, tds_docs, tds_accounts, tax_category_map, journal_entry_party_map, invoice_net_total_map + filters, tds_docs, tds_accounts, tax_category_map, journal_entry_party_map, net_total_map ): party_map = get_party_pan_map(filters.get("party_type")) tax_rate_map = get_tax_rate_map(filters) @@ -39,7 +46,7 @@ def get_result( out = [] for name, details in gle_map.items(): - tax_amount, total_amount = 0, 0 + tax_amount, total_amount, grand_total, base_total = 0, 0, 0, 0 tax_withholding_category = tax_category_map.get(name) rate = tax_rate_map.get(tax_withholding_category) @@ -60,8 +67,8 @@ def get_result( if entry.account in tds_accounts: tax_amount += entry.credit - entry.debit - if invoice_net_total_map.get(name): - total_amount = invoice_net_total_map.get(name) + if net_total_map.get(name): + total_amount, grand_total, base_total = net_total_map.get(name) else: total_amount += entry.credit @@ -69,15 +76,13 @@ def get_result( if party_map.get(party, {}).get("party_type") == "Supplier": party_name = "supplier_name" party_type = "supplier_type" - table_name = "Supplier" else: party_name = "customer_name" party_type = "customer_type" - table_name = "Customer" row = { "pan" - if frappe.db.has_column(table_name, "pan") + if frappe.db.has_column(filters.party_type, "pan") else "tax_id": party_map.get(party, {}).get("pan"), "party": party_map.get(party, {}).get("name"), } @@ -91,6 +96,8 @@ def get_result( "entity_type": party_map.get(party, {}).get(party_type), "rate": rate, "total_amount": total_amount, + "grand_total": grand_total, + "base_total": base_total, "tax_amount": tax_amount, "transaction_date": posting_date, "transaction_type": voucher_type, @@ -144,9 +151,9 @@ def get_gle_map(documents): def get_columns(filters): - pan = "pan" if frappe.db.has_column("Supplier", "pan") else "tax_id" + pan = "pan" if frappe.db.has_column(filters.party_type, "pan") else "tax_id" columns = [ - {"label": _(frappe.unscrub(pan)), "fieldname": pan, "fieldtype": "Data", "width": 90}, + {"label": _(frappe.unscrub(pan)), "fieldname": pan, "fieldtype": "Data", "width": 60}, { "label": _(filters.get("party_type")), "fieldname": "party", @@ -158,19 +165,36 @@ def get_columns(filters): if filters.naming_series == "Naming Series": columns.append( - {"label": _("Party Name"), "fieldname": "party_name", "fieldtype": "Data", "width": 180} + { + "label": _(filters.party_type + " Name"), + "fieldname": "party_name", + "fieldtype": "Data", + "width": 180, + } ) columns.extend( [ + { + "label": _("Date of Transaction"), + "fieldname": "transaction_date", + "fieldtype": "Date", + "width": 100, + }, { "label": _("Section Code"), "options": "Tax Withholding Category", "fieldname": "section_code", "fieldtype": "Link", - "width": 180, + "width": 90, + }, + {"label": _("Entity Type"), "fieldname": "entity_type", "fieldtype": "Data", "width": 100}, + { + "label": _("Total Amount"), + "fieldname": "total_amount", + "fieldtype": "Float", + "width": 90, }, - {"label": _("Entity Type"), "fieldname": "entity_type", "fieldtype": "Data", "width": 120}, { "label": _("TDS Rate %") if filters.get("party_type") == "Supplier" else _("TCS Rate %"), "fieldname": "rate", @@ -178,21 +202,21 @@ def get_columns(filters): "width": 90, }, { - "label": _("Total Amount"), - "fieldname": "total_amount", + "label": _("Tax Amount"), + "fieldname": "tax_amount", "fieldtype": "Float", "width": 90, }, { - "label": _("TDS Amount") if filters.get("party_type") == "Supplier" else _("TCS Amount"), - "fieldname": "tax_amount", + "label": _("Grand Total"), + "fieldname": "grand_total", "fieldtype": "Float", "width": 90, }, { - "label": _("Date of Transaction"), - "fieldname": "transaction_date", - "fieldtype": "Date", + "label": _("Base Total"), + "fieldname": "base_total", + "fieldtype": "Float", "width": 90, }, {"label": _("Transaction Type"), "fieldname": "transaction_type", "width": 100}, @@ -216,7 +240,7 @@ def get_tds_docs(filters): payment_entries = [] journal_entries = [] tax_category_map = frappe._dict() - invoice_net_total_map = frappe._dict() + net_total_map = frappe._dict() or_filters = frappe._dict() journal_entry_party_map = frappe._dict() bank_accounts = frappe.get_all("Account", {"is_group": 0, "account_type": "Bank"}, pluck="name") @@ -260,13 +284,13 @@ def get_tds_docs(filters): tds_documents.append(d.voucher_no) if purchase_invoices: - get_doc_info(purchase_invoices, "Purchase Invoice", tax_category_map, invoice_net_total_map) + get_doc_info(purchase_invoices, "Purchase Invoice", tax_category_map, net_total_map) if sales_invoices: - get_doc_info(sales_invoices, "Sales Invoice", tax_category_map, invoice_net_total_map) + get_doc_info(sales_invoices, "Sales Invoice", tax_category_map, net_total_map) if payment_entries: - get_doc_info(payment_entries, "Payment Entry", tax_category_map) + get_doc_info(payment_entries, "Payment Entry", tax_category_map, net_total_map) if journal_entries: journal_entry_party_map = get_journal_entry_party_map(journal_entries) @@ -277,7 +301,7 @@ def get_tds_docs(filters): tds_accounts, tax_category_map, journal_entry_party_map, - invoice_net_total_map, + net_total_map, ) @@ -295,11 +319,25 @@ def get_journal_entry_party_map(journal_entries): return journal_entry_party_map -def get_doc_info(vouchers, doctype, tax_category_map, invoice_net_total_map=None): +def get_doc_info(vouchers, doctype, tax_category_map, net_total_map=None): if doctype == "Purchase Invoice": - fields = ["name", "tax_withholding_category", "base_tax_withholding_net_total"] - if doctype == "Sales Invoice": - fields = ["name", "base_net_total"] + fields = [ + "name", + "tax_withholding_category", + "base_tax_withholding_net_total", + "grand_total", + "base_total", + ] + elif doctype == "Sales Invoice": + fields = ["name", "base_net_total", "grand_total", "base_total"] + elif doctype == "Payment Entry": + fields = [ + "name", + "tax_withholding_category", + "paid_amount", + "paid_amount_after_tax", + "base_paid_amount", + ] else: fields = ["name", "tax_withholding_category"] @@ -308,9 +346,15 @@ def get_doc_info(vouchers, doctype, tax_category_map, invoice_net_total_map=None for entry in entries: tax_category_map.update({entry.name: entry.tax_withholding_category}) if doctype == "Purchase Invoice": - invoice_net_total_map.update({entry.name: entry.base_tax_withholding_net_total}) - if doctype == "Sales Invoice": - invoice_net_total_map.update({entry.name: entry.base_net_total}) + net_total_map.update( + {entry.name: [entry.base_tax_withholding_net_total, entry.grand_total, entry.base_total]} + ) + elif doctype == "Sales Invoice": + net_total_map.update({entry.name: [entry.base_net_total, entry.grand_total, entry.base_total]}) + elif doctype == "Payment Entry": + net_total_map.update( + {entry.name: [entry.paid_amount, entry.paid_amount_after_tax, entry.base_paid_amount]} + ) def get_tax_rate_map(filters): From 716d5c0b98bee5dfcec02e1a84ab6b0343abc1ac Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Mon, 14 Aug 2023 18:55:16 +0530 Subject: [PATCH 168/501] fix: standard formula to calculate the "difference" (#36612) fix: standard formula to calculate the "difference" (#36612) (cherry picked from commit 843e77e72d17320fbf0f10bde4920ae600f62a40) Co-authored-by: HarryPaulo --- erpnext/accounts/doctype/pos_closing_entry/pos_closing_entry.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/accounts/doctype/pos_closing_entry/pos_closing_entry.js b/erpnext/accounts/doctype/pos_closing_entry/pos_closing_entry.js index a6c0102a7f91..91e71e90dd80 100644 --- a/erpnext/accounts/doctype/pos_closing_entry/pos_closing_entry.js +++ b/erpnext/accounts/doctype/pos_closing_entry/pos_closing_entry.js @@ -153,7 +153,7 @@ frappe.ui.form.on('POS Closing Entry', { frappe.ui.form.on('POS Closing Entry Detail', { closing_amount: (frm, cdt, cdn) => { const row = locals[cdt][cdn]; - frappe.model.set_value(cdt, cdn, "difference", flt(row.expected_amount - row.closing_amount)); + frappe.model.set_value(cdt, cdn, "difference", flt(row.closing_amount - row.expected_amount)); } }) From 83cbc1bef6fb8b9302c7d6ba747d648296edd8a3 Mon Sep 17 00:00:00 2001 From: Kevin Shenk Date: Mon, 14 Aug 2023 09:33:01 -0400 Subject: [PATCH 169/501] fix: Document Name link validation in Bank Reconciliation Tool (#36495) fix: format_row broke Document Name link validation #35540 broke Voucher Matching, leading to an invalid link exception on submission. This is because the format_row() function overwrites the row data instead of just providing a formatter on the DataTable column, and therefore passes through the formatted (linked) column data instead of the Document Name only. This patch moves the appropriate frappe.form.formatters.Link function to a dedicated format hook on the DataTable columns definition, both fixing the error and retaining the functionality of #35540. (cherry picked from commit 7ab55b1bb293517aaac11193f25efdf6ccbc7bb4) # Conflicts: # erpnext/public/js/bank_reconciliation_tool/dialog_manager.js --- .../bank_reconciliation_tool/dialog_manager.js | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/erpnext/public/js/bank_reconciliation_tool/dialog_manager.js b/erpnext/public/js/bank_reconciliation_tool/dialog_manager.js index 1271e38049a0..4e2de8b0ff8e 100644 --- a/erpnext/public/js/bank_reconciliation_tool/dialog_manager.js +++ b/erpnext/public/js/bank_reconciliation_tool/dialog_manager.js @@ -117,6 +117,9 @@ erpnext.accounts.bank_reconciliation.DialogManager = class DialogManager { name: __("Document Name"), editable: false, width: 1, + format: (value, row) => { + return frappe.form.formatters.Link(value, {options: row[2].content}); + }, }, { name: __("Reference Date"), @@ -141,6 +144,20 @@ erpnext.accounts.bank_reconciliation.DialogManager = class DialogManager { ]; } +<<<<<<< HEAD +======= + format_row(row) { + return [ + row[1], // Document Type + row[2], // Document Name + row[5] || row[8], // Reference Date + format_currency(row[3], row[9]), // Remaining + row[4], // Reference Number + row[6], // Party + ]; + } + +>>>>>>> 7ab55b1bb2 (fix: Document Name link validation in Bank Reconciliation Tool (#36495)) get_datatable(proposals_wrapper) { if (!this.datatable) { const datatable_options = { From ca34b63470283360dda1a6ac05ec5bd88ed9533c Mon Sep 17 00:00:00 2001 From: Devin Slauenwhite Date: Mon, 14 Aug 2023 12:14:03 -0400 Subject: [PATCH 170/501] feat: Reallow customizing company abbreviation on setup. (#36646) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Bernd Oliver Sünderhauf --- erpnext/public/js/setup_wizard.js | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/erpnext/public/js/setup_wizard.js b/erpnext/public/js/setup_wizard.js index a913844e1867..934fd1f88ae2 100644 --- a/erpnext/public/js/setup_wizard.js +++ b/erpnext/public/js/setup_wizard.js @@ -24,12 +24,14 @@ erpnext.setup.slides_settings = [ fieldtype: 'Data', reqd: 1 }, + { fieldtype: "Column Break" }, { fieldname: 'company_abbr', label: __('Company Abbreviation'), fieldtype: 'Data', - hidden: 1 + reqd: 1 }, + { fieldtype: "Section Break" }, { fieldname: 'chart_of_accounts', label: __('Chart of Accounts'), options: "", fieldtype: 'Select' @@ -134,18 +136,20 @@ erpnext.setup.slides_settings = [ me.charts_modal(slide, chart_template); }); - slide.get_input("company_name").on("change", function () { + slide.get_input("company_name").on("input", function () { let parts = slide.get_input("company_name").val().split(" "); let abbr = $.map(parts, function (p) { return p ? p.substr(0, 1) : null }).join(""); slide.get_field("company_abbr").set_value(abbr.slice(0, 10).toUpperCase()); }).val(frappe.boot.sysdefaults.company_name || "").trigger("change"); slide.get_input("company_abbr").on("change", function () { - if (slide.get_input("company_abbr").val().length > 10) { + let abbr = slide.get_input("company_abbr").val(); + if (abbr.length > 10) { frappe.msgprint(__("Company Abbreviation cannot have more than 5 characters")); - slide.get_field("company_abbr").set_value(""); + abbr = abbr.slice(0, 10); } - }); + slide.get_field("company_abbr").set_value(abbr); + }).val(frappe.boot.sysdefaults.company_abbr || "").trigger("change"); }, charts_modal: function(slide, chart_template) { From 3a82eb4ccf00cd1a34a52703e534eb476b2b56b5 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Mon, 14 Aug 2023 17:38:44 +0530 Subject: [PATCH 171/501] refactor: toggle for negative rates in Selling Settings (cherry picked from commit a0fc68538fbfd941f2d26741770b037b89dea36a) # Conflicts: # erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py # erpnext/patches.txt # erpnext/selling/doctype/selling_settings/selling_settings.json --- .../sales_invoice/test_sales_invoice.py | 60 +++++++++++++++++++ erpnext/controllers/status_updater.py | 15 ++++- erpnext/patches.txt | 21 ++++++- .../selling_settings/selling_settings.json | 18 +++++- 4 files changed, 109 insertions(+), 5 deletions(-) diff --git a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py index 277e584aeaf6..34f35af5d570 100644 --- a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py +++ b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py @@ -3316,6 +3316,66 @@ def test_sales_invoice_with_payable_tax_account(self): ) self.assertRaises(frappe.ValidationError, si.submit) +<<<<<<< HEAD +======= + def test_advance_entries_as_liability(self): + from erpnext.accounts.doctype.payment_entry.test_payment_entry import create_payment_entry + + account = create_account( + parent_account="Current Liabilities - _TC", + account_name="Advances Received", + company="_Test Company", + account_type="Receivable", + ) + + set_advance_flag(company="_Test Company", flag=1, default_account=account) + + pe = create_payment_entry( + company="_Test Company", + payment_type="Receive", + party_type="Customer", + party="_Test Customer", + paid_from="Debtors - _TC", + paid_to="Cash - _TC", + paid_amount=1000, + ) + pe.submit() + + si = create_sales_invoice( + company="_Test Company", + customer="_Test Customer", + do_not_save=True, + do_not_submit=True, + rate=500, + price_list_rate=500, + ) + si.base_grand_total = 500 + si.grand_total = 500 + si.set_advances() + for advance in si.advances: + advance.allocated_amount = 500 if advance.reference_name == pe.name else 0 + si.save() + si.submit() + + self.assertEqual(si.advances[0].allocated_amount, 500) + + # Check GL Entry against payment doctype + expected_gle = [ + ["Advances Received - _TC", 500, 0.0, nowdate()], + ["Cash - _TC", 1000, 0.0, nowdate()], + ["Debtors - _TC", 0.0, 1000, nowdate()], + ["Debtors - _TC", 0.0, 500, nowdate()], + ] + + check_gl_entries(self, pe.name, expected_gle, nowdate(), voucher_type="Payment Entry") + + si.load_from_db() + self.assertEqual(si.outstanding_amount, 0) + + set_advance_flag(company="_Test Company", flag=0, default_account="") + + @change_settings("Selling Settings", {"allow_negative_rates_for_items": 0}) +>>>>>>> a0fc68538f (refactor: toggle for negative rates in Selling Settings) def test_sales_return_negative_rate(self): si = create_sales_invoice(is_return=1, qty=-2, rate=-10, do_not_save=True) self.assertRaises(frappe.ValidationError, si.save) diff --git a/erpnext/controllers/status_updater.py b/erpnext/controllers/status_updater.py index f3663cc52711..73a248fb531d 100644 --- a/erpnext/controllers/status_updater.py +++ b/erpnext/controllers/status_updater.py @@ -5,7 +5,7 @@ import frappe from frappe import _ from frappe.model.document import Document -from frappe.utils import comma_or, flt, getdate, now, nowdate +from frappe.utils import comma_or, flt, get_link_to_form, getdate, now, nowdate class OverAllowanceError(frappe.ValidationError): @@ -233,8 +233,17 @@ def validate_qty(self): if hasattr(d, "qty") and d.qty > 0 and self.get("is_return"): frappe.throw(_("For an item {0}, quantity must be negative number").format(d.item_code)) - if hasattr(d, "item_code") and hasattr(d, "rate") and flt(d.rate) < 0: - frappe.throw(_("For an item {0}, rate must be a positive number").format(d.item_code)) + if not frappe.db.get_single_value("Selling Settings", "allow_negative_rates_for_items"): + if hasattr(d, "item_code") and hasattr(d, "rate") and flt(d.rate) < 0: + frappe.throw( + _( + "For item {0}, rate must be a positive number. To Allow negative rates, enable {1} in {2}" + ).format( + frappe.bold(d.item_code), + frappe.bold(_("`Allow Negative rates for Items`")), + get_link_to_form("Selling Settings", "Selling Settings"), + ), + ) if d.doctype == args["source_dt"] and d.get(args["join_field"]): args["name"] = d.get(args["join_field"]) diff --git a/erpnext/patches.txt b/erpnext/patches.txt index 75f728afa88b..0215f0830f35 100644 --- a/erpnext/patches.txt +++ b/erpnext/patches.txt @@ -328,13 +328,32 @@ erpnext.patches.v14_0.set_pick_list_status erpnext.patches.v13_0.update_docs_link erpnext.patches.v14_0.enable_all_leads execute:frappe.db.set_single_value("Accounts Settings", "merge_similar_account_heads", 0) +<<<<<<< HEAD # below migration patches should always run last erpnext.patches.v14_0.migrate_gl_to_payment_ledger +======= +erpnext.patches.v14_0.update_reference_type_in_journal_entry_accounts +erpnext.patches.v14_0.update_subscription_details +execute:frappe.delete_doc_if_exists("Report", "Tax Detail") +erpnext.patches.v15_0.enable_all_leads +>>>>>>> a0fc68538f (refactor: toggle for negative rates in Selling Settings) erpnext.patches.v14_0.update_company_in_ldc erpnext.patches.v14_0.set_packed_qty_in_draft_delivery_notes erpnext.patches.v14_0.cleanup_workspaces +<<<<<<< HEAD erpnext.patches.v14_0.enable_allow_existing_serial_no erpnext.patches.v14_0.set_report_in_process_SOA erpnext.patches.v14_0.create_accounting_dimensions_for_closing_balance erpnext.patches.v14_0.update_closing_balances #15-07-2023 -execute:frappe.defaults.clear_default("fiscal_year") \ No newline at end of file +execute:frappe.defaults.clear_default("fiscal_year") +======= +erpnext.patches.v15_0.remove_loan_management_module #2023-07-03 +erpnext.patches.v14_0.set_report_in_process_SOA +erpnext.buying.doctype.supplier.patches.migrate_supplier_portal_users +execute:frappe.defaults.clear_default("fiscal_year") +erpnext.patches.v15_0.remove_exotel_integration +erpnext.patches.v14_0.single_to_multi_dunning +execute:frappe.db.set_single_value('Selling Settings', 'allow_negative_rates_for_items', 0) +# below migration patch should always run last +erpnext.patches.v14_0.migrate_gl_to_payment_ledger +>>>>>>> a0fc68538f (refactor: toggle for negative rates in Selling Settings) diff --git a/erpnext/selling/doctype/selling_settings/selling_settings.json b/erpnext/selling/doctype/selling_settings/selling_settings.json index af148c51fb97..3417afa1282f 100644 --- a/erpnext/selling/doctype/selling_settings/selling_settings.json +++ b/erpnext/selling/doctype/selling_settings/selling_settings.json @@ -20,6 +20,7 @@ "editable_price_list_rate", "validate_selling_price", "editable_bundle_item_rates", + "allow_negative_rates_for_items", "sales_transactions_settings_section", "so_required", "dn_required", @@ -186,6 +187,21 @@ "fieldname": "over_order_allowance", "fieldtype": "Float", "label": "Over Order Allowance (%)" +<<<<<<< HEAD +======= + }, + { + "default": "0", + "fieldname": "dont_reserve_sales_order_qty_on_sales_return", + "fieldtype": "Check", + "label": "Don't Reserve Sales Order Qty on Sales Return" + }, + { + "default": "0", + "fieldname": "allow_negative_rates_for_items", + "fieldtype": "Check", + "label": "Allow Negative rates for Items" +>>>>>>> a0fc68538f (refactor: toggle for negative rates in Selling Settings) } ], "icon": "fa fa-cog", @@ -193,7 +209,7 @@ "index_web_pages_for_search": 1, "issingle": 1, "links": [], - "modified": "2023-08-09 15:35:42.914354", + "modified": "2023-08-14 20:33:05.693667", "modified_by": "Administrator", "module": "Selling", "name": "Selling Settings", From e55c160438c80491c88af8a31e3cc8f4cf4bec37 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Tue, 15 Aug 2023 08:26:04 +0530 Subject: [PATCH 172/501] chore: resolve conflicts --- .../sales_invoice/test_sales_invoice.py | 59 ------------------- erpnext/patches.txt | 18 ------ .../selling_settings/selling_settings.json | 9 --- 3 files changed, 86 deletions(-) diff --git a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py index 34f35af5d570..14d43a14a6a5 100644 --- a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py +++ b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py @@ -3316,66 +3316,7 @@ def test_sales_invoice_with_payable_tax_account(self): ) self.assertRaises(frappe.ValidationError, si.submit) -<<<<<<< HEAD -======= - def test_advance_entries_as_liability(self): - from erpnext.accounts.doctype.payment_entry.test_payment_entry import create_payment_entry - - account = create_account( - parent_account="Current Liabilities - _TC", - account_name="Advances Received", - company="_Test Company", - account_type="Receivable", - ) - - set_advance_flag(company="_Test Company", flag=1, default_account=account) - - pe = create_payment_entry( - company="_Test Company", - payment_type="Receive", - party_type="Customer", - party="_Test Customer", - paid_from="Debtors - _TC", - paid_to="Cash - _TC", - paid_amount=1000, - ) - pe.submit() - - si = create_sales_invoice( - company="_Test Company", - customer="_Test Customer", - do_not_save=True, - do_not_submit=True, - rate=500, - price_list_rate=500, - ) - si.base_grand_total = 500 - si.grand_total = 500 - si.set_advances() - for advance in si.advances: - advance.allocated_amount = 500 if advance.reference_name == pe.name else 0 - si.save() - si.submit() - - self.assertEqual(si.advances[0].allocated_amount, 500) - - # Check GL Entry against payment doctype - expected_gle = [ - ["Advances Received - _TC", 500, 0.0, nowdate()], - ["Cash - _TC", 1000, 0.0, nowdate()], - ["Debtors - _TC", 0.0, 1000, nowdate()], - ["Debtors - _TC", 0.0, 500, nowdate()], - ] - - check_gl_entries(self, pe.name, expected_gle, nowdate(), voucher_type="Payment Entry") - - si.load_from_db() - self.assertEqual(si.outstanding_amount, 0) - - set_advance_flag(company="_Test Company", flag=0, default_account="") - @change_settings("Selling Settings", {"allow_negative_rates_for_items": 0}) ->>>>>>> a0fc68538f (refactor: toggle for negative rates in Selling Settings) def test_sales_return_negative_rate(self): si = create_sales_invoice(is_return=1, qty=-2, rate=-10, do_not_save=True) self.assertRaises(frappe.ValidationError, si.save) diff --git a/erpnext/patches.txt b/erpnext/patches.txt index 0215f0830f35..693333bb5db6 100644 --- a/erpnext/patches.txt +++ b/erpnext/patches.txt @@ -328,32 +328,14 @@ erpnext.patches.v14_0.set_pick_list_status erpnext.patches.v13_0.update_docs_link erpnext.patches.v14_0.enable_all_leads execute:frappe.db.set_single_value("Accounts Settings", "merge_similar_account_heads", 0) -<<<<<<< HEAD -# below migration patches should always run last -erpnext.patches.v14_0.migrate_gl_to_payment_ledger -======= -erpnext.patches.v14_0.update_reference_type_in_journal_entry_accounts -erpnext.patches.v14_0.update_subscription_details -execute:frappe.delete_doc_if_exists("Report", "Tax Detail") -erpnext.patches.v15_0.enable_all_leads ->>>>>>> a0fc68538f (refactor: toggle for negative rates in Selling Settings) erpnext.patches.v14_0.update_company_in_ldc erpnext.patches.v14_0.set_packed_qty_in_draft_delivery_notes erpnext.patches.v14_0.cleanup_workspaces -<<<<<<< HEAD erpnext.patches.v14_0.enable_allow_existing_serial_no erpnext.patches.v14_0.set_report_in_process_SOA erpnext.patches.v14_0.create_accounting_dimensions_for_closing_balance erpnext.patches.v14_0.update_closing_balances #15-07-2023 execute:frappe.defaults.clear_default("fiscal_year") -======= -erpnext.patches.v15_0.remove_loan_management_module #2023-07-03 -erpnext.patches.v14_0.set_report_in_process_SOA -erpnext.buying.doctype.supplier.patches.migrate_supplier_portal_users -execute:frappe.defaults.clear_default("fiscal_year") -erpnext.patches.v15_0.remove_exotel_integration -erpnext.patches.v14_0.single_to_multi_dunning execute:frappe.db.set_single_value('Selling Settings', 'allow_negative_rates_for_items', 0) # below migration patch should always run last erpnext.patches.v14_0.migrate_gl_to_payment_ledger ->>>>>>> a0fc68538f (refactor: toggle for negative rates in Selling Settings) diff --git a/erpnext/selling/doctype/selling_settings/selling_settings.json b/erpnext/selling/doctype/selling_settings/selling_settings.json index 3417afa1282f..46bdcfa5f15c 100644 --- a/erpnext/selling/doctype/selling_settings/selling_settings.json +++ b/erpnext/selling/doctype/selling_settings/selling_settings.json @@ -187,21 +187,12 @@ "fieldname": "over_order_allowance", "fieldtype": "Float", "label": "Over Order Allowance (%)" -<<<<<<< HEAD -======= - }, - { - "default": "0", - "fieldname": "dont_reserve_sales_order_qty_on_sales_return", - "fieldtype": "Check", - "label": "Don't Reserve Sales Order Qty on Sales Return" }, { "default": "0", "fieldname": "allow_negative_rates_for_items", "fieldtype": "Check", "label": "Allow Negative rates for Items" ->>>>>>> a0fc68538f (refactor: toggle for negative rates in Selling Settings) } ], "icon": "fa fa-cog", From 33d5250cec18fa42850184021aa1da79cf3f4317 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Tue, 15 Aug 2023 18:27:01 +0530 Subject: [PATCH 173/501] chore: add validation for depreciation expense account in asset category (backport #36659) (#36661) chore: add validation for depreciation expense account in asset category (#36659) (cherry picked from commit e0c79d3b53399e336bf6ff45489751b7d6c58f6a) Co-authored-by: Anand Baburajan --- erpnext/assets/doctype/asset_category/asset_category.js | 1 + erpnext/assets/doctype/asset_category/asset_category.py | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/erpnext/assets/doctype/asset_category/asset_category.js b/erpnext/assets/doctype/asset_category/asset_category.js index c702687072d9..7dde14ea0e61 100644 --- a/erpnext/assets/doctype/asset_category/asset_category.js +++ b/erpnext/assets/doctype/asset_category/asset_category.js @@ -33,6 +33,7 @@ frappe.ui.form.on('Asset Category', { var d = locals[cdt][cdn]; return { "filters": { + "account_type": "Depreciation", "root_type": ["in", ["Expense", "Income"]], "is_group": 0, "company": d.company_name diff --git a/erpnext/assets/doctype/asset_category/asset_category.py b/erpnext/assets/doctype/asset_category/asset_category.py index 2e1def98fc3f..8d351412ca81 100644 --- a/erpnext/assets/doctype/asset_category/asset_category.py +++ b/erpnext/assets/doctype/asset_category/asset_category.py @@ -53,7 +53,7 @@ def validate_account_types(self): account_type_map = { "fixed_asset_account": {"account_type": ["Fixed Asset"]}, "accumulated_depreciation_account": {"account_type": ["Accumulated Depreciation"]}, - "depreciation_expense_account": {"root_type": ["Expense", "Income"]}, + "depreciation_expense_account": {"account_type": ["Depreciation"]}, "capital_work_in_progress_account": {"account_type": ["Capital Work in Progress"]}, } for d in self.accounts: From 99777d3fa40e6a3225a33321ede92da759312553 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Wed, 16 Aug 2023 08:09:04 +0530 Subject: [PATCH 174/501] fix: re-add permission that was unintentionally removed (#36663) fix: re-add permission that was unintentionally removed Remove `Reversal OF ITC` and re-add permissions. Both of them unintended changes (cherry picked from commit 45662fa646cbd861d89e827bc557a3f2a25964b1) Co-authored-by: ruthra kumar --- .../doctype/journal_entry/journal_entry.json | 40 ++++++++++++++++++- 1 file changed, 39 insertions(+), 1 deletion(-) diff --git a/erpnext/accounts/doctype/journal_entry/journal_entry.json b/erpnext/accounts/doctype/journal_entry/journal_entry.json index cff84dc9252c..2eb54a54d541 100644 --- a/erpnext/accounts/doctype/journal_entry/journal_entry.json +++ b/erpnext/accounts/doctype/journal_entry/journal_entry.json @@ -555,7 +555,45 @@ "name": "Journal Entry", "naming_rule": "By \"Naming Series\" field", "owner": "Administrator", - "permissions": [], + "permissions": [ + { + "amend": 1, + "cancel": 1, + "create": 1, + "delete": 1, + "email": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "Accounts User", + "share": 1, + "submit": 1, + "write": 1 + }, + { + "amend": 1, + "cancel": 1, + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "import": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "Accounts Manager", + "share": 1, + "submit": 1, + "write": 1 + }, + { + "email": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "Auditor" + } + ], "search_fields": "voucher_type,posting_date, due_date, cheque_no", "sort_field": "modified", "sort_order": "DESC", From 1deebe87574115a563b99dd04442f1b599150e09 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Wed, 16 Aug 2023 08:09:38 +0530 Subject: [PATCH 175/501] fix: Tax withholding post LDC limit consumed (#36611) * fix: Tax withholding post LDC limit consumed (#36611) * fix: Tax withholding post LDC limit consumed * fix: LDC condition check (cherry picked from commit 985ff9781b9f18f7d2da55acaecab8ebf3c51bb7) # Conflicts: # erpnext/accounts/doctype/tax_withholding_category/test_tax_withholding_category.py * chore: resolve conflicts * chore: linting issues --------- Co-authored-by: Deepesh Garg --- .../tax_withholding_category.py | 62 +++++++--------- .../test_tax_withholding_category.py | 73 +++++++++++++++++++ 2 files changed, 100 insertions(+), 35 deletions(-) diff --git a/erpnext/accounts/doctype/tax_withholding_category/tax_withholding_category.py b/erpnext/accounts/doctype/tax_withholding_category/tax_withholding_category.py index e66a886bf9a1..d17ca08c408d 100644 --- a/erpnext/accounts/doctype/tax_withholding_category/tax_withholding_category.py +++ b/erpnext/accounts/doctype/tax_withholding_category/tax_withholding_category.py @@ -262,14 +262,20 @@ def get_tax_amount(party_type, parties, inv, tax_details, posting_date, pan_no=N if tax_deducted: net_total = inv.tax_withholding_net_total if ldc: - tax_amount = get_tds_amount_from_ldc(ldc, parties, tax_details, posting_date, net_total) + limit_consumed = get_limit_consumed(ldc, parties) + if is_valid_certificate(ldc, posting_date, limit_consumed): + tax_amount = get_lower_deduction_amount( + net_total, limit_consumed, ldc.certificate_limit, ldc.rate, tax_details + ) + else: + tax_amount = net_total * tax_details.rate / 100 if net_total > 0 else 0 else: tax_amount = net_total * tax_details.rate / 100 if net_total > 0 else 0 # once tds is deducted, not need to add vouchers in the invoice voucher_wise_amount = {} else: - tax_amount = get_tds_amount(ldc, parties, inv, tax_details, tax_deducted, vouchers) + tax_amount = get_tds_amount(ldc, parties, inv, tax_details, vouchers) elif party_type == "Customer": if tax_deducted: @@ -416,7 +422,7 @@ def get_deducted_tax(taxable_vouchers, tax_details): return sum(entries) -def get_tds_amount(ldc, parties, inv, tax_details, tax_deducted, vouchers): +def get_tds_amount(ldc, parties, inv, tax_details, vouchers): tds_amount = 0 invoice_filters = {"name": ("in", vouchers), "docstatus": 1, "apply_tds": 1} @@ -496,15 +502,10 @@ def get_tds_amount(ldc, parties, inv, tax_details, tax_deducted, vouchers): net_total += inv.tax_withholding_net_total supp_credit_amt = net_total - cumulative_threshold - if ldc and is_valid_certificate( - ldc.valid_from, - ldc.valid_upto, - inv.get("posting_date") or inv.get("transaction_date"), - tax_deducted, - inv.tax_withholding_net_total, - ldc.certificate_limit, - ): - tds_amount = get_ltds_amount(supp_credit_amt, 0, ldc.certificate_limit, ldc.rate, tax_details) + if ldc and is_valid_certificate(ldc, inv.get("posting_date") or inv.get("transaction_date"), 0): + tds_amount = get_lower_deduction_amount( + supp_credit_amt, 0, ldc.certificate_limit, ldc.rate, tax_details + ) else: tds_amount = supp_credit_amt * tax_details.rate / 100 if supp_credit_amt > 0 else 0 @@ -582,8 +583,7 @@ def get_invoice_total_without_tcs(inv, tax_details): return inv.grand_total - tcs_tax_row_amount -def get_tds_amount_from_ldc(ldc, parties, tax_details, posting_date, net_total): - tds_amount = 0 +def get_limit_consumed(ldc, parties): limit_consumed = frappe.db.get_value( "Purchase Invoice", { @@ -597,37 +597,29 @@ def get_tds_amount_from_ldc(ldc, parties, tax_details, posting_date, net_total): "sum(tax_withholding_net_total)", ) - if is_valid_certificate( - ldc.valid_from, ldc.valid_upto, posting_date, limit_consumed, net_total, ldc.certificate_limit - ): - tds_amount = get_ltds_amount( - net_total, limit_consumed, ldc.certificate_limit, ldc.rate, tax_details - ) - - return tds_amount + return limit_consumed -def get_ltds_amount(current_amount, deducted_amount, certificate_limit, rate, tax_details): - if certificate_limit - flt(deducted_amount) - flt(current_amount) >= 0: +def get_lower_deduction_amount( + current_amount, limit_consumed, certificate_limit, rate, tax_details +): + if certificate_limit - flt(limit_consumed) - flt(current_amount) >= 0: return current_amount * rate / 100 else: - ltds_amount = certificate_limit - flt(deducted_amount) + ltds_amount = certificate_limit - flt(limit_consumed) tds_amount = current_amount - ltds_amount return ltds_amount * rate / 100 + tds_amount * tax_details.rate / 100 -def is_valid_certificate( - valid_from, valid_upto, posting_date, deducted_amount, current_amount, certificate_limit -): - valid = False - - available_amount = flt(certificate_limit) - flt(deducted_amount) - - if (getdate(valid_from) <= getdate(posting_date) <= getdate(valid_upto)) and available_amount > 0: - valid = True +def is_valid_certificate(ldc, posting_date, limit_consumed): + available_amount = flt(ldc.certificate_limit) - flt(limit_consumed) + if ( + getdate(ldc.valid_from) <= getdate(posting_date) <= getdate(ldc.valid_upto) + ) and available_amount > 0: + return True - return valid + return False def normal_round(number): diff --git a/erpnext/accounts/doctype/tax_withholding_category/test_tax_withholding_category.py b/erpnext/accounts/doctype/tax_withholding_category/test_tax_withholding_category.py index f8e0e2992f74..0a749f966520 100644 --- a/erpnext/accounts/doctype/tax_withholding_category/test_tax_withholding_category.py +++ b/erpnext/accounts/doctype/tax_withholding_category/test_tax_withholding_category.py @@ -4,6 +4,7 @@ import unittest import frappe +from frappe.custom.doctype.custom_field.custom_field import create_custom_fields from frappe.tests.utils import change_settings from frappe.utils import today @@ -18,6 +19,7 @@ def setUpClass(self): # create relevant supplier, etc create_records() create_tax_withholding_category_records() + make_pan_no_field() def tearDown(self): cancel_invoices() @@ -456,6 +458,40 @@ def test_tax_withholding_via_payment_entry_for_advances(self): pe2.cancel() pe3.cancel() + def test_lower_deduction_certificate_application(self): + frappe.db.set_value( + "Supplier", + "Test LDC Supplier", + { + "tax_withholding_category": "Test Service Category", + "pan": "ABCTY1234D", + }, + ) + + create_lower_deduction_certificate( + supplier="Test LDC Supplier", + certificate_no="1AE0423AAJ", + tax_withholding_category="Test Service Category", + tax_rate=2, + limit=50000, + ) + + pi1 = create_purchase_invoice(supplier="Test LDC Supplier", rate=35000) + pi1.submit() + self.assertEqual(pi1.taxes[0].tax_amount, 700) + + pi2 = create_purchase_invoice(supplier="Test LDC Supplier", rate=35000) + pi2.submit() + self.assertEqual(pi2.taxes[0].tax_amount, 2300) + + pi3 = create_purchase_invoice(supplier="Test LDC Supplier", rate=35000) + pi3.submit() + self.assertEqual(pi3.taxes[0].tax_amount, 3500) + + pi1.cancel() + pi2.cancel() + pi3.cancel() + def cancel_invoices(): purchase_invoices = frappe.get_all( @@ -615,6 +651,7 @@ def create_records(): "Test TDS Supplier6", "Test TDS Supplier7", "Test TDS Supplier8", + "Test LDC Supplier", ]: if frappe.db.exists("Supplier", name): continue @@ -811,3 +848,39 @@ def create_tax_withholding_category( "accounts": [{"company": "_Test Company", "account": account}], } ).insert() + + +def create_lower_deduction_certificate( + supplier, tax_withholding_category, tax_rate, certificate_no, limit +): + fiscal_year = get_fiscal_year(today(), company="_Test Company") + if not frappe.db.exists("Lower Deduction Certificate", certificate_no): + frappe.get_doc( + { + "doctype": "Lower Deduction Certificate", + "company": "_Test Company", + "supplier": supplier, + "certificate_no": certificate_no, + "tax_withholding_category": tax_withholding_category, + "fiscal_year": fiscal_year[0], + "valid_from": fiscal_year[1], + "valid_upto": fiscal_year[2], + "rate": tax_rate, + "certificate_limit": limit, + } + ).insert() + + +def make_pan_no_field(): + pan_field = { + "Supplier": [ + { + "fieldname": "pan", + "label": "PAN", + "fieldtype": "Data", + "translatable": 0, + } + ] + } + + create_custom_fields(pan_field, update=1) From b773465b419661953462c8689bf82b8680ddb93d Mon Sep 17 00:00:00 2001 From: Frappe PR Bot Date: Wed, 16 Aug 2023 06:34:09 +0000 Subject: [PATCH 176/501] chore(release): Bumped to Version 14.35.0 # [14.35.0](https://github.com/frappe/erpnext/compare/v14.34.3...v14.35.0) (2023-08-16) ### Bug Fixes * Allow backdated repayment cancels for term loans ([1377cf4](https://github.com/frappe/erpnext/commit/1377cf4cf1bfad982f91c0b65d3ffaf28ed24dd1)) * allow negative stock condition for batch item ([#36586](https://github.com/frappe/erpnext/issues/36586)) ([ee04c6d](https://github.com/frappe/erpnext/commit/ee04c6d5c52428f6f8e28fdf1a4e25075bee59ff)) * AR/AP report based on payment terms ([#36574](https://github.com/frappe/erpnext/issues/36574)) ([ac0fff7](https://github.com/frappe/erpnext/commit/ac0fff7e94598abac128c5ed7e2ed33de98b7be2)) * better remarks on Cr note created by Reconciliation ([5443592](https://github.com/frappe/erpnext/commit/5443592d84c180e27168afb624d43e36645f42f8)) * Button Alignment center in hero slider ([#36607](https://github.com/frappe/erpnext/issues/36607)) ([b131f70](https://github.com/frappe/erpnext/commit/b131f70ed6a8e3b763d59106084440e08a73b815)), closes [#36561](https://github.com/frappe/erpnext/issues/36561) * cr/dr note should be posted for exc gain/loss ([349601b](https://github.com/frappe/erpnext/commit/349601b4b971010cfc7d260c644c9ec7b95924a3)) * disallow mulitple SO with same PO No ([b901cfd](https://github.com/frappe/erpnext/commit/b901cfdbe25e53b80f53391560a58e6acfdc104c)) * don't show disabled items in `Item Shortage Report` (backport [#36550](https://github.com/frappe/erpnext/issues/36550)) ([#36571](https://github.com/frappe/erpnext/issues/36571)) ([19cfcea](https://github.com/frappe/erpnext/commit/19cfcea78efa065c4ab1daa23f71da240d1e0ec0)) * fetch `Stock UOM` from Item if not set (backport [#36606](https://github.com/frappe/erpnext/issues/36606)) ([#36617](https://github.com/frappe/erpnext/issues/36617)) ([8b13185](https://github.com/frappe/erpnext/commit/8b13185c25786afd05df0aa9dc56b2494450bc74)) * Group Account total not showing in Financial Statements ([2912648](https://github.com/frappe/erpnext/commit/291264815196fdbec70fcd667db760a9017bb40e)) * incorrect available qty for backdated stock reco with batch ([#36581](https://github.com/frappe/erpnext/issues/36581)) ([2800ad3](https://github.com/frappe/erpnext/commit/2800ad39d2adc40b932b4626a1be4df89916973c)) * incorrect gain/loss on allocation change on reconciliation tool ([39c439d](https://github.com/frappe/erpnext/commit/39c439dc4b8dc7f054644917141ecfd861656e12)) * Make default sales update frequency as monthly instead of each transaction ([4ca1f3b](https://github.com/frappe/erpnext/commit/4ca1f3b9cf69b00a80499b5d7d59a36a00521bf6)) * move company rename to long queue ([8083c0b](https://github.com/frappe/erpnext/commit/8083c0b59e3e371a39f18280edd092e985e5d0cd)) * precision issue while submitting the stock entry ([#36575](https://github.com/frappe/erpnext/issues/36575)) ([a864e07](https://github.com/frappe/erpnext/commit/a864e07d4ff2cf2d22754ad9d23a2a3718ece770)) * re-add permission that was unintentionally removed ([#36663](https://github.com/frappe/erpnext/issues/36663)) ([99777d3](https://github.com/frappe/erpnext/commit/99777d3fa40e6a3225a33321ede92da759312553)) * **RFQ:** link to supplier portal ([eb2f68e](https://github.com/frappe/erpnext/commit/eb2f68ec987151cc355312947dda4f9ea24e23fe)) * standard formula to calculate the "difference" ([#36612](https://github.com/frappe/erpnext/issues/36612)) ([716d5c0](https://github.com/frappe/erpnext/commit/716d5c0b98bee5dfcec02e1a84ab6b0343abc1ac)) * Tax withholding post LDC limit consumed ([#36611](https://github.com/frappe/erpnext/issues/36611)) ([1deebe8](https://github.com/frappe/erpnext/commit/1deebe87574115a563b99dd04442f1b599150e09)) * **test:** replace hardcoded reference to adv with dynamic one ([2e6bfa3](https://github.com/frappe/erpnext/commit/2e6bfa36de244e2944cd2ff6d42fff4e6ba3cbb8)) * **test:** test case breakage in Github Actions ([3542df7](https://github.com/frappe/erpnext/commit/3542df70f68e76cc9c7b806900b723b4258e5e94)) * unhide `uom` and `stock_uom` fields in print view ([b49309c](https://github.com/frappe/erpnext/commit/b49309c160fe20af7a7ee6fdccdc8e07382b4e76)) * validation blocks partial payment for SO and PO ([ce08f03](https://github.com/frappe/erpnext/commit/ce08f038d276782e58d82155b8122dd5423998dc)) * wrap none type rate under flt (backport [#36602](https://github.com/frappe/erpnext/issues/36602)) ([#36604](https://github.com/frappe/erpnext/issues/36604)) ([63c061e](https://github.com/frappe/erpnext/commit/63c061e5e823cb05c3fb2264c032bec7e90955a4)) * wrong currency on financial-statement based reports ([#36524](https://github.com/frappe/erpnext/issues/36524)) ([3044f46](https://github.com/frappe/erpnext/commit/3044f46c52a3ea7cf5bcbfb4abc88bc400aea4a4)) ### Features * add voucher totals in tds payable report ([#36568](https://github.com/frappe/erpnext/issues/36568)) ([90b390c](https://github.com/frappe/erpnext/commit/90b390c2c5554b779da4989e9d4475039681fa9b)) * daily asset depreciation method ([#36587](https://github.com/frappe/erpnext/issues/36587)) ([2918127](https://github.com/frappe/erpnext/commit/29181274c8591c3028d556e3ded2c6e210c6cf12)) * Reallow customizing company abbreviation on setup. ([#36646](https://github.com/frappe/erpnext/issues/36646)) ([ca34b63](https://github.com/frappe/erpnext/commit/ca34b63470283360dda1a6ac05ec5bd88ed9533c)) ### Performance Improvements * **invoice:** Faster return amount query (backport [#36556](https://github.com/frappe/erpnext/issues/36556)) ([#36557](https://github.com/frappe/erpnext/issues/36557)) ([fe41be9](https://github.com/frappe/erpnext/commit/fe41be953d301b3a92680b5d8eaa04fb5f8a4d95)) --- erpnext/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/__init__.py b/erpnext/__init__.py index 24573a20c125..1aab20134b7b 100644 --- a/erpnext/__init__.py +++ b/erpnext/__init__.py @@ -3,7 +3,7 @@ import frappe -__version__ = "14.34.3" +__version__ = "14.35.0" def get_default_company(user=None): From f8d6fe6be06458050105b9c57939f332e396ca80 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Tue, 8 Aug 2023 20:27:12 +0530 Subject: [PATCH 177/501] feat: utility to repost accounting ledgers without cancellation (#36469) * feat: introduce doctypes for repost * refactor: basic filters and validations * chore: basic validations * chore: added barebones function to generate ledger entries * chore: repost on submit * chore: repost in background * chore: include payment entry and journal entry * chore: ignore repost doc on cancel * chore: preview method * chore: rudimentary form of preview * refactor: preview template * refactor: basic background colors to differentiate old and new * chore: remove commented code * test: basic functionality * chore: fix conflict * chore: prevent repost on invoices with deferred accounting * refactor(test): rename and test basic validations and methods * refactor(test): test all validations * fix(test): use proper name account name * refactor(test): fix failing test case * refactor(test): clear old entries * refactor(test): simpler logic to clear old records * refactor(test): make use of deletion flag * refactor(test): split into multiple test cases (cherry picked from commit e64b004eca86255159c799d9b47f36a75fe70ba0) # Conflicts: # erpnext/accounts/doctype/journal_entry/journal_entry.js --- .../doctype/journal_entry/journal_entry.js | 4 + .../doctype/journal_entry/journal_entry.py | 2 + .../doctype/payment_entry/payment_entry.js | 2 +- .../doctype/payment_entry/payment_entry.py | 2 + .../purchase_invoice/purchase_invoice.js | 2 +- .../purchase_invoice/purchase_invoice.py | 2 + .../repost_accounting_ledger/__init__.py | 0 .../repost_accounting_ledger.html | 44 ++++ .../repost_accounting_ledger.js | 50 +++++ .../repost_accounting_ledger.json | 81 +++++++ .../repost_accounting_ledger.py | 183 ++++++++++++++++ .../test_repost_accounting_ledger.py | 202 ++++++++++++++++++ .../__init__.py | 0 .../repost_accounting_ledger_items.json | 40 ++++ .../repost_accounting_ledger_items.py | 9 + .../doctype/sales_invoice/sales_invoice.js | 2 +- .../doctype/sales_invoice/sales_invoice.py | 2 + erpnext/accounts/test/accounts_mixin.py | 53 +++-- 18 files changed, 663 insertions(+), 17 deletions(-) create mode 100644 erpnext/accounts/doctype/repost_accounting_ledger/__init__.py create mode 100644 erpnext/accounts/doctype/repost_accounting_ledger/repost_accounting_ledger.html create mode 100644 erpnext/accounts/doctype/repost_accounting_ledger/repost_accounting_ledger.js create mode 100644 erpnext/accounts/doctype/repost_accounting_ledger/repost_accounting_ledger.json create mode 100644 erpnext/accounts/doctype/repost_accounting_ledger/repost_accounting_ledger.py create mode 100644 erpnext/accounts/doctype/repost_accounting_ledger/test_repost_accounting_ledger.py create mode 100644 erpnext/accounts/doctype/repost_accounting_ledger_items/__init__.py create mode 100644 erpnext/accounts/doctype/repost_accounting_ledger_items/repost_accounting_ledger_items.json create mode 100644 erpnext/accounts/doctype/repost_accounting_ledger_items/repost_accounting_ledger_items.py diff --git a/erpnext/accounts/doctype/journal_entry/journal_entry.js b/erpnext/accounts/doctype/journal_entry/journal_entry.js index 302acc4f1f76..4edf7114fbf7 100644 --- a/erpnext/accounts/doctype/journal_entry/journal_entry.js +++ b/erpnext/accounts/doctype/journal_entry/journal_entry.js @@ -8,7 +8,11 @@ frappe.provide("erpnext.journal_entry"); frappe.ui.form.on("Journal Entry", { setup: function(frm) { frm.add_fetch("bank_account", "account", "account"); +<<<<<<< HEAD frm.ignore_doctypes_on_cancel_all = ['Sales Invoice', 'Purchase Invoice', 'Journal Entry', "Repost Payment Ledger", 'Asset', 'Asset Movement']; +======= + frm.ignore_doctypes_on_cancel_all = ['Sales Invoice', 'Purchase Invoice', 'Journal Entry', "Repost Payment Ledger", 'Asset', 'Asset Movement', 'Asset Depreciation Schedule', "Repost Accounting Ledger"]; +>>>>>>> e64b004eca (feat: utility to repost accounting ledgers without cancellation (#36469)) }, refresh: function(frm) { diff --git a/erpnext/accounts/doctype/journal_entry/journal_entry.py b/erpnext/accounts/doctype/journal_entry/journal_entry.py index f68980261349..4328635aaa62 100644 --- a/erpnext/accounts/doctype/journal_entry/journal_entry.py +++ b/erpnext/accounts/doctype/journal_entry/journal_entry.py @@ -96,6 +96,8 @@ def on_cancel(self): "Payment Ledger Entry", "Repost Payment Ledger", "Repost Payment Ledger Items", + "Repost Accounting Ledger", + "Repost Accounting Ledger Items", ) self.make_gl_entries(1) self.update_advance_paid() diff --git a/erpnext/accounts/doctype/payment_entry/payment_entry.js b/erpnext/accounts/doctype/payment_entry/payment_entry.js index 3c2fb1dd0ed0..cd788a896a83 100644 --- a/erpnext/accounts/doctype/payment_entry/payment_entry.js +++ b/erpnext/accounts/doctype/payment_entry/payment_entry.js @@ -7,7 +7,7 @@ cur_frm.cscript.tax_table = "Advance Taxes and Charges"; frappe.ui.form.on('Payment Entry', { onload: function(frm) { - frm.ignore_doctypes_on_cancel_all = ['Sales Invoice', 'Purchase Invoice', "Journal Entry", "Repost Payment Ledger"]; + frm.ignore_doctypes_on_cancel_all = ['Sales Invoice', 'Purchase Invoice', 'Journal Entry', 'Repost Payment Ledger','Repost Accounting Ledger']; if(frm.doc.__islocal) { if (!frm.doc.paid_from) frm.set_value("paid_from_account_currency", null); diff --git a/erpnext/accounts/doctype/payment_entry/payment_entry.py b/erpnext/accounts/doctype/payment_entry/payment_entry.py index 379903dade3f..c835b55664c4 100644 --- a/erpnext/accounts/doctype/payment_entry/payment_entry.py +++ b/erpnext/accounts/doctype/payment_entry/payment_entry.py @@ -105,6 +105,8 @@ def on_cancel(self): "Payment Ledger Entry", "Repost Payment Ledger", "Repost Payment Ledger Items", + "Repost Accounting Ledger", + "Repost Accounting Ledger Items", ) super(PaymentEntry, self).on_cancel() self.make_gl_entries(cancel=1) diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js index ab7884d52092..5c82cf99438f 100644 --- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js +++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js @@ -31,7 +31,7 @@ erpnext.accounts.PurchaseInvoice = class PurchaseInvoice extends erpnext.buying. super.onload(); // Ignore linked advances - this.frm.ignore_doctypes_on_cancel_all = ['Journal Entry', 'Payment Entry', 'Purchase Invoice', "Repost Payment Ledger"]; + this.frm.ignore_doctypes_on_cancel_all = ['Journal Entry', 'Payment Entry', 'Purchase Invoice', "Repost Payment Ledger", "Repost Accounting Ledger"]; if(!this.frm.doc.__islocal) { // show credit_to in print format diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py index cefb502ede19..6161e5b36ee0 100644 --- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py +++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py @@ -1422,6 +1422,8 @@ def on_cancel(self): "Repost Item Valuation", "Repost Payment Ledger", "Repost Payment Ledger Items", + "Repost Accounting Ledger", + "Repost Accounting Ledger Items", "Payment Ledger Entry", "Tax Withheld Vouchers", ) diff --git a/erpnext/accounts/doctype/repost_accounting_ledger/__init__.py b/erpnext/accounts/doctype/repost_accounting_ledger/__init__.py new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/erpnext/accounts/doctype/repost_accounting_ledger/repost_accounting_ledger.html b/erpnext/accounts/doctype/repost_accounting_ledger/repost_accounting_ledger.html new file mode 100644 index 000000000000..2dec8f753f2b --- /dev/null +++ b/erpnext/accounts/doctype/repost_accounting_ledger/repost_accounting_ledger.html @@ -0,0 +1,44 @@ + + + + + + {% for col in gl_columns%} + + {% endfor %} + + + + {% for col in gl_columns%} + + {% endfor %} + + +{% for gl in gl_data%} +{% if gl["old"]%} + +{% else %} + +{% endif %} + {% for col in gl_columns %} + + {% endfor %} + +{% endfor %} +
{{ col.label }}
+ {{ gl[col.fieldname] }} +
diff --git a/erpnext/accounts/doctype/repost_accounting_ledger/repost_accounting_ledger.js b/erpnext/accounts/doctype/repost_accounting_ledger/repost_accounting_ledger.js new file mode 100644 index 000000000000..3a87a380d199 --- /dev/null +++ b/erpnext/accounts/doctype/repost_accounting_ledger/repost_accounting_ledger.js @@ -0,0 +1,50 @@ +// Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and contributors +// For license information, please see license.txt + +frappe.ui.form.on("Repost Accounting Ledger", { + setup: function(frm) { + frm.fields_dict['vouchers'].grid.get_field('voucher_type').get_query = function(doc) { + return { + filters: { + name: ['in', ['Purchase Invoice', 'Sales Invoice', 'Payment Entry', 'Journal Entry']], + } + } + } + + frm.fields_dict['vouchers'].grid.get_field('voucher_no').get_query = function(doc) { + if (doc.company) { + return { + filters: { + company: doc.company, + docstatus: 1 + } + } + } + } + }, + + refresh: function(frm) { + frm.add_custom_button(__('Show Preview'), () => { + frm.call({ + method: 'generate_preview', + doc: frm.doc, + freeze: true, + freeze_message: __('Generating Preview'), + callback: function(r) { + if (r && r.message) { + let content = r.message; + let opts = { + title: "Preview", + subtitle: "preview", + content: content, + print_settings: {orientation: "landscape"}, + columns: [], + data: [], + } + frappe.render_grid(opts); + } + } + }); + }); + } +}); diff --git a/erpnext/accounts/doctype/repost_accounting_ledger/repost_accounting_ledger.json b/erpnext/accounts/doctype/repost_accounting_ledger/repost_accounting_ledger.json new file mode 100644 index 000000000000..8d56c9bb11dc --- /dev/null +++ b/erpnext/accounts/doctype/repost_accounting_ledger/repost_accounting_ledger.json @@ -0,0 +1,81 @@ +{ + "actions": [], + "allow_rename": 1, + "autoname": "format:ACC-REPOST-{#####}", + "creation": "2023-07-04 13:07:32.923675", + "default_view": "List", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "company", + "column_break_vpup", + "delete_cancelled_entries", + "section_break_metl", + "vouchers", + "amended_from" + ], + "fields": [ + { + "fieldname": "company", + "fieldtype": "Link", + "label": "Company", + "options": "Company" + }, + { + "fieldname": "amended_from", + "fieldtype": "Link", + "label": "Amended From", + "no_copy": 1, + "options": "Repost Accounting Ledger", + "print_hide": 1, + "read_only": 1 + }, + { + "fieldname": "vouchers", + "fieldtype": "Table", + "label": "Vouchers", + "options": "Repost Accounting Ledger Items" + }, + { + "fieldname": "column_break_vpup", + "fieldtype": "Column Break" + }, + { + "fieldname": "section_break_metl", + "fieldtype": "Section Break" + }, + { + "default": "0", + "fieldname": "delete_cancelled_entries", + "fieldtype": "Check", + "label": "Delete Cancelled Ledger Entries" + } + ], + "index_web_pages_for_search": 1, + "is_submittable": 1, + "links": [], + "modified": "2023-07-27 15:47:58.975034", + "modified_by": "Administrator", + "module": "Accounts", + "name": "Repost Accounting Ledger", + "naming_rule": "Expression", + "owner": "Administrator", + "permissions": [ + { + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "System Manager", + "share": 1, + "write": 1 + } + ], + "sort_field": "modified", + "sort_order": "DESC", + "states": [] +} \ No newline at end of file diff --git a/erpnext/accounts/doctype/repost_accounting_ledger/repost_accounting_ledger.py b/erpnext/accounts/doctype/repost_accounting_ledger/repost_accounting_ledger.py new file mode 100644 index 000000000000..4cf2ed2f46cc --- /dev/null +++ b/erpnext/accounts/doctype/repost_accounting_ledger/repost_accounting_ledger.py @@ -0,0 +1,183 @@ +# Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and contributors +# For license information, please see license.txt + +import frappe +from frappe import _, qb +from frappe.model.document import Document +from frappe.utils.data import comma_and + + +class RepostAccountingLedger(Document): + def __init__(self, *args, **kwargs): + super(RepostAccountingLedger, self).__init__(*args, **kwargs) + self._allowed_types = set( + ["Purchase Invoice", "Sales Invoice", "Payment Entry", "Journal Entry"] + ) + + def validate(self): + self.validate_vouchers() + self.validate_for_closed_fiscal_year() + self.validate_for_deferred_accounting() + + def validate_for_deferred_accounting(self): + sales_docs = [x.voucher_no for x in self.vouchers if x.voucher_type == "Sales Invoice"] + docs_with_deferred_revenue = frappe.db.get_all( + "Sales Invoice Item", + filters={"parent": ["in", sales_docs], "docstatus": 1, "enable_deferred_revenue": True}, + fields=["parent"], + as_list=1, + ) + + purchase_docs = [x.voucher_no for x in self.vouchers if x.voucher_type == "Purchase Invoice"] + docs_with_deferred_expense = frappe.db.get_all( + "Purchase Invoice Item", + filters={"parent": ["in", purchase_docs], "docstatus": 1, "enable_deferred_expense": 1}, + fields=["parent"], + as_list=1, + ) + + if docs_with_deferred_revenue or docs_with_deferred_expense: + frappe.throw( + _("Documents: {0} have deferred revenue/expense enabled for them. Cannot repost.").format( + frappe.bold( + comma_and([x[0] for x in docs_with_deferred_expense + docs_with_deferred_revenue]) + ) + ) + ) + + def validate_for_closed_fiscal_year(self): + if self.vouchers: + latest_pcv = ( + frappe.db.get_all( + "Period Closing Voucher", + filters={"company": self.company}, + order_by="posting_date desc", + pluck="posting_date", + limit=1, + ) + or None + ) + if not latest_pcv: + return + + for vtype in self._allowed_types: + if names := [x.voucher_no for x in self.vouchers if x.voucher_type == vtype]: + latest_voucher = frappe.db.get_all( + vtype, + filters={"name": ["in", names]}, + pluck="posting_date", + order_by="posting_date desc", + limit=1, + )[0] + if latest_voucher and latest_pcv[0] >= latest_voucher: + frappe.throw(_("Cannot Resubmit Ledger entries for vouchers in Closed fiscal year.")) + + def validate_vouchers(self): + if self.vouchers: + # Validate voucher types + voucher_types = set([x.voucher_type for x in self.vouchers]) + if disallowed_types := voucher_types.difference(self._allowed_types): + frappe.throw( + _("{0} types are not allowed. Only {1} are.").format( + frappe.bold(comma_and(list(disallowed_types))), + frappe.bold(comma_and(list(self._allowed_types))), + ) + ) + + def get_existing_ledger_entries(self): + vouchers = [x.voucher_no for x in self.vouchers] + gl = qb.DocType("GL Entry") + existing_gles = ( + qb.from_(gl) + .select(gl.star) + .where((gl.voucher_no.isin(vouchers)) & (gl.is_cancelled == 0)) + .run(as_dict=True) + ) + self.gles = frappe._dict({}) + + for gle in existing_gles: + self.gles.setdefault((gle.voucher_type, gle.voucher_no), frappe._dict({})).setdefault( + "existing", [] + ).append(gle.update({"old": True})) + + def generate_preview_data(self): + self.gl_entries = [] + self.get_existing_ledger_entries() + for x in self.vouchers: + doc = frappe.get_doc(x.voucher_type, x.voucher_no) + if doc.doctype in ["Payment Entry", "Journal Entry"]: + gle_map = doc.build_gl_map() + else: + gle_map = doc.get_gl_entries() + + old_entries = self.gles.get((x.voucher_type, x.voucher_no)) + if old_entries: + self.gl_entries.extend(old_entries.existing) + self.gl_entries.extend(gle_map) + + @frappe.whitelist() + def generate_preview(self): + from erpnext.accounts.report.general_ledger.general_ledger import get_columns as get_gl_columns + + gl_columns = [] + gl_data = [] + + self.generate_preview_data() + if self.gl_entries: + filters = {"company": self.company, "include_dimensions": 1} + for x in get_gl_columns(filters): + if x["fieldname"] == "gl_entry": + x["fieldname"] = "name" + gl_columns.append(x) + + gl_data = self.gl_entries + rendered_page = frappe.render_template( + "erpnext/accounts/doctype/repost_accounting_ledger/repost_accounting_ledger.html", + {"gl_columns": gl_columns, "gl_data": gl_data}, + ) + + return rendered_page + + def on_submit(self): + job_name = "repost_accounting_ledger_" + self.name + frappe.enqueue( + method="erpnext.accounts.doctype.repost_accounting_ledger.repost_accounting_ledger.start_repost", + account_repost_doc=self.name, + is_async=True, + job_name=job_name, + ) + frappe.msgprint(_("Repost has started in the background")) + + +@frappe.whitelist() +def start_repost(account_repost_doc=str) -> None: + if account_repost_doc: + repost_doc = frappe.get_doc("Repost Accounting Ledger", account_repost_doc) + + if repost_doc.docstatus == 1: + # Prevent repost on invoices with deferred accounting + repost_doc.validate_for_deferred_accounting() + + for x in repost_doc.vouchers: + doc = frappe.get_doc(x.voucher_type, x.voucher_no) + + if repost_doc.delete_cancelled_entries: + frappe.db.delete("GL Entry", filters={"voucher_type": doc.doctype, "voucher_no": doc.name}) + frappe.db.delete( + "Payment Ledger Entry", filters={"voucher_type": doc.doctype, "voucher_no": doc.name} + ) + + if doc.doctype in ["Sales Invoice", "Purchase Invoice"]: + if not repost_doc.delete_cancelled_entries: + doc.docstatus = 2 + doc.make_gl_entries_on_cancel() + + doc.docstatus = 1 + doc.make_gl_entries() + + elif doc.doctype in ["Payment Entry", "Journal Entry"]: + if not repost_doc.delete_cancelled_entries: + doc.make_gl_entries(1) + doc.make_gl_entries() + + frappe.db.commit() diff --git a/erpnext/accounts/doctype/repost_accounting_ledger/test_repost_accounting_ledger.py b/erpnext/accounts/doctype/repost_accounting_ledger/test_repost_accounting_ledger.py new file mode 100644 index 000000000000..0e75dd2e3e17 --- /dev/null +++ b/erpnext/accounts/doctype/repost_accounting_ledger/test_repost_accounting_ledger.py @@ -0,0 +1,202 @@ +# Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and Contributors +# See license.txt + +import frappe +from frappe import qb +from frappe.query_builder.functions import Sum +from frappe.tests.utils import FrappeTestCase, change_settings +from frappe.utils import add_days, nowdate, today + +from erpnext.accounts.doctype.payment_entry.payment_entry import get_payment_entry +from erpnext.accounts.doctype.payment_request.payment_request import make_payment_request +from erpnext.accounts.doctype.repost_accounting_ledger.repost_accounting_ledger import start_repost +from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice +from erpnext.accounts.test.accounts_mixin import AccountsTestMixin +from erpnext.accounts.utils import get_fiscal_year + + +class TestRepostAccountingLedger(AccountsTestMixin, FrappeTestCase): + def setUp(self): + self.create_company() + self.create_customer() + self.create_item() + + def teadDown(self): + frappe.db.rollback() + + def test_01_basic_functions(self): + si = create_sales_invoice( + item=self.item, + company=self.company, + customer=self.customer, + debit_to=self.debit_to, + parent_cost_center=self.cost_center, + cost_center=self.cost_center, + rate=100, + ) + + preq = frappe.get_doc( + make_payment_request( + dt=si.doctype, + dn=si.name, + payment_request_type="Inward", + party_type="Customer", + party=si.customer, + ) + ) + preq.save().submit() + + # Test Validation Error + ral = frappe.new_doc("Repost Accounting Ledger") + ral.company = self.company + ral.delete_cancelled_entries = True + ral.append("vouchers", {"voucher_type": si.doctype, "voucher_no": si.name}) + ral.append( + "vouchers", {"voucher_type": preq.doctype, "voucher_no": preq.name} + ) # this should throw validation error + self.assertRaises(frappe.ValidationError, ral.save) + ral.vouchers.pop() + preq.cancel() + preq.delete() + + pe = get_payment_entry(si.doctype, si.name) + pe.save().submit() + ral.append("vouchers", {"voucher_type": pe.doctype, "voucher_no": pe.name}) + ral.save() + + # manually set an incorrect debit amount in DB + gle = frappe.db.get_all("GL Entry", filters={"voucher_no": si.name, "account": self.debit_to}) + frappe.db.set_value("GL Entry", gle[0], "debit", 90) + + gl = qb.DocType("GL Entry") + res = ( + qb.from_(gl) + .select(gl.voucher_no, Sum(gl.debit).as_("debit"), Sum(gl.credit).as_("credit")) + .where((gl.voucher_no == si.name) & (gl.is_cancelled == 0)) + .run() + ) + + # Assert incorrect ledger balance + self.assertNotEqual(res[0], (si.name, 100, 100)) + + # Submit repost document + ral.save().submit() + + # background jobs don't run on test cases. Manually triggering repost function. + start_repost(ral.name) + + res = ( + qb.from_(gl) + .select(gl.voucher_no, Sum(gl.debit).as_("debit"), Sum(gl.credit).as_("credit")) + .where((gl.voucher_no == si.name) & (gl.is_cancelled == 0)) + .run() + ) + + # Ledger should reflect correct amount post repost + self.assertEqual(res[0], (si.name, 100, 100)) + + def test_02_deferred_accounting_valiations(self): + si = create_sales_invoice( + item=self.item, + company=self.company, + customer=self.customer, + debit_to=self.debit_to, + parent_cost_center=self.cost_center, + cost_center=self.cost_center, + rate=100, + do_not_submit=True, + ) + si.items[0].enable_deferred_revenue = True + si.items[0].deferred_revenue_account = self.deferred_revenue + si.items[0].service_start_date = nowdate() + si.items[0].service_end_date = add_days(nowdate(), 90) + si.save().submit() + + ral = frappe.new_doc("Repost Accounting Ledger") + ral.company = self.company + ral.append("vouchers", {"voucher_type": si.doctype, "voucher_no": si.name}) + self.assertRaises(frappe.ValidationError, ral.save) + + @change_settings("Accounts Settings", {"delete_linked_ledger_entries": 1}) + def test_04_pcv_validation(self): + # Clear old GL entries so PCV can be submitted. + gl = frappe.qb.DocType("GL Entry") + qb.from_(gl).delete().where(gl.company == self.company).run() + + si = create_sales_invoice( + item=self.item, + company=self.company, + customer=self.customer, + debit_to=self.debit_to, + parent_cost_center=self.cost_center, + cost_center=self.cost_center, + rate=100, + ) + pcv = frappe.get_doc( + { + "doctype": "Period Closing Voucher", + "transaction_date": today(), + "posting_date": today(), + "company": self.company, + "fiscal_year": get_fiscal_year(today(), company=self.company)[0], + "cost_center": self.cost_center, + "closing_account_head": self.retained_earnings, + "remarks": "test", + } + ) + pcv.save().submit() + + ral = frappe.new_doc("Repost Accounting Ledger") + ral.company = self.company + ral.append("vouchers", {"voucher_type": si.doctype, "voucher_no": si.name}) + self.assertRaises(frappe.ValidationError, ral.save) + + pcv.reload() + pcv.cancel() + pcv.delete() + + def test_03_deletion_flag_and_preview_function(self): + si = create_sales_invoice( + item=self.item, + company=self.company, + customer=self.customer, + debit_to=self.debit_to, + parent_cost_center=self.cost_center, + cost_center=self.cost_center, + rate=100, + ) + + pe = get_payment_entry(si.doctype, si.name) + pe.save().submit() + + # without deletion flag set + ral = frappe.new_doc("Repost Accounting Ledger") + ral.company = self.company + ral.delete_cancelled_entries = False + ral.append("vouchers", {"voucher_type": si.doctype, "voucher_no": si.name}) + ral.append("vouchers", {"voucher_type": pe.doctype, "voucher_no": pe.name}) + ral.save() + + # assert preview data is generated + preview = ral.generate_preview() + self.assertIsNotNone(preview) + + ral.save().submit() + + # background jobs don't run on test cases. Manually triggering repost function. + start_repost(ral.name) + + self.assertIsNotNone(frappe.db.exists("GL Entry", {"voucher_no": si.name, "is_cancelled": 1})) + self.assertIsNotNone(frappe.db.exists("GL Entry", {"voucher_no": pe.name, "is_cancelled": 1})) + + # with deletion flag set + ral = frappe.new_doc("Repost Accounting Ledger") + ral.company = self.company + ral.delete_cancelled_entries = True + ral.append("vouchers", {"voucher_type": si.doctype, "voucher_no": si.name}) + ral.append("vouchers", {"voucher_type": pe.doctype, "voucher_no": pe.name}) + ral.save().submit() + + start_repost(ral.name) + self.assertIsNone(frappe.db.exists("GL Entry", {"voucher_no": si.name, "is_cancelled": 1})) + self.assertIsNone(frappe.db.exists("GL Entry", {"voucher_no": pe.name, "is_cancelled": 1})) diff --git a/erpnext/accounts/doctype/repost_accounting_ledger_items/__init__.py b/erpnext/accounts/doctype/repost_accounting_ledger_items/__init__.py new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/erpnext/accounts/doctype/repost_accounting_ledger_items/repost_accounting_ledger_items.json b/erpnext/accounts/doctype/repost_accounting_ledger_items/repost_accounting_ledger_items.json new file mode 100644 index 000000000000..4a2041f88c6b --- /dev/null +++ b/erpnext/accounts/doctype/repost_accounting_ledger_items/repost_accounting_ledger_items.json @@ -0,0 +1,40 @@ +{ + "actions": [], + "allow_rename": 1, + "creation": "2023-07-04 14:14:01.243848", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "voucher_type", + "voucher_no" + ], + "fields": [ + { + "fieldname": "voucher_type", + "fieldtype": "Link", + "in_list_view": 1, + "label": "Voucher Type", + "options": "DocType" + }, + { + "fieldname": "voucher_no", + "fieldtype": "Dynamic Link", + "in_list_view": 1, + "label": "Voucher No", + "options": "voucher_type" + } + ], + "index_web_pages_for_search": 1, + "istable": 1, + "links": [], + "modified": "2023-07-04 14:15:51.165584", + "modified_by": "Administrator", + "module": "Accounts", + "name": "Repost Accounting Ledger Items", + "owner": "Administrator", + "permissions": [], + "sort_field": "modified", + "sort_order": "DESC", + "states": [] +} \ No newline at end of file diff --git a/erpnext/accounts/doctype/repost_accounting_ledger_items/repost_accounting_ledger_items.py b/erpnext/accounts/doctype/repost_accounting_ledger_items/repost_accounting_ledger_items.py new file mode 100644 index 000000000000..9221f447355c --- /dev/null +++ b/erpnext/accounts/doctype/repost_accounting_ledger_items/repost_accounting_ledger_items.py @@ -0,0 +1,9 @@ +# Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and contributors +# For license information, please see license.txt + +# import frappe +from frappe.model.document import Document + + +class RepostAccountingLedgerItems(Document): + pass diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.js b/erpnext/accounts/doctype/sales_invoice/sales_invoice.js index df3db37bc650..d6977d39a9fe 100644 --- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.js +++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.js @@ -34,7 +34,7 @@ erpnext.accounts.SalesInvoiceController = class SalesInvoiceController extends e super.onload(); this.frm.ignore_doctypes_on_cancel_all = ['POS Invoice', 'Timesheet', 'POS Invoice Merge Log', - 'POS Closing Entry', 'Journal Entry', 'Payment Entry', "Repost Payment Ledger"]; + 'POS Closing Entry', 'Journal Entry', 'Payment Entry', "Repost Payment Ledger", "Repost Accounting Ledger"]; if(!this.frm.doc.__islocal && !this.frm.doc.customer && this.frm.doc.debit_to) { // show debit_to in print format diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py index ab629913cd41..3f9fe0441d12 100644 --- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py +++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py @@ -399,6 +399,8 @@ def on_cancel(self): "Repost Item Valuation", "Repost Payment Ledger", "Repost Payment Ledger Items", + "Repost Accounting Ledger", + "Repost Accounting Ledger Items", "Payment Ledger Entry", ) diff --git a/erpnext/accounts/test/accounts_mixin.py b/erpnext/accounts/test/accounts_mixin.py index c82164ef6447..70bbf7e694dd 100644 --- a/erpnext/accounts/test/accounts_mixin.py +++ b/erpnext/accounts/test/accounts_mixin.py @@ -4,7 +4,7 @@ class AccountsTestMixin: - def create_customer(self, customer_name, currency=None): + def create_customer(self, customer_name="_Test Customer", currency=None): if not frappe.db.exists("Customer", customer_name): customer = frappe.new_doc("Customer") customer.customer_name = customer_name @@ -17,7 +17,7 @@ def create_customer(self, customer_name, currency=None): else: self.customer = customer_name - def create_supplier(self, supplier_name, currency=None): + def create_supplier(self, supplier_name="_Test Supplier", currency=None): if not frappe.db.exists("Supplier", supplier_name): supplier = frappe.new_doc("Supplier") supplier.supplier_name = supplier_name @@ -31,7 +31,7 @@ def create_supplier(self, supplier_name, currency=None): else: self.supplier = supplier_name - def create_item(self, item_name, is_stock=0, warehouse=None, company=None): + def create_item(self, item_name="_Test Item", is_stock=0, warehouse=None, company=None): item = create_item(item_name, is_stock_item=is_stock, warehouse=warehouse, company=company) self.item = item.name @@ -62,19 +62,44 @@ def create_company(self, company_name="_Test Company", abbr="_TC"): self.debit_usd = "Debtors USD - " + abbr self.cash = "Cash - " + abbr self.creditors = "Creditors - " + abbr + self.retained_earnings = "Retained Earnings - " + abbr - # create bank account - bank_account = "HDFC - " + abbr - if frappe.db.exists("Account", bank_account): - self.bank = bank_account - else: - bank_acc = frappe.get_doc( + # Deferred revenue, expense and bank accounts + other_accounts = [ + frappe._dict( + { + "attribute_name": "deferred_revenue", + "account_name": "Deferred Revenue", + "parent_account": "Current Liabilities - " + abbr, + } + ), + frappe._dict( { - "doctype": "Account", + "attribute_name": "deferred_expense", + "account_name": "Deferred Expense", + "parent_account": "Current Assets - " + abbr, + } + ), + frappe._dict( + { + "attribute_name": "bank", "account_name": "HDFC", "parent_account": "Bank Accounts - " + abbr, - "company": self.company, } - ) - bank_acc.save() - self.bank = bank_acc.name + ), + ] + for acc in other_accounts: + acc_name = acc.account_name + " - " + abbr + if frappe.db.exists("Account", acc_name): + setattr(self, acc.attribute_name, acc_name) + else: + new_acc = frappe.get_doc( + { + "doctype": "Account", + "account_name": acc.account_name, + "parent_account": acc.parent_account, + "company": self.company, + } + ) + new_acc.save() + setattr(self, acc.attribute_name, new_acc.name) From b6134749c30731b551fc33f21bad6745dda3fd49 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Wed, 16 Aug 2023 16:44:07 +0530 Subject: [PATCH 178/501] chore: resolve conflict --- erpnext/accounts/doctype/journal_entry/journal_entry.js | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/erpnext/accounts/doctype/journal_entry/journal_entry.js b/erpnext/accounts/doctype/journal_entry/journal_entry.js index 4edf7114fbf7..199068529d42 100644 --- a/erpnext/accounts/doctype/journal_entry/journal_entry.js +++ b/erpnext/accounts/doctype/journal_entry/journal_entry.js @@ -8,11 +8,7 @@ frappe.provide("erpnext.journal_entry"); frappe.ui.form.on("Journal Entry", { setup: function(frm) { frm.add_fetch("bank_account", "account", "account"); -<<<<<<< HEAD - frm.ignore_doctypes_on_cancel_all = ['Sales Invoice', 'Purchase Invoice', 'Journal Entry', "Repost Payment Ledger", 'Asset', 'Asset Movement']; -======= - frm.ignore_doctypes_on_cancel_all = ['Sales Invoice', 'Purchase Invoice', 'Journal Entry', "Repost Payment Ledger", 'Asset', 'Asset Movement', 'Asset Depreciation Schedule', "Repost Accounting Ledger"]; ->>>>>>> e64b004eca (feat: utility to repost accounting ledgers without cancellation (#36469)) + frm.ignore_doctypes_on_cancel_all = ['Sales Invoice', 'Purchase Invoice', 'Journal Entry', 'Repost Payment Ledger', 'Asset', 'Asset Movement', 'Repost Accounting Ledger']; }, refresh: function(frm) { From 3e23e1fb91fdcc6e97929bb5528bb61dd15d7892 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Wed, 16 Aug 2023 19:10:39 +0530 Subject: [PATCH 179/501] fix(UX): Ignore prepared report (cherry picked from commit 124c0dbd880dde227dacf6136f4e463acc08737f) --- erpnext/public/js/controllers/stock_controller.js | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/erpnext/public/js/controllers/stock_controller.js b/erpnext/public/js/controllers/stock_controller.js index d346357a8f82..2674df9c4af2 100644 --- a/erpnext/public/js/controllers/stock_controller.js +++ b/erpnext/public/js/controllers/stock_controller.js @@ -57,7 +57,8 @@ erpnext.stock.StockController = class StockController extends frappe.ui.form.Con from_date: me.frm.doc.posting_date, to_date: moment(me.frm.doc.modified).format('YYYY-MM-DD'), company: me.frm.doc.company, - show_cancelled_entries: me.frm.doc.docstatus === 2 + show_cancelled_entries: me.frm.doc.docstatus === 2, + ignore_prepared_report: true }; frappe.set_route("query-report", "Stock Ledger"); }, __("View")); @@ -75,7 +76,8 @@ erpnext.stock.StockController = class StockController extends frappe.ui.form.Con to_date: moment(me.frm.doc.modified).format('YYYY-MM-DD'), company: me.frm.doc.company, group_by: "Group by Voucher (Consolidated)", - show_cancelled_entries: me.frm.doc.docstatus === 2 + show_cancelled_entries: me.frm.doc.docstatus === 2, + ignore_prepared_report: true }; frappe.set_route("query-report", "General Ledger"); }, __("View")); From 47345e81a1342c3a6c904c66368460b18dd2816a Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Tue, 15 Aug 2023 05:17:01 +0530 Subject: [PATCH 180/501] perf: pull latest details only for referenced vouchers (cherry picked from commit deb0d7129438ff7966e1e50f5f7eb0a78e2fdb49) # Conflicts: # erpnext/accounts/doctype/payment_entry/payment_entry.py --- .../doctype/payment_entry/payment_entry.py | 96 ++++++++++++++++--- erpnext/accounts/utils.py | 2 + 2 files changed, 85 insertions(+), 13 deletions(-) diff --git a/erpnext/accounts/doctype/payment_entry/payment_entry.py b/erpnext/accounts/doctype/payment_entry/payment_entry.py index 379903dade3f..35a0e85a37da 100644 --- a/erpnext/accounts/doctype/payment_entry/payment_entry.py +++ b/erpnext/accounts/doctype/payment_entry/payment_entry.py @@ -185,6 +185,7 @@ def term_based_allocation_enabled_for_reference( return False def validate_allocated_amount_with_latest_data(self): +<<<<<<< HEAD latest_references = get_outstanding_reference_documents( { "posting_date": self.posting_date, @@ -197,16 +198,36 @@ def validate_allocated_amount_with_latest_data(self): "get_orders_to_be_billed": True, } ) +======= + if self.references: + uniq_vouchers = set([(x.reference_doctype, x.reference_name) for x in self.references]) + vouchers = [frappe._dict({"voucher_type": x[0], "voucher_no": x[1]}) for x in uniq_vouchers] + latest_references = get_outstanding_reference_documents( + { + "posting_date": self.posting_date, + "company": self.company, + "party_type": self.party_type, + "payment_type": self.payment_type, + "party": self.party, + "party_account": self.paid_from if self.payment_type == "Receive" else self.paid_to, + "get_outstanding_invoices": True, + "get_orders_to_be_billed": True, + "vouchers": vouchers, + }, + validate=True, + ) +>>>>>>> deb0d71294 (perf: pull latest details only for referenced vouchers) - # Group latest_references by (voucher_type, voucher_no) - latest_lookup = {} - for d in latest_references: - d = frappe._dict(d) - latest_lookup.setdefault((d.voucher_type, d.voucher_no), frappe._dict())[d.payment_term] = d + # Group latest_references by (voucher_type, voucher_no) + latest_lookup = {} + for d in latest_references: + d = frappe._dict(d) + latest_lookup.setdefault((d.voucher_type, d.voucher_no), frappe._dict())[d.payment_term] = d - for idx, d in enumerate(self.get("references"), start=1): - latest = latest_lookup.get((d.reference_doctype, d.reference_name)) or frappe._dict() + for idx, d in enumerate(self.get("references"), start=1): + latest = latest_lookup.get((d.reference_doctype, d.reference_name)) or frappe._dict() +<<<<<<< HEAD # If term based allocation is enabled, throw if ( d.payment_term is None or d.payment_term == "" @@ -254,15 +275,63 @@ def validate_allocated_amount_with_latest_data(self): "Row #{0}: Allocated amount:{1} is greater than outstanding amount:{2} for Payment Term {3}" ).format( d.idx, d.allocated_amount, latest.payment_term_outstanding, d.payment_term +======= + # If term based allocation is enabled, throw + if ( + d.payment_term is None or d.payment_term == "" + ) and self.term_based_allocation_enabled_for_reference( + d.reference_doctype, d.reference_name + ): + frappe.throw( + _( + "{0} has Payment Term based allocation enabled. Select a Payment Term for Row #{1} in Payment References section" + ).format(frappe.bold(d.reference_name), frappe.bold(idx)) +>>>>>>> deb0d71294 (perf: pull latest details only for referenced vouchers) ) - ) - if (flt(d.allocated_amount)) > 0 and flt(d.allocated_amount) > flt(latest.outstanding_amount): - frappe.throw(fail_message.format(d.idx)) + # if no payment template is used by invoice and has a custom term(no `payment_term`), then invoice outstanding will be in 'None' key + latest = latest.get(d.payment_term) or latest.get(None) - # Check for negative outstanding invoices as well - if flt(d.allocated_amount) < 0 and flt(d.allocated_amount) < flt(latest.outstanding_amount): - frappe.throw(fail_message.format(d.idx)) + # The reference has already been fully paid + if not latest: + frappe.throw( + _("{0} {1} has already been fully paid.").format(_(d.reference_doctype), d.reference_name) + ) + # The reference has already been partly paid + elif latest.outstanding_amount < latest.invoice_amount and flt( + d.outstanding_amount, d.precision("outstanding_amount") + ) != flt(latest.outstanding_amount, d.precision("outstanding_amount")): + frappe.throw( + _( + "{0} {1} has already been partly paid. Please use the 'Get Outstanding Invoice' or the 'Get Outstanding Orders' button to get the latest outstanding amounts." + ).format(_(d.reference_doctype), d.reference_name) + ) + + fail_message = _("Row #{0}: Allocated Amount cannot be greater than outstanding amount.") + + if ( + d.payment_term + and ( + (flt(d.allocated_amount)) > 0 + and latest.payment_term_outstanding + and (flt(d.allocated_amount) > flt(latest.payment_term_outstanding)) + ) + and self.term_based_allocation_enabled_for_reference(d.reference_doctype, d.reference_name) + ): + frappe.throw( + _( + "Row #{0}: Allocated amount:{1} is greater than outstanding amount:{2} for Payment Term {3}" + ).format( + d.idx, d.allocated_amount, latest.payment_term_outstanding, d.payment_term + ) + ) + + if (flt(d.allocated_amount)) > 0 and flt(d.allocated_amount) > flt(latest.outstanding_amount): + frappe.throw(fail_message.format(d.idx)) + + # Check for negative outstanding invoices as well + if flt(d.allocated_amount) < 0 and flt(d.allocated_amount) < flt(latest.outstanding_amount): + frappe.throw(fail_message.format(d.idx)) def delink_advance_entry_references(self): for reference in self.references: @@ -1463,6 +1532,7 @@ def get_outstanding_reference_documents(args): min_outstanding=args.get("outstanding_amt_greater_than"), max_outstanding=args.get("outstanding_amt_less_than"), accounting_dimensions=accounting_dimensions_filter, + vouchers=args.get("vouchers") or None, ) outstanding_invoices = split_invoices_based_on_payment_terms( diff --git a/erpnext/accounts/utils.py b/erpnext/accounts/utils.py index 3e06a36e67ed..2df3387b83e3 100644 --- a/erpnext/accounts/utils.py +++ b/erpnext/accounts/utils.py @@ -884,6 +884,7 @@ def get_outstanding_invoices( min_outstanding=None, max_outstanding=None, accounting_dimensions=None, + vouchers=None, ): ple = qb.DocType("Payment Ledger Entry") @@ -909,6 +910,7 @@ def get_outstanding_invoices( ple_query = QueryPaymentLedger() invoice_list = ple_query.get_voucher_outstandings( + vouchers=vouchers, common_filter=common_filter, posting_date=posting_date, min_outstanding=min_outstanding, From 3f33d4cf763a46d111c6084f8a61c940c0d72496 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Thu, 17 Aug 2023 09:21:11 +0530 Subject: [PATCH 181/501] chore: resolve conflicts --- .../doctype/payment_entry/payment_entry.py | 68 +------------------ 1 file changed, 1 insertion(+), 67 deletions(-) diff --git a/erpnext/accounts/doctype/payment_entry/payment_entry.py b/erpnext/accounts/doctype/payment_entry/payment_entry.py index 35a0e85a37da..c94e02325131 100644 --- a/erpnext/accounts/doctype/payment_entry/payment_entry.py +++ b/erpnext/accounts/doctype/payment_entry/payment_entry.py @@ -185,20 +185,6 @@ def term_based_allocation_enabled_for_reference( return False def validate_allocated_amount_with_latest_data(self): -<<<<<<< HEAD - latest_references = get_outstanding_reference_documents( - { - "posting_date": self.posting_date, - "company": self.company, - "party_type": self.party_type, - "payment_type": self.payment_type, - "party": self.party, - "party_account": self.paid_from if self.payment_type == "Receive" else self.paid_to, - "get_outstanding_invoices": True, - "get_orders_to_be_billed": True, - } - ) -======= if self.references: uniq_vouchers = set([(x.reference_doctype, x.reference_name) for x in self.references]) vouchers = [frappe._dict({"voucher_type": x[0], "voucher_no": x[1]}) for x in uniq_vouchers] @@ -213,10 +199,8 @@ def validate_allocated_amount_with_latest_data(self): "get_outstanding_invoices": True, "get_orders_to_be_billed": True, "vouchers": vouchers, - }, - validate=True, + } ) ->>>>>>> deb0d71294 (perf: pull latest details only for referenced vouchers) # Group latest_references by (voucher_type, voucher_no) latest_lookup = {} @@ -227,55 +211,6 @@ def validate_allocated_amount_with_latest_data(self): for idx, d in enumerate(self.get("references"), start=1): latest = latest_lookup.get((d.reference_doctype, d.reference_name)) or frappe._dict() -<<<<<<< HEAD - # If term based allocation is enabled, throw - if ( - d.payment_term is None or d.payment_term == "" - ) and self.term_based_allocation_enabled_for_reference( - d.reference_doctype, d.reference_name - ): - frappe.throw( - _( - "{0} has Payment Term based allocation enabled. Select a Payment Term for Row #{1} in Payment References section" - ).format(frappe.bold(d.reference_name), frappe.bold(idx)) - ) - - # if no payment template is used by invoice and has a custom term(no `payment_term`), then invoice outstanding will be in 'None' key - latest = latest.get(d.payment_term) or latest.get(None) - - # The reference has already been fully paid - if not latest: - frappe.throw( - _("{0} {1} has already been fully paid.").format(_(d.reference_doctype), d.reference_name) - ) - # The reference has already been partly paid - elif latest.outstanding_amount < latest.invoice_amount and flt( - d.outstanding_amount, d.precision("outstanding_amount") - ) != flt(latest.outstanding_amount, d.precision("outstanding_amount")): - - frappe.throw( - _( - "{0} {1} has already been partly paid. Please use the 'Get Outstanding Invoice' or the 'Get Outstanding Orders' button to get the latest outstanding amounts." - ).format(_(d.reference_doctype), d.reference_name) - ) - - fail_message = _("Row #{0}: Allocated Amount cannot be greater than outstanding amount.") - - if ( - d.payment_term - and ( - (flt(d.allocated_amount)) > 0 - and latest.payment_term_outstanding - and (flt(d.allocated_amount) > flt(latest.payment_term_outstanding)) - ) - and self.term_based_allocation_enabled_for_reference(d.reference_doctype, d.reference_name) - ): - frappe.throw( - _( - "Row #{0}: Allocated amount:{1} is greater than outstanding amount:{2} for Payment Term {3}" - ).format( - d.idx, d.allocated_amount, latest.payment_term_outstanding, d.payment_term -======= # If term based allocation is enabled, throw if ( d.payment_term is None or d.payment_term == "" @@ -286,7 +221,6 @@ def validate_allocated_amount_with_latest_data(self): _( "{0} has Payment Term based allocation enabled. Select a Payment Term for Row #{1} in Payment References section" ).format(frappe.bold(d.reference_name), frappe.bold(idx)) ->>>>>>> deb0d71294 (perf: pull latest details only for referenced vouchers) ) # if no payment template is used by invoice and has a custom term(no `payment_term`), then invoice outstanding will be in 'None' key From c308bd53097679c0d5dab16af2786861b1dedf2f Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Thu, 17 Aug 2023 12:11:37 +0530 Subject: [PATCH 182/501] feat(RFQ): make email message fully configurable (backport #36353) (#36531) * feat(RFQ): make email message fully configurable (#36353) feat: make RFQ message fully configurable (cherry picked from commit 21080afd92d42c12d302f8f1499c0697e571790e) * fix(RFQ): hide description in print and report --------- Co-authored-by: Raffael Meyer <14891507+barredterra@users.noreply.github.com> --- .../request_for_quotation.js | 16 ++++---- .../request_for_quotation.json | 32 ++++++---------- .../request_for_quotation.py | 37 ++++++++----------- .../emails/request_for_quotation.html | 29 --------------- 4 files changed, 35 insertions(+), 79 deletions(-) delete mode 100644 erpnext/templates/emails/request_for_quotation.html diff --git a/erpnext/buying/doctype/request_for_quotation/request_for_quotation.js b/erpnext/buying/doctype/request_for_quotation/request_for_quotation.js index 2f0b7862a82d..30abad528bfc 100644 --- a/erpnext/buying/doctype/request_for_quotation/request_for_quotation.js +++ b/erpnext/buying/doctype/request_for_quotation/request_for_quotation.js @@ -245,19 +245,21 @@ frappe.ui.form.on("Request for Quotation",{ ] }); - dialog.fields_dict['supplier'].df.onchange = () => { - var supplier = dialog.get_value('supplier'); - frm.call('get_supplier_email_preview', {supplier: supplier}).then(result => { + dialog.fields_dict["supplier"].df.onchange = () => { + frm.call("get_supplier_email_preview", { + supplier: dialog.get_value("supplier"), + }).then(({ message }) => { dialog.fields_dict.email_preview.$wrapper.empty(); - dialog.fields_dict.email_preview.$wrapper.append(result.message); + dialog.fields_dict.email_preview.$wrapper.append( + message.message + ); + dialog.set_value("subject", message.subject); }); - - } + }; dialog.fields_dict.note.$wrapper.append(`

This is a preview of the email to be sent. A PDF of the document will automatically be attached with the email.

`); - dialog.set_value("subject", frm.doc.subject); dialog.show(); } }) diff --git a/erpnext/buying/doctype/request_for_quotation/request_for_quotation.json b/erpnext/buying/doctype/request_for_quotation/request_for_quotation.json index c16abb2f41b5..fbfc1ac16935 100644 --- a/erpnext/buying/doctype/request_for_quotation/request_for_quotation.json +++ b/erpnext/buying/doctype/request_for_quotation/request_for_quotation.json @@ -20,11 +20,10 @@ "items_section", "items", "supplier_response_section", - "salutation", - "subject", - "col_break_email_1", "email_template", "preview", + "col_break_email_1", + "html_llwp", "send_attached_files", "sec_break_email_2", "message_for_supplier", @@ -237,23 +236,6 @@ "print_hide": 1, "read_only": 1 }, - { - "fetch_from": "email_template.subject", - "fetch_if_empty": 1, - "fieldname": "subject", - "fieldtype": "Data", - "label": "Subject", - "print_hide": 1 - }, - { - "description": "Select a greeting for the receiver. E.g. Mr., Ms., etc.", - "fieldname": "salutation", - "fieldtype": "Link", - "label": "Salutation", - "no_copy": 1, - "options": "Salutation", - "print_hide": 1 - }, { "fieldname": "col_break_email_1", "fieldtype": "Column Break" @@ -287,6 +269,14 @@ "fieldtype": "Data", "label": "Named Place" }, + { + "fieldname": "html_llwp", + "fieldtype": "HTML", + "options": "

In your Email Template, you can use the following special variables:\n

\n
    \n
  • \n {{ update_password_link }}: A link where your supplier can set a new password to log into your portal.\n
  • \n
  • \n {{ portal_link }}: A link to this RFQ in your supplier portal.\n
  • \n
  • \n {{ supplier_name }}: The company name of your supplier.\n
  • \n
  • \n {{ contact.salutation }} {{ contact.last_name }}: The contact person of your supplier.\n
  • \n {{ user_fullname }}: Your full name.\n
  • \n
\n

\n

Apart from these, you can access all values in this RFQ, like {{ message_for_supplier }} or {{ terms }}.

", + "print_hide": 1, + "read_only": 1, + "report_hide": 1 + }, { "default": "1", "description": "If enabled, all files attached to this document will be attached to each email", @@ -299,7 +289,7 @@ "index_web_pages_for_search": 1, "is_submittable": 1, "links": [], - "modified": "2023-07-27 16:41:48.468873", + "modified": "2023-08-08 16:30:10.870429", "modified_by": "Administrator", "module": "Buying", "name": "Request for Quotation", diff --git a/erpnext/buying/doctype/request_for_quotation/request_for_quotation.py b/erpnext/buying/doctype/request_for_quotation/request_for_quotation.py index 63e393aecd67..e9385777e5dd 100644 --- a/erpnext/buying/doctype/request_for_quotation/request_for_quotation.py +++ b/erpnext/buying/doctype/request_for_quotation/request_for_quotation.py @@ -182,35 +182,28 @@ def supplier_rfq_mail(self, data, update_password_link, rfq_link, preview=False) if full_name == "Guest": full_name = "Administrator" - # send document dict and some important data from suppliers row - # to render message_for_supplier from any template doc_args = self.as_dict() - doc_args.update({"supplier": data.get("supplier"), "supplier_name": data.get("supplier_name")}) - # Get Contact Full Name - supplier_name = None if data.get("contact"): - contact_name = frappe.db.get_value( - "Contact", data.get("contact"), ["first_name", "middle_name", "last_name"] - ) - supplier_name = (" ").join(x for x in contact_name if x) # remove any blank values - - args = { - "update_password_link": update_password_link, - "message": frappe.render_template(self.message_for_supplier, doc_args), - "rfq_link": rfq_link, - "user_fullname": full_name, - "supplier_name": supplier_name or data.get("supplier_name"), - "supplier_salutation": self.salutation or "Dear Mx.", - } + contact = frappe.get_doc("Contact", data.get("contact")) + doc_args["contact"] = contact.as_dict() - subject = self.subject or _("Request for Quotation") - template = "templates/emails/request_for_quotation.html" + doc_args.update( + { + "supplier": data.get("supplier"), + "supplier_name": data.get("supplier_name"), + "update_password_link": f'{_("Set Password")}', + "portal_link": f' {_("Submit your Quotation")} ', + "user_fullname": full_name, + } + ) + email_template = frappe.get_doc("Email Template", self.email_template) + message = frappe.render_template(email_template.response_, doc_args) + subject = frappe.render_template(email_template.subject, doc_args) sender = frappe.session.user not in STANDARD_USERS and frappe.session.user or None - message = frappe.get_template(template).render(args) if preview: - return message + return {"message": message, "subject": subject} attachments = None if self.send_attached_files: diff --git a/erpnext/templates/emails/request_for_quotation.html b/erpnext/templates/emails/request_for_quotation.html deleted file mode 100644 index 5b073e604ff9..000000000000 --- a/erpnext/templates/emails/request_for_quotation.html +++ /dev/null @@ -1,29 +0,0 @@ -

{{_("Request for Quotation")}}

-

{{ supplier_salutation if supplier_salutation else ''}} {{ supplier_name }},

-

{{ message }}

-

{{_("The Request for Quotation can be accessed by clicking on the following button")}}:

-
- - {{ _("Submit your Quotation") }} - -
-
-{% if update_password_link %} -
-

{{_("Please click on the following button to set your new password")}}:

- - {{_("Set Password") }} - -
-
-{% endif %} -

- {{_("Regards")}},
- {{ user_fullname }} -

From 1f76c6972b80d91fe549622556098009468ccb2c Mon Sep 17 00:00:00 2001 From: Shariq Ansari Date: Thu, 17 Aug 2023 11:59:47 +0530 Subject: [PATCH 183/501] fix: check tax and charges if it is passed (cherry picked from commit 7ec6909159bb96a16f368f464f026acaf4354900) --- erpnext/controllers/accounts_controller.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/controllers/accounts_controller.py b/erpnext/controllers/accounts_controller.py index 7afd80b4bcf0..97061baba2b8 100644 --- a/erpnext/controllers/accounts_controller.py +++ b/erpnext/controllers/accounts_controller.py @@ -715,7 +715,7 @@ def set_other_charges(self): def validate_enabled_taxes_and_charges(self): taxes_and_charges_doctype = self.meta.get_options("taxes_and_charges") - if frappe.get_cached_value(taxes_and_charges_doctype, self.taxes_and_charges, "disabled"): + if self.taxes_and_charges and frappe.get_cached_value(taxes_and_charges_doctype, self.taxes_and_charges, "disabled"): frappe.throw( _("{0} '{1}' is disabled").format(taxes_and_charges_doctype, self.taxes_and_charges) ) From f186015f583c26e1374a060b0e99f4a7721b75a1 Mon Sep 17 00:00:00 2001 From: Shariq Ansari <30859809+shariquerik@users.noreply.github.com> Date: Thu, 17 Aug 2023 12:14:13 +0530 Subject: [PATCH 184/501] chore: linter fix (cherry picked from commit 21c1141fdb2fd7adff1056e11a114d6b60523d41) --- erpnext/controllers/accounts_controller.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/erpnext/controllers/accounts_controller.py b/erpnext/controllers/accounts_controller.py index 97061baba2b8..76fe6a91182e 100644 --- a/erpnext/controllers/accounts_controller.py +++ b/erpnext/controllers/accounts_controller.py @@ -715,7 +715,9 @@ def set_other_charges(self): def validate_enabled_taxes_and_charges(self): taxes_and_charges_doctype = self.meta.get_options("taxes_and_charges") - if self.taxes_and_charges and frappe.get_cached_value(taxes_and_charges_doctype, self.taxes_and_charges, "disabled"): + if self.taxes_and_charges and frappe.get_cached_value( + taxes_and_charges_doctype, self.taxes_and_charges, "disabled" + ): frappe.throw( _("{0} '{1}' is disabled").format(taxes_and_charges_doctype, self.taxes_and_charges) ) From 36147ec02c595971dfcb73ec471bf726cad1997e Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Thu, 17 Aug 2023 16:00:17 +0530 Subject: [PATCH 185/501] fix(RFQ): make "update password" and "submit quotation" buttons the same size (backport #36667) (#36686) fix(RFQ): make "update password" and "submit quotation" buttons the same size (#36667) fix(RFQ): button styling Co-authored-by: Raffael Meyer <14891507+barredterra@users.noreply.github.com> --- .../doctype/request_for_quotation/request_for_quotation.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/buying/doctype/request_for_quotation/request_for_quotation.py b/erpnext/buying/doctype/request_for_quotation/request_for_quotation.py index e9385777e5dd..56840c11a6e7 100644 --- a/erpnext/buying/doctype/request_for_quotation/request_for_quotation.py +++ b/erpnext/buying/doctype/request_for_quotation/request_for_quotation.py @@ -193,7 +193,7 @@ def supplier_rfq_mail(self, data, update_password_link, rfq_link, preview=False) "supplier": data.get("supplier"), "supplier_name": data.get("supplier_name"), "update_password_link": f'{_("Set Password")}', - "portal_link": f' {_("Submit your Quotation")} ', + "portal_link": f' {_("Submit your Quotation")} ', "user_fullname": full_name, } ) From 9668615f7e9e602f39c00cd3bd4cbcb09039d2f2 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Thu, 17 Aug 2023 17:13:51 +0530 Subject: [PATCH 186/501] feat: Tick on checkbox to include draft timesheets (backport #36577) (#36640) feat: Tick on checkbox to include draft timesheets (#36577) feat: Tick on Check box to include Draft Timesheets (cherry picked from commit 75652799cd0e5b5e44dbcc418ee623d61adbaaa1) Co-authored-by: ViralKansodiya <141210323+viralkansodiya@users.noreply.github.com> --- erpnext/projects/report/billing_summary.py | 6 ++++-- .../employee_billing_summary/employee_billing_summary.js | 5 +++++ .../project_billing_summary/project_billing_summary.js | 5 +++++ 3 files changed, 14 insertions(+), 2 deletions(-) diff --git a/erpnext/projects/report/billing_summary.py b/erpnext/projects/report/billing_summary.py index bc8f2afb8c90..ac1524a49ddc 100644 --- a/erpnext/projects/report/billing_summary.py +++ b/erpnext/projects/report/billing_summary.py @@ -98,9 +98,11 @@ def get_timesheets(filters): record_filters = [ ["start_date", "<=", filters.to_date], ["end_date", ">=", filters.from_date], - ["docstatus", "=", 1], ] - + if not filters.get("include_draft_timesheets"): + record_filters.append(["docstatus", "=", 1]) + else: + record_filters.append(["docstatus", "!=", 2]) if "employee" in filters: record_filters.append(["employee", "=", filters.employee]) diff --git a/erpnext/projects/report/employee_billing_summary/employee_billing_summary.js b/erpnext/projects/report/employee_billing_summary/employee_billing_summary.js index 13f49ed6bed4..9c904c57872a 100644 --- a/erpnext/projects/report/employee_billing_summary/employee_billing_summary.js +++ b/erpnext/projects/report/employee_billing_summary/employee_billing_summary.js @@ -25,5 +25,10 @@ frappe.query_reports["Employee Billing Summary"] = { default: frappe.datetime.add_days(frappe.datetime.month_start(), -1), reqd: 1 }, + { + fieldname:"include_draft_timesheets", + label: __("Include Timesheets in Draft Status"), + fieldtype: "Check", + }, ] } diff --git a/erpnext/projects/report/project_billing_summary/project_billing_summary.js b/erpnext/projects/report/project_billing_summary/project_billing_summary.js index caac1d86b458..6a6f3677e3fb 100644 --- a/erpnext/projects/report/project_billing_summary/project_billing_summary.js +++ b/erpnext/projects/report/project_billing_summary/project_billing_summary.js @@ -25,5 +25,10 @@ frappe.query_reports["Project Billing Summary"] = { default: frappe.datetime.add_days(frappe.datetime.month_start(),-1), reqd: 1 }, + { + fieldname:"include_draft_timesheets", + label: __("Include Timesheets in Draft Status"), + fieldtype: "Check", + }, ] } From 641fe7738c59bcfe275eaed7020363a90b0896a3 Mon Sep 17 00:00:00 2001 From: Dany Robert Date: Thu, 17 Aug 2023 18:01:15 +0530 Subject: [PATCH 187/501] fix(ux): change batch selection message to alert (#36500) * fix(ux): change batch selection message to alert * chore: linters --- erpnext/stock/doctype/batch/batch.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/erpnext/stock/doctype/batch/batch.py b/erpnext/stock/doctype/batch/batch.py index 1843c6e7975b..f377f94a8cf8 100644 --- a/erpnext/stock/doctype/batch/batch.py +++ b/erpnext/stock/doctype/batch/batch.py @@ -297,7 +297,9 @@ def get_batch_no(item_code, warehouse, qty=1, throw=False, serial_no=None): frappe.msgprint( _( "Please select a Batch for Item {0}. Unable to find a single batch that fulfills this requirement" - ).format(frappe.bold(item_code)) + ).format(frappe.bold(item_code)), + indicator="yellow", + alert=(not throw), ) if throw: raise UnableToSelectBatchError From 2aa3b30b56801168b0082a98a5f2cd4a0e818b38 Mon Sep 17 00:00:00 2001 From: Frappe PR Bot Date: Thu, 17 Aug 2023 12:32:49 +0000 Subject: [PATCH 188/501] chore(release): Bumped to Version 14.35.1 ## [14.35.1](https://github.com/frappe/erpnext/compare/v14.35.0...v14.35.1) (2023-08-17) ### Bug Fixes * **ux:** change batch selection message to alert ([#36500](https://github.com/frappe/erpnext/issues/36500)) ([641fe77](https://github.com/frappe/erpnext/commit/641fe7738c59bcfe275eaed7020363a90b0896a3)) --- erpnext/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/__init__.py b/erpnext/__init__.py index 1aab20134b7b..8d4340c59de9 100644 --- a/erpnext/__init__.py +++ b/erpnext/__init__.py @@ -3,7 +3,7 @@ import frappe -__version__ = "14.35.0" +__version__ = "14.35.1" def get_default_company(user=None): From 0a4947a8f612244822b95fdbc94cf679a7855286 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Thu, 17 Aug 2023 18:03:58 +0530 Subject: [PATCH 189/501] fix(ux): change batch selection message to alert (backport #36500) (#36697) fix(ux): change batch selection message to alert (#36500) * fix(ux): change batch selection message to alert * chore: linters (cherry picked from commit 641fe7738c59bcfe275eaed7020363a90b0896a3) Co-authored-by: Dany Robert --- erpnext/stock/doctype/batch/batch.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/erpnext/stock/doctype/batch/batch.py b/erpnext/stock/doctype/batch/batch.py index 1843c6e7975b..f377f94a8cf8 100644 --- a/erpnext/stock/doctype/batch/batch.py +++ b/erpnext/stock/doctype/batch/batch.py @@ -297,7 +297,9 @@ def get_batch_no(item_code, warehouse, qty=1, throw=False, serial_no=None): frappe.msgprint( _( "Please select a Batch for Item {0}. Unable to find a single batch that fulfills this requirement" - ).format(frappe.bold(item_code)) + ).format(frappe.bold(item_code)), + indicator="yellow", + alert=(not throw), ) if throw: raise UnableToSelectBatchError From ee5a1d91ff2961919ad996e9e5b9f2ec152426e0 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Thu, 17 Aug 2023 20:45:53 +0530 Subject: [PATCH 190/501] chore: resolve conflicts --- .../js/bank_reconciliation_tool/dialog_manager.js | 14 -------------- 1 file changed, 14 deletions(-) diff --git a/erpnext/public/js/bank_reconciliation_tool/dialog_manager.js b/erpnext/public/js/bank_reconciliation_tool/dialog_manager.js index 4e2de8b0ff8e..ddc105305816 100644 --- a/erpnext/public/js/bank_reconciliation_tool/dialog_manager.js +++ b/erpnext/public/js/bank_reconciliation_tool/dialog_manager.js @@ -144,20 +144,6 @@ erpnext.accounts.bank_reconciliation.DialogManager = class DialogManager { ]; } -<<<<<<< HEAD -======= - format_row(row) { - return [ - row[1], // Document Type - row[2], // Document Name - row[5] || row[8], // Reference Date - format_currency(row[3], row[9]), // Remaining - row[4], // Reference Number - row[6], // Party - ]; - } - ->>>>>>> 7ab55b1bb2 (fix: Document Name link validation in Bank Reconciliation Tool (#36495)) get_datatable(proposals_wrapper) { if (!this.datatable) { const datatable_options = { From 3198f2669dcbb638d5bdfac3575722918dbd8046 Mon Sep 17 00:00:00 2001 From: Gursheen Anand Date: Sat, 15 Jul 2023 19:32:56 +0530 Subject: [PATCH 191/501] fix: make offsetting entry for acc dimensions (cherry picked from commit d3759b397160ec343fc3085db0a37552c78b28c0) --- .../accounting_dimension_detail.json | 22 +++++++++- .../doctype/journal_entry/journal_entry.py | 44 +++++++++++++++++++ 2 files changed, 65 insertions(+), 1 deletion(-) diff --git a/erpnext/accounts/doctype/accounting_dimension_detail/accounting_dimension_detail.json b/erpnext/accounts/doctype/accounting_dimension_detail/accounting_dimension_detail.json index e9e1f43f9908..7b6120a583b6 100644 --- a/erpnext/accounts/doctype/accounting_dimension_detail/accounting_dimension_detail.json +++ b/erpnext/accounts/doctype/accounting_dimension_detail/accounting_dimension_detail.json @@ -8,7 +8,10 @@ "reference_document", "default_dimension", "mandatory_for_bs", - "mandatory_for_pl" + "mandatory_for_pl", + "column_break_lqns", + "automatically_post_balancing_accounting_entry", + "offsetting_account" ], "fields": [ { @@ -50,6 +53,23 @@ "fieldtype": "Check", "in_list_view": 1, "label": "Mandatory For Profit and Loss Account" + }, + { + "default": "0", + "fieldname": "automatically_post_balancing_accounting_entry", + "fieldtype": "Check", + "label": "Automatically post balancing accounting entry" + }, + { + "fieldname": "offsetting_account", + "fieldtype": "Link", + "label": "Offsetting Account", + "mandatory_depends_on": "eval: doc.automatically_post_balancing_accounting_entry", + "options": "Account" + }, + { + "fieldname": "column_break_lqns", + "fieldtype": "Column Break" } ], "istable": 1, diff --git a/erpnext/accounts/doctype/journal_entry/journal_entry.py b/erpnext/accounts/doctype/journal_entry/journal_entry.py index f68980261349..bb0d13664197 100644 --- a/erpnext/accounts/doctype/journal_entry/journal_entry.py +++ b/erpnext/accounts/doctype/journal_entry/journal_entry.py @@ -913,8 +913,52 @@ def build_gl_map(self): item=d, ) ) + + self.make_acc_dimensions_offsetting_entry(gl_map, d) + return gl_map + def make_acc_dimensions_offsetting_entry(self, gl_map, d): + accounting_dimensions = frappe.db.get_list("Accounting Dimension", {"disabled": 0}, pluck="name") + for dimension in accounting_dimensions: + dimension_details = frappe.db.get_values( + "Accounting Dimension Detail", + {"parent": dimension}, + ["automatically_post_balancing_accounting_entry", "offsetting_account"], + )[0] + if dimension_details[0] == 1: + offsetting_account = dimension_details[1] + gl_map.append( + self.get_gl_dict( + { + "account": offsetting_account, + "party_type": d.party_type, + "due_date": self.due_date, + "party": d.party, + "against": d.against_account, + "debit": flt(d.credit, d.precision("credit")), + "credit": flt(d.debit, d.precision("credit")), + "account_currency": d.account_currency, + "debit_in_account_currency": flt( + d.credit_in_account_currency, d.precision("credit_in_account_currency") + ), + "credit_in_account_currency": flt( + d.debit_in_account_currency, d.precision("debit_in_account_currency") + ), + "against_voucher_type": d.reference_type, + "against_voucher": d.reference_name, + "remarks": _( + "Offsetting for Accounting Dimension - {dimension}".format(dimension=dimension) + ), + "voucher_detail_no": d.reference_detail_no, + "cost_center": d.cost_center, + "project": d.project, + "finance_book": self.finance_book, + }, + item=d, + ) + ) + def make_gl_entries(self, cancel=0, adv_adj=0): from erpnext.accounts.general_ledger import make_gl_entries From c1f1a21714423d44c2469a2a867ccbb996395113 Mon Sep 17 00:00:00 2001 From: Gursheen Anand Date: Mon, 17 Jul 2023 11:47:33 +0530 Subject: [PATCH 192/501] fix: fetch accounting dimension details specific to company (cherry picked from commit 4e09de4db2d13fc251d36378cc9aaff775ff910c) --- .../accounts/doctype/journal_entry/journal_entry.py | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/erpnext/accounts/doctype/journal_entry/journal_entry.py b/erpnext/accounts/doctype/journal_entry/journal_entry.py index bb0d13664197..e8d90da23d29 100644 --- a/erpnext/accounts/doctype/journal_entry/journal_entry.py +++ b/erpnext/accounts/doctype/journal_entry/journal_entry.py @@ -923,10 +923,11 @@ def make_acc_dimensions_offsetting_entry(self, gl_map, d): for dimension in accounting_dimensions: dimension_details = frappe.db.get_values( "Accounting Dimension Detail", - {"parent": dimension}, + {"parent": dimension, "company": self.company}, ["automatically_post_balancing_accounting_entry", "offsetting_account"], - )[0] - if dimension_details[0] == 1: + ) + dimension_details = dimension_details[0] if len(dimension_details) > 0 else None + if dimension_details and dimension_details[0] == 1: offsetting_account = dimension_details[1] gl_map.append( self.get_gl_dict( @@ -947,9 +948,7 @@ def make_acc_dimensions_offsetting_entry(self, gl_map, d): ), "against_voucher_type": d.reference_type, "against_voucher": d.reference_name, - "remarks": _( - "Offsetting for Accounting Dimension - {dimension}".format(dimension=dimension) - ), + "remarks": _("Offsetting for Accounting Dimension") + " - {0}".format(dimension), "voucher_detail_no": d.reference_detail_no, "cost_center": d.cost_center, "project": d.project, From f578c3219d517a7ba102ab4e270102b4eb6e9ded Mon Sep 17 00:00:00 2001 From: Gursheen Anand Date: Mon, 17 Jul 2023 15:17:53 +0530 Subject: [PATCH 193/501] fix: make offsetting entry for all doctypes (cherry picked from commit 22ba12172fc1143efd778aee4cd296cf6970d736) --- .../doctype/journal_entry/journal_entry.py | 43 ------------------ erpnext/accounts/general_ledger.py | 45 +++++++++++++++++++ 2 files changed, 45 insertions(+), 43 deletions(-) diff --git a/erpnext/accounts/doctype/journal_entry/journal_entry.py b/erpnext/accounts/doctype/journal_entry/journal_entry.py index e8d90da23d29..f68980261349 100644 --- a/erpnext/accounts/doctype/journal_entry/journal_entry.py +++ b/erpnext/accounts/doctype/journal_entry/journal_entry.py @@ -913,51 +913,8 @@ def build_gl_map(self): item=d, ) ) - - self.make_acc_dimensions_offsetting_entry(gl_map, d) - return gl_map - def make_acc_dimensions_offsetting_entry(self, gl_map, d): - accounting_dimensions = frappe.db.get_list("Accounting Dimension", {"disabled": 0}, pluck="name") - for dimension in accounting_dimensions: - dimension_details = frappe.db.get_values( - "Accounting Dimension Detail", - {"parent": dimension, "company": self.company}, - ["automatically_post_balancing_accounting_entry", "offsetting_account"], - ) - dimension_details = dimension_details[0] if len(dimension_details) > 0 else None - if dimension_details and dimension_details[0] == 1: - offsetting_account = dimension_details[1] - gl_map.append( - self.get_gl_dict( - { - "account": offsetting_account, - "party_type": d.party_type, - "due_date": self.due_date, - "party": d.party, - "against": d.against_account, - "debit": flt(d.credit, d.precision("credit")), - "credit": flt(d.debit, d.precision("credit")), - "account_currency": d.account_currency, - "debit_in_account_currency": flt( - d.credit_in_account_currency, d.precision("credit_in_account_currency") - ), - "credit_in_account_currency": flt( - d.debit_in_account_currency, d.precision("debit_in_account_currency") - ), - "against_voucher_type": d.reference_type, - "against_voucher": d.reference_name, - "remarks": _("Offsetting for Accounting Dimension") + " - {0}".format(dimension), - "voucher_detail_no": d.reference_detail_no, - "cost_center": d.cost_center, - "project": d.project, - "finance_book": self.finance_book, - }, - item=d, - ) - ) - def make_gl_entries(self, cancel=0, adv_adj=0): from erpnext.accounts.general_ledger import make_gl_entries diff --git a/erpnext/accounts/general_ledger.py b/erpnext/accounts/general_ledger.py index 5d4035ee65e9..4b837e2cd710 100644 --- a/erpnext/accounts/general_ledger.py +++ b/erpnext/accounts/general_ledger.py @@ -27,6 +27,7 @@ def make_gl_entries( from_repost=False, ): if gl_map: + make_acc_dimensions_offsetting_entry(gl_map) if not cancel: validate_accounting_period(gl_map) validate_disabled_accounts(gl_map) @@ -51,6 +52,50 @@ def make_gl_entries( make_reverse_gl_entries(gl_map, adv_adj=adv_adj, update_outstanding=update_outstanding) +def make_acc_dimensions_offsetting_entry(gl_map): + accounting_dimensions_to_offset = get_accounting_dimensions_for_offsetting_entry(gl_map) + if len(accounting_dimensions_to_offset) == 0: + return + + offsetting_entries = [] + for gle in gl_map: + for dimension in accounting_dimensions_to_offset: + dimension_details = frappe.db.get_values( + "Accounting Dimension Detail", + {"parent": dimension, "company": gle.company}, + ["automatically_post_balancing_accounting_entry", "offsetting_account"], + ) + dimension_details = dimension_details[0] if len(dimension_details) > 0 else None + if dimension_details and dimension_details[0] == 1: + offsetting_account = dimension_details[1] + offsetting_entry = gle.copy() + offsetting_entry.update( + { + "account": offsetting_account, + "debit": flt(gle.credit), + "credit": flt(gle.debit), + "debit_in_account_currency": flt(gle.credit_in_account_currency), + "credit_in_account_currency": flt(gle.debit_in_account_currency), + "remarks": _("Offsetting for Accounting Dimension") + " - {0}".format(dimension), + "against_voucher": None, + } + ) + offsetting_entry["against_voucher_type"] = None + offsetting_entries.append(offsetting_entry) + gl_map += offsetting_entries + + +def get_accounting_dimensions_for_offsetting_entry(gl_map): + acc_dimensions = frappe.db.get_list("Accounting Dimension", {"disabled": 0}, pluck="name") + accounting_dimensions_to_offset = [] + for acc_dimension in acc_dimensions: + fieldname = acc_dimension.lower().replace(" ", "_") + values = set([entry[fieldname] for entry in gl_map]) + if len(values) > 1: + accounting_dimensions_to_offset.append(acc_dimension) + return accounting_dimensions_to_offset + + def validate_disabled_accounts(gl_map): accounts = [d.account for d in gl_map if d.account] From 248d4082c0900a9583e06f79f73e9158884a6170 Mon Sep 17 00:00:00 2001 From: Gursheen Anand Date: Mon, 17 Jul 2023 17:57:03 +0530 Subject: [PATCH 194/501] test: TB report balanced whenfiltered using acc dimension (cherry picked from commit 4004427892aedf332e2b5ae3c47c4debcdf6ae05) --- .../trial_balance/test_trial_balance.py | 111 ++++++++++++++++++ 1 file changed, 111 insertions(+) create mode 100644 erpnext/accounts/report/trial_balance/test_trial_balance.py diff --git a/erpnext/accounts/report/trial_balance/test_trial_balance.py b/erpnext/accounts/report/trial_balance/test_trial_balance.py new file mode 100644 index 000000000000..d4d055464f6d --- /dev/null +++ b/erpnext/accounts/report/trial_balance/test_trial_balance.py @@ -0,0 +1,111 @@ +# Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and Contributors +# MIT License. See license.txt + +import frappe +from frappe.tests.utils import FrappeTestCase +from frappe.utils import today + +from erpnext.accounts.report.trial_balance.trial_balance import execute + + +class TestTrialBalance(FrappeTestCase): + def setUp(self): + from erpnext.accounts.doctype.account.test_account import create_account + from erpnext.accounts.doctype.cost_center.test_cost_center import create_cost_center + from erpnext.accounts.utils import get_fiscal_year + + self.company = create_company() + create_cost_center( + cost_center_name="Test Cost Center", + company="Trial Balance Company", + parent_cost_center="Trial Balance Company - TBC", + ) + create_account( + account_name="Offsetting", + company="Trial Balance Company", + parent_account="Temporary Accounts - TBC", + ) + self.fiscal_year = get_fiscal_year(today(), company="Trial Balance Company")[0] + create_accounting_dimension() + + def test_offsetting_entries_for_accounting_dimensions(self): + """ + Checks if Trial Balance Report is balanced when filtered using a particular Accounting Dimension + """ + from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice + + frappe.db.sql("delete from `tabSales Invoice` where company='Trial Balance Company'") + frappe.db.sql("delete from `tabGL Entry` where company='Trial Balance Company'") + + branch1 = frappe.new_doc("Branch") + branch1.branch = "Location 1" + branch1.insert() + branch2 = frappe.new_doc("Branch") + branch2.branch = "Location 2" + branch2.insert() + + si = create_sales_invoice( + company=self.company, + debit_to="Debtors - TBC", + cost_center="Test Cost Center - TBC", + income_account="Sales - TBC", + do_not_submit=1, + ) + si.branch = "Location 1" + si.items[0].branch = "Location 2" + si.save() + si.submit() + + filters = frappe._dict( + {"company": self.company, "fiscal_year": self.fiscal_year, "branch": ["Location 1"]} + ) + total_row = execute(filters)[1][-1] + self.assertEqual(total_row["debit"], total_row["credit"]) + + def tearDown(self): + disable_dimension() + + +def create_company(**args): + args = frappe._dict(args) + company = frappe.get_doc( + { + "doctype": "Company", + "company_name": args.company_name or "Trial Balance Company", + "country": args.country or "India", + "default_currency": args.currency or "INR", + } + ) + company.insert(ignore_if_duplicate=True) + return company.name + + +def create_accounting_dimension(**args): + args = frappe._dict(args) + document_type = args.document_type or "Branch" + if not frappe.db.exists("Accounting Dimension", {"document_type": "Branch"}): + accounting_dimension = frappe.get_doc( + {"doctype": "Accounting Dimension", "document_type": document_type} + ).insert() + else: + accounting_dimension = frappe.get_doc("Accounting Dimension", document_type) + accounting_dimension.disabled = 0 + + accounting_dimension.append( + "dimension_defaults", + { + "company": args.company or "Trial Balance Company", + "automatically_post_balancing_accounting_entry": 1, + "offsetting_account": args.offsetting_account or "Offsetting - TBC", + }, + ) + accounting_dimension.save() + return accounting_dimension.name + + +def disable_dimension(**args): + args = frappe._dict(args) + document_type = args.document_type or "Branch" + dimension = frappe.get_doc("Accounting Dimension", document_type) + dimension.disabled = 1 + dimension.save() From 11ba553dbd72996de1118c27743dea30267a4244 Mon Sep 17 00:00:00 2001 From: Gursheen Anand Date: Mon, 17 Jul 2023 18:40:52 +0530 Subject: [PATCH 195/501] fix: dict value for dimension for gl entries defined without the dimension (cherry picked from commit ed3bef1840ac7482f673a69f01dfdb7608037955) --- erpnext/accounts/general_ledger.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/accounts/general_ledger.py b/erpnext/accounts/general_ledger.py index 4b837e2cd710..30c6d19f90b1 100644 --- a/erpnext/accounts/general_ledger.py +++ b/erpnext/accounts/general_ledger.py @@ -90,7 +90,7 @@ def get_accounting_dimensions_for_offsetting_entry(gl_map): accounting_dimensions_to_offset = [] for acc_dimension in acc_dimensions: fieldname = acc_dimension.lower().replace(" ", "_") - values = set([entry[fieldname] for entry in gl_map]) + values = set([entry.get(fieldname) for entry in gl_map]) if len(values) > 1: accounting_dimensions_to_offset.append(acc_dimension) return accounting_dimensions_to_offset From 1269f2d301fe03aaf36d3c2b749d6774b6e1e786 Mon Sep 17 00:00:00 2001 From: Gursheen Anand Date: Tue, 18 Jul 2023 12:12:24 +0530 Subject: [PATCH 196/501] fix: divide offsetting amount for multiple dimensions (cherry picked from commit 1e1e4b93c17e7c30f14a59237ffa54cfe093594c) --- erpnext/accounts/general_ledger.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/erpnext/accounts/general_ledger.py b/erpnext/accounts/general_ledger.py index 30c6d19f90b1..987db65d49e3 100644 --- a/erpnext/accounts/general_ledger.py +++ b/erpnext/accounts/general_ledger.py @@ -54,7 +54,8 @@ def make_gl_entries( def make_acc_dimensions_offsetting_entry(gl_map): accounting_dimensions_to_offset = get_accounting_dimensions_for_offsetting_entry(gl_map) - if len(accounting_dimensions_to_offset) == 0: + no_of_dimensions = len(accounting_dimensions_to_offset) + if no_of_dimensions == 0: return offsetting_entries = [] @@ -72,10 +73,10 @@ def make_acc_dimensions_offsetting_entry(gl_map): offsetting_entry.update( { "account": offsetting_account, - "debit": flt(gle.credit), - "credit": flt(gle.debit), - "debit_in_account_currency": flt(gle.credit_in_account_currency), - "credit_in_account_currency": flt(gle.debit_in_account_currency), + "debit": flt(gle.credit) / no_of_dimensions if gle.credit != 0 else 0, + "credit": flt(gle.debit) / no_of_dimensions if gle.debit != 0 else 0, + "debit_in_account_currency": flt(gle.credit) / no_of_dimensions if gle.credit != 0 else 0, + "credit_in_account_currency": flt(gle.debit) / no_of_dimensions if gle.debit != 0 else 0, "remarks": _("Offsetting for Accounting Dimension") + " - {0}".format(dimension), "against_voucher": None, } From 2c8c3a022c28314e87eaafd3ef7c991a86e8f8af Mon Sep 17 00:00:00 2001 From: Gursheen Anand Date: Tue, 18 Jul 2023 12:51:09 +0530 Subject: [PATCH 197/501] fix: divide offsetting amount only when account exists (cherry picked from commit 3a3ffa23076f368c4c24fbec1777205a12c76146) --- erpnext/accounts/general_ledger.py | 57 +++++++++++++++++------------- 1 file changed, 32 insertions(+), 25 deletions(-) diff --git a/erpnext/accounts/general_ledger.py b/erpnext/accounts/general_ledger.py index 987db65d49e3..d9c7a6a7ebe5 100644 --- a/erpnext/accounts/general_ledger.py +++ b/erpnext/accounts/general_ledger.py @@ -53,7 +53,9 @@ def make_gl_entries( def make_acc_dimensions_offsetting_entry(gl_map): - accounting_dimensions_to_offset = get_accounting_dimensions_for_offsetting_entry(gl_map) + accounting_dimensions_to_offset = get_accounting_dimensions_for_offsetting_entry( + gl_map, gl_map[0].company + ) no_of_dimensions = len(accounting_dimensions_to_offset) if no_of_dimensions == 0: return @@ -61,36 +63,41 @@ def make_acc_dimensions_offsetting_entry(gl_map): offsetting_entries = [] for gle in gl_map: for dimension in accounting_dimensions_to_offset: - dimension_details = frappe.db.get_values( - "Accounting Dimension Detail", - {"parent": dimension, "company": gle.company}, - ["automatically_post_balancing_accounting_entry", "offsetting_account"], + offsetting_account = dimension.offsetting_account + offsetting_entry = gle.copy() + offsetting_entry.update( + { + "account": offsetting_account, + "debit": flt(gle.credit) / no_of_dimensions if gle.credit != 0 else 0, + "credit": flt(gle.debit) / no_of_dimensions if gle.debit != 0 else 0, + "debit_in_account_currency": flt(gle.credit) / no_of_dimensions if gle.credit != 0 else 0, + "credit_in_account_currency": flt(gle.debit) / no_of_dimensions if gle.debit != 0 else 0, + "remarks": _("Offsetting for Accounting Dimension") + " - {0}".format(dimension.name), + "against_voucher": None, + } ) - dimension_details = dimension_details[0] if len(dimension_details) > 0 else None - if dimension_details and dimension_details[0] == 1: - offsetting_account = dimension_details[1] - offsetting_entry = gle.copy() - offsetting_entry.update( - { - "account": offsetting_account, - "debit": flt(gle.credit) / no_of_dimensions if gle.credit != 0 else 0, - "credit": flt(gle.debit) / no_of_dimensions if gle.debit != 0 else 0, - "debit_in_account_currency": flt(gle.credit) / no_of_dimensions if gle.credit != 0 else 0, - "credit_in_account_currency": flt(gle.debit) / no_of_dimensions if gle.debit != 0 else 0, - "remarks": _("Offsetting for Accounting Dimension") + " - {0}".format(dimension), - "against_voucher": None, - } - ) - offsetting_entry["against_voucher_type"] = None - offsetting_entries.append(offsetting_entry) + offsetting_entry["against_voucher_type"] = None + offsetting_entries.append(offsetting_entry) gl_map += offsetting_entries -def get_accounting_dimensions_for_offsetting_entry(gl_map): - acc_dimensions = frappe.db.get_list("Accounting Dimension", {"disabled": 0}, pluck="name") +def get_accounting_dimensions_for_offsetting_entry(gl_map, company): + acc_dimension = frappe.qb.DocType("Accounting Dimension") + dimension_detail = frappe.qb.DocType("Accounting Dimension Detail") + acc_dimensions = ( + frappe.qb.from_(acc_dimension) + .inner_join(dimension_detail) + .on(acc_dimension.name == dimension_detail.parent) + .select(acc_dimension.name, dimension_detail.offsetting_account) + .where( + (acc_dimension.disabled == 0) + & (dimension_detail.company == company) + & (dimension_detail.automatically_post_balancing_accounting_entry == 1) + ) + ).run(as_dict=True) accounting_dimensions_to_offset = [] for acc_dimension in acc_dimensions: - fieldname = acc_dimension.lower().replace(" ", "_") + fieldname = acc_dimension.name.lower().replace(" ", "_") values = set([entry.get(fieldname) for entry in gl_map]) if len(values) > 1: accounting_dimensions_to_offset.append(acc_dimension) From 8530a28c62e935b735932767d669374db7d5b9e9 Mon Sep 17 00:00:00 2001 From: Gursheen Anand Date: Tue, 18 Jul 2023 15:51:01 +0530 Subject: [PATCH 198/501] test: PI offsetting entry for accounting dimension (cherry picked from commit 77deac4fb904a6a727afbd0eb174c2abbaed8834) # Conflicts: # erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py --- .../purchase_invoice/test_purchase_invoice.py | 108 ++++++++++++++++++ 1 file changed, 108 insertions(+) diff --git a/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py b/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py index f60c83dcf5cc..df371c9ef771 100644 --- a/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py +++ b/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py @@ -1725,10 +1725,108 @@ def test_gl_entries_for_standalone_debit_note(self): rate = flt(sle.stock_value_difference) / flt(sle.actual_qty) self.assertAlmostEqual(returned_inv.items[0].rate, rate) +<<<<<<< HEAD def test_payment_allocation_for_payment_terms(self): from erpnext.buying.doctype.purchase_order.test_purchase_order import ( create_pr_against_po, create_purchase_order, +======= + def test_offsetting_entries_for_accounting_dimensions(self): + from erpnext.accounts.doctype.account.test_account import create_account + + create_account( + account_name="Offsetting", + company="_Test Company", + parent_account="Temporary Accounts - _TC", + ) + + clear_dimension_defaults("Branch") + accounting_dimension = frappe.get_doc("Accounting Dimension", "Branch") + accounting_dimension.disabled = 0 + accounting_dimension.append( + "dimension_defaults", + { + "company": "_Test Company", + "automatically_post_balancing_accounting_entry": 1, + "offsetting_account": "Offsetting - _TC", + }, + ) + accounting_dimension.save() + + branch1 = frappe.new_doc("Branch") + branch1.branch = "Location 1" + branch1.insert(ignore_if_duplicate=True) + branch2 = frappe.new_doc("Branch") + branch2.branch = "Location 2" + branch2.insert(ignore_if_duplicate=True) + + pi = make_purchase_invoice( + company="_Test Company", + customer="_Test Supplier", + do_not_save=True, + do_not_submit=True, + rate=1000, + price_list_rate=1000, + qty=1, + ) + pi.branch = branch1.branch + pi.items[0].branch = branch2.branch + pi.save() + pi.submit() + + expected_gle = [ + ["_Test Account Cost for Goods Sold - _TC", 1000, 0.0, nowdate(), {"branch": branch2.branch}], + ["Creditors - _TC", 0.0, 1000, nowdate(), {"branch": branch1.branch}], + ["Offsetting - _TC", 1000, 0.0, nowdate(), {"branch": branch1.branch}], + ["Offsetting - _TC", 0.0, 1000, nowdate(), {"branch": branch2.branch}], + ] + + check_gl_entries( + self, + pi.name, + expected_gle, + nowdate(), + voucher_type="Purchase Invoice", + check_acc_dimensions=True, + ) + clear_dimension_defaults("Branch") + + +def clear_dimension_defaults(dimension_name): + accounting_dimension = frappe.get_doc("Accounting Dimension", dimension_name) + accounting_dimension.dimension_defaults = [] + accounting_dimension.save() + + +def set_advance_flag(company, flag, default_account): + frappe.db.set_value( + "Company", + company, + { + "book_advance_payments_in_separate_party_account": flag, + "default_advance_paid_account": default_account, + }, + ) + + +def check_gl_entries( + doc, + voucher_no, + expected_gle, + posting_date, + voucher_type="Purchase Invoice", + check_acc_dimensions=False, +): + gl = frappe.qb.DocType("GL Entry") + query = ( + frappe.qb.from_(gl) + .select(gl.account, gl.debit, gl.credit, gl.posting_date) + .where( + (gl.voucher_type == voucher_type) + & (gl.voucher_no == voucher_no) + & (gl.posting_date >= posting_date) + & (gl.is_cancelled == 0) +>>>>>>> 77deac4fb9 (test: PI offsetting entry for accounting dimension) ) from erpnext.selling.doctype.sales_order.test_sales_order import ( automatically_fetch_payment_terms, @@ -1781,12 +1879,22 @@ def check_gl_entries(doc, voucher_no, expected_gle, posting_date): (voucher_no, posting_date), as_dict=1, ) +<<<<<<< HEAD +======= + if check_acc_dimensions: + for col in list(expected_gle[0][4].keys()): + query = query.select(col) + gl_entries = query.run(as_dict=True) +>>>>>>> 77deac4fb9 (test: PI offsetting entry for accounting dimension) for i, gle in enumerate(gl_entries): doc.assertEqual(expected_gle[i][0], gle.account) doc.assertEqual(expected_gle[i][1], gle.debit) doc.assertEqual(expected_gle[i][2], gle.credit) doc.assertEqual(getdate(expected_gle[i][3]), gle.posting_date) + if check_acc_dimensions: + for acc_dimension in expected_gle[i][4]: + doc.assertEqual(expected_gle[i][4][acc_dimension], gle[acc_dimension]) def create_tax_witholding_category(category_name, company, account): From cdb66bf19824469b55b37c5f6f93b0022fabdd0c Mon Sep 17 00:00:00 2001 From: Gursheen Anand Date: Wed, 19 Jul 2023 12:02:26 +0530 Subject: [PATCH 199/501] fix: duplicate acc dimension in test (cherry picked from commit b3f6d991b5fd8347311ef05506237f409c1b327b) --- .../purchase_invoice/test_purchase_invoice.py | 23 ++++--------------- .../trial_balance/test_trial_balance.py | 12 ++++++++-- 2 files changed, 15 insertions(+), 20 deletions(-) diff --git a/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py b/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py index df371c9ef771..4c094e86c96f 100644 --- a/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py +++ b/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py @@ -1733,6 +1733,10 @@ def test_payment_allocation_for_payment_terms(self): ======= def test_offsetting_entries_for_accounting_dimensions(self): from erpnext.accounts.doctype.account.test_account import create_account + from erpnext.accounts.report.trial_balance.test_trial_balance import ( + clear_dimension_defaults, + create_accounting_dimension, + ) create_account( account_name="Offsetting", @@ -1740,18 +1744,7 @@ def test_offsetting_entries_for_accounting_dimensions(self): parent_account="Temporary Accounts - _TC", ) - clear_dimension_defaults("Branch") - accounting_dimension = frappe.get_doc("Accounting Dimension", "Branch") - accounting_dimension.disabled = 0 - accounting_dimension.append( - "dimension_defaults", - { - "company": "_Test Company", - "automatically_post_balancing_accounting_entry": 1, - "offsetting_account": "Offsetting - _TC", - }, - ) - accounting_dimension.save() + create_accounting_dimension() branch1 = frappe.new_doc("Branch") branch1.branch = "Location 1" @@ -1792,12 +1785,6 @@ def test_offsetting_entries_for_accounting_dimensions(self): clear_dimension_defaults("Branch") -def clear_dimension_defaults(dimension_name): - accounting_dimension = frappe.get_doc("Accounting Dimension", dimension_name) - accounting_dimension.dimension_defaults = [] - accounting_dimension.save() - - def set_advance_flag(company, flag, default_account): frappe.db.set_value( "Company", diff --git a/erpnext/accounts/report/trial_balance/test_trial_balance.py b/erpnext/accounts/report/trial_balance/test_trial_balance.py index d4d055464f6d..1a29e9f69206 100644 --- a/erpnext/accounts/report/trial_balance/test_trial_balance.py +++ b/erpnext/accounts/report/trial_balance/test_trial_balance.py @@ -39,10 +39,10 @@ def test_offsetting_entries_for_accounting_dimensions(self): branch1 = frappe.new_doc("Branch") branch1.branch = "Location 1" - branch1.insert() + branch1.insert(ignore_if_duplicate=True) branch2 = frappe.new_doc("Branch") branch2.branch = "Location 2" - branch2.insert() + branch2.insert(ignore_if_duplicate=True) si = create_sales_invoice( company=self.company, @@ -91,6 +91,8 @@ def create_accounting_dimension(**args): accounting_dimension = frappe.get_doc("Accounting Dimension", document_type) accounting_dimension.disabled = 0 + clear_dimension_defaults(document_type) + accounting_dimension.load_from_db() accounting_dimension.append( "dimension_defaults", { @@ -109,3 +111,9 @@ def disable_dimension(**args): dimension = frappe.get_doc("Accounting Dimension", document_type) dimension.disabled = 1 dimension.save() + + +def clear_dimension_defaults(dimension_name): + accounting_dimension = frappe.get_doc("Accounting Dimension", dimension_name) + accounting_dimension.dimension_defaults = [] + accounting_dimension.save() From 7ac35b496aabac0344175b61fbf0bbe6945539fe Mon Sep 17 00:00:00 2001 From: Gursheen Anand Date: Wed, 19 Jul 2023 12:26:57 +0530 Subject: [PATCH 200/501] fix: fetch acc dimensions correctly when fieldname is different from name (cherry picked from commit e19a6f5dcba50f70ad1378a9b19236be4e1d7ab1) --- erpnext/accounts/general_ledger.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/erpnext/accounts/general_ledger.py b/erpnext/accounts/general_ledger.py index d9c7a6a7ebe5..901a49a498e5 100644 --- a/erpnext/accounts/general_ledger.py +++ b/erpnext/accounts/general_ledger.py @@ -88,7 +88,7 @@ def get_accounting_dimensions_for_offsetting_entry(gl_map, company): frappe.qb.from_(acc_dimension) .inner_join(dimension_detail) .on(acc_dimension.name == dimension_detail.parent) - .select(acc_dimension.name, dimension_detail.offsetting_account) + .select(acc_dimension.fieldname, dimension_detail.offsetting_account) .where( (acc_dimension.disabled == 0) & (dimension_detail.company == company) @@ -97,8 +97,7 @@ def get_accounting_dimensions_for_offsetting_entry(gl_map, company): ).run(as_dict=True) accounting_dimensions_to_offset = [] for acc_dimension in acc_dimensions: - fieldname = acc_dimension.name.lower().replace(" ", "_") - values = set([entry.get(fieldname) for entry in gl_map]) + values = set([entry.get(acc_dimension.fieldname) for entry in gl_map]) if len(values) > 1: accounting_dimensions_to_offset.append(acc_dimension) return accounting_dimensions_to_offset From 2a467a9fbff26e01c77e45d7a297387ad1d83030 Mon Sep 17 00:00:00 2001 From: Gursheen Anand Date: Wed, 19 Jul 2023 16:07:32 +0530 Subject: [PATCH 201/501] fix: clear dimension defaults after test (cherry picked from commit 23e56d3ec18f0c8006bf9709520ee48847529bcf) --- .../accounts/doctype/purchase_invoice/test_purchase_invoice.py | 2 ++ erpnext/accounts/report/trial_balance/test_trial_balance.py | 3 +-- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py b/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py index 4c094e86c96f..adb3556ebcd6 100644 --- a/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py +++ b/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py @@ -1736,6 +1736,7 @@ def test_offsetting_entries_for_accounting_dimensions(self): from erpnext.accounts.report.trial_balance.test_trial_balance import ( clear_dimension_defaults, create_accounting_dimension, + disable_dimension, ) create_account( @@ -1783,6 +1784,7 @@ def test_offsetting_entries_for_accounting_dimensions(self): check_acc_dimensions=True, ) clear_dimension_defaults("Branch") + disable_dimension() def set_advance_flag(company, flag, default_account): diff --git a/erpnext/accounts/report/trial_balance/test_trial_balance.py b/erpnext/accounts/report/trial_balance/test_trial_balance.py index 1a29e9f69206..732035dc9faf 100644 --- a/erpnext/accounts/report/trial_balance/test_trial_balance.py +++ b/erpnext/accounts/report/trial_balance/test_trial_balance.py @@ -63,6 +63,7 @@ def test_offsetting_entries_for_accounting_dimensions(self): self.assertEqual(total_row["debit"], total_row["credit"]) def tearDown(self): + clear_dimension_defaults("Branch") disable_dimension() @@ -91,8 +92,6 @@ def create_accounting_dimension(**args): accounting_dimension = frappe.get_doc("Accounting Dimension", document_type) accounting_dimension.disabled = 0 - clear_dimension_defaults(document_type) - accounting_dimension.load_from_db() accounting_dimension.append( "dimension_defaults", { From 37ef6e959b8019b953ff30175fb38b85fe16ea47 Mon Sep 17 00:00:00 2001 From: Gursheen Anand Date: Wed, 19 Jul 2023 18:15:59 +0530 Subject: [PATCH 202/501] fix: reset dimension defaults when company changedin test (cherry picked from commit 3f5afb9cac22a8399c2157bca32a1793f6b31e8b) --- .../purchase_invoice/test_purchase_invoice.py | 2 +- .../report/trial_balance/test_trial_balance.py | 13 ++++++------- 2 files changed, 7 insertions(+), 8 deletions(-) diff --git a/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py b/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py index adb3556ebcd6..0dfc065d45ef 100644 --- a/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py +++ b/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py @@ -1745,7 +1745,7 @@ def test_offsetting_entries_for_accounting_dimensions(self): parent_account="Temporary Accounts - _TC", ) - create_accounting_dimension() + create_accounting_dimension(company="_Test Company", offsetting_account="Offsetting - _TC") branch1 = frappe.new_doc("Branch") branch1.branch = "Location 1" diff --git a/erpnext/accounts/report/trial_balance/test_trial_balance.py b/erpnext/accounts/report/trial_balance/test_trial_balance.py index 732035dc9faf..cd0429be4c21 100644 --- a/erpnext/accounts/report/trial_balance/test_trial_balance.py +++ b/erpnext/accounts/report/trial_balance/test_trial_balance.py @@ -84,14 +84,14 @@ def create_company(**args): def create_accounting_dimension(**args): args = frappe._dict(args) document_type = args.document_type or "Branch" - if not frappe.db.exists("Accounting Dimension", {"document_type": "Branch"}): - accounting_dimension = frappe.get_doc( - {"doctype": "Accounting Dimension", "document_type": document_type} - ).insert() - else: + if frappe.db.exists("Accounting Dimension", document_type): accounting_dimension = frappe.get_doc("Accounting Dimension", document_type) accounting_dimension.disabled = 0 - + else: + accounting_dimension = frappe.new_doc("Accounting Dimension") + accounting_dimension.document_type = document_type + accounting_dimension.insert() + accounting_dimension.save() accounting_dimension.append( "dimension_defaults", { @@ -101,7 +101,6 @@ def create_accounting_dimension(**args): }, ) accounting_dimension.save() - return accounting_dimension.name def disable_dimension(**args): From 01ae513f70308489ee814c1b361b24b2dbb34d60 Mon Sep 17 00:00:00 2001 From: Gursheen Anand Date: Thu, 27 Jul 2023 15:45:48 +0530 Subject: [PATCH 203/501] fix: dimension name in remark (cherry picked from commit 4f9242d699e8aa5c5595a12cafe763737978b01b) --- erpnext/accounts/general_ledger.py | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/erpnext/accounts/general_ledger.py b/erpnext/accounts/general_ledger.py index 901a49a498e5..e7fc81a0662c 100644 --- a/erpnext/accounts/general_ledger.py +++ b/erpnext/accounts/general_ledger.py @@ -63,15 +63,16 @@ def make_acc_dimensions_offsetting_entry(gl_map): offsetting_entries = [] for gle in gl_map: for dimension in accounting_dimensions_to_offset: - offsetting_account = dimension.offsetting_account offsetting_entry = gle.copy() + debit = flt(gle.credit) / no_of_dimensions if gle.credit != 0 else 0 + credit = flt(gle.debit) / no_of_dimensions if gle.debit != 0 else 0 offsetting_entry.update( { - "account": offsetting_account, - "debit": flt(gle.credit) / no_of_dimensions if gle.credit != 0 else 0, - "credit": flt(gle.debit) / no_of_dimensions if gle.debit != 0 else 0, - "debit_in_account_currency": flt(gle.credit) / no_of_dimensions if gle.credit != 0 else 0, - "credit_in_account_currency": flt(gle.debit) / no_of_dimensions if gle.debit != 0 else 0, + "account": dimension.offsetting_account, + "debit": debit, + "credit": credit, + "debit_in_account_currency": debit, + "credit_in_account_currency": credit, "remarks": _("Offsetting for Accounting Dimension") + " - {0}".format(dimension.name), "against_voucher": None, } @@ -88,7 +89,7 @@ def get_accounting_dimensions_for_offsetting_entry(gl_map, company): frappe.qb.from_(acc_dimension) .inner_join(dimension_detail) .on(acc_dimension.name == dimension_detail.parent) - .select(acc_dimension.fieldname, dimension_detail.offsetting_account) + .select(acc_dimension.fieldname, acc_dimension.name, dimension_detail.offsetting_account) .where( (acc_dimension.disabled == 0) & (dimension_detail.company == company) From e62ffa990d2015b9df734d783fb94627bb5b78fb Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Sat, 29 Jul 2023 11:52:54 +0530 Subject: [PATCH 204/501] fix: Add company filters for account (cherry picked from commit ecca9cb023f91c7504e53b4f709b01622f156fd9) # Conflicts: # erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py --- .../accounting_dimension.js | 11 +++++++ .../accounting_dimension.py | 10 +++++++ .../purchase_invoice/test_purchase_invoice.py | 29 +++++++++++++------ erpnext/accounts/general_ledger.py | 7 ++++- .../trial_balance/test_trial_balance.py | 3 +- 5 files changed, 49 insertions(+), 11 deletions(-) diff --git a/erpnext/accounts/doctype/accounting_dimension/accounting_dimension.js b/erpnext/accounts/doctype/accounting_dimension/accounting_dimension.js index 2fa1d53c60c7..2f53f7b640d8 100644 --- a/erpnext/accounts/doctype/accounting_dimension/accounting_dimension.js +++ b/erpnext/accounts/doctype/accounting_dimension/accounting_dimension.js @@ -15,6 +15,17 @@ frappe.ui.form.on('Accounting Dimension', { }; }); + frm.set_query("offsetting_account", "dimension_defaults", function(doc, cdt, cdn) { + let d = locals[cdt][cdn]; + return { + filters: { + company: d.company, + root_type: ["in", ["Asset", "Liability"]], + is_group: 0 + } + } + }); + if (!frm.is_new()) { frm.add_custom_button(__('Show {0}', [frm.doc.document_type]), function () { frappe.set_route("List", frm.doc.document_type); diff --git a/erpnext/accounts/doctype/accounting_dimension/accounting_dimension.py b/erpnext/accounts/doctype/accounting_dimension/accounting_dimension.py index 15c84d462f17..cfe5e6e80092 100644 --- a/erpnext/accounts/doctype/accounting_dimension/accounting_dimension.py +++ b/erpnext/accounts/doctype/accounting_dimension/accounting_dimension.py @@ -39,6 +39,8 @@ def validate(self): if not self.is_new(): self.validate_document_type_change() + self.validate_dimension_defaults() + def validate_document_type_change(self): doctype_before_save = frappe.db.get_value("Accounting Dimension", self.name, "document_type") if doctype_before_save != self.document_type: @@ -46,6 +48,14 @@ def validate_document_type_change(self): message += _("Please create a new Accounting Dimension if required.") frappe.throw(message) + def validate_dimension_defaults(self): + companies = [] + for default in self.get("dimension_defaults"): + if default.company not in companies: + companies.append(default.company) + else: + frappe.throw(_("Company {0} is added more than once").format(frappe.bold(default.company))) + def after_insert(self): if frappe.flags.in_test: make_dimension_in_accounting_doctypes(doc=self) diff --git a/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py b/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py index 0dfc065d45ef..397dbf6efd46 100644 --- a/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py +++ b/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py @@ -1769,10 +1769,10 @@ def test_offsetting_entries_for_accounting_dimensions(self): pi.submit() expected_gle = [ - ["_Test Account Cost for Goods Sold - _TC", 1000, 0.0, nowdate(), {"branch": branch2.branch}], - ["Creditors - _TC", 0.0, 1000, nowdate(), {"branch": branch1.branch}], - ["Offsetting - _TC", 1000, 0.0, nowdate(), {"branch": branch1.branch}], - ["Offsetting - _TC", 0.0, 1000, nowdate(), {"branch": branch2.branch}], + ["_Test Account Cost for Goods Sold - _TC", 1000, 0.0, nowdate(), branch2.branch], + ["Creditors - _TC", 0.0, 1000, nowdate(), branch1.branch], + ["Offsetting - _TC", 1000, 0.0, nowdate(), branch1.branch], + ["Offsetting - _TC", 0.0, 1000, nowdate(), branch2.branch], ] check_gl_entries( @@ -1781,7 +1781,7 @@ def test_offsetting_entries_for_accounting_dimensions(self): expected_gle, nowdate(), voucher_type="Purchase Invoice", - check_acc_dimensions=True, + additional_columns=["branch"], ) clear_dimension_defaults("Branch") disable_dimension() @@ -1804,7 +1804,7 @@ def check_gl_entries( expected_gle, posting_date, voucher_type="Purchase Invoice", - check_acc_dimensions=False, + additional_columns=None, ): gl = frappe.qb.DocType("GL Entry") query = ( @@ -1869,10 +1869,18 @@ def check_gl_entries(doc, voucher_no, expected_gle, posting_date): as_dict=1, ) <<<<<<< HEAD +<<<<<<< HEAD ======= if check_acc_dimensions: for col in list(expected_gle[0][4].keys()): query = query.select(col) +======= + + if additional_columns: + for col in additional_columns: + query = query.select(gl[col]) + +>>>>>>> ecca9cb023 (fix: Add company filters for account) gl_entries = query.run(as_dict=True) >>>>>>> 77deac4fb9 (test: PI offsetting entry for accounting dimension) @@ -1881,9 +1889,12 @@ def check_gl_entries(doc, voucher_no, expected_gle, posting_date): doc.assertEqual(expected_gle[i][1], gle.debit) doc.assertEqual(expected_gle[i][2], gle.credit) doc.assertEqual(getdate(expected_gle[i][3]), gle.posting_date) - if check_acc_dimensions: - for acc_dimension in expected_gle[i][4]: - doc.assertEqual(expected_gle[i][4][acc_dimension], gle[acc_dimension]) + + if additional_columns: + j = 4 + for col in additional_columns: + doc.assertEqual(expected_gle[i][j], gle[col]) + j += 1 def create_tax_witholding_category(category_name, company, account): diff --git a/erpnext/accounts/general_ledger.py b/erpnext/accounts/general_ledger.py index e7fc81a0662c..ed6d9dbe026e 100644 --- a/erpnext/accounts/general_ledger.py +++ b/erpnext/accounts/general_ledger.py @@ -27,8 +27,8 @@ def make_gl_entries( from_repost=False, ): if gl_map: - make_acc_dimensions_offsetting_entry(gl_map) if not cancel: + make_acc_dimensions_offsetting_entry(gl_map) validate_accounting_period(gl_map) validate_disabled_accounts(gl_map) gl_map = process_gl_map(gl_map, merge_entries) @@ -61,6 +61,7 @@ def make_acc_dimensions_offsetting_entry(gl_map): return offsetting_entries = [] + for gle in gl_map: for dimension in accounting_dimensions_to_offset: offsetting_entry = gle.copy() @@ -79,12 +80,14 @@ def make_acc_dimensions_offsetting_entry(gl_map): ) offsetting_entry["against_voucher_type"] = None offsetting_entries.append(offsetting_entry) + gl_map += offsetting_entries def get_accounting_dimensions_for_offsetting_entry(gl_map, company): acc_dimension = frappe.qb.DocType("Accounting Dimension") dimension_detail = frappe.qb.DocType("Accounting Dimension Detail") + acc_dimensions = ( frappe.qb.from_(acc_dimension) .inner_join(dimension_detail) @@ -96,11 +99,13 @@ def get_accounting_dimensions_for_offsetting_entry(gl_map, company): & (dimension_detail.automatically_post_balancing_accounting_entry == 1) ) ).run(as_dict=True) + accounting_dimensions_to_offset = [] for acc_dimension in acc_dimensions: values = set([entry.get(acc_dimension.fieldname) for entry in gl_map]) if len(values) > 1: accounting_dimensions_to_offset.append(acc_dimension) + return accounting_dimensions_to_offset diff --git a/erpnext/accounts/report/trial_balance/test_trial_balance.py b/erpnext/accounts/report/trial_balance/test_trial_balance.py index cd0429be4c21..4682ac4500a8 100644 --- a/erpnext/accounts/report/trial_balance/test_trial_balance.py +++ b/erpnext/accounts/report/trial_balance/test_trial_balance.py @@ -91,7 +91,8 @@ def create_accounting_dimension(**args): accounting_dimension = frappe.new_doc("Accounting Dimension") accounting_dimension.document_type = document_type accounting_dimension.insert() - accounting_dimension.save() + + accounting_dimension.set("dimension_defaults", []) accounting_dimension.append( "dimension_defaults", { From 5bd2a0923f4693463ceb703cc216375325dd9c13 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Fri, 18 Aug 2023 14:04:46 +0530 Subject: [PATCH 205/501] fix: broken consolidated report due to finance book filter (cherry picked from commit 96847db0ec78add9eaf781bef66464af6e13e07f) --- .../consolidated_financial_statement.py | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/erpnext/accounts/report/consolidated_financial_statement/consolidated_financial_statement.py b/erpnext/accounts/report/consolidated_financial_statement/consolidated_financial_statement.py index cdcf73f66200..fe4b6c71ebc3 100644 --- a/erpnext/accounts/report/consolidated_financial_statement/consolidated_financial_statement.py +++ b/erpnext/accounts/report/consolidated_financial_statement/consolidated_financial_statement.py @@ -749,13 +749,18 @@ def get_additional_conditions(from_date, ignore_closing_entries, filters, d): if from_date: additional_conditions.append(gle.posting_date >= from_date) - finance_book = filters.get("finance_book") - company_fb = frappe.get_cached_value("Company", d.name, "default_finance_book") + finance_books = [] + finance_books.append("") + if filter_fb := filters.get("finance_book"): + finance_books.append(filter_fb) if filters.get("include_default_book_entries"): - additional_conditions.append((gle.finance_book.isin([finance_book, company_fb, "", None]))) + if company_fb := frappe.get_cached_value("Company", d.name, "default_finance_book"): + finance_books.append(company_fb) + + additional_conditions.append((gle.finance_book.isin(finance_books)) | gle.finance_book.isnull()) else: - additional_conditions.append((gle.finance_book.isin([finance_book, "", None]))) + additional_conditions.append((gle.finance_book.isin(finance_books)) | gle.finance_book.isnull()) return additional_conditions From 620b21fec59e40178fde96085003d419804f21e0 Mon Sep 17 00:00:00 2001 From: rohitwaghchaure Date: Fri, 18 Aug 2023 18:33:04 +0530 Subject: [PATCH 206/501] fix: timeout error coming during reposting (#36715) --- erpnext/stock/stock_ledger.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/erpnext/stock/stock_ledger.py b/erpnext/stock/stock_ledger.py index d8284af60473..8cd6325c9d33 100644 --- a/erpnext/stock/stock_ledger.py +++ b/erpnext/stock/stock_ledger.py @@ -521,7 +521,7 @@ def get_dependent_entries_to_fix(self, entries_to_fix, sle): def update_distinct_item_warehouses(self, dependant_sle): key = (dependant_sle.item_code, dependant_sle.warehouse) - val = frappe._dict({"sle": dependant_sle, "dependent_voucher_detail_nos": []}) + val = frappe._dict({"sle": dependant_sle}) if key not in self.distinct_item_warehouses: self.distinct_item_warehouses[key] = val @@ -535,6 +535,8 @@ def update_distinct_item_warehouses(self, dependant_sle): if getdate(dependant_sle.posting_date) < getdate(existing_sle_posting_date): val.sle_changed = True + dependent_voucher_detail_nos.append(dependant_sle.voucher_detail_no) + val.dependent_voucher_detail_nos = dependent_voucher_detail_nos self.distinct_item_warehouses[key] = val self.new_items_found = True elif dependant_sle.voucher_detail_no not in set(dependent_voucher_detail_nos): From d5a596dff117522b5986a0509d0f16ca279d0e9b Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Fri, 18 Aug 2023 20:59:20 +0530 Subject: [PATCH 207/501] fix: timeout error coming during reposting (backport #36715) (#36716) fix: timeout error coming during reposting (#36715) (cherry picked from commit 620b21fec59e40178fde96085003d419804f21e0) Co-authored-by: rohitwaghchaure --- erpnext/stock/stock_ledger.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/erpnext/stock/stock_ledger.py b/erpnext/stock/stock_ledger.py index d8284af60473..8cd6325c9d33 100644 --- a/erpnext/stock/stock_ledger.py +++ b/erpnext/stock/stock_ledger.py @@ -521,7 +521,7 @@ def get_dependent_entries_to_fix(self, entries_to_fix, sle): def update_distinct_item_warehouses(self, dependant_sle): key = (dependant_sle.item_code, dependant_sle.warehouse) - val = frappe._dict({"sle": dependant_sle, "dependent_voucher_detail_nos": []}) + val = frappe._dict({"sle": dependant_sle}) if key not in self.distinct_item_warehouses: self.distinct_item_warehouses[key] = val @@ -535,6 +535,8 @@ def update_distinct_item_warehouses(self, dependant_sle): if getdate(dependant_sle.posting_date) < getdate(existing_sle_posting_date): val.sle_changed = True + dependent_voucher_detail_nos.append(dependant_sle.voucher_detail_no) + val.dependent_voucher_detail_nos = dependent_voucher_detail_nos self.distinct_item_warehouses[key] = val self.new_items_found = True elif dependant_sle.voucher_detail_no not in set(dependent_voucher_detail_nos): From de3b67c32749520808137aaabc609adea0dfb984 Mon Sep 17 00:00:00 2001 From: Frappe PR Bot Date: Fri, 18 Aug 2023 15:30:59 +0000 Subject: [PATCH 208/501] chore(release): Bumped to Version 14.35.2 ## [14.35.2](https://github.com/frappe/erpnext/compare/v14.35.1...v14.35.2) (2023-08-18) ### Bug Fixes * timeout error coming during reposting (backport [#36715](https://github.com/frappe/erpnext/issues/36715)) ([#36716](https://github.com/frappe/erpnext/issues/36716)) ([d5a596d](https://github.com/frappe/erpnext/commit/d5a596dff117522b5986a0509d0f16ca279d0e9b)) --- erpnext/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/__init__.py b/erpnext/__init__.py index 8d4340c59de9..ae6060e3d629 100644 --- a/erpnext/__init__.py +++ b/erpnext/__init__.py @@ -3,7 +3,7 @@ import frappe -__version__ = "14.35.1" +__version__ = "14.35.2" def get_default_company(user=None): From c74a414313d721e7ee07820f3597c9ba1b7c171b Mon Sep 17 00:00:00 2001 From: Ritvik Sardana Date: Thu, 17 Aug 2023 17:29:01 +0530 Subject: [PATCH 209/501] fix: mode of payment fetched from pos profile company in POS (cherry picked from commit 1bdd43d0f6aaaabdd18cc3e35869a968ecb07049) --- erpnext/accounts/doctype/pos_invoice/pos_invoice.js | 1 + erpnext/accounts/doctype/pos_invoice/pos_invoice.py | 10 ++++++++++ 2 files changed, 11 insertions(+) diff --git a/erpnext/accounts/doctype/pos_invoice/pos_invoice.js b/erpnext/accounts/doctype/pos_invoice/pos_invoice.js index cced37589bad..720b549ccb34 100644 --- a/erpnext/accounts/doctype/pos_invoice/pos_invoice.js +++ b/erpnext/accounts/doctype/pos_invoice/pos_invoice.js @@ -130,6 +130,7 @@ erpnext.selling.POSInvoiceController = class POSInvoiceController extends erpnex args: { "pos_profile": frm.pos_profile }, callback: ({ message: profile }) => { this.update_customer_groups_settings(profile?.customer_groups); + this.frm.set_value("company", profile?.company); }, }); } diff --git a/erpnext/accounts/doctype/pos_invoice/pos_invoice.py b/erpnext/accounts/doctype/pos_invoice/pos_invoice.py index aea801af9dd5..0ff230bb18be 100644 --- a/erpnext/accounts/doctype/pos_invoice/pos_invoice.py +++ b/erpnext/accounts/doctype/pos_invoice/pos_invoice.py @@ -54,6 +54,7 @@ def validate(self): self.validate_pos() self.validate_payment_amount() self.validate_loyalty_transaction() + self.validate_company_with_pos_company() if self.coupon_code: from erpnext.accounts.doctype.pricing_rule.utils import validate_coupon_code @@ -370,6 +371,14 @@ def validate_payment_amount(self): if total_amount_in_payments and total_amount_in_payments < invoice_total: frappe.throw(_("Total payments amount can't be greater than {}").format(-invoice_total)) + def validate_company_with_pos_company(self): + if self.company != frappe.db.get_value("POS Profile", self.pos_profile, "company"): + frappe.throw( + _("Company {} does not match with POS Profile Company {}").format( + self.company, frappe.db.get_value("POS Profile", self.pos_profile, "company") + ) + ) + def validate_loyalty_transaction(self): if self.redeem_loyalty_points and ( not self.loyalty_redemption_account or not self.loyalty_redemption_cost_center @@ -448,6 +457,7 @@ def set_pos_fields(self, for_validate=False): profile = {} if self.pos_profile: profile = frappe.get_doc("POS Profile", self.pos_profile) + self.company = profile.get("company") if not self.get("payments") and not for_validate: update_multi_mode_option(self, profile) From 2f92981afee00e6af0072892349ce2f19d3707ec Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Sun, 20 Aug 2023 15:47:17 +0530 Subject: [PATCH 210/501] chore: resolve conflicts --- .../purchase_invoice/test_purchase_invoice.py | 137 ++++++------------ 1 file changed, 42 insertions(+), 95 deletions(-) diff --git a/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py b/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py index 397dbf6efd46..d43c1a822a14 100644 --- a/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py +++ b/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py @@ -1725,12 +1725,52 @@ def test_gl_entries_for_standalone_debit_note(self): rate = flt(sle.stock_value_difference) / flt(sle.actual_qty) self.assertAlmostEqual(returned_inv.items[0].rate, rate) -<<<<<<< HEAD def test_payment_allocation_for_payment_terms(self): from erpnext.buying.doctype.purchase_order.test_purchase_order import ( create_pr_against_po, create_purchase_order, -======= + ) + from erpnext.selling.doctype.sales_order.test_sales_order import ( + automatically_fetch_payment_terms, + ) + from erpnext.stock.doctype.purchase_receipt.purchase_receipt import ( + make_purchase_invoice as make_pi_from_pr, + ) + + automatically_fetch_payment_terms() + frappe.db.set_value( + "Payment Terms Template", + "_Test Payment Term Template", + "allocate_payment_based_on_payment_terms", + 0, + ) + + po = create_purchase_order(do_not_save=1) + po.payment_terms_template = "_Test Payment Term Template" + po.save() + po.submit() + + pr = create_pr_against_po(po.name, received_qty=4) + pi = make_pi_from_pr(pr.name) + self.assertEqual(pi.payment_schedule[0].payment_amount, 1000) + + frappe.db.set_value( + "Payment Terms Template", + "_Test Payment Term Template", + "allocate_payment_based_on_payment_terms", + 1, + ) + pi = make_pi_from_pr(pr.name) + self.assertEqual(pi.payment_schedule[0].payment_amount, 2500) + + automatically_fetch_payment_terms(enable=0) + frappe.db.set_value( + "Payment Terms Template", + "_Test Payment Term Template", + "allocate_payment_based_on_payment_terms", + 0, + ) + def test_offsetting_entries_for_accounting_dimensions(self): from erpnext.accounts.doctype.account.test_account import create_account from erpnext.accounts.report.trial_balance.test_trial_balance import ( @@ -1787,78 +1827,6 @@ def test_offsetting_entries_for_accounting_dimensions(self): disable_dimension() -def set_advance_flag(company, flag, default_account): - frappe.db.set_value( - "Company", - company, - { - "book_advance_payments_in_separate_party_account": flag, - "default_advance_paid_account": default_account, - }, - ) - - -def check_gl_entries( - doc, - voucher_no, - expected_gle, - posting_date, - voucher_type="Purchase Invoice", - additional_columns=None, -): - gl = frappe.qb.DocType("GL Entry") - query = ( - frappe.qb.from_(gl) - .select(gl.account, gl.debit, gl.credit, gl.posting_date) - .where( - (gl.voucher_type == voucher_type) - & (gl.voucher_no == voucher_no) - & (gl.posting_date >= posting_date) - & (gl.is_cancelled == 0) ->>>>>>> 77deac4fb9 (test: PI offsetting entry for accounting dimension) - ) - from erpnext.selling.doctype.sales_order.test_sales_order import ( - automatically_fetch_payment_terms, - ) - from erpnext.stock.doctype.purchase_receipt.purchase_receipt import ( - make_purchase_invoice as make_pi_from_pr, - ) - - automatically_fetch_payment_terms() - frappe.db.set_value( - "Payment Terms Template", - "_Test Payment Term Template", - "allocate_payment_based_on_payment_terms", - 0, - ) - - po = create_purchase_order(do_not_save=1) - po.payment_terms_template = "_Test Payment Term Template" - po.save() - po.submit() - - pr = create_pr_against_po(po.name, received_qty=4) - pi = make_pi_from_pr(pr.name) - self.assertEqual(pi.payment_schedule[0].payment_amount, 1000) - - frappe.db.set_value( - "Payment Terms Template", - "_Test Payment Term Template", - "allocate_payment_based_on_payment_terms", - 1, - ) - pi = make_pi_from_pr(pr.name) - self.assertEqual(pi.payment_schedule[0].payment_amount, 2500) - - automatically_fetch_payment_terms(enable=0) - frappe.db.set_value( - "Payment Terms Template", - "_Test Payment Term Template", - "allocate_payment_based_on_payment_terms", - 0, - ) - - def check_gl_entries(doc, voucher_no, expected_gle, posting_date): gl_entries = frappe.db.sql( """select account, debit, credit, posting_date @@ -1868,21 +1836,6 @@ def check_gl_entries(doc, voucher_no, expected_gle, posting_date): (voucher_no, posting_date), as_dict=1, ) -<<<<<<< HEAD -<<<<<<< HEAD -======= - if check_acc_dimensions: - for col in list(expected_gle[0][4].keys()): - query = query.select(col) -======= - - if additional_columns: - for col in additional_columns: - query = query.select(gl[col]) - ->>>>>>> ecca9cb023 (fix: Add company filters for account) - gl_entries = query.run(as_dict=True) ->>>>>>> 77deac4fb9 (test: PI offsetting entry for accounting dimension) for i, gle in enumerate(gl_entries): doc.assertEqual(expected_gle[i][0], gle.account) @@ -1890,12 +1843,6 @@ def check_gl_entries(doc, voucher_no, expected_gle, posting_date): doc.assertEqual(expected_gle[i][2], gle.credit) doc.assertEqual(getdate(expected_gle[i][3]), gle.posting_date) - if additional_columns: - j = 4 - for col in additional_columns: - doc.assertEqual(expected_gle[i][j], gle[col]) - j += 1 - def create_tax_witholding_category(category_name, company, account): from erpnext.accounts.utils import get_fiscal_year From 72d9dc6c8551d994689b09ee2d87ba302bc03729 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Mon, 21 Aug 2023 10:04:20 +0530 Subject: [PATCH 211/501] chore: resolve more conflicts --- .../purchase_invoice/test_purchase_invoice.py | 39 +++++++++++++++---- 1 file changed, 31 insertions(+), 8 deletions(-) diff --git a/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py b/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py index d43c1a822a14..0f8e77952cf5 100644 --- a/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py +++ b/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py @@ -1827,22 +1827,45 @@ def test_offsetting_entries_for_accounting_dimensions(self): disable_dimension() -def check_gl_entries(doc, voucher_no, expected_gle, posting_date): - gl_entries = frappe.db.sql( - """select account, debit, credit, posting_date - from `tabGL Entry` - where voucher_type='Purchase Invoice' and voucher_no=%s and posting_date >= %s - order by posting_date asc, account asc""", - (voucher_no, posting_date), - as_dict=1, +def check_gl_entries( + doc, + voucher_no, + expected_gle, + posting_date, + voucher_type="Purchase Invoice", + additional_columns=None, +): + gl = frappe.qb.DocType("GL Entry") + query = ( + frappe.qb.from_(gl) + .select(gl.account, gl.debit, gl.credit, gl.posting_date) + .where( + (gl.voucher_type == voucher_type) + & (gl.voucher_no == voucher_no) + & (gl.posting_date >= posting_date) + & (gl.is_cancelled == 0) + ) + .orderby(gl.posting_date, gl.account, gl.creation) ) + if additional_columns: + for col in additional_columns: + query = query.select(gl[col]) + + gl_entries = query.run(as_dict=True) + for i, gle in enumerate(gl_entries): doc.assertEqual(expected_gle[i][0], gle.account) doc.assertEqual(expected_gle[i][1], gle.debit) doc.assertEqual(expected_gle[i][2], gle.credit) doc.assertEqual(getdate(expected_gle[i][3]), gle.posting_date) + if additional_columns: + j = 4 + for col in additional_columns: + doc.assertEqual(expected_gle[i][j], gle[col]) + j += 1 + def create_tax_witholding_category(category_name, company, account): from erpnext.accounts.utils import get_fiscal_year From e1bd9a7e8dbfe0fbbe6afddfa2370ed36bbf6767 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Mon, 21 Aug 2023 15:21:11 +0530 Subject: [PATCH 212/501] fix: don't throw if item does not have default BOM (backport #36709) (#36734) * fix: don't throw if item does not have default BOM (cherry picked from commit 268c19e745c80ac586ebf1ed477fe6459dfc39f3) * fix: throw if `BOM No` is not set (cherry picked from commit 2e22b019a0d15dacb3881fdc6a9fc5ccb476a355) --------- Co-authored-by: s-aga-r --- .../doctype/production_plan/production_plan.py | 5 ++++- erpnext/manufacturing/doctype/work_order/work_order.py | 9 ++++++--- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/erpnext/manufacturing/doctype/production_plan/production_plan.py b/erpnext/manufacturing/doctype/production_plan/production_plan.py index 8d75c3cb60d3..db4003bc5850 100644 --- a/erpnext/manufacturing/doctype/production_plan/production_plan.py +++ b/erpnext/manufacturing/doctype/production_plan/production_plan.py @@ -347,7 +347,7 @@ def add_items(self, items): if not data.pending_qty: continue - item_details = get_item_details(data.item_code) + item_details = get_item_details(data.item_code, throw=False) if self.combine_items: if item_details.bom_no in refs: refs[item_details.bom_no]["so_details"].append( @@ -795,6 +795,9 @@ def get_sub_assembly_items(self, manufacturing_type=None): if not row.item_code: frappe.throw(_("Row #{0}: Please select Item Code in Assembly Items").format(row.idx)) + if not row.bom_no: + frappe.throw(_("Row #{0}: Please select the BOM No in Assembly Items").format(row.idx)) + bom_data = [] warehouse = row.warehouse if self.skip_available_sub_assembly_item else None diff --git a/erpnext/manufacturing/doctype/work_order/work_order.py b/erpnext/manufacturing/doctype/work_order/work_order.py index 92678e44c844..c4b6846376f2 100644 --- a/erpnext/manufacturing/doctype/work_order/work_order.py +++ b/erpnext/manufacturing/doctype/work_order/work_order.py @@ -1075,7 +1075,7 @@ def get_bom_operations(doctype, txt, searchfield, start, page_len, filters): @frappe.whitelist() -def get_item_details(item, project=None, skip_bom_info=False): +def get_item_details(item, project=None, skip_bom_info=False, throw=True): res = frappe.db.sql( """ select stock_uom, description, item_name, allow_alternative_item, @@ -1111,12 +1111,15 @@ def get_item_details(item, project=None, skip_bom_info=False): if not res["bom_no"]: if project: - res = get_item_details(item) + res = get_item_details(item, throw=throw) frappe.msgprint( _("Default BOM not found for Item {0} and Project {1}").format(item, project), alert=1 ) else: - frappe.throw(_("Default BOM for {0} not found").format(item)) + msg = _("Default BOM for {0} not found").format(item) + frappe.msgprint(msg, raise_exception=throw, indicator="yellow", alert=(not throw)) + + return res bom_data = frappe.db.get_value( "BOM", From 3634e80341df075a3759b8f3b8994a02f423273d Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Mon, 21 Aug 2023 15:59:52 +0530 Subject: [PATCH 213/501] fix: add missing items labels back (#36737) [skip ci] (cherry picked from commit 86cac1e1d2b2600c4bfb7e5535ab21847f00330e) --- erpnext/accounts/doctype/sales_invoice/sales_invoice.json | 1 + 1 file changed, 1 insertion(+) diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.json b/erpnext/accounts/doctype/sales_invoice/sales_invoice.json index fb60dd58724b..5fc711d893b9 100644 --- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.json +++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.json @@ -714,6 +714,7 @@ "fieldtype": "Table", "hide_days": 1, "hide_seconds": 1, + "label": "Items", "oldfieldname": "entries", "oldfieldtype": "Table", "options": "Sales Invoice Item", From 460607956821479d13977b841669a456deb6f30e Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Sun, 20 Aug 2023 20:13:31 +0530 Subject: [PATCH 214/501] fix: include gain/loss journal in AR/AP reports (cherry picked from commit e3104f189844043c0c06f3d7f622391f83a0956f) --- .../report/accounts_receivable/accounts_receivable.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/erpnext/accounts/report/accounts_receivable/accounts_receivable.py b/erpnext/accounts/report/accounts_receivable/accounts_receivable.py index f78a84086a9f..a7b35a579a9c 100755 --- a/erpnext/accounts/report/accounts_receivable/accounts_receivable.py +++ b/erpnext/accounts/report/accounts_receivable/accounts_receivable.py @@ -1090,7 +1090,10 @@ def get_exchange_rate_revaluations(self): .where( (je.company == self.filters.company) & (je.posting_date.lte(self.filters.report_date)) - & (je.voucher_type == "Exchange Rate Revaluation") + & ( + (je.voucher_type == "Exchange Rate Revaluation") + | (je.voucher_type == "Exchange Gain Or Loss") + ) ) .run() ) From 296a4d7a12714f978c2520483ed3b162ac71f843 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Sun, 20 Aug 2023 21:17:27 +0530 Subject: [PATCH 215/501] fix: broken advance field in Accounts Receivable summary rpt (cherry picked from commit 896b123fb116d48f3632ca9463ee9ecc4f46f945) --- erpnext/accounts/party.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/accounts/party.py b/erpnext/accounts/party.py index 3aea40316b5a..9bdd39c0edd1 100644 --- a/erpnext/accounts/party.py +++ b/erpnext/accounts/party.py @@ -912,7 +912,7 @@ def get_partywise_advanced_payment_amount( if party: query = query.where(gle.party == party) - data = query.run(as_dict=True) + data = query.run() if data: return frappe._dict(data) From 928e4758245794c6610cf5f235570dc63abc2f3e Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Mon, 21 Aug 2023 08:46:55 +0530 Subject: [PATCH 216/501] refactor: use payment ledger to fetch advance amount (cherry picked from commit 0dc5e5c4303a64c52df5d9858c0de8fee576268e) --- erpnext/accounts/party.py | 29 +++++++++---------- .../accounts_receivable_summary.py | 1 - 2 files changed, 14 insertions(+), 16 deletions(-) diff --git a/erpnext/accounts/party.py b/erpnext/accounts/party.py index 9bdd39c0edd1..0c51e727c920 100644 --- a/erpnext/accounts/party.py +++ b/erpnext/accounts/party.py @@ -14,7 +14,7 @@ from frappe.contacts.doctype.contact.contact import get_contact_details from frappe.core.doctype.user_permission.user_permission import get_permitted_documents from frappe.model.utils import get_fetch_values -from frappe.query_builder.functions import Date, Sum +from frappe.query_builder.functions import Abs, Date, Sum from frappe.utils import ( add_days, add_months, @@ -884,33 +884,32 @@ def get_party_shipping_address(doctype: str, name: str) -> Optional[str]: def get_partywise_advanced_payment_amount( - party_type, posting_date=None, future_payment=0, company=None, party=None, account_type=None + party_type, posting_date=None, future_payment=0, company=None, party=None ): - gle = frappe.qb.DocType("GL Entry") + ple = frappe.qb.DocType("Payment Ledger Entry") query = ( - frappe.qb.from_(gle) - .select(gle.party) + frappe.qb.from_(ple) + .select(ple.party, Abs(Sum(ple.amount).as_("amount"))) .where( - (gle.party_type.isin(party_type)) & (gle.against_voucher.isnull()) & (gle.is_cancelled == 0) + (ple.party_type.isin(party_type)) + & (ple.amount < 0) + & (ple.against_voucher_no == ple.voucher_no) + & (ple.delinked == 0) ) - .groupby(gle.party) + .groupby(ple.party) ) - if account_type == "Receivable": - query = query.select(Sum(gle.credit).as_("amount")) - else: - query = query.select(Sum(gle.debit).as_("amount")) if posting_date: if future_payment: - query = query.where((gle.posting_date <= posting_date) | (Date(gle.creation) <= posting_date)) + query = query.where((ple.posting_date <= posting_date) | (Date(ple.creation) <= posting_date)) else: - query = query.where(gle.posting_date <= posting_date) + query = query.where(ple.posting_date <= posting_date) if company: - query = query.where(gle.company == company) + query = query.where(ple.company == company) if party: - query = query.where(gle.party == party) + query = query.where(ple.party == party) data = query.run() if data: diff --git a/erpnext/accounts/report/accounts_receivable_summary/accounts_receivable_summary.py b/erpnext/accounts/report/accounts_receivable_summary/accounts_receivable_summary.py index da4c9dabbf66..3675e80b54ac 100644 --- a/erpnext/accounts/report/accounts_receivable_summary/accounts_receivable_summary.py +++ b/erpnext/accounts/report/accounts_receivable_summary/accounts_receivable_summary.py @@ -50,7 +50,6 @@ def get_data(self, args): self.filters.show_future_payments, self.filters.company, party=party, - account_type=self.account_type, ) or {} ) From 37cee42561f295821a583deeff83c6365238fe01 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Mon, 21 Aug 2023 14:54:03 +0530 Subject: [PATCH 217/501] test: add test for receivable summary report (cherry picked from commit af52f21ecefa35a1ab7cdec4f7d64156933d91eb) --- .../test_accounts_receivable_summary.py | 203 ++++++++++++++++++ erpnext/accounts/test/accounts_mixin.py | 13 ++ 2 files changed, 216 insertions(+) create mode 100644 erpnext/accounts/report/accounts_receivable_summary/test_accounts_receivable_summary.py diff --git a/erpnext/accounts/report/accounts_receivable_summary/test_accounts_receivable_summary.py b/erpnext/accounts/report/accounts_receivable_summary/test_accounts_receivable_summary.py new file mode 100644 index 000000000000..3ee35a114d15 --- /dev/null +++ b/erpnext/accounts/report/accounts_receivable_summary/test_accounts_receivable_summary.py @@ -0,0 +1,203 @@ +import unittest + +import frappe +from frappe.tests.utils import FrappeTestCase, change_settings +from frappe.utils import today + +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_summary.accounts_receivable_summary import execute +from erpnext.accounts.test.accounts_mixin import AccountsTestMixin + + +class TestAccountsReceivable(AccountsTestMixin, FrappeTestCase): + def setUp(self): + self.maxDiff = None + self.create_company() + self.create_customer() + self.create_item() + self.clear_old_entries() + + def tearDown(self): + frappe.db.rollback() + + def test_01_receivable_summary_output(self): + """ + Test for Invoices, Paid, Advance and Outstanding + """ + filters = { + "company": self.company, + "customer": self.customer, + "posting_date": today(), + "range1": 30, + "range2": 60, + "range3": 90, + "range4": 120, + } + + si = create_sales_invoice( + item=self.item, + company=self.company, + customer=self.customer, + debit_to=self.debit_to, + posting_date=today(), + parent_cost_center=self.cost_center, + cost_center=self.cost_center, + rate=200, + price_list_rate=200, + ) + + customer_group, customer_territory = frappe.db.get_all( + "Customer", + filters={"name": self.customer}, + fields=["customer_group", "territory"], + as_list=True, + )[0] + + report = execute(filters) + rpt_output = report[1] + expected_data = { + "party_type": "Customer", + "advance": 0, + "party": self.customer, + "invoiced": 200.0, + "paid": 0.0, + "credit_note": 0.0, + "outstanding": 200.0, + "range1": 200.0, + "range2": 0.0, + "range3": 0.0, + "range4": 0.0, + "range5": 0.0, + "total_due": 200.0, + "future_amount": 0.0, + "sales_person": [], + "currency": si.currency, + "territory": customer_territory, + "customer_group": customer_group, + } + + self.assertEqual(len(rpt_output), 1) + self.assertDictEqual(rpt_output[0], expected_data) + + # simulate advance payment + pe = get_payment_entry(si.doctype, si.name) + pe.paid_amount = 50 + pe.references[0].allocated_amount = 0 # this essitially removes the reference + pe.save().submit() + + # update expected data with advance + expected_data.update( + { + "advance": 50.0, + "outstanding": 150.0, + "range1": 150.0, + "total_due": 150.0, + } + ) + + report = execute(filters) + rpt_output = report[1] + self.assertEqual(len(rpt_output), 1) + self.assertDictEqual(rpt_output[0], expected_data) + + # make partial payment + pe = get_payment_entry(si.doctype, si.name) + pe.paid_amount = 125 + pe.references[0].allocated_amount = 125 + pe.save().submit() + + # update expected data after advance and partial payment + expected_data.update( + {"advance": 50.0, "paid": 125.0, "outstanding": 25.0, "range1": 25.0, "total_due": 25.0} + ) + + report = execute(filters) + rpt_output = report[1] + self.assertEqual(len(rpt_output), 1) + self.assertDictEqual(rpt_output[0], expected_data) + + @change_settings("Selling Settings", {"cust_master_name": "Naming Series"}) + def test_02_various_filters_and_output(self): + filters = { + "company": self.company, + "customer": self.customer, + "posting_date": today(), + "range1": 30, + "range2": 60, + "range3": 90, + "range4": 120, + } + + si = create_sales_invoice( + item=self.item, + company=self.company, + customer=self.customer, + debit_to=self.debit_to, + posting_date=today(), + parent_cost_center=self.cost_center, + cost_center=self.cost_center, + rate=200, + price_list_rate=200, + ) + # make partial payment + pe = get_payment_entry(si.doctype, si.name) + pe.paid_amount = 150 + pe.references[0].allocated_amount = 150 + pe.save().submit() + + customer_group, customer_territory = frappe.db.get_all( + "Customer", + filters={"name": self.customer}, + fields=["customer_group", "territory"], + as_list=True, + )[0] + + report = execute(filters) + rpt_output = report[1] + expected_data = { + "party_type": "Customer", + "advance": 0, + "party": self.customer, + "party_name": self.customer, + "invoiced": 200.0, + "paid": 150.0, + "credit_note": 0.0, + "outstanding": 50.0, + "range1": 50.0, + "range2": 0.0, + "range3": 0.0, + "range4": 0.0, + "range5": 0.0, + "total_due": 50.0, + "future_amount": 0.0, + "sales_person": [], + "currency": si.currency, + "territory": customer_territory, + "customer_group": customer_group, + } + + self.assertEqual(len(rpt_output), 1) + self.assertDictEqual(rpt_output[0], expected_data) + + # with gl balance filter + filters.update({"show_gl_balance": True}) + expected_data.update({"gl_balance": 50.0, "diff": 0.0}) + report = execute(filters) + rpt_output = report[1] + self.assertEqual(len(rpt_output), 1) + self.assertDictEqual(rpt_output[0], expected_data) + + # with gl balance and future payments filter + filters.update({"show_future_payments": True}) + expected_data.update({"remaining_balance": 50.0}) + report = execute(filters) + rpt_output = report[1] + self.assertEqual(len(rpt_output), 1) + self.assertDictEqual(rpt_output[0], expected_data) + + # invoice fully paid + pe = get_payment_entry(si.doctype, si.name).save().submit() + report = execute(filters) + rpt_output = report[1] + self.assertEqual(len(rpt_output), 0) diff --git a/erpnext/accounts/test/accounts_mixin.py b/erpnext/accounts/test/accounts_mixin.py index 70bbf7e694dd..debfffdcbb3e 100644 --- a/erpnext/accounts/test/accounts_mixin.py +++ b/erpnext/accounts/test/accounts_mixin.py @@ -1,4 +1,5 @@ import frappe +from frappe import qb from erpnext.stock.doctype.item.test_item import create_item @@ -103,3 +104,15 @@ def create_company(self, company_name="_Test Company", abbr="_TC"): ) new_acc.save() setattr(self, acc.attribute_name, new_acc.name) + + def clear_old_entries(self): + doctype_list = [ + "GL Entry", + "Payment Ledger Entry", + "Sales Invoice", + "Purchase Invoice", + "Payment Entry", + "Journal Entry", + ] + for doctype in doctype_list: + qb.from_(qb.DocType(doctype)).delete().where(qb.DocType(doctype).company == self.company).run() From cb9aad3e873b430926f9007e6fcd2dbbf2e71111 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Mon, 21 Aug 2023 21:35:26 +0530 Subject: [PATCH 218/501] fix: incorrect gl balance on multi company setup --- .../accounts_receivable_summary.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/erpnext/accounts/report/accounts_receivable_summary/accounts_receivable_summary.py b/erpnext/accounts/report/accounts_receivable_summary/accounts_receivable_summary.py index 3675e80b54ac..cffc87895ef8 100644 --- a/erpnext/accounts/report/accounts_receivable_summary/accounts_receivable_summary.py +++ b/erpnext/accounts/report/accounts_receivable_summary/accounts_receivable_summary.py @@ -55,7 +55,7 @@ def get_data(self, args): ) if self.filters.show_gl_balance: - gl_balance_map = get_gl_balance(self.filters.report_date) + gl_balance_map = get_gl_balance(self.filters.report_date, self.filters.company) for party, party_dict in self.party_total.items(): if party_dict.outstanding == 0: @@ -232,12 +232,12 @@ def setup_ageing_columns(self): self.add_column(label="Total Amount Due", fieldname="total_due") -def get_gl_balance(report_date): +def get_gl_balance(report_date, company): return frappe._dict( frappe.db.get_all( "GL Entry", fields=["party", "sum(debit - credit)"], - filters={"posting_date": ("<=", report_date), "is_cancelled": 0}, + filters={"posting_date": ("<=", report_date), "is_cancelled": 0, "company": company}, group_by="party", as_list=1, ) From a0575ed2b0dec34ba1de14fe7b09aa8195be5e45 Mon Sep 17 00:00:00 2001 From: Anand Baburajan Date: Tue, 22 Aug 2023 01:40:42 +0530 Subject: [PATCH 219/501] fix: incorrect schedule in asset value adjustment (#36725) * fix: incorrect schedule in asset value adjustment * chore: remove unnecessary commented code * test: check schedules in test * test: improving the test * chore: better function name * chore: use None instead of 0 for default value after depr * chore: typo --- erpnext/assets/doctype/asset/asset.py | 94 +++++++++++++++---- .../asset_value_adjustment.py | 58 +++--------- .../test_asset_value_adjustment.py | 54 ++++++++--- 3 files changed, 129 insertions(+), 77 deletions(-) diff --git a/erpnext/assets/doctype/asset/asset.py b/erpnext/assets/doctype/asset/asset.py index f4a1e3cc190c..34d5430210c5 100644 --- a/erpnext/assets/doctype/asset/asset.py +++ b/erpnext/assets/doctype/asset/asset.py @@ -81,18 +81,27 @@ def validate_asset_and_reference(self): _("Purchase Invoice cannot be made against an existing asset {0}").format(self.name) ) - def prepare_depreciation_data(self, date_of_disposal=None, date_of_return=None): + def prepare_depreciation_data( + self, + date_of_disposal=None, + date_of_return=None, + value_after_depreciation=None, + ignore_booked_entry=False, + ): if self.calculate_depreciation: self.value_after_depreciation = 0 self.set_depreciation_rate() if self.should_prepare_depreciation_schedule(): - self.make_depreciation_schedule(date_of_disposal) - self.set_accumulated_depreciation(date_of_disposal, date_of_return) + self.make_depreciation_schedule(date_of_disposal, value_after_depreciation) + self.set_accumulated_depreciation(date_of_disposal, date_of_return, ignore_booked_entry) else: self.finance_books = [] - self.value_after_depreciation = flt(self.gross_purchase_amount) - flt( - self.opening_accumulated_depreciation - ) + if value_after_depreciation: + self.value_after_depreciation = value_after_depreciation + else: + self.value_after_depreciation = flt(self.gross_purchase_amount) - flt( + self.opening_accumulated_depreciation + ) def should_prepare_depreciation_schedule(self): if not self.get("schedules"): @@ -285,7 +294,7 @@ def set_depreciation_rate(self): self.get_depreciation_rate(d, on_validate=True), d.precision("rate_of_depreciation") ) - def make_depreciation_schedule(self, date_of_disposal): + def make_depreciation_schedule(self, date_of_disposal, value_after_depreciation=None): if not self.get("schedules"): self.schedules = [] @@ -295,24 +304,30 @@ def make_depreciation_schedule(self, date_of_disposal): start = self.clear_depreciation_schedule() for finance_book in self.get("finance_books"): - self._make_depreciation_schedule(finance_book, start, date_of_disposal) + self._make_depreciation_schedule( + finance_book, start, date_of_disposal, value_after_depreciation + ) if len(self.get("finance_books")) > 1 and any(start): self.sort_depreciation_schedule() - def _make_depreciation_schedule(self, finance_book, start, date_of_disposal): + def _make_depreciation_schedule( + self, finance_book, start, date_of_disposal, value_after_depreciation=None + ): self.validate_asset_finance_books(finance_book) - value_after_depreciation = self._get_value_after_depreciation_for_making_schedule(finance_book) + if not value_after_depreciation: + value_after_depreciation = self._get_value_after_depreciation_for_making_schedule(finance_book) + finance_book.value_after_depreciation = value_after_depreciation - number_of_pending_depreciations = cint(finance_book.total_number_of_depreciations) - cint( + final_number_of_depreciations = cint(finance_book.total_number_of_depreciations) - cint( self.number_of_depreciations_booked ) has_pro_rata = self.check_is_pro_rata(finance_book) if has_pro_rata: - number_of_pending_depreciations += 1 + final_number_of_depreciations += 1 has_wdv_or_dd_non_yearly_pro_rata = False if ( @@ -328,7 +343,9 @@ def _make_depreciation_schedule(self, finance_book, start, date_of_disposal): depreciation_amount = 0 - for n in range(start[finance_book.idx - 1], number_of_pending_depreciations): + number_of_pending_depreciations = final_number_of_depreciations - start[finance_book.idx - 1] + + for n in range(start[finance_book.idx - 1], final_number_of_depreciations): # If depreciation is already completed (for double declining balance) if skip_row: continue @@ -345,10 +362,11 @@ def _make_depreciation_schedule(self, finance_book, start, date_of_disposal): n, prev_depreciation_amount, has_wdv_or_dd_non_yearly_pro_rata, + number_of_pending_depreciations, ) if not has_pro_rata or ( - n < (cint(number_of_pending_depreciations) - 1) or number_of_pending_depreciations == 2 + n < (cint(final_number_of_depreciations) - 1) or final_number_of_depreciations == 2 ): schedule_date = add_months( finance_book.depreciation_start_date, n * cint(finance_book.frequency_of_depreciation) @@ -416,7 +434,7 @@ def _make_depreciation_schedule(self, finance_book, start, date_of_disposal): ) # For last row - elif has_pro_rata and n == cint(number_of_pending_depreciations) - 1: + elif has_pro_rata and n == cint(final_number_of_depreciations) - 1: if not self.flags.increase_in_asset_life: # In case of increase_in_asset_life, the self.to_date is already set on asset_repair submission self.to_date = add_months( @@ -447,7 +465,7 @@ def _make_depreciation_schedule(self, finance_book, start, date_of_disposal): # Adjust depreciation amount in the last period based on the expected value after useful life if finance_book.expected_value_after_useful_life and ( ( - n == cint(number_of_pending_depreciations) - 1 + n == cint(final_number_of_depreciations) - 1 and value_after_depreciation != finance_book.expected_value_after_useful_life ) or value_after_depreciation < finance_book.expected_value_after_useful_life @@ -690,7 +708,10 @@ def set_accumulated_depreciation( if s.finance_book_id == d.finance_book_id and (s.depreciation_method == "Straight Line" or s.depreciation_method == "Manual") ] - accumulated_depreciation = flt(self.opening_accumulated_depreciation) + if i > 0 and self.flags.decrease_in_asset_value_due_to_value_adjustment: + accumulated_depreciation = self.get("schedules")[i - 1].accumulated_depreciation_amount + else: + accumulated_depreciation = flt(self.opening_accumulated_depreciation) value_after_depreciation = flt( self.get("finance_books")[cint(d.finance_book_id) - 1].value_after_depreciation ) @@ -1296,11 +1317,14 @@ def get_depreciation_amount( schedule_idx=0, prev_depreciation_amount=0, has_wdv_or_dd_non_yearly_pro_rata=False, + number_of_pending_depreciations=0, ): frappe.flags.company = asset.company if fb_row.depreciation_method in ("Straight Line", "Manual"): - return get_straight_line_or_manual_depr_amount(asset, fb_row, schedule_idx) + return get_straight_line_or_manual_depr_amount( + asset, fb_row, schedule_idx, number_of_pending_depreciations + ) else: rate_of_depreciation = get_updated_rate_of_depreciation_for_wdv_and_dd( asset, depreciable_value, fb_row @@ -1320,7 +1344,9 @@ def get_updated_rate_of_depreciation_for_wdv_and_dd(asset, depreciable_value, fb return fb_row.rate_of_depreciation -def get_straight_line_or_manual_depr_amount(asset, row, schedule_idx): +def get_straight_line_or_manual_depr_amount( + asset, row, schedule_idx, number_of_pending_depreciations +): # if the Depreciation Schedule is being modified after Asset Repair due to increase in asset life and value if asset.flags.increase_in_asset_life: return (flt(row.value_after_depreciation) - flt(row.expected_value_after_useful_life)) / ( @@ -1331,6 +1357,36 @@ def get_straight_line_or_manual_depr_amount(asset, row, schedule_idx): return (flt(row.value_after_depreciation) - flt(row.expected_value_after_useful_life)) / flt( row.total_number_of_depreciations ) + # if the Depreciation Schedule is being modified after Asset Value Adjustment due to decrease in asset value + elif asset.flags.decrease_in_asset_value_due_to_value_adjustment: + if row.daily_depreciation: + daily_depr_amount = ( + flt(row.value_after_depreciation) - flt(row.expected_value_after_useful_life) + ) / date_diff( + add_months( + row.depreciation_start_date, + flt(row.total_number_of_depreciations - asset.number_of_depreciations_booked) + * row.frequency_of_depreciation, + ), + add_months( + row.depreciation_start_date, + flt( + row.total_number_of_depreciations + - asset.number_of_depreciations_booked + - number_of_pending_depreciations + ) + * row.frequency_of_depreciation, + ), + ) + to_date = add_months(row.depreciation_start_date, schedule_idx * row.frequency_of_depreciation) + from_date = add_months( + row.depreciation_start_date, (schedule_idx - 1) * row.frequency_of_depreciation + ) + return daily_depr_amount * date_diff(to_date, from_date) + else: + return ( + flt(row.value_after_depreciation) - flt(row.expected_value_after_useful_life) + ) / number_of_pending_depreciations # if the Depreciation Schedule is being prepared for the first time else: if row.daily_depreciation: diff --git a/erpnext/assets/doctype/asset_value_adjustment/asset_value_adjustment.py b/erpnext/assets/doctype/asset_value_adjustment/asset_value_adjustment.py index 9928b2f5f383..29e7a9bdfd6c 100644 --- a/erpnext/assets/doctype/asset_value_adjustment/asset_value_adjustment.py +++ b/erpnext/assets/doctype/asset_value_adjustment/asset_value_adjustment.py @@ -5,15 +5,12 @@ import frappe from frappe import _ from frappe.model.document import Document -from frappe.utils import cint, date_diff, flt, formatdate, getdate +from frappe.utils import flt, formatdate, getdate from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import ( get_checks_for_pl_and_bs_accounts, ) -from erpnext.assets.doctype.asset.asset import ( - get_asset_value_after_depreciation, - get_depreciation_amount, -) +from erpnext.assets.doctype.asset.asset import get_asset_value_after_depreciation from erpnext.assets.doctype.asset.depreciation import get_depreciation_accounts @@ -25,10 +22,10 @@ def validate(self): def on_submit(self): self.make_depreciation_entry() - self.reschedule_depreciations(self.new_asset_value) + self.update_asset(self.new_asset_value) def on_cancel(self): - self.reschedule_depreciations(self.current_asset_value) + self.update_asset(self.current_asset_value) def validate_date(self): asset_purchase_date = frappe.db.get_value("Asset", self.asset, "purchase_date") @@ -71,12 +68,16 @@ def make_depreciation_entry(self): "account": accumulated_depreciation_account, "credit_in_account_currency": self.difference_amount, "cost_center": depreciation_cost_center or self.cost_center, + "reference_type": "Asset", + "reference_name": asset.name, } debit_entry = { "account": depreciation_expense_account, "debit_in_account_currency": self.difference_amount, "cost_center": depreciation_cost_center or self.cost_center, + "reference_type": "Asset", + "reference_name": asset.name, } accounting_dimensions = get_checks_for_pl_and_bs_accounts() @@ -106,44 +107,11 @@ def make_depreciation_entry(self): self.db_set("journal_entry", je.name) - def reschedule_depreciations(self, asset_value): + def update_asset(self, asset_value): asset = frappe.get_doc("Asset", self.asset) - country = frappe.get_value("Company", self.company, "country") - - for d in asset.finance_books: - d.value_after_depreciation = asset_value - if d.depreciation_method in ("Straight Line", "Manual"): - end_date = max(s.schedule_date for s in asset.schedules if cint(s.finance_book_id) == d.idx) - total_days = date_diff(end_date, self.date) - rate_per_day = flt(d.value_after_depreciation - d.expected_value_after_useful_life) / flt( - total_days - ) - from_date = self.date - else: - no_of_depreciations = len( - [ - s.name for s in asset.schedules if (cint(s.finance_book_id) == d.idx and not s.journal_entry) - ] - ) + asset.flags.decrease_in_asset_value_due_to_value_adjustment = True - value_after_depreciation = d.value_after_depreciation - for data in asset.schedules: - if cint(data.finance_book_id) == d.idx and not data.journal_entry: - if d.depreciation_method in ("Straight Line", "Manual"): - days = date_diff(data.schedule_date, from_date) - depreciation_amount = days * rate_per_day - from_date = data.schedule_date - else: - depreciation_amount = get_depreciation_amount(asset, value_after_depreciation, d) - - if depreciation_amount: - value_after_depreciation -= flt(depreciation_amount) - data.depreciation_amount = depreciation_amount - - d.db_update() - - asset.set_accumulated_depreciation(ignore_booked_entry=True) - for asset_data in asset.schedules: - if not asset_data.journal_entry: - asset_data.db_update() + asset.prepare_depreciation_data(value_after_depreciation=asset_value, ignore_booked_entry=True) + asset.flags.ignore_validate_update_after_submit = True + asset.save() diff --git a/erpnext/assets/doctype/asset_value_adjustment/test_asset_value_adjustment.py b/erpnext/assets/doctype/asset_value_adjustment/test_asset_value_adjustment.py index b2aa3958080b..977a9b3714be 100644 --- a/erpnext/assets/doctype/asset_value_adjustment/test_asset_value_adjustment.py +++ b/erpnext/assets/doctype/asset_value_adjustment/test_asset_value_adjustment.py @@ -4,9 +4,10 @@ import unittest import frappe -from frappe.utils import add_days, get_last_day, nowdate +from frappe.utils import add_days, cstr, get_last_day, getdate, nowdate from erpnext.assets.doctype.asset.asset import get_asset_value_after_depreciation +from erpnext.assets.doctype.asset.depreciation import post_depreciation_entries from erpnext.assets.doctype.asset.test_asset import create_asset_data from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import make_purchase_receipt @@ -46,40 +47,44 @@ def test_current_asset_value(self): def test_asset_depreciation_value_adjustment(self): pr = make_purchase_receipt( - item_code="Macbook Pro", qty=1, rate=100000.0, location="Test Location" + item_code="Macbook Pro", qty=1, rate=120000.0, location="Test Location" ) asset_name = frappe.db.get_value("Asset", {"purchase_receipt": pr.name}, "name") asset_doc = frappe.get_doc("Asset", asset_name) asset_doc.calculate_depreciation = 1 - month_end_date = get_last_day(nowdate()) - purchase_date = nowdate() if nowdate() != month_end_date else add_days(nowdate(), -15) - - asset_doc.available_for_use_date = purchase_date - asset_doc.purchase_date = purchase_date + asset_doc.available_for_use_date = "2023-01-15" + asset_doc.purchase_date = "2023-01-15" asset_doc.calculate_depreciation = 1 asset_doc.append( "finance_books", { "expected_value_after_useful_life": 200, "depreciation_method": "Straight Line", - "total_number_of_depreciations": 3, - "frequency_of_depreciation": 10, - "depreciation_start_date": month_end_date, + "total_number_of_depreciations": 12, + "frequency_of_depreciation": 1, + "depreciation_start_date": "2023-01-31", }, ) asset_doc.submit() + post_depreciation_entries(getdate("2023-08-21")) + current_value = get_asset_value_after_depreciation(asset_doc.name) adj_doc = make_asset_value_adjustment( - asset=asset_doc.name, current_asset_value=current_value, new_asset_value=50000.0 + asset=asset_doc.name, + current_asset_value=current_value, + new_asset_value=50000.0, + date="2023-08-21", ) adj_doc.submit() + asset_doc.reload() + expected_gle = ( - ("_Test Accumulated Depreciations - _TC", 0.0, 50000.0), - ("_Test Depreciations - _TC", 50000.0, 0.0), + ("_Test Accumulated Depreciations - _TC", 0.0, 4625.29), + ("_Test Depreciations - _TC", 4625.29, 0.0), ) gle = frappe.db.sql( @@ -91,6 +96,29 @@ def test_asset_depreciation_value_adjustment(self): self.assertSequenceEqual(gle, expected_gle) + expected_schedules = [ + ["2023-01-31", 5474.73, 5474.73], + ["2023-02-28", 9983.33, 15458.06], + ["2023-03-31", 9983.33, 25441.39], + ["2023-04-30", 9983.33, 35424.72], + ["2023-05-31", 9983.33, 45408.05], + ["2023-06-30", 9983.33, 55391.38], + ["2023-07-31", 9983.33, 65374.71], + ["2023-08-31", 8300.0, 73674.71], + ["2023-09-30", 8300.0, 81974.71], + ["2023-10-31", 8300.0, 90274.71], + ["2023-11-30", 8300.0, 98574.71], + ["2023-12-31", 8300.0, 106874.71], + ["2024-01-15", 8300.0, 115174.71], + ] + + schedules = [ + [cstr(d.schedule_date), d.depreciation_amount, d.accumulated_depreciation_amount] + for d in asset_doc.get("schedules") + ] + + self.assertEqual(schedules, expected_schedules) + def make_asset_value_adjustment(**args): args = frappe._dict(args) From 19eb6af65fc0b9d953e90aab0a95149e4f69ca52 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Tue, 22 Aug 2023 05:32:56 +0530 Subject: [PATCH 220/501] chore: clean up stale code in reconciliation tool (cherry picked from commit e93b92705147adad3ffc6d37e863b898cfe8378c) --- .../payment_reconciliation.py | 63 ------------------- 1 file changed, 63 deletions(-) diff --git a/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py b/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py index b6708ce24b14..cc3ec26066f8 100644 --- a/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py +++ b/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py @@ -385,59 +385,6 @@ def reconcile(self): self.get_unreconciled_entries() - def make_difference_entry(self, row): - journal_entry = frappe.new_doc("Journal Entry") - journal_entry.voucher_type = "Exchange Gain Or Loss" - journal_entry.company = self.company - journal_entry.posting_date = nowdate() - journal_entry.multi_currency = 1 - - party_account_currency = frappe.get_cached_value( - "Account", self.receivable_payable_account, "account_currency" - ) - difference_account_currency = frappe.get_cached_value( - "Account", row.difference_account, "account_currency" - ) - - # Account Currency has balance - dr_or_cr = "debit" if self.party_type == "Customer" else "credit" - reverse_dr_or_cr = "debit" if dr_or_cr == "credit" else "credit" - - journal_account = frappe._dict( - { - "account": self.receivable_payable_account, - "party_type": self.party_type, - "party": self.party, - "account_currency": party_account_currency, - "exchange_rate": 0, - "cost_center": erpnext.get_default_cost_center(self.company), - "reference_type": row.against_voucher_type, - "reference_name": row.against_voucher, - dr_or_cr: flt(row.difference_amount), - dr_or_cr + "_in_account_currency": 0, - } - ) - - journal_entry.append("accounts", journal_account) - - journal_account = frappe._dict( - { - "account": row.difference_account, - "account_currency": difference_account_currency, - "exchange_rate": 1, - "cost_center": erpnext.get_default_cost_center(self.company), - reverse_dr_or_cr + "_in_account_currency": flt(row.difference_amount), - reverse_dr_or_cr: flt(row.difference_amount), - } - ) - - journal_entry.append("accounts", journal_account) - - journal_entry.save() - journal_entry.submit() - - return journal_entry - def get_payment_details(self, row, dr_or_cr): return frappe._dict( { @@ -603,16 +550,6 @@ def get_conditions(self, get_payments=False): def reconcile_dr_cr_note(dr_cr_notes, company): - def get_difference_row(inv): - if inv.difference_amount != 0 and inv.difference_account: - difference_row = { - "account": inv.difference_account, - inv.dr_or_cr: abs(inv.difference_amount) if inv.difference_amount > 0 else 0, - reconcile_dr_or_cr: abs(inv.difference_amount) if inv.difference_amount < 0 else 0, - "cost_center": erpnext.get_default_cost_center(company), - } - return difference_row - for inv in dr_cr_notes: voucher_type = "Credit Note" if inv.voucher_type == "Sales Invoice" else "Debit Note" From 873ee384a1f0832faf84015167f7bdc697f1dc46 Mon Sep 17 00:00:00 2001 From: rohitwaghchaure Date: Tue, 22 Aug 2023 16:57:08 +0530 Subject: [PATCH 221/501] fix: not able to make stock entry (#36759) --- erpnext/stock/doctype/material_request/material_request.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/erpnext/stock/doctype/material_request/material_request.py b/erpnext/stock/doctype/material_request/material_request.py index 159fd32c1230..bfb0d4e18533 100644 --- a/erpnext/stock/doctype/material_request/material_request.py +++ b/erpnext/stock/doctype/material_request/material_request.py @@ -660,7 +660,10 @@ def set_missing_values(source, target): "job_card_item": "job_card_item", }, "postprocess": update_item, - "condition": lambda doc: doc.ordered_qty < doc.stock_qty, + "condition": lambda doc: ( + flt(doc.ordered_qty, doc.precision("ordered_qty")) + < flt(doc.stock_qty, doc.precision("ordered_qty")) + ), }, }, target_doc, From 6a9935c00e502a28fa976c963a218f7e11b891ec Mon Sep 17 00:00:00 2001 From: rohitwaghchaure Date: Tue, 22 Aug 2023 19:25:50 +0530 Subject: [PATCH 222/501] fix: Procurement Tracker report not showing material request items (#36768) --- .../procurement_tracker.py | 52 ++++++++++--------- 1 file changed, 28 insertions(+), 24 deletions(-) diff --git a/erpnext/buying/report/procurement_tracker/procurement_tracker.py b/erpnext/buying/report/procurement_tracker/procurement_tracker.py index 71019e803775..a7e03c08face 100644 --- a/erpnext/buying/report/procurement_tracker/procurement_tracker.py +++ b/erpnext/buying/report/procurement_tracker/procurement_tracker.py @@ -154,31 +154,35 @@ def get_data(filters): procurement_record = [] if procurement_record_against_mr: procurement_record += procurement_record_against_mr + for po in purchase_order_entry: # fetch material records linked to the purchase order item - mr_record = mr_records.get(po.material_request_item, [{}])[0] - procurement_detail = { - "material_request_date": mr_record.get("transaction_date"), - "cost_center": po.cost_center, - "project": po.project, - "requesting_site": po.warehouse, - "requestor": po.owner, - "material_request_no": po.material_request, - "item_code": po.item_code, - "quantity": flt(po.qty), - "unit_of_measurement": po.stock_uom, - "status": po.status, - "purchase_order_date": po.transaction_date, - "purchase_order": po.parent, - "supplier": po.supplier, - "estimated_cost": flt(mr_record.get("amount")), - "actual_cost": flt(pi_records.get(po.name)), - "purchase_order_amt": flt(po.amount), - "purchase_order_amt_in_company_currency": flt(po.base_amount), - "expected_delivery_date": po.schedule_date, - "actual_delivery_date": pr_records.get(po.name), - } - procurement_record.append(procurement_detail) + material_requests = mr_records.get(po.material_request_item, [{}]) + + for mr_record in material_requests: + procurement_detail = { + "material_request_date": mr_record.get("transaction_date"), + "cost_center": po.cost_center, + "project": po.project, + "requesting_site": po.warehouse, + "requestor": po.owner, + "material_request_no": po.material_request, + "item_code": po.item_code, + "quantity": flt(po.qty), + "unit_of_measurement": po.stock_uom, + "status": po.status, + "purchase_order_date": po.transaction_date, + "purchase_order": po.parent, + "supplier": po.supplier, + "estimated_cost": flt(mr_record.get("amount")), + "actual_cost": flt(pi_records.get(po.name)), + "purchase_order_amt": flt(po.amount), + "purchase_order_amt_in_company_currency": flt(po.base_amount), + "expected_delivery_date": po.schedule_date, + "actual_delivery_date": pr_records.get(po.name), + } + procurement_record.append(procurement_detail) + return procurement_record @@ -301,7 +305,7 @@ def get_po_entries(filters): & (parent.name == child.parent) & (parent.status.notin(("Closed", "Completed", "Cancelled"))) ) - .groupby(parent.name, child.item_code) + .groupby(parent.name, child.material_request_item) ) query = apply_filters_on_query(filters, parent, child, query) From 94612b90ef96d20fad29af051eeed74b43971a2a Mon Sep 17 00:00:00 2001 From: RitvikSardana Date: Tue, 22 Aug 2023 14:12:42 +0530 Subject: [PATCH 223/501] fix: Accounts Payable Currency bug (cherry picked from commit 9349bc77c58aedf36e80739412f5786b40c97c19) --- .../report/accounts_receivable/accounts_receivable.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/erpnext/accounts/report/accounts_receivable/accounts_receivable.py b/erpnext/accounts/report/accounts_receivable/accounts_receivable.py index a7b35a579a9c..751063ad8e64 100755 --- a/erpnext/accounts/report/accounts_receivable/accounts_receivable.py +++ b/erpnext/accounts/report/accounts_receivable/accounts_receivable.py @@ -214,8 +214,8 @@ def update_voucher_balance(self, ple): for party_type in self.party_type: if self.filters.get(scrub(party_type)): amount = ple.amount_in_account_currency - else: - amount = ple.amount + else: + amount = ple.amount amount_in_account_currency = ple.amount_in_account_currency # update voucher From d1b2ce35b6452da2953ab99f0146a9d39f39ff5d Mon Sep 17 00:00:00 2001 From: Frappe PR Bot Date: Wed, 23 Aug 2023 02:59:58 +0000 Subject: [PATCH 224/501] chore(release): Bumped to Version 14.36.0 # [14.36.0](https://github.com/frappe/erpnext/compare/v14.35.2...v14.36.0) (2023-08-23) ### Bug Fixes * Accounts Payable Currency bug ([94612b9](https://github.com/frappe/erpnext/commit/94612b90ef96d20fad29af051eeed74b43971a2a)) * Add company filters for account ([e62ffa9](https://github.com/frappe/erpnext/commit/e62ffa990d2015b9df734d783fb94627bb5b78fb)) * add missing items labels back ([#36737](https://github.com/frappe/erpnext/issues/36737)) ([3634e80](https://github.com/frappe/erpnext/commit/3634e80341df075a3759b8f3b8994a02f423273d)) * broken advance field in Accounts Receivable summary rpt ([296a4d7](https://github.com/frappe/erpnext/commit/296a4d7a12714f978c2520483ed3b162ac71f843)) * broken consolidated report due to finance book filter ([5bd2a09](https://github.com/frappe/erpnext/commit/5bd2a0923f4693463ceb703cc216375325dd9c13)) * check tax and charges if it is passed ([1f76c69](https://github.com/frappe/erpnext/commit/1f76c6972b80d91fe549622556098009468ccb2c)) * clear dimension defaults after test ([2a467a9](https://github.com/frappe/erpnext/commit/2a467a9fbff26e01c77e45d7a297387ad1d83030)) * dict value for dimension for gl entries defined without the dimension ([11ba553](https://github.com/frappe/erpnext/commit/11ba553dbd72996de1118c27743dea30267a4244)) * dimension name in remark ([01ae513](https://github.com/frappe/erpnext/commit/01ae513f70308489ee814c1b361b24b2dbb34d60)) * divide offsetting amount for multiple dimensions ([1269f2d](https://github.com/frappe/erpnext/commit/1269f2d301fe03aaf36d3c2b749d6774b6e1e786)) * divide offsetting amount only when account exists ([2c8c3a0](https://github.com/frappe/erpnext/commit/2c8c3a022c28314e87eaafd3ef7c991a86e8f8af)) * Document Name link validation in Bank Reconciliation Tool ([#36495](https://github.com/frappe/erpnext/issues/36495)) ([83cbc1b](https://github.com/frappe/erpnext/commit/83cbc1bef6fb8b9302c7d6ba747d648296edd8a3)), closes [#35540](https://github.com/frappe/erpnext/issues/35540) [#35540](https://github.com/frappe/erpnext/issues/35540) * don't throw if item does not have default BOM (backport [#36709](https://github.com/frappe/erpnext/issues/36709)) ([#36734](https://github.com/frappe/erpnext/issues/36734)) ([e1bd9a7](https://github.com/frappe/erpnext/commit/e1bd9a7e8dbfe0fbbe6afddfa2370ed36bbf6767)) * duplicate acc dimension in test ([cdb66bf](https://github.com/frappe/erpnext/commit/cdb66bf19824469b55b37c5f6f93b0022fabdd0c)) * fetch acc dimensions correctly when fieldname is different from name ([7ac35b4](https://github.com/frappe/erpnext/commit/7ac35b496aabac0344175b61fbf0bbe6945539fe)) * fetch accounting dimension details specific to company ([c1f1a21](https://github.com/frappe/erpnext/commit/c1f1a21714423d44c2469a2a867ccbb996395113)) * include gain/loss journal in AR/AP reports ([4606079](https://github.com/frappe/erpnext/commit/460607956821479d13977b841669a456deb6f30e)) * incorrect gl balance on multi company setup ([cb9aad3](https://github.com/frappe/erpnext/commit/cb9aad3e873b430926f9007e6fcd2dbbf2e71111)) * incorrect schedule in asset value adjustment ([#36725](https://github.com/frappe/erpnext/issues/36725)) ([a0575ed](https://github.com/frappe/erpnext/commit/a0575ed2b0dec34ba1de14fe7b09aa8195be5e45)) * make offsetting entry for acc dimensions ([3198f26](https://github.com/frappe/erpnext/commit/3198f2669dcbb638d5bdfac3575722918dbd8046)) * make offsetting entry for all doctypes ([f578c32](https://github.com/frappe/erpnext/commit/f578c3219d517a7ba102ab4e270102b4eb6e9ded)) * mode of payment fetched from pos profile company in POS ([c74a414](https://github.com/frappe/erpnext/commit/c74a414313d721e7ee07820f3597c9ba1b7c171b)) * not able to make stock entry ([#36759](https://github.com/frappe/erpnext/issues/36759)) ([873ee38](https://github.com/frappe/erpnext/commit/873ee384a1f0832faf84015167f7bdc697f1dc46)) * Procurement Tracker report not showing material request items ([#36768](https://github.com/frappe/erpnext/issues/36768)) ([6a9935c](https://github.com/frappe/erpnext/commit/6a9935c00e502a28fa976c963a218f7e11b891ec)) * reset dimension defaults when company changedin test ([37ef6e9](https://github.com/frappe/erpnext/commit/37ef6e959b8019b953ff30175fb38b85fe16ea47)) * **RFQ:** make "update password" and "submit quotation" buttons the same size (backport [#36667](https://github.com/frappe/erpnext/issues/36667)) ([#36686](https://github.com/frappe/erpnext/issues/36686)) ([36147ec](https://github.com/frappe/erpnext/commit/36147ec02c595971dfcb73ec471bf726cad1997e)) * timeout error coming during reposting ([#36715](https://github.com/frappe/erpnext/issues/36715)) ([620b21f](https://github.com/frappe/erpnext/commit/620b21fec59e40178fde96085003d419804f21e0)) * **ux:** change batch selection message to alert (backport [#36500](https://github.com/frappe/erpnext/issues/36500)) ([#36697](https://github.com/frappe/erpnext/issues/36697)) ([0a4947a](https://github.com/frappe/erpnext/commit/0a4947a8f612244822b95fdbc94cf679a7855286)) * **UX:** Ignore prepared report ([3e23e1f](https://github.com/frappe/erpnext/commit/3e23e1fb91fdcc6e97929bb5528bb61dd15d7892)) ### Features * **RFQ:** make email message fully configurable (backport [#36353](https://github.com/frappe/erpnext/issues/36353)) ([#36531](https://github.com/frappe/erpnext/issues/36531)) ([c308bd5](https://github.com/frappe/erpnext/commit/c308bd53097679c0d5dab16af2786861b1dedf2f)) * Tick on checkbox to include draft timesheets (backport [#36577](https://github.com/frappe/erpnext/issues/36577)) ([#36640](https://github.com/frappe/erpnext/issues/36640)) ([9668615](https://github.com/frappe/erpnext/commit/9668615f7e9e602f39c00cd3bd4cbcb09039d2f2)) * utility to repost accounting ledgers without cancellation ([#36469](https://github.com/frappe/erpnext/issues/36469)) ([f8d6fe6](https://github.com/frappe/erpnext/commit/f8d6fe6be06458050105b9c57939f332e396ca80)) ### Performance Improvements * pull latest details only for referenced vouchers ([47345e8](https://github.com/frappe/erpnext/commit/47345e81a1342c3a6c904c66368460b18dd2816a)) --- erpnext/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/__init__.py b/erpnext/__init__.py index ae6060e3d629..22bf6f806938 100644 --- a/erpnext/__init__.py +++ b/erpnext/__init__.py @@ -3,7 +3,7 @@ import frappe -__version__ = "14.35.2" +__version__ = "14.36.0" def get_default_company(user=None): From 4556c36736cf79e9861c2feb5e022961e738bbf5 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Wed, 3 May 2023 20:45:12 +0530 Subject: [PATCH 225/501] refactor: limit output to 50 in reconciliation tool (cherry picked from commit 7a381affce02e5aea2c5a9a38ebf2638e8128f9a) # Conflicts: # erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.json --- .../payment_reconciliation.json | 29 +++++++++++++++++++ .../payment_reconciliation.py | 2 ++ erpnext/accounts/utils.py | 23 +++++++++++++-- 3 files changed, 52 insertions(+), 2 deletions(-) diff --git a/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.json b/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.json index 18d34850850f..c988b36cb017 100644 --- a/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.json +++ b/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.json @@ -26,8 +26,10 @@ "bank_cash_account", "cost_center", "sec_break1", + "invoice_name", "invoices", "column_break_15", + "payment_name", "payments", "sec_break2", "allocation" @@ -136,6 +138,7 @@ "label": "Minimum Invoice Amount" }, { + "default": "50", "description": "System will fetch all the entries if limit value is zero.", "fieldname": "invoice_limit", "fieldtype": "Int", @@ -166,6 +169,7 @@ "label": "Maximum Payment Amount" }, { + "default": "50", "description": "System will fetch all the entries if limit value is zero.", "fieldname": "payment_limit", "fieldtype": "Int", @@ -185,13 +189,38 @@ "fieldtype": "Link", "label": "Cost Center", "options": "Cost Center" +<<<<<<< HEAD +======= + }, + { + "depends_on": "eval:doc.party", + "fieldname": "default_advance_account", + "fieldtype": "Link", + "label": "Default Advance Account", + "mandatory_depends_on": "doc.party_type", + "options": "Account" + }, + { + "fieldname": "invoice_name", + "fieldtype": "Data", + "label": "Filter on Invoice" + }, + { + "fieldname": "payment_name", + "fieldtype": "Data", + "label": "Filter on Payment" +>>>>>>> 7a381affce (refactor: limit output to 50 in reconciliation tool) } ], "hide_toolbar": 1, "icon": "icon-resize-horizontal", "issingle": 1, "links": [], +<<<<<<< HEAD "modified": "2022-04-29 15:37:10.246831", +======= + "modified": "2023-08-15 05:35:50.109290", +>>>>>>> 7a381affce (refactor: limit output to 50 in reconciliation tool) "modified_by": "Administrator", "module": "Accounts", "name": "Payment Reconciliation", diff --git a/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py b/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py index cc3ec26066f8..6db39cc9991f 100644 --- a/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py +++ b/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py @@ -210,6 +210,8 @@ def get_invoice_entries(self): min_outstanding=self.minimum_invoice_amount if self.minimum_invoice_amount else None, max_outstanding=self.maximum_invoice_amount if self.maximum_invoice_amount else None, accounting_dimensions=self.accounting_dimension_filter_conditions, + limit=self.invoice_limit, + voucher_no=self.invoice_name, ) cr_dr_notes = ( diff --git a/erpnext/accounts/utils.py b/erpnext/accounts/utils.py index 2df3387b83e3..a089a856c046 100644 --- a/erpnext/accounts/utils.py +++ b/erpnext/accounts/utils.py @@ -884,7 +884,9 @@ def get_outstanding_invoices( min_outstanding=None, max_outstanding=None, accounting_dimensions=None, - vouchers=None, + vouchers=None, # list of dicts [{'voucher_type': '', 'voucher_no': ''}] for filtering + limit=None, # passed by reconciliation tool + voucher_no=None, # filter passed by reconciliation tool ): ple = qb.DocType("Payment Ledger Entry") @@ -917,6 +919,8 @@ def get_outstanding_invoices( max_outstanding=max_outstanding, get_invoices=True, accounting_dimensions=accounting_dimensions or [], + limit=limit, + voucher_no=voucher_no, ) for d in invoice_list: @@ -1648,12 +1652,13 @@ def __init__(self): self.voucher_posting_date = [] self.min_outstanding = None self.max_outstanding = None + self.limit = self.voucher_no = None def reset(self): # clear filters self.vouchers.clear() self.common_filter.clear() - self.min_outstanding = self.max_outstanding = None + self.min_outstanding = self.max_outstanding = self.limit = None # clear result self.voucher_outstandings.clear() @@ -1667,6 +1672,7 @@ def query_for_outstanding(self): filter_on_voucher_no = [] filter_on_against_voucher_no = [] + if self.vouchers: voucher_types = set([x.voucher_type for x in self.vouchers]) voucher_nos = set([x.voucher_no for x in self.vouchers]) @@ -1677,6 +1683,10 @@ def query_for_outstanding(self): filter_on_against_voucher_no.append(ple.against_voucher_type.isin(voucher_types)) filter_on_against_voucher_no.append(ple.against_voucher_no.isin(voucher_nos)) + if self.voucher_no: + filter_on_voucher_no.append(ple.voucher_no.like(f"%{self.voucher_no}%")) + filter_on_against_voucher_no.append(ple.against_voucher_no.like(f"%{self.voucher_no}%")) + # build outstanding amount filter filter_on_outstanding_amount = [] if self.min_outstanding: @@ -1792,6 +1802,11 @@ def query_for_outstanding(self): ) ) + if self.limit: + self.cte_query_voucher_amount_and_outstanding = ( + self.cte_query_voucher_amount_and_outstanding.limit(self.limit) + ) + # execute SQL self.voucher_outstandings = self.cte_query_voucher_amount_and_outstanding.run(as_dict=True) @@ -1805,6 +1820,8 @@ def get_voucher_outstandings( get_payments=False, get_invoices=False, accounting_dimensions=None, + limit=None, + voucher_no=None, ): """ Fetch voucher amount and outstanding amount from Payment Ledger using Database CTE @@ -1826,6 +1843,8 @@ def get_voucher_outstandings( self.max_outstanding = max_outstanding self.get_payments = get_payments self.get_invoices = get_invoices + self.limit = limit + self.voucher_no = voucher_no self.query_for_outstanding() return self.voucher_outstandings From d727a13562d3585e0522071b9e25dfd2e8ca7294 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Tue, 4 Jul 2023 08:40:47 +0530 Subject: [PATCH 226/501] refactor: trigger on value change (cherry picked from commit e48f8139ebdd5aa64e3b9139157948a7a17afb61) --- .../payment_reconciliation/payment_reconciliation.js | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.js b/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.js index 07f35c9fe11b..6f1f34bc9f33 100644 --- a/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.js +++ b/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.js @@ -151,6 +151,15 @@ erpnext.accounts.PaymentReconciliationController = class PaymentReconciliationCo this.frm.refresh(); } + invoice_name() { + this.frm.trigger("get_unreconciled_entries"); + } + + payment_name() { + this.frm.trigger("get_unreconciled_entries"); + } + + clear_child_tables() { this.frm.clear_table("invoices"); this.frm.clear_table("payments"); From a8c53eeb931c2fe15bdfe3072c88539be7aeed4b Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Tue, 4 Jul 2023 08:49:30 +0530 Subject: [PATCH 227/501] refactor: filter on cr/dr notes (cherry picked from commit 52f609e67a30988e498cf03bc0da7fa946ab3f5d) --- .../payment_reconciliation.py | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py b/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py index 6db39cc9991f..258587b9999c 100644 --- a/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py +++ b/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py @@ -5,6 +5,7 @@ import frappe from frappe import _, msgprint, qb from frappe.model.document import Document +from frappe.query_builder import Criterion from frappe.query_builder.custom import ConstantColumn from frappe.utils import flt, fmt_money, get_link_to_form, getdate, nowdate, today @@ -130,6 +131,15 @@ def get_jv_entries(self): def get_return_invoices(self): voucher_type = "Sales Invoice" if self.party_type == "Customer" else "Purchase Invoice" doc = qb.DocType(voucher_type) + + conditions = [] + conditions.append(doc.docstatus == 1) + conditions.append(doc[frappe.scrub(self.party_type)] == self.party) + conditions.append(doc.is_return == 1) + + if self.payment_name: + conditions.append(doc.name.like(f"%{self.payment_name}%")) + self.return_invoices = ( qb.from_(doc) .select( @@ -137,11 +147,7 @@ def get_return_invoices(self): doc.name.as_("voucher_no"), doc.return_against, ) - .where( - (doc.docstatus == 1) - & (doc[frappe.scrub(self.party_type)] == self.party) - & (doc.is_return == 1) - ) + .where(Criterion.all(conditions)) .run(as_dict=True) ) From c5080abd4648bd03c062bf62ccaf95a6a69d35ce Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Tue, 4 Jul 2023 09:02:05 +0530 Subject: [PATCH 228/501] refactor: filter on advance payments (cherry picked from commit 86bac2cf52e6094755e8f21df871a7f56a4d53a5) # Conflicts: # erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py # erpnext/controllers/accounts_controller.py --- .../payment_reconciliation.py | 18 +++++++++ erpnext/controllers/accounts_controller.py | 38 +++++++++++++++++++ 2 files changed, 56 insertions(+) diff --git a/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py b/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py index 258587b9999c..051f769fb240 100644 --- a/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py +++ b/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py @@ -58,7 +58,25 @@ def get_nonreconciled_payment_entries(self): def get_payment_entries(self): order_doctype = "Sales Order" if self.party_type == "Customer" else "Purchase Order" +<<<<<<< HEAD condition = self.get_conditions(get_payments=True) +======= + condition = frappe._dict( + { + "company": self.get("company"), + "get_payments": True, + "cost_center": self.get("cost_center"), + "from_payment_date": self.get("from_payment_date"), + "to_payment_date": self.get("to_payment_date"), + "maximum_payment_amount": self.get("maximum_payment_amount"), + "minimum_payment_amount": self.get("minimum_payment_amount"), + } + ) + + if self.payment_name: + condition.update({"name": self.payment_name}) + +>>>>>>> 86bac2cf52 (refactor: filter on advance payments) payment_entries = get_advance_payment_entries( self.party_type, self.party, diff --git a/erpnext/controllers/accounts_controller.py b/erpnext/controllers/accounts_controller.py index 76fe6a91182e..8b88599c4d1c 100644 --- a/erpnext/controllers/accounts_controller.py +++ b/erpnext/controllers/accounts_controller.py @@ -2320,10 +2320,48 @@ def get_advance_payment_entries( payment_entries_against_order, unallocated_payment_entries = [], [] limit_cond = "limit %s" % limit if limit else "" +<<<<<<< HEAD if order_list or against_all_orders: if order_list: reference_condition = " and t2.reference_name in ({0})".format( ", ".join(["%s"] * len(order_list)) +======= + if payment_type == "Receive": + q = q.select((payment_entry.source_exchange_rate).as_("exchange_rate")) + else: + q = q.select((payment_entry.target_exchange_rate).as_("exchange_rate")) + + if condition: + if condition.get("name", None): + q = q.where(payment_entry.name.like(f"%{condition.get('name')}%")) + + q = q.where(payment_entry.company == condition["company"]) + q = ( + q.where(payment_entry.posting_date >= condition["from_payment_date"]) + if condition.get("from_payment_date") + else q + ) + q = ( + q.where(payment_entry.posting_date <= condition["to_payment_date"]) + if condition.get("to_payment_date") + else q + ) + if condition.get("get_payments") == True: + q = ( + q.where(payment_entry.cost_center == condition["cost_center"]) + if condition.get("cost_center") + else q + ) + q = ( + q.where(payment_entry.unallocated_amount >= condition["minimum_payment_amount"]) + if condition.get("minimum_payment_amount") + else q + ) + q = ( + q.where(payment_entry.unallocated_amount <= condition["maximum_payment_amount"]) + if condition.get("maximum_payment_amount") + else q +>>>>>>> 86bac2cf52 (refactor: filter on advance payments) ) else: reference_condition = "" From ab9da5281e338611f6bb3dcceb19eb9949e21501 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Tue, 4 Jul 2023 09:06:08 +0530 Subject: [PATCH 229/501] refactor: filter for journal entries (cherry picked from commit d01f0f2e965d776b9b142e33559efe012b4734e5) --- .../doctype/payment_reconciliation/payment_reconciliation.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py b/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py index 051f769fb240..27b5af600b6b 100644 --- a/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py +++ b/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py @@ -92,6 +92,9 @@ def get_payment_entries(self): def get_jv_entries(self): condition = self.get_conditions() + if self.payment_name: + condition += f" and t1.name like '%%{self.payment_name}%%'" + if self.get("cost_center"): condition += f" and t2.cost_center = '{self.cost_center}' " From 4a4cba071542ef14786abf220bbde5477d036399 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Wed, 23 Aug 2023 12:57:00 +0530 Subject: [PATCH 230/501] chore: resolve conflict --- .../payment_reconciliation.json | 17 +-------- .../payment_reconciliation.py | 17 +-------- erpnext/controllers/accounts_controller.py | 38 ------------------- 3 files changed, 2 insertions(+), 70 deletions(-) diff --git a/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.json b/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.json index c988b36cb017..0dc9c135b8c9 100644 --- a/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.json +++ b/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.json @@ -189,16 +189,6 @@ "fieldtype": "Link", "label": "Cost Center", "options": "Cost Center" -<<<<<<< HEAD -======= - }, - { - "depends_on": "eval:doc.party", - "fieldname": "default_advance_account", - "fieldtype": "Link", - "label": "Default Advance Account", - "mandatory_depends_on": "doc.party_type", - "options": "Account" }, { "fieldname": "invoice_name", @@ -209,18 +199,13 @@ "fieldname": "payment_name", "fieldtype": "Data", "label": "Filter on Payment" ->>>>>>> 7a381affce (refactor: limit output to 50 in reconciliation tool) } ], "hide_toolbar": 1, "icon": "icon-resize-horizontal", "issingle": 1, "links": [], -<<<<<<< HEAD - "modified": "2022-04-29 15:37:10.246831", -======= "modified": "2023-08-15 05:35:50.109290", ->>>>>>> 7a381affce (refactor: limit output to 50 in reconciliation tool) "modified_by": "Administrator", "module": "Accounts", "name": "Payment Reconciliation", @@ -247,4 +232,4 @@ "sort_order": "DESC", "states": [], "track_changes": 1 -} \ No newline at end of file +} diff --git a/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py b/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py index 27b5af600b6b..abffd262961b 100644 --- a/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py +++ b/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py @@ -58,25 +58,10 @@ def get_nonreconciled_payment_entries(self): def get_payment_entries(self): order_doctype = "Sales Order" if self.party_type == "Customer" else "Purchase Order" -<<<<<<< HEAD condition = self.get_conditions(get_payments=True) -======= - condition = frappe._dict( - { - "company": self.get("company"), - "get_payments": True, - "cost_center": self.get("cost_center"), - "from_payment_date": self.get("from_payment_date"), - "to_payment_date": self.get("to_payment_date"), - "maximum_payment_amount": self.get("maximum_payment_amount"), - "minimum_payment_amount": self.get("minimum_payment_amount"), - } - ) - if self.payment_name: - condition.update({"name": self.payment_name}) + condition += "name like '%%{0}%%'".format(self.payment_name) ->>>>>>> 86bac2cf52 (refactor: filter on advance payments) payment_entries = get_advance_payment_entries( self.party_type, self.party, diff --git a/erpnext/controllers/accounts_controller.py b/erpnext/controllers/accounts_controller.py index 8b88599c4d1c..76fe6a91182e 100644 --- a/erpnext/controllers/accounts_controller.py +++ b/erpnext/controllers/accounts_controller.py @@ -2320,48 +2320,10 @@ def get_advance_payment_entries( payment_entries_against_order, unallocated_payment_entries = [], [] limit_cond = "limit %s" % limit if limit else "" -<<<<<<< HEAD if order_list or against_all_orders: if order_list: reference_condition = " and t2.reference_name in ({0})".format( ", ".join(["%s"] * len(order_list)) -======= - if payment_type == "Receive": - q = q.select((payment_entry.source_exchange_rate).as_("exchange_rate")) - else: - q = q.select((payment_entry.target_exchange_rate).as_("exchange_rate")) - - if condition: - if condition.get("name", None): - q = q.where(payment_entry.name.like(f"%{condition.get('name')}%")) - - q = q.where(payment_entry.company == condition["company"]) - q = ( - q.where(payment_entry.posting_date >= condition["from_payment_date"]) - if condition.get("from_payment_date") - else q - ) - q = ( - q.where(payment_entry.posting_date <= condition["to_payment_date"]) - if condition.get("to_payment_date") - else q - ) - if condition.get("get_payments") == True: - q = ( - q.where(payment_entry.cost_center == condition["cost_center"]) - if condition.get("cost_center") - else q - ) - q = ( - q.where(payment_entry.unallocated_amount >= condition["minimum_payment_amount"]) - if condition.get("minimum_payment_amount") - else q - ) - q = ( - q.where(payment_entry.unallocated_amount <= condition["maximum_payment_amount"]) - if condition.get("maximum_payment_amount") - else q ->>>>>>> 86bac2cf52 (refactor: filter on advance payments) ) else: reference_condition = "" From 20c45c79757a6c3d6ece0815973426de58a8a587 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Wed, 23 Aug 2023 14:15:35 +0530 Subject: [PATCH 231/501] chore: linter fix --- erpnext/accounts/utils.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/erpnext/accounts/utils.py b/erpnext/accounts/utils.py index a089a856c046..0c01ff78c8c1 100644 --- a/erpnext/accounts/utils.py +++ b/erpnext/accounts/utils.py @@ -884,9 +884,9 @@ def get_outstanding_invoices( min_outstanding=None, max_outstanding=None, accounting_dimensions=None, - vouchers=None, # list of dicts [{'voucher_type': '', 'voucher_no': ''}] for filtering - limit=None, # passed by reconciliation tool - voucher_no=None, # filter passed by reconciliation tool + vouchers=None, # list of dicts [{'voucher_type': '', 'voucher_no': ''}] for filtering + limit=None, # passed by reconciliation tool + voucher_no=None, # filter passed by reconciliation tool ): ple = qb.DocType("Payment Ledger Entry") From 4fa07777e98ab9a4848f235d7a5e80b88a538b80 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Thu, 24 Aug 2023 10:15:05 +0530 Subject: [PATCH 232/501] feat(MR): Project and Cost Center in Connections (backport #36794) (#36795) feat(MR): Project and Cost Center in Connections (#36794) (cherry picked from commit 54ffe41b54d1a2246d338668300a35dff6254665) Co-authored-by: Raffael Meyer <14891507+barredterra@users.noreply.github.com> --- .../doctype/material_request/material_request_dashboard.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/erpnext/stock/doctype/material_request/material_request_dashboard.py b/erpnext/stock/doctype/material_request/material_request_dashboard.py index 2bba52a4e253..f91ea6a0bba5 100644 --- a/erpnext/stock/doctype/material_request/material_request_dashboard.py +++ b/erpnext/stock/doctype/material_request/material_request_dashboard.py @@ -6,6 +6,8 @@ def get_data(): "fieldname": "material_request", "internal_links": { "Sales Order": ["items", "sales_order"], + "Project": ["items", "project"], + "Cost Center": ["items", "cost_center"], }, "transactions": [ { @@ -15,5 +17,6 @@ def get_data(): {"label": _("Stock"), "items": ["Stock Entry", "Purchase Receipt", "Pick List"]}, {"label": _("Manufacturing"), "items": ["Work Order"]}, {"label": _("Internal Transfer"), "items": ["Sales Order"]}, + {"label": _("Accounting Dimensions"), "items": ["Project", "Cost Center"]}, ], } From 6edfcf4de82156be6e2ef2204e264e59a2d3b6eb Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Thu, 24 Aug 2023 10:42:59 +0530 Subject: [PATCH 233/501] fix: SCR return status (backport #36793) (#36796) fix: SCR return status (#36793) (cherry picked from commit 723563c16797e77b0722048db18dfcf0c32535a6) Co-authored-by: s-aga-r --- .../subcontracting_receipt.py | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.py b/erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.py index ecec73e265c2..cf457dfe82db 100644 --- a/erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.py +++ b/erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.py @@ -268,17 +268,24 @@ def update_status(self, status=None, update_modified=False): status = "Draft" elif self.docstatus == 1: status = "Completed" + if self.is_return: status = "Return" - return_against = frappe.get_doc("Subcontracting Receipt", self.return_against) - return_against.run_method("update_status") elif self.per_returned == 100: status = "Return Issued" + elif self.docstatus == 2: status = "Cancelled" + if self.is_return: + frappe.get_doc("Subcontracting Receipt", self.return_against).update_status( + update_modified=update_modified + ) + if status: - frappe.db.set_value("Subcontracting Receipt", self.name, "status", status, update_modified) + frappe.db.set_value( + "Subcontracting Receipt", self.name, "status", status, update_modified=update_modified + ) def get_gl_entries(self, warehouse_account=None): from erpnext.accounts.general_ledger import process_gl_map From e8dc63c89c01f6c8617a911ac87f56fc5768da12 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Thu, 24 Aug 2023 11:49:03 +0530 Subject: [PATCH 234/501] fix: Tax withholding reversal on Debit Notes (cherry picked from commit 6d9cebfee94a3485b207bbd5d298a680c26addef) --- .../tax_withholding_category/tax_withholding_category.py | 4 ++-- erpnext/controllers/sales_and_purchase_return.py | 2 ++ 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/erpnext/accounts/doctype/tax_withholding_category/tax_withholding_category.py b/erpnext/accounts/doctype/tax_withholding_category/tax_withholding_category.py index d17ca08c408d..943c0057f995 100644 --- a/erpnext/accounts/doctype/tax_withholding_category/tax_withholding_category.py +++ b/erpnext/accounts/doctype/tax_withholding_category/tax_withholding_category.py @@ -268,9 +268,9 @@ def get_tax_amount(party_type, parties, inv, tax_details, posting_date, pan_no=N net_total, limit_consumed, ldc.certificate_limit, ldc.rate, tax_details ) else: - tax_amount = net_total * tax_details.rate / 100 if net_total > 0 else 0 + tax_amount = net_total * tax_details.rate / 100 else: - tax_amount = net_total * tax_details.rate / 100 if net_total > 0 else 0 + tax_amount = net_total * tax_details.rate / 100 # once tds is deducted, not need to add vouchers in the invoice voucher_wise_amount = {} diff --git a/erpnext/controllers/sales_and_purchase_return.py b/erpnext/controllers/sales_and_purchase_return.py index b5d8246c1165..a69b21c7c1d9 100644 --- a/erpnext/controllers/sales_and_purchase_return.py +++ b/erpnext/controllers/sales_and_purchase_return.py @@ -345,6 +345,8 @@ def set_missing_values(source, target): elif doctype == "Purchase Invoice": # look for Print Heading "Debit Note" doc.select_print_heading = frappe.get_cached_value("Print Heading", _("Debit Note")) + if source.tax_withholding_category: + doc.set_onload("supplier_tds", source.tax_withholding_category) for tax in doc.get("taxes") or []: if tax.charge_type == "Actual": From fd4159423d7e127499bdde04184f9e11f5fa9e78 Mon Sep 17 00:00:00 2001 From: ViralKansodiya <141210323+viralkansodiya@users.noreply.github.com> Date: Fri, 25 Aug 2023 13:43:53 +0530 Subject: [PATCH 235/501] fix: error listindexoutofrange when save a production plan (#36807) fix: error listindexoutof range when save a production plan --- .../manufacturing/doctype/production_plan/production_plan.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/manufacturing/doctype/production_plan/production_plan.py b/erpnext/manufacturing/doctype/production_plan/production_plan.py index db4003bc5850..a494550423f5 100644 --- a/erpnext/manufacturing/doctype/production_plan/production_plan.py +++ b/erpnext/manufacturing/doctype/production_plan/production_plan.py @@ -53,7 +53,7 @@ def validate_sales_orders(self, sales_order=None): data = sales_order_query(filters={"company": self.company, "sales_orders": sales_orders}) title = _("Production Plan Already Submitted") - if not data: + if not data and sales_orders: msg = _("No items are available in the sales order {0} for production").format(sales_orders[0]) if len(sales_orders) > 1: sales_orders = ", ".join(sales_orders) From bc6bd81f87921c61cc855379eb2a915dadb2ff2b Mon Sep 17 00:00:00 2001 From: Gursheen Anand Date: Fri, 25 Aug 2023 15:05:26 +0530 Subject: [PATCH 236/501] fix: fetch JVs in tax withholding report without party values --- .../tds_payable_monthly.py | 54 ++++++++----------- 1 file changed, 23 insertions(+), 31 deletions(-) diff --git a/erpnext/accounts/report/tds_payable_monthly/tds_payable_monthly.py b/erpnext/accounts/report/tds_payable_monthly/tds_payable_monthly.py index 7d166614722e..7191720c57e5 100644 --- a/erpnext/accounts/report/tds_payable_monthly/tds_payable_monthly.py +++ b/erpnext/accounts/report/tds_payable_monthly/tds_payable_monthly.py @@ -257,7 +257,7 @@ def get_tds_docs(filters): } party = frappe.get_all(filters.get("party_type"), pluck="name") - query_filters.update({"against": ("in", party)}) + or_filters.update({"against": ("in", party), "voucher_type": "Journal Entry"}) if filters.get("party"): del query_filters["account"] @@ -294,7 +294,7 @@ def get_tds_docs(filters): if journal_entries: journal_entry_party_map = get_journal_entry_party_map(journal_entries) - get_doc_info(journal_entries, "Journal Entry", tax_category_map) + get_doc_info(journal_entries, "Journal Entry", tax_category_map, net_total_map) return ( tds_documents, @@ -309,7 +309,11 @@ def get_journal_entry_party_map(journal_entries): journal_entry_party_map = {} for d in frappe.db.get_all( "Journal Entry Account", - {"parent": ("in", journal_entries), "party_type": "Supplier", "party": ("is", "set")}, + { + "parent": ("in", journal_entries), + "party_type": ("in", ("Supplier", "Customer")), + "party": ("is", "set"), + }, ["parent", "party"], ): if d.parent not in journal_entry_party_map: @@ -320,41 +324,29 @@ def get_journal_entry_party_map(journal_entries): def get_doc_info(vouchers, doctype, tax_category_map, net_total_map=None): - if doctype == "Purchase Invoice": - fields = [ - "name", - "tax_withholding_category", - "base_tax_withholding_net_total", - "grand_total", - "base_total", - ] - elif doctype == "Sales Invoice": - fields = ["name", "base_net_total", "grand_total", "base_total"] - elif doctype == "Payment Entry": - fields = [ - "name", - "tax_withholding_category", - "paid_amount", - "paid_amount_after_tax", - "base_paid_amount", - ] - else: - fields = ["name", "tax_withholding_category"] + common_fields = ["name", "tax_withholding_category"] + fields_dict = { + "Purchase Invoice": ["base_tax_withholding_net_total", "grand_total", "base_total"], + "Sales Invoice": ["base_net_total", "grand_total", "base_total"], + "Payment Entry": ["paid_amount", "paid_amount_after_tax", "base_paid_amount"], + "Journal Entry": ["total_amount"], + } - entries = frappe.get_all(doctype, filters={"name": ("in", vouchers)}, fields=fields) + entries = frappe.get_all( + doctype, filters={"name": ("in", vouchers)}, fields=common_fields + fields_dict[doctype] + ) for entry in entries: tax_category_map.update({entry.name: entry.tax_withholding_category}) if doctype == "Purchase Invoice": - net_total_map.update( - {entry.name: [entry.base_tax_withholding_net_total, entry.grand_total, entry.base_total]} - ) + value = [entry.base_tax_withholding_net_total, entry.grand_total, entry.base_total] elif doctype == "Sales Invoice": - net_total_map.update({entry.name: [entry.base_net_total, entry.grand_total, entry.base_total]}) + value = [entry.base_net_total, entry.grand_total, entry.base_total] elif doctype == "Payment Entry": - net_total_map.update( - {entry.name: [entry.paid_amount, entry.paid_amount_after_tax, entry.base_paid_amount]} - ) + value = [entry.paid_amount, entry.paid_amount_after_tax, entry.base_paid_amount] + else: + value = [entry.total_amount] * 3 + net_total_map.update({entry.name: value}) def get_tax_rate_map(filters): From c4d338a59b932390e6b48a31a18e07037b7419b2 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Thu, 24 Aug 2023 20:46:33 +0530 Subject: [PATCH 237/501] refactor(test): make use of mixin in ar/ap report tests (cherry picked from commit bb7bed4c1a10aa49bbb1d14654454866918cf35c) --- .../test_accounts_receivable.py | 211 ++++++++---------- erpnext/accounts/test/accounts_mixin.py | 25 ++- 2 files changed, 121 insertions(+), 115 deletions(-) diff --git a/erpnext/accounts/report/accounts_receivable/test_accounts_receivable.py b/erpnext/accounts/report/accounts_receivable/test_accounts_receivable.py index 6f1889b34e14..0099e79e5dc8 100644 --- a/erpnext/accounts/report/accounts_receivable/test_accounts_receivable.py +++ b/erpnext/accounts/report/accounts_receivable/test_accounts_receivable.py @@ -8,20 +8,17 @@ 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 +from erpnext.accounts.test.accounts_mixin import AccountsTestMixin from erpnext.selling.doctype.sales_order.test_sales_order import make_sales_order -class TestAccountsReceivable(FrappeTestCase): +class TestAccountsReceivable(AccountsTestMixin, FrappeTestCase): def setUp(self): - frappe.db.sql("delete from `tabSales Invoice` where company='_Test Company 2'") - frappe.db.sql("delete from `tabSales Order` where company='_Test Company 2'") - 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() + self.create_company() + self.create_customer() + self.create_item() + self.create_usd_receivable_account() + self.clear_old_entries() def tearDown(self): frappe.db.rollback() @@ -49,9 +46,61 @@ def create_usd_account(self): debtors_usd.account_type = debtors.account_type self.debtors_usd = debtors_usd.save().name + def create_sales_invoice(self, no_payment_schedule=False, do_not_submit=False): + frappe.set_user("Administrator") + si = create_sales_invoice( + item=self.item, + company=self.company, + customer=self.customer, + debit_to=self.debit_to, + posting_date=today(), + parent_cost_center=self.cost_center, + cost_center=self.cost_center, + rate=100, + price_list_rate=100, + do_not_save=1, + ) + 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 = si.save() + if not do_not_submit: + si = si.submit() + return si + + def create_payment_entry(self, docname): + pe = get_payment_entry("Sales Invoice", docname, bank_account=self.cash, party_amount=40) + pe.paid_from = self.debit_to + pe.insert() + pe.submit() + + def create_credit_note(self, docname): + credit_note = create_sales_invoice( + company=self.company, + customer=self.customer, + item=self.item, + qty=-1, + debit_to=self.debit_to, + cost_center=self.cost_center, + is_return=1, + return_against=docname, + ) + + return credit_note + def test_accounts_receivable(self): filters = { - "company": "_Test Company 2", + "company": self.company, "based_on_payment_terms": 1, "report_date": today(), "range1": 30, @@ -61,7 +110,9 @@ def test_accounts_receivable(self): } # check invoice grand total and invoiced column's value for 3 payment terms - name = make_sales_invoice().name + si = self.create_sales_invoice() + name = si.name + report = execute(filters) expected_data = [[100, 30], [100, 50], [100, 20]] @@ -71,7 +122,7 @@ def test_accounts_receivable(self): self.assertEqual(expected_data[i - 1], [row.invoice_grand_total, row.invoiced]) # check invoice grand total, invoiced, paid and outstanding column's value after payment - make_payment(name) + self.create_payment_entry(si.name) report = execute(filters) expected_data_after_payment = [[100, 50, 10, 40], [100, 20, 0, 20]] @@ -84,10 +135,10 @@ def test_accounts_receivable(self): ) # check invoice grand total, invoiced, paid and outstanding column's value after credit note - make_credit_note(name) + self.create_credit_note(si.name) report = execute(filters) - expected_data_after_credit_note = [100, 0, 0, 40, -40, "Debtors - _TC2"] + expected_data_after_credit_note = [100, 0, 0, 40, -40, self.debit_to] row = report[1][0] self.assertEqual( @@ -108,21 +159,20 @@ def test_payment_againt_po_in_receivable_report(self): """ so = make_sales_order( - company="_Test Company 2", - customer="_Test Customer 2", - warehouse="Finished Goods - _TC2", - currency="EUR", - debit_to="Debtors - _TC2", - income_account="Sales - _TC2", - expense_account="Cost of Goods Sold - _TC2", - cost_center="Main - _TC2", + company=self.company, + customer=self.customer, + warehouse=self.warehouse, + debit_to=self.debit_to, + income_account=self.income_account, + expense_account=self.expense_account, + cost_center=self.cost_center, ) pe = get_payment_entry(so.doctype, so.name) pe = pe.save().submit() filters = { - "company": "_Test Company 2", + "company": self.company, "based_on_payment_terms": 0, "report_date": today(), "range1": 30, @@ -147,34 +197,32 @@ def test_payment_againt_po_in_receivable_report(self): ) @change_settings( - "Accounts Settings", {"allow_multi_currency_invoices_against_single_party_account": 1} + "Accounts Settings", + {"allow_multi_currency_invoices_against_single_party_account": 1, "allow_stale": 0}, ) def test_exchange_revaluation_for_party(self): """ - Exchange Revaluation for party on Receivable/Payable shoule be included + Exchange Revaluation for party on Receivable/Payable should be included """ - 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 = frappe.get_doc("Company", self.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 = self.create_sales_invoice(no_payment_schedule=True, do_not_submit=True) si.currency = "USD" - si.conversion_rate = 0.90 + si.conversion_rate = 80 si.debit_to = self.debtors_usd si = si.save().submit() # Exchange Revaluation err = frappe.new_doc("Exchange Rate Revaluation") - err.company = company + err.company = self.company err.posting_date = today() accounts = err.get_accounts_data() err.extend("accounts", accounts) - err.accounts[0].new_exchange_rate = 0.95 + err.accounts[0].new_exchange_rate = 85 row = err.accounts[0] row.new_balance_in_base_currency = flt( row.new_exchange_rate * flt(row.balance_in_account_currency) @@ -189,7 +237,7 @@ def test_exchange_revaluation_for_party(self): je = je.submit() filters = { - "company": company, + "company": self.company, "report_date": today(), "range1": 30, "range2": 60, @@ -198,7 +246,7 @@ def test_exchange_revaluation_for_party(self): } report = execute(filters) - expected_data_for_err = [0, -5, 0, 5] + expected_data_for_err = [0, -500, 0, 500] 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, @@ -214,46 +262,43 @@ def test_payment_against_credit_note(self): """ Payment against credit/debit note should be considered against the parent invoice """ - company = "_Test Company 2" - customer = "_Test Customer 2" - si1 = make_sales_invoice() + si1 = self.create_sales_invoice() - pe = get_payment_entry("Sales Invoice", si1.name, bank_account="Cash - _TC2") - pe.paid_from = "Debtors - _TC2" + pe = get_payment_entry(si1.doctype, si1.name, bank_account=self.cash) + pe.paid_from = self.debit_to pe.insert() pe.submit() - cr_note = make_credit_note(si1.name) + cr_note = self.create_credit_note(si1.name) - si2 = make_sales_invoice() + si2 = self.create_sales_invoice() # manually link cr_note with si2 using journal entry je = frappe.new_doc("Journal Entry") - je.company = company + je.company = self.company je.voucher_type = "Credit Note" je.posting_date = today() - debit_account = "Debtors - _TC2" debit_entry = { - "account": debit_account, + "account": self.debit_to, "party_type": "Customer", - "party": customer, + "party": self.customer, "debit": 100, "debit_in_account_currency": 100, "reference_type": cr_note.doctype, "reference_name": cr_note.name, - "cost_center": "Main - _TC2", + "cost_center": self.cost_center, } credit_entry = { - "account": debit_account, + "account": self.debit_to, "party_type": "Customer", - "party": customer, + "party": self.customer, "credit": 100, "credit_in_account_currency": 100, "reference_type": si2.doctype, "reference_name": si2.name, - "cost_center": "Main - _TC2", + "cost_center": self.cost_center, } je.append("accounts", debit_entry) @@ -261,7 +306,7 @@ def test_payment_against_credit_note(self): je = je.save().submit() filters = { - "company": company, + "company": self.company, "report_date": today(), "range1": 30, "range2": 60, @@ -270,65 +315,3 @@ def test_payment_against_credit_note(self): } report = execute(filters) self.assertEqual(report[1], []) - - -def make_sales_invoice(no_payment_schedule=False, do_not_submit=False): - frappe.set_user("Administrator") - - si = create_sales_invoice( - company="_Test Company 2", - customer="_Test Customer 2", - currency="EUR", - warehouse="Finished Goods - _TC2", - debit_to="Debtors - _TC2", - income_account="Sales - _TC2", - expense_account="Cost of Goods Sold - _TC2", - cost_center="Main - _TC2", - do_not_save=1, - ) - - 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 = si.save() - - if not do_not_submit: - si = si.submit() - - return si - - -def make_payment(docname): - pe = get_payment_entry("Sales Invoice", docname, bank_account="Cash - _TC2", party_amount=40) - pe.paid_from = "Debtors - _TC2" - pe.insert() - pe.submit() - - -def make_credit_note(docname): - credit_note = create_sales_invoice( - company="_Test Company 2", - customer="_Test Customer 2", - currency="EUR", - qty=-1, - warehouse="Finished Goods - _TC2", - debit_to="Debtors - _TC2", - income_account="Sales - _TC2", - expense_account="Cost of Goods Sold - _TC2", - cost_center="Main - _TC2", - is_return=1, - return_against=docname, - ) - - return credit_note diff --git a/erpnext/accounts/test/accounts_mixin.py b/erpnext/accounts/test/accounts_mixin.py index debfffdcbb3e..bf01362c97ff 100644 --- a/erpnext/accounts/test/accounts_mixin.py +++ b/erpnext/accounts/test/accounts_mixin.py @@ -60,7 +60,6 @@ def create_company(self, company_name="_Test Company", abbr="_TC"): self.income_account = "Sales - " + abbr self.expense_account = "Cost of Goods Sold - " + abbr self.debit_to = "Debtors - " + abbr - self.debit_usd = "Debtors USD - " + abbr self.cash = "Cash - " + abbr self.creditors = "Creditors - " + abbr self.retained_earnings = "Retained Earnings - " + abbr @@ -105,6 +104,28 @@ def create_company(self, company_name="_Test Company", abbr="_TC"): new_acc.save() setattr(self, acc.attribute_name, new_acc.name) + def create_usd_receivable_account(self): + account_name = "Debtors USD" + if not frappe.db.get_value( + "Account", filters={"account_name": account_name, "company": self.company} + ): + acc = frappe.new_doc("Account") + acc.account_name = account_name + acc.parent_account = "Accounts Receivable - " + self.company_abbr + acc.company = self.company + acc.account_currency = "USD" + acc.account_type = "Receivable" + acc.insert() + else: + name = frappe.db.get_value( + "Account", + filters={"account_name": account_name, "company": self.company}, + fieldname="name", + pluck=True, + ) + acc = frappe.get_doc("Account", name) + self.debtors_usd = acc.name + def clear_old_entries(self): doctype_list = [ "GL Entry", @@ -113,6 +134,8 @@ def clear_old_entries(self): "Purchase Invoice", "Payment Entry", "Journal Entry", + "Sales Order", + "Exchange Rate Revaluation", ] for doctype in doctype_list: qb.from_(qb.DocType(doctype)).delete().where(qb.DocType(doctype).company == self.company).run() From 78b0a52d41f32a9fa0557d835a8e1265eeb4ef36 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Fri, 25 Aug 2023 15:41:18 +0530 Subject: [PATCH 238/501] test: increase coverage in ar/ap report (cherry picked from commit ce81ffd84438a9fe9df251507adea50e1f55f089) --- .../test_accounts_receivable.py | 257 +++++++++++++++++- 1 file changed, 255 insertions(+), 2 deletions(-) diff --git a/erpnext/accounts/report/accounts_receivable/test_accounts_receivable.py b/erpnext/accounts/report/accounts_receivable/test_accounts_receivable.py index 0099e79e5dc8..0c7d931d2d54 100644 --- a/erpnext/accounts/report/accounts_receivable/test_accounts_receivable.py +++ b/erpnext/accounts/report/accounts_receivable/test_accounts_receivable.py @@ -107,6 +107,7 @@ def test_accounts_receivable(self): "range2": 60, "range3": 90, "range4": 120, + "show_remarks": True, } # check invoice grand total and invoiced column's value for 3 payment terms @@ -115,11 +116,11 @@ def test_accounts_receivable(self): report = execute(filters) - expected_data = [[100, 30], [100, 50], [100, 20]] + expected_data = [[100, 30, "No Remarks"], [100, 50, "No Remarks"], [100, 20, "No Remarks"]] for i in range(3): row = report[1][i - 1] - self.assertEqual(expected_data[i - 1], [row.invoice_grand_total, row.invoiced]) + self.assertEqual(expected_data[i - 1], [row.invoice_grand_total, row.invoiced, row.remarks]) # check invoice grand total, invoiced, paid and outstanding column's value after payment self.create_payment_entry(si.name) @@ -315,3 +316,255 @@ def test_payment_against_credit_note(self): } report = execute(filters) self.assertEqual(report[1], []) + + def test_group_by_party(self): + si1 = self.create_sales_invoice(do_not_submit=True) + si1.posting_date = add_days(today(), -1) + si1.save().submit() + si2 = self.create_sales_invoice(do_not_submit=True) + si2.items[0].rate = 85 + si2.save().submit() + + filters = { + "company": self.company, + "report_date": today(), + "range1": 30, + "range2": 60, + "range3": 90, + "range4": 120, + "group_by_party": True, + } + report = execute(filters)[1] + self.assertEqual(len(report), 5) + + # assert voucher rows + expected_voucher_rows = [ + [100.0, 100.0, 100.0, 100.0], + [85.0, 85.0, 85.0, 85.0], + ] + voucher_rows = [] + for x in report[0:2]: + voucher_rows.append( + [x.invoiced, x.outstanding, x.invoiced_in_account_currency, x.outstanding_in_account_currency] + ) + self.assertEqual(expected_voucher_rows, voucher_rows) + + # assert total rows + expected_total_rows = [ + [self.customer, 185.0, 185.0], # party total + {}, # empty row for padding + ["Total", 185.0, 185.0], # grand total + ] + party_total_row = report[2] + self.assertEqual( + expected_total_rows[0], + [ + party_total_row.get("party"), + party_total_row.get("invoiced"), + party_total_row.get("outstanding"), + ], + ) + empty_row = report[3] + self.assertEqual(expected_total_rows[1], empty_row) + grand_total_row = report[4] + self.assertEqual( + expected_total_rows[2], + [ + grand_total_row.get("party"), + grand_total_row.get("invoiced"), + grand_total_row.get("outstanding"), + ], + ) + + def test_future_payments(self): + si = self.create_sales_invoice() + pe = get_payment_entry(si.doctype, si.name) + pe.posting_date = add_days(today(), 1) + pe.paid_amount = 90.0 + pe.references[0].allocated_amount = 90.0 + pe.save().submit() + filters = { + "company": self.company, + "report_date": today(), + "range1": 30, + "range2": 60, + "range3": 90, + "range4": 120, + "show_future_payments": True, + } + report = execute(filters)[1] + self.assertEqual(len(report), 1) + + expected_data = [100.0, 100.0, 10.0, 90.0] + + row = report[0] + self.assertEqual( + expected_data, [row.invoiced, row.outstanding, row.remaining_balance, row.future_amount] + ) + + pe.cancel() + # full payment in future date + pe = get_payment_entry(si.doctype, si.name) + pe.posting_date = add_days(today(), 1) + pe.save().submit() + report = execute(filters)[1] + self.assertEqual(len(report), 1) + expected_data = [100.0, 100.0, 0.0, 100.0] + row = report[0] + self.assertEqual( + expected_data, [row.invoiced, row.outstanding, row.remaining_balance, row.future_amount] + ) + + pe.cancel() + # over payment in future date + pe = get_payment_entry(si.doctype, si.name) + pe.posting_date = add_days(today(), 1) + pe.paid_amount = 110 + pe.save().submit() + report = execute(filters)[1] + self.assertEqual(len(report), 2) + expected_data = [[100.0, 0.0, 100.0, 0.0, 100.0], [0.0, 10.0, -10.0, -10.0, 0.0]] + for idx, row in enumerate(report): + self.assertEqual( + expected_data[idx], + [row.invoiced, row.paid, row.outstanding, row.remaining_balance, row.future_amount], + ) + + def test_sales_person(self): + sales_person = ( + frappe.get_doc({"doctype": "Sales Person", "sales_person_name": "John Clark", "enabled": True}) + .insert() + .submit() + ) + si = self.create_sales_invoice(do_not_submit=True) + si.append("sales_team", {"sales_person": sales_person.name, "allocated_percentage": 100}) + si.save().submit() + + filters = { + "company": self.company, + "report_date": today(), + "range1": 30, + "range2": 60, + "range3": 90, + "range4": 120, + "sales_person": sales_person.name, + "show_sales_person": True, + } + report = execute(filters)[1] + self.assertEqual(len(report), 1) + + expected_data = [100.0, 100.0, sales_person.name] + + row = report[0] + self.assertEqual(expected_data, [row.invoiced, row.outstanding, row.sales_person]) + + def test_cost_center_filter(self): + si = self.create_sales_invoice() + filters = { + "company": self.company, + "report_date": today(), + "range1": 30, + "range2": 60, + "range3": 90, + "range4": 120, + "cost_center": self.cost_center, + } + report = execute(filters)[1] + self.assertEqual(len(report), 1) + expected_data = [100.0, 100.0, self.cost_center] + row = report[0] + self.assertEqual(expected_data, [row.invoiced, row.outstanding, row.cost_center]) + + def test_customer_group_filter(self): + si = self.create_sales_invoice() + cus_group = frappe.db.get_value("Customer", self.customer, "customer_group") + filters = { + "company": self.company, + "report_date": today(), + "range1": 30, + "range2": 60, + "range3": 90, + "range4": 120, + "customer_group": cus_group, + } + report = execute(filters)[1] + self.assertEqual(len(report), 1) + expected_data = [100.0, 100.0, cus_group] + row = report[0] + self.assertEqual(expected_data, [row.invoiced, row.outstanding, row.customer_group]) + + filters.update({"customer_group": "Individual"}) + report = execute(filters)[1] + self.assertEqual(len(report), 0) + + def test_party_account_filter(self): + si1 = self.create_sales_invoice() + self.customer2 = ( + frappe.get_doc( + { + "doctype": "Customer", + "customer_name": "Jane Doe", + "type": "Individual", + "default_currency": "USD", + } + ) + .insert() + .submit() + ) + + si2 = self.create_sales_invoice(do_not_submit=True) + si2.posting_date = add_days(today(), -1) + si2.customer = self.customer2 + si2.currency = "USD" + si2.conversion_rate = 80 + si2.debit_to = self.debtors_usd + si2.save().submit() + + # Filter on company currency receivable account + filters = { + "company": self.company, + "report_date": today(), + "range1": 30, + "range2": 60, + "range3": 90, + "range4": 120, + "party_account": self.debit_to, + } + report = execute(filters)[1] + self.assertEqual(len(report), 1) + expected_data = [100.0, 100.0, self.debit_to, si1.currency] + row = report[0] + self.assertEqual( + expected_data, [row.invoiced, row.outstanding, row.party_account, row.account_currency] + ) + + # Filter on USD receivable account + filters.update({"party_account": self.debtors_usd}) + report = execute(filters)[1] + self.assertEqual(len(report), 1) + expected_data = [8000.0, 8000.0, self.debtors_usd, si2.currency] + row = report[0] + self.assertEqual( + expected_data, [row.invoiced, row.outstanding, row.party_account, row.account_currency] + ) + + # without filter on party account + filters.pop("party_account") + report = execute(filters)[1] + self.assertEqual(len(report), 2) + expected_data = [ + [8000.0, 8000.0, 100.0, 100.0, self.debtors_usd, si2.currency], + [100.0, 100.0, 100.0, 100.0, self.debit_to, si1.currency], + ] + for idx, row in enumerate(report): + self.assertEqual( + expected_data[idx], + [ + row.invoiced, + row.outstanding, + row.invoiced_in_account_currency, + row.outstanding_in_account_currency, + row.party_account, + row.account_currency, + ], + ) From 0f98cc85e9dce00fa333474cfb438f243503df81 Mon Sep 17 00:00:00 2001 From: Gourav Saini <63018500+gouravsaini021@users.noreply.github.com> Date: Sat, 26 Aug 2023 18:04:01 +0530 Subject: [PATCH 239/501] fix: Allow to make return against sales invoice which has closed sales order fix: Allow to make return against sales invoice which has closed sales order --- erpnext/controllers/selling_controller.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/erpnext/controllers/selling_controller.py b/erpnext/controllers/selling_controller.py index ef289ff6a674..8948b7e3f7c3 100644 --- a/erpnext/controllers/selling_controller.py +++ b/erpnext/controllers/selling_controller.py @@ -388,7 +388,7 @@ def check_sales_order_on_hold_or_close(self, ref_fieldname): for d in self.get("items"): if d.get(ref_fieldname): status = frappe.db.get_value("Sales Order", d.get(ref_fieldname), "status") - if status in ("Closed", "On Hold"): + if status in ("Closed", "On Hold") and not self.is_return: frappe.throw(_("Sales Order {0} is {1}").format(d.get(ref_fieldname), status)) def update_reserved_qty(self): @@ -404,7 +404,9 @@ def update_reserved_qty(self): if so and so_item_rows: sales_order = frappe.get_doc("Sales Order", so) - if sales_order.status in ["Closed", "Cancelled"]: + if (sales_order.status == "Closed" and not self.is_return) or sales_order.status in [ + "Cancelled" + ]: frappe.throw( _("{0} {1} is cancelled or closed").format(_("Sales Order"), so), frappe.InvalidStatusError ) From c07548a61235f371b93a7afde9ab7e25c6dc03d0 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Sat, 26 Aug 2023 19:29:15 +0530 Subject: [PATCH 240/501] fix: missing company flag for regional fn (#36791) fix: missing company flag for regional fn (#36791) * fix: missing company flag for regional fn (cherry picked from commit 9bc5952dd55fd1dc8d2c8061e325c202793f81d3) Co-authored-by: Dany Robert --- erpnext/controllers/accounts_controller.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/erpnext/controllers/accounts_controller.py b/erpnext/controllers/accounts_controller.py index 76fe6a91182e..53039cfd8b5c 100644 --- a/erpnext/controllers/accounts_controller.py +++ b/erpnext/controllers/accounts_controller.py @@ -200,9 +200,9 @@ def validate(self): # apply tax withholding only if checked and applicable self.set_tax_withholding() - validate_regional(self) - - validate_einvoice_fields(self) + with temporary_flag("company", self.company): + validate_regional(self) + validate_einvoice_fields(self) if self.doctype != "Material Request" and not self.ignore_pricing_rule: apply_pricing_rule_on_transaction(self) From 9b2a84f2597644d3b8dfb1758516fd2ae344d1a9 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Sat, 26 Aug 2023 19:29:57 +0530 Subject: [PATCH 241/501] chore: update fr translation for Naming Series (#36785) * chore: update fr translation for Naming Series (#36785) * chore: update fr translation for Naming Series * chore: update fr translation * chore: update fr translation * chore: update fr translation (cherry picked from commit e462edc6282f6e95b948f4f2583dabd96a92ca62) # Conflicts: # erpnext/translations/fr.csv * chore: resolve conflicts --------- Co-authored-by: HENRY Florian Co-authored-by: Deepesh Garg --- erpnext/translations/fr.csv | 114 ++++++++++++++++++++---------------- 1 file changed, 63 insertions(+), 51 deletions(-) diff --git a/erpnext/translations/fr.csv b/erpnext/translations/fr.csv index fde3f57211e0..eb9076b4062f 100644 --- a/erpnext/translations/fr.csv +++ b/erpnext/translations/fr.csv @@ -3536,7 +3536,7 @@ Quality Feedback Template,Modèle de commentaires sur la qualité, Rules for applying different promotional schemes.,Règles d'application de différents programmes promotionnels., Shift,Décalage, Show {0},Montrer {0}, -"Special Characters except '-', '#', '.', '/', '{{' and '}}' not allowed in naming series {0}","Caractères spéciaux sauf "-", "#", ".", "/", "{{" Et "}}" non autorisés dans les séries de nommage {0}", +"Special Characters except '-', '#', '.', '/', '{{' and '}}' not allowed in naming series {0}","Caractères spéciaux sauf "-", "#", ".", "/", "{{" Et "}}" non autorisés dans les masques de numérotation {0}", Target Details,Détails de la cible, {0} already has a Parent Procedure {1}.,{0} a déjà une procédure parent {1}., API,API, @@ -3551,7 +3551,7 @@ Importing {0} of {1},Importer {0} de {1}, Invalid URL,URL invalide, Landscape,Paysage, Last Sync On,Dernière synchronisation le, -Naming Series,Nom de série, +Naming Series,Masque de numérotation, No data to export,Aucune donnée à exporter, Portrait,Portrait, Print Heading,Imprimer Titre, @@ -4282,7 +4282,7 @@ Please set {0},Veuillez définir {0},supplier Draft,Brouillon,"docstatus,=,0" Cancelled,Annulé,"docstatus,=,2" Please setup Instructor Naming System in Education > Education Settings,Veuillez configurer le système de dénomination de l'instructeur dans Éducation> Paramètres de l'éducation, -Please set Naming Series for {0} via Setup > Settings > Naming Series,Veuillez définir la série de noms pour {0} via Configuration> Paramètres> Série de noms, +Please set Naming Series for {0} via Setup > Settings > Naming Series,Veuillez définir le masque de numérotation pour {0} via Configuration> Paramètres> Série de noms, UOM Conversion factor ({0} -> {1}) not found for item: {2},Facteur de conversion UdM ({0} -> {1}) introuvable pour l'article: {2}, Item Code > Item Group > Brand,Code article> Groupe d'articles> Marque, Customer > Customer Group > Territory,Client> Groupe de clients> Territoire, @@ -4297,7 +4297,7 @@ Fetch Serial Numbers based on FIFO,Récupérer les numéros de série basés sur Current Odometer Value should be greater than Last Odometer Value {0},La valeur actuelle de l'odomètre doit être supérieure à la dernière valeur de l'odomètre {0}, No additional expenses has been added,Aucune dépense supplémentaire n'a été ajoutée, Asset{} {assets_link} created for {},Élément {} {assets_link} créé pour {}, -Row {}: Asset Naming Series is mandatory for the auto creation for item {},Ligne {}: la série de noms d'éléments est obligatoire pour la création automatique de l'élément {}, +Row {}: Asset Naming Series is mandatory for the auto creation for item {},Ligne {}: Le masque de numérotation d'éléments est obligatoire pour la création automatique de l'élément {}, Assets not created for {0}. You will have to create asset manually.,Éléments non créés pour {0}. Vous devrez créer un actif manuellement., {0} {1} has accounting entries in currency {2} for company {3}. Please select a receivable or payable account with currency {2}.,{0} {1} a des écritures comptables dans la devise {2} pour l'entreprise {3}. Veuillez sélectionner un compte à recevoir ou à payer avec la devise {2}., Invalid Account,Compte invalide, @@ -4321,7 +4321,7 @@ Advanced Settings,Réglages avancés, Path,Chemin, Components,Composants, Verified By,Vérifié Par, -Invalid naming series (. missing) for {0},Série de noms non valide (. Manquante) pour {0}, +Invalid naming series (. missing) for {0},Masque de numérotation non valide (. Manquante) pour {0}, Filter Based On,Filtre basé sur, Reqd by date,Reqd par date, Manufacturer Part Number {0} is invalid,Le numéro de pièce du fabricant {0} n'est pas valide, @@ -5933,7 +5933,7 @@ Student Admission Program,Programme d'admission des étudiants, Minimum Age,Âge Minimum, Maximum Age,Âge Maximum, Application Fee,Frais de Dossier, -Naming Series (for Student Applicant),Nom de série (pour un candidat étudiant), +Naming Series (for Student Applicant),Masque de numérotation (pour un candidat étudiant), LMS Only,LMS seulement, EDU-APP-.YYYY.-,EDU-APP-YYYY.-, Application Status,État de la Demande, @@ -6424,7 +6424,7 @@ Hotel Reservation User,Utilisateur chargé des réservations d'hôtel, Hotel Room Reservation Item,Article de réservation de la chambre d'hôtel, Hotel Settings,Paramètres d'Hotel, Default Taxes and Charges,Taxes et frais par défaut, -Default Invoice Naming Series,Numéro de série par défaut pour les factures, +Default Invoice Naming Series,Masque de numérotation par défaut pour les factures, Additional Salary,Salaire supplémentaire, HR,RH, HR-ADS-.YY.-.MM.-,HR-ADS-.YY .-. MM.-, @@ -8034,7 +8034,7 @@ Default Unit of Measure,Unité de Mesure par Défaut, Maintain Stock,Maintenir Stock, Standard Selling Rate,Prix de Vente Standard, Auto Create Assets on Purchase,Création automatique d'actifs à l'achat, -Asset Naming Series,Nom de série de l'actif, +Asset Naming Series,Masque de numérotation de l'actif, Over Delivery/Receipt Allowance (%),Surlivrance / indemnité de réception (%), Barcodes,Codes-barres, Shelf Life In Days,Durée de conservation en jours, @@ -8053,7 +8053,7 @@ Serial Nos and Batches,N° de Série et Lots, Has Batch No,A un Numéro de Lot, Automatically Create New Batch,Créer un Nouveau Lot Automatiquement, Batch Number Series,Série de numéros de lots, -"Example: ABCD.#####. If series is set and Batch No is not mentioned in transactions, then automatic batch number will be created based on this series. If you always want to explicitly mention Batch No for this item, leave this blank. Note: this setting will take priority over the Naming Series Prefix in Stock Settings.","Exemple: ABCD. #####. Si la série est définie et que le numéro de lot n'est pas mentionné dans les transactions, un numéro de lot sera automatiquement créé en avec cette série. Si vous préferez mentionner explicitement et systématiquement le numéro de lot pour cet article, laissez ce champ vide. Remarque: ce paramètre aura la priorité sur le préfixe de la série dans les paramètres de stock.", +"Example: ABCD.#####. If series is set and Batch No is not mentioned in transactions, then automatic batch number will be created based on this series. If you always want to explicitly mention Batch No for this item, leave this blank. Note: this setting will take priority over the Naming Series Prefix in Stock Settings.","Exemple: ABCD. #####. Si le masque est définie et que le numéro de lot n'est pas mentionné dans les transactions, un numéro de lot sera automatiquement créé en avec ce masque. Si vous préferez mentionner explicitement et systématiquement le numéro de lot pour cet article, laissez ce champ vide. Remarque: ce paramètre aura la priorité sur le préfixe du masque dans les paramètres de stock.", Has Expiry Date,A une date d'expiration, Retain Sample,Conserver l'échantillon, Max Sample Quantity,Quantité maximum d'échantillon, @@ -8353,8 +8353,8 @@ Inter Warehouse Transfer Settings,Paramètres de transfert entre entrepôts, Freeze Stock Entries,Geler les Entrées de Stocks, Stock Frozen Upto,Stock Gelé Jusqu'au, Batch Identification,Identification par lots, -Use Naming Series,Utiliser la série de noms, -Naming Series Prefix,Préfix du nom de série, +Use Naming Series,Utiliser le masque de numérotation, +Naming Series Prefix,Préfix du masque de numérotation, UOM Category,Catégorie d'unité de mesure (UdM), UOM Conversion Detail,Détails de Conversion de l'UdM, Variant Field,Champ de Variante, @@ -8824,7 +8824,7 @@ Is Inter State,Est Inter State, Purchase Details,Détails d'achat, Depreciation Posting Date,Date comptable de l'amortissement, "By default, the Supplier Name is set as per the Supplier Name entered. If you want Suppliers to be named by a ","Par défaut, le nom du fournisseur est défini selon le nom du fournisseur saisi. Si vous souhaitez que les fournisseurs soient nommés par un", - choose the 'Naming Series' option.,choisissez l'option 'Naming Series'., + choose the 'Naming Series' option.,choisissez l'option 'Masque de numérotation'., Configure the default Price List when creating a new Purchase transaction. Item prices will be fetched from this Price List.,Configurez la liste de prix par défaut lors de la création d'une nouvelle transaction d'achat. Les prix des articles seront extraits de cette liste de prix., "If this option is configured 'Yes', ERPNext will prevent you from creating a Purchase Invoice or Receipt without creating a Purchase Order first. This configuration can be overridden for a particular supplier by enabling the 'Allow Purchase Invoice Creation Without Purchase Order' checkbox in the Supplier master.","Si cette option est configurée «Oui», ERPNext vous empêchera de créer une facture d'achat ou un reçu sans créer d'abord une Commande d'Achat. Cette configuration peut être remplacée pour un fournisseur particulier en cochant la case «Autoriser la création de facture d'achat sans commmande d'achat» dans la fiche fournisseur.", "If this option is configured 'Yes', ERPNext will prevent you from creating a Purchase Invoice without creating a Purchase Receipt first. This configuration can be overridden for a particular supplier by enabling the 'Allow Purchase Invoice Creation Without Purchase Receipt' checkbox in the Supplier master.","Si cette option est configurée «Oui», ERPNext vous empêchera de créer une facture d'achat sans créer d'abord un reçu d'achat. Cette configuration peut être remplacée pour un fournisseur particulier en cochant la case "Autoriser la création de facture d'achat sans reçu d'achat" dans la fiche fournisseur.", @@ -9858,14 +9858,14 @@ Is Rate Adjustment Entry (Debit Note),Est un justement du prix de la note de dé Issue a debit note with 0 qty against an existing Sales Invoice,Creer une note de débit avec une quatité à O pour la facture Control Historical Stock Transactions,Controle de l'historique des stransaction de stock No stock transactions can be created or modified before this date.,Aucune transaction ne peux être créée ou modifié avant cette date. -Stock transactions that are older than the mentioned days cannot be modified.,Les transactions de stock plus ancienne que le nombre de jours ci-dessus ne peuvent être modifiées -Role Allowed to Create/Edit Back-dated Transactions,Rôle autorisé à créer et modifier des transactions anti-datée -"If mentioned, the system will allow only the users with this Role to create or modify any stock transaction earlier than the latest stock transaction for a specific item and warehouse. If set as blank, it allows all users to create/edit back-dated transactions.","Les utilisateur de ce role pourront creer et modifier des transactions dans le passé. Si vide tout les utilisateurs pourrons le faire" -Auto Insert Item Price If Missing,Création du prix de l'article dans les listes de prix si abscent -Update Existing Price List Rate,Mise a jour automatique du prix dans les listes de prix -Show Barcode Field in Stock Transactions,Afficher le champ Code Barre dans les transactions de stock -Convert Item Description to Clean HTML in Transactions,Convertir les descriptions d'articles en HTML valide lors des transactions -Have Default Naming Series for Batch ID?,Nom de série par défaut pour les Lots ou Séries +Stock transactions that are older than the mentioned days cannot be modified.,Les transactions de stock plus ancienne que le nombre de jours ci-dessus ne peuvent être modifiées, +Role Allowed to Create/Edit Back-dated Transactions,Rôle autorisé à créer et modifier des transactions anti-datée, +"If mentioned, the system will allow only the users with this Role to create or modify any stock transaction earlier than the latest stock transaction for a specific item and warehouse. If set as blank, it allows all users to create/edit back-dated transactions.",Les utilisateur de ce role pourront creer et modifier des transactions dans le passé. Si vide tout les utilisateurs pourrons le faire +Auto Insert Item Price If Missing,Création du prix de l'article dans les listes de prix si abscent, +Update Existing Price List Rate,Mise a jour automatique du prix dans les listes de prix, +Show Barcode Field in Stock Transactions,Afficher le champ Code Barre dans les transactions de stock, +Convert Item Description to Clean HTML in Transactions,Convertir les descriptions d'articles en HTML valide lors des transactions, +Have Default Naming Series for Batch ID?,Masque de numérotation par défaut pour les Lots ou Séries, "The percentage you are allowed to transfer more against the quantity ordered. For example, if you have ordered 100 units, and your Allowance is 10%, then you are allowed transfer 110 units","Le pourcentage de quantité que vous pourrez réceptionner en plus de la quantité commandée. Par exemple, vous avez commandé 100 unités, votre pourcentage de dépassement est de 10%, vous pourrez réceptionner 110 unités" Allowed Items,Articles autorisés Party Specific Item,Restriction d'article disponible @@ -9892,34 +9892,46 @@ Interview Feedback,Retour d'entretien Journal Energy Point,Historique des points d'énergies Billing Address Details,Adresse de facturation (détails) Supplier Address Details,Adresse Fournisseur (détails) -Retail,Commerce -Users,Utilisateurs -Permission Manager,Gestion des permissions -Fetch Timesheet,Récuprer les temps saisis -Get Supplier Group Details,Appliquer les informations depuis le Groupe de fournisseur -Quality Inspection(s),Inspection(s) Qualité -Set Advances and Allocate (FIFO),Affecter les encours au réglement -Apply Putaway Rule,Appliquer la régle de routage d'entrepot -Delete Transactions,Supprimer les transactions -Default Payment Discount Account,Compte par défaut des paiements de remise -Unrealized Profit / Loss Account,Compte de perte -Enable Provisional Accounting For Non Stock Items,Activer la provision pour les articles non stockés -Publish in Website,Publier sur le Site Web -List View,Vue en liste -Allow Excess Material Transfer,Autoriser les transfert de stock supérieurs à l'attendue -Allow transferring raw materials even after the Required Quantity is fulfilled,Autoriser les transfert de matiéres premiére mais si la quantité requise est atteinte -Add Corrective Operation Cost in Finished Good Valuation,Ajouter des opérations de correction de coût pour la valorisation des produits finis -Make Serial No / Batch from Work Order,Générer des numéros de séries / lots depuis les Ordres de Fabrications -System will automatically create the serial numbers / batch for the Finished Good on submission of work order,le systéme va créer des numéros de séries / lots à la validation des produit finis depuis les Ordres de Fabrications -Allow material consumptions without immediately manufacturing finished goods against a Work Order,Autoriser la consommation sans immédiatement fabriqué les produit fini dans les ordres de fabrication -Quality Inspection Parameter,Paramétre des Inspection Qualité -Parameter Group,Groupe de paramétre -E Commerce Settings,Paramétrage E-Commerce -Follow these steps to create a landing page for your store:,Suivez les intructions suivantes pour créer votre page d'accueil de boutique en ligne -Show Price in Quotation,Afficher les prix sur les devis -Add-ons,Extensions -Enable Wishlist,Activer la liste de souhaits -Enable Reviews and Ratings,Activer les avis et notes -Enable Recommendations,Activer les recommendations -Item Search Settings,Paramétrage de la recherche d'article -Purchase demande,Demande de materiel +Retail,Commerce, +Users,Utilisateurs, +Permission Manager,Gestion des permissions, +Fetch Timesheet,Récuprer les temps saisis, +Get Supplier Group Details,Appliquer les informations depuis le Groupe de fournisseur, +Quality Inspection(s),Inspection(s) Qualite, +Set Advances and Allocate (FIFO),Affecter les encours au réglement, +Apply Putaway Rule,Appliquer la régle de routage d'entrepot, +Delete Transactions,Supprimer les transactions, +Default Payment Discount Account,Compte par défaut des paiements de remise, +Unrealized Profit / Loss Account,Compte de perte, +Enable Provisional Accounting For Non Stock Items,Activer la provision pour les articles non stockés, +Publish in Website,Publier sur le Site Web, +List View,Vue en liste, +Allow Excess Material Transfer,Autoriser les transfert de stock supérieurs à l'attendue, +Allow transferring raw materials even after the Required Quantity is fulfilled,Autoriser les transfert de matiéres premiére mais si la quantité requise est atteinte, +Add Corrective Operation Cost in Finished Good Valuation,Ajouter des opérations de correction de coût pour la valorisation des produits finis, +Make Serial No / Batch from Work Order,Générer des numéros de séries / lots depuis les Ordres de Fabrications, +System will automatically create the serial numbers / batch for the Finished Good on submission of work order,le systéme va créer des numéros de séries / lots à la validation des produit finis depuis les Ordres de Fabrications, +Allow material consumptions without immediately manufacturing finished goods against a Work Order,Autoriser la consommation sans immédiatement fabriqué les produit fini dans les ordres de fabrication, +Quality Inspection Parameter,Paramétre des Inspection Qualite, +Parameter Group,Groupe de paramétre, +E Commerce Settings,Paramétrage E-Commerce, +Follow these steps to create a landing page for your store:,Suivez les intructions suivantes pour créer votre page d'accueil de boutique en ligne, +Show Price in Quotation,Afficher les prix sur les devis, +Add-ons,Extensions, +Enable Wishlist,Activer la liste de souhaits, +Enable Reviews and Ratings,Activer les avis et notes, +Enable Recommendations,Activer les recommendations, +Item Search Settings,Paramétrage de la recherche d'article, +Purchase demande,Demande de materiel, +Internal Customer,Client interne +Internal Supplier,Fournisseur interne +Contact & Address,Contact et Adresse +Primary Address and Contact,Adresse et contact principal +Supplier Primary Contact,Contact fournisseur principal +Supplier Primary Address,Adresse fournisseur principal +From Opportunity,Depuis l'opportunité +Default Receivable Accounts,Compte de débit par défaut +Receivable Accounts,Compte de débit +Mention if a non-standard receivable account,Veuillez mentionner s'il s'agit d'un compte débiteur non standard +Allow Purchase,Autoriser à l'achat +Inventory Settings,Paramétrage de l'inventaire From 256c3c81a473b21e6fd44c75fe61e7896e119876 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Sun, 27 Aug 2023 08:29:53 +0530 Subject: [PATCH 242/501] test: Exchange Rate Revaluation functions and its impact on ledger (cherry picked from commit d40504b9731bbeaf153e87b0d8d765a17fa3c43e) --- .../test_exchange_rate_revaluation.py | 294 +++++++++++++++++- 1 file changed, 292 insertions(+), 2 deletions(-) diff --git a/erpnext/accounts/doctype/exchange_rate_revaluation/test_exchange_rate_revaluation.py b/erpnext/accounts/doctype/exchange_rate_revaluation/test_exchange_rate_revaluation.py index ec55e60fd1f4..ced04ced3fdd 100644 --- a/erpnext/accounts/doctype/exchange_rate_revaluation/test_exchange_rate_revaluation.py +++ b/erpnext/accounts/doctype/exchange_rate_revaluation/test_exchange_rate_revaluation.py @@ -3,6 +3,296 @@ import unittest +import frappe +from frappe import qb +from frappe.tests.utils import FrappeTestCase, change_settings +from frappe.utils import add_days, flt, today -class TestExchangeRateRevaluation(unittest.TestCase): - pass +from erpnext import get_default_cost_center +from erpnext.accounts.doctype.payment_entry.payment_entry import get_payment_entry +from erpnext.accounts.doctype.payment_entry.test_payment_entry import create_payment_entry +from erpnext.accounts.doctype.purchase_invoice.test_purchase_invoice import make_purchase_invoice +from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice +from erpnext.accounts.party import get_party_account +from erpnext.accounts.test.accounts_mixin import AccountsTestMixin +from erpnext.stock.doctype.item.test_item import create_item + + +class TestExchangeRateRevaluation(AccountsTestMixin, FrappeTestCase): + def setUp(self): + self.create_company() + self.create_usd_receivable_account() + self.create_item() + self.create_customer() + self.clear_old_entries() + self.set_system_and_company_settings() + + def tearDown(self): + frappe.db.rollback() + + def set_system_and_company_settings(self): + # set number and currency precision + system_settings = frappe.get_doc("System Settings") + system_settings.float_precision = 2 + system_settings.currency_precision = 2 + system_settings.save() + + # Using Exchange Gain/Loss account for unrealized as well. + company_doc = frappe.get_doc("Company", self.company) + company_doc.unrealized_exchange_gain_loss_account = company_doc.exchange_gain_loss_account + company_doc.save() + + @change_settings( + "Accounts Settings", + {"allow_multi_currency_invoices_against_single_party_account": 1, "allow_stale": 0}, + ) + def test_01_revaluation_of_forex_balance(self): + """ + Test Forex account balance and Journal creation post Revaluation + """ + si = create_sales_invoice( + item=self.item, + company=self.company, + customer=self.customer, + debit_to=self.debtors_usd, + posting_date=today(), + parent_cost_center=self.cost_center, + cost_center=self.cost_center, + rate=100, + price_list_rate=100, + do_not_submit=1, + ) + si.currency = "USD" + si.conversion_rate = 80 + si.save().submit() + + err = frappe.new_doc("Exchange Rate Revaluation") + err.company = (self.company,) + err.posting_date = today() + accounts = err.get_accounts_data() + err.extend("accounts", accounts) + row = err.accounts[0] + row.new_exchange_rate = 85 + 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() + + # Create JV for ERR + err_journals = err.make_jv_entries() + je = frappe.get_doc("Journal Entry", err_journals.get("revaluation_jv")) + je = je.submit() + + je.reload() + self.assertEqual(je.voucher_type, "Exchange Rate Revaluation") + self.assertEqual(je.total_debit, 8500.0) + self.assertEqual(je.total_credit, 8500.0) + + acc_balance = frappe.db.get_all( + "GL Entry", + filters={"account": self.debtors_usd, "is_cancelled": 0}, + fields=["sum(debit)-sum(credit) as balance"], + )[0] + self.assertEqual(acc_balance.balance, 8500.0) + + @change_settings( + "Accounts Settings", + {"allow_multi_currency_invoices_against_single_party_account": 1, "allow_stale": 0}, + ) + def test_02_accounts_only_with_base_currency_balance(self): + """ + Test Revaluation on Forex account with balance only in base currency + """ + si = create_sales_invoice( + item=self.item, + company=self.company, + customer=self.customer, + debit_to=self.debtors_usd, + posting_date=today(), + parent_cost_center=self.cost_center, + cost_center=self.cost_center, + rate=100, + price_list_rate=100, + do_not_submit=1, + ) + si.currency = "USD" + si.conversion_rate = 80 + si.save().submit() + + pe = get_payment_entry(si.doctype, si.name) + pe.source_exchange_rate = 85 + pe.received_amount = 8500 + pe.save().submit() + + # Cancel the auto created gain/loss JE to simulate balance only in base currency + je = frappe.db.get_all( + "Journal Entry Account", filters={"reference_name": si.name}, pluck="parent" + )[0] + frappe.get_doc("Journal Entry", je).cancel() + + err = frappe.new_doc("Exchange Rate Revaluation") + err.company = (self.company,) + err.posting_date = today() + err.fetch_and_calculate_accounts_data() + err = err.save().submit() + + # Create JV for ERR + self.assertTrue(err.check_journal_entry_condition()) + err_journals = err.make_jv_entries() + je = frappe.get_doc("Journal Entry", err_journals.get("zero_balance_jv")) + je = je.submit() + + je.reload() + self.assertEqual(je.voucher_type, "Exchange Gain Or Loss") + self.assertEqual(len(je.accounts), 2) + # Only base currency fields will be posted to + for acc in je.accounts: + self.assertEqual(acc.debit_in_account_currency, 0) + self.assertEqual(acc.credit_in_account_currency, 0) + + self.assertEqual(je.total_debit, 500.0) + self.assertEqual(je.total_credit, 500.0) + + acc_balance = frappe.db.get_all( + "GL Entry", + filters={"account": self.debtors_usd, "is_cancelled": 0}, + fields=[ + "sum(debit)-sum(credit) as balance", + "sum(debit_in_account_currency)-sum(credit_in_account_currency) as balance_in_account_currency", + ], + )[0] + # account shouldn't have balance in base and account currency + self.assertEqual(acc_balance.balance, 0.0) + self.assertEqual(acc_balance.balance_in_account_currency, 0.0) + + @change_settings( + "Accounts Settings", + {"allow_multi_currency_invoices_against_single_party_account": 1, "allow_stale": 0}, + ) + def test_03_accounts_only_with_account_currency_balance(self): + """ + Test Revaluation on Forex account with balance only in account currency + """ + precision = frappe.db.get_single_value("System Settings", "currency_precision") + + # posting on previous date to make sure that ERR picks up the Payment entry's exchange + # rate while calculating gain/loss for account currency balance + si = create_sales_invoice( + item=self.item, + company=self.company, + customer=self.customer, + debit_to=self.debtors_usd, + posting_date=add_days(today(), -1), + parent_cost_center=self.cost_center, + cost_center=self.cost_center, + rate=100, + price_list_rate=100, + do_not_submit=1, + ) + si.currency = "USD" + si.conversion_rate = 80 + si.save().submit() + + pe = get_payment_entry(si.doctype, si.name) + pe.paid_amount = 95 + pe.source_exchange_rate = 84.211 + pe.received_amount = 8000 + pe.references = [] + pe.save().submit() + + acc_balance = frappe.db.get_all( + "GL Entry", + filters={"account": self.debtors_usd, "is_cancelled": 0}, + fields=[ + "sum(debit)-sum(credit) as balance", + "sum(debit_in_account_currency)-sum(credit_in_account_currency) as balance_in_account_currency", + ], + )[0] + # account should have balance only in account currency + self.assertEqual(flt(acc_balance.balance, precision), 0.0) + self.assertEqual(flt(acc_balance.balance_in_account_currency, precision), 5.0) # in USD + + err = frappe.new_doc("Exchange Rate Revaluation") + err.company = (self.company,) + err.posting_date = today() + err.fetch_and_calculate_accounts_data() + err.set_total_gain_loss() + err = err.save().submit() + + # Create JV for ERR + self.assertTrue(err.check_journal_entry_condition()) + err_journals = err.make_jv_entries() + je = frappe.get_doc("Journal Entry", err_journals.get("zero_balance_jv")) + je = je.submit() + + je.reload() + self.assertEqual(je.voucher_type, "Exchange Gain Or Loss") + self.assertEqual(len(je.accounts), 2) + # Only account currency fields will be posted to + for acc in je.accounts: + self.assertEqual(flt(acc.debit, precision), 0.0) + self.assertEqual(flt(acc.credit, precision), 0.0) + + row = [x for x in je.accounts if x.account == self.debtors_usd][0] + self.assertEqual(flt(row.credit_in_account_currency, precision), 5.0) # in USD + row = [x for x in je.accounts if x.account != self.debtors_usd][0] + self.assertEqual(flt(row.debit_in_account_currency, precision), 421.06) # in INR + + # total_debit and total_credit will be 0.0, as JV is posting only to account currency fields + self.assertEqual(flt(je.total_debit, precision), 0.0) + self.assertEqual(flt(je.total_credit, precision), 0.0) + + acc_balance = frappe.db.get_all( + "GL Entry", + filters={"account": self.debtors_usd, "is_cancelled": 0}, + fields=[ + "sum(debit)-sum(credit) as balance", + "sum(debit_in_account_currency)-sum(credit_in_account_currency) as balance_in_account_currency", + ], + )[0] + # account shouldn't have balance in base and account currency post revaluation + self.assertEqual(flt(acc_balance.balance, precision), 0.0) + self.assertEqual(flt(acc_balance.balance_in_account_currency, precision), 0.0) + + @change_settings( + "Accounts Settings", + {"allow_multi_currency_invoices_against_single_party_account": 1, "allow_stale": 0}, + ) + def test_04_get_account_details_function(self): + si = create_sales_invoice( + item=self.item, + company=self.company, + customer=self.customer, + debit_to=self.debtors_usd, + posting_date=today(), + parent_cost_center=self.cost_center, + cost_center=self.cost_center, + rate=100, + price_list_rate=100, + do_not_submit=1, + ) + si.currency = "USD" + si.conversion_rate = 80 + si.save().submit() + + from erpnext.accounts.doctype.exchange_rate_revaluation.exchange_rate_revaluation import ( + get_account_details, + ) + + account_details = get_account_details( + self.company, si.posting_date, self.debtors_usd, "Customer", self.customer, 0.05 + ) + # not checking for new exchange rate and balances as it is dependent on live exchange rates + expected_data = { + "account_currency": "USD", + "balance_in_base_currency": 8000.0, + "balance_in_account_currency": 100.0, + "current_exchange_rate": 80.0, + "zero_balance": False, + "new_balance_in_account_currency": 100.0, + } + + for key, val in expected_data.items(): + self.assertEqual(expected_data.get(key), account_details.get(key)) From bd41cb221b9cfb231271f07f0977cb09771c5454 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Mon, 28 Aug 2023 11:53:11 +0530 Subject: [PATCH 243/501] fix: Asset Category filter is not working in asset depreciation(#36806) fix: Asset Category filter is not working in asset depreciation fix: Asset Category filter is not working in asset depreciation and balances Co-authored-by: ubuntu (cherry picked from commit 388a42ec7ee0e6a2a36b81c6f3c84b07deb8b12c) Co-authored-by: ViralKansodiya <141210323+viralkansodiya@users.noreply.github.com> --- .../asset_depreciations_and_balances.py | 27 ++++++++++++++----- 1 file changed, 21 insertions(+), 6 deletions(-) diff --git a/erpnext/accounts/report/asset_depreciations_and_balances/asset_depreciations_and_balances.py b/erpnext/accounts/report/asset_depreciations_and_balances/asset_depreciations_and_balances.py index d67eee3552da..bdc8d8504f87 100644 --- a/erpnext/accounts/report/asset_depreciations_and_balances/asset_depreciations_and_balances.py +++ b/erpnext/accounts/report/asset_depreciations_and_balances/asset_depreciations_and_balances.py @@ -58,6 +58,9 @@ def get_data(filters): def get_asset_categories(filters): + condition = "" + if filters.get("asset_category"): + condition += " and asset_category = %(asset_category)s" return frappe.db.sql( """ SELECT asset_category, @@ -98,15 +101,25 @@ def get_asset_categories(filters): 0 end), 0) as cost_of_scrapped_asset from `tabAsset` - where docstatus=1 and company=%(company)s and purchase_date <= %(to_date)s + where docstatus=1 and company=%(company)s and purchase_date <= %(to_date)s {} group by asset_category - """, - {"to_date": filters.to_date, "from_date": filters.from_date, "company": filters.company}, + """.format( + condition + ), + { + "to_date": filters.to_date, + "from_date": filters.from_date, + "company": filters.company, + "asset_category": filters.get("asset_category"), + }, as_dict=1, ) def get_assets(filters): + condition = "" + if filters.get("asset_category"): + condition = " and a.asset_category = '{}'".format(filters.get("asset_category")) return frappe.db.sql( """ SELECT results.asset_category, @@ -138,7 +151,7 @@ def get_assets(filters): aca.parent = a.asset_category and aca.company_name = %(company)s join `tabCompany` company on company.name = %(company)s - where a.docstatus=1 and a.company=%(company)s and a.purchase_date <= %(to_date)s and gle.debit != 0 and gle.is_cancelled = 0 and gle.account = ifnull(aca.depreciation_expense_account, company.depreciation_expense_account) + where a.docstatus=1 and a.company=%(company)s and a.purchase_date <= %(to_date)s and gle.debit != 0 and gle.is_cancelled = 0 and gle.account = ifnull(aca.depreciation_expense_account, company.depreciation_expense_account) {0} group by a.asset_category union SELECT a.asset_category, @@ -154,10 +167,12 @@ def get_assets(filters): end), 0) as depreciation_eliminated_during_the_period, 0 as depreciation_amount_during_the_period from `tabAsset` a - where a.docstatus=1 and a.company=%(company)s and a.purchase_date <= %(to_date)s + where a.docstatus=1 and a.company=%(company)s and a.purchase_date <= %(to_date)s {0} group by a.asset_category) as results group by results.asset_category - """, + """.format( + condition + ), {"to_date": filters.to_date, "from_date": filters.from_date, "company": filters.company}, as_dict=1, ) From adc87f16a31d9d35505168e97cc0967f89267d77 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Mon, 28 Aug 2023 13:59:00 +0530 Subject: [PATCH 244/501] fix: fetch rounded total while pulling reference details on SO (cherry picked from commit 714b8289c1cf23d9a0039e94f305d926a82a28a0) --- erpnext/accounts/doctype/payment_entry/payment_entry.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/erpnext/accounts/doctype/payment_entry/payment_entry.py b/erpnext/accounts/doctype/payment_entry/payment_entry.py index 127768071a06..765e69b75bd0 100644 --- a/erpnext/accounts/doctype/payment_entry/payment_entry.py +++ b/erpnext/accounts/doctype/payment_entry/payment_entry.py @@ -1867,10 +1867,15 @@ def get_reference_details(reference_doctype, reference_name, party_account_curre if not total_amount: if party_account_currency == company_currency: # for handling cases that don't have multi-currency (base field) - total_amount = ref_doc.get("base_grand_total") or ref_doc.get("grand_total") + total_amount = ( + ref_doc.get("base_rounded_total") + or ref_doc.get("rounded_total") + or ref_doc.get("base_grand_total") + or ref_doc.get("grand_total") + ) exchange_rate = 1 else: - total_amount = ref_doc.get("grand_total") + total_amount = ref_doc.get("rounded_total") or ref_doc.get("grand_total") if not exchange_rate: # Get the exchange rate from the original ref doc # or get it based on the posting date of the ref doc. From 0350c6985608cff38a6a7e7cb0b5329f6601fc6d Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Mon, 28 Aug 2023 14:21:21 +0530 Subject: [PATCH 245/501] test: allocation err misfire on Sales Order (cherry picked from commit 67a0969b782deb1847ea75e2f565df824d6df36e) --- .../doctype/payment_entry/test_payment_entry.py | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/erpnext/accounts/doctype/payment_entry/test_payment_entry.py b/erpnext/accounts/doctype/payment_entry/test_payment_entry.py index 21379458874f..6b897acc27d6 100644 --- a/erpnext/accounts/doctype/payment_entry/test_payment_entry.py +++ b/erpnext/accounts/doctype/payment_entry/test_payment_entry.py @@ -1201,6 +1201,22 @@ def test_overallocation_validation_shouldnt_misfire(self): template.allocate_payment_based_on_payment_terms = 1 template.save() + def test_allocation_validation_for_sales_order(self): + so = make_sales_order(do_not_save=True) + so.items[0].rate = 99.55 + so.save().submit() + pe = get_payment_entry("Sales Order", so.name, bank_account="_Test Cash - _TC") + pe.paid_from = "Debtors - _TC" + pe.paid_amount = 45.55 + pe.references[0].allocated_amount = 45.55 + pe.save().submit() + pe = get_payment_entry("Sales Order", so.name, bank_account="_Test Cash - _TC") + pe.paid_from = "Debtors - _TC" + pe.save().submit() + + so.reload() + self.assertEqual(so.advance_paid, so.rounded_total) + def create_payment_entry(**args): payment_entry = frappe.new_doc("Payment Entry") From 05f657e690bc94856a6c22bd5ac35262c3cabf00 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Mon, 28 Aug 2023 14:30:37 +0530 Subject: [PATCH 246/501] test: assert rounded amount is calculated (cherry picked from commit 2fdbe82835625b578b052ac09a5a4f531e2beab5) --- erpnext/accounts/doctype/payment_entry/test_payment_entry.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/erpnext/accounts/doctype/payment_entry/test_payment_entry.py b/erpnext/accounts/doctype/payment_entry/test_payment_entry.py index 6b897acc27d6..2de009f8c438 100644 --- a/erpnext/accounts/doctype/payment_entry/test_payment_entry.py +++ b/erpnext/accounts/doctype/payment_entry/test_payment_entry.py @@ -1205,6 +1205,7 @@ def test_allocation_validation_for_sales_order(self): so = make_sales_order(do_not_save=True) so.items[0].rate = 99.55 so.save().submit() + self.assertGreater(so.rounded_total, 0.0) pe = get_payment_entry("Sales Order", so.name, bank_account="_Test Cash - _TC") pe.paid_from = "Debtors - _TC" pe.paid_amount = 45.55 @@ -1212,6 +1213,7 @@ def test_allocation_validation_for_sales_order(self): pe.save().submit() pe = get_payment_entry("Sales Order", so.name, bank_account="_Test Cash - _TC") pe.paid_from = "Debtors - _TC" + # No validation error should be thrown here. pe.save().submit() so.reload() From 9789b7bdefd453ac647f0cb760b681c2f6d5a76e Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Mon, 28 Aug 2023 18:17:50 +0530 Subject: [PATCH 247/501] fix: error in report when data is not available to load chart in report (backport #36842) (#36853) fix: error in report when data is not available to load chart in report (#36842) (cherry picked from commit 3a2933db4d12766d3183893ba2edc9cb5faca527) Co-authored-by: ViralKansodiya <141210323+viralkansodiya@users.noreply.github.com> --- .../assets/report/fixed_asset_register/fixed_asset_register.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/erpnext/assets/report/fixed_asset_register/fixed_asset_register.py b/erpnext/assets/report/fixed_asset_register/fixed_asset_register.py index bf62a8fb39c7..383be973477d 100644 --- a/erpnext/assets/report/fixed_asset_register/fixed_asset_register.py +++ b/erpnext/assets/report/fixed_asset_register/fixed_asset_register.py @@ -156,6 +156,8 @@ def get_data(filters): def prepare_chart_data(data, filters): + if not data: + return labels_values_map = {} if filters.filter_based_on not in ("Date Range", "Fiscal Year"): filters_filter_based_on = "Date Range" From d2091cc22c6bdc1b83b67d82c0b305f1654ae6d6 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Tue, 29 Aug 2023 11:01:40 +0530 Subject: [PATCH 248/501] fix: create entries for only PR items present in LCV (#36852) fix: create entries for only PR items present in LCV (#36852) * fix: check if item code exists in lcv before creating gle * refactor: use qb to fetch lcv items (cherry picked from commit 26e8b8f95920cdc5b8d541a394a3389c0480a8fa) Co-authored-by: Gursheen Kaur Anand <40693548+GursheenK@users.noreply.github.com> --- .../purchase_invoice/purchase_invoice.py | 29 ++++++------- .../landed_cost_voucher.py | 42 +++++++++++++------ .../purchase_receipt/purchase_receipt.py | 41 +++++++++--------- 3 files changed, 65 insertions(+), 47 deletions(-) diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py index 6161e5b36ee0..2340f486f145 100644 --- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py +++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py @@ -768,21 +768,22 @@ def make_item_gl_entries(self, gl_entries): # Amount added through landed-cost-voucher if landed_cost_entries: - for account, amount in landed_cost_entries[(item.item_code, item.name)].items(): - gl_entries.append( - self.get_gl_dict( - { - "account": account, - "against": item.expense_account, - "cost_center": item.cost_center, - "remarks": self.get("remarks") or _("Accounting Entry for Stock"), - "credit": flt(amount["base_amount"]), - "credit_in_account_currency": flt(amount["amount"]), - "project": item.project or self.project, - }, - item=item, + if (item.item_code, item.name) in landed_cost_entries: + for account, amount in landed_cost_entries[(item.item_code, item.name)].items(): + gl_entries.append( + self.get_gl_dict( + { + "account": account, + "against": item.expense_account, + "cost_center": item.cost_center, + "remarks": self.get("remarks") or _("Accounting Entry for Stock"), + "credit": flt(amount["base_amount"]), + "credit_in_account_currency": flt(amount["amount"]), + "project": item.project or self.project, + }, + item=item, + ) ) - ) # sub-contracting warehouse if flt(item.rm_supp_cost): diff --git a/erpnext/stock/doctype/landed_cost_voucher/landed_cost_voucher.py b/erpnext/stock/doctype/landed_cost_voucher/landed_cost_voucher.py index 111a0861b719..7f0dc2df9f32 100644 --- a/erpnext/stock/doctype/landed_cost_voucher/landed_cost_voucher.py +++ b/erpnext/stock/doctype/landed_cost_voucher/landed_cost_voucher.py @@ -6,6 +6,7 @@ from frappe import _ from frappe.model.document import Document from frappe.model.meta import get_field_precision +from frappe.query_builder.custom import ConstantColumn from frappe.utils import flt import erpnext @@ -19,19 +20,7 @@ def get_items_from_purchase_receipts(self): self.set("items", []) for pr in self.get("purchase_receipts"): if pr.receipt_document_type and pr.receipt_document: - pr_items = frappe.db.sql( - """select pr_item.item_code, pr_item.description, - pr_item.qty, pr_item.base_rate, pr_item.base_amount, pr_item.name, - pr_item.cost_center, pr_item.is_fixed_asset - from `tab{doctype} Item` pr_item where parent = %s - and exists(select name from tabItem - where name = pr_item.item_code and (is_stock_item = 1 or is_fixed_asset=1)) - """.format( - doctype=pr.receipt_document_type - ), - pr.receipt_document, - as_dict=True, - ) + pr_items = get_pr_items(pr) for d in pr_items: item = self.append("items") @@ -247,3 +236,30 @@ def update_rate_in_serial_no_for_non_asset_items(self, receipt_document): ), tuple([item.valuation_rate] + serial_nos), ) + + +def get_pr_items(purchase_receipt): + item = frappe.qb.DocType("Item") + pr_item = frappe.qb.DocType(purchase_receipt.receipt_document_type + " Item") + return ( + frappe.qb.from_(pr_item) + .inner_join(item) + .on(item.name == pr_item.item_code) + .select( + pr_item.item_code, + pr_item.description, + pr_item.qty, + pr_item.base_rate, + pr_item.base_amount, + pr_item.name, + pr_item.cost_center, + pr_item.is_fixed_asset, + ConstantColumn(purchase_receipt.receipt_document_type).as_("receipt_document_type"), + ConstantColumn(purchase_receipt.receipt_document).as_("receipt_document"), + ) + .where( + (pr_item.parent == purchase_receipt.receipt_document) + & ((item.is_stock_item == 1) | (item.is_fixed_asset == 1)) + ) + .run(as_dict=True) + ) diff --git a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py index c793529e843c..4f6c6364a472 100644 --- a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py +++ b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py @@ -472,27 +472,28 @@ def make_item_gl_entries(self, gl_entries, warehouse_account=None): # Amount added through landed-cos-voucher if d.landed_cost_voucher_amount and landed_cost_entries: - for account, amount in landed_cost_entries[(d.item_code, d.name)].items(): - account_currency = get_account_currency(account) - credit_amount = ( - flt(amount["base_amount"]) - if (amount["base_amount"] or account_currency != self.company_currency) - else flt(amount["amount"]) - ) + if (d.item_code, d.name) in landed_cost_entries: + for account, amount in landed_cost_entries[(d.item_code, d.name)].items(): + account_currency = get_account_currency(account) + credit_amount = ( + flt(amount["base_amount"]) + if (amount["base_amount"] or account_currency != self.company_currency) + else flt(amount["amount"]) + ) - self.add_gl_entry( - gl_entries=gl_entries, - account=account, - cost_center=d.cost_center, - debit=0.0, - credit=credit_amount, - remarks=remarks, - against_account=warehouse_account_name, - credit_in_account_currency=flt(amount["amount"]), - account_currency=account_currency, - project=d.project, - item=d, - ) + self.add_gl_entry( + gl_entries=gl_entries, + account=account, + cost_center=d.cost_center, + debit=0.0, + credit=credit_amount, + remarks=remarks, + against_account=warehouse_account_name, + credit_in_account_currency=flt(amount["amount"]), + account_currency=account_currency, + project=d.project, + item=d, + ) if d.rate_difference_with_purchase_invoice and stock_rbnb: account_currency = get_account_currency(stock_rbnb) From 22247cfa179c178ba8d6bbd76dd566f6440ee5fa Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Tue, 29 Aug 2023 16:57:59 +0530 Subject: [PATCH 249/501] fix: added valuation field type (Float/Currency) in the filter (backport #36866) (#36868) fix: added valuation field type (Float/Currency) in the filter (#36866) (cherry picked from commit dea802dc4134f7ed1f85ff337ddcb2d521087644) Co-authored-by: rohitwaghchaure --- erpnext/stock/report/stock_balance/stock_balance.js | 8 ++++++++ erpnext/stock/report/stock_balance/stock_balance.py | 5 ++++- erpnext/stock/report/stock_ledger/stock_ledger.js | 10 +++++++++- erpnext/stock/report/stock_ledger/stock_ledger.py | 12 ++++++++---- 4 files changed, 29 insertions(+), 6 deletions(-) diff --git a/erpnext/stock/report/stock_balance/stock_balance.js b/erpnext/stock/report/stock_balance/stock_balance.js index 33ed955a5c48..6de5f00ece84 100644 --- a/erpnext/stock/report/stock_balance/stock_balance.js +++ b/erpnext/stock/report/stock_balance/stock_balance.js @@ -71,6 +71,14 @@ frappe.query_reports["Stock Balance"] = { "width": "80", "options": "Warehouse Type" }, + { + "fieldname": "valuation_field_type", + "label": __("Valuation Field Type"), + "fieldtype": "Select", + "width": "80", + "options": "Currency\nFloat", + "default": "Currency" + }, { "fieldname":"include_uom", "label": __("Include UOM"), diff --git a/erpnext/stock/report/stock_balance/stock_balance.py b/erpnext/stock/report/stock_balance/stock_balance.py index d362d818ebf9..7e81a72028f4 100644 --- a/erpnext/stock/report/stock_balance/stock_balance.py +++ b/erpnext/stock/report/stock_balance/stock_balance.py @@ -430,9 +430,12 @@ def get_columns(self): { "label": _("Valuation Rate"), "fieldname": "val_rate", - "fieldtype": "Float", + "fieldtype": self.filters.valuation_field_type or "Currency", "width": 90, "convertible": "rate", + "options": "Company:company:default_currency" + if self.filters.valuation_field_type == "Currency" + else None, }, { "label": _("Company"), diff --git a/erpnext/stock/report/stock_ledger/stock_ledger.js b/erpnext/stock/report/stock_ledger/stock_ledger.js index 0def161d2833..b00b422a67a3 100644 --- a/erpnext/stock/report/stock_ledger/stock_ledger.js +++ b/erpnext/stock/report/stock_ledger/stock_ledger.js @@ -82,7 +82,15 @@ frappe.query_reports["Stock Ledger"] = { "label": __("Include UOM"), "fieldtype": "Link", "options": "UOM" - } + }, + { + "fieldname": "valuation_field_type", + "label": __("Valuation Field Type"), + "fieldtype": "Select", + "width": "80", + "options": "Currency\nFloat", + "default": "Currency" + }, ], "formatter": function (value, row, column, data, default_formatter) { value = default_formatter(value, row, column, data); diff --git a/erpnext/stock/report/stock_ledger/stock_ledger.py b/erpnext/stock/report/stock_ledger/stock_ledger.py index ed28ed3ee46f..eeef39641b01 100644 --- a/erpnext/stock/report/stock_ledger/stock_ledger.py +++ b/erpnext/stock/report/stock_ledger/stock_ledger.py @@ -196,17 +196,21 @@ def get_columns(filters): { "label": _("Avg Rate (Balance Stock)"), "fieldname": "valuation_rate", - "fieldtype": "Float", + "fieldtype": filters.valuation_field_type, "width": 180, - "options": "Company:company:default_currency", + "options": "Company:company:default_currency" + if filters.valuation_field_type == "Currency" + else None, "convertible": "rate", }, { "label": _("Valuation Rate"), "fieldname": "in_out_rate", - "fieldtype": "Float", + "fieldtype": filters.valuation_field_type, "width": 140, - "options": "Company:company:default_currency", + "options": "Company:company:default_currency" + if filters.valuation_field_type == "Currency" + else None, "convertible": "rate", }, { From b95ab3425a2f61caffab3bcbbb09e0b91b8ff1ba Mon Sep 17 00:00:00 2001 From: Frappe PR Bot Date: Wed, 30 Aug 2023 13:56:17 +0000 Subject: [PATCH 250/501] chore(release): Bumped to Version 14.37.0 # [14.37.0](https://github.com/frappe/erpnext/compare/v14.36.0...v14.37.0) (2023-08-30) ### Bug Fixes * added valuation field type (Float/Currency) in the filter (backport [#36866](https://github.com/frappe/erpnext/issues/36866)) ([#36868](https://github.com/frappe/erpnext/issues/36868)) ([22247cf](https://github.com/frappe/erpnext/commit/22247cfa179c178ba8d6bbd76dd566f6440ee5fa)) * Allow to make return against sales invoice which has closed sales order ([0f98cc8](https://github.com/frappe/erpnext/commit/0f98cc85e9dce00fa333474cfb438f243503df81)) * Asset Category filter is not working in asset depreciation([#36806](https://github.com/frappe/erpnext/issues/36806)) ([bd41cb2](https://github.com/frappe/erpnext/commit/bd41cb221b9cfb231271f07f0977cb09771c5454)) * create entries for only PR items present in LCV ([#36852](https://github.com/frappe/erpnext/issues/36852)) ([d2091cc](https://github.com/frappe/erpnext/commit/d2091cc22c6bdc1b83b67d82c0b305f1654ae6d6)) * error in report when data is not available to load chart in report (backport [#36842](https://github.com/frappe/erpnext/issues/36842)) ([#36853](https://github.com/frappe/erpnext/issues/36853)) ([9789b7b](https://github.com/frappe/erpnext/commit/9789b7bdefd453ac647f0cb760b681c2f6d5a76e)) * error listindexoutofrange when save a production plan ([#36807](https://github.com/frappe/erpnext/issues/36807)) ([fd41594](https://github.com/frappe/erpnext/commit/fd4159423d7e127499bdde04184f9e11f5fa9e78)) * fetch JVs in tax withholding report without party values ([bc6bd81](https://github.com/frappe/erpnext/commit/bc6bd81f87921c61cc855379eb2a915dadb2ff2b)) * fetch rounded total while pulling reference details on SO ([adc87f1](https://github.com/frappe/erpnext/commit/adc87f16a31d9d35505168e97cc0967f89267d77)) * missing company flag for regional fn ([#36791](https://github.com/frappe/erpnext/issues/36791)) ([c07548a](https://github.com/frappe/erpnext/commit/c07548a61235f371b93a7afde9ab7e25c6dc03d0)) * SCR return status (backport [#36793](https://github.com/frappe/erpnext/issues/36793)) ([#36796](https://github.com/frappe/erpnext/issues/36796)) ([6edfcf4](https://github.com/frappe/erpnext/commit/6edfcf4de82156be6e2ef2204e264e59a2d3b6eb)) * Tax withholding reversal on Debit Notes ([e8dc63c](https://github.com/frappe/erpnext/commit/e8dc63c89c01f6c8617a911ac87f56fc5768da12)) ### Features * **MR:** Project and Cost Center in Connections (backport [#36794](https://github.com/frappe/erpnext/issues/36794)) ([#36795](https://github.com/frappe/erpnext/issues/36795)) ([4fa0777](https://github.com/frappe/erpnext/commit/4fa07777e98ab9a4848f235d7a5e80b88a538b80)) --- erpnext/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/__init__.py b/erpnext/__init__.py index 22bf6f806938..6248a1b510af 100644 --- a/erpnext/__init__.py +++ b/erpnext/__init__.py @@ -3,7 +3,7 @@ import frappe -__version__ = "14.36.0" +__version__ = "14.37.0" def get_default_company(user=None): From 420536ca522df9bfd99d7e6298a5b719cee40d09 Mon Sep 17 00:00:00 2001 From: "Nihantra C. Patel" <141945075+Nihantra-Patel@users.noreply.github.com> Date: Thu, 31 Aug 2023 13:37:30 +0530 Subject: [PATCH 251/501] fix: Set the default filter in All Trends Report --- erpnext/public/js/sales_trends_filters.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/public/js/sales_trends_filters.js b/erpnext/public/js/sales_trends_filters.js index b9c4dca91309..9a70a3da4c61 100644 --- a/erpnext/public/js/sales_trends_filters.js +++ b/erpnext/public/js/sales_trends_filters.js @@ -48,7 +48,7 @@ erpnext.get_sales_trends_filters = function() { "label": __("Fiscal Year"), "fieldtype": "Link", "options":'Fiscal Year', - "default": frappe.sys_defaults.fiscal_year + "default": erpnext.utils.get_fiscal_year(frappe.datetime.get_today()) }, { "fieldname":"company", From 132957f59e096f1ac049f6c448cbeb20fe33acd6 Mon Sep 17 00:00:00 2001 From: "Nihantra C. Patel" <141945075+Nihantra-Patel@users.noreply.github.com> Date: Thu, 31 Aug 2023 13:38:42 +0530 Subject: [PATCH 252/501] fix: Set the default filter in All Trends Report --- erpnext/public/js/purchase_trends_filters.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/public/js/purchase_trends_filters.js b/erpnext/public/js/purchase_trends_filters.js index c786a8674e6b..77f1d2b496ae 100644 --- a/erpnext/public/js/purchase_trends_filters.js +++ b/erpnext/public/js/purchase_trends_filters.js @@ -28,7 +28,7 @@ erpnext.get_purchase_trends_filters = function() { "label": __("Fiscal Year"), "fieldtype": "Link", "options":'Fiscal Year', - "default": frappe.sys_defaults.fiscal_year + "default": erpnext.utils.get_fiscal_year(frappe.datetime.get_today()) }, { "fieldname":"period_based_on", From 46bea0e56f45d81a1f06d78fd77e648888d9a179 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Thu, 31 Aug 2023 16:55:23 +0530 Subject: [PATCH 253/501] fix: calcuate received/paid amount on rate change in PE (cherry picked from commit 64d835374b281fc7406e574b0306ed48656010ca) --- erpnext/accounts/doctype/payment_entry/payment_entry.js | 8 ++++++++ erpnext/accounts/doctype/payment_entry/payment_entry.py | 4 ++-- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/erpnext/accounts/doctype/payment_entry/payment_entry.js b/erpnext/accounts/doctype/payment_entry/payment_entry.js index cd788a896a83..4e3cf35b6186 100644 --- a/erpnext/accounts/doctype/payment_entry/payment_entry.js +++ b/erpnext/accounts/doctype/payment_entry/payment_entry.js @@ -526,12 +526,16 @@ frappe.ui.form.on('Payment Entry', { }, source_exchange_rate: function(frm) { + let company_currency = frappe.get_doc(":Company", frm.doc.company).default_currency; if (frm.doc.paid_amount) { frm.set_value("base_paid_amount", flt(frm.doc.paid_amount) * flt(frm.doc.source_exchange_rate)); // target exchange rate should always be same as source if both account currencies is same if(frm.doc.paid_from_account_currency == frm.doc.paid_to_account_currency) { frm.set_value("target_exchange_rate", frm.doc.source_exchange_rate); frm.set_value("base_received_amount", frm.doc.base_paid_amount); + } else if (company_currency == frm.doc.paid_to_account_currency) { + frm.set_value("received_amount", frm.doc.base_paid_amount); + frm.set_value("base_received_amount", frm.doc.base_paid_amount); } frm.events.set_unallocated_amount(frm); @@ -543,6 +547,7 @@ frappe.ui.form.on('Payment Entry', { target_exchange_rate: function(frm) { frm.set_paid_amount_based_on_received_amount = true; + let company_currency = frappe.get_doc(":Company", frm.doc.company).default_currency; if (frm.doc.received_amount) { frm.set_value("base_received_amount", @@ -552,6 +557,9 @@ frappe.ui.form.on('Payment Entry', { (frm.doc.paid_from_account_currency == frm.doc.paid_to_account_currency)) { frm.set_value("source_exchange_rate", frm.doc.target_exchange_rate); frm.set_value("base_paid_amount", frm.doc.base_received_amount); + } else if (company_currency == frm.doc.paid_from_account_currency) { + frm.set_value("paid_amount", frm.doc.base_received_amount); + frm.set_value("base_paid_amount", frm.doc.base_received_amount); } frm.events.set_unallocated_amount(frm); diff --git a/erpnext/accounts/doctype/payment_entry/payment_entry.py b/erpnext/accounts/doctype/payment_entry/payment_entry.py index 765e69b75bd0..1c6c99ed3e50 100644 --- a/erpnext/accounts/doctype/payment_entry/payment_entry.py +++ b/erpnext/accounts/doctype/payment_entry/payment_entry.py @@ -2167,7 +2167,7 @@ def set_paid_amount_and_received_amount( if bank_amount: received_amount = bank_amount else: - if company_currency != bank.account_currency: + if bank and company_currency != bank.account_currency: received_amount = paid_amount / doc.get("conversion_rate", 1) else: received_amount = paid_amount * doc.get("conversion_rate", 1) @@ -2176,7 +2176,7 @@ def set_paid_amount_and_received_amount( if bank_amount: paid_amount = bank_amount else: - if company_currency != bank.account_currency: + if bank and company_currency != bank.account_currency: paid_amount = received_amount / doc.get("conversion_rate", 1) else: # if party account currency and bank currency is different then populate paid amount as well From 09d9263082cb466d9b1a70dfb22867f2c1f222f2 Mon Sep 17 00:00:00 2001 From: Frappe PR Bot Date: Thu, 31 Aug 2023 14:11:41 +0000 Subject: [PATCH 254/501] chore(release): Bumped to Version 14.37.1 ## [14.37.1](https://github.com/frappe/erpnext/compare/v14.37.0...v14.37.1) (2023-08-31) ### Bug Fixes * calcuate received/paid amount on rate change in PE ([46bea0e](https://github.com/frappe/erpnext/commit/46bea0e56f45d81a1f06d78fd77e648888d9a179)) --- erpnext/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/__init__.py b/erpnext/__init__.py index 6248a1b510af..5d2189e7c11f 100644 --- a/erpnext/__init__.py +++ b/erpnext/__init__.py @@ -3,7 +3,7 @@ import frappe -__version__ = "14.37.0" +__version__ = "14.37.1" def get_default_company(user=None): From 0a632660e065a9a5642a2e624081ba60de72f3b0 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Thu, 31 Aug 2023 16:55:23 +0530 Subject: [PATCH 255/501] fix: calcuate received/paid amount on rate change in PE (cherry picked from commit 64d835374b281fc7406e574b0306ed48656010ca) --- erpnext/accounts/doctype/payment_entry/payment_entry.js | 8 ++++++++ erpnext/accounts/doctype/payment_entry/payment_entry.py | 4 ++-- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/erpnext/accounts/doctype/payment_entry/payment_entry.js b/erpnext/accounts/doctype/payment_entry/payment_entry.js index cd788a896a83..4e3cf35b6186 100644 --- a/erpnext/accounts/doctype/payment_entry/payment_entry.js +++ b/erpnext/accounts/doctype/payment_entry/payment_entry.js @@ -526,12 +526,16 @@ frappe.ui.form.on('Payment Entry', { }, source_exchange_rate: function(frm) { + let company_currency = frappe.get_doc(":Company", frm.doc.company).default_currency; if (frm.doc.paid_amount) { frm.set_value("base_paid_amount", flt(frm.doc.paid_amount) * flt(frm.doc.source_exchange_rate)); // target exchange rate should always be same as source if both account currencies is same if(frm.doc.paid_from_account_currency == frm.doc.paid_to_account_currency) { frm.set_value("target_exchange_rate", frm.doc.source_exchange_rate); frm.set_value("base_received_amount", frm.doc.base_paid_amount); + } else if (company_currency == frm.doc.paid_to_account_currency) { + frm.set_value("received_amount", frm.doc.base_paid_amount); + frm.set_value("base_received_amount", frm.doc.base_paid_amount); } frm.events.set_unallocated_amount(frm); @@ -543,6 +547,7 @@ frappe.ui.form.on('Payment Entry', { target_exchange_rate: function(frm) { frm.set_paid_amount_based_on_received_amount = true; + let company_currency = frappe.get_doc(":Company", frm.doc.company).default_currency; if (frm.doc.received_amount) { frm.set_value("base_received_amount", @@ -552,6 +557,9 @@ frappe.ui.form.on('Payment Entry', { (frm.doc.paid_from_account_currency == frm.doc.paid_to_account_currency)) { frm.set_value("source_exchange_rate", frm.doc.target_exchange_rate); frm.set_value("base_paid_amount", frm.doc.base_received_amount); + } else if (company_currency == frm.doc.paid_from_account_currency) { + frm.set_value("paid_amount", frm.doc.base_received_amount); + frm.set_value("base_paid_amount", frm.doc.base_received_amount); } frm.events.set_unallocated_amount(frm); diff --git a/erpnext/accounts/doctype/payment_entry/payment_entry.py b/erpnext/accounts/doctype/payment_entry/payment_entry.py index 765e69b75bd0..1c6c99ed3e50 100644 --- a/erpnext/accounts/doctype/payment_entry/payment_entry.py +++ b/erpnext/accounts/doctype/payment_entry/payment_entry.py @@ -2167,7 +2167,7 @@ def set_paid_amount_and_received_amount( if bank_amount: received_amount = bank_amount else: - if company_currency != bank.account_currency: + if bank and company_currency != bank.account_currency: received_amount = paid_amount / doc.get("conversion_rate", 1) else: received_amount = paid_amount * doc.get("conversion_rate", 1) @@ -2176,7 +2176,7 @@ def set_paid_amount_and_received_amount( if bank_amount: paid_amount = bank_amount else: - if company_currency != bank.account_currency: + if bank and company_currency != bank.account_currency: paid_amount = received_amount / doc.get("conversion_rate", 1) else: # if party account currency and bank currency is different then populate paid amount as well From a8b58800bb5f411ddcb9a595f42fec47ac9bcd12 Mon Sep 17 00:00:00 2001 From: Gursheen Anand Date: Thu, 31 Aug 2023 17:08:51 +0530 Subject: [PATCH 256/501] fix: fetch discount amount for gle in base currency (cherry picked from commit 112cfe6dfac66fd2b1c57c51ff0dff83eba9274a) --- erpnext/controllers/accounts_controller.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/erpnext/controllers/accounts_controller.py b/erpnext/controllers/accounts_controller.py index 53039cfd8b5c..b225e3d5f1e8 100644 --- a/erpnext/controllers/accounts_controller.py +++ b/erpnext/controllers/accounts_controller.py @@ -1354,7 +1354,7 @@ def make_discount_gl_entries(self, gl_entries): { "account": self.additional_discount_account, "against": supplier_or_customer, - dr_or_cr: self.discount_amount, + dr_or_cr: self.base_discount_amount, "cost_center": self.cost_center, }, item=self, @@ -1626,6 +1626,7 @@ def validate_currency(self): and party_account_currency != self.company_currency and self.currency != party_account_currency ): + frappe.throw( _("Accounting Entry for {0}: {1} can only be made in currency: {2}").format( party_type, party, party_account_currency From 9bc2b419e3ba855d7bc180b9ea6c12253e1a6ee5 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Thu, 31 Aug 2023 21:37:50 +0530 Subject: [PATCH 257/501] fix: difference amount in UI should not be calculated (cherry picked from commit a7e0709ae85734ceb8509cf444f8e300010d7e9c) --- .../doctype/payment_entry/payment_entry.js | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/erpnext/accounts/doctype/payment_entry/payment_entry.js b/erpnext/accounts/doctype/payment_entry/payment_entry.js index 4e3cf35b6186..d43a057db027 100644 --- a/erpnext/accounts/doctype/payment_entry/payment_entry.js +++ b/erpnext/accounts/doctype/payment_entry/payment_entry.js @@ -538,7 +538,9 @@ frappe.ui.form.on('Payment Entry', { frm.set_value("base_received_amount", frm.doc.base_paid_amount); } - frm.events.set_unallocated_amount(frm); + // set_unallocated_amount is called by below method, + // no need trigger separately + frm.events.set_total_allocated_amount(frm); } // Make read only if Accounts Settings doesn't allow stale rates @@ -562,7 +564,9 @@ frappe.ui.form.on('Payment Entry', { frm.set_value("base_paid_amount", frm.doc.base_received_amount); } - frm.events.set_unallocated_amount(frm); + // set_unallocated_amount is called by below method, + // no need trigger separately + frm.events.set_total_allocated_amount(frm); } frm.set_paid_amount_based_on_received_amount = false; @@ -878,12 +882,18 @@ frappe.ui.form.on('Payment Entry', { }, set_total_allocated_amount: function(frm) { + let exchange_rate = 1; + if (frm.doc.payment_type == "Receive") { + exchange_rate = frm.doc.source_exchange_rate; + } else if (frm.doc.payment_type == "Pay") { + exchange_rate = frm.doc.target_exchange_rate; + } var total_allocated_amount = 0.0; var base_total_allocated_amount = 0.0; $.each(frm.doc.references || [], function(i, row) { if (row.allocated_amount) { total_allocated_amount += flt(row.allocated_amount); - base_total_allocated_amount += flt(flt(row.allocated_amount)*flt(row.exchange_rate), + base_total_allocated_amount += flt(flt(row.allocated_amount)*flt(exchange_rate), precision("base_paid_amount")); } }); From 98c26403c1daeb95feacb004d11768da9550a235 Mon Sep 17 00:00:00 2001 From: RitvikSardana <65544983+RitvikSardana@users.noreply.github.com> Date: Sat, 2 Sep 2023 13:08:08 +0530 Subject: [PATCH 258/501] fix: account payable currency and value (#36859) * fix: account payable currency and value * fix: added party_type and party in accounts payable report * chore: code cleanup * fix: customer group test case failure * fix: added test case of the issue * fix: filter toggle for party_type * fix: filter toggle for party_type * chore: fix typo --------- Co-authored-by: ruthra kumar (cherry picked from commit e599f75a51173c3adcd6259eb9729070d8ffc47a) --- .../accounts_payable/accounts_payable.js | 53 ++++++++------- .../accounts_payable/test_accounts_payable.py | 67 +++++++++++++++++++ .../accounts_receivable.js | 10 +-- .../accounts_receivable.py | 23 ++++--- erpnext/accounts/test/accounts_mixin.py | 22 ++++++ 5 files changed, 132 insertions(+), 43 deletions(-) create mode 100644 erpnext/accounts/report/accounts_payable/test_accounts_payable.py diff --git a/erpnext/accounts/report/accounts_payable/accounts_payable.js b/erpnext/accounts/report/accounts_payable/accounts_payable.js index e1a30a4b77e0..27a85701edda 100644 --- a/erpnext/accounts/report/accounts_payable/accounts_payable.js +++ b/erpnext/accounts/report/accounts_payable/accounts_payable.js @@ -37,24 +37,6 @@ frappe.query_reports["Accounts Payable"] = { } } }, - { - "fieldname": "supplier", - "label": __("Supplier"), - "fieldtype": "Link", - "options": "Supplier", - on_change: () => { - var supplier = frappe.query_report.get_filter_value('supplier'); - if (supplier) { - frappe.db.get_value('Supplier', supplier, "tax_id", function(value) { - frappe.query_report.set_filter_value('tax_id', value["tax_id"]); - }); - } else { - frappe.query_report.set_filter_value('tax_id', ""); - } - - frappe.query_report.refresh(); - } - }, { "fieldname": "party_account", "label": __("Payable Account"), @@ -112,11 +94,38 @@ frappe.query_reports["Accounts Payable"] = { "fieldtype": "Link", "options": "Payment Terms Template" }, + { + "fieldname": "party_type", + "label": __("Party Type"), + "fieldtype": "Link", + "options": "Party Type", + get_query: () => { + return { + filters: { + 'account_type': 'Payable' + } + }; + }, + on_change: () => { + frappe.query_report.set_filter_value('party', ""); + let party_type = frappe.query_report.get_filter_value('party_type'); + frappe.query_report.toggle_filter_display('supplier_group', frappe.query_report.get_filter_value('party_type') !== "Supplier"); + + } + + }, + { + "fieldname":"party", + "label": __("Party"), + "fieldtype": "Dynamic Link", + "options": "party_type", + }, { "fieldname": "supplier_group", "label": __("Supplier Group"), "fieldtype": "Link", - "options": "Supplier Group" + "options": "Supplier Group", + "hidden": 1 }, { "fieldname": "group_by_party", @@ -133,12 +142,6 @@ frappe.query_reports["Accounts Payable"] = { "label": __("Show Remarks"), "fieldtype": "Check", }, - { - "fieldname": "tax_id", - "label": __("Tax Id"), - "fieldtype": "Data", - "hidden": 1 - }, { "fieldname": "show_future_payments", "label": __("Show Future Payments"), diff --git a/erpnext/accounts/report/accounts_payable/test_accounts_payable.py b/erpnext/accounts/report/accounts_payable/test_accounts_payable.py new file mode 100644 index 000000000000..cb84cf4fc0a6 --- /dev/null +++ b/erpnext/accounts/report/accounts_payable/test_accounts_payable.py @@ -0,0 +1,67 @@ +import unittest + +import frappe +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.purchase_invoice.test_purchase_invoice import make_purchase_invoice +from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice +from erpnext.accounts.report.accounts_payable.accounts_payable import execute +from erpnext.accounts.test.accounts_mixin import AccountsTestMixin +from erpnext.selling.doctype.sales_order.test_sales_order import make_sales_order + + +class TestAccountsReceivable(AccountsTestMixin, FrappeTestCase): + def setUp(self): + self.create_company() + self.create_customer() + self.create_item() + self.create_supplier(currency="USD", supplier_name="Test Supplier2") + self.create_usd_payable_account() + + def tearDown(self): + frappe.db.rollback() + + def test_accounts_receivable_with_supplier(self): + pi = self.create_purchase_invoice(do_not_submit=True) + pi.currency = "USD" + pi.conversion_rate = 80 + pi.credit_to = self.creditors_usd + pi = pi.save().submit() + + filters = { + "company": self.company, + "party_type": "Supplier", + "party": self.supplier, + "report_date": today(), + "range1": 30, + "range2": 60, + "range3": 90, + "range4": 120, + } + + data = execute(filters) + self.assertEqual(data[1][0].get("outstanding"), 300) + self.assertEqual(data[1][0].get("currency"), "USD") + + def create_purchase_invoice(self, do_not_submit=False): + frappe.set_user("Administrator") + pi = make_purchase_invoice( + item=self.item, + company=self.company, + supplier=self.supplier, + is_return=False, + update_stock=False, + posting_date=frappe.utils.datetime.date(2021, 5, 1), + do_not_save=1, + rate=300, + price_list_rate=300, + qty=1, + ) + + pi = pi.save() + if not do_not_submit: + pi = pi.submit() + return pi diff --git a/erpnext/accounts/report/accounts_receivable/accounts_receivable.js b/erpnext/accounts/report/accounts_receivable/accounts_receivable.js index 0b4e577f6cb3..cb8ec876e9ee 100644 --- a/erpnext/accounts/report/accounts_receivable/accounts_receivable.js +++ b/erpnext/accounts/report/accounts_receivable/accounts_receivable.js @@ -46,8 +46,7 @@ frappe.query_reports["Accounts Receivable"] = { var customer = frappe.query_report.get_filter_value('customer'); var company = frappe.query_report.get_filter_value('company'); if (customer) { - frappe.db.get_value('Customer', customer, ["tax_id", "customer_name", "payment_terms"], function(value) { - frappe.query_report.set_filter_value('tax_id', value["tax_id"]); + frappe.db.get_value('Customer', customer, ["customer_name", "payment_terms"], function(value) { frappe.query_report.set_filter_value('customer_name', value["customer_name"]); frappe.query_report.set_filter_value('payment_terms', value["payment_terms"]); }); @@ -59,7 +58,6 @@ frappe.query_reports["Accounts Receivable"] = { } }, "Customer"); } else { - frappe.query_report.set_filter_value('tax_id', ""); frappe.query_report.set_filter_value('customer_name', ""); frappe.query_report.set_filter_value('credit_limit', ""); frappe.query_report.set_filter_value('payment_terms', ""); @@ -172,12 +170,6 @@ frappe.query_reports["Accounts Receivable"] = { "label": __("Show Sales Person"), "fieldtype": "Check", }, - { - "fieldname": "tax_id", - "label": __("Tax Id"), - "fieldtype": "Data", - "hidden": 1 - }, { "fieldname": "show_remarks", "label": __("Show Remarks"), diff --git a/erpnext/accounts/report/accounts_receivable/accounts_receivable.py b/erpnext/accounts/report/accounts_receivable/accounts_receivable.py index 751063ad8e64..3700f00ee226 100755 --- a/erpnext/accounts/report/accounts_receivable/accounts_receivable.py +++ b/erpnext/accounts/report/accounts_receivable/accounts_receivable.py @@ -211,11 +211,10 @@ def update_voucher_balance(self, ple): return # amount in "Party Currency", if its supplied. If not, amount in company currency - for party_type in self.party_type: - if self.filters.get(scrub(party_type)): - amount = ple.amount_in_account_currency - else: - amount = ple.amount + if self.filters.get("party_type") and self.filters.get("party"): + amount = ple.amount_in_account_currency + else: + amount = ple.amount amount_in_account_currency = ple.amount_in_account_currency # update voucher @@ -426,10 +425,9 @@ def set_party_details(self, row): # customer / supplier name party_details = self.get_party_details(row.party) or {} row.update(party_details) - for party_type in self.party_type: - if self.filters.get(scrub(party_type)): - row.currency = row.account_currency - break + + if self.filters.get("party_type") and self.filters.get("party"): + row.currency = row.account_currency else: row.currency = self.company_currency @@ -765,6 +763,7 @@ def get_sales_invoices_or_customers_based_on_sales_person(self): def prepare_conditions(self): self.qb_selection_filter = [] self.or_filters = [] + for party_type in self.party_type: party_type_field = scrub(party_type) self.or_filters.append(self.ple.party_type == party_type) @@ -800,6 +799,12 @@ def add_common_filters(self, party_type_field): if self.filters.get(party_type_field): self.qb_selection_filter.append(self.ple.party == self.filters.get(party_type_field)) + if self.filters.get("party_type"): + self.qb_selection_filter.append(self.filters.party_type == self.ple.party_type) + + if self.filters.get("party"): + self.qb_selection_filter.append(self.filters.party == self.ple.party) + if self.filters.party_account: self.qb_selection_filter.append(self.ple.account == self.filters.party_account) else: diff --git a/erpnext/accounts/test/accounts_mixin.py b/erpnext/accounts/test/accounts_mixin.py index bf01362c97ff..08688608f4b1 100644 --- a/erpnext/accounts/test/accounts_mixin.py +++ b/erpnext/accounts/test/accounts_mixin.py @@ -126,6 +126,28 @@ def create_usd_receivable_account(self): acc = frappe.get_doc("Account", name) self.debtors_usd = acc.name + def create_usd_payable_account(self): + account_name = "Creditors USD" + if not frappe.db.get_value( + "Account", filters={"account_name": account_name, "company": self.company} + ): + acc = frappe.new_doc("Account") + acc.account_name = account_name + acc.parent_account = "Accounts Payable - " + self.company_abbr + acc.company = self.company + acc.account_currency = "USD" + acc.account_type = "Payable" + acc.insert() + else: + name = frappe.db.get_value( + "Account", + filters={"account_name": account_name, "company": self.company}, + fieldname="name", + pluck=True, + ) + acc = frappe.get_doc("Account", name) + self.creditors_usd = acc.name + def clear_old_entries(self): doctype_list = [ "GL Entry", From 5a226a8395d1d6c51917ee06584c6bfd0467cb16 Mon Sep 17 00:00:00 2001 From: Gursheen Anand Date: Fri, 1 Sep 2023 12:36:40 +0530 Subject: [PATCH 259/501] fix: reduce threshold for bg job fn (cherry picked from commit b6e6f2ef8da1c89fd3105066f0ded7406bb0f31c) --- .../doctype/period_closing_voucher/period_closing_voucher.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/accounts/doctype/period_closing_voucher/period_closing_voucher.py b/erpnext/accounts/doctype/period_closing_voucher/period_closing_voucher.py index 49472484ef44..af1c06643a1c 100644 --- a/erpnext/accounts/doctype/period_closing_voucher/period_closing_voucher.py +++ b/erpnext/accounts/doctype/period_closing_voucher/period_closing_voucher.py @@ -126,7 +126,7 @@ def check_if_previous_year_closed(self): def make_gl_entries(self, get_opening_entries=False): gl_entries = self.get_gl_entries() closing_entries = self.get_grouped_gl_entries(get_opening_entries=get_opening_entries) - if len(gl_entries) > 5000: + if len(gl_entries + closing_entries) > 3000: frappe.enqueue( process_gl_entries, gl_entries=gl_entries, From 61752ac2b49c5e6ecaf720f87e63d53fb5c5d5e2 Mon Sep 17 00:00:00 2001 From: barredterra <14891507+barredterra@users.noreply.github.com> Date: Thu, 15 Jun 2023 18:09:56 +0200 Subject: [PATCH 260/501] fix: only show "Unreconcile" if reconciled (cherry picked from commit 91e574609f3001ba280806d6e6c8bacc3bb1cddc) --- .../doctype/bank_transaction/bank_transaction.js | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/erpnext/accounts/doctype/bank_transaction/bank_transaction.js b/erpnext/accounts/doctype/bank_transaction/bank_transaction.js index e548b4c7e9a8..b3cc1cbb1beb 100644 --- a/erpnext/accounts/doctype/bank_transaction/bank_transaction.js +++ b/erpnext/accounts/doctype/bank_transaction/bank_transaction.js @@ -13,10 +13,11 @@ frappe.ui.form.on("Bank Transaction", { }); }, refresh(frm) { - frm.add_custom_button(__('Unreconcile Transaction'), () => { - frm.call('remove_payment_entries') - .then( () => frm.refresh() ); - }); + if (!frm.is_dirty() && frm.doc.payment_entries.length > 0) { + frm.add_custom_button(__("Unreconcile Transaction"), () => { + frm.call("remove_payment_entries").then(() => frm.refresh()); + }); + } }, bank_account: function (frm) { set_bank_statement_filter(frm); From 345c6084e5461bd43ed7ef7aa7a66b50831f0fab Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Sat, 2 Sep 2023 17:53:08 +0530 Subject: [PATCH 261/501] feat(RFQ): optionally send document print (#36363) feat(RFQ): optionally send document print (#36363) --- .../request_for_quotation.json | 11 ++++++++++- .../request_for_quotation.py | 17 +++++++++++++++-- 2 files changed, 25 insertions(+), 3 deletions(-) diff --git a/erpnext/buying/doctype/request_for_quotation/request_for_quotation.json b/erpnext/buying/doctype/request_for_quotation/request_for_quotation.json index fbfc1ac16935..06dbd86ba126 100644 --- a/erpnext/buying/doctype/request_for_quotation/request_for_quotation.json +++ b/erpnext/buying/doctype/request_for_quotation/request_for_quotation.json @@ -25,6 +25,7 @@ "col_break_email_1", "html_llwp", "send_attached_files", + "send_document_print", "sec_break_email_2", "message_for_supplier", "terms_section_break", @@ -283,13 +284,21 @@ "fieldname": "send_attached_files", "fieldtype": "Check", "label": "Send Attached Files" + }, + { + "default": "0", + "description": "If enabled, a print of this document will be attached to each email", + "fieldname": "send_document_print", + "fieldtype": "Check", + "label": "Send Document Print", + "print_hide": 1 } ], "icon": "fa fa-shopping-cart", "index_web_pages_for_search": 1, "is_submittable": 1, "links": [], - "modified": "2023-08-08 16:30:10.870429", + "modified": "2023-08-09 12:20:26.850623", "modified_by": "Administrator", "module": "Buying", "name": "Request for Quotation", diff --git a/erpnext/buying/doctype/request_for_quotation/request_for_quotation.py b/erpnext/buying/doctype/request_for_quotation/request_for_quotation.py index 56840c11a6e7..6b39982bb816 100644 --- a/erpnext/buying/doctype/request_for_quotation/request_for_quotation.py +++ b/erpnext/buying/doctype/request_for_quotation/request_for_quotation.py @@ -205,10 +205,24 @@ def supplier_rfq_mail(self, data, update_password_link, rfq_link, preview=False) if preview: return {"message": message, "subject": subject} - attachments = None + attachments = [] if self.send_attached_files: attachments = self.get_attachments() + if self.send_document_print: + supplier_language = frappe.db.get_value("Supplier", data.supplier, "language") + system_language = frappe.db.get_single_value("System Settings", "language") + attachments.append( + frappe.attach_print( + self.doctype, + self.name, + doc=self, + print_format=self.meta.default_print_format or "Standard", + lang=supplier_language or system_language, + letterhead=self.letter_head, + ) + ) + self.send_email(data, sender, subject, message, attachments) def send_email(self, data, sender, subject, message, attachments): @@ -218,7 +232,6 @@ def send_email(self, data, sender, subject, message, attachments): recipients=data.email_id, sender=sender, attachments=attachments, - print_format=self.meta.default_print_format or "Standard", send_email=True, doctype=self.doctype, name=self.name, From 0ff1546b9b36ee769ae56ab2d91c84d9eb99a158 Mon Sep 17 00:00:00 2001 From: Anand Baburajan Date: Sat, 2 Sep 2023 18:32:16 +0530 Subject: [PATCH 262/501] chore: patch to correct asset values if je has workflow [v14] (#36915) chore: patch to correct asset values if je has workflow --- .../asset_capitalization.py | 3 - erpnext/patches.txt | 1 + ...correct_asset_value_if_je_with_workflow.py | 119 ++++++++++++++++++ 3 files changed, 120 insertions(+), 3 deletions(-) create mode 100644 erpnext/patches/v14_0/correct_asset_value_if_je_with_workflow.py diff --git a/erpnext/assets/doctype/asset_capitalization/asset_capitalization.py b/erpnext/assets/doctype/asset_capitalization/asset_capitalization.py index fb480420b97a..44db6920c131 100644 --- a/erpnext/assets/doctype/asset_capitalization/asset_capitalization.py +++ b/erpnext/assets/doctype/asset_capitalization/asset_capitalization.py @@ -399,14 +399,11 @@ def get_gl_entries_for_consumed_stock_items( def get_gl_entries_for_consumed_asset_items( self, gl_entries, target_account, target_against, precision ): - self.are_all_asset_items_non_depreciable = True - # Consumed Assets for item in self.asset_items: asset = frappe.get_doc("Asset", item.asset) if asset.calculate_depreciation: - self.are_all_asset_items_non_depreciable = False depreciate_asset(asset, self.posting_date) asset.reload() diff --git a/erpnext/patches.txt b/erpnext/patches.txt index 53d4b44bbc67..cde484eb0fa3 100644 --- a/erpnext/patches.txt +++ b/erpnext/patches.txt @@ -338,5 +338,6 @@ erpnext.patches.v14_0.create_accounting_dimensions_for_closing_balance erpnext.patches.v14_0.update_closing_balances #15-07-2023 execute:frappe.defaults.clear_default("fiscal_year") execute:frappe.db.set_single_value('Selling Settings', 'allow_negative_rates_for_items', 0) +erpnext.patches.v14_0.correct_asset_value_if_je_with_workflow # below migration patch should always run last erpnext.patches.v14_0.migrate_gl_to_payment_ledger diff --git a/erpnext/patches/v14_0/correct_asset_value_if_je_with_workflow.py b/erpnext/patches/v14_0/correct_asset_value_if_je_with_workflow.py new file mode 100644 index 000000000000..aededa2287d6 --- /dev/null +++ b/erpnext/patches/v14_0/correct_asset_value_if_je_with_workflow.py @@ -0,0 +1,119 @@ +import frappe +from frappe.model.workflow import get_workflow_name +from frappe.query_builder.functions import IfNull, Sum + + +def execute(): + active_je_workflow = get_workflow_name("Journal Entry") + if not active_je_workflow: + return + + correct_value_for_assets_with_manual_depr_entries() + + finance_books = frappe.db.get_all("Finance Book", pluck="name") + + if finance_books: + for fb_name in finance_books: + correct_value_for_assets_with_auto_depr(fb_name) + + correct_value_for_assets_with_auto_depr() + + +def correct_value_for_assets_with_manual_depr_entries(): + asset = frappe.qb.DocType("Asset") + gle = frappe.qb.DocType("GL Entry") + aca = frappe.qb.DocType("Asset Category Account") + company = frappe.qb.DocType("Company") + + asset_details_and_depr_amount_map = ( + frappe.qb.from_(gle) + .join(asset) + .on(gle.against_voucher == asset.name) + .join(aca) + .on((aca.parent == asset.asset_category) & (aca.company_name == asset.company)) + .join(company) + .on(company.name == asset.company) + .select( + asset.name.as_("asset_name"), + asset.gross_purchase_amount.as_("gross_purchase_amount"), + asset.opening_accumulated_depreciation.as_("opening_accumulated_depreciation"), + Sum(gle.debit).as_("depr_amount"), + ) + .where( + gle.account == IfNull(aca.depreciation_expense_account, company.depreciation_expense_account) + ) + .where(gle.debit != 0) + .where(gle.is_cancelled == 0) + .where(asset.docstatus == 1) + .where(asset.calculate_depreciation == 0) + .groupby(asset.name) + ) + + frappe.qb.update(asset).join(asset_details_and_depr_amount_map).on( + asset_details_and_depr_amount_map.asset_name == asset.name + ).set( + asset.value_after_depreciation, + asset_details_and_depr_amount_map.gross_purchase_amount + - asset_details_and_depr_amount_map.opening_accumulated_depreciation + - asset_details_and_depr_amount_map.depr_amount, + ).run() + + +def correct_value_for_assets_with_auto_depr(fb_name=None): + asset = frappe.qb.DocType("Asset") + gle = frappe.qb.DocType("GL Entry") + aca = frappe.qb.DocType("Asset Category Account") + company = frappe.qb.DocType("Company") + afb = frappe.qb.DocType("Asset Finance Book") + + asset_details_and_depr_amount_map = ( + frappe.qb.from_(gle) + .join(asset) + .on(gle.against_voucher == asset.name) + .join(aca) + .on((aca.parent == asset.asset_category) & (aca.company_name == asset.company)) + .join(company) + .on(company.name == asset.company) + .select( + asset.name.as_("asset_name"), + asset.gross_purchase_amount.as_("gross_purchase_amount"), + asset.opening_accumulated_depreciation.as_("opening_accumulated_depreciation"), + Sum(gle.debit).as_("depr_amount"), + ) + .where( + gle.account == IfNull(aca.depreciation_expense_account, company.depreciation_expense_account) + ) + .where(gle.debit != 0) + .where(gle.is_cancelled == 0) + .where(asset.docstatus == 1) + .where(asset.calculate_depreciation == 1) + .groupby(asset.name) + ) + + if fb_name: + asset_details_and_depr_amount_map = asset_details_and_depr_amount_map.where( + gle.finance_book == fb_name + ) + else: + asset_details_and_depr_amount_map = asset_details_and_depr_amount_map.where( + (gle.finance_book.isin([""])) | (gle.finance_book.isnull()) + ) + + query = ( + frappe.qb.update(afb) + .join(asset_details_and_depr_amount_map) + .on(asset_details_and_depr_amount_map.asset_name == afb.parent) + .set( + afb.value_after_depreciation, + asset_details_and_depr_amount_map.gross_purchase_amount + - asset_details_and_depr_amount_map.opening_accumulated_depreciation + - asset_details_and_depr_amount_map.depr_amount, + ) + ) + + if fb_name: + query = query.where(afb.finance_book == fb_name) + else: + query = query.where((afb.finance_book.isin([""])) | (afb.finance_book.isnull())) + + query.run() From 9168b3b0e8be0afeb45a6c42534675a831b6e1c4 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Fri, 1 Sep 2023 14:57:12 +0530 Subject: [PATCH 263/501] fix: deduplicate gain/loss JE creation for journals as payment (cherry picked from commit 79c6f0165bc8db4c41173b88ea0c02326e108760) # Conflicts: # erpnext/controllers/accounts_controller.py --- erpnext/accounts/utils.py | 9 +- erpnext/controllers/accounts_controller.py | 107 ++++++++++++++++++--- 2 files changed, 98 insertions(+), 18 deletions(-) diff --git a/erpnext/accounts/utils.py b/erpnext/accounts/utils.py index 0c01ff78c8c1..2e9eca2e4a74 100644 --- a/erpnext/accounts/utils.py +++ b/erpnext/accounts/utils.py @@ -458,10 +458,12 @@ def reconcile_against_document(args, skip_ref_details_update_for_pe=False): # n # update ref in advance entry if voucher_type == "Journal Entry": - update_reference_in_journal_entry(entry, doc, do_not_save=True) + referenced_row = update_reference_in_journal_entry(entry, doc, do_not_save=False) # advance section in sales/purchase invoice and reconciliation tool,both pass on exchange gain/loss # amount and account in args - doc.make_exchange_gain_loss_journal(args) + # referenced_row is used to deduplicate gain/loss journal + entry.update({"referenced_row": referenced_row}) + doc.make_exchange_gain_loss_journal([entry]) else: update_reference_in_payment_entry( entry, doc, do_not_save=True, skip_ref_details_update_for_pe=skip_ref_details_update_for_pe @@ -605,6 +607,8 @@ def update_reference_in_journal_entry(d, journal_entry, do_not_save=False): if not do_not_save: journal_entry.save(ignore_permissions=True) + return new_row.name + def update_reference_in_payment_entry( d, payment_entry, do_not_save=False, skip_ref_details_update_for_pe=False @@ -1871,6 +1875,7 @@ def create_gain_loss_journal( journal_entry.company = company journal_entry.posting_date = nowdate() journal_entry.multi_currency = 1 + journal_entry.is_system_generated = True party_account_currency = frappe.get_cached_value("Account", party_account, "account_currency") diff --git a/erpnext/controllers/accounts_controller.py b/erpnext/controllers/accounts_controller.py index b225e3d5f1e8..c56c0a240c30 100644 --- a/erpnext/controllers/accounts_controller.py +++ b/erpnext/controllers/accounts_controller.py @@ -969,6 +969,74 @@ def set_advance_gain_or_loss(self): d.exchange_gain_loss = difference +<<<<<<< HEAD +======= + def make_precision_loss_gl_entry(self, gl_entries): + round_off_account, round_off_cost_center = get_round_off_account_and_cost_center( + self.company, "Purchase Invoice", self.name, self.use_company_roundoff_cost_center + ) + + precision_loss = self.get("base_net_total") - flt( + self.get("net_total") * self.conversion_rate, self.precision("net_total") + ) + + credit_or_debit = "credit" if self.doctype == "Purchase Invoice" else "debit" + against = self.supplier if self.doctype == "Purchase Invoice" else self.customer + + if precision_loss: + gl_entries.append( + self.get_gl_dict( + { + "account": round_off_account, + "against": against, + credit_or_debit: precision_loss, + "cost_center": round_off_cost_center + if self.use_company_roundoff_cost_center + else self.cost_center or round_off_cost_center, + "remarks": _("Net total calculation precision loss"), + } + ) + ) + + def gain_loss_journal_already_booked( + self, + gain_loss_account, + exc_gain_loss, + ref2_dt, + ref2_dn, + ref2_detail_no, + ) -> bool: + """ + Check if gain/loss is booked + """ + if res := frappe.db.get_all( + "Journal Entry Account", + filters={ + "docstatus": 1, + "account": gain_loss_account, + "reference_type": ref2_dt, # this will be Journal Entry + "reference_name": ref2_dn, + "reference_detail_no": ref2_detail_no, + }, + pluck="parent", + ): + # deduplicate + res = list({x for x in res}) + if exc_vouchers := frappe.db.get_all( + "Journal Entry", + filters={"name": ["in", res], "voucher_type": "Exchange Gain Or Loss"}, + fields=["voucher_type", "total_debit", "total_credit"], + ): + booked_voucher = exc_vouchers[0] + if ( + booked_voucher.total_debit == exc_gain_loss + and booked_voucher.total_credit == exc_gain_loss + and booked_voucher.voucher_type == "Exchange Gain Or Loss" + ): + return True + return False + +>>>>>>> 79c6f0165b (fix: deduplicate gain/loss JE creation for journals as payment) def make_exchange_gain_loss_journal(self, args: dict = None) -> None: """ Make Exchange Gain/Loss journal for Invoices and Payments @@ -997,27 +1065,34 @@ def make_exchange_gain_loss_journal(self, args: dict = None) -> None: reverse_dr_or_cr = "debit" if dr_or_cr == "credit" else "credit" - je = create_gain_loss_journal( - self.company, - arg.get("party_type"), - arg.get("party"), - party_account, + if not self.gain_loss_journal_already_booked( gain_loss_account, difference_amount, - dr_or_cr, - reverse_dr_or_cr, - arg.get("against_voucher_type"), - arg.get("against_voucher"), - arg.get("idx"), self.doctype, self.name, - arg.get("idx"), - ) - frappe.msgprint( - _("Exchange Gain/Loss amount has been booked through {0}").format( - get_link_to_form("Journal Entry", je) + arg.get("referenced_row"), + ): + je = create_gain_loss_journal( + self.company, + arg.get("party_type"), + arg.get("party"), + party_account, + gain_loss_account, + difference_amount, + dr_or_cr, + reverse_dr_or_cr, + arg.get("against_voucher_type"), + arg.get("against_voucher"), + arg.get("idx"), + self.doctype, + self.name, + arg.get("referenced_row"), + ) + frappe.msgprint( + _("Exchange Gain/Loss amount has been booked through {0}").format( + get_link_to_form("Journal Entry", je) + ) ) - ) if self.get("doctype") == "Payment Entry": # For Payment Entry, exchange_gain_loss field in the `references` table is the trigger for journal creation From 475750302b97cdfeae05fdb5ab4d3144bf6860c0 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Sat, 2 Sep 2023 13:28:51 +0530 Subject: [PATCH 264/501] test: deduplicate gain/loss JE on reconciling journals against inv (cherry picked from commit cb6da6ec590114711161b79a2dc52af7b4ccbdde) --- .../tests/test_accounts_controller.py | 45 +++++++++++++++++++ 1 file changed, 45 insertions(+) diff --git a/erpnext/controllers/tests/test_accounts_controller.py b/erpnext/controllers/tests/test_accounts_controller.py index 0f8e133e0fdc..e4f741424c46 100644 --- a/erpnext/controllers/tests/test_accounts_controller.py +++ b/erpnext/controllers/tests/test_accounts_controller.py @@ -941,6 +941,51 @@ def test_23_same_journal_split_against_single_invoice(self): self.assertEqual(exc_je_for_si, []) self.assertEqual(exc_je_for_je, []) + def test_24_journal_against_multiple_invoices(self): + si1 = self.create_sales_invoice(qty=1, conversion_rate=80, rate=1) + si2 = self.create_sales_invoice(qty=1, conversion_rate=80, rate=1) + + # Payment + je = self.create_journal_entry( + acc1=self.debit_usd, + acc1_exc_rate=75, + acc2=self.cash, + acc1_amount=-2, + acc2_amount=-150, + acc2_exc_rate=1, + ) + je.accounts[0].party_type = "Customer" + je.accounts[0].party = self.customer + je = je.save().submit() + + pr = self.create_payment_reconciliation() + pr.get_unreconciled_entries() + self.assertEqual(len(pr.invoices), 2) + self.assertEqual(len(pr.payments), 1) + invoices = [x.as_dict() for x in pr.invoices] + payments = [x.as_dict() for x in pr.payments] + pr.allocate_entries(frappe._dict({"invoices": invoices, "payments": payments})) + pr.reconcile() + self.assertEqual(len(pr.invoices), 0) + self.assertEqual(len(pr.payments), 0) + + si1.reload() + si2.reload() + + self.assertEqual(si1.outstanding_amount, 0) + self.assertEqual(si2.outstanding_amount, 0) + self.assert_ledger_outstanding(si1.doctype, si1.name, 0.0, 0.0) + self.assert_ledger_outstanding(si2.doctype, si2.name, 0.0, 0.0) + + # Exchange Gain/Loss Journal should've been cancelled + # remove payment JE from list + exc_je_for_si1 = [x for x in self.get_journals_for(si1.doctype, si1.name) if x.parent != je.name] + exc_je_for_si2 = [x for x in self.get_journals_for(si2.doctype, si2.name) if x.parent != je.name] + exc_je_for_je = [x for x in self.get_journals_for(je.doctype, je.name) if x.parent != je.name] + self.assertEqual(len(exc_je_for_si1), 1) + self.assertEqual(len(exc_je_for_si2), 1) + self.assertEqual(len(exc_je_for_je), 2) + def test_30_cr_note_against_sales_invoice(self): """ Reconciling Cr Note against Sales Invoice, both having different exchange rates From 7fd96d011647d16a5ecc477541a5a7d08247224a Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Sat, 2 Sep 2023 14:43:25 +0530 Subject: [PATCH 265/501] test: extend test to cancellation (cherry picked from commit 79fa562004a0754d361509d0dd6149d316fa2640) --- erpnext/controllers/tests/test_accounts_controller.py | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/erpnext/controllers/tests/test_accounts_controller.py b/erpnext/controllers/tests/test_accounts_controller.py index e4f741424c46..0c112aeedd31 100644 --- a/erpnext/controllers/tests/test_accounts_controller.py +++ b/erpnext/controllers/tests/test_accounts_controller.py @@ -977,7 +977,7 @@ def test_24_journal_against_multiple_invoices(self): self.assert_ledger_outstanding(si1.doctype, si1.name, 0.0, 0.0) self.assert_ledger_outstanding(si2.doctype, si2.name, 0.0, 0.0) - # Exchange Gain/Loss Journal should've been cancelled + # Exchange Gain/Loss Journal should've been created # remove payment JE from list exc_je_for_si1 = [x for x in self.get_journals_for(si1.doctype, si1.name) if x.parent != je.name] exc_je_for_si2 = [x for x in self.get_journals_for(si2.doctype, si2.name) if x.parent != je.name] @@ -986,6 +986,15 @@ def test_24_journal_against_multiple_invoices(self): self.assertEqual(len(exc_je_for_si2), 1) self.assertEqual(len(exc_je_for_je), 2) + si1.cancel() + # Gain/Loss JE of si1 should've been cancelled + exc_je_for_si1 = [x for x in self.get_journals_for(si1.doctype, si1.name) if x.parent != je.name] + exc_je_for_si2 = [x for x in self.get_journals_for(si2.doctype, si2.name) if x.parent != je.name] + exc_je_for_je = [x for x in self.get_journals_for(je.doctype, je.name) if x.parent != je.name] + self.assertEqual(len(exc_je_for_si1), 0) + self.assertEqual(len(exc_je_for_si2), 1) + self.assertEqual(len(exc_je_for_je), 1) + def test_30_cr_note_against_sales_invoice(self): """ Reconciling Cr Note against Sales Invoice, both having different exchange rates From d24c8b1bbcf076960352fb906fec68f7981d1205 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Sun, 3 Sep 2023 07:21:09 +0530 Subject: [PATCH 266/501] refactor: use payment's CC for gain/loss if company default is unset (cherry picked from commit d6a3b9a5c7306b71584594872b06349f76951cd4) # Conflicts: # erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py --- .../payment_reconciliation.py | 16 +++++++++++++++- .../payment_reconciliation_allocation.json | 11 +++++++++-- .../payment_reconciliation_payment.json | 11 +++++++++-- erpnext/accounts/utils.py | 7 +++++-- erpnext/controllers/accounts_controller.py | 2 ++ 5 files changed, 40 insertions(+), 7 deletions(-) diff --git a/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py b/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py index abffd262961b..a19e4267fdbf 100644 --- a/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py +++ b/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py @@ -100,7 +100,7 @@ def get_jv_entries(self): "Journal Entry" as reference_type, t1.name as reference_name, t1.posting_date, t1.remark as remarks, t2.name as reference_row, {dr_or_cr} as amount, t2.is_advance, t2.exchange_rate, - t2.account_currency as currency + t2.account_currency as currency, t2.cost_center as cost_center from `tabJournal Entry` t1, `tabJournal Entry Account` t2 where @@ -196,6 +196,7 @@ def get_dr_or_cr_notes(self): "amount": -(inv.outstanding_in_account_currency), "posting_date": inv.posting_date, "currency": inv.currency, + "cost_center": inv.cost_center, } ) ) @@ -344,6 +345,7 @@ def get_allocated_entry(self, pay, inv, allocated_amount): "allocated_amount": allocated_amount, "difference_amount": pay.get("difference_amount"), "currency": inv.get("currency"), + "cost_center": pay.get("cost_center"), } ) @@ -418,6 +420,7 @@ def get_payment_details(self, row, dr_or_cr): "allocated_amount": flt(row.get("allocated_amount")), "difference_amount": flt(row.get("difference_amount")), "difference_account": row.get("difference_account"), + "cost_center": row.get("cost_center"), } ) @@ -590,7 +593,12 @@ def reconcile_dr_cr_note(dr_cr_notes, company): inv.dr_or_cr: abs(inv.allocated_amount), "reference_type": inv.against_voucher_type, "reference_name": inv.against_voucher, +<<<<<<< HEAD "cost_center": erpnext.get_default_cost_center(company), +======= + "cost_center": inv.cost_center or erpnext.get_default_cost_center(company), + "exchange_rate": inv.exchange_rate, +>>>>>>> d6a3b9a5c7 (refactor: use payment's CC for gain/loss if company default is unset) "user_remark": f"{fmt_money(flt(inv.allocated_amount), currency=company_currency)} against {inv.against_voucher}", "exchange_rate": inv.exchange_rate, }, @@ -605,7 +613,12 @@ def reconcile_dr_cr_note(dr_cr_notes, company): ), "reference_type": inv.voucher_type, "reference_name": inv.voucher_no, +<<<<<<< HEAD "cost_center": erpnext.get_default_cost_center(company), +======= + "cost_center": inv.cost_center or erpnext.get_default_cost_center(company), + "exchange_rate": inv.exchange_rate, +>>>>>>> d6a3b9a5c7 (refactor: use payment's CC for gain/loss if company default is unset) "user_remark": f"{fmt_money(flt(inv.allocated_amount), currency=company_currency)} from {inv.voucher_no}", "exchange_rate": inv.exchange_rate, }, @@ -644,4 +657,5 @@ def reconcile_dr_cr_note(dr_cr_notes, company): inv.against_voucher_type, inv.against_voucher, None, + inv.cost_center, ) diff --git a/erpnext/accounts/doctype/payment_reconciliation_allocation/payment_reconciliation_allocation.json b/erpnext/accounts/doctype/payment_reconciliation_allocation/payment_reconciliation_allocation.json index 0f7e47acfee4..ec718aa70d31 100644 --- a/erpnext/accounts/doctype/payment_reconciliation_allocation/payment_reconciliation_allocation.json +++ b/erpnext/accounts/doctype/payment_reconciliation_allocation/payment_reconciliation_allocation.json @@ -22,7 +22,8 @@ "column_break_7", "difference_account", "exchange_rate", - "currency" + "currency", + "cost_center" ], "fields": [ { @@ -144,11 +145,17 @@ "fieldtype": "Float", "label": "Exchange Rate", "read_only": 1 + }, + { + "fieldname": "cost_center", + "fieldtype": "Link", + "label": "Cost Center", + "options": "Cost Center" } ], "istable": 1, "links": [], - "modified": "2022-12-24 21:01:14.882747", + "modified": "2023-09-03 07:52:33.684217", "modified_by": "Administrator", "module": "Accounts", "name": "Payment Reconciliation Allocation", diff --git a/erpnext/accounts/doctype/payment_reconciliation_payment/payment_reconciliation_payment.json b/erpnext/accounts/doctype/payment_reconciliation_payment/payment_reconciliation_payment.json index d300ea97abc4..17f3900880c6 100644 --- a/erpnext/accounts/doctype/payment_reconciliation_payment/payment_reconciliation_payment.json +++ b/erpnext/accounts/doctype/payment_reconciliation_payment/payment_reconciliation_payment.json @@ -16,7 +16,8 @@ "sec_break1", "remark", "currency", - "exchange_rate" + "exchange_rate", + "cost_center" ], "fields": [ { @@ -98,11 +99,17 @@ "fieldtype": "Float", "hidden": 1, "label": "Exchange Rate" + }, + { + "fieldname": "cost_center", + "fieldtype": "Link", + "label": "Cost Center", + "options": "Cost Center" } ], "istable": 1, "links": [], - "modified": "2022-11-08 18:18:36.268760", + "modified": "2023-09-03 07:43:29.965353", "modified_by": "Administrator", "module": "Accounts", "name": "Payment Reconciliation Payment", diff --git a/erpnext/accounts/utils.py b/erpnext/accounts/utils.py index 2e9eca2e4a74..267f22d119b1 100644 --- a/erpnext/accounts/utils.py +++ b/erpnext/accounts/utils.py @@ -1724,6 +1724,7 @@ def query_for_outstanding(self): ple.posting_date, ple.due_date, ple.account_currency.as_("currency"), + ple.cost_center.as_("cost_center"), Sum(ple.amount).as_("amount"), Sum(ple.amount_in_account_currency).as_("amount_in_account_currency"), ) @@ -1786,6 +1787,7 @@ def query_for_outstanding(self): ).as_("paid_amount_in_account_currency"), Table("vouchers").due_date, Table("vouchers").currency, + Table("vouchers").cost_center.as_("cost_center"), ) .where(Criterion.all(filter_on_outstanding_amount)) ) @@ -1869,6 +1871,7 @@ def create_gain_loss_journal( ref2_dt, ref2_dn, ref2_detail_no, + cost_center, ) -> str: journal_entry = frappe.new_doc("Journal Entry") journal_entry.voucher_type = "Exchange Gain Or Loss" @@ -1894,7 +1897,7 @@ def create_gain_loss_journal( "party": party, "account_currency": party_account_currency, "exchange_rate": 0, - "cost_center": erpnext.get_default_cost_center(company), + "cost_center": cost_center or erpnext.get_default_cost_center(company), "reference_type": ref1_dt, "reference_name": ref1_dn, "reference_detail_no": ref1_detail_no, @@ -1910,7 +1913,7 @@ def create_gain_loss_journal( "account": gain_loss_account, "account_currency": gain_loss_account_currency, "exchange_rate": 1, - "cost_center": erpnext.get_default_cost_center(company), + "cost_center": cost_center or erpnext.get_default_cost_center(company), "reference_type": ref2_dt, "reference_name": ref2_dn, "reference_detail_no": ref2_detail_no, diff --git a/erpnext/controllers/accounts_controller.py b/erpnext/controllers/accounts_controller.py index c56c0a240c30..a319f1914e7e 100644 --- a/erpnext/controllers/accounts_controller.py +++ b/erpnext/controllers/accounts_controller.py @@ -1087,6 +1087,7 @@ def make_exchange_gain_loss_journal(self, args: dict = None) -> None: self.doctype, self.name, arg.get("referenced_row"), + arg.get("cost_center"), ) frappe.msgprint( _("Exchange Gain/Loss amount has been booked through {0}").format( @@ -1165,6 +1166,7 @@ def make_exchange_gain_loss_journal(self, args: dict = None) -> None: self.doctype, self.name, d.idx, + self.cost_center, ) frappe.msgprint( _("Exchange Gain/Loss amount has been booked through {0}").format( From c8d81cc52d5da73491590a15ec0f18205fa96ee4 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Sun, 3 Sep 2023 08:07:02 +0530 Subject: [PATCH 267/501] test: cost center inheritance from payment (cherry picked from commit 0366928db5737f6c22525485ed3a3c423c4815ec) --- .../tests/test_accounts_controller.py | 137 ++++++++++++++++++ 1 file changed, 137 insertions(+) diff --git a/erpnext/controllers/tests/test_accounts_controller.py b/erpnext/controllers/tests/test_accounts_controller.py index 0c112aeedd31..391258fde778 100644 --- a/erpnext/controllers/tests/test_accounts_controller.py +++ b/erpnext/controllers/tests/test_accounts_controller.py @@ -55,6 +55,7 @@ class TestAccountsController(FrappeTestCase): 10 series - Sales Invoice against Payment Entries 20 series - Sales Invoice against Journals 30 series - Sales Invoice against Credit Notes + 40 series - Company default Cost center is unset """ def setUp(self): @@ -1051,3 +1052,139 @@ def test_30_cr_note_against_sales_invoice(self): si.reload() self.assertEqual(si.outstanding_amount, 1) self.assert_ledger_outstanding(si.doctype, si.name, 80.0, 1.0) + + def test_40_cost_center_from_payment_entry(self): + """ + Gain/Loss JE should inherit cost center from payment if company default is unset + """ + # remove default cost center + cc = frappe.db.get_value("Company", self.company, "cost_center") + frappe.db.set_value("Company", self.company, "cost_center", None) + + rate_in_account_currency = 1 + si = self.create_sales_invoice(qty=1, rate=rate_in_account_currency, do_not_submit=True) + si.cost_center = None + si.save().submit() + + pe = get_payment_entry(si.doctype, si.name) + pe.source_exchange_rate = 75 + pe.received_amount = 75 + pe.cost_center = self.cost_center + pe = pe.save().submit() + + # Exchange Gain/Loss Journal should've been created. + exc_je_for_si = self.get_journals_for(si.doctype, si.name) + exc_je_for_pe = self.get_journals_for(pe.doctype, pe.name) + self.assertNotEqual(exc_je_for_si, []) + self.assertEqual(len(exc_je_for_si), 1) + self.assertEqual(len(exc_je_for_pe), 1) + self.assertEqual(exc_je_for_si[0], exc_je_for_pe[0]) + + self.assertEqual( + [self.cost_center, self.cost_center], + frappe.db.get_all( + "Journal Entry Account", filters={"parent": exc_je_for_si[0].parent}, pluck="cost_center" + ), + ) + frappe.db.set_value("Company", self.company, "cost_center", cc) + + def test_41_cost_center_from_journal_entry(self): + """ + Gain/Loss JE should inherit cost center from payment if company default is unset + """ + # remove default cost center + cc = frappe.db.get_value("Company", self.company, "cost_center") + frappe.db.set_value("Company", self.company, "cost_center", None) + + rate_in_account_currency = 1 + si = self.create_sales_invoice(qty=1, rate=rate_in_account_currency, do_not_submit=True) + si.cost_center = None + si.save().submit() + + je = self.create_journal_entry( + acc1=self.debit_usd, + acc1_exc_rate=75, + acc2=self.cash, + acc1_amount=-1, + acc2_amount=-75, + acc2_exc_rate=1, + ) + je.accounts[0].party_type = "Customer" + je.accounts[0].party = self.customer + je.accounts[0].cost_center = self.cost_center + je = je.save().submit() + + # Reconcile + pr = self.create_payment_reconciliation() + pr.get_unreconciled_entries() + self.assertEqual(len(pr.invoices), 1) + self.assertEqual(len(pr.payments), 1) + invoices = [x.as_dict() for x in pr.invoices] + payments = [x.as_dict() for x in pr.payments] + pr.allocate_entries(frappe._dict({"invoices": invoices, "payments": payments})) + pr.reconcile() + self.assertEqual(len(pr.invoices), 0) + self.assertEqual(len(pr.payments), 0) + + # Exchange Gain/Loss Journal should've been created. + exc_je_for_si = [x for x in self.get_journals_for(si.doctype, si.name) if x.parent != je.name] + exc_je_for_je = [x for x in self.get_journals_for(je.doctype, je.name) if x.parent != je.name] + self.assertNotEqual(exc_je_for_si, []) + self.assertEqual(len(exc_je_for_si), 1) + self.assertEqual(len(exc_je_for_je), 1) + self.assertEqual(exc_je_for_si[0], exc_je_for_je[0]) + + self.assertEqual( + [self.cost_center, self.cost_center], + frappe.db.get_all( + "Journal Entry Account", filters={"parent": exc_je_for_si[0].parent}, pluck="cost_center" + ), + ) + frappe.db.set_value("Company", self.company, "cost_center", cc) + + def test_42_cost_center_from_cr_note(self): + """ + Gain/Loss JE should inherit cost center from payment if company default is unset + """ + # remove default cost center + cc = frappe.db.get_value("Company", self.company, "cost_center") + frappe.db.set_value("Company", self.company, "cost_center", None) + + rate_in_account_currency = 1 + si = self.create_sales_invoice(qty=1, rate=rate_in_account_currency, do_not_submit=True) + si.cost_center = None + si.save().submit() + + cr_note = self.create_sales_invoice(qty=-1, conversion_rate=75, rate=1, do_not_save=True) + cr_note.cost_center = self.cost_center + cr_note.is_return = 1 + cr_note.save().submit() + + # Reconcile + pr = self.create_payment_reconciliation() + pr.get_unreconciled_entries() + self.assertEqual(len(pr.invoices), 1) + self.assertEqual(len(pr.payments), 1) + invoices = [x.as_dict() for x in pr.invoices] + payments = [x.as_dict() for x in pr.payments] + pr.allocate_entries(frappe._dict({"invoices": invoices, "payments": payments})) + pr.reconcile() + self.assertEqual(len(pr.invoices), 0) + self.assertEqual(len(pr.payments), 0) + + # Exchange Gain/Loss Journal should've been created. + exc_je_for_si = self.get_journals_for(si.doctype, si.name) + exc_je_for_cr_note = self.get_journals_for(cr_note.doctype, cr_note.name) + self.assertNotEqual(exc_je_for_si, []) + self.assertEqual(len(exc_je_for_si), 2) + self.assertEqual(len(exc_je_for_cr_note), 2) + self.assertEqual(exc_je_for_si, exc_je_for_cr_note) + + for x in exc_je_for_si + exc_je_for_cr_note: + with self.subTest(x=x): + self.assertEqual( + [self.cost_center, self.cost_center], + frappe.db.get_all("Journal Entry Account", filters={"parent": x.parent}, pluck="cost_center"), + ) + + frappe.db.set_value("Company", self.company, "cost_center", cc) From 5523bc5081cfafaf1f17a7abfa1d7081d35ab88e Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Sun, 3 Sep 2023 10:26:23 +0530 Subject: [PATCH 268/501] chore: resolve merge conflict --- .../payment_reconciliation.py | 10 ------- erpnext/controllers/accounts_controller.py | 30 ------------------- 2 files changed, 40 deletions(-) diff --git a/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py b/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py index a19e4267fdbf..e57c3259d1f2 100644 --- a/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py +++ b/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py @@ -593,12 +593,7 @@ def reconcile_dr_cr_note(dr_cr_notes, company): inv.dr_or_cr: abs(inv.allocated_amount), "reference_type": inv.against_voucher_type, "reference_name": inv.against_voucher, -<<<<<<< HEAD - "cost_center": erpnext.get_default_cost_center(company), -======= "cost_center": inv.cost_center or erpnext.get_default_cost_center(company), - "exchange_rate": inv.exchange_rate, ->>>>>>> d6a3b9a5c7 (refactor: use payment's CC for gain/loss if company default is unset) "user_remark": f"{fmt_money(flt(inv.allocated_amount), currency=company_currency)} against {inv.against_voucher}", "exchange_rate": inv.exchange_rate, }, @@ -613,12 +608,7 @@ def reconcile_dr_cr_note(dr_cr_notes, company): ), "reference_type": inv.voucher_type, "reference_name": inv.voucher_no, -<<<<<<< HEAD - "cost_center": erpnext.get_default_cost_center(company), -======= "cost_center": inv.cost_center or erpnext.get_default_cost_center(company), - "exchange_rate": inv.exchange_rate, ->>>>>>> d6a3b9a5c7 (refactor: use payment's CC for gain/loss if company default is unset) "user_remark": f"{fmt_money(flt(inv.allocated_amount), currency=company_currency)} from {inv.voucher_no}", "exchange_rate": inv.exchange_rate, }, diff --git a/erpnext/controllers/accounts_controller.py b/erpnext/controllers/accounts_controller.py index a319f1914e7e..b476d0ffb486 100644 --- a/erpnext/controllers/accounts_controller.py +++ b/erpnext/controllers/accounts_controller.py @@ -969,35 +969,6 @@ def set_advance_gain_or_loss(self): d.exchange_gain_loss = difference -<<<<<<< HEAD -======= - def make_precision_loss_gl_entry(self, gl_entries): - round_off_account, round_off_cost_center = get_round_off_account_and_cost_center( - self.company, "Purchase Invoice", self.name, self.use_company_roundoff_cost_center - ) - - precision_loss = self.get("base_net_total") - flt( - self.get("net_total") * self.conversion_rate, self.precision("net_total") - ) - - credit_or_debit = "credit" if self.doctype == "Purchase Invoice" else "debit" - against = self.supplier if self.doctype == "Purchase Invoice" else self.customer - - if precision_loss: - gl_entries.append( - self.get_gl_dict( - { - "account": round_off_account, - "against": against, - credit_or_debit: precision_loss, - "cost_center": round_off_cost_center - if self.use_company_roundoff_cost_center - else self.cost_center or round_off_cost_center, - "remarks": _("Net total calculation precision loss"), - } - ) - ) - def gain_loss_journal_already_booked( self, gain_loss_account, @@ -1036,7 +1007,6 @@ def gain_loss_journal_already_booked( return True return False ->>>>>>> 79c6f0165b (fix: deduplicate gain/loss JE creation for journals as payment) def make_exchange_gain_loss_journal(self, args: dict = None) -> None: """ Make Exchange Gain/Loss journal for Invoices and Payments From 5c8bee0a9592f61939aad47d601ef16eac65d63a Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Sun, 3 Sep 2023 16:38:44 +0530 Subject: [PATCH 269/501] fix: when create doc from item dashboard default uom (buying or selling) is not correctly selected (backport #36892) (#36928) fix: when create doc from item dashboard default uom (buying or selling) is not correctly selected (#36892) fix: when create doc from item dashboard defaut uom is not correctly selected (cherry picked from commit 24e1144de5bf129a9d9e2c79a760d257afe1a861) Co-authored-by: HENRY Florian --- erpnext/stock/doctype/item/item.js | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/erpnext/stock/doctype/item/item.js b/erpnext/stock/doctype/item/item.js index 9a9ddf440443..86f9af25e7a2 100644 --- a/erpnext/stock/doctype/item/item.js +++ b/erpnext/stock/doctype/item/item.js @@ -3,6 +3,9 @@ frappe.provide("erpnext.item"); +const SALES_DOCTYPES = ['Quotation', 'Sales Order', 'Delivery Note', 'Sales Invoice']; +const PURCHASE_DOCTYPES = ['Purchase Order', 'Purchase Receipt', 'Purchase Invoice']; + frappe.ui.form.on("Item", { setup: function(frm) { frm.add_fetch('attribute', 'numeric_values', 'numeric_values'); @@ -894,7 +897,13 @@ function open_form(frm, doctype, child_doctype, parentfield) { let new_child_doc = frappe.model.add_child(new_doc, child_doctype, parentfield); new_child_doc.item_code = frm.doc.name; new_child_doc.item_name = frm.doc.item_name; - new_child_doc.uom = frm.doc.stock_uom; + if (in_list(SALES_DOCTYPES, doctype) && frm.doc.sales_uom) { + new_child_doc.uom = frm.doc.sales_uom; + } else if (in_list(PURCHASE_DOCTYPES, doctype) && frm.doc.purchase_uom) { + new_child_doc.uom = frm.doc.purchase_uom; + } else { + new_child_doc.uom = frm.doc.stock_uom; + } new_child_doc.description = frm.doc.description; if (!new_child_doc.qty) { new_child_doc.qty = 1.0; From 11e67c7dc00ed0c3f300644e59d4eca923b2f69b Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Sun, 3 Sep 2023 19:05:05 +0530 Subject: [PATCH 270/501] refactor: remove `Recalculate Rate` from SCR Item (backport #36929) (#36931) * refactor: remove `Recalculate Rate` from SCR Item (#36929) (cherry picked from commit cd8ddae7c5ca30791e999b1654a2f33bf2ecc704) # Conflicts: # erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.js # erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.py # erpnext/subcontracting/doctype/subcontracting_receipt_item/subcontracting_receipt_item.json * chore: `conflicts` --------- Co-authored-by: s-aga-r --- .../subcontracting_receipt/subcontracting_receipt.py | 7 +++---- .../subcontracting_receipt_item.json | 10 +--------- 2 files changed, 4 insertions(+), 13 deletions(-) diff --git a/erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.py b/erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.py index cf457dfe82db..130f38fb8091 100644 --- a/erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.py +++ b/erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.py @@ -176,10 +176,9 @@ def calculate_items_qty_and_amount(self): item.rm_cost_per_qty = item.rm_supp_cost / item.qty rm_supp_cost.pop(item.name) - if item.recalculate_rate: - item.rate = ( - flt(item.rm_cost_per_qty) + flt(item.service_cost_per_qty) + flt(item.additional_cost_per_qty) - ) + item.rate = ( + flt(item.rm_cost_per_qty) + flt(item.service_cost_per_qty) + flt(item.additional_cost_per_qty) + ) item.received_qty = item.qty + flt(item.rejected_qty) item.amount = item.qty * item.rate diff --git a/erpnext/subcontracting/doctype/subcontracting_receipt_item/subcontracting_receipt_item.json b/erpnext/subcontracting/doctype/subcontracting_receipt_item/subcontracting_receipt_item.json index f6cf3402ad24..f7e88d0fb7fe 100644 --- a/erpnext/subcontracting/doctype/subcontracting_receipt_item/subcontracting_receipt_item.json +++ b/erpnext/subcontracting/doctype/subcontracting_receipt_item/subcontracting_receipt_item.json @@ -29,7 +29,6 @@ "rate_and_amount", "rate", "amount", - "recalculate_rate", "column_break_19", "rm_cost_per_qty", "service_cost_per_qty", @@ -196,7 +195,6 @@ "options": "currency", "print_width": "100px", "read_only": 1, - "read_only_depends_on": "eval: doc.recalculate_rate", "width": "100px" }, { @@ -466,18 +464,12 @@ "fieldname": "accounting_details_section", "fieldtype": "Section Break", "label": "Accounting Details" - }, - { - "default": "1", - "fieldname": "recalculate_rate", - "fieldtype": "Check", - "label": "Recalculate Rate" } ], "idx": 1, "istable": 1, "links": [], - "modified": "2023-07-06 18:44:45.599761", + "modified": "2023-09-03 17:04:21.214316", "modified_by": "Administrator", "module": "Subcontracting", "name": "Subcontracting Receipt Item", From 451cc7bc12cbbd6fb17afac05bf3871116d6415b Mon Sep 17 00:00:00 2001 From: RitvikSardana Date: Tue, 29 Aug 2023 13:07:13 +0530 Subject: [PATCH 271/501] fix: added ignore_user_permissions to parent field of tree doctypes (cherry picked from commit de433d86268cae34d8e595fb6006dc99b23fe4db) --- erpnext/assets/doctype/location/location.json | 6 ++- .../quality_procedure/quality_procedure.json | 4 +- .../setup/doctype/department/department.json | 43 ++++++------------- 3 files changed, 20 insertions(+), 33 deletions(-) diff --git a/erpnext/assets/doctype/location/location.json b/erpnext/assets/doctype/location/location.json index f56fd05d98c8..9202fb9d95f6 100644 --- a/erpnext/assets/doctype/location/location.json +++ b/erpnext/assets/doctype/location/location.json @@ -39,6 +39,7 @@ { "fieldname": "parent_location", "fieldtype": "Link", + "ignore_user_permissions": 1, "label": "Parent Location", "options": "Location", "search_index": 1 @@ -141,11 +142,11 @@ ], "is_tree": 1, "links": [], - "modified": "2020-05-08 16:11:11.375701", + "modified": "2023-08-29 12:49:33.290527", "modified_by": "Administrator", "module": "Assets", "name": "Location", - "name_case": "Title Case", + "naming_rule": "By fieldname", "nsm_parent_field": "parent_location", "owner": "Administrator", "permissions": [ @@ -224,5 +225,6 @@ "show_name_in_global_search": 1, "sort_field": "modified", "sort_order": "DESC", + "states": [], "track_changes": 1 } \ No newline at end of file diff --git a/erpnext/quality_management/doctype/quality_procedure/quality_procedure.json b/erpnext/quality_management/doctype/quality_procedure/quality_procedure.json index f588f9aea1a4..f5d7a6dd0c52 100644 --- a/erpnext/quality_management/doctype/quality_procedure/quality_procedure.json +++ b/erpnext/quality_management/doctype/quality_procedure/quality_procedure.json @@ -23,6 +23,7 @@ { "fieldname": "parent_quality_procedure", "fieldtype": "Link", + "ignore_user_permissions": 1, "label": "Parent Procedure", "options": "Quality Procedure" }, @@ -115,7 +116,7 @@ "link_fieldname": "procedure" } ], - "modified": "2020-10-26 15:25:39.316088", + "modified": "2023-08-29 12:49:53.963370", "modified_by": "Administrator", "module": "Quality Management", "name": "Quality Procedure", @@ -149,5 +150,6 @@ ], "sort_field": "modified", "sort_order": "DESC", + "states": [], "track_changes": 1 } \ No newline at end of file diff --git a/erpnext/setup/doctype/department/department.json b/erpnext/setup/doctype/department/department.json index 5a16bae0f2e6..99deca5c19d4 100644 --- a/erpnext/setup/doctype/department/department.json +++ b/erpnext/setup/doctype/department/department.json @@ -25,18 +25,15 @@ "label": "Department", "oldfieldname": "department_name", "oldfieldtype": "Data", - "reqd": 1, - "show_days": 1, - "show_seconds": 1 + "reqd": 1 }, { "fieldname": "parent_department", "fieldtype": "Link", + "ignore_user_permissions": 1, "in_list_view": 1, "label": "Parent Department", - "options": "Department", - "show_days": 1, - "show_seconds": 1 + "options": "Department" }, { "fieldname": "company", @@ -44,9 +41,7 @@ "in_standard_filter": 1, "label": "Company", "options": "Company", - "reqd": 1, - "show_days": 1, - "show_seconds": 1 + "reqd": 1 }, { "bold": 1, @@ -54,17 +49,13 @@ "fieldname": "is_group", "fieldtype": "Check", "in_list_view": 1, - "label": "Is Group", - "show_days": 1, - "show_seconds": 1 + "label": "Is Group" }, { "default": "0", "fieldname": "disabled", "fieldtype": "Check", - "label": "Disabled", - "show_days": 1, - "show_seconds": 1 + "label": "Disabled" }, { "fieldname": "lft", @@ -72,9 +63,7 @@ "hidden": 1, "label": "lft", "print_hide": 1, - "read_only": 1, - "show_days": 1, - "show_seconds": 1 + "read_only": 1 }, { "fieldname": "rgt", @@ -82,9 +71,7 @@ "hidden": 1, "label": "rgt", "print_hide": 1, - "read_only": 1, - "show_days": 1, - "show_seconds": 1 + "read_only": 1 }, { "fieldname": "old_parent", @@ -92,22 +79,18 @@ "hidden": 1, "ignore_user_permissions": 1, "label": "Old Parent", - "print_hide": 1, - "show_days": 1, - "show_seconds": 1 + "print_hide": 1 }, { "fieldname": "column_break_3", - "fieldtype": "Column Break", - "show_days": 1, - "show_seconds": 1 + "fieldtype": "Column Break" } ], "icon": "fa fa-sitemap", "idx": 1, "is_tree": 1, "links": [], - "modified": "2020-06-10 12:28:00.563272", + "modified": "2023-08-28 17:26:46.826501", "modified_by": "Administrator", "module": "Setup", "name": "Department", @@ -147,12 +130,12 @@ "read": 1, "report": 1, "role": "HR Manager", - "set_user_permissions": 1, "share": 1, "write": 1 } ], "show_name_in_global_search": 1, "sort_field": "modified", - "sort_order": "ASC" + "sort_order": "ASC", + "states": [] } \ No newline at end of file From 068f1b5a6b15fdeed79d28a2e1db1c0d057afac9 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Mon, 4 Sep 2023 11:46:42 +0530 Subject: [PATCH 272/501] fix: invalid gain/loss JE created on base currency Expense Claim (cherry picked from commit 75d95acb23a9afcb53c188ceb9c2a71405b929ba) --- erpnext/accounts/doctype/payment_entry/payment_entry.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/erpnext/accounts/doctype/payment_entry/payment_entry.py b/erpnext/accounts/doctype/payment_entry/payment_entry.py index 1c6c99ed3e50..0e2de4ac3952 100644 --- a/erpnext/accounts/doctype/payment_entry/payment_entry.py +++ b/erpnext/accounts/doctype/payment_entry/payment_entry.py @@ -809,6 +809,11 @@ def calculate_base_allocated_amount_for_reference(self, d) -> float: flt(d.allocated_amount) * flt(exchange_rate), self.precision("base_paid_amount") ) + # on rare case, when `exchange_rate` is unset, gain/loss amount is incorrectly calculated + # for base currency transactions + if d.exchange_rate is None: + d.exchange_rate = 1 + allocated_amount_in_pe_exchange_rate = flt( flt(d.allocated_amount) * flt(d.exchange_rate), self.precision("base_paid_amount") ) From 18f8f7f09cefa3c4b102c2a0ce6a90476d1e7d16 Mon Sep 17 00:00:00 2001 From: Gursheen Anand Date: Mon, 4 Sep 2023 17:30:10 +0530 Subject: [PATCH 273/501] fix: remove withholding category from common fields --- .../tds_payable_monthly/tds_payable_monthly.py | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/erpnext/accounts/report/tds_payable_monthly/tds_payable_monthly.py b/erpnext/accounts/report/tds_payable_monthly/tds_payable_monthly.py index 7191720c57e5..91ad3d6873ac 100644 --- a/erpnext/accounts/report/tds_payable_monthly/tds_payable_monthly.py +++ b/erpnext/accounts/report/tds_payable_monthly/tds_payable_monthly.py @@ -324,12 +324,22 @@ def get_journal_entry_party_map(journal_entries): def get_doc_info(vouchers, doctype, tax_category_map, net_total_map=None): - common_fields = ["name", "tax_withholding_category"] + common_fields = ["name"] fields_dict = { - "Purchase Invoice": ["base_tax_withholding_net_total", "grand_total", "base_total"], + "Purchase Invoice": [ + "tax_withholding_category", + "base_tax_withholding_net_total", + "grand_total", + "base_total", + ], "Sales Invoice": ["base_net_total", "grand_total", "base_total"], - "Payment Entry": ["paid_amount", "paid_amount_after_tax", "base_paid_amount"], - "Journal Entry": ["total_amount"], + "Payment Entry": [ + "tax_withholding_category", + "paid_amount", + "paid_amount_after_tax", + "base_paid_amount", + ], + "Journal Entry": ["tax_withholding_category", "total_amount"], } entries = frappe.get_all( From 01eae2b7588eccf12003fb42fd9787dd81b2b794 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Mon, 4 Sep 2023 20:38:10 +0530 Subject: [PATCH 274/501] refactor: gain/loss should use same posting date as payment (cherry picked from commit f7865da4d258f131529dc0fb907f7ecbcbfdb23b) --- .../doctype/payment_reconciliation/payment_reconciliation.py | 1 + erpnext/accounts/utils.py | 3 ++- erpnext/controllers/accounts_controller.py | 3 +++ 3 files changed, 6 insertions(+), 1 deletion(-) diff --git a/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py b/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py index e57c3259d1f2..7d294e873d4a 100644 --- a/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py +++ b/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py @@ -634,6 +634,7 @@ def reconcile_dr_cr_note(dr_cr_notes, company): create_gain_loss_journal( company, + today(), inv.party_type, inv.party, inv.account, diff --git a/erpnext/accounts/utils.py b/erpnext/accounts/utils.py index 267f22d119b1..8f0ef869ad32 100644 --- a/erpnext/accounts/utils.py +++ b/erpnext/accounts/utils.py @@ -1858,6 +1858,7 @@ def get_voucher_outstandings( def create_gain_loss_journal( company, + posting_date, party_type, party, party_account, @@ -1876,7 +1877,7 @@ def create_gain_loss_journal( journal_entry = frappe.new_doc("Journal Entry") journal_entry.voucher_type = "Exchange Gain Or Loss" journal_entry.company = company - journal_entry.posting_date = nowdate() + journal_entry.posting_date = posting_date or nowdate() journal_entry.multi_currency = 1 journal_entry.is_system_generated = True diff --git a/erpnext/controllers/accounts_controller.py b/erpnext/controllers/accounts_controller.py index b476d0ffb486..df77e5260f5d 100644 --- a/erpnext/controllers/accounts_controller.py +++ b/erpnext/controllers/accounts_controller.py @@ -1042,8 +1042,10 @@ def make_exchange_gain_loss_journal(self, args: dict = None) -> None: self.name, arg.get("referenced_row"), ): + posting_date = frappe.db.get_value(arg.voucher_type, arg.voucher_no, "posting_date") je = create_gain_loss_journal( self.company, + posting_date, arg.get("party_type"), arg.get("party"), party_account, @@ -1123,6 +1125,7 @@ def make_exchange_gain_loss_journal(self, args: dict = None) -> None: je = create_gain_loss_journal( self.company, + self.posting_date, self.party_type, self.party, party_account, From 09e2f24329a9c9f7b29f6987b02fca05fc9bc4c5 Mon Sep 17 00:00:00 2001 From: Anand Baburajan Date: Tue, 5 Sep 2023 12:44:14 +0530 Subject: [PATCH 275/501] fix: allow payment_account of loan repayment to be edited (#36948) --- .../doctype/loan_repayment/loan_repayment.json | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/erpnext/loan_management/doctype/loan_repayment/loan_repayment.json b/erpnext/loan_management/doctype/loan_repayment/loan_repayment.json index 76dc8b462e31..4d2c45d029ff 100644 --- a/erpnext/loan_management/doctype/loan_repayment/loan_repayment.json +++ b/erpnext/loan_management/doctype/loan_repayment/loan_repayment.json @@ -263,12 +263,13 @@ "label": "Accounting Details" }, { + "depends_on": "eval:!doc.repay_from_salary", "fetch_from": "against_loan.payment_account", + "fetch_if_empty": 1, "fieldname": "payment_account", "fieldtype": "Link", "label": "Repayment Account", - "options": "Account", - "read_only": 1 + "options": "Account" }, { "fieldname": "column_break_36", @@ -294,7 +295,7 @@ "index_web_pages_for_search": 1, "is_submittable": 1, "links": [], - "modified": "2022-06-21 10:10:07.742298", + "modified": "2023-09-04 15:44:29.148766", "modified_by": "Administrator", "module": "Loan Management", "name": "Loan Repayment", From 035eaa5e40e64df45259f834ab49798d7367f43e Mon Sep 17 00:00:00 2001 From: Gursheen Anand Date: Tue, 5 Sep 2023 15:24:59 +0530 Subject: [PATCH 276/501] test: tds payable monthly --- .../test_tds_payable_monthly.py | 116 ++++++++++++++++++ 1 file changed, 116 insertions(+) create mode 100644 erpnext/accounts/report/tds_payable_monthly/test_tds_payable_monthly.py diff --git a/erpnext/accounts/report/tds_payable_monthly/test_tds_payable_monthly.py b/erpnext/accounts/report/tds_payable_monthly/test_tds_payable_monthly.py new file mode 100644 index 000000000000..d918d9e898b4 --- /dev/null +++ b/erpnext/accounts/report/tds_payable_monthly/test_tds_payable_monthly.py @@ -0,0 +1,116 @@ +# Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and Contributors +# MIT License. See license.txt + +import frappe +from frappe.tests.utils import FrappeTestCase +from frappe.utils import today + +from erpnext.accounts.doctype.cost_center.test_cost_center import create_cost_center +from erpnext.accounts.doctype.payment_entry.test_payment_entry import create_payment_entry +from erpnext.accounts.doctype.purchase_invoice.test_purchase_invoice import make_purchase_invoice +from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice +from erpnext.accounts.doctype.tax_withholding_category.test_tax_withholding_category import ( + create_tax_withholding_category, +) +from erpnext.accounts.report.tds_payable_monthly.tds_payable_monthly import execute +from erpnext.accounts.utils import get_fiscal_year + + +class TestTdsPayableMonthly(FrappeTestCase): + def setUp(self): + delete_docs() + create_tax_accounts() + create_tax_categories() + + def test_tax_withholding_for_customers(self): + si = create_sales_invoice(rate=1000) + pe = create_tcs_payment_entry() + filters = frappe._dict( + company="_Test Company", party_type="Customer", from_date=today(), to_date=today() + ) + result = execute(filters)[1] + expected_values = [ + [pe.name, "TCS", 0.075, 2550, 0.9, 2550.9], + [si.name, "TCS", 0.075, 1000, 0.9, 1000.9], + ] + self.check_expected_values(result, expected_values) + + def check_expected_values(self, result, expected_values): + for i in range(len(result)): + voucher = frappe._dict(result[i]) + voucher_expected_values = expected_values[i] + self.assertEqual(voucher.ref_no, voucher_expected_values[0]) + self.assertEqual(voucher.section_code, voucher_expected_values[1]) + self.assertEqual(voucher.rate, voucher_expected_values[2]) + self.assertEqual(voucher.base_total, voucher_expected_values[3]) + self.assertEqual(voucher.tax_amount, voucher_expected_values[4]) + self.assertEqual(voucher.grand_total, voucher_expected_values[5]) + + def tearDown(self): + delete_docs() + + +def delete_docs(): + frappe.db.sql("delete from `tabSales Invoice` where company='_Test Company'") + frappe.db.sql("delete from `tabPurchase Invoice` where company='_Test Company'") + frappe.db.sql("delete from `tabGL Entry` where company='_Test Company'") + frappe.db.sql("delete from `tabPayment Entry` where company='_Test Company'") + + +def create_tax_accounts(): + account_names = ["TCS", "TDS"] + for account in account_names: + frappe.get_doc( + { + "doctype": "Account", + "company": "_Test Company", + "account_name": account, + "parent_account": "Duties and Taxes - _TC", + "report_type": "Balance Sheet", + "root_type": "Liability", + } + ).insert(ignore_if_duplicate=True) + + +def create_tax_categories(): + fiscal_year = get_fiscal_year(today(), company="_Test Company") + from_date = fiscal_year[1] + to_date = fiscal_year[2] + + tax_category = create_tax_withholding_category( + category_name="TCS", + rate=0.075, + from_date=from_date, + to_date=to_date, + account="TCS - _TC", + cumulative_threshold=300, + ) + + customer = frappe.get_doc("Customer", "_Test Customer") + customer.tax_withholding_category = "TCS" + customer.save() + + +def create_tcs_payment_entry(): + payment_entry = create_payment_entry( + payment_type="Receive", + party_type="Customer", + party="_Test Customer", + paid_from="Debtors - _TC", + paid_to="Cash - _TC", + paid_amount=2550, + ) + + payment_entry.append( + "taxes", + { + "account_head": "TCS - _TC", + "charge_type": "Actual", + "tax_amount": 0.9, + "add_deduct_tax": "Add", + "description": "Test", + "cost_center": "Main - _TC", + }, + ) + payment_entry.submit() + return payment_entry From fc79af592648912fe25a64802370eb5389de754a Mon Sep 17 00:00:00 2001 From: Gursheen Kaur Anand <40693548+GursheenK@users.noreply.github.com> Date: Tue, 5 Sep 2023 16:14:16 +0530 Subject: [PATCH 277/501] fix: prorate factor for subscription plan (#36953) --- .../doctype/subscription/test_subscription.py | 20 ++++++++++++++++ .../subscription_plan/subscription_plan.py | 23 +++++++++---------- 2 files changed, 31 insertions(+), 12 deletions(-) diff --git a/erpnext/accounts/doctype/subscription/test_subscription.py b/erpnext/accounts/doctype/subscription/test_subscription.py index eb17daa282f7..c911e7fe12d5 100644 --- a/erpnext/accounts/doctype/subscription/test_subscription.py +++ b/erpnext/accounts/doctype/subscription/test_subscription.py @@ -694,3 +694,23 @@ def test_multicurrency_subscription(self): # Check the currency of the created invoice currency = frappe.db.get_value("Sales Invoice", subscription.invoices[0].invoice, "currency") self.assertEqual(currency, "USD") + + def test_plan_rate_for_midmonth_start_date(self): + subscription = frappe.new_doc("Subscription") + subscription.party_type = "Supplier" + subscription.party = "_Test Supplier" + subscription.generate_invoice_at_period_start = 1 + subscription.follow_calendar_months = 1 + subscription.generate_new_invoices_past_due_date = 1 + subscription.start_date = "2023-04-08" + subscription.end_date = "2024-02-27" + subscription.append("plans", {"plan": "_Test Plan Name 4", "qty": 1}) + subscription.save() + + subscription.process() + + self.assertEqual(len(subscription.invoices), 1) + pi = frappe.get_doc("Purchase Invoice", subscription.invoices[0].invoice) + self.assertEqual(pi.total, 55333.33) + + subscription.delete() diff --git a/erpnext/accounts/doctype/subscription_plan/subscription_plan.py b/erpnext/accounts/doctype/subscription_plan/subscription_plan.py index f3acdc5aa87f..75223c2ccca2 100644 --- a/erpnext/accounts/doctype/subscription_plan/subscription_plan.py +++ b/erpnext/accounts/doctype/subscription_plan/subscription_plan.py @@ -57,18 +57,17 @@ def get_plan_rate( prorate = frappe.db.get_single_value("Subscription Settings", "prorate") if prorate: - prorate_factor = flt( - date_diff(start_date, get_first_day(start_date)) - / date_diff(get_last_day(start_date), get_first_day(start_date)), - 1, - ) + cost -= plan.cost * get_prorate_factor(start_date, end_date) + return cost - prorate_factor += flt( - date_diff(get_last_day(end_date), end_date) - / date_diff(get_last_day(end_date), get_first_day(end_date)), - 1, - ) - cost -= plan.cost * prorate_factor +def get_prorate_factor(start_date, end_date): + total_days_to_skip = date_diff(start_date, get_first_day(start_date)) + total_days_in_month = int(get_last_day(start_date).strftime("%d")) + prorate_factor = flt(total_days_to_skip / total_days_in_month) - return cost + total_days_to_skip = date_diff(get_last_day(end_date), end_date) + total_days_in_month = int(get_last_day(end_date).strftime("%d")) + prorate_factor += flt(total_days_to_skip / total_days_in_month) + + return prorate_factor From 119273639c14cc89bddc45f402f6c2d8c8abc8f3 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Tue, 5 Sep 2023 16:21:33 +0530 Subject: [PATCH 278/501] fix: added validation for unique serial numbers in pos invoice (#36302) fix: added validation for unique serial numbers in pos invoice (#36302) * fix: added validation for unique serial numbers in pos invoice * fix: updated title of validation * fix: removed extra whitespace * fix: added validation for duplicate batch numbers --------- Co-authored-by: Ritvik Sardana (cherry picked from commit a165b37fd73f6b68f17c19f7e537e08ba035bb2f) Co-authored-by: RitvikSardana <65544983+RitvikSardana@users.noreply.github.com> --- .../doctype/pos_invoice/pos_invoice.py | 24 ++++++++++++++++++- 1 file changed, 23 insertions(+), 1 deletion(-) diff --git a/erpnext/accounts/doctype/pos_invoice/pos_invoice.py b/erpnext/accounts/doctype/pos_invoice/pos_invoice.py index 0ff230bb18be..e7a7ae24ceb1 100644 --- a/erpnext/accounts/doctype/pos_invoice/pos_invoice.py +++ b/erpnext/accounts/doctype/pos_invoice/pos_invoice.py @@ -1,6 +1,6 @@ # Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors and contributors # For license information, please see license.txt - +import collections import frappe from frappe import _ @@ -43,6 +43,7 @@ def validate(self): self.validate_debit_to_acc() self.validate_write_off_account() self.validate_change_amount() + self.validate_duplicate_serial_and_batch_no() self.validate_change_account() self.validate_item_cost_centers() self.validate_warehouse() @@ -155,6 +156,27 @@ def validate_pos_reserved_serial_nos(self, item): title=_("Item Unavailable"), ) + def validate_duplicate_serial_and_batch_no(self): + serial_nos = [] + batch_nos = [] + + for row in self.get("items"): + if row.serial_no: + serial_nos = row.serial_no.split("\n") + + if row.batch_no and not row.serial_no: + batch_nos.append(row.batch_no) + + if serial_nos: + for key, value in collections.Counter(serial_nos).items(): + if value > 1: + frappe.throw(_("Duplicate Serial No {0} found").format("key")) + + if batch_nos: + for key, value in collections.Counter(batch_nos).items(): + if value > 1: + frappe.throw(_("Duplicate Batch No {0} found").format("key")) + def validate_pos_reserved_batch_qty(self, item): filters = {"item_code": item.item_code, "warehouse": item.warehouse, "batch_no": item.batch_no} From c125dea0f1478b25ef1234b18764597931bff7d8 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Tue, 5 Sep 2023 16:29:03 +0530 Subject: [PATCH 279/501] fix: ignore mandatory fields while saving WO (backport #36954) (#36970) fix: ignore mandatory fields while saving WO (#36954) (cherry picked from commit f809e12747bec4fbfe640b6e3c554258b780e99d) Co-authored-by: s-aga-r --- erpnext/stock/doctype/material_request/material_request.py | 1 + 1 file changed, 1 insertion(+) diff --git a/erpnext/stock/doctype/material_request/material_request.py b/erpnext/stock/doctype/material_request/material_request.py index bfb0d4e18533..311b0ed8ce9a 100644 --- a/erpnext/stock/doctype/material_request/material_request.py +++ b/erpnext/stock/doctype/material_request/material_request.py @@ -706,6 +706,7 @@ def raise_work_orders(material_request): ) wo_order.set_work_order_operations() + wo_order.flags.ignore_mandatory = True wo_order.save() work_orders.append(wo_order.name) From e3d64fc5536e05b85f0db8f53079f90fb685c554 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Tue, 5 Sep 2023 16:32:58 +0530 Subject: [PATCH 280/501] fix: index error on Receivable report based on payment terms (#36963) fix: index error on Receivable report based on payment terms cr note's don't have payment terms. So, skip for them. (cherry picked from commit b9c556c4a9ac1102113c73ae9d92b5614bd603f2) Co-authored-by: ruthra kumar --- .../report/accounts_receivable/accounts_receivable.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/erpnext/accounts/report/accounts_receivable/accounts_receivable.py b/erpnext/accounts/report/accounts_receivable/accounts_receivable.py index 3700f00ee226..14f8993727a9 100755 --- a/erpnext/accounts/report/accounts_receivable/accounts_receivable.py +++ b/erpnext/accounts/report/accounts_receivable/accounts_receivable.py @@ -467,6 +467,10 @@ def get_payment_terms(self, row): original_row = frappe._dict(row) row.payment_terms = [] + # Cr Note's don't have Payment Terms + if not payment_terms_details: + return + # Advance allocated during invoicing is not considered in payment terms # Deduct that from paid amount pre allocation row.paid -= flt(payment_terms_details[0].total_advance) From dbeb132688b958f96531977861880641499b747e Mon Sep 17 00:00:00 2001 From: Gursheen Anand Date: Tue, 5 Sep 2023 17:33:30 +0530 Subject: [PATCH 281/501] refactor: use accounts mixin --- .../test_tds_payable_monthly.py | 25 ++++++++----------- 1 file changed, 10 insertions(+), 15 deletions(-) diff --git a/erpnext/accounts/report/tds_payable_monthly/test_tds_payable_monthly.py b/erpnext/accounts/report/tds_payable_monthly/test_tds_payable_monthly.py index d918d9e898b4..89ecef1904c0 100644 --- a/erpnext/accounts/report/tds_payable_monthly/test_tds_payable_monthly.py +++ b/erpnext/accounts/report/tds_payable_monthly/test_tds_payable_monthly.py @@ -13,14 +13,16 @@ create_tax_withholding_category, ) from erpnext.accounts.report.tds_payable_monthly.tds_payable_monthly import execute +from erpnext.accounts.test.accounts_mixin import AccountsTestMixin from erpnext.accounts.utils import get_fiscal_year -class TestTdsPayableMonthly(FrappeTestCase): +class TestTdsPayableMonthly(AccountsTestMixin, FrappeTestCase): def setUp(self): - delete_docs() + self.create_company() + self.clear_old_entries() create_tax_accounts() - create_tax_categories() + create_tcs_category() def test_tax_withholding_for_customers(self): si = create_sales_invoice(rate=1000) @@ -30,8 +32,8 @@ def test_tax_withholding_for_customers(self): ) result = execute(filters)[1] expected_values = [ - [pe.name, "TCS", 0.075, 2550, 0.9, 2550.9], - [si.name, "TCS", 0.075, 1000, 0.9, 1000.9], + [pe.name, "TCS", 0.075, 2550, 0.53, 2550.53], + [si.name, "TCS", 0.075, 1000, 0.53, 1000.53], ] self.check_expected_values(result, expected_values) @@ -47,14 +49,7 @@ def check_expected_values(self, result, expected_values): self.assertEqual(voucher.grand_total, voucher_expected_values[5]) def tearDown(self): - delete_docs() - - -def delete_docs(): - frappe.db.sql("delete from `tabSales Invoice` where company='_Test Company'") - frappe.db.sql("delete from `tabPurchase Invoice` where company='_Test Company'") - frappe.db.sql("delete from `tabGL Entry` where company='_Test Company'") - frappe.db.sql("delete from `tabPayment Entry` where company='_Test Company'") + self.clear_old_entries() def create_tax_accounts(): @@ -72,7 +67,7 @@ def create_tax_accounts(): ).insert(ignore_if_duplicate=True) -def create_tax_categories(): +def create_tcs_category(): fiscal_year = get_fiscal_year(today(), company="_Test Company") from_date = fiscal_year[1] to_date = fiscal_year[2] @@ -106,7 +101,7 @@ def create_tcs_payment_entry(): { "account_head": "TCS - _TC", "charge_type": "Actual", - "tax_amount": 0.9, + "tax_amount": 0.53, "add_deduct_tax": "Add", "description": "Test", "cost_center": "Main - _TC", From 58163d5aa8d7a24a942c78ae576f295db135364b Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Tue, 5 Sep 2023 18:14:44 +0530 Subject: [PATCH 282/501] fix: ask for asset related accounts only when needed (backport #36960) (#36971) fix: ask for asset related accounts only when needed (#36960) * fix: only ask for asset_received_but_not_billed account when needed * chore: remove unnecessary if condition * fix: only ask for expenses_included_in_asset_valuation account when needed (cherry picked from commit 174f95d699fde51185868864cdc74e792d2ff686) Co-authored-by: Anand Baburajan --- .../purchase_invoice/purchase_invoice.py | 94 ++++++++++--------- 1 file changed, 51 insertions(+), 43 deletions(-) diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py index 2340f486f145..9737ee2c53e0 100644 --- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py +++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py @@ -269,9 +269,7 @@ def set_expense_account(self, for_validate=False): stock_not_billed_account = self.get_company_default("stock_received_but_not_billed") stock_items = self.get_stock_items() - asset_items = [d.is_fixed_asset for d in self.items if d.is_fixed_asset] - if len(asset_items) > 0: - asset_received_but_not_billed = self.get_company_default("asset_received_but_not_billed") + asset_received_but_not_billed = None if self.update_stock: self.validate_item_code() @@ -365,6 +363,8 @@ def set_expense_account(self, for_validate=False): ) item.expense_account = asset_category_account elif item.is_fixed_asset and item.pr_detail: + if not asset_received_but_not_billed: + asset_received_but_not_billed = self.get_company_default("asset_received_but_not_billed") item.expense_account = asset_received_but_not_billed elif not item.expense_account and for_validate: throw(_("Expense account is mandatory for item {0}").format(item.item_code or item.item_name)) @@ -978,8 +978,9 @@ def make_item_gl_entries(self, gl_entries): ) def get_asset_gl_entry(self, gl_entries): - arbnb_account = self.get_company_default("asset_received_but_not_billed") - eiiav_account = self.get_company_default("expenses_included_in_asset_valuation") + arbnb_account = None + eiiav_account = None + asset_eiiav_currency = None for item in self.get("items"): if item.is_fixed_asset: @@ -991,6 +992,8 @@ def get_asset_gl_entry(self, gl_entries): "Asset Received But Not Billed", "Fixed Asset", ]: + if not arbnb_account: + arbnb_account = self.get_company_default("asset_received_but_not_billed") item.expense_account = arbnb_account if not self.update_stock: @@ -1013,7 +1016,10 @@ def get_asset_gl_entry(self, gl_entries): ) if item.item_tax_amount: - asset_eiiav_currency = get_account_currency(eiiav_account) + if not eiiav_account or not asset_eiiav_currency: + eiiav_account = self.get_company_default("expenses_included_in_asset_valuation") + asset_eiiav_currency = get_account_currency(eiiav_account) + gl_entries.append( self.get_gl_dict( { @@ -1056,7 +1062,10 @@ def get_asset_gl_entry(self, gl_entries): ) if item.item_tax_amount and not cint(erpnext.is_perpetual_inventory_enabled(self.company)): - asset_eiiav_currency = get_account_currency(eiiav_account) + if not eiiav_account or not asset_eiiav_currency: + eiiav_account = self.get_company_default("expenses_included_in_asset_valuation") + asset_eiiav_currency = get_account_currency(eiiav_account) + gl_entries.append( self.get_gl_dict( { @@ -1076,47 +1085,46 @@ def get_asset_gl_entry(self, gl_entries): ) ) - # When update stock is checked # Assets are bought through this document then it will be linked to this document - if self.update_stock: - if flt(item.landed_cost_voucher_amount): - gl_entries.append( - self.get_gl_dict( - { - "account": eiiav_account, - "against": cwip_account, - "cost_center": item.cost_center, - "remarks": self.get("remarks") or _("Accounting Entry for Stock"), - "credit": flt(item.landed_cost_voucher_amount), - "project": item.project or self.project, - }, - item=item, - ) - ) + if flt(item.landed_cost_voucher_amount): + if not eiiav_account: + eiiav_account = self.get_company_default("expenses_included_in_asset_valuation") - gl_entries.append( - self.get_gl_dict( - { - "account": cwip_account, - "against": eiiav_account, - "cost_center": item.cost_center, - "remarks": self.get("remarks") or _("Accounting Entry for Stock"), - "debit": flt(item.landed_cost_voucher_amount), - "project": item.project or self.project, - }, - item=item, - ) + gl_entries.append( + self.get_gl_dict( + { + "account": eiiav_account, + "against": cwip_account, + "cost_center": item.cost_center, + "remarks": self.get("remarks") or _("Accounting Entry for Stock"), + "credit": flt(item.landed_cost_voucher_amount), + "project": item.project or self.project, + }, + item=item, ) - - # update gross amount of assets bought through this document - assets = frappe.db.get_all( - "Asset", filters={"purchase_invoice": self.name, "item_code": item.item_code} ) - for asset in assets: - frappe.db.set_value("Asset", asset.name, "gross_purchase_amount", flt(item.valuation_rate)) - frappe.db.set_value( - "Asset", asset.name, "purchase_receipt_amount", flt(item.valuation_rate) + + gl_entries.append( + self.get_gl_dict( + { + "account": cwip_account, + "against": eiiav_account, + "cost_center": item.cost_center, + "remarks": self.get("remarks") or _("Accounting Entry for Stock"), + "debit": flt(item.landed_cost_voucher_amount), + "project": item.project or self.project, + }, + item=item, ) + ) + + # update gross amount of assets bought through this document + assets = frappe.db.get_all( + "Asset", filters={"purchase_invoice": self.name, "item_code": item.item_code} + ) + for asset in assets: + frappe.db.set_value("Asset", asset.name, "gross_purchase_amount", flt(item.valuation_rate)) + frappe.db.set_value("Asset", asset.name, "purchase_receipt_amount", flt(item.valuation_rate)) return gl_entries From fe69d5364dd03dd2e916e0c3385f3d4e6e0163b7 Mon Sep 17 00:00:00 2001 From: Dany Robert Date: Fri, 1 Sep 2023 04:58:59 +0000 Subject: [PATCH 283/501] fix: `company` is ambiguous (cherry picked from commit 3e1065a56164f8bb01d4361d378fa5d0a6130372) --- .../item_wise_purchase_register/item_wise_purchase_register.py | 2 +- .../report/item_wise_sales_register/item_wise_sales_register.py | 2 +- 2 files changed, 2 insertions(+), 2 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 050e6bc5d2f3..ce1a62d00659 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 @@ -287,7 +287,7 @@ def get_conditions(filters): conditions = "" for opts in ( - ("company", " and company=%(company)s"), + ("company", " and `tabPurchase Invoice`.company=%(company)s"), ("supplier", " and `tabPurchase Invoice`.supplier = %(supplier)s"), ("item_code", " and `tabPurchase Invoice Item`.item_code = %(item_code)s"), ("from_date", " and `tabPurchase Invoice`.posting_date>=%(from_date)s"), diff --git a/erpnext/accounts/report/item_wise_sales_register/item_wise_sales_register.py b/erpnext/accounts/report/item_wise_sales_register/item_wise_sales_register.py index 4d24dd907621..19bb449cd94d 100644 --- a/erpnext/accounts/report/item_wise_sales_register/item_wise_sales_register.py +++ b/erpnext/accounts/report/item_wise_sales_register/item_wise_sales_register.py @@ -332,7 +332,7 @@ def get_conditions(filters, additional_conditions=None): conditions = "" for opts in ( - ("company", " and company=%(company)s"), + ("company", " and `tabSales Invoice`.company=%(company)s"), ("customer", " and `tabSales Invoice`.customer = %(customer)s"), ("item_code", " and `tabSales Invoice Item`.item_code = %(item_code)s"), ("from_date", " and `tabSales Invoice`.posting_date>=%(from_date)s"), From a4b80d1ec4e2a27fed4d0d9a53e61f2e65e9227b Mon Sep 17 00:00:00 2001 From: Frappe PR Bot Date: Wed, 6 Sep 2023 05:57:04 +0000 Subject: [PATCH 284/501] chore(release): Bumped to Version 14.38.0 # [14.38.0](https://github.com/frappe/erpnext/compare/v14.37.1...v14.38.0) (2023-09-06) ### Bug Fixes * account payable currency and value ([#36859](https://github.com/frappe/erpnext/issues/36859)) ([98c2640](https://github.com/frappe/erpnext/commit/98c26403c1daeb95feacb004d11768da9550a235)) * added ignore_user_permissions to parent field of tree doctypes ([451cc7b](https://github.com/frappe/erpnext/commit/451cc7bc12cbbd6fb17afac05bf3871116d6415b)) * added validation for unique serial numbers in pos invoice ([#36302](https://github.com/frappe/erpnext/issues/36302)) ([1192736](https://github.com/frappe/erpnext/commit/119273639c14cc89bddc45f402f6c2d8c8abc8f3)) * allow payment_account of loan repayment to be edited ([#36948](https://github.com/frappe/erpnext/issues/36948)) ([09e2f24](https://github.com/frappe/erpnext/commit/09e2f24329a9c9f7b29f6987b02fca05fc9bc4c5)) * ask for asset related accounts only when needed (backport [#36960](https://github.com/frappe/erpnext/issues/36960)) ([#36971](https://github.com/frappe/erpnext/issues/36971)) ([58163d5](https://github.com/frappe/erpnext/commit/58163d5aa8d7a24a942c78ae576f295db135364b)) * calcuate received/paid amount on rate change in PE ([0a63266](https://github.com/frappe/erpnext/commit/0a632660e065a9a5642a2e624081ba60de72f3b0)) * deduplicate gain/loss JE creation for journals as payment ([9168b3b](https://github.com/frappe/erpnext/commit/9168b3b0e8be0afeb45a6c42534675a831b6e1c4)) * difference amount in UI should not be calculated ([9bc2b41](https://github.com/frappe/erpnext/commit/9bc2b419e3ba855d7bc180b9ea6c12253e1a6ee5)) * fetch discount amount for gle in base currency ([a8b5880](https://github.com/frappe/erpnext/commit/a8b58800bb5f411ddcb9a595f42fec47ac9bcd12)) * ignore mandatory fields while saving WO (backport [#36954](https://github.com/frappe/erpnext/issues/36954)) ([#36970](https://github.com/frappe/erpnext/issues/36970)) ([c125dea](https://github.com/frappe/erpnext/commit/c125dea0f1478b25ef1234b18764597931bff7d8)) * index error on Receivable report based on payment terms ([#36963](https://github.com/frappe/erpnext/issues/36963)) ([e3d64fc](https://github.com/frappe/erpnext/commit/e3d64fc5536e05b85f0db8f53079f90fb685c554)) * invalid gain/loss JE created on base currency Expense Claim ([068f1b5](https://github.com/frappe/erpnext/commit/068f1b5a6b15fdeed79d28a2e1db1c0d057afac9)) * only show "Unreconcile" if reconciled ([61752ac](https://github.com/frappe/erpnext/commit/61752ac2b49c5e6ecaf720f87e63d53fb5c5d5e2)) * prorate factor for subscription plan ([#36953](https://github.com/frappe/erpnext/issues/36953)) ([fc79af5](https://github.com/frappe/erpnext/commit/fc79af592648912fe25a64802370eb5389de754a)) * reduce threshold for bg job fn ([5a226a8](https://github.com/frappe/erpnext/commit/5a226a8395d1d6c51917ee06584c6bfd0467cb16)) * remove withholding category from common fields ([18f8f7f](https://github.com/frappe/erpnext/commit/18f8f7f09cefa3c4b102c2a0ce6a90476d1e7d16)) * Set the default filter in All Trends Report ([132957f](https://github.com/frappe/erpnext/commit/132957f59e096f1ac049f6c448cbeb20fe33acd6)) * Set the default filter in All Trends Report ([420536c](https://github.com/frappe/erpnext/commit/420536ca522df9bfd99d7e6298a5b719cee40d09)) * when create doc from item dashboard default uom (buying or selling) is not correctly selected (backport [#36892](https://github.com/frappe/erpnext/issues/36892)) ([#36928](https://github.com/frappe/erpnext/issues/36928)) ([5c8bee0](https://github.com/frappe/erpnext/commit/5c8bee0a9592f61939aad47d601ef16eac65d63a)) ### Features * **RFQ:** optionally send document print ([#36363](https://github.com/frappe/erpnext/issues/36363)) ([345c608](https://github.com/frappe/erpnext/commit/345c6084e5461bd43ed7ef7aa7a66b50831f0fab)) --- erpnext/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/__init__.py b/erpnext/__init__.py index 5d2189e7c11f..4734e8cc0b97 100644 --- a/erpnext/__init__.py +++ b/erpnext/__init__.py @@ -3,7 +3,7 @@ import frappe -__version__ = "14.37.1" +__version__ = "14.38.0" def get_default_company(user=None): From 4fede56d98f803ee4912fa80cea386135c378d2c Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Wed, 6 Sep 2023 06:45:58 +0000 Subject: [PATCH 285/501] fix: use primary key for link lookup (backport #36919) (#36978) fix: use primary key for link lookup (#36919) (cherry picked from commit 8ce6b8179e1e583a63dc47bd430dd7b9f1dd23aa) Co-authored-by: Devin Slauenwhite --- erpnext/stock/report/stock_balance/stock_balance.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/erpnext/stock/report/stock_balance/stock_balance.py b/erpnext/stock/report/stock_balance/stock_balance.py index 7e81a72028f4..80bf8508cf31 100644 --- a/erpnext/stock/report/stock_balance/stock_balance.py +++ b/erpnext/stock/report/stock_balance/stock_balance.py @@ -323,8 +323,10 @@ def apply_items_filters(self, query, item_table) -> str: for field in ["item_code", "brand"]: if not self.filters.get(field): continue - - query = query.where(item_table[field] == self.filters.get(field)) + elif field == "item_code": + query = query.where(item_table.name == self.filters.get(field)) + else: + query = query.where(item_table[field] == self.filters.get(field)) return query From f251d6cb699cf403e559e37276ebcca01498291e Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Wed, 6 Sep 2023 15:18:21 +0530 Subject: [PATCH 286/501] fix: Update party type for payroll payable account --- .../doctype/loan_repayment/loan_repayment.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/erpnext/loan_management/doctype/loan_repayment/loan_repayment.py b/erpnext/loan_management/doctype/loan_repayment/loan_repayment.py index d7e11aafa81c..c4bacda4321d 100644 --- a/erpnext/loan_management/doctype/loan_repayment/loan_repayment.py +++ b/erpnext/loan_management/doctype/loan_repayment/loan_repayment.py @@ -405,6 +405,16 @@ def make_gl_entries(self, cancel=0, adv_adj=0): else: payment_account = self.payment_account + payment_party_type = "" + payment_party = "" + + if ( + hasattr(self, "process_payroll_accounting_entry_based_on_employee") + and self.process_payroll_accounting_entry_based_on_employee + ): + payment_party_type = "Employee" + payment_party = self.applicant + if self.total_penalty_paid: gle_map.append( self.get_gl_dict( @@ -452,6 +462,8 @@ def make_gl_entries(self, cancel=0, adv_adj=0): "remarks": _(remarks), "cost_center": self.cost_center, "posting_date": getdate(self.posting_date), + "party_type": payment_party_type, + "party": payment_party, } ) ) From e210b28f0d2fbac90faaa92d0ca9645545b0637b Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Wed, 6 Sep 2023 15:39:46 +0530 Subject: [PATCH 287/501] chore: asset finance books validation (backport #36979) (#36981) * chore: asset finance books validation (#36979) (cherry picked from commit 0077659e931063653af415b9857b4f61bfcf227d) * chore: fix tests --------- Co-authored-by: Anand Baburajan --- erpnext/assets/doctype/asset/asset.py | 22 ++++++++++++++++++++++ erpnext/assets/doctype/asset/test_asset.py | 14 ++++++++++++++ 2 files changed, 36 insertions(+) diff --git a/erpnext/assets/doctype/asset/asset.py b/erpnext/assets/doctype/asset/asset.py index 34d5430210c5..2647ed7b895b 100644 --- a/erpnext/assets/doctype/asset/asset.py +++ b/erpnext/assets/doctype/asset/asset.py @@ -40,6 +40,7 @@ def validate(self): self.validate_item() self.validate_cost_center() self.set_missing_values() + self.validate_finance_books() if not self.split_from: self.prepare_depreciation_data() self.validate_gross_and_purchase_amount() @@ -206,6 +207,27 @@ def set_missing_values(self): finance_books = get_item_details(self.item_code, self.asset_category) self.set("finance_books", finance_books) + def validate_finance_books(self): + if not self.calculate_depreciation or len(self.finance_books) == 1: + return + + finance_books = set() + + for d in self.finance_books: + if d.finance_book in finance_books: + frappe.throw( + _("Row #{}: Please use a different Finance Book.").format(d.idx), + title=_("Duplicate Finance Book"), + ) + else: + finance_books.add(d.finance_book) + + if not d.finance_book: + frappe.throw( + _("Row #{}: Finance Book should not be empty since you're using multiple.").format(d.idx), + title=_("Missing Finance Book"), + ) + def validate_asset_values(self): if not self.asset_category: self.asset_category = frappe.get_cached_value("Item", self.item_code, "asset_category") diff --git a/erpnext/assets/doctype/asset/test_asset.py b/erpnext/assets/doctype/asset/test_asset.py index a2826d929b80..87e74712d5b5 100644 --- a/erpnext/assets/doctype/asset/test_asset.py +++ b/erpnext/assets/doctype/asset/test_asset.py @@ -1332,6 +1332,7 @@ def test_clear_depreciation_schedule_for_multiple_finance_books(self): asset.append( "finance_books", { + "finance_book": "Test Finance Book 1", "depreciation_method": "Straight Line", "frequency_of_depreciation": 1, "total_number_of_depreciations": 3, @@ -1342,6 +1343,7 @@ def test_clear_depreciation_schedule_for_multiple_finance_books(self): asset.append( "finance_books", { + "finance_book": "Test Finance Book 2", "depreciation_method": "Straight Line", "frequency_of_depreciation": 1, "total_number_of_depreciations": 6, @@ -1352,6 +1354,7 @@ def test_clear_depreciation_schedule_for_multiple_finance_books(self): asset.append( "finance_books", { + "finance_book": "Test Finance Book 3", "depreciation_method": "Straight Line", "frequency_of_depreciation": 12, "total_number_of_depreciations": 3, @@ -1381,6 +1384,7 @@ def test_depreciation_schedules_are_set_up_for_multiple_finance_books(self): asset.append( "finance_books", { + "finance_book": "Test Finance Book 1", "depreciation_method": "Straight Line", "frequency_of_depreciation": 12, "total_number_of_depreciations": 3, @@ -1391,6 +1395,7 @@ def test_depreciation_schedules_are_set_up_for_multiple_finance_books(self): asset.append( "finance_books", { + "finance_book": "Test Finance Book 2", "depreciation_method": "Straight Line", "frequency_of_depreciation": 12, "total_number_of_depreciations": 6, @@ -1647,6 +1652,15 @@ def create_asset_data(): if not frappe.db.exists("Location", "Test Location"): frappe.get_doc({"doctype": "Location", "location_name": "Test Location"}).insert() + if not frappe.db.exists("Finance Book", "Test Finance Book 1"): + frappe.get_doc({"doctype": "Finance Book", "finance_book_name": "Test Finance Book 1"}).insert() + + if not frappe.db.exists("Finance Book", "Test Finance Book 2"): + frappe.get_doc({"doctype": "Finance Book", "finance_book_name": "Test Finance Book 2"}).insert() + + if not frappe.db.exists("Finance Book", "Test Finance Book 3"): + frappe.get_doc({"doctype": "Finance Book", "finance_book_name": "Test Finance Book 3"}).insert() + def create_asset(**args): args = frappe._dict(args) From 1894371b684cefba1210d333a9adce1bdc1a8029 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Wed, 6 Sep 2023 16:29:12 +0530 Subject: [PATCH 288/501] chore: Update function --- .../loan_management/doctype/loan_repayment/loan_repayment.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/erpnext/loan_management/doctype/loan_repayment/loan_repayment.py b/erpnext/loan_management/doctype/loan_repayment/loan_repayment.py index c4bacda4321d..4171ca683057 100644 --- a/erpnext/loan_management/doctype/loan_repayment/loan_repayment.py +++ b/erpnext/loan_management/doctype/loan_repayment/loan_repayment.py @@ -502,6 +502,7 @@ def create_repayment_entry( amount_paid, penalty_amount=None, payroll_payable_account=None, + process_payroll_accounting_entry_based_on_employee=0, ): lr = frappe.get_doc( @@ -518,6 +519,7 @@ def create_repayment_entry( "amount_paid": amount_paid, "loan_type": loan_type, "payroll_payable_account": payroll_payable_account, + "process_payroll_accounting_entry_based_on_employee": process_payroll_accounting_entry_based_on_employee, } ).insert() From 24a481525019c9ed91eb47b919df9c855c56d843 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Wed, 6 Sep 2023 13:19:00 +0530 Subject: [PATCH 289/501] chore: Update employee for tests (cherry picked from commit ae01d70b337795215c8738341780dbe24d275c46) --- erpnext/setup/doctype/employee/test_employee.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/erpnext/setup/doctype/employee/test_employee.py b/erpnext/setup/doctype/employee/test_employee.py index 071c336326f5..5a693c5effb9 100644 --- a/erpnext/setup/doctype/employee/test_employee.py +++ b/erpnext/setup/doctype/employee/test_employee.py @@ -66,5 +66,8 @@ def make_employee(user, company=None, **kwargs): employee.insert() return employee.name else: - frappe.db.set_value("Employee", {"employee_name": user}, "status", "Active") - return frappe.get_value("Employee", {"employee_name": user}, "name") + employee = frappe.get_doc("Employee", {"employee_name": user}) + employee.update(kwargs) + employee.status = "Active" + employee.save() + return employee.name From 2ae4463b7692704a214fceec5d67cd6c32b67af3 Mon Sep 17 00:00:00 2001 From: Anand Baburajan Date: Wed, 6 Sep 2023 22:20:29 +0530 Subject: [PATCH 290/501] fix: correct asset daily depr schedule calculation [v14] (#36991) fix: correct asset daily depr schedule calculation --- erpnext/assets/doctype/asset/asset.py | 76 +++++++++++++++------- erpnext/assets/doctype/asset/test_asset.py | 24 +++---- 2 files changed, 63 insertions(+), 37 deletions(-) diff --git a/erpnext/assets/doctype/asset/asset.py b/erpnext/assets/doctype/asset/asset.py index 2647ed7b895b..e72ddcb12a00 100644 --- a/erpnext/assets/doctype/asset/asset.py +++ b/erpnext/assets/doctype/asset/asset.py @@ -1385,26 +1385,41 @@ def get_straight_line_or_manual_depr_amount( daily_depr_amount = ( flt(row.value_after_depreciation) - flt(row.expected_value_after_useful_life) ) / date_diff( - add_months( - row.depreciation_start_date, - flt(row.total_number_of_depreciations - asset.number_of_depreciations_booked) - * row.frequency_of_depreciation, - ), - add_months( - row.depreciation_start_date, - flt( - row.total_number_of_depreciations - - asset.number_of_depreciations_booked - - number_of_pending_depreciations + get_last_day( + add_months( + row.depreciation_start_date, + flt(row.total_number_of_depreciations - asset.number_of_depreciations_booked - 1) + * row.frequency_of_depreciation, ) - * row.frequency_of_depreciation, ), + add_days( + get_last_day( + add_months( + row.depreciation_start_date, + flt( + row.total_number_of_depreciations + - asset.number_of_depreciations_booked + - number_of_pending_depreciations + - 1 + ) + * row.frequency_of_depreciation, + ) + ), + 1, + ), + ) + + to_date = get_last_day( + add_months(row.depreciation_start_date, schedule_idx * row.frequency_of_depreciation) ) - to_date = add_months(row.depreciation_start_date, schedule_idx * row.frequency_of_depreciation) - from_date = add_months( - row.depreciation_start_date, (schedule_idx - 1) * row.frequency_of_depreciation + from_date = add_days( + get_last_day( + add_months(row.depreciation_start_date, (schedule_idx - 1) * row.frequency_of_depreciation) + ), + 1, ) - return daily_depr_amount * date_diff(to_date, from_date) + + return daily_depr_amount * (date_diff(to_date, from_date) + 1) else: return ( flt(row.value_after_depreciation) - flt(row.expected_value_after_useful_life) @@ -1417,18 +1432,29 @@ def get_straight_line_or_manual_depr_amount( - flt(asset.opening_accumulated_depreciation) - flt(row.expected_value_after_useful_life) ) / date_diff( - add_months( - row.depreciation_start_date, - flt(row.total_number_of_depreciations - asset.number_of_depreciations_booked) - * row.frequency_of_depreciation, + get_last_day( + add_months( + row.depreciation_start_date, + flt(row.total_number_of_depreciations - asset.number_of_depreciations_booked - 1) + * row.frequency_of_depreciation, + ) + ), + add_days( + get_last_day(add_months(row.depreciation_start_date, -1 * row.frequency_of_depreciation)), 1 ), - row.depreciation_start_date, ) - to_date = add_months(row.depreciation_start_date, schedule_idx * row.frequency_of_depreciation) - from_date = add_months( - row.depreciation_start_date, (schedule_idx - 1) * row.frequency_of_depreciation + + to_date = get_last_day( + add_months(row.depreciation_start_date, schedule_idx * row.frequency_of_depreciation) ) - return daily_depr_amount * date_diff(to_date, from_date) + from_date = add_days( + get_last_day( + add_months(row.depreciation_start_date, (schedule_idx - 1) * row.frequency_of_depreciation) + ), + 1, + ) + + return daily_depr_amount * (date_diff(to_date, from_date) + 1) else: return ( flt(asset.gross_purchase_amount) diff --git a/erpnext/assets/doctype/asset/test_asset.py b/erpnext/assets/doctype/asset/test_asset.py index 87e74712d5b5..1adbeed65a5d 100644 --- a/erpnext/assets/doctype/asset/test_asset.py +++ b/erpnext/assets/doctype/asset/test_asset.py @@ -743,18 +743,18 @@ def test_schedule_for_straight_line_method_with_daily_depreciation(self): ) expected_schedules = [ - ["2023-01-31", 1019.18, 1019.18], - ["2023-02-28", 920.55, 1939.73], - ["2023-03-31", 1019.18, 2958.91], - ["2023-04-30", 986.3, 3945.21], - ["2023-05-31", 1019.18, 4964.39], - ["2023-06-30", 986.3, 5950.69], - ["2023-07-31", 1019.18, 6969.87], - ["2023-08-31", 1019.18, 7989.05], - ["2023-09-30", 986.3, 8975.35], - ["2023-10-31", 1019.18, 9994.53], - ["2023-11-30", 986.3, 10980.83], - ["2023-12-31", 1019.17, 12000.0], + ["2023-01-31", 1021.98, 1021.98], + ["2023-02-28", 923.08, 1945.06], + ["2023-03-31", 1021.98, 2967.04], + ["2023-04-30", 989.01, 3956.05], + ["2023-05-31", 1021.98, 4978.03], + ["2023-06-30", 989.01, 5967.04], + ["2023-07-31", 1021.98, 6989.02], + ["2023-08-31", 1021.98, 8011.0], + ["2023-09-30", 989.01, 9000.01], + ["2023-10-31", 1021.98, 10021.99], + ["2023-11-30", 989.01, 11011.0], + ["2023-12-31", 989.0, 12000.0], ] schedules = [ From 2077b2cde413a66347b3b72e201a2702d7f6c0b6 Mon Sep 17 00:00:00 2001 From: Gursheen Anand Date: Mon, 28 Aug 2023 13:09:51 +0530 Subject: [PATCH 291/501] fix: show letterhead and terms for AR pdf (cherry picked from commit 0a9187ea422f2312ffd9508f1f3c6a120c228f71) --- ...ement_of_accounts_accounts_receivable.html | 22 ++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts_accounts_receivable.html b/erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts_accounts_receivable.html index 259526f8c43a..647600a9fea0 100644 --- a/erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts_accounts_receivable.html +++ b/erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts_accounts_receivable.html @@ -8,9 +8,24 @@ } +
+ {% if letter_head.content %} +
{{ letter_head.content }}
+
+ {% endif %} +
+ +

{{ _(report.report_name) }}

- {{ filters.customer }} + {{ filters.customer_name }}

{% if (filters.tax_id) %} @@ -341,4 +356,9 @@

{{ _("Ageing Report based on ") }} {{ ageing.ageing_base {% endif %} + {% if terms_and_conditions %} +
+ {{ terms_and_conditions }} +
+ {% endif %}

{{ _("Printed On ") }}{{ frappe.utils.now() }}

From 657ca7ff22602ab34acc25c3c2a2d7bcd2ba25e9 Mon Sep 17 00:00:00 2001 From: Gursheen Anand Date: Mon, 28 Aug 2023 13:11:28 +0530 Subject: [PATCH 292/501] feat: add field for specifying pdf name (cherry picked from commit 5c2a949593727ec6c9f84f8b801035e4da01558d) # Conflicts: # erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.py --- .../process_statement_of_accounts.json | 10 ++++++++-- .../process_statement_of_accounts.py | 19 +++++++++++++++++-- 2 files changed, 25 insertions(+), 4 deletions(-) diff --git a/erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.json b/erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.json index 45373741f39c..e711ae0de2bb 100644 --- a/erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.json +++ b/erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.json @@ -49,6 +49,7 @@ "column_break_21", "start_date", "section_break_33", + "pdf_name", "subject", "column_break_28", "cc_to", @@ -273,7 +274,7 @@ "fieldname": "help_text", "fieldtype": "HTML", "label": "Help Text", - "options": "
\n

Note

\n
    \n
  • \nYou can use Jinja tags in Subject and Body fields for dynamic values.\n
  • \n All fields in this doctype are available under the doc object and all fields for the customer to whom the mail will go to is available under the customer object.\n
\n

Examples

\n\n
    \n
  • Subject:

    Statement Of Accounts for {{ customer.name }}

  • \n
  • Body:

    \n
    Hello {{ customer.name }},
    PFA your Statement Of Accounts from {{ doc.from_date }} to {{ doc.to_date }}.
  • \n
\n" + "options": "
\n

Note

\n
    \n
  • \nYou can use Jinja tags in Subject and Body fields for dynamic values.\n
  • \n All fields in this doctype are available under the doc object and all fields for the customer to whom the mail will go to is available under the customer object.\n
\n

Examples

\n\n
    \n
  • Subject:

    Statement Of Accounts for {{ customer.customer_name }}

  • \n
  • Body:

    \n
    Hello {{ customer.customer_name }},
    PFA your Statement Of Accounts from {{ doc.from_date }} to {{ doc.to_date }}.
  • \n
\n" }, { "fieldname": "subject", @@ -368,10 +369,15 @@ "fieldname": "based_on_payment_terms", "fieldtype": "Check", "label": "Based On Payment Terms" + }, + { + "fieldname": "pdf_name", + "fieldtype": "Data", + "label": "PDF Name" } ], "links": [], - "modified": "2023-06-23 10:13:15.051950", + "modified": "2023-08-28 12:59:53.071334", "modified_by": "Administrator", "module": "Accounts", "name": "Process Statement Of Accounts", diff --git a/erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.py b/erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.py index 6cd601f663d3..b9ea21429589 100644 --- a/erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.py +++ b/erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.py @@ -26,7 +26,13 @@ def validate(self): if not self.subject: self.subject = "Statement Of Accounts for {{ customer.customer_name }}" if not self.body: - self.body = "Hello {{ customer.name }},
PFA your Statement Of Accounts from {{ doc.from_date }} to {{ doc.to_date }}." + if self.report == "General Ledger": + body_str = " from {{ doc.from_date }} to {{ doc.to_date }}." + else: + body_str = " until {{ doc.posting_date }}." + self.body = "Hello {{ customer.customer_name }},
PFA your Statement Of Accounts" + body_str + if not self.pdf_name: + self.pdf_name = "{{ customer.customer_name }}" validate_template(self.subject) validate_template(self.body) @@ -139,6 +145,7 @@ def get_ar_filters(doc, entry): return { "report_date": doc.posting_date if doc.posting_date else None, "customer": entry.customer, + "customer_name": entry.customer_name if entry.customer_name else None, "payment_terms_template": doc.payment_terms_template if doc.payment_terms_template else None, "sales_partner": doc.sales_partner if doc.sales_partner else None, "sales_person": doc.sales_person if doc.sales_person else None, @@ -368,10 +375,18 @@ def send_emails(document_name, from_scheduler=False): if report: for customer, report_pdf in report.items(): - attachments = [{"fname": customer + ".pdf", "fcontent": report_pdf}] + context = get_context(customer, doc) + filename = frappe.render_template(doc.pdf_name, context) + attachments = [{"fname": filename + ".pdf", "fcontent": report_pdf}] recipients, cc = get_recipients_and_cc(customer, doc) +<<<<<<< HEAD context = get_context(customer, doc) +======= + if not recipients: + continue + +>>>>>>> 5c2a949593 (feat: add field for specifying pdf name) subject = frappe.render_template(doc.subject, context) message = frappe.render_template(doc.body, context) From 53270dd933d1e1ab22787de4ecbab6894810c5e8 Mon Sep 17 00:00:00 2001 From: Gursheen Anand Date: Mon, 28 Aug 2023 17:14:15 +0530 Subject: [PATCH 293/501] fix: generate pdf only when result exists (cherry picked from commit f07f4ce86f5963d03ea41ab699df2c4ce53ef2c5) --- .../process_statement_of_accounts.py | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.py b/erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.py index b9ea21429589..6c8afbecb5fe 100644 --- a/erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.py +++ b/erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.py @@ -63,11 +63,6 @@ def get_report_pdf(doc, consolidated=True): filters = get_common_filters(doc) - if doc.report == "General Ledger": - filters.update(get_gl_filters(doc, entry, tax_id, presentation_currency)) - else: - filters.update(get_ar_filters(doc, entry)) - if doc.report == "General Ledger": col, res = get_soa(filters) for x in [0, -2, -1]: @@ -75,8 +70,11 @@ def get_report_pdf(doc, consolidated=True): if len(res) == 3: continue else: + filters.update(get_ar_filters(doc, entry)) ar_res = get_ar_soa(filters) col, res = ar_res[0], ar_res[1] + if not res: + continue statement_dict[entry.customer] = get_html(doc, filters, entry, col, res, ageing) From f9f1ac3601a8ddb3c6c56bdc97d3b718f7c04d51 Mon Sep 17 00:00:00 2001 From: Gursheen Anand Date: Tue, 29 Aug 2023 14:51:35 +0530 Subject: [PATCH 294/501] test: auto email for ar report (cherry picked from commit a006b66e45c1b0c192674ad522b05f5565513e35) --- .../process_statement_of_accounts.py | 9 +++-- .../test_process_statement_of_accounts.py | 37 ++++++++++++++++++- 2 files changed, 41 insertions(+), 5 deletions(-) diff --git a/erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.py b/erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.py index 6c8afbecb5fe..ef6ba3e3ab1f 100644 --- a/erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.py +++ b/erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.py @@ -367,7 +367,7 @@ def download_statements(document_name): @frappe.whitelist() -def send_emails(document_name, from_scheduler=False): +def send_emails(document_name, from_scheduler=False, posting_date=None): doc = frappe.get_doc("Process Statement Of Accounts", document_name) report = get_report_pdf(doc, consolidated=False) @@ -403,7 +403,7 @@ def send_emails(document_name, from_scheduler=False): ) if doc.enable_auto_email and from_scheduler: - new_to_date = getdate(today()) + new_to_date = getdate(posting_date or today()) if doc.frequency == "Weekly": new_to_date = add_days(new_to_date, 7) else: @@ -414,6 +414,8 @@ def send_emails(document_name, from_scheduler=False): ) doc.db_set("to_date", new_to_date, commit=True) doc.db_set("from_date", new_from_date, commit=True) + doc.db_set("posting_date", new_to_date, commit=True) + doc.db_set("report", doc.report, commit=True) return True else: return False @@ -423,7 +425,8 @@ def send_emails(document_name, from_scheduler=False): def send_auto_email(): selected = frappe.get_list( "Process Statement Of Accounts", - filters={"to_date": format_date(today()), "enable_auto_email": 1}, + filters={"enable_auto_email": 1}, + or_filters={"to_date": format_date(today()), "posting_date": format_date(today())}, ) for entry in selected: send_emails(entry.name, from_scheduler=True) diff --git a/erpnext/accounts/doctype/process_statement_of_accounts/test_process_statement_of_accounts.py b/erpnext/accounts/doctype/process_statement_of_accounts/test_process_statement_of_accounts.py index c281040aaf2d..fb0d8d152f00 100644 --- a/erpnext/accounts/doctype/process_statement_of_accounts/test_process_statement_of_accounts.py +++ b/erpnext/accounts/doctype/process_statement_of_accounts/test_process_statement_of_accounts.py @@ -1,9 +1,42 @@ # Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and Contributors # See license.txt -# import frappe import unittest +import frappe +from frappe.utils import add_days, getdate, today + +from erpnext.accounts.doctype.process_statement_of_accounts.process_statement_of_accounts import ( + send_emails, +) +from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice + class TestProcessStatementOfAccounts(unittest.TestCase): - pass + def setUp(self): + self.si = create_sales_invoice() + self.process_soa = create_process_soa() + + def test_auto_email_for_process_soa_ar(self): + send_emails(self.process_soa.name, from_scheduler=True) + self.process_soa.load_from_db() + self.assertEqual(self.process_soa.posting_date, getdate(add_days(today(), 7))) + + def tearDown(self): + frappe.delete_doc_if_exists("Process Statement Of Accounts", "Test Process SOA") + + +def create_process_soa(): + frappe.delete_doc_if_exists("Process Statement Of Accounts", "Test Process SOA") + process_soa = frappe.new_doc("Process Statement Of Accounts") + soa_dict = { + "name": "Test Process SOA", + "company": "_Test Company", + } + process_soa.update(soa_dict) + process_soa.set("customers", [{"customer": "_Test Customer"}]) + process_soa.enable_auto_email = 1 + process_soa.frequency = "Weekly" + process_soa.report = "Accounts Receivable" + process_soa.save() + return process_soa From 284181d766e4b321e0fc642b3230c1ce86338e37 Mon Sep 17 00:00:00 2001 From: Gursheen Anand Date: Tue, 29 Aug 2023 16:16:50 +0530 Subject: [PATCH 295/501] fix: remove report field db set (cherry picked from commit 060da2c5bc47300d8e7103af63626d0b53ce2807) --- .../process_statement_of_accounts.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.py b/erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.py index ef6ba3e3ab1f..21708ab4ec6e 100644 --- a/erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.py +++ b/erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.py @@ -412,10 +412,11 @@ def send_emails(document_name, from_scheduler=False, posting_date=None): doc.add_comment( "Comment", "Emails sent on: " + frappe.utils.format_datetime(frappe.utils.now()) ) - doc.db_set("to_date", new_to_date, commit=True) - doc.db_set("from_date", new_from_date, commit=True) - doc.db_set("posting_date", new_to_date, commit=True) - doc.db_set("report", doc.report, commit=True) + if doc.report == "General Ledger": + doc.db_set("to_date", new_to_date, commit=True) + doc.db_set("from_date", new_from_date, commit=True) + else: + doc.db_set("posting_date", new_to_date, commit=True) return True else: return False From acd9c692017d811f668e300fb0fedfb093df6458 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Sun, 10 Sep 2023 17:26:41 +0530 Subject: [PATCH 296/501] feat: Add half-yearly asset maintenance periodicity. (backport #37006) (#37014) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit feat: Add half-yearly asset maintenance periodicity. (#37006) (cherry picked from commit 846ae32d922bbff8a1a02719b615a36a1eaf1eaa) Co-authored-by: Bernd Oliver Sünderhauf <46800703+bosue@users.noreply.github.com> --- .../assets/doctype/asset_maintenance/asset_maintenance.py | 6 ++++-- .../asset_maintenance_task/asset_maintenance_task.json | 4 ++-- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/erpnext/assets/doctype/asset_maintenance/asset_maintenance.py b/erpnext/assets/doctype/asset_maintenance/asset_maintenance.py index 83031415ec3d..5c40072086ea 100644 --- a/erpnext/assets/doctype/asset_maintenance/asset_maintenance.py +++ b/erpnext/assets/doctype/asset_maintenance/asset_maintenance.py @@ -80,14 +80,16 @@ def calculate_next_due_date( next_due_date = add_days(start_date, 7) if periodicity == "Monthly": next_due_date = add_months(start_date, 1) + if periodicity == "Quarterly": + next_due_date = add_months(start_date, 3) + if periodicity == "Half-yearly": + next_due_date = add_months(start_date, 6) if periodicity == "Yearly": next_due_date = add_years(start_date, 1) if periodicity == "2 Yearly": next_due_date = add_years(start_date, 2) if periodicity == "3 Yearly": next_due_date = add_years(start_date, 3) - if periodicity == "Quarterly": - next_due_date = add_months(start_date, 3) if end_date and ( (start_date and start_date >= end_date) or (last_completion_date and last_completion_date >= end_date) diff --git a/erpnext/assets/doctype/asset_maintenance_task/asset_maintenance_task.json b/erpnext/assets/doctype/asset_maintenance_task/asset_maintenance_task.json index b7cb23e66878..80d90c63473c 100644 --- a/erpnext/assets/doctype/asset_maintenance_task/asset_maintenance_task.json +++ b/erpnext/assets/doctype/asset_maintenance_task/asset_maintenance_task.json @@ -71,7 +71,7 @@ "fieldtype": "Select", "in_list_view": 1, "label": "Periodicity", - "options": "\nDaily\nWeekly\nMonthly\nQuarterly\nYearly\n2 Yearly\n3 Yearly", + "options": "\nDaily\nWeekly\nMonthly\nQuarterly\nHalf-yearly\nYearly\n2 Yearly\n3 Yearly", "reqd": 1 }, { @@ -153,4 +153,4 @@ "quick_entry": 1, "sort_field": "modified", "sort_order": "DESC" -} \ No newline at end of file +} From d9f15e96b55b674de3e3aba96c1307fa49257760 Mon Sep 17 00:00:00 2001 From: Florian HENRY Date: Sun, 10 Sep 2023 17:18:42 +0200 Subject: [PATCH 297/501] refactor(region): Splitting of France Regional logic from ERPNext --- erpnext/patches/v14_0/france_depreciation_warning.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) create mode 100644 erpnext/patches/v14_0/france_depreciation_warning.py diff --git a/erpnext/patches/v14_0/france_depreciation_warning.py b/erpnext/patches/v14_0/france_depreciation_warning.py new file mode 100644 index 000000000000..8d8719f8623a --- /dev/null +++ b/erpnext/patches/v14_0/france_depreciation_warning.py @@ -0,0 +1,12 @@ +import click +import frappe + + +def execute(): + if "erpnext_france" in frappe.get_installed_apps(): + return + click.secho( + "Feature for Feature Region will be remove in version-15 and moved to a separate app\n" + "Please install the app to continue using the regionnal France features: git@github.com:scopen-coop/erpnext_france.git", + fg="yellow", + ) From fb7ca8fca2a5a24b73435ee22b261e53d1a2c174 Mon Sep 17 00:00:00 2001 From: Florian HENRY Date: Sun, 10 Sep 2023 17:19:35 +0200 Subject: [PATCH 298/501] refactor(region): Splitting of France Regional logic from ERPNext --- erpnext/patches/v14_0/france_depreciation_warning.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/patches/v14_0/france_depreciation_warning.py b/erpnext/patches/v14_0/france_depreciation_warning.py index 8d8719f8623a..45ec07e77375 100644 --- a/erpnext/patches/v14_0/france_depreciation_warning.py +++ b/erpnext/patches/v14_0/france_depreciation_warning.py @@ -7,6 +7,6 @@ def execute(): return click.secho( "Feature for Feature Region will be remove in version-15 and moved to a separate app\n" - "Please install the app to continue using the regionnal France features: git@github.com:scopen-coop/erpnext_france.git", + "Please install the app to continue using the regionnal France features: https://github.com/scopen-coop/erpnext_france.git", fg="yellow", ) From 7de3d08ce39e4722495c49756579ea9461a92935 Mon Sep 17 00:00:00 2001 From: Florian HENRY Date: Sun, 10 Sep 2023 17:23:35 +0200 Subject: [PATCH 299/501] refactor(region): Splitting of France Regional logic from ERPNext --- erpnext/patches/v14_0/france_depreciation_warning.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/patches/v14_0/france_depreciation_warning.py b/erpnext/patches/v14_0/france_depreciation_warning.py index 45ec07e77375..27acfd947038 100644 --- a/erpnext/patches/v14_0/france_depreciation_warning.py +++ b/erpnext/patches/v14_0/france_depreciation_warning.py @@ -6,7 +6,7 @@ def execute(): if "erpnext_france" in frappe.get_installed_apps(): return click.secho( - "Feature for Feature Region will be remove in version-15 and moved to a separate app\n" + "Feature for Region France will be remove in version-15 and moved to a separate app\n" "Please install the app to continue using the regionnal France features: https://github.com/scopen-coop/erpnext_france.git", fg="yellow", ) From 5c0a232e0fc4c6cb67fbdb4b76747fbd0878b109 Mon Sep 17 00:00:00 2001 From: Florian HENRY Date: Sun, 10 Sep 2023 17:24:01 +0200 Subject: [PATCH 300/501] refactor(region): Splitting of France Regional logic from ERPNext --- erpnext/patches/v14_0/france_depreciation_warning.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/patches/v14_0/france_depreciation_warning.py b/erpnext/patches/v14_0/france_depreciation_warning.py index 27acfd947038..5efbc5bd7fbb 100644 --- a/erpnext/patches/v14_0/france_depreciation_warning.py +++ b/erpnext/patches/v14_0/france_depreciation_warning.py @@ -6,7 +6,7 @@ def execute(): if "erpnext_france" in frappe.get_installed_apps(): return click.secho( - "Feature for Region France will be remove in version-15 and moved to a separate app\n" + "Feature for region France will be remove in version-15 and moved to a separate app\n" "Please install the app to continue using the regionnal France features: https://github.com/scopen-coop/erpnext_france.git", fg="yellow", ) From 619644af04a44b38c0c4f5160b3c897ea2535181 Mon Sep 17 00:00:00 2001 From: Gursheen Kaur Anand <40693548+GursheenK@users.noreply.github.com> Date: Mon, 11 Sep 2023 11:25:30 +0530 Subject: [PATCH 301/501] chore: resolve conflicts --- .../process_statement_of_accounts.py | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.py b/erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.py index 21708ab4ec6e..d2249c2147fd 100644 --- a/erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.py +++ b/erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.py @@ -378,13 +378,9 @@ def send_emails(document_name, from_scheduler=False, posting_date=None): attachments = [{"fname": filename + ".pdf", "fcontent": report_pdf}] recipients, cc = get_recipients_and_cc(customer, doc) -<<<<<<< HEAD - context = get_context(customer, doc) -======= if not recipients: continue - ->>>>>>> 5c2a949593 (feat: add field for specifying pdf name) + subject = frappe.render_template(doc.subject, context) message = frappe.render_template(doc.body, context) From a35abf840397f02ababf4ab869464bf82709a98a Mon Sep 17 00:00:00 2001 From: Gursheen Kaur Anand <40693548+GursheenK@users.noreply.github.com> Date: Mon, 11 Sep 2023 12:06:24 +0530 Subject: [PATCH 302/501] chore: linting issues --- .../process_statement_of_accounts.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.py b/erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.py index d2249c2147fd..e1f329522051 100644 --- a/erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.py +++ b/erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.py @@ -380,7 +380,7 @@ def send_emails(document_name, from_scheduler=False, posting_date=None): recipients, cc = get_recipients_and_cc(customer, doc) if not recipients: continue - + subject = frappe.render_template(doc.subject, context) message = frappe.render_template(doc.body, context) From 21be889a771f815a720d44494ba2e6e8174adb83 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Mon, 11 Sep 2023 18:56:20 +0530 Subject: [PATCH 303/501] fix(ux): docstatus filter for `Reference Name` in QI (backport #37024) (#37028) fix(ux): docstatus filter for `Reference Name` in QI (#37024) (cherry picked from commit d739ab6ee3165b0c661086e71ad78aa1c41f58a6) Co-authored-by: s-aga-r --- .../doctype/quality_inspection/quality_inspection.js | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/erpnext/stock/doctype/quality_inspection/quality_inspection.js b/erpnext/stock/doctype/quality_inspection/quality_inspection.js index eea28791a9ff..05fa2324dd47 100644 --- a/erpnext/stock/doctype/quality_inspection/quality_inspection.js +++ b/erpnext/stock/doctype/quality_inspection/quality_inspection.js @@ -6,6 +6,14 @@ cur_frm.cscript.refresh = cur_frm.cscript.inspection_type; frappe.ui.form.on("Quality Inspection", { setup: function(frm) { + frm.set_query("reference_name", function() { + return { + filters: { + "docstatus": ["!=", 2], + } + } + }); + frm.set_query("batch_no", function() { return { filters: { From 66027877d3cdd7ac5de421d9c5b631fe07630f6a Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Tue, 12 Sep 2023 09:04:37 +0530 Subject: [PATCH 304/501] fix: `Parent Task` link with `Project Task` (backport #37025) (#37033) * feat: new field in `Task` to hold ref of Template Task (cherry picked from commit b4bcd9ba3f1bfcbd72867d24448ed14bda579dd4) # Conflicts: # erpnext/projects/doctype/task/task.json * fix: set `Template Task` ref in `Project Task` (cherry picked from commit d3295c43e3ba86b66d2085572f921f8af94e27d5) * fix: reload task before save (cherry picked from commit 5cae2e79bd1fcc6222ba08988af2c9d1353ede6c) * test: add test case for Task having common subject (cherry picked from commit 0d5c8f03bdcfb8f34bf389eb3c67f57302be6b75) * chore: `conflicts` --------- Co-authored-by: s-aga-r --- erpnext/projects/doctype/project/project.py | 12 +++-- .../projects/doctype/project/test_project.py | 49 +++++++++++++++++-- erpnext/projects/doctype/task/task.json | 13 ++++- 3 files changed, 66 insertions(+), 8 deletions(-) diff --git a/erpnext/projects/doctype/project/project.py b/erpnext/projects/doctype/project/project.py index d80133c988a3..082ba9152072 100644 --- a/erpnext/projects/doctype/project/project.py +++ b/erpnext/projects/doctype/project/project.py @@ -84,6 +84,7 @@ def create_task_from_template(self, task_details): issue=task_details.issue, is_group=task_details.is_group, color=task_details.color, + template_task=task_details.name, ) ).insert() @@ -103,9 +104,13 @@ def update_if_holiday(self, date): return date def dependency_mapping(self, template_tasks, project_tasks): - for template_task in template_tasks: - project_task = list(filter(lambda x: x.subject == template_task.subject, project_tasks))[0] - project_task = frappe.get_doc("Task", project_task.name) + for project_task in project_tasks: + if project_task.get("template_task"): + template_task = frappe.get_doc("Task", project_task.template_task) + else: + template_task = list(filter(lambda x: x.subject == project_task.subject, template_tasks))[0] + template_task = frappe.get_doc("Task", template_task.name) + self.check_depends_on_value(template_task, project_task, project_tasks) self.check_for_parent_tasks(template_task, project_task, project_tasks) @@ -117,6 +122,7 @@ def check_depends_on_value(self, template_task, project_task, project_tasks): filter(lambda x: x.subject == child_task_subject, project_tasks) ) if len(corresponding_project_task): + project_task.reload() # reload, as it might have been updated in the previous iteration project_task.append("depends_on", {"task": corresponding_project_task[0].name}) project_task.save() diff --git a/erpnext/projects/doctype/project/test_project.py b/erpnext/projects/doctype/project/test_project.py index 8a599cef7538..e49fecd1f470 100644 --- a/erpnext/projects/doctype/project/test_project.py +++ b/erpnext/projects/doctype/project/test_project.py @@ -1,9 +1,8 @@ # Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors # License: GNU General Public License v3. See license.txt -import unittest - import frappe +from frappe.tests.utils import FrappeTestCase from frappe.utils import add_days, getdate, nowdate from erpnext.projects.doctype.project_template.test_project_template import make_project_template @@ -15,7 +14,7 @@ test_ignore = ["Sales Order"] -class TestProject(unittest.TestCase): +class TestProject(FrappeTestCase): def test_project_with_template_having_no_parent_and_depend_tasks(self): project_name = "Test Project with Template - No Parent and Dependend Tasks" frappe.db.sql(""" delete from tabTask where project = %s """, project_name) @@ -155,6 +154,50 @@ def test_project_linking_with_sales_order(self): so.reload() self.assertFalse(so.project) + def test_project_with_template_tasks_having_common_name(self): + # Step - 1: Create Template Parent Tasks + template_parent_task1 = create_task(subject="Parent Task - 1", is_template=1, is_group=1) + template_parent_task2 = create_task(subject="Parent Task - 2", is_template=1, is_group=1) + template_parent_task3 = create_task(subject="Parent Task - 1", is_template=1, is_group=1) + + # Step - 2: Create Template Child Tasks + template_task1 = create_task( + subject="Task - 1", is_template=1, parent_task=template_parent_task1.name + ) + template_task2 = create_task( + subject="Task - 2", is_template=1, parent_task=template_parent_task2.name + ) + template_task3 = create_task( + subject="Task - 1", is_template=1, parent_task=template_parent_task3.name + ) + + # Step - 3: Create Project Template + template_tasks = [ + template_parent_task1, + template_task1, + template_parent_task2, + template_task2, + template_parent_task3, + template_task3, + ] + project_template = make_project_template( + "Project template with common Task Subject", template_tasks + ) + + # Step - 4: Create Project against the Project Template + project = get_project("Project with common Task Subject", project_template) + project_tasks = frappe.get_all( + "Task", {"project": project.name}, ["subject", "parent_task", "is_group"] + ) + + # Test - 1: No. of Project Tasks should be equal to No. of Template Tasks + self.assertEquals(len(project_tasks), len(template_tasks)) + + # Test - 2: All child Project Tasks should have Parent Task linked + for pt in project_tasks: + if not pt.is_group: + self.assertIsNotNone(pt.parent_task) + def get_project(name, template): diff --git a/erpnext/projects/doctype/task/task.json b/erpnext/projects/doctype/task/task.json index 141a99e612b4..33a8799f96c9 100644 --- a/erpnext/projects/doctype/task/task.json +++ b/erpnext/projects/doctype/task/task.json @@ -52,13 +52,15 @@ "company", "lft", "rgt", - "old_parent" + "old_parent", + "template_task" ], "fields": [ { "fieldname": "subject", "fieldtype": "Data", "in_global_search": 1, + "in_list_view": 1, "in_standard_filter": 1, "label": "Subject", "reqd": 1, @@ -138,6 +140,7 @@ "fieldname": "parent_task", "fieldtype": "Link", "ignore_user_permissions": 1, + "in_list_view": 1, "label": "Parent Task", "options": "Task", "search_index": 1 @@ -382,6 +385,12 @@ "fieldtype": "Date", "label": "Completed On", "mandatory_depends_on": "eval: doc.status == \"Completed\"" + }, + { + "fieldname": "template_task", + "fieldtype": "Data", + "hidden": 1, + "label": "Template Task" } ], "icon": "fa fa-check", @@ -389,7 +398,7 @@ "is_tree": 1, "links": [], "max_attachments": 5, - "modified": "2022-06-23 16:58:47.005241", + "modified": "2023-09-06 13:52:05.861175", "modified_by": "Administrator", "module": "Projects", "name": "Task", From d278b116030df0f272c3c101b75fe3b66a344c04 Mon Sep 17 00:00:00 2001 From: rohitwaghchaure Date: Tue, 12 Sep 2023 13:32:56 +0530 Subject: [PATCH 305/501] feat: provision to set required by from Production Plan (#37039) * feat: provision to set the Required By date from production plan * test: added test case for validate schedule_date --- .../material_request_plan_item.json | 41 ++++++++++++++----- .../production_plan/production_plan.py | 2 +- .../production_plan/test_production_plan.py | 12 +++++- 3 files changed, 42 insertions(+), 13 deletions(-) diff --git a/erpnext/manufacturing/doctype/material_request_plan_item/material_request_plan_item.json b/erpnext/manufacturing/doctype/material_request_plan_item/material_request_plan_item.json index 09bf1d8a7368..d07bf0fa66bb 100644 --- a/erpnext/manufacturing/doctype/material_request_plan_item/material_request_plan_item.json +++ b/erpnext/manufacturing/doctype/material_request_plan_item/material_request_plan_item.json @@ -10,22 +10,25 @@ "warehouse", "item_name", "material_request_type", - "actual_qty", - "ordered_qty", + "quantity", "required_bom_qty", "column_break_4", - "quantity", + "schedule_date", "uom", "conversion_factor", - "projected_qty", - "reserved_qty_for_production", - "safety_stock", "item_details", "description", "min_order_qty", "section_break_8", "sales_order", - "requested_qty" + "bin_qty_section", + "actual_qty", + "requested_qty", + "reserved_qty_for_production", + "column_break_yhelv", + "ordered_qty", + "projected_qty", + "safety_stock" ], "fields": [ { @@ -65,7 +68,7 @@ "fieldtype": "Column Break" }, { - "columns": 1, + "columns": 2, "fieldname": "quantity", "fieldtype": "Float", "in_list_view": 1, @@ -80,12 +83,12 @@ "read_only": 1 }, { - "columns": 2, + "columns": 1, "default": "0", "fieldname": "actual_qty", "fieldtype": "Float", "in_list_view": 1, - "label": "Available Qty", + "label": "Qty In Stock", "no_copy": 1, "read_only": 1 }, @@ -176,11 +179,27 @@ "fieldtype": "Float", "label": "Conversion Factor", "read_only": 1 + }, + { + "columns": 1, + "fieldname": "schedule_date", + "fieldtype": "Date", + "in_list_view": 1, + "label": "Required By" + }, + { + "fieldname": "bin_qty_section", + "fieldtype": "Section Break", + "label": "BIN Qty" + }, + { + "fieldname": "column_break_yhelv", + "fieldtype": "Column Break" } ], "istable": 1, "links": [], - "modified": "2023-05-03 12:43:29.895754", + "modified": "2023-09-12 12:09:08.358326", "modified_by": "Administrator", "module": "Manufacturing", "name": "Material Request Plan Item", diff --git a/erpnext/manufacturing/doctype/production_plan/production_plan.py b/erpnext/manufacturing/doctype/production_plan/production_plan.py index a494550423f5..795cb97fffaf 100644 --- a/erpnext/manufacturing/doctype/production_plan/production_plan.py +++ b/erpnext/manufacturing/doctype/production_plan/production_plan.py @@ -725,7 +725,7 @@ def make_material_request(self): # key for Sales Order:Material Request Type:Customer key = "{}:{}:{}".format(item.sales_order, material_request_type, item_doc.customer or "") - schedule_date = add_days(nowdate(), cint(item_doc.lead_time_days)) + schedule_date = item.schedule_date or add_days(nowdate(), cint(item_doc.lead_time_days)) if not key in material_request_map: # make a new MR for the combination diff --git a/erpnext/manufacturing/doctype/production_plan/test_production_plan.py b/erpnext/manufacturing/doctype/production_plan/test_production_plan.py index 2871a29d7682..f5778847b9cd 100644 --- a/erpnext/manufacturing/doctype/production_plan/test_production_plan.py +++ b/erpnext/manufacturing/doctype/production_plan/test_production_plan.py @@ -2,7 +2,7 @@ # See license.txt import frappe from frappe.tests.utils import FrappeTestCase -from frappe.utils import add_to_date, flt, now_datetime, nowdate +from frappe.utils import add_to_date, flt, getdate, now_datetime, nowdate from erpnext.controllers.item_variant import create_variant from erpnext.manufacturing.doctype.production_plan.production_plan import ( @@ -58,6 +58,9 @@ def test_production_plan_mr_creation(self): pln = create_production_plan(item_code="Test Production Item 1") self.assertTrue(len(pln.mr_items), 2) + for row in pln.mr_items: + row.schedule_date = add_to_date(nowdate(), days=10) + pln.make_material_request() pln.reload() self.assertTrue(pln.status, "Material Requested") @@ -71,6 +74,13 @@ def test_production_plan_mr_creation(self): self.assertTrue(len(material_requests), 2) + for row in material_requests: + mr_schedule_date = getdate(frappe.db.get_value("Material Request", row[0], "schedule_date")) + + expected_date = getdate(add_to_date(nowdate(), days=10)) + + self.assertEqual(mr_schedule_date, expected_date) + pln.make_work_order() work_orders = frappe.get_all( "Work Order", fields=["name"], filters={"production_plan": pln.name}, as_list=1 From 7af8aec8796476385e14531b37d95c4235f1c71e Mon Sep 17 00:00:00 2001 From: Frappe PR Bot Date: Tue, 12 Sep 2023 12:22:33 +0000 Subject: [PATCH 306/501] chore(release): Bumped to Version 14.39.0 # [14.39.0](https://github.com/frappe/erpnext/compare/v14.38.0...v14.39.0) (2023-09-12) ### Bug Fixes * `company` is ambiguous ([fe69d53](https://github.com/frappe/erpnext/commit/fe69d5364dd03dd2e916e0c3385f3d4e6e0163b7)) * `Parent Task` link with `Project Task` (backport [#37025](https://github.com/frappe/erpnext/issues/37025)) ([#37033](https://github.com/frappe/erpnext/issues/37033)) ([6602787](https://github.com/frappe/erpnext/commit/66027877d3cdd7ac5de421d9c5b631fe07630f6a)) * correct asset daily depr schedule calculation [v14] ([#36991](https://github.com/frappe/erpnext/issues/36991)) ([2ae4463](https://github.com/frappe/erpnext/commit/2ae4463b7692704a214fceec5d67cd6c32b67af3)) * generate pdf only when result exists ([53270dd](https://github.com/frappe/erpnext/commit/53270dd933d1e1ab22787de4ecbab6894810c5e8)) * remove report field db set ([284181d](https://github.com/frappe/erpnext/commit/284181d766e4b321e0fc642b3230c1ce86338e37)) * show letterhead and terms for AR pdf ([2077b2c](https://github.com/frappe/erpnext/commit/2077b2cde413a66347b3b72e201a2702d7f6c0b6)) * Update party type for payroll payable account ([f251d6c](https://github.com/frappe/erpnext/commit/f251d6cb699cf403e559e37276ebcca01498291e)) * use primary key for link lookup (backport [#36919](https://github.com/frappe/erpnext/issues/36919)) ([#36978](https://github.com/frappe/erpnext/issues/36978)) ([4fede56](https://github.com/frappe/erpnext/commit/4fede56d98f803ee4912fa80cea386135c378d2c)) * **ux:** docstatus filter for `Reference Name` in QI (backport [#37024](https://github.com/frappe/erpnext/issues/37024)) ([#37028](https://github.com/frappe/erpnext/issues/37028)) ([21be889](https://github.com/frappe/erpnext/commit/21be889a771f815a720d44494ba2e6e8174adb83)) ### Features * add field for specifying pdf name ([657ca7f](https://github.com/frappe/erpnext/commit/657ca7ff22602ab34acc25c3c2a2d7bcd2ba25e9)) * Add half-yearly asset maintenance periodicity. (backport [#37006](https://github.com/frappe/erpnext/issues/37006)) ([#37014](https://github.com/frappe/erpnext/issues/37014)) ([acd9c69](https://github.com/frappe/erpnext/commit/acd9c692017d811f668e300fb0fedfb093df6458)) * provision to set required by from Production Plan ([#37039](https://github.com/frappe/erpnext/issues/37039)) ([d278b11](https://github.com/frappe/erpnext/commit/d278b116030df0f272c3c101b75fe3b66a344c04)) --- erpnext/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/__init__.py b/erpnext/__init__.py index 4734e8cc0b97..12a6ba9d77a5 100644 --- a/erpnext/__init__.py +++ b/erpnext/__init__.py @@ -3,7 +3,7 @@ import frappe -__version__ = "14.38.0" +__version__ = "14.39.0" def get_default_company(user=None): From 413b40f5a770e062732f850714d3ea75de2b4337 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Wed, 13 Sep 2023 06:03:12 +0530 Subject: [PATCH 307/501] fix: packed item using expired price (cherry picked from commit 47ffa4983c8752d706af80fefbedd3f80dc53f3b) --- erpnext/stock/doctype/packed_item/packed_item.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/erpnext/stock/doctype/packed_item/packed_item.py b/erpnext/stock/doctype/packed_item/packed_item.py index dbd8de4fcb0e..a9e9ad1a639f 100644 --- a/erpnext/stock/doctype/packed_item/packed_item.py +++ b/erpnext/stock/doctype/packed_item/packed_item.py @@ -207,6 +207,9 @@ def update_packed_item_price_data(pi_row, item_data, doc): "conversion_rate": doc.get("conversion_rate"), } ) + if not row_data.get("transaction_date"): + row_data.update({"transaction_date": doc.get("transaction_date")}) + rate = get_price_list_rate(row_data, item_doc).get("price_list_rate") pi_row.rate = rate or item_data.get("valuation_rate") or 0.0 From aa0a756111bb20887c9d6ec11a9bf56d2261a55e Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Wed, 13 Sep 2023 06:24:57 +0530 Subject: [PATCH 308/501] test: expired item price should not be picked (cherry picked from commit 055156d28a637a5dfa3b73ba5740826642884aa0) --- .../doctype/sales_order/test_sales_order.py | 55 +++++++++++++++++++ 1 file changed, 55 insertions(+) diff --git a/erpnext/selling/doctype/sales_order/test_sales_order.py b/erpnext/selling/doctype/sales_order/test_sales_order.py index 608e23a82680..799ad555a526 100644 --- a/erpnext/selling/doctype/sales_order/test_sales_order.py +++ b/erpnext/selling/doctype/sales_order/test_sales_order.py @@ -1998,6 +1998,61 @@ def test_packed_items_for_partial_sales_order(self): self.assertEqual(len(dn.packed_items), 1) self.assertEqual(dn.items[0].item_code, "_Test Product Bundle Item Partial 2") + @change_settings("Selling Settings", {"editable_bundle_item_rates": 1}) + def test_expired_rate_for_packed_item(self): + bundle = "_Test Product Bundle 1" + packed_item = "_Packed Item 1" + + # test Update Items with product bundle + for product_bundle in [bundle]: + if not frappe.db.exists("Item", product_bundle): + bundle_item = make_item(product_bundle, {"is_stock_item": 0}) + bundle_item.append( + "item_defaults", {"company": "_Test Company", "default_warehouse": "_Test Warehouse - _TC"} + ) + bundle_item.save(ignore_permissions=True) + + for product_bundle in [packed_item]: + if not frappe.db.exists("Item", product_bundle): + make_item(product_bundle, {"is_stock_item": 0, "stock_uom": "Nos"}) + + make_product_bundle(bundle, [packed_item], 1) + + for scenario in [ + {"valid_upto": add_days(nowdate(), -1), "expected_rate": 0.0}, + {"valid_upto": add_days(nowdate(), 1), "expected_rate": 111.0}, + ]: + with self.subTest(scenario=scenario): + frappe.get_doc( + { + "doctype": "Item Price", + "item_code": packed_item, + "selling": 1, + "price_list": "_Test Price List", + "valid_from": add_days(nowdate(), -1), + "valid_upto": scenario.get("valid_upto"), + "price_list_rate": 111, + } + ).save() + + so = frappe.new_doc("Sales Order") + so.transaction_date = nowdate() + so.delivery_date = nowdate() + so.set_warehouse = "" + so.company = "_Test Company" + so.customer = "_Test Customer" + so.currency = "INR" + so.selling_price_list = "_Test Price List" + so.append("items", {"item_code": bundle, "qty": 1}) + so.save() + + self.assertEqual(len(so.items), 1) + self.assertEqual(len(so.packed_items), 1) + self.assertEqual(so.items[0].item_code, bundle) + self.assertEqual(so.packed_items[0].item_code, packed_item) + self.assertEqual(so.items[0].rate, scenario.get("expected_rate")) + self.assertEqual(so.packed_items[0].rate, scenario.get("expected_rate")) + def automatically_fetch_payment_terms(enable=1): accounts_settings = frappe.get_doc("Accounts Settings") From 9bc44a3b40c661ceb4821d90f70fc160d406bf8e Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Tue, 12 Sep 2023 21:14:08 +0530 Subject: [PATCH 309/501] fix: Apply dimension filter, irrespective of dimesion columns (cherry picked from commit 769db0b3bc76319a746e24ef4e3ced1de597e0b1) --- .../report/general_ledger/general_ledger.py | 27 +++++++++---------- 1 file changed, 13 insertions(+), 14 deletions(-) diff --git a/erpnext/accounts/report/general_ledger/general_ledger.py b/erpnext/accounts/report/general_ledger/general_ledger.py index 23403a4b15b2..d670a3569755 100644 --- a/erpnext/accounts/report/general_ledger/general_ledger.py +++ b/erpnext/accounts/report/general_ledger/general_ledger.py @@ -272,20 +272,19 @@ def get_conditions(filters): if match_conditions: conditions.append(match_conditions) - if filters.get("include_dimensions"): - accounting_dimensions = get_accounting_dimensions(as_list=False) - - if accounting_dimensions: - for dimension in accounting_dimensions: - if not dimension.disabled: - if filters.get(dimension.fieldname): - if frappe.get_cached_value("DocType", dimension.document_type, "is_tree"): - filters[dimension.fieldname] = get_dimension_with_children( - dimension.document_type, filters.get(dimension.fieldname) - ) - conditions.append("{0} in %({0})s".format(dimension.fieldname)) - else: - conditions.append("{0} in %({0})s".format(dimension.fieldname)) + accounting_dimensions = get_accounting_dimensions(as_list=False) + + if accounting_dimensions: + for dimension in accounting_dimensions: + if not dimension.disabled: + if filters.get(dimension.fieldname): + if frappe.get_cached_value("DocType", dimension.document_type, "is_tree"): + filters[dimension.fieldname] = get_dimension_with_children( + dimension.document_type, filters.get(dimension.fieldname) + ) + conditions.append("{0} in %({0})s".format(dimension.fieldname)) + else: + conditions.append("{0} in %({0})s".format(dimension.fieldname)) return "and {}".format(" and ".join(conditions)) if conditions else "" From c2a0c1e989aab8ec136a6e8c15687abcab3cb55b Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Wed, 13 Sep 2023 16:06:08 +0530 Subject: [PATCH 310/501] fix: + btn not appearing for delivery note connection (backport #36980) (#37070) fix: move SI and DI connected links to internal_and_external_links (cherry picked from commit e1a94a9ba1c1241012cc0458791686e26ad5483d) Co-authored-by: anandbaburajan --- .../accounts/doctype/sales_invoice/sales_invoice_dashboard.py | 4 +++- .../stock/doctype/delivery_note/delivery_note_dashboard.py | 4 +++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice_dashboard.py b/erpnext/accounts/doctype/sales_invoice/sales_invoice_dashboard.py index 6fdcf263a552..fd95c1fe0e55 100644 --- a/erpnext/accounts/doctype/sales_invoice/sales_invoice_dashboard.py +++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice_dashboard.py @@ -15,9 +15,11 @@ def get_data(): }, "internal_links": { "Sales Order": ["items", "sales_order"], - "Delivery Note": ["items", "delivery_note"], "Timesheet": ["timesheets", "time_sheet"], }, + "internal_and_external_links": { + "Delivery Note": ["items", "delivery_note"], + }, "transactions": [ { "label": _("Payment"), diff --git a/erpnext/stock/doctype/delivery_note/delivery_note_dashboard.py b/erpnext/stock/doctype/delivery_note/delivery_note_dashboard.py index e66c23324da5..d4a574da73f2 100644 --- a/erpnext/stock/doctype/delivery_note/delivery_note_dashboard.py +++ b/erpnext/stock/doctype/delivery_note/delivery_note_dashboard.py @@ -11,10 +11,12 @@ def get_data(): }, "internal_links": { "Sales Order": ["items", "against_sales_order"], - "Sales Invoice": ["items", "against_sales_invoice"], "Material Request": ["items", "material_request"], "Purchase Order": ["items", "purchase_order"], }, + "internal_and_external_links": { + "Sales Invoice": ["items", "against_sales_invoice"], + }, "transactions": [ {"label": _("Related"), "items": ["Sales Invoice", "Packing Slip", "Delivery Trip"]}, {"label": _("Reference"), "items": ["Sales Order", "Shipment", "Quality Inspection"]}, From b56c9b91f11dd9e84d02b82ea2b0b2f727c5d4e1 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Wed, 13 Sep 2023 11:26:13 +0000 Subject: [PATCH 311/501] fix: accepted warehouse and rejected warehouse can't be same (backport #36973) (#37071) * fix: ignore user permissions for `From Warehouse` in PR (cherry picked from commit d2f32861158236964ba18daa01144ea455607d80) # Conflicts: # erpnext/buying/doctype/purchase_order/purchase_order.json # erpnext/buying/doctype/purchase_order_item/purchase_order_item.json * chore: `conflicts` * chore: `conflicts` --------- Co-authored-by: s-aga-r --- erpnext/buying/doctype/purchase_order/purchase_order.json | 3 ++- .../doctype/purchase_order_item/purchase_order_item.json | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/erpnext/buying/doctype/purchase_order/purchase_order.json b/erpnext/buying/doctype/purchase_order/purchase_order.json index 645abf25a8f9..bacd98bea77d 100644 --- a/erpnext/buying/doctype/purchase_order/purchase_order.json +++ b/erpnext/buying/doctype/purchase_order/purchase_order.json @@ -1171,6 +1171,7 @@ "depends_on": "is_internal_supplier", "fieldname": "set_from_warehouse", "fieldtype": "Link", + "ignore_user_permissions": 1, "label": "Set From Warehouse", "options": "Warehouse" }, @@ -1271,7 +1272,7 @@ "idx": 105, "is_submittable": 1, "links": [], - "modified": "2023-05-24 11:16:41.195340", + "modified": "2023-09-13 16:21:07.361700", "modified_by": "Administrator", "module": "Buying", "name": "Purchase Order", diff --git a/erpnext/buying/doctype/purchase_order_item/purchase_order_item.json b/erpnext/buying/doctype/purchase_order_item/purchase_order_item.json index c645b04e1294..fe1b97025394 100644 --- a/erpnext/buying/doctype/purchase_order_item/purchase_order_item.json +++ b/erpnext/buying/doctype/purchase_order_item/purchase_order_item.json @@ -878,6 +878,7 @@ "depends_on": "eval:parent.is_internal_supplier", "fieldname": "from_warehouse", "fieldtype": "Link", + "ignore_user_permissions": 1, "label": "From Warehouse", "options": "Warehouse" }, @@ -902,7 +903,7 @@ "index_web_pages_for_search": 1, "istable": 1, "links": [], - "modified": "2022-11-29 16:47:41.364387", + "modified": "2023-09-13 16:22:40.825092", "modified_by": "Administrator", "module": "Buying", "name": "Purchase Order Item", From 8772e40bae96db49b1d8653a5e8e5cb975f492ad Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Wed, 13 Sep 2023 18:16:33 +0530 Subject: [PATCH 312/501] fix: Purchase Receipt Provisional Accounting GL Entries (backport #37046) (#37068) * fix: Purchase Receipt Provisional Accounting GL Entries (cherry picked from commit 6bab0eeaa1ac53c85a4a7b9668ea4ffadf99be4d) * test: Purchase Receipt Provisional Accounting GL Entries (cherry picked from commit 1c78a5a9aa2dbd4d77e7f411699c4cb7dd265cc9) * fix(test): PR Provisional Accounting --------- Co-authored-by: s-aga-r --- .../buying/doctype/supplier/test_supplier.py | 4 ++ .../purchase_receipt/purchase_receipt.py | 4 +- .../purchase_receipt/test_purchase_receipt.py | 43 +++++++++++++++++++ 3 files changed, 49 insertions(+), 2 deletions(-) diff --git a/erpnext/buying/doctype/supplier/test_supplier.py b/erpnext/buying/doctype/supplier/test_supplier.py index b9fc344647b7..7c7467e6f649 100644 --- a/erpnext/buying/doctype/supplier/test_supplier.py +++ b/erpnext/buying/doctype/supplier/test_supplier.py @@ -195,6 +195,9 @@ def test_serach_fields_for_supplier(self): def create_supplier(**args): args = frappe._dict(args) + if not args.supplier_name: + args.supplier_name = frappe.generate_hash() + if frappe.db.exists("Supplier", args.supplier_name): return frappe.get_doc("Supplier", args.supplier_name) @@ -202,6 +205,7 @@ def create_supplier(**args): { "doctype": "Supplier", "supplier_name": args.supplier_name, + "default_currency": args.default_currency, "supplier_group": args.supplier_group or "Services", "supplier_type": args.supplier_type or "Company", "tax_withholding_category": args.tax_withholding_category, diff --git a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py index 4f6c6364a472..1873efc711ae 100644 --- a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py +++ b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py @@ -605,7 +605,7 @@ def add_provisional_gl_entry( account=provisional_account, cost_center=item.cost_center, debit=0.0, - credit=multiplication_factor * item.amount, + credit=multiplication_factor * item.base_amount, remarks=remarks, against_account=expense_account, account_currency=credit_currency, @@ -619,7 +619,7 @@ def add_provisional_gl_entry( gl_entries=gl_entries, account=expense_account, cost_center=item.cost_center, - debit=multiplication_factor * item.amount, + debit=multiplication_factor * item.base_amount, credit=0.0, remarks=remarks, against_account=provisional_account, diff --git a/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py b/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py index c9433cf51060..2f46809f49d7 100644 --- a/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py +++ b/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py @@ -2024,6 +2024,49 @@ def test_purchase_receipt_with_backdated_landed_cost_voucher(self): ste7.reload() self.assertEqual(ste7.items[0].valuation_rate, valuation_rate) + def test_purchase_receipt_provisional_accounting(self): + # Step - 1: Create Supplier with Default Currency as USD + from erpnext.buying.doctype.supplier.test_supplier import create_supplier + + supplier = create_supplier(default_currency="USD") + + # Step - 2: Setup Company for Provisional Accounting + from erpnext.accounts.doctype.account.test_account import create_account + + provisional_account = create_account( + account_name="Provision Account", + parent_account="Current Liabilities - _TC", + company="_Test Company", + ) + company = frappe.get_doc("Company", "_Test Company") + company.enable_provisional_accounting_for_non_stock_items = 1 + company.default_provisional_account = provisional_account + company.save() + + # Step - 3: Create Non-Stock Item + item = make_item(properties={"is_stock_item": 0}) + + # Step - 4: Create Purchase Receipt + pr = make_purchase_receipt( + qty=2, + item_code=item.name, + company=company.name, + supplier=supplier.name, + currency=supplier.default_currency, + ) + + # Test - 1: Total and Base Total should not be the same as the currency is different + self.assertNotEqual(flt(pr.total, 2), flt(pr.base_total, 2)) + self.assertEqual(flt(pr.total * pr.conversion_rate, 2), flt(pr.base_total, 2)) + + # Test - 2: Sum of Debit or Credit should be equal to Purchase Receipt Base Total + amount = frappe.db.get_value("GL Entry", {"docstatus": 1, "voucher_no": pr.name}, ["sum(debit)"]) + expected_amount = pr.base_total + self.assertEqual(amount, expected_amount) + + company.enable_provisional_accounting_for_non_stock_items = 0 + company.save() + def prepare_data_for_internal_transfer(): from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_internal_supplier From 3ecdf028f28229d230faef07babd126ef13ac4fc Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Wed, 13 Sep 2023 21:03:45 +0530 Subject: [PATCH 313/501] fix: Remove redundant code (#37001) fix: Remove redundant code (#37001) fix: Remove redundant code (cherry picked from commit 96363dbb07ebb7f22e6450a2e4600d4b80deb4c3) Co-authored-by: ViralKansodiya <141210323+viralkansodiya@users.noreply.github.com> --- erpnext/e_commerce/shopping_cart/cart.py | 1 - 1 file changed, 1 deletion(-) diff --git a/erpnext/e_commerce/shopping_cart/cart.py b/erpnext/e_commerce/shopping_cart/cart.py index 4f9088e8c083..57746a234bcb 100644 --- a/erpnext/e_commerce/shopping_cart/cart.py +++ b/erpnext/e_commerce/shopping_cart/cart.py @@ -634,7 +634,6 @@ def get_applicable_shipping_rules(party=None, quotation=None): shipping_rules = get_shipping_rules(quotation) if shipping_rules: - rule_label_map = frappe.db.get_values("Shipping Rule", shipping_rules, "label") # we need this in sorted order as per the position of the rule in the settings page return [[rule, rule] for rule in shipping_rules] From f2395a92971f48904e88afb929285def7ba221e4 Mon Sep 17 00:00:00 2001 From: rohitwaghchaure Date: Thu, 14 Sep 2023 14:28:05 +0530 Subject: [PATCH 314/501] fix: precision issue and column name (#37073) --- .../requested_items_to_order_and_receive.py | 43 +++++++++++++------ 1 file changed, 31 insertions(+), 12 deletions(-) diff --git a/erpnext/buying/report/requested_items_to_order_and_receive/requested_items_to_order_and_receive.py b/erpnext/buying/report/requested_items_to_order_and_receive/requested_items_to_order_and_receive.py index 21241e086036..07187352eb70 100644 --- a/erpnext/buying/report/requested_items_to_order_and_receive/requested_items_to_order_and_receive.py +++ b/erpnext/buying/report/requested_items_to_order_and_receive/requested_items_to_order_and_receive.py @@ -7,7 +7,7 @@ import frappe from frappe import _ from frappe.query_builder.functions import Coalesce, Sum -from frappe.utils import date_diff, flt, getdate +from frappe.utils import cint, date_diff, flt, getdate def execute(filters=None): @@ -47,8 +47,10 @@ def get_data(filters): mr.transaction_date.as_("date"), mr_item.schedule_date.as_("required_date"), mr_item.item_code.as_("item_code"), - Sum(Coalesce(mr_item.stock_qty, 0)).as_("qty"), - Coalesce(mr_item.stock_uom, "").as_("uom"), + Sum(Coalesce(mr_item.qty, 0)).as_("qty"), + Sum(Coalesce(mr_item.stock_qty, 0)).as_("stock_qty"), + Coalesce(mr_item.uom, "").as_("uom"), + Coalesce(mr_item.stock_uom, "").as_("stock_uom"), Sum(Coalesce(mr_item.ordered_qty, 0)).as_("ordered_qty"), Sum(Coalesce(mr_item.received_qty, 0)).as_("received_qty"), (Sum(Coalesce(mr_item.stock_qty, 0)) - Sum(Coalesce(mr_item.received_qty, 0))).as_( @@ -96,7 +98,7 @@ def get_conditions(filters, query, mr, mr_item): def update_qty_columns(row_to_update, data_row): - fields = ["qty", "ordered_qty", "received_qty", "qty_to_receive", "qty_to_order"] + fields = ["qty", "stock_qty", "ordered_qty", "received_qty", "qty_to_receive", "qty_to_order"] for field in fields: row_to_update[field] += flt(data_row[field]) @@ -104,16 +106,20 @@ def update_qty_columns(row_to_update, data_row): def prepare_data(data, filters): """Prepare consolidated Report data and Chart data""" material_request_map, item_qty_map = {}, {} + precision = cint(frappe.db.get_default("float_precision")) or 2 for row in data: # item wise map for charts if not row["item_code"] in item_qty_map: item_qty_map[row["item_code"]] = { - "qty": row["qty"], - "ordered_qty": row["ordered_qty"], - "received_qty": row["received_qty"], - "qty_to_receive": row["qty_to_receive"], - "qty_to_order": row["qty_to_order"], + "qty": flt(row["stock_qty"], precision), + "stock_qty": flt(row["stock_qty"], precision), + "stock_uom": row["stock_uom"], + "uom": row["uom"], + "ordered_qty": flt(row["ordered_qty"], precision), + "received_qty": flt(row["received_qty"], precision), + "qty_to_receive": flt(row["qty_to_receive"], precision), + "qty_to_order": flt(row["qty_to_order"], precision), } else: item_entry = item_qty_map[row["item_code"]] @@ -200,21 +206,34 @@ def get_columns(filters): {"label": _("Item Name"), "fieldname": "item_name", "fieldtype": "Data", "width": 100}, {"label": _("Description"), "fieldname": "description", "fieldtype": "Data", "width": 200}, { - "label": _("Stock UOM"), + "label": _("UOM"), "fieldname": "uom", "fieldtype": "Data", "width": 100, }, + { + "label": _("Stock UOM"), + "fieldname": "stock_uom", + "fieldtype": "Data", + "width": 100, + }, ] ) columns.extend( [ { - "label": _("Stock Qty"), + "label": _("Qty"), "fieldname": "qty", "fieldtype": "Float", - "width": 120, + "width": 140, + "convertible": "qty", + }, + { + "label": _("Qty in Stock UOM"), + "fieldname": "stock_qty", + "fieldtype": "Float", + "width": 140, "convertible": "qty", }, { From fffa13f22b39cd768b081a52d454a03b386596ba Mon Sep 17 00:00:00 2001 From: s-aga-r Date: Fri, 15 Sep 2023 16:31:53 +0530 Subject: [PATCH 315/501] fix: validate duplicate serial no in DN --- .../stock/doctype/delivery_note/delivery_note.py | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/erpnext/stock/doctype/delivery_note/delivery_note.py b/erpnext/stock/doctype/delivery_note/delivery_note.py index 3a056500b542..ba6e247d2e41 100644 --- a/erpnext/stock/doctype/delivery_note/delivery_note.py +++ b/erpnext/stock/doctype/delivery_note/delivery_note.py @@ -138,6 +138,7 @@ def validate(self): self.validate_uom_is_integer("stock_uom", "stock_qty") self.validate_uom_is_integer("uom", "qty") self.validate_with_previous_doc() + self.validate_duplicate_serial_nos() from erpnext.stock.doctype.packed_item.packed_item import make_packing_list @@ -412,6 +413,21 @@ def get_product_bundle_list(self): pluck="name", ) + def validate_duplicate_serial_nos(self): + serial_nos = [] + for item in self.items: + if not item.serial_no: + continue + + for serial_no in item.serial_no.split("\n"): + if serial_no in serial_nos: + frappe.throw( + _("Row #{0}: Serial No {1} is already selected.").format(item.idx, serial_no), + title=_("Duplicate Serial No"), + ) + else: + serial_nos.append(serial_no) + def update_billed_amount_based_on_so(so_detail, update_modified=True): from frappe.query_builder.functions import Sum From e5177a6e4692d8a5ae7cf0209a402ee273ef3304 Mon Sep 17 00:00:00 2001 From: s-aga-r Date: Fri, 15 Sep 2023 17:04:32 +0530 Subject: [PATCH 316/501] test: add test case for DN duplicate serial nos --- .../delivery_note/test_delivery_note.py | 32 +++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/erpnext/stock/doctype/delivery_note/test_delivery_note.py b/erpnext/stock/doctype/delivery_note/test_delivery_note.py index 2565d1b76d19..2acfd84d9440 100644 --- a/erpnext/stock/doctype/delivery_note/test_delivery_note.py +++ b/erpnext/stock/doctype/delivery_note/test_delivery_note.py @@ -1211,6 +1211,38 @@ def test_batch_expiry_for_delivery_note(self): self.assertTrue(return_dn.docstatus == 1) + def test_duplicate_serial_no_in_delivery_note(self): + # Step - 1: Create Serial Item + serial_item = make_item( + properties={ + "is_stock_item": 1, + "has_serial_no": 1, + "serial_no_series": frappe.generate_hash("", 10) + ".###", + } + ).name + + # Step - 2: Inward Stock + se = make_stock_entry(item_code=serial_item, target="_Test Warehouse - _TC", qty=4) + + # Step - 3: Create Delivery Note with Duplicare Serial Nos + serial_nos = se.items[0].serial_no.split("\n") + dn = create_delivery_note( + item_code=serial_item, + warehouse="_Test Warehouse - _TC", + qty=2, + do_not_save=True, + ) + dn.items[0].serial_no = "\n".join(serial_nos[:2]) + dn.append("items", dn.items[0].as_dict()) + + # Test - 1: ValidationError should be raised + self.assertRaises(frappe.ValidationError, dn.save) + + # Step - 4: Submit Delivery Note with unique Serial Nos + dn.items[1].serial_no = "\n".join(serial_nos[2:]) + dn.save() + dn.submit() + def tearDown(self): frappe.db.rollback() frappe.db.set_single_value("Selling Settings", "dont_reserve_sales_order_qty_on_sales_return", 0) From b33db6c79a6b7f37c93f7e49d6721ee8d7593527 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Fri, 15 Sep 2023 17:54:20 +0530 Subject: [PATCH 317/501] fix: asset validation misfire on debit notes (cherry picked from commit 097b9892dca4d8c41dfbe17ffd812287692d424e) --- erpnext/controllers/buying_controller.py | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/erpnext/controllers/buying_controller.py b/erpnext/controllers/buying_controller.py index 1de399677303..01990a3a268d 100644 --- a/erpnext/controllers/buying_controller.py +++ b/erpnext/controllers/buying_controller.py @@ -162,10 +162,13 @@ def validate_asset_return(self): purchase_doc_field = ( "purchase_receipt" if self.doctype == "Purchase Receipt" else "purchase_invoice" ) - not_cancelled_asset = [ - d.name - for d in frappe.db.get_all("Asset", {purchase_doc_field: self.return_against, "docstatus": 1}) - ] + not_cancelled_asset = [] + if self.return_against: + not_cancelled_asset = [ + d.name + for d in frappe.db.get_all("Asset", {purchase_doc_field: self.return_against, "docstatus": 1}) + ] + if self.is_return and len(not_cancelled_asset): frappe.throw( _( From 727dcc5034749f2505052aa05c9ad8d8b94fa324 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Fri, 15 Sep 2023 21:47:15 +0530 Subject: [PATCH 318/501] fix: ignore user permissions for `Source Warehouse` in MR (backport #37102) (#37110) fix: ignore user permissions for `Source Warehouse` in MR (#37102) fix: ignore user permissions for Source Warehouse in MR (cherry picked from commit fc016680c9015e3d5089bac84868e7556f96d77a) Co-authored-by: s-aga-r --- erpnext/stock/doctype/material_request/material_request.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/erpnext/stock/doctype/material_request/material_request.json b/erpnext/stock/doctype/material_request/material_request.json index ffec57ca1dfc..25c765bbced3 100644 --- a/erpnext/stock/doctype/material_request/material_request.json +++ b/erpnext/stock/doctype/material_request/material_request.json @@ -296,6 +296,7 @@ "depends_on": "eval:doc.material_request_type == 'Material Transfer'", "fieldname": "set_from_warehouse", "fieldtype": "Link", + "ignore_user_permissions": 1, "label": "Set Source Warehouse", "options": "Warehouse" }, @@ -356,7 +357,7 @@ "idx": 70, "is_submittable": 1, "links": [], - "modified": "2023-07-25 17:19:31.662662", + "modified": "2023-09-15 12:07:24.789471", "modified_by": "Administrator", "module": "Stock", "name": "Material Request", From a563fed6dcaf99fa799cbf034dc6d38473e0fd57 Mon Sep 17 00:00:00 2001 From: HENRY Florian Date: Sat, 16 Sep 2023 11:47:26 +0200 Subject: [PATCH 319/501] fix(ux): move `get_route_options_for_new_doc` to `refresh` (#37092) fix: move `get_route_options_for_new_doc` to `refresh` --- erpnext/public/js/controllers/transaction.js | 32 ++++++++----------- .../stock/doctype/stock_entry/stock_entry.js | 18 +++++------ .../subcontracting_receipt.js | 20 ++++++------ 3 files changed, 32 insertions(+), 38 deletions(-) diff --git a/erpnext/public/js/controllers/transaction.js b/erpnext/public/js/controllers/transaction.js index b6b6e2e5ad60..fe24b18098a1 100644 --- a/erpnext/public/js/controllers/transaction.js +++ b/erpnext/public/js/controllers/transaction.js @@ -119,19 +119,10 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe } }); - if(this.frm.fields_dict["items"].grid.get_field('batch_no')) { - this.frm.set_query("batch_no", "items", function(doc, cdt, cdn) { + if(this.frm.fields_dict['items'].grid.get_field('batch_no')) { + this.frm.set_query('batch_no', 'items', function(doc, cdt, cdn) { return me.set_query_for_batch(doc, cdt, cdn); }); - - let batch_field = this.frm.get_docfield('items', 'batch_no'); - if (batch_field) { - batch_field.get_route_options_for_new_doc = (row) => { - return { - 'item': row.doc.item_code - } - }; - } } if( @@ -196,14 +187,6 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe }); } - let batch_no_field = this.frm.get_docfield("items", "batch_no"); - if (batch_no_field) { - batch_no_field.get_route_options_for_new_doc = function(row) { - return { - "item": row.doc.item_code - } - }; - } if (this.frm.fields_dict["items"].grid.get_field('blanket_order')) { this.frm.set_query("blanket_order", "items", function(doc, cdt, cdn) { @@ -257,6 +240,17 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe } ]); } + + if(this.frm.fields_dict['items'].grid.get_field('batch_no')) { + let batch_field = this.frm.get_docfield('items', 'batch_no'); + if (batch_field) { + batch_field.get_route_options_for_new_doc = (row) => { + return { + 'item': row.doc.item_code + } + }; + } + } } is_return() { diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.js b/erpnext/stock/doctype/stock_entry/stock_entry.js index dd08ef4d1e39..6dd0a58645c8 100644 --- a/erpnext/stock/doctype/stock_entry/stock_entry.js +++ b/erpnext/stock/doctype/stock_entry/stock_entry.js @@ -101,15 +101,6 @@ frappe.ui.form.on('Stock Entry', { } }); - let batch_field = frm.get_docfield('items', 'batch_no'); - if (batch_field) { - batch_field.get_route_options_for_new_doc = (row) => { - return { - 'item': row.doc.item_code - } - }; - } - frm.add_fetch("bom_no", "inspection_required", "inspection_required"); erpnext.accounts.dimensions.setup_dimension_filters(frm, frm.doctype); @@ -345,6 +336,15 @@ frappe.ui.form.on('Stock Entry', { if(!check_should_not_attach_bom_items(frm.doc.bom_no)) { erpnext.accounts.dimensions.update_dimension(frm, frm.doctype); } + + let batch_field = frm.get_docfield('items', 'batch_no'); + if (batch_field) { + batch_field.get_route_options_for_new_doc = (row) => { + return { + 'item': row.doc.item_code + } + }; + } }, get_items_from_transit_entry: function(frm) { diff --git a/erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.js b/erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.js index 4bf008ac406f..e335c6ba7a05 100644 --- a/erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.js +++ b/erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.js @@ -75,15 +75,6 @@ frappe.ui.form.on('Subcontracting Receipt', { } } }); - - let batch_no_field = frm.get_docfield('items', 'batch_no'); - if (batch_no_field) { - batch_no_field.get_route_options_for_new_doc = function(row) { - return { - 'item': row.doc.item_code - } - }; - } }, refresh: (frm) => { @@ -148,6 +139,15 @@ frappe.ui.form.on('Subcontracting Receipt', { frm.fields_dict.supplied_items.grid.update_docfield_property('consumed_qty', 'read_only', frm.doc.__onload && frm.doc.__onload.backflush_based_on === 'BOM'); } + + let batch_no_field = frm.get_docfield('items', 'batch_no'); + if (batch_no_field) { + batch_no_field.get_route_options_for_new_doc = function(row) { + return { + 'item': row.doc.item_code + } + }; + } }, set_warehouse: (frm) => { @@ -202,4 +202,4 @@ let set_missing_values = (frm) => { if (!r.exc) frm.refresh(); }, }); -}; \ No newline at end of file +}; From 13aaff30a5d6ae27d131eecee6f9f54575f2688c Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Mon, 18 Sep 2023 09:57:11 +0530 Subject: [PATCH 320/501] fix: company wise deferred accounting fields in item (#37023) * fix: company wise deferred accounting fields in item (#37023) * fix: move deferred accounts in accounting section * fix: move deferred check boxes in item accounting * fix: show company wise acc in filters * fix: fetch item deferred account from child table * fix: tests using deferred acc * refactor: use cached value * fix: cached value call * feat: patch to migrate deferred acc * fix: hardcode education module doctypes in patch * chore: resolve conflicts --------- Co-authored-by: Deepesh Garg (cherry picked from commit 099468e3cf1f96a32e2257729ee2a48024ab91d3) # Conflicts: # erpnext/patches.txt # erpnext/patches/v14_0/delete_education_doctypes.py # erpnext/stock/doctype/item/item.json * chore: resolve conflicts * chore: resolve conflicts --------- Co-authored-by: Gursheen Kaur Anand <40693548+GursheenK@users.noreply.github.com> Co-authored-by: Deepesh Garg --- .../purchase_invoice/test_purchase_invoice.py | 2 +- .../sales_invoice/test_sales_invoice.py | 4 +- .../test_deferred_revenue_and_expense.py | 4 +- erpnext/patches.txt | 1 + .../v14_0/delete_education_doctypes.py | 11 ++++ .../v14_0/delete_healthcare_doctypes.py | 2 +- ...rate_deferred_accounts_to_item_defaults.py | 39 ++++++++++++ erpnext/stock/doctype/item/item.js | 6 +- erpnext/stock/doctype/item/item.json | 61 +++++++------------ .../doctype/item_default/item_default.json | 31 +++++++++- erpnext/stock/get_item_details.py | 6 +- 11 files changed, 118 insertions(+), 49 deletions(-) create mode 100644 erpnext/patches/v14_0/migrate_deferred_accounts_to_item_defaults.py diff --git a/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py b/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py index 0f8e77952cf5..30265aeb50ea 100644 --- a/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py +++ b/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py @@ -1153,7 +1153,7 @@ def test_deferred_expense_via_journal_entry(self): item = create_item("_Test Item for Deferred Accounting", is_purchase_item=True) item.enable_deferred_expense = 1 - item.deferred_expense_account = deferred_account + item.item_defaults[0].deferred_expense_account = deferred_account item.save() pi = make_purchase_invoice(item=item.name, qty=1, rate=100, do_not_save=True) diff --git a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py index eee99dcfde04..378be113e7c1 100644 --- a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py +++ b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py @@ -2322,7 +2322,7 @@ def test_deferred_revenue(self): item = create_item("_Test Item for Deferred Accounting") item.enable_deferred_revenue = 1 - item.deferred_revenue_account = deferred_account + item.item_defaults[0].deferred_revenue_account = deferred_account item.no_of_months = 12 item.save() @@ -3102,7 +3102,7 @@ def test_multi_currency_deferred_revenue_via_journal_entry(self): item = create_item("_Test Item for Deferred Accounting") item.enable_deferred_expense = 1 - item.deferred_revenue_account = deferred_account + item.item_defaults[0].deferred_revenue_account = deferred_account item.save() si = create_sales_invoice( diff --git a/erpnext/accounts/report/deferred_revenue_and_expense/test_deferred_revenue_and_expense.py b/erpnext/accounts/report/deferred_revenue_and_expense/test_deferred_revenue_and_expense.py index 28d0c20a9187..7b1a9027780e 100644 --- a/erpnext/accounts/report/deferred_revenue_and_expense/test_deferred_revenue_and_expense.py +++ b/erpnext/accounts/report/deferred_revenue_and_expense/test_deferred_revenue_and_expense.py @@ -81,7 +81,7 @@ def test_deferred_revenue(self): self.create_item("_Test Internet Subscription", 0, self.warehouse, self.company) item = frappe.get_doc("Item", self.item) item.enable_deferred_revenue = 1 - item.deferred_revenue_account = self.deferred_revenue_account + item.item_defaults[0].deferred_revenue_account = self.deferred_revenue_account item.no_of_months = 3 item.save() @@ -150,7 +150,7 @@ def test_deferred_expense(self): self.create_item("_Test Office Desk", 0, self.warehouse, self.company) item = frappe.get_doc("Item", self.item) item.enable_deferred_expense = 1 - item.deferred_expense_account = self.deferred_expense_account + item.item_defaults[0].deferred_expense_account = self.deferred_expense_account item.no_of_months_exp = 3 item.save() diff --git a/erpnext/patches.txt b/erpnext/patches.txt index cde484eb0fa3..19f8dab9a176 100644 --- a/erpnext/patches.txt +++ b/erpnext/patches.txt @@ -339,5 +339,6 @@ erpnext.patches.v14_0.update_closing_balances #15-07-2023 execute:frappe.defaults.clear_default("fiscal_year") execute:frappe.db.set_single_value('Selling Settings', 'allow_negative_rates_for_items', 0) erpnext.patches.v14_0.correct_asset_value_if_je_with_workflow +erpnext.patches.v14_0.migrate_deferred_accounts_to_item_defaults # below migration patch should always run last erpnext.patches.v14_0.migrate_gl_to_payment_ledger diff --git a/erpnext/patches/v14_0/delete_education_doctypes.py b/erpnext/patches/v14_0/delete_education_doctypes.py index 76b2300fd2a1..55b64eaabd87 100644 --- a/erpnext/patches/v14_0/delete_education_doctypes.py +++ b/erpnext/patches/v14_0/delete_education_doctypes.py @@ -46,6 +46,17 @@ def execute(): for doctype in doctypes: frappe.delete_doc("DocType", doctype, ignore_missing=True) + titles = [ + "Fees", + "Student Admission", + "Grant Application", + "Chapter", + "Certification Application", + ] + items = frappe.get_all("Portal Menu Item", filters=[["title", "in", titles]], pluck="name") + for item in items: + frappe.delete_doc("Portal Menu Item", item, ignore_missing=True, force=True) + frappe.delete_doc("Module Def", "Education", ignore_missing=True, force=True) click.secho( diff --git a/erpnext/patches/v14_0/delete_healthcare_doctypes.py b/erpnext/patches/v14_0/delete_healthcare_doctypes.py index 2c699e4a9f21..896a4409507e 100644 --- a/erpnext/patches/v14_0/delete_healthcare_doctypes.py +++ b/erpnext/patches/v14_0/delete_healthcare_doctypes.py @@ -41,7 +41,7 @@ def execute(): for card in cards: frappe.delete_doc("Number Card", card, ignore_missing=True, force=True) - titles = ["Lab Test", "Prescription", "Patient Appointment"] + titles = ["Lab Test", "Prescription", "Patient Appointment", "Patient"] items = frappe.get_all("Portal Menu Item", filters=[["title", "in", titles]], pluck="name") for item in items: frappe.delete_doc("Portal Menu Item", item, ignore_missing=True, force=True) diff --git a/erpnext/patches/v14_0/migrate_deferred_accounts_to_item_defaults.py b/erpnext/patches/v14_0/migrate_deferred_accounts_to_item_defaults.py new file mode 100644 index 000000000000..44b830babb20 --- /dev/null +++ b/erpnext/patches/v14_0/migrate_deferred_accounts_to_item_defaults.py @@ -0,0 +1,39 @@ +import frappe + + +def execute(): + try: + item_dict = get_deferred_accounts() + add_to_item_defaults(item_dict) + except Exception: + frappe.db.rollback() + frappe.log_error("Failed to migrate deferred accounts in Item Defaults.") + + +def get_deferred_accounts(): + item = frappe.qb.DocType("Item") + return ( + frappe.qb.from_(item) + .select(item.name, item.deferred_expense_account, item.deferred_revenue_account) + .where((item.enable_deferred_expense == 1) | (item.enable_deferred_revenue == 1)) + .run(as_dict=True) + ) + + +def add_to_item_defaults(item_dict): + for item in item_dict: + add_company_wise_item_default(item, "deferred_expense_account") + add_company_wise_item_default(item, "deferred_revenue_account") + + +def add_company_wise_item_default(item, account_type): + company = frappe.get_cached_value("Account", item[account_type], "company") + if company and item[account_type]: + item_defaults = frappe.get_cached_value("Item", item["name"], "item_defaults") + for item_row in item_defaults: + if item_row.company == company: + frappe.set_value("Item Default", item_row.name, account_type, item[account_type]) + break + else: + item_defaults.append({"company": company, account_type: item[account_type]}) + frappe.set_value("Item", item["name"], "item_defaults", item_defaults) diff --git a/erpnext/stock/doctype/item/item.js b/erpnext/stock/doctype/item/item.js index 86f9af25e7a2..b306a41bb83d 100644 --- a/erpnext/stock/doctype/item/item.js +++ b/erpnext/stock/doctype/item/item.js @@ -350,18 +350,20 @@ $.extend(erpnext.item, { } } - frm.fields_dict['deferred_revenue_account'].get_query = function() { + frm.fields_dict["item_defaults"].grid.get_field("deferred_revenue_account").get_query = function(doc, cdt, cdn) { return { filters: { + "company": locals[cdt][cdn].company, 'root_type': 'Liability', "is_group": 0 } } } - frm.fields_dict['deferred_expense_account'].get_query = function() { + frm.fields_dict["item_defaults"].grid.get_field("deferred_expense_account").get_query = function(doc, cdt, cdn) { return { filters: { + "company": locals[cdt][cdn].company, 'root_type': 'Asset', "is_group": 0 } diff --git a/erpnext/stock/doctype/item/item.json b/erpnext/stock/doctype/item/item.json index 7f4ba032e860..7d0a387f43ef 100644 --- a/erpnext/stock/doctype/item/item.json +++ b/erpnext/stock/doctype/item/item.json @@ -70,6 +70,13 @@ "variant_based_on", "attributes", "accounting", + "deferred_accounting_section", + "enable_deferred_expense", + "no_of_months_exp", + "column_break_9s9o", + "enable_deferred_revenue", + "no_of_months", + "section_break_avcp", "item_defaults", "purchasing_tab", "purchase_uom", @@ -85,10 +92,6 @@ "delivered_by_supplier", "column_break2", "supplier_items", - "deferred_expense_section", - "enable_deferred_expense", - "deferred_expense_account", - "no_of_months_exp", "foreign_trade_details", "country_of_origin", "column_break_59", @@ -99,10 +102,6 @@ "is_sales_item", "column_break3", "max_discount", - "deferred_revenue", - "enable_deferred_revenue", - "deferred_revenue_account", - "no_of_months", "customer_details", "customer_items", "item_tax_section_break", @@ -657,20 +656,6 @@ "oldfieldname": "max_discount", "oldfieldtype": "Currency" }, - { - "collapsible": 1, - "fieldname": "deferred_revenue", - "fieldtype": "Section Break", - "label": "Deferred Revenue" - }, - { - "depends_on": "enable_deferred_revenue", - "fieldname": "deferred_revenue_account", - "fieldtype": "Link", - "ignore_user_permissions": 1, - "label": "Deferred Revenue Account", - "options": "Account" - }, { "default": "0", "fieldname": "enable_deferred_revenue", @@ -681,21 +666,7 @@ "depends_on": "enable_deferred_revenue", "fieldname": "no_of_months", "fieldtype": "Int", - "label": "No of Months" - }, - { - "collapsible": 1, - "fieldname": "deferred_expense_section", - "fieldtype": "Section Break", - "label": "Deferred Expense" - }, - { - "depends_on": "enable_deferred_expense", - "fieldname": "deferred_expense_account", - "fieldtype": "Link", - "ignore_user_permissions": 1, - "label": "Deferred Expense Account", - "options": "Account" + "label": "No of Months (Revenue)" }, { "default": "0", @@ -904,6 +875,20 @@ "fieldname": "accounting", "fieldtype": "Tab Break", "label": "Accounting" + }, + { + "fieldname": "column_break_9s9o", + "fieldtype": "Column Break" + }, + { + "fieldname": "section_break_avcp", + "fieldtype": "Section Break" + }, + { + "collapsible": 1, + "fieldname": "deferred_accounting_section", + "fieldtype": "Section Break", + "label": "Deferred Accounting" } ], "icon": "fa fa-tag", @@ -912,7 +897,7 @@ "index_web_pages_for_search": 1, "links": [], "make_attachments_public": 1, - "modified": "2023-07-14 17:18:18.658942", + "modified": "2023-09-11 13:46:32.688051", "modified_by": "Administrator", "module": "Stock", "name": "Item", diff --git a/erpnext/stock/doctype/item_default/item_default.json b/erpnext/stock/doctype/item_default/item_default.json index 042d398256a5..28956612762b 100644 --- a/erpnext/stock/doctype/item_default/item_default.json +++ b/erpnext/stock/doctype/item_default/item_default.json @@ -19,7 +19,11 @@ "selling_defaults", "selling_cost_center", "column_break_12", - "income_account" + "income_account", + "deferred_accounting_defaults_section", + "deferred_expense_account", + "column_break_kwad", + "deferred_revenue_account" ], "fields": [ { @@ -108,11 +112,34 @@ "fieldtype": "Link", "label": "Default Provisional Account", "options": "Account" + }, + { + "fieldname": "deferred_accounting_defaults_section", + "fieldtype": "Section Break", + "label": "Deferred Accounting Defaults" + }, + { + "depends_on": "eval: parent.enable_deferred_expense", + "fieldname": "deferred_expense_account", + "fieldtype": "Link", + "label": "Deferred Expense Account", + "options": "Account" + }, + { + "depends_on": "eval: parent.enable_deferred_revenue", + "fieldname": "deferred_revenue_account", + "fieldtype": "Link", + "label": "Deferred Revenue Account", + "options": "Account" + }, + { + "fieldname": "column_break_kwad", + "fieldtype": "Column Break" } ], "istable": 1, "links": [], - "modified": "2022-04-10 20:18:54.148195", + "modified": "2023-09-04 12:33:14.607267", "modified_by": "Administrator", "module": "Stock", "name": "Item Default", diff --git a/erpnext/stock/get_item_details.py b/erpnext/stock/get_item_details.py index f3adefb3e74c..f7eb859f8304 100644 --- a/erpnext/stock/get_item_details.py +++ b/erpnext/stock/get_item_details.py @@ -729,7 +729,11 @@ def get_default_discount_account(args, item): def get_default_deferred_account(args, item, fieldname=None): if item.get("enable_deferred_revenue") or item.get("enable_deferred_expense"): return ( - item.get(fieldname) + frappe.get_cached_value( + "Item Default", + {"parent": args.item_code, "company": args.get("company")}, + fieldname, + ) or args.get(fieldname) or frappe.get_cached_value("Company", args.company, "default_" + fieldname) ) From 8b2328c6d31b2985f40a5516a2a6377a1bc53218 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Mon, 2 Jan 2023 12:21:45 +0530 Subject: [PATCH 321/501] refactor: show balance checkbox in Accounts Settings (cherry picked from commit 1b78fae6fc5e61a02781abe298a6436364a6695d) # Conflicts: # erpnext/accounts/doctype/accounts_settings/accounts_settings.json --- .../accounts_settings/accounts_settings.json | 21 +++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/erpnext/accounts/doctype/accounts_settings/accounts_settings.json b/erpnext/accounts/doctype/accounts_settings/accounts_settings.json index 467c68f10218..bfd1eb37f479 100644 --- a/erpnext/accounts/doctype/accounts_settings/accounts_settings.json +++ b/erpnext/accounts/doctype/accounts_settings/accounts_settings.json @@ -64,9 +64,14 @@ "column_break_25", "frozen_accounts_modifier", "report_settings_sb", +<<<<<<< HEAD "banking_tab", "enable_party_matching", "enable_fuzzy_matching" +======= + "tab_break_dpet", + "show_balance_in_coa" +>>>>>>> 1b78fae6fc (refactor: show balance checkbox in Accounts Settings) ], "fields": [ { @@ -355,6 +360,7 @@ "label": "Allow multi-currency invoices against single party account " }, { +<<<<<<< HEAD "default": "0", "description": "Split Early Payment Discount Loss into Income and Tax Loss", "fieldname": "book_tax_discount_loss", @@ -416,6 +422,17 @@ "fieldname": "ignore_account_closing_balance", "fieldtype": "Check", "label": "Ignore Account Closing Balance" +======= + "fieldname": "tab_break_dpet", + "fieldtype": "Tab Break", + "label": "Chart Of Accounts" + }, + { + "default": "1", + "fieldname": "show_balance_in_coa", + "fieldtype": "Check", + "label": "Show Balances in Chart Of Accounts" +>>>>>>> 1b78fae6fc (refactor: show balance checkbox in Accounts Settings) } ], "icon": "icon-cog", @@ -423,7 +440,11 @@ "index_web_pages_for_search": 1, "issingle": 1, "links": [], +<<<<<<< HEAD "modified": "2023-07-27 15:05:34.000264", +======= + "modified": "2023-01-02 12:07:42.434214", +>>>>>>> 1b78fae6fc (refactor: show balance checkbox in Accounts Settings) "modified_by": "Administrator", "module": "Accounts", "name": "Accounts Settings", From 18702841af6fc13ae89e1a2bc56ea703143e076b Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Mon, 2 Jan 2023 12:22:33 +0530 Subject: [PATCH 322/501] refactor: Show Balance in COA based on Accounts Settings (cherry picked from commit 23fbe86d51193c7242119bec81d4d63c8191d49d) --- .../accounts/doctype/account/account_tree.js | 55 ++++++++++--------- 1 file changed, 30 insertions(+), 25 deletions(-) diff --git a/erpnext/accounts/doctype/account/account_tree.js b/erpnext/accounts/doctype/account/account_tree.js index 8ae90ceb3834..d537adfcbfdc 100644 --- a/erpnext/accounts/doctype/account/account_tree.js +++ b/erpnext/accounts/doctype/account/account_tree.js @@ -56,36 +56,41 @@ frappe.treeview_settings["Account"] = { accounts = nodes; } - const get_balances = frappe.call({ - method: 'erpnext.accounts.utils.get_account_balances', - args: { - accounts: accounts, - company: cur_tree.args.company - }, - }); + frappe.db.get_single_value("Accounts Settings", "show_balance_in_coa").then((value) => { + if(value) { + + const get_balances = frappe.call({ + method: 'erpnext.accounts.utils.get_account_balances', + args: { + accounts: accounts, + company: cur_tree.args.company + }, + }); - get_balances.then(r => { - if (!r.message || r.message.length == 0) return; + get_balances.then(r => { + if (!r.message || r.message.length == 0) return; - for (let account of r.message) { + for (let account of r.message) { - const node = cur_tree.nodes && cur_tree.nodes[account.value]; - if (!node || node.is_root) continue; + const node = cur_tree.nodes && cur_tree.nodes[account.value]; + if (!node || node.is_root) continue; - // show Dr if positive since balance is calculated as debit - credit else show Cr - const balance = account.balance_in_account_currency || account.balance; - const dr_or_cr = balance > 0 ? "Dr": "Cr"; - const format = (value, currency) => format_currency(Math.abs(value), currency); + // show Dr if positive since balance is calculated as debit - credit else show Cr + const balance = account.balance_in_account_currency || account.balance; + const dr_or_cr = balance > 0 ? "Dr": "Cr"; + const format = (value, currency) => format_currency(Math.abs(value), currency); - if (account.balance!==undefined) { - node.parent && node.parent.find('.balance-area').remove(); - $('' - + (account.balance_in_account_currency ? - (format(account.balance_in_account_currency, account.account_currency) + " / ") : "") - + format(account.balance, account.company_currency) - + " " + dr_or_cr - + '').insertBefore(node.$ul); - } + if (account.balance!==undefined) { + node.parent && node.parent.find('.balance-area').remove(); + $('' + + (account.balance_in_account_currency ? + (format(account.balance_in_account_currency, account.account_currency) + " / ") : "") + + format(account.balance, account.company_currency) + + " " + dr_or_cr + + '').insertBefore(node.$ul); + } + } + }); } }); }, From 79321f56caf12af2d2d5abfd3260104cc1279936 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Mon, 18 Sep 2023 10:29:29 +0530 Subject: [PATCH 323/501] chore: resolve conflicts --- .../accounts_settings/accounts_settings.json | 14 +++----------- 1 file changed, 3 insertions(+), 11 deletions(-) diff --git a/erpnext/accounts/doctype/accounts_settings/accounts_settings.json b/erpnext/accounts/doctype/accounts_settings/accounts_settings.json index bfd1eb37f479..3ab9d2b60d5b 100644 --- a/erpnext/accounts/doctype/accounts_settings/accounts_settings.json +++ b/erpnext/accounts/doctype/accounts_settings/accounts_settings.json @@ -64,14 +64,11 @@ "column_break_25", "frozen_accounts_modifier", "report_settings_sb", -<<<<<<< HEAD "banking_tab", "enable_party_matching", - "enable_fuzzy_matching" -======= + "enable_fuzzy_matching", "tab_break_dpet", "show_balance_in_coa" ->>>>>>> 1b78fae6fc (refactor: show balance checkbox in Accounts Settings) ], "fields": [ { @@ -360,7 +357,6 @@ "label": "Allow multi-currency invoices against single party account " }, { -<<<<<<< HEAD "default": "0", "description": "Split Early Payment Discount Loss into Income and Tax Loss", "fieldname": "book_tax_discount_loss", @@ -422,7 +418,8 @@ "fieldname": "ignore_account_closing_balance", "fieldtype": "Check", "label": "Ignore Account Closing Balance" -======= + }, + { "fieldname": "tab_break_dpet", "fieldtype": "Tab Break", "label": "Chart Of Accounts" @@ -432,7 +429,6 @@ "fieldname": "show_balance_in_coa", "fieldtype": "Check", "label": "Show Balances in Chart Of Accounts" ->>>>>>> 1b78fae6fc (refactor: show balance checkbox in Accounts Settings) } ], "icon": "icon-cog", @@ -440,11 +436,7 @@ "index_web_pages_for_search": 1, "issingle": 1, "links": [], -<<<<<<< HEAD "modified": "2023-07-27 15:05:34.000264", -======= - "modified": "2023-01-02 12:07:42.434214", ->>>>>>> 1b78fae6fc (refactor: show balance checkbox in Accounts Settings) "modified_by": "Administrator", "module": "Accounts", "name": "Accounts Settings", From c41cb3930c76efb620cf8bc27672dab8c5302e6c Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Mon, 18 Sep 2023 13:03:08 +0530 Subject: [PATCH 324/501] fix: Don't allow merging accounts with different currency (#37074) * fix: Don't allow merging accounts with different currency (#37074) * fix: Don't allow merging accounts with different currency * test: Update conflicting values * test: Update conflicting values (cherry picked from commit 5e21e7cd1da12cad1d3491d92aafa2664c8518a8) # Conflicts: # erpnext/accounts/doctype/account/account.js # erpnext/accounts/doctype/account/account.py * chore: resolve conflicts --------- Co-authored-by: Deepesh Garg --- erpnext/accounts/doctype/account/account.js | 3 - erpnext/accounts/doctype/account/account.py | 39 ++++-- .../accounts/doctype/account/test_account.py | 119 ++++++++++-------- .../doctype/ledger_merge/ledger_merge.py | 3 - 4 files changed, 94 insertions(+), 70 deletions(-) diff --git a/erpnext/accounts/doctype/account/account.js b/erpnext/accounts/doctype/account/account.js index 320e1cab7c3d..7d63b257faff 100644 --- a/erpnext/accounts/doctype/account/account.js +++ b/erpnext/accounts/doctype/account/account.js @@ -117,9 +117,6 @@ frappe.ui.form.on('Account', { args: { old: frm.doc.name, new: data.name, - is_group: frm.doc.is_group, - root_type: frm.doc.root_type, - company: frm.doc.company }, callback: function(r) { if(!r.exc) { diff --git a/erpnext/accounts/doctype/account/account.py b/erpnext/accounts/doctype/account/account.py index bbe4c54a71ad..22ddc2ffae3b 100644 --- a/erpnext/accounts/doctype/account/account.py +++ b/erpnext/accounts/doctype/account/account.py @@ -18,6 +18,10 @@ class BalanceMismatchError(frappe.ValidationError): pass +class InvalidAccountMergeError(frappe.ValidationError): + pass + + class Account(NestedSet): nsm_parent_field = "parent_account" @@ -444,24 +448,35 @@ def update_account_number(name, account_name, account_number=None, from_descenda @frappe.whitelist() -def merge_account(old, new, is_group, root_type, company): +def merge_account(old, new): # Validate properties before merging - if not frappe.db.exists("Account", new): - throw(_("Account {0} does not exist").format(new)) + new_account = frappe.get_cached_doc("Account", new) + old_account = frappe.get_cached_doc("Account", old) - val = list(frappe.db.get_value("Account", new, ["is_group", "root_type", "company"])) + if not new_account: + throw(_("Account {0} does not exist").format(new)) - if val != [cint(is_group), root_type, company]: + if ( + cint(new_account.is_group), + new_account.root_type, + new_account.company, + cstr(new_account.account_currency), + ) != ( + cint(old_account.is_group), + old_account.root_type, + old_account.company, + cstr(old_account.account_currency), + ): throw( - _( - """Merging is only possible if following properties are same in both records. Is Group, Root Type, Company""" - ) + msg=_( + """Merging is only possible if following properties are same in both records. Is Group, Root Type, Company and Account Currency""" + ), + title=("Invalid Accounts"), + exc=InvalidAccountMergeError, ) - if is_group and frappe.db.get_value("Account", new, "parent_account") == old: - frappe.db.set_value( - "Account", new, "parent_account", frappe.db.get_value("Account", old, "parent_account") - ) + if old_account.is_group and new_account.parent_account == old: + new_account.db_set("parent_account", frappe.get_cached_value("Account", old, "parent_account")) frappe.rename_doc("Account", old, new, merge=1, force=1) diff --git a/erpnext/accounts/doctype/account/test_account.py b/erpnext/accounts/doctype/account/test_account.py index 62303bd723f6..30eebef7fba4 100644 --- a/erpnext/accounts/doctype/account/test_account.py +++ b/erpnext/accounts/doctype/account/test_account.py @@ -7,7 +7,11 @@ import frappe from frappe.test_runner import make_test_records -from erpnext.accounts.doctype.account.account import merge_account, update_account_number +from erpnext.accounts.doctype.account.account import ( + InvalidAccountMergeError, + merge_account, + update_account_number, +) from erpnext.stock import get_company_default_inventory_account, get_warehouse_account test_dependencies = ["Company"] @@ -47,49 +51,53 @@ def test_rename_account(self): frappe.delete_doc("Account", "1211-11-4 - 6 - Debtors 1 - Test - - _TC") def test_merge_account(self): - if not frappe.db.exists("Account", "Current Assets - _TC"): - acc = frappe.new_doc("Account") - acc.account_name = "Current Assets" - acc.is_group = 1 - acc.parent_account = "Application of Funds (Assets) - _TC" - acc.company = "_Test Company" - acc.insert() - if not frappe.db.exists("Account", "Securities and Deposits - _TC"): - acc = frappe.new_doc("Account") - acc.account_name = "Securities and Deposits" - acc.parent_account = "Current Assets - _TC" - acc.is_group = 1 - acc.company = "_Test Company" - acc.insert() - if not frappe.db.exists("Account", "Earnest Money - _TC"): - acc = frappe.new_doc("Account") - acc.account_name = "Earnest Money" - acc.parent_account = "Securities and Deposits - _TC" - acc.company = "_Test Company" - acc.insert() - if not frappe.db.exists("Account", "Cash In Hand - _TC"): - acc = frappe.new_doc("Account") - acc.account_name = "Cash In Hand" - acc.is_group = 1 - acc.parent_account = "Current Assets - _TC" - acc.company = "_Test Company" - acc.insert() - if not frappe.db.exists("Account", "Accumulated Depreciation - _TC"): - acc = frappe.new_doc("Account") - acc.account_name = "Accumulated Depreciation" - acc.parent_account = "Fixed Assets - _TC" - acc.company = "_Test Company" - acc.account_type = "Accumulated Depreciation" - acc.insert() + create_account( + account_name="Current Assets", + is_group=1, + parent_account="Application of Funds (Assets) - _TC", + company="_Test Company", + ) + + create_account( + account_name="Securities and Deposits", + is_group=1, + parent_account="Current Assets - _TC", + company="_Test Company", + ) + + create_account( + account_name="Earnest Money", + parent_account="Securities and Deposits - _TC", + company="_Test Company", + ) + + create_account( + account_name="Cash In Hand", + is_group=1, + parent_account="Current Assets - _TC", + company="_Test Company", + ) + + create_account( + account_name="Receivable INR", + parent_account="Current Assets - _TC", + company="_Test Company", + account_currency="INR", + ) + + create_account( + account_name="Receivable USD", + parent_account="Current Assets - _TC", + company="_Test Company", + account_currency="USD", + ) - doc = frappe.get_doc("Account", "Securities and Deposits - _TC") parent = frappe.db.get_value("Account", "Earnest Money - _TC", "parent_account") self.assertEqual(parent, "Securities and Deposits - _TC") - merge_account( - "Securities and Deposits - _TC", "Cash In Hand - _TC", doc.is_group, doc.root_type, doc.company - ) + merge_account("Securities and Deposits - _TC", "Cash In Hand - _TC") + parent = frappe.db.get_value("Account", "Earnest Money - _TC", "parent_account") # Parent account of the child account changes after merging @@ -98,30 +106,28 @@ def test_merge_account(self): # Old account doesn't exist after merging self.assertFalse(frappe.db.exists("Account", "Securities and Deposits - _TC")) - doc = frappe.get_doc("Account", "Current Assets - _TC") - # Raise error as is_group property doesn't match self.assertRaises( - frappe.ValidationError, + InvalidAccountMergeError, merge_account, "Current Assets - _TC", "Accumulated Depreciation - _TC", - doc.is_group, - doc.root_type, - doc.company, ) - doc = frappe.get_doc("Account", "Capital Stock - _TC") - # Raise error as root_type property doesn't match self.assertRaises( - frappe.ValidationError, + InvalidAccountMergeError, merge_account, "Capital Stock - _TC", "Softwares - _TC", - doc.is_group, - doc.root_type, - doc.company, + ) + + # Raise error as currency doesn't match + self.assertRaises( + InvalidAccountMergeError, + merge_account, + "Receivable INR - _TC", + "Receivable USD - _TC", ) def test_account_sync(self): @@ -400,11 +406,20 @@ def create_account(**kwargs): "Account", filters={"account_name": kwargs.get("account_name"), "company": kwargs.get("company")} ) if account: - return account + account = frappe.get_doc("Account", account) + account.update( + dict( + is_group=kwargs.get("is_group", 0), + parent_account=kwargs.get("parent_account"), + ) + ) + account.save() + return account.name else: account = frappe.get_doc( dict( doctype="Account", + is_group=kwargs.get("is_group", 0), account_name=kwargs.get("account_name"), account_type=kwargs.get("account_type"), parent_account=kwargs.get("parent_account"), diff --git a/erpnext/accounts/doctype/ledger_merge/ledger_merge.py b/erpnext/accounts/doctype/ledger_merge/ledger_merge.py index 18e5a1ac85bd..6e89d039932a 100644 --- a/erpnext/accounts/doctype/ledger_merge/ledger_merge.py +++ b/erpnext/accounts/doctype/ledger_merge/ledger_merge.py @@ -49,9 +49,6 @@ def start_merge(docname): merge_account( row.account, ledger_merge.account, - ledger_merge.is_group, - ledger_merge.root_type, - ledger_merge.company, ) row.db_set("merged", 1) frappe.db.commit() From 4b700b726fe4576b215623d21c1ab0d425852205 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Mon, 18 Sep 2023 12:39:23 +0530 Subject: [PATCH 325/501] refactor: better date filters in `Get Outstanding Invoices` dialog (cherry picked from commit 9004721859bb940b29bbcf3a16816a5dc72a1b37) --- erpnext/accounts/doctype/payment_entry/payment_entry.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/erpnext/accounts/doctype/payment_entry/payment_entry.py b/erpnext/accounts/doctype/payment_entry/payment_entry.py index 0e2de4ac3952..c5501a58306a 100644 --- a/erpnext/accounts/doctype/payment_entry/payment_entry.py +++ b/erpnext/accounts/doctype/payment_entry/payment_entry.py @@ -1455,6 +1455,14 @@ def get_outstanding_reference_documents(args): fieldname, args.get(date_fields[0]), args.get(date_fields[1]) ) posting_and_due_date.append(ple[fieldname][args.get(date_fields[0]) : args.get(date_fields[1])]) + elif args.get(date_fields[0]): + # if only from date is supplied + condition += " and {0} >= '{1}'".format(fieldname, args.get(date_fields[0])) + posting_and_due_date.append(ple[fieldname].gte(args.get(date_fields[0]))) + elif args.get(date_fields[1]): + # if only to date is supplied + condition += " and {0} <= '{1}'".format(fieldname, args.get(date_fields[1])) + posting_and_due_date.append(ple[fieldname].lte(args.get(date_fields[1]))) if args.get("company"): condition += " and company = {0}".format(frappe.db.escape(args.get("company"))) From e62b783f340844783352030de422c6be14b3ddfc Mon Sep 17 00:00:00 2001 From: rohitwaghchaure Date: Mon, 18 Sep 2023 16:10:21 +0530 Subject: [PATCH 326/501] fix: don't set from warehouse for purchase material request (#37132) --- .../production_plan/production_plan.js | 9 +++++ .../production_plan/production_plan.py | 10 +++++- .../production_plan/test_production_plan.py | 35 +++++++++++++++++++ 3 files changed, 53 insertions(+), 1 deletion(-) diff --git a/erpnext/manufacturing/doctype/production_plan/production_plan.js b/erpnext/manufacturing/doctype/production_plan/production_plan.js index 46c554c1e80d..72438ddceeae 100644 --- a/erpnext/manufacturing/doctype/production_plan/production_plan.js +++ b/erpnext/manufacturing/doctype/production_plan/production_plan.js @@ -476,6 +476,15 @@ frappe.ui.form.on("Material Request Plan Item", { } }) } + }, + + material_request_type(frm, cdt, cdn) { + let row = locals[cdt][cdn]; + + if (row.from_warehouse && + row.material_request_type !== "Material Transfer") { + frappe.model.set_value(cdt, cdn, 'from_warehouse', ''); + } } }); diff --git a/erpnext/manufacturing/doctype/production_plan/production_plan.py b/erpnext/manufacturing/doctype/production_plan/production_plan.py index 795cb97fffaf..fcaae79d6c5e 100644 --- a/erpnext/manufacturing/doctype/production_plan/production_plan.py +++ b/erpnext/manufacturing/doctype/production_plan/production_plan.py @@ -40,6 +40,12 @@ def validate(self): self._rename_temporary_references() validate_uom_is_integer(self, "stock_uom", "planned_qty") self.validate_sales_orders() + self.validate_material_request_type() + + def validate_material_request_type(self): + for row in self.get("mr_items"): + if row.from_warehouse and row.material_request_type != "Material Transfer": + row.from_warehouse = "" @frappe.whitelist() def validate_sales_orders(self, sales_order=None): @@ -749,7 +755,9 @@ def make_material_request(self): "items", { "item_code": item.item_code, - "from_warehouse": item.from_warehouse, + "from_warehouse": item.from_warehouse + if material_request_type == "Material Transfer" + else None, "qty": item.quantity, "schedule_date": schedule_date, "warehouse": item.warehouse, diff --git a/erpnext/manufacturing/doctype/production_plan/test_production_plan.py b/erpnext/manufacturing/doctype/production_plan/test_production_plan.py index f5778847b9cd..780806450505 100644 --- a/erpnext/manufacturing/doctype/production_plan/test_production_plan.py +++ b/erpnext/manufacturing/doctype/production_plan/test_production_plan.py @@ -1087,6 +1087,41 @@ def test_resered_qty_for_production_plan_for_material_requests_with_multi_UOM(se ) self.assertEqual(reserved_qty_after_mr, before_qty) + def test_from_warehouse_for_purchase_material_request(self): + from erpnext.stock.doctype.warehouse.test_warehouse import create_warehouse + from erpnext.stock.utils import get_or_make_bin + + create_item("RM-TEST-123 For Purchase", valuation_rate=100) + bin_name = get_or_make_bin("RM-TEST-123 For Purchase", "_Test Warehouse - _TC") + t_warehouse = create_warehouse("_Test Store - _TC") + make_stock_entry( + item_code="Raw Material Item 1", + qty=5, + rate=100, + target=t_warehouse, + ) + + plan = create_production_plan(item_code="Test Production Item 1", do_not_save=1) + mr_items = get_items_for_material_requests( + plan.as_dict(), warehouses=[{"warehouse": t_warehouse}] + ) + + for d in mr_items: + plan.append("mr_items", d) + + plan.save() + + for row in plan.mr_items: + if row.material_request_type == "Material Transfer": + self.assertEqual(row.from_warehouse, t_warehouse) + + row.material_request_type = "Purchase" + + plan.save() + + for row in plan.mr_items: + self.assertFalse(row.from_warehouse) + def test_skip_available_qty_for_sub_assembly_items(self): from erpnext.manufacturing.doctype.bom.test_bom import create_nested_bom From 366325ca3ca0a99bb2384755c6f1cc7021649380 Mon Sep 17 00:00:00 2001 From: RitvikSardana <65544983+RitvikSardana@users.noreply.github.com> Date: Mon, 18 Sep 2023 16:32:45 +0530 Subject: [PATCH 327/501] fix: Duplicate Serial Nos validation in POS (#36927) * fix: added validation for duplicate serial nos in pos * chore: code cleanup * chore: code cleanup * fix: removed duplicate batch number validation * chore: code cleanup --- .../doctype/pos_invoice/pos_invoice.py | 15 +++------ .../doctype/pos_invoice/test_pos_invoice.py | 31 +++++++++++++++++++ 2 files changed, 35 insertions(+), 11 deletions(-) diff --git a/erpnext/accounts/doctype/pos_invoice/pos_invoice.py b/erpnext/accounts/doctype/pos_invoice/pos_invoice.py index e7a7ae24ceb1..ad3a70a862fc 100644 --- a/erpnext/accounts/doctype/pos_invoice/pos_invoice.py +++ b/erpnext/accounts/doctype/pos_invoice/pos_invoice.py @@ -1,5 +1,7 @@ # Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors and contributors # For license information, please see license.txt + + import collections import frappe @@ -43,7 +45,6 @@ def validate(self): self.validate_debit_to_acc() self.validate_write_off_account() self.validate_change_amount() - self.validate_duplicate_serial_and_batch_no() self.validate_change_account() self.validate_item_cost_centers() self.validate_warehouse() @@ -56,6 +57,7 @@ def validate(self): self.validate_payment_amount() self.validate_loyalty_transaction() self.validate_company_with_pos_company() + self.validate_duplicate_serial_no() if self.coupon_code: from erpnext.accounts.doctype.pricing_rule.utils import validate_coupon_code @@ -156,27 +158,18 @@ def validate_pos_reserved_serial_nos(self, item): title=_("Item Unavailable"), ) - def validate_duplicate_serial_and_batch_no(self): + def validate_duplicate_serial_no(self): serial_nos = [] - batch_nos = [] for row in self.get("items"): if row.serial_no: serial_nos = row.serial_no.split("\n") - if row.batch_no and not row.serial_no: - batch_nos.append(row.batch_no) - if serial_nos: for key, value in collections.Counter(serial_nos).items(): if value > 1: frappe.throw(_("Duplicate Serial No {0} found").format("key")) - if batch_nos: - for key, value in collections.Counter(batch_nos).items(): - if value > 1: - frappe.throw(_("Duplicate Batch No {0} found").format("key")) - def validate_pos_reserved_batch_qty(self, item): filters = {"item_code": item.item_code, "warehouse": item.warehouse, "batch_no": item.batch_no} diff --git a/erpnext/accounts/doctype/pos_invoice/test_pos_invoice.py b/erpnext/accounts/doctype/pos_invoice/test_pos_invoice.py index 3132fdd259a0..bd2fee3b0ad9 100644 --- a/erpnext/accounts/doctype/pos_invoice/test_pos_invoice.py +++ b/erpnext/accounts/doctype/pos_invoice/test_pos_invoice.py @@ -464,6 +464,37 @@ def test_delivered_serialized_item_transaction(self): pos2.insert() self.assertRaises(frappe.ValidationError, pos2.submit) + def test_pos_invoice_with_duplicate_serial_no(self): + from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos + from erpnext.stock.doctype.stock_entry.test_stock_entry import make_serialized_item + + se = make_serialized_item( + company="_Test Company", + target_warehouse="Stores - _TC", + cost_center="Main - _TC", + expense_account="Cost of Goods Sold - _TC", + ) + + serial_nos = get_serial_nos(se.get("items")[0].serial_no) + + pos = create_pos_invoice( + company="_Test Company", + debit_to="Debtors - _TC", + account_for_change_amount="Cash - _TC", + warehouse="Stores - _TC", + income_account="Sales - _TC", + expense_account="Cost of Goods Sold - _TC", + cost_center="Main - _TC", + item=se.get("items")[0].item_code, + rate=1000, + qty=2, + do_not_save=1, + ) + + pos.get("items")[0].has_serial_no = 1 + pos.get("items")[0].serial_no = serial_nos[0] + "\n" + serial_nos[0] + self.assertRaises(frappe.ValidationError, pos.submit) + def test_invalid_serial_no_validation(self): from erpnext.stock.doctype.stock_entry.test_stock_entry import make_serialized_item From dfe5f63f592a01eee786239ac89fa360b3c96534 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Tue, 19 Sep 2023 13:07:30 +0530 Subject: [PATCH 328/501] refactor: more generic filters in Accounts Receivable report (backport #37131) (#37137) * refactor: replace 'customer' filter with 'party_type' and 'party' (cherry picked from commit ac650d2e7a088f468551ed21874c653055ef1422) * chore: remove stale code (cherry picked from commit 083c82c20607bee5b817ab792f8615dc72aaa97b) * refactor(test): AR output filtered on USD customers (cherry picked from commit 08d91ab831928c88e25d88682abc7b97fa91f516) --------- Co-authored-by: ruthra kumar --- .../accounts_payable/test_accounts_payable.py | 2 +- .../accounts_receivable.js | 59 +++++++------------ .../accounts_receivable.py | 28 +++++---- .../test_accounts_receivable.py | 37 ++++++++++++ 4 files changed, 76 insertions(+), 50 deletions(-) diff --git a/erpnext/accounts/report/accounts_payable/test_accounts_payable.py b/erpnext/accounts/report/accounts_payable/test_accounts_payable.py index cb84cf4fc0a6..3cf93cc8659b 100644 --- a/erpnext/accounts/report/accounts_payable/test_accounts_payable.py +++ b/erpnext/accounts/report/accounts_payable/test_accounts_payable.py @@ -24,7 +24,7 @@ def setUp(self): def tearDown(self): frappe.db.rollback() - def test_accounts_receivable_with_supplier(self): + def test_accounts_payable_for_foreign_currency_supplier(self): pi = self.create_purchase_invoice(do_not_submit=True) pi.currency = "USD" pi.conversion_rate = 80 diff --git a/erpnext/accounts/report/accounts_receivable/accounts_receivable.js b/erpnext/accounts/report/accounts_receivable/accounts_receivable.js index cb8ec876e9ee..bb00d616dbcc 100644 --- a/erpnext/accounts/report/accounts_receivable/accounts_receivable.js +++ b/erpnext/accounts/report/accounts_receivable/accounts_receivable.js @@ -38,32 +38,31 @@ frappe.query_reports["Accounts Receivable"] = { } }, { - "fieldname": "customer", - "label": __("Customer"), + "fieldname": "party_type", + "label": __("Party Type"), "fieldtype": "Link", - "options": "Customer", + "options": "Party Type", + "Default": "Customer", + get_query: () => { + return { + filters: { + 'account_type': 'Receivable' + } + }; + }, on_change: () => { - var customer = frappe.query_report.get_filter_value('customer'); - var company = frappe.query_report.get_filter_value('company'); - if (customer) { - frappe.db.get_value('Customer', customer, ["customer_name", "payment_terms"], function(value) { - frappe.query_report.set_filter_value('customer_name', value["customer_name"]); - frappe.query_report.set_filter_value('payment_terms', value["payment_terms"]); - }); + frappe.query_report.set_filter_value('party', ""); + let party_type = frappe.query_report.get_filter_value('party_type'); + frappe.query_report.toggle_filter_display('customer_group', frappe.query_report.get_filter_value('party_type') !== "Customer"); - frappe.db.get_value('Customer Credit Limit', {'parent': customer, 'company': company}, - ["credit_limit"], function(value) { - if (value) { - frappe.query_report.set_filter_value('credit_limit', value["credit_limit"]); - } - }, "Customer"); - } else { - frappe.query_report.set_filter_value('customer_name', ""); - frappe.query_report.set_filter_value('credit_limit', ""); - frappe.query_report.set_filter_value('payment_terms', ""); - } } }, + { + "fieldname":"party", + "label": __("Party"), + "fieldtype": "Dynamic Link", + "options": "party_type", + }, { "fieldname": "party_account", "label": __("Receivable Account"), @@ -174,24 +173,6 @@ frappe.query_reports["Accounts Receivable"] = { "fieldname": "show_remarks", "label": __("Show Remarks"), "fieldtype": "Check", - }, - { - "fieldname": "customer_name", - "label": __("Customer Name"), - "fieldtype": "Data", - "hidden": 1 - }, - { - "fieldname": "payment_terms", - "label": __("Payment Tems"), - "fieldtype": "Data", - "hidden": 1 - }, - { - "fieldname": "credit_limit", - "label": __("Credit Limit"), - "fieldtype": "Currency", - "hidden": 1 } ], diff --git a/erpnext/accounts/report/accounts_receivable/accounts_receivable.py b/erpnext/accounts/report/accounts_receivable/accounts_receivable.py index 14f8993727a9..794240236525 100755 --- a/erpnext/accounts/report/accounts_receivable/accounts_receivable.py +++ b/erpnext/accounts/report/accounts_receivable/accounts_receivable.py @@ -769,15 +769,12 @@ def prepare_conditions(self): self.or_filters = [] for party_type in self.party_type: - party_type_field = scrub(party_type) - self.or_filters.append(self.ple.party_type == party_type) + self.add_common_filters() - self.add_common_filters(party_type_field=party_type_field) - - if party_type_field == "customer": + if self.account_type == "Receivable": self.add_customer_filters() - elif party_type_field == "supplier": + elif self.account_type == "Payable": self.add_supplier_filters() if self.filters.cost_center: @@ -793,16 +790,13 @@ def get_cost_center_conditions(self): ] self.qb_selection_filter.append(self.ple.cost_center.isin(cost_center_list)) - def add_common_filters(self, party_type_field): + def add_common_filters(self): if self.filters.company: self.qb_selection_filter.append(self.ple.company == self.filters.company) if self.filters.finance_book: self.qb_selection_filter.append(self.ple.finance_book == self.filters.finance_book) - if self.filters.get(party_type_field): - self.qb_selection_filter.append(self.ple.party == self.filters.get(party_type_field)) - if self.filters.get("party_type"): self.qb_selection_filter.append(self.filters.party_type == self.ple.party_type) @@ -969,6 +963,20 @@ def get_columns(self): fieldtype="Link", options="Contact", ) + if self.filters.party_type == "Customer": + self.add_column( + _("Customer Name"), + fieldname="customer_name", + fieldtype="Link", + options="Customer", + ) + elif self.filters.party_type == "Supplier": + self.add_column( + _("Supplier Name"), + fieldname="supplier_name", + fieldtype="Link", + options="Supplier", + ) self.add_column(label=_("Cost Center"), fieldname="cost_center", fieldtype="Data") self.add_column(label=_("Voucher Type"), fieldname="voucher_type", fieldtype="Data") diff --git a/erpnext/accounts/report/accounts_receivable/test_accounts_receivable.py b/erpnext/accounts/report/accounts_receivable/test_accounts_receivable.py index 0c7d931d2d54..b98916ee443f 100644 --- a/erpnext/accounts/report/accounts_receivable/test_accounts_receivable.py +++ b/erpnext/accounts/report/accounts_receivable/test_accounts_receivable.py @@ -568,3 +568,40 @@ def test_party_account_filter(self): row.account_currency, ], ) + + def test_usd_customer_filter(self): + filters = { + "company": self.company, + "party_type": "Customer", + "party": self.customer, + "report_date": today(), + "range1": 30, + "range2": 60, + "range3": 90, + "range4": 120, + } + + si = self.create_sales_invoice(no_payment_schedule=True, do_not_submit=True) + si.currency = "USD" + si.conversion_rate = 80 + si.debit_to = self.debtors_usd + si.save().submit() + name = si.name + + # check invoice grand total and invoiced column's value for 3 payment terms + report = execute(filters) + + expected = { + "voucher_type": si.doctype, + "voucher_no": si.name, + "party_account": self.debtors_usd, + "customer_name": self.customer, + "invoiced": 100.0, + "outstanding": 100.0, + "account_currency": "USD", + } + self.assertEqual(len(report[1]), 1) + report_output = report[1][0] + for field in expected: + with self.subTest(field=field): + self.assertEqual(report_output.get(field), expected.get(field)) From af05864e6db336b2897b1138ec25e59caba0a568 Mon Sep 17 00:00:00 2001 From: RitvikSardana <65544983+RitvikSardana@users.noreply.github.com> Date: Tue, 19 Sep 2023 13:09:35 +0530 Subject: [PATCH 329/501] fix: POS opening Issue if Product Bundle is available (#37138) fix: POS opening issue because of Product Bundle --- erpnext/accounts/doctype/pos_invoice/pos_invoice.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/accounts/doctype/pos_invoice/pos_invoice.py b/erpnext/accounts/doctype/pos_invoice/pos_invoice.py index ad3a70a862fc..7c013b6abef4 100644 --- a/erpnext/accounts/doctype/pos_invoice/pos_invoice.py +++ b/erpnext/accounts/doctype/pos_invoice/pos_invoice.py @@ -676,7 +676,7 @@ def get_bundle_availability(bundle_item_code, warehouse): item_pos_reserved_qty = get_pos_reserved_qty(item.item_code, warehouse) available_qty = item_bin_qty - item_pos_reserved_qty - max_available_bundles = available_qty / item.stock_qty + max_available_bundles = available_qty / item.qty if bundle_bin_qty > max_available_bundles and frappe.get_value( "Item", item.item_code, "is_stock_item" ): From 29ff0ce286255307ac78652776a8e158b89222ae Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Tue, 19 Sep 2023 14:01:47 +0530 Subject: [PATCH 330/501] feat: provision to create RIV from `Stock Ledger Invariant Check` report (backport #37115) (#37147) feat: provision to create RIV from `Stock Ledger Invariant Check` report (#37115) * feat: provision to create RIV from `Stock Ledger Invariant Check` report * fix: `linter` (cherry picked from commit 9c9d0ecb73c581d965f8fafe3347be01147f1a4b) Co-authored-by: s-aga-r --- .../stock_ledger_invariant_check.js | 83 ++++++++++++++----- .../stock_ledger_invariant_check.py | 33 ++++++++ 2 files changed, 96 insertions(+), 20 deletions(-) diff --git a/erpnext/stock/report/stock_ledger_invariant_check/stock_ledger_invariant_check.js b/erpnext/stock/report/stock_ledger_invariant_check/stock_ledger_invariant_check.js index 31f389f236e7..74849058c3c1 100644 --- a/erpnext/stock/report/stock_ledger_invariant_check/stock_ledger_invariant_check.js +++ b/erpnext/stock/report/stock_ledger_invariant_check/stock_ledger_invariant_check.js @@ -3,23 +3,23 @@ /* eslint-disable */ const DIFFERNCE_FIELD_NAMES = [ - "difference_in_qty", - "fifo_qty_diff", - "fifo_value_diff", - "fifo_valuation_diff", - "valuation_diff", - "fifo_difference_diff", - "diff_value_diff" + 'difference_in_qty', + 'fifo_qty_diff', + 'fifo_value_diff', + 'fifo_valuation_diff', + 'valuation_diff', + 'fifo_difference_diff', + 'diff_value_diff' ]; -frappe.query_reports["Stock Ledger Invariant Check"] = { - "filters": [ +frappe.query_reports['Stock Ledger Invariant Check'] = { + 'filters': [ { - "fieldname": "item_code", - "fieldtype": "Link", - "label": "Item", - "mandatory": 1, - "options": "Item", + 'fieldname': 'item_code', + 'fieldtype': 'Link', + 'label': 'Item', + 'mandatory': 1, + 'options': 'Item', get_query: function() { return { filters: {is_stock_item: 1, has_serial_no: 0} @@ -27,18 +27,61 @@ frappe.query_reports["Stock Ledger Invariant Check"] = { } }, { - "fieldname": "warehouse", - "fieldtype": "Link", - "label": "Warehouse", - "mandatory": 1, - "options": "Warehouse", + 'fieldname': 'warehouse', + 'fieldtype': 'Link', + 'label': 'Warehouse', + 'mandatory': 1, + 'options': 'Warehouse', } ], + formatter (value, row, column, data, default_formatter) { value = default_formatter(value, row, column, data); if (DIFFERNCE_FIELD_NAMES.includes(column.fieldname) && Math.abs(data[column.fieldname]) > 0.001) { - value = "" + value + ""; + value = '' + value + ''; } return value; }, + + get_datatable_options(options) { + return Object.assign(options, { + checkboxColumn: true, + }); + }, + + onload(report) { + report.page.add_inner_button(__('Create Reposting Entry'), () => { + let message = ` +
+

+ Reposting Entry will change the value of + accounts Stock In Hand, and Stock Expenses + in the Trial Balance report and will also change + the Balance Value in the Stock Balance report. +

+

Are you sure you want to create a Reposting Entry?

+
`; + let indexes = frappe.query_report.datatable.rowmanager.getCheckedRows(); + let selected_rows = indexes.map(i => frappe.query_report.data[i]); + + if (!selected_rows.length) { + frappe.throw(__('Please select a row to create a Reposting Entry')); + } + else if (selected_rows.length > 1) { + frappe.throw(__('Please select only one row to create a Reposting Entry')); + } + else { + frappe.confirm(__(message), () => { + frappe.call({ + method: 'erpnext.stock.report.stock_ledger_invariant_check.stock_ledger_invariant_check.create_reposting_entries', + args: { + rows: selected_rows, + item_code: frappe.query_report.get_filter_values().item_code, + warehouse: frappe.query_report.get_filter_values().warehouse, + } + }); + }); + } + }); + }, }; diff --git a/erpnext/stock/report/stock_ledger_invariant_check/stock_ledger_invariant_check.py b/erpnext/stock/report/stock_ledger_invariant_check/stock_ledger_invariant_check.py index ed0e2fc31bd1..e11c9bb7891e 100644 --- a/erpnext/stock/report/stock_ledger_invariant_check/stock_ledger_invariant_check.py +++ b/erpnext/stock/report/stock_ledger_invariant_check/stock_ledger_invariant_check.py @@ -5,6 +5,7 @@ import frappe from frappe import _ +from frappe.utils import get_link_to_form, parse_json SLE_FIELDS = ( "name", @@ -258,3 +259,35 @@ def get_columns(): "label": _("H - J"), }, ] + + +@frappe.whitelist() +def create_reposting_entries(rows, item_code=None, warehouse=None): + if isinstance(rows, str): + rows = parse_json(rows) + + entries = [] + for row in rows: + row = frappe._dict(row) + + try: + doc = frappe.get_doc( + { + "doctype": "Repost Item Valuation", + "based_on": "Item and Warehouse", + "status": "Queued", + "item_code": item_code or row.item_code, + "warehouse": warehouse or row.warehouse, + "posting_date": row.posting_date, + "posting_time": row.posting_time, + "allow_nagative_stock": 1, + } + ).submit() + + entries.append(get_link_to_form("Repost Item Valuation", doc.name)) + except frappe.DuplicateEntryError: + continue + + if entries: + entries = ", ".join(entries) + frappe.msgprint(_("Reposting entries created: {0}").format(entries)) From 480a0ca7a8d97bbaa4de103a722e823ff5aed47f Mon Sep 17 00:00:00 2001 From: Anand Baburajan Date: Tue, 19 Sep 2023 15:18:35 +0530 Subject: [PATCH 331/501] fix: fetch logic for repay_from_salary in loan_repayment [v14] (#37135) * fix: fetch logic for repay_from_salary in loan_repayment * chore: only fetch repay_from_salary if field exists --- .../doctype/loan_repayment/loan_repayment.js | 9 ++++++++- .../doctype/loan_repayment/loan_repayment.py | 6 ++++++ 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/erpnext/loan_management/doctype/loan_repayment/loan_repayment.js b/erpnext/loan_management/doctype/loan_repayment/loan_repayment.js index 82a2d802b80d..53581339faef 100644 --- a/erpnext/loan_management/doctype/loan_repayment/loan_repayment.js +++ b/erpnext/loan_management/doctype/loan_repayment/loan_repayment.js @@ -6,7 +6,14 @@ frappe.ui.form.on('Loan Repayment', { // refresh: function(frm) { - // } + // }, + + setup: function(frm) { + if (frappe.meta.has_field("Loan Repayment", "repay_from_salary")) { + frm.add_fetch("against_loan", "repay_from_salary", "repay_from_salary"); + } + }, + onload: function(frm) { frm.set_query('against_loan', function() { return { diff --git a/erpnext/loan_management/doctype/loan_repayment/loan_repayment.py b/erpnext/loan_management/doctype/loan_repayment/loan_repayment.py index f0e113b0e7b6..0d16c7f0c50c 100644 --- a/erpnext/loan_management/doctype/loan_repayment/loan_repayment.py +++ b/erpnext/loan_management/doctype/loan_repayment/loan_repayment.py @@ -80,6 +80,12 @@ def set_missing_values(self, amounts): if amounts.get("due_date"): self.due_date = amounts.get("due_date") + if hasattr(self, "repay_from_salary") and hasattr(self, "payroll_payable_account"): + if self.repay_from_salary and not self.payroll_payable_account: + frappe.throw(_("Please set Payroll Payable Account in Loan Repayment")) + elif not self.repay_from_salary and self.payroll_payable_account: + self.repay_from_salary = 1 + def check_future_entries(self): future_repayment_date = frappe.db.get_value( "Loan Repayment", From 699ad8080236e9b54c93e71587a027080ce422eb Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Tue, 19 Sep 2023 19:30:47 +0530 Subject: [PATCH 332/501] fix: incorrect stock ledger entries in DN (backport #36944) (#37066) fix: incorrect stock ledger entries in DN (#36944) (cherry picked from commit 0e83190c190ea122fd6d221704c29556b0394d48) Co-authored-by: rohitwaghchaure --- erpnext/stock/doctype/delivery_note/delivery_note.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/erpnext/stock/doctype/delivery_note/delivery_note.json b/erpnext/stock/doctype/delivery_note/delivery_note.json index ff0e1b66bf8b..11f2cafc35d5 100644 --- a/erpnext/stock/doctype/delivery_note/delivery_note.json +++ b/erpnext/stock/doctype/delivery_note/delivery_note.json @@ -1252,6 +1252,7 @@ "depends_on": "eval: doc.is_internal_customer", "fieldname": "set_target_warehouse", "fieldtype": "Link", + "ignore_user_permissions": 1, "in_standard_filter": 1, "label": "Set Target Warehouse", "no_copy": 1, @@ -1399,7 +1400,7 @@ "idx": 146, "is_submittable": 1, "links": [], - "modified": "2023-06-16 14:58:55.066602", + "modified": "2023-09-04 14:15:28.363184", "modified_by": "Administrator", "module": "Stock", "name": "Delivery Note", From 1cb8c64c94858134c4765f2c21ebbce7394ec795 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Tue, 19 Sep 2023 19:44:29 +0530 Subject: [PATCH 333/501] fix: after applying coupon code, field in_words not updated (#37133) fix: after applying coupon code, field in_words not updated (#37133) * fix: after applying coupon code, field in_words not updated * fix: changed the order of the function set_total_in_words (cherry picked from commit 03f0abf6ded44021583041b53355737bf385660b) Co-authored-by: RitvikSardana <65544983+RitvikSardana@users.noreply.github.com> --- erpnext/controllers/accounts_controller.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/erpnext/controllers/accounts_controller.py b/erpnext/controllers/accounts_controller.py index df77e5260f5d..bd2354e74522 100644 --- a/erpnext/controllers/accounts_controller.py +++ b/erpnext/controllers/accounts_controller.py @@ -168,7 +168,6 @@ def validate(self): self.validate_value("base_grand_total", ">=", 0) validate_return(self) - self.set_total_in_words() self.validate_all_documents_schedule() @@ -207,6 +206,8 @@ def validate(self): if self.doctype != "Material Request" and not self.ignore_pricing_rule: apply_pricing_rule_on_transaction(self) + self.set_total_in_words() + def before_cancel(self): validate_einvoice_fields(self) From 8236814270f2a74bb5ae46ea7d3818c2ddc7619b Mon Sep 17 00:00:00 2001 From: Raffael Meyer <14891507+barredterra@users.noreply.github.com> Date: Tue, 19 Sep 2023 16:27:43 +0200 Subject: [PATCH 334/501] fix: german translation of Sales and Purchase Invoice (#37122) * fix: german translation of Purchase Invoice * fix: german translation of Sales Invoice (cherry picked from commit 84a9000db2f92e81dec825be1df42672305aec5d) # Conflicts: # erpnext/translations/de.csv --- erpnext/translations/de.csv | 23 +++++++++++++---------- 1 file changed, 13 insertions(+), 10 deletions(-) diff --git a/erpnext/translations/de.csv b/erpnext/translations/de.csv index 42d313f4db19..6dae398e334d 100644 --- a/erpnext/translations/de.csv +++ b/erpnext/translations/de.csv @@ -1312,7 +1312,7 @@ Invalid GSTIN! A GSTIN must have 15 characters.,Ungültige GSTIN! Eine GSTIN mus Invalid GSTIN! First 2 digits of GSTIN should match with State number {0}.,Ungültige GSTIN! Die ersten beiden Ziffern von GSTIN sollten mit der Statusnummer {0} übereinstimmen., Invalid GSTIN! The input you've entered doesn't match the format of GSTIN.,Ungültige GSTIN! Die von Ihnen eingegebene Eingabe stimmt nicht mit dem Format von GSTIN überein., Invalid Posting Time,Ungültige Buchungszeit, -Invalid Purchase Invoice,Ungültige Einkaufsrechnung, +Invalid Purchase Invoice,Ungültige Eingangsrechnung, Invalid attribute {0} {1},Ungültiges Attribut {0} {1}, Invalid quantity specified for item {0}. Quantity should be greater than 0.,Ungültzige Anzahl für Artikel {0} angegeben. Anzahl sollte größer als 0 sein., Invalid reference {0} {1},Ungültige Referenz {0} {1}, @@ -1969,8 +1969,12 @@ Please check Multi Currency option to allow accounts with other currency,"Bitte Please click on 'Generate Schedule',"Bitte auf ""Zeitplan generieren"" klicken", Please click on 'Generate Schedule' to fetch Serial No added for Item {0},"Bitte auf ""Zeitplan generieren"" klicken, um die Seriennummer für Artikel {0} abzurufen", Please click on 'Generate Schedule' to get schedule,"Bitte auf ""Zeitplan generieren"" klicken, um den Zeitplan zu erhalten", +<<<<<<< HEAD Please confirm once you have completed your training,"Bitte bestätigen Sie, sobald Sie Ihre Ausbildung abgeschlossen haben", Please create purchase receipt or purchase invoice for the item {0},Bitte erstellen Sie eine Kaufquittung oder eine Kaufrechnung für den Artikel {0}, +======= +Please create purchase receipt or purchase invoice for the item {0},Bitte erstellen Sie eine Kaufquittung oder eine Eingangsrechnungen für den Artikel {0}, +>>>>>>> 84a9000db2 (fix: german translation of Sales and Purchase Invoice (#37122)) Please define grade for Threshold 0%,Bitte definieren Sie Grade for Threshold 0%, Please enable Applicable on Booking Actual Expenses,Bitte aktivieren Sie Anwendbar bei der Buchung von tatsächlichen Ausgaben, Please enable Applicable on Purchase Order and Applicable on Booking Actual Expenses,Bitte aktivieren Sie Anwendbar bei Bestellung und Anwendbar bei Buchung von tatsächlichen Ausgaben, @@ -4937,7 +4941,7 @@ POS Customer Group,POS Kundengruppe, POS Field,POS-Feld, POS Item Group,POS Artikelgruppe, Company Address,Anschrift des Unternehmens, -Update Stock,Lagerbestand aktualisieren, +Update Stock,Lagerbestand aktualisieren, Ignore Pricing Rule,Preisregel ignorieren, Applicable for Users,Anwendbar für Benutzer, Sales Invoice Payment,Ausgangsrechnung-Zahlungen, @@ -5134,7 +5138,6 @@ ACC-SINV-.YYYY.-,ACC-SINV-.JJJJ.-, Include Payment (POS),(POS) Zahlung einschließen, Offline POS Name,Offline-Verkaufsstellen-Name, Is Return (Credit Note),ist Rücklieferung (Gutschrift), -Return Against Sales Invoice,Zurück zur Kundenrechnung, Update Billed Amount in Sales Order,Aktualisierung des Rechnungsbetrags im Auftrag, Customer PO Details,Auftragsdetails, Customer's Purchase Order,Bestellung des Kunden, @@ -7673,8 +7676,8 @@ Default Company Bank Account,Standard-Bankkonto des Unternehmens, From Lead,Aus Lead, Account Manager,Kundenberater, Accounts Manager,Buchhalter, -Allow Sales Invoice Creation Without Sales Order,Ermöglichen Sie die Erstellung von Kundenrechnungen ohne Auftrag, -Allow Sales Invoice Creation Without Delivery Note,Ermöglichen Sie die Erstellung einer Ausgangsrechnung ohne Lieferschein, +Allow Sales Invoice Creation Without Sales Order,Ermöglichen Sie die Erstellung von Ausgangsrechnungen ohne Auftrag, +Allow Sales Invoice Creation Without Delivery Note,Ermöglichen Sie die Erstellung von Ausgangsrechnungen ohne Lieferschein, Default Price List,Standardpreisliste, Primary Address and Contact,Hauptadresse und -kontakt, "Select, to make the customer searchable with these fields","Wählen Sie, um den Kunden mit diesen Feldern durchsuchbar zu machen", @@ -9598,8 +9601,8 @@ This role is allowed to submit transactions that exceed credit limits,"Diese Rol Show Inclusive Tax in Print,Inklusive Steuern im Druck anzeigen, Only select this if you have set up the Cash Flow Mapper documents,"Wählen Sie diese Option nur, wenn Sie die Cash Flow Mapper-Dokumente eingerichtet haben", Payment Channel,Zahlungskanal, -Is Purchase Order Required for Purchase Invoice & Receipt Creation?,Ist für die Erstellung von Kaufrechnungen und Quittungen eine Bestellung erforderlich?, -Is Purchase Receipt Required for Purchase Invoice Creation?,Ist für die Erstellung der Kaufrechnung ein Kaufbeleg erforderlich?, +Is Purchase Order Required for Purchase Invoice & Receipt Creation?,Ist für die Erstellung von Eingangsrechnungen und Quittungen eine Bestellung erforderlich?, +Is Purchase Receipt Required for Purchase Invoice Creation?,Ist für die Erstellung der Eingangsrechnungen ein Kaufbeleg erforderlich?, Maintain Same Rate Throughout the Purchase Cycle,Behalten Sie den gleichen Preis während des gesamten Kaufzyklus bei, Allow Item To Be Added Multiple Times in a Transaction,"Zulassen, dass ein Element in einer Transaktion mehrmals hinzugefügt wird", Suppliers,Lieferanten, @@ -9657,8 +9660,8 @@ Purchase Order already created for all Sales Order items,Bestellung bereits für Select Items,Gegenstände auswählen, Against Default Supplier,Gegen Standardlieferanten, Auto close Opportunity after the no. of days mentioned above,Gelegenheit zum automatischen Schließen nach der Nr. der oben genannten Tage, -Is Sales Order Required for Sales Invoice & Delivery Note Creation?,Ist ein Auftrag für die Erstellung von Kundenrechnungen und Lieferscheinen erforderlich?, -Is Delivery Note Required for Sales Invoice Creation?,Ist für die Erstellung der Ausgangsrechnung ein Lieferschein erforderlich?, +Is Sales Order Required for Sales Invoice & Delivery Note Creation?,Ist ein Auftrag für die Erstellung von Ausgangsrechnungen und Lieferscheinen erforderlich?, +Is Delivery Note Required for Sales Invoice Creation?,Ist ein Lieferschein für die Erstellung von Ausgangsrechnungen erforderlich?, How often should Project and Company be updated based on Sales Transactions?,Wie oft sollten Projekt und Unternehmen basierend auf Verkaufstransaktionen aktualisiert werden?, Allow User to Edit Price List Rate in Transactions,Benutzer darf Preisliste in Transaktionen bearbeiten, Allow Item to Be Added Multiple Times in a Transaction,"Zulassen, dass ein Element in einer Transaktion mehrmals hinzugefügt wird", @@ -9804,7 +9807,7 @@ or it is not the default inventory account,oder es ist nicht das Standard-Invent Expense Head Changed,Ausgabenkopf geändert, because expense is booked against this account in Purchase Receipt {},weil die Kosten für dieses Konto im Kaufbeleg {} gebucht werden, as no Purchase Receipt is created against Item {}. ,da für Artikel {} kein Kaufbeleg erstellt wird., -This is done to handle accounting for cases when Purchase Receipt is created after Purchase Invoice,"Dies erfolgt zur Abrechnung von Fällen, in denen der Kaufbeleg nach der Kaufrechnung erstellt wird", +This is done to handle accounting for cases when Purchase Receipt is created after Purchase Invoice,"Dies erfolgt zur Abrechnung von Fällen, in denen der Kaufbeleg nach der Eingangsrechnung erstellt wird", Purchase Order Required for item {},Bestellung erforderlich für Artikel {}, To submit the invoice without purchase order please set {} ,"Um die Rechnung ohne Bestellung einzureichen, setzen Sie bitte {}", as {} in {},wie in {}, From 8c14dbd7ac4d60c3f0df95252510eaef08007790 Mon Sep 17 00:00:00 2001 From: HENRY Florian Date: Tue, 19 Sep 2023 16:56:05 +0200 Subject: [PATCH 335/501] chore: translation manual backport of #35697 (#37053) chore: translation manual backport of #35697 --- erpnext/translations/fr.csv | 149 ++++++++++++++++++------------------ 1 file changed, 74 insertions(+), 75 deletions(-) diff --git a/erpnext/translations/fr.csv b/erpnext/translations/fr.csv index eb9076b4062f..0865e2f7a7d9 100644 --- a/erpnext/translations/fr.csv +++ b/erpnext/translations/fr.csv @@ -115,7 +115,7 @@ Add Customers,Ajouter des clients, Add Employees,Ajouter des employés, Add Item,Ajouter un Article, Add Items,Ajouter des articles, -Add Leads,Créer des Prospects, +Add Leads,Créer des Leads, Add Multiple Tasks,Ajouter plusieurs tâches, Add Row,Ajouter une Ligne, Add Sales Partners,Ajouter des partenaires commerciaux, @@ -658,8 +658,8 @@ Create Invoice,Créer une facture, Create Invoices,Créer des factures, Create Job Card,Créer une carte de travail, Create Journal Entry,Créer une entrée de journal, -Create Lead,Créer un Prospect, -Create Leads,Créer des Prospects, +Create Lead,Créer un Lead, +Create Leads,Créer des Lead, Create Maintenance Visit,Créer une visite de maintenance, Create Material Request,Créer une demande de matériel, Create Multiple,Créer plusieurs, @@ -951,7 +951,7 @@ End time cannot be before start time,L'heure de fin ne peut pas être avant l'he Ends On date cannot be before Next Contact Date.,La date de fin ne peut pas être avant la prochaine date de contact, Energy,Énergie, Engineer,Ingénieur, -Enough Parts to Build,Pièces Suffisantes pour Construire +Enough Parts to Build,Pièces Suffisantes pour Construire, Enroll,Inscrire, Enrolling student,Inscrire un étudiant, Enrolling students,Inscription des étudiants, @@ -1426,13 +1426,12 @@ Last Purchase Price,Dernier prix d'achat, Last Purchase Rate,Dernier Prix d'Achat, Latest,Dernier, Latest price updated in all BOMs,Prix les plus récents mis à jour dans toutes les nomenclatures, -Lead,Prospect, -Lead Count,Nombre de Prospects, +Lead Count,Nombre de Lead, Lead Owner,Responsable du Prospect, -Lead Owner cannot be same as the Lead,Le Responsable du Prospect ne peut pas être identique au Prospect, +Lead Owner cannot be same as the Lead,Le Responsable du Prospect ne peut pas être identique au Lead, Lead Time Days,Jours de Délai, Lead to Quotation,Du Prospect au Devis, -"Leads help you get business, add all your contacts and more as your leads","Les prospects vous aident à obtenir des contrats, ajoutez tous vos contacts et plus dans votre liste de prospects", +"Leads help you get business, add all your contacts and more as your leads","Les lead vous aident à obtenir des contrats, ajoutez tous vos contacts et plus dans votre liste de lead", Learn,Apprendre, Leave Approval Notification,Notification d'approbation de congés, Leave Blocked,Laisser Verrouillé, @@ -1596,7 +1595,7 @@ Middle Name,Deuxième Nom, Middle Name (Optional),Deuxième Prénom (Optionnel), Min Amt can not be greater than Max Amt,Min Amt ne peut pas être supérieur à Max Amt, Min Qty can not be greater than Max Qty,Qté Min ne peut pas être supérieure à Qté Max, -Minimum Lead Age (Days),Âge Minimum du Prospect (Jours), +Minimum Lead Age (Days),Âge Minimum du lead (Jours), Miscellaneous Expenses,Charges Diverses, Missing Currency Exchange Rates for {0},Taux de Change Manquant pour {0}, Missing email template for dispatch. Please set one in Delivery Settings.,Modèle de courrier électronique manquant pour l'envoi. Veuillez en définir un dans les paramètres de livraison., @@ -1676,7 +1675,7 @@ New {0} pricing rules are created,De nouvelles règles de tarification {0} sont Newsletters,Newsletters, Newspaper Publishers,Éditeurs de journaux, Next,Suivant, -Next Contact By cannot be same as the Lead Email Address,Prochain Contact Par ne peut être identique à l’Adresse Email du Prospect, +Next Contact By cannot be same as the Lead Email Address,Prochain Contact Par ne peut être identique à l’Adresse Email du Lead, Next Contact Date cannot be in the past,La Date de Prochain Contact ne peut pas être dans le passé, Next Steps,Prochaines étapes, No Action,Pas d'action, @@ -1808,9 +1807,9 @@ Operation Time must be greater than 0 for Operation {0},Temps de l'Opération do Operations,Opérations, Operations cannot be left blank,Les opérations ne peuvent pas être laissées vides, Opp Count,Compte d'Opportunités, -Opp/Lead %,Opp / Prospect %, +Opp/Lead %,Opp / Lead %, Opportunities,Opportunités, -Opportunities by lead source,Opportunités par source de plomb, +Opportunities by lead source,Opportunités par source de lead, Opportunity,Opportunité, Opportunity Amount,Montant de l'opportunité, Optional Holiday List not set for leave period {0},Une liste de vacances facultative n'est pas définie pour la période de congé {0}, @@ -2007,7 +2006,7 @@ Please mention Basic and HRA component in Company,Veuillez mentionner les compos Please mention Round Off Account in Company,Veuillez indiquer le Compte d’Arrondi de la Société, Please mention Round Off Cost Center in Company,Veuillez indiquer le Centre de Coûts d’Arrondi de la Société, Please mention no of visits required,Veuillez indiquer le nb de visites requises, -Please mention the Lead Name in Lead {0},Veuillez mentionner le nom du Prospect dans le Prospect {0}, +Please mention the Lead Name in Lead {0},Veuillez mentionner le nom du Lead dans le Lead {0}, Please pull items from Delivery Note,Veuillez récupérer les articles des Bons de Livraison, Please register the SIREN number in the company information file,Veuillez enregistrer le numéro SIREN dans la fiche d'information de la société, Please remove this Invoice {0} from C-Form {1},Veuillez retirez cette Facture {0} du C-Form {1}, @@ -2277,7 +2276,7 @@ Queued for replacing the BOM. It may take a few minutes.,En file d'attente pour Queued for updating latest price in all Bill of Materials. It may take a few minutes.,Mise à jour des prix les plus récents dans toutes les nomenclatures en file d'attente. Cela peut prendre quelques minutes., Quick Journal Entry,Écriture Rapide dans le Journal, Quot Count,Compte de Devis, -Quot/Lead %,Devis / Prospects %, +Quot/Lead %,Devis / Lead %, Quotation,Devis, Quotation {0} is cancelled,Devis {0} est annulée, Quotation {0} not of type {1},Le devis {0} n'est pas du type {1}, @@ -2285,7 +2284,7 @@ Quotations,Devis, "Quotations are proposals, bids you have sent to your customers","Les devis sont des propositions, offres que vous avez envoyées à vos clients", Quotations received from Suppliers.,Devis reçus des Fournisseurs., Quotations: ,Devis :, -Quotes to Leads or Customers.,Devis de Prospects ou Clients., +Quotes to Leads or Customers.,Devis de Lead ou Clients., RFQs are not allowed for {0} due to a scorecard standing of {1},Les Appels d'Offres ne sont pas autorisés pour {0} en raison d'une note de {1} sur la fiche d'évaluation, Range,Plage, Rate,Prix, @@ -2888,7 +2887,7 @@ Supplies made to UIN holders,Fournitures faites aux titulaires de l'UIN, Supplies made to Unregistered Persons,Fournitures faites à des personnes non inscrites, Suppliies made to Composition Taxable Persons,Suppleies à des personnes assujetties à la composition, Supply Type,Type d'approvisionnement, -Support,"Assistance/Support", +Support,Assistance/Support, Support Analytics,Analyse de l'assistance, Support Settings,Paramètres du module Assistance, Support Tickets,Ticket d'assistance, @@ -3037,7 +3036,7 @@ To Date must be greater than From Date,La date de fin doit être supérieure à To Date should be within the Fiscal Year. Assuming To Date = {0},La Date Finale doit être dans l'exercice. En supposant Date Finale = {0}, To Datetime,À la Date, To Deliver,À Livrer, -{} To Deliver,{} à livrer +{} To Deliver,{} à livrer, To Deliver and Bill,À Livrer et Facturer, To Fiscal Year,À l'année fiscale, To GSTIN,GSTIN (Destination), @@ -3122,7 +3121,7 @@ Total(Amt),Total (Mnt), Total(Qty),Total (Qté), Traceability,Traçabilité, Traceback,Retraçage, -Track Leads by Lead Source.,Suivre les prospects par sources, +Track Leads by Lead Source.,Suivre les leads par sources, Training,Formation, Training Event,Événement de formation, Training Events,Événements de formation, @@ -3243,8 +3242,8 @@ View Chart of Accounts,Voir le plan comptable, View Fees Records,Voir les honoraires, View Form,Voir le formulaire, View Lab Tests,Afficher les tests de laboratoire, -View Leads,Voir Prospects, -View Ledger,Voir le Livre, +View Leads,Voir Lead, +View Ledger,Voir le Journal, View Now,Voir maintenant, View a list of all the help videos,Afficher la liste de toutes les vidéos d'aide, View in Cart,Voir Panier, @@ -3677,7 +3676,7 @@ Couldn't Set Service Level Agreement {0}.,Impossible de définir le contrat de s Country,Pays, Country Code in File does not match with country code set up in the system,Le code de pays dans le fichier ne correspond pas au code de pays configuré dans le système, Create New Contact,Créer un nouveau contact, -Create New Lead,Créer une nouvelle piste, +Create New Lead,Créer une nouvelle lead, Create Pick List,Créer une liste de choix, Create Quality Inspection for Item {0},Créer un contrôle qualité pour l'article {0}, Creating Accounts...,Création de comptes ..., @@ -3784,7 +3783,7 @@ Group Warehouses cannot be used in transactions. Please change the value of {0}, Help,Aidez-moi, Help Article,Article d’Aide, "Helps you keep tracks of Contracts based on Supplier, Customer and Employee","Vous aide à garder une trace des contrats en fonction du fournisseur, client et employé", -Helps you manage appointments with your leads,Vous aide à gérer les rendez-vous avec vos prospects, +Helps you manage appointments with your leads,Vous aide à gérer les rendez-vous avec vos leads, Home,Accueil, IBAN is not valid,IBAN n'est pas valide, Import Data from CSV / Excel files.,Importer des données à partir de fichiers CSV / Excel, @@ -3880,7 +3879,7 @@ Only expired allocation can be cancelled,Seule l'allocation expirée peut être Only users with the {0} role can create backdated leave applications,Seuls les utilisateurs avec le rôle {0} peuvent créer des demandes de congé antidatées, Open,Ouvert, Open Contact,Contact ouvert, -Open Lead,Ouvrir le Prospect, +Open Lead,Ouvrir le Lead, Opening and Closing,Ouverture et fermeture, Operating Cost as per Work Order / BOM,Coût d'exploitation selon l'ordre de fabrication / nomenclature, Order Amount,Montant de la commande, @@ -3926,7 +3925,7 @@ Please select another payment method. Stripe does not support transactions in cu Please select the customer.,S'il vous plaît sélectionner le client., Please set a Supplier against the Items to be considered in the Purchase Order.,Veuillez définir un fournisseur par rapport aux articles à prendre en compte dans la Commande d'Achat., Please set account heads in GST Settings for Compnay {0},Définissez les en-têtes de compte dans les paramètres de la TPS pour le service {0}., -Please set an email id for the Lead {0},Veuillez définir un identifiant de messagerie pour le prospect {0}., +Please set an email id for the Lead {0},Veuillez définir un identifiant de messagerie pour le lead {0}., Please set default UOM in Stock Settings,Veuillez définir l'UdM par défaut dans les paramètres de stock, Please set filter based on Item or Warehouse due to a large amount of entries.,Veuillez définir le filtre en fonction de l'article ou de l'entrepôt en raison d'une grande quantité d'entrées., Please set up the Campaign Schedule in the Campaign {0},Configurez le calendrier de la campagne dans la campagne {0}., @@ -4943,8 +4942,8 @@ Min Qty,Qté Min, Max Qty,Qté Max, Min Amt,Montant Min, Max Amt,Montant Max, -"If rate is zero them item will be treated as ""Free Item""",Si le prix est à 0 alors l'article sera traité comme article gratuit -Is Recursive,Est récursif +"If rate is zero them item will be treated as ""Free Item""",Si le prix est à 0 alors l'article sera traité comme article gratuit, +Is Recursive,Est récursif, "Discounts to be applied in sequential ranges like buy 1 get 1, buy 2 get 2, buy 3 get 3 and so on","La remise sera appliquée séquentiellement telque : acheter 1 => recupérer 1, acheter 2 => recupérer 2, acheter 3 => recupérer 3, etc..." Period Settings,Paramètres de période, Margin,Marge, @@ -5600,7 +5599,7 @@ Call Log,Journal d'appel, Received By,Reçu par, Caller Information,Informations sur l'appelant, Contact Name,Nom du Contact, -Lead Name,Nom du Prospect, +Lead Name,Nom du Lead, Ringing,Sonnerie, Missed,Manqué, Call Duration in seconds,Durée d'appel en secondes, @@ -5668,7 +5667,7 @@ Fulfilment Terms and Conditions,Termes et conditions d'exécution, Contract Template Fulfilment Terms,Conditions d'exécution du modèle de contrat, Email Campaign,Campagne Email, Email Campaign For ,Campagne d'email pour, -Lead is an Organization,Le prospect est une organisation, +Lead is an Organization,Le Lead est une organisation, CRM-LEAD-.YYYY.-,CRM-LEAD-.YYYY.-, Person Name,Nom de la Personne, Lost Quotation,Devis Perdu, @@ -5683,7 +5682,7 @@ Next Contact Date,Date du Prochain Contact, Ends On,Se termine le, Address & Contact,Adresse & Contact, Mobile No.,N° Mobile., -Lead Type,Type de Prospect, +Lead Type,Type de Lead, Channel Partner,Partenaire de Canal, Consultant,Consultant, Market Segment,Part de Marché, @@ -5706,7 +5705,7 @@ Opportunity Lost Reason,Raison perdue, Potential Sales Deal,Ventes Potentielles, CRM-OPP-.YYYY.-,CRM-OPP-YYYY.-, Opportunity From,Opportunité De, -Customer / Lead Name,Nom du Client / Prospect, +Customer / Lead Name,Nom du Client / Lead, Opportunity Type,Type d'Opportunité, Converted By,Converti par, Sales Stage,Stade de vente, @@ -5716,7 +5715,7 @@ To Discuss,À Discuter, With Items,Avec Articles, Probability (%),Probabilité (%), Contact Info,Information du Contact, -Customer / Lead Address,Adresse du Client / Prospect, +Customer / Lead Address,Adresse du Lead / Prospect, Contact Mobile No,N° de Portable du Contact, Enter name of campaign if source of enquiry is campaign,Entrez le nom de la campagne si la source de l'enquête est une campagne, Opportunity Date,Date d'Opportunité, @@ -7240,7 +7239,7 @@ Replace,Remplacer, Update latest price in all BOMs,Mettre à jour le prix le plus récent dans toutes les nomenclatures, BOM Website Item,Article de nomenclature du Site Internet, BOM Website Operation,Opération de nomenclature du Site Internet, -Operation Time,Durée de l'Opération +Operation Time,Durée de l'Opération, PO-JOB.#####,PO-JOB. #####, Timing Detail,Détail du timing, Time Logs,Time Logs, @@ -7645,7 +7644,7 @@ Campaign Schedules,Horaires de campagne, Buyer of Goods and Services.,Acheteur des Biens et Services., CUST-.YYYY.-,CUST-.YYYY.-, Default Company Bank Account,Compte bancaire d'entreprise par défaut, -From Lead,Du Prospect, +From Lead,Du Lead, Account Manager,Gestionnaire de compte, Allow Sales Invoice Creation Without Sales Order,Autoriser la création de factures de vente sans commande client, Allow Sales Invoice Creation Without Delivery Note,Autoriser la création de factures de vente sans bon de livraison, @@ -7672,7 +7671,7 @@ Installation Date,Date d'Installation, Installation Time,Temps d'Installation, Installation Note Item,Article Remarque d'Installation, Installed Qty,Qté Installée, -Lead Source,Source du Prospect, +Lead Source,Source du Lead, Period Start Date,Date de début de la période, Period End Date,Date de fin de la période, Cashier,Caissier, @@ -8517,8 +8516,8 @@ Item-wise Sales Register,Registre des Ventes par Article, Items To Be Requested,Articles À Demander, Reserved,Réservé, Itemwise Recommended Reorder Level,Renouvellement Recommandé par Article, -Lead Details,Détails du Prospect, -Lead Owner Efficiency,Efficacité des Responsables des Prospects, +Lead Details,Détails du Lead, +Lead Owner Efficiency,Efficacité des Responsables des Leads, Loan Repayment and Closure,Remboursement et clôture de prêts, Loan Security Status,État de la sécurité du prêt, Lost Opportunity,Occasion perdue, @@ -9207,7 +9206,7 @@ Time Required (In Mins),Temps requis (en minutes), From Posting Date,À partir de la date de publication, To Posting Date,À la date de publication, No records found,Aucun enregistrement trouvé, -Customer/Lead Name,Nom du client / prospect, +Customer/Lead Name,Nom du client / lead, Unmarked Days,Jours non marqués, Jan,Jan, Feb,fév, @@ -9471,7 +9470,7 @@ Row {0}: Loan Security {1} added multiple times,Ligne {0}: Garantie de prêt {1} Row #{0}: Child Item should not be a Product Bundle. Please remove Item {1} and Save,Ligne n ° {0}: l'élément enfant ne doit pas être un ensemble de produits. Veuillez supprimer l'élément {1} et enregistrer, Credit limit reached for customer {0},Limite de crédit atteinte pour le client {0}, Could not auto create Customer due to the following missing mandatory field(s):,Impossible de créer automatiquement le client en raison du ou des champs obligatoires manquants suivants:, -Please create Customer from Lead {0}.,Veuillez créer un client à partir du prospect {0}., +Please create Customer from Lead {0}.,Veuillez créer un client à partir du lead {0}., Mandatory Missing,Obligatoire manquant, Please set Payroll based on in Payroll settings,Veuillez définir la paie en fonction des paramètres de paie, Additional Salary: {0} already exist for Salary Component: {1} for period {2} and {3},Salaire supplémentaire: {0} existe déjà pour le composant de salaire: {1} pour la période {2} et {3}, @@ -9834,29 +9833,29 @@ Enable European Access,Activer l'accès européen, Creating Purchase Order ...,Création d'une commande d'achat ..., "Select a Supplier from the Default Suppliers of the items below. On selection, a Purchase Order will be made against items belonging to the selected Supplier only.","Sélectionnez un fournisseur parmi les fournisseurs par défaut des articles ci-dessous. Lors de la sélection, une commande d'achat sera effectué contre des articles appartenant uniquement au fournisseur sélectionné.", Row #{}: You must select {} serial numbers for item {}.,Ligne n ° {}: vous devez sélectionner {} numéros de série pour l'article {}., -Update Rate as per Last Purchase,Mettre à jour avec les derniers prix d'achats -Company Shipping Address,Adresse d'expédition -Shipping Address Details,Détail d'adresse d'expédition -Company Billing Address,Adresse de la société de facturation +Update Rate as per Last Purchase,Mettre à jour avec les derniers prix d'achats, +Company Shipping Address,Adresse d'expédition, +Shipping Address Details,Détail d'adresse d'expédition, +Company Billing Address,Adresse de la société de facturation, Supplier Address Details, -Bank Reconciliation Tool,Outil de réconcialiation d'écritures bancaires -Supplier Contact,Contact fournisseur -Subcontracting,Sous traitance -Order Status,Statut de la commande -Build,Personnalisations avancées -Dispatch Address Name,Adresse de livraison intermédiaire -Amount Eligible for Commission,Montant éligible à comission -Grant Commission,Eligible aux commissions -Stock Transactions Settings, Paramétre des transactions -Role Allowed to Over Deliver/Receive, Rôle autorisé à dépasser cette limite -Users with this role are allowed to over deliver/receive against orders above the allowance percentage,Rôle Utilisateur qui sont autorisé à livrée/commandé au-delà de la limite -Over Transfer Allowance,Autorisation de limite de transfert -Quality Inspection Settings,Paramétre de l'inspection qualité -Action If Quality Inspection Is Rejected,Action si l'inspection qualité est rejetée -Disable Serial No And Batch Selector,Désactiver le sélecteur de numéro de lot/série -Is Rate Adjustment Entry (Debit Note),Est un justement du prix de la note de débit -Issue a debit note with 0 qty against an existing Sales Invoice,Creer une note de débit avec une quatité à O pour la facture -Control Historical Stock Transactions,Controle de l'historique des stransaction de stock +Bank Reconciliation Tool,Outil de réconcialiation d'écritures bancaires, +Supplier Contact,Contact fournisseur, +Subcontracting,Sous traitance, +Order Status,Statut de la commande, +Build,Personnalisations avancées, +Dispatch Address Name,Adresse de livraison intermédiaire, +Amount Eligible for Commission,Montant éligible à comission, +Grant Commission,Eligible aux commissions, +Stock Transactions Settings, Paramétre des transactions, +Role Allowed to Over Deliver/Receive, Rôle autorisé à dépasser cette limite, +Users with this role are allowed to over deliver/receive against orders above the allowance percentage,Rôle Utilisateur qui sont autorisé à livrée/commandé au-delà de la limite, +Over Transfer Allowance,Autorisation de limite de transfert, +Quality Inspection Settings,Paramétre de l'inspection qualité, +Action If Quality Inspection Is Rejected,Action si l'inspection qualité est rejetée, +Disable Serial No And Batch Selector,Désactiver le sélecteur de numéro de lot/série, +Is Rate Adjustment Entry (Debit Note),Est un justement du prix de la note de débit, +Issue a debit note with 0 qty against an existing Sales Invoice,Creer une note de débit avec une quatité à O pour la facture, +Control Historical Stock Transactions,Controle de l'historique des stransaction de stock, No stock transactions can be created or modified before this date.,Aucune transaction ne peux être créée ou modifié avant cette date. Stock transactions that are older than the mentioned days cannot be modified.,Les transactions de stock plus ancienne que le nombre de jours ci-dessus ne peuvent être modifiées, Role Allowed to Create/Edit Back-dated Transactions,Rôle autorisé à créer et modifier des transactions anti-datée, @@ -9867,29 +9866,29 @@ Show Barcode Field in Stock Transactions,Afficher le champ Code Barre dans les t Convert Item Description to Clean HTML in Transactions,Convertir les descriptions d'articles en HTML valide lors des transactions, Have Default Naming Series for Batch ID?,Masque de numérotation par défaut pour les Lots ou Séries, "The percentage you are allowed to transfer more against the quantity ordered. For example, if you have ordered 100 units, and your Allowance is 10%, then you are allowed transfer 110 units","Le pourcentage de quantité que vous pourrez réceptionner en plus de la quantité commandée. Par exemple, vous avez commandé 100 unités, votre pourcentage de dépassement est de 10%, vous pourrez réceptionner 110 unités" -Allowed Items,Articles autorisés -Party Specific Item,Restriction d'article disponible -Restrict Items Based On,Type de critére de restriction -Based On Value,critére de restriction +Allowed Items,Articles autorisés, +Party Specific Item,Restriction d'article disponible, +Restrict Items Based On,Type de critére de restriction, +Based On Value,critére de restriction, Unit of Measure (UOM),Unité de mesure (UdM), Unit Of Measure (UOM),Unité de mesure (UdM), -CRM Settings,Paramètres CRM -Do Not Explode,Ne pas décomposer -Quick Access, Accés rapides -{} Available,{} Disponible.s -{} Pending,{} En attente.s -{} To Bill,{} à facturer -{} To Receive,{} A recevoir +CRM Settings,Paramètres CRM, +Do Not Explode,Ne pas décomposer, +Quick Access, Accés rapides, +{} Available,{} Disponible.s, +{} Pending,{} En attente.s, +{} To Bill,{} à facturer, +{} To Receive,{} A recevoir, {} Active,{} Actif.ve(s) {} Open,{} Ouvert.e(s) -Incorrect Data Report,Rapport de données incohérentes -Incorrect Serial No Valuation,Valorisation inccorecte par Num. Série / Lots -Incorrect Balance Qty After Transaction,Equilibre des quantités aprés une transaction +Incorrect Data Report,Rapport de données incohérentes, +Incorrect Serial No Valuation,Valorisation inccorecte par Num. Série / Lots, +Incorrect Balance Qty After Transaction,Equilibre des quantités aprés une transaction, Interview Type,Type d'entretien Interview Round,Cycle d'entretien Interview,Entretien Interview Feedback,Retour d'entretien -Journal Energy Point,Historique des points d'énergies +Journal Energy Point,Historique des points d'énergies, Billing Address Details,Adresse de facturation (détails) Supplier Address Details,Adresse Fournisseur (détails) Retail,Commerce, From c5df164e1cf3a3f86b3c69cf9c5db3211b2af935 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Tue, 19 Sep 2023 20:41:53 +0530 Subject: [PATCH 336/501] chore: translations dutch (#37042) chore: translations dutch (#37042) update: translations dutch (cherry picked from commit c35dea7177649adcf068ea8f4d594012bbe35a0f) Co-authored-by: RJPvT <48353029+RJPvT@users.noreply.github.com> --- erpnext/translations/nl.csv | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/translations/nl.csv b/erpnext/translations/nl.csv index 1efc5000b779..92671115bfc6 100644 --- a/erpnext/translations/nl.csv +++ b/erpnext/translations/nl.csv @@ -2364,7 +2364,7 @@ Report Type is mandatory,Rapport type is verplicht, Reports,rapporten, Reqd By Date,Benodigd op datum, Reqd Qty,Gewenste hoeveelheid, -Request for Quotation,Offerte, +Request for Quotation,Offerte-verzoek, Request for Quotations,Verzoek om offertes, Request for Raw Materials,Verzoek om grondstoffen, Request for purchase.,Inkoopaanvraag, From 271392562821f0aeac2e81d44e3d623500cf4133 Mon Sep 17 00:00:00 2001 From: Frappe PR Bot Date: Wed, 20 Sep 2023 06:24:47 +0000 Subject: [PATCH 337/501] chore(release): Bumped to Version 14.40.0 # [14.40.0](https://github.com/frappe/erpnext/compare/v14.39.0...v14.40.0) (2023-09-20) ### Bug Fixes * + btn not appearing for delivery note connection (backport [#36980](https://github.com/frappe/erpnext/issues/36980)) ([#37070](https://github.com/frappe/erpnext/issues/37070)) ([c2a0c1e](https://github.com/frappe/erpnext/commit/c2a0c1e989aab8ec136a6e8c15687abcab3cb55b)) * accepted warehouse and rejected warehouse can't be same (backport [#36973](https://github.com/frappe/erpnext/issues/36973)) ([#37071](https://github.com/frappe/erpnext/issues/37071)) ([b56c9b9](https://github.com/frappe/erpnext/commit/b56c9b91f11dd9e84d02b82ea2b0b2f727c5d4e1)) * after applying coupon code, field in_words not updated ([#37133](https://github.com/frappe/erpnext/issues/37133)) ([1cb8c64](https://github.com/frappe/erpnext/commit/1cb8c64c94858134c4765f2c21ebbce7394ec795)) * Apply dimension filter, irrespective of dimesion columns ([9bc44a3](https://github.com/frappe/erpnext/commit/9bc44a3b40c661ceb4821d90f70fc160d406bf8e)) * asset validation misfire on debit notes ([b33db6c](https://github.com/frappe/erpnext/commit/b33db6c79a6b7f37c93f7e49d6721ee8d7593527)) * company wise deferred accounting fields in item ([#37023](https://github.com/frappe/erpnext/issues/37023)) ([13aaff3](https://github.com/frappe/erpnext/commit/13aaff30a5d6ae27d131eecee6f9f54575f2688c)) * Don't allow merging accounts with different currency ([#37074](https://github.com/frappe/erpnext/issues/37074)) ([c41cb39](https://github.com/frappe/erpnext/commit/c41cb3930c76efb620cf8bc27672dab8c5302e6c)) * don't set from warehouse for purchase material request ([#37132](https://github.com/frappe/erpnext/issues/37132)) ([e62b783](https://github.com/frappe/erpnext/commit/e62b783f340844783352030de422c6be14b3ddfc)) * Duplicate Serial Nos validation in POS ([#36927](https://github.com/frappe/erpnext/issues/36927)) ([366325c](https://github.com/frappe/erpnext/commit/366325ca3ca0a99bb2384755c6f1cc7021649380)) * fetch logic for repay_from_salary in loan_repayment [v14] ([#37135](https://github.com/frappe/erpnext/issues/37135)) ([480a0ca](https://github.com/frappe/erpnext/commit/480a0ca7a8d97bbaa4de103a722e823ff5aed47f)) * ignore user permissions for `Source Warehouse` in MR (backport [#37102](https://github.com/frappe/erpnext/issues/37102)) ([#37110](https://github.com/frappe/erpnext/issues/37110)) ([727dcc5](https://github.com/frappe/erpnext/commit/727dcc5034749f2505052aa05c9ad8d8b94fa324)) * incorrect stock ledger entries in DN (backport [#36944](https://github.com/frappe/erpnext/issues/36944)) ([#37066](https://github.com/frappe/erpnext/issues/37066)) ([699ad80](https://github.com/frappe/erpnext/commit/699ad8080236e9b54c93e71587a027080ce422eb)) * packed item using expired price ([413b40f](https://github.com/frappe/erpnext/commit/413b40f5a770e062732f850714d3ea75de2b4337)) * POS opening Issue if Product Bundle is available ([#37138](https://github.com/frappe/erpnext/issues/37138)) ([af05864](https://github.com/frappe/erpnext/commit/af05864e6db336b2897b1138ec25e59caba0a568)) * precision issue and column name ([#37073](https://github.com/frappe/erpnext/issues/37073)) ([f2395a9](https://github.com/frappe/erpnext/commit/f2395a92971f48904e88afb929285def7ba221e4)) * Purchase Receipt Provisional Accounting GL Entries (backport [#37046](https://github.com/frappe/erpnext/issues/37046)) ([#37068](https://github.com/frappe/erpnext/issues/37068)) ([8772e40](https://github.com/frappe/erpnext/commit/8772e40bae96db49b1d8653a5e8e5cb975f492ad)) * Remove redundant code ([#37001](https://github.com/frappe/erpnext/issues/37001)) ([3ecdf02](https://github.com/frappe/erpnext/commit/3ecdf028f28229d230faef07babd126ef13ac4fc)) * **ux:** move `get_route_options_for_new_doc` to `refresh` ([#37092](https://github.com/frappe/erpnext/issues/37092)) ([a563fed](https://github.com/frappe/erpnext/commit/a563fed6dcaf99fa799cbf034dc6d38473e0fd57)) * validate duplicate serial no in DN ([fffa13f](https://github.com/frappe/erpnext/commit/fffa13f22b39cd768b081a52d454a03b386596ba)) ### Features * provision to create RIV from `Stock Ledger Invariant Check` report (backport [#37115](https://github.com/frappe/erpnext/issues/37115)) ([#37147](https://github.com/frappe/erpnext/issues/37147)) ([29ff0ce](https://github.com/frappe/erpnext/commit/29ff0ce286255307ac78652776a8e158b89222ae)) --- erpnext/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/__init__.py b/erpnext/__init__.py index 12a6ba9d77a5..529c9ca9d802 100644 --- a/erpnext/__init__.py +++ b/erpnext/__init__.py @@ -3,7 +3,7 @@ import frappe -__version__ = "14.39.0" +__version__ = "14.40.0" def get_default_company(user=None): From 56657b6122c056a8ba091b41fd1b6e94447f34da Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Wed, 20 Sep 2023 18:06:30 +0530 Subject: [PATCH 338/501] fix: allow to select parent warehouse in the website item (backport #37047) (#37173) fix: allow to select parent warehouse in the website item (#37047) (cherry picked from commit e6199dc80227023d70c7b7dabb7b0239345f9be5) Co-authored-by: rohitwaghchaure --- .../doctype/website_item/test_website_item.py | 6 +-- .../doctype/website_item/website_item.js | 6 --- .../doctype/website_item/website_item.json | 4 +- .../e_commerce/product_data_engine/query.py | 10 ++--- erpnext/e_commerce/shopping_cart/cart.py | 14 ++++++- erpnext/e_commerce/variant_selector/utils.py | 19 +++++---- erpnext/stock/get_item_details.py | 3 ++ .../generators/item/item_add_to_cart.html | 2 +- erpnext/templates/pages/wishlist.py | 16 ++++++-- erpnext/utilities/product.py | 39 ++++++++++++------- 10 files changed, 75 insertions(+), 44 deletions(-) diff --git a/erpnext/e_commerce/doctype/website_item/test_website_item.py b/erpnext/e_commerce/doctype/website_item/test_website_item.py index 019a5f9ee4f4..8eebfdb83afa 100644 --- a/erpnext/e_commerce/doctype/website_item/test_website_item.py +++ b/erpnext/e_commerce/doctype/website_item/test_website_item.py @@ -312,7 +312,7 @@ def test_website_item_stock_when_out_of_stock(self): # check if stock details are fetched and item not in stock with warehouse set data = get_product_info_for_website(item_code, skip_quotation_creation=True) self.assertFalse(bool(data.product_info["in_stock"])) - self.assertEqual(data.product_info["stock_qty"][0][0], 0) + self.assertEqual(data.product_info["stock_qty"], 0) # disable show stock availability setup_e_commerce_settings({"show_stock_availability": 0}) @@ -355,7 +355,7 @@ def test_website_item_stock_when_in_stock(self): # check if stock details are fetched and item is in stock with warehouse set data = get_product_info_for_website(item_code, skip_quotation_creation=True) self.assertTrue(bool(data.product_info["in_stock"])) - self.assertEqual(data.product_info["stock_qty"][0][0], 2) + self.assertEqual(data.product_info["stock_qty"], 2) # unset warehouse frappe.db.set_value("Website Item", {"item_code": item_code}, "website_warehouse", "") @@ -364,7 +364,7 @@ def test_website_item_stock_when_in_stock(self): # (even though it has stock in some warehouse) data = get_product_info_for_website(item_code, skip_quotation_creation=True) self.assertFalse(bool(data.product_info["in_stock"])) - self.assertFalse(bool(data.product_info["stock_qty"])) + self.assertFalse(data.product_info["stock_qty"]) # disable show stock availability setup_e_commerce_settings({"show_stock_availability": 0}) diff --git a/erpnext/e_commerce/doctype/website_item/website_item.js b/erpnext/e_commerce/doctype/website_item/website_item.js index 7b7193e833aa..b6595cce8a9c 100644 --- a/erpnext/e_commerce/doctype/website_item/website_item.js +++ b/erpnext/e_commerce/doctype/website_item/website_item.js @@ -5,12 +5,6 @@ frappe.ui.form.on('Website Item', { onload: (frm) => { // should never check Private frm.fields_dict["website_image"].df.is_private = 0; - - frm.set_query("website_warehouse", () => { - return { - filters: {"is_group": 0} - }; - }); }, refresh: (frm) => { diff --git a/erpnext/e_commerce/doctype/website_item/website_item.json b/erpnext/e_commerce/doctype/website_item/website_item.json index 6556eabf4abf..6f551a0b42d6 100644 --- a/erpnext/e_commerce/doctype/website_item/website_item.json +++ b/erpnext/e_commerce/doctype/website_item/website_item.json @@ -135,7 +135,7 @@ "fieldtype": "Column Break" }, { - "description": "Show Stock availability based on this warehouse.", + "description": "Show Stock availability based on this warehouse. If the parent warehouse is selected, then the system will display the consolidated available quantity of all child warehouses.", "fieldname": "website_warehouse", "fieldtype": "Link", "ignore_user_permissions": 1, @@ -348,7 +348,7 @@ "index_web_pages_for_search": 1, "links": [], "make_attachments_public": 1, - "modified": "2022-09-30 04:01:52.090732", + "modified": "2023-09-12 14:19:22.822689", "modified_by": "Administrator", "module": "E-commerce", "name": "Website Item", diff --git a/erpnext/e_commerce/product_data_engine/query.py b/erpnext/e_commerce/product_data_engine/query.py index e6a595a03447..975f87608a63 100644 --- a/erpnext/e_commerce/product_data_engine/query.py +++ b/erpnext/e_commerce/product_data_engine/query.py @@ -259,6 +259,10 @@ def get_price_discount_info(self, item, price_object, discount_list): ) def get_stock_availability(self, item): + from erpnext.templates.pages.wishlist import ( + get_stock_availability as get_stock_availability_from_template, + ) + """Modify item object and add stock details.""" item.in_stock = False warehouse = item.get("website_warehouse") @@ -274,11 +278,7 @@ def get_stock_availability(self, item): else: item.in_stock = True elif warehouse: - # stock item and has warehouse - actual_qty = frappe.db.get_value( - "Bin", {"item_code": item.item_code, "warehouse": item.get("website_warehouse")}, "actual_qty" - ) - item.in_stock = bool(flt(actual_qty)) + item.in_stock = get_stock_availability_from_template(item.item_code, warehouse) def get_cart_items(self): customer = get_customer(silent=True) diff --git a/erpnext/e_commerce/shopping_cart/cart.py b/erpnext/e_commerce/shopping_cart/cart.py index 57746a234bcb..030b439ae4b8 100644 --- a/erpnext/e_commerce/shopping_cart/cart.py +++ b/erpnext/e_commerce/shopping_cart/cart.py @@ -111,8 +111,8 @@ def place_order(): item_stock = get_web_item_qty_in_stock(item.item_code, "website_warehouse") if not cint(item_stock.in_stock): throw(_("{0} Not in Stock").format(item.item_code)) - if item.qty > item_stock.stock_qty[0][0]: - throw(_("Only {0} in Stock for item {1}").format(item_stock.stock_qty[0][0], item.item_code)) + if item.qty > item_stock.stock_qty: + throw(_("Only {0} in Stock for item {1}").format(item_stock.stock_qty, item.item_code)) sales_order.flags.ignore_permissions = True sales_order.insert() @@ -150,6 +150,10 @@ def update_cart(item_code, qty, additional_notes=None, with_items=False): empty_card = True else: + warehouse = frappe.get_cached_value( + "Website Item", {"item_code": item_code}, "website_warehouse" + ) + quotation_items = quotation.get("items", {"item_code": item_code}) if not quotation_items: quotation.append( @@ -159,11 +163,13 @@ def update_cart(item_code, qty, additional_notes=None, with_items=False): "item_code": item_code, "qty": qty, "additional_notes": additional_notes, + "warehouse": warehouse, }, ) else: quotation_items[0].qty = qty quotation_items[0].additional_notes = additional_notes + quotation_items[0].warehouse = warehouse apply_cart_settings(quotation=quotation) @@ -322,6 +328,10 @@ def decorate_quotation_doc(doc): fields = fields[2:] d.update(frappe.db.get_value("Website Item", {"item_code": item_code}, fields, as_dict=True)) + website_warehouse = frappe.get_cached_value( + "Website Item", {"item_code": item_code}, "website_warehouse" + ) + d.warehouse = website_warehouse return doc diff --git a/erpnext/e_commerce/variant_selector/utils.py b/erpnext/e_commerce/variant_selector/utils.py index 4466c4574368..88356f5e9096 100644 --- a/erpnext/e_commerce/variant_selector/utils.py +++ b/erpnext/e_commerce/variant_selector/utils.py @@ -104,6 +104,8 @@ def get_attributes_and_values(item_code): @frappe.whitelist(allow_guest=True) def get_next_attribute_and_values(item_code, selected_attributes): + from erpnext.stock.doctype.warehouse.warehouse import get_child_warehouses + """Find the count of Items that match the selected attributes. Also, find the attribute values that are not applicable for further searching. If less than equal to 10 items are found, return item_codes of those items. @@ -168,7 +170,7 @@ def get_next_attribute_and_values(item_code, selected_attributes): product_info = None product_id = "" - website_warehouse = "" + warehouse = "" if exact_match or filtered_items: if exact_match and len(exact_match) == 1: product_id = exact_match[0] @@ -176,16 +178,19 @@ def get_next_attribute_and_values(item_code, selected_attributes): product_id = list(filtered_items)[0] if product_id: - website_warehouse = frappe.get_cached_value( + warehouse = frappe.get_cached_value( "Website Item", {"item_code": product_id}, "website_warehouse" ) available_qty = 0.0 - if website_warehouse: - available_qty = flt( - frappe.db.get_value( - "Bin", {"item_code": product_id, "warehouse": website_warehouse}, "actual_qty" - ) + if warehouse and frappe.get_cached_value("Warehouse", warehouse, "is_group") == 1: + warehouses = get_child_warehouses(warehouse) + else: + warehouses = [warehouse] if warehouse else [] + + for warehouse in warehouses: + available_qty += flt( + frappe.db.get_value("Bin", {"item_code": product_id, "warehouse": warehouse}, "actual_qty") ) return { diff --git a/erpnext/stock/get_item_details.py b/erpnext/stock/get_item_details.py index f7eb859f8304..13f484b1c851 100644 --- a/erpnext/stock/get_item_details.py +++ b/erpnext/stock/get_item_details.py @@ -1392,6 +1392,9 @@ def _get_bom(item): @frappe.whitelist() def get_valuation_rate(item_code, company, warehouse=None): + if frappe.get_cached_value("Warehouse", warehouse, "is_group"): + return {"valuation_rate": 0.0} + item = get_item_defaults(item_code, company) item_group = get_item_group_defaults(item_code, company) brand = get_brand_defaults(item_code, company) diff --git a/erpnext/templates/generators/item/item_add_to_cart.html b/erpnext/templates/generators/item/item_add_to_cart.html index 1381dfe3b743..9bd3f7514c95 100644 --- a/erpnext/templates/generators/item/item_add_to_cart.html +++ b/erpnext/templates/generators/item/item_add_to_cart.html @@ -49,7 +49,7 @@ {{ _('In stock') }} {% if product_info.show_stock_qty and product_info.stock_qty %} - ({{ product_info.stock_qty[0][0] }}) + ({{ product_info.stock_qty }}) {% endif %} {% endif %} diff --git a/erpnext/templates/pages/wishlist.py b/erpnext/templates/pages/wishlist.py index d70f27c9d9d1..17607e45f919 100644 --- a/erpnext/templates/pages/wishlist.py +++ b/erpnext/templates/pages/wishlist.py @@ -25,9 +25,19 @@ def get_context(context): def get_stock_availability(item_code, warehouse): - stock_qty = frappe.utils.flt( - frappe.db.get_value("Bin", {"item_code": item_code, "warehouse": warehouse}, "actual_qty") - ) + from erpnext.stock.doctype.warehouse.warehouse import get_child_warehouses + + if warehouse and frappe.get_cached_value("Warehouse", warehouse, "is_group") == 1: + warehouses = get_child_warehouses(warehouse) + else: + warehouses = [warehouse] if warehouse else [] + + stock_qty = 0.0 + for warehouse in warehouses: + stock_qty += frappe.utils.flt( + frappe.db.get_value("Bin", {"item_code": item_code, "warehouse": warehouse}, "actual_qty") + ) + return bool(stock_qty) diff --git a/erpnext/utilities/product.py b/erpnext/utilities/product.py index afe9654e8eaa..e967f7061bb1 100644 --- a/erpnext/utilities/product.py +++ b/erpnext/utilities/product.py @@ -6,6 +6,7 @@ from erpnext.accounts.doctype.pricing_rule.pricing_rule import get_pricing_rule_for_item from erpnext.stock.doctype.batch.batch import get_batch_qty +from erpnext.stock.doctype.warehouse.warehouse import get_child_warehouses def get_web_item_qty_in_stock(item_code, item_warehouse_field, warehouse=None): @@ -22,23 +23,31 @@ def get_web_item_qty_in_stock(item_code, item_warehouse_field, warehouse=None): "Website Item", {"item_code": template_item_code}, item_warehouse_field ) - if warehouse: - stock_qty = frappe.db.sql( - """ - select GREATEST(S.actual_qty - S.reserved_qty - S.reserved_qty_for_production - S.reserved_qty_for_sub_contract, 0) / IFNULL(C.conversion_factor, 1) - from tabBin S - inner join `tabItem` I on S.item_code = I.Item_code - left join `tabUOM Conversion Detail` C on I.sales_uom = C.uom and C.parent = I.Item_code - where S.item_code=%s and S.warehouse=%s""", - (item_code, warehouse), - ) + if warehouse and frappe.get_cached_value("Warehouse", warehouse, "is_group") == 1: + warehouses = get_child_warehouses(warehouse) + else: + warehouses = [warehouse] if warehouse else [] + + total_stock = 0.0 + if warehouses: + for warehouse in warehouses: + stock_qty = frappe.db.sql( + """ + select GREATEST(S.actual_qty - S.reserved_qty - S.reserved_qty_for_production - S.reserved_qty_for_sub_contract, 0) / IFNULL(C.conversion_factor, 1) + from tabBin S + inner join `tabItem` I on S.item_code = I.Item_code + left join `tabUOM Conversion Detail` C on I.sales_uom = C.uom and C.parent = I.Item_code + where S.item_code=%s and S.warehouse=%s""", + (item_code, warehouse), + ) + + if stock_qty: + total_stock += adjust_qty_for_expired_items(item_code, stock_qty, warehouse) - if stock_qty: - stock_qty = adjust_qty_for_expired_items(item_code, stock_qty, warehouse) - in_stock = stock_qty[0][0] > 0 and 1 or 0 + in_stock = total_stock > 0 and 1 or 0 return frappe._dict( - {"in_stock": in_stock, "stock_qty": stock_qty, "is_stock_item": is_stock_item} + {"in_stock": in_stock, "stock_qty": total_stock, "is_stock_item": is_stock_item} ) @@ -56,7 +65,7 @@ def adjust_qty_for_expired_items(item_code, stock_qty, warehouse): if not stock_qty[0][0]: break - return stock_qty + return stock_qty[0][0] if stock_qty else 0 def get_expired_batches(batches): From 03e52d385969504564b5079c2551b34dd373292f Mon Sep 17 00:00:00 2001 From: Raffael Meyer <14891507+barredterra@users.noreply.github.com> Date: Wed, 20 Sep 2023 14:38:42 +0200 Subject: [PATCH 339/501] chore: resolve conflicts --- erpnext/translations/de.csv | 4 ---- 1 file changed, 4 deletions(-) diff --git a/erpnext/translations/de.csv b/erpnext/translations/de.csv index 6dae398e334d..dcba85b4d206 100644 --- a/erpnext/translations/de.csv +++ b/erpnext/translations/de.csv @@ -1969,12 +1969,8 @@ Please check Multi Currency option to allow accounts with other currency,"Bitte Please click on 'Generate Schedule',"Bitte auf ""Zeitplan generieren"" klicken", Please click on 'Generate Schedule' to fetch Serial No added for Item {0},"Bitte auf ""Zeitplan generieren"" klicken, um die Seriennummer für Artikel {0} abzurufen", Please click on 'Generate Schedule' to get schedule,"Bitte auf ""Zeitplan generieren"" klicken, um den Zeitplan zu erhalten", -<<<<<<< HEAD Please confirm once you have completed your training,"Bitte bestätigen Sie, sobald Sie Ihre Ausbildung abgeschlossen haben", -Please create purchase receipt or purchase invoice for the item {0},Bitte erstellen Sie eine Kaufquittung oder eine Kaufrechnung für den Artikel {0}, -======= Please create purchase receipt or purchase invoice for the item {0},Bitte erstellen Sie eine Kaufquittung oder eine Eingangsrechnungen für den Artikel {0}, ->>>>>>> 84a9000db2 (fix: german translation of Sales and Purchase Invoice (#37122)) Please define grade for Threshold 0%,Bitte definieren Sie Grade for Threshold 0%, Please enable Applicable on Booking Actual Expenses,Bitte aktivieren Sie Anwendbar bei der Buchung von tatsächlichen Ausgaben, Please enable Applicable on Purchase Order and Applicable on Booking Actual Expenses,Bitte aktivieren Sie Anwendbar bei Bestellung und Anwendbar bei Buchung von tatsächlichen Ausgaben, From f2bcfb5f977927cad9f4288b667bbce4d231c6c8 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Wed, 20 Sep 2023 19:21:54 +0530 Subject: [PATCH 340/501] fix: labels for `Stock Ledger Invariant Check` report (backport #37150) (#37176) fix: labels for `Stock Ledger Invariant Check` report (#37150) refactor: `Stock Ledger Invariant Check` report (cherry picked from commit f0859ecc60a8a08f547bf124f222ef5cd3282502) Co-authored-by: s-aga-r --- .../stock_ledger_invariant_check.js | 4 ++-- .../stock_ledger_invariant_check.py | 22 +++++++++---------- 2 files changed, 13 insertions(+), 13 deletions(-) diff --git a/erpnext/stock/report/stock_ledger_invariant_check/stock_ledger_invariant_check.js b/erpnext/stock/report/stock_ledger_invariant_check/stock_ledger_invariant_check.js index 74849058c3c1..94e0b2dce3b4 100644 --- a/erpnext/stock/report/stock_ledger_invariant_check/stock_ledger_invariant_check.js +++ b/erpnext/stock/report/stock_ledger_invariant_check/stock_ledger_invariant_check.js @@ -2,7 +2,7 @@ // For license information, please see license.txt /* eslint-disable */ -const DIFFERNCE_FIELD_NAMES = [ +const DIFFERENCE_FIELD_NAMES = [ 'difference_in_qty', 'fifo_qty_diff', 'fifo_value_diff', @@ -37,7 +37,7 @@ frappe.query_reports['Stock Ledger Invariant Check'] = { formatter (value, row, column, data, default_formatter) { value = default_formatter(value, row, column, data); - if (DIFFERNCE_FIELD_NAMES.includes(column.fieldname) && Math.abs(data[column.fieldname]) > 0.001) { + if (DIFFERENCE_FIELD_NAMES.includes(column.fieldname) && Math.abs(data[column.fieldname]) > 0.001) { value = '' + value + ''; } return value; diff --git a/erpnext/stock/report/stock_ledger_invariant_check/stock_ledger_invariant_check.py b/erpnext/stock/report/stock_ledger_invariant_check/stock_ledger_invariant_check.py index e11c9bb7891e..ca15afe444d5 100644 --- a/erpnext/stock/report/stock_ledger_invariant_check/stock_ledger_invariant_check.py +++ b/erpnext/stock/report/stock_ledger_invariant_check/stock_ledger_invariant_check.py @@ -186,7 +186,7 @@ def get_columns(): { "fieldname": "fifo_queue_qty", "fieldtype": "Float", - "label": _("(C) Total qty in queue"), + "label": _("(C) Total Qty in Queue"), }, { "fieldname": "fifo_qty_diff", @@ -211,52 +211,52 @@ def get_columns(): { "fieldname": "stock_value_difference", "fieldtype": "Float", - "label": _("(F) Stock Value Difference"), + "label": _("(F) Change in Stock Value"), }, { "fieldname": "stock_value_from_diff", "fieldtype": "Float", - "label": _("Balance Stock Value using (F)"), + "label": _("(G) Sum of Change in Stock Value"), }, { "fieldname": "diff_value_diff", "fieldtype": "Float", - "label": _("K - D"), + "label": _("G - D"), }, { "fieldname": "fifo_stock_diff", "fieldtype": "Float", - "label": _("(G) Stock Value difference (FIFO queue)"), + "label": _("(H) Change in Stock Value (FIFO Queue)"), }, { "fieldname": "fifo_difference_diff", "fieldtype": "Float", - "label": _("F - G"), + "label": _("H - F"), }, { "fieldname": "valuation_rate", "fieldtype": "Float", - "label": _("(H) Valuation Rate"), + "label": _("(I) Valuation Rate"), }, { "fieldname": "fifo_valuation_rate", "fieldtype": "Float", - "label": _("(I) Valuation Rate as per FIFO"), + "label": _("(J) Valuation Rate as per FIFO"), }, { "fieldname": "fifo_valuation_diff", "fieldtype": "Float", - "label": _("H - I"), + "label": _("I - J"), }, { "fieldname": "balance_value_by_qty", "fieldtype": "Float", - "label": _("(J) Valuation = Value (D) ÷ Qty (A)"), + "label": _("(K) Valuation = Value (D) ÷ Qty (A)"), }, { "fieldname": "valuation_diff", "fieldtype": "Float", - "label": _("H - J"), + "label": _("I - K"), }, ] From 02fc67c83c94b6277e4e6dafd22381ed04da0ad6 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Thu, 21 Sep 2023 07:49:02 +0000 Subject: [PATCH 341/501] feat: `Stock Ledger Variance` report (backport #37165) (#37183) feat: `Stock Ledger Variance` report (#37165) * feat: `Stock Ledger Variance` report * refactor: `get_data()` (cherry picked from commit acda72d6165aa395fedebf9522aa0adf45c25fa2) Co-authored-by: s-aga-r --- .../report/stock_ledger_variance/__init__.py | 0 .../stock_ledger_variance.js | 101 +++++++ .../stock_ledger_variance.json | 22 ++ .../stock_ledger_variance.py | 279 ++++++++++++++++++ 4 files changed, 402 insertions(+) create mode 100644 erpnext/stock/report/stock_ledger_variance/__init__.py create mode 100644 erpnext/stock/report/stock_ledger_variance/stock_ledger_variance.js create mode 100644 erpnext/stock/report/stock_ledger_variance/stock_ledger_variance.json create mode 100644 erpnext/stock/report/stock_ledger_variance/stock_ledger_variance.py diff --git a/erpnext/stock/report/stock_ledger_variance/__init__.py b/erpnext/stock/report/stock_ledger_variance/__init__.py new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/erpnext/stock/report/stock_ledger_variance/stock_ledger_variance.js b/erpnext/stock/report/stock_ledger_variance/stock_ledger_variance.js new file mode 100644 index 000000000000..b1e4a74571ea --- /dev/null +++ b/erpnext/stock/report/stock_ledger_variance/stock_ledger_variance.js @@ -0,0 +1,101 @@ +// Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and contributors +// For license information, please see license.txt + +const DIFFERENCE_FIELD_NAMES = [ + "difference_in_qty", + "fifo_qty_diff", + "fifo_value_diff", + "fifo_valuation_diff", + "valuation_diff", + "fifo_difference_diff", + "diff_value_diff" +]; + +frappe.query_reports["Stock Ledger Variance"] = { + "filters": [ + { + "fieldname": "item_code", + "fieldtype": "Link", + "label": "Item", + "options": "Item", + get_query: function() { + return { + filters: {is_stock_item: 1, has_serial_no: 0} + } + } + }, + { + "fieldname": "warehouse", + "fieldtype": "Link", + "label": "Warehouse", + "options": "Warehouse", + get_query: function() { + return { + filters: {is_group: 0, disabled: 0} + } + } + }, + { + "fieldname": "difference_in", + "fieldtype": "Select", + "label": "Difference In", + "options": [ + "", + "Qty", + "Value", + "Valuation", + ], + }, + { + "fieldname": "include_disabled", + "fieldtype": "Check", + "label": "Include Disabled", + } + ], + + formatter (value, row, column, data, default_formatter) { + value = default_formatter(value, row, column, data); + + if (DIFFERENCE_FIELD_NAMES.includes(column.fieldname) && Math.abs(data[column.fieldname]) > 0.001) { + value = "" + value + ""; + } + + return value; + }, + + get_datatable_options(options) { + return Object.assign(options, { + checkboxColumn: true, + }); + }, + + onload(report) { + report.page.add_inner_button(__('Create Reposting Entries'), () => { + let message = ` +
+

+ Reposting Entries will change the value of + accounts Stock In Hand, and Stock Expenses + in the Trial Balance report and will also change + the Balance Value in the Stock Balance report. +

+

Are you sure you want to create Reposting Entries?

+
`; + let indexes = frappe.query_report.datatable.rowmanager.getCheckedRows(); + let selected_rows = indexes.map(i => frappe.query_report.data[i]); + + if (!selected_rows.length) { + frappe.throw(__("Please select rows to create Reposting Entries")); + } + + frappe.confirm(__(message), () => { + frappe.call({ + method: 'erpnext.stock.report.stock_ledger_invariant_check.stock_ledger_invariant_check.create_reposting_entries', + args: { + rows: selected_rows, + } + }); + }); + }); + }, +}; diff --git a/erpnext/stock/report/stock_ledger_variance/stock_ledger_variance.json b/erpnext/stock/report/stock_ledger_variance/stock_ledger_variance.json new file mode 100644 index 000000000000..f36ed1b9ca6c --- /dev/null +++ b/erpnext/stock/report/stock_ledger_variance/stock_ledger_variance.json @@ -0,0 +1,22 @@ +{ + "add_total_row": 0, + "columns": [], + "creation": "2023-09-20 10:44:19.414449", + "disabled": 0, + "docstatus": 0, + "doctype": "Report", + "filters": [], + "idx": 0, + "is_standard": "Yes", + "letterhead": null, + "modified": "2023-09-20 10:44:19.414449", + "modified_by": "Administrator", + "module": "Stock", + "name": "Stock Ledger Variance", + "owner": "Administrator", + "prepared_report": 0, + "ref_doctype": "Stock Ledger Entry", + "report_name": "Stock Ledger Variance", + "report_type": "Script Report", + "roles": [] +} \ No newline at end of file diff --git a/erpnext/stock/report/stock_ledger_variance/stock_ledger_variance.py b/erpnext/stock/report/stock_ledger_variance/stock_ledger_variance.py new file mode 100644 index 000000000000..732f108ac413 --- /dev/null +++ b/erpnext/stock/report/stock_ledger_variance/stock_ledger_variance.py @@ -0,0 +1,279 @@ +# Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and contributors +# For license information, please see license.txt + +import frappe +from frappe import _ +from frappe.utils import cint, flt + +from erpnext.stock.report.stock_ledger_invariant_check.stock_ledger_invariant_check import ( + get_data as stock_ledger_invariant_check, +) + + +def execute(filters=None): + columns, data = [], [] + + filters = frappe._dict(filters or {}) + columns = get_columns() + data = get_data(filters) + + return columns, data + + +def get_columns(): + return [ + { + "fieldname": "name", + "fieldtype": "Link", + "label": _("Stock Ledger Entry"), + "options": "Stock Ledger Entry", + }, + { + "fieldname": "posting_date", + "fieldtype": "Data", + "label": _("Posting Date"), + }, + { + "fieldname": "posting_time", + "fieldtype": "Data", + "label": _("Posting Time"), + }, + { + "fieldname": "creation", + "fieldtype": "Data", + "label": _("Creation"), + }, + { + "fieldname": "item_code", + "fieldtype": "Link", + "label": _("Item"), + "options": "Item", + }, + { + "fieldname": "warehouse", + "fieldtype": "Link", + "label": _("Warehouse"), + "options": "Warehouse", + }, + { + "fieldname": "voucher_type", + "fieldtype": "Link", + "label": _("Voucher Type"), + "options": "DocType", + }, + { + "fieldname": "voucher_no", + "fieldtype": "Dynamic Link", + "label": _("Voucher No"), + "options": "voucher_type", + }, + { + "fieldname": "batch_no", + "fieldtype": "Link", + "label": _("Batch"), + "options": "Batch", + }, + { + "fieldname": "use_batchwise_valuation", + "fieldtype": "Check", + "label": _("Batchwise Valuation"), + }, + { + "fieldname": "actual_qty", + "fieldtype": "Float", + "label": _("Qty Change"), + }, + { + "fieldname": "incoming_rate", + "fieldtype": "Float", + "label": _("Incoming Rate"), + }, + { + "fieldname": "consumption_rate", + "fieldtype": "Float", + "label": _("Consumption Rate"), + }, + { + "fieldname": "qty_after_transaction", + "fieldtype": "Float", + "label": _("(A) Qty After Transaction"), + }, + { + "fieldname": "expected_qty_after_transaction", + "fieldtype": "Float", + "label": _("(B) Expected Qty After Transaction"), + }, + { + "fieldname": "difference_in_qty", + "fieldtype": "Float", + "label": _("A - B"), + }, + { + "fieldname": "stock_queue", + "fieldtype": "Data", + "label": _("FIFO/LIFO Queue"), + }, + { + "fieldname": "fifo_queue_qty", + "fieldtype": "Float", + "label": _("(C) Total Qty in Queue"), + }, + { + "fieldname": "fifo_qty_diff", + "fieldtype": "Float", + "label": _("A - C"), + }, + { + "fieldname": "stock_value", + "fieldtype": "Float", + "label": _("(D) Balance Stock Value"), + }, + { + "fieldname": "fifo_stock_value", + "fieldtype": "Float", + "label": _("(E) Balance Stock Value in Queue"), + }, + { + "fieldname": "fifo_value_diff", + "fieldtype": "Float", + "label": _("D - E"), + }, + { + "fieldname": "stock_value_difference", + "fieldtype": "Float", + "label": _("(F) Change in Stock Value"), + }, + { + "fieldname": "stock_value_from_diff", + "fieldtype": "Float", + "label": _("(G) Sum of Change in Stock Value"), + }, + { + "fieldname": "diff_value_diff", + "fieldtype": "Float", + "label": _("G - D"), + }, + { + "fieldname": "fifo_stock_diff", + "fieldtype": "Float", + "label": _("(H) Change in Stock Value (FIFO Queue)"), + }, + { + "fieldname": "fifo_difference_diff", + "fieldtype": "Float", + "label": _("H - F"), + }, + { + "fieldname": "valuation_rate", + "fieldtype": "Float", + "label": _("(I) Valuation Rate"), + }, + { + "fieldname": "fifo_valuation_rate", + "fieldtype": "Float", + "label": _("(J) Valuation Rate as per FIFO"), + }, + { + "fieldname": "fifo_valuation_diff", + "fieldtype": "Float", + "label": _("I - J"), + }, + { + "fieldname": "balance_value_by_qty", + "fieldtype": "Float", + "label": _("(K) Valuation = Value (D) ÷ Qty (A)"), + }, + { + "fieldname": "valuation_diff", + "fieldtype": "Float", + "label": _("I - K"), + }, + ] + + +def get_data(filters=None): + filters = frappe._dict(filters or {}) + item_warehouse_map = get_item_warehouse_combinations(filters) + + data = [] + if item_warehouse_map: + precision = cint(frappe.db.get_single_value("System Settings", "float_precision")) + + for item_warehouse in item_warehouse_map: + report_data = stock_ledger_invariant_check(item_warehouse) + + if not report_data: + continue + + for row in report_data: + if has_difference(row, precision, filters.difference_in): + data.append(add_item_warehouse_details(row, item_warehouse)) + break + + return data + + +def get_item_warehouse_combinations(filters: dict = None) -> dict: + filters = frappe._dict(filters or {}) + + bin = frappe.qb.DocType("Bin") + item = frappe.qb.DocType("Item") + warehouse = frappe.qb.DocType("Warehouse") + + query = ( + frappe.qb.from_(bin) + .inner_join(item) + .on(bin.item_code == item.name) + .inner_join(warehouse) + .on(bin.warehouse == warehouse.name) + .select( + bin.item_code, + bin.warehouse, + ) + .where((item.is_stock_item == 1) & (item.has_serial_no == 0) & (warehouse.is_group == 0)) + ) + + if filters.item_code: + query = query.where(item.name == filters.item_code) + if filters.warehouse: + query = query.where(warehouse.name == filters.warehouse) + if not filters.include_disabled: + query = query.where((item.disabled == 0) & (warehouse.disabled == 0)) + + return query.run(as_dict=1) + + +def has_difference(row, precision, difference_in): + has_qty_difference = flt(row.difference_in_qty, precision) or flt(row.fifo_qty_diff, precision) + has_value_difference = ( + flt(row.diff_value_diff, precision) + or flt(row.fifo_value_diff, precision) + or flt(row.fifo_difference_diff, precision) + ) + has_valuation_difference = flt(row.valuation_diff, precision) or flt( + row.fifo_valuation_diff, precision + ) + + if difference_in == "Qty" and has_qty_difference: + return True + elif difference_in == "Value" and has_value_difference: + return True + elif difference_in == "Valuation" and has_valuation_difference: + return True + elif difference_in not in ["Qty", "Value", "Valuation"] and ( + has_qty_difference or has_value_difference or has_valuation_difference + ): + return True + + return False + + +def add_item_warehouse_details(row, item_warehouse): + row.update( + { + "item_code": item_warehouse.item_code, + "warehouse": item_warehouse.warehouse, + } + ) + + return row From 1181dcf521b6d5bc6f96a217f6991e0d5cb138d3 Mon Sep 17 00:00:00 2001 From: marination Date: Wed, 13 Sep 2023 14:24:56 +0530 Subject: [PATCH 342/501] fix: Update `advance_paid` in SO/PO after unlinking from advance entry (cherry picked from commit 426350eee6efdfeddf300bbeccd7674f0a6d7b9b) --- erpnext/accounts/utils.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/erpnext/accounts/utils.py b/erpnext/accounts/utils.py index 8f0ef869ad32..971932e415ab 100644 --- a/erpnext/accounts/utils.py +++ b/erpnext/accounts/utils.py @@ -560,6 +560,10 @@ def update_reference_in_journal_entry(d, journal_entry, do_not_save=False): """ jv_detail = journal_entry.get("accounts", {"name": d["voucher_detail_no"]})[0] + # Update Advance Paid in SO/PO since they might be getting unlinked + if jv_detail.get("reference_type") in ("Sales Order", "Purchase Order"): + frappe.get_doc(jv_detail.reference_type, jv_detail.reference_name).set_total_advance_paid() + if flt(d["unadjusted_amount"]) - flt(d["allocated_amount"]) != 0: # adjust the unreconciled balance amount_in_account_currency = flt(d["unadjusted_amount"]) - flt(d["allocated_amount"]) @@ -625,6 +629,13 @@ def update_reference_in_payment_entry( if d.voucher_detail_no: existing_row = payment_entry.get("references", {"name": d["voucher_detail_no"]})[0] + + # Update Advance Paid in SO/PO since they are getting unlinked + if existing_row.get("reference_doctype") in ("Sales Order", "Purchase Order"): + frappe.get_doc( + existing_row.reference_doctype, existing_row.reference_name + ).set_total_advance_paid() + original_row = existing_row.as_dict().copy() existing_row.update(reference_details) From 591c720e5165019672faacb9369c9da8087dfbbe Mon Sep 17 00:00:00 2001 From: marination Date: Wed, 13 Sep 2023 19:09:03 +0530 Subject: [PATCH 343/501] test: Impact on SO of advance PE submit and unlinking/replacement by SI (cherry picked from commit 8a4954d713a31a60581414be38ca90c1fad3c794) --- .../sales_invoice/test_sales_invoice.py | 32 ++++++++++++++++--- 1 file changed, 28 insertions(+), 4 deletions(-) diff --git a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py index 378be113e7c1..e0a7ff002bbd 100644 --- a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py +++ b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py @@ -1801,6 +1801,10 @@ def test_outstanding_amount_after_advance_jv_cancellation(self): ) def test_outstanding_amount_after_advance_payment_entry_cancellation(self): + """Test impact of advance PE submission/cancellation on SI and SO.""" + from erpnext.selling.doctype.sales_order.test_sales_order import make_sales_order + + sales_order = make_sales_order(item_code="138-CMS Shoe", qty=1, price_list_rate=500) pe = frappe.get_doc( { "doctype": "Payment Entry", @@ -1820,10 +1824,25 @@ def test_outstanding_amount_after_advance_payment_entry_cancellation(self): "paid_to": "_Test Cash - _TC", } ) + pe.append( + "references", + { + "reference_doctype": "Sales Order", + "reference_name": sales_order.name, + "total_amount": sales_order.grand_total, + "outstanding_amount": sales_order.grand_total, + "allocated_amount": 300, + }, + ) pe.insert() pe.submit() + sales_order.reload() + self.assertEqual(sales_order.advance_paid, 300) + si = frappe.copy_doc(test_records[0]) + si.items[0].sales_order = sales_order.name + si.items[0].so_detail = sales_order.get("items")[0].name si.is_pos = 0 si.append( "advances", @@ -1831,6 +1850,7 @@ def test_outstanding_amount_after_advance_payment_entry_cancellation(self): "doctype": "Sales Invoice Advance", "reference_type": "Payment Entry", "reference_name": pe.name, + "reference_row": pe.references[0].name, "advance_amount": 300, "allocated_amount": 300, "remarks": pe.remarks, @@ -1839,7 +1859,13 @@ def test_outstanding_amount_after_advance_payment_entry_cancellation(self): si.insert() si.submit() - si.load_from_db() + si.reload() + pe.reload() + sales_order.reload() + + # Check if SO is unlinked/replaced by SI in PE & if SO advance paid is 0 + self.assertEqual(pe.references[0].reference_name, si.name) + self.assertEqual(sales_order.advance_paid, 0.0) # check outstanding after advance allocation self.assertEqual( @@ -1847,11 +1873,9 @@ def test_outstanding_amount_after_advance_payment_entry_cancellation(self): flt(si.rounded_total - si.total_advance, si.precision("outstanding_amount")), ) - # added to avoid Document has been modified exception - pe = frappe.get_doc("Payment Entry", pe.name) pe.cancel() + si.reload() - si.load_from_db() # check outstanding after advance cancellation self.assertEqual( flt(si.outstanding_amount), From 9d5fce9091c2e0bd9f32ebf4735f4fefb69aab3b Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Fri, 22 Sep 2023 12:21:57 +0530 Subject: [PATCH 344/501] refactor: ignore PLE's on PCV cancellation (cherry picked from commit 301092dad1020634986dfa744a279b2a793ff30f) --- .../doctype/period_closing_voucher/period_closing_voucher.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/accounts/doctype/period_closing_voucher/period_closing_voucher.py b/erpnext/accounts/doctype/period_closing_voucher/period_closing_voucher.py index af1c06643a1c..d984d86af254 100644 --- a/erpnext/accounts/doctype/period_closing_voucher/period_closing_voucher.py +++ b/erpnext/accounts/doctype/period_closing_voucher/period_closing_voucher.py @@ -33,7 +33,7 @@ def on_submit(self): def on_cancel(self): self.validate_future_closing_vouchers() self.db_set("gle_processing_status", "In Progress") - self.ignore_linked_doctypes = ("GL Entry", "Stock Ledger Entry") + self.ignore_linked_doctypes = ("GL Entry", "Stock Ledger Entry", "Payment Ledger Entry") gle_count = frappe.db.count( "GL Entry", {"voucher_type": "Period Closing Voucher", "voucher_no": self.name, "is_cancelled": 0}, From b4bc44db4a8d459de02ee80b0516f20913897b95 Mon Sep 17 00:00:00 2001 From: Gursheen Anand Date: Sat, 23 Sep 2023 17:50:45 +0530 Subject: [PATCH 345/501] fix: apply gl report filters (cherry picked from commit 5346c67b020fe2ae1bdf01399aa7bf6e57dedaeb) --- .../process_statement_of_accounts.py | 1 + 1 file changed, 1 insertion(+) diff --git a/erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.py b/erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.py index e1f329522051..c622d3089953 100644 --- a/erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.py +++ b/erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.py @@ -64,6 +64,7 @@ def get_report_pdf(doc, consolidated=True): filters = get_common_filters(doc) if doc.report == "General Ledger": + filters.update(get_gl_filters(doc, entry, tax_id, presentation_currency)) col, res = get_soa(filters) for x in [0, -2, -1]: res[x]["account"] = res[x]["account"].replace("'", "") From 57c82c18008ace3b95ccf984be06bb60ec51fe66 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Sat, 23 Sep 2023 14:34:11 +0530 Subject: [PATCH 346/501] feat: Toggle net values in Trial Balance report (cherry picked from commit 06a45897def2595916c93e4b15ded624011adecb) --- .../accounts/report/trial_balance/trial_balance.js | 6 ++++++ .../accounts/report/trial_balance/trial_balance.py | 11 +++++++---- 2 files changed, 13 insertions(+), 4 deletions(-) diff --git a/erpnext/accounts/report/trial_balance/trial_balance.js b/erpnext/accounts/report/trial_balance/trial_balance.js index e45c3adcb6dd..6e233c802f0b 100644 --- a/erpnext/accounts/report/trial_balance/trial_balance.js +++ b/erpnext/accounts/report/trial_balance/trial_balance.js @@ -99,6 +99,12 @@ frappe.require("assets/erpnext/js/financial_statements.js", function() { "label": __("Include Default Book Entries"), "fieldtype": "Check", "default": 1 + }, + { + "fieldname": "show_net_values", + "label": __("Show net values in opening and closing columns"), + "fieldtype": "Check", + "default": 1 } ], "formatter": erpnext.financial_statements.formatter, diff --git a/erpnext/accounts/report/trial_balance/trial_balance.py b/erpnext/accounts/report/trial_balance/trial_balance.py index 376571f03461..2a8aa0c202fc 100644 --- a/erpnext/accounts/report/trial_balance/trial_balance.py +++ b/erpnext/accounts/report/trial_balance/trial_balance.py @@ -120,7 +120,9 @@ def get_data(filters): ignore_opening_entries=True, ) - calculate_values(accounts, gl_entries_by_account, opening_balances) + calculate_values( + accounts, gl_entries_by_account, opening_balances, filters.get("show_net_values") + ) accumulate_values_into_parents(accounts, accounts_by_name) data = prepare_data(accounts, filters, parent_children_map, company_currency) @@ -310,7 +312,7 @@ def get_opening_balance( return gle -def calculate_values(accounts, gl_entries_by_account, opening_balances): +def calculate_values(accounts, gl_entries_by_account, opening_balances, show_net_values): init = { "opening_debit": 0.0, "opening_credit": 0.0, @@ -335,7 +337,8 @@ def calculate_values(accounts, gl_entries_by_account, opening_balances): d["closing_debit"] = d["opening_debit"] + d["debit"] d["closing_credit"] = d["opening_credit"] + d["credit"] - prepare_opening_closing(d) + if show_net_values: + prepare_opening_closing(d) def calculate_total_row(accounts, company_currency): @@ -375,7 +378,7 @@ def prepare_data(accounts, filters, parent_children_map, company_currency): for d in accounts: # Prepare opening closing for group account - if parent_children_map.get(d.account): + if parent_children_map.get(d.account) and filters.get("show_net_values"): prepare_opening_closing(d) has_value = False From e0da8d261f7b0f73f898bc80f51788843cae6051 Mon Sep 17 00:00:00 2001 From: milanpethani Date: Wed, 20 Sep 2023 11:58:46 +0530 Subject: [PATCH 347/501] fix: set customer currency in pos_invoice if exists if currency exists in the profile and customer currency doesn't exists still it will update currency to None, so update customer currency only if exists (cherry picked from commit 041d52e82880610a99903ecf4c459e98bd1fff76) --- erpnext/accounts/doctype/pos_invoice/pos_invoice.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/accounts/doctype/pos_invoice/pos_invoice.py b/erpnext/accounts/doctype/pos_invoice/pos_invoice.py index 7c013b6abef4..d3058e44509a 100644 --- a/erpnext/accounts/doctype/pos_invoice/pos_invoice.py +++ b/erpnext/accounts/doctype/pos_invoice/pos_invoice.py @@ -518,7 +518,7 @@ def set_pos_fields(self, for_validate=False): selling_price_list = ( customer_price_list or customer_group_price_list or profile.get("selling_price_list") ) - if customer_currency != profile.get("currency"): + if customer_currency and customer_currency != profile.get("currency"): self.set("currency", customer_currency) else: From 25f800d3f5cfd4a3c4d58b455b54d47a782d0d76 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Sun, 24 Sep 2023 22:24:56 +0530 Subject: [PATCH 348/501] fix(Material Request): consider project for item details (backport #37215) (#37221) fix(Material Request): consider project for item details (#37215) fix(Material Request): project in item details (cherry picked from commit 7c4ebe27334b188c9fce225103a1e9ce9b64f40e) Co-authored-by: Raffael Meyer <14891507+barredterra@users.noreply.github.com> --- erpnext/stock/doctype/material_request/material_request.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/erpnext/stock/doctype/material_request/material_request.js b/erpnext/stock/doctype/material_request/material_request.js index b096b024f44c..2632501b718b 100644 --- a/erpnext/stock/doctype/material_request/material_request.js +++ b/erpnext/stock/doctype/material_request/material_request.js @@ -218,7 +218,8 @@ frappe.ui.form.on('Material Request', { plc_conversion_rate: 1, rate: item.rate, uom: item.uom, - conversion_factor: item.conversion_factor + conversion_factor: item.conversion_factor, + project: item.project, }, overwrite_warehouse: overwrite_warehouse }, From c9ba777e3cf71dd2b30bc3a5a9d22ede0cebe12a Mon Sep 17 00:00:00 2001 From: Florian HENRY Date: Mon, 25 Sep 2023 10:24:32 +0200 Subject: [PATCH 349/501] refactor(region): Splitting of France Regional logic from ERPNext --- erpnext/patches.txt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/erpnext/patches.txt b/erpnext/patches.txt index 19f8dab9a176..37e709673886 100644 --- a/erpnext/patches.txt +++ b/erpnext/patches.txt @@ -269,6 +269,7 @@ erpnext.patches.v13_0.show_hr_payroll_deprecation_warning erpnext.patches.v13_0.reset_corrupt_defaults erpnext.patches.v13_0.create_accounting_dimensions_for_asset_repair erpnext.patches.v14_0.update_reference_due_date_in_journal_entry +erpnext.patches.v14_0.france_depreciation_warning [post_model_sync] execute:frappe.delete_doc_if_exists('Workspace', 'ERPNext Integrations Settings') @@ -333,7 +334,7 @@ erpnext.patches.v14_0.update_company_in_ldc erpnext.patches.v14_0.set_packed_qty_in_draft_delivery_notes erpnext.patches.v14_0.cleanup_workspaces erpnext.patches.v14_0.enable_allow_existing_serial_no -erpnext.patches.v14_0.set_report_in_process_SOA +erpnext.patches.v14_0.set_report_in_process_SOA erpnext.patches.v14_0.create_accounting_dimensions_for_closing_balance erpnext.patches.v14_0.update_closing_balances #15-07-2023 execute:frappe.defaults.clear_default("fiscal_year") From 279f21d1e54208790a6fdcf4c828fdef3a74468d Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Tue, 26 Sep 2023 13:32:05 +0530 Subject: [PATCH 350/501] fix: incorrect `Parent Task` getting set for 2nd to nth child Task (backport #37230) (#37240) fix: incorrect `Parent Task` getting set for 2nd to nth child Task (#37230) (cherry picked from commit 73fc97495040fe574cef94930e06aaa7b4e87b7b) Co-authored-by: s-aga-r --- erpnext/projects/doctype/project/project.py | 29 ++++++++------------- 1 file changed, 11 insertions(+), 18 deletions(-) diff --git a/erpnext/projects/doctype/project/project.py b/erpnext/projects/doctype/project/project.py index 082ba9152072..5c351faae1a1 100644 --- a/erpnext/projects/doctype/project/project.py +++ b/erpnext/projects/doctype/project/project.py @@ -67,6 +67,7 @@ def copy_from_template(self): tmp_task_details.append(template_task_details) task = self.create_task_from_template(template_task_details) project_tasks.append(task) + self.dependency_mapping(tmp_task_details, project_tasks) def create_task_from_template(self, task_details): @@ -105,36 +106,28 @@ def update_if_holiday(self, date): def dependency_mapping(self, template_tasks, project_tasks): for project_task in project_tasks: - if project_task.get("template_task"): - template_task = frappe.get_doc("Task", project_task.template_task) - else: - template_task = list(filter(lambda x: x.subject == project_task.subject, template_tasks))[0] - template_task = frappe.get_doc("Task", template_task.name) + template_task = frappe.get_doc("Task", project_task.template_task) self.check_depends_on_value(template_task, project_task, project_tasks) self.check_for_parent_tasks(template_task, project_task, project_tasks) def check_depends_on_value(self, template_task, project_task, project_tasks): if template_task.get("depends_on") and not project_task.get("depends_on"): + project_template_map = {pt.template_task: pt for pt in project_tasks} + for child_task in template_task.get("depends_on"): - child_task_subject = frappe.db.get_value("Task", child_task.task, "subject") - corresponding_project_task = list( - filter(lambda x: x.subject == child_task_subject, project_tasks) - ) - if len(corresponding_project_task): + if project_template_map and project_template_map.get(child_task.task): project_task.reload() # reload, as it might have been updated in the previous iteration - project_task.append("depends_on", {"task": corresponding_project_task[0].name}) + project_task.append("depends_on", {"task": project_template_map.get(child_task.task).name}) project_task.save() def check_for_parent_tasks(self, template_task, project_task, project_tasks): if template_task.get("parent_task") and not project_task.get("parent_task"): - parent_task_subject = frappe.db.get_value("Task", template_task.get("parent_task"), "subject") - corresponding_project_task = list( - filter(lambda x: x.subject == parent_task_subject, project_tasks) - ) - if len(corresponding_project_task): - project_task.parent_task = corresponding_project_task[0].name - project_task.save() + for pt in project_tasks: + if pt.template_task == template_task.parent_task: + project_task.parent_task = pt.name + project_task.save() + break def is_row_updated(self, row, existing_task_data, fields): if self.get("__islocal") or not existing_task_data: From 70e2093941dba32ae06d4af123e9daa2441c8e71 Mon Sep 17 00:00:00 2001 From: s-aga-r Date: Tue, 26 Sep 2023 16:34:19 +0530 Subject: [PATCH 351/501] fix: validate duplicate serial no on submit in DN (#37243) --- erpnext/stock/doctype/delivery_note/delivery_note.py | 4 +++- erpnext/stock/doctype/delivery_note/test_delivery_note.py | 8 ++------ 2 files changed, 5 insertions(+), 7 deletions(-) diff --git a/erpnext/stock/doctype/delivery_note/delivery_note.py b/erpnext/stock/doctype/delivery_note/delivery_note.py index ba6e247d2e41..115827a60cba 100644 --- a/erpnext/stock/doctype/delivery_note/delivery_note.py +++ b/erpnext/stock/doctype/delivery_note/delivery_note.py @@ -138,7 +138,9 @@ def validate(self): self.validate_uom_is_integer("stock_uom", "stock_qty") self.validate_uom_is_integer("uom", "qty") self.validate_with_previous_doc() - self.validate_duplicate_serial_nos() + + if self.get("_action") == "submit": + self.validate_duplicate_serial_nos() from erpnext.stock.doctype.packed_item.packed_item import make_packing_list diff --git a/erpnext/stock/doctype/delivery_note/test_delivery_note.py b/erpnext/stock/doctype/delivery_note/test_delivery_note.py index 2acfd84d9440..093e16c1cf81 100644 --- a/erpnext/stock/doctype/delivery_note/test_delivery_note.py +++ b/erpnext/stock/doctype/delivery_note/test_delivery_note.py @@ -1234,14 +1234,10 @@ def test_duplicate_serial_no_in_delivery_note(self): ) dn.items[0].serial_no = "\n".join(serial_nos[:2]) dn.append("items", dn.items[0].as_dict()) + dn.save() # Test - 1: ValidationError should be raised - self.assertRaises(frappe.ValidationError, dn.save) - - # Step - 4: Submit Delivery Note with unique Serial Nos - dn.items[1].serial_no = "\n".join(serial_nos[2:]) - dn.save() - dn.submit() + self.assertRaises(frappe.ValidationError, dn.submit) def tearDown(self): frappe.db.rollback() From 78ab2013e59bb6a95d10164328ad3f7c815497b5 Mon Sep 17 00:00:00 2001 From: rohitwaghchaure Date: Tue, 26 Sep 2023 16:46:19 +0530 Subject: [PATCH 352/501] fix: serial number decimal issue (#37242) --- erpnext/controllers/subcontracting_controller.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/controllers/subcontracting_controller.py b/erpnext/controllers/subcontracting_controller.py index b01b76d1ec91..6faddd2a8be9 100644 --- a/erpnext/controllers/subcontracting_controller.py +++ b/erpnext/controllers/subcontracting_controller.py @@ -655,7 +655,7 @@ def make_sl_entries_for_supplier_warehouse(self, sl_entries): { "item_code": item.rm_item_code, "warehouse": self.supplier_warehouse, - "actual_qty": -1 * flt(item.consumed_qty), + "actual_qty": -1 * flt(item.consumed_qty, item.precision("consumed_qty")), "dependant_sle_voucher_detail_no": item.reference_name, }, ) From 0a0d5b3e6612be1b6e1c9febd4a86a772a02511d Mon Sep 17 00:00:00 2001 From: rohitwaghchaure Date: Tue, 26 Sep 2023 17:55:33 +0530 Subject: [PATCH 353/501] fix: reserved qty for production plan (#37251) --- .../production_plan/test_production_plan.py | 53 +++++++++++++++++++ .../doctype/work_order/work_order.py | 15 +++--- 2 files changed, 61 insertions(+), 7 deletions(-) diff --git a/erpnext/manufacturing/doctype/production_plan/test_production_plan.py b/erpnext/manufacturing/doctype/production_plan/test_production_plan.py index 780806450505..9ea746cbd8b9 100644 --- a/erpnext/manufacturing/doctype/production_plan/test_production_plan.py +++ b/erpnext/manufacturing/doctype/production_plan/test_production_plan.py @@ -1039,6 +1039,59 @@ def test_resered_qty_for_production_plan_for_material_requests(self): self.assertEqual(after_qty, before_qty) + def test_resered_qty_for_production_plan_for_work_order(self): + from erpnext.stock.utils import get_or_make_bin + + bin_name = get_or_make_bin("Raw Material Item 1", "_Test Warehouse - _TC") + before_qty = flt(frappe.db.get_value("Bin", bin_name, "reserved_qty_for_production_plan")) + + pln = create_production_plan(item_code="Test Production Item 1") + + bin_name = get_or_make_bin("Raw Material Item 1", "_Test Warehouse - _TC") + after_qty = flt(frappe.db.get_value("Bin", bin_name, "reserved_qty_for_production_plan")) + + self.assertEqual(after_qty - before_qty, 1) + + pln.make_work_order() + + work_orders = [] + for row in frappe.get_all("Work Order", filters={"production_plan": pln.name}, fields=["name"]): + wo_doc = frappe.get_doc("Work Order", row.name) + wo_doc.source_warehouse = "_Test Warehouse - _TC" + wo_doc.wip_warehouse = "_Test Warehouse 1 - _TC" + wo_doc.fg_warehouse = "_Test Warehouse - _TC" + for d in wo_doc.required_items: + d.source_warehouse = "_Test Warehouse - _TC" + make_stock_entry( + item_code=d.item_code, + qty=d.required_qty, + rate=100, + target="_Test Warehouse - _TC", + ) + + wo_doc.submit() + work_orders.append(wo_doc) + + bin_name = get_or_make_bin("Raw Material Item 1", "_Test Warehouse - _TC") + after_qty = flt(frappe.db.get_value("Bin", bin_name, "reserved_qty_for_production_plan")) + + self.assertEqual(after_qty, before_qty) + + rm_work_order = None + for wo_doc in work_orders: + for d in wo_doc.required_items: + if d.item_code == "Raw Material Item 1": + rm_work_order = wo_doc + break + + if rm_work_order: + s = frappe.get_doc(make_se_from_wo(rm_work_order.name, "Material Transfer for Manufacture", 1)) + s.submit() + bin_name = get_or_make_bin("Raw Material Item 1", "_Test Warehouse - _TC") + after_qty = flt(frappe.db.get_value("Bin", bin_name, "reserved_qty_for_production_plan")) + + self.assertEqual(after_qty, before_qty) + def test_resered_qty_for_production_plan_for_material_requests_with_multi_UOM(self): from erpnext.stock.utils import get_or_make_bin diff --git a/erpnext/manufacturing/doctype/work_order/work_order.py b/erpnext/manufacturing/doctype/work_order/work_order.py index c4b6846376f2..070aaf00459a 100644 --- a/erpnext/manufacturing/doctype/work_order/work_order.py +++ b/erpnext/manufacturing/doctype/work_order/work_order.py @@ -1497,16 +1497,17 @@ def get_reserved_qty_for_production( wo = frappe.qb.DocType("Work Order") wo_item = frappe.qb.DocType("Work Order Item") + if check_production_plan: + qty_field = wo_item.required_qty + else: + qty_field = Case() + qty_field = qty_field.when(wo.skip_transfer == 0, wo_item.required_qty - wo_item.transferred_qty) + qty_field = qty_field.else_(wo_item.required_qty - wo_item.consumed_qty) + query = ( frappe.qb.from_(wo) .from_(wo_item) - .select( - Sum( - Case() - .when(wo.skip_transfer == 0, wo_item.required_qty - wo_item.transferred_qty) - .else_(wo_item.required_qty - wo_item.consumed_qty) - ) - ) + .select(Sum(qty_field)) .where( (wo_item.item_code == item_code) & (wo_item.parent == wo.name) From 1dc58b36603ffbd11a5467f62b36d9148a831534 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Wed, 27 Sep 2023 10:22:27 +0530 Subject: [PATCH 354/501] fix: AP & AR summary filters to match AR (backport #37248) (#37261) * fix: set AR filters after rename (cherry picked from commit 832d7e7d7bdf8569c4ae0d35d929f7a0a440954f) * fix: change filters for AR summary (cherry picked from commit 7d96044d8eb74ad1cb78548183350b7512cc091b) * fix: set new AP summary filters (cherry picked from commit 76a5d94f37054a8c24f84fecf7d55fd9b0d01e81) --------- Co-authored-by: Gursheen Anand --- .../process_statement_of_accounts.py | 3 ++- .../accounts_payable/accounts_payable.js | 3 --- .../accounts_payable_summary.js | 23 +++++++++++++++--- .../accounts_receivable.js | 2 -- .../accounts_receivable_summary.js | 24 ++++++++++++++++--- 5 files changed, 43 insertions(+), 12 deletions(-) diff --git a/erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.py b/erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.py index c622d3089953..5055c345917e 100644 --- a/erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.py +++ b/erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.py @@ -143,7 +143,8 @@ def get_gl_filters(doc, entry, tax_id, presentation_currency): def get_ar_filters(doc, entry): return { "report_date": doc.posting_date if doc.posting_date else None, - "customer": entry.customer, + "party_type": "Customer", + "party": entry.customer, "customer_name": entry.customer_name if entry.customer_name else None, "payment_terms_template": doc.payment_terms_template if doc.payment_terms_template else None, "sales_partner": doc.sales_partner if doc.sales_partner else None, diff --git a/erpnext/accounts/report/accounts_payable/accounts_payable.js b/erpnext/accounts/report/accounts_payable/accounts_payable.js index 27a85701edda..484ff7fa2b33 100644 --- a/erpnext/accounts/report/accounts_payable/accounts_payable.js +++ b/erpnext/accounts/report/accounts_payable/accounts_payable.js @@ -108,11 +108,8 @@ frappe.query_reports["Accounts Payable"] = { }, on_change: () => { frappe.query_report.set_filter_value('party', ""); - let party_type = frappe.query_report.get_filter_value('party_type'); frappe.query_report.toggle_filter_display('supplier_group', frappe.query_report.get_filter_value('party_type') !== "Supplier"); - } - }, { "fieldname":"party", diff --git a/erpnext/accounts/report/accounts_payable_summary/accounts_payable_summary.js b/erpnext/accounts/report/accounts_payable_summary/accounts_payable_summary.js index ea200720dff9..8a1725c0485c 100644 --- a/erpnext/accounts/report/accounts_payable_summary/accounts_payable_summary.js +++ b/erpnext/accounts/report/accounts_payable_summary/accounts_payable_summary.js @@ -72,10 +72,27 @@ frappe.query_reports["Accounts Payable Summary"] = { } }, { - "fieldname":"supplier", - "label": __("Supplier"), + "fieldname": "party_type", + "label": __("Party Type"), "fieldtype": "Link", - "options": "Supplier" + "options": "Party Type", + get_query: () => { + return { + filters: { + 'account_type': 'Payable' + } + }; + }, + on_change: () => { + frappe.query_report.set_filter_value('party', ""); + frappe.query_report.toggle_filter_display('supplier_group', frappe.query_report.get_filter_value('party_type') !== "Supplier"); + } + }, + { + "fieldname":"party", + "label": __("Party"), + "fieldtype": "Dynamic Link", + "options": "party_type", }, { "fieldname":"payment_terms_template", diff --git a/erpnext/accounts/report/accounts_receivable/accounts_receivable.js b/erpnext/accounts/report/accounts_receivable/accounts_receivable.js index bb00d616dbcc..67a14e7880a0 100644 --- a/erpnext/accounts/report/accounts_receivable/accounts_receivable.js +++ b/erpnext/accounts/report/accounts_receivable/accounts_receivable.js @@ -52,9 +52,7 @@ frappe.query_reports["Accounts Receivable"] = { }, on_change: () => { frappe.query_report.set_filter_value('party', ""); - let party_type = frappe.query_report.get_filter_value('party_type'); frappe.query_report.toggle_filter_display('customer_group', frappe.query_report.get_filter_value('party_type') !== "Customer"); - } }, { diff --git a/erpnext/accounts/report/accounts_receivable_summary/accounts_receivable_summary.js b/erpnext/accounts/report/accounts_receivable_summary/accounts_receivable_summary.js index 715cd6476e8a..a78fbeb03088 100644 --- a/erpnext/accounts/report/accounts_receivable_summary/accounts_receivable_summary.js +++ b/erpnext/accounts/report/accounts_receivable_summary/accounts_receivable_summary.js @@ -72,10 +72,28 @@ frappe.query_reports["Accounts Receivable Summary"] = { } }, { - "fieldname":"customer", - "label": __("Customer"), + "fieldname": "party_type", + "label": __("Party Type"), "fieldtype": "Link", - "options": "Customer" + "options": "Party Type", + "Default": "Customer", + get_query: () => { + return { + filters: { + 'account_type': 'Receivable' + } + }; + }, + on_change: () => { + frappe.query_report.set_filter_value('party', ""); + frappe.query_report.toggle_filter_display('customer_group', frappe.query_report.get_filter_value('party_type') !== "Customer"); + } + }, + { + "fieldname":"party", + "label": __("Party"), + "fieldtype": "Dynamic Link", + "options": "party_type", }, { "fieldname":"customer_group", From 92eabe3cf50799c6709c44197e138738560aac14 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Wed, 27 Sep 2023 10:22:09 +0530 Subject: [PATCH 355/501] fix: PCV posting issues (#37029) * fix: PCV posting issues * fix: process closing entries separately in a background job * test: Update tests * chore: fix broken ci (cherry picked from commit 8c5fcb825716a63a074b1d83b830a06412b4c035) # Conflicts: # erpnext/accounts/doctype/payment_request/payment_request.json --- .../payment_request/payment_request.json | 11 ++++ .../period_closing_voucher.json | 10 +++- .../period_closing_voucher.py | 53 ++++++++++++++----- .../test_period_closing_voucher.py | 2 +- 4 files changed, 60 insertions(+), 16 deletions(-) diff --git a/erpnext/accounts/doctype/payment_request/payment_request.json b/erpnext/accounts/doctype/payment_request/payment_request.json index 381f3fb531a9..8b139e27d467 100644 --- a/erpnext/accounts/doctype/payment_request/payment_request.json +++ b/erpnext/accounts/doctype/payment_request/payment_request.json @@ -317,9 +317,16 @@ }, { "fieldname": "payment_url", +<<<<<<< HEAD "fieldtype": "Small Text", "hidden": 1, "label": "payment_url", +======= + "fieldtype": "Data", + "hidden": 1, + "length": 500, + "options": "URL", +>>>>>>> 8c5fcb8257 (fix: PCV posting issues (#37029)) "read_only": 1 }, { @@ -394,7 +401,11 @@ "index_web_pages_for_search": 1, "is_submittable": 1, "links": [], +<<<<<<< HEAD "modified": "2022-12-21 16:56:40.115737", +======= + "modified": "2023-09-27 09:51:42.277638", +>>>>>>> 8c5fcb8257 (fix: PCV posting issues (#37029)) "modified_by": "Administrator", "module": "Accounts", "name": "Payment Request", diff --git a/erpnext/accounts/doctype/period_closing_voucher/period_closing_voucher.json b/erpnext/accounts/doctype/period_closing_voucher/period_closing_voucher.json index 54a76b341963..624b5f82f64c 100644 --- a/erpnext/accounts/doctype/period_closing_voucher/period_closing_voucher.json +++ b/erpnext/accounts/doctype/period_closing_voucher/period_closing_voucher.json @@ -8,6 +8,7 @@ "transaction_date", "posting_date", "fiscal_year", + "year_start_date", "amended_from", "company", "column_break1", @@ -100,16 +101,22 @@ "fieldtype": "Text", "label": "Error Message", "read_only": 1 + }, + { + "fieldname": "year_start_date", + "fieldtype": "Date", + "label": "Year Start Date" } ], "icon": "fa fa-file-text", "idx": 1, "is_submittable": 1, "links": [], - "modified": "2022-07-20 14:51:04.714154", + "modified": "2023-09-11 20:19:11.810533", "modified_by": "Administrator", "module": "Accounts", "name": "Period Closing Voucher", + "naming_rule": "Expression (old style)", "owner": "Administrator", "permissions": [ { @@ -144,5 +151,6 @@ "search_fields": "posting_date, fiscal_year", "sort_field": "modified", "sort_order": "DESC", + "states": [], "title_field": "closing_account_head" } \ No newline at end of file diff --git a/erpnext/accounts/doctype/period_closing_voucher/period_closing_voucher.py b/erpnext/accounts/doctype/period_closing_voucher/period_closing_voucher.py index d984d86af254..674db6c2e430 100644 --- a/erpnext/accounts/doctype/period_closing_voucher/period_closing_voucher.py +++ b/erpnext/accounts/doctype/period_closing_voucher/period_closing_voucher.py @@ -95,15 +95,23 @@ def validate_posting_date(self): self.check_if_previous_year_closed() - pce = frappe.db.sql( - """select name from `tabPeriod Closing Voucher` - where posting_date > %s and fiscal_year = %s and docstatus = 1 and company = %s""", - (self.posting_date, self.fiscal_year, self.company), + pcv = frappe.qb.DocType("Period Closing Voucher") + existing_entry = ( + frappe.qb.from_(pcv) + .select(pcv.name) + .where( + (pcv.posting_date >= self.posting_date) + & (pcv.fiscal_year == self.fiscal_year) + & (pcv.docstatus == 1) + & (pcv.company == self.company) + ) + .run() ) - if pce and pce[0][0]: + + if existing_entry and existing_entry[0][0]: frappe.throw( _("Another Period Closing Entry {0} has been made after {1}").format( - pce[0][0], self.posting_date + existing_entry[0][0], self.posting_date ) ) @@ -130,18 +138,27 @@ def make_gl_entries(self, get_opening_entries=False): frappe.enqueue( process_gl_entries, gl_entries=gl_entries, + voucher_name=self.name, + timeout=3000, + ) + + frappe.enqueue( + process_closing_entries, + gl_entries=gl_entries, closing_entries=closing_entries, voucher_name=self.name, company=self.company, closing_date=self.posting_date, - queue="long", + timeout=3000, ) + frappe.msgprint( _("The GL Entries will be processed in the background, it can take a few minutes."), alert=True, ) else: - process_gl_entries(gl_entries, closing_entries, self.name, self.company, self.posting_date) + process_gl_entries(gl_entries, self.name) + process_closing_entries(gl_entries, closing_entries, self.name, self.company, self.posting_date) def get_grouped_gl_entries(self, get_opening_entries=False): closing_entries = [] @@ -322,17 +339,12 @@ def get_balances_based_on_dimensions( return query.run(as_dict=1) -def process_gl_entries(gl_entries, closing_entries, voucher_name, company, closing_date): - from erpnext.accounts.doctype.account_closing_balance.account_closing_balance import ( - make_closing_entries, - ) +def process_gl_entries(gl_entries, voucher_name): from erpnext.accounts.general_ledger import make_gl_entries try: if gl_entries: make_gl_entries(gl_entries, merge_entries=False) - - make_closing_entries(gl_entries + closing_entries, voucher_name, company, closing_date) frappe.db.set_value("Period Closing Voucher", voucher_name, "gle_processing_status", "Completed") except Exception as e: frappe.db.rollback() @@ -340,6 +352,19 @@ def process_gl_entries(gl_entries, closing_entries, voucher_name, company, closi frappe.db.set_value("Period Closing Voucher", voucher_name, "gle_processing_status", "Failed") +def process_closing_entries(gl_entries, closing_entries, voucher_name, company, closing_date): + from erpnext.accounts.doctype.account_closing_balance.account_closing_balance import ( + make_closing_entries, + ) + + try: + if gl_entries + closing_entries: + make_closing_entries(gl_entries + closing_entries, voucher_name, company, closing_date) + except Exception as e: + frappe.db.rollback() + frappe.log_error(e) + + def make_reverse_gl_entries(voucher_type, voucher_no): from erpnext.accounts.general_ledger import make_reverse_gl_entries diff --git a/erpnext/accounts/doctype/period_closing_voucher/test_period_closing_voucher.py b/erpnext/accounts/doctype/period_closing_voucher/test_period_closing_voucher.py index 2ea0971ee72d..103aea965bb3 100644 --- a/erpnext/accounts/doctype/period_closing_voucher/test_period_closing_voucher.py +++ b/erpnext/accounts/doctype/period_closing_voucher/test_period_closing_voucher.py @@ -10,7 +10,7 @@ from erpnext.accounts.doctype.finance_book.test_finance_book import create_finance_book from erpnext.accounts.doctype.journal_entry.test_journal_entry import make_journal_entry from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice -from erpnext.accounts.utils import get_fiscal_year, now +from erpnext.accounts.utils import get_fiscal_year class TestPeriodClosingVoucher(unittest.TestCase): From e483b4a78a260629e994eb2e0a36a59b96919288 Mon Sep 17 00:00:00 2001 From: vr-greycube <66350441+vr-greycube@users.noreply.github.com> Date: Wed, 27 Sep 2023 10:38:32 +0530 Subject: [PATCH 356/501] fix: Use default Cost Center of the Company for additional discount (#37234) fix: Set cost center as default company cost center When Discount Accounting in enabled in Selling Settings, use Company default Cost Center while making GL entries for additional_discount_account (cherry picked from commit 4ada5a488ebee75c3286af5c9492ea53274730c4) --- erpnext/controllers/accounts_controller.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/controllers/accounts_controller.py b/erpnext/controllers/accounts_controller.py index bd2354e74522..c7e21638bc75 100644 --- a/erpnext/controllers/accounts_controller.py +++ b/erpnext/controllers/accounts_controller.py @@ -1406,7 +1406,7 @@ def make_discount_gl_entries(self, gl_entries): "account": self.additional_discount_account, "against": supplier_or_customer, dr_or_cr: self.base_discount_amount, - "cost_center": self.cost_center, + "cost_center": self.cost_center or erpnext.get_default_cost_center(self.company), }, item=self, ) From abb60442918c5e86e00bbf8e5f0d07e5f1f5f2ab Mon Sep 17 00:00:00 2001 From: Frappe PR Bot Date: Wed, 27 Sep 2023 05:40:33 +0000 Subject: [PATCH 357/501] chore(release): Bumped to Version 14.41.0 # [14.41.0](https://github.com/frappe/erpnext/compare/v14.40.0...v14.41.0) (2023-09-27) ### Bug Fixes * allow to select parent warehouse in the website item (backport [#37047](https://github.com/frappe/erpnext/issues/37047)) ([#37173](https://github.com/frappe/erpnext/issues/37173)) ([56657b6](https://github.com/frappe/erpnext/commit/56657b6122c056a8ba091b41fd1b6e94447f34da)) * AP & AR summary filters to match AR (backport [#37248](https://github.com/frappe/erpnext/issues/37248)) ([#37261](https://github.com/frappe/erpnext/issues/37261)) ([1dc58b3](https://github.com/frappe/erpnext/commit/1dc58b36603ffbd11a5467f62b36d9148a831534)) * apply gl report filters ([b4bc44d](https://github.com/frappe/erpnext/commit/b4bc44db4a8d459de02ee80b0516f20913897b95)) * german translation of Sales and Purchase Invoice ([#37122](https://github.com/frappe/erpnext/issues/37122)) ([8236814](https://github.com/frappe/erpnext/commit/8236814270f2a74bb5ae46ea7d3818c2ddc7619b)) * incorrect `Parent Task` getting set for 2nd to nth child Task (backport [#37230](https://github.com/frappe/erpnext/issues/37230)) ([#37240](https://github.com/frappe/erpnext/issues/37240)) ([279f21d](https://github.com/frappe/erpnext/commit/279f21d1e54208790a6fdcf4c828fdef3a74468d)) * labels for `Stock Ledger Invariant Check` report (backport [#37150](https://github.com/frappe/erpnext/issues/37150)) ([#37176](https://github.com/frappe/erpnext/issues/37176)) ([f2bcfb5](https://github.com/frappe/erpnext/commit/f2bcfb5f977927cad9f4288b667bbce4d231c6c8)) * **Material Request:** consider project for item details (backport [#37215](https://github.com/frappe/erpnext/issues/37215)) ([#37221](https://github.com/frappe/erpnext/issues/37221)) ([25f800d](https://github.com/frappe/erpnext/commit/25f800d3f5cfd4a3c4d58b455b54d47a782d0d76)) * reserved qty for production plan ([#37251](https://github.com/frappe/erpnext/issues/37251)) ([0a0d5b3](https://github.com/frappe/erpnext/commit/0a0d5b3e6612be1b6e1c9febd4a86a772a02511d)) * serial number decimal issue ([#37242](https://github.com/frappe/erpnext/issues/37242)) ([78ab201](https://github.com/frappe/erpnext/commit/78ab2013e59bb6a95d10164328ad3f7c815497b5)) * set customer currency in pos_invoice if exists ([e0da8d2](https://github.com/frappe/erpnext/commit/e0da8d261f7b0f73f898bc80f51788843cae6051)) * Update `advance_paid` in SO/PO after unlinking from advance entry ([1181dcf](https://github.com/frappe/erpnext/commit/1181dcf521b6d5bc6f96a217f6991e0d5cb138d3)) * validate duplicate serial no on submit in DN ([#37243](https://github.com/frappe/erpnext/issues/37243)) ([70e2093](https://github.com/frappe/erpnext/commit/70e2093941dba32ae06d4af123e9daa2441c8e71)) ### Features * `Stock Ledger Variance` report (backport [#37165](https://github.com/frappe/erpnext/issues/37165)) ([#37183](https://github.com/frappe/erpnext/issues/37183)) ([02fc67c](https://github.com/frappe/erpnext/commit/02fc67c83c94b6277e4e6dafd22381ed04da0ad6)) * Toggle net values in Trial Balance report ([57c82c1](https://github.com/frappe/erpnext/commit/57c82c18008ace3b95ccf984be06bb60ec51fe66)) --- erpnext/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/__init__.py b/erpnext/__init__.py index 529c9ca9d802..22fe46b476b7 100644 --- a/erpnext/__init__.py +++ b/erpnext/__init__.py @@ -3,7 +3,7 @@ import frappe -__version__ = "14.40.0" +__version__ = "14.41.0" def get_default_company(user=None): From 8d813e32565f2eb2ce5708067605e9cf1d816c81 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Wed, 27 Sep 2023 11:19:39 +0530 Subject: [PATCH 358/501] chore: resolve conflicts --- .../doctype/payment_request/payment_request.json | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/erpnext/accounts/doctype/payment_request/payment_request.json b/erpnext/accounts/doctype/payment_request/payment_request.json index 8b139e27d467..381f3fb531a9 100644 --- a/erpnext/accounts/doctype/payment_request/payment_request.json +++ b/erpnext/accounts/doctype/payment_request/payment_request.json @@ -317,16 +317,9 @@ }, { "fieldname": "payment_url", -<<<<<<< HEAD "fieldtype": "Small Text", "hidden": 1, "label": "payment_url", -======= - "fieldtype": "Data", - "hidden": 1, - "length": 500, - "options": "URL", ->>>>>>> 8c5fcb8257 (fix: PCV posting issues (#37029)) "read_only": 1 }, { @@ -401,11 +394,7 @@ "index_web_pages_for_search": 1, "is_submittable": 1, "links": [], -<<<<<<< HEAD "modified": "2022-12-21 16:56:40.115737", -======= - "modified": "2023-09-27 09:51:42.277638", ->>>>>>> 8c5fcb8257 (fix: PCV posting issues (#37029)) "modified_by": "Administrator", "module": "Accounts", "name": "Payment Request", From 6a8146ba8a2ef34da586adca3a3c27b12821deec Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Wed, 27 Sep 2023 12:36:58 +0530 Subject: [PATCH 359/501] fix: trial balance report freezes when adding filters (backport #37264) (#37265) fix: trial balance report freezes when adding filters (#37264) fix: Only add onclick if correct data is returned workaround for https://github.com/frappe/datatable/issues/177 (cherry picked from commit 2dc95e5d59526a531b064815d195df8413d1b837) Co-authored-by: Ankush Menat --- erpnext/public/js/financial_statements.js | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/erpnext/public/js/financial_statements.js b/erpnext/public/js/financial_statements.js index 959cf507d53d..907a775bfa53 100644 --- a/erpnext/public/js/financial_statements.js +++ b/erpnext/public/js/financial_statements.js @@ -6,8 +6,10 @@ erpnext.financial_statements = { if (data && column.fieldname=="account") { value = data.account_name || value; - column.link_onclick = - "erpnext.financial_statements.open_general_ledger(" + JSON.stringify(data) + ")"; + if (data.account) { + column.link_onclick = + "erpnext.financial_statements.open_general_ledger(" + JSON.stringify(data) + ")"; + } column.is_tree = true; } From 53817e463fa8f69c5138a38556826d3ee914b811 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Wed, 27 Sep 2023 12:37:05 +0530 Subject: [PATCH 360/501] fix: trial balance report freezes when adding filters (backport #37264) (#37266) fix: trial balance report freezes when adding filters (#37264) fix: Only add onclick if correct data is returned workaround for https://github.com/frappe/datatable/issues/177 (cherry picked from commit 2dc95e5d59526a531b064815d195df8413d1b837) Co-authored-by: Ankush Menat --- erpnext/public/js/financial_statements.js | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/erpnext/public/js/financial_statements.js b/erpnext/public/js/financial_statements.js index 959cf507d53d..907a775bfa53 100644 --- a/erpnext/public/js/financial_statements.js +++ b/erpnext/public/js/financial_statements.js @@ -6,8 +6,10 @@ erpnext.financial_statements = { if (data && column.fieldname=="account") { value = data.account_name || value; - column.link_onclick = - "erpnext.financial_statements.open_general_ledger(" + JSON.stringify(data) + ")"; + if (data.account) { + column.link_onclick = + "erpnext.financial_statements.open_general_ledger(" + JSON.stringify(data) + ")"; + } column.is_tree = true; } From 1b86e7e2f5f121e2b1885a914a2cdbe221c0f0da Mon Sep 17 00:00:00 2001 From: Frappe PR Bot Date: Wed, 27 Sep 2023 07:08:57 +0000 Subject: [PATCH 361/501] chore(release): Bumped to Version 14.41.1 ## [14.41.1](https://github.com/frappe/erpnext/compare/v14.41.0...v14.41.1) (2023-09-27) ### Bug Fixes * trial balance report freezes when adding filters (backport [#37264](https://github.com/frappe/erpnext/issues/37264)) ([#37266](https://github.com/frappe/erpnext/issues/37266)) ([53817e4](https://github.com/frappe/erpnext/commit/53817e463fa8f69c5138a38556826d3ee914b811)) --- erpnext/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/__init__.py b/erpnext/__init__.py index 22fe46b476b7..f25a601a2ec3 100644 --- a/erpnext/__init__.py +++ b/erpnext/__init__.py @@ -3,7 +3,7 @@ import frappe -__version__ = "14.41.0" +__version__ = "14.41.1" def get_default_company(user=None): From 8fe4a4d3aa7857e5e241bcb7145065753ab8b4cb Mon Sep 17 00:00:00 2001 From: rohitwaghchaure Date: Wed, 27 Sep 2023 20:02:05 +0530 Subject: [PATCH 362/501] fix: incorrect qty for material request in Production Plan (#37270) --- .../production_plan/production_plan.py | 27 ++++----- .../production_plan/test_production_plan.py | 58 +++++++++++++++++++ 2 files changed, 70 insertions(+), 15 deletions(-) diff --git a/erpnext/manufacturing/doctype/production_plan/production_plan.py b/erpnext/manufacturing/doctype/production_plan/production_plan.py index fcaae79d6c5e..52bfeafea515 100644 --- a/erpnext/manufacturing/doctype/production_plan/production_plan.py +++ b/erpnext/manufacturing/doctype/production_plan/production_plan.py @@ -1508,6 +1508,10 @@ def get_items_for_material_requests(doc, warehouses=None, get_parent_warehouse_d def get_materials_from_other_locations(item, warehouses, new_mr_items, company): from erpnext.stock.doctype.pick_list.pick_list import get_available_item_locations + stock_uom, purchase_uom = frappe.db.get_value( + "Item", item.get("item_code"), ["stock_uom", "purchase_uom"] + ) + locations = get_available_item_locations( item.get("item_code"), warehouses, item.get("quantity"), company, ignore_validation=True ) @@ -1518,6 +1522,10 @@ def get_materials_from_other_locations(item, warehouses, new_mr_items, company): if required_qty <= 0: return + conversion_factor = 1.0 + if purchase_uom != stock_uom and purchase_uom == item["uom"]: + conversion_factor = get_uom_conversion_factor(item["item_code"], item["uom"]) + new_dict = copy.deepcopy(item) quantity = required_qty if d.get("qty") > required_qty else d.get("qty") @@ -1530,25 +1538,14 @@ def get_materials_from_other_locations(item, warehouses, new_mr_items, company): } ) - required_qty -= quantity + required_qty -= quantity / conversion_factor new_mr_items.append(new_dict) # raise purchase request for remaining qty - if required_qty: - stock_uom, purchase_uom = frappe.db.get_value( - "Item", item["item_code"], ["stock_uom", "purchase_uom"] - ) - - if purchase_uom != stock_uom and purchase_uom == item["uom"]: - conversion_factor = get_uom_conversion_factor(item["item_code"], item["uom"]) - if not (conversion_factor or frappe.flags.show_qty_in_stock_uom): - frappe.throw( - _("UOM Conversion factor ({0} -> {1}) not found for item: {2}").format( - purchase_uom, stock_uom, item["item_code"] - ) - ) - required_qty = required_qty / conversion_factor + precision = frappe.get_precision("Material Request Plan Item", "quantity") + if flt(required_qty, precision) > 0: + required_qty = required_qty if frappe.db.get_value("UOM", purchase_uom, "must_be_whole_number"): required_qty = ceil(required_qty) diff --git a/erpnext/manufacturing/doctype/production_plan/test_production_plan.py b/erpnext/manufacturing/doctype/production_plan/test_production_plan.py index 9ea746cbd8b9..c276f6a91b91 100644 --- a/erpnext/manufacturing/doctype/production_plan/test_production_plan.py +++ b/erpnext/manufacturing/doctype/production_plan/test_production_plan.py @@ -1219,6 +1219,64 @@ def test_skip_available_qty_for_sub_assembly_items(self): if row.item_code == "SubAssembly2 For SUB Test": self.assertEqual(row.quantity, 10) + def test_transfer_and_purchase_mrp_for_purchase_uom(self): + from erpnext.manufacturing.doctype.bom.test_bom import create_nested_bom + from erpnext.stock.doctype.warehouse.test_warehouse import create_warehouse + + bom_tree = { + "Test FG Item INK PEN": { + "Test RM Item INK": {}, + } + } + + parent_bom = create_nested_bom(bom_tree, prefix="") + if not frappe.db.exists("UOM Conversion Detail", {"parent": "Test RM Item INK", "uom": "Kg"}): + doc = frappe.get_doc("Item", "Test RM Item INK") + doc.purchase_uom = "Kg" + doc.append("uoms", {"uom": "Kg", "conversion_factor": 0.5}) + doc.save() + + wh1 = create_warehouse("PNE Warehouse", company="_Test Company") + wh2 = create_warehouse("MBE Warehouse", company="_Test Company") + mrp_warhouse = create_warehouse("MRPBE Warehouse", company="_Test Company") + + make_stock_entry( + item_code="Test RM Item INK", + qty=2, + rate=100, + target=wh1, + ) + + make_stock_entry( + item_code="Test RM Item INK", + qty=2, + rate=100, + target=wh2, + ) + + plan = create_production_plan( + item_code=parent_bom.item, + planned_qty=10, + do_not_submit=1, + warehouse="_Test Warehouse - _TC", + ) + + plan.for_warehouse = mrp_warhouse + + items = get_items_for_material_requests( + plan.as_dict(), warehouses=[{"warehouse": wh1}, {"warehouse": wh2}] + ) + + for row in items: + row = frappe._dict(row) + if row.material_request_type == "Material Transfer": + self.assertTrue(row.from_warehouse in [wh1, wh2]) + self.assertEqual(row.quantity, 2) + + if row.material_request_type == "Purchase": + self.assertTrue(row.warehouse == mrp_warhouse) + self.assertEqual(row.quantity, 12) + def create_production_plan(**args): """ From 49f0f1ca09c6182d3e55a94733f1acc4ad5a1342 Mon Sep 17 00:00:00 2001 From: Gursheen Anand Date: Wed, 27 Sep 2023 13:01:18 +0530 Subject: [PATCH 363/501] fix: set route filter values for AP (cherry picked from commit 888ed36eed9eae73d23de71feeb67425a085e950) --- .../accounts_payable/accounts_payable.js | 38 ++++++++++++------- erpnext/buying/doctype/supplier/supplier.js | 2 +- 2 files changed, 26 insertions(+), 14 deletions(-) diff --git a/erpnext/accounts/report/accounts_payable/accounts_payable.js b/erpnext/accounts/report/accounts_payable/accounts_payable.js index 484ff7fa2b33..9c73cbb344f6 100644 --- a/erpnext/accounts/report/accounts_payable/accounts_payable.js +++ b/erpnext/accounts/report/accounts_payable/accounts_payable.js @@ -95,18 +95,11 @@ frappe.query_reports["Accounts Payable"] = { "options": "Payment Terms Template" }, { - "fieldname": "party_type", + "fieldname":"party_type", "label": __("Party Type"), - "fieldtype": "Link", - "options": "Party Type", - get_query: () => { - return { - filters: { - 'account_type': 'Payable' - } - }; - }, - on_change: () => { + "fieldtype": "Autocomplete", + options: get_party_type_options(), + on_change: function() { frappe.query_report.set_filter_value('party', ""); frappe.query_report.toggle_filter_display('supplier_group', frappe.query_report.get_filter_value('party_type') !== "Supplier"); } @@ -114,8 +107,15 @@ frappe.query_reports["Accounts Payable"] = { { "fieldname":"party", "label": __("Party"), - "fieldtype": "Dynamic Link", - "options": "party_type", + "fieldtype": "MultiSelectList", + get_data: function(txt) { + if (!frappe.query_report.filters) return; + + let party_type = frappe.query_report.get_filter_value('party_type'); + if (!party_type) return; + + return frappe.db.get_link_options(party_type, txt); + }, }, { "fieldname": "supplier_group", @@ -164,3 +164,15 @@ frappe.query_reports["Accounts Payable"] = { } erpnext.utils.add_dimensions('Accounts Payable', 9); + +function get_party_type_options() { + let options = []; + frappe.db.get_list( + "Party Type", {filters:{"account_type": "Payable"}, fields:['name']} + ).then((res) => { + res.forEach((party_type) => { + options.push(party_type.name); + }); + }); + return options; +} \ No newline at end of file diff --git a/erpnext/buying/doctype/supplier/supplier.js b/erpnext/buying/doctype/supplier/supplier.js index 1ae6f0364746..e0e33b6848bd 100644 --- a/erpnext/buying/doctype/supplier/supplier.js +++ b/erpnext/buying/doctype/supplier/supplier.js @@ -68,7 +68,7 @@ frappe.ui.form.on("Supplier", { }, __("View")); frm.add_custom_button(__('Accounts Payable'), function () { - frappe.set_route('query-report', 'Accounts Payable', { supplier: frm.doc.name }); + frappe.set_route('query-report', 'Accounts Payable', { party_type: "Supplier", party: frm.doc.name }); }, __("View")); frm.add_custom_button(__('Bank Account'), function () { From 2b30727fdcc447d8e96b4d735feb230f2956d04c Mon Sep 17 00:00:00 2001 From: Gursheen Anand Date: Wed, 27 Sep 2023 13:01:48 +0530 Subject: [PATCH 364/501] fix: set route filter values for AR (cherry picked from commit 9d15124a6a3ad1bacc944f2f62b3999548bd40e8) --- .../accounts_receivable.js | 42 ++++++++++++------- erpnext/selling/doctype/customer/customer.js | 2 +- 2 files changed, 29 insertions(+), 15 deletions(-) diff --git a/erpnext/accounts/report/accounts_receivable/accounts_receivable.js b/erpnext/accounts/report/accounts_receivable/accounts_receivable.js index 67a14e7880a0..1073be0bdc40 100644 --- a/erpnext/accounts/report/accounts_receivable/accounts_receivable.js +++ b/erpnext/accounts/report/accounts_receivable/accounts_receivable.js @@ -1,6 +1,8 @@ // Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors // License: GNU General Public License v3. See license.txt +frappe.provide("erpnext.utils"); + frappe.query_reports["Accounts Receivable"] = { "filters": [ { @@ -38,19 +40,11 @@ frappe.query_reports["Accounts Receivable"] = { } }, { - "fieldname": "party_type", + "fieldname":"party_type", "label": __("Party Type"), - "fieldtype": "Link", - "options": "Party Type", - "Default": "Customer", - get_query: () => { - return { - filters: { - 'account_type': 'Receivable' - } - }; - }, - on_change: () => { + "fieldtype": "Autocomplete", + options: get_party_type_options(), + on_change: function() { frappe.query_report.set_filter_value('party', ""); frappe.query_report.toggle_filter_display('customer_group', frappe.query_report.get_filter_value('party_type') !== "Customer"); } @@ -58,8 +52,15 @@ frappe.query_reports["Accounts Receivable"] = { { "fieldname":"party", "label": __("Party"), - "fieldtype": "Dynamic Link", - "options": "party_type", + "fieldtype": "MultiSelectList", + get_data: function(txt) { + if (!frappe.query_report.filters) return; + + let party_type = frappe.query_report.get_filter_value('party_type'); + if (!party_type) return; + + return frappe.db.get_link_options(party_type, txt); + }, }, { "fieldname": "party_account", @@ -192,3 +193,16 @@ frappe.query_reports["Accounts Receivable"] = { } erpnext.utils.add_dimensions('Accounts Receivable', 9); + + +function get_party_type_options() { + let options = []; + frappe.db.get_list( + "Party Type", {filters:{"account_type": "Receivable"}, fields:['name']} + ).then((res) => { + res.forEach((party_type) => { + options.push(party_type.name); + }); + }); + return options; +} \ No newline at end of file diff --git a/erpnext/selling/doctype/customer/customer.js b/erpnext/selling/doctype/customer/customer.js index b53f339229b2..432abfce73b2 100644 --- a/erpnext/selling/doctype/customer/customer.js +++ b/erpnext/selling/doctype/customer/customer.js @@ -118,7 +118,7 @@ frappe.ui.form.on("Customer", { // custom buttons frm.add_custom_button(__('Accounts Receivable'), function () { - frappe.set_route('query-report', 'Accounts Receivable', {customer:frm.doc.name}); + frappe.set_route('query-report', 'Accounts Receivable', { party_type: "Customer", party: frm.doc.name }); }, __('View')); frm.add_custom_button(__('Accounting Ledger'), function () { From 6d7aa2ae942c34daf35ed617f52d58f94d9c43e5 Mon Sep 17 00:00:00 2001 From: Gursheen Anand Date: Wed, 27 Sep 2023 13:02:52 +0530 Subject: [PATCH 365/501] fix: query for multiselect filter (cherry picked from commit e7239e02d4ff7ec0338f45aa4264dc65d12d3f36) --- .../accounts/report/accounts_receivable/accounts_receivable.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/accounts/report/accounts_receivable/accounts_receivable.py b/erpnext/accounts/report/accounts_receivable/accounts_receivable.py index 794240236525..e3b671f39730 100755 --- a/erpnext/accounts/report/accounts_receivable/accounts_receivable.py +++ b/erpnext/accounts/report/accounts_receivable/accounts_receivable.py @@ -801,7 +801,7 @@ def add_common_filters(self): self.qb_selection_filter.append(self.filters.party_type == self.ple.party_type) if self.filters.get("party"): - self.qb_selection_filter.append(self.filters.party == self.ple.party) + self.qb_selection_filter.append(self.ple.party.isin(self.filters.party)) if self.filters.party_account: self.qb_selection_filter.append(self.ple.account == self.filters.party_account) From 403ff697e94d79a31dd61d6ea9f5770106a07cf2 Mon Sep 17 00:00:00 2001 From: Gursheen Anand Date: Wed, 27 Sep 2023 13:03:37 +0530 Subject: [PATCH 366/501] fix: summary report filters (cherry picked from commit f7cb68a45fade2499d51d92a5a096f0640b039f2) --- .../accounts_payable_summary.js | 38 +++++++++++------- .../accounts_receivable_summary.js | 39 ++++++++++++------- 2 files changed, 50 insertions(+), 27 deletions(-) diff --git a/erpnext/accounts/report/accounts_payable_summary/accounts_payable_summary.js b/erpnext/accounts/report/accounts_payable_summary/accounts_payable_summary.js index 8a1725c0485c..9e575e669d22 100644 --- a/erpnext/accounts/report/accounts_payable_summary/accounts_payable_summary.js +++ b/erpnext/accounts/report/accounts_payable_summary/accounts_payable_summary.js @@ -72,18 +72,11 @@ frappe.query_reports["Accounts Payable Summary"] = { } }, { - "fieldname": "party_type", + "fieldname":"party_type", "label": __("Party Type"), - "fieldtype": "Link", - "options": "Party Type", - get_query: () => { - return { - filters: { - 'account_type': 'Payable' - } - }; - }, - on_change: () => { + "fieldtype": "Autocomplete", + options: get_party_type_options(), + on_change: function() { frappe.query_report.set_filter_value('party', ""); frappe.query_report.toggle_filter_display('supplier_group', frappe.query_report.get_filter_value('party_type') !== "Supplier"); } @@ -91,8 +84,15 @@ frappe.query_reports["Accounts Payable Summary"] = { { "fieldname":"party", "label": __("Party"), - "fieldtype": "Dynamic Link", - "options": "party_type", + "fieldtype": "MultiSelectList", + get_data: function(txt) { + if (!frappe.query_report.filters) return; + + let party_type = frappe.query_report.get_filter_value('party_type'); + if (!party_type) return; + + return frappe.db.get_link_options(party_type, txt); + }, }, { "fieldname":"payment_terms_template", @@ -122,3 +122,15 @@ frappe.query_reports["Accounts Payable Summary"] = { } erpnext.utils.add_dimensions('Accounts Payable Summary', 9); + +function get_party_type_options() { + let options = []; + frappe.db.get_list( + "Party Type", {filters:{"account_type": "Payable"}, fields:['name']} + ).then((res) => { + res.forEach((party_type) => { + options.push(party_type.name); + }); + }); + return options; +} \ No newline at end of file diff --git a/erpnext/accounts/report/accounts_receivable_summary/accounts_receivable_summary.js b/erpnext/accounts/report/accounts_receivable_summary/accounts_receivable_summary.js index a78fbeb03088..5ad10c7890a0 100644 --- a/erpnext/accounts/report/accounts_receivable_summary/accounts_receivable_summary.js +++ b/erpnext/accounts/report/accounts_receivable_summary/accounts_receivable_summary.js @@ -72,19 +72,11 @@ frappe.query_reports["Accounts Receivable Summary"] = { } }, { - "fieldname": "party_type", + "fieldname":"party_type", "label": __("Party Type"), - "fieldtype": "Link", - "options": "Party Type", - "Default": "Customer", - get_query: () => { - return { - filters: { - 'account_type': 'Receivable' - } - }; - }, - on_change: () => { + "fieldtype": "Autocomplete", + options: get_party_type_options(), + on_change: function() { frappe.query_report.set_filter_value('party', ""); frappe.query_report.toggle_filter_display('customer_group', frappe.query_report.get_filter_value('party_type') !== "Customer"); } @@ -92,8 +84,15 @@ frappe.query_reports["Accounts Receivable Summary"] = { { "fieldname":"party", "label": __("Party"), - "fieldtype": "Dynamic Link", - "options": "party_type", + "fieldtype": "MultiSelectList", + get_data: function(txt) { + if (!frappe.query_report.filters) return; + + let party_type = frappe.query_report.get_filter_value('party_type'); + if (!party_type) return; + + return frappe.db.get_link_options(party_type, txt); + }, }, { "fieldname":"customer_group", @@ -151,3 +150,15 @@ frappe.query_reports["Accounts Receivable Summary"] = { } erpnext.utils.add_dimensions('Accounts Receivable Summary', 9); + +function get_party_type_options() { + let options = []; + frappe.db.get_list( + "Party Type", {filters:{"account_type": "Receivable"}, fields:['name']} + ).then((res) => { + res.forEach((party_type) => { + options.push(party_type.name); + }); + }); + return options; +} \ No newline at end of file From 4962b67358a7b31cbe0c93f186dc7fe030271cc7 Mon Sep 17 00:00:00 2001 From: Gursheen Anand Date: Wed, 27 Sep 2023 13:04:13 +0530 Subject: [PATCH 367/501] fix: process soa filter for multiselect (cherry picked from commit 4b28154f5ee724d1a2c109f5b9e5b27bc707436b) --- .../process_statement_of_accounts.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.py b/erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.py index 5055c345917e..3f8731afe651 100644 --- a/erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.py +++ b/erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.py @@ -144,7 +144,7 @@ def get_ar_filters(doc, entry): return { "report_date": doc.posting_date if doc.posting_date else None, "party_type": "Customer", - "party": entry.customer, + "party": [entry.customer], "customer_name": entry.customer_name if entry.customer_name else None, "payment_terms_template": doc.payment_terms_template if doc.payment_terms_template else None, "sales_partner": doc.sales_partner if doc.sales_partner else None, From 28756bf7b6ddd0b012235e4c0c44d583586c08a5 Mon Sep 17 00:00:00 2001 From: Gursheen Anand Date: Wed, 27 Sep 2023 13:57:49 +0530 Subject: [PATCH 368/501] fix: party format in test (cherry picked from commit 59e8abfd57d85719147129d22e4cc2293a379d71) --- .../accounts/report/accounts_payable/test_accounts_payable.py | 2 +- .../report/accounts_receivable/test_accounts_receivable.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/erpnext/accounts/report/accounts_payable/test_accounts_payable.py b/erpnext/accounts/report/accounts_payable/test_accounts_payable.py index 3cf93cc8659b..9f03d92cd508 100644 --- a/erpnext/accounts/report/accounts_payable/test_accounts_payable.py +++ b/erpnext/accounts/report/accounts_payable/test_accounts_payable.py @@ -34,7 +34,7 @@ def test_accounts_payable_for_foreign_currency_supplier(self): filters = { "company": self.company, "party_type": "Supplier", - "party": self.supplier, + "party": [self.supplier], "report_date": today(), "range1": 30, "range2": 60, diff --git a/erpnext/accounts/report/accounts_receivable/test_accounts_receivable.py b/erpnext/accounts/report/accounts_receivable/test_accounts_receivable.py index b98916ee443f..8c13f85a98ed 100644 --- a/erpnext/accounts/report/accounts_receivable/test_accounts_receivable.py +++ b/erpnext/accounts/report/accounts_receivable/test_accounts_receivable.py @@ -573,7 +573,7 @@ def test_usd_customer_filter(self): filters = { "company": self.company, "party_type": "Customer", - "party": self.customer, + "party": [self.customer], "report_date": today(), "range1": 30, "range2": 60, From 01b54134ae79b60d1599a828944c0b73ab8b860b Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Thu, 28 Sep 2023 11:16:57 +0530 Subject: [PATCH 369/501] test: multi select party filter in AR report (cherry picked from commit 2c7d6aec89327330be415b8c624c9272fb3d613b) --- .../test_accounts_receivable.py | 38 +++++++++++++++++++ 1 file changed, 38 insertions(+) diff --git a/erpnext/accounts/report/accounts_receivable/test_accounts_receivable.py b/erpnext/accounts/report/accounts_receivable/test_accounts_receivable.py index 8c13f85a98ed..4307689158f8 100644 --- a/erpnext/accounts/report/accounts_receivable/test_accounts_receivable.py +++ b/erpnext/accounts/report/accounts_receivable/test_accounts_receivable.py @@ -605,3 +605,41 @@ def test_usd_customer_filter(self): for field in expected: with self.subTest(field=field): self.assertEqual(report_output.get(field), expected.get(field)) + + def test_multi_select_party_filter(self): + self.customer1 = self.customer + self.create_customer("_Test Customer 2") + self.customer2 = self.customer + self.create_customer("_Test Customer 3") + self.customer3 = self.customer + + filters = { + "company": self.company, + "party_type": "Customer", + "party": [self.customer1, self.customer3], + "report_date": today(), + "range1": 30, + "range2": 60, + "range3": 90, + "range4": 120, + } + + si1 = self.create_sales_invoice(no_payment_schedule=True, do_not_submit=True) + si1.customer = self.customer1 + si1.save().submit() + + si2 = self.create_sales_invoice(no_payment_schedule=True, do_not_submit=True) + si2.customer = self.customer2 + si2.save().submit() + + si3 = self.create_sales_invoice(no_payment_schedule=True, do_not_submit=True) + si3.customer = self.customer3 + si3.save().submit() + + # check invoice grand total and invoiced column's value for 3 payment terms + report = execute(filters) + + expected_output = {self.customer1, self.customer3} + self.assertEqual(len(report[1]), 2) + output_for = set([x.party for x in report[1]]) + self.assertEqual(output_for, expected_output) From 21c1c7194bf5c7d24c2aec55ffac3ad3d0cdfc68 Mon Sep 17 00:00:00 2001 From: HENRY Florian Date: Fri, 29 Sep 2023 04:59:18 +0200 Subject: [PATCH 370/501] refactor: In Quotation Item, discount_and_margin section should have same collapsible_depends_on as other similar DocType (Sales Order Item,Sales Invoice Item,...) (#37272) --- erpnext/selling/doctype/quotation_item/quotation_item.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/erpnext/selling/doctype/quotation_item/quotation_item.json b/erpnext/selling/doctype/quotation_item/quotation_item.json index fb810318e930..f5e641dceb47 100644 --- a/erpnext/selling/doctype/quotation_item/quotation_item.json +++ b/erpnext/selling/doctype/quotation_item/quotation_item.json @@ -236,6 +236,7 @@ }, { "collapsible": 1, + "collapsible_depends_on": "eval: doc.margin_type || doc.discount_amount", "fieldname": "discount_and_margin", "fieldtype": "Section Break", "label": "Discount and Margin" @@ -667,7 +668,7 @@ "idx": 1, "istable": 1, "links": [], - "modified": "2023-02-06 11:00:07.042364", + "modified": "2023-09-27 14:02:12.332407", "modified_by": "Administrator", "module": "Selling", "name": "Quotation Item", From cfb3a9eabf2f77bd7bd96121c281f55fd644c535 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Fri, 29 Sep 2023 12:18:35 +0530 Subject: [PATCH 371/501] fix: incorrect qty for material request in Production Plan (backport #37270) (#37290) fix: incorrect qty for material request in Production Plan (#37270) (cherry picked from commit 8fe4a4d3aa7857e5e241bcb7145065753ab8b4cb) Co-authored-by: rohitwaghchaure --- .../production_plan/production_plan.py | 27 ++++----- .../production_plan/test_production_plan.py | 58 +++++++++++++++++++ 2 files changed, 70 insertions(+), 15 deletions(-) diff --git a/erpnext/manufacturing/doctype/production_plan/production_plan.py b/erpnext/manufacturing/doctype/production_plan/production_plan.py index fcaae79d6c5e..52bfeafea515 100644 --- a/erpnext/manufacturing/doctype/production_plan/production_plan.py +++ b/erpnext/manufacturing/doctype/production_plan/production_plan.py @@ -1508,6 +1508,10 @@ def get_items_for_material_requests(doc, warehouses=None, get_parent_warehouse_d def get_materials_from_other_locations(item, warehouses, new_mr_items, company): from erpnext.stock.doctype.pick_list.pick_list import get_available_item_locations + stock_uom, purchase_uom = frappe.db.get_value( + "Item", item.get("item_code"), ["stock_uom", "purchase_uom"] + ) + locations = get_available_item_locations( item.get("item_code"), warehouses, item.get("quantity"), company, ignore_validation=True ) @@ -1518,6 +1522,10 @@ def get_materials_from_other_locations(item, warehouses, new_mr_items, company): if required_qty <= 0: return + conversion_factor = 1.0 + if purchase_uom != stock_uom and purchase_uom == item["uom"]: + conversion_factor = get_uom_conversion_factor(item["item_code"], item["uom"]) + new_dict = copy.deepcopy(item) quantity = required_qty if d.get("qty") > required_qty else d.get("qty") @@ -1530,25 +1538,14 @@ def get_materials_from_other_locations(item, warehouses, new_mr_items, company): } ) - required_qty -= quantity + required_qty -= quantity / conversion_factor new_mr_items.append(new_dict) # raise purchase request for remaining qty - if required_qty: - stock_uom, purchase_uom = frappe.db.get_value( - "Item", item["item_code"], ["stock_uom", "purchase_uom"] - ) - - if purchase_uom != stock_uom and purchase_uom == item["uom"]: - conversion_factor = get_uom_conversion_factor(item["item_code"], item["uom"]) - if not (conversion_factor or frappe.flags.show_qty_in_stock_uom): - frappe.throw( - _("UOM Conversion factor ({0} -> {1}) not found for item: {2}").format( - purchase_uom, stock_uom, item["item_code"] - ) - ) - required_qty = required_qty / conversion_factor + precision = frappe.get_precision("Material Request Plan Item", "quantity") + if flt(required_qty, precision) > 0: + required_qty = required_qty if frappe.db.get_value("UOM", purchase_uom, "must_be_whole_number"): required_qty = ceil(required_qty) diff --git a/erpnext/manufacturing/doctype/production_plan/test_production_plan.py b/erpnext/manufacturing/doctype/production_plan/test_production_plan.py index 9ea746cbd8b9..c276f6a91b91 100644 --- a/erpnext/manufacturing/doctype/production_plan/test_production_plan.py +++ b/erpnext/manufacturing/doctype/production_plan/test_production_plan.py @@ -1219,6 +1219,64 @@ def test_skip_available_qty_for_sub_assembly_items(self): if row.item_code == "SubAssembly2 For SUB Test": self.assertEqual(row.quantity, 10) + def test_transfer_and_purchase_mrp_for_purchase_uom(self): + from erpnext.manufacturing.doctype.bom.test_bom import create_nested_bom + from erpnext.stock.doctype.warehouse.test_warehouse import create_warehouse + + bom_tree = { + "Test FG Item INK PEN": { + "Test RM Item INK": {}, + } + } + + parent_bom = create_nested_bom(bom_tree, prefix="") + if not frappe.db.exists("UOM Conversion Detail", {"parent": "Test RM Item INK", "uom": "Kg"}): + doc = frappe.get_doc("Item", "Test RM Item INK") + doc.purchase_uom = "Kg" + doc.append("uoms", {"uom": "Kg", "conversion_factor": 0.5}) + doc.save() + + wh1 = create_warehouse("PNE Warehouse", company="_Test Company") + wh2 = create_warehouse("MBE Warehouse", company="_Test Company") + mrp_warhouse = create_warehouse("MRPBE Warehouse", company="_Test Company") + + make_stock_entry( + item_code="Test RM Item INK", + qty=2, + rate=100, + target=wh1, + ) + + make_stock_entry( + item_code="Test RM Item INK", + qty=2, + rate=100, + target=wh2, + ) + + plan = create_production_plan( + item_code=parent_bom.item, + planned_qty=10, + do_not_submit=1, + warehouse="_Test Warehouse - _TC", + ) + + plan.for_warehouse = mrp_warhouse + + items = get_items_for_material_requests( + plan.as_dict(), warehouses=[{"warehouse": wh1}, {"warehouse": wh2}] + ) + + for row in items: + row = frappe._dict(row) + if row.material_request_type == "Material Transfer": + self.assertTrue(row.from_warehouse in [wh1, wh2]) + self.assertEqual(row.quantity, 2) + + if row.material_request_type == "Purchase": + self.assertTrue(row.warehouse == mrp_warhouse) + self.assertEqual(row.quantity, 12) + def create_production_plan(**args): """ From 7fd4d3c8822a3fbc724f1ac4f093124a03b0abfe Mon Sep 17 00:00:00 2001 From: Frappe PR Bot Date: Fri, 29 Sep 2023 06:50:09 +0000 Subject: [PATCH 372/501] chore(release): Bumped to Version 14.41.2 ## [14.41.2](https://github.com/frappe/erpnext/compare/v14.41.1...v14.41.2) (2023-09-29) ### Bug Fixes * incorrect qty for material request in Production Plan (backport [#37270](https://github.com/frappe/erpnext/issues/37270)) ([#37290](https://github.com/frappe/erpnext/issues/37290)) ([cfb3a9e](https://github.com/frappe/erpnext/commit/cfb3a9eabf2f77bd7bd96121c281f55fd644c535)) --- erpnext/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/__init__.py b/erpnext/__init__.py index f25a601a2ec3..7ef8bf3f03f5 100644 --- a/erpnext/__init__.py +++ b/erpnext/__init__.py @@ -3,7 +3,7 @@ import frappe -__version__ = "14.41.1" +__version__ = "14.41.2" def get_default_company(user=None): From 7f1483ad707feac6413994400217413e5c167c7b Mon Sep 17 00:00:00 2001 From: rohitwaghchaure Date: Fri, 29 Sep 2023 14:35:45 +0530 Subject: [PATCH 373/501] fix: Description field for the 'Ignore Available Stock' (#37293) --- .../doctype/production_plan/production_plan.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/erpnext/manufacturing/doctype/production_plan/production_plan.json b/erpnext/manufacturing/doctype/production_plan/production_plan.json index 0d0fd5e27060..4a0041662bc4 100644 --- a/erpnext/manufacturing/doctype/production_plan/production_plan.json +++ b/erpnext/manufacturing/doctype/production_plan/production_plan.json @@ -228,7 +228,7 @@ }, { "default": "0", - "description": "If enabled, the system won't create material requests for the available items.", + "description": "If enabled, the system will create material requests even if the stock exists in the 'Raw Materials Warehouse'.", "fieldname": "ignore_existing_ordered_qty", "fieldtype": "Check", "label": "Ignore Available Stock" @@ -422,7 +422,7 @@ "index_web_pages_for_search": 1, "is_submittable": 1, "links": [], - "modified": "2023-07-28 13:37:43.926686", + "modified": "2023-09-29 11:41:03.246059", "modified_by": "Administrator", "module": "Manufacturing", "name": "Production Plan", From 95e0bf5e0edc7886bcb56c781ec56e0b72577b8e Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Fri, 29 Sep 2023 15:07:10 +0530 Subject: [PATCH 374/501] fix: Not unique table/alias: 'tabTask' (backport #37285) (#37298) fix: Not unique table/alias: 'tabTask' (#37285) (cherry picked from commit 361e55511886d97a8b3b03a2d56a7220200c76a6) Co-authored-by: s-aga-r --- erpnext/projects/doctype/task/task.json | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/erpnext/projects/doctype/task/task.json b/erpnext/projects/doctype/task/task.json index 33a8799f96c9..3d3a463ec0dd 100644 --- a/erpnext/projects/doctype/task/task.json +++ b/erpnext/projects/doctype/task/task.json @@ -60,7 +60,6 @@ "fieldname": "subject", "fieldtype": "Data", "in_global_search": 1, - "in_list_view": 1, "in_standard_filter": 1, "label": "Subject", "reqd": 1, @@ -140,7 +139,6 @@ "fieldname": "parent_task", "fieldtype": "Link", "ignore_user_permissions": 1, - "in_list_view": 1, "label": "Parent Task", "options": "Task", "search_index": 1 @@ -398,7 +396,7 @@ "is_tree": 1, "links": [], "max_attachments": 5, - "modified": "2023-09-06 13:52:05.861175", + "modified": "2023-09-28 13:52:05.861175", "modified_by": "Administrator", "module": "Projects", "name": "Task", From 04f0dfb691fa0f9ea03518b28a7053454610f7c7 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Sun, 1 Oct 2023 21:31:17 +0530 Subject: [PATCH 375/501] fix: ignore user permissions for `Source Warehouse` (backport #37313) (#37314) * fix: ignore user permissions for `Source Warehouse` (#37313) (cherry picked from commit e7f4b7b190ab2da7fcd7579abdb0ba7bbed17b65) # Conflicts: # erpnext/accounts/doctype/purchase_invoice/purchase_invoice.json # erpnext/stock/doctype/purchase_receipt/purchase_receipt.json * chore: `conflicts` --------- Co-authored-by: s-aga-r --- .../accounts/doctype/purchase_invoice/purchase_invoice.json | 3 ++- erpnext/buying/doctype/purchase_order/purchase_order.json | 3 ++- erpnext/stock/doctype/purchase_receipt/purchase_receipt.json | 3 ++- 3 files changed, 6 insertions(+), 3 deletions(-) diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.json b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.json index 6fa71654f67f..7e848a0b4e82 100644 --- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.json +++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.json @@ -1377,6 +1377,7 @@ "depends_on": "eval:doc.is_subcontracted", "fieldname": "supplier_warehouse", "fieldtype": "Link", + "ignore_user_permissions": 1, "label": "Supplier Warehouse", "no_copy": 1, "options": "Warehouse", @@ -1574,7 +1575,7 @@ "idx": 204, "is_submittable": 1, "links": [], - "modified": "2023-07-04 17:23:59.145031", + "modified": "2023-10-01 21:01:47.282533", "modified_by": "Administrator", "module": "Accounts", "name": "Purchase Invoice", diff --git a/erpnext/buying/doctype/purchase_order/purchase_order.json b/erpnext/buying/doctype/purchase_order/purchase_order.json index bacd98bea77d..01ce8c33ff9d 100644 --- a/erpnext/buying/doctype/purchase_order/purchase_order.json +++ b/erpnext/buying/doctype/purchase_order/purchase_order.json @@ -475,6 +475,7 @@ "depends_on": "eval:doc.is_subcontracted", "fieldname": "supplier_warehouse", "fieldtype": "Link", + "ignore_user_permissions": 1, "label": "Supplier Warehouse", "options": "Warehouse" }, @@ -1272,7 +1273,7 @@ "idx": 105, "is_submittable": 1, "links": [], - "modified": "2023-09-13 16:21:07.361700", + "modified": "2023-10-01 20:58:07.851037", "modified_by": "Administrator", "module": "Buying", "name": "Purchase Order", diff --git a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.json b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.json index f33c7a65cff4..aae1bad0977d 100755 --- a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.json +++ b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.json @@ -463,6 +463,7 @@ "depends_on": "eval:doc.is_subcontracted", "fieldname": "supplier_warehouse", "fieldtype": "Link", + "ignore_user_permissions": 1, "label": "Supplier Warehouse", "no_copy": 1, "oldfieldname": "supplier_warehouse", @@ -1240,7 +1241,7 @@ "idx": 261, "is_submittable": 1, "links": [], - "modified": "2023-07-04 17:24:17.025390", + "modified": "2023-10-01 21:00:44.556816", "modified_by": "Administrator", "module": "Stock", "name": "Purchase Receipt", From f7de825e891154d1df2ae0dd5b832067f87bef35 Mon Sep 17 00:00:00 2001 From: Smit Vora Date: Tue, 19 Sep 2023 20:47:21 +0530 Subject: [PATCH 376/501] chore: add regional support for getting payment entries (#37119) chore: add regional support for get payment entries (cherry picked from commit 3e282bfbceb6bbf3cfb015f92790b534335f97da) --- .../payment_reconciliation/payment_reconciliation.py | 10 ++++++++-- erpnext/controllers/accounts_controller.py | 7 ++++++- 2 files changed, 14 insertions(+), 3 deletions(-) diff --git a/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py b/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py index 7d294e873d4a..08923e742665 100644 --- a/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py +++ b/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py @@ -19,7 +19,7 @@ get_outstanding_invoices, reconcile_against_document, ) -from erpnext.controllers.accounts_controller import get_advance_payment_entries +from erpnext.controllers.accounts_controller import get_advance_payment_entries_for_regional class PaymentReconciliation(Document): @@ -62,7 +62,7 @@ def get_payment_entries(self): if self.payment_name: condition += "name like '%%{0}%%'".format(self.payment_name) - payment_entries = get_advance_payment_entries( + payment_entries = get_advance_payment_entries_for_regional( self.party_type, self.party, self.receivable_payable_account, @@ -350,6 +350,7 @@ def get_allocated_entry(self, pay, inv, allocated_amount): ) def reconcile_allocations(self, skip_ref_details_update_for_pe=False): + adjust_allocations_for_taxes(self) dr_or_cr = ( "credit_in_account_currency" if erpnext.get_party_account_type(self.party_type) == "Receivable" @@ -650,3 +651,8 @@ def reconcile_dr_cr_note(dr_cr_notes, company): None, inv.cost_center, ) + + +@erpnext.allow_regional +def adjust_allocations_for_taxes(doc): + pass diff --git a/erpnext/controllers/accounts_controller.py b/erpnext/controllers/accounts_controller.py index c7e21638bc75..257223234a87 100644 --- a/erpnext/controllers/accounts_controller.py +++ b/erpnext/controllers/accounts_controller.py @@ -909,7 +909,7 @@ def get_advance_entries(self, include_unallocated=True): party_type, party, party_account, amount_field, order_doctype, order_list, include_unallocated ) - payment_entries = get_advance_payment_entries( + payment_entries = get_advance_payment_entries_for_regional( party_type, party, party_account, order_doctype, order_list, include_unallocated ) @@ -2349,6 +2349,11 @@ def get_advance_journal_entries( return list(journal_entries) +@erpnext.allow_regional +def get_advance_payment_entries_for_regional(*args, **kwargs): + return get_advance_payment_entries(*args, **kwargs) + + def get_advance_payment_entries( party_type, party, From 6daea6ccb2355df957ba73cff917f15d6db4eba7 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Tue, 3 Oct 2023 13:35:10 +0530 Subject: [PATCH 377/501] feat: asset salvage_value_percentage (backport #37302) (#37334) * feat: asset salvage_value_percentage (#37302) * feat: asset salvage_value_percentage * chore: add missing parameter in get_item_details * chore: change asset depr table colors (cherry picked from commit fed94845ceca1a755cb997ec4a1f069b2f2a0534) # Conflicts: # erpnext/assets/doctype/asset/asset.js # erpnext/assets/doctype/asset_activity/asset_activity.json # erpnext/assets/doctype/asset_finance_book/asset_finance_book.json * chore: resolving conflicts --------- Co-authored-by: Anand Baburajan --- erpnext/assets/doctype/asset/asset.js | 29 +++++++++++++++++-- erpnext/assets/doctype/asset/asset.py | 12 ++++++-- .../asset_finance_book.json | 8 ++++- 3 files changed, 43 insertions(+), 6 deletions(-) diff --git a/erpnext/assets/doctype/asset/asset.js b/erpnext/assets/doctype/asset/asset.js index 0923d0093f97..5c1da91fdf99 100644 --- a/erpnext/assets/doctype/asset/asset.js +++ b/erpnext/assets/doctype/asset/asset.js @@ -287,7 +287,8 @@ frappe.ui.form.on('Asset', { method: "erpnext.assets.doctype.asset.asset.get_item_details", args: { item_code: frm.doc.item_code, - asset_category: frm.doc.asset_category + asset_category: frm.doc.asset_category, + gross_purchase_amount: frm.doc.gross_purchase_amount }, callback: function(r, rt) { if(r.message) { @@ -504,7 +505,21 @@ frappe.ui.form.on('Asset', { } }); } - } + }, + + set_salvage_value_percentage_or_expected_value_after_useful_life: function(frm, row, salvage_value_percentage_changed, expected_value_after_useful_life_changed) { + if (expected_value_after_useful_life_changed) { + frappe.flags.from_set_salvage_value_percentage_or_expected_value_after_useful_life = true; + const new_salvage_value_percentage = flt((row.expected_value_after_useful_life * 100) / frm.doc.gross_purchase_amount, precision("salvage_value_percentage", row)); + frappe.model.set_value(row.doctype, row.name, "salvage_value_percentage", new_salvage_value_percentage); + frappe.flags.from_set_salvage_value_percentage_or_expected_value_after_useful_life = false; + } else if (salvage_value_percentage_changed) { + frappe.flags.from_set_salvage_value_percentage_or_expected_value_after_useful_life = true; + const new_expected_value_after_useful_life = flt(frm.doc.gross_purchase_amount * (row.salvage_value_percentage / 100), precision('gross_purchase_amount')); + frappe.model.set_value(row.doctype, row.name, "expected_value_after_useful_life", new_expected_value_after_useful_life); + frappe.flags.from_set_salvage_value_percentage_or_expected_value_after_useful_life = false; + } + }, }); frappe.ui.form.on('Asset Finance Book', { @@ -516,9 +531,19 @@ frappe.ui.form.on('Asset Finance Book', { expected_value_after_useful_life: function(frm, cdt, cdn) { const row = locals[cdt][cdn]; + if (!frappe.flags.from_set_salvage_value_percentage_or_expected_value_after_useful_life) { + frm.events.set_salvage_value_percentage_or_expected_value_after_useful_life(frm, row, false, true); + } frm.events.set_depreciation_rate(frm, row); }, + salvage_value_percentage: function(frm, cdt, cdn) { + const row = locals[cdt][cdn]; + if (!frappe.flags.from_set_salvage_value_percentage_or_expected_value_after_useful_life) { + frm.events.set_salvage_value_percentage_or_expected_value_after_useful_life(frm, row, true, false); + } + }, + frequency_of_depreciation: function(frm, cdt, cdn) { const row = locals[cdt][cdn]; frm.events.set_depreciation_rate(frm, row); diff --git a/erpnext/assets/doctype/asset/asset.py b/erpnext/assets/doctype/asset/asset.py index e72ddcb12a00..81a35ad8f93b 100644 --- a/erpnext/assets/doctype/asset/asset.py +++ b/erpnext/assets/doctype/asset/asset.py @@ -204,7 +204,9 @@ def set_missing_values(self): self.asset_category = frappe.get_cached_value("Item", self.item_code, "asset_category") if self.item_code and not self.get("finance_books"): - finance_books = get_item_details(self.item_code, self.asset_category) + finance_books = get_item_details( + self.item_code, self.asset_category, self.gross_purchase_amount + ) self.set("finance_books", finance_books) def validate_finance_books(self): @@ -1195,7 +1197,7 @@ def transfer_asset(args): @frappe.whitelist() -def get_item_details(item_code, asset_category): +def get_item_details(item_code, asset_category, gross_purchase_amount): asset_category_doc = frappe.get_doc("Asset Category", asset_category) books = [] for d in asset_category_doc.finance_books: @@ -1205,7 +1207,11 @@ def get_item_details(item_code, asset_category): "depreciation_method": d.depreciation_method, "total_number_of_depreciations": d.total_number_of_depreciations, "frequency_of_depreciation": d.frequency_of_depreciation, - "start_date": nowdate(), + "daily_depreciation": d.daily_depreciation, + "salvage_value_percentage": d.salvage_value_percentage, + "expected_value_after_useful_life": flt(gross_purchase_amount) + * flt(d.salvage_value_percentage / 100), + "depreciation_start_date": d.depreciation_start_date or nowdate(), } ) diff --git a/erpnext/assets/doctype/asset_finance_book/asset_finance_book.json b/erpnext/assets/doctype/asset_finance_book/asset_finance_book.json index 1f80e3a67bda..ea1a81128437 100644 --- a/erpnext/assets/doctype/asset_finance_book/asset_finance_book.json +++ b/erpnext/assets/doctype/asset_finance_book/asset_finance_book.json @@ -12,6 +12,7 @@ "column_break_5", "frequency_of_depreciation", "depreciation_start_date", + "salvage_value_percentage", "expected_value_after_useful_life", "value_after_depreciation", "rate_of_depreciation" @@ -87,12 +88,17 @@ "fieldname": "daily_depreciation", "fieldtype": "Check", "label": "Daily Depreciation" + }, + { + "fieldname": "salvage_value_percentage", + "fieldtype": "Percent", + "label": "Salvage Value Percentage" } ], "index_web_pages_for_search": 1, "istable": 1, "links": [], - "modified": "2023-08-10 18:56:09.022246", + "modified": "2023-09-29 15:39:52.740594", "modified_by": "Administrator", "module": "Assets", "name": "Asset Finance Book", From 82e8606b3c4403d79556c896a98f942762cd8fa7 Mon Sep 17 00:00:00 2001 From: rohitwaghchaure Date: Tue, 3 Oct 2023 18:53:43 +0530 Subject: [PATCH 378/501] fix: currency symbol in the Supplier Quotation Comparison report (#37337) fix: currency in the Supplier Quotation Comparison report --- .../supplier_quotation_comparison.py | 35 +++++++++++++++++-- 1 file changed, 33 insertions(+), 2 deletions(-) diff --git a/erpnext/buying/report/supplier_quotation_comparison/supplier_quotation_comparison.py b/erpnext/buying/report/supplier_quotation_comparison/supplier_quotation_comparison.py index a72829096181..01ff28d8103d 100644 --- a/erpnext/buying/report/supplier_quotation_comparison/supplier_quotation_comparison.py +++ b/erpnext/buying/report/supplier_quotation_comparison/supplier_quotation_comparison.py @@ -35,8 +35,12 @@ def get_data(filters): sq_item.parent, sq_item.item_code, sq_item.qty, + sq.currency, sq_item.stock_qty, sq_item.amount, + sq_item.base_rate, + sq_item.base_amount, + sq.price_list_currency, sq_item.uom, sq_item.stock_uom, sq_item.request_for_quotation, @@ -105,7 +109,11 @@ def prepare_data(supplier_quotation_data, filters): "qty": data.get("qty"), "price": flt(data.get("amount") * exchange_rate, float_precision), "uom": data.get("uom"), + "price_list_currency": data.get("price_list_currency"), + "currency": data.get("currency"), "stock_uom": data.get("stock_uom"), + "base_amount": flt(data.get("base_amount"), float_precision), + "base_rate": flt(data.get("base_rate"), float_precision), "request_for_quotation": data.get("request_for_quotation"), "valid_till": data.get("valid_till"), "lead_time_days": data.get("lead_time_days"), @@ -183,6 +191,8 @@ def prepare_chart_data(suppliers, qty_list, supplier_qty_price_map): def get_columns(filters): + currency = frappe.get_cached_value("Company", filters.get("company"), "default_currency") + group_by_columns = [ { "fieldname": "supplier_name", @@ -203,11 +213,18 @@ def get_columns(filters): columns = [ {"fieldname": "uom", "label": _("UOM"), "fieldtype": "Link", "options": "UOM", "width": 90}, {"fieldname": "qty", "label": _("Quantity"), "fieldtype": "Float", "width": 80}, + { + "fieldname": "currency", + "label": _("Currency"), + "fieldtype": "Link", + "options": "Currency", + "width": 110, + }, { "fieldname": "price", "label": _("Price"), "fieldtype": "Currency", - "options": "Company:company:default_currency", + "options": "currency", "width": 110, }, { @@ -221,9 +238,23 @@ def get_columns(filters): "fieldname": "price_per_unit", "label": _("Price per Unit (Stock UOM)"), "fieldtype": "Currency", - "options": "Company:company:default_currency", + "options": "currency", "width": 120, }, + { + "fieldname": "base_amount", + "label": _("Price ({0})").format(currency), + "fieldtype": "Currency", + "options": "price_list_currency", + "width": 180, + }, + { + "fieldname": "base_rate", + "label": _("Price Per Unit ({0})").format(currency), + "fieldtype": "Currency", + "options": "price_list_currency", + "width": 180, + }, { "fieldname": "quotation", "label": _("Supplier Quotation"), From 020aedb8b010a5503e8f57f7e690aaf0b3f07266 Mon Sep 17 00:00:00 2001 From: Gursheen Anand Date: Fri, 29 Sep 2023 18:20:02 +0530 Subject: [PATCH 379/501] fix: add only float row values for total (cherry picked from commit 1dab195560021347c2edfae4ffcedc93be647868) --- .../accounts_receivable_summary.py | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/erpnext/accounts/report/accounts_receivable_summary/accounts_receivable_summary.py b/erpnext/accounts/report/accounts_receivable_summary/accounts_receivable_summary.py index cffc87895ef8..f89841523a12 100644 --- a/erpnext/accounts/report/accounts_receivable_summary/accounts_receivable_summary.py +++ b/erpnext/accounts/report/accounts_receivable_summary/accounts_receivable_summary.py @@ -99,13 +99,11 @@ def get_party_total(self, args): # Add all amount columns for k in list(self.party_total[d.party]): - if k not in ["currency", "sales_person"]: - - self.party_total[d.party][k] += d.get(k, 0.0) + if type(self.party_total[d.party][k]) == float: + self.party_total[d.party][k] += d.get(k) or 0.0 # set territory, customer_group, sales person etc self.set_party_details(d) - self.party_total[d.party].update({"party_type": d.party_type}) def init_party_total(self, row): self.party_total.setdefault( @@ -124,6 +122,7 @@ def init_party_total(self, row): "total_due": 0.0, "future_amount": 0.0, "sales_person": [], + "party_type": row.party_type, } ), ) @@ -133,13 +132,12 @@ def set_party_details(self, row): for key in ("territory", "customer_group", "supplier_group"): if row.get(key): - self.party_total[row.party][key] = row.get(key) - + self.party_total[row.party][key] = row.get(key, "") if row.sales_person: - self.party_total[row.party].sales_person.append(row.sales_person) + self.party_total[row.party].sales_person.append(row.get("sales_person", "")) if self.filters.sales_partner: - self.party_total[row.party]["default_sales_partner"] = row.get("default_sales_partner") + self.party_total[row.party]["default_sales_partner"] = row.get("default_sales_partner", "") def get_columns(self): self.columns = [] From f3b872a8e2081e2c51a015ce0fe87e328f0306bd Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Tue, 3 Oct 2023 17:38:43 +0530 Subject: [PATCH 380/501] refactor: use `isinstance` over `type` (cherry picked from commit 67440c38aef1f94432c0ec97f9bef065363d3a1b) --- .../accounts_receivable_summary/accounts_receivable_summary.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/accounts/report/accounts_receivable_summary/accounts_receivable_summary.py b/erpnext/accounts/report/accounts_receivable_summary/accounts_receivable_summary.py index f89841523a12..60274cd8b108 100644 --- a/erpnext/accounts/report/accounts_receivable_summary/accounts_receivable_summary.py +++ b/erpnext/accounts/report/accounts_receivable_summary/accounts_receivable_summary.py @@ -99,7 +99,7 @@ def get_party_total(self, args): # Add all amount columns for k in list(self.party_total[d.party]): - if type(self.party_total[d.party][k]) == float: + if isinstance(self.party_total[d.party][k], float): self.party_total[d.party][k] += d.get(k) or 0.0 # set territory, customer_group, sales person etc From c3aeb2dec58190d16a18e2609fd57054bda54e43 Mon Sep 17 00:00:00 2001 From: rohitwaghchaure Date: Tue, 3 Oct 2023 20:34:10 +0530 Subject: [PATCH 381/501] =?UTF-8?q?fix:=20do=20not=20consider=20submitted?= =?UTF-8?q?=20Work=20Orders=20in=20the=20Production=20Plan=20Res=E2=80=A6?= =?UTF-8?q?=20(#37343)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit fix: do not consider submitted Work Orders in the Production Plan Reserve qty --- .../production_plan/production_plan.py | 43 ++++++++++++++++-- .../production_plan/test_production_plan.py | 45 +++++++++++++++++++ .../doctype/work_order/work_order.py | 10 ++++- 3 files changed, 93 insertions(+), 5 deletions(-) diff --git a/erpnext/manufacturing/doctype/production_plan/production_plan.py b/erpnext/manufacturing/doctype/production_plan/production_plan.py index 52bfeafea515..f1817e3305bb 100644 --- a/erpnext/manufacturing/doctype/production_plan/production_plan.py +++ b/erpnext/manufacturing/doctype/production_plan/production_plan.py @@ -8,6 +8,7 @@ import frappe from frappe import _, msgprint from frappe.model.document import Document +from frappe.query_builder import Case from frappe.query_builder.functions import IfNull, Sum from frappe.utils import ( add_days, @@ -1616,18 +1617,33 @@ def get_reserved_qty_for_production_plan(item_code, warehouse): table = frappe.qb.DocType("Production Plan") child = frappe.qb.DocType("Material Request Plan Item") + completed_production_plans = get_completed_production_plans() + + case = Case() query = ( frappe.qb.from_(table) .inner_join(child) .on(table.name == child.parent) - .select(Sum(child.quantity * IfNull(child.conversion_factor, 1.0))) + .select( + Sum( + child.quantity + * IfNull( + case.when(child.material_request_type == "Purchase", child.conversion_factor).else_(1.0), 1.0 + ) + ) + ) .where( (table.docstatus == 1) & (child.item_code == item_code) & (child.warehouse == warehouse) & (table.status.notin(["Completed", "Closed"])) ) - ).run() + ) + + if completed_production_plans: + query = query.where(table.name.notin(completed_production_plans)) + + query = query.run() if not query: return 0.0 @@ -1635,7 +1651,9 @@ def get_reserved_qty_for_production_plan(item_code, warehouse): reserved_qty_for_production_plan = flt(query[0][0]) reserved_qty_for_production = flt( - get_reserved_qty_for_production(item_code, warehouse, check_production_plan=True) + get_reserved_qty_for_production( + item_code, warehouse, completed_production_plans, check_production_plan=True + ) ) if reserved_qty_for_production > reserved_qty_for_production_plan: @@ -1644,6 +1662,25 @@ def get_reserved_qty_for_production_plan(item_code, warehouse): return reserved_qty_for_production_plan - reserved_qty_for_production +def get_completed_production_plans(): + table = frappe.qb.DocType("Production Plan") + child = frappe.qb.DocType("Production Plan Item") + + query = ( + frappe.qb.from_(table) + .inner_join(child) + .on(table.name == child.parent) + .select(table.name) + .where( + (table.docstatus == 1) + & (table.status.notin(["Completed", "Closed"])) + & (child.ordered_qty >= child.planned_qty) + ) + ).run(as_dict=True) + + return list(set([d.name for d in query])) + + def get_raw_materials_of_sub_assembly_items( item_details, company, bom_no, include_non_stock_items, sub_assembly_items, planned_qty=1 ): diff --git a/erpnext/manufacturing/doctype/production_plan/test_production_plan.py b/erpnext/manufacturing/doctype/production_plan/test_production_plan.py index c276f6a91b91..7014c067e2b4 100644 --- a/erpnext/manufacturing/doctype/production_plan/test_production_plan.py +++ b/erpnext/manufacturing/doctype/production_plan/test_production_plan.py @@ -6,6 +6,7 @@ from erpnext.controllers.item_variant import create_variant from erpnext.manufacturing.doctype.production_plan.production_plan import ( + get_completed_production_plans, get_items_for_material_requests, get_sales_orders, get_warehouse_list, @@ -1092,6 +1093,50 @@ def test_resered_qty_for_production_plan_for_work_order(self): self.assertEqual(after_qty, before_qty) + def test_resered_qty_for_production_plan_for_less_rm_qty(self): + from erpnext.stock.utils import get_or_make_bin + + bin_name = get_or_make_bin("Raw Material Item 1", "_Test Warehouse - _TC") + before_qty = flt(frappe.db.get_value("Bin", bin_name, "reserved_qty_for_production_plan")) + + pln = create_production_plan(item_code="Test Production Item 1", planned_qty=10) + + bin_name = get_or_make_bin("Raw Material Item 1", "_Test Warehouse - _TC") + after_qty = flt(frappe.db.get_value("Bin", bin_name, "reserved_qty_for_production_plan")) + + self.assertEqual(after_qty - before_qty, 10) + + pln.make_work_order() + + plans = [] + for row in frappe.get_all("Work Order", filters={"production_plan": pln.name}, fields=["name"]): + wo_doc = frappe.get_doc("Work Order", row.name) + wo_doc.source_warehouse = "_Test Warehouse - _TC" + wo_doc.wip_warehouse = "_Test Warehouse 1 - _TC" + wo_doc.fg_warehouse = "_Test Warehouse - _TC" + for d in wo_doc.required_items: + d.source_warehouse = "_Test Warehouse - _TC" + print(d.required_qty, "before") + d.required_qty -= 5 + make_stock_entry( + item_code=d.item_code, + qty=d.required_qty, + rate=100, + target="_Test Warehouse - _TC", + ) + + wo_doc.submit() + plans.append(pln.name) + + bin_name = get_or_make_bin("Raw Material Item 1", "_Test Warehouse - _TC") + after_qty = flt(frappe.db.get_value("Bin", bin_name, "reserved_qty_for_production_plan")) + + self.assertEqual(after_qty, before_qty) + + completed_plans = get_completed_production_plans() + for plan in plans: + self.assertTrue(plan in completed_plans) + def test_resered_qty_for_production_plan_for_material_requests_with_multi_UOM(self): from erpnext.stock.utils import get_or_make_bin diff --git a/erpnext/manufacturing/doctype/work_order/work_order.py b/erpnext/manufacturing/doctype/work_order/work_order.py index 070aaf00459a..a491501d4b7a 100644 --- a/erpnext/manufacturing/doctype/work_order/work_order.py +++ b/erpnext/manufacturing/doctype/work_order/work_order.py @@ -362,10 +362,10 @@ def on_submit(self): else: self.update_work_order_qty_in_so() + self.update_ordered_qty() self.update_reserved_qty_for_production() self.update_completed_qty_in_material_request() self.update_planned_qty() - self.update_ordered_qty() self.create_job_card() def on_cancel(self): @@ -1491,7 +1491,10 @@ def update_item_quantity(source, target, source_parent): def get_reserved_qty_for_production( - item_code: str, warehouse: str, check_production_plan: bool = False + item_code: str, + warehouse: str, + completed_production_plans: list = None, + check_production_plan: bool = False, ) -> float: """Get total reserved quantity for any item in specified warehouse""" wo = frappe.qb.DocType("Work Order") @@ -1524,6 +1527,9 @@ def get_reserved_qty_for_production( if check_production_plan: query = query.where(wo.production_plan.isnotnull()) + if completed_production_plans: + query = query.where(wo.production_plan.notin(completed_production_plans)) + return query.run()[0][0] or 0.0 From e975a10a751cb1eb190ea7e06cc1eccca80554d2 Mon Sep 17 00:00:00 2001 From: rohitwaghchaure Date: Tue, 3 Oct 2023 22:08:37 +0530 Subject: [PATCH 382/501] chore: fix linter issue (#37349) --- .../doctype/production_plan/test_production_plan.py | 1 - 1 file changed, 1 deletion(-) diff --git a/erpnext/manufacturing/doctype/production_plan/test_production_plan.py b/erpnext/manufacturing/doctype/production_plan/test_production_plan.py index 7014c067e2b4..55122f7069c8 100644 --- a/erpnext/manufacturing/doctype/production_plan/test_production_plan.py +++ b/erpnext/manufacturing/doctype/production_plan/test_production_plan.py @@ -1116,7 +1116,6 @@ def test_resered_qty_for_production_plan_for_less_rm_qty(self): wo_doc.fg_warehouse = "_Test Warehouse - _TC" for d in wo_doc.required_items: d.source_warehouse = "_Test Warehouse - _TC" - print(d.required_qty, "before") d.required_qty -= 5 make_stock_entry( item_code=d.item_code, From 643bb0511ce6b858d84613d08b551b7f15c6364a Mon Sep 17 00:00:00 2001 From: rohitwaghchaure Date: Tue, 3 Oct 2023 22:56:33 +0530 Subject: [PATCH 383/501] fix: validation message for valuation rate (#37301) --- erpnext/controllers/selling_controller.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/erpnext/controllers/selling_controller.py b/erpnext/controllers/selling_controller.py index 8948b7e3f7c3..5daaba8e8925 100644 --- a/erpnext/controllers/selling_controller.py +++ b/erpnext/controllers/selling_controller.py @@ -282,7 +282,9 @@ def throw_message(idx, item_name, rate, ref_rate_field): last_valuation_rate_in_sales_uom = last_valuation_rate * (item.conversion_factor or 1) if flt(item.base_net_rate) < flt(last_valuation_rate_in_sales_uom): - throw_message(item.idx, item.item_name, last_valuation_rate_in_sales_uom, "valuation rate") + throw_message( + item.idx, item.item_name, last_valuation_rate_in_sales_uom, "valuation rate (Moving Average)" + ) def get_item_list(self): il = [] From 9f1b9320e9b56eb15509917f3f49da4d1274b285 Mon Sep 17 00:00:00 2001 From: Frappe PR Bot Date: Wed, 4 Oct 2023 02:15:16 +0000 Subject: [PATCH 384/501] chore(release): Bumped to Version 14.42.0 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit # [14.42.0](https://github.com/frappe/erpnext/compare/v14.41.2...v14.42.0) (2023-10-04) ### Bug Fixes * add only float row values for total ([020aedb](https://github.com/frappe/erpnext/commit/020aedb8b010a5503e8f57f7e690aaf0b3f07266)) * currency symbol in the Supplier Quotation Comparison report ([#37337](https://github.com/frappe/erpnext/issues/37337)) ([82e8606](https://github.com/frappe/erpnext/commit/82e8606b3c4403d79556c896a98f942762cd8fa7)) * Description field for the 'Ignore Available Stock' ([#37293](https://github.com/frappe/erpnext/issues/37293)) ([7f1483a](https://github.com/frappe/erpnext/commit/7f1483ad707feac6413994400217413e5c167c7b)) * do not consider submitted Work Orders in the Production Plan Res… ([#37343](https://github.com/frappe/erpnext/issues/37343)) ([c3aeb2d](https://github.com/frappe/erpnext/commit/c3aeb2dec58190d16a18e2609fd57054bda54e43)) * ignore user permissions for `Source Warehouse` (backport [#37313](https://github.com/frappe/erpnext/issues/37313)) ([#37314](https://github.com/frappe/erpnext/issues/37314)) ([04f0dfb](https://github.com/frappe/erpnext/commit/04f0dfb691fa0f9ea03518b28a7053454610f7c7)) * incorrect qty for material request in Production Plan ([#37270](https://github.com/frappe/erpnext/issues/37270)) ([8fe4a4d](https://github.com/frappe/erpnext/commit/8fe4a4d3aa7857e5e241bcb7145065753ab8b4cb)) * Not unique table/alias: 'tabTask' (backport [#37285](https://github.com/frappe/erpnext/issues/37285)) ([#37298](https://github.com/frappe/erpnext/issues/37298)) ([95e0bf5](https://github.com/frappe/erpnext/commit/95e0bf5e0edc7886bcb56c781ec56e0b72577b8e)) * party format in test ([28756bf](https://github.com/frappe/erpnext/commit/28756bf7b6ddd0b012235e4c0c44d583586c08a5)) * PCV posting issues ([#37029](https://github.com/frappe/erpnext/issues/37029)) ([92eabe3](https://github.com/frappe/erpnext/commit/92eabe3cf50799c6709c44197e138738560aac14)) * process soa filter for multiselect ([4962b67](https://github.com/frappe/erpnext/commit/4962b67358a7b31cbe0c93f186dc7fe030271cc7)) * query for multiselect filter ([6d7aa2a](https://github.com/frappe/erpnext/commit/6d7aa2ae942c34daf35ed617f52d58f94d9c43e5)) * set route filter values for AP ([49f0f1c](https://github.com/frappe/erpnext/commit/49f0f1ca09c6182d3e55a94733f1acc4ad5a1342)) * set route filter values for AR ([2b30727](https://github.com/frappe/erpnext/commit/2b30727fdcc447d8e96b4d735feb230f2956d04c)) * summary report filters ([403ff69](https://github.com/frappe/erpnext/commit/403ff697e94d79a31dd61d6ea9f5770106a07cf2)) * trial balance report freezes when adding filters (backport [#37264](https://github.com/frappe/erpnext/issues/37264)) ([#37265](https://github.com/frappe/erpnext/issues/37265)) ([6a8146b](https://github.com/frappe/erpnext/commit/6a8146ba8a2ef34da586adca3a3c27b12821deec)) * Use default Cost Center of the Company for additional discount ([#37234](https://github.com/frappe/erpnext/issues/37234)) ([e483b4a](https://github.com/frappe/erpnext/commit/e483b4a78a260629e994eb2e0a36a59b96919288)) * validation message for valuation rate ([#37301](https://github.com/frappe/erpnext/issues/37301)) ([643bb05](https://github.com/frappe/erpnext/commit/643bb0511ce6b858d84613d08b551b7f15c6364a)) ### Features * asset salvage_value_percentage (backport [#37302](https://github.com/frappe/erpnext/issues/37302)) ([#37334](https://github.com/frappe/erpnext/issues/37334)) ([6daea6c](https://github.com/frappe/erpnext/commit/6daea6ccb2355df957ba73cff917f15d6db4eba7)) --- erpnext/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/__init__.py b/erpnext/__init__.py index 7ef8bf3f03f5..eb7d50ff1985 100644 --- a/erpnext/__init__.py +++ b/erpnext/__init__.py @@ -3,7 +3,7 @@ import frappe -__version__ = "14.41.2" +__version__ = "14.42.0" def get_default_company(user=None): From 0ecd7d2bf5eb522315776bae5633486d89791e00 Mon Sep 17 00:00:00 2001 From: Anand Baburajan Date: Wed, 4 Oct 2023 10:29:14 +0530 Subject: [PATCH 385/501] feat: composite WIP asset (#37352) feat: wip composite asset --- .../purchase_invoice/purchase_invoice.js | 6 + .../purchase_invoice_item.json | 11 +- erpnext/assets/doctype/asset/asset.js | 44 ++++- erpnext/assets/doctype/asset/asset.json | 29 +++- erpnext/assets/doctype/asset/asset.py | 11 +- erpnext/assets/doctype/asset/test_asset.py | 1 + .../asset_capitalization.js | 72 ++++++-- .../asset_capitalization.json | 36 ++-- .../asset_capitalization.py | 159 ++++++++++++++++-- .../test_asset_capitalization.py | 74 ++++++++ .../purchase_receipt/purchase_receipt.js | 6 + .../purchase_receipt_item.json | 11 +- 12 files changed, 406 insertions(+), 54 deletions(-) diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js index 5c82cf99438f..ceb8204bd5da 100644 --- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js +++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js @@ -460,6 +460,12 @@ cur_frm.set_query("expense_account", "items", function(doc) { } }); +cur_frm.set_query("wip_composite_asset", "items", function() { + return { + filters: {'is_composite_asset': 1, 'docstatus': 0 } + } +}); + cur_frm.cscript.expense_account = function(doc, cdt, cdn){ var d = locals[cdt][cdn]; if(d.idx == 1 && d.expense_account){ diff --git a/erpnext/accounts/doctype/purchase_invoice_item/purchase_invoice_item.json b/erpnext/accounts/doctype/purchase_invoice_item/purchase_invoice_item.json index 92db6b61bb97..04f28beda9ef 100644 --- a/erpnext/accounts/doctype/purchase_invoice_item/purchase_invoice_item.json +++ b/erpnext/accounts/doctype/purchase_invoice_item/purchase_invoice_item.json @@ -75,6 +75,7 @@ "manufacturer_part_no", "accounting", "expense_account", + "wip_composite_asset", "col_break5", "is_fixed_asset", "asset_location", @@ -877,12 +878,18 @@ "fieldname": "apply_tds", "fieldtype": "Check", "label": "Apply TDS" + }, + { + "fieldname": "wip_composite_asset", + "fieldtype": "Link", + "label": "WIP Composite Asset", + "options": "Asset" } ], "idx": 1, "istable": 1, "links": [], - "modified": "2023-07-04 17:22:21.501152", + "modified": "2023-10-03 21:01:01.824892", "modified_by": "Administrator", "module": "Accounts", "name": "Purchase Invoice Item", @@ -892,4 +899,4 @@ "sort_field": "modified", "sort_order": "DESC", "states": [] -} +} \ No newline at end of file diff --git a/erpnext/assets/doctype/asset/asset.js b/erpnext/assets/doctype/asset/asset.js index 5c1da91fdf99..0605189fec05 100644 --- a/erpnext/assets/doctype/asset/asset.js +++ b/erpnext/assets/doctype/asset/asset.js @@ -147,6 +147,15 @@ frappe.ui.form.on('Asset', { if (frm.doc.docstatus == 0) { frm.toggle_reqd("finance_books", frm.doc.calculate_depreciation); + + if (frm.doc.is_composite_asset && !frm.doc.capitalized_in) { + $('.primary-action').prop('hidden', true); + $('.form-message').text('Capitalize this asset to confirm'); + + frm.add_custom_button(__("Capitalize Asset"), function() { + frm.trigger("create_asset_capitalization"); + }); + } } }, @@ -168,7 +177,7 @@ frappe.ui.form.on('Asset', { frm.set_df_property('purchase_invoice', 'read_only', 1); frm.set_df_property('purchase_receipt', 'read_only', 1); } - else if (frm.doc.is_existing_asset) { + else if (frm.doc.is_existing_asset || frm.doc.is_composite_asset) { frm.toggle_reqd('purchase_receipt', 0); frm.toggle_reqd('purchase_invoice', 0); } @@ -300,7 +309,17 @@ frappe.ui.form.on('Asset', { is_existing_asset: function(frm) { frm.trigger("toggle_reference_doc"); - // frm.toggle_reqd("next_depreciation_date", (!frm.doc.is_existing_asset && frm.doc.calculate_depreciation)); + }, + + is_composite_asset: function(frm) { + if(frm.doc.is_composite_asset) { + frm.set_value('gross_purchase_amount', 0); + frm.set_df_property('gross_purchase_amount', 'read_only', 1); + } else { + frm.set_df_property('gross_purchase_amount', 'read_only', 0); + } + + frm.trigger("toggle_reference_doc"); }, make_schedules_editable: function(frm) { @@ -361,6 +380,19 @@ frappe.ui.form.on('Asset', { }); }, + create_asset_capitalization: function(frm) { + frappe.call({ + args: { + "asset": frm.doc.name, + }, + method: "erpnext.assets.doctype.asset.asset.create_asset_capitalization", + callback: function(r) { + var doclist = frappe.model.sync(r.message); + frappe.set_route("Form", doclist[0].doctype, doclist[0].name); + } + }); + }, + split_asset: function(frm) { const title = __('Split Asset'); @@ -424,9 +456,11 @@ frappe.ui.form.on('Asset', { }, gross_purchase_amount: function(frm) { - frm.doc.finance_books.forEach(d => { - frm.events.set_depreciation_rate(frm, d); - }) + if (frm.doc.finance_books) { + frm.doc.finance_books.forEach(d => { + frm.events.set_depreciation_rate(frm, d); + }) + } }, purchase_receipt: (frm) => { diff --git a/erpnext/assets/doctype/asset/asset.json b/erpnext/assets/doctype/asset/asset.json index 060d991945b2..1da3edcc60eb 100644 --- a/erpnext/assets/doctype/asset/asset.json +++ b/erpnext/assets/doctype/asset/asset.json @@ -14,6 +14,7 @@ "asset_owner", "asset_owner_company", "is_existing_asset", + "is_composite_asset", "supplier", "customer", "image", @@ -72,7 +73,8 @@ "purchase_receipt_amount", "default_finance_book", "depr_entry_posting_status", - "amended_from" + "amended_from", + "capitalized_in" ], "fields": [ { @@ -199,7 +201,7 @@ "fieldtype": "Date", "label": "Purchase Date", "read_only": 1, - "read_only_depends_on": "eval:!doc.is_existing_asset", + "read_only_depends_on": "eval:!doc.is_existing_asset && !doc.is_composite_asset", "reqd": 1 }, { @@ -237,10 +239,12 @@ "default": "0", "fieldname": "calculate_depreciation", "fieldtype": "Check", - "label": "Calculate Depreciation" + "label": "Calculate Depreciation", + "read_only_depends_on": "eval:doc.is_composite_asset && !doc.gross_purchase_amount" }, { "default": "0", + "depends_on": "eval:!doc.is_composite_asset", "fieldname": "is_existing_asset", "fieldtype": "Check", "label": "Is Existing Asset" @@ -492,7 +496,7 @@ "fieldname": "asset_quantity", "fieldtype": "Int", "label": "Asset Quantity", - "read_only_depends_on": "eval:!doc.is_existing_asset" + "read_only_depends_on": "eval:!doc.is_existing_asset && !doc.is_composite_asset" }, { "fieldname": "depr_entry_posting_status", @@ -510,6 +514,21 @@ "fieldname": "is_fully_depreciated", "fieldtype": "Check", "label": "Is Fully Depreciated" + }, + { + "default": "0", + "depends_on": "eval:!doc.is_existing_asset", + "fieldname": "is_composite_asset", + "fieldtype": "Check", + "label": "Is Composite Asset" + }, + { + "fieldname": "capitalized_in", + "fieldtype": "Link", + "hidden": 1, + "label": "Capitalized In", + "options": "Asset Capitalization", + "read_only": 1 } ], "idx": 72, @@ -538,7 +557,7 @@ "table_fieldname": "accounts" } ], - "modified": "2023-08-10 20:25:09.913073", + "modified": "2023-10-03 23:28:26.732269", "modified_by": "Administrator", "module": "Assets", "name": "Asset", diff --git a/erpnext/assets/doctype/asset/asset.py b/erpnext/assets/doctype/asset/asset.py index 81a35ad8f93b..d54d15afaf6e 100644 --- a/erpnext/assets/doctype/asset/asset.py +++ b/erpnext/assets/doctype/asset/asset.py @@ -234,7 +234,7 @@ def validate_asset_values(self): if not self.asset_category: self.asset_category = frappe.get_cached_value("Item", self.item_code, "asset_category") - if not flt(self.gross_purchase_amount): + if not flt(self.gross_purchase_amount) and not self.is_composite_asset: frappe.throw(_("Gross Purchase Amount is mandatory"), frappe.MandatoryError) if is_cwip_accounting_enabled(self.asset_category): @@ -1166,6 +1166,15 @@ def create_asset_repair(asset, asset_name): return asset_repair +@frappe.whitelist() +def create_asset_capitalization(asset): + asset_capitalization = frappe.new_doc("Asset Capitalization") + asset_capitalization.update( + {"target_asset": asset, "capitalization_method": "Choose a WIP composite asset"} + ) + return asset_capitalization + + @frappe.whitelist() def create_asset_value_adjustment(asset, asset_category, company): asset_value_adjustment = frappe.new_doc("Asset Value Adjustment") diff --git a/erpnext/assets/doctype/asset/test_asset.py b/erpnext/assets/doctype/asset/test_asset.py index 1adbeed65a5d..fc36df8aec52 100644 --- a/erpnext/assets/doctype/asset/test_asset.py +++ b/erpnext/assets/doctype/asset/test_asset.py @@ -1686,6 +1686,7 @@ def create_asset(**args): "location": args.location or "Test Location", "asset_owner": args.asset_owner or "Company", "is_existing_asset": args.is_existing_asset or 1, + "is_composite_asset": args.is_composite_asset or 0, "asset_quantity": args.get("asset_quantity") or 1, "depr_entry_posting_status": args.depr_entry_posting_status or "", } diff --git a/erpnext/assets/doctype/asset_capitalization/asset_capitalization.js b/erpnext/assets/doctype/asset_capitalization/asset_capitalization.js index b312f93d319c..304bdf26dee8 100644 --- a/erpnext/assets/doctype/asset_capitalization/asset_capitalization.js +++ b/erpnext/assets/doctype/asset_capitalization/asset_capitalization.js @@ -15,9 +15,15 @@ erpnext.assets.AssetCapitalization = class AssetCapitalization extends erpnext.s refresh() { this.show_general_ledger(); + if ((this.frm.doc.stock_items && this.frm.doc.stock_items.length) || !this.frm.doc.target_is_fixed_asset) { this.show_stock_ledger(); } + + if (this.frm.doc.stock_items && !this.frm.doc.stock_items.length && this.frm.doc.target_asset && this.frm.doc.capitalization_method === "Choose a WIP composite asset") { + this.set_consumed_stock_items_tagged_to_wip_composite_asset(this.frm.doc.target_asset); + this.get_target_asset_details(); + } } setup_queries() { @@ -34,18 +40,9 @@ erpnext.assets.AssetCapitalization = class AssetCapitalization extends erpnext.s }); me.frm.set_query("target_asset", function() { - var filters = {}; - - if (me.frm.doc.target_item_code) { - filters['item_code'] = me.frm.doc.target_item_code; - } - - filters['status'] = ["not in", ["Draft", "Scrapped", "Sold", "Capitalized", "Decapitalized"]]; - filters['docstatus'] = 1; - return { - filters: filters - }; + filters: {'is_composite_asset': 1, 'docstatus': 0 } + } }); me.frm.set_query("asset", "asset_items", function() { @@ -104,6 +101,39 @@ erpnext.assets.AssetCapitalization = class AssetCapitalization extends erpnext.s return this.get_target_item_details(); } + target_asset() { + if (this.frm.doc.target_asset && this.frm.doc.capitalization_method === "Choose a WIP composite asset") { + this.set_consumed_stock_items_tagged_to_wip_composite_asset(this.frm.doc.target_asset); + this.get_target_asset_details(); + } + } + + set_consumed_stock_items_tagged_to_wip_composite_asset(asset) { + var me = this; + + if (asset) { + return me.frm.call({ + method: "erpnext.assets.doctype.asset_capitalization.asset_capitalization.get_items_tagged_to_wip_composite_asset", + args: { + asset: asset, + }, + callback: function (r) { + if (!r.exc && r.message) { + me.frm.clear_table("stock_items"); + + for (let item of r.message) { + me.frm.add_child("stock_items", item); + } + + refresh_field("stock_items"); + + me.calculate_totals(); + } + } + }); + } + } + item_code(doc, cdt, cdn) { var row = frappe.get_doc(cdt, cdn); if (cdt === "Asset Capitalization Stock Item") { @@ -218,6 +248,26 @@ erpnext.assets.AssetCapitalization = class AssetCapitalization extends erpnext.s } } + get_target_asset_details() { + var me = this; + + if (me.frm.doc.target_asset) { + return me.frm.call({ + method: "erpnext.assets.doctype.asset_capitalization.asset_capitalization.get_target_asset_details", + child: me.frm.doc, + args: { + asset: me.frm.doc.target_asset, + company: me.frm.doc.company, + }, + callback: function (r) { + if (!r.exc) { + me.frm.refresh_fields(); + } + } + }); + } + } + get_consumed_stock_item_details(row) { var me = this; diff --git a/erpnext/assets/doctype/asset_capitalization/asset_capitalization.json b/erpnext/assets/doctype/asset_capitalization/asset_capitalization.json index 04b0c4e5132c..9ddc44212f6d 100644 --- a/erpnext/assets/doctype/asset_capitalization/asset_capitalization.json +++ b/erpnext/assets/doctype/asset_capitalization/asset_capitalization.json @@ -8,24 +8,25 @@ "engine": "InnoDB", "field_order": [ "title", + "company", "naming_series", "entry_type", - "target_item_code", - "target_asset", "target_item_name", "target_is_fixed_asset", "target_has_batch_no", "target_has_serial_no", "column_break_9", - "target_asset_name", + "capitalization_method", + "target_item_code", "target_asset_location", + "target_asset", + "target_asset_name", "target_warehouse", "target_qty", "target_stock_uom", "target_batch_no", "target_serial_no", "column_break_5", - "company", "finance_book", "posting_date", "posting_time", @@ -57,12 +58,13 @@ "label": "Title" }, { + "depends_on": "eval:(doc.target_item_code && !doc.__islocal && doc.capitalization_method !== 'Choose a WIP composite asset') || ((doc.entry_type=='Capitalization' && doc.capitalization_method=='Create a new composite asset') || doc.entry_type=='Decapitalization')", "fieldname": "target_item_code", "fieldtype": "Link", "in_standard_filter": 1, "label": "Target Item Code", - "options": "Item", - "reqd": 1 + "mandatory_depends_on": "eval:(doc.entry_type=='Capitalization' && doc.capitalization_method=='Create a new composite asset') || doc.entry_type=='Decapitalization'", + "options": "Item" }, { "depends_on": "eval:doc.target_item_code && doc.target_item_name != doc.target_item_code", @@ -86,16 +88,18 @@ "fieldtype": "Column Break" }, { + "depends_on": "eval:(doc.target_asset && !doc.__islocal) || (doc.entry_type=='Capitalization' && doc.capitalization_method=='Choose a WIP composite asset')", "fieldname": "target_asset", "fieldtype": "Link", "in_standard_filter": 1, "label": "Target Asset", + "mandatory_depends_on": "eval:doc.entry_type=='Capitalization' && doc.capitalization_method=='Choose a WIP composite asset'", "no_copy": 1, "options": "Asset", - "read_only": 1 + "read_only_depends_on": "eval:(doc.entry_type=='Decapitalization') || (doc.entry_type=='Capitalization' && doc.capitalization_method=='Create a new composite asset')" }, { - "depends_on": "eval:doc.entry_type=='Capitalization'", + "depends_on": "eval:(doc.target_asset_name && !doc.__islocal) || (doc.target_asset && doc.entry_type=='Capitalization' && doc.capitalization_method=='Choose a WIP composite asset')", "fetch_from": "target_asset.asset_name", "fieldname": "target_asset_name", "fieldtype": "Data", @@ -186,12 +190,14 @@ }, { "default": "1", + "depends_on": "eval:doc.entry_type=='Decapitalization'", "fieldname": "target_qty", "fieldtype": "Float", "label": "Target Qty", "read_only_depends_on": "eval:doc.entry_type=='Capitalization'" }, { + "depends_on": "eval:doc.entry_type=='Decapitalization'", "fetch_from": "target_item_code.stock_uom", "fieldname": "target_stock_uom", "fieldtype": "Link", @@ -331,18 +337,26 @@ "read_only": 1 }, { - "depends_on": "eval:doc.entry_type=='Capitalization'", + "depends_on": "eval:doc.entry_type=='Capitalization' && doc.capitalization_method=='Create a new composite asset'", "fieldname": "target_asset_location", "fieldtype": "Link", "label": "Target Asset Location", - "mandatory_depends_on": "eval:doc.entry_type=='Capitalization'", + "mandatory_depends_on": "eval:doc.entry_type=='Capitalization' && doc.capitalization_method=='Create a new composite asset'", "options": "Location" + }, + { + "depends_on": "eval:doc.entry_type=='Capitalization'", + "fieldname": "capitalization_method", + "fieldtype": "Select", + "label": "Capitalization Method", + "mandatory_depends_on": "eval:doc.entry_type=='Capitalization'", + "options": "\nCreate a new composite asset\nChoose a WIP composite asset" } ], "index_web_pages_for_search": 1, "is_submittable": 1, "links": [], - "modified": "2023-06-22 14:17:07.995120", + "modified": "2023-10-03 22:55:59.461456", "modified_by": "Administrator", "module": "Assets", "name": "Asset Capitalization", diff --git a/erpnext/assets/doctype/asset_capitalization/asset_capitalization.py b/erpnext/assets/doctype/asset_capitalization/asset_capitalization.py index 44db6920c131..04654104c7ab 100644 --- a/erpnext/assets/doctype/asset_capitalization/asset_capitalization.py +++ b/erpnext/assets/doctype/asset_capitalization/asset_capitalization.py @@ -53,6 +53,7 @@ def validate(self): self.validate_posting_time() self.set_missing_values(for_validate=True) self.validate_target_item() + self.validate_target_asset() self.validate_consumed_stock_item() self.validate_consumed_asset_item() self.validate_service_item() @@ -63,12 +64,12 @@ def validate(self): def before_submit(self): self.validate_source_mandatory() - if self.entry_type == "Capitalization": - self.create_target_asset() + self.create_target_asset() def on_submit(self): self.update_stock_ledger() self.make_gl_entries() + self.update_target_asset() def on_cancel(self): self.ignore_linked_doctypes = ("GL Entry", "Stock Ledger Entry", "Repost Item Valuation") @@ -85,6 +86,11 @@ def set_missing_values(self, for_validate=False): if self.meta.has_field(k) and (not self.get(k) or k in force_fields): self.set(k, v) + target_asset_details = get_target_asset_details(self.target_asset, self.company) + for k, v in target_asset_details.items(): + if self.meta.has_field(k) and (not self.get(k) or k in force_fields): + self.set(k, v) + for d in self.stock_items: args = self.as_dict() args.update(d.as_dict()) @@ -146,6 +152,33 @@ def validate_target_item(self): self.validate_item(target_item) + def validate_target_asset(self): + if self.target_asset: + target_asset = self.get_asset_for_validation(self.target_asset) + + if not target_asset.is_composite_asset: + frappe.throw(_("Target Asset {0} needs to be composite asset").format(target_asset.name)) + + if target_asset.item_code != self.target_item_code: + frappe.throw( + _("Asset {0} does not belong to Item {1}").format(self.target_asset, self.target_item_code) + ) + + if target_asset.status in ("Scrapped", "Sold", "Capitalized", "Decapitalized"): + frappe.throw( + _("Target Asset {0} cannot be {1}").format(target_asset.name, target_asset.status) + ) + + if target_asset.docstatus == 1: + frappe.throw(_("Target Asset {0} cannot be submitted").format(target_asset.name)) + elif target_asset.docstatus == 2: + frappe.throw(_("Target Asset {0} cannot be cancelled").format(target_asset.name)) + + if target_asset.company != self.company: + frappe.throw( + _("Target Asset {0} does not belong to company {1}").format(target_asset.name, self.company) + ) + def validate_consumed_stock_item(self): for d in self.stock_items: if d.item_code: @@ -170,7 +203,23 @@ def validate_consumed_asset_item(self): ) asset = self.get_asset_for_validation(d.asset) - self.validate_asset(asset) + + if asset.status in ("Draft", "Scrapped", "Sold", "Capitalized", "Decapitalized"): + frappe.throw( + _("Row #{0}: Consumed Asset {1} cannot be {2}").format(d.idx, asset.name, asset.status) + ) + + if asset.docstatus == 0: + frappe.throw(_("Row #{0}: Consumed Asset {1} cannot be Draft").format(d.idx, asset.name)) + elif asset.docstatus == 2: + frappe.throw(_("Row #{0}: Consumed Asset {1} cannot be cancelled").format(d.idx, asset.name)) + + if asset.company != self.company: + frappe.throw( + _("Row #{0}: Consumed Asset {1} does not belong to company {2}").format( + d.idx, asset.name, self.company + ) + ) def validate_service_item(self): for d in self.service_items: @@ -205,21 +254,12 @@ def validate_item(self, item): def get_asset_for_validation(self, asset): return frappe.db.get_value( - "Asset", asset, ["name", "item_code", "company", "status", "docstatus"], as_dict=1 + "Asset", + asset, + ["name", "item_code", "company", "status", "docstatus", "is_composite_asset"], + as_dict=1, ) - def validate_asset(self, asset): - if asset.status in ("Draft", "Scrapped", "Sold", "Capitalized", "Decapitalized"): - frappe.throw(_("Asset {0} is {1}").format(asset.name, asset.status)) - - if asset.docstatus == 0: - frappe.throw(_("Asset {0} is Draft").format(asset.name)) - if asset.docstatus == 2: - frappe.throw(_("Asset {0} is cancelled").format(asset.name)) - - if asset.company != self.company: - frappe.throw(_("Asset {0} does not belong to company {1}").format(asset.name, self.company)) - @frappe.whitelist() def set_warehouse_details(self): for d in self.get("stock_items"): @@ -485,16 +525,25 @@ def get_gl_entries_for_target_item(self, gl_entries, target_against, precision): ) def create_target_asset(self): + if ( + self.entry_type != "Capitalization" + or self.capitalization_method != "Create a new composite asset" + ): + return + total_target_asset_value = flt(self.total_value, self.precision("total_value")) + asset_doc = frappe.new_doc("Asset") asset_doc.company = self.company asset_doc.item_code = self.target_item_code - asset_doc.is_existing_asset = 1 + asset_doc.is_composite_asset = 1 asset_doc.location = self.target_asset_location asset_doc.available_for_use_date = self.posting_date asset_doc.purchase_date = self.posting_date asset_doc.gross_purchase_amount = total_target_asset_value asset_doc.purchase_receipt_amount = total_target_asset_value + asset_doc.purchase_receipt_amount = total_target_asset_value + asset_doc.capitalized_in = self.name asset_doc.flags.ignore_validate = True asset_doc.insert() @@ -510,6 +559,28 @@ def create_target_asset(self): ).format(get_link_to_form("Asset", asset_doc.name)) ) + def update_target_asset(self): + if ( + self.entry_type != "Capitalization" + or self.capitalization_method != "Choose a WIP composite asset" + ): + return + + total_target_asset_value = flt(self.total_value, self.precision("total_value")) + + asset_doc = frappe.get_doc("Asset", self.target_asset) + asset_doc.gross_purchase_amount = total_target_asset_value + asset_doc.purchase_receipt_amount = total_target_asset_value + asset_doc.capitalized_in = self.name + asset_doc.flags.ignore_validate = True + asset_doc.save() + + frappe.msgprint( + _( + "Asset {0} has been updated. Please set the depreciation details if any and submit it." + ).format(get_link_to_form("Asset", asset_doc.name)) + ) + def restore_consumed_asset_items(self): for item in self.asset_items: asset = frappe.get_doc("Asset", item.asset) @@ -568,6 +639,33 @@ def get_target_item_details(item_code=None, company=None): return out +@frappe.whitelist() +def get_target_asset_details(asset=None, company=None): + out = frappe._dict() + + # Get Asset Details + asset_details = frappe._dict() + if asset: + asset_details = frappe.db.get_value("Asset", asset, ["asset_name", "item_code"], as_dict=1) + if not asset_details: + frappe.throw(_("Asset {0} does not exist").format(asset)) + + # Re-set item code from Asset + out.target_item_code = asset_details.item_code + + # Set Asset Details + out.asset_name = asset_details.asset_name + + if asset_details.item_code: + out.target_fixed_asset_account = get_asset_category_account( + "fixed_asset_account", item=asset_details.item_code, company=company + ) + else: + out.target_fixed_asset_account = None + + return out + + @frappe.whitelist() def get_consumed_stock_item_details(args): if isinstance(args, string_types): @@ -716,3 +814,30 @@ def get_service_item_details(args): ) return out + + +@frappe.whitelist() +def get_items_tagged_to_wip_composite_asset(asset): + fields = [ + "item_code", + "item_name", + "batch_no", + "serial_no", + "stock_qty", + "stock_uom", + "warehouse", + "cost_center", + "qty", + "valuation_rate", + "amount", + ] + + pi_items = frappe.get_all( + "Purchase Invoice Item", filters={"wip_composite_asset": asset}, fields=fields + ) + + pr_items = frappe.get_all( + "Purchase Receipt Item", filters={"wip_composite_asset": asset}, fields=fields + ) + + return pi_items + pr_items diff --git a/erpnext/assets/doctype/asset_capitalization/test_asset_capitalization.py b/erpnext/assets/doctype/asset_capitalization/test_asset_capitalization.py index ead7abbf3406..60c62d49afab 100644 --- a/erpnext/assets/doctype/asset_capitalization/test_asset_capitalization.py +++ b/erpnext/assets/doctype/asset_capitalization/test_asset_capitalization.py @@ -50,6 +50,7 @@ def test_capitalization_with_perpetual_inventory(self): # Create and submit Asset Captitalization asset_capitalization = create_asset_capitalization( entry_type="Capitalization", + capitalization_method="Create a new composite asset", target_item_code="Macbook Pro", target_asset_location="Test Location", stock_qty=stock_qty, @@ -139,6 +140,7 @@ def test_capitalization_with_periodical_inventory(self): # Create and submit Asset Captitalization asset_capitalization = create_asset_capitalization( entry_type="Capitalization", + capitalization_method="Create a new composite asset", target_item_code="Macbook Pro", target_asset_location="Test Location", stock_qty=stock_qty, @@ -203,6 +205,77 @@ def test_capitalization_with_periodical_inventory(self): self.assertFalse(get_actual_gle_dict(asset_capitalization.name)) self.assertFalse(get_actual_sle_dict(asset_capitalization.name)) + def test_capitalization_with_wip_composite_asset(self): + company = "_Test Company with perpetual inventory" + set_depreciation_settings_in_company(company=company) + + stock_rate = 1000 + stock_qty = 2 + stock_amount = 2000 + + total_amount = 2000 + + wip_composite_asset = create_asset( + asset_name="Asset Capitalization WIP Composite Asset", + is_composite_asset=1, + warehouse="Stores - TCP1", + company=company, + ) + + # Create and submit Asset Captitalization + asset_capitalization = create_asset_capitalization( + entry_type="Capitalization", + capitalization_method="Choose a WIP composite asset", + target_asset=wip_composite_asset, + target_asset_location="Test Location", + stock_qty=stock_qty, + stock_rate=stock_rate, + service_expense_account="Expenses Included In Asset Valuation - TCP1", + company=company, + submit=1, + ) + + # Test Asset Capitalization values + self.assertEqual(asset_capitalization.entry_type, "Capitalization") + self.assertEqual(asset_capitalization.capitalization_method, "Choose a WIP composite asset") + self.assertEqual(asset_capitalization.target_qty, 1) + + self.assertEqual(asset_capitalization.stock_items[0].valuation_rate, stock_rate) + self.assertEqual(asset_capitalization.stock_items[0].amount, stock_amount) + self.assertEqual(asset_capitalization.stock_items_total, stock_amount) + + self.assertEqual(asset_capitalization.total_value, total_amount) + self.assertEqual(asset_capitalization.target_incoming_rate, total_amount) + + # Test Target Asset values + target_asset = frappe.get_doc("Asset", asset_capitalization.target_asset) + self.assertEqual(target_asset.gross_purchase_amount, total_amount) + self.assertEqual(target_asset.purchase_receipt_amount, total_amount) + + # Test General Ledger Entries + expected_gle = { + "_Test Fixed Asset - TCP1": 2000, + "_Test Warehouse - TCP1": -2000, + } + actual_gle = get_actual_gle_dict(asset_capitalization.name) + + self.assertEqual(actual_gle, expected_gle) + + # Test Stock Ledger Entries + expected_sle = { + ("Capitalization Source Stock Item", "_Test Warehouse - TCP1"): { + "actual_qty": -stock_qty, + "stock_value_difference": -stock_amount, + } + } + actual_sle = get_actual_sle_dict(asset_capitalization.name) + self.assertEqual(actual_sle, expected_sle) + + # Cancel Asset Capitalization and make test entries and status are reversed + asset_capitalization.cancel() + self.assertFalse(get_actual_gle_dict(asset_capitalization.name)) + self.assertFalse(get_actual_sle_dict(asset_capitalization.name)) + def test_decapitalization_with_depreciation(self): # Variables purchase_date = "2020-01-01" @@ -326,6 +399,7 @@ def create_asset_capitalization(**args): asset_capitalization.update( { "entry_type": args.entry_type or "Capitalization", + "capitalization_method": args.capitalization_method or None, "company": company, "posting_date": args.posting_date or now.strftime("%Y-%m-%d"), "posting_time": args.posting_time or now.strftime("%H:%M:%S.%f"), diff --git a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.js b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.js index 312c166f8b7f..8966fbcbb3c3 100644 --- a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.js +++ b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.js @@ -35,6 +35,12 @@ frappe.ui.form.on("Purchase Receipt", { } }); + frm.set_query("wip_composite_asset", "items", function() { + return { + filters: {'is_composite_asset': 1, 'docstatus': 0 } + } + }); + frm.set_query("taxes_and_charges", function() { return { filters: {'company': frm.doc.company } diff --git a/erpnext/stock/doctype/purchase_receipt_item/purchase_receipt_item.json b/erpnext/stock/doctype/purchase_receipt_item/purchase_receipt_item.json index 76f476edf8a0..4911523e7ed4 100644 --- a/erpnext/stock/doctype/purchase_receipt_item/purchase_receipt_item.json +++ b/erpnext/stock/doctype/purchase_receipt_item/purchase_receipt_item.json @@ -110,6 +110,7 @@ "manufacturer_part_no", "accounting_details_section", "expense_account", + "wip_composite_asset", "column_break_102", "provisional_expense_account", "accounting_dimensions_section", @@ -1018,12 +1019,18 @@ "no_copy": 1, "print_hide": 1, "read_only": 1 + }, + { + "fieldname": "wip_composite_asset", + "fieldtype": "Link", + "label": "WIP Composite Asset", + "options": "Asset" } ], "idx": 1, "istable": 1, "links": [], - "modified": "2023-07-04 17:22:02.830029", + "modified": "2023-10-03 21:11:50.547261", "modified_by": "Administrator", "module": "Stock", "name": "Purchase Receipt Item", @@ -1034,4 +1041,4 @@ "sort_field": "modified", "sort_order": "DESC", "states": [] -} +} \ No newline at end of file From 2b38b780bab39d8e3cce62a77cae6fd0e971652b Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Wed, 4 Oct 2023 14:29:45 +0530 Subject: [PATCH 386/501] fix(Employee): enable `no_copy` for `relieving_date` (backport #37344) (#37358) Co-authored-by: Rucha Mahabal Co-authored-by: Jignesh (GreyCube Technologies) fix(Employee): enable `no_copy` for `relieving_date` (#37344) --- erpnext/setup/doctype/employee/employee.json | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/erpnext/setup/doctype/employee/employee.json b/erpnext/setup/doctype/employee/employee.json index 6cb4292226c5..1143ccb7b10e 100644 --- a/erpnext/setup/doctype/employee/employee.json +++ b/erpnext/setup/doctype/employee/employee.json @@ -616,6 +616,7 @@ "fieldname": "relieving_date", "fieldtype": "Date", "label": "Relieving Date", + "no_copy": 1, "mandatory_depends_on": "eval:doc.status == \"Left\"", "oldfieldname": "relieving_date", "oldfieldtype": "Date" @@ -822,7 +823,7 @@ "idx": 24, "image_field": "image", "links": [], - "modified": "2023-03-30 15:57:05.174592", + "modified": "2023-10-04 10:57:05.174592", "modified_by": "Administrator", "module": "Setup", "name": "Employee", @@ -870,4 +871,4 @@ "sort_order": "DESC", "states": [], "title_field": "employee_name" -} \ No newline at end of file +} From 69476861419f067ce6638063b3c2ca14ced04e7b Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Wed, 4 Oct 2023 19:46:30 +0530 Subject: [PATCH 387/501] feat: composite WIP asset (backport #37352) (#37360) feat: composite WIP asset (#37352) feat: wip composite asset (cherry picked from commit 0ecd7d2bf5eb522315776bae5633486d89791e00) Co-authored-by: Anand Baburajan --- .../purchase_invoice/purchase_invoice.js | 6 + .../purchase_invoice_item.json | 11 +- erpnext/assets/doctype/asset/asset.js | 44 ++++- erpnext/assets/doctype/asset/asset.json | 29 +++- erpnext/assets/doctype/asset/asset.py | 11 +- erpnext/assets/doctype/asset/test_asset.py | 1 + .../asset_capitalization.js | 72 ++++++-- .../asset_capitalization.json | 36 ++-- .../asset_capitalization.py | 159 ++++++++++++++++-- .../test_asset_capitalization.py | 74 ++++++++ .../purchase_receipt/purchase_receipt.js | 6 + .../purchase_receipt_item.json | 11 +- 12 files changed, 406 insertions(+), 54 deletions(-) diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js index 5c82cf99438f..ceb8204bd5da 100644 --- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js +++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js @@ -460,6 +460,12 @@ cur_frm.set_query("expense_account", "items", function(doc) { } }); +cur_frm.set_query("wip_composite_asset", "items", function() { + return { + filters: {'is_composite_asset': 1, 'docstatus': 0 } + } +}); + cur_frm.cscript.expense_account = function(doc, cdt, cdn){ var d = locals[cdt][cdn]; if(d.idx == 1 && d.expense_account){ diff --git a/erpnext/accounts/doctype/purchase_invoice_item/purchase_invoice_item.json b/erpnext/accounts/doctype/purchase_invoice_item/purchase_invoice_item.json index 92db6b61bb97..04f28beda9ef 100644 --- a/erpnext/accounts/doctype/purchase_invoice_item/purchase_invoice_item.json +++ b/erpnext/accounts/doctype/purchase_invoice_item/purchase_invoice_item.json @@ -75,6 +75,7 @@ "manufacturer_part_no", "accounting", "expense_account", + "wip_composite_asset", "col_break5", "is_fixed_asset", "asset_location", @@ -877,12 +878,18 @@ "fieldname": "apply_tds", "fieldtype": "Check", "label": "Apply TDS" + }, + { + "fieldname": "wip_composite_asset", + "fieldtype": "Link", + "label": "WIP Composite Asset", + "options": "Asset" } ], "idx": 1, "istable": 1, "links": [], - "modified": "2023-07-04 17:22:21.501152", + "modified": "2023-10-03 21:01:01.824892", "modified_by": "Administrator", "module": "Accounts", "name": "Purchase Invoice Item", @@ -892,4 +899,4 @@ "sort_field": "modified", "sort_order": "DESC", "states": [] -} +} \ No newline at end of file diff --git a/erpnext/assets/doctype/asset/asset.js b/erpnext/assets/doctype/asset/asset.js index 5c1da91fdf99..0605189fec05 100644 --- a/erpnext/assets/doctype/asset/asset.js +++ b/erpnext/assets/doctype/asset/asset.js @@ -147,6 +147,15 @@ frappe.ui.form.on('Asset', { if (frm.doc.docstatus == 0) { frm.toggle_reqd("finance_books", frm.doc.calculate_depreciation); + + if (frm.doc.is_composite_asset && !frm.doc.capitalized_in) { + $('.primary-action').prop('hidden', true); + $('.form-message').text('Capitalize this asset to confirm'); + + frm.add_custom_button(__("Capitalize Asset"), function() { + frm.trigger("create_asset_capitalization"); + }); + } } }, @@ -168,7 +177,7 @@ frappe.ui.form.on('Asset', { frm.set_df_property('purchase_invoice', 'read_only', 1); frm.set_df_property('purchase_receipt', 'read_only', 1); } - else if (frm.doc.is_existing_asset) { + else if (frm.doc.is_existing_asset || frm.doc.is_composite_asset) { frm.toggle_reqd('purchase_receipt', 0); frm.toggle_reqd('purchase_invoice', 0); } @@ -300,7 +309,17 @@ frappe.ui.form.on('Asset', { is_existing_asset: function(frm) { frm.trigger("toggle_reference_doc"); - // frm.toggle_reqd("next_depreciation_date", (!frm.doc.is_existing_asset && frm.doc.calculate_depreciation)); + }, + + is_composite_asset: function(frm) { + if(frm.doc.is_composite_asset) { + frm.set_value('gross_purchase_amount', 0); + frm.set_df_property('gross_purchase_amount', 'read_only', 1); + } else { + frm.set_df_property('gross_purchase_amount', 'read_only', 0); + } + + frm.trigger("toggle_reference_doc"); }, make_schedules_editable: function(frm) { @@ -361,6 +380,19 @@ frappe.ui.form.on('Asset', { }); }, + create_asset_capitalization: function(frm) { + frappe.call({ + args: { + "asset": frm.doc.name, + }, + method: "erpnext.assets.doctype.asset.asset.create_asset_capitalization", + callback: function(r) { + var doclist = frappe.model.sync(r.message); + frappe.set_route("Form", doclist[0].doctype, doclist[0].name); + } + }); + }, + split_asset: function(frm) { const title = __('Split Asset'); @@ -424,9 +456,11 @@ frappe.ui.form.on('Asset', { }, gross_purchase_amount: function(frm) { - frm.doc.finance_books.forEach(d => { - frm.events.set_depreciation_rate(frm, d); - }) + if (frm.doc.finance_books) { + frm.doc.finance_books.forEach(d => { + frm.events.set_depreciation_rate(frm, d); + }) + } }, purchase_receipt: (frm) => { diff --git a/erpnext/assets/doctype/asset/asset.json b/erpnext/assets/doctype/asset/asset.json index 060d991945b2..1da3edcc60eb 100644 --- a/erpnext/assets/doctype/asset/asset.json +++ b/erpnext/assets/doctype/asset/asset.json @@ -14,6 +14,7 @@ "asset_owner", "asset_owner_company", "is_existing_asset", + "is_composite_asset", "supplier", "customer", "image", @@ -72,7 +73,8 @@ "purchase_receipt_amount", "default_finance_book", "depr_entry_posting_status", - "amended_from" + "amended_from", + "capitalized_in" ], "fields": [ { @@ -199,7 +201,7 @@ "fieldtype": "Date", "label": "Purchase Date", "read_only": 1, - "read_only_depends_on": "eval:!doc.is_existing_asset", + "read_only_depends_on": "eval:!doc.is_existing_asset && !doc.is_composite_asset", "reqd": 1 }, { @@ -237,10 +239,12 @@ "default": "0", "fieldname": "calculate_depreciation", "fieldtype": "Check", - "label": "Calculate Depreciation" + "label": "Calculate Depreciation", + "read_only_depends_on": "eval:doc.is_composite_asset && !doc.gross_purchase_amount" }, { "default": "0", + "depends_on": "eval:!doc.is_composite_asset", "fieldname": "is_existing_asset", "fieldtype": "Check", "label": "Is Existing Asset" @@ -492,7 +496,7 @@ "fieldname": "asset_quantity", "fieldtype": "Int", "label": "Asset Quantity", - "read_only_depends_on": "eval:!doc.is_existing_asset" + "read_only_depends_on": "eval:!doc.is_existing_asset && !doc.is_composite_asset" }, { "fieldname": "depr_entry_posting_status", @@ -510,6 +514,21 @@ "fieldname": "is_fully_depreciated", "fieldtype": "Check", "label": "Is Fully Depreciated" + }, + { + "default": "0", + "depends_on": "eval:!doc.is_existing_asset", + "fieldname": "is_composite_asset", + "fieldtype": "Check", + "label": "Is Composite Asset" + }, + { + "fieldname": "capitalized_in", + "fieldtype": "Link", + "hidden": 1, + "label": "Capitalized In", + "options": "Asset Capitalization", + "read_only": 1 } ], "idx": 72, @@ -538,7 +557,7 @@ "table_fieldname": "accounts" } ], - "modified": "2023-08-10 20:25:09.913073", + "modified": "2023-10-03 23:28:26.732269", "modified_by": "Administrator", "module": "Assets", "name": "Asset", diff --git a/erpnext/assets/doctype/asset/asset.py b/erpnext/assets/doctype/asset/asset.py index 81a35ad8f93b..d54d15afaf6e 100644 --- a/erpnext/assets/doctype/asset/asset.py +++ b/erpnext/assets/doctype/asset/asset.py @@ -234,7 +234,7 @@ def validate_asset_values(self): if not self.asset_category: self.asset_category = frappe.get_cached_value("Item", self.item_code, "asset_category") - if not flt(self.gross_purchase_amount): + if not flt(self.gross_purchase_amount) and not self.is_composite_asset: frappe.throw(_("Gross Purchase Amount is mandatory"), frappe.MandatoryError) if is_cwip_accounting_enabled(self.asset_category): @@ -1166,6 +1166,15 @@ def create_asset_repair(asset, asset_name): return asset_repair +@frappe.whitelist() +def create_asset_capitalization(asset): + asset_capitalization = frappe.new_doc("Asset Capitalization") + asset_capitalization.update( + {"target_asset": asset, "capitalization_method": "Choose a WIP composite asset"} + ) + return asset_capitalization + + @frappe.whitelist() def create_asset_value_adjustment(asset, asset_category, company): asset_value_adjustment = frappe.new_doc("Asset Value Adjustment") diff --git a/erpnext/assets/doctype/asset/test_asset.py b/erpnext/assets/doctype/asset/test_asset.py index 1adbeed65a5d..fc36df8aec52 100644 --- a/erpnext/assets/doctype/asset/test_asset.py +++ b/erpnext/assets/doctype/asset/test_asset.py @@ -1686,6 +1686,7 @@ def create_asset(**args): "location": args.location or "Test Location", "asset_owner": args.asset_owner or "Company", "is_existing_asset": args.is_existing_asset or 1, + "is_composite_asset": args.is_composite_asset or 0, "asset_quantity": args.get("asset_quantity") or 1, "depr_entry_posting_status": args.depr_entry_posting_status or "", } diff --git a/erpnext/assets/doctype/asset_capitalization/asset_capitalization.js b/erpnext/assets/doctype/asset_capitalization/asset_capitalization.js index b312f93d319c..304bdf26dee8 100644 --- a/erpnext/assets/doctype/asset_capitalization/asset_capitalization.js +++ b/erpnext/assets/doctype/asset_capitalization/asset_capitalization.js @@ -15,9 +15,15 @@ erpnext.assets.AssetCapitalization = class AssetCapitalization extends erpnext.s refresh() { this.show_general_ledger(); + if ((this.frm.doc.stock_items && this.frm.doc.stock_items.length) || !this.frm.doc.target_is_fixed_asset) { this.show_stock_ledger(); } + + if (this.frm.doc.stock_items && !this.frm.doc.stock_items.length && this.frm.doc.target_asset && this.frm.doc.capitalization_method === "Choose a WIP composite asset") { + this.set_consumed_stock_items_tagged_to_wip_composite_asset(this.frm.doc.target_asset); + this.get_target_asset_details(); + } } setup_queries() { @@ -34,18 +40,9 @@ erpnext.assets.AssetCapitalization = class AssetCapitalization extends erpnext.s }); me.frm.set_query("target_asset", function() { - var filters = {}; - - if (me.frm.doc.target_item_code) { - filters['item_code'] = me.frm.doc.target_item_code; - } - - filters['status'] = ["not in", ["Draft", "Scrapped", "Sold", "Capitalized", "Decapitalized"]]; - filters['docstatus'] = 1; - return { - filters: filters - }; + filters: {'is_composite_asset': 1, 'docstatus': 0 } + } }); me.frm.set_query("asset", "asset_items", function() { @@ -104,6 +101,39 @@ erpnext.assets.AssetCapitalization = class AssetCapitalization extends erpnext.s return this.get_target_item_details(); } + target_asset() { + if (this.frm.doc.target_asset && this.frm.doc.capitalization_method === "Choose a WIP composite asset") { + this.set_consumed_stock_items_tagged_to_wip_composite_asset(this.frm.doc.target_asset); + this.get_target_asset_details(); + } + } + + set_consumed_stock_items_tagged_to_wip_composite_asset(asset) { + var me = this; + + if (asset) { + return me.frm.call({ + method: "erpnext.assets.doctype.asset_capitalization.asset_capitalization.get_items_tagged_to_wip_composite_asset", + args: { + asset: asset, + }, + callback: function (r) { + if (!r.exc && r.message) { + me.frm.clear_table("stock_items"); + + for (let item of r.message) { + me.frm.add_child("stock_items", item); + } + + refresh_field("stock_items"); + + me.calculate_totals(); + } + } + }); + } + } + item_code(doc, cdt, cdn) { var row = frappe.get_doc(cdt, cdn); if (cdt === "Asset Capitalization Stock Item") { @@ -218,6 +248,26 @@ erpnext.assets.AssetCapitalization = class AssetCapitalization extends erpnext.s } } + get_target_asset_details() { + var me = this; + + if (me.frm.doc.target_asset) { + return me.frm.call({ + method: "erpnext.assets.doctype.asset_capitalization.asset_capitalization.get_target_asset_details", + child: me.frm.doc, + args: { + asset: me.frm.doc.target_asset, + company: me.frm.doc.company, + }, + callback: function (r) { + if (!r.exc) { + me.frm.refresh_fields(); + } + } + }); + } + } + get_consumed_stock_item_details(row) { var me = this; diff --git a/erpnext/assets/doctype/asset_capitalization/asset_capitalization.json b/erpnext/assets/doctype/asset_capitalization/asset_capitalization.json index 04b0c4e5132c..9ddc44212f6d 100644 --- a/erpnext/assets/doctype/asset_capitalization/asset_capitalization.json +++ b/erpnext/assets/doctype/asset_capitalization/asset_capitalization.json @@ -8,24 +8,25 @@ "engine": "InnoDB", "field_order": [ "title", + "company", "naming_series", "entry_type", - "target_item_code", - "target_asset", "target_item_name", "target_is_fixed_asset", "target_has_batch_no", "target_has_serial_no", "column_break_9", - "target_asset_name", + "capitalization_method", + "target_item_code", "target_asset_location", + "target_asset", + "target_asset_name", "target_warehouse", "target_qty", "target_stock_uom", "target_batch_no", "target_serial_no", "column_break_5", - "company", "finance_book", "posting_date", "posting_time", @@ -57,12 +58,13 @@ "label": "Title" }, { + "depends_on": "eval:(doc.target_item_code && !doc.__islocal && doc.capitalization_method !== 'Choose a WIP composite asset') || ((doc.entry_type=='Capitalization' && doc.capitalization_method=='Create a new composite asset') || doc.entry_type=='Decapitalization')", "fieldname": "target_item_code", "fieldtype": "Link", "in_standard_filter": 1, "label": "Target Item Code", - "options": "Item", - "reqd": 1 + "mandatory_depends_on": "eval:(doc.entry_type=='Capitalization' && doc.capitalization_method=='Create a new composite asset') || doc.entry_type=='Decapitalization'", + "options": "Item" }, { "depends_on": "eval:doc.target_item_code && doc.target_item_name != doc.target_item_code", @@ -86,16 +88,18 @@ "fieldtype": "Column Break" }, { + "depends_on": "eval:(doc.target_asset && !doc.__islocal) || (doc.entry_type=='Capitalization' && doc.capitalization_method=='Choose a WIP composite asset')", "fieldname": "target_asset", "fieldtype": "Link", "in_standard_filter": 1, "label": "Target Asset", + "mandatory_depends_on": "eval:doc.entry_type=='Capitalization' && doc.capitalization_method=='Choose a WIP composite asset'", "no_copy": 1, "options": "Asset", - "read_only": 1 + "read_only_depends_on": "eval:(doc.entry_type=='Decapitalization') || (doc.entry_type=='Capitalization' && doc.capitalization_method=='Create a new composite asset')" }, { - "depends_on": "eval:doc.entry_type=='Capitalization'", + "depends_on": "eval:(doc.target_asset_name && !doc.__islocal) || (doc.target_asset && doc.entry_type=='Capitalization' && doc.capitalization_method=='Choose a WIP composite asset')", "fetch_from": "target_asset.asset_name", "fieldname": "target_asset_name", "fieldtype": "Data", @@ -186,12 +190,14 @@ }, { "default": "1", + "depends_on": "eval:doc.entry_type=='Decapitalization'", "fieldname": "target_qty", "fieldtype": "Float", "label": "Target Qty", "read_only_depends_on": "eval:doc.entry_type=='Capitalization'" }, { + "depends_on": "eval:doc.entry_type=='Decapitalization'", "fetch_from": "target_item_code.stock_uom", "fieldname": "target_stock_uom", "fieldtype": "Link", @@ -331,18 +337,26 @@ "read_only": 1 }, { - "depends_on": "eval:doc.entry_type=='Capitalization'", + "depends_on": "eval:doc.entry_type=='Capitalization' && doc.capitalization_method=='Create a new composite asset'", "fieldname": "target_asset_location", "fieldtype": "Link", "label": "Target Asset Location", - "mandatory_depends_on": "eval:doc.entry_type=='Capitalization'", + "mandatory_depends_on": "eval:doc.entry_type=='Capitalization' && doc.capitalization_method=='Create a new composite asset'", "options": "Location" + }, + { + "depends_on": "eval:doc.entry_type=='Capitalization'", + "fieldname": "capitalization_method", + "fieldtype": "Select", + "label": "Capitalization Method", + "mandatory_depends_on": "eval:doc.entry_type=='Capitalization'", + "options": "\nCreate a new composite asset\nChoose a WIP composite asset" } ], "index_web_pages_for_search": 1, "is_submittable": 1, "links": [], - "modified": "2023-06-22 14:17:07.995120", + "modified": "2023-10-03 22:55:59.461456", "modified_by": "Administrator", "module": "Assets", "name": "Asset Capitalization", diff --git a/erpnext/assets/doctype/asset_capitalization/asset_capitalization.py b/erpnext/assets/doctype/asset_capitalization/asset_capitalization.py index 44db6920c131..04654104c7ab 100644 --- a/erpnext/assets/doctype/asset_capitalization/asset_capitalization.py +++ b/erpnext/assets/doctype/asset_capitalization/asset_capitalization.py @@ -53,6 +53,7 @@ def validate(self): self.validate_posting_time() self.set_missing_values(for_validate=True) self.validate_target_item() + self.validate_target_asset() self.validate_consumed_stock_item() self.validate_consumed_asset_item() self.validate_service_item() @@ -63,12 +64,12 @@ def validate(self): def before_submit(self): self.validate_source_mandatory() - if self.entry_type == "Capitalization": - self.create_target_asset() + self.create_target_asset() def on_submit(self): self.update_stock_ledger() self.make_gl_entries() + self.update_target_asset() def on_cancel(self): self.ignore_linked_doctypes = ("GL Entry", "Stock Ledger Entry", "Repost Item Valuation") @@ -85,6 +86,11 @@ def set_missing_values(self, for_validate=False): if self.meta.has_field(k) and (not self.get(k) or k in force_fields): self.set(k, v) + target_asset_details = get_target_asset_details(self.target_asset, self.company) + for k, v in target_asset_details.items(): + if self.meta.has_field(k) and (not self.get(k) or k in force_fields): + self.set(k, v) + for d in self.stock_items: args = self.as_dict() args.update(d.as_dict()) @@ -146,6 +152,33 @@ def validate_target_item(self): self.validate_item(target_item) + def validate_target_asset(self): + if self.target_asset: + target_asset = self.get_asset_for_validation(self.target_asset) + + if not target_asset.is_composite_asset: + frappe.throw(_("Target Asset {0} needs to be composite asset").format(target_asset.name)) + + if target_asset.item_code != self.target_item_code: + frappe.throw( + _("Asset {0} does not belong to Item {1}").format(self.target_asset, self.target_item_code) + ) + + if target_asset.status in ("Scrapped", "Sold", "Capitalized", "Decapitalized"): + frappe.throw( + _("Target Asset {0} cannot be {1}").format(target_asset.name, target_asset.status) + ) + + if target_asset.docstatus == 1: + frappe.throw(_("Target Asset {0} cannot be submitted").format(target_asset.name)) + elif target_asset.docstatus == 2: + frappe.throw(_("Target Asset {0} cannot be cancelled").format(target_asset.name)) + + if target_asset.company != self.company: + frappe.throw( + _("Target Asset {0} does not belong to company {1}").format(target_asset.name, self.company) + ) + def validate_consumed_stock_item(self): for d in self.stock_items: if d.item_code: @@ -170,7 +203,23 @@ def validate_consumed_asset_item(self): ) asset = self.get_asset_for_validation(d.asset) - self.validate_asset(asset) + + if asset.status in ("Draft", "Scrapped", "Sold", "Capitalized", "Decapitalized"): + frappe.throw( + _("Row #{0}: Consumed Asset {1} cannot be {2}").format(d.idx, asset.name, asset.status) + ) + + if asset.docstatus == 0: + frappe.throw(_("Row #{0}: Consumed Asset {1} cannot be Draft").format(d.idx, asset.name)) + elif asset.docstatus == 2: + frappe.throw(_("Row #{0}: Consumed Asset {1} cannot be cancelled").format(d.idx, asset.name)) + + if asset.company != self.company: + frappe.throw( + _("Row #{0}: Consumed Asset {1} does not belong to company {2}").format( + d.idx, asset.name, self.company + ) + ) def validate_service_item(self): for d in self.service_items: @@ -205,21 +254,12 @@ def validate_item(self, item): def get_asset_for_validation(self, asset): return frappe.db.get_value( - "Asset", asset, ["name", "item_code", "company", "status", "docstatus"], as_dict=1 + "Asset", + asset, + ["name", "item_code", "company", "status", "docstatus", "is_composite_asset"], + as_dict=1, ) - def validate_asset(self, asset): - if asset.status in ("Draft", "Scrapped", "Sold", "Capitalized", "Decapitalized"): - frappe.throw(_("Asset {0} is {1}").format(asset.name, asset.status)) - - if asset.docstatus == 0: - frappe.throw(_("Asset {0} is Draft").format(asset.name)) - if asset.docstatus == 2: - frappe.throw(_("Asset {0} is cancelled").format(asset.name)) - - if asset.company != self.company: - frappe.throw(_("Asset {0} does not belong to company {1}").format(asset.name, self.company)) - @frappe.whitelist() def set_warehouse_details(self): for d in self.get("stock_items"): @@ -485,16 +525,25 @@ def get_gl_entries_for_target_item(self, gl_entries, target_against, precision): ) def create_target_asset(self): + if ( + self.entry_type != "Capitalization" + or self.capitalization_method != "Create a new composite asset" + ): + return + total_target_asset_value = flt(self.total_value, self.precision("total_value")) + asset_doc = frappe.new_doc("Asset") asset_doc.company = self.company asset_doc.item_code = self.target_item_code - asset_doc.is_existing_asset = 1 + asset_doc.is_composite_asset = 1 asset_doc.location = self.target_asset_location asset_doc.available_for_use_date = self.posting_date asset_doc.purchase_date = self.posting_date asset_doc.gross_purchase_amount = total_target_asset_value asset_doc.purchase_receipt_amount = total_target_asset_value + asset_doc.purchase_receipt_amount = total_target_asset_value + asset_doc.capitalized_in = self.name asset_doc.flags.ignore_validate = True asset_doc.insert() @@ -510,6 +559,28 @@ def create_target_asset(self): ).format(get_link_to_form("Asset", asset_doc.name)) ) + def update_target_asset(self): + if ( + self.entry_type != "Capitalization" + or self.capitalization_method != "Choose a WIP composite asset" + ): + return + + total_target_asset_value = flt(self.total_value, self.precision("total_value")) + + asset_doc = frappe.get_doc("Asset", self.target_asset) + asset_doc.gross_purchase_amount = total_target_asset_value + asset_doc.purchase_receipt_amount = total_target_asset_value + asset_doc.capitalized_in = self.name + asset_doc.flags.ignore_validate = True + asset_doc.save() + + frappe.msgprint( + _( + "Asset {0} has been updated. Please set the depreciation details if any and submit it." + ).format(get_link_to_form("Asset", asset_doc.name)) + ) + def restore_consumed_asset_items(self): for item in self.asset_items: asset = frappe.get_doc("Asset", item.asset) @@ -568,6 +639,33 @@ def get_target_item_details(item_code=None, company=None): return out +@frappe.whitelist() +def get_target_asset_details(asset=None, company=None): + out = frappe._dict() + + # Get Asset Details + asset_details = frappe._dict() + if asset: + asset_details = frappe.db.get_value("Asset", asset, ["asset_name", "item_code"], as_dict=1) + if not asset_details: + frappe.throw(_("Asset {0} does not exist").format(asset)) + + # Re-set item code from Asset + out.target_item_code = asset_details.item_code + + # Set Asset Details + out.asset_name = asset_details.asset_name + + if asset_details.item_code: + out.target_fixed_asset_account = get_asset_category_account( + "fixed_asset_account", item=asset_details.item_code, company=company + ) + else: + out.target_fixed_asset_account = None + + return out + + @frappe.whitelist() def get_consumed_stock_item_details(args): if isinstance(args, string_types): @@ -716,3 +814,30 @@ def get_service_item_details(args): ) return out + + +@frappe.whitelist() +def get_items_tagged_to_wip_composite_asset(asset): + fields = [ + "item_code", + "item_name", + "batch_no", + "serial_no", + "stock_qty", + "stock_uom", + "warehouse", + "cost_center", + "qty", + "valuation_rate", + "amount", + ] + + pi_items = frappe.get_all( + "Purchase Invoice Item", filters={"wip_composite_asset": asset}, fields=fields + ) + + pr_items = frappe.get_all( + "Purchase Receipt Item", filters={"wip_composite_asset": asset}, fields=fields + ) + + return pi_items + pr_items diff --git a/erpnext/assets/doctype/asset_capitalization/test_asset_capitalization.py b/erpnext/assets/doctype/asset_capitalization/test_asset_capitalization.py index ead7abbf3406..60c62d49afab 100644 --- a/erpnext/assets/doctype/asset_capitalization/test_asset_capitalization.py +++ b/erpnext/assets/doctype/asset_capitalization/test_asset_capitalization.py @@ -50,6 +50,7 @@ def test_capitalization_with_perpetual_inventory(self): # Create and submit Asset Captitalization asset_capitalization = create_asset_capitalization( entry_type="Capitalization", + capitalization_method="Create a new composite asset", target_item_code="Macbook Pro", target_asset_location="Test Location", stock_qty=stock_qty, @@ -139,6 +140,7 @@ def test_capitalization_with_periodical_inventory(self): # Create and submit Asset Captitalization asset_capitalization = create_asset_capitalization( entry_type="Capitalization", + capitalization_method="Create a new composite asset", target_item_code="Macbook Pro", target_asset_location="Test Location", stock_qty=stock_qty, @@ -203,6 +205,77 @@ def test_capitalization_with_periodical_inventory(self): self.assertFalse(get_actual_gle_dict(asset_capitalization.name)) self.assertFalse(get_actual_sle_dict(asset_capitalization.name)) + def test_capitalization_with_wip_composite_asset(self): + company = "_Test Company with perpetual inventory" + set_depreciation_settings_in_company(company=company) + + stock_rate = 1000 + stock_qty = 2 + stock_amount = 2000 + + total_amount = 2000 + + wip_composite_asset = create_asset( + asset_name="Asset Capitalization WIP Composite Asset", + is_composite_asset=1, + warehouse="Stores - TCP1", + company=company, + ) + + # Create and submit Asset Captitalization + asset_capitalization = create_asset_capitalization( + entry_type="Capitalization", + capitalization_method="Choose a WIP composite asset", + target_asset=wip_composite_asset, + target_asset_location="Test Location", + stock_qty=stock_qty, + stock_rate=stock_rate, + service_expense_account="Expenses Included In Asset Valuation - TCP1", + company=company, + submit=1, + ) + + # Test Asset Capitalization values + self.assertEqual(asset_capitalization.entry_type, "Capitalization") + self.assertEqual(asset_capitalization.capitalization_method, "Choose a WIP composite asset") + self.assertEqual(asset_capitalization.target_qty, 1) + + self.assertEqual(asset_capitalization.stock_items[0].valuation_rate, stock_rate) + self.assertEqual(asset_capitalization.stock_items[0].amount, stock_amount) + self.assertEqual(asset_capitalization.stock_items_total, stock_amount) + + self.assertEqual(asset_capitalization.total_value, total_amount) + self.assertEqual(asset_capitalization.target_incoming_rate, total_amount) + + # Test Target Asset values + target_asset = frappe.get_doc("Asset", asset_capitalization.target_asset) + self.assertEqual(target_asset.gross_purchase_amount, total_amount) + self.assertEqual(target_asset.purchase_receipt_amount, total_amount) + + # Test General Ledger Entries + expected_gle = { + "_Test Fixed Asset - TCP1": 2000, + "_Test Warehouse - TCP1": -2000, + } + actual_gle = get_actual_gle_dict(asset_capitalization.name) + + self.assertEqual(actual_gle, expected_gle) + + # Test Stock Ledger Entries + expected_sle = { + ("Capitalization Source Stock Item", "_Test Warehouse - TCP1"): { + "actual_qty": -stock_qty, + "stock_value_difference": -stock_amount, + } + } + actual_sle = get_actual_sle_dict(asset_capitalization.name) + self.assertEqual(actual_sle, expected_sle) + + # Cancel Asset Capitalization and make test entries and status are reversed + asset_capitalization.cancel() + self.assertFalse(get_actual_gle_dict(asset_capitalization.name)) + self.assertFalse(get_actual_sle_dict(asset_capitalization.name)) + def test_decapitalization_with_depreciation(self): # Variables purchase_date = "2020-01-01" @@ -326,6 +399,7 @@ def create_asset_capitalization(**args): asset_capitalization.update( { "entry_type": args.entry_type or "Capitalization", + "capitalization_method": args.capitalization_method or None, "company": company, "posting_date": args.posting_date or now.strftime("%Y-%m-%d"), "posting_time": args.posting_time or now.strftime("%H:%M:%S.%f"), diff --git a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.js b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.js index 312c166f8b7f..8966fbcbb3c3 100644 --- a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.js +++ b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.js @@ -35,6 +35,12 @@ frappe.ui.form.on("Purchase Receipt", { } }); + frm.set_query("wip_composite_asset", "items", function() { + return { + filters: {'is_composite_asset': 1, 'docstatus': 0 } + } + }); + frm.set_query("taxes_and_charges", function() { return { filters: {'company': frm.doc.company } diff --git a/erpnext/stock/doctype/purchase_receipt_item/purchase_receipt_item.json b/erpnext/stock/doctype/purchase_receipt_item/purchase_receipt_item.json index 76f476edf8a0..4911523e7ed4 100644 --- a/erpnext/stock/doctype/purchase_receipt_item/purchase_receipt_item.json +++ b/erpnext/stock/doctype/purchase_receipt_item/purchase_receipt_item.json @@ -110,6 +110,7 @@ "manufacturer_part_no", "accounting_details_section", "expense_account", + "wip_composite_asset", "column_break_102", "provisional_expense_account", "accounting_dimensions_section", @@ -1018,12 +1019,18 @@ "no_copy": 1, "print_hide": 1, "read_only": 1 + }, + { + "fieldname": "wip_composite_asset", + "fieldtype": "Link", + "label": "WIP Composite Asset", + "options": "Asset" } ], "idx": 1, "istable": 1, "links": [], - "modified": "2023-07-04 17:22:02.830029", + "modified": "2023-10-03 21:11:50.547261", "modified_by": "Administrator", "module": "Stock", "name": "Purchase Receipt Item", @@ -1034,4 +1041,4 @@ "sort_field": "modified", "sort_order": "DESC", "states": [] -} +} \ No newline at end of file From cf9fc552f0783afd66ade15461bb89f39d4abff1 Mon Sep 17 00:00:00 2001 From: Frappe PR Bot Date: Wed, 4 Oct 2023 14:18:28 +0000 Subject: [PATCH 388/501] chore(release): Bumped to Version 14.43.0 # [14.43.0](https://github.com/frappe/erpnext/compare/v14.42.0...v14.43.0) (2023-10-04) ### Features * composite WIP asset (backport [#37352](https://github.com/frappe/erpnext/issues/37352)) ([#37360](https://github.com/frappe/erpnext/issues/37360)) ([6947686](https://github.com/frappe/erpnext/commit/69476861419f067ce6638063b3c2ca14ced04e7b)) --- erpnext/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/__init__.py b/erpnext/__init__.py index eb7d50ff1985..25e95bb009a6 100644 --- a/erpnext/__init__.py +++ b/erpnext/__init__.py @@ -3,7 +3,7 @@ import frappe -__version__ = "14.42.0" +__version__ = "14.43.0" def get_default_company(user=None): From 67a43c353c3ec49be8c6422c47c55c97fe0d8ad1 Mon Sep 17 00:00:00 2001 From: anandbaburajan Date: Wed, 4 Oct 2023 22:11:15 +0530 Subject: [PATCH 389/501] test: fixing test_capitalization_with_wip_composite_asset (cherry picked from commit 9468513d7cf692624127d235129589b94a58d4b5) --- .../doctype/asset_capitalization/test_asset_capitalization.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/assets/doctype/asset_capitalization/test_asset_capitalization.py b/erpnext/assets/doctype/asset_capitalization/test_asset_capitalization.py index 60c62d49afab..59b65ec3fd02 100644 --- a/erpnext/assets/doctype/asset_capitalization/test_asset_capitalization.py +++ b/erpnext/assets/doctype/asset_capitalization/test_asset_capitalization.py @@ -226,7 +226,7 @@ def test_capitalization_with_wip_composite_asset(self): asset_capitalization = create_asset_capitalization( entry_type="Capitalization", capitalization_method="Choose a WIP composite asset", - target_asset=wip_composite_asset, + target_asset=wip_composite_asset.name, target_asset_location="Test Location", stock_qty=stock_qty, stock_rate=stock_rate, From 587a965bdf1ba0fc37e9015a7d2e5da7c47db29d Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Fri, 29 Sep 2023 13:40:59 +0530 Subject: [PATCH 390/501] refactor: block Payment Entry as ref in JE from UI (cherry picked from commit d391e8150557004164a12666a5b5b251e7756042) --- .../accounts/doctype/journal_entry/journal_entry.js | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/erpnext/accounts/doctype/journal_entry/journal_entry.js b/erpnext/accounts/doctype/journal_entry/journal_entry.js index 199068529d42..7719f0e7c909 100644 --- a/erpnext/accounts/doctype/journal_entry/journal_entry.js +++ b/erpnext/accounts/doctype/journal_entry/journal_entry.js @@ -51,7 +51,15 @@ frappe.ui.form.on("Journal Entry", { }, __('Make')); } }, - + before_save: function(frm) { + if ((frm.doc.docstatus == 0) && (!frm.doc.is_system_generated)) { + let payment_entry_references = frm.doc.accounts.filter(elem => (elem.reference_type == "Payment Entry")); + if (payment_entry_references.length > 0) { + let rows = payment_entry_references.map(x => "#"+x.idx); + frappe.throw(__("Rows: {0} have 'Payment Entry' as reference_type. This should not be set manually.", [frappe.utils.comma_and(rows)])); + } + } + }, make_inter_company_journal_entry: function(frm) { var d = new frappe.ui.Dialog({ title: __("Select Company"), From 98a9007e9f629ead141e05261011fbefb0f17918 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Wed, 4 Oct 2023 15:23:29 +0530 Subject: [PATCH 391/501] refactor: introduce access_key field (cherry picked from commit 81591a34c29977f78825a687a6cf206a4eb7855a) # Conflicts: # erpnext/accounts/doctype/currency_exchange_settings/currency_exchange_settings.json --- .../currency_exchange_settings.json | 20 +++++++++++++++++++ .../currency_exchange_settings.py | 10 ++++++++++ 2 files changed, 30 insertions(+) diff --git a/erpnext/accounts/doctype/currency_exchange_settings/currency_exchange_settings.json b/erpnext/accounts/doctype/currency_exchange_settings/currency_exchange_settings.json index 7921fcc2b961..dede6bd033dd 100644 --- a/erpnext/accounts/doctype/currency_exchange_settings/currency_exchange_settings.json +++ b/erpnext/accounts/doctype/currency_exchange_settings/currency_exchange_settings.json @@ -8,6 +8,7 @@ "api_details_section", "service_provider", "api_endpoint", + "access_key", "url", "column_break_3", "help", @@ -77,12 +78,31 @@ "label": "Service Provider", "options": "frankfurter.app\nexchangerate.host\nCustom", "reqd": 1 +<<<<<<< HEAD +======= + }, + { + "default": "0", + "fieldname": "disabled", + "fieldtype": "Check", + "label": "Disabled" + }, + { + "depends_on": "eval:doc.service_provider == 'exchangerate.host';", + "fieldname": "access_key", + "fieldtype": "Data", + "label": "Access Key" +>>>>>>> 81591a34c2 (refactor: introduce access_key field) } ], "index_web_pages_for_search": 1, "issingle": 1, "links": [], +<<<<<<< HEAD "modified": "2022-01-10 15:51:14.521174", +======= + "modified": "2023-10-04 15:30:25.333860", +>>>>>>> 81591a34c2 (refactor: introduce access_key field) "modified_by": "Administrator", "module": "Accounts", "name": "Currency Exchange Settings", diff --git a/erpnext/accounts/doctype/currency_exchange_settings/currency_exchange_settings.py b/erpnext/accounts/doctype/currency_exchange_settings/currency_exchange_settings.py index d618c5ca1173..117d5ff21e84 100644 --- a/erpnext/accounts/doctype/currency_exchange_settings/currency_exchange_settings.py +++ b/erpnext/accounts/doctype/currency_exchange_settings/currency_exchange_settings.py @@ -18,11 +18,21 @@ def validate(self): def set_parameters_and_result(self): if self.service_provider == "exchangerate.host": + + if not self.access_key: + frappe.throw( + _("Access Key is required for Service Provider: {0}").format( + frappe.bold(self.service_provider) + ) + ) + self.set("result_key", []) self.set("req_params", []) self.api_endpoint = "https://api.exchangerate.host/convert" self.append("result_key", {"key": "result"}) + self.append("req_params", {"key": "access_key", "value": self.access_key}) + self.append("req_params", {"key": "amount", "value": "1"}) self.append("req_params", {"key": "date", "value": "{transaction_date}"}) self.append("req_params", {"key": "from", "value": "{from_currency}"}) self.append("req_params", {"key": "to", "value": "{to_currency}"}) From 04b8527ba8d2104078a5031d59eab5d9ce9b8198 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Wed, 4 Oct 2023 17:18:26 +0530 Subject: [PATCH 392/501] chore: refactor test case for exchangerate.host provider (cherry picked from commit c8e3dc6c4c6dbf248670fd553b39f6ba69232c2c) --- .../setup/doctype/currency_exchange/test_currency_exchange.py | 1 + 1 file changed, 1 insertion(+) diff --git a/erpnext/setup/doctype/currency_exchange/test_currency_exchange.py b/erpnext/setup/doctype/currency_exchange/test_currency_exchange.py index e3d281a56456..d4defdf88de6 100644 --- a/erpnext/setup/doctype/currency_exchange/test_currency_exchange.py +++ b/erpnext/setup/doctype/currency_exchange/test_currency_exchange.py @@ -121,6 +121,7 @@ def test_exchange_rate_via_exchangerate_host(self, mock_get): # Update Currency Exchange Rate settings = frappe.get_single("Currency Exchange Settings") settings.service_provider = "exchangerate.host" + settings.access_key = "12345667890" settings.save() # Update exchange From 1ca0516fe549320838620f14dc0e7611a43350e7 Mon Sep 17 00:00:00 2001 From: Devin Slauenwhite Date: Mon, 16 Jan 2023 10:30:10 -0500 Subject: [PATCH 393/501] feat: disable currency exchange api. (#33593) (cherry picked from commit 179a31ed5e0688d90e8b95da44d980a8cb7c0323) --- .../currency_exchange_settings.json | 9 ++++++++- erpnext/setup/utils.py | 5 +++++ 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/erpnext/accounts/doctype/currency_exchange_settings/currency_exchange_settings.json b/erpnext/accounts/doctype/currency_exchange_settings/currency_exchange_settings.json index 7921fcc2b961..c62b711f2c21 100644 --- a/erpnext/accounts/doctype/currency_exchange_settings/currency_exchange_settings.json +++ b/erpnext/accounts/doctype/currency_exchange_settings/currency_exchange_settings.json @@ -6,6 +6,7 @@ "engine": "InnoDB", "field_order": [ "api_details_section", + "disabled", "service_provider", "api_endpoint", "url", @@ -77,12 +78,18 @@ "label": "Service Provider", "options": "frankfurter.app\nexchangerate.host\nCustom", "reqd": 1 + }, + { + "default": "0", + "fieldname": "disabled", + "fieldtype": "Check", + "label": "Disabled" } ], "index_web_pages_for_search": 1, "issingle": 1, "links": [], - "modified": "2022-01-10 15:51:14.521174", + "modified": "2023-01-09 12:19:03.955906", "modified_by": "Administrator", "module": "Accounts", "name": "Currency Exchange Settings", diff --git a/erpnext/setup/utils.py b/erpnext/setup/utils.py index 54bd8c355d6b..bab57fe267ab 100644 --- a/erpnext/setup/utils.py +++ b/erpnext/setup/utils.py @@ -81,6 +81,11 @@ def get_exchange_rate(from_currency, to_currency, transaction_date=None, args=No if entries: return flt(entries[0].exchange_rate) + if frappe.get_cached_value( + "Currency Exchange Settings", "Currency Exchange Settings", "disabled" + ): + return 0.00 + try: cache = frappe.cache() key = "currency_exchange_rate_{0}:{1}:{2}".format(transaction_date, from_currency, to_currency) From c1d40a6bfa74c48bc10d493a464a1f2bdd8a9bbd Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Thu, 5 Oct 2023 13:13:11 +0530 Subject: [PATCH 394/501] fix: fetch company details for Lead based quotation (cherry picked from commit f388864fd5a3ce95e0349d7fd37fb3878834262c) --- erpnext/crm/doctype/lead/lead.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/crm/doctype/lead/lead.py b/erpnext/crm/doctype/lead/lead.py index 460974972c58..294c41b93416 100644 --- a/erpnext/crm/doctype/lead/lead.py +++ b/erpnext/crm/doctype/lead/lead.py @@ -382,7 +382,7 @@ def get_lead_details(lead, posting_date=None, company=None): } ) - set_address_details(out, lead, "Lead") + set_address_details(out, lead, "Lead", company=company) taxes_and_charges = set_taxes( None, From 63f45739e05d728e8844320ff0c7dbafa8660acf Mon Sep 17 00:00:00 2001 From: rohitwaghchaure Date: Fri, 6 Oct 2023 11:58:15 +0530 Subject: [PATCH 395/501] fix: incorrect status of the returned purchase receipt (#37300) --- erpnext/controllers/taxes_and_totals.py | 4 ++- .../public/js/controllers/taxes_and_totals.js | 10 ++++++- .../purchase_receipt/purchase_receipt.py | 4 +++ .../purchase_receipt/test_purchase_receipt.py | 26 +++++++++++++++++++ 4 files changed, 42 insertions(+), 2 deletions(-) diff --git a/erpnext/controllers/taxes_and_totals.py b/erpnext/controllers/taxes_and_totals.py index 62d4c5386828..95bf0e4688ef 100644 --- a/erpnext/controllers/taxes_and_totals.py +++ b/erpnext/controllers/taxes_and_totals.py @@ -190,7 +190,9 @@ def calculate_item_values(self): item.net_rate = item.rate - if not item.qty and self.doc.get("is_return"): + if ( + not item.qty and self.doc.get("is_return") and self.doc.get("doctype") != "Purchase Receipt" + ): item.amount = flt(-1 * item.rate, item.precision("amount")) elif not item.qty and self.doc.get("is_debit_note"): item.amount = flt(item.rate, item.precision("amount")) diff --git a/erpnext/public/js/controllers/taxes_and_totals.js b/erpnext/public/js/controllers/taxes_and_totals.js index 6f4e602abb60..b7ed22346b45 100644 --- a/erpnext/public/js/controllers/taxes_and_totals.js +++ b/erpnext/public/js/controllers/taxes_and_totals.js @@ -135,7 +135,15 @@ erpnext.taxes_and_totals = class TaxesAndTotals extends erpnext.payments { } else { // allow for '0' qty on Credit/Debit notes - let qty = item.qty || (me.frm.doc.is_debit_note ? 1 : -1); + let qty = flt(item.qty); + if (!qty) { + qty = (me.frm.doc.is_debit_note ? 1 : -1); + if (me.frm.doc.doctype !== "Purchase Receipt" && me.frm.doc.is_return === 1) { + // In case of Purchase Receipt, qty can be 0 if all items are rejected + qty = flt(item.qty); + } + } + item.net_amount = item.amount = flt(item.rate * qty, precision("amount", item)); } diff --git a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py index 1873efc711ae..4a651cd0d18b 100644 --- a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py +++ b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py @@ -958,6 +958,10 @@ def update_billing_percentage(pr_doc, update_modified=True, adjust_incoming_rate total_amount += total_billable_amount total_billed_amount += flt(item.billed_amt) + + if pr_doc.get("is_return") and not total_amount and total_billed_amount: + total_amount = total_billed_amount + if adjust_incoming_rate: adjusted_amt = 0.0 if item.billed_amt and item.amount: diff --git a/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py b/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py index 2f46809f49d7..82694a0b1929 100644 --- a/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py +++ b/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py @@ -2067,6 +2067,32 @@ def test_purchase_receipt_provisional_accounting(self): company.enable_provisional_accounting_for_non_stock_items = 0 company.save() + def test_purchase_return_status_with_debit_note(self): + pr = make_purchase_receipt(rejected_qty=10, received_qty=10, rate=100, do_not_save=1) + pr.items[0].qty = 0 + pr.items[0].stock_qty = 0 + pr.submit() + + return_pr = make_purchase_receipt( + is_return=1, + return_against=pr.name, + qty=0, + rejected_qty=10 * -1, + received_qty=10 * -1, + do_not_save=1, + ) + return_pr.items[0].qty = 0.0 + return_pr.items[0].stock_qty = 0.0 + return_pr.submit() + + self.assertEqual(return_pr.status, "To Bill") + + pi = make_purchase_invoice(return_pr.name) + pi.submit() + + return_pr.reload() + self.assertEqual(return_pr.status, "Completed") + def prepare_data_for_internal_transfer(): from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_internal_supplier From 4c337a6f44127e474503862f3ff0d718c4eeffad Mon Sep 17 00:00:00 2001 From: rohitwaghchaure Date: Fri, 6 Oct 2023 13:06:13 +0530 Subject: [PATCH 396/501] fix: added validation for the batch on stock reco (#37174) --- .../stock_reconciliation.py | 8 ++++ .../test_stock_reconciliation.py | 44 ++++++++++++++++++- 2 files changed, 50 insertions(+), 2 deletions(-) diff --git a/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py b/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py index bb1a9b362141..9f2cab233aff 100644 --- a/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py +++ b/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py @@ -167,6 +167,14 @@ def _get_msg(row_num, msg): if flt(row.valuation_rate) < 0: self.validation_messages.append(_get_msg(row_num, _("Negative Valuation Rate is not allowed"))) + if row.batch_no and frappe.get_cached_value("Batch", row.batch_no, "item") != row.item_code: + self.validation_messages.append( + _get_msg( + row_num, + _("Batch {0} does not belong to item {1}").format(bold(row.batch_no), bold(row.item_code)), + ) + ) + if row.qty and row.valuation_rate in ["", None]: row.valuation_rate = get_stock_balance( row.item_code, row.warehouse, self.posting_date, self.posting_time, with_valuation_rate=True diff --git a/erpnext/stock/doctype/stock_reconciliation/test_stock_reconciliation.py b/erpnext/stock/doctype/stock_reconciliation/test_stock_reconciliation.py index df6777bbe4ca..d1e5c5d345f1 100644 --- a/erpnext/stock/doctype/stock_reconciliation/test_stock_reconciliation.py +++ b/erpnext/stock/doctype/stock_reconciliation/test_stock_reconciliation.py @@ -604,9 +604,9 @@ def test_valid_batch(self): create_batch_item_with_batch("Testing Batch Item 1", "001") create_batch_item_with_batch("Testing Batch Item 2", "002") sr = create_stock_reconciliation( - item_code="Testing Batch Item 1", qty=1, rate=100, batch_no="002", do_not_submit=True + item_code="Testing Batch Item 1", qty=1, rate=100, batch_no="002", do_not_save=True ) - self.assertRaises(frappe.ValidationError, sr.submit) + self.assertRaises(frappe.ValidationError, sr.save) def test_serial_no_cancellation(self): from erpnext.stock.doctype.stock_entry.test_stock_entry import make_stock_entry @@ -916,6 +916,46 @@ def test_negative_stock_reco_for_batch(self): # Check if Negative Stock is blocked self.assertRaises(frappe.ValidationError, sr.submit) + def test_batch_item_validation(self): + from erpnext.stock.doctype.stock_entry.test_stock_entry import make_stock_entry + + item_code = self.make_item( + "Test Batch Item Original", + { + "is_stock_item": 1, + "has_batch_no": 1, + "batch_number_series": "BNS9.####", + "create_new_batch": 1, + }, + ).name + + sr = make_stock_entry( + item_code=item_code, + target="_Test Warehouse - _TC", + qty=100, + basic_rate=100, + posting_date=nowdate(), + ) + + new_item_code = self.make_item( + "Test Batch Item New 1", + { + "is_stock_item": 1, + "has_batch_no": 1, + }, + ).name + + sr = create_stock_reconciliation( + item_code=new_item_code, + warehouse="_Test Warehouse - _TC", + qty=10, + rate=100, + batch_no=sr.items[0].batch_no, + do_not_save=True, + ) + + self.assertRaises(frappe.ValidationError, sr.save) + def create_batch_item_with_batch(item_name, batch_id): batch_item_doc = create_item(item_name, is_stock_item=1) From 1480acabb0faeae61c7c055bb7d1e81877b87cfb Mon Sep 17 00:00:00 2001 From: rohitwaghchaure Date: Fri, 6 Oct 2023 17:55:32 +0530 Subject: [PATCH 397/501] feat: validate negative stock for inventory dimension (#37373) * feat: validate negative stock for inventory dimension * test: test case for validate negative stock for inv dimension --- .../inventory_dimension.js | 2 +- .../inventory_dimension.json | 14 +++- .../inventory_dimension.py | 5 +- .../test_inventory_dimension.py | 67 ++++++++++++++++++ .../stock_ledger_entry/stock_ledger_entry.py | 69 ++++++++++++++++++- .../stock_reconciliation.py | 39 ++++++++++- erpnext/stock/stock_ledger.py | 20 +++++- erpnext/stock/utils.py | 9 ++- 8 files changed, 215 insertions(+), 10 deletions(-) diff --git a/erpnext/stock/doctype/inventory_dimension/inventory_dimension.js b/erpnext/stock/doctype/inventory_dimension/inventory_dimension.js index 0310682a2c17..35d1c02719cc 100644 --- a/erpnext/stock/doctype/inventory_dimension/inventory_dimension.js +++ b/erpnext/stock/doctype/inventory_dimension/inventory_dimension.js @@ -37,7 +37,7 @@ frappe.ui.form.on('Inventory Dimension', { if (frm.doc.__onload && frm.doc.__onload.has_stock_ledger && frm.doc.__onload.has_stock_ledger.length) { let allow_to_edit_fields = ['disabled', 'fetch_from_parent', - 'type_of_transaction', 'condition', 'mandatory_depends_on']; + 'type_of_transaction', 'condition', 'mandatory_depends_on', 'validate_negative_stock']; frm.fields.forEach((field) => { if (!in_list(allow_to_edit_fields, field.df.fieldname)) { diff --git a/erpnext/stock/doctype/inventory_dimension/inventory_dimension.json b/erpnext/stock/doctype/inventory_dimension/inventory_dimension.json index eb6102a436e3..0e4055251f0c 100644 --- a/erpnext/stock/doctype/inventory_dimension/inventory_dimension.json +++ b/erpnext/stock/doctype/inventory_dimension/inventory_dimension.json @@ -17,6 +17,8 @@ "target_fieldname", "applicable_for_documents_tab", "apply_to_all_doctypes", + "column_break_niy2u", + "validate_negative_stock", "column_break_13", "document_type", "type_of_transaction", @@ -173,11 +175,21 @@ "fieldname": "reqd", "fieldtype": "Check", "label": "Mandatory" + }, + { + "fieldname": "column_break_niy2u", + "fieldtype": "Column Break" + }, + { + "default": "0", + "fieldname": "validate_negative_stock", + "fieldtype": "Check", + "label": "Validate Negative Stock" } ], "index_web_pages_for_search": 1, "links": [], - "modified": "2023-01-31 13:44:38.507698", + "modified": "2023-10-05 12:52:18.705431", "modified_by": "Administrator", "module": "Stock", "name": "Inventory Dimension", diff --git a/erpnext/stock/doctype/inventory_dimension/inventory_dimension.py b/erpnext/stock/doctype/inventory_dimension/inventory_dimension.py index 8bff4d514709..257d18fc33a2 100644 --- a/erpnext/stock/doctype/inventory_dimension/inventory_dimension.py +++ b/erpnext/stock/doctype/inventory_dimension/inventory_dimension.py @@ -60,6 +60,7 @@ def do_not_update_document(self): "fetch_from_parent", "type_of_transaction", "condition", + "validate_negative_stock", ] for field in frappe.get_meta("Inventory Dimension").fields: @@ -160,6 +161,7 @@ def get_dimension_fields(self, doctype=None): insert_after="inventory_dimension", options=self.reference_document, label=label, + search_index=1, reqd=self.reqd, mandatory_depends_on=self.mandatory_depends_on, ), @@ -255,7 +257,7 @@ def field_exists(doctype, fieldname) -> str or None: def get_inventory_documents( doctype=None, txt=None, searchfield=None, start=None, page_len=None, filters=None ): - and_filters = [["DocField", "parent", "not in", ["Batch", "Serial No"]]] + and_filters = [["DocField", "parent", "not in", ["Batch", "Serial No", "Item Price"]]] or_filters = [ ["DocField", "options", "in", ["Batch", "Serial No"]], ["DocField", "parent", "in", ["Putaway Rule"]], @@ -340,6 +342,7 @@ def get_inventory_dimensions(): fields=[ "distinct target_fieldname as fieldname", "reference_document as doctype", + "validate_negative_stock", ], filters={"disabled": 0}, ) diff --git a/erpnext/stock/doctype/inventory_dimension/test_inventory_dimension.py b/erpnext/stock/doctype/inventory_dimension/test_inventory_dimension.py index b1d7f8f00c6f..531bc3f109f1 100644 --- a/erpnext/stock/doctype/inventory_dimension/test_inventory_dimension.py +++ b/erpnext/stock/doctype/inventory_dimension/test_inventory_dimension.py @@ -414,6 +414,53 @@ def test_inter_transfer_return_against_inventory_dimension(self): else: self.assertEqual(d.store, "Inter Transfer Store 2") + def test_validate_negative_stock_for_inventory_dimension(self): + frappe.local.inventory_dimensions = {} + item_code = "Test Negative Inventory Dimension Item" + frappe.db.set_single_value("Stock Settings", "allow_negative_stock", 1) + create_item(item_code) + + inv_dimension = create_inventory_dimension( + apply_to_all_doctypes=1, + dimension_name="Inv Site", + reference_document="Inv Site", + document_type="Inv Site", + validate_negative_stock=1, + ) + + warehouse = create_warehouse("Negative Stock Warehouse") + doc = make_stock_entry(item_code=item_code, target=warehouse, qty=10, do_not_submit=True) + + doc.items[0].to_inv_site = "Site 1" + doc.submit() + + site_name = frappe.get_all( + "Stock Ledger Entry", filters={"voucher_no": doc.name, "is_cancelled": 0}, fields=["inv_site"] + )[0].inv_site + + self.assertEqual(site_name, "Site 1") + + doc = make_stock_entry(item_code=item_code, source=warehouse, qty=100, do_not_submit=True) + + doc.items[0].inv_site = "Site 1" + self.assertRaises(frappe.ValidationError, doc.submit) + + inv_dimension.reload() + inv_dimension.db_set("validate_negative_stock", 0) + frappe.local.inventory_dimensions = {} + + doc = make_stock_entry(item_code=item_code, source=warehouse, qty=100, do_not_submit=True) + + doc.items[0].inv_site = "Site 1" + doc.submit() + self.assertEqual(doc.docstatus, 1) + + site_name = frappe.get_all( + "Stock Ledger Entry", filters={"voucher_no": doc.name, "is_cancelled": 0}, fields=["inv_site"] + )[0].inv_site + + self.assertEqual(site_name, "Site 1") + def get_voucher_sl_entries(voucher_no, fields): return frappe.get_all( @@ -504,6 +551,26 @@ def prepare_test_data(): } ).insert(ignore_permissions=True) + if not frappe.db.exists("DocType", "Inv Site"): + frappe.get_doc( + { + "doctype": "DocType", + "name": "Inv Site", + "module": "Stock", + "custom": 1, + "naming_rule": "By fieldname", + "autoname": "field:site_name", + "fields": [{"label": "Site Name", "fieldname": "site_name", "fieldtype": "Data"}], + "permissions": [ + {"role": "System Manager", "permlevel": 0, "read": 1, "write": 1, "create": 1, "delete": 1} + ], + } + ).insert(ignore_permissions=True) + + for site in ["Site 1", "Site 2"]: + if not frappe.db.exists("Inv Site", site): + frappe.get_doc({"doctype": "Inv Site", "site_name": site}).insert(ignore_permissions=True) + def create_inventory_dimension(**args): args = frappe._dict(args) diff --git a/erpnext/stock/doctype/stock_ledger_entry/stock_ledger_entry.py b/erpnext/stock/doctype/stock_ledger_entry/stock_ledger_entry.py index 052f7781c130..921b04aab8c8 100644 --- a/erpnext/stock/doctype/stock_ledger_entry/stock_ledger_entry.py +++ b/erpnext/stock/doctype/stock_ledger_entry/stock_ledger_entry.py @@ -5,13 +5,15 @@ from datetime import date import frappe -from frappe import _ +from frappe import _, bold from frappe.core.doctype.role.role import get_users from frappe.model.document import Document -from frappe.utils import add_days, cint, formatdate, get_datetime, getdate +from frappe.utils import add_days, cint, flt, formatdate, get_datetime, getdate from erpnext.accounts.utils import get_fiscal_year from erpnext.controllers.item_variant import ItemTemplateCannotHaveStock +from erpnext.stock.doctype.inventory_dimension.inventory_dimension import get_inventory_dimensions +from erpnext.stock.stock_ledger import get_previous_sle class StockFreezeError(frappe.ValidationError): @@ -48,6 +50,69 @@ def validate(self): self.validate_and_set_fiscal_year() self.block_transactions_against_group_warehouse() self.validate_with_last_transaction_posting_time() + self.validate_inventory_dimension_negative_stock() + + def validate_inventory_dimension_negative_stock(self): + extra_cond = "" + kwargs = {} + + dimensions = self._get_inventory_dimensions() + if not dimensions: + return + + for dimension, values in dimensions.items(): + kwargs[dimension] = values.get("value") + extra_cond += f" and {dimension} = %({dimension})s" + + kwargs.update( + { + "item_code": self.item_code, + "warehouse": self.warehouse, + "posting_date": self.posting_date, + "posting_time": self.posting_time, + "company": self.company, + } + ) + + sle = get_previous_sle(kwargs, extra_cond=extra_cond) + if sle: + flt_precision = cint(frappe.db.get_default("float_precision")) or 2 + diff = sle.qty_after_transaction + flt(self.actual_qty) + diff = flt(diff, flt_precision) + if diff < 0 and abs(diff) > 0.0001: + self.throw_validation_error(diff, dimensions) + + def throw_validation_error(self, diff, dimensions): + dimension_msg = _(", with the inventory {0}: {1}").format( + "dimensions" if len(dimensions) > 1 else "dimension", + ", ".join(f"{bold(d.doctype)} ({d.value})" for k, d in dimensions.items()), + ) + + msg = _( + "{0} units of {1} are required in {2}{3}, on {4} {5} for {6} to complete the transaction." + ).format( + abs(diff), + frappe.get_desk_link("Item", self.item_code), + frappe.get_desk_link("Warehouse", self.warehouse), + dimension_msg, + self.posting_date, + self.posting_time, + frappe.get_desk_link(self.voucher_type, self.voucher_no), + ) + + frappe.throw(msg, title=_("Inventory Dimension Negative Stock")) + + def _get_inventory_dimensions(self): + inv_dimensions = get_inventory_dimensions() + inv_dimension_dict = {} + for dimension in inv_dimensions: + if not dimension.get("validate_negative_stock") or not self.get(dimension.fieldname): + continue + + dimension["value"] = self.get(dimension.fieldname) + inv_dimension_dict.setdefault(dimension.fieldname, dimension) + + return inv_dimension_dict def on_submit(self): self.check_stock_frozen_date() diff --git a/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py b/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py index 9f2cab233aff..e469291eac99 100644 --- a/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py +++ b/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py @@ -12,6 +12,7 @@ from erpnext.accounts.utils import get_company_default from erpnext.controllers.stock_controller import StockController from erpnext.stock.doctype.batch.batch import get_batch_qty +from erpnext.stock.doctype.inventory_dimension.inventory_dimension import get_inventory_dimensions from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos from erpnext.stock.utils import get_stock_balance @@ -45,10 +46,22 @@ def validate(self): self.clean_serial_nos() self.set_total_qty_and_amount() self.validate_putaway_capacity() + self.validate_inventory_dimension() if self._action == "submit": self.make_batches("warehouse") + def validate_inventory_dimension(self): + dimensions = get_inventory_dimensions() + for dimension in dimensions: + for row in self.items: + if not row.batch_no and row.current_qty and row.get(dimension.get("fieldname")): + frappe.throw( + _( + "Row #{0}: You cannot use the inventory dimension '{1}' in Stock Reconciliation to modify the quantity or valuation rate. Stock reconciliation with inventory dimensions is intended solely for performing opening entries." + ).format(row.idx, bold(dimension.get("doctype"))) + ) + def on_submit(self): self.update_stock_ledger() self.make_gl_entries() @@ -70,8 +83,19 @@ def remove_items_with_no_change(self): self.difference_amount = 0.0 def _changed(item): + inventory_dimensions_dict = {} + if not item.batch_no and not item.serial_no: + for dimension in get_inventory_dimensions(): + if item.get(dimension.get("fieldname")): + inventory_dimensions_dict[dimension.get("fieldname")] = item.get(dimension.get("fieldname")) + item_dict = get_stock_balance_for( - item.item_code, item.warehouse, self.posting_date, self.posting_time, batch_no=item.batch_no + item.item_code, + item.warehouse, + self.posting_date, + self.posting_time, + batch_no=item.batch_no, + inventory_dimensions_dict=inventory_dimensions_dict, ) if ( @@ -423,6 +447,12 @@ def get_sle_for_items(self, row, serial_nos=None): if not row.batch_no: data.qty_after_transaction = flt(row.qty, row.precision("qty")) + dimensions = get_inventory_dimensions() + has_dimensions = False + for dimension in dimensions: + if row.get(dimension.get("fieldname")): + has_dimensions = True + if self.docstatus == 2 and not row.batch_no: if row.current_qty: data.actual_qty = -1 * row.current_qty @@ -437,6 +467,11 @@ def get_sle_for_items(self, row, serial_nos=None): data.valuation_rate = flt(row.valuation_rate) data.stock_value_difference = -1 * flt(row.amount_difference) + elif self.docstatus == 1 and has_dimensions and not row.batch_no: + data.actual_qty = row.qty + data.qty_after_transaction = 0.0 + data.incoming_rate = flt(row.valuation_rate) + self.update_inventory_dimensions(row, data) return data @@ -825,6 +860,7 @@ def get_stock_balance_for( posting_time, batch_no: Optional[str] = None, with_valuation_rate: bool = True, + inventory_dimensions_dict=None, ): frappe.has_permission("Stock Reconciliation", "write", throw=True) @@ -853,6 +889,7 @@ def get_stock_balance_for( posting_time, with_valuation_rate=with_valuation_rate, with_serial_no=has_serial_no, + inventory_dimensions_dict=inventory_dimensions_dict, ) if has_serial_no: diff --git a/erpnext/stock/stock_ledger.py b/erpnext/stock/stock_ledger.py index 8cd6325c9d33..108d36a490d9 100644 --- a/erpnext/stock/stock_ledger.py +++ b/erpnext/stock/stock_ledger.py @@ -13,6 +13,7 @@ import erpnext from erpnext.stock.doctype.bin.bin import update_qty as update_bin_qty +from erpnext.stock.doctype.inventory_dimension.inventory_dimension import get_inventory_dimensions from erpnext.stock.utils import ( get_incoming_outgoing_rate_for_cancel, get_or_make_bin, @@ -582,6 +583,13 @@ def process_sle(self, sle): ): sle.outgoing_rate = get_incoming_rate_for_inter_company_transfer(sle) + dimensions = get_inventory_dimensions() + has_dimensions = False + if dimensions: + for dimension in dimensions: + if sle.get(dimension.get("fieldname")): + has_dimensions = True + if get_serial_nos(sle.serial_no): self.get_serialized_values(sle) self.wh_data.qty_after_transaction += flt(sle.actual_qty) @@ -596,7 +604,7 @@ def process_sle(self, sle): ): self.update_batched_values(sle) else: - if sle.voucher_type == "Stock Reconciliation" and not sle.batch_no: + if sle.voucher_type == "Stock Reconciliation" and not sle.batch_no and not has_dimensions: # assert self.wh_data.valuation_rate = sle.valuation_rate self.wh_data.qty_after_transaction = sle.qty_after_transaction @@ -1186,7 +1194,7 @@ def get_previous_sle_of_current_voucher(args, operator="<", exclude_current_vouc return sle[0] if sle else frappe._dict() -def get_previous_sle(args, for_update=False): +def get_previous_sle(args, for_update=False, extra_cond=None): """ get the last sle on or before the current time-bucket, to get actual qty before transaction, this function @@ -1201,7 +1209,9 @@ def get_previous_sle(args, for_update=False): } """ args["name"] = args.get("sle", None) or "" - sle = get_stock_ledger_entries(args, "<=", "desc", "limit 1", for_update=for_update) + sle = get_stock_ledger_entries( + args, "<=", "desc", "limit 1", for_update=for_update, extra_cond=extra_cond + ) return sle and sle[0] or {} @@ -1213,6 +1223,7 @@ def get_stock_ledger_entries( for_update=False, debug=False, check_serial_no=True, + extra_cond=None, ): """get stock ledger entries filtered by specific posting datetime conditions""" conditions = " and timestamp(posting_date, posting_time) {0} timestamp(%(posting_date)s, %(posting_time)s)".format( @@ -1250,6 +1261,9 @@ def get_stock_ledger_entries( if operator in (">", "<=") and previous_sle.get("name"): conditions += " and name!=%(name)s" + if extra_cond: + conditions += f"{extra_cond}" + return frappe.db.sql( """ select *, timestamp(posting_date, posting_time) as "timestamp" diff --git a/erpnext/stock/utils.py b/erpnext/stock/utils.py index a7e37d5961aa..9f654fc6632b 100644 --- a/erpnext/stock/utils.py +++ b/erpnext/stock/utils.py @@ -94,6 +94,7 @@ def get_stock_balance( posting_time=None, with_valuation_rate=False, with_serial_no=False, + inventory_dimensions_dict=None, ): """Returns stock balance quantity at given warehouse on given posting date or current date. @@ -113,7 +114,13 @@ def get_stock_balance( "posting_time": posting_time, } - last_entry = get_previous_sle(args) + extra_cond = "" + if inventory_dimensions_dict: + for field, value in inventory_dimensions_dict.items(): + args[field] = value + extra_cond += f" and {field} = %({field})s" + + last_entry = get_previous_sle(args, extra_cond=extra_cond) if with_valuation_rate: if with_serial_no: From 15d2024b8e8349562cae0f979d1d483d4d969d32 Mon Sep 17 00:00:00 2001 From: Gursheen Anand Date: Fri, 29 Sep 2023 11:12:19 +0530 Subject: [PATCH 398/501] fix: ageing summary in AR (cherry picked from commit d9eb44e62d9307f4100ca8e8b500309ca6da6922) --- .../process_statement_of_accounts.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.py b/erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.py index 3f8731afe651..1352c892a56b 100644 --- a/erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.py +++ b/erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.py @@ -100,7 +100,8 @@ def set_ageing(doc, entry): "range2": 60, "range3": 90, "range4": 120, - "customer": entry.customer, + "party_type": "Customer", + "party": entry.customer, } ) col1, ageing = get_ageing(ageing_filters) From c29eab12dfe19cc3adfb41d9a0ffe287bd7e1164 Mon Sep 17 00:00:00 2001 From: Gursheen Anand Date: Fri, 29 Sep 2023 14:34:43 +0530 Subject: [PATCH 399/501] refactor: separate function for statement dict (cherry picked from commit 67f878ff8c49f0f2e3704aa7264b35fb3418a36e) # Conflicts: # erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.py --- .../process_statement_of_accounts.py | 24 +++++++++++++++++-- 1 file changed, 22 insertions(+), 2 deletions(-) diff --git a/erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.py b/erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.py index 1352c892a56b..cc6a00c1954f 100644 --- a/erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.py +++ b/erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.py @@ -47,6 +47,20 @@ def validate(self): def get_report_pdf(doc, consolidated=True): + statement_dict = get_statement_dict(doc) + if not bool(statement_dict): + return False + elif consolidated: + delimiter = '
' if doc.include_break else "" + result = delimiter.join(list(statement_dict.values())) + return get_pdf(result, {"orientation": doc.orientation}) + else: + for customer, statement_html in statement_dict.items(): + statement_dict[customer] = get_pdf(statement_html, {"orientation": doc.orientation}) + return statement_dict + + +def get_statement_dict(doc, get_statement_dict=False): statement_dict = {} ageing = "" @@ -77,8 +91,11 @@ def get_report_pdf(doc, consolidated=True): if not res: continue - statement_dict[entry.customer] = get_html(doc, filters, entry, col, res, ageing) + statement_dict[entry.customer] = ( + [res, ageing] if get_statement_dict else get_html(doc, filters, entry, col, res, ageing) + ) +<<<<<<< HEAD if not bool(statement_dict): return False elif consolidated: @@ -88,6 +105,9 @@ def get_report_pdf(doc, consolidated=True): for customer, statement_html in statement_dict.items(): statement_dict[customer] = get_pdf(statement_html, {"orientation": doc.orientation}) return statement_dict +======= + return statement_dict +>>>>>>> 67f878ff8c (refactor: separate function for statement dict) def set_ageing(doc, entry): @@ -101,7 +121,7 @@ def set_ageing(doc, entry): "range3": 90, "range4": 120, "party_type": "Customer", - "party": entry.customer, + "party": [entry.customer], } ) col1, ageing = get_ageing(ageing_filters) From 24b1100c8f007bb03e9fd89010ffc7487d30b71c Mon Sep 17 00:00:00 2001 From: Gursheen Anand Date: Fri, 29 Sep 2023 14:35:43 +0530 Subject: [PATCH 400/501] test: process soa for gl and ar (cherry picked from commit 644e25e587affcf2a5fb821f148e468b154c261a) --- .../test_process_statement_of_accounts.py | 100 +++++++++++++++--- 1 file changed, 84 insertions(+), 16 deletions(-) diff --git a/erpnext/accounts/doctype/process_statement_of_accounts/test_process_statement_of_accounts.py b/erpnext/accounts/doctype/process_statement_of_accounts/test_process_statement_of_accounts.py index fb0d8d152f00..a3a74df40291 100644 --- a/erpnext/accounts/doctype/process_statement_of_accounts/test_process_statement_of_accounts.py +++ b/erpnext/accounts/doctype/process_statement_of_accounts/test_process_statement_of_accounts.py @@ -4,39 +4,107 @@ import unittest import frappe +from frappe.tests.utils import FrappeTestCase from frappe.utils import add_days, getdate, today from erpnext.accounts.doctype.process_statement_of_accounts.process_statement_of_accounts import ( + get_statement_dict, send_emails, ) from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice +from erpnext.accounts.test.accounts_mixin import AccountsTestMixin -class TestProcessStatementOfAccounts(unittest.TestCase): +class TestProcessStatementOfAccounts(AccountsTestMixin, FrappeTestCase): def setUp(self): + self.create_company() + self.create_customer() + self.create_customer(customer_name="Other Customer") + self.clear_old_entries() self.si = create_sales_invoice() - self.process_soa = create_process_soa() + create_sales_invoice(customer="Other Customer") + + def test_process_soa_for_gl(self): + """Tests the utils for Statement of Accounts(General Ledger)""" + process_soa = create_process_soa( + name="_Test Process SOA for GL", + customers=[{"customer": "_Test Customer"}, {"customer": "Other Customer"}], + ) + statement_dict = get_statement_dict(process_soa, get_statement_dict=True) + + # Checks if the statements are filtered based on the Customer + self.assertIn("Other Customer", statement_dict) + self.assertIn("_Test Customer", statement_dict) + + # Checks if the correct number of receivable entries exist + # 3 rows for opening and closing and 1 row for SI + receivable_entries = statement_dict["_Test Customer"][0] + self.assertEqual(len(receivable_entries), 4) + + # Checks the amount for the receivable entry + self.assertEqual(receivable_entries[1].voucher_no, self.si.name) + self.assertEqual(receivable_entries[1].balance, 100) + + def test_process_soa_for_ar(self): + """Tests the utils for Statement of Accounts(Accounts Receivable)""" + process_soa = create_process_soa(name="_Test Process SOA for AR", report="Accounts Receivable") + statement_dict = get_statement_dict(process_soa, get_statement_dict=True) + + # Checks if the statements are filtered based on the Customer + self.assertNotIn("Other Customer", statement_dict) + self.assertIn("_Test Customer", statement_dict) + + # Checks if the correct number of receivable entries exist + receivable_entries = statement_dict["_Test Customer"][0] + self.assertEqual(len(receivable_entries), 1) + + # Checks the amount for the receivable entry + self.assertEqual(receivable_entries[0].voucher_no, self.si.name) + self.assertEqual(receivable_entries[0].total_due, 100) + + # Checks the ageing summary for AR + ageing_summary = statement_dict["_Test Customer"][1][0] + expected_summary = frappe._dict( + range1=100, + range2=0, + range3=0, + range4=0, + range5=0, + ) + self.check_ageing_summary(ageing_summary, expected_summary) def test_auto_email_for_process_soa_ar(self): - send_emails(self.process_soa.name, from_scheduler=True) - self.process_soa.load_from_db() - self.assertEqual(self.process_soa.posting_date, getdate(add_days(today(), 7))) + process_soa = create_process_soa( + name="_Test Process SOA", enable_auto_email=1, report="Accounts Receivable" + ) + send_emails(process_soa.name, from_scheduler=True) + process_soa.load_from_db() + self.assertEqual(process_soa.posting_date, getdate(add_days(today(), 7))) + + def check_ageing_summary(self, ageing, expected_ageing): + for age_range in expected_ageing: + self.assertEqual(expected_ageing[age_range], ageing.get(age_range)) def tearDown(self): - frappe.delete_doc_if_exists("Process Statement Of Accounts", "Test Process SOA") + frappe.db.rollback() -def create_process_soa(): - frappe.delete_doc_if_exists("Process Statement Of Accounts", "Test Process SOA") +def create_process_soa(**args): + args = frappe._dict(args) + frappe.delete_doc_if_exists("Process Statement Of Accounts", args.name) process_soa = frappe.new_doc("Process Statement Of Accounts") - soa_dict = { - "name": "Test Process SOA", - "company": "_Test Company", - } + soa_dict = frappe._dict( + name=args.name, + company=args.company or "_Test Company", + customers=args.customers or [{"customer": "_Test Customer"}], + enable_auto_email=1 if args.enable_auto_email else 0, + frequency=args.frequency or "Weekly", + report=args.report or "General Ledger", + from_date=args.from_date or getdate(today()), + to_date=args.to_date or getdate(today()), + posting_date=args.posting_date or getdate(today()), + include_ageing=1, + ) process_soa.update(soa_dict) - process_soa.set("customers", [{"customer": "_Test Customer"}]) - process_soa.enable_auto_email = 1 - process_soa.frequency = "Weekly" - process_soa.report = "Accounts Receivable" process_soa.save() return process_soa From 3f59518d01bb4fa81869fb10580f4da5bcf4bf19 Mon Sep 17 00:00:00 2001 From: Gursheen Kaur Anand <40693548+GursheenK@users.noreply.github.com> Date: Sat, 7 Oct 2023 15:21:50 +0530 Subject: [PATCH 401/501] chore: resolve conflicts --- .../process_statement_of_accounts.py | 14 +------------- 1 file changed, 1 insertion(+), 13 deletions(-) diff --git a/erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.py b/erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.py index cc6a00c1954f..16ca7d457fb2 100644 --- a/erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.py +++ b/erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.py @@ -94,20 +94,8 @@ def get_statement_dict(doc, get_statement_dict=False): statement_dict[entry.customer] = ( [res, ageing] if get_statement_dict else get_html(doc, filters, entry, col, res, ageing) ) - -<<<<<<< HEAD - if not bool(statement_dict): - return False - elif consolidated: - result = "".join(list(statement_dict.values())) - return get_pdf(result, {"orientation": doc.orientation}) - else: - for customer, statement_html in statement_dict.items(): - statement_dict[customer] = get_pdf(statement_html, {"orientation": doc.orientation}) - return statement_dict -======= + return statement_dict ->>>>>>> 67f878ff8c (refactor: separate function for statement dict) def set_ageing(doc, entry): From 77d719af6e146ff243c264fabf1cc329c393c293 Mon Sep 17 00:00:00 2001 From: Gursheen Kaur Anand <40693548+GursheenK@users.noreply.github.com> Date: Sat, 7 Oct 2023 15:40:27 +0530 Subject: [PATCH 402/501] chore: linting issues --- .../process_statement_of_accounts.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.py b/erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.py index 16ca7d457fb2..b7d6827f64c5 100644 --- a/erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.py +++ b/erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.py @@ -94,7 +94,7 @@ def get_statement_dict(doc, get_statement_dict=False): statement_dict[entry.customer] = ( [res, ageing] if get_statement_dict else get_html(doc, filters, entry, col, res, ageing) ) - + return statement_dict From e58b3b11e9aee1da3e8610d19112cdb30801a9fb Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Sun, 8 Oct 2023 13:16:28 +0530 Subject: [PATCH 403/501] fix: exception on exporting errored rows (cherry picked from commit d3c6000904387f64605fdb316412f1ee1948bd86) --- .../doctype/bank_statement_import/bank_statement_import.js | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/erpnext/accounts/doctype/bank_statement_import/bank_statement_import.js b/erpnext/accounts/doctype/bank_statement_import/bank_statement_import.js index 04af32346bbd..a70af7a90e3d 100644 --- a/erpnext/accounts/doctype/bank_statement_import/bank_statement_import.js +++ b/erpnext/accounts/doctype/bank_statement_import/bank_statement_import.js @@ -352,10 +352,11 @@ frappe.ui.form.on("Bank Statement Import", { export_errored_rows(frm) { open_url_post( - "/api/method/frappe.core.doctype.data_import.data_import.download_errored_template", + "/api/method/erpnext.accounts.doctype.bank_statement_import.bank_statement_import.download_errored_template", { data_import_name: frm.doc.name, - } + }, + true ); }, From ae8355c95391d0b4223a8c17a1f0fa786f45ed96 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Tue, 22 Aug 2023 10:26:42 +0530 Subject: [PATCH 404/501] feat: introduce unreconcile doctype (cherry picked from commit dc7162329594337ee5b869f42d44fb28d8eaf2e3) --- .../unreconcile_payment_entries/__init__.py | 0 .../unreconcile_payment_entries.json | 71 ++++++++++++++++++ .../unreconcile_payment_entries.py | 9 +++ .../doctype/unreconcile_payments/__init__.py | 0 .../test_unreconcile_payments.py | 9 +++ .../unreconcile_payments.js | 8 +++ .../unreconcile_payments.json | 72 +++++++++++++++++++ .../unreconcile_payments.py | 9 +++ 8 files changed, 178 insertions(+) create mode 100644 erpnext/accounts/doctype/unreconcile_payment_entries/__init__.py create mode 100644 erpnext/accounts/doctype/unreconcile_payment_entries/unreconcile_payment_entries.json create mode 100644 erpnext/accounts/doctype/unreconcile_payment_entries/unreconcile_payment_entries.py create mode 100644 erpnext/accounts/doctype/unreconcile_payments/__init__.py create mode 100644 erpnext/accounts/doctype/unreconcile_payments/test_unreconcile_payments.py create mode 100644 erpnext/accounts/doctype/unreconcile_payments/unreconcile_payments.js create mode 100644 erpnext/accounts/doctype/unreconcile_payments/unreconcile_payments.json create mode 100644 erpnext/accounts/doctype/unreconcile_payments/unreconcile_payments.py diff --git a/erpnext/accounts/doctype/unreconcile_payment_entries/__init__.py b/erpnext/accounts/doctype/unreconcile_payment_entries/__init__.py new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/erpnext/accounts/doctype/unreconcile_payment_entries/unreconcile_payment_entries.json b/erpnext/accounts/doctype/unreconcile_payment_entries/unreconcile_payment_entries.json new file mode 100644 index 000000000000..5beb39d03422 --- /dev/null +++ b/erpnext/accounts/doctype/unreconcile_payment_entries/unreconcile_payment_entries.json @@ -0,0 +1,71 @@ +{ + "actions": [], + "allow_rename": 1, + "creation": "2023-08-22 10:28:10.196712", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "voucher_type", + "voucher_no", + "reference_type", + "reference_name", + "allocated_amount", + "unlinked" + ], + "fields": [ + { + "fieldname": "voucher_type", + "fieldtype": "Link", + "in_list_view": 1, + "label": "Voucher Type", + "options": "DocType" + }, + { + "fieldname": "voucher_no", + "fieldtype": "Dynamic Link", + "in_list_view": 1, + "label": "Voucher No", + "options": "voucher_type" + }, + { + "fieldname": "reference_type", + "fieldtype": "Link", + "in_list_view": 1, + "label": "Reference Type", + "options": "DocType" + }, + { + "fieldname": "reference_name", + "fieldtype": "Dynamic Link", + "in_list_view": 1, + "label": "Reference Name", + "options": "reference_type" + }, + { + "fieldname": "allocated_amount", + "fieldtype": "Int", + "in_list_view": 1, + "label": "Allocated Amount" + }, + { + "default": "0", + "fieldname": "unlinked", + "fieldtype": "Check", + "in_list_view": 1, + "label": "Unlinked" + } + ], + "index_web_pages_for_search": 1, + "istable": 1, + "links": [], + "modified": "2023-08-22 11:22:20.381079", + "modified_by": "Administrator", + "module": "Accounts", + "name": "Unreconcile Payment Entries", + "owner": "Administrator", + "permissions": [], + "sort_field": "modified", + "sort_order": "DESC", + "states": [] +} \ No newline at end of file diff --git a/erpnext/accounts/doctype/unreconcile_payment_entries/unreconcile_payment_entries.py b/erpnext/accounts/doctype/unreconcile_payment_entries/unreconcile_payment_entries.py new file mode 100644 index 000000000000..c41545c2685b --- /dev/null +++ b/erpnext/accounts/doctype/unreconcile_payment_entries/unreconcile_payment_entries.py @@ -0,0 +1,9 @@ +# Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and contributors +# For license information, please see license.txt + +# import frappe +from frappe.model.document import Document + + +class UnreconcilePaymentEntries(Document): + pass diff --git a/erpnext/accounts/doctype/unreconcile_payments/__init__.py b/erpnext/accounts/doctype/unreconcile_payments/__init__.py new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/erpnext/accounts/doctype/unreconcile_payments/test_unreconcile_payments.py b/erpnext/accounts/doctype/unreconcile_payments/test_unreconcile_payments.py new file mode 100644 index 000000000000..85af5211aefb --- /dev/null +++ b/erpnext/accounts/doctype/unreconcile_payments/test_unreconcile_payments.py @@ -0,0 +1,9 @@ +# Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and Contributors +# See license.txt + +# import frappe +from frappe.tests.utils import FrappeTestCase + + +class TestUnreconcilePayments(FrappeTestCase): + pass diff --git a/erpnext/accounts/doctype/unreconcile_payments/unreconcile_payments.js b/erpnext/accounts/doctype/unreconcile_payments/unreconcile_payments.js new file mode 100644 index 000000000000..d6670037d466 --- /dev/null +++ b/erpnext/accounts/doctype/unreconcile_payments/unreconcile_payments.js @@ -0,0 +1,8 @@ +// Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and contributors +// For license information, please see license.txt + +// frappe.ui.form.on("Unreconcile Payments", { +// refresh(frm) { + +// }, +// }); diff --git a/erpnext/accounts/doctype/unreconcile_payments/unreconcile_payments.json b/erpnext/accounts/doctype/unreconcile_payments/unreconcile_payments.json new file mode 100644 index 000000000000..c182a63b654e --- /dev/null +++ b/erpnext/accounts/doctype/unreconcile_payments/unreconcile_payments.json @@ -0,0 +1,72 @@ +{ + "actions": [], + "allow_rename": 1, + "autoname": "format:UNREC-{#####}", + "creation": "2023-08-22 10:26:34.421423", + "default_view": "List", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "company", + "entries", + "amended_from" + ], + "fields": [ + { + "fieldname": "amended_from", + "fieldtype": "Link", + "label": "Amended From", + "no_copy": 1, + "options": "Unreconcile Payments", + "print_hide": 1, + "read_only": 1 + }, + { + "fieldname": "company", + "fieldtype": "Link", + "label": "Company", + "options": "Company" + }, + { + "fieldname": "entries", + "fieldtype": "Table", + "label": "Entries", + "options": "Unreconcile Payment Entries" + } + ], + "index_web_pages_for_search": 1, + "is_submittable": 1, + "links": [], + "modified": "2023-08-22 11:07:03.854434", + "modified_by": "Administrator", + "module": "Accounts", + "name": "Unreconcile Payments", + "naming_rule": "Expression", + "owner": "Administrator", + "permissions": [ + { + "create": 1, + "delete": 1, + "read": 1, + "role": "Accounts Manager", + "select": 1, + "share": 1, + "submit": 1, + "write": 1 + }, + { + "create": 1, + "delete": 1, + "read": 1, + "role": "Accounts User", + "select": 1, + "share": 1, + "submit": 1, + "write": 1 + } + ], + "sort_field": "modified", + "sort_order": "DESC", + "states": [] +} \ No newline at end of file diff --git a/erpnext/accounts/doctype/unreconcile_payments/unreconcile_payments.py b/erpnext/accounts/doctype/unreconcile_payments/unreconcile_payments.py new file mode 100644 index 000000000000..96bcc009170c --- /dev/null +++ b/erpnext/accounts/doctype/unreconcile_payments/unreconcile_payments.py @@ -0,0 +1,9 @@ +# Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and contributors +# For license information, please see license.txt + +# import frappe +from frappe.model.document import Document + + +class UnreconcilePayments(Document): + pass From 77fa0f68df25d17b2ff711ce28e88a51950a06c4 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Tue, 22 Aug 2023 15:01:14 +0530 Subject: [PATCH 405/501] chore: working state on barebones functions (cherry picked from commit e48a90efe69f36dc455df3fefa8131384903e422) --- .../unreconcile_payment_entries.json | 36 ++++++------------- .../unreconcile_payments.js | 25 ++++++++++--- .../unreconcile_payments.json | 22 +++++++++--- .../unreconcile_payments.py | 33 +++++++++++++++-- 4 files changed, 80 insertions(+), 36 deletions(-) diff --git a/erpnext/accounts/doctype/unreconcile_payment_entries/unreconcile_payment_entries.json b/erpnext/accounts/doctype/unreconcile_payment_entries/unreconcile_payment_entries.json index 5beb39d03422..f70f4db2a8e7 100644 --- a/erpnext/accounts/doctype/unreconcile_payment_entries/unreconcile_payment_entries.json +++ b/erpnext/accounts/doctype/unreconcile_payment_entries/unreconcile_payment_entries.json @@ -6,41 +6,18 @@ "editable_grid": 1, "engine": "InnoDB", "field_order": [ - "voucher_type", - "voucher_no", - "reference_type", + "reference_doctype", "reference_name", "allocated_amount", "unlinked" ], "fields": [ - { - "fieldname": "voucher_type", - "fieldtype": "Link", - "in_list_view": 1, - "label": "Voucher Type", - "options": "DocType" - }, - { - "fieldname": "voucher_no", - "fieldtype": "Dynamic Link", - "in_list_view": 1, - "label": "Voucher No", - "options": "voucher_type" - }, - { - "fieldname": "reference_type", - "fieldtype": "Link", - "in_list_view": 1, - "label": "Reference Type", - "options": "DocType" - }, { "fieldname": "reference_name", "fieldtype": "Dynamic Link", "in_list_view": 1, "label": "Reference Name", - "options": "reference_type" + "options": "reference_doctype" }, { "fieldname": "allocated_amount", @@ -54,12 +31,19 @@ "fieldtype": "Check", "in_list_view": 1, "label": "Unlinked" + }, + { + "fieldname": "reference_doctype", + "fieldtype": "Link", + "in_list_view": 1, + "label": "Reference Type", + "options": "DocType" } ], "index_web_pages_for_search": 1, "istable": 1, "links": [], - "modified": "2023-08-22 11:22:20.381079", + "modified": "2023-08-22 15:00:33.203161", "modified_by": "Administrator", "module": "Accounts", "name": "Unreconcile Payment Entries", diff --git a/erpnext/accounts/doctype/unreconcile_payments/unreconcile_payments.js b/erpnext/accounts/doctype/unreconcile_payments/unreconcile_payments.js index d6670037d466..03a8253dd2f6 100644 --- a/erpnext/accounts/doctype/unreconcile_payments/unreconcile_payments.js +++ b/erpnext/accounts/doctype/unreconcile_payments/unreconcile_payments.js @@ -1,8 +1,25 @@ // Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and contributors // For license information, please see license.txt -// frappe.ui.form.on("Unreconcile Payments", { -// refresh(frm) { +frappe.ui.form.on("Unreconcile Payments", { + refresh(frm) { + frm.set_query("voucher_type", function() { + return { + filters: { + name: "Payment Entry" + } + } + }); -// }, -// }); + + frm.set_query("voucher_no", function(doc) { + return { + filters: { + company: doc.company, + docstatus: 1 + } + } + }); + + }, +}); diff --git a/erpnext/accounts/doctype/unreconcile_payments/unreconcile_payments.json b/erpnext/accounts/doctype/unreconcile_payments/unreconcile_payments.json index c182a63b654e..f4b3cd709015 100644 --- a/erpnext/accounts/doctype/unreconcile_payments/unreconcile_payments.json +++ b/erpnext/accounts/doctype/unreconcile_payments/unreconcile_payments.json @@ -9,7 +9,9 @@ "engine": "InnoDB", "field_order": [ "company", - "entries", + "voucher_type", + "voucher_no", + "references", "amended_from" ], "fields": [ @@ -29,16 +31,28 @@ "options": "Company" }, { - "fieldname": "entries", + "fieldname": "voucher_type", + "fieldtype": "Link", + "label": "Voucher Type", + "options": "DocType" + }, + { + "fieldname": "voucher_no", + "fieldtype": "Dynamic Link", + "label": "Voucher No", + "options": "voucher_type" + }, + { + "fieldname": "references", "fieldtype": "Table", - "label": "Entries", + "label": "References", "options": "Unreconcile Payment Entries" } ], "index_web_pages_for_search": 1, "is_submittable": 1, "links": [], - "modified": "2023-08-22 11:07:03.854434", + "modified": "2023-08-22 14:11:13.073414", "modified_by": "Administrator", "module": "Accounts", "name": "Unreconcile Payments", diff --git a/erpnext/accounts/doctype/unreconcile_payments/unreconcile_payments.py b/erpnext/accounts/doctype/unreconcile_payments/unreconcile_payments.py index 96bcc009170c..df08d79f01f9 100644 --- a/erpnext/accounts/doctype/unreconcile_payments/unreconcile_payments.py +++ b/erpnext/accounts/doctype/unreconcile_payments/unreconcile_payments.py @@ -1,9 +1,38 @@ # Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and contributors # For license information, please see license.txt -# import frappe +import frappe from frappe.model.document import Document +from erpnext.accounts.utils import unlink_ref_doc_from_payment_entries, update_voucher_outstanding + class UnreconcilePayments(Document): - pass + def before_save(self): + if self.voucher_type == "Payment Entry": + references = frappe.db.get_all( + "Payment Entry Reference", + filters={"docstatus": 1, "parent": self.voucher_no}, + fields=["reference_doctype", "reference_name", "allocated_amount"], + ) + + self.set("references", []) + for ref in references: + self.append("references", ref) + + def on_submit(self): + payment_type, paid_from, paid_to, party_type, party = frappe.db.get_all( + self.voucher_type, + filters={"name": self.voucher_no}, + fields=["payment_type", "paid_from", "paid_to", "party_type", "party"], + as_list=1, + )[0] + account = paid_from if payment_type == "Receive" else paid_to + + for ref in self.references: + doc = frappe.get_doc(ref.reference_doctype, ref.reference_name) + unlink_ref_doc_from_payment_entries(doc) + update_voucher_outstanding( + ref.reference_doctype, ref.reference_name, account, party_type, party + ) + frappe.db.set_value("Unreconcile Payment Entries", ref.name, "unlinked", True) From b9647ac0a4974ed310f652e9af20acfef2ec2b28 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Thu, 24 Aug 2023 14:52:26 +0530 Subject: [PATCH 406/501] refactor: adding 'Get Allocations' button (cherry picked from commit 5114a9580db961a006d9b2f3c4dc08f207f374c7) --- .../unreconcile_payment_entries.json | 5 ++- .../unreconcile_payments.js | 16 +++++++ .../unreconcile_payments.json | 14 ++++-- .../unreconcile_payments.py | 43 +++++++++++++------ 4 files changed, 60 insertions(+), 18 deletions(-) diff --git a/erpnext/accounts/doctype/unreconcile_payment_entries/unreconcile_payment_entries.json b/erpnext/accounts/doctype/unreconcile_payment_entries/unreconcile_payment_entries.json index f70f4db2a8e7..c4afaa8bcacd 100644 --- a/erpnext/accounts/doctype/unreconcile_payment_entries/unreconcile_payment_entries.json +++ b/erpnext/accounts/doctype/unreconcile_payment_entries/unreconcile_payment_entries.json @@ -30,7 +30,8 @@ "fieldname": "unlinked", "fieldtype": "Check", "in_list_view": 1, - "label": "Unlinked" + "label": "Unlinked", + "read_only": 1 }, { "fieldname": "reference_doctype", @@ -43,7 +44,7 @@ "index_web_pages_for_search": 1, "istable": 1, "links": [], - "modified": "2023-08-22 15:00:33.203161", + "modified": "2023-08-24 14:48:10.018574", "modified_by": "Administrator", "module": "Accounts", "name": "Unreconcile Payment Entries", diff --git a/erpnext/accounts/doctype/unreconcile_payments/unreconcile_payments.js b/erpnext/accounts/doctype/unreconcile_payments/unreconcile_payments.js index 03a8253dd2f6..ef7c958113c4 100644 --- a/erpnext/accounts/doctype/unreconcile_payments/unreconcile_payments.js +++ b/erpnext/accounts/doctype/unreconcile_payments/unreconcile_payments.js @@ -22,4 +22,20 @@ frappe.ui.form.on("Unreconcile Payments", { }); }, + get_allocations: function(frm) { + frm.clear_table("allocations"); + frappe.call({ + method: "get_allocations_from_payment", + doc: frm.doc, + callback: function(r) { + if (r.message) { + r.message.forEach(x => { + frm.add_child("allocations", x) + }) + frm.refresh_fields(); + } + } + }) + + } }); diff --git a/erpnext/accounts/doctype/unreconcile_payments/unreconcile_payments.json b/erpnext/accounts/doctype/unreconcile_payments/unreconcile_payments.json index f4b3cd709015..68af5dcc12a9 100644 --- a/erpnext/accounts/doctype/unreconcile_payments/unreconcile_payments.json +++ b/erpnext/accounts/doctype/unreconcile_payments/unreconcile_payments.json @@ -11,7 +11,8 @@ "company", "voucher_type", "voucher_no", - "references", + "get_allocations", + "allocations", "amended_from" ], "fields": [ @@ -43,16 +44,21 @@ "options": "voucher_type" }, { - "fieldname": "references", + "fieldname": "get_allocations", + "fieldtype": "Button", + "label": "Get Allocations" + }, + { + "fieldname": "allocations", "fieldtype": "Table", - "label": "References", + "label": "Allocations", "options": "Unreconcile Payment Entries" } ], "index_web_pages_for_search": 1, "is_submittable": 1, "links": [], - "modified": "2023-08-22 14:11:13.073414", + "modified": "2023-08-24 16:53:50.767700", "modified_by": "Administrator", "module": "Accounts", "name": "Unreconcile Payments", diff --git a/erpnext/accounts/doctype/unreconcile_payments/unreconcile_payments.py b/erpnext/accounts/doctype/unreconcile_payments/unreconcile_payments.py index df08d79f01f9..ab2cc718ada5 100644 --- a/erpnext/accounts/doctype/unreconcile_payments/unreconcile_payments.py +++ b/erpnext/accounts/doctype/unreconcile_payments/unreconcile_payments.py @@ -2,25 +2,44 @@ # For license information, please see license.txt import frappe +from frappe import qb from frappe.model.document import Document +from frappe.query_builder.functions import Sum from erpnext.accounts.utils import unlink_ref_doc_from_payment_entries, update_voucher_outstanding class UnreconcilePayments(Document): - def before_save(self): + # def validate(self): + # parent = set([alloc.parent for alloc in self.allocations]) + # if len(parent) != 1: + # pass + + @frappe.whitelist() + def get_allocations_from_payment(self): if self.voucher_type == "Payment Entry": - references = frappe.db.get_all( - "Payment Entry Reference", - filters={"docstatus": 1, "parent": self.voucher_no}, - fields=["reference_doctype", "reference_name", "allocated_amount"], + per = qb.DocType("Payment Entry Reference") + allocated_references = ( + qb.from_(per) + .select( + per.reference_doctype, per.reference_name, Sum(per.allocated_amount).as_("allocated_amount") + ) + .where((per.docstatus == 1) & (per.parent == self.voucher_no)) + .groupby(per.reference_name) + .run(as_dict=True) ) + return allocated_references + + def add_references(self): + allocations = self.get_allocations_from_payment() - self.set("references", []) - for ref in references: - self.append("references", ref) + for alloc in allocations: + self.append("allocations", alloc) def on_submit(self): + # todo: add more granular unlinking + # different amounts for same invoice should be individually unlinkable + payment_type, paid_from, paid_to, party_type, party = frappe.db.get_all( self.voucher_type, filters={"name": self.voucher_no}, @@ -29,10 +48,10 @@ def on_submit(self): )[0] account = paid_from if payment_type == "Receive" else paid_to - for ref in self.references: - doc = frappe.get_doc(ref.reference_doctype, ref.reference_name) + for alloc in self.allocations: + doc = frappe.get_doc(alloc.reference_doctype, alloc.reference_name) unlink_ref_doc_from_payment_entries(doc) update_voucher_outstanding( - ref.reference_doctype, ref.reference_name, account, party_type, party + alloc.reference_doctype, alloc.reference_name, account, party_type, party ) - frappe.db.set_value("Unreconcile Payment Entries", ref.name, "unlinked", True) + frappe.db.set_value("Unreconcile Payment Entries", alloc.name, "unlinked", True) From fb41f5f88c0e3d78bc0deb0069cdd7c8d8783dc4 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Thu, 24 Aug 2023 17:55:02 +0530 Subject: [PATCH 407/501] test: basic unreconcile function (cherry picked from commit 0faffaa8db495f94d8bbd673faac5d4acdcc58a4) --- .../test_unreconcile_payments.py | 98 ++++++++++++++++++- 1 file changed, 95 insertions(+), 3 deletions(-) diff --git a/erpnext/accounts/doctype/unreconcile_payments/test_unreconcile_payments.py b/erpnext/accounts/doctype/unreconcile_payments/test_unreconcile_payments.py index 85af5211aefb..2bb8a54c3508 100644 --- a/erpnext/accounts/doctype/unreconcile_payments/test_unreconcile_payments.py +++ b/erpnext/accounts/doctype/unreconcile_payments/test_unreconcile_payments.py @@ -1,9 +1,101 @@ # Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and Contributors # See license.txt -# import frappe +import frappe from frappe.tests.utils import FrappeTestCase +from frappe.utils import today +from erpnext.accounts.doctype.payment_entry.test_payment_entry import create_payment_entry +from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice +from erpnext.accounts.test.accounts_mixin import AccountsTestMixin -class TestUnreconcilePayments(FrappeTestCase): - pass + +class TestUnreconcilePayments(AccountsTestMixin, FrappeTestCase): + def setUp(self): + self.create_company() + self.create_customer() + self.create_item() + self.clear_old_entries() + + def tearDown(self): + frappe.db.rollback() + + def test_01_unreconcile_invoice(self): + si1 = create_sales_invoice( + item=self.item, + company=self.company, + customer=self.customer, + debit_to=self.debit_to, + posting_date=today(), + parent_cost_center=self.cost_center, + cost_center=self.cost_center, + rate=100, + price_list_rate=100, + ) + + si2 = create_sales_invoice( + item=self.item, + company=self.company, + customer=self.customer, + debit_to=self.debit_to, + posting_date=today(), + parent_cost_center=self.cost_center, + cost_center=self.cost_center, + rate=100, + price_list_rate=100, + ) + + pe = create_payment_entry( + company=self.company, + payment_type="Receive", + party_type="Customer", + party=self.customer, + paid_from=self.debit_to, + paid_to=self.cash, + paid_amount=200, + save=True, + ) + + pe.append( + "references", + {"reference_doctype": si1.doctype, "reference_name": si1.name, "allocated_amount": 100}, + ) + pe.append( + "references", + {"reference_doctype": si2.doctype, "reference_name": si2.name, "allocated_amount": 100}, + ) + # Allocation payment against both invoices + pe.save().submit() + + # Assert outstanding + si1.reload() + si2.reload() + self.assertEqual(si1.outstanding_amount, 0) + self.assertEqual(si2.outstanding_amount, 0) + + unreconcile = frappe.get_doc( + { + "doctype": "Unreconcile Payments", + "company": self.company, + "voucher_type": pe.doctype, + "voucher_no": pe.name, + } + ) + unreconcile.add_references() + self.assertEqual(len(unreconcile.allocations), 2) + allocations = [x.reference_name for x in unreconcile.allocations] + self.assertEquals([si1.name, si2.name], allocations) + # unreconcile si1 + for x in unreconcile.allocations: + if x.reference_name != si1.name: + unreconcile.remove(x) + unreconcile.save().submit() + + # Assert outstanding + si1.reload() + si2.reload() + self.assertEqual(si1.outstanding_amount, 100) + self.assertEqual(si2.outstanding_amount, 0) + + pe.reload() + self.assertEqual(len(pe.references), 1) From 9531a45b941e9481c5c1d0f5520c3fef4d6b1b09 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Sat, 26 Aug 2023 20:29:50 +0530 Subject: [PATCH 408/501] feat: UI for unreconcile (cherry picked from commit fc6be5bfb9a2cf1a79d0150fc5867ba0cb988f64) --- .../doctype/sales_invoice/sales_invoice.js | 44 +++++++++++++++++ .../unreconcile_payments.py | 49 ++++++++++++++++++- 2 files changed, 92 insertions(+), 1 deletion(-) diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.js b/erpnext/accounts/doctype/sales_invoice/sales_invoice.js index d6977d39a9fe..7c41070580d7 100644 --- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.js +++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.js @@ -177,6 +177,50 @@ erpnext.accounts.SalesInvoiceController = class SalesInvoiceController extends e }, __('Create')); } } + + if (doc.docstatus == 1) { + frappe.call({ + "method": "erpnext.accounts.doctype.unreconcile_payments.unreconcile_payments.doc_has_payments", + "args": { + "doctype": this.frm.doc.doctype, + "docname": this.frm.doc.name + }, + callback: function(r) { + if (r.message) { + me.frm.add_custom_button(__("Un-Reconcile"), function() { + me.unreconcile_prompt(); + }); + } + } + }); + } + } + + unreconcile_prompt() { + // get linked payments + let query_args = { + query:"erpnext.accounts.doctype.unreconcile_payments.unreconcile_payments.get_linked_payments_for_doc", + filters: { + doctype: this.frm.doc.doctype, + docname: this.frm.doc.name + } + } + + new frappe.ui.form.MultiSelectDialog({ + doctype: "Payment Ledger Entry", + target: this.cur_frm, + setters: { }, + add_filters_group: 0, + date_field: "posting_date", + columns: ["voucher_type", "voucher_no", "allocated_amount"], + get_query() { + return query_args; + }, + action(selections) { + console.log(selections); + } + }); + } make_maintenance_schedule() { diff --git a/erpnext/accounts/doctype/unreconcile_payments/unreconcile_payments.py b/erpnext/accounts/doctype/unreconcile_payments/unreconcile_payments.py index ab2cc718ada5..ed978cbc376b 100644 --- a/erpnext/accounts/doctype/unreconcile_payments/unreconcile_payments.py +++ b/erpnext/accounts/doctype/unreconcile_payments/unreconcile_payments.py @@ -4,7 +4,7 @@ import frappe from frappe import qb from frappe.model.document import Document -from frappe.query_builder.functions import Sum +from frappe.query_builder.functions import Abs, Sum from erpnext.accounts.utils import unlink_ref_doc_from_payment_entries, update_voucher_outstanding @@ -55,3 +55,50 @@ def on_submit(self): alloc.reference_doctype, alloc.reference_name, account, party_type, party ) frappe.db.set_value("Unreconcile Payment Entries", alloc.name, "unlinked", True) + + +@frappe.whitelist() +def doc_has_payments(doctype, docname): + if doctype in ["Sales Invoice", "Purchase Invoice"]: + return frappe.db.count( + "Payment Ledger Entry", + filters={"delinked": 0, "against_voucher_no": docname, "amount": ["<", 0]}, + ) + else: + return frappe.db.count( + "Payment Ledger Entry", + filters={"delinked": 0, "voucher_no": docname, "against_voucher_no": ["!=", docname]}, + ) + + +@frappe.whitelist() +def get_linked_payments_for_doc(doctype, txt, searchfield, start, page_len, filters): + if filters.get("doctype") and filters.get("docname"): + _dt = filters.get("doctype") + _dn = filters.get("docname") + ple = qb.DocType("Payment Ledger Entry") + if _dt in ["Sales Invoice", "Purchase Invoice"]: + res = ( + qb.from_(ple) + .select( + ple.voucher_type, + ple.voucher_no, + Abs(Sum(ple.amount_in_account_currency)).as_("allocated_amount"), + ) + .where((ple.delinked == 0) & (ple.against_voucher_no == _dn) & (ple.amount < 0)) + .groupby(ple.against_voucher_no) + .run(as_dict=True) + ) + return res + else: + return frappe.db.get_all( + "Payment Ledger Entry", + filters={ + "delinked": 0, + "voucher_no": _dn, + "against_voucher_no": ["!=", _dn], + "amount": ["<", 0], + }, + group_by="against_voucher_no", + fields=["against_voucher_type", "against_voucher_no", "Sum(amount_in_account_currency)"], + ) From cb35218eec8954c7419863c659d3176805a6f79d Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Sat, 26 Aug 2023 20:45:18 +0530 Subject: [PATCH 409/501] feat: filter on voucher no (cherry picked from commit 41eb2c9f5a2aeadab0fd2401cca86bd8302f7eb2) --- erpnext/accounts/doctype/sales_invoice/sales_invoice.js | 2 ++ .../doctype/unreconcile_payments/unreconcile_payments.py | 8 +++++++- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.js b/erpnext/accounts/doctype/sales_invoice/sales_invoice.js index 7c41070580d7..850ca840fedd 100644 --- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.js +++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.js @@ -213,6 +213,8 @@ erpnext.accounts.SalesInvoiceController = class SalesInvoiceController extends e add_filters_group: 0, date_field: "posting_date", columns: ["voucher_type", "voucher_no", "allocated_amount"], + primary_action_label: "Un-Reconcile", + title: "Un-Reconcile Payments", get_query() { return query_args; }, diff --git a/erpnext/accounts/doctype/unreconcile_payments/unreconcile_payments.py b/erpnext/accounts/doctype/unreconcile_payments/unreconcile_payments.py index ed978cbc376b..dfd2d29e0f4b 100644 --- a/erpnext/accounts/doctype/unreconcile_payments/unreconcile_payments.py +++ b/erpnext/accounts/doctype/unreconcile_payments/unreconcile_payments.py @@ -4,6 +4,7 @@ import frappe from frappe import qb from frappe.model.document import Document +from frappe.query_builder import Criterion from frappe.query_builder.functions import Abs, Sum from erpnext.accounts.utils import unlink_ref_doc_from_payment_entries, update_voucher_outstanding @@ -78,6 +79,11 @@ def get_linked_payments_for_doc(doctype, txt, searchfield, start, page_len, filt _dn = filters.get("docname") ple = qb.DocType("Payment Ledger Entry") if _dt in ["Sales Invoice", "Purchase Invoice"]: + criteria = [(ple.delinked == 0), (ple.against_voucher_no == _dn), (ple.amount < 0)] + + if txt: + criteria.append(ple.voucher_no.like(f"%{txt}%")) + res = ( qb.from_(ple) .select( @@ -85,7 +91,7 @@ def get_linked_payments_for_doc(doctype, txt, searchfield, start, page_len, filt ple.voucher_no, Abs(Sum(ple.amount_in_account_currency)).as_("allocated_amount"), ) - .where((ple.delinked == 0) & (ple.against_voucher_no == _dn) & (ple.amount < 0)) + .where(Criterion.all(criteria)) .groupby(ple.against_voucher_no) .run(as_dict=True) ) From 37fc82cd114181f62e5850095222fa963e719fee Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Mon, 28 Aug 2023 16:27:29 +0530 Subject: [PATCH 410/501] chore: delete references upon parent deletion (cherry picked from commit fbdfb8151c1f79fcc9b835a4ccc5c954e65b743f) --- erpnext/controllers/accounts_controller.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/erpnext/controllers/accounts_controller.py b/erpnext/controllers/accounts_controller.py index c7e21638bc75..d830ab7b414c 100644 --- a/erpnext/controllers/accounts_controller.py +++ b/erpnext/controllers/accounts_controller.py @@ -218,6 +218,11 @@ def on_trash(self): (rpi.voucher_type == self.doctype) & (rpi.voucher_no == self.name) ).run() + upe = frappe.qb.DocType("UnReconcile Payment Entries") + frappe.qb.from_(upe).delete().where( + (upe.reference_doctype == self.doctype) & (upe.reference_name == self.name) + ).run() + # delete sl and gl entries on deletion of transaction if frappe.db.get_single_value("Accounts Settings", "delete_linked_ledger_entries"): ple = frappe.qb.DocType("Payment Ledger Entry") From 9422422dcc6ee0a71bc9539fa2cb91dd231feeb4 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Mon, 28 Aug 2023 17:36:12 +0530 Subject: [PATCH 411/501] refactor: remove references using framework (cherry picked from commit 42df0d3d6729a57953bc9cb0ef8622eae34829a0) --- .../doctype/sales_invoice/sales_invoice.js | 2 +- .../doctype/sales_invoice/sales_invoice.py | 2 ++ erpnext/controllers/accounts_controller.py | 27 ++++++++++++++++--- 3 files changed, 26 insertions(+), 5 deletions(-) diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.js b/erpnext/accounts/doctype/sales_invoice/sales_invoice.js index 850ca840fedd..8b6b65a9fe71 100644 --- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.js +++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.js @@ -34,7 +34,7 @@ erpnext.accounts.SalesInvoiceController = class SalesInvoiceController extends e super.onload(); this.frm.ignore_doctypes_on_cancel_all = ['POS Invoice', 'Timesheet', 'POS Invoice Merge Log', - 'POS Closing Entry', 'Journal Entry', 'Payment Entry', "Repost Payment Ledger", "Repost Accounting Ledger"]; + 'POS Closing Entry', 'Journal Entry', 'Payment Entry', "Repost Payment Ledger", "Repost Accounting Ledger", "Unreconcile Payments", "Unreconcile Payment Entries"]; if(!this.frm.doc.__islocal && !this.frm.doc.customer && this.frm.doc.debit_to) { // show debit_to in print format diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py index 3f9fe0441d12..7d431b4205b2 100644 --- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py +++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py @@ -401,6 +401,8 @@ def on_cancel(self): "Repost Payment Ledger Items", "Repost Accounting Ledger", "Repost Accounting Ledger Items", + "Unreconcile Payments", + "Unreconcile Payment Entries", "Payment Ledger Entry", ) diff --git a/erpnext/controllers/accounts_controller.py b/erpnext/controllers/accounts_controller.py index d830ab7b414c..792557b2b2af 100644 --- a/erpnext/controllers/accounts_controller.py +++ b/erpnext/controllers/accounts_controller.py @@ -211,6 +211,28 @@ def validate(self): def before_cancel(self): validate_einvoice_fields(self) + def _remove_references_in_unreconcile(self): + upe = frappe.qb.DocType("UnReconcile Payment Entries") + rows = ( + frappe.qb.from_(upe) + .select(upe.name, upe.parent) + .where((upe.reference_doctype == self.doctype) & (upe.reference_name == self.name)) + .run(as_dict=True) + ) + + references_map = frappe._dict() + for x in rows: + references_map.setdefault(x.parent, []).append(x.name) + + for doc, rows in references_map.items(): + unreconcile_doc = frappe.get_doc("Unreconcile Payments", doc) + for row in rows: + unreconcile_doc.remove(unreconcile_doc.get("allocations", {"name": row})[0]) + + unreconcile_doc.flags.ignore_validate_update_after_submit = True + unreconcile_doc.flags.ignore_links = True + unreconcile_doc.save(ignore_permissions=True) + def on_trash(self): # delete references in 'Repost Payment Ledger' rpi = frappe.qb.DocType("Repost Payment Ledger Items") @@ -218,10 +240,7 @@ def on_trash(self): (rpi.voucher_type == self.doctype) & (rpi.voucher_no == self.name) ).run() - upe = frappe.qb.DocType("UnReconcile Payment Entries") - frappe.qb.from_(upe).delete().where( - (upe.reference_doctype == self.doctype) & (upe.reference_name == self.name) - ).run() + self._remove_references_in_unreconcile() # delete sl and gl entries on deletion of transaction if frappe.db.get_single_value("Accounts Settings", "delete_linked_ledger_entries"): From 2fd500ce2642bd4caf0c93e6da3cccc7caeeab97 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Mon, 28 Aug 2023 17:43:05 +0530 Subject: [PATCH 412/501] chore: track changes (cherry picked from commit 489a545bbb1a814cb18164321e10a0d17042272c) --- .../doctype/unreconcile_payments/unreconcile_payments.json | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/erpnext/accounts/doctype/unreconcile_payments/unreconcile_payments.json b/erpnext/accounts/doctype/unreconcile_payments/unreconcile_payments.json index 68af5dcc12a9..f29e61b6ef67 100644 --- a/erpnext/accounts/doctype/unreconcile_payments/unreconcile_payments.json +++ b/erpnext/accounts/doctype/unreconcile_payments/unreconcile_payments.json @@ -58,7 +58,7 @@ "index_web_pages_for_search": 1, "is_submittable": 1, "links": [], - "modified": "2023-08-24 16:53:50.767700", + "modified": "2023-08-28 17:42:50.261377", "modified_by": "Administrator", "module": "Accounts", "name": "Unreconcile Payments", @@ -88,5 +88,6 @@ ], "sort_field": "modified", "sort_order": "DESC", - "states": [] + "states": [], + "track_changes": 1 } \ No newline at end of file From 3a670264b24c0d57070c5530f039b56ed837a599 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Mon, 28 Aug 2023 17:49:09 +0530 Subject: [PATCH 413/501] chore: delete unreoncile doc upon parent doc deletion (cherry picked from commit 6bbe47c6714546114d77e34cbefbc7f30227050f) --- .../doctype/payment_entry/payment_entry.js | 2 +- .../doctype/payment_entry/payment_entry.py | 2 ++ erpnext/controllers/accounts_controller.py | 33 ++++++++++++------- 3 files changed, 24 insertions(+), 13 deletions(-) diff --git a/erpnext/accounts/doctype/payment_entry/payment_entry.js b/erpnext/accounts/doctype/payment_entry/payment_entry.js index d43a057db027..9f495e9fd8a7 100644 --- a/erpnext/accounts/doctype/payment_entry/payment_entry.js +++ b/erpnext/accounts/doctype/payment_entry/payment_entry.js @@ -7,7 +7,7 @@ cur_frm.cscript.tax_table = "Advance Taxes and Charges"; frappe.ui.form.on('Payment Entry', { onload: function(frm) { - frm.ignore_doctypes_on_cancel_all = ['Sales Invoice', 'Purchase Invoice', 'Journal Entry', 'Repost Payment Ledger','Repost Accounting Ledger']; + frm.ignore_doctypes_on_cancel_all = ['Sales Invoice', 'Purchase Invoice', 'Journal Entry', 'Repost Payment Ledger','Repost Accounting Ledger', 'Unreconcile Payments', 'Unreconcile Payment Entries']; if(frm.doc.__islocal) { if (!frm.doc.paid_from) frm.set_value("paid_from_account_currency", null); diff --git a/erpnext/accounts/doctype/payment_entry/payment_entry.py b/erpnext/accounts/doctype/payment_entry/payment_entry.py index c5501a58306a..35ab729339d8 100644 --- a/erpnext/accounts/doctype/payment_entry/payment_entry.py +++ b/erpnext/accounts/doctype/payment_entry/payment_entry.py @@ -107,6 +107,8 @@ def on_cancel(self): "Repost Payment Ledger Items", "Repost Accounting Ledger", "Repost Accounting Ledger Items", + "Unreconcile Payments", + "Unreconcile Payment Entries", ) super(PaymentEntry, self).on_cancel() self.make_gl_entries(cancel=1) diff --git a/erpnext/controllers/accounts_controller.py b/erpnext/controllers/accounts_controller.py index 792557b2b2af..ffd8870553bd 100644 --- a/erpnext/controllers/accounts_controller.py +++ b/erpnext/controllers/accounts_controller.py @@ -220,18 +220,27 @@ def _remove_references_in_unreconcile(self): .run(as_dict=True) ) - references_map = frappe._dict() - for x in rows: - references_map.setdefault(x.parent, []).append(x.name) - - for doc, rows in references_map.items(): - unreconcile_doc = frappe.get_doc("Unreconcile Payments", doc) - for row in rows: - unreconcile_doc.remove(unreconcile_doc.get("allocations", {"name": row})[0]) - - unreconcile_doc.flags.ignore_validate_update_after_submit = True - unreconcile_doc.flags.ignore_links = True - unreconcile_doc.save(ignore_permissions=True) + if rows: + references_map = frappe._dict() + for x in rows: + references_map.setdefault(x.parent, []).append(x.name) + + for doc, rows in references_map.items(): + unreconcile_doc = frappe.get_doc("Unreconcile Payments", doc) + for row in rows: + unreconcile_doc.remove(unreconcile_doc.get("allocations", {"name": row})[0]) + + unreconcile_doc.flags.ignore_validate_update_after_submit = True + unreconcile_doc.flags.ignore_links = True + unreconcile_doc.save(ignore_permissions=True) + + # delete docs upon parent doc deletion + unreconcile_docs = frappe.db.get_all("Unreconcile Payments", filters={"voucher_no": self.name}) + for x in unreconcile_docs: + _doc = frappe.get_doc("Unreconcile Payments", x.name) + if _doc.docstatus == 1: + _doc.cancel() + _doc.delete() def on_trash(self): # delete references in 'Repost Payment Ledger' From b8865896572b2e50f402039338175653d1cf2c7a Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Tue, 29 Aug 2023 11:27:16 +0530 Subject: [PATCH 414/501] refactor: add UI elements (cherry picked from commit 58dc0e52e197e89653f0778fc434d86611616808) --- .../doctype/sales_invoice/sales_invoice.js | 69 +++++++++++++------ .../unreconcile_payments.py | 37 +++++++--- 2 files changed, 76 insertions(+), 30 deletions(-) diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.js b/erpnext/accounts/doctype/sales_invoice/sales_invoice.js index 8b6b65a9fe71..b682e910c15b 100644 --- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.js +++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.js @@ -197,32 +197,57 @@ erpnext.accounts.SalesInvoiceController = class SalesInvoiceController extends e } unreconcile_prompt() { - // get linked payments - let query_args = { - query:"erpnext.accounts.doctype.unreconcile_payments.unreconcile_payments.get_linked_payments_for_doc", - filters: { - doctype: this.frm.doc.doctype, - docname: this.frm.doc.name - } - } + let child_table_fields = [ + { label: __("Voucher Type"), fieldname: "voucher_type", fieldtype: "Dynamic Link", options: "DocType", in_list_view: 1, read_only: 1}, + { label: __("Voucher No"), fieldname: "voucher_no", fieldtype: "Link", options: "voucher_type", in_list_view: 1, read_only: 1 }, + { label: __("Allocated Amount"), fieldname: "allocated_amount", fieldtype: "Float", in_list_view: 1, read_only: 1 }, + ] + let unreconcile_dialog_fields = [ + { + label: __('Allocations'), + fieldname: 'allocations', + fieldtype: 'Table', + read_only: 1, + fields: child_table_fields, + }, + ]; - new frappe.ui.form.MultiSelectDialog({ - doctype: "Payment Ledger Entry", - target: this.cur_frm, - setters: { }, - add_filters_group: 0, - date_field: "posting_date", - columns: ["voucher_type", "voucher_no", "allocated_amount"], - primary_action_label: "Un-Reconcile", - title: "Un-Reconcile Payments", - get_query() { - return query_args; + // get linked payments + frappe.call({ + "method": "erpnext.accounts.doctype.unreconcile_payments.unreconcile_payments.get_linked_payments_for_doc", + "args": { + "company": this.frm.doc.company, + "doctype": this.frm.doc.doctype, + "docname": this.frm.doc.name }, - action(selections) { - console.log(selections); + callback: function(r) { + if (r.message) { + // populate child table with allocations + unreconcile_dialog_fields[0].data = r.message; + unreconcile_dialog_fields[0].get_data = function(){ return r.message}; + + let d = new frappe.ui.Dialog({ + title: 'Un-Reconcile Allocations', + fields: unreconcile_dialog_fields, + size: 'large', + cannot_add_rows: 1, + primary_action_label: 'Un-Reconcile', + primary_action(values) { + + let selected_allocations = values.allocations.filter(x=>x.__checked); + if (selected_allocations.length > 0) { + // assuming each row is an individual voucher + // pass this to server side method that created unreconcile doc for row + } else { + frappe.msgprint("No Selection"); + } + } + }); + + d.show(); + } } }); - } make_maintenance_schedule() { diff --git a/erpnext/accounts/doctype/unreconcile_payments/unreconcile_payments.py b/erpnext/accounts/doctype/unreconcile_payments/unreconcile_payments.py index dfd2d29e0f4b..cced2b3de498 100644 --- a/erpnext/accounts/doctype/unreconcile_payments/unreconcile_payments.py +++ b/erpnext/accounts/doctype/unreconcile_payments/unreconcile_payments.py @@ -73,20 +73,25 @@ def doc_has_payments(doctype, docname): @frappe.whitelist() -def get_linked_payments_for_doc(doctype, txt, searchfield, start, page_len, filters): - if filters.get("doctype") and filters.get("docname"): - _dt = filters.get("doctype") - _dn = filters.get("docname") +def get_linked_payments_for_doc( + company: str = None, doctype: str = None, docname: str = None +) -> list: + if company and doctype and docname: + _dt = doctype + _dn = docname ple = qb.DocType("Payment Ledger Entry") if _dt in ["Sales Invoice", "Purchase Invoice"]: - criteria = [(ple.delinked == 0), (ple.against_voucher_no == _dn), (ple.amount < 0)] - - if txt: - criteria.append(ple.voucher_no.like(f"%{txt}%")) + criteria = [ + (ple.delinked == 0), + (ple.against_voucher_no == _dn), + (ple.amount < 0), + (ple.company == company), + ] res = ( qb.from_(ple) .select( + ple.company, ple.voucher_type, ple.voucher_no, Abs(Sum(ple.amount_in_account_currency)).as_("allocated_amount"), @@ -108,3 +113,19 @@ def get_linked_payments_for_doc(doctype, txt, searchfield, start, page_len, filt group_by="against_voucher_no", fields=["against_voucher_type", "against_voucher_no", "Sum(amount_in_account_currency)"], ) + return [] + + +@frappe.whitelist() +def create_unreconcile_doc_for_selection( + company: str = None, dt: str = None, dn: str = None, selections: list = None +): + if selections: + # assuming each row is a unique voucher + for row in selections: + unrecon = frappe.new_doc("Unreconcile Payments") + unrecon.company = company + unrecon.voucher_type = dt + unrecon.voucher_type = dn + unrecon.add_references() + # remove unselected references From 1e93d0bcc4b0c7d75fb4b41855aa4909eaa59d42 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Tue, 29 Aug 2023 13:19:26 +0530 Subject: [PATCH 415/501] chore: move dialog building function to `utils.js` file (cherry picked from commit 5981c7e0ad1d80236654ceb4214b97a178fe5a05) --- .../doctype/sales_invoice/sales_invoice.js | 55 +----------------- erpnext/public/js/utils.js | 56 +++++++++++++++++++ 2 files changed, 57 insertions(+), 54 deletions(-) diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.js b/erpnext/accounts/doctype/sales_invoice/sales_invoice.js index b682e910c15b..90ee76139295 100644 --- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.js +++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.js @@ -188,7 +188,7 @@ erpnext.accounts.SalesInvoiceController = class SalesInvoiceController extends e callback: function(r) { if (r.message) { me.frm.add_custom_button(__("Un-Reconcile"), function() { - me.unreconcile_prompt(); + erpnext.utils.build_unreconcile_dialog(cur_frm); }); } } @@ -196,59 +196,6 @@ erpnext.accounts.SalesInvoiceController = class SalesInvoiceController extends e } } - unreconcile_prompt() { - let child_table_fields = [ - { label: __("Voucher Type"), fieldname: "voucher_type", fieldtype: "Dynamic Link", options: "DocType", in_list_view: 1, read_only: 1}, - { label: __("Voucher No"), fieldname: "voucher_no", fieldtype: "Link", options: "voucher_type", in_list_view: 1, read_only: 1 }, - { label: __("Allocated Amount"), fieldname: "allocated_amount", fieldtype: "Float", in_list_view: 1, read_only: 1 }, - ] - let unreconcile_dialog_fields = [ - { - label: __('Allocations'), - fieldname: 'allocations', - fieldtype: 'Table', - read_only: 1, - fields: child_table_fields, - }, - ]; - - // get linked payments - frappe.call({ - "method": "erpnext.accounts.doctype.unreconcile_payments.unreconcile_payments.get_linked_payments_for_doc", - "args": { - "company": this.frm.doc.company, - "doctype": this.frm.doc.doctype, - "docname": this.frm.doc.name - }, - callback: function(r) { - if (r.message) { - // populate child table with allocations - unreconcile_dialog_fields[0].data = r.message; - unreconcile_dialog_fields[0].get_data = function(){ return r.message}; - - let d = new frappe.ui.Dialog({ - title: 'Un-Reconcile Allocations', - fields: unreconcile_dialog_fields, - size: 'large', - cannot_add_rows: 1, - primary_action_label: 'Un-Reconcile', - primary_action(values) { - - let selected_allocations = values.allocations.filter(x=>x.__checked); - if (selected_allocations.length > 0) { - // assuming each row is an individual voucher - // pass this to server side method that created unreconcile doc for row - } else { - frappe.msgprint("No Selection"); - } - } - }); - - d.show(); - } - } - }); - } make_maintenance_schedule() { frappe.model.open_mapped_doc({ diff --git a/erpnext/public/js/utils.js b/erpnext/public/js/utils.js index a6b4ea12bbe5..14532286696d 100755 --- a/erpnext/public/js/utils.js +++ b/erpnext/public/js/utils.js @@ -666,6 +666,62 @@ erpnext.utils.update_child_items = function(opts) { }).show(); } +erpnext.utils.build_unreconcile_dialog = function(frm) { + if (['Sales Invoice', 'Purchase Invoice', 'Payment Entry', 'Journal Entry'].includes(frm.doc.doctype)) { + let child_table_fields = [ + { label: __("Voucher Type"), fieldname: "voucher_type", fieldtype: "Dynamic Link", options: "DocType", in_list_view: 1, read_only: 1}, + { label: __("Voucher No"), fieldname: "voucher_no", fieldtype: "Link", options: "voucher_type", in_list_view: 1, read_only: 1 }, + { label: __("Allocated Amount"), fieldname: "allocated_amount", fieldtype: "Float", in_list_view: 1, read_only: 1 }, + ] + let unreconcile_dialog_fields = [ + { + label: __('Allocations'), + fieldname: 'allocations', + fieldtype: 'Table', + read_only: 1, + fields: child_table_fields, + }, + ]; + + // get linked payments + frappe.call({ + "method": "erpnext.accounts.doctype.unreconcile_payments.unreconcile_payments.get_linked_payments_for_doc", + "args": { + "company": frm.doc.company, + "doctype": frm.doc.doctype, + "docname": frm.doc.name + }, + callback: function(r) { + if (r.message) { + // populate child table with allocations + unreconcile_dialog_fields[0].data = r.message; + unreconcile_dialog_fields[0].get_data = function(){ return r.message}; + + let d = new frappe.ui.Dialog({ + title: 'Un-Reconcile Allocations', + fields: unreconcile_dialog_fields, + size: 'large', + cannot_add_rows: 1, + primary_action_label: 'Un-Reconcile', + primary_action(values) { + + let selected_allocations = values.allocations.filter(x=>x.__checked); + if (selected_allocations.length > 0) { + // assuming each row is an individual voucher + // pass this to server side method that created unreconcile doc for row + } else { + frappe.msgprint("No Selection"); + } + } + }); + + d.show(); + } + } + }); + } +} + erpnext.utils.map_current_doc = function(opts) { function _map() { if($.isArray(cur_frm.doc.items) && cur_frm.doc.items.length > 0) { From e464f5e419df0e9370d3581f8a95b7451facd001 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Tue, 29 Aug 2023 13:46:29 +0530 Subject: [PATCH 416/501] chore: move functions to a separate file in utils (cherry picked from commit 25fe75218578a44302f1335f8db0caa17d4d7608) # Conflicts: # erpnext/accounts/doctype/payment_entry/payment_entry.js # erpnext/public/js/erpnext.bundle.js --- .../doctype/payment_entry/payment_entry.js | 5 + .../doctype/sales_invoice/sales_invoice.js | 17 +-- .../unreconcile_payments.py | 19 +++- erpnext/public/js/erpnext.bundle.js | 5 + erpnext/public/js/utils.js | 53 --------- erpnext/public/js/utils/unreconcile.js | 106 ++++++++++++++++++ 6 files changed, 130 insertions(+), 75 deletions(-) create mode 100644 erpnext/public/js/utils/unreconcile.js diff --git a/erpnext/accounts/doctype/payment_entry/payment_entry.js b/erpnext/accounts/doctype/payment_entry/payment_entry.js index 9f495e9fd8a7..48a18e95b7a5 100644 --- a/erpnext/accounts/doctype/payment_entry/payment_entry.js +++ b/erpnext/accounts/doctype/payment_entry/payment_entry.js @@ -152,6 +152,11 @@ frappe.ui.form.on('Payment Entry', { frm.events.hide_unhide_fields(frm); frm.events.set_dynamic_labels(frm); frm.events.show_general_ledger(frm); +<<<<<<< HEAD +======= + erpnext.accounts.ledger_preview.show_accounting_ledger_preview(frm); + erpnext.accounts.unreconcile_payments.add_unreconcile_btn(frm); +>>>>>>> 25fe752185 (chore: move functions to a separate file in utils) }, validate_company: (frm) => { diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.js b/erpnext/accounts/doctype/sales_invoice/sales_invoice.js index 90ee76139295..a411889fbddb 100644 --- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.js +++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.js @@ -178,22 +178,7 @@ erpnext.accounts.SalesInvoiceController = class SalesInvoiceController extends e } } - if (doc.docstatus == 1) { - frappe.call({ - "method": "erpnext.accounts.doctype.unreconcile_payments.unreconcile_payments.doc_has_payments", - "args": { - "doctype": this.frm.doc.doctype, - "docname": this.frm.doc.name - }, - callback: function(r) { - if (r.message) { - me.frm.add_custom_button(__("Un-Reconcile"), function() { - erpnext.utils.build_unreconcile_dialog(cur_frm); - }); - } - } - }); - } + erpnext.accounts.unreconcile_payments.add_unreconcile_btn(me.frm); } diff --git a/erpnext/accounts/doctype/unreconcile_payments/unreconcile_payments.py b/erpnext/accounts/doctype/unreconcile_payments/unreconcile_payments.py index cced2b3de498..c80365b0ef0f 100644 --- a/erpnext/accounts/doctype/unreconcile_payments/unreconcile_payments.py +++ b/erpnext/accounts/doctype/unreconcile_payments/unreconcile_payments.py @@ -117,15 +117,22 @@ def get_linked_payments_for_doc( @frappe.whitelist() -def create_unreconcile_doc_for_selection( - company: str = None, dt: str = None, dn: str = None, selections: list = None -): +def create_unreconcile_doc_for_selection(selections=None): if selections: + selections = frappe.json.loads(selections) # assuming each row is a unique voucher for row in selections: unrecon = frappe.new_doc("Unreconcile Payments") - unrecon.company = company - unrecon.voucher_type = dt - unrecon.voucher_type = dn + unrecon.company = row.get("company") + unrecon.voucher_type = row.get("voucher_type") + unrecon.voucher_no = row.get("voucher_no") unrecon.add_references() + # remove unselected references + unrecon.allocations = [ + x + for x in unrecon.allocations + if x.reference_doctype == row.get("against_voucher_type") + and x.reference_name == row.get("against_voucher_no") + ] + unrecon.save().submit() diff --git a/erpnext/public/js/erpnext.bundle.js b/erpnext/public/js/erpnext.bundle.js index 7b230af26990..5df6318a1cd6 100644 --- a/erpnext/public/js/erpnext.bundle.js +++ b/erpnext/public/js/erpnext.bundle.js @@ -18,6 +18,11 @@ import "./utils/customer_quick_entry"; import "./utils/supplier_quick_entry"; import "./call_popup/call_popup"; import "./utils/dimension_tree_filter"; +<<<<<<< HEAD +======= +import "./utils/ledger_preview.js"; +import "./utils/unreconcile.js"; +>>>>>>> 25fe752185 (chore: move functions to a separate file in utils) import "./utils/barcode_scanner"; import "./telephony"; import "./templates/call_link.html"; diff --git a/erpnext/public/js/utils.js b/erpnext/public/js/utils.js index 14532286696d..eafc1ed70e63 100755 --- a/erpnext/public/js/utils.js +++ b/erpnext/public/js/utils.js @@ -666,61 +666,8 @@ erpnext.utils.update_child_items = function(opts) { }).show(); } -erpnext.utils.build_unreconcile_dialog = function(frm) { - if (['Sales Invoice', 'Purchase Invoice', 'Payment Entry', 'Journal Entry'].includes(frm.doc.doctype)) { - let child_table_fields = [ - { label: __("Voucher Type"), fieldname: "voucher_type", fieldtype: "Dynamic Link", options: "DocType", in_list_view: 1, read_only: 1}, - { label: __("Voucher No"), fieldname: "voucher_no", fieldtype: "Link", options: "voucher_type", in_list_view: 1, read_only: 1 }, - { label: __("Allocated Amount"), fieldname: "allocated_amount", fieldtype: "Float", in_list_view: 1, read_only: 1 }, - ] - let unreconcile_dialog_fields = [ - { - label: __('Allocations'), - fieldname: 'allocations', - fieldtype: 'Table', - read_only: 1, - fields: child_table_fields, - }, - ]; - // get linked payments - frappe.call({ - "method": "erpnext.accounts.doctype.unreconcile_payments.unreconcile_payments.get_linked_payments_for_doc", - "args": { - "company": frm.doc.company, - "doctype": frm.doc.doctype, - "docname": frm.doc.name - }, - callback: function(r) { - if (r.message) { - // populate child table with allocations - unreconcile_dialog_fields[0].data = r.message; - unreconcile_dialog_fields[0].get_data = function(){ return r.message}; - - let d = new frappe.ui.Dialog({ - title: 'Un-Reconcile Allocations', - fields: unreconcile_dialog_fields, - size: 'large', - cannot_add_rows: 1, - primary_action_label: 'Un-Reconcile', - primary_action(values) { - - let selected_allocations = values.allocations.filter(x=>x.__checked); - if (selected_allocations.length > 0) { - // assuming each row is an individual voucher - // pass this to server side method that created unreconcile doc for row - } else { - frappe.msgprint("No Selection"); - } - } - }); - d.show(); - } - } - }); - } -} erpnext.utils.map_current_doc = function(opts) { function _map() { diff --git a/erpnext/public/js/utils/unreconcile.js b/erpnext/public/js/utils/unreconcile.js new file mode 100644 index 000000000000..509cd3941002 --- /dev/null +++ b/erpnext/public/js/utils/unreconcile.js @@ -0,0 +1,106 @@ +frappe.provide('erpnext.accounts'); + +erpnext.accounts.unreconcile_payments = { + add_unreconcile_btn(frm) { + if (frm.doc.docstatus == 1) { + frappe.call({ + "method": "erpnext.accounts.doctype.unreconcile_payments.unreconcile_payments.doc_has_payments", + "args": { + "doctype": frm.doc.doctype, + "docname": frm.doc.name + }, + callback: function(r) { + if (r.message) { + frm.add_custom_button(__("Un-Reconcile"), function() { + erpnext.accounts.unreconcile_payments.build_unreconcile_dialog(frm); + }); + } + } + }); + } + }, + + build_unreconcile_dialog(frm) { + if (['Sales Invoice', 'Purchase Invoice', 'Payment Entry', 'Journal Entry'].includes(frm.doc.doctype)) { + let child_table_fields = [ + { label: __("Voucher Type"), fieldname: "voucher_type", fieldtype: "Dynamic Link", options: "DocType", in_list_view: 1, read_only: 1}, + { label: __("Voucher No"), fieldname: "voucher_no", fieldtype: "Link", options: "voucher_type", in_list_view: 1, read_only: 1 }, + { label: __("Allocated Amount"), fieldname: "allocated_amount", fieldtype: "Float", in_list_view: 1, read_only: 1 }, + ] + let unreconcile_dialog_fields = [ + { + label: __('Allocations'), + fieldname: 'allocations', + fieldtype: 'Table', + read_only: 1, + fields: child_table_fields, + }, + ]; + + // get linked payments + frappe.call({ + "method": "erpnext.accounts.doctype.unreconcile_payments.unreconcile_payments.get_linked_payments_for_doc", + "args": { + "company": frm.doc.company, + "doctype": frm.doc.doctype, + "docname": frm.doc.name + }, + callback: function(r) { + if (r.message) { + // populate child table with allocations + unreconcile_dialog_fields[0].data = r.message; + unreconcile_dialog_fields[0].get_data = function(){ return r.message}; + + let d = new frappe.ui.Dialog({ + title: 'Un-Reconcile Allocations', + fields: unreconcile_dialog_fields, + size: 'large', + cannot_add_rows: 1, + primary_action_label: 'Un-Reconcile', + primary_action(values) { + + let selected_allocations = values.allocations.filter(x=>x.__checked); + if (selected_allocations.length > 0) { + // assuming each row is an individual voucher + // pass this to server side method that creates unreconcile doc for each row + if (['Sales Invoice', 'Purchase Invoice'].includes(frm.doc.doctype)) { + let selection_map = selected_allocations.map(function(elem) { + return { + company: elem.company, + voucher_type: elem.voucher_type, + voucher_no: elem.voucher_no, + against_voucher_type: frm.doc.doctype, + against_voucher_no: frm.doc.name + }; + + }); + + erpnext.utils.create_unreconcile_docs(selection_map); + d.hide(); + } + + } else { + frappe.msgprint("No Selection"); + } + } + }); + + d.show(); + } + } + }); + } + }, + + create_unreconcile_docs(selection_map) { + frappe.call({ + "method": "erpnext.accounts.doctype.unreconcile_payments.unreconcile_payments.create_unreconcile_doc_for_selection", + "args": { + "selections": selection_map + }, + }); + } + + + +} From 7651ecbc2bb9672456c10dc79dd3f85a7abc34c5 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Tue, 29 Aug 2023 15:15:14 +0530 Subject: [PATCH 417/501] chore: fetch logic for payment entry (cherry picked from commit 1981f3837a10b5c0c2298a682190e0e6689a8b19) --- .../unreconcile_payments.py | 31 +++++++----- erpnext/public/js/utils/unreconcile.js | 48 ++++++++++++------- 2 files changed, 51 insertions(+), 28 deletions(-) diff --git a/erpnext/accounts/doctype/unreconcile_payments/unreconcile_payments.py b/erpnext/accounts/doctype/unreconcile_payments/unreconcile_payments.py index c80365b0ef0f..b6dd363cea55 100644 --- a/erpnext/accounts/doctype/unreconcile_payments/unreconcile_payments.py +++ b/erpnext/accounts/doctype/unreconcile_payments/unreconcile_payments.py @@ -82,10 +82,10 @@ def get_linked_payments_for_doc( ple = qb.DocType("Payment Ledger Entry") if _dt in ["Sales Invoice", "Purchase Invoice"]: criteria = [ + (ple.company == company), (ple.delinked == 0), (ple.against_voucher_no == _dn), (ple.amount < 0), - (ple.company == company), ] res = ( @@ -102,17 +102,26 @@ def get_linked_payments_for_doc( ) return res else: - return frappe.db.get_all( - "Payment Ledger Entry", - filters={ - "delinked": 0, - "voucher_no": _dn, - "against_voucher_no": ["!=", _dn], - "amount": ["<", 0], - }, - group_by="against_voucher_no", - fields=["against_voucher_type", "against_voucher_no", "Sum(amount_in_account_currency)"], + criteria = [ + (ple.company == company), + (ple.delinked == 0), + (ple.voucher_no == _dn), + (ple.against_voucher_no != _dn), + ] + + query = ( + qb.from_(ple) + .select( + ple.company, + ple.against_voucher_type.as_("voucher_type"), + ple.against_voucher_no.as_("voucher_no"), + Abs(Sum(ple.amount_in_account_currency)).as_("allocated_amount"), + ) + .where(Criterion.all(criteria)) + .groupby(ple.against_voucher_no) ) + res = query.run(as_dict=True) + return res return [] diff --git a/erpnext/public/js/utils/unreconcile.js b/erpnext/public/js/utils/unreconcile.js index 509cd3941002..46555fe2a2b6 100644 --- a/erpnext/public/js/utils/unreconcile.js +++ b/erpnext/public/js/utils/unreconcile.js @@ -20,6 +20,34 @@ erpnext.accounts.unreconcile_payments = { } }, + build_selection_map(frm, selections) { + // assuming each row is an individual voucher + // pass this to server side method that creates unreconcile doc for each row + let selection_map = []; + if (['Sales Invoice', 'Purchase Invoice'].includes(frm.doc.doctype)) { + selection_map = selections.map(function(elem) { + return { + company: elem.company, + voucher_type: elem.voucher_type, + voucher_no: elem.voucher_no, + against_voucher_type: frm.doc.doctype, + against_voucher_no: frm.doc.name + }; + }); + } else if (['Payment Entry', 'Journal Entry'].includes(frm.doc.doctype)) { + selection_map = selections.map(function(elem) { + return { + company: elem.company, + voucher_type: frm.doc.doctype, + voucher_no: frm.doc.name, + against_voucher_type: elem.voucher_type, + against_voucher_no: elem.voucher_no, + }; + }); + } + return selection_map; + }, + build_unreconcile_dialog(frm) { if (['Sales Invoice', 'Purchase Invoice', 'Payment Entry', 'Journal Entry'].includes(frm.doc.doctype)) { let child_table_fields = [ @@ -61,23 +89,9 @@ erpnext.accounts.unreconcile_payments = { let selected_allocations = values.allocations.filter(x=>x.__checked); if (selected_allocations.length > 0) { - // assuming each row is an individual voucher - // pass this to server side method that creates unreconcile doc for each row - if (['Sales Invoice', 'Purchase Invoice'].includes(frm.doc.doctype)) { - let selection_map = selected_allocations.map(function(elem) { - return { - company: elem.company, - voucher_type: elem.voucher_type, - voucher_no: elem.voucher_no, - against_voucher_type: frm.doc.doctype, - against_voucher_no: frm.doc.name - }; - - }); - - erpnext.utils.create_unreconcile_docs(selection_map); - d.hide(); - } + let selection_map = erpnext.accounts.unreconcile_payments.build_selection_map(frm, selected_allocations); + erpnext.accounts.unreconcile_payments.create_unreconcile_docs(selection_map); + d.hide(); } else { frappe.msgprint("No Selection"); From f4e1959cc7d69d4872b31b530f62d4157bb33063 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Tue, 29 Aug 2023 21:45:17 +0530 Subject: [PATCH 418/501] chore: code cleanup (cherry picked from commit 69683776a5e46c32ba16664264f3aa9bc09a03f5) --- .../doctype/unreconcile_payments/unreconcile_payments.py | 5 ----- 1 file changed, 5 deletions(-) diff --git a/erpnext/accounts/doctype/unreconcile_payments/unreconcile_payments.py b/erpnext/accounts/doctype/unreconcile_payments/unreconcile_payments.py index b6dd363cea55..01f910e56461 100644 --- a/erpnext/accounts/doctype/unreconcile_payments/unreconcile_payments.py +++ b/erpnext/accounts/doctype/unreconcile_payments/unreconcile_payments.py @@ -11,11 +11,6 @@ class UnreconcilePayments(Document): - # def validate(self): - # parent = set([alloc.parent for alloc in self.allocations]) - # if len(parent) != 1: - # pass - @frappe.whitelist() def get_allocations_from_payment(self): if self.voucher_type == "Payment Entry": From 84e4a2509cc08e59c5c5a857cffb8754e90688f6 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Tue, 29 Aug 2023 21:49:21 +0530 Subject: [PATCH 419/501] chore: rename and add trigger in journal entry (cherry picked from commit 0ccb6d8242c8bcb44457745553124629e4dc5434) --- erpnext/accounts/doctype/journal_entry/journal_entry.js | 2 ++ .../doctype/unreconcile_payments/unreconcile_payments.py | 2 +- erpnext/public/js/utils/unreconcile.js | 2 +- 3 files changed, 4 insertions(+), 2 deletions(-) diff --git a/erpnext/accounts/doctype/journal_entry/journal_entry.js b/erpnext/accounts/doctype/journal_entry/journal_entry.js index 7719f0e7c909..fe570a5ba8aa 100644 --- a/erpnext/accounts/doctype/journal_entry/journal_entry.js +++ b/erpnext/accounts/doctype/journal_entry/journal_entry.js @@ -50,6 +50,8 @@ frappe.ui.form.on("Journal Entry", { frm.trigger("make_inter_company_journal_entry"); }, __('Make')); } + + erpnext.accounts.unreconcile_payments.add_unreconcile_btn(frm); }, before_save: function(frm) { if ((frm.doc.docstatus == 0) && (!frm.doc.is_system_generated)) { diff --git a/erpnext/accounts/doctype/unreconcile_payments/unreconcile_payments.py b/erpnext/accounts/doctype/unreconcile_payments/unreconcile_payments.py index 01f910e56461..9b80c0a3f85c 100644 --- a/erpnext/accounts/doctype/unreconcile_payments/unreconcile_payments.py +++ b/erpnext/accounts/doctype/unreconcile_payments/unreconcile_payments.py @@ -54,7 +54,7 @@ def on_submit(self): @frappe.whitelist() -def doc_has_payments(doctype, docname): +def doc_has_references(doctype, docname): if doctype in ["Sales Invoice", "Purchase Invoice"]: return frappe.db.count( "Payment Ledger Entry", diff --git a/erpnext/public/js/utils/unreconcile.js b/erpnext/public/js/utils/unreconcile.js index 46555fe2a2b6..df07643bb7cf 100644 --- a/erpnext/public/js/utils/unreconcile.js +++ b/erpnext/public/js/utils/unreconcile.js @@ -4,7 +4,7 @@ erpnext.accounts.unreconcile_payments = { add_unreconcile_btn(frm) { if (frm.doc.docstatus == 1) { frappe.call({ - "method": "erpnext.accounts.doctype.unreconcile_payments.unreconcile_payments.doc_has_payments", + "method": "erpnext.accounts.doctype.unreconcile_payments.unreconcile_payments.doc_has_references", "args": { "doctype": frm.doc.doctype, "docname": frm.doc.name From 1a69db0f80b3d287e95bf1d34c8d6554718c5f7a Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Wed, 30 Aug 2023 10:02:47 +0530 Subject: [PATCH 420/501] refactor: modularisation and group by voucher_no (cherry picked from commit cce96669f0b651795522ac350319993df1d482e9) --- .../unreconcile_payments.py | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/erpnext/accounts/doctype/unreconcile_payments/unreconcile_payments.py b/erpnext/accounts/doctype/unreconcile_payments/unreconcile_payments.py index 9b80c0a3f85c..8aef772ad585 100644 --- a/erpnext/accounts/doctype/unreconcile_payments/unreconcile_payments.py +++ b/erpnext/accounts/doctype/unreconcile_payments/unreconcile_payments.py @@ -13,6 +13,7 @@ class UnreconcilePayments(Document): @frappe.whitelist() def get_allocations_from_payment(self): + allocated_references = [] if self.voucher_type == "Payment Entry": per = qb.DocType("Payment Entry Reference") allocated_references = ( @@ -24,7 +25,19 @@ def get_allocations_from_payment(self): .groupby(per.reference_name) .run(as_dict=True) ) - return allocated_references + elif self.voucher_type == "Journal Entry": + jea = qb.DocType("Journal Entry Account") + allocated_references = ( + qb.from_(jea) + .select( + jea.reference_type, jea.reference_name, Sum(jea.allocated_amount).as_("allocated_amount") + ) + .where((jea.docstatus == 1) & (jea.parent == self.voucher_no)) + .groupby(jea.reference_name) + .run(as_dict=True) + ) + + return allocated_references def add_references(self): allocations = self.get_allocations_from_payment() @@ -92,7 +105,7 @@ def get_linked_payments_for_doc( Abs(Sum(ple.amount_in_account_currency)).as_("allocated_amount"), ) .where(Criterion.all(criteria)) - .groupby(ple.against_voucher_no) + .groupby(ple.voucher_no, ple.against_voucher_no) .run(as_dict=True) ) return res From cd2d335256ede5980a0757a80fabc5140adc3d10 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Wed, 30 Aug 2023 10:43:00 +0530 Subject: [PATCH 421/501] feat: unreconcile support for journal entry (cherry picked from commit 285963acdba73bfdb0f9e5d7f4fac3d765b282d6) --- .../unreconcile_payments.js | 2 +- .../unreconcile_payments.py | 28 +++++++++++++++---- 2 files changed, 23 insertions(+), 7 deletions(-) diff --git a/erpnext/accounts/doctype/unreconcile_payments/unreconcile_payments.js b/erpnext/accounts/doctype/unreconcile_payments/unreconcile_payments.js index ef7c958113c4..c522567637fb 100644 --- a/erpnext/accounts/doctype/unreconcile_payments/unreconcile_payments.js +++ b/erpnext/accounts/doctype/unreconcile_payments/unreconcile_payments.js @@ -6,7 +6,7 @@ frappe.ui.form.on("Unreconcile Payments", { frm.set_query("voucher_type", function() { return { filters: { - name: "Payment Entry" + name: ["in", ["Payment Entry", "Journal Entry"]] } } }); diff --git a/erpnext/accounts/doctype/unreconcile_payments/unreconcile_payments.py b/erpnext/accounts/doctype/unreconcile_payments/unreconcile_payments.py index 8aef772ad585..a32313f4a5cf 100644 --- a/erpnext/accounts/doctype/unreconcile_payments/unreconcile_payments.py +++ b/erpnext/accounts/doctype/unreconcile_payments/unreconcile_payments.py @@ -2,15 +2,21 @@ # For license information, please see license.txt import frappe -from frappe import qb +from frappe import _, qb from frappe.model.document import Document from frappe.query_builder import Criterion from frappe.query_builder.functions import Abs, Sum +from frappe.utils.data import comma_and from erpnext.accounts.utils import unlink_ref_doc_from_payment_entries, update_voucher_outstanding class UnreconcilePayments(Document): + def validate(self): + self.supported_types = ["Payment Entry", "Journal Entry"] + if not self.voucher_type in self.supported_types: + frappe.throw(_("Only {0} are supported").format(comma_and(self.supported_types))) + @frappe.whitelist() def get_allocations_from_payment(self): allocated_references = [] @@ -26,14 +32,24 @@ def get_allocations_from_payment(self): .run(as_dict=True) ) elif self.voucher_type == "Journal Entry": - jea = qb.DocType("Journal Entry Account") + # for journals, using payment ledger to fetch allocation. + # this way we can avoid vaildating account type and reference details individually on child table + + ple = qb.DocType("Payment Ledger Entry") allocated_references = ( - qb.from_(jea) + qb.from_(ple) .select( - jea.reference_type, jea.reference_name, Sum(jea.allocated_amount).as_("allocated_amount") + ple.against_voucher_type.as_("reference_doctype"), + ple.against_voucher_no.as_("reference_name"), + Abs(Sum(ple.amount_in_account_currency)).as_("allocated_amount"), + ) + .where( + (ple.docstatus == 1) + & (ple.voucher_type == self.voucher_type) + & (ple.voucher_no == self.voucher_no) + & (ple.voucher_no != ple.against_voucher_no) ) - .where((jea.docstatus == 1) & (jea.parent == self.voucher_no)) - .groupby(jea.reference_name) + .groupby(ple.against_voucher_type, ple.against_voucher_no) .run(as_dict=True) ) From 335cb5fd283149884dad53c208b6dd50940df77e Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Wed, 30 Aug 2023 11:02:03 +0530 Subject: [PATCH 422/501] refactor: single fetch and unlinking logic for JE and PE (cherry picked from commit de910ab152801dcfa18fe72d45853680716630b6) --- .../unreconcile_payment_entries.json | 20 ++++++- .../unreconcile_payments.py | 58 ++++++------------- 2 files changed, 38 insertions(+), 40 deletions(-) diff --git a/erpnext/accounts/doctype/unreconcile_payment_entries/unreconcile_payment_entries.json b/erpnext/accounts/doctype/unreconcile_payment_entries/unreconcile_payment_entries.json index c4afaa8bcacd..955c3bbe031d 100644 --- a/erpnext/accounts/doctype/unreconcile_payment_entries/unreconcile_payment_entries.json +++ b/erpnext/accounts/doctype/unreconcile_payment_entries/unreconcile_payment_entries.json @@ -6,6 +6,9 @@ "editable_grid": 1, "engine": "InnoDB", "field_order": [ + "account", + "party_type", + "party", "reference_doctype", "reference_name", "allocated_amount", @@ -39,12 +42,27 @@ "in_list_view": 1, "label": "Reference Type", "options": "DocType" + }, + { + "fieldname": "account", + "fieldtype": "Data", + "label": "Account" + }, + { + "fieldname": "party_type", + "fieldtype": "Data", + "label": "Party Type" + }, + { + "fieldname": "party", + "fieldtype": "Data", + "label": "Party" } ], "index_web_pages_for_search": 1, "istable": 1, "links": [], - "modified": "2023-08-24 14:48:10.018574", + "modified": "2023-08-30 10:58:45.322668", "modified_by": "Administrator", "module": "Accounts", "name": "Unreconcile Payment Entries", diff --git a/erpnext/accounts/doctype/unreconcile_payments/unreconcile_payments.py b/erpnext/accounts/doctype/unreconcile_payments/unreconcile_payments.py index a32313f4a5cf..1688b6e4984f 100644 --- a/erpnext/accounts/doctype/unreconcile_payments/unreconcile_payments.py +++ b/erpnext/accounts/doctype/unreconcile_payments/unreconcile_payments.py @@ -20,38 +20,26 @@ def validate(self): @frappe.whitelist() def get_allocations_from_payment(self): allocated_references = [] - if self.voucher_type == "Payment Entry": - per = qb.DocType("Payment Entry Reference") - allocated_references = ( - qb.from_(per) - .select( - per.reference_doctype, per.reference_name, Sum(per.allocated_amount).as_("allocated_amount") - ) - .where((per.docstatus == 1) & (per.parent == self.voucher_no)) - .groupby(per.reference_name) - .run(as_dict=True) + ple = qb.DocType("Payment Ledger Entry") + allocated_references = ( + qb.from_(ple) + .select( + ple.account, + ple.party_type, + ple.party, + ple.against_voucher_type.as_("reference_doctype"), + ple.against_voucher_no.as_("reference_name"), + Abs(Sum(ple.amount_in_account_currency)).as_("allocated_amount"), ) - elif self.voucher_type == "Journal Entry": - # for journals, using payment ledger to fetch allocation. - # this way we can avoid vaildating account type and reference details individually on child table - - ple = qb.DocType("Payment Ledger Entry") - allocated_references = ( - qb.from_(ple) - .select( - ple.against_voucher_type.as_("reference_doctype"), - ple.against_voucher_no.as_("reference_name"), - Abs(Sum(ple.amount_in_account_currency)).as_("allocated_amount"), - ) - .where( - (ple.docstatus == 1) - & (ple.voucher_type == self.voucher_type) - & (ple.voucher_no == self.voucher_no) - & (ple.voucher_no != ple.against_voucher_no) - ) - .groupby(ple.against_voucher_type, ple.against_voucher_no) - .run(as_dict=True) + .where( + (ple.docstatus == 1) + & (ple.voucher_type == self.voucher_type) + & (ple.voucher_no == self.voucher_no) + & (ple.voucher_no != ple.against_voucher_no) ) + .groupby(ple.against_voucher_type, ple.against_voucher_no) + .run(as_dict=True) + ) return allocated_references @@ -65,19 +53,11 @@ def on_submit(self): # todo: add more granular unlinking # different amounts for same invoice should be individually unlinkable - payment_type, paid_from, paid_to, party_type, party = frappe.db.get_all( - self.voucher_type, - filters={"name": self.voucher_no}, - fields=["payment_type", "paid_from", "paid_to", "party_type", "party"], - as_list=1, - )[0] - account = paid_from if payment_type == "Receive" else paid_to - for alloc in self.allocations: doc = frappe.get_doc(alloc.reference_doctype, alloc.reference_name) unlink_ref_doc_from_payment_entries(doc) update_voucher_outstanding( - alloc.reference_doctype, alloc.reference_name, account, party_type, party + alloc.reference_doctype, alloc.reference_name, alloc.account, alloc.party_type, alloc.party ) frappe.db.set_value("Unreconcile Payment Entries", alloc.name, "unlinked", True) From 3cbaea389b90f6602c3fe1a4e54244adc6d0df3b Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Wed, 30 Aug 2023 13:25:23 +0530 Subject: [PATCH 423/501] refactor: convert raw sql to query_builder (cherry picked from commit 0130aea2aa1ac93cd790af3652ea7b43871c23c8) --- erpnext/accounts/utils.py | 119 ++++++++++++++++++++------------------ 1 file changed, 64 insertions(+), 55 deletions(-) diff --git a/erpnext/accounts/utils.py b/erpnext/accounts/utils.py index 971932e415ab..9777fd534025 100644 --- a/erpnext/accounts/utils.py +++ b/erpnext/accounts/utils.py @@ -693,72 +693,87 @@ def cancel_exchange_gain_loss_journal(parent_doc: dict | object) -> None: frappe.get_doc("Journal Entry", doc[0]).cancel() -def unlink_ref_doc_from_payment_entries(ref_doc): - remove_ref_doc_link_from_jv(ref_doc.doctype, ref_doc.name) - remove_ref_doc_link_from_pe(ref_doc.doctype, ref_doc.name) - - frappe.db.sql( - """update `tabGL Entry` - set against_voucher_type=null, against_voucher=null, - modified=%s, modified_by=%s - where against_voucher_type=%s and against_voucher=%s - and voucher_no != ifnull(against_voucher, '')""", - (now(), frappe.session.user, ref_doc.doctype, ref_doc.name), - ) +def update_accounting_ledgers_after_reference_removal(ref_type: str = None, ref_no: str = None): + # General Ledger + gle = qb.DocType("GL Entry") + qb.update(gle).set(gle.against_voucher_type, None).set(gle.against_voucher, None).set( + gle.modified, now() + ).set(gle.modified_by, frappe.session.user).where( + (gle.against_voucher_type == ref_type) & (gle.against_voucher == ref_no) + ).run() + # Payment Ledger ple = qb.DocType("Payment Ledger Entry") - qb.update(ple).set(ple.against_voucher_type, ple.voucher_type).set( ple.against_voucher_no, ple.voucher_no ).set(ple.modified, now()).set(ple.modified_by, frappe.session.user).where( - (ple.against_voucher_type == ref_doc.doctype) - & (ple.against_voucher_no == ref_doc.name) - & (ple.delinked == 0) + (ple.against_voucher_type == ref_type) & (ple.against_voucher_no == ref_no) & (ple.delinked == 0) ).run() + +def remove_ref_from_advance_section(ref_doc: object = None): if ref_doc.doctype in ("Sales Invoice", "Purchase Invoice"): ref_doc.set("advances", []) + adv_type = qb.DocType(f"{ref_doc.doctype} Advance") + qb.from_(adv_type).delete().where(adv_type.parent == ref_doc.name).run() - frappe.db.sql( - """delete from `tab{0} Advance` where parent = %s""".format(ref_doc.doctype), ref_doc.name - ) + +def unlink_ref_doc_from_payment_entries(ref_doc): + remove_ref_doc_link_from_jv(ref_doc.doctype, ref_doc.name) + remove_ref_doc_link_from_pe(ref_doc.doctype, ref_doc.name) + update_accounting_ledgers_after_reference_removal(ref_doc.doctype, ref_doc.name) def remove_ref_doc_link_from_jv(ref_type, ref_no): - linked_jv = frappe.db.sql_list( - """select parent from `tabJournal Entry Account` - where reference_type=%s and reference_name=%s and docstatus < 2""", - (ref_type, ref_no), + jea = qb.DocType("Journal Entry Account") + + linked_jv = ( + qb.from_(jea) + .select(jea.parent) + .select( + (jea.reference_type == ref_type) & (jea.reference_name == ref_no) & (jea.docstatus.lt(2)) + ) + .run(as_list=1) ) + linked_jv = convert_to_list(linked_jv) if linked_jv: - frappe.db.sql( - """update `tabJournal Entry Account` - set reference_type=null, reference_name = null, - modified=%s, modified_by=%s - where reference_type=%s and reference_name=%s - and docstatus < 2""", - (now(), frappe.session.user, ref_type, ref_no), - ) + qb.update(jea).set(jea.reference_type, None).set(jea.reference_name, None).set( + jea.modified, now() + ).set(jea.modified_by, frappe.session.user).where( + (jea.reference_type == ref_type) & (jea.reference_name == ref_no) + ).run() frappe.msgprint(_("Journal Entries {0} are un-linked").format("\n".join(linked_jv))) +def convert_to_list(result): + """ + Convert tuple to list + """ + return [x[0] for x in result] + + def remove_ref_doc_link_from_pe(ref_type, ref_no): - linked_pe = frappe.db.sql_list( - """select parent from `tabPayment Entry Reference` - where reference_doctype=%s and reference_name=%s and docstatus < 2""", - (ref_type, ref_no), + per = qb.DocType("Payment Entry Reference") + pay = qb.DocType("Payment Entry") + + linked_pe = ( + qb.from_(per) + .select(per.parent) + .where( + (per.reference_doctype == ref_type) & (per.reference_name == ref_no) & (per.docstatus.lt(2)) + ) + .run(as_list=1) ) + linked_pe = convert_to_list(linked_pe) if linked_pe: - frappe.db.sql( - """update `tabPayment Entry Reference` - set allocated_amount=0, modified=%s, modified_by=%s - where reference_doctype=%s and reference_name=%s - and docstatus < 2""", - (now(), frappe.session.user, ref_type, ref_no), - ) + qb.update(per).set(per.allocated_amount, 0).set(per.modified, now()).set( + per.modified_by, frappe.session.user + ).where( + (per.docstatus.lt(2) & (per.reference_doctype == ref_type) & (per.reference_name == ref_no)) + ).run() for pe in linked_pe: try: @@ -772,19 +787,13 @@ def remove_ref_doc_link_from_pe(ref_type, ref_no): msg += _("Please cancel payment entry manually first") frappe.throw(msg, exc=PaymentEntryUnlinkError, title=_("Payment Unlink Error")) - frappe.db.sql( - """update `tabPayment Entry` set total_allocated_amount=%s, - base_total_allocated_amount=%s, unallocated_amount=%s, modified=%s, modified_by=%s - where name=%s""", - ( - pe_doc.total_allocated_amount, - pe_doc.base_total_allocated_amount, - pe_doc.unallocated_amount, - now(), - frappe.session.user, - pe, - ), - ) + qb.update(pay).set(pay.total_allocated_amount, pe_doc.total_allocated_amount).set( + pay.base_total_allocated_amount, pe_doc.base_total_allocated_amount + ).set(pay.unallocated_amount, pe_doc.unallocated_amount).set(pay.modified, now()).set( + pay.modified_by, frappe.session.user + ).where( + pay.name == pe + ).run() frappe.msgprint(_("Payment Entries {0} are un-linked").format("\n".join(linked_pe))) From 8954bd77590ce766635bff24f569a523a735c3b2 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Wed, 30 Aug 2023 16:30:14 +0530 Subject: [PATCH 424/501] chore: type info (cherry picked from commit b4dc2bdf28bf9c1c7043750a4c91d786c230ea6a) --- .../doctype/unreconcile_payments/unreconcile_payments.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/accounts/doctype/unreconcile_payments/unreconcile_payments.py b/erpnext/accounts/doctype/unreconcile_payments/unreconcile_payments.py index 1688b6e4984f..304ccccb0890 100644 --- a/erpnext/accounts/doctype/unreconcile_payments/unreconcile_payments.py +++ b/erpnext/accounts/doctype/unreconcile_payments/unreconcile_payments.py @@ -63,7 +63,7 @@ def on_submit(self): @frappe.whitelist() -def doc_has_references(doctype, docname): +def doc_has_references(doctype: str = None, docname: str = None): if doctype in ["Sales Invoice", "Purchase Invoice"]: return frappe.db.count( "Payment Ledger Entry", From f5718390b74e1430d73947305c1d0e3898f10799 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Wed, 30 Aug 2023 16:44:25 +0530 Subject: [PATCH 425/501] refactor: unlink individual vouchers from payments (cherry picked from commit 9b6eac23b6ad38323c68f42fdcf1a2f8916705a9) --- .../unreconcile_payments.py | 2 +- erpnext/accounts/utils.py | 101 +++++++++++++----- 2 files changed, 75 insertions(+), 28 deletions(-) diff --git a/erpnext/accounts/doctype/unreconcile_payments/unreconcile_payments.py b/erpnext/accounts/doctype/unreconcile_payments/unreconcile_payments.py index 304ccccb0890..5161a9282378 100644 --- a/erpnext/accounts/doctype/unreconcile_payments/unreconcile_payments.py +++ b/erpnext/accounts/doctype/unreconcile_payments/unreconcile_payments.py @@ -55,7 +55,7 @@ def on_submit(self): for alloc in self.allocations: doc = frappe.get_doc(alloc.reference_doctype, alloc.reference_name) - unlink_ref_doc_from_payment_entries(doc) + unlink_ref_doc_from_payment_entries(doc, self.voucher_no) update_voucher_outstanding( alloc.reference_doctype, alloc.reference_name, alloc.account, alloc.party_type, alloc.party ) diff --git a/erpnext/accounts/utils.py b/erpnext/accounts/utils.py index 9777fd534025..77ac3023ca80 100644 --- a/erpnext/accounts/utils.py +++ b/erpnext/accounts/utils.py @@ -693,38 +693,62 @@ def cancel_exchange_gain_loss_journal(parent_doc: dict | object) -> None: frappe.get_doc("Journal Entry", doc[0]).cancel() -def update_accounting_ledgers_after_reference_removal(ref_type: str = None, ref_no: str = None): +def update_accounting_ledgers_after_reference_removal( + ref_type: str = None, ref_no: str = None, payment_name: str = None +): # General Ledger gle = qb.DocType("GL Entry") - qb.update(gle).set(gle.against_voucher_type, None).set(gle.against_voucher, None).set( - gle.modified, now() - ).set(gle.modified_by, frappe.session.user).where( - (gle.against_voucher_type == ref_type) & (gle.against_voucher == ref_no) - ).run() + gle_update_query = ( + qb.update(gle) + .set(gle.against_voucher_type, None) + .set(gle.against_voucher, None) + .set(gle.modified, now()) + .set(gle.modified_by, frappe.session.user) + .where((gle.against_voucher_type == ref_type) & (gle.against_voucher == ref_no)) + ) + + if payment_name: + gle_update_query = gle_update_query.where(gle.voucher_no == payment_name) + gle_update_query.run() # Payment Ledger ple = qb.DocType("Payment Ledger Entry") - qb.update(ple).set(ple.against_voucher_type, ple.voucher_type).set( - ple.against_voucher_no, ple.voucher_no - ).set(ple.modified, now()).set(ple.modified_by, frappe.session.user).where( - (ple.against_voucher_type == ref_type) & (ple.against_voucher_no == ref_no) & (ple.delinked == 0) - ).run() + ple_update_query = ( + qb.update(ple) + .set(ple.against_voucher_type, ple.voucher_type) + .set(ple.against_voucher_no, ple.voucher_no) + .set(ple.modified, now()) + .set(ple.modified_by, frappe.session.user) + .where( + (ple.against_voucher_type == ref_type) + & (ple.against_voucher_no == ref_no) + & (ple.delinked == 0) + ) + ) + + if payment_name: + ple_update_query = ple_update_query.where(ple.voucher_no == payment_name) + ple_update_query.run() def remove_ref_from_advance_section(ref_doc: object = None): + # TODO: this might need some testing if ref_doc.doctype in ("Sales Invoice", "Purchase Invoice"): ref_doc.set("advances", []) adv_type = qb.DocType(f"{ref_doc.doctype} Advance") qb.from_(adv_type).delete().where(adv_type.parent == ref_doc.name).run() -def unlink_ref_doc_from_payment_entries(ref_doc): - remove_ref_doc_link_from_jv(ref_doc.doctype, ref_doc.name) - remove_ref_doc_link_from_pe(ref_doc.doctype, ref_doc.name) - update_accounting_ledgers_after_reference_removal(ref_doc.doctype, ref_doc.name) +def unlink_ref_doc_from_payment_entries(ref_doc: object = None, payment_name: str = None): + remove_ref_doc_link_from_jv(ref_doc.doctype, ref_doc.name, payment_name) + remove_ref_doc_link_from_pe(ref_doc.doctype, ref_doc.name, payment_name) + update_accounting_ledgers_after_reference_removal(ref_doc.doctype, ref_doc.name, payment_name) + remove_ref_from_advance_section(ref_doc) -def remove_ref_doc_link_from_jv(ref_type, ref_no): +def remove_ref_doc_link_from_jv( + ref_type: str = None, ref_no: str = None, payment_name: str = None +): jea = qb.DocType("Journal Entry Account") linked_jv = ( @@ -736,13 +760,23 @@ def remove_ref_doc_link_from_jv(ref_type, ref_no): .run(as_list=1) ) linked_jv = convert_to_list(linked_jv) + # remove reference only from specified payment + linked_jv = [x for x in linked_jv if x == payment_name] if payment_name else linked_jv if linked_jv: - qb.update(jea).set(jea.reference_type, None).set(jea.reference_name, None).set( - jea.modified, now() - ).set(jea.modified_by, frappe.session.user).where( - (jea.reference_type == ref_type) & (jea.reference_name == ref_no) - ).run() + update_query = ( + qb.update(jea) + .set(jea.reference_type, None) + .set(jea.reference_name, None) + .set(jea.modified, now()) + .set(jea.modified_by, frappe.session.user) + .where((jea.reference_type == ref_type) & (jea.reference_name == ref_no)) + ) + + if payment_name: + update_query = update_query.where(jea.parent == payment_name) + + update_query.run() frappe.msgprint(_("Journal Entries {0} are un-linked").format("\n".join(linked_jv))) @@ -754,7 +788,9 @@ def convert_to_list(result): return [x[0] for x in result] -def remove_ref_doc_link_from_pe(ref_type, ref_no): +def remove_ref_doc_link_from_pe( + ref_type: str = None, ref_no: str = None, payment_name: str = None +): per = qb.DocType("Payment Entry Reference") pay = qb.DocType("Payment Entry") @@ -767,13 +803,24 @@ def remove_ref_doc_link_from_pe(ref_type, ref_no): .run(as_list=1) ) linked_pe = convert_to_list(linked_pe) + # remove reference only from specified payment + linked_pe = [x for x in linked_pe if x == payment_name] if payment_name else linked_pe if linked_pe: - qb.update(per).set(per.allocated_amount, 0).set(per.modified, now()).set( - per.modified_by, frappe.session.user - ).where( - (per.docstatus.lt(2) & (per.reference_doctype == ref_type) & (per.reference_name == ref_no)) - ).run() + update_query = ( + qb.update(per) + .set(per.allocated_amount, 0) + .set(per.modified, now()) + .set(per.modified_by, frappe.session.user) + .where( + (per.docstatus.lt(2) & (per.reference_doctype == ref_type) & (per.reference_name == ref_no)) + ) + ) + + if payment_name: + update_query = update_query.where(per.parent == payment_name) + + update_query.run() for pe in linked_pe: try: From cf308912a17829499dee2f68ed9c15dca92b6910 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Wed, 30 Aug 2023 17:03:02 +0530 Subject: [PATCH 426/501] test: more granular unreconciliation (cherry picked from commit 67980188a7a673d42848005b5b1ebbad9a6c98df) --- .../test_unreconcile_payments.py | 109 ++++++++++++++++++ 1 file changed, 109 insertions(+) diff --git a/erpnext/accounts/doctype/unreconcile_payments/test_unreconcile_payments.py b/erpnext/accounts/doctype/unreconcile_payments/test_unreconcile_payments.py index 2bb8a54c3508..924a950c4fef 100644 --- a/erpnext/accounts/doctype/unreconcile_payments/test_unreconcile_payments.py +++ b/erpnext/accounts/doctype/unreconcile_payments/test_unreconcile_payments.py @@ -99,3 +99,112 @@ def test_01_unreconcile_invoice(self): pe.reload() self.assertEqual(len(pe.references), 1) + self.assertEqual(pe.unallocated_amount, 100) + + def test_02_unreconcile_one_payment_from_multi_payments(self): + """ + Scenario: 2 payments, both split against 2 invoices + Unreconcile only one payment from one invoice + """ + si1 = create_sales_invoice( + item=self.item, + company=self.company, + customer=self.customer, + debit_to=self.debit_to, + posting_date=today(), + parent_cost_center=self.cost_center, + cost_center=self.cost_center, + rate=100, + price_list_rate=100, + ) + + si2 = create_sales_invoice( + item=self.item, + company=self.company, + customer=self.customer, + debit_to=self.debit_to, + posting_date=today(), + parent_cost_center=self.cost_center, + cost_center=self.cost_center, + rate=100, + price_list_rate=100, + ) + + pe1 = create_payment_entry( + company=self.company, + payment_type="Receive", + party_type="Customer", + party=self.customer, + paid_from=self.debit_to, + paid_to=self.cash, + paid_amount=100, + save=True, + ) + pe1.append( + "references", + {"reference_doctype": si1.doctype, "reference_name": si1.name, "allocated_amount": 50}, + ) + pe1.append( + "references", + {"reference_doctype": si2.doctype, "reference_name": si2.name, "allocated_amount": 50}, + ) + # Allocation payment against both invoices + pe1.save().submit() + + pe2 = create_payment_entry( + company=self.company, + payment_type="Receive", + party_type="Customer", + party=self.customer, + paid_from=self.debit_to, + paid_to=self.cash, + paid_amount=100, + save=True, + ) + pe2.append( + "references", + {"reference_doctype": si1.doctype, "reference_name": si1.name, "allocated_amount": 50}, + ) + pe2.append( + "references", + {"reference_doctype": si2.doctype, "reference_name": si2.name, "allocated_amount": 50}, + ) + # Allocation payment against both invoices + pe2.save().submit() + + # Assert outstanding + si1.reload() + si2.reload() + self.assertEqual(si1.outstanding_amount, 0) + self.assertEqual(si2.outstanding_amount, 0) + + unreconcile = frappe.get_doc( + { + "doctype": "Unreconcile Payments", + "company": self.company, + "voucher_type": pe2.doctype, + "voucher_no": pe2.name, + } + ) + unreconcile.add_references() + self.assertEqual(len(unreconcile.allocations), 2) + allocations = [x.reference_name for x in unreconcile.allocations] + self.assertEquals([si1.name, si2.name], allocations) + # unreconcile si1 from pe2 + for x in unreconcile.allocations: + if x.reference_name != si1.name: + unreconcile.remove(x) + unreconcile.save().submit() + + # Assert outstanding + si1.reload() + si2.reload() + self.assertEqual(si1.outstanding_amount, 50) + self.assertEqual(si2.outstanding_amount, 0) + + pe1.reload() + pe2.reload() + self.assertEqual(len(pe1.references), 2) + self.assertEqual(len(pe2.references), 1) + self.assertEqual(pe1.unallocated_amount, 0) + self.assertEqual(pe2.unallocated_amount, 50) From 606c99e57cdd36951ef8472df58dcf54e862e476 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Wed, 30 Aug 2023 20:50:16 +0530 Subject: [PATCH 427/501] fix: typo in doctype name and qb (cherry picked from commit 9a1588f1cccc5336ca7a7f45be66f84ab3dc1e06) --- erpnext/accounts/utils.py | 4 +--- erpnext/controllers/accounts_controller.py | 2 +- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/erpnext/accounts/utils.py b/erpnext/accounts/utils.py index 77ac3023ca80..a78ef2e93847 100644 --- a/erpnext/accounts/utils.py +++ b/erpnext/accounts/utils.py @@ -754,9 +754,7 @@ def remove_ref_doc_link_from_jv( linked_jv = ( qb.from_(jea) .select(jea.parent) - .select( - (jea.reference_type == ref_type) & (jea.reference_name == ref_no) & (jea.docstatus.lt(2)) - ) + .where((jea.reference_type == ref_type) & (jea.reference_name == ref_no) & (jea.docstatus.lt(2))) .run(as_list=1) ) linked_jv = convert_to_list(linked_jv) diff --git a/erpnext/controllers/accounts_controller.py b/erpnext/controllers/accounts_controller.py index ffd8870553bd..2c919b2288d5 100644 --- a/erpnext/controllers/accounts_controller.py +++ b/erpnext/controllers/accounts_controller.py @@ -212,7 +212,7 @@ def before_cancel(self): validate_einvoice_fields(self) def _remove_references_in_unreconcile(self): - upe = frappe.qb.DocType("UnReconcile Payment Entries") + upe = frappe.qb.DocType("Unreconcile Payment Entries") rows = ( frappe.qb.from_(upe) .select(upe.name, upe.parent) From 669d692844e1a0d3f66d96988081886b5616ce50 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Tue, 5 Sep 2023 08:53:10 +0530 Subject: [PATCH 428/501] refactor: display allocated amount in account currency with symbol (cherry picked from commit 6fd1c1bca263b05cf035c97fcf56d209c137b61b) --- .../unreconcile_payment_entries.json | 15 ++++++++++++--- .../unreconcile_payments/unreconcile_payments.py | 2 ++ erpnext/public/js/utils/unreconcile.js | 5 +++-- 3 files changed, 17 insertions(+), 5 deletions(-) diff --git a/erpnext/accounts/doctype/unreconcile_payment_entries/unreconcile_payment_entries.json b/erpnext/accounts/doctype/unreconcile_payment_entries/unreconcile_payment_entries.json index 955c3bbe031d..42da669e6500 100644 --- a/erpnext/accounts/doctype/unreconcile_payment_entries/unreconcile_payment_entries.json +++ b/erpnext/accounts/doctype/unreconcile_payment_entries/unreconcile_payment_entries.json @@ -12,6 +12,7 @@ "reference_doctype", "reference_name", "allocated_amount", + "account_currency", "unlinked" ], "fields": [ @@ -24,9 +25,10 @@ }, { "fieldname": "allocated_amount", - "fieldtype": "Int", + "fieldtype": "Currency", "in_list_view": 1, - "label": "Allocated Amount" + "label": "Allocated Amount", + "options": "account_currency" }, { "default": "0", @@ -57,12 +59,19 @@ "fieldname": "party", "fieldtype": "Data", "label": "Party" + }, + { + "fieldname": "account_currency", + "fieldtype": "Link", + "label": "Account Currency", + "options": "Currency", + "read_only": 1 } ], "index_web_pages_for_search": 1, "istable": 1, "links": [], - "modified": "2023-08-30 10:58:45.322668", + "modified": "2023-09-05 09:33:28.620149", "modified_by": "Administrator", "module": "Accounts", "name": "Unreconcile Payment Entries", diff --git a/erpnext/accounts/doctype/unreconcile_payments/unreconcile_payments.py b/erpnext/accounts/doctype/unreconcile_payments/unreconcile_payments.py index 5161a9282378..25f85db71f77 100644 --- a/erpnext/accounts/doctype/unreconcile_payments/unreconcile_payments.py +++ b/erpnext/accounts/doctype/unreconcile_payments/unreconcile_payments.py @@ -30,6 +30,7 @@ def get_allocations_from_payment(self): ple.against_voucher_type.as_("reference_doctype"), ple.against_voucher_no.as_("reference_name"), Abs(Sum(ple.amount_in_account_currency)).as_("allocated_amount"), + ple.account_currency, ) .where( (ple.docstatus == 1) @@ -99,6 +100,7 @@ def get_linked_payments_for_doc( ple.voucher_type, ple.voucher_no, Abs(Sum(ple.amount_in_account_currency)).as_("allocated_amount"), + ple.account_currency, ) .where(Criterion.all(criteria)) .groupby(ple.voucher_no, ple.against_voucher_no) diff --git a/erpnext/public/js/utils/unreconcile.js b/erpnext/public/js/utils/unreconcile.js index df07643bb7cf..cd44f3578b01 100644 --- a/erpnext/public/js/utils/unreconcile.js +++ b/erpnext/public/js/utils/unreconcile.js @@ -53,7 +53,8 @@ erpnext.accounts.unreconcile_payments = { let child_table_fields = [ { label: __("Voucher Type"), fieldname: "voucher_type", fieldtype: "Dynamic Link", options: "DocType", in_list_view: 1, read_only: 1}, { label: __("Voucher No"), fieldname: "voucher_no", fieldtype: "Link", options: "voucher_type", in_list_view: 1, read_only: 1 }, - { label: __("Allocated Amount"), fieldname: "allocated_amount", fieldtype: "Float", in_list_view: 1, read_only: 1 }, + { label: __("Allocated Amount"), fieldname: "allocated_amount", fieldtype: "Currency", in_list_view: 1, read_only: 1 , options: "account_currency"}, + { label: __("Currency"), fieldname: "account_currency", fieldtype: "Currency", read_only: 1}, ] let unreconcile_dialog_fields = [ { @@ -83,7 +84,7 @@ erpnext.accounts.unreconcile_payments = { title: 'Un-Reconcile Allocations', fields: unreconcile_dialog_fields, size: 'large', - cannot_add_rows: 1, + cannot_add_rows: true, primary_action_label: 'Un-Reconcile', primary_action(values) { From 75d3093aea6c286230c6323077622211d610f7e0 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Tue, 5 Sep 2023 09:52:36 +0530 Subject: [PATCH 429/501] refactor: only cancel specific gain/loss je (cherry picked from commit 5dbcf7d2b94dae06ef7fc31b3142606d65611bff) --- erpnext/accounts/utils.py | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/erpnext/accounts/utils.py b/erpnext/accounts/utils.py index a78ef2e93847..76339713a223 100644 --- a/erpnext/accounts/utils.py +++ b/erpnext/accounts/utils.py @@ -663,7 +663,9 @@ def update_reference_in_payment_entry( payment_entry.save(ignore_permissions=True) -def cancel_exchange_gain_loss_journal(parent_doc: dict | object) -> None: +def cancel_exchange_gain_loss_journal( + parent_doc: dict | object, referenced_dt: str = None, referenced_dn: str = None +) -> None: """ Cancel Exchange Gain/Loss for Sales/Purchase Invoice, if they have any. """ @@ -690,7 +692,18 @@ def cancel_exchange_gain_loss_journal(parent_doc: dict | object) -> None: as_list=1, ) for doc in gain_loss_journals: - frappe.get_doc("Journal Entry", doc[0]).cancel() + gain_loss_je = frappe.get_doc("Journal Entry", doc[0]) + if referenced_dt and referenced_dn: + references = [(x.reference_type, x.reference_name) for x in gain_loss_je.accounts] + if ( + len(references) == 2 + and (referenced_dt, referenced_dn) in references + and (parent_doc.doctype, parent_doc.name) in references + ): + # only cancel JE generated against parent_doc and referenced_dn + gain_loss_je.cancel() + else: + gain_loss_je.cancel() def update_accounting_ledgers_after_reference_removal( From 4bd83b5058d439d7aedc3589806118f9d0e8c22d Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Tue, 5 Sep 2023 09:59:34 +0530 Subject: [PATCH 430/501] refactor: cancel gain/loss JE on multi currency transactions (cherry picked from commit 1d93d66c30e69bfcb277123462bf822aa3c3c1d4) --- .../unreconcile_payments/unreconcile_payments.py | 13 +++++++++---- erpnext/public/js/utils/unreconcile.js | 6 ++++++ 2 files changed, 15 insertions(+), 4 deletions(-) diff --git a/erpnext/accounts/doctype/unreconcile_payments/unreconcile_payments.py b/erpnext/accounts/doctype/unreconcile_payments/unreconcile_payments.py index 25f85db71f77..4f9fb50d463c 100644 --- a/erpnext/accounts/doctype/unreconcile_payments/unreconcile_payments.py +++ b/erpnext/accounts/doctype/unreconcile_payments/unreconcile_payments.py @@ -8,7 +8,11 @@ from frappe.query_builder.functions import Abs, Sum from frappe.utils.data import comma_and -from erpnext.accounts.utils import unlink_ref_doc_from_payment_entries, update_voucher_outstanding +from erpnext.accounts.utils import ( + cancel_exchange_gain_loss_journal, + unlink_ref_doc_from_payment_entries, + update_voucher_outstanding, +) class UnreconcilePayments(Document): @@ -51,12 +55,11 @@ def add_references(self): self.append("allocations", alloc) def on_submit(self): - # todo: add more granular unlinking - # different amounts for same invoice should be individually unlinkable - + # todo: more granular unreconciliation for alloc in self.allocations: doc = frappe.get_doc(alloc.reference_doctype, alloc.reference_name) unlink_ref_doc_from_payment_entries(doc, self.voucher_no) + cancel_exchange_gain_loss_journal(doc, self.voucher_type, self.voucher_no) update_voucher_outstanding( alloc.reference_doctype, alloc.reference_name, alloc.account, alloc.party_type, alloc.party ) @@ -104,6 +107,7 @@ def get_linked_payments_for_doc( ) .where(Criterion.all(criteria)) .groupby(ple.voucher_no, ple.against_voucher_no) + .having(qb.Field("allocated_amount") > 0) .run(as_dict=True) ) return res @@ -122,6 +126,7 @@ def get_linked_payments_for_doc( ple.against_voucher_type.as_("voucher_type"), ple.against_voucher_no.as_("voucher_no"), Abs(Sum(ple.amount_in_account_currency)).as_("allocated_amount"), + ple.account_currency, ) .where(Criterion.all(criteria)) .groupby(ple.against_voucher_no) diff --git a/erpnext/public/js/utils/unreconcile.js b/erpnext/public/js/utils/unreconcile.js index cd44f3578b01..acc77a64b017 100644 --- a/erpnext/public/js/utils/unreconcile.js +++ b/erpnext/public/js/utils/unreconcile.js @@ -3,6 +3,12 @@ frappe.provide('erpnext.accounts'); erpnext.accounts.unreconcile_payments = { add_unreconcile_btn(frm) { if (frm.doc.docstatus == 1) { + if(((frm.doc.doctype == "Journal Entry") && (frm.doc.voucher_type != "Journal Entry")) + || !["Purchase Invoice", "Sales Invoice", "Journal Entry", "Payment Entry"].includes(frm.doc.doctype) + ) { + return; + } + frappe.call({ "method": "erpnext.accounts.doctype.unreconcile_payments.unreconcile_payments.doc_has_references", "args": { From aba51ee352d9f16ce6f9ae16a7538b4e43dbfd58 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Fri, 8 Sep 2023 21:43:23 +0530 Subject: [PATCH 431/501] refactor(test): more modularization (cherry picked from commit 5c09fdf9419e301ffbf3787db02d689758f4757e) --- .../test_unreconcile_payments.py | 108 +++++------------- 1 file changed, 30 insertions(+), 78 deletions(-) diff --git a/erpnext/accounts/doctype/unreconcile_payments/test_unreconcile_payments.py b/erpnext/accounts/doctype/unreconcile_payments/test_unreconcile_payments.py index 924a950c4fef..3d7c6cbe3217 100644 --- a/erpnext/accounts/doctype/unreconcile_payments/test_unreconcile_payments.py +++ b/erpnext/accounts/doctype/unreconcile_payments/test_unreconcile_payments.py @@ -20,20 +20,8 @@ def setUp(self): def tearDown(self): frappe.db.rollback() - def test_01_unreconcile_invoice(self): - si1 = create_sales_invoice( - item=self.item, - company=self.company, - customer=self.customer, - debit_to=self.debit_to, - posting_date=today(), - parent_cost_center=self.cost_center, - cost_center=self.cost_center, - rate=100, - price_list_rate=100, - ) - - si2 = create_sales_invoice( + def create_sales_invoice(self): + si = create_sales_invoice( item=self.item, company=self.company, customer=self.customer, @@ -44,7 +32,9 @@ def test_01_unreconcile_invoice(self): rate=100, price_list_rate=100, ) + return si + def create_payment_entry(self): pe = create_payment_entry( company=self.company, payment_type="Receive", @@ -55,7 +45,13 @@ def test_01_unreconcile_invoice(self): paid_amount=200, save=True, ) + return pe + def test_01_unreconcile_invoice(self): + si1 = self.create_sales_invoice() + si2 = self.create_sales_invoice() + + pe = self.create_payment_entry() pe.append( "references", {"reference_doctype": si1.doctype, "reference_name": si1.name, "allocated_amount": 100}, @@ -68,10 +64,10 @@ def test_01_unreconcile_invoice(self): pe.save().submit() # Assert outstanding - si1.reload() - si2.reload() + [doc.reload() for doc in [si1, si2, pe]] self.assertEqual(si1.outstanding_amount, 0) self.assertEqual(si2.outstanding_amount, 0) + self.assertEqual(pe.unallocated_amount, 0) unreconcile = frappe.get_doc( { @@ -92,54 +88,22 @@ def test_01_unreconcile_invoice(self): unreconcile.save().submit() # Assert outstanding - si1.reload() - si2.reload() + [doc.reload() for doc in [si1, si2, pe]] self.assertEqual(si1.outstanding_amount, 100) self.assertEqual(si2.outstanding_amount, 0) - - pe.reload() self.assertEqual(len(pe.references), 1) self.assertEqual(pe.unallocated_amount, 100) def test_02_unreconcile_one_payment_from_multi_payments(self): """ - Scenario: 2 payments, both split against 2 invoices + Scenario: 2 payments, both split against 2 different invoices Unreconcile only one payment from one invoice """ - si1 = create_sales_invoice( - item=self.item, - company=self.company, - customer=self.customer, - debit_to=self.debit_to, - posting_date=today(), - parent_cost_center=self.cost_center, - cost_center=self.cost_center, - rate=100, - price_list_rate=100, - ) - - si2 = create_sales_invoice( - item=self.item, - company=self.company, - customer=self.customer, - debit_to=self.debit_to, - posting_date=today(), - parent_cost_center=self.cost_center, - cost_center=self.cost_center, - rate=100, - price_list_rate=100, - ) - - pe1 = create_payment_entry( - company=self.company, - payment_type="Receive", - party_type="Customer", - party=self.customer, - paid_from=self.debit_to, - paid_to=self.cash, - paid_amount=100, - save=True, - ) + si1 = self.create_sales_invoice() + si2 = self.create_sales_invoice() + pe1 = self.create_payment_entry() + pe1.paid_amount = 100 + # Allocate payment against both invoices pe1.append( "references", {"reference_doctype": si1.doctype, "reference_name": si1.name, "allocated_amount": 50}, @@ -148,19 +112,11 @@ def test_02_unreconcile_one_payment_from_multi_payments(self): "references", {"reference_doctype": si2.doctype, "reference_name": si2.name, "allocated_amount": 50}, ) - # Allocation payment against both invoices pe1.save().submit() - pe2 = create_payment_entry( - company=self.company, - payment_type="Receive", - party_type="Customer", - party=self.customer, - paid_from=self.debit_to, - paid_to=self.cash, - paid_amount=100, - save=True, - ) + pe2 = self.create_payment_entry() + pe2.paid_amount = 100 + # Allocate payment against both invoices pe2.append( "references", {"reference_doctype": si1.doctype, "reference_name": si1.name, "allocated_amount": 50}, @@ -169,14 +125,14 @@ def test_02_unreconcile_one_payment_from_multi_payments(self): "references", {"reference_doctype": si2.doctype, "reference_name": si2.name, "allocated_amount": 50}, ) - # Allocation payment against both invoices pe2.save().submit() - # Assert outstanding - si1.reload() - si2.reload() - self.assertEqual(si1.outstanding_amount, 0) - self.assertEqual(si2.outstanding_amount, 0) + # Assert outstanding and unallocated + [doc.reload() for doc in [si1, si2, pe1, pe2]] + self.assertEqual(si1.outstanding_amount, 0.0) + self.assertEqual(si2.outstanding_amount, 0.0) + self.assertEqual(pe1.unallocated_amount, 0.0) + self.assertEqual(pe2.unallocated_amount, 0.0) unreconcile = frappe.get_doc( { @@ -196,14 +152,10 @@ def test_02_unreconcile_one_payment_from_multi_payments(self): unreconcile.remove(x) unreconcile.save().submit() - # Assert outstanding - si1.reload() - si2.reload() + # Assert outstanding and unallocated + [doc.reload() for doc in [si1, si2, pe1, pe2]] self.assertEqual(si1.outstanding_amount, 50) self.assertEqual(si2.outstanding_amount, 0) - - pe1.reload() - pe2.reload() self.assertEqual(len(pe1.references), 2) self.assertEqual(len(pe2.references), 1) self.assertEqual(pe1.unallocated_amount, 0) From 63274527d20c8a84d0aec855599a2b4cb2016b28 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Sat, 9 Sep 2023 07:24:56 +0530 Subject: [PATCH 432/501] test: multi currency invoice unreconciliation exchange gain/loss associated with the unreconcile invoice should be cancelled as well (cherry picked from commit d3987757151949a6ad37e906f3a14d0863307b97) --- .../test_unreconcile_payments.py | 156 +++++++++++++++++- 1 file changed, 155 insertions(+), 1 deletion(-) diff --git a/erpnext/accounts/doctype/unreconcile_payments/test_unreconcile_payments.py b/erpnext/accounts/doctype/unreconcile_payments/test_unreconcile_payments.py index 3d7c6cbe3217..78e04bff8198 100644 --- a/erpnext/accounts/doctype/unreconcile_payments/test_unreconcile_payments.py +++ b/erpnext/accounts/doctype/unreconcile_payments/test_unreconcile_payments.py @@ -14,13 +14,14 @@ class TestUnreconcilePayments(AccountsTestMixin, FrappeTestCase): def setUp(self): self.create_company() self.create_customer() + self.create_usd_receivable_account() self.create_item() self.clear_old_entries() def tearDown(self): frappe.db.rollback() - def create_sales_invoice(self): + def create_sales_invoice(self, do_not_submit=False): si = create_sales_invoice( item=self.item, company=self.company, @@ -31,6 +32,7 @@ def create_sales_invoice(self): cost_center=self.cost_center, rate=100, price_list_rate=100, + do_not_submit=do_not_submit, ) return si @@ -160,3 +162,155 @@ def test_02_unreconcile_one_payment_from_multi_payments(self): self.assertEqual(len(pe2.references), 1) self.assertEqual(pe1.unallocated_amount, 0) self.assertEqual(pe2.unallocated_amount, 50) + + def test_03_unreconciliation_on_multi_currency_invoice(self): + self.create_customer("_Test MC Customer USD", "USD") + si1 = self.create_sales_invoice(do_not_submit=True) + si1.currency = "USD" + si1.debit_to = self.debtors_usd + si1.conversion_rate = 80 + si1.save().submit() + + si2 = self.create_sales_invoice(do_not_submit=True) + si2.currency = "USD" + si2.debit_to = self.debtors_usd + si2.conversion_rate = 80 + si2.save().submit() + + pe = self.create_payment_entry() + pe.paid_from = self.debtors_usd + pe.paid_from_account_currency = "USD" + pe.source_exchange_rate = 75 + pe.received_amount = 75 * 200 + pe.save() + # Allocate payment against both invoices + pe.append( + "references", + {"reference_doctype": si1.doctype, "reference_name": si1.name, "allocated_amount": 100}, + ) + pe.append( + "references", + {"reference_doctype": si2.doctype, "reference_name": si2.name, "allocated_amount": 100}, + ) + pe.save().submit() + + unreconcile = frappe.get_doc( + { + "doctype": "Unreconcile Payments", + "company": self.company, + "voucher_type": pe.doctype, + "voucher_no": pe.name, + } + ) + unreconcile.add_references() + self.assertEqual(len(unreconcile.allocations), 2) + allocations = [x.reference_name for x in unreconcile.allocations] + self.assertEquals([si1.name, si2.name], allocations) + # unreconcile si1 from pe + for x in unreconcile.allocations: + if x.reference_name != si1.name: + unreconcile.remove(x) + unreconcile.save().submit() + + # Assert outstanding and unallocated + [doc.reload() for doc in [si1, si2, pe]] + self.assertEqual(si1.outstanding_amount, 100) + self.assertEqual(si2.outstanding_amount, 0) + self.assertEqual(len(pe.references), 1) + self.assertEqual(pe.unallocated_amount, 100) + + # Exc gain/loss JE should've been cancelled as well + self.assertEqual( + frappe.db.count( + "Journal Entry Account", + filters={"reference_type": si1.doctype, "reference_name": si1.name, "docstatus": 1}, + ), + 0, + ) + + def test_04_unreconciliation_on_multi_currency_invoice(self): + """ + 2 payments split against 2 foreign currency invoices + """ + self.create_customer("_Test MC Customer USD", "USD") + si1 = self.create_sales_invoice(do_not_submit=True) + si1.currency = "USD" + si1.debit_to = self.debtors_usd + si1.conversion_rate = 80 + si1.save().submit() + + si2 = self.create_sales_invoice(do_not_submit=True) + si2.currency = "USD" + si2.debit_to = self.debtors_usd + si2.conversion_rate = 80 + si2.save().submit() + + pe1 = self.create_payment_entry() + pe1.paid_from = self.debtors_usd + pe1.paid_from_account_currency = "USD" + pe1.source_exchange_rate = 75 + pe1.received_amount = 75 * 100 + pe1.save() + # Allocate payment against both invoices + pe1.append( + "references", + {"reference_doctype": si1.doctype, "reference_name": si1.name, "allocated_amount": 50}, + ) + pe1.append( + "references", + {"reference_doctype": si2.doctype, "reference_name": si2.name, "allocated_amount": 50}, + ) + pe1.save().submit() + + pe2 = self.create_payment_entry() + pe2.paid_from = self.debtors_usd + pe2.paid_from_account_currency = "USD" + pe2.source_exchange_rate = 75 + pe2.received_amount = 75 * 100 + pe2.save() + # Allocate payment against both invoices + pe2.append( + "references", + {"reference_doctype": si1.doctype, "reference_name": si1.name, "allocated_amount": 50}, + ) + pe2.append( + "references", + {"reference_doctype": si2.doctype, "reference_name": si2.name, "allocated_amount": 50}, + ) + pe2.save().submit() + + unreconcile = frappe.get_doc( + { + "doctype": "Unreconcile Payments", + "company": self.company, + "voucher_type": pe2.doctype, + "voucher_no": pe2.name, + } + ) + unreconcile.add_references() + self.assertEqual(len(unreconcile.allocations), 2) + allocations = [x.reference_name for x in unreconcile.allocations] + self.assertEquals([si1.name, si2.name], allocations) + # unreconcile si1 from pe2 + for x in unreconcile.allocations: + if x.reference_name != si1.name: + unreconcile.remove(x) + unreconcile.save().submit() + + # Assert outstanding and unallocated + [doc.reload() for doc in [si1, si2, pe1, pe2]] + self.assertEqual(si1.outstanding_amount, 50) + self.assertEqual(si2.outstanding_amount, 0) + self.assertEqual(len(pe1.references), 2) + self.assertEqual(len(pe2.references), 1) + self.assertEqual(pe1.unallocated_amount, 0) + self.assertEqual(pe2.unallocated_amount, 50) + + # Exc gain/loss JE from PE1 should be available + self.assertEqual( + frappe.db.count( + "Journal Entry Account", + filters={"reference_type": si1.doctype, "reference_name": si1.name, "docstatus": 1}, + ), + 1, + ) From 8fc705ea6a92de51abeffc73f5c2cd2d787e981b Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Mon, 9 Oct 2023 15:17:15 +0530 Subject: [PATCH 433/501] chore: resolve conflicts --- erpnext/accounts/doctype/payment_entry/payment_entry.js | 4 ---- erpnext/public/js/erpnext.bundle.js | 4 ---- 2 files changed, 8 deletions(-) diff --git a/erpnext/accounts/doctype/payment_entry/payment_entry.js b/erpnext/accounts/doctype/payment_entry/payment_entry.js index 48a18e95b7a5..d5b047389edb 100644 --- a/erpnext/accounts/doctype/payment_entry/payment_entry.js +++ b/erpnext/accounts/doctype/payment_entry/payment_entry.js @@ -152,11 +152,7 @@ frappe.ui.form.on('Payment Entry', { frm.events.hide_unhide_fields(frm); frm.events.set_dynamic_labels(frm); frm.events.show_general_ledger(frm); -<<<<<<< HEAD -======= - erpnext.accounts.ledger_preview.show_accounting_ledger_preview(frm); erpnext.accounts.unreconcile_payments.add_unreconcile_btn(frm); ->>>>>>> 25fe752185 (chore: move functions to a separate file in utils) }, validate_company: (frm) => { diff --git a/erpnext/public/js/erpnext.bundle.js b/erpnext/public/js/erpnext.bundle.js index 5df6318a1cd6..730ee23173db 100644 --- a/erpnext/public/js/erpnext.bundle.js +++ b/erpnext/public/js/erpnext.bundle.js @@ -18,11 +18,7 @@ import "./utils/customer_quick_entry"; import "./utils/supplier_quick_entry"; import "./call_popup/call_popup"; import "./utils/dimension_tree_filter"; -<<<<<<< HEAD -======= -import "./utils/ledger_preview.js"; import "./utils/unreconcile.js"; ->>>>>>> 25fe752185 (chore: move functions to a separate file in utils) import "./utils/barcode_scanner"; import "./telephony"; import "./templates/call_link.html"; From 24852e46c17a0274de4b222004ecc838fb468831 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Mon, 9 Oct 2023 20:56:43 +0530 Subject: [PATCH 434/501] chore: rewrite query using query builder (backport #37310) (#37415) * chore: rewrite query using query builder (cherry picked from commit 25718f5cc758a30b6641e7c9be12f4386dc81a28) * chore: fix shopping cart tests (cherry picked from commit fb51cae88b20274a836165a9b7aa70fe1a436ff4) --------- Co-authored-by: Deepesh Garg --- .../crm/report/lead_details/lead_details.py | 77 ++++++------- .../shopping_cart/test_shopping_cart.py | 103 +++++++++++++----- 2 files changed, 110 insertions(+), 70 deletions(-) diff --git a/erpnext/crm/report/lead_details/lead_details.py b/erpnext/crm/report/lead_details/lead_details.py index 7b8c43b2d65f..98dfbec18bee 100644 --- a/erpnext/crm/report/lead_details/lead_details.py +++ b/erpnext/crm/report/lead_details/lead_details.py @@ -4,6 +4,7 @@ import frappe from frappe import _ +from frappe.query_builder.functions import Concat_ws, Date def execute(filters=None): @@ -69,53 +70,41 @@ def get_columns(): def get_data(filters): - return frappe.db.sql( - """ - SELECT - `tabLead`.name, - `tabLead`.lead_name, - `tabLead`.status, - `tabLead`.lead_owner, - `tabLead`.territory, - `tabLead`.source, - `tabLead`.email_id, - `tabLead`.mobile_no, - `tabLead`.phone, - `tabLead`.owner, - `tabLead`.company, - concat_ws(', ', - trim(',' from `tabAddress`.address_line1), - trim(',' from tabAddress.address_line2) - ) AS address, - `tabAddress`.state, - `tabAddress`.pincode, - `tabAddress`.country - FROM - `tabLead` left join `tabDynamic Link` on ( - `tabLead`.name = `tabDynamic Link`.link_name and - `tabDynamic Link`.parenttype = 'Address') - left join `tabAddress` on ( - `tabAddress`.name=`tabDynamic Link`.parent) - WHERE - company = %(company)s - AND DATE(`tabLead`.creation) BETWEEN %(from_date)s AND %(to_date)s - {conditions} - ORDER BY - `tabLead`.creation asc """.format( - conditions=get_conditions(filters) - ), - filters, - as_dict=1, - ) - + lead = frappe.qb.DocType("Lead") + address = frappe.qb.DocType("Address") + dynamic_link = frappe.qb.DocType("Dynamic Link") -def get_conditions(filters): - conditions = [] + query = ( + frappe.qb.from_(lead) + .left_join(dynamic_link) + .on((lead.name == dynamic_link.link_name) & (dynamic_link.parenttype == "Address")) + .left_join(address) + .on(address.name == dynamic_link.parent) + .select( + lead.name, + lead.lead_name, + lead.status, + lead.lead_owner, + lead.territory, + lead.source, + lead.email_id, + lead.mobile_no, + lead.phone, + lead.owner, + lead.company, + (Concat_ws(", ", address.address_line1, address.address_line2)).as_("address"), + address.state, + address.pincode, + address.country, + ) + .where(lead.company == filters.company) + .where(Date(lead.creation).between(filters.from_date, filters.to_date)) + ) if filters.get("territory"): - conditions.append(" and `tabLead`.territory=%(territory)s") + query = query.where(lead.territory == filters.get("territory")) if filters.get("status"): - conditions.append(" and `tabLead`.status=%(status)s") + query = query.where(lead.status == filters.get("status")) - return " ".join(conditions) if conditions else "" + return query.run(as_dict=1) diff --git a/erpnext/e_commerce/shopping_cart/test_shopping_cart.py b/erpnext/e_commerce/shopping_cart/test_shopping_cart.py index f44f8fe2984c..363a80545b0f 100644 --- a/erpnext/e_commerce/shopping_cart/test_shopping_cart.py +++ b/erpnext/e_commerce/shopping_cart/test_shopping_cart.py @@ -17,7 +17,6 @@ request_for_quotation, update_cart, ) -from erpnext.tests.utils import create_test_contact_and_address class TestShoppingCart(unittest.TestCase): @@ -28,7 +27,6 @@ class TestShoppingCart(unittest.TestCase): def setUp(self): frappe.set_user("Administrator") - create_test_contact_and_address() self.enable_shopping_cart() if not frappe.db.exists("Website Item", {"item_code": "_Test Item"}): make_website_item(frappe.get_cached_doc("Item", "_Test Item")) @@ -46,48 +44,57 @@ def tearDownClass(cls): frappe.db.sql("delete from `tabTax Rule`") def test_get_cart_new_user(self): - self.login_as_new_user() - + self.login_as_customer( + "test_contact_two_customer@example.com", "_Test Contact 2 For _Test Customer" + ) + create_address_and_contact( + address_title="_Test Address for Customer 2", + first_name="_Test Contact for Customer 2", + email="test_contact_two_customer@example.com", + customer="_Test Customer 2", + ) # test if lead is created and quotation with new lead is fetched - quotation = _get_cart_quotation() + customer = frappe.get_doc("Customer", "_Test Customer 2") + quotation = _get_cart_quotation(party=customer) self.assertEqual(quotation.quotation_to, "Customer") self.assertEqual( quotation.contact_person, - frappe.db.get_value("Contact", dict(email_id="test_cart_user@example.com")), + frappe.db.get_value("Contact", dict(email_id="test_contact_two_customer@example.com")), ) self.assertEqual(quotation.contact_email, frappe.session.user) return quotation - def test_get_cart_customer(self): - def validate_quotation(): + def test_get_cart_customer(self, customer="_Test Customer 2"): + def validate_quotation(customer_name): # test if quotation with customer is fetched - quotation = _get_cart_quotation() + party = frappe.get_doc("Customer", customer_name) + quotation = _get_cart_quotation(party=party) self.assertEqual(quotation.quotation_to, "Customer") - self.assertEqual(quotation.party_name, "_Test Customer") + self.assertEqual(quotation.party_name, customer_name) self.assertEqual(quotation.contact_email, frappe.session.user) return quotation - self.login_as_customer( - "test_contact_two_customer@example.com", "_Test Contact 2 For _Test Customer" - ) - validate_quotation() - - self.login_as_customer() - quotation = validate_quotation() - + quotation = validate_quotation(customer) return quotation def test_add_to_cart(self): - self.login_as_customer() - + self.login_as_customer( + "test_contact_two_customer@example.com", "_Test Contact 2 For _Test Customer" + ) + create_address_and_contact( + address_title="_Test Address for Customer 2", + first_name="_Test Contact for Customer 2", + email="test_contact_two_customer@example.com", + customer="_Test Customer 2", + ) # clear existing quotations self.clear_existing_quotations() # add first item update_cart("_Test Item", 1) - quotation = self.test_get_cart_customer() + quotation = self.test_get_cart_customer("_Test Customer 2") self.assertEqual(quotation.get("items")[0].item_code, "_Test Item") self.assertEqual(quotation.get("items")[0].qty, 1) @@ -95,7 +102,7 @@ def test_add_to_cart(self): # add second item update_cart("_Test Item 2", 1) - quotation = self.test_get_cart_customer() + quotation = self.test_get_cart_customer("_Test Customer 2") self.assertEqual(quotation.get("items")[1].item_code, "_Test Item 2") self.assertEqual(quotation.get("items")[1].qty, 1) self.assertEqual(quotation.get("items")[1].amount, 20) @@ -108,7 +115,7 @@ def test_update_cart(self): # update first item update_cart("_Test Item", 5) - quotation = self.test_get_cart_customer() + quotation = self.test_get_cart_customer("_Test Customer 2") self.assertEqual(quotation.get("items")[0].item_code, "_Test Item") self.assertEqual(quotation.get("items")[0].qty, 5) self.assertEqual(quotation.get("items")[0].amount, 50) @@ -121,7 +128,7 @@ def test_remove_from_cart(self): # remove first item update_cart("_Test Item", 0) - quotation = self.test_get_cart_customer() + quotation = self.test_get_cart_customer("_Test Customer 2") self.assertEqual(quotation.get("items")[0].item_code, "_Test Item 2") self.assertEqual(quotation.get("items")[0].qty, 1) @@ -132,7 +139,17 @@ def test_remove_from_cart(self): @unittest.skip("Flaky in CI") def test_tax_rule(self): self.create_tax_rule() - self.login_as_customer() + + self.login_as_customer( + "test_contact_two_customer@example.com", "_Test Contact 2 For _Test Customer" + ) + create_address_and_contact( + address_title="_Test Address for Customer 2", + first_name="_Test Contact for Customer 2", + email="test_contact_two_customer@example.com", + customer="_Test Customer 2", + ) + quotation = self.create_quotation() from erpnext.accounts.party import set_taxes @@ -320,7 +337,7 @@ def create_user_if_not_exists(self, email, first_name=None): if frappe.db.exists("User", email): return - frappe.get_doc( + user = frappe.get_doc( { "doctype": "User", "user_type": "Website User", @@ -330,6 +347,40 @@ def create_user_if_not_exists(self, email, first_name=None): } ).insert(ignore_permissions=True) + user.add_roles("Customer") + + +def create_address_and_contact(**kwargs): + if not frappe.db.get_value("Address", {"address_title": kwargs.get("address_title")}): + frappe.get_doc( + { + "doctype": "Address", + "address_title": kwargs.get("address_title"), + "address_type": kwargs.get("address_type") or "Office", + "address_line1": kwargs.get("address_line1") or "Station Road", + "city": kwargs.get("city") or "_Test City", + "state": kwargs.get("state") or "Test State", + "country": kwargs.get("country") or "India", + "links": [ + {"link_doctype": "Customer", "link_name": kwargs.get("customer") or "_Test Customer"} + ], + } + ).insert() + + if not frappe.db.get_value("Contact", {"first_name": kwargs.get("first_name")}): + contact = frappe.get_doc( + { + "doctype": "Contact", + "first_name": kwargs.get("first_name"), + "links": [ + {"link_doctype": "Customer", "link_name": kwargs.get("customer") or "_Test Customer"} + ], + } + ) + contact.add_email(kwargs.get("email") or "test_contact_customer@example.com", is_primary=True) + contact.add_phone(kwargs.get("phone") or "+91 0000000000", is_primary_phone=True) + contact.insert() + test_dependencies = [ "Sales Taxes and Charges Template", From eed58634ba09ff78bf041d93c63c25146b25ecb9 Mon Sep 17 00:00:00 2001 From: David Arnold Date: Sun, 17 Sep 2023 20:17:40 +0200 Subject: [PATCH 435/501] fix: payment request rounding in multi-currency and on status update (cherry picked from commit 6e1ad4c5bded8193be96a3e5bbc1df1aa0e3f956) --- erpnext/accounts/doctype/payment_request/payment_request.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/accounts/doctype/payment_request/payment_request.py b/erpnext/accounts/doctype/payment_request/payment_request.py index 0955664d98be..f6653f87f0f9 100644 --- a/erpnext/accounts/doctype/payment_request/payment_request.py +++ b/erpnext/accounts/doctype/payment_request/payment_request.py @@ -249,7 +249,7 @@ def create_payment_entry(self, submit=True): if ( party_account_currency == ref_doc.company_currency and party_account_currency != self.currency ): - party_amount = ref_doc.base_grand_total + party_amount = ref_doc.get("base_rounded_total") or ref_doc.get("base_grand_total") else: party_amount = self.grand_total From b22ac137f50b979331eca4349702343f9af4714f Mon Sep 17 00:00:00 2001 From: Gursheen Anand Date: Fri, 22 Sep 2023 12:41:17 +0530 Subject: [PATCH 436/501] fix: allocate amt for payment term invoices (cherry picked from commit ac28a5b372d96badd7ba808308a3f1270943f892) --- .../doctype/payment_entry/payment_entry.js | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/erpnext/accounts/doctype/payment_entry/payment_entry.js b/erpnext/accounts/doctype/payment_entry/payment_entry.js index d43a057db027..c72df4224ee5 100644 --- a/erpnext/accounts/doctype/payment_entry/payment_entry.js +++ b/erpnext/accounts/doctype/payment_entry/payment_entry.js @@ -858,20 +858,25 @@ frappe.ui.form.on('Payment Entry', { } } + let outstanding_amount; $.each(frm.doc.references || [], function(i, row) { if (frappe.flags.allocate_payment_amount == 0) { //If allocate payment amount checkbox is unchecked, set zero to allocate amount row.allocated_amount = 0; - } else if (frappe.flags.allocate_payment_amount != 0 && (!row.allocated_amount || paid_amount_change)) { - if (row.outstanding_amount > 0 && allocated_positive_outstanding >= 0) { - row.allocated_amount = (row.outstanding_amount >= allocated_positive_outstanding) ? - allocated_positive_outstanding : row.outstanding_amount; + } else if (frappe.flags.allocate_payment_amount != 0 && (row.payment_term || !row.allocated_amount || paid_amount_change)) { + if(row.payment_term) + outstanding_amount = row.allocated_amount; + else + outstanding_amount = row.outstanding_amount; + if (outstanding_amount > 0 && allocated_positive_outstanding >= 0) { + row.allocated_amount = (outstanding_amount >= allocated_positive_outstanding) ? + allocated_positive_outstanding : outstanding_amount; allocated_positive_outstanding -= flt(row.allocated_amount); - } else if (row.outstanding_amount < 0 && allocated_negative_outstanding) { - row.allocated_amount = (Math.abs(row.outstanding_amount) >= allocated_negative_outstanding) ? - -1*allocated_negative_outstanding : row.outstanding_amount; + } else if (outstanding_amount < 0 && allocated_negative_outstanding) { + row.allocated_amount = (Math.abs(outstanding_amount) >= allocated_negative_outstanding) ? + -1*allocated_negative_outstanding : outstanding_amount; allocated_negative_outstanding -= Math.abs(flt(row.allocated_amount)); } } From 06b04770fc878c00e1073ca07af1c09286f92a08 Mon Sep 17 00:00:00 2001 From: Gursheen Anand Date: Sat, 23 Sep 2023 14:57:53 +0530 Subject: [PATCH 437/501] fix: split inv allocated amt on server side (cherry picked from commit b3aa201eb570167c44225438031438065eef9728) --- .../doctype/payment_entry/payment_entry.js | 18 +++++++----------- .../doctype/payment_entry/payment_entry.py | 15 ++++++++------- 2 files changed, 15 insertions(+), 18 deletions(-) diff --git a/erpnext/accounts/doctype/payment_entry/payment_entry.js b/erpnext/accounts/doctype/payment_entry/payment_entry.js index c72df4224ee5..a6ea140e74da 100644 --- a/erpnext/accounts/doctype/payment_entry/payment_entry.js +++ b/erpnext/accounts/doctype/payment_entry/payment_entry.js @@ -864,19 +864,15 @@ frappe.ui.form.on('Payment Entry', { //If allocate payment amount checkbox is unchecked, set zero to allocate amount row.allocated_amount = 0; - } else if (frappe.flags.allocate_payment_amount != 0 && (row.payment_term || !row.allocated_amount || paid_amount_change)) { - if(row.payment_term) - outstanding_amount = row.allocated_amount; - else - outstanding_amount = row.outstanding_amount; - if (outstanding_amount > 0 && allocated_positive_outstanding >= 0) { - row.allocated_amount = (outstanding_amount >= allocated_positive_outstanding) ? - allocated_positive_outstanding : outstanding_amount; + } else if (frappe.flags.allocate_payment_amount != 0 && (!row.allocated_amount || paid_amount_change)) { + if (row.outstanding_amount > 0 && allocated_positive_outstanding >= 0) { + row.allocated_amount = (row.outstanding_amount >= allocated_positive_outstanding) ? + allocated_positive_outstanding : row.outstanding_amount; allocated_positive_outstanding -= flt(row.allocated_amount); - } else if (outstanding_amount < 0 && allocated_negative_outstanding) { - row.allocated_amount = (Math.abs(outstanding_amount) >= allocated_negative_outstanding) ? - -1*allocated_negative_outstanding : outstanding_amount; + } else if (row.outstanding_amount < 0 && allocated_negative_outstanding) { + row.allocated_amount = (Math.abs(row.outstanding_amount) >= allocated_negative_outstanding) ? + -1*allocated_negative_outstanding : row.outstanding_amount; allocated_negative_outstanding -= Math.abs(flt(row.allocated_amount)); } } diff --git a/erpnext/accounts/doctype/payment_entry/payment_entry.py b/erpnext/accounts/doctype/payment_entry/payment_entry.py index c5501a58306a..9c22deadb2f3 100644 --- a/erpnext/accounts/doctype/payment_entry/payment_entry.py +++ b/erpnext/accounts/doctype/payment_entry/payment_entry.py @@ -227,16 +227,18 @@ def validate_allocated_amount_with_latest_data(self): # if no payment template is used by invoice and has a custom term(no `payment_term`), then invoice outstanding will be in 'None' key latest = latest.get(d.payment_term) or latest.get(None) - # The reference has already been fully paid if not latest: frappe.throw( _("{0} {1} has already been fully paid.").format(_(d.reference_doctype), d.reference_name) ) # The reference has already been partly paid - elif latest.outstanding_amount < latest.invoice_amount and flt( - d.outstanding_amount, d.precision("outstanding_amount") - ) != flt(latest.outstanding_amount, d.precision("outstanding_amount")): + elif ( + latest.outstanding_amount < latest.invoice_amount + and flt(d.outstanding_amount, d.precision("outstanding_amount")) + != flt(latest.outstanding_amount, d.precision("outstanding_amount")) + and d.payment_term == "" + ): frappe.throw( _( "{0} {1} has already been partly paid. Please use the 'Get Outstanding Invoice' or the 'Get Outstanding Orders' button to get the latest outstanding amounts." @@ -1600,11 +1602,10 @@ def split_invoices_based_on_payment_terms(outstanding_invoices, company): "voucher_type": d.voucher_type, "posting_date": d.posting_date, "invoice_amount": flt(d.invoice_amount), - "outstanding_amount": flt(d.outstanding_amount), - "payment_term_outstanding": payment_term_outstanding, - "allocated_amount": payment_term_outstanding + "outstanding_amount": payment_term_outstanding if payment_term_outstanding else d.outstanding_amount, + "payment_term_outstanding": payment_term_outstanding, "payment_amount": payment_term.payment_amount, "payment_term": payment_term.payment_term, } From bc0db696c9cae33a2ea8ca105f3f2c32cb7c879c Mon Sep 17 00:00:00 2001 From: Gursheen Anand Date: Sat, 23 Sep 2023 14:59:12 +0530 Subject: [PATCH 438/501] chore: remove unused variable (cherry picked from commit 545f2ccdf1a66ccdcea04de532e637fa502b7e67) --- erpnext/accounts/doctype/payment_entry/payment_entry.js | 1 - 1 file changed, 1 deletion(-) diff --git a/erpnext/accounts/doctype/payment_entry/payment_entry.js b/erpnext/accounts/doctype/payment_entry/payment_entry.js index a6ea140e74da..d43a057db027 100644 --- a/erpnext/accounts/doctype/payment_entry/payment_entry.js +++ b/erpnext/accounts/doctype/payment_entry/payment_entry.js @@ -858,7 +858,6 @@ frappe.ui.form.on('Payment Entry', { } } - let outstanding_amount; $.each(frm.doc.references || [], function(i, row) { if (frappe.flags.allocate_payment_amount == 0) { //If allocate payment amount checkbox is unchecked, set zero to allocate amount From f5245f6b3fdf8f89702458fd7bb2e04e4eacd927 Mon Sep 17 00:00:00 2001 From: Gursheen Anand Date: Thu, 21 Sep 2023 17:26:20 +0530 Subject: [PATCH 439/501] feat: allow on submit fields (cherry picked from commit e922ec60eb44196255a1e4b9cce839d6f7f05493) # Conflicts: # erpnext/accounts/doctype/purchase_invoice/purchase_invoice.json --- .../purchase_invoice/purchase_invoice.json | 25 ++++++++++++++----- .../purchase_invoice_item.json | 1 + .../purchase_taxes_and_charges.json | 2 ++ 3 files changed, 22 insertions(+), 6 deletions(-) diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.json b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.json index 7e848a0b4e82..864f0b81890c 100644 --- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.json +++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.json @@ -166,6 +166,7 @@ "against_expense_account", "column_break_63", "unrealized_profit_loss_account", + "repost_required", "subscription_section", "auto_repeat", "update_auto_repeat_reference", @@ -190,8 +191,7 @@ "inter_company_invoice_reference", "is_old_subcontracting_flow", "remarks", - "connections_tab", - "column_break_38" + "connections_tab" ], "fields": [ { @@ -987,6 +987,7 @@ "print_hide": 1 }, { + "allow_on_submit": 1, "fieldname": "cash_bank_account", "fieldtype": "Link", "label": "Cash/Bank Account", @@ -1050,6 +1051,7 @@ "fieldtype": "Column Break" }, { + "allow_on_submit": 1, "depends_on": "eval:flt(doc.write_off_amount)!=0", "fieldname": "write_off_account", "fieldtype": "Link", @@ -1213,6 +1215,7 @@ "read_only": 1 }, { + "allow_on_submit": 1, "default": "No", "fieldname": "is_opening", "fieldtype": "Select", @@ -1345,6 +1348,7 @@ "options": "Project" }, { + "allow_on_submit": 1, "depends_on": "eval:doc.is_internal_supplier", "description": "Unrealized Profit/Loss account for intra-company transfers", "fieldname": "unrealized_profit_loss_account", @@ -1495,10 +1499,6 @@ "fieldname": "column_break_6", "fieldtype": "Column Break" }, - { - "fieldname": "column_break_38", - "fieldtype": "Column Break" - }, { "fieldname": "column_break_50", "fieldtype": "Column Break" @@ -1569,13 +1569,26 @@ "fieldname": "use_company_roundoff_cost_center", "fieldtype": "Check", "label": "Use Company Default Round Off Cost Center" + }, + { + "default": "0", + "fieldname": "repost_required", + "fieldtype": "Check", + "hidden": 1, + "label": "Repost Required", + "options": "Account", + "read_only": 1 } ], "icon": "fa fa-file-text", "idx": 204, "is_submittable": 1, "links": [], +<<<<<<< HEAD "modified": "2023-10-01 21:01:47.282533", +======= + "modified": "2023-09-21 12:22:04.545106", +>>>>>>> e922ec60eb (feat: allow on submit fields) "modified_by": "Administrator", "module": "Accounts", "name": "Purchase Invoice", diff --git a/erpnext/accounts/doctype/purchase_invoice_item/purchase_invoice_item.json b/erpnext/accounts/doctype/purchase_invoice_item/purchase_invoice_item.json index 04f28beda9ef..c7357360ec0d 100644 --- a/erpnext/accounts/doctype/purchase_invoice_item/purchase_invoice_item.json +++ b/erpnext/accounts/doctype/purchase_invoice_item/purchase_invoice_item.json @@ -468,6 +468,7 @@ "label": "Accounting" }, { + "allow_on_submit": 1, "fieldname": "expense_account", "fieldtype": "Link", "label": "Expense Head", diff --git a/erpnext/accounts/doctype/purchase_taxes_and_charges/purchase_taxes_and_charges.json b/erpnext/accounts/doctype/purchase_taxes_and_charges/purchase_taxes_and_charges.json index d86abade924d..347cae05b72b 100644 --- a/erpnext/accounts/doctype/purchase_taxes_and_charges/purchase_taxes_and_charges.json +++ b/erpnext/accounts/doctype/purchase_taxes_and_charges/purchase_taxes_and_charges.json @@ -86,6 +86,7 @@ "fieldtype": "Column Break" }, { + "allow_on_submit": 1, "columns": 2, "fieldname": "account_head", "fieldtype": "Link", @@ -97,6 +98,7 @@ "reqd": 1 }, { + "allow_on_submit": 1, "default": ":Company", "fieldname": "cost_center", "fieldtype": "Link", From 79e414cb9747570e2ca747d1a9ab7dd18a453b3b Mon Sep 17 00:00:00 2001 From: Gursheen Anand Date: Thu, 21 Sep 2023 17:28:07 +0530 Subject: [PATCH 440/501] refactor: move reposting logic to common controller (cherry picked from commit 68effd93bdb1a91a8625d983cd9b8afeb7b1eb3b) # Conflicts: # erpnext/accounts/doctype/sales_invoice/sales_invoice.py --- .../doctype/sales_invoice/sales_invoice.py | 72 +++++++------------ erpnext/controllers/accounts_controller.py | 55 ++++++++++++++ 2 files changed, 80 insertions(+), 47 deletions(-) diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py index 7d431b4205b2..a8525522df36 100644 --- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py +++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py @@ -11,9 +11,6 @@ import erpnext from erpnext.accounts.deferred_revenue import validate_service_stop_date -from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import ( - get_accounting_dimensions, -) from erpnext.accounts.doctype.loyalty_program.loyalty_program import ( get_loyalty_program_details_with_points, validate_loyalty_points, @@ -529,54 +526,32 @@ def on_update(self): def on_update_after_submit(self): if hasattr(self, "repost_required"): - needs_repost = 0 - - # Check if any field affecting accounting entry is altered - doc_before_update = self.get_doc_before_save() - accounting_dimensions = get_accounting_dimensions() + ["cost_center", "project"] - - # Check if opening entry check updated - if doc_before_update.get("is_opening") != self.is_opening: - needs_repost = 1 - - if not needs_repost: - # Parent Level Accounts excluding party account - for field in ( - "additional_discount_account", - "cash_bank_account", - "account_for_change_amount", - "write_off_account", - "loyalty_redemption_account", - "unrealized_profit_loss_account", - ): - if doc_before_update.get(field) != self.get(field): - needs_repost = 1 - break - - # Check for parent accounting dimensions - for dimension in accounting_dimensions: - if doc_before_update.get(dimension) != self.get(dimension): - needs_repost = 1 - break - - # Check for child tables - if self.check_if_child_table_updated( - "items", - doc_before_update, - ("income_account", "expense_account", "discount_account"), - accounting_dimensions, - ): - needs_repost = 1 - - if self.check_if_child_table_updated( - "taxes", doc_before_update, ("account_head",), accounting_dimensions - ): - needs_repost = 1 - + fields_to_check = [ + "additional_discount_account", + "cash_bank_account", + "account_for_change_amount", + "write_off_account", + "loyalty_redemption_account", + "unrealized_profit_loss_account", + ] + child_tables = { + "items": ("income_account", "expense_account", "discount_account"), + "taxes": ("account_head",), + } + self.needs_repost = self.check_if_fields_updated(fields_to_check, child_tables) + self.validate_deferred_accounting_before_repost() self.validate_accounts() + self.db_set("repost_required", self.needs_repost) +<<<<<<< HEAD # validate if deferred revenue is enabled for any item # Don't allow to update the invoice if deferred revenue is enabled +======= + def validate_deferred_accounting_before_repost(self): + # validate if deferred revenue is enabled for any item + # Don't allow to update the invoice if deferred revenue is enabled + if self.needs_repost: +>>>>>>> 68effd93bd (refactor: move reposting logic to common controller) for item in self.get("items"): if item.enable_deferred_revenue: frappe.throw( @@ -584,6 +559,7 @@ def on_update_after_submit(self): "Deferred Revenue is enabled for item {0}. You cannot update the invoice after submission." ).format(item.item_code) ) +<<<<<<< HEAD self.db_set("repost_required", needs_repost) @@ -601,6 +577,8 @@ def check_if_child_table_updated( return True return False +======= +>>>>>>> 68effd93bd (refactor: move reposting logic to common controller) @frappe.whitelist() def repost_accounting_entries(self): diff --git a/erpnext/controllers/accounts_controller.py b/erpnext/controllers/accounts_controller.py index 534119762afd..78f171089b36 100644 --- a/erpnext/controllers/accounts_controller.py +++ b/erpnext/controllers/accounts_controller.py @@ -2159,6 +2159,44 @@ def check_finance_books(self, item, asset): _("Select finance book for the item {0} at row {1}").format(item.item_code, item.idx) ) + def check_if_fields_updated(self, fields_to_check, child_tables): + # Check if any field affecting accounting entry is altered + doc_before_update = self.get_doc_before_save() + accounting_dimensions = get_accounting_dimensions() + ["cost_center", "project"] + + # Check if opening entry check updated + needs_repost = doc_before_update.get("is_opening") != self.is_opening + + if not needs_repost: + # Parent Level Accounts excluding party account + fields_to_check += accounting_dimensions + for field in fields_to_check: + if doc_before_update.get(field) != self.get(field): + needs_repost = 1 + break + + if not needs_repost: + # Check for child tables + for table in child_tables: + needs_repost = check_if_child_table_updated( + doc_before_update.get(table), self.get(table), child_tables[table] + ) + if needs_repost: + break + + return needs_repost + + @frappe.whitelist() + def repost_accounting_entries(self): + if self.repost_required: + self.docstatus = 2 + self.make_gl_entries_on_cancel() + self.docstatus = 1 + self.make_gl_entries() + self.db_set("repost_required", 0) + else: + frappe.throw(_("No updates pending for reposting")) + @frappe.whitelist() def get_tax_rate(account_head): @@ -3044,6 +3082,23 @@ def should_update_supplied_items(doc) -> bool: parent.set_status() +def check_if_child_table_updated( + child_table_before_update, child_table_after_update, fields_to_check +): + accounting_dimensions = get_accounting_dimensions() + ["cost_center", "project"] + # Check if any field affecting accounting entry is altered + for index, item in enumerate(child_table_after_update): + for field in fields_to_check: + if child_table_before_update[index].get(field) != item.get(field): + return True + + for dimension in accounting_dimensions: + if child_table_before_update[index].get(dimension) != item.get(dimension): + return True + + return False + + @erpnext.allow_regional def validate_regional(doc): pass From cde848dc7fa755201391fd024e94e0a6935c632e Mon Sep 17 00:00:00 2001 From: Gursheen Anand Date: Thu, 21 Sep 2023 17:29:14 +0530 Subject: [PATCH 441/501] feat: add repost btn in invoice (cherry picked from commit e77814fbc0b15a47c8ecde579fc0d2a9e200a476) --- .../purchase_invoice/purchase_invoice.js | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js index ceb8204bd5da..d8f02c19beea 100644 --- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js +++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js @@ -59,6 +59,25 @@ erpnext.accounts.PurchaseInvoice = class PurchaseInvoice extends erpnext.buying. this.show_stock_ledger(); } + if (this.frm.doc.repost_required && this.frm.doc.docstatus===1) { + this.frm.set_intro(__("Accounting entries for this invoice needs to be reposted. Please click on 'Repost' button to update.")); + this.frm.add_custom_button(__('Repost Accounting Entries'), + () => { + this.frm.call({ + doc: this.frm.doc, + method: 'repost_accounting_entries', + freeze: true, + freeze_message: __('Reposting...'), + callback: (r) => { + if (!r.exc) { + frappe.msgprint(__('Accounting Entries are reposted')); + me.frm.refresh(); + } + } + }); + }).removeClass('btn-default').addClass('btn-warning'); + } + if(!doc.is_return && doc.docstatus == 1 && doc.outstanding_amount != 0){ if(doc.on_hold) { this.frm.add_custom_button( From 2d13dda49c59784fee4257ff56ebf679617713f9 Mon Sep 17 00:00:00 2001 From: Gursheen Anand Date: Thu, 21 Sep 2023 17:30:53 +0530 Subject: [PATCH 442/501] feat: allow repost for pi (cherry picked from commit 23470bf52dd8f7b5fee127e5bc506938ae9632f3) --- .../purchase_invoice/purchase_invoice.py | 25 +++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py index 9737ee2c53e0..1842ad903b90 100644 --- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py +++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py @@ -529,6 +529,31 @@ def on_submit(self): self.process_common_party_accounting() + def on_update_after_submit(self): + if hasattr(self, "repost_required"): + fields_to_check = [ + "cash_bank_account", + "write_off_account" "unrealized_profit_loss_account", + ] + child_tables = {"items": ("expense_account",), "taxes": ("account_head",)} + self.needs_repost = self.check_if_fields_updated(fields_to_check, child_tables) + self.validate_deferred_accounting_before_repost() + self.validate_write_off_account() + self.validate_expense_account() + self.db_set("repost_required", self.needs_repost) + + def validate_deferred_accounting_before_repost(self): + # validate if deferred expense is enabled for any item + # Don't allow to update the invoice if deferred expense is enabled + if self.needs_repost: + for item in self.get("items"): + if item.enable_deferred_expense: + frappe.throw( + _( + "Deferred Expense is enabled for item {0}. You cannot update the invoice after submission." + ).format(item.item_code) + ) + def make_gl_entries(self, gl_entries=None, from_repost=False): if not gl_entries: gl_entries = self.get_gl_entries() From 6c8a65e03b9ab4b57f5c254566ea9a57c79dd2e8 Mon Sep 17 00:00:00 2001 From: Gursheen Anand Date: Thu, 21 Sep 2023 17:41:59 +0530 Subject: [PATCH 443/501] fix: linting issues (cherry picked from commit c88f6d1fa7f28453d81cabf6726bf8eb1b5d8969) --- erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py index 1842ad903b90..cde6221da49e 100644 --- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py +++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py @@ -533,7 +533,8 @@ def on_update_after_submit(self): if hasattr(self, "repost_required"): fields_to_check = [ "cash_bank_account", - "write_off_account" "unrealized_profit_loss_account", + "write_off_account", + "unrealized_profit_loss_account", ] child_tables = {"items": ("expense_account",), "taxes": ("account_head",)} self.needs_repost = self.check_if_fields_updated(fields_to_check, child_tables) From a512d27dbb53999556b0194dd4f69e17147995d1 Mon Sep 17 00:00:00 2001 From: Gursheen Anand Date: Fri, 22 Sep 2023 11:22:25 +0530 Subject: [PATCH 444/501] test: reposted acc entries for pi (cherry picked from commit c66c4385759b25243f946b708055bccc12561809) # Conflicts: # erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py --- .../purchase_invoice/purchase_invoice.js | 4 +- .../purchase_invoice/test_purchase_invoice.py | 86 ++++++++++++++++++- 2 files changed, 87 insertions(+), 3 deletions(-) diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js index d8f02c19beea..ee5a50af0582 100644 --- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js +++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js @@ -60,7 +60,7 @@ erpnext.accounts.PurchaseInvoice = class PurchaseInvoice extends erpnext.buying. } if (this.frm.doc.repost_required && this.frm.doc.docstatus===1) { - this.frm.set_intro(__("Accounting entries for this invoice needs to be reposted. Please click on 'Repost' button to update.")); + this.frm.set_intro(__("Accounting entries for this invoice need to be reposted. Please click on 'Repost' button to update.")); this.frm.add_custom_button(__('Repost Accounting Entries'), () => { this.frm.call({ @@ -70,7 +70,7 @@ erpnext.accounts.PurchaseInvoice = class PurchaseInvoice extends erpnext.buying. freeze_message: __('Reposting...'), callback: (r) => { if (!r.exc) { - frappe.msgprint(__('Accounting Entries are reposted')); + frappe.msgprint(__('Accounting Entries are reposted.')); me.frm.refresh(); } } diff --git a/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py b/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py index 30265aeb50ea..51208885044f 100644 --- a/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py +++ b/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py @@ -1710,6 +1710,65 @@ def test_batch_expiry_for_purchase_invoice(self): self.assertTrue(return_pi.docstatus == 1) +<<<<<<< HEAD +======= + def test_advance_entries_as_asset(self): + from erpnext.accounts.doctype.payment_entry.test_payment_entry import create_payment_entry + + account = create_account( + parent_account="Current Assets - _TC", + account_name="Advances Paid", + company="_Test Company", + account_type="Receivable", + ) + + set_advance_flag(company="_Test Company", flag=1, default_account=account) + + pe = create_payment_entry( + company="_Test Company", + payment_type="Pay", + party_type="Supplier", + party="_Test Supplier", + paid_from="Cash - _TC", + paid_to="Creditors - _TC", + paid_amount=500, + ) + pe.submit() + + pi = make_purchase_invoice( + company="_Test Company", + do_not_save=True, + do_not_submit=True, + rate=1000, + price_list_rate=1000, + qty=1, + ) + pi.base_grand_total = 1000 + pi.grand_total = 1000 + pi.set_advances() + for advance in pi.advances: + advance.allocated_amount = 500 if advance.reference_name == pe.name else 0 + pi.save() + pi.submit() + + self.assertEqual(pi.advances[0].allocated_amount, 500) + + # Check GL Entry against payment doctype + expected_gle = [ + ["Advances Paid - _TC", 0.0, 500, nowdate()], + ["Cash - _TC", 0.0, 500, nowdate()], + ["Creditors - _TC", 500, 0.0, nowdate()], + ["Creditors - _TC", 500, 0.0, nowdate()], + ] + + check_gl_entries(self, pe.name, expected_gle, nowdate(), voucher_type="Payment Entry") + + pi.load_from_db() + self.assertEqual(pi.outstanding_amount, 500) + + set_advance_flag(company="_Test Company", flag=0, default_account="") + +>>>>>>> c66c438575 (test: reposted acc entries for pi) def test_gl_entries_for_standalone_debit_note(self): make_purchase_invoice(qty=5, rate=500, update_stock=True) @@ -1796,7 +1855,6 @@ def test_offsetting_entries_for_accounting_dimensions(self): pi = make_purchase_invoice( company="_Test Company", - customer="_Test Supplier", do_not_save=True, do_not_submit=True, rate=1000, @@ -1826,6 +1884,32 @@ def test_offsetting_entries_for_accounting_dimensions(self): clear_dimension_defaults("Branch") disable_dimension() + def test_repost_accounting_entries(self): + pi = make_purchase_invoice( + rate=1000, + price_list_rate=1000, + qty=1, + ) + expected_gle = [ + ["_Test Account Cost for Goods Sold - _TC", 1000, 0.0, nowdate()], + ["Creditors - _TC", 0.0, 1000, nowdate()], + ] + check_gl_entries(self, pi.name, expected_gle, nowdate()) + + pi.items[0].expense_account = "Service - _TC" + pi.save() + pi.load_from_db() + self.assertTrue(pi.repost_required) + pi.repost_accounting_entries() + + expected_gle = [ + ["Creditors - _TC", 0.0, 1000, nowdate()], + ["Service - _TC", 1000, 0.0, nowdate()], + ] + check_gl_entries(self, pi.name, expected_gle, nowdate()) + pi.load_from_db() + self.assertFalse(pi.repost_required) + def check_gl_entries( doc, From 8c83bbc0962fbad193ef91cd7ee5158018712c21 Mon Sep 17 00:00:00 2001 From: Gursheen Anand Date: Sat, 23 Sep 2023 19:11:46 +0530 Subject: [PATCH 445/501] refactor: remove unused method (cherry picked from commit ba7212c98b81a43e5f63d010eadf99635c95e349) --- .../accounts/doctype/sales_invoice/sales_invoice.py | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py index a8525522df36..361e5769df5a 100644 --- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py +++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py @@ -580,17 +580,6 @@ def check_if_child_table_updated( ======= >>>>>>> 68effd93bd (refactor: move reposting logic to common controller) - @frappe.whitelist() - def repost_accounting_entries(self): - if self.repost_required: - self.docstatus = 2 - self.make_gl_entries_on_cancel() - self.docstatus = 1 - self.make_gl_entries() - self.db_set("repost_required", 0) - else: - frappe.throw(_("No updates pending for reposting")) - def set_paid_amount(self): paid_amount = 0.0 base_paid_amount = 0.0 From 677525b2cf5f9c9b422c6e145b1ec3cc1354813c Mon Sep 17 00:00:00 2001 From: Gursheen Anand Date: Sat, 23 Sep 2023 19:13:33 +0530 Subject: [PATCH 446/501] refactor: use repost accounting legder (cherry picked from commit 7ebf0836833d9226ca9e0d2d231b35be4f438842) --- erpnext/controllers/accounts_controller.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/erpnext/controllers/accounts_controller.py b/erpnext/controllers/accounts_controller.py index 78f171089b36..072537dcbb45 100644 --- a/erpnext/controllers/accounts_controller.py +++ b/erpnext/controllers/accounts_controller.py @@ -2189,10 +2189,11 @@ def check_if_fields_updated(self, fields_to_check, child_tables): @frappe.whitelist() def repost_accounting_entries(self): if self.repost_required: - self.docstatus = 2 - self.make_gl_entries_on_cancel() - self.docstatus = 1 - self.make_gl_entries() + repost_ledger = frappe.new_doc("Repost Accounting Ledger") + repost_ledger.company = self.company + repost_ledger.append("vouchers", {"voucher_type": self.doctype, "voucher_no": self.name}) + repost_ledger.insert() + repost_ledger.submit() self.db_set("repost_required", 0) else: frappe.throw(_("No updates pending for reposting")) From c9bcf79e83d16e17627fd116c45e1ea2abfd21dc Mon Sep 17 00:00:00 2001 From: Gursheen Anand Date: Sat, 23 Sep 2023 19:18:06 +0530 Subject: [PATCH 447/501] refactor: remove repeated validation for voucher (cherry picked from commit a856091ff4756462b6cf0f02493f6fb6ddf59921) # Conflicts: # erpnext/accounts/doctype/sales_invoice/sales_invoice.py --- .../doctype/purchase_invoice/purchase_invoice.py | 13 ------------- .../accounts/doctype/sales_invoice/sales_invoice.py | 4 +++- 2 files changed, 3 insertions(+), 14 deletions(-) diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py index cde6221da49e..c17062c943a1 100644 --- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py +++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py @@ -538,23 +538,10 @@ def on_update_after_submit(self): ] child_tables = {"items": ("expense_account",), "taxes": ("account_head",)} self.needs_repost = self.check_if_fields_updated(fields_to_check, child_tables) - self.validate_deferred_accounting_before_repost() self.validate_write_off_account() self.validate_expense_account() self.db_set("repost_required", self.needs_repost) - def validate_deferred_accounting_before_repost(self): - # validate if deferred expense is enabled for any item - # Don't allow to update the invoice if deferred expense is enabled - if self.needs_repost: - for item in self.get("items"): - if item.enable_deferred_expense: - frappe.throw( - _( - "Deferred Expense is enabled for item {0}. You cannot update the invoice after submission." - ).format(item.item_code) - ) - def make_gl_entries(self, gl_entries=None, from_repost=False): if not gl_entries: gl_entries = self.get_gl_entries() diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py index 361e5769df5a..6b8baa8b2c9f 100644 --- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py +++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py @@ -539,10 +539,10 @@ def on_update_after_submit(self): "taxes": ("account_head",), } self.needs_repost = self.check_if_fields_updated(fields_to_check, child_tables) - self.validate_deferred_accounting_before_repost() self.validate_accounts() self.db_set("repost_required", self.needs_repost) +<<<<<<< HEAD <<<<<<< HEAD # validate if deferred revenue is enabled for any item # Don't allow to update the invoice if deferred revenue is enabled @@ -580,6 +580,8 @@ def check_if_child_table_updated( ======= >>>>>>> 68effd93bd (refactor: move reposting logic to common controller) +======= +>>>>>>> a856091ff4 (refactor: remove repeated validation for voucher) def set_paid_amount(self): paid_amount = 0.0 base_paid_amount = 0.0 From 4123e7b2447d7ed5de4957ac48ea1deff5a50649 Mon Sep 17 00:00:00 2001 From: Gursheen Anand Date: Sat, 23 Sep 2023 19:30:42 +0530 Subject: [PATCH 448/501] fix: do not run bg job for single doc (cherry picked from commit 1856050ef9022d5688bcd98ea4edb146946b8b7a) --- .../repost_accounting_ledger.py | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/erpnext/accounts/doctype/repost_accounting_ledger/repost_accounting_ledger.py b/erpnext/accounts/doctype/repost_accounting_ledger/repost_accounting_ledger.py index 4cf2ed2f46cc..e533fed35b42 100644 --- a/erpnext/accounts/doctype/repost_accounting_ledger/repost_accounting_ledger.py +++ b/erpnext/accounts/doctype/repost_accounting_ledger/repost_accounting_ledger.py @@ -139,14 +139,17 @@ def generate_preview(self): return rendered_page def on_submit(self): - job_name = "repost_accounting_ledger_" + self.name - frappe.enqueue( - method="erpnext.accounts.doctype.repost_accounting_ledger.repost_accounting_ledger.start_repost", - account_repost_doc=self.name, - is_async=True, - job_name=job_name, - ) - frappe.msgprint(_("Repost has started in the background")) + if len(self.vouchers) > 1: + job_name = "repost_accounting_ledger_" + self.name + frappe.enqueue( + method="erpnext.accounts.doctype.repost_accounting_ledger.repost_accounting_ledger.start_repost", + account_repost_doc=self.name, + is_async=True, + job_name=job_name, + ) + frappe.msgprint(_("Repost has started in the background")) + else: + start_repost(self.name) @frappe.whitelist() From bec3e8ed96f24ea6b048f88d23d3dbc6bc457e0f Mon Sep 17 00:00:00 2001 From: Gursheen Anand Date: Sat, 23 Sep 2023 20:29:38 +0530 Subject: [PATCH 449/501] fix: call validate before setting repost flag (cherry picked from commit 8ef0d8870830b1a4f848a91d201fb740e7f47aa0) --- .../purchase_invoice/purchase_invoice.py | 11 ++++- .../repost_accounting_ledger.py | 46 ++++++++++--------- 2 files changed, 33 insertions(+), 24 deletions(-) diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py index c17062c943a1..f6ec446ef357 100644 --- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py +++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py @@ -11,6 +11,9 @@ import erpnext from erpnext.accounts.deferred_revenue import validate_service_stop_date from erpnext.accounts.doctype.gl_entry.gl_entry import update_outstanding_amt +from erpnext.accounts.doctype.repost_accounting_ledger.repost_accounting_ledger import ( + validate_docs_for_deferred_accounting, +) from erpnext.accounts.doctype.sales_invoice.sales_invoice import ( check_if_return_invoice_linked_with_payment_entry, get_total_in_party_account_currency, @@ -487,6 +490,11 @@ def validate_purchase_receipt_if_update_stock(self): _("Stock cannot be updated against Purchase Receipt {0}").format(item.purchase_receipt) ) + def validate_for_repost(self): + self.validate_write_off_account() + self.validate_expense_account() + validate_docs_for_deferred_accounting([], [self.name]) + def on_submit(self): super(PurchaseInvoice, self).on_submit() @@ -538,8 +546,7 @@ def on_update_after_submit(self): ] child_tables = {"items": ("expense_account",), "taxes": ("account_head",)} self.needs_repost = self.check_if_fields_updated(fields_to_check, child_tables) - self.validate_write_off_account() - self.validate_expense_account() + self.validate_for_repost() self.db_set("repost_required", self.needs_repost) def make_gl_entries(self, gl_entries=None, from_repost=False): diff --git a/erpnext/accounts/doctype/repost_accounting_ledger/repost_accounting_ledger.py b/erpnext/accounts/doctype/repost_accounting_ledger/repost_accounting_ledger.py index e533fed35b42..dbb0971fdea2 100644 --- a/erpnext/accounts/doctype/repost_accounting_ledger/repost_accounting_ledger.py +++ b/erpnext/accounts/doctype/repost_accounting_ledger/repost_accounting_ledger.py @@ -21,29 +21,8 @@ def validate(self): def validate_for_deferred_accounting(self): sales_docs = [x.voucher_no for x in self.vouchers if x.voucher_type == "Sales Invoice"] - docs_with_deferred_revenue = frappe.db.get_all( - "Sales Invoice Item", - filters={"parent": ["in", sales_docs], "docstatus": 1, "enable_deferred_revenue": True}, - fields=["parent"], - as_list=1, - ) - purchase_docs = [x.voucher_no for x in self.vouchers if x.voucher_type == "Purchase Invoice"] - docs_with_deferred_expense = frappe.db.get_all( - "Purchase Invoice Item", - filters={"parent": ["in", purchase_docs], "docstatus": 1, "enable_deferred_expense": 1}, - fields=["parent"], - as_list=1, - ) - - if docs_with_deferred_revenue or docs_with_deferred_expense: - frappe.throw( - _("Documents: {0} have deferred revenue/expense enabled for them. Cannot repost.").format( - frappe.bold( - comma_and([x[0] for x in docs_with_deferred_expense + docs_with_deferred_revenue]) - ) - ) - ) + validate_docs_for_deferred_accounting(sales_docs, purchase_docs) def validate_for_closed_fiscal_year(self): if self.vouchers: @@ -184,3 +163,26 @@ def start_repost(account_repost_doc=str) -> None: doc.make_gl_entries() frappe.db.commit() + + +def validate_docs_for_deferred_accounting(sales_docs, purchase_docs): + docs_with_deferred_revenue = frappe.db.get_all( + "Sales Invoice Item", + filters={"parent": ["in", sales_docs], "docstatus": 1, "enable_deferred_revenue": True}, + fields=["parent"], + as_list=1, + ) + + docs_with_deferred_expense = frappe.db.get_all( + "Purchase Invoice Item", + filters={"parent": ["in", purchase_docs], "docstatus": 1, "enable_deferred_expense": 1}, + fields=["parent"], + as_list=1, + ) + + if docs_with_deferred_revenue or docs_with_deferred_expense: + frappe.throw( + _("Documents: {0} have deferred revenue/expense enabled for them. Cannot repost.").format( + frappe.bold(comma_and([x[0] for x in docs_with_deferred_expense + docs_with_deferred_revenue])) + ) + ) From 3dc68e3b002ec0f072809d9d94a175b16ea98e8d Mon Sep 17 00:00:00 2001 From: Gursheen Anand Date: Sat, 23 Sep 2023 20:30:50 +0530 Subject: [PATCH 450/501] fix: validation for si (cherry picked from commit 61c6ebbb95b0e7ecbb86312646fe862b9a471d95) --- .../accounts/doctype/sales_invoice/sales_invoice.py | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py index 6b8baa8b2c9f..c26d3c0283be 100644 --- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py +++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py @@ -15,6 +15,9 @@ get_loyalty_program_details_with_points, validate_loyalty_points, ) +from erpnext.accounts.doctype.repost_accounting_ledger.repost_accounting_ledger import ( + validate_docs_for_deferred_accounting, +) from erpnext.accounts.doctype.tax_withholding_category.tax_withholding_category import ( get_party_tax_withholding_details, ) @@ -173,6 +176,12 @@ def validate_accounts(self): self.validate_account_for_change_amount() self.validate_income_account() + def validate_for_repost(self): + self.validate_write_off_account() + self.validate_account_for_change_amount() + self.validate_income_account() + validate_docs_for_deferred_accounting([self.name], []) + def validate_fixed_asset(self): for d in self.get("items"): if d.is_fixed_asset and d.meta.get_field("asset") and d.asset: @@ -539,7 +548,7 @@ def on_update_after_submit(self): "taxes": ("account_head",), } self.needs_repost = self.check_if_fields_updated(fields_to_check, child_tables) - self.validate_accounts() + self.validate_for_repost() self.db_set("repost_required", self.needs_repost) <<<<<<< HEAD From debfbc4761085ade3e2c826e18fef9cecd4dfd5e Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Tue, 26 Sep 2023 14:23:21 +0530 Subject: [PATCH 451/501] refactor: remove references in repost doctypes upon parent doc delet (cherry picked from commit ed7f67b1a85a1c2ef47bba77267e20cf6bbced71) --- .../repost_accounting_ledger.json | 5 ++- .../repost_payment_ledger.json | 5 ++- erpnext/controllers/accounts_controller.py | 37 ++++++++++++++++--- 3 files changed, 37 insertions(+), 10 deletions(-) diff --git a/erpnext/accounts/doctype/repost_accounting_ledger/repost_accounting_ledger.json b/erpnext/accounts/doctype/repost_accounting_ledger/repost_accounting_ledger.json index 8d56c9bb11dc..5b7cd2b0b209 100644 --- a/erpnext/accounts/doctype/repost_accounting_ledger/repost_accounting_ledger.json +++ b/erpnext/accounts/doctype/repost_accounting_ledger/repost_accounting_ledger.json @@ -55,7 +55,7 @@ "index_web_pages_for_search": 1, "is_submittable": 1, "links": [], - "modified": "2023-07-27 15:47:58.975034", + "modified": "2023-09-26 14:21:27.362567", "modified_by": "Administrator", "module": "Accounts", "name": "Repost Accounting Ledger", @@ -77,5 +77,6 @@ ], "sort_field": "modified", "sort_order": "DESC", - "states": [] + "states": [], + "track_changes": 1 } \ No newline at end of file diff --git a/erpnext/accounts/doctype/repost_payment_ledger/repost_payment_ledger.json b/erpnext/accounts/doctype/repost_payment_ledger/repost_payment_ledger.json index 5175fd169ffe..ed8d395a0eca 100644 --- a/erpnext/accounts/doctype/repost_payment_ledger/repost_payment_ledger.json +++ b/erpnext/accounts/doctype/repost_payment_ledger/repost_payment_ledger.json @@ -99,7 +99,7 @@ "index_web_pages_for_search": 1, "is_submittable": 1, "links": [], - "modified": "2022-11-08 07:38:40.079038", + "modified": "2023-09-26 14:21:35.719727", "modified_by": "Administrator", "module": "Accounts", "name": "Repost Payment Ledger", @@ -155,5 +155,6 @@ ], "sort_field": "modified", "sort_order": "DESC", - "states": [] + "states": [], + "track_changes": 1 } \ No newline at end of file diff --git a/erpnext/controllers/accounts_controller.py b/erpnext/controllers/accounts_controller.py index 072537dcbb45..7207743e095b 100644 --- a/erpnext/controllers/accounts_controller.py +++ b/erpnext/controllers/accounts_controller.py @@ -242,13 +242,38 @@ def _remove_references_in_unreconcile(self): _doc.cancel() _doc.delete() - def on_trash(self): - # delete references in 'Repost Payment Ledger' - rpi = frappe.qb.DocType("Repost Payment Ledger Items") - frappe.qb.from_(rpi).delete().where( - (rpi.voucher_type == self.doctype) & (rpi.voucher_no == self.name) - ).run() + def _remove_references_in_repost_doctypes(self): + repost_doctypes = ["Repost Payment Ledger Items", "Repost Accounting Ledger Items"] + + for _doctype in repost_doctypes: + dt = frappe.qb.DocType(_doctype) + rows = ( + frappe.qb.from_(dt) + .select(dt.name, dt.parent, dt.parenttype) + .where((dt.voucher_type == self.doctype) & (dt.voucher_no == self.name)) + .run(as_dict=True) + ) + + if rows: + references_map = frappe._dict() + for x in rows: + references_map.setdefault((x.parenttype, x.parent), []).append(x.name) + for doc, rows in references_map.items(): + repost_doc = frappe.get_doc(doc[0], doc[1]) + + for row in rows: + if _doctype == "Repost Payment Ledger Items": + repost_doc.remove(repost_doc.get("repost_vouchers", {"name": row})[0]) + else: + repost_doc.remove(repost_doc.get("vouchers", {"name": row})[0]) + + repost_doc.flags.ignore_validate_update_after_submit = True + repost_doc.flags.ignore_links = True + repost_doc.save(ignore_permissions=True) + + def on_trash(self): + self._remove_references_in_repost_doctypes() self._remove_references_in_unreconcile() # delete sl and gl entries on deletion of transaction From 0aad942312da44584f30b44249baeeed15145926 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Tue, 10 Oct 2023 11:06:33 +0530 Subject: [PATCH 452/501] fix: fetch dependent task subject and project (backport #37401) (#37421) fix: fetch dependent task subject and project (#37401) (cherry picked from commit 78eaf5d0356e00f4a07b84a3dc5ee7916871efaf) Co-authored-by: s-aga-r --- .../task_depends_on/task_depends_on.json | 192 ++++-------------- 1 file changed, 44 insertions(+), 148 deletions(-) diff --git a/erpnext/projects/doctype/task_depends_on/task_depends_on.json b/erpnext/projects/doctype/task_depends_on/task_depends_on.json index dbbe9d3c7b54..5102986f00d0 100644 --- a/erpnext/projects/doctype/task_depends_on/task_depends_on.json +++ b/erpnext/projects/doctype/task_depends_on/task_depends_on.json @@ -1,156 +1,52 @@ { - "allow_copy": 0, - "allow_import": 0, - "allow_rename": 0, - "beta": 0, - "creation": "2015-04-29 04:52:48.868079", - "custom": 0, - "docstatus": 0, - "doctype": "DocType", - "document_type": "", - "editable_grid": 1, + "actions": [], + "creation": "2015-04-29 04:52:48.868079", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "task", + "column_break_2", + "subject", + "project" + ], "fields": [ { - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "task", - "fieldtype": "Link", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 1, - "in_standard_filter": 0, - "label": "Task", - "length": 0, - "no_copy": 0, - "options": "Task", - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "unique": 0 - }, + "fieldname": "task", + "fieldtype": "Link", + "in_list_view": 1, + "label": "Task", + "options": "Task" + }, { - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "column_break_2", - "fieldtype": "Column Break", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "unique": 0 - }, + "fieldname": "column_break_2", + "fieldtype": "Column Break" + }, { - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "subject", - "fieldtype": "Text", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 1, - "in_standard_filter": 0, - "label": "Subject", - "length": 0, - "no_copy": 0, - "options": "", - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 1, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "unique": 0 - }, + "fetch_from": "task.subject", + "fieldname": "subject", + "fieldtype": "Text", + "in_list_view": 1, + "label": "Subject", + "read_only": 1 + }, { - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "project", - "fieldtype": "Text", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Project", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 1, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "unique": 0 + "fetch_from": "task.project", + "fieldname": "project", + "fieldtype": "Text", + "label": "Project", + "read_only": 1 } - ], - "hide_heading": 0, - "hide_toolbar": 0, - "idx": 0, - "image_view": 0, - "in_create": 0, - - "is_submittable": 0, - "issingle": 0, - "istable": 1, - "max_attachments": 0, - "modified": "2017-02-24 04:56:04.862502", - "modified_by": "Administrator", - "module": "Projects", - "name": "Task Depends On", - "name_case": "", - "owner": "Administrator", - "permissions": [], - "quick_entry": 0, - "read_only": 0, - "read_only_onload": 0, - "show_name_in_global_search": 0, - "sort_field": "modified", - "sort_order": "DESC", - "track_changes": 0, - "track_seen": 0 + ], + "istable": 1, + "links": [], + "modified": "2023-10-09 11:34:14.335853", + "modified_by": "Administrator", + "module": "Projects", + "name": "Task Depends On", + "owner": "Administrator", + "permissions": [], + "sort_field": "modified", + "sort_order": "DESC", + "states": [] } \ No newline at end of file From b0ac0973275a58628d0236071ec921eac4781c9e Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Tue, 10 Oct 2023 11:16:12 +0530 Subject: [PATCH 453/501] chore: resolve conflicts --- .../purchase_invoice/purchase_invoice.json | 4 -- .../purchase_invoice/test_purchase_invoice.py | 59 ------------------- .../doctype/sales_invoice/sales_invoice.py | 40 ------------- 3 files changed, 103 deletions(-) diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.json b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.json index 864f0b81890c..1f3b17ee147d 100644 --- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.json +++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.json @@ -1584,11 +1584,7 @@ "idx": 204, "is_submittable": 1, "links": [], -<<<<<<< HEAD "modified": "2023-10-01 21:01:47.282533", -======= - "modified": "2023-09-21 12:22:04.545106", ->>>>>>> e922ec60eb (feat: allow on submit fields) "modified_by": "Administrator", "module": "Accounts", "name": "Purchase Invoice", diff --git a/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py b/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py index 51208885044f..47126d3846f5 100644 --- a/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py +++ b/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py @@ -1710,65 +1710,6 @@ def test_batch_expiry_for_purchase_invoice(self): self.assertTrue(return_pi.docstatus == 1) -<<<<<<< HEAD -======= - def test_advance_entries_as_asset(self): - from erpnext.accounts.doctype.payment_entry.test_payment_entry import create_payment_entry - - account = create_account( - parent_account="Current Assets - _TC", - account_name="Advances Paid", - company="_Test Company", - account_type="Receivable", - ) - - set_advance_flag(company="_Test Company", flag=1, default_account=account) - - pe = create_payment_entry( - company="_Test Company", - payment_type="Pay", - party_type="Supplier", - party="_Test Supplier", - paid_from="Cash - _TC", - paid_to="Creditors - _TC", - paid_amount=500, - ) - pe.submit() - - pi = make_purchase_invoice( - company="_Test Company", - do_not_save=True, - do_not_submit=True, - rate=1000, - price_list_rate=1000, - qty=1, - ) - pi.base_grand_total = 1000 - pi.grand_total = 1000 - pi.set_advances() - for advance in pi.advances: - advance.allocated_amount = 500 if advance.reference_name == pe.name else 0 - pi.save() - pi.submit() - - self.assertEqual(pi.advances[0].allocated_amount, 500) - - # Check GL Entry against payment doctype - expected_gle = [ - ["Advances Paid - _TC", 0.0, 500, nowdate()], - ["Cash - _TC", 0.0, 500, nowdate()], - ["Creditors - _TC", 500, 0.0, nowdate()], - ["Creditors - _TC", 500, 0.0, nowdate()], - ] - - check_gl_entries(self, pe.name, expected_gle, nowdate(), voucher_type="Payment Entry") - - pi.load_from_db() - self.assertEqual(pi.outstanding_amount, 500) - - set_advance_flag(company="_Test Company", flag=0, default_account="") - ->>>>>>> c66c438575 (test: reposted acc entries for pi) def test_gl_entries_for_standalone_debit_note(self): make_purchase_invoice(qty=5, rate=500, update_stock=True) diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py index c26d3c0283be..03aca8ad5882 100644 --- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py +++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py @@ -551,46 +551,6 @@ def on_update_after_submit(self): self.validate_for_repost() self.db_set("repost_required", self.needs_repost) -<<<<<<< HEAD -<<<<<<< HEAD - # validate if deferred revenue is enabled for any item - # Don't allow to update the invoice if deferred revenue is enabled -======= - def validate_deferred_accounting_before_repost(self): - # validate if deferred revenue is enabled for any item - # Don't allow to update the invoice if deferred revenue is enabled - if self.needs_repost: ->>>>>>> 68effd93bd (refactor: move reposting logic to common controller) - for item in self.get("items"): - if item.enable_deferred_revenue: - frappe.throw( - _( - "Deferred Revenue is enabled for item {0}. You cannot update the invoice after submission." - ).format(item.item_code) - ) -<<<<<<< HEAD - - self.db_set("repost_required", needs_repost) - - def check_if_child_table_updated( - self, child_table, doc_before_update, fields_to_check, accounting_dimensions - ): - # Check if any field affecting accounting entry is altered - for index, item in enumerate(self.get(child_table)): - for field in fields_to_check: - if doc_before_update.get(child_table)[index].get(field) != item.get(field): - return True - - for dimension in accounting_dimensions: - if doc_before_update.get(child_table)[index].get(dimension) != item.get(dimension): - return True - - return False -======= ->>>>>>> 68effd93bd (refactor: move reposting logic to common controller) - -======= ->>>>>>> a856091ff4 (refactor: remove repeated validation for voucher) def set_paid_amount(self): paid_amount = 0.0 base_paid_amount = 0.0 From ee1255a716451015da11a8afe62b4c0af2193e0c Mon Sep 17 00:00:00 2001 From: Gursheen Kaur Anand <40693548+GursheenK@users.noreply.github.com> Date: Mon, 31 Jul 2023 14:29:20 +0530 Subject: [PATCH 454/501] fix: ignore cancelled gle in voucher-wise balance report (#36417) fix: ignore cancelled gle (cherry picked from commit 1ddfaa7605f710b2fd12ccdfa38824cd086f576d) --- .../accounts/report/voucher_wise_balance/voucher_wise_balance.py | 1 + 1 file changed, 1 insertion(+) diff --git a/erpnext/accounts/report/voucher_wise_balance/voucher_wise_balance.py b/erpnext/accounts/report/voucher_wise_balance/voucher_wise_balance.py index 5ab3611b9af8..bd9e9fccadce 100644 --- a/erpnext/accounts/report/voucher_wise_balance/voucher_wise_balance.py +++ b/erpnext/accounts/report/voucher_wise_balance/voucher_wise_balance.py @@ -46,6 +46,7 @@ def get_data(filters): .select( gle.voucher_type, gle.voucher_no, Sum(gle.debit).as_("debit"), Sum(gle.credit).as_("credit") ) + .where(gle.is_cancelled == 0) .groupby(gle.voucher_no) ) query = apply_filters(query, filters, gle) From fb0b426fe435357ad49513ae15e2b12a845864e0 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Wed, 11 Oct 2023 13:51:29 +0530 Subject: [PATCH 455/501] fix(ux): allow MR to Stop until fully received (backport #37452) (#37456) fix(ux): allow MR to Stop until fully received (cherry picked from commit 0d7a0f393deed1f3bec5d5925bd5f9bb4eab99c5) Co-authored-by: s-aga-r --- .../doctype/material_request/material_request.js | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/erpnext/stock/doctype/material_request/material_request.js b/erpnext/stock/doctype/material_request/material_request.js index 2632501b718b..ec075bb6bad8 100644 --- a/erpnext/stock/doctype/material_request/material_request.js +++ b/erpnext/stock/doctype/material_request/material_request.js @@ -102,6 +102,12 @@ frappe.ui.form.on('Material Request', { if (frm.doc.docstatus == 1 && frm.doc.status != 'Stopped') { let precision = frappe.defaults.get_default("float_precision"); + + if (flt(frm.doc.per_received, precision) < 100) { + frm.add_custom_button(__('Stop'), + () => frm.events.update_status(frm, 'Stopped')); + } + if (flt(frm.doc.per_ordered, precision) < 100) { let add_create_pick_list_button = () => { frm.add_custom_button(__('Pick List'), @@ -148,11 +154,6 @@ frappe.ui.form.on('Material Request', { } frm.page.set_inner_btn_group_as_primary(__('Create')); - - // stop - frm.add_custom_button(__('Stop'), - () => frm.events.update_status(frm, 'Stopped')); - } } From 573b159541b6aebbdcc86202576c38c69c947b1e Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Wed, 11 Oct 2023 14:41:32 +0530 Subject: [PATCH 456/501] fix: production plan reserved qty incorrect calculation (backport #37400) (#37458) fix: production plan reserved qty incorrect calculation (#37400) (cherry picked from commit f3238f910509813a24fbc7ebfb726f42f6addd6f) Co-authored-by: rohitwaghchaure --- .../production_plan/production_plan.py | 23 ++++++------------- .../production_plan/test_production_plan.py | 6 ++--- .../doctype/work_order/work_order.py | 19 ++++++++------- 3 files changed, 21 insertions(+), 27 deletions(-) diff --git a/erpnext/manufacturing/doctype/production_plan/production_plan.py b/erpnext/manufacturing/doctype/production_plan/production_plan.py index f1817e3305bb..5fc764fb6f7f 100644 --- a/erpnext/manufacturing/doctype/production_plan/production_plan.py +++ b/erpnext/manufacturing/doctype/production_plan/production_plan.py @@ -8,7 +8,6 @@ import frappe from frappe import _, msgprint from frappe.model.document import Document -from frappe.query_builder import Case from frappe.query_builder.functions import IfNull, Sum from frappe.utils import ( add_days, @@ -1617,21 +1616,13 @@ def get_reserved_qty_for_production_plan(item_code, warehouse): table = frappe.qb.DocType("Production Plan") child = frappe.qb.DocType("Material Request Plan Item") - completed_production_plans = get_completed_production_plans() + non_completed_production_plans = get_non_completed_production_plans() - case = Case() query = ( frappe.qb.from_(table) .inner_join(child) .on(table.name == child.parent) - .select( - Sum( - child.quantity - * IfNull( - case.when(child.material_request_type == "Purchase", child.conversion_factor).else_(1.0), 1.0 - ) - ) - ) + .select(Sum(child.required_bom_qty)) .where( (table.docstatus == 1) & (child.item_code == item_code) @@ -1640,8 +1631,8 @@ def get_reserved_qty_for_production_plan(item_code, warehouse): ) ) - if completed_production_plans: - query = query.where(table.name.notin(completed_production_plans)) + if non_completed_production_plans: + query = query.where(table.name.isin(non_completed_production_plans)) query = query.run() @@ -1652,7 +1643,7 @@ def get_reserved_qty_for_production_plan(item_code, warehouse): reserved_qty_for_production = flt( get_reserved_qty_for_production( - item_code, warehouse, completed_production_plans, check_production_plan=True + item_code, warehouse, non_completed_production_plans, check_production_plan=True ) ) @@ -1662,7 +1653,7 @@ def get_reserved_qty_for_production_plan(item_code, warehouse): return reserved_qty_for_production_plan - reserved_qty_for_production -def get_completed_production_plans(): +def get_non_completed_production_plans(): table = frappe.qb.DocType("Production Plan") child = frappe.qb.DocType("Production Plan Item") @@ -1674,7 +1665,7 @@ def get_completed_production_plans(): .where( (table.docstatus == 1) & (table.status.notin(["Completed", "Closed"])) - & (child.ordered_qty >= child.planned_qty) + & (child.planned_qty > child.ordered_qty) ) ).run(as_dict=True) diff --git a/erpnext/manufacturing/doctype/production_plan/test_production_plan.py b/erpnext/manufacturing/doctype/production_plan/test_production_plan.py index 55122f7069c8..dbd3083ab583 100644 --- a/erpnext/manufacturing/doctype/production_plan/test_production_plan.py +++ b/erpnext/manufacturing/doctype/production_plan/test_production_plan.py @@ -6,8 +6,8 @@ from erpnext.controllers.item_variant import create_variant from erpnext.manufacturing.doctype.production_plan.production_plan import ( - get_completed_production_plans, get_items_for_material_requests, + get_non_completed_production_plans, get_sales_orders, get_warehouse_list, ) @@ -1132,9 +1132,9 @@ def test_resered_qty_for_production_plan_for_less_rm_qty(self): self.assertEqual(after_qty, before_qty) - completed_plans = get_completed_production_plans() + completed_plans = get_non_completed_production_plans() for plan in plans: - self.assertTrue(plan in completed_plans) + self.assertFalse(plan in completed_plans) def test_resered_qty_for_production_plan_for_material_requests_with_multi_UOM(self): from erpnext.stock.utils import get_or_make_bin diff --git a/erpnext/manufacturing/doctype/work_order/work_order.py b/erpnext/manufacturing/doctype/work_order/work_order.py index a491501d4b7a..93d015dc93ba 100644 --- a/erpnext/manufacturing/doctype/work_order/work_order.py +++ b/erpnext/manufacturing/doctype/work_order/work_order.py @@ -1493,7 +1493,7 @@ def update_item_quantity(source, target, source_parent): def get_reserved_qty_for_production( item_code: str, warehouse: str, - completed_production_plans: list = None, + non_completed_production_plans: list = None, check_production_plan: bool = False, ) -> float: """Get total reserved quantity for any item in specified warehouse""" @@ -1516,19 +1516,22 @@ def get_reserved_qty_for_production( & (wo_item.parent == wo.name) & (wo.docstatus == 1) & (wo_item.source_warehouse == warehouse) - & (wo.status.notin(["Stopped", "Completed", "Closed"])) - & ( - (wo_item.required_qty > wo_item.transferred_qty) - | (wo_item.required_qty > wo_item.consumed_qty) - ) ) ) if check_production_plan: query = query.where(wo.production_plan.isnotnull()) + else: + query = query.where( + (wo.status.notin(["Stopped", "Completed", "Closed"])) + & ( + (wo_item.required_qty > wo_item.transferred_qty) + | (wo_item.required_qty > wo_item.consumed_qty) + ) + ) - if completed_production_plans: - query = query.where(wo.production_plan.notin(completed_production_plans)) + if non_completed_production_plans: + query = query.where(wo.production_plan.isin(non_completed_production_plans)) return query.run()[0][0] or 0.0 From e4ed1d684d20dc3c7f5469ec4960941bf9db753c Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Thu, 5 Oct 2023 13:13:11 +0530 Subject: [PATCH 457/501] fix: fetch company details for Lead based quotation (cherry picked from commit f388864fd5a3ce95e0349d7fd37fb3878834262c) (cherry picked from commit c1d40a6bfa74c48bc10d493a464a1f2bdd8a9bbd) --- erpnext/crm/doctype/lead/lead.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/crm/doctype/lead/lead.py b/erpnext/crm/doctype/lead/lead.py index 460974972c58..294c41b93416 100644 --- a/erpnext/crm/doctype/lead/lead.py +++ b/erpnext/crm/doctype/lead/lead.py @@ -382,7 +382,7 @@ def get_lead_details(lead, posting_date=None, company=None): } ) - set_address_details(out, lead, "Lead") + set_address_details(out, lead, "Lead", company=company) taxes_and_charges = set_taxes( None, From 48ceead9d0eb335091399185ede9dfa07c622aee Mon Sep 17 00:00:00 2001 From: Frappe PR Bot Date: Wed, 11 Oct 2023 11:14:16 +0000 Subject: [PATCH 458/501] chore(release): Bumped to Version 14.43.1 ## [14.43.1](https://github.com/frappe/erpnext/compare/v14.43.0...v14.43.1) (2023-10-11) ### Bug Fixes * fetch company details for Lead based quotation ([e4ed1d6](https://github.com/frappe/erpnext/commit/e4ed1d684d20dc3c7f5469ec4960941bf9db753c)) --- erpnext/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/__init__.py b/erpnext/__init__.py index 25e95bb009a6..1d01d112e033 100644 --- a/erpnext/__init__.py +++ b/erpnext/__init__.py @@ -3,7 +3,7 @@ import frappe -__version__ = "14.43.0" +__version__ = "14.43.1" def get_default_company(user=None): From 26ad6885845962130cfb178fcbdbe2c4e75ce194 Mon Sep 17 00:00:00 2001 From: s-aga-r Date: Wed, 11 Oct 2023 18:44:32 +0530 Subject: [PATCH 459/501] fix: negative valuation rate in PR return (#37424) * fix: negative valuation rate in PR return * test: add test case for PR return --- erpnext/controllers/buying_controller.py | 20 +++++-- .../purchase_receipt/test_purchase_receipt.py | 54 +++++++++++++++++++ erpnext/stock/stock_ledger.py | 18 ++++--- 3 files changed, 80 insertions(+), 12 deletions(-) diff --git a/erpnext/controllers/buying_controller.py b/erpnext/controllers/buying_controller.py index 01990a3a268d..a38905c7e2bd 100644 --- a/erpnext/controllers/buying_controller.py +++ b/erpnext/controllers/buying_controller.py @@ -14,7 +14,8 @@ from erpnext.controllers.sales_and_purchase_return import get_rate_for_return from erpnext.controllers.subcontracting_controller import SubcontractingController from erpnext.stock.get_item_details import get_conversion_factor -from erpnext.stock.utils import get_incoming_rate +from erpnext.stock.stock_ledger import get_previous_sle +from erpnext.stock.utils import get_incoming_rate, get_valuation_method class QtyMismatchError(ValidationError): @@ -514,9 +515,20 @@ def update_stock_ledger(self, allow_negative_stock=False, via_landed_cost_vouche ) if self.is_return: - outgoing_rate = get_rate_for_return( - self.doctype, self.name, d.item_code, self.return_against, item_row=d - ) + if get_valuation_method(d.item_code) == "Moving Average": + previous_sle = get_previous_sle( + { + "item_code": d.item_code, + "warehouse": d.warehouse, + "posting_date": self.posting_date, + "posting_time": self.posting_time, + } + ) + outgoing_rate = flt(previous_sle.get("valuation_rate")) + else: + outgoing_rate = get_rate_for_return( + self.doctype, self.name, d.item_code, self.return_against, item_row=d + ) sle.update({"outgoing_rate": outgoing_rate, "recalculate_rate": 1}) if d.from_warehouse: diff --git a/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py b/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py index 82694a0b1929..463353e25495 100644 --- a/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py +++ b/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py @@ -2093,6 +2093,60 @@ def test_purchase_return_status_with_debit_note(self): return_pr.reload() self.assertEqual(return_pr.status, "Completed") + def test_valuation_rate_in_return_purchase_receipt_for_moving_average(self): + from erpnext.stock.doctype.stock_entry.test_stock_entry import make_stock_entry + from erpnext.stock.stock_ledger import get_previous_sle + + # Step - 1: Create an Item (Valuation Method = Moving Average) + item_code = make_item(properties={"is_stock_item": 1, "valuation_method": "Moving Average"}).name + + # Step - 2: Create a Purchase Receipt (Qty = 10, Rate = 100) + pr = make_purchase_receipt(qty=10, rate=100, item_code=item_code) + + # Step - 3: Create a Material Receipt Stock Entry (Qty = 100, Basic Rate = 10) + warehouse = "_Test Warehouse - _TC" + make_stock_entry( + purpose="Material Receipt", + item_code=item_code, + to_warehouse=warehouse, + qty=100, + rate=10, + ) + + # Step - 4: Create a Material Issue Stock Entry (Qty = 100, Basic Rate = 18.18 [Auto Fetched]) + make_stock_entry( + purpose="Material Issue", item_code=item_code, from_warehouse=warehouse, qty=100 + ) + + # Step - 5: Create a Return Purchase Return (Qty = -8, Rate = 100 [Auto fetched]) + return_pr = make_purchase_receipt( + is_return=1, + return_against=pr.name, + item_code=item_code, + qty=-8, + ) + + sle = frappe.db.get_value( + "Stock Ledger Entry", + {"voucher_no": return_pr.name, "voucher_detail_no": return_pr.items[0].name}, + ["posting_date", "posting_time", "outgoing_rate", "valuation_rate"], + as_dict=1, + ) + previous_sle_valuation_rate = get_previous_sle( + { + "item_code": item_code, + "warehouse": warehouse, + "posting_date": sle.posting_date, + "posting_time": sle.posting_time, + } + ).get("valuation_rate") + + # Test - 1: Valuation Rate should be equal to Outgoing Rate + self.assertEqual(flt(sle.outgoing_rate, 2), flt(sle.valuation_rate, 2)) + + # Test - 2: Valuation Rate should be equal to Previous SLE Valuation Rate + self.assertEqual(flt(sle.valuation_rate, 2), flt(previous_sle_valuation_rate, 2)) + def prepare_data_for_internal_transfer(): from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_internal_supplier diff --git a/erpnext/stock/stock_ledger.py b/erpnext/stock/stock_ledger.py index 108d36a490d9..13bbe1f5c08c 100644 --- a/erpnext/stock/stock_ledger.py +++ b/erpnext/stock/stock_ledger.py @@ -698,14 +698,16 @@ def get_incoming_outgoing_rate_from_transaction(self, sle): get_rate_for_return, # don't move this import to top ) - rate = get_rate_for_return( - sle.voucher_type, - sle.voucher_no, - sle.item_code, - voucher_detail_no=sle.voucher_detail_no, - sle=sle, - ) - + if self.valuation_method == "Moving Average": + rate = self.data[self.args.warehouse].previous_sle.valuation_rate + else: + rate = get_rate_for_return( + sle.voucher_type, + sle.voucher_no, + sle.item_code, + voucher_detail_no=sle.voucher_detail_no, + sle=sle, + ) elif ( sle.voucher_type in ["Purchase Receipt", "Purchase Invoice"] and sle.voucher_detail_no From 8dd26949b76b8762fa4f5a1b825bf052ba248f5d Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Tue, 10 Oct 2023 17:12:47 +0530 Subject: [PATCH 460/501] refactor: for non-repost fields, don't validate (cherry picked from commit c1782c50158e50e1e57e7eeda276ffd46203f86c) --- .../accounts/doctype/purchase_invoice/purchase_invoice.py | 5 +++-- erpnext/accounts/doctype/sales_invoice/sales_invoice.py | 5 +++-- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py index f6ec446ef357..c5e45d4f3a9e 100644 --- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py +++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py @@ -546,8 +546,9 @@ def on_update_after_submit(self): ] child_tables = {"items": ("expense_account",), "taxes": ("account_head",)} self.needs_repost = self.check_if_fields_updated(fields_to_check, child_tables) - self.validate_for_repost() - self.db_set("repost_required", self.needs_repost) + if self.needs_repost: + self.validate_for_repost() + self.db_set("repost_required", self.needs_repost) def make_gl_entries(self, gl_entries=None, from_repost=False): if not gl_entries: diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py index 03aca8ad5882..7f124f541f31 100644 --- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py +++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py @@ -548,8 +548,9 @@ def on_update_after_submit(self): "taxes": ("account_head",), } self.needs_repost = self.check_if_fields_updated(fields_to_check, child_tables) - self.validate_for_repost() - self.db_set("repost_required", self.needs_repost) + if self.needs_repost: + self.validate_for_repost() + self.db_set("repost_required", self.needs_repost) def set_paid_amount(self): paid_amount = 0.0 From d37a1811db239657dc1517501105918bbc4fb14d Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Wed, 11 Oct 2023 14:42:23 +0530 Subject: [PATCH 461/501] refactor: add validation for Advances in SI/PI (cherry picked from commit 0cdd6435a556309d62240fc669ce431efee040c3) --- erpnext/controllers/accounts_controller.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/erpnext/controllers/accounts_controller.py b/erpnext/controllers/accounts_controller.py index 7207743e095b..ab20f82ea257 100644 --- a/erpnext/controllers/accounts_controller.py +++ b/erpnext/controllers/accounts_controller.py @@ -12,6 +12,7 @@ add_days, add_months, cint, + comma_and, flt, fmt_money, formatdate, @@ -180,6 +181,17 @@ def validate(self): self.validate_party_account_currency() if self.doctype in ["Purchase Invoice", "Sales Invoice"]: + if invalid_advances := [ + x for x in self.advances if not x.reference_type or not x.reference_name + ]: + frappe.throw( + _( + "Rows: {0} in {1} section are Invalid. Reference Name should point to a valid Payment Entry or Journal Entry." + ).format( + frappe.bold(comma_and([x.idx for x in invalid_advances])), frappe.bold(_("Advance Payments")) + ) + ) + pos_check_field = "is_pos" if self.doctype == "Sales Invoice" else "is_paid" if cint(self.allocate_advances_automatically) and not cint(self.get(pos_check_field)): self.set_advances() From 2815952a03cdbb11fec2fbfa1301f5076ae424ed Mon Sep 17 00:00:00 2001 From: Frappe PR Bot Date: Thu, 12 Oct 2023 02:15:37 +0000 Subject: [PATCH 462/501] chore(release): Bumped to Version 14.44.0 # [14.44.0](https://github.com/frappe/erpnext/compare/v14.43.1...v14.44.0) (2023-10-12) ### Bug Fixes * added validation for the batch on stock reco ([#37174](https://github.com/frappe/erpnext/issues/37174)) ([4c337a6](https://github.com/frappe/erpnext/commit/4c337a6f44127e474503862f3ff0d718c4eeffad)) * ageing summary in AR ([15d2024](https://github.com/frappe/erpnext/commit/15d2024b8e8349562cae0f979d1d483d4d969d32)) * allocate amt for payment term invoices ([b22ac13](https://github.com/frappe/erpnext/commit/b22ac137f50b979331eca4349702343f9af4714f)) * call validate before setting repost flag ([bec3e8e](https://github.com/frappe/erpnext/commit/bec3e8ed96f24ea6b048f88d23d3dbc6bc457e0f)) * do not run bg job for single doc ([4123e7b](https://github.com/frappe/erpnext/commit/4123e7b2447d7ed5de4957ac48ea1deff5a50649)) * **Employee:** enable `no_copy` for `relieving_date` (backport [#37344](https://github.com/frappe/erpnext/issues/37344)) ([#37358](https://github.com/frappe/erpnext/issues/37358)) ([2b38b78](https://github.com/frappe/erpnext/commit/2b38b780bab39d8e3cce62a77cae6fd0e971652b)) * exception on exporting errored rows ([e58b3b1](https://github.com/frappe/erpnext/commit/e58b3b11e9aee1da3e8610d19112cdb30801a9fb)) * fetch company details for Lead based quotation ([c1d40a6](https://github.com/frappe/erpnext/commit/c1d40a6bfa74c48bc10d493a464a1f2bdd8a9bbd)) * fetch dependent task subject and project (backport [#37401](https://github.com/frappe/erpnext/issues/37401)) ([#37421](https://github.com/frappe/erpnext/issues/37421)) ([0aad942](https://github.com/frappe/erpnext/commit/0aad942312da44584f30b44249baeeed15145926)) * ignore cancelled gle in voucher-wise balance report ([#36417](https://github.com/frappe/erpnext/issues/36417)) ([ee1255a](https://github.com/frappe/erpnext/commit/ee1255a716451015da11a8afe62b4c0af2193e0c)) * incorrect status of the returned purchase receipt ([#37300](https://github.com/frappe/erpnext/issues/37300)) ([63f4573](https://github.com/frappe/erpnext/commit/63f45739e05d728e8844320ff0c7dbafa8660acf)) * linting issues ([6c8a65e](https://github.com/frappe/erpnext/commit/6c8a65e03b9ab4b57f5c254566ea9a57c79dd2e8)) * negative valuation rate in PR return ([#37424](https://github.com/frappe/erpnext/issues/37424)) ([26ad688](https://github.com/frappe/erpnext/commit/26ad6885845962130cfb178fcbdbe2c4e75ce194)) * payment request rounding in multi-currency and on status update ([eed5863](https://github.com/frappe/erpnext/commit/eed58634ba09ff78bf041d93c63c25146b25ecb9)) * production plan reserved qty incorrect calculation (backport [#37400](https://github.com/frappe/erpnext/issues/37400)) ([#37458](https://github.com/frappe/erpnext/issues/37458)) ([573b159](https://github.com/frappe/erpnext/commit/573b159541b6aebbdcc86202576c38c69c947b1e)) * split inv allocated amt on server side ([06b0477](https://github.com/frappe/erpnext/commit/06b04770fc878c00e1073ca07af1c09286f92a08)) * typo in doctype name and qb ([606c99e](https://github.com/frappe/erpnext/commit/606c99e57cdd36951ef8472df58dcf54e862e476)) * **ux:** allow MR to Stop until fully received (backport [#37452](https://github.com/frappe/erpnext/issues/37452)) ([#37456](https://github.com/frappe/erpnext/issues/37456)) ([fb0b426](https://github.com/frappe/erpnext/commit/fb0b426fe435357ad49513ae15e2b12a845864e0)) * validation for si ([3dc68e3](https://github.com/frappe/erpnext/commit/3dc68e3b002ec0f072809d9d94a175b16ea98e8d)) ### Features * add repost btn in invoice ([cde848d](https://github.com/frappe/erpnext/commit/cde848dc7fa755201391fd024e94e0a6935c632e)) * allow on submit fields ([f5245f6](https://github.com/frappe/erpnext/commit/f5245f6b3fdf8f89702458fd7bb2e04e4eacd927)) * allow repost for pi ([2d13dda](https://github.com/frappe/erpnext/commit/2d13dda49c59784fee4257ff56ebf679617713f9)) * composite WIP asset ([#37352](https://github.com/frappe/erpnext/issues/37352)) ([0ecd7d2](https://github.com/frappe/erpnext/commit/0ecd7d2bf5eb522315776bae5633486d89791e00)) * disable currency exchange api. ([#33593](https://github.com/frappe/erpnext/issues/33593)) ([1ca0516](https://github.com/frappe/erpnext/commit/1ca0516fe549320838620f14dc0e7611a43350e7)) * filter on voucher no ([cb35218](https://github.com/frappe/erpnext/commit/cb35218eec8954c7419863c659d3176805a6f79d)) * introduce unreconcile doctype ([ae8355c](https://github.com/frappe/erpnext/commit/ae8355c95391d0b4223a8c17a1f0fa786f45ed96)) * UI for unreconcile ([9531a45](https://github.com/frappe/erpnext/commit/9531a45b941e9481c5c1d0f5520c3fef4d6b1b09)) * unreconcile support for journal entry ([cd2d335](https://github.com/frappe/erpnext/commit/cd2d335256ede5980a0757a80fabc5140adc3d10)) * validate negative stock for inventory dimension ([#37373](https://github.com/frappe/erpnext/issues/37373)) ([1480aca](https://github.com/frappe/erpnext/commit/1480acabb0faeae61c7c055bb7d1e81877b87cfb)) --- erpnext/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/__init__.py b/erpnext/__init__.py index 1d01d112e033..26f7360563a4 100644 --- a/erpnext/__init__.py +++ b/erpnext/__init__.py @@ -3,7 +3,7 @@ import frappe -__version__ = "14.43.1" +__version__ = "14.44.0" def get_default_company(user=None): From f9b2355066b04489b7f4a69270eeac5032d5c32c Mon Sep 17 00:00:00 2001 From: barredterra <14891507+barredterra@users.noreply.github.com> Date: Mon, 9 Oct 2023 23:45:58 +0200 Subject: [PATCH 463/501] fix: german tranlations of "Is Return" (cherry picked from commit 38ca164662532a97469db4b2d0c1519a570120eb) --- erpnext/translations/de.csv | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/erpnext/translations/de.csv b/erpnext/translations/de.csv index dcba85b4d206..0c840f44ea89 100644 --- a/erpnext/translations/de.csv +++ b/erpnext/translations/de.csv @@ -5007,7 +5007,7 @@ ACC-PINV-.YYYY.-,ACC-PINV-.JJJJ.-, Tax Withholding Category,Steuereinbehalt Kategorie, Edit Posting Date and Time,Buchungsdatum und -uhrzeit bearbeiten, Is Paid,Ist bezahlt, -Is Return (Debit Note),ist Rücklieferung (Lastschrift), +Is Return (Debit Note),Ist Rechnungskorrektur (Retoure), Apply Tax Withholding Amount,Steuereinbehaltungsbetrag anwenden, Accounting Dimensions ,Buchhaltung Dimensionen, Supplier Invoice Details,Lieferant Rechnungsdetails, @@ -5133,7 +5133,7 @@ Default Bank / Cash account will be automatically updated in Salary Journal Entr ACC-SINV-.YYYY.-,ACC-SINV-.JJJJ.-, Include Payment (POS),(POS) Zahlung einschließen, Offline POS Name,Offline-Verkaufsstellen-Name, -Is Return (Credit Note),ist Rücklieferung (Gutschrift), +Is Return (Credit Note),Ist Rechnungskorrektur (Retoure), Update Billed Amount in Sales Order,Aktualisierung des Rechnungsbetrags im Auftrag, Customer PO Details,Auftragsdetails, Customer's Purchase Order,Bestellung des Kunden, @@ -7993,7 +7993,7 @@ Customs Tariff Number,Zolltarifnummer, Tariff Number,Tarifnummer, Delivery To,Lieferung an, MAT-DN-.YYYY.-,MAT-DN-.YYYY.-, -Is Return,Ist Rückgabe, +Is Return,Ist Retoure, Issue Credit Note,Gutschrift ausgeben, Return Against Delivery Note,Zurück zum Lieferschein, Customer's Purchase Order No,Bestellnummer des Kunden, From 0590f21814c189df07d87bc08a33c9d4fed4b95e Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Thu, 12 Oct 2023 19:44:36 +0530 Subject: [PATCH 464/501] fix: don't set finance books if gross_purchase_amount is not set (backport #37480) (#37482) fix: don't set finance books if gross_purchase_amount is not set (#37480) (cherry picked from commit 18e3a8907a78c323d1aacce6d46732f97ee8fdd2) Co-authored-by: Anand Baburajan --- erpnext/assets/doctype/asset/asset.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/erpnext/assets/doctype/asset/asset.js b/erpnext/assets/doctype/asset/asset.js index 0605189fec05..5d7794e1bccb 100644 --- a/erpnext/assets/doctype/asset/asset.js +++ b/erpnext/assets/doctype/asset/asset.js @@ -284,7 +284,7 @@ frappe.ui.form.on('Asset', { item_code: function(frm) { - if(frm.doc.item_code && frm.doc.calculate_depreciation) { + if(frm.doc.item_code && frm.doc.calculate_depreciation && frm.doc.gross_purchase_amount) { frm.trigger('set_finance_book'); } else { frm.set_value('finance_books', []); @@ -448,7 +448,7 @@ frappe.ui.form.on('Asset', { calculate_depreciation: function(frm) { frm.toggle_reqd("finance_books", frm.doc.calculate_depreciation); - if (frm.doc.item_code && frm.doc.calculate_depreciation ) { + if (frm.doc.item_code && frm.doc.calculate_depreciation && frm.doc.gross_purchase_amount) { frm.trigger("set_finance_book"); } else { frm.set_value("finance_books", []); From cf0ab51348fdc9e7266004b3caf2de9fa4025acb Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Thu, 12 Oct 2023 20:43:15 +0530 Subject: [PATCH 465/501] refactor(patch): ignore links on closing balance patch (cherry picked from commit 17ca8756a72765e10e17d2a2b81f29129263ab26) --- .../doctype/account_closing_balance/account_closing_balance.py | 1 + 1 file changed, 1 insertion(+) diff --git a/erpnext/accounts/doctype/account_closing_balance/account_closing_balance.py b/erpnext/accounts/doctype/account_closing_balance/account_closing_balance.py index e75af7047f19..d06bd833c8bc 100644 --- a/erpnext/accounts/doctype/account_closing_balance/account_closing_balance.py +++ b/erpnext/accounts/doctype/account_closing_balance/account_closing_balance.py @@ -37,6 +37,7 @@ def make_closing_entries(closing_entries, voucher_name, company, closing_date): } ) cle.flags.ignore_permissions = True + cle.flags.ignore_links = True cle.submit() From d2b22db5001ae6544e872234c6c3434f24c5a6b1 Mon Sep 17 00:00:00 2001 From: s-aga-r Date: Fri, 13 Oct 2023 10:22:20 +0530 Subject: [PATCH 466/501] fix: use `flt` to ignore TypeError (#37481) --- erpnext/stock/stock_ledger.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/stock/stock_ledger.py b/erpnext/stock/stock_ledger.py index 13bbe1f5c08c..84bcb99f738e 100644 --- a/erpnext/stock/stock_ledger.py +++ b/erpnext/stock/stock_ledger.py @@ -699,7 +699,7 @@ def get_incoming_outgoing_rate_from_transaction(self, sle): ) if self.valuation_method == "Moving Average": - rate = self.data[self.args.warehouse].previous_sle.valuation_rate + rate = flt(self.data[self.args.warehouse].previous_sle.valuation_rate) else: rate = get_rate_for_return( sle.voucher_type, From d266423011ff4c16f3e12231df34ffeeed56cf62 Mon Sep 17 00:00:00 2001 From: Dany Robert Date: Tue, 10 Oct 2023 10:30:09 +0000 Subject: [PATCH 467/501] fix(gp): wrong `allocated_amount` on multi sales person invoice (cherry picked from commit bda82bf1e9622dda9f7fa42b27b31b3879de342b) --- erpnext/accounts/report/gross_profit/gross_profit.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/erpnext/accounts/report/gross_profit/gross_profit.py b/erpnext/accounts/report/gross_profit/gross_profit.py index 2bfb4105c1de..de3d57d095a8 100644 --- a/erpnext/accounts/report/gross_profit/gross_profit.py +++ b/erpnext/accounts/report/gross_profit/gross_profit.py @@ -544,6 +544,8 @@ def get_average_rate_based_on_group_by(self): new_row.qty += flt(row.qty) new_row.buying_amount += flt(row.buying_amount, self.currency_precision) new_row.base_amount += flt(row.base_amount, self.currency_precision) + if self.filters.get("group_by") == "Sales Person": + new_row.allocated_amount += flt(row.allocated_amount, self.currency_precision) new_row = self.set_average_rate(new_row) self.grouped_data.append(new_row) From 6f143d35aab7258832acf8ded0ab42d339e33dc0 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Fri, 13 Oct 2023 15:39:54 +0530 Subject: [PATCH 468/501] fix: keyerror on gl and pl comparision report (cherry picked from commit ad00df0af6556a806e3b9b40ecaac719f0ce20a4) --- .../general_and_payment_ledger_comparison.py | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/erpnext/accounts/report/general_and_payment_ledger_comparison/general_and_payment_ledger_comparison.py b/erpnext/accounts/report/general_and_payment_ledger_comparison/general_and_payment_ledger_comparison.py index 553c137f0247..099884a48ecc 100644 --- a/erpnext/accounts/report/general_and_payment_ledger_comparison/general_and_payment_ledger_comparison.py +++ b/erpnext/accounts/report/general_and_payment_ledger_comparison/general_and_payment_ledger_comparison.py @@ -133,15 +133,17 @@ def compare(self): self.gle_balances = set(val.gle) | self.gle_balances self.ple_balances = set(val.ple) | self.ple_balances - self.diff1 = self.gle_balances.difference(self.ple_balances) - self.diff2 = self.ple_balances.difference(self.gle_balances) + self.variation_in_payment_ledger = self.gle_balances.difference(self.ple_balances) + self.variation_in_general_ledger = self.ple_balances.difference(self.gle_balances) self.diff = frappe._dict({}) - for x in self.diff1: + for x in self.variation_in_payment_ledger: self.diff[(x[0], x[1], x[2], x[3])] = frappe._dict({"gl_balance": x[4]}) - for x in self.diff2: - self.diff[(x[0], x[1], x[2], x[3])].update(frappe._dict({"pl_balance": x[4]})) + for x in self.variation_in_general_ledger: + self.diff.setdefault((x[0], x[1], x[2], x[3]), frappe._dict({"gl_balance": 0.0})).update( + frappe._dict({"pl_balance": x[4]}) + ) def generate_data(self): self.data = [] From 9406ddbff08d895a01aa2b9cd132879bea3a0fbc Mon Sep 17 00:00:00 2001 From: s-aga-r Date: Sat, 14 Oct 2023 16:53:29 +0530 Subject: [PATCH 469/501] fix: Stock Reconciliation Insufficient Stock Error (#37494) * fix: Stock Reconciliation Insufficient Stock Error * fix: linter * test: add test case for Stock Reco Batch Item --- .../test_stock_reconciliation.py | 52 +++++++++++++++++++ erpnext/stock/stock_ledger.py | 44 +++++++++------- 2 files changed, 77 insertions(+), 19 deletions(-) diff --git a/erpnext/stock/doctype/stock_reconciliation/test_stock_reconciliation.py b/erpnext/stock/doctype/stock_reconciliation/test_stock_reconciliation.py index d1e5c5d345f1..c913af3301a6 100644 --- a/erpnext/stock/doctype/stock_reconciliation/test_stock_reconciliation.py +++ b/erpnext/stock/doctype/stock_reconciliation/test_stock_reconciliation.py @@ -956,6 +956,58 @@ def test_batch_item_validation(self): self.assertRaises(frappe.ValidationError, sr.save) + @change_settings("Stock Settings", {"allow_negative_stock": 0}) + def test_backdated_stock_reco_for_batch_item_dont_have_future_sle(self): + # Step - 1: Create a Batch Item + from erpnext.stock.doctype.item.test_item import make_item + + item = make_item( + properties={ + "is_stock_item": 1, + "has_batch_no": 1, + "create_new_batch": 1, + "batch_number_series": "TEST-BATCH-.###", + } + ).name + + # Step - 2: Create Opening Stock Reconciliation + sr1 = create_stock_reconciliation( + item_code=item, + warehouse="_Test Warehouse - _TC", + qty=10, + purpose="Opening Stock", + posting_date=add_days(nowdate(), -2), + ) + + # Step - 3: Create Stock Entry (Material Receipt) + from erpnext.stock.doctype.stock_entry.test_stock_entry import make_stock_entry + + se1 = make_stock_entry( + item_code=item, + target="_Test Warehouse - _TC", + qty=100, + ) + + # Step - 4: Create Stock Entry (Material Issue) + make_stock_entry( + item_code=item, + source="_Test Warehouse - _TC", + qty=100, + batch_no=se1.items[0].batch_no, + purpose="Material Issue", + ) + + # Step - 5: Create Stock Reconciliation (Backdated) after the Stock Reconciliation 1 (Step - 2) + sr2 = create_stock_reconciliation( + item_code=item, + warehouse="_Test Warehouse - _TC", + qty=5, + batch_no=sr1.items[0].batch_no, + posting_date=add_days(nowdate(), -1), + ) + + self.assertEqual(sr2.docstatus, 1) + def create_batch_item_with_batch(item_name, batch_id): batch_item_doc = create_item(item_name, is_stock_item=1) diff --git a/erpnext/stock/stock_ledger.py b/erpnext/stock/stock_ledger.py index 84bcb99f738e..bdae87c7ee7e 100644 --- a/erpnext/stock/stock_ledger.py +++ b/erpnext/stock/stock_ledger.py @@ -1617,27 +1617,33 @@ def is_negative_with_precision(neg_sle, is_batch=False): return qty_deficit < 0 and abs(qty_deficit) > 0.0001 -def get_future_sle_with_negative_qty(args): - return frappe.db.sql( - """ - select - qty_after_transaction, posting_date, posting_time, - voucher_type, voucher_no - from `tabStock Ledger Entry` - where - item_code = %(item_code)s - and warehouse = %(warehouse)s - and voucher_no != %(voucher_no)s - and timestamp(posting_date, posting_time) >= timestamp(%(posting_date)s, %(posting_time)s) - and is_cancelled = 0 - and qty_after_transaction < 0 - order by timestamp(posting_date, posting_time) asc - limit 1 - """, - args, - as_dict=1, +def get_future_sle_with_negative_qty(sle): + SLE = frappe.qb.DocType("Stock Ledger Entry") + query = ( + frappe.qb.from_(SLE) + .select( + SLE.qty_after_transaction, SLE.posting_date, SLE.posting_time, SLE.voucher_type, SLE.voucher_no + ) + .where( + (SLE.item_code == sle.item_code) + & (SLE.warehouse == sle.warehouse) + & (SLE.voucher_no != sle.voucher_no) + & ( + CombineDatetime(SLE.posting_date, SLE.posting_time) + >= CombineDatetime(sle.posting_date, sle.posting_time) + ) + & (SLE.is_cancelled == 0) + & (SLE.qty_after_transaction < 0) + ) + .orderby(CombineDatetime(SLE.posting_date, SLE.posting_time)) + .limit(1) ) + if sle.voucher_type == "Stock Reconciliation" and sle.batch_no: + query = query.where(SLE.batch_no == sle.batch_no) + + return query.run(as_dict=True) + def get_future_sle_with_negative_batch_qty(args): return frappe.db.sql( From f1814a1a2a1c82c83c0f04fcc27b3b92526dd906 Mon Sep 17 00:00:00 2001 From: s-aga-r Date: Sun, 15 Oct 2023 09:57:39 +0530 Subject: [PATCH 470/501] fix: serial and batch no get removed on save of return DN (#37476) * fix: serial and batch no get removed on save of return DN * test: add test case for DN return with product bundle --- .../doctype/delivery_note/delivery_note.py | 12 ++++ .../delivery_note/test_delivery_note.py | 55 +++++++++++++++++-- .../doctype/packed_item/packed_item.json | 3 +- 3 files changed, 63 insertions(+), 7 deletions(-) diff --git a/erpnext/stock/doctype/delivery_note/delivery_note.py b/erpnext/stock/doctype/delivery_note/delivery_note.py index 115827a60cba..b18ee9943c7e 100644 --- a/erpnext/stock/doctype/delivery_note/delivery_note.py +++ b/erpnext/stock/doctype/delivery_note/delivery_note.py @@ -144,6 +144,7 @@ def validate(self): from erpnext.stock.doctype.packed_item.packed_item import make_packing_list + self.set_product_bundle_reference_in_packed_items() # should be called before `make_packing_list` make_packing_list(self) if self._action != "submit" and not self.is_return: @@ -430,6 +431,17 @@ def validate_duplicate_serial_nos(self): else: serial_nos.append(serial_no) + def set_product_bundle_reference_in_packed_items(self): + if self.packed_items and ((self.is_return and self.return_against) or self.amended_from): + if items_ref_map := { + item.dn_detail or item.get("_amended_from"): item.name + for item in self.items + if item.dn_detail or item.get("_amended_from") + }: + for item in self.packed_items: + if item.parent_detail_docname in items_ref_map: + item.parent_detail_docname = items_ref_map[item.parent_detail_docname] + def update_billed_amount_based_on_so(so_detail, update_modified=True): from frappe.query_builder.functions import Sum diff --git a/erpnext/stock/doctype/delivery_note/test_delivery_note.py b/erpnext/stock/doctype/delivery_note/test_delivery_note.py index 093e16c1cf81..8ea87f00c5b8 100644 --- a/erpnext/stock/doctype/delivery_note/test_delivery_note.py +++ b/erpnext/stock/doctype/delivery_note/test_delivery_note.py @@ -10,6 +10,7 @@ from erpnext.accounts.doctype.account.test_account import get_inventory_account from erpnext.accounts.utils import get_balance_on +from erpnext.controllers.sales_and_purchase_return import make_return_doc from erpnext.selling.doctype.product_bundle.test_product_bundle import make_product_bundle from erpnext.selling.doctype.sales_order.test_sales_order import ( automatically_fetch_payment_terms, @@ -268,8 +269,6 @@ def test_sales_return_for_non_bundled_items_partial(self): self.assertEqual(dn.items[0].returned_qty, 2) self.assertEqual(dn.per_returned, 40) - from erpnext.controllers.sales_and_purchase_return import make_return_doc - return_dn_2 = make_return_doc("Delivery Note", dn.name) # Check if unreturned amount is mapped in 2nd return @@ -361,8 +360,6 @@ def test_delivery_note_return_valuation_on_different_warehuose(self): dn.submit() self.assertEqual(dn.items[0].incoming_rate, 150) - from erpnext.controllers.sales_and_purchase_return import make_return_doc - return_dn = make_return_doc(dn.doctype, dn.name) return_dn.items[0].warehouse = return_warehouse return_dn.save().submit() @@ -1182,7 +1179,6 @@ def test_internal_transfer_precision_gle(self): ) def test_batch_expiry_for_delivery_note(self): - from erpnext.controllers.sales_and_purchase_return import make_return_doc from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import make_purchase_receipt item = make_item( @@ -1239,6 +1235,55 @@ def test_duplicate_serial_no_in_delivery_note(self): # Test - 1: ValidationError should be raised self.assertRaises(frappe.ValidationError, dn.submit) + def test_packed_items_for_return_delivery_note(self): + # Step - 1: Create Items + product_bundle_item = make_item(properties={"is_stock_item": 0}).name + batch_item = make_item( + properties={ + "is_stock_item": 1, + "has_batch_no": 1, + "create_new_batch": 1, + "batch_number_series": "TEST-BATCH-.#####", + } + ).name + serial_item = make_item( + properties={"is_stock_item": 1, "has_serial_no": 1, "serial_no_series": "TEST-SERIAL-.#####"} + ).name + + # Step - 2: Inward Stock + se1 = make_stock_entry(item_code=batch_item, target="_Test Warehouse - _TC", qty=3) + serial_nos = ( + make_stock_entry(item_code=serial_item, target="_Test Warehouse - _TC", qty=3) + .items[0] + .serial_no + ) + + # Step - 3: Create a Product Bundle + from erpnext.stock.doctype.stock_ledger_entry.test_stock_ledger_entry import ( + create_product_bundle_item, + ) + + create_product_bundle_item(product_bundle_item, packed_items=[[batch_item, 1], [serial_item, 1]]) + + # Step - 4: Create a Delivery Note for the Product Bundle + dn = create_delivery_note( + item_code=product_bundle_item, + warehouse="_Test Warehouse - _TC", + qty=3, + do_not_submit=True, + ) + dn.packed_items[1].serial_no = serial_nos + dn.save() + dn.submit() + + # Step - 5: Create a Return Delivery Note(Sales Return) + return_dn = make_return_doc(dn.doctype, dn.name) + return_dn.save() + return_dn.submit() + + self.assertEqual(return_dn.packed_items[0].batch_no, dn.packed_items[0].batch_no) + self.assertEqual(return_dn.packed_items[1].serial_no, dn.packed_items[1].serial_no) + def tearDown(self): frappe.db.rollback() frappe.db.set_single_value("Selling Settings", "dont_reserve_sales_order_qty_on_sales_return", 0) diff --git a/erpnext/stock/doctype/packed_item/packed_item.json b/erpnext/stock/doctype/packed_item/packed_item.json index c5fb2411c281..679c6c149e96 100644 --- a/erpnext/stock/doctype/packed_item/packed_item.json +++ b/erpnext/stock/doctype/packed_item/packed_item.json @@ -192,7 +192,6 @@ "fieldtype": "Data", "hidden": 1, "label": "Parent Detail docname", - "no_copy": 1, "oldfieldname": "parent_detail_docname", "oldfieldtype": "Data", "print_hide": 1, @@ -259,7 +258,7 @@ "index_web_pages_for_search": 1, "istable": 1, "links": [], - "modified": "2023-04-28 13:16:38.460806", + "modified": "2023-10-14 23:26:11.755425", "modified_by": "Administrator", "module": "Stock", "name": "Packed Item", From 405d1528c3d8dcff120003ffaacaf24d429c806a Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Mon, 2 Oct 2023 09:28:42 +0530 Subject: [PATCH 471/501] test: use fixtures for sales and purchase invoice (cherry picked from commit c322e5f38140b1fab8f940db542e25c2b122ab54) # Conflicts: # erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py --- .../accounts/doctype/sales_invoice/test_sales_invoice.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py index e0a7ff002bbd..55fecc560e3a 100644 --- a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py +++ b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py @@ -6,8 +6,12 @@ import frappe from frappe.model.dynamic_links import get_dynamic_link_map +<<<<<<< HEAD from frappe.model.naming import make_autoname from frappe.tests.utils import change_settings +======= +from frappe.tests.utils import FrappeTestCase, change_settings +>>>>>>> c322e5f381 (test: use fixtures for sales and purchase invoice) from frappe.utils import add_days, flt, getdate, nowdate, today import erpnext @@ -38,7 +42,7 @@ from erpnext.stock.utils import get_incoming_rate, get_stock_balance -class TestSalesInvoice(unittest.TestCase): +class TestSalesInvoice(FrappeTestCase): def setUp(self): from erpnext.stock.doctype.stock_ledger_entry.test_stock_ledger_entry import create_items @@ -46,6 +50,9 @@ def setUp(self): create_internal_parties() setup_accounts() + def tearDown(self): + frappe.db.rollback() + def make(self): w = frappe.copy_doc(test_records[0]) w.is_pos = 0 From 9d6b434d1f30acac9315cdbeac75266158b0998d Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Mon, 2 Oct 2023 10:56:39 +0530 Subject: [PATCH 472/501] refactor(test): unset accounts frozen date (cherry picked from commit fc50b174eb5f37a40b522f7e073d11260e0c12c8) --- erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py index 55fecc560e3a..ca6d83f4cefc 100644 --- a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py +++ b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py @@ -49,10 +49,14 @@ def setUp(self): create_items(["_Test Internal Transfer Item"], uoms=[{"uom": "Box", "conversion_factor": 10}]) create_internal_parties() setup_accounts() + self.remove_accounts_frozen_date() def tearDown(self): frappe.db.rollback() + def remove_accounts_frozen_date(self): + frappe.db.set_single_value("Accounts Settings", "acc_frozen_upto", None) + def make(self): w = frappe.copy_doc(test_records[0]) w.is_pos = 0 From 485cb7dd280b3f218c3bdf5ef723a36a0f9dbb67 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Mon, 2 Oct 2023 13:12:21 +0530 Subject: [PATCH 473/501] refactor(test): use @change_settings in sales invoice (cherry picked from commit 58065f31b1e2e550661a47b4442f6861406ebec5) # Conflicts: # erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py --- .../sales_invoice/test_sales_invoice.py | 27 ++++++++++++------- 1 file changed, 18 insertions(+), 9 deletions(-) diff --git a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py index ca6d83f4cefc..08d991c00352 100644 --- a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py +++ b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py @@ -49,14 +49,10 @@ def setUp(self): create_items(["_Test Internal Transfer Item"], uoms=[{"uom": "Box", "conversion_factor": 10}]) create_internal_parties() setup_accounts() - self.remove_accounts_frozen_date() def tearDown(self): frappe.db.rollback() - def remove_accounts_frozen_date(self): - frappe.db.set_single_value("Accounts Settings", "acc_frozen_upto", None) - def make(self): w = frappe.copy_doc(test_records[0]) w.is_pos = 0 @@ -3083,8 +3079,12 @@ def test_sales_commission(self): si.commission_rate = commission_rate self.assertRaises(frappe.ValidationError, si.save) + @change_settings("Accounts Settings", {"acc_frozen_upto": add_days(getdate(), 1)}) def test_sales_invoice_submission_post_account_freezing_date(self): +<<<<<<< HEAD frappe.db.set_value("Accounts Settings", None, "acc_frozen_upto", add_days(getdate(), 1)) +======= +>>>>>>> 58065f31b1 (refactor(test): use @change_settings in sales invoice) si = create_sales_invoice(do_not_save=True) si.posting_date = add_days(getdate(), 1) si.save() @@ -3093,8 +3093,11 @@ def test_sales_invoice_submission_post_account_freezing_date(self): si.posting_date = getdate() si.submit() +<<<<<<< HEAD frappe.db.set_value("Accounts Settings", None, "acc_frozen_upto", None) +======= +>>>>>>> 58065f31b1 (refactor(test): use @change_settings in sales invoice) def test_over_billing_case_against_delivery_note(self): """ Test a case where duplicating the item with qty = 1 in the invoice @@ -3123,6 +3126,13 @@ def test_over_billing_case_against_delivery_note(self): frappe.db.set_value("Accounts Settings", None, "over_billing_allowance", over_billing_allowance) + @change_settings( + "Accounts Settings", + { + "book_deferred_entries_via_journal_entry": 1, + "submit_journal_entries": 1, + }, + ) def test_multi_currency_deferred_revenue_via_journal_entry(self): deferred_account = create_account( account_name="Deferred Revenue", @@ -3130,11 +3140,6 @@ def test_multi_currency_deferred_revenue_via_journal_entry(self): company="_Test Company", ) - acc_settings = frappe.get_single("Accounts Settings") - acc_settings.book_deferred_entries_via_journal_entry = 1 - acc_settings.submit_journal_entries = 1 - acc_settings.save() - item = create_item("_Test Item for Deferred Accounting") item.enable_deferred_expense = 1 item.item_defaults[0].deferred_revenue_account = deferred_account @@ -3200,12 +3205,16 @@ def test_multi_currency_deferred_revenue_via_journal_entry(self): self.assertEqual(expected_gle[i][2], gle.debit) self.assertEqual(getdate(expected_gle[i][3]), gle.posting_date) +<<<<<<< HEAD acc_settings = frappe.get_single("Accounts Settings") acc_settings.book_deferred_entries_via_journal_entry = 0 acc_settings.submit_journal_entries = 0 acc_settings.save() frappe.db.set_value("Accounts Settings", None, "acc_frozen_upto", None) +======= + frappe.db.set_single_value("Accounts Settings", "acc_frozen_upto", None) +>>>>>>> 58065f31b1 (refactor(test): use @change_settings in sales invoice) def test_standalone_serial_no_return(self): si = create_sales_invoice( From 91a5bd86151e72e93e2ab8db86aaa718f169a6f1 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Mon, 2 Oct 2023 14:57:31 +0530 Subject: [PATCH 474/501] refactor(test): fix broken test cases in Sales Invoice (cherry picked from commit 8ebe5733ac61b6291b22901dbbf070093200706f) # Conflicts: # erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py --- erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py index 08d991c00352..528fc6f6c26e 100644 --- a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py +++ b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py @@ -49,6 +49,7 @@ def setUp(self): create_items(["_Test Internal Transfer Item"], uoms=[{"uom": "Box", "conversion_factor": 10}]) create_internal_parties() setup_accounts() + frappe.db.set_single_value("Accounts Settings", "acc_frozen_upto", None) def tearDown(self): frappe.db.rollback() @@ -3205,6 +3206,7 @@ def test_multi_currency_deferred_revenue_via_journal_entry(self): self.assertEqual(expected_gle[i][2], gle.debit) self.assertEqual(getdate(expected_gle[i][3]), gle.posting_date) +<<<<<<< HEAD <<<<<<< HEAD acc_settings = frappe.get_single("Accounts Settings") acc_settings.book_deferred_entries_via_journal_entry = 0 @@ -3216,6 +3218,8 @@ def test_multi_currency_deferred_revenue_via_journal_entry(self): frappe.db.set_single_value("Accounts Settings", "acc_frozen_upto", None) >>>>>>> 58065f31b1 (refactor(test): use @change_settings in sales invoice) +======= +>>>>>>> 8ebe5733ac (refactor(test): fix broken test cases in Sales Invoice) def test_standalone_serial_no_return(self): si = create_sales_invoice( item_code="_Test Serialized Item With Series", update_stock=True, is_return=True, qty=-1 From 5699a8daa29c67e2e219838d25ed7f7813efda62 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Wed, 11 Oct 2023 20:18:59 +0530 Subject: [PATCH 475/501] refactor(test): use @change_settings to fix failing test cases (cherry picked from commit de9baef84a77f5bb2aa94f200147d0689462b9c3) --- erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py index 528fc6f6c26e..6aae93388d85 100644 --- a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py +++ b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py @@ -180,6 +180,7 @@ def test_payment_entry_unlink_against_invoice(self): self.assertRaises(frappe.LinkExistsError, si.cancel) unlink_payment_on_cancel_of_invoice() + @change_settings("Accounts Settings", {"unlink_payment_on_cancellation_of_invoice": 1}) def test_payment_entry_unlink_against_standalone_credit_note(self): from erpnext.accounts.doctype.payment_entry.test_payment_entry import get_payment_entry @@ -1301,6 +1302,7 @@ def _insert_delivery_note(self): dn.submit() return dn + @change_settings("Accounts Settings", {"unlink_payment_on_cancellation_of_invoice": 1}) def test_sales_invoice_with_advance(self): from erpnext.accounts.doctype.journal_entry.test_journal_entry import ( test_records as jv_test_records, From b97fdbe6fce93ec52e688a2be692a254dc13f651 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Sun, 15 Oct 2023 05:56:52 +0530 Subject: [PATCH 476/501] refactor(test): use test fixture in subscription (cherry picked from commit 3bdf4f628c40d4e8ac19a41c738a8ba382d90d99) # Conflicts: # erpnext/accounts/doctype/subscription/test_subscription.py --- .../doctype/subscription/test_subscription.py | 525 ++++++++++++++++++ 1 file changed, 525 insertions(+) diff --git a/erpnext/accounts/doctype/subscription/test_subscription.py b/erpnext/accounts/doctype/subscription/test_subscription.py index c911e7fe12d5..4acddfec7e24 100644 --- a/erpnext/accounts/doctype/subscription/test_subscription.py +++ b/erpnext/accounts/doctype/subscription/test_subscription.py @@ -4,6 +4,7 @@ import unittest import frappe +from frappe.tests.utils import FrappeTestCase from frappe.utils.data import ( add_days, add_months, @@ -19,8 +20,532 @@ test_dependencies = ("UOM", "Item Group", "Item") +<<<<<<< HEAD def create_plan(): if not frappe.db.exists("Subscription Plan", "_Test Plan Name"): +======= +class TestSubscription(FrappeTestCase): + def setUp(self): + make_plans() + create_parties() + reset_settings() + frappe.db.set_single_value("Accounts Settings", "acc_frozen_upto", None) + + def tearDown(self): + frappe.db.rollback() + + def test_create_subscription_with_trial_with_correct_period(self): + subscription = create_subscription( + trial_period_start=nowdate(), trial_period_end=add_months(nowdate(), 1) + ) + self.assertEqual(subscription.trial_period_start, nowdate()) + self.assertEqual(subscription.trial_period_end, add_months(nowdate(), 1)) + self.assertEqual( + add_days(subscription.trial_period_end, 1), get_date_str(subscription.current_invoice_start) + ) + self.assertEqual( + add_to_date(subscription.current_invoice_start, months=1, days=-1), + get_date_str(subscription.current_invoice_end), + ) + self.assertEqual(subscription.invoices, []) + self.assertEqual(subscription.status, "Trialling") + + def test_create_subscription_without_trial_with_correct_period(self): + subscription = create_subscription() + self.assertEqual(subscription.trial_period_start, None) + self.assertEqual(subscription.trial_period_end, None) + self.assertEqual(subscription.current_invoice_start, nowdate()) + self.assertEqual(subscription.current_invoice_end, add_to_date(nowdate(), months=1, days=-1)) + # No invoice is created + self.assertEqual(len(subscription.invoices), 0) + self.assertEqual(subscription.status, "Active") + + def test_create_subscription_trial_with_wrong_dates(self): + subscription = create_subscription( + trial_period_start=add_days(nowdate(), 30), trial_period_end=nowdate(), do_not_save=True + ) + self.assertRaises(frappe.ValidationError, subscription.save) + + def test_invoice_is_generated_at_end_of_billing_period(self): + subscription = create_subscription(start_date="2018-01-01") + self.assertEqual(subscription.status, "Active") + self.assertEqual(subscription.current_invoice_start, "2018-01-01") + self.assertEqual(subscription.current_invoice_end, "2018-01-31") + + subscription.process(posting_date="2018-01-31") + self.assertEqual(len(subscription.invoices), 1) + self.assertEqual(subscription.current_invoice_start, "2018-02-01") + self.assertEqual(subscription.current_invoice_end, "2018-02-28") + self.assertEqual(subscription.status, "Unpaid") + + def test_status_goes_back_to_active_after_invoice_is_paid(self): + subscription = create_subscription( + start_date="2018-01-01", generate_invoice_at="Beginning of the current subscription period" + ) + subscription.process(posting_date="2018-01-01") # generate first invoice + self.assertEqual(len(subscription.invoices), 1) + + # Status is unpaid as Days until Due is zero and grace period is Zero + self.assertEqual(subscription.status, "Unpaid") + + subscription.get_current_invoice() + current_invoice = subscription.get_current_invoice() + + self.assertIsNotNone(current_invoice) + + current_invoice.db_set("outstanding_amount", 0) + current_invoice.db_set("status", "Paid") + subscription.process() + + self.assertEqual(subscription.status, "Active") + self.assertEqual(subscription.current_invoice_start, add_months(subscription.start_date, 1)) + self.assertEqual(len(subscription.invoices), 1) + + def test_subscription_cancel_after_grace_period(self): + settings = frappe.get_single("Subscription Settings") + settings.cancel_after_grace = 1 + settings.save() + + subscription = create_subscription(start_date="2018-01-01") + self.assertEqual(subscription.status, "Active") + + subscription.process(posting_date="2018-01-31") # generate first invoice + # This should change status to Cancelled since grace period is 0 + # And is backdated subscription so subscription will be cancelled after processing + self.assertEqual(subscription.status, "Cancelled") + + def test_subscription_unpaid_after_grace_period(self): + settings = frappe.get_single("Subscription Settings") + default_grace_period_action = settings.cancel_after_grace + settings.cancel_after_grace = 0 + settings.save() + + subscription = create_subscription(start_date="2018-01-01") + subscription.process(posting_date="2018-01-31") # generate first invoice + + # Status is unpaid as Days until Due is zero and grace period is Zero + self.assertEqual(subscription.status, "Unpaid") + + settings.cancel_after_grace = default_grace_period_action + settings.save() + + def test_subscription_invoice_days_until_due(self): + _date = add_months(nowdate(), -1) + subscription = create_subscription(start_date=_date, days_until_due=10) + + subscription.process(posting_date=subscription.current_invoice_end) # generate first invoice + self.assertEqual(len(subscription.invoices), 1) + self.assertEqual(subscription.status, "Active") + + def test_subscription_is_past_due_doesnt_change_within_grace_period(self): + settings = frappe.get_single("Subscription Settings") + grace_period = settings.grace_period + settings.grace_period = 1000 + settings.save() + + subscription = create_subscription(start_date=add_days(nowdate(), -1000)) + + subscription.process(posting_date=subscription.current_invoice_end) # generate first invoice + self.assertEqual(subscription.status, "Past Due Date") + + subscription.process() + # Grace period is 1000 days so status should remain as Past Due Date + self.assertEqual(subscription.status, "Past Due Date") + + subscription.process() + self.assertEqual(subscription.status, "Past Due Date") + + subscription.process() + self.assertEqual(subscription.status, "Past Due Date") + + settings.grace_period = grace_period + settings.save() + + def test_subscription_remains_active_during_invoice_period(self): + subscription = create_subscription() # no changes expected + + self.assertEqual(subscription.status, "Active") + self.assertEqual(subscription.current_invoice_start, nowdate()) + self.assertEqual(subscription.current_invoice_end, add_to_date(nowdate(), months=1, days=-1)) + self.assertEqual(len(subscription.invoices), 0) + + subscription.process() # no changes expected still + self.assertEqual(subscription.status, "Active") + self.assertEqual(subscription.current_invoice_start, nowdate()) + self.assertEqual(subscription.current_invoice_end, add_to_date(nowdate(), months=1, days=-1)) + self.assertEqual(len(subscription.invoices), 0) + + subscription.process() # no changes expected yet still + self.assertEqual(subscription.status, "Active") + self.assertEqual(subscription.current_invoice_start, nowdate()) + self.assertEqual(subscription.current_invoice_end, add_to_date(nowdate(), months=1, days=-1)) + self.assertEqual(len(subscription.invoices), 0) + + def test_subscription_cancellation(self): + subscription = create_subscription() + subscription.cancel_subscription() + + self.assertEqual(subscription.status, "Cancelled") + + def test_subscription_cancellation_invoices(self): + settings = frappe.get_single("Subscription Settings") + to_prorate = settings.prorate + settings.prorate = 1 + settings.save() + + subscription = create_subscription() + + self.assertEqual(subscription.status, "Active") + + subscription.cancel_subscription() + # Invoice must have been generated + self.assertEqual(len(subscription.invoices), 1) + + invoice = subscription.get_current_invoice() + diff = flt(date_diff(nowdate(), subscription.current_invoice_start) + 1) + plan_days = flt( + date_diff(subscription.current_invoice_end, subscription.current_invoice_start) + 1 + ) + prorate_factor = flt(diff / plan_days) + + self.assertEqual( + flt( + get_prorata_factor( + subscription.current_invoice_end, + subscription.current_invoice_start, + cint(subscription.generate_invoice_at == "Beginning of the current subscription period"), + ), + 2, + ), + flt(prorate_factor, 2), + ) + self.assertEqual(flt(invoice.grand_total, 2), flt(prorate_factor * 900, 2)) + self.assertEqual(subscription.status, "Cancelled") + + settings.prorate = to_prorate + settings.save() + + def test_subscription_cancellation_invoices_with_prorata_false(self): + settings = frappe.get_single("Subscription Settings") + to_prorate = settings.prorate + settings.prorate = 0 + settings.save() + + subscription = create_subscription() + subscription.cancel_subscription() + invoice = subscription.get_current_invoice() + + self.assertEqual(invoice.grand_total, 900) + + settings.prorate = to_prorate + settings.save() + + def test_subscription_cancellation_invoices_with_prorata_true(self): + settings = frappe.get_single("Subscription Settings") + to_prorate = settings.prorate + settings.prorate = 1 + settings.save() + + subscription = create_subscription() + subscription.cancel_subscription() + + invoice = subscription.get_current_invoice() + diff = flt(date_diff(nowdate(), subscription.current_invoice_start) + 1) + plan_days = flt( + date_diff(subscription.current_invoice_end, subscription.current_invoice_start) + 1 + ) + prorate_factor = flt(diff / plan_days) + + self.assertEqual(flt(invoice.grand_total, 2), flt(prorate_factor * 900, 2)) + + settings.prorate = to_prorate + settings.save() + + def test_subscription_cancellation_and_process(self): + settings = frappe.get_single("Subscription Settings") + default_grace_period_action = settings.cancel_after_grace + settings.cancel_after_grace = 1 + settings.save() + + subscription = create_subscription(start_date="2018-01-01") + subscription.process() # generate first invoice + + # Generate an invoice for the cancelled period + subscription.cancel_subscription() + self.assertEqual(subscription.status, "Cancelled") + self.assertEqual(len(subscription.invoices), 1) + + subscription.process() + self.assertEqual(subscription.status, "Cancelled") + self.assertEqual(len(subscription.invoices), 1) + + subscription.process() + self.assertEqual(subscription.status, "Cancelled") + self.assertEqual(len(subscription.invoices), 1) + + settings.cancel_after_grace = default_grace_period_action + settings.save() + + def test_subscription_restart_and_process(self): + settings = frappe.get_single("Subscription Settings") + default_grace_period_action = settings.cancel_after_grace + settings.grace_period = 0 + settings.cancel_after_grace = 0 + settings.save() + + subscription = create_subscription(start_date="2018-01-01") + subscription.process(posting_date="2018-01-31") # generate first invoice + + # Status is unpaid as Days until Due is zero and grace period is Zero + self.assertEqual(subscription.status, "Unpaid") + + subscription.cancel_subscription() + self.assertEqual(subscription.status, "Cancelled") + + subscription.restart_subscription() + self.assertEqual(subscription.status, "Active") + self.assertEqual(len(subscription.invoices), 1) + + subscription.process() + self.assertEqual(subscription.status, "Unpaid") + self.assertEqual(len(subscription.invoices), 1) + + subscription.process() + self.assertEqual(subscription.status, "Unpaid") + self.assertEqual(len(subscription.invoices), 1) + + settings.cancel_after_grace = default_grace_period_action + settings.save() + + def test_subscription_unpaid_back_to_active(self): + settings = frappe.get_single("Subscription Settings") + default_grace_period_action = settings.cancel_after_grace + settings.cancel_after_grace = 0 + settings.save() + + subscription = create_subscription( + start_date="2018-01-01", generate_invoice_at="Beginning of the current subscription period" + ) + subscription.process(subscription.current_invoice_start) # generate first invoice + # This should change status to Unpaid since grace period is 0 + self.assertEqual(subscription.status, "Unpaid") + + invoice = subscription.get_current_invoice() + invoice.db_set("outstanding_amount", 0) + invoice.db_set("status", "Paid") + + subscription.process() + self.assertEqual(subscription.status, "Active") + + # A new invoice is generated + subscription.process(posting_date=subscription.current_invoice_start) + self.assertEqual(subscription.status, "Unpaid") + + settings.cancel_after_grace = default_grace_period_action + settings.save() + + def test_restart_active_subscription(self): + subscription = create_subscription() + self.assertRaises(frappe.ValidationError, subscription.restart_subscription) + + def test_subscription_invoice_discount_percentage(self): + subscription = create_subscription(additional_discount_percentage=10) + subscription.cancel_subscription() + + invoice = subscription.get_current_invoice() + + self.assertEqual(invoice.additional_discount_percentage, 10) + self.assertEqual(invoice.apply_discount_on, "Grand Total") + + def test_subscription_invoice_discount_amount(self): + subscription = create_subscription(additional_discount_amount=11) + subscription.cancel_subscription() + + invoice = subscription.get_current_invoice() + + self.assertEqual(invoice.discount_amount, 11) + self.assertEqual(invoice.apply_discount_on, "Grand Total") + + def test_prepaid_subscriptions(self): + # Create a non pre-billed subscription, processing should not create + # invoices. + subscription = create_subscription() + subscription.process() + self.assertEqual(len(subscription.invoices), 0) + + # Change the subscription type to prebilled and process it. + # Prepaid invoice should be generated + subscription.generate_invoice_at = "Beginning of the current subscription period" + subscription.save() + subscription.process() + + self.assertEqual(len(subscription.invoices), 1) + + def test_prepaid_subscriptions_with_prorate_true(self): + settings = frappe.get_single("Subscription Settings") + to_prorate = settings.prorate + settings.prorate = 1 + settings.save() + + subscription = create_subscription( + generate_invoice_at="Beginning of the current subscription period" + ) + subscription.process() + subscription.cancel_subscription() + + self.assertEqual(len(subscription.invoices), 1) + + current_inv = subscription.get_current_invoice() + self.assertEqual(current_inv.status, "Unpaid") + + prorate_factor = 1 + + self.assertEqual(flt(current_inv.grand_total, 2), flt(prorate_factor * 900, 2)) + + settings.prorate = to_prorate + settings.save() + + def test_subscription_with_follow_calendar_months(self): + subscription = frappe.new_doc("Subscription") + subscription.company = "_Test Company" + subscription.party_type = "Supplier" + subscription.party = "_Test Supplier" + subscription.generate_invoice_at = "Beginning of the current subscription period" + subscription.follow_calendar_months = 1 + + # select subscription start date as "2018-01-15" + subscription.start_date = "2018-01-15" + subscription.end_date = "2018-07-15" + subscription.append("plans", {"plan": "_Test Plan Name 4", "qty": 1}) + subscription.save() + + # even though subscription starts at "2018-01-15" and Billing interval is Month and count 3 + # First invoice will end at "2018-03-31" instead of "2018-04-14" + self.assertEqual(get_date_str(subscription.current_invoice_end), "2018-03-31") + + def test_subscription_generate_invoice_past_due(self): + subscription = create_subscription( + start_date="2018-01-01", + party_type="Supplier", + party="_Test Supplier", + generate_invoice_at="Beginning of the current subscription period", + generate_new_invoices_past_due_date=1, + plans=[{"plan": "_Test Plan Name 4", "qty": 1}], + ) + + # Process subscription and create first invoice + # Subscription status will be unpaid since due date has already passed + subscription.process(posting_date="2018-01-01") + self.assertEqual(len(subscription.invoices), 1) + self.assertEqual(subscription.status, "Unpaid") + + # Now the Subscription is unpaid + # Even then new invoice should be created as we have enabled `generate_new_invoices_past_due_date` in + # subscription and the interval between the subscriptions is 3 months + subscription.process(posting_date="2018-04-01") + self.assertEqual(len(subscription.invoices), 2) + + def test_subscription_without_generate_invoice_past_due(self): + subscription = create_subscription( + start_date="2018-01-01", + generate_invoice_at="Beginning of the current subscription period", + plans=[{"plan": "_Test Plan Name 4", "qty": 1}], + ) + + # Process subscription and create first invoice + # Subscription status will be unpaid since due date has already passed + subscription.process() + self.assertEqual(len(subscription.invoices), 1) + self.assertEqual(subscription.status, "Unpaid") + + subscription.process() + self.assertEqual(len(subscription.invoices), 1) + + def test_multi_currency_subscription(self): + subscription = create_subscription( + start_date="2018-01-01", + generate_invoice_at="Beginning of the current subscription period", + plans=[{"plan": "_Test Plan Multicurrency", "qty": 1}], + party="_Test Subscription Customer", + ) + + subscription.process() + self.assertEqual(len(subscription.invoices), 1) + self.assertEqual(subscription.status, "Unpaid") + + # Check the currency of the created invoice + currency = frappe.db.get_value("Sales Invoice", subscription.invoices[0].name, "currency") + self.assertEqual(currency, "USD") + + def test_subscription_recovery(self): + """Test if Subscription recovers when start/end date run out of sync with created invoices.""" + subscription = create_subscription( + start_date="2021-01-01", + submit_invoice=0, + generate_new_invoices_past_due_date=1, + party="_Test Subscription Customer", + ) + + # create invoices for the first two moths + subscription.process(posting_date="2021-01-31") + + subscription.process(posting_date="2021-02-28") + + self.assertEqual(len(subscription.invoices), 2) + self.assertEqual( + getdate(frappe.db.get_value("Sales Invoice", subscription.invoices[0].name, "from_date")), + getdate("2021-01-01"), + ) + self.assertEqual( + getdate(frappe.db.get_value("Sales Invoice", subscription.invoices[1].name, "from_date")), + getdate("2021-02-01"), + ) + + # recreate most recent invoice + subscription.process(posting_date="2022-01-31") + + self.assertEqual(len(subscription.invoices), 2) + self.assertEqual( + getdate(frappe.db.get_value("Sales Invoice", subscription.invoices[0].name, "from_date")), + getdate("2021-01-01"), + ) + self.assertEqual( + getdate(frappe.db.get_value("Sales Invoice", subscription.invoices[1].name, "from_date")), + getdate("2021-02-01"), + ) + + def test_subscription_invoice_generation_before_days(self): + subscription = create_subscription( + start_date="2023-01-01", + generate_invoice_at="Days before the current subscription period", + number_of_days=10, + generate_new_invoices_past_due_date=1, + ) + + subscription.process(posting_date="2022-12-22") + self.assertEqual(len(subscription.invoices), 1) + + subscription.process(posting_date="2023-01-22") + self.assertEqual(len(subscription.invoices), 2) + + +def make_plans(): + create_plan(plan_name="_Test Plan Name", cost=900) + create_plan(plan_name="_Test Plan Name 2", cost=1999) + create_plan( + plan_name="_Test Plan Name 3", cost=1999, billing_interval="Day", billing_interval_count=14 + ) + create_plan( + plan_name="_Test Plan Name 4", cost=20000, billing_interval="Month", billing_interval_count=3 + ) + create_plan( + plan_name="_Test Plan Multicurrency", cost=50, billing_interval="Month", currency="USD" + ) + + +def create_plan(**kwargs): + if not frappe.db.exists("Subscription Plan", kwargs.get("plan_name")): +>>>>>>> 3bdf4f628c (refactor(test): use test fixture in subscription) plan = frappe.new_doc("Subscription Plan") plan.plan_name = "_Test Plan Name" plan.item = "_Test Non Stock Item" From 33becb7b325046d1b17ab6c0362219bcbb94bd53 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Sun, 15 Oct 2023 06:35:22 +0530 Subject: [PATCH 477/501] refactor(test): use test fixture in purchase invoice (cherry picked from commit a2e064d214ffb9f012f2144bee14cd467e935241) --- .../doctype/purchase_invoice/test_purchase_invoice.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py b/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py index 47126d3846f5..60892e9fc84b 100644 --- a/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py +++ b/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py @@ -5,7 +5,7 @@ import unittest import frappe -from frappe.tests.utils import change_settings +from frappe.tests.utils import FrappeTestCase, change_settings from frappe.utils import add_days, cint, flt, getdate, nowdate, today import erpnext @@ -33,7 +33,7 @@ test_ignore = ["Serial No"] -class TestPurchaseInvoice(unittest.TestCase, StockTestMixin): +class TestPurchaseInvoice(FrappeTestCase, StockTestMixin): @classmethod def setUpClass(self): unlink_payment_on_cancel_of_invoice() @@ -43,6 +43,9 @@ def setUpClass(self): def tearDownClass(self): unlink_payment_on_cancel_of_invoice(0) + def tearDown(self): + frappe.db.rollback() + def test_purchase_invoice_received_qty(self): """ 1. Test if received qty is validated against accepted + rejected From d78316869b5e13321f5ac3266b6eed19272fa550 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Sun, 15 Oct 2023 07:11:11 +0530 Subject: [PATCH 478/501] refactor(test): make use of @change_settings in PI test cases (cherry picked from commit 0207d6e7c996cd6c1b04f2ba171fdf3d6ccfa130) --- .../doctype/purchase_invoice/test_purchase_invoice.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py b/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py index 60892e9fc84b..170a163b45f0 100644 --- a/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py +++ b/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py @@ -420,6 +420,7 @@ def test_purchase_invoice_calculation(self): self.assertEqual(tax.tax_amount, expected_values[i][1]) self.assertEqual(tax.total, expected_values[i][2]) + @change_settings("Accounts Settings", {"unlink_payment_on_cancellation_of_invoice": 1}) def test_purchase_invoice_with_advance(self): from erpnext.accounts.doctype.journal_entry.test_journal_entry import ( test_records as jv_test_records, @@ -474,6 +475,7 @@ def test_purchase_invoice_with_advance(self): ) ) + @change_settings("Accounts Settings", {"unlink_payment_on_cancellation_of_invoice": 1}) def test_invoice_with_advance_and_multi_payment_terms(self): from erpnext.accounts.doctype.journal_entry.test_journal_entry import ( test_records as jv_test_records, @@ -1212,6 +1214,7 @@ def test_deferred_expense_via_journal_entry(self): acc_settings.submit_journal_entriessubmit_journal_entries = 0 acc_settings.save() + @change_settings("Accounts Settings", {"unlink_payment_on_cancellation_of_invoice": 1}) def test_gain_loss_with_advance_entry(self): unlink_enabled = frappe.db.get_value( "Accounts Settings", "Accounts Settings", "unlink_payment_on_cancel_of_invoice" @@ -1414,6 +1417,7 @@ def test_gain_loss_with_advance_entry(self): ) frappe.db.set_value("Company", "_Test Company", "exchange_gain_loss_account", original_account) + @change_settings("Accounts Settings", {"unlink_payment_on_cancellation_of_invoice": 1}) def test_purchase_invoice_advance_taxes(self): from erpnext.accounts.doctype.payment_entry.payment_entry import get_payment_entry From 8d1eac89e3f858b07bc9c27e0948a550c9e16c89 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Sun, 15 Oct 2023 08:07:29 +0530 Subject: [PATCH 479/501] refactor(test): make sure TDS Payable is available for testing (cherry picked from commit fbabf4ac2e96c473884c94e59b715d14dee3f960) --- .../accounts/doctype/sales_invoice/test_sales_invoice.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py index 6aae93388d85..ceeed34c8214 100644 --- a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py +++ b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py @@ -2784,6 +2784,13 @@ def test_additional_discount_for_sales_invoice_with_discount_accounting_enabled( company="_Test Company", ) + tds_payable_account = create_account( + account_name="TDS Payable", + account_type="Tax", + parent_account="Duties and Taxes - _TC", + company="_Test Company", + ) + si = create_sales_invoice(parent_cost_center="Main - _TC", do_not_save=1) si.apply_discount_on = "Grand Total" si.additional_discount_account = additional_discount_account From 7f903532f396445be30fe57af61ff7bb88c649e5 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Sun, 15 Oct 2023 10:40:57 +0530 Subject: [PATCH 480/501] chore: resovle conflicts --- .../sales_invoice/test_sales_invoice.py | 27 - .../doctype/subscription/test_subscription.py | 530 +----------------- 2 files changed, 5 insertions(+), 552 deletions(-) diff --git a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py index ceeed34c8214..272382e8c182 100644 --- a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py +++ b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py @@ -6,12 +6,8 @@ import frappe from frappe.model.dynamic_links import get_dynamic_link_map -<<<<<<< HEAD from frappe.model.naming import make_autoname -from frappe.tests.utils import change_settings -======= from frappe.tests.utils import FrappeTestCase, change_settings ->>>>>>> c322e5f381 (test: use fixtures for sales and purchase invoice) from frappe.utils import add_days, flt, getdate, nowdate, today import erpnext @@ -3091,10 +3087,6 @@ def test_sales_commission(self): @change_settings("Accounts Settings", {"acc_frozen_upto": add_days(getdate(), 1)}) def test_sales_invoice_submission_post_account_freezing_date(self): -<<<<<<< HEAD - frappe.db.set_value("Accounts Settings", None, "acc_frozen_upto", add_days(getdate(), 1)) -======= ->>>>>>> 58065f31b1 (refactor(test): use @change_settings in sales invoice) si = create_sales_invoice(do_not_save=True) si.posting_date = add_days(getdate(), 1) si.save() @@ -3103,11 +3095,6 @@ def test_sales_invoice_submission_post_account_freezing_date(self): si.posting_date = getdate() si.submit() -<<<<<<< HEAD - frappe.db.set_value("Accounts Settings", None, "acc_frozen_upto", None) - -======= ->>>>>>> 58065f31b1 (refactor(test): use @change_settings in sales invoice) def test_over_billing_case_against_delivery_note(self): """ Test a case where duplicating the item with qty = 1 in the invoice @@ -3215,20 +3202,6 @@ def test_multi_currency_deferred_revenue_via_journal_entry(self): self.assertEqual(expected_gle[i][2], gle.debit) self.assertEqual(getdate(expected_gle[i][3]), gle.posting_date) -<<<<<<< HEAD -<<<<<<< HEAD - acc_settings = frappe.get_single("Accounts Settings") - acc_settings.book_deferred_entries_via_journal_entry = 0 - acc_settings.submit_journal_entries = 0 - acc_settings.save() - - frappe.db.set_value("Accounts Settings", None, "acc_frozen_upto", None) -======= - frappe.db.set_single_value("Accounts Settings", "acc_frozen_upto", None) ->>>>>>> 58065f31b1 (refactor(test): use @change_settings in sales invoice) - -======= ->>>>>>> 8ebe5733ac (refactor(test): fix broken test cases in Sales Invoice) def test_standalone_serial_no_return(self): si = create_sales_invoice( item_code="_Test Serialized Item With Series", update_stock=True, is_return=True, qty=-1 diff --git a/erpnext/accounts/doctype/subscription/test_subscription.py b/erpnext/accounts/doctype/subscription/test_subscription.py index 4acddfec7e24..89ba0c8055e6 100644 --- a/erpnext/accounts/doctype/subscription/test_subscription.py +++ b/erpnext/accounts/doctype/subscription/test_subscription.py @@ -20,532 +20,8 @@ test_dependencies = ("UOM", "Item Group", "Item") -<<<<<<< HEAD def create_plan(): if not frappe.db.exists("Subscription Plan", "_Test Plan Name"): -======= -class TestSubscription(FrappeTestCase): - def setUp(self): - make_plans() - create_parties() - reset_settings() - frappe.db.set_single_value("Accounts Settings", "acc_frozen_upto", None) - - def tearDown(self): - frappe.db.rollback() - - def test_create_subscription_with_trial_with_correct_period(self): - subscription = create_subscription( - trial_period_start=nowdate(), trial_period_end=add_months(nowdate(), 1) - ) - self.assertEqual(subscription.trial_period_start, nowdate()) - self.assertEqual(subscription.trial_period_end, add_months(nowdate(), 1)) - self.assertEqual( - add_days(subscription.trial_period_end, 1), get_date_str(subscription.current_invoice_start) - ) - self.assertEqual( - add_to_date(subscription.current_invoice_start, months=1, days=-1), - get_date_str(subscription.current_invoice_end), - ) - self.assertEqual(subscription.invoices, []) - self.assertEqual(subscription.status, "Trialling") - - def test_create_subscription_without_trial_with_correct_period(self): - subscription = create_subscription() - self.assertEqual(subscription.trial_period_start, None) - self.assertEqual(subscription.trial_period_end, None) - self.assertEqual(subscription.current_invoice_start, nowdate()) - self.assertEqual(subscription.current_invoice_end, add_to_date(nowdate(), months=1, days=-1)) - # No invoice is created - self.assertEqual(len(subscription.invoices), 0) - self.assertEqual(subscription.status, "Active") - - def test_create_subscription_trial_with_wrong_dates(self): - subscription = create_subscription( - trial_period_start=add_days(nowdate(), 30), trial_period_end=nowdate(), do_not_save=True - ) - self.assertRaises(frappe.ValidationError, subscription.save) - - def test_invoice_is_generated_at_end_of_billing_period(self): - subscription = create_subscription(start_date="2018-01-01") - self.assertEqual(subscription.status, "Active") - self.assertEqual(subscription.current_invoice_start, "2018-01-01") - self.assertEqual(subscription.current_invoice_end, "2018-01-31") - - subscription.process(posting_date="2018-01-31") - self.assertEqual(len(subscription.invoices), 1) - self.assertEqual(subscription.current_invoice_start, "2018-02-01") - self.assertEqual(subscription.current_invoice_end, "2018-02-28") - self.assertEqual(subscription.status, "Unpaid") - - def test_status_goes_back_to_active_after_invoice_is_paid(self): - subscription = create_subscription( - start_date="2018-01-01", generate_invoice_at="Beginning of the current subscription period" - ) - subscription.process(posting_date="2018-01-01") # generate first invoice - self.assertEqual(len(subscription.invoices), 1) - - # Status is unpaid as Days until Due is zero and grace period is Zero - self.assertEqual(subscription.status, "Unpaid") - - subscription.get_current_invoice() - current_invoice = subscription.get_current_invoice() - - self.assertIsNotNone(current_invoice) - - current_invoice.db_set("outstanding_amount", 0) - current_invoice.db_set("status", "Paid") - subscription.process() - - self.assertEqual(subscription.status, "Active") - self.assertEqual(subscription.current_invoice_start, add_months(subscription.start_date, 1)) - self.assertEqual(len(subscription.invoices), 1) - - def test_subscription_cancel_after_grace_period(self): - settings = frappe.get_single("Subscription Settings") - settings.cancel_after_grace = 1 - settings.save() - - subscription = create_subscription(start_date="2018-01-01") - self.assertEqual(subscription.status, "Active") - - subscription.process(posting_date="2018-01-31") # generate first invoice - # This should change status to Cancelled since grace period is 0 - # And is backdated subscription so subscription will be cancelled after processing - self.assertEqual(subscription.status, "Cancelled") - - def test_subscription_unpaid_after_grace_period(self): - settings = frappe.get_single("Subscription Settings") - default_grace_period_action = settings.cancel_after_grace - settings.cancel_after_grace = 0 - settings.save() - - subscription = create_subscription(start_date="2018-01-01") - subscription.process(posting_date="2018-01-31") # generate first invoice - - # Status is unpaid as Days until Due is zero and grace period is Zero - self.assertEqual(subscription.status, "Unpaid") - - settings.cancel_after_grace = default_grace_period_action - settings.save() - - def test_subscription_invoice_days_until_due(self): - _date = add_months(nowdate(), -1) - subscription = create_subscription(start_date=_date, days_until_due=10) - - subscription.process(posting_date=subscription.current_invoice_end) # generate first invoice - self.assertEqual(len(subscription.invoices), 1) - self.assertEqual(subscription.status, "Active") - - def test_subscription_is_past_due_doesnt_change_within_grace_period(self): - settings = frappe.get_single("Subscription Settings") - grace_period = settings.grace_period - settings.grace_period = 1000 - settings.save() - - subscription = create_subscription(start_date=add_days(nowdate(), -1000)) - - subscription.process(posting_date=subscription.current_invoice_end) # generate first invoice - self.assertEqual(subscription.status, "Past Due Date") - - subscription.process() - # Grace period is 1000 days so status should remain as Past Due Date - self.assertEqual(subscription.status, "Past Due Date") - - subscription.process() - self.assertEqual(subscription.status, "Past Due Date") - - subscription.process() - self.assertEqual(subscription.status, "Past Due Date") - - settings.grace_period = grace_period - settings.save() - - def test_subscription_remains_active_during_invoice_period(self): - subscription = create_subscription() # no changes expected - - self.assertEqual(subscription.status, "Active") - self.assertEqual(subscription.current_invoice_start, nowdate()) - self.assertEqual(subscription.current_invoice_end, add_to_date(nowdate(), months=1, days=-1)) - self.assertEqual(len(subscription.invoices), 0) - - subscription.process() # no changes expected still - self.assertEqual(subscription.status, "Active") - self.assertEqual(subscription.current_invoice_start, nowdate()) - self.assertEqual(subscription.current_invoice_end, add_to_date(nowdate(), months=1, days=-1)) - self.assertEqual(len(subscription.invoices), 0) - - subscription.process() # no changes expected yet still - self.assertEqual(subscription.status, "Active") - self.assertEqual(subscription.current_invoice_start, nowdate()) - self.assertEqual(subscription.current_invoice_end, add_to_date(nowdate(), months=1, days=-1)) - self.assertEqual(len(subscription.invoices), 0) - - def test_subscription_cancellation(self): - subscription = create_subscription() - subscription.cancel_subscription() - - self.assertEqual(subscription.status, "Cancelled") - - def test_subscription_cancellation_invoices(self): - settings = frappe.get_single("Subscription Settings") - to_prorate = settings.prorate - settings.prorate = 1 - settings.save() - - subscription = create_subscription() - - self.assertEqual(subscription.status, "Active") - - subscription.cancel_subscription() - # Invoice must have been generated - self.assertEqual(len(subscription.invoices), 1) - - invoice = subscription.get_current_invoice() - diff = flt(date_diff(nowdate(), subscription.current_invoice_start) + 1) - plan_days = flt( - date_diff(subscription.current_invoice_end, subscription.current_invoice_start) + 1 - ) - prorate_factor = flt(diff / plan_days) - - self.assertEqual( - flt( - get_prorata_factor( - subscription.current_invoice_end, - subscription.current_invoice_start, - cint(subscription.generate_invoice_at == "Beginning of the current subscription period"), - ), - 2, - ), - flt(prorate_factor, 2), - ) - self.assertEqual(flt(invoice.grand_total, 2), flt(prorate_factor * 900, 2)) - self.assertEqual(subscription.status, "Cancelled") - - settings.prorate = to_prorate - settings.save() - - def test_subscription_cancellation_invoices_with_prorata_false(self): - settings = frappe.get_single("Subscription Settings") - to_prorate = settings.prorate - settings.prorate = 0 - settings.save() - - subscription = create_subscription() - subscription.cancel_subscription() - invoice = subscription.get_current_invoice() - - self.assertEqual(invoice.grand_total, 900) - - settings.prorate = to_prorate - settings.save() - - def test_subscription_cancellation_invoices_with_prorata_true(self): - settings = frappe.get_single("Subscription Settings") - to_prorate = settings.prorate - settings.prorate = 1 - settings.save() - - subscription = create_subscription() - subscription.cancel_subscription() - - invoice = subscription.get_current_invoice() - diff = flt(date_diff(nowdate(), subscription.current_invoice_start) + 1) - plan_days = flt( - date_diff(subscription.current_invoice_end, subscription.current_invoice_start) + 1 - ) - prorate_factor = flt(diff / plan_days) - - self.assertEqual(flt(invoice.grand_total, 2), flt(prorate_factor * 900, 2)) - - settings.prorate = to_prorate - settings.save() - - def test_subscription_cancellation_and_process(self): - settings = frappe.get_single("Subscription Settings") - default_grace_period_action = settings.cancel_after_grace - settings.cancel_after_grace = 1 - settings.save() - - subscription = create_subscription(start_date="2018-01-01") - subscription.process() # generate first invoice - - # Generate an invoice for the cancelled period - subscription.cancel_subscription() - self.assertEqual(subscription.status, "Cancelled") - self.assertEqual(len(subscription.invoices), 1) - - subscription.process() - self.assertEqual(subscription.status, "Cancelled") - self.assertEqual(len(subscription.invoices), 1) - - subscription.process() - self.assertEqual(subscription.status, "Cancelled") - self.assertEqual(len(subscription.invoices), 1) - - settings.cancel_after_grace = default_grace_period_action - settings.save() - - def test_subscription_restart_and_process(self): - settings = frappe.get_single("Subscription Settings") - default_grace_period_action = settings.cancel_after_grace - settings.grace_period = 0 - settings.cancel_after_grace = 0 - settings.save() - - subscription = create_subscription(start_date="2018-01-01") - subscription.process(posting_date="2018-01-31") # generate first invoice - - # Status is unpaid as Days until Due is zero and grace period is Zero - self.assertEqual(subscription.status, "Unpaid") - - subscription.cancel_subscription() - self.assertEqual(subscription.status, "Cancelled") - - subscription.restart_subscription() - self.assertEqual(subscription.status, "Active") - self.assertEqual(len(subscription.invoices), 1) - - subscription.process() - self.assertEqual(subscription.status, "Unpaid") - self.assertEqual(len(subscription.invoices), 1) - - subscription.process() - self.assertEqual(subscription.status, "Unpaid") - self.assertEqual(len(subscription.invoices), 1) - - settings.cancel_after_grace = default_grace_period_action - settings.save() - - def test_subscription_unpaid_back_to_active(self): - settings = frappe.get_single("Subscription Settings") - default_grace_period_action = settings.cancel_after_grace - settings.cancel_after_grace = 0 - settings.save() - - subscription = create_subscription( - start_date="2018-01-01", generate_invoice_at="Beginning of the current subscription period" - ) - subscription.process(subscription.current_invoice_start) # generate first invoice - # This should change status to Unpaid since grace period is 0 - self.assertEqual(subscription.status, "Unpaid") - - invoice = subscription.get_current_invoice() - invoice.db_set("outstanding_amount", 0) - invoice.db_set("status", "Paid") - - subscription.process() - self.assertEqual(subscription.status, "Active") - - # A new invoice is generated - subscription.process(posting_date=subscription.current_invoice_start) - self.assertEqual(subscription.status, "Unpaid") - - settings.cancel_after_grace = default_grace_period_action - settings.save() - - def test_restart_active_subscription(self): - subscription = create_subscription() - self.assertRaises(frappe.ValidationError, subscription.restart_subscription) - - def test_subscription_invoice_discount_percentage(self): - subscription = create_subscription(additional_discount_percentage=10) - subscription.cancel_subscription() - - invoice = subscription.get_current_invoice() - - self.assertEqual(invoice.additional_discount_percentage, 10) - self.assertEqual(invoice.apply_discount_on, "Grand Total") - - def test_subscription_invoice_discount_amount(self): - subscription = create_subscription(additional_discount_amount=11) - subscription.cancel_subscription() - - invoice = subscription.get_current_invoice() - - self.assertEqual(invoice.discount_amount, 11) - self.assertEqual(invoice.apply_discount_on, "Grand Total") - - def test_prepaid_subscriptions(self): - # Create a non pre-billed subscription, processing should not create - # invoices. - subscription = create_subscription() - subscription.process() - self.assertEqual(len(subscription.invoices), 0) - - # Change the subscription type to prebilled and process it. - # Prepaid invoice should be generated - subscription.generate_invoice_at = "Beginning of the current subscription period" - subscription.save() - subscription.process() - - self.assertEqual(len(subscription.invoices), 1) - - def test_prepaid_subscriptions_with_prorate_true(self): - settings = frappe.get_single("Subscription Settings") - to_prorate = settings.prorate - settings.prorate = 1 - settings.save() - - subscription = create_subscription( - generate_invoice_at="Beginning of the current subscription period" - ) - subscription.process() - subscription.cancel_subscription() - - self.assertEqual(len(subscription.invoices), 1) - - current_inv = subscription.get_current_invoice() - self.assertEqual(current_inv.status, "Unpaid") - - prorate_factor = 1 - - self.assertEqual(flt(current_inv.grand_total, 2), flt(prorate_factor * 900, 2)) - - settings.prorate = to_prorate - settings.save() - - def test_subscription_with_follow_calendar_months(self): - subscription = frappe.new_doc("Subscription") - subscription.company = "_Test Company" - subscription.party_type = "Supplier" - subscription.party = "_Test Supplier" - subscription.generate_invoice_at = "Beginning of the current subscription period" - subscription.follow_calendar_months = 1 - - # select subscription start date as "2018-01-15" - subscription.start_date = "2018-01-15" - subscription.end_date = "2018-07-15" - subscription.append("plans", {"plan": "_Test Plan Name 4", "qty": 1}) - subscription.save() - - # even though subscription starts at "2018-01-15" and Billing interval is Month and count 3 - # First invoice will end at "2018-03-31" instead of "2018-04-14" - self.assertEqual(get_date_str(subscription.current_invoice_end), "2018-03-31") - - def test_subscription_generate_invoice_past_due(self): - subscription = create_subscription( - start_date="2018-01-01", - party_type="Supplier", - party="_Test Supplier", - generate_invoice_at="Beginning of the current subscription period", - generate_new_invoices_past_due_date=1, - plans=[{"plan": "_Test Plan Name 4", "qty": 1}], - ) - - # Process subscription and create first invoice - # Subscription status will be unpaid since due date has already passed - subscription.process(posting_date="2018-01-01") - self.assertEqual(len(subscription.invoices), 1) - self.assertEqual(subscription.status, "Unpaid") - - # Now the Subscription is unpaid - # Even then new invoice should be created as we have enabled `generate_new_invoices_past_due_date` in - # subscription and the interval between the subscriptions is 3 months - subscription.process(posting_date="2018-04-01") - self.assertEqual(len(subscription.invoices), 2) - - def test_subscription_without_generate_invoice_past_due(self): - subscription = create_subscription( - start_date="2018-01-01", - generate_invoice_at="Beginning of the current subscription period", - plans=[{"plan": "_Test Plan Name 4", "qty": 1}], - ) - - # Process subscription and create first invoice - # Subscription status will be unpaid since due date has already passed - subscription.process() - self.assertEqual(len(subscription.invoices), 1) - self.assertEqual(subscription.status, "Unpaid") - - subscription.process() - self.assertEqual(len(subscription.invoices), 1) - - def test_multi_currency_subscription(self): - subscription = create_subscription( - start_date="2018-01-01", - generate_invoice_at="Beginning of the current subscription period", - plans=[{"plan": "_Test Plan Multicurrency", "qty": 1}], - party="_Test Subscription Customer", - ) - - subscription.process() - self.assertEqual(len(subscription.invoices), 1) - self.assertEqual(subscription.status, "Unpaid") - - # Check the currency of the created invoice - currency = frappe.db.get_value("Sales Invoice", subscription.invoices[0].name, "currency") - self.assertEqual(currency, "USD") - - def test_subscription_recovery(self): - """Test if Subscription recovers when start/end date run out of sync with created invoices.""" - subscription = create_subscription( - start_date="2021-01-01", - submit_invoice=0, - generate_new_invoices_past_due_date=1, - party="_Test Subscription Customer", - ) - - # create invoices for the first two moths - subscription.process(posting_date="2021-01-31") - - subscription.process(posting_date="2021-02-28") - - self.assertEqual(len(subscription.invoices), 2) - self.assertEqual( - getdate(frappe.db.get_value("Sales Invoice", subscription.invoices[0].name, "from_date")), - getdate("2021-01-01"), - ) - self.assertEqual( - getdate(frappe.db.get_value("Sales Invoice", subscription.invoices[1].name, "from_date")), - getdate("2021-02-01"), - ) - - # recreate most recent invoice - subscription.process(posting_date="2022-01-31") - - self.assertEqual(len(subscription.invoices), 2) - self.assertEqual( - getdate(frappe.db.get_value("Sales Invoice", subscription.invoices[0].name, "from_date")), - getdate("2021-01-01"), - ) - self.assertEqual( - getdate(frappe.db.get_value("Sales Invoice", subscription.invoices[1].name, "from_date")), - getdate("2021-02-01"), - ) - - def test_subscription_invoice_generation_before_days(self): - subscription = create_subscription( - start_date="2023-01-01", - generate_invoice_at="Days before the current subscription period", - number_of_days=10, - generate_new_invoices_past_due_date=1, - ) - - subscription.process(posting_date="2022-12-22") - self.assertEqual(len(subscription.invoices), 1) - - subscription.process(posting_date="2023-01-22") - self.assertEqual(len(subscription.invoices), 2) - - -def make_plans(): - create_plan(plan_name="_Test Plan Name", cost=900) - create_plan(plan_name="_Test Plan Name 2", cost=1999) - create_plan( - plan_name="_Test Plan Name 3", cost=1999, billing_interval="Day", billing_interval_count=14 - ) - create_plan( - plan_name="_Test Plan Name 4", cost=20000, billing_interval="Month", billing_interval_count=3 - ) - create_plan( - plan_name="_Test Plan Multicurrency", cost=50, billing_interval="Month", currency="USD" - ) - - -def create_plan(**kwargs): - if not frappe.db.exists("Subscription Plan", kwargs.get("plan_name")): ->>>>>>> 3bdf4f628c (refactor(test): use test fixture in subscription) plan = frappe.new_doc("Subscription Plan") plan.plan_name = "_Test Plan Name" plan.item = "_Test Non Stock Item" @@ -615,10 +91,14 @@ def create_parties(): customer.insert() -class TestSubscription(unittest.TestCase): +class TestSubscription(FrappeTestCase): def setUp(self): create_plan() create_parties() + frappe.db.set_single_value("Accounts Settings", "acc_frozen_upto", None) + + def tearDown(self): + frappe.db.rollback() def test_create_subscription_with_trial_with_correct_period(self): subscription = frappe.new_doc("Subscription") From 78e22af3caf6b07be1b80a03f3cd78ee874e1925 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Sun, 15 Oct 2023 11:19:16 +0530 Subject: [PATCH 481/501] fix: inflated total amt in TDS report using back calculation --- .../report/tds_payable_monthly/tds_payable_monthly.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/erpnext/accounts/report/tds_payable_monthly/tds_payable_monthly.py b/erpnext/accounts/report/tds_payable_monthly/tds_payable_monthly.py index 91ad3d6873ac..f2ec31c70e10 100644 --- a/erpnext/accounts/report/tds_payable_monthly/tds_payable_monthly.py +++ b/erpnext/accounts/report/tds_payable_monthly/tds_payable_monthly.py @@ -68,7 +68,11 @@ def get_result( tax_amount += entry.credit - entry.debit if net_total_map.get(name): - total_amount, grand_total, base_total = net_total_map.get(name) + if voucher_type == "Journal Entry": + # back calcalute total amount from rate and tax_amount + total_amount = grand_total = base_total = tax_amount / (rate / 100) + else: + total_amount, grand_total, base_total = net_total_map.get(name) else: total_amount += entry.credit From d1f6d62d72c9543996ef638d3d3005b36fc054c4 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Sun, 15 Oct 2023 12:19:34 +0530 Subject: [PATCH 482/501] chore: fix flaky test case --- .../test_bank_reconciliation_statement.py | 1 + 1 file changed, 1 insertion(+) diff --git a/erpnext/accounts/report/bank_reconciliation_statement/test_bank_reconciliation_statement.py b/erpnext/accounts/report/bank_reconciliation_statement/test_bank_reconciliation_statement.py index d7c871608ec4..b1be53ba73f5 100644 --- a/erpnext/accounts/report/bank_reconciliation_statement/test_bank_reconciliation_statement.py +++ b/erpnext/accounts/report/bank_reconciliation_statement/test_bank_reconciliation_statement.py @@ -23,6 +23,7 @@ def setUp(self): "Payment Entry", ]: frappe.db.delete(dt) + frappe.db.set_single_value("Accounts Settings", "acc_frozen_upto", None) def test_loan_entries_in_bank_reco_statement(self): create_loan_accounts() From 1b94510f08cb50dd786eddc6a20f1ef5f321beb0 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Mon, 16 Oct 2023 01:23:13 +0530 Subject: [PATCH 483/501] fix: consider received qty while creating SO -> MR (backport #37414) (#37514) fix: consider received qty while creating SO -> MR (cherry picked from commit b2cee396ac9edea5ba920382bfc27f3736600775) Co-authored-by: s-aga-r --- .../doctype/sales_order/sales_order.py | 40 +++++++++++-------- 1 file changed, 24 insertions(+), 16 deletions(-) diff --git a/erpnext/selling/doctype/sales_order/sales_order.py b/erpnext/selling/doctype/sales_order/sales_order.py index 485ac60e7440..25ea9189553f 100755 --- a/erpnext/selling/doctype/sales_order/sales_order.py +++ b/erpnext/selling/doctype/sales_order/sales_order.py @@ -542,29 +542,37 @@ def close_or_unclose_sales_orders(names, status): def get_requested_item_qty(sales_order): - return frappe._dict( - frappe.db.sql( - """ - select sales_order_item, sum(qty) - from `tabMaterial Request Item` - where docstatus = 1 - and sales_order = %s - group by sales_order_item - """, - sales_order, - ) - ) + result = {} + for d in frappe.db.get_all( + "Material Request Item", + filters={"docstatus": 1, "sales_order": sales_order}, + fields=["sales_order_item", "sum(qty) as qty", "sum(received_qty) as received_qty"], + group_by="sales_order_item", + ): + result[d.sales_order_item] = frappe._dict({"qty": d.qty, "received_qty": d.received_qty}) + + return result @frappe.whitelist() def make_material_request(source_name, target_doc=None): requested_item_qty = get_requested_item_qty(source_name) + def get_remaining_qty(so_item): + return flt( + flt(so_item.qty) + - flt(requested_item_qty.get(so_item.name, {}).get("qty")) + - max( + flt(so_item.get("delivered_qty")) + - flt(requested_item_qty.get(so_item.name, {}).get("received_qty")), + 0, + ) + ) + def update_item(source, target, source_parent): # qty is for packed items, because packed items don't have stock_qty field - qty = source.get("qty") target.project = source_parent.project - target.qty = qty - requested_item_qty.get(source.name, 0) - flt(source.get("delivered_qty")) + target.qty = get_remaining_qty(source) target.stock_qty = flt(target.qty) * flt(target.conversion_factor) args = target.as_dict().copy() @@ -597,8 +605,8 @@ def update_item(source, target, source_parent): "Sales Order Item": { "doctype": "Material Request Item", "field_map": {"name": "sales_order_item", "parent": "sales_order"}, - "condition": lambda doc: not frappe.db.exists("Product Bundle", doc.item_code) - and (doc.stock_qty - flt(doc.get("delivered_qty"))) > requested_item_qty.get(doc.name, 0), + "condition": lambda item: not frappe.db.exists("Product Bundle", item.item_code) + and get_remaining_qty(item) > 0, "postprocess": update_item, }, }, From c32258e4b690758b4a2e667edd85caa99083ab62 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Mon, 16 Oct 2023 01:24:10 +0530 Subject: [PATCH 484/501] fix: GL Entries not getting created for PR Return (backport #37513) (#37516) * fix: GL Entries not getting created for PR Return (cherry picked from commit 46add06a29f8a0a5990dfd3aeda39f01413071bb) * test: add test case for PR return with zero rate (cherry picked from commit 253d4782c63963df78216ce51d9f9f9a80791531) # Conflicts: # erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py * chore: `conflicts` --------- Co-authored-by: s-aga-r --- .../purchase_receipt/purchase_receipt.py | 2 +- .../purchase_receipt/test_purchase_receipt.py | 56 +++++++++++++++++++ 2 files changed, 57 insertions(+), 1 deletion(-) diff --git a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py index 4a651cd0d18b..da534b245c65 100644 --- a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py +++ b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py @@ -341,7 +341,7 @@ def make_item_gl_entries(self, gl_entries, warehouse_account=None): exchange_rate_map, net_rate_map = get_purchase_document_details(self) for d in self.get("items"): - if d.item_code in stock_items and flt(d.valuation_rate) and flt(d.qty): + if d.item_code in stock_items and flt(d.qty) and (flt(d.valuation_rate) or self.is_return): if warehouse_account.get(d.warehouse): stock_value_diff = frappe.db.get_value( "Stock Ledger Entry", diff --git a/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py b/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py index 463353e25495..a93d5b1bbbed 100644 --- a/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py +++ b/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py @@ -2147,6 +2147,62 @@ def test_valuation_rate_in_return_purchase_receipt_for_moving_average(self): # Test - 2: Valuation Rate should be equal to Previous SLE Valuation Rate self.assertEqual(flt(sle.valuation_rate, 2), flt(previous_sle_valuation_rate, 2)) + def test_purchase_return_with_zero_rate(self): + company = "_Test Company with perpetual inventory" + + # Step - 1: Create Item + item, warehouse = ( + make_item(properties={"is_stock_item": 1, "valuation_method": "Moving Average"}).name, + "Stores - TCP1", + ) + + # Step - 2: Create Stock Entry (Material Receipt) + from erpnext.stock.doctype.stock_entry.test_stock_entry import make_stock_entry + + se = make_stock_entry( + purpose="Material Receipt", + item_code=item, + qty=100, + basic_rate=100, + to_warehouse=warehouse, + company=company, + ) + + # Step - 3: Create Purchase Receipt + pr = make_purchase_receipt( + item_code=item, + qty=5, + rate=0, + warehouse=warehouse, + company=company, + ) + + # Step - 4: Create Purchase Return + from erpnext.controllers.sales_and_purchase_return import make_return_doc + + pr_return = make_return_doc("Purchase Receipt", pr.name) + pr_return.save() + pr_return.submit() + + sl_entries = get_sl_entries(pr_return.doctype, pr_return.name) + gl_entries = get_gl_entries(pr_return.doctype, pr_return.name) + + # Test - 1: SLE Stock Value Difference should be equal to Qty * Average Rate + average_rate = ( + (se.items[0].qty * se.items[0].basic_rate) + (pr.items[0].qty * pr.items[0].rate) + ) / (se.items[0].qty + pr.items[0].qty) + expected_stock_value_difference = pr_return.items[0].qty * average_rate + self.assertEqual( + flt(sl_entries[0].stock_value_difference, 2), flt(expected_stock_value_difference, 2) + ) + + # Test - 2: GL Entries should be created for Stock Value Difference + self.assertEqual(len(gl_entries), 2) + + # Test - 3: SLE Stock Value Difference should be equal to Debit or Credit of GL Entries. + for entry in gl_entries: + self.assertEqual(abs(entry.debit + entry.credit), abs(sl_entries[0].stock_value_difference)) + def prepare_data_for_internal_transfer(): from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_internal_supplier From 44f7de0f31f4464f2823038356bb52f479fa3d5b Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Mon, 16 Oct 2023 14:37:16 +0530 Subject: [PATCH 485/501] fix: Incorrect vat amount in KSA VAT report --- erpnext/regional/report/ksa_vat/ksa_vat.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/erpnext/regional/report/ksa_vat/ksa_vat.py b/erpnext/regional/report/ksa_vat/ksa_vat.py index 3571f962667c..8c1cf3c80fec 100644 --- a/erpnext/regional/report/ksa_vat/ksa_vat.py +++ b/erpnext/regional/report/ksa_vat/ksa_vat.py @@ -177,7 +177,8 @@ def get_tax_data_for_each_vat_setting(vat_setting, filters, doctype): "parent": invoice.name, "item_tax_template": vat_setting.item_tax_template, }, - fields=["item_code", "base_net_amount"], + fields=["item_code", "sum(base_net_amount) as base_net_amount"], + group_by="item_code, item_tax_template", ) for item in invoice_items: From 001c230688b6cd757c7f0bd39afc2c1543c21080 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Mon, 16 Oct 2023 16:48:55 +0530 Subject: [PATCH 486/501] perf: index `dn_detail` in `Delivery Note Item` (backport #37528) (#37530) * perf: index `dn_detail` in `Delivery Note Item` (cherry picked from commit 5b4528e6146aaeb8f86f9fde3f272635d005eeec) # Conflicts: # erpnext/stock/doctype/delivery_note_item/delivery_note_item.json * chore: `conflicts` --------- Co-authored-by: s-aga-r --- .../stock/doctype/delivery_note_item/delivery_note_item.json | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/erpnext/stock/doctype/delivery_note_item/delivery_note_item.json b/erpnext/stock/doctype/delivery_note_item/delivery_note_item.json index edfb269da9ae..237088f64a74 100644 --- a/erpnext/stock/doctype/delivery_note_item/delivery_note_item.json +++ b/erpnext/stock/doctype/delivery_note_item/delivery_note_item.json @@ -741,7 +741,8 @@ "label": "Against Delivery Note Item", "no_copy": 1, "print_hide": 1, - "read_only": 1 + "read_only": 1, + "search_index": 1 }, { "fieldname": "stock_qty_sec_break", @@ -868,7 +869,7 @@ "index_web_pages_for_search": 1, "istable": 1, "links": [], - "modified": "2023-07-25 11:58:28.101919", + "modified": "2023-10-16 16:18:18.013379", "modified_by": "Administrator", "module": "Stock", "name": "Delivery Note Item", From 76ef61c24fab6190790034da0e5e8ea1d3d2e242 Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Mon, 16 Oct 2023 16:53:43 +0530 Subject: [PATCH 487/501] fix: keep customer/supplier website role by default (cherry picked from commit d2096cfdb752bff03f8d3a00262d86c9eeb76c37) --- erpnext/setup/install.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/erpnext/setup/install.py b/erpnext/setup/install.py index 1e047d1e4dd3..71b1ca7c0586 100644 --- a/erpnext/setup/install.py +++ b/erpnext/setup/install.py @@ -33,6 +33,7 @@ def after_install(): add_app_name() setup_log_settings() hide_workspaces() + update_roles() frappe.db.commit() @@ -214,6 +215,12 @@ def hide_workspaces(): frappe.db.set_value("Workspace", ws, "public", 0) +def update_roles(): + website_user_roles = ("Customer", "Supplier") + for role in website_user_roles: + frappe.db.set_value("Role", role, "desk_access", 0) + + def create_default_role_profiles(): for role_profile_name, roles in DEFAULT_ROLE_PROFILES.items(): role_profile = frappe.new_doc("Role Profile") From 71cb7d37ee9202f4a4e3a4e7f1b55105b4a8a26d Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Tue, 3 Oct 2023 09:53:41 +0530 Subject: [PATCH 488/501] refactor: checkbox to toggle exchange rate inheritence in PO->PI (cherry picked from commit 08315522bbc198fce1168a5e8522684cad750276) --- .../doctype/purchase_invoice/purchase_invoice.json | 10 +++++++++- .../doctype/buying_settings/buying_settings.json | 10 +++++++++- erpnext/controllers/accounts_controller.py | 11 +++++++++++ 3 files changed, 29 insertions(+), 2 deletions(-) diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.json b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.json index 1f3b17ee147d..02a5114f345f 100644 --- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.json +++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.json @@ -36,6 +36,7 @@ "currency_and_price_list", "currency", "conversion_rate", + "use_transaction_date_exchange_rate", "column_break2", "buying_price_list", "price_list_currency", @@ -1578,13 +1579,20 @@ "label": "Repost Required", "options": "Account", "read_only": 1 + }, + { + "default": "0", + "fieldname": "use_transaction_date_exchange_rate", + "fieldtype": "Check", + "label": "Use Transaction Date Exchange Rate", + "read_only": 1 } ], "icon": "fa fa-file-text", "idx": 204, "is_submittable": 1, "links": [], - "modified": "2023-10-01 21:01:47.282533", + "modified": "2023-10-16 16:24:51.886231", "modified_by": "Administrator", "module": "Accounts", "name": "Purchase Invoice", diff --git a/erpnext/buying/doctype/buying_settings/buying_settings.json b/erpnext/buying/doctype/buying_settings/buying_settings.json index 8c73e56a99e2..71cb01b188f6 100644 --- a/erpnext/buying/doctype/buying_settings/buying_settings.json +++ b/erpnext/buying/doctype/buying_settings/buying_settings.json @@ -24,6 +24,7 @@ "bill_for_rejected_quantity_in_purchase_invoice", "disable_last_purchase_rate", "show_pay_button", + "use_transaction_date_exchange_rate", "subcontract", "backflush_raw_materials_of_subcontract_based_on", "column_break_11", @@ -164,6 +165,13 @@ "fieldname": "over_order_allowance", "fieldtype": "Float", "label": "Over Order Allowance (%)" + }, + { + "default": "0", + "description": "While making Purchase Invoice from Purchase Order, use Exchange Rate on Invoice's transaction date rather than inheriting it from Purchase Order. Only applies for Purchase Invoice.", + "fieldname": "use_transaction_date_exchange_rate", + "fieldtype": "Check", + "label": "Use Transaction Date Exchange Rate" } ], "icon": "fa fa-cog", @@ -171,7 +179,7 @@ "index_web_pages_for_search": 1, "issingle": 1, "links": [], - "modified": "2023-03-02 17:02:14.404622", + "modified": "2023-10-16 16:22:03.201078", "modified_by": "Administrator", "module": "Buying", "name": "Buying Settings", diff --git a/erpnext/controllers/accounts_controller.py b/erpnext/controllers/accounts_controller.py index ab20f82ea257..7ae1b0c1ad2a 100644 --- a/erpnext/controllers/accounts_controller.py +++ b/erpnext/controllers/accounts_controller.py @@ -583,6 +583,17 @@ def set_price_list_currency(self, buying_or_selling): self.currency, self.company_currency, transaction_date, args ) + if ( + self.currency + and buying_or_selling == "Buying" + and frappe.db.get_single_value("Buying Settings", "use_transaction_date_exchange_rate") + and self.doctype == "Purchase Invoice" + ): + self.use_transaction_date_exchange_rate = True + self.conversion_rate = get_exchange_rate( + self.currency, self.company_currency, transaction_date, args + ) + def set_missing_item_details(self, for_validate=False): """set missing item values""" from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos From 50258502585d30791bd6716b4d2e2935721047f9 Mon Sep 17 00:00:00 2001 From: s-aga-r Date: Tue, 17 Oct 2023 12:20:23 +0530 Subject: [PATCH 489/501] fix: same Serial No get mapped while creating SO -> DN (#37527) * fix: same Serial No get mapped while creating SO -> DN * test: add test case for DN with repetitive serial item --- erpnext/controllers/accounts_controller.py | 7 +++++ .../delivery_note/test_delivery_note.py | 29 ++++++++++++++++++- erpnext/stock/get_item_details.py | 4 +++ 3 files changed, 39 insertions(+), 1 deletion(-) diff --git a/erpnext/controllers/accounts_controller.py b/erpnext/controllers/accounts_controller.py index 7ae1b0c1ad2a..c17866162b6c 100644 --- a/erpnext/controllers/accounts_controller.py +++ b/erpnext/controllers/accounts_controller.py @@ -617,6 +617,7 @@ def set_missing_item_details(self, for_validate=False): self.pricing_rules = [] + selected_serial_nos_map = {} for item in self.get("items"): if item.get("item_code"): args = parent_dict.copy() @@ -628,6 +629,7 @@ def set_missing_item_details(self, for_validate=False): args["ignore_pricing_rule"] = ( self.ignore_pricing_rule if hasattr(self, "ignore_pricing_rule") else 0 ) + args["ignore_serial_nos"] = selected_serial_nos_map.get(item.get("item_code")) if not args.get("transaction_date"): args["transaction_date"] = args.get("posting_date") @@ -684,6 +686,11 @@ def set_missing_item_details(self, for_validate=False): if ret.get("pricing_rules"): self.apply_pricing_rule_on_items(item, ret) self.set_pricing_rule_details(item, ret) + + if ret.get("serial_no"): + selected_serial_nos_map.setdefault(item.get("item_code"), []).extend( + ret.get("serial_no").split("\n") + ) else: # Transactions line item without item code diff --git a/erpnext/stock/doctype/delivery_note/test_delivery_note.py b/erpnext/stock/doctype/delivery_note/test_delivery_note.py index 8ea87f00c5b8..c6f3197a6686 100644 --- a/erpnext/stock/doctype/delivery_note/test_delivery_note.py +++ b/erpnext/stock/doctype/delivery_note/test_delivery_note.py @@ -5,7 +5,7 @@ import json import frappe -from frappe.tests.utils import FrappeTestCase +from frappe.tests.utils import FrappeTestCase, change_settings from frappe.utils import add_days, cstr, flt, nowdate, nowtime, today from erpnext.accounts.doctype.account.test_account import get_inventory_account @@ -1284,6 +1284,33 @@ def test_packed_items_for_return_delivery_note(self): self.assertEqual(return_dn.packed_items[0].batch_no, dn.packed_items[0].batch_no) self.assertEqual(return_dn.packed_items[1].serial_no, dn.packed_items[1].serial_no) + @change_settings("Stock Settings", {"automatically_set_serial_nos_based_on_fifo": 1}) + def test_delivery_note_for_repetitive_serial_item(self): + # Step - 1: Create Serial Item + item, warehouse = ( + make_item( + properties={"is_stock_item": 1, "has_serial_no": 1, "serial_no_series": "TEST-SERIAL-.###"} + ).name, + "_Test Warehouse - _TC", + ) + + # Step - 2: Inward Stock + make_stock_entry(item_code=item, target=warehouse, qty=5) + + # Step - 3: Create Delivery Note with repetitive Serial Item + dn = create_delivery_note(item_code=item, warehouse=warehouse, qty=2, do_not_save=True) + dn.append("items", dn.items[0].as_dict()) + dn.items[1].qty = 3 + dn.save() + dn.submit() + + # Test - 1: Serial Nos should be different for each line item + serial_nos = [] + for item in dn.items: + for serial_no in item.serial_no.split("\n"): + self.assertNotIn(serial_no, serial_nos) + serial_nos.append(serial_no) + def tearDown(self): frappe.db.rollback() frappe.db.set_single_value("Selling Settings", "dont_reserve_sales_order_qty_on_sales_return", 0) diff --git a/erpnext/stock/get_item_details.py b/erpnext/stock/get_item_details.py index 13f484b1c851..92c945b254b8 100644 --- a/erpnext/stock/get_item_details.py +++ b/erpnext/stock/get_item_details.py @@ -160,6 +160,7 @@ def update_stock(args, out): and out.warehouse and out.stock_qty > 0 ): + out["ignore_serial_nos"] = args.get("ignore_serial_nos") if out.has_batch_no and not args.get("batch_no"): out.batch_no = get_batch_no(out.item_code, out.warehouse, out.qty) @@ -1140,6 +1141,8 @@ def get_serial_nos_by_fifo(args, sales_order=None): query = query.where(sn.sales_order == sales_order) if args.batch_no: query = query.where(sn.batch_no == args.batch_no) + if args.ignore_serial_nos: + query = query.where(sn.name.notin(args.ignore_serial_nos)) serial_nos = query.run(as_list=True) serial_nos = [s[0] for s in serial_nos] @@ -1450,6 +1453,7 @@ def get_serial_no(args, serial_nos=None, sales_order=None): "item_code": args.get("item_code"), "warehouse": args.get("warehouse"), "stock_qty": args.get("stock_qty"), + "ignore_serial_nos": args.get("ignore_serial_nos"), } ) args = process_args(args) From e23710bf005c83abd84a1bc5cdaefcd17e3c7427 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Tue, 17 Oct 2023 15:03:16 +0530 Subject: [PATCH 490/501] fix(test): project test case (backport #37541) (#37543) fix(test): project test case (cherry picked from commit fd6aee15e6bf3b7ea18487ab1b24b0a77526ac85) Co-authored-by: s-aga-r --- erpnext/projects/doctype/task_depends_on/task_depends_on.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/erpnext/projects/doctype/task_depends_on/task_depends_on.json b/erpnext/projects/doctype/task_depends_on/task_depends_on.json index 5102986f00d0..3300b7eb9057 100644 --- a/erpnext/projects/doctype/task_depends_on/task_depends_on.json +++ b/erpnext/projects/doctype/task_depends_on/task_depends_on.json @@ -24,6 +24,7 @@ }, { "fetch_from": "task.subject", + "fetch_if_empty": 1, "fieldname": "subject", "fieldtype": "Text", "in_list_view": 1, @@ -31,7 +32,6 @@ "read_only": 1 }, { - "fetch_from": "task.project", "fieldname": "project", "fieldtype": "Text", "label": "Project", @@ -40,7 +40,7 @@ ], "istable": 1, "links": [], - "modified": "2023-10-09 11:34:14.335853", + "modified": "2023-10-17 12:45:21.536165", "modified_by": "Administrator", "module": "Projects", "name": "Task Depends On", From bfa93cd3f6ac85b4d203e31cea0414c6765b5378 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Tue, 17 Oct 2023 18:19:47 +0530 Subject: [PATCH 491/501] chore: Add accounting dimensions to Sales Order Item table (cherry picked from commit e31db1891237aa22c19d71929f69d9db5596ae3c) # Conflicts: # erpnext/patches.txt --- .../accounting_dimension.py | 27 +++++++++++++++++++ erpnext/hooks.py | 1 + erpnext/patches.txt | 7 +++++ ...counting_dimensions_in_sales_order_item.py | 7 +++++ .../sales_order_item/sales_order_item.json | 11 ++++++-- 5 files changed, 51 insertions(+), 2 deletions(-) create mode 100644 erpnext/patches/v14_0/create_accounting_dimensions_in_sales_order_item.py diff --git a/erpnext/accounts/doctype/accounting_dimension/accounting_dimension.py b/erpnext/accounts/doctype/accounting_dimension/accounting_dimension.py index cfe5e6e80092..8afd313322ec 100644 --- a/erpnext/accounts/doctype/accounting_dimension/accounting_dimension.py +++ b/erpnext/accounts/doctype/accounting_dimension/accounting_dimension.py @@ -301,3 +301,30 @@ def get_dimensions(with_cost_center_and_project=False): default_dimensions_map[dimension.company][dimension.fieldname] = dimension.default_dimension return dimension_filters, default_dimensions_map + + +def create_accounting_dimensions_for_doctype(doctype): + accounting_dimensions = frappe.db.get_all( + "Accounting Dimension", fields=["fieldname", "label", "document_type", "disabled"] + ) + + if not accounting_dimensions: + return + + 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) diff --git a/erpnext/hooks.py b/erpnext/hooks.py index b2a76f2038ca..6d5dddd9e976 100644 --- a/erpnext/hooks.py +++ b/erpnext/hooks.py @@ -517,6 +517,7 @@ "Sales Invoice Item", "Purchase Invoice Item", "Purchase Order Item", + "Sales Order Item", "Journal Entry Account", "Material Request Item", "Delivery Note Item", diff --git a/erpnext/patches.txt b/erpnext/patches.txt index 37e709673886..ec4c405feee3 100644 --- a/erpnext/patches.txt +++ b/erpnext/patches.txt @@ -341,5 +341,12 @@ execute:frappe.defaults.clear_default("fiscal_year") execute:frappe.db.set_single_value('Selling Settings', 'allow_negative_rates_for_items', 0) erpnext.patches.v14_0.correct_asset_value_if_je_with_workflow erpnext.patches.v14_0.migrate_deferred_accounts_to_item_defaults +<<<<<<< HEAD +======= +erpnext.patches.v14_0.update_invoicing_period_in_subscription +execute:frappe.delete_doc("Page", "welcome-to-erpnext") +erpnext.patches.v15_0.delete_payment_gateway_doctypes +erpnext.patches.v14_0.create_accounting_dimensions_in_sales_order_item +>>>>>>> e31db18912 (chore: Add accounting dimensions to Sales Order Item table) # below migration patch should always run last erpnext.patches.v14_0.migrate_gl_to_payment_ledger diff --git a/erpnext/patches/v14_0/create_accounting_dimensions_in_sales_order_item.py b/erpnext/patches/v14_0/create_accounting_dimensions_in_sales_order_item.py new file mode 100644 index 000000000000..8f77c35b1290 --- /dev/null +++ b/erpnext/patches/v14_0/create_accounting_dimensions_in_sales_order_item.py @@ -0,0 +1,7 @@ +from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import ( + create_accounting_dimensions_for_doctype, +) + + +def execute(): + create_accounting_dimensions_for_doctype(doctype="Sales Order Item") diff --git a/erpnext/selling/doctype/sales_order_item/sales_order_item.json b/erpnext/selling/doctype/sales_order_item/sales_order_item.json index a3e9977244cf..e97141130950 100644 --- a/erpnext/selling/doctype/sales_order_item/sales_order_item.json +++ b/erpnext/selling/doctype/sales_order_item/sales_order_item.json @@ -66,6 +66,7 @@ "total_weight", "column_break_21", "weight_uom", + "accounting_dimensions_section", "warehouse_and_reference", "warehouse", "target_warehouse", @@ -868,12 +869,18 @@ "label": "Production Plan Qty", "no_copy": 1, "read_only": 1 + }, + { + "collapsible": 1, + "fieldname": "accounting_dimensions_section", + "fieldtype": "Section Break", + "label": "Accounting Dimensions" } ], "idx": 1, "istable": 1, "links": [], - "modified": "2023-07-28 14:56:42.031636", + "modified": "2023-10-17 18:18:26.475259", "modified_by": "Administrator", "module": "Selling", "name": "Sales Order Item", @@ -884,4 +891,4 @@ "sort_order": "DESC", "states": [], "track_changes": 1 -} +} \ No newline at end of file From 7db69883649671024dcc81c669d0bcce0676a50a Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Wed, 18 Oct 2023 09:00:49 +0530 Subject: [PATCH 492/501] chore: resolve conflicts --- erpnext/patches.txt | 6 ------ 1 file changed, 6 deletions(-) diff --git a/erpnext/patches.txt b/erpnext/patches.txt index ec4c405feee3..abdd09383bb1 100644 --- a/erpnext/patches.txt +++ b/erpnext/patches.txt @@ -341,12 +341,6 @@ execute:frappe.defaults.clear_default("fiscal_year") execute:frappe.db.set_single_value('Selling Settings', 'allow_negative_rates_for_items', 0) erpnext.patches.v14_0.correct_asset_value_if_je_with_workflow erpnext.patches.v14_0.migrate_deferred_accounts_to_item_defaults -<<<<<<< HEAD -======= -erpnext.patches.v14_0.update_invoicing_period_in_subscription -execute:frappe.delete_doc("Page", "welcome-to-erpnext") -erpnext.patches.v15_0.delete_payment_gateway_doctypes erpnext.patches.v14_0.create_accounting_dimensions_in_sales_order_item ->>>>>>> e31db18912 (chore: Add accounting dimensions to Sales Order Item table) # below migration patch should always run last erpnext.patches.v14_0.migrate_gl_to_payment_ledger From 3499089323908f9b8748c72d43bf4c57de47ab0d Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Tue, 17 Oct 2023 16:50:20 +0530 Subject: [PATCH 493/501] refactor: use account in key while grouping voucher in ar/ap report (cherry picked from commit 601ab4567ea7062b78448235fc2fc62b15dce6a6) --- .../report/accounts_receivable/accounts_receivable.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/erpnext/accounts/report/accounts_receivable/accounts_receivable.py b/erpnext/accounts/report/accounts_receivable/accounts_receivable.py index e3b671f39730..b9c7a0bfb877 100755 --- a/erpnext/accounts/report/accounts_receivable/accounts_receivable.py +++ b/erpnext/accounts/report/accounts_receivable/accounts_receivable.py @@ -116,7 +116,7 @@ def init_voucher_balance(self): # build all keys, since we want to exclude vouchers beyond the report date for ple in self.ple_entries: # get the balance object for voucher_type - key = (ple.voucher_type, ple.voucher_no, ple.party) + key = (ple.account, ple.voucher_type, ple.voucher_no, ple.party) if not key in self.voucher_balance: self.voucher_balance[key] = frappe._dict( voucher_type=ple.voucher_type, @@ -183,7 +183,7 @@ def get_voucher_balance(self, ple): ): return - key = (ple.against_voucher_type, ple.against_voucher_no, ple.party) + key = (ple.account, ple.against_voucher_type, ple.against_voucher_no, ple.party) # If payment is made against credit note # and credit note is made against a Sales Invoice @@ -192,13 +192,13 @@ def get_voucher_balance(self, ple): if ple.against_voucher_no in self.return_entries: return_against = self.return_entries.get(ple.against_voucher_no) if return_against: - key = (ple.against_voucher_type, return_against, ple.party) + key = (ple.account, ple.against_voucher_type, return_against, ple.party) row = self.voucher_balance.get(key) if not row: # no invoice, this is an invoice / stand-alone payment / credit note - row = self.voucher_balance.get((ple.voucher_type, ple.voucher_no, ple.party)) + row = self.voucher_balance.get((ple.account, ple.voucher_type, ple.voucher_no, ple.party)) row.party_type = ple.party_type return row From 760eab961ddbf3568450ed5a3847de3582f08f9d Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Tue, 17 Oct 2023 20:37:48 +0530 Subject: [PATCH 494/501] test: report output if party is missing (cherry picked from commit 244cec64b2641c50bd6102e6dba65a481d24da0d) --- .../test_accounts_receivable.py | 115 ++++++++++++++---- 1 file changed, 92 insertions(+), 23 deletions(-) diff --git a/erpnext/accounts/report/accounts_receivable/test_accounts_receivable.py b/erpnext/accounts/report/accounts_receivable/test_accounts_receivable.py index 4307689158f8..cbeb6d3106d4 100644 --- a/erpnext/accounts/report/accounts_receivable/test_accounts_receivable.py +++ b/erpnext/accounts/report/accounts_receivable/test_accounts_receivable.py @@ -1,6 +1,7 @@ import unittest import frappe +from frappe import qb from frappe.tests.utils import FrappeTestCase, change_settings from frappe.utils import add_days, flt, getdate, today @@ -23,29 +24,6 @@ def setUp(self): 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 create_sales_invoice(self, no_payment_schedule=False, do_not_submit=False): frappe.set_user("Administrator") si = create_sales_invoice( @@ -643,3 +621,94 @@ def test_multi_select_party_filter(self): self.assertEqual(len(report[1]), 2) output_for = set([x.party for x in report[1]]) self.assertEqual(output_for, expected_output) + + def test_report_output_if_party_is_missing(self): + acc_name = "Additional Debtors" + if not frappe.db.get_value( + "Account", filters={"account_name": acc_name, "company": self.company} + ): + additional_receivable_acc = frappe.get_doc( + { + "doctype": "Account", + "account_name": acc_name, + "parent_account": "Accounts Receivable - " + self.company_abbr, + "company": self.company, + "account_type": "Receivable", + } + ).save() + self.debtors2 = additional_receivable_acc.name + + je = frappe.new_doc("Journal Entry") + je.company = self.company + je.posting_date = today() + je.append( + "accounts", + { + "account": self.debit_to, + "party_type": "Customer", + "party": self.customer, + "debit_in_account_currency": 150, + "credit_in_account_currency": 0, + "cost_center": self.cost_center, + }, + ) + je.append( + "accounts", + { + "account": self.debtors2, + "party_type": "Customer", + "party": self.customer, + "debit_in_account_currency": 200, + "credit_in_account_currency": 0, + "cost_center": self.cost_center, + }, + ) + je.append( + "accounts", + { + "account": self.cash, + "debit_in_account_currency": 0, + "credit_in_account_currency": 350, + "cost_center": self.cost_center, + }, + ) + je.save().submit() + + # manually remove party from Payment Ledger + ple = qb.DocType("Payment Ledger Entry") + qb.update(ple).set(ple.party, None).where(ple.voucher_no == je.name).run() + + filters = { + "company": self.company, + "report_date": today(), + "range1": 30, + "range2": 60, + "range3": 90, + "range4": 120, + } + + report_ouput = execute(filters)[1] + expected_data = [ + [self.debtors2, je.doctype, je.name, "Customer", self.customer, 200.0, 0.0, 0.0, 200.0], + [self.debit_to, je.doctype, je.name, "Customer", self.customer, 150.0, 0.0, 0.0, 150.0], + ] + self.assertEqual(len(report_ouput), 2) + # fetch only required fields + report_output = [ + [ + x.party_account, + x.voucher_type, + x.voucher_no, + "Customer", + self.customer, + x.invoiced, + x.paid, + x.credit_note, + x.outstanding, + ] + for x in report_ouput + ] + # use account name to sort + # post sorting output should be [[Additional Debtors, ...], [Debtors, ...]] + report_output = sorted(report_output, key=lambda x: x[0]) + self.assertEqual(expected_data, report_output) From ac7d6d6d59c96a2e05ba8f68ce47b3576ba4aa48 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Wed, 18 Oct 2023 06:00:29 +0000 Subject: [PATCH 495/501] fix: billed_qty to show a sum of all invoiced qty from the purchase order item. (backport #37539) (#37558) fix: billed_qty to show a sum of all invoiced qty from the purchase order item. (cherry picked from commit 8a72f4f58aee90e7155105b7275113555e788401) Co-authored-by: HarryPaulo --- .../report/purchase_order_analysis/purchase_order_analysis.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/erpnext/buying/report/purchase_order_analysis/purchase_order_analysis.py b/erpnext/buying/report/purchase_order_analysis/purchase_order_analysis.py index e10c0e2fccf8..b6e46302ffe8 100644 --- a/erpnext/buying/report/purchase_order_analysis/purchase_order_analysis.py +++ b/erpnext/buying/report/purchase_order_analysis/purchase_order_analysis.py @@ -6,7 +6,7 @@ import frappe from frappe import _ -from frappe.query_builder.functions import IfNull +from frappe.query_builder.functions import IfNull, Sum from frappe.utils import date_diff, flt, getdate @@ -57,7 +57,7 @@ def get_data(filters): po_item.qty, po_item.received_qty, (po_item.qty - po_item.received_qty).as_("pending_qty"), - IfNull(pi_item.qty, 0).as_("billed_qty"), + Sum(IfNull(pi_item.qty, 0)).as_("billed_qty"), po_item.base_amount.as_("amount"), (po_item.received_qty * po_item.base_rate).as_("received_qty_amount"), (po_item.billed_amt * IfNull(po.conversion_rate, 1)).as_("billed_amount"), From e8d082560add16592121232bb1814cba4f2ce0fb Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Fri, 15 Sep 2023 16:19:27 +0530 Subject: [PATCH 496/501] refactor: move `unreconcile` btn inside a drop down (cherry picked from commit f2b0ac6868387611d9d6d00274fea6655aceb9c0) --- erpnext/public/js/utils/unreconcile.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/public/js/utils/unreconcile.js b/erpnext/public/js/utils/unreconcile.js index acc77a64b017..bbdd51d6e54a 100644 --- a/erpnext/public/js/utils/unreconcile.js +++ b/erpnext/public/js/utils/unreconcile.js @@ -19,7 +19,7 @@ erpnext.accounts.unreconcile_payments = { if (r.message) { frm.add_custom_button(__("Un-Reconcile"), function() { erpnext.accounts.unreconcile_payments.build_unreconcile_dialog(frm); - }); + }, __('Actions')); } } }); From 54f672e1444202595f17c4783b3138eb156ffde1 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Fri, 15 Sep 2023 16:21:50 +0530 Subject: [PATCH 497/501] refactor: add `unreconcile` btn to purchase invoice (cherry picked from commit 94ce43b0d5d131e31e6fc01ac06cfa748d73caed) --- erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js | 1 + 1 file changed, 1 insertion(+) diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js index ee5a50af0582..04b00e09a44c 100644 --- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js +++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js @@ -181,6 +181,7 @@ erpnext.accounts.PurchaseInvoice = class PurchaseInvoice extends erpnext.buying. } this.frm.set_df_property("tax_withholding_category", "hidden", doc.apply_tds ? 0 : 1); + erpnext.accounts.unreconcile_payments.add_unreconcile_btn(me.frm); } unblock_invoice() { From 022f85dd085c530955207e77d533f6d136f28156 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Wed, 18 Oct 2023 17:21:46 +0530 Subject: [PATCH 498/501] fix: e-commerce permissions for address (#37554) * fix: E-commerce permissions (cherry picked from commit f4d74990fe1cc2abda56359ce8d09644526c62a6) # Conflicts: # erpnext/controllers/selling_controller.py * chore: conflicts --------- Co-authored-by: Ankush Menat --- erpnext/accounts/party.py | 32 ++++++++++++++++------- erpnext/controllers/selling_controller.py | 6 +++-- 2 files changed, 27 insertions(+), 11 deletions(-) diff --git a/erpnext/accounts/party.py b/erpnext/accounts/party.py index 0c51e727c920..66b56684fae3 100644 --- a/erpnext/accounts/party.py +++ b/erpnext/accounts/party.py @@ -6,11 +6,7 @@ import frappe from frappe import _, msgprint, scrub -from frappe.contacts.doctype.address.address import ( - get_address_display, - get_company_address, - get_default_address, -) +from frappe.contacts.doctype.address.address import get_company_address, get_default_address from frappe.contacts.doctype.contact.contact import get_contact_details from frappe.core.doctype.user_permission.user_permission import get_permitted_documents from frappe.model.utils import get_fetch_values @@ -133,6 +129,7 @@ def _get_party_details( party_address, company_address, shipping_address, + ignore_permissions=ignore_permissions, ) set_contact_details(party_details, party, party_type) set_other_values(party_details, party, party_type) @@ -193,6 +190,8 @@ def set_address_details( party_address=None, company_address=None, shipping_address=None, + *, + ignore_permissions=False ): billing_address_field = ( "customer_address" if party_type == "Lead" else party_type.lower() + "_address" @@ -205,13 +204,17 @@ def set_address_details( get_fetch_values(doctype, billing_address_field, party_details[billing_address_field]) ) # address display - party_details.address_display = get_address_display(party_details[billing_address_field]) + party_details.address_display = render_address( + party_details[billing_address_field], check_permissions=not ignore_permissions + ) # shipping address if party_type in ["Customer", "Lead"]: party_details.shipping_address_name = shipping_address or get_party_shipping_address( party_type, party.name ) - party_details.shipping_address = get_address_display(party_details["shipping_address_name"]) + party_details.shipping_address = render_address( + party_details["shipping_address_name"], check_permissions=not ignore_permissions + ) if doctype: party_details.update( get_fetch_values(doctype, "shipping_address_name", party_details.shipping_address_name) @@ -229,7 +232,7 @@ def set_address_details( if shipping_address: party_details.update( shipping_address=shipping_address, - shipping_address_display=get_address_display(shipping_address), + shipping_address_display=render_address(shipping_address), **get_fetch_values(doctype, "shipping_address", shipping_address) ) @@ -238,7 +241,8 @@ def set_address_details( party_details.update( billing_address=party_details.company_address, billing_address_display=( - party_details.company_address_display or get_address_display(party_details.company_address) + party_details.company_address_display + or render_address(party_details.company_address, check_permissions=False) ), **get_fetch_values(doctype, "billing_address", party_details.company_address) ) @@ -957,3 +961,13 @@ def add_party_account(party_type, party, company, account): doc.append("accounts", accounts) doc.save() + + +def render_address(address, check_permissions=True): + try: + from frappe.contacts.doctype.address.address import render_address as _render + except ImportError: + # Older frappe versions where this function is not available + from frappe.contacts.doctype.address.address import get_address_display as _render + + return frappe.call(_render, address, check_permissions=check_permissions) diff --git a/erpnext/controllers/selling_controller.py b/erpnext/controllers/selling_controller.py index 5daaba8e8925..f005a7f7a391 100644 --- a/erpnext/controllers/selling_controller.py +++ b/erpnext/controllers/selling_controller.py @@ -4,9 +4,9 @@ import frappe from frappe import _, bold, throw -from frappe.contacts.doctype.address.address import get_address_display from frappe.utils import cint, cstr, flt, get_link_to_form, nowtime +from erpnext.accounts.party import render_address from erpnext.controllers.accounts_controller import get_taxes_and_charges from erpnext.controllers.sales_and_purchase_return import get_rate_for_return from erpnext.controllers.stock_controller import StockController @@ -591,7 +591,9 @@ def set_customer_address(self): for address_field, address_display_field in address_dict.items(): if self.get(address_field): - self.set(address_display_field, get_address_display(self.get(address_field))) + self.set( + address_display_field, render_address(self.get(address_field), check_permissions=False) + ) def validate_for_duplicate_items(self): check_list, chk_dupl_itm = [], [] From 95abd7908f7548e14401390f3c74fc8679eaf8c2 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Thu, 19 Oct 2023 13:38:24 +0530 Subject: [PATCH 499/501] fix: payment entry count on supplier dashboard (backport #37571) (#37575) fix: payment entry count on supplier dashboard (#37571) (cherry picked from commit 10311ff114b7513b47df9be993fa568d6e586f1d) Co-authored-by: rohitwaghchaure --- erpnext/buying/doctype/supplier/supplier_dashboard.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/buying/doctype/supplier/supplier_dashboard.py b/erpnext/buying/doctype/supplier/supplier_dashboard.py index 11bb06e0caa0..3bd306e65919 100644 --- a/erpnext/buying/doctype/supplier/supplier_dashboard.py +++ b/erpnext/buying/doctype/supplier/supplier_dashboard.py @@ -8,7 +8,7 @@ def get_data(): "This is based on transactions against this Supplier. See timeline below for details" ), "fieldname": "supplier", - "non_standard_fieldnames": {"Payment Entry": "party_name", "Bank Account": "party"}, + "non_standard_fieldnames": {"Payment Entry": "party", "Bank Account": "party"}, "transactions": [ {"label": _("Procurement"), "items": ["Request for Quotation", "Supplier Quotation"]}, {"label": _("Orders"), "items": ["Purchase Order", "Purchase Receipt", "Purchase Invoice"]}, From e1504efd406398f5688f705c863fc1e9bc49dae5 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Thu, 19 Oct 2023 13:50:55 +0530 Subject: [PATCH 500/501] fix: Issues related to RFQ and Supplier Quotation on Portal (backport #37565) (#37577) * fix: Issues related to RFQ and Supplier Quotation on Portal (#37565) fix: RFQ and Supplier Quotation for Portal (cherry picked from commit 2851a41310a050afee753c8ac396eb808d0d123c) * chore: removed backport changes --------- Co-authored-by: rohitwaghchaure --- erpnext/accounts/party.py | 30 +++++++++++++++++-- .../includes/order/order_macros.html | 2 +- erpnext/templates/includes/rfq.js | 4 +-- .../templates/includes/rfq/rfq_macros.html | 24 +++++++++------ erpnext/templates/pages/order.html | 4 +-- erpnext/templates/pages/rfq.html | 4 +-- 6 files changed, 50 insertions(+), 18 deletions(-) diff --git a/erpnext/accounts/party.py b/erpnext/accounts/party.py index 66b56684fae3..150b56742e43 100644 --- a/erpnext/accounts/party.py +++ b/erpnext/accounts/party.py @@ -7,7 +7,6 @@ import frappe from frappe import _, msgprint, scrub from frappe.contacts.doctype.address.address import get_company_address, get_default_address -from frappe.contacts.doctype.contact.contact import get_contact_details from frappe.core.doctype.user_permission.user_permission import get_permitted_documents from frappe.model.utils import get_fetch_values from frappe.query_builder.functions import Abs, Date, Sum @@ -294,7 +293,34 @@ def set_contact_details(party_details, party, party_type): } ) else: - party_details.update(get_contact_details(party_details.contact_person)) + fields = [ + "name as contact_person", + "salutation", + "first_name", + "last_name", + "email_id as contact_email", + "mobile_no as contact_mobile", + "phone as contact_phone", + "designation as contact_designation", + "department as contact_department", + ] + + contact_details = frappe.db.get_value( + "Contact", party_details.contact_person, fields, as_dict=True + ) + + contact_details.contact_display = " ".join( + filter( + None, + [ + contact_details.get("salutation"), + contact_details.get("first_name"), + contact_details.get("last_name"), + ], + ) + ) + + party_details.update(contact_details) def set_other_values(party_details, party, party_type): diff --git a/erpnext/templates/includes/order/order_macros.html b/erpnext/templates/includes/order/order_macros.html index d95b28961c6b..8799a3b1eab6 100644 --- a/erpnext/templates/includes/order/order_macros.html +++ b/erpnext/templates/includes/order/order_macros.html @@ -7,7 +7,7 @@ {% if d.thumbnail or d.image %} {{ product_image(d.thumbnail or d.image, no_border=True) }} {% else %} -
+
{{ frappe.utils.get_abbr(d.item_name) or "NA" }}
{% endif %} diff --git a/erpnext/templates/includes/rfq.js b/erpnext/templates/includes/rfq.js index 37beb5a584ba..e78776fd29f5 100644 --- a/erpnext/templates/includes/rfq.js +++ b/erpnext/templates/includes/rfq.js @@ -72,7 +72,7 @@ rfq = class rfq { } submit_rfq(){ - $('.btn-sm').click(function(){ + $('.btn-sm').click(function() { frappe.freeze(); frappe.call({ type: "POST", @@ -81,7 +81,7 @@ rfq = class rfq { doc: doc }, btn: this, - callback: function(r){ + callback: function(r) { frappe.unfreeze(); if(r.message){ $('.btn-sm').hide() diff --git a/erpnext/templates/includes/rfq/rfq_macros.html b/erpnext/templates/includes/rfq/rfq_macros.html index 88724c30de60..78ec6ff5f8b3 100644 --- a/erpnext/templates/includes/rfq/rfq_macros.html +++ b/erpnext/templates/includes/rfq/rfq_macros.html @@ -1,19 +1,25 @@ {% from "erpnext/templates/includes/macros.html" import product_image_square, product_image %} {% macro item_name_and_description(d, doc) %} -
-
- {{ product_image(d.image) }} -
-
- {{ d.item_code }} -

{{ d.description }}

+
+
+ {% if d.image %} + {{ product_image(d.image) }} + {% else %} +
+ {{ frappe.utils.get_abbr(d.item_name)}} +
+ {% endif %} +
+
+ {{ d.item_code }} +

{{ d.description }}

{% set supplier_part_no = frappe.db.get_value("Item Supplier", {'parent': d.item_code, 'supplier': doc.supplier}, "supplier_part_no") %}

{% if supplier_part_no %} {{_("Supplier Part No") + ": "+ supplier_part_no}} {% endif %}

-
-
+
+
{% endmacro %} diff --git a/erpnext/templates/pages/order.html b/erpnext/templates/pages/order.html index bc34ad5ac500..d9cb75ac5ac2 100644 --- a/erpnext/templates/pages/order.html +++ b/erpnext/templates/pages/order.html @@ -165,7 +165,6 @@

{{ doc.name }}

{% endif %} - {% if attachments %}
@@ -193,6 +192,7 @@

{{ doc.name }}

{% endif %} {% endblock %} + {% block script %} -{% endblock %} \ No newline at end of file +{% endblock %} diff --git a/erpnext/templates/pages/rfq.html b/erpnext/templates/pages/rfq.html index 6516482c230e..d371bf2161de 100644 --- a/erpnext/templates/pages/rfq.html +++ b/erpnext/templates/pages/rfq.html @@ -1,7 +1,7 @@ {% extends "templates/web.html" %} {% block header %} -

{{ doc.name }}

+

{{ doc.name }}

{% endblock %} {% block script %} @@ -16,7 +16,7 @@

{{ doc.name }}

{% if doc.items %} + {{ _("Make Quotation") }} {% endif %} {% endblock %} From 6931db98f19b039d5cd53002c4070dce75dc5ca9 Mon Sep 17 00:00:00 2001 From: Frappe PR Bot Date: Thu, 19 Oct 2023 11:36:28 +0000 Subject: [PATCH 501/501] chore(release): Bumped to Version 14.44.1 ## [14.44.1](https://github.com/frappe/erpnext/compare/v14.44.0...v14.44.1) (2023-10-19) ### Bug Fixes * billed_qty to show a sum of all invoiced qty from the purchase order item. (backport [#37539](https://github.com/frappe/erpnext/issues/37539)) ([#37558](https://github.com/frappe/erpnext/issues/37558)) ([ac7d6d6](https://github.com/frappe/erpnext/commit/ac7d6d6d59c96a2e05ba8f68ce47b3576ba4aa48)) * consider received qty while creating SO -> MR (backport [#37414](https://github.com/frappe/erpnext/issues/37414)) ([#37514](https://github.com/frappe/erpnext/issues/37514)) ([1b94510](https://github.com/frappe/erpnext/commit/1b94510f08cb50dd786eddc6a20f1ef5f321beb0)) * don't set finance books if gross_purchase_amount is not set (backport [#37480](https://github.com/frappe/erpnext/issues/37480)) ([#37482](https://github.com/frappe/erpnext/issues/37482)) ([0590f21](https://github.com/frappe/erpnext/commit/0590f21814c189df07d87bc08a33c9d4fed4b95e)) * e-commerce permissions for address ([#37554](https://github.com/frappe/erpnext/issues/37554)) ([022f85d](https://github.com/frappe/erpnext/commit/022f85dd085c530955207e77d533f6d136f28156)) * german tranlations of "Is Return" ([f9b2355](https://github.com/frappe/erpnext/commit/f9b2355066b04489b7f4a69270eeac5032d5c32c)) * GL Entries not getting created for PR Return (backport [#37513](https://github.com/frappe/erpnext/issues/37513)) ([#37516](https://github.com/frappe/erpnext/issues/37516)) ([c32258e](https://github.com/frappe/erpnext/commit/c32258e4b690758b4a2e667edd85caa99083ab62)) * **gp:** wrong `allocated_amount` on multi sales person invoice ([d266423](https://github.com/frappe/erpnext/commit/d266423011ff4c16f3e12231df34ffeeed56cf62)) * Incorrect vat amount in KSA VAT report ([44f7de0](https://github.com/frappe/erpnext/commit/44f7de0f31f4464f2823038356bb52f479fa3d5b)) * inflated total amt in TDS report using back calculation ([78e22af](https://github.com/frappe/erpnext/commit/78e22af3caf6b07be1b80a03f3cd78ee874e1925)) * Issues related to RFQ and Supplier Quotation on Portal (backport [#37565](https://github.com/frappe/erpnext/issues/37565)) ([#37577](https://github.com/frappe/erpnext/issues/37577)) ([e1504ef](https://github.com/frappe/erpnext/commit/e1504efd406398f5688f705c863fc1e9bc49dae5)) * keep customer/supplier website role by default ([76ef61c](https://github.com/frappe/erpnext/commit/76ef61c24fab6190790034da0e5e8ea1d3d2e242)) * keyerror on gl and pl comparision report ([6f143d3](https://github.com/frappe/erpnext/commit/6f143d35aab7258832acf8ded0ab42d339e33dc0)) * payment entry count on supplier dashboard (backport [#37571](https://github.com/frappe/erpnext/issues/37571)) ([#37575](https://github.com/frappe/erpnext/issues/37575)) ([95abd79](https://github.com/frappe/erpnext/commit/95abd7908f7548e14401390f3c74fc8679eaf8c2)) * same Serial No get mapped while creating SO -> DN ([#37527](https://github.com/frappe/erpnext/issues/37527)) ([5025850](https://github.com/frappe/erpnext/commit/50258502585d30791bd6716b4d2e2935721047f9)) * serial and batch no get removed on save of return DN ([#37476](https://github.com/frappe/erpnext/issues/37476)) ([f1814a1](https://github.com/frappe/erpnext/commit/f1814a1a2a1c82c83c0f04fcc27b3b92526dd906)) * Stock Reconciliation Insufficient Stock Error ([#37494](https://github.com/frappe/erpnext/issues/37494)) ([9406ddb](https://github.com/frappe/erpnext/commit/9406ddbff08d895a01aa2b9cd132879bea3a0fbc)) * **test:** project test case (backport [#37541](https://github.com/frappe/erpnext/issues/37541)) ([#37543](https://github.com/frappe/erpnext/issues/37543)) ([e23710b](https://github.com/frappe/erpnext/commit/e23710bf005c83abd84a1bc5cdaefcd17e3c7427)) * use `flt` to ignore TypeError ([#37481](https://github.com/frappe/erpnext/issues/37481)) ([d2b22db](https://github.com/frappe/erpnext/commit/d2b22db5001ae6544e872234c6c3434f24c5a6b1)) ### Performance Improvements * index `dn_detail` in `Delivery Note Item` (backport [#37528](https://github.com/frappe/erpnext/issues/37528)) ([#37530](https://github.com/frappe/erpnext/issues/37530)) ([001c230](https://github.com/frappe/erpnext/commit/001c230688b6cd757c7f0bd39afc2c1543c21080)) --- erpnext/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/__init__.py b/erpnext/__init__.py index 26f7360563a4..31f1fa066e42 100644 --- a/erpnext/__init__.py +++ b/erpnext/__init__.py @@ -3,7 +3,7 @@ import frappe -__version__ = "14.44.0" +__version__ = "14.44.1" def get_default_company(user=None):