blob: 4f1891fd750ed5e39d3f6062deaa05681aa0d694 [file] [log] [blame]
Anand Doshi885e0742015-03-03 14:55:30 +05301# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
Rushabh Mehtae67d1fb2013-08-05 14:59:54 +05302# License: GNU General Public License v3. See license.txt
Nabin Hait9d0f6362013-01-07 18:51:11 +05303
Nabin Hait9d0f6362013-01-07 18:51:11 +05304
Chillar Anand915b3432021-09-02 16:44:59 +05305import json
6
7import frappe
8from frappe import _
9from frappe.utils import cstr, flt, get_link_to_form, nowdate, nowtime
Achilles Rasquinha56b2e122018-02-13 14:42:40 +053010
Chillar Anand915b3432021-09-02 16:44:59 +053011import erpnext
Ankush Menat61c5ad42022-01-15 18:06:50 +053012from erpnext.stock.valuation import FIFOValuation, LIFOValuation
Chillar Anand915b3432021-09-02 16:44:59 +053013
14
Ankush Menat494bd9e2022-03-28 18:52:46 +053015class InvalidWarehouseCompany(frappe.ValidationError):
16 pass
17
18
19class PendingRepostingError(frappe.ValidationError):
20 pass
21
Anand Doshi2ce39cf2014-04-07 18:51:58 +053022
Shreya Shahe0a47ae2018-08-28 13:46:22 +053023def get_stock_value_from_bin(warehouse=None, item_code=None):
Sachin Mane19a5a5d2018-06-21 13:01:48 +053024 values = {}
25 conditions = ""
26 if warehouse:
rohitwaghchauref1fab872019-09-05 14:47:43 +053027 conditions += """ and `tabBin`.warehouse in (
Sachin Mane19a5a5d2018-06-21 13:01:48 +053028 select w2.name from `tabWarehouse` w1
29 join `tabWarehouse` w2 on
30 w1.name = %(warehouse)s
31 and w2.lft between w1.lft and w1.rgt
32 ) """
33
Ankush Menat494bd9e2022-03-28 18:52:46 +053034 values["warehouse"] = warehouse
Sachin Mane19a5a5d2018-06-21 13:01:48 +053035
36 if item_code:
rohitwaghchauref1fab872019-09-05 14:47:43 +053037 conditions += " and `tabBin`.item_code = %(item_code)s"
Sachin Mane19a5a5d2018-06-21 13:01:48 +053038
Ankush Menat494bd9e2022-03-28 18:52:46 +053039 values["item_code"] = item_code
Sachin Mane19a5a5d2018-06-21 13:01:48 +053040
Ankush Menat494bd9e2022-03-28 18:52:46 +053041 query = (
42 """select sum(stock_value) from `tabBin`, `tabItem` where 1 = 1
43 and `tabItem`.name = `tabBin`.item_code and ifnull(`tabItem`.disabled, 0) = 0 %s"""
44 % conditions
45 )
Sachin Mane19a5a5d2018-06-21 13:01:48 +053046
47 stock_value = frappe.db.sql(query, values)
48
Shreya Shahe0a47ae2018-08-28 13:46:22 +053049 return stock_value
Sachin Mane19a5a5d2018-06-21 13:01:48 +053050
Ankush Menat494bd9e2022-03-28 18:52:46 +053051
Rushabh Mehtaf8509872014-10-08 12:03:19 +053052def get_stock_value_on(warehouse=None, posting_date=None, item_code=None):
Ankush Menat494bd9e2022-03-28 18:52:46 +053053 if not posting_date:
54 posting_date = nowdate()
Anand Doshi2ce39cf2014-04-07 18:51:58 +053055
Rushabh Mehtaf8509872014-10-08 12:03:19 +053056 values, condition = [posting_date], ""
57
58 if warehouse:
Sachin Mane19a5a5d2018-06-21 13:01:48 +053059
Saurabh4d029492016-06-23 12:44:06 +053060 lft, rgt, is_group = frappe.db.get_value("Warehouse", warehouse, ["lft", "rgt", "is_group"])
Sachin Mane19a5a5d2018-06-21 13:01:48 +053061
Saurabh93d68ac2016-06-26 22:50:11 +053062 if is_group:
Saurabh4d029492016-06-23 12:44:06 +053063 values.extend([lft, rgt])
Saurabh554f6f72016-06-06 14:22:37 +053064 condition += "and exists (\
65 select name from `tabWarehouse` wh where wh.name = sle.warehouse\
66 and wh.lft >= %s and wh.rgt <= %s)"
Sachin Mane19a5a5d2018-06-21 13:01:48 +053067
Saurabh554f6f72016-06-06 14:22:37 +053068 else:
69 values.append(warehouse)
70 condition += " AND warehouse = %s"
Rushabh Mehtaf8509872014-10-08 12:03:19 +053071
72 if item_code:
73 values.append(item_code)
itusedyetnew8aafbd22019-03-20 11:10:41 +053074 condition += " AND item_code = %s"
Rushabh Mehtaf8509872014-10-08 12:03:19 +053075
Ankush Menat494bd9e2022-03-28 18:52:46 +053076 stock_ledger_entries = frappe.db.sql(
77 """
Saurabh554f6f72016-06-06 14:22:37 +053078 SELECT item_code, stock_value, name, warehouse
79 FROM `tabStock Ledger Entry` sle
Rushabh Mehtaf8509872014-10-08 12:03:19 +053080 WHERE posting_date <= %s {0}
Nabin Haita77b8c92020-12-21 14:45:50 +053081 and is_cancelled = 0
Aditya Hase0c164242019-01-07 22:07:13 +053082 ORDER BY timestamp(posting_date, posting_time) DESC, creation DESC
Ankush Menat494bd9e2022-03-28 18:52:46 +053083 """.format(
84 condition
85 ),
86 values,
87 as_dict=1,
88 )
Anand Doshi2ce39cf2014-04-07 18:51:58 +053089
Nabin Hait0dd7be12013-08-02 11:45:43 +053090 sle_map = {}
91 for sle in stock_ledger_entries:
Achilles Rasquinhab4de7e32018-03-09 12:35:47 +053092 if not (sle.item_code, sle.warehouse) in sle_map:
Nabin Hait949a9202017-07-05 13:55:41 +053093 sle_map[(sle.item_code, sle.warehouse)] = flt(sle.stock_value)
Sachin Mane19a5a5d2018-06-21 13:01:48 +053094
Nabin Hait625da792013-09-25 10:32:51 +053095 return sum(sle_map.values())
Anand Doshi2ce39cf2014-04-07 18:51:58 +053096
Ankush Menat494bd9e2022-03-28 18:52:46 +053097
nick98226f48d4b2017-01-09 12:12:36 +053098@frappe.whitelist()
Ankush Menat494bd9e2022-03-28 18:52:46 +053099def get_stock_balance(
100 item_code,
101 warehouse,
102 posting_date=None,
103 posting_time=None,
104 with_valuation_rate=False,
105 with_serial_no=False,
106):
Rushabh Mehta2712e362015-02-17 12:50:20 +0530107 """Returns stock balance quantity at given warehouse on given posting date or current date.
108
109 If `with_valuation_rate` is True, will return tuple (qty, rate)"""
Rushabh Mehtadc93e0a2015-02-20 15:11:56 +0530110
111 from erpnext.stock.stock_ledger import get_previous_sle
112
Ankush Menat494bd9e2022-03-28 18:52:46 +0530113 if posting_date is None:
114 posting_date = nowdate()
115 if posting_time is None:
116 posting_time = nowtime()
Rushabh Mehtadc93e0a2015-02-20 15:11:56 +0530117
Rohit Waghchaure560f8222020-04-06 15:02:43 +0530118 args = {
Rushabh Mehtadc93e0a2015-02-20 15:11:56 +0530119 "item_code": item_code,
Ankush Menat494bd9e2022-03-28 18:52:46 +0530120 "warehouse": warehouse,
Rushabh Mehtadc93e0a2015-02-20 15:11:56 +0530121 "posting_date": posting_date,
Ankush Menat494bd9e2022-03-28 18:52:46 +0530122 "posting_time": posting_time,
Rohit Waghchaure560f8222020-04-06 15:02:43 +0530123 }
124
125 last_entry = get_previous_sle(args)
Rushabh Mehtaf8509872014-10-08 12:03:19 +0530126
Rushabh Mehta2712e362015-02-17 12:50:20 +0530127 if with_valuation_rate:
Rohit Waghchaure560f8222020-04-06 15:02:43 +0530128 if with_serial_no:
Ankush Menat2aa019a2021-10-29 14:32:13 +0530129 serial_nos = get_serial_nos_data_after_transactions(args)
Rohit Waghchaure560f8222020-04-06 15:02:43 +0530130
Ankush Menat494bd9e2022-03-28 18:52:46 +0530131 return (
132 (last_entry.qty_after_transaction, last_entry.valuation_rate, serial_nos)
133 if last_entry
134 else (0.0, 0.0, None)
135 )
Rohit Waghchaure560f8222020-04-06 15:02:43 +0530136 else:
Ankush Menat494bd9e2022-03-28 18:52:46 +0530137 return (
138 (last_entry.qty_after_transaction, last_entry.valuation_rate) if last_entry else (0.0, 0.0)
139 )
Rushabh Mehtaf8509872014-10-08 12:03:19 +0530140 else:
nick9822cc699a92017-06-20 17:13:29 +0530141 return last_entry.qty_after_transaction if last_entry else 0.0
Rushabh Mehtaf8509872014-10-08 12:03:19 +0530142
Ankush Menat494bd9e2022-03-28 18:52:46 +0530143
Rohit Waghchaure560f8222020-04-06 15:02:43 +0530144def get_serial_nos_data_after_transactions(args):
Noah Jacobdeb6b382021-10-21 17:17:11 +0530145 from pypika import CustomFunction
146
Ankush Menatf4b60a42021-10-29 14:56:54 +0530147 serial_nos = set()
Noah Jacobdeb6b382021-10-21 17:17:11 +0530148 args = frappe._dict(args)
Ankush Menat494bd9e2022-03-28 18:52:46 +0530149 sle = frappe.qb.DocType("Stock Ledger Entry")
150 Timestamp = CustomFunction("timestamp", ["date", "time"])
Noah Jacobdeb6b382021-10-21 17:17:11 +0530151
Ankush Menat494bd9e2022-03-28 18:52:46 +0530152 stock_ledger_entries = (
153 frappe.qb.from_(sle)
154 .select("serial_no", "actual_qty")
155 .where(
156 (sle.item_code == args.item_code)
157 & (sle.warehouse == args.warehouse)
158 & (
159 Timestamp(sle.posting_date, sle.posting_time) < Timestamp(args.posting_date, args.posting_time)
160 )
161 & (sle.is_cancelled == 0)
162 )
163 .orderby(sle.posting_date, sle.posting_time, sle.creation)
164 .run(as_dict=1)
165 )
Rohit Waghchaure560f8222020-04-06 15:02:43 +0530166
Ankush Menatf4b60a42021-10-29 14:56:54 +0530167 for stock_ledger_entry in stock_ledger_entries:
168 changed_serial_no = get_serial_nos_data(stock_ledger_entry.serial_no)
169 if stock_ledger_entry.actual_qty > 0:
170 serial_nos.update(changed_serial_no)
Rohit Waghchaure560f8222020-04-06 15:02:43 +0530171 else:
Ankush Menatf4b60a42021-10-29 14:56:54 +0530172 serial_nos.difference_update(changed_serial_no)
Rohit Waghchaure560f8222020-04-06 15:02:43 +0530173
Ankush Menat494bd9e2022-03-28 18:52:46 +0530174 return "\n".join(serial_nos)
175
Rohit Waghchaure560f8222020-04-06 15:02:43 +0530176
177def get_serial_nos_data(serial_nos):
178 from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos
Ankush Menat494bd9e2022-03-28 18:52:46 +0530179
Rohit Waghchaure560f8222020-04-06 15:02:43 +0530180 return get_serial_nos(serial_nos)
181
Ankush Menat494bd9e2022-03-28 18:52:46 +0530182
Nabin Hait949a9202017-07-05 13:55:41 +0530183@frappe.whitelist()
184def get_latest_stock_qty(item_code, warehouse=None):
185 values, condition = [item_code], ""
186 if warehouse:
187 lft, rgt, is_group = frappe.db.get_value("Warehouse", warehouse, ["lft", "rgt", "is_group"])
Sachin Mane19a5a5d2018-06-21 13:01:48 +0530188
Nabin Hait949a9202017-07-05 13:55:41 +0530189 if is_group:
190 values.extend([lft, rgt])
191 condition += "and exists (\
192 select name from `tabWarehouse` wh where wh.name = tabBin.warehouse\
193 and wh.lft >= %s and wh.rgt <= %s)"
Sachin Mane19a5a5d2018-06-21 13:01:48 +0530194
Nabin Hait949a9202017-07-05 13:55:41 +0530195 else:
196 values.append(warehouse)
197 condition += " AND warehouse = %s"
Sachin Mane19a5a5d2018-06-21 13:01:48 +0530198
Ankush Menat494bd9e2022-03-28 18:52:46 +0530199 actual_qty = frappe.db.sql(
200 """select sum(actual_qty) from tabBin
201 where item_code=%s {0}""".format(
202 condition
203 ),
204 values,
205 )[0][0]
Nabin Hait949a9202017-07-05 13:55:41 +0530206
207 return actual_qty
208
209
Nabin Hait47dc3182013-08-06 15:58:16 +0530210def get_latest_stock_balance():
211 bin_map = {}
Ankush Menat494bd9e2022-03-28 18:52:46 +0530212 for d in frappe.db.sql(
213 """SELECT item_code, warehouse, stock_value as stock_value
214 FROM tabBin""",
215 as_dict=1,
216 ):
217 bin_map.setdefault(d.warehouse, {}).setdefault(d.item_code, flt(d.stock_value))
Anand Doshi2ce39cf2014-04-07 18:51:58 +0530218
Nabin Hait47dc3182013-08-06 15:58:16 +0530219 return bin_map
Anand Doshi2ce39cf2014-04-07 18:51:58 +0530220
Ankush Menat494bd9e2022-03-28 18:52:46 +0530221
Nabin Hait74c281c2013-08-19 16:17:18 +0530222def get_bin(item_code, warehouse):
Anand Doshie9baaa62014-02-26 12:35:33 +0530223 bin = frappe.db.get_value("Bin", {"item_code": item_code, "warehouse": warehouse})
Nabin Hait74c281c2013-08-19 16:17:18 +0530224 if not bin:
Ankush Menat08810db2022-01-30 16:25:42 +0530225 bin_obj = _create_bin(item_code, warehouse)
Nabin Hait74c281c2013-08-19 16:17:18 +0530226 else:
Ankush Menat494bd9e2022-03-28 18:52:46 +0530227 bin_obj = frappe.get_doc("Bin", bin, for_update=True)
Anand Doshi6dfd4302015-02-10 14:41:27 +0530228 bin_obj.flags.ignore_permissions = True
Nabin Hait74c281c2013-08-19 16:17:18 +0530229 return bin_obj
230
Ankush Menat494bd9e2022-03-28 18:52:46 +0530231
232def get_or_make_bin(item_code: str, warehouse: str) -> str:
233 bin_record = frappe.db.get_value("Bin", {"item_code": item_code, "warehouse": warehouse})
Deepesh Garg6f107da2021-10-12 20:15:55 +0530234
235 if not bin_record:
Ankush Menat08810db2022-01-30 16:25:42 +0530236 bin_obj = _create_bin(item_code, warehouse)
237 bin_record = bin_obj.name
238 return bin_record
239
Ankush Menat494bd9e2022-03-28 18:52:46 +0530240
Ankush Menat08810db2022-01-30 16:25:42 +0530241def _create_bin(item_code, warehouse):
242 """Create a bin and take care of concurrent inserts."""
243
244 bin_creation_savepoint = "create_bin"
245 try:
246 frappe.db.savepoint(bin_creation_savepoint)
247 bin_obj = frappe.get_doc(doctype="Bin", item_code=item_code, warehouse=warehouse)
Deepesh Garg6f107da2021-10-12 20:15:55 +0530248 bin_obj.flags.ignore_permissions = 1
249 bin_obj.insert()
Ankush Menat08810db2022-01-30 16:25:42 +0530250 except frappe.UniqueValidationError:
251 frappe.db.rollback(save_point=bin_creation_savepoint) # preserve transaction in postgres
252 bin_obj = frappe.get_last_doc("Bin", {"item_code": item_code, "warehouse": warehouse})
Deepesh Garg6f107da2021-10-12 20:15:55 +0530253
Ankush Menat08810db2022-01-30 16:25:42 +0530254 return bin_obj
Deepesh Garg6f107da2021-10-12 20:15:55 +0530255
Ankush Menat494bd9e2022-03-28 18:52:46 +0530256
Nabin Hait5eefff12015-12-07 10:44:56 +0530257@frappe.whitelist()
Nabin Hait7ba092e2018-02-01 10:51:27 +0530258def get_incoming_rate(args, raise_error_if_no_rate=True):
Nabin Hait9d0f6362013-01-07 18:51:11 +0530259 """Get Incoming Rate based on valuation method"""
Ankush Menatab926522022-02-19 15:37:03 +0530260 from erpnext.stock.stock_ledger import (
261 get_batch_incoming_rate,
262 get_previous_sle,
263 get_valuation_rate,
264 )
Ankush Menat494bd9e2022-03-28 18:52:46 +0530265
Ankush Menat8fe5feb2021-11-04 19:48:32 +0530266 if isinstance(args, str):
Nabin Hait41c8cf62015-12-08 14:50:24 +0530267 args = json.loads(args)
Anand Doshi2ce39cf2014-04-07 18:51:58 +0530268
Ankush Menat494bd9e2022-03-28 18:52:46 +0530269 voucher_no = args.get("voucher_no") or args.get("name")
Ankush Menatab926522022-02-19 15:37:03 +0530270
271 in_rate = None
Anand Doshi40a8ae22014-08-29 16:28:31 +0530272 if (args.get("serial_no") or "").strip():
Nabin Hait9d0f6362013-01-07 18:51:11 +0530273 in_rate = get_avg_purchase_rate(args.get("serial_no"))
Ankush Menat494bd9e2022-03-28 18:52:46 +0530274 elif args.get("batch_no") and frappe.db.get_value(
275 "Batch", args.get("batch_no"), "use_batchwise_valuation", cache=True
276 ):
Ankush Menatab926522022-02-19 15:37:03 +0530277 in_rate = get_batch_incoming_rate(
Ankush Menat494bd9e2022-03-28 18:52:46 +0530278 item_code=args.get("item_code"),
279 warehouse=args.get("warehouse"),
Ankush Menatab926522022-02-19 15:37:03 +0530280 batch_no=args.get("batch_no"),
281 posting_date=args.get("posting_date"),
282 posting_time=args.get("posting_time"),
283 )
Nabin Hait9d0f6362013-01-07 18:51:11 +0530284 else:
285 valuation_method = get_valuation_method(args.get("item_code"))
286 previous_sle = get_previous_sle(args)
Ankush Menat494bd9e2022-03-28 18:52:46 +0530287 if valuation_method in ("FIFO", "LIFO"):
rohitwaghchaurece8adec2017-12-15 12:13:50 +0530288 if previous_sle:
Ankush Menat494bd9e2022-03-28 18:52:46 +0530289 previous_stock_queue = json.loads(previous_sle.get("stock_queue", "[]") or "[]")
290 in_rate = (
291 _get_fifo_lifo_rate(previous_stock_queue, args.get("qty") or 0, valuation_method)
292 if previous_stock_queue
293 else 0
294 )
295 elif valuation_method == "Moving Average":
296 in_rate = previous_sle.get("valuation_rate") or 0
Anand Doshi094610d2014-04-16 19:56:53 +0530297
Ankush Menatab926522022-02-19 15:37:03 +0530298 if in_rate is None:
Ankush Menat494bd9e2022-03-28 18:52:46 +0530299 in_rate = get_valuation_rate(
300 args.get("item_code"),
301 args.get("warehouse"),
302 args.get("voucher_type"),
303 voucher_no,
304 args.get("allow_zero_valuation"),
305 currency=erpnext.get_company_currency(args.get("company")),
306 company=args.get("company"),
307 raise_error_if_no_rate=raise_error_if_no_rate,
308 batch_no=args.get("batch_no"),
309 )
rohitwaghchaurece8adec2017-12-15 12:13:50 +0530310
Nabin Haita77b8c92020-12-21 14:45:50 +0530311 return flt(in_rate)
Anand Doshi2ce39cf2014-04-07 18:51:58 +0530312
Ankush Menat494bd9e2022-03-28 18:52:46 +0530313
Nabin Hait9d0f6362013-01-07 18:51:11 +0530314def get_avg_purchase_rate(serial_nos):
315 """get average value of serial numbers"""
Anand Doshi2ce39cf2014-04-07 18:51:58 +0530316
Nabin Hait9d0f6362013-01-07 18:51:11 +0530317 serial_nos = get_valid_serial_nos(serial_nos)
Ankush Menat494bd9e2022-03-28 18:52:46 +0530318 return flt(
319 frappe.db.sql(
320 """select avg(purchase_rate) from `tabSerial No`
321 where name in (%s)"""
322 % ", ".join(["%s"] * len(serial_nos)),
323 tuple(serial_nos),
324 )[0][0]
325 )
326
Nabin Hait9d0f6362013-01-07 18:51:11 +0530327
328def get_valuation_method(item_code):
329 """get valuation method from item or default"""
Ankush Menat494bd9e2022-03-28 18:52:46 +0530330 val_method = frappe.db.get_value("Item", item_code, "valuation_method", cache=True)
Nabin Hait9d0f6362013-01-07 18:51:11 +0530331 if not val_method:
Ankush Menat494bd9e2022-03-28 18:52:46 +0530332 val_method = (
333 frappe.db.get_value("Stock Settings", None, "valuation_method", cache=True) or "FIFO"
334 )
Nabin Hait9d0f6362013-01-07 18:51:11 +0530335 return val_method
Anand Doshi2ce39cf2014-04-07 18:51:58 +0530336
Ankush Menat494bd9e2022-03-28 18:52:46 +0530337
Nabin Hait831207f2013-01-16 14:15:48 +0530338def get_fifo_rate(previous_stock_queue, qty):
339 """get FIFO (average) Rate from Queue"""
Ankush Menat61c5ad42022-01-15 18:06:50 +0530340 return _get_fifo_lifo_rate(previous_stock_queue, qty, "FIFO")
Anand Doshi094610d2014-04-16 19:56:53 +0530341
Ankush Menat494bd9e2022-03-28 18:52:46 +0530342
Ankush Menat61c5ad42022-01-15 18:06:50 +0530343def get_lifo_rate(previous_stock_queue, qty):
344 """get LIFO (average) Rate from Queue"""
345 return _get_fifo_lifo_rate(previous_stock_queue, qty, "LIFO")
346
347
348def _get_fifo_lifo_rate(previous_stock_queue, qty, method):
349 ValuationKlass = LIFOValuation if method == "LIFO" else FIFOValuation
350
351 stock_queue = ValuationKlass(previous_stock_queue)
352 if flt(qty) >= 0:
353 total_qty, total_value = stock_queue.get_total_stock_and_value()
354 return total_value / total_qty if total_qty else 0.0
355 else:
356 popped_bins = stock_queue.remove_stock(abs(flt(qty)))
357
358 total_qty, total_value = ValuationKlass(popped_bins).get_total_stock_and_value()
359 return total_value / total_qty if total_qty else 0.0
Anand Doshi2ce39cf2014-04-07 18:51:58 +0530360
Ankush Menat494bd9e2022-03-28 18:52:46 +0530361
362def get_valid_serial_nos(sr_nos, qty=0, item_code=""):
Nabin Hait9d0f6362013-01-07 18:51:11 +0530363 """split serial nos, validate and return list of valid serial nos"""
364 # TODO: remove duplicates in client side
Ankush Menat494bd9e2022-03-28 18:52:46 +0530365 serial_nos = cstr(sr_nos).strip().replace(",", "\n").split("\n")
Anand Doshi2ce39cf2014-04-07 18:51:58 +0530366
Nabin Hait9d0f6362013-01-07 18:51:11 +0530367 valid_serial_nos = []
368 for val in serial_nos:
369 if val:
370 val = val.strip()
371 if val in valid_serial_nos:
Rushabh Mehta9f0d6252014-04-14 19:20:45 +0530372 frappe.throw(_("Serial number {0} entered more than once").format(val))
Nabin Hait9d0f6362013-01-07 18:51:11 +0530373 else:
374 valid_serial_nos.append(val)
Anand Doshi2ce39cf2014-04-07 18:51:58 +0530375
Nabin Hait9d0f6362013-01-07 18:51:11 +0530376 if qty and len(valid_serial_nos) != abs(qty):
Rushabh Mehta9f0d6252014-04-14 19:20:45 +0530377 frappe.throw(_("{0} valid serial nos for Item {1}").format(abs(qty), item_code))
Anand Doshi2ce39cf2014-04-07 18:51:58 +0530378
Rushabh Mehta0dbe8982013-02-04 13:56:50 +0530379 return valid_serial_nos
Nabin Haita72c5122013-03-06 18:50:53 +0530380
Ankush Menat494bd9e2022-03-28 18:52:46 +0530381
Anand Doshi373680b2013-10-10 16:04:40 +0530382def validate_warehouse_company(warehouse, company):
Ankush91527152021-08-11 11:17:50 +0530383 warehouse_company = frappe.db.get_value("Warehouse", warehouse, "company", cache=True)
Anand Doshi373680b2013-10-10 16:04:40 +0530384 if warehouse_company and warehouse_company != company:
Ankush Menat494bd9e2022-03-28 18:52:46 +0530385 frappe.throw(
386 _("Warehouse {0} does not belong to company {1}").format(warehouse, company),
387 InvalidWarehouseCompany,
388 )
389
Saurabh3d6aecd2016-06-20 17:25:45 +0530390
Saurabh4d029492016-06-23 12:44:06 +0530391def is_group_warehouse(warehouse):
Ankush91527152021-08-11 11:17:50 +0530392 if frappe.db.get_value("Warehouse", warehouse, "is_group", cache=True):
Saurabh4d029492016-06-23 12:44:06 +0530393 frappe.throw(_("Group node warehouse is not allowed to select for transactions"))
Saifb4cf72c2018-10-18 17:29:47 +0500394
Ankush Menat494bd9e2022-03-28 18:52:46 +0530395
Jannat Patel30c88732021-02-11 11:46:48 +0530396def validate_disabled_warehouse(warehouse):
Ankush91527152021-08-11 11:17:50 +0530397 if frappe.db.get_value("Warehouse", warehouse, "disabled", cache=True):
Ankush Menat494bd9e2022-03-28 18:52:46 +0530398 frappe.throw(
399 _("Disabled Warehouse {0} cannot be used for this transaction.").format(
400 get_link_to_form("Warehouse", warehouse)
401 )
402 )
403
Jannat Patel30c88732021-02-11 11:46:48 +0530404
Saifb4cf72c2018-10-18 17:29:47 +0500405def update_included_uom_in_report(columns, result, include_uom, conversion_factors):
406 if not include_uom or not conversion_factors:
407 return
408
409 convertible_cols = {}
rohitwaghchaureed1cc182019-09-30 15:15:52 +0530410 is_dict_obj = False
411 if isinstance(result[0], dict):
412 is_dict_obj = True
413
414 convertible_columns = {}
415 for idx, d in enumerate(columns):
416 key = d.get("fieldname") if is_dict_obj else idx
417 if d.get("convertible"):
418 convertible_columns.setdefault(key, d.get("convertible"))
419
420 # Add new column to show qty/rate as per the selected UOM
Ankush Menat494bd9e2022-03-28 18:52:46 +0530421 columns.insert(
422 idx + 1,
423 {
424 "label": "{0} (per {1})".format(d.get("label"), include_uom),
425 "fieldname": "{0}_{1}".format(d.get("fieldname"), frappe.scrub(include_uom)),
426 "fieldtype": "Currency" if d.get("convertible") == "rate" else "Float",
427 },
428 )
Saifb4cf72c2018-10-18 17:29:47 +0500429
rohitwaghchaure001ee5e2019-11-11 17:43:48 +0530430 update_dict_values = []
Saifb4cf72c2018-10-18 17:29:47 +0500431 for row_idx, row in enumerate(result):
rohitwaghchaureed1cc182019-09-30 15:15:52 +0530432 data = row.items() if is_dict_obj else enumerate(row)
433 for key, value in data:
Noah Jacobd8668f72021-07-15 18:32:15 +0530434 if key not in convertible_columns:
rohitwaghchaureed1cc182019-09-30 15:15:52 +0530435 continue
Noah Jacobd8668f72021-07-15 18:32:15 +0530436 # If no conversion factor for the UOM, defaults to 1
437 if not conversion_factors[row_idx]:
438 conversion_factors[row_idx] = 1
Saifb4cf72c2018-10-18 17:29:47 +0500439
Ankush Menat494bd9e2022-03-28 18:52:46 +0530440 if convertible_columns.get(key) == "rate":
Noah Jacobd8668f72021-07-15 18:32:15 +0530441 new_value = flt(value) * conversion_factors[row_idx]
rohitwaghchaureed1cc182019-09-30 15:15:52 +0530442 else:
Noah Jacobd8668f72021-07-15 18:32:15 +0530443 new_value = flt(value) / conversion_factors[row_idx]
rohitwaghchaureed1cc182019-09-30 15:15:52 +0530444
445 if not is_dict_obj:
Ankush Menat494bd9e2022-03-28 18:52:46 +0530446 row.insert(key + 1, new_value)
rohitwaghchaureed1cc182019-09-30 15:15:52 +0530447 else:
448 new_key = "{0}_{1}".format(key, frappe.scrub(include_uom))
rohitwaghchaure001ee5e2019-11-11 17:43:48 +0530449 update_dict_values.append([row, new_key, new_value])
450
451 for data in update_dict_values:
452 row, key, value = data
453 row[key] = value
Rohit Waghchaure05d3bcb2019-04-28 18:39:18 +0530454
Ankush Menat494bd9e2022-03-28 18:52:46 +0530455
Rohit Waghchaurecf55c9c2019-11-14 18:22:20 +0530456def get_available_serial_nos(args):
Ankush Menat494bd9e2022-03-28 18:52:46 +0530457 return frappe.db.sql(
458 """ SELECT name from `tabSerial No`
Rohit Waghchaurecf55c9c2019-11-14 18:22:20 +0530459 WHERE item_code = %(item_code)s and warehouse = %(warehouse)s
460 and timestamp(purchase_date, purchase_time) <= timestamp(%(posting_date)s, %(posting_time)s)
Ankush Menat494bd9e2022-03-28 18:52:46 +0530461 """,
462 args,
463 as_dict=1,
464 )
465
Suraj Shettybc001d22019-09-16 19:57:04 +0530466
467def add_additional_uom_columns(columns, result, include_uom, conversion_factors):
468 if not include_uom or not conversion_factors:
469 return
470
471 convertible_column_map = {}
472 for col_idx in list(reversed(range(0, len(columns)))):
473 col = columns[col_idx]
Ankush Menat494bd9e2022-03-28 18:52:46 +0530474 if isinstance(col, dict) and col.get("convertible") in ["rate", "qty"]:
Suraj Shettybc001d22019-09-16 19:57:04 +0530475 next_col = col_idx + 1
476 columns.insert(next_col, col.copy())
Ankush Menat494bd9e2022-03-28 18:52:46 +0530477 columns[next_col]["fieldname"] += "_alt"
478 convertible_column_map[col.get("fieldname")] = frappe._dict(
479 {"converted_col": columns[next_col]["fieldname"], "for_type": col.get("convertible")}
480 )
481 if col.get("convertible") == "rate":
482 columns[next_col]["label"] += " (per {})".format(include_uom)
Suraj Shettybc001d22019-09-16 19:57:04 +0530483 else:
Ankush Menat494bd9e2022-03-28 18:52:46 +0530484 columns[next_col]["label"] += " ({})".format(include_uom)
Suraj Shettybc001d22019-09-16 19:57:04 +0530485
486 for row_idx, row in enumerate(result):
487 for convertible_col, data in convertible_column_map.items():
Ankush Menat494bd9e2022-03-28 18:52:46 +0530488 conversion_factor = conversion_factors[row.get("item_code")] or 1
Suraj Shettybc001d22019-09-16 19:57:04 +0530489 for_type = data.for_type
490 value_before_conversion = row.get(convertible_col)
Ankush Menat494bd9e2022-03-28 18:52:46 +0530491 if for_type == "rate":
Suraj Shettybc001d22019-09-16 19:57:04 +0530492 row[data.converted_col] = flt(value_before_conversion) * conversion_factor
493 else:
494 row[data.converted_col] = flt(value_before_conversion) / conversion_factor
495
Deepesh Garg2a9c5ba2020-04-30 10:38:58 +0530496 result[row_idx] = row
497
Ankush Menat494bd9e2022-03-28 18:52:46 +0530498
Deepesh Garg2a9c5ba2020-04-30 10:38:58 +0530499def get_incoming_outgoing_rate_for_cancel(item_code, voucher_type, voucher_no, voucher_detail_no):
Ankush Menat494bd9e2022-03-28 18:52:46 +0530500 outgoing_rate = frappe.db.sql(
501 """SELECT abs(stock_value_difference / actual_qty)
Deepesh Garg2a9c5ba2020-04-30 10:38:58 +0530502 FROM `tabStock Ledger Entry`
503 WHERE voucher_type = %s and voucher_no = %s
504 and item_code = %s and voucher_detail_no = %s
505 ORDER BY CREATION DESC limit 1""",
Ankush Menat494bd9e2022-03-28 18:52:46 +0530506 (voucher_type, voucher_no, item_code, voucher_detail_no),
507 )
Deepesh Garg2a9c5ba2020-04-30 10:38:58 +0530508
509 outgoing_rate = outgoing_rate[0][0] if outgoing_rate else 0.0
510
Nabin Haita77b8c92020-12-21 14:45:50 +0530511 return outgoing_rate
512
Ankush Menat494bd9e2022-03-28 18:52:46 +0530513
Nabin Haita77b8c92020-12-21 14:45:50 +0530514def is_reposting_item_valuation_in_progress():
Ankush Menat494bd9e2022-03-28 18:52:46 +0530515 reposting_in_progress = frappe.db.exists(
516 "Repost Item Valuation", {"docstatus": 1, "status": ["in", ["Queued", "In Progress"]]}
517 )
Nabin Haita77b8c92020-12-21 14:45:50 +0530518 if reposting_in_progress:
Ankush Menat494bd9e2022-03-28 18:52:46 +0530519 frappe.msgprint(
520 _("Item valuation reposting in progress. Report might show incorrect item valuation."), alert=1
521 )
522
Ankush Menatd37541d2021-12-10 12:04:10 +0530523
524def check_pending_reposting(posting_date: str, throw_error: bool = True) -> bool:
525 """Check if there are pending reposting job till the specified posting date."""
526
527 filters = {
528 "docstatus": 1,
Ankush Menatb12fe0f2022-03-29 13:54:26 +0530529 "status": ["in", ["Queued", "In Progress"]],
Ankush Menatd37541d2021-12-10 12:04:10 +0530530 "posting_date": ["<=", posting_date],
531 }
532
Ankush Menat494bd9e2022-03-28 18:52:46 +0530533 reposting_pending = frappe.db.exists("Repost Item Valuation", filters)
Ankush Menatd37541d2021-12-10 12:04:10 +0530534 if reposting_pending and throw_error:
Ankush Menat494bd9e2022-03-28 18:52:46 +0530535 msg = _(
536 "Stock/Accounts can not be frozen as processing of backdated entries is going on. Please try again later."
537 )
538 frappe.msgprint(
539 msg,
540 raise_exception=PendingRepostingError,
541 title="Stock Reposting Ongoing",
542 indicator="red",
543 primary_action={
544 "label": _("Show pending entries"),
545 "client_action": "erpnext.route_to_pending_reposts",
546 "args": filters,
547 },
548 )
Ankush Menatd37541d2021-12-10 12:04:10 +0530549
550 return bool(reposting_pending)