From ce1ee98a1210531d8372f718552f70e7a28eb37e Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Wed, 6 Nov 2024 13:30:27 +0530 Subject: [PATCH 01/17] refactor: moving item code trigger to server side 1 to 1 barebones migration --- erpnext/public/js/controllers/transaction.js | 19 +- erpnext/utilities/transaction_base.py | 262 +++++++++++++++++++ 2 files changed, 280 insertions(+), 1 deletion(-) diff --git a/erpnext/public/js/controllers/transaction.js b/erpnext/public/js/controllers/transaction.js index a4f65a25f2f..67f7f9db11c 100644 --- a/erpnext/public/js/controllers/transaction.js +++ b/erpnext/public/js/controllers/transaction.js @@ -497,8 +497,25 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe } item_code(doc, cdt, cdn) { - var me = this; var item = frappe.get_doc(cdt, cdn); + frappe.call({ + doc: doc, + method: "item_code_trigger", + args: { + item: item + }, + callback: function(r) { + if(!r.exc) { + cur_frm.refresh_fields(); + refresh_field("items"); + } + } + }); + + return; + + var me = this; + var update_stock = 0, show_batch_dialog = 0; item.weight_per_unit = 0; diff --git a/erpnext/utilities/transaction_base.py b/erpnext/utilities/transaction_base.py index 2e4bdac6aab..f29c59f809d 100644 --- a/erpnext/utilities/transaction_base.py +++ b/erpnext/utilities/transaction_base.py @@ -8,6 +8,8 @@ from frappe import _ from frappe.utils import cint, flt, get_time, now_datetime from erpnext.controllers.status_updater import StatusUpdater +from erpnext.stock.get_item_details import get_item_details +from erpnext.stock.utils import get_incoming_rate class UOMMustBeIntegerError(frappe.ValidationError): @@ -231,6 +233,266 @@ class TransactionBase(StatusUpdater): ) ) + @frappe.whitelist() + def item_code_trigger(self, item): + # 'item' - child table row from UI. Possibly has user-set values + # Convert it to Frappe doc for better attribute access + item = frappe.get_doc(item) + + # Server side 'item' doc. Update this to reflect in UI + item_obj = self.get("items", {"name": item.name})[0] + + # 'item_details' has values fetched by system for backend + item_details = get_item_details( + frappe._dict( + { + "item_code": item.get("item_code"), + "barcode": item.get("barcode"), + "serial_no": item.get("serial_no"), + "batch_no": item.get("batch_no"), + "set_warehouse": self.get("set_warehouse"), + "warehouse": item.get("warehouse"), + "customer": self.get("customer") or self.get("party_name"), + "quotation_to": self.get("quotation_to"), + "supplier": self.get("supplier"), + "currency": self.get("currency"), + "is_internal_supplier": self.get("is_internal_supplier"), + "is_internal_customer": self.get("is_internal_customer"), + "update_stock": self.update_stock + if self.doctype in ["Purchase Invoice", "Sales Invoice"] + else False, + "conversion_rate": self.get("conversion_rate"), + "price_list": self.get("selling_price_list") or self.get("buying_price_list"), + "price_list_currency": self.get("price_list_currency"), + "plc_conversion_rate": self.get("plc_conversion_rate"), + "company": self.get("company"), + "order_type": self.get("order_type"), + "is_pos": cint(self.get("is_pos")), + "is_return": cint(self.get("is_return)")), + "is_subcontracted": self.get("is_subcontracted"), + "ignore_pricing_rule": self.get("ignore_pricing_rule"), + "doctype": self.get("doctype"), + "name": self.get("name"), + "project": item.get("project") or self.get("project"), + "qty": item.get("qty") or 1, + "net_rate": item.get("rate"), + "base_net_rate": item.get("base_net_rate"), + "stock_qty": item.get("stock_qty"), + "conversion_factor": item.get("conversion_factor"), + "weight_per_unit": item.get("weight_per_unit"), + "uom": item.get("uom"), + "weight_uom": item.get("weight_uom"), + "manufacturer": item.get("manufacturer"), + "stock_uom": item.get("stock_uom"), + "pos_profile": self.get("pos_profile") if cint(self.get("is_pos")) else "", + "cost_center": item.get("cost_center"), + "tax_category": self.get("tax_category"), + "item_tax_template": item.get("item_tax_template"), + "child_doctype": item.get("doctype"), + "child_docname": item.get("name"), + "is_old_subcontracting_flow": self.get("is_old_subcontracting_flow"), + } + ) + ) + + self.set_fetched_values(item_obj, item_details) + self.set_item_rate_and_discounts(item, item_obj, item_details) + + self.add_taxes_from_item_template(item, item_obj, item_details) + self.add_free_item(item, item_obj, item_details) + return + + # self.handle_internal_parties(item, item_details) + # if self.get("is_internal_customer") or self.get("is_internal_supplier"): + # TODO: this is already called in handle_internal_parties() -> price_list_rate, Remove? + # self.calculate_taxes_and_totals() + + def set_fetched_values(self, item_obj: object, item_details: dict) -> None: + for k, v in item_details.items(): + if hasattr(item_obj, k): + setattr(item_obj, k, v) + + def handle_internal_parties(self, item, item_details): + if ( + self.get("is_internal_customer") or self.get("is_internal_supplier") + ) and self.represents_company == self.company: + args = frappe._dict( + { + "item_code": item.item_code, + "warehouse": item.from_warehouse + if self.doctype in ["Purchase Receipt", "Purchase Invoice"] + else item.warehouse, + "posting_date": self.posting_date, + "posting_time": self.posting_time, + "qty": item.qty * item.conversion_factor, + "serial_no": item.serial_no, + "batch_no": item.batch_no, + "voucher_type": self.doctype, + "company": self.company, + "allow_zero_valuation_rate": item.allow_zero_valuation_rate, + } + ) + rate = get_incoming_rate(args=args) + item.rate = rate * item.conversion_factor + else: + self.price_list_rate(item, item_details) + + def add_taxes_from_item_template(self, item: object, item_obj: object, item_details: dict) -> None: + if item_details.item_tax_rate and frappe.db.get_single_value( + "Accounts Settings", "add_taxes_from_item_tax_template" + ): + item_tax_template = frappe.json.loads(item_details.item_tax_rate) + for tax_head, rate in item_tax_template.items(): + found = [x for x in self.taxes if x.account_head == tax_head] + if not found: + self.append("taxes", {"charge_type": "On Net Total", "account_head": tax_head, "rate": 0}) + + def price_list_rate(self, item, item_details): + if item.doctype in [ + "Quotation Item", + "Sales Order Item", + "Delivery Note Item", + "Sales Invoice Item", + "POS Invoice Item", + "Purchase Invoice Item", + "Purchase Order Item", + "Purchase Receipt Item", + ]: + # self.apply_pricing_rule_on_item(item, item_details) + self.apply_pricing_rule_on_item(item) + else: + item.rate = flt( + item.price_list_rate * (1 - item.discount_percentage / 100.0), item.precision("rate") + ) + self.calculate_taxes_and_totals() + + def copy_from_first_row(self, row, fields): + if self.items and row: + # TODO: find a alternate mechanism for setting dimensions + fields.append("cost_center") + first_row = self.items[0] + [setattr(row, k, first_row.get(k)) for k in fields if hasattr(first_row, k)] + + def add_free_item(self, item: object, item_obj: object, item_details: dict) -> None: + free_items = item_details.get("free_item_data") + if free_items and len(free_items): + existing_free_items = [x for x in self.items if x.is_free_item] + existing_items = [ + {"item_code": x.item_code, "pricing_rules": x.pricing_rules} for x in self.items + ] + + for free_item in free_items: + _matches = [ + x + for x in existing_free_items + if x.item_code == free_item.get('item_code') and x.pricing_rules == free_item.get('pricing_rules') + ] + if _matches: + row_to_modify = _matches[0] + else: + row_to_modify = self.append("items") + + for k, v in free_item.items(): + setattr(row_to_modify, k, free_item.get(k)) + + self.copy_from_first_row(row_to_modify, ["expense_account", "income_account"]) + + def conversion_factor(self): + if frappe.get_meta(item.doctype).has_field("stock_qty"): + # frappe.model.round_floats_in(item, ["qty", "conversion_factor"]); + item.stock_qty = flt(item.qty * item.conversion_factor, item.precision("stock_qty")) + + # this.toggle_conversion_factor(item); + if self.doctype != "Material Request": + item.total_weight = flt(item.stock_qty * item.weight_per_unit) + self.calculate_net_weight() + + # TODO: for handling customization not to fetch price list rate + if frappe.flags.dont_fetch_price_list_rate: + return + + if not dont_fetch_price_list_rate and frappe.meta.has_field(doc.doctype, "price_list_currency"): + self.apply_price_list(item, true) + self.calculate_stock_uom_rate(doc, cdt, cdn) + + def set_item_rate_and_discounts(self, item: object, item_obj: object, item_details: dict) -> None: + effective_item_rate = item_details.price_list_rate + item_rate = item_details.rate + + # Field order precedance + # blanket_order_rate -> margin_type -> discount_percentage -> discount_amount + if item.parenttype in ["Sales Order", "Quotation"] and item.blanket_order_rate: + effective_item_rate = item.blanket_order_rate + + if item.margin_type == "Percentage": + item_obj.rate_with_margin = flt(effective_item_rate) + flt(effective_item_rate) * ( + flt(item.margin_rate_or_amount) / 100 + ) + else: + item_obj.rate_with_margin = flt(effective_item_rate) + flt(item.margin_rate_or_amount) + + item_obj.base_rate_with_margin = flt(item_obj.rate_with_margin) * flt(self.conversion_rate) + item_rate = flt(item_obj.rate_with_margin, item_obj.precision("rate")) + + if item.discount_percentage and not item.discount_amount: + item_obj.discount_amount = flt(item_obj.rate_with_margin) * flt(item.discount_percentage) / 100 + + if item.discount_amount and item.discount_amount > 0: + item_rate = flt((item_obj.rate_with_margin) - (item_obj.discount_amount), item.precision("rate")) + item_obj.discount_percentage = ( + 100 * flt(item_obj.discount_amount) / flt(item_obj.rate_with_margin) + ) + + item_obj.rate = item_rate + + def calculate_net_weight(self): + self.total_net_weight = sum([x.total_weight for x in self.items]) + self.apply_shipping_rule() + + def apply_price_list(self, item, reset_plc_conversion): + # We need to reset plc_conversion_rate sometimes because the call to + # `erpnext.stock.get_item_details.apply_price_list` is sensitive to its value + + if self.doctype == "Material Request": + return + + if not reset_plc_conversion: + self.plc_conversion_rate = "" + + if not (item.items or item.price_list): + return + + if self.in_apply_price_list: + return + + self.in_apply_price_list = True + # return this.frm.call({ + # method: "erpnext.stock.get_item_details.apply_price_list", + # args: { args: args, doc: me.frm.doc }, + # callback: function(r) { + # if (!r.exc) { + # frappe.run_serially([ + # () => me.frm.set_value("price_list_currency", r.message.parent.price_list_currency), + # () => me.frm.set_value("plc_conversion_rate", r.message.parent.plc_conversion_rate), + # () => { + # if(args.items.length) { + # me._set_values_for_item_list(r.message.children); + # $.each(r.message.children || [], function(i, d) { + # me.apply_discount_on_item(d, d.doctype, d.name, 'discount_percentage'); + # }); + # } + # }, + # () => { me.in_apply_price_list = false; } + # ]); + + # } else { + # me.in_apply_price_list = false; + # } + # } + # }).always(() => { + # me.in_apply_price_list = false; + # }); + def delete_events(ref_type, ref_name): events = ( From 600d92100cd9112eff78ec8c9d493ce5d18e59a6 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Mon, 25 Nov 2024 14:27:46 +0530 Subject: [PATCH 02/17] refactor: use helper method to fetch item details --- erpnext/utilities/transaction_base.py | 28 +++++++++++++++------------ 1 file changed, 16 insertions(+), 12 deletions(-) diff --git a/erpnext/utilities/transaction_base.py b/erpnext/utilities/transaction_base.py index f29c59f809d..8b459314b56 100644 --- a/erpnext/utilities/transaction_base.py +++ b/erpnext/utilities/transaction_base.py @@ -233,17 +233,8 @@ class TransactionBase(StatusUpdater): ) ) - @frappe.whitelist() - def item_code_trigger(self, item): - # 'item' - child table row from UI. Possibly has user-set values - # Convert it to Frappe doc for better attribute access - item = frappe.get_doc(item) - - # Server side 'item' doc. Update this to reflect in UI - item_obj = self.get("items", {"name": item.name})[0] - - # 'item_details' has values fetched by system for backend - item_details = get_item_details( + def fetch_item_details(self, item: dict) -> dict: + return get_item_details( frappe._dict( { "item_code": item.get("item_code"), @@ -295,6 +286,18 @@ class TransactionBase(StatusUpdater): ) ) + @frappe.whitelist() + def item_code_trigger(self, item): + # 'item' - child table row from UI. Possibly has user-set values + # Convert it to Frappe doc for better attribute access + item = frappe.get_doc(item) + + # Server side 'item' doc. Update this to reflect in UI + item_obj = self.get("items", {"name": item.name})[0] + + # 'item_details' has values fetched by system for backend + item_details = self.fetch_item_details(item) + self.set_fetched_values(item_obj, item_details) self.set_item_rate_and_discounts(item, item_obj, item_details) @@ -385,7 +388,8 @@ class TransactionBase(StatusUpdater): _matches = [ x for x in existing_free_items - if x.item_code == free_item.get('item_code') and x.pricing_rules == free_item.get('pricing_rules') + if x.item_code == free_item.get("item_code") + and x.pricing_rules == free_item.get("pricing_rules") ] if _matches: row_to_modify = _matches[0] From 88e6b572a870b00e1ee93a0186d75b3f48651785 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Mon, 25 Nov 2024 14:40:28 +0530 Subject: [PATCH 03/17] refactor: move remaining logic Handle internal parties conversion factor and applying pricing list rate --- erpnext/utilities/transaction_base.py | 133 ++++++++++++-------------- 1 file changed, 61 insertions(+), 72 deletions(-) diff --git a/erpnext/utilities/transaction_base.py b/erpnext/utilities/transaction_base.py index 8b459314b56..2b6cc2d40b2 100644 --- a/erpnext/utilities/transaction_base.py +++ b/erpnext/utilities/transaction_base.py @@ -300,22 +300,18 @@ class TransactionBase(StatusUpdater): self.set_fetched_values(item_obj, item_details) self.set_item_rate_and_discounts(item, item_obj, item_details) - self.add_taxes_from_item_template(item, item_obj, item_details) self.add_free_item(item, item_obj, item_details) - return - - # self.handle_internal_parties(item, item_details) - # if self.get("is_internal_customer") or self.get("is_internal_supplier"): - # TODO: this is already called in handle_internal_parties() -> price_list_rate, Remove? - # self.calculate_taxes_and_totals() + self.handle_internal_parties(item, item_obj, item_details) + self.conversion_factor(item, item_obj, item_details) + self.calculate_taxes_and_totals() def set_fetched_values(self, item_obj: object, item_details: dict) -> None: for k, v in item_details.items(): if hasattr(item_obj, k): setattr(item_obj, k, v) - def handle_internal_parties(self, item, item_details): + def handle_internal_parties(self, item: object, item_obj: object, item_details: dict) -> None: if ( self.get("is_internal_customer") or self.get("is_internal_supplier") ) and self.represents_company == self.company: @@ -336,38 +332,25 @@ class TransactionBase(StatusUpdater): } ) rate = get_incoming_rate(args=args) - item.rate = rate * item.conversion_factor + item_obj.rate = rate * item.conversion_factor else: - self.price_list_rate(item, item_details) + self.set_rate_based_on_price_list(item, item_obj, item_details) def add_taxes_from_item_template(self, item: object, item_obj: object, item_details: dict) -> None: if item_details.item_tax_rate and frappe.db.get_single_value( "Accounts Settings", "add_taxes_from_item_tax_template" ): item_tax_template = frappe.json.loads(item_details.item_tax_rate) - for tax_head, rate in item_tax_template.items(): + for tax_head, _rate in item_tax_template.items(): found = [x for x in self.taxes if x.account_head == tax_head] if not found: self.append("taxes", {"charge_type": "On Net Total", "account_head": tax_head, "rate": 0}) - def price_list_rate(self, item, item_details): - if item.doctype in [ - "Quotation Item", - "Sales Order Item", - "Delivery Note Item", - "Sales Invoice Item", - "POS Invoice Item", - "Purchase Invoice Item", - "Purchase Order Item", - "Purchase Receipt Item", - ]: - # self.apply_pricing_rule_on_item(item, item_details) - self.apply_pricing_rule_on_item(item) - else: - item.rate = flt( + def set_rate_based_on_price_list(self, item: object, item_obj: object, item_details: dict) -> None: + if item.price_list_rate and item.discount_percentage: + item_obj.rate = flt( item.price_list_rate * (1 - item.discount_percentage / 100.0), item.precision("rate") ) - self.calculate_taxes_and_totals() def copy_from_first_row(self, row, fields): if self.items and row: @@ -380,10 +363,6 @@ class TransactionBase(StatusUpdater): free_items = item_details.get("free_item_data") if free_items and len(free_items): existing_free_items = [x for x in self.items if x.is_free_item] - existing_items = [ - {"item_code": x.item_code, "pricing_rules": x.pricing_rules} for x in self.items - ] - for free_item in free_items: _matches = [ x @@ -396,28 +375,34 @@ class TransactionBase(StatusUpdater): else: row_to_modify = self.append("items") - for k, v in free_item.items(): + for k, _v in free_item.items(): setattr(row_to_modify, k, free_item.get(k)) self.copy_from_first_row(row_to_modify, ["expense_account", "income_account"]) - def conversion_factor(self): - if frappe.get_meta(item.doctype).has_field("stock_qty"): - # frappe.model.round_floats_in(item, ["qty", "conversion_factor"]); - item.stock_qty = flt(item.qty * item.conversion_factor, item.precision("stock_qty")) + def conversion_factor(self, item: object, item_obj: object, item_details: dict) -> None: + if frappe.get_meta(item_obj.doctype).has_field("stock_qty"): + item_obj.stock_qty = flt( + item_obj.qty * item_obj.conversion_factor, item_obj.precision("stock_qty") + ) - # this.toggle_conversion_factor(item); if self.doctype != "Material Request": - item.total_weight = flt(item.stock_qty * item.weight_per_unit) + item_obj.total_weight = flt(item_obj.stock_qty * item_obj.weight_per_unit) self.calculate_net_weight() # TODO: for handling customization not to fetch price list rate if frappe.flags.dont_fetch_price_list_rate: return - if not dont_fetch_price_list_rate and frappe.meta.has_field(doc.doctype, "price_list_currency"): - self.apply_price_list(item, true) - self.calculate_stock_uom_rate(doc, cdt, cdn) + if not frappe.flags.dont_fetch_price_list_rate and frappe.get_meta(self.doctype).has_field( + "price_list_currency" + ): + self._apply_price_list(item, item_obj, True) + self.calculate_stock_uom_rate(item_obj) + + def calculate_stock_uom_rate(self, item_obj: object) -> None: + if item_obj.rate: + item_obj.stock_uom_rate = flt(item_obj.rate) / flt(item_obj.conversion_factor) def set_item_rate_and_discounts(self, item: object, item_obj: object, item_details: dict) -> None: effective_item_rate = item_details.price_list_rate @@ -453,49 +438,53 @@ class TransactionBase(StatusUpdater): self.total_net_weight = sum([x.total_weight for x in self.items]) self.apply_shipping_rule() - def apply_price_list(self, item, reset_plc_conversion): - # We need to reset plc_conversion_rate sometimes because the call to - # `erpnext.stock.get_item_details.apply_price_list` is sensitive to its value - + def _apply_price_list(self, item: object, item_obj: object, reset_plc_conversion: bool) -> None: if self.doctype == "Material Request": return if not reset_plc_conversion: self.plc_conversion_rate = "" - if not (item.items or item.price_list): + if not self.items or not (item_obj.get("selling_price_list") or item_obj.get("buying_price_list")): return - if self.in_apply_price_list: + if self.get("in_apply_price_list"): return self.in_apply_price_list = True - # return this.frm.call({ - # method: "erpnext.stock.get_item_details.apply_price_list", - # args: { args: args, doc: me.frm.doc }, - # callback: function(r) { - # if (!r.exc) { - # frappe.run_serially([ - # () => me.frm.set_value("price_list_currency", r.message.parent.price_list_currency), - # () => me.frm.set_value("plc_conversion_rate", r.message.parent.plc_conversion_rate), - # () => { - # if(args.items.length) { - # me._set_values_for_item_list(r.message.children); - # $.each(r.message.children || [], function(i, d) { - # me.apply_discount_on_item(d, d.doctype, d.name, 'discount_percentage'); - # }); - # } - # }, - # () => { me.in_apply_price_list = false; } - # ]); - # } else { - # me.in_apply_price_list = false; - # } - # } - # }).always(() => { - # me.in_apply_price_list = false; - # }); + from erpnext.stock.get_item_details import apply_price_list + + args = { + "items": [x.as_dict() for x in self.items], + "customer": self.customer or self.party_name, + "quotation_to": self.quotation_to, + "customer_group": self.customer_group, + "territory": self.territory, + "supplier": self.supplier, + "supplier_group": self.supplier_group, + "currency": self.currency, + "conversion_rate": self.conversion_rate, + "price_list": self.selling_price_list or self.buying_price_list, + "price_list_currency": self.price_list_currency, + "plc_conversion_rate": self.plc_conversion_rate, + "company": self.company, + "transaction_date": self.transaction_date or self.posting_date, + "campaign": self.campaign, + "sales_partner": self.sales_partner, + "ignore_pricing_rule": self.ignore_pricing_rule, + "doctype": self.doctype, + "name": self.name, + "is_return": self.is_return, + "update_stock": self.update_stock if self.doctype in ["Sales Invoice", "Purchase Invoice"] else 0, + "conversion_factor": self.conversion_factor, + "pos_profile": self.pos_profile if self.doctype == "Sales Invoice" else "", + "coupon_code": self.coupon_code, + "is_internal_supplier": self.is_internal_supplier, + "is_internal_customer": self.is_internal_customer, + } + # TODO: test method call impact on document + apply_price_list(cts=args, as_doc=True, doc=self) def delete_events(ref_type, ref_name): From af580c9977ca9b097123ffa3b85a327783d9d7b9 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Mon, 2 Dec 2024 17:48:44 +0530 Subject: [PATCH 04/17] refactor: dynamic dimension handling and more --- erpnext/utilities/transaction_base.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/erpnext/utilities/transaction_base.py b/erpnext/utilities/transaction_base.py index 2b6cc2d40b2..70db9fd94a2 100644 --- a/erpnext/utilities/transaction_base.py +++ b/erpnext/utilities/transaction_base.py @@ -7,6 +7,7 @@ import frappe.share from frappe import _ from frappe.utils import cint, flt, get_time, now_datetime +from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import get_dimensions from erpnext.controllers.status_updater import StatusUpdater from erpnext.stock.get_item_details import get_item_details from erpnext.stock.utils import get_incoming_rate @@ -354,8 +355,7 @@ class TransactionBase(StatusUpdater): def copy_from_first_row(self, row, fields): if self.items and row: - # TODO: find a alternate mechanism for setting dimensions - fields.append("cost_center") + fields.extend([x.get("fieldname") for x in get_dimensions(True)[0]]) first_row = self.items[0] [setattr(row, k, first_row.get(k)) for k in fields if hasattr(first_row, k)] @@ -435,7 +435,7 @@ class TransactionBase(StatusUpdater): item_obj.rate = item_rate def calculate_net_weight(self): - self.total_net_weight = sum([x.total_weight for x in self.items]) + self.total_net_weight = sum([x.get("total_weight") or 0 for x in self.items]) self.apply_shipping_rule() def _apply_price_list(self, item: object, item_obj: object, reset_plc_conversion: bool) -> None: From d0153065b08fa8c5a224d93949f0f07377997dbf Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Wed, 4 Dec 2024 14:03:18 +0530 Subject: [PATCH 05/17] chore: rename method --- erpnext/public/js/controllers/transaction.js | 2 +- erpnext/utilities/transaction_base.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/erpnext/public/js/controllers/transaction.js b/erpnext/public/js/controllers/transaction.js index 67f7f9db11c..e71c7f15120 100644 --- a/erpnext/public/js/controllers/transaction.js +++ b/erpnext/public/js/controllers/transaction.js @@ -500,7 +500,7 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe var item = frappe.get_doc(cdt, cdn); frappe.call({ doc: doc, - method: "item_code_trigger", + method: "process_item_selection", args: { item: item }, diff --git a/erpnext/utilities/transaction_base.py b/erpnext/utilities/transaction_base.py index 70db9fd94a2..02257d8145c 100644 --- a/erpnext/utilities/transaction_base.py +++ b/erpnext/utilities/transaction_base.py @@ -288,7 +288,7 @@ class TransactionBase(StatusUpdater): ) @frappe.whitelist() - def item_code_trigger(self, item): + def process_item_selection(self, item): # 'item' - child table row from UI. Possibly has user-set values # Convert it to Frappe doc for better attribute access item = frappe.get_doc(item) From d68f30769ab0c79b977e9f1576b13a9b7ea8c577 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Wed, 4 Dec 2024 14:13:09 +0530 Subject: [PATCH 06/17] refactor: only use 'item_obj' --- erpnext/utilities/transaction_base.py | 28 ++++++++++++++++----------- 1 file changed, 17 insertions(+), 11 deletions(-) diff --git a/erpnext/utilities/transaction_base.py b/erpnext/utilities/transaction_base.py index 02257d8145c..c8416a5a592 100644 --- a/erpnext/utilities/transaction_base.py +++ b/erpnext/utilities/transaction_base.py @@ -300,7 +300,8 @@ class TransactionBase(StatusUpdater): item_details = self.fetch_item_details(item) self.set_fetched_values(item_obj, item_details) - self.set_item_rate_and_discounts(item, item_obj, item_details) + self.set_item_rate_and_discounts(item_obj, item_details) + # self.set_item_rate_and_discounts(item, item_obj, item_details) self.add_taxes_from_item_template(item, item_obj, item_details) self.add_free_item(item, item_obj, item_details) self.handle_internal_parties(item, item_obj, item_details) @@ -404,30 +405,35 @@ class TransactionBase(StatusUpdater): if item_obj.rate: item_obj.stock_uom_rate = flt(item_obj.rate) / flt(item_obj.conversion_factor) - def set_item_rate_and_discounts(self, item: object, item_obj: object, item_details: dict) -> None: + def set_item_rate_and_discounts(self, item_obj: object, item_details: dict) -> None: + # def set_item_rate_and_discounts(self, item: object, item_obj: object, item_details: dict) -> None: effective_item_rate = item_details.price_list_rate item_rate = item_details.rate # Field order precedance # blanket_order_rate -> margin_type -> discount_percentage -> discount_amount - if item.parenttype in ["Sales Order", "Quotation"] and item.blanket_order_rate: - effective_item_rate = item.blanket_order_rate + if item_obj.parenttype in ["Sales Order", "Quotation"] and item_obj.blanket_order_rate: + effective_item_rate = item_obj.blanket_order_rate - if item.margin_type == "Percentage": + if item_obj.margin_type == "Percentage": item_obj.rate_with_margin = flt(effective_item_rate) + flt(effective_item_rate) * ( - flt(item.margin_rate_or_amount) / 100 + flt(item_obj.margin_rate_or_amount) / 100 ) else: - item_obj.rate_with_margin = flt(effective_item_rate) + flt(item.margin_rate_or_amount) + item_obj.rate_with_margin = flt(effective_item_rate) + flt(item_obj.margin_rate_or_amount) item_obj.base_rate_with_margin = flt(item_obj.rate_with_margin) * flt(self.conversion_rate) item_rate = flt(item_obj.rate_with_margin, item_obj.precision("rate")) - if item.discount_percentage and not item.discount_amount: - item_obj.discount_amount = flt(item_obj.rate_with_margin) * flt(item.discount_percentage) / 100 + if item_obj.discount_percentage and not item_obj.discount_amount: + item_obj.discount_amount = ( + flt(item_obj.rate_with_margin) * flt(item_obj.discount_percentage) / 100 + ) - if item.discount_amount and item.discount_amount > 0: - item_rate = flt((item_obj.rate_with_margin) - (item_obj.discount_amount), item.precision("rate")) + if item_obj.discount_amount and item_obj.discount_amount > 0: + item_rate = flt( + (item_obj.rate_with_margin) - (item_obj.discount_amount), item_obj.precision("rate") + ) item_obj.discount_percentage = ( 100 * flt(item_obj.discount_amount) / flt(item_obj.rate_with_margin) ) From 364126d2e409df19024fde3f3be6dd033fb7cf41 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Wed, 4 Dec 2024 16:17:01 +0530 Subject: [PATCH 07/17] refactor: remove unneccesary 'item' variable --- erpnext/public/js/controllers/transaction.js | 2 +- erpnext/utilities/transaction_base.py | 54 +++++++++----------- 2 files changed, 26 insertions(+), 30 deletions(-) diff --git a/erpnext/public/js/controllers/transaction.js b/erpnext/public/js/controllers/transaction.js index e71c7f15120..0187b9b21e6 100644 --- a/erpnext/public/js/controllers/transaction.js +++ b/erpnext/public/js/controllers/transaction.js @@ -502,7 +502,7 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe doc: doc, method: "process_item_selection", args: { - item: item + item: item.name }, callback: function(r) { if(!r.exc) { diff --git a/erpnext/utilities/transaction_base.py b/erpnext/utilities/transaction_base.py index c8416a5a592..431ee729447 100644 --- a/erpnext/utilities/transaction_base.py +++ b/erpnext/utilities/transaction_base.py @@ -289,23 +289,18 @@ class TransactionBase(StatusUpdater): @frappe.whitelist() def process_item_selection(self, item): - # 'item' - child table row from UI. Possibly has user-set values - # Convert it to Frappe doc for better attribute access - item = frappe.get_doc(item) - # Server side 'item' doc. Update this to reflect in UI - item_obj = self.get("items", {"name": item.name})[0] + item_obj = self.get("items", {"name": item})[0] # 'item_details' has values fetched by system for backend - item_details = self.fetch_item_details(item) + item_details = self.fetch_item_details(item_obj) self.set_fetched_values(item_obj, item_details) self.set_item_rate_and_discounts(item_obj, item_details) - # self.set_item_rate_and_discounts(item, item_obj, item_details) - self.add_taxes_from_item_template(item, item_obj, item_details) - self.add_free_item(item, item_obj, item_details) - self.handle_internal_parties(item, item_obj, item_details) - self.conversion_factor(item, item_obj, item_details) + self.add_taxes_from_item_template(item_obj, item_details) + self.add_free_item(item_obj, item_details) + self.handle_internal_parties(item_obj, item_details) + self.conversion_factor(item_obj, item_details) self.calculate_taxes_and_totals() def set_fetched_values(self, item_obj: object, item_details: dict) -> None: @@ -313,32 +308,32 @@ class TransactionBase(StatusUpdater): if hasattr(item_obj, k): setattr(item_obj, k, v) - def handle_internal_parties(self, item: object, item_obj: object, item_details: dict) -> None: + def handle_internal_parties(self, item_obj: object, item_details: dict) -> None: if ( self.get("is_internal_customer") or self.get("is_internal_supplier") ) and self.represents_company == self.company: args = frappe._dict( { - "item_code": item.item_code, - "warehouse": item.from_warehouse + "item_code": item_obj.item_code, + "warehouse": item_obj.from_warehouse if self.doctype in ["Purchase Receipt", "Purchase Invoice"] - else item.warehouse, + else item_obj.warehouse, "posting_date": self.posting_date, "posting_time": self.posting_time, - "qty": item.qty * item.conversion_factor, - "serial_no": item.serial_no, - "batch_no": item.batch_no, + "qty": item_obj.qty * item_obj.conversion_factor, + "serial_no": item_obj.serial_no, + "batch_no": item_obj.batch_no, "voucher_type": self.doctype, "company": self.company, - "allow_zero_valuation_rate": item.allow_zero_valuation_rate, + "allow_zero_valuation_rate": item_obj.allow_zero_valuation_rate, } ) rate = get_incoming_rate(args=args) - item_obj.rate = rate * item.conversion_factor + item_obj.rate = rate * item_obj.conversion_factor else: - self.set_rate_based_on_price_list(item, item_obj, item_details) + self.set_rate_based_on_price_list(item_obj, item_details) - def add_taxes_from_item_template(self, item: object, item_obj: object, item_details: dict) -> None: + def add_taxes_from_item_template(self, item_obj: object, item_details: dict) -> None: if item_details.item_tax_rate and frappe.db.get_single_value( "Accounts Settings", "add_taxes_from_item_tax_template" ): @@ -348,10 +343,11 @@ class TransactionBase(StatusUpdater): if not found: self.append("taxes", {"charge_type": "On Net Total", "account_head": tax_head, "rate": 0}) - def set_rate_based_on_price_list(self, item: object, item_obj: object, item_details: dict) -> None: - if item.price_list_rate and item.discount_percentage: + def set_rate_based_on_price_list(self, item_obj: object, item_details: dict) -> None: + if item_obj.price_list_rate and item_obj.discount_percentage: item_obj.rate = flt( - item.price_list_rate * (1 - item.discount_percentage / 100.0), item.precision("rate") + item_obj.price_list_rate * (1 - item_obj.discount_percentage / 100.0), + item_obj.precision("rate"), ) def copy_from_first_row(self, row, fields): @@ -360,7 +356,7 @@ class TransactionBase(StatusUpdater): first_row = self.items[0] [setattr(row, k, first_row.get(k)) for k in fields if hasattr(first_row, k)] - def add_free_item(self, item: object, item_obj: object, item_details: dict) -> None: + def add_free_item(self, item_obj: object, item_details: dict) -> None: free_items = item_details.get("free_item_data") if free_items and len(free_items): existing_free_items = [x for x in self.items if x.is_free_item] @@ -381,7 +377,7 @@ class TransactionBase(StatusUpdater): self.copy_from_first_row(row_to_modify, ["expense_account", "income_account"]) - def conversion_factor(self, item: object, item_obj: object, item_details: dict) -> None: + def conversion_factor(self, item_obj: object, item_details: dict) -> None: if frappe.get_meta(item_obj.doctype).has_field("stock_qty"): item_obj.stock_qty = flt( item_obj.qty * item_obj.conversion_factor, item_obj.precision("stock_qty") @@ -398,7 +394,7 @@ class TransactionBase(StatusUpdater): if not frappe.flags.dont_fetch_price_list_rate and frappe.get_meta(self.doctype).has_field( "price_list_currency" ): - self._apply_price_list(item, item_obj, True) + self._apply_price_list(item_obj, True) self.calculate_stock_uom_rate(item_obj) def calculate_stock_uom_rate(self, item_obj: object) -> None: @@ -444,7 +440,7 @@ class TransactionBase(StatusUpdater): self.total_net_weight = sum([x.get("total_weight") or 0 for x in self.items]) self.apply_shipping_rule() - def _apply_price_list(self, item: object, item_obj: object, reset_plc_conversion: bool) -> None: + def _apply_price_list(self, item_obj: object, reset_plc_conversion: bool) -> None: if self.doctype == "Material Request": return From 5b4987e160d98073671fbe722d01eb83a31c3b85 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Thu, 5 Dec 2024 10:24:23 +0530 Subject: [PATCH 08/17] chore: cleanup code --- erpnext/utilities/transaction_base.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/erpnext/utilities/transaction_base.py b/erpnext/utilities/transaction_base.py index 431ee729447..97b274da576 100644 --- a/erpnext/utilities/transaction_base.py +++ b/erpnext/utilities/transaction_base.py @@ -292,7 +292,7 @@ class TransactionBase(StatusUpdater): # Server side 'item' doc. Update this to reflect in UI item_obj = self.get("items", {"name": item})[0] - # 'item_details' has values fetched by system for backend + # 'item_details' has latest item related values item_details = self.fetch_item_details(item_obj) self.set_fetched_values(item_obj, item_details) @@ -402,7 +402,6 @@ class TransactionBase(StatusUpdater): item_obj.stock_uom_rate = flt(item_obj.rate) / flt(item_obj.conversion_factor) def set_item_rate_and_discounts(self, item_obj: object, item_details: dict) -> None: - # def set_item_rate_and_discounts(self, item: object, item_obj: object, item_details: dict) -> None: effective_item_rate = item_details.price_list_rate item_rate = item_details.rate From a06a6ccaa6967631ba6fc909063222994886716e Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Thu, 5 Dec 2024 11:59:26 +0530 Subject: [PATCH 09/17] test: auto-filling of basic details on items --- erpnext/accounts/test/accounts_mixin.py | 20 +++++++++ erpnext/utilities/test_transaction_base.py | 50 ++++++++++++++++++++++ 2 files changed, 70 insertions(+) create mode 100644 erpnext/utilities/test_transaction_base.py diff --git a/erpnext/accounts/test/accounts_mixin.py b/erpnext/accounts/test/accounts_mixin.py index f2550f738e2..ee651ee9f4c 100644 --- a/erpnext/accounts/test/accounts_mixin.py +++ b/erpnext/accounts/test/accounts_mixin.py @@ -207,3 +207,23 @@ class AccountsTestMixin: ] for doctype in doctype_list: qb.from_(qb.DocType(doctype)).delete().where(qb.DocType(doctype).company == self.company).run() + + def create_price_list(self): + pl_name = "Mixin Price List" + if not frappe.db.exists("Price List", pl_name): + self.price_list = ( + frappe.get_doc( + { + "doctype": "Price List", + "currency": "INR", + "enabled": True, + "selling": True, + "buying": True, + "price_list_name": pl_name, + } + ) + .insert() + .name + ) + else: + self.price_list = frappe.get_doc("Price List", pl_name).name diff --git a/erpnext/utilities/test_transaction_base.py b/erpnext/utilities/test_transaction_base.py new file mode 100644 index 00000000000..854d6cf0e11 --- /dev/null +++ b/erpnext/utilities/test_transaction_base.py @@ -0,0 +1,50 @@ +import frappe +from frappe import qb +from frappe.tests import IntegrationTestCase +from frappe.utils import getdate, today + +from erpnext.accounts.test.accounts_mixin import AccountsTestMixin + + +class TestAccountsController(AccountsTestMixin, IntegrationTestCase): + def setUp(self): + self.create_company() + self.create_customer() + self.create_item() + self.create_usd_receivable_account() + self.create_price_list() + self.clear_old_entries() + + def tearDown(self): + frappe.db.rollback() + + def test_01_basic_item_details(self): + # set Item Price + frappe.get_doc( + { + "doctype": "Item Price", + "item_code": self.item, + "price_list": self.price_list, + "price_list_rate": 90, + "selling": True, + "rate": 90, + "valid_from": today(), + } + ).insert() + + si = frappe.get_doc( + { + "doctype": "Sales Invoice", + "company": self.company, + "customer": self.customer, + "debit_to": self.debit_to, + "posting_date": today(), + "cost_center": self.cost_center, + "conversion_rate": 1, + "selling_price_list": self.price_list, + } + ) + itm = si.append("items") + itm.item_code = self.item + si.process_item_selection(si.items[0].name) + self.assertEqual(itm.rate, 90) From b21d5934e65d8eb35bb975a97e85c019e2f14a0b Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Fri, 6 Dec 2024 11:42:09 +0530 Subject: [PATCH 10/17] refactor: make it configurable --- .../selling_settings/selling_settings.json | 17 +++++++++++++++-- .../selling_settings/selling_settings.py | 1 + 2 files changed, 16 insertions(+), 2 deletions(-) diff --git a/erpnext/selling/doctype/selling_settings/selling_settings.json b/erpnext/selling/doctype/selling_settings/selling_settings.json index c873561df88..8203abcf2e2 100644 --- a/erpnext/selling/doctype/selling_settings/selling_settings.json +++ b/erpnext/selling/doctype/selling_settings/selling_settings.json @@ -33,7 +33,9 @@ "dont_reserve_sales_order_qty_on_sales_return", "hide_tax_id", "enable_discount_accounting", - "enable_cutoff_date_on_bulk_delivery_note_creation" + "enable_cutoff_date_on_bulk_delivery_note_creation", + "experimental_section", + "use_server_side_reactivity" ], "fields": [ { @@ -207,6 +209,17 @@ "fieldname": "enable_cutoff_date_on_bulk_delivery_note_creation", "fieldtype": "Check", "label": "Enable Cut-Off Date on Bulk Delivery Note Creation" + }, + { + "fieldname": "experimental_section", + "fieldtype": "Section Break", + "label": "Experimental" + }, + { + "default": "1", + "fieldname": "use_server_side_reactivity", + "fieldtype": "Check", + "label": "Use Server Side Reactivity" } ], "icon": "fa fa-cog", @@ -214,7 +227,7 @@ "index_web_pages_for_search": 1, "issingle": 1, "links": [], - "modified": "2024-03-27 13:10:38.633352", + "modified": "2024-12-06 11:41:54.722337", "modified_by": "Administrator", "module": "Selling", "name": "Selling Settings", diff --git a/erpnext/selling/doctype/selling_settings/selling_settings.py b/erpnext/selling/doctype/selling_settings/selling_settings.py index a4881771573..1ecb3ab9a6a 100644 --- a/erpnext/selling/doctype/selling_settings/selling_settings.py +++ b/erpnext/selling/doctype/selling_settings/selling_settings.py @@ -40,6 +40,7 @@ class SellingSettings(Document): selling_price_list: DF.Link | None so_required: DF.Literal["No", "Yes"] territory: DF.Link | None + use_server_side_reactivity: DF.Check validate_selling_price: DF.Check # end: auto-generated types From 67b28a786428a4b4138d5db4f047ab78987ce78c Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Fri, 6 Dec 2024 12:01:24 +0530 Subject: [PATCH 11/17] refactor: configurable reactivity --- erpnext/public/js/controllers/transaction.js | 84 +++++++++++--------- 1 file changed, 48 insertions(+), 36 deletions(-) diff --git a/erpnext/public/js/controllers/transaction.js b/erpnext/public/js/controllers/transaction.js index 0187b9b21e6..d56670035df 100644 --- a/erpnext/public/js/controllers/transaction.js +++ b/erpnext/public/js/controllers/transaction.js @@ -497,25 +497,36 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe } item_code(doc, cdt, cdn) { - var item = frappe.get_doc(cdt, cdn); - frappe.call({ - doc: doc, - method: "process_item_selection", - args: { - item: item.name - }, - callback: function(r) { - if(!r.exc) { - cur_frm.refresh_fields(); - refresh_field("items"); - } - } - }); - - return; - var me = this; + // Experimental: This will be removed once stability is achieved. + frappe.db.get_single_value('Selling Settings', 'use_server_side_reactivity') + .then((value) => { + if (value) { + var item = frappe.get_doc(cdt, cdn); + frappe.call({ + doc: doc, + method: "process_item_selection", + args: { + item: item.name + }, + callback: function(r) { + if(!r.exc) { + cur_frm.refresh_fields(); + refresh_field("items"); + } + } + }); + } else { + me.process_item_selection(doc, cdt, cdn); + } + }); + + } + + process_item_selection(doc, cdt, cdn) { + var item = frappe.get_doc(cdt, cdn); + var me = this; var update_stock = 0, show_batch_dialog = 0; item.weight_per_unit = 0; @@ -527,7 +538,7 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe show_batch_dialog = update_stock; } else if((this.frm.doc.doctype === 'Purchase Receipt') || - this.frm.doc.doctype === 'Delivery Note') { + this.frm.doc.doctype === 'Delivery Note') { show_batch_dialog = 1; } @@ -600,10 +611,10 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe frappe.run_serially([ () => { if (item.docstatus === 0 - && frappe.meta.has_field(item.doctype, "use_serial_batch_fields") - && !item.use_serial_batch_fields - && cint(frappe.user_defaults?.use_serial_batch_fields) === 1 - ) { + && frappe.meta.has_field(item.doctype, "use_serial_batch_fields") + && !item.use_serial_batch_fields + && cint(frappe.user_defaults?.use_serial_batch_fields) === 1 + ) { item["use_serial_batch_fields"] = 1; } }, @@ -618,7 +629,7 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe // for internal customer instead of pricing rule directly apply valuation rate on item if ((me.frm.doc.is_internal_customer || me.frm.doc.is_internal_supplier) && me.frm.doc.represents_company === me.frm.doc.company) { me.get_incoming_rate(item, me.frm.posting_date, me.frm.posting_time, - me.frm.doc.doctype, me.frm.doc.company); + me.frm.doc.doctype, me.frm.doc.company); } else { me.frm.script_manager.trigger("price_list_rate", cdt, cdn); } @@ -632,24 +643,24 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe () => { if (show_batch_dialog && !frappe.flags.trigger_from_barcode_scanner) return frappe.db.get_value("Item", item.item_code, ["has_batch_no", "has_serial_no"]) - .then((r) => { - if (r.message && - (r.message.has_batch_no || r.message.has_serial_no)) { - frappe.flags.hide_serial_batch_dialog = false; - } else { - show_batch_dialog = false; - } - }); + .then((r) => { + if (r.message && + (r.message.has_batch_no || r.message.has_serial_no)) { + frappe.flags.hide_serial_batch_dialog = false; + } else { + show_batch_dialog = false; + } + }); }, () => { // check if batch serial selector is disabled or not if (show_batch_dialog && !frappe.flags.hide_serial_batch_dialog) return frappe.db.get_single_value('Stock Settings', 'disable_serial_no_and_batch_selector') - .then((value) => { - if (value) { - frappe.flags.hide_serial_batch_dialog = true; - } - }); + .then((value) => { + if (value) { + frappe.flags.hide_serial_batch_dialog = true; + } + }); }, () => { if(show_batch_dialog && !frappe.flags.hide_serial_batch_dialog && !frappe.flags.dialog_set) { @@ -693,6 +704,7 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe } } + price_list_rate(doc, cdt, cdn) { var item = frappe.get_doc(cdt, cdn); frappe.model.round_floats_in(item, ["price_list_rate", "discount_percentage"]); From 6ea9c0c48db137dc05fa07bee7d2befd860cd56c Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Tue, 10 Dec 2024 10:50:37 +0530 Subject: [PATCH 12/17] refactor: pass config through boot --- erpnext/public/js/controllers/transaction.js | 37 +++++++++----------- erpnext/startup/boot.py | 3 ++ 2 files changed, 19 insertions(+), 21 deletions(-) diff --git a/erpnext/public/js/controllers/transaction.js b/erpnext/public/js/controllers/transaction.js index d56670035df..b3238c41153 100644 --- a/erpnext/public/js/controllers/transaction.js +++ b/erpnext/public/js/controllers/transaction.js @@ -499,29 +499,24 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe item_code(doc, cdt, cdn) { var me = this; // Experimental: This will be removed once stability is achieved. - frappe.db.get_single_value('Selling Settings', 'use_server_side_reactivity') - .then((value) => { - if (value) { - var item = frappe.get_doc(cdt, cdn); - frappe.call({ - doc: doc, - method: "process_item_selection", - args: { - item: item.name - }, - callback: function(r) { - if(!r.exc) { - cur_frm.refresh_fields(); - refresh_field("items"); - } - } - }); - } else { - me.process_item_selection(doc, cdt, cdn); + if (frappe.boot.sysdefaults.use_server_side_reactivity) { + var item = frappe.get_doc(cdt, cdn); + frappe.call({ + doc: doc, + method: "process_item_selection", + args: { + item: item.name + }, + callback: function(r) { + if(!r.exc) { + cur_frm.refresh_fields(); + refresh_field("items"); + } } - }); - + } else { + me.process_item_selection(doc, cdt, cdn); + } } process_item_selection(doc, cdt, cdn) { diff --git a/erpnext/startup/boot.py b/erpnext/startup/boot.py index 6ef0cdeee38..12de9273834 100644 --- a/erpnext/startup/boot.py +++ b/erpnext/startup/boot.py @@ -16,6 +16,9 @@ def boot_session(bootinfo): bootinfo.sysdefaults.territory = frappe.db.get_single_value("Selling Settings", "territory") bootinfo.sysdefaults.customer_group = frappe.db.get_single_value("Selling Settings", "customer_group") + bootinfo.sysdefaults.use_server_side_reactivity = frappe.db.get_single_value( + "Selling Settings", "use_server_side_reactivity" + ) bootinfo.sysdefaults.allow_stale = cint( frappe.db.get_single_value("Accounts Settings", "allow_stale") ) From 8a710f85e2f90465cc3f1d2d353bddb81ebb80ca Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Tue, 10 Dec 2024 11:14:48 +0530 Subject: [PATCH 13/17] refactor: replcate deprecated `cur_frm` --- erpnext/public/js/controllers/transaction.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/erpnext/public/js/controllers/transaction.js b/erpnext/public/js/controllers/transaction.js index b3238c41153..ea9aa95b144 100644 --- a/erpnext/public/js/controllers/transaction.js +++ b/erpnext/public/js/controllers/transaction.js @@ -509,8 +509,7 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe }, callback: function(r) { if(!r.exc) { - cur_frm.refresh_fields(); - refresh_field("items"); + me.frm.refresh_fields(); } } }); From c97e058bc6ae56892a600d34aaf7c87474a3c65e Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Tue, 10 Dec 2024 11:20:16 +0530 Subject: [PATCH 14/17] chore: rename and move test file --- .../tests/test_reactivity.py} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename erpnext/{utilities/test_transaction_base.py => controllers/tests/test_reactivity.py} (100%) diff --git a/erpnext/utilities/test_transaction_base.py b/erpnext/controllers/tests/test_reactivity.py similarity index 100% rename from erpnext/utilities/test_transaction_base.py rename to erpnext/controllers/tests/test_reactivity.py From 22c1608745719fa79241ee7faa925d0b112d17b2 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Tue, 10 Dec 2024 13:04:44 +0530 Subject: [PATCH 15/17] test: assert all reqd are set --- erpnext/controllers/tests/test_reactivity.py | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/erpnext/controllers/tests/test_reactivity.py b/erpnext/controllers/tests/test_reactivity.py index 854d6cf0e11..5e8ec4259a2 100644 --- a/erpnext/controllers/tests/test_reactivity.py +++ b/erpnext/controllers/tests/test_reactivity.py @@ -6,7 +6,7 @@ from frappe.utils import getdate, today from erpnext.accounts.test.accounts_mixin import AccountsTestMixin -class TestAccountsController(AccountsTestMixin, IntegrationTestCase): +class TestReactivity(AccountsTestMixin, IntegrationTestCase): def setUp(self): self.create_company() self.create_customer() @@ -48,3 +48,12 @@ class TestAccountsController(AccountsTestMixin, IntegrationTestCase): itm.item_code = self.item si.process_item_selection(si.items[0].name) self.assertEqual(itm.rate, 90) + + df = qb.DocType("DocField") + _res = ( + qb.from_(df).select(df.fieldname).where(df.parent.eq("Sales Invoice Item") & df.reqd.eq(1)).run() + ) + for field in _res: + with self.subTest(field=field): + self.assertIsNotNone(itm.get(field[0])) + si.save().submit() From 267d9606f8f6d6b1c327a3ce80d2b056fb72d473 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Tue, 10 Dec 2024 14:16:17 +0530 Subject: [PATCH 16/17] refactor(test): disable unwanted dimensions --- erpnext/controllers/tests/test_reactivity.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/erpnext/controllers/tests/test_reactivity.py b/erpnext/controllers/tests/test_reactivity.py index 5e8ec4259a2..73f7962836c 100644 --- a/erpnext/controllers/tests/test_reactivity.py +++ b/erpnext/controllers/tests/test_reactivity.py @@ -3,6 +3,7 @@ from frappe import qb from frappe.tests import IntegrationTestCase from frappe.utils import getdate, today +from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import disable_dimension from erpnext.accounts.test.accounts_mixin import AccountsTestMixin @@ -18,7 +19,16 @@ class TestReactivity(AccountsTestMixin, IntegrationTestCase): def tearDown(self): frappe.db.rollback() + def disable_dimensions(self): + res = frappe.db.get_all("Accounting Dimension", filters={"disabled": False}) + for x in res: + dim = frappe.get_doc("Accounting Dimension", x.name) + dim.disabled = True + dim.save() + def test_01_basic_item_details(self): + self.disable_dimensions() + # set Item Price frappe.get_doc( { From 146761010925aa2ad7cfa73f8074367f18339c05 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Tue, 10 Dec 2024 16:02:13 +0530 Subject: [PATCH 17/17] fix: linter; dont change doc after DB update --- .../selling/doctype/selling_settings/selling_settings.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/erpnext/selling/doctype/selling_settings/selling_settings.py b/erpnext/selling/doctype/selling_settings/selling_settings.py index 1ecb3ab9a6a..216a74ab688 100644 --- a/erpnext/selling/doctype/selling_settings/selling_settings.py +++ b/erpnext/selling/doctype/selling_settings/selling_settings.py @@ -70,15 +70,15 @@ class SellingSettings(Document): ) def toggle_hide_tax_id(self): - self.hide_tax_id = cint(self.hide_tax_id) + _hide_tax_id = cint(self.hide_tax_id) # Make property setters to hide tax_id fields for doctype in ("Sales Order", "Sales Invoice", "Delivery Note"): make_property_setter( - doctype, "tax_id", "hidden", self.hide_tax_id, "Check", validate_fields_for_doctype=False + doctype, "tax_id", "hidden", _hide_tax_id, "Check", validate_fields_for_doctype=False ) make_property_setter( - doctype, "tax_id", "print_hide", self.hide_tax_id, "Check", validate_fields_for_doctype=False + doctype, "tax_id", "print_hide", _hide_tax_id, "Check", validate_fields_for_doctype=False ) def toggle_editable_rate_for_bundle_items(self):