blob: a24e852d26c7f7e5162ee5768448b5c5cb028b62 [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:
Akhil Narang3effaf22024-03-27 11:37:26 +053061 projected_qty = flt(item_warehouse_projected_qty.get(kwargs.item_code, {}).get(kwargs.warehouse))
Rushabh Mehtaf8509872014-10-08 12:03:19 +053062
Devin Slauenwhitef9ed8c12023-06-24 06:33:15 -040063 if (reorder_level or reorder_qty) and projected_qty <= reorder_level:
Rushabh Mehtaf8509872014-10-08 12:03:19 +053064 deficiency = reorder_level - projected_qty
65 if deficiency > reorder_qty:
66 reorder_qty = deficiency
67
Rohit Waghchaure951023f2024-01-31 11:32:17 +053068 company = warehouse_company.get(kwargs.warehouse) or default_company
Rushabh Mehtaf8509872014-10-08 12:03:19 +053069
Rohit Waghchaure951023f2024-01-31 11:32:17 +053070 material_requests[kwargs.material_request_type].setdefault(company, []).append(
71 {
72 "item_code": kwargs.item_code,
73 "warehouse": kwargs.warehouse,
74 "reorder_qty": reorder_qty,
75 "item_details": kwargs.item_details,
76 }
Ankush Menat494bd9e2022-03-28 18:52:46 +053077 )
Rushabh Mehtaf8509872014-10-08 12:03:19 +053078
Rohit Waghchaure951023f2024-01-31 11:32:17 +053079 for item_code, reorder_levels in items_to_consider.items():
80 for d in reorder_levels:
81 if d.has_variants:
82 continue
Rushabh Mehtaf8509872014-10-08 12:03:19 +053083
Rohit Waghchaure951023f2024-01-31 11:32:17 +053084 add_to_material_request(
85 item_code=item_code,
86 warehouse=d.warehouse,
87 reorder_level=d.warehouse_reorder_level,
88 reorder_qty=d.warehouse_reorder_qty,
89 material_request_type=d.material_request_type,
90 warehouse_group=d.warehouse_group,
91 item_details=frappe._dict(
92 {
93 "item_code": item_code,
94 "name": item_code,
95 "item_name": d.item_name,
96 "item_group": d.item_group,
97 "brand": d.brand,
98 "description": d.description,
99 "stock_uom": d.stock_uom,
100 "purchase_uom": d.purchase_uom,
101 }
102 ),
103 )
Rushabh Mehtaf8509872014-10-08 12:03:19 +0530104
Rushabh Mehtaf8509872014-10-08 12:03:19 +0530105 if material_requests:
106 return create_material_request(material_requests)
107
Ankush Menat494bd9e2022-03-28 18:52:46 +0530108
Rohit Waghchaure951023f2024-01-31 11:32:17 +0530109def get_items_for_reorder() -> dict[str, list]:
110 reorder_table = frappe.qb.DocType("Item Reorder")
111 item_table = frappe.qb.DocType("Item")
112
113 query = (
114 frappe.qb.from_(reorder_table)
115 .inner_join(item_table)
116 .on(reorder_table.parent == item_table.name)
117 .select(
118 reorder_table.warehouse,
119 reorder_table.warehouse_group,
120 reorder_table.material_request_type,
121 reorder_table.warehouse_reorder_level,
122 reorder_table.warehouse_reorder_qty,
123 item_table.name,
124 item_table.stock_uom,
125 item_table.purchase_uom,
126 item_table.description,
127 item_table.item_name,
128 item_table.item_group,
129 item_table.brand,
130 item_table.variant_of,
131 item_table.has_variants,
132 )
133 .where(
134 (item_table.disabled == 0)
135 & (item_table.is_stock_item == 1)
136 & (
137 (item_table.end_of_life.isnull())
138 | (item_table.end_of_life > nowdate())
139 | (item_table.end_of_life == "0000-00-00")
140 )
141 )
142 )
143
144 data = query.run(as_dict=True)
145 itemwise_reorder = frappe._dict({})
146 for d in data:
147 itemwise_reorder.setdefault(d.name, []).append(d)
148
149 itemwise_reorder = get_reorder_levels_for_variants(itemwise_reorder)
150
151 return itemwise_reorder
152
153
154def get_reorder_levels_for_variants(itemwise_reorder):
155 item_table = frappe.qb.DocType("Item")
156
157 query = (
158 frappe.qb.from_(item_table)
159 .select(
160 item_table.name,
161 item_table.variant_of,
162 )
163 .where(
164 (item_table.disabled == 0)
165 & (item_table.is_stock_item == 1)
166 & (
167 (item_table.end_of_life.isnull())
168 | (item_table.end_of_life > nowdate())
169 | (item_table.end_of_life == "0000-00-00")
170 )
171 & (item_table.variant_of.notnull())
172 )
173 )
174
175 variants_item = query.run(as_dict=True)
176 for row in variants_item:
177 if not itemwise_reorder.get(row.name) and itemwise_reorder.get(row.variant_of):
178 itemwise_reorder.setdefault(row.name, []).extend(itemwise_reorder.get(row.variant_of, []))
179
180 return itemwise_reorder
181
182
Anand Doshi7b2b0cd2015-10-15 18:07:51 +0530183def get_item_warehouse_projected_qty(items_to_consider):
Rushabh Mehtaf8509872014-10-08 12:03:19 +0530184 item_warehouse_projected_qty = {}
Rohit Waghchaure951023f2024-01-31 11:32:17 +0530185 items_to_consider = list(items_to_consider.keys())
Rushabh Mehtaf8509872014-10-08 12:03:19 +0530186
Ankush Menat494bd9e2022-03-28 18:52:46 +0530187 for item_code, warehouse, projected_qty in frappe.db.sql(
188 """select item_code, warehouse, projected_qty
Akhil Narang3effaf22024-03-27 11:37:26 +0530189 from tabBin where item_code in ({})
Conor74a782d2022-06-17 06:31:27 -0500190 and (warehouse != '' and warehouse is not null)""".format(
Ankush Menat494bd9e2022-03-28 18:52:46 +0530191 ", ".join(["%s"] * len(items_to_consider))
192 ),
193 items_to_consider,
194 ):
Rohit Waghchauref83f6aa2018-01-20 15:44:38 +0530195 if item_code not in item_warehouse_projected_qty:
196 item_warehouse_projected_qty.setdefault(item_code, {})
197
198 if warehouse not in item_warehouse_projected_qty.get(item_code):
199 item_warehouse_projected_qty[item_code][warehouse] = flt(projected_qty)
200
Saurabh3d6aecd2016-06-20 17:25:45 +0530201 warehouse_doc = frappe.get_doc("Warehouse", warehouse)
Rohit Waghchauref83f6aa2018-01-20 15:44:38 +0530202
ppd19909bd84272017-11-15 05:43:09 +0100203 while warehouse_doc.parent_warehouse:
Saurabh3d6aecd2016-06-20 17:25:45 +0530204 if not item_warehouse_projected_qty.get(item_code, {}).get(warehouse_doc.parent_warehouse):
Ankush Menat494bd9e2022-03-28 18:52:46 +0530205 item_warehouse_projected_qty.setdefault(item_code, {})[warehouse_doc.parent_warehouse] = flt(
206 projected_qty
207 )
Saurabh3d6aecd2016-06-20 17:25:45 +0530208 else:
209 item_warehouse_projected_qty[item_code][warehouse_doc.parent_warehouse] += flt(projected_qty)
ppd19909bd84272017-11-15 05:43:09 +0100210 warehouse_doc = frappe.get_doc("Warehouse", warehouse_doc.parent_warehouse)
Rohit Waghchauref83f6aa2018-01-20 15:44:38 +0530211
Rushabh Mehtaf8509872014-10-08 12:03:19 +0530212 return item_warehouse_projected_qty
213
Ankush Menat494bd9e2022-03-28 18:52:46 +0530214
Rushabh Mehtaf8509872014-10-08 12:03:19 +0530215def create_material_request(material_requests):
Ankush Menat494bd9e2022-03-28 18:52:46 +0530216 """Create indent on reaching reorder level"""
Rushabh Mehtaf8509872014-10-08 12:03:19 +0530217 mr_list = []
Rushabh Mehtaf8509872014-10-08 12:03:19 +0530218 exceptions_list = []
219
Rushabh Mehta548afba2022-05-02 15:04:26 +0530220 def _log_exception(mr):
Rushabh Mehtaf8509872014-10-08 12:03:19 +0530221 if frappe.local.message_log:
222 exceptions_list.extend(frappe.local.message_log)
223 frappe.local.message_log = []
224 else:
Ankush Menat510fdf72024-01-01 13:10:03 +0530225 exceptions_list.append(frappe.get_traceback(with_context=True))
Rushabh Mehtaf8509872014-10-08 12:03:19 +0530226
Rushabh Mehta548afba2022-05-02 15:04:26 +0530227 mr.log_error("Unable to create material request")
rohitwaghchaure1e6788b2020-02-26 11:25:34 +0530228
Rohit Waghchaure764f3422024-01-24 11:45:15 +0530229 company_wise_mr = frappe._dict({})
Rushabh Mehtaf8509872014-10-08 12:03:19 +0530230 for request_type in material_requests:
231 for company in material_requests[request_type]:
232 try:
233 items = material_requests[request_type][company]
234 if not items:
235 continue
236
237 mr = frappe.new_doc("Material Request")
Ankush Menat494bd9e2022-03-28 18:52:46 +0530238 mr.update(
239 {
240 "company": company,
241 "transaction_date": nowdate(),
Akhil Narang3effaf22024-03-27 11:37:26 +0530242 "material_request_type": "Material Transfer"
243 if request_type == "Transfer"
244 else request_type,
Ankush Menat494bd9e2022-03-28 18:52:46 +0530245 }
246 )
Rushabh Mehtaf8509872014-10-08 12:03:19 +0530247
248 for d in items:
249 d = frappe._dict(d)
Rohit Waghchaure951023f2024-01-31 11:32:17 +0530250 item = d.get("item_details")
rohitwaghchauree8ccc0e2017-11-21 16:17:22 +0530251 uom = item.stock_uom
252 conversion_factor = 1.0
253
Ankush Menat494bd9e2022-03-28 18:52:46 +0530254 if request_type == "Purchase":
rohitwaghchauree8ccc0e2017-11-21 16:17:22 +0530255 uom = item.purchase_uom or item.stock_uom
256 if uom != item.stock_uom:
Ankush Menat494bd9e2022-03-28 18:52:46 +0530257 conversion_factor = (
258 frappe.db.get_value(
Akhil Narang3effaf22024-03-27 11:37:26 +0530259 "UOM Conversion Detail",
260 {"parent": item.name, "uom": uom},
261 "conversion_factor",
Ankush Menat494bd9e2022-03-28 18:52:46 +0530262 )
263 or 1.0
264 )
rohitwaghchauree8ccc0e2017-11-21 16:17:22 +0530265
Aland4b24712021-10-06 18:16:33 +0530266 must_be_whole_number = frappe.db.get_value("UOM", uom, "must_be_whole_number", cache=True)
267 qty = d.reorder_qty / conversion_factor
268 if must_be_whole_number:
269 qty = ceil(qty)
270
Ankush Menat494bd9e2022-03-28 18:52:46 +0530271 mr.append(
272 "items",
273 {
274 "doctype": "Material Request Item",
275 "item_code": d.item_code,
276 "schedule_date": add_days(nowdate(), cint(item.lead_time_days)),
277 "qty": qty,
Rohit Waghchaure951023f2024-01-31 11:32:17 +0530278 "conversion_factor": conversion_factor,
Ankush Menat494bd9e2022-03-28 18:52:46 +0530279 "uom": uom,
280 "stock_uom": item.stock_uom,
281 "warehouse": d.warehouse,
282 "item_name": item.item_name,
283 "description": item.description,
284 "item_group": item.item_group,
285 "brand": item.brand,
286 },
287 )
Rushabh Mehtaf8509872014-10-08 12:03:19 +0530288
rohitwaghchaure373b5702017-11-27 11:27:28 +0530289 schedule_dates = [d.schedule_date for d in mr.items]
290 mr.schedule_date = max(schedule_dates or [nowdate()])
rohitwaghchaure1e6788b2020-02-26 11:25:34 +0530291 mr.flags.ignore_mandatory = True
Rushabh Mehtaf8509872014-10-08 12:03:19 +0530292 mr.insert()
293 mr.submit()
294 mr_list.append(mr)
295
Rohit Waghchaure764f3422024-01-24 11:45:15 +0530296 company_wise_mr.setdefault(company, []).append(mr)
297
Ankush Menat694ae812021-09-01 14:40:56 +0530298 except Exception:
Rushabh Mehta548afba2022-05-02 15:04:26 +0530299 _log_exception(mr)
Rushabh Mehtaf8509872014-10-08 12:03:19 +0530300
Rohit Waghchaure764f3422024-01-24 11:45:15 +0530301 if company_wise_mr:
Rushabh Mehtaf8509872014-10-08 12:03:19 +0530302 if getattr(frappe.local, "reorder_email_notify", None) is None:
Ankush Menat494bd9e2022-03-28 18:52:46 +0530303 frappe.local.reorder_email_notify = cint(
Ankush Menatbb18ae82024-01-09 21:56:47 +0530304 frappe.db.get_single_value("Stock Settings", "reorder_email_notify")
Ankush Menat494bd9e2022-03-28 18:52:46 +0530305 )
Rushabh Mehtaf8509872014-10-08 12:03:19 +0530306
Ankush Menat494bd9e2022-03-28 18:52:46 +0530307 if frappe.local.reorder_email_notify:
Rohit Waghchaure764f3422024-01-24 11:45:15 +0530308 send_email_notification(company_wise_mr)
Rushabh Mehtaf8509872014-10-08 12:03:19 +0530309
310 if exceptions_list:
311 notify_errors(exceptions_list)
312
313 return mr_list
314
Rushabh Mehtaf8509872014-10-08 12:03:19 +0530315
Rohit Waghchaure764f3422024-01-24 11:45:15 +0530316def send_email_notification(company_wise_mr):
Ankush Menat494bd9e2022-03-28 18:52:46 +0530317 """Notify user about auto creation of indent"""
318
Rohit Waghchaure764f3422024-01-24 11:45:15 +0530319 for company, mr_list in company_wise_mr.items():
320 email_list = get_email_list(company)
321
322 if not email_list:
323 continue
324
325 msg = frappe.render_template("templates/emails/reorder_item.html", {"mr_list": mr_list})
326
Akhil Narang3effaf22024-03-27 11:37:26 +0530327 frappe.sendmail(recipients=email_list, subject=_("Auto Material Requests Generated"), message=msg)
Rohit Waghchaure764f3422024-01-24 11:45:15 +0530328
329
330def get_email_list(company):
331 users = get_comapny_wise_users(company)
332 user_table = frappe.qb.DocType("User")
333 role_table = frappe.qb.DocType("Has Role")
334
335 query = (
336 frappe.qb.from_(user_table)
337 .inner_join(role_table)
338 .on(user_table.name == role_table.parent)
339 .select(user_table.email)
340 .where(
341 (role_table.role.isin(["Purchase Manager", "Stock Manager"]))
342 & (user_table.name.notin(["Administrator", "All", "Guest"]))
343 & (user_table.enabled == 1)
344 & (user_table.docstatus < 2)
345 )
Ankush Menat494bd9e2022-03-28 18:52:46 +0530346 )
Rushabh Mehtaf8509872014-10-08 12:03:19 +0530347
Rohit Waghchaure764f3422024-01-24 11:45:15 +0530348 if users:
349 query = query.where(user_table.name.isin(users))
Anand Doshi5cf7a0b2015-11-26 15:11:18 +0530350
Rohit Waghchaure764f3422024-01-24 11:45:15 +0530351 emails = query.run(as_dict=True)
352
353 return list(set([email.email for email in emails]))
354
355
356def get_comapny_wise_users(company):
357 users = frappe.get_all(
358 "User Permission",
359 filters={"allow": "Company", "for_value": company, "apply_to_all_doctypes": 1},
360 fields=["user"],
361 )
362
363 return [user.user for user in users]
Ankush Menat494bd9e2022-03-28 18:52:46 +0530364
Rushabh Mehtaf8509872014-10-08 12:03:19 +0530365
366def notify_errors(exceptions_list):
Marica9ea1ad42020-04-21 12:52:29 +0530367 subject = _("[Important] [ERPNext] Auto Reorder Errors")
Ankush Menat494bd9e2022-03-28 18:52:46 +0530368 content = (
369 _("Dear System Manager,")
370 + "<br>"
371 + _(
barredterra806696a2024-01-24 02:59:52 +0100372 "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 +0530373 )
374 + "<br>"
375 )
Rushabh Mehtaf8509872014-10-08 12:03:19 +0530376
Marica9ea1ad42020-04-21 12:52:29 +0530377 for exception in exceptions_list:
Ankush Menatecb39d82022-05-23 20:06:24 +0530378 try:
379 exception = json.loads(exception)
Akhil Narang3effaf22024-03-27 11:37:26 +0530380 error_message = """<div class='small text-muted'>{}</div><br>""".format(
Ankush Menatecb39d82022-05-23 20:06:24 +0530381 _(exception.get("message"))
382 )
383 content += error_message
384 except Exception:
385 pass
Rushabh Mehtaf8509872014-10-08 12:03:19 +0530386
Marica9ea1ad42020-04-21 12:52:29 +0530387 content += _("Regards,") + "<br>" + _("Administrator")
Rushabh Mehtaf8509872014-10-08 12:03:19 +0530388
389 from frappe.email import sendmail_to_system_managers
Ankush Menat494bd9e2022-03-28 18:52:46 +0530390
Rushabh Mehtaf8509872014-10-08 12:03:19 +0530391 sendmail_to_system_managers(subject, content)