diff --git a/erpnext/controllers/selling_controller.py b/erpnext/controllers/selling_controller.py index 1794f92bee7bb25758e2fa7da46aa355a05caa5b..d4115b70b9ace24b789221a0ada86b27746650ff 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 @@ -651,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): @@ -732,9 +741,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 +785,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 diff --git a/erpnext/public/js/utils/party.js b/erpnext/public/js/utils/party.js index f9dc6354aeb4a27accd8f6780edad094b5292dc9..7027de4dac7cc23e78810f323cd87594bda7849e 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 bf8d4ec70de5c08afb7c5fec36e0a0868de5fc44..845fc9b5d1061be8075309e12acc2e2f05dfdaa2 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 5067bc2ff027e386f84d508cfdd4ca42cf715fbf..d1ad378c2c6344da018352656f1260b5b636e722 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 ac0a010ec68bd590ac1dc77f3b741f209bf83ca5..8312731f7b8390ed34abab279c52a33496718926 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,33 @@ "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", + "project", + "column_break_geoo", + "cost_center", "warehouse_and_reference", "warehouse", "target_warehouse", @@ -931,16 +947,100 @@ "read_only": 1 }, { - "collapsible": 1, + "fieldname": "cost_calculation_section", + "fieldtype": "Section Break", + "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 + }, + { "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-03-21 18:15:56.625005", + "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 83d3f3bc0765b9af31ff063780b8019967918bdb..74e47e4d192087a3df98926e9c3b8a74de596686 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,106 @@ 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 + cost_center: DF.Link | None + 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 + project: DF.Link | None + 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 diff --git a/erpnext/selling/doctype/selling_settings/selling_settings.py b/erpnext/selling/doctype/selling_settings/selling_settings.py index 7c7a62dec59ec6f9e18c3372fd61e0865dbea5a2..cc1332dc0058650198f5798c646ed616b0b8117c 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, + )