diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py index 17b4ca6152b..95d4c83aa26 100644 --- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py +++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py @@ -1023,6 +1023,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: @@ -1164,25 +1168,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 32f35502a52..2ed15b00828 100644 --- a/erpnext/controllers/stock_controller.py +++ b/erpnext/controllers/stock_controller.py @@ -1248,7 +1248,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": @@ -1270,28 +1276,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`) @@ -1978,6 +1992,7 @@ class StockController(AccountsController): voucher_detail_no=None, item=None, posting_date=None, + dimensions=None, ): gl_entry = { "account": account, @@ -2003,6 +2018,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 e27a02f1e7e..15cc84048dd 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 4332b7429a6..40515a60187 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/purchase_receipt/purchase_receipt.py b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py index 24227810dcb..3bd6847b5c6 100644 --- a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py +++ b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py @@ -481,6 +481,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") @@ -607,32 +610,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 936b64bbd5c..572346da031 100644 --- a/erpnext/stock/doctype/stock_entry/stock_entry.py +++ b/erpnext/stock/doctype/stock_entry/stock_entry.py @@ -2498,6 +2498,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 @@ -2506,52 +2510,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 9e70e005318..c2a5d59c88c 100644 --- a/erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.py +++ b/erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.py @@ -908,55 +908,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"):