blob: 59f8b20b4133aaa07df1a9d9946c60e5cf433064 [file] [log] [blame]
Anand Doshi885e0742015-03-03 14:55:30 +05301# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
Rushabh Mehtaf8509872014-10-08 12:03:19 +05302# License: GNU General Public License v3. See license.txt
3
Chillar Anand915b3432021-09-02 16:44:59 +05304
Marica9ea1ad42020-04-21 12:52:29 +05305import json
Aland4b24712021-10-06 18:16:33 +05306from math import ceil
Chillar Anand915b3432021-09-02 16:44:59 +05307
8import frappe
Anand Doshi5cf7a0b2015-11-26 15:11:18 +05309from frappe import _
Chillar Anand915b3432021-09-02 16:44:59 +053010from frappe.utils import add_days, cint, flt, nowdate
11
12import erpnext
13
Rushabh Mehtaf8509872014-10-08 12:03:19 +053014
15def reorder_item():
Ankush Menat494bd9e2022-03-28 18:52:46 +053016 """Reorder item if stock reaches reorder level"""
Rushabh Mehtaf8509872014-10-08 12:03:19 +053017 # if initial setup not completed, return
Anand Doshi8860b9d2015-05-28 01:09:21 -040018 if not (frappe.db.a_row_exists("Company") and frappe.db.a_row_exists("Fiscal Year")):
Rushabh Mehtaf8509872014-10-08 12:03:19 +053019 return
20
Ankush Menatbb18ae82024-01-09 21:56:47 +053021 if cint(frappe.db.get_single_value("Stock Settings", "auto_indent")):
Rushabh Mehtaf8509872014-10-08 12:03:19 +053022 return _reorder_item()
23
Ankush Menat494bd9e2022-03-28 18:52:46 +053024
Rushabh Mehtaf8509872014-10-08 12:03:19 +053025def _reorder_item():
Rohit Waghchaure08aadb82016-07-29 13:23:30 +053026 material_requests = {"Purchase": {}, "Transfer": {}, "Material Issue": {}, "Manufacture": {}}
Ankush Menat494bd9e2022-03-28 18:52:46 +053027 warehouse_company = frappe._dict(
28 frappe.db.sql(
29 """select name, company from `tabWarehouse`
30 where disabled=0"""
31 )
32 )
33 default_company = (
34 erpnext.get_default_company() or frappe.db.sql("""select name from tabCompany limit 1""")[0][0]
35 )
Rushabh Mehtaf8509872014-10-08 12:03:19 +053036
Rohit Waghchaure951023f2024-01-31 11:32:17 +053037 items_to_consider = get_items_for_reorder()
Anand Doshi7b2b0cd2015-10-15 18:07:51 +053038
39 if not items_to_consider:
40 return
41
42 item_warehouse_projected_qty = get_item_warehouse_projected_qty(items_to_consider)
43
Rohit Waghchaure951023f2024-01-31 11:32:17 +053044 def add_to_material_request(**kwargs):
45 if isinstance(kwargs, dict):
46 kwargs = frappe._dict(kwargs)
47
48 if kwargs.warehouse not in warehouse_company:
Anand Doshi7b2b0cd2015-10-15 18:07:51 +053049 # a disabled warehouse
Rushabh Mehtaf8509872014-10-08 12:03:19 +053050 return
51
Rohit Waghchaure951023f2024-01-31 11:32:17 +053052 reorder_level = flt(kwargs.reorder_level)
53 reorder_qty = flt(kwargs.reorder_qty)
Anand Doshi7b2b0cd2015-10-15 18:07:51 +053054
55 # projected_qty will be 0 if Bin does not exist
Rohit Waghchaure951023f2024-01-31 11:32:17 +053056 if kwargs.warehouse_group:
57 projected_qty = flt(
58 item_warehouse_projected_qty.get(kwargs.item_code, {}).get(kwargs.warehouse_group)
59 )
Saurabh3d6aecd2016-06-20 17:25:45 +053060 else:
Rohit Waghchaure951023f2024-01-31 11:32:17 +053061 projected_qty = flt(
62 item_warehouse_projected_qty.get(kwargs.item_code, {}).get(kwargs.warehouse)
63 )
Rushabh Mehtaf8509872014-10-08 12:03:19 +053064
Devin Slauenwhitef9ed8c12023-06-24 06:33:15 -040065 if (reorder_level or reorder_qty) and projected_qty <= reorder_level:
Rushabh Mehtaf8509872014-10-08 12:03:19 +053066 deficiency = reorder_level - projected_qty
67 if deficiency > reorder_qty:
68 reorder_qty = deficiency
69
Rohit Waghchaure951023f2024-01-31 11:32:17 +053070 company = warehouse_company.get(kwargs.warehouse) or default_company
Rushabh Mehtaf8509872014-10-08 12:03:19 +053071
Rohit Waghchaure951023f2024-01-31 11:32:17 +053072 material_requests[kwargs.material_request_type].setdefault(company, []).append(
73 {
74 "item_code": kwargs.item_code,
75 "warehouse": kwargs.warehouse,
76 "reorder_qty": reorder_qty,
77 "item_details": kwargs.item_details,
78 }
Ankush Menat494bd9e2022-03-28 18:52:46 +053079 )
Rushabh Mehtaf8509872014-10-08 12:03:19 +053080
Rohit Waghchaure951023f2024-01-31 11:32:17 +053081 for item_code, reorder_levels in items_to_consider.items():
82 for d in reorder_levels:
83 if d.has_variants:
84 continue
Rushabh Mehtaf8509872014-10-08 12:03:19 +053085
Rohit Waghchaure951023f2024-01-31 11:32:17 +053086 add_to_material_request(
87 item_code=item_code,
88 warehouse=d.warehouse,
89 reorder_level=d.warehouse_reorder_level,
90 reorder_qty=d.warehouse_reorder_qty,
91 material_request_type=d.material_request_type,
92 warehouse_group=d.warehouse_group,
93 item_details=frappe._dict(
94 {
95 "item_code": item_code,
96 "name": item_code,
97 "item_name": d.item_name,
98 "item_group": d.item_group,
99 "brand": d.brand,
100 "description": d.description,
101 "stock_uom": d.stock_uom,
102 "purchase_uom": d.purchase_uom,
103 }
104 ),
105 )
Rushabh Mehtaf8509872014-10-08 12:03:19 +0530106
Rushabh Mehtaf8509872014-10-08 12:03:19 +0530107 if material_requests:
108 return create_material_request(material_requests)
109
Ankush Menat494bd9e2022-03-28 18:52:46 +0530110
Rohit Waghchaure951023f2024-01-31 11:32:17 +0530111def get_items_for_reorder() -> dict[str, list]:
112 reorder_table = frappe.qb.DocType("Item Reorder")
113 item_table = frappe.qb.DocType("Item")
114
115 query = (
116 frappe.qb.from_(reorder_table)
117 .inner_join(item_table)
118 .on(reorder_table.parent == item_table.name)
119 .select(
120 reorder_table.warehouse,
121 reorder_table.warehouse_group,
122 reorder_table.material_request_type,
123 reorder_table.warehouse_reorder_level,
124 reorder_table.warehouse_reorder_qty,
125 item_table.name,
126 item_table.stock_uom,
127 item_table.purchase_uom,
128 item_table.description,
129 item_table.item_name,
130 item_table.item_group,
131 item_table.brand,
132 item_table.variant_of,
133 item_table.has_variants,
134 )
135 .where(
136 (item_table.disabled == 0)
137 & (item_table.is_stock_item == 1)
138 & (
139 (item_table.end_of_life.isnull())
140 | (item_table.end_of_life > nowdate())
141 | (item_table.end_of_life == "0000-00-00")
142 )
143 )
144 )
145
146 data = query.run(as_dict=True)
147 itemwise_reorder = frappe._dict({})
148 for d in data:
149 itemwise_reorder.setdefault(d.name, []).append(d)
150
151 itemwise_reorder = get_reorder_levels_for_variants(itemwise_reorder)
152
153 return itemwise_reorder
154
155
156def get_reorder_levels_for_variants(itemwise_reorder):
157 item_table = frappe.qb.DocType("Item")
158
159 query = (
160 frappe.qb.from_(item_table)
161 .select(
162 item_table.name,
163 item_table.variant_of,
164 )
165 .where(
166 (item_table.disabled == 0)
167 & (item_table.is_stock_item == 1)
168 & (
169 (item_table.end_of_life.isnull())
170 | (item_table.end_of_life > nowdate())
171 | (item_table.end_of_life == "0000-00-00")
172 )
173 & (item_table.variant_of.notnull())
174 )
175 )
176
177 variants_item = query.run(as_dict=True)
178 for row in variants_item:
179 if not itemwise_reorder.get(row.name) and itemwise_reorder.get(row.variant_of):
180 itemwise_reorder.setdefault(row.name, []).extend(itemwise_reorder.get(row.variant_of, []))
181
182 return itemwise_reorder
183
184
Anand Doshi7b2b0cd2015-10-15 18:07:51 +0530185def get_item_warehouse_projected_qty(items_to_consider):
Rushabh Mehtaf8509872014-10-08 12:03:19 +0530186 item_warehouse_projected_qty = {}
Rohit Waghchaure951023f2024-01-31 11:32:17 +0530187 items_to_consider = list(items_to_consider.keys())
Rushabh Mehtaf8509872014-10-08 12:03:19 +0530188
Ankush Menat494bd9e2022-03-28 18:52:46 +0530189 for item_code, warehouse, projected_qty in frappe.db.sql(
190 """select item_code, warehouse, projected_qty
Anand Doshi7b2b0cd2015-10-15 18:07:51 +0530191 from tabBin where item_code in ({0})
Conor74a782d2022-06-17 06:31:27 -0500192 and (warehouse != '' and warehouse is not null)""".format(
Ankush Menat494bd9e2022-03-28 18:52:46 +0530193 ", ".join(["%s"] * len(items_to_consider))
194 ),
195 items_to_consider,
196 ):
Rohit Waghchauref83f6aa2018-01-20 15:44:38 +0530197
198 if item_code not in item_warehouse_projected_qty:
199 item_warehouse_projected_qty.setdefault(item_code, {})
200
201 if warehouse not in item_warehouse_projected_qty.get(item_code):
202 item_warehouse_projected_qty[item_code][warehouse] = flt(projected_qty)
203
Saurabh3d6aecd2016-06-20 17:25:45 +0530204 warehouse_doc = frappe.get_doc("Warehouse", warehouse)
Rohit Waghchauref83f6aa2018-01-20 15:44:38 +0530205
ppd19909bd84272017-11-15 05:43:09 +0100206 while warehouse_doc.parent_warehouse:
Saurabh3d6aecd2016-06-20 17:25:45 +0530207 if not item_warehouse_projected_qty.get(item_code, {}).get(warehouse_doc.parent_warehouse):
Ankush Menat494bd9e2022-03-28 18:52:46 +0530208 item_warehouse_projected_qty.setdefault(item_code, {})[warehouse_doc.parent_warehouse] = flt(
209 projected_qty
210 )
Saurabh3d6aecd2016-06-20 17:25:45 +0530211 else:
212 item_warehouse_projected_qty[item_code][warehouse_doc.parent_warehouse] += flt(projected_qty)
ppd19909bd84272017-11-15 05:43:09 +0100213 warehouse_doc = frappe.get_doc("Warehouse", warehouse_doc.parent_warehouse)
Rohit Waghchauref83f6aa2018-01-20 15:44:38 +0530214
Rushabh Mehtaf8509872014-10-08 12:03:19 +0530215 return item_warehouse_projected_qty
216
Ankush Menat494bd9e2022-03-28 18:52:46 +0530217
Rushabh Mehtaf8509872014-10-08 12:03:19 +0530218def create_material_request(material_requests):
Ankush Menat494bd9e2022-03-28 18:52:46 +0530219 """Create indent on reaching reorder level"""
Rushabh Mehtaf8509872014-10-08 12:03:19 +0530220 mr_list = []
Rushabh Mehtaf8509872014-10-08 12:03:19 +0530221 exceptions_list = []
222
Rushabh Mehta548afba2022-05-02 15:04:26 +0530223 def _log_exception(mr):
Rushabh Mehtaf8509872014-10-08 12:03:19 +0530224 if frappe.local.message_log:
225 exceptions_list.extend(frappe.local.message_log)
226 frappe.local.message_log = []
227 else:
Ankush Menat510fdf72024-01-01 13:10:03 +0530228 exceptions_list.append(frappe.get_traceback(with_context=True))
Rushabh Mehtaf8509872014-10-08 12:03:19 +0530229
Rushabh Mehta548afba2022-05-02 15:04:26 +0530230 mr.log_error("Unable to create material request")
rohitwaghchaure1e6788b2020-02-26 11:25:34 +0530231
Rohit Waghchaure764f3422024-01-24 11:45:15 +0530232 company_wise_mr = frappe._dict({})
Rushabh Mehtaf8509872014-10-08 12:03:19 +0530233 for request_type in material_requests:
234 for company in material_requests[request_type]:
235 try:
236 items = material_requests[request_type][company]
237 if not items:
238 continue
239
240 mr = frappe.new_doc("Material Request")
Ankush Menat494bd9e2022-03-28 18:52:46 +0530241 mr.update(
242 {
243 "company": company,
244 "transaction_date": nowdate(),
245 "material_request_type": "Material Transfer" if request_type == "Transfer" else request_type,
246 }
247 )
Rushabh Mehtaf8509872014-10-08 12:03:19 +0530248
249 for d in items:
250 d = frappe._dict(d)
Rohit Waghchaure951023f2024-01-31 11:32:17 +0530251 item = d.get("item_details")
rohitwaghchauree8ccc0e2017-11-21 16:17:22 +0530252 uom = item.stock_uom
253 conversion_factor = 1.0
254
Ankush Menat494bd9e2022-03-28 18:52:46 +0530255 if request_type == "Purchase":
rohitwaghchauree8ccc0e2017-11-21 16:17:22 +0530256 uom = item.purchase_uom or item.stock_uom
257 if uom != item.stock_uom:
Ankush Menat494bd9e2022-03-28 18:52:46 +0530258 conversion_factor = (
259 frappe.db.get_value(
260 "UOM Conversion Detail", {"parent": item.name, "uom": uom}, "conversion_factor"
261 )
262 or 1.0
263 )
rohitwaghchauree8ccc0e2017-11-21 16:17:22 +0530264
Aland4b24712021-10-06 18:16:33 +0530265 must_be_whole_number = frappe.db.get_value("UOM", uom, "must_be_whole_number", cache=True)
266 qty = d.reorder_qty / conversion_factor
267 if must_be_whole_number:
268 qty = ceil(qty)
269
Ankush Menat494bd9e2022-03-28 18:52:46 +0530270 mr.append(
271 "items",
272 {
273 "doctype": "Material Request Item",
274 "item_code": d.item_code,
275 "schedule_date": add_days(nowdate(), cint(item.lead_time_days)),
276 "qty": qty,
Rohit Waghchaure951023f2024-01-31 11:32:17 +0530277 "conversion_factor": conversion_factor,
Ankush Menat494bd9e2022-03-28 18:52:46 +0530278 "uom": uom,
279 "stock_uom": item.stock_uom,
280 "warehouse": d.warehouse,
281 "item_name": item.item_name,
282 "description": item.description,
283 "item_group": item.item_group,
284 "brand": item.brand,
285 },
286 )
Rushabh Mehtaf8509872014-10-08 12:03:19 +0530287
rohitwaghchaure373b5702017-11-27 11:27:28 +0530288 schedule_dates = [d.schedule_date for d in mr.items]
289 mr.schedule_date = max(schedule_dates or [nowdate()])
rohitwaghchaure1e6788b2020-02-26 11:25:34 +0530290 mr.flags.ignore_mandatory = True
Rushabh Mehtaf8509872014-10-08 12:03:19 +0530291 mr.insert()
292 mr.submit()
293 mr_list.append(mr)
294
Rohit Waghchaure764f3422024-01-24 11:45:15 +0530295 company_wise_mr.setdefault(company, []).append(mr)
296
Ankush Menat694ae812021-09-01 14:40:56 +0530297 except Exception:
Rushabh Mehta548afba2022-05-02 15:04:26 +0530298 _log_exception(mr)
Rushabh Mehtaf8509872014-10-08 12:03:19 +0530299
Rohit Waghchaure764f3422024-01-24 11:45:15 +0530300 if company_wise_mr:
Rushabh Mehtaf8509872014-10-08 12:03:19 +0530301 if getattr(frappe.local, "reorder_email_notify", None) is None:
Ankush Menat494bd9e2022-03-28 18:52:46 +0530302 frappe.local.reorder_email_notify = cint(
Ankush Menatbb18ae82024-01-09 21:56:47 +0530303 frappe.db.get_single_value("Stock Settings", "reorder_email_notify")
Ankush Menat494bd9e2022-03-28 18:52:46 +0530304 )
Rushabh Mehtaf8509872014-10-08 12:03:19 +0530305
Ankush Menat494bd9e2022-03-28 18:52:46 +0530306 if frappe.local.reorder_email_notify:
Rohit Waghchaure764f3422024-01-24 11:45:15 +0530307 send_email_notification(company_wise_mr)
Rushabh Mehtaf8509872014-10-08 12:03:19 +0530308
309 if exceptions_list:
310 notify_errors(exceptions_list)
311
312 return mr_list
313
Rushabh Mehtaf8509872014-10-08 12:03:19 +0530314
Rohit Waghchaure764f3422024-01-24 11:45:15 +0530315def send_email_notification(company_wise_mr):
Ankush Menat494bd9e2022-03-28 18:52:46 +0530316 """Notify user about auto creation of indent"""
317
Rohit Waghchaure764f3422024-01-24 11:45:15 +0530318 for company, mr_list in company_wise_mr.items():
319 email_list = get_email_list(company)
320
321 if not email_list:
322 continue
323
324 msg = frappe.render_template("templates/emails/reorder_item.html", {"mr_list": mr_list})
325
326 frappe.sendmail(
327 recipients=email_list, subject=_("Auto Material Requests Generated"), message=msg
328 )
329
330
331def get_email_list(company):
332 users = get_comapny_wise_users(company)
333 user_table = frappe.qb.DocType("User")
334 role_table = frappe.qb.DocType("Has Role")
335
336 query = (
337 frappe.qb.from_(user_table)
338 .inner_join(role_table)
339 .on(user_table.name == role_table.parent)
340 .select(user_table.email)
341 .where(
342 (role_table.role.isin(["Purchase Manager", "Stock Manager"]))
343 & (user_table.name.notin(["Administrator", "All", "Guest"]))
344 & (user_table.enabled == 1)
345 & (user_table.docstatus < 2)
346 )
Ankush Menat494bd9e2022-03-28 18:52:46 +0530347 )
Rushabh Mehtaf8509872014-10-08 12:03:19 +0530348
Rohit Waghchaure764f3422024-01-24 11:45:15 +0530349 if users:
350 query = query.where(user_table.name.isin(users))
Anand Doshi5cf7a0b2015-11-26 15:11:18 +0530351
Rohit Waghchaure764f3422024-01-24 11:45:15 +0530352 emails = query.run(as_dict=True)
353
354 return list(set([email.email for email in emails]))
355
356
357def get_comapny_wise_users(company):
358 users = frappe.get_all(
359 "User Permission",
360 filters={"allow": "Company", "for_value": company, "apply_to_all_doctypes": 1},
361 fields=["user"],
362 )
363
364 return [user.user for user in users]
Ankush Menat494bd9e2022-03-28 18:52:46 +0530365
Rushabh Mehtaf8509872014-10-08 12:03:19 +0530366
367def notify_errors(exceptions_list):
Marica9ea1ad42020-04-21 12:52:29 +0530368 subject = _("[Important] [ERPNext] Auto Reorder Errors")
Ankush Menat494bd9e2022-03-28 18:52:46 +0530369 content = (
370 _("Dear System Manager,")
371 + "<br>"
372 + _(
barredterra806696a2024-01-24 02:59:52 +0100373 "An error occurred for certain Items while creating Material Requests based on Re-order level. Please rectify these issues :"
Ankush Menat494bd9e2022-03-28 18:52:46 +0530374 )
375 + "<br>"
376 )
Rushabh Mehtaf8509872014-10-08 12:03:19 +0530377
Marica9ea1ad42020-04-21 12:52:29 +0530378 for exception in exceptions_list:
Ankush Menatecb39d82022-05-23 20:06:24 +0530379 try:
380 exception = json.loads(exception)
381 error_message = """<div class='small text-muted'>{0}</div><br>""".format(
382 _(exception.get("message"))
383 )
384 content += error_message
385 except Exception:
386 pass
Rushabh Mehtaf8509872014-10-08 12:03:19 +0530387
Marica9ea1ad42020-04-21 12:52:29 +0530388 content += _("Regards,") + "<br>" + _("Administrator")
Rushabh Mehtaf8509872014-10-08 12:03:19 +0530389
390 from frappe.email import sendmail_to_system_managers
Ankush Menat494bd9e2022-03-28 18:52:46 +0530391
Rushabh Mehtaf8509872014-10-08 12:03:19 +0530392 sendmail_to_system_managers(subject, content)