From 91982d1e4f7012a6a28ed8116a65fdfd9b9f973f Mon Sep 17 00:00:00 2001 From: marination Date: Tue, 24 Jan 2023 18:03:53 +0530 Subject: [PATCH 01/65] feat: Filter out alternative item rows in taxes and totals for Quotation - Added a Quotation Item field `is_alternative_item` - Use filtered rows for taxes and totals computation --- erpnext/controllers/taxes_and_totals.py | 32 ++++++++++++------- .../quotation_item/quotation_item.json | 11 ++++++- 2 files changed, 30 insertions(+), 13 deletions(-) diff --git a/erpnext/controllers/taxes_and_totals.py b/erpnext/controllers/taxes_and_totals.py index 8c403aa9bfe..5815cfa7ac9 100644 --- a/erpnext/controllers/taxes_and_totals.py +++ b/erpnext/controllers/taxes_and_totals.py @@ -24,11 +24,19 @@ class calculate_taxes_and_totals(object): def __init__(self, doc: Document): self.doc = doc frappe.flags.round_off_applicable_accounts = [] + + self._items = self.filter_rows() if self.doc.doctype == "Quotation" else self.doc.get("items") + get_round_off_applicable_accounts(self.doc.company, frappe.flags.round_off_applicable_accounts) self.calculate() + def filter_rows(self): + """Exclude rows, that do not fulfill the filter criteria, from totals computation.""" + items = list(filter(lambda item: not item.get("is_alternative_item"), self.doc.get("items"))) + return items + def calculate(self): - if not len(self.doc.get("items")): + if not len(self._items): return self.discount_amount_applied = False @@ -70,7 +78,7 @@ class calculate_taxes_and_totals(object): if hasattr(self.doc, "tax_withholding_net_total"): sum_net_amount = 0 sum_base_net_amount = 0 - for item in self.doc.get("items"): + for item in self._items: if hasattr(item, "apply_tds") and item.apply_tds: sum_net_amount += item.net_amount sum_base_net_amount += item.base_net_amount @@ -79,7 +87,7 @@ class calculate_taxes_and_totals(object): self.doc.base_tax_withholding_net_total = sum_base_net_amount def validate_item_tax_template(self): - for item in self.doc.get("items"): + for item in self._items: if item.item_code and item.get("item_tax_template"): item_doc = frappe.get_cached_doc("Item", item.item_code) args = { @@ -137,7 +145,7 @@ class calculate_taxes_and_totals(object): return if not self.discount_amount_applied: - for item in self.doc.get("items"): + for item in self._items: self.doc.round_floats_in(item) if item.discount_percentage == 100: @@ -236,7 +244,7 @@ class calculate_taxes_and_totals(object): if not any(cint(tax.included_in_print_rate) for tax in self.doc.get("taxes")): return - for item in self.doc.get("items"): + for item in self._items: item_tax_map = self._load_item_tax_rate(item.item_tax_rate) cumulated_tax_fraction = 0 total_inclusive_tax_amount_per_qty = 0 @@ -317,7 +325,7 @@ class calculate_taxes_and_totals(object): self.doc.total ) = self.doc.base_total = self.doc.net_total = self.doc.base_net_total = 0.0 - for item in self.doc.get("items"): + for item in self._items: self.doc.total += item.amount self.doc.total_qty += item.qty self.doc.base_total += item.base_amount @@ -354,7 +362,7 @@ class calculate_taxes_and_totals(object): ] ) - for n, item in enumerate(self.doc.get("items")): + for n, item in enumerate(self._items): item_tax_map = self._load_item_tax_rate(item.item_tax_rate) for i, tax in enumerate(self.doc.get("taxes")): # tax_amount represents the amount of tax for the current step @@ -363,7 +371,7 @@ class calculate_taxes_and_totals(object): # Adjust divisional loss to the last item if tax.charge_type == "Actual": actual_tax_dict[tax.idx] -= current_tax_amount - if n == len(self.doc.get("items")) - 1: + if n == len(self._items) - 1: current_tax_amount += actual_tax_dict[tax.idx] # accumulate tax amount into tax.tax_amount @@ -391,7 +399,7 @@ class calculate_taxes_and_totals(object): ) # set precision in the last item iteration - if n == len(self.doc.get("items")) - 1: + if n == len(self._items) - 1: self.round_off_totals(tax) self._set_in_company_currency(tax, ["tax_amount", "tax_amount_after_discount_amount"]) @@ -570,7 +578,7 @@ class calculate_taxes_and_totals(object): def calculate_total_net_weight(self): if self.doc.meta.get_field("total_net_weight"): self.doc.total_net_weight = 0.0 - for d in self.doc.items: + for d in self._items: if d.total_weight: self.doc.total_net_weight += d.total_weight @@ -630,7 +638,7 @@ class calculate_taxes_and_totals(object): if total_for_discount_amount: # calculate item amount after Discount Amount - for i, item in enumerate(self.doc.get("items")): + for i, item in enumerate(self._items): distributed_amount = ( flt(self.doc.discount_amount) * item.net_amount / total_for_discount_amount ) @@ -643,7 +651,7 @@ class calculate_taxes_and_totals(object): self.doc.apply_discount_on == "Net Total" or not taxes or total_for_discount_amount == self.doc.net_total - ) and i == len(self.doc.get("items")) - 1: + ) and i == len(self._items) - 1: discount_amount_loss = flt( self.doc.net_total - net_total - self.doc.discount_amount, self.doc.precision("net_total") ) diff --git a/erpnext/selling/doctype/quotation_item/quotation_item.json b/erpnext/selling/doctype/quotation_item/quotation_item.json index ca7dfd23378..eaa4d1d7477 100644 --- a/erpnext/selling/doctype/quotation_item/quotation_item.json +++ b/erpnext/selling/doctype/quotation_item/quotation_item.json @@ -49,6 +49,7 @@ "pricing_rules", "stock_uom_rate", "is_free_item", + "is_alternative_item", "section_break_43", "valuation_rate", "column_break_45", @@ -643,12 +644,19 @@ "no_copy": 1, "options": "currency", "read_only": 1 + }, + { + "default": "0", + "fieldname": "is_alternative_item", + "fieldtype": "Check", + "label": "Is Alternative Item", + "print_hide": 1 } ], "idx": 1, "istable": 1, "links": [], - "modified": "2022-12-25 02:49:53.926625", + "modified": "2023-01-24 08:48:06.290335", "modified_by": "Administrator", "module": "Selling", "name": "Quotation Item", @@ -656,5 +664,6 @@ "permissions": [], "sort_field": "modified", "sort_order": "DESC", + "states": [], "track_changes": 1 } \ No newline at end of file From f19eadab9ab1a7defd74cf3b7337012e4e111ac8 Mon Sep 17 00:00:00 2001 From: marination Date: Wed, 25 Jan 2023 13:10:03 +0530 Subject: [PATCH 02/65] feat: Consider filtered items table in JS for totals computation - Set `_items` as filtered rows if quotation else the entire table. Set at entry point of JS API - Use `_items` instead of `items` to compute taxes and charges. Exclude alternative item rows --- .../public/js/controllers/taxes_and_totals.js | 30 ++++++++++++------- 1 file changed, 19 insertions(+), 11 deletions(-) diff --git a/erpnext/public/js/controllers/taxes_and_totals.js b/erpnext/public/js/controllers/taxes_and_totals.js index 2ce0c7eb00d..607b928d019 100644 --- a/erpnext/public/js/controllers/taxes_and_totals.js +++ b/erpnext/public/js/controllers/taxes_and_totals.js @@ -91,6 +91,9 @@ erpnext.taxes_and_totals = class TaxesAndTotals extends erpnext.payments { } _calculate_taxes_and_totals() { + const is_quotation = this.frm.doc.doctype == "Quotation"; + this.frm.doc._items = is_quotation ? this.filtered_items() : this.frm.doc.items; + this.validate_conversion_rate(); this.calculate_item_values(); this.initialize_taxes(); @@ -122,7 +125,7 @@ erpnext.taxes_and_totals = class TaxesAndTotals extends erpnext.payments { calculate_item_values() { var me = this; if (!this.discount_amount_applied) { - for (const item of this.frm.doc.items || []) { + for (const item of this.frm.doc._items || []) { frappe.model.round_floats_in(item); item.net_rate = item.rate; item.qty = item.qty === undefined ? (me.frm.doc.is_return ? -1 : 1) : item.qty; @@ -197,7 +200,7 @@ erpnext.taxes_and_totals = class TaxesAndTotals extends erpnext.payments { }); if(has_inclusive_tax==false) return; - $.each(me.frm.doc["items"] || [], function(n, item) { + $.each(me.frm.doc._items || [], function(n, item) { var item_tax_map = me._load_item_tax_rate(item.item_tax_rate); var cumulated_tax_fraction = 0.0; var total_inclusive_tax_amount_per_qty = 0; @@ -268,7 +271,7 @@ erpnext.taxes_and_totals = class TaxesAndTotals extends erpnext.payments { var me = this; this.frm.doc.total_qty = this.frm.doc.total = this.frm.doc.base_total = this.frm.doc.net_total = this.frm.doc.base_net_total = 0.0; - $.each(this.frm.doc["items"] || [], function(i, item) { + $.each(this.frm.doc._items || [], function(i, item) { me.frm.doc.total += item.amount; me.frm.doc.total_qty += item.qty; me.frm.doc.base_total += item.base_amount; @@ -321,7 +324,7 @@ erpnext.taxes_and_totals = class TaxesAndTotals extends erpnext.payments { } }); - $.each(this.frm.doc["items"] || [], function(n, item) { + $.each(this.frm.doc._items || [], function(n, item) { var item_tax_map = me._load_item_tax_rate(item.item_tax_rate); $.each(me.frm.doc["taxes"] || [], function(i, tax) { // tax_amount represents the amount of tax for the current step @@ -330,7 +333,7 @@ erpnext.taxes_and_totals = class TaxesAndTotals extends erpnext.payments { // Adjust divisional loss to the last item if (tax.charge_type == "Actual") { actual_tax_dict[tax.idx] -= current_tax_amount; - if (n == me.frm.doc["items"].length - 1) { + if (n == me.frm.doc._items.length - 1) { current_tax_amount += actual_tax_dict[tax.idx]; } } @@ -367,7 +370,7 @@ erpnext.taxes_and_totals = class TaxesAndTotals extends erpnext.payments { } // set precision in the last item iteration - if (n == me.frm.doc["items"].length - 1) { + if (n == me.frm.doc._items.length - 1) { me.round_off_totals(tax); me.set_in_company_currency(tax, ["tax_amount", "tax_amount_after_discount_amount"]); @@ -590,10 +593,11 @@ erpnext.taxes_and_totals = class TaxesAndTotals extends erpnext.payments { _cleanup() { this.frm.doc.base_in_words = this.frm.doc.in_words = ""; + let items = this.frm.doc._items; - if(this.frm.doc["items"] && this.frm.doc["items"].length) { - if(!frappe.meta.get_docfield(this.frm.doc["items"][0].doctype, "item_tax_amount", this.frm.doctype)) { - $.each(this.frm.doc["items"] || [], function(i, item) { + if(items && items.length) { + if(!frappe.meta.get_docfield(items[0].doctype, "item_tax_amount", this.frm.doctype)) { + $.each(items || [], function(i, item) { delete item["item_tax_amount"]; }); } @@ -646,7 +650,7 @@ erpnext.taxes_and_totals = class TaxesAndTotals extends erpnext.payments { var net_total = 0; // calculate item amount after Discount Amount if (total_for_discount_amount) { - $.each(this.frm.doc["items"] || [], function(i, item) { + $.each(this.frm.doc._items || [], function(i, item) { distributed_amount = flt(me.frm.doc.discount_amount) * item.net_amount / total_for_discount_amount; item.net_amount = flt(item.net_amount - distributed_amount, precision("base_amount", item)); @@ -654,7 +658,7 @@ erpnext.taxes_and_totals = class TaxesAndTotals extends erpnext.payments { // discount amount rounding loss adjustment if no taxes if ((!(me.frm.doc.taxes || []).length || total_for_discount_amount==me.frm.doc.net_total || (me.frm.doc.apply_discount_on == "Net Total")) - && i == (me.frm.doc.items || []).length - 1) { + && i == (me.frm.doc._items || []).length - 1) { var discount_amount_loss = flt(me.frm.doc.net_total - net_total - me.frm.doc.discount_amount, precision("net_total")); item.net_amount = flt(item.net_amount + discount_amount_loss, @@ -883,4 +887,8 @@ erpnext.taxes_and_totals = class TaxesAndTotals extends erpnext.payments { } } + + filtered_items() { + return this.frm.doc.items.filter(item => !item["is_alternative_item"]); + } }; From cef7dfd0b48ba6ebd6dfb9eabb722c78e4493ccb Mon Sep 17 00:00:00 2001 From: marination Date: Thu, 26 Jan 2023 14:36:25 +0530 Subject: [PATCH 03/65] feat: Dialog to select alternative item before creating Sales order - Users can leave the row blank in the dialog if original item is to be used - Else users can select an alternative item against an original item - In the document, users must check `Is Alternative Item` if needed and also specify which item it is an altenrative to since there are no documented mappings --- erpnext/controllers/taxes_and_totals.py | 2 +- .../public/js/controllers/taxes_and_totals.js | 2 +- .../selling/doctype/quotation/quotation.js | 79 ++++++++++++++++++- .../quotation_item/quotation_item.json | 18 ++++- 4 files changed, 94 insertions(+), 7 deletions(-) diff --git a/erpnext/controllers/taxes_and_totals.py b/erpnext/controllers/taxes_and_totals.py index 5815cfa7ac9..1edd7bf85e1 100644 --- a/erpnext/controllers/taxes_and_totals.py +++ b/erpnext/controllers/taxes_and_totals.py @@ -32,7 +32,7 @@ class calculate_taxes_and_totals(object): def filter_rows(self): """Exclude rows, that do not fulfill the filter criteria, from totals computation.""" - items = list(filter(lambda item: not item.get("is_alternative_item"), self.doc.get("items"))) + items = list(filter(lambda item: not item.get("is_alternative"), self.doc.get("items"))) return items def calculate(self): diff --git a/erpnext/public/js/controllers/taxes_and_totals.js b/erpnext/public/js/controllers/taxes_and_totals.js index 607b928d019..029d6c0c417 100644 --- a/erpnext/public/js/controllers/taxes_and_totals.js +++ b/erpnext/public/js/controllers/taxes_and_totals.js @@ -889,6 +889,6 @@ erpnext.taxes_and_totals = class TaxesAndTotals extends erpnext.payments { } filtered_items() { - return this.frm.doc.items.filter(item => !item["is_alternative_item"]); + return this.frm.doc.items.filter(item => !item["is_alternative"]); } }; diff --git a/erpnext/selling/doctype/quotation/quotation.js b/erpnext/selling/doctype/quotation/quotation.js index 6b42e4daead..6f75673a8e5 100644 --- a/erpnext/selling/doctype/quotation/quotation.js +++ b/erpnext/selling/doctype/quotation/quotation.js @@ -87,7 +87,7 @@ erpnext.selling.QuotationController = class QuotationController extends erpnext. if (doc.docstatus == 1 && !["Lost", "Ordered"].includes(doc.status)) { this.frm.add_custom_button( __("Sales Order"), - this.frm.cscript["Make Sales Order"], + () => this.make_sales_order(), __("Create") ); @@ -141,6 +141,20 @@ erpnext.selling.QuotationController = class QuotationController extends erpnext. } + make_sales_order() { + var me = this; + + let has_alternative_item = this.frm.doc.items.some((item) => item.is_alternative); + if (has_alternative_item) { + this.show_alternative_item_dialog(); + } else { + frappe.model.open_mapped_doc({ + method: "erpnext.selling.doctype.quotation.quotation.make_sales_order", + frm: me.frm + }); + } + } + set_dynamic_field_label(){ if (this.frm.doc.quotation_to == "Customer") { @@ -216,6 +230,69 @@ erpnext.selling.QuotationController = class QuotationController extends erpnext. } }) } + + show_alternative_item_dialog() { + // Create a `{original item: [alternate items]}` map + const item_alt_map = {}; + this.frm.doc.items.filter( + (item) => item.is_alternative + ).forEach((item) => + (item_alt_map[item.alternative_to] ??= []).push(item.item_code) + ) + + const fields = [{ + fieldtype:"Link", + fieldname:"original_item", + options: "Item", + label: __("Original Item"), + read_only: 1, + in_list_view: 1, + }, + { + fieldtype:"Link", + fieldname:"alternative_item", + options: "Item", + label: __("Alternative Item"), + in_list_view: 1, + get_query: (row, cdt, cdn) => { + return { + filters: { + "item_code": ["in", item_alt_map[row.original_item]] + } + } + }, + }]; + + this.data = Object.keys(item_alt_map).map((item) => { + return {"original_item": item} + }); + + const dialog = new frappe.ui.Dialog({ + title: __("Select Alternatives for Sales Order"), + fields: [ + { + fieldname: "alternative_items", + fieldtype: "Table", + label: "Items with Alternatives", + cannot_add_rows: true, + in_place_edit: true, + reqd: 1, + data: this.data, + description: __("Select an alternative to be used in the Sales Order or leave it blank to use the original item."), + get_data: () => { + return this.data; + }, + fields: fields + }, + ], + primary_action: function() { + this.hide(); + }, + primary_action_label: __('Continue') + }); + + dialog.show(); + } }; cur_frm.script_manager.make(erpnext.selling.QuotationController); diff --git a/erpnext/selling/doctype/quotation_item/quotation_item.json b/erpnext/selling/doctype/quotation_item/quotation_item.json index eaa4d1d7477..f62a0997dcf 100644 --- a/erpnext/selling/doctype/quotation_item/quotation_item.json +++ b/erpnext/selling/doctype/quotation_item/quotation_item.json @@ -49,7 +49,8 @@ "pricing_rules", "stock_uom_rate", "is_free_item", - "is_alternative_item", + "is_alternative", + "alternative_to", "section_break_43", "valuation_rate", "column_break_45", @@ -647,16 +648,25 @@ }, { "default": "0", - "fieldname": "is_alternative_item", + "fieldname": "is_alternative", "fieldtype": "Check", - "label": "Is Alternative Item", + "label": "Is Alternative", + "print_hide": 1 + }, + { + "depends_on": "is_alternative", + "fieldname": "alternative_to", + "fieldtype": "Link", + "label": "Alternative To", + "mandatory_depends_on": "is_alternative", + "options": "Item", "print_hide": 1 } ], "idx": 1, "istable": 1, "links": [], - "modified": "2023-01-24 08:48:06.290335", + "modified": "2023-01-26 07:32:02.768197", "modified_by": "Administrator", "module": "Selling", "name": "Quotation Item", From 94cacb60de00bda141537eb59d3d775004576a3d Mon Sep 17 00:00:00 2001 From: marination Date: Mon, 30 Jan 2023 13:54:30 +0530 Subject: [PATCH 04/65] feat: Filter rows to be mapped on server side mapping function - Pass dialog selections to `make_sales_order` - Map either original item or its alternative depending on mapping - Only qty check for simple rows (without alternatives and not an alternative itself) --- .../selling/doctype/quotation/quotation.js | 20 +++++++------ .../selling/doctype/quotation/quotation.py | 29 ++++++++++++++++++- 2 files changed, 39 insertions(+), 10 deletions(-) diff --git a/erpnext/selling/doctype/quotation/quotation.js b/erpnext/selling/doctype/quotation/quotation.js index 6f75673a8e5..0ea424f1b43 100644 --- a/erpnext/selling/doctype/quotation/quotation.js +++ b/erpnext/selling/doctype/quotation/quotation.js @@ -233,7 +233,9 @@ erpnext.selling.QuotationController = class QuotationController extends erpnext. show_alternative_item_dialog() { // Create a `{original item: [alternate items]}` map - const item_alt_map = {}; + var me = this; + let item_alt_map = {}; + this.frm.doc.items.filter( (item) => item.is_alternative ).forEach((item) => @@ -286,7 +288,14 @@ erpnext.selling.QuotationController = class QuotationController extends erpnext. }, ], primary_action: function() { - this.hide(); + frappe.model.open_mapped_doc({ + method: "erpnext.selling.doctype.quotation.quotation.make_sales_order", + frm: me.frm, + args: { + mapping: dialog.get_value("alternative_items") + } + }); + dialog.hide(); }, primary_action_label: __('Continue') }); @@ -297,13 +306,6 @@ erpnext.selling.QuotationController = class QuotationController extends erpnext. cur_frm.script_manager.make(erpnext.selling.QuotationController); -cur_frm.cscript['Make Sales Order'] = function() { - frappe.model.open_mapped_doc({ - method: "erpnext.selling.doctype.quotation.quotation.make_sales_order", - frm: cur_frm - }) -} - frappe.ui.form.on("Quotation Item", "items_on_form_rendered", "packed_items_on_form_rendered", function(frm, cdt, cdn) { // enable tax_amount field if Actual }) diff --git a/erpnext/selling/doctype/quotation/quotation.py b/erpnext/selling/doctype/quotation/quotation.py index 6836d56647f..d4ae66e53b9 100644 --- a/erpnext/selling/doctype/quotation/quotation.py +++ b/erpnext/selling/doctype/quotation/quotation.py @@ -210,6 +210,10 @@ def _make_sales_order(source_name, target_doc=None, ignore_permissions=False): ) ) + alternative_map = { + x.get("original_item") : x.get("alternative_item") for x in frappe.flags.get("args", {}).get("mapping", []) + } + def set_missing_values(source, target): if customer: target.customer = customer.name @@ -233,6 +237,29 @@ def _make_sales_order(source_name, target_doc=None, ignore_permissions=False): target.blanket_order = obj.blanket_order target.blanket_order_rate = obj.blanket_order_rate + def can_map_row(item) -> bool: + """ + Row mapping from Quotation to Sales order: + 1. Simple row: Map if adequate qty + 2. Has Alternative Item: Map if no alternative was selected against original item and #1 + 3. Is Alternative Item: Map if alternative was selected against original item and #1 + """ + has_qty = item.qty > 0 + + has_alternative = item.item_code in alternative_map + is_alternative = item.is_alternative + + if not alternative_map or not (is_alternative or has_alternative): + # No alternative items in doc or current row is a simple item (without alternatives) + return has_qty + + if is_alternative: + is_selected = alternative_map.get(item.alternative_to) == item.item_code + else: + is_selected = alternative_map.get(item.item_code) is None + return is_selected and has_qty + + doclist = get_mapped_doc( "Quotation", source_name, @@ -242,7 +269,7 @@ def _make_sales_order(source_name, target_doc=None, ignore_permissions=False): "doctype": "Sales Order Item", "field_map": {"parent": "prevdoc_docname", "name": "quotation_item"}, "postprocess": update_item, - "condition": lambda doc: doc.qty > 0, + "condition": can_map_row, }, "Sales Taxes and Charges": {"doctype": "Sales Taxes and Charges", "add_if_empty": True}, "Sales Team": {"doctype": "Sales Team", "add_if_empty": True}, From fa9b327501a33850374f69f64dcf27ac5b2f2ae3 Mon Sep 17 00:00:00 2001 From: marination Date: Mon, 30 Jan 2023 16:27:01 +0530 Subject: [PATCH 05/65] chore: Validate 'alternative_to' field values, must be a valid non-alterntaive item from table --- erpnext/selling/doctype/quotation/quotation.py | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/erpnext/selling/doctype/quotation/quotation.py b/erpnext/selling/doctype/quotation/quotation.py index d4ae66e53b9..f5613bab15c 100644 --- a/erpnext/selling/doctype/quotation/quotation.py +++ b/erpnext/selling/doctype/quotation/quotation.py @@ -28,6 +28,7 @@ class Quotation(SellingController): self.validate_valid_till() self.validate_shopping_cart_items() self.set_customer_name() + self.validate_alternative_items() if self.items: self.with_items = 1 @@ -99,6 +100,21 @@ class Quotation(SellingController): ) self.customer_name = company_name or lead_name + def validate_alternative_items(self): + items_with_alternatives = filter(lambda item: not item.is_alternative, self.get("items")) + items_with_alternatives = map(lambda item: item.item_code, items_with_alternatives) + + alternative_items = filter(lambda item: item.is_alternative, self.get("items")) + for row in alternative_items: + if row.alternative_to not in items_with_alternatives: + frappe.throw( + _("Row #{0}: {1} is not a valid non-alternative Item from the table").format( + row.idx, frappe.bold(row.alternative_to) + ), + title=_("Invalid Item"), + ) + + def update_opportunity(self, status): for opportunity in set(d.prevdoc_docname for d in self.get("items")): if opportunity: From ece6358e60f5c0bfae6129e3d9613de0d9a40402 Mon Sep 17 00:00:00 2001 From: marination Date: Tue, 31 Jan 2023 17:13:05 +0530 Subject: [PATCH 06/65] fix: Iterate over list instead of map's output and formatting --- erpnext/selling/doctype/quotation/quotation.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/erpnext/selling/doctype/quotation/quotation.py b/erpnext/selling/doctype/quotation/quotation.py index f5613bab15c..98d5c91179b 100644 --- a/erpnext/selling/doctype/quotation/quotation.py +++ b/erpnext/selling/doctype/quotation/quotation.py @@ -102,7 +102,7 @@ class Quotation(SellingController): def validate_alternative_items(self): items_with_alternatives = filter(lambda item: not item.is_alternative, self.get("items")) - items_with_alternatives = map(lambda item: item.item_code, items_with_alternatives) + items_with_alternatives = list(map(lambda item: item.item_code, items_with_alternatives)) alternative_items = filter(lambda item: item.is_alternative, self.get("items")) for row in alternative_items: @@ -114,7 +114,6 @@ class Quotation(SellingController): title=_("Invalid Item"), ) - def update_opportunity(self, status): for opportunity in set(d.prevdoc_docname for d in self.get("items")): if opportunity: @@ -227,7 +226,8 @@ def _make_sales_order(source_name, target_doc=None, ignore_permissions=False): ) alternative_map = { - x.get("original_item") : x.get("alternative_item") for x in frappe.flags.get("args", {}).get("mapping", []) + x.get("original_item"): x.get("alternative_item") + for x in frappe.flags.get("args", {}).get("mapping", []) } def set_missing_values(source, target): @@ -253,7 +253,7 @@ def _make_sales_order(source_name, target_doc=None, ignore_permissions=False): target.blanket_order = obj.blanket_order target.blanket_order_rate = obj.blanket_order_rate - def can_map_row(item) -> bool: + def can_map_row(item) -> bool: """ Row mapping from Quotation to Sales order: 1. Simple row: Map if adequate qty @@ -275,7 +275,6 @@ def _make_sales_order(source_name, target_doc=None, ignore_permissions=False): is_selected = alternative_map.get(item.item_code) is None return is_selected and has_qty - doclist = get_mapped_doc( "Quotation", source_name, From b3fe7c6dad442c5000959dde8537bfcdf6b55390 Mon Sep 17 00:00:00 2001 From: marination Date: Wed, 1 Feb 2023 19:10:32 +0530 Subject: [PATCH 07/65] fix: Consider only ordered alternative/original item for Quotation status - The original and its alternatives make a set of items where one is chosen - While setting order status of Quotation, check if the chosen item from the set is fully ordered or not - Filter out unselected items from the set - Create a map containing the set of items and if they were ordered or not for ease of grouping - The simple items will work as it used to --- .../selling/doctype/quotation/quotation.py | 68 +++++++++++++++++-- 1 file changed, 62 insertions(+), 6 deletions(-) diff --git a/erpnext/selling/doctype/quotation/quotation.py b/erpnext/selling/doctype/quotation/quotation.py index 98d5c91179b..6ef1458f49f 100644 --- a/erpnext/selling/doctype/quotation/quotation.py +++ b/erpnext/selling/doctype/quotation/quotation.py @@ -61,6 +61,7 @@ class Quotation(SellingController): ) def get_ordered_status(self): + status = "Open" ordered_items = frappe._dict( frappe.db.get_all( "Sales Order Item", @@ -71,16 +72,35 @@ class Quotation(SellingController): ) ) - status = "Open" - if ordered_items: + if not ordered_items: + return status + + alternative_items = list(filter(lambda row: row.is_alternative, self.get("items"))) + self._items = self.get_valid_items(alternative_items) if alternative_items else self.get("items") + + if any(row.qty > ordered_items.get(row.item_code, 0.0) for row in self._items): + status = "Partially Ordered" + else: status = "Ordered" - for item in self.get("items"): - if item.qty > ordered_items.get(item.item_code, 0.0): - status = "Partially Ordered" - return status + def get_valid_items(self, alternative_items): + """ + Filters out unordered alternative items/original item from table. + """ + alternatives_map = self.get_alternative_item_map(alternative_items) + + def can_map(row) -> bool: + if row.is_alternative: + return alternatives_map[row.alternative_to][row.item_code] + elif row.item_code in alternatives_map: + return alternatives_map[row.item_code][row.item_code] + else: + return True + + return list(filter(can_map, self.get("items"))) + def is_fully_ordered(self): return self.get_ordered_status() == "Ordered" @@ -114,6 +134,42 @@ class Quotation(SellingController): title=_("Invalid Item"), ) + def get_alternative_item_map(self, alternative_items): + """ + Returns a map of alternatives & the original item with which one was selected by the Customer. + This is to identify sets of alternative-original items from the table. + Structure: + { + 'Original Item': {'Original Item': False, 'Alt-1': True, 'Alt-2': False} + } + """ + alternatives_map = {} + + def add_to_map(row): + in_sales_order = frappe.db.exists( + "Sales Order Item", {"quotation_item": row.name, "item_code": row.item_code} + ) + alternatives_map[row.alternative_to][row.item_code] = bool(in_sales_order) + + for row in alternative_items: + if not alternatives_map.get(row.alternative_to): + alternatives_map.setdefault(row.alternative_to, {}) + add_to_map(row) + + original_item_row = frappe._dict( + name=frappe.get_value( + "Quotation Item", {"item_code": row.alternative_to, "is_alternative": 0} + ), + item_code=row.alternative_to, + alternative_to=row.alternative_to, + ) + add_to_map(original_item_row) + continue + + add_to_map(row) + + return alternatives_map + def update_opportunity(self, status): for opportunity in set(d.prevdoc_docname for d in self.get("items")): if opportunity: From 03321f5f1396bb386b08d289db827962c9b6cbc3 Mon Sep 17 00:00:00 2001 From: marination Date: Thu, 2 Feb 2023 17:28:03 +0530 Subject: [PATCH 08/65] chore: Code simplification - Map is not required, avoid filter multiple times, use single loop instead - Better variable name - Reduce LOC --- .../selling/doctype/quotation/quotation.js | 2 +- .../selling/doctype/quotation/quotation.py | 80 +++++++------------ 2 files changed, 31 insertions(+), 51 deletions(-) diff --git a/erpnext/selling/doctype/quotation/quotation.js b/erpnext/selling/doctype/quotation/quotation.js index 0ea424f1b43..183619e6f3a 100644 --- a/erpnext/selling/doctype/quotation/quotation.js +++ b/erpnext/selling/doctype/quotation/quotation.js @@ -232,10 +232,10 @@ erpnext.selling.QuotationController = class QuotationController extends erpnext. } show_alternative_item_dialog() { - // Create a `{original item: [alternate items]}` map var me = this; let item_alt_map = {}; + // Create a `{original item: [alternate items]}` map this.frm.doc.items.filter( (item) => item.is_alternative ).forEach((item) => diff --git a/erpnext/selling/doctype/quotation/quotation.py b/erpnext/selling/doctype/quotation/quotation.py index 6ef1458f49f..d7882c9eb4d 100644 --- a/erpnext/selling/doctype/quotation/quotation.py +++ b/erpnext/selling/doctype/quotation/quotation.py @@ -75,8 +75,8 @@ class Quotation(SellingController): if not ordered_items: return status - alternative_items = list(filter(lambda row: row.is_alternative, self.get("items"))) - self._items = self.get_valid_items(alternative_items) if alternative_items else self.get("items") + has_alternatives = any(row.is_alternative for row in self.get("items")) + self._items = self.get_valid_items() if has_alternatives else self.get("items") if any(row.qty > ordered_items.get(row.item_code, 0.0) for row in self._items): status = "Partially Ordered" @@ -85,19 +85,26 @@ class Quotation(SellingController): return status - def get_valid_items(self, alternative_items): + def get_valid_items(self): """ - Filters out unordered alternative items/original item from table. + Filters out items in an alternatives set that were not ordered. """ - alternatives_map = self.get_alternative_item_map(alternative_items) + + def is_in_sales_order(row): + in_sales_order = bool( + frappe.db.exists( + "Sales Order Item", {"quotation_item": row.name, "item_code": row.item_code, "docstatus": 1} + ) + ) + return in_sales_order + + items_with_alternatives = self.get_items_having_alternatives() def can_map(row) -> bool: - if row.is_alternative: - return alternatives_map[row.alternative_to][row.item_code] - elif row.item_code in alternatives_map: - return alternatives_map[row.item_code][row.item_code] - else: - return True + if row.is_alternative or (row.item_code in items_with_alternatives): + return is_in_sales_order(row) + + return True return list(filter(can_map, self.get("items"))) @@ -121,12 +128,16 @@ class Quotation(SellingController): self.customer_name = company_name or lead_name def validate_alternative_items(self): - items_with_alternatives = filter(lambda item: not item.is_alternative, self.get("items")) - items_with_alternatives = list(map(lambda item: item.item_code, items_with_alternatives)) + if not any(row.is_alternative for row in self.get("items")): + return + + non_alternative_items = filter(lambda item: not item.is_alternative, self.get("items")) + non_alternative_items = list(map(lambda item: item.item_code, non_alternative_items)) alternative_items = filter(lambda item: item.is_alternative, self.get("items")) + for row in alternative_items: - if row.alternative_to not in items_with_alternatives: + if row.alternative_to not in non_alternative_items: frappe.throw( _("Row #{0}: {1} is not a valid non-alternative Item from the table").format( row.idx, frappe.bold(row.alternative_to) @@ -134,42 +145,6 @@ class Quotation(SellingController): title=_("Invalid Item"), ) - def get_alternative_item_map(self, alternative_items): - """ - Returns a map of alternatives & the original item with which one was selected by the Customer. - This is to identify sets of alternative-original items from the table. - Structure: - { - 'Original Item': {'Original Item': False, 'Alt-1': True, 'Alt-2': False} - } - """ - alternatives_map = {} - - def add_to_map(row): - in_sales_order = frappe.db.exists( - "Sales Order Item", {"quotation_item": row.name, "item_code": row.item_code} - ) - alternatives_map[row.alternative_to][row.item_code] = bool(in_sales_order) - - for row in alternative_items: - if not alternatives_map.get(row.alternative_to): - alternatives_map.setdefault(row.alternative_to, {}) - add_to_map(row) - - original_item_row = frappe._dict( - name=frappe.get_value( - "Quotation Item", {"item_code": row.alternative_to, "is_alternative": 0} - ), - item_code=row.alternative_to, - alternative_to=row.alternative_to, - ) - add_to_map(original_item_row) - continue - - add_to_map(row) - - return alternatives_map - def update_opportunity(self, status): for opportunity in set(d.prevdoc_docname for d in self.get("items")): if opportunity: @@ -247,6 +222,11 @@ class Quotation(SellingController): def on_recurring(self, reference_doc, auto_repeat_doc): self.valid_till = None + def get_items_having_alternatives(self): + alternative_items = filter(lambda item: item.is_alternative, self.get("items")) + items_with_alternatives = set((map(lambda item: item.alternative_to, alternative_items))) + return items_with_alternatives + def get_list_context(context=None): from erpnext.controllers.website_list_for_contact import get_list_context From db2076db693a54a8962588ba26a31682a1acc99f Mon Sep 17 00:00:00 2001 From: marination Date: Mon, 6 Feb 2023 16:25:38 +0530 Subject: [PATCH 09/65] refactor: Order based alternative items mapping - Alternatives must be followed by a non-alternative item row - On submit, store non-alternative rows in hidden checkbox to avoid recomputation - Check for valid/mappable rows by row name - UI: Select from table rows.Add single row for original/alternative item in dialog - UI: Indicator for alternative items in dialog grid - UI: Indicator legend and description of table - DB: Added check field 'Has Alternative Item' not to be confused with 'Has Alternative' in Mfg --- .../selling/doctype/quotation/quotation.js | 93 ++++++++++++------- .../selling/doctype/quotation/quotation.py | 72 +++++++------- .../quotation_item/quotation_item.json | 18 ++-- 3 files changed, 102 insertions(+), 81 deletions(-) diff --git a/erpnext/selling/doctype/quotation/quotation.js b/erpnext/selling/doctype/quotation/quotation.js index 183619e6f3a..a67f9b05cc2 100644 --- a/erpnext/selling/doctype/quotation/quotation.js +++ b/erpnext/selling/doctype/quotation/quotation.js @@ -146,7 +146,7 @@ erpnext.selling.QuotationController = class QuotationController extends erpnext. let has_alternative_item = this.frm.doc.items.some((item) => item.is_alternative); if (has_alternative_item) { - this.show_alternative_item_dialog(); + this.show_alternative_items_dialog(); } else { frappe.model.open_mapped_doc({ method: "erpnext.selling.doctype.quotation.quotation.make_sales_order", @@ -231,60 +231,83 @@ erpnext.selling.QuotationController = class QuotationController extends erpnext. }) } - show_alternative_item_dialog() { + show_alternative_items_dialog() { var me = this; - let item_alt_map = {}; - // Create a `{original item: [alternate items]}` map - this.frm.doc.items.filter( - (item) => item.is_alternative - ).forEach((item) => - (item_alt_map[item.alternative_to] ??= []).push(item.item_code) - ) - - const fields = [{ - fieldtype:"Link", - fieldname:"original_item", - options: "Item", - label: __("Original Item"), + const table_fields = [ + { + fieldtype:"Data", + fieldname:"name", + label: __("Name"), read_only: 1, - in_list_view: 1, }, { fieldtype:"Link", - fieldname:"alternative_item", + fieldname:"item_code", options: "Item", - label: __("Alternative Item"), + label: __("Item Code"), + read_only: 1, in_list_view: 1, - get_query: (row, cdt, cdn) => { - return { - filters: { - "item_code": ["in", item_alt_map[row.original_item]] - } - } - }, + columns: 2, + formatter: (value, df, options, doc) => { + return doc.is_alternative ? `${value}` : value; + } + }, + { + fieldtype:"Data", + fieldname:"description", + label: __("Description"), + in_list_view: 1, + read_only: 1, + }, + { + fieldtype:"Currency", + fieldname:"amount", + label: __("Amount"), + options: "currency", + in_list_view: 1, + read_only: 1, + }, + { + fieldtype:"Check", + fieldname:"is_alternative", + label: __("Is Alternative"), + read_only: 1, }]; - this.data = Object.keys(item_alt_map).map((item) => { - return {"original_item": item} + + this.data = this.frm.doc.items.filter( + (item) => item.is_alternative || item.has_alternative_item + ).map((item) => { + return { + "name": item.name, + "item_code": item.item_code, + "description": item.description, + "amount": item.amount, + "is_alternative": item.is_alternative, + } }); const dialog = new frappe.ui.Dialog({ - title: __("Select Alternatives for Sales Order"), + title: __("Select Alternative Items for Sales Order"), fields: [ + { + fieldname: "info", + fieldtype: "HTML", + read_only: 1 + }, { fieldname: "alternative_items", fieldtype: "Table", - label: "Items with Alternatives", cannot_add_rows: true, in_place_edit: true, reqd: 1, data: this.data, - description: __("Select an alternative to be used in the Sales Order or leave it blank to use the original item."), + description: __("Select an item from each set to be used in the Sales Order."), get_data: () => { return this.data; }, - fields: fields + fields: table_fields }, ], primary_action: function() { @@ -292,7 +315,7 @@ erpnext.selling.QuotationController = class QuotationController extends erpnext. method: "erpnext.selling.doctype.quotation.quotation.make_sales_order", frm: me.frm, args: { - mapping: dialog.get_value("alternative_items") + selected_items: dialog.fields_dict.alternative_items.grid.get_selected_children() } }); dialog.hide(); @@ -300,6 +323,12 @@ erpnext.selling.QuotationController = class QuotationController extends erpnext. primary_action_label: __('Continue') }); + dialog.fields_dict.info.$wrapper.html( + `

+ + Alternative Items +

` + ) dialog.show(); } }; diff --git a/erpnext/selling/doctype/quotation/quotation.py b/erpnext/selling/doctype/quotation/quotation.py index d7882c9eb4d..a4a5667f8e1 100644 --- a/erpnext/selling/doctype/quotation/quotation.py +++ b/erpnext/selling/doctype/quotation/quotation.py @@ -28,7 +28,6 @@ class Quotation(SellingController): self.validate_valid_till() self.validate_shopping_cart_items() self.set_customer_name() - self.validate_alternative_items() if self.items: self.with_items = 1 @@ -36,6 +35,9 @@ class Quotation(SellingController): make_packing_list(self) + def before_submit(self): + self.set_has_alternative_item() + def validate_valid_till(self): if self.valid_till and getdate(self.valid_till) < getdate(self.transaction_date): frappe.throw(_("Valid till date cannot be before transaction date")) @@ -60,6 +62,16 @@ class Quotation(SellingController): title=_("Unpublished Item"), ) + def set_has_alternative_item(self): + """Mark 'Has Alternative Item' for rows.""" + if not any(row.is_alternative for row in self.get("items")): + return + + items_with_alternatives = self.get_rows_with_alternatives() + for row in self.get("items"): + if not row.is_alternative and row.name in items_with_alternatives: + row.has_alternative_item = 1 + def get_ordered_status(self): status = "Open" ordered_items = frappe._dict( @@ -98,10 +110,8 @@ class Quotation(SellingController): ) return in_sales_order - items_with_alternatives = self.get_items_having_alternatives() - def can_map(row) -> bool: - if row.is_alternative or (row.item_code in items_with_alternatives): + if row.is_alternative or row.has_alternative_item: return is_in_sales_order(row) return True @@ -127,24 +137,6 @@ class Quotation(SellingController): ) self.customer_name = company_name or lead_name - def validate_alternative_items(self): - if not any(row.is_alternative for row in self.get("items")): - return - - non_alternative_items = filter(lambda item: not item.is_alternative, self.get("items")) - non_alternative_items = list(map(lambda item: item.item_code, non_alternative_items)) - - alternative_items = filter(lambda item: item.is_alternative, self.get("items")) - - for row in alternative_items: - if row.alternative_to not in non_alternative_items: - frappe.throw( - _("Row #{0}: {1} is not a valid non-alternative Item from the table").format( - row.idx, frappe.bold(row.alternative_to) - ), - title=_("Invalid Item"), - ) - def update_opportunity(self, status): for opportunity in set(d.prevdoc_docname for d in self.get("items")): if opportunity: @@ -222,10 +214,21 @@ class Quotation(SellingController): def on_recurring(self, reference_doc, auto_repeat_doc): self.valid_till = None - def get_items_having_alternatives(self): - alternative_items = filter(lambda item: item.is_alternative, self.get("items")) - items_with_alternatives = set((map(lambda item: item.alternative_to, alternative_items))) - return items_with_alternatives + def get_rows_with_alternatives(self): + rows_with_alternatives = [] + table_length = len(self.get("items")) + + for idx, row in enumerate(self.get("items")): + if row.is_alternative: + continue + + if idx == (table_length - 1): + break + + if self.get("items")[idx + 1].is_alternative: + rows_with_alternatives.append(row.name) + + return rows_with_alternatives def get_list_context(context=None): @@ -261,10 +264,7 @@ def _make_sales_order(source_name, target_doc=None, ignore_permissions=False): ) ) - alternative_map = { - x.get("original_item"): x.get("alternative_item") - for x in frappe.flags.get("args", {}).get("mapping", []) - } + selected_rows = [x.get("name") for x in frappe.flags.get("args", {}).get("selected_items", [])] def set_missing_values(source, target): if customer: @@ -297,19 +297,11 @@ def _make_sales_order(source_name, target_doc=None, ignore_permissions=False): 3. Is Alternative Item: Map if alternative was selected against original item and #1 """ has_qty = item.qty > 0 - - has_alternative = item.item_code in alternative_map - is_alternative = item.is_alternative - - if not alternative_map or not (is_alternative or has_alternative): + if not (item.is_alternative or item.has_alternative_item): # No alternative items in doc or current row is a simple item (without alternatives) return has_qty - if is_alternative: - is_selected = alternative_map.get(item.alternative_to) == item.item_code - else: - is_selected = alternative_map.get(item.item_code) is None - return is_selected and has_qty + return (item.name in selected_rows) and has_qty doclist = get_mapped_doc( "Quotation", diff --git a/erpnext/selling/doctype/quotation_item/quotation_item.json b/erpnext/selling/doctype/quotation_item/quotation_item.json index f62a0997dcf..f2aabc52400 100644 --- a/erpnext/selling/doctype/quotation_item/quotation_item.json +++ b/erpnext/selling/doctype/quotation_item/quotation_item.json @@ -50,7 +50,7 @@ "stock_uom_rate", "is_free_item", "is_alternative", - "alternative_to", + "has_alternative_item", "section_break_43", "valuation_rate", "column_break_45", @@ -654,19 +654,19 @@ "print_hide": 1 }, { - "depends_on": "is_alternative", - "fieldname": "alternative_to", - "fieldtype": "Link", - "label": "Alternative To", - "mandatory_depends_on": "is_alternative", - "options": "Item", - "print_hide": 1 + "default": "0", + "fieldname": "has_alternative_item", + "fieldtype": "Check", + "hidden": 1, + "label": "Has Alternative Item", + "print_hide": 1, + "read_only": 1 } ], "idx": 1, "istable": 1, "links": [], - "modified": "2023-01-26 07:32:02.768197", + "modified": "2023-02-06 11:00:07.042364", "modified_by": "Administrator", "module": "Selling", "name": "Quotation Item", From 74fab53e281b42c2eb3436ae3145d820108b1c13 Mon Sep 17 00:00:00 2001 From: marination Date: Wed, 15 Feb 2023 15:01:55 +0530 Subject: [PATCH 10/65] test: Alternative items in Quotation - Taxes and totals, mapping, back updation --- .../doctype/quotation/test_quotation.py | 133 ++++++++++++++++++ 1 file changed, 133 insertions(+) diff --git a/erpnext/selling/doctype/quotation/test_quotation.py b/erpnext/selling/doctype/quotation/test_quotation.py index cdf5f5d00c5..67f6518657e 100644 --- a/erpnext/selling/doctype/quotation/test_quotation.py +++ b/erpnext/selling/doctype/quotation/test_quotation.py @@ -457,6 +457,139 @@ class TestQuotation(FrappeTestCase): expected_index = id + 1 self.assertEqual(item.idx, expected_index) + def test_alternative_items_with_stock_items(self): + """ + Check if taxes & totals considers only non-alternative items with: + - One set of non-alternative & alternative items [first 3 rows] + - One simple stock item + """ + from erpnext.stock.doctype.item.test_item import make_item + + item_list = [] + stock_items = { + "_Test Simple Item 1": 100, + "_Test Alt 1": 120, + "_Test Alt 2": 110, + "_Test Simple Item 2": 200, + } + + for item, rate in stock_items.items(): + make_item(item, {"is_stock_item": 1}) + item_list.append( + { + "item_code": item, + "qty": 1, + "rate": rate, + "is_alternative": bool("Alt" in item), + } + ) + + quotation = make_quotation(item_list=item_list, do_not_submit=1) + quotation.append( + "taxes", + { + "account_head": "_Test Account VAT - _TC", + "charge_type": "On Net Total", + "cost_center": "_Test Cost Center - _TC", + "description": "VAT", + "doctype": "Sales Taxes and Charges", + "rate": 10, + }, + ) + quotation.submit() + + self.assertEqual(quotation.net_total, 300) + self.assertEqual(quotation.grand_total, 330) + + def test_alternative_items_with_service_items(self): + """ + Check if taxes & totals considers only non-alternative items with: + - One set of non-alternative & alternative service items [first 3 rows] + - One simple non-alternative service item + All having the same item code and unique item name/description due to + dynamic services + """ + from erpnext.stock.doctype.item.test_item import make_item + + item_list = [] + service_items = { + "Tiling with Standard Tiles": 100, + "Alt Tiling with Durable Tiles": 150, + "Alt Tiling with Premium Tiles": 180, + "False Ceiling with Material #234": 190, + } + + make_item("_Test Dynamic Service Item", {"is_stock_item": 0}) + + for name, rate in service_items.items(): + item_list.append( + { + "item_code": "_Test Dynamic Service Item", + "item_name": name, + "description": name, + "qty": 1, + "rate": rate, + "is_alternative": bool("Alt" in name), + } + ) + + quotation = make_quotation(item_list=item_list, do_not_submit=1) + quotation.append( + "taxes", + { + "account_head": "_Test Account VAT - _TC", + "charge_type": "On Net Total", + "cost_center": "_Test Cost Center - _TC", + "description": "VAT", + "doctype": "Sales Taxes and Charges", + "rate": 10, + }, + ) + quotation.submit() + + self.assertEqual(quotation.net_total, 290) + self.assertEqual(quotation.grand_total, 319) + + def test_alternative_items_sales_order_mapping_with_stock_items(self): + from erpnext.selling.doctype.quotation.quotation import make_sales_order + from erpnext.stock.doctype.item.test_item import make_item + + frappe.flags.args = frappe._dict() + item_list = [] + stock_items = { + "_Test Simple Item 1": 100, + "_Test Alt 1": 120, + "_Test Alt 2": 110, + "_Test Simple Item 2": 200, + } + + for item, rate in stock_items.items(): + make_item(item, {"is_stock_item": 1}) + item_list.append( + { + "item_code": item, + "qty": 1, + "rate": rate, + "is_alternative": bool("Alt" in item), + "warehouse": "_Test Warehouse - _TC", + } + ) + + quotation = make_quotation(item_list=item_list) + + frappe.flags.args.selected_items = [quotation.items[2]] + sales_order = make_sales_order(quotation.name) + sales_order.delivery_date = add_days(sales_order.transaction_date, 10) + sales_order.save() + + self.assertEqual(sales_order.items[0].item_code, "_Test Alt 2") + self.assertEqual(sales_order.items[1].item_code, "_Test Simple Item 2") + self.assertEqual(sales_order.net_total, 310) + + sales_order.submit() + quotation.reload() + self.assertEqual(quotation.status, "Ordered") + test_records = frappe.get_test_records("Quotation") From 6b144baa69c0c31c8e7fb7e9ba3ccd9d9671e7fb Mon Sep 17 00:00:00 2001 From: s-aga-r Date: Sat, 18 Feb 2023 12:18:41 +0530 Subject: [PATCH 11/65] refactor: rewrite `get_item_details.py` queries in `QB` --- erpnext/stock/get_item_details.py | 271 +++++++++++++++--------------- 1 file changed, 140 insertions(+), 131 deletions(-) diff --git a/erpnext/stock/get_item_details.py b/erpnext/stock/get_item_details.py index b53f429edf2..e2a1f3973c2 100644 --- a/erpnext/stock/get_item_details.py +++ b/erpnext/stock/get_item_details.py @@ -8,6 +8,7 @@ import frappe from frappe import _, throw from frappe.model import child_table_fields, default_fields from frappe.model.meta import get_field_precision +from frappe.query_builder.functions import CombineDatetime, IfNull, Sum from frappe.utils import add_days, add_months, cint, cstr, flt, getdate from erpnext import get_company_currency @@ -526,12 +527,8 @@ def get_barcode_data(items_list): itemwise_barcode = {} for item in items_list: - barcodes = frappe.db.sql( - """ - select barcode from `tabItem Barcode` where parent = %s - """, - item.item_code, - as_dict=1, + barcodes = frappe.db.get_all( + "Item Barcode", filters={"parent": item.item_code}, fields="barcode" ) for barcode in barcodes: @@ -891,34 +888,36 @@ def get_item_price(args, item_code, ignore_party=False): :param item_code: str, Item Doctype field item_code """ - args["item_code"] = item_code - - conditions = """where item_code=%(item_code)s - and price_list=%(price_list)s - and ifnull(uom, '') in ('', %(uom)s)""" - - conditions += "and ifnull(batch_no, '') in ('', %(batch_no)s)" + ip = frappe.qb.DocType("Item Price") + query = ( + frappe.qb.from_(ip) + .select(ip.name, ip.price_list_rate, ip.uom) + .where( + (ip.item_code == item_code) + & (ip.price_list == args.get("price_list")) + & (IfNull(ip.uom, "").isin(["", args.get("uom")])) + & (IfNull(ip.batch_no, "").isin(["", args.get("batch_no")])) + ) + .orderby(ip.valid_from, order=frappe.qb.desc) + .orderby(IfNull(ip.batch_no, ""), order=frappe.qb.desc) + .orderby(ip.uom, order=frappe.qb.desc) + ) if not ignore_party: if args.get("customer"): - conditions += " and customer=%(customer)s" + query = query.where(ip.customer == args.get("customer")) elif args.get("supplier"): - conditions += " and supplier=%(supplier)s" + query = query.where(ip.supplier == args.get("supplier")) else: - conditions += "and (customer is null or customer = '') and (supplier is null or supplier = '')" + query = query.where((IfNull(ip.customer, "") == "") & (IfNull(ip.supplier, "") == "")) if args.get("transaction_date"): - conditions += """ and %(transaction_date)s between - ifnull(valid_from, '2000-01-01') and ifnull(valid_upto, '2500-12-31')""" + query = query.where( + (IfNull(ip.valid_from, "2000-01-01") <= args["transaction_date"]) + & (IfNull(ip.valid_upto, "2500-12-31") >= args["transaction_date"]) + ) - return frappe.db.sql( - """ select name, price_list_rate, uom - from `tabItem Price` {conditions} - order by valid_from desc, ifnull(batch_no, '') desc, uom desc """.format( - conditions=conditions - ), - args, - ) + return query.run() def get_price_list_rate_for(args, item_code): @@ -1091,91 +1090,88 @@ def get_pos_profile(company, pos_profile=None, user=None): if not user: user = frappe.session["user"] - condition = "pfu.user = %(user)s AND pfu.default=1" - if user and company: - condition = "pfu.user = %(user)s AND pf.company = %(company)s AND pfu.default=1" + pf = frappe.qb.DocType("POS Profile") + pfu = frappe.qb.DocType("POS Profile User") - pos_profile = frappe.db.sql( - """SELECT pf.* - FROM - `tabPOS Profile` pf LEFT JOIN `tabPOS Profile User` pfu - ON - pf.name = pfu.parent - WHERE - {cond} AND pf.disabled = 0 - """.format( - cond=condition - ), - {"user": user, "company": company}, - as_dict=1, + query = ( + frappe.qb.from_(pf) + .left_join(pfu) + .on(pf.name == pfu.parent) + .select(pf.star) + .where((pfu.user == user) & (pfu.default == 1)) ) + if company: + query = query.where(pf.company == company) + + pos_profile = query.run(as_dict=True) + if not pos_profile and company: - pos_profile = frappe.db.sql( - """SELECT pf.* - FROM - `tabPOS Profile` pf LEFT JOIN `tabPOS Profile User` pfu - ON - pf.name = pfu.parent - WHERE - pf.company = %(company)s AND pf.disabled = 0 - """, - {"company": company}, - as_dict=1, - ) + pos_profile = ( + frappe.qb.from_(pf) + .left_join(pfu) + .on(pf.name == pfu.parent) + .select(pf.star) + .where((pf.company == company) & (pf.disabled == 0)) + ).run(as_dict=True) return pos_profile and pos_profile[0] or None def get_serial_nos_by_fifo(args, sales_order=None): if frappe.db.get_single_value("Stock Settings", "automatically_set_serial_nos_based_on_fifo"): - return "\n".join( - frappe.db.sql_list( - """select name from `tabSerial No` - where item_code=%(item_code)s and warehouse=%(warehouse)s and - sales_order=IF(%(sales_order)s IS NULL, sales_order, %(sales_order)s) - order by timestamp(purchase_date, purchase_time) - asc limit %(qty)s""", - { - "item_code": args.item_code, - "warehouse": args.warehouse, - "qty": abs(cint(args.stock_qty)), - "sales_order": sales_order, - }, - ) + sn = frappe.qb.DocType("Serial No") + query = ( + frappe.qb.from_(sn) + .select(sn.name) + .where((sn.item_code == args.item_code) & (sn.warehouse == args.warehouse)) + .orderby(CombineDatetime(sn.purchase_date, sn.purchase_time)) + .limit(abs(cint(args.stock_qty))) ) + if sales_order: + query = query.where(sn.sales_order == sales_order) + + serial_nos = query.run(as_list=True) + serial_nos = [s[0] for s in serial_nos] + + return "\n".join(serial_nos) + def get_serial_no_batchwise(args, sales_order=None): if frappe.db.get_single_value("Stock Settings", "automatically_set_serial_nos_based_on_fifo"): - return "\n".join( - frappe.db.sql_list( - """select name from `tabSerial No` - where item_code=%(item_code)s and warehouse=%(warehouse)s and - sales_order=IF(%(sales_order)s IS NULL, sales_order, %(sales_order)s) - and batch_no=IF(%(batch_no)s IS NULL, batch_no, %(batch_no)s) order - by timestamp(purchase_date, purchase_time) asc limit %(qty)s""", - { - "item_code": args.item_code, - "warehouse": args.warehouse, - "batch_no": args.batch_no, - "qty": abs(cint(args.stock_qty)), - "sales_order": sales_order, - }, - ) + sn = frappe.qb.DocType("Serial No") + query = ( + frappe.qb.from_(sn) + .select(sn.name) + .where((sn.item_code == args.item_code) & (sn.warehouse == args.warehouse)) + .orderby(CombineDatetime(sn.purchase_date, sn.purchase_time)) + .limit(abs(cint(args.stock_qty))) ) + if sales_order: + query = query.where(sn.sales_order == sales_order) + if args.batch_no: + query = query.where(sn.batch_no == args.batch_no) + + serial_nos = query.run(as_list=True) + serial_nos = [s[0] for s in serial_nos] + + return "\n".join(serial_nos) + @frappe.whitelist() def get_conversion_factor(item_code, uom): variant_of = frappe.db.get_value("Item", item_code, "variant_of", cache=True) filters = {"parent": item_code, "uom": uom} + if variant_of: filters["parent"] = ("in", (item_code, variant_of)) conversion_factor = frappe.db.get_value("UOM Conversion Detail", filters, "conversion_factor") if not conversion_factor: stock_uom = frappe.db.get_value("Item", item_code, "stock_uom") conversion_factor = get_uom_conv_factor(uom, stock_uom) + return {"conversion_factor": conversion_factor or 1.0} @@ -1194,7 +1190,6 @@ def get_bin_details(item_code, warehouse, company=None, include_child_warehouses if warehouse: from frappe.query_builder.functions import Coalesce, Sum - from erpnext.stock.doctype.warehouse.warehouse import get_child_warehouses warehouses = get_child_warehouses(warehouse) if include_child_warehouses else [warehouse] @@ -1217,12 +1212,16 @@ def get_bin_details(item_code, warehouse, company=None, include_child_warehouses def get_company_total_stock(item_code, company): - return frappe.db.sql( - """SELECT sum(actual_qty) from - (`tabBin` INNER JOIN `tabWarehouse` ON `tabBin`.warehouse = `tabWarehouse`.name) - WHERE `tabWarehouse`.company = %s and `tabBin`.item_code = %s""", - (company, item_code), - )[0][0] + bin = frappe.qb.DocType("Bin") + wh = frappe.qb.DocType("Warehouse") + + return ( + frappe.qb.from_(bin) + .inner_join(wh) + .on(bin.warehouse == wh.name) + .select(Sum(bin.actual_qty)) + .where((wh.company == company) & (bin.item_code == item_code)) + ).run()[0][0] @frappe.whitelist() @@ -1231,6 +1230,7 @@ def get_serial_no_details(item_code, warehouse, stock_qty, serial_no): {"item_code": item_code, "warehouse": warehouse, "stock_qty": stock_qty, "serial_no": serial_no} ) serial_no = get_serial_no(args) + return {"serial_no": serial_no} @@ -1250,6 +1250,7 @@ def get_bin_details_and_serial_nos( bin_details_and_serial_nos.update( get_serial_no_details(item_code, warehouse, stock_qty, serial_no) ) + return bin_details_and_serial_nos @@ -1264,6 +1265,7 @@ def get_batch_qty_and_serial_no(batch_no, stock_qty, warehouse, item_code, has_s ) serial_no = get_serial_no(args) batch_qty_and_serial_no.update({"serial_no": serial_no}) + return batch_qty_and_serial_no @@ -1336,7 +1338,6 @@ def apply_price_list(args, as_doc=False): def apply_price_list_on_item(args): item_doc = frappe.db.get_value("Item", args.item_code, ["name", "variant_of"], as_dict=1) item_details = get_price_list_rate(args, item_doc) - item_details.update(get_pricing_rule_for_item(args)) return item_details @@ -1420,12 +1421,15 @@ def get_valuation_rate(item_code, company, warehouse=None): ) or {"valuation_rate": 0} elif not item.get("is_stock_item"): - valuation_rate = frappe.db.sql( - """select sum(base_net_amount) / sum(qty*conversion_factor) - from `tabPurchase Invoice Item` - where item_code = %s and docstatus=1""", - item_code, - ) + pi_item = frappe.qb.DocType("Purchase Invoice Item") + valuation_rate = ( + frappe.qb.from_(pi_item) + .select((Sum(pi_item.base_net_amount) / Sum(pi_item.qty * pi_item.conversion_factor))) + .where( + (pi_item.docstatus == 1) + & (pi_item.item_code == item_code) + ) + ).run() if valuation_rate: return {"valuation_rate": valuation_rate[0][0] or 0.0} @@ -1483,31 +1487,35 @@ def get_blanket_order_details(args): args = frappe._dict(json.loads(args)) blanket_order_details = None - condition = "" - if args.item_code: - if args.customer and args.doctype == "Sales Order": - condition = " and bo.customer=%(customer)s" - elif args.supplier and args.doctype == "Purchase Order": - condition = " and bo.supplier=%(supplier)s" - if args.blanket_order: - condition += " and bo.name =%(blanket_order)s" - if args.transaction_date: - condition += " and bo.to_date>=%(transaction_date)s" - blanket_order_details = frappe.db.sql( - """ - select boi.rate as blanket_order_rate, bo.name as blanket_order - from `tabBlanket Order` bo, `tabBlanket Order Item` boi - where bo.company=%(company)s and boi.item_code=%(item_code)s - and bo.docstatus=1 and bo.name = boi.parent {0} - """.format( - condition - ), - args, - as_dict=True, + if args.item_code: + bo = frappe.qb.DocType("Blanket Order") + bo_item = frappe.qb.DocType("Blanket Order Item") + + query = ( + frappe.qb.from_(bo) + .from_(bo_item) + .select(bo_item.rate.as_("blanket_order_rate"), bo.name.as_("blanket_order")) + .where( + (bo.company == args.company) + & (bo_item.item_code == args.item_code) + & (bo.docstatus == 1) + & (bo.name == bo_item.parent) + ) ) + if args.customer and args.doctype == "Sales Order": + query = query.where(bo.customer == args.customer) + elif args.supplier and args.doctype == "Purchase Order": + query = query.where(bo.supplier == args.supplier) + if args.blanket_order: + query = query.where(bo.name == args.blanket_order) + if args.transaction_date: + query = query.where(bo.to_date >= args.transaction_date) + + blanket_order_details = query.run(as_dict=True) blanket_order_details = blanket_order_details[0] if blanket_order_details else "" + return blanket_order_details @@ -1517,10 +1525,10 @@ def get_so_reservation_for_item(args): if get_reserved_qty_for_so(args.get("against_sales_order"), args.get("item_code")): reserved_so = args.get("against_sales_order") elif args.get("against_sales_invoice"): - sales_order = frappe.db.sql( - """select sales_order from `tabSales Invoice Item` where - parent=%s and item_code=%s""", - (args.get("against_sales_invoice"), args.get("item_code")), + sales_order = frappe.db.get_all( + "Sales Invoice Item", + filters={"parent": args.get("against_sales_invoice"), "item_code": args.get("item_code")}, + fields="sales_order", ) if sales_order and sales_order[0]: if get_reserved_qty_for_so(sales_order[0][0], args.get("item_code")): @@ -1532,13 +1540,14 @@ def get_so_reservation_for_item(args): def get_reserved_qty_for_so(sales_order, item_code): - reserved_qty = frappe.db.sql( - """select sum(qty) from `tabSales Order Item` - where parent=%s and item_code=%s and ensure_delivery_based_on_produced_serial_no=1 - """, - (sales_order, item_code), + reserved_qty = frappe.db.get_value( + "Sales Order Item", + filters={ + "parent": sales_order, + "item_code": item_code, + "ensure_delivery_based_on_produced_serial_no": 1, + }, + fieldname="sum(qty)", ) - if reserved_qty and reserved_qty[0][0]: - return reserved_qty[0][0] - else: - return 0 + + return reserved_qty or 0 From 35489fbbf94080644094a73dd1cc643de1a059c9 Mon Sep 17 00:00:00 2001 From: s-aga-r Date: Sat, 18 Feb 2023 14:33:31 +0530 Subject: [PATCH 12/65] refactor: remove method `get_serial_no_batchwise` from `get_item_details.py` --- erpnext/stock/get_item_details.py | 22 +--------------------- 1 file changed, 1 insertion(+), 21 deletions(-) diff --git a/erpnext/stock/get_item_details.py b/erpnext/stock/get_item_details.py index e2a1f3973c2..f017af6e915 100644 --- a/erpnext/stock/get_item_details.py +++ b/erpnext/stock/get_item_details.py @@ -1119,26 +1119,6 @@ def get_pos_profile(company, pos_profile=None, user=None): def get_serial_nos_by_fifo(args, sales_order=None): - if frappe.db.get_single_value("Stock Settings", "automatically_set_serial_nos_based_on_fifo"): - sn = frappe.qb.DocType("Serial No") - query = ( - frappe.qb.from_(sn) - .select(sn.name) - .where((sn.item_code == args.item_code) & (sn.warehouse == args.warehouse)) - .orderby(CombineDatetime(sn.purchase_date, sn.purchase_time)) - .limit(abs(cint(args.stock_qty))) - ) - - if sales_order: - query = query.where(sn.sales_order == sales_order) - - serial_nos = query.run(as_list=True) - serial_nos = [s[0] for s in serial_nos] - - return "\n".join(serial_nos) - - -def get_serial_no_batchwise(args, sales_order=None): if frappe.db.get_single_value("Stock Settings", "automatically_set_serial_nos_based_on_fifo"): sn = frappe.qb.DocType("Serial No") query = ( @@ -1455,7 +1435,7 @@ def get_serial_no(args, serial_nos=None, sales_order=None): if args.get("warehouse") and args.get("stock_qty") and args.get("item_code"): has_serial_no = frappe.get_value("Item", {"item_code": args.item_code}, "has_serial_no") if args.get("batch_no") and has_serial_no == 1: - return get_serial_no_batchwise(args, sales_order) + return get_serial_nos_by_fifo(args, sales_order) elif has_serial_no == 1: args = json.dumps( { From 3c96791d52f06b611ea14a2814a651af2ecd7649 Mon Sep 17 00:00:00 2001 From: Marica Date: Mon, 20 Feb 2023 12:04:14 +0530 Subject: [PATCH 13/65] fix: Use block variable Co-authored-by: Deepesh Garg --- erpnext/selling/doctype/quotation/quotation.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/selling/doctype/quotation/quotation.js b/erpnext/selling/doctype/quotation/quotation.js index f77dce8f1ab..81ef44d53ed 100644 --- a/erpnext/selling/doctype/quotation/quotation.js +++ b/erpnext/selling/doctype/quotation/quotation.js @@ -236,7 +236,7 @@ erpnext.selling.QuotationController = class QuotationController extends erpnext. } show_alternative_items_dialog() { - var me = this; + let me = this; const table_fields = [ { From 19456127cfde02bdf6873da5dff89ea519e5dddd Mon Sep 17 00:00:00 2001 From: marination Date: Mon, 20 Feb 2023 20:52:14 +0530 Subject: [PATCH 14/65] fix: Handle `Get Items From` in Sales Order - Map all non alternatives from Quotation to SO if no selected items - Show disclaimer mentioning that Qtns with alternatives must be mapped to SO from the Qtn form --- erpnext/selling/doctype/quotation/quotation.py | 18 +++++++++++------- .../selling/doctype/sales_order/sales_order.js | 13 +++++++++++-- 2 files changed, 22 insertions(+), 9 deletions(-) diff --git a/erpnext/selling/doctype/quotation/quotation.py b/erpnext/selling/doctype/quotation/quotation.py index 185f63c345e..b5eddce89ed 100644 --- a/erpnext/selling/doctype/quotation/quotation.py +++ b/erpnext/selling/doctype/quotation/quotation.py @@ -303,16 +303,20 @@ def _make_sales_order(source_name, target_doc=None, ignore_permissions=False): def can_map_row(item) -> bool: """ Row mapping from Quotation to Sales order: - 1. Simple row: Map if adequate qty - 2. Has Alternative Item: Map if no alternative was selected against original item and #1 - 3. Is Alternative Item: Map if alternative was selected against original item and #1 + 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 """ has_qty = item.qty > 0 - if not (item.is_alternative or item.has_alternative_item): - # No alternative items in doc or current row is a simple item (without alternatives) - return has_qty - return (item.name in selected_rows) and has_qty + 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 + + # Simple row + return has_qty doclist = get_mapped_doc( "Quotation", diff --git a/erpnext/selling/doctype/sales_order/sales_order.js b/erpnext/selling/doctype/sales_order/sales_order.js index fb64772479b..a0a63f61d2b 100644 --- a/erpnext/selling/doctype/sales_order/sales_order.js +++ b/erpnext/selling/doctype/sales_order/sales_order.js @@ -275,7 +275,7 @@ erpnext.selling.SalesOrderController = class SalesOrderController extends erpnex if (this.frm.doc.docstatus===0) { this.frm.add_custom_button(__('Quotation'), function() { - erpnext.utils.map_current_doc({ + let d = erpnext.utils.map_current_doc({ method: "erpnext.selling.doctype.quotation.quotation.make_sales_order", source_doctype: "Quotation", target: me.frm, @@ -293,7 +293,16 @@ erpnext.selling.SalesOrderController = class SalesOrderController extends erpnex docstatus: 1, status: ["!=", "Lost"] } - }) + }); + + setTimeout(() => { + d.$parent.append(` + + ${__("Note: Please create Sales Orders from individual Quotations to select from among Alternative Items.")} + + `); + }, 200); + }, __("Get Items From")); } From 6b789e2f0492c3e6932852507b746d1111412028 Mon Sep 17 00:00:00 2001 From: marination Date: Mon, 20 Feb 2023 21:17:43 +0530 Subject: [PATCH 15/65] fix: Map only non alternative items from Quotation in Sales Invoice - Since there's no item selection, only Quotation selection :/ --- erpnext/selling/doctype/quotation/quotation.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/erpnext/selling/doctype/quotation/quotation.py b/erpnext/selling/doctype/quotation/quotation.py index b5eddce89ed..fc66db20d29 100644 --- a/erpnext/selling/doctype/quotation/quotation.py +++ b/erpnext/selling/doctype/quotation/quotation.py @@ -396,7 +396,11 @@ def _make_sales_invoice(source_name, target_doc=None, ignore_permissions=False): source_name, { "Quotation": {"doctype": "Sales Invoice", "validation": {"docstatus": ["=", 1]}}, - "Quotation Item": {"doctype": "Sales Invoice Item", "postprocess": update_item}, + "Quotation Item": { + "doctype": "Sales Invoice Item", + "postprocess": update_item, + "condition": lambda row: not row.is_alternative, + }, "Sales Taxes and Charges": {"doctype": "Sales Taxes and Charges", "add_if_empty": True}, "Sales Team": {"doctype": "Sales Team", "add_if_empty": True}, }, From 19c0b7a5234b017c7a15e8a7b149d386f85180e6 Mon Sep 17 00:00:00 2001 From: vishnu Date: Wed, 22 Feb 2023 10:24:45 +0000 Subject: [PATCH 16/65] fix: currency in coa import --- .../chart_of_accounts/chart_of_accounts.py | 15 ++++++++++++++- .../chart_of_accounts_importer.py | 6 +++++- 2 files changed, 19 insertions(+), 2 deletions(-) diff --git a/erpnext/accounts/doctype/account/chart_of_accounts/chart_of_accounts.py b/erpnext/accounts/doctype/account/chart_of_accounts/chart_of_accounts.py index 75f8f0645c9..9e67c4cf0d0 100644 --- a/erpnext/accounts/doctype/account/chart_of_accounts/chart_of_accounts.py +++ b/erpnext/accounts/doctype/account/chart_of_accounts/chart_of_accounts.py @@ -29,6 +29,7 @@ def create_charts( "root_type", "is_group", "tax_rate", + "account_currency", ]: account_number = cstr(child.get("account_number")).strip() @@ -95,7 +96,17 @@ def identify_is_group(child): is_group = child.get("is_group") elif len( set(child.keys()) - - set(["account_name", "account_type", "root_type", "is_group", "tax_rate", "account_number"]) + - set( + [ + "account_name", + "account_type", + "root_type", + "is_group", + "tax_rate", + "account_number", + "account_currency", + ] + ) ): is_group = 1 else: @@ -185,6 +196,7 @@ def get_account_tree_from_existing_company(existing_company): "root_type", "tax_rate", "account_number", + "account_currency", ], order_by="lft, rgt", ) @@ -267,6 +279,7 @@ def build_tree_from_json(chart_template, chart_data=None, from_coa_importer=Fals "root_type", "is_group", "tax_rate", + "account_currency", ]: continue diff --git a/erpnext/accounts/doctype/chart_of_accounts_importer/chart_of_accounts_importer.py b/erpnext/accounts/doctype/chart_of_accounts_importer/chart_of_accounts_importer.py index 220b74727b9..dd2bd887757 100644 --- a/erpnext/accounts/doctype/chart_of_accounts_importer/chart_of_accounts_importer.py +++ b/erpnext/accounts/doctype/chart_of_accounts_importer/chart_of_accounts_importer.py @@ -36,7 +36,7 @@ def validate_columns(data): no_of_columns = max([len(d) for d in data]) - if no_of_columns > 7: + if no_of_columns > 8: frappe.throw( _("More columns found than expected. Please compare the uploaded file with standard template"), title=(_("Wrong Template")), @@ -233,6 +233,7 @@ def build_forest(data): is_group, account_type, root_type, + account_currency, ) = i if not account_name: @@ -253,6 +254,8 @@ def build_forest(data): charts_map[account_name]["account_type"] = account_type if root_type: charts_map[account_name]["root_type"] = root_type + if account_currency: + charts_map[account_name]["account_currency"] = account_currency path = return_parent(data, account_name)[::-1] paths.append(path) # List of path is created line_no += 1 @@ -315,6 +318,7 @@ def get_template(template_type): "Is Group", "Account Type", "Root Type", + "account_currency", ] writer = UnicodeWriter() writer.writerow(fields) From e3c000d0bec35542b30811aa3f2f16782210770a Mon Sep 17 00:00:00 2001 From: vishnu Date: Wed, 22 Feb 2023 10:53:11 +0000 Subject: [PATCH 17/65] chore: change column label --- .../chart_of_accounts_importer/chart_of_accounts_importer.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/accounts/doctype/chart_of_accounts_importer/chart_of_accounts_importer.py b/erpnext/accounts/doctype/chart_of_accounts_importer/chart_of_accounts_importer.py index dd2bd887757..cb7da179012 100644 --- a/erpnext/accounts/doctype/chart_of_accounts_importer/chart_of_accounts_importer.py +++ b/erpnext/accounts/doctype/chart_of_accounts_importer/chart_of_accounts_importer.py @@ -318,7 +318,7 @@ def get_template(template_type): "Is Group", "Account Type", "Root Type", - "account_currency", + "Account Currency", ] writer = UnicodeWriter() writer.writerow(fields) From 6417ae0ee83d3a2b384987cc3747917e92d6a4ab Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Thu, 23 Feb 2023 20:04:14 +0530 Subject: [PATCH 18/65] fix: user shouldn't able to make item price for item template --- .../stock/doctype/item_price/item_price.js | 13 ++++++++++- .../stock/doctype/item_price/item_price.py | 9 +++++++- .../doctype/item_price/test_item_price.py | 22 +++++++++++++++++++ 3 files changed, 42 insertions(+), 2 deletions(-) diff --git a/erpnext/stock/doctype/item_price/item_price.js b/erpnext/stock/doctype/item_price/item_price.js index 12cf6cf84d5..ce489ff52b4 100644 --- a/erpnext/stock/doctype/item_price/item_price.js +++ b/erpnext/stock/doctype/item_price/item_price.js @@ -2,7 +2,18 @@ // License: GNU General Public License v3. See license.txt frappe.ui.form.on("Item Price", { - onload: function (frm) { + setup(frm) { + frm.set_query("item_code", function() { + return { + filters: { + "disabled": 0, + "has_variants": 0 + } + }; + }); + }, + + onload(frm) { // Fetch price list details frm.add_fetch("price_list", "buying", "buying"); frm.add_fetch("price_list", "selling", "selling"); diff --git a/erpnext/stock/doctype/item_price/item_price.py b/erpnext/stock/doctype/item_price/item_price.py index bcd31ada83e..54d1ae634f5 100644 --- a/erpnext/stock/doctype/item_price/item_price.py +++ b/erpnext/stock/doctype/item_price/item_price.py @@ -3,7 +3,7 @@ import frappe -from frappe import _ +from frappe import _, bold from frappe.model.document import Document from frappe.query_builder import Criterion from frappe.query_builder.functions import Cast_ @@ -21,6 +21,7 @@ class ItemPrice(Document): self.update_price_list_details() self.update_item_details() self.check_duplicates() + self.validate_item_template() def validate_item(self): if not frappe.db.exists("Item", self.item_code): @@ -49,6 +50,12 @@ class ItemPrice(Document): "Item", self.item_code, ["item_name", "description"] ) + def validate_item_template(self): + if frappe.get_cached_value("Item", self.item_code, "has_variants"): + msg = f"Item Price cannot be created for the template item {bold(self.item_code)}" + + frappe.throw(_(msg)) + def check_duplicates(self): item_price = frappe.qb.DocType("Item Price") diff --git a/erpnext/stock/doctype/item_price/test_item_price.py b/erpnext/stock/doctype/item_price/test_item_price.py index 30d933e247d..8fd4938fa35 100644 --- a/erpnext/stock/doctype/item_price/test_item_price.py +++ b/erpnext/stock/doctype/item_price/test_item_price.py @@ -16,6 +16,28 @@ class TestItemPrice(FrappeTestCase): frappe.db.sql("delete from `tabItem Price`") make_test_records_for_doctype("Item Price", force=True) + def test_template_item_price(self): + from erpnext.stock.doctype.item.test_item import make_item + + item = make_item( + "Test Template Item 1", + { + "has_variants": 1, + "variant_based_on": "Manufacturer", + }, + ) + + doc = frappe.get_doc( + { + "doctype": "Item Price", + "price_list": "_Test Price List", + "item_code": item.name, + "price_list_rate": 100, + } + ) + + self.assertRaises(frappe.ValidationError, doc.save) + def test_duplicate_item(self): doc = frappe.copy_doc(test_records[0]) self.assertRaises(ItemPriceDuplicateItem, doc.save) From 49af5ba4349a03131e70c23a4580cb6c9c7d0c7d Mon Sep 17 00:00:00 2001 From: HarryPaulo Date: Thu, 23 Feb 2023 18:11:09 -0300 Subject: [PATCH 19/65] fix: Performance improvement when adding a new item --- erpnext/public/js/controllers/transaction.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/public/js/controllers/transaction.js b/erpnext/public/js/controllers/transaction.js index 09f2c5d5cb1..2d2945473b9 100644 --- a/erpnext/public/js/controllers/transaction.js +++ b/erpnext/public/js/controllers/transaction.js @@ -488,7 +488,7 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe () => { var d = locals[cdt][cdn]; me.add_taxes_from_item_tax_template(d.item_tax_rate); - if (d.free_item_data) { + if (d.free_item_data && d.free_item_data.length > 0) { me.apply_product_discount(d); } }, From 8e46aebc50110fab1a3f123aa22cb8e06907d451 Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Fri, 24 Feb 2023 14:42:55 +0530 Subject: [PATCH 20/65] fix: conversion factor not set --- erpnext/stock/doctype/item/item.js | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/erpnext/stock/doctype/item/item.js b/erpnext/stock/doctype/item/item.js index 5bcb05aa988..9a9ddf44044 100644 --- a/erpnext/stock/doctype/item/item.js +++ b/erpnext/stock/doctype/item/item.js @@ -33,6 +33,9 @@ frappe.ui.form.on("Item", { 'Material Request': () => { open_form(frm, "Material Request", "Material Request Item", "items"); }, + 'Stock Entry': () => { + open_form(frm, "Stock Entry", "Stock Entry Detail", "items"); + }, }; }, @@ -893,6 +896,9 @@ function open_form(frm, doctype, child_doctype, parentfield) { new_child_doc.item_name = frm.doc.item_name; new_child_doc.uom = frm.doc.stock_uom; new_child_doc.description = frm.doc.description; + if (!new_child_doc.qty) { + new_child_doc.qty = 1.0; + } frappe.run_serially([ () => frappe.ui.form.make_quick_entry(doctype, null, null, new_doc), From e26c6dc76b16707c31e9a0277c2710a4c5cb43f0 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Fri, 24 Feb 2023 15:28:14 +0530 Subject: [PATCH 21/65] Revert "fix: Concurrency issues in Sales and Purchase returns" (#34202) Revert "fix: Concurrency issues in Sales and Purchase returns (#34019)" This reverts commit a67284e96dabe71b76a373b5f1f3142dccf3952b. --- erpnext/controllers/sales_and_purchase_return.py | 1 - 1 file changed, 1 deletion(-) diff --git a/erpnext/controllers/sales_and_purchase_return.py b/erpnext/controllers/sales_and_purchase_return.py index fc6793a9bbc..9fcb769bc8c 100644 --- a/erpnext/controllers/sales_and_purchase_return.py +++ b/erpnext/controllers/sales_and_purchase_return.py @@ -252,7 +252,6 @@ def get_already_returned_items(doc): child.parent = par.name and par.docstatus = 1 and par.is_return = 1 and par.return_against = %s group by item_code - for update """.format( column, doc.doctype, doc.doctype ), From 7d10dd9ea8671c27709e88350c6799ba187c4929 Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Fri, 24 Feb 2023 17:50:00 +0530 Subject: [PATCH 22/65] fix: not able to repost gl entries --- erpnext/stock/doctype/purchase_receipt/purchase_receipt.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py index af0d1483253..f19f0a1359a 100644 --- a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py +++ b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py @@ -473,7 +473,7 @@ class PurchaseReceipt(BuyingController): ) divisional_loss = flt( - valuation_amount_as_per_doc - stock_value_diff, d.precision("base_net_amount") + valuation_amount_as_per_doc - flt(stock_value_diff), d.precision("base_net_amount") ) if divisional_loss: From b6bad728cdff7fd79f175dbd5b631f9bd5234ed7 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Fri, 24 Feb 2023 14:11:53 +0530 Subject: [PATCH 23/65] fix: permission error while calling get_work_order_items --- .../doctype/sales_order/sales_order.js | 23 ++-- .../doctype/sales_order/sales_order.py | 102 ++++++++++-------- 2 files changed, 66 insertions(+), 59 deletions(-) diff --git a/erpnext/selling/doctype/sales_order/sales_order.js b/erpnext/selling/doctype/sales_order/sales_order.js index fb64772479b..ee0752549da 100644 --- a/erpnext/selling/doctype/sales_order/sales_order.js +++ b/erpnext/selling/doctype/sales_order/sales_order.js @@ -309,9 +309,12 @@ erpnext.selling.SalesOrderController = class SalesOrderController extends erpnex make_work_order() { var me = this; - this.frm.call({ - doc: this.frm.doc, - method: 'get_work_order_items', + me.frm.call({ + method: "erpnext.selling.doctype.sales_order.sales_order.get_work_order_items", + args: { + sales_order: this.frm.docname, + }, + freeze: true, callback: function(r) { if(!r.message) { frappe.msgprint({ @@ -321,14 +324,7 @@ erpnext.selling.SalesOrderController = class SalesOrderController extends erpnex }); return; } - else if(!r.message) { - frappe.msgprint({ - title: __('Work Order not created'), - message: __('Work Order already created for all items with BOM'), - indicator: 'orange' - }); - return; - } else { + else { const fields = [{ label: 'Items', fieldtype: 'Table', @@ -429,9 +425,9 @@ erpnext.selling.SalesOrderController = class SalesOrderController extends erpnex make_raw_material_request() { var me = this; this.frm.call({ - doc: this.frm.doc, - method: 'get_work_order_items', + method: "erpnext.selling.doctype.sales_order.sales_order.get_work_order_items", args: { + sales_order: this.frm.docname, for_raw_material_request: 1 }, callback: function(r) { @@ -450,6 +446,7 @@ erpnext.selling.SalesOrderController = class SalesOrderController extends erpnex } make_raw_material_request_dialog(r) { + var me = this; var fields = [ {fieldtype:'Check', fieldname:'include_exploded_items', label: __('Include Exploded Items')}, diff --git a/erpnext/selling/doctype/sales_order/sales_order.py b/erpnext/selling/doctype/sales_order/sales_order.py index ca6a51a6f36..385d0f3a585 100755 --- a/erpnext/selling/doctype/sales_order/sales_order.py +++ b/erpnext/selling/doctype/sales_order/sales_order.py @@ -6,11 +6,12 @@ import json import frappe import frappe.utils -from frappe import _ +from frappe import _, qb from frappe.contacts.doctype.address.address import get_company_address from frappe.desk.notifications import clear_doctype_notifications from frappe.model.mapper import get_mapped_doc from frappe.model.utils import get_fetch_values +from frappe.query_builder.functions import Sum from frappe.utils import add_days, cint, cstr, flt, get_link_to_form, getdate, nowdate, strip_html from erpnext.accounts.doctype.sales_invoice.sales_invoice import ( @@ -414,51 +415,6 @@ class SalesOrder(SellingController): self.indicator_color = "green" self.indicator_title = _("Paid") - @frappe.whitelist() - def get_work_order_items(self, for_raw_material_request=0): - """Returns items with BOM that already do not have a linked work order""" - items = [] - item_codes = [i.item_code for i in self.items] - product_bundle_parents = [ - pb.new_item_code - for pb in frappe.get_all( - "Product Bundle", {"new_item_code": ["in", item_codes]}, ["new_item_code"] - ) - ] - - for table in [self.items, self.packed_items]: - for i in table: - bom = get_default_bom(i.item_code) - stock_qty = i.qty if i.doctype == "Packed Item" else i.stock_qty - - if not for_raw_material_request: - total_work_order_qty = flt( - frappe.db.sql( - """select sum(qty) from `tabWork Order` - where production_item=%s and sales_order=%s and sales_order_item = %s and docstatus<2""", - (i.item_code, self.name, i.name), - )[0][0] - ) - pending_qty = stock_qty - total_work_order_qty - else: - pending_qty = stock_qty - - if pending_qty and i.item_code not in product_bundle_parents: - items.append( - dict( - name=i.name, - item_code=i.item_code, - description=i.description, - bom=bom or "", - warehouse=i.warehouse, - pending_qty=pending_qty, - required_qty=pending_qty if for_raw_material_request else 0, - sales_order_item=i.name, - ) - ) - - return items - def on_recurring(self, reference_doc, auto_repeat_doc): def _get_delivery_date(ref_doc_delivery_date, red_doc_transaction_date, transaction_date): delivery_date = auto_repeat_doc.get_next_schedule_date(schedule_date=ref_doc_delivery_date) @@ -1350,3 +1306,57 @@ def update_produced_qty_in_so_item(sales_order, sales_order_item): return frappe.db.set_value("Sales Order Item", sales_order_item, "produced_qty", total_produced_qty) + + +@frappe.whitelist() +def get_work_order_items(sales_order, for_raw_material_request=0): + """Returns items with BOM that already do not have a linked work order""" + if sales_order: + so = frappe.get_doc("Sales Order", sales_order) + + wo = qb.DocType("Work Order") + + items = [] + item_codes = [i.item_code for i in so.items] + product_bundle_parents = [ + pb.new_item_code + for pb in frappe.get_all( + "Product Bundle", {"new_item_code": ["in", item_codes]}, ["new_item_code"] + ) + ] + + for table in [so.items, so.packed_items]: + for i in table: + bom = get_default_bom(i.item_code) + stock_qty = i.qty if i.doctype == "Packed Item" else i.stock_qty + + if not for_raw_material_request: + total_work_order_qty = flt( + qb.from_(wo) + .select(Sum(wo.qty)) + .where( + (wo.production_item == i.item_code) + & (wo.sales_order == so.name) * (wo.sales_order_item == i.name) + & (wo.docstatus.lte(2)) + ) + .run()[0][0] + ) + pending_qty = stock_qty - total_work_order_qty + else: + pending_qty = stock_qty + + if pending_qty and i.item_code not in product_bundle_parents: + items.append( + dict( + name=i.name, + item_code=i.item_code, + description=i.description, + bom=bom or "", + warehouse=i.warehouse, + pending_qty=pending_qty, + required_qty=pending_qty if for_raw_material_request else 0, + sales_order_item=i.name, + ) + ) + + return items From a11d3327dfb5b016b2ff7ca33a5a0431d6e91019 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Fri, 24 Feb 2023 20:21:58 +0530 Subject: [PATCH 24/65] fix(test): use standalone method to fetch work orders from SO --- .../selling/doctype/sales_order/test_sales_order.py | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/erpnext/selling/doctype/sales_order/test_sales_order.py b/erpnext/selling/doctype/sales_order/test_sales_order.py index d4d7c58eb82..627914f0c7e 100644 --- a/erpnext/selling/doctype/sales_order/test_sales_order.py +++ b/erpnext/selling/doctype/sales_order/test_sales_order.py @@ -1217,6 +1217,8 @@ class TestSalesOrder(FrappeTestCase): self.assertTrue(si.get("payment_schedule")) def test_make_work_order(self): + from erpnext.selling.doctype.sales_order.sales_order import get_work_order_items + # Make a new Sales Order so = make_sales_order( **{ @@ -1230,7 +1232,7 @@ class TestSalesOrder(FrappeTestCase): # Raise Work Orders po_items = [] so_item_name = {} - for item in so.get_work_order_items(): + for item in get_work_order_items(so.name): po_items.append( { "warehouse": item.get("warehouse"), @@ -1448,6 +1450,7 @@ class TestSalesOrder(FrappeTestCase): from erpnext.controllers.item_variant import create_variant from erpnext.manufacturing.doctype.production_plan.test_production_plan import make_bom + from erpnext.selling.doctype.sales_order.sales_order import get_work_order_items make_item( # template item "Test-WO-Tshirt", @@ -1487,7 +1490,7 @@ class TestSalesOrder(FrappeTestCase): ] } ) - wo_items = so.get_work_order_items() + wo_items = get_work_order_items(so.name) self.assertEqual(wo_items[0].get("item_code"), "Test-WO-Tshirt-R") self.assertEqual(wo_items[0].get("bom"), red_var_bom.name) @@ -1497,6 +1500,8 @@ class TestSalesOrder(FrappeTestCase): self.assertEqual(wo_items[1].get("bom"), template_bom.name) def test_request_for_raw_materials(self): + from erpnext.selling.doctype.sales_order.sales_order import get_work_order_items + item = make_item( "_Test Finished Item", { @@ -1529,7 +1534,7 @@ class TestSalesOrder(FrappeTestCase): so = make_sales_order(**{"item_list": [{"item_code": item.item_code, "qty": 1, "rate": 1000}]}) so.submit() mr_dict = frappe._dict() - items = so.get_work_order_items(1) + items = get_work_order_items(so.name, 1) mr_dict["items"] = items mr_dict["include_exploded_items"] = 0 mr_dict["ignore_existing_ordered_qty"] = 1 From dd74839eba24c1cd9a0b90d8d353a9cecb920f99 Mon Sep 17 00:00:00 2001 From: anandbaburajan Date: Sun, 26 Feb 2023 17:36:36 +0530 Subject: [PATCH 25/65] fix: manual depr schedule --- .../asset_depreciation_schedule.js | 6 +- .../asset_depreciation_schedule.json | 24 ++++++- .../asset_depreciation_schedule.py | 63 +++++++++++++++++-- ...sset_depreciation_schedules_from_assets.py | 8 ++- 4 files changed, 90 insertions(+), 11 deletions(-) diff --git a/erpnext/assets/doctype/asset_depreciation_schedule/asset_depreciation_schedule.js b/erpnext/assets/doctype/asset_depreciation_schedule/asset_depreciation_schedule.js index c28b2b3b6a3..3d2dff179aa 100644 --- a/erpnext/assets/doctype/asset_depreciation_schedule/asset_depreciation_schedule.js +++ b/erpnext/assets/doctype/asset_depreciation_schedule/asset_depreciation_schedule.js @@ -43,9 +43,9 @@ erpnext.asset.set_accumulated_depreciation = function(frm) { if(frm.doc.depreciation_method != "Manual") return; var accumulated_depreciation = flt(frm.doc.opening_accumulated_depreciation); - $.each(frm.doc.schedules || [], function(i, row) { + + $.each(frm.doc.depreciation_schedule || [], function(i, row) { accumulated_depreciation += flt(row.depreciation_amount); - frappe.model.set_value(row.doctype, row.name, - "accumulated_depreciation_amount", accumulated_depreciation); + frappe.model.set_value(row.doctype, row.name, "accumulated_depreciation_amount", accumulated_depreciation); }) }; diff --git a/erpnext/assets/doctype/asset_depreciation_schedule/asset_depreciation_schedule.json b/erpnext/assets/doctype/asset_depreciation_schedule/asset_depreciation_schedule.json index 898c4820791..d38508d0c42 100644 --- a/erpnext/assets/doctype/asset_depreciation_schedule/asset_depreciation_schedule.json +++ b/erpnext/assets/doctype/asset_depreciation_schedule/asset_depreciation_schedule.json @@ -10,7 +10,9 @@ "asset", "naming_series", "column_break_2", + "gross_purchase_amount", "opening_accumulated_depreciation", + "number_of_depreciations_booked", "finance_book", "finance_book_id", "depreciation_details_section", @@ -148,18 +150,36 @@ "read_only": 1 }, { - "depends_on": "opening_accumulated_depreciation", "fieldname": "opening_accumulated_depreciation", "fieldtype": "Currency", + "hidden": 1, "label": "Opening Accumulated Depreciation", "options": "Company:company:default_currency", + "print_hide": 1, + "read_only": 1 + }, + { + "fieldname": "gross_purchase_amount", + "fieldtype": "Currency", + "hidden": 1, + "label": "Gross Purchase Amount", + "options": "Company:company:default_currency", + "print_hide": 1, + "read_only": 1 + }, + { + "fieldname": "number_of_depreciations_booked", + "fieldtype": "Int", + "hidden": 1, + "label": "Number of Depreciations Booked", + "print_hide": 1, "read_only": 1 } ], "index_web_pages_for_search": 1, "is_submittable": 1, "links": [], - "modified": "2023-01-16 21:08:21.421260", + "modified": "2023-02-26 16:37:23.734806", "modified_by": "Administrator", "module": "Assets", "name": "Asset Depreciation Schedule", 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 6f026625441..b75fbcbeb3d 100644 --- a/erpnext/assets/doctype/asset_depreciation_schedule/asset_depreciation_schedule.py +++ b/erpnext/assets/doctype/asset_depreciation_schedule/asset_depreciation_schedule.py @@ -4,7 +4,15 @@ import frappe from frappe import _ from frappe.model.document import Document -from frappe.utils import add_days, add_months, cint, flt, get_last_day, is_last_day_of_the_month +from frappe.utils import ( + add_days, + add_months, + cint, + flt, + get_last_day, + getdate, + is_last_day_of_the_month, +) class AssetDepreciationSchedule(Document): @@ -83,15 +91,58 @@ class AssetDepreciationSchedule(Document): date_of_return=None, update_asset_finance_book_row=True, ): + have_asset_details_been_modified = self.have_asset_details_been_modified(asset_doc) + not_manual_depr_or_have_manual_depr_details_been_modified = ( + self.not_manual_depr_or_have_manual_depr_details_been_modified(row) + ) + self.set_draft_asset_depr_schedule_details(asset_doc, row) - self.make_depr_schedule(asset_doc, row, date_of_disposal, update_asset_finance_book_row) - self.set_accumulated_depreciation(row, date_of_disposal, date_of_return) + + if self.should_prepare_depreciation_schedule( + have_asset_details_been_modified, not_manual_depr_or_have_manual_depr_details_been_modified + ): + self.make_depr_schedule(asset_doc, row, date_of_disposal, update_asset_finance_book_row) + self.set_accumulated_depreciation(row, date_of_disposal, date_of_return) + + def have_asset_details_been_modified(self, asset_doc): + return ( + asset_doc.gross_purchase_amount != self.gross_purchase_amount + or asset_doc.opening_accumulated_depreciation != self.opening_accumulated_depreciation + or asset_doc.number_of_depreciations_booked != self.number_of_depreciations_booked + ) + + def not_manual_depr_or_have_manual_depr_details_been_modified(self, row): + return ( + self.depreciation_method != "Manual" + or row.total_number_of_depreciations != self.total_number_of_depreciations + or row.frequency_of_depreciation != self.frequency_of_depreciation + or getdate(row.depreciation_start_date) != self.get("depreciation_schedule")[0].schedule_date + or row.expected_value_after_useful_life != self.expected_value_after_useful_life + ) + + def should_prepare_depreciation_schedule( + self, have_asset_details_been_modified, not_manual_depr_or_have_manual_depr_details_been_modified + ): + if not self.get("depreciation_schedule"): + return True + + old_asset_depr_schedule_doc = self.get_doc_before_save() + + if self.docstatus != 0 and not old_asset_depr_schedule_doc: + return True + + if have_asset_details_been_modified or not_manual_depr_or_have_manual_depr_details_been_modified: + return True + + return False def set_draft_asset_depr_schedule_details(self, asset_doc, row): self.asset = asset_doc.name self.finance_book = row.finance_book self.finance_book_id = row.idx self.opening_accumulated_depreciation = asset_doc.opening_accumulated_depreciation + self.number_of_depreciations_booked = asset_doc.number_of_depreciations_booked + self.gross_purchase_amount = asset_doc.gross_purchase_amount self.depreciation_method = row.depreciation_method self.total_number_of_depreciations = row.total_number_of_depreciations self.frequency_of_depreciation = row.frequency_of_depreciation @@ -102,7 +153,7 @@ class AssetDepreciationSchedule(Document): def make_depr_schedule( self, asset_doc, row, date_of_disposal, update_asset_finance_book_row=True ): - if row.depreciation_method != "Manual" and not self.get("depreciation_schedule"): + if not self.get("depreciation_schedule"): self.depreciation_schedule = [] if not asset_doc.available_for_use_date: @@ -293,7 +344,9 @@ class AssetDepreciationSchedule(Document): ignore_booked_entry=False, ): straight_line_idx = [ - d.idx for d in self.get("depreciation_schedule") if d.depreciation_method == "Straight Line" + d.idx + for d in self.get("depreciation_schedule") + if d.depreciation_method == "Straight Line" or d.depreciation_method == "Manual" ] accumulated_depreciation = flt(self.opening_accumulated_depreciation) diff --git a/erpnext/patches/v15_0/create_asset_depreciation_schedules_from_assets.py b/erpnext/patches/v15_0/create_asset_depreciation_schedules_from_assets.py index 371ecbc8c13..5c46bf32807 100644 --- a/erpnext/patches/v15_0/create_asset_depreciation_schedules_from_assets.py +++ b/erpnext/patches/v15_0/create_asset_depreciation_schedules_from_assets.py @@ -27,7 +27,13 @@ def get_details_of_draft_or_submitted_depreciable_assets(): records = ( frappe.qb.from_(asset) - .select(asset.name, asset.opening_accumulated_depreciation, asset.docstatus) + .select( + asset.name, + asset.opening_accumulated_depreciation, + asset.gross_purchase_amount, + asset.number_of_depreciations_booked, + asset.docstatus, + ) .where(asset.calculate_depreciation == 1) .where(asset.docstatus < 2) ).run(as_dict=True) From 83f3e317e1ad29a1c8303a56070d601c3d102608 Mon Sep 17 00:00:00 2001 From: Brian Pond <19827963+brian-pond@users.noreply.github.com> Date: Sat, 25 Feb 2023 12:41:23 -0800 Subject: [PATCH 26/65] fix: Remove missing DocField in fetch_from --- .../maintenance_visit_purpose.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/erpnext/maintenance/doctype/maintenance_visit_purpose/maintenance_visit_purpose.json b/erpnext/maintenance/doctype/maintenance_visit_purpose/maintenance_visit_purpose.json index 158f143ae86..ba053555531 100644 --- a/erpnext/maintenance/doctype/maintenance_visit_purpose/maintenance_visit_purpose.json +++ b/erpnext/maintenance/doctype/maintenance_visit_purpose/maintenance_visit_purpose.json @@ -64,8 +64,6 @@ "fieldtype": "Section Break" }, { - "fetch_from": "prevdoc_detail_docname.sales_person", - "fetch_if_empty": 1, "fieldname": "service_person", "fieldtype": "Link", "in_list_view": 1, @@ -110,13 +108,15 @@ "idx": 1, "istable": 1, "links": [], - "modified": "2021-05-27 17:47:21.474282", + "modified": "2023-02-27 11:09:33.114458", "modified_by": "Administrator", "module": "Maintenance", "name": "Maintenance Visit Purpose", + "naming_rule": "Random", "owner": "Administrator", "permissions": [], "sort_field": "modified", "sort_order": "DESC", + "states": [], "track_changes": 1 } \ No newline at end of file From bbb6a62a7ddbc5337998652721160437f79301eb Mon Sep 17 00:00:00 2001 From: Patrick Eissler <77415730+PatrickDenis-stack@users.noreply.github.com> Date: Mon, 27 Feb 2023 07:19:22 +0100 Subject: [PATCH 27/65] chore: add german translations (#34167) * chore: add german translations * Apply suggestions from code review Co-authored-by: Raffael Meyer <14891507+barredterra@users.noreply.github.com> --------- Co-authored-by: Raffael Meyer <14891507+barredterra@users.noreply.github.com> --- erpnext/translations/de.csv | 2 ++ 1 file changed, 2 insertions(+) diff --git a/erpnext/translations/de.csv b/erpnext/translations/de.csv index 5a0a863a47e..bec3ce242b0 100644 --- a/erpnext/translations/de.csv +++ b/erpnext/translations/de.csv @@ -9916,3 +9916,5 @@ Cost and Freight,Kosten und Fracht, Delivered at Place,Geliefert benannter Ort, Delivered at Place Unloaded,Geliefert benannter Ort entladen, Delivered Duty Paid,Geliefert verzollt, +Discount Validity,Frist für den Rabatt, +Discount Validity Based On,Frist für den Rabatt berechnet sich nach, From c09a61f360bad2faa12ac1118665d71203061361 Mon Sep 17 00:00:00 2001 From: s-aga-r Date: Thu, 23 Feb 2023 12:26:19 +0530 Subject: [PATCH 28/65] fix: set `from_warehouse` and `to_warehouse` while mapping SE --- erpnext/stock/doctype/material_request/material_request.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/erpnext/stock/doctype/material_request/material_request.py b/erpnext/stock/doctype/material_request/material_request.py index 6426fe8015a..dcbc460262c 100644 --- a/erpnext/stock/doctype/material_request/material_request.py +++ b/erpnext/stock/doctype/material_request/material_request.py @@ -587,6 +587,9 @@ def make_stock_entry(source_name, target_doc=None): def set_missing_values(source, target): target.purpose = source.material_request_type + target.from_warehouse = source.set_from_warehouse + target.to_warehouse = source.set_warehouse + if source.job_card: target.purpose = "Material Transfer for Manufacture" @@ -722,6 +725,7 @@ def create_pick_list(source_name, target_doc=None): def make_in_transit_stock_entry(source_name, in_transit_warehouse): ste_doc = make_stock_entry(source_name) ste_doc.add_to_transit = 1 + ste_doc.to_warehouse = in_transit_warehouse for row in ste_doc.items: row.t_warehouse = in_transit_warehouse From 1de531e56e8820b19f3aa85cc194e7174dc4d678 Mon Sep 17 00:00:00 2001 From: Vishal Date: Tue, 14 Feb 2023 18:43:48 +0530 Subject: [PATCH 29/65] fix: multiple pos conversion issue resolved --- erpnext/accounts/doctype/pos_invoice/pos_invoice.py | 6 +++--- erpnext/selling/page/point_of_sale/pos_controller.js | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/erpnext/accounts/doctype/pos_invoice/pos_invoice.py b/erpnext/accounts/doctype/pos_invoice/pos_invoice.py index a1239d64a01..74db259787b 100644 --- a/erpnext/accounts/doctype/pos_invoice/pos_invoice.py +++ b/erpnext/accounts/doctype/pos_invoice/pos_invoice.py @@ -161,7 +161,7 @@ class POSInvoice(SalesInvoice): bold_item_name = frappe.bold(item.item_name) bold_extra_batch_qty_needed = frappe.bold( - abs(available_batch_qty - reserved_batch_qty - item.qty) + abs(available_batch_qty - reserved_batch_qty - item.qty * item.conversion_factor) ) bold_invalid_batch_no = frappe.bold(item.batch_no) @@ -172,7 +172,7 @@ class POSInvoice(SalesInvoice): ).format(item.idx, bold_invalid_batch_no, bold_item_name), title=_("Item Unavailable"), ) - elif (available_batch_qty - reserved_batch_qty - item.qty) < 0: + elif (available_batch_qty - reserved_batch_qty - item.qty * item.conversion_factor) < 0: frappe.throw( _( "Row #{}: Batch No. {} of item {} has less than required stock available, {} more required" @@ -246,7 +246,7 @@ class POSInvoice(SalesInvoice): ), title=_("Item Unavailable"), ) - elif is_stock_item and flt(available_stock) < flt(d.qty): + elif is_stock_item and flt(available_stock) < flt(d.qty * d.conversion_factor): frappe.throw( _( "Row #{}: Stock quantity not enough for Item Code: {} under warehouse {}. Available quantity {}." diff --git a/erpnext/selling/page/point_of_sale/pos_controller.js b/erpnext/selling/page/point_of_sale/pos_controller.js index c442774d0f7..5e70d6dc1da 100644 --- a/erpnext/selling/page/point_of_sale/pos_controller.js +++ b/erpnext/selling/page/point_of_sale/pos_controller.js @@ -522,7 +522,7 @@ erpnext.PointOfSale.Controller = class { const from_selector = field === 'qty' && value === "+1"; if (from_selector) - value = flt(item_row.qty) + flt(value); + value = flt(item_row.qty * item_row.conversion_factor) + flt(value); if (item_row_exists) { if (field === 'qty') From 3ebe7d861d45a6e0b83bcf5df275923bb882c5e0 Mon Sep 17 00:00:00 2001 From: Vishal Date: Thu, 23 Feb 2023 12:23:05 +0530 Subject: [PATCH 30/65] chore: minor changes added to code --- erpnext/accounts/doctype/pos_invoice/pos_invoice.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/erpnext/accounts/doctype/pos_invoice/pos_invoice.py b/erpnext/accounts/doctype/pos_invoice/pos_invoice.py index 74db259787b..11bc71bff82 100644 --- a/erpnext/accounts/doctype/pos_invoice/pos_invoice.py +++ b/erpnext/accounts/doctype/pos_invoice/pos_invoice.py @@ -161,7 +161,7 @@ class POSInvoice(SalesInvoice): bold_item_name = frappe.bold(item.item_name) bold_extra_batch_qty_needed = frappe.bold( - abs(available_batch_qty - reserved_batch_qty - item.qty * item.conversion_factor) + abs(available_batch_qty - reserved_batch_qty - item.stock_qty) ) bold_invalid_batch_no = frappe.bold(item.batch_no) @@ -172,7 +172,7 @@ class POSInvoice(SalesInvoice): ).format(item.idx, bold_invalid_batch_no, bold_item_name), title=_("Item Unavailable"), ) - elif (available_batch_qty - reserved_batch_qty - item.qty * item.conversion_factor) < 0: + elif (available_batch_qty - reserved_batch_qty - item.stock_qty) < 0: frappe.throw( _( "Row #{}: Batch No. {} of item {} has less than required stock available, {} more required" @@ -651,7 +651,7 @@ def get_bundle_availability(bundle_item_code, warehouse): item_pos_reserved_qty = get_pos_reserved_qty(item.item_code, warehouse) available_qty = item_bin_qty - item_pos_reserved_qty - max_available_bundles = available_qty / item.qty + max_available_bundles = available_qty / item.stock_qty if bundle_bin_qty > max_available_bundles and frappe.get_value( "Item", item.item_code, "is_stock_item" ): From a51bec02690120c55bd265bae23d34e113a652bd Mon Sep 17 00:00:00 2001 From: Vishal Date: Thu, 23 Feb 2023 12:26:42 +0530 Subject: [PATCH 31/65] chore: minor change --- erpnext/accounts/doctype/pos_invoice/pos_invoice.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/accounts/doctype/pos_invoice/pos_invoice.py b/erpnext/accounts/doctype/pos_invoice/pos_invoice.py index 11bc71bff82..b40649bbaec 100644 --- a/erpnext/accounts/doctype/pos_invoice/pos_invoice.py +++ b/erpnext/accounts/doctype/pos_invoice/pos_invoice.py @@ -246,7 +246,7 @@ class POSInvoice(SalesInvoice): ), title=_("Item Unavailable"), ) - elif is_stock_item and flt(available_stock) < flt(d.qty * d.conversion_factor): + elif is_stock_item and flt(available_stock) < flt(d.stock_qty): frappe.throw( _( "Row #{}: Stock quantity not enough for Item Code: {} under warehouse {}. Available quantity {}." From f18ae5856f0bd8594dd7141b2b85b7ab6a02497b Mon Sep 17 00:00:00 2001 From: Vishal Date: Tue, 28 Feb 2023 11:01:54 +0530 Subject: [PATCH 32/65] chore: minor changes in pos_controller --- erpnext/selling/page/point_of_sale/pos_controller.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/selling/page/point_of_sale/pos_controller.js b/erpnext/selling/page/point_of_sale/pos_controller.js index 5e70d6dc1da..46320e5538f 100644 --- a/erpnext/selling/page/point_of_sale/pos_controller.js +++ b/erpnext/selling/page/point_of_sale/pos_controller.js @@ -522,7 +522,7 @@ erpnext.PointOfSale.Controller = class { const from_selector = field === 'qty' && value === "+1"; if (from_selector) - value = flt(item_row.qty * item_row.conversion_factor) + flt(value); + value = flt(item_row.stock_qty) + flt(value); if (item_row_exists) { if (field === 'qty') From eab775ef3222c2948fa643436612816f01cb52bb Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Wed, 22 Feb 2023 12:30:09 +0530 Subject: [PATCH 33/65] feat: adjust purchase receipt valuation rate as per purchase invoice rate --- .../purchase_invoice/purchase_invoice.py | 35 +++++++++++++- .../buying_settings/buying_settings.json | 11 ++++- erpnext/controllers/buying_controller.py | 5 +- .../purchase_receipt/purchase_receipt.py | 47 ++++++++++++++++++- .../purchase_receipt_item.json | 11 ++++- 5 files changed, 103 insertions(+), 6 deletions(-) diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py index 21addab240b..28ed8b7da14 100644 --- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py +++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py @@ -528,6 +528,32 @@ class PurchaseInvoice(BuyingController): self.update_advance_tax_references() self.process_common_party_accounting() + self.adjust_incoming_rate_of_purchase_receipt() + + def adjust_incoming_rate_of_purchase_receipt(self): + if ( + not frappe.db.get_single_value( + "Buying Settings", "adjust_incoming_rate_based_on_purchase_invoice_rate" + ) + and self.is_subcontracted + ): + return + + purchase_receipts = [] + for item in self.items: + if item.purchase_receipt and item.purchase_receipt not in purchase_receipts: + purchase_receipts.append(item.purchase_receipt) + + for purchase_receipt in purchase_receipts: + doc = frappe.get_doc("Purchase Receipt", purchase_receipt) + doc.docstatus = 2 + doc.update_stock_ledger(allow_negative_stock=True, via_landed_cost_voucher=True) + doc.make_gl_entries_on_cancel() + + doc.docstatus = 1 + doc.update_stock_ledger(allow_negative_stock=True, via_landed_cost_voucher=True) + doc.make_gl_entries() + doc.repost_future_sle_and_gle() def make_gl_entries(self, gl_entries=None, from_repost=False): if not gl_entries: @@ -1423,6 +1449,7 @@ class PurchaseInvoice(BuyingController): "Tax Withheld Vouchers", ) self.update_advance_tax_references(cancel=1) + self.adjust_incoming_rate_of_purchase_receipt() def update_project(self): project_list = [] @@ -1485,11 +1512,17 @@ class PurchaseInvoice(BuyingController): if po_details: updated_pr += update_billed_amount_based_on_po(po_details, update_modified) + adjust_incoming_rate = frappe.db.get_single_value( + "Buying Settings", "adjust_incoming_rate_based_on_purchase_invoice_rate" + ) + for pr in set(updated_pr): from erpnext.stock.doctype.purchase_receipt.purchase_receipt import update_billing_percentage pr_doc = frappe.get_doc("Purchase Receipt", pr) - update_billing_percentage(pr_doc, update_modified=update_modified) + update_billing_percentage( + pr_doc, update_modified=update_modified, adjust_incoming_rate=adjust_incoming_rate + ) def get_pr_details_billed_amt(self): # Get billed amount based on purchase receipt item reference (pr_detail) in purchase invoice diff --git a/erpnext/buying/doctype/buying_settings/buying_settings.json b/erpnext/buying/doctype/buying_settings/buying_settings.json index 652dcf0d43c..7bbe89a063b 100644 --- a/erpnext/buying/doctype/buying_settings/buying_settings.json +++ b/erpnext/buying/doctype/buying_settings/buying_settings.json @@ -18,6 +18,7 @@ "pr_required", "column_break_12", "maintain_same_rate", + "adjust_incoming_rate_based_on_purchase_invoice_rate", "allow_multiple_items", "bill_for_rejected_quantity_in_purchase_invoice", "disable_last_purchase_rate", @@ -147,6 +148,14 @@ "fieldname": "show_pay_button", "fieldtype": "Check", "label": "Show Pay Button in Purchase Order Portal" + }, + { + "default": "0", + "depends_on": "eval: !doc.maintain_same_rate", + "description": "Users can enable the checkbox If they want to adjust the incoming rate (set using purchase receipt) based on the purchase invoice rate.", + "fieldname": "adjust_incoming_rate_based_on_purchase_invoice_rate", + "fieldtype": "Check", + "label": "Adjust Incoming Rate Based on Purchase Invoice Rate" } ], "icon": "fa fa-cog", @@ -154,7 +163,7 @@ "index_web_pages_for_search": 1, "issingle": 1, "links": [], - "modified": "2023-02-15 14:42:10.200679", + "modified": "2023-02-20 14:25:58.544143", "modified_by": "Administrator", "module": "Buying", "name": "Buying Settings", diff --git a/erpnext/controllers/buying_controller.py b/erpnext/controllers/buying_controller.py index 4f7d9ad92e8..603468d96f0 100644 --- a/erpnext/controllers/buying_controller.py +++ b/erpnext/controllers/buying_controller.py @@ -265,7 +265,10 @@ class BuyingController(SubcontractingController): ) / qty_in_stock_uom else: item.valuation_rate = ( - item.base_net_amount + item.item_tax_amount + flt(item.landed_cost_voucher_amount) + item.base_net_amount + + item.item_tax_amount + + flt(item.landed_cost_voucher_amount) + + flt(item.get("adjust_incoming_rate")) ) / qty_in_stock_uom else: item.valuation_rate = 0.0 diff --git a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py index c8a4bd3d276..0f3224cffb3 100644 --- a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py +++ b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py @@ -293,6 +293,7 @@ class PurchaseReceipt(BuyingController): get_purchase_document_details, ) + stock_rbnb = None if erpnext.is_perpetual_inventory_enabled(self.company): stock_rbnb = self.get_company_default("stock_received_but_not_billed") landed_cost_entries = get_item_account_wise_additional_cost(self.name) @@ -450,6 +451,21 @@ class PurchaseReceipt(BuyingController): item=d, ) + if d.adjust_incoming_rate and stock_rbnb: + account_currency = get_account_currency(stock_rbnb) + self.add_gl_entry( + gl_entries=gl_entries, + account=stock_rbnb, + cost_center=d.cost_center, + debit=0.0, + credit=flt(d.adjust_incoming_rate), + remarks=_("Adjustment based on Purchase Invoice rate"), + against_account=warehouse_account_name, + account_currency=account_currency, + project=d.project, + item=d, + ) + # sub-contracting warehouse if flt(d.rm_supp_cost) and warehouse_account.get(self.supplier_warehouse): self.add_gl_entry( @@ -470,6 +486,7 @@ class PurchaseReceipt(BuyingController): + flt(d.landed_cost_voucher_amount) + flt(d.rm_supp_cost) + flt(d.item_tax_amount) + + flt(d.adjust_incoming_rate) ) divisional_loss = flt( @@ -765,7 +782,7 @@ class PurchaseReceipt(BuyingController): updated_pr += update_billed_amount_based_on_po(po_details, update_modified) for pr in set(updated_pr): - pr_doc = self if (pr == self.name) else frappe.get_cached_doc("Purchase Receipt", pr) + pr_doc = self if (pr == self.name) else frappe.get_doc("Purchase Receipt", pr) update_billing_percentage(pr_doc, update_modified=update_modified) self.load_from_db() @@ -881,7 +898,7 @@ def get_billed_amount_against_po(po_items): return {d.po_detail: flt(d.billed_amt) for d in query} -def update_billing_percentage(pr_doc, update_modified=True): +def update_billing_percentage(pr_doc, update_modified=True, adjust_incoming_rate=False): # Reload as billed amount was set in db directly pr_doc.load_from_db() @@ -897,6 +914,12 @@ def update_billing_percentage(pr_doc, update_modified=True): total_amount += total_billable_amount total_billed_amount += flt(item.billed_amt) + if adjust_incoming_rate: + adjusted_amt = 0.0 + if item.billed_amt and item.amount: + adjusted_amt = flt(item.billed_amt) - flt(item.amount) + + item.db_set("adjust_incoming_rate", adjusted_amt, update_modified=False) percent_billed = round(100 * (total_billed_amount / (total_amount or 1)), 6) pr_doc.db_set("per_billed", percent_billed) @@ -906,6 +929,26 @@ def update_billing_percentage(pr_doc, update_modified=True): pr_doc.set_status(update=True) pr_doc.notify_update() + if adjust_incoming_rate: + adjust_incoming_rate_for_pr(pr_doc) + + +def adjust_incoming_rate_for_pr(doc): + doc.update_valuation_rate(reset_outgoing_rate=False) + + for item in doc.get("items"): + item.db_update() + + doc.docstatus = 2 + doc.update_stock_ledger(allow_negative_stock=True, via_landed_cost_voucher=True) + doc.make_gl_entries_on_cancel() + + # update stock & gl entries for submit state of PR + doc.docstatus = 1 + doc.update_stock_ledger(allow_negative_stock=True, via_landed_cost_voucher=True) + doc.make_gl_entries() + doc.repost_future_sle_and_gle() + def get_item_wise_returned_qty(pr_doc): items = [d.name for d in pr_doc.items] diff --git a/erpnext/stock/doctype/purchase_receipt_item/purchase_receipt_item.json b/erpnext/stock/doctype/purchase_receipt_item/purchase_receipt_item.json index 7a350b9e446..e12c5834619 100644 --- a/erpnext/stock/doctype/purchase_receipt_item/purchase_receipt_item.json +++ b/erpnext/stock/doctype/purchase_receipt_item/purchase_receipt_item.json @@ -69,6 +69,7 @@ "item_tax_amount", "rm_supp_cost", "landed_cost_voucher_amount", + "adjust_incoming_rate", "billed_amt", "warehouse_and_reference", "warehouse", @@ -1007,12 +1008,20 @@ "fieldtype": "Check", "label": "Has Item Scanned", "read_only": 1 + }, + { + "fieldname": "adjust_incoming_rate", + "fieldtype": "Currency", + "label": "Adjust Incoming Rate (Purchase Invoice)", + "no_copy": 1, + "print_hide": 1, + "read_only": 1 } ], "idx": 1, "istable": 1, "links": [], - "modified": "2023-01-18 15:48:58.114923", + "modified": "2023-02-28 12:05:59.732266", "modified_by": "Administrator", "module": "Stock", "name": "Purchase Receipt Item", From 8e865537170251e8a788a73034345f12b90017d1 Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Tue, 28 Feb 2023 14:39:05 +0530 Subject: [PATCH 34/65] test: added test cases --- .../purchase_invoice/purchase_invoice.py | 27 ------ .../purchase_invoice/test_purchase_invoice.py | 88 +++++++++++++++++++ .../buying_settings/buying_settings.py | 7 ++ 3 files changed, 95 insertions(+), 27 deletions(-) diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py index 28ed8b7da14..140eaafba1f 100644 --- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py +++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py @@ -528,32 +528,6 @@ class PurchaseInvoice(BuyingController): self.update_advance_tax_references() self.process_common_party_accounting() - self.adjust_incoming_rate_of_purchase_receipt() - - def adjust_incoming_rate_of_purchase_receipt(self): - if ( - not frappe.db.get_single_value( - "Buying Settings", "adjust_incoming_rate_based_on_purchase_invoice_rate" - ) - and self.is_subcontracted - ): - return - - purchase_receipts = [] - for item in self.items: - if item.purchase_receipt and item.purchase_receipt not in purchase_receipts: - purchase_receipts.append(item.purchase_receipt) - - for purchase_receipt in purchase_receipts: - doc = frappe.get_doc("Purchase Receipt", purchase_receipt) - doc.docstatus = 2 - doc.update_stock_ledger(allow_negative_stock=True, via_landed_cost_voucher=True) - doc.make_gl_entries_on_cancel() - - doc.docstatus = 1 - doc.update_stock_ledger(allow_negative_stock=True, via_landed_cost_voucher=True) - doc.make_gl_entries() - doc.repost_future_sle_and_gle() def make_gl_entries(self, gl_entries=None, from_repost=False): if not gl_entries: @@ -1449,7 +1423,6 @@ class PurchaseInvoice(BuyingController): "Tax Withheld Vouchers", ) self.update_advance_tax_references(cancel=1) - self.adjust_incoming_rate_of_purchase_receipt() def update_project(self): project_list = [] diff --git a/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py b/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py index f901257ccf6..d20eddfc96f 100644 --- a/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py +++ b/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py @@ -1523,6 +1523,94 @@ class TestPurchaseInvoice(unittest.TestCase, StockTestMixin): company.enable_provisional_accounting_for_non_stock_items = 0 company.save() + def test_adjust_incoming_rate(self): + frappe.db.set_single_value("Buying Settings", "maintain_same_rate", 0) + + frappe.db.set_single_value( + "Buying Settings", "adjust_incoming_rate_based_on_purchase_invoice_rate", 1 + ) + + # Increase the cost of the item + + pr = make_purchase_receipt(qty=1, rate=100) + + stock_value_difference = frappe.db.get_value( + "Stock Ledger Entry", + {"voucher_type": "Purchase Receipt", "voucher_no": pr.name}, + "stock_value_difference", + ) + self.assertEqual(stock_value_difference, 100) + + pi = create_purchase_invoice_from_receipt(pr.name) + for row in pi.items: + row.rate = 150 + + pi.save() + pi.submit() + + stock_value_difference = frappe.db.get_value( + "Stock Ledger Entry", + {"voucher_type": "Purchase Receipt", "voucher_no": pr.name}, + "stock_value_difference", + ) + self.assertEqual(stock_value_difference, 150) + + # Reduce the cost of the item + + pr = make_purchase_receipt(qty=1, rate=100) + + stock_value_difference = frappe.db.get_value( + "Stock Ledger Entry", + {"voucher_type": "Purchase Receipt", "voucher_no": pr.name}, + "stock_value_difference", + ) + self.assertEqual(stock_value_difference, 100) + + pi = create_purchase_invoice_from_receipt(pr.name) + for row in pi.items: + row.rate = 50 + + pi.save() + pi.submit() + + stock_value_difference = frappe.db.get_value( + "Stock Ledger Entry", + {"voucher_type": "Purchase Receipt", "voucher_no": pr.name}, + "stock_value_difference", + ) + self.assertEqual(stock_value_difference, 50) + + frappe.db.set_single_value( + "Buying Settings", "adjust_incoming_rate_based_on_purchase_invoice_rate", 0 + ) + + # Don't adjust incoming rate + + pr = make_purchase_receipt(qty=1, rate=100) + + stock_value_difference = frappe.db.get_value( + "Stock Ledger Entry", + {"voucher_type": "Purchase Receipt", "voucher_no": pr.name}, + "stock_value_difference", + ) + self.assertEqual(stock_value_difference, 100) + + pi = create_purchase_invoice_from_receipt(pr.name) + for row in pi.items: + row.rate = 50 + + pi.save() + pi.submit() + + stock_value_difference = frappe.db.get_value( + "Stock Ledger Entry", + {"voucher_type": "Purchase Receipt", "voucher_no": pr.name}, + "stock_value_difference", + ) + self.assertEqual(stock_value_difference, 100) + + frappe.db.set_single_value("Buying Settings", "maintain_same_rate", 1) + def test_item_less_defaults(self): pi = frappe.new_doc("Purchase Invoice") diff --git a/erpnext/buying/doctype/buying_settings/buying_settings.py b/erpnext/buying/doctype/buying_settings/buying_settings.py index be1ebdeb64e..d14ffc90ad7 100644 --- a/erpnext/buying/doctype/buying_settings/buying_settings.py +++ b/erpnext/buying/doctype/buying_settings/buying_settings.py @@ -21,3 +21,10 @@ class BuyingSettings(Document): self.get("supp_master_name") == "Naming Series", hide_name_field=False, ) + + def before_save(self): + self.check_maintain_same_rate() + + def check_maintain_same_rate(self): + if self.maintain_same_rate: + self.adjust_incoming_rate_based_on_purchase_invoice_rate = 0 From 58c027d4cc5895041446243ce9690f0948cee070 Mon Sep 17 00:00:00 2001 From: s-aga-r Date: Tue, 28 Feb 2023 15:14:34 +0530 Subject: [PATCH 35/65] chore: `Linters` --- erpnext/stock/get_item_details.py | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/erpnext/stock/get_item_details.py b/erpnext/stock/get_item_details.py index f017af6e915..489ec6ebecc 100644 --- a/erpnext/stock/get_item_details.py +++ b/erpnext/stock/get_item_details.py @@ -1144,7 +1144,7 @@ def get_serial_nos_by_fifo(args, sales_order=None): def get_conversion_factor(item_code, uom): variant_of = frappe.db.get_value("Item", item_code, "variant_of", cache=True) filters = {"parent": item_code, "uom": uom} - + if variant_of: filters["parent"] = ("in", (item_code, variant_of)) conversion_factor = frappe.db.get_value("UOM Conversion Detail", filters, "conversion_factor") @@ -1170,6 +1170,7 @@ def get_bin_details(item_code, warehouse, company=None, include_child_warehouses if warehouse: from frappe.query_builder.functions import Coalesce, Sum + from erpnext.stock.doctype.warehouse.warehouse import get_child_warehouses warehouses = get_child_warehouses(warehouse) if include_child_warehouses else [warehouse] @@ -1245,7 +1246,7 @@ def get_batch_qty_and_serial_no(batch_no, stock_qty, warehouse, item_code, has_s ) serial_no = get_serial_no(args) batch_qty_and_serial_no.update({"serial_no": serial_no}) - + return batch_qty_and_serial_no @@ -1405,10 +1406,7 @@ def get_valuation_rate(item_code, company, warehouse=None): valuation_rate = ( frappe.qb.from_(pi_item) .select((Sum(pi_item.base_net_amount) / Sum(pi_item.qty * pi_item.conversion_factor))) - .where( - (pi_item.docstatus == 1) - & (pi_item.item_code == item_code) - ) + .where((pi_item.docstatus == 1) & (pi_item.item_code == item_code)) ).run() if valuation_rate: From a8445da02aa48dfa2a44e426074c59f4f04e5356 Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Tue, 28 Feb 2023 15:47:45 +0530 Subject: [PATCH 36/65] fix: labels name --- .../accounts/doctype/purchase_invoice/purchase_invoice.py | 2 +- .../doctype/purchase_invoice/test_purchase_invoice.py | 4 ++-- .../buying/doctype/buying_settings/buying_settings.json | 8 ++++---- erpnext/buying/doctype/buying_settings/buying_settings.py | 2 +- erpnext/controllers/buying_controller.py | 2 +- .../stock/doctype/purchase_receipt/purchase_receipt.py | 8 ++++---- .../purchase_receipt_item/purchase_receipt_item.json | 8 ++++---- 7 files changed, 17 insertions(+), 17 deletions(-) diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py index 140eaafba1f..b79af71bef3 100644 --- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py +++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py @@ -1486,7 +1486,7 @@ class PurchaseInvoice(BuyingController): updated_pr += update_billed_amount_based_on_po(po_details, update_modified) adjust_incoming_rate = frappe.db.get_single_value( - "Buying Settings", "adjust_incoming_rate_based_on_purchase_invoice_rate" + "Buying Settings", "set_landed_cost_based_on_purchase_invoice_rate" ) for pr in set(updated_pr): diff --git a/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py b/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py index d20eddfc96f..a6d7df6971f 100644 --- a/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py +++ b/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py @@ -1527,7 +1527,7 @@ class TestPurchaseInvoice(unittest.TestCase, StockTestMixin): frappe.db.set_single_value("Buying Settings", "maintain_same_rate", 0) frappe.db.set_single_value( - "Buying Settings", "adjust_incoming_rate_based_on_purchase_invoice_rate", 1 + "Buying Settings", "set_landed_cost_based_on_purchase_invoice_rate", 1 ) # Increase the cost of the item @@ -1581,7 +1581,7 @@ class TestPurchaseInvoice(unittest.TestCase, StockTestMixin): self.assertEqual(stock_value_difference, 50) frappe.db.set_single_value( - "Buying Settings", "adjust_incoming_rate_based_on_purchase_invoice_rate", 0 + "Buying Settings", "set_landed_cost_based_on_purchase_invoice_rate", 0 ) # Don't adjust incoming rate diff --git a/erpnext/buying/doctype/buying_settings/buying_settings.json b/erpnext/buying/doctype/buying_settings/buying_settings.json index 7bbe89a063b..95857e4604d 100644 --- a/erpnext/buying/doctype/buying_settings/buying_settings.json +++ b/erpnext/buying/doctype/buying_settings/buying_settings.json @@ -18,7 +18,7 @@ "pr_required", "column_break_12", "maintain_same_rate", - "adjust_incoming_rate_based_on_purchase_invoice_rate", + "set_landed_cost_based_on_purchase_invoice_rate", "allow_multiple_items", "bill_for_rejected_quantity_in_purchase_invoice", "disable_last_purchase_rate", @@ -153,9 +153,9 @@ "default": "0", "depends_on": "eval: !doc.maintain_same_rate", "description": "Users can enable the checkbox If they want to adjust the incoming rate (set using purchase receipt) based on the purchase invoice rate.", - "fieldname": "adjust_incoming_rate_based_on_purchase_invoice_rate", + "fieldname": "set_landed_cost_based_on_purchase_invoice_rate", "fieldtype": "Check", - "label": "Adjust Incoming Rate Based on Purchase Invoice Rate" + "label": "Set Landed Cost Based on Purchase Invoice Rate" } ], "icon": "fa fa-cog", @@ -163,7 +163,7 @@ "index_web_pages_for_search": 1, "issingle": 1, "links": [], - "modified": "2023-02-20 14:25:58.544143", + "modified": "2023-02-28 15:41:32.686805", "modified_by": "Administrator", "module": "Buying", "name": "Buying Settings", diff --git a/erpnext/buying/doctype/buying_settings/buying_settings.py b/erpnext/buying/doctype/buying_settings/buying_settings.py index d14ffc90ad7..4680a889d3a 100644 --- a/erpnext/buying/doctype/buying_settings/buying_settings.py +++ b/erpnext/buying/doctype/buying_settings/buying_settings.py @@ -27,4 +27,4 @@ class BuyingSettings(Document): def check_maintain_same_rate(self): if self.maintain_same_rate: - self.adjust_incoming_rate_based_on_purchase_invoice_rate = 0 + self.set_landed_cost_based_on_purchase_invoice_rate = 0 diff --git a/erpnext/controllers/buying_controller.py b/erpnext/controllers/buying_controller.py index 603468d96f0..e15b61287eb 100644 --- a/erpnext/controllers/buying_controller.py +++ b/erpnext/controllers/buying_controller.py @@ -268,7 +268,7 @@ class BuyingController(SubcontractingController): item.base_net_amount + item.item_tax_amount + flt(item.landed_cost_voucher_amount) - + flt(item.get("adjust_incoming_rate")) + + flt(item.get("rate_difference_with_purchase_invoice")) ) / qty_in_stock_uom else: item.valuation_rate = 0.0 diff --git a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py index 0f3224cffb3..c1abd31bcc1 100644 --- a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py +++ b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py @@ -451,14 +451,14 @@ class PurchaseReceipt(BuyingController): item=d, ) - if d.adjust_incoming_rate and stock_rbnb: + if d.rate_difference_with_purchase_invoice and stock_rbnb: account_currency = get_account_currency(stock_rbnb) self.add_gl_entry( gl_entries=gl_entries, account=stock_rbnb, cost_center=d.cost_center, debit=0.0, - credit=flt(d.adjust_incoming_rate), + credit=flt(d.rate_difference_with_purchase_invoice), remarks=_("Adjustment based on Purchase Invoice rate"), against_account=warehouse_account_name, account_currency=account_currency, @@ -486,7 +486,7 @@ class PurchaseReceipt(BuyingController): + flt(d.landed_cost_voucher_amount) + flt(d.rm_supp_cost) + flt(d.item_tax_amount) - + flt(d.adjust_incoming_rate) + + flt(d.rate_difference_with_purchase_invoice) ) divisional_loss = flt( @@ -919,7 +919,7 @@ def update_billing_percentage(pr_doc, update_modified=True, adjust_incoming_rate if item.billed_amt and item.amount: adjusted_amt = flt(item.billed_amt) - flt(item.amount) - item.db_set("adjust_incoming_rate", adjusted_amt, update_modified=False) + item.db_set("rate_difference_with_purchase_invoice", adjusted_amt, update_modified=False) percent_billed = round(100 * (total_billed_amount / (total_amount or 1)), 6) pr_doc.db_set("per_billed", percent_billed) diff --git a/erpnext/stock/doctype/purchase_receipt_item/purchase_receipt_item.json b/erpnext/stock/doctype/purchase_receipt_item/purchase_receipt_item.json index e12c5834619..cd320fdfcd0 100644 --- a/erpnext/stock/doctype/purchase_receipt_item/purchase_receipt_item.json +++ b/erpnext/stock/doctype/purchase_receipt_item/purchase_receipt_item.json @@ -69,7 +69,7 @@ "item_tax_amount", "rm_supp_cost", "landed_cost_voucher_amount", - "adjust_incoming_rate", + "rate_difference_with_purchase_invoice", "billed_amt", "warehouse_and_reference", "warehouse", @@ -1010,9 +1010,9 @@ "read_only": 1 }, { - "fieldname": "adjust_incoming_rate", + "fieldname": "rate_difference_with_purchase_invoice", "fieldtype": "Currency", - "label": "Adjust Incoming Rate (Purchase Invoice)", + "label": "Rate Difference with Purchase Invoice", "no_copy": 1, "print_hide": 1, "read_only": 1 @@ -1021,7 +1021,7 @@ "idx": 1, "istable": 1, "links": [], - "modified": "2023-02-28 12:05:59.732266", + "modified": "2023-02-28 15:43:04.470104", "modified_by": "Administrator", "module": "Stock", "name": "Purchase Receipt Item", From 35c70f39fa28860b6c22dc3091eadb79c727a4de Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Tue, 28 Feb 2023 12:05:10 +0530 Subject: [PATCH 37/65] fix: pos return throwing amount greater than grand total --- erpnext/public/js/controllers/taxes_and_totals.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/erpnext/public/js/controllers/taxes_and_totals.js b/erpnext/public/js/controllers/taxes_and_totals.js index a87c3ec9514..974b937fa26 100644 --- a/erpnext/public/js/controllers/taxes_and_totals.js +++ b/erpnext/public/js/controllers/taxes_and_totals.js @@ -131,8 +131,8 @@ erpnext.taxes_and_totals = class TaxesAndTotals extends erpnext.payments { item.net_amount = item.amount = flt(item.rate * item.qty, precision("amount", item)); } else { - let qty = item.qty || 1; - qty = me.frm.doc.is_return ? -1 * qty : qty; + // allow for '0' qty on Credit/Debit notes + let qty = item.qty || -1 item.net_amount = item.amount = flt(item.rate * qty, precision("amount", item)); } From dfddc4efc31c7853b395c64dc978cce2c37aa0db Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Tue, 28 Feb 2023 17:53:51 +0530 Subject: [PATCH 38/65] fix: default date in Subcontracting reports --- .../subcontracted_item_to_be_received.js | 4 ++-- .../subcontracted_raw_materials_to_be_transferred.js | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/erpnext/buying/report/subcontracted_item_to_be_received/subcontracted_item_to_be_received.js b/erpnext/buying/report/subcontracted_item_to_be_received/subcontracted_item_to_be_received.js index 6304a0908d0..9db769d59bf 100644 --- a/erpnext/buying/report/subcontracted_item_to_be_received/subcontracted_item_to_be_received.js +++ b/erpnext/buying/report/subcontracted_item_to_be_received/subcontracted_item_to_be_received.js @@ -22,14 +22,14 @@ frappe.query_reports["Subcontracted Item To Be Received"] = { fieldname:"from_date", label: __("From Date"), fieldtype: "Date", - default: frappe.datetime.add_months(frappe.datetime.month_start(), -1), + default: frappe.datetime.add_months(frappe.datetime.get_today(), -1), reqd: 1 }, { fieldname:"to_date", label: __("To Date"), fieldtype: "Date", - default: frappe.datetime.add_days(frappe.datetime.month_start(),-1), + default: frappe.datetime.get_today(), reqd: 1 }, ] diff --git a/erpnext/buying/report/subcontracted_raw_materials_to_be_transferred/subcontracted_raw_materials_to_be_transferred.js b/erpnext/buying/report/subcontracted_raw_materials_to_be_transferred/subcontracted_raw_materials_to_be_transferred.js index b6739fe6632..7e5338f353b 100644 --- a/erpnext/buying/report/subcontracted_raw_materials_to_be_transferred/subcontracted_raw_materials_to_be_transferred.js +++ b/erpnext/buying/report/subcontracted_raw_materials_to_be_transferred/subcontracted_raw_materials_to_be_transferred.js @@ -22,14 +22,14 @@ frappe.query_reports["Subcontracted Raw Materials To Be Transferred"] = { fieldname:"from_date", label: __("From Date"), fieldtype: "Date", - default: frappe.datetime.add_months(frappe.datetime.month_start(), -1), + default: frappe.datetime.add_months(frappe.datetime.get_today(), -1), reqd: 1 }, { fieldname:"to_date", label: __("To Date"), fieldtype: "Date", - default: frappe.datetime.add_days(frappe.datetime.month_start(),-1), + default: frappe.datetime.get_today(), reqd: 1 }, ] From b38fe240900da4e3ac64c7bb03ef3aecbcd09f0d Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Wed, 1 Mar 2023 11:06:46 +0530 Subject: [PATCH 39/65] fix: consumed qty validation for subcontracting receipt --- .../subcontracting_receipt.py | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.py b/erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.py index f4fd4de169d..95dbc83bf80 100644 --- a/erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.py +++ b/erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.py @@ -191,14 +191,17 @@ class SubcontractingReceipt(SubcontractingController): def validate_available_qty_for_consumption(self): for item in self.get("supplied_items"): + precision = item.precision("consumed_qty") if ( - item.available_qty_for_consumption and item.available_qty_for_consumption < item.consumed_qty + item.available_qty_for_consumption + and flt(item.available_qty_for_consumption, precision) - flt(item.consumed_qty, precision) < 0 ): - frappe.throw( - _( - "Row {0}: Consumed Qty must be less than or equal to Available Qty For Consumption in Consumed Items Table." - ).format(item.idx) - ) + msg = f"""Row {item.idx}: Consumed Qty {flt(item.consumed_qty, precision)} + must be less than or equal to Available Qty For Consumption + {flt(item.available_qty_for_consumption, precision)} + in Consumed Items Table.""" + + frappe.throw(_(msg)) def validate_items_qty(self): for item in self.items: From 28dd1a25cb3c292883c709ae06d331a56d7c0f40 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Wed, 1 Mar 2023 15:00:24 +0530 Subject: [PATCH 40/65] chore: Make finance book read only --- erpnext/accounts/doctype/journal_entry/journal_entry.json | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/erpnext/accounts/doctype/journal_entry/journal_entry.json b/erpnext/accounts/doctype/journal_entry/journal_entry.json index 498fc7c295f..80e72226d3d 100644 --- a/erpnext/accounts/doctype/journal_entry/journal_entry.json +++ b/erpnext/accounts/doctype/journal_entry/journal_entry.json @@ -137,7 +137,8 @@ "fieldname": "finance_book", "fieldtype": "Link", "label": "Finance Book", - "options": "Finance Book" + "options": "Finance Book", + "read_only": 1 }, { "fieldname": "2_add_edit_gl_entries", @@ -538,7 +539,7 @@ "idx": 176, "is_submittable": 1, "links": [], - "modified": "2023-01-17 12:53:53.280620", + "modified": "2023-03-01 14:58:59.286591", "modified_by": "Administrator", "module": "Accounts", "name": "Journal Entry", From 46a722d51c7c4a6b210ce4553f76bf93958fcf21 Mon Sep 17 00:00:00 2001 From: Suraj Shetty Date: Wed, 1 Mar 2023 16:16:58 +0530 Subject: [PATCH 41/65] fix: Wrap unexpectedly long text in remark (cherry picked from commit ba66a6714c495f9d70dad79344342779c8011c6e) # Conflicts: # erpnext/accounts/report/general_ledger/general_ledger.html (cherry picked from commit b13bf1ebc517ccd9c5b6b079208ca3883e7418fd) --- .../report/general_ledger/general_ledger.html | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/erpnext/accounts/report/general_ledger/general_ledger.html b/erpnext/accounts/report/general_ledger/general_ledger.html index 475be92add5..e91c05dfb8c 100644 --- a/erpnext/accounts/report/general_ledger/general_ledger.html +++ b/erpnext/accounts/report/general_ledger/general_ledger.html @@ -38,8 +38,11 @@ {% if(data[i].posting_date) { %} {%= frappe.datetime.str_to_user(data[i].posting_date) %} {%= data[i].voucher_type %} -
{%= data[i].voucher_no %} - +
{%= data[i].voucher_no %} + + {% var longest_word = cstr(data[i].remarks).split(" ").reduce((longest, word) => word.length > longest.length ? word : longest, ""); %} + 45 %} class="overflow-wrap-anywhere" {% endif %}> + {% if(!(filters.party || filters.account)) { %} {%= data[i].party || data[i].account %}
@@ -49,11 +52,22 @@ {% if(data[i].bill_no) { %}
{%= __("Supplier Invoice No") %}: {%= data[i].bill_no %} {% } %} +<<<<<<< HEAD {%= format_currency(data[i].debit, filters.presentation_currency || data[i].account_currency) %} {%= format_currency(data[i].credit, filters.presentation_currency || data[i].account_currency) %} +======= +
+ + + {%= format_currency(data[i].debit, filters.presentation_currency) %} + + + {%= format_currency(data[i].credit, filters.presentation_currency) %} + +>>>>>>> ba66a6714c (fix: Wrap unexpectedly long text in remark) {% } else { %} From 6cd2ba5c663e6b9b89bf6938a158fe9a849f51cc Mon Sep 17 00:00:00 2001 From: Suraj Shetty <13928957+surajshetty3416@users.noreply.github.com> Date: Wed, 1 Mar 2023 16:25:25 +0530 Subject: [PATCH 42/65] fix: Resolve conflicts (cherry picked from commit f6469d83981386a7bb57b41178055eae5cda30bf) --- .../accounts/report/general_ledger/general_ledger.html | 8 -------- 1 file changed, 8 deletions(-) diff --git a/erpnext/accounts/report/general_ledger/general_ledger.html b/erpnext/accounts/report/general_ledger/general_ledger.html index e91c05dfb8c..2d5ca497654 100644 --- a/erpnext/accounts/report/general_ledger/general_ledger.html +++ b/erpnext/accounts/report/general_ledger/general_ledger.html @@ -52,13 +52,6 @@ {% if(data[i].bill_no) { %}
{%= __("Supplier Invoice No") %}: {%= data[i].bill_no %} {% } %} -<<<<<<< HEAD - - - {%= format_currency(data[i].debit, filters.presentation_currency || data[i].account_currency) %} - - {%= format_currency(data[i].credit, filters.presentation_currency || data[i].account_currency) %} -======= @@ -67,7 +60,6 @@ {%= format_currency(data[i].credit, filters.presentation_currency) %} ->>>>>>> ba66a6714c (fix: Wrap unexpectedly long text in remark) {% } else { %} From a9f0a11ce62d8337fcbc1ebdda3be1b818a5f8a2 Mon Sep 17 00:00:00 2001 From: s-aga-r Date: Thu, 2 Mar 2023 11:15:46 +0530 Subject: [PATCH 43/65] fix: `rejected_serial_no` not getting copied from PR to PR(Return) --- erpnext/controllers/sales_and_purchase_return.py | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/erpnext/controllers/sales_and_purchase_return.py b/erpnext/controllers/sales_and_purchase_return.py index 9fcb769bc8c..9219b20b320 100644 --- a/erpnext/controllers/sales_and_purchase_return.py +++ b/erpnext/controllers/sales_and_purchase_return.py @@ -400,6 +400,16 @@ def make_return_doc(doctype: str, source_name: str, target_doc=None): if serial_nos: target_doc.serial_no = "\n".join(serial_nos) + if source_doc.get("rejected_serial_no"): + returned_serial_nos = get_returned_serial_nos( + source_doc, source_parent, serial_no_field="rejected_serial_no" + ) + rejected_serial_nos = list( + set(get_serial_nos(source_doc.rejected_serial_no)) - set(returned_serial_nos) + ) + if rejected_serial_nos: + target_doc.rejected_serial_no = "\n".join(rejected_serial_nos) + if doctype in ["Purchase Receipt", "Subcontracting Receipt"]: returned_qty_map = get_returned_qty_map_for_row( source_parent.name, source_parent.supplier, source_doc.name, doctype @@ -610,7 +620,7 @@ def get_filters( return filters -def get_returned_serial_nos(child_doc, parent_doc): +def get_returned_serial_nos(child_doc, parent_doc, serial_no_field="serial_no"): from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos return_ref_field = frappe.scrub(child_doc.doctype) @@ -619,7 +629,7 @@ def get_returned_serial_nos(child_doc, parent_doc): serial_nos = [] - fields = ["`{0}`.`serial_no`".format("tab" + child_doc.doctype)] + fields = [f"`{'tab' + child_doc.doctype}`.`{serial_no_field}`"] filters = [ [parent_doc.doctype, "return_against", "=", parent_doc.name], @@ -629,6 +639,6 @@ def get_returned_serial_nos(child_doc, parent_doc): ] for row in frappe.get_all(parent_doc.doctype, fields=fields, filters=filters): - serial_nos.extend(get_serial_nos(row.serial_no)) + serial_nos.extend(get_serial_nos(row.get(serial_no_field))) return serial_nos From cb0b6de4b97009d0f7d1053f4e065416971a4832 Mon Sep 17 00:00:00 2001 From: s-aga-r Date: Thu, 2 Mar 2023 11:27:46 +0530 Subject: [PATCH 44/65] fix: `Serial No is mandatory` even if the `qty` is `0` --- erpnext/controllers/sales_and_purchase_return.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/controllers/sales_and_purchase_return.py b/erpnext/controllers/sales_and_purchase_return.py index 9219b20b320..15c270e58ad 100644 --- a/erpnext/controllers/sales_and_purchase_return.py +++ b/erpnext/controllers/sales_and_purchase_return.py @@ -131,7 +131,7 @@ def validate_returned_items(doc): ) elif ref.serial_no: - if not d.serial_no: + if d.qty and not d.serial_no: frappe.throw(_("Row # {0}: Serial No is mandatory").format(d.idx)) else: serial_nos = get_serial_nos(d.serial_no) From 330ae419be6767df1f3fe4c5f503d0496e527699 Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Thu, 2 Mar 2023 11:59:18 +0530 Subject: [PATCH 45/65] fix!: Parse float as per number format in quality inspection (#34259) fix: Parse float as per number format in quality inspection --- .../quality_inspection/quality_inspection.py | 26 +++++++++++--- .../test_quality_inspection.py | 36 ++++++++++++++++++- 2 files changed, 57 insertions(+), 5 deletions(-) diff --git a/erpnext/stock/doctype/quality_inspection/quality_inspection.py b/erpnext/stock/doctype/quality_inspection/quality_inspection.py index 2a9f091bd09..9673c81e553 100644 --- a/erpnext/stock/doctype/quality_inspection/quality_inspection.py +++ b/erpnext/stock/doctype/quality_inspection/quality_inspection.py @@ -6,7 +6,7 @@ import frappe from frappe import _ from frappe.model.document import Document from frappe.model.mapper import get_mapped_doc -from frappe.utils import cint, cstr, flt +from frappe.utils import cint, cstr, flt, get_number_format_info from erpnext.stock.doctype.quality_inspection_template.quality_inspection_template import ( get_template_details, @@ -156,7 +156,9 @@ class QualityInspection(Document): for i in range(1, 11): reading_value = reading.get("reading_" + str(i)) if reading_value is not None and reading_value.strip(): - result = flt(reading.get("min_value")) <= flt(reading_value) <= flt(reading.get("max_value")) + result = ( + flt(reading.get("min_value")) <= parse_float(reading_value) <= flt(reading.get("max_value")) + ) if not result: return False return True @@ -196,7 +198,7 @@ class QualityInspection(Document): # numeric readings for i in range(1, 11): field = "reading_" + str(i) - data[field] = flt(reading.get(field)) + data[field] = parse_float(reading.get(field)) data["mean"] = self.calculate_mean(reading) return data @@ -210,7 +212,7 @@ class QualityInspection(Document): for i in range(1, 11): reading_value = reading.get("reading_" + str(i)) if reading_value is not None and reading_value.strip(): - readings_list.append(flt(reading_value)) + readings_list.append(parse_float(reading_value)) actual_mean = mean(readings_list) if readings_list else 0 return actual_mean @@ -324,3 +326,19 @@ def make_quality_inspection(source_name, target_doc=None): ) return doc + + +def parse_float(num: str) -> float: + """Since reading_# fields are `Data` field they might contain number which + is representation in user's prefered number format instead of machine + readable format. This function converts them to machine readable format.""" + + number_format = frappe.db.get_default("number_format") or "#,###.##" + decimal_str, comma_str, _number_format_precision = get_number_format_info(number_format) + + if decimal_str == "," and comma_str == ".": + num = num.replace(",", "#$") + num = num.replace(".", ",") + num = num.replace("#$", ".") + + return flt(num) diff --git a/erpnext/stock/doctype/quality_inspection/test_quality_inspection.py b/erpnext/stock/doctype/quality_inspection/test_quality_inspection.py index 4f19643ad52..9d2e1396226 100644 --- a/erpnext/stock/doctype/quality_inspection/test_quality_inspection.py +++ b/erpnext/stock/doctype/quality_inspection/test_quality_inspection.py @@ -2,7 +2,7 @@ # See license.txt import frappe -from frappe.tests.utils import FrappeTestCase +from frappe.tests.utils import FrappeTestCase, change_settings from frappe.utils import nowdate from erpnext.controllers.stock_controller import ( @@ -216,6 +216,40 @@ class TestQualityInspection(FrappeTestCase): qa.save() self.assertEqual(qa.status, "Accepted") + @change_settings("System Settings", {"number_format": "#.###,##"}) + def test_diff_number_format(self): + self.assertEqual(frappe.db.get_default("number_format"), "#.###,##") # sanity check + + # Test QI based on acceptance values (Non formula) + dn = create_delivery_note(item_code="_Test Item with QA", do_not_submit=True) + readings = [ + { + "specification": "Iron Content", # numeric reading + "min_value": 60, + "max_value": 100, + "reading_1": "70,000", + }, + { + "specification": "Iron Content", # numeric reading + "min_value": 60, + "max_value": 100, + "reading_1": "1.100,00", + }, + ] + + qa = create_quality_inspection( + reference_type="Delivery Note", reference_name=dn.name, readings=readings, do_not_save=True + ) + + qa.save() + + # status must be auto set as per formula + self.assertEqual(qa.readings[0].status, "Accepted") + self.assertEqual(qa.readings[1].status, "Rejected") + + qa.delete() + dn.delete() + def create_quality_inspection(**args): args = frappe._dict(args) From 0e1b7760a8b688d419bf6f99e67d222ca430d7de Mon Sep 17 00:00:00 2001 From: s-aga-r Date: Sat, 4 Mar 2023 01:05:55 +0530 Subject: [PATCH 46/65] fix: `Inventory Dimension` for `Stock Reconciliation` --- .../stock/doctype/stock_reconciliation/stock_reconciliation.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py b/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py index 398b3c98e38..3dbdfd56e28 100644 --- a/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py +++ b/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py @@ -406,6 +406,8 @@ class StockReconciliation(StockController): } ) + self.update_inventory_dimensions(row, data) + if not row.batch_no: data.qty_after_transaction = flt(row.qty, row.precision("qty")) From e6a02719f7840498382d448bd19eb5435e7cc433 Mon Sep 17 00:00:00 2001 From: s-aga-r Date: Sat, 4 Mar 2023 15:26:10 +0530 Subject: [PATCH 47/65] fix: update inventory dimensions before returning sle --- .../doctype/stock_reconciliation/stock_reconciliation.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py b/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py index 3dbdfd56e28..a89981f82e9 100644 --- a/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py +++ b/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py @@ -397,6 +397,7 @@ class StockReconciliation(StockController): "voucher_type": self.doctype, "voucher_no": self.name, "voucher_detail_no": row.name, + "actual_qty": flt(row.current_qty), "company": self.company, "stock_uom": frappe.db.get_value("Item", row.item_code, "stock_uom"), "is_cancelled": 1 if self.docstatus == 2 else 0, @@ -406,8 +407,6 @@ class StockReconciliation(StockController): } ) - self.update_inventory_dimensions(row, data) - if not row.batch_no: data.qty_after_transaction = flt(row.qty, row.precision("qty")) @@ -425,6 +424,8 @@ class StockReconciliation(StockController): data.valuation_rate = flt(row.valuation_rate) data.stock_value_difference = -1 * flt(row.amount_difference) + self.update_inventory_dimensions(row, data) + return data def make_sle_on_cancel(self): From 70de444b7b868d9fbf89413684cedc85ca491d4f Mon Sep 17 00:00:00 2001 From: s-aga-r Date: Sat, 4 Mar 2023 17:06:07 +0530 Subject: [PATCH 48/65] fix: Stock Reconciliation `actual_qty` --- .../stock/doctype/stock_reconciliation/stock_reconciliation.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py b/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py index a89981f82e9..3f6a2c881b8 100644 --- a/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py +++ b/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py @@ -397,7 +397,7 @@ class StockReconciliation(StockController): "voucher_type": self.doctype, "voucher_no": self.name, "voucher_detail_no": row.name, - "actual_qty": flt(row.current_qty), + "actual_qty": 0, "company": self.company, "stock_uom": frappe.db.get_value("Item", row.item_code, "stock_uom"), "is_cancelled": 1 if self.docstatus == 2 else 0, From 502a37a8641ed82c2936bfed1544b5178e1a3355 Mon Sep 17 00:00:00 2001 From: barredterra <14891507+barredterra@users.noreply.github.com> Date: Sat, 4 Mar 2023 19:31:40 +0100 Subject: [PATCH 49/65] refactor: use renamed timezone utils https://github.com/frappe/frappe/pull/20253 --- .../service_level_agreement/service_level_agreement.py | 4 ++-- erpnext/utilities/doctype/video/video.py | 3 ++- erpnext/www/book_appointment/index.py | 5 +++-- 3 files changed, 7 insertions(+), 5 deletions(-) diff --git a/erpnext/support/doctype/service_level_agreement/service_level_agreement.py b/erpnext/support/doctype/service_level_agreement/service_level_agreement.py index 7f4e9efa948..2a078c4395b 100644 --- a/erpnext/support/doctype/service_level_agreement/service_level_agreement.py +++ b/erpnext/support/doctype/service_level_agreement/service_level_agreement.py @@ -13,8 +13,8 @@ from frappe.utils import ( get_datetime, get_datetime_str, get_link_to_form, + get_system_timezone, get_time, - get_time_zone, get_weekdays, getdate, nowdate, @@ -981,7 +981,7 @@ def convert_utc_to_user_timezone(utc_timestamp, user): def get_tz(user): - return frappe.db.get_value("User", user, "time_zone") or get_time_zone() + return frappe.db.get_value("User", user, "time_zone") or get_system_timezone() @frappe.whitelist() diff --git a/erpnext/utilities/doctype/video/video.py b/erpnext/utilities/doctype/video/video.py index 13b7877b21d..62033a5e18e 100644 --- a/erpnext/utilities/doctype/video/video.py +++ b/erpnext/utilities/doctype/video/video.py @@ -10,6 +10,7 @@ import pytz from frappe import _ from frappe.model.document import Document from frappe.utils import cint +from frappe.utils.data import get_system_timezone from pyyoutube import Api @@ -64,7 +65,7 @@ def update_youtube_data(): frequency = get_frequency(frequency) time = datetime.now() - timezone = pytz.timezone(frappe.utils.get_time_zone()) + timezone = pytz.timezone(get_system_timezone()) site_time = time.astimezone(timezone) if frequency == 30: diff --git a/erpnext/www/book_appointment/index.py b/erpnext/www/book_appointment/index.py index dfca9465ed1..f50c207ab98 100644 --- a/erpnext/www/book_appointment/index.py +++ b/erpnext/www/book_appointment/index.py @@ -4,6 +4,7 @@ import json import frappe import pytz from frappe import _ +from frappe.utils.data import get_system_timezone WEEKDAYS = ["Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday"] @@ -125,7 +126,7 @@ def filter_timeslots(date, timeslots): def convert_to_guest_timezone(guest_tz, datetimeobject): guest_tz = pytz.timezone(guest_tz) - local_timezone = pytz.timezone(frappe.utils.get_time_zone()) + local_timezone = pytz.timezone(get_system_timezone()) datetimeobject = local_timezone.localize(datetimeobject) datetimeobject = datetimeobject.astimezone(guest_tz) return datetimeobject @@ -134,7 +135,7 @@ def convert_to_guest_timezone(guest_tz, datetimeobject): def convert_to_system_timezone(guest_tz, datetimeobject): guest_tz = pytz.timezone(guest_tz) datetimeobject = guest_tz.localize(datetimeobject) - system_tz = pytz.timezone(frappe.utils.get_time_zone()) + system_tz = pytz.timezone(get_system_timezone()) datetimeobject = datetimeobject.astimezone(system_tz) return datetimeobject From 2f157fa5d3e08c642847042282512536e9a20ca5 Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Mon, 6 Mar 2023 12:02:35 +0530 Subject: [PATCH 50/65] fix: BOM Update log not completed --- erpnext/hooks.py | 2 +- .../manufacturing/doctype/bom_update_log/bom_update_log.py | 5 ++++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/erpnext/hooks.py b/erpnext/hooks.py index fd19d2585cc..579c794d45b 100644 --- a/erpnext/hooks.py +++ b/erpnext/hooks.py @@ -362,7 +362,7 @@ auto_cancel_exempted_doctypes = [ scheduler_events = { "cron": { - "0/5 * * * *": [ + "0/15 * * * *": [ "erpnext.manufacturing.doctype.bom_update_log.bom_update_log.resume_bom_cost_update_jobs", ], "0/30 * * * *": [ diff --git a/erpnext/manufacturing/doctype/bom_update_log/bom_update_log.py b/erpnext/manufacturing/doctype/bom_update_log/bom_update_log.py index c3f52d45833..51f7b24e745 100644 --- a/erpnext/manufacturing/doctype/bom_update_log/bom_update_log.py +++ b/erpnext/manufacturing/doctype/bom_update_log/bom_update_log.py @@ -212,7 +212,7 @@ def resume_bom_cost_update_jobs(): ["name", "boms_updated", "status"], ) incomplete_level = any(row.get("status") == "Pending" for row in bom_batches) - if not bom_batches or incomplete_level: + if not bom_batches or not incomplete_level: continue # Prep parent BOMs & updated processed BOMs for next level @@ -252,6 +252,9 @@ def get_processed_current_boms( current_boms = [] for row in bom_batches: + if not row.boms_updated: + continue + boms_updated = json.loads(row.boms_updated) current_boms.extend(boms_updated) boms_updated_dict = {bom: True for bom in boms_updated} From de18f98c5c3d0ee5cc9d3df5b389917670514e64 Mon Sep 17 00:00:00 2001 From: s-aga-r Date: Sun, 5 Mar 2023 21:57:02 +0530 Subject: [PATCH 51/65] perf: Stock Entry (Material Transfer) --- .../doctype/job_card/job_card.py | 63 ++++++++++++------- 1 file changed, 42 insertions(+), 21 deletions(-) diff --git a/erpnext/manufacturing/doctype/job_card/job_card.py b/erpnext/manufacturing/doctype/job_card/job_card.py index 3133628cbf2..e82f37977cd 100644 --- a/erpnext/manufacturing/doctype/job_card/job_card.py +++ b/erpnext/manufacturing/doctype/job_card/job_card.py @@ -561,7 +561,34 @@ class JobCard(Document): ) def set_transferred_qty_in_job_card_item(self, ste_doc): - from frappe.query_builder.functions import Sum + def _get_job_card_items_transferred_qty(ste_doc): + from frappe.query_builder.functions import Sum + + job_card_items_transferred_qty = {} + job_card_items = [ + x.get("job_card_item") for x in ste_doc.get("items") if x.get("job_card_item") + ] + + if job_card_items: + se = frappe.qb.DocType("Stock Entry") + sed = frappe.qb.DocType("Stock Entry Detail") + + query = ( + frappe.qb.from_(sed) + .join(se) + .on(sed.parent == se.name) + .select(sed.job_card_item, Sum(sed.qty)) + .where( + (sed.job_card_item.isin(job_card_items)) + & (se.docstatus == 1) + & (se.purpose == "Material Transfer for Manufacture") + ) + .groupby(sed.job_card_item) + ) + + job_card_items_transferred_qty = frappe._dict(query.run(as_list=True)) + + return job_card_items_transferred_qty def _validate_over_transfer(row, transferred_qty): "Block over transfer of items if not allowed in settings." @@ -578,29 +605,23 @@ class JobCard(Document): exc=JobCardOverTransferError, ) - for row in ste_doc.items: - if not row.job_card_item: - continue - - sed = frappe.qb.DocType("Stock Entry Detail") - se = frappe.qb.DocType("Stock Entry") - transferred_qty = ( - frappe.qb.from_(sed) - .join(se) - .on(sed.parent == se.name) - .select(Sum(sed.qty)) - .where( - (sed.job_card_item == row.job_card_item) - & (se.docstatus == 1) - & (se.purpose == "Material Transfer for Manufacture") - ) - ).run()[0][0] + job_card_items_transferred_qty = _get_job_card_items_transferred_qty(ste_doc) + if job_card_items_transferred_qty: allow_excess = frappe.db.get_single_value("Manufacturing Settings", "job_card_excess_transfer") - if not allow_excess: - _validate_over_transfer(row, transferred_qty) - frappe.db.set_value("Job Card Item", row.job_card_item, "transferred_qty", flt(transferred_qty)) + for row in ste_doc.items: + if not row.job_card_item: + continue + + transferred_qty = flt(job_card_items_transferred_qty.get(row.job_card_item)) + + if not allow_excess: + _validate_over_transfer(row, transferred_qty) + + frappe.db.set_value( + "Job Card Item", row.job_card_item, "transferred_qty", flt(transferred_qty) + ) def set_transferred_qty(self, update_status=False): "Set total FG Qty in Job Card for which RM was transferred." From 8ad9e99cea5e3f235d0b3ead812d2a3a11d40081 Mon Sep 17 00:00:00 2001 From: s-aga-r Date: Tue, 7 Mar 2023 01:11:23 +0530 Subject: [PATCH 52/65] perf: `update_completed_qty()` in `material_request.py` --- .../material_request/material_request.py | 51 ++++++++++++------- 1 file changed, 34 insertions(+), 17 deletions(-) diff --git a/erpnext/stock/doctype/material_request/material_request.py b/erpnext/stock/doctype/material_request/material_request.py index dcbc460262c..8aeb7511f41 100644 --- a/erpnext/stock/doctype/material_request/material_request.py +++ b/erpnext/stock/doctype/material_request/material_request.py @@ -10,6 +10,7 @@ import json import frappe from frappe import _, msgprint from frappe.model.mapper import get_mapped_doc +from frappe.query_builder.functions import Sum from frappe.utils import cint, cstr, flt, get_link_to_form, getdate, new_line_sep, nowdate from erpnext.buying.utils import check_on_hold_or_closed_status, validate_for_items @@ -180,6 +181,34 @@ class MaterialRequest(BuyingController): self.update_requested_qty() self.update_requested_qty_in_production_plan() + def get_mr_items_ordered_qty(self, mr_items): + mr_items_ordered_qty = {} + mr_items = [d.name for d in self.get("items") if d.name in mr_items] + + doctype = qty_field = None + if self.material_request_type in ("Material Issue", "Material Transfer", "Customer Provided"): + doctype = frappe.qb.DocType("Stock Entry Detail") + qty_field = doctype.transfer_qty + elif self.material_request_type == "Manufacture": + doctype = frappe.qb.DocType("Work Order") + qty_field = doctype.qty + + if doctype and qty_field: + query = ( + frappe.qb.from_(doctype) + .select(doctype.material_request_item, Sum(qty_field)) + .where( + (doctype.material_request == self.name) + & (doctype.material_request_item.isin(mr_items)) + & (doctype.docstatus == 1) + ) + .groupby(doctype.material_request_item) + ) + + mr_items_ordered_qty = frappe._dict(query.run()) + + return mr_items_ordered_qty + def update_completed_qty(self, mr_items=None, update_modified=True): if self.material_request_type == "Purchase": return @@ -187,18 +216,13 @@ class MaterialRequest(BuyingController): if not mr_items: mr_items = [d.name for d in self.get("items")] + mr_items_ordered_qty = self.get_mr_items_ordered_qty(mr_items) + mr_qty_allowance = frappe.db.get_single_value("Stock Settings", "mr_qty_allowance") + for d in self.get("items"): if d.name in mr_items: if self.material_request_type in ("Material Issue", "Material Transfer", "Customer Provided"): - d.ordered_qty = flt( - frappe.db.sql( - """select sum(transfer_qty) - from `tabStock Entry Detail` where material_request = %s - and material_request_item = %s and docstatus = 1""", - (self.name, d.name), - )[0][0] - ) - mr_qty_allowance = frappe.db.get_single_value("Stock Settings", "mr_qty_allowance") + d.ordered_qty = flt(mr_items_ordered_qty.get(d.name)) if mr_qty_allowance: allowed_qty = d.qty + (d.qty * (mr_qty_allowance / 100)) @@ -217,14 +241,7 @@ class MaterialRequest(BuyingController): ) elif self.material_request_type == "Manufacture": - d.ordered_qty = flt( - frappe.db.sql( - """select sum(qty) - from `tabWork Order` where material_request = %s - and material_request_item = %s and docstatus = 1""", - (self.name, d.name), - )[0][0] - ) + d.ordered_qty = flt(mr_items_ordered_qty.get(d.name)) frappe.db.set_value(d.doctype, d.name, "ordered_qty", d.ordered_qty) From ea8e23384d3029569691e266700e3ebcdff6e17b Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Tue, 7 Mar 2023 11:40:29 +0530 Subject: [PATCH 53/65] fix: Payment Request against sales order with disabled rounded total (#34281) * fix: Payment Request against sales order with disabled rounded total * chore: Do not consider advance amount --- erpnext/accounts/doctype/payment_request/payment_request.py | 6 +----- .../doctype/payment_request/test_payment_request.py | 5 ++++- 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/erpnext/accounts/doctype/payment_request/payment_request.py b/erpnext/accounts/doctype/payment_request/payment_request.py index 2f43914c453..7005c17362b 100644 --- a/erpnext/accounts/doctype/payment_request/payment_request.py +++ b/erpnext/accounts/doctype/payment_request/payment_request.py @@ -495,26 +495,22 @@ def get_amount(ref_doc, payment_account=None): """get amount based on doctype""" dt = ref_doc.doctype if dt in ["Sales Order", "Purchase Order"]: - grand_total = flt(ref_doc.rounded_total) - flt(ref_doc.advance_paid) - + grand_total = flt(ref_doc.rounded_total) or flt(ref_doc.grand_total) elif dt in ["Sales Invoice", "Purchase Invoice"]: if ref_doc.party_account_currency == ref_doc.currency: grand_total = flt(ref_doc.outstanding_amount) else: grand_total = flt(ref_doc.outstanding_amount) / ref_doc.conversion_rate - elif dt == "POS Invoice": for pay in ref_doc.payments: if pay.type == "Phone" and pay.account == payment_account: grand_total = pay.amount break - elif dt == "Fees": grand_total = ref_doc.outstanding_amount if grand_total > 0: return grand_total - else: frappe.throw(_("Payment Entry is already created")) diff --git a/erpnext/accounts/doctype/payment_request/test_payment_request.py b/erpnext/accounts/doctype/payment_request/test_payment_request.py index 477c726940c..4279aa4f85c 100644 --- a/erpnext/accounts/doctype/payment_request/test_payment_request.py +++ b/erpnext/accounts/doctype/payment_request/test_payment_request.py @@ -45,7 +45,10 @@ class TestPaymentRequest(unittest.TestCase): frappe.get_doc(method).insert(ignore_permissions=True) def test_payment_request_linkings(self): - so_inr = make_sales_order(currency="INR") + so_inr = make_sales_order(currency="INR", do_not_save=True) + so_inr.disable_rounded_total = 1 + so_inr.save() + pr = make_payment_request( dt="Sales Order", dn=so_inr.name, From 7d0199d743c7861e883cadd582c036cc8d9b0a62 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Tue, 7 Mar 2023 11:41:26 +0530 Subject: [PATCH 54/65] fix: Default sales team not getting set (#34284) --- erpnext/controllers/selling_controller.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/erpnext/controllers/selling_controller.py b/erpnext/controllers/selling_controller.py index 8b4d28bc7dd..e1f5062af03 100644 --- a/erpnext/controllers/selling_controller.py +++ b/erpnext/controllers/selling_controller.py @@ -84,6 +84,9 @@ class SellingController(StockController): ) if not self.meta.get_field("sales_team"): party_details.pop("sales_team") + else: + self.set("sales_team", party_details.get("sales_team")) + self.update_if_missing(party_details) elif lead: From 10632d75b049616525fb2810c94db8a7a53759b0 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Tue, 7 Mar 2023 11:44:09 +0530 Subject: [PATCH 55/65] fix: Do not calculate commission post submit (#34267) * fix: Do not calculate commision post submit * chore: Update condition to match server side logic Co-authored-by: Raffael Meyer <14891507+barredterra@users.noreply.github.com> --------- Co-authored-by: Raffael Meyer <14891507+barredterra@users.noreply.github.com> --- erpnext/controllers/selling_controller.py | 2 +- erpnext/selling/sales_common.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/erpnext/controllers/selling_controller.py b/erpnext/controllers/selling_controller.py index e1f5062af03..3ea0216bb10 100644 --- a/erpnext/controllers/selling_controller.py +++ b/erpnext/controllers/selling_controller.py @@ -139,7 +139,7 @@ class SellingController(StockController): self.in_words = money_in_words(amount, self.currency) def calculate_commission(self): - if not self.meta.get_field("commission_rate"): + if not self.meta.get_field("commission_rate") or self.docstatus.is_submitted(): return self.round_floats_in(self, ("amount_eligible_for_commission", "commission_rate")) diff --git a/erpnext/selling/sales_common.js b/erpnext/selling/sales_common.js index 5ce6e9c1460..f1df3a11de4 100644 --- a/erpnext/selling/sales_common.js +++ b/erpnext/selling/sales_common.js @@ -253,7 +253,7 @@ erpnext.selling.SellingController = class SellingController extends erpnext.Tran } calculate_commission() { - if(!this.frm.fields_dict.commission_rate) return; + if(!this.frm.fields_dict.commission_rate || this.frm.doc.docstatus === 1) return; if(this.frm.doc.commission_rate > 100) { this.frm.set_value("commission_rate", 100); From 2feb27e399632c858a7de144ae8873dc92e6af81 Mon Sep 17 00:00:00 2001 From: Marica Date: Tue, 7 Mar 2023 15:45:18 +0530 Subject: [PATCH 56/65] fix(minor): Dirty the form after clicking on Get advances button in Invoices (#34323) fix(minor): Dirty form after clicking on Get advances button --- erpnext/public/js/controllers/transaction.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/erpnext/public/js/controllers/transaction.js b/erpnext/public/js/controllers/transaction.js index 2d2945473b9..8d69ea0c994 100644 --- a/erpnext/public/js/controllers/transaction.js +++ b/erpnext/public/js/controllers/transaction.js @@ -1884,11 +1884,13 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe get_advances() { if(!this.frm.is_return) { + var me = this; return this.frm.call({ method: "set_advances", doc: this.frm.doc, callback: function(r, rt) { refresh_field("advances"); + me.frm.dirty(); } }) } From 6de826b8c467bffdf2a3c20805dc334bf60925d0 Mon Sep 17 00:00:00 2001 From: Devin Slauenwhite Date: Tue, 7 Mar 2023 06:46:15 -0500 Subject: [PATCH 57/65] fix: exchange rate revaluation errors (#33947) * fix: set new balance for non-positive balances * fix: don't add debit: 0, credit: 0 entries to journal entry. * fix: add journal entry difference to unbooked gain/loss of exchange. * chore: linter * chore: remove invlaid TODO. [skip-ci] --- .../exchange_rate_revaluation.py | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/erpnext/accounts/doctype/exchange_rate_revaluation/exchange_rate_revaluation.py b/erpnext/accounts/doctype/exchange_rate_revaluation/exchange_rate_revaluation.py index d67d59b5d45..a4f6a74a5ab 100644 --- a/erpnext/accounts/doctype/exchange_rate_revaluation/exchange_rate_revaluation.py +++ b/erpnext/accounts/doctype/exchange_rate_revaluation/exchange_rate_revaluation.py @@ -211,8 +211,7 @@ class ExchangeRateRevaluation(Document): # Handle Accounts with '0' balance in Account/Base Currency for d in [x for x in account_details if x.zero_balance]: - # TODO: Set new balance in Base/Account currency - if d.balance > 0: + if d.balance != 0: current_exchange_rate = new_exchange_rate = 0 new_balance_in_account_currency = 0 # this will be '0' @@ -399,6 +398,9 @@ class ExchangeRateRevaluation(Document): journal_entry_accounts = [] for d in accounts: + if not flt(d.get("balance_in_account_currency"), d.precision("balance_in_account_currency")): + continue + dr_or_cr = ( "debit_in_account_currency" if d.get("balance_in_account_currency") > 0 @@ -448,7 +450,13 @@ class ExchangeRateRevaluation(Document): } ) - journal_entry_accounts.append( + journal_entry.set("accounts", journal_entry_accounts) + journal_entry.set_amounts_in_company_currency() + journal_entry.set_total_debit_credit() + + self.gain_loss_unbooked += journal_entry.difference - self.gain_loss_unbooked + journal_entry.append( + "accounts", { "account": unrealized_exchange_gain_loss_account, "balance": get_balance_on(unrealized_exchange_gain_loss_account), @@ -460,10 +468,9 @@ class ExchangeRateRevaluation(Document): "exchange_rate": 1, "reference_type": "Exchange Rate Revaluation", "reference_name": self.name, - } + }, ) - journal_entry.set("accounts", journal_entry_accounts) journal_entry.set_amounts_in_company_currency() journal_entry.set_total_debit_credit() journal_entry.save() From 71de72bdd06301be0d18a19bbdee32d5d8f18f7c Mon Sep 17 00:00:00 2001 From: Solufyin <34390782+Solufyin@users.noreply.github.com> Date: Tue, 7 Mar 2023 17:44:31 +0530 Subject: [PATCH 58/65] fix: Set contact filter link in Opportunity (#34325) Co-authored-by: Nihantra C. Patel --- erpnext/crm/doctype/opportunity/opportunity.js | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/erpnext/crm/doctype/opportunity/opportunity.js b/erpnext/crm/doctype/opportunity/opportunity.js index 1f76a1ae2eb..b2617955a36 100644 --- a/erpnext/crm/doctype/opportunity/opportunity.js +++ b/erpnext/crm/doctype/opportunity/opportunity.js @@ -19,10 +19,6 @@ frappe.ui.form.on("Opportunity", { } } }); - - if (frm.doc.opportunity_from && frm.doc.party_name){ - frm.trigger('set_contact_link'); - } }, validate: function(frm) { @@ -130,6 +126,10 @@ frappe.ui.form.on("Opportunity", { } else { frappe.contacts.clear_address_and_contact(frm); } + + if (frm.doc.opportunity_from && frm.doc.party_name) { + frm.trigger('set_contact_link'); + } }, set_contact_link: function(frm) { @@ -137,6 +137,8 @@ frappe.ui.form.on("Opportunity", { frappe.dynamic_link = {doc: frm.doc, fieldname: 'party_name', doctype: 'Customer'} } else if(frm.doc.opportunity_from == "Lead" && frm.doc.party_name) { frappe.dynamic_link = {doc: frm.doc, fieldname: 'party_name', doctype: 'Lead'} + } else if (frm.doc.opportunity_from == "Prospect" && frm.doc.party_name) { + frappe.dynamic_link = {doc: frm.doc, fieldname: 'party_name', doctype: 'Prospect'} } }, From a65b80911b0b4f29b3b0e103bf6aae92c6578d8f Mon Sep 17 00:00:00 2001 From: s-aga-r Date: Wed, 8 Mar 2023 12:59:50 +0530 Subject: [PATCH 59/65] fix: `BOM Stock Report` --- .../bom_stock_report/bom_stock_report.py | 87 +++++++++---------- 1 file changed, 43 insertions(+), 44 deletions(-) diff --git a/erpnext/manufacturing/report/bom_stock_report/bom_stock_report.py b/erpnext/manufacturing/report/bom_stock_report/bom_stock_report.py index cdf1541f888..3573a3a93d8 100644 --- a/erpnext/manufacturing/report/bom_stock_report/bom_stock_report.py +++ b/erpnext/manufacturing/report/bom_stock_report/bom_stock_report.py @@ -4,7 +4,8 @@ import frappe from frappe import _ -from frappe.query_builder.functions import Sum +from frappe.query_builder.functions import Floor, Sum +from frappe.utils import cint from pypika.terms import ExistsCriterion @@ -34,57 +35,55 @@ def get_columns(): def get_bom_stock(filters): - qty_to_produce = filters.get("qty_to_produce") or 1 - if int(qty_to_produce) < 0: - frappe.throw(_("Quantity to Produce can not be less than Zero")) + qty_to_produce = filters.get("qty_to_produce") + if cint(qty_to_produce) <= 0: + frappe.throw(_("Quantity to Produce should be greater than zero.")) if filters.get("show_exploded_view"): bom_item_table = "BOM Explosion Item" else: bom_item_table = "BOM Item" - bin = frappe.qb.DocType("Bin") - bom = frappe.qb.DocType("BOM") - bom_item = frappe.qb.DocType(bom_item_table) - - query = ( - frappe.qb.from_(bom) - .inner_join(bom_item) - .on(bom.name == bom_item.parent) - .left_join(bin) - .on(bom_item.item_code == bin.item_code) - .select( - bom_item.item_code, - bom_item.description, - bom_item.stock_qty, - bom_item.stock_uom, - (bom_item.stock_qty / bom.quantity) * qty_to_produce, - Sum(bin.actual_qty), - Sum(bin.actual_qty) / (bom_item.stock_qty / bom.quantity), - ) - .where((bom_item.parent == filters.get("bom")) & (bom_item.parenttype == "BOM")) - .groupby(bom_item.item_code) + warehouse_details = frappe.db.get_value( + "Warehouse", filters.get("warehouse"), ["lft", "rgt"], as_dict=1 ) - if filters.get("warehouse"): - warehouse_details = frappe.db.get_value( - "Warehouse", filters.get("warehouse"), ["lft", "rgt"], as_dict=1 - ) + BOM = frappe.qb.DocType("BOM") + BOM_ITEM = frappe.qb.DocType(bom_item_table) + BIN = frappe.qb.DocType("Bin") + WH = frappe.qb.DocType("Warehouse") + CONDITIONS = () - if warehouse_details: - wh = frappe.qb.DocType("Warehouse") - query = query.where( - ExistsCriterion( - frappe.qb.from_(wh) - .select(wh.name) - .where( - (wh.lft >= warehouse_details.lft) - & (wh.rgt <= warehouse_details.rgt) - & (bin.warehouse == wh.name) - ) - ) + if warehouse_details: + CONDITIONS = ExistsCriterion( + frappe.qb.from_(WH) + .select(WH.name) + .where( + (WH.lft >= warehouse_details.lft) + & (WH.rgt <= warehouse_details.rgt) + & (BIN.warehouse == WH.name) ) - else: - query = query.where(bin.warehouse == filters.get("warehouse")) + ) + else: + CONDITIONS = BIN.warehouse == filters.get("warehouse") - return query.run() + QUERY = ( + frappe.qb.from_(BOM) + .inner_join(BOM_ITEM) + .on(BOM.name == BOM_ITEM.parent) + .left_join(BIN) + .on((BOM_ITEM.item_code == BIN.item_code) & (CONDITIONS)) + .select( + BOM_ITEM.item_code, + BOM_ITEM.description, + BOM_ITEM.stock_qty, + BOM_ITEM.stock_uom, + BOM_ITEM.stock_qty * qty_to_produce / BOM.quantity, + Sum(BIN.actual_qty).as_("actual_qty"), + Sum(Floor(BIN.actual_qty / (BOM_ITEM.stock_qty * qty_to_produce / BOM.quantity))), + ) + .where((BOM_ITEM.parent == filters.get("bom")) & (BOM_ITEM.parenttype == "BOM")) + .groupby(BOM_ITEM.item_code) + ) + + return QUERY.run() From b53dcb04ed2e25e4ef5398ba0fcfef6ab0049200 Mon Sep 17 00:00:00 2001 From: s-aga-r Date: Wed, 8 Mar 2023 14:00:06 +0530 Subject: [PATCH 60/65] test: add test cases for `BOM Stock Report` --- .../bom_stock_report/test_bom_stock_report.py | 108 ++++++++++++++++++ 1 file changed, 108 insertions(+) create mode 100644 erpnext/manufacturing/report/bom_stock_report/test_bom_stock_report.py diff --git a/erpnext/manufacturing/report/bom_stock_report/test_bom_stock_report.py b/erpnext/manufacturing/report/bom_stock_report/test_bom_stock_report.py new file mode 100644 index 00000000000..1c56ebe24d4 --- /dev/null +++ b/erpnext/manufacturing/report/bom_stock_report/test_bom_stock_report.py @@ -0,0 +1,108 @@ +# Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and contributors +# For license information, please see license.txt + + +import frappe +from frappe.exceptions import ValidationError +from frappe.tests.utils import FrappeTestCase +from frappe.utils import floor + +from erpnext.manufacturing.doctype.production_plan.test_production_plan import make_bom +from erpnext.manufacturing.report.bom_stock_report.bom_stock_report import ( + get_bom_stock as bom_stock_report, +) +from erpnext.stock.doctype.item.test_item import make_item +from erpnext.stock.doctype.stock_entry.test_stock_entry import make_stock_entry + + +class TestBomStockReport(FrappeTestCase): + def setUp(self): + self.warehouse = "_Test Warehouse - _TC" + self.fg_item, self.rm_items = create_items() + make_stock_entry(target=self.warehouse, item_code=self.rm_items[0], qty=20, basic_rate=100) + make_stock_entry(target=self.warehouse, item_code=self.rm_items[1], qty=40, basic_rate=200) + self.bom = make_bom(item=self.fg_item, quantity=1, raw_materials=self.rm_items, rm_qty=10) + + def test_bom_stock_report(self): + # Test 1: When `qty_to_produce` is 0. + filters = frappe._dict( + { + "bom": self.bom.name, + "warehouse": "Stores - _TC", + "qty_to_produce": 0, + } + ) + self.assertRaises(ValidationError, bom_stock_report, filters) + + # Test 2: When stock is not available. + data = bom_stock_report( + frappe._dict( + { + "bom": self.bom.name, + "warehouse": "Stores - _TC", + "qty_to_produce": 1, + } + ) + ) + expected_data = get_expected_data(self.bom, "Stores - _TC", 1) + self.assertSetEqual(set(tuple(x) for x in data), set(tuple(x) for x in expected_data)) + + # Test 3: When stock is available. + data = bom_stock_report( + frappe._dict( + { + "bom": self.bom.name, + "warehouse": self.warehouse, + "qty_to_produce": 1, + } + ) + ) + expected_data = get_expected_data(self.bom, self.warehouse, 1) + self.assertSetEqual(set(tuple(x) for x in data), set(tuple(x) for x in expected_data)) + + +def create_items(): + fg_item = make_item(properties={"is_stock_item": 1}).name + rm_item1 = make_item( + properties={ + "is_stock_item": 1, + "standard_rate": 100, + "opening_stock": 100, + "last_purchase_rate": 100, + } + ).name + rm_item2 = make_item( + properties={ + "is_stock_item": 1, + "standard_rate": 200, + "opening_stock": 200, + "last_purchase_rate": 200, + } + ).name + + return fg_item, [rm_item1, rm_item2] + + +def get_expected_data(bom, warehouse, qty_to_produce, show_exploded_view=False): + expected_data = [] + + for item in bom.get("exploded_items") if show_exploded_view else bom.get("items"): + in_stock_qty = frappe.get_cached_value( + "Bin", {"item_code": item.item_code, "warehouse": warehouse}, "actual_qty" + ) + + expected_data.append( + [ + item.item_code, + item.description, + item.stock_qty, + item.stock_uom, + item.stock_qty * qty_to_produce / bom.quantity, + in_stock_qty, + floor(in_stock_qty / (item.stock_qty * qty_to_produce / bom.quantity)) + if in_stock_qty + else None, + ] + ) + + return expected_data From 52ab11389bfcd0e8ef5b904f96f964846c29dcf8 Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Wed, 8 Mar 2023 15:31:25 +0530 Subject: [PATCH 61/65] chore: drop hypothesis dependency pinned in framework now --- pyproject.toml | 3 --- 1 file changed, 3 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 1c93eed6511..0718e5b4a16 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -28,9 +28,6 @@ dependencies = [ requires = ["flit_core >=3.4,<4"] build-backend = "flit_core.buildapi" -[tool.bench.dev-dependencies] -hypothesis = "~=6.31.0" - [tool.black] line-length = 99 From baef5ae1efcf096dcaa9b4d7c06f800330014ba7 Mon Sep 17 00:00:00 2001 From: s-aga-r Date: Wed, 8 Mar 2023 20:07:30 +0530 Subject: [PATCH 62/65] chore: `Alternative Item Code` error msg --- erpnext/stock/doctype/item_alternative/item_alternative.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/stock/doctype/item_alternative/item_alternative.py b/erpnext/stock/doctype/item_alternative/item_alternative.py index fb1a28d846b..0c24d3c780f 100644 --- a/erpnext/stock/doctype/item_alternative/item_alternative.py +++ b/erpnext/stock/doctype/item_alternative/item_alternative.py @@ -54,7 +54,7 @@ class ItemAlternative(Document): if not item_data.allow_alternative_item: frappe.throw(alternate_item_check_msg.format(self.item_code)) if self.two_way and not alternative_item_data.allow_alternative_item: - frappe.throw(alternate_item_check_msg.format(self.item_code)) + frappe.throw(alternate_item_check_msg.format(self.alternative_item_code)) def validate_duplicate(self): if frappe.db.get_value( From 046834a97aaab40fc6e606328bf18cc4df99f71f Mon Sep 17 00:00:00 2001 From: s-aga-r Date: Wed, 8 Mar 2023 20:40:03 +0530 Subject: [PATCH 63/65] fix: `required_qty` get reset to `1` for Alternative Item in WO --- erpnext/manufacturing/doctype/work_order/work_order.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/manufacturing/doctype/work_order/work_order.js b/erpnext/manufacturing/doctype/work_order/work_order.js index 4aff42cb73d..97480b29454 100644 --- a/erpnext/manufacturing/doctype/work_order/work_order.js +++ b/erpnext/manufacturing/doctype/work_order/work_order.js @@ -506,7 +506,7 @@ frappe.ui.form.on("Work Order Item", { callback: function(r) { if (r.message) { frappe.model.set_value(cdt, cdn, { - "required_qty": 1, + "required_qty": row.required_qty || 1, "item_name": r.message.item_name, "description": r.message.description, "source_warehouse": r.message.default_warehouse, From 9a8f8e8b7da532499a8916755a915d2da8081577 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Thu, 9 Mar 2023 15:36:52 +0530 Subject: [PATCH 64/65] Revert "fix: Default sales team not getting set" (#34376) Revert "fix: Default sales team not getting set (#34284)" This reverts commit 7d0199d743c7861e883cadd582c036cc8d9b0a62. --- erpnext/controllers/selling_controller.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/erpnext/controllers/selling_controller.py b/erpnext/controllers/selling_controller.py index 3ea0216bb10..fc16a917d1f 100644 --- a/erpnext/controllers/selling_controller.py +++ b/erpnext/controllers/selling_controller.py @@ -84,9 +84,6 @@ class SellingController(StockController): ) if not self.meta.get_field("sales_team"): party_details.pop("sales_team") - else: - self.set("sales_team", party_details.get("sales_team")) - self.update_if_missing(party_details) elif lead: From 9c1e5663949b6469db47e12f3174b4de7c9b1cd0 Mon Sep 17 00:00:00 2001 From: s-aga-r Date: Thu, 9 Mar 2023 22:12:35 +0530 Subject: [PATCH 65/65] fix: filters not getting applied on `Web Form` --- erpnext/controllers/website_list_for_contact.py | 17 +++++++---------- 1 file changed, 7 insertions(+), 10 deletions(-) diff --git a/erpnext/controllers/website_list_for_contact.py b/erpnext/controllers/website_list_for_contact.py index 467323035ea..7c3c38706dc 100644 --- a/erpnext/controllers/website_list_for_contact.py +++ b/erpnext/controllers/website_list_for_contact.py @@ -76,12 +76,9 @@ def get_transaction_list( ignore_permissions = False if not filters: - filters = [] + filters = {} - if doctype in ["Supplier Quotation", "Purchase Invoice"]: - filters.append((doctype, "docstatus", "<", 2)) - else: - filters.append((doctype, "docstatus", "=", 1)) + filters["docstatus"] = ["<", "2"] if doctype in ["Supplier Quotation", "Purchase Invoice"] else 1 if (user != "Guest" and is_website_user()) or doctype == "Request for Quotation": parties_doctype = ( @@ -92,12 +89,12 @@ def get_transaction_list( if customers: if doctype == "Quotation": - filters.append(("quotation_to", "=", "Customer")) - filters.append(("party_name", "in", customers)) + filters["quotation_to"] = "Customer" + filters["party_name"] = ["in", customers] else: - filters.append(("customer", "in", customers)) + filters["customer"] = ["in", customers] elif suppliers: - filters.append(("supplier", "in", suppliers)) + filters["supplier"] = ["in", suppliers] elif not custom: return [] @@ -110,7 +107,7 @@ def get_transaction_list( if not customers and not suppliers and custom: ignore_permissions = False - filters = [] + filters = {} transactions = get_list_for_transactions( doctype,