From 33366fce6c97ede0dac51cad19315c922939fffe Mon Sep 17 00:00:00 2001 From: marination Date: Fri, 28 Feb 2025 23:04:29 +0100 Subject: [PATCH 01/49] feat: Unit Price Contract (cherry picked from commit c1e4e7af28fd7005ee26eb3dbb0c1d3609fa10b3) # Conflicts: # erpnext/controllers/accounts_controller.py # erpnext/selling/doctype/quotation/quotation.json # erpnext/selling/doctype/sales_order/sales_order.py # erpnext/selling/doctype/selling_settings/selling_settings.json --- erpnext/controllers/accounts_controller.py | 9 ++++ .../selling/doctype/quotation/quotation.js | 18 +++++++ .../selling/doctype/quotation/quotation.json | 13 +++++ .../selling/doctype/quotation/quotation.py | 26 ++++++++-- .../doctype/sales_order/sales_order.js | 23 +++++++- .../doctype/sales_order/sales_order.json | 11 +++- .../doctype/sales_order/sales_order.py | 52 +++++++++++++++---- .../selling_settings/selling_settings.json | 51 ++++++++++++++++++ .../selling_settings/selling_settings.py | 2 + 9 files changed, 188 insertions(+), 17 deletions(-) diff --git a/erpnext/controllers/accounts_controller.py b/erpnext/controllers/accounts_controller.py index f576bc91541..a3180388141 100644 --- a/erpnext/controllers/accounts_controller.py +++ b/erpnext/controllers/accounts_controller.py @@ -1259,8 +1259,17 @@ class AccountsController(TransactionBase): ) def validate_qty_is_not_zero(self): +<<<<<<< HEAD if self.doctype == "Purchase Receipt": return +======= + if self.flags.allow_zero_qty: + return + + for item in self.items: + if self.doctype == "Purchase Receipt" and item.rejected_qty: + continue +>>>>>>> c1e4e7af28 (feat: Unit Price Contract) for item in self.items: if not flt(item.qty): diff --git a/erpnext/selling/doctype/quotation/quotation.js b/erpnext/selling/doctype/quotation/quotation.js index 2e88ba9e482..07869ab339b 100644 --- a/erpnext/selling/doctype/quotation/quotation.js +++ b/erpnext/selling/doctype/quotation/quotation.js @@ -35,12 +35,20 @@ frappe.ui.form.on("Quotation", { }, }; }); + + frm.set_indicator_formatter("item_code", function (doc) { + return !doc.qty && frm.doc.has_unit_price_items ? "yellow" : ""; + }); }, refresh: function (frm) { frm.trigger("set_label"); frm.trigger("set_dynamic_field_label"); + if (frm.doc.docstatus === 0) { + frm.trigger("set_unit_price_items_note"); + } + let sbb_field = frm.get_docfield("packed_items", "serial_and_batch_bundle"); if (sbb_field) { sbb_field.get_route_options_for_new_doc = (row) => { @@ -64,6 +72,16 @@ frappe.ui.form.on("Quotation", { set_label: function (frm) { frm.fields_dict.customer_address.set_label(__(frm.doc.quotation_to + " Address")); }, + + set_unit_price_items_note: function (frm) { + if (frm.doc.has_unit_price_items) { + frm.dashboard.set_headline_alert( + __("The Quotation contains Unit Price Items with 0 Qty."), + "yellow", + true + ); + } + }, }); erpnext.selling.QuotationController = class QuotationController extends erpnext.selling.SellingController { diff --git a/erpnext/selling/doctype/quotation/quotation.json b/erpnext/selling/doctype/quotation/quotation.json index 4d257ff69e7..e087256d8d4 100644 --- a/erpnext/selling/doctype/quotation/quotation.json +++ b/erpnext/selling/doctype/quotation/quotation.json @@ -21,6 +21,7 @@ "column_break1", "order_type", "company", + "has_unit_price_items", "amended_from", "currency_and_price_list", "currency", @@ -1084,13 +1085,24 @@ "label": "Company Contact Person", "options": "Contact", "print_hide": 1 + }, + { + "default": "0", + "fieldname": "has_unit_price_items", + "fieldtype": "Check", + "hidden": 1, + "label": "Has Unit Price Items" } ], "icon": "fa fa-shopping-cart", "idx": 82, "is_submittable": 1, "links": [], +<<<<<<< HEAD "modified": "2024-11-26 12:43:29.293637", +======= + "modified": "2025-02-28 18:52:44.063265", +>>>>>>> c1e4e7af28 (feat: Unit Price Contract) "modified_by": "Administrator", "module": "Selling", "name": "Quotation", @@ -1181,6 +1193,7 @@ "role": "Maintenance User" } ], + "row_format": "Dynamic", "search_fields": "status,transaction_date,party_name,order_type", "show_name_in_global_search": 1, "sort_field": "modified", diff --git a/erpnext/selling/doctype/quotation/quotation.py b/erpnext/selling/doctype/quotation/quotation.py index 48614671da2..ce74e1bafed 100644 --- a/erpnext/selling/doctype/quotation/quotation.py +++ b/erpnext/selling/doctype/quotation/quotation.py @@ -19,8 +19,6 @@ class Quotation(SellingController): from typing import TYPE_CHECKING if TYPE_CHECKING: - from frappe.types import DF - from erpnext.accounts.doctype.payment_schedule.payment_schedule import PaymentSchedule from erpnext.accounts.doctype.pricing_rule_detail.pricing_rule_detail import PricingRuleDetail from erpnext.accounts.doctype.sales_taxes_and_charges.sales_taxes_and_charges import ( @@ -32,6 +30,7 @@ class Quotation(SellingController): QuotationLostReasonDetail, ) from erpnext.stock.doctype.packed_item.packed_item import PackedItem + from frappe.types import DF additional_discount_percentage: DF.Float address_display: DF.SmallText | None @@ -67,6 +66,7 @@ class Quotation(SellingController): enq_det: DF.Text | None grand_total: DF.Currency group_same_items: DF.Check + has_unit_price_items: DF.Check ignore_pricing_rule: DF.Check in_words: DF.Data | None incoterm: DF.Link | None @@ -126,6 +126,10 @@ class Quotation(SellingController): self.indicator_color = "gray" self.indicator_title = "Expired" + def before_validate(self): + self.set_has_unit_price_items() + self.flags.allow_zero_qty = self.has_unit_price_items + def validate(self): super().validate() self.set_status() @@ -157,6 +161,17 @@ class Quotation(SellingController): if not row.is_alternative and row.name in items_with_alternatives: row.has_alternative_item = 1 + def set_has_unit_price_items(self): + """ + If permitted in settings and any item has 0 qty, the SO has unit price items. + """ + if not frappe.db.get_single_value("Selling Settings", "allow_zero_qty_in_quotation"): + return + + self.has_unit_price_items = any( + not row.qty for row in self.get("items") if (row.item_code and not row.qty) + ) + def get_ordered_status(self): status = "Open" ordered_items = frappe._dict( @@ -411,11 +426,14 @@ def _make_sales_order(source_name, target_doc=None, ignore_permissions=False): 2. If selections: Is Alternative Item/Has Alternative Item: Map if selected and adequate qty 3. If selections: Simple row: Map if adequate qty """ + # has_unit_price_items = 0 is accepted as the qty uncertain for some items + has_unit_price_items = frappe.db.get_value("Quotation", source_name, "has_unit_price_items") + balance_qty = item.qty - ordered_items.get(item.item_code, 0.0) - if balance_qty <= 0: + if balance_qty <= 0 and not has_unit_price_items: return False - has_qty = balance_qty + has_qty = balance_qty or has_unit_price_items if not selected_rows: return not item.is_alternative diff --git a/erpnext/selling/doctype/sales_order/sales_order.js b/erpnext/selling/doctype/sales_order/sales_order.js index 400acd3c6f1..6c8cc7ac1df 100644 --- a/erpnext/selling/doctype/sales_order/sales_order.js +++ b/erpnext/selling/doctype/sales_order/sales_order.js @@ -23,7 +23,16 @@ frappe.ui.form.on("Sales Order", { // formatter for material request item frm.set_indicator_formatter("item_code", function (doc) { - return doc.stock_qty <= doc.delivered_qty ? "green" : "orange"; + let color; + if (!doc.qty && frm.doc.has_unit_price_items) { + color = "yellow"; + } else if (doc.stock_qty <= doc.delivered_qty) { + color = "green"; + } else { + color = "orange"; + } + + return color; }); frm.set_query("bom_no", "items", function (doc, cdt, cdn) { @@ -97,6 +106,8 @@ frappe.ui.form.on("Sales Order", { } if (frm.doc.docstatus === 0) { + frm.trigger("set_unit_price_items_note"); + if (frm.doc.is_internal_customer) { frm.events.get_items_from_internal_purchase_order(frm); } @@ -528,6 +539,16 @@ frappe.ui.form.on("Sales Order", { }; frappe.set_route("query-report", "Reserved Stock"); }, + + set_unit_price_items_note: function (frm) { + if (frm.doc.has_unit_price_items && !frm.is_new()) { + frm.dashboard.set_headline_alert( + __("The Sales Order contains Unit Price Items with 0 Qty."), + "yellow", + true + ); + } + }, }); frappe.ui.form.on("Sales Order Item", { diff --git a/erpnext/selling/doctype/sales_order/sales_order.json b/erpnext/selling/doctype/sales_order/sales_order.json index 4db7bf6f003..5fc808798e2 100644 --- a/erpnext/selling/doctype/sales_order/sales_order.json +++ b/erpnext/selling/doctype/sales_order/sales_order.json @@ -25,6 +25,7 @@ "po_date", "company", "skip_delivery_note", + "has_unit_price_items", "amended_from", "accounting_dimensions_section", "cost_center", @@ -1649,13 +1650,20 @@ "label": "Company Contact Person", "options": "Contact", "print_hide": 1 + }, + { + "default": "0", + "fieldname": "has_unit_price_items", + "fieldtype": "Check", + "hidden": 1, + "label": "Has Unit Price Items" } ], "icon": "fa fa-file-text", "idx": 105, "is_submittable": 1, "links": [], - "modified": "2025-02-06 16:02:20.320877", + "modified": "2025-02-28 18:52:01.932669", "modified_by": "Administrator", "module": "Selling", "name": "Sales Order", @@ -1724,6 +1732,7 @@ "write": 1 } ], + "row_format": "Dynamic", "search_fields": "status,transaction_date,customer,customer_name, territory,order_type,company", "show_name_in_global_search": 1, "sort_field": "modified", diff --git a/erpnext/selling/doctype/sales_order/sales_order.py b/erpnext/selling/doctype/sales_order/sales_order.py index f7bf732e8e8..73604f86d29 100755 --- a/erpnext/selling/doctype/sales_order/sales_order.py +++ b/erpnext/selling/doctype/sales_order/sales_order.py @@ -52,16 +52,13 @@ class SalesOrder(SellingController): from typing import TYPE_CHECKING if TYPE_CHECKING: - from frappe.types import DF - from erpnext.accounts.doctype.payment_schedule.payment_schedule import PaymentSchedule from erpnext.accounts.doctype.pricing_rule_detail.pricing_rule_detail import PricingRuleDetail - from erpnext.accounts.doctype.sales_taxes_and_charges.sales_taxes_and_charges import ( - SalesTaxesandCharges, - ) + from erpnext.accounts.doctype.sales_taxes_and_charges.sales_taxes_and_charges import SalesTaxesandCharges from erpnext.selling.doctype.sales_order_item.sales_order_item import SalesOrderItem from erpnext.selling.doctype.sales_team.sales_team import SalesTeam from erpnext.stock.doctype.packed_item.packed_item import PackedItem + from frappe.types import DF additional_discount_percentage: DF.Float address_display: DF.SmallText | None @@ -100,9 +97,7 @@ class SalesOrder(SellingController): customer_group: DF.Link | None customer_name: DF.Data | None delivery_date: DF.Date | None - delivery_status: DF.Literal[ - "Not Delivered", "Fully Delivered", "Partly Delivered", "Closed", "Not Applicable" - ] + delivery_status: DF.Literal["Not Delivered", "Fully Delivered", "Partly Delivered", "Closed", "Not Applicable"] disable_rounded_total: DF.Check discount_amount: DF.Currency dispatch_address: DF.SmallText | None @@ -110,6 +105,7 @@ class SalesOrder(SellingController): from_date: DF.Date | None grand_total: DF.Currency group_same_items: DF.Check + has_unit_price_items: DF.Check ignore_pricing_rule: DF.Check in_words: DF.Data | None incoterm: DF.Link | None @@ -152,6 +148,7 @@ class SalesOrder(SellingController): shipping_address_name: DF.Link | None shipping_rule: DF.Link | None skip_delivery_note: DF.Check +<<<<<<< HEAD source: DF.Link | None status: DF.Literal[ "", @@ -165,6 +162,9 @@ class SalesOrder(SellingController): "Cancelled", "Closed", ] +======= + status: DF.Literal["", "Draft", "On Hold", "To Pay", "To Deliver and Bill", "To Bill", "To Deliver", "Completed", "Cancelled", "Closed"] +>>>>>>> c1e4e7af28 (feat: Unit Price Contract) tax_category: DF.Link | None tax_id: DF.Data | None taxes: DF.Table[SalesTaxesandCharges] @@ -195,6 +195,10 @@ class SalesOrder(SellingController): if has_reserved_stock(self.doctype, self.name): self.set_onload("has_reserved_stock", True) + def before_validate(self): + self.set_has_unit_price_items() + self.flags.allow_zero_qty = self.has_unit_price_items + def validate(self): super().validate() self.validate_delivery_date() @@ -231,6 +235,17 @@ class SalesOrder(SellingController): self.reset_default_field_value("set_warehouse", "items", "warehouse") + def set_has_unit_price_items(self): + """ + If permitted in settings and any item has 0 qty, the SO has unit price items. + """ + if not frappe.db.get_single_value("Selling Settings", "allow_zero_qty_in_sales_order"): + return + + self.has_unit_price_items = any( + not row.qty for row in self.get("items") if (row.item_code and not row.qty) + ) + def validate_po(self): # validate p.o date v/s delivery date if self.po_date and not self.skip_delivery_note: @@ -1093,7 +1108,13 @@ def make_sales_invoice(source_name, target_doc=None, ignore_permissions=False): target.debit_to = get_party_account("Customer", source.customer, source.company) def update_item(source, target, source_parent): - target.amount = flt(source.amount) - flt(source.billed_amt) + if source_parent.has_unit_price_items: + # 0 Amount rows (as seen in Unit Price Items) should be mapped as it is + pending_amount = flt(source.amount) - flt(source.billed_amt) + target.amount = pending_amount if flt(source.amount) else 0 + else: + target.amount = flt(source.amount) - flt(source.billed_amt) + target.base_amount = target.amount * flt(source_parent.conversion_rate) target.qty = ( target.amount / flt(source.rate) @@ -1111,6 +1132,10 @@ def make_sales_invoice(source_name, target_doc=None, ignore_permissions=False): if cost_center: target.cost_center = cost_center + # has_unit_price_items = 0 is accepted as the qty uncertain for some items + has_unit_price_items = frappe.db.get_value( + "Sales Order", source_name, "has_unit_price_items" + ) doclist = get_mapped_doc( "Sales Order", source_name, @@ -1131,8 +1156,13 @@ def make_sales_invoice(source_name, target_doc=None, ignore_permissions=False): "parent": "sales_order", }, "postprocess": update_item, - "condition": lambda doc: doc.qty - and (doc.base_amount == 0 or abs(doc.billed_amt) < abs(doc.amount)), + "condition": lambda doc: ( + doc.qty + and ( + doc.base_amount == 0 + or abs(doc.billed_amt) < abs(doc.amount) + ) + ) or has_unit_price_items, }, "Sales Taxes and Charges": { "doctype": "Sales Taxes and Charges", diff --git a/erpnext/selling/doctype/selling_settings/selling_settings.json b/erpnext/selling/doctype/selling_settings/selling_settings.json index d6829ce24b0..01b7701f488 100644 --- a/erpnext/selling/doctype/selling_settings/selling_settings.json +++ b/erpnext/selling/doctype/selling_settings/selling_settings.json @@ -32,7 +32,16 @@ "allow_sales_order_creation_for_expired_quotation", "dont_reserve_sales_order_qty_on_sales_return", "hide_tax_id", +<<<<<<< HEAD "enable_discount_accounting" +======= + "enable_discount_accounting", + "enable_cutoff_date_on_bulk_delivery_note_creation", + "allow_zero_qty_in_sales_order", + "allow_zero_qty_in_quotation", + "experimental_section", + "use_server_side_reactivity" +>>>>>>> c1e4e7af28 (feat: Unit Price Contract) ], "fields": [ { @@ -200,14 +209,51 @@ "fieldname": "blanket_order_allowance", "fieldtype": "Float", "label": "Blanket Order Allowance (%)" +<<<<<<< HEAD +======= + }, + { + "default": "0", + "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" + }, + { + "default": "0", + "fieldname": "allow_zero_qty_in_sales_order", + "fieldtype": "Check", + "label": "Allow 0 Qty in Sales Order (Unit Price Contract)" + }, + { + "default": "0", + "fieldname": "allow_zero_qty_in_quotation", + "fieldtype": "Check", + "label": "Allow 0 Qty in Quotation (Unit Price Contract)" +>>>>>>> c1e4e7af28 (feat: Unit Price Contract) } ], + "grid_page_length": 50, "icon": "fa fa-cog", "idx": 1, "index_web_pages_for_search": 1, "issingle": 1, "links": [], +<<<<<<< HEAD "modified": "2023-10-25 14:03:03.966701", +======= + "modified": "2025-02-28 18:19:46.436595", +>>>>>>> c1e4e7af28 (feat: Unit Price Contract) "modified_by": "Administrator", "module": "Selling", "name": "Selling Settings", @@ -232,7 +278,12 @@ "write": 1 } ], +<<<<<<< HEAD "sort_field": "modified", +======= + "row_format": "Dynamic", + "sort_field": "creation", +>>>>>>> c1e4e7af28 (feat: Unit Price Contract) "sort_order": "DESC", "states": [], "track_changes": 1 diff --git a/erpnext/selling/doctype/selling_settings/selling_settings.py b/erpnext/selling/doctype/selling_settings/selling_settings.py index 24fe909a7a6..2954c353a77 100644 --- a/erpnext/selling/doctype/selling_settings/selling_settings.py +++ b/erpnext/selling/doctype/selling_settings/selling_settings.py @@ -23,6 +23,8 @@ class SellingSettings(Document): allow_multiple_items: DF.Check allow_negative_rates_for_items: DF.Check allow_sales_order_creation_for_expired_quotation: DF.Check + allow_zero_qty_in_quotation: DF.Check + allow_zero_qty_in_sales_order: DF.Check blanket_order_allowance: DF.Float cust_master_name: DF.Literal["Customer Name", "Naming Series", "Auto Name"] customer_group: DF.Link | None From 91e167fe72814c04a78a9d94d7d6d0b92fc197d4 Mon Sep 17 00:00:00 2001 From: marination Date: Mon, 3 Mar 2025 16:24:22 +0100 Subject: [PATCH 02/49] fix: Linters (cherry picked from commit 71f65bab5e140cf8ba2f3e64e209b7b33bf35b71) # Conflicts: # erpnext/selling/doctype/sales_order/sales_order.py --- .../selling/doctype/quotation/quotation.py | 3 +- .../doctype/sales_order/sales_order.py | 30 +++++++++++-------- 2 files changed, 20 insertions(+), 13 deletions(-) diff --git a/erpnext/selling/doctype/quotation/quotation.py b/erpnext/selling/doctype/quotation/quotation.py index ce74e1bafed..a4f79c1683d 100644 --- a/erpnext/selling/doctype/quotation/quotation.py +++ b/erpnext/selling/doctype/quotation/quotation.py @@ -19,6 +19,8 @@ class Quotation(SellingController): from typing import TYPE_CHECKING if TYPE_CHECKING: + from frappe.types import DF + from erpnext.accounts.doctype.payment_schedule.payment_schedule import PaymentSchedule from erpnext.accounts.doctype.pricing_rule_detail.pricing_rule_detail import PricingRuleDetail from erpnext.accounts.doctype.sales_taxes_and_charges.sales_taxes_and_charges import ( @@ -30,7 +32,6 @@ class Quotation(SellingController): QuotationLostReasonDetail, ) from erpnext.stock.doctype.packed_item.packed_item import PackedItem - from frappe.types import DF additional_discount_percentage: DF.Float address_display: DF.SmallText | None diff --git a/erpnext/selling/doctype/sales_order/sales_order.py b/erpnext/selling/doctype/sales_order/sales_order.py index 73604f86d29..54ee5f0d21d 100755 --- a/erpnext/selling/doctype/sales_order/sales_order.py +++ b/erpnext/selling/doctype/sales_order/sales_order.py @@ -52,13 +52,16 @@ class SalesOrder(SellingController): from typing import TYPE_CHECKING if TYPE_CHECKING: + from frappe.types import DF + from erpnext.accounts.doctype.payment_schedule.payment_schedule import PaymentSchedule from erpnext.accounts.doctype.pricing_rule_detail.pricing_rule_detail import PricingRuleDetail - from erpnext.accounts.doctype.sales_taxes_and_charges.sales_taxes_and_charges import SalesTaxesandCharges + from erpnext.accounts.doctype.sales_taxes_and_charges.sales_taxes_and_charges import ( + SalesTaxesandCharges, + ) from erpnext.selling.doctype.sales_order_item.sales_order_item import SalesOrderItem from erpnext.selling.doctype.sales_team.sales_team import SalesTeam from erpnext.stock.doctype.packed_item.packed_item import PackedItem - from frappe.types import DF additional_discount_percentage: DF.Float address_display: DF.SmallText | None @@ -97,7 +100,9 @@ class SalesOrder(SellingController): customer_group: DF.Link | None customer_name: DF.Data | None delivery_date: DF.Date | None - delivery_status: DF.Literal["Not Delivered", "Fully Delivered", "Partly Delivered", "Closed", "Not Applicable"] + delivery_status: DF.Literal[ + "Not Delivered", "Fully Delivered", "Partly Delivered", "Closed", "Not Applicable" + ] disable_rounded_total: DF.Check discount_amount: DF.Currency dispatch_address: DF.SmallText | None @@ -148,8 +153,11 @@ class SalesOrder(SellingController): shipping_address_name: DF.Link | None shipping_rule: DF.Link | None skip_delivery_note: DF.Check +<<<<<<< HEAD <<<<<<< HEAD source: DF.Link | None +======= +>>>>>>> 71f65bab5e (fix: Linters) status: DF.Literal[ "", "Draft", @@ -162,9 +170,12 @@ class SalesOrder(SellingController): "Cancelled", "Closed", ] +<<<<<<< HEAD ======= status: DF.Literal["", "Draft", "On Hold", "To Pay", "To Deliver and Bill", "To Bill", "To Deliver", "Completed", "Cancelled", "Closed"] >>>>>>> c1e4e7af28 (feat: Unit Price Contract) +======= +>>>>>>> 71f65bab5e (fix: Linters) tax_category: DF.Link | None tax_id: DF.Data | None taxes: DF.Table[SalesTaxesandCharges] @@ -1133,9 +1144,7 @@ def make_sales_invoice(source_name, target_doc=None, ignore_permissions=False): target.cost_center = cost_center # has_unit_price_items = 0 is accepted as the qty uncertain for some items - has_unit_price_items = frappe.db.get_value( - "Sales Order", source_name, "has_unit_price_items" - ) + has_unit_price_items = frappe.db.get_value("Sales Order", source_name, "has_unit_price_items") doclist = get_mapped_doc( "Sales Order", source_name, @@ -1157,12 +1166,9 @@ def make_sales_invoice(source_name, target_doc=None, ignore_permissions=False): }, "postprocess": update_item, "condition": lambda doc: ( - doc.qty - and ( - doc.base_amount == 0 - or abs(doc.billed_amt) < abs(doc.amount) - ) - ) or has_unit_price_items, + doc.qty and (doc.base_amount == 0 or abs(doc.billed_amt) < abs(doc.amount)) + ) + or has_unit_price_items, }, "Sales Taxes and Charges": { "doctype": "Sales Taxes and Charges", From f8fa775af345fe0016d70d76aa2d2ae065088820 Mon Sep 17 00:00:00 2001 From: marination Date: Mon, 3 Mar 2025 17:53:00 +0100 Subject: [PATCH 03/49] feat: Unit Price Items in Buying (RFQ, SQ, PO) - chore: Extract `set_unit_price_items_note` into a util (cherry picked from commit e403d3f153a3d1c134a844c5c22139521e250480) # Conflicts: # erpnext/buying/doctype/buying_settings/buying_settings.json # erpnext/buying/doctype/purchase_order/purchase_order.json # erpnext/buying/doctype/request_for_quotation/request_for_quotation.json # erpnext/selling/doctype/quotation/quotation.json # erpnext/selling/doctype/selling_settings/selling_settings.json --- .../buying_settings/buying_settings.json | 31 +++++++++++++++++++ .../buying_settings/buying_settings.py | 3 ++ .../doctype/purchase_order/purchase_order.js | 14 ++++++++- .../purchase_order/purchase_order.json | 14 +++++++++ .../doctype/purchase_order/purchase_order.py | 16 ++++++++++ .../request_for_quotation.js | 8 +++++ .../request_for_quotation.json | 15 +++++++++ .../request_for_quotation.py | 16 ++++++++++ .../supplier_quotation/supplier_quotation.js | 7 +++++ .../supplier_quotation.json | 13 +++++++- .../supplier_quotation/supplier_quotation.py | 16 ++++++++++ erpnext/public/js/controllers/transaction.js | 10 ++++++ .../selling/doctype/quotation/quotation.js | 12 +------ .../selling/doctype/quotation/quotation.json | 7 ++++- .../doctype/sales_order/sales_order.js | 12 +------ .../doctype/sales_order/sales_order.json | 5 +-- .../selling_settings/selling_settings.json | 12 +++++-- 17 files changed, 182 insertions(+), 29 deletions(-) diff --git a/erpnext/buying/doctype/buying_settings/buying_settings.json b/erpnext/buying/doctype/buying_settings/buying_settings.json index ae854f29343..a92eb23a23d 100644 --- a/erpnext/buying/doctype/buying_settings/buying_settings.json +++ b/erpnext/buying/doctype/buying_settings/buying_settings.json @@ -25,6 +25,9 @@ "disable_last_purchase_rate", "show_pay_button", "use_transaction_date_exchange_rate", + "allow_zero_qty_in_request_for_quotation", + "allow_zero_qty_in_supplier_quotation", + "allow_zero_qty_in_purchase_order", "subcontract", "backflush_raw_materials_of_subcontract_based_on", "column_break_11", @@ -207,14 +210,37 @@ "fieldtype": "Select", "label": "Update frequency of Project", "options": "Each Transaction\nManual" + }, + { + "default": "0", + "fieldname": "allow_zero_qty_in_purchase_order", + "fieldtype": "Check", + "label": "Allow 0 Qty in Purchase Order (Unit Price Items)" + }, + { + "default": "0", + "fieldname": "allow_zero_qty_in_request_for_quotation", + "fieldtype": "Check", + "label": "Allow 0 Qty in Request for Quotation (Unit Price Items)" + }, + { + "default": "0", + "fieldname": "allow_zero_qty_in_supplier_quotation", + "fieldtype": "Check", + "label": "Allow 0 Qty in Supplier Quotation (Unit Price Items)" } ], + "grid_page_length": 50, "icon": "fa fa-cog", "idx": 1, "index_web_pages_for_search": 1, "issingle": 1, "links": [], +<<<<<<< HEAD "modified": "2024-01-31 13:34:18.101256", +======= + "modified": "2025-03-03 17:32:25.939482", +>>>>>>> e403d3f153 (feat: Unit Price Items in Buying (RFQ, SQ, PO)) "modified_by": "Administrator", "module": "Buying", "name": "Buying Settings", @@ -260,7 +286,12 @@ "role": "Purchase User" } ], +<<<<<<< HEAD "sort_field": "modified", +======= + "row_format": "Dynamic", + "sort_field": "creation", +>>>>>>> e403d3f153 (feat: Unit Price Items in Buying (RFQ, SQ, PO)) "sort_order": "DESC", "states": [], "track_changes": 1 diff --git a/erpnext/buying/doctype/buying_settings/buying_settings.py b/erpnext/buying/doctype/buying_settings/buying_settings.py index ec9b88888b7..4dde7c8dabf 100644 --- a/erpnext/buying/doctype/buying_settings/buying_settings.py +++ b/erpnext/buying/doctype/buying_settings/buying_settings.py @@ -18,6 +18,9 @@ class BuyingSettings(Document): from frappe.types import DF allow_multiple_items: DF.Check + allow_zero_qty_in_purchase_order: DF.Check + allow_zero_qty_in_request_for_quotation: DF.Check + allow_zero_qty_in_supplier_quotation: DF.Check auto_create_purchase_receipt: DF.Check auto_create_subcontracting_order: DF.Check backflush_raw_materials_of_subcontract_based_on: DF.Literal[ diff --git a/erpnext/buying/doctype/purchase_order/purchase_order.js b/erpnext/buying/doctype/purchase_order/purchase_order.js index 0b7c9de467a..7925d59d25a 100644 --- a/erpnext/buying/doctype/purchase_order/purchase_order.js +++ b/erpnext/buying/doctype/purchase_order/purchase_order.js @@ -26,7 +26,15 @@ frappe.ui.form.on("Purchase Order", { } frm.set_indicator_formatter("item_code", function (doc) { - return doc.qty <= doc.received_qty ? "green" : "orange"; + let color; + if (!doc.qty && frm.doc.has_unit_price_items) { + color = "yellow"; + } else if (doc.qty <= doc.received_qty) { + color = "green"; + } else { + color = "orange"; + } + return color; }); frm.set_query("expense_account", "items", function () { @@ -63,6 +71,10 @@ frappe.ui.form.on("Purchase Order", { } }); } + + if (frm.doc.docstatus == 0) { + erpnext.set_unit_price_items_note(frm); + } }, supplier: function (frm) { diff --git a/erpnext/buying/doctype/purchase_order/purchase_order.json b/erpnext/buying/doctype/purchase_order/purchase_order.json index 7c9362ebaf9..bc63e8756ad 100644 --- a/erpnext/buying/doctype/purchase_order/purchase_order.json +++ b/erpnext/buying/doctype/purchase_order/purchase_order.json @@ -24,6 +24,7 @@ "apply_tds", "tax_withholding_category", "is_subcontracted", + "has_unit_price_items", "supplier_warehouse", "amended_from", "accounting_dimensions_section", @@ -1280,11 +1281,20 @@ "print_hide": 1 }, { +<<<<<<< HEAD "fieldname": "dispatch_address_display", "fieldtype": "Text Editor", "label": "Dispatch Address Details", "print_hide": 1, "read_only": 1 +======= + "default": "0", + "fieldname": "has_unit_price_items", + "fieldtype": "Check", + "hidden": 1, + "label": "Has Unit Price Items", + "no_copy": 1 +>>>>>>> e403d3f153 (feat: Unit Price Items in Buying (RFQ, SQ, PO)) } ], "grid_page_length": 50, @@ -1292,7 +1302,11 @@ "idx": 105, "is_submittable": 1, "links": [], +<<<<<<< HEAD "modified": "2025-04-09 16:54:08.836106", +======= + "modified": "2025-03-03 16:48:08.697520", +>>>>>>> e403d3f153 (feat: Unit Price Items in Buying (RFQ, SQ, PO)) "modified_by": "Administrator", "module": "Buying", "name": "Purchase Order", diff --git a/erpnext/buying/doctype/purchase_order/purchase_order.py b/erpnext/buying/doctype/purchase_order/purchase_order.py index a18d9fce186..9ddbbb991d6 100644 --- a/erpnext/buying/doctype/purchase_order/purchase_order.py +++ b/erpnext/buying/doctype/purchase_order/purchase_order.py @@ -97,6 +97,7 @@ class PurchaseOrder(BuyingController): from_date: DF.Date | None grand_total: DF.Currency group_same_items: DF.Check + has_unit_price_items: DF.Check ignore_pricing_rule: DF.Check in_words: DF.Data | None incoterm: DF.Link | None @@ -191,6 +192,10 @@ class PurchaseOrder(BuyingController): self.set_onload("supplier_tds", supplier_tds) self.set_onload("can_update_items", self.can_update_items()) + def before_validate(self): + self.set_has_unit_price_items() + self.flags.allow_zero_qty = self.has_unit_price_items + def validate(self): super().validate() @@ -223,6 +228,17 @@ class PurchaseOrder(BuyingController): ) self.reset_default_field_value("set_warehouse", "items", "warehouse") + def set_has_unit_price_items(self): + """ + If permitted in settings and any item has 0 qty, the PO has unit price items. + """ + if not frappe.db.get_single_value("Buying Settings", "allow_zero_qty_in_purchase_order"): + return + + self.has_unit_price_items = any( + not row.qty for row in self.get("items") if (row.item_code and not row.qty) + ) + def validate_with_previous_doc(self): mri_compare_fields = [["project", "="], ["item_code", "="]] if self.is_subcontracted: diff --git a/erpnext/buying/doctype/request_for_quotation/request_for_quotation.js b/erpnext/buying/doctype/request_for_quotation/request_for_quotation.js index 96597bd9753..e88a98759d0 100644 --- a/erpnext/buying/doctype/request_for_quotation/request_for_quotation.js +++ b/erpnext/buying/doctype/request_for_quotation/request_for_quotation.js @@ -28,6 +28,10 @@ frappe.ui.form.on("Request for Quotation", { is_group: 0, }, })); + + frm.set_indicator_formatter("item_code", function (doc) { + return !doc.qty && frm.doc.has_unit_price_items ? "yellow" : ""; + }); }, onload: function (frm) { @@ -163,6 +167,10 @@ frappe.ui.form.on("Request for Quotation", { __("View") ); } + + if (frm.doc.docstatus === 0) { + erpnext.set_unit_price_items_note(frm); + } }, show_supplier_quotation_comparison(frm) { diff --git a/erpnext/buying/doctype/request_for_quotation/request_for_quotation.json b/erpnext/buying/doctype/request_for_quotation/request_for_quotation.json index fd73f77ff8f..056535af17b 100644 --- a/erpnext/buying/doctype/request_for_quotation/request_for_quotation.json +++ b/erpnext/buying/doctype/request_for_quotation/request_for_quotation.json @@ -16,6 +16,7 @@ "transaction_date", "schedule_date", "status", + "has_unit_price_items", "amended_from", "suppliers_section", "suppliers", @@ -306,13 +307,26 @@ "fieldtype": "Small Text", "label": "Billing Address Details", "read_only": 1 + }, + { + "default": "0", + "fieldname": "has_unit_price_items", + "fieldtype": "Check", + "hidden": 1, + "label": "Has Unit Price Items", + "no_copy": 1 } ], + "grid_page_length": 50, "icon": "fa fa-shopping-cart", "index_web_pages_for_search": 1, "is_submittable": 1, "links": [], +<<<<<<< HEAD "modified": "2023-11-06 12:45:28.898706", +======= + "modified": "2025-03-03 16:48:39.856779", +>>>>>>> e403d3f153 (feat: Unit Price Items in Buying (RFQ, SQ, PO)) "modified_by": "Administrator", "module": "Buying", "name": "Request for Quotation", @@ -377,6 +391,7 @@ "role": "All" } ], + "row_format": "Dynamic", "search_fields": "status, transaction_date", "show_name_in_global_search": 1, "sort_field": "modified", diff --git a/erpnext/buying/doctype/request_for_quotation/request_for_quotation.py b/erpnext/buying/doctype/request_for_quotation/request_for_quotation.py index bef41394742..e9bc9694a70 100644 --- a/erpnext/buying/doctype/request_for_quotation/request_for_quotation.py +++ b/erpnext/buying/doctype/request_for_quotation/request_for_quotation.py @@ -42,6 +42,7 @@ class RequestforQuotation(BuyingController): billing_address_display: DF.SmallText | None company: DF.Link email_template: DF.Link | None + has_unit_price_items: DF.Check incoterm: DF.Link | None items: DF.Table[RequestforQuotationItem] letter_head: DF.Link | None @@ -61,6 +62,10 @@ class RequestforQuotation(BuyingController): vendor: DF.Link | None # end: auto-generated types + def before_validate(self): + self.set_has_unit_price_items() + self.flags.allow_zero_qty = self.has_unit_price_items + def validate(self): self.validate_duplicate_supplier() self.validate_supplier_list() @@ -72,6 +77,17 @@ class RequestforQuotation(BuyingController): # after amend and save, status still shows as cancelled, until submit self.db_set("status", "Draft") + def set_has_unit_price_items(self): + """ + If permitted in settings and any item has 0 qty, the RFQ has unit price items. + """ + if not frappe.db.get_single_value("Buying Settings", "allow_zero_qty_in_request_for_quotation"): + return + + self.has_unit_price_items = any( + not row.qty for row in self.get("items") if (row.item_code and not row.qty) + ) + def validate_duplicate_supplier(self): supplier_list = [d.supplier for d in self.suppliers] if len(supplier_list) != len(set(supplier_list)): diff --git a/erpnext/buying/doctype/supplier_quotation/supplier_quotation.js b/erpnext/buying/doctype/supplier_quotation/supplier_quotation.js index c1698710135..fccca81f8ce 100644 --- a/erpnext/buying/doctype/supplier_quotation/supplier_quotation.js +++ b/erpnext/buying/doctype/supplier_quotation/supplier_quotation.js @@ -11,6 +11,11 @@ erpnext.buying.SupplierQuotationController = class SupplierQuotationController e Quotation: "Quotation", }; + const me = this; + this.frm.set_indicator_formatter("item_code", function (doc) { + return !doc.qty && me.frm.doc.has_unit_price_items ? "yellow" : ""; + }); + super.setup(); } @@ -26,6 +31,8 @@ erpnext.buying.SupplierQuotationController = class SupplierQuotationController e cur_frm.page.set_inner_btn_group_as_primary(__("Create")); cur_frm.add_custom_button(__("Quotation"), this.make_quotation, __("Create")); } else if (this.frm.doc.docstatus === 0) { + erpnext.set_unit_price_items_note(this.frm); + this.frm.add_custom_button( __("Material Request"), function () { diff --git a/erpnext/buying/doctype/supplier_quotation/supplier_quotation.json b/erpnext/buying/doctype/supplier_quotation/supplier_quotation.json index 4a8cd8bf9e6..6682c7e3585 100644 --- a/erpnext/buying/doctype/supplier_quotation/supplier_quotation.json +++ b/erpnext/buying/doctype/supplier_quotation/supplier_quotation.json @@ -19,6 +19,7 @@ "transaction_date", "valid_till", "quotation_number", + "has_unit_price_items", "amended_from", "accounting_dimensions_section", "cost_center", @@ -921,14 +922,23 @@ "fieldname": "accounting_dimensions_section", "fieldtype": "Section Break", "label": "Accounting Dimensions" + }, + { + "default": "0", + "fieldname": "has_unit_price_items", + "fieldtype": "Check", + "hidden": 1, + "label": "Has Unit Price Items", + "no_copy": 1 } ], + "grid_page_length": 50, "icon": "fa fa-shopping-cart", "idx": 29, "index_web_pages_for_search": 1, "is_submittable": 1, "links": [], - "modified": "2024-03-28 10:20:30.231915", + "modified": "2025-03-03 17:39:38.459977", "modified_by": "Administrator", "module": "Buying", "name": "Supplier Quotation", @@ -989,6 +999,7 @@ "write": 1 } ], + "row_format": "Dynamic", "search_fields": "status, transaction_date, supplier,grand_total", "show_name_in_global_search": 1, "sort_field": "modified", diff --git a/erpnext/buying/doctype/supplier_quotation/supplier_quotation.py b/erpnext/buying/doctype/supplier_quotation/supplier_quotation.py index 215022e18a6..5557f1a80ae 100644 --- a/erpnext/buying/doctype/supplier_quotation/supplier_quotation.py +++ b/erpnext/buying/doctype/supplier_quotation/supplier_quotation.py @@ -60,6 +60,7 @@ class SupplierQuotation(BuyingController): discount_amount: DF.Currency grand_total: DF.Currency group_same_items: DF.Check + has_unit_price_items: DF.Check ignore_pricing_rule: DF.Check in_words: DF.Data | None incoterm: DF.Link | None @@ -103,6 +104,10 @@ class SupplierQuotation(BuyingController): valid_till: DF.Date | None # end: auto-generated types + def before_validate(self): + self.set_has_unit_price_items() + self.flags.allow_zero_qty = self.has_unit_price_items + def validate(self): super().validate() @@ -129,6 +134,17 @@ class SupplierQuotation(BuyingController): def on_trash(self): pass + def set_has_unit_price_items(self): + """ + If permitted in settings and any item has 0 qty, the SQ has unit price items. + """ + if not frappe.db.get_single_value("Buying Settings", "allow_zero_qty_in_supplier_quotation"): + return + + self.has_unit_price_items = any( + not row.qty for row in self.get("items") if (row.item_code and not row.qty) + ) + def validate_with_previous_doc(self): super().validate_with_previous_doc( { diff --git a/erpnext/public/js/controllers/transaction.js b/erpnext/public/js/controllers/transaction.js index 1fcdd459a3f..886ddd2acc0 100644 --- a/erpnext/public/js/controllers/transaction.js +++ b/erpnext/public/js/controllers/transaction.js @@ -2773,3 +2773,13 @@ erpnext.apply_putaway_rule = (frm, purpose=null) => { } }); }; + +erpnext.set_unit_price_items_note = (frm) => { + if (frm.doc.has_unit_price_items && !frm.is_new()) { + frm.dashboard.set_headline_alert( + __("The {0} contains Unit Price Items with 0 Qty.", [__(frm.doc.doctype)]), + "yellow", + true + ); + } +}; diff --git a/erpnext/selling/doctype/quotation/quotation.js b/erpnext/selling/doctype/quotation/quotation.js index 07869ab339b..6cb676ac58e 100644 --- a/erpnext/selling/doctype/quotation/quotation.js +++ b/erpnext/selling/doctype/quotation/quotation.js @@ -46,7 +46,7 @@ frappe.ui.form.on("Quotation", { frm.trigger("set_dynamic_field_label"); if (frm.doc.docstatus === 0) { - frm.trigger("set_unit_price_items_note"); + erpnext.set_unit_price_items_note(frm); } let sbb_field = frm.get_docfield("packed_items", "serial_and_batch_bundle"); @@ -72,16 +72,6 @@ frappe.ui.form.on("Quotation", { set_label: function (frm) { frm.fields_dict.customer_address.set_label(__(frm.doc.quotation_to + " Address")); }, - - set_unit_price_items_note: function (frm) { - if (frm.doc.has_unit_price_items) { - frm.dashboard.set_headline_alert( - __("The Quotation contains Unit Price Items with 0 Qty."), - "yellow", - true - ); - } - }, }); erpnext.selling.QuotationController = class QuotationController extends erpnext.selling.SellingController { diff --git a/erpnext/selling/doctype/quotation/quotation.json b/erpnext/selling/doctype/quotation/quotation.json index e087256d8d4..5bf8f474625 100644 --- a/erpnext/selling/doctype/quotation/quotation.json +++ b/erpnext/selling/doctype/quotation/quotation.json @@ -1091,18 +1091,23 @@ "fieldname": "has_unit_price_items", "fieldtype": "Check", "hidden": 1, - "label": "Has Unit Price Items" + "label": "Has Unit Price Items", + "no_copy": 1 } ], "icon": "fa fa-shopping-cart", "idx": 82, "is_submittable": 1, "links": [], +<<<<<<< HEAD <<<<<<< HEAD "modified": "2024-11-26 12:43:29.293637", ======= "modified": "2025-02-28 18:52:44.063265", >>>>>>> c1e4e7af28 (feat: Unit Price Contract) +======= + "modified": "2025-03-03 16:49:20.050303", +>>>>>>> e403d3f153 (feat: Unit Price Items in Buying (RFQ, SQ, PO)) "modified_by": "Administrator", "module": "Selling", "name": "Quotation", diff --git a/erpnext/selling/doctype/sales_order/sales_order.js b/erpnext/selling/doctype/sales_order/sales_order.js index 6c8cc7ac1df..ca3d2ac824b 100644 --- a/erpnext/selling/doctype/sales_order/sales_order.js +++ b/erpnext/selling/doctype/sales_order/sales_order.js @@ -106,7 +106,7 @@ frappe.ui.form.on("Sales Order", { } if (frm.doc.docstatus === 0) { - frm.trigger("set_unit_price_items_note"); + erpnext.set_unit_price_items_note(frm); if (frm.doc.is_internal_customer) { frm.events.get_items_from_internal_purchase_order(frm); @@ -539,16 +539,6 @@ frappe.ui.form.on("Sales Order", { }; frappe.set_route("query-report", "Reserved Stock"); }, - - set_unit_price_items_note: function (frm) { - if (frm.doc.has_unit_price_items && !frm.is_new()) { - frm.dashboard.set_headline_alert( - __("The Sales Order contains Unit Price Items with 0 Qty."), - "yellow", - true - ); - } - }, }); frappe.ui.form.on("Sales Order Item", { diff --git a/erpnext/selling/doctype/sales_order/sales_order.json b/erpnext/selling/doctype/sales_order/sales_order.json index 5fc808798e2..801648b4479 100644 --- a/erpnext/selling/doctype/sales_order/sales_order.json +++ b/erpnext/selling/doctype/sales_order/sales_order.json @@ -1656,14 +1656,15 @@ "fieldname": "has_unit_price_items", "fieldtype": "Check", "hidden": 1, - "label": "Has Unit Price Items" + "label": "Has Unit Price Items", + "no_copy": 1 } ], "icon": "fa fa-file-text", "idx": 105, "is_submittable": 1, "links": [], - "modified": "2025-02-28 18:52:01.932669", + "modified": "2025-03-03 16:49:00.676927", "modified_by": "Administrator", "module": "Selling", "name": "Sales Order", diff --git a/erpnext/selling/doctype/selling_settings/selling_settings.json b/erpnext/selling/doctype/selling_settings/selling_settings.json index 01b7701f488..0e857430b5e 100644 --- a/erpnext/selling/doctype/selling_settings/selling_settings.json +++ b/erpnext/selling/doctype/selling_settings/selling_settings.json @@ -37,8 +37,8 @@ ======= "enable_discount_accounting", "enable_cutoff_date_on_bulk_delivery_note_creation", - "allow_zero_qty_in_sales_order", "allow_zero_qty_in_quotation", + "allow_zero_qty_in_sales_order", "experimental_section", "use_server_side_reactivity" >>>>>>> c1e4e7af28 (feat: Unit Price Contract) @@ -233,14 +233,18 @@ "default": "0", "fieldname": "allow_zero_qty_in_sales_order", "fieldtype": "Check", - "label": "Allow 0 Qty in Sales Order (Unit Price Contract)" + "label": "Allow 0 Qty in Sales Order (Unit Price Items)" }, { "default": "0", "fieldname": "allow_zero_qty_in_quotation", "fieldtype": "Check", +<<<<<<< HEAD "label": "Allow 0 Qty in Quotation (Unit Price Contract)" >>>>>>> c1e4e7af28 (feat: Unit Price Contract) +======= + "label": "Allow 0 Qty in Quotation (Unit Price Items)" +>>>>>>> e403d3f153 (feat: Unit Price Items in Buying (RFQ, SQ, PO)) } ], "grid_page_length": 50, @@ -249,11 +253,15 @@ "index_web_pages_for_search": 1, "issingle": 1, "links": [], +<<<<<<< HEAD <<<<<<< HEAD "modified": "2023-10-25 14:03:03.966701", ======= "modified": "2025-02-28 18:19:46.436595", >>>>>>> c1e4e7af28 (feat: Unit Price Contract) +======= + "modified": "2025-03-03 16:39:16.360823", +>>>>>>> e403d3f153 (feat: Unit Price Items in Buying (RFQ, SQ, PO)) "modified_by": "Administrator", "module": "Selling", "name": "Selling Settings", From c19065e675bd7f87a595f7b0a5b06e3ceebbcafd Mon Sep 17 00:00:00 2001 From: marination <25857446+marination@users.noreply.github.com> Date: Wed, 12 Mar 2025 18:32:59 +0100 Subject: [PATCH 04/49] test: Zero Qty in RFQ and Supplier Quotation (cherry picked from commit 8f96c0b5461488dbe2739d5d8e65d42a44b3b301) # Conflicts: # erpnext/buying/doctype/request_for_quotation/test_request_for_quotation.py # erpnext/buying/doctype/supplier_quotation/test_supplier_quotation.py --- .../test_request_for_quotation.py | 45 +++++++++++++++ .../test_supplier_quotation.py | 55 +++++++++++++++++++ 2 files changed, 100 insertions(+) diff --git a/erpnext/buying/doctype/request_for_quotation/test_request_for_quotation.py b/erpnext/buying/doctype/request_for_quotation/test_request_for_quotation.py index 51b4d7a5c89..6e21f4596f6 100644 --- a/erpnext/buying/doctype/request_for_quotation/test_request_for_quotation.py +++ b/erpnext/buying/doctype/request_for_quotation/test_request_for_quotation.py @@ -5,7 +5,11 @@ from urllib.parse import urlparse import frappe +<<<<<<< HEAD from frappe.tests.utils import FrappeTestCase +======= +from frappe.tests import IntegrationTestCase, UnitTestCase, change_settings +>>>>>>> 8f96c0b546 (test: Zero Qty in RFQ and Supplier Quotation) from frappe.utils import nowdate from erpnext.buying.doctype.request_for_quotation.request_for_quotation import ( @@ -20,7 +24,39 @@ from erpnext.stock.doctype.item.test_item import make_item from erpnext.templates.pages.rfq import check_supplier_has_docname_access +<<<<<<< HEAD class TestRequestforQuotation(FrappeTestCase): +======= +class UnitTestRequestForQuotation(UnitTestCase): + """ + Unit tests for RequestForQuotation. + Use this class for testing individual functions and methods. + """ + + pass + + +class TestRequestforQuotation(IntegrationTestCase): + def test_rfq_qty(self): + rfq = make_request_for_quotation(qty=0, do_not_save=True) + with self.assertRaises(InvalidQtyError): + rfq.save() + + # No error with qty=1 + rfq.items[0].qty = 1 + rfq.save() + self.assertEqual(rfq.items[0].qty, 1) + + def test_rfq_zero_qty(self): + """ + Test if RFQ with zero qty (Unit Price Item) is conditionally allowed. + """ + rfq = make_request_for_quotation(qty=0, do_not_save=True) + + with change_settings("Buying Settings", {"allow_zero_qty_in_request_for_quotation": 1}): + rfq.save() + +>>>>>>> 8f96c0b546 (test: Zero Qty in RFQ and Supplier Quotation) def test_quote_status(self): rfq = make_request_for_quotation() @@ -161,6 +197,15 @@ class TestRequestforQuotation(FrappeTestCase): supplier_doc.reload() self.assertTrue(supplier_doc.portal_users[0].user) + @change_settings("Buying Settings", {"allow_zero_qty_in_request_for_quotation": 1}) + def test_map_supplier_quotation_from_zero_qty_rfq(self): + rfq = make_request_for_quotation(qty=0) + sq = make_supplier_quotation_from_rfq(rfq.name, for_supplier=rfq.get("suppliers")[0].supplier) + + self.assertEqual(len(sq.items), 1) + self.assertEqual(sq.items[0].qty, 0) + self.assertEqual(sq.items[0].item_code, rfq.items[0].item_code) + def make_request_for_quotation(**args) -> "RequestforQuotation": """ diff --git a/erpnext/buying/doctype/supplier_quotation/test_supplier_quotation.py b/erpnext/buying/doctype/supplier_quotation/test_supplier_quotation.py index 84df61de373..31bee01e775 100644 --- a/erpnext/buying/doctype/supplier_quotation/test_supplier_quotation.py +++ b/erpnext/buying/doctype/supplier_quotation/test_supplier_quotation.py @@ -3,6 +3,7 @@ import frappe +<<<<<<< HEAD from frappe.tests.utils import FrappeTestCase from frappe.utils import add_days, today @@ -12,6 +13,47 @@ class TestPurchaseOrder(FrappeTestCase): from erpnext.buying.doctype.supplier_quotation.supplier_quotation import make_purchase_order sq = frappe.copy_doc(test_records[0]).insert() +======= +from frappe.tests import IntegrationTestCase, UnitTestCase, change_settings + +from erpnext.buying.doctype.supplier_quotation.supplier_quotation import make_purchase_order +from erpnext.controllers.accounts_controller import InvalidQtyError + + +class UnitTestSupplierQuotation(UnitTestCase): + """ + Unit tests for SupplierQuotation. + Use this class for testing individual functions and methods. + """ + + pass + + +class TestPurchaseOrder(IntegrationTestCase): + def test_supplier_quotation_qty(self): + sq = frappe.copy_doc(self.globalTestRecords["Supplier Quotation"][0]) + sq.items[0].qty = 0 + with self.assertRaises(InvalidQtyError): + sq.save() + + # No error with qty=1 + sq.items[0].qty = 1 + sq.save() + self.assertEqual(sq.items[0].qty, 1) + + def test_supplier_quotation_zero_qty(self): + """ + Test if RFQ with zero qty (Unit Price Item) is conditionally allowed. + """ + sq = frappe.copy_doc(self.globalTestRecords["Supplier Quotation"][0]) + sq.items[0].qty = 0 + + with change_settings("Buying Settings", {"allow_zero_qty_in_supplier_quotation": 1}): + sq.save() + + def test_make_purchase_order(self): + sq = frappe.copy_doc(self.globalTestRecords["Supplier Quotation"][0]).insert() +>>>>>>> 8f96c0b546 (test: Zero Qty in RFQ and Supplier Quotation) self.assertRaises(frappe.ValidationError, make_purchase_order, sq.name) @@ -30,5 +72,18 @@ class TestPurchaseOrder(FrappeTestCase): po.insert() +<<<<<<< HEAD test_records = frappe.get_test_records("Supplier Quotation") +======= + @change_settings("Buying Settings", {"allow_zero_qty_in_supplier_quotation": 1}) + def test_map_purchase_order_from_zero_qty_supplier_quotation(self): + sq = frappe.copy_doc(self.globalTestRecords["Supplier Quotation"][0]) + sq.items[0].qty = 0 + sq.submit() + + po = make_purchase_order(sq.name) + self.assertEqual(len(po.get("items")), 1) + self.assertEqual(po.get("items")[0].qty, 0) + self.assertEqual(po.get("items")[0].item_code, sq.get("items")[0].item_code) +>>>>>>> 8f96c0b546 (test: Zero Qty in RFQ and Supplier Quotation) From eba73df88e28fe5239b215503b80a56533d31e18 Mon Sep 17 00:00:00 2001 From: marination <25857446+marination@users.noreply.github.com> Date: Fri, 14 Mar 2025 18:00:40 +0100 Subject: [PATCH 05/49] test: Purchase Order with Unit Price Items - chore: Fix error message in accounts controller (cherry picked from commit eea758f5b2c986b0efccdb6f387475b9800ff4e0) # Conflicts: # erpnext/buying/doctype/purchase_order/test_purchase_order.py # erpnext/buying/doctype/supplier_quotation/test_supplier_quotation.py --- .../doctype/purchase_order/purchase_order.py | 8 +- .../purchase_order/test_purchase_order.py | 95 +++++++++++++++++++ .../test_request_for_quotation.py | 5 +- .../test_supplier_quotation.py | 5 + erpnext/controllers/accounts_controller.py | 6 +- 5 files changed, 112 insertions(+), 7 deletions(-) diff --git a/erpnext/buying/doctype/purchase_order/purchase_order.py b/erpnext/buying/doctype/purchase_order/purchase_order.py index 9ddbbb991d6..d883a41a197 100644 --- a/erpnext/buying/doctype/purchase_order/purchase_order.py +++ b/erpnext/buying/doctype/purchase_order/purchase_order.py @@ -723,8 +723,10 @@ def set_missing_values(source, target): @frappe.whitelist() def make_purchase_receipt(source_name, target_doc=None): + has_unit_price_items = frappe.db.get_value("Purchase Order", source_name, "has_unit_price_items") + def update_item(obj, target, source_parent): - target.qty = flt(obj.qty) - flt(obj.received_qty) + target.qty = flt(obj.qty) - flt(obj.received_qty) if not has_unit_price_items else 0 target.stock_qty = (flt(obj.qty) - flt(obj.received_qty)) * flt(obj.conversion_factor) target.amount = (flt(obj.qty) - flt(obj.received_qty)) * flt(obj.rate) target.base_amount = ( @@ -755,7 +757,9 @@ def make_purchase_receipt(source_name, target_doc=None): "wip_composite_asset": "wip_composite_asset", }, "postprocess": update_item, - "condition": lambda doc: abs(doc.received_qty) < abs(doc.qty) + "condition": lambda doc: ( + abs(doc.received_qty) < abs(doc.qty) if not has_unit_price_items else True + ) and doc.delivered_by_supplier != 1, }, "Purchase Taxes and Charges": {"doctype": "Purchase Taxes and Charges", "reset_value": True}, diff --git a/erpnext/buying/doctype/purchase_order/test_purchase_order.py b/erpnext/buying/doctype/purchase_order/test_purchase_order.py index 404c83cdb50..59143ed564e 100644 --- a/erpnext/buying/doctype/purchase_order/test_purchase_order.py +++ b/erpnext/buying/doctype/purchase_order/test_purchase_order.py @@ -5,7 +5,11 @@ import json import frappe +<<<<<<< HEAD from frappe.tests.utils import FrappeTestCase, change_settings +======= +from frappe.tests import IntegrationTestCase, UnitTestCase, change_settings +>>>>>>> eea758f5b2 (test: Purchase Order with Unit Price Items) from frappe.utils import add_days, flt, getdate, nowdate from frappe.utils.data import today @@ -44,6 +48,21 @@ class TestPurchaseOrder(FrappeTestCase): po.items[1].qty = 0 self.assertRaises(InvalidQtyError, po.save) +<<<<<<< HEAD +======= + # No error with qty=1 + po.items[1].qty = 1 + po.save() + self.assertEqual(po.items[1].qty, 1) + + def test_purchase_order_zero_qty(self): + po = create_purchase_order(qty=0, do_not_save=True) + + with change_settings("Buying Settings", {"allow_zero_qty_in_purchase_order": 1}): + po.save() + self.assertEqual(po.items[0].qty, 0) + +>>>>>>> eea758f5b2 (test: Purchase Order with Unit Price Items) def test_make_purchase_receipt(self): po = create_purchase_order(do_not_submit=True) self.assertRaises(frappe.ValidationError, make_purchase_receipt, po.name) @@ -1199,6 +1218,82 @@ class TestPurchaseOrder(FrappeTestCase): po.reload() self.assertEqual(po.per_billed, 100) + @IntegrationTestCase.change_settings("Buying Settings", {"allow_zero_qty_in_purchase_order": 1}) + def test_receive_zero_qty_purchase_order(self): + """ + Test the flow of a Unit Price PO and PR creation against it until completion. + Flow: + PO Qty 0 -> Receive +5 -> Receive +5 -> Update PO Qty +10 -> PO is 100% received + """ + po = create_purchase_order(qty=0) + pr = make_purchase_receipt(po.name) + + self.assertEqual(pr.items[0].qty, 0) + pr.items[0].qty = 5 + pr.submit() + + po.reload() + self.assertEqual(po.items[0].received_qty, 5) + # PO still has qty 0, so billed % should be unset + self.assertFalse(po.per_received) + self.assertEqual(po.status, "To Receive and Bill") + + # Test: PR can be made against PO as long PO qty is 0 OR PO qty > received qty + pr2 = make_purchase_receipt(po.name) + self.assertEqual(pr2.items[0].qty, 0) + pr2.items[0].qty = 5 + pr2.submit() + + po.reload() + self.assertEqual(po.items[0].received_qty, 10) + self.assertFalse(po.per_received) + self.assertEqual(po.status, "To Receive and Bill") + + # Update PO Item Qty to 10 after receipt of items + first_item_of_po = po.items[0] + trans_item = json.dumps( + [ + { + "item_code": first_item_of_po.item_code, + "rate": first_item_of_po.rate, + "qty": 10, + "docname": first_item_of_po.name, + } + ] + ) + update_child_qty_rate("Purchase Order", trans_item, po.name) + + # PO should be updated to 100% received + po.reload() + self.assertEqual(po.items[0].qty, 10) + self.assertEqual(po.per_received, 100.0) + self.assertEqual(po.status, "To Bill") + + @IntegrationTestCase.change_settings("Buying Settings", {"allow_zero_qty_in_purchase_order": 1}) + def test_bill_zero_qty_purchase_order(self): + po = create_purchase_order(qty=0) + + self.assertEqual(po.grand_total, 0) + self.assertFalse(po.per_billed) + self.assertEqual(po.items[0].qty, 0) + self.assertEqual(po.items[0].rate, 500) + + pi = make_pi_from_po(po.name) + self.assertEqual(pi.items[0].qty, 0) + self.assertEqual(pi.items[0].rate, 500) + + pi.items[0].qty = 5 + pi.submit() + + self.assertEqual(pi.grand_total, 2500) + + po.reload() + self.assertEqual(po.items[0].amount, 0) + self.assertEqual(po.items[0].billed_amt, 2500) + # PO still has qty 0, so billed % should be unset + self.assertFalse(po.per_billed) + self.assertEqual(po.status, "To Receive and Bill") + def create_po_for_sc_testing(): from erpnext.controllers.tests.test_subcontracting_controller import ( diff --git a/erpnext/buying/doctype/request_for_quotation/test_request_for_quotation.py b/erpnext/buying/doctype/request_for_quotation/test_request_for_quotation.py index 6e21f4596f6..cd6d61ab926 100644 --- a/erpnext/buying/doctype/request_for_quotation/test_request_for_quotation.py +++ b/erpnext/buying/doctype/request_for_quotation/test_request_for_quotation.py @@ -55,6 +55,7 @@ class TestRequestforQuotation(IntegrationTestCase): with change_settings("Buying Settings", {"allow_zero_qty_in_request_for_quotation": 1}): rfq.save() + self.assertEqual(rfq.items[0].qty, 0) >>>>>>> 8f96c0b546 (test: Zero Qty in RFQ and Supplier Quotation) def test_quote_status(self): @@ -197,8 +198,8 @@ class TestRequestforQuotation(IntegrationTestCase): supplier_doc.reload() self.assertTrue(supplier_doc.portal_users[0].user) - @change_settings("Buying Settings", {"allow_zero_qty_in_request_for_quotation": 1}) - def test_map_supplier_quotation_from_zero_qty_rfq(self): + @IntegrationTestCase.change_settings("Buying Settings", {"allow_zero_qty_in_request_for_quotation": 1}) + def test_supplier_quotation_from_zero_qty_rfq(self): rfq = make_request_for_quotation(qty=0) sq = make_supplier_quotation_from_rfq(rfq.name, for_supplier=rfq.get("suppliers")[0].supplier) diff --git a/erpnext/buying/doctype/supplier_quotation/test_supplier_quotation.py b/erpnext/buying/doctype/supplier_quotation/test_supplier_quotation.py index 31bee01e775..87014c135f5 100644 --- a/erpnext/buying/doctype/supplier_quotation/test_supplier_quotation.py +++ b/erpnext/buying/doctype/supplier_quotation/test_supplier_quotation.py @@ -50,6 +50,7 @@ class TestPurchaseOrder(IntegrationTestCase): with change_settings("Buying Settings", {"allow_zero_qty_in_supplier_quotation": 1}): sq.save() + self.assertEqual(sq.items[0].qty, 0) def test_make_purchase_order(self): sq = frappe.copy_doc(self.globalTestRecords["Supplier Quotation"][0]).insert() @@ -73,10 +74,14 @@ class TestPurchaseOrder(IntegrationTestCase): po.insert() <<<<<<< HEAD +<<<<<<< HEAD test_records = frappe.get_test_records("Supplier Quotation") ======= @change_settings("Buying Settings", {"allow_zero_qty_in_supplier_quotation": 1}) +======= + @IntegrationTestCase.change_settings("Buying Settings", {"allow_zero_qty_in_supplier_quotation": 1}) +>>>>>>> eea758f5b2 (test: Purchase Order with Unit Price Items) def test_map_purchase_order_from_zero_qty_supplier_quotation(self): sq = frappe.copy_doc(self.globalTestRecords["Supplier Quotation"][0]) sq.items[0].qty = 0 diff --git a/erpnext/controllers/accounts_controller.py b/erpnext/controllers/accounts_controller.py index a3180388141..28d55973d74 100644 --- a/erpnext/controllers/accounts_controller.py +++ b/erpnext/controllers/accounts_controller.py @@ -3746,9 +3746,9 @@ def update_child_qty_rate(parent_doctype, trans_items, parent_doctype_name, chil ) if amount_below_billed_amt and row_rate > 0.0: frappe.throw( - _("Row #{0}: Cannot set Rate if amount is greater than billed amount for Item {1}.").format( - child_item.idx, child_item.item_code - ) + _( + "Row #{0}: Cannot set Rate if the billed amount is greater than the amount for Item {1}." + ).format(child_item.idx, child_item.item_code) ) else: child_item.rate = row_rate From 2d96a625303ca9b7ad629f3b8be3ea241aa83810 Mon Sep 17 00:00:00 2001 From: marination <25857446+marination@users.noreply.github.com> Date: Fri, 14 Mar 2025 18:54:11 +0100 Subject: [PATCH 06/49] test: Sales Order + fix: Mapping of Items from Quotation & SO (cherry picked from commit 55981c8358f1d9f503ceec918f217c209896180f) # Conflicts: # erpnext/selling/doctype/sales_order/test_sales_order.py --- .../purchase_order/test_purchase_order.py | 2 +- .../selling/doctype/quotation/quotation.py | 8 +- .../doctype/sales_order/sales_order.js | 8 +- .../doctype/sales_order/sales_order.py | 9 +- .../doctype/sales_order/test_sales_order.py | 113 ++++++++++++++++++ 5 files changed, 130 insertions(+), 10 deletions(-) diff --git a/erpnext/buying/doctype/purchase_order/test_purchase_order.py b/erpnext/buying/doctype/purchase_order/test_purchase_order.py index 59143ed564e..74b23434b2f 100644 --- a/erpnext/buying/doctype/purchase_order/test_purchase_order.py +++ b/erpnext/buying/doctype/purchase_order/test_purchase_order.py @@ -1234,7 +1234,7 @@ class TestPurchaseOrder(FrappeTestCase): po.reload() self.assertEqual(po.items[0].received_qty, 5) - # PO still has qty 0, so billed % should be unset + # PO still has qty 0, so received % should be unset self.assertFalse(po.per_received) self.assertEqual(po.status, "To Receive and Bill") diff --git a/erpnext/selling/doctype/quotation/quotation.py b/erpnext/selling/doctype/quotation/quotation.py index a4f79c1683d..ff33cc43f17 100644 --- a/erpnext/selling/doctype/quotation/quotation.py +++ b/erpnext/selling/doctype/quotation/quotation.py @@ -380,6 +380,8 @@ def _make_sales_order(source_name, target_doc=None, ignore_permissions=False): as_list=1, ) ) + # 0 qty is accepted, as the qty uncertain for some items + has_unit_price_items = frappe.db.get_value("Quotation", source_name, "has_unit_price_items") selected_rows = [x.get("name") for x in frappe.flags.get("args", {}).get("selected_items", [])] @@ -427,14 +429,12 @@ def _make_sales_order(source_name, target_doc=None, ignore_permissions=False): 2. If selections: Is Alternative Item/Has Alternative Item: Map if selected and adequate qty 3. If selections: Simple row: Map if adequate qty """ - # has_unit_price_items = 0 is accepted as the qty uncertain for some items - has_unit_price_items = frappe.db.get_value("Quotation", source_name, "has_unit_price_items") - balance_qty = item.qty - ordered_items.get(item.item_code, 0.0) if balance_qty <= 0 and not has_unit_price_items: + # False if qty <=0 in a 'normal' scenario return False - has_qty = balance_qty or has_unit_price_items + has_qty: bool = (balance_qty > 0) or has_unit_price_items if not selected_rows: return not item.is_alternative diff --git a/erpnext/selling/doctype/sales_order/sales_order.js b/erpnext/selling/doctype/sales_order/sales_order.js index ca3d2ac824b..0c4cd43ec36 100644 --- a/erpnext/selling/doctype/sales_order/sales_order.js +++ b/erpnext/selling/doctype/sales_order/sales_order.js @@ -597,10 +597,12 @@ erpnext.selling.SalesOrderController = class SalesOrderController extends erpnex } if (doc.status !== "Closed") { if (doc.status !== "On Hold") { + const items_are_deliverable = this.frm.doc.items.some( + (item) => item.delivered_by_supplier === 0 && item.qty > flt(item.delivered_qty) + ); allow_delivery = - this.frm.doc.items.some( - (item) => item.delivered_by_supplier === 0 && item.qty > flt(item.delivered_qty) - ) && !this.frm.doc.skip_delivery_note; + (this.frm.doc.has_unit_price_items || items_are_deliverable) && + !this.frm.doc.skip_delivery_note; if (this.frm.has_perm("submit")) { if (flt(doc.per_delivered) < 100 || flt(doc.per_billed) < 100) { diff --git a/erpnext/selling/doctype/sales_order/sales_order.py b/erpnext/selling/doctype/sales_order/sales_order.py index 54ee5f0d21d..f1b405404fd 100755 --- a/erpnext/selling/doctype/sales_order/sales_order.py +++ b/erpnext/selling/doctype/sales_order/sales_order.py @@ -962,6 +962,9 @@ def make_delivery_note(source_name, target_doc=None, kwargs=None): kwargs = frappe._dict(kwargs) + # 0 qty is accepted, as the qty is uncertain for some items + has_unit_price_items = frappe.db.get_value("Sales Order", source_name, "has_unit_price_items") + sre_details = {} if kwargs.for_reserved_stock: sre_details = get_sre_reserved_qty_details_for_voucher("Sales Order", source_name) @@ -1008,12 +1011,14 @@ def make_delivery_note(source_name, target_doc=None, kwargs=None): if cstr(doc.delivery_date) not in frappe.flags.args.delivery_dates: return False - return abs(doc.delivered_qty) < abs(doc.qty) and doc.delivered_by_supplier != 1 + return ( + (abs(doc.delivered_qty) < abs(doc.qty)) or has_unit_price_items + ) and doc.delivered_by_supplier != 1 def update_item(source, target, source_parent): target.base_amount = (flt(source.qty) - flt(source.delivered_qty)) * flt(source.base_rate) target.amount = (flt(source.qty) - flt(source.delivered_qty)) * flt(source.rate) - target.qty = flt(source.qty) - flt(source.delivered_qty) + target.qty = flt(source.qty) - flt(source.delivered_qty) if not has_unit_price_items else 0 item = get_item_defaults(target.item_code, source_parent.company) item_group = get_item_group_defaults(target.item_code, source_parent.company) diff --git a/erpnext/selling/doctype/sales_order/test_sales_order.py b/erpnext/selling/doctype/sales_order/test_sales_order.py index 82a8535a66d..177e5083cc5 100644 --- a/erpnext/selling/doctype/sales_order/test_sales_order.py +++ b/erpnext/selling/doctype/sales_order/test_sales_order.py @@ -6,7 +6,11 @@ import json import frappe import frappe.permissions from frappe.core.doctype.user_permission.test_user_permission import create_user +<<<<<<< HEAD from frappe.tests.utils import FrappeTestCase, change_settings +======= +from frappe.tests import IntegrationTestCase, change_settings +>>>>>>> 55981c8358 (test: Sales Order + fix: Mapping of Items from Quotation & SO) from frappe.utils import add_days, flt, getdate, nowdate, today from erpnext.accounts.test.accounts_mixin import AccountsTestMixin @@ -86,6 +90,39 @@ class TestSalesOrder(AccountsTestMixin, FrappeTestCase): ) update_child_qty_rate("Sales Order", trans_item, so.name) +<<<<<<< HEAD +======= + def test_sales_order_qty(self): + so = make_sales_order(qty=1, do_not_save=True) + + # NonNegativeError with qty=-1 + so.append( + "items", + { + "item_code": "_Test Item", + "qty": -1, + "rate": 10, + }, + ) + self.assertRaises(frappe.NonNegativeError, so.save) + + # InvalidQtyError with qty=0 + so.items[1].qty = 0 + self.assertRaises(InvalidQtyError, so.save) + + # No error with qty=1 + so.items[1].qty = 1 + so.save() + self.assertEqual(so.items[0].qty, 1) + + def test_sales_order_zero_qty(self): + po = make_sales_order(qty=0, do_not_save=True) + + with change_settings("Selling Settings", {"allow_zero_qty_in_sales_order": 1}): + po.save() + self.assertEqual(po.items[0].qty, 0) + +>>>>>>> 55981c8358 (test: Sales Order + fix: Mapping of Items from Quotation & SO) def test_make_material_request(self): so = make_sales_order(do_not_submit=True) @@ -2192,6 +2229,82 @@ class TestSalesOrder(AccountsTestMixin, FrappeTestCase): po.submit() self.assertEqual(po.taxes[0].tax_amount, 2) + @IntegrationTestCase.change_settings("Selling Settings", {"allow_zero_qty_in_sales_order": 1}) + def test_deliver_zero_qty_purchase_order(self): + """ + Test the flow of a Unit Price SO and DN creation against it until completion. + Flow: + SO Qty 0 -> Deliver +5 -> Deliver +5 -> Update SO Qty +10 -> SO is 100% delivered + """ + so = make_sales_order(qty=0) + dn = make_delivery_note(so.name) + + self.assertEqual(dn.items[0].qty, 0) + dn.items[0].qty = 5 + dn.submit() + + so.reload() + self.assertEqual(so.items[0].delivered_qty, 5) + # SO still has qty 0, so delivered % should be unset + self.assertFalse(so.per_delivered) + self.assertEqual(so.status, "To Deliver and Bill") + + # Test: DN can be made against SO as long SO qty is 0 OR SO qty > delivered qty + dn2 = make_delivery_note(so.name) + self.assertEqual(dn2.items[0].qty, 0) + dn2.items[0].qty = 5 + dn2.submit() + + so.reload() + self.assertEqual(so.items[0].delivered_qty, 10) + self.assertFalse(so.per_delivered) + self.assertEqual(so.status, "To Deliver and Bill") + + # Update SO Item Qty to 10 after delivery of items + first_item_of_so = so.items[0] + trans_item = json.dumps( + [ + { + "item_code": first_item_of_so.item_code, + "rate": first_item_of_so.rate, + "qty": 10, + "docname": first_item_of_so.name, + } + ] + ) + update_child_qty_rate("Sales Order", trans_item, so.name) + + # SO should be updated to 100% delivered + so.reload() + self.assertEqual(so.items[0].qty, 10) + self.assertEqual(so.per_delivered, 100.0) + self.assertEqual(so.status, "To Bill") + + @IntegrationTestCase.change_settings("Selling Settings", {"allow_zero_qty_in_sales_order": 1}) + def test_bill_zero_qty_sales_order(self): + so = make_sales_order(qty=0) + + self.assertEqual(so.grand_total, 0) + self.assertFalse(so.per_billed) + self.assertEqual(so.items[0].qty, 0) + self.assertEqual(so.items[0].rate, 100) + + si = make_sales_invoice(so.name) + self.assertEqual(si.items[0].qty, 0) + self.assertEqual(si.items[0].rate, 100) + + si.items[0].qty = 5 + si.submit() + + self.assertEqual(si.grand_total, 500) + + so.reload() + self.assertEqual(so.items[0].amount, 0) + self.assertEqual(so.items[0].billed_amt, 500) + # SO still has qty 0, so billed % should be unset + self.assertFalse(so.per_billed) + self.assertEqual(so.status, "To Deliver and Bill") + def automatically_fetch_payment_terms(enable=1): accounts_settings = frappe.get_doc("Accounts Settings") From d9636018f57692f72e3defc9610bf4b9177a2a44 Mon Sep 17 00:00:00 2001 From: marination <25857446+marination@users.noreply.github.com> Date: Mon, 17 Mar 2025 19:08:16 +0100 Subject: [PATCH 07/49] fix: Treat rows as Unit Price rows only until the qty is 0 - The unit price check should depend on the row qty being 0 - Once the row ceases to be 0, it is treated as an ordinary row - test: PO, SO and Quotation (cherry picked from commit 0447c7be0abf428e049b76f871a691214c3b613a) # Conflicts: # erpnext/selling/doctype/quotation/test_quotation.py # erpnext/selling/doctype/sales_order/test_sales_order.py --- .../doctype/purchase_order/purchase_order.py | 7 +- .../purchase_order/test_purchase_order.py | 22 ++-- .../selling/doctype/quotation/quotation.py | 21 ++-- .../doctype/quotation/test_quotation.py | 69 ++++++++++++ .../doctype/sales_order/sales_order.py | 32 ++++-- .../doctype/sales_order/test_sales_order.py | 104 +++++++++++++++--- 6 files changed, 203 insertions(+), 52 deletions(-) diff --git a/erpnext/buying/doctype/purchase_order/purchase_order.py b/erpnext/buying/doctype/purchase_order/purchase_order.py index d883a41a197..43ef287854e 100644 --- a/erpnext/buying/doctype/purchase_order/purchase_order.py +++ b/erpnext/buying/doctype/purchase_order/purchase_order.py @@ -725,8 +725,11 @@ def set_missing_values(source, target): def make_purchase_receipt(source_name, target_doc=None): has_unit_price_items = frappe.db.get_value("Purchase Order", source_name, "has_unit_price_items") + def is_unit_price_row(source): + return has_unit_price_items and source.qty == 0 + def update_item(obj, target, source_parent): - target.qty = flt(obj.qty) - flt(obj.received_qty) if not has_unit_price_items else 0 + target.qty = flt(obj.qty) if is_unit_price_row(obj) else flt(obj.qty) - flt(obj.received_qty) target.stock_qty = (flt(obj.qty) - flt(obj.received_qty)) * flt(obj.conversion_factor) target.amount = (flt(obj.qty) - flt(obj.received_qty)) * flt(obj.rate) target.base_amount = ( @@ -758,7 +761,7 @@ def make_purchase_receipt(source_name, target_doc=None): }, "postprocess": update_item, "condition": lambda doc: ( - abs(doc.received_qty) < abs(doc.qty) if not has_unit_price_items else True + True if is_unit_price_row(doc) else abs(doc.received_qty) < abs(doc.qty) ) and doc.delivered_by_supplier != 1, }, diff --git a/erpnext/buying/doctype/purchase_order/test_purchase_order.py b/erpnext/buying/doctype/purchase_order/test_purchase_order.py index 74b23434b2f..38710f7a111 100644 --- a/erpnext/buying/doctype/purchase_order/test_purchase_order.py +++ b/erpnext/buying/doctype/purchase_order/test_purchase_order.py @@ -1234,18 +1234,6 @@ class TestPurchaseOrder(FrappeTestCase): po.reload() self.assertEqual(po.items[0].received_qty, 5) - # PO still has qty 0, so received % should be unset - self.assertFalse(po.per_received) - self.assertEqual(po.status, "To Receive and Bill") - - # Test: PR can be made against PO as long PO qty is 0 OR PO qty > received qty - pr2 = make_purchase_receipt(po.name) - self.assertEqual(pr2.items[0].qty, 0) - pr2.items[0].qty = 5 - pr2.submit() - - po.reload() - self.assertEqual(po.items[0].received_qty, 10) self.assertFalse(po.per_received) self.assertEqual(po.status, "To Receive and Bill") @@ -1263,9 +1251,19 @@ class TestPurchaseOrder(FrappeTestCase): ) update_child_qty_rate("Purchase Order", trans_item, po.name) + # Test: PR can be made against PO as long PO qty is 0 OR PO qty > received qty + pr2 = make_purchase_receipt(po.name) + + po.reload() + self.assertEqual(po.items[0].qty, 10) + self.assertEqual(pr2.items[0].qty, 5) + + pr2.submit() + # PO should be updated to 100% received po.reload() self.assertEqual(po.items[0].qty, 10) + self.assertEqual(po.items[0].received_qty, 10) self.assertEqual(po.per_received, 100.0) self.assertEqual(po.status, "To Bill") diff --git a/erpnext/selling/doctype/quotation/quotation.py b/erpnext/selling/doctype/quotation/quotation.py index ff33cc43f17..7816485a9f0 100644 --- a/erpnext/selling/doctype/quotation/quotation.py +++ b/erpnext/selling/doctype/quotation/quotation.py @@ -380,10 +380,14 @@ def _make_sales_order(source_name, target_doc=None, ignore_permissions=False): as_list=1, ) ) + + selected_rows = [x.get("name") for x in frappe.flags.get("args", {}).get("selected_items", [])] + # 0 qty is accepted, as the qty uncertain for some items has_unit_price_items = frappe.db.get_value("Quotation", source_name, "has_unit_price_items") - selected_rows = [x.get("name") for x in frappe.flags.get("args", {}).get("selected_items", [])] + def is_unit_price_row(source) -> bool: + return has_unit_price_items and source.qty == 0 def set_missing_values(source, target): if customer: @@ -413,7 +417,7 @@ def _make_sales_order(source_name, target_doc=None, ignore_permissions=False): target.run_method("calculate_taxes_and_totals") def update_item(obj, target, source_parent): - balance_qty = obj.qty - ordered_items.get(obj.item_code, 0.0) + balance_qty = obj.qty if is_unit_price_row(obj) else obj.qty - ordered_items.get(obj.item_code, 0.0) target.qty = balance_qty if balance_qty > 0 else 0 target.stock_qty = flt(target.qty) * flt(obj.conversion_factor) @@ -427,23 +431,22 @@ def _make_sales_order(source_name, target_doc=None, ignore_permissions=False): Row mapping from Quotation to Sales order: 1. If no selections, map all non-alternative rows (that sum up to the grand total) 2. If selections: Is Alternative Item/Has Alternative Item: Map if selected and adequate qty - 3. If selections: Simple row: Map if adequate qty + 3. If no selections: Simple row: Map if adequate qty """ balance_qty = item.qty - ordered_items.get(item.item_code, 0.0) - if balance_qty <= 0 and not has_unit_price_items: - # False if qty <=0 in a 'normal' scenario - return False + has_valid_qty: bool = (balance_qty > 0) or is_unit_price_row(item) - has_qty: bool = (balance_qty > 0) or has_unit_price_items + if not has_valid_qty: + return False if not selected_rows: return not item.is_alternative if selected_rows and (item.is_alternative or item.has_alternative_item): - return (item.name in selected_rows) and has_qty + return item.name in selected_rows # Simple row - return has_qty + return True doclist = get_mapped_doc( "Quotation", diff --git a/erpnext/selling/doctype/quotation/test_quotation.py b/erpnext/selling/doctype/quotation/test_quotation.py index b41637143e5..9a8d2d5c5fa 100644 --- a/erpnext/selling/doctype/quotation/test_quotation.py +++ b/erpnext/selling/doctype/quotation/test_quotation.py @@ -2,13 +2,49 @@ # License: GNU General Public License v3. See license.txt import frappe +<<<<<<< HEAD from frappe.tests.utils import FrappeTestCase +======= +from frappe.tests import IntegrationTestCase, UnitTestCase, change_settings +>>>>>>> 0447c7be0a (fix: Treat rows as Unit Price rows only until the qty is 0) from frappe.utils import add_days, add_months, flt, getdate, nowdate test_dependencies = ["Product Bundle"] +<<<<<<< HEAD class TestQuotation(FrappeTestCase): +======= +class UnitTestQuotation(UnitTestCase): + """ + Unit tests for Quotation. + Use this class for testing individual functions and methods. + """ + + pass + + +class TestQuotation(IntegrationTestCase): + def test_quotation_qty(self): + qo = make_quotation(qty=0, do_not_save=True) + with self.assertRaises(InvalidQtyError): + qo.save() + + # No error with qty=1 + qo.items[0].qty = 1 + qo.save() + self.assertEqual(qo.items[0].qty, 1) + + def test_quotation_zero_qty(self): + """ + Test if Quote with zero qty (Unit Price Item) is conditionally allowed. + """ + qo = make_quotation(qty=0, do_not_save=True) + with change_settings("Selling Settings", {"allow_zero_qty_in_quotation": 1}): + qo.save() + self.assertEqual(qo.items[0].qty, 0) + +>>>>>>> 0447c7be0a (fix: Treat rows as Unit Price rows only until the qty is 0) def test_make_quotation_without_terms(self): quotation = make_quotation(do_not_save=1) self.assertFalse(quotation.get("payment_schedule")) @@ -761,6 +797,39 @@ class TestQuotation(FrappeTestCase): self.assertEqual(quotation.rounding_adjustment, 0) self.assertEqual(quotation.rounded_total, 0) + @IntegrationTestCase.change_settings("Selling Settings", {"allow_zero_qty_in_quotation": 1}) + def test_so_from_zero_qty_quotation(self): + from erpnext.selling.doctype.quotation.quotation import make_sales_order + from erpnext.stock.doctype.item.test_item import make_item + + make_item("_Test Item 2", {"is_stock_item": 1}) + quotation = make_quotation(qty=0, do_not_save=1) + quotation.append("items", {"item_code": "_Test Item 2", "qty": 10, "rate": 100}) + quotation.submit() + + sales_order = make_sales_order(quotation.name) + sales_order.delivery_date = nowdate() + self.assertEqual(sales_order.items[0].qty, 0) + self.assertEqual(sales_order.items[1].qty, 10) + + sales_order.items[0].qty = 10 + sales_order.items[1].qty = 5 + sales_order.submit() + + quotation.reload() + self.assertEqual(quotation.status, "Partially Ordered") + + sales_order_2 = make_sales_order(quotation.name) + sales_order_2.delivery_date = nowdate() + self.assertEqual(sales_order_2.items[0].qty, 0) + self.assertEqual(sales_order_2.items[1].qty, 5) + + del sales_order_2.items[0] + sales_order_2.submit() + + quotation.reload() + self.assertEqual(quotation.status, "Ordered") + test_records = frappe.get_test_records("Quotation") diff --git a/erpnext/selling/doctype/sales_order/sales_order.py b/erpnext/selling/doctype/sales_order/sales_order.py index f1b405404fd..e0cfa8d5250 100755 --- a/erpnext/selling/doctype/sales_order/sales_order.py +++ b/erpnext/selling/doctype/sales_order/sales_order.py @@ -962,9 +962,6 @@ def make_delivery_note(source_name, target_doc=None, kwargs=None): kwargs = frappe._dict(kwargs) - # 0 qty is accepted, as the qty is uncertain for some items - has_unit_price_items = frappe.db.get_value("Sales Order", source_name, "has_unit_price_items") - sre_details = {} if kwargs.for_reserved_stock: sre_details = get_sre_reserved_qty_details_for_voucher("Sales Order", source_name) @@ -975,6 +972,12 @@ def make_delivery_note(source_name, target_doc=None, kwargs=None): "Sales Team": {"doctype": "Sales Team", "add_if_empty": True}, } + # 0 qty is accepted, as the qty is uncertain for some items + has_unit_price_items = frappe.db.get_value("Sales Order", source_name, "has_unit_price_items") + + def is_unit_price_row(source): + return has_unit_price_items and source.qty == 0 + def set_missing_values(source, target): if kwargs.get("ignore_pricing_rule"): # Skip pricing rule when the dn is creating from the pick list @@ -1012,13 +1015,15 @@ def make_delivery_note(source_name, target_doc=None, kwargs=None): return False return ( - (abs(doc.delivered_qty) < abs(doc.qty)) or has_unit_price_items + (abs(doc.delivered_qty) < abs(doc.qty)) or is_unit_price_row(doc) ) and doc.delivered_by_supplier != 1 def update_item(source, target, source_parent): target.base_amount = (flt(source.qty) - flt(source.delivered_qty)) * flt(source.base_rate) target.amount = (flt(source.qty) - flt(source.delivered_qty)) * flt(source.rate) - target.qty = flt(source.qty) - flt(source.delivered_qty) if not has_unit_price_items else 0 + target.qty = ( + flt(source.qty) if is_unit_price_row(source) else flt(source.qty) - flt(source.delivered_qty) + ) item = get_item_defaults(target.item_code, source_parent.company) item_group = get_item_group_defaults(target.item_code, source_parent.company) @@ -1095,6 +1100,12 @@ def make_delivery_note(source_name, target_doc=None, kwargs=None): @frappe.whitelist() def make_sales_invoice(source_name, target_doc=None, ignore_permissions=False): + # 0 qty is accepted, as the qty is uncertain for some items + has_unit_price_items = frappe.db.get_value("Sales Order", source_name, "has_unit_price_items") + + def is_unit_price_row(source): + return has_unit_price_items and source.qty == 0 + def postprocess(source, target): set_missing_values(source, target) # Get the advance paid Journal Entries in Sales Invoice Advance @@ -1135,7 +1146,7 @@ def make_sales_invoice(source_name, target_doc=None, ignore_permissions=False): target.qty = ( target.amount / flt(source.rate) if (source.rate and source.billed_amt) - else source.qty - source.returned_qty + else (source.qty if is_unit_price_row(source) else source.qty - source.returned_qty) ) if source_parent.project: @@ -1148,8 +1159,6 @@ def make_sales_invoice(source_name, target_doc=None, ignore_permissions=False): if cost_center: target.cost_center = cost_center - # has_unit_price_items = 0 is accepted as the qty uncertain for some items - has_unit_price_items = frappe.db.get_value("Sales Order", source_name, "has_unit_price_items") doclist = get_mapped_doc( "Sales Order", source_name, @@ -1171,9 +1180,10 @@ def make_sales_invoice(source_name, target_doc=None, ignore_permissions=False): }, "postprocess": update_item, "condition": lambda doc: ( - doc.qty and (doc.base_amount == 0 or abs(doc.billed_amt) < abs(doc.amount)) - ) - or has_unit_price_items, + True + if is_unit_price_row(doc) + else (doc.qty and (doc.base_amount == 0 or abs(doc.billed_amt) < abs(doc.amount))) + ), }, "Sales Taxes and Charges": { "doctype": "Sales Taxes and Charges", diff --git a/erpnext/selling/doctype/sales_order/test_sales_order.py b/erpnext/selling/doctype/sales_order/test_sales_order.py index 177e5083cc5..c3deeca2d0e 100644 --- a/erpnext/selling/doctype/sales_order/test_sales_order.py +++ b/erpnext/selling/doctype/sales_order/test_sales_order.py @@ -1983,6 +1983,79 @@ class TestSalesOrder(AccountsTestMixin, FrappeTestCase): self.assertEqual(so.items[0].rate, scenario.get("expected_rate")) self.assertEqual(so.packed_items[0].rate, scenario.get("expected_rate")) +<<<<<<< HEAD +======= + @patch( + # this also shadows one (1) call to _get_payment_gateway_controller + "erpnext.accounts.doctype.payment_request.payment_request.PaymentRequest.get_payment_url", + return_value=None, + ) + def test_sales_order_advance_payment_status(self, mocked_get_payment_url): + from erpnext.accounts.doctype.payment_entry.test_payment_entry import get_payment_entry + from erpnext.accounts.doctype.payment_request.payment_request import make_payment_request + + # Flow progressing to SI with payment entries "moved" from SO to SI + so = make_sales_order(qty=1, rate=100, do_not_submit=True) + # no-op; for optical consistency with how a webshop SO would look like + so.order_type = "Shopping Cart" + so.submit() + self.assertEqual(frappe.db.get_value(so.doctype, so.name, "advance_payment_status"), "Not Requested") + + pr = make_payment_request( + dt=so.doctype, + dn=so.name, + order_type="Shopping Cart", + submit_doc=True, + return_doc=True, + mute_email=True, + ) + self.assertEqual(frappe.db.get_value(so.doctype, so.name, "advance_payment_status"), "Requested") + + pe = pr.set_as_paid() + pr.reload() # status updated + pe.reload() # references moved to Sales Invoice + self.assertEqual(pr.status, "Paid") + self.assertEqual(pe.references[0].reference_doctype, "Sales Invoice") + self.assertEqual(frappe.db.get_value(so.doctype, so.name, "advance_payment_status"), "Fully Paid") + + pe.cancel() + pr.reload() + self.assertEqual(pr.status, "Paid") # TODO: this might be a bug + so.reload() # reload + # regardless, since the references have already "handed-over" to SI, + # the SO keeps its historical state at the time of hand over + self.assertEqual(frappe.db.get_value(so.doctype, so.name, "advance_payment_status"), "Fully Paid") + + pr.cancel() + self.assertEqual( + frappe.db.get_value(so.doctype, so.name, "advance_payment_status"), "Not Requested" + ) # TODO: this might be a bug; handover has happened + + # Flow NOT progressing to SI with payment entries NOT "moved" + so = make_sales_order(qty=1, rate=100) + self.assertEqual(frappe.db.get_value(so.doctype, so.name, "advance_payment_status"), "Not Requested") + + pr = make_payment_request( + dt=so.doctype, dn=so.name, submit_doc=True, return_doc=True, mute_email=True + ) + self.assertEqual(frappe.db.get_value(so.doctype, so.name, "advance_payment_status"), "Requested") + + pe = get_payment_entry(so.doctype, so.name).save().submit() + self.assertEqual(frappe.db.get_value(so.doctype, so.name, "advance_payment_status"), "Fully Paid") + + pe.reload() + pe.cancel() + self.assertEqual( + frappe.db.get_value(so.doctype, so.name, "advance_payment_status"), "Requested" + ) # here: reset + + pr.reload() + pr.cancel() + self.assertEqual( + frappe.db.get_value(so.doctype, so.name, "advance_payment_status"), "Not Requested" + ) # here: reset + +>>>>>>> 0447c7be0a (fix: Treat rows as Unit Price rows only until the qty is 0) def test_pick_list_without_rejected_materials(self): serial_and_batch_item = make_item( "_Test Serial and Batch Item for Rejected Materials", @@ -2234,7 +2307,7 @@ class TestSalesOrder(AccountsTestMixin, FrappeTestCase): """ Test the flow of a Unit Price SO and DN creation against it until completion. Flow: - SO Qty 0 -> Deliver +5 -> Deliver +5 -> Update SO Qty +10 -> SO is 100% delivered + SO Qty 0 -> Deliver +5 -> Update SO Qty +10 -> Deliver +5 -> SO is 100% delivered """ so = make_sales_order(qty=0) dn = make_delivery_note(so.name) @@ -2243,24 +2316,13 @@ class TestSalesOrder(AccountsTestMixin, FrappeTestCase): dn.items[0].qty = 5 dn.submit() + # Test SO impact after DN so.reload() self.assertEqual(so.items[0].delivered_qty, 5) - # SO still has qty 0, so delivered % should be unset self.assertFalse(so.per_delivered) self.assertEqual(so.status, "To Deliver and Bill") - # Test: DN can be made against SO as long SO qty is 0 OR SO qty > delivered qty - dn2 = make_delivery_note(so.name) - self.assertEqual(dn2.items[0].qty, 0) - dn2.items[0].qty = 5 - dn2.submit() - - so.reload() - self.assertEqual(so.items[0].delivered_qty, 10) - self.assertFalse(so.per_delivered) - self.assertEqual(so.status, "To Deliver and Bill") - - # Update SO Item Qty to 10 after delivery of items + # Update SO Qty to final qty first_item_of_so = so.items[0] trans_item = json.dumps( [ @@ -2274,9 +2336,17 @@ class TestSalesOrder(AccountsTestMixin, FrappeTestCase): ) update_child_qty_rate("Sales Order", trans_item, so.name) - # SO should be updated to 100% delivered + # Test: DN maps pending qty from SO + dn2 = make_delivery_note(so.name) + so.reload() self.assertEqual(so.items[0].qty, 10) + self.assertEqual(dn2.items[0].qty, 5) + + dn2.submit() + + so.reload() + self.assertEqual(so.items[0].delivered_qty, 10) self.assertEqual(so.per_delivered, 100.0) self.assertEqual(so.status, "To Bill") @@ -2296,11 +2366,9 @@ class TestSalesOrder(AccountsTestMixin, FrappeTestCase): si.items[0].qty = 5 si.submit() - self.assertEqual(si.grand_total, 500) - so.reload() self.assertEqual(so.items[0].amount, 0) - self.assertEqual(so.items[0].billed_amt, 500) + self.assertEqual(so.items[0].billed_amt, si.grand_total) # SO still has qty 0, so billed % should be unset self.assertFalse(so.per_billed) self.assertEqual(so.status, "To Deliver and Bill") From f94a14c06ae63afcfe4d6d33757f80bc1f068002 Mon Sep 17 00:00:00 2001 From: marination <25857446+marination@users.noreply.github.com> Date: Mon, 17 Mar 2025 19:26:45 +0100 Subject: [PATCH 08/49] fix: Headline rendered twice on first save - `refresh` gets triggered twice and that renders the note twice - Remove any existing note before rendering (cherry picked from commit bf62f9ad578e9d655451088398eaaa2f772348c7) --- erpnext/public/js/controllers/transaction.js | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/erpnext/public/js/controllers/transaction.js b/erpnext/public/js/controllers/transaction.js index 886ddd2acc0..b7706028114 100644 --- a/erpnext/public/js/controllers/transaction.js +++ b/erpnext/public/js/controllers/transaction.js @@ -2776,8 +2776,14 @@ erpnext.apply_putaway_rule = (frm, purpose=null) => { erpnext.set_unit_price_items_note = (frm) => { if (frm.doc.has_unit_price_items && !frm.is_new()) { - frm.dashboard.set_headline_alert( - __("The {0} contains Unit Price Items with 0 Qty.", [__(frm.doc.doctype)]), + // Remove existing note + const $note = $(frm.layout.wrapper.find(".unit-price-items-note")); + if ($note.length) { $note.parent().remove(); } + + frm.layout.show_message( + `
+ ${__("The {0} contains Unit Price Items.", [__(frm.doc.doctype)])} +
`, "yellow", true ); From 3deb11e5b231520e79ee1df424634d87f4c8e486 Mon Sep 17 00:00:00 2001 From: marination <25857446+marination@users.noreply.github.com> Date: Tue, 13 May 2025 13:35:19 +0200 Subject: [PATCH 09/49] fix: Merge conflicts --- .../buying_settings/buying_settings.json | 9 -- .../purchase_order/purchase_order.json | 9 +- .../purchase_order/test_purchase_order.py | 18 +-- .../request_for_quotation.json | 4 - .../test_request_for_quotation.py | 31 +---- .../test_supplier_quotation.py | 63 ++--------- erpnext/controllers/accounts_controller.py | 11 +- .../selling/doctype/quotation/quotation.json | 8 -- .../doctype/quotation/test_quotation.py | 31 +---- .../doctype/sales_order/sales_order.py | 10 -- .../doctype/sales_order/test_sales_order.py | 107 +----------------- .../selling_settings/selling_settings.json | 46 +------- 12 files changed, 19 insertions(+), 328 deletions(-) diff --git a/erpnext/buying/doctype/buying_settings/buying_settings.json b/erpnext/buying/doctype/buying_settings/buying_settings.json index a92eb23a23d..4a89ff78862 100644 --- a/erpnext/buying/doctype/buying_settings/buying_settings.json +++ b/erpnext/buying/doctype/buying_settings/buying_settings.json @@ -236,11 +236,7 @@ "index_web_pages_for_search": 1, "issingle": 1, "links": [], -<<<<<<< HEAD - "modified": "2024-01-31 13:34:18.101256", -======= "modified": "2025-03-03 17:32:25.939482", ->>>>>>> e403d3f153 (feat: Unit Price Items in Buying (RFQ, SQ, PO)) "modified_by": "Administrator", "module": "Buying", "name": "Buying Settings", @@ -286,12 +282,7 @@ "role": "Purchase User" } ], -<<<<<<< HEAD "sort_field": "modified", -======= - "row_format": "Dynamic", - "sort_field": "creation", ->>>>>>> e403d3f153 (feat: Unit Price Items in Buying (RFQ, SQ, PO)) "sort_order": "DESC", "states": [], "track_changes": 1 diff --git a/erpnext/buying/doctype/purchase_order/purchase_order.json b/erpnext/buying/doctype/purchase_order/purchase_order.json index bc63e8756ad..59b44a22e61 100644 --- a/erpnext/buying/doctype/purchase_order/purchase_order.json +++ b/erpnext/buying/doctype/purchase_order/purchase_order.json @@ -1281,20 +1281,19 @@ "print_hide": 1 }, { -<<<<<<< HEAD "fieldname": "dispatch_address_display", "fieldtype": "Text Editor", "label": "Dispatch Address Details", "print_hide": 1, "read_only": 1 -======= + }, + { "default": "0", "fieldname": "has_unit_price_items", "fieldtype": "Check", "hidden": 1, "label": "Has Unit Price Items", "no_copy": 1 ->>>>>>> e403d3f153 (feat: Unit Price Items in Buying (RFQ, SQ, PO)) } ], "grid_page_length": 50, @@ -1302,11 +1301,7 @@ "idx": 105, "is_submittable": 1, "links": [], -<<<<<<< HEAD "modified": "2025-04-09 16:54:08.836106", -======= - "modified": "2025-03-03 16:48:08.697520", ->>>>>>> e403d3f153 (feat: Unit Price Items in Buying (RFQ, SQ, PO)) "modified_by": "Administrator", "module": "Buying", "name": "Purchase Order", diff --git a/erpnext/buying/doctype/purchase_order/test_purchase_order.py b/erpnext/buying/doctype/purchase_order/test_purchase_order.py index 38710f7a111..68286333937 100644 --- a/erpnext/buying/doctype/purchase_order/test_purchase_order.py +++ b/erpnext/buying/doctype/purchase_order/test_purchase_order.py @@ -5,11 +5,7 @@ import json import frappe -<<<<<<< HEAD from frappe.tests.utils import FrappeTestCase, change_settings -======= -from frappe.tests import IntegrationTestCase, UnitTestCase, change_settings ->>>>>>> eea758f5b2 (test: Purchase Order with Unit Price Items) from frappe.utils import add_days, flt, getdate, nowdate from frappe.utils.data import today @@ -48,13 +44,6 @@ class TestPurchaseOrder(FrappeTestCase): po.items[1].qty = 0 self.assertRaises(InvalidQtyError, po.save) -<<<<<<< HEAD -======= - # No error with qty=1 - po.items[1].qty = 1 - po.save() - self.assertEqual(po.items[1].qty, 1) - def test_purchase_order_zero_qty(self): po = create_purchase_order(qty=0, do_not_save=True) @@ -62,7 +51,6 @@ class TestPurchaseOrder(FrappeTestCase): po.save() self.assertEqual(po.items[0].qty, 0) ->>>>>>> eea758f5b2 (test: Purchase Order with Unit Price Items) def test_make_purchase_receipt(self): po = create_purchase_order(do_not_submit=True) self.assertRaises(frappe.ValidationError, make_purchase_receipt, po.name) @@ -812,8 +800,6 @@ class TestPurchaseOrder(FrappeTestCase): po_doc.reload() self.assertEqual(po_doc.advance_paid, 5000) - from erpnext.buying.doctype.purchase_order.purchase_order import make_purchase_invoice - company_doc.book_advance_payments_in_separate_party_account = False company_doc.save() @@ -1218,7 +1204,7 @@ class TestPurchaseOrder(FrappeTestCase): po.reload() self.assertEqual(po.per_billed, 100) - @IntegrationTestCase.change_settings("Buying Settings", {"allow_zero_qty_in_purchase_order": 1}) + @change_settings("Buying Settings", {"allow_zero_qty_in_purchase_order": 1}) def test_receive_zero_qty_purchase_order(self): """ Test the flow of a Unit Price PO and PR creation against it until completion. @@ -1267,7 +1253,7 @@ class TestPurchaseOrder(FrappeTestCase): self.assertEqual(po.per_received, 100.0) self.assertEqual(po.status, "To Bill") - @IntegrationTestCase.change_settings("Buying Settings", {"allow_zero_qty_in_purchase_order": 1}) + @change_settings("Buying Settings", {"allow_zero_qty_in_purchase_order": 1}) def test_bill_zero_qty_purchase_order(self): po = create_purchase_order(qty=0) diff --git a/erpnext/buying/doctype/request_for_quotation/request_for_quotation.json b/erpnext/buying/doctype/request_for_quotation/request_for_quotation.json index 056535af17b..824484f9c20 100644 --- a/erpnext/buying/doctype/request_for_quotation/request_for_quotation.json +++ b/erpnext/buying/doctype/request_for_quotation/request_for_quotation.json @@ -322,11 +322,7 @@ "index_web_pages_for_search": 1, "is_submittable": 1, "links": [], -<<<<<<< HEAD - "modified": "2023-11-06 12:45:28.898706", -======= "modified": "2025-03-03 16:48:39.856779", ->>>>>>> e403d3f153 (feat: Unit Price Items in Buying (RFQ, SQ, PO)) "modified_by": "Administrator", "module": "Buying", "name": "Request for Quotation", diff --git a/erpnext/buying/doctype/request_for_quotation/test_request_for_quotation.py b/erpnext/buying/doctype/request_for_quotation/test_request_for_quotation.py index cd6d61ab926..417299e8519 100644 --- a/erpnext/buying/doctype/request_for_quotation/test_request_for_quotation.py +++ b/erpnext/buying/doctype/request_for_quotation/test_request_for_quotation.py @@ -5,11 +5,7 @@ from urllib.parse import urlparse import frappe -<<<<<<< HEAD -from frappe.tests.utils import FrappeTestCase -======= -from frappe.tests import IntegrationTestCase, UnitTestCase, change_settings ->>>>>>> 8f96c0b546 (test: Zero Qty in RFQ and Supplier Quotation) +from frappe.tests.utils import FrappeTestCase, change_settings from frappe.utils import nowdate from erpnext.buying.doctype.request_for_quotation.request_for_quotation import ( @@ -24,29 +20,7 @@ from erpnext.stock.doctype.item.test_item import make_item from erpnext.templates.pages.rfq import check_supplier_has_docname_access -<<<<<<< HEAD class TestRequestforQuotation(FrappeTestCase): -======= -class UnitTestRequestForQuotation(UnitTestCase): - """ - Unit tests for RequestForQuotation. - Use this class for testing individual functions and methods. - """ - - pass - - -class TestRequestforQuotation(IntegrationTestCase): - def test_rfq_qty(self): - rfq = make_request_for_quotation(qty=0, do_not_save=True) - with self.assertRaises(InvalidQtyError): - rfq.save() - - # No error with qty=1 - rfq.items[0].qty = 1 - rfq.save() - self.assertEqual(rfq.items[0].qty, 1) - def test_rfq_zero_qty(self): """ Test if RFQ with zero qty (Unit Price Item) is conditionally allowed. @@ -57,7 +31,6 @@ class TestRequestforQuotation(IntegrationTestCase): rfq.save() self.assertEqual(rfq.items[0].qty, 0) ->>>>>>> 8f96c0b546 (test: Zero Qty in RFQ and Supplier Quotation) def test_quote_status(self): rfq = make_request_for_quotation() @@ -198,7 +171,7 @@ class TestRequestforQuotation(IntegrationTestCase): supplier_doc.reload() self.assertTrue(supplier_doc.portal_users[0].user) - @IntegrationTestCase.change_settings("Buying Settings", {"allow_zero_qty_in_request_for_quotation": 1}) + @change_settings("Buying Settings", {"allow_zero_qty_in_request_for_quotation": 1}) def test_supplier_quotation_from_zero_qty_rfq(self): rfq = make_request_for_quotation(qty=0) sq = make_supplier_quotation_from_rfq(rfq.name, for_supplier=rfq.get("suppliers")[0].supplier) diff --git a/erpnext/buying/doctype/supplier_quotation/test_supplier_quotation.py b/erpnext/buying/doctype/supplier_quotation/test_supplier_quotation.py index 87014c135f5..b5b6aa707aa 100644 --- a/erpnext/buying/doctype/supplier_quotation/test_supplier_quotation.py +++ b/erpnext/buying/doctype/supplier_quotation/test_supplier_quotation.py @@ -3,58 +3,15 @@ import frappe -<<<<<<< HEAD -from frappe.tests.utils import FrappeTestCase +from frappe.tests.utils import FrappeTestCase, change_settings from frappe.utils import add_days, today +from erpnext.buying.doctype.supplier_quotation.supplier_quotation import make_purchase_order + class TestPurchaseOrder(FrappeTestCase): def test_make_purchase_order(self): - from erpnext.buying.doctype.supplier_quotation.supplier_quotation import make_purchase_order - sq = frappe.copy_doc(test_records[0]).insert() -======= -from frappe.tests import IntegrationTestCase, UnitTestCase, change_settings - -from erpnext.buying.doctype.supplier_quotation.supplier_quotation import make_purchase_order -from erpnext.controllers.accounts_controller import InvalidQtyError - - -class UnitTestSupplierQuotation(UnitTestCase): - """ - Unit tests for SupplierQuotation. - Use this class for testing individual functions and methods. - """ - - pass - - -class TestPurchaseOrder(IntegrationTestCase): - def test_supplier_quotation_qty(self): - sq = frappe.copy_doc(self.globalTestRecords["Supplier Quotation"][0]) - sq.items[0].qty = 0 - with self.assertRaises(InvalidQtyError): - sq.save() - - # No error with qty=1 - sq.items[0].qty = 1 - sq.save() - self.assertEqual(sq.items[0].qty, 1) - - def test_supplier_quotation_zero_qty(self): - """ - Test if RFQ with zero qty (Unit Price Item) is conditionally allowed. - """ - sq = frappe.copy_doc(self.globalTestRecords["Supplier Quotation"][0]) - sq.items[0].qty = 0 - - with change_settings("Buying Settings", {"allow_zero_qty_in_supplier_quotation": 1}): - sq.save() - self.assertEqual(sq.items[0].qty, 0) - - def test_make_purchase_order(self): - sq = frappe.copy_doc(self.globalTestRecords["Supplier Quotation"][0]).insert() ->>>>>>> 8f96c0b546 (test: Zero Qty in RFQ and Supplier Quotation) self.assertRaises(frappe.ValidationError, make_purchase_order, sq.name) @@ -73,17 +30,9 @@ class TestPurchaseOrder(IntegrationTestCase): po.insert() -<<<<<<< HEAD -<<<<<<< HEAD - -test_records = frappe.get_test_records("Supplier Quotation") -======= @change_settings("Buying Settings", {"allow_zero_qty_in_supplier_quotation": 1}) -======= - @IntegrationTestCase.change_settings("Buying Settings", {"allow_zero_qty_in_supplier_quotation": 1}) ->>>>>>> eea758f5b2 (test: Purchase Order with Unit Price Items) def test_map_purchase_order_from_zero_qty_supplier_quotation(self): - sq = frappe.copy_doc(self.globalTestRecords["Supplier Quotation"][0]) + sq = frappe.copy_doc(test_records[0]).insert() sq.items[0].qty = 0 sq.submit() @@ -91,4 +40,6 @@ test_records = frappe.get_test_records("Supplier Quotation") self.assertEqual(len(po.get("items")), 1) self.assertEqual(po.get("items")[0].qty, 0) self.assertEqual(po.get("items")[0].item_code, sq.get("items")[0].item_code) ->>>>>>> 8f96c0b546 (test: Zero Qty in RFQ and Supplier Quotation) + + +test_records = frappe.get_test_records("Supplier Quotation") diff --git a/erpnext/controllers/accounts_controller.py b/erpnext/controllers/accounts_controller.py index 28d55973d74..4949c4cbf04 100644 --- a/erpnext/controllers/accounts_controller.py +++ b/erpnext/controllers/accounts_controller.py @@ -1259,17 +1259,8 @@ class AccountsController(TransactionBase): ) def validate_qty_is_not_zero(self): -<<<<<<< HEAD - if self.doctype == "Purchase Receipt": + if self.doctype == "Purchase Receipt" or self.flags.allow_zero_qty: return -======= - if self.flags.allow_zero_qty: - return - - for item in self.items: - if self.doctype == "Purchase Receipt" and item.rejected_qty: - continue ->>>>>>> c1e4e7af28 (feat: Unit Price Contract) for item in self.items: if not flt(item.qty): diff --git a/erpnext/selling/doctype/quotation/quotation.json b/erpnext/selling/doctype/quotation/quotation.json index 5bf8f474625..5a674581a9b 100644 --- a/erpnext/selling/doctype/quotation/quotation.json +++ b/erpnext/selling/doctype/quotation/quotation.json @@ -1099,15 +1099,7 @@ "idx": 82, "is_submittable": 1, "links": [], -<<<<<<< HEAD -<<<<<<< HEAD - "modified": "2024-11-26 12:43:29.293637", -======= - "modified": "2025-02-28 18:52:44.063265", ->>>>>>> c1e4e7af28 (feat: Unit Price Contract) -======= "modified": "2025-03-03 16:49:20.050303", ->>>>>>> e403d3f153 (feat: Unit Price Items in Buying (RFQ, SQ, PO)) "modified_by": "Administrator", "module": "Selling", "name": "Quotation", diff --git a/erpnext/selling/doctype/quotation/test_quotation.py b/erpnext/selling/doctype/quotation/test_quotation.py index 9a8d2d5c5fa..7a3d102f35b 100644 --- a/erpnext/selling/doctype/quotation/test_quotation.py +++ b/erpnext/selling/doctype/quotation/test_quotation.py @@ -2,39 +2,13 @@ # License: GNU General Public License v3. See license.txt import frappe -<<<<<<< HEAD -from frappe.tests.utils import FrappeTestCase -======= -from frappe.tests import IntegrationTestCase, UnitTestCase, change_settings ->>>>>>> 0447c7be0a (fix: Treat rows as Unit Price rows only until the qty is 0) +from frappe.tests.utils import FrappeTestCase, change_settings from frappe.utils import add_days, add_months, flt, getdate, nowdate test_dependencies = ["Product Bundle"] -<<<<<<< HEAD class TestQuotation(FrappeTestCase): -======= -class UnitTestQuotation(UnitTestCase): - """ - Unit tests for Quotation. - Use this class for testing individual functions and methods. - """ - - pass - - -class TestQuotation(IntegrationTestCase): - def test_quotation_qty(self): - qo = make_quotation(qty=0, do_not_save=True) - with self.assertRaises(InvalidQtyError): - qo.save() - - # No error with qty=1 - qo.items[0].qty = 1 - qo.save() - self.assertEqual(qo.items[0].qty, 1) - def test_quotation_zero_qty(self): """ Test if Quote with zero qty (Unit Price Item) is conditionally allowed. @@ -44,7 +18,6 @@ class TestQuotation(IntegrationTestCase): qo.save() self.assertEqual(qo.items[0].qty, 0) ->>>>>>> 0447c7be0a (fix: Treat rows as Unit Price rows only until the qty is 0) def test_make_quotation_without_terms(self): quotation = make_quotation(do_not_save=1) self.assertFalse(quotation.get("payment_schedule")) @@ -797,7 +770,7 @@ class TestQuotation(IntegrationTestCase): self.assertEqual(quotation.rounding_adjustment, 0) self.assertEqual(quotation.rounded_total, 0) - @IntegrationTestCase.change_settings("Selling Settings", {"allow_zero_qty_in_quotation": 1}) + @change_settings("Selling Settings", {"allow_zero_qty_in_quotation": 1}) def test_so_from_zero_qty_quotation(self): from erpnext.selling.doctype.quotation.quotation import make_sales_order from erpnext.stock.doctype.item.test_item import make_item diff --git a/erpnext/selling/doctype/sales_order/sales_order.py b/erpnext/selling/doctype/sales_order/sales_order.py index e0cfa8d5250..5643f6db0f5 100755 --- a/erpnext/selling/doctype/sales_order/sales_order.py +++ b/erpnext/selling/doctype/sales_order/sales_order.py @@ -153,11 +153,7 @@ class SalesOrder(SellingController): shipping_address_name: DF.Link | None shipping_rule: DF.Link | None skip_delivery_note: DF.Check -<<<<<<< HEAD -<<<<<<< HEAD source: DF.Link | None -======= ->>>>>>> 71f65bab5e (fix: Linters) status: DF.Literal[ "", "Draft", @@ -170,12 +166,6 @@ class SalesOrder(SellingController): "Cancelled", "Closed", ] -<<<<<<< HEAD -======= - status: DF.Literal["", "Draft", "On Hold", "To Pay", "To Deliver and Bill", "To Bill", "To Deliver", "Completed", "Cancelled", "Closed"] ->>>>>>> c1e4e7af28 (feat: Unit Price Contract) -======= ->>>>>>> 71f65bab5e (fix: Linters) tax_category: DF.Link | None tax_id: DF.Data | None taxes: DF.Table[SalesTaxesandCharges] diff --git a/erpnext/selling/doctype/sales_order/test_sales_order.py b/erpnext/selling/doctype/sales_order/test_sales_order.py index c3deeca2d0e..71206c90964 100644 --- a/erpnext/selling/doctype/sales_order/test_sales_order.py +++ b/erpnext/selling/doctype/sales_order/test_sales_order.py @@ -6,11 +6,7 @@ import json import frappe import frappe.permissions from frappe.core.doctype.user_permission.test_user_permission import create_user -<<<<<<< HEAD from frappe.tests.utils import FrappeTestCase, change_settings -======= -from frappe.tests import IntegrationTestCase, change_settings ->>>>>>> 55981c8358 (test: Sales Order + fix: Mapping of Items from Quotation & SO) from frappe.utils import add_days, flt, getdate, nowdate, today from erpnext.accounts.test.accounts_mixin import AccountsTestMixin @@ -90,31 +86,6 @@ class TestSalesOrder(AccountsTestMixin, FrappeTestCase): ) update_child_qty_rate("Sales Order", trans_item, so.name) -<<<<<<< HEAD -======= - def test_sales_order_qty(self): - so = make_sales_order(qty=1, do_not_save=True) - - # NonNegativeError with qty=-1 - so.append( - "items", - { - "item_code": "_Test Item", - "qty": -1, - "rate": 10, - }, - ) - self.assertRaises(frappe.NonNegativeError, so.save) - - # InvalidQtyError with qty=0 - so.items[1].qty = 0 - self.assertRaises(InvalidQtyError, so.save) - - # No error with qty=1 - so.items[1].qty = 1 - so.save() - self.assertEqual(so.items[0].qty, 1) - def test_sales_order_zero_qty(self): po = make_sales_order(qty=0, do_not_save=True) @@ -122,7 +93,6 @@ class TestSalesOrder(AccountsTestMixin, FrappeTestCase): po.save() self.assertEqual(po.items[0].qty, 0) ->>>>>>> 55981c8358 (test: Sales Order + fix: Mapping of Items from Quotation & SO) def test_make_material_request(self): so = make_sales_order(do_not_submit=True) @@ -1983,79 +1953,6 @@ class TestSalesOrder(AccountsTestMixin, FrappeTestCase): self.assertEqual(so.items[0].rate, scenario.get("expected_rate")) self.assertEqual(so.packed_items[0].rate, scenario.get("expected_rate")) -<<<<<<< HEAD -======= - @patch( - # this also shadows one (1) call to _get_payment_gateway_controller - "erpnext.accounts.doctype.payment_request.payment_request.PaymentRequest.get_payment_url", - return_value=None, - ) - def test_sales_order_advance_payment_status(self, mocked_get_payment_url): - from erpnext.accounts.doctype.payment_entry.test_payment_entry import get_payment_entry - from erpnext.accounts.doctype.payment_request.payment_request import make_payment_request - - # Flow progressing to SI with payment entries "moved" from SO to SI - so = make_sales_order(qty=1, rate=100, do_not_submit=True) - # no-op; for optical consistency with how a webshop SO would look like - so.order_type = "Shopping Cart" - so.submit() - self.assertEqual(frappe.db.get_value(so.doctype, so.name, "advance_payment_status"), "Not Requested") - - pr = make_payment_request( - dt=so.doctype, - dn=so.name, - order_type="Shopping Cart", - submit_doc=True, - return_doc=True, - mute_email=True, - ) - self.assertEqual(frappe.db.get_value(so.doctype, so.name, "advance_payment_status"), "Requested") - - pe = pr.set_as_paid() - pr.reload() # status updated - pe.reload() # references moved to Sales Invoice - self.assertEqual(pr.status, "Paid") - self.assertEqual(pe.references[0].reference_doctype, "Sales Invoice") - self.assertEqual(frappe.db.get_value(so.doctype, so.name, "advance_payment_status"), "Fully Paid") - - pe.cancel() - pr.reload() - self.assertEqual(pr.status, "Paid") # TODO: this might be a bug - so.reload() # reload - # regardless, since the references have already "handed-over" to SI, - # the SO keeps its historical state at the time of hand over - self.assertEqual(frappe.db.get_value(so.doctype, so.name, "advance_payment_status"), "Fully Paid") - - pr.cancel() - self.assertEqual( - frappe.db.get_value(so.doctype, so.name, "advance_payment_status"), "Not Requested" - ) # TODO: this might be a bug; handover has happened - - # Flow NOT progressing to SI with payment entries NOT "moved" - so = make_sales_order(qty=1, rate=100) - self.assertEqual(frappe.db.get_value(so.doctype, so.name, "advance_payment_status"), "Not Requested") - - pr = make_payment_request( - dt=so.doctype, dn=so.name, submit_doc=True, return_doc=True, mute_email=True - ) - self.assertEqual(frappe.db.get_value(so.doctype, so.name, "advance_payment_status"), "Requested") - - pe = get_payment_entry(so.doctype, so.name).save().submit() - self.assertEqual(frappe.db.get_value(so.doctype, so.name, "advance_payment_status"), "Fully Paid") - - pe.reload() - pe.cancel() - self.assertEqual( - frappe.db.get_value(so.doctype, so.name, "advance_payment_status"), "Requested" - ) # here: reset - - pr.reload() - pr.cancel() - self.assertEqual( - frappe.db.get_value(so.doctype, so.name, "advance_payment_status"), "Not Requested" - ) # here: reset - ->>>>>>> 0447c7be0a (fix: Treat rows as Unit Price rows only until the qty is 0) def test_pick_list_without_rejected_materials(self): serial_and_batch_item = make_item( "_Test Serial and Batch Item for Rejected Materials", @@ -2302,7 +2199,7 @@ class TestSalesOrder(AccountsTestMixin, FrappeTestCase): po.submit() self.assertEqual(po.taxes[0].tax_amount, 2) - @IntegrationTestCase.change_settings("Selling Settings", {"allow_zero_qty_in_sales_order": 1}) + @change_settings("Selling Settings", {"allow_zero_qty_in_sales_order": 1}) def test_deliver_zero_qty_purchase_order(self): """ Test the flow of a Unit Price SO and DN creation against it until completion. @@ -2350,7 +2247,7 @@ class TestSalesOrder(AccountsTestMixin, FrappeTestCase): self.assertEqual(so.per_delivered, 100.0) self.assertEqual(so.status, "To Bill") - @IntegrationTestCase.change_settings("Selling Settings", {"allow_zero_qty_in_sales_order": 1}) + @change_settings("Selling Settings", {"allow_zero_qty_in_sales_order": 1}) def test_bill_zero_qty_sales_order(self): so = make_sales_order(qty=0) diff --git a/erpnext/selling/doctype/selling_settings/selling_settings.json b/erpnext/selling/doctype/selling_settings/selling_settings.json index 0e857430b5e..0c82de001a8 100644 --- a/erpnext/selling/doctype/selling_settings/selling_settings.json +++ b/erpnext/selling/doctype/selling_settings/selling_settings.json @@ -32,16 +32,9 @@ "allow_sales_order_creation_for_expired_quotation", "dont_reserve_sales_order_qty_on_sales_return", "hide_tax_id", -<<<<<<< HEAD - "enable_discount_accounting" -======= "enable_discount_accounting", - "enable_cutoff_date_on_bulk_delivery_note_creation", "allow_zero_qty_in_quotation", - "allow_zero_qty_in_sales_order", - "experimental_section", - "use_server_side_reactivity" ->>>>>>> c1e4e7af28 (feat: Unit Price Contract) + "allow_zero_qty_in_sales_order" ], "fields": [ { @@ -209,25 +202,6 @@ "fieldname": "blanket_order_allowance", "fieldtype": "Float", "label": "Blanket Order Allowance (%)" -<<<<<<< HEAD -======= - }, - { - "default": "0", - "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" }, { "default": "0", @@ -239,12 +213,7 @@ "default": "0", "fieldname": "allow_zero_qty_in_quotation", "fieldtype": "Check", -<<<<<<< HEAD - "label": "Allow 0 Qty in Quotation (Unit Price Contract)" ->>>>>>> c1e4e7af28 (feat: Unit Price Contract) -======= "label": "Allow 0 Qty in Quotation (Unit Price Items)" ->>>>>>> e403d3f153 (feat: Unit Price Items in Buying (RFQ, SQ, PO)) } ], "grid_page_length": 50, @@ -253,15 +222,7 @@ "index_web_pages_for_search": 1, "issingle": 1, "links": [], -<<<<<<< HEAD -<<<<<<< HEAD - "modified": "2023-10-25 14:03:03.966701", -======= - "modified": "2025-02-28 18:19:46.436595", ->>>>>>> c1e4e7af28 (feat: Unit Price Contract) -======= "modified": "2025-03-03 16:39:16.360823", ->>>>>>> e403d3f153 (feat: Unit Price Items in Buying (RFQ, SQ, PO)) "modified_by": "Administrator", "module": "Selling", "name": "Selling Settings", @@ -286,12 +247,7 @@ "write": 1 } ], -<<<<<<< HEAD "sort_field": "modified", -======= - "row_format": "Dynamic", - "sort_field": "creation", ->>>>>>> c1e4e7af28 (feat: Unit Price Contract) "sort_order": "DESC", "states": [], "track_changes": 1 From 8891f46a22f1f7ed6ffb2ec37c4a7bbef00fb9ba Mon Sep 17 00:00:00 2001 From: marination <25857446+marination@users.noreply.github.com> Date: Tue, 6 May 2025 12:33:03 +0200 Subject: [PATCH 10/49] fix: Relabel unit price settings for more clarity --- .../doctype/buying_settings/buying_settings.json | 11 +++++++---- .../doctype/selling_settings/selling_settings.json | 10 ++++++---- 2 files changed, 13 insertions(+), 8 deletions(-) diff --git a/erpnext/buying/doctype/buying_settings/buying_settings.json b/erpnext/buying/doctype/buying_settings/buying_settings.json index 4a89ff78862..38eae23b83e 100644 --- a/erpnext/buying/doctype/buying_settings/buying_settings.json +++ b/erpnext/buying/doctype/buying_settings/buying_settings.json @@ -213,21 +213,24 @@ }, { "default": "0", + "description": "Allows items in the Purchase Order to have a quantity of 0. Useful when the rate/unit price is agreed upon, but the quantity is not yet determined.", "fieldname": "allow_zero_qty_in_purchase_order", "fieldtype": "Check", - "label": "Allow 0 Qty in Purchase Order (Unit Price Items)" + "label": "Allow Zero Quantity Items in Purchase Order" }, { "default": "0", + "description": "Allows items in the Request for Quotation to have a quantity of 0. Useful when the rate/unit price is agreed upon, but the quantity is not yet determined.", "fieldname": "allow_zero_qty_in_request_for_quotation", "fieldtype": "Check", - "label": "Allow 0 Qty in Request for Quotation (Unit Price Items)" + "label": "Allow Zero Quantity Items in Request for Quotation" }, { "default": "0", + "description": "Allows items in the Supplier Quotation to have a quantity of 0. Useful when the rate/unit price is agreed upon, but the quantity is not yet determined.", "fieldname": "allow_zero_qty_in_supplier_quotation", "fieldtype": "Check", - "label": "Allow 0 Qty in Supplier Quotation (Unit Price Items)" + "label": "Allow Zero Quantity Items in Supplier Quotation" } ], "grid_page_length": 50, @@ -236,7 +239,7 @@ "index_web_pages_for_search": 1, "issingle": 1, "links": [], - "modified": "2025-03-03 17:32:25.939482", + "modified": "2025-05-06 12:32:00.195378", "modified_by": "Administrator", "module": "Buying", "name": "Buying Settings", diff --git a/erpnext/selling/doctype/selling_settings/selling_settings.json b/erpnext/selling/doctype/selling_settings/selling_settings.json index 0c82de001a8..f10f9d297e8 100644 --- a/erpnext/selling/doctype/selling_settings/selling_settings.json +++ b/erpnext/selling/doctype/selling_settings/selling_settings.json @@ -205,15 +205,17 @@ }, { "default": "0", + "description": "Allows items in the Sales Order to have a quantity of 0. Useful when the rate/unit price is agreed upon, but the quantity is not yet determined.", "fieldname": "allow_zero_qty_in_sales_order", "fieldtype": "Check", - "label": "Allow 0 Qty in Sales Order (Unit Price Items)" + "label": "Allow Zero Quantity Items in Sales Order" }, { "default": "0", + "description": "Allows items in the Quotation to have a quantity of 0. Useful when the rate/unit price is agreed upon, but the quantity is not yet determined.", "fieldname": "allow_zero_qty_in_quotation", "fieldtype": "Check", - "label": "Allow 0 Qty in Quotation (Unit Price Items)" + "label": "Allow Zero Quantity Items in Quotation" } ], "grid_page_length": 50, @@ -222,7 +224,7 @@ "index_web_pages_for_search": 1, "issingle": 1, "links": [], - "modified": "2025-03-03 16:39:16.360823", + "modified": "2025-05-06 12:30:13.342694", "modified_by": "Administrator", "module": "Selling", "name": "Selling Settings", @@ -251,4 +253,4 @@ "sort_order": "DESC", "states": [], "track_changes": 1 -} \ No newline at end of file +} From 0286788e9757b2a261c9198767daadad0533e198 Mon Sep 17 00:00:00 2001 From: marination <25857446+marination@users.noreply.github.com> Date: Tue, 6 May 2025 15:28:56 +0200 Subject: [PATCH 11/49] chore: Relabel according to review changes --- .../doctype/buying_settings/buying_settings.json | 14 +++++++------- .../doctype/selling_settings/selling_settings.json | 10 +++++----- 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/erpnext/buying/doctype/buying_settings/buying_settings.json b/erpnext/buying/doctype/buying_settings/buying_settings.json index 38eae23b83e..1b1df3fa1e0 100644 --- a/erpnext/buying/doctype/buying_settings/buying_settings.json +++ b/erpnext/buying/doctype/buying_settings/buying_settings.json @@ -213,24 +213,24 @@ }, { "default": "0", - "description": "Allows items in the Purchase Order to have a quantity of 0. Useful when the rate/unit price is agreed upon, but the quantity is not yet determined.", + "description": "Allows users to submit Purchase Orders with zero quantity. Useful when rates are fixed but the quantities are not. Eg. Rate Contracts.", "fieldname": "allow_zero_qty_in_purchase_order", "fieldtype": "Check", - "label": "Allow Zero Quantity Items in Purchase Order" + "label": "Allow Purchase Order with Zero Quantity" }, { "default": "0", - "description": "Allows items in the Request for Quotation to have a quantity of 0. Useful when the rate/unit price is agreed upon, but the quantity is not yet determined.", + "description": "Allows users to submit Request for Quotations with zero quantity. Useful when rates are fixed but the quantities are not. Eg. Rate Contracts.", "fieldname": "allow_zero_qty_in_request_for_quotation", "fieldtype": "Check", - "label": "Allow Zero Quantity Items in Request for Quotation" + "label": "Allow Request for Quotation with Zero Quantity" }, { "default": "0", - "description": "Allows items in the Supplier Quotation to have a quantity of 0. Useful when the rate/unit price is agreed upon, but the quantity is not yet determined.", + "description": "Allows users to submit Supplier Quotations with zero quantity. Useful when rates are fixed but the quantities are not. Eg. Rate Contracts.", "fieldname": "allow_zero_qty_in_supplier_quotation", "fieldtype": "Check", - "label": "Allow Zero Quantity Items in Supplier Quotation" + "label": "Allow Supplier Quotation with Zero Quantity" } ], "grid_page_length": 50, @@ -239,7 +239,7 @@ "index_web_pages_for_search": 1, "issingle": 1, "links": [], - "modified": "2025-05-06 12:32:00.195378", + "modified": "2025-05-06 15:21:49.639642", "modified_by": "Administrator", "module": "Buying", "name": "Buying Settings", diff --git a/erpnext/selling/doctype/selling_settings/selling_settings.json b/erpnext/selling/doctype/selling_settings/selling_settings.json index f10f9d297e8..0553ef44980 100644 --- a/erpnext/selling/doctype/selling_settings/selling_settings.json +++ b/erpnext/selling/doctype/selling_settings/selling_settings.json @@ -205,17 +205,17 @@ }, { "default": "0", - "description": "Allows items in the Sales Order to have a quantity of 0. Useful when the rate/unit price is agreed upon, but the quantity is not yet determined.", + "description": "Allows users to submit Sales Orders with zero quantity. Useful when rates are fixed but the quantities are not. Eg. Rate Contracts.", "fieldname": "allow_zero_qty_in_sales_order", "fieldtype": "Check", - "label": "Allow Zero Quantity Items in Sales Order" + "label": "Allow Sales Order with Zero Quantity" }, { "default": "0", - "description": "Allows items in the Quotation to have a quantity of 0. Useful when the rate/unit price is agreed upon, but the quantity is not yet determined.", + "description": "Allows users to submit Quotations with zero quantity. Useful when rates are fixed but the quantities are not. Eg. Rate Contracts.", "fieldname": "allow_zero_qty_in_quotation", "fieldtype": "Check", - "label": "Allow Zero Quantity Items in Quotation" + "label": "Allow Quotation with Zero Quantity" } ], "grid_page_length": 50, @@ -224,7 +224,7 @@ "index_web_pages_for_search": 1, "issingle": 1, "links": [], - "modified": "2025-05-06 12:30:13.342694", + "modified": "2025-05-06 15:23:14.332971", "modified_by": "Administrator", "module": "Selling", "name": "Selling Settings", From f95a3f5b8b9badb095b07b42214c6cd27275b880 Mon Sep 17 00:00:00 2001 From: marination <25857446+marination@users.noreply.github.com> Date: Tue, 6 May 2025 14:24:36 +0200 Subject: [PATCH 12/49] fix(portal): User cannot create 0 qty SQ from RFQ - The portal uses `create_supplier_quotation` for SQ creation which excludes 0 qty items --- .../request_for_quotation.py | 7 +++---- .../test_request_for_quotation.py | 17 +++++++++++++++++ 2 files changed, 20 insertions(+), 4 deletions(-) diff --git a/erpnext/buying/doctype/request_for_quotation/request_for_quotation.py b/erpnext/buying/doctype/request_for_quotation/request_for_quotation.py index ce4d44fb68e..27793236dc3 100644 --- a/erpnext/buying/doctype/request_for_quotation/request_for_quotation.py +++ b/erpnext/buying/doctype/request_for_quotation/request_for_quotation.py @@ -456,11 +456,10 @@ def create_supplier_quotation(doc): def add_items(sq_doc, supplier, items): for data in items: - if data.get("qty") > 0: - if isinstance(data, dict): - data = frappe._dict(data) + if isinstance(data, dict): + data = frappe._dict(data) - create_rfq_items(sq_doc, supplier, data) + create_rfq_items(sq_doc, supplier, data) def create_rfq_items(sq_doc, supplier, data): diff --git a/erpnext/buying/doctype/request_for_quotation/test_request_for_quotation.py b/erpnext/buying/doctype/request_for_quotation/test_request_for_quotation.py index 0a9347faefe..f119840e042 100644 --- a/erpnext/buying/doctype/request_for_quotation/test_request_for_quotation.py +++ b/erpnext/buying/doctype/request_for_quotation/test_request_for_quotation.py @@ -191,6 +191,23 @@ class TestRequestforQuotation(FrappeTestCase): self.assertEqual(sq.items[0].qty, 0) self.assertEqual(sq.items[0].item_code, rfq.items[0].item_code) + @change_settings( + "Buying Settings", + { + "allow_zero_qty_in_request_for_quotation": 1, + "allow_zero_qty_in_supplier_quotation": 1, + }, + ) + def test_supplier_quotation_from_zero_qty_rfq_in_portal(self): + rfq = make_request_for_quotation(qty=0) + rfq.supplier = rfq.suppliers[0].supplier + sq_name = create_supplier_quotation(rfq) + + sq = frappe.get_doc("Supplier Quotation", sq_name) + self.assertEqual(len(sq.items), 1) + self.assertEqual(sq.items[0].qty, 0) + self.assertEqual(sq.items[0].item_code, rfq.items[0].item_code) + def make_request_for_quotation(**args) -> "RequestforQuotation": """ From 37f4cf536756d5a54d4903fd5c60bf0c6c706827 Mon Sep 17 00:00:00 2001 From: marination <25857446+marination@users.noreply.github.com> Date: Tue, 13 May 2025 17:18:40 +0200 Subject: [PATCH 13/49] fix: Linter (due to conflicts resolved on gh) --- erpnext/buying/doctype/purchase_order/test_purchase_order.py | 4 ++-- .../request_for_quotation/test_request_for_quotation.py | 4 ++-- erpnext/selling/doctype/quotation/test_quotation.py | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/erpnext/buying/doctype/purchase_order/test_purchase_order.py b/erpnext/buying/doctype/purchase_order/test_purchase_order.py index 5c8220894b8..c6ee35d6090 100644 --- a/erpnext/buying/doctype/purchase_order/test_purchase_order.py +++ b/erpnext/buying/doctype/purchase_order/test_purchase_order.py @@ -46,8 +46,8 @@ class TestPurchaseOrder(FrappeTestCase): # InvalidQtyError with qty=0 po.items[1].qty = 0 self.assertRaises(InvalidQtyError, po.save) - - # No error with qty=1 + + # No error with qty=1 po.items[1].qty = 1 po.save() self.assertEqual(po.items[1].qty, 1) diff --git a/erpnext/buying/doctype/request_for_quotation/test_request_for_quotation.py b/erpnext/buying/doctype/request_for_quotation/test_request_for_quotation.py index f119840e042..1a8b3a8ac47 100644 --- a/erpnext/buying/doctype/request_for_quotation/test_request_for_quotation.py +++ b/erpnext/buying/doctype/request_for_quotation/test_request_for_quotation.py @@ -31,8 +31,8 @@ class TestRequestforQuotation(FrappeTestCase): rfq.items[0].qty = 1 rfq.save() self.assertEqual(rfq.items[0].qty, 1) - - def test_rfq_zero_qty(self): + + def test_rfq_zero_qty(self): """ Test if RFQ with zero qty (Unit Price Item) is conditionally allowed. """ diff --git a/erpnext/selling/doctype/quotation/test_quotation.py b/erpnext/selling/doctype/quotation/test_quotation.py index 49652cbde85..f6f4d98d541 100644 --- a/erpnext/selling/doctype/quotation/test_quotation.py +++ b/erpnext/selling/doctype/quotation/test_quotation.py @@ -20,8 +20,8 @@ class TestQuotation(FrappeTestCase): qo.items[0].qty = 1 qo.save() self.assertEqual(qo.items[0].qty, 1) - - def test_quotation_zero_qty(self): + + def test_quotation_zero_qty(self): """ Test if Quote with zero qty (Unit Price Item) is conditionally allowed. """ From 9a78283ecb33d5c901a507e362f3b88193c0e542 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Tue, 20 May 2025 20:46:57 +0530 Subject: [PATCH 14/49] fix: incorrect inventory dimension for material transfer (backport #47592) (#47644) fix: incorrect inventory dimension for material transfer (#47592) (cherry picked from commit 738cb6a0c12fc7d804e058cdc5115519fceec338) Co-authored-by: rohitwaghchaure --- erpnext/controllers/stock_controller.py | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/erpnext/controllers/stock_controller.py b/erpnext/controllers/stock_controller.py index f079274528c..3654ad5e6c5 100644 --- a/erpnext/controllers/stock_controller.py +++ b/erpnext/controllers/stock_controller.py @@ -892,7 +892,7 @@ class StockController(AccountsController): or sl_dict.actual_qty < 0 and self.get("is_return") ) - and self.doctype in ["Purchase Invoice", "Purchase Receipt"] + and self.doctype in ["Purchase Invoice", "Purchase Receipt", "Stock Entry"] ) or ( ( sl_dict.actual_qty < 0 @@ -902,6 +902,15 @@ class StockController(AccountsController): ) and self.doctype in ["Sales Invoice", "Delivery Note", "Stock Entry"] ): + if self.doctype == "Stock Entry": + if row.get("t_warehouse") == sl_dict.warehouse and sl_dict.get("actual_qty") > 0: + fieldname = f"to_{dimension.source_fieldname}" + if dimension.source_fieldname.startswith("to_"): + fieldname = f"{dimension.source_fieldname}" + + sl_dict[dimension.target_fieldname] = row.get(fieldname) + return + sl_dict[dimension.target_fieldname] = row.get(dimension.source_fieldname) else: fieldname_start_with = "to" From db318a4e9bbc79a25e97af7814417f3457aecaf5 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Wed, 21 May 2025 15:09:48 +0530 Subject: [PATCH 15/49] fix: pos invoice status not updating on cancel (backport #47556) (#47657) fix: pos invoice status not updating on cancel (#47556) (cherry picked from commit 8c86def018fb733e2e155db09e5e00024cb1cb33) Co-authored-by: Diptanil Saha --- erpnext/accounts/doctype/pos_invoice/pos_invoice.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/erpnext/accounts/doctype/pos_invoice/pos_invoice.py b/erpnext/accounts/doctype/pos_invoice/pos_invoice.py index 58c14a2bc5d..b0c69be4a1c 100644 --- a/erpnext/accounts/doctype/pos_invoice/pos_invoice.py +++ b/erpnext/accounts/doctype/pos_invoice/pos_invoice.py @@ -273,6 +273,8 @@ class POSInvoice(SalesInvoice): against_psi_doc.delete_loyalty_point_entry() against_psi_doc.make_loyalty_point_entry() + self.db_set("status", "Cancelled") + if self.coupon_code: from erpnext.accounts.doctype.pricing_rule.utils import update_coupon_code_count From cd1c10a43f2ad24ae59da4a6ddee676bef3cc741 Mon Sep 17 00:00:00 2001 From: Mihir Kandoi Date: Thu, 22 May 2025 12:52:31 +0530 Subject: [PATCH 16/49] fix: include rejected amount in PI/PR overbilling validation logic (#47572) * fix: include rejected amount in PI/PR overbilling validation logic * fix: add check if amount is 0 * fix: unneccessary condition (cherry picked from commit 8d9888b1b6403a9199e30d1986c9751bd0502ca5) --- .../stock/doctype/purchase_receipt/purchase_receipt.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py index 6d4a348ef9d..29481473d7f 100644 --- a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py +++ b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py @@ -1106,6 +1106,10 @@ def update_billing_percentage(pr_doc, update_modified=True, adjust_incoming_rate if pr_doc.get("is_return") and not total_amount and total_billed_amount: total_amount = total_billed_amount + amount = item.amount + if frappe.db.get_single_value("Buying Settings", "bill_for_rejected_quantity_in_purchase_invoice"): + amount += flt(item.rejected_qty * item.rate, item.precision("amount")) + if adjust_incoming_rate: adjusted_amt = 0.0 @@ -1120,8 +1124,8 @@ def update_billing_percentage(pr_doc, update_modified=True, adjust_incoming_rate adjusted_amt = flt(adjusted_amt * flt(pr_doc.conversion_rate), item.precision("amount")) item.db_set("amount_difference_with_purchase_invoice", adjusted_amt, update_modified=False) - elif item.billed_amt > item.amount: - per_over_billed = (flt(item.billed_amt / item.amount, 2) * 100) - 100 + elif item.billed_amt > amount: + per_over_billed = (flt(item.billed_amt / amount, 2) * 100) - 100 if per_over_billed > over_billing_allowance: frappe.throw( _("Over Billing Allowance exceeded for Purchase Receipt Item {0} ({1}) by {2}%").format( From 2c22615b6b3a7e50623bb593c335fc99d14126e2 Mon Sep 17 00:00:00 2001 From: Mihir Kandoi Date: Wed, 21 May 2025 15:42:51 +0530 Subject: [PATCH 17/49] fix: exchange rate not being fetched when creating supplier quotation from MR (cherry picked from commit 9d12ae071a7e41a669c56f1eb39575ec3186382c) --- erpnext/stock/doctype/material_request/material_request.py | 1 + 1 file changed, 1 insertion(+) diff --git a/erpnext/stock/doctype/material_request/material_request.py b/erpnext/stock/doctype/material_request/material_request.py index 54b3e17641e..956f47bb978 100644 --- a/erpnext/stock/doctype/material_request/material_request.py +++ b/erpnext/stock/doctype/material_request/material_request.py @@ -643,6 +643,7 @@ def make_supplier_quotation(source_name, target_doc=None): postprocess, ) + doclist.set_onload("load_after_mapping", False) return doclist From 3d2d1ba0720ef8a14d54af130d4b1fff46b0272f Mon Sep 17 00:00:00 2001 From: ljain112 Date: Thu, 22 May 2025 12:34:55 +0530 Subject: [PATCH 18/49] fix: typo in TREE_DOCTYPES list "Terrirtory" should be "Territory" (cherry picked from commit 51162cb1a3122cf0bd98f808bd3f46cf8016a93c) --- .../report/customer_ledger_summary/customer_ledger_summary.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/accounts/report/customer_ledger_summary/customer_ledger_summary.py b/erpnext/accounts/report/customer_ledger_summary/customer_ledger_summary.py index 963fb3556a6..1648166d041 100644 --- a/erpnext/accounts/report/customer_ledger_summary/customer_ledger_summary.py +++ b/erpnext/accounts/report/customer_ledger_summary/customer_ledger_summary.py @@ -15,7 +15,7 @@ from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import ( ) TREE_DOCTYPES = frozenset( - ["Customer Group", "Terrirtory", "Supplier Group", "Sales Partner", "Sales Person", "Cost Center"] + ["Customer Group", "Territory", "Supplier Group", "Sales Partner", "Sales Person", "Cost Center"] ) From 62aa1cdb335171f7b4fb8aedb43df1f73a179f5f Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Thu, 22 May 2025 16:17:18 +0530 Subject: [PATCH 19/49] fix: incorrect valuation rate due to positive qty (backport #47686) (#47688) fix: incorrect valuation rate due to positive qty (#47686) (cherry picked from commit 6ed97b5fdada89a1971d7e7eb69e6834f0b3b3bc) Co-authored-by: Khushi Rawat <142375893+khushi8112@users.noreply.github.com> --- .../assets/doctype/asset_capitalization/asset_capitalization.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/assets/doctype/asset_capitalization/asset_capitalization.js b/erpnext/assets/doctype/asset_capitalization/asset_capitalization.js index 2afc1ecb439..5d47cc13e5b 100644 --- a/erpnext/assets/doctype/asset_capitalization/asset_capitalization.js +++ b/erpnext/assets/doctype/asset_capitalization/asset_capitalization.js @@ -402,7 +402,7 @@ erpnext.assets.AssetCapitalization = class AssetCapitalization extends erpnext.s args: { item_code: item.item_code, warehouse: cstr(item.warehouse), - qty: flt(item.stock_qty), + qty: -1 * flt(item.stock_qty), serial_no: item.serial_no, posting_date: me.frm.doc.posting_date, posting_time: me.frm.doc.posting_time, From f17b7b5ee9718e1265a73c1ae1a1f566a64dbc3f Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Fri, 23 May 2025 09:49:51 +0530 Subject: [PATCH 20/49] fix: skip last purchase rate for free item (backport #47693) (#47696) fix: skip last purchase rate for free item (#47693) (cherry picked from commit c3b17024bd8c024c91dbde578b979dc00de83b33) Co-authored-by: rohitwaghchaure --- erpnext/buying/utils.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/erpnext/buying/utils.py b/erpnext/buying/utils.py index 259c262d0d7..54f00417a89 100644 --- a/erpnext/buying/utils.py +++ b/erpnext/buying/utils.py @@ -20,6 +20,9 @@ def update_last_purchase_rate(doc, is_submit) -> None: this_purchase_date = getdate(doc.get("posting_date") or doc.get("transaction_date")) for d in doc.get("items"): + if d.get("is_free_item"): + continue + # get last purchase details last_purchase_details = get_last_purchase_details(d.item_code, doc.name) From f41bcc6fecfe1c4aad32dab16da28cef3d3f110e Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Sat, 24 May 2025 17:27:20 +0530 Subject: [PATCH 21/49] fix: absence of rounding causing discrepancy in the valuation rate calculation (backport #47700) (#47711) fix: absence of rounding causing discrepancy in the valuation rate calculation (#47700) (cherry picked from commit 1e8ed22421cf70803c41dc342b405b52ca55321a) Co-authored-by: rohitwaghchaure --- erpnext/stock/stock_ledger.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/erpnext/stock/stock_ledger.py b/erpnext/stock/stock_ledger.py index ebf4973b066..0cda4baaadd 100644 --- a/erpnext/stock/stock_ledger.py +++ b/erpnext/stock/stock_ledger.py @@ -882,6 +882,12 @@ class update_entries_after: self.wh_data.stock_value = flt(self.wh_data.qty_after_transaction) * flt( self.wh_data.valuation_rate ) + + if sle.actual_qty < 0 and self.wh_data.qty_after_transaction != 0: + self.wh_data.valuation_rate = flt( + self.wh_data.stock_value, self.currency_precision + ) / flt(self.wh_data.qty_after_transaction, self.flt_precision) + else: self.update_queue_values(sle) From 2961e595c2a8861e2f30f526780f90b6f45cad36 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Sun, 25 May 2025 20:51:14 +0530 Subject: [PATCH 22/49] fix: filter of item for manufacture type material request (backport #47712) (#47717) fix: filter of item for manufacture type material request (#47712) (cherry picked from commit 874750f9ceca3c6351ea7bdc09939326588d5489) Co-authored-by: rohitwaghchaure --- .../material_request/material_request.js | 32 +++++++++---------- 1 file changed, 15 insertions(+), 17 deletions(-) diff --git a/erpnext/stock/doctype/material_request/material_request.js b/erpnext/stock/doctype/material_request/material_request.js index 5f46ef968b0..18d8919a668 100644 --- a/erpnext/stock/doctype/material_request/material_request.js +++ b/erpnext/stock/doctype/material_request/material_request.js @@ -560,25 +560,23 @@ erpnext.buying.MaterialRequestController = class MaterialRequestController exten onload() { this.frm.set_query("item_code", "items", function (doc, cdt, cdn) { + let filters = { is_stock_item: 1 }; + if (doc.material_request_type == "Customer Provided") { - return { - query: "erpnext.controllers.queries.item_query", - filters: { - customer: doc.customer, - is_stock_item: 1, - }, - }; - } else if (doc.material_request_type == "Purchase") { - return { - query: "erpnext.controllers.queries.item_query", - filters: { is_purchase_item: 1 }, - }; - } else { - return { - query: "erpnext.controllers.queries.item_query", - filters: { is_stock_item: 1 }, - }; + filters.customer = doc.customer; + } else if ( + doc.material_request_type == "Purchase" || + doc.material_request_type == "Subcontracting" + ) { + filters = { is_purchase_item: 1 }; + } else if (doc.material_request_type == "Manufacture") { + filters.include_item_in_manufacturing = 1; } + + return { + query: "erpnext.controllers.queries.item_query", + filters: filters, + }; }); } From e05888502f16f577399b9f0ebd7bf480b0390384 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Mon, 26 May 2025 07:53:55 +0530 Subject: [PATCH 23/49] fix: skip drop ship items (backport #47670) (#47718) fix: skip drop ship items (#47670) (cherry picked from commit 67c86ec028b41f960f8a837623199ad15e2b5272) Co-authored-by: rohitwaghchaure --- erpnext/utilities/transaction_base.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/erpnext/utilities/transaction_base.py b/erpnext/utilities/transaction_base.py index 2e4bdac6aab..b7283529772 100644 --- a/erpnext/utilities/transaction_base.py +++ b/erpnext/utilities/transaction_base.py @@ -163,6 +163,9 @@ class TransactionBase(StatusUpdater): child_table_values = set() for row in self.get(child_table): + if default_field == "set_warehouse" and row.get("delivered_by_supplier"): + continue + child_table_values.add(row.get(child_table_field)) if len(child_table_values) > 1: From ba009f4626d4f661e9a7a37b5ad0ee39603f493b Mon Sep 17 00:00:00 2001 From: Prateek Karamchandani Date: Fri, 25 Apr 2025 06:19:00 +0000 Subject: [PATCH 24/49] fix: display stock value in currency format in chart warehouse wise stock value (cherry picked from commit 7a5cbc759c14387a7c6a7c205a4ae02d5cb7a5cf) --- .../warehouse_wise_stock_value.json | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/erpnext/stock/dashboard_chart/warehouse_wise_stock_value/warehouse_wise_stock_value.json b/erpnext/stock/dashboard_chart/warehouse_wise_stock_value/warehouse_wise_stock_value.json index 400ec5e0e64..b12d2339397 100644 --- a/erpnext/stock/dashboard_chart/warehouse_wise_stock_value/warehouse_wise_stock_value.json +++ b/erpnext/stock/dashboard_chart/warehouse_wise_stock_value/warehouse_wise_stock_value.json @@ -2,14 +2,15 @@ "chart_name": "Warehouse wise Stock Value", "chart_type": "Custom", "creation": "2022-03-30 00:58:02.018824", + "custom_options": "{\n \"fieldtype\": \"Currency\"\n}", "docstatus": 0, "doctype": "Dashboard Chart", "filters_json": "{}", "idx": 0, "is_public": 1, "is_standard": 1, - "last_synced_on": "2024-12-23 18:44:46.822164", - "modified": "2024-12-23 19:31:17.003946", + "last_synced_on": "2025-04-25 11:38:43.644402", + "modified": "2025-04-25 11:39:12.585542", "modified_by": "Administrator", "module": "Stock", "name": "Warehouse wise Stock Value", From a13794495556da0d1d00d981efc2d571e1868c91 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Mon, 26 May 2025 13:29:41 +0530 Subject: [PATCH 25/49] fix: patch to rename group_by filter in custom reports (backport #47709) (#47730) * fix: patch to rename group_by filter in custom reports (cherry picked from commit 0d19c18c0628fc7f4a2727c8e5c9d0fe0f93d66d) # Conflicts: # erpnext/patches.txt * fix: using python instead of sql query (cherry picked from commit 48eccb1f734a1e0e158e28892759bc77c6e4904e) * chore: resolve conflict --------- Co-authored-by: diptanilsaha --- erpnext/patches.txt | 1 + ...p_by_to_categorize_by_in_custom_reports.py | 24 +++++++++++++++++++ 2 files changed, 25 insertions(+) create mode 100644 erpnext/patches/v15_0/rename_group_by_to_categorize_by_in_custom_reports.py diff --git a/erpnext/patches.txt b/erpnext/patches.txt index cf8ae44a8d7..fb8c625f6c7 100644 --- a/erpnext/patches.txt +++ b/erpnext/patches.txt @@ -404,3 +404,4 @@ erpnext.patches.v15_0.update_payment_schedule_fields_in_invoices erpnext.patches.v15_0.rename_group_by_to_categorize_by execute:frappe.db.set_single_value("Accounts Settings", "receivable_payable_fetch_method", "Buffered Cursor") erpnext.patches.v14_0.set_update_price_list_based_on +erpnext.patches.v15_0.rename_group_by_to_categorize_by_in_custom_reports diff --git a/erpnext/patches/v15_0/rename_group_by_to_categorize_by_in_custom_reports.py b/erpnext/patches/v15_0/rename_group_by_to_categorize_by_in_custom_reports.py new file mode 100644 index 00000000000..bc671a4a6ac --- /dev/null +++ b/erpnext/patches/v15_0/rename_group_by_to_categorize_by_in_custom_reports.py @@ -0,0 +1,24 @@ +import json + +import frappe + + +def execute(): + custom_reports = frappe.get_all( + "Report", + filters={ + "report_type": "Custom Report", + "reference_report": ["in", ["General Ledger", "Supplier Quotation Comparison"]], + }, + fields=["name", "json"], + ) + + for report in custom_reports: + report_json = json.loads(report.json) + + if "filters" in report_json and "group_by" in report_json["filters"]: + report_json["filters"]["categorize_by"] = ( + report_json["filters"].pop("group_by").replace("Group", "Categorize") + ) + + frappe.db.set_value("Report", report.name, "json", json.dumps(report_json)) From 48f786e4932c0899412a349e5754e47cf3a24b04 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Fri, 23 May 2025 13:50:46 +0530 Subject: [PATCH 26/49] refactor: full name field in contract (cherry picked from commit 016924361a781c94c821c18c438bb93df1aa9efd) # Conflicts: # erpnext/crm/doctype/contract/contract.json --- erpnext/crm/doctype/contract/contract.json | 15 ++++++++++++++- erpnext/crm/doctype/contract/contract.py | 1 + 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/erpnext/crm/doctype/contract/contract.json b/erpnext/crm/doctype/contract/contract.json index de3230f0e67..4c76adeb248 100755 --- a/erpnext/crm/doctype/contract/contract.json +++ b/erpnext/crm/doctype/contract/contract.json @@ -14,6 +14,7 @@ "party_user", "status", "fulfilment_status", + "party_full_name", "sb_terms", "start_date", "cb_date", @@ -244,11 +245,22 @@ "fieldname": "authorised_by_section", "fieldtype": "Section Break", "label": "Authorised By" + }, + { + "fieldname": "party_full_name", + "fieldtype": "Data", + "label": "Party Full Name", + "read_only": 1 } ], + "grid_page_length": 50, "is_submittable": 1, "links": [], +<<<<<<< HEAD "modified": "2020-12-07 11:15:58.385521", +======= + "modified": "2025-05-23 13:54:03.346537", +>>>>>>> 016924361a (refactor: full name field in contract) "modified_by": "Administrator", "module": "CRM", "name": "Contract", @@ -315,9 +327,10 @@ "write": 1 } ], + "row_format": "Dynamic", "show_name_in_global_search": 1, "sort_field": "modified", "sort_order": "DESC", "track_changes": 1, "track_seen": 1 -} \ No newline at end of file +} diff --git a/erpnext/crm/doctype/contract/contract.py b/erpnext/crm/doctype/contract/contract.py index 6c3aace6fd8..1951dd25dee 100644 --- a/erpnext/crm/doctype/contract/contract.py +++ b/erpnext/crm/doctype/contract/contract.py @@ -34,6 +34,7 @@ class Contract(Document): fulfilment_terms: DF.Table[ContractFulfilmentChecklist] ip_address: DF.Data | None is_signed: DF.Check + party_full_name: DF.Data | None party_name: DF.DynamicLink party_type: DF.Literal["Customer", "Supplier", "Employee"] party_user: DF.Link | None From 9abac4c6df09210a1c4661133ed7712eb88d8f6f Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Fri, 23 May 2025 14:04:07 +0530 Subject: [PATCH 27/49] refactor: fetch party name on selection (cherry picked from commit 752024e222b9d74b4193f410bf4a3ec840810b28) --- erpnext/crm/doctype/contract/contract.js | 6 ++++++ erpnext/crm/doctype/contract/contract.py | 7 +++++++ 2 files changed, 13 insertions(+) diff --git a/erpnext/crm/doctype/contract/contract.js b/erpnext/crm/doctype/contract/contract.js index 8d44c22db28..42bd7f9b769 100644 --- a/erpnext/crm/doctype/contract/contract.js +++ b/erpnext/crm/doctype/contract/contract.js @@ -29,4 +29,10 @@ frappe.ui.form.on("Contract", { }); } }, + party_name: function (frm) { + let field = frm.doc.party_type.toLowerCase() + "_name"; + frappe.db.get_value(frm.doc.party_type, frm.doc.party_name, field, (r) => { + frm.set_value("party_full_name", r[field]); + }); + }, }); diff --git a/erpnext/crm/doctype/contract/contract.py b/erpnext/crm/doctype/contract/contract.py index 1951dd25dee..64f89552062 100644 --- a/erpnext/crm/doctype/contract/contract.py +++ b/erpnext/crm/doctype/contract/contract.py @@ -60,10 +60,17 @@ class Contract(Document): self.name = _(name) def validate(self): + self.set_missing_values() self.validate_dates() self.update_contract_status() self.update_fulfilment_status() + def set_missing_values(self): + if not self.party_full_name: + field = self.party_type.lower() + "_name" + if res := frappe.db.get_value(self.party_type, self.party_name, field): + self.party_full_name = res + def before_submit(self): self.signed_by_company = frappe.session.user From d5d1a51b929b3ec888ff45d20407d08483dbece5 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Fri, 23 May 2025 14:24:50 +0530 Subject: [PATCH 28/49] refactor: patch old contract with full party name (cherry picked from commit 8e2221178b00f8f148ad38bb90a6928ade9bdc23) # Conflicts: # erpnext/patches.txt --- erpnext/patches.txt | 5 +++++ .../patches/v14_0/update_full_name_in_contract.py | 15 +++++++++++++++ 2 files changed, 20 insertions(+) create mode 100644 erpnext/patches/v14_0/update_full_name_in_contract.py diff --git a/erpnext/patches.txt b/erpnext/patches.txt index fb8c625f6c7..9dc1947cf2f 100644 --- a/erpnext/patches.txt +++ b/erpnext/patches.txt @@ -405,3 +405,8 @@ erpnext.patches.v15_0.rename_group_by_to_categorize_by execute:frappe.db.set_single_value("Accounts Settings", "receivable_payable_fetch_method", "Buffered Cursor") erpnext.patches.v14_0.set_update_price_list_based_on erpnext.patches.v15_0.rename_group_by_to_categorize_by_in_custom_reports +<<<<<<< HEAD +======= +erpnext.patches.v15_0.remove_agriculture_roles +erpnext.patches.v14_0.update_full_name_in_contract +>>>>>>> 8e2221178b (refactor: patch old contract with full party name) diff --git a/erpnext/patches/v14_0/update_full_name_in_contract.py b/erpnext/patches/v14_0/update_full_name_in_contract.py new file mode 100644 index 00000000000..19ee055ad12 --- /dev/null +++ b/erpnext/patches/v14_0/update_full_name_in_contract.py @@ -0,0 +1,15 @@ +import frappe +from frappe import qb + + +def execute(): + con = qb.DocType("Contract") + for c in ( + qb.from_(con) + .select(con.name, con.party_type, con.party_name) + .where(con.party_full_name.isnull()) + .run(as_dict=True) + ): + field = c.party_type.lower() + "_name" + if res := frappe.db.get_value(c.party_type, c.party_name, field): + frappe.db.set_value("Contract", c.name, "party_full_name", res) From c09b258d57ecd02da3acc3a7940deba35858a756 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Mon, 26 May 2025 17:48:11 +0530 Subject: [PATCH 29/49] chore: resolve conflicts --- erpnext/crm/doctype/contract/contract.json | 4 ---- erpnext/patches.txt | 4 ---- 2 files changed, 8 deletions(-) diff --git a/erpnext/crm/doctype/contract/contract.json b/erpnext/crm/doctype/contract/contract.json index 4c76adeb248..948243402fe 100755 --- a/erpnext/crm/doctype/contract/contract.json +++ b/erpnext/crm/doctype/contract/contract.json @@ -256,11 +256,7 @@ "grid_page_length": 50, "is_submittable": 1, "links": [], -<<<<<<< HEAD - "modified": "2020-12-07 11:15:58.385521", -======= "modified": "2025-05-23 13:54:03.346537", ->>>>>>> 016924361a (refactor: full name field in contract) "modified_by": "Administrator", "module": "CRM", "name": "Contract", diff --git a/erpnext/patches.txt b/erpnext/patches.txt index 9dc1947cf2f..8bd4e847c22 100644 --- a/erpnext/patches.txt +++ b/erpnext/patches.txt @@ -405,8 +405,4 @@ erpnext.patches.v15_0.rename_group_by_to_categorize_by execute:frappe.db.set_single_value("Accounts Settings", "receivable_payable_fetch_method", "Buffered Cursor") erpnext.patches.v14_0.set_update_price_list_based_on erpnext.patches.v15_0.rename_group_by_to_categorize_by_in_custom_reports -<<<<<<< HEAD -======= -erpnext.patches.v15_0.remove_agriculture_roles erpnext.patches.v14_0.update_full_name_in_contract ->>>>>>> 8e2221178b (refactor: patch old contract with full party name) From b3cbbf2ce38e734ae6df27b415a01cba04180d5e Mon Sep 17 00:00:00 2001 From: ljain112 Date: Wed, 21 May 2025 13:18:09 +0530 Subject: [PATCH 30/49] fix: show general ledger in doc currency in Process Statement Of Accounts (cherry picked from commit 998f6a29a4acebf5b9373f8c8fa2d417a3f6d361) --- .../process_statement_of_accounts.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.py b/erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.py index 1c4bb66357f..714ed623796 100644 --- a/erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.py +++ b/erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.py @@ -129,8 +129,8 @@ def get_statement_dict(doc, get_statement_dict=False): tax_id = frappe.get_doc("Customer", entry.customer).tax_id presentation_currency = ( - get_party_account_currency("Customer", entry.customer, doc.company) - or doc.currency + doc.currency + or get_party_account_currency("Customer", entry.customer, doc.company) or get_company_currency(doc.company) ) From 9d2f396d75b37f271d65f2b93d6945fc728682fe Mon Sep 17 00:00:00 2001 From: ljain112 Date: Wed, 21 May 2025 14:37:47 +0530 Subject: [PATCH 31/49] chore: update test case because currency is auto set to system currency (cherry picked from commit 22a94d6817fdeb8470228aaac45c007bebbe5438) --- .../test_process_statement_of_accounts.py | 1 + 1 file changed, 1 insertion(+) diff --git a/erpnext/accounts/doctype/process_statement_of_accounts/test_process_statement_of_accounts.py b/erpnext/accounts/doctype/process_statement_of_accounts/test_process_statement_of_accounts.py index 92dbb5ef273..ebfe96e771e 100644 --- a/erpnext/accounts/doctype/process_statement_of_accounts/test_process_statement_of_accounts.py +++ b/erpnext/accounts/doctype/process_statement_of_accounts/test_process_statement_of_accounts.py @@ -97,6 +97,7 @@ def create_process_soa(**args): company=args.company or "_Test Company", customers=args.customers or [{"customer": "_Test Customer"}], enable_auto_email=1 if args.enable_auto_email else 0, + currency=args.currency or "", frequency=args.frequency or "Weekly", report=args.report or "General Ledger", from_date=args.from_date or getdate(today()), From 01b0d1057e6fed3b414d7a55e85279869e35ea33 Mon Sep 17 00:00:00 2001 From: mahsem <137205921+mahsem@users.noreply.github.com> Date: Wed, 21 May 2025 12:46:36 +0200 Subject: [PATCH 32/49] fix: translate_pos_buttons (cherry picked from commit ce45d1664db996bcdb220fe0ffe09076defa3aeb) --- .../selling/page/point_of_sale/pos_past_order_summary.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/erpnext/selling/page/point_of_sale/pos_past_order_summary.js b/erpnext/selling/page/point_of_sale/pos_past_order_summary.js index cf775176c07..ed1abc97b51 100644 --- a/erpnext/selling/page/point_of_sale/pos_past_order_summary.js +++ b/erpnext/selling/page/point_of_sale/pos_past_order_summary.js @@ -321,17 +321,17 @@ erpnext.PointOfSale.PastOrderSummary = class { get_condition_btn_map(after_submission) { if (after_submission) - return [{ condition: true, visible_btns: ["Print Receipt", "Email Receipt", "New Order"] }]; + return [{ condition: true, visible_btns: [__("Print Receipt"), __("Email Receipt"), __("New Order")] }]; return [ - { condition: this.doc.docstatus === 0, visible_btns: ["Edit Order", "Delete Order"] }, + { condition: this.doc.docstatus === 0, visible_btns: [__("Edit Order"), __("Delete Order")] }, { condition: !this.doc.is_return && this.doc.docstatus === 1, - visible_btns: ["Print Receipt", "Email Receipt", "Return"], + visible_btns: [__("Print Receipt"), __("Email Receipt"), __("Return")], }, { condition: this.doc.is_return && this.doc.docstatus === 1, - visible_btns: ["Print Receipt", "Email Receipt"], + visible_btns: [__("Print Receipt"), __("Email Receipt")], }, ]; } From c44493fd7e5acebb59748321a0a68bc59fe2c894 Mon Sep 17 00:00:00 2001 From: mahsem <137205921+mahsem@users.noreply.github.com> Date: Wed, 21 May 2025 12:54:25 +0200 Subject: [PATCH 33/49] fix: linter (cherry picked from commit 4a6b5b99931c2bedbb4fc12dfb16790bccc53733) --- .../selling/page/point_of_sale/pos_past_order_summary.js | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/erpnext/selling/page/point_of_sale/pos_past_order_summary.js b/erpnext/selling/page/point_of_sale/pos_past_order_summary.js index ed1abc97b51..c346f7dc257 100644 --- a/erpnext/selling/page/point_of_sale/pos_past_order_summary.js +++ b/erpnext/selling/page/point_of_sale/pos_past_order_summary.js @@ -321,7 +321,12 @@ erpnext.PointOfSale.PastOrderSummary = class { get_condition_btn_map(after_submission) { if (after_submission) - return [{ condition: true, visible_btns: [__("Print Receipt"), __("Email Receipt"), __("New Order")] }]; + return [ + { + condition: true, + visible_btns: [__("Print Receipt"), __("Email Receipt"), __("New Order")] + }, + ]; return [ { condition: this.doc.docstatus === 0, visible_btns: [__("Edit Order"), __("Delete Order")] }, From 2c8db092a0f0280dcdb20bb3c0c922310aa52248 Mon Sep 17 00:00:00 2001 From: mahsem <137205921+mahsem@users.noreply.github.com> Date: Wed, 21 May 2025 12:59:07 +0200 Subject: [PATCH 34/49] fix: prettier (cherry picked from commit 1953c8489ccd86fcc27a7d492fd618b138fbf8f2) --- erpnext/selling/page/point_of_sale/pos_past_order_summary.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/selling/page/point_of_sale/pos_past_order_summary.js b/erpnext/selling/page/point_of_sale/pos_past_order_summary.js index c346f7dc257..ac28a5e18b0 100644 --- a/erpnext/selling/page/point_of_sale/pos_past_order_summary.js +++ b/erpnext/selling/page/point_of_sale/pos_past_order_summary.js @@ -324,7 +324,7 @@ erpnext.PointOfSale.PastOrderSummary = class { return [ { condition: true, - visible_btns: [__("Print Receipt"), __("Email Receipt"), __("New Order")] + visible_btns: [__("Print Receipt"), __("Email Receipt"), __("New Order")], }, ]; From 194e41a2d99895dd5f34277729e446e84043c952 Mon Sep 17 00:00:00 2001 From: mahsem <137205921+mahsem@users.noreply.github.com> Date: Wed, 21 May 2025 13:01:11 +0200 Subject: [PATCH 35/49] fix: space (cherry picked from commit a442ec4e80e9d5839f742081f6233bf0371cd3ec) --- .../selling/page/point_of_sale/pos_past_order_summary.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/erpnext/selling/page/point_of_sale/pos_past_order_summary.js b/erpnext/selling/page/point_of_sale/pos_past_order_summary.js index ac28a5e18b0..d14fbd4c7d4 100644 --- a/erpnext/selling/page/point_of_sale/pos_past_order_summary.js +++ b/erpnext/selling/page/point_of_sale/pos_past_order_summary.js @@ -322,10 +322,10 @@ erpnext.PointOfSale.PastOrderSummary = class { get_condition_btn_map(after_submission) { if (after_submission) return [ - { - condition: true, - visible_btns: [__("Print Receipt"), __("Email Receipt"), __("New Order")], - }, + { + condition: true, + visible_btns: [__("Print Receipt"), __("Email Receipt"), __("New Order")], + }, ]; return [ From fe78bb60c4a24b8bd087b01ba88f4d355f529472 Mon Sep 17 00:00:00 2001 From: mahsem <137205921+mahsem@users.noreply.github.com> Date: Wed, 21 May 2025 13:04:08 +0200 Subject: [PATCH 36/49] fix: space (cherry picked from commit 50a5b5190902be01deb3e3ceddb7d881da06d379) --- erpnext/selling/page/point_of_sale/pos_past_order_summary.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/selling/page/point_of_sale/pos_past_order_summary.js b/erpnext/selling/page/point_of_sale/pos_past_order_summary.js index d14fbd4c7d4..6d27ba4568d 100644 --- a/erpnext/selling/page/point_of_sale/pos_past_order_summary.js +++ b/erpnext/selling/page/point_of_sale/pos_past_order_summary.js @@ -324,7 +324,7 @@ erpnext.PointOfSale.PastOrderSummary = class { return [ { condition: true, - visible_btns: [__("Print Receipt"), __("Email Receipt"), __("New Order")], + visible_btns: [__("Print Receipt"), __("Email Receipt"), __("New Order")], }, ]; From 0f2264658f798b97185985378280b929b8183ef7 Mon Sep 17 00:00:00 2001 From: mahsem <137205921+mahsem@users.noreply.github.com> Date: Wed, 21 May 2025 13:21:55 +0200 Subject: [PATCH 37/49] fix: prettier (cherry picked from commit 2839fc94607bc3e2063675d8d5927c3c435a2fed) --- erpnext/selling/page/point_of_sale/pos_past_order_summary.js | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/erpnext/selling/page/point_of_sale/pos_past_order_summary.js b/erpnext/selling/page/point_of_sale/pos_past_order_summary.js index 6d27ba4568d..0ad25b0a473 100644 --- a/erpnext/selling/page/point_of_sale/pos_past_order_summary.js +++ b/erpnext/selling/page/point_of_sale/pos_past_order_summary.js @@ -329,7 +329,10 @@ erpnext.PointOfSale.PastOrderSummary = class { ]; return [ - { condition: this.doc.docstatus === 0, visible_btns: [__("Edit Order"), __("Delete Order")] }, + { + condition: this.doc.docstatus === 0, + visible_btns: [__("Edit Order"), __("Delete Order")], + }, { condition: !this.doc.is_return && this.doc.docstatus === 1, visible_btns: [__("Print Receipt"), __("Email Receipt"), __("Return")], From 60dfe36195668f05d9ef5f94fac0e39a4434916f Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Tue, 27 May 2025 13:06:32 +0530 Subject: [PATCH 38/49] fix: create Quality Inspection button not showing (backport #47746) (#47750) fix: create Quality Inspection button not showing (#47746) (cherry picked from commit d8cb073eafdfc5e35aca876c6af5f6fe285dfc7b) Co-authored-by: rohitwaghchaure --- erpnext/controllers/buying_controller.py | 8 ++++++++ erpnext/controllers/selling_controller.py | 8 ++++++++ erpnext/public/js/controllers/transaction.js | 5 ++++- 3 files changed, 20 insertions(+), 1 deletion(-) diff --git a/erpnext/controllers/buying_controller.py b/erpnext/controllers/buying_controller.py index 3fdf92e7990..385dce625fb 100644 --- a/erpnext/controllers/buying_controller.py +++ b/erpnext/controllers/buying_controller.py @@ -70,6 +70,14 @@ class BuyingController(SubcontractingController): frappe.db.get_single_value("Buying Settings", "backflush_raw_materials_of_subcontract_based_on"), ) + if self.docstatus == 1 and self.doctype in ["Purchase Receipt", "Purchase Invoice"]: + self.set_onload( + "allow_to_make_qc_after_submission", + frappe.db.get_single_value( + "Stock Settings", "allow_to_make_quality_inspection_after_purchase_or_delivery" + ), + ) + def create_package_for_transfer(self) -> None: """Create serial and batch package for Sourece Warehouse in case of inter transfer.""" diff --git a/erpnext/controllers/selling_controller.py b/erpnext/controllers/selling_controller.py index 26ac65589f1..73ce4c83e4c 100644 --- a/erpnext/controllers/selling_controller.py +++ b/erpnext/controllers/selling_controller.py @@ -31,6 +31,14 @@ class SellingController(StockController): ) ) + if self.docstatus == 1 and self.doctype in ["Delivery Note", "Sales Invoice"]: + self.set_onload( + "allow_to_make_qc_after_submission", + frappe.db.get_single_value( + "Stock Settings", "allow_to_make_quality_inspection_after_purchase_or_delivery" + ), + ) + def validate(self): super().validate() self.validate_items() diff --git a/erpnext/public/js/controllers/transaction.js b/erpnext/public/js/controllers/transaction.js index b7706028114..b513a9628d5 100644 --- a/erpnext/public/js/controllers/transaction.js +++ b/erpnext/public/js/controllers/transaction.js @@ -330,7 +330,10 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe } const me = this; - if (!this.frm.is_new() && this.frm.doc.docstatus === 0 && frappe.model.can_create("Quality Inspection") && show_qc_button) { + if (!this.frm.is_new() + && (this.frm.doc.docstatus === 0 || this.frm.doc.__onload?.allow_to_make_qc_after_submission) + && frappe.model.can_create("Quality Inspection") + && show_qc_button) { this.frm.add_custom_button(__("Quality Inspection(s)"), () => { me.make_quality_inspection(); }, __("Create")); From d3d22f699e0b34c4c89b46dbf314b148aa1c2dbb Mon Sep 17 00:00:00 2001 From: ljain112 Date: Mon, 26 May 2025 15:55:55 +0530 Subject: [PATCH 39/49] fix: party account based on party type's account type (cherry picked from commit 19b1650522d911a000d59df7476175eb52d55d62) --- erpnext/accounts/party.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/erpnext/accounts/party.py b/erpnext/accounts/party.py index 419baaa3d47..52550f148b7 100644 --- a/erpnext/accounts/party.py +++ b/erpnext/accounts/party.py @@ -460,6 +460,12 @@ def get_party_account(party_type, party=None, company=None, include_advance=Fals if (account and account_currency != existing_gle_currency) or not account: account = get_party_gle_account(party_type, party, company) + # get default account on the basis of party type + if not account: + account_type = frappe.get_cached_value("Party Type", party_type, "account_type") + default_account_name = "default_" + account_type.lower() + "_account" + account = frappe.get_cached_value("Company", company, default_account_name) + if include_advance and party_type in ["Customer", "Supplier", "Student"]: advance_account = get_party_advance_account(party_type, party, company) if advance_account: From a2f5975133218742ec1e51d4609378f72aef5bac Mon Sep 17 00:00:00 2001 From: ljain112 Date: Mon, 26 May 2025 15:36:12 +0530 Subject: [PATCH 40/49] fix: only include advances within the tcs period (cherry picked from commit 477ec9fdccb8eeea3f431a6dc02242a11bb2c628) --- .../tax_withholding_category.py | 1 + .../test_tax_withholding_category.py | 22 ++++++++++++++----- 2 files changed, 18 insertions(+), 5 deletions(-) diff --git a/erpnext/accounts/doctype/tax_withholding_category/tax_withholding_category.py b/erpnext/accounts/doctype/tax_withholding_category/tax_withholding_category.py index e45802ca9f2..fa9b226374c 100644 --- a/erpnext/accounts/doctype/tax_withholding_category/tax_withholding_category.py +++ b/erpnext/accounts/doctype/tax_withholding_category/tax_withholding_category.py @@ -671,6 +671,7 @@ def get_tcs_amount(parties, inv, tax_details, vouchers, adv_vouchers): conditions.append(ple.party.isin(parties)) conditions.append(ple.voucher_no == ple.against_voucher_no) conditions.append(ple.company == inv.company) + conditions.append(ple.posting_date[tax_details.from_date : tax_details.to_date]) advance_amt = ( qb.from_(ple).select(Abs(Sum(ple.amount))).where(Criterion.all(conditions)).run()[0][0] or 0.0 diff --git a/erpnext/accounts/doctype/tax_withholding_category/test_tax_withholding_category.py b/erpnext/accounts/doctype/tax_withholding_category/test_tax_withholding_category.py index ae0a098137a..3ea6801ad35 100644 --- a/erpnext/accounts/doctype/tax_withholding_category/test_tax_withholding_category.py +++ b/erpnext/accounts/doctype/tax_withholding_category/test_tax_withholding_category.py @@ -288,17 +288,18 @@ class TestTaxWithholdingCategory(FrappeTestCase): frappe.db.set_value( "Customer", "Test TCS Customer", "tax_withholding_category", "Cumulative Threshold TCS" ) + fiscal_year = get_fiscal_year(today(), company="_Test Company") vouchers = [] # create advance payment - pe = create_payment_entry( + pe1 = create_payment_entry( payment_type="Receive", party_type="Customer", party="Test TCS Customer", paid_amount=20000 ) - pe.paid_from = "Debtors - _TC" - pe.paid_to = "Cash - _TC" - pe.submit() - vouchers.append(pe) + pe1.paid_from = "Debtors - _TC" + pe1.paid_to = "Cash - _TC" + pe1.submit() + vouchers.append(pe1) # create invoice si1 = create_sales_invoice(customer="Test TCS Customer", rate=5000) @@ -320,6 +321,17 @@ class TestTaxWithholdingCategory(FrappeTestCase): # make another invoice # sum of unallocated amount from payment entry and this sales invoice will breach cumulative threashold # TDS should be calculated + + # this payment should not be considered for TCS calculation as it is outside of fiscal year + pe2 = create_payment_entry( + payment_type="Receive", party_type="Customer", party="Test TCS Customer", paid_amount=10000 + ) + pe2.paid_from = "Debtors - _TC" + pe2.paid_to = "Cash - _TC" + pe2.posting_date = add_days(fiscal_year[1], -10) + pe2.submit() + vouchers.append(pe2) + si2 = create_sales_invoice(customer="Test TCS Customer", rate=15000) si2.submit() vouchers.append(si2) From 8ed6e98565da21ccc6fd147dbef1db8d86605d40 Mon Sep 17 00:00:00 2001 From: Khushi Rawat <142375893+khushi8112@users.noreply.github.com> Date: Tue, 27 May 2025 13:34:03 +0530 Subject: [PATCH 41/49] fix: updated value after depreciation after value adjustment --- .../asset_depreciation_schedule.py | 6 ------ .../asset_value_adjustment.py | 14 ++++++++++++-- 2 files changed, 12 insertions(+), 8 deletions(-) diff --git a/erpnext/assets/doctype/asset_depreciation_schedule/asset_depreciation_schedule.py b/erpnext/assets/doctype/asset_depreciation_schedule/asset_depreciation_schedule.py index 5bddea56183..7fab7b67752 100644 --- a/erpnext/assets/doctype/asset_depreciation_schedule/asset_depreciation_schedule.py +++ b/erpnext/assets/doctype/asset_depreciation_schedule/asset_depreciation_schedule.py @@ -256,10 +256,6 @@ class AssetDepreciationSchedule(Document): ): asset_doc.validate_asset_finance_books(row) - if not value_after_depreciation: - value_after_depreciation = _get_value_after_depreciation_for_making_schedule(asset_doc, row) - row.value_after_depreciation = value_after_depreciation - if update_asset_finance_book_row: row.db_update() @@ -1068,8 +1064,6 @@ def make_new_active_asset_depr_schedules_and_cancel_current_ones( ) new_asset_depr_schedule_doc = frappe.copy_doc(current_asset_depr_schedule_doc) - if asset_doc.flags.decrease_in_asset_value_due_to_value_adjustment and not value_after_depreciation: - value_after_depreciation = row.value_after_depreciation - difference_amount if asset_doc.flags.increase_in_asset_value_due_to_repair and row.depreciation_method in ( "Written Down Value", diff --git a/erpnext/assets/doctype/asset_value_adjustment/asset_value_adjustment.py b/erpnext/assets/doctype/asset_value_adjustment/asset_value_adjustment.py index 6766b827f7f..14f2837641b 100644 --- a/erpnext/assets/doctype/asset_value_adjustment/asset_value_adjustment.py +++ b/erpnext/assets/doctype/asset_value_adjustment/asset_value_adjustment.py @@ -5,7 +5,7 @@ import frappe from frappe import _ from frappe.model.document import Document -from frappe.utils import flt, formatdate, get_link_to_form, getdate +from frappe.utils import cstr, flt, formatdate, get_link_to_form, getdate from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import ( get_checks_for_pl_and_bs_accounts, @@ -188,12 +188,22 @@ class AssetValueAdjustment(Document): get_link_to_form(self.get("doctype"), self.get("name")), ) + difference_amount = self.difference_amount if self.docstatus == 1 else -1 * self.difference_amount + if asset.calculate_depreciation: + for row in asset.finance_books: + if cstr(row.finance_book) == cstr(self.finance_book): + row.value_after_depreciation += flt(difference_amount) + row.db_update() + + asset.value_after_depreciation += flt(difference_amount) + asset.db_update() + make_new_active_asset_depr_schedules_and_cancel_current_ones( asset, notes, value_after_depreciation=asset_value, ignore_booked_entry=True, - difference_amount=self.difference_amount, + difference_amount=difference_amount, ) asset.flags.ignore_validate_update_after_submit = True asset.save() From 895231a8a75adf127ec3d019909fe7a7ebbdde12 Mon Sep 17 00:00:00 2001 From: ljain112 Date: Thu, 22 May 2025 13:49:26 +0530 Subject: [PATCH 42/49] fix: setting paid amount to 0 when is_paid is unchecked in purchase invoice (cherry picked from commit e358a9e53fcdc651a2f73400b0bccc7a0f60352e) --- erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js | 2 ++ erpnext/controllers/accounts_controller.py | 3 +++ 2 files changed, 5 insertions(+) diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js index 13430b46449..c3935029670 100644 --- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js +++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js @@ -425,6 +425,8 @@ erpnext.accounts.PurchaseInvoice = class PurchaseInvoice extends erpnext.buying. this.frm.set_value("is_paid", 0); frappe.msgprint(__("Please specify Company to proceed")); } + } else { + this.frm.set_value("paid_amount", 0); } this.calculate_outstanding_amount(); this.frm.refresh_fields(); diff --git a/erpnext/controllers/accounts_controller.py b/erpnext/controllers/accounts_controller.py index 3cbb6d33188..237f19274e3 100644 --- a/erpnext/controllers/accounts_controller.py +++ b/erpnext/controllers/accounts_controller.py @@ -649,6 +649,9 @@ class AccountsController(TransactionBase): self.base_paid_amount = flt( self.paid_amount * self.conversion_rate, self.precision("base_paid_amount") ) + else: + self.paid_amount = 0 + self.base_paid_amount = 0 def set_missing_values(self, for_validate=False): if frappe.flags.in_test: From 74e29f12183a6fb2205720bf4491078d82a56a57 Mon Sep 17 00:00:00 2001 From: Khushi Rawat <142375893+khushi8112@users.noreply.github.com> Date: Tue, 27 May 2025 14:14:30 +0530 Subject: [PATCH 43/49] fix: made changes specifically for value adjustment entry --- .../asset_depreciation_schedule.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/erpnext/assets/doctype/asset_depreciation_schedule/asset_depreciation_schedule.py b/erpnext/assets/doctype/asset_depreciation_schedule/asset_depreciation_schedule.py index 7fab7b67752..f9c4913b1a2 100644 --- a/erpnext/assets/doctype/asset_depreciation_schedule/asset_depreciation_schedule.py +++ b/erpnext/assets/doctype/asset_depreciation_schedule/asset_depreciation_schedule.py @@ -255,6 +255,12 @@ class AssetDepreciationSchedule(Document): value_after_depreciation, ): asset_doc.validate_asset_finance_books(row) + if ( + not value_after_depreciation + and not asset_doc.flags.decrease_in_asset_value_due_to_value_adjustment + ): + value_after_depreciation = _get_value_after_depreciation_for_making_schedule(asset_doc, row) + row.value_after_depreciation = value_after_depreciation if update_asset_finance_book_row: row.db_update() From 63ba27e359a0bf633118b6dca4b5226ef6d4b4c1 Mon Sep 17 00:00:00 2001 From: Khushi Rawat <142375893+khushi8112@users.noreply.github.com> Date: Tue, 27 May 2025 15:10:51 +0530 Subject: [PATCH 44/49] fix: do not update same field twice --- .../doctype/asset_value_adjustment/asset_value_adjustment.py | 1 - 1 file changed, 1 deletion(-) diff --git a/erpnext/assets/doctype/asset_value_adjustment/asset_value_adjustment.py b/erpnext/assets/doctype/asset_value_adjustment/asset_value_adjustment.py index 14f2837641b..0f4d8a9ae95 100644 --- a/erpnext/assets/doctype/asset_value_adjustment/asset_value_adjustment.py +++ b/erpnext/assets/doctype/asset_value_adjustment/asset_value_adjustment.py @@ -195,7 +195,6 @@ class AssetValueAdjustment(Document): row.value_after_depreciation += flt(difference_amount) row.db_update() - asset.value_after_depreciation += flt(difference_amount) asset.db_update() make_new_active_asset_depr_schedules_and_cancel_current_ones( From 4fd1af21184483cd274aa70bd176fd244e748f44 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Tue, 27 May 2025 15:55:40 +0530 Subject: [PATCH 45/49] fix: patch to set status cancelled for already cancelled pos invoices (backport #47725) (#47759) * fix: patch to set status cancelled for already cancelled pos invoices (#47725) (cherry picked from commit 4d1d66e579c795e4732d7c88621350780966ae29) # Conflicts: # erpnext/patches.txt * chore: resolve conflict --------- Co-authored-by: Diptanil Saha --- erpnext/patches.txt | 1 + .../set_cancelled_status_to_cancelled_pos_invoice.py | 8 ++++++++ 2 files changed, 9 insertions(+) create mode 100644 erpnext/patches/v15_0/set_cancelled_status_to_cancelled_pos_invoice.py diff --git a/erpnext/patches.txt b/erpnext/patches.txt index 8bd4e847c22..d78e86bd1d7 100644 --- a/erpnext/patches.txt +++ b/erpnext/patches.txt @@ -404,5 +404,6 @@ erpnext.patches.v15_0.update_payment_schedule_fields_in_invoices erpnext.patches.v15_0.rename_group_by_to_categorize_by execute:frappe.db.set_single_value("Accounts Settings", "receivable_payable_fetch_method", "Buffered Cursor") erpnext.patches.v14_0.set_update_price_list_based_on +erpnext.patches.v15_0.set_cancelled_status_to_cancelled_pos_invoice erpnext.patches.v15_0.rename_group_by_to_categorize_by_in_custom_reports erpnext.patches.v14_0.update_full_name_in_contract diff --git a/erpnext/patches/v15_0/set_cancelled_status_to_cancelled_pos_invoice.py b/erpnext/patches/v15_0/set_cancelled_status_to_cancelled_pos_invoice.py new file mode 100644 index 00000000000..a4141ec20e6 --- /dev/null +++ b/erpnext/patches/v15_0/set_cancelled_status_to_cancelled_pos_invoice.py @@ -0,0 +1,8 @@ +import frappe +from frappe.query_builder import DocType + + +def execute(): + POSInvoice = DocType("POS Invoice") + + frappe.qb.update(POSInvoice).set(POSInvoice.status, "Cancelled").where(POSInvoice.docstatus == 2).run() From 92d5e91e1f6ccd0a8db7b13080a2e225c3fdf2f6 Mon Sep 17 00:00:00 2001 From: Karuppasamy923 Date: Tue, 27 May 2025 16:19:43 +0530 Subject: [PATCH 46/49] feat: add validation for Item Tax Template on rate change (cherry picked from commit a9a957edc76a54a116cc51528e31f7ce826ee68e) --- erpnext/public/js/controllers/transaction.js | 23 ++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/erpnext/public/js/controllers/transaction.js b/erpnext/public/js/controllers/transaction.js index b513a9628d5..db4783582ec 100644 --- a/erpnext/public/js/controllers/transaction.js +++ b/erpnext/public/js/controllers/transaction.js @@ -42,6 +42,29 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe } item.base_rate_with_margin = item.rate_with_margin * flt(frm.doc.conversion_rate); + if (item.item_code && item.rate) { + frappe.call({ + method: "frappe.client.get_value", + args: { + doctype: "Item Tax", + parent: "Item", + filters: { + parent: item.item_code, + minimum_net_rate: ["<=", item.rate], + maximum_net_rate: [">=", item.rate] + }, + fieldname: "item_tax_template" + }, + callback: function(r) { + const tax_rule = r.message; + + let matched_template = tax_rule ? tax_rule.item_tax_template : null; + + frappe.model.set_value(cdt, cdn, 'item_tax_template', matched_template); + } + }); + } + cur_frm.cscript.set_gross_profit(item); cur_frm.cscript.calculate_taxes_and_totals(); cur_frm.cscript.calculate_stock_uom_rate(frm, cdt, cdn); From fb2df77da2c97e665f685a4f2503a8a1240ec01b Mon Sep 17 00:00:00 2001 From: ljain112 Date: Tue, 27 May 2025 16:51:52 +0530 Subject: [PATCH 47/49] fix: use pypika object `LiteralValue` for adding match conditions (cherry picked from commit 9093e5e3636f4c3cbd9fcb89387268f584c05947) --- .../customer_ledger_summary/customer_ledger_summary.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/erpnext/accounts/report/customer_ledger_summary/customer_ledger_summary.py b/erpnext/accounts/report/customer_ledger_summary/customer_ledger_summary.py index 1648166d041..42a6358cdfe 100644 --- a/erpnext/accounts/report/customer_ledger_summary/customer_ledger_summary.py +++ b/erpnext/accounts/report/customer_ledger_summary/customer_ledger_summary.py @@ -8,6 +8,7 @@ from frappe.query_builder import Criterion, Tuple from frappe.query_builder.functions import IfNull from frappe.utils import getdate, nowdate from frappe.utils.nestedset import get_descendants_of +from pypika.terms import LiteralValue from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import ( get_accounting_dimensions, @@ -77,13 +78,12 @@ class PartyLedgerSummaryReport: from frappe.desk.reportview import build_match_conditions - query, params = query.walk() match_conditions = build_match_conditions(party_type) if match_conditions: - query += "and" + match_conditions + query = query.where(LiteralValue(match_conditions)) - party_details = frappe.db.sql(query, params, as_dict=True) + party_details = query.run(as_dict=True) for row in party_details: self.parties.append(row.party) From db97dbd3940c0e65e51e5a15b0b2ca64240c1c52 Mon Sep 17 00:00:00 2001 From: l0gesh29 Date: Tue, 27 May 2025 17:04:41 +0530 Subject: [PATCH 48/49] fix: add no_copy for lost reasons (cherry picked from commit 98e889a516b5bd194f65847671e74a63c74947bd) --- erpnext/selling/doctype/quotation/quotation.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/erpnext/selling/doctype/quotation/quotation.json b/erpnext/selling/doctype/quotation/quotation.json index 5a674581a9b..b3aaae99c4d 100644 --- a/erpnext/selling/doctype/quotation/quotation.json +++ b/erpnext/selling/doctype/quotation/quotation.json @@ -936,6 +936,7 @@ "fieldname": "lost_reasons", "fieldtype": "Table MultiSelect", "label": "Lost Reasons", + "no_copy": 1, "options": "Quotation Lost Reason Detail", "read_only": 1 }, @@ -1099,7 +1100,7 @@ "idx": 82, "is_submittable": 1, "links": [], - "modified": "2025-03-03 16:49:20.050303", + "modified": "2025-05-27 16:04:39.208077", "modified_by": "Administrator", "module": "Selling", "name": "Quotation", From f783bf60a4d9611621d2f30d79ba8293e23623bb Mon Sep 17 00:00:00 2001 From: ljain112 Date: Tue, 27 May 2025 17:27:29 +0530 Subject: [PATCH 49/49] fix: handle multiselect filters for tree doctypes in Customer Ledger Summary Report (cherry picked from commit 536f7d5ff8f699f80659fa39aa9b9f8bf1f1bc9f) --- .../customer_ledger_summary.py | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/erpnext/accounts/report/customer_ledger_summary/customer_ledger_summary.py b/erpnext/accounts/report/customer_ledger_summary/customer_ledger_summary.py index 42a6358cdfe..b90f922d82b 100644 --- a/erpnext/accounts/report/customer_ledger_summary/customer_ledger_summary.py +++ b/erpnext/accounts/report/customer_ledger_summary/customer_ledger_summary.py @@ -458,9 +458,16 @@ class PartyLedgerSummaryReport: def get_children(doctype, value): - children = get_descendants_of(doctype, value) + if not isinstance(value, list): + value = [d.strip() for d in value.strip().split(",") if d] - return [value, *children] + all_children = [] + + for d in value: + all_children += get_descendants_of(doctype, value) + all_children.append(d) + + return list(set(all_children)) def execute(filters=None):