mirror of
https://github.com/frappe/erpnext.git
synced 2026-05-27 00:44:45 +00:00
Merge branch 'version-12-hotfix' into batch-source-reference
This commit is contained in:
@@ -184,17 +184,11 @@ class StockReconciliation(StockController):
|
|||||||
from erpnext.stock.stock_ledger import get_previous_sle
|
from erpnext.stock.stock_ledger import get_previous_sle
|
||||||
|
|
||||||
sl_entries = []
|
sl_entries = []
|
||||||
has_serial_no = False
|
|
||||||
has_batch_no = False
|
|
||||||
for row in self.items:
|
|
||||||
item = frappe.get_doc("Item", row.item_code)
|
|
||||||
if item.has_batch_no:
|
|
||||||
has_batch_no = True
|
|
||||||
|
|
||||||
if item.has_serial_no or item.has_batch_no:
|
serialized_items = False
|
||||||
has_serial_no = True
|
for row in self.items:
|
||||||
self.get_sle_for_serialized_items(row, sl_entries)
|
item = frappe.get_cached_doc("Item", row.item_code)
|
||||||
else:
|
if not (item.has_serial_no or item.has_batch_no):
|
||||||
if row.serial_no or row.batch_no:
|
if row.serial_no or row.batch_no:
|
||||||
frappe.throw(_("Row #{0}: Item {1} is not a Serialized/Batched Item. It cannot have a Serial No/Batch No against it.") \
|
frappe.throw(_("Row #{0}: Item {1} is not a Serialized/Batched Item. It cannot have a Serial No/Batch No against it.") \
|
||||||
.format(row.idx, frappe.bold(row.item_code)))
|
.format(row.idx, frappe.bold(row.item_code)))
|
||||||
@@ -222,88 +216,91 @@ class StockReconciliation(StockController):
|
|||||||
|
|
||||||
sl_entries.append(self.get_sle_for_items(row))
|
sl_entries.append(self.get_sle_for_items(row))
|
||||||
|
|
||||||
|
else:
|
||||||
|
serialized_items = True
|
||||||
|
|
||||||
|
if serialized_items:
|
||||||
|
self.get_sle_for_serialized_items(sl_entries)
|
||||||
|
|
||||||
if sl_entries:
|
if sl_entries:
|
||||||
if has_serial_no:
|
allow_negative_stock = frappe.get_cached_value("Stock Settings", None, "allow_negative_stock")
|
||||||
sl_entries = self.merge_similar_item_serial_nos(sl_entries)
|
|
||||||
|
|
||||||
allow_negative_stock = False
|
|
||||||
if has_batch_no:
|
|
||||||
allow_negative_stock = True
|
|
||||||
|
|
||||||
self.make_sl_entries(sl_entries, allow_negative_stock=allow_negative_stock)
|
self.make_sl_entries(sl_entries, allow_negative_stock=allow_negative_stock)
|
||||||
|
|
||||||
if has_serial_no and sl_entries:
|
def get_sle_for_serialized_items(self, sl_entries):
|
||||||
self.update_valuation_rate_for_serial_no()
|
self.issue_existing_serial_and_batch(sl_entries)
|
||||||
|
self.add_new_serial_and_batch(sl_entries)
|
||||||
|
self.update_valuation_rate_for_serial_no()
|
||||||
|
|
||||||
def get_sle_for_serialized_items(self, row, sl_entries):
|
if sl_entries:
|
||||||
|
sl_entries = self.merge_similar_item_serial_nos(sl_entries)
|
||||||
|
|
||||||
|
def issue_existing_serial_and_batch(self, sl_entries):
|
||||||
from erpnext.stock.stock_ledger import get_previous_sle
|
from erpnext.stock.stock_ledger import get_previous_sle
|
||||||
|
|
||||||
serial_nos = get_serial_nos(row.serial_no)
|
for row in self.items:
|
||||||
|
serial_nos = get_serial_nos(row.serial_no) or []
|
||||||
|
|
||||||
|
# To issue existing serial nos
|
||||||
# To issue existing serial nos
|
if row.current_qty and (row.current_serial_no or row.batch_no):
|
||||||
if row.current_qty and (row.current_serial_no or row.batch_no):
|
args = self.get_sle_for_items(row)
|
||||||
args = self.get_sle_for_items(row)
|
|
||||||
args.update({
|
|
||||||
'actual_qty': -1 * row.current_qty,
|
|
||||||
'serial_no': row.current_serial_no,
|
|
||||||
'batch_no': row.batch_no,
|
|
||||||
'valuation_rate': row.current_valuation_rate
|
|
||||||
})
|
|
||||||
|
|
||||||
if row.current_serial_no:
|
|
||||||
args.update({
|
args.update({
|
||||||
'qty_after_transaction': 0,
|
'actual_qty': -1 * row.current_qty,
|
||||||
|
'serial_no': row.current_serial_no,
|
||||||
|
'batch_no': row.batch_no,
|
||||||
|
'valuation_rate': row.current_valuation_rate
|
||||||
})
|
})
|
||||||
|
|
||||||
sl_entries.append(args)
|
if row.current_serial_no:
|
||||||
|
args.update({
|
||||||
|
'qty_after_transaction': 0,
|
||||||
|
})
|
||||||
|
|
||||||
qty_after_transaction = 0
|
sl_entries.append(args)
|
||||||
for serial_no in serial_nos:
|
|
||||||
args = self.get_sle_for_items(row, [serial_no])
|
|
||||||
|
|
||||||
previous_sle = get_previous_sle({
|
qty_after_transaction = 0
|
||||||
"item_code": row.item_code,
|
for serial_no in serial_nos:
|
||||||
"posting_date": self.posting_date,
|
args = self.get_sle_for_items(row, [serial_no])
|
||||||
"posting_time": self.posting_time,
|
|
||||||
"serial_no": serial_no
|
|
||||||
})
|
|
||||||
|
|
||||||
if previous_sle and row.warehouse != previous_sle.get("warehouse"):
|
previous_sle = get_previous_sle({
|
||||||
# If serial no exists in different warehouse
|
"item_code": row.item_code,
|
||||||
|
"posting_date": self.posting_date,
|
||||||
warehouse = previous_sle.get("warehouse", '') or row.warehouse
|
"posting_time": self.posting_time,
|
||||||
|
"serial_no": serial_no
|
||||||
if not qty_after_transaction:
|
|
||||||
qty_after_transaction = get_stock_balance(row.item_code,
|
|
||||||
warehouse, self.posting_date, self.posting_time)
|
|
||||||
|
|
||||||
qty_after_transaction -= 1
|
|
||||||
|
|
||||||
new_args = args.copy()
|
|
||||||
new_args.update({
|
|
||||||
'actual_qty': -1,
|
|
||||||
'qty_after_transaction': qty_after_transaction,
|
|
||||||
'warehouse': warehouse,
|
|
||||||
'valuation_rate': previous_sle.get("valuation_rate")
|
|
||||||
})
|
})
|
||||||
|
|
||||||
sl_entries.append(new_args)
|
if previous_sle and row.warehouse != previous_sle.get("warehouse"):
|
||||||
|
# If serial no exists in different warehouse
|
||||||
|
|
||||||
if row.qty:
|
warehouse = previous_sle.get("warehouse", '') or row.warehouse
|
||||||
args = self.get_sle_for_items(row)
|
|
||||||
|
|
||||||
args.update({
|
if not qty_after_transaction:
|
||||||
'actual_qty': row.qty,
|
qty_after_transaction = get_stock_balance(row.item_code,
|
||||||
'incoming_rate': row.valuation_rate,
|
warehouse, self.posting_date, self.posting_time)
|
||||||
'valuation_rate': row.valuation_rate
|
|
||||||
})
|
|
||||||
|
|
||||||
sl_entries.append(args)
|
qty_after_transaction -= 1
|
||||||
|
|
||||||
if serial_nos == get_serial_nos(row.current_serial_no):
|
new_args = args.copy()
|
||||||
# update valuation rate
|
new_args.update({
|
||||||
self.update_valuation_rate_for_serial_nos(row, serial_nos)
|
'actual_qty': -1,
|
||||||
|
'qty_after_transaction': qty_after_transaction,
|
||||||
|
'warehouse': warehouse,
|
||||||
|
'valuation_rate': previous_sle.get("valuation_rate")
|
||||||
|
})
|
||||||
|
|
||||||
|
sl_entries.append(new_args)
|
||||||
|
|
||||||
|
def add_new_serial_and_batch(self, sl_entries):
|
||||||
|
for row in self.items:
|
||||||
|
if row.qty:
|
||||||
|
args = self.get_sle_for_items(row)
|
||||||
|
|
||||||
|
args.update({
|
||||||
|
'actual_qty': row.qty,
|
||||||
|
'incoming_rate': row.valuation_rate,
|
||||||
|
'valuation_rate': row.valuation_rate
|
||||||
|
})
|
||||||
|
|
||||||
|
sl_entries.append(args)
|
||||||
|
|
||||||
def update_valuation_rate_for_serial_no(self):
|
def update_valuation_rate_for_serial_no(self):
|
||||||
for d in self.items:
|
for d in self.items:
|
||||||
@@ -361,17 +358,9 @@ class StockReconciliation(StockController):
|
|||||||
where voucher_type=%s and voucher_no=%s""", (self.doctype, self.name))
|
where voucher_type=%s and voucher_no=%s""", (self.doctype, self.name))
|
||||||
|
|
||||||
sl_entries = []
|
sl_entries = []
|
||||||
|
self.get_sle_for_serialized_items(sl_entries)
|
||||||
has_serial_no = False
|
|
||||||
for row in self.items:
|
|
||||||
if row.serial_no or row.batch_no or row.current_serial_no:
|
|
||||||
has_serial_no = True
|
|
||||||
self.get_sle_for_serialized_items(row, sl_entries)
|
|
||||||
|
|
||||||
if sl_entries:
|
if sl_entries:
|
||||||
if has_serial_no:
|
|
||||||
sl_entries = self.merge_similar_item_serial_nos(sl_entries)
|
|
||||||
|
|
||||||
sl_entries.reverse()
|
sl_entries.reverse()
|
||||||
allow_negative_stock = frappe.db.get_value("Stock Settings", None, "allow_negative_stock")
|
allow_negative_stock = frappe.db.get_value("Stock Settings", None, "allow_negative_stock")
|
||||||
self.make_sl_entries(sl_entries, allow_negative_stock=allow_negative_stock)
|
self.make_sl_entries(sl_entries, allow_negative_stock=allow_negative_stock)
|
||||||
|
|||||||
@@ -308,6 +308,59 @@ class TestStockReconciliation(unittest.TestCase):
|
|||||||
if frappe.db.exists("Serial No", sn):
|
if frappe.db.exists("Serial No", sn):
|
||||||
frappe.delete_doc("Serial No", sn)
|
frappe.delete_doc("Serial No", sn)
|
||||||
|
|
||||||
|
def test_stock_reco_for_same_item_with_multiple_batches(self):
|
||||||
|
from erpnext.stock.doctype.stock_entry.test_stock_entry import make_stock_entry
|
||||||
|
|
||||||
|
set_perpetual_inventory()
|
||||||
|
|
||||||
|
item_code = "Stock-Reco-batch-Item-2"
|
||||||
|
warehouse = "_Test Warehouse for Stock Reco3 - _TC"
|
||||||
|
|
||||||
|
create_warehouse("_Test Warehouse for Stock Reco3", {"is_group": 0,
|
||||||
|
"parent_warehouse": "_Test Warehouse Group - _TC", "company": "_Test Company"})
|
||||||
|
|
||||||
|
batch_item_doc = create_item(item_code, is_stock_item=1)
|
||||||
|
if not batch_item_doc.has_batch_no:
|
||||||
|
frappe.db.set_value("Item", item_code, {
|
||||||
|
"has_batch_no": 1,
|
||||||
|
"create_new_batch": 1,
|
||||||
|
"batch_number_series": "Test-C.####"
|
||||||
|
})
|
||||||
|
|
||||||
|
# inward entries with different batch and valuation rate
|
||||||
|
ste1=make_stock_entry(posting_date="2012-12-15", posting_time="02:00", item_code=item_code,
|
||||||
|
target=warehouse, qty=6, basic_rate=700)
|
||||||
|
ste2=make_stock_entry(posting_date="2012-12-16", posting_time="02:00", item_code=item_code,
|
||||||
|
target=warehouse, qty=3, basic_rate=200)
|
||||||
|
ste3=make_stock_entry(posting_date="2012-12-17", posting_time="02:00", item_code=item_code,
|
||||||
|
target=warehouse, qty=2, basic_rate=500)
|
||||||
|
ste4=make_stock_entry(posting_date="2012-12-17", posting_time="02:00", item_code=item_code,
|
||||||
|
target=warehouse, qty=4, basic_rate=100)
|
||||||
|
|
||||||
|
batchwise_item_details = {}
|
||||||
|
for stock_doc in [ste1, ste2, ste3, ste4]:
|
||||||
|
self.assertEqual(item_code, stock_doc.items[0].item_code)
|
||||||
|
batchwise_item_details[stock_doc.items[0].batch_no] = [stock_doc.items[0].qty, 0.01]
|
||||||
|
|
||||||
|
stock_balance = frappe.get_all("Stock Ledger Entry",
|
||||||
|
filters = {"item_code": item_code, "warehouse": warehouse},
|
||||||
|
fields=["sum(stock_value_difference)"], as_list=1)
|
||||||
|
|
||||||
|
self.assertEqual(flt(stock_balance[0][0]), 6200.00)
|
||||||
|
|
||||||
|
sr = create_stock_reconciliation(item_code=item_code,
|
||||||
|
warehouse = warehouse, batch_details = batchwise_item_details)
|
||||||
|
|
||||||
|
stock_balance = frappe.get_all("Stock Ledger Entry",
|
||||||
|
filters = {"item_code": item_code, "warehouse": warehouse},
|
||||||
|
fields=["sum(stock_value_difference)"], as_list=1)
|
||||||
|
|
||||||
|
self.assertEqual(flt(stock_balance[0][0]), 0.15)
|
||||||
|
|
||||||
|
for doc in [sr, ste1, ste2, ste3, ste4]:
|
||||||
|
doc.cancel()
|
||||||
|
frappe.delete_doc(doc.doctype, doc.name)
|
||||||
|
|
||||||
def insert_existing_sle(warehouse):
|
def insert_existing_sle(warehouse):
|
||||||
from erpnext.stock.doctype.stock_entry.test_stock_entry import make_stock_entry
|
from erpnext.stock.doctype.stock_entry.test_stock_entry import make_stock_entry
|
||||||
|
|
||||||
@@ -354,14 +407,24 @@ def create_stock_reconciliation(**args):
|
|||||||
or frappe.get_cached_value("Company", sr.company, "cost_center") \
|
or frappe.get_cached_value("Company", sr.company, "cost_center") \
|
||||||
or "_Test Cost Center - _TC"
|
or "_Test Cost Center - _TC"
|
||||||
|
|
||||||
sr.append("items", {
|
if not args.batch_details:
|
||||||
"item_code": args.item_code or "_Test Item",
|
sr.append("items", {
|
||||||
"warehouse": args.warehouse or "_Test Warehouse - _TC",
|
"item_code": args.item_code or "_Test Item",
|
||||||
"qty": args.qty,
|
"warehouse": args.warehouse or "_Test Warehouse - _TC",
|
||||||
"valuation_rate": args.rate,
|
"qty": args.qty,
|
||||||
"serial_no": args.serial_no,
|
"valuation_rate": args.rate,
|
||||||
"batch_no": args.batch_no
|
"serial_no": args.serial_no,
|
||||||
})
|
"batch_no": args.batch_no
|
||||||
|
})
|
||||||
|
elif args.batch_details:
|
||||||
|
for batch, data in args.batch_details.items():
|
||||||
|
sr.append("items", {
|
||||||
|
"item_code": args.item_code or "_Test Item",
|
||||||
|
"warehouse": args.warehouse or "_Test Warehouse - _TC",
|
||||||
|
"qty": data[0],
|
||||||
|
"valuation_rate": data[1],
|
||||||
|
"batch_no": batch
|
||||||
|
})
|
||||||
|
|
||||||
if not args.do_not_save:
|
if not args.do_not_save:
|
||||||
sr.insert()
|
sr.insert()
|
||||||
@@ -370,6 +433,7 @@ def create_stock_reconciliation(**args):
|
|||||||
sr.submit()
|
sr.submit()
|
||||||
except EmptyStockReconciliationItemsError:
|
except EmptyStockReconciliationItemsError:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
return sr
|
return sr
|
||||||
|
|
||||||
def set_valuation_method(item_code, valuation_method):
|
def set_valuation_method(item_code, valuation_method):
|
||||||
|
|||||||
Reference in New Issue
Block a user