From c9418aab45a147b2654de60f3817006a25e36d62 Mon Sep 17 00:00:00 2001 From: s-aga-r Date: Tue, 4 Apr 2023 16:57:14 +0530 Subject: [PATCH 01/75] chore: add items field label --- erpnext/buying/doctype/purchase_order/purchase_order.json | 6 +++--- .../doctype/supplier_quotation/supplier_quotation.json | 4 ++-- erpnext/selling/doctype/quotation/quotation.json | 4 ++-- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/erpnext/buying/doctype/purchase_order/purchase_order.json b/erpnext/buying/doctype/purchase_order/purchase_order.json index 29afc8476e..ff08ddd33d 100644 --- a/erpnext/buying/doctype/purchase_order/purchase_order.json +++ b/erpnext/buying/doctype/purchase_order/purchase_order.json @@ -495,6 +495,7 @@ "allow_bulk_edit": 1, "fieldname": "items", "fieldtype": "Table", + "label": "Items", "oldfieldname": "po_details", "oldfieldtype": "Table", "options": "Purchase Order Item", @@ -1100,8 +1101,7 @@ { "fieldname": "before_items_section", "fieldtype": "Section Break", - "hide_border": 1, - "label": "Items" + "hide_border": 1 }, { "fieldname": "items_col_break", @@ -1271,7 +1271,7 @@ "idx": 105, "is_submittable": 1, "links": [], - "modified": "2023-01-28 18:59:16.322824", + "modified": "2023-04-14 16:42:29.448464", "modified_by": "Administrator", "module": "Buying", "name": "Purchase Order", diff --git a/erpnext/buying/doctype/supplier_quotation/supplier_quotation.json b/erpnext/buying/doctype/supplier_quotation/supplier_quotation.json index c5b369bedd..11ff91af94 100644 --- a/erpnext/buying/doctype/supplier_quotation/supplier_quotation.json +++ b/erpnext/buying/doctype/supplier_quotation/supplier_quotation.json @@ -310,7 +310,6 @@ "fieldname": "items_section", "fieldtype": "Section Break", "hide_border": 1, - "label": "Items", "oldfieldtype": "Section Break", "options": "fa fa-shopping-cart" }, @@ -318,6 +317,7 @@ "allow_bulk_edit": 1, "fieldname": "items", "fieldtype": "Table", + "label": "Items", "oldfieldname": "po_details", "oldfieldtype": "Table", "options": "Supplier Quotation Item", @@ -844,7 +844,7 @@ "index_web_pages_for_search": 1, "is_submittable": 1, "links": [], - "modified": "2022-12-12 18:35:39.740974", + "modified": "2023-04-14 16:43:41.714832", "modified_by": "Administrator", "module": "Buying", "name": "Supplier Quotation", diff --git a/erpnext/selling/doctype/quotation/quotation.json b/erpnext/selling/doctype/quotation/quotation.json index eb2c0a48ac..2ffa6a5c12 100644 --- a/erpnext/selling/doctype/quotation/quotation.json +++ b/erpnext/selling/doctype/quotation/quotation.json @@ -416,7 +416,6 @@ "fieldname": "items_section", "fieldtype": "Section Break", "hide_border": 1, - "label": "Items", "oldfieldtype": "Section Break", "options": "fa fa-shopping-cart" }, @@ -424,6 +423,7 @@ "allow_bulk_edit": 1, "fieldname": "items", "fieldtype": "Table", + "label": "Items", "oldfieldname": "quotation_details", "oldfieldtype": "Table", "options": "Quotation Item", @@ -1072,7 +1072,7 @@ "idx": 82, "is_submittable": 1, "links": [], - "modified": "2022-12-12 18:32:28.671332", + "modified": "2023-04-14 16:50:44.550098", "modified_by": "Administrator", "module": "Selling", "name": "Quotation", From e4f152a41638ed91e505c3e48156cad6493d681f Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Tue, 18 Apr 2023 08:24:22 +0530 Subject: [PATCH 02/75] fix: whitelist doc method This should've been whitelisted, looks like it was missed out closes https://github.com/frappe/erpnext/issues/34898 --- erpnext/e_commerce/doctype/website_item/website_item.py | 1 + 1 file changed, 1 insertion(+) diff --git a/erpnext/e_commerce/doctype/website_item/website_item.py b/erpnext/e_commerce/doctype/website_item/website_item.py index 3e5d5f768f..81b8ecab48 100644 --- a/erpnext/e_commerce/doctype/website_item/website_item.py +++ b/erpnext/e_commerce/doctype/website_item/website_item.py @@ -315,6 +315,7 @@ class WebsiteItem(WebsiteGenerator): self.item_code, skip_quotation_creation=True ) + @frappe.whitelist() def copy_specification_from_item_group(self): self.set("website_specifications", []) if self.item_group: From cc185bd2feb6aad356d858c3301ba434e7bff66b Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Tue, 18 Apr 2023 08:40:24 +0530 Subject: [PATCH 03/75] chore: update codeowners [skip ci] --- CODEOWNERS | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CODEOWNERS b/CODEOWNERS index 7f8c4d1ac8..540680c796 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -22,4 +22,4 @@ erpnext/controllers/ @deepeshgarg007 @rohitwaghchaure erpnext/patches/ @deepeshgarg007 .github/ @deepeshgarg007 -pyproject.toml @ankush +pyproject.toml @phot0n From a3e3fe149d910a7613d6be54a5528c76a5e38e14 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Thu, 13 Apr 2023 09:59:35 +0530 Subject: [PATCH 04/75] refactor: checkbox to toggle merging of JE account heads --- .../accounts_settings/accounts_settings.json | 16 +++++++++++++++- .../doctype/journal_entry/journal_entry.py | 10 +++++++++- 2 files changed, 24 insertions(+), 2 deletions(-) diff --git a/erpnext/accounts/doctype/accounts_settings/accounts_settings.json b/erpnext/accounts/doctype/accounts_settings/accounts_settings.json index 5cecddd2a9..a354d7a36c 100644 --- a/erpnext/accounts/doctype/accounts_settings/accounts_settings.json +++ b/erpnext/accounts/doctype/accounts_settings/accounts_settings.json @@ -19,6 +19,8 @@ "column_break_17", "enable_common_party_accounting", "allow_multi_currency_invoices_against_single_party_account", + "journals_section", + "merge_similar_account_heads", "report_setting_section", "use_custom_cash_flow", "deferred_accounting_settings_section", @@ -369,6 +371,18 @@ "fieldname": "book_tax_discount_loss", "fieldtype": "Check", "label": "Book Tax Loss on Early Payment Discount" + }, + { + "fieldname": "journals_section", + "fieldtype": "Section Break", + "label": "Journals" + }, + { + "default": "0", + "description": "Rows with Same Account heads will be merged on Ledger", + "fieldname": "merge_similar_account_heads", + "fieldtype": "Check", + "label": "Merge Similar Account Heads" } ], "icon": "icon-cog", @@ -376,7 +390,7 @@ "index_web_pages_for_search": 1, "issingle": 1, "links": [], - "modified": "2023-04-14 17:22:03.680886", + "modified": "2023-04-17 11:45:42.049247", "modified_by": "Administrator", "module": "Accounts", "name": "Accounts Settings", diff --git a/erpnext/accounts/doctype/journal_entry/journal_entry.py b/erpnext/accounts/doctype/journal_entry/journal_entry.py index 68364beba2..0f8ae4f37d 100644 --- a/erpnext/accounts/doctype/journal_entry/journal_entry.py +++ b/erpnext/accounts/doctype/journal_entry/journal_entry.py @@ -885,6 +885,8 @@ class JournalEntry(AccountsController): def make_gl_entries(self, cancel=0, adv_adj=0): from erpnext.accounts.general_ledger import make_gl_entries + merge_entries = frappe.db.get_single_value("Accounts Settings", "merge_similar_account_heads") + gl_map = self.build_gl_map() if self.voucher_type in ("Deferred Revenue", "Deferred Expense"): update_outstanding = "No" @@ -892,7 +894,13 @@ class JournalEntry(AccountsController): update_outstanding = "Yes" if gl_map: - make_gl_entries(gl_map, cancel=cancel, adv_adj=adv_adj, update_outstanding=update_outstanding) + make_gl_entries( + gl_map, + cancel=cancel, + adv_adj=adv_adj, + merge_entries=merge_entries, + update_outstanding=update_outstanding, + ) @frappe.whitelist() def get_balance(self, difference_account=None): From 3f537d30bd484eb44c2ed8efb99267cd943555de Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Mon, 17 Apr 2023 17:18:07 +0530 Subject: [PATCH 05/75] chore(patch): by default ledger entries of JE's will not be merged --- erpnext/patches.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/erpnext/patches.txt b/erpnext/patches.txt index 3357b06fbb..74c8af1f0c 100644 --- a/erpnext/patches.txt +++ b/erpnext/patches.txt @@ -327,6 +327,7 @@ erpnext.patches.v15_0.update_asset_value_for_manual_depr_entries erpnext.patches.v15_0.update_gpa_and_ndb_for_assdeprsch erpnext.patches.v14_0.create_accounting_dimensions_for_closing_balance erpnext.patches.v14_0.update_closing_balances +execute:frappe.db.set_single_value("Accounts Settings", "merge_similar_account_heads", 0) # below migration patches should always run last erpnext.patches.v14_0.migrate_gl_to_payment_ledger execute:frappe.delete_doc_if_exists("Report", "Tax Detail") From 91a398a191a62ea9f7d5583c9fda6e7997337303 Mon Sep 17 00:00:00 2001 From: s-aga-r Date: Tue, 18 Apr 2023 12:55:16 +0530 Subject: [PATCH 06/75] fix: use CombineDatetime instead of Timestamp in QB queries --- erpnext/stock/doctype/batch/batch.py | 5 +++-- .../incorrect_stock_value_report.py | 4 ++-- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/erpnext/stock/doctype/batch/batch.py b/erpnext/stock/doctype/batch/batch.py index 3b9fe7b97c..e6f5527543 100644 --- a/erpnext/stock/doctype/batch/batch.py +++ b/erpnext/stock/doctype/batch/batch.py @@ -6,7 +6,7 @@ import frappe from frappe import _ from frappe.model.document import Document from frappe.model.naming import make_autoname, revert_series_if_last -from frappe.query_builder.functions import CurDate, Sum, Timestamp +from frappe.query_builder.functions import CombineDatetime, CurDate, Sum from frappe.utils import cint, flt, get_link_to_form, nowtime from frappe.utils.data import add_days from frappe.utils.jinja import render_template @@ -192,7 +192,8 @@ def get_batch_qty( posting_time = nowtime() query = query.where( - Timestamp(sle.posting_date, sle.posting_time) <= Timestamp(posting_date, posting_time) + CombineDatetime(sle.posting_date, sle.posting_time) + <= CombineDatetime(posting_date, posting_time) ) out = query.run(as_list=True)[0][0] or 0 diff --git a/erpnext/stock/report/incorrect_stock_value_report/incorrect_stock_value_report.py b/erpnext/stock/report/incorrect_stock_value_report/incorrect_stock_value_report.py index df01b14d11..e9c96084d9 100644 --- a/erpnext/stock/report/incorrect_stock_value_report/incorrect_stock_value_report.py +++ b/erpnext/stock/report/incorrect_stock_value_report/incorrect_stock_value_report.py @@ -5,7 +5,7 @@ import frappe from frappe import _ from frappe.query_builder import Field -from frappe.query_builder.functions import Min, Timestamp +from frappe.query_builder.functions import CombineDatetime, Min from frappe.utils import add_days, getdate, today import erpnext @@ -75,7 +75,7 @@ def get_data(report_filters): & (sle.company == report_filters.company) & (sle.is_cancelled == 0) ) - .orderby(Timestamp(sle.posting_date, sle.posting_time), sle.creation) + .orderby(CombineDatetime(sle.posting_date, sle.posting_time), sle.creation) ).run(as_dict=True) for d in data: From e91abbfbe30234a7c577bab13cacea9ab54b4a56 Mon Sep 17 00:00:00 2001 From: s-aga-r Date: Tue, 18 Apr 2023 12:04:34 +0530 Subject: [PATCH 07/75] fix: add item-code filter for SCR supplied-items batch-no --- .../subcontracting_receipt/subcontracting_receipt.js | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.js b/erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.js index 3a2c53f4e4..45289b1dab 100644 --- a/erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.js +++ b/erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.js @@ -67,6 +67,15 @@ frappe.ui.form.on('Subcontracting Receipt', { } }); + frm.set_query('batch_no', 'supplied_items', function(doc, cdt, cdn) { + var row = locals[cdt][cdn]; + return { + filters: { + item: row.rm_item_code + } + } + }); + let batch_no_field = frm.get_docfield("items", "batch_no"); if (batch_no_field) { batch_no_field.get_route_options_for_new_doc = function(row) { From 6fca9adcd4a881b09630f5e0ad875df136e1d6e0 Mon Sep 17 00:00:00 2001 From: s-aga-r Date: Tue, 18 Apr 2023 18:38:28 +0530 Subject: [PATCH 08/75] fix: internal Purchase Receipt GL Entries --- .../doctype/purchase_receipt/purchase_receipt.py | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py index d268cc1196..530427328a 100644 --- a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py +++ b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py @@ -380,7 +380,19 @@ class PurchaseReceipt(BuyingController): outgoing_amount = d.base_net_amount if self.is_internal_supplier and d.valuation_rate: - outgoing_amount = d.valuation_rate * d.stock_qty + outgoing_amount = abs( + frappe.db.get_value( + "Stock Ledger Entry", + { + "voucher_type": "Purchase Receipt", + "voucher_no": self.name, + "voucher_detail_no": d.name, + "warehouse": d.from_warehouse, + "is_cancelled": 0, + }, + "stock_value_difference", + ) + ) credit_amount = outgoing_amount if credit_amount: From 11cb2db3fee91877fc6c05ae692b8163ca61d84e Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Wed, 19 Apr 2023 11:51:08 +0530 Subject: [PATCH 09/75] refactor: move set_missing_ref_detials out of set_missing_values --- erpnext/accounts/doctype/payment_entry/payment_entry.py | 4 ++-- erpnext/accounts/utils.py | 1 + 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/erpnext/accounts/doctype/payment_entry/payment_entry.py b/erpnext/accounts/doctype/payment_entry/payment_entry.py index 3583dc7a90..086e1e3c35 100644 --- a/erpnext/accounts/doctype/payment_entry/payment_entry.py +++ b/erpnext/accounts/doctype/payment_entry/payment_entry.py @@ -60,6 +60,7 @@ class PaymentEntry(AccountsController): def validate(self): self.setup_party_account_field() self.set_missing_values() + self.set_missing_ref_details() self.validate_payment_type() self.validate_party_details() self.set_exchange_rate() @@ -219,8 +220,6 @@ class PaymentEntry(AccountsController): else self.paid_to_account_currency ) - self.set_missing_ref_details() - def set_missing_ref_details(self, force=False): for d in self.get("references"): if d.allocated_amount: @@ -1811,6 +1810,7 @@ def get_payment_entry( pe.setup_party_account_field() pe.set_missing_values() + pe.set_missing_ref_details() update_accounting_dimensions(pe, doc) diff --git a/erpnext/accounts/utils.py b/erpnext/accounts/utils.py index 2ab9ef64b3..f10cff0686 100644 --- a/erpnext/accounts/utils.py +++ b/erpnext/accounts/utils.py @@ -646,6 +646,7 @@ def update_reference_in_payment_entry(d, payment_entry, do_not_save=False): payment_entry.flags.ignore_validate_update_after_submit = True payment_entry.setup_party_account_field() payment_entry.set_missing_values() + payment_entry.set_missing_ref_details() payment_entry.set_amounts() if not do_not_save: From e43bc38e05349a780e0a4a97812cebea4a90a109 Mon Sep 17 00:00:00 2001 From: s-aga-r Date: Wed, 19 Apr 2023 12:05:17 +0530 Subject: [PATCH 10/75] refactor: rewrite `get_stock_value_on()` queries in `QB` --- erpnext/stock/utils.py | 49 +++++++++++++++++++++--------------------- 1 file changed, 25 insertions(+), 24 deletions(-) diff --git a/erpnext/stock/utils.py b/erpnext/stock/utils.py index b8c5187b2c..d928dca723 100644 --- a/erpnext/stock/utils.py +++ b/erpnext/stock/utils.py @@ -9,6 +9,7 @@ import frappe from frappe import _ from frappe.query_builder.functions import CombineDatetime from frappe.utils import cstr, flt, get_link_to_form, nowdate, nowtime +from pypika.terms import ExistsCriterion import erpnext from erpnext.stock.valuation import FIFOValuation, LIFOValuation @@ -57,39 +58,39 @@ def get_stock_value_on(warehouse=None, posting_date=None, item_code=None): if not posting_date: posting_date = nowdate() - values, condition = [posting_date], "" + sle = frappe.qb.DocType("Stock Ledger Entry") + query = ( + frappe.qb.from_(sle) + .select( + sle.item_code, + sle.stock_value, + sle.name, + sle.warehouse, + ) + .where((sle.posting_date <= posting_date) & (sle.is_cancelled == 0)) + .orderby(CombineDatetime(sle.posting_date, sle.posting_time), order=frappe.qb.desc) + .orderby(sle.creation, order=frappe.qb.desc) + ) if warehouse: - lft, rgt, is_group = frappe.db.get_value("Warehouse", warehouse, ["lft", "rgt", "is_group"]) if is_group: - values.extend([lft, rgt]) - condition += "and exists (\ - select name from `tabWarehouse` wh where wh.name = sle.warehouse\ - and wh.lft >= %s and wh.rgt <= %s)" - + wh = frappe.qb.DocType("Warehouse") + query = query.where( + ExistsCriterion( + frappe.qb.from_(wh) + .select(wh.name) + .where((wh.name == sle.warehouse) & (wh.lft >= lft) & (wh.rgt <= rgt)) + ) + ) else: - values.append(warehouse) - condition += " AND warehouse = %s" + query = query.where(sle.warehouse == warehouse) if item_code: - values.append(item_code) - condition += " AND item_code = %s" + query = query.where(sle.item_code == item_code) - stock_ledger_entries = frappe.db.sql( - """ - SELECT item_code, stock_value, name, warehouse - FROM `tabStock Ledger Entry` sle - WHERE posting_date <= %s {0} - and is_cancelled = 0 - ORDER BY timestamp(posting_date, posting_time) DESC, creation DESC - """.format( - condition - ), - values, - as_dict=1, - ) + stock_ledger_entries = query.run(as_dict=True) sle_map = {} for sle in stock_ledger_entries: From c86c543fbfaa46c45f06a745a0841e821e14e712 Mon Sep 17 00:00:00 2001 From: s-aga-r Date: Tue, 18 Apr 2023 21:03:23 +0530 Subject: [PATCH 11/75] test: add test case for internal PR GL Entries --- .../purchase_receipt/test_purchase_receipt.py | 83 +++++++++++++++++++ 1 file changed, 83 insertions(+) diff --git a/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py b/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py index 7567cfe98c..8af279a9c8 100644 --- a/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py +++ b/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py @@ -1610,6 +1610,89 @@ class TestPurchaseReceipt(FrappeTestCase): frappe.db.set_single_value("Stock Settings", "over_delivery_receipt_allowance", 0) + def test_internal_pr_gl_entries(self): + from erpnext.stock import get_warehouse_account_map + from erpnext.stock.doctype.delivery_note.delivery_note import make_inter_company_purchase_receipt + from erpnext.stock.doctype.delivery_note.test_delivery_note import create_delivery_note + from erpnext.stock.doctype.stock_entry.test_stock_entry import make_stock_entry + from erpnext.stock.doctype.stock_reconciliation.test_stock_reconciliation import ( + create_stock_reconciliation, + ) + + prepare_data_for_internal_transfer() + customer = "_Test Internal Customer 2" + company = "_Test Company with perpetual inventory" + from_warehouse = create_warehouse("_Test Internal From Warehouse New", company=company) + target_warehouse = create_warehouse("_Test Internal GIT Warehouse New", company=company) + to_warehouse = create_warehouse("_Test Internal To Warehouse New", company=company) + + item = make_item(properties={"is_stock_item": 1, "valuation_rate": 100}) + make_stock_entry( + purpose="Material Receipt", + item_code=item.name, + qty=10, + company=company, + to_warehouse=from_warehouse, + posting_date=add_days(today(), -3), + ) + + # Step - 1: Create Delivery Note with Internal Customer + dn = create_delivery_note( + item_code=item.name, + company=company, + customer=customer, + cost_center="Main - TCP1", + expense_account="Cost of Goods Sold - TCP1", + qty=10, + rate=100, + warehouse=from_warehouse, + target_warehouse=target_warehouse, + posting_date=add_days(today(), -2), + ) + + # Step - 2: Create Internal Purchase Receipt + pr = make_inter_company_purchase_receipt(dn.name) + pr.items[0].qty = 10 + pr.items[0].from_warehouse = target_warehouse + pr.items[0].warehouse = to_warehouse + pr.items[0].rejected_warehouse = from_warehouse + pr.save() + pr.submit() + + # Step - 3: Create back-date Stock Reconciliation [After DN and Before PR] + create_stock_reconciliation( + item_code=item, + warehouse=target_warehouse, + qty=10, + rate=50, + company=company, + posting_date=add_days(today(), -1), + ) + + warehouse_account = get_warehouse_account_map(company) + stock_account_value = frappe.db.get_value( + "GL Entry", + { + "account": warehouse_account[target_warehouse]["account"], + "voucher_type": "Purchase Receipt", + "voucher_no": pr.name, + "is_cancelled": 0, + }, + fieldname=["credit"], + ) + stock_diff = frappe.db.get_value( + "Stock Ledger Entry", + { + "voucher_type": "Purchase Receipt", + "voucher_no": pr.name, + "is_cancelled": 0, + }, + fieldname=["sum(stock_value_difference)"], + ) + + # Value of Stock Account should be equal to the sum of Stock Value Difference + self.assertEqual(stock_account_value, stock_diff) + def prepare_data_for_internal_transfer(): from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_internal_supplier From b7d6e30f63c70408fa29993e5cd958c26fb2e33c Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Wed, 19 Apr 2023 12:11:05 +0530 Subject: [PATCH 12/75] refactor: update ref details for selected references set_missing_ref_details can update only for selected references --- erpnext/accounts/doctype/payment_entry/payment_entry.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/erpnext/accounts/doctype/payment_entry/payment_entry.py b/erpnext/accounts/doctype/payment_entry/payment_entry.py index 086e1e3c35..ee4d4d29e2 100644 --- a/erpnext/accounts/doctype/payment_entry/payment_entry.py +++ b/erpnext/accounts/doctype/payment_entry/payment_entry.py @@ -220,9 +220,16 @@ class PaymentEntry(AccountsController): else self.paid_to_account_currency ) - def set_missing_ref_details(self, force=False): + def set_missing_ref_details( + self, force: bool = False, update_ref_details_only_for: list | None = None + ) -> None: for d in self.get("references"): if d.allocated_amount: + if update_ref_details_only_for and ( + not (d.reference_doctype, d.reference_name) in update_ref_details_only_for + ): + continue + ref_details = get_reference_details( d.reference_doctype, d.reference_name, self.party_account_currency ) From 59f3fedbf7b92161f842479d05563f723d61bd78 Mon Sep 17 00:00:00 2001 From: Vishal Dhayagude Date: Wed, 19 Apr 2023 15:57:28 +0530 Subject: [PATCH 13/75] fix: batch qty conversion factor issue fixed in pos transaction (#34917) --- erpnext/selling/page/point_of_sale/pos_controller.js | 6 ++++-- erpnext/stock/doctype/batch/batch.py | 2 +- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/erpnext/selling/page/point_of_sale/pos_controller.js b/erpnext/selling/page/point_of_sale/pos_controller.js index 46320e5538..016ebf0cd1 100644 --- a/erpnext/selling/page/point_of_sale/pos_controller.js +++ b/erpnext/selling/page/point_of_sale/pos_controller.js @@ -559,8 +559,10 @@ erpnext.PointOfSale.Controller = class { item_row = this.frm.add_child('items', new_item); - if (field === 'qty' && value !== 0 && !this.allow_negative_stock) - await this.check_stock_availability(item_row, value, this.frm.doc.set_warehouse); + if (field === 'qty' && value !== 0 && !this.allow_negative_stock) { + const qty_needed = value * item_row.conversion_factor; + await this.check_stock_availability(item_row, qty_needed, this.frm.doc.set_warehouse); + } await this.trigger_new_item_events(item_row); diff --git a/erpnext/stock/doctype/batch/batch.py b/erpnext/stock/doctype/batch/batch.py index 3b9fe7b97c..d8f119a20c 100644 --- a/erpnext/stock/doctype/batch/batch.py +++ b/erpnext/stock/doctype/batch/batch.py @@ -376,7 +376,7 @@ def get_pos_reserved_batch_qty(filters): p = frappe.qb.DocType("POS Invoice").as_("p") item = frappe.qb.DocType("POS Invoice Item").as_("item") - sum_qty = frappe.query_builder.functions.Sum(item.qty).as_("qty") + sum_qty = frappe.query_builder.functions.Sum(item.stock_qty).as_("qty") reserved_batch_qty = ( frappe.qb.from_(p) From a77182645f734db83b728f90d4cfc55acf6e28cd Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Wed, 19 Apr 2023 20:35:14 +0530 Subject: [PATCH 14/75] fix: Incorrect difference value in Stock and Account Value Comparison report --- .../stock_and_account_value_comparison.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/stock/report/stock_and_account_value_comparison/stock_and_account_value_comparison.py b/erpnext/stock/report/stock_and_account_value_comparison/stock_and_account_value_comparison.py index 106e877c4c..5fb456502e 100644 --- a/erpnext/stock/report/stock_and_account_value_comparison/stock_and_account_value_comparison.py +++ b/erpnext/stock/report/stock_and_account_value_comparison/stock_and_account_value_comparison.py @@ -41,7 +41,7 @@ def get_data(report_filters): key = (d.voucher_type, d.voucher_no) gl_data = voucher_wise_gl_data.get(key) or {} d.account_value = gl_data.get("account_value", 0) - d.difference_value = d.stock_value - d.account_value + d.difference_value = abs(d.stock_value) - abs(d.account_value) if abs(d.difference_value) > 0.1: data.append(d) From fcfa8842a7d0413a865741be4f7904034b365411 Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Thu, 20 Apr 2023 09:48:15 +0530 Subject: [PATCH 15/75] fix: limit stock reco issue --- erpnext/stock/stock_ledger.py | 1 + 1 file changed, 1 insertion(+) diff --git a/erpnext/stock/stock_ledger.py b/erpnext/stock/stock_ledger.py index b638f08ed9..c197769d0a 100644 --- a/erpnext/stock/stock_ledger.py +++ b/erpnext/stock/stock_ledger.py @@ -1453,6 +1453,7 @@ def get_next_stock_reco(kwargs): ) .orderby(CombineDatetime(sle.posting_date, sle.posting_time)) .orderby(sle.creation) + .limit(1) ) if kwargs.get("batch_no"): From 11c8503180cda70b1aebce3711def05eec840133 Mon Sep 17 00:00:00 2001 From: s-aga-r Date: Wed, 19 Apr 2023 16:01:45 +0530 Subject: [PATCH 16/75] fix(test): `test_backdated_stock_reco_cancellation_future_negative_stock` --- .../doctype/stock_reconciliation/test_stock_reconciliation.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/erpnext/stock/doctype/stock_reconciliation/test_stock_reconciliation.py b/erpnext/stock/doctype/stock_reconciliation/test_stock_reconciliation.py index 7d59441d8b..2e5d2c3aaf 100644 --- a/erpnext/stock/doctype/stock_reconciliation/test_stock_reconciliation.py +++ b/erpnext/stock/doctype/stock_reconciliation/test_stock_reconciliation.py @@ -530,7 +530,9 @@ class TestStockReconciliation(FrappeTestCase, StockTestMixin): # check if cancellation of stock reco is blocked self.assertRaises(NegativeStockError, sr.cancel) - repost_exists = bool(frappe.db.exists("Repost Item Valuation", {"voucher_no": sr.name})) + repost_exists = bool( + frappe.db.exists("Repost Item Valuation", {"voucher_no": sr.name, "status": "Queued"}) + ) self.assertFalse(repost_exists, msg="Negative stock validation not working on reco cancellation") def test_intermediate_sr_bin_update(self): From df0682fa8c85221e214fda0e05a1699377868fae Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Thu, 20 Apr 2023 10:39:15 +0530 Subject: [PATCH 17/75] fix: broken set exchagne gain/loss btn broken in payment entry --- erpnext/accounts/doctype/payment_entry/payment_entry.js | 1 + 1 file changed, 1 insertion(+) diff --git a/erpnext/accounts/doctype/payment_entry/payment_entry.js b/erpnext/accounts/doctype/payment_entry/payment_entry.js index ed6d0a710a..07761c7705 100644 --- a/erpnext/accounts/doctype/payment_entry/payment_entry.js +++ b/erpnext/accounts/doctype/payment_entry/payment_entry.js @@ -977,6 +977,7 @@ frappe.ui.form.on('Payment Entry', { precision("difference_amount")); const add_deductions = (details) => { + let row = null; if (!write_off_row.length && difference_amount) { row = frm.add_child("deductions"); row.account = details[account]; From ea6eeace8028ded8d19ae4795887ec9750298d89 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Thu, 20 Apr 2023 12:48:44 +0530 Subject: [PATCH 18/75] fix: filtering via batch no(#34950) * fix: filtering via batch no --- erpnext/stock/stock_ledger.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/stock/stock_ledger.py b/erpnext/stock/stock_ledger.py index c197769d0a..0f12987fbb 100644 --- a/erpnext/stock/stock_ledger.py +++ b/erpnext/stock/stock_ledger.py @@ -1457,7 +1457,7 @@ def get_next_stock_reco(kwargs): ) if kwargs.get("batch_no"): - query.where(sle.batch_no == kwargs.get("batch_no")) + query = query.where(sle.batch_no == kwargs.get("batch_no")) return query.run(as_dict=True) From b572bef71d88e604d730b70e59d0b219fac54c32 Mon Sep 17 00:00:00 2001 From: Nihantra Patel Date: Thu, 20 Apr 2023 14:32:32 +0530 Subject: [PATCH 19/75] fix: process_loss_percentage in BOM --- erpnext/manufacturing/doctype/bom/bom.js | 1 - 1 file changed, 1 deletion(-) diff --git a/erpnext/manufacturing/doctype/bom/bom.js b/erpnext/manufacturing/doctype/bom/bom.js index 4304193afa..7cdcef9c7a 100644 --- a/erpnext/manufacturing/doctype/bom/bom.js +++ b/erpnext/manufacturing/doctype/bom/bom.js @@ -411,7 +411,6 @@ frappe.ui.form.on("BOM", { } frm.set_value("process_loss_qty", qty); - frm.set_value("add_process_loss_cost_in_fg", qty ? 1: 0); } }); From 8108b2de0aa86b743c767808e38349644c2f9be9 Mon Sep 17 00:00:00 2001 From: s-aga-r Date: Thu, 20 Apr 2023 12:39:30 +0530 Subject: [PATCH 20/75] fix: `PermissionError` in Work Order --- .../doctype/work_order/work_order.js | 20 +++++++++---------- 1 file changed, 9 insertions(+), 11 deletions(-) diff --git a/erpnext/manufacturing/doctype/work_order/work_order.js b/erpnext/manufacturing/doctype/work_order/work_order.js index 97480b2945..d0c9966f8b 100644 --- a/erpnext/manufacturing/doctype/work_order/work_order.js +++ b/erpnext/manufacturing/doctype/work_order/work_order.js @@ -625,20 +625,18 @@ erpnext.work_order = { // all materials transferred for manufacturing, make this primary finish_btn.addClass('btn-primary'); } - } else { - frappe.db.get_doc("Manufacturing Settings").then((doc) => { - let allowance_percentage = doc.overproduction_percentage_for_work_order; + } else if (frm.doc.__onload && frm.doc.__onload.overproduction_percentage) { + let allowance_percentage = frm.doc.__onload.overproduction_percentage; - if (allowance_percentage > 0) { - let allowed_qty = frm.doc.qty + ((allowance_percentage / 100) * frm.doc.qty); + if (allowance_percentage > 0) { + let allowed_qty = frm.doc.qty + ((allowance_percentage / 100) * frm.doc.qty); - if ((flt(doc.produced_qty) < allowed_qty)) { - frm.add_custom_button(__('Finish'), function() { - erpnext.work_order.make_se(frm, 'Manufacture'); - }); - } + if ((flt(doc.produced_qty) < allowed_qty)) { + frm.add_custom_button(__('Finish'), function() { + erpnext.work_order.make_se(frm, 'Manufacture'); + }); } - }); + } } } } else { From a90a5b4aa4c8a363f66496d29f23d183928453ae Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Thu, 20 Apr 2023 16:01:05 +0530 Subject: [PATCH 21/75] fix: removed depends on for the Employee Detail section --- erpnext/projects/doctype/timesheet/timesheet.json | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/erpnext/projects/doctype/timesheet/timesheet.json b/erpnext/projects/doctype/timesheet/timesheet.json index 468300661a..ba6262dc3d 100644 --- a/erpnext/projects/doctype/timesheet/timesheet.json +++ b/erpnext/projects/doctype/timesheet/timesheet.json @@ -96,7 +96,6 @@ "read_only": 1 }, { - "depends_on": "eval:!doc.work_order || doc.docstatus == 1", "fieldname": "employee_detail", "fieldtype": "Section Break", "label": "Employee Detail" @@ -311,7 +310,7 @@ "idx": 1, "is_submittable": 1, "links": [], - "modified": "2023-02-14 04:55:41.735991", + "modified": "2023-04-20 15:59:11.107831", "modified_by": "Administrator", "module": "Projects", "name": "Timesheet", From c3b5dcb7675b07979b2095e885ed12037ceac940 Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Thu, 20 Apr 2023 16:36:10 +0530 Subject: [PATCH 22/75] fix: stock entry type issue --- erpnext/stock/doctype/material_request/material_request.py | 2 +- .../stock/doctype/material_request/test_material_request.py | 4 ++++ erpnext/stock/doctype/stock_entry/stock_entry.py | 2 +- erpnext/stock/doctype/stock_entry/test_stock_entry.py | 3 +++ 4 files changed, 9 insertions(+), 2 deletions(-) diff --git a/erpnext/stock/doctype/material_request/material_request.py b/erpnext/stock/doctype/material_request/material_request.py index 8aeb7511f4..3967282358 100644 --- a/erpnext/stock/doctype/material_request/material_request.py +++ b/erpnext/stock/doctype/material_request/material_request.py @@ -616,7 +616,7 @@ def make_stock_entry(source_name, target_doc=None): target.set_transfer_qty() target.set_actual_qty() target.calculate_rate_and_amount(raise_error_if_no_rate=False) - target.set_stock_entry_type() + target.stock_entry_type = target.purpose target.set_job_card_data() doclist = get_mapped_doc( diff --git a/erpnext/stock/doctype/material_request/test_material_request.py b/erpnext/stock/doctype/material_request/test_material_request.py index a707c74c7d..03f58c664d 100644 --- a/erpnext/stock/doctype/material_request/test_material_request.py +++ b/erpnext/stock/doctype/material_request/test_material_request.py @@ -54,6 +54,8 @@ class TestMaterialRequest(FrappeTestCase): mr.submit() se = make_stock_entry(mr.name) + self.assertEqual(se.stock_entry_type, "Material Transfer") + self.assertEqual(se.purpose, "Material Transfer") self.assertEqual(se.doctype, "Stock Entry") self.assertEqual(len(se.get("items")), len(mr.get("items"))) @@ -69,6 +71,8 @@ class TestMaterialRequest(FrappeTestCase): in_transit_warehouse = get_in_transit_warehouse(mr.company) se = make_in_transit_stock_entry(mr.name, in_transit_warehouse) + self.assertEqual(se.stock_entry_type, "Material Transfer") + self.assertEqual(se.purpose, "Material Transfer") self.assertEqual(se.doctype, "Stock Entry") for row in se.get("items"): self.assertEqual(row.t_warehouse, in_transit_warehouse) diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.py b/erpnext/stock/doctype/stock_entry/stock_entry.py index 36c875f308..b5e5299f87 100644 --- a/erpnext/stock/doctype/stock_entry/stock_entry.py +++ b/erpnext/stock/doctype/stock_entry/stock_entry.py @@ -2346,7 +2346,7 @@ def move_sample_to_retention_warehouse(company, items): @frappe.whitelist() def make_stock_in_entry(source_name, target_doc=None): def set_missing_values(source, target): - target.set_stock_entry_type() + target.stock_entry_type = "Material Transfer" target.set_missing_values() def update_item(source_doc, target_doc, source_parent): diff --git a/erpnext/stock/doctype/stock_entry/test_stock_entry.py b/erpnext/stock/doctype/stock_entry/test_stock_entry.py index cc06bd709a..c43a1b1b81 100644 --- a/erpnext/stock/doctype/stock_entry/test_stock_entry.py +++ b/erpnext/stock/doctype/stock_entry/test_stock_entry.py @@ -202,6 +202,9 @@ class TestStockEntry(FrappeTestCase): ) end_transit_entry = make_stock_in_entry(transit_entry.name) + + self.assertEqual(end_transit_entry.stock_entry_type, "Material Transfer") + self.assertEqual(end_transit_entry.purpose, "Material Transfer") self.assertEqual(transit_entry.name, end_transit_entry.outgoing_stock_entry) self.assertEqual(transit_entry.name, end_transit_entry.items[0].against_stock_entry) self.assertEqual(transit_entry.items[0].name, end_transit_entry.items[0].ste_detail) From 17ef3c964f194816c60d49fa8ec471b184869d3e Mon Sep 17 00:00:00 2001 From: Sagar Vora Date: Tue, 4 Apr 2023 17:49:50 +0530 Subject: [PATCH 23/75] fix: set `frappe.flags.company` to call regional code accurately --- erpnext/accounts/party.py | 2 ++ erpnext/controllers/taxes_and_totals.py | 2 ++ 2 files changed, 4 insertions(+) diff --git a/erpnext/accounts/party.py b/erpnext/accounts/party.py index ac9368e69c..7747042825 100644 --- a/erpnext/accounts/party.py +++ b/erpnext/accounts/party.py @@ -259,6 +259,8 @@ def set_address_details( ) if doctype in TRANSACTION_TYPES: + # required to set correct region + frappe.flags.company = company get_regional_address_details(party_details, doctype, company) return party_address, shipping_address diff --git a/erpnext/controllers/taxes_and_totals.py b/erpnext/controllers/taxes_and_totals.py index 1edd7bf85e..4661c5ca7e 100644 --- a/erpnext/controllers/taxes_and_totals.py +++ b/erpnext/controllers/taxes_and_totals.py @@ -976,6 +976,8 @@ def get_itemised_tax_breakup_html(doc): @frappe.whitelist() def get_round_off_applicable_accounts(company, account_list): + # required to set correct region + frappe.flags.company = company account_list = get_regional_round_off_accounts(company, account_list) return account_list From 2fa641f86de8ad0be5f39575726d3672bfd885d4 Mon Sep 17 00:00:00 2001 From: Sagar Vora Date: Tue, 4 Apr 2023 17:50:31 +0530 Subject: [PATCH 24/75] fix: simplify `erpnext.get_region` --- erpnext/__init__.py | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/erpnext/__init__.py b/erpnext/__init__.py index e0f0c98e9c..5ccb1b942f 100644 --- a/erpnext/__init__.py +++ b/erpnext/__init__.py @@ -120,12 +120,14 @@ def get_region(company=None): You can also set global company flag in `frappe.flags.company` """ - if company or frappe.flags.company: - return frappe.get_cached_value("Company", company or frappe.flags.company, "country") - elif frappe.flags.country: - return frappe.flags.country - else: - return frappe.get_system_settings("country") + + if not company: + company = frappe.local.flags.company + + if company: + return frappe.get_cached_value("Company", company, "country") + + return frappe.flags.country or frappe.get_system_settings("country") def allow_regional(fn): From 776b56ccd13c45e9eda0a67e8c9f42e65acb3135 Mon Sep 17 00:00:00 2001 From: Sagar Vora Date: Tue, 4 Apr 2023 19:24:07 +0530 Subject: [PATCH 25/75] fix: use `functools.wraps` to preserve doc signature --- erpnext/__init__.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/erpnext/__init__.py b/erpnext/__init__.py index 5ccb1b942f..c9c9c9c6df 100644 --- a/erpnext/__init__.py +++ b/erpnext/__init__.py @@ -1,3 +1,4 @@ +import functools import inspect import frappe @@ -138,6 +139,7 @@ def allow_regional(fn): def myfunction(): pass""" + @functools.wraps(fn) def caller(*args, **kwargs): overrides = frappe.get_hooks("regional_overrides", {}).get(get_region()) function_path = f"{inspect.getmodule(fn).__name__}.{fn.__name__}" From a02705ded7dfd8f2fbd1b4ad0ace5a8b8f3da45c Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Fri, 21 Apr 2023 13:25:32 +0530 Subject: [PATCH 26/75] chore: Move source and campaign to more info section (#34946) --- .../doctype/sales_order/sales_order.json | 18 +++--------------- .../doctype/delivery_note/delivery_note.json | 13 ++++--------- 2 files changed, 7 insertions(+), 24 deletions(-) diff --git a/erpnext/selling/doctype/sales_order/sales_order.json b/erpnext/selling/doctype/sales_order/sales_order.json index ccea8407ab..4f498fb20d 100644 --- a/erpnext/selling/doctype/sales_order/sales_order.json +++ b/erpnext/selling/doctype/sales_order/sales_order.json @@ -30,10 +30,6 @@ "cost_center", "dimension_col_break", "project", - "column_break_77", - "source", - "campaign", - "custom_dimensions_section", "currency_and_price_list", "currency", "conversion_rate", @@ -162,7 +158,9 @@ "is_internal_customer", "represents_company", "column_break_152", + "source", "inter_company_order_reference", + "campaign", "party_account_currency", "connections_tab" ], @@ -1164,12 +1162,6 @@ "print_hide": 1, "read_only": 1 }, - { - "fieldname": "column_break_77", - "fieldtype": "Column Break", - "hide_days": 1, - "hide_seconds": 1 - }, { "fieldname": "source", "fieldtype": "Link", @@ -1612,10 +1604,6 @@ "fieldname": "column_break_92", "fieldtype": "Column Break" }, - { - "fieldname": "custom_dimensions_section", - "fieldtype": "Section Break" - }, { "collapsible": 1, "fieldname": "additional_info_section", @@ -1643,7 +1631,7 @@ "idx": 105, "is_submittable": 1, "links": [], - "modified": "2022-12-12 18:34:00.681780", + "modified": "2023-04-20 11:14:01.036202", "modified_by": "Administrator", "module": "Selling", "name": "Sales Order", diff --git a/erpnext/stock/doctype/delivery_note/delivery_note.json b/erpnext/stock/doctype/delivery_note/delivery_note.json index 0c1f82029e..2adf9c310f 100644 --- a/erpnext/stock/doctype/delivery_note/delivery_note.json +++ b/erpnext/stock/doctype/delivery_note/delivery_note.json @@ -28,8 +28,6 @@ "column_break_18", "project", "dimension_col_break", - "campaign", - "source", "custom_dimensions_section", "currency_and_price_list", "currency", @@ -161,11 +159,12 @@ "inter_company_reference", "customer_group", "territory", + "source", + "campaign", "column_break5", "excise_page", "instructions", - "connections_tab", - "column_break_25" + "connections_tab" ], "fields": [ { @@ -1339,10 +1338,6 @@ "fieldname": "column_break_10", "fieldtype": "Column Break" }, - { - "fieldname": "column_break_25", - "fieldtype": "Column Break" - }, { "fieldname": "section_break_30", "fieldtype": "Section Break", @@ -1403,7 +1398,7 @@ "idx": 146, "is_submittable": 1, "links": [], - "modified": "2023-02-14 04:45:44.179670", + "modified": "2023-04-21 11:15:23.931084", "modified_by": "Administrator", "module": "Stock", "name": "Delivery Note", From af8da53cf4920688e8b756d25685964e0e57b9f3 Mon Sep 17 00:00:00 2001 From: HENRY Florian Date: Fri, 21 Apr 2023 09:56:32 +0200 Subject: [PATCH 27/75] fix: FEC report for France accountancy (#34781) * fix: FEC report for France Accountancy legal requirement * fix: FEC report for France Accountancy legal requirement * fix: change to query standard * fix: change to query standard * fix: columns to standard dict * fix: columns to standard dict * fix: columns to data * refactor: french report FEC * refactor: french report FEC (2) --------- Co-authored-by: barredterra <14891507+barredterra@users.noreply.github.com> --- .../fichier_des_ecritures_comptables_[fec].py | 305 ++++++++++++------ 1 file changed, 198 insertions(+), 107 deletions(-) diff --git a/erpnext/regional/report/fichier_des_ecritures_comptables_[fec]/fichier_des_ecritures_comptables_[fec].py b/erpnext/regional/report/fichier_des_ecritures_comptables_[fec]/fichier_des_ecritures_comptables_[fec].py index c75179ee5d..6717989008 100644 --- a/erpnext/regional/report/fichier_des_ecritures_comptables_[fec]/fichier_des_ecritures_comptables_[fec].py +++ b/erpnext/regional/report/fichier_des_ecritures_comptables_[fec]/fichier_des_ecritures_comptables_[fec].py @@ -1,31 +1,135 @@ # Copyright (c) 2018, Frappe Technologies Pvt. Ltd. and contributors # For license information, please see license.txt - import re import frappe from frappe import _ from frappe.utils import format_datetime +COLUMNS = [ + { + "label": "JournalCode", + "fieldname": "JournalCode", + "fieldtype": "Data", + "width": 90, + }, + { + "label": "JournalLib", + "fieldname": "JournalLib", + "fieldtype": "Data", + "width": 90, + }, + { + "label": "EcritureNum", + "fieldname": "EcritureNum", + "fieldtype": "Data", + "width": 90, + }, + { + "label": "EcritureDate", + "fieldname": "EcritureDate", + "fieldtype": "Data", + "width": 90, + }, + { + "label": "CompteNum", + "fieldname": "CompteNum", + "fieldtype": "Link", + "options": "Account", + "width": 100, + }, + { + "label": "CompteLib", + "fieldname": "CompteLib", + "fieldtype": "Link", + "options": "Account", + "width": 200, + }, + { + "label": "CompAuxNum", + "fieldname": "CompAuxNum", + "fieldtype": "Data", + "width": 90, + }, + { + "label": "CompAuxLib", + "fieldname": "CompAuxLib", + "fieldtype": "Data", + "width": 90, + }, + { + "label": "PieceRef", + "fieldname": "PieceRef", + "fieldtype": "Data", + "width": 90, + }, + { + "label": "PieceDate", + "fieldname": "PieceDate", + "fieldtype": "Data", + "width": 90, + }, + { + "label": "EcritureLib", + "fieldname": "EcritureLib", + "fieldtype": "Data", + "width": 90, + }, + { + "label": "Debit", + "fieldname": "Debit", + "fieldtype": "Data", + "width": 90, + }, + { + "label": "Credit", + "fieldname": "Credit", + "fieldtype": "Data", + "width": 90, + }, + { + "label": "EcritureLet", + "fieldname": "EcritureLet", + "fieldtype": "Data", + "width": 90, + }, + { + "label": "DateLet", + "fieldname": "DateLet", + "fieldtype": "Data", + "width": 90, + }, + { + "label": "ValidDate", + "fieldname": "ValidDate", + "fieldtype": "Data", + "width": 90, + }, + { + "label": "Montantdevise", + "fieldname": "Montantdevise", + "fieldtype": "Data", + "width": 90, + }, + { + "label": "Idevise", + "fieldname": "Idevise", + "fieldtype": "Data", + "width": 90, + }, +] + def execute(filters=None): - account_details = {} - for acc in frappe.db.sql("""select name, is_group from tabAccount""", as_dict=1): - account_details.setdefault(acc.name, acc) - - validate_filters(filters, account_details) - - filters = set_account_currency(filters) - - columns = get_columns(filters) - - res = get_result(filters) - - return columns, res + validate_filters(filters) + return COLUMNS, get_result( + company=filters["company"], + fiscal_year=filters["fiscal_year"], + ) -def validate_filters(filters, account_details): +def validate_filters(filters): if not filters.get("company"): frappe.throw(_("{0} is mandatory").format(_("Company"))) @@ -33,107 +137,96 @@ def validate_filters(filters, account_details): frappe.throw(_("{0} is mandatory").format(_("Fiscal Year"))) -def set_account_currency(filters): +def get_gl_entries(company, fiscal_year): + gle = frappe.qb.DocType("GL Entry") + sales_invoice = frappe.qb.DocType("Sales Invoice") + purchase_invoice = frappe.qb.DocType("Purchase Invoice") + journal_entry = frappe.qb.DocType("Journal Entry") + payment_entry = frappe.qb.DocType("Payment Entry") + customer = frappe.qb.DocType("Customer") + supplier = frappe.qb.DocType("Supplier") + employee = frappe.qb.DocType("Employee") - filters["company_currency"] = frappe.get_cached_value( - "Company", filters.company, "default_currency" + debit = frappe.query_builder.functions.Sum(gle.debit).as_("debit") + credit = frappe.query_builder.functions.Sum(gle.credit).as_("credit") + debit_currency = frappe.query_builder.functions.Sum(gle.debit_in_account_currency).as_( + "debitCurr" + ) + credit_currency = frappe.query_builder.functions.Sum(gle.credit_in_account_currency).as_( + "creditCurr" ) - return filters - - -def get_columns(filters): - columns = [ - "JournalCode" + "::90", - "JournalLib" + "::90", - "EcritureNum" + ":Dynamic Link:90", - "EcritureDate" + "::90", - "CompteNum" + ":Link/Account:100", - "CompteLib" + ":Link/Account:200", - "CompAuxNum" + "::90", - "CompAuxLib" + "::90", - "PieceRef" + "::90", - "PieceDate" + "::90", - "EcritureLib" + "::90", - "Debit" + "::90", - "Credit" + "::90", - "EcritureLet" + "::90", - "DateLet" + "::90", - "ValidDate" + "::90", - "Montantdevise" + "::90", - "Idevise" + "::90", - ] - - return columns - - -def get_result(filters): - gl_entries = get_gl_entries(filters) - - result = get_result_as_list(gl_entries, filters) - - return result - - -def get_gl_entries(filters): - - group_by_condition = ( - "group by voucher_type, voucher_no, account" - if filters.get("group_by_voucher") - else "group by gl.name" + query = ( + frappe.qb.from_(gle) + .left_join(sales_invoice) + .on(gle.voucher_no == sales_invoice.name) + .left_join(purchase_invoice) + .on(gle.voucher_no == purchase_invoice.name) + .left_join(journal_entry) + .on(gle.voucher_no == journal_entry.name) + .left_join(payment_entry) + .on(gle.voucher_no == payment_entry.name) + .left_join(customer) + .on(gle.party == customer.name) + .left_join(supplier) + .on(gle.party == supplier.name) + .left_join(employee) + .on(gle.party == employee.name) + .select( + gle.posting_date.as_("GlPostDate"), + gle.name.as_("GlName"), + gle.account, + gle.transaction_date, + debit, + credit, + debit_currency, + credit_currency, + gle.voucher_type, + gle.voucher_no, + gle.against_voucher_type, + gle.against_voucher, + gle.account_currency, + gle.against, + gle.party_type, + gle.party, + sales_invoice.name.as_("InvName"), + sales_invoice.title.as_("InvTitle"), + sales_invoice.posting_date.as_("InvPostDate"), + purchase_invoice.name.as_("PurName"), + purchase_invoice.title.as_("PurTitle"), + purchase_invoice.posting_date.as_("PurPostDate"), + journal_entry.cheque_no.as_("JnlRef"), + journal_entry.posting_date.as_("JnlPostDate"), + journal_entry.title.as_("JnlTitle"), + payment_entry.name.as_("PayName"), + payment_entry.posting_date.as_("PayPostDate"), + payment_entry.title.as_("PayTitle"), + customer.customer_name, + customer.name.as_("cusName"), + supplier.supplier_name, + supplier.name.as_("supName"), + employee.employee_name, + employee.name.as_("empName"), + ) + .where((gle.company == company) & (gle.fiscal_year == fiscal_year)) + .groupby(gle.voucher_type, gle.voucher_no, gle.account) + .orderby(gle.posting_date, gle.voucher_no) ) - gl_entries = frappe.db.sql( - """ - select - gl.posting_date as GlPostDate, gl.name as GlName, gl.account, gl.transaction_date, - sum(gl.debit) as debit, sum(gl.credit) as credit, - sum(gl.debit_in_account_currency) as debitCurr, sum(gl.credit_in_account_currency) as creditCurr, - gl.voucher_type, gl.voucher_no, gl.against_voucher_type, - gl.against_voucher, gl.account_currency, gl.against, - gl.party_type, gl.party, - inv.name as InvName, inv.title as InvTitle, inv.posting_date as InvPostDate, - pur.name as PurName, pur.title as PurTitle, pur.posting_date as PurPostDate, - jnl.cheque_no as JnlRef, jnl.posting_date as JnlPostDate, jnl.title as JnlTitle, - pay.name as PayName, pay.posting_date as PayPostDate, pay.title as PayTitle, - cus.customer_name, cus.name as cusName, - sup.supplier_name, sup.name as supName, - emp.employee_name, emp.name as empName, - stu.title as student_name, stu.name as stuName, - member_name, mem.name as memName - - from `tabGL Entry` gl - left join `tabSales Invoice` inv on gl.voucher_no = inv.name - left join `tabPurchase Invoice` pur on gl.voucher_no = pur.name - left join `tabJournal Entry` jnl on gl.voucher_no = jnl.name - left join `tabPayment Entry` pay on gl.voucher_no = pay.name - left join `tabCustomer` cus on gl.party = cus.name - left join `tabSupplier` sup on gl.party = sup.name - left join `tabEmployee` emp on gl.party = emp.name - left join `tabStudent` stu on gl.party = stu.name - left join `tabMember` mem on gl.party = mem.name - where gl.company=%(company)s and gl.fiscal_year=%(fiscal_year)s - {group_by_condition} - order by GlPostDate, voucher_no""".format( - group_by_condition=group_by_condition - ), - filters, - as_dict=1, - ) - - return gl_entries + return query.run(as_dict=True) -def get_result_as_list(data, filters): +def get_result(company, fiscal_year): + data = get_gl_entries(company, fiscal_year) + result = [] - company_currency = frappe.get_cached_value("Company", filters.company, "default_currency") + company_currency = frappe.get_cached_value("Company", company, "default_currency") accounts = frappe.get_all( - "Account", filters={"Company": filters.company}, fields=["name", "account_number"] + "Account", filters={"Company": company}, fields=["name", "account_number"] ) for d in data: - JournalCode = re.split("-|/|[0-9]", d.get("voucher_no"))[0] if d.get("voucher_no").startswith("{0}-".format(JournalCode)) or d.get("voucher_no").startswith( @@ -141,9 +234,7 @@ def get_result_as_list(data, filters): ): EcritureNum = re.split("-|/", d.get("voucher_no"))[1] else: - EcritureNum = re.search( - r"{0}(\d+)".format(JournalCode), d.get("voucher_no"), re.IGNORECASE - ).group(1) + EcritureNum = re.search(r"{0}(\d+)".format(JournalCode), d.get("voucher_no"), re.IGNORECASE)[1] EcritureDate = format_datetime(d.get("GlPostDate"), "yyyyMMdd") @@ -185,7 +276,7 @@ def get_result_as_list(data, filters): ValidDate = format_datetime(d.get("GlPostDate"), "yyyyMMdd") - PieceRef = d.get("voucher_no") if d.get("voucher_no") else "Sans Reference" + PieceRef = d.get("voucher_no") or "Sans Reference" # EcritureLib is the reference title unless it is an opening entry if d.get("is_opening") == "Yes": From 9a37ac6c2563f8f6459c5c47a95ef349a9fc10bf Mon Sep 17 00:00:00 2001 From: s-aga-r Date: Fri, 21 Apr 2023 13:28:14 +0530 Subject: [PATCH 28/75] refactor: sum up SLE value in query --- erpnext/stock/utils.py | 18 +++--------------- 1 file changed, 3 insertions(+), 15 deletions(-) diff --git a/erpnext/stock/utils.py b/erpnext/stock/utils.py index d928dca723..9c2e2c805b 100644 --- a/erpnext/stock/utils.py +++ b/erpnext/stock/utils.py @@ -7,7 +7,7 @@ from typing import Dict, Optional import frappe from frappe import _ -from frappe.query_builder.functions import CombineDatetime +from frappe.query_builder.functions import CombineDatetime, IfNull, Sum from frappe.utils import cstr, flt, get_link_to_form, nowdate, nowtime from pypika.terms import ExistsCriterion @@ -61,12 +61,7 @@ def get_stock_value_on(warehouse=None, posting_date=None, item_code=None): sle = frappe.qb.DocType("Stock Ledger Entry") query = ( frappe.qb.from_(sle) - .select( - sle.item_code, - sle.stock_value, - sle.name, - sle.warehouse, - ) + .select(IfNull(Sum(sle.stock_value_difference), 0)) .where((sle.posting_date <= posting_date) & (sle.is_cancelled == 0)) .orderby(CombineDatetime(sle.posting_date, sle.posting_time), order=frappe.qb.desc) .orderby(sle.creation, order=frappe.qb.desc) @@ -90,14 +85,7 @@ def get_stock_value_on(warehouse=None, posting_date=None, item_code=None): if item_code: query = query.where(sle.item_code == item_code) - stock_ledger_entries = query.run(as_dict=True) - - sle_map = {} - for sle in stock_ledger_entries: - if not (sle.item_code, sle.warehouse) in sle_map: - sle_map[(sle.item_code, sle.warehouse)] = flt(sle.stock_value) - - return sum(sle_map.values()) + return query.run(as_list=True)[0][0] @frappe.whitelist() From 19911b48fdb89ca943ce1e5131eb465ebe12547c Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Fri, 21 Apr 2023 16:49:48 +0530 Subject: [PATCH 29/75] fix: validation for internal transfer entry --- .../doctype/sales_invoice/sales_invoice.js | 1 + erpnext/controllers/accounts_controller.py | 11 +++- .../purchase_receipt/test_purchase_receipt.py | 58 +++++++++++++++++++ 3 files changed, 69 insertions(+), 1 deletion(-) diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.js b/erpnext/accounts/doctype/sales_invoice/sales_invoice.js index 56e412b297..8cb29505eb 100644 --- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.js +++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.js @@ -334,6 +334,7 @@ erpnext.accounts.SalesInvoiceController = class SalesInvoiceController extends e } make_inter_company_invoice() { + let me = this; frappe.model.open_mapped_doc({ method: "erpnext.accounts.doctype.sales_invoice.sales_invoice.make_inter_company_purchase_invoice", frm: me.frm diff --git a/erpnext/controllers/accounts_controller.py b/erpnext/controllers/accounts_controller.py index c7416228ee..642d51c325 100644 --- a/erpnext/controllers/accounts_controller.py +++ b/erpnext/controllers/accounts_controller.py @@ -5,7 +5,7 @@ import json import frappe -from frappe import _, throw +from frappe import _, bold, throw from frappe.model.workflow import get_workflow_name, is_transition_condition_satisfied from frappe.query_builder.functions import Abs, Sum from frappe.utils import ( @@ -405,6 +405,15 @@ class AccountsController(TransactionBase): msg += _("Please create purchase from internal sale or delivery document itself") frappe.throw(msg, title=_("Internal Sales Reference Missing")) + label = "Delivery Note Item" if self.doctype == "Purchase Receipt" else "Sales Invoice Item" + + field = frappe.scrub(label) + + for row in self.get("items"): + if not row.get(field): + msg = f"At Row {row.idx}: The field {bold(label)} is mandatory for internal transfer" + frappe.throw(_(msg), title=_("Internal Transfer Reference Missing")) + def disable_pricing_rule_on_internal_transfer(self): if not self.get("ignore_pricing_rule") and self.is_internal_transfer(): self.ignore_pricing_rule = 1 diff --git a/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py b/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py index 8af279a9c8..c34f9daeef 100644 --- a/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py +++ b/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py @@ -1693,6 +1693,64 @@ class TestPurchaseReceipt(FrappeTestCase): # Value of Stock Account should be equal to the sum of Stock Value Difference self.assertEqual(stock_account_value, stock_diff) + def test_internal_pr_reference(self): + item = make_item(properties={"is_stock_item": 1, "valuation_rate": 100}) + customer = "_Test Internal Customer 2" + company = "_Test Company with perpetual inventory" + from_warehouse = create_warehouse("_Test Internal From Warehouse New 1", company=company) + target_warehouse = create_warehouse("_Test Internal GIT Warehouse New 1", company=company) + to_warehouse = create_warehouse("_Test Internal To Warehouse New 1", company=company) + + # Step 2: Create Stock Entry (Material Receipt) + from erpnext.stock.doctype.stock_entry.test_stock_entry import make_stock_entry + + make_stock_entry( + purpose="Material Receipt", + item_code=item.name, + qty=15, + company=company, + to_warehouse=from_warehouse, + ) + + # Step 3: Create Delivery Note with Internal Customer + from erpnext.stock.doctype.delivery_note.test_delivery_note import create_delivery_note + + dn = create_delivery_note( + item_code=item.name, + company=company, + customer=customer, + cost_center="Main - TCP1", + expense_account="Cost of Goods Sold - TCP1", + qty=10, + rate=100, + warehouse=from_warehouse, + target_warehouse=target_warehouse, + ) + + # Step 4: Create Internal Purchase Receipt + from erpnext.controllers.status_updater import OverAllowanceError + from erpnext.stock.doctype.delivery_note.delivery_note import make_inter_company_purchase_receipt + + pr = make_inter_company_purchase_receipt(dn.name) + pr.inter_company_reference = "" + self.assertRaises(frappe.ValidationError, pr.save) + + pr.inter_company_reference = dn.name + pr.items[0].qty = 10 + pr.items[0].from_warehouse = target_warehouse + pr.items[0].warehouse = to_warehouse + pr.items[0].rejected_warehouse = from_warehouse + pr.save() + + delivery_note_item = pr.items[0].delivery_note_item + pr.items[0].delivery_note_item = "" + + self.assertRaises(frappe.ValidationError, pr.save) + + pr.load_from_db() + pr.items[0].delivery_note_item = delivery_note_item + pr.save() + def prepare_data_for_internal_transfer(): from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_internal_supplier From 6a0b7c9e8cc84ba0196440a5a0486323c54c720d Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Fri, 21 Apr 2023 17:34:22 +0530 Subject: [PATCH 30/75] fix: added validation for extra job card --- .../doctype/job_card/job_card.py | 31 +++++++++++ .../doctype/work_order/test_work_order.py | 51 +++++++++++++++++++ 2 files changed, 82 insertions(+) diff --git a/erpnext/manufacturing/doctype/job_card/job_card.py b/erpnext/manufacturing/doctype/job_card/job_card.py index e82f37977c..f89951619e 100644 --- a/erpnext/manufacturing/doctype/job_card/job_card.py +++ b/erpnext/manufacturing/doctype/job_card/job_card.py @@ -74,6 +74,37 @@ class JobCard(Document): self.update_sub_operation_status() self.validate_work_order() + def on_update(self): + self.validate_job_card_qty() + + def validate_job_card_qty(self): + if not (self.operation_id and self.work_order): + return + + wo_qty = flt(frappe.get_cached_value("Work Order", self.work_order, "qty")) + + completed_qty = flt( + frappe.db.get_value("Work Order Operation", self.operation_id, "completed_qty") + ) + + job_card_qty = frappe.get_all( + "Job Card", + fields=["sum(for_quantity)"], + filters={ + "work_order": self.work_order, + "operation_id": self.operation_id, + "docstatus": ["!=", 2], + }, + as_list=1, + ) + + job_card_qty = flt(job_card_qty[0][0]) if job_card_qty else 0 + + if job_card_qty and ((job_card_qty - completed_qty) > wo_qty): + msg = f"""Job Card quantity cannot be greater than + Work Order quantity for the operation {self.operation}""" + frappe.throw(_(msg), title=_("Extra Job Card Quantity")) + def set_sub_operations(self): if not self.sub_operations and self.operation: self.sub_operations = [] diff --git a/erpnext/manufacturing/doctype/work_order/test_work_order.py b/erpnext/manufacturing/doctype/work_order/test_work_order.py index 729ed42f51..540b7dc9ea 100644 --- a/erpnext/manufacturing/doctype/work_order/test_work_order.py +++ b/erpnext/manufacturing/doctype/work_order/test_work_order.py @@ -1598,6 +1598,57 @@ class TestWorkOrder(FrappeTestCase): self.assertEqual(row.to_time, add_to_date(planned_start_date, minutes=30)) self.assertEqual(row.workstation, workstations_to_check[index]) + def test_job_card_extra_qty(self): + items = [ + "Test FG Item for Scrap Item Test 1", + "Test RM Item 1 for Scrap Item Test 1", + "Test RM Item 2 for Scrap Item Test 1", + ] + + company = "_Test Company with perpetual inventory" + for item_code in items: + create_item( + item_code=item_code, + is_stock_item=1, + is_purchase_item=1, + opening_stock=100, + valuation_rate=10, + company=company, + warehouse="Stores - TCP1", + ) + + item = "Test FG Item for Scrap Item Test 1" + raw_materials = ["Test RM Item 1 for Scrap Item Test 1", "Test RM Item 2 for Scrap Item Test 1"] + if not frappe.db.get_value("BOM", {"item": item}): + bom = make_bom( + item=item, source_warehouse="Stores - TCP1", raw_materials=raw_materials, do_not_save=True + ) + bom.with_operations = 1 + bom.append( + "operations", + { + "operation": "_Test Operation 1", + "workstation": "_Test Workstation 1", + "hour_rate": 20, + "time_in_mins": 60, + }, + ) + + bom.submit() + + wo_order = make_wo_order_test_record( + item=item, + company=company, + planned_start_date=now(), + qty=20, + ) + job_card = frappe.db.get_value("Job Card", {"work_order": wo_order.name}, "name") + job_card_doc = frappe.get_doc("Job Card", job_card) + + # Make another Job Card for the same Work Order + job_card2 = frappe.copy_doc(job_card_doc) + self.assertRaises(frappe.ValidationError, job_card2.save) + def prepare_data_for_workstation_type_check(): from erpnext.manufacturing.doctype.operation.test_operation import make_operation From ac871797b28d87a09c5ca26e618fdf01b087f9de Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Fri, 21 Apr 2023 18:05:29 +0530 Subject: [PATCH 31/75] fix: SLA permissions (#34981) --- .../service_level_agreement.json | 13 +++---------- 1 file changed, 3 insertions(+), 10 deletions(-) diff --git a/erpnext/support/doctype/service_level_agreement/service_level_agreement.json b/erpnext/support/doctype/service_level_agreement/service_level_agreement.json index 1698e2380f..1c6f24b23c 100644 --- a/erpnext/support/doctype/service_level_agreement/service_level_agreement.json +++ b/erpnext/support/doctype/service_level_agreement/service_level_agreement.json @@ -192,7 +192,7 @@ } ], "links": [], - "modified": "2021-11-26 15:45:33.289911", + "modified": "2023-04-21 17:16:56.192560", "modified_by": "Administrator", "module": "Support", "name": "Service Level Agreement", @@ -212,19 +212,12 @@ "write": 1 }, { - "create": 1, - "delete": 1, - "email": 1, - "export": 1, - "print": 1, "read": 1, - "report": 1, - "role": "All", - "share": 1, - "write": 1 + "role": "All" } ], "sort_field": "modified", "sort_order": "DESC", + "states": [], "track_changes": 1 } \ No newline at end of file From f2253dd6452d2ef421fb449182b98a5618a22021 Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Sat, 22 Apr 2023 11:16:12 +0530 Subject: [PATCH 32/75] fix: duplicate reposting entries of same voucher --- erpnext/stock/stock_ledger.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/erpnext/stock/stock_ledger.py b/erpnext/stock/stock_ledger.py index 0f12987fbb..861ea84de3 100644 --- a/erpnext/stock/stock_ledger.py +++ b/erpnext/stock/stock_ledger.py @@ -1388,7 +1388,11 @@ def update_qty_in_future_sle(args, allow_negative_stock=False): def regenerate_sle_for_batch_stock_reco(detail): doc = frappe.get_cached_doc("Stock Reconciliation", detail.voucher_no) doc.recalculate_current_qty(detail.item_code, detail.batch_no) - doc.repost_future_sle_and_gle() + + if not frappe.db.exists( + "Repost Item Valuation", {"voucher_no": doc.name, "status": "Queued", "docstatus": "1"} + ): + doc.repost_future_sle_and_gle() def get_stock_reco_qty_shift(args): From ed14d1ce443e87ba69b1960eb08854af4d62e39e Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Sat, 22 Apr 2023 17:24:35 +0530 Subject: [PATCH 33/75] feat: Reconcile Payments in background (#34596) * feat: auto reconcile in background * chore: Option to enable auto reconciliation in settings * refactor: validate if feature is enabled in settings * refactor: check for running job while using reconciliation tool * chore: using doc to get filter values * chore: use frappe.db.get_value in validations * chore: cleanup commented out code * chore: replace get_list with get_all * chore: use block scope variable * chore: type information for functions * refactor: flag to ignore job validation check * refactor: update parent doc status if all reconciled * chore: create test_records file * test: create a bunch of vouchers for testing auto reconcile * chore: renamed auto_reconcile to process_payment_reconciliation * chore: another child doctype to hold payments * chore: remove duplicate field * chore: add fetched payments to log * chore: Popup comment message update * chore: replace get_all with get_value * chore: replace label in settings page * chore: remove unit test and records * refactor: status in reconciliation log * refactor: set status in log as well * chore: fix field name * chore: change triggered job name * chore: use status field in list view of log * chore: status while there are no allocations * refactor: split trigger function into two * chore: adding cancelled status * refactor: function trigger queued docs * chore: cron job scheduled * chore: fixing accouts settings json file * chore: typos and variable scope * chore: use 'pluck' in db call * chore: remove redundant whitelist decorator * chore: use single DB call to fetch values * chore: replace get_all with get_value * refactor: use raw db calls to fetch reconciliation log records Using get_doc on `Process Payment Reconciliation Log` is costly when handling large volumes of invoices. Use raw frappe.db.get_all to selectively pull status and reconciled count * chore: update status on successful batch operation * chore: make payment table readonly * chore: ability to pause the background job * chore: remove isolate_each_allocation * chore: more description in progress bar * refactor: partially working state * refactor: update reconcile flag and setting hard limits for fetching * chore: make allocation editable -- NEED TO REVERT * chore: pause button * refactor: skip setter function in Payment Entry for better performan * refactor: split reconcile function and skip a setter function 1. Split reconcile function into 2 2. While reconciling against payment entry, skip a set_missing_ref_details setter method * chore: increase payment limit * refactor: replace frappe.db.get_all with frappe.db.get_value * chore: remove unwanted doctypes * refactor: make allocation table readonly * perf: update ref_details only for newly linked invoices * chore: rename skip flag * refactor(UI): receivable_payable field should auto populate * refactor: no control statements in finally block * chore: cleanup section and rename checkbox * chore: update new fieldname in code * chore: update error msg * refactor: start and pause integrated into status pause checkbox has been removed * refactor: added cancelled status to the log doctype 1. Moved the status section to the bottom in parent doc 2. Using alerts to indicate Job trigger status --- .../accounts_settings/accounts_settings.json | 21 +- .../payment_reconciliation.js | 26 + .../payment_reconciliation.py | 34 +- .../__init__.py | 0 .../process_payment_reconciliation.js | 130 +++++ .../process_payment_reconciliation.json | 173 ++++++ .../process_payment_reconciliation.py | 503 ++++++++++++++++++ ...rocess_payment_reconciliation_dashboard.py | 15 + .../process_payment_reconciliation_list.js | 15 + .../test_process_payment_reconciliation.py | 9 + .../__init__.py | 0 .../process_payment_reconciliation_log.js | 17 + .../process_payment_reconciliation_log.json | 137 +++++ .../process_payment_reconciliation_log.py | 9 + ...process_payment_reconciliation_log_list.js | 15 + ...test_process_payment_reconciliation_log.py | 9 + .../__init__.py | 0 ...ayment_reconciliation_log_allocations.json | 170 ++++++ ..._payment_reconciliation_log_allocations.py | 9 + erpnext/accounts/utils.py | 13 +- erpnext/hooks.py | 1 + 21 files changed, 1290 insertions(+), 16 deletions(-) create mode 100644 erpnext/accounts/doctype/process_payment_reconciliation/__init__.py create mode 100644 erpnext/accounts/doctype/process_payment_reconciliation/process_payment_reconciliation.js create mode 100644 erpnext/accounts/doctype/process_payment_reconciliation/process_payment_reconciliation.json create mode 100644 erpnext/accounts/doctype/process_payment_reconciliation/process_payment_reconciliation.py create mode 100644 erpnext/accounts/doctype/process_payment_reconciliation/process_payment_reconciliation_dashboard.py create mode 100644 erpnext/accounts/doctype/process_payment_reconciliation/process_payment_reconciliation_list.js create mode 100644 erpnext/accounts/doctype/process_payment_reconciliation/test_process_payment_reconciliation.py create mode 100644 erpnext/accounts/doctype/process_payment_reconciliation_log/__init__.py create mode 100644 erpnext/accounts/doctype/process_payment_reconciliation_log/process_payment_reconciliation_log.js create mode 100644 erpnext/accounts/doctype/process_payment_reconciliation_log/process_payment_reconciliation_log.json create mode 100644 erpnext/accounts/doctype/process_payment_reconciliation_log/process_payment_reconciliation_log.py create mode 100644 erpnext/accounts/doctype/process_payment_reconciliation_log/process_payment_reconciliation_log_list.js create mode 100644 erpnext/accounts/doctype/process_payment_reconciliation_log/test_process_payment_reconciliation_log.py create mode 100644 erpnext/accounts/doctype/process_payment_reconciliation_log_allocations/__init__.py create mode 100644 erpnext/accounts/doctype/process_payment_reconciliation_log_allocations/process_payment_reconciliation_log_allocations.json create mode 100644 erpnext/accounts/doctype/process_payment_reconciliation_log_allocations/process_payment_reconciliation_log_allocations.py diff --git a/erpnext/accounts/doctype/accounts_settings/accounts_settings.json b/erpnext/accounts/doctype/accounts_settings/accounts_settings.json index a354d7a36c..2996836de8 100644 --- a/erpnext/accounts/doctype/accounts_settings/accounts_settings.json +++ b/erpnext/accounts/doctype/accounts_settings/accounts_settings.json @@ -40,6 +40,8 @@ "show_payment_schedule_in_print", "currency_exchange_section", "allow_stale", + "section_break_jpd0", + "auto_reconcile_payments", "stale_days", "invoicing_settings_tab", "accounts_transactions_settings_section", @@ -59,7 +61,6 @@ "acc_frozen_upto", "column_break_25", "frozen_accounts_modifier", - "report_settings_sb", "tab_break_dpet", "show_balance_in_coa" ], @@ -172,11 +173,6 @@ "fieldtype": "Int", "label": "Stale Days" }, - { - "fieldname": "report_settings_sb", - "fieldtype": "Section Break", - "label": "Report Settings" - }, { "default": "0", "description": "Only select this if you have set up the Cash Flow Mapper documents", @@ -383,6 +379,17 @@ "fieldname": "merge_similar_account_heads", "fieldtype": "Check", "label": "Merge Similar Account Heads" + }, + { + "fieldname": "section_break_jpd0", + "fieldtype": "Section Break", + "label": "Payment Reconciliations" + }, + { + "default": "0", + "fieldname": "auto_reconcile_payments", + "fieldtype": "Check", + "label": "Auto Reconcile Payments" } ], "icon": "icon-cog", @@ -390,7 +397,7 @@ "index_web_pages_for_search": 1, "issingle": 1, "links": [], - "modified": "2023-04-17 11:45:42.049247", + "modified": "2023-04-21 13:11:37.130743", "modified_by": "Administrator", "module": "Accounts", "name": "Accounts Settings", diff --git a/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.js b/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.js index caffac5354..08d38dde47 100644 --- a/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.js +++ b/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.js @@ -82,6 +82,32 @@ erpnext.accounts.PaymentReconciliationController = class PaymentReconciliationCo this.frm.change_custom_button_type('Get Unreconciled Entries', null, 'default'); this.frm.change_custom_button_type('Allocate', null, 'default'); } + + // 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 + } + } + }).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"); + } + }); + } + }); + } + } company() { diff --git a/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py b/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py index d8082d058f..cc2b9420cc 100644 --- a/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py +++ b/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py @@ -7,9 +7,12 @@ from frappe import _, msgprint, qb from frappe.model.document import Document from frappe.query_builder.custom import ConstantColumn from frappe.query_builder.functions import IfNull -from frappe.utils import flt, getdate, nowdate, today +from frappe.utils import flt, get_link_to_form, getdate, nowdate, today import erpnext +from erpnext.accounts.doctype.process_payment_reconciliation.process_payment_reconciliation import ( + is_any_doc_running, +) from erpnext.accounts.utils import ( QueryPaymentLedger, get_outstanding_invoices, @@ -304,9 +307,7 @@ class PaymentReconciliation(Document): } ) - @frappe.whitelist() - def reconcile(self): - self.validate_allocation() + def reconcile_allocations(self, skip_ref_details_update_for_pe=False): dr_or_cr = ( "credit_in_account_currency" if erpnext.get_party_account_type(self.party_type) == "Receivable" @@ -330,12 +331,35 @@ class PaymentReconciliation(Document): self.make_difference_entry(payment_details) if entry_list: - reconcile_against_document(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) + @frappe.whitelist() + def reconcile(self): + if frappe.db.get_single_value("Accounts Settings", "auto_reconcile_payments"): + running_doc = is_any_doc_running( + dict( + company=self.company, + party_type=self.party_type, + party=self.party, + receivable_payable_account=self.receivable_payable_account, + ) + ) + + if running_doc: + frappe.throw( + _("A Reconciliation Job {0} is running for the same filters. Cannot reconcile now").format( + get_link_to_form("Auto Reconcile", running_doc) + ) + ) + return + + self.validate_allocation() + self.reconcile_allocations() msgprint(_("Successfully Reconciled")) + self.get_unreconciled_entries() def make_difference_entry(self, row): diff --git a/erpnext/accounts/doctype/process_payment_reconciliation/__init__.py b/erpnext/accounts/doctype/process_payment_reconciliation/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/erpnext/accounts/doctype/process_payment_reconciliation/process_payment_reconciliation.js b/erpnext/accounts/doctype/process_payment_reconciliation/process_payment_reconciliation.js new file mode 100644 index 0000000000..dd601bfc45 --- /dev/null +++ b/erpnext/accounts/doctype/process_payment_reconciliation/process_payment_reconciliation.js @@ -0,0 +1,130 @@ +// Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and contributors +// For license information, please see license.txt + +frappe.ui.form.on("Process Payment Reconciliation", { + onload: function(frm) { + // set queries + frm.set_query("party_type", function() { + return { + "filters": { + "name": ["in", Object.keys(frappe.boot.party_account_types)], + } + } + }); + frm.set_query('receivable_payable_account', function(doc) { + return { + filters: { + "company": doc.company, + "is_group": 0, + "account_type": frappe.boot.party_account_types[doc.party_type] + } + }; + }); + frm.set_query('cost_center', function(doc) { + return { + filters: { + "company": doc.company, + "is_group": 0, + } + }; + }); + frm.set_query('bank_cash_account', function(doc) { + return { + filters:[ + ['Account', 'company', '=', doc.company], + ['Account', 'is_group', '=', 0], + ['Account', 'account_type', 'in', ['Bank', 'Cash']] + ] + }; + }); + + }, + refresh: function(frm) { + if (frm.doc.docstatus==1 && ['Queued', 'Paused'].find(x => x == frm.doc.status)) { + let execute_btn = __("Start / Resume") + + frm.add_custom_button(execute_btn, () => { + frm.call({ + method: 'erpnext.accounts.doctype.process_payment_reconciliation.process_payment_reconciliation.trigger_job_for_doc', + args: { + docname: frm.doc.name + } + }).then(r => { + if(!r.exc) { + frappe.show_alert(__("Job Started")); + frm.reload_doc(); + } + }); + }); + } + if (frm.doc.docstatus==1 && ['Completed', 'Running', 'Paused', 'Partially Reconciled'].find(x => x == frm.doc.status)) { + frm.call({ + 'method': "erpnext.accounts.doctype.process_payment_reconciliation.process_payment_reconciliation.get_reconciled_count", + args: { + "docname": frm.docname, + } + }).then(r => { + if (r.message) { + let progress = 0; + let description = ""; + + if (r.message.processed) { + progress = (r.message.processed/r.message.total) * 100; + description = r.message.processed + "/" + r.message.total + " processed"; + } else if (r.message.total == 0 && frm.doc.status == "Completed") { + progress = 100; + } + + + frm.dashboard.add_progress('Reconciliation Progress', progress, description); + } + }) + } + if (frm.doc.docstatus==1 && frm.doc.status == 'Running') { + let execute_btn = __("Pause") + + frm.add_custom_button(execute_btn, () => { + frm.call({ + 'method': "erpnext.accounts.doctype.process_payment_reconciliation.process_payment_reconciliation.pause_job_for_doc", + args: { + "docname": frm.docname, + } + }).then(r => { + if (!r.exc) { + frappe.show_alert(__("Job Paused")); + frm.reload_doc() + } + }); + + }); + } + }, + company(frm) { + frm.set_value('party', ''); + frm.set_value('receivable_payable_account', ''); + }, + party_type(frm) { + frm.set_value('party', ''); + }, + + party(frm) { + frm.set_value('receivable_payable_account', ''); + if (!frm.doc.receivable_payable_account && frm.doc.party_type && frm.doc.party) { + return frappe.call({ + method: "erpnext.accounts.party.get_party_account", + args: { + company: frm.doc.company, + party_type: frm.doc.party_type, + party: frm.doc.party + }, + callback: (r) => { + if (!r.exc && r.message) { + frm.set_value("receivable_payable_account", r.message); + } + frm.refresh(); + + } + }); + } + } +}); diff --git a/erpnext/accounts/doctype/process_payment_reconciliation/process_payment_reconciliation.json b/erpnext/accounts/doctype/process_payment_reconciliation/process_payment_reconciliation.json new file mode 100644 index 0000000000..8bb7092dc5 --- /dev/null +++ b/erpnext/accounts/doctype/process_payment_reconciliation/process_payment_reconciliation.json @@ -0,0 +1,173 @@ +{ + "actions": [], + "autoname": "format:ACC-PPR-{#####}", + "beta": 1, + "creation": "2023-03-30 21:28:39.793927", + "default_view": "List", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "company", + "party_type", + "column_break_io6c", + "party", + "receivable_payable_account", + "filter_section", + "from_invoice_date", + "to_invoice_date", + "column_break_kegk", + "from_payment_date", + "to_payment_date", + "column_break_uj04", + "cost_center", + "bank_cash_account", + "section_break_2n02", + "status", + "error_log", + "section_break_a8yx", + "amended_from" + ], + "fields": [ + { + "allow_on_submit": 1, + "fieldname": "status", + "fieldtype": "Select", + "label": "Status", + "options": "\nQueued\nRunning\nPaused\nCompleted\nPartially Reconciled\nFailed\nCancelled", + "read_only": 1 + }, + { + "fieldname": "company", + "fieldtype": "Link", + "in_list_view": 1, + "label": "Company", + "options": "Company", + "reqd": 1 + }, + { + "fieldname": "party_type", + "fieldtype": "Link", + "in_list_view": 1, + "label": "Party Type", + "options": "DocType", + "reqd": 1 + }, + { + "fieldname": "column_break_io6c", + "fieldtype": "Column Break" + }, + { + "fieldname": "party", + "fieldtype": "Dynamic Link", + "in_list_view": 1, + "label": "Party", + "options": "party_type", + "reqd": 1 + }, + { + "fieldname": "receivable_payable_account", + "fieldtype": "Link", + "in_list_view": 1, + "label": "Receivable/Payable Account", + "options": "Account", + "reqd": 1 + }, + { + "fieldname": "filter_section", + "fieldtype": "Section Break", + "label": "Filters" + }, + { + "fieldname": "from_invoice_date", + "fieldtype": "Date", + "label": "From Invoice Date" + }, + { + "fieldname": "to_invoice_date", + "fieldtype": "Date", + "label": "To Invoice Date" + }, + { + "fieldname": "column_break_kegk", + "fieldtype": "Column Break" + }, + { + "fieldname": "from_payment_date", + "fieldtype": "Date", + "label": "From Payment Date" + }, + { + "fieldname": "to_payment_date", + "fieldtype": "Date", + "label": "To Payment Date" + }, + { + "fieldname": "column_break_uj04", + "fieldtype": "Column Break" + }, + { + "fieldname": "cost_center", + "fieldtype": "Link", + "label": "Cost Center", + "options": "Cost Center" + }, + { + "fieldname": "bank_cash_account", + "fieldtype": "Link", + "label": "Bank/Cash Account", + "options": "Account" + }, + { + "fieldname": "section_break_2n02", + "fieldtype": "Section Break", + "label": "Status" + }, + { + "depends_on": "eval:doc.error_log", + "fieldname": "error_log", + "fieldtype": "Long Text", + "label": "Error Log" + }, + { + "fieldname": "amended_from", + "fieldtype": "Link", + "label": "Amended From", + "no_copy": 1, + "options": "Process Payment Reconciliation", + "print_hide": 1, + "read_only": 1 + }, + { + "fieldname": "section_break_a8yx", + "fieldtype": "Section Break" + } + ], + "index_web_pages_for_search": 1, + "is_submittable": 1, + "links": [], + "modified": "2023-04-21 17:19:30.912953", + "modified_by": "Administrator", + "module": "Accounts", + "name": "Process Payment Reconciliation", + "naming_rule": "Expression", + "owner": "Administrator", + "permissions": [ + { + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "System Manager", + "share": 1, + "write": 1 + } + ], + "sort_field": "modified", + "sort_order": "DESC", + "states": [], + "title_field": "company" +} \ No newline at end of file diff --git a/erpnext/accounts/doctype/process_payment_reconciliation/process_payment_reconciliation.py b/erpnext/accounts/doctype/process_payment_reconciliation/process_payment_reconciliation.py new file mode 100644 index 0000000000..ecb51ce144 --- /dev/null +++ b/erpnext/accounts/doctype/process_payment_reconciliation/process_payment_reconciliation.py @@ -0,0 +1,503 @@ +# Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and contributors +# For license information, please see license.txt + +import frappe +from frappe import _, qb +from frappe.model.document import Document +from frappe.utils import get_link_to_form +from frappe.utils.scheduler import is_scheduler_inactive + + +class ProcessPaymentReconciliation(Document): + def validate(self): + self.validate_receivable_payable_account() + self.validate_bank_cash_account() + + def validate_receivable_payable_account(self): + if self.receivable_payable_account: + if self.company != frappe.db.get_value("Account", self.receivable_payable_account, "company"): + frappe.throw( + _("Receivable/Payable Account: {0} doesn't belong to company {1}").format( + frappe.bold(self.receivable_payable_account), frappe.bold(self.company) + ) + ) + + def validate_bank_cash_account(self): + if self.bank_cash_account: + if self.company != frappe.db.get_value("Account", self.bank_cash_account, "company"): + frappe.throw( + _("Bank/Cash Account {0} doesn't belong to company {1}").format( + frappe.bold(self.bank_cash_account), frappe.bold(self.company) + ) + ) + + def before_save(self): + self.status = "" + self.error_log = "" + + def on_submit(self): + self.db_set("status", "Queued") + self.db_set("error_log", None) + + def on_cancel(self): + self.db_set("status", "Cancelled") + log = frappe.db.get_value( + "Process Payment Reconciliation Log", filters={"process_pr": self.name} + ) + if log: + frappe.db.set_value("Process Payment Reconciliation Log", log, "status", "Cancelled") + + +@frappe.whitelist() +def get_reconciled_count(docname: str | None = None) -> float: + current_status = {} + if docname: + reconcile_log = frappe.db.get_value( + "Process Payment Reconciliation Log", filters={"process_pr": docname}, fieldname="name" + ) + if reconcile_log: + res = frappe.get_all( + "Process Payment Reconciliation Log", + filters={"name": reconcile_log}, + fields=["reconciled_entries", "total_allocations"], + as_list=1, + ) + current_status["processed"], current_status["total"] = res[0] + + return current_status + + +def get_pr_instance(doc: str): + process_payment_reconciliation = frappe.get_doc("Process Payment Reconciliation", doc) + + pr = frappe.get_doc("Payment Reconciliation") + fields = [ + "company", + "party_type", + "party", + "receivable_payable_account", + "from_invoice_date", + "to_invoice_date", + "from_payment_date", + "to_payment_date", + ] + d = {} + for field in fields: + d[field] = process_payment_reconciliation.get(field) + pr.update(d) + pr.invoice_limit = 1000 + pr.payment_limit = 1000 + return pr + + +def is_job_running(job_name: str) -> bool: + jobs = frappe.db.get_all("RQ Job", filters={"status": ["in", ["started", "queued"]]}) + for x in jobs: + if x.job_name == job_name: + return True + return False + + +@frappe.whitelist() +def pause_job_for_doc(docname: str | None = None): + if docname: + frappe.db.set_value("Process Payment Reconciliation", docname, "status", "Paused") + log = frappe.db.get_value("Process Payment Reconciliation Log", filters={"process_pr": docname}) + if log: + frappe.db.set_value("Process Payment Reconciliation Log", log, "status", "Paused") + + +@frappe.whitelist() +def trigger_job_for_doc(docname: str | None = None): + """ + Trigger background job + """ + if not docname: + return + + if not frappe.db.get_single_value("Accounts Settings", "auto_reconcile_payments"): + frappe.throw( + _("Auto Reconciliation of Payments has been disabled. Enable it through {0}").format( + get_link_to_form("Accounts Settings", "Accounts Settings") + ) + ) + + return + + if not is_scheduler_inactive(): + if frappe.db.get_value("Process Payment Reconciliation", docname, "status") == "Queued": + frappe.db.set_value("Process Payment Reconciliation", docname, "status", "Running") + job_name = f"start_processing_{docname}" + if not is_job_running(job_name): + job = frappe.enqueue( + method="erpnext.accounts.doctype.process_payment_reconciliation.process_payment_reconciliation.reconcile_based_on_filters", + queue="long", + is_async=True, + job_name=job_name, + enqueue_after_commit=True, + doc=docname, + ) + + elif frappe.db.get_value("Process Payment Reconciliation", docname, "status") == "Paused": + frappe.db.set_value("Process Payment Reconciliation", docname, "status", "Running") + log = frappe.db.get_value("Process Payment Reconciliation Log", filters={"process_pr": docname}) + if log: + frappe.db.set_value("Process Payment Reconciliation Log", log, "status", "Running") + + # Resume tasks for running doc + job_name = f"start_processing_{docname}" + if not is_job_running(job_name): + job = frappe.enqueue( + method="erpnext.accounts.doctype.process_payment_reconciliation.process_payment_reconciliation.reconcile_based_on_filters", + queue="long", + is_async=True, + job_name=job_name, + doc=docname, + ) + else: + frappe.msgprint(_("Scheduler is Inactive. Can't trigger job now.")) + + +def trigger_reconciliation_for_queued_docs(): + """ + Will be called from Cron Job + Fetch queued docs and start reconciliation process for each one + """ + if not frappe.db.get_single_value("Accounts Settings", "auto_reconcile_payments"): + frappe.throw( + _("Auto Reconciliation of Payments has been disabled. Enable it through {0}").format( + get_link_to_form("Accounts Settings", "Accounts Settings") + ) + ) + + return + + if not is_scheduler_inactive(): + # Get all queued documents + all_queued = frappe.db.get_all( + "Process Payment Reconciliation", + filters={"docstatus": 1, "status": "Queued"}, + order_by="creation desc", + as_list=1, + ) + + docs_to_trigger = [] + unique_filters = set() + queue_size = 5 + + fields = ["company", "party_type", "party", "receivable_payable_account"] + + def get_filters_as_tuple(fields, doc): + filters = () + for x in fields: + filters += tuple(doc.get(x)) + return filters + + for x in all_queued: + doc = frappe.get_doc("Process Payment Reconciliation", x) + filters = get_filters_as_tuple(fields, doc) + if filters not in unique_filters: + unique_filters.add(filters) + docs_to_trigger.append(doc.name) + if len(docs_to_trigger) == queue_size: + break + + # trigger reconcilation process for queue_size unique filters + for doc in docs_to_trigger: + trigger_job_for_doc(doc) + + else: + frappe.msgprint(_("Scheduler is Inactive. Can't trigger jobs now.")) + + +def reconcile_based_on_filters(doc: None | str = None) -> None: + """ + Identify current state of document and execute next tasks in background + """ + if doc: + log = frappe.db.get_value("Process Payment Reconciliation Log", filters={"process_pr": doc}) + if not log: + log = frappe.new_doc("Process Payment Reconciliation Log") + log.process_pr = doc + log.status = "Running" + log = log.save() + + job_name = f"process_{doc}_fetch_and_allocate" + if not is_job_running(job_name): + job = frappe.enqueue( + method="erpnext.accounts.doctype.process_payment_reconciliation.process_payment_reconciliation.fetch_and_allocate", + queue="long", + timeout="3600", + is_async=True, + job_name=job_name, + enqueue_after_commit=True, + doc=doc, + ) + else: + res = frappe.get_all( + "Process Payment Reconciliation Log", + filters={"name": log}, + fields=["allocated", "reconciled"], + as_list=1, + ) + allocated, reconciled = res[0] + + if not allocated: + job_name = f"process__{doc}_fetch_and_allocate" + if not is_job_running(job_name): + job = frappe.enqueue( + method="erpnext.accounts.doctype.process_payment_reconciliation.process_payment_reconciliation.fetch_and_allocate", + queue="long", + timeout="3600", + is_async=True, + job_name=job_name, + enqueue_after_commit=True, + doc=doc, + ) + elif not reconciled: + allocation = get_next_allocation(log) + if allocation: + reconcile_job_name = ( + f"process_{doc}_reconcile_allocation_{allocation[0].idx}_{allocation[-1].idx}" + ) + else: + reconcile_job_name = f"process_{doc}_reconcile" + if not is_job_running(reconcile_job_name): + job = frappe.enqueue( + method="erpnext.accounts.doctype.process_payment_reconciliation.process_payment_reconciliation.reconcile", + queue="long", + timeout="3600", + is_async=True, + job_name=reconcile_job_name, + enqueue_after_commit=True, + doc=doc, + ) + elif reconciled: + frappe.db.set_value("Process Payment Reconciliation", doc, "status", "Completed") + + +def get_next_allocation(log: str) -> list: + if log: + allocations = [] + next = frappe.db.get_all( + "Process Payment Reconciliation Log Allocations", + filters={"parent": log, "reconciled": 0}, + fields=["reference_type", "reference_name"], + order_by="idx", + limit=1, + ) + + if next: + allocations = frappe.db.get_all( + "Process Payment Reconciliation Log Allocations", + filters={ + "parent": log, + "reconciled": 0, + "reference_type": next[0].reference_type, + "reference_name": next[0].reference_name, + }, + fields=["*"], + order_by="idx", + ) + + return allocations + return [] + + +def fetch_and_allocate(doc: str) -> None: + """ + Fetch Invoices and Payments based on filters applied. FIFO ordering is used for allocation. + """ + + if doc: + log = frappe.db.get_value("Process Payment Reconciliation Log", filters={"process_pr": doc}) + if log: + if not frappe.db.get_value("Process Payment Reconciliation Log", log, "allocated"): + reconcile_log = frappe.get_doc("Process Payment Reconciliation Log", log) + + pr = get_pr_instance(doc) + pr.get_unreconciled_entries() + + if len(pr.invoices) > 0 and len(pr.payments) > 0: + invoices = [x.as_dict() for x in pr.invoices] + payments = [x.as_dict() for x in pr.payments] + pr.allocate_entries(frappe._dict({"invoices": invoices, "payments": payments})) + + for x in pr.get("allocation"): + reconcile_log.append( + "allocations", + x.as_dict().update( + { + "parenttype": "Process Payment Reconciliation Log", + "parent": reconcile_log.name, + "name": None, + "reconciled": False, + } + ), + ) + reconcile_log.allocated = True + reconcile_log.total_allocations = len(reconcile_log.get("allocations")) + reconcile_log.reconciled_entries = 0 + reconcile_log.save() + + # generate reconcile job name + allocation = get_next_allocation(log) + if allocation: + reconcile_job_name = ( + f"process_{doc}_reconcile_allocation_{allocation[0].idx}_{allocation[-1].idx}" + ) + else: + reconcile_job_name = f"process_{doc}_reconcile" + + if not is_job_running(reconcile_job_name): + job = frappe.enqueue( + method="erpnext.accounts.doctype.process_payment_reconciliation.process_payment_reconciliation.reconcile", + queue="long", + timeout="3600", + is_async=True, + job_name=reconcile_job_name, + enqueue_after_commit=True, + doc=doc, + ) + + +def reconcile(doc: None | str = None) -> None: + if doc: + log = frappe.db.get_value("Process Payment Reconciliation Log", filters={"process_pr": doc}) + if log: + res = frappe.get_all( + "Process Payment Reconciliation Log", + filters={"name": log}, + fields=["reconciled_entries", "total_allocations"], + as_list=1, + limit=1, + ) + + reconciled_entries, total_allocations = res[0] + if reconciled_entries != total_allocations: + try: + # Fetch next allocation + allocations = get_next_allocation(log) + + pr = get_pr_instance(doc) + + # pass allocation to PR instance + for x in allocations: + pr.append("allocation", x) + + # reconcile + pr.reconcile_allocations(skip_ref_details_update_for_pe=True) + + # If Payment Entry, update details only for newly linked references + # This is for performance + if allocations[0].reference_type == "Payment Entry": + + references = [(x.invoice_type, x.invoice_number) for x in allocations] + pe = frappe.get_doc(allocations[0].reference_type, allocations[0].reference_name) + pe.flags.ignore_validate_update_after_submit = True + pe.set_missing_ref_details(update_ref_details_only_for=references) + pe.save() + + # Update reconciled flag + allocation_names = [x.name for x in allocations] + ppa = qb.DocType("Process Payment Reconciliation Log Allocations") + qb.update(ppa).set(ppa.reconciled, True).where(ppa.name.isin(allocation_names)).run() + + # Update reconciled count + reconciled_count = frappe.db.count( + "Process Payment Reconciliation Log Allocations", filters={"parent": log, "reconciled": True} + ) + frappe.db.set_value( + "Process Payment Reconciliation Log", log, "reconciled_entries", reconciled_count + ) + + except Exception as err: + # Update the parent doc about the exception + frappe.db.rollback() + + traceback = frappe.get_traceback() + if traceback: + message = "Traceback:
" + traceback + frappe.db.set_value("Process Payment Reconciliation Log", log, "error_log", message) + frappe.db.set_value( + "Process Payment Reconciliation", + doc, + "error_log", + message, + ) + if reconciled_entries and total_allocations and reconciled_entries < total_allocations: + frappe.db.set_value( + "Process Payment Reconciliation Log", log, "status", "Partially Reconciled" + ) + frappe.db.set_value( + "Process Payment Reconciliation", + doc, + "status", + "Partially Reconciled", + ) + else: + frappe.db.set_value("Process Payment Reconciliation Log", log, "status", "Failed") + frappe.db.set_value( + "Process Payment Reconciliation", + doc, + "status", + "Failed", + ) + finally: + if reconciled_entries == total_allocations: + frappe.db.set_value("Process Payment Reconciliation Log", log, "status", "Reconciled") + frappe.db.set_value("Process Payment Reconciliation Log", log, "reconciled", True) + frappe.db.set_value("Process Payment Reconciliation", doc, "status", "Completed") + else: + + if not (frappe.db.get_value("Process Payment Reconciliation", doc, "status") == "Paused"): + # trigger next batch in job + # generate reconcile job name + allocation = get_next_allocation(log) + if allocation: + reconcile_job_name = ( + f"process_{doc}_reconcile_allocation_{allocation[0].idx}_{allocation[-1].idx}" + ) + else: + reconcile_job_name = f"process_{doc}_reconcile" + + if not is_job_running(reconcile_job_name): + job = frappe.enqueue( + method="erpnext.accounts.doctype.process_payment_reconciliation.process_payment_reconciliation.reconcile", + queue="long", + timeout="3600", + is_async=True, + job_name=reconcile_job_name, + enqueue_after_commit=True, + doc=doc, + ) + else: + frappe.db.set_value("Process Payment Reconciliation Log", log, "status", "Reconciled") + frappe.db.set_value("Process Payment Reconciliation Log", log, "reconciled", True) + frappe.db.set_value("Process Payment Reconciliation", doc, "status", "Completed") + + +@frappe.whitelist() +def is_any_doc_running(for_filter: str | dict | None = None) -> str | None: + running_doc = None + if for_filter: + if type(for_filter) == str: + for_filter = frappe.json.loads(for_filter) + + running_doc = frappe.db.get_value( + "Process Payment Reconciliation", + filters={ + "docstatus": 1, + "status": ["in", ["Running", "Paused"]], + "company": for_filter.get("company"), + "party_type": for_filter.get("party_type"), + "party": for_filter.get("party"), + "receivable_payable_account": for_filter.get("receivable_payable_account"), + }, + fieldname="name", + ) + else: + running_doc = frappe.db.get_value( + "Process Payment Reconciliation", filters={"docstatus": 1, "status": "Running"} + ) + return running_doc diff --git a/erpnext/accounts/doctype/process_payment_reconciliation/process_payment_reconciliation_dashboard.py b/erpnext/accounts/doctype/process_payment_reconciliation/process_payment_reconciliation_dashboard.py new file mode 100644 index 0000000000..784f4548bd --- /dev/null +++ b/erpnext/accounts/doctype/process_payment_reconciliation/process_payment_reconciliation_dashboard.py @@ -0,0 +1,15 @@ +from frappe import _ + + +def get_data(): + return { + "fieldname": "process_pr", + "transactions": [ + { + "label": _("Reconciliation Logs"), + "items": [ + "Process Payment Reconciliation Log", + ], + }, + ], + } diff --git a/erpnext/accounts/doctype/process_payment_reconciliation/process_payment_reconciliation_list.js b/erpnext/accounts/doctype/process_payment_reconciliation/process_payment_reconciliation_list.js new file mode 100644 index 0000000000..8012d6e037 --- /dev/null +++ b/erpnext/accounts/doctype/process_payment_reconciliation/process_payment_reconciliation_list.js @@ -0,0 +1,15 @@ +frappe.listview_settings['Process Payment Reconciliation'] = { + add_fields: ["status"], + get_indicator: function(doc) { + let colors = { + 'Queued': 'orange', + 'Paused': 'orange', + 'Completed': 'green', + 'Partially Reconciled': 'orange', + 'Running': 'blue', + 'Failed': 'red', + }; + let status = doc.status; + return [__(status), colors[status], 'status,=,'+status]; + }, +}; diff --git a/erpnext/accounts/doctype/process_payment_reconciliation/test_process_payment_reconciliation.py b/erpnext/accounts/doctype/process_payment_reconciliation/test_process_payment_reconciliation.py new file mode 100644 index 0000000000..ad1e952579 --- /dev/null +++ b/erpnext/accounts/doctype/process_payment_reconciliation/test_process_payment_reconciliation.py @@ -0,0 +1,9 @@ +# Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and Contributors +# See license.txt + +# import frappe +from frappe.tests.utils import FrappeTestCase + + +class TestProcessPaymentReconciliation(FrappeTestCase): + pass diff --git a/erpnext/accounts/doctype/process_payment_reconciliation_log/__init__.py b/erpnext/accounts/doctype/process_payment_reconciliation_log/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/erpnext/accounts/doctype/process_payment_reconciliation_log/process_payment_reconciliation_log.js b/erpnext/accounts/doctype/process_payment_reconciliation_log/process_payment_reconciliation_log.js new file mode 100644 index 0000000000..2468f10bcc --- /dev/null +++ b/erpnext/accounts/doctype/process_payment_reconciliation_log/process_payment_reconciliation_log.js @@ -0,0 +1,17 @@ +// Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and contributors +// For license information, please see license.txt + +frappe.ui.form.on("Process Payment Reconciliation Log", { + refresh(frm) { + if (['Completed', 'Running', 'Paused', 'Partially Reconciled'].find(x => x == frm.doc.status)) { + let progress = 0; + if (frm.doc.reconciled_entries != 0) { + progress = frm.doc.reconciled_entries / frm.doc.total_allocations * 100; + } else if(frm.doc.total_allocations == 0 && frm.doc.status == "Completed"){ + progress = 100; + } + frm.dashboard.add_progress(__('Reconciliation Progress'), progress); + } + + }, +}); diff --git a/erpnext/accounts/doctype/process_payment_reconciliation_log/process_payment_reconciliation_log.json b/erpnext/accounts/doctype/process_payment_reconciliation_log/process_payment_reconciliation_log.json new file mode 100644 index 0000000000..1131a0fca6 --- /dev/null +++ b/erpnext/accounts/doctype/process_payment_reconciliation_log/process_payment_reconciliation_log.json @@ -0,0 +1,137 @@ +{ + "actions": [], + "autoname": "format:PPR-LOG-{##}", + "beta": 1, + "creation": "2023-03-13 15:00:09.149681", + "default_view": "List", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "process_pr", + "section_break_fvdw", + "status", + "tasks_section", + "allocated", + "reconciled", + "column_break_yhin", + "total_allocations", + "reconciled_entries", + "section_break_4ywv", + "error_log", + "allocations_section", + "allocations" + ], + "fields": [ + { + "fieldname": "allocations", + "fieldtype": "Table", + "label": "Allocations", + "options": "Process Payment Reconciliation Log Allocations", + "read_only": 1 + }, + { + "default": "0", + "description": "All allocations have been successfully reconciled", + "fieldname": "reconciled", + "fieldtype": "Check", + "label": "Reconciled", + "read_only": 1 + }, + { + "fieldname": "total_allocations", + "fieldtype": "Int", + "in_list_view": 1, + "label": "Total Allocations", + "read_only": 1 + }, + { + "default": "0", + "description": "Invoices and Payments have been Fetched and Allocated", + "fieldname": "allocated", + "fieldtype": "Check", + "label": "Allocated", + "read_only": 1 + }, + { + "fieldname": "reconciled_entries", + "fieldtype": "Int", + "in_list_view": 1, + "label": "Reconciled Entries", + "read_only": 1 + }, + { + "fieldname": "tasks_section", + "fieldtype": "Section Break", + "label": "Tasks" + }, + { + "fieldname": "allocations_section", + "fieldtype": "Section Break", + "label": "Allocations" + }, + { + "fieldname": "column_break_yhin", + "fieldtype": "Column Break" + }, + { + "fieldname": "section_break_4ywv", + "fieldtype": "Section Break" + }, + { + "depends_on": "eval:doc.error_log", + "fieldname": "error_log", + "fieldtype": "Long Text", + "label": "Reconciliation Error Log", + "read_only": 1 + }, + { + "fieldname": "process_pr", + "fieldtype": "Link", + "in_list_view": 1, + "label": "Parent Document", + "options": "Process Payment Reconciliation", + "read_only": 1, + "reqd": 1 + }, + { + "fieldname": "section_break_fvdw", + "fieldtype": "Section Break", + "label": "Status" + }, + { + "fieldname": "status", + "fieldtype": "Select", + "label": "Status", + "options": "Running\nPaused\nReconciled\nPartially Reconciled\nFailed\nCancelled", + "read_only": 1 + } + ], + "in_create": 1, + "index_web_pages_for_search": 1, + "links": [], + "modified": "2023-04-21 17:36:26.642617", + "modified_by": "Administrator", + "module": "Accounts", + "name": "Process Payment Reconciliation Log", + "naming_rule": "Expression", + "owner": "Administrator", + "permissions": [ + { + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "System Manager", + "share": 1, + "write": 1 + } + ], + "search_fields": "allocated, reconciled, total_allocations, reconciled_entries", + "sort_field": "modified", + "sort_order": "DESC", + "states": [] +} \ No newline at end of file diff --git a/erpnext/accounts/doctype/process_payment_reconciliation_log/process_payment_reconciliation_log.py b/erpnext/accounts/doctype/process_payment_reconciliation_log/process_payment_reconciliation_log.py new file mode 100644 index 0000000000..85d70a4832 --- /dev/null +++ b/erpnext/accounts/doctype/process_payment_reconciliation_log/process_payment_reconciliation_log.py @@ -0,0 +1,9 @@ +# Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and contributors +# For license information, please see license.txt + +# import frappe +from frappe.model.document import Document + + +class ProcessPaymentReconciliationLog(Document): + pass diff --git a/erpnext/accounts/doctype/process_payment_reconciliation_log/process_payment_reconciliation_log_list.js b/erpnext/accounts/doctype/process_payment_reconciliation_log/process_payment_reconciliation_log_list.js new file mode 100644 index 0000000000..5a652048a2 --- /dev/null +++ b/erpnext/accounts/doctype/process_payment_reconciliation_log/process_payment_reconciliation_log_list.js @@ -0,0 +1,15 @@ +frappe.listview_settings['Process Payment Reconciliation Log'] = { + add_fields: ["status"], + get_indicator: function(doc) { + var colors = { + 'Partially Reconciled': 'orange', + 'Paused': 'orange', + 'Reconciled': 'green', + 'Failed': 'red', + 'Cancelled': 'red', + 'Running': 'blue', + }; + let status = doc.status; + return [__(status), colors[status], "status,=,"+status]; + }, +}; diff --git a/erpnext/accounts/doctype/process_payment_reconciliation_log/test_process_payment_reconciliation_log.py b/erpnext/accounts/doctype/process_payment_reconciliation_log/test_process_payment_reconciliation_log.py new file mode 100644 index 0000000000..c2da62e2de --- /dev/null +++ b/erpnext/accounts/doctype/process_payment_reconciliation_log/test_process_payment_reconciliation_log.py @@ -0,0 +1,9 @@ +# Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and Contributors +# See license.txt + +# import frappe +from frappe.tests.utils import FrappeTestCase + + +class TestProcessPaymentReconciliationLog(FrappeTestCase): + pass diff --git a/erpnext/accounts/doctype/process_payment_reconciliation_log_allocations/__init__.py b/erpnext/accounts/doctype/process_payment_reconciliation_log_allocations/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/erpnext/accounts/doctype/process_payment_reconciliation_log_allocations/process_payment_reconciliation_log_allocations.json b/erpnext/accounts/doctype/process_payment_reconciliation_log_allocations/process_payment_reconciliation_log_allocations.json new file mode 100644 index 0000000000..b97d73886a --- /dev/null +++ b/erpnext/accounts/doctype/process_payment_reconciliation_log_allocations/process_payment_reconciliation_log_allocations.json @@ -0,0 +1,170 @@ +{ + "actions": [], + "creation": "2023-03-13 13:51:27.351463", + "default_view": "List", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "reference_type", + "reference_name", + "reference_row", + "column_break_3", + "invoice_type", + "invoice_number", + "section_break_6", + "allocated_amount", + "unreconciled_amount", + "column_break_8", + "amount", + "is_advance", + "section_break_5", + "difference_amount", + "column_break_7", + "difference_account", + "exchange_rate", + "currency", + "reconciled" + ], + "fields": [ + { + "fieldname": "reference_type", + "fieldtype": "Link", + "label": "Reference Type", + "options": "DocType", + "read_only": 1, + "reqd": 1 + }, + { + "fieldname": "reference_name", + "fieldtype": "Dynamic Link", + "in_list_view": 1, + "label": "Reference Name", + "options": "reference_type", + "read_only": 1, + "reqd": 1 + }, + { + "fieldname": "reference_row", + "fieldtype": "Data", + "hidden": 1, + "label": "Reference Row", + "read_only": 1 + }, + { + "fieldname": "column_break_3", + "fieldtype": "Column Break" + }, + { + "fieldname": "invoice_type", + "fieldtype": "Link", + "label": "Invoice Type", + "options": "DocType", + "read_only": 1, + "reqd": 1 + }, + { + "fieldname": "invoice_number", + "fieldtype": "Dynamic Link", + "in_list_view": 1, + "label": "Invoice Number", + "options": "invoice_type", + "read_only": 1, + "reqd": 1 + }, + { + "fieldname": "section_break_6", + "fieldtype": "Section Break" + }, + { + "fieldname": "allocated_amount", + "fieldtype": "Currency", + "in_list_view": 1, + "label": "Allocated Amount", + "options": "currency", + "reqd": 1 + }, + { + "fieldname": "unreconciled_amount", + "fieldtype": "Currency", + "hidden": 1, + "label": "Unreconciled Amount", + "options": "currency", + "read_only": 1 + }, + { + "fieldname": "column_break_8", + "fieldtype": "Column Break" + }, + { + "fieldname": "amount", + "fieldtype": "Currency", + "hidden": 1, + "label": "Amount", + "options": "currency", + "read_only": 1 + }, + { + "fieldname": "is_advance", + "fieldtype": "Data", + "hidden": 1, + "label": "Is Advance", + "read_only": 1 + }, + { + "fieldname": "section_break_5", + "fieldtype": "Section Break" + }, + { + "fieldname": "difference_amount", + "fieldtype": "Currency", + "in_list_view": 1, + "label": "Difference Amount", + "options": "Currency", + "read_only": 1 + }, + { + "fieldname": "column_break_7", + "fieldtype": "Column Break" + }, + { + "fieldname": "difference_account", + "fieldtype": "Link", + "label": "Difference Account", + "options": "Account", + "read_only": 1 + }, + { + "fieldname": "exchange_rate", + "fieldtype": "Float", + "label": "Exchange Rate", + "read_only": 1 + }, + { + "fieldname": "currency", + "fieldtype": "Link", + "hidden": 1, + "label": "Currency", + "options": "Currency" + }, + { + "default": "0", + "fieldname": "reconciled", + "fieldtype": "Check", + "in_list_view": 1, + "label": "Reconciled" + } + ], + "istable": 1, + "links": [], + "modified": "2023-03-20 21:05:43.121945", + "modified_by": "Administrator", + "module": "Accounts", + "name": "Process Payment Reconciliation Log Allocations", + "owner": "Administrator", + "permissions": [], + "sort_field": "modified", + "sort_order": "DESC", + "states": [], + "track_changes": 1 +} \ No newline at end of file diff --git a/erpnext/accounts/doctype/process_payment_reconciliation_log_allocations/process_payment_reconciliation_log_allocations.py b/erpnext/accounts/doctype/process_payment_reconciliation_log_allocations/process_payment_reconciliation_log_allocations.py new file mode 100644 index 0000000000..c3e43297d0 --- /dev/null +++ b/erpnext/accounts/doctype/process_payment_reconciliation_log_allocations/process_payment_reconciliation_log_allocations.py @@ -0,0 +1,9 @@ +# Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and contributors +# For license information, please see license.txt + +# import frappe +from frappe.model.document import Document + + +class ProcessPaymentReconciliationLogAllocations(Document): + pass diff --git a/erpnext/accounts/utils.py b/erpnext/accounts/utils.py index f10cff0686..890872f433 100644 --- a/erpnext/accounts/utils.py +++ b/erpnext/accounts/utils.py @@ -436,7 +436,7 @@ def add_cc(args=None): return cc.name -def reconcile_against_document(args): # nosemgrep +def reconcile_against_document(args, skip_ref_details_update_for_pe=False): # nosemgrep """ Cancel PE or JV, Update against document, split if required and resubmit """ @@ -465,7 +465,9 @@ def reconcile_against_document(args): # nosemgrep if voucher_type == "Journal Entry": update_reference_in_journal_entry(entry, doc, do_not_save=True) else: - update_reference_in_payment_entry(entry, doc, do_not_save=True) + update_reference_in_payment_entry( + entry, doc, do_not_save=True, skip_ref_details_update_for_pe=skip_ref_details_update_for_pe + ) doc.save(ignore_permissions=True) # re-submit advance entry @@ -602,7 +604,9 @@ def update_reference_in_journal_entry(d, journal_entry, do_not_save=False): journal_entry.save(ignore_permissions=True) -def update_reference_in_payment_entry(d, payment_entry, do_not_save=False): +def update_reference_in_payment_entry( + d, payment_entry, do_not_save=False, skip_ref_details_update_for_pe=False +): reference_details = { "reference_doctype": d.against_voucher_type, "reference_name": d.against_voucher, @@ -646,7 +650,8 @@ def update_reference_in_payment_entry(d, payment_entry, do_not_save=False): payment_entry.flags.ignore_validate_update_after_submit = True payment_entry.setup_party_account_field() payment_entry.set_missing_values() - payment_entry.set_missing_ref_details() + if not skip_ref_details_update_for_pe: + payment_entry.set_missing_ref_details() payment_entry.set_amounts() if not do_not_save: diff --git a/erpnext/hooks.py b/erpnext/hooks.py index 862a546a76..02b301e995 100644 --- a/erpnext/hooks.py +++ b/erpnext/hooks.py @@ -362,6 +362,7 @@ scheduler_events = { "cron": { "0/15 * * * *": [ "erpnext.manufacturing.doctype.bom_update_log.bom_update_log.resume_bom_cost_update_jobs", + "erpnext.accounts.doctype.process_payment_reconciliation.process_payment_reconciliation.trigger_reconciliation_for_queued_docs", ], "0/30 * * * *": [ "erpnext.utilities.doctype.video.video.update_youtube_data", From 02c3b41dc27f4826b63b5694862af7c0ce9dc7e1 Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Mon, 24 Apr 2023 14:50:27 +0530 Subject: [PATCH 34/75] fix: item not showing in the BOM --- erpnext/manufacturing/doctype/bom/bom.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/manufacturing/doctype/bom/bom.py b/erpnext/manufacturing/doctype/bom/bom.py index a085af859a..b53149affd 100644 --- a/erpnext/manufacturing/doctype/bom/bom.py +++ b/erpnext/manufacturing/doctype/bom/bom.py @@ -1317,7 +1317,7 @@ def item_query(doctype, txt, searchfield, start, page_len, filters): if not field in searchfields ] - query_filters = {"disabled": 0, "end_of_life": (">", today())} + query_filters = {"disabled": 0, "ifnull(end_of_life, '3099-12-31')": (">", today())} or_cond_filters = {} if txt: From 379b215aeafe0d6fe01952cd31ab536ef4959974 Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Mon, 24 Apr 2023 17:32:32 +0530 Subject: [PATCH 35/75] fix: incorrect OR condition causing timeout error --- erpnext/stock/stock_ledger.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/erpnext/stock/stock_ledger.py b/erpnext/stock/stock_ledger.py index 0f12987fbb..6234f95e87 100644 --- a/erpnext/stock/stock_ledger.py +++ b/erpnext/stock/stock_ledger.py @@ -1441,13 +1441,13 @@ def get_next_stock_reco(kwargs): ( CombineDatetime(sle.posting_date, sle.posting_time) > CombineDatetime(kwargs.get("posting_date"), kwargs.get("posting_time")) - | ( - ( - CombineDatetime(sle.posting_date, sle.posting_time) - == CombineDatetime(kwargs.get("posting_date"), kwargs.get("posting_time")) - ) - & (sle.creation > kwargs.get("creation")) + ) + | ( + ( + CombineDatetime(sle.posting_date, sle.posting_time) + == CombineDatetime(kwargs.get("posting_date"), kwargs.get("posting_time")) ) + & (sle.creation > kwargs.get("creation")) ) ) ) From cb7a99cbaa2caa9746a49dd09c2c2bdea5ba1540 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Fri, 21 Apr 2023 12:59:52 +0530 Subject: [PATCH 36/75] Revert "fix: Rate from LDC in TDS reports (#33699)" This reverts commit db9beb3cddc78376ccd30b57efafa35381b482d6. --- .../report/tds_payable_monthly/tds_payable_monthly.py | 7 ------- 1 file changed, 7 deletions(-) diff --git a/erpnext/accounts/report/tds_payable_monthly/tds_payable_monthly.py b/erpnext/accounts/report/tds_payable_monthly/tds_payable_monthly.py index bfe2a0fd2b..98838907be 100644 --- a/erpnext/accounts/report/tds_payable_monthly/tds_payable_monthly.py +++ b/erpnext/accounts/report/tds_payable_monthly/tds_payable_monthly.py @@ -4,7 +4,6 @@ import frappe from frappe import _ -from frappe.utils import flt def execute(filters=None): @@ -66,12 +65,6 @@ def get_result( else: total_amount_credited += entry.credit - ## Check if ldc is applied and show rate as per ldc - actual_rate = (tds_deducted / total_amount_credited) * 100 - - if flt(actual_rate) < flt(rate): - rate = actual_rate - if tds_deducted: row = { "pan" From 7a63fbef4fcb0f57e5ec38f7900c6905cfe3954b Mon Sep 17 00:00:00 2001 From: rohitwaghchaure Date: Tue, 25 Apr 2023 12:01:26 +0530 Subject: [PATCH 37/75] =?UTF-8?q?Revert=20"fix:=20Incorrect=20difference?= =?UTF-8?q?=20value=20in=20Stock=20and=20Account=20Value=20Comparison?= =?UTF-8?q?=E2=80=A6"?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../stock_and_account_value_comparison.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/stock/report/stock_and_account_value_comparison/stock_and_account_value_comparison.py b/erpnext/stock/report/stock_and_account_value_comparison/stock_and_account_value_comparison.py index 5fb456502e..106e877c4c 100644 --- a/erpnext/stock/report/stock_and_account_value_comparison/stock_and_account_value_comparison.py +++ b/erpnext/stock/report/stock_and_account_value_comparison/stock_and_account_value_comparison.py @@ -41,7 +41,7 @@ def get_data(report_filters): key = (d.voucher_type, d.voucher_no) gl_data = voucher_wise_gl_data.get(key) or {} d.account_value = gl_data.get("account_value", 0) - d.difference_value = abs(d.stock_value) - abs(d.account_value) + d.difference_value = d.stock_value - d.account_value if abs(d.difference_value) > 0.1: data.append(d) From ca388ed9cdc05a8797b9812989e88d5cdaf1a811 Mon Sep 17 00:00:00 2001 From: Anand Baburajan Date: Tue, 25 Apr 2023 12:45:05 +0530 Subject: [PATCH 38/75] fix: value of depreciable assets not updating after manual depr entry [develop] (#35020) * fix: value of depreciable assets not updating after manual depr entry * chore: add asset depr schedule to jv's ignore_doctypes_on_cancel_all --- .../accounts/doctype/account/test_account.py | 2 +- .../doctype/journal_entry/journal_entry.js | 2 +- .../doctype/journal_entry/journal_entry.py | 72 ++++++++++++------- .../asset_depreciations_and_balances.py | 24 +------ erpnext/assets/doctype/asset/depreciation.py | 1 + erpnext/assets/doctype/asset/test_asset.py | 62 +++++++++++++++- 6 files changed, 108 insertions(+), 55 deletions(-) diff --git a/erpnext/accounts/doctype/account/test_account.py b/erpnext/accounts/doctype/account/test_account.py index f9c9173af0..3a360c48c4 100644 --- a/erpnext/accounts/doctype/account/test_account.py +++ b/erpnext/accounts/doctype/account/test_account.py @@ -297,7 +297,7 @@ def _make_test_records(verbose=None): # fixed asset depreciation ["_Test Fixed Asset", "Current Assets", 0, "Fixed Asset", None], ["_Test Accumulated Depreciations", "Current Assets", 0, "Accumulated Depreciation", None], - ["_Test Depreciations", "Expenses", 0, None, None], + ["_Test Depreciations", "Expenses", 0, "Depreciation", None], ["_Test Gain/Loss on Asset Disposal", "Expenses", 0, None, None], # Receivable / Payable Account ["_Test Receivable", "Current Assets", 0, "Receivable", None], diff --git a/erpnext/accounts/doctype/journal_entry/journal_entry.js b/erpnext/accounts/doctype/journal_entry/journal_entry.js index 089f20b467..b31cc3212e 100644 --- a/erpnext/accounts/doctype/journal_entry/journal_entry.js +++ b/erpnext/accounts/doctype/journal_entry/journal_entry.js @@ -8,7 +8,7 @@ frappe.provide("erpnext.journal_entry"); frappe.ui.form.on("Journal Entry", { setup: function(frm) { frm.add_fetch("bank_account", "account", "account"); - frm.ignore_doctypes_on_cancel_all = ['Sales Invoice', 'Purchase Invoice', 'Journal Entry', "Repost Payment Ledger"]; + frm.ignore_doctypes_on_cancel_all = ['Sales Invoice', 'Purchase Invoice', 'Journal Entry', "Repost Payment Ledger", 'Asset Depreciation Schedule']; }, refresh: function(frm) { diff --git a/erpnext/accounts/doctype/journal_entry/journal_entry.py b/erpnext/accounts/doctype/journal_entry/journal_entry.py index 0f8ae4f37d..34a753f267 100644 --- a/erpnext/accounts/doctype/journal_entry/journal_entry.py +++ b/erpnext/accounts/doctype/journal_entry/journal_entry.py @@ -69,6 +69,7 @@ class JournalEntry(AccountsController): self.validate_empty_accounts_table() self.set_account_and_party_balance() self.validate_inter_company_accounts() + self.validate_depr_entry_voucher_type() if self.docstatus == 0: self.apply_tax_withholding() @@ -130,6 +131,13 @@ class JournalEntry(AccountsController): if self.total_credit != doc.total_debit or self.total_debit != doc.total_credit: frappe.throw(_("Total Credit/ Debit Amount should be same as linked Journal Entry")) + def validate_depr_entry_voucher_type(self): + if ( + any(d.account_type == "Depreciation" for d in self.get("accounts")) + and self.voucher_type != "Depreciation Entry" + ): + frappe.throw(_("Journal Entry type should be set as Depreciation Entry for asset depreciation")) + def validate_stock_accounts(self): stock_accounts = get_stock_accounts(self.company, self.doctype, self.name) for account in stock_accounts: @@ -233,25 +241,30 @@ class JournalEntry(AccountsController): self.remove(d) def update_asset_value(self): - if self.voucher_type != "Depreciation Entry": + if self.flags.planned_depr_entry or self.voucher_type != "Depreciation Entry": return - processed_assets = [] - for d in self.get("accounts"): if ( - d.reference_type == "Asset" and d.reference_name and d.reference_name not in processed_assets + d.reference_type == "Asset" + and d.reference_name + and d.account_type == "Depreciation" + and d.debit ): - processed_assets.append(d.reference_name) - asset = frappe.get_doc("Asset", d.reference_name) if asset.calculate_depreciation: - continue - - depr_value = d.debit or d.credit - - asset.db_set("value_after_depreciation", asset.value_after_depreciation - depr_value) + fb_idx = 1 + if self.finance_book: + for fb_row in asset.get("finance_books"): + if fb_row.finance_book == self.finance_book: + fb_idx = fb_row.idx + break + fb_row = asset.get("finance_books")[fb_idx - 1] + fb_row.value_after_depreciation -= d.debit + fb_row.db_update() + else: + asset.db_set("value_after_depreciation", asset.value_after_depreciation - d.debit) asset.set_status() @@ -316,42 +329,47 @@ class JournalEntry(AccountsController): if self.voucher_type != "Depreciation Entry": return - processed_assets = [] - for d in self.get("accounts"): if ( - d.reference_type == "Asset" and d.reference_name and d.reference_name not in processed_assets + d.reference_type == "Asset" + and d.reference_name + and d.account_type == "Depreciation" + and d.debit ): - processed_assets.append(d.reference_name) - asset = frappe.get_doc("Asset", d.reference_name) if asset.calculate_depreciation: je_found = False - for row in asset.get("finance_books"): + for fb_row in asset.get("finance_books"): if je_found: break - depr_schedule = get_depr_schedule(asset.name, "Active", row.finance_book) + depr_schedule = get_depr_schedule(asset.name, "Active", fb_row.finance_book) for s in depr_schedule or []: if s.journal_entry == self.name: s.db_set("journal_entry", None) - row.value_after_depreciation += s.depreciation_amount - row.db_update() - - asset.set_status() + fb_row.value_after_depreciation += d.debit + fb_row.db_update() je_found = True break + if not je_found: + fb_idx = 1 + if self.finance_book: + for fb_row in asset.get("finance_books"): + if fb_row.finance_book == self.finance_book: + fb_idx = fb_row.idx + break + + fb_row = asset.get("finance_books")[fb_idx - 1] + fb_row.value_after_depreciation += d.debit + fb_row.db_update() else: - depr_value = d.debit or d.credit - - asset.db_set("value_after_depreciation", asset.value_after_depreciation + depr_value) - - asset.set_status() + asset.db_set("value_after_depreciation", asset.value_after_depreciation + d.debit) + asset.set_status() def unlink_inter_company_jv(self): if ( diff --git a/erpnext/accounts/report/asset_depreciations_and_balances/asset_depreciations_and_balances.py b/erpnext/accounts/report/asset_depreciations_and_balances/asset_depreciations_and_balances.py index 5827697023..d67eee3552 100644 --- a/erpnext/accounts/report/asset_depreciations_and_balances/asset_depreciations_and_balances.py +++ b/erpnext/accounts/report/asset_depreciations_and_balances/asset_depreciations_and_balances.py @@ -114,28 +114,6 @@ def get_assets(filters): sum(results.depreciation_eliminated_during_the_period) as depreciation_eliminated_during_the_period, sum(results.depreciation_amount_during_the_period) as depreciation_amount_during_the_period from (SELECT a.asset_category, - ifnull(sum(case when ds.schedule_date < %(from_date)s and (ifnull(a.disposal_date, 0) = 0 or a.disposal_date >= %(from_date)s) then - ds.depreciation_amount - else - 0 - end), 0) as accumulated_depreciation_as_on_from_date, - ifnull(sum(case when ifnull(a.disposal_date, 0) != 0 and a.disposal_date >= %(from_date)s - and a.disposal_date <= %(to_date)s and ds.schedule_date <= a.disposal_date then - ds.depreciation_amount - else - 0 - end), 0) as depreciation_eliminated_during_the_period, - ifnull(sum(case when ds.schedule_date >= %(from_date)s and ds.schedule_date <= %(to_date)s - and (ifnull(a.disposal_date, 0) = 0 or ds.schedule_date <= a.disposal_date) then - ds.depreciation_amount - else - 0 - end), 0) as depreciation_amount_during_the_period - from `tabAsset` a, `tabAsset Depreciation Schedule` ads, `tabDepreciation Schedule` ds - where a.docstatus=1 and a.company=%(company)s and a.purchase_date <= %(to_date)s and ads.asset = a.name and ads.docstatus=1 and ads.name = ds.parent and ifnull(ds.journal_entry, '') != '' - group by a.asset_category - union - SELECT a.asset_category, ifnull(sum(case when gle.posting_date < %(from_date)s and (ifnull(a.disposal_date, 0) = 0 or a.disposal_date >= %(from_date)s) then gle.debit else @@ -160,7 +138,7 @@ def get_assets(filters): aca.parent = a.asset_category and aca.company_name = %(company)s join `tabCompany` company on company.name = %(company)s - where a.docstatus=1 and a.company=%(company)s and a.calculate_depreciation=0 and a.purchase_date <= %(to_date)s and gle.debit != 0 and gle.is_cancelled = 0 and gle.account = ifnull(aca.depreciation_expense_account, company.depreciation_expense_account) + where a.docstatus=1 and a.company=%(company)s and a.purchase_date <= %(to_date)s and gle.debit != 0 and gle.is_cancelled = 0 and gle.account = ifnull(aca.depreciation_expense_account, company.depreciation_expense_account) group by a.asset_category union SELECT a.asset_category, diff --git a/erpnext/assets/doctype/asset/depreciation.py b/erpnext/assets/doctype/asset/depreciation.py index 028e3d6268..f23ae2f165 100644 --- a/erpnext/assets/doctype/asset/depreciation.py +++ b/erpnext/assets/doctype/asset/depreciation.py @@ -157,6 +157,7 @@ def make_depreciation_entry(asset_depr_schedule_name, date=None): je.append("accounts", debit_entry) je.flags.ignore_permissions = True + je.flags.planned_depr_entry = True je.save() if not je.meta.get_workflow(): je.submit() diff --git a/erpnext/assets/doctype/asset/test_asset.py b/erpnext/assets/doctype/asset/test_asset.py index cde02809f1..203612ff1b 100644 --- a/erpnext/assets/doctype/asset/test_asset.py +++ b/erpnext/assets/doctype/asset/test_asset.py @@ -1511,7 +1511,7 @@ class TestDepreciationBasics(AssetSetup): ) self.assertEqual(asset.status, "Submitted") - self.assertEqual(asset.get("value_after_depreciation"), 100000) + self.assertEqual(asset.get_value_after_depreciation(), 100000) jv = make_journal_entry( "_Test Depreciations - _TC", "_Test Accumulated Depreciations - _TC", 100, save=False @@ -1524,12 +1524,68 @@ class TestDepreciationBasics(AssetSetup): jv.submit() asset.reload() - self.assertEqual(asset.get("value_after_depreciation"), 99900) + self.assertEqual(asset.get_value_after_depreciation(), 99900) jv.cancel() asset.reload() - self.assertEqual(asset.get("value_after_depreciation"), 100000) + self.assertEqual(asset.get_value_after_depreciation(), 100000) + + def test_manual_depreciation_for_depreciable_asset(self): + asset = create_asset( + item_code="Macbook Pro", + calculate_depreciation=1, + purchase_date="2020-01-30", + available_for_use_date="2020-01-30", + expected_value_after_useful_life=10000, + total_number_of_depreciations=10, + frequency_of_depreciation=1, + submit=1, + ) + + self.assertEqual(asset.status, "Submitted") + self.assertEqual(asset.get_value_after_depreciation(), 100000) + + jv = make_journal_entry( + "_Test Depreciations - _TC", "_Test Accumulated Depreciations - _TC", 100, save=False + ) + for d in jv.accounts: + d.reference_type = "Asset" + d.reference_name = asset.name + jv.voucher_type = "Depreciation Entry" + jv.insert() + jv.submit() + + asset.reload() + self.assertEqual(asset.get_value_after_depreciation(), 99900) + + jv.cancel() + + asset.reload() + self.assertEqual(asset.get_value_after_depreciation(), 100000) + + def test_manual_depreciation_with_incorrect_jv_voucher_type(self): + asset = create_asset( + item_code="Macbook Pro", + calculate_depreciation=1, + purchase_date="2020-01-30", + available_for_use_date="2020-01-30", + expected_value_after_useful_life=10000, + total_number_of_depreciations=10, + frequency_of_depreciation=1, + submit=1, + ) + + jv = make_journal_entry( + "_Test Depreciations - _TC", "_Test Accumulated Depreciations - _TC", 100, save=False + ) + for d in jv.accounts: + d.reference_type = "Asset" + d.reference_name = asset.name + d.account_type = "Depreciation" + jv.voucher_type = "Journal Entry" + + self.assertRaises(frappe.ValidationError, jv.insert) def create_asset_data(): From e782a054c80656f378da6108bdd91fae99de685e Mon Sep 17 00:00:00 2001 From: s-aga-r Date: Tue, 25 Apr 2023 13:54:36 +0530 Subject: [PATCH 39/75] refactor: `get_stock_value_on()` to get stock value of multiple warehouses at once --- erpnext/accounts/utils.py | 5 +--- .../incorrect_stock_value_report.py | 2 +- erpnext/stock/utils.py | 28 +++++++++---------- 3 files changed, 15 insertions(+), 20 deletions(-) diff --git a/erpnext/accounts/utils.py b/erpnext/accounts/utils.py index 2ab9ef64b3..015bce5547 100644 --- a/erpnext/accounts/utils.py +++ b/erpnext/accounts/utils.py @@ -1368,10 +1368,7 @@ def get_stock_and_account_balance(account=None, posting_date=None, company=None) if wh_details.account == account and not wh_details.is_group ] - total_stock_value = 0.0 - for warehouse in related_warehouses: - value = get_stock_value_on(warehouse, posting_date) - total_stock_value += value + total_stock_value = get_stock_value_on(related_warehouses, posting_date) precision = frappe.get_precision("Journal Entry Account", "debit_in_account_currency") return flt(account_balance, precision), flt(total_stock_value, precision), related_warehouses diff --git a/erpnext/stock/report/incorrect_stock_value_report/incorrect_stock_value_report.py b/erpnext/stock/report/incorrect_stock_value_report/incorrect_stock_value_report.py index df01b14d11..16ff5278e7 100644 --- a/erpnext/stock/report/incorrect_stock_value_report/incorrect_stock_value_report.py +++ b/erpnext/stock/report/incorrect_stock_value_report/incorrect_stock_value_report.py @@ -84,7 +84,7 @@ def get_data(report_filters): closing_date = add_days(from_date, -1) for key, stock_data in voucher_wise_dict.items(): prev_stock_value = get_stock_value_on( - posting_date=closing_date, item_code=key[0], warehouse=key[1] + posting_date=closing_date, item_code=key[0], warehouses=key[1] ) for data in stock_data: expected_stock_value = prev_stock_value + data.stock_value_difference diff --git a/erpnext/stock/utils.py b/erpnext/stock/utils.py index 9c2e2c805b..fb526971ed 100644 --- a/erpnext/stock/utils.py +++ b/erpnext/stock/utils.py @@ -9,9 +9,9 @@ import frappe from frappe import _ from frappe.query_builder.functions import CombineDatetime, IfNull, Sum from frappe.utils import cstr, flt, get_link_to_form, nowdate, nowtime -from pypika.terms import ExistsCriterion import erpnext +from erpnext.stock.doctype.warehouse.warehouse import get_child_warehouses from erpnext.stock.valuation import FIFOValuation, LIFOValuation BarcodeScanResult = Dict[str, Optional[str]] @@ -54,7 +54,9 @@ def get_stock_value_from_bin(warehouse=None, item_code=None): return stock_value -def get_stock_value_on(warehouse=None, posting_date=None, item_code=None): +def get_stock_value_on( + warehouses: list | str = None, posting_date: str = None, item_code: str = None +) -> float: if not posting_date: posting_date = nowdate() @@ -67,20 +69,16 @@ def get_stock_value_on(warehouse=None, posting_date=None, item_code=None): .orderby(sle.creation, order=frappe.qb.desc) ) - if warehouse: - lft, rgt, is_group = frappe.db.get_value("Warehouse", warehouse, ["lft", "rgt", "is_group"]) + if warehouses: + if isinstance(warehouses, str): + warehouses = [warehouses] - if is_group: - wh = frappe.qb.DocType("Warehouse") - query = query.where( - ExistsCriterion( - frappe.qb.from_(wh) - .select(wh.name) - .where((wh.name == sle.warehouse) & (wh.lft >= lft) & (wh.rgt <= rgt)) - ) - ) - else: - query = query.where(sle.warehouse == warehouse) + warehouses = set(warehouses) + for wh in list(warehouses): + if frappe.db.get_value("Warehouse", wh, "is_group"): + warehouses.update(get_child_warehouses(wh)) + + query = query.where(sle.warehouse.isin(warehouses)) if item_code: query = query.where(sle.item_code == item_code) From e08d636bf7979356f60301260177e57213e84fd7 Mon Sep 17 00:00:00 2001 From: Anand Baburajan Date: Tue, 25 Apr 2023 15:16:50 +0530 Subject: [PATCH 40/75] fix: use filter_by_finance_book instead of only_depreciable_assets in fixed asset register (#35031) fix: use filter_by_finance_book instead of only_depreciable_assets --- .../report/fixed_asset_register/fixed_asset_register.js | 6 +++--- .../report/fixed_asset_register/fixed_asset_register.py | 4 +--- 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/erpnext/assets/report/fixed_asset_register/fixed_asset_register.js b/erpnext/assets/report/fixed_asset_register/fixed_asset_register.js index 65a4226ebd..4f7b836107 100644 --- a/erpnext/assets/report/fixed_asset_register/fixed_asset_register.js +++ b/erpnext/assets/report/fixed_asset_register/fixed_asset_register.js @@ -94,11 +94,11 @@ frappe.query_reports["Fixed Asset Register"] = { label: __("Finance Book"), fieldtype: "Link", options: "Finance Book", - depends_on: "eval: doc.only_depreciable_assets == 1", + depends_on: "eval: doc.filter_by_finance_book == 1", }, { - fieldname:"only_depreciable_assets", - label: __("Only depreciable assets"), + fieldname:"filter_by_finance_book", + label: __("Filter by Finance Book"), fieldtype: "Check" }, { 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 5fbcbe2f7f..984b3fd982 100644 --- a/erpnext/assets/report/fixed_asset_register/fixed_asset_register.py +++ b/erpnext/assets/report/fixed_asset_register/fixed_asset_register.py @@ -45,8 +45,6 @@ def get_conditions(filters): filters.year_end_date = getdate(fiscal_year.year_end_date) conditions[date_field] = ["between", [filters.year_start_date, filters.year_end_date]] - if filters.get("only_depreciable_assets"): - conditions["calculate_depreciation"] = filters.get("only_depreciable_assets") if filters.get("only_existing_assets"): conditions["is_existing_asset"] = filters.get("only_existing_assets") if filters.get("asset_category"): @@ -106,7 +104,7 @@ def get_data(filters): assets_linked_to_fb = None - if filters.only_depreciable_assets: + if filters.filter_by_finance_book: assets_linked_to_fb = frappe.db.get_all( doctype="Asset Finance Book", filters={"finance_book": filters.finance_book or ("is", "not set")}, From 6de71eb15857291295921df984c59d4d871eb9f0 Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Tue, 25 Apr 2023 18:33:31 +0530 Subject: [PATCH 41/75] fix: pass reference_doctype in link queries (#35038) --- erpnext/controllers/queries.py | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/erpnext/controllers/queries.py b/erpnext/controllers/queries.py index b0cf724166..799fed99cc 100644 --- a/erpnext/controllers/queries.py +++ b/erpnext/controllers/queries.py @@ -576,7 +576,9 @@ def get_income_account(doctype, txt, searchfield, start, page_len, filters): @frappe.whitelist() @frappe.validate_and_sanitize_search_inputs -def get_filtered_dimensions(doctype, txt, searchfield, start, page_len, filters): +def get_filtered_dimensions( + doctype, txt, searchfield, start, page_len, filters, reference_doctype=None +): from erpnext.accounts.doctype.accounting_dimension_filter.accounting_dimension_filter import ( get_dimension_filter_map, ) @@ -617,7 +619,12 @@ def get_filtered_dimensions(doctype, txt, searchfield, start, page_len, filters) query_filters.append(["name", query_selector, dimensions]) output = frappe.get_list( - doctype, fields=fields, filters=query_filters, or_filters=or_filters, as_list=1 + doctype, + fields=fields, + filters=query_filters, + or_filters=or_filters, + as_list=1, + reference_doctype=reference_doctype, ) return [tuple(d) for d in set(output)] From b545e3def01fb9b9dd6b964478efcb7d764ce386 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Tue, 25 Apr 2023 19:07:45 +0530 Subject: [PATCH 42/75] fix: Add company field to lower deduction certificate (#34914) --- .../tax_withholding_category.py | 5 +++-- erpnext/patches.txt | 1 + erpnext/patches/v14_0/update_company_in_ldc.py | 14 ++++++++++++++ .../lower_deduction_certificate.json | 11 ++++++++++- 4 files changed, 28 insertions(+), 3 deletions(-) create mode 100644 erpnext/patches/v14_0/update_company_in_ldc.py 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 f0146ea70e..ad3477ef3d 100644 --- a/erpnext/accounts/doctype/tax_withholding_category/tax_withholding_category.py +++ b/erpnext/accounts/doctype/tax_withholding_category/tax_withholding_category.py @@ -215,7 +215,7 @@ def get_tax_row_for_tds(tax_details, tax_amount): } -def get_lower_deduction_certificate(tax_details, pan_no): +def get_lower_deduction_certificate(company, tax_details, pan_no): ldc_name = frappe.db.get_value( "Lower Deduction Certificate", { @@ -223,6 +223,7 @@ def get_lower_deduction_certificate(tax_details, pan_no): "tax_withholding_category": tax_details.tax_withholding_category, "valid_from": (">=", tax_details.from_date), "valid_upto": ("<=", tax_details.to_date), + "company": company, }, "name", ) @@ -255,7 +256,7 @@ def get_tax_amount(party_type, parties, inv, tax_details, posting_date, pan_no=N tax_amount = 0 if party_type == "Supplier": - ldc = get_lower_deduction_certificate(tax_details, pan_no) + ldc = get_lower_deduction_certificate(inv.company, tax_details, pan_no) if tax_deducted: net_total = inv.tax_withholding_net_total if ldc: diff --git a/erpnext/patches.txt b/erpnext/patches.txt index 74c8af1f0c..03c7b01856 100644 --- a/erpnext/patches.txt +++ b/erpnext/patches.txt @@ -332,3 +332,4 @@ execute:frappe.db.set_single_value("Accounts Settings", "merge_similar_account_h erpnext.patches.v14_0.migrate_gl_to_payment_ledger execute:frappe.delete_doc_if_exists("Report", "Tax Detail") erpnext.patches.v15_0.enable_all_leads +erpnext.patches.v14_0.update_company_in_ldc diff --git a/erpnext/patches/v14_0/update_company_in_ldc.py b/erpnext/patches/v14_0/update_company_in_ldc.py new file mode 100644 index 0000000000..ca95cf2fd7 --- /dev/null +++ b/erpnext/patches/v14_0/update_company_in_ldc.py @@ -0,0 +1,14 @@ +# Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and Contributors +# License: MIT. See LICENSE + + +import frappe + +from erpnext import get_default_company + + +def execute(): + company = get_default_company() + if company: + for d in frappe.get_all("Lower Deduction Certificate", pluck="name"): + frappe.db.set_value("Lower Deduction Certificate", d, "company", company, update_modified=False) diff --git a/erpnext/regional/doctype/lower_deduction_certificate/lower_deduction_certificate.json b/erpnext/regional/doctype/lower_deduction_certificate/lower_deduction_certificate.json index c32ab6bec2..d332b4e76b 100644 --- a/erpnext/regional/doctype/lower_deduction_certificate/lower_deduction_certificate.json +++ b/erpnext/regional/doctype/lower_deduction_certificate/lower_deduction_certificate.json @@ -10,6 +10,7 @@ "tax_withholding_category", "fiscal_year", "column_break_3", + "company", "certificate_no", "section_break_3", "supplier", @@ -123,11 +124,18 @@ "label": "Tax Withholding Category", "options": "Tax Withholding Category", "reqd": 1 + }, + { + "fieldname": "company", + "fieldtype": "Link", + "label": "Company", + "options": "Company", + "reqd": 1 } ], "index_web_pages_for_search": 1, "links": [], - "modified": "2021-10-23 18:33:38.962622", + "modified": "2023-04-18 08:25:35.302081", "modified_by": "Administrator", "module": "Regional", "name": "Lower Deduction Certificate", @@ -136,5 +144,6 @@ "permissions": [], "sort_field": "modified", "sort_order": "DESC", + "states": [], "track_changes": 1 } \ No newline at end of file From 22290c2694e626132de692a04e0fce2dba4c29df Mon Sep 17 00:00:00 2001 From: HarryPaulo Date: Tue, 25 Apr 2023 10:43:53 -0300 Subject: [PATCH 43/75] fix: respect title_field from doctype to bulk transactions (#34928) --- erpnext/utilities/bulk_transaction.py | 1 + 1 file changed, 1 insertion(+) diff --git a/erpnext/utilities/bulk_transaction.py b/erpnext/utilities/bulk_transaction.py index c1579b3cbc..7fc1d9734c 100644 --- a/erpnext/utilities/bulk_transaction.py +++ b/erpnext/utilities/bulk_transaction.py @@ -104,6 +104,7 @@ def task(doc_name, from_doctype, to_doctype): obj = mapper[from_doctype][to_doctype](doc_name) obj.flags.ignore_validate = True + obj.set_title_field() obj.insert(ignore_mandatory=True) From f1acc5fabb27999dbae2f56885c8922a4b683139 Mon Sep 17 00:00:00 2001 From: Solufy Solution <34390782+Solufyin@users.noreply.github.com> Date: Tue, 25 Apr 2023 19:16:30 +0530 Subject: [PATCH 44/75] fix: Bulk Payment Entry from PO/SO (#34942) Co-authored-by: Nihantra Patel --- .../buying/doctype/purchase_order/purchase_order_list.js | 2 +- erpnext/selling/doctype/sales_order/sales_order_list.js | 2 +- erpnext/utilities/bulk_transaction.py | 6 +++--- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/erpnext/buying/doctype/purchase_order/purchase_order_list.js b/erpnext/buying/doctype/purchase_order/purchase_order_list.js index d7907e4274..6594746cfc 100644 --- a/erpnext/buying/doctype/purchase_order/purchase_order_list.js +++ b/erpnext/buying/doctype/purchase_order/purchase_order_list.js @@ -43,7 +43,7 @@ frappe.listview_settings['Purchase Order'] = { }); listview.page.add_action_item(__("Advance Payment"), ()=>{ - erpnext.bulk_transaction_processing.create(listview, "Purchase Order", "Advance Payment"); + erpnext.bulk_transaction_processing.create(listview, "Purchase Order", "Payment Entry"); }); } diff --git a/erpnext/selling/doctype/sales_order/sales_order_list.js b/erpnext/selling/doctype/sales_order/sales_order_list.js index 4691190d2a..64c58ef5d7 100644 --- a/erpnext/selling/doctype/sales_order/sales_order_list.js +++ b/erpnext/selling/doctype/sales_order/sales_order_list.js @@ -57,7 +57,7 @@ frappe.listview_settings['Sales Order'] = { }); listview.page.add_action_item(__("Advance Payment"), ()=>{ - erpnext.bulk_transaction_processing.create(listview, "Sales Order", "Advance Payment"); + erpnext.bulk_transaction_processing.create(listview, "Sales Order", "Payment Entry"); }); } diff --git a/erpnext/utilities/bulk_transaction.py b/erpnext/utilities/bulk_transaction.py index 7fc1d9734c..5e57b31793 100644 --- a/erpnext/utilities/bulk_transaction.py +++ b/erpnext/utilities/bulk_transaction.py @@ -69,7 +69,7 @@ def task(doc_name, from_doctype, to_doctype): "Sales Order": { "Sales Invoice": sales_order.make_sales_invoice, "Delivery Note": sales_order.make_delivery_note, - "Advance Payment": payment_entry.get_payment_entry, + "Payment Entry": payment_entry.get_payment_entry, }, "Sales Invoice": { "Delivery Note": sales_invoice.make_delivery_note, @@ -86,11 +86,11 @@ def task(doc_name, from_doctype, to_doctype): "Supplier Quotation": { "Purchase Order": supplier_quotation.make_purchase_order, "Purchase Invoice": supplier_quotation.make_purchase_invoice, - "Advance Payment": payment_entry.get_payment_entry, }, "Purchase Order": { "Purchase Invoice": purchase_order.make_purchase_invoice, "Purchase Receipt": purchase_order.make_purchase_receipt, + "Payment Entry": payment_entry.get_payment_entry, }, "Purchase Invoice": { "Purchase Receipt": purchase_invoice.make_purchase_receipt, @@ -98,7 +98,7 @@ def task(doc_name, from_doctype, to_doctype): }, "Purchase Receipt": {"Purchase Invoice": purchase_receipt.make_purchase_invoice}, } - if to_doctype in ["Advance Payment", "Payment Entry"]: + if to_doctype in ["Payment Entry"]: obj = mapper[from_doctype][to_doctype](from_doctype, doc_name) else: obj = mapper[from_doctype][to_doctype](doc_name) From ecea9b44a339fcfb261696118add5873a4d625fb Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Tue, 25 Apr 2023 19:17:13 +0530 Subject: [PATCH 45/75] fix: Payment entry with TDS in bank reco statement (#34961) --- erpnext/accounts/doctype/bank_clearance/bank_clearance.py | 2 +- .../report/bank_clearance_summary/bank_clearance_summary.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/erpnext/accounts/doctype/bank_clearance/bank_clearance.py b/erpnext/accounts/doctype/bank_clearance/bank_clearance.py index 081718726b..8ad0bd17b4 100644 --- a/erpnext/accounts/doctype/bank_clearance/bank_clearance.py +++ b/erpnext/accounts/doctype/bank_clearance/bank_clearance.py @@ -56,7 +56,7 @@ class BankClearance(Document): select "Payment Entry" as payment_document, name as payment_entry, reference_no as cheque_number, reference_date as cheque_date, - if(paid_from=%(account)s, paid_amount, 0) as credit, + if(paid_from=%(account)s, paid_amount + total_taxes_and_charges, 0) as credit, if(paid_from=%(account)s, 0, received_amount) as debit, posting_date, ifnull(party,if(paid_from=%(account)s,paid_to,paid_from)) as against_account, clearance_date, if(paid_to=%(account)s, paid_to_account_currency, paid_from_account_currency) as account_currency diff --git a/erpnext/accounts/report/bank_clearance_summary/bank_clearance_summary.py b/erpnext/accounts/report/bank_clearance_summary/bank_clearance_summary.py index 306af722ba..2d68bb70b8 100644 --- a/erpnext/accounts/report/bank_clearance_summary/bank_clearance_summary.py +++ b/erpnext/accounts/report/bank_clearance_summary/bank_clearance_summary.py @@ -80,7 +80,7 @@ def get_entries(filters): payment_entries = frappe.db.sql( """SELECT "Payment Entry", name, posting_date, reference_no, clearance_date, party, - if(paid_from=%(account)s, paid_amount * -1, received_amount) + if(paid_from=%(account)s, ((paid_amount * -1) - total_taxes_and_charges) , received_amount) FROM `tabPayment Entry` WHERE From 72b5c1f70a51f18cda48c8e71602ee2d620e582b Mon Sep 17 00:00:00 2001 From: Nabin Hait Date: Tue, 25 Apr 2023 19:18:08 +0530 Subject: [PATCH 46/75] fix: Use set instead of db_set as it is called from validate (#34967) --- erpnext/crm/doctype/opportunity/opportunity.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/crm/doctype/opportunity/opportunity.py b/erpnext/crm/doctype/opportunity/opportunity.py index 6a5fead0f8..2a8d65f486 100644 --- a/erpnext/crm/doctype/opportunity/opportunity.py +++ b/erpnext/crm/doctype/opportunity/opportunity.py @@ -59,7 +59,7 @@ class Opportunity(TransactionBase, CRMNote): if not self.get(field) and frappe.db.field_exists(self.opportunity_from, field): try: value = frappe.db.get_value(self.opportunity_from, self.party_name, field) - self.db_set(field, value) + self.set(field, value) except Exception: continue From f7b50f2adef11b9f2bd843d6bca3f2b6cf7be19b Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Tue, 25 Apr 2023 19:18:45 +0530 Subject: [PATCH 47/75] fix: Unable to allocate advance against invoice (#35007) --- erpnext/controllers/accounts_controller.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/erpnext/controllers/accounts_controller.py b/erpnext/controllers/accounts_controller.py index 642d51c325..6982f716bf 100644 --- a/erpnext/controllers/accounts_controller.py +++ b/erpnext/controllers/accounts_controller.py @@ -1662,7 +1662,10 @@ class AccountsController(TransactionBase): ) self.append("payment_schedule", data) - if not automatically_fetch_payment_terms: + if not ( + automatically_fetch_payment_terms + and self.linked_order_has_payment_terms(po_or_so, fieldname, doctype) + ): for d in self.get("payment_schedule"): if d.invoice_portion: d.payment_amount = flt( From 74fb2bec3a5055d6bb76be0d5e3fd80899c642d1 Mon Sep 17 00:00:00 2001 From: Nandhinidevi123 Date: Tue, 25 Apr 2023 20:08:35 +0530 Subject: [PATCH 48/75] add if condition for workstation filter --- erpnext/manufacturing/doctype/bom/bom.js | 2 +- erpnext/manufacturing/doctype/routing/routing.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/erpnext/manufacturing/doctype/bom/bom.js b/erpnext/manufacturing/doctype/bom/bom.js index 7cdcef9c7a..ad9aafe066 100644 --- a/erpnext/manufacturing/doctype/bom/bom.js +++ b/erpnext/manufacturing/doctype/bom/bom.js @@ -652,7 +652,7 @@ frappe.ui.form.on("BOM Operation", "operation", function(frm, cdt, cdn) { frappe.ui.form.on("BOM Operation", "workstation", function(frm, cdt, cdn) { var d = locals[cdt][cdn]; - + if(!d.workstation) return; frappe.call({ "method": "frappe.client.get", args: { diff --git a/erpnext/manufacturing/doctype/routing/routing.js b/erpnext/manufacturing/doctype/routing/routing.js index b480c70ad5..784e83a4c0 100644 --- a/erpnext/manufacturing/doctype/routing/routing.js +++ b/erpnext/manufacturing/doctype/routing/routing.js @@ -50,7 +50,7 @@ frappe.ui.form.on('BOM Operation', { workstation: function(frm, cdt, cdn) { const d = locals[cdt][cdn]; - + if(!d.workstation) return; frappe.call({ "method": "frappe.client.get", args: { From d6bc8bba8b7ed748483bf61b03c8c87eb54f8ab0 Mon Sep 17 00:00:00 2001 From: Raffael Meyer <14891507+barredterra@users.noreply.github.com> Date: Tue, 25 Apr 2023 17:21:11 +0200 Subject: [PATCH 49/75] fix: per_billed condition for Payment Entry (#34969) --- erpnext/accounts/doctype/payment_entry/payment_entry.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/erpnext/accounts/doctype/payment_entry/payment_entry.py b/erpnext/accounts/doctype/payment_entry/payment_entry.py index ee4d4d29e2..082128ac1e 100644 --- a/erpnext/accounts/doctype/payment_entry/payment_entry.py +++ b/erpnext/accounts/doctype/payment_entry/payment_entry.py @@ -1693,7 +1693,10 @@ def get_payment_entry( ): reference_doc = None doc = frappe.get_doc(dt, dn) - if dt in ("Sales Order", "Purchase Order") and flt(doc.per_billed, 2) >= 99.99: + over_billing_allowance = frappe.db.get_single_value("Accounts Settings", "over_billing_allowance") + if dt in ("Sales Order", "Purchase Order") and flt(doc.per_billed, 2) >= ( + 100.0 + over_billing_allowance + ): frappe.throw(_("Can only make payment against unbilled {0}").format(dt)) if not party_type: From f88431a79a6bda662352ec38b8fe650c7f07fdd3 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Tue, 25 Apr 2023 20:54:22 +0530 Subject: [PATCH 50/75] fix: Common party JV cost center (#35008) --- erpnext/controllers/accounts_controller.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/erpnext/controllers/accounts_controller.py b/erpnext/controllers/accounts_controller.py index 6982f716bf..d0ec654162 100644 --- a/erpnext/controllers/accounts_controller.py +++ b/erpnext/controllers/accounts_controller.py @@ -1904,12 +1904,14 @@ class AccountsController(TransactionBase): reconcilation_entry.party = secondary_party reconcilation_entry.reference_type = self.doctype reconcilation_entry.reference_name = self.name - reconcilation_entry.cost_center = self.cost_center + reconcilation_entry.cost_center = self.cost_center or erpnext.get_default_cost_center( + self.company + ) advance_entry.account = primary_account advance_entry.party_type = primary_party_type advance_entry.party = primary_party - advance_entry.cost_center = self.cost_center + advance_entry.cost_center = self.cost_center or erpnext.get_default_cost_center(self.company) advance_entry.is_advance = "Yes" if self.doctype == "Sales Invoice": From c4512d552e3262276df907148fafa57319bdebb1 Mon Sep 17 00:00:00 2001 From: Ernesto Ruiz Date: Tue, 25 Apr 2023 09:26:53 -0600 Subject: [PATCH 51/75] chore: Add translate function to Depreciation Journal Entry Remark (#35022) chore: Add translate function to Depreciation Journal Entry Remark --- erpnext/assets/doctype/asset/asset.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/assets/doctype/asset/asset.py b/erpnext/assets/doctype/asset/asset.py index 6001254762..42f531189a 100644 --- a/erpnext/assets/doctype/asset/asset.py +++ b/erpnext/assets/doctype/asset/asset.py @@ -783,7 +783,7 @@ def make_journal_entry(asset_name): je.voucher_type = "Depreciation Entry" je.naming_series = depreciation_series je.company = asset.company - je.remark = "Depreciation Entry against asset {0}".format(asset_name) + je.remark = _("Depreciation Entry against asset {0}").format(asset_name) je.append( "accounts", From 3d90b970d19ee0f41582dada6dbcdc4999fde10a Mon Sep 17 00:00:00 2001 From: tundebabzy Date: Tue, 25 Apr 2023 16:27:59 +0100 Subject: [PATCH 52/75] fix: click handler should not attempt indexed access of empty array (#35013) fix: click handler should not attempt indexed access of empty array --- erpnext/public/js/projects/timer.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/public/js/projects/timer.js b/erpnext/public/js/projects/timer.js index 9dae7118d9..0209f4c232 100644 --- a/erpnext/public/js/projects/timer.js +++ b/erpnext/public/js/projects/timer.js @@ -68,7 +68,7 @@ erpnext.timesheet.control_timer = function(frm, dialog, row, timestamp=0) { // New activity if no activities found var args = dialog.get_values(); if(!args) return; - if (frm.doc.time_logs.length <= 1 && !frm.doc.time_logs[0].activity_type && !frm.doc.time_logs[0].from_time) { + if (frm.doc.time_logs.length == 1 && !frm.doc.time_logs[0].activity_type && !frm.doc.time_logs[0].from_time) { frm.doc.time_logs = []; } row = frappe.model.add_child(frm.doc, "Timesheet Detail", "time_logs"); From ab0f7794b77a1cc6ff312331fd84e1541cfe2ac8 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Tue, 25 Apr 2023 21:08:50 +0530 Subject: [PATCH 53/75] fix: wrong qty of remaining work orders to be created when using "Create" > "Work Order" (#34726) fix: wrong qty of remaining work orders to be created when using "Create" > "Work Order" (#34726) * fix: convert asynchronous field update to synchronous * fix: wrong qty of remaining work orders to be created when using "Create" > "Work Order" (cherry picked from commit 189b020d228bdb1c0c589697162cf91718b2fa27) Co-authored-by: danjeremynavarro <46537526+danjeremynavarro@users.noreply.github.com> --- erpnext/selling/doctype/sales_order/sales_order.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/erpnext/selling/doctype/sales_order/sales_order.py b/erpnext/selling/doctype/sales_order/sales_order.py index ee9161bee4..d995517af8 100755 --- a/erpnext/selling/doctype/sales_order/sales_order.py +++ b/erpnext/selling/doctype/sales_order/sales_order.py @@ -1340,8 +1340,9 @@ def get_work_order_items(sales_order, for_raw_material_request=0): .select(Sum(wo.qty)) .where( (wo.production_item == i.item_code) - & (wo.sales_order == so.name) * (wo.sales_order_item == i.name) - & (wo.docstatus.lte(2)) + & (wo.sales_order == so.name) + & (wo.sales_order_item == i.name) + & (wo.docstatus.lt(2)) ) .run()[0][0] ) From c36dc3dc57629c4f77e492508beb09b705a2f25b Mon Sep 17 00:00:00 2001 From: "Kitti U. @ Ecosoft" Date: Tue, 25 Apr 2023 23:04:28 +0700 Subject: [PATCH 54/75] fix: v14, Bank Reconcile Tools not cover case JV debit bank (#35000) --- .../doctype/bank_transaction/bank_transaction.py | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/erpnext/accounts/doctype/bank_transaction/bank_transaction.py b/erpnext/accounts/doctype/bank_transaction/bank_transaction.py index fcbaf329f5..b441af9660 100644 --- a/erpnext/accounts/doctype/bank_transaction/bank_transaction.py +++ b/erpnext/accounts/doctype/bank_transaction/bank_transaction.py @@ -281,10 +281,13 @@ def get_paid_amount(payment_entry, currency, gl_bank_account): ) elif payment_entry.payment_document == "Journal Entry": - return frappe.db.get_value( - "Journal Entry Account", - {"parent": payment_entry.payment_entry, "account": gl_bank_account}, - "sum(credit_in_account_currency)", + return abs( + frappe.db.get_value( + "Journal Entry Account", + {"parent": payment_entry.payment_entry, "account": gl_bank_account}, + "sum(debit_in_account_currency-credit_in_account_currency)", + ) + or 0 ) elif payment_entry.payment_document == "Expense Claim": From 3be1ab9b8dbf339e23138e5e518c950b2e7a1b97 Mon Sep 17 00:00:00 2001 From: HarryPaulo Date: Tue, 25 Apr 2023 13:24:41 -0300 Subject: [PATCH 55/75] fix: allow submit delivery note when the sales order was billed... (#34910) --- erpnext/stock/doctype/delivery_note/delivery_note.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/erpnext/stock/doctype/delivery_note/delivery_note.py b/erpnext/stock/doctype/delivery_note/delivery_note.py index 9f9f5cbe2a..9f6dd24fa6 100644 --- a/erpnext/stock/doctype/delivery_note/delivery_note.py +++ b/erpnext/stock/doctype/delivery_note/delivery_note.py @@ -271,6 +271,9 @@ class DeliveryNote(SellingController): def check_credit_limit(self): from erpnext.selling.doctype.customer.customer import check_credit_limit + if self.per_billed == 100: + return + extra_amount = 0 validate_against_credit_limit = False bypass_credit_limit_check_at_sales_order = cint( From 1e2deee57933792e612c4f2c63cae147d8242194 Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Wed, 26 Apr 2023 20:30:53 +0530 Subject: [PATCH 56/75] fix: don't create material request from sales order against the delivered items --- .../doctype/sales_order/sales_order.py | 4 +-- .../doctype/sales_order/test_sales_order.py | 31 +++++++++++++++++++ 2 files changed, 33 insertions(+), 2 deletions(-) diff --git a/erpnext/selling/doctype/sales_order/sales_order.py b/erpnext/selling/doctype/sales_order/sales_order.py index ee9161bee4..67971d10b6 100755 --- a/erpnext/selling/doctype/sales_order/sales_order.py +++ b/erpnext/selling/doctype/sales_order/sales_order.py @@ -547,7 +547,7 @@ def make_material_request(source_name, target_doc=None): # qty is for packed items, because packed items don't have stock_qty field qty = source.get("qty") target.project = source_parent.project - target.qty = qty - requested_item_qty.get(source.name, 0) + target.qty = qty - requested_item_qty.get(source.name, 0) - source.delivered_qty target.stock_qty = flt(target.qty) * flt(target.conversion_factor) args = target.as_dict().copy() @@ -581,7 +581,7 @@ def make_material_request(source_name, target_doc=None): "doctype": "Material Request Item", "field_map": {"name": "sales_order_item", "parent": "sales_order"}, "condition": lambda doc: not frappe.db.exists("Product Bundle", doc.item_code) - and doc.stock_qty > requested_item_qty.get(doc.name, 0), + and (doc.stock_qty - doc.delivered_qty) > requested_item_qty.get(doc.name, 0), "postprocess": update_item, }, }, diff --git a/erpnext/selling/doctype/sales_order/test_sales_order.py b/erpnext/selling/doctype/sales_order/test_sales_order.py index 627914f0c7..ba8bbc2185 100644 --- a/erpnext/selling/doctype/sales_order/test_sales_order.py +++ b/erpnext/selling/doctype/sales_order/test_sales_order.py @@ -1878,6 +1878,37 @@ class TestSalesOrder(FrappeTestCase): self.assertEqual(pe.references[1].reference_name, so.name) self.assertEqual(pe.references[1].allocated_amount, 300) + def test_delivered_item_material_request(self): + "SO -> MR (Manufacture) -> WO. Test if WO Qty is updated in SO." + from erpnext.manufacturing.doctype.work_order.work_order import ( + make_stock_entry as make_se_from_wo, + ) + from erpnext.stock.doctype.material_request.material_request import raise_work_orders + + so = make_sales_order( + item_list=[ + {"item_code": "_Test FG Item", "qty": 10, "rate": 100, "warehouse": "Work In Progress - _TC"} + ] + ) + + make_stock_entry( + item_code="_Test FG Item", target="Work In Progress - _TC", qty=4, basic_rate=100 + ) + + dn = make_delivery_note(so.name) + dn.items[0].qty = 4 + dn.submit() + + so.load_from_db() + self.assertEqual(so.items[0].delivered_qty, 4) + + mr = make_material_request(so.name) + mr.material_request_type = "Purchase" + mr.schedule_date = today() + mr.save() + + self.assertEqual(mr.items[0].qty, 6) + def automatically_fetch_payment_terms(enable=1): accounts_settings = frappe.get_doc("Accounts Settings") From 6dfca79af31e4aa39a48a323f23c7b326b229504 Mon Sep 17 00:00:00 2001 From: Nihantra Patel Date: Thu, 27 Apr 2023 15:04:50 +0530 Subject: [PATCH 57/75] fix: Report link, option, and added a link for Sales Person in GP --- erpnext/accounts/report/gross_profit/gross_profit.js | 2 +- erpnext/accounts/report/gross_profit/gross_profit.py | 9 +++++---- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/erpnext/accounts/report/gross_profit/gross_profit.js b/erpnext/accounts/report/gross_profit/gross_profit.js index e89d42977b..53921dc66e 100644 --- a/erpnext/accounts/report/gross_profit/gross_profit.js +++ b/erpnext/accounts/report/gross_profit/gross_profit.js @@ -73,7 +73,7 @@ frappe.query_reports["Gross Profit"] = { if (column.fieldname == "sales_invoice" && column.options == "Item" && data && data.indent == 0) { column._options = "Sales Invoice"; } else { - column._options = "Item"; + column._options = ""; } value = default_formatter(value, row, column, data); diff --git a/erpnext/accounts/report/gross_profit/gross_profit.py b/erpnext/accounts/report/gross_profit/gross_profit.py index 01fee281b0..81f5928628 100644 --- a/erpnext/accounts/report/gross_profit/gross_profit.py +++ b/erpnext/accounts/report/gross_profit/gross_profit.py @@ -250,7 +250,7 @@ def get_columns(group_wise_columns, filters): "label": _("Warehouse"), "fieldname": "warehouse", "fieldtype": "Link", - "options": "warehouse", + "options": "Warehouse", "width": 100, }, "qty": {"label": _("Qty"), "fieldname": "qty", "fieldtype": "Float", "width": 80}, @@ -305,7 +305,8 @@ def get_columns(group_wise_columns, filters): "sales_person": { "label": _("Sales Person"), "fieldname": "sales_person", - "fieldtype": "Data", + "fieldtype": "Link", + "options": "Sales Person", "width": 100, }, "allocated_amount": { @@ -326,14 +327,14 @@ def get_columns(group_wise_columns, filters): "label": _("Customer Group"), "fieldname": "customer_group", "fieldtype": "Link", - "options": "customer", + "options": "Customer Group", "width": 100, }, "territory": { "label": _("Territory"), "fieldname": "territory", "fieldtype": "Link", - "options": "territory", + "options": "Territory", "width": 100, }, "monthly": { From 72dd7884a89b7dcce342410c8bc3f3da56bf99e8 Mon Sep 17 00:00:00 2001 From: Nihantra Patel Date: Thu, 27 Apr 2023 17:04:39 +0530 Subject: [PATCH 58/75] fix: Hyperlink in Quality Inspection Summary --- .../quality_inspection_summary/quality_inspection_summary.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/manufacturing/report/quality_inspection_summary/quality_inspection_summary.py b/erpnext/manufacturing/report/quality_inspection_summary/quality_inspection_summary.py index de96a6c032..38e05852ee 100644 --- a/erpnext/manufacturing/report/quality_inspection_summary/quality_inspection_summary.py +++ b/erpnext/manufacturing/report/quality_inspection_summary/quality_inspection_summary.py @@ -69,7 +69,7 @@ def get_columns(filters): "label": _("Id"), "fieldname": "name", "fieldtype": "Link", - "options": "Work Order", + "options": "Quality Inspection", "width": 100, }, {"label": _("Report Date"), "fieldname": "report_date", "fieldtype": "Date", "width": 150}, From b44331c981974cae72175fa10c8d10f0378129b9 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Fri, 28 Apr 2023 14:06:04 +0530 Subject: [PATCH 59/75] refactor: checkbox in purchase invoice --- .../doctype/purchase_invoice/purchase_invoice.json | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.json b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.json index b4d369e6c6..f76dfff0f8 100644 --- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.json +++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.json @@ -89,6 +89,7 @@ "column_break8", "grand_total", "rounding_adjustment", + "use_company_roundoff_cost_center", "rounded_total", "in_words", "total_advance", @@ -1559,13 +1560,19 @@ "fieldname": "only_include_allocated_payments", "fieldtype": "Check", "label": "Only Include Allocated Payments" + }, + { + "default": "0", + "fieldname": "use_company_roundoff_cost_center", + "fieldtype": "Check", + "label": "Use Company Default Round Off Cost Center" } ], "icon": "fa fa-file-text", "idx": 204, "is_submittable": 1, "links": [], - "modified": "2023-04-03 22:57:14.074982", + "modified": "2023-04-28 12:57:50.832598", "modified_by": "Administrator", "module": "Accounts", "name": "Purchase Invoice", From ebe67875103e4b2aecad8cc5cd20a896cfe5ebd9 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Fri, 28 Apr 2023 14:07:28 +0530 Subject: [PATCH 60/75] refactor: checkbox to toggle parent doc cost center preference --- .../doctype/purchase_invoice/purchase_invoice.py | 12 ++++++++---- erpnext/accounts/general_ledger.py | 6 ++++-- 2 files changed, 12 insertions(+), 6 deletions(-) diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py index a617447856..868a150edf 100644 --- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py +++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py @@ -978,7 +978,7 @@ class PurchaseInvoice(BuyingController): def make_precision_loss_gl_entry(self, gl_entries): round_off_account, round_off_cost_center = get_round_off_account_and_cost_center( - self.company, "Purchase Invoice", self.name + self.company, "Purchase Invoice", self.name, self.use_company_roundoff_cost_center ) precision_loss = self.get("base_net_total") - flt( @@ -992,7 +992,9 @@ class PurchaseInvoice(BuyingController): "account": round_off_account, "against": self.supplier, "credit": precision_loss, - "cost_center": self.cost_center or round_off_cost_center, + "cost_center": round_off_cost_center + if self.use_company_roundoff_cost_center + else self.cost_center or round_off_cost_center, "remarks": _("Net total calculation precision loss"), } ) @@ -1386,7 +1388,7 @@ class PurchaseInvoice(BuyingController): not self.is_internal_transfer() and self.rounding_adjustment and self.base_rounding_adjustment ): round_off_account, round_off_cost_center = get_round_off_account_and_cost_center( - self.company, "Purchase Invoice", self.name + self.company, "Purchase Invoice", self.name, self.use_company_roundoff_cost_center ) gl_entries.append( @@ -1396,7 +1398,9 @@ class PurchaseInvoice(BuyingController): "against": self.supplier, "debit_in_account_currency": self.rounding_adjustment, "debit": self.base_rounding_adjustment, - "cost_center": self.cost_center or round_off_cost_center, + "cost_center": round_off_cost_center + if self.use_company_roundoff_cost_center + else (self.cost_center or round_off_cost_center), }, item=self, ) diff --git a/erpnext/accounts/general_ledger.py b/erpnext/accounts/general_ledger.py index 6b2546e820..a929ff17b0 100644 --- a/erpnext/accounts/general_ledger.py +++ b/erpnext/accounts/general_ledger.py @@ -475,7 +475,9 @@ def update_accounting_dimensions(round_off_gle): round_off_gle[dimension] = dimension_values.get(dimension) -def get_round_off_account_and_cost_center(company, voucher_type, voucher_no): +def get_round_off_account_and_cost_center( + company, voucher_type, voucher_no, use_company_default=False +): round_off_account, round_off_cost_center = frappe.get_cached_value( "Company", company, ["round_off_account", "round_off_cost_center"] ) or [None, None] @@ -483,7 +485,7 @@ def get_round_off_account_and_cost_center(company, voucher_type, voucher_no): meta = frappe.get_meta(voucher_type) # Give first preference to parent cost center for round off GLE - if meta.has_field("cost_center"): + if not use_company_default and meta.has_field("cost_center"): parent_cost_center = frappe.db.get_value(voucher_type, voucher_no, "cost_center") if parent_cost_center: round_off_cost_center = parent_cost_center From 0f3b06cc8aa36e8f3695d32b3f4a0b915744814b Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Fri, 28 Apr 2023 14:18:08 +0530 Subject: [PATCH 61/75] refactor: checkbox in Sales Invoice --- .../accounts/doctype/sales_invoice/sales_invoice.json | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.json b/erpnext/accounts/doctype/sales_invoice/sales_invoice.json index a41e13c8ea..6a65b30ceb 100644 --- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.json +++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.json @@ -79,6 +79,7 @@ "column_break5", "grand_total", "rounding_adjustment", + "use_company_roundoff_cost_center", "rounded_total", "in_words", "total_advance", @@ -2135,6 +2136,12 @@ "fieldname": "only_include_allocated_payments", "fieldtype": "Check", "label": "Only Include Allocated Payments" + }, + { + "default": "0", + "fieldname": "use_company_roundoff_cost_center", + "fieldtype": "Check", + "label": "Use Company default Cost Center for Round off" } ], "icon": "fa fa-file-text", @@ -2147,7 +2154,7 @@ "link_fieldname": "consolidated_invoice" } ], - "modified": "2023-04-03 22:55:14.206473", + "modified": "2023-04-28 14:15:59.901154", "modified_by": "Administrator", "module": "Accounts", "name": "Sales Invoice", From 4ccce933941d9caab29ac1c551f55777b7da8e0b Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Fri, 28 Apr 2023 14:20:06 +0530 Subject: [PATCH 62/75] refactor: checkbox to toggle parent doc cost center preference --- erpnext/accounts/doctype/sales_invoice/sales_invoice.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py index db619950e1..e16b1b1195 100644 --- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py +++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py @@ -1464,7 +1464,7 @@ class SalesInvoice(SellingController): and not self.is_internal_transfer() ): round_off_account, round_off_cost_center = get_round_off_account_and_cost_center( - self.company, "Sales Invoice", self.name + self.company, "Sales Invoice", self.name, self.use_company_roundoff_cost_center ) gl_entries.append( @@ -1476,7 +1476,9 @@ class SalesInvoice(SellingController): self.rounding_adjustment, self.precision("rounding_adjustment") ), "credit": flt(self.base_rounding_adjustment, self.precision("base_rounding_adjustment")), - "cost_center": self.cost_center or round_off_cost_center, + "cost_center": round_off_cost_center + if self.use_company_roundoff_cost_center + else (self.cost_center or round_off_cost_center), }, item=self, ) From bdf2f7416a012576561e4911e89088c80bc6af6e Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Fri, 28 Apr 2023 15:16:02 +0530 Subject: [PATCH 63/75] fix: not able to create delivery note from sales order --- erpnext/stock/get_item_details.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/erpnext/stock/get_item_details.py b/erpnext/stock/get_item_details.py index ce85702f48..f3adefb3e7 100644 --- a/erpnext/stock/get_item_details.py +++ b/erpnext/stock/get_item_details.py @@ -1508,11 +1508,15 @@ def get_so_reservation_for_item(args): elif args.get("against_sales_invoice"): sales_order = frappe.db.get_all( "Sales Invoice Item", - filters={"parent": args.get("against_sales_invoice"), "item_code": args.get("item_code")}, + filters={ + "parent": args.get("against_sales_invoice"), + "item_code": args.get("item_code"), + "docstatus": 1, + }, fields="sales_order", ) if sales_order and sales_order[0]: - if get_reserved_qty_for_so(sales_order[0][0], args.get("item_code")): + if get_reserved_qty_for_so(sales_order[0].sales_order, args.get("item_code")): reserved_so = sales_order[0] elif args.get("sales_order"): if get_reserved_qty_for_so(args.get("sales_order"), args.get("item_code")): From 123355392b58f9a383632a995ba17e47d4281283 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Thu, 27 Apr 2023 12:56:27 +0530 Subject: [PATCH 64/75] fix: incorrect paid_amount and exchange rate in PE If Company master has no default cash or bank account set but Party has default company bank account set. In this case, paid_amount and conversion rate are not calculated correctly --- .../doctype/payment_entry/payment_entry.py | 37 +++++++++++++------ 1 file changed, 26 insertions(+), 11 deletions(-) diff --git a/erpnext/accounts/doctype/payment_entry/payment_entry.py b/erpnext/accounts/doctype/payment_entry/payment_entry.py index ee4d4d29e2..6e9a48e03f 100644 --- a/erpnext/accounts/doctype/payment_entry/payment_entry.py +++ b/erpnext/accounts/doctype/payment_entry/payment_entry.py @@ -1712,6 +1712,13 @@ def get_payment_entry( # bank or cash bank = get_bank_cash_account(doc, bank_account) + # if default bank or cash account is not set in company master and party has default company bank account, fetch it + if party_type in ["Customer", "Supplier"] and not bank: + party_bank_account = get_party_bank_account(party_type, doc.get(scrub(party_type))) + if party_bank_account: + account = frappe.db.get_value("Bank Account", party_bank_account, "account") + bank = get_bank_cash_account(doc, account) + paid_amount, received_amount = set_paid_amount_and_received_amount( dt, party_account_currency, bank, outstanding_amount, payment_type, bank_amount, doc ) @@ -1928,19 +1935,27 @@ def set_paid_amount_and_received_amount( paid_amount = received_amount = 0 if party_account_currency == bank.account_currency: paid_amount = received_amount = abs(outstanding_amount) - elif payment_type == "Receive": - paid_amount = abs(outstanding_amount) - if bank_amount: - received_amount = bank_amount - else: - received_amount = paid_amount * doc.get("conversion_rate", 1) else: - received_amount = abs(outstanding_amount) - if bank_amount: - paid_amount = bank_amount + company_currency = frappe.get_cached_value("Company", doc.get("company"), "default_currency") + if payment_type == "Receive": + paid_amount = abs(outstanding_amount) + if bank_amount: + received_amount = bank_amount + else: + if company_currency != bank.account_currency: + received_amount = paid_amount / doc.get("conversion_rate", 1) + else: + received_amount = paid_amount * doc.get("conversion_rate", 1) else: - # if party account currency and bank currency is different then populate paid amount as well - paid_amount = received_amount * doc.get("conversion_rate", 1) + received_amount = abs(outstanding_amount) + if bank_amount: + paid_amount = bank_amount + else: + if company_currency != bank.account_currency: + paid_amount = received_amount / doc.get("conversion_rate", 1) + else: + # if party account currency and bank currency is different then populate paid amount as well + paid_amount = received_amount * doc.get("conversion_rate", 1) return paid_amount, received_amount From f3b3dabb9a6e5f8cd34c4a07ed54738b28f2b69a Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Mon, 1 May 2023 10:50:51 +0530 Subject: [PATCH 65/75] fix: Naming series error in Journal Entry template (#35084) --- .../journal_entry_template.js | 27 ++++++++++--------- 1 file changed, 15 insertions(+), 12 deletions(-) diff --git a/erpnext/accounts/doctype/journal_entry_template/journal_entry_template.js b/erpnext/accounts/doctype/journal_entry_template/journal_entry_template.js index 88f1c9069c..5ebdf61db2 100644 --- a/erpnext/accounts/doctype/journal_entry_template/journal_entry_template.js +++ b/erpnext/accounts/doctype/journal_entry_template/journal_entry_template.js @@ -2,6 +2,21 @@ // For license information, please see license.txt frappe.ui.form.on("Journal Entry Template", { + onload: function(frm) { + if(frm.is_new()) { + frappe.call({ + type: "GET", + method: "erpnext.accounts.doctype.journal_entry_template.journal_entry_template.get_naming_series", + callback: function(r){ + if(r.message) { + frm.set_df_property("naming_series", "options", r.message.split("\n")); + frm.set_value("naming_series", r.message.split("\n")[0]); + frm.refresh_field("naming_series"); + } + } + }); + } + }, refresh: function(frm) { frappe.model.set_default_values(frm.doc); @@ -19,18 +34,6 @@ frappe.ui.form.on("Journal Entry Template", { return { filters: filters }; }); - - frappe.call({ - type: "GET", - method: "erpnext.accounts.doctype.journal_entry_template.journal_entry_template.get_naming_series", - callback: function(r){ - if(r.message){ - frm.set_df_property("naming_series", "options", r.message.split("\n")); - frm.set_value("naming_series", r.message.split("\n")[0]); - frm.refresh_field("naming_series"); - } - } - }); }, voucher_type: function(frm) { var add_accounts = function(doc, r) { From 64be694087292f99c81d8550ee5987696ef615c4 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Mon, 1 May 2023 10:56:46 +0530 Subject: [PATCH 66/75] fix: Patch for posting closing balances (#35037) --- erpnext/patches/v14_0/update_closing_balances.py | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/erpnext/patches/v14_0/update_closing_balances.py b/erpnext/patches/v14_0/update_closing_balances.py index 40a18516cb..f47e730fd2 100644 --- a/erpnext/patches/v14_0/update_closing_balances.py +++ b/erpnext/patches/v14_0/update_closing_balances.py @@ -26,7 +26,15 @@ def execute(): pcv_doc.year_start_date = get_fiscal_year( pcv.posting_date, pcv.fiscal_year, company=pcv.company )[1] - gl_entries = pcv_doc.get_gl_entries() + + gl_entries = frappe.db.get_all( + "GL Entry", filters={"voucher_no": pcv.name, "is_cancelled": 0}, fields=["*"] + ) + for entry in gl_entries: + entry["is_period_closing_voucher_entry"] = 1 + entry["closing_date"] = pcv_doc.posting_date + entry["period_closing_voucher"] = pcv_doc.name + closing_entries = pcv_doc.get_grouped_gl_entries(get_opening_entries=get_opening_entries) make_closing_entries(gl_entries + closing_entries, voucher_name=pcv.name) company_wise_order[pcv.company].append(pcv.posting_date) From ea0b03ae9e481dc1048fb5d3fbffc3a948c07573 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Mon, 1 May 2023 13:09:47 +0530 Subject: [PATCH 67/75] fix: Updates in process statement of Accounts (#35064) --- .../process_statement_of_accounts.html | 7 ++++++- .../process_statement_of_accounts.json | 14 +++++++++++++- .../process_statement_of_accounts.py | 2 +- .../process_statement_of_accounts_customer.json | 4 ++-- 4 files changed, 22 insertions(+), 5 deletions(-) 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 b9680dfb3b..03abc93e0b 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 @@ -15,7 +15,12 @@

{{ _("STATEMENTS OF ACCOUNTS") }}

-
{{ _("Customer: ") }} {{filters.party_name[0] }}
+ {% if filters.party[0] == filters.party_name[0] %} +
{{ _("Customer: ") }} {{ filters.party_name[0] }}
+ {% else %} +
{{ _("Customer: ") }} {{ filters.party[0] }}
+
{{ _("Customer Name: ") }} {{filters.party_name[0] }}
+ {% endif %}
{{ _("Date: ") }} {{ frappe.format(filters.from_date, 'Date')}} 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 16602d317a..e23620fd4e 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 @@ -36,6 +36,8 @@ "terms_and_conditions", "section_break_1", "enable_auto_email", + "column_break_ocfq", + "sender", "section_break_18", "frequency", "filter_duration", @@ -298,10 +300,20 @@ "fieldname": "show_net_values_in_party_account", "fieldtype": "Check", "label": "Show Net Values in Party Account" + }, + { + "fieldname": "sender", + "fieldtype": "Link", + "label": "Sender", + "options": "Email Account" + }, + { + "fieldname": "column_break_ocfq", + "fieldtype": "Column Break" } ], "links": [], - "modified": "2022-11-10 17:44:17.165991", + "modified": "2023-04-26 12:46:43.645455", "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 a482931a8e..b36f33be3b 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 @@ -334,7 +334,7 @@ def send_emails(document_name, from_scheduler=False): queue="short", method=frappe.sendmail, recipients=recipients, - sender=frappe.session.user, + sender=doc.sender or frappe.session.user, cc=cc, subject=subject, message=message, diff --git a/erpnext/accounts/doctype/process_statement_of_accounts_customer/process_statement_of_accounts_customer.json b/erpnext/accounts/doctype/process_statement_of_accounts_customer/process_statement_of_accounts_customer.json index 8bffd6a93b..1749d72e16 100644 --- a/erpnext/accounts/doctype/process_statement_of_accounts_customer/process_statement_of_accounts_customer.json +++ b/erpnext/accounts/doctype/process_statement_of_accounts_customer/process_statement_of_accounts_customer.json @@ -27,7 +27,7 @@ }, { "fieldname": "billing_email", - "fieldtype": "Read Only", + "fieldtype": "Data", "in_list_view": 1, "label": "Billing Email" }, @@ -41,7 +41,7 @@ ], "istable": 1, "links": [], - "modified": "2023-03-13 00:12:34.508086", + "modified": "2023-04-26 13:02:41.964499", "modified_by": "Administrator", "module": "Accounts", "name": "Process Statement Of Accounts Customer", From effb34bbfa4144590ab4075cfcb603c50ca5c561 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Tue, 25 Apr 2023 20:26:50 +0530 Subject: [PATCH 68/75] refactor: don't book exch gain/loss for sales/purchase orders --- .../doctype/payment_entry/payment_entry.py | 30 +++++++++++++++---- 1 file changed, 24 insertions(+), 6 deletions(-) diff --git a/erpnext/accounts/doctype/payment_entry/payment_entry.py b/erpnext/accounts/doctype/payment_entry/payment_entry.py index ee4d4d29e2..59641e58e1 100644 --- a/erpnext/accounts/doctype/payment_entry/payment_entry.py +++ b/erpnext/accounts/doctype/payment_entry/payment_entry.py @@ -654,6 +654,28 @@ class PaymentEntry(AccountsController): self.precision("base_received_amount"), ) + def calculate_base_allocated_amount_for_reference(self, d) -> float: + base_allocated_amount = 0 + if d.reference_doctype in frappe.get_hooks("advance_payment_doctypes"): + # When referencing Sales/Purchase Order, use the source/target exchange rate depending on payment type. + # This is so there are no Exchange Gain/Loss generated for such doctypes + + exchange_rate = 1 + if self.payment_type == "Receive": + exchange_rate = self.source_exchange_rate + elif self.payment_type == "Pay": + exchange_rate = self.target_exchange_rate + + base_allocated_amount += flt( + flt(d.allocated_amount) * flt(exchange_rate), self.precision("base_paid_amount") + ) + else: + base_allocated_amount += flt( + flt(d.allocated_amount) * flt(d.exchange_rate), self.precision("base_paid_amount") + ) + + return base_allocated_amount + def set_total_allocated_amount(self): if self.payment_type == "Internal Transfer": return @@ -662,9 +684,7 @@ class PaymentEntry(AccountsController): for d in self.get("references"): if d.allocated_amount: total_allocated_amount += flt(d.allocated_amount) - base_total_allocated_amount += flt( - flt(d.allocated_amount) * flt(d.exchange_rate), self.precision("base_paid_amount") - ) + base_total_allocated_amount += self.calculate_base_allocated_amount_for_reference(d) self.total_allocated_amount = abs(total_allocated_amount) self.base_total_allocated_amount = abs(base_total_allocated_amount) @@ -881,9 +901,7 @@ class PaymentEntry(AccountsController): } ) - allocated_amount_in_company_currency = flt( - flt(d.allocated_amount) * flt(d.exchange_rate), self.precision("paid_amount") - ) + allocated_amount_in_company_currency = self.calculate_base_allocated_amount_for_reference(d) gle.update( { From ce4e18c8d232bbf19fea74e6e6133b773d21f3d5 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Mon, 1 May 2023 13:54:15 +0530 Subject: [PATCH 69/75] test: Sales/Purchase Orders will not book Exchange gain/loss --- .../payment_entry/test_payment_entry.py | 32 +++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/erpnext/accounts/doctype/payment_entry/test_payment_entry.py b/erpnext/accounts/doctype/payment_entry/test_payment_entry.py index 67049c47ad..68f333dc29 100644 --- a/erpnext/accounts/doctype/payment_entry/test_payment_entry.py +++ b/erpnext/accounts/doctype/payment_entry/test_payment_entry.py @@ -51,6 +51,38 @@ class TestPaymentEntry(FrappeTestCase): so_advance_paid = frappe.db.get_value("Sales Order", so.name, "advance_paid") self.assertEqual(so_advance_paid, 0) + def test_payment_against_sales_order_usd_to_inr(self): + so = make_sales_order( + customer="_Test Customer USD", currency="USD", qty=1, rate=100, do_not_submit=True + ) + so.conversion_rate = 50 + so.submit() + pe = get_payment_entry("Sales Order", so.name) + pe.source_exchange_rate = 55 + pe.received_amount = 5500 + pe.insert() + pe.submit() + + # there should be no difference amount + pe.reload() + self.assertEqual(pe.difference_amount, 0) + self.assertEqual(pe.deductions, []) + + expected_gle = dict( + (d[0], d) + for d in [["_Test Receivable USD - _TC", 0, 5500, so.name], ["Cash - _TC", 5500.0, 0, None]] + ) + + self.validate_gl_entries(pe.name, expected_gle) + + so_advance_paid = frappe.db.get_value("Sales Order", so.name, "advance_paid") + self.assertEqual(so_advance_paid, 100) + + pe.cancel() + + so_advance_paid = frappe.db.get_value("Sales Order", so.name, "advance_paid") + self.assertEqual(so_advance_paid, 0) + def test_payment_entry_for_blocked_supplier_invoice(self): supplier = frappe.get_doc("Supplier", "_Test Supplier") supplier.on_hold = 1 From f751727149392cfa304e20b2d50a7cbeef17d388 Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Mon, 1 May 2023 18:47:25 +0530 Subject: [PATCH 70/75] fix: don't allow to make reposting for the closed period --- .../test_period_closing_voucher.py | 20 ++++++++++++- .../repost_item_valuation.py | 29 ++++++++++++++++++- 2 files changed, 47 insertions(+), 2 deletions(-) diff --git a/erpnext/accounts/doctype/period_closing_voucher/test_period_closing_voucher.py b/erpnext/accounts/doctype/period_closing_voucher/test_period_closing_voucher.py index 62ae8572e4..5d08e8d1c2 100644 --- a/erpnext/accounts/doctype/period_closing_voucher/test_period_closing_voucher.py +++ b/erpnext/accounts/doctype/period_closing_voucher/test_period_closing_voucher.py @@ -205,7 +205,7 @@ class TestPeriodClosingVoucher(unittest.TestCase): self.assertRaises(frappe.ValidationError, jv1.submit) - def test_closing_balance_with_dimensions(self): + def test_closing_balance_with_dimensions_and_test_reposting_entry(self): frappe.db.sql("delete from `tabGL Entry` where company='Test PCV Company'") frappe.db.sql("delete from `tabPeriod Closing Voucher` where company='Test PCV Company'") frappe.db.sql("delete from `tabAccount Closing Balance` where company='Test PCV Company'") @@ -299,6 +299,24 @@ class TestPeriodClosingVoucher(unittest.TestCase): self.assertEqual(cc2_closing_balance.credit, 500) self.assertEqual(cc2_closing_balance.credit_in_account_currency, 500) + warehouse = frappe.db.get_value("Warehouse", {"company": company}, "name") + + repost_doc = frappe.get_doc( + { + "doctype": "Repost Item Valuation", + "company": company, + "posting_date": "2020-03-15", + "based_on": "Item and Warehouse", + "item_code": "Test Item 1", + "warehouse": warehouse, + } + ) + + self.assertRaises(frappe.ValidationError, repost_doc.save) + + repost_doc.posting_date = today() + repost_doc.save() + def make_period_closing_voucher(self, posting_date=None, submit=True): surplus_account = create_account() cost_center = create_cost_center("Test Cost Center 1") 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 bbed099da9..aabc6fcfe3 100644 --- a/erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.py +++ b/erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.py @@ -6,7 +6,7 @@ from frappe import _ from frappe.exceptions import QueryDeadlockError, QueryTimeoutError from frappe.model.document import Document from frappe.query_builder import DocType, Interval -from frappe.query_builder.functions import Now +from frappe.query_builder.functions import Max, Now from frappe.utils import cint, get_link_to_form, get_weekday, getdate, now, nowtime from frappe.utils.user import get_users_with_role from rq.timeouts import JobTimeoutException @@ -36,11 +36,38 @@ class RepostItemValuation(Document): ) def validate(self): + self.validate_period_closing_voucher() self.set_status(write=False) self.reset_field_values() self.set_company() self.validate_accounts_freeze() + def validate_period_closing_voucher(self): + 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}" + frappe.throw(_(msg)) + + @staticmethod + def get_max_year_end_date(company): + data = frappe.get_all( + "Period Closing Voucher", fields=["fiscal_year"], filters={"docstatus": 1, "company": company} + ) + + if not data: + return + + fiscal_years = [d.fiscal_year for d in data] + table = frappe.qb.DocType("Fiscal Year") + + query = ( + frappe.qb.from_(table) + .select(Max(table.year_end_date)) + .where((table.name.isin(fiscal_years)) & (table.disabled == 0)) + ).run() + + return query[0][0] if query else None + def validate_accounts_freeze(self): acc_settings = frappe.db.get_value( "Accounts Settings", From 49674585a534974208d67687c6aad8c1a601da99 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Mon, 1 May 2023 19:59:58 +0530 Subject: [PATCH 71/75] fix: handle expected_value_after_useful_life properly in asset value adjustment (backport #35117) (#35119) fix: handle expected_value_after_useful_life properly in asset value adjustment (#35117) (cherry picked from commit 80230fec3ef079b75825447a61836f8a9ce64f57) Co-authored-by: Anand Baburajan --- .../doctype/asset_value_adjustment/asset_value_adjustment.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/erpnext/assets/doctype/asset_value_adjustment/asset_value_adjustment.py b/erpnext/assets/doctype/asset_value_adjustment/asset_value_adjustment.py index 021332883d..8426ed43ff 100644 --- a/erpnext/assets/doctype/asset_value_adjustment/asset_value_adjustment.py +++ b/erpnext/assets/doctype/asset_value_adjustment/asset_value_adjustment.py @@ -150,7 +150,9 @@ class AssetValueAdjustment(Document): if d.depreciation_method in ("Straight Line", "Manual"): end_date = max(s.schedule_date for s in depr_schedule) total_days = date_diff(end_date, self.date) - rate_per_day = flt(d.value_after_depreciation) / flt(total_days) + rate_per_day = flt(d.value_after_depreciation - d.expected_value_after_useful_life) / flt( + total_days + ) from_date = self.date else: no_of_depreciations = len([s.name for s in depr_schedule if not s.journal_entry]) From 2d5ccc07b1f277ebeddc9033af19ba1878fdb734 Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Mon, 1 May 2023 21:17:18 +0530 Subject: [PATCH 72/75] fix: timeout error while submitting delivery note --- erpnext/controllers/stock_controller.py | 16 +++++++--------- .../stock/doctype/delivery_note/delivery_note.py | 4 ++-- .../delivery_note_item/delivery_note_item.json | 8 +++++--- erpnext/stock/stock_ledger.py | 2 +- 4 files changed, 15 insertions(+), 15 deletions(-) diff --git a/erpnext/controllers/stock_controller.py b/erpnext/controllers/stock_controller.py index 479fef72c6..a27e34819d 100644 --- a/erpnext/controllers/stock_controller.py +++ b/erpnext/controllers/stock_controller.py @@ -329,9 +329,10 @@ class StockController(AccountsController): """Create batches if required. Called before submit""" for d in self.items: if d.get(warehouse_field) and not d.batch_no: - has_batch_no, create_new_batch = frappe.db.get_value( + has_batch_no, create_new_batch = frappe.get_cached_value( "Item", d.item_code, ["has_batch_no", "create_new_batch"] ) + if has_batch_no and create_new_batch: d.batch_no = ( frappe.get_doc( @@ -414,7 +415,7 @@ class StockController(AccountsController): "voucher_no": self.name, "voucher_detail_no": d.name, "actual_qty": (self.docstatus == 1 and 1 or -1) * flt(d.get("stock_qty")), - "stock_uom": frappe.db.get_value( + "stock_uom": frappe.get_cached_value( "Item", args.get("item_code") or d.get("item_code"), "stock_uom" ), "incoming_rate": 0, @@ -609,7 +610,7 @@ class StockController(AccountsController): def validate_customer_provided_item(self): for d in self.get("items"): # Customer Provided parts will have zero valuation rate - if frappe.db.get_value("Item", d.item_code, "is_customer_provided_item"): + if frappe.get_cached_value("Item", d.item_code, "is_customer_provided_item"): d.allow_zero_valuation_rate = 1 def set_rate_of_stock_uom(self): @@ -722,7 +723,7 @@ class StockController(AccountsController): message += _("Please adjust the qty or edit {0} to proceed.").format(rule_link) return message - def repost_future_sle_and_gle(self): + def repost_future_sle_and_gle(self, force=False): args = frappe._dict( { "posting_date": self.posting_date, @@ -733,7 +734,7 @@ class StockController(AccountsController): } ) - if future_sle_exists(args) or repost_required_for_queue(self): + if force or future_sle_exists(args) or repost_required_for_queue(self): item_based_reposting = cint( frappe.db.get_single_value("Stock Reposting Settings", "item_based_reposting") ) @@ -894,9 +895,6 @@ def future_sle_exists(args, sl_entries=None): ) for d in data: - if key not in frappe.local.future_sle: - frappe.local.future_sle[key] = frappe._dict({}) - frappe.local.future_sle[key][(d.item_code, d.warehouse)] = d.total_row return len(data) @@ -919,7 +917,7 @@ def validate_future_sle_not_exists(args, key, sl_entries=None): def get_cached_data(args, key): if key not in frappe.local.future_sle: - return False + frappe.local.future_sle[key] = frappe._dict({}) if args.get("item_code"): item_key = (args.get("item_code"), args.get("warehouse")) diff --git a/erpnext/stock/doctype/delivery_note/delivery_note.py b/erpnext/stock/doctype/delivery_note/delivery_note.py index 9f6dd24fa6..c18e851500 100644 --- a/erpnext/stock/doctype/delivery_note/delivery_note.py +++ b/erpnext/stock/doctype/delivery_note/delivery_note.py @@ -118,7 +118,7 @@ class DeliveryNote(SellingController): def so_required(self): """check in manage account if sales order required or not""" - if frappe.db.get_value("Selling Settings", None, "so_required") == "Yes": + if frappe.db.get_single_value("Selling Settings", "so_required") == "Yes": for d in self.get("items"): if not d.against_sales_order: frappe.throw(_("Sales Order required for Item {0}").format(d.item_code)) @@ -205,7 +205,7 @@ class DeliveryNote(SellingController): super(DeliveryNote, self).validate_warehouse() for d in self.get_item_list(): - if not d["warehouse"] and frappe.db.get_value("Item", d["item_code"], "is_stock_item") == 1: + if not d["warehouse"] and frappe.get_cached_value("Item", d["item_code"], "is_stock_item") == 1: frappe.throw(_("Warehouse required for stock Item {0}").format(d["item_code"])) def update_current_stock(self): diff --git a/erpnext/stock/doctype/delivery_note_item/delivery_note_item.json b/erpnext/stock/doctype/delivery_note_item/delivery_note_item.json index 180adee0cb..e46cab0576 100644 --- a/erpnext/stock/doctype/delivery_note_item/delivery_note_item.json +++ b/erpnext/stock/doctype/delivery_note_item/delivery_note_item.json @@ -629,7 +629,8 @@ "no_copy": 1, "options": "Sales Order", "print_hide": 1, - "read_only": 1 + "read_only": 1, + "search_index": 1 }, { "fieldname": "against_sales_invoice", @@ -662,7 +663,8 @@ "label": "Against Sales Invoice Item", "no_copy": 1, "print_hide": 1, - "read_only": 1 + "read_only": 1, + "search_index": 1 }, { "fieldname": "installed_qty", @@ -854,7 +856,7 @@ "index_web_pages_for_search": 1, "istable": 1, "links": [], - "modified": "2023-04-06 09:28:29.182053", + "modified": "2023-05-01 21:05:14.175640", "modified_by": "Administrator", "module": "Stock", "name": "Delivery Note Item", diff --git a/erpnext/stock/stock_ledger.py b/erpnext/stock/stock_ledger.py index 82fc0df8de..8b517bf1e0 100644 --- a/erpnext/stock/stock_ledger.py +++ b/erpnext/stock/stock_ledger.py @@ -1392,7 +1392,7 @@ def regenerate_sle_for_batch_stock_reco(detail): if not frappe.db.exists( "Repost Item Valuation", {"voucher_no": doc.name, "status": "Queued", "docstatus": "1"} ): - doc.repost_future_sle_and_gle() + doc.repost_future_sle_and_gle(force=True) def get_stock_reco_qty_shift(args): From 6864b11f83a56efd8d5202d91d6f5e7080afc114 Mon Sep 17 00:00:00 2001 From: Anand Baburajan Date: Tue, 2 May 2023 20:58:22 +0530 Subject: [PATCH 73/75] fix: handle finance book properly in trial balance and general ledger (#35085) * fix: get default fb properly and handle different fb and default fb case * chore: minor UX improvement * fix: handle FBs properly in general ledger --- .../accounts/report/financial_statements.py | 11 ++++++++-- .../report/general_ledger/general_ledger.js | 3 ++- .../report/general_ledger/general_ledger.py | 22 ++++++++++++++----- .../report/trial_balance/trial_balance.py | 11 ++++++++-- 4 files changed, 36 insertions(+), 11 deletions(-) diff --git a/erpnext/accounts/report/financial_statements.py b/erpnext/accounts/report/financial_statements.py index debe655f81..76a01db714 100644 --- a/erpnext/accounts/report/financial_statements.py +++ b/erpnext/accounts/report/financial_statements.py @@ -538,13 +538,20 @@ def apply_additional_conditions(doctype, query, from_date, ignore_closing_entrie query = query.where(gl_entry.cost_center.isin(filters.cost_center)) if filters.get("include_default_book_entries"): + company_fb = frappe.get_cached_value("Company", filters.company, "default_finance_book") + + if filters.finance_book and company_fb and cstr(filters.finance_book) != cstr(company_fb): + frappe.throw( + _("To use a different finance book, please uncheck 'Include Default Book Entries'") + ) + query = query.where( - (gl_entry.finance_book.isin([cstr(filters.finance_book), cstr(filters.company_fb), ""])) + (gl_entry.finance_book.isin([cstr(filters.finance_book), cstr(company_fb)])) | (gl_entry.finance_book.isnull()) ) else: query = query.where( - (gl_entry.finance_book.isin([cstr(filters.company_fb), ""])) | (gl_entry.finance_book.isnull()) + (gl_entry.finance_book.isin([cstr(filters.finance_book)])) | (gl_entry.finance_book.isnull()) ) if accounting_dimensions: diff --git a/erpnext/accounts/report/general_ledger/general_ledger.js b/erpnext/accounts/report/general_ledger/general_ledger.js index 2100f26c1e..57a9091cf9 100644 --- a/erpnext/accounts/report/general_ledger/general_ledger.js +++ b/erpnext/accounts/report/general_ledger/general_ledger.js @@ -176,7 +176,8 @@ frappe.query_reports["General Ledger"] = { { "fieldname": "include_default_book_entries", "label": __("Include Default Book Entries"), - "fieldtype": "Check" + "fieldtype": "Check", + "default": 1 }, { "fieldname": "show_cancelled_entries", diff --git a/erpnext/accounts/report/general_ledger/general_ledger.py b/erpnext/accounts/report/general_ledger/general_ledger.py index 27b84c4e77..0b05c11668 100644 --- a/erpnext/accounts/report/general_ledger/general_ledger.py +++ b/erpnext/accounts/report/general_ledger/general_ledger.py @@ -244,13 +244,23 @@ def get_conditions(filters): if filters.get("project"): conditions.append("project in %(project)s") - if filters.get("finance_book"): - if filters.get("include_default_book_entries"): - conditions.append( - "(finance_book in (%(finance_book)s, %(company_fb)s, '') OR finance_book IS NULL)" - ) + if filters.get("include_default_book_entries"): + if filters.get("finance_book"): + if filters.get("company_fb") and cstr(filters.get("finance_book")) != cstr( + filters.get("company_fb") + ): + frappe.throw( + _("To use a different finance book, please uncheck 'Include Default Book Entries'") + ) + else: + conditions.append("(finance_book in (%(finance_book)s) OR finance_book IS NULL)") else: - conditions.append("finance_book in (%(finance_book)s)") + conditions.append("(finance_book in (%(company_fb)s) OR finance_book IS NULL)") + else: + if filters.get("finance_book"): + conditions.append("(finance_book in (%(finance_book)s) OR finance_book IS NULL)") + else: + conditions.append("(finance_book IS NULL)") if not filters.get("show_cancelled_entries"): conditions.append("is_cancelled = 0") diff --git a/erpnext/accounts/report/trial_balance/trial_balance.py b/erpnext/accounts/report/trial_balance/trial_balance.py index 53611ab919..57dac2af49 100644 --- a/erpnext/accounts/report/trial_balance/trial_balance.py +++ b/erpnext/accounts/report/trial_balance/trial_balance.py @@ -248,13 +248,20 @@ def get_opening_balance( opening_balance = opening_balance.where(closing_balance.project == filters.project) if filters.get("include_default_book_entries"): + company_fb = frappe.get_cached_value("Company", filters.company, "default_finance_book") + + if filters.finance_book and company_fb and cstr(filters.finance_book) != cstr(company_fb): + frappe.throw( + _("To use a different finance book, please uncheck 'Include Default Book Entries'") + ) + opening_balance = opening_balance.where( - (closing_balance.finance_book.isin([cstr(filters.finance_book), cstr(filters.company_fb), ""])) + (closing_balance.finance_book.isin([cstr(filters.finance_book), cstr(company_fb)])) | (closing_balance.finance_book.isnull()) ) else: opening_balance = opening_balance.where( - (closing_balance.finance_book.isin([cstr(filters.finance_book), ""])) + (closing_balance.finance_book.isin([cstr(filters.finance_book)])) | (closing_balance.finance_book.isnull()) ) From 06e91e758f18ea463351b5c6b6e5ab5204a4705c Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Wed, 3 May 2023 18:06:47 +0530 Subject: [PATCH 74/75] feat: reserve qty against production plan raw materials in BIN --- .../material_request_plan_item.json | 9 +++- .../production_plan/production_plan.js | 10 ++--- .../production_plan/production_plan.py | 44 +++++++++++++++++++ .../production_plan/test_production_plan.py | 21 +++++++++ .../doctype/work_order/work_order.py | 30 +++++++++---- erpnext/stock/doctype/bin/bin.json | 9 +++- erpnext/stock/doctype/bin/bin.py | 30 ++++++++++++- .../stock_projected_qty.py | 9 ++++ 8 files changed, 143 insertions(+), 19 deletions(-) diff --git a/erpnext/manufacturing/doctype/material_request_plan_item/material_request_plan_item.json b/erpnext/manufacturing/doctype/material_request_plan_item/material_request_plan_item.json index 8c61d545b8..09bf1d8a73 100644 --- a/erpnext/manufacturing/doctype/material_request_plan_item/material_request_plan_item.json +++ b/erpnext/manufacturing/doctype/material_request_plan_item/material_request_plan_item.json @@ -16,6 +16,7 @@ "column_break_4", "quantity", "uom", + "conversion_factor", "projected_qty", "reserved_qty_for_production", "safety_stock", @@ -169,11 +170,17 @@ "label": "Qty As Per BOM", "no_copy": 1, "read_only": 1 + }, + { + "fieldname": "conversion_factor", + "fieldtype": "Float", + "label": "Conversion Factor", + "read_only": 1 } ], "istable": 1, "links": [], - "modified": "2022-11-26 14:59:25.879631", + "modified": "2023-05-03 12:43:29.895754", "modified_by": "Administrator", "module": "Manufacturing", "name": "Material Request Plan Item", diff --git a/erpnext/manufacturing/doctype/production_plan/production_plan.js b/erpnext/manufacturing/doctype/production_plan/production_plan.js index 62715e6565..ab7aa52bb7 100644 --- a/erpnext/manufacturing/doctype/production_plan/production_plan.js +++ b/erpnext/manufacturing/doctype/production_plan/production_plan.js @@ -336,10 +336,6 @@ frappe.ui.form.on('Production Plan', { }, get_items_for_material_requests(frm, warehouses) { - let set_fields = ['actual_qty', 'item_code','item_name', 'description', 'uom', 'from_warehouse', - 'min_order_qty', 'required_bom_qty', 'quantity', 'sales_order', 'warehouse', 'projected_qty', 'ordered_qty', - 'reserved_qty_for_production', 'material_request_type']; - frappe.call({ method: "erpnext.manufacturing.doctype.production_plan.production_plan.get_items_for_material_requests", freeze: true, @@ -352,11 +348,11 @@ frappe.ui.form.on('Production Plan', { frm.set_value('mr_items', []); r.message.forEach(row => { let d = frm.add_child('mr_items'); - set_fields.forEach(field => { - if (row[field]) { + for (let field in row) { + if (field !== 'name') { d[field] = row[field]; } - }); + } }); } refresh_field('mr_items'); diff --git a/erpnext/manufacturing/doctype/production_plan/production_plan.py b/erpnext/manufacturing/doctype/production_plan/production_plan.py index 0cc0f80cf1..df50cbf269 100644 --- a/erpnext/manufacturing/doctype/production_plan/production_plan.py +++ b/erpnext/manufacturing/doctype/production_plan/production_plan.py @@ -28,6 +28,7 @@ from erpnext.manufacturing.doctype.bom.bom import validate_bom_no from erpnext.manufacturing.doctype.work_order.work_order import get_item_details from erpnext.setup.doctype.item_group.item_group import get_item_group_defaults from erpnext.stock.get_item_details import get_conversion_factor +from erpnext.stock.utils import get_or_make_bin from erpnext.utilities.transaction_base import validate_uom_is_integer @@ -398,9 +399,20 @@ class ProductionPlan(Document): self.set_status() self.db_set("status", self.status) + def on_submit(self): + self.update_bin_qty() + def on_cancel(self): self.db_set("status", "Cancelled") self.delete_draft_work_order() + self.update_bin_qty() + + def update_bin_qty(self): + for d in self.mr_items: + if d.warehouse: + bin_name = get_or_make_bin(d.item_code, d.warehouse) + bin = frappe.get_doc("Bin", bin_name, for_update=True) + bin.update_reserved_qty_for_production_plan() def delete_draft_work_order(self): for d in frappe.get_all( @@ -1068,6 +1080,7 @@ def get_material_request_items( "item_code": row.item_code, "item_name": row.item_name, "quantity": required_qty / conversion_factor, + "conversion_factor": conversion_factor, "required_bom_qty": total_qty, "stock_uom": row.get("stock_uom"), "warehouse": warehouse @@ -1474,3 +1487,34 @@ def set_default_warehouses(row, default_warehouses): for field in ["wip_warehouse", "fg_warehouse"]: if not row.get(field): row[field] = default_warehouses.get(field) + + +def get_reserved_qty_for_production_plan(item_code, warehouse): + from erpnext.manufacturing.doctype.work_order.work_order import get_reserved_qty_for_production + + table = frappe.qb.DocType("Production Plan") + child = frappe.qb.DocType("Material Request Plan Item") + + query = ( + frappe.qb.from_(table) + .inner_join(child) + .on(table.name == child.parent) + .select(Sum(child.quantity * IfNull(child.conversion_factor, 1.0))) + .where( + (table.docstatus == 1) + & (child.item_code == item_code) + & (child.warehouse == warehouse) + & (table.status.notin(["Completed", "Closed"])) + ) + ).run() + + if not query: + return 0.0 + + reserved_qty_for_production_plan = flt(query[0][0]) + + reserved_qty_for_production = flt( + get_reserved_qty_for_production(item_code, warehouse, check_production_plan=True) + ) + + return reserved_qty_for_production_plan - reserved_qty_for_production diff --git a/erpnext/manufacturing/doctype/production_plan/test_production_plan.py b/erpnext/manufacturing/doctype/production_plan/test_production_plan.py index 2bf14c24cf..91864d09db 100644 --- a/erpnext/manufacturing/doctype/production_plan/test_production_plan.py +++ b/erpnext/manufacturing/doctype/production_plan/test_production_plan.py @@ -868,6 +868,27 @@ class TestProductionPlan(FrappeTestCase): for item_code in mr_items: self.assertTrue(item_code in validate_mr_items) + def test_resered_qty_for_production_plan_for_material_requests(self): + from erpnext.stock.utils import get_or_make_bin + + bin_name = get_or_make_bin("Raw Material Item 1", "_Test Warehouse - _TC") + before_qty = flt(frappe.db.get_value("Bin", bin_name, "reserved_qty_for_production_plan")) + + pln = create_production_plan(item_code="Test Production Item 1") + + bin_name = get_or_make_bin("Raw Material Item 1", "_Test Warehouse - _TC") + after_qty = flt(frappe.db.get_value("Bin", bin_name, "reserved_qty_for_production_plan")) + + self.assertEqual(after_qty - before_qty, 1) + + pln = frappe.get_doc("Production Plan", pln.name) + pln.cancel() + + bin_name = get_or_make_bin("Raw Material Item 1", "_Test Warehouse - _TC") + after_qty = flt(frappe.db.get_value("Bin", bin_name, "reserved_qty_for_production_plan")) + + self.assertEqual(after_qty, before_qty) + def create_production_plan(**args): """ diff --git a/erpnext/manufacturing/doctype/work_order/work_order.py b/erpnext/manufacturing/doctype/work_order/work_order.py index 66b871c746..75845226a6 100644 --- a/erpnext/manufacturing/doctype/work_order/work_order.py +++ b/erpnext/manufacturing/doctype/work_order/work_order.py @@ -558,12 +558,19 @@ class WorkOrder(Document): and self.production_plan_item and not self.production_plan_sub_assembly_item ): - qty = frappe.get_value("Production Plan Item", self.production_plan_item, "ordered_qty") or 0.0 + table = frappe.qb.DocType("Work Order") - if self.docstatus == 1: - qty += self.qty - elif self.docstatus == 2: - qty -= self.qty + query = ( + frappe.qb.from_(table) + .select(Sum(table.qty)) + .where( + (table.production_plan == self.production_plan) + & (table.production_plan_item == self.production_plan_item) + & (table.docstatus == 1) + ) + ).run() + + qty = flt(query[0][0]) if query else 0 frappe.db.set_value("Production Plan Item", self.production_plan_item, "ordered_qty", qty) @@ -1476,12 +1483,14 @@ def create_pick_list(source_name, target_doc=None, for_qty=None): return doc -def get_reserved_qty_for_production(item_code: str, warehouse: str) -> float: +def get_reserved_qty_for_production( + item_code: str, warehouse: str, check_production_plan: bool = False +) -> float: """Get total reserved quantity for any item in specified warehouse""" wo = frappe.qb.DocType("Work Order") wo_item = frappe.qb.DocType("Work Order Item") - return ( + query = ( frappe.qb.from_(wo) .from_(wo_item) .select( @@ -1502,7 +1511,12 @@ def get_reserved_qty_for_production(item_code: str, warehouse: str) -> float: | (wo_item.required_qty > wo_item.consumed_qty) ) ) - ).run()[0][0] or 0.0 + ) + + if check_production_plan: + query = query.where(wo.production_plan.isnotnull()) + + return query.run()[0][0] or 0.0 @frappe.whitelist() diff --git a/erpnext/stock/doctype/bin/bin.json b/erpnext/stock/doctype/bin/bin.json index d822f4a609..a11572776a 100644 --- a/erpnext/stock/doctype/bin/bin.json +++ b/erpnext/stock/doctype/bin/bin.json @@ -15,6 +15,7 @@ "projected_qty", "reserved_qty_for_production", "reserved_qty_for_sub_contract", + "reserved_qty_for_production_plan", "ma_rate", "stock_uom", "fcfs_rate", @@ -165,13 +166,19 @@ "oldfieldname": "stock_value", "oldfieldtype": "Currency", "read_only": 1 + }, + { + "fieldname": "reserved_qty_for_production_plan", + "fieldtype": "Float", + "label": "Reserved Qty for Production Plan", + "read_only": 1 } ], "hide_toolbar": 1, "idx": 1, "in_create": 1, "links": [], - "modified": "2022-03-30 07:22:23.868602", + "modified": "2023-05-02 23:26:21.806965", "modified_by": "Administrator", "module": "Stock", "name": "Bin", diff --git a/erpnext/stock/doctype/bin/bin.py b/erpnext/stock/doctype/bin/bin.py index 72654e6f81..5abea9e69f 100644 --- a/erpnext/stock/doctype/bin/bin.py +++ b/erpnext/stock/doctype/bin/bin.py @@ -24,8 +24,30 @@ class Bin(Document): - flt(self.reserved_qty) - flt(self.reserved_qty_for_production) - flt(self.reserved_qty_for_sub_contract) + - flt(self.reserved_qty_for_production_plan) ) + def update_reserved_qty_for_production_plan(self, skip_project_qty_update=False): + """Update qty reserved for production from Production Plan tables + in open production plan""" + from erpnext.manufacturing.doctype.production_plan.production_plan import ( + get_reserved_qty_for_production_plan, + ) + + self.reserved_qty_for_production_plan = get_reserved_qty_for_production_plan( + self.item_code, self.warehouse + ) + + self.db_set( + "reserved_qty_for_production_plan", + flt(self.reserved_qty_for_production_plan), + update_modified=True, + ) + + if not skip_project_qty_update: + self.set_projected_qty() + self.db_set("projected_qty", self.projected_qty, update_modified=True) + def update_reserved_qty_for_production(self): """Update qty reserved for production from Production Item tables in open work orders""" @@ -35,11 +57,13 @@ class Bin(Document): self.item_code, self.warehouse ) - self.set_projected_qty() - self.db_set( "reserved_qty_for_production", flt(self.reserved_qty_for_production), update_modified=True ) + + self.update_reserved_qty_for_production_plan(skip_project_qty_update=True) + + self.set_projected_qty() self.db_set("projected_qty", self.projected_qty, update_modified=True) def update_reserved_qty_for_sub_contracting(self, subcontract_doctype="Subcontracting Order"): @@ -141,6 +165,7 @@ def get_bin_details(bin_name): "planned_qty", "reserved_qty_for_production", "reserved_qty_for_sub_contract", + "reserved_qty_for_production_plan", ], as_dict=1, ) @@ -188,6 +213,7 @@ def update_qty(bin_name, args): - flt(reserved_qty) - flt(bin_details.reserved_qty_for_production) - flt(bin_details.reserved_qty_for_sub_contract) + - flt(bin_details.reserved_qty_for_production_plan) ) frappe.db.set_value( diff --git a/erpnext/stock/report/stock_projected_qty/stock_projected_qty.py b/erpnext/stock/report/stock_projected_qty/stock_projected_qty.py index f477d8f08f..31c756da82 100644 --- a/erpnext/stock/report/stock_projected_qty/stock_projected_qty.py +++ b/erpnext/stock/report/stock_projected_qty/stock_projected_qty.py @@ -76,6 +76,7 @@ def execute(filters=None): bin.ordered_qty, bin.reserved_qty, bin.reserved_qty_for_production, + bin.reserved_qty_for_production_plan, bin.reserved_qty_for_sub_contract, reserved_qty_for_pos, bin.projected_qty, @@ -173,6 +174,13 @@ def get_columns(): "width": 100, "convertible": "qty", }, + { + "label": _("Reserved for Production Plan"), + "fieldname": "reserved_qty_for_production_plan", + "fieldtype": "Float", + "width": 100, + "convertible": "qty", + }, { "label": _("Reserved for Sub Contracting"), "fieldname": "reserved_qty_for_sub_contract", @@ -232,6 +240,7 @@ def get_bin_list(filters): bin.reserved_qty, bin.reserved_qty_for_production, bin.reserved_qty_for_sub_contract, + bin.reserved_qty_for_production_plan, bin.projected_qty, ) .orderby(bin.item_code, bin.warehouse) From a84d0af81e1dbba309a76471cd98e91bf6441d22 Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Wed, 3 May 2023 23:22:59 +0530 Subject: [PATCH 75/75] fix: over production percentage not considered in validation --- .../doctype/job_card/job_card.py | 19 +++++++++++++++++-- .../doctype/work_order/test_work_order.py | 8 ++++++++ 2 files changed, 25 insertions(+), 2 deletions(-) diff --git a/erpnext/manufacturing/doctype/job_card/job_card.py b/erpnext/manufacturing/doctype/job_card/job_card.py index f89951619e..877362dcba 100644 --- a/erpnext/manufacturing/doctype/job_card/job_card.py +++ b/erpnext/manufacturing/doctype/job_card/job_card.py @@ -87,6 +87,12 @@ class JobCard(Document): frappe.db.get_value("Work Order Operation", self.operation_id, "completed_qty") ) + over_production_percentage = flt( + frappe.db.get_single_value("Manufacturing Settings", "overproduction_percentage_for_work_order") + ) + + wo_qty = wo_qty + (wo_qty * over_production_percentage / 100) + job_card_qty = frappe.get_all( "Job Card", fields=["sum(for_quantity)"], @@ -101,8 +107,17 @@ class JobCard(Document): job_card_qty = flt(job_card_qty[0][0]) if job_card_qty else 0 if job_card_qty and ((job_card_qty - completed_qty) > wo_qty): - msg = f"""Job Card quantity cannot be greater than - Work Order quantity for the operation {self.operation}""" + form_link = get_link_to_form("Manufacturing Settings", "Manufacturing Settings") + + msg = f""" + Qty To Manufacture in the job card + cannot be greater than Qty To Manufacture in the + work order for the operation {bold(self.operation)}. +

Solution: Either you can reduce the + Qty To Manufacture in the job card or set the + 'Overproduction Percentage For Work Order' + in the {form_link}.""" + frappe.throw(_(msg), title=_("Extra Job Card Quantity")) def set_sub_operations(self): diff --git a/erpnext/manufacturing/doctype/work_order/test_work_order.py b/erpnext/manufacturing/doctype/work_order/test_work_order.py index 540b7dc9ea..bb53c8c225 100644 --- a/erpnext/manufacturing/doctype/work_order/test_work_order.py +++ b/erpnext/manufacturing/doctype/work_order/test_work_order.py @@ -1649,6 +1649,14 @@ class TestWorkOrder(FrappeTestCase): job_card2 = frappe.copy_doc(job_card_doc) self.assertRaises(frappe.ValidationError, job_card2.save) + frappe.db.set_single_value( + "Manufacturing Settings", "overproduction_percentage_for_work_order", 100 + ) + + job_card2 = frappe.copy_doc(job_card_doc) + job_card2.time_logs = [] + job_card2.save() + def prepare_data_for_workstation_type_check(): from erpnext.manufacturing.doctype.operation.test_operation import make_operation