Stock Reconciliation logic simplified

This commit is contained in:
Nabin Hait 2014-10-07 11:25:04 +05:30
parent e0c83e22d9
commit b96c014daf
7 changed files with 75 additions and 125 deletions

View File

@ -138,9 +138,17 @@ erpnext.StockAnalytics = erpnext.StockGridReport.extend({
item.valuation_method : sys_defaults.valuation_method; item.valuation_method : sys_defaults.valuation_method;
var is_fifo = valuation_method == "FIFO"; var is_fifo = valuation_method == "FIFO";
var diff = me.get_value_diff(wh, sl, is_fifo); if(sl.voucher_type=="Stock Reconciliation") {
var diff = (sl.qty_after_transaction * sl.valuation_rate) - item.closing_qty_value;
} else {
var diff = me.get_value_diff(wh, sl, is_fifo);
}
} else { } else {
var diff = sl.qty; if(sl.voucher_type=="Stock Reconciliation") {
var diff = sl.qty_after_transaction - item.closing_qty_value;
} else {
var diff = sl.qty;
}
} }
if(posting_datetime < from_date) { if(posting_datetime < from_date) {
@ -150,6 +158,8 @@ erpnext.StockAnalytics = erpnext.StockGridReport.extend({
} else { } else {
break; break;
} }
item.closing_qty_value += diff;
} }
} }
}, },

View File

@ -78,7 +78,8 @@ data_map = {
"Stock Ledger Entry": { "Stock Ledger Entry": {
"columns": ["name", "posting_date", "posting_time", "item_code", "warehouse", "columns": ["name", "posting_date", "posting_time", "item_code", "warehouse",
"actual_qty as qty", "voucher_type", "voucher_no", "project", "actual_qty as qty", "voucher_type", "voucher_no", "project",
"ifnull(incoming_rate,0) as incoming_rate", "stock_uom", "serial_no"], "ifnull(incoming_rate,0) as incoming_rate", "stock_uom", "serial_no",
"qty_after_transaction", "valuation_rate"],
"order_by": "posting_date, posting_time, name", "order_by": "posting_date, posting_time, name",
"links": { "links": {
"item_code": ["Item", "name"], "item_code": ["Item", "name"],

View File

@ -11,27 +11,27 @@ class Bin(Document):
def validate(self): def validate(self):
if self.get("__islocal") or not self.stock_uom: if self.get("__islocal") or not self.stock_uom:
self.stock_uom = frappe.db.get_value('Item', self.item_code, 'stock_uom') self.stock_uom = frappe.db.get_value('Item', self.item_code, 'stock_uom')
self.validate_mandatory() self.validate_mandatory()
self.projected_qty = flt(self.actual_qty) + flt(self.ordered_qty) + \ self.projected_qty = flt(self.actual_qty) + flt(self.ordered_qty) + \
flt(self.indented_qty) + flt(self.planned_qty) - flt(self.reserved_qty) flt(self.indented_qty) + flt(self.planned_qty) - flt(self.reserved_qty)
def validate_mandatory(self): def validate_mandatory(self):
qf = ['actual_qty', 'reserved_qty', 'ordered_qty', 'indented_qty'] qf = ['actual_qty', 'reserved_qty', 'ordered_qty', 'indented_qty']
for f in qf: for f in qf:
if (not getattr(self, f, None)) or (not self.get(f)): if (not getattr(self, f, None)) or (not self.get(f)):
self.set(f, 0.0) self.set(f, 0.0)
def update_stock(self, args): def update_stock(self, args):
self.update_qty(args) self.update_qty(args)
if args.get("actual_qty"): if args.get("actual_qty"):
from erpnext.stock.stock_ledger import update_entries_after from erpnext.stock.stock_ledger import update_entries_after
if not args.get("posting_date"): if not args.get("posting_date"):
args["posting_date"] = nowdate() args["posting_date"] = nowdate()
# update valuation and qty after transaction for post dated entry # update valuation and qty after transaction for post dated entry
update_entries_after({ update_entries_after({
"item_code": self.item_code, "item_code": self.item_code,
@ -39,21 +39,24 @@ class Bin(Document):
"posting_date": args.get("posting_date"), "posting_date": args.get("posting_date"),
"posting_time": args.get("posting_time") "posting_time": args.get("posting_time")
}) })
def update_qty(self, args): def update_qty(self, args):
# update the stock values (for current quantities) # update the stock values (for current quantities)
if args.get("voucher_type")=="Stock Reconciliation":
self.actual_qty = flt(self.actual_qty) + flt(args.get("actual_qty")) self.actual_qty = args.get("qty_after_transaction")
else:
self.actual_qty = flt(self.actual_qty) + flt(args.get("actual_qty"))
self.ordered_qty = flt(self.ordered_qty) + flt(args.get("ordered_qty")) self.ordered_qty = flt(self.ordered_qty) + flt(args.get("ordered_qty"))
self.reserved_qty = flt(self.reserved_qty) + flt(args.get("reserved_qty")) self.reserved_qty = flt(self.reserved_qty) + flt(args.get("reserved_qty"))
self.indented_qty = flt(self.indented_qty) + flt(args.get("indented_qty")) self.indented_qty = flt(self.indented_qty) + flt(args.get("indented_qty"))
self.planned_qty = flt(self.planned_qty) + flt(args.get("planned_qty")) self.planned_qty = flt(self.planned_qty) + flt(args.get("planned_qty"))
self.projected_qty = flt(self.actual_qty) + flt(self.ordered_qty) + \ self.projected_qty = flt(self.actual_qty) + flt(self.ordered_qty) + \
flt(self.indented_qty) + flt(self.planned_qty) - flt(self.reserved_qty) flt(self.indented_qty) + flt(self.planned_qty) - flt(self.reserved_qty)
self.save() self.save()
def get_first_sle(self): def get_first_sle(self):
sle = frappe.db.sql(""" sle = frappe.db.sql("""
select * from `tabStock Ledger Entry` select * from `tabStock Ledger Entry`
@ -62,4 +65,4 @@ class Bin(Document):
order by timestamp(posting_date, posting_time) asc, name asc order by timestamp(posting_date, posting_time) asc, name asc
limit 1 limit 1
""", (self.item_code, self.warehouse), as_dict=1) """, (self.item_code, self.warehouse), as_dict=1)
return sle and sle[0] or None return sle and sle[0] or None

View File

@ -44,7 +44,7 @@ class StockLedgerEntry(Document):
formatdate(self.posting_date), self.posting_time)) formatdate(self.posting_date), self.posting_time))
def validate_mandatory(self): def validate_mandatory(self):
mandatory = ['warehouse','posting_date','voucher_type','voucher_no','actual_qty','company'] mandatory = ['warehouse','posting_date','voucher_type','voucher_no','company']
for k in mandatory: for k in mandatory:
if not self.get(k): if not self.get(k):
frappe.throw(_("{0} is required").format(self.meta.get_label(k))) frappe.throw(_("{0} is required").format(self.meta.get_label(k)))

View File

@ -129,7 +129,6 @@ class StockReconciliation(StockController):
def insert_stock_ledger_entries(self): def insert_stock_ledger_entries(self):
""" find difference between current and expected entries """ find difference between current and expected entries
and create stock ledger entries based on the difference""" and create stock ledger entries based on the difference"""
from erpnext.stock.utils import get_valuation_method
from erpnext.stock.stock_ledger import get_previous_sle from erpnext.stock.stock_ledger import get_previous_sle
row_template = ["item_code", "warehouse", "qty", "valuation_rate"] row_template = ["item_code", "warehouse", "qty", "valuation_rate"]
@ -141,105 +140,27 @@ class StockReconciliation(StockController):
for row_num, row in enumerate(data[data.index(self.head_row)+1:]): for row_num, row in enumerate(data[data.index(self.head_row)+1:]):
row = frappe._dict(zip(row_template, row)) row = frappe._dict(zip(row_template, row))
row["row_num"] = row_num row["row_num"] = row_num
previous_sle = get_previous_sle({
"item_code": row.item_code,
"warehouse": row.warehouse,
"posting_date": self.posting_date,
"posting_time": self.posting_time
})
# check valuation rate mandatory if row.qty in ("", None) or row.valuation_rate in ("", None):
if row.qty not in ["", None] and not row.valuation_rate and \ previous_sle = get_previous_sle({
flt(previous_sle.get("qty_after_transaction")) <= 0: "item_code": row.item_code,
"warehouse": row.warehouse,
"posting_date": self.posting_date,
"posting_time": self.posting_time
})
if row.qty in ("", None):
row.qty = previous_sle.get("qty_after_transaction")
if row.valuation_rate in ("", None):
row.valuation_rate = previous_sle.get("valuation_rate")
if row.qty and not row.valuation_rate:
frappe.throw(_("Valuation Rate required for Item {0}").format(row.item_code)) frappe.throw(_("Valuation Rate required for Item {0}").format(row.item_code))
change_in_qty = row.qty not in ["", None] and \ self.insert_entries(row)
(flt(row.qty) - flt(previous_sle.get("qty_after_transaction")))
change_in_rate = row.valuation_rate not in ["", None] and \ def insert_entries(self, row):
(flt(row.valuation_rate) - flt(previous_sle.get("valuation_rate")))
if get_valuation_method(row.item_code) == "Moving Average":
self.sle_for_moving_avg(row, previous_sle, change_in_qty, change_in_rate)
else:
self.sle_for_fifo(row, previous_sle, change_in_qty, change_in_rate)
def sle_for_moving_avg(self, row, previous_sle, change_in_qty, change_in_rate):
"""Insert Stock Ledger Entries for Moving Average valuation"""
def _get_incoming_rate(qty, valuation_rate, previous_qty, previous_valuation_rate):
if previous_valuation_rate == 0:
return flt(valuation_rate)
else:
if valuation_rate in ["", None]:
valuation_rate = previous_valuation_rate
return (qty * valuation_rate - previous_qty * previous_valuation_rate) \
/ flt(qty - previous_qty)
if change_in_qty:
# if change in qty, irrespective of change in rate
incoming_rate = _get_incoming_rate(flt(row.qty), flt(row.valuation_rate),
flt(previous_sle.get("qty_after_transaction")), flt(previous_sle.get("valuation_rate")))
row["voucher_detail_no"] = "Row: " + cstr(row.row_num) + "/Actual Entry"
self.insert_entries({"actual_qty": change_in_qty, "incoming_rate": incoming_rate}, row)
elif change_in_rate and flt(previous_sle.get("qty_after_transaction")) > 0:
# if no change in qty, but change in rate
# and positive actual stock before this reconciliation
incoming_rate = _get_incoming_rate(
flt(previous_sle.get("qty_after_transaction"))+1, flt(row.valuation_rate),
flt(previous_sle.get("qty_after_transaction")),
flt(previous_sle.get("valuation_rate")))
# +1 entry
row["voucher_detail_no"] = "Row: " + cstr(row.row_num) + "/Valuation Adjustment +1"
self.insert_entries({"actual_qty": 1, "incoming_rate": incoming_rate}, row)
# -1 entry
row["voucher_detail_no"] = "Row: " + cstr(row.row_num) + "/Valuation Adjustment -1"
self.insert_entries({"actual_qty": -1}, row)
def sle_for_fifo(self, row, previous_sle, change_in_qty, change_in_rate):
"""Insert Stock Ledger Entries for FIFO valuation"""
previous_stock_queue = json.loads(previous_sle.get("stock_queue") or "[]")
previous_stock_qty = sum((batch[0] for batch in previous_stock_queue))
previous_stock_value = sum((batch[0] * batch[1] for batch in \
previous_stock_queue))
def _insert_entries():
if previous_stock_queue != [[row.qty, row.valuation_rate]]:
# make entry as per attachment
if flt(row.qty):
row["voucher_detail_no"] = "Row: " + cstr(row.row_num) + "/Actual Entry"
self.insert_entries({"actual_qty": row.qty,
"incoming_rate": flt(row.valuation_rate)}, row)
# Make reverse entry
if previous_stock_qty:
row["voucher_detail_no"] = "Row: " + cstr(row.row_num) + "/Reverse Entry"
self.insert_entries({"actual_qty": -1 * previous_stock_qty,
"incoming_rate": previous_stock_qty < 0 and
flt(row.valuation_rate) or 0}, row)
if change_in_qty:
if row.valuation_rate in ["", None]:
# dont want change in valuation
if previous_stock_qty > 0:
# set valuation_rate as previous valuation_rate
row.valuation_rate = previous_stock_value / flt(previous_stock_qty)
_insert_entries()
elif change_in_rate and previous_stock_qty > 0:
# if no change in qty, but change in rate
# and positive actual stock before this reconciliation
row.qty = previous_stock_qty
_insert_entries()
def insert_entries(self, opts, row):
"""Insert Stock Ledger Entries""" """Insert Stock Ledger Entries"""
args = frappe._dict({ args = frappe._dict({
"doctype": "Stock Ledger Entry", "doctype": "Stock Ledger Entry",
@ -253,9 +174,10 @@ class StockReconciliation(StockController):
"stock_uom": frappe.db.get_value("Item", row.item_code, "stock_uom"), "stock_uom": frappe.db.get_value("Item", row.item_code, "stock_uom"),
"voucher_detail_no": row.voucher_detail_no, "voucher_detail_no": row.voucher_detail_no,
"fiscal_year": self.fiscal_year, "fiscal_year": self.fiscal_year,
"is_cancelled": "No" "is_cancelled": "No",
"qty_after_transaction": row.qty,
"valuation_rate": row.valuation_rate
}) })
args.update(opts)
self.make_sl_entries([args]) self.make_sl_entries([args])
# append to entries # append to entries
@ -295,7 +217,7 @@ class StockReconciliation(StockController):
if not self.expense_account: if not self.expense_account:
msgprint(_("Please enter Expense Account"), raise_exception=1) msgprint(_("Please enter Expense Account"), raise_exception=1)
elif not frappe.db.sql("""select * from `tabStock Ledger Entry`"""): elif not frappe.db.sql("""select name from `tabStock Ledger Entry` limit 1"""):
if frappe.db.get_value("Account", self.expense_account, "report_type") == "Profit and Loss": if frappe.db.get_value("Account", self.expense_account, "report_type") == "Profit and Loss":
frappe.throw(_("Difference Account must be a 'Liability' type account, since this Stock Reconciliation is an Opening Entry")) frappe.throw(_("Difference Account must be a 'Liability' type account, since this Stock Reconciliation is an Opening Entry"))

View File

@ -104,8 +104,13 @@ erpnext.StockBalance = erpnext.StockAnalytics.extend({
item.valuation_method : sys_defaults.valuation_method; item.valuation_method : sys_defaults.valuation_method;
var is_fifo = valuation_method == "FIFO"; var is_fifo = valuation_method == "FIFO";
var qty_diff = sl.qty; if(sl.voucher_type=="Stock Reconciliation") {
var value_diff = me.get_value_diff(wh, sl, is_fifo); var qty_diff = sl.qty_after_trasaction - item.closing_qty;
var value_diff = (sl.valuation_rate * sl.qty_after_trasaction) - item.closing_value;
} else {
var qty_diff = sl.qty;
var value_diff = me.get_value_diff(wh, sl, is_fifo);
}
if(sl_posting_date < from_date) { if(sl_posting_date < from_date) {
item.opening_qty += qty_diff; item.opening_qty += qty_diff;

View File

@ -95,14 +95,23 @@ def update_entries_after(args, verbose=1):
qty_after_transaction += flt(sle.actual_qty) qty_after_transaction += flt(sle.actual_qty)
continue continue
if sle.serial_no: if sle.serial_no:
valuation_rate = get_serialized_values(qty_after_transaction, sle, valuation_rate) valuation_rate = get_serialized_values(qty_after_transaction, sle, valuation_rate)
elif valuation_method == "Moving Average": qty_after_transaction += flt(sle.actual_qty)
valuation_rate = get_moving_average_values(qty_after_transaction, sle, valuation_rate)
else:
valuation_rate = get_fifo_values(qty_after_transaction, sle, stock_queue)
qty_after_transaction += flt(sle.actual_qty) else:
if sle.voucher_type=="Stock Reconciliation":
valuation_rate = sle.valuation_rate
qty_after_transaction = sle.qty_after_transaction
stock_queue = [[qty_after_transaction, valuation_rate]]
else:
if valuation_method == "Moving Average":
valuation_rate = get_moving_average_values(qty_after_transaction, sle, valuation_rate)
else:
valuation_rate = get_fifo_values(qty_after_transaction, sle, stock_queue)
qty_after_transaction += flt(sle.actual_qty)
# get stock value # get stock value
if sle.serial_no: if sle.serial_no: