Skip to content

Commit

Permalink
fix: incorrect available qty for backdated stock reco with batch (#37858
Browse files Browse the repository at this point in the history
)

* fix: incorrect available qty for backdated stock reco with batch

* test: added test case
  • Loading branch information
rohitwaghchaure authored Nov 3, 2023
1 parent 469ae2c commit d4c0dbf
Show file tree
Hide file tree
Showing 7 changed files with 229 additions and 80 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -121,7 +121,7 @@ def validate_serial_nos_duplicate(self):
def throw_error_message(self, message, exception=frappe.ValidationError):
frappe.throw(_(message), exception, title=_("Error"))

def set_incoming_rate(self, row=None, save=False):
def set_incoming_rate(self, row=None, save=False, allow_negative_stock=False):
if self.type_of_transaction not in ["Inward", "Outward"] or self.voucher_type in [
"Installation Note",
"Job Card",
Expand All @@ -131,7 +131,9 @@ def set_incoming_rate(self, row=None, save=False):
return

if self.type_of_transaction == "Outward":
self.set_incoming_rate_for_outward_transaction(row, save)
self.set_incoming_rate_for_outward_transaction(
row, save, allow_negative_stock=allow_negative_stock
)
else:
self.set_incoming_rate_for_inward_transaction(row, save)

Expand All @@ -152,7 +154,9 @@ def calculate_total_qty(self, save=True):
def get_serial_nos(self):
return [d.serial_no for d in self.entries if d.serial_no]

def set_incoming_rate_for_outward_transaction(self, row=None, save=False):
def set_incoming_rate_for_outward_transaction(
self, row=None, save=False, allow_negative_stock=False
):
sle = self.get_sle_for_outward_transaction()

if self.has_serial_no:
Expand Down Expand Up @@ -181,7 +185,8 @@ def set_incoming_rate_for_outward_transaction(self, row=None, save=False):
if self.docstatus == 1:
available_qty += flt(d.qty)

self.validate_negative_batch(d.batch_no, available_qty)
if not allow_negative_stock:
self.validate_negative_batch(d.batch_no, available_qty)

d.stock_value_difference = flt(d.qty) * flt(d.incoming_rate)

Expand Down
137 changes: 98 additions & 39 deletions erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
import frappe
from frappe import _, bold, msgprint
from frappe.query_builder.functions import CombineDatetime, Sum
from frappe.utils import cint, cstr, flt
from frappe.utils import add_to_date, cint, cstr, flt

import erpnext
from erpnext.accounts.utils import get_company_default
Expand Down Expand Up @@ -88,9 +88,12 @@ def on_cancel(self):
self.repost_future_sle_and_gle()
self.delete_auto_created_batches()

def set_current_serial_and_batch_bundle(self):
def set_current_serial_and_batch_bundle(self, voucher_detail_no=None, save=False) -> None:
"""Set Serial and Batch Bundle for each item"""
for item in self.items:
if voucher_detail_no and voucher_detail_no != item.name:
continue

item_details = frappe.get_cached_value(
"Item", item.item_code, ["has_serial_no", "has_batch_no"], as_dict=1
)
Expand Down Expand Up @@ -148,6 +151,7 @@ def set_current_serial_and_batch_bundle(self):
"warehouse": item.warehouse,
"posting_date": self.posting_date,
"posting_time": self.posting_time,
"ignore_voucher_nos": [self.name],
}
)
)
Expand All @@ -163,11 +167,36 @@ def set_current_serial_and_batch_bundle(self):
)

if not serial_and_batch_bundle.entries:
if voucher_detail_no:
return

continue

item.current_serial_and_batch_bundle = serial_and_batch_bundle.save().name
serial_and_batch_bundle.save()
item.current_serial_and_batch_bundle = serial_and_batch_bundle.name
item.current_qty = abs(serial_and_batch_bundle.total_qty)
item.current_valuation_rate = abs(serial_and_batch_bundle.avg_rate)
if save:
sle_creation = frappe.db.get_value(
"Serial and Batch Bundle", item.serial_and_batch_bundle, "creation"
)
creation = add_to_date(sle_creation, seconds=-1)
item.db_set(
{
"current_serial_and_batch_bundle": item.current_serial_and_batch_bundle,
"current_qty": item.current_qty,
"current_valuation_rate": item.current_valuation_rate,
"creation": creation,
}
)

serial_and_batch_bundle.db_set(
{
"creation": creation,
"voucher_no": self.name,
"voucher_detail_no": voucher_detail_no,
}
)

def set_new_serial_and_batch_bundle(self):
for item in self.items:
Expand Down Expand Up @@ -689,56 +718,84 @@ def cancel(self):
else:
self._cancel()

def recalculate_current_qty(self, item_code, batch_no):
def recalculate_current_qty(self, voucher_detail_no, sle_creation, add_new_sle=False):
from erpnext.stock.stock_ledger import get_valuation_rate

sl_entries = []

for row in self.items:
if (
not (row.item_code == item_code and row.batch_no == batch_no)
and not row.serial_and_batch_bundle
):
if voucher_detail_no != row.name:
continue

current_qty = 0.0
if row.current_serial_and_batch_bundle:
self.recalculate_qty_for_serial_and_batch_bundle(row)
continue

current_qty = get_batch_qty_for_stock_reco(
item_code, row.warehouse, batch_no, self.posting_date, self.posting_time, self.name
)
current_qty = self.get_qty_for_serial_and_batch_bundle(row)
elif row.batch_no:
current_qty = get_batch_qty_for_stock_reco(
row.item_code, row.warehouse, row.batch_no, self.posting_date, self.posting_time, self.name
)

precesion = row.precision("current_qty")
if flt(current_qty, precesion) == flt(row.current_qty, precesion):
continue

val_rate = get_valuation_rate(
item_code, row.warehouse, self.doctype, self.name, company=self.company, batch_no=batch_no
)
if flt(current_qty, precesion) != flt(row.current_qty, precesion):
val_rate = get_valuation_rate(
row.item_code,
row.warehouse,
self.doctype,
self.name,
company=self.company,
batch_no=row.batch_no,
serial_and_batch_bundle=row.current_serial_and_batch_bundle,
)

row.current_valuation_rate = val_rate
if not row.current_qty and current_qty:
sle = self.get_sle_for_items(row)
sle.actual_qty = current_qty * -1
sle.valuation_rate = val_rate
sl_entries.append(sle)
row.current_valuation_rate = val_rate
row.current_qty = current_qty
row.db_set(
{
"current_qty": row.current_qty,
"current_valuation_rate": row.current_valuation_rate,
"current_amount": flt(row.current_qty * row.current_valuation_rate),
}
)

row.current_qty = current_qty
row.db_set(
{
"current_qty": row.current_qty,
"current_valuation_rate": row.current_valuation_rate,
"current_amount": flt(row.current_qty * row.current_valuation_rate),
}
)
if (
add_new_sle
and not frappe.db.get_value(
"Stock Ledger Entry",
{"voucher_detail_no": row.name, "actual_qty": ("<", 0), "is_cancelled": 0},
"name",
)
and (not row.current_serial_and_batch_bundle and not row.batch_no)
):
self.set_current_serial_and_batch_bundle(voucher_detail_no, save=True)
row.reload()

if row.current_qty > 0 and row.current_serial_and_batch_bundle:
new_sle = self.get_sle_for_items(row)
new_sle.actual_qty = row.current_qty * -1
new_sle.valuation_rate = row.current_valuation_rate
new_sle.creation_time = add_to_date(sle_creation, seconds=-1)
new_sle.serial_and_batch_bundle = row.current_serial_and_batch_bundle
new_sle.qty_after_transaction = 0.0
sl_entries.append(new_sle)

if sl_entries:
self.make_sl_entries(sl_entries, allow_negative_stock=True)
self.make_sl_entries(sl_entries, allow_negative_stock=self.has_negative_stock_allowed())
if not frappe.db.exists("Repost Item Valuation", {"voucher_no": self.name, "status": "Queued"}):
self.repost_future_sle_and_gle(force=True)

def has_negative_stock_allowed(self):
allow_negative_stock = cint(frappe.db.get_single_value("Stock Settings", "allow_negative_stock"))

def recalculate_qty_for_serial_and_batch_bundle(self, row):
if all(d.serial_and_batch_bundle and flt(d.qty) == flt(d.current_qty) for d in self.items):
allow_negative_stock = True

return allow_negative_stock

def get_qty_for_serial_and_batch_bundle(self, row):
doc = frappe.get_doc("Serial and Batch Bundle", row.current_serial_and_batch_bundle)
precision = doc.entries[0].precision("qty")

current_qty = 0
for d in doc.entries:
qty = (
get_batch_qty(
Expand All @@ -751,10 +808,12 @@ def recalculate_qty_for_serial_and_batch_bundle(self, row):
or 0
) * -1

if flt(d.qty, precision) == flt(qty, precision):
continue
if flt(d.qty, precision) != flt(qty, precision):
d.db_set("qty", qty)

current_qty += qty

d.db_set("qty", qty)
return abs(current_qty)


def get_batch_qty_for_stock_reco(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -742,13 +742,6 @@ def test_backdated_stock_reco_entry(self):

se2.cancel()

self.assertTrue(frappe.db.exists("Repost Item Valuation", {"voucher_no": stock_reco.name}))

self.assertEqual(
frappe.db.get_value("Repost Item Valuation", {"voucher_no": stock_reco.name}, "status"),
"Completed",
)

sle = frappe.get_all(
"Stock Ledger Entry",
filters={"item_code": item_code, "warehouse": warehouse, "is_cancelled": 0},
Expand All @@ -766,6 +759,68 @@ def test_backdated_stock_reco_entry(self):

self.assertEqual(flt(sle[0].actual_qty), flt(-100.0))

def test_backdated_stock_reco_entry_with_batch(self):
from erpnext.stock.doctype.stock_entry.test_stock_entry import make_stock_entry

item_code = self.make_item(
"Test New Batch Item ABCVSD",
{
"is_stock_item": 1,
"has_batch_no": 1,
"batch_number_series": "BNS9.####",
"create_new_batch": 1,
},
).name

warehouse = "_Test Warehouse - _TC"

# Stock Reco for 100, Balace Qty 100
stock_reco = create_stock_reconciliation(
item_code=item_code,
posting_date=nowdate(),
posting_time="11:00:00",
warehouse=warehouse,
qty=100,
rate=100,
)

sles = frappe.get_all(
"Stock Ledger Entry",
fields=["actual_qty"],
filters={"voucher_no": stock_reco.name, "is_cancelled": 0},
)

self.assertEqual(len(sles), 1)

stock_reco.reload()
batch_no = get_batch_from_bundle(stock_reco.items[0].serial_and_batch_bundle)

# Stock Reco for 100, Balace Qty 100
stock_reco1 = create_stock_reconciliation(
item_code=item_code,
posting_date=add_days(nowdate(), -1),
posting_time="11:00:00",
batch_no=batch_no,
warehouse=warehouse,
qty=60,
rate=100,
)

sles = frappe.get_all(
"Stock Ledger Entry",
fields=["actual_qty"],
filters={"voucher_no": stock_reco.name, "is_cancelled": 0},
)

stock_reco1.reload()
new_batch_no = get_batch_from_bundle(stock_reco1.items[0].serial_and_batch_bundle)

self.assertEqual(len(sles), 2)

for row in sles:
if row.actual_qty < 0:
self.assertEqual(row.actual_qty, -60)

def test_update_stock_reconciliation_while_reposting(self):
from erpnext.stock.doctype.stock_entry.test_stock_entry import make_stock_entry

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -205,6 +205,7 @@
"fieldname": "current_serial_and_batch_bundle",
"fieldtype": "Link",
"label": "Current Serial / Batch Bundle",
"no_copy": 1,
"options": "Serial and Batch Bundle",
"read_only": 1
},
Expand All @@ -216,7 +217,7 @@
],
"istable": 1,
"links": [],
"modified": "2023-07-26 12:54:34.011915",
"modified": "2023-11-02 15:47:07.929550",
"modified_by": "Administrator",
"module": "Stock",
"name": "Stock Reconciliation Item",
Expand Down
8 changes: 8 additions & 0 deletions erpnext/stock/report/stock_ledger/stock_ledger.py
Original file line number Diff line number Diff line change
Expand Up @@ -249,6 +249,13 @@ def get_columns(filters):
"options": "Serial No",
"width": 100,
},
{
"label": _("Serial and Batch Bundle"),
"fieldname": "serial_and_batch_bundle",
"fieldtype": "Link",
"options": "Serial and Batch Bundle",
"width": 100,
},
{"label": _("Balance Serial No"), "fieldname": "balance_serial_no", "width": 100},
{
"label": _("Project"),
Expand Down Expand Up @@ -287,6 +294,7 @@ def get_stock_ledger_entries(filters, items):
sle.voucher_type,
sle.qty_after_transaction,
sle.stock_value_difference,
sle.serial_and_batch_bundle,
sle.voucher_no,
sle.stock_value,
sle.batch_no,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
"stock_value_difference",
"valuation_rate",
"voucher_detail_no",
"serial_and_batch_bundle",
)


Expand Down Expand Up @@ -64,7 +65,11 @@ def add_invariant_check_fields(sles):

balance_qty += sle.actual_qty
balance_stock_value += sle.stock_value_difference
if sle.voucher_type == "Stock Reconciliation" and not sle.batch_no:
if (
sle.voucher_type == "Stock Reconciliation"
and not sle.batch_no
and not sle.serial_and_batch_bundle
):
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
Expand Down Expand Up @@ -143,6 +148,12 @@ def get_columns():
"label": _("Batch"),
"options": "Batch",
},
{
"fieldname": "serial_and_batch_bundle",
"fieldtype": "Link",
"label": _("Serial and Batch Bundle"),
"options": "Serial and Batch Bundle",
},
{
"fieldname": "use_batchwise_valuation",
"fieldtype": "Check",
Expand Down
Loading

0 comments on commit d4c0dbf

Please sign in to comment.