mirror of
https://github.com/frappe/erpnext.git
synced 2026-04-21 15:48:29 +00:00
feat: track purchases in accounting
This commit is contained in:
@@ -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):
|
||||
|
||||
@@ -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({})
|
||||
|
||||
@@ -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", {}],
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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")
|
||||
|
||||
Reference in New Issue
Block a user