Merge pull request #29939 from nextchamp-saqib/ordered-qty-for-packed-items
feat: update ordered qty for packed items
diff --git a/erpnext/controllers/stock_controller.py b/erpnext/controllers/stock_controller.py
index c8e5edd..8972c32 100644
--- a/erpnext/controllers/stock_controller.py
+++ b/erpnext/controllers/stock_controller.py
@@ -507,13 +507,41 @@
"voucher_no": self.name,
"company": self.company
})
- if future_sle_exists(args):
+
+ if future_sle_exists(args) or repost_required_for_queue(self):
item_based_reposting = cint(frappe.db.get_single_value("Stock Reposting Settings", "item_based_reposting"))
if item_based_reposting:
create_item_wise_repost_entries(voucher_type=self.doctype, voucher_no=self.name)
else:
create_repost_item_valuation_entry(args)
+def repost_required_for_queue(doc: StockController) -> bool:
+ """check if stock document contains repeated item-warehouse with queue based valuation.
+
+ if queue exists for repeated items then SLEs need to reprocessed in background again.
+ """
+
+ consuming_sles = frappe.db.get_all("Stock Ledger Entry",
+ filters={
+ "voucher_type": doc.doctype,
+ "voucher_no": doc.name,
+ "actual_qty": ("<", 0),
+ "is_cancelled": 0
+ },
+ fields=["item_code", "warehouse", "stock_queue"]
+ )
+ item_warehouses = [(sle.item_code, sle.warehouse) for sle in consuming_sles]
+
+ unique_item_warehouses = set(item_warehouses)
+
+ if len(unique_item_warehouses) == len(item_warehouses):
+ return False
+
+ for sle in consuming_sles:
+ if sle.stock_queue != "[]": # using FIFO/LIFO valuation
+ return True
+ return False
+
@frappe.whitelist()
def make_quality_inspections(doctype, docname, items):
diff --git a/erpnext/projects/doctype/timesheet/timesheet.js b/erpnext/projects/doctype/timesheet/timesheet.js
index f615f05..453d46c 100644
--- a/erpnext/projects/doctype/timesheet/timesheet.js
+++ b/erpnext/projects/doctype/timesheet/timesheet.js
@@ -116,7 +116,7 @@
currency: function(frm) {
let base_currency = frappe.defaults.get_global_default('currency');
- if (base_currency != frm.doc.currency) {
+ if (frm.doc.currency && (base_currency != frm.doc.currency)) {
frappe.call({
method: "erpnext.setup.utils.get_exchange_rate",
args: {
diff --git a/erpnext/setup/doctype/company/company.py b/erpnext/setup/doctype/company/company.py
index 95b1e8b..36ad8fe 100644
--- a/erpnext/setup/doctype/company/company.py
+++ b/erpnext/setup/doctype/company/company.py
@@ -3,7 +3,6 @@
import json
-import os
import frappe
import frappe.defaults
@@ -422,14 +421,14 @@
return " - ".join(parts)
def install_country_fixtures(company, country):
- path = frappe.get_app_path('erpnext', 'regional', frappe.scrub(country))
- if os.path.exists(path.encode("utf-8")):
- try:
- module_name = "erpnext.regional.{0}.setup.setup".format(frappe.scrub(country))
- frappe.get_attr(module_name)(company, False)
- except Exception as e:
- frappe.log_error()
- frappe.throw(_("Failed to setup defaults for country {0}. Please contact support.").format(frappe.bold(country)))
+ try:
+ module_name = f"erpnext.regional.{frappe.scrub(country)}.setup.setup"
+ frappe.get_attr(module_name)(company, False)
+ except ImportError:
+ pass
+ except Exception:
+ frappe.log_error()
+ frappe.throw(_("Failed to setup defaults for country {0}. Please contact support.").format(frappe.bold(country)))
def update_company_current_month_sales(company):
diff --git a/erpnext/stock/doctype/landed_cost_voucher/test_landed_cost_voucher.py b/erpnext/stock/doctype/landed_cost_voucher/test_landed_cost_voucher.py
index dbaefc1..6dc4fee 100644
--- a/erpnext/stock/doctype/landed_cost_voucher/test_landed_cost_voucher.py
+++ b/erpnext/stock/doctype/landed_cost_voucher/test_landed_cost_voucher.py
@@ -11,6 +11,7 @@
from erpnext.accounts.doctype.purchase_invoice.test_purchase_invoice import make_purchase_invoice
from erpnext.accounts.utils import update_gl_entries_after
from erpnext.assets.doctype.asset.test_asset import create_asset_category, create_fixed_asset_item
+from erpnext.stock.doctype.delivery_note.test_delivery_note import create_delivery_note
from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import (
get_gl_entries,
make_purchase_receipt,
@@ -177,6 +178,53 @@
self.assertEqual(serial_no.purchase_rate - serial_no_rate, 5.0)
self.assertEqual(serial_no.warehouse, "Stores - TCP1")
+ def test_serialized_lcv_delivered(self):
+ """In some cases you'd want to deliver before you can know all the
+ landed costs, this should be allowed for serial nos too.
+
+ Case:
+ - receipt a serial no @ X rate
+ - delivery the serial no @ X rate
+ - add LCV to receipt X + Y
+ - LCV should be successful
+ - delivery should reflect X+Y valuation.
+ """
+ serial_no = "LCV_TEST_SR_NO"
+ item_code = "_Test Serialized Item"
+ warehouse = "Stores - TCP1"
+
+ pr = make_purchase_receipt(company="_Test Company with perpetual inventory",
+ warehouse=warehouse, qty=1, rate=200,
+ item_code=item_code, serial_no=serial_no)
+
+ serial_no_rate = frappe.db.get_value("Serial No", serial_no, "purchase_rate")
+
+ # deliver it before creating LCV
+ dn = create_delivery_note(item_code=item_code,
+ company='_Test Company with perpetual inventory', warehouse='Stores - TCP1',
+ serial_no=serial_no, qty=1, rate=500,
+ cost_center = 'Main - TCP1', expense_account = "Cost of Goods Sold - TCP1")
+
+ charges = 10
+ create_landed_cost_voucher("Purchase Receipt", pr.name, pr.company, charges=charges)
+
+ new_purchase_rate = serial_no_rate + charges
+
+ serial_no = frappe.db.get_value("Serial No", serial_no,
+ ["warehouse", "purchase_rate"], as_dict=1)
+
+ self.assertEqual(serial_no.purchase_rate, new_purchase_rate)
+
+ stock_value_difference = frappe.db.get_value("Stock Ledger Entry",
+ filters={
+ "voucher_no": dn.name,
+ "voucher_type": dn.doctype,
+ "is_cancelled": 0 # LCV cancels with same name.
+ },
+ fieldname="stock_value_difference")
+
+ # reposting should update the purchase rate in future delivery
+ self.assertEqual(stock_value_difference, -new_purchase_rate)
def test_landed_cost_voucher_for_odd_numbers (self):
pr = make_purchase_receipt(company="_Test Company with perpetual inventory", warehouse = "Stores - TCP1", supplier_warehouse = "Work in Progress - TCP1", do_not_save=True)
diff --git a/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py b/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py
index a24acb1..fa28f22 100644
--- a/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py
+++ b/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py
@@ -161,6 +161,15 @@
qty=abs(existing_bin_qty)
)
+ existing_bin_qty, existing_bin_stock_value = frappe.db.get_value(
+ "Bin",
+ {
+ "item_code": "_Test Item",
+ "warehouse": "_Test Warehouse - _TC"
+ },
+ ["actual_qty", "stock_value"]
+ )
+
pr = make_purchase_receipt()
stock_value_difference = frappe.db.get_value(
diff --git a/erpnext/stock/doctype/stock_ledger_entry/test_stock_ledger_entry.py b/erpnext/stock/doctype/stock_ledger_entry/test_stock_ledger_entry.py
index 01d25b2..684a8d4 100644
--- a/erpnext/stock/doctype/stock_ledger_entry/test_stock_ledger_entry.py
+++ b/erpnext/stock/doctype/stock_ledger_entry/test_stock_ledger_entry.py
@@ -389,10 +389,13 @@
)
- def assertSLEs(self, doc, expected_sles):
+ def assertSLEs(self, doc, expected_sles, sle_filters=None):
""" Compare sorted SLEs, useful for vouchers that create multiple SLEs for same line"""
- sles = frappe.get_all("Stock Ledger Entry", fields=["*"],
- filters={"voucher_no": doc.name, "voucher_type": doc.doctype, "is_cancelled":0},
+
+ filters = {"voucher_no": doc.name, "voucher_type": doc.doctype, "is_cancelled": 0}
+ if sle_filters:
+ filters.update(sle_filters)
+ sles = frappe.get_all("Stock Ledger Entry", fields=["*"], filters=filters,
order_by="timestamp(posting_date, posting_time), creation")
for exp_sle, act_sle in zip(expected_sles, sles):
@@ -665,6 +668,78 @@
{"actual_qty": -10, "stock_value_difference": -10*40, "stock_queue": []},
]))
+ def test_fifo_dependent_consumption(self):
+ item = make_item("_TestFifoTransferRates")
+ source = "_Test Warehouse - _TC"
+ target = "Stores - _TC"
+
+ rates = [10 * i for i in range(1, 20)]
+
+ receipt = make_stock_entry(item_code=item.name, target=source, qty=10, do_not_save=True, rate=10)
+ for rate in rates[1:]:
+ row = frappe.copy_doc(receipt.items[0], ignore_no_copy=False)
+ row.basic_rate = rate
+ receipt.append("items", row)
+
+ receipt.save()
+ receipt.submit()
+
+ expected_queues = []
+ for idx, rate in enumerate(rates, start=1):
+ expected_queues.append(
+ {"stock_queue": [[10, 10 * i] for i in range(1, idx + 1)]}
+ )
+ self.assertSLEs(receipt, expected_queues)
+
+ transfer = make_stock_entry(item_code=item.name, source=source, target=target, qty=10, do_not_save=True, rate=10)
+ for rate in rates[1:]:
+ row = frappe.copy_doc(transfer.items[0], ignore_no_copy=False)
+ transfer.append("items", row)
+
+ transfer.save()
+ transfer.submit()
+
+ # same exact queue should be transferred
+ self.assertSLEs(transfer, expected_queues, sle_filters={"warehouse": target})
+
+ def test_fifo_multi_item_repack_consumption(self):
+ rm = make_item("_TestFifoRepackRM")
+ packed = make_item("_TestFifoRepackFinished")
+ warehouse = "_Test Warehouse - _TC"
+
+ rates = [10 * i for i in range(1, 5)]
+
+ receipt = make_stock_entry(item_code=rm.name, target=warehouse, qty=10, do_not_save=True, rate=10)
+ for rate in rates[1:]:
+ row = frappe.copy_doc(receipt.items[0], ignore_no_copy=False)
+ row.basic_rate = rate
+ receipt.append("items", row)
+
+ receipt.save()
+ receipt.submit()
+
+ repack = make_stock_entry(item_code=rm.name, source=warehouse, qty=10,
+ do_not_save=True, rate=10, purpose="Repack")
+ for rate in rates[1:]:
+ row = frappe.copy_doc(repack.items[0], ignore_no_copy=False)
+ repack.append("items", row)
+
+ repack.append("items", {
+ "item_code": packed.name,
+ "t_warehouse": warehouse,
+ "qty": 1,
+ "transfer_qty": 1,
+ })
+
+ repack.save()
+ repack.submit()
+
+ # same exact queue should be transferred
+ self.assertSLEs(repack, [
+ {"incoming_rate": sum(rates) * 10}
+ ], sle_filters={"item_code": packed.name})
+
+
def create_repack_entry(**args):
args = frappe._dict(args)
repack = frappe.new_doc("Stock Entry")
diff --git a/erpnext/stock/report/stock_ledger_invariant_check/stock_ledger_invariant_check.py b/erpnext/stock/report/stock_ledger_invariant_check/stock_ledger_invariant_check.py
index 7826d34..1ba2482 100644
--- a/erpnext/stock/report/stock_ledger_invariant_check/stock_ledger_invariant_check.py
+++ b/erpnext/stock/report/stock_ledger_invariant_check/stock_ledger_invariant_check.py
@@ -21,6 +21,7 @@
"stock_value",
"stock_value_difference",
"valuation_rate",
+ "voucher_detail_no",
)
@@ -66,7 +67,9 @@
balance_qty += sle.actual_qty
balance_stock_value += sle.stock_value_difference
if sle.voucher_type == "Stock Reconciliation" and not sle.batch_no:
- balance_qty = sle.qty_after_transaction
+ balance_qty = frappe.db.get_value("Stock Reconciliation Item", sle.voucher_detail_no, "qty")
+ if balance_qty is None:
+ balance_qty = sle.qty_after_transaction
sle.fifo_queue_qty = fifo_qty
sle.fifo_stock_value = fifo_value
diff --git a/erpnext/stock/stock_ledger.py b/erpnext/stock/stock_ledger.py
index 1b90086..6975552 100644
--- a/erpnext/stock/stock_ledger.py
+++ b/erpnext/stock/stock_ledger.py
@@ -28,6 +28,16 @@
def make_sl_entries(sl_entries, allow_negative_stock=False, via_landed_cost_voucher=False):
+ """ Create SL entries from SL entry dicts
+
+ args:
+ - allow_negative_stock: disable negative stock valiations if true
+ - via_landed_cost_voucher: landed cost voucher cancels and reposts
+ entries of purchase document. This flag is used to identify if
+ cancellation and repost is happening via landed cost voucher, in
+ such cases certain validations need to be ignored (like negative
+ stock)
+ """
from erpnext.controllers.stock_controller import future_sle_exists
if sl_entries:
cancel = sl_entries[0].get("is_cancelled")
@@ -39,7 +49,7 @@
future_sle_exists(args, sl_entries)
for sle in sl_entries:
- if sle.serial_no:
+ if sle.serial_no and not via_landed_cost_voucher:
validate_serial_no(sle)
if cancel: