feat: add side-by-side defaults comparison view in item defaults grid (#55017)

* feat: add side-by-side defaults comparison view in item defaults grid

* fix: coderabbit suggested changes

* fix: change label of the fields

* fix: description design
This commit is contained in:
Khushi Rawat
2026-06-05 15:43:57 +05:30
committed by GitHub
parent 5873f55cf0
commit 5aab5502f0
4 changed files with 603 additions and 137 deletions

View File

@@ -101,3 +101,139 @@ frappe.ui.form.on("Item Group", {
page_name: frappe.utils.warn_page_name_change,
});
frappe.ui.form.on("Item Default", {
form_render: function (frm, cdt, cdn) {
if (!frm.fields_dict["item_group_defaults"]) return;
const row = locals[cdt][cdn];
if (!row || !row.company) {
Object.values(COMPANY_DEFAULTS_TO_VF).forEach((vf) => frappe.model.set_value(cdt, cdn, vf, ""));
return;
}
setTimeout(() => {
const $grid_row = frm.fields_dict["item_group_defaults"].grid.wrapper.find(
`.grid-row[data-name="${cdn}"]`
);
$grid_row.find(".column-label").eq(1).text(__("Item Group Override"));
}, 50);
const $grid_row = frm.fields_dict["item_group_defaults"].grid.wrapper.find(
`.grid-row[data-name="${cdn}"]`
);
if (!$grid_row.find(".item-defaults-desc").length) {
$grid_row.find(".grid-form-body").prepend(`
<div class="row">
<div class="col-xs-12">
<div class="item-defaults-desc" style="
background: var(--control-bg);
border-radius: var(--border-radius-sm);
padding: 6px 6px 8px 14px;
color: var(--text-muted);
">
${__(
"Left column shows system-level defaults (Company / Stock Settings). Right column is where you set overrides for this item group."
)}
</div>
</div>
</div>
`);
}
populate_item_group_company_defaults(frm, cdt, cdn, row);
},
company: function (frm, cdt, cdn) {
if (!frm.fields_dict["item_group_defaults"]) return;
const row = locals[cdt][cdn];
if (!row || !row.company) {
Object.values(COMPANY_DEFAULTS_TO_VF).forEach((vf) => frappe.model.set_value(cdt, cdn, vf, ""));
return;
}
populate_item_group_company_defaults(frm, cdt, cdn, row);
},
});
const COMPANY_DEFAULTS_TO_VF = {
default_warehouse: "vf_default_warehouse",
default_inventory_account: "vf_default_inventory_account",
buying_cost_center: "vf_buying_cost_center",
selling_cost_center: "vf_selling_cost_center",
expense_account: "vf_expense_account",
income_account: "vf_income_account",
default_provisional_account: "vf_default_provisional_account",
purchase_expense_account: "vf_purchase_expense_account",
default_cogs_account: "vf_default_cogs_account",
deferred_expense_account: "vf_deferred_expense_account",
deferred_revenue_account: "vf_deferred_revenue_account",
default_price_list: "vf_default_price_list",
default_discount_account: "vf_default_discount_account",
default_supplier: "vf_default_supplier",
purchase_expense_contra_account: "vf_purchase_expense_contra_account",
};
const FIELD_DEFAULT_SOURCE = {
default_warehouse: "Stock Settings",
default_inventory_account: "Company",
buying_cost_center: "Company",
selling_cost_center: "Company",
expense_account: "Company",
income_account: "Company",
default_provisional_account: "Company",
purchase_expense_account: "Company",
default_cogs_account: "Company",
deferred_expense_account: "Company",
deferred_revenue_account: "Company",
default_price_list: null,
default_discount_account: "Company",
default_supplier: null,
purchase_expense_contra_account: "Company",
};
function populate_item_group_company_defaults(frm, cdt, cdn, row) {
const company = row.company;
frappe.call({
method: "erpnext.setup.doctype.item_group.item_group.get_company_resolved_defaults",
args: { company: company },
freeze: false,
callback: function (r) {
if (!r.message) return;
const current_row = locals[cdt][cdn];
if (!current_row || current_row.company !== company) return;
const defaults = r.message;
Object.entries(COMPANY_DEFAULTS_TO_VF).forEach(([key, vf_field]) => {
frappe.model.set_value(cdt, cdn, vf_field, defaults[key] || "—");
});
setTimeout(() => update_item_group_vf_labels(frm, cdn, defaults), 50);
},
});
}
function update_item_group_vf_labels(frm, cdn, defaults) {
const $grid_row = frm.fields_dict["item_group_defaults"].grid.wrapper.find(
`.grid-row[data-name="${cdn}"]`
);
if (!$grid_row.length) return;
Object.entries(COMPANY_DEFAULTS_TO_VF).forEach(([key, vf_field]) => {
const $label = $grid_row.find(`[data-fieldname="${vf_field}"]`).find(".control-label, label").first();
if (!$label.length) return;
if (!$label.data("base-label")) {
$label.data("base-label", $label.text().trim());
}
const base = $label.data("base-label");
const source = FIELD_DEFAULT_SOURCE[key];
$label.text(source ? `${base} (${__(source)})` : base);
});
}

View File

@@ -94,3 +94,36 @@ def get_item_group_defaults(item, company):
return row
return frappe._dict()
@frappe.whitelist()
def get_company_resolved_defaults(company: str) -> dict:
"""
Returns effective default values for a company by checking:
1. Company document
2. Stock Settings (for warehouse fallback)
3. Accounts Settings (for deferred account fallbacks)
"""
if not company:
return {}
company_doc = frappe.get_cached_doc("Company", company)
default_warehouse = frappe.db.get_single_value("Stock Settings", "default_warehouse")
return {
"default_warehouse": default_warehouse,
"default_inventory_account": company_doc.get("default_inventory_account"),
"buying_cost_center": company_doc.get("cost_center"),
"selling_cost_center": company_doc.get("cost_center"),
"expense_account": company_doc.get("default_expense_account"),
"income_account": company_doc.get("default_income_account"),
"default_provisional_account": company_doc.get("default_provisional_account"),
"purchase_expense_account": company_doc.get("purchase_expense_account"),
"default_cogs_account": company_doc.get("default_expense_account"),
"deferred_expense_account": company_doc.get("default_deferred_expense_account"),
"deferred_revenue_account": company_doc.get("default_deferred_revenue_account"),
"default_discount_account": company_doc.get("default_discount_account"),
"purchase_expense_contra_account": company_doc.get("purchase_expense_contra_account"),
"default_price_list": "",
"default_supplier": "",
}

View File

@@ -6,6 +6,24 @@ frappe.provide("erpnext.item");
const SALES_DOCTYPES = ["Quotation", "Sales Order", "Delivery Note", "Sales Invoice"];
const PURCHASE_DOCTYPES = ["Purchase Order", "Purchase Receipt", "Purchase Invoice"];
const virtual_field_map = {
default_warehouse: "vf_default_warehouse",
default_price_list: "vf_default_price_list",
default_discount_account: "vf_default_discount_account",
default_inventory_account: "vf_default_inventory_account",
buying_cost_center: "vf_buying_cost_center",
default_supplier: "vf_default_supplier",
expense_account: "vf_expense_account",
default_provisional_account: "vf_default_provisional_account",
purchase_expense_account: "vf_purchase_expense_account",
purchase_expense_contra_account: "vf_purchase_expense_contra_account",
selling_cost_center: "vf_selling_cost_center",
income_account: "vf_income_account",
default_cogs_account: "vf_default_cogs_account",
deferred_expense_account: "vf_deferred_expense_account",
deferred_revenue_account: "vf_deferred_revenue_account",
};
frappe.ui.form.on("Item", {
valuation_method(frm) {
if (!frm.is_new() && frm.doc.valuation_method === "Moving Average") {
@@ -346,6 +364,53 @@ frappe.ui.form.on("Item", {
},
});
frappe.ui.form.on("Item Default", {
form_render: function (frm, cdt, cdn) {
if (!frm.fields_dict["item_defaults"]) return;
const row = locals[cdt][cdn];
if (!row || !row.company) {
Object.values(virtual_field_map).forEach((vf) => frappe.model.set_value(cdt, cdn, vf, ""));
return;
}
const $grid_row = frm.fields_dict["item_defaults"].grid.wrapper.find(`.grid-row[data-name="${cdn}"]`);
if (!$grid_row.find(".item-defaults-desc").length) {
$grid_row.find(".grid-form-body").prepend(`
<div class="row">
<div class="col-xs-12">
<div class="item-defaults-desc" style="
background: var(--control-bg);
border-radius: var(--border-radius-sm);
padding: 6px 6px 8px 14px;
color: var(--text-muted);
">
${__(
"Left column shows inherited defaults (Item Group → Company / Stock Settings). Right column is where you set overrides for this item only."
)}
</div>
</div>
</div>
`);
}
erpnext.item.populate_virtual_fields(frm, cdt, cdn, row);
},
company: function (frm, cdt, cdn) {
if (!frm.fields_dict["item_defaults"]) return;
const row = locals[cdt][cdn];
if (!row || !row.company) {
Object.values(virtual_field_map).forEach((vf) => frappe.model.set_value(cdt, cdn, vf, ""));
return;
}
erpnext.item.populate_virtual_fields(frm, cdt, cdn, row);
},
});
frappe.ui.form.on("Item Reorder", {
reorder_levels_add: function (frm, cdt, cdn) {
var row = frappe.get_doc(cdt, cdn);
@@ -449,6 +514,93 @@ function render_serial_batch_banner(wrapper) {
}
$.extend(erpnext.item, {
populate_virtual_fields: function (frm, cdt, cdn, row) {
if (!frm.doc.item_group || !row.company) {
Object.values(virtual_field_map).forEach((vf) => frappe.model.set_value(cdt, cdn, vf, ""));
return;
}
const company = row.company;
const item_group = frm.doc.item_group;
frappe.call({
method: "frappe.client.get",
args: { doctype: "Item Group", name: frm.doc.item_group },
freeze: false,
callback: function (r) {
if (!r.message) return;
const current_row = locals[cdt][cdn];
if (!current_row || current_row.company !== company || frm.doc.item_group !== item_group)
return;
const group_defaults =
(r.message.item_group_defaults || []).find((d) => d.company === company) || {};
// Set Item Group values immediately; collect fields that need company fallback
const needs_company_fallback = [];
Object.entries(virtual_field_map).forEach(([real_field, vf_field]) => {
if (group_defaults[real_field]) {
frappe.model.set_value(cdt, cdn, vf_field, group_defaults[real_field]);
} else {
frappe.model.set_value(cdt, cdn, vf_field, "");
needs_company_fallback.push(real_field);
}
});
if (!needs_company_fallback.length) {
setTimeout(() => erpnext.item.update_vf_labels(frm, cdn, {}), 50);
return;
}
frappe.call({
method: "erpnext.setup.doctype.item_group.item_group.get_company_resolved_defaults",
args: { company: company },
freeze: false,
callback: function (cr) {
const current_row = locals[cdt][cdn];
if (
!current_row ||
current_row.company !== company ||
frm.doc.item_group !== item_group
)
return;
const company_defaults = cr.message || {};
const from_company = {};
needs_company_fallback.forEach((real_field) => {
const val = company_defaults[real_field] || "";
if (val) from_company[real_field] = val;
frappe.model.set_value(cdt, cdn, virtual_field_map[real_field], val || "—");
});
setTimeout(() => erpnext.item.update_vf_labels(frm, cdn, from_company), 50);
},
});
},
});
},
update_vf_labels: function (frm, cdn, from_company) {
const $grid_row = frm.fields_dict["item_defaults"].grid.wrapper.find(`.grid-row[data-name="${cdn}"]`);
if (!$grid_row.length) return;
Object.entries(virtual_field_map).forEach(([real_field, vf_field]) => {
const $label = $grid_row
.find(`[data-fieldname="${vf_field}"]`)
.find(".control-label, label")
.first();
if (!$label.length) return;
if (!$label.data("base-label")) {
$label.data("base-label", $label.text().trim());
}
const base = $label.data("base-label");
$label.text(from_company[real_field] ? `${base} (Company)` : `${base} (Item Group)`);
});
},
setup_queries: function (frm) {
frm.fields_dict["item_defaults"].grid.get_field("expense_account").get_query = function (
doc,

View File

@@ -5,34 +5,60 @@
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
"company_section",
"company",
"from_section",
"column_break_general",
"vf_default_warehouse",
"vf_default_price_list",
"vf_default_discount_account",
"vf_default_inventory_account",
"column_break_njfg",
"default_warehouse",
"column_break_3",
"default_price_list",
"default_discount_account",
"default_inventory_account",
"inventory_account_currency",
"purchase_defaults",
"column_break_purchase",
"vf_buying_cost_center",
"vf_default_supplier",
"vf_expense_account",
"vf_default_provisional_account",
"vf_purchase_expense_account",
"vf_purchase_expense_contra_account",
"column_break_ghzl",
"buying_cost_center",
"default_supplier",
"column_break_8",
"expense_account",
"default_provisional_account",
"column_break_cpif",
"purchase_expense_account",
"purchase_expense_contra_account",
"selling_defaults",
"column_break_sales",
"vf_selling_cost_center",
"vf_income_account",
"column_break_ilsl",
"selling_cost_center",
"column_break_12",
"income_account",
"cost_of_good_sold_section",
"column_break_cogs",
"vf_default_cogs_account",
"column_break_vnkl",
"default_cogs_account",
"deferred_accounting_defaults_section",
"column_break_deferred",
"vf_deferred_expense_account",
"vf_deferred_revenue_account",
"column_break_okiu",
"deferred_expense_account",
"column_break_kwad",
"deferred_revenue_account"
],
"fields": [
{
"fieldname": "company_section",
"fieldtype": "Section Break"
},
{
"fieldname": "company",
"fieldtype": "Link",
@@ -46,157 +72,30 @@
"fieldname": "default_warehouse",
"fieldtype": "Link",
"in_list_view": 1,
"label": "Default Warehouse",
"label": "Warehouse",
"options": "Warehouse",
"search_index": 1
},
{
"fieldname": "column_break_3",
"fieldtype": "Column Break"
},
{
"description": "Default price list for buying or selling this item",
"fieldname": "default_price_list",
"fieldtype": "Link",
"in_list_view": 1,
"label": "Default Price List",
"label": "Price List",
"options": "Price List",
"show_description_on_click": 1
},
{
"fieldname": "purchase_defaults",
"fieldtype": "Section Break",
"label": "Purchase Defaults"
},
{
"description": "Cost center used for tracking purchase expenses for this item",
"fieldname": "buying_cost_center",
"fieldtype": "Link",
"label": "Default Buying Cost Center",
"options": "Cost Center",
"show_description_on_click": 1
},
{
"description": "This supplier will be auto-selected in new purchase transactions",
"fieldname": "default_supplier",
"fieldtype": "Link",
"label": "Default Supplier",
"options": "Supplier"
},
{
"fieldname": "column_break_8",
"fieldtype": "Column Break"
},
{
"description": "Account where the cost of this item will be debited on purchase",
"fieldname": "expense_account",
"fieldtype": "Link",
"label": "Default Expense Account",
"options": "Account",
"show_description_on_click": 1
},
{
"fieldname": "selling_defaults",
"fieldtype": "Section Break",
"label": "Sales Defaults"
},
{
"description": "Cost center used for tracking sales revenue for this item",
"fieldname": "selling_cost_center",
"fieldtype": "Link",
"label": "Default Selling Cost Center",
"options": "Cost Center",
"show_description_on_click": 1
},
{
"fieldname": "column_break_12",
"fieldtype": "Column Break"
},
{
"description": "Account where revenue from selling this item will be credited",
"fieldname": "income_account",
"fieldtype": "Link",
"label": "Default Income Account",
"options": "Account",
"show_description_on_click": 1
},
{
"fieldname": "default_discount_account",
"fieldtype": "Link",
"label": "Default Discount Account",
"options": "Account"
},
{
"description": "Provisional liability account used for service items before invoice is received",
"fieldname": "default_provisional_account",
"fieldtype": "Link",
"label": "Default Provisional Account (Service)",
"options": "Account"
},
{
"fieldname": "deferred_accounting_defaults_section",
"fieldtype": "Section Break",
"label": "Deferred Accounting Defaults"
},
{
"depends_on": "eval: parent.enable_deferred_expense",
"description": "When you pay for something upfront (like annual insurance), the cost is held here and recognized gradually over time",
"fieldname": "deferred_expense_account",
"fieldtype": "Link",
"label": "Deferred Expense Account",
"options": "Account",
"show_description_on_click": 1
},
{
"depends_on": "eval: parent.enable_deferred_revenue",
"description": "Revenue received in advance (e.g. annual subscription) is held here and recognized gradually over time",
"fieldname": "deferred_revenue_account",
"fieldtype": "Link",
"label": "Deferred Revenue Account",
"options": "Account",
"show_description_on_click": 1
},
{
"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"
},
{
"description": "Account where cost of goods sold will be posted when this item is sold",
"fieldname": "default_cogs_account",
"fieldtype": "Link",
"label": "Default COGS Account",
"options": "Account",
"show_description_on_click": 1
},
{
"description": "Account to record additional purchase expenses like freight or customs for this item",
"fieldname": "purchase_expense_account",
"fieldtype": "Link",
"label": "Purchase Expense Account",
"options": "Account",
"show_description_on_click": 1
},
{
"description": "Used to balance the books when recording extra purchase costs like freight or customs",
"fieldname": "purchase_expense_contra_account",
"fieldtype": "Link",
"label": "Purchase Expense Contra Account",
"label": "Discount Account",
"options": "Account"
},
{
"description": "Stock account where inventory value for this item will be tracked",
"fieldname": "default_inventory_account",
"fieldtype": "Link",
"label": "Default Inventory Account",
"label": "Inventory Account",
"options": "Account",
"show_description_on_click": 1
},
@@ -207,11 +106,257 @@
"label": "Inventory Account Currency",
"options": "Currency",
"read_only": 1
},
{
"fieldname": "column_break_general",
"fieldtype": "Column Break",
"label": "Inherited Default"
},
{
"fieldname": "vf_default_warehouse",
"fieldtype": "Read Only",
"is_virtual": 1,
"label": "Warehouse"
},
{
"fieldname": "vf_default_price_list",
"fieldtype": "Read Only",
"is_virtual": 1,
"label": "Price List"
},
{
"fieldname": "vf_default_discount_account",
"fieldtype": "Read Only",
"is_virtual": 1,
"label": "Discount Account"
},
{
"fieldname": "vf_default_inventory_account",
"fieldtype": "Read Only",
"is_virtual": 1,
"label": "Inventory Account"
},
{
"fieldname": "purchase_defaults",
"fieldtype": "Section Break",
"label": "Purchase Defaults"
},
{
"description": "Cost center used for tracking purchase expenses for this item",
"fieldname": "buying_cost_center",
"fieldtype": "Link",
"label": "Buying Cost Center",
"options": "Cost Center",
"show_description_on_click": 1
},
{
"description": "This supplier will be auto-selected in new purchase transactions",
"fieldname": "default_supplier",
"fieldtype": "Link",
"label": "Supplier",
"options": "Supplier",
"show_description_on_click": 1
},
{
"description": "Account where the cost of this item will be debited on purchase",
"fieldname": "expense_account",
"fieldtype": "Link",
"label": "Expense Account",
"options": "Account",
"show_description_on_click": 1
},
{
"description": "Provisional liability account used for service items before invoice is received",
"fieldname": "default_provisional_account",
"fieldtype": "Link",
"label": "Provisional Account (Service)",
"options": "Account",
"show_description_on_click": 1
},
{
"description": "Account to record additional purchase expenses like freight or customs",
"fieldname": "purchase_expense_account",
"fieldtype": "Link",
"label": "Purchase Expense Account",
"options": "Account",
"show_description_on_click": 1
},
{
"description": "Used to balance the books when recording extra purchase costs",
"fieldname": "purchase_expense_contra_account",
"fieldtype": "Link",
"label": "Purchase Expense Contra Account",
"options": "Account",
"show_description_on_click": 1
},
{
"fieldname": "column_break_purchase",
"fieldtype": "Column Break"
},
{
"fieldname": "vf_buying_cost_center",
"fieldtype": "Read Only",
"is_virtual": 1,
"label": "Buying Cost Center"
},
{
"fieldname": "vf_default_supplier",
"fieldtype": "Read Only",
"is_virtual": 1,
"label": "Supplier"
},
{
"fieldname": "vf_expense_account",
"fieldtype": "Read Only",
"is_virtual": 1,
"label": "Expense Account"
},
{
"fieldname": "vf_default_provisional_account",
"fieldtype": "Read Only",
"is_virtual": 1,
"label": "Provisional Account (Service)"
},
{
"fieldname": "vf_purchase_expense_account",
"fieldtype": "Read Only",
"is_virtual": 1,
"label": "Purchase Expense Account"
},
{
"fieldname": "vf_purchase_expense_contra_account",
"fieldtype": "Read Only",
"is_virtual": 1,
"label": "Purchase Expense Contra Account"
},
{
"fieldname": "selling_defaults",
"fieldtype": "Section Break",
"label": "Sales Defaults"
},
{
"description": "Cost center used for tracking sales revenue for this item",
"fieldname": "selling_cost_center",
"fieldtype": "Link",
"label": "Selling Cost Center",
"options": "Cost Center",
"show_description_on_click": 1
},
{
"description": "Account where revenue from selling this item will be credited",
"fieldname": "income_account",
"fieldtype": "Link",
"label": "Income Account",
"options": "Account",
"show_description_on_click": 1
},
{
"fieldname": "column_break_sales",
"fieldtype": "Column Break"
},
{
"fieldname": "vf_selling_cost_center",
"fieldtype": "Read Only",
"is_virtual": 1,
"label": "Selling Cost Center"
},
{
"fieldname": "vf_income_account",
"fieldtype": "Read Only",
"is_virtual": 1,
"label": "Income Account"
},
{
"fieldname": "cost_of_good_sold_section",
"fieldtype": "Section Break",
"label": "Cost of Goods Sold"
},
{
"description": "Account where cost of goods sold will be posted when this item is sold",
"fieldname": "default_cogs_account",
"fieldtype": "Link",
"label": "COGS Account",
"options": "Account",
"show_description_on_click": 1
},
{
"fieldname": "column_break_cogs",
"fieldtype": "Column Break"
},
{
"fieldname": "vf_default_cogs_account",
"fieldtype": "Read Only",
"is_virtual": 1,
"label": "COGS Account"
},
{
"fieldname": "deferred_accounting_defaults_section",
"fieldtype": "Section Break",
"label": "Deferred Accounting Defaults"
},
{
"depends_on": "eval: parent.enable_deferred_expense",
"fieldname": "deferred_expense_account",
"fieldtype": "Link",
"label": "Expense Account",
"options": "Account",
"show_description_on_click": 1
},
{
"depends_on": "eval: parent.enable_deferred_revenue",
"fieldname": "deferred_revenue_account",
"fieldtype": "Link",
"label": "Revenue Account",
"options": "Account",
"show_description_on_click": 1
},
{
"fieldname": "column_break_deferred",
"fieldtype": "Column Break"
},
{
"depends_on": "eval: parent.enable_deferred_expense",
"fieldname": "vf_deferred_expense_account",
"fieldtype": "Read Only",
"is_virtual": 1,
"label": "Deferred Expense Account"
},
{
"depends_on": "eval: parent.enable_deferred_revenue",
"fieldname": "vf_deferred_revenue_account",
"fieldtype": "Read Only",
"is_virtual": 1,
"label": "Deferred Revenue Account"
},
{
"fieldname": "column_break_njfg",
"fieldtype": "Column Break",
"label": "Item Override"
},
{
"fieldname": "column_break_ghzl",
"fieldtype": "Column Break"
},
{
"fieldname": "column_break_ilsl",
"fieldtype": "Column Break"
},
{
"fieldname": "column_break_vnkl",
"fieldtype": "Column Break"
},
{
"fieldname": "column_break_okiu",
"fieldtype": "Column Break"
},
{
"fieldname": "from_section",
"fieldtype": "Section Break"
}
],
"istable": 1,
"links": [],
"modified": "2026-04-27 01:49:01.396845",
"modified": "2026-06-03 17:25:35.982082",
"modified_by": "Administrator",
"module": "Stock",
"name": "Item Default",