fix(stock): carry accounting dimensions from landed cost voucher charges into gl entries

This commit is contained in:
ervishnucs
2026-08-30 22:59:36 +05:30
parent eb4c327a2b
commit 0c7be311b8
10 changed files with 335 additions and 135 deletions

View File

@@ -1023,6 +1023,10 @@ class PurchaseInvoice(BuyingController):
gl_entries.append(self.get_gl_dict(gl, self.party_account_currency, item=self)) gl_entries.append(self.get_gl_dict(gl, self.party_account_currency, item=self))
def make_item_gl_entries(self, gl_entries): 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 # item gl entries
stock_items = self.get_stock_items() stock_items = self.get_stock_items()
if self.update_stock and self.auto_accounting_for_stock: if self.update_stock and self.auto_accounting_for_stock:
@@ -1164,25 +1168,34 @@ class PurchaseInvoice(BuyingController):
# Amount added through landed-cost-voucher # Amount added through landed-cost-voucher
if landed_cost_entries: if landed_cost_entries:
if (item.item_code, item.name) in landed_cost_entries: for entry in landed_cost_entries.get((item.item_code, item.name), []):
for account, base_amount in landed_cost_entries[ if not (entry.amount or entry.base_amount):
(item.item_code, item.name) continue
].items():
gl_entries.append( lcv_account_currency = get_account_currency(entry.expense_account)
self.get_gl_dict( 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": account, "account": entry.expense_account,
"against": item.expense_account, "against": item.expense_account,
"cost_center": item.cost_center, "cost_center": entry.dimensions.cost_center or item.cost_center,
"remarks": self.get("remarks") or _("Accounting Entry for Stock"), "remarks": self.get("remarks") or _("Accounting Entry for Stock"),
"credit": flt(base_amount["base_amount"]), "credit": flt(entry.base_amount),
"credit_in_account_currency": flt(base_amount["amount"]), "credit_in_account_currency": flt(entry.amount),
"credit_in_transaction_currency": item.net_amount, "credit_in_transaction_currency": credit_in_transaction_currency,
"project": item.project or self.project, "project": entry.dimensions.project or item.project or self.project,
}, },
item=item, item=item,
) )
) gl_dict.update(get_custom_dimension_overrides(entry))
gl_entries.append(gl_dict)
# sub-contracting warehouse # sub-contracting warehouse
if flt(item.rm_supp_cost): if flt(item.rm_supp_cost):

View File

@@ -1248,7 +1248,13 @@ class StockController(AccountsController):
if not landed_cost_vouchers: if not landed_cost_vouchers:
return return
from erpnext.stock.doctype.landed_cost_voucher.landed_cost_voucher import (
get_lcv_dimension_fields,
get_row_dimensions,
)
item_account_wise_cost = {} item_account_wise_cost = {}
dimension_fields = get_lcv_dimension_fields()
row_fieldname = "purchase_receipt_item" row_fieldname = "purchase_receipt_item"
if self.doctype == "Stock Entry": if self.doctype == "Stock Entry":
@@ -1270,28 +1276,36 @@ class StockController(AccountsController):
for item in landed_cost_voucher_doc.items: for item in landed_cost_voucher_doc.items:
if item.receipt_document == self.name: 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: for account in landed_cost_voucher_doc.taxes:
exchange_rate = account.exchange_rate or 1 exchange_rate = account.exchange_rate or 1
item_account_wise_cost.setdefault((item.item_code, item.get(row_fieldname)), {}) dimensions = get_row_dimensions(account, item, dimension_fields)
item_account_wise_cost[(item.item_code, item.get(row_fieldname))].setdefault( group_key = (
account.expense_account, {"amount": 0.0, "base_amount": 0.0} 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))][ item_row = charges.get(group_key)
account.expense_account 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: 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 account.base_amount * item.get(based_on_field) / total_item_cost
) )
else: else:
item_row["amount"] += item.applicable_charges / exchange_rate item_row.amount += item.applicable_charges / exchange_rate
item_row["base_amount"] += item.applicable_charges 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): def validate_inventory_dimension_mandatory(self):
# Mandatory inventory dimensions are enforced here (instead of via field-level `reqd`) # Mandatory inventory dimensions are enforced here (instead of via field-level `reqd`)
@@ -1978,6 +1992,7 @@ class StockController(AccountsController):
voucher_detail_no=None, voucher_detail_no=None,
item=None, item=None,
posting_date=None, posting_date=None,
dimensions=None,
): ):
gl_entry = { gl_entry = {
"account": account, "account": account,
@@ -2003,6 +2018,9 @@ class StockController(AccountsController):
if posting_date: if posting_date:
gl_entry.update({"posting_date": 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)) gl_entries.append(self.get_gl_dict(gl_entry, item=item))
def update_stock_reservation_entries(self): def update_stock_reservation_entries(self):

View File

@@ -559,6 +559,7 @@ accounting_dimension_doctypes = [
"Purchase Taxes and Charges", "Purchase Taxes and Charges",
"Shipping Rule", "Shipping Rule",
"Landed Cost Item", "Landed Cost Item",
"Landed Cost Taxes and Charges",
"Asset Value Adjustment", "Asset Value Adjustment",
"Asset Repair", "Asset Repair",
"Asset Capitalization", "Asset Capitalization",

View File

@@ -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.v15_0.backfill_sla_link_filters_on_docfield
erpnext.patches.v16_0.crm_settings_handle_allowed_users_for_frappe_crm 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.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.access_control_for_project_users
erpnext.patches.v16_0.enable_book_stock_expense_gl_entries erpnext.patches.v16_0.enable_book_stock_expense_gl_entries
erpnext.patches.v16_0.rename_ar_ap_ageing_filter erpnext.patches.v16_0.rename_ar_ap_ageing_filter

View File

@@ -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"])

View File

@@ -17,7 +17,11 @@
"has_operating_cost", "has_operating_cost",
"operation_id", "operation_id",
"qty", "qty",
"operating_component" "operating_component",
"accounting_dimensions_section",
"cost_center",
"dimension_col_break",
"project"
], ],
"fields": [ "fields": [
{ {
@@ -107,13 +111,34 @@
"label": "Operating Component", "label": "Operating Component",
"no_copy": 1, "no_copy": 1,
"read_only": 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, "grid_page_length": 50,
"index_web_pages_for_search": 1, "index_web_pages_for_search": 1,
"istable": 1, "istable": 1,
"links": [], "links": [],
"modified": "2026-05-19 12:21:07.953801", "modified": "2026-08-04 10:00:00.000000",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Stock", "module": "Stock",
"name": "Landed Cost Taxes and Charges", "name": "Landed Cost Taxes and Charges",

View File

@@ -88,6 +88,7 @@ class LandedCostVoucher(Document):
self.set_applicable_charges_on_item() self.set_applicable_charges_on_item()
self.set_total_vendor_invoices_cost() self.set_total_vendor_invoices_cost()
self.validate_mandatory_dimensions()
def set_total_vendor_invoices_cost(self): def set_total_vendor_invoices_cost(self):
self.total_vendor_invoices_cost = 0.0 self.total_vendor_invoices_cost = 0.0
@@ -196,6 +197,92 @@ class LandedCostVoucher(Document):
exc=IncorrectCompanyValidationError, 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): def set_total_taxes_and_charges(self):
self.total_taxes_and_charges = sum(flt(d.base_amount) for d in self.get("taxes")) 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")) query = query.where(doctype.name == filters.get("name"))
return query 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")
}

View File

@@ -481,6 +481,9 @@ class PurchaseReceipt(BuyingController):
from erpnext.accounts.doctype.purchase_invoice.purchase_invoice import ( from erpnext.accounts.doctype.purchase_invoice.purchase_invoice import (
get_purchase_document_details, 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( provisional_accounting_for_non_stock_items = cint(
frappe.db.get_value("Company", self.company, "enable_provisional_accounting_for_non_stock_items") frappe.db.get_value("Company", self.company, "enable_provisional_accounting_for_non_stock_items")
@@ -607,31 +610,37 @@ class PurchaseReceipt(BuyingController):
def make_landed_cost_gl_entries(item): def make_landed_cost_gl_entries(item):
# Amount added through landed-cost-voucher # Amount added through landed-cost-voucher
if item.landed_cost_voucher_amount and landed_cost_entries: if not (item.landed_cost_voucher_amount and landed_cost_entries):
if (item.item_code, item.name) in landed_cost_entries: return
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
account = entry.expense_account
if not account: if not account:
validate_account("Landed Cost 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( self.add_gl_entry(
gl_entries=gl_entries, gl_entries=gl_entries,
account=account, account=account,
cost_center=item.cost_center, cost_center=entry.dimensions.cost_center or item.cost_center,
debit=0.0, debit=0.0,
credit=credit_amount, credit=credit_amount,
remarks=remarks, remarks=remarks,
against_account=stock_asset_account_name, against_account=stock_asset_account_name,
credit_in_account_currency=flt(amount["amount"]), credit_in_account_currency=flt(entry.amount),
account_currency=account_currency, account_currency=account_currency,
project=item.project, project=entry.dimensions.project or item.project,
item=item, item=item,
dimensions=get_custom_dimension_overrides(entry),
) )
def make_expenses_added_to_stock_entries(item): def make_expenses_added_to_stock_entries(item):

View File

@@ -2498,6 +2498,10 @@ class StockEntry(StockController, SubcontractingInwardController):
return process_gl_map(gl_entries, from_repost=frappe.flags.through_repost_item_valuation) 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): 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() landed_cost_entries = self.get_item_account_wise_lcv_entries()
if not landed_cost_entries: if not landed_cost_entries:
return return
@@ -2506,36 +2510,37 @@ class StockEntry(StockController, SubcontractingInwardController):
if item.s_warehouse: if item.s_warehouse:
continue continue
if (item.item_code, item.name) in landed_cost_entries: for entry in landed_cost_entries.get((item.item_code, item.name), []):
for account, amount in landed_cost_entries[(item.item_code, item.name)].items(): if not (entry.amount or entry.base_amount):
account_currency = get_account_currency(account) continue
account_currency = get_account_currency(entry.expense_account)
credit_amount = ( credit_amount = (
flt(amount["base_amount"]) flt(entry.base_amount)
if (amount["base_amount"] or account_currency != self.company_currency) if (entry.base_amount or account_currency != self.company_currency)
else flt(amount["amount"]) else flt(entry.amount)
) )
_inv_dict = self.get_inventory_account_dict(item, inventory_account_map, "t_warehouse") _inv_dict = self.get_inventory_account_dict(item, inventory_account_map, "t_warehouse")
gl_entries.append( gl_dict = self.get_gl_dict(
self.get_gl_dict(
{ {
"account": account, "account": entry.expense_account,
"against": _inv_dict["account"], "against": _inv_dict["account"],
"cost_center": item.cost_center, "cost_center": entry.dimensions.cost_center or item.cost_center,
"debit": 0.0, "debit": 0.0,
"credit": credit_amount, "credit": credit_amount,
"remarks": _("Accounting Entry for LCV in Stock Entry {0}").format(self.name), "remarks": _("Accounting Entry for LCV in Stock Entry {0}").format(self.name),
"credit_in_account_currency": flt(amount["amount"]), "credit_in_account_currency": flt(entry.amount),
"account_currency": account_currency, "account_currency": account_currency,
"project": item.project, "project": entry.dimensions.project or item.project,
}, },
item=item, item=item,
) )
) gl_dict.update(get_custom_dimension_overrides(entry))
gl_entries.append(gl_dict)
account_currency = get_account_currency(item.expense_account) account_currency = get_account_currency(item.expense_account)
# credit amount in negative to knock off the debit entry
gl_entries.append( gl_entries.append(
self.get_gl_dict( self.get_gl_dict(
{ {
@@ -2545,7 +2550,7 @@ class StockEntry(StockController, SubcontractingInwardController):
"debit": 0.0, "debit": 0.0,
"credit": credit_amount * -1, "credit": credit_amount * -1,
"remarks": _("Accounting Entry for LCV in Stock Entry {0}").format(self.name), "remarks": _("Accounting Entry for LCV in Stock Entry {0}").format(self.name),
"debit_in_account_currency": flt(amount["amount"]), "debit_in_account_currency": flt(entry.amount),
"account_currency": account_currency, "account_currency": account_currency,
"project": item.project, "project": item.project,
}, },

View File

@@ -908,42 +908,50 @@ class SubcontractingReceipt(SubcontractingController):
) )
def make_item_gl_entries_for_lcv(self, gl_entries, inventory_account_map): 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() landed_cost_entries = self.get_item_account_wise_lcv_entries()
if not landed_cost_entries: if not landed_cost_entries:
return return
for item in self.items: 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) 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(): for entry in item_entries:
account_currency = get_account_currency(account) if not (entry.amount or entry.base_amount):
continue
account_currency = get_account_currency(entry.expense_account)
credit_amount = ( credit_amount = (
flt(amount["base_amount"]) flt(entry.base_amount)
if (amount["base_amount"] or account_currency != self.company_currency) if (entry.base_amount or account_currency != self.company_currency)
else flt(amount["amount"]) else flt(entry.amount)
) )
self.add_gl_entry( self.add_gl_entry(
gl_entries=gl_entries, gl_entries=gl_entries,
account=account, account=entry.expense_account,
cost_center=item.cost_center, cost_center=entry.dimensions.cost_center or item.cost_center,
debit=0.0, debit=0.0,
credit=credit_amount, credit=credit_amount,
remarks=remarks, remarks=remarks,
against_account=_inv_dict["account"], against_account=_inv_dict["account"],
credit_in_account_currency=flt(amount["amount"]), credit_in_account_currency=flt(entry.amount),
account_currency=account_currency, account_currency=account_currency,
project=item.project, project=entry.dimensions.project or item.project,
item=item, item=item,
dimensions=get_custom_dimension_overrides(entry),
) )
account_currency = get_account_currency(item.expense_account) account_currency = get_account_currency(item.expense_account)
# credit amount in negative to knock off the debit entry
self.add_gl_entry( self.add_gl_entry(
gl_entries=gl_entries, gl_entries=gl_entries,
account=item.expense_account, account=item.expense_account,
@@ -952,7 +960,7 @@ class SubcontractingReceipt(SubcontractingController):
credit=credit_amount * -1, credit=credit_amount * -1,
remarks=remarks, remarks=remarks,
against_account=_inv_dict["account"], against_account=_inv_dict["account"],
debit_in_account_currency=flt(amount["amount"]), debit_in_account_currency=flt(entry.amount),
account_currency=account_currency, account_currency=account_currency,
project=item.project, project=item.project,
item=item, item=item,