Merge pull request #29987 from marination/mr-to-order-and-receive-report
fix: Get MRs that are yet to be received but fully ordered in Report `Requested Items to Order and Receive`
diff --git a/erpnext/accounts/doctype/payment_entry/payment_entry.js b/erpnext/accounts/doctype/payment_entry/payment_entry.js
index cc32a6c..b2b818a 100644
--- a/erpnext/accounts/doctype/payment_entry/payment_entry.js
+++ b/erpnext/accounts/doctype/payment_entry/payment_entry.js
@@ -346,6 +346,8 @@
}
frm.set_party_account_based_on_party = true;
+ let company_currency = frappe.get_doc(":Company", frm.doc.company).default_currency;
+
return frappe.call({
method: "erpnext.accounts.doctype.payment_entry.payment_entry.get_party_details",
args: {
@@ -379,7 +381,11 @@
if (r.message.bank_account) {
frm.set_value("bank_account", r.message.bank_account);
}
- }
+ },
+ () => frm.events.set_current_exchange_rate(frm, "source_exchange_rate",
+ frm.doc.paid_from_account_currency, company_currency),
+ () => frm.events.set_current_exchange_rate(frm, "target_exchange_rate",
+ frm.doc.paid_to_account_currency, company_currency)
]);
}
}
@@ -483,14 +489,14 @@
},
paid_from_account_currency: function(frm) {
- if(!frm.doc.paid_from_account_currency) return;
- var company_currency = frappe.get_doc(":Company", frm.doc.company).default_currency;
+ if(!frm.doc.paid_from_account_currency || !frm.doc.company) return;
+ let company_currency = frappe.get_doc(":Company", frm.doc.company).default_currency;
if (frm.doc.paid_from_account_currency == company_currency) {
frm.set_value("source_exchange_rate", 1);
} else if (frm.doc.paid_from){
if (in_list(["Internal Transfer", "Pay"], frm.doc.payment_type)) {
- var company_currency = frappe.get_doc(":Company", frm.doc.company).default_currency;
+ let company_currency = frappe.get_doc(":Company", frm.doc.company).default_currency;
frappe.call({
method: "erpnext.setup.utils.get_exchange_rate",
args: {
@@ -510,8 +516,8 @@
},
paid_to_account_currency: function(frm) {
- if(!frm.doc.paid_to_account_currency) return;
- var company_currency = frappe.get_doc(":Company", frm.doc.company).default_currency;
+ if(!frm.doc.paid_to_account_currency || !frm.doc.company) return;
+ let company_currency = frappe.get_doc(":Company", frm.doc.company).default_currency;
frm.events.set_current_exchange_rate(frm, "target_exchange_rate",
frm.doc.paid_to_account_currency, company_currency);
diff --git a/erpnext/buying/doctype/purchase_order/purchase_order.py b/erpnext/buying/doctype/purchase_order/purchase_order.py
index 1b5f35e..2e7d306 100644
--- a/erpnext/buying/doctype/purchase_order/purchase_order.py
+++ b/erpnext/buying/doctype/purchase_order/purchase_order.py
@@ -316,6 +316,16 @@
'target_ref_field': 'stock_qty',
'source_field': 'stock_qty'
})
+ self.status_updater.append({
+ 'source_dt': 'Purchase Order Item',
+ 'target_dt': 'Packed Item',
+ 'target_field': 'ordered_qty',
+ 'target_parent_dt': 'Sales Order',
+ 'target_parent_field': '',
+ 'join_field': 'sales_order_packed_item',
+ 'target_ref_field': 'qty',
+ 'source_field': 'stock_qty'
+ })
def update_delivered_qty_in_sales_order(self):
"""Update delivered qty in Sales Order for drop ship"""
diff --git a/erpnext/buying/doctype/purchase_order_item/purchase_order_item.json b/erpnext/buying/doctype/purchase_order_item/purchase_order_item.json
index 87cd575..a18c527 100644
--- a/erpnext/buying/doctype/purchase_order_item/purchase_order_item.json
+++ b/erpnext/buying/doctype/purchase_order_item/purchase_order_item.json
@@ -63,6 +63,7 @@
"material_request_item",
"sales_order",
"sales_order_item",
+ "sales_order_packed_item",
"supplier_quotation",
"supplier_quotation_item",
"col_break5",
@@ -837,21 +838,30 @@
"label": "Product Bundle",
"options": "Product Bundle",
"read_only": 1
+ },
+ {
+ "fieldname": "sales_order_packed_item",
+ "fieldtype": "Data",
+ "label": "Sales Order Packed Item",
+ "no_copy": 1,
+ "print_hide": 1
}
],
"idx": 1,
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
- "modified": "2021-08-30 20:06:26.712097",
+ "modified": "2022-02-02 13:10:18.398976",
"modified_by": "Administrator",
"module": "Buying",
"name": "Purchase Order Item",
+ "naming_rule": "Random",
"owner": "Administrator",
"permissions": [],
"quick_entry": 1,
"search_fields": "item_name",
"sort_field": "modified",
"sort_order": "DESC",
+ "states": [],
"track_changes": 1
}
\ No newline at end of file
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/manufacturing/doctype/operation/operation_dashboard.py b/erpnext/manufacturing/doctype/operation/operation_dashboard.py
index 4fbcf49..9f7efa2 100644
--- a/erpnext/manufacturing/doctype/operation/operation_dashboard.py
+++ b/erpnext/manufacturing/doctype/operation/operation_dashboard.py
@@ -7,7 +7,7 @@
'transactions': [
{
'label': _('Manufacture'),
- 'items': ['BOM', 'Work Order', 'Job Card', 'Timesheet']
+ 'items': ['BOM', 'Work Order', 'Job Card']
}
]
}
diff --git a/erpnext/manufacturing/doctype/routing/routing.js b/erpnext/manufacturing/doctype/routing/routing.js
index 33a313e..b480c70 100644
--- a/erpnext/manufacturing/doctype/routing/routing.js
+++ b/erpnext/manufacturing/doctype/routing/routing.js
@@ -17,7 +17,7 @@
},
calculate_operating_cost: function(frm, child) {
- const operating_cost = flt(flt(child.hour_rate) * flt(child.time_in_mins) / 60, 2);
+ const operating_cost = flt(flt(child.hour_rate) * flt(child.time_in_mins) / 60, precision("operating_cost", child));
frappe.model.set_value(child.doctype, child.name, "operating_cost", operating_cost);
}
});
diff --git a/erpnext/manufacturing/doctype/routing/routing.py b/erpnext/manufacturing/doctype/routing/routing.py
index 1c76634..b207906 100644
--- a/erpnext/manufacturing/doctype/routing/routing.py
+++ b/erpnext/manufacturing/doctype/routing/routing.py
@@ -20,7 +20,8 @@
for operation in self.operations:
if not operation.hour_rate:
operation.hour_rate = frappe.db.get_value("Workstation", operation.workstation, 'hour_rate')
- operation.operating_cost = flt(flt(operation.hour_rate) * flt(operation.time_in_mins) / 60, 2)
+ operation.operating_cost = flt(flt(operation.hour_rate) * flt(operation.time_in_mins) / 60,
+ operation.precision("operating_cost"))
def set_routing_id(self):
sequence_id = 0
diff --git a/erpnext/manufacturing/doctype/workstation/workstation_dashboard.py b/erpnext/manufacturing/doctype/workstation/workstation_dashboard.py
index bc481ca..1fa1494 100644
--- a/erpnext/manufacturing/doctype/workstation/workstation_dashboard.py
+++ b/erpnext/manufacturing/doctype/workstation/workstation_dashboard.py
@@ -11,9 +11,9 @@
},
{
'label': _('Transaction'),
- 'items': ['Work Order', 'Job Card', 'Timesheet']
+ 'items': ['Work Order', 'Job Card',]
}
],
'disable_create_buttons': ['BOM', 'Routing', 'Operation',
- 'Work Order', 'Job Card', 'Timesheet']
+ 'Work Order', 'Job Card',]
}
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/selling/doctype/sales_order/sales_order.js b/erpnext/selling/doctype/sales_order/sales_order.js
index eb98e6c..f80eaf2 100644
--- a/erpnext/selling/doctype/sales_order/sales_order.js
+++ b/erpnext/selling/doctype/sales_order/sales_order.js
@@ -562,6 +562,7 @@
var me = this;
var dialog = new frappe.ui.Dialog({
title: __("Select Items"),
+ size: "large",
fields: [
{
"fieldtype": "Check",
@@ -663,7 +664,8 @@
} else {
let po_items = [];
me.frm.doc.items.forEach(d => {
- let pending_qty = (flt(d.stock_qty) - flt(d.ordered_qty)) / flt(d.conversion_factor);
+ let ordered_qty = me.get_ordered_qty(d, me.frm.doc);
+ let pending_qty = (flt(d.stock_qty) - ordered_qty) / flt(d.conversion_factor);
if (pending_qty > 0) {
po_items.push({
"doctype": "Sales Order Item",
@@ -689,6 +691,24 @@
dialog.show();
}
+ get_ordered_qty(item, so) {
+ let ordered_qty = item.ordered_qty;
+ if (so.packed_items) {
+ // calculate ordered qty based on packed items in case of product bundle
+ let packed_items = so.packed_items.filter(
+ (pi) => pi.parent_detail_docname == item.name
+ );
+ if (packed_items) {
+ ordered_qty = packed_items.reduce(
+ (sum, pi) => sum + flt(pi.ordered_qty),
+ 0
+ );
+ ordered_qty = ordered_qty / packed_items.length;
+ }
+ }
+ return ordered_qty;
+ }
+
hold_sales_order(){
var me = this;
var d = new frappe.ui.Dialog({
diff --git a/erpnext/selling/doctype/sales_order/sales_order.py b/erpnext/selling/doctype/sales_order/sales_order.py
index 0f5b1e3..abbb3c9 100755
--- a/erpnext/selling/doctype/sales_order/sales_order.py
+++ b/erpnext/selling/doctype/sales_order/sales_order.py
@@ -877,6 +877,9 @@
target.stock_qty = (flt(source.stock_qty) - flt(source.ordered_qty))
target.project = source_parent.project
+ def update_item_for_packed_item(source, target, source_parent):
+ target.qty = flt(source.qty) - flt(source.ordered_qty)
+
# po = frappe.get_list("Purchase Order", filters={"sales_order":source_name, "supplier":supplier, "docstatus": ("<", "2")})
doc = get_mapped_doc("Sales Order", source_name, {
"Sales Order": {
@@ -920,6 +923,7 @@
"Packed Item": {
"doctype": "Purchase Order Item",
"field_map": [
+ ["name", "sales_order_packed_item"],
["parent", "sales_order"],
["uom", "uom"],
["conversion_factor", "conversion_factor"],
@@ -934,6 +938,7 @@
"supplier",
"pricing_rules"
],
+ "postprocess": update_item_for_packed_item,
"condition": lambda doc: doc.parent_item in items_to_map
}
}, target_doc, set_missing_values)
diff --git a/erpnext/selling/doctype/sales_order/test_sales_order.py b/erpnext/selling/doctype/sales_order/test_sales_order.py
index f5a34c0..b528479 100644
--- a/erpnext/selling/doctype/sales_order/test_sales_order.py
+++ b/erpnext/selling/doctype/sales_order/test_sales_order.py
@@ -959,6 +959,42 @@
self.assertEqual(purchase_order.items[0].item_code, "_Test Bundle Item 1")
self.assertEqual(purchase_order.items[1].item_code, "_Test Bundle Item 2")
+ def test_purchase_order_updates_packed_item_ordered_qty(self):
+ """
+ Tests if the packed item's `ordered_qty` is updated with the quantity of the Purchase Order
+ """
+ from erpnext.selling.doctype.sales_order.sales_order import make_purchase_order
+
+ product_bundle = make_item("_Test Product Bundle", {"is_stock_item": 0})
+ make_item("_Test Bundle Item 1", {"is_stock_item": 1})
+ make_item("_Test Bundle Item 2", {"is_stock_item": 1})
+
+ make_product_bundle("_Test Product Bundle",
+ ["_Test Bundle Item 1", "_Test Bundle Item 2"])
+
+ so_items = [
+ {
+ "item_code": product_bundle.item_code,
+ "warehouse": "",
+ "qty": 2,
+ "rate": 400,
+ "delivered_by_supplier": 1,
+ "supplier": '_Test Supplier'
+ }
+ ]
+
+ so = make_sales_order(item_list=so_items)
+
+ purchase_order = make_purchase_order(so.name, selected_items=so_items)
+ purchase_order.supplier = "_Test Supplier"
+ purchase_order.set_warehouse = "_Test Warehouse - _TC"
+ purchase_order.save()
+ purchase_order.submit()
+
+ so.reload()
+ self.assertEqual(so.packed_items[0].ordered_qty, 2)
+ self.assertEqual(so.packed_items[1].ordered_qty, 2)
+
def test_reserved_qty_for_closing_so(self):
bin = frappe.get_all("Bin", filters={"item_code": "_Test Item", "warehouse": "_Test Warehouse - _TC"},
fields=["reserved_qty"])
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/packed_item/packed_item.json b/erpnext/stock/doctype/packed_item/packed_item.json
index d2d4789..d6e2e9c 100644
--- a/erpnext/stock/doctype/packed_item/packed_item.json
+++ b/erpnext/stock/doctype/packed_item/packed_item.json
@@ -26,6 +26,7 @@
"section_break_13",
"actual_qty",
"projected_qty",
+ "ordered_qty",
"column_break_16",
"incoming_rate",
"page_break",
@@ -224,13 +225,21 @@
"label": "Rate",
"print_hide": 1,
"read_only": 1
+ },
+ {
+ "allow_on_submit": 1,
+ "fieldname": "ordered_qty",
+ "fieldtype": "Float",
+ "label": "Ordered Qty",
+ "no_copy": 1,
+ "read_only": 1
}
],
"idx": 1,
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
- "modified": "2022-01-28 16:03:30.780111",
+ "modified": "2022-02-22 12:57:45.325488",
"modified_by": "Administrator",
"module": "Stock",
"name": "Packed Item",
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: