From d6bd9e25dd9202df350ce67c47d92fe00ec2d1df Mon Sep 17 00:00:00 2001 From: Charles-Henri Decultot Date: Mon, 20 May 2024 14:34:34 +0200 Subject: [PATCH 1/5] feat: Add margin calculation to sales order --- erpnext/public/js/utils/party.js | 62 +++++++++--- .../doctype/sales_order/sales_order.json | 27 ++++- .../doctype/sales_order/sales_order.py | 9 +- .../sales_order_item/sales_order_item.json | 87 ++++++++++++++-- .../sales_order_item/sales_order_item.py | 98 +++++++++++++++++++ 5 files changed, 263 insertions(+), 20 deletions(-) diff --git a/erpnext/public/js/utils/party.js b/erpnext/public/js/utils/party.js index f9dc6354ae..7027de4dac 100644 --- a/erpnext/public/js/utils/party.js +++ b/erpnext/public/js/utils/party.js @@ -75,11 +75,25 @@ erpnext.utils.get_party_details = function(frm, method, args, callback) { if (frappe.meta.get_docfield(frm.doc.doctype, "taxes")) { - if (!erpnext.utils.validate_mandatory(frm, "Posting / Transaction Date", - args.posting_date, args.party_type=="Customer" ? "customer": "supplier")) return; + if ( + !erpnext.utils.validate_mandatory( + frm, + __("Posting / Transaction Date"), + args.posting_date, + args.party_type == "Customer" ? "customer" : "supplier" + ) + ) + return; } - if (!erpnext.utils.validate_mandatory(frm, "Company", frm.doc.company, args.party_type=="Customer" ? "customer": "supplier")) { + if ( + !erpnext.utils.validate_mandatory( + frm, + __("Company"), + frm.doc.company, + args.party_type == "Customer" ? "customer" : "supplier" + ) + ) { return; } @@ -152,13 +166,25 @@ erpnext.utils.set_taxes_from_address = function(frm, triggered_from_field, billi if (frm.updating_party_details) return; if (frappe.meta.get_docfield(frm.doc.doctype, "taxes")) { - if (!erpnext.utils.validate_mandatory(frm, "Lead / Customer / Supplier", - frm.doc.customer || frm.doc.supplier || frm.doc.lead || frm.doc.party_name, triggered_from_field)) { + if ( + !erpnext.utils.validate_mandatory( + frm, + __("Lead / Customer / Supplier"), + frm.doc.customer || frm.doc.supplier || frm.doc.lead || frm.doc.party_name, + triggered_from_field + ) + ) { return; } - if (!erpnext.utils.validate_mandatory(frm, "Posting / Transaction Date", - frm.doc.posting_date || frm.doc.transaction_date, triggered_from_field)) { + if ( + !erpnext.utils.validate_mandatory( + frm, + __("Posting / Transaction Date"), + frm.doc.posting_date || frm.doc.transaction_date, + triggered_from_field + ) + ) { return; } } else { @@ -186,17 +212,29 @@ erpnext.utils.set_taxes_from_address = function(frm, triggered_from_field, billi erpnext.utils.set_taxes = function(frm, triggered_from_field) { if (frappe.meta.get_docfield(frm.doc.doctype, "taxes")) { - if (!erpnext.utils.validate_mandatory(frm, "Company", frm.doc.company, triggered_from_field)) { + if (!erpnext.utils.validate_mandatory(frm, __("Company"), frm.doc.company, triggered_from_field)) { return; } - if (!erpnext.utils.validate_mandatory(frm, "Lead / Customer / Supplier", - frm.doc.customer || frm.doc.supplier || frm.doc.lead || frm.doc.party_name, triggered_from_field)) { + if ( + !erpnext.utils.validate_mandatory( + frm, + __("Lead / Customer / Supplier"), + frm.doc.customer || frm.doc.supplier || frm.doc.lead || frm.doc.party_name, + triggered_from_field + ) + ) { return; } - if (!erpnext.utils.validate_mandatory(frm, "Posting / Transaction Date", - frm.doc.posting_date || frm.doc.transaction_date, triggered_from_field)) { + if ( + !erpnext.utils.validate_mandatory( + frm, + __("Posting / Transaction Date"), + frm.doc.posting_date || frm.doc.transaction_date, + triggered_from_field + ) + ) { return; } } else { diff --git a/erpnext/selling/doctype/sales_order/sales_order.json b/erpnext/selling/doctype/sales_order/sales_order.json index bf8d4ec70d..845fc9b5d1 100644 --- a/erpnext/selling/doctype/sales_order/sales_order.json +++ b/erpnext/selling/doctype/sales_order/sales_order.json @@ -159,6 +159,10 @@ "campaign", "external_reference", "party_account_currency", + "margin_section", + "markup_percentage", + "column_break_zpzm", + "gross_profit_percentage", "subscription__auto_repeat_tab", "subscription_section", "from_date", @@ -1666,13 +1670,34 @@ "fieldname": "subscription__auto_repeat_tab", "fieldtype": "Tab Break", "label": "Subscription / Auto Repeat" + }, + { + "fieldname": "margin_section", + "fieldtype": "Section Break", + "label": "Margin / Markup" + }, + { + "fieldname": "markup_percentage", + "fieldtype": "Percent", + "label": "Markup Percentage", + "read_only": 1 + }, + { + "fieldname": "column_break_zpzm", + "fieldtype": "Column Break" + }, + { + "fieldname": "gross_profit_percentage", + "fieldtype": "Percent", + "label": "Gross Profit Percentage", + "read_only": 1 } ], "icon": "uil uil-file-alt", "idx": 105, "is_submittable": 1, "links": [], - "modified": "2024-03-29 16:27:41.539613", + "modified": "2024-05-20 14:21:12.427725", "modified_by": "Administrator", "module": "Selling", "name": "Sales Order", diff --git a/erpnext/selling/doctype/sales_order/sales_order.py b/erpnext/selling/doctype/sales_order/sales_order.py index 5067bc2ff0..d1ad378c2c 100755 --- a/erpnext/selling/doctype/sales_order/sales_order.py +++ b/erpnext/selling/doctype/sales_order/sales_order.py @@ -68,7 +68,9 @@ class SalesOrder(SellingController): additional_discount_percentage: DF.Float address_display: DF.TextEditor | None advance_paid: DF.Currency - advance_payment_status: DF.Literal["Not Requested", "Requested", "Partially Paid", "Fully Paid"] + advance_payment_status: DF.Literal[ + "Not Requested", "Requested", "Partially Paid", "Fully Paid", "Pending", "Failed" + ] amended_from: DF.Link | None amount_eligible_for_commission: DF.Currency apply_discount_on: DF.Literal["", "Grand Total", "Net Total"] @@ -108,8 +110,10 @@ class SalesOrder(SellingController): discount_amount: DF.Currency dispatch_address: DF.TextEditor | None dispatch_address_name: DF.Link | None + external_reference: DF.Data | None from_date: DF.Date | None grand_total: DF.Currency + gross_profit_percentage: DF.Percent group_same_items: DF.Check ignore_pricing_rule: DF.Check in_words: DF.Data | None @@ -121,6 +125,7 @@ class SalesOrder(SellingController): letter_head: DF.Link | None loyalty_amount: DF.Currency loyalty_points: DF.Int + markup_percentage: DF.Percent named_place: DF.Data | None naming_series: DF.Literal["SAL-ORD-.YYYY.-"] net_total: DF.Currency @@ -139,6 +144,7 @@ class SalesOrder(SellingController): price_list_currency: DF.Link pricing_rules: DF.Table[PricingRuleDetail] project: DF.Link | None + recurrence_period: DF.Link | None represents_company: DF.Link | None reserve_stock: DF.Check rounded_total: DF.Currency @@ -166,6 +172,7 @@ class SalesOrder(SellingController): "Cancelled", "Closed", ] + subscription: DF.Link | None tax_category: DF.Link | None tax_id: DF.Data | None taxes: DF.Table[SalesTaxesandCharges] diff --git a/erpnext/selling/doctype/sales_order_item/sales_order_item.json b/erpnext/selling/doctype/sales_order_item/sales_order_item.json index ac0a010ec6..c9e2c00965 100644 --- a/erpnext/selling/doctype/sales_order_item/sales_order_item.json +++ b/erpnext/selling/doctype/sales_order_item/sales_order_item.json @@ -60,17 +60,29 @@ "base_net_rate", "base_net_amount", "billed_amt", + "cost_calculation_section", "valuation_rate", + "last_purchase_rate", + "column_break_kzzw", + "supplier", + "supplier_unit_cost_price", + "section_break_kssi", + "gross_profit_calculation_rule", + "gross_profit_percentage", + "markup_percentage", "gross_profit", + "column_break_dxth", + "base_unit_cost_price", + "additional_costs_percentage", + "additional_costs_amount", + "unit_cost_price", "drop_ship_section", "delivered_by_supplier", - "supplier", "item_weight_details", "weight_per_unit", "total_weight", "column_break_21", "weight_uom", - "accounting_dimensions_section", "warehouse_and_reference", "warehouse", "target_warehouse", @@ -931,16 +943,79 @@ "read_only": 1 }, { - "collapsible": 1, - "fieldname": "accounting_dimensions_section", + "fieldname": "cost_calculation_section", "fieldtype": "Section Break", - "label": "Accounting Dimensions" + "label": "Cost Calculation" + }, + { + "fieldname": "column_break_kzzw", + "fieldtype": "Column Break" + }, + { + "description": "Best supplier price if no supplier is set", + "fieldname": "supplier_unit_cost_price", + "fieldtype": "Currency", + "label": "Supplier Unit Price", + "read_only": 1 + }, + { + "fieldname": "section_break_kssi", + "fieldtype": "Section Break" + }, + { + "fieldname": "gross_profit_calculation_rule", + "fieldtype": "Select", + "label": "Cost Price Based On", + "options": "Valuation Rate\nLast Purchase Rate\nSupplier Cost Price" + }, + { + "description": "Set a custom gross profit percentage to recalculate your item selling rate", + "fieldname": "gross_profit_percentage", + "fieldtype": "Percent", + "label": "Gross Profit Percentage" + }, + { + "description": "Set a custom mark-up percentage to recalculate your item selling rate", + "fieldname": "markup_percentage", + "fieldtype": "Percent", + "label": "Mark-up percentage" + }, + { + "fieldname": "column_break_dxth", + "fieldtype": "Column Break" + }, + { + "fieldname": "base_unit_cost_price", + "fieldtype": "Currency", + "label": "Base Unit Cost Price" + }, + { + "fieldname": "additional_costs_percentage", + "fieldtype": "Percent", + "label": "Additional Costs Percentage" + }, + { + "fieldname": "additional_costs_amount", + "fieldtype": "Currency", + "label": "Additional Costs Amount" + }, + { + "fieldname": "unit_cost_price", + "fieldtype": "Currency", + "label": "Unit Cost Price", + "read_only": 1 + }, + { + "fieldname": "last_purchase_rate", + "fieldtype": "Currency", + "label": "Last Purchase Rate", + "read_only": 1 } ], "idx": 1, "istable": 1, "links": [], - "modified": "2024-03-21 18:15:56.625005", + "modified": "2024-05-20 14:33:40.109058", "modified_by": "Administrator", "module": "Selling", "name": "Sales Order Item", diff --git a/erpnext/selling/doctype/sales_order_item/sales_order_item.py b/erpnext/selling/doctype/sales_order_item/sales_order_item.py index 83d3f3bc07..350ec3125f 100644 --- a/erpnext/selling/doctype/sales_order_item/sales_order_item.py +++ b/erpnext/selling/doctype/sales_order_item/sales_order_item.py @@ -7,6 +7,104 @@ from frappe.model.document import Document class SalesOrderItem(Document): + # begin: auto-generated types + # This code is auto-generated. Do not modify anything in this block. + + from typing import TYPE_CHECKING + + if TYPE_CHECKING: + from frappe.types import DF + + actual_qty: DF.Float + additional_costs_amount: DF.Currency + additional_costs_percentage: DF.Percent + additional_notes: DF.Text | None + against_blanket_order: DF.Check + amount: DF.Currency + base_amount: DF.Currency + base_net_amount: DF.Currency + base_net_rate: DF.Currency + base_price_list_rate: DF.Currency + base_rate: DF.Currency + base_rate_with_margin: DF.Currency + base_unit_cost_price: DF.Currency + billed_amt: DF.Currency + blanket_order: DF.Link | None + blanket_order_rate: DF.Currency + bom_no: DF.Link | None + brand: DF.Link | None + conversion_factor: DF.Float + customer_item_code: DF.Data | None + delivered_by_supplier: DF.Check + delivered_qty: DF.Float + delivery_date: DF.Date | None + description: DF.TextEditor | None + discount_amount: DF.Currency + discount_percentage: DF.Percent + ensure_delivery_based_on_produced_serial_no: DF.Check + grant_commission: DF.Check + gross_profit: DF.Currency + gross_profit_calculation_rule: DF.Literal[ + "Valuation Rate", "Last Purchase Rate", "Supplier Cost Price" + ] + gross_profit_percentage: DF.Percent + image: DF.Attach | None + is_free_item: DF.Check + is_recurring_item: DF.Check + is_stock_item: DF.Check + item_booking: DF.Link | None + item_code: DF.Link | None + item_group: DF.Link | None + item_name: DF.Data + item_tax_rate: DF.Code | None + item_tax_template: DF.Link | None + last_purchase_rate: DF.Currency + margin_rate_or_amount: DF.Float + margin_type: DF.Literal["", "Percentage", "Amount"] + markup_percentage: DF.Percent + material_request: DF.Link | None + material_request_item: DF.Data | None + net_amount: DF.Currency + net_rate: DF.Currency + ordered_qty: DF.Float + page_break: DF.Check + parent: DF.Data + parentfield: DF.Data + parenttype: DF.Data + picked_qty: DF.Float + planned_qty: DF.Float + prevdoc_docname: DF.Link | None + price_list_rate: DF.Currency + pricing_rules: DF.SmallText | None + produced_qty: DF.Float + production_plan_qty: DF.Float + projected_qty: DF.Float + purchase_order: DF.Link | None + purchase_order_item: DF.Data | None + qty: DF.Float + quotation_item: DF.Data | None + rate: DF.Currency + rate_with_margin: DF.Currency + reserve_stock: DF.Check + returned_qty: DF.Float + stock_qty: DF.Float + stock_reserved_qty: DF.Float + stock_uom: DF.Link | None + stock_uom_rate: DF.Currency + supplier: DF.Link | None + supplier_unit_cost_price: DF.Currency + target_warehouse: DF.Link | None + total_weight: DF.Float + transaction_date: DF.Date | None + unit_cost_price: DF.Currency + uom: DF.Link + valuation_rate: DF.Currency + warehouse: DF.Link | None + weight_per_unit: DF.Float + weight_uom: DF.Link | None + work_order_qty: DF.Float + # end: auto-generated types + pass -- GitLab From 7238d00c5989606aff11d8b3cd7aad6781432c0c Mon Sep 17 00:00:00 2001 From: Charles-Henri Decultot Date: Mon, 20 May 2024 14:43:09 +0200 Subject: [PATCH 2/5] fix: Add missing accounting dimensions --- .../sales_order_item/sales_order_item.json | 27 ++++++++++++++++++- .../sales_order_item/sales_order_item.py | 2 ++ 2 files changed, 28 insertions(+), 1 deletion(-) diff --git a/erpnext/selling/doctype/sales_order_item/sales_order_item.json b/erpnext/selling/doctype/sales_order_item/sales_order_item.json index c9e2c00965..8312731f7b 100644 --- a/erpnext/selling/doctype/sales_order_item/sales_order_item.json +++ b/erpnext/selling/doctype/sales_order_item/sales_order_item.json @@ -83,6 +83,10 @@ "total_weight", "column_break_21", "weight_uom", + "accounting_dimensions_section", + "project", + "column_break_geoo", + "cost_center", "warehouse_and_reference", "warehouse", "target_warehouse", @@ -1010,12 +1014,33 @@ "fieldtype": "Currency", "label": "Last Purchase Rate", "read_only": 1 + }, + { + "fieldname": "accounting_dimensions_section", + "fieldtype": "Section Break", + "label": "Accounting Dimensions" + }, + { + "fieldname": "project", + "fieldtype": "Link", + "label": "Project", + "options": "Project" + }, + { + "fieldname": "column_break_geoo", + "fieldtype": "Column Break" + }, + { + "fieldname": "cost_center", + "fieldtype": "Link", + "label": "Cost Center", + "options": "Cost Center" } ], "idx": 1, "istable": 1, "links": [], - "modified": "2024-05-20 14:33:40.109058", + "modified": "2024-05-20 14:42:24.670935", "modified_by": "Administrator", "module": "Selling", "name": "Sales Order Item", diff --git a/erpnext/selling/doctype/sales_order_item/sales_order_item.py b/erpnext/selling/doctype/sales_order_item/sales_order_item.py index 350ec3125f..74e47e4d19 100644 --- a/erpnext/selling/doctype/sales_order_item/sales_order_item.py +++ b/erpnext/selling/doctype/sales_order_item/sales_order_item.py @@ -34,6 +34,7 @@ class SalesOrderItem(Document): bom_no: DF.Link | None brand: DF.Link | None conversion_factor: DF.Float + cost_center: DF.Link | None customer_item_code: DF.Data | None delivered_by_supplier: DF.Check delivered_qty: DF.Float @@ -78,6 +79,7 @@ class SalesOrderItem(Document): pricing_rules: DF.SmallText | None produced_qty: DF.Float production_plan_qty: DF.Float + project: DF.Link | None projected_qty: DF.Float purchase_order: DF.Link | None purchase_order_item: DF.Data | None -- GitLab From bd2d116ba8df6c1d4f4dabd0cd1f90ae466bc804 Mon Sep 17 00:00:00 2001 From: Charles-Henri Decultot Date: Mon, 20 May 2024 14:54:23 +0200 Subject: [PATCH 3/5] fix: Calculate total margin on sales orders also --- erpnext/controllers/selling_controller.py | 33 ++++++++++++----------- 1 file changed, 17 insertions(+), 16 deletions(-) diff --git a/erpnext/controllers/selling_controller.py b/erpnext/controllers/selling_controller.py index 1794f92bee..f077cc677d 100644 --- a/erpnext/controllers/selling_controller.py +++ b/erpnext/controllers/selling_controller.py @@ -20,13 +20,13 @@ class SellingController(StockController): self.flags.ignore_permlevel_for_fields = ["selling_price_list", "price_list_currency"] def onload(self): - super(SellingController, self).onload() + super().onload() if self.doctype in ("Sales Order", "Delivery Note", "Sales Invoice"): for item in self.get("items") + (self.get("packed_items") or []): item.update(get_bin_details(item.item_code, item.warehouse, include_child_warehouses=True)) def validate(self): - super(SellingController, self).validate() + super().validate() self.validate_items() if not self.get("is_debit_note"): self.validate_max_discount() @@ -45,7 +45,7 @@ class SellingController(StockController): self.set_serial_and_batch_bundle(table_field) def set_missing_values(self, for_validate=False): - super(SellingController, self).set_missing_values(for_validate) + super().set_missing_values(for_validate) # set contact and address details for customer, if they are not mentioned self.set_missing_lead_customer_details(for_validate=for_validate) @@ -291,7 +291,10 @@ class SellingController(StockController): if flt(item.base_net_rate) < flt(last_valuation_rate_in_sales_uom): throw_message( - item.idx, item.item_name, last_valuation_rate_in_sales_uom, "valuation rate (Moving Average)" + item.idx, + item.item_name, + last_valuation_rate_in_sales_uom, + "valuation rate (Moving Average)", ) def get_item_list(self): @@ -423,7 +426,8 @@ class SellingController(StockController): "Cancelled" ]: frappe.throw( - _("{0} {1} is cancelled or closed").format(_("Sales Order"), so), frappe.InvalidStatusError + _("{0} {1} is cancelled or closed").format(_("Sales Order"), so), + frappe.InvalidStatusError, ) sales_order.update_reserved_qty(so_item_rows) @@ -627,11 +631,12 @@ class SellingController(StockController): if self.doctype in ["Sales Order", "Quotation"]: for item in self.items: item.gross_profit = flt( - ((item.base_rate - flt(item.valuation_rate)) * item.stock_qty), self.precision("amount", item) + ((item.base_rate - flt(item.valuation_rate)) * item.stock_qty), + self.precision("amount", item), ) def set_profit_margins(self): - if self.doctype == "Quotation": + if self.doctype in ("Quotation", "Sales Order"): total_gross_profit = 0.0 total_cost = 0.0 total_selling_amount = 0.0 @@ -732,9 +737,9 @@ class SellingController(StockController): if d.get("target_warehouse") and d.get("warehouse") == d.get("target_warehouse"): warehouse = frappe.bold(d.get("target_warehouse")) frappe.throw( - _("Row {0}: Delivery Warehouse ({1}) and Customer Warehouse ({2}) can not be same").format( - d.idx, warehouse, warehouse - ) + _( + "Row {0}: Delivery Warehouse ({1}) and Customer Warehouse ({2}) can not be same" + ).format(d.idx, warehouse, warehouse) ) if not self.get("is_internal_customer") and any(d.get("target_warehouse") for d in items): @@ -776,14 +781,10 @@ def get_serial_and_batch_bundle(child, parent): if child.get("use_serial_batch_fields"): return - if not frappe.db.get_single_value( - "Stock Settings", "auto_create_serial_and_batch_bundle_for_outward" - ): + if not frappe.db.get_single_value("Stock Settings", "auto_create_serial_and_batch_bundle_for_outward"): return - item_details = frappe.db.get_value( - "Item", child.item_code, ["has_serial_no", "has_batch_no"], as_dict=1 - ) + item_details = frappe.db.get_value("Item", child.item_code, ["has_serial_no", "has_batch_no"], as_dict=1) if not item_details.has_serial_no and not item_details.has_batch_no: return -- GitLab From 24b2daba87f394c21b9a362243881bb8c0f6de25 Mon Sep 17 00:00:00 2001 From: Charles-Henri Decultot Date: Mon, 20 May 2024 17:52:25 +0200 Subject: [PATCH 4/5] fix: rules for field display toggle --- .../selling_settings/selling_settings.py | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/erpnext/selling/doctype/selling_settings/selling_settings.py b/erpnext/selling/doctype/selling_settings/selling_settings.py index 7c7a62dec5..cc1332dc00 100644 --- a/erpnext/selling/doctype/selling_settings/selling_settings.py +++ b/erpnext/selling/doctype/selling_settings/selling_settings.py @@ -163,13 +163,14 @@ class SellingSettings(Document): read_only_field = "markup_percentage" displayed_field = "gross_profit_percentage" - delete_property_setter("Quotation Item", "read_only", displayed_field) + for dt in ("Quotation Item", "Sales Order Item"): + delete_property_setter(dt, "read_only", displayed_field) - make_property_setter( - "Quotation Item", - read_only_field, - "read_only", - 1, - "Check", - validate_fields_for_doctype=False, - ) + make_property_setter( + dt, + read_only_field, + "read_only", + 1, + "Check", + validate_fields_for_doctype=False, + ) -- GitLab From 091b63f2fb9fa6537aaa44bcb40eb22a42227dd4 Mon Sep 17 00:00:00 2001 From: Charles-Henri Decultot Date: Mon, 20 May 2024 21:38:49 +0200 Subject: [PATCH 5/5] fix: Set precision --- erpnext/controllers/selling_controller.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/erpnext/controllers/selling_controller.py b/erpnext/controllers/selling_controller.py index f077cc677d..d4115b70b9 100644 --- a/erpnext/controllers/selling_controller.py +++ b/erpnext/controllers/selling_controller.py @@ -656,11 +656,15 @@ class SellingController(StockController): self.precision("amount", item), ) - self.gross_profit_percentage = total_gross_profit / total_cost * 100.0 if total_cost else 0.0 - self.markup_percentage = ( + self.gross_profit_percentage = flt( + total_gross_profit / total_cost * 100.0 if total_cost else 0.0, + precision=self.precision("gross_profit_percentage"), + ) + self.markup_percentage = flt( (total_selling_amount - total_cost) / total_selling_amount * 100.0 if total_selling_amount - else 0.0 + else 0.0, + precision=self.precision("markup_percentage"), ) def set_customer_address(self): -- GitLab