From e68b08817e0577a640c0cf19707b02b9b69036de Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Thu, 8 Jun 2023 17:08:22 +0530 Subject: [PATCH 01/26] fix: don't allow to make reposting entry for closing stock balance period (cherry picked from commit 96c5c7b1dfb953e665c2683ac68ad67b55474bcb) --- .../repost_item_valuation.py | 41 ++++++++++++++++++- .../test_repost_item_valuation.py | 30 ++++++++++++++ 2 files changed, 70 insertions(+), 1 deletion(-) diff --git a/erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.py b/erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.py index d3bcab76ab5..5e61f0f196a 100644 --- a/erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.py +++ b/erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.py @@ -12,6 +12,7 @@ from frappe.utils.user import get_users_with_role from rq.timeouts import JobTimeoutException import erpnext +from erpnext.accounts.general_ledger import validate_accounting_period from erpnext.accounts.utils import get_future_stock_vouchers, repost_gle_for_stock_vouchers from erpnext.stock.stock_ledger import ( get_affected_transactions, @@ -43,11 +44,49 @@ class RepostItemValuation(Document): self.validate_accounts_freeze() def validate_period_closing_voucher(self): + # Period Closing Voucher year_end_date = self.get_max_year_end_date(self.company) if year_end_date and getdate(self.posting_date) <= getdate(year_end_date): - msg = f"Due to period closing, you cannot repost item valuation before {year_end_date}" + date = frappe.format(year_end_date, "Date") + msg = f"Due to period closing, you cannot repost item valuation before {date}" frappe.throw(_(msg)) + # Accounting Period + if self.voucher_type: + validate_accounting_period( + [ + frappe._dict( + { + "posting_date": self.posting_date, + "company": self.company, + "voucher_type": self.voucher_type, + } + ) + ] + ) + + # Closing Stock Balance + closing_stock = self.get_closing_stock_balance() + if closing_stock and closing_stock[0].name: + name = get_link_to_form("Closing Stock Balance", closing_stock[0].name) + to_date = frappe.format(closing_stock[0].to_date, "Date") + msg = f"Due to closing stock balance {name}, you cannot repost item valuation before {to_date}" + frappe.throw(_(msg)) + + def get_closing_stock_balance(self): + filters = { + "company": self.company, + "status": "Completed", + "docstatus": 1, + "to_date": (">=", self.posting_date), + } + + for field in ["warehouse", "item_code"]: + if self.get(field): + filters.update({field: ("in", ["", self.get(field)])}) + + return frappe.get_all("Closing Stock Balance", fields=["name", "to_date"], filters=filters) + @staticmethod def get_max_year_end_date(company): data = frappe.get_all( diff --git a/erpnext/stock/doctype/repost_item_valuation/test_repost_item_valuation.py b/erpnext/stock/doctype/repost_item_valuation/test_repost_item_valuation.py index 9c4d997b316..1853f45f583 100644 --- a/erpnext/stock/doctype/repost_item_valuation/test_repost_item_valuation.py +++ b/erpnext/stock/doctype/repost_item_valuation/test_repost_item_valuation.py @@ -392,3 +392,33 @@ class TestRepostItemValuation(FrappeTestCase, StockTestMixin): pr.cancel() self.assertTrue(pr.docstatus == 2) self.assertTrue(frappe.db.exists("Repost Item Valuation", {"voucher_no": pr.name})) + + def test_repost_item_valuation_for_closing_stock_balance(self): + from erpnext.stock.doctype.closing_stock_balance.closing_stock_balance import ( + prepare_closing_stock_balance, + ) + + doc = frappe.new_doc("Closing Stock Balance") + doc.company = "_Test Company" + doc.from_date = today() + doc.to_date = today() + doc.submit() + + prepare_closing_stock_balance(doc.name) + doc.load_from_db() + self.assertEqual(doc.docstatus, 1) + self.assertEqual(doc.status, "Completed") + + riv = frappe.new_doc("Repost Item Valuation") + riv.update( + { + "item_code": "_Test Item", + "warehouse": "_Test Warehouse - _TC", + "based_on": "Item and Warehouse", + "posting_date": today(), + "posting_time": "00:01:00", + } + ) + + self.assertRaises(frappe.ValidationError, riv.save) + doc.cancel() From 20f2bef55f13f862a17dbc3ecfef5743661ec946 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Wed, 21 Jun 2023 15:37:21 +0530 Subject: [PATCH 02/26] fix: issue of asset value_after_depreciation field getting updated twice if workflow is enabled in Journal Entry (backport #35821) (#35827) Fixes issue of asset value_after_depreciation field getting updated twice if workflow is enabled in Journal Entry (#35821) * Fixes issue of asset value_after_depreciation field getting updated twice if workflow is enabled in Journal Entry * chore: remove unnecessary line break * chore: formatting --------- Co-authored-by: Anand Baburajan (cherry picked from commit 000ebe447991185e4d5a4050b066fa0781575e65) Co-authored-by: saeedkola --- erpnext/assets/doctype/asset/depreciation.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/erpnext/assets/doctype/asset/depreciation.py b/erpnext/assets/doctype/asset/depreciation.py index 543c75a195f..a650db208ed 100644 --- a/erpnext/assets/doctype/asset/depreciation.py +++ b/erpnext/assets/doctype/asset/depreciation.py @@ -137,15 +137,15 @@ def make_depreciation_entry(asset_name, date=None): je.flags.ignore_permissions = True je.flags.planned_depr_entry = True je.save() - if not je.meta.get_workflow(): - je.submit() d.db_set("journal_entry", je.name) - 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() + 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() asset.db_set("depr_entry_posting_status", "Successful") From 200ddbf66c1e82016c3356c2012f209730e407fb Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Wed, 21 Jun 2023 14:19:02 +0530 Subject: [PATCH 03/26] fix: no permission for accounts settings on payment reconciliation (cherry picked from commit ad758b8d8511853934b7cfcfde42eddeff9a333c) --- .../payment_reconciliation.js | 40 ++++++++++--------- .../payment_reconciliation.py | 4 ++ 2 files changed, 26 insertions(+), 18 deletions(-) diff --git a/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.js b/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.js index 08d38dde474..07f35c9fe11 100644 --- a/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.js +++ b/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.js @@ -85,25 +85,29 @@ erpnext.accounts.PaymentReconciliationController = class PaymentReconciliationCo // check for any running reconciliation jobs if (this.frm.doc.receivable_payable_account) { - frappe.db.get_single_value("Accounts Settings", "auto_reconcile_payments").then((enabled) => { - if(enabled) { - this.frm.call({ - 'method': "erpnext.accounts.doctype.process_payment_reconciliation.process_payment_reconciliation.is_any_doc_running", - "args": { - for_filter: { - company: this.frm.doc.company, - party_type: this.frm.doc.party_type, - party: this.frm.doc.party, - receivable_payable_account: this.frm.doc.receivable_payable_account + this.frm.call({ + doc: this.frm.doc, + method: 'is_auto_process_enabled', + callback: (r) => { + if (r.message) { + this.frm.call({ + 'method': "erpnext.accounts.doctype.process_payment_reconciliation.process_payment_reconciliation.is_any_doc_running", + "args": { + for_filter: { + company: this.frm.doc.company, + party_type: this.frm.doc.party_type, + party: this.frm.doc.party, + receivable_payable_account: this.frm.doc.receivable_payable_account + } } - } - }).then(r => { - if (r.message) { - let doc_link = frappe.utils.get_form_link("Process Payment Reconciliation", r.message, true); - let msg = __("Payment Reconciliation Job: {0} is running for this party. Can't reconcile now.", [doc_link]); - this.frm.dashboard.add_comment(msg, "yellow"); - } - }); + }).then(r => { + if (r.message) { + let doc_link = frappe.utils.get_form_link("Process Payment Reconciliation", r.message, true); + let msg = __("Payment Reconciliation Job: {0} is running for this party. Can't reconcile now.", [doc_link]); + this.frm.dashboard.add_comment(msg, "yellow"); + } + }); + } } }); } diff --git a/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py b/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py index 2c8faecf4b1..2e4e3b0e078 100644 --- a/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py +++ b/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py @@ -252,6 +252,10 @@ class PaymentReconciliation(Document): return difference_amount + @frappe.whitelist() + def is_auto_process_enabled(self): + return frappe.db.get_single_value("Accounts Settings", "auto_reconcile_payments") + @frappe.whitelist() def calculate_difference_on_allocation_change(self, payment_entry, invoice, allocated_amount): invoice_exchange_map = self.get_invoice_exchange_map(invoice, payment_entry) From cacb0f6fdedd292102840e1cffaa7af0364b0fbf Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Wed, 21 Jun 2023 21:52:44 +0530 Subject: [PATCH 04/26] fix: incorrect cost center error in bank reconciliation (cherry picked from commit 41b9e928680a5dca42e10c383d0b9301482540fb) --- .../bank_reconciliation_tool/bank_reconciliation_tool.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/erpnext/accounts/doctype/bank_reconciliation_tool/bank_reconciliation_tool.py b/erpnext/accounts/doctype/bank_reconciliation_tool/bank_reconciliation_tool.py index c4a23a640c3..0eef3e9a67b 100644 --- a/erpnext/accounts/doctype/bank_reconciliation_tool/bank_reconciliation_tool.py +++ b/erpnext/accounts/doctype/bank_reconciliation_tool/bank_reconciliation_tool.py @@ -10,6 +10,7 @@ from frappe.model.document import Document from frappe.query_builder.custom import ConstantColumn from frappe.utils import cint, flt +from erpnext import get_default_cost_center from erpnext.accounts.doctype.bank_transaction.bank_transaction import get_total_allocated_amount from erpnext.accounts.report.bank_reconciliation_statement.bank_reconciliation_statement import ( get_amounts_not_reflected_in_system, @@ -140,6 +141,9 @@ def create_journal_entry_bts( second_account ) ) + + company = frappe.get_value("Account", company_account, "company") + accounts = [] # Multi Currency? accounts.append( @@ -149,6 +153,7 @@ def create_journal_entry_bts( "debit_in_account_currency": bank_transaction.withdrawal, "party_type": party_type, "party": party, + "cost_center": get_default_cost_center(company), } ) @@ -158,11 +163,10 @@ def create_journal_entry_bts( "bank_account": bank_transaction.bank_account, "credit_in_account_currency": bank_transaction.withdrawal, "debit_in_account_currency": bank_transaction.deposit, + "cost_center": get_default_cost_center(company), } ) - company = frappe.get_value("Account", company_account, "company") - journal_entry_dict = { "voucher_type": entry_type, "company": company, From e44783a3c5e0e798810c4297a1861a612e6981d1 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Thu, 22 Jun 2023 12:40:02 +0530 Subject: [PATCH 05/26] refactor: increase precision for current exc rate in ERR (cherry picked from commit b4db25dd188134dba30e22977c0fa4665ba749be) --- .../exchange_rate_revaluation_account.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/erpnext/accounts/doctype/exchange_rate_revaluation_account/exchange_rate_revaluation_account.json b/erpnext/accounts/doctype/exchange_rate_revaluation_account/exchange_rate_revaluation_account.json index 0a7d0579b15..fd2d931315c 100644 --- a/erpnext/accounts/doctype/exchange_rate_revaluation_account/exchange_rate_revaluation_account.json +++ b/erpnext/accounts/doctype/exchange_rate_revaluation_account/exchange_rate_revaluation_account.json @@ -73,6 +73,7 @@ "fieldname": "current_exchange_rate", "fieldtype": "Float", "label": "Current Exchange Rate", + "precision": "9", "read_only": 1 }, { @@ -148,7 +149,7 @@ ], "istable": 1, "links": [], - "modified": "2023-06-20 07:21:40.743460", + "modified": "2023-06-22 12:39:56.446722", "modified_by": "Administrator", "module": "Accounts", "name": "Exchange Rate Revaluation Account", From fb823b53d1d9a631035016fa3b33003bf8ce297a Mon Sep 17 00:00:00 2001 From: Anand Baburajan Date: Thu, 22 Jun 2023 17:14:24 +0530 Subject: [PATCH 06/26] fix: asset capitalization (#35832) * fix: misc asset capitalisation fixes * chore: add location in tests and remove unnecessary code * chore: more fixes and removals * chore: show company and fix tests * chore: make target qty read only on capitalization --- .../asset_capitalization.js | 25 ---- .../asset_capitalization.json | 28 ++-- .../asset_capitalization.py | 121 +++++++----------- .../test_asset_capitalization.py | 25 +--- 4 files changed, 69 insertions(+), 130 deletions(-) diff --git a/erpnext/assets/doctype/asset_capitalization/asset_capitalization.js b/erpnext/assets/doctype/asset_capitalization/asset_capitalization.js index 9c7f70b0e57..b312f93d319 100644 --- a/erpnext/assets/doctype/asset_capitalization/asset_capitalization.js +++ b/erpnext/assets/doctype/asset_capitalization/asset_capitalization.js @@ -14,7 +14,6 @@ erpnext.assets.AssetCapitalization = class AssetCapitalization extends erpnext.s } refresh() { - erpnext.hide_company(); 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(); @@ -105,10 +104,6 @@ erpnext.assets.AssetCapitalization = class AssetCapitalization extends erpnext.s return this.get_target_item_details(); } - target_asset() { - return this.get_target_asset_details(); - } - item_code(doc, cdt, cdn) { var row = frappe.get_doc(cdt, cdn); if (cdt === "Asset Capitalization Stock Item") { @@ -223,26 +218,6 @@ 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 d1be5752d61..04b0c4e5132 100644 --- a/erpnext/assets/doctype/asset_capitalization/asset_capitalization.json +++ b/erpnext/assets/doctype/asset_capitalization/asset_capitalization.json @@ -11,13 +11,14 @@ "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", "target_asset_name", + "target_asset_location", "target_warehouse", "target_qty", "target_stock_uom", @@ -85,14 +86,13 @@ "fieldtype": "Column Break" }, { - "depends_on": "eval:doc.entry_type=='Capitalization'", "fieldname": "target_asset", "fieldtype": "Link", "in_standard_filter": 1, "label": "Target Asset", - "mandatory_depends_on": "eval:doc.entry_type=='Capitalization'", "no_copy": 1, - "options": "Asset" + "options": "Asset", + "read_only": 1 }, { "depends_on": "eval:doc.entry_type=='Capitalization'", @@ -108,11 +108,11 @@ "fieldtype": "Column Break" }, { - "fetch_from": "asset.company", "fieldname": "company", "fieldtype": "Link", "label": "Company", "options": "Company", + "remember_last_selected_value": 1, "reqd": 1 }, { @@ -158,7 +158,7 @@ "read_only": 1 }, { - "depends_on": "eval:doc.docstatus == 0 || (doc.stock_items && doc.stock_items.length)", + "depends_on": "eval:doc.entry_type=='Capitalization' && (doc.docstatus == 0 || (doc.stock_items && doc.stock_items.length))", "fieldname": "section_break_16", "fieldtype": "Section Break", "label": "Consumed Stock Items" @@ -189,7 +189,7 @@ "fieldname": "target_qty", "fieldtype": "Float", "label": "Target Qty", - "read_only_depends_on": "target_is_fixed_asset" + "read_only_depends_on": "eval:doc.entry_type=='Capitalization'" }, { "fetch_from": "target_item_code.stock_uom", @@ -227,7 +227,7 @@ "depends_on": "eval:doc.docstatus == 0 || (doc.asset_items && doc.asset_items.length)", "fieldname": "section_break_26", "fieldtype": "Section Break", - "label": "Consumed Asset Items" + "label": "Consumed Assets" }, { "fieldname": "asset_items", @@ -266,7 +266,7 @@ "options": "Finance Book" }, { - "depends_on": "eval:doc.docstatus == 0 || (doc.service_items && doc.service_items.length)", + "depends_on": "eval:doc.entry_type=='Capitalization' && (doc.docstatus == 0 || (doc.service_items && doc.service_items.length))", "fieldname": "service_expenses_section", "fieldtype": "Section Break", "label": "Service Expenses" @@ -329,12 +329,20 @@ "label": "Target Fixed Asset Account", "options": "Account", "read_only": 1 + }, + { + "depends_on": "eval:doc.entry_type=='Capitalization'", + "fieldname": "target_asset_location", + "fieldtype": "Link", + "label": "Target Asset Location", + "mandatory_depends_on": "eval:doc.entry_type=='Capitalization'", + "options": "Location" } ], "index_web_pages_for_search": 1, "is_submittable": 1, "links": [], - "modified": "2022-09-12 15:09:40.771332", + "modified": "2023-06-22 14:17:07.995120", "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 f648823b5bf..5625fbb523b 100644 --- a/erpnext/assets/doctype/asset_capitalization/asset_capitalization.py +++ b/erpnext/assets/doctype/asset_capitalization/asset_capitalization.py @@ -7,7 +7,7 @@ import frappe # import erpnext from frappe import _ -from frappe.utils import cint, flt +from frappe.utils import cint, flt, get_link_to_form from six import string_types import erpnext @@ -43,7 +43,6 @@ force_fields = [ "target_has_batch_no", "target_stock_uom", "stock_uom", - "target_fixed_asset_account", "fixed_asset_account", "valuation_rate", ] @@ -54,7 +53,6 @@ class AssetCapitalization(StockController): self.validate_posting_time() self.set_missing_values(for_validate=True) self.validate_target_item() - self.validate_target_asset() self.validate_consumed_stock_item() self.validate_consumed_asset_item() self.validate_service_item() @@ -65,17 +63,18 @@ class AssetCapitalization(StockController): def before_submit(self): self.validate_source_mandatory() + if self.entry_type == "Capitalization": + 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") self.update_stock_ledger() self.make_gl_entries() - self.update_target_asset() + self.restore_consumed_asset_items() def set_title(self): self.title = self.target_asset_name or self.target_item_name or self.target_item_code @@ -86,15 +85,6 @@ class AssetCapitalization(StockController): if self.meta.has_field(k) and (not self.get(k) or k in force_fields): self.set(k, v) - # Remove asset if item not a fixed asset - if not self.target_is_fixed_asset: - self.target_asset = None - - 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,9 +136,6 @@ class AssetCapitalization(StockController): if not target_item.is_stock_item: self.target_warehouse = None - if not target_item.is_fixed_asset: - self.target_asset = None - self.target_fixed_asset_account = None if not target_item.has_batch_no: self.target_batch_no = None if not target_item.has_serial_no: @@ -159,17 +146,6 @@ class AssetCapitalization(StockController): self.validate_item(target_item) - def validate_target_asset(self): - if self.target_asset: - target_asset = self.get_asset_for_validation(self.target_asset) - - if 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) - ) - - self.validate_asset(target_asset) - def validate_consumed_stock_item(self): for d in self.stock_items: if d.item_code: @@ -379,7 +355,11 @@ class AssetCapitalization(StockController): 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 def get_target_account(self): @@ -422,11 +402,14 @@ class AssetCapitalization(StockController): 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 = self.get_asset(item) + 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() @@ -507,30 +490,41 @@ class AssetCapitalization(StockController): ) ) - def update_target_asset(self): + def create_target_asset(self): total_target_asset_value = flt(self.total_value, self.precision("total_value")) - if self.docstatus == 1 and self.entry_type == "Capitalization": - asset_doc = frappe.get_doc("Asset", self.target_asset) - 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.prepare_depreciation_data() - asset_doc.flags.ignore_validate_update_after_submit = True - asset_doc.save() - elif self.docstatus == 2: - for item in self.asset_items: - asset = self.get_asset(item) - asset.db_set("disposal_date", None) - self.set_consumed_asset_status(asset) + 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.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.flags.ignore_validate = True + asset_doc.insert() - if asset.calculate_depreciation: - reverse_depreciation_entry_made_after_disposal(asset, self.posting_date) - reset_depreciation_schedule(asset, self.posting_date) + self.target_asset = asset_doc.name - def get_asset(self, item): - asset = frappe.get_doc("Asset", item.asset) - self.check_finance_books(item, asset) - return asset + self.target_fixed_asset_account = get_asset_category_account( + "fixed_asset_account", item=self.target_item_code, company=asset_doc.company + ) + + frappe.msgprint( + _( + "Asset {0} has been created. 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) + asset.db_set("disposal_date", None) + self.set_consumed_asset_status(asset) + + if asset.calculate_depreciation: + reverse_depreciation_entry_made_after_disposal(asset, self.posting_date) + reset_depreciation_schedule(asset, self.posting_date) def set_consumed_asset_status(self, asset): if self.docstatus == 1: @@ -580,33 +574,6 @@ 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): diff --git a/erpnext/assets/doctype/asset_capitalization/test_asset_capitalization.py b/erpnext/assets/doctype/asset_capitalization/test_asset_capitalization.py index 86861f0b165..ead7abbf340 100644 --- a/erpnext/assets/doctype/asset_capitalization/test_asset_capitalization.py +++ b/erpnext/assets/doctype/asset_capitalization/test_asset_capitalization.py @@ -39,13 +39,6 @@ class TestAssetCapitalization(unittest.TestCase): total_amount = 103000 - # Create assets - target_asset = create_asset( - asset_name="Asset Capitalization Target Asset", - submit=1, - warehouse="Stores - TCP1", - company=company, - ) consumed_asset = create_asset( asset_name="Asset Capitalization Consumable Asset", asset_value=consumed_asset_value, @@ -57,7 +50,8 @@ class TestAssetCapitalization(unittest.TestCase): # Create and submit Asset Captitalization asset_capitalization = create_asset_capitalization( entry_type="Capitalization", - target_asset=target_asset.name, + target_item_code="Macbook Pro", + target_asset_location="Test Location", stock_qty=stock_qty, stock_rate=stock_rate, consumed_asset=consumed_asset.name, @@ -86,7 +80,7 @@ class TestAssetCapitalization(unittest.TestCase): self.assertEqual(asset_capitalization.target_incoming_rate, total_amount) # Test Target Asset values - target_asset.reload() + 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) @@ -134,13 +128,6 @@ class TestAssetCapitalization(unittest.TestCase): total_amount = 103000 - # Create assets - target_asset = create_asset( - asset_name="Asset Capitalization Target Asset", - submit=1, - warehouse="Stores - _TC", - company=company, - ) consumed_asset = create_asset( asset_name="Asset Capitalization Consumable Asset", asset_value=consumed_asset_value, @@ -152,7 +139,8 @@ class TestAssetCapitalization(unittest.TestCase): # Create and submit Asset Captitalization asset_capitalization = create_asset_capitalization( entry_type="Capitalization", - target_asset=target_asset.name, + target_item_code="Macbook Pro", + target_asset_location="Test Location", stock_qty=stock_qty, stock_rate=stock_rate, consumed_asset=consumed_asset.name, @@ -181,7 +169,7 @@ class TestAssetCapitalization(unittest.TestCase): self.assertEqual(asset_capitalization.target_incoming_rate, total_amount) # Test Target Asset values - target_asset.reload() + 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) @@ -343,6 +331,7 @@ def create_asset_capitalization(**args): "posting_time": args.posting_time or now.strftime("%H:%M:%S.%f"), "target_item_code": target_item_code, "target_asset": target_asset.name, + "target_asset_location": "Test Location", "target_warehouse": target_warehouse, "target_qty": flt(args.target_qty) or 1, "target_batch_no": args.target_batch_no, From 8ddfc34c304928c848999825bba6a5ca05d7942e Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Thu, 22 Jun 2023 15:55:18 +0530 Subject: [PATCH 07/26] fix: multiple Work Orders agaist same production plan (cherry picked from commit 80fffbd64bce62cc61a6d5df3865cad18b46a730) --- .../doctype/production_plan/production_plan.js | 2 +- .../doctype/production_plan/production_plan.py | 6 ++++++ .../doctype/production_plan/test_production_plan.py | 7 +++++++ 3 files changed, 14 insertions(+), 1 deletion(-) diff --git a/erpnext/manufacturing/doctype/production_plan/production_plan.js b/erpnext/manufacturing/doctype/production_plan/production_plan.js index 45a59cf7325..48986910b07 100644 --- a/erpnext/manufacturing/doctype/production_plan/production_plan.js +++ b/erpnext/manufacturing/doctype/production_plan/production_plan.js @@ -99,7 +99,7 @@ frappe.ui.form.on('Production Plan', { }, __('Create')); } - if (frm.doc.mr_items && !in_list(['Material Requested', 'Closed'], frm.doc.status)) { + if (frm.doc.mr_items && frm.doc.mr_items.length && !in_list(['Material Requested', 'Closed'], frm.doc.status)) { frm.add_custom_button(__("Material Request"), ()=> { frm.trigger("make_material_request"); }, __('Create')); diff --git a/erpnext/manufacturing/doctype/production_plan/production_plan.py b/erpnext/manufacturing/doctype/production_plan/production_plan.py index 0800bdd2af9..6dc1ff6a49f 100644 --- a/erpnext/manufacturing/doctype/production_plan/production_plan.py +++ b/erpnext/manufacturing/doctype/production_plan/production_plan.py @@ -515,6 +515,9 @@ class ProductionPlan(Document): self.show_list_created_message("Work Order", wo_list) self.show_list_created_message("Purchase Order", po_list) + if not wo_list: + frappe.msgprint(_("No Work Orders were created")) + def make_work_order_for_finished_goods(self, wo_list, default_warehouses): items_data = self.get_production_items() @@ -618,6 +621,9 @@ class ProductionPlan(Document): def create_work_order(self, item): from erpnext.manufacturing.doctype.work_order.work_order import OverProductionError + if item.get("qty") <= 0: + return + wo = frappe.new_doc("Work Order") wo.update(item) wo.planned_start_date = item.get("planned_start_date") or item.get("schedule_date") diff --git a/erpnext/manufacturing/doctype/production_plan/test_production_plan.py b/erpnext/manufacturing/doctype/production_plan/test_production_plan.py index 75b43ec1c30..fcfba7fca56 100644 --- a/erpnext/manufacturing/doctype/production_plan/test_production_plan.py +++ b/erpnext/manufacturing/doctype/production_plan/test_production_plan.py @@ -76,6 +76,13 @@ class TestProductionPlan(FrappeTestCase): "Work Order", fields=["name"], filters={"production_plan": pln.name}, as_list=1 ) + pln.make_work_order() + nwork_orders = frappe.get_all( + "Work Order", fields=["name"], filters={"production_plan": pln.name}, as_list=1 + ) + + self.assertTrue(len(work_orders), len(nwork_orders)) + self.assertTrue(len(work_orders), len(pln.po_items)) for name in material_requests: From 69780da0996d00a6e8fc81b2fce860e47f971b30 Mon Sep 17 00:00:00 2001 From: Anand Baburajan Date: Thu, 22 Jun 2023 22:22:18 +0530 Subject: [PATCH 08/26] chore: asset scrap and restore fixes [v14] (#35851) chore: better err msg on cancelling JE for asset scrap and allow restoring non-depr assets --- .../doctype/journal_entry/journal_entry.py | 15 +++++++++++---- erpnext/assets/doctype/asset/depreciation.py | 3 +++ 2 files changed, 14 insertions(+), 4 deletions(-) diff --git a/erpnext/accounts/doctype/journal_entry/journal_entry.py b/erpnext/accounts/doctype/journal_entry/journal_entry.py index b275cada953..0411fd1104e 100644 --- a/erpnext/accounts/doctype/journal_entry/journal_entry.py +++ b/erpnext/accounts/doctype/journal_entry/journal_entry.py @@ -326,12 +326,10 @@ class JournalEntry(AccountsController): d.db_update() def unlink_asset_reference(self): - if self.voucher_type != "Depreciation Entry": - return - for d in self.get("accounts"): if ( - d.reference_type == "Asset" + self.voucher_type == "Depreciation Entry" + and d.reference_type == "Asset" and d.reference_name and d.account_type == "Depreciation" and d.debit @@ -358,6 +356,15 @@ class JournalEntry(AccountsController): else: asset.db_set("value_after_depreciation", asset.value_after_depreciation + d.debit) asset.set_status() + elif self.voucher_type == "Journal Entry" and d.reference_type == "Asset" and d.reference_name: + journal_entry_for_scrap = frappe.db.get_value( + "Asset", d.reference_name, "journal_entry_for_scrap" + ) + + if journal_entry_for_scrap == self.name: + frappe.throw( + _("Journal Entry for Asset scrapping cannot be cancelled. Please restore the Asset.") + ) def unlink_inter_company_jv(self): if ( diff --git a/erpnext/assets/doctype/asset/depreciation.py b/erpnext/assets/doctype/asset/depreciation.py index a650db208ed..fff06a6928f 100644 --- a/erpnext/assets/doctype/asset/depreciation.py +++ b/erpnext/assets/doctype/asset/depreciation.py @@ -343,6 +343,9 @@ def modify_depreciation_schedule_for_asset_repairs(asset): def reverse_depreciation_entry_made_after_disposal(asset, date): + if not asset.calculate_depreciation: + return + row = -1 finance_book = asset.get("schedules")[0].get("finance_book") for schedule in asset.get("schedules"): From 42d09448eed316268d9a5a2c9e25791bc48fa326 Mon Sep 17 00:00:00 2001 From: Anand Baburajan Date: Fri, 23 Jun 2023 08:21:32 +0530 Subject: [PATCH 09/26] fix: show non-depreciable assets in fixed asset register (#35858) fix: show non-depr assets in fixed asset register --- .../report/fixed_asset_register/fixed_asset_register.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) 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 f810819b4fc..6911f94bbbb 100644 --- a/erpnext/assets/report/fixed_asset_register/fixed_asset_register.py +++ b/erpnext/assets/report/fixed_asset_register/fixed_asset_register.py @@ -115,7 +115,11 @@ def get_data(filters): depreciation_amount_map = get_asset_depreciation_amount_map(filters, finance_book) for asset in assets_record: - if assets_linked_to_fb and asset.asset_id not in assets_linked_to_fb: + if ( + assets_linked_to_fb + and asset.calculate_depreciation + and asset.asset_id not in assets_linked_to_fb + ): continue asset_value = get_asset_value_after_depreciation( From 21336f1a2c3470e770848068e53fdc144be30ce5 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Fri, 23 Jun 2023 12:39:47 +0530 Subject: [PATCH 10/26] ci: use multiple python version in patch test (#35846) ci: use multiple python version in patch test (cherry picked from commit 56e81ada56332b7dfb721610575ee1bffb2a97d3) Co-authored-by: Deepesh Garg --- .github/workflows/patch.yml | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/.github/workflows/patch.yml b/.github/workflows/patch.yml index 4d2dc586779..d6a03af535f 100644 --- a/.github/workflows/patch.yml +++ b/.github/workflows/patch.yml @@ -43,9 +43,11 @@ jobs: fi - name: Setup Python - uses: "gabrielfalcao/pyenv-action@v9" + uses: "actions/setup-python@v4" with: - versions: 3.10:latest, 3.7:latest + python-version: | + 3.7 + 3.10 - name: Setup Node uses: actions/setup-node@v2 @@ -92,7 +94,6 @@ jobs: - name: Install run: | pip install frappe-bench - pyenv global $(pyenv versions | grep '3.10') bash ${GITHUB_WORKSPACE}/.github/helper/install.sh env: DB: mariadb @@ -107,7 +108,6 @@ jobs: git -C "apps/frappe" remote set-url upstream https://github.com/frappe/frappe.git git -C "apps/erpnext" remote set-url upstream https://github.com/frappe/erpnext.git - pyenv global $(pyenv versions | grep '3.7') for version in $(seq 12 13) do echo "Updating to v$version" @@ -120,7 +120,7 @@ jobs: git -C "apps/erpnext" checkout -q -f $branch_name rm -rf ~/frappe-bench/env - bench setup env + bench setup env --python python3.7 bench pip install -e ./apps/payments bench pip install -e ./apps/erpnext @@ -132,9 +132,8 @@ jobs: git -C "apps/frappe" checkout -q -f "${GITHUB_BASE_REF:-${GITHUB_REF##*/}}" git -C "apps/erpnext" checkout -q -f "$GITHUB_SHA" - pyenv global $(pyenv versions | grep '3.10') rm -rf ~/frappe-bench/env - bench -v setup env + bench -v setup env --python python3.10 bench pip install -e ./apps/payments bench pip install -e ./apps/erpnext From b0234489ca39659c3d5f6385233da42c8c39bb24 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Fri, 23 Jun 2023 15:55:51 +0530 Subject: [PATCH 11/26] fix: Remove special treatment for P&L Accounts (#35602) fix: Remove special treatment for P&L Accounts (cherry picked from commit 0bd4de450431b2992b08389570a1526f187f0672) Co-authored-by: Deepesh Garg --- erpnext/accounts/utils.py | 5 ----- 1 file changed, 5 deletions(-) diff --git a/erpnext/accounts/utils.py b/erpnext/accounts/utils.py index 320cf9de68c..99c4fb31b5e 100644 --- a/erpnext/accounts/utils.py +++ b/erpnext/accounts/utils.py @@ -221,11 +221,6 @@ def get_balance_on( if not (frappe.flags.ignore_account_permission or ignore_account_permission): acc.check_permission("read") - if report_type == "Profit and Loss": - # for pl accounts, get balance within a fiscal year - cond.append( - "posting_date >= '%s' and voucher_type != 'Period Closing Voucher'" % year_start_date - ) # different filter for group and ledger - improved performance if acc.is_group: cond.append( From 5c388a132fd79d4b26ca40b894f0b661bccf756f Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Sat, 24 Jun 2023 12:28:43 +0530 Subject: [PATCH 12/26] fix: reconcile invoice against credit note. (#35604) * test: reconcile credit against invoice (cherry picked from commit f68ab3dfff50a2161858f072b33f75c306a731f4) * fix: missing attribute error (cherry picked from commit 7973951c370de0bc95c82ed132b109cdade9f2b9) * fix: reconcile invoice against credit note (cherry picked from commit 54935438e127d984ca28ddac4cda84e090e7f72a) --------- Co-authored-by: Devin Slauenwhite --- .../payment_reconciliation.py | 29 ++++++++++--- .../test_payment_reconciliation.py | 43 ++++++++++++++++++- 2 files changed, 65 insertions(+), 7 deletions(-) diff --git a/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py b/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py index 2e4e3b0e078..a709740d2fe 100644 --- a/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py +++ b/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py @@ -336,6 +336,7 @@ class PaymentReconciliation(Document): entry_list = [] dr_or_cr_notes = [] + difference_entries = [] for row in self.get("allocation"): reconciled_entry = [] if row.invoice_number and row.allocated_amount: @@ -348,13 +349,15 @@ class PaymentReconciliation(Document): reconciled_entry.append(payment_details) if payment_details.difference_amount: - self.make_difference_entry(payment_details) + difference_entries.append( + self.make_difference_entry(payment_details, do_not_save_and_submit=bool(dr_or_cr_notes)) + ) if entry_list: reconcile_against_document(entry_list, skip_ref_details_update_for_pe) if dr_or_cr_notes: - reconcile_dr_cr_note(dr_or_cr_notes, self.company) + reconcile_dr_cr_note(dr_or_cr_notes, difference_entries, self.company) @frappe.whitelist() def reconcile(self): @@ -382,7 +385,7 @@ class PaymentReconciliation(Document): self.get_unreconciled_entries() - def make_difference_entry(self, row): + def make_difference_entry(self, row, do_not_save_and_submit=False): journal_entry = frappe.new_doc("Journal Entry") journal_entry.voucher_type = "Exchange Gain Or Loss" journal_entry.company = self.company @@ -430,8 +433,11 @@ class PaymentReconciliation(Document): journal_entry.append("accounts", journal_account) - journal_entry.save() - journal_entry.submit() + if not do_not_save_and_submit: + journal_entry.save() + journal_entry.submit() + + return journal_entry def get_payment_details(self, row, dr_or_cr): return frappe._dict( @@ -597,7 +603,14 @@ class PaymentReconciliation(Document): return condition -def reconcile_dr_cr_note(dr_cr_notes, company): +def reconcile_dr_cr_note(dr_cr_notes, difference_entries, company): + def find_difference_entry(voucher_type, voucher_no): + for jv in difference_entries: + accounts = iter(jv.accounts) + for account in accounts: + if account.reference_type == voucher_type and account.reference_name == voucher_no: + return next(accounts) + for inv in dr_cr_notes: voucher_type = "Credit Note" if inv.voucher_type == "Sales Invoice" else "Debit Note" @@ -642,5 +655,9 @@ def reconcile_dr_cr_note(dr_cr_notes, company): ], } ) + + if difference_entry := find_difference_entry(inv.against_voucher_type, inv.against_voucher): + jv.append("accounts", difference_entry) + jv.flags.ignore_mandatory = True jv.submit() diff --git a/erpnext/accounts/doctype/payment_reconciliation/test_payment_reconciliation.py b/erpnext/accounts/doctype/payment_reconciliation/test_payment_reconciliation.py index 3be11ae31a7..2ac7df0e39b 100644 --- a/erpnext/accounts/doctype/payment_reconciliation/test_payment_reconciliation.py +++ b/erpnext/accounts/doctype/payment_reconciliation/test_payment_reconciliation.py @@ -11,10 +11,13 @@ from frappe.utils import add_days, flt, nowdate from erpnext import get_default_cost_center from erpnext.accounts.doctype.payment_entry.payment_entry import get_payment_entry from erpnext.accounts.doctype.payment_entry.test_payment_entry import create_payment_entry +from erpnext.accounts.doctype.purchase_invoice.test_purchase_invoice import make_purchase_invoice from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice from erpnext.accounts.party import get_party_account from erpnext.stock.doctype.item.test_item import create_item +test_dependencies = ["Item"] + class TestPaymentReconciliation(FrappeTestCase): def setUp(self): @@ -163,7 +166,9 @@ class TestPaymentReconciliation(FrappeTestCase): def create_payment_reconciliation(self): pr = frappe.new_doc("Payment Reconciliation") pr.company = self.company - pr.party_type = "Customer" + pr.party_type = ( + self.party_type if hasattr(self, "party_type") and self.party_type else "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() @@ -890,6 +895,42 @@ class TestPaymentReconciliation(FrappeTestCase): self.assertEqual(pr.allocation[0].allocated_amount, 85) self.assertEqual(pr.allocation[0].difference_amount, 0) + def test_reconciliation_purchase_invoice_against_return(self): + pi = make_purchase_invoice( + supplier="_Test Supplier USD", currency="USD", conversion_rate=50 + ).submit() + + pi_return = frappe.get_doc(pi.as_dict()) + pi_return.name = None + pi_return.docstatus = 0 + pi_return.is_return = 1 + pi_return.conversion_rate = 80 + pi_return.items[0].qty = -pi_return.items[0].qty + pi_return.submit() + + self.company = "_Test Company" + self.party_type = "Supplier" + self.customer = "_Test Supplier USD" + + pr = self.create_payment_reconciliation() + pr.get_unreconciled_entries() + + invoices = [] + payments = [] + for invoice in pr.invoices: + if invoice.invoice_number == pi.name: + invoices.append(invoice.as_dict()) + break + for payment in pr.payments: + if payment.reference_name == pi_return.name: + payments.append(payment.as_dict()) + break + + pr.allocate_entries(frappe._dict({"invoices": invoices, "payments": payments})) + + # Should not raise frappe.exceptions.ValidationError: Total Debit must be equal to Total Credit. + pr.reconcile() + def make_customer(customer_name, currency=None): if not frappe.db.exists("Customer", customer_name): From 3dd3935e7612b056bb881f241d50656386f86c15 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Sat, 24 Jun 2023 12:29:26 +0530 Subject: [PATCH 13/26] fix: Payment Term must be mandatory if `Allocate Payment based on ..` is checked (#35798) fix: Payment Term must be mandatory if `Allocate Payment based on ..` is checked (#35798) - Front and Back end validation of condition - Fix test to accomodate fix (cherry picked from commit 2868baebab6a3c76ca4121c7098ebc27d0f4b04a) Co-authored-by: Marica --- .../payment_terms_template/payment_terms_template.js | 6 +++++- .../payment_terms_template/payment_terms_template.py | 7 +++++-- .../doctype/purchase_receipt/test_purchase_receipt.py | 7 +++++++ 3 files changed, 17 insertions(+), 3 deletions(-) diff --git a/erpnext/accounts/doctype/payment_terms_template/payment_terms_template.js b/erpnext/accounts/doctype/payment_terms_template/payment_terms_template.js index ea18adefa35..6046c13e146 100644 --- a/erpnext/accounts/doctype/payment_terms_template/payment_terms_template.js +++ b/erpnext/accounts/doctype/payment_terms_template/payment_terms_template.js @@ -2,7 +2,11 @@ // For license information, please see license.txt frappe.ui.form.on('Payment Terms Template', { - setup: function(frm) { + refresh: function(frm) { + frm.fields_dict.terms.grid.toggle_reqd("payment_term", frm.doc.allocate_payment_based_on_payment_terms); + }, + allocate_payment_based_on_payment_terms: function(frm) { + frm.fields_dict.terms.grid.toggle_reqd("payment_term", frm.doc.allocate_payment_based_on_payment_terms); } }); diff --git a/erpnext/accounts/doctype/payment_terms_template/payment_terms_template.py b/erpnext/accounts/doctype/payment_terms_template/payment_terms_template.py index ea3b76c5243..7b04a68e89a 100644 --- a/erpnext/accounts/doctype/payment_terms_template/payment_terms_template.py +++ b/erpnext/accounts/doctype/payment_terms_template/payment_terms_template.py @@ -11,7 +11,7 @@ from frappe.utils import flt class PaymentTermsTemplate(Document): def validate(self): self.validate_invoice_portion() - self.check_duplicate_terms() + self.validate_terms() def validate_invoice_portion(self): total_portion = 0 @@ -23,9 +23,12 @@ class PaymentTermsTemplate(Document): _("Combined invoice portion must equal 100%"), raise_exception=1, indicator="red" ) - def check_duplicate_terms(self): + def validate_terms(self): terms = [] for term in self.terms: + if self.allocate_payment_based_on_payment_terms and not term.payment_term: + frappe.throw(_("Row {0}: Payment Term is mandatory").format(term.idx)) + term_info = (term.payment_term, term.credit_days, term.credit_months, term.due_date_based_on) if term_info in terms: frappe.msgprint( diff --git a/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py b/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py index 7965864cd43..3ed480f88da 100644 --- a/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py +++ b/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py @@ -63,6 +63,11 @@ class TestPurchaseReceipt(FrappeTestCase): self.assertEqual(sl_entry_cancelled[1].actual_qty, -0.5) def test_make_purchase_invoice(self): + from erpnext.accounts.doctype.payment_entry.test_payment_entry import create_payment_term + + create_payment_term("_Test Payment Term 1 for Purchase Invoice") + create_payment_term("_Test Payment Term 2 for Purchase Invoice") + if not frappe.db.exists( "Payment Terms Template", "_Test Payment Terms Template For Purchase Invoice" ): @@ -74,12 +79,14 @@ class TestPurchaseReceipt(FrappeTestCase): "terms": [ { "doctype": "Payment Terms Template Detail", + "payment_term": "_Test Payment Term 1 for Purchase Invoice", "invoice_portion": 50.00, "credit_days_based_on": "Day(s) after invoice date", "credit_days": 00, }, { "doctype": "Payment Terms Template Detail", + "payment_term": "_Test Payment Term 2 for Purchase Invoice", "invoice_portion": 50.00, "credit_days_based_on": "Day(s) after invoice date", "credit_days": 30, From d53b1978962f7e5baa1c5069bc624dc558705725 Mon Sep 17 00:00:00 2001 From: Marica Date: Sat, 24 Jun 2023 12:30:08 +0530 Subject: [PATCH 14/26] feat: Auto set Party in Bank Transaction (#34675) * feat: Party auto-matcher from Bank Transaction data - Created Bank Party Mapper - Created class to auto match by account/iban or party name/description(fuzzy) - Automatch and set in transaction or create mapper - `rapidfuzz` introduced * chore: Single query with or filter to search Party Mapper by name/desc * feat: Store Party bank details in party records (Customer/Supplier/Employee/Shareholder) * fix: Don't set description as key in Mapper doc if matched by description - Description is volatile and will keep changing - It will lead to multiple Bank Party Mapper docs for the same party that will never be referenced again - Parts of the descripton keep changing which is why it will never match a mapper record - If matched by desc, dont create mapper record. * feat: Manually Update/Correct Party in Bank Transaction - On updating bank trans.n party after submit, the corresponding mapper doc will be updated too - The mapper doc in turn will update all linked bank transactions that do not have this updated value - Added Bank Party Mapper hidden link in Bank Transaction - Rename field in BPM to `Party Name` as it does not hold description data - If a BT matches with a BPM record, link that record in the BT * chore: Perform automatch on submit - misc: Clearer naming * chore: Make auto matching party configurable - Checkbox in Accounts settings "Enable Automatic Party Matching" - Check before invoking automatching methods - misc: Remove TODO comments * fix: Match by both Account No and IBAN & other cleanups - A BT could have both account and iban, and a Supplier could have only IBAN set - In this case, matching by either (only account) gives no match - Match by Account OR IBAN, use `or_filters` - If matched, set both account no. and IBAN in Bank Party Mapper - Explain AutoMatchParty - Add type hints to return values - Use `set_value` to set values in BT after matching since its an after submit event * test: Match by Account No, IBAN, Party Name, Desc and match correction * fix: Remove bank details fields from Shareholder * fix: Use existing bank fields to match by bank account no/IBAN - Remove newly added fields in Party doctypes to store bank details - Use Bank Account's fields to match against account no/iban - For employee, if Bank Account does not exist, find in Employee doctype against account no/iban * fix: Tests * feat: Optional Fuzzy Matching & Skip Matches for multiple similar matches - Fuzzy matching can be enabled optionally in the settings - If a query gets multiple matches with the same score, do not set a party as it is an extremely close call - misc: Add 'cancelled' status to Bank transaction - Test for skipping matching with extremely close matches * chore: Remove Bank Party Mapper implementation - Matching by Acc No/IBAN can easily happen with Bank Accounts. It's not a tedious query - Historical lookups for Party Name/Desc match are very tricky. The user could have manually set a match and we would not know. Also this leaves the Bank Party Mapper only useful for Party Name/Desc lookups, which feels excessive. - We want to reduce the number of places the same data is stored and reduce confusion - The Party Name/Desc will optionally happen fuzzily, or not at all - There will be no Mapper lookups * chore: Remove instances of `bank_party_mapper` and use `new_doc` --- .../accounts_settings/accounts_settings.json | 27 ++- .../bank_transaction/auto_match_party.py | 178 ++++++++++++++++++ .../bank_transaction/bank_transaction.json | 31 ++- .../bank_transaction/bank_transaction.py | 23 +++ .../bank_transaction/test_auto_match_party.py | 151 +++++++++++++++ .../doctype/shareholder/shareholder.json | 7 +- erpnext/buying/doctype/supplier/supplier.json | 2 +- .../selling/doctype/customer/customer.json | 2 +- erpnext/setup/doctype/employee/employee.json | 15 +- pyproject.toml | 1 + 10 files changed, 425 insertions(+), 12 deletions(-) create mode 100644 erpnext/accounts/doctype/bank_transaction/auto_match_party.py create mode 100644 erpnext/accounts/doctype/bank_transaction/test_auto_match_party.py diff --git a/erpnext/accounts/doctype/accounts_settings/accounts_settings.json b/erpnext/accounts/doctype/accounts_settings/accounts_settings.json index 6c99d29dbfc..47c4396e102 100644 --- a/erpnext/accounts/doctype/accounts_settings/accounts_settings.json +++ b/erpnext/accounts/doctype/accounts_settings/accounts_settings.json @@ -62,7 +62,10 @@ "acc_frozen_upto", "column_break_25", "frozen_accounts_modifier", - "report_settings_sb" + "report_settings_sb", + "banking_tab", + "enable_party_matching", + "enable_fuzzy_matching" ], "fields": [ { @@ -385,6 +388,26 @@ "fieldname": "show_taxes_as_table_in_print", "fieldtype": "Check", "label": "Show Taxes as Table in Print" + }, + { + "fieldname": "banking_tab", + "fieldtype": "Tab Break", + "label": "Banking" + }, + { + "default": "0", + "description": "Auto match and set the Party in Bank Transactions", + "fieldname": "enable_party_matching", + "fieldtype": "Check", + "label": "Enable Automatic Party Matching" + }, + { + "default": "0", + "depends_on": "enable_party_matching", + "description": "Approximately match the description/party name against parties", + "fieldname": "enable_fuzzy_matching", + "fieldtype": "Check", + "label": "Enable Fuzzy Matching" } ], "icon": "icon-cog", @@ -392,7 +415,7 @@ "index_web_pages_for_search": 1, "issingle": 1, "links": [], - "modified": "2023-06-13 18:47:46.430291", + "modified": "2023-06-15 18:47:46.430291", "modified_by": "Administrator", "module": "Accounts", "name": "Accounts Settings", diff --git a/erpnext/accounts/doctype/bank_transaction/auto_match_party.py b/erpnext/accounts/doctype/bank_transaction/auto_match_party.py new file mode 100644 index 00000000000..5d94a08f2f0 --- /dev/null +++ b/erpnext/accounts/doctype/bank_transaction/auto_match_party.py @@ -0,0 +1,178 @@ +from typing import Tuple, Union + +import frappe +from frappe.utils import flt +from rapidfuzz import fuzz, process + + +class AutoMatchParty: + """ + Matches by Account/IBAN and then by Party Name/Description sequentially. + Returns when a result is obtained. + + Result (if present) is of the form: (Party Type, Party,) + """ + + def __init__(self, **kwargs) -> None: + self.__dict__.update(kwargs) + + def get(self, key): + return self.__dict__.get(key, None) + + def match(self) -> Union[Tuple, None]: + result = None + result = AutoMatchbyAccountIBAN( + bank_party_account_number=self.bank_party_account_number, + bank_party_iban=self.bank_party_iban, + deposit=self.deposit, + ).match() + + fuzzy_matching_enabled = frappe.db.get_single_value("Accounts Settings", "enable_fuzzy_matching") + if not result and fuzzy_matching_enabled: + result = AutoMatchbyPartyNameDescription( + bank_party_name=self.bank_party_name, description=self.description, deposit=self.deposit + ).match() + + return result + + +class AutoMatchbyAccountIBAN: + def __init__(self, **kwargs) -> None: + self.__dict__.update(kwargs) + + def get(self, key): + return self.__dict__.get(key, None) + + def match(self): + if not (self.bank_party_account_number or self.bank_party_iban): + return None + + result = self.match_account_in_party() + return result + + def match_account_in_party(self) -> Union[Tuple, None]: + """Check if there is a IBAN/Account No. match in Customer/Supplier/Employee""" + result = None + parties = get_parties_in_order(self.deposit) + or_filters = self.get_or_filters() + + for party in parties: + party_result = frappe.db.get_all( + "Bank Account", or_filters=or_filters, pluck="party", limit_page_length=1 + ) + + if party == "Employee" and not party_result: + # Search in Bank Accounts first for Employee, and then Employee record + if "bank_account_no" in or_filters: + or_filters["bank_ac_no"] = or_filters.pop("bank_account_no") + + party_result = frappe.db.get_all( + party, or_filters=or_filters, pluck="name", limit_page_length=1 + ) + + if party_result: + result = ( + party, + party_result[0], + ) + break + + return result + + def get_or_filters(self) -> dict: + or_filters = {} + if self.bank_party_account_number: + or_filters["bank_account_no"] = self.bank_party_account_number + + if self.bank_party_iban: + or_filters["iban"] = self.bank_party_iban + + return or_filters + + +class AutoMatchbyPartyNameDescription: + def __init__(self, **kwargs) -> None: + self.__dict__.update(kwargs) + + def get(self, key): + return self.__dict__.get(key, None) + + def match(self) -> Union[Tuple, None]: + # fuzzy search by customer/supplier & employee + if not (self.bank_party_name or self.description): + return None + + result = self.match_party_name_desc_in_party() + return result + + def match_party_name_desc_in_party(self) -> Union[Tuple, None]: + """Fuzzy search party name and/or description against parties in the system""" + result = None + parties = get_parties_in_order(self.deposit) + + for party in parties: + filters = {"status": "Active"} if party == "Employee" else {"disabled": 0} + names = frappe.get_all(party, filters=filters, pluck=party.lower() + "_name") + + for field in ["bank_party_name", "description"]: + if not self.get(field): + continue + + result, skip = self.fuzzy_search_and_return_result(party, names, field) + if result or skip: + break + + if result or skip: + # Skip If: It was hard to distinguish between close matches and so match is None + # OR if the right match was found + break + + return result + + def fuzzy_search_and_return_result(self, party, names, field) -> Union[Tuple, None]: + skip = False + result = process.extract(query=self.get(field), choices=names, scorer=fuzz.token_set_ratio) + party_name, skip = self.process_fuzzy_result(result) + + if not party_name: + return None, skip + + return ( + party, + party_name, + ), skip + + def process_fuzzy_result(self, result: Union[list, None]): + """ + If there are multiple valid close matches return None as result may be faulty. + Return the result only if one accurate match stands out. + + Returns: Result, Skip (whether or not to discontinue matching) + """ + PARTY, SCORE, CUTOFF = 0, 1, 80 + + if not result or not len(result): + return None, False + + first_result = result[0] + if len(result) == 1: + return (first_result[PARTY] if first_result[SCORE] > CUTOFF else None), True + + second_result = result[1] + if first_result[SCORE] > CUTOFF: + # If multiple matches with the same score, return None but discontinue matching + # Matches were found but were too close to distinguish between + if first_result[SCORE] == second_result[SCORE]: + return None, True + + return first_result[PARTY], True + else: + return None, False + + +def get_parties_in_order(deposit: float) -> list: + parties = ["Supplier", "Employee", "Customer"] # most -> least likely to receive + if flt(deposit) > 0: + parties = ["Customer", "Supplier", "Employee"] # most -> least likely to pay + + return parties diff --git a/erpnext/accounts/doctype/bank_transaction/bank_transaction.json b/erpnext/accounts/doctype/bank_transaction/bank_transaction.json index 768d2f0fa45..bb7a4771b2d 100644 --- a/erpnext/accounts/doctype/bank_transaction/bank_transaction.json +++ b/erpnext/accounts/doctype/bank_transaction/bank_transaction.json @@ -33,7 +33,11 @@ "unallocated_amount", "party_section", "party_type", - "party" + "party", + "column_break_3czf", + "bank_party_name", + "bank_party_account_number", + "bank_party_iban" ], "fields": [ { @@ -63,7 +67,7 @@ "fieldtype": "Select", "in_standard_filter": 1, "label": "Status", - "options": "\nPending\nSettled\nUnreconciled\nReconciled" + "options": "\nPending\nSettled\nUnreconciled\nReconciled\nCancelled" }, { "fieldname": "bank_account", @@ -202,11 +206,30 @@ "fieldtype": "Data", "label": "Transaction Type", "length": 50 + }, + { + "fieldname": "column_break_3czf", + "fieldtype": "Column Break" + }, + { + "fieldname": "bank_party_name", + "fieldtype": "Data", + "label": "Party Name/Account Holder (Bank Statement)" + }, + { + "fieldname": "bank_party_iban", + "fieldtype": "Data", + "label": "Party IBAN (Bank Statement)" + }, + { + "fieldname": "bank_party_account_number", + "fieldtype": "Data", + "label": "Party Account No. (Bank Statement)" } ], "is_submittable": 1, "links": [], - "modified": "2022-05-29 18:36:50.475964", + "modified": "2023-06-06 13:58:12.821411", "modified_by": "Administrator", "module": "Accounts", "name": "Bank Transaction", @@ -260,4 +283,4 @@ "states": [], "title_field": "bank_account", "track_changes": 1 -} +} \ No newline at end of file diff --git a/erpnext/accounts/doctype/bank_transaction/bank_transaction.py b/erpnext/accounts/doctype/bank_transaction/bank_transaction.py index b441af96600..f82337fbd77 100644 --- a/erpnext/accounts/doctype/bank_transaction/bank_transaction.py +++ b/erpnext/accounts/doctype/bank_transaction/bank_transaction.py @@ -15,6 +15,9 @@ class BankTransaction(StatusUpdater): self.clear_linked_payment_entries() self.set_status() + if frappe.db.get_single_value("Accounts Settings", "enable_party_matching"): + self.auto_set_party() + _saving_flag = False # nosemgrep: frappe-semgrep-rules.rules.frappe-modifying-but-not-comitting @@ -146,6 +149,26 @@ class BankTransaction(StatusUpdater): payment_entry.payment_document, payment_entry.payment_entry, clearance_date, self ) + def auto_set_party(self): + from erpnext.accounts.doctype.bank_transaction.auto_match_party import AutoMatchParty + + if self.party_type and self.party: + return + + result = AutoMatchParty( + bank_party_account_number=self.bank_party_account_number, + bank_party_iban=self.bank_party_iban, + bank_party_name=self.bank_party_name, + description=self.description, + deposit=self.deposit, + ).match() + + if result: + party_type, party = result + frappe.db.set_value( + "Bank Transaction", self.name, field={"party_type": party_type, "party": party} + ) + @frappe.whitelist() def get_doctypes_for_bank_reconciliation(): diff --git a/erpnext/accounts/doctype/bank_transaction/test_auto_match_party.py b/erpnext/accounts/doctype/bank_transaction/test_auto_match_party.py new file mode 100644 index 00000000000..36ef1fca074 --- /dev/null +++ b/erpnext/accounts/doctype/bank_transaction/test_auto_match_party.py @@ -0,0 +1,151 @@ +# Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and Contributors +# License: GNU General Public License v3. See license.txt + +import frappe +from frappe.tests.utils import FrappeTestCase +from frappe.utils import nowdate + +from erpnext.accounts.doctype.bank_transaction.test_bank_transaction import create_bank_account + + +class TestAutoMatchParty(FrappeTestCase): + @classmethod + def setUpClass(cls): + create_bank_account() + frappe.db.set_single_value("Accounts Settings", "enable_party_matching", 1) + frappe.db.set_single_value("Accounts Settings", "enable_fuzzy_matching", 1) + return super().setUpClass() + + @classmethod + def tearDownClass(cls): + frappe.db.set_single_value("Accounts Settings", "enable_party_matching", 0) + frappe.db.set_single_value("Accounts Settings", "enable_fuzzy_matching", 0) + + def test_match_by_account_number(self): + create_supplier_for_match(account_no="000000003716541159") + doc = create_bank_transaction( + withdrawal=1200, + transaction_id="562213b0ca1bf838dab8f2c6a39bbc3b", + account_no="000000003716541159", + iban="DE02000000003716541159", + ) + + self.assertEqual(doc.party_type, "Supplier") + self.assertEqual(doc.party, "John Doe & Co.") + + def test_match_by_iban(self): + create_supplier_for_match(iban="DE02000000003716541159") + doc = create_bank_transaction( + withdrawal=1200, + transaction_id="c5455a224602afaa51592a9d9250600d", + account_no="000000003716541159", + iban="DE02000000003716541159", + ) + + self.assertEqual(doc.party_type, "Supplier") + self.assertEqual(doc.party, "John Doe & Co.") + + def test_match_by_party_name(self): + create_supplier_for_match(supplier_name="Jackson Ella W.") + doc = create_bank_transaction( + withdrawal=1200, + transaction_id="1f6f661f347ff7b1ea588665f473adb1", + party_name="Ella Jackson", + iban="DE04000000003716545346", + ) + self.assertEqual(doc.party_type, "Supplier") + self.assertEqual(doc.party, "Jackson Ella W.") + + def test_match_by_description(self): + create_supplier_for_match(supplier_name="Microsoft") + doc = create_bank_transaction( + description="Auftraggeber: microsoft payments Buchungstext: msft ..e3006b5hdy. ref. j375979555927627/5536", + withdrawal=1200, + transaction_id="8df880a2d09c3bed3fea358ca5168c5a", + party_name="", + ) + self.assertEqual(doc.party_type, "Supplier") + self.assertEqual(doc.party, "Microsoft") + + def test_skip_match_if_multiple_close_results(self): + create_supplier_for_match(supplier_name="Adithya Medical & General Stores") + create_supplier_for_match(supplier_name="Adithya Medical And General Stores") + + doc = create_bank_transaction( + description="Paracetamol Consignment, SINV-0009", + withdrawal=24.85, + transaction_id="3a1da4ee2dc5a980138d56ef3460cbd9", + party_name="Adithya Medical & General", + ) + + # Mapping is skipped as both Supplier names have the same match score + self.assertEqual(doc.party_type, None) + self.assertEqual(doc.party, None) + + +def create_supplier_for_match(supplier_name="John Doe & Co.", iban=None, account_no=None): + if frappe.db.exists("Supplier", {"supplier_name": supplier_name}): + # Update related Bank Account details + if not (iban or account_no): + return + + frappe.db.set_value( + dt="Bank Account", + dn={"party": supplier_name}, + field={"iban": iban, "bank_account_no": account_no}, + ) + return + + # Create Supplier and Bank Account for the same + supplier = frappe.new_doc("Supplier") + supplier.supplier_name = supplier_name + supplier.supplier_group = "Services" + supplier.supplier_type = "Company" + supplier.insert() + + if not frappe.db.exists("Bank", "TestBank"): + bank = frappe.new_doc("Bank") + bank.bank_name = "TestBank" + bank.insert(ignore_if_duplicate=True) + + if not frappe.db.exists("Bank Account", supplier.name + " - " + "TestBank"): + bank_account = frappe.new_doc("Bank Account") + bank_account.account_name = supplier.name + bank_account.bank = "TestBank" + bank_account.iban = iban + bank_account.bank_account_no = account_no + bank_account.party_type = "Supplier" + bank_account.party = supplier.name + bank_account.insert() + + +def create_bank_transaction( + description=None, + withdrawal=0, + deposit=0, + transaction_id=None, + party_name=None, + account_no=None, + iban=None, +): + doc = frappe.new_doc("Bank Transaction") + doc.update( + { + "doctype": "Bank Transaction", + "description": description or "1512567 BG/000002918 OPSKATTUZWXXX AT776000000098709837 Herr G", + "date": nowdate(), + "withdrawal": withdrawal, + "deposit": deposit, + "currency": "INR", + "bank_account": "Checking Account - Citi Bank", + "transaction_id": transaction_id, + "bank_party_name": party_name, + "bank_party_account_number": account_no, + "bank_party_iban": iban, + } + ) + doc.insert() + doc.submit() + doc.reload() + + return doc diff --git a/erpnext/accounts/doctype/shareholder/shareholder.json b/erpnext/accounts/doctype/shareholder/shareholder.json index e94aea94b75..e80b05720e0 100644 --- a/erpnext/accounts/doctype/shareholder/shareholder.json +++ b/erpnext/accounts/doctype/shareholder/shareholder.json @@ -1,4 +1,5 @@ { + "actions": [], "autoname": "naming_series:", "creation": "2017-12-25 16:50:53.878430", "doctype": "DocType", @@ -111,11 +112,12 @@ "read_only": 1 } ], - "modified": "2019-11-17 23:24:11.395882", + "links": [], + "modified": "2023-04-10 22:02:20.406087", "modified_by": "Administrator", "module": "Accounts", "name": "Shareholder", - "name_case": "Title Case", + "naming_rule": "By \"Naming Series\" field", "owner": "Administrator", "permissions": [ { @@ -158,6 +160,7 @@ "search_fields": "folio_no", "sort_field": "modified", "sort_order": "DESC", + "states": [], "title_field": "title", "track_changes": 1 } \ No newline at end of file diff --git a/erpnext/buying/doctype/supplier/supplier.json b/erpnext/buying/doctype/supplier/supplier.json index 66eafe9547a..3aca21ed6a6 100644 --- a/erpnext/buying/doctype/supplier/supplier.json +++ b/erpnext/buying/doctype/supplier/supplier.json @@ -457,7 +457,7 @@ "link_fieldname": "party" } ], - "modified": "2022-11-09 18:02:59.075203", + "modified": "2023-05-09 15:34:13.408932", "modified_by": "Administrator", "module": "Buying", "name": "Supplier", diff --git a/erpnext/selling/doctype/customer/customer.json b/erpnext/selling/doctype/customer/customer.json index 7482a33653c..18bb4b9bb46 100644 --- a/erpnext/selling/doctype/customer/customer.json +++ b/erpnext/selling/doctype/customer/customer.json @@ -568,7 +568,7 @@ "link_fieldname": "party" } ], - "modified": "2022-11-08 15:52:34.462657", + "modified": "2023-05-09 15:38:40.255193", "modified_by": "Administrator", "module": "Selling", "name": "Customer", diff --git a/erpnext/setup/doctype/employee/employee.json b/erpnext/setup/doctype/employee/employee.json index 99693d90918..6cb4292226c 100644 --- a/erpnext/setup/doctype/employee/employee.json +++ b/erpnext/setup/doctype/employee/employee.json @@ -78,7 +78,9 @@ "salary_mode", "bank_details_section", "bank_name", + "column_break_heye", "bank_ac_no", + "iban", "personal_details", "marital_status", "family_background", @@ -804,17 +806,26 @@ { "fieldname": "column_break_104", "fieldtype": "Column Break" + }, + { + "fieldname": "column_break_heye", + "fieldtype": "Column Break" + }, + { + "depends_on": "eval:doc.salary_mode == 'Bank'", + "fieldname": "iban", + "fieldtype": "Data", + "label": "IBAN" } ], "icon": "fa fa-user", "idx": 24, "image_field": "image", "links": [], - "modified": "2022-09-13 10:27:14.579197", + "modified": "2023-03-30 15:57:05.174592", "modified_by": "Administrator", "module": "Setup", "name": "Employee", - "name_case": "Title Case", "naming_rule": "By \"Naming Series\" field", "owner": "Administrator", "permissions": [ diff --git a/pyproject.toml b/pyproject.toml index 5acfd392726..9b48e6b9668 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -13,6 +13,7 @@ dependencies = [ "python-stdnum~=1.16", "Unidecode~=1.2.0", "redisearch~=2.1.0", + "rapidfuzz~=2.15.0", # integration dependencies "gocardless-pro~=1.22.0", From fafb46eebd87b684f634c1207fdfce9219ecc1c2 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Sat, 24 Jun 2023 12:30:40 +0530 Subject: [PATCH 15/26] fix: make credit note and debit note exclusive (#35781) * fix: make credit note and debit note exclusive (#35781) (cherry picked from commit 4fbff2095447f12e6941d39b156b50fa4f1c7b80) # Conflicts: # erpnext/accounts/doctype/sales_invoice/sales_invoice.json * chore: resolve conflicts --------- Co-authored-by: Smit Vora Co-authored-by: Deepesh Garg --- erpnext/accounts/doctype/sales_invoice/sales_invoice.json | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.json b/erpnext/accounts/doctype/sales_invoice/sales_invoice.json index 0e6118abe6b..fb60dd58724 100644 --- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.json +++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.json @@ -320,6 +320,7 @@ }, { "default": "0", + "depends_on": "eval: !doc.is_debit_note", "fieldname": "is_return", "fieldtype": "Check", "hide_days": 1, @@ -1959,6 +1960,7 @@ }, { "default": "0", + "depends_on": "eval: !doc.is_return", "description": "Issue a debit note with 0 qty against an existing Sales Invoice", "fieldname": "is_debit_note", "fieldtype": "Check", @@ -2153,7 +2155,7 @@ "link_fieldname": "consolidated_invoice" } ], - "modified": "2023-04-28 14:15:59.901154", + "modified": "2023-06-19 16:02:05.309332", "modified_by": "Administrator", "module": "Accounts", "name": "Sales Invoice", From 8ecca2a1cf93dbebdc379f4b7036a8d728664837 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Sat, 24 Jun 2023 18:58:13 +0530 Subject: [PATCH 16/26] fix: POS Closing Entry load all invoices with one request on save (#35819) fix: POS Closing Entry load all invoices with one request on save (#35819) fix: load all invoices with one request (cherry picked from commit 1e2001605973741f7cf7d919b79d7f618cac632a) Co-authored-by: HarryPaulo --- .../pos_closing_entry/pos_closing_entry.js | 39 +++++++++++-------- 1 file changed, 23 insertions(+), 16 deletions(-) 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 e6d9fe2b54d..a6c0102a7f9 100644 --- a/erpnext/accounts/doctype/pos_closing_entry/pos_closing_entry.js +++ b/erpnext/accounts/doctype/pos_closing_entry/pos_closing_entry.js @@ -123,22 +123,29 @@ frappe.ui.form.on('POS Closing Entry', { row.expected_amount = row.opening_amount; } - const pos_inv_promises = frm.doc.pos_transactions.map( - row => frappe.db.get_doc("POS Invoice", row.pos_invoice) - ); - - const pos_invoices = await Promise.all(pos_inv_promises); - - for (let doc of pos_invoices) { - frm.doc.grand_total += flt(doc.grand_total); - frm.doc.net_total += flt(doc.net_total); - frm.doc.total_quantity += flt(doc.total_qty); - refresh_payments(doc, frm); - refresh_taxes(doc, frm); - refresh_fields(frm); - set_html_data(frm); - } - + await Promise.all([ + frappe.call({ + method: 'erpnext.accounts.doctype.pos_closing_entry.pos_closing_entry.get_pos_invoices', + args: { + start: frappe.datetime.get_datetime_as_string(frm.doc.period_start_date), + end: frappe.datetime.get_datetime_as_string(frm.doc.period_end_date), + pos_profile: frm.doc.pos_profile, + user: frm.doc.user + }, + callback: (r) => { + let pos_invoices = r.message; + for (let doc of pos_invoices) { + frm.doc.grand_total += flt(doc.grand_total); + frm.doc.net_total += flt(doc.net_total); + frm.doc.total_quantity += flt(doc.total_qty); + refresh_payments(doc, frm); + refresh_taxes(doc, frm); + refresh_fields(frm); + set_html_data(frm); + } + } + }) + ]) frappe.dom.unfreeze(); } }); From 8f13b484a942126dbbc028481f7d7ae4106da9ce Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Sat, 24 Jun 2023 19:57:13 +0530 Subject: [PATCH 17/26] fix: use correct fieldname for purchase receipt column in item_wise_purchase_register report (#35828) fix: use correct fieldname for purchase receipt column in item_wise_purcchase_register report (cherry picked from commit dcfc86e3afa53e393c4dec74fa3f7d3a01be2aa4) Co-authored-by: phot0n --- .../item_wise_purchase_register.py | 4 ++-- 1 file 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 d34c21348c8..924c14bdb94 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 @@ -87,7 +87,7 @@ def _execute(filters=None, additional_table_columns=None, additional_query_colum "project": d.project, "company": d.company, "purchase_order": d.purchase_order, - "purchase_receipt": d.purchase_receipt, + "purchase_receipt": purchase_receipt, "expense_account": expense_account, "stock_qty": d.stock_qty, "stock_uom": d.stock_uom, @@ -241,7 +241,7 @@ def get_columns(additional_table_columns, filters): }, { "label": _("Purchase Receipt"), - "fieldname": "Purchase Receipt", + "fieldname": "purchase_receipt", "fieldtype": "Link", "options": "Purchase Receipt", "width": 100, From 4a7d75b5cc89063b5bd230251d13598d7d51e3b6 Mon Sep 17 00:00:00 2001 From: saeedkola Date: Sat, 24 Jun 2023 19:58:31 +0530 Subject: [PATCH 18/26] fix: Set Asset cost center default as PR or PI Item Cost Center while auto creating (#35844) * fix: Set Asset cost center default as PR or PI Item Cost Center while auto creating * chore: Linting Issues --------- Co-authored-by: Deepesh Garg --- erpnext/controllers/buying_controller.py | 1 + 1 file changed, 1 insertion(+) diff --git a/erpnext/controllers/buying_controller.py b/erpnext/controllers/buying_controller.py index 1ace3bf44f2..04636de6406 100644 --- a/erpnext/controllers/buying_controller.py +++ b/erpnext/controllers/buying_controller.py @@ -709,6 +709,7 @@ class BuyingController(SubcontractingController): "asset_quantity": row.qty if is_grouped_asset else 0, "purchase_receipt": self.name if self.doctype == "Purchase Receipt" else None, "purchase_invoice": self.name if self.doctype == "Purchase Invoice" else None, + "cost_center": row.cost_center, } ) From 21d560cd1963a7254bd40cc0389a9eb3cab31d0b Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Sun, 25 Jun 2023 16:21:18 +0530 Subject: [PATCH 19/26] feat: Provision to send Accounts Receivable Reports using Process SOA (#35789) * feat: Provision to send Accounts Receivable Reports using Process Statement of Accounts Issue #35707 (cherry picked from commit b3d565c91faca2b06d6814a6ea69e0191b267b7e) # Conflicts: # erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.py * fix: add patch for setting default value of report field (cherry picked from commit 555c126eb9b2badeacfa742fa379f33038b7085b) # Conflicts: # erpnext/patches.txt * fix: modify patch (cherry picked from commit cde82bc0cc978c3f4fec183825b709460062f8a8) * chore: update typo in patch (cherry picked from commit 4de7a4c5718acbc144729f143cdfc0f61a3693f7) * chore: Resolve conflicts --------- Co-authored-by: Gursheen Anand Co-authored-by: Deepesh Garg --- .../process_statement_of_accounts.html | 2 +- .../process_statement_of_accounts.js | 14 + .../process_statement_of_accounts.json | 68 +++- .../process_statement_of_accounts.py | 190 ++++++---- ...ement_of_accounts_accounts_receivable.html | 348 ++++++++++++++++++ .../accounts_receivable.html | 2 +- erpnext/patches.txt | 1 + .../v14_0/set_report_in_process_SOA.py | 10 + 8 files changed, 560 insertions(+), 75 deletions(-) create mode 100644 erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts_accounts_receivable.html create mode 100644 erpnext/patches/v14_0/set_report_in_process_SOA.py diff --git a/erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.html b/erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.html index 03abc93e0b8..5307ccb1931 100644 --- a/erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.html +++ b/erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.html @@ -1,6 +1,6 @@
- {% if letter_head %} + {% if letter_head.content %}
{{ letter_head.content }}

{% endif %} diff --git a/erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.js b/erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.js index 7dd77fbb3c7..c5908b783ee 100644 --- a/erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.js +++ b/erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.js @@ -63,6 +63,20 @@ frappe.ui.form.on('Process Statement Of Accounts', { frm.set_value('to_date', frappe.datetime.get_today()); } }, + report: function(frm){ + let filters = { + 'company': frm.doc.company, + } + if(frm.doc.report == 'Accounts Receivable'){ + filters['account_type'] = 'Receivable'; + } + frm.set_query("account", function() { + return { + filters: filters + }; + }); + + }, customer_collection: function(frm){ frm.set_value('collection_name', ''); if(frm.doc.customer_collection){ 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 0620e566b74..45373741f39 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 @@ -6,17 +6,24 @@ "editable_grid": 1, "engine": "InnoDB", "field_order": [ + "report", "section_break_11", "from_date", + "posting_date", "company", "account", "group_by", "cost_center", + "territory", "column_break_14", "to_date", "finance_book", "currency", "project", + "payment_terms_template", + "sales_partner", + "sales_person", + "based_on_payment_terms", "section_break_3", "customer_collection", "collection_name", @@ -65,14 +72,14 @@ "reqd": 1 }, { - "depends_on": "eval:doc.enable_auto_email == 0;", + "depends_on": "eval:(doc.enable_auto_email == 0 && doc.report == 'General Ledger');", "fieldname": "from_date", "fieldtype": "Date", "label": "From Date", "mandatory_depends_on": "eval:doc.frequency == '';" }, { - "depends_on": "eval:doc.enable_auto_email == 0;", + "depends_on": "eval:(doc.enable_auto_email == 0 && doc.report == 'General Ledger');", "fieldname": "to_date", "fieldtype": "Date", "label": "To Date", @@ -85,6 +92,7 @@ "options": "PSOA Cost Center" }, { + "depends_on": "eval: (doc.report == 'General Ledger');", "fieldname": "project", "fieldtype": "Table MultiSelect", "label": "Project", @@ -102,7 +110,7 @@ { "fieldname": "section_break_11", "fieldtype": "Section Break", - "label": "General Ledger Filters" + "label": "Report Filters" }, { "fieldname": "column_break_14", @@ -162,12 +170,14 @@ }, { "default": "Group by Voucher (Consolidated)", + "depends_on": "eval:(doc.report == 'General Ledger');", "fieldname": "group_by", "fieldtype": "Select", "label": "Group By", "options": "\nGroup by Voucher\nGroup by Voucher (Consolidated)" }, { + "depends_on": "eval: (doc.report == 'General Ledger');", "fieldname": "currency", "fieldtype": "Link", "label": "Currency", @@ -295,6 +305,7 @@ }, { "default": "0", + "depends_on": "eval: (doc.report == 'General Ledger');", "fieldname": "show_net_values_in_party_account", "fieldtype": "Check", "label": "Show Net Values in Party Account" @@ -308,10 +319,59 @@ { "fieldname": "column_break_ocfq", "fieldtype": "Column Break" + }, + { + "fieldname": "report", + "fieldtype": "Select", + "label": "Report", + "options": "General Ledger\nAccounts Receivable", + "reqd": 1 + }, + { + "default": "Today", + "depends_on": "eval:(doc.report == 'Accounts Receivable');", + "fieldname": "posting_date", + "fieldtype": "Date", + "label": "Posting Date" + }, + { + "depends_on": "eval: (doc.report == 'Accounts Receivable');", + "fieldname": "payment_terms_template", + "fieldtype": "Link", + "label": "Payment Terms Template", + "options": "Payment Terms Template" + }, + { + "depends_on": "eval: (doc.report == 'Accounts Receivable');", + "fieldname": "sales_partner", + "fieldtype": "Link", + "label": "Sales Partner", + "options": "Sales Partner" + }, + { + "depends_on": "eval: (doc.report == 'Accounts Receivable');", + "fieldname": "sales_person", + "fieldtype": "Link", + "label": "Sales Person", + "options": "Sales Person" + }, + { + "depends_on": "eval: (doc.report == 'Accounts Receivable');", + "fieldname": "territory", + "fieldtype": "Link", + "label": "Territory", + "options": "Territory" + }, + { + "default": "0", + "depends_on": "eval:(doc.report == 'Accounts Receivable');", + "fieldname": "based_on_payment_terms", + "fieldtype": "Check", + "label": "Based On Payment Terms" } ], "links": [], - "modified": "2023-04-26 12:46:43.645455", + "modified": "2023-06-23 10:13:15.051950", "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 18d2f3b5a13..ab0baf6273b 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 @@ -14,6 +14,7 @@ from frappe.www.printview import get_print_style from erpnext import get_company_currency from erpnext.accounts.party import get_party_account_currency +from erpnext.accounts.report.accounts_receivable.accounts_receivable import execute as get_ar_soa from erpnext.accounts.report.accounts_receivable_summary.accounts_receivable_summary import ( execute as get_ageing, ) @@ -42,29 +43,10 @@ class ProcessStatementOfAccounts(Document): def get_report_pdf(doc, consolidated=True): statement_dict = {} ageing = "" - base_template_path = "frappe/www/printview.html" - template_path = ( - "erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.html" - ) for entry in doc.customers: if doc.include_ageing: - ageing_filters = frappe._dict( - { - "company": doc.company, - "report_date": doc.to_date, - "ageing_based_on": doc.ageing_based_on, - "range1": 30, - "range2": 60, - "range3": 90, - "range4": 120, - "customer": entry.customer, - } - ) - col1, ageing = get_ageing(ageing_filters) - - if ageing: - ageing[0]["ageing_based_on"] = doc.ageing_based_on + ageing = set_ageing(doc, entry) tax_id = frappe.get_doc("Customer", entry.customer).tax_id presentation_currency = ( @@ -72,59 +54,25 @@ def get_report_pdf(doc, consolidated=True): or doc.currency or get_company_currency(doc.company) ) - if doc.letter_head: - from frappe.www.printview import get_letter_head - letter_head = get_letter_head(doc, 0) + filters = get_common_filters(doc) - filters = frappe._dict( - { - "from_date": doc.from_date, - "to_date": doc.to_date, - "company": doc.company, - "finance_book": doc.finance_book if doc.finance_book else None, - "account": [doc.account] if doc.account else None, - "party_type": "Customer", - "party": [entry.customer], - "party_name": [entry.customer_name] if entry.customer_name else None, - "presentation_currency": presentation_currency, - "group_by": doc.group_by, - "currency": doc.currency, - "cost_center": [cc.cost_center_name for cc in doc.cost_center], - "project": [p.project_name for p in doc.project], - "show_opening_entries": 0, - "include_default_book_entries": 0, - "tax_id": tax_id if tax_id else None, - } - ) - col, res = get_soa(filters) + if doc.report == "General Ledger": + filters.update(get_gl_filters(doc, entry, tax_id, presentation_currency)) + else: + filters.update(get_ar_filters(doc, entry)) - for x in [0, -2, -1]: - res[x]["account"] = res[x]["account"].replace("'", "") + if doc.report == "General Ledger": + col, res = get_soa(filters) + for x in [0, -2, -1]: + res[x]["account"] = res[x]["account"].replace("'", "") + if len(res) == 3: + continue + else: + ar_res = get_ar_soa(filters) + col, res = ar_res[0], ar_res[1] - if len(res) == 3: - continue - - html = frappe.render_template( - template_path, - { - "filters": filters, - "data": res, - "ageing": ageing[0] if (doc.include_ageing and ageing) else None, - "letter_head": letter_head if doc.letter_head else None, - "terms_and_conditions": frappe.db.get_value( - "Terms and Conditions", doc.terms_and_conditions, "terms" - ) - if doc.terms_and_conditions - else None, - }, - ) - - html = frappe.render_template( - base_template_path, - {"body": html, "css": get_print_style(), "title": "Statement For " + entry.customer}, - ) - statement_dict[entry.customer] = html + statement_dict[entry.customer] = get_html(doc, filters, entry, col, res, ageing) if not bool(statement_dict): return False @@ -137,6 +85,110 @@ def get_report_pdf(doc, consolidated=True): return statement_dict +def set_ageing(doc, entry): + ageing_filters = frappe._dict( + { + "company": doc.company, + "report_date": doc.to_date, + "ageing_based_on": doc.ageing_based_on, + "range1": 30, + "range2": 60, + "range3": 90, + "range4": 120, + "customer": entry.customer, + } + ) + col1, ageing = get_ageing(ageing_filters) + + if ageing: + ageing[0]["ageing_based_on"] = doc.ageing_based_on + + return ageing + + +def get_common_filters(doc): + return frappe._dict( + { + "company": doc.company, + "finance_book": doc.finance_book if doc.finance_book else None, + "account": [doc.account] if doc.account else None, + "cost_center": [cc.cost_center_name for cc in doc.cost_center], + } + ) + + +def get_gl_filters(doc, entry, tax_id, presentation_currency): + return { + "from_date": doc.from_date, + "to_date": doc.to_date, + "party_type": "Customer", + "party": [entry.customer], + "party_name": [entry.customer_name] if entry.customer_name else None, + "presentation_currency": presentation_currency, + "group_by": doc.group_by, + "currency": doc.currency, + "project": [p.project_name for p in doc.project], + "show_opening_entries": 0, + "include_default_book_entries": 0, + "tax_id": tax_id if tax_id else None, + "show_net_values_in_party_account": doc.show_net_values_in_party_account, + } + + +def get_ar_filters(doc, entry): + return { + "report_date": doc.posting_date if doc.posting_date else None, + "customer_name": entry.customer, + "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, + "territory": doc.territory if doc.territory else None, + "based_on_payment_terms": doc.based_on_payment_terms, + "report_name": "Accounts Receivable", + "ageing_based_on": doc.ageing_based_on, + "range1": 30, + "range2": 60, + "range3": 90, + "range4": 120, + } + + +def get_html(doc, filters, entry, col, res, ageing): + base_template_path = "frappe/www/printview.html" + template_path = ( + "erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.html" + if doc.report == "General Ledger" + else "erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts_accounts_receivable.html" + ) + + if doc.letter_head: + from frappe.www.printview import get_letter_head + + letter_head = get_letter_head(doc, 0) + + html = frappe.render_template( + template_path, + { + "filters": filters, + "data": res, + "report": {"report_name": doc.report, "columns": col}, + "ageing": ageing[0] if (doc.include_ageing and ageing) else None, + "letter_head": letter_head if doc.letter_head else None, + "terms_and_conditions": frappe.db.get_value( + "Terms and Conditions", doc.terms_and_conditions, "terms" + ) + if doc.terms_and_conditions + else None, + }, + ) + + html = frappe.render_template( + base_template_path, + {"body": html, "css": get_print_style(), "title": "Statement For " + entry.customer}, + ) + return html + + def get_customers_based_on_territory_or_customer_group(customer_collection, collection_name): fields_dict = { "Customer Group": "customer_group", 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 new file mode 100644 index 00000000000..07e1896292d --- /dev/null +++ b/erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts_accounts_receivable.html @@ -0,0 +1,348 @@ + + +

{{ _(report.report_name) }}

+

+ {% if (filters.customer_name) %} + {{ filters.customer_name }} + {% else %} + {{ filters.customer ~ filters.supplier }} + {% endif %} +

+
+ {% if (filters.tax_id) %} + {{ _("Tax Id: ") }}{{ filters.tax_id }} + {% endif %} +
+
+ {{ _(filters.ageing_based_on) }} + {{ _("Until") }} + {{ frappe.format(filters.report_date, 'Date') }} +
+ +
+
+ {% if(filters.payment_terms) %} + {{ _("Payment Terms") }}: {{ filters.payment_terms }} + {% endif %} +
+
+ {% if(filters.credit_limit) %} + {{ _("Credit Limit") }}: {{ frappe.utils.fmt_money(filters.credit_limit) }} + {% endif %} +
+
+ + {% if(filters.show_future_payments) %} + {% set balance_row = data.slice(-1).pop() %} + {% for i in report.columns %} + {% if i.fieldname == 'age' %} + {% set elem = i %} + {% endif %} + {% endfor %} + {% set start = report.columns.findIndex(elem) %} + {% set range1 = report.columns[start].label %} + {% set range2 = report.columns[start+1].label %} + {% set range3 = report.columns[start+2].label %} + {% set range4 = report.columns[start+3].label %} + {% set range5 = report.columns[start+4].label %} + {% set range6 = report.columns[start+5].label %} + + {% if(balance_row) %} + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
(Amount in {{ data[0]["currency"] ~ "" }})
{{ _(" ") }}{{ _(range1) }}{{ _(range2) }}{{ _(range3) }}{{ _(range4) }}{{ _(range5) }}{{ _(range6) }}{{ _("Total") }}
{{ _("Total Outstanding") }} + {{ format_number(balance_row["age"], null, 2) }} + + {{ frappe.utils.fmt_money(balance_row["range1"], data[data.length-1]["currency"]) }} + + {{ frappe.utils.fmt_money(balance_row["range2"], data[data.length-1]["currency"]) }} + + {{ frappe.utils.fmt_money(balance_row["range3"], data[data.length-1]["currency"]) }} + + {{ frappe.utils.fmt_money(balance_row["range4"], data[data.length-1]["currency"]) }} + + {{ frappe.utils.fmt_money(balance_row["range5"], data[data.length-1]["currency"]) }} + + {{ frappe.utils.fmt_money(flt(balance_row["outstanding"]), data[data.length-1]["currency"]) }} +
{{ _("Future Payments") }} + {{ frappe.utils.fmt_money(flt(balance_row[("future_amount")]), data[data.length-1]["currency"]) }} +
+ {% endif %} + {% endif %} + + + + {% if(report.report_name == "Accounts Receivable" or report.report_name == "Accounts Payable") %} + + + + {% if(report.report_name == "Accounts Receivable" and filters.show_sales_person) %} + + + {% else %} + + {% endif %} + {% if not(filters.show_future_payments) %} + + {% endif %} + + {% if not(filters.show_future_payments) %} + + + {% endif %} + + {% if(filters.show_future_payments) %} + {% if(report.report_name == "Accounts Receivable") %} + + {% endif %} + + + + {% endif %} + {% else %} + + + + + + {% endif %} + + + + {% for i in range(data|length) %} + + {% if(report.report_name == "Accounts Receivable" or report.report_name == "Accounts Payable") %} + {% if(data[i]["party"]) %} + + + + + {% if(report.report_name == "Accounts Receivable" and filters.show_sales_person) %} + + {% endif %} + + {% if not (filters.show_future_payments) %} + + {% endif %} + + + + {% if not(filters.show_future_payments) %} + + + {% endif %} + + + {% if(filters.show_future_payments) %} + {% if(report.report_name == "Accounts Receivable") %} + + {% endif %} + + + + {% endif %} + {% else %} + + {% if not(filters.show_future_payments) %} + + {% endif %} + {% if(report.report_name == "Accounts Receivable" and filters.show_sales_person) %} + + {% endif %} + + + + + {% if not(filters.show_future_payments) %} + + + {% endif %} + + + {% if(filters.show_future_payments) %} + {% if(report.report_name == "Accounts Receivable") %} + + {% endif %} + + + + {% endif %} + {% endif %} + {% else %} + {% if(data[i]["party"] or " ") %} + {% if not(data[i]["is_total_row"]) %} + + {% else %} + + {% endif %} + + + + + {% endif %} + {% endif %} + + {% endfor %} + + + + + + + + + +
{{ _("Date") }}{{ _("Age (Days)") }}{{ _("Reference") }}{{ _("Sales Person") }}{{ _("Reference") }} + {% if (filters.customer or filters.supplier or filters.customer_name) %} + {{ _("Remarks") }} + {% else %} + {{ _("Party") }} + {% endif %} + {{ _("Invoiced Amount") }}{{ _("Paid Amount") }} + {% if report.report_name == "Accounts Receivable" %} + {{ _('Credit Note') }} + {% else %} + {{ _('Debit Note') }} + {% endif %} + {{ _("Outstanding Amount") }}{{ _("Customer LPO No.") }}{{ _("Future Payment Ref") }}{{ _("Future Payment Amount") }}{{ _("Remaining Balance") }} + {% if (filters.customer or filters.supplier or filters.customer_name) %} + {{ _("Remarks")}} + {% else %} + {{ _("Party") }} + {% endif %} + {{ _("Total Invoiced Amount") }}{{ _("Total Paid Amount") }} + {% if report.report_name == "Accounts Receivable Summary" %} + {{ _('Credit Note Amount') }} + {% else %} + {{ _('Debit Note Amount') }} + {% endif %} + {{ _("Total Outstanding Amount") }}
{{ (data[i]["posting_date"]) }}{{ data[i]["age"] }} + {% if not(filters.show_future_payments) %} + {{ data[i]["voucher_type"] }} +
+ {% endif %} + {{ data[i]["voucher_no"] }} +
{{ data[i]["sales_person"] }} + {% if(not(filters.customer or filters.supplier or filters.customer_name)) %} + {{ data[i]["party"] }} + {% if(data[i]["customer_name"] and data[i]["customer_name"] != data[i]["party"]) %} +
{{ data[i]["customer_name"] }} + {% elif(data[i]["supplier_name"] != data[i]["party"]) %} +
{{ data[i]["supplier_name"] }} + {% endif %} + {% endif %} +
+ {% if data[i]["remarks"] %} + {{ _("Remarks") }}: + {{ data[i]["remarks"] }} + {% endif %} +
+
+ {{ frappe.utils.fmt_money(data[i]["invoiced"], currency=data[i]["currency"]) }} + {{ frappe.utils.fmt_money(data[i]["paid"], currency=data[i]["currency"]) }} + {{ frappe.utils.fmt_money(data[i]["credit_note"], currency=data[i]["currency"]) }} + {{ frappe.utils.fmt_money(data[i]["outstanding"], currency=data[i]["currency"]) }} + {{ data[i]["po_no"] }}{{ data[i]["future_ref"] }}{{ frappe.utils.fmt_money(data[i]["future_amount"], currency=data[i]["currency"]) }}{{ frappe.utils.fmt_money(data[i]["remaining_balance"], currency=data[i]["currency"]) }}{{ _("Total") }} + {{ frappe.utils.fmt_money(data[i]["invoiced"], data[i]["currency"]) }} + {{ frappe.utils.fmt_money(data[i]["paid"], currency=data[i]["currency"]) }}{{ frappe.utils.fmt_money(data[i]["credit_note"], currency=data[i]["currency"]) }} + {{ frappe.utils.fmt_money(data[i]["outstanding"], currency=data[i]["currency"]) }} + {{ data[i]["po_no"] }}{{ data[i]["future_ref"] }}{{ frappe.utils.fmt_money(data[i]["future_amount"], currency=data[i]["currency"]) }}{{ frappe.utils.fmt_money(data[i]["remaining_balance"], currency=data[i]["currency"]) }} + {% if(not(filters.customer | filters.supplier)) %} + {{ data[i]["party"] }} + {% if(data[i]["customer_name"] and data[i]["customer_name"] != data[i]["party"]) %} +
{{ data[i]["customer_name"] }} + {% elif(data[i]["supplier_name"] != data[i]["party"]) %} +
{{ data[i]["supplier_name"] }} + {% endif %} + {% endif %} +
{{ _("Remarks") }}: + {{ data[i]["remarks"] }} +
{{ _("Total") }}{{ frappe.utils.fmt_money(data[i]["invoiced"], currency=data[i]["currency"]) }}{{ frappe.utils.fmt_money(data[i]["paid"], currency=data[i]["currency"]) }}{{ frappe.utils.fmt_money(data[i]["credit_note"], currency=data[i]["currency"]) }}{{ frappe.utils.fmt_money(data[i]["outstanding"], currency=data[i]["currency"]) }}
{{ frappe.utils.fmt_money(data|sum(attribute="invoiced"), currency=data[0]["currency"]) }}{{ frappe.utils.fmt_money(data|sum(attribute="paid"), currency=data[0]["currency"]) }}{{ frappe.utils.fmt_money(data|sum(attribute="credit_note"), currency=data[0]["currency"]) }}{{ frappe.utils.fmt_money(data|sum(attribute="outstanding"), currency=data[0]["currency"]) }}
+
+ {% if ageing %} +

{{ _("Ageing Report based on ") }} {{ ageing.ageing_based_on }} + {{ _("up to " ) }} {{ frappe.format(filters.report_date, 'Date')}} +

+ + + + + + + + + + + + + + + + + +
30 Days60 Days90 Days120 Days
{{ frappe.utils.fmt_money(ageing.range1, currency=data[0]["currency"]) }}{{ frappe.utils.fmt_money(ageing.range2, currency=data[0]["currency"]) }}{{ frappe.utils.fmt_money(ageing.range3, currency=data[0]["currency"]) }}{{ frappe.utils.fmt_money(ageing.range4, currency=data[0]["currency"]) }}
+ {% endif %} +

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

diff --git a/erpnext/accounts/report/accounts_receivable/accounts_receivable.html b/erpnext/accounts/report/accounts_receivable/accounts_receivable.html index f2bf9424f72..ed3b9915591 100644 --- a/erpnext/accounts/report/accounts_receivable/accounts_receivable.html +++ b/erpnext/accounts/report/accounts_receivable/accounts_receivable.html @@ -284,4 +284,4 @@ {% } %} -

{{ __("Printed On ") }}{%= frappe.datetime.str_to_user(frappe.datetime.get_datetime_as_string()) %}

+

{{ __("Printed On ") }}{%= frappe.datetime.str_to_user(frappe.datetime.get_datetime_as_string()) %}

\ No newline at end of file diff --git a/erpnext/patches.txt b/erpnext/patches.txt index 205047602a5..390e7219522 100644 --- a/erpnext/patches.txt +++ b/erpnext/patches.txt @@ -334,3 +334,4 @@ 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 diff --git a/erpnext/patches/v14_0/set_report_in_process_SOA.py b/erpnext/patches/v14_0/set_report_in_process_SOA.py new file mode 100644 index 00000000000..9eb5e3ab9bd --- /dev/null +++ b/erpnext/patches/v14_0/set_report_in_process_SOA.py @@ -0,0 +1,10 @@ +# Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and Contributors +# License: MIT. See LICENSE + +import frappe + + +def execute(): + process_soa = frappe.qb.DocType("Process Statement Of Accounts") + q = frappe.qb.update(process_soa).set(process_soa.report, "General Ledger") + q.run() From 3ca4f24d213b3119d6134b117c3f01ad87e82a1b Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Mon, 26 Jun 2023 15:31:20 +0530 Subject: [PATCH 20/26] refactor: simplify exchange logic on cr/dr note reconciliation (cherry picked from commit af75f6cea7aac9f765201b8c97b687dec5dc67de) --- .../payment_reconciliation.py | 38 ++++++++++--------- 1 file changed, 20 insertions(+), 18 deletions(-) diff --git a/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py b/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py index a709740d2fe..216d4eccac7 100644 --- a/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py +++ b/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py @@ -336,7 +336,6 @@ class PaymentReconciliation(Document): entry_list = [] dr_or_cr_notes = [] - difference_entries = [] for row in self.get("allocation"): reconciled_entry = [] if row.invoice_number and row.allocated_amount: @@ -348,16 +347,17 @@ class PaymentReconciliation(Document): payment_details = self.get_payment_details(row, dr_or_cr) reconciled_entry.append(payment_details) - if payment_details.difference_amount: - difference_entries.append( - self.make_difference_entry(payment_details, do_not_save_and_submit=bool(dr_or_cr_notes)) - ) + 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) if dr_or_cr_notes: - reconcile_dr_cr_note(dr_or_cr_notes, difference_entries, self.company) + reconcile_dr_cr_note(dr_or_cr_notes, self.company) @frappe.whitelist() def reconcile(self): @@ -385,7 +385,7 @@ class PaymentReconciliation(Document): self.get_unreconciled_entries() - def make_difference_entry(self, row, do_not_save_and_submit=False): + 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 @@ -433,9 +433,8 @@ class PaymentReconciliation(Document): journal_entry.append("accounts", journal_account) - if not do_not_save_and_submit: - journal_entry.save() - journal_entry.submit() + journal_entry.save() + journal_entry.submit() return journal_entry @@ -603,13 +602,16 @@ class PaymentReconciliation(Document): return condition -def reconcile_dr_cr_note(dr_cr_notes, difference_entries, company): - def find_difference_entry(voucher_type, voucher_no): - for jv in difference_entries: - accounts = iter(jv.accounts) - for account in accounts: - if account.reference_type == voucher_type and account.reference_name == voucher_no: - return next(accounts) +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" @@ -656,7 +658,7 @@ def reconcile_dr_cr_note(dr_cr_notes, difference_entries, company): } ) - if difference_entry := find_difference_entry(inv.against_voucher_type, inv.against_voucher): + if difference_entry := get_difference_row(inv): jv.append("accounts", difference_entry) jv.flags.ignore_mandatory = True From 851b8871b239f56d4625a80e8bd531d642e9b066 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Mon, 26 Jun 2023 22:01:12 +0530 Subject: [PATCH 21/26] fix: TDS amount calculation post LDC breach (cherry picked from commit 1f9ef6c48faf330c47daafae485ac833489db08d) --- .../tax_withholding_category/tax_withholding_category.py | 4 +++- 1 file changed, 3 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 d8827e09662..3764979bdfd 100644 --- a/erpnext/accounts/doctype/tax_withholding_category/tax_withholding_category.py +++ b/erpnext/accounts/doctype/tax_withholding_category/tax_withholding_category.py @@ -573,7 +573,9 @@ def get_tds_amount_from_ldc(ldc, parties, tax_details, posting_date, net_total): "supplier": ("in", parties), "apply_tds": 1, "docstatus": 1, + "tax_withholding_category": ldc.tax_withholding_category, "posting_date": ("between", (ldc.valid_from, ldc.valid_upto)), + "company": ldc.company, }, "sum(tax_withholding_net_total)", ) @@ -603,7 +605,7 @@ def is_valid_certificate( ): valid = False - available_amount = flt(certificate_limit) - flt(deducted_amount) - flt(current_amount) + available_amount = flt(certificate_limit) - flt(deducted_amount) if (getdate(valid_from) <= getdate(posting_date) <= getdate(valid_upto)) and available_amount > 0: valid = True From fc051d143c14d137908aa09c5cac83c7133392ba Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Tue, 27 Jun 2023 11:38:44 +0530 Subject: [PATCH 22/26] fix: delivery trip driver is only required on submit (backport #35876) (#35893) fix: delivery trip driver is only required on submit (#35876) This allows drafting trips and stops without yet deciding on the assignable driver which, in real life, may well be decided on after preparing and planning the trip. (cherry picked from commit 742df8a25e6249e67eb755e449bdbe969e052b7e) Co-authored-by: David Arnold --- erpnext/stock/doctype/delivery_trip/delivery_trip.json | 7 ++++--- erpnext/stock/doctype/delivery_trip/delivery_trip.py | 3 +++ 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/erpnext/stock/doctype/delivery_trip/delivery_trip.json b/erpnext/stock/doctype/delivery_trip/delivery_trip.json index 11b71c20761..9d8fe46e8ca 100644 --- a/erpnext/stock/doctype/delivery_trip/delivery_trip.json +++ b/erpnext/stock/doctype/delivery_trip/delivery_trip.json @@ -66,8 +66,7 @@ "fieldname": "driver", "fieldtype": "Link", "label": "Driver", - "options": "Driver", - "reqd": 1 + "options": "Driver" }, { "fetch_from": "driver.full_name", @@ -189,10 +188,11 @@ ], "is_submittable": 1, "links": [], - "modified": "2021-04-30 21:21:36.610142", + "modified": "2023-06-27 11:22:27.927637", "modified_by": "Administrator", "module": "Stock", "name": "Delivery Trip", + "naming_rule": "By \"Naming Series\" field", "owner": "Administrator", "permissions": [ { @@ -228,5 +228,6 @@ ], "sort_field": "modified", "sort_order": "DESC", + "states": [], "title_field": "driver_name" } \ No newline at end of file diff --git a/erpnext/stock/doctype/delivery_trip/delivery_trip.py b/erpnext/stock/doctype/delivery_trip/delivery_trip.py index 1febbded52b..af2f4113e1e 100644 --- a/erpnext/stock/doctype/delivery_trip/delivery_trip.py +++ b/erpnext/stock/doctype/delivery_trip/delivery_trip.py @@ -24,6 +24,9 @@ class DeliveryTrip(Document): ) def validate(self): + if self._action == "submit" and not self.driver: + frappe.throw(_("A driver must be set to submit.")) + self.validate_stop_addresses() def on_submit(self): From 2ef2057f44690565d33b7d4c1695e4ec440e3114 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Tue, 27 Jun 2023 12:30:23 +0530 Subject: [PATCH 23/26] fix(ux): PO Get Items From Open Material Requests (backport #35894) (#35895) fix(ux): PO Get Items From Open Material Requests (cherry picked from commit 3a00bf83d68be5f083f9290f9cfcdf756e67570a) 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 c6c9f1f98a3..8fa8f305549 100644 --- a/erpnext/buying/doctype/purchase_order/purchase_order.js +++ b/erpnext/buying/doctype/purchase_order/purchase_order.js @@ -286,7 +286,7 @@ erpnext.buying.PurchaseOrderController = class PurchaseOrderController extends e source_name: this.frm.doc.supplier, target: this.frm, setters: { - company: me.frm.doc.company + company: this.frm.doc.company }, get_query_filters: { docstatus: ["!=", 2], From 87ba19647311c1b7b7b8aa7724b1bd773dfce440 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Tue, 27 Jun 2023 14:19:57 +0530 Subject: [PATCH 24/26] fix: filter parent warehouses not showing (backport #35897) (#35899) fix: filter parent warehouses not showing (#35897) (cherry picked from commit af418d2342281c5a1fe6291971ee94e2eca42b3b) Co-authored-by: HLD --- erpnext/stock/doctype/warehouse/warehouse.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/stock/doctype/warehouse/warehouse.js b/erpnext/stock/doctype/warehouse/warehouse.js index 87a23efc590..746a1cbaf17 100644 --- a/erpnext/stock/doctype/warehouse/warehouse.js +++ b/erpnext/stock/doctype/warehouse/warehouse.js @@ -13,7 +13,7 @@ frappe.ui.form.on("Warehouse", { }; }); - frm.set_query("parent_warehouse", function () { + frm.set_query("parent_warehouse", function (doc) { return { filters: { is_group: 1, From 33ee01174bb909ee46bb3d714d6f94fa0aefe48f Mon Sep 17 00:00:00 2001 From: Anand Baburajan Date: Wed, 28 Jun 2023 09:49:30 +0530 Subject: [PATCH 25/26] perf: improve item wise register reports (#35908) --- .../item_wise_purchase_register.py | 31 ++++++++++++------- .../item_wise_sales_register.py | 30 ++++++++++++------ 2 files changed, 39 insertions(+), 22 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 924c14bdb94..6fdb2f337c0 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 @@ -15,7 +15,6 @@ from erpnext.accounts.report.item_wise_sales_register.item_wise_sales_register i get_group_by_conditions, get_tax_accounts, ) -from erpnext.selling.report.item_wise_sales_history.item_wise_sales_history import get_item_details def execute(filters=None): @@ -40,6 +39,16 @@ def _execute(filters=None, additional_table_columns=None, additional_query_colum tax_doctype="Purchase Taxes and Charges", ) + scrubbed_tax_fields = {} + + for tax in tax_columns: + scrubbed_tax_fields.update( + { + tax + " Rate": frappe.scrub(tax + " Rate"), + tax + " Amount": frappe.scrub(tax + " Amount"), + } + ) + po_pr_map = get_purchase_receipts_against_purchase_order(item_list) data = [] @@ -50,11 +59,7 @@ def _execute(filters=None, additional_table_columns=None, additional_query_colum if filters.get("group_by"): grand_total = get_grand_total(filters, "Purchase Invoice") - item_details = get_item_details() - for d in item_list: - item_record = item_details.get(d.item_code) - purchase_receipt = None if d.purchase_receipt: purchase_receipt = d.purchase_receipt @@ -67,8 +72,8 @@ def _execute(filters=None, additional_table_columns=None, additional_query_colum row = { "item_code": d.item_code, - "item_name": item_record.item_name if item_record else d.item_name, - "item_group": item_record.item_group if item_record else d.item_group, + "item_name": d.pi_item_name if d.pi_item_name else d.i_item_name, + "item_group": d.pi_item_group if d.pi_item_group else d.i_item_group, "description": d.description, "invoice": d.parent, "posting_date": d.posting_date, @@ -101,8 +106,8 @@ def _execute(filters=None, additional_table_columns=None, additional_query_colum item_tax = itemised_tax.get(d.name, {}).get(tax, {}) row.update( { - frappe.scrub(tax + " Rate"): item_tax.get("tax_rate", 0), - frappe.scrub(tax + " Amount"): item_tax.get("tax_amount", 0), + scrubbed_tax_fields[tax + " Rate"]: item_tax.get("tax_rate", 0), + scrubbed_tax_fields[tax + " Amount"]: item_tax.get("tax_amount", 0), } ) total_tax += flt(item_tax.get("tax_amount")) @@ -325,15 +330,17 @@ def get_items(filters, additional_query_columns): `tabPurchase Invoice`.supplier, `tabPurchase Invoice`.remarks, `tabPurchase Invoice`.base_net_total, `tabPurchase Invoice`.unrealized_profit_loss_account, `tabPurchase Invoice Item`.`item_code`, `tabPurchase Invoice Item`.description, - `tabPurchase Invoice Item`.`item_name`, `tabPurchase Invoice Item`.`item_group`, + `tabPurchase Invoice Item`.`item_name` as pi_item_name, `tabPurchase Invoice Item`.`item_group` as pi_item_group, + `tabItem`.`item_name` as i_item_name, `tabItem`.`item_group` as i_item_group, `tabPurchase Invoice Item`.`project`, `tabPurchase Invoice Item`.`purchase_order`, `tabPurchase Invoice Item`.`purchase_receipt`, `tabPurchase Invoice Item`.`po_detail`, `tabPurchase Invoice Item`.`expense_account`, `tabPurchase Invoice Item`.`stock_qty`, `tabPurchase Invoice Item`.`stock_uom`, `tabPurchase Invoice Item`.`base_net_amount`, `tabPurchase Invoice`.`supplier_name`, `tabPurchase Invoice`.`mode_of_payment` {0} - from `tabPurchase Invoice`, `tabPurchase Invoice Item` + from `tabPurchase Invoice`, `tabPurchase Invoice Item`, `tabItem` where `tabPurchase Invoice`.name = `tabPurchase Invoice Item`.`parent` and - `tabPurchase Invoice`.docstatus = 1 %s + `tabItem`.name = `tabPurchase Invoice Item`.`item_code` and + `tabPurchase Invoice`.docstatus = 1 %s """.format( additional_query_columns ) 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 0ebe13f4f32..bd7d02e0430 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 @@ -11,7 +11,6 @@ from frappe.utils.xlsxutils import handle_html from erpnext.accounts.report.sales_register.sales_register import get_mode_of_payments from erpnext.selling.report.item_wise_sales_history.item_wise_sales_history import ( get_customer_details, - get_item_details, ) @@ -35,6 +34,16 @@ def _execute( if item_list: itemised_tax, tax_columns = get_tax_accounts(item_list, columns, company_currency) + scrubbed_tax_fields = {} + + for tax in tax_columns: + scrubbed_tax_fields.update( + { + tax + " Rate": frappe.scrub(tax + " Rate"), + tax + " Amount": frappe.scrub(tax + " Amount"), + } + ) + mode_of_payments = get_mode_of_payments(set(d.parent for d in item_list)) so_dn_map = get_delivery_notes_against_sales_order(item_list) @@ -47,11 +56,9 @@ def _execute( grand_total = get_grand_total(filters, "Sales Invoice") customer_details = get_customer_details() - item_details = get_item_details() for d in item_list: customer_record = customer_details.get(d.customer) - item_record = item_details.get(d.item_code) delivery_note = None if d.delivery_note: @@ -64,8 +71,8 @@ def _execute( row = { "item_code": d.item_code, - "item_name": item_record.item_name if item_record else d.item_name, - "item_group": item_record.item_group if item_record else d.item_group, + "item_name": d.si_item_name if d.si_item_name else d.i_item_name, + "item_group": d.si_item_group if d.si_item_group else d.i_item_group, "description": d.description, "invoice": d.parent, "posting_date": d.posting_date, @@ -107,8 +114,8 @@ def _execute( item_tax = itemised_tax.get(d.name, {}).get(tax, {}) row.update( { - frappe.scrub(tax + " Rate"): item_tax.get("tax_rate", 0), - frappe.scrub(tax + " Amount"): item_tax.get("tax_amount", 0), + scrubbed_tax_fields[tax + " Rate"]: item_tax.get("tax_rate", 0), + scrubbed_tax_fields[tax + " Amount"]: item_tax.get("tax_amount", 0), } ) if item_tax.get("is_other_charges"): @@ -404,15 +411,18 @@ def get_items(filters, additional_query_columns, additional_conditions=None): `tabSales Invoice Item`.project, `tabSales Invoice Item`.item_code, `tabSales Invoice Item`.description, `tabSales Invoice Item`.`item_name`, `tabSales Invoice Item`.`item_group`, + `tabSales Invoice Item`.`item_name` as si_item_name, `tabSales Invoice Item`.`item_group` as si_item_group, + `tabItem`.`item_name` as i_item_name, `tabItem`.`item_group` as i_item_group, `tabSales Invoice Item`.sales_order, `tabSales Invoice Item`.delivery_note, `tabSales Invoice Item`.income_account, `tabSales Invoice Item`.cost_center, `tabSales Invoice Item`.stock_qty, `tabSales Invoice Item`.stock_uom, `tabSales Invoice Item`.base_net_rate, `tabSales Invoice Item`.base_net_amount, `tabSales Invoice`.customer_name, `tabSales Invoice`.customer_group, `tabSales Invoice Item`.so_detail, `tabSales Invoice`.update_stock, `tabSales Invoice Item`.uom, `tabSales Invoice Item`.qty {0} - from `tabSales Invoice`, `tabSales Invoice Item` - where `tabSales Invoice`.name = `tabSales Invoice Item`.parent - and `tabSales Invoice`.docstatus = 1 {1} + from `tabSales Invoice`, `tabSales Invoice Item`, `tabItem` + where `tabSales Invoice`.name = `tabSales Invoice Item`.parent and + `tabItem`.name = `tabSales Invoice Item`.`item_code` and + `tabSales Invoice`.docstatus = 1 {1} """.format( additional_query_columns or "", conditions ), From e16c14863b52aaa7856c799ad64fe977d4a4fbbe Mon Sep 17 00:00:00 2001 From: Anand Baburajan Date: Wed, 28 Jun 2023 20:15:40 +0530 Subject: [PATCH 26/26] fix: asset movement (#35918) fix: asset movement fixes --- .../doctype/asset_movement/asset_movement.js | 18 +++++++++------- .../asset_movement/asset_movement.json | 7 +++++-- .../doctype/asset_movement/asset_movement.py | 19 +++++++---------- .../asset_movement/test_asset_movement.py | 21 ++++++++++++++----- 4 files changed, 38 insertions(+), 27 deletions(-) diff --git a/erpnext/assets/doctype/asset_movement/asset_movement.js b/erpnext/assets/doctype/asset_movement/asset_movement.js index 2df7db97446..f9c600731b3 100644 --- a/erpnext/assets/doctype/asset_movement/asset_movement.js +++ b/erpnext/assets/doctype/asset_movement/asset_movement.js @@ -70,19 +70,21 @@ frappe.ui.form.on('Asset Movement', { else if (frm.doc.purpose === 'Issue') { fieldnames_to_be_altered = { target_location: { read_only: 1, reqd: 0 }, - source_location: { read_only: 1, reqd: 1 }, + source_location: { read_only: 1, reqd: 0 }, from_employee: { read_only: 1, reqd: 0 }, to_employee: { read_only: 0, reqd: 1 } }; } - Object.keys(fieldnames_to_be_altered).forEach(fieldname => { - let property_to_be_altered = fieldnames_to_be_altered[fieldname]; - Object.keys(property_to_be_altered).forEach(property => { - let value = property_to_be_altered[property]; - frm.set_df_property(fieldname, property, value, cdn, 'assets'); + if (fieldnames_to_be_altered) { + Object.keys(fieldnames_to_be_altered).forEach(fieldname => { + let property_to_be_altered = fieldnames_to_be_altered[fieldname]; + Object.keys(property_to_be_altered).forEach(property => { + let value = property_to_be_altered[property]; + frm.fields_dict['assets'].grid.update_docfield_property(fieldname, property, value); + }); }); - }); - frm.refresh_field('assets'); + frm.refresh_field('assets'); + } } }); diff --git a/erpnext/assets/doctype/asset_movement/asset_movement.json b/erpnext/assets/doctype/asset_movement/asset_movement.json index bdce639b039..5382f9e75f2 100644 --- a/erpnext/assets/doctype/asset_movement/asset_movement.json +++ b/erpnext/assets/doctype/asset_movement/asset_movement.json @@ -37,6 +37,7 @@ "reqd": 1 }, { + "default": "Now", "fieldname": "transaction_date", "fieldtype": "Datetime", "in_list_view": 1, @@ -95,10 +96,11 @@ "index_web_pages_for_search": 1, "is_submittable": 1, "links": [], - "modified": "2021-01-22 12:30:55.295670", + "modified": "2023-06-28 16:54:26.571083", "modified_by": "Administrator", "module": "Assets", "name": "Asset Movement", + "naming_rule": "Expression", "owner": "Administrator", "permissions": [ { @@ -148,5 +150,6 @@ } ], "sort_field": "modified", - "sort_order": "DESC" + "sort_order": "DESC", + "states": [] } \ No newline at end of file diff --git a/erpnext/assets/doctype/asset_movement/asset_movement.py b/erpnext/assets/doctype/asset_movement/asset_movement.py index 143f215db2e..b58ca10482b 100644 --- a/erpnext/assets/doctype/asset_movement/asset_movement.py +++ b/erpnext/assets/doctype/asset_movement/asset_movement.py @@ -28,25 +28,20 @@ class AssetMovement(Document): def validate_location(self): for d in self.assets: if self.purpose in ["Transfer", "Issue"]: - if not d.source_location: - d.source_location = frappe.db.get_value("Asset", d.asset, "location") - - if not d.source_location: - frappe.throw(_("Source Location is required for the Asset {0}").format(d.asset)) - + current_location = frappe.db.get_value("Asset", d.asset, "location") if d.source_location: - current_location = frappe.db.get_value("Asset", d.asset, "location") - if current_location != d.source_location: frappe.throw( _("Asset {0} does not belongs to the location {1}").format(d.asset, d.source_location) ) + else: + d.source_location = current_location if self.purpose == "Issue": if d.target_location: frappe.throw( _( - "Issuing cannot be done to a location. Please enter employee who has issued Asset {0}" + "Issuing cannot be done to a location. Please enter employee to issue the Asset {0} to" ).format(d.asset), title=_("Incorrect Movement Purpose"), ) @@ -107,12 +102,12 @@ class AssetMovement(Document): ) def on_submit(self): - self.set_latest_location_in_asset() + self.set_latest_location_and_custodian_in_asset() def on_cancel(self): - self.set_latest_location_in_asset() + self.set_latest_location_and_custodian_in_asset() - def set_latest_location_in_asset(self): + def set_latest_location_and_custodian_in_asset(self): current_location, current_employee = "", "" cond = "1=1" diff --git a/erpnext/assets/doctype/asset_movement/test_asset_movement.py b/erpnext/assets/doctype/asset_movement/test_asset_movement.py index 72c05752c57..27e7e557f19 100644 --- a/erpnext/assets/doctype/asset_movement/test_asset_movement.py +++ b/erpnext/assets/doctype/asset_movement/test_asset_movement.py @@ -47,7 +47,7 @@ class TestAssetMovement(unittest.TestCase): if not frappe.db.exists("Location", "Test Location 2"): frappe.get_doc({"doctype": "Location", "location_name": "Test Location 2"}).insert() - movement1 = create_asset_movement( + create_asset_movement( purpose="Transfer", company=asset.company, assets=[ @@ -58,7 +58,7 @@ class TestAssetMovement(unittest.TestCase): ) self.assertEqual(frappe.db.get_value("Asset", asset.name, "location"), "Test Location 2") - create_asset_movement( + movement1 = create_asset_movement( purpose="Transfer", company=asset.company, assets=[ @@ -70,21 +70,32 @@ class TestAssetMovement(unittest.TestCase): self.assertEqual(frappe.db.get_value("Asset", asset.name, "location"), "Test Location") movement1.cancel() - self.assertEqual(frappe.db.get_value("Asset", asset.name, "location"), "Test Location") + self.assertEqual(frappe.db.get_value("Asset", asset.name, "location"), "Test Location 2") employee = make_employee("testassetmovemp@example.com", company="_Test Company") create_asset_movement( purpose="Issue", company=asset.company, - assets=[{"asset": asset.name, "source_location": "Test Location", "to_employee": employee}], + assets=[{"asset": asset.name, "source_location": "Test Location 2", "to_employee": employee}], reference_doctype="Purchase Receipt", reference_name=pr.name, ) - # after issuing asset should belong to an employee not at a location + # after issuing, asset should belong to an employee not at a location self.assertEqual(frappe.db.get_value("Asset", asset.name, "location"), None) self.assertEqual(frappe.db.get_value("Asset", asset.name, "custodian"), employee) + create_asset_movement( + purpose="Receipt", + company=asset.company, + assets=[{"asset": asset.name, "from_employee": employee, "target_location": "Test Location"}], + reference_doctype="Purchase Receipt", + reference_name=pr.name, + ) + + # after receiving, asset should belong to a location not at an employee + self.assertEqual(frappe.db.get_value("Asset", asset.name, "location"), "Test Location") + def test_last_movement_cancellation(self): pr = make_purchase_receipt( item_code="Macbook Pro", qty=1, rate=100000.0, location="Test Location"