blob: 942c200f52fd5e672a1ddb2b582c3632bbbb4288 [file] [log] [blame]
Nabin Hait9d0f6362013-01-07 18:51:11 +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, _
19import json
Nabin Hait62d06292013-05-22 16:19:10 +053020from webnotes.utils import flt, cstr, nowdate, add_days, cint
Rushabh Mehta5117d9c2013-02-19 15:27:31 +053021from webnotes.defaults import get_global_default
Anand Doshiad6180e2013-06-17 11:57:04 +053022from webnotes.utils.email_lib import sendmail
Nabin Hait9d0f6362013-01-07 18:51:11 +053023
24def validate_end_of_life(item_code, end_of_life=None, verbose=1):
25 if not end_of_life:
26 end_of_life = webnotes.conn.get_value("Item", item_code, "end_of_life")
27
28 from webnotes.utils import getdate, now_datetime, formatdate
Anand Doshiad6180e2013-06-17 11:57:04 +053029 if end_of_life and getdate(end_of_life) <= now_datetime().date():
Nabin Hait9d0f6362013-01-07 18:51:11 +053030 msg = (_("Item") + " %(item_code)s: " + _("reached its end of life on") + \
31 " %(date)s. " + _("Please check") + ": %(end_of_life_label)s " + \
32 "in Item master") % {
33 "item_code": item_code,
34 "date": formatdate(end_of_life),
Anand Doshia43b29e2013-02-20 15:55:10 +053035 "end_of_life_label": webnotes.get_doctype("Item").get_label("end_of_life")
Nabin Hait9d0f6362013-01-07 18:51:11 +053036 }
37
38 _msgprint(msg, verbose)
39
40def validate_is_stock_item(item_code, is_stock_item=None, verbose=1):
41 if not is_stock_item:
42 is_stock_item = webnotes.conn.get_value("Item", item_code, "is_stock_item")
43
44 if is_stock_item != "Yes":
45 msg = (_("Item") + " %(item_code)s: " + _("is not a Stock Item")) % {
46 "item_code": item_code,
47 }
48
49 _msgprint(msg, verbose)
50
51def validate_cancelled_item(item_code, docstatus=None, verbose=1):
52 if docstatus is None:
53 docstatus = webnotes.conn.get_value("Item", item_code, "docstatus")
54
55 if docstatus == 2:
56 msg = (_("Item") + " %(item_code)s: " + _("is a cancelled Item")) % {
57 "item_code": item_code,
58 }
59
60 _msgprint(msg, verbose)
61
62def _msgprint(msg, verbose):
63 if verbose:
64 msgprint(msg, raise_exception=True)
65 else:
66 raise webnotes.ValidationError, msg
67
Nabin Hait9d0f6362013-01-07 18:51:11 +053068def get_incoming_rate(args):
69 """Get Incoming Rate based on valuation method"""
Anand Doshi1b531862013-01-10 19:29:51 +053070 from stock.stock_ledger import get_previous_sle
Nabin Hait9d0f6362013-01-07 18:51:11 +053071
72 in_rate = 0
73 if args.get("serial_no"):
74 in_rate = get_avg_purchase_rate(args.get("serial_no"))
75 elif args.get("bom_no"):
76 result = webnotes.conn.sql("""select ifnull(total_cost, 0) / ifnull(quantity, 1)
77 from `tabBOM` where name = %s and docstatus=1 and is_active=1""", args.get("bom_no"))
78 in_rate = result and flt(result[0][0]) or 0
79 else:
80 valuation_method = get_valuation_method(args.get("item_code"))
81 previous_sle = get_previous_sle(args)
82 if valuation_method == 'FIFO':
Nabin Hait831207f2013-01-16 14:15:48 +053083 if not previous_sle:
84 return 0.0
85 previous_stock_queue = json.loads(previous_sle.get('stock_queue', '[]'))
86 in_rate = previous_stock_queue and \
Nabin Hait6b1f21d2013-01-16 17:17:17 +053087 get_fifo_rate(previous_stock_queue, args.get("qty") or 0) or 0
Nabin Hait9d0f6362013-01-07 18:51:11 +053088 elif valuation_method == 'Moving Average':
89 in_rate = previous_sle.get('valuation_rate') or 0
90 return in_rate
91
92def get_avg_purchase_rate(serial_nos):
93 """get average value of serial numbers"""
94
95 serial_nos = get_valid_serial_nos(serial_nos)
96 return flt(webnotes.conn.sql("""select avg(ifnull(purchase_rate, 0)) from `tabSerial No`
97 where name in (%s)""" % ", ".join(["%s"] * len(serial_nos)),
98 tuple(serial_nos))[0][0])
99
100def get_valuation_method(item_code):
101 """get valuation method from item or default"""
102 val_method = webnotes.conn.get_value('Item', item_code, 'valuation_method')
103 if not val_method:
Rushabh Mehta5117d9c2013-02-19 15:27:31 +0530104 val_method = get_global_default('valuation_method') or "FIFO"
Nabin Hait9d0f6362013-01-07 18:51:11 +0530105 return val_method
106
Nabin Hait831207f2013-01-16 14:15:48 +0530107def get_fifo_rate(previous_stock_queue, qty):
108 """get FIFO (average) Rate from Queue"""
109 if qty >= 0:
110 total = sum(f[0] for f in previous_stock_queue)
111 return total and sum(f[0] * f[1] for f in previous_stock_queue) / flt(total) or 0.0
112 else:
113 outgoing_cost = 0
114 qty_to_pop = abs(qty)
Nabin Hait64d7c4b2013-01-17 12:22:59 +0530115 while qty_to_pop and previous_stock_queue:
Nabin Hait831207f2013-01-16 14:15:48 +0530116 batch = previous_stock_queue[0]
117 if 0 < batch[0] <= qty_to_pop:
118 # if batch qty > 0
119 # not enough or exactly same qty in current batch, clear batch
120 outgoing_cost += flt(batch[0]) * flt(batch[1])
121 qty_to_pop -= batch[0]
122 previous_stock_queue.pop(0)
123 else:
124 # all from current batch
125 outgoing_cost += flt(qty_to_pop) * flt(batch[1])
126 batch[0] -= qty_to_pop
127 qty_to_pop = 0
Nabin Hait64d7c4b2013-01-17 12:22:59 +0530128 # if queue gets blank and qty_to_pop remaining, get average rate of full queue
129 return outgoing_cost / abs(qty) - qty_to_pop
Nabin Hait9d0f6362013-01-07 18:51:11 +0530130
131def get_valid_serial_nos(sr_nos, qty=0, item_code=''):
132 """split serial nos, validate and return list of valid serial nos"""
133 # TODO: remove duplicates in client side
134 serial_nos = cstr(sr_nos).strip().replace(',', '\n').split('\n')
135
136 valid_serial_nos = []
137 for val in serial_nos:
138 if val:
139 val = val.strip()
140 if val in valid_serial_nos:
141 msgprint("You have entered duplicate serial no: '%s'" % val, raise_exception=1)
142 else:
143 valid_serial_nos.append(val)
144
145 if qty and len(valid_serial_nos) != abs(qty):
146 msgprint("Please enter serial nos for "
147 + cstr(abs(qty)) + " quantity against item code: " + item_code,
148 raise_exception=1)
149
Rushabh Mehta0dbe8982013-02-04 13:56:50 +0530150 return valid_serial_nos
151
Nabin Haitdc95c152013-02-07 12:08:38 +0530152def get_warehouse_list(doctype, txt, searchfield, start, page_len, filters):
Rushabh Mehta0dbe8982013-02-04 13:56:50 +0530153 """used in search queries"""
154 wlist = []
155 for w in webnotes.conn.sql_list("""select name from tabWarehouse
156 where name like '%%%s%%'""" % txt):
157 if webnotes.session.user=="Administrator":
158 wlist.append([w])
159 else:
160 warehouse_users = webnotes.conn.sql_list("""select user from `tabWarehouse User`
161 where parent=%s""", w)
162 if not warehouse_users:
163 wlist.append([w])
164 elif webnotes.session.user in warehouse_users:
165 wlist.append([w])
166 return wlist
Nabin Haita72c5122013-03-06 18:50:53 +0530167
Nabin Hait8c7234f2013-03-11 16:32:33 +0530168def get_buying_amount(item_code, warehouse, qty, voucher_type, voucher_no, voucher_detail_no,
Nabin Haitc3afb252013-03-19 12:01:24 +0530169 stock_ledger_entries, item_sales_bom=None):
170 if item_sales_bom and item_sales_bom.get(item_code):
Nabin Haita72c5122013-03-06 18:50:53 +0530171 # sales bom item
172 buying_amount = 0.0
173 for bom_item in item_sales_bom[item_code]:
Anand Doshi96b189c2013-03-26 18:43:10 +0530174 if bom_item.get("parent_detail_docname")==voucher_detail_no:
Anand Doshi8c454202013-03-28 16:40:30 +0530175 buying_amount += _get_buying_amount(voucher_type, voucher_no, voucher_detail_no,
Anand Doshi96b189c2013-03-26 18:43:10 +0530176 bom_item.item_code, bom_item.warehouse or warehouse,
177 bom_item.total_qty or (bom_item.qty * qty), stock_ledger_entries)
Nabin Haita72c5122013-03-06 18:50:53 +0530178 return buying_amount
179 else:
180 # doesn't have sales bom
Nabin Hait8c7234f2013-03-11 16:32:33 +0530181 return _get_buying_amount(voucher_type, voucher_no, voucher_detail_no,
182 item_code, warehouse, qty, stock_ledger_entries)
Nabin Haita72c5122013-03-06 18:50:53 +0530183
Nabin Hait8c7234f2013-03-11 16:32:33 +0530184def _get_buying_amount(voucher_type, voucher_no, item_row, item_code, warehouse, qty,
185 stock_ledger_entries):
Anand Doshi96b189c2013-03-26 18:43:10 +0530186 relevant_stock_ledger_entries = [sle for sle in stock_ledger_entries
187 if sle.item_code == item_code and sle.warehouse == warehouse]
188
189 for i, sle in enumerate(relevant_stock_ledger_entries):
Nabin Hait0cfbc5f2013-03-12 11:34:56 +0530190 if sle.voucher_type == voucher_type and sle.voucher_no == voucher_no and \
Anand Doshi8c454202013-03-28 16:40:30 +0530191 sle.voucher_detail_no == item_row:
Anand Doshi96b189c2013-03-26 18:43:10 +0530192 previous_stock_value = len(relevant_stock_ledger_entries) > i+1 and \
193 flt(relevant_stock_ledger_entries[i+1].stock_value) or 0.0
194
Nabin Haitc3afb252013-03-19 12:01:24 +0530195 buying_amount = previous_stock_value - flt(sle.stock_value)
Anand Doshi6d8d3b42013-03-21 18:45:02 +0530196
Nabin Haitc3afb252013-03-19 12:01:24 +0530197 return buying_amount
Nabin Hait62d06292013-05-22 16:19:10 +0530198 return 0.0
199
200
201def reorder_item():
202 """ Reorder item if stock reaches reorder level"""
203 if not hasattr(webnotes, "auto_indent"):
Rushabh Mehta7a93d5d2013-06-24 18:18:46 +0530204 webnotes.auto_indent = webnotes.conn.get_value('Stock Settings', None, 'auto_indent')
Nabin Hait62d06292013-05-22 16:19:10 +0530205
206 if webnotes.auto_indent:
207 material_requests = {}
208 bin_list = webnotes.conn.sql("""select item_code, warehouse, projected_qty
Anand Doshiad6180e2013-06-17 11:57:04 +0530209 from tabBin where ifnull(item_code, '') != '' and ifnull(warehouse, '') != ''
210 and exists (select name from `tabItem`
211 where `tabItem`.name = `tabBin`.item_code and
212 is_stock_item='Yes' and (is_purchase_item='Yes' or is_sub_contracted_item='Yes') and
213 (ifnull(end_of_life, '')='') or end_of_life > now())""",
Nabin Hait62d06292013-05-22 16:19:10 +0530214 as_dict=True)
215 for bin in bin_list:
216 #check if re-order is required
217 item_reorder = webnotes.conn.get("Item Reorder",
218 {"parent": bin.item_code, "warehouse": bin.warehouse})
219 if item_reorder:
220 reorder_level = item_reorder.warehouse_reorder_level
221 reorder_qty = item_reorder.warehouse_reorder_qty
222 material_request_type = item_reorder.material_request_type or "Purchase"
223 else:
224 reorder_level, reorder_qty = webnotes.conn.get_value("Item", bin.item_code,
225 ["re_order_level", "re_order_qty"])
226 material_request_type = "Purchase"
227
Anand Doshiad6180e2013-06-17 11:57:04 +0530228 if flt(reorder_level) and flt(bin.projected_qty) < flt(reorder_level):
Nabin Hait62d06292013-05-22 16:19:10 +0530229 if flt(reorder_level) - flt(bin.projected_qty) > flt(reorder_qty):
230 reorder_qty = flt(reorder_level) - flt(bin.projected_qty)
231
232 company = webnotes.conn.get_value("Warehouse", bin.warehouse, "company") or \
233 webnotes.defaults.get_defaults()["company"] or \
234 webnotes.conn.sql("""select name from tabCompany limit 1""")[0][0]
235
236 material_requests.setdefault(material_request_type, webnotes._dict()).setdefault(
237 company, []).append(webnotes._dict({
238 "item_code": bin.item_code,
239 "warehouse": bin.warehouse,
240 "reorder_qty": reorder_qty
241 })
242 )
243
244 create_material_request(material_requests)
245
246def create_material_request(material_requests):
247 """ Create indent on reaching reorder level """
248 mr_list = []
249 defaults = webnotes.defaults.get_defaults()
Anand Doshiad6180e2013-06-17 11:57:04 +0530250 exceptions_list = []
Nabin Hait62d06292013-05-22 16:19:10 +0530251 for request_type in material_requests:
252 for company in material_requests[request_type]:
Anand Doshiad6180e2013-06-17 11:57:04 +0530253 try:
254 items = material_requests[request_type][company]
255 if not items:
256 continue
257
Nabin Hait62d06292013-05-22 16:19:10 +0530258 mr = [{
259 "doctype": "Material Request",
260 "company": company,
261 "fiscal_year": defaults.fiscal_year,
262 "transaction_date": nowdate(),
263 "material_request_type": request_type,
264 "remark": _("This is an auto generated Material Request.") + \
265 _("""It was raised because the (actual + ordered + indented - reserved)
266 quantity reaches re-order level when the following record was created""")
267 }]
268
Anand Doshiad6180e2013-06-17 11:57:04 +0530269 for d in items:
270 item = webnotes.doc("Item", d.item_code)
271 mr.append({
272 "doctype": "Material Request Item",
273 "parenttype": "Material Request",
274 "parentfield": "indent_details",
275 "item_code": d.item_code,
276 "schedule_date": add_days(nowdate(),cint(item.lead_time_days)),
277 "uom": item.stock_uom,
278 "warehouse": d.warehouse,
279 "item_name": item.item_name,
280 "description": item.description,
281 "item_group": item.item_group,
282 "qty": d.reorder_qty,
283 "brand": item.brand,
284 })
Nabin Hait62d06292013-05-22 16:19:10 +0530285
Anand Doshiad6180e2013-06-17 11:57:04 +0530286 mr_bean = webnotes.bean(mr)
287 mr_bean.insert()
288 mr_bean.submit()
289 mr_list.append(mr_bean)
290
291 except:
292 if webnotes.message_log:
293 exceptions_list.append([] + webnotes.message_log)
294 webnotes.message_log = []
295 else:
296 exceptions_list.append(webnotes.getTraceback())
Nabin Hait62d06292013-05-22 16:19:10 +0530297
298 if mr_list:
299 if not hasattr(webnotes, "reorder_email_notify"):
Rushabh Mehta7a93d5d2013-06-24 18:18:46 +0530300 webnotes.reorder_email_notify = webnotes.conn.get_value('Stock Settings', None,
Nabin Hait62d06292013-05-22 16:19:10 +0530301 'reorder_email_notify')
302
303 if(webnotes.reorder_email_notify):
304 send_email_notification(mr_list)
Anand Doshiad6180e2013-06-17 11:57:04 +0530305
306 if exceptions_list:
307 notify_errors(exceptions_list)
Nabin Hait62d06292013-05-22 16:19:10 +0530308
309def send_email_notification(mr_list):
310 """ Notify user about auto creation of indent"""
311
Nabin Hait62d06292013-05-22 16:19:10 +0530312 email_list = webnotes.conn.sql_list("""select distinct r.parent
313 from tabUserRole r, tabProfile p
314 where p.name = r.parent and p.enabled = 1 and p.docstatus < 2
315 and r.role in ('Purchase Manager','Material Manager')
316 and p.name not in ('Administrator', 'All', 'Guest')""")
317
318 msg="""<h3>Following Material Requests has been raised automatically \
319 based on item reorder level:</h3>"""
320 for mr in mr_list:
321 msg += "<p><b><u>" + mr.doc.name + """</u></b></p><table class='table table-bordered'><tr>
322 <th>Item Code</th><th>Warehouse</th><th>Qty</th><th>UOM</th></tr>"""
323 for item in mr.doclist.get({"parentfield": "indent_details"}):
324 msg += "<tr><td>" + item.item_code + "</td><td>" + item.warehouse + "</td><td>" + \
325 cstr(item.qty) + "</td><td>" + cstr(item.uom) + "</td></tr>"
326 msg += "</table>"
327
Anand Doshiad6180e2013-06-17 11:57:04 +0530328 sendmail(email_list, subject='Auto Material Request Generation Notification', msg = msg)
329
330def notify_errors(exceptions_list):
331 subject = "[Important] [ERPNext] Error(s) while creating Material Requests based on Re-order Levels"
332 msg = """Dear System Manager,
333
334 An error occured for certain Items while creating Material Requests based on Re-order level.
335
336 Please rectify these issues:
337 ---
338
339 %s
340
341 ---
342 Regards,
343 Administrator""" % ("\n\n".join(["\n".join(msg) for msg in exceptions_list]),)
344
345 from webnotes.profile import get_system_managers
346 sendmail(get_system_managers(), subject=subject, msg=msg)