blob: d4475efa3a69e928f77ed5139d8b6ad38ed5bef5 [file] [log] [blame]
Nabin Hait902e8602013-01-08 18:29:24 +05301# ERPNext - web based ERP (http://erpnext.com)
2# Copyright (C) 2012 Web Notes Technologies Pvt Ltd
3#
4# This program is free software: you can redistribute it and/or modify
5# it under the terms of the GNU General Public License as published by
6# the Free Software Foundation, either version 3 of the License, or
7# (at your option) any later version.
8#
9# This program is distributed in the hope that it will be useful,
10# but WITHOUT ANY WARRANTY; without even the implied warranty of
11# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12# GNU General Public License for more details.
13#
14# You should have received a copy of the GNU General Public License
15# along with this program. If not, see <http://www.gnu.org/licenses/>.
16
17import webnotes
18from webnotes import msgprint, _
19from webnotes.utils import cint
20from stock.utils import _msgprint, get_valuation_method
21
22# future reposting
23
24_exceptions = []
25def update_entries_after(args, verbose=1):
26 """
27 update valution rate and qty after transaction
28 from the current time-bucket onwards
29
30 args = {
31 "item_code": "ABC",
32 "warehouse": "XYZ",
33 "posting_date": "2012-12-12",
34 "posting_time": "12:00"
35 }
36 """
37 previous_sle = get_sle_before_datetime(args)
38
39 qty_after_transaction = flt(previous_sle.get("qty_after_transaction"))
40 valuation_rate = flt(previous_sle.get("valuation_rate"))
41 stock_queue = json.loads(previous_sle.get("stock_queue") or "[]")
42
43 entries_to_fix = get_sle_after_datetime(previous_sle or \
44 {"item_code": args["item_code"], "warehouse": args["warehouse"]})
45
46 valuation_method = get_valuation_method(args["item_code"])
47
48 for sle in entries_to_fix:
49 if sle.serial_nos or valuation_method == "FIFO" or \
50 not cint(webnotes.conn.get_default("allow_negative_stock")):
51 # validate negative stock for serialized items, fifo valuation
52 # or when negative stock is not allowed for moving average
53 if not validate_negative_stock(qty_after_transaction, sle):
54 qty_after_transaction += flt(sle.actual_qty)
55 continue
56
57 if sle.serial_nos:
58 valuation_rate, incoming_rate = get_serialized_values(qty_after_transaction, sle,
59 valuation_rate)
60 elif valuation_method == "Moving Average":
61 valuation_rate, incoming_rate = get_moving_average_values(qty_after_transaction, sle,
62 valuation_rate)
63 else:
64 valuation_rate, incoming_rate = get_fifo_values(qty_after_transaction, sle,
65 stock_queue)
66
67 qty_after_transaction += flt(sle.actual_qty)
68
69 # get stock value
70 if serial_nos:
71 stock_value = qty_after_transaction * valuation_rate
72 elif valuation_method == "Moving Average":
73 stock_value = (qty_after_transaction > 0) and \
74 (qty_after_transaction * valuation_rate) or 0
75 else:
76 stock_value = sum((flt(batch[0]) * flt(batch[1]) for batch in stock_queue))
77
78 # update current sle
79 webnotes.conn.sql("""update `tabStock Ledger Entry`
80 set qty_after_transaction=%s, valuation_rate=%s, stock_queue=%s, stock_value=%s,
81 incoming_rate = %s where name=%s""", (qty_after_transaction, valuation_rate,
82 json.dumps(stock_queue), stock_value, incoming_rate, sle.name))
83
84 if _exceptions:
85 _raise_exceptions(args)
86
87 # update bin
88 webnotes.conn.sql("""update `tabBin` set valuation_rate=%s, actual_qty=%s, stock_value=%s,
89 projected_qty = (actual_qty + indented_qty + ordered_qty + planned_qty - reserved_qty)
90 where item_code=%s and warehouse=%s""", (valuation_rate, qty_after_transaction,
91 stock_value, args["item_code"], args["warehouse"]))
92
93def get_sle_before_datetime(args):
94 """
95 get previous stock ledger entry before current time-bucket
96
97 Details:
98 get the last sle before the current time-bucket, so that all values
99 are reposted from the current time-bucket onwards.
100 this is necessary because at the time of cancellation, there may be
101 entries between the cancelled entries in the same time-bucket
102 """
103 sle = get_stock_ledger_entries(args,
104 ["timestamp(posting_date, posting_time) < timestamp(%%(posting_date)s, %%(posting_time)s)"],
105 "limit 1")
106
107 return sle and sle[0] or webnotes._dict()
108
109def get_sle_after_datetime(args):
110 """get Stock Ledger Entries after a particular datetime, for reposting"""
111 return get_stock_ledger_entries(args,
112 ["timestamp(posting_date, posting_time) > timestamp(%%(posting_date)s, %%(posting_time)s)"])
113
114def get_stock_ledger_entries(args, conditions=None, limit=None):
115 """get stock ledger entries filtered by specific posting datetime conditions"""
116 if not args.get("posting_date"):
117 args["posting_date"] = "1900-01-01"
118 if not args.get("posting_time"):
119 args["posting_time"] = "12:00"
120
121 return webnotes.conn.sql("""select * from `tabStock Ledger Entry`
122 where item_code = %%(item_code)s
123 and warehouse = %%(warehouse)s
124 and ifnull(is_cancelled, 'No') = 'No'
125 %(conditions)s
126 order by timestamp(posting_date, posting_time) desc, name desc
127 %(limit)s""" % {
128 "conditions": conditions and ("and " + " and ".join(conditions)) or "",
129 "limit": limit or ""
130 }, args, as_dict=1)
131
132def validate_negative_stock(qty_after_transaction, sle):
133 """
134 validate negative stock for entries current datetime onwards
135 will not consider cancelled entries
136 """
137 diff = qty_after_transaction + flt(sle.actual_qty)
138
139 if diff < 0 and abs(diff) > 0.0001:
140 # negative stock!
141 global _exceptions
142 exc = sle.copy().update({"diff": diff})
143 _exceptions.append(exc)
144 return False
145 else:
146 return True
147
148def get_serialized_values(qty_after_transaction, sle, valuation_rate):
149 incoming_rate = flt(sle.incoming_rate)
150 actual_qty = flt(sle.actual_qty)
151 serial_nos = cstr(sle.serial_nos).split("\n")
152
153 if incoming_rate < 0:
154 # wrong incoming rate
155 incoming_rate = valuation_rate
156 elif incoming_rate == 0 or flt(sle.actual_qty) < 0:
157 # In case of delivery/stock issue, get average purchase rate
158 # of serial nos of current entry
159 incoming_rate = flt(webnotes.conn.sql("""select avg(ifnull(purchase_rate, 0))
160 from `tabSerial No` where name in (%s)""" % (", ".join(["%s"]*len(serial_nos))),
161 tuple(serial_nos))[0][0])
162
163 if incoming_rate and not valuation_rate:
164 valuation_rate = incoming_rate
165 else:
166 new_stock_qty = qty_after_transaction + actual_qty
167 if new_stock_qty > 0:
168 new_stock_value = qty_after_transaction * valuation_rate + actual_qty * incoming_rate
169 if new_stock_value > 0:
170 # calculate new valuation rate only if stock value is positive
171 # else it remains the same as that of previous entry
172 valuation_rate = new_stock_value / new_stock_qty
173
174 return valuation_rate, incoming_rate
175
176def get_moving_average_values(qty_after_transaction, sle, valuation_rate):
177 incoming_rate = flt(sle.incoming_rate)
178 actual_qty = flt(sle.actual_qty)
179
180 if not incoming_rate or actual_qty < 0:
181 # In case of delivery/stock issue in_rate = 0 or wrong incoming rate
182 incoming_rate = valuation_rate
183
184 # val_rate is same as previous entry if :
185 # 1. actual qty is negative(delivery note / stock entry)
186 # 2. cancelled entry
187 # 3. val_rate is negative
188 # Otherwise it will be calculated as per moving average
189 new_stock_qty = qty_after_transaction + actual_qty
190 new_stock_value = qty_after_transaction * valuation_rate + actual_qty * incoming_rate
191 if actual_qty > 0 and new_stock_qty > 0 and new_stock_value > 0:
192 valuation_rate = new_stock_value / flt(new_stock_qty)
193 elif new_stock_qty <= 0:
194 valuation_rate = 0.0
195
196 return valuation_rate, incoming_rate
197
198def get_fifo_values(qty_after_transaction, sle, stock_queue):
199 incoming_rate = flt(sle.incoming_rate)
200 actual_qty = flt(sle.actual_qty)
201
202 if not stock_queue:
203 stock_queue.append([0, 0])
204
205 if actual_qty > 0:
206 if stock_queue[-1][0] > 0:
207 stock_queue.append([actual_qty, incoming_rate])
208 else:
209 qty = stock_queue[-1][0] + actual_qty
210 stock_queue[-1] = [qty, qty > 0 and incoming_rate or 0]
211 else:
212 incoming_cost = 0
213 qty_to_pop = abs(actual_qty)
214 while qty_to_pop:
215 batch = stock_queue[0]
216
217 if 0 < batch[0] <= qty_to_pop:
218 # if batch qty > 0
219 # not enough or exactly same qty in current batch, clear batch
220 incoming_cost += flt(batch[0]) * flt(batch[1])
221 qty_to_pop -= batch[0]
222 stock_queue.pop(0)
223 else:
224 # all from current batch
225 incoming_cost += flt(qty_to_pop) * flt(batch[1])
226 batch[0] -= qty_to_pop
227 qty_to_pop = 0
228
229 incoming_rate = incoming_cost / flt(abs(actual_qty))
230
231 stock_value = sum((flt(batch[0]) * flt(batch[1]) for batch in stock_queue))
232 stock_qty = sum((flt(batch[0]) for batch in stock_queue))
233
234 valuation_rate = stock_qty and (stock_value / flt(stock_qty)) or 0
235
236 return valuation_rate, incoming_rate
237
238def _raise_exceptions(args):
239 deficiency = min(e["diff"] for e in _exceptions)
240 msg = """Negative stock error:
241 Cannot complete this transaction because stock will start
242 becoming negative (%s) for Item <b>%s</b> in Warehouse
243 <b>%s</b> on <b>%s %s</b> in Transaction %s %s.
244 Total Quantity Deficiency: <b>%s</b>""" % \
245 (_exceptions[0]["diff"], args.get("item_code"), args.get("warehouse"),
246 _exceptions[0]["posting_date"], _exceptions[0]["posting_time"],
247 _exceptions[0]["voucher_type"], _exceptions[0]["voucher_no"],
248 abs(deficiency))
249 if verbose:
250 msgprint(msg, raise_exception=1)
251 else:
252 raise webnotes.ValidationError, msg