diff --git a/erpnext/controllers/sales_and_purchase_return.py b/erpnext/controllers/sales_and_purchase_return.py index 4ad3d5a656e..262bff7d640 100644 --- a/erpnext/controllers/sales_and_purchase_return.py +++ b/erpnext/controllers/sales_and_purchase_return.py @@ -12,7 +12,6 @@ from frappe.utils import cint, flt, format_datetime, get_datetime import erpnext from erpnext.stock.serial_batch_bundle import get_batches_from_bundle -from erpnext.stock.serial_batch_bundle import get_serial_nos as get_serial_nos_from_bundle from erpnext.stock.utils import get_combine_datetime, get_incoming_rate, get_valuation_method @@ -145,7 +144,7 @@ def validate_returned_items(doc): ref.rate and flt(d.rate) > ref.rate and doc.doctype in ("Delivery Note", "Sales Invoice") - and get_valuation_method(ref.item_code) != "Moving Average" + and get_valuation_method(ref.item_code, doc.company) != "Moving Average" ): frappe.throw( _("Row # {0}: Rate cannot be greater than the rate used in {1} {2}").format( diff --git a/erpnext/controllers/selling_controller.py b/erpnext/controllers/selling_controller.py index 8b0a0f19f9b..f6b293605c4 100644 --- a/erpnext/controllers/selling_controller.py +++ b/erpnext/controllers/selling_controller.py @@ -524,7 +524,7 @@ class SellingController(StockController): ) if not self.get("return_against") or ( - get_valuation_method(d.item_code) == "Moving Average" + get_valuation_method(d.item_code, self.company) == "Moving Average" and self.get("is_return") and not item_details.has_serial_no and not item_details.has_batch_no @@ -535,7 +535,10 @@ class SellingController(StockController): if ( not d.incoming_rate or self.is_internal_transfer() - or (get_valuation_method(d.item_code) == "Moving Average" and self.get("is_return")) + or ( + get_valuation_method(d.item_code, self.company) == "Moving Average" + and self.get("is_return") + ) ): d.incoming_rate = get_incoming_rate( { @@ -560,7 +563,7 @@ class SellingController(StockController): not d.incoming_rate and self.get("return_against") and self.get("is_return") - and get_valuation_method(d.item_code) == "Moving Average" + and get_valuation_method(d.item_code, self.company) == "Moving Average" ): d.incoming_rate = get_rate_for_return( self.doctype, self.name, d.item_code, self.return_against, item_row=d diff --git a/erpnext/patches.txt b/erpnext/patches.txt index ea8bea587da..4cfc3d2a232 100644 --- a/erpnext/patches.txt +++ b/erpnext/patches.txt @@ -446,4 +446,5 @@ erpnext.patches.v16_0.add_new_stock_entry_types erpnext.patches.v15_0.set_asset_status_if_not_already_set erpnext.patches.v15_0.toggle_legacy_controller_for_period_closing erpnext.patches.v16_0.update_serial_batch_entries -erpnext.patches.v16_0.set_company_wise_warehouses \ No newline at end of file +erpnext.patches.v16_0.set_company_wise_warehouses +erpnext.patches.v16_0.set_valuation_method_on_companies diff --git a/erpnext/patches/v16_0/set_valuation_method_on_companies.py b/erpnext/patches/v16_0/set_valuation_method_on_companies.py new file mode 100644 index 00000000000..f1ea8064dcf --- /dev/null +++ b/erpnext/patches/v16_0/set_valuation_method_on_companies.py @@ -0,0 +1,7 @@ +import frappe + + +def execute(): + valuation_method = frappe.get_single_value("Stock Settings", "valuation_method") + for company in frappe.get_all("Company", pluck="name"): + frappe.db.set_value("Company", company, "valuation_method", valuation_method) diff --git a/erpnext/setup/doctype/company/company.json b/erpnext/setup/doctype/company/company.json index 32c89dc531d..546f2af8dc8 100644 --- a/erpnext/setup/doctype/company/company.json +++ b/erpnext/setup/doctype/company/company.json @@ -117,6 +117,7 @@ "enable_item_wise_inventory_account", "enable_provisional_accounting_for_non_stock_items", "default_inventory_account", + "valuation_method", "column_break_32", "stock_adjustment_account", "stock_received_but_not_billed", @@ -890,6 +891,14 @@ "fieldtype": "Check", "label": "Enable Item-wise Inventory Account" }, + { + "default": "FIFO", + "fieldname": "valuation_method", + "fieldtype": "Select", + "label": "Default Stock Valuation Method", + "options": "FIFO\nMoving Average\nLIFO", + "reqd": 1 + }, { "fieldname": "default_wip_warehouse", "fieldtype": "Link", diff --git a/erpnext/setup/doctype/company/company.py b/erpnext/setup/doctype/company/company.py index 6656af2ef11..05b2044ac71 100644 --- a/erpnext/setup/doctype/company/company.py +++ b/erpnext/setup/doctype/company/company.py @@ -116,6 +116,7 @@ class Company(NestedSet): transactions_annual_history: DF.Code | None unrealized_exchange_gain_loss_account: DF.Link | None unrealized_profit_loss_account: DF.Link | None + valuation_method: DF.Literal["FIFO", "Moving Average", "LIFO"] website: DF.Data | None write_off_account: DF.Link | None # end: auto-generated types @@ -166,6 +167,32 @@ class Company(NestedSet): self.validate_parent_company() self.set_reporting_currency() self.validate_inventory_account_settings() + self.cant_change_valuation_method() + + def cant_change_valuation_method(self): + doc_before_save = self.get_doc_before_save() + if not doc_before_save: + return + + previous_valuation_method = doc_before_save.get("valuation_method") + + if previous_valuation_method and previous_valuation_method != self.valuation_method: + # check if there are any stock ledger entries against items + # which does not have it's own valuation method + sle = frappe.db.sql( + """select name from `tabStock Ledger Entry` sle + where exists(select name from tabItem + where name=sle.item_code and (valuation_method is null or valuation_method='')) and sle.company=%s limit 1 + """, + self.name, + ) + + if sle: + frappe.throw( + _( + "Can't change the valuation method, as there are transactions against some items which do not have its own valuation method" + ) + ) def validate_inventory_account_settings(self): doc_before_save = self.get_doc_before_save() diff --git a/erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.py b/erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.py index ca7e3709b94..69b3abdc5ee 100644 --- a/erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.py +++ b/erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.py @@ -514,7 +514,7 @@ class SerialandBatchBundle(Document): if hasattr(sn_obj, "stock_queue") and sn_obj.stock_queue: stock_queue = parse_json(sn_obj.stock_queue) - val_method = get_valuation_method(self.item_code) + val_method = get_valuation_method(self.item_code, self.company) for d in self.entries: available_qty = 0 @@ -642,7 +642,7 @@ class SerialandBatchBundle(Document): def set_incoming_rate_for_inward_transaction(self, row=None, save=False, prev_sle=None): from erpnext.stock.utils import get_valuation_method - valuation_method = get_valuation_method(self.item_code) + valuation_method = get_valuation_method(self.item_code, self.company) valuation_field = "valuation_rate" if self.voucher_type in ["Sales Invoice", "Delivery Note", "Quotation"]: diff --git a/erpnext/stock/report/stock_ageing/stock_ageing.py b/erpnext/stock/report/stock_ageing/stock_ageing.py index e2cce6e99da..0f37b3f0116 100644 --- a/erpnext/stock/report/stock_ageing/stock_ageing.py +++ b/erpnext/stock/report/stock_ageing/stock_ageing.py @@ -52,7 +52,7 @@ def format_report_data(filters: Filters, item_details: dict, to_date: str) -> li range_values = get_range_age(filters, fifo_queue, to_date, item_dict) check_and_replace_valuations_if_moving_average( - range_values, details.valuation_method, details.valuation_rate + range_values, details.valuation_method, details.valuation_rate, filters.get("company") ) row = [details.name, details.item_name, details.description, details.item_group, details.brand] @@ -76,10 +76,12 @@ def format_report_data(filters: Filters, item_details: dict, to_date: str) -> li return data -def check_and_replace_valuations_if_moving_average(range_values, item_valuation_method, valuation_rate): +def check_and_replace_valuations_if_moving_average( + range_values, item_valuation_method, valuation_rate, company +): if item_valuation_method == "Moving Average" or ( not item_valuation_method - and frappe.db.get_single_value("Stock Settings", "valuation_method") == "Moving Average" + and frappe.get_cached_value("Company", company, "valuation_method") == "Moving Average" ): for i in range(0, len(range_values), 2): range_values[i + 1] = range_values[i] * valuation_rate diff --git a/erpnext/stock/report/stock_ledger_variance/stock_ledger_variance.py b/erpnext/stock/report/stock_ledger_variance/stock_ledger_variance.py index 808afadd05a..97243d57001 100644 --- a/erpnext/stock/report/stock_ledger_variance/stock_ledger_variance.py +++ b/erpnext/stock/report/stock_ledger_variance/stock_ledger_variance.py @@ -201,7 +201,7 @@ def get_columns(): def get_data(filters=None): filters = frappe._dict(filters or {}) item_warehouse_map = get_item_warehouse_combinations(filters) - valuation_method = frappe.db.get_single_value("Stock Settings", "valuation_method") + valuation_method = frappe.get_cached_value("Company", filters.get("company"), "valuation_method") data = [] if item_warehouse_map: diff --git a/erpnext/stock/serial_batch_bundle.py b/erpnext/stock/serial_batch_bundle.py index e7cbc8bceed..1fb5d1391aa 100644 --- a/erpnext/stock/serial_batch_bundle.py +++ b/erpnext/stock/serial_batch_bundle.py @@ -861,9 +861,9 @@ class BatchNoValuation(DeprecatedBatchNoValuation): self.batchwise_valuation_batches = [] self.non_batchwise_valuation_batches = [] - if get_valuation_method(self.sle.item_code) == "Moving Average" and frappe.get_single_value( - "Stock Settings", "do_not_use_batchwise_valuation" - ): + if get_valuation_method( + self.sle.item_code, self.sle.company + ) == "Moving Average" and frappe.get_single_value("Stock Settings", "do_not_use_batchwise_valuation"): self.non_batchwise_valuation_batches = self.batches return diff --git a/erpnext/stock/stock_ledger.py b/erpnext/stock/stock_ledger.py index 1801aeb2f9e..2c9e57afecb 100644 --- a/erpnext/stock/stock_ledger.py +++ b/erpnext/stock/stock_ledger.py @@ -563,7 +563,7 @@ class update_entries_after: self.company = frappe.get_cached_value("Warehouse", self.args.warehouse, "company") self.set_precision() - self.valuation_method = get_valuation_method(self.item_code) + self.valuation_method = get_valuation_method(self.item_code, self.company) self.new_items_found = False self.distinct_item_warehouses = args.get("distinct_item_warehouses", frappe._dict()) @@ -1087,7 +1087,7 @@ class update_entries_after: avg_rate = 0.0 for d in sabb_data: - incoming_rate = get_incoming_rate_for_serial_and_batch(self.item_code, d, sn_obj) + incoming_rate = get_incoming_rate_for_serial_and_batch(self.item_code, d, sn_obj, self.company) amount = incoming_rate * flt(d.qty) tot_amt += flt(amount) total_qty += flt(d.qty) @@ -2398,7 +2398,7 @@ def get_serial_from_sabb(serial_and_batch_bundle): ) -def get_incoming_rate_for_serial_and_batch(item_code, row, sn_obj): +def get_incoming_rate_for_serial_and_batch(item_code, row, sn_obj, company): if row.serial_no: return abs(sn_obj.serial_no_incoming_rate.get(row.serial_no, 0.0)) else: @@ -2406,7 +2406,7 @@ def get_incoming_rate_for_serial_and_batch(item_code, row, sn_obj): if hasattr(sn_obj, "stock_queue") and sn_obj.stock_queue: stock_queue = parse_json(sn_obj.stock_queue) - val_method = get_valuation_method(item_code) + val_method = get_valuation_method(item_code, company) actual_qty = row.qty if stock_queue and val_method == "FIFO" and row.batch_no in sn_obj.non_batchwise_valuation_batches: diff --git a/erpnext/stock/utils.py b/erpnext/stock/utils.py index bee190e13a1..b891e0744fc 100644 --- a/erpnext/stock/utils.py +++ b/erpnext/stock/utils.py @@ -302,7 +302,7 @@ def get_incoming_rate(args, raise_error_if_no_rate=True): return batch_obj.get_incoming_rate() else: - valuation_method = get_valuation_method(args.get("item_code")) + valuation_method = get_valuation_method(args.get("item_code"), args.get("company")) previous_sle = get_previous_sle(args) if valuation_method in ("FIFO", "LIFO"): if previous_sle: @@ -374,11 +374,15 @@ def get_avg_purchase_rate(serial_nos): @frappe.request_cache -def get_valuation_method(item_code): +def get_valuation_method(item_code, company=None): """get valuation method from item or default""" val_method = frappe.get_cached_value("Item", item_code, "valuation_method") if not val_method: - val_method = frappe.get_cached_doc("Stock Settings").valuation_method or "FIFO" + val_method = ( + frappe.get_cached_value("Company", company, "valuation_method") + if company + else frappe.get_single_value("Stock Settings", "valuation_method") or "FIFO" + ) return val_method