feat: track purchases in accounting

This commit is contained in:
Rohit Waghchaure
2025-10-01 18:53:49 +05:30
parent a5a3f52c64
commit 05f2b43344
13 changed files with 270 additions and 16 deletions

View File

@@ -884,6 +884,7 @@ class PurchaseInvoice(BuyingController):
self.make_write_off_gl_entry(gl_entries)
self.make_gle_for_rounding_adjustment(gl_entries)
self.set_transaction_currency_and_rate_in_gl_map(gl_entries)
self.set_gl_entry_for_purchase_expense(gl_entries)
return gl_entries
def check_asset_cwip_enabled(self):

View File

@@ -17,7 +17,7 @@ from erpnext.accounts.party import get_party_details
from erpnext.buying.utils import update_last_purchase_rate, validate_for_items
from erpnext.controllers.sales_and_purchase_return import get_rate_for_return
from erpnext.controllers.subcontracting_controller import SubcontractingController
from erpnext.stock.get_item_details import get_conversion_factor
from erpnext.stock.get_item_details import get_conversion_factor, get_item_defaults
from erpnext.stock.utils import get_incoming_rate
@@ -307,6 +307,60 @@ class BuyingController(SubcontractingController):
address_display_field, render_address(self.get(address_field), check_permissions=False)
)
def set_gl_entry_for_purchase_expense(self, gl_entries):
if self.doctype == "Purchase Invoice" and not self.update_stock:
return
for row in self.items:
details = get_purchase_expense_account(row.item_code, self.company)
if not details.purchase_expense_account:
details.purchase_expense_account = frappe.get_cached_value(
"Company", self.company, "purchase_expense_account"
)
if not details.purchase_expense_account:
return
if not details.purchase_expense_contra_account:
details.purchase_expense_contra_account = frappe.get_cached_value(
"Company", self.company, "purchase_expense_contra_account"
)
if not details.purchase_expense_contra_account:
frappe.throw(
_("Please set Purchase Expense Contra Account in Company {0}").format(self.company)
)
amount = flt(row.valuation_rate * row.stock_qty, row.precision("base_amount"))
self.add_gl_entry(
gl_entries=gl_entries,
account=details.purchase_expense_account,
cost_center=row.cost_center,
debit=amount,
credit=0.0,
remarks=_("Purchase Expense for Item {0}").format(row.item_code),
against_account=details.purchase_expense_contra_account,
account_currency=frappe.get_cached_value(
"Account", details.purchase_expense_account, "account_currency"
),
item=row,
)
self.add_gl_entry(
gl_entries=gl_entries,
account=details.purchase_expense_contra_account,
cost_center=row.cost_center,
debit=0.0,
credit=amount,
remarks=_("Purchase Expense for Item {0}").format(row.item_code),
against_account=details.purchase_expense_account,
account_currency=frappe.get_cached_value(
"Account", details.purchase_expense_contra_account, "account_currency"
),
item=row,
)
def set_total_in_words(self):
from frappe.utils import money_in_words
@@ -1171,3 +1225,33 @@ def validate_item_type(doc, fieldname, message):
@erpnext.allow_regional
def update_regional_item_valuation_rate(doc):
pass
@frappe.request_cache
def get_purchase_expense_account(item_code, company):
defaults = get_item_defaults(item_code, company)
details = frappe._dict(
{
"purchase_expense_account": defaults.get("purchase_expense_account"),
"purchase_expense_contra_account": defaults.get("purchase_expense_contra_account"),
}
)
if not details.purchase_expense_account:
details = frappe.db.get_value(
"Item Default",
{"parent": defaults.item_group, "company": company},
["purchase_expense_account", "purchase_expense_contra_account"],
as_dict=1,
) or frappe._dict({})
if not details.purchase_expense_account:
details = frappe.db.get_value(
"Item Default",
{"parent": defaults.brand, "company": company},
["purchase_expense_account", "purchase_expense_contra_account"],
as_dict=1,
)
return details or frappe._dict({})

View File

@@ -278,6 +278,8 @@ erpnext.company.setup_queries = function (frm) {
["depreciation_expense_account", { root_type: "Expense", account_type: "Depreciation" }],
["disposal_account", { report_type: "Profit and Loss" }],
["default_inventory_account", { account_type: "Stock" }],
["purchase_expense_account", { root_type: "Expense" }],
["purchase_expense_contra_account", { root_type: "Expense" }],
["cost_center", {}],
["round_off_cost_center", {}],
["depreciation_cost_center", {}],

View File

@@ -106,6 +106,10 @@
"default_warehouse_for_sales_return",
"credit_limit",
"transactions_annual_history",
"purchase_expense_section",
"purchase_expense_account",
"column_break_ereg",
"purchase_expense_contra_account",
"stock_tab",
"auto_accounting_for_stock_settings",
"enable_perpetual_inventory",
@@ -844,6 +848,27 @@
"options": "Currency",
"print_hide": 1,
"read_only": 1
},
{
"fieldname": "purchase_expense_section",
"fieldtype": "Section Break",
"label": "Purchase Expense"
},
{
"fieldname": "column_break_ereg",
"fieldtype": "Column Break"
},
{
"fieldname": "purchase_expense_account",
"fieldtype": "Link",
"label": "Purchase Expense Account",
"options": "Account"
},
{
"fieldname": "purchase_expense_contra_account",
"fieldtype": "Link",
"label": "Purchase Expense Contra Account",
"options": "Account"
}
],
"icon": "fa fa-building",
@@ -851,7 +876,7 @@
"image_field": "company_logo",
"is_tree": 1,
"links": [],
"modified": "2025-08-25 18:34:03.602046",
"modified": "2025-10-01 17:34:10.971627",
"modified_by": "Administrator",
"module": "Setup",
"name": "Company",

View File

@@ -86,6 +86,8 @@ class Company(NestedSet):
parent_company: DF.Link | None
payment_terms: DF.Link | None
phone_no: DF.Data | None
purchase_expense_account: DF.Link | None
purchase_expense_contra_account: DF.Link | None
reconcile_on_advance_payment_date: DF.Check
reconciliation_takes_effect_on: DF.Literal[
"Advance Payment Date", "Oldest Of Invoice Or Advance", "Reconciliation Date"

View File

@@ -488,6 +488,21 @@ $.extend(erpnext.item, {
},
};
});
let fields = ["purchase_expense_account", "purchase_expense_contra_account", "default_cogs_account"];
fields.forEach((field) => {
frm.set_query(field, "item_defaults", (doc, cdt, cdn) => {
let row = locals[cdt][cdn];
return {
filters: {
company: row.company,
root_type: "Expense",
is_group: 0,
},
};
});
});
},
make_dashboard: function (frm) {

View File

@@ -16,20 +16,22 @@
"item_name",
"item_group",
"stock_uom",
"opening_stock",
"valuation_rate",
"standard_rate",
"column_break0",
"disabled",
"allow_alternative_item",
"is_stock_item",
"has_variants",
"opening_stock",
"valuation_rate",
"standard_rate",
"is_fixed_asset",
"auto_create_assets",
"is_grouped_asset",
"asset_category",
"asset_naming_series",
"section_break_znra",
"over_delivery_receipt_allowance",
"column_break_wugd",
"over_billing_allowance",
"image",
"section_break_11",
@@ -63,6 +65,8 @@
"column_break_37",
"has_serial_no",
"serial_no_series",
"defaults_tab",
"item_defaults",
"variants_section",
"variant_of",
"variant_based_on",
@@ -74,8 +78,6 @@
"column_break_9s9o",
"enable_deferred_revenue",
"no_of_months",
"section_break_avcp",
"item_defaults",
"purchasing_tab",
"purchase_uom",
"min_order_qty",
@@ -887,10 +889,6 @@
"fieldname": "column_break_9s9o",
"fieldtype": "Column Break"
},
{
"fieldname": "section_break_avcp",
"fieldtype": "Section Break"
},
{
"collapsible": 1,
"fieldname": "deferred_accounting_section",
@@ -936,6 +934,19 @@
"fieldname": "production_capacity",
"fieldtype": "Int",
"label": "Production Capacity"
},
{
"fieldname": "defaults_tab",
"fieldtype": "Tab Break",
"label": "Defaults"
},
{
"fieldname": "section_break_znra",
"fieldtype": "Section Break"
},
{
"fieldname": "column_break_wugd",
"fieldtype": "Column Break"
}
],
"icon": "fa fa-tag",
@@ -943,7 +954,7 @@
"image_field": "image",
"links": [],
"make_attachments_public": 1,
"modified": "2025-08-14 23:35:56.293048",
"modified": "2025-10-01 16:58:40.946604",
"modified_by": "Administrator",
"module": "Stock",
"name": "Item",

View File

@@ -305,6 +305,7 @@ class TestItem(IntegrationTestCase):
"company": "_Test Company",
"default_warehouse": "_Test Warehouse 2 - _TC", # no override
"expense_account": "_Test Account Stock Expenses - _TC", # override brand default
"default_cogs_account": "_Test Account Cost for Goods Sold - _TC", # override brand default
"buying_cost_center": "_Test Write Off Cost Center - _TC", # override item group default
}
],
@@ -315,7 +316,7 @@ class TestItem(IntegrationTestCase):
"item_code": "Test Item With Defaults",
"warehouse": "_Test Warehouse 2 - _TC", # from item
"income_account": "_Test Account Sales - _TC", # from brand
"expense_account": "_Test Account Stock Expenses - _TC", # from item
"expense_account": "_Test Account Cost for Goods Sold - _TC", # from item
"cost_center": "_Test Cost Center 2 - _TC", # from item group
}
sales_item_details = get_item_details(

View File

@@ -16,10 +16,15 @@
"column_break_8",
"expense_account",
"default_provisional_account",
"column_break_cpif",
"purchase_expense_account",
"purchase_expense_contra_account",
"selling_defaults",
"selling_cost_center",
"column_break_12",
"income_account",
"cost_of_good_sold_section",
"default_cogs_account",
"deferred_accounting_defaults_section",
"deferred_expense_account",
"column_break_kwad",
@@ -111,7 +116,7 @@
{
"fieldname": "default_provisional_account",
"fieldtype": "Link",
"label": "Default Provisional Account",
"label": "Default Provisional Account (Service)",
"options": "Account"
},
{
@@ -136,19 +141,47 @@
{
"fieldname": "column_break_kwad",
"fieldtype": "Column Break"
},
{
"fieldname": "column_break_cpif",
"fieldtype": "Column Break"
},
{
"fieldname": "cost_of_good_sold_section",
"fieldtype": "Section Break",
"label": "Cost of Goods Sold"
},
{
"fieldname": "default_cogs_account",
"fieldtype": "Link",
"label": "Default COGS Account",
"options": "Account"
},
{
"fieldname": "purchase_expense_account",
"fieldtype": "Link",
"label": "Purchase Expense Account",
"options": "Account"
},
{
"fieldname": "purchase_expense_contra_account",
"fieldtype": "Link",
"label": "Purchase Expense Contra Account",
"options": "Account"
}
],
"istable": 1,
"links": [],
"modified": "2025-03-17 13:46:09.719105",
"modified": "2025-10-01 19:17:33.687836",
"modified_by": "Administrator",
"module": "Stock",
"name": "Item Default",
"owner": "Administrator",
"permissions": [],
"quick_entry": 1,
"row_format": "Dynamic",
"sort_field": "creation",
"sort_order": "DESC",
"states": [],
"track_changes": 1
}
}

View File

@@ -16,6 +16,7 @@ class ItemDefault(Document):
buying_cost_center: DF.Link | None
company: DF.Link
default_cogs_account: DF.Link | None
default_discount_account: DF.Link | None
default_price_list: DF.Link | None
default_provisional_account: DF.Link | None
@@ -28,6 +29,8 @@ class ItemDefault(Document):
parent: DF.Data
parentfield: DF.Data
parenttype: DF.Data
purchase_expense_account: DF.Link | None
purchase_expense_contra_account: DF.Link | None
selling_cost_center: DF.Link | None
# end: auto-generated types

View File

@@ -479,6 +479,7 @@ class PurchaseReceipt(BuyingController):
self.make_item_gl_entries(gl_entries, warehouse_account=warehouse_account)
self.make_tax_gl_entries(gl_entries, via_landed_cost_voucher)
self.set_gl_entry_for_purchase_expense(gl_entries)
update_regional_gl_entries(gl_entries, self)
return process_gl_map(gl_entries, from_repost=frappe.flags.through_repost_item_valuation)

View File

@@ -4415,6 +4415,69 @@ class TestPurchaseReceipt(IntegrationTestCase):
self.assertEqual(srbnb_cost, 1000)
def test_purchase_expense_account(self):
item = "Test Item with Purchase Expense Account"
make_item(item, {"is_stock_item": 1})
company = "_Test Company with perpetual inventory"
expense_account = "_Test Account Purchase Expense - TCP1"
expense_contra_account = "_Test Account Purchase Contra Expense - TCP1"
if not frappe.db.exists("Account", expense_account):
frappe.get_doc(
{
"doctype": "Account",
"account_name": "_Test Account Purchase Expense",
"parent_account": "Stock Expenses - TCP1",
"company": company,
"is_group": 0,
"root_type": "Expense",
}
).insert()
if not frappe.db.exists("Account", expense_contra_account):
frappe.get_doc(
{
"doctype": "Account",
"account_name": "_Test Account Purchase Contra Expense",
"parent_account": "Stock Expenses - TCP1",
"company": company,
"is_group": 0,
"root_type": "Expense",
}
).insert()
item_doc = frappe.get_doc("Item", item)
item_doc.append(
"item_defaults",
{
"company": company,
"default_warehouse": "Stores - TCP1",
"purchase_expense_account": expense_account,
"purchase_expense_contra_account": expense_contra_account,
},
)
item_doc.save()
pr = make_purchase_receipt(
item_code=item,
qty=10,
rate=100,
company=company,
warehouse="Stores - TCP1",
)
gl_entries = get_gl_entries(pr.doctype, pr.name)
accounts = [d.account for d in gl_entries]
self.assertTrue(expense_account in accounts)
self.assertTrue(expense_contra_account in accounts)
for row in gl_entries:
if row.account == expense_account:
self.assertEqual(row.debit, 1000)
if row.account == expense_contra_account:
self.assertEqual(row.credit, 1000)
def prepare_data_for_internal_transfer():
from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_internal_supplier

View File

@@ -868,6 +868,19 @@ def get_default_income_account(ctx: ItemDetailsCtx, item, item_group, brand):
def get_default_expense_account(ctx: ItemDetailsCtx, item, item_group, brand):
if ctx.get("doctype") in ["Sales Invoice", "Delivery Note"]:
expense_account = (
item.get("default_cogs_account")
or item_group.get("default_cogs_account")
or brand.get("default_cogs_account")
)
if not expense_account:
expense_account = frappe.get_cached_value("Company", ctx.company, "default_expense_account")
if expense_account:
return expense_account
return (
item.get("expense_account")
or item_group.get("expense_account")