diff --git a/erpnext/accounts/doctype/account/account.json b/erpnext/accounts/doctype/account/account.json index 0c9232015d9..e87b59ea9cb 100644 --- a/erpnext/accounts/doctype/account/account.json +++ b/erpnext/accounts/doctype/account/account.json @@ -121,7 +121,8 @@ "label": "Account Type", "oldfieldname": "account_type", "oldfieldtype": "Select", - "options": "\nAccumulated Depreciation\nAsset Received But Not Billed\nBank\nCash\nChargeable\nCapital Work in Progress\nCost of Goods Sold\nCurrent Asset\nCurrent Liability\nDepreciation\nDirect Expense\nDirect Income\nEquity\nExpense Account\nExpenses Included In Asset Valuation\nExpenses Included In Valuation\nFixed Asset\nIncome Account\nIndirect Expense\nIndirect Income\nLiability\nPayable\nReceivable\nRound Off\nStock\nStock Adjustment\nStock Received But Not Billed\nService Received But Not Billed\nTax\nTemporary" + "options": "\nAccumulated Depreciation\nAsset Received But Not Billed\nBank\nCash\nChargeable\nCapital Work in Progress\nCost of Goods Sold\nCurrent Asset\nCurrent Liability\nDepreciation\nDirect Expense\nDirect Income\nEquity\nExpense Account\nExpenses Included In Asset Valuation\nExpenses Included In Valuation\nFixed Asset\nIncome Account\nIndirect Expense\nIndirect Income\nLiability\nPayable\nReceivable\nRound Off\nStock\nStock Adjustment\nStock Received But Not Billed\nService Received But Not Billed\nTax\nTemporary", + "search_index": 1 }, { "description": "Rate at which this tax is applied", @@ -190,7 +191,7 @@ "idx": 1, "is_tree": 1, "links": [], - "modified": "2023-07-20 18:18:44.405723", + "modified": "2024-06-27 16:23:04.444354", "modified_by": "Administrator", "module": "Accounts", "name": "Account", @@ -251,4 +252,4 @@ "sort_order": "ASC", "states": [], "track_changes": 1 -} \ No newline at end of file +} diff --git a/erpnext/accounts/doctype/accounting_dimension/accounting_dimension.py b/erpnext/accounts/doctype/accounting_dimension/accounting_dimension.py index 7a2f4e4b71b..db99bcd223b 100644 --- a/erpnext/accounts/doctype/accounting_dimension/accounting_dimension.py +++ b/erpnext/accounts/doctype/accounting_dimension/accounting_dimension.py @@ -255,14 +255,16 @@ def get_accounting_dimensions(as_list=True, filters=None): def get_checks_for_pl_and_bs_accounts(): - dimensions = frappe.db.sql( - """SELECT p.label, p.disabled, p.fieldname, c.default_dimension, c.company, c.mandatory_for_pl, c.mandatory_for_bs - FROM `tabAccounting Dimension`p ,`tabAccounting Dimension Detail` c - WHERE p.name = c.parent""", - as_dict=1, - ) + if frappe.flags.accounting_dimensions_details is None: + # nosemgrep + frappe.flags.accounting_dimensions_details = frappe.db.sql( + """SELECT p.label, p.disabled, p.fieldname, c.default_dimension, c.company, c.mandatory_for_pl, c.mandatory_for_bs + FROM `tabAccounting Dimension`p ,`tabAccounting Dimension Detail` c + WHERE p.name = c.parent""", + as_dict=1, + ) - return dimensions + return frappe.flags.accounting_dimensions_details def get_dimension_with_children(doctype, dimensions): diff --git a/erpnext/accounts/doctype/accounting_dimension/test_accounting_dimension.py b/erpnext/accounts/doctype/accounting_dimension/test_accounting_dimension.py index cb7f5f5da78..10dbe3bab0f 100644 --- a/erpnext/accounts/doctype/accounting_dimension/test_accounting_dimension.py +++ b/erpnext/accounts/doctype/accounting_dimension/test_accounting_dimension.py @@ -78,6 +78,8 @@ class TestAccountingDimension(unittest.TestCase): def tearDown(self): disable_dimension() + frappe.flags.accounting_dimensions_details = None + frappe.flags.dimension_filter_map = None def create_dimension(): diff --git a/erpnext/accounts/doctype/accounting_dimension_filter/accounting_dimension_filter.py b/erpnext/accounts/doctype/accounting_dimension_filter/accounting_dimension_filter.py index 01f6e60bf3b..1954b4b0efe 100644 --- a/erpnext/accounts/doctype/accounting_dimension_filter/accounting_dimension_filter.py +++ b/erpnext/accounts/doctype/accounting_dimension_filter/accounting_dimension_filter.py @@ -66,37 +66,41 @@ class AccountingDimensionFilter(Document): def get_dimension_filter_map(): - filters = frappe.db.sql( - """ - SELECT - a.applicable_on_account, d.dimension_value, p.accounting_dimension, - p.allow_or_restrict, a.is_mandatory - FROM - `tabApplicable On Account` a, - `tabAccounting Dimension Filter` p - LEFT JOIN `tabAllowed Dimension` d ON d.parent = p.name - WHERE - p.name = a.parent - AND p.disabled = 0 - """, - as_dict=1, - ) - - dimension_filter_map = {} - - for f in filters: - f.fieldname = scrub(f.accounting_dimension) - - build_map( - dimension_filter_map, - f.fieldname, - f.applicable_on_account, - f.dimension_value, - f.allow_or_restrict, - f.is_mandatory, + if not frappe.flags.get("dimension_filter_map"): + # nosemgrep + filters = frappe.db.sql( + """ + SELECT + a.applicable_on_account, d.dimension_value, p.accounting_dimension, + p.allow_or_restrict, a.is_mandatory + FROM + `tabApplicable On Account` a, `tabAllowed Dimension` d, + `tabAccounting Dimension Filter` p + WHERE + p.name = a.parent + AND p.disabled = 0 + AND p.name = d.parent + """, + as_dict=1, ) - return dimension_filter_map + dimension_filter_map = {} + + for f in filters: + f.fieldname = scrub(f.accounting_dimension) + + build_map( + dimension_filter_map, + f.fieldname, + f.applicable_on_account, + f.dimension_value, + f.allow_or_restrict, + f.is_mandatory, + ) + + frappe.flags.dimension_filter_map = dimension_filter_map + + return frappe.flags.dimension_filter_map def build_map(map_object, dimension, account, filter_value, allow_or_restrict, is_mandatory): diff --git a/erpnext/accounts/doctype/accounting_dimension_filter/test_accounting_dimension_filter.py b/erpnext/accounts/doctype/accounting_dimension_filter/test_accounting_dimension_filter.py index 3dec7c87020..77057c1e20e 100644 --- a/erpnext/accounts/doctype/accounting_dimension_filter/test_accounting_dimension_filter.py +++ b/erpnext/accounts/doctype/accounting_dimension_filter/test_accounting_dimension_filter.py @@ -47,6 +47,8 @@ class TestAccountingDimensionFilter(unittest.TestCase): def tearDown(self): disable_dimension_filter() disable_dimension() + frappe.flags.accounting_dimensions_details = None + frappe.flags.dimension_filter_map = None for si in self.invoice_list: si.load_from_db() diff --git a/erpnext/accounts/doctype/budget/budget.py b/erpnext/accounts/doctype/budget/budget.py index 11f78ae1763..145480138d6 100644 --- a/erpnext/accounts/doctype/budget/budget.py +++ b/erpnext/accounts/doctype/budget/budget.py @@ -142,6 +142,8 @@ class Budget(Document): def validate_expense_against_budget(args, expense_amount=0): args = frappe._dict(args) + if not frappe.get_all("Budget", limit=1): + return if args.get("company") and not args.fiscal_year: args.fiscal_year = get_fiscal_year(args.get("posting_date"), company=args.get("company"))[0] @@ -149,6 +151,9 @@ def validate_expense_against_budget(args, expense_amount=0): "Company", args.get("company"), "exception_budget_approver_role" ) + if not frappe.get_cached_value("Budget", {"fiscal_year": args.fiscal_year, "company": args.company}): # nosec + return + if not args.account: args.account = args.get("expense_account") @@ -175,12 +180,12 @@ def validate_expense_against_budget(args, expense_amount=0): if ( args.get(budget_against) and args.account - and frappe.db.get_value("Account", {"name": args.account, "root_type": "Expense"}) + and (frappe.get_cached_value("Account", args.account, "root_type") == "Expense") ): doctype = dimension.get("document_type") if frappe.get_cached_value("DocType", doctype, "is_tree"): - lft, rgt = frappe.db.get_value(doctype, args.get(budget_against), ["lft", "rgt"]) + lft, rgt = frappe.get_cached_value(doctype, args.get(budget_against), ["lft", "rgt"]) condition = f"""and exists(select name from `tab{doctype}` where lft<={lft} and rgt>={rgt} and name=b.{budget_against})""" # nosec args.is_tree = True diff --git a/erpnext/accounts/doctype/budget_account/budget_account.json b/erpnext/accounts/doctype/budget_account/budget_account.json index ead07614a7f..c7d872647f1 100644 --- a/erpnext/accounts/doctype/budget_account/budget_account.json +++ b/erpnext/accounts/doctype/budget_account/budget_account.json @@ -1,94 +1,42 @@ { - "allow_copy": 0, - "allow_import": 0, - "allow_rename": 0, - "beta": 0, - "creation": "2016-05-16 11:54:09.286135", - "custom": 0, - "docstatus": 0, - "doctype": "DocType", - "document_type": "", - "editable_grid": 1, - "engine": "InnoDB", + "actions": [], + "creation": "2016-05-16 11:54:09.286135", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "account", + "budget_amount" + ], "fields": [ { - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "account", - "fieldtype": "Link", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_list_view": 1, - "label": "Account", - "length": 0, - "no_copy": 0, - "options": "Account", - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 1, - "search_index": 0, - "set_only_once": 0, - "unique": 0 - }, + "fieldname": "account", + "fieldtype": "Link", + "in_list_view": 1, + "label": "Account", + "options": "Account", + "reqd": 1, + "search_index": 1 + }, { - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "budget_amount", - "fieldtype": "Currency", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_list_view": 1, - "label": "Budget Amount", - "length": 0, - "no_copy": 0, - "options": "Company:company:default_currency", - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 1, - "search_index": 0, - "set_only_once": 0, - "unique": 0 + "fieldname": "budget_amount", + "fieldtype": "Currency", + "in_list_view": 1, + "label": "Budget Amount", + "options": "Company:company:default_currency", + "reqd": 1 } - ], - "hide_heading": 0, - "hide_toolbar": 0, - "idx": 0, - "image_view": 0, - "in_create": 0, - - "is_submittable": 0, - "issingle": 0, - "istable": 1, - "max_attachments": 0, - "modified": "2017-01-02 17:02:53.339420", - "modified_by": "Administrator", - "module": "Accounts", - "name": "Budget Account", - "name_case": "", - "owner": "Administrator", - "permissions": [], - "quick_entry": 1, - "read_only": 0, - "read_only_onload": 0, - "sort_field": "modified", - "sort_order": "DESC", - "track_seen": 0 + ], + "istable": 1, + "links": [], + "modified": "2024-03-04 15:43:27.016947", + "modified_by": "Administrator", + "module": "Accounts", + "name": "Budget Account", + "owner": "Administrator", + "permissions": [], + "quick_entry": 1, + "sort_field": "modified", + "sort_order": "DESC", + "states": [] } \ No newline at end of file diff --git a/erpnext/accounts/doctype/gl_entry/gl_entry.json b/erpnext/accounts/doctype/gl_entry/gl_entry.json index 0800971269b..2d106ad8cee 100644 --- a/erpnext/accounts/doctype/gl_entry/gl_entry.json +++ b/erpnext/accounts/doctype/gl_entry/gl_entry.json @@ -179,7 +179,8 @@ "fieldname": "voucher_detail_no", "fieldtype": "Data", "label": "Voucher Detail No", - "read_only": 1 + "read_only": 1, + "search_index": 1 }, { "fieldname": "project", @@ -290,7 +291,7 @@ "idx": 1, "in_create": 1, "links": [], - "modified": "2023-12-18 15:38:14.006208", + "modified": "2024-07-02 14:31:51.496466", "modified_by": "Administrator", "module": "Accounts", "name": "GL Entry", @@ -322,7 +323,7 @@ ], "quick_entry": 1, "search_fields": "voucher_no,account,posting_date,against_voucher", - "sort_field": "modified", + "sort_field": "creation", "sort_order": "DESC", "states": [] -} +} \ No newline at end of file diff --git a/erpnext/accounts/doctype/gl_entry/gl_entry.py b/erpnext/accounts/doctype/gl_entry/gl_entry.py index 594c3054cfd..d74224c4aa2 100644 --- a/erpnext/accounts/doctype/gl_entry/gl_entry.py +++ b/erpnext/accounts/doctype/gl_entry/gl_entry.py @@ -32,8 +32,6 @@ class GLEntry(Document): account: DF.Link | None account_currency: DF.Link | None against: DF.Text | None - against_link: DF.DynamicLink | None - against_type: DF.Link | None against_voucher: DF.DynamicLink | None against_voucher_type: DF.Link | None company: DF.Link | None @@ -328,7 +326,7 @@ def update_outstanding_amt( party_condition = "" if against_voucher_type == "Sales Invoice": - party_account = frappe.db.get_value(against_voucher_type, against_voucher, "debit_to") + party_account = frappe.get_cached_value(against_voucher_type, against_voucher, "debit_to") account_condition = f"and account in ({frappe.db.escape(account)}, {frappe.db.escape(party_account)})" else: account_condition = f" and account = {frappe.db.escape(account)}" @@ -392,7 +390,9 @@ def update_outstanding_amt( def validate_frozen_account(account, adv_adj=None): frozen_account = frappe.get_cached_value("Account", account, "freeze_account") if frozen_account == "Yes" and not adv_adj: - frozen_accounts_modifier = frappe.db.get_value("Accounts Settings", None, "frozen_accounts_modifier") + frozen_accounts_modifier = frappe.get_cached_value( + "Accounts Settings", None, "frozen_accounts_modifier" + ) if not frozen_accounts_modifier: frappe.throw(_("Account {0} is frozen").format(account)) diff --git a/erpnext/accounts/doctype/payment_entry/payment_entry.py b/erpnext/accounts/doctype/payment_entry/payment_entry.py index a2882206b4a..fb3500e2c58 100644 --- a/erpnext/accounts/doctype/payment_entry/payment_entry.py +++ b/erpnext/accounts/doctype/payment_entry/payment_entry.py @@ -1217,13 +1217,21 @@ class PaymentEntry(AccountsController): if reference.reference_doctype == "Sales Invoice": return "credit", reference.account + if reference.reference_doctype == "Purchase Invoice": + return "debit", reference.account + if reference.reference_doctype == "Payment Entry": + # reference.account_type and reference.payment_type is only available for Reverse payments if reference.account_type == "Receivable" and reference.payment_type == "Pay": return "credit", self.party_account else: return "debit", self.party_account - return "debit", reference.account + if reference.reference_doctype == "Journal Entry": + if self.party_type == "Customer" and self.payment_type == "Receive": + return "credit", reference.account + else: + return "debit", reference.account def add_advance_gl_for_reference(self, gl_entries, invoice): args_dict = { diff --git a/erpnext/accounts/doctype/payment_entry/test_payment_entry.py b/erpnext/accounts/doctype/payment_entry/test_payment_entry.py index 4631f8905e6..cc03dc260bb 100644 --- a/erpnext/accounts/doctype/payment_entry/test_payment_entry.py +++ b/erpnext/accounts/doctype/payment_entry/test_payment_entry.py @@ -82,7 +82,7 @@ class TestPaymentEntry(FrappeTestCase): expected_gle = dict( (d[0], d) - for d in [["_Test Receivable USD - _TC", 0, 5500, so.name], ["Cash - _TC", 5500.0, 0, None]] + for d in [["_Test Receivable USD - _TC", 0, 5500, so.name], [pe.paid_to, 5500.0, 0, None]] ) self.validate_gl_entries(pe.name, expected_gle) diff --git a/erpnext/accounts/doctype/payment_ledger_entry/payment_ledger_entry.py b/erpnext/accounts/doctype/payment_ledger_entry/payment_ledger_entry.py index ca591d42073..2bc44893c20 100644 --- a/erpnext/accounts/doctype/payment_ledger_entry/payment_ledger_entry.py +++ b/erpnext/accounts/doctype/payment_ledger_entry/payment_ledger_entry.py @@ -161,11 +161,12 @@ class PaymentLedgerEntry(Document): def on_update(self): adv_adj = self.flags.adv_adj if not self.flags.from_repost: - self.validate_account_details() - self.validate_dimensions_for_pl_and_bs() - self.validate_allowed_dimensions() - validate_balance_type(self.account, adv_adj) validate_frozen_account(self.account, adv_adj) + if not self.delinked: + self.validate_account_details() + self.validate_dimensions_for_pl_and_bs() + self.validate_allowed_dimensions() + validate_balance_type(self.account, adv_adj) # update outstanding amount if ( diff --git a/erpnext/accounts/doctype/payment_ledger_entry/test_payment_ledger_entry.py b/erpnext/accounts/doctype/payment_ledger_entry/test_payment_ledger_entry.py index 3eac98d7910..9a33a7ccf6d 100644 --- a/erpnext/accounts/doctype/payment_ledger_entry/test_payment_ledger_entry.py +++ b/erpnext/accounts/doctype/payment_ledger_entry/test_payment_ledger_entry.py @@ -509,7 +509,11 @@ class TestPaymentLedgerEntry(FrappeTestCase): @change_settings( "Accounts Settings", - {"unlink_payment_on_cancellation_of_invoice": 1, "delete_linked_ledger_entries": 1}, + { + "unlink_payment_on_cancellation_of_invoice": 1, + "delete_linked_ledger_entries": 1, + "unlink_advance_payment_on_cancelation_of_order": 1, + }, ) def test_advance_payment_unlink_on_order_cancellation(self): transaction_date = nowdate() diff --git a/erpnext/accounts/doctype/payment_reconciliation/test_payment_reconciliation.py b/erpnext/accounts/doctype/payment_reconciliation/test_payment_reconciliation.py index 53f69a47e75..a5295585221 100644 --- a/erpnext/accounts/doctype/payment_reconciliation/test_payment_reconciliation.py +++ b/erpnext/accounts/doctype/payment_reconciliation/test_payment_reconciliation.py @@ -109,6 +109,14 @@ class TestPaymentReconciliation(FrappeTestCase): "account_currency": "INR", "account_type": "Payable", }, + # 'Receivable' account for capturing advance received, under 'Liabilities' group + { + "attribute": "advance_receivable_account", + "account_name": "Advance Received", + "parent_account": "Current Liabilities - _PR", + "account_currency": "INR", + "account_type": "Receivable", + }, ] for x in accounts: @@ -1574,6 +1582,229 @@ class TestPaymentReconciliation(FrappeTestCase): ) self.assertEqual(len(pl_entries), 3) + def test_advance_payment_reconciliation_against_journal_for_customer(self): + frappe.db.set_value( + "Company", + self.company, + { + "book_advance_payments_in_separate_party_account": 1, + "default_advance_received_account": self.advance_receivable_account, + "reconcile_on_advance_payment_date": 0, + }, + ) + amount = 200.0 + je = self.create_journal_entry(self.debit_to, self.bank, amount) + je.accounts[0].cost_center = self.main_cc.name + je.accounts[0].party_type = "Customer" + je.accounts[0].party = self.customer + je.accounts[1].cost_center = self.main_cc.name + je = je.save().submit() + + pe = self.create_payment_entry(amount=amount).save().submit() + + pr = self.create_payment_reconciliation() + pr.default_advance_account = self.advance_receivable_account + pr.get_unreconciled_entries() + self.assertEqual(len(pr.invoices), 1) + self.assertEqual(len(pr.payments), 1) + invoices = [invoice.as_dict() for invoice in pr.invoices] + payments = [payment.as_dict() for payment in pr.payments] + pr.allocate_entries(frappe._dict({"invoices": invoices, "payments": payments})) + pr.reconcile() + + # Assert Ledger Entries + gl_entries = frappe.db.get_all( + "GL Entry", + filters={"voucher_no": pe.name, "is_cancelled": 0}, + ) + self.assertEqual(len(gl_entries), 4) + pl_entries = frappe.db.get_all( + "Payment Ledger Entry", + filters={"voucher_no": pe.name, "delinked": 0}, + ) + self.assertEqual(len(pl_entries), 3) + + gl_entries = frappe.db.get_all( + "GL Entry", + filters={"voucher_no": pe.name, "is_cancelled": 0}, + fields=["account", "voucher_no", "against_voucher", "debit", "credit"], + order_by="account, against_voucher, debit", + ) + expected_gle = [ + { + "account": self.advance_receivable_account, + "voucher_no": pe.name, + "against_voucher": pe.name, + "debit": 0.0, + "credit": amount, + }, + { + "account": self.advance_receivable_account, + "voucher_no": pe.name, + "against_voucher": pe.name, + "debit": amount, + "credit": 0.0, + }, + { + "account": self.debit_to, + "voucher_no": pe.name, + "against_voucher": je.name, + "debit": 0.0, + "credit": amount, + }, + { + "account": self.bank, + "voucher_no": pe.name, + "against_voucher": None, + "debit": amount, + "credit": 0.0, + }, + ] + self.assertEqual(gl_entries, expected_gle) + + pl_entries = frappe.db.get_all( + "Payment Ledger Entry", + filters={"voucher_no": pe.name}, + fields=["account", "voucher_no", "against_voucher_no", "amount"], + order_by="account, against_voucher_no, amount", + ) + expected_ple = [ + { + "account": self.advance_receivable_account, + "voucher_no": pe.name, + "against_voucher_no": pe.name, + "amount": -amount, + }, + { + "account": self.advance_receivable_account, + "voucher_no": pe.name, + "against_voucher_no": pe.name, + "amount": amount, + }, + { + "account": self.debit_to, + "voucher_no": pe.name, + "against_voucher_no": je.name, + "amount": -amount, + }, + ] + self.assertEqual(pl_entries, expected_ple) + + def test_advance_payment_reconciliation_against_journal_for_supplier(self): + self.supplier = make_supplier("_Test Supplier") + frappe.db.set_value( + "Company", + self.company, + { + "book_advance_payments_in_separate_party_account": 1, + "default_advance_paid_account": self.advance_payable_account, + "reconcile_on_advance_payment_date": 0, + }, + ) + amount = 200.0 + je = self.create_journal_entry(self.creditors, self.bank, -amount) + je.accounts[0].cost_center = self.main_cc.name + je.accounts[0].party_type = "Supplier" + je.accounts[0].party = self.supplier + je.accounts[1].cost_center = self.main_cc.name + je = je.save().submit() + + pe = self.create_payment_entry(amount=amount) + pe.payment_type = "Pay" + pe.party_type = "Supplier" + pe.paid_from = self.bank + pe.paid_to = self.creditors + pe.party = self.supplier + pe.save().submit() + + pr = self.create_payment_reconciliation(party_is_customer=False) + pr.default_advance_account = self.advance_payable_account + pr.get_unreconciled_entries() + self.assertEqual(len(pr.invoices), 1) + self.assertEqual(len(pr.payments), 1) + invoices = [invoice.as_dict() for invoice in pr.invoices] + payments = [payment.as_dict() for payment in pr.payments] + pr.allocate_entries(frappe._dict({"invoices": invoices, "payments": payments})) + pr.reconcile() + + # Assert Ledger Entries + gl_entries = frappe.db.get_all( + "GL Entry", + filters={"voucher_no": pe.name, "is_cancelled": 0}, + ) + self.assertEqual(len(gl_entries), 4) + pl_entries = frappe.db.get_all( + "Payment Ledger Entry", + filters={"voucher_no": pe.name, "delinked": 0}, + ) + self.assertEqual(len(pl_entries), 3) + + gl_entries = frappe.db.get_all( + "GL Entry", + filters={"voucher_no": pe.name, "is_cancelled": 0}, + fields=["account", "voucher_no", "against_voucher", "debit", "credit"], + order_by="account, against_voucher, debit", + ) + expected_gle = [ + { + "account": self.advance_payable_account, + "voucher_no": pe.name, + "against_voucher": pe.name, + "debit": 0.0, + "credit": amount, + }, + { + "account": self.advance_payable_account, + "voucher_no": pe.name, + "against_voucher": pe.name, + "debit": amount, + "credit": 0.0, + }, + { + "account": self.creditors, + "voucher_no": pe.name, + "against_voucher": je.name, + "debit": amount, + "credit": 0.0, + }, + { + "account": self.bank, + "voucher_no": pe.name, + "against_voucher": None, + "debit": 0.0, + "credit": amount, + }, + ] + self.assertEqual(gl_entries, expected_gle) + + pl_entries = frappe.db.get_all( + "Payment Ledger Entry", + filters={"voucher_no": pe.name}, + fields=["account", "voucher_no", "against_voucher_no", "amount"], + order_by="account, against_voucher_no, amount", + ) + expected_ple = [ + { + "account": self.advance_payable_account, + "voucher_no": pe.name, + "against_voucher_no": pe.name, + "amount": -amount, + }, + { + "account": self.advance_payable_account, + "voucher_no": pe.name, + "against_voucher_no": pe.name, + "amount": amount, + }, + { + "account": self.creditors, + "voucher_no": pe.name, + "against_voucher_no": je.name, + "amount": -amount, + }, + ] + self.assertEqual(pl_entries, expected_ple) + def make_customer(customer_name, currency=None): if not frappe.db.exists("Customer", customer_name): diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py index dce742d1021..0f64c9d697f 100644 --- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py +++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py @@ -549,6 +549,21 @@ class PurchaseInvoice(BuyingController): item.expense_account = stock_not_billed_account elif item.is_fixed_asset: account = None + if not item.pr_detail and item.po_detail: + receipt_item = frappe.get_cached_value( + "Purchase Receipt Item", + { + "purchase_order": item.purchase_order, + "purchase_order_item": item.po_detail, + "docstatus": 1, + }, + ["name", "parent"], + as_dict=1, + ) + if receipt_item: + item.pr_detail = receipt_item.name + item.purchase_receipt = receipt_item.parent + if item.pr_detail: if not self.asset_received_but_not_billed: self.asset_received_but_not_billed = self.get_company_default( @@ -795,13 +810,12 @@ class PurchaseInvoice(BuyingController): self.db_set("repost_required", self.needs_repost) def make_gl_entries(self, gl_entries=None, from_repost=False): - if not gl_entries: - gl_entries = self.get_gl_entries() + update_outstanding = "No" if (cint(self.is_paid) or self.write_off_account) else "Yes" + if self.docstatus == 1: + if not gl_entries: + gl_entries = self.get_gl_entries() - if gl_entries: - update_outstanding = "No" if (cint(self.is_paid) or self.write_off_account) else "Yes" - - if self.docstatus == 1: + if gl_entries: make_gl_entries( gl_entries, update_outstanding=update_outstanding, @@ -809,32 +823,43 @@ class PurchaseInvoice(BuyingController): from_repost=from_repost, ) self.make_exchange_gain_loss_journal() - elif self.docstatus == 2: - provisional_entries = [a for a in gl_entries if a.voucher_type == "Purchase Receipt"] - make_reverse_gl_entries(voucher_type=self.doctype, voucher_no=self.name) - if provisional_entries: - for entry in provisional_entries: - frappe.db.set_value( - "GL Entry", - { - "voucher_type": "Purchase Receipt", - "voucher_detail_no": entry.voucher_detail_no, - }, - "is_cancelled", - 1, - ) - - if update_outstanding == "No": - update_outstanding_amt( - self.credit_to, - "Supplier", - self.supplier, - self.doctype, - self.return_against if cint(self.is_return) and self.return_against else self.name, - ) - - elif self.docstatus == 2 and cint(self.update_stock) and self.auto_accounting_for_stock: + elif self.docstatus == 2: make_reverse_gl_entries(voucher_type=self.doctype, voucher_no=self.name) + self.cancel_provisional_entries() + + self.update_supplier_outstanding(update_outstanding) + + def cancel_provisional_entries(self): + rows = set() + purchase_receipts = set() + for d in self.items: + if d.purchase_receipt: + purchase_receipts.add(d.purchase_receipt) + rows.add(d.name) + + if rows: + # cancel gl entries + gle = qb.DocType("GL Entry") + gle_update_query = ( + qb.update(gle) + .set(gle.is_cancelled, 1) + .where( + (gle.voucher_type == "Purchase Receipt") + & (gle.voucher_no.isin(purchase_receipts)) + & (gle.voucher_detail_no.isin(rows)) + ) + ) + gle_update_query.run() + + def update_supplier_outstanding(self, update_outstanding): + if update_outstanding == "No": + update_outstanding_amt( + self.credit_to, + "Supplier", + self.supplier, + self.doctype, + self.return_against if cint(self.is_return) and self.return_against else self.name, + ) def get_gl_entries(self, warehouse_account=None): self.auto_accounting_for_stock = erpnext.is_perpetual_inventory_enabled(self.company) @@ -947,8 +972,8 @@ class PurchaseInvoice(BuyingController): "Company", self.company, "enable_provisional_accounting_for_non_stock_items" ) ) - - purchase_receipt_doc_map = {} + if provisional_accounting_for_non_stock_items: + self.get_provisional_accounts() for item in self.get("items"): if flt(item.base_net_amount): @@ -1087,49 +1112,7 @@ class PurchaseInvoice(BuyingController): dummy, amount = self.get_amount_and_base_amount(item, None) if provisional_accounting_for_non_stock_items: - if item.purchase_receipt: - provisional_account, pr_qty, pr_base_rate, pr_rate = frappe.get_cached_value( - "Purchase Receipt Item", - item.pr_detail, - ["provisional_expense_account", "qty", "base_rate", "rate"], - ) - provisional_account = provisional_account or self.get_company_default( - "default_provisional_account" - ) - purchase_receipt_doc = purchase_receipt_doc_map.get(item.purchase_receipt) - - if not purchase_receipt_doc: - purchase_receipt_doc = frappe.get_doc( - "Purchase Receipt", item.purchase_receipt - ) - purchase_receipt_doc_map[item.purchase_receipt] = purchase_receipt_doc - - # Post reverse entry for Stock-Received-But-Not-Billed if it is booked in Purchase Receipt - expense_booked_in_pr = frappe.db.get_value( - "GL Entry", - { - "is_cancelled": 0, - "voucher_type": "Purchase Receipt", - "voucher_no": item.purchase_receipt, - "voucher_detail_no": item.pr_detail, - "account": provisional_account, - }, - "name", - ) - - if expense_booked_in_pr: - # Intentionally passing purchase invoice item to handle partial billing - purchase_receipt_doc.add_provisional_gl_entry( - item, - gl_entries, - self.posting_date, - provisional_account, - reverse=1, - item_amount=( - (min(item.qty, pr_qty) * pr_rate) - * purchase_receipt_doc.get("conversion_rate") - ), - ) + self.make_provisional_gl_entry(gl_entries, item) if not self.is_internal_transfer(): gl_entries.append( @@ -1225,6 +1208,59 @@ class PurchaseInvoice(BuyingController): if item.is_fixed_asset and item.landed_cost_voucher_amount: self.update_gross_purchase_amount_for_linked_assets(item) + def get_provisional_accounts(self): + self.provisional_accounts = frappe._dict() + linked_purchase_receipts = set([d.purchase_receipt for d in self.items if d.purchase_receipt]) + pr_items = frappe.get_all( + "Purchase Receipt Item", + filters={"parent": ("in", linked_purchase_receipts)}, + fields=["name", "provisional_expense_account", "qty", "base_rate"], + ) + default_provisional_account = self.get_company_default("default_provisional_account") + provisional_accounts = set( + [ + d.provisional_expense_account + if d.provisional_expense_account + else default_provisional_account + for d in pr_items + ] + ) + + provisional_gl_entries = frappe.get_all( + "GL Entry", + filters={ + "voucher_type": "Purchase Receipt", + "voucher_no": ("in", linked_purchase_receipts), + "account": ("in", provisional_accounts), + "is_cancelled": 0, + }, + fields=["voucher_detail_no"], + ) + rows_with_provisional_entries = [d.voucher_detail_no for d in provisional_gl_entries] + for item in pr_items: + self.provisional_accounts[item.name] = { + "provisional_account": item.provisional_expense_account or default_provisional_account, + "qty": item.qty, + "base_rate": item.base_rate, + "has_provisional_entry": item.name in rows_with_provisional_entries, + } + + def make_provisional_gl_entry(self, gl_entries, item): + if item.purchase_receipt: + pr_item = self.provisional_accounts.get(item.pr_detail, {}) + if pr_item.get("has_provisional_entry"): + purchase_receipt_doc = frappe.get_cached_doc("Purchase Receipt", item.purchase_receipt) + + # Intentionally passing purchase invoice item to handle partial billing + purchase_receipt_doc.add_provisional_gl_entry( + item, + gl_entries, + self.posting_date, + pr_item.get("provisional_account"), + reverse=1, + item_amount=(min(item.qty, pr_item.get("qty")) * pr_item.get("base_rate")), + ) + def update_gross_purchase_amount_for_linked_assets(self, item): assets = frappe.db.get_all( "Asset", diff --git a/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py b/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py index 3c3b081fa2f..4980c22fac1 100644 --- a/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py +++ b/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py @@ -10,7 +10,11 @@ import erpnext from erpnext.accounts.doctype.account.test_account import create_account, get_inventory_account from erpnext.accounts.doctype.payment_entry.payment_entry import get_payment_entry from erpnext.buying.doctype.purchase_order.purchase_order import get_mapped_purchase_invoice -from erpnext.buying.doctype.purchase_order.test_purchase_order import create_purchase_order +from erpnext.buying.doctype.purchase_order.purchase_order import make_purchase_invoice as make_pi_from_po +from erpnext.buying.doctype.purchase_order.test_purchase_order import ( + create_pr_against_po, + create_purchase_order, +) from erpnext.buying.doctype.supplier.test_supplier import create_supplier from erpnext.controllers.accounts_controller import get_payment_terms from erpnext.controllers.buying_controller import QtyMismatchError @@ -2185,6 +2189,56 @@ class TestPurchaseInvoice(FrappeTestCase, StockTestMixin): self.assertEqual(row.serial_no, "\n".join(serial_nos[:2])) self.assertEqual(row.rejected_serial_no, serial_nos[2]) + def test_make_pr_and_pi_from_po(self): + from erpnext.assets.doctype.asset.test_asset import create_asset_category + + if not frappe.db.exists("Asset Category", "Computers"): + create_asset_category() + + item = create_item( + item_code="_Test_Item", is_stock_item=0, is_fixed_asset=1, asset_category="Computers" + ) + po = create_purchase_order(item_code=item.item_code) + pr = create_pr_against_po(po.name, 10) + pi = make_pi_from_po(po.name) + pi.insert() + pi.submit() + + pr_gl_entries = frappe.db.sql( + """select account, debit, credit + from `tabGL Entry` where voucher_type='Purchase Receipt' and voucher_no=%s + order by account asc""", + pr.name, + as_dict=1, + ) + + pr_expected_values = [ + ["Asset Received But Not Billed - _TC", 0, 5000], + ["CWIP Account - _TC", 5000, 0], + ] + + for i, gle in enumerate(pr_gl_entries): + self.assertEqual(pr_expected_values[i][0], gle.account) + self.assertEqual(pr_expected_values[i][1], gle.debit) + self.assertEqual(pr_expected_values[i][2], gle.credit) + + pi_gl_entries = frappe.db.sql( + """select account, debit, credit + from `tabGL Entry` where voucher_type='Purchase Invoice' and voucher_no=%s + order by account asc""", + pi.name, + as_dict=1, + ) + pi_expected_values = [ + ["Asset Received But Not Billed - _TC", 5000, 0], + ["Creditors - _TC", 0, 5000], + ] + + for i, gle in enumerate(pi_gl_entries): + self.assertEqual(pi_expected_values[i][0], gle.account) + self.assertEqual(pi_expected_values[i][1], gle.debit) + self.assertEqual(pi_expected_values[i][2], gle.credit) + def set_advance_flag(company, flag, default_account): frappe.db.set_value( diff --git a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py index 5cffe11e995..b0d62339be0 100644 --- a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py +++ b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py @@ -2202,13 +2202,14 @@ class TestSalesInvoice(FrappeTestCase): self.assertEqual(si.total_taxes_and_charges, 228.82) self.assertEqual(si.rounding_adjustment, -0.01) - expected_values = [ - ["_Test Account Service Tax - _TC", 0.0, 114.41], - ["_Test Account VAT - _TC", 0.0, 114.41], - [si.debit_to, 1500, 0.0], - ["Round Off - _TC", 0.01, 0.01], - ["Sales - _TC", 0.0, 1271.18], - ] + round_off_account = frappe.get_cached_value("Company", "_Test Company", "round_off_account") + expected_values = { + "_Test Account Service Tax - _TC": [0.0, 114.41], + "_Test Account VAT - _TC": [0.0, 114.41], + si.debit_to: [1500, 0.0], + round_off_account: [0.01, 0.01], + "Sales - _TC": [0.0, 1271.18], + } gl_entries = frappe.db.sql( """select account, sum(debit) as debit, sum(credit) as credit @@ -2219,10 +2220,10 @@ class TestSalesInvoice(FrappeTestCase): as_dict=1, ) - for i, gle in enumerate(gl_entries): - self.assertEqual(expected_values[i][0], gle.account) - self.assertEqual(expected_values[i][1], gle.debit) - self.assertEqual(expected_values[i][2], gle.credit) + for gle in gl_entries: + expected_account_values = expected_values[gle.account] + self.assertEqual(expected_account_values[0], gle.debit) + self.assertEqual(expected_account_values[1], gle.credit) def test_rounding_adjustment_3(self): from erpnext.accounts.doctype.accounting_dimension.test_accounting_dimension import ( @@ -2270,6 +2271,7 @@ class TestSalesInvoice(FrappeTestCase): self.assertEqual(si.total_taxes_and_charges, 480.86) self.assertEqual(si.rounding_adjustment, -0.02) + round_off_account = frappe.get_cached_value("Company", "_Test Company", "round_off_account") expected_values = dict( (d[0], d) for d in [ @@ -2277,7 +2279,7 @@ class TestSalesInvoice(FrappeTestCase): ["_Test Account Service Tax - _TC", 0.0, 240.43], ["_Test Account VAT - _TC", 0.0, 240.43], ["Sales - _TC", 0.0, 4007.15], - ["Round Off - _TC", 0.02, 0.01], + [round_off_account, 0.02, 0.01], ] ) @@ -2306,8 +2308,9 @@ class TestSalesInvoice(FrappeTestCase): as_dict=1, ) - self.assertEqual(round_off_gle.cost_center, "_Test Cost Center 2 - _TC") - self.assertEqual(round_off_gle.location, "Block 1") + if round_off_gle: + self.assertEqual(round_off_gle.cost_center, "_Test Cost Center 2 - _TC") + self.assertEqual(round_off_gle.location, "Block 1") disable_dimension() diff --git a/erpnext/accounts/doctype/unreconcile_payment/test_unreconcile_payment.py b/erpnext/accounts/doctype/unreconcile_payment/test_unreconcile_payment.py index 882dd1d6dab..43dfbfaef60 100644 --- a/erpnext/accounts/doctype/unreconcile_payment/test_unreconcile_payment.py +++ b/erpnext/accounts/doctype/unreconcile_payment/test_unreconcile_payment.py @@ -107,7 +107,7 @@ class TestUnreconcilePayment(AccountsTestMixin, FrappeTestCase): self.assertEqual(len(pe.references), 1) self.assertEqual(pe.unallocated_amount, 100) - def test_02_unreconcile_one_payment_from_multi_payments(self): + def test_02_unreconcile_one_payment_among_multi_payments(self): """ Scenario: 2 payments, both split against 2 different invoices Unreconcile only one payment from one invoice diff --git a/erpnext/accounts/general_ledger.py b/erpnext/accounts/general_ledger.py index 3f01dee888d..2fd7b5d3c81 100644 --- a/erpnext/accounts/general_ledger.py +++ b/erpnext/accounts/general_ledger.py @@ -7,7 +7,7 @@ import copy import frappe from frappe import _ from frappe.model.meta import get_field_precision -from frappe.utils import cint, cstr, flt, formatdate, getdate, now +from frappe.utils import cint, flt, formatdate, getdate, now import erpnext from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import ( @@ -228,11 +228,13 @@ def get_cost_center_allocation_data(company, posting_date): def merge_similar_entries(gl_map, precision=None): merged_gl_map = [] accounting_dimensions = get_accounting_dimensions() + merge_properties = get_merge_properties(accounting_dimensions) for entry in gl_map: + entry.merge_key = get_merge_key(entry, merge_properties) # if there is already an entry in this account then just add it # to that entry - same_head = check_if_in_list(entry, merged_gl_map, accounting_dimensions) + same_head = check_if_in_list(entry, merged_gl_map) if same_head: same_head.debit = flt(same_head.debit) + flt(entry.debit) same_head.debit_in_account_currency = flt(same_head.debit_in_account_currency) + flt( @@ -273,34 +275,35 @@ def merge_similar_entries(gl_map, precision=None): return merged_gl_map -def check_if_in_list(gle, gl_map, dimensions=None): - account_head_fieldnames = [ - "voucher_detail_no", - "party", - "against_voucher", +def get_merge_properties(dimensions=None): + merge_properties = [ + "account", "cost_center", - "against_voucher_type", + "party", "party_type", + "voucher_detail_no", + "against_voucher", + "against_voucher_type", "project", "finance_book", "voucher_no", ] - if dimensions: - account_head_fieldnames = account_head_fieldnames + dimensions + merge_properties.extend(dimensions) + return merge_properties + +def get_merge_key(entry, merge_properties): + merge_key = [] + for fieldname in merge_properties: + merge_key.append(entry.get(fieldname, "")) + + return tuple(merge_key) + + +def check_if_in_list(gle, gl_map): for e in gl_map: - same_head = True - if e.account != gle.account: - same_head = False - continue - - for fieldname in account_head_fieldnames: - if cstr(e.get(fieldname)) != cstr(gle.get(fieldname)): - same_head = False - break - - if same_head: + if e.merge_key == gle.merge_key: return e diff --git a/erpnext/accounts/utils.py b/erpnext/accounts/utils.py index 1ad24c46603..2558e976bea 100644 --- a/erpnext/accounts/utils.py +++ b/erpnext/accounts/utils.py @@ -10,7 +10,7 @@ import frappe.defaults from frappe import _, qb, throw from frappe.model.meta import get_field_precision from frappe.query_builder import AliasedQuery, Criterion, Table -from frappe.query_builder.functions import Sum +from frappe.query_builder.functions import Count, Sum from frappe.query_builder.utils import DocType from frappe.utils import ( add_days, @@ -1492,24 +1492,39 @@ def get_stock_accounts(company, voucher_type=None, voucher_no=None): ) ] - return stock_accounts + return list(set(stock_accounts)) def get_stock_and_account_balance(account=None, posting_date=None, company=None): if not posting_date: posting_date = nowdate() - warehouse_account = get_warehouse_account_map(company) - account_balance = get_balance_on( account, posting_date, in_account_currency=False, ignore_account_permission=True ) - related_warehouses = [ - wh - for wh, wh_details in warehouse_account.items() - if wh_details.account == account and not wh_details.is_group - ] + account_table = frappe.qb.DocType("Account") + query = ( + frappe.qb.from_(account_table) + .select(Count(account_table.name)) + .where( + (account_table.account_type == "Stock") + & (account_table.company == company) + & (account_table.is_group == 0) + ) + ) + + no_of_stock_accounts = cint(query.run()[0][0]) + + related_warehouses = [] + if no_of_stock_accounts > 1: + warehouse_account = get_warehouse_account_map(company) + + related_warehouses = [ + wh + for wh, wh_details in warehouse_account.items() + if wh_details.account == account and not wh_details.is_group + ] total_stock_value = get_stock_value_on(related_warehouses, posting_date) diff --git a/erpnext/assets/doctype/asset_maintenance/asset_maintenance.py b/erpnext/assets/doctype/asset_maintenance/asset_maintenance.py index 570c9751a57..b44164f2dae 100644 --- a/erpnext/assets/doctype/asset_maintenance/asset_maintenance.py +++ b/erpnext/assets/doctype/asset_maintenance/asset_maintenance.py @@ -18,9 +18,7 @@ class AssetMaintenance(Document): if TYPE_CHECKING: from frappe.types import DF - from erpnext.assets.doctype.asset_maintenance_task.asset_maintenance_task import ( - AssetMaintenanceTask, - ) + from erpnext.assets.doctype.asset_maintenance_task.asset_maintenance_task import AssetMaintenanceTask asset_category: DF.ReadOnly | None asset_maintenance_tasks: DF.Table[AssetMaintenanceTask] @@ -47,6 +45,11 @@ class AssetMaintenance(Document): assign_tasks(self.name, task.assign_to, task.maintenance_task, task.next_due_date) self.sync_maintenance_tasks() + def after_delete(self): + asset = frappe.get_doc("Asset", self.asset_name) + if asset.status == "In Maintenance": + asset.set_status() + def sync_maintenance_tasks(self): tasks_names = [] for task in self.get("asset_maintenance_tasks"): diff --git a/erpnext/assets/doctype/asset_maintenance_log/asset_maintenance_log.py b/erpnext/assets/doctype/asset_maintenance_log/asset_maintenance_log.py index 009bcc3e69a..95d02714c5b 100644 --- a/erpnext/assets/doctype/asset_maintenance_log/asset_maintenance_log.py +++ b/erpnext/assets/doctype/asset_maintenance_log/asset_maintenance_log.py @@ -5,7 +5,8 @@ import frappe from frappe import _ from frappe.model.document import Document -from frappe.utils import getdate, nowdate +from frappe.query_builder import DocType +from frappe.utils import getdate, nowdate, today from erpnext.assets.doctype.asset_maintenance.asset_maintenance import calculate_next_due_date @@ -75,6 +76,17 @@ class AssetMaintenanceLog(Document): asset_maintenance_doc.save() +def update_asset_maintenance_log_status(): + AssetMaintenanceLog = DocType("Asset Maintenance Log") + ( + frappe.qb.update(AssetMaintenanceLog) + .set(AssetMaintenanceLog.maintenance_status, "Overdue") + .where( + (AssetMaintenanceLog.maintenance_status == "Planned") & (AssetMaintenanceLog.due_date < today()) + ) + ).run() + + @frappe.whitelist() @frappe.validate_and_sanitize_search_inputs def get_maintenance_tasks(doctype, txt, searchfield, start, page_len, filters): diff --git a/erpnext/assets/doctype/asset_repair/asset_repair.js b/erpnext/assets/doctype/asset_repair/asset_repair.js index 27a4eb6e995..489fbaca6b2 100644 --- a/erpnext/assets/doctype/asset_repair/asset_repair.js +++ b/erpnext/assets/doctype/asset_repair/asset_repair.js @@ -20,14 +20,14 @@ frappe.ui.form.on("Asset Repair", { }; }; - frm.fields_dict.warehouse.get_query = function (doc) { + frm.set_query("warehouse", "stock_items", function () { return { filters: { is_group: 0, - company: doc.company, + company: frm.doc.company, }, }; - }; + }); frm.set_query("serial_and_batch_bundle", "stock_items", (doc, cdt, cdn) => { let row = locals[cdt][cdn]; @@ -79,7 +79,7 @@ frappe.ui.form.on("Asset Repair", { }); } - if (frm.doc.repair_status == "Completed") { + if (frm.doc.repair_status == "Completed" && !frm.doc.completion_date) { frm.set_value("completion_date", frappe.datetime.now_datetime()); } }, @@ -87,15 +87,48 @@ frappe.ui.form.on("Asset Repair", { stock_items_on_form_rendered() { erpnext.setup_serial_or_batch_no(); }, + + stock_consumption: function (frm) { + if (!frm.doc.stock_consumption) { + frm.clear_table("stock_items"); + frm.refresh_field("stock_items"); + } + }, + + purchase_invoice: function (frm) { + if (frm.doc.purchase_invoice) { + frappe.call({ + method: "frappe.client.get_value", + args: { + doctype: "Purchase Invoice", + fieldname: "base_net_total", + filters: { name: frm.doc.purchase_invoice }, + }, + callback: function (r) { + if (r.message) { + frm.set_value("repair_cost", r.message.base_net_total); + } + }, + }); + } else { + frm.set_value("repair_cost", 0); + } + }, }); frappe.ui.form.on("Asset Repair Consumed Item", { - item_code: function (frm, cdt, cdn) { + warehouse: function (frm, cdt, cdn) { var item = locals[cdt][cdn]; + if (!item.item_code) { + frappe.msgprint(__("Please select an item code before setting the warehouse.")); + frappe.model.set_value(cdt, cdn, "warehouse", ""); + return; + } + let item_args = { item_code: item.item_code, - warehouse: frm.doc.warehouse, + warehouse: item.warehouse, qty: item.consumed_quantity, serial_no: item.serial_no, company: frm.doc.company, diff --git a/erpnext/assets/doctype/asset_repair/asset_repair.json b/erpnext/assets/doctype/asset_repair/asset_repair.json index accb5bf54b5..c98f5a8d7f4 100644 --- a/erpnext/assets/doctype/asset_repair/asset_repair.json +++ b/erpnext/assets/doctype/asset_repair/asset_repair.json @@ -22,16 +22,14 @@ "column_break_14", "project", "accounting_details", - "repair_cost", + "purchase_invoice", "capitalize_repair_cost", "stock_consumption", "column_break_8", - "purchase_invoice", + "repair_cost", "stock_consumption_details_section", - "warehouse", "stock_items", "total_repair_cost", - "stock_entry", "asset_depreciation_details_section", "increase_in_asset_life", "section_break_9", @@ -122,7 +120,8 @@ "default": "0", "fieldname": "repair_cost", "fieldtype": "Currency", - "label": "Repair Cost" + "label": "Repair Cost", + "read_only": 1 }, { "fieldname": "amended_from", @@ -218,13 +217,6 @@ "label": "Total Repair Cost", "read_only": 1 }, - { - "depends_on": "stock_consumption", - "fieldname": "warehouse", - "fieldtype": "Link", - "label": "Warehouse", - "options": "Warehouse" - }, { "depends_on": "capitalize_repair_cost", "fieldname": "asset_depreciation_details_section", @@ -251,20 +243,12 @@ "fieldtype": "Link", "label": "Company", "options": "Company" - }, - { - "fieldname": "stock_entry", - "fieldtype": "Link", - "label": "Stock Entry", - "no_copy": 1, - "options": "Stock Entry", - "read_only": 1 } ], "index_web_pages_for_search": 1, "is_submittable": 1, "links": [], - "modified": "2022-08-16 15:55:25.023471", + "modified": "2024-06-13 16:14:14.398356", "modified_by": "Administrator", "module": "Assets", "name": "Asset Repair", diff --git a/erpnext/assets/doctype/asset_repair/asset_repair.py b/erpnext/assets/doctype/asset_repair/asset_repair.py index ccde836fe0d..903e68e32e0 100644 --- a/erpnext/assets/doctype/asset_repair/asset_repair.py +++ b/erpnext/assets/doctype/asset_repair/asset_repair.py @@ -47,20 +47,25 @@ class AssetRepair(AccountsController): repair_cost: DF.Currency repair_status: DF.Literal["Pending", "Completed", "Cancelled"] stock_consumption: DF.Check - stock_entry: DF.Link | None stock_items: DF.Table[AssetRepairConsumedItem] total_repair_cost: DF.Currency - warehouse: DF.Link | None # end: auto-generated types def validate(self): self.asset_doc = frappe.get_doc("Asset", self.asset) + self.validate_dates() self.update_status() if self.get("stock_items"): self.set_stock_items_cost() self.calculate_total_repair_cost() + def validate_dates(self): + if self.completion_date and (self.failure_date > self.completion_date): + frappe.throw( + _("Completion Date can not be before Failure Date. Please adjust the dates accordingly.") + ) + def update_status(self): if self.repair_status == "Pending" and self.asset_doc.status != "Out of Order": frappe.db.set_value("Asset", self.asset, "status", "Out of Order") @@ -105,22 +110,22 @@ class AssetRepair(AccountsController): if self.asset_doc.calculate_depreciation and self.increase_in_asset_life: self.modify_depreciation_schedule() - notes = _( - "This schedule was created when Asset {0} was repaired through Asset Repair {1}." - ).format( - get_link_to_form(self.asset_doc.doctype, self.asset_doc.name), - get_link_to_form(self.doctype, self.name), - ) - self.asset_doc.flags.ignore_validate_update_after_submit = True - make_new_active_asset_depr_schedules_and_cancel_current_ones(self.asset_doc, notes) - self.asset_doc.save() + notes = _( + "This schedule was created when Asset {0} was repaired through Asset Repair {1}." + ).format( + get_link_to_form(self.asset_doc.doctype, self.asset_doc.name), + get_link_to_form(self.doctype, self.name), + ) + self.asset_doc.flags.ignore_validate_update_after_submit = True + make_new_active_asset_depr_schedules_and_cancel_current_ones(self.asset_doc, notes) + self.asset_doc.save() - add_asset_activity( - self.asset, - _("Asset updated after completion of Asset Repair {0}").format( - get_link_to_form("Asset Repair", self.name) - ), - ) + add_asset_activity( + self.asset, + _("Asset updated after completion of Asset Repair {0}").format( + get_link_to_form("Asset Repair", self.name) + ), + ) def before_cancel(self): self.asset_doc = frappe.get_doc("Asset", self.asset) @@ -136,29 +141,28 @@ class AssetRepair(AccountsController): self.asset_doc.total_asset_cost -= self.repair_cost self.asset_doc.additional_asset_cost -= self.repair_cost - if self.get("stock_consumption"): - self.increase_stock_quantity() if self.get("capitalize_repair_cost"): self.ignore_linked_doctypes = ("GL Entry", "Stock Ledger Entry") self.make_gl_entries(cancel=True) - self.db_set("stock_entry", None) if self.asset_doc.calculate_depreciation and self.increase_in_asset_life: self.revert_depreciation_schedule_on_cancellation() - notes = _("This schedule was created when Asset {0}'s Asset Repair {1} was cancelled.").format( - get_link_to_form(self.asset_doc.doctype, self.asset_doc.name), - get_link_to_form(self.doctype, self.name), - ) - self.asset_doc.flags.ignore_validate_update_after_submit = True - make_new_active_asset_depr_schedules_and_cancel_current_ones(self.asset_doc, notes) - self.asset_doc.save() + notes = _( + "This schedule was created when Asset {0}'s Asset Repair {1} was cancelled." + ).format( + get_link_to_form(self.asset_doc.doctype, self.asset_doc.name), + get_link_to_form(self.doctype, self.name), + ) + self.asset_doc.flags.ignore_validate_update_after_submit = True + make_new_active_asset_depr_schedules_and_cancel_current_ones(self.asset_doc, notes) + self.asset_doc.save() - add_asset_activity( - self.asset, - _("Asset updated after cancellation of Asset Repair {0}").format( - get_link_to_form("Asset Repair", self.name) - ), - ) + add_asset_activity( + self.asset, + _("Asset updated after cancellation of Asset Repair {0}").format( + get_link_to_form("Asset Repair", self.name) + ), + ) def after_delete(self): frappe.get_doc("Asset", self.asset).set_status() @@ -170,11 +174,6 @@ class AssetRepair(AccountsController): def check_for_stock_items_and_warehouse(self): if not self.get("stock_items"): frappe.throw(_("Please enter Stock Items consumed during the Repair."), title=_("Missing Items")) - if not self.warehouse: - frappe.throw( - _("Please enter Warehouse from which Stock Items consumed during the Repair were taken."), - title=_("Missing Warehouse"), - ) def increase_asset_value(self): total_value_of_stock_consumed = self.get_total_value_of_stock_consumed() @@ -208,6 +207,7 @@ class AssetRepair(AccountsController): stock_entry = frappe.get_doc( {"doctype": "Stock Entry", "stock_entry_type": "Material Issue", "company": self.company} ) + stock_entry.asset_repair = self.name for stock_item in self.get("stock_items"): self.validate_serial_no(stock_item) @@ -215,7 +215,7 @@ class AssetRepair(AccountsController): stock_entry.append( "items", { - "s_warehouse": self.warehouse, + "s_warehouse": stock_item.warehouse, "item_code": stock_item.item_code, "qty": stock_item.consumed_quantity, "basic_rate": stock_item.valuation_rate, @@ -228,8 +228,6 @@ class AssetRepair(AccountsController): stock_entry.insert() stock_entry.submit() - self.db_set("stock_entry", stock_entry.name) - def validate_serial_no(self, stock_item): if not stock_item.serial_and_batch_bundle and frappe.get_cached_value( "Item", stock_item.item_code, "has_serial_no" @@ -247,12 +245,6 @@ class AssetRepair(AccountsController): "Serial and Batch Bundle", stock_item.serial_and_batch_bundle, values_to_update ) - def increase_stock_quantity(self): - if self.stock_entry: - stock_entry = frappe.get_doc("Stock Entry", self.stock_entry) - stock_entry.flags.ignore_links = True - stock_entry.cancel() - def make_gl_entries(self, cancel=False): if flt(self.total_repair_cost) > 0: gl_entries = self.get_gl_entries() @@ -316,7 +308,7 @@ class AssetRepair(AccountsController): return # creating GL Entries for each row in Stock Items based on the Stock Entry created for it - stock_entry = frappe.get_doc("Stock Entry", self.stock_entry) + stock_entry = frappe.get_doc("Stock Entry", {"asset_repair": self.name}) default_expense_account = None if not erpnext.is_perpetual_inventory_enabled(self.company): @@ -357,7 +349,7 @@ class AssetRepair(AccountsController): "cost_center": self.cost_center, "posting_date": getdate(), "against_voucher_type": "Stock Entry", - "against_voucher": self.stock_entry, + "against_voucher": stock_entry.name, "company": self.company, }, item=self, diff --git a/erpnext/assets/doctype/asset_repair/test_asset_repair.py b/erpnext/assets/doctype/asset_repair/test_asset_repair.py index 278da1b08bf..44d08869a63 100644 --- a/erpnext/assets/doctype/asset_repair/test_asset_repair.py +++ b/erpnext/assets/doctype/asset_repair/test_asset_repair.py @@ -76,14 +76,14 @@ class TestAssetRepair(unittest.TestCase): def test_warehouse(self): asset_repair = create_asset_repair(stock_consumption=1) self.assertTrue(asset_repair.stock_consumption) - self.assertTrue(asset_repair.warehouse) + self.assertTrue(asset_repair.stock_items[0].warehouse) def test_decrease_stock_quantity(self): asset_repair = create_asset_repair(stock_consumption=1, submit=1) stock_entry = frappe.get_last_doc("Stock Entry") self.assertEqual(stock_entry.stock_entry_type, "Material Issue") - self.assertEqual(stock_entry.items[0].s_warehouse, asset_repair.warehouse) + self.assertEqual(stock_entry.items[0].s_warehouse, asset_repair.stock_items[0].warehouse) self.assertEqual(stock_entry.items[0].item_code, asset_repair.stock_items[0].item_code) self.assertEqual(stock_entry.items[0].qty, asset_repair.stock_items[0].consumed_quantity) @@ -114,14 +114,14 @@ class TestAssetRepair(unittest.TestCase): asset_repair.repair_status = "Completed" self.assertRaises(frappe.ValidationError, asset_repair.submit) - def test_increase_in_asset_value_due_to_stock_consumption(self): + def test_no_increase_in_asset_value_when_not_capitalized(self): asset = create_asset(calculate_depreciation=1, submit=1) initial_asset_value = get_asset_value_after_depreciation(asset.name) - asset_repair = create_asset_repair(asset=asset, stock_consumption=1, submit=1) + create_asset_repair(asset=asset, stock_consumption=1, submit=1) asset.reload() increase_in_asset_value = get_asset_value_after_depreciation(asset.name) - initial_asset_value - self.assertEqual(asset_repair.stock_items[0].total_value, increase_in_asset_value) + self.assertEqual(increase_in_asset_value, 0) def test_increase_in_asset_value_due_to_repair_cost_capitalisation(self): asset = create_asset(calculate_depreciation=1, submit=1) @@ -185,7 +185,7 @@ class TestAssetRepair(unittest.TestCase): frappe.get_doc("Purchase Invoice", asset_repair.purchase_invoice).items[0].expense_account ) stock_entry_expense_account = ( - frappe.get_doc("Stock Entry", asset_repair.stock_entry).get("items")[0].expense_account + frappe.get_doc("Stock Entry", {"asset_repair": asset_repair.name}).get("items")[0].expense_account ) expected_values = { @@ -260,6 +260,12 @@ class TestAssetRepair(unittest.TestCase): asset.finance_books[0].value_after_depreciation, ) + def test_asset_repiar_link_in_stock_entry(self): + asset = create_asset(calculate_depreciation=1, submit=1) + asset_repair = create_asset_repair(asset=asset, stock_consumption=1, submit=1) + stock_entry = frappe.get_last_doc("Stock Entry") + self.assertEqual(stock_entry.asset_repair, asset_repair.name) + def num_of_depreciations(asset): return asset.finance_books[0].total_number_of_depreciations @@ -289,7 +295,7 @@ def create_asset_repair(**args): if args.stock_consumption: asset_repair.stock_consumption = 1 - asset_repair.warehouse = args.warehouse or create_warehouse("Test Warehouse", company=asset.company) + warehouse = args.warehouse or create_warehouse("Test Warehouse", company=asset.company) bundle = None if args.serial_no: @@ -297,8 +303,8 @@ def create_asset_repair(**args): frappe._dict( { "item_code": args.item_code, - "warehouse": asset_repair.warehouse, - "company": frappe.get_cached_value("Warehouse", asset_repair.warehouse, "company"), + "warehouse": warehouse, + "company": frappe.get_cached_value("Warehouse", warehouse, "company"), "qty": (flt(args.stock_qty) or 1) * -1, "voucher_type": "Asset Repair", "type_of_transaction": "Asset Repair", @@ -314,6 +320,7 @@ def create_asset_repair(**args): "stock_items", { "item_code": args.item_code or "_Test Stock Item", + "warehouse": warehouse, "valuation_rate": args.rate if args.get("rate") is not None else 100, "consumed_quantity": args.qty or 1, "serial_and_batch_bundle": bundle, @@ -333,7 +340,7 @@ def create_asset_repair(**args): stock_entry.append( "items", { - "t_warehouse": asset_repair.warehouse, + "t_warehouse": asset_repair.stock_items[0].warehouse, "item_code": asset_repair.stock_items[0].item_code, "qty": asset_repair.stock_items[0].consumed_quantity, "basic_rate": args.rate if args.get("rate") is not None else 100, @@ -351,7 +358,7 @@ def create_asset_repair(**args): company=asset.company, expense_account=frappe.db.get_value("Company", asset.company, "default_expense_account"), cost_center=asset_repair.cost_center, - warehouse=asset_repair.warehouse, + warehouse=args.warehouse or create_warehouse("Test Warehouse", company=asset.company), ) asset_repair.purchase_invoice = pi.name diff --git a/erpnext/assets/doctype/asset_repair_consumed_item/asset_repair_consumed_item.json b/erpnext/assets/doctype/asset_repair_consumed_item/asset_repair_consumed_item.json index 6910c2eebf6..c4c13ce413b 100644 --- a/erpnext/assets/doctype/asset_repair_consumed_item/asset_repair_consumed_item.json +++ b/erpnext/assets/doctype/asset_repair_consumed_item/asset_repair_consumed_item.json @@ -6,6 +6,7 @@ "engine": "InnoDB", "field_order": [ "item_code", + "warehouse", "valuation_rate", "consumed_quantity", "total_value", @@ -44,19 +45,28 @@ "fieldtype": "Link", "in_list_view": 1, "label": "Item", - "options": "Item" + "options": "Item", + "reqd": 1 }, { "fieldname": "serial_and_batch_bundle", "fieldtype": "Link", "label": "Serial and Batch Bundle", "options": "Serial and Batch Bundle" + }, + { + "fieldname": "warehouse", + "fieldtype": "Link", + "in_list_view": 1, + "label": "Warehouse", + "options": "Warehouse", + "reqd": 1 } ], "index_web_pages_for_search": 1, "istable": 1, "links": [], - "modified": "2023-04-06 02:24:20.375870", + "modified": "2024-06-13 12:01:47.147333", "modified_by": "Administrator", "module": "Assets", "name": "Asset Repair Consumed Item", diff --git a/erpnext/assets/doctype/asset_repair_consumed_item/asset_repair_consumed_item.py b/erpnext/assets/doctype/asset_repair_consumed_item/asset_repair_consumed_item.py index ab43cfe62aa..4d41397a0b1 100644 --- a/erpnext/assets/doctype/asset_repair_consumed_item/asset_repair_consumed_item.py +++ b/erpnext/assets/doctype/asset_repair_consumed_item/asset_repair_consumed_item.py @@ -15,7 +15,7 @@ class AssetRepairConsumedItem(Document): from frappe.types import DF consumed_quantity: DF.Data | None - item_code: DF.Link | None + item_code: DF.Link parent: DF.Data parentfield: DF.Data parenttype: DF.Data @@ -23,6 +23,7 @@ class AssetRepairConsumedItem(Document): serial_no: DF.SmallText | None total_value: DF.Currency valuation_rate: DF.Currency + warehouse: DF.Link # end: auto-generated types pass diff --git a/erpnext/controllers/accounts_controller.py b/erpnext/controllers/accounts_controller.py index f576ecc1b11..0cbf1b320ca 100644 --- a/erpnext/controllers/accounts_controller.py +++ b/erpnext/controllers/accounts_controller.py @@ -1764,8 +1764,8 @@ class AccountsController(TransactionBase): item_allowance = {} global_qty_allowance, global_amount_allowance = None, None - role_allowed_to_over_bill = frappe.db.get_single_value( - "Accounts Settings", "role_allowed_to_over_bill" + role_allowed_to_over_bill = frappe.get_cached_value( + "Accounts Settings", None, "role_allowed_to_over_bill" ) user_roles = frappe.get_roles() diff --git a/erpnext/controllers/status_updater.py b/erpnext/controllers/status_updater.py index 896413aaed8..492000d71d4 100644 --- a/erpnext/controllers/status_updater.py +++ b/erpnext/controllers/status_updater.py @@ -554,6 +554,7 @@ class StatusUpdater(Document): ref_doc.set_status(update=True) +@frappe.request_cache def get_allowance_for( item_code, item_allowance=None, @@ -583,20 +584,20 @@ def get_allowance_for( global_amount_allowance, ) - qty_allowance, over_billing_allowance = frappe.db.get_value( + qty_allowance, over_billing_allowance = frappe.get_cached_value( "Item", item_code, ["over_delivery_receipt_allowance", "over_billing_allowance"] ) if qty_or_amount == "qty" and not qty_allowance: if global_qty_allowance is None: global_qty_allowance = flt( - frappe.db.get_single_value("Stock Settings", "over_delivery_receipt_allowance") + frappe.get_cached_value("Stock Settings", None, "over_delivery_receipt_allowance") ) qty_allowance = global_qty_allowance elif qty_or_amount == "amount" and not over_billing_allowance: if global_amount_allowance is None: global_amount_allowance = flt( - frappe.db.get_single_value("Accounts Settings", "over_billing_allowance") + frappe.get_cached_value("Accounts Settings", None, "over_billing_allowance") ) over_billing_allowance = global_amount_allowance diff --git a/erpnext/hooks.py b/erpnext/hooks.py index 64cbc297d20..a080af9a827 100644 --- a/erpnext/hooks.py +++ b/erpnext/hooks.py @@ -442,6 +442,7 @@ scheduler_events = { "erpnext.accounts.doctype.process_statement_of_accounts.process_statement_of_accounts.send_auto_email", "erpnext.accounts.utils.auto_create_exchange_rate_revaluation_daily", "erpnext.accounts.utils.run_ledger_health_checks", + "erpnext.assets.doctype.asset.asset_maintenance_log.update_asset_maintenance_log_status", ], "weekly": [ "erpnext.accounts.utils.auto_create_exchange_rate_revaluation_weekly", diff --git a/erpnext/manufacturing/doctype/bom_creator/bom_creator.js b/erpnext/manufacturing/doctype/bom_creator/bom_creator.js index 32231aa4949..34d0fc7131b 100644 --- a/erpnext/manufacturing/doctype/bom_creator/bom_creator.js +++ b/erpnext/manufacturing/doctype/bom_creator/bom_creator.js @@ -212,7 +212,6 @@ erpnext.bom.BomConfigurator = class BomConfigurator extends erpnext.TransactionC item.stock_qty = flt(item.qty * item.conversion_factor, precision("stock_qty", item)); refresh_field("stock_qty", item.name, item.parentfield); this.toggle_conversion_factor(item); - this.frm.events.update_cost(this.frm); } } }; diff --git a/erpnext/manufacturing/doctype/workstation/workstation.json b/erpnext/manufacturing/doctype/workstation/workstation.json index 5912714052b..4758e5d3588 100644 --- a/erpnext/manufacturing/doctype/workstation/workstation.json +++ b/erpnext/manufacturing/doctype/workstation/workstation.json @@ -17,8 +17,6 @@ "column_break_3", "production_capacity", "warehouse", - "production_capacity_section", - "parts_per_hour", "workstation_status_tab", "status", "column_break_glcv", @@ -210,16 +208,6 @@ "label": "Warehouse", "options": "Warehouse" }, - { - "fieldname": "production_capacity_section", - "fieldtype": "Section Break", - "label": "Production Capacity" - }, - { - "fieldname": "parts_per_hour", - "fieldtype": "Float", - "label": "Parts Per Hour" - }, { "fieldname": "total_working_hours", "fieldtype": "Float", @@ -252,7 +240,7 @@ "idx": 1, "image_field": "on_status_image", "links": [], - "modified": "2023-11-30 12:43:35.808845", + "modified": "2024-06-20 14:17:13.806609", "modified_by": "Administrator", "module": "Manufacturing", "name": "Workstation", @@ -277,4 +265,4 @@ "sort_order": "ASC", "states": [], "track_changes": 1 -} \ No newline at end of file +} diff --git a/erpnext/manufacturing/doctype/workstation/workstation.py b/erpnext/manufacturing/doctype/workstation/workstation.py index 0966d26b781..326a8b37efc 100644 --- a/erpnext/manufacturing/doctype/workstation/workstation.py +++ b/erpnext/manufacturing/doctype/workstation/workstation.py @@ -55,6 +55,9 @@ class Workstation(Document): hour_rate_electricity: DF.Currency hour_rate_labour: DF.Currency hour_rate_rent: DF.Currency + off_status_image: DF.AttachImage | None + on_status_image: DF.AttachImage | None + plant_floor: DF.Link | None production_capacity: DF.Int working_hours: DF.Table[WorkstationWorkingHour] workstation_name: DF.Data diff --git a/erpnext/patches.txt b/erpnext/patches.txt index dba491a728d..66e98dde5b4 100644 --- a/erpnext/patches.txt +++ b/erpnext/patches.txt @@ -366,4 +366,6 @@ erpnext.patches.v15_0.remove_cancelled_asset_capitalization_from_asset erpnext.patches.v15_0.rename_purchase_receipt_amount_to_purchase_amount erpnext.patches.v14_0.enable_set_priority_for_pricing_rules #1 erpnext.patches.v15_0.rename_number_of_depreciations_booked_to_opening_booked_depreciations +erpnext.patches.v15_0.update_warehouse_field_in_asset_repair_consumed_item_doctype +erpnext.patches.v15_0.update_asset_repair_field_in_stock_entry erpnext.patches.v15_0.update_total_number_of_booked_depreciations diff --git a/erpnext/patches/v15_0/update_asset_repair_field_in_stock_entry.py b/erpnext/patches/v15_0/update_asset_repair_field_in_stock_entry.py new file mode 100644 index 00000000000..cc0668d9caa --- /dev/null +++ b/erpnext/patches/v15_0/update_asset_repair_field_in_stock_entry.py @@ -0,0 +1,15 @@ +import frappe +from frappe.query_builder import DocType + + +def execute(): + if frappe.db.has_column("Asset Repair", "stock_entry"): + AssetRepair = DocType("Asset Repair") + StockEntry = DocType("Stock Entry") + + ( + frappe.qb.update(StockEntry) + .join(AssetRepair) + .on(StockEntry.name == AssetRepair.stock_entry) + .set(StockEntry.asset_repair, AssetRepair.name) + ).run() diff --git a/erpnext/patches/v15_0/update_total_number_of_booked_depreciations.py b/erpnext/patches/v15_0/update_total_number_of_booked_depreciations.py index 4b556c2b512..82f1c88903d 100644 --- a/erpnext/patches/v15_0/update_total_number_of_booked_depreciations.py +++ b/erpnext/patches/v15_0/update_total_number_of_booked_depreciations.py @@ -18,9 +18,10 @@ def execute(): depr_schedule = get_depr_schedule(asset.name, "Active", fb_row.finance_book) total_number_of_booked_depreciations = asset.opening_number_of_booked_depreciations or 0 - for je in depr_schedule: - if je.journal_entry: - total_number_of_booked_depreciations += 1 + if depr_schedule: + for je in depr_schedule: + if je.journal_entry: + total_number_of_booked_depreciations += 1 frappe.db.set_value( "Asset Finance Book", fb_row.name, diff --git a/erpnext/patches/v15_0/update_warehouse_field_in_asset_repair_consumed_item_doctype.py b/erpnext/patches/v15_0/update_warehouse_field_in_asset_repair_consumed_item_doctype.py new file mode 100644 index 00000000000..e0291826930 --- /dev/null +++ b/erpnext/patches/v15_0/update_warehouse_field_in_asset_repair_consumed_item_doctype.py @@ -0,0 +1,14 @@ +import frappe + + +# not able to use frappe.qb because of this bug https://github.com/frappe/frappe/issues/20292 +def execute(): + if frappe.db.has_column("Asset Repair", "warehouse"): + # nosemgrep + frappe.db.sql( + """UPDATE `tabAsset Repair Consumed Item` ar_item + JOIN `tabAsset Repair` ar + ON ar.name = ar_item.parent + SET ar_item.warehouse = ar.warehouse + WHERE ifnull(ar.warehouse, '') != ''""" + ) diff --git a/erpnext/selling/doctype/sales_order/sales_order.py b/erpnext/selling/doctype/sales_order/sales_order.py index af67f07a360..a00b08ab097 100755 --- a/erpnext/selling/doctype/sales_order/sales_order.py +++ b/erpnext/selling/doctype/sales_order/sales_order.py @@ -496,6 +496,10 @@ class SalesOrder(SellingController): def update_status(self, status): self.check_modified_date() self.set_status(update=True, status=status) + # Upon Sales Order Re-open, check for credit limit. + # Limit should be checked after the 'Hold/Closed' status is reset. + if status == "Draft" and self.docstatus == 1: + self.check_credit_limit() self.update_reserved_qty() self.notify_update() clear_doctype_notifications(self) diff --git a/erpnext/selling/doctype/sales_order/test_sales_order.py b/erpnext/selling/doctype/sales_order/test_sales_order.py index bd51543b36e..53c629a90b4 100644 --- a/erpnext/selling/doctype/sales_order/test_sales_order.py +++ b/erpnext/selling/doctype/sales_order/test_sales_order.py @@ -9,6 +9,7 @@ from frappe.core.doctype.user_permission.test_user_permission import create_user from frappe.tests.utils import FrappeTestCase, change_settings from frappe.utils import add_days, flt, getdate, nowdate, today +from erpnext.accounts.test.accounts_mixin import AccountsTestMixin from erpnext.controllers.accounts_controller import update_child_qty_rate from erpnext.maintenance.doctype.maintenance_schedule.test_maintenance_schedule import ( make_maintenance_schedule, @@ -31,7 +32,7 @@ from erpnext.stock.doctype.item.test_item import make_item from erpnext.stock.doctype.stock_entry.stock_entry_utils import make_stock_entry -class TestSalesOrder(FrappeTestCase): +class TestSalesOrder(AccountsTestMixin, FrappeTestCase): @classmethod def setUpClass(cls): super().setUpClass() @@ -49,6 +50,9 @@ class TestSalesOrder(FrappeTestCase): ) super().tearDownClass() + def setUp(self): + self.create_customer("_Test Customer Credit") + def tearDown(self): frappe.set_user("Administrator") @@ -2086,6 +2090,28 @@ class TestSalesOrder(FrappeTestCase): frappe.db.set_single_value("Stock Settings", "update_existing_price_list_rate", 0) frappe.db.set_single_value("Stock Settings", "auto_insert_price_list_rate_if_missing", 0) + def test_credit_limit_on_so_reopning(self): + # set credit limit + company = "_Test Company" + customer = frappe.get_doc("Customer", self.customer) + customer.credit_limits = [] + customer.append( + "credit_limits", {"company": company, "credit_limit": 1000, "bypass_credit_limit_check": False} + ) + customer.save() + + so1 = make_sales_order(qty=9, rate=100, do_not_submit=True) + so1.customer = self.customer + so1.save().submit() + + so1.update_status("Closed") + + so2 = make_sales_order(qty=9, rate=100, do_not_submit=True) + so2.customer = self.customer + so2.save().submit() + + self.assertRaises(frappe.ValidationError, so1.update_status, "Draft") + def automatically_fetch_payment_terms(enable=1): accounts_settings = frappe.get_doc("Accounts Settings") diff --git a/erpnext/stock/doctype/item_price/item_price.json b/erpnext/stock/doctype/item_price/item_price.json index 3daf4dc2bd8..c6950b9a10f 100644 --- a/erpnext/stock/doctype/item_price/item_price.json +++ b/erpnext/stock/doctype/item_price/item_price.json @@ -107,7 +107,8 @@ "in_standard_filter": 1, "label": "Price List", "options": "Price List", - "reqd": 1 + "reqd": 1, + "search_index": 1 }, { "bold": 1, diff --git a/erpnext/stock/doctype/pick_list/pick_list.py b/erpnext/stock/doctype/pick_list/pick_list.py index 69a1bdf17d8..6dd53f290c9 100644 --- a/erpnext/stock/doctype/pick_list/pick_list.py +++ b/erpnext/stock/doctype/pick_list/pick_list.py @@ -963,6 +963,7 @@ def get_available_item_locations_for_batched_item( { "item_code": item_code, "warehouse": from_warehouses, + "based_on": frappe.db.get_single_value("Stock Settings", "pick_serial_and_batch_based_on"), } ) ) diff --git a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py index 6a00fed5588..a8cb7fd83b4 100644 --- a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py +++ b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py @@ -661,7 +661,7 @@ class PurchaseReceipt(BuyingController): if not ( (erpnext.is_perpetual_inventory_enabled(self.company) and d.item_code in stock_items) - or d.is_fixed_asset + or (d.is_fixed_asset and not d.purchase_invoice) ): continue @@ -776,6 +776,13 @@ class PurchaseReceipt(BuyingController): posting_date=posting_date, ) + def is_landed_cost_booked_for_any_item(self) -> bool: + for x in self.items: + if x.landed_cost_voucher_amount != 0: + return True + + return False + def make_tax_gl_entries(self, gl_entries, via_landed_cost_voucher=False): negative_expense_to_be_booked = sum([flt(d.item_tax_amount) for d in self.get("items")]) is_asset_pr = any(d.is_fixed_asset for d in self.get("items")) @@ -811,7 +818,7 @@ class PurchaseReceipt(BuyingController): i = 1 for tax in self.get("taxes"): if valuation_tax.get(tax.name): - if via_landed_cost_voucher: + if via_landed_cost_voucher or self.is_landed_cost_booked_for_any_item(): account = tax.account_head else: negative_expense_booked_in_pi = frappe.db.sql( diff --git a/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py b/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py index 16553749d0a..968cb68dac0 100644 --- a/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py +++ b/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py @@ -3020,6 +3020,156 @@ class TestPurchaseReceipt(FrappeTestCase): self.assertEqual(pr_return.items[0].rejected_qty, 0.0) self.assertEqual(pr_return.items[0].rejected_warehouse, "") + def test_tax_account_heads_on_lcv_and_item_repost(self): + """ + PO -> PR -> PI + PR -> LCV + Backdated `Repost Item valuation` should not merge tax account heads into stock_rbnb + """ + from erpnext.accounts.doctype.account.test_account import create_account + from erpnext.buying.doctype.purchase_order.test_purchase_order import ( + create_purchase_order, + make_pr_against_po, + ) + from erpnext.stock.doctype.purchase_receipt.purchase_receipt import make_purchase_invoice + + stock_rbnb = "Stock Received But Not Billed - _TC" + stock_in_hand = "Stock In Hand - _TC" + test_cc = "_Test Cost Center - _TC" + test_company = "_Test Company" + creditors = "Creditors - _TC" + lcv_expense_account = "Expenses Included In Valuation - _TC" + + company_doc = frappe.get_doc("Company", test_company) + company_doc.enable_perpetual_inventory = True + company_doc.stock_received_but_not_billed = stock_rbnb + company_doc.default_inventory_account = stock_in_hand + company_doc.save() + + packaging_charges_account = create_account( + account_name="Packaging Charges", + parent_account="Indirect Expenses - _TC", + company=test_company, + account_type="Tax", + ) + + po = create_purchase_order(qty=10, rate=100, do_not_save=1) + po.taxes = [] + po.append( + "taxes", + { + "category": "Valuation and Total", + "account_head": packaging_charges_account, + "cost_center": test_cc, + "description": "Test", + "add_deduct_tax": "Add", + "charge_type": "Actual", + "tax_amount": 250, + }, + ) + po.save().submit() + + pr = make_pr_against_po(po.name, received_qty=10) + pr_gl_entries = get_gl_entries(pr.doctype, pr.name, skip_cancelled=True) + expected_pr_gles = [ + {"account": stock_rbnb, "debit": 0.0, "credit": 1000.0, "cost_center": test_cc}, + {"account": stock_in_hand, "debit": 1250.0, "credit": 0.0, "cost_center": test_cc}, + {"account": packaging_charges_account, "debit": 0.0, "credit": 250.0, "cost_center": test_cc}, + ] + self.assertEqual(expected_pr_gles, pr_gl_entries) + + # Make PI against Purchase Receipt + pi = make_purchase_invoice(pr.name).save().submit() + pi_gl_entries = get_gl_entries(pi.doctype, pi.name, skip_cancelled=True) + expected_pi_gles = [ + {"account": stock_rbnb, "debit": 1000.0, "credit": 0.0, "cost_center": test_cc}, + {"account": packaging_charges_account, "debit": 250.0, "credit": 0.0, "cost_center": test_cc}, + {"account": creditors, "debit": 0.0, "credit": 1250.0, "cost_center": None}, + ] + self.assertEqual(expected_pi_gles, pi_gl_entries) + + lcv = self.create_lcv(pr.doctype, pr.name, test_company, lcv_expense_account) + pr_gles_after_lcv = get_gl_entries(pr.doctype, pr.name, skip_cancelled=True) + expected_pr_gles_after_lcv = [ + {"account": stock_rbnb, "debit": 0.0, "credit": 1000.0, "cost_center": test_cc}, + {"account": stock_in_hand, "debit": 1300.0, "credit": 0.0, "cost_center": test_cc}, + {"account": packaging_charges_account, "debit": 0.0, "credit": 250.0, "cost_center": test_cc}, + {"account": lcv_expense_account, "debit": 0.0, "credit": 50.0, "cost_center": test_cc}, + ] + self.assertEqual(expected_pr_gles_after_lcv, pr_gles_after_lcv) + + # Trigger Repost Item Valudation on a older date + repost_doc = frappe.get_doc( + { + "doctype": "Repost Item Valuation", + "based_on": "Item and Warehouse", + "item_code": pr.items[0].item_code, + "warehouse": pr.items[0].warehouse, + "posting_date": add_days(pr.posting_date, -1), + "posting_time": "00:00:00", + "company": pr.company, + "allow_negative_stock": 1, + "via_landed_cost_voucher": 0, + "allow_zero_rate": 0, + } + ) + repost_doc.save().submit() + + pr_gles_after_repost = get_gl_entries(pr.doctype, pr.name, skip_cancelled=True) + expected_pr_gles_after_repost = [ + {"account": stock_rbnb, "debit": 0.0, "credit": 1000.0, "cost_center": test_cc}, + {"account": stock_in_hand, "debit": 1300.0, "credit": 0.0, "cost_center": test_cc}, + {"account": packaging_charges_account, "debit": 0.0, "credit": 250.0, "cost_center": test_cc}, + {"account": lcv_expense_account, "debit": 0.0, "credit": 50.0, "cost_center": test_cc}, + ] + self.assertEqual(len(pr_gles_after_repost), len(expected_pr_gles_after_repost)) + self.assertEqual(expected_pr_gles_after_repost, pr_gles_after_repost) + + # teardown + lcv.reload() + lcv.cancel() + pi.reload() + pi.cancel() + pr.reload() + pr.cancel() + + company_doc.enable_perpetual_inventory = False + company_doc.stock_received_but_not_billed = None + company_doc.default_inventory_account = None + company_doc.save() + + def create_lcv(self, receipt_document_type, receipt_document, company, expense_account, charges=50): + ref_doc = frappe.get_doc(receipt_document_type, receipt_document) + + lcv = frappe.new_doc("Landed Cost Voucher") + lcv.company = company + lcv.distribute_charges_based_on = "Qty" + lcv.set( + "purchase_receipts", + [ + { + "receipt_document_type": receipt_document_type, + "receipt_document": receipt_document, + "supplier": ref_doc.supplier, + "posting_date": ref_doc.posting_date, + "grand_total": ref_doc.base_grand_total, + } + ], + ) + + lcv.set( + "taxes", + [ + { + "description": "Testing", + "expense_account": expense_account, + "amount": charges, + } + ], + ) + lcv.save().submit() + return lcv + def prepare_data_for_internal_transfer(): from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_internal_supplier diff --git a/erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.js b/erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.js index f0506ab6930..6f2548ac8ea 100644 --- a/erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.js +++ b/erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.js @@ -128,9 +128,7 @@ frappe.ui.form.on("Repost Item Valuation", { method: "restart_reposting", doc: frm.doc, callback: function (r) { - if (!r.exc) { - frm.refresh(); - } + frm.reload_doc(); }, }); }, diff --git a/erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.json b/erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.json index 1c5b521c296..67e97964e18 100644 --- a/erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.json +++ b/erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.json @@ -218,13 +218,14 @@ "fieldname": "reposting_data_file", "fieldtype": "Attach", "label": "Reposting Data File", + "no_copy": 1, "read_only": 1 } ], "index_web_pages_for_search": 1, "is_submittable": 1, "links": [], - "modified": "2023-05-31 12:48:57.138693", + "modified": "2024-06-27 16:55:23.150146", "modified_by": "Administrator", "module": "Stock", "name": "Repost Item Valuation", @@ -276,4 +277,4 @@ "sort_field": "modified", "sort_order": "DESC", "states": [] -} \ No newline at end of file +} 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 40767704f4e..717b1c31026 100644 --- a/erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.py +++ b/erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.py @@ -179,7 +179,7 @@ class RepostItemValuation(Document): def clear_attachment(self): if attachments := get_attachments(self.doctype, self.name): attachment = attachments[0] - frappe.delete_doc("File", attachment.name) + frappe.delete_doc("File", attachment.name, ignore_permissions=True) if self.reposting_data_file: self.db_set("reposting_data_file", None) @@ -219,6 +219,7 @@ class RepostItemValuation(Document): self.distinct_item_and_warehouse = None self.items_to_be_repost = None self.gl_reposting_index = 0 + self.clear_attachment() self.db_update() def deduplicate_similar_repost(self): @@ -271,6 +272,7 @@ def repost(doc): repost_gl_entries(doc) doc.set_status("Completed") + doc.db_set("reposting_data_file", None) remove_attached_file(doc.name) except Exception as e: @@ -315,7 +317,7 @@ def remove_attached_file(docname): if file_name := frappe.db.get_value( "File", {"attached_to_name": docname, "attached_to_doctype": "Repost Item Valuation"}, "name" ): - frappe.delete_doc("File", file_name, delete_permanently=True) + frappe.delete_doc("File", file_name, ignore_permissions=True, delete_permanently=True) def repost_sl_entries(doc): diff --git a/erpnext/stock/doctype/serial_and_batch_bundle/test_serial_and_batch_bundle.py b/erpnext/stock/doctype/serial_and_batch_bundle/test_serial_and_batch_bundle.py index c313917bd4c..2913af4a724 100644 --- a/erpnext/stock/doctype/serial_and_batch_bundle/test_serial_and_batch_bundle.py +++ b/erpnext/stock/doctype/serial_and_batch_bundle/test_serial_and_batch_bundle.py @@ -646,6 +646,61 @@ class TestSerialandBatchBundle(FrappeTestCase): self.assertEqual(flt(stock_value_difference, 2), 353.0 * -1) + def test_pick_serial_nos_for_batch_item(self): + item_code = make_item( + "Test Pick Serial Nos for Batch Item 1", + properties={ + "is_stock_item": 1, + "has_batch_no": 1, + "create_new_batch": 1, + "batch_no_series": "PSNBI-TSNVL-.#####", + "has_serial_no": 1, + "serial_no_series": "SN-PSNBI-TSNVL-.#####", + }, + ).name + + se = make_stock_entry( + item_code=item_code, + qty=10, + target="_Test Warehouse - _TC", + rate=500, + ) + + batch1 = get_batch_from_bundle(se.items[0].serial_and_batch_bundle) + serial_nos1 = get_serial_nos_from_bundle(se.items[0].serial_and_batch_bundle) + + se = make_stock_entry( + item_code=item_code, + qty=10, + target="_Test Warehouse - _TC", + rate=500, + ) + + batch2 = get_batch_from_bundle(se.items[0].serial_and_batch_bundle) + serial_nos2 = get_serial_nos_from_bundle(se.items[0].serial_and_batch_bundle) + + se = make_stock_entry( + item_code=item_code, + qty=10, + source="_Test Warehouse - _TC", + use_serial_batch_fields=True, + batch_no=batch2, + ) + + serial_nos = get_serial_nos_from_bundle(se.items[0].serial_and_batch_bundle) + self.assertEqual(serial_nos, serial_nos2) + + se = make_stock_entry( + item_code=item_code, + qty=10, + source="_Test Warehouse - _TC", + use_serial_batch_fields=True, + batch_no=batch1, + ) + + serial_nos = get_serial_nos_from_bundle(se.items[0].serial_and_batch_bundle) + self.assertEqual(serial_nos, serial_nos1) + def get_batch_from_bundle(bundle): from erpnext.stock.serial_batch_bundle import get_batch_nos diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.json b/erpnext/stock/doctype/stock_entry/stock_entry.json index d45296f1310..a090b37033b 100644 --- a/erpnext/stock/doctype/stock_entry/stock_entry.json +++ b/erpnext/stock/doctype/stock_entry/stock_entry.json @@ -20,6 +20,7 @@ "sales_invoice_no", "pick_list", "purchase_receipt_no", + "asset_repair", "col2", "company", "posting_date", @@ -674,6 +675,14 @@ { "fieldname": "column_break_eaoa", "fieldtype": "Column Break" + }, + { + "depends_on": "eval:doc.asset_repair", + "fieldname": "asset_repair", + "fieldtype": "Link", + "label": "Asset Repair", + "options": "Asset Repair", + "read_only": 1 } ], "icon": "fa fa-file-text", @@ -681,7 +690,7 @@ "index_web_pages_for_search": 1, "is_submittable": 1, "links": [], - "modified": "2024-01-12 11:56:58.644882", + "modified": "2024-06-26 19:12:17.937088", "modified_by": "Administrator", "module": "Stock", "name": "Stock Entry", diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.py b/erpnext/stock/doctype/stock_entry/stock_entry.py index 005ad287d78..f08ae6c286c 100644 --- a/erpnext/stock/doctype/stock_entry/stock_entry.py +++ b/erpnext/stock/doctype/stock_entry/stock_entry.py @@ -98,6 +98,7 @@ class StockEntry(StockController): address_display: DF.SmallText | None amended_from: DF.Link | None apply_putaway_rule: DF.Check + asset_repair: DF.Link | None bom_no: DF.Link | None company: DF.Link credit_note: DF.Link | None diff --git a/erpnext/stock/doctype/stock_ledger_entry/stock_ledger_entry.json b/erpnext/stock/doctype/stock_ledger_entry/stock_ledger_entry.json index e8e82af25ac..58b6e4a74b6 100644 --- a/erpnext/stock/doctype/stock_ledger_entry/stock_ledger_entry.json +++ b/erpnext/stock/doctype/stock_ledger_entry/stock_ledger_entry.json @@ -101,6 +101,7 @@ "oldfieldtype": "Date", "print_width": "100px", "read_only": 1, + "search_index": 1, "width": "100px" }, { @@ -360,7 +361,7 @@ "in_create": 1, "index_web_pages_for_search": 1, "links": [], - "modified": "2024-03-13 09:56:13.021696", + "modified": "2024-06-27 16:23:18.820049", "modified_by": "Administrator", "module": "Stock", "name": "Stock Ledger Entry", @@ -384,4 +385,4 @@ "sort_field": "modified", "sort_order": "DESC", "states": [] -} \ No newline at end of file +} diff --git a/erpnext/stock/get_item_details.py b/erpnext/stock/get_item_details.py index 5da3c066869..a7aa7dce452 100644 --- a/erpnext/stock/get_item_details.py +++ b/erpnext/stock/get_item_details.py @@ -103,19 +103,8 @@ def get_item_details(args, doc=None, for_validate=False, overwrite_warehouse=Tru if args.customer and cint(args.is_pos): out.update(get_pos_profile_item_details(args.company, args, update_data=True)) - if args.get("doctype") == "Material Request" and args.get("material_request_type") == "Material Transfer": - out.update(get_bin_details(args.item_code, args.get("from_warehouse"))) - - elif out.get("warehouse"): - if doc and doc.get("doctype") == "Purchase Order": - # calculate company_total_stock only for po - bin_details = get_bin_details( - args.item_code, out.warehouse, args.company, include_child_warehouses=True - ) - else: - bin_details = get_bin_details(args.item_code, out.warehouse, include_child_warehouses=True) - - out.update(bin_details) + if item.is_stock_item: + update_bin_details(args, out, doc) # update args with out, if key or value not exists for key, value in out.items(): @@ -166,6 +155,19 @@ def set_valuation_rate(out, args): out.update(get_valuation_rate(args.item_code, args.company, out.get("warehouse"))) +def update_bin_details(args, out, doc): + if args.get("doctype") == "Material Request" and args.get("material_request_type") == "Material Transfer": + out.update(get_bin_details(args.item_code, args.get("from_warehouse"))) + + elif out.get("warehouse"): + company = args.company if (doc and doc.get("doctype") == "Purchase Order") else None + + # calculate company_total_stock only for po + bin_details = get_bin_details(args.item_code, out.warehouse, company, include_child_warehouses=True) + + out.update(bin_details) + + def process_args(args): if isinstance(args, str): args = json.loads(args) diff --git a/erpnext/stock/report/stock_balance/stock_balance.js b/erpnext/stock/report/stock_balance/stock_balance.js index ca2c053fdb1..d80261895aa 100644 --- a/erpnext/stock/report/stock_balance/stock_balance.js +++ b/erpnext/stock/report/stock_balance/stock_balance.js @@ -101,6 +101,12 @@ frappe.query_reports["Stock Balance"] = { fieldtype: "Check", default: 0, }, + { + fieldname: "include_zero_stock_items", + label: __("Include Zero Stock Items"), + fieldtype: "Check", + default: 0, + }, ], formatter: function (value, row, column, data, default_formatter) { diff --git a/erpnext/stock/report/stock_balance/stock_balance.py b/erpnext/stock/report/stock_balance/stock_balance.py index 27d9f1164bc..2694ba03c8b 100644 --- a/erpnext/stock/report/stock_balance/stock_balance.py +++ b/erpnext/stock/report/stock_balance/stock_balance.py @@ -138,7 +138,12 @@ class StockBalanceReport: {"reserved_stock": sre_details.get((report_data.item_code, report_data.warehouse), 0.0)} ) - if report_data and report_data.bal_qty == 0 and report_data.bal_val == 0: + if ( + not self.filters.get("include_zero_stock_items") + and report_data + and report_data.bal_qty == 0 + and report_data.bal_val == 0 + ): continue self.data.append(report_data) diff --git a/erpnext/stock/serial_batch_bundle.py b/erpnext/stock/serial_batch_bundle.py index 1fa5665c141..c5346d2b0a3 100644 --- a/erpnext/stock/serial_batch_bundle.py +++ b/erpnext/stock/serial_batch_bundle.py @@ -950,7 +950,17 @@ class SerialBatchCreation: if self.get("ignore_serial_nos"): kwargs["ignore_serial_nos"] = self.ignore_serial_nos - if self.has_serial_no and not self.get("serial_nos"): + if ( + self.has_serial_no + and self.has_batch_no + and not self.get("serial_nos") + and self.get("batches") + and len(self.get("batches")) == 1 + ): + # If only one batch is available and no serial no is available + kwargs["batches"] = next(iter(self.get("batches").keys())) + self.serial_nos = get_serial_nos_for_outward(kwargs) + elif self.has_serial_no and not self.get("serial_nos"): self.serial_nos = get_serial_nos_for_outward(kwargs) elif not self.has_serial_no and self.has_batch_no and not self.get("batches"): self.batches = get_available_batches(kwargs) diff --git a/erpnext/stock/utils.py b/erpnext/stock/utils.py index f2388e5737a..4c502e106ec 100644 --- a/erpnext/stock/utils.py +++ b/erpnext/stock/utils.py @@ -68,8 +68,6 @@ def get_stock_value_on( frappe.qb.from_(sle) .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) ) if warehouses: diff --git a/erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.py b/erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.py index 5e717e1f22a..48203167187 100644 --- a/erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.py +++ b/erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.py @@ -426,6 +426,12 @@ class SubcontractingReceipt(SubcontractingController): ) def validate_available_qty_for_consumption(self): + if ( + frappe.db.get_single_value("Buying Settings", "backflush_raw_materials_of_subcontract_based_on") + == "BOM" + ): + return + for item in self.get("supplied_items"): precision = item.precision("consumed_qty") if ( diff --git a/erpnext/subcontracting/doctype/subcontracting_receipt/test_subcontracting_receipt.py b/erpnext/subcontracting/doctype/subcontracting_receipt/test_subcontracting_receipt.py index 0f5fe7ab958..8ff5c8f27b0 100644 --- a/erpnext/subcontracting/doctype/subcontracting_receipt/test_subcontracting_receipt.py +++ b/erpnext/subcontracting/doctype/subcontracting_receipt/test_subcontracting_receipt.py @@ -81,6 +81,7 @@ class TestSubcontractingReceipt(FrappeTestCase): self.assertEqual(scr.get("items")[0].rm_supp_cost, flt(rm_supp_cost)) def test_available_qty_for_consumption(self): + set_backflush_based_on("BOM") make_stock_entry(item_code="_Test Item", qty=100, target="_Test Warehouse 1 - _TC", basic_rate=100) make_stock_entry( item_code="_Test Item Home Desktop 100", @@ -125,7 +126,7 @@ class TestSubcontractingReceipt(FrappeTestCase): ) scr = make_subcontracting_receipt(sco.name) scr.save() - self.assertRaises(frappe.ValidationError, scr.submit) + scr.submit() def test_subcontracting_gle_fg_item_rate_zero(self): from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import get_gl_entries @@ -476,6 +477,21 @@ class TestSubcontractingReceipt(FrappeTestCase): # consumed_qty should be (accepted_qty * qty_consumed_per_unit) = (6 * 1) = 6 self.assertEqual(scr.supplied_items[0].consumed_qty, 6) + # Do not transfer materials to the supplier warehouse and check whether system allows to consumed directly from the supplier's warehouse + sco = get_subcontracting_order(service_items=service_items) + + # Transfer RM's + rm_items = get_rm_items(sco.supplied_items) + itemwise_details = make_stock_in_entry(rm_items=rm_items, warehouse="_Test Warehouse 1 - _TC") + + # Create Subcontracting Receipt + scr = make_subcontracting_receipt(sco.name) + scr.submit() + self.assertEqual(scr.docstatus, 1) + + for item in scr.supplied_items: + self.assertFalse(item.available_qty_for_consumption) + def test_supplied_items_cost_after_reposting(self): # Set Backflush Based On as "BOM" set_backflush_based_on("BOM")