diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py index 60ef027280e..c423893ddc4 100644 --- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py +++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py @@ -1024,6 +1024,10 @@ class PurchaseInvoice(BuyingController): gl_entries.append(self.get_gl_dict(gl, self.party_account_currency, item=self)) def make_item_gl_entries(self, gl_entries): + from erpnext.stock.doctype.landed_cost_voucher.landed_cost_voucher import ( + get_custom_dimension_overrides, + ) + # item gl entries stock_items = self.get_stock_items() if self.update_stock and self.auto_accounting_for_stock: @@ -1165,25 +1169,34 @@ class PurchaseInvoice(BuyingController): # Amount added through landed-cost-voucher if landed_cost_entries: - if (item.item_code, item.name) in landed_cost_entries: - for account, base_amount in landed_cost_entries[ - (item.item_code, item.name) - ].items(): - gl_entries.append( - self.get_gl_dict( - { - "account": account, - "against": item.expense_account, - "cost_center": item.cost_center, - "remarks": self.get("remarks") or _("Accounting Entry for Stock"), - "credit": flt(base_amount["base_amount"]), - "credit_in_account_currency": flt(base_amount["amount"]), - "credit_in_transaction_currency": item.net_amount, - "project": item.project or self.project, - }, - item=item, - ) + for entry in landed_cost_entries.get((item.item_code, item.name), []): + if not (entry.amount or entry.base_amount): + continue + + lcv_account_currency = get_account_currency(entry.expense_account) + credit_in_transaction_currency = ( + flt(entry.amount) + if lcv_account_currency == self.currency + else flt( + entry.base_amount / self.conversion_rate, item.precision("net_amount") ) + ) + + gl_dict = self.get_gl_dict( + { + "account": entry.expense_account, + "against": item.expense_account, + "cost_center": entry.dimensions.cost_center or item.cost_center, + "remarks": self.get("remarks") or _("Accounting Entry for Stock"), + "credit": flt(entry.base_amount), + "credit_in_account_currency": flt(entry.amount), + "credit_in_transaction_currency": credit_in_transaction_currency, + "project": entry.dimensions.project or item.project or self.project, + }, + item=item, + ) + gl_dict.update(get_custom_dimension_overrides(entry)) + gl_entries.append(gl_dict) # sub-contracting warehouse if flt(item.rm_supp_cost): diff --git a/erpnext/controllers/stock_controller.py b/erpnext/controllers/stock_controller.py index dd0d7f6dae6..54864b8f8d9 100644 --- a/erpnext/controllers/stock_controller.py +++ b/erpnext/controllers/stock_controller.py @@ -1246,7 +1246,13 @@ class StockController(AccountsController): if not landed_cost_vouchers: return + from erpnext.stock.doctype.landed_cost_voucher.landed_cost_voucher import ( + get_lcv_dimension_fields, + get_row_dimensions, + ) + item_account_wise_cost = {} + dimension_fields = get_lcv_dimension_fields() row_fieldname = "purchase_receipt_item" if self.doctype == "Stock Entry": @@ -1268,28 +1274,36 @@ class StockController(AccountsController): for item in landed_cost_voucher_doc.items: if item.receipt_document == self.name: + charges = item_account_wise_cost.setdefault((item.item_code, item.get(row_fieldname)), {}) + for account in landed_cost_voucher_doc.taxes: exchange_rate = account.exchange_rate or 1 - item_account_wise_cost.setdefault((item.item_code, item.get(row_fieldname)), {}) - item_account_wise_cost[(item.item_code, item.get(row_fieldname))].setdefault( - account.expense_account, {"amount": 0.0, "base_amount": 0.0} + dimensions = get_row_dimensions(account, item, dimension_fields) + group_key = ( + account.expense_account, + tuple(dimensions.get(field) for field in dimension_fields), ) - item_row = item_account_wise_cost[(item.item_code, item.get(row_fieldname))][ - account.expense_account - ] + item_row = charges.get(group_key) + if item_row is None: + item_row = charges[group_key] = frappe._dict( + expense_account=account.expense_account, + amount=0.0, + base_amount=0.0, + dimensions=dimensions, + ) if total_item_cost > 0: - item_row["amount"] += account.amount * item.get(based_on_field) / total_item_cost + item_row.amount += account.amount * item.get(based_on_field) / total_item_cost - item_row["base_amount"] += ( + item_row.base_amount += ( account.base_amount * item.get(based_on_field) / total_item_cost ) else: - item_row["amount"] += item.applicable_charges / exchange_rate - item_row["base_amount"] += item.applicable_charges + item_row.amount += item.applicable_charges / exchange_rate + item_row.base_amount += item.applicable_charges - return item_account_wise_cost + return {key: list(charges.values()) for key, charges in item_account_wise_cost.items()} def validate_inventory_dimension_mandatory(self): # Mandatory inventory dimensions are enforced here (instead of via field-level `reqd`) @@ -1976,6 +1990,7 @@ class StockController(AccountsController): voucher_detail_no=None, item=None, posting_date=None, + dimensions=None, ): gl_entry = { "account": account, @@ -2001,6 +2016,9 @@ class StockController(AccountsController): if posting_date: gl_entry.update({"posting_date": posting_date}) + if dimensions: + gl_entry.update(dimensions) + gl_entries.append(self.get_gl_dict(gl_entry, item=item)) def update_stock_reservation_entries(self): diff --git a/erpnext/hooks.py b/erpnext/hooks.py index a4fc6766108..e3d03caf638 100644 --- a/erpnext/hooks.py +++ b/erpnext/hooks.py @@ -559,6 +559,7 @@ accounting_dimension_doctypes = [ "Purchase Taxes and Charges", "Shipping Rule", "Landed Cost Item", + "Landed Cost Taxes and Charges", "Asset Value Adjustment", "Asset Repair", "Asset Capitalization", diff --git a/erpnext/patches.txt b/erpnext/patches.txt index 16ddb87fac4..68cdf2dca17 100644 --- a/erpnext/patches.txt +++ b/erpnext/patches.txt @@ -490,6 +490,7 @@ erpnext.patches.v15_0.backfill_sla_link_filters_on_custom_field erpnext.patches.v15_0.backfill_sla_link_filters_on_docfield erpnext.patches.v16_0.crm_settings_handle_allowed_users_for_frappe_crm erpnext.patches.v16_0.backfill_pick_list_transferred_qty +erpnext.patches.v16_0.create_accounting_dimensions_in_landed_cost_taxes_and_charges erpnext.patches.v16_0.access_control_for_project_users erpnext.patches.v16_0.enable_book_stock_expense_gl_entries erpnext.patches.v16_0.rename_ar_ap_ageing_filter diff --git a/erpnext/patches/v16_0/create_accounting_dimensions_in_landed_cost_taxes_and_charges.py b/erpnext/patches/v16_0/create_accounting_dimensions_in_landed_cost_taxes_and_charges.py new file mode 100644 index 00000000000..4fa19fac744 --- /dev/null +++ b/erpnext/patches/v16_0/create_accounting_dimensions_in_landed_cost_taxes_and_charges.py @@ -0,0 +1,11 @@ +from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import ( + get_dimensions, + make_dimension_in_accounting_doctypes, +) + + +def execute(): + dimensions_and_defaults = get_dimensions() + if dimensions_and_defaults: + for dimension in dimensions_and_defaults[0]: + make_dimension_in_accounting_doctypes(dimension, ["Landed Cost Taxes and Charges"]) diff --git a/erpnext/stock/doctype/landed_cost_taxes_and_charges/landed_cost_taxes_and_charges.json b/erpnext/stock/doctype/landed_cost_taxes_and_charges/landed_cost_taxes_and_charges.json index 6d638b6e59b..5e13fec040e 100644 --- a/erpnext/stock/doctype/landed_cost_taxes_and_charges/landed_cost_taxes_and_charges.json +++ b/erpnext/stock/doctype/landed_cost_taxes_and_charges/landed_cost_taxes_and_charges.json @@ -17,7 +17,11 @@ "has_operating_cost", "operation_id", "qty", - "operating_component" + "operating_component", + "accounting_dimensions_section", + "cost_center", + "dimension_col_break", + "project" ], "fields": [ { @@ -107,13 +111,34 @@ "label": "Operating Component", "no_copy": 1, "read_only": 1 + }, + { + "fieldname": "accounting_dimensions_section", + "fieldtype": "Section Break", + "label": "Accounting Dimensions" + }, + { + "fieldname": "cost_center", + "fieldtype": "Link", + "label": "Cost Center", + "options": "Cost Center" + }, + { + "fieldname": "dimension_col_break", + "fieldtype": "Column Break" + }, + { + "fieldname": "project", + "fieldtype": "Link", + "label": "Project", + "options": "Project" } ], "grid_page_length": 50, "index_web_pages_for_search": 1, "istable": 1, "links": [], - "modified": "2026-05-19 12:21:07.953801", + "modified": "2026-08-04 10:00:00.000000", "modified_by": "Administrator", "module": "Stock", "name": "Landed Cost Taxes and Charges", diff --git a/erpnext/stock/doctype/landed_cost_voucher/landed_cost_voucher.py b/erpnext/stock/doctype/landed_cost_voucher/landed_cost_voucher.py index 0832afe427e..2ccb203bf96 100644 --- a/erpnext/stock/doctype/landed_cost_voucher/landed_cost_voucher.py +++ b/erpnext/stock/doctype/landed_cost_voucher/landed_cost_voucher.py @@ -88,6 +88,7 @@ class LandedCostVoucher(Document): self.set_applicable_charges_on_item() self.set_total_vendor_invoices_cost() + self.validate_mandatory_dimensions() def set_total_vendor_invoices_cost(self): self.total_vendor_invoices_cost = 0.0 @@ -196,6 +197,92 @@ class LandedCostVoucher(Document): exc=IncorrectCompanyValidationError, ) + def validate_mandatory_dimensions(self): + from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import ( + get_accounting_dimensions, + get_checks_for_pl_and_bs_accounts, + ) + from erpnext.accounts.doctype.accounting_dimension_filter.accounting_dimension_filter import ( + get_dimension_filter_map, + ) + + if not is_perpetual_inventory_enabled(self.company): + return + + company_checks = [ + check + for check in get_checks_for_pl_and_bs_accounts() + if check.company == self.company and (check.mandatory_for_pl or check.mandatory_for_bs) + ] + dimension_filter_map = get_dimension_filter_map() + + if not company_checks and not dimension_filter_map: + return + + labels = {d.fieldname: d.label for d in get_accounting_dimensions(as_list=False)} + receipts = {} + + for tax in self.get("taxes"): + if not tax.expense_account: + continue + + report_type = frappe.get_cached_value("Account", tax.expense_account, "report_type") + + mandatory = {} + for check in company_checks: + is_mandatory = ( + check.mandatory_for_pl if report_type == "Profit and Loss" else check.mandatory_for_bs + ) + if is_mandatory: + mandatory[check.fieldname] = check.label + + for (fieldname, account), dimension_filter in dimension_filter_map.items(): + if account == tax.expense_account and dimension_filter.get("is_mandatory"): + mandatory.setdefault(fieldname, labels.get(fieldname) or frappe.unscrub(fieldname)) + + for fieldname, label in mandatory.items(): + if tax.get(fieldname): + continue + + for item in self.get("items"): + if self.get_receipt_dimension(receipts, item, fieldname): + continue + + frappe.throw( + _( + "Row {0}: Accounting Dimension {1} is mandatory for account {2}." + " Set it on this Taxes and Charges row, or on Item Row {3} ({4})." + ).format( + tax.idx, + frappe.bold(label), + frappe.bold(tax.expense_account), + item.idx, + frappe.bold(item.item_code), + ), + title=_("Missing Accounting Dimension"), + ) + + def get_receipt_dimension(self, receipts, item, fieldname): + if item.get(fieldname): + return item.get(fieldname) + + key = (item.receipt_document_type, item.receipt_document) + if key not in receipts: + receipts[key] = frappe.get_doc(*key) if item.receipt_document else None + + receipt = receipts[key] + if not receipt: + return None + + row_fieldname = "stock_entry_item" if receipt.doctype == "Stock Entry" else "purchase_receipt_item" + receipt_row_name = item.get(row_fieldname) + + for row in receipt.get("items") or []: + if row.name == receipt_row_name and row.get(fieldname): + return row.get(fieldname) + + return receipt.get(fieldname) + def set_total_taxes_and_charges(self): self.total_taxes_and_charges = sum(flt(d.base_amount) for d in self.get("taxes")) @@ -519,3 +606,25 @@ def get_vendor_invoice_query(filters): query = query.where(doctype.name == filters.get("name")) return query + + +def get_lcv_dimension_fields(): + from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import ( + get_accounting_dimensions, + ) + + return ["cost_center", "project", *get_accounting_dimensions()] + + +def get_row_dimensions(tax_row, lcv_item, dimension_fields): + return frappe._dict( + {field: (tax_row.get(field) or lcv_item.get(field) or None) for field in dimension_fields} + ) + + +def get_custom_dimension_overrides(entry): + return { + dimension: value + for dimension, value in (entry.dimensions or {}).items() + if value and dimension not in ("cost_center", "project") + } diff --git a/erpnext/stock/doctype/landed_cost_voucher/test_landed_cost_voucher.py b/erpnext/stock/doctype/landed_cost_voucher/test_landed_cost_voucher.py index a45787635ed..732235ca0ec 100644 --- a/erpnext/stock/doctype/landed_cost_voucher/test_landed_cost_voucher.py +++ b/erpnext/stock/doctype/landed_cost_voucher/test_landed_cost_voucher.py @@ -1408,3 +1408,265 @@ def distribute_landed_cost_on_items(lcv): for item in lcv.get("items"): item.applicable_charges = flt(item.get(based_on)) * flt(lcv.total_taxes_and_charges) / flt(total) item.applicable_charges = flt(item.applicable_charges, lcv.precision("applicable_charges", item)) + + +def ensure_dimension_fields_on_lcv_charges(dimensions): + from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import ( + make_dimension_in_accounting_doctypes, + ) + + created = False + + for name in dimensions: + dimension = frappe.get_doc("Accounting Dimension", name) + if frappe.db.exists( + "Custom Field", {"dt": "Landed Cost Taxes and Charges", "fieldname": dimension.fieldname} + ): + continue + + make_dimension_in_accounting_doctypes(dimension, ["Landed Cost Taxes and Charges"]) + created = True + + if created: + frappe.clear_cache(doctype="Landed Cost Taxes and Charges") + + +def create_branch(branch): + if not frappe.db.exists("Branch", branch): + frappe.get_doc({"doctype": "Branch", "branch": branch}).insert() + + return branch + + +class TestLandedCostVoucherAccountingDimensions(ERPNextTestSuite): + def setUp(self): + self.company = "_Test Company with perpetual inventory" + self.warehouse = "Stores - TCP1" + self.expense_account = get_expense_account(self.company) + + ensure_dimension_fields_on_lcv_charges(["Branch"]) + self.branch_a = create_branch("_Test LCV Branch A") + self.branch_b = create_branch("_Test LCV Branch B") + + def make_lcv(self, pr, charges, do_not_submit=False): + lcv = frappe.new_doc("Landed Cost Voucher") + lcv.company = self.company + lcv.distribute_charges_based_on = "Amount" + lcv.set( + "purchase_receipts", + [ + { + "receipt_document_type": "Purchase Receipt", + "receipt_document": pr.name, + "supplier": pr.supplier, + "posting_date": pr.posting_date, + "grand_total": pr.base_grand_total, + } + ], + ) + + for idx, charge in enumerate(charges): + lcv.append( + "taxes", + { + "description": f"_Test Charge {idx + 1}", + "expense_account": charge.pop("expense_account", self.expense_account), + **charge, + }, + ) + + lcv.insert() + + if not do_not_submit: + lcv.submit() + + return lcv + + def get_lcv_gl_entries(self, pr, account=None): + return frappe.get_all( + "GL Entry", + filters={ + "voucher_type": "Purchase Receipt", + "voucher_no": pr.name, + "is_cancelled": 0, + **({"account": account} if account else {}), + }, + fields=["account", "debit", "credit", "cost_center", "project", "branch"], + order_by="credit desc", + ) + + def make_dimension_mandatory(self, name, mandatory_for_pl=0, mandatory_for_bs=0): + dimension = frappe.get_doc("Accounting Dimension", name) + row = next((d for d in dimension.dimension_defaults if d.company == self.company), None) + + if row: + previous = (row.mandatory_for_pl, row.mandatory_for_bs) + self.addCleanup(self.restore_dimension_default, name, previous) + else: + row = dimension.append( + "dimension_defaults", + {"company": self.company, "reference_document": dimension.document_type}, + ) + self.addCleanup(self.remove_dimension_default, name) + + row.mandatory_for_pl = mandatory_for_pl + row.mandatory_for_bs = mandatory_for_bs + dimension.save() + + def restore_dimension_default(self, name, previous): + dimension = frappe.get_doc("Accounting Dimension", name) + for row in dimension.dimension_defaults: + if row.company == self.company: + row.mandatory_for_pl, row.mandatory_for_bs = previous + dimension.save() + + def remove_dimension_default(self, name): + dimension = frappe.get_doc("Accounting Dimension", name) + dimension.set( + "dimension_defaults", + [d for d in dimension.dimension_defaults if d.company != self.company], + ) + dimension.save() + + def test_charge_row_dimension_reaches_gl_entry(self): + pr = make_purchase_receipt(company=self.company, warehouse=self.warehouse) + self.make_lcv(pr, [{"amount": 100, "branch": self.branch_a}]) + + charge_entries = self.get_lcv_gl_entries(pr, self.expense_account) + self.assertEqual(len(charge_entries), 1) + self.assertEqual(charge_entries[0].credit, 100.0) + self.assertEqual(charge_entries[0].branch, self.branch_a) + + stock_account = get_inventory_account(self.company, self.warehouse) + self.assertFalse(self.get_lcv_gl_entries(pr, stock_account)[0].branch) + + def test_charge_row_cost_center_and_project_override_receipt_item(self): + from erpnext.accounts.doctype.cost_center.test_cost_center import create_cost_center + + create_cost_center( + cost_center_name="_Test LCV Cost Center", + company=self.company, + parent_cost_center=f"{self.company} - TCP1", + ) + cost_center = "_Test LCV Cost Center - TCP1" + + if not frappe.db.exists("Project", {"project_name": "_Test LCV Project"}): + frappe.get_doc( + {"doctype": "Project", "project_name": "_Test LCV Project", "company": self.company} + ).insert() + project = frappe.db.get_value("Project", {"project_name": "_Test LCV Project"}) + + pr = make_purchase_receipt(company=self.company, warehouse=self.warehouse) + item_cost_center = pr.items[0].cost_center + + self.make_lcv(pr, [{"amount": 100, "cost_center": cost_center, "project": project}]) + + charge_entries = self.get_lcv_gl_entries(pr, self.expense_account) + self.assertEqual(len(charge_entries), 1) + self.assertEqual(charge_entries[0].cost_center, cost_center) + self.assertEqual(charge_entries[0].project, project) + + stock_account = get_inventory_account(self.company, self.warehouse) + self.assertEqual(self.get_lcv_gl_entries(pr, stock_account)[0].cost_center, item_cost_center) + + def test_blank_charge_row_falls_back_to_receipt_item(self): + pr = make_purchase_receipt(company=self.company, warehouse=self.warehouse) + self.make_lcv(pr, [{"amount": 100}]) + + charge_entries = self.get_lcv_gl_entries(pr, self.expense_account) + self.assertEqual(len(charge_entries), 1) + self.assertEqual(charge_entries[0].cost_center, pr.items[0].cost_center) + self.assertFalse(charge_entries[0].branch) + + def test_charge_rows_on_same_account_with_different_dimensions_stay_separate(self): + pr = make_purchase_receipt(company=self.company, warehouse=self.warehouse) + self.make_lcv( + pr, + [ + {"amount": 60, "branch": self.branch_a}, + {"amount": 40, "branch": self.branch_b}, + ], + ) + + charge_entries = self.get_lcv_gl_entries(pr, self.expense_account) + self.assertEqual(len(charge_entries), 2) + self.assertEqual( + {(e.branch, e.credit) for e in charge_entries}, + {(self.branch_a, 60.0), (self.branch_b, 40.0)}, + ) + self.assertEqual(sum(e.credit for e in charge_entries), 100.0) + + def test_two_vouchers_on_same_account_with_different_dimensions_stay_separate(self): + pr = make_purchase_receipt(company=self.company, warehouse=self.warehouse) + self.make_lcv(pr, [{"amount": 60, "branch": self.branch_a}]) + self.make_lcv(pr, [{"amount": 40, "branch": self.branch_b}]) + + charge_entries = self.get_lcv_gl_entries(pr, self.expense_account) + self.assertEqual(len(charge_entries), 2) + self.assertEqual( + {(e.branch, e.credit) for e in charge_entries}, + {(self.branch_a, 60.0), (self.branch_b, 40.0)}, + ) + + def test_mandatory_pl_dimension_is_satisfied_by_charge_row(self): + pr = make_purchase_receipt(company=self.company, warehouse=self.warehouse) + self.make_dimension_mandatory("Branch", mandatory_for_pl=1) + + self.make_lcv(pr, [{"amount": 100, "branch": self.branch_a}]) + + charge_entries = self.get_lcv_gl_entries(pr, self.expense_account) + self.assertEqual(len(charge_entries), 1) + self.assertEqual(charge_entries[0].branch, self.branch_a) + + def test_missing_mandatory_dimension_is_reported_on_the_voucher(self): + pr = make_purchase_receipt(company=self.company, warehouse=self.warehouse) + self.make_dimension_mandatory("Branch", mandatory_for_pl=1) + + with self.assertRaises(frappe.ValidationError) as raised: + self.make_lcv(pr, [{"amount": 100}]) + + message = str(raised.exception) + self.assertIn("Branch", message) + self.assertIn(self.expense_account, message) + + def test_dimensions_survive_reposting(self): + pr = make_purchase_receipt(company=self.company, warehouse=self.warehouse) + self.make_lcv( + pr, + [ + {"amount": 60, "branch": self.branch_a}, + {"amount": 40, "branch": self.branch_b}, + ], + ) + + before = {(e.branch, e.credit) for e in self.get_lcv_gl_entries(pr, self.expense_account)} + + items, warehouses = pr.get_items_and_warehouses() + update_gl_entries_after(pr.posting_date, pr.posting_time, warehouses, items, company=pr.company) + + after = {(e.branch, e.credit) for e in self.get_lcv_gl_entries(pr, self.expense_account)} + self.assertEqual(before, after) + + def test_cancelling_the_voucher_nets_each_dimension_to_zero(self): + pr = make_purchase_receipt(company=self.company, warehouse=self.warehouse) + lcv = self.make_lcv( + pr, + [ + {"amount": 60, "branch": self.branch_a}, + {"amount": 40, "branch": self.branch_b}, + ], + ) + + lcv.reload() + lcv.cancel() + + balances = {} + for entry in frappe.get_all( + "GL Entry", + filters={"voucher_no": pr.name, "account": self.expense_account}, + fields=["branch", "debit", "credit"], + ): + balances[entry.branch] = balances.get(entry.branch, 0.0) + entry.debit - entry.credit + + for branch, balance in balances.items(): + self.assertEqual(flt(balance, 2), 0.0, msg=f"branch {branch} does not net to zero") diff --git a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py index 0ac3e411cd4..e8a2389f252 100644 --- a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py +++ b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py @@ -482,6 +482,9 @@ class PurchaseReceipt(BuyingController): from erpnext.accounts.doctype.purchase_invoice.purchase_invoice import ( get_purchase_document_details, ) + from erpnext.stock.doctype.landed_cost_voucher.landed_cost_voucher import ( + get_custom_dimension_overrides, + ) provisional_accounting_for_non_stock_items = cint( frappe.db.get_value("Company", self.company, "enable_provisional_accounting_for_non_stock_items") @@ -608,32 +611,38 @@ class PurchaseReceipt(BuyingController): def make_landed_cost_gl_entries(item): # Amount added through landed-cost-voucher - if item.landed_cost_voucher_amount and landed_cost_entries: - if (item.item_code, item.name) in landed_cost_entries: - for account, amount in landed_cost_entries[(item.item_code, item.name)].items(): - account_currency = get_account_currency(account) - credit_amount = ( - flt(amount["base_amount"]) - if (amount["base_amount"] or account_currency != self.company_currency) - else flt(amount["amount"]) - ) + if not (item.landed_cost_voucher_amount and landed_cost_entries): + return - if not account: - validate_account("Landed Cost Account") + for entry in landed_cost_entries.get((item.item_code, item.name), []): + if not (entry.amount or entry.base_amount): + continue - self.add_gl_entry( - gl_entries=gl_entries, - account=account, - cost_center=item.cost_center, - debit=0.0, - credit=credit_amount, - remarks=remarks, - against_account=stock_asset_account_name, - credit_in_account_currency=flt(amount["amount"]), - account_currency=account_currency, - project=item.project, - item=item, - ) + account = entry.expense_account + if not account: + validate_account("Landed Cost Account") + + account_currency = get_account_currency(account) + credit_amount = ( + flt(entry.base_amount) + if (entry.base_amount or account_currency != self.company_currency) + else flt(entry.amount) + ) + + self.add_gl_entry( + gl_entries=gl_entries, + account=account, + cost_center=entry.dimensions.cost_center or item.cost_center, + debit=0.0, + credit=credit_amount, + remarks=remarks, + against_account=stock_asset_account_name, + credit_in_account_currency=flt(entry.amount), + account_currency=account_currency, + project=entry.dimensions.project or item.project, + item=item, + dimensions=get_custom_dimension_overrides(entry), + ) def make_expenses_added_to_stock_entries(item): if not self.book_stock_expense_enabled(): diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.py b/erpnext/stock/doctype/stock_entry/stock_entry.py index 8a3a9e63ac9..e699594f5ba 100644 --- a/erpnext/stock/doctype/stock_entry/stock_entry.py +++ b/erpnext/stock/doctype/stock_entry/stock_entry.py @@ -2440,6 +2440,10 @@ class StockEntry(StockController, SubcontractingInwardController): return process_gl_map(gl_entries, from_repost=frappe.flags.through_repost_item_valuation) def set_gl_entries_for_landed_cost_voucher(self, gl_entries, inventory_account_map): + from erpnext.stock.doctype.landed_cost_voucher.landed_cost_voucher import ( + get_custom_dimension_overrides, + ) + landed_cost_entries = self.get_item_account_wise_lcv_entries() if not landed_cost_entries: return @@ -2448,52 +2452,53 @@ class StockEntry(StockController, SubcontractingInwardController): if item.s_warehouse: continue - if (item.item_code, item.name) in landed_cost_entries: - for account, amount in landed_cost_entries[(item.item_code, item.name)].items(): - account_currency = get_account_currency(account) - credit_amount = ( - flt(amount["base_amount"]) - if (amount["base_amount"] or account_currency != self.company_currency) - else flt(amount["amount"]) - ) + for entry in landed_cost_entries.get((item.item_code, item.name), []): + if not (entry.amount or entry.base_amount): + continue - _inv_dict = self.get_inventory_account_dict(item, inventory_account_map, "t_warehouse") - gl_entries.append( - self.get_gl_dict( - { - "account": account, - "against": _inv_dict["account"], - "cost_center": item.cost_center, - "debit": 0.0, - "credit": credit_amount, - "remarks": _("Accounting Entry for LCV in Stock Entry {0}").format(self.name), - "credit_in_account_currency": flt(amount["amount"]), - "account_currency": account_currency, - "project": item.project, - }, - item=item, - ) - ) + account_currency = get_account_currency(entry.expense_account) + credit_amount = ( + flt(entry.base_amount) + if (entry.base_amount or account_currency != self.company_currency) + else flt(entry.amount) + ) - account_currency = get_account_currency(item.expense_account) + _inv_dict = self.get_inventory_account_dict(item, inventory_account_map, "t_warehouse") + gl_dict = self.get_gl_dict( + { + "account": entry.expense_account, + "against": _inv_dict["account"], + "cost_center": entry.dimensions.cost_center or item.cost_center, + "debit": 0.0, + "credit": credit_amount, + "remarks": _("Accounting Entry for LCV in Stock Entry {0}").format(self.name), + "credit_in_account_currency": flt(entry.amount), + "account_currency": account_currency, + "project": entry.dimensions.project or item.project, + }, + item=item, + ) + gl_dict.update(get_custom_dimension_overrides(entry)) + gl_entries.append(gl_dict) - # credit amount in negative to knock off the debit entry - gl_entries.append( - self.get_gl_dict( - { - "account": item.expense_account, - "against": _inv_dict["account"], - "cost_center": item.cost_center, - "debit": 0.0, - "credit": credit_amount * -1, - "remarks": _("Accounting Entry for LCV in Stock Entry {0}").format(self.name), - "debit_in_account_currency": flt(amount["amount"]), - "account_currency": account_currency, - "project": item.project, - }, - item=item, - ) + account_currency = get_account_currency(item.expense_account) + + gl_entries.append( + self.get_gl_dict( + { + "account": item.expense_account, + "against": _inv_dict["account"], + "cost_center": item.cost_center, + "debit": 0.0, + "credit": credit_amount * -1, + "remarks": _("Accounting Entry for LCV in Stock Entry {0}").format(self.name), + "debit_in_account_currency": flt(entry.amount), + "account_currency": account_currency, + "project": item.project, + }, + item=item, ) + ) def update_work_order(self): def _validate_work_order(pro_doc): diff --git a/erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.py b/erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.py index f819bd21baa..d962e6119e1 100644 --- a/erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.py +++ b/erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.py @@ -984,55 +984,63 @@ class SubcontractingReceipt(SubcontractingController): ) def make_item_gl_entries_for_lcv(self, gl_entries, inventory_account_map): + from erpnext.stock.doctype.landed_cost_voucher.landed_cost_voucher import ( + get_custom_dimension_overrides, + ) + landed_cost_entries = self.get_item_account_wise_lcv_entries() if not landed_cost_entries: return for item in self.items: - if item.landed_cost_voucher_amount and landed_cost_entries: + item_entries = landed_cost_entries.get((item.item_code, item.name), []) + + if item.landed_cost_voucher_amount and item_entries: remarks = _("Accounting Entry for Landed Cost Voucher for SCR {0}").format(self.name) - if (item.item_code, item.name) in landed_cost_entries: - _inv_dict = self.get_inventory_account_dict(item, inventory_account_map) + _inv_dict = self.get_inventory_account_dict(item, inventory_account_map) - for account, amount in landed_cost_entries[(item.item_code, item.name)].items(): - account_currency = get_account_currency(account) - credit_amount = ( - flt(amount["base_amount"]) - if (amount["base_amount"] or account_currency != self.company_currency) - else flt(amount["amount"]) - ) + for entry in item_entries: + if not (entry.amount or entry.base_amount): + continue - self.add_gl_entry( - gl_entries=gl_entries, - account=account, - cost_center=item.cost_center, - debit=0.0, - credit=credit_amount, - remarks=remarks, - against_account=_inv_dict["account"], - credit_in_account_currency=flt(amount["amount"]), - account_currency=account_currency, - project=item.project, - item=item, - ) + account_currency = get_account_currency(entry.expense_account) + credit_amount = ( + flt(entry.base_amount) + if (entry.base_amount or account_currency != self.company_currency) + else flt(entry.amount) + ) - account_currency = get_account_currency(item.expense_account) + self.add_gl_entry( + gl_entries=gl_entries, + account=entry.expense_account, + cost_center=entry.dimensions.cost_center or item.cost_center, + debit=0.0, + credit=credit_amount, + remarks=remarks, + against_account=_inv_dict["account"], + credit_in_account_currency=flt(entry.amount), + account_currency=account_currency, + project=entry.dimensions.project or item.project, + item=item, + dimensions=get_custom_dimension_overrides(entry), + ) - # credit amount in negative to knock off the debit entry - self.add_gl_entry( - gl_entries=gl_entries, - account=item.expense_account, - cost_center=item.cost_center, - debit=0.0, - credit=credit_amount * -1, - remarks=remarks, - against_account=_inv_dict["account"], - debit_in_account_currency=flt(amount["amount"]), - account_currency=account_currency, - project=item.project, - item=item, - ) + account_currency = get_account_currency(item.expense_account) + + self.add_gl_entry( + gl_entries=gl_entries, + account=item.expense_account, + cost_center=item.cost_center, + debit=0.0, + credit=credit_amount * -1, + remarks=remarks, + against_account=_inv_dict["account"], + debit_in_account_currency=flt(entry.amount), + account_currency=account_currency, + project=item.project, + item=item, + ) def auto_create_purchase_receipt(self): if frappe.db.get_single_value("Buying Settings", "auto_create_purchase_receipt"):