Maintain negative stock balance if balance qty is negative

This commit is contained in:
Nabin Hait 2014-10-09 19:25:03 +05:30
parent b7e5ad0a31
commit 4d74216147
8 changed files with 118 additions and 50 deletions

View File

@ -269,7 +269,7 @@ class BuyingController(StockController):
# get raw materials rate
if self.doctype == "Purchase Receipt":
from erpnext.stock.utils import get_incoming_rate
rm.rate = get_incoming_rate({
item_rate = get_incoming_rate({
"item_code": bom_item.item_code,
"warehouse": self.supplier_warehouse,
"posting_date": self.posting_date,
@ -277,6 +277,7 @@ class BuyingController(StockController):
"qty": -1 * required_qty,
"serial_no": rm.serial_no
})
rm.rate = item_rate or bom_item.rate
else:
rm.rate = bom_item.rate

View File

@ -305,9 +305,15 @@ def get_valuation_rate(item_code, warehouse):
last_valuation_rate = frappe.db.sql("""select valuation_rate
from `tabStock Ledger Entry`
where item_code = %s and warehouse = %s
and ifnull(qty_after_transaction, 0) > 0
and ifnull(valuation_rate, 0) > 0
order by posting_date desc, posting_time desc, name desc limit 1""", (item_code, warehouse))
if not last_valuation_rate:
last_valuation_rate = frappe.db.sql("""select valuation_rate
from `tabStock Ledger Entry`
where item_code = %s and ifnull(valuation_rate, 0) > 0
order by posting_date desc, posting_time desc, name desc limit 1""", item_code)
valuation_rate = flt(last_valuation_rate[0][0]) if last_valuation_rate else 0
if not valuation_rate:

View File

@ -9,14 +9,64 @@ from erpnext.stock.doctype.serial_no.serial_no import *
from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import set_perpetual_inventory
from erpnext.stock.doctype.stock_ledger_entry.stock_ledger_entry import StockFreezeError
class TestStockEntry(unittest.TestCase):
def get_sle(**args):
condition, values = "", []
for key, value in args.iteritems():
condition += " and " if condition else " where "
condition += "`{0}`=%s".format(key)
values.append(value)
return frappe.db.sql("""select * from `tabStock Ledger Entry` %s
order by timestamp(posting_date, posting_time) desc, name desc limit 1"""% condition,
values, as_dict=1)
def make_zero(item_code, warehouse):
sle = get_sle(item_code = item_code, warehouse = warehouse)
qty = sle[0].qty_after_transaction if sle else 0
if qty < 0:
make_stock_entry(item_code, None, warehouse, abs(qty), incoming_rate=10)
elif qty > 0:
make_stock_entry(item_code, warehouse, None, qty, incoming_rate=10)
class TestStockEntry(unittest.TestCase):
def tearDown(self):
frappe.set_user("Administrator")
set_perpetual_inventory(0)
if hasattr(self, "old_default_company"):
frappe.db.set_default("company", self.old_default_company)
def test_fifo(self):
frappe.db.set_default("allow_negative_stock", 1)
item_code = "_Test Item 2"
warehouse = "_Test Warehouse - _TC"
make_zero(item_code, warehouse)
make_stock_entry(item_code, None, warehouse, 1, incoming_rate=10)
sle = get_sle(item_code = item_code, warehouse = warehouse)[0]
self.assertEqual([[1, 10]], eval(sle.stock_queue))
# negative qty
make_zero(item_code, warehouse)
make_stock_entry(item_code, warehouse, None, 1, incoming_rate=10)
sle = get_sle(item_code = item_code, warehouse = warehouse)[0]
self.assertEqual([[-1, 10]], eval(sle.stock_queue))
# further negative
make_stock_entry(item_code, warehouse, None, 1)
sle = get_sle(item_code = item_code, warehouse = warehouse)[0]
self.assertEqual([[-2, 10]], eval(sle.stock_queue))
# move stock to positive
make_stock_entry(item_code, None, warehouse, 3, incoming_rate=10)
sle = get_sle(item_code = item_code, warehouse = warehouse)[0]
self.assertEqual([[1, 10]], eval(sle.stock_queue))
frappe.db.set_default("allow_negative_stock", 0)
def test_auto_material_request(self):
frappe.db.sql("""delete from `tabMaterial Request Item`""")
frappe.db.sql("""delete from `tabMaterial Request`""")
@ -889,5 +939,3 @@ def make_stock_entry(item, source, target, qty, incoming_rate=None):
s.insert()
s.submit()
return s
test_records = frappe.get_test_records('Stock Entry')

View File

@ -28,7 +28,7 @@ class TestStockReconciliation(unittest.TestCase):
[20, "", "2012-12-26", "12:05", 16000, 15, 18000],
[10, 2000, "2012-12-26", "12:10", 20000, 5, 6000],
[1, 1000, "2012-12-01", "00:00", 1000, 11, 13200],
[0, "", "2012-12-26", "12:10", 0, -5, 0]
[0, "", "2012-12-26", "12:10", 0, -5, -6000]
]
for d in input_data:
@ -63,16 +63,16 @@ class TestStockReconciliation(unittest.TestCase):
input_data = [
[50, 1000, "2012-12-26", "12:00", 50000, 45, 48000],
[5, 1000, "2012-12-26", "12:00", 5000, 0, 0],
[15, 1000, "2012-12-26", "12:00", 15000, 10, 12000],
[15, 1000, "2012-12-26", "12:00", 15000, 10, 11500],
[25, 900, "2012-12-26", "12:00", 22500, 20, 22500],
[20, 500, "2012-12-26", "12:00", 10000, 15, 18000],
[50, 1000, "2013-01-01", "12:00", 50000, 65, 68000],
[5, 1000, "2013-01-01", "12:00", 5000, 20, 23000],
["", 1000, "2012-12-26", "12:05", 15000, 10, 12000],
["", 1000, "2012-12-26", "12:05", 15000, 10, 11500],
[20, "", "2012-12-26", "12:05", 18000, 15, 18000],
[10, 2000, "2012-12-26", "12:10", 20000, 5, 6000],
[1, 1000, "2012-12-01", "00:00", 1000, 11, 13200],
[0, "", "2012-12-26", "12:10", 0, -5, 0]
[10, 2000, "2012-12-26", "12:10", 20000, 5, 7600],
[1, 1000, "2012-12-01", "00:00", 1000, 11, 12512.73],
[0, "", "2012-12-26", "12:10", 0, -5, -5142.86]
]

View File

@ -126,6 +126,7 @@ erpnext.StockBalance = erpnext.StockAnalytics.extend({
&& this.stock_entry_map[sl.voucher_no].purpose=="Material Transfer";
if(!ignore_inflow_outflow) {
if(qty_diff < 0) {
item.outflow_qty += Math.abs(qty_diff);
} else {

View File

@ -106,18 +106,19 @@ def update_entries_after(args, verbose=1):
stock_queue = [[qty_after_transaction, valuation_rate]]
else:
if valuation_method == "Moving Average":
if flt(sle.actual_qty) > 0:
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
if sle.serial_no:
stock_value = qty_after_transaction * valuation_rate
elif valuation_method == "Moving Average":
stock_value = (qty_after_transaction > 0) and \
(qty_after_transaction * valuation_rate) or 0
stock_value = qty_after_transaction * valuation_rate
else:
stock_value = sum((flt(batch[0]) * flt(batch[1]) for batch in stock_queue))
@ -256,64 +257,73 @@ def get_moving_average_values(qty_after_transaction, sle, valuation_rate):
actual_qty = flt(sle.actual_qty)
if not incoming_rate:
# In case of delivery/stock issue in_rate = 0 or wrong incoming rate
# If wrong incoming rate
incoming_rate = valuation_rate
elif qty_after_transaction < 0:
elif qty_after_transaction < 0 and not valuation_rate:
# if negative stock, take current valuation rate as incoming rate
valuation_rate = incoming_rate
new_stock_qty = qty_after_transaction + actual_qty
new_stock_value = qty_after_transaction * valuation_rate + actual_qty * incoming_rate
new_stock_qty = abs(qty_after_transaction) + actual_qty
new_stock_value = (abs(qty_after_transaction) * valuation_rate) + (actual_qty * incoming_rate)
if new_stock_qty > 0 and new_stock_value > 0:
if new_stock_qty:
valuation_rate = new_stock_value / flt(new_stock_qty)
elif new_stock_qty <= 0:
valuation_rate = 0.0
# NOTE: val_rate is same as previous entry if new stock value is negative
return valuation_rate
return abs(valuation_rate)
def get_fifo_values(qty_after_transaction, sle, stock_queue):
incoming_rate = flt(sle.incoming_rate)
actual_qty = flt(sle.actual_qty)
if not stock_queue:
stock_queue.append([0, 0])
intialize_stock_queue(stock_queue, sle.item_code, sle.warehouse)
if actual_qty > 0:
if stock_queue[-1][0] > 0:
stock_queue.append([actual_qty, incoming_rate])
else:
qty = stock_queue[-1][0] + actual_qty
stock_queue[-1] = [qty, qty > 0 and incoming_rate or 0]
if qty == 0:
stock_queue.pop(-1)
else:
stock_queue[-1] = [qty, incoming_rate]
else:
incoming_cost = 0
qty_to_pop = abs(actual_qty)
while qty_to_pop:
if not stock_queue:
stock_queue.append([0, 0])
intialize_stock_queue(stock_queue, sle.item_code, sle.warehouse)
batch = stock_queue[0]
if 0 < batch[0] <= qty_to_pop:
# if batch qty > 0
# not enough or exactly same qty in current batch, clear batch
incoming_cost += flt(batch[0]) * flt(batch[1])
qty_to_pop -= batch[0]
# print qty_to_pop, batch
if qty_to_pop >= batch[0]:
# consume current batch
qty_to_pop = qty_to_pop - batch[0]
stock_queue.pop(0)
if not stock_queue and qty_to_pop:
# stock finished, qty still remains to be withdrawn
# negative stock, keep in as a negative batch
stock_queue.append([-qty_to_pop, batch[1]])
break
else:
# all from current batch
incoming_cost += flt(qty_to_pop) * flt(batch[1])
batch[0] -= qty_to_pop
# qty found in current batch
# consume it and exit
batch[0] = batch[0] - qty_to_pop
qty_to_pop = 0
stock_value = sum((flt(batch[0]) * flt(batch[1]) for batch in stock_queue))
stock_qty = sum((flt(batch[0]) for batch in stock_queue))
valuation_rate = stock_qty and (stock_value / flt(stock_qty)) or 0
valuation_rate = (stock_value / flt(stock_qty)) if stock_qty else 0
return valuation_rate
return abs(valuation_rate)
def intialize_stock_queue(stock_queue, item_code, warehouse):
if not stock_queue:
from erpnext.controllers.stock_controller import get_valuation_rate
estimated_val_rate = get_valuation_rate(item_code, warehouse)
stock_queue.append([0, estimated_val_rate])
def _raise_exceptions(args, verbose=1):
deficiency = min(e["diff"] for e in _exceptions)

View File

@ -5,7 +5,6 @@ import frappe
from frappe import _
import json
from frappe.utils import flt, cstr, nowdate, add_days, cint
from frappe.defaults import get_global_default
from frappe.utils.email_lib import sendmail
from erpnext.accounts.utils import get_fiscal_year, FiscalYearError
@ -94,7 +93,7 @@ def get_valuation_method(item_code):
"""get valuation method from item or default"""
val_method = frappe.db.get_value('Item', item_code, 'valuation_method')
if not val_method:
val_method = get_global_default('valuation_method') or "FIFO"
val_method = frappe.db.get_value("Stock Settings", None, "valuation_method") or "FIFO"
return val_method
def get_fifo_rate(previous_stock_queue, qty):

View File

@ -213,10 +213,13 @@ def repost_all_stock_vouchers():
vouchers = frappe.db.sql("""select distinct voucher_type, voucher_no
from `tabStock Ledger Entry` order by posting_date, posting_time, name""")
print len(vouchers)
rejected = []
# vouchers = [["Purchase Receipt", "GRN00062"]]
# vouchers = [["Delivery Note", "DN00060"]]
i = 0
for voucher_type, voucher_no in vouchers:
print voucher_type, voucher_no
i+=1
print i
try:
for dt in ["Stock Ledger Entry", "GL Entry"]:
frappe.db.sql("""delete from `tab%s` where voucher_type=%s and voucher_no=%s"""%