blob: aefc692496b4d46875999c2d2009372ba769f32b [file] [log] [blame]
Maricad6078aa2022-06-17 15:13:13 +05301# Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and Contributors
Rushabh Mehtae67d1fb2013-08-05 14:59:54 +05302# License: GNU General Public License v3. See license.txt
Nabin Hait902e8602013-01-08 18:29:24 +05303
Rohit Waghchaurebb3e5d02021-04-24 17:28:33 +05304import copy
Nabin Hait26d46552013-01-09 15:23:05 +05305import json
Ankush Menatecdb4932022-04-17 19:06:13 +05306from typing import Optional, Set, Tuple
Chillar Anand915b3432021-09-02 16:44:59 +05307
8import frappe
Rohit Waghchaurefb1a40c2023-05-31 14:11:15 +05309from frappe import _, scrub
Chillar Anand915b3432021-09-02 16:44:59 +053010from frappe.model.meta import get_field_precision
Ankush Menate1c16872022-04-21 20:01:48 +053011from frappe.query_builder.functions import CombineDatetime, Sum
Rohit Waghchaurefb1a40c2023-05-31 14:11:15 +053012from frappe.utils import (
13 cint,
14 cstr,
15 flt,
16 get_link_to_form,
17 getdate,
18 gzip_compress,
19 gzip_decompress,
20 now,
21 nowdate,
22 parse_json,
23)
Achilles Rasquinha361366e2018-02-14 17:08:59 +053024
Chillar Anand915b3432021-09-02 16:44:59 +053025import erpnext
Ankush Menatcef84c22021-12-03 12:18:59 +053026from erpnext.stock.doctype.bin.bin import update_qty as update_bin_qty
s-aga-rf0acb202023-04-12 14:13:54 +053027from erpnext.stock.doctype.stock_reservation_entry.stock_reservation_entry import (
28 get_sre_reserved_qty_for_item_and_warehouse as get_reserved_stock,
29)
Rohit Waghchaure46704642023-03-23 11:41:20 +053030from erpnext.stock.serial_batch_bundle import BatchNoValuation, SerialNoValuation
Chillar Anand915b3432021-09-02 16:44:59 +053031from erpnext.stock.utils import (
Chillar Anand915b3432021-09-02 16:44:59 +053032 get_incoming_outgoing_rate_for_cancel,
Deepesh Garg6f107da2021-10-12 20:15:55 +053033 get_or_make_bin,
Chillar Anand915b3432021-09-02 16:44:59 +053034 get_valuation_method,
35)
Ankush Menatb534fee2022-02-19 20:58:36 +053036from erpnext.stock.valuation import FIFOValuation, LIFOValuation, round_off_if_near_zero
Chillar Anand915b3432021-09-02 16:44:59 +053037
Nabin Hait97bce3a2021-07-12 13:24:43 +053038
Ankush Menat494bd9e2022-03-28 18:52:46 +053039class NegativeStockError(frappe.ValidationError):
40 pass
41
42
Rohit Waghchaurebb3e5d02021-04-24 17:28:33 +053043class SerialNoExistsInFutureTransaction(frappe.ValidationError):
44 pass
Nabin Hait902e8602013-01-08 18:29:24 +053045
Anand Doshi5b004ff2013-09-25 19:55:41 +053046
Deepesh Garg2a9c5ba2020-04-30 10:38:58 +053047def make_sl_entries(sl_entries, allow_negative_stock=False, via_landed_cost_voucher=False):
Ankush Menat494bd9e2022-03-28 18:52:46 +053048 """Create SL entries from SL entry dicts
Ankush Menateb8495a2022-03-02 12:01:51 +053049
Ankush Menat494bd9e2022-03-28 18:52:46 +053050 args:
51 - allow_negative_stock: disable negative stock valiations if true
52 - via_landed_cost_voucher: landed cost voucher cancels and reposts
53 entries of purchase document. This flag is used to identify if
54 cancellation and repost is happening via landed cost voucher, in
55 such cases certain validations need to be ignored (like negative
56 stock)
Ankush Menateb8495a2022-03-02 12:01:51 +053057 """
Rohit Waghchaure4d81d452021-06-15 10:21:44 +053058 from erpnext.controllers.stock_controller import future_sle_exists
Ankush Menat494bd9e2022-03-28 18:52:46 +053059
Nabin Haitca775742013-09-26 16:16:44 +053060 if sl_entries:
Deepesh Garg2a9c5ba2020-04-30 10:38:58 +053061 cancel = sl_entries[0].get("is_cancelled")
Nabin Haitca775742013-09-26 16:16:44 +053062 if cancel:
Nabin Hait186a0452021-02-18 14:14:21 +053063 validate_cancellation(sl_entries)
Ankush Menat494bd9e2022-03-28 18:52:46 +053064 set_as_cancel(sl_entries[0].get("voucher_type"), sl_entries[0].get("voucher_no"))
Nabin Haitdc82d4f2014-04-07 12:02:57 +053065
Rohit Waghchaure4d81d452021-06-15 10:21:44 +053066 args = get_args_for_future_sle(sl_entries[0])
67 future_sle_exists(args, sl_entries)
68
Nabin Haitca775742013-09-26 16:16:44 +053069 for sle in sl_entries:
Ankush Menatefc4b942022-03-02 11:19:12 +053070 if sle.serial_no and not via_landed_cost_voucher:
Rohit Waghchaurebb3e5d02021-04-24 17:28:33 +053071 validate_serial_no(sle)
72
Nabin Haita77b8c92020-12-21 14:45:50 +053073 if cancel:
Ankush Menat494bd9e2022-03-28 18:52:46 +053074 sle["actual_qty"] = -flt(sle.get("actual_qty"))
Deepesh Garg2a9c5ba2020-04-30 10:38:58 +053075
Ankush Menat494bd9e2022-03-28 18:52:46 +053076 if sle["actual_qty"] < 0 and not sle.get("outgoing_rate"):
77 sle["outgoing_rate"] = get_incoming_outgoing_rate_for_cancel(
78 sle.item_code, sle.voucher_type, sle.voucher_no, sle.voucher_detail_no
79 )
80 sle["incoming_rate"] = 0.0
Deepesh Garg2a9c5ba2020-04-30 10:38:58 +053081
Ankush Menat494bd9e2022-03-28 18:52:46 +053082 if sle["actual_qty"] > 0 and not sle.get("incoming_rate"):
83 sle["incoming_rate"] = get_incoming_outgoing_rate_for_cancel(
84 sle.item_code, sle.voucher_type, sle.voucher_no, sle.voucher_detail_no
85 )
86 sle["outgoing_rate"] = 0.0
Nabin Haitdc82d4f2014-04-07 12:02:57 +053087
Ankush Menat494bd9e2022-03-28 18:52:46 +053088 if sle.get("actual_qty") or sle.get("voucher_type") == "Stock Reconciliation":
Nabin Haita77b8c92020-12-21 14:45:50 +053089 sle_doc = make_entry(sle, allow_negative_stock, via_landed_cost_voucher)
Deepesh Gargb4be2922021-01-28 13:09:56 +053090
Nabin Haita77b8c92020-12-21 14:45:50 +053091 args = sle_doc.as_dict()
marination40389772021-07-02 17:13:45 +053092
93 if sle.get("voucher_type") == "Stock Reconciliation":
94 # preserve previous_qty_after_transaction for qty reposting
95 args.previous_qty_after_transaction = sle.get("previous_qty_after_transaction")
96
Ankush Menat494bd9e2022-03-28 18:52:46 +053097 is_stock_item = frappe.get_cached_value("Item", args.get("item_code"), "is_stock_item")
Ankush Menatcef84c22021-12-03 12:18:59 +053098 if is_stock_item:
99 bin_name = get_or_make_bin(args.get("item_code"), args.get("warehouse"))
Ankush Menatcef84c22021-12-03 12:18:59 +0530100 repost_current_voucher(args, allow_negative_stock, via_landed_cost_voucher)
Ankush Menatff9a6e82021-12-20 15:07:41 +0530101 update_bin_qty(bin_name, args)
Ankush Menatcef84c22021-12-03 12:18:59 +0530102 else:
Ankush Menat494bd9e2022-03-28 18:52:46 +0530103 frappe.msgprint(
104 _("Item {0} ignored since it is not a stock item").format(args.get("item_code"))
105 )
106
Ankush Menatcef84c22021-12-03 12:18:59 +0530107
108def repost_current_voucher(args, allow_negative_stock=False, via_landed_cost_voucher=False):
109 if args.get("actual_qty") or args.get("voucher_type") == "Stock Reconciliation":
110 if not args.get("posting_date"):
111 args["posting_date"] = nowdate()
112
marination7a5fd712022-07-04 17:46:54 +0530113 if not (args.get("is_cancelled") and via_landed_cost_voucher):
114 # Reposts only current voucher SL Entries
115 # Updates valuation rate, stock value, stock queue for current transaction
116 update_entries_after(
117 {
118 "item_code": args.get("item_code"),
119 "warehouse": args.get("warehouse"),
120 "posting_date": args.get("posting_date"),
121 "posting_time": args.get("posting_time"),
122 "voucher_type": args.get("voucher_type"),
123 "voucher_no": args.get("voucher_no"),
124 "sle_id": args.get("name"),
125 "creation": args.get("creation"),
126 },
127 allow_negative_stock=allow_negative_stock,
128 via_landed_cost_voucher=via_landed_cost_voucher,
129 )
Ankush Menatcef84c22021-12-03 12:18:59 +0530130
131 # update qty in future sle and Validate negative qty
marination7a5fd712022-07-04 17:46:54 +0530132 # For LCV: update future balances with -ve LCV SLE, which will be balanced by +ve LCV SLE
Ankush Menatcef84c22021-12-03 12:18:59 +0530133 update_qty_in_future_sle(args, allow_negative_stock)
134
Nabin Haitadeb9762014-10-06 11:53:52 +0530135
Rohit Waghchaure4d81d452021-06-15 10:21:44 +0530136def get_args_for_future_sle(row):
Ankush Menat494bd9e2022-03-28 18:52:46 +0530137 return frappe._dict(
138 {
139 "voucher_type": row.get("voucher_type"),
140 "voucher_no": row.get("voucher_no"),
141 "posting_date": row.get("posting_date"),
142 "posting_time": row.get("posting_time"),
143 }
144 )
145
Rohit Waghchaure4d81d452021-06-15 10:21:44 +0530146
Rohit Waghchaurebb3e5d02021-04-24 17:28:33 +0530147def validate_serial_no(sle):
148 from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos
Ankush Menat66bf21f2022-01-16 20:45:59 +0530149
Rohit Waghchaurebb3e5d02021-04-24 17:28:33 +0530150 for sn in get_serial_nos(sle.serial_no):
151 args = copy.deepcopy(sle)
152 args.serial_no = sn
Ankush Menat494bd9e2022-03-28 18:52:46 +0530153 args.warehouse = ""
Rohit Waghchaurebb3e5d02021-04-24 17:28:33 +0530154
155 vouchers = []
Ankush Menat494bd9e2022-03-28 18:52:46 +0530156 for row in get_stock_ledger_entries(args, ">"):
Rohit Waghchaurebb3e5d02021-04-24 17:28:33 +0530157 voucher_type = frappe.bold(row.voucher_type)
158 voucher_no = frappe.bold(get_link_to_form(row.voucher_type, row.voucher_no))
Ankush Menat494bd9e2022-03-28 18:52:46 +0530159 vouchers.append(f"{voucher_type} {voucher_no}")
Rohit Waghchaurebb3e5d02021-04-24 17:28:33 +0530160
161 if vouchers:
162 serial_no = frappe.bold(sn)
Ankush Menat494bd9e2022-03-28 18:52:46 +0530163 msg = (
164 f"""The serial no {serial_no} has been used in the future transactions so you need to cancel them first.
165 The list of the transactions are as below."""
166 + "<br><br><ul><li>"
167 )
Rohit Waghchaurebb3e5d02021-04-24 17:28:33 +0530168
Ankush Menat494bd9e2022-03-28 18:52:46 +0530169 msg += "</li><li>".join(vouchers)
170 msg += "</li></ul>"
Rohit Waghchaurebb3e5d02021-04-24 17:28:33 +0530171
Ankush Menat494bd9e2022-03-28 18:52:46 +0530172 title = "Cannot Submit" if not sle.get("is_cancelled") else "Cannot Cancel"
Rohit Waghchaurebb3e5d02021-04-24 17:28:33 +0530173 frappe.throw(_(msg), title=_(title), exc=SerialNoExistsInFutureTransaction)
174
Ankush Menat494bd9e2022-03-28 18:52:46 +0530175
Nabin Hait186a0452021-02-18 14:14:21 +0530176def validate_cancellation(args):
177 if args[0].get("is_cancelled"):
Ankush Menat494bd9e2022-03-28 18:52:46 +0530178 repost_entry = frappe.db.get_value(
179 "Repost Item Valuation",
180 {"voucher_type": args[0].voucher_type, "voucher_no": args[0].voucher_no, "docstatus": 1},
181 ["name", "status"],
182 as_dict=1,
183 )
Nabin Hait186a0452021-02-18 14:14:21 +0530184
185 if repost_entry:
Ankush Menat494bd9e2022-03-28 18:52:46 +0530186 if repost_entry.status == "In Progress":
187 frappe.throw(
188 _(
189 "Cannot cancel the transaction. Reposting of item valuation on submission is not completed yet."
190 )
191 )
192 if repost_entry.status == "Queued":
Nabin Haitd46b2362021-02-23 16:38:52 +0530193 doc = frappe.get_doc("Repost Item Valuation", repost_entry.name)
Ankush Menata2819982022-04-08 13:20:25 +0530194 doc.status = "Skipped"
Ankush Menataa024fc2021-11-18 12:51:26 +0530195 doc.flags.ignore_permissions = True
Nabin Haitd46b2362021-02-23 16:38:52 +0530196 doc.cancel()
Nabin Haitdc82d4f2014-04-07 12:02:57 +0530197
Ankush Menat494bd9e2022-03-28 18:52:46 +0530198
Nabin Hait9653f602013-08-20 15:37:33 +0530199def set_as_cancel(voucher_type, voucher_no):
Ankush Menat494bd9e2022-03-28 18:52:46 +0530200 frappe.db.sql(
201 """update `tabStock Ledger Entry` set is_cancelled=1,
Nabin Hait9653f602013-08-20 15:37:33 +0530202 modified=%s, modified_by=%s
Deepesh Garg2a9c5ba2020-04-30 10:38:58 +0530203 where voucher_type=%s and voucher_no=%s and is_cancelled = 0""",
Ankush Menat494bd9e2022-03-28 18:52:46 +0530204 (now(), frappe.session.user, voucher_type, voucher_no),
205 )
206
Nabin Haitdc82d4f2014-04-07 12:02:57 +0530207
Nabin Hait54c865e2015-03-27 15:38:31 +0530208def make_entry(args, allow_negative_stock=False, via_landed_cost_voucher=False):
Saqib Ansaric7fc6092021-10-12 13:30:40 +0530209 args["doctype"] = "Stock Ledger Entry"
Rushabh Mehtaa504f062014-04-04 12:16:26 +0530210 sle = frappe.get_doc(args)
Anand Doshi6dfd4302015-02-10 14:41:27 +0530211 sle.flags.ignore_permissions = 1
Ankush Menat494bd9e2022-03-28 18:52:46 +0530212 sle.allow_negative_stock = allow_negative_stock
Nabin Hait54c865e2015-03-27 15:38:31 +0530213 sle.via_landed_cost_voucher = via_landed_cost_voucher
Nabin Haitaeba24e2013-08-23 15:17:36 +0530214 sle.submit()
Nabin Haita77b8c92020-12-21 14:45:50 +0530215 return sle
Nabin Haitdc82d4f2014-04-07 12:02:57 +0530216
Ankush Menat494bd9e2022-03-28 18:52:46 +0530217
218def repost_future_sle(
219 args=None,
220 voucher_type=None,
221 voucher_no=None,
222 allow_negative_stock=None,
223 via_landed_cost_voucher=False,
224 doc=None,
225):
Nabin Haite1fa7232022-07-20 15:19:09 +0530226 if not args:
227 args = [] # set args to empty list if None to avoid enumerate error
Rohit Waghchaure1d80d372022-07-19 16:36:22 +0530228
Rohit Waghchaurefb1a40c2023-05-31 14:11:15 +0530229 reposting_data = {}
230 if doc and doc.reposting_data_file:
231 reposting_data = get_reposting_data(doc.reposting_data_file)
232
Rohit Waghchaure1d80d372022-07-19 16:36:22 +0530233 items_to_be_repost = get_items_to_be_repost(
Rohit Waghchaurefb1a40c2023-05-31 14:11:15 +0530234 voucher_type=voucher_type, voucher_no=voucher_no, doc=doc, reposting_data=reposting_data
Rohit Waghchaure1d80d372022-07-19 16:36:22 +0530235 )
236 if items_to_be_repost:
237 args = items_to_be_repost
Deepesh Gargb4be2922021-01-28 13:09:56 +0530238
Rohit Waghchaurefb1a40c2023-05-31 14:11:15 +0530239 distinct_item_warehouses = get_distinct_item_warehouse(args, doc, reposting_data=reposting_data)
240 affected_transactions = get_affected_transactions(doc, reposting_data=reposting_data)
Nabin Haita77b8c92020-12-21 14:45:50 +0530241
rohitwaghchaure31fe5f52021-08-02 11:01:30 +0530242 i = get_current_index(doc) or 0
Nabin Haita77b8c92020-12-21 14:45:50 +0530243 while i < len(args):
rohitwaghchaure31fe5f52021-08-02 11:01:30 +0530244 validate_item_warehouse(args[i])
245
Ankush Menat494bd9e2022-03-28 18:52:46 +0530246 obj = update_entries_after(
247 {
248 "item_code": args[i].get("item_code"),
249 "warehouse": args[i].get("warehouse"),
250 "posting_date": args[i].get("posting_date"),
251 "posting_time": args[i].get("posting_time"),
252 "creation": args[i].get("creation"),
253 "distinct_item_warehouses": distinct_item_warehouses,
254 },
255 allow_negative_stock=allow_negative_stock,
256 via_landed_cost_voucher=via_landed_cost_voucher,
257 )
Ankush Menatecdb4932022-04-17 19:06:13 +0530258 affected_transactions.update(obj.affected_transactions)
Nabin Haita77b8c92020-12-21 14:45:50 +0530259
Ankush Menat494bd9e2022-03-28 18:52:46 +0530260 distinct_item_warehouses[
261 (args[i].get("item_code"), args[i].get("warehouse"))
262 ].reposting_status = True
Deepesh Gargb4be2922021-01-28 13:09:56 +0530263
Nabin Hait97bce3a2021-07-12 13:24:43 +0530264 if obj.new_items_found:
Ankush Menat8fe5feb2021-11-04 19:48:32 +0530265 for item_wh, data in distinct_item_warehouses.items():
Ankush Menat494bd9e2022-03-28 18:52:46 +0530266 if ("args_idx" not in data and not data.reposting_status) or (
267 data.sle_changed and data.reposting_status
268 ):
Nabin Hait97bce3a2021-07-12 13:24:43 +0530269 data.args_idx = len(args)
270 args.append(data.sle)
271 elif data.sle_changed and not data.reposting_status:
272 args[data.args_idx] = data.sle
rohitwaghchaure31fe5f52021-08-02 11:01:30 +0530273
Nabin Hait97bce3a2021-07-12 13:24:43 +0530274 data.sle_changed = False
Nabin Haita77b8c92020-12-21 14:45:50 +0530275 i += 1
276
Rohit Waghchaure78c8bb22022-07-04 20:24:18 +0530277 if doc:
Ankush Menatecdb4932022-04-17 19:06:13 +0530278 update_args_in_repost_item_valuation(
279 doc, i, args, distinct_item_warehouses, affected_transactions
280 )
rohitwaghchaure31fe5f52021-08-02 11:01:30 +0530281
Ankush Menat494bd9e2022-03-28 18:52:46 +0530282
Rohit Waghchaurefb1a40c2023-05-31 14:11:15 +0530283def get_reposting_data(file_path) -> dict:
284 file_name = frappe.db.get_value(
285 "File",
286 {
287 "file_url": file_path,
288 "attached_to_field": "reposting_data_file",
289 },
290 "name",
291 )
292
293 if not file_name:
294 return frappe._dict()
295
296 attached_file = frappe.get_doc("File", file_name)
297
298 data = gzip_decompress(attached_file.get_content())
299 if data := json.loads(data.decode("utf-8")):
300 data = data
301
302 return parse_json(data)
303
304
rohitwaghchaure31fe5f52021-08-02 11:01:30 +0530305def validate_item_warehouse(args):
Ankush Menat494bd9e2022-03-28 18:52:46 +0530306 for field in ["item_code", "warehouse", "posting_date", "posting_time"]:
s-aga-reeda2642022-01-12 20:55:30 +0530307 if args.get(field) in [None, ""]:
s-aga-rba77da02022-11-28 18:01:30 +0530308 validation_msg = f"The field {frappe.unscrub(field)} is required for the reposting"
rohitwaghchaure31fe5f52021-08-02 11:01:30 +0530309 frappe.throw(_(validation_msg))
310
Ankush Menat494bd9e2022-03-28 18:52:46 +0530311
Ankush Menatecdb4932022-04-17 19:06:13 +0530312def update_args_in_repost_item_valuation(
313 doc, index, args, distinct_item_warehouses, affected_transactions
314):
Rohit Waghchaurefb1a40c2023-05-31 14:11:15 +0530315 if not doc.items_to_be_repost:
316 file_name = ""
317 if doc.reposting_data_file:
318 file_name = get_reposting_file_name(doc.doctype, doc.name)
319 # frappe.delete_doc("File", file_name, ignore_permissions=True, delete_permanently=True)
320
321 doc.reposting_data_file = create_json_gz_file(
322 {
323 "items_to_be_repost": args,
324 "distinct_item_and_warehouse": {str(k): v for k, v in distinct_item_warehouses.items()},
325 "affected_transactions": affected_transactions,
326 },
327 doc,
328 file_name,
329 )
330
331 doc.db_set(
332 {
333 "current_index": index,
334 "total_reposting_count": len(args),
335 "reposting_data_file": doc.reposting_data_file,
336 }
337 )
338
339 else:
340 doc.db_set(
341 {
342 "items_to_be_repost": json.dumps(args, default=str),
343 "distinct_item_and_warehouse": json.dumps(
344 {str(k): v for k, v in distinct_item_warehouses.items()}, default=str
345 ),
346 "current_index": index,
347 "affected_transactions": frappe.as_json(affected_transactions),
348 }
349 )
rohitwaghchaure31fe5f52021-08-02 11:01:30 +0530350
Ankush Menatecdb4932022-04-17 19:06:13 +0530351 if not frappe.flags.in_test:
352 frappe.db.commit()
rohitwaghchaure31fe5f52021-08-02 11:01:30 +0530353
Ankush Menat494bd9e2022-03-28 18:52:46 +0530354 frappe.publish_realtime(
355 "item_reposting_progress",
Rohit Waghchaurefb1a40c2023-05-31 14:11:15 +0530356 {
357 "name": doc.name,
358 "items_to_be_repost": json.dumps(args, default=str),
359 "current_index": index,
360 "total_reposting_count": len(args),
361 },
Ankush Menat494bd9e2022-03-28 18:52:46 +0530362 )
363
rohitwaghchaure31fe5f52021-08-02 11:01:30 +0530364
Rohit Waghchaurefb1a40c2023-05-31 14:11:15 +0530365def get_reposting_file_name(dt, dn):
366 return frappe.db.get_value(
367 "File",
368 {
369 "attached_to_doctype": dt,
370 "attached_to_name": dn,
371 "attached_to_field": "reposting_data_file",
372 },
373 "name",
374 )
375
376
377def create_json_gz_file(data, doc, file_name=None) -> str:
378 encoded_content = frappe.safe_encode(frappe.as_json(data))
379 compressed_content = gzip_compress(encoded_content)
380
381 if not file_name:
382 json_filename = f"{scrub(doc.doctype)}-{scrub(doc.name)}.json.gz"
383 _file = frappe.get_doc(
384 {
385 "doctype": "File",
386 "file_name": json_filename,
387 "attached_to_doctype": doc.doctype,
388 "attached_to_name": doc.name,
389 "attached_to_field": "reposting_data_file",
390 "content": compressed_content,
391 "is_private": 1,
392 }
393 )
394 _file.save(ignore_permissions=True)
395
396 return _file.file_url
397 else:
398 file_doc = frappe.get_doc("File", file_name)
399 path = file_doc.get_full_path()
400
401 with open(path, "wb") as f:
402 f.write(compressed_content)
403
404 return doc.reposting_data_file
405
406
407def get_items_to_be_repost(voucher_type=None, voucher_no=None, doc=None, reposting_data=None):
408 if not reposting_data and doc and doc.reposting_data_file:
409 reposting_data = get_reposting_data(doc.reposting_data_file)
410
411 if reposting_data and reposting_data.items_to_be_repost:
412 return reposting_data.items_to_be_repost
413
Rohit Waghchaure1d80d372022-07-19 16:36:22 +0530414 items_to_be_repost = []
Rohit Waghchaurefb1a40c2023-05-31 14:11:15 +0530415
rohitwaghchaure31fe5f52021-08-02 11:01:30 +0530416 if doc and doc.items_to_be_repost:
Rohit Waghchaure1d80d372022-07-19 16:36:22 +0530417 items_to_be_repost = json.loads(doc.items_to_be_repost) or []
rohitwaghchaure31fe5f52021-08-02 11:01:30 +0530418
Rohit Waghchaure1d80d372022-07-19 16:36:22 +0530419 if not items_to_be_repost and voucher_type and voucher_no:
420 items_to_be_repost = frappe.db.get_all(
421 "Stock Ledger Entry",
422 filters={"voucher_type": voucher_type, "voucher_no": voucher_no},
423 fields=["item_code", "warehouse", "posting_date", "posting_time", "creation"],
424 order_by="creation asc",
425 group_by="item_code, warehouse",
426 )
427
Nabin Haite1fa7232022-07-20 15:19:09 +0530428 return items_to_be_repost or []
Nabin Hait74c281c2013-08-19 16:17:18 +0530429
Ankush Menat494bd9e2022-03-28 18:52:46 +0530430
Rohit Waghchaurefb1a40c2023-05-31 14:11:15 +0530431def get_distinct_item_warehouse(args=None, doc=None, reposting_data=None):
432 if not reposting_data and doc and doc.reposting_data_file:
433 reposting_data = get_reposting_data(doc.reposting_data_file)
434
435 if reposting_data and reposting_data.distinct_item_and_warehouse:
436 return reposting_data.distinct_item_and_warehouse
437
rohitwaghchaure31fe5f52021-08-02 11:01:30 +0530438 distinct_item_warehouses = {}
Rohit Waghchaurefb1a40c2023-05-31 14:11:15 +0530439
rohitwaghchaure31fe5f52021-08-02 11:01:30 +0530440 if doc and doc.distinct_item_and_warehouse:
441 distinct_item_warehouses = json.loads(doc.distinct_item_and_warehouse)
Ankush Menat494bd9e2022-03-28 18:52:46 +0530442 distinct_item_warehouses = {
443 frappe.safe_eval(k): frappe._dict(v) for k, v in distinct_item_warehouses.items()
444 }
rohitwaghchaure31fe5f52021-08-02 11:01:30 +0530445 else:
446 for i, d in enumerate(args):
Ankush Menat494bd9e2022-03-28 18:52:46 +0530447 distinct_item_warehouses.setdefault(
448 (d.item_code, d.warehouse), frappe._dict({"reposting_status": False, "sle": d, "args_idx": i})
449 )
rohitwaghchaure31fe5f52021-08-02 11:01:30 +0530450
451 return distinct_item_warehouses
452
Ankush Menat494bd9e2022-03-28 18:52:46 +0530453
Rohit Waghchaurefb1a40c2023-05-31 14:11:15 +0530454def get_affected_transactions(doc, reposting_data=None) -> Set[Tuple[str, str]]:
455 if not reposting_data and doc and doc.reposting_data_file:
456 reposting_data = get_reposting_data(doc.reposting_data_file)
457
458 if reposting_data and reposting_data.affected_transactions:
459 return {tuple(transaction) for transaction in reposting_data.affected_transactions}
460
Ankush Menatecdb4932022-04-17 19:06:13 +0530461 if not doc.affected_transactions:
462 return set()
463
464 transactions = frappe.parse_json(doc.affected_transactions)
465 return {tuple(transaction) for transaction in transactions}
466
467
rohitwaghchaure31fe5f52021-08-02 11:01:30 +0530468def get_current_index(doc=None):
469 if doc and doc.current_index:
470 return doc.current_index
471
Ankush Menat494bd9e2022-03-28 18:52:46 +0530472
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530473class update_entries_after(object):
Nabin Hait902e8602013-01-08 18:29:24 +0530474 """
Ankush Menat494bd9e2022-03-28 18:52:46 +0530475 update valution rate and qty after transaction
476 from the current time-bucket onwards
Nabin Haitdc82d4f2014-04-07 12:02:57 +0530477
Ankush Menat494bd9e2022-03-28 18:52:46 +0530478 :param args: args as dict
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530479
Ankush Menat494bd9e2022-03-28 18:52:46 +0530480 args = {
481 "item_code": "ABC",
482 "warehouse": "XYZ",
483 "posting_date": "2012-12-12",
484 "posting_time": "12:00"
485 }
Nabin Hait902e8602013-01-08 18:29:24 +0530486 """
Ankush Menat494bd9e2022-03-28 18:52:46 +0530487
488 def __init__(
489 self,
490 args,
491 allow_zero_rate=False,
492 allow_negative_stock=None,
493 via_landed_cost_voucher=False,
494 verbose=1,
495 ):
Nabin Haita77b8c92020-12-21 14:45:50 +0530496 self.exceptions = {}
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530497 self.verbose = verbose
498 self.allow_zero_rate = allow_zero_rate
Anand Doshi0dc79f42015-04-06 12:59:34 +0530499 self.via_landed_cost_voucher = via_landed_cost_voucher
Ankush Menateb8b4242022-02-12 13:08:28 +0530500 self.item_code = args.get("item_code")
Ankush Menat494bd9e2022-03-28 18:52:46 +0530501 self.allow_negative_stock = allow_negative_stock or is_negative_stock_allowed(
502 item_code=self.item_code
503 )
Nabin Haitdc82d4f2014-04-07 12:02:57 +0530504
Nabin Haita77b8c92020-12-21 14:45:50 +0530505 self.args = frappe._dict(args)
Nabin Haita77b8c92020-12-21 14:45:50 +0530506 if self.args.sle_id:
Ankush Menat494bd9e2022-03-28 18:52:46 +0530507 self.args["name"] = self.args.sle_id
Nabin Haitd46b2362021-02-23 16:38:52 +0530508
Nabin Haita77b8c92020-12-21 14:45:50 +0530509 self.company = frappe.get_cached_value("Warehouse", self.args.warehouse, "company")
Maricad6078aa2022-06-17 15:13:13 +0530510 self.set_precision()
Nabin Haita77b8c92020-12-21 14:45:50 +0530511 self.valuation_method = get_valuation_method(self.item_code)
Nabin Hait97bce3a2021-07-12 13:24:43 +0530512
513 self.new_items_found = False
514 self.distinct_item_warehouses = args.get("distinct_item_warehouses", frappe._dict())
Ankush Menatecdb4932022-04-17 19:06:13 +0530515 self.affected_transactions: Set[Tuple[str, str]] = set()
s-aga-rf0acb202023-04-12 14:13:54 +0530516 self.reserved_stock = get_reserved_stock(self.args.item_code, self.args.warehouse)
Nabin Haita77b8c92020-12-21 14:45:50 +0530517
518 self.data = frappe._dict()
519 self.initialize_previous_data(self.args)
Nabin Haita77b8c92020-12-21 14:45:50 +0530520 self.build()
Deepesh Gargb4be2922021-01-28 13:09:56 +0530521
Maricad6078aa2022-06-17 15:13:13 +0530522 def set_precision(self):
523 self.flt_precision = cint(frappe.db.get_default("float_precision")) or 2
524 self.currency_precision = get_field_precision(
525 frappe.get_meta("Stock Ledger Entry").get_field("stock_value")
Ankush Menat494bd9e2022-03-28 18:52:46 +0530526 )
Nabin Haita77b8c92020-12-21 14:45:50 +0530527
528 def initialize_previous_data(self, args):
529 """
Ankush Menat494bd9e2022-03-28 18:52:46 +0530530 Get previous sl entries for current item for each related warehouse
531 and assigns into self.data dict
Nabin Haita77b8c92020-12-21 14:45:50 +0530532
Ankush Menat494bd9e2022-03-28 18:52:46 +0530533 :Data Structure:
Nabin Haita77b8c92020-12-21 14:45:50 +0530534
Ankush Menat494bd9e2022-03-28 18:52:46 +0530535 self.data = {
536 warehouse1: {
537 'previus_sle': {},
538 'qty_after_transaction': 10,
539 'valuation_rate': 100,
540 'stock_value': 1000,
541 'prev_stock_value': 1000,
542 'stock_queue': '[[10, 100]]',
543 'stock_value_difference': 1000
544 }
545 }
Nabin Haita77b8c92020-12-21 14:45:50 +0530546
547 """
Ankush Menatc1d986a2021-08-31 19:43:42 +0530548 self.data.setdefault(args.warehouse, frappe._dict())
549 warehouse_dict = self.data[args.warehouse]
marination8418c4b2021-06-22 21:35:25 +0530550 previous_sle = get_previous_sle_of_current_voucher(args)
Ankush Menatc1d986a2021-08-31 19:43:42 +0530551 warehouse_dict.previous_sle = previous_sle
Nabin Haitbb777562013-08-29 18:19:37 +0530552
Ankush Menatc1d986a2021-08-31 19:43:42 +0530553 for key in ("qty_after_transaction", "valuation_rate", "stock_value"):
554 setattr(warehouse_dict, key, flt(previous_sle.get(key)))
555
Ankush Menat494bd9e2022-03-28 18:52:46 +0530556 warehouse_dict.update(
557 {
558 "prev_stock_value": previous_sle.stock_value or 0.0,
559 "stock_queue": json.loads(previous_sle.stock_queue or "[]"),
560 "stock_value_difference": 0.0,
561 }
562 )
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530563
Nabin Haita77b8c92020-12-21 14:45:50 +0530564 def build(self):
Sagar Vorae50324a2021-03-31 12:44:03 +0530565 from erpnext.controllers.stock_controller import future_sle_exists
Nabin Hait186a0452021-02-18 14:14:21 +0530566
Nabin Haita77b8c92020-12-21 14:45:50 +0530567 if self.args.get("sle_id"):
Nabin Hait186a0452021-02-18 14:14:21 +0530568 self.process_sle_against_current_timestamp()
Sagar Vorae50324a2021-03-31 12:44:03 +0530569 if not future_sle_exists(self.args):
Nabin Hait186a0452021-02-18 14:14:21 +0530570 self.update_bin()
Deepesh Garg2a9c5ba2020-04-30 10:38:58 +0530571 else:
Nabin Haita77b8c92020-12-21 14:45:50 +0530572 entries_to_fix = self.get_future_entries_to_fix()
573
574 i = 0
575 while i < len(entries_to_fix):
576 sle = entries_to_fix[i]
577 i += 1
578
Deepesh Garg2a9c5ba2020-04-30 10:38:58 +0530579 self.process_sle(sle)
Rohit Waghchaure9e5e2de2023-05-25 23:41:56 +0530580 self.update_bin_data(sle)
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530581
Nabin Haita77b8c92020-12-21 14:45:50 +0530582 if sle.dependant_sle_voucher_detail_no:
Nabin Hait243d59b2021-02-02 16:55:13 +0530583 entries_to_fix = self.get_dependent_entries_to_fix(entries_to_fix, sle)
Nabin Haitd46b2362021-02-23 16:38:52 +0530584
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530585 if self.exceptions:
586 self.raise_exceptions()
587
Nabin Hait186a0452021-02-18 14:14:21 +0530588 def process_sle_against_current_timestamp(self):
Nabin Haita77b8c92020-12-21 14:45:50 +0530589 sl_entries = self.get_sle_against_current_voucher()
590 for sle in sl_entries:
591 self.process_sle(sle)
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530592
Nabin Haita77b8c92020-12-21 14:45:50 +0530593 def get_sle_against_current_voucher(self):
Ankush Menat494bd9e2022-03-28 18:52:46 +0530594 self.args["time_format"] = "%H:%i:%s"
Nabin Haitf2be0802021-02-15 19:27:49 +0530595
Ankush Menat494bd9e2022-03-28 18:52:46 +0530596 return frappe.db.sql(
597 """
Nabin Haita77b8c92020-12-21 14:45:50 +0530598 select
599 *, timestamp(posting_date, posting_time) as "timestamp"
600 from
601 `tabStock Ledger Entry`
602 where
603 item_code = %(item_code)s
604 and warehouse = %(warehouse)s
rohitwaghchaurefe4540d2021-08-26 12:52:36 +0530605 and is_cancelled = 0
Rohit Waghchaurea05c47e2022-12-22 10:24:04 +0530606 and (
607 posting_date = %(posting_date)s and
608 time_format(posting_time, %(time_format)s) = time_format(%(posting_time)s, %(time_format)s)
609 )
Nabin Haita77b8c92020-12-21 14:45:50 +0530610 order by
611 creation ASC
612 for update
Ankush Menat494bd9e2022-03-28 18:52:46 +0530613 """,
614 self.args,
615 as_dict=1,
616 )
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530617
Nabin Haita77b8c92020-12-21 14:45:50 +0530618 def get_future_entries_to_fix(self):
619 # includes current entry!
Ankush Menat494bd9e2022-03-28 18:52:46 +0530620 args = self.data[self.args.warehouse].previous_sle or frappe._dict(
621 {"item_code": self.item_code, "warehouse": self.args.warehouse}
622 )
Deepesh Gargb4be2922021-01-28 13:09:56 +0530623
Nabin Haita77b8c92020-12-21 14:45:50 +0530624 return list(self.get_sle_after_datetime(args))
Rushabh Mehta538607e2016-06-12 11:03:00 +0530625
Nabin Haita77b8c92020-12-21 14:45:50 +0530626 def get_dependent_entries_to_fix(self, entries_to_fix, sle):
Ankush Menat494bd9e2022-03-28 18:52:46 +0530627 dependant_sle = get_sle_by_voucher_detail_no(
628 sle.dependant_sle_voucher_detail_no, excluded_sle=sle.name
629 )
Deepesh Gargb4be2922021-01-28 13:09:56 +0530630
Nabin Haita77b8c92020-12-21 14:45:50 +0530631 if not dependant_sle:
Nabin Hait243d59b2021-02-02 16:55:13 +0530632 return entries_to_fix
Ankush Menat494bd9e2022-03-28 18:52:46 +0530633 elif (
634 dependant_sle.item_code == self.item_code and dependant_sle.warehouse == self.args.warehouse
635 ):
Nabin Hait243d59b2021-02-02 16:55:13 +0530636 return entries_to_fix
637 elif dependant_sle.item_code != self.item_code:
Nabin Hait97bce3a2021-07-12 13:24:43 +0530638 self.update_distinct_item_warehouses(dependant_sle)
Nabin Hait243d59b2021-02-02 16:55:13 +0530639 return entries_to_fix
640 elif dependant_sle.item_code == self.item_code and dependant_sle.warehouse in self.data:
641 return entries_to_fix
Nabin Hait97bce3a2021-07-12 13:24:43 +0530642 else:
Rohit Waghchaure1d80d372022-07-19 16:36:22 +0530643 self.initialize_previous_data(dependant_sle)
644 self.update_distinct_item_warehouses(dependant_sle)
Rohit Waghchaure78c8bb22022-07-04 20:24:18 +0530645 return entries_to_fix
Nabin Hait97bce3a2021-07-12 13:24:43 +0530646
647 def update_distinct_item_warehouses(self, dependant_sle):
648 key = (dependant_sle.item_code, dependant_sle.warehouse)
Ankush Menat494bd9e2022-03-28 18:52:46 +0530649 val = frappe._dict({"sle": dependant_sle})
Nabin Hait97bce3a2021-07-12 13:24:43 +0530650 if key not in self.distinct_item_warehouses:
651 self.distinct_item_warehouses[key] = val
652 self.new_items_found = True
653 else:
Ankush Menat494bd9e2022-03-28 18:52:46 +0530654 existing_sle_posting_date = (
655 self.distinct_item_warehouses[key].get("sle", {}).get("posting_date")
656 )
Nabin Hait97bce3a2021-07-12 13:24:43 +0530657 if getdate(dependant_sle.posting_date) < getdate(existing_sle_posting_date):
658 val.sle_changed = True
659 self.distinct_item_warehouses[key] = val
660 self.new_items_found = True
661
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530662 def process_sle(self, sle):
Nabin Haita77b8c92020-12-21 14:45:50 +0530663 # previous sle data for this warehouse
664 self.wh_data = self.data[sle.warehouse]
Ankush Menatecdb4932022-04-17 19:06:13 +0530665 self.affected_transactions.add((sle.voucher_type, sle.voucher_no))
Nabin Haita77b8c92020-12-21 14:45:50 +0530666
Anand Doshi0dc79f42015-04-06 12:59:34 +0530667 if (sle.serial_no and not self.via_landed_cost_voucher) or not cint(self.allow_negative_stock):
Nabin Haitdc82d4f2014-04-07 12:02:57 +0530668 # validate negative stock for serialized items, fifo valuation
Nabin Hait902e8602013-01-08 18:29:24 +0530669 # or when negative stock is not allowed for moving average
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530670 if not self.validate_negative_stock(sle):
Nabin Haita77b8c92020-12-21 14:45:50 +0530671 self.wh_data.qty_after_transaction += flt(sle.actual_qty)
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530672 return
Nabin Haitb96c0142014-10-07 11:25:04 +0530673
Nabin Haita77b8c92020-12-21 14:45:50 +0530674 # Get dynamic incoming/outgoing rate
Ankush Menat701878f2022-03-01 18:08:29 +0530675 if not self.args.get("sle_id"):
676 self.get_dynamic_incoming_outgoing_rate(sle)
Deepesh Gargb4be2922021-01-28 13:09:56 +0530677
Rohit Waghchaure3266e542022-10-12 15:09:50 +0530678 if (
Rohit Waghchaure7bfc8f12023-04-14 12:22:19 +0530679 sle.voucher_type == "Stock Reconciliation"
680 and sle.batch_no
681 and sle.voucher_detail_no
682 and sle.actual_qty < 0
683 ):
684 self.reset_actual_qty_for_stock_reco(sle)
685
686 if (
Rohit Waghchaure3266e542022-10-12 15:09:50 +0530687 sle.voucher_type in ["Purchase Receipt", "Purchase Invoice"]
688 and sle.voucher_detail_no
689 and sle.actual_qty < 0
Rohit Waghchaureb5a2ccf2023-05-04 15:38:35 +0530690 and is_internal_transfer(sle)
Rohit Waghchaure3266e542022-10-12 15:09:50 +0530691 ):
692 sle.outgoing_rate = get_incoming_rate_for_inter_company_transfer(sle)
693
Rohit Waghchauree6143ab2023-03-13 14:51:43 +0530694 if sle.serial_and_batch_bundle:
695 if frappe.get_cached_value("Item", sle.item_code, "has_serial_no"):
Rohit Waghchaure46704642023-03-23 11:41:20 +0530696 SerialNoValuation(
Rohit Waghchauree6143ab2023-03-13 14:51:43 +0530697 sle=sle,
698 sle_self=self,
699 wh_data=self.wh_data,
700 warehouse=sle.warehouse,
701 item_code=sle.item_code,
702 )
703 else:
Rohit Waghchaure46704642023-03-23 11:41:20 +0530704 BatchNoValuation(
Rohit Waghchauree6143ab2023-03-13 14:51:43 +0530705 sle=sle,
706 sle_self=self,
707 wh_data=self.wh_data,
708 warehouse=sle.warehouse,
709 item_code=sle.item_code,
710 )
Nabin Haitb96c0142014-10-07 11:25:04 +0530711 else:
Ankush Menat494bd9e2022-03-28 18:52:46 +0530712 if sle.voucher_type == "Stock Reconciliation" and not sle.batch_no:
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530713 # assert
Nabin Haita77b8c92020-12-21 14:45:50 +0530714 self.wh_data.valuation_rate = sle.valuation_rate
715 self.wh_data.qty_after_transaction = sle.qty_after_transaction
Ankush Menat494bd9e2022-03-28 18:52:46 +0530716 self.wh_data.stock_value = flt(self.wh_data.qty_after_transaction) * flt(
717 self.wh_data.valuation_rate
718 )
Ankush Menatb0cf6192022-01-16 13:02:23 +0530719 if self.valuation_method != "Moving Average":
720 self.wh_data.stock_queue = [[self.wh_data.qty_after_transaction, self.wh_data.valuation_rate]]
Nabin Haitb96c0142014-10-07 11:25:04 +0530721 else:
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530722 if self.valuation_method == "Moving Average":
723 self.get_moving_average_values(sle)
Nabin Haita77b8c92020-12-21 14:45:50 +0530724 self.wh_data.qty_after_transaction += flt(sle.actual_qty)
Ankush Menat494bd9e2022-03-28 18:52:46 +0530725 self.wh_data.stock_value = flt(self.wh_data.qty_after_transaction) * flt(
726 self.wh_data.valuation_rate
727 )
Nabin Haitb96c0142014-10-07 11:25:04 +0530728 else:
Ankush Menatf089d392022-02-02 12:51:21 +0530729 self.update_queue_values(sle)
Nabin Haitb96c0142014-10-07 11:25:04 +0530730
Rushabh Mehta54047782013-12-26 11:07:46 +0530731 # rounding as per precision
Maricad6078aa2022-06-17 15:13:13 +0530732 self.wh_data.stock_value = flt(self.wh_data.stock_value, self.currency_precision)
Ankush Menat609d2fc2022-02-20 11:35:53 +0530733 if not self.wh_data.qty_after_transaction:
734 self.wh_data.stock_value = 0.0
Rohit Waghchaurec2d74612023-03-29 11:40:36 +0530735
Nabin Haita77b8c92020-12-21 14:45:50 +0530736 stock_value_difference = self.wh_data.stock_value - self.wh_data.prev_stock_value
737 self.wh_data.prev_stock_value = self.wh_data.stock_value
Nabin Haitdc82d4f2014-04-07 12:02:57 +0530738
Nabin Hait902e8602013-01-08 18:29:24 +0530739 # update current sle
Nabin Haita77b8c92020-12-21 14:45:50 +0530740 sle.qty_after_transaction = self.wh_data.qty_after_transaction
741 sle.valuation_rate = self.wh_data.valuation_rate
742 sle.stock_value = self.wh_data.stock_value
743 sle.stock_queue = json.dumps(self.wh_data.stock_queue)
Rushabh Mehta2e0e7112015-02-18 11:38:05 +0530744 sle.stock_value_difference = stock_value_difference
Ankush Menat494bd9e2022-03-28 18:52:46 +0530745 sle.doctype = "Stock Ledger Entry"
Rohit Waghchaure683a47f2022-10-11 15:11:39 +0530746
Rushabh Mehta8bb6e532015-02-18 20:22:59 +0530747 frappe.get_doc(sle).db_update()
Nabin Haitdc82d4f2014-04-07 12:02:57 +0530748
Ankush Menat701878f2022-03-01 18:08:29 +0530749 if not self.args.get("sle_id"):
750 self.update_outgoing_rate_on_transaction(sle)
Nabin Haita77b8c92020-12-21 14:45:50 +0530751
Rohit Waghchaure7bfc8f12023-04-14 12:22:19 +0530752 def reset_actual_qty_for_stock_reco(self, sle):
753 current_qty = frappe.get_cached_value(
754 "Stock Reconciliation Item", sle.voucher_detail_no, "current_qty"
755 )
756
757 if current_qty:
758 sle.actual_qty = current_qty * -1
759 elif current_qty == 0:
760 sle.is_cancelled = 1
761
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530762 def validate_negative_stock(self, sle):
763 """
Ankush Menat494bd9e2022-03-28 18:52:46 +0530764 validate negative stock for entries current datetime onwards
765 will not consider cancelled entries
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530766 """
s-aga-rf0acb202023-04-12 14:13:54 +0530767 diff = self.wh_data.qty_after_transaction + flt(sle.actual_qty) - flt(self.reserved_stock)
Maricad6078aa2022-06-17 15:13:13 +0530768 diff = flt(diff, self.flt_precision) # respect system precision
Nabin Haitdc82d4f2014-04-07 12:02:57 +0530769
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530770 if diff < 0 and abs(diff) > 0.0001:
771 # negative stock!
772 exc = sle.copy().update({"diff": diff})
Nabin Haita77b8c92020-12-21 14:45:50 +0530773 self.exceptions.setdefault(sle.warehouse, []).append(exc)
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530774 return False
Nabin Hait902e8602013-01-08 18:29:24 +0530775 else:
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530776 return True
777
Nabin Haita77b8c92020-12-21 14:45:50 +0530778 def get_dynamic_incoming_outgoing_rate(self, sle):
779 # Get updated incoming/outgoing rate from transaction
780 if sle.recalculate_rate:
781 rate = self.get_incoming_outgoing_rate_from_transaction(sle)
782
783 if flt(sle.actual_qty) >= 0:
784 sle.incoming_rate = rate
785 else:
786 sle.outgoing_rate = rate
787
788 def get_incoming_outgoing_rate_from_transaction(self, sle):
789 rate = 0
790 # Material Transfer, Repack, Manufacturing
791 if sle.voucher_type == "Stock Entry":
Nabin Hait97bce3a2021-07-12 13:24:43 +0530792 self.recalculate_amounts_in_stock_entry(sle.voucher_no)
Nabin Haita77b8c92020-12-21 14:45:50 +0530793 rate = frappe.db.get_value("Stock Entry Detail", sle.voucher_detail_no, "valuation_rate")
794 # Sales and Purchase Return
Ankush Menat494bd9e2022-03-28 18:52:46 +0530795 elif sle.voucher_type in (
796 "Purchase Receipt",
797 "Purchase Invoice",
798 "Delivery Note",
799 "Sales Invoice",
Sagar Sharma323bdf82022-05-17 15:14:07 +0530800 "Subcontracting Receipt",
Ankush Menat494bd9e2022-03-28 18:52:46 +0530801 ):
Nabin Haita77b8c92020-12-21 14:45:50 +0530802 if frappe.get_cached_value(sle.voucher_type, sle.voucher_no, "is_return"):
Chillar Anand915b3432021-09-02 16:44:59 +0530803 from erpnext.controllers.sales_and_purchase_return import (
804 get_rate_for_return, # don't move this import to top
805 )
Ankush Menat494bd9e2022-03-28 18:52:46 +0530806
807 rate = get_rate_for_return(
808 sle.voucher_type,
809 sle.voucher_no,
810 sle.item_code,
811 voucher_detail_no=sle.voucher_detail_no,
812 sle=sle,
813 )
Rohit Waghchaureddd24ea2022-08-09 14:50:20 +0530814
815 elif (
816 sle.voucher_type in ["Purchase Receipt", "Purchase Invoice"]
Rohit Waghchaurea03b4ce2022-09-08 19:16:00 +0530817 and sle.voucher_detail_no
Rohit Waghchaureb5a2ccf2023-05-04 15:38:35 +0530818 and is_internal_transfer(sle)
Rohit Waghchaureddd24ea2022-08-09 14:50:20 +0530819 ):
Rohit Waghchaure683a47f2022-10-11 15:11:39 +0530820 rate = get_incoming_rate_for_inter_company_transfer(sle)
Nabin Haita77b8c92020-12-21 14:45:50 +0530821 else:
822 if sle.voucher_type in ("Purchase Receipt", "Purchase Invoice"):
Deepesh Gargb4be2922021-01-28 13:09:56 +0530823 rate_field = "valuation_rate"
Sagar Sharma323bdf82022-05-17 15:14:07 +0530824 elif sle.voucher_type == "Subcontracting Receipt":
825 rate_field = "rate"
Nabin Haita77b8c92020-12-21 14:45:50 +0530826 else:
827 rate_field = "incoming_rate"
828
829 # check in item table
Ankush Menat494bd9e2022-03-28 18:52:46 +0530830 item_code, incoming_rate = frappe.db.get_value(
831 sle.voucher_type + " Item", sle.voucher_detail_no, ["item_code", rate_field]
832 )
Nabin Haita77b8c92020-12-21 14:45:50 +0530833
834 if item_code == sle.item_code:
835 rate = incoming_rate
836 else:
837 if sle.voucher_type in ("Delivery Note", "Sales Invoice"):
838 ref_doctype = "Packed Item"
Sagar Sharma323bdf82022-05-17 15:14:07 +0530839 elif sle == "Subcontracting Receipt":
840 ref_doctype = "Subcontracting Receipt Supplied Item"
Nabin Haita77b8c92020-12-21 14:45:50 +0530841 else:
842 ref_doctype = "Purchase Receipt Item Supplied"
Deepesh Gargb4be2922021-01-28 13:09:56 +0530843
Ankush Menat494bd9e2022-03-28 18:52:46 +0530844 rate = frappe.db.get_value(
845 ref_doctype,
846 {"parent_detail_docname": sle.voucher_detail_no, "item_code": sle.item_code},
847 rate_field,
848 )
Nabin Haita77b8c92020-12-21 14:45:50 +0530849
850 return rate
851
852 def update_outgoing_rate_on_transaction(self, sle):
853 """
Ankush Menat494bd9e2022-03-28 18:52:46 +0530854 Update outgoing rate in Stock Entry, Delivery Note, Sales Invoice and Sales Return
855 In case of Stock Entry, also calculate FG Item rate and total incoming/outgoing amount
Nabin Haita77b8c92020-12-21 14:45:50 +0530856 """
857 if sle.actual_qty and sle.voucher_detail_no:
858 outgoing_rate = abs(flt(sle.stock_value_difference)) / abs(sle.actual_qty)
859
860 if flt(sle.actual_qty) < 0 and sle.voucher_type == "Stock Entry":
861 self.update_rate_on_stock_entry(sle, outgoing_rate)
862 elif sle.voucher_type in ("Delivery Note", "Sales Invoice"):
863 self.update_rate_on_delivery_and_sales_return(sle, outgoing_rate)
864 elif flt(sle.actual_qty) < 0 and sle.voucher_type in ("Purchase Receipt", "Purchase Invoice"):
865 self.update_rate_on_purchase_receipt(sle, outgoing_rate)
Sagar Sharma323bdf82022-05-17 15:14:07 +0530866 elif flt(sle.actual_qty) < 0 and sle.voucher_type == "Subcontracting Receipt":
867 self.update_rate_on_subcontracting_receipt(sle, outgoing_rate)
s-aga-r88a3f652023-05-30 16:54:28 +0530868 elif sle.voucher_type == "Stock Reconciliation":
869 self.update_rate_on_stock_reconciliation(sle)
Nabin Haita77b8c92020-12-21 14:45:50 +0530870
871 def update_rate_on_stock_entry(self, sle, outgoing_rate):
872 frappe.db.set_value("Stock Entry Detail", sle.voucher_detail_no, "basic_rate", outgoing_rate)
873
Ankush Menat701878f2022-03-01 18:08:29 +0530874 # Update outgoing item's rate, recalculate FG Item's rate and total incoming/outgoing amount
875 if not sle.dependant_sle_voucher_detail_no:
876 self.recalculate_amounts_in_stock_entry(sle.voucher_no)
Nabin Hait97bce3a2021-07-12 13:24:43 +0530877
878 def recalculate_amounts_in_stock_entry(self, voucher_no):
879 stock_entry = frappe.get_doc("Stock Entry", voucher_no, for_update=True)
Nabin Haita77b8c92020-12-21 14:45:50 +0530880 stock_entry.calculate_rate_and_amount(reset_outgoing_rate=False, raise_error_if_no_rate=False)
881 stock_entry.db_update()
882 for d in stock_entry.items:
883 d.db_update()
Deepesh Gargb4be2922021-01-28 13:09:56 +0530884
Nabin Haita77b8c92020-12-21 14:45:50 +0530885 def update_rate_on_delivery_and_sales_return(self, sle, outgoing_rate):
886 # Update item's incoming rate on transaction
887 item_code = frappe.db.get_value(sle.voucher_type + " Item", sle.voucher_detail_no, "item_code")
888 if item_code == sle.item_code:
Ankush Menat494bd9e2022-03-28 18:52:46 +0530889 frappe.db.set_value(
890 sle.voucher_type + " Item", sle.voucher_detail_no, "incoming_rate", outgoing_rate
891 )
Nabin Haita77b8c92020-12-21 14:45:50 +0530892 else:
893 # packed item
Ankush Menat494bd9e2022-03-28 18:52:46 +0530894 frappe.db.set_value(
895 "Packed Item",
Nabin Haita77b8c92020-12-21 14:45:50 +0530896 {"parent_detail_docname": sle.voucher_detail_no, "item_code": sle.item_code},
Ankush Menat494bd9e2022-03-28 18:52:46 +0530897 "incoming_rate",
898 outgoing_rate,
899 )
Nabin Haita77b8c92020-12-21 14:45:50 +0530900
901 def update_rate_on_purchase_receipt(self, sle, outgoing_rate):
902 if frappe.db.exists(sle.voucher_type + " Item", sle.voucher_detail_no):
Rohit Waghchaure683a47f2022-10-11 15:11:39 +0530903 if sle.voucher_type in ["Purchase Receipt", "Purchase Invoice"] and frappe.get_cached_value(
904 sle.voucher_type, sle.voucher_no, "is_internal_supplier"
905 ):
906 frappe.db.set_value(
907 f"{sle.voucher_type} Item", sle.voucher_detail_no, "valuation_rate", sle.outgoing_rate
908 )
Nabin Haita77b8c92020-12-21 14:45:50 +0530909 else:
Ankush Menat494bd9e2022-03-28 18:52:46 +0530910 frappe.db.set_value(
911 "Purchase Receipt Item Supplied", sle.voucher_detail_no, "rate", outgoing_rate
912 )
Nabin Haita77b8c92020-12-21 14:45:50 +0530913
914 # Recalculate subcontracted item's rate in case of subcontracted purchase receipt/invoice
Sagar Sharmad074c932022-03-31 19:57:42 +0530915 if frappe.get_cached_value(sle.voucher_type, sle.voucher_no, "is_subcontracted"):
Rohit Waghchauree5fb2392021-06-18 20:37:42 +0530916 doc = frappe.get_doc(sle.voucher_type, sle.voucher_no)
Nabin Haita77b8c92020-12-21 14:45:50 +0530917 doc.update_valuation_rate(reset_outgoing_rate=False)
Ankush Menat494bd9e2022-03-28 18:52:46 +0530918 for d in doc.items + doc.supplied_items:
Nabin Haita77b8c92020-12-21 14:45:50 +0530919 d.db_update()
920
Sagar Sharma323bdf82022-05-17 15:14:07 +0530921 def update_rate_on_subcontracting_receipt(self, sle, outgoing_rate):
s-aga-ra6cb6c62023-05-03 09:51:58 +0530922 if frappe.db.exists("Subcontracting Receipt Item", sle.voucher_detail_no):
923 frappe.db.set_value("Subcontracting Receipt Item", sle.voucher_detail_no, "rate", outgoing_rate)
Sagar Sharma323bdf82022-05-17 15:14:07 +0530924 else:
925 frappe.db.set_value(
Sagar Sharma9c72c2a2023-05-12 11:46:32 +0530926 "Subcontracting Receipt Supplied Item",
927 sle.voucher_detail_no,
928 {"rate": outgoing_rate, "amount": abs(sle.actual_qty) * outgoing_rate},
Sagar Sharma323bdf82022-05-17 15:14:07 +0530929 )
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530930
s-aga-ra6cb6c62023-05-03 09:51:58 +0530931 scr = frappe.get_doc("Subcontracting Receipt", sle.voucher_no, for_update=True)
Sagar Sharma9c72c2a2023-05-12 11:46:32 +0530932 scr.calculate_items_qty_and_amount()
s-aga-ra6cb6c62023-05-03 09:51:58 +0530933 scr.db_update()
Sagar Sharma9c72c2a2023-05-12 11:46:32 +0530934 for d in scr.items:
s-aga-ra6cb6c62023-05-03 09:51:58 +0530935 d.db_update()
936
s-aga-r88a3f652023-05-30 16:54:28 +0530937 def update_rate_on_stock_reconciliation(self, sle):
938 if not sle.serial_no and not sle.batch_no:
939 sr = frappe.get_doc("Stock Reconciliation", sle.voucher_no, for_update=True)
940
941 for item in sr.items:
942 # Skip for Serial and Batch Items
943 if item.serial_no or item.batch_no:
944 continue
945
946 previous_sle = get_previous_sle(
947 {
948 "item_code": item.item_code,
949 "warehouse": item.warehouse,
950 "posting_date": sr.posting_date,
951 "posting_time": sr.posting_time,
952 "sle": sle.name,
953 }
954 )
955
956 item.current_qty = previous_sle.get("qty_after_transaction") or 0.0
957 item.current_valuation_rate = previous_sle.get("valuation_rate") or 0.0
958 item.current_amount = flt(item.current_qty) * flt(item.current_valuation_rate)
959
960 item.amount = flt(item.qty) * flt(item.valuation_rate)
961 item.amount_difference = item.amount - item.current_amount
962 else:
963 sr.difference_amount = sum([item.amount_difference for item in sr.items])
964 sr.db_update()
965
966 for item in sr.items:
967 item.db_update()
968
Nabin Hait328c4f92020-01-02 19:00:32 +0530969 def get_incoming_value_for_serial_nos(self, sle, serial_nos):
970 # get rate from serial nos within same company
Ankush Menat494bd9e2022-03-28 18:52:46 +0530971 all_serial_nos = frappe.get_all(
972 "Serial No", fields=["purchase_rate", "name", "company"], filters={"name": ("in", serial_nos)}
973 )
Nabin Hait328c4f92020-01-02 19:00:32 +0530974
Ankush Menat494bd9e2022-03-28 18:52:46 +0530975 incoming_values = sum(flt(d.purchase_rate) for d in all_serial_nos if d.company == sle.company)
Nabin Hait328c4f92020-01-02 19:00:32 +0530976
977 # Get rate for serial nos which has been transferred to other company
Ankush Menat494bd9e2022-03-28 18:52:46 +0530978 invalid_serial_nos = [d.name for d in all_serial_nos if d.company != sle.company]
Nabin Hait328c4f92020-01-02 19:00:32 +0530979 for serial_no in invalid_serial_nos:
Ankush Menat494bd9e2022-03-28 18:52:46 +0530980 incoming_rate = frappe.db.sql(
981 """
Nabin Hait328c4f92020-01-02 19:00:32 +0530982 select incoming_rate
983 from `tabStock Ledger Entry`
984 where
985 company = %s
986 and actual_qty > 0
Ankush Menat82ea9582022-01-16 20:19:04 +0530987 and is_cancelled = 0
Nabin Hait328c4f92020-01-02 19:00:32 +0530988 and (serial_no = %s
989 or serial_no like %s
990 or serial_no like %s
991 or serial_no like %s
992 )
993 order by posting_date desc
994 limit 1
Ankush Menat494bd9e2022-03-28 18:52:46 +0530995 """,
996 (sle.company, serial_no, serial_no + "\n%", "%\n" + serial_no, "%\n" + serial_no + "\n%"),
997 )
Nabin Hait328c4f92020-01-02 19:00:32 +0530998
999 incoming_values += flt(incoming_rate[0][0]) if incoming_rate else 0
1000
1001 return incoming_values
1002
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +05301003 def get_moving_average_values(self, sle):
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +05301004 actual_qty = flt(sle.actual_qty)
Nabin Haita77b8c92020-12-21 14:45:50 +05301005 new_stock_qty = flt(self.wh_data.qty_after_transaction) + actual_qty
Nabin Hait6dfc78b2016-06-24 12:28:55 +05301006 if new_stock_qty >= 0:
1007 if actual_qty > 0:
Nabin Haita77b8c92020-12-21 14:45:50 +05301008 if flt(self.wh_data.qty_after_transaction) <= 0:
1009 self.wh_data.valuation_rate = sle.incoming_rate
Nabin Hait6dfc78b2016-06-24 12:28:55 +05301010 else:
Ankush Menat494bd9e2022-03-28 18:52:46 +05301011 new_stock_value = (self.wh_data.qty_after_transaction * self.wh_data.valuation_rate) + (
1012 actual_qty * sle.incoming_rate
1013 )
Rushabh Mehta14a908b2015-10-15 12:28:20 +05301014
Nabin Haita77b8c92020-12-21 14:45:50 +05301015 self.wh_data.valuation_rate = new_stock_value / new_stock_qty
Rushabh Mehta14a908b2015-10-15 12:28:20 +05301016
Nabin Hait6dfc78b2016-06-24 12:28:55 +05301017 elif sle.outgoing_rate:
1018 if new_stock_qty:
Ankush Menat494bd9e2022-03-28 18:52:46 +05301019 new_stock_value = (self.wh_data.qty_after_transaction * self.wh_data.valuation_rate) + (
1020 actual_qty * sle.outgoing_rate
1021 )
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +05301022
Nabin Haita77b8c92020-12-21 14:45:50 +05301023 self.wh_data.valuation_rate = new_stock_value / new_stock_qty
Nabin Hait6dfc78b2016-06-24 12:28:55 +05301024 else:
Nabin Haita77b8c92020-12-21 14:45:50 +05301025 self.wh_data.valuation_rate = sle.outgoing_rate
Nabin Hait6dfc78b2016-06-24 12:28:55 +05301026 else:
Nabin Haita77b8c92020-12-21 14:45:50 +05301027 if flt(self.wh_data.qty_after_transaction) >= 0 and sle.outgoing_rate:
1028 self.wh_data.valuation_rate = sle.outgoing_rate
Rushabh Mehta14a908b2015-10-15 12:28:20 +05301029
Nabin Haita77b8c92020-12-21 14:45:50 +05301030 if not self.wh_data.valuation_rate and actual_qty > 0:
1031 self.wh_data.valuation_rate = sle.incoming_rate
Rushabh Mehtacc8b2b22017-03-31 12:44:29 +05301032
Rushabh Mehtaaedaac62017-05-04 09:35:19 +05301033 # Get valuation rate from previous SLE or Item master, if item does not have the
Javier Wong9b11d9b2017-04-14 18:24:04 +08001034 # allow zero valuration rate flag set
Nabin Haita77b8c92020-12-21 14:45:50 +05301035 if not self.wh_data.valuation_rate and sle.voucher_detail_no:
Ankush Menat494bd9e2022-03-28 18:52:46 +05301036 allow_zero_valuation_rate = self.check_if_allow_zero_valuation_rate(
1037 sle.voucher_type, sle.voucher_detail_no
1038 )
Javier Wong9b11d9b2017-04-14 18:24:04 +08001039 if not allow_zero_valuation_rate:
Ankush Menatd7ca83e2022-02-19 19:35:33 +05301040 self.wh_data.valuation_rate = self.get_fallback_rate(sle)
Rushabh Mehtacc8b2b22017-03-31 12:44:29 +05301041
Ankush Menatf089d392022-02-02 12:51:21 +05301042 def update_queue_values(self, sle):
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +05301043 incoming_rate = flt(sle.incoming_rate)
1044 actual_qty = flt(sle.actual_qty)
Nabin Haitada485f2015-07-17 15:09:56 +05301045 outgoing_rate = flt(sle.outgoing_rate)
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +05301046
Ankush Menat494bd9e2022-03-28 18:52:46 +05301047 self.wh_data.qty_after_transaction = round_off_if_near_zero(
1048 self.wh_data.qty_after_transaction + actual_qty
1049 )
Ankush Menatb534fee2022-02-19 20:58:36 +05301050
Ankush Menat97e18a12022-01-15 17:42:25 +05301051 if self.valuation_method == "LIFO":
1052 stock_queue = LIFOValuation(self.wh_data.stock_queue)
1053 else:
1054 stock_queue = FIFOValuation(self.wh_data.stock_queue)
1055
Ankush Menatb534fee2022-02-19 20:58:36 +05301056 _prev_qty, prev_stock_value = stock_queue.get_total_stock_and_value()
1057
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +05301058 if actual_qty > 0:
Ankush Menat97e18a12022-01-15 17:42:25 +05301059 stock_queue.add_stock(qty=actual_qty, rate=incoming_rate)
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +05301060 else:
Ankush Menat494bd9e2022-03-28 18:52:46 +05301061
Ankush Menat4b29fb62021-12-18 18:40:22 +05301062 def rate_generator() -> float:
Ankush Menat494bd9e2022-03-28 18:52:46 +05301063 allow_zero_valuation_rate = self.check_if_allow_zero_valuation_rate(
1064 sle.voucher_type, sle.voucher_detail_no
1065 )
Ankush Menat4b29fb62021-12-18 18:40:22 +05301066 if not allow_zero_valuation_rate:
Ankush Menatd7ca83e2022-02-19 19:35:33 +05301067 return self.get_fallback_rate(sle)
Nabin Haitada485f2015-07-17 15:09:56 +05301068 else:
Ankush Menat4b29fb62021-12-18 18:40:22 +05301069 return 0.0
Rushabh Mehtacca33b22016-07-08 18:24:46 +05301070
Ankush Menat494bd9e2022-03-28 18:52:46 +05301071 stock_queue.remove_stock(
1072 qty=abs(actual_qty), outgoing_rate=outgoing_rate, rate_generator=rate_generator
1073 )
Nabin Haitdc82d4f2014-04-07 12:02:57 +05301074
Ankush Menatb534fee2022-02-19 20:58:36 +05301075 _qty, stock_value = stock_queue.get_total_stock_and_value()
1076
1077 stock_value_difference = stock_value - prev_stock_value
Nabin Haitdc82d4f2014-04-07 12:02:57 +05301078
Ankush Menat97e18a12022-01-15 17:42:25 +05301079 self.wh_data.stock_queue = stock_queue.state
Ankush Menat494bd9e2022-03-28 18:52:46 +05301080 self.wh_data.stock_value = round_off_if_near_zero(
1081 self.wh_data.stock_value + stock_value_difference
1082 )
Rushabh Mehtacca33b22016-07-08 18:24:46 +05301083
Nabin Haita77b8c92020-12-21 14:45:50 +05301084 if not self.wh_data.stock_queue:
Ankush Menat494bd9e2022-03-28 18:52:46 +05301085 self.wh_data.stock_queue.append(
1086 [0, sle.incoming_rate or sle.outgoing_rate or self.wh_data.valuation_rate]
1087 )
Rushabh Mehtacc8b2b22017-03-31 12:44:29 +05301088
Ankush Menatb534fee2022-02-19 20:58:36 +05301089 if self.wh_data.qty_after_transaction:
1090 self.wh_data.valuation_rate = self.wh_data.stock_value / self.wh_data.qty_after_transaction
1091
Ankush Menatce0514c2022-02-15 11:41:41 +05301092 def update_batched_values(self, sle):
1093 incoming_rate = flt(sle.incoming_rate)
1094 actual_qty = flt(sle.actual_qty)
Ankush Menat4b29fb62021-12-18 18:40:22 +05301095
Ankush Menat494bd9e2022-03-28 18:52:46 +05301096 self.wh_data.qty_after_transaction = round_off_if_near_zero(
1097 self.wh_data.qty_after_transaction + actual_qty
1098 )
Ankush Menatce0514c2022-02-15 11:41:41 +05301099
1100 if actual_qty > 0:
1101 stock_value_difference = incoming_rate * actual_qty
Ankush Menatce0514c2022-02-15 11:41:41 +05301102 else:
Ankush Menat494bd9e2022-03-28 18:52:46 +05301103 outgoing_rate = get_batch_incoming_rate(
1104 item_code=sle.item_code,
1105 warehouse=sle.warehouse,
Rohit Waghchaure6c9b2122022-12-05 14:48:18 +05301106 serial_and_batch_bundle=sle.serial_and_batch_bundle,
Ankush Menat494bd9e2022-03-28 18:52:46 +05301107 posting_date=sle.posting_date,
1108 posting_time=sle.posting_time,
1109 creation=sle.creation,
1110 )
Ankush Menataba7a7c2022-02-19 19:36:28 +05301111 if outgoing_rate is None:
1112 # This can *only* happen if qty available for the batch is zero.
1113 # in such case fall back various other rates.
1114 # future entries will correct the overall accounting as each
1115 # batch individually uses moving average rates.
1116 outgoing_rate = self.get_fallback_rate(sle)
Ankush Menatce0514c2022-02-15 11:41:41 +05301117 stock_value_difference = outgoing_rate * actual_qty
Ankush Menatce0514c2022-02-15 11:41:41 +05301118
Ankush Menat494bd9e2022-03-28 18:52:46 +05301119 self.wh_data.stock_value = round_off_if_near_zero(
1120 self.wh_data.stock_value + stock_value_difference
1121 )
Ankush Menatce0514c2022-02-15 11:41:41 +05301122 if self.wh_data.qty_after_transaction:
1123 self.wh_data.valuation_rate = self.wh_data.stock_value / self.wh_data.qty_after_transaction
Ankush Menat4b29fb62021-12-18 18:40:22 +05301124
Javier Wong9b11d9b2017-04-14 18:24:04 +08001125 def check_if_allow_zero_valuation_rate(self, voucher_type, voucher_detail_no):
deepeshgarg007f9c0ef32019-07-30 18:49:19 +05301126 ref_item_dt = ""
1127
1128 if voucher_type == "Stock Entry":
1129 ref_item_dt = voucher_type + " Detail"
1130 elif voucher_type in ["Purchase Invoice", "Sales Invoice", "Delivery Note", "Purchase Receipt"]:
1131 ref_item_dt = voucher_type + " Item"
1132
1133 if ref_item_dt:
1134 return frappe.db.get_value(ref_item_dt, voucher_detail_no, "allow_zero_valuation_rate")
1135 else:
1136 return 0
Rushabh Mehtacc8b2b22017-03-31 12:44:29 +05301137
Ankush Menatd7ca83e2022-02-19 19:35:33 +05301138 def get_fallback_rate(self, sle) -> float:
1139 """When exact incoming rate isn't available use any of other "average" rates as fallback.
Ankush Menat494bd9e2022-03-28 18:52:46 +05301140 This should only get used for negative stock."""
1141 return get_valuation_rate(
1142 sle.item_code,
1143 sle.warehouse,
1144 sle.voucher_type,
1145 sle.voucher_no,
1146 self.allow_zero_rate,
1147 currency=erpnext.get_company_currency(sle.company),
1148 company=sle.company,
1149 batch_no=sle.batch_no,
1150 )
Ankush Menatd7ca83e2022-02-19 19:35:33 +05301151
Nabin Haita77b8c92020-12-21 14:45:50 +05301152 def get_sle_before_datetime(self, args):
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +05301153 """get previous stock ledger entry before current time-bucket"""
Nabin Haita77b8c92020-12-21 14:45:50 +05301154 sle = get_stock_ledger_entries(args, "<", "desc", "limit 1", for_update=False)
1155 sle = sle[0] if sle else frappe._dict()
1156 return sle
Deepesh Garg2a9c5ba2020-04-30 10:38:58 +05301157
Nabin Haita77b8c92020-12-21 14:45:50 +05301158 def get_sle_after_datetime(self, args):
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +05301159 """get Stock Ledger Entries after a particular datetime, for reposting"""
Nabin Haita77b8c92020-12-21 14:45:50 +05301160 return get_stock_ledger_entries(args, ">", "asc", for_update=True, check_serial_no=False)
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +05301161
1162 def raise_exceptions(self):
Nabin Haita77b8c92020-12-21 14:45:50 +05301163 msg_list = []
Ankush Menat8fe5feb2021-11-04 19:48:32 +05301164 for warehouse, exceptions in self.exceptions.items():
Nabin Haita77b8c92020-12-21 14:45:50 +05301165 deficiency = min(e["diff"] for e in exceptions)
Rushabh Mehta538607e2016-06-12 11:03:00 +05301166
Ankush Menat494bd9e2022-03-28 18:52:46 +05301167 if (
1168 exceptions[0]["voucher_type"],
1169 exceptions[0]["voucher_no"],
1170 ) in frappe.local.flags.currently_saving:
Nabin Hait3edefb12016-07-20 16:13:18 +05301171
Nabin Haita77b8c92020-12-21 14:45:50 +05301172 msg = _("{0} units of {1} needed in {2} to complete this transaction.").format(
s-aga-rf0acb202023-04-12 14:13:54 +05301173 frappe.bold(abs(deficiency)),
Ankush Menat494bd9e2022-03-28 18:52:46 +05301174 frappe.get_desk_link("Item", exceptions[0]["item_code"]),
1175 frappe.get_desk_link("Warehouse", warehouse),
1176 )
Nabin Haita77b8c92020-12-21 14:45:50 +05301177 else:
Ankush Menat494bd9e2022-03-28 18:52:46 +05301178 msg = _(
1179 "{0} units of {1} needed in {2} on {3} {4} for {5} to complete this transaction."
1180 ).format(
s-aga-rf0acb202023-04-12 14:13:54 +05301181 frappe.bold(abs(deficiency)),
Ankush Menat494bd9e2022-03-28 18:52:46 +05301182 frappe.get_desk_link("Item", exceptions[0]["item_code"]),
1183 frappe.get_desk_link("Warehouse", warehouse),
1184 exceptions[0]["posting_date"],
1185 exceptions[0]["posting_time"],
1186 frappe.get_desk_link(exceptions[0]["voucher_type"], exceptions[0]["voucher_no"]),
1187 )
Rushabh Mehta538607e2016-06-12 11:03:00 +05301188
Nabin Haita77b8c92020-12-21 14:45:50 +05301189 if msg:
s-aga-rf0acb202023-04-12 14:13:54 +05301190 if self.reserved_stock:
s-aga-r7e8fd8f2023-04-21 17:44:44 +05301191 allowed_qty = abs(exceptions[0]["actual_qty"]) - abs(exceptions[0]["diff"])
1192 msg = "{0} As {1} units are reserved, you are allowed to consume only {2} units.".format(
1193 msg, frappe.bold(self.reserved_stock), frappe.bold(allowed_qty)
1194 )
s-aga-rf0acb202023-04-12 14:13:54 +05301195
Nabin Haita77b8c92020-12-21 14:45:50 +05301196 msg_list.append(msg)
1197
1198 if msg_list:
1199 message = "\n\n".join(msg_list)
1200 if self.verbose:
Ankush Menat494bd9e2022-03-28 18:52:46 +05301201 frappe.throw(message, NegativeStockError, title=_("Insufficient Stock"))
Nabin Haita77b8c92020-12-21 14:45:50 +05301202 else:
1203 raise NegativeStockError(message)
Deepesh Gargb4be2922021-01-28 13:09:56 +05301204
Rohit Waghchaure9e5e2de2023-05-25 23:41:56 +05301205 def update_bin_data(self, sle):
1206 bin_name = get_or_make_bin(sle.item_code, sle.warehouse)
Rohit Waghchaure718ad3f2023-05-26 11:29:22 +05301207 values_to_update = {
1208 "actual_qty": sle.qty_after_transaction,
1209 "stock_value": sle.stock_value,
1210 }
1211
1212 if sle.valuation_rate is not None:
1213 values_to_update["valuation_rate"] = sle.valuation_rate
1214
1215 frappe.db.set_value("Bin", bin_name, values_to_update)
Rohit Waghchaure9e5e2de2023-05-25 23:41:56 +05301216
Nabin Haita77b8c92020-12-21 14:45:50 +05301217 def update_bin(self):
1218 # update bin for each warehouse
Ankush Menat8fe5feb2021-11-04 19:48:32 +05301219 for warehouse, data in self.data.items():
Ankush Menat97060c42021-12-03 11:50:38 +05301220 bin_name = get_or_make_bin(self.item_code, warehouse)
Deepesh Garg6f107da2021-10-12 20:15:55 +05301221
Ankush Menat494bd9e2022-03-28 18:52:46 +05301222 updated_values = {"actual_qty": data.qty_after_transaction, "stock_value": data.stock_value}
Ankush Menat7dd10362022-03-10 17:07:57 +05301223 if data.valuation_rate is not None:
1224 updated_values["valuation_rate"] = data.valuation_rate
Ankush Menat8376fbc2022-10-06 20:35:33 +05301225 frappe.db.set_value("Bin", bin_name, updated_values, update_modified=True)
Nabin Haitdc82d4f2014-04-07 12:02:57 +05301226
marination8418c4b2021-06-22 21:35:25 +05301227
Rohit Waghchaure6d513e22023-02-02 18:40:15 +05301228def get_previous_sle_of_current_voucher(args, operator="<", exclude_current_voucher=False):
marination8418c4b2021-06-22 21:35:25 +05301229 """get stock ledger entries filtered by specific posting datetime conditions"""
1230
Ankush Menat494bd9e2022-03-28 18:52:46 +05301231 args["time_format"] = "%H:%i:%s"
marination8418c4b2021-06-22 21:35:25 +05301232 if not args.get("posting_date"):
1233 args["posting_date"] = "1900-01-01"
1234 if not args.get("posting_time"):
1235 args["posting_time"] = "00:00"
1236
1237 voucher_condition = ""
1238 if exclude_current_voucher:
1239 voucher_no = args.get("voucher_no")
1240 voucher_condition = f"and voucher_no != '{voucher_no}'"
1241
Ankush Menat494bd9e2022-03-28 18:52:46 +05301242 sle = frappe.db.sql(
1243 """
marination8418c4b2021-06-22 21:35:25 +05301244 select *, timestamp(posting_date, posting_time) as "timestamp"
1245 from `tabStock Ledger Entry`
1246 where item_code = %(item_code)s
1247 and warehouse = %(warehouse)s
1248 and is_cancelled = 0
1249 {voucher_condition}
Rohit Waghchaurea05c47e2022-12-22 10:24:04 +05301250 and (
1251 posting_date < %(posting_date)s or
1252 (
1253 posting_date = %(posting_date)s and
Rohit Waghchaure6d513e22023-02-02 18:40:15 +05301254 time_format(posting_time, %(time_format)s) {operator} time_format(%(posting_time)s, %(time_format)s)
Rohit Waghchaurea05c47e2022-12-22 10:24:04 +05301255 )
1256 )
marination8418c4b2021-06-22 21:35:25 +05301257 order by timestamp(posting_date, posting_time) desc, creation desc
1258 limit 1
Ankush Menat494bd9e2022-03-28 18:52:46 +05301259 for update""".format(
Rohit Waghchaure6d513e22023-02-02 18:40:15 +05301260 operator=operator, voucher_condition=voucher_condition
Ankush Menat494bd9e2022-03-28 18:52:46 +05301261 ),
1262 args,
1263 as_dict=1,
1264 )
marination8418c4b2021-06-22 21:35:25 +05301265
1266 return sle[0] if sle else frappe._dict()
1267
Ankush Menat494bd9e2022-03-28 18:52:46 +05301268
Anand Doshi4dc7caa2013-01-11 11:44:49 +05301269def get_previous_sle(args, for_update=False):
Anand Doshi1b531862013-01-10 19:29:51 +05301270 """
Ankush Menat494bd9e2022-03-28 18:52:46 +05301271 get the last sle on or before the current time-bucket,
1272 to get actual qty before transaction, this function
1273 is called from various transaction like stock entry, reco etc
Nabin Haitdc82d4f2014-04-07 12:02:57 +05301274
Ankush Menat494bd9e2022-03-28 18:52:46 +05301275 args = {
1276 "item_code": "ABC",
1277 "warehouse": "XYZ",
1278 "posting_date": "2012-12-12",
1279 "posting_time": "12:00",
1280 "sle": "name of reference Stock Ledger Entry"
1281 }
Anand Doshi1b531862013-01-10 19:29:51 +05301282 """
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +05301283 args["name"] = args.get("sle", None) or ""
1284 sle = get_stock_ledger_entries(args, "<=", "desc", "limit 1", for_update=for_update)
Pratik Vyas16371b72013-09-18 18:31:03 +05301285 return sle and sle[0] or {}
Nabin Haitfb6e4342014-10-15 11:34:40 +05301286
Ankush Menat494bd9e2022-03-28 18:52:46 +05301287
1288def get_stock_ledger_entries(
1289 previous_sle,
1290 operator=None,
1291 order="desc",
1292 limit=None,
1293 for_update=False,
1294 debug=False,
1295 check_serial_no=True,
1296):
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +05301297 """get stock ledger entries filtered by specific posting datetime conditions"""
Ankush Menat494bd9e2022-03-28 18:52:46 +05301298 conditions = " and timestamp(posting_date, posting_time) {0} timestamp(%(posting_date)s, %(posting_time)s)".format(
1299 operator
1300 )
Nabin Haitb9ce1042018-02-01 14:58:50 +05301301 if previous_sle.get("warehouse"):
1302 conditions += " and warehouse = %(warehouse)s"
1303 elif previous_sle.get("warehouse_condition"):
1304 conditions += " and " + previous_sle.get("warehouse_condition")
1305
Rohit Waghchaure66aa37f2019-05-24 16:53:51 +05301306 if check_serial_no and previous_sle.get("serial_no"):
Rohit Waghchaurebb3e5d02021-04-24 17:28:33 +05301307 # conditions += " and serial_no like {}".format(frappe.db.escape('%{0}%'.format(previous_sle.get("serial_no"))))
1308 serial_no = previous_sle.get("serial_no")
Ankush Menat494bd9e2022-03-28 18:52:46 +05301309 conditions += (
1310 """ and
Rohit Waghchaurebb3e5d02021-04-24 17:28:33 +05301311 (
1312 serial_no = {0}
1313 or serial_no like {1}
1314 or serial_no like {2}
1315 or serial_no like {3}
1316 )
Ankush Menat494bd9e2022-03-28 18:52:46 +05301317 """
1318 ).format(
1319 frappe.db.escape(serial_no),
1320 frappe.db.escape("{}\n%".format(serial_no)),
1321 frappe.db.escape("%\n{}".format(serial_no)),
1322 frappe.db.escape("%\n{}\n%".format(serial_no)),
1323 )
Rohit Waghchaure05d3bcb2019-04-28 18:39:18 +05301324
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +05301325 if not previous_sle.get("posting_date"):
1326 previous_sle["posting_date"] = "1900-01-01"
1327 if not previous_sle.get("posting_time"):
1328 previous_sle["posting_time"] = "00:00"
1329
1330 if operator in (">", "<=") and previous_sle.get("name"):
1331 conditions += " and name!=%(name)s"
1332
Ankush Menat494bd9e2022-03-28 18:52:46 +05301333 return frappe.db.sql(
1334 """
Deepesh Garg2a9c5ba2020-04-30 10:38:58 +05301335 select *, timestamp(posting_date, posting_time) as "timestamp"
1336 from `tabStock Ledger Entry`
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +05301337 where item_code = %%(item_code)s
Nabin Haita77b8c92020-12-21 14:45:50 +05301338 and is_cancelled = 0
Nabin Haitb9ce1042018-02-01 14:58:50 +05301339 %(conditions)s
Aditya Hase0c164242019-01-07 22:07:13 +05301340 order by timestamp(posting_date, posting_time) %(order)s, creation %(order)s
Ankush Menat494bd9e2022-03-28 18:52:46 +05301341 %(limit)s %(for_update)s"""
1342 % {
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +05301343 "conditions": conditions,
1344 "limit": limit or "",
1345 "for_update": for_update and "for update" or "",
Ankush Menat494bd9e2022-03-28 18:52:46 +05301346 "order": order,
1347 },
1348 previous_sle,
1349 as_dict=1,
1350 debug=debug,
1351 )
1352
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +05301353
Nabin Haita77b8c92020-12-21 14:45:50 +05301354def get_sle_by_voucher_detail_no(voucher_detail_no, excluded_sle=None):
Ankush Menat494bd9e2022-03-28 18:52:46 +05301355 return frappe.db.get_value(
1356 "Stock Ledger Entry",
Rohit Waghchauref8c852c2023-02-01 15:38:12 +05301357 {"voucher_detail_no": voucher_detail_no, "name": ["!=", excluded_sle], "is_cancelled": 0},
Ankush Menat494bd9e2022-03-28 18:52:46 +05301358 [
1359 "item_code",
1360 "warehouse",
1361 "posting_date",
1362 "posting_time",
1363 "timestamp(posting_date, posting_time) as timestamp",
1364 ],
1365 as_dict=1,
1366 )
Deepesh Garg2a9c5ba2020-04-30 10:38:58 +05301367
Ankush Menatce0514c2022-02-15 11:41:41 +05301368
Ankush Menat494bd9e2022-03-28 18:52:46 +05301369def get_batch_incoming_rate(
Rohit Waghchaure6c9b2122022-12-05 14:48:18 +05301370 item_code, warehouse, serial_and_batch_bundle, posting_date, posting_time, creation=None
Ankush Menat494bd9e2022-03-28 18:52:46 +05301371):
1372
Ankush Menat102fff22022-02-19 15:51:04 +05301373 sle = frappe.qb.DocType("Stock Ledger Entry")
Rohit Waghchaure5bb31732023-03-21 10:54:41 +05301374 batch_ledger = frappe.qb.DocType("Serial and Batch Entry")
Ankush Menat102fff22022-02-19 15:51:04 +05301375
Ankush Menate1c16872022-04-21 20:01:48 +05301376 timestamp_condition = CombineDatetime(sle.posting_date, sle.posting_time) < CombineDatetime(
Ankush Menat494bd9e2022-03-28 18:52:46 +05301377 posting_date, posting_time
1378 )
Ankush Menat102fff22022-02-19 15:51:04 +05301379 if creation:
1380 timestamp_condition |= (
Ankush Menate1c16872022-04-21 20:01:48 +05301381 CombineDatetime(sle.posting_date, sle.posting_time)
1382 == CombineDatetime(posting_date, posting_time)
Ankush Menat494bd9e2022-03-28 18:52:46 +05301383 ) & (sle.creation < creation)
Ankush Menat102fff22022-02-19 15:51:04 +05301384
Rohit Waghchaure6c9b2122022-12-05 14:48:18 +05301385 batches = frappe.get_all(
Rohit Waghchaure5bb31732023-03-21 10:54:41 +05301386 "Serial and Batch Entry", fields=["batch_no"], filters={"parent": serial_and_batch_bundle}
Rohit Waghchaure6c9b2122022-12-05 14:48:18 +05301387 )
1388
Ankush Menat102fff22022-02-19 15:51:04 +05301389 batch_details = (
Ankush Menat494bd9e2022-03-28 18:52:46 +05301390 frappe.qb.from_(sle)
Rohit Waghchaure6c9b2122022-12-05 14:48:18 +05301391 .inner_join(batch_ledger)
1392 .on(sle.serial_and_batch_bundle == batch_ledger.parent)
1393 .select(
1394 Sum(
1395 Case()
1396 .when(sle.actual_qty > 0, batch_ledger.qty * batch_ledger.incoming_rate)
1397 .else_(batch_ledger.qty * batch_ledger.outgoing_rate * -1)
1398 ).as_("batch_value"),
1399 Sum(Case().when(sle.actual_qty > 0, batch_ledger.qty).else_(batch_ledger.qty * -1)).as_(
1400 "batch_qty"
1401 ),
1402 )
Ankush Menat494bd9e2022-03-28 18:52:46 +05301403 .where(
1404 (sle.item_code == item_code)
1405 & (sle.warehouse == warehouse)
Rohit Waghchaure6c9b2122022-12-05 14:48:18 +05301406 & (batch_ledger.batch_no.isin([row.batch_no for row in batches]))
Ankush Menat494bd9e2022-03-28 18:52:46 +05301407 & (sle.is_cancelled == 0)
1408 )
1409 .where(timestamp_condition)
Ankush Menat102fff22022-02-19 15:51:04 +05301410 ).run(as_dict=True)
Ankush Menatce0514c2022-02-15 11:41:41 +05301411
1412 if batch_details and batch_details[0].batch_qty:
1413 return batch_details[0].batch_value / batch_details[0].batch_qty
1414
1415
Ankush Menat494bd9e2022-03-28 18:52:46 +05301416def get_valuation_rate(
1417 item_code,
1418 warehouse,
1419 voucher_type,
1420 voucher_no,
1421 allow_zero_rate=False,
1422 currency=None,
1423 company=None,
1424 raise_error_if_no_rate=True,
Rohit Waghchaurec2d74612023-03-29 11:40:36 +05301425 serial_and_batch_bundle=None,
Ankush Menat494bd9e2022-03-28 18:52:46 +05301426):
Rohit Waghchaurea5f40942017-06-16 15:21:36 +05301427
Ankush Menatf7ffe042021-11-01 13:21:14 +05301428 if not company:
Ankush Menat494bd9e2022-03-28 18:52:46 +05301429 company = frappe.get_cached_value("Warehouse", warehouse, "company")
Ankush Menatf7ffe042021-11-01 13:21:14 +05301430
Ankush Menat342d09a2022-02-19 14:28:51 +05301431 last_valuation_rate = None
1432
1433 # Get moving average rate of a specific batch number
Rohit Waghchaurec2d74612023-03-29 11:40:36 +05301434 if warehouse and serial_and_batch_bundle:
1435 batch_obj = BatchNoValuation(
1436 sle=frappe._dict(
1437 {
1438 "item_code": item_code,
1439 "warehouse": warehouse,
1440 "actual_qty": -1,
1441 "serial_and_batch_bundle": serial_and_batch_bundle,
1442 }
1443 )
Ankush Menat494bd9e2022-03-28 18:52:46 +05301444 )
Ankush Menat342d09a2022-02-19 14:28:51 +05301445
Rohit Waghchaurec2d74612023-03-29 11:40:36 +05301446 return batch_obj.get_incoming_rate()
1447
Ankush Menatf7ffe042021-11-01 13:21:14 +05301448 # Get valuation rate from last sle for the same item and warehouse
Ankush Menat342d09a2022-02-19 14:28:51 +05301449 if not last_valuation_rate or last_valuation_rate[0][0] is None:
Ankush Menat494bd9e2022-03-28 18:52:46 +05301450 last_valuation_rate = frappe.db.sql(
1451 """select valuation_rate
Ankush Menat342d09a2022-02-19 14:28:51 +05301452 from `tabStock Ledger Entry` force index (item_warehouse)
1453 where
1454 item_code = %s
1455 AND warehouse = %s
1456 AND valuation_rate >= 0
1457 AND is_cancelled = 0
1458 AND NOT (voucher_no = %s AND voucher_type = %s)
Ankush Menat494bd9e2022-03-28 18:52:46 +05301459 order by posting_date desc, posting_time desc, name desc limit 1""",
1460 (item_code, warehouse, voucher_no, voucher_type),
1461 )
Nabin Haitfb6e4342014-10-15 11:34:40 +05301462
Nabin Haita645f362018-03-01 10:31:24 +05301463 if last_valuation_rate:
Nabin Haita77b8c92020-12-21 14:45:50 +05301464 return flt(last_valuation_rate[0][0])
Nabin Haita645f362018-03-01 10:31:24 +05301465
1466 # If negative stock allowed, and item delivered without any incoming entry,
1467 # system does not found any SLE, then take valuation rate from Item
1468 valuation_rate = frappe.db.get_value("Item", item_code, "valuation_rate")
Nabin Haitfb6e4342014-10-15 11:34:40 +05301469
1470 if not valuation_rate:
Nabin Haita645f362018-03-01 10:31:24 +05301471 # try Item Standard rate
1472 valuation_rate = frappe.db.get_value("Item", item_code, "standard_rate")
Nabin Haitfb6e4342014-10-15 11:34:40 +05301473
Rushabh Mehtaaedaac62017-05-04 09:35:19 +05301474 if not valuation_rate:
Nabin Haita645f362018-03-01 10:31:24 +05301475 # try in price list
Ankush Menat494bd9e2022-03-28 18:52:46 +05301476 valuation_rate = frappe.db.get_value(
1477 "Item Price", dict(item_code=item_code, buying=1, currency=currency), "price_list_rate"
1478 )
Rushabh Mehtacc8b2b22017-03-31 12:44:29 +05301479
Ankush Menat494bd9e2022-03-28 18:52:46 +05301480 if (
1481 not allow_zero_rate
1482 and not valuation_rate
1483 and raise_error_if_no_rate
1484 and cint(erpnext.is_perpetual_inventory_enabled(company))
1485 ):
Rohit Waghchaurebb3e5d02021-04-24 17:28:33 +05301486 form_link = get_link_to_form("Item", item_code)
Marica97715f22020-05-11 20:45:37 +05301487
Ankush Menat494bd9e2022-03-28 18:52:46 +05301488 message = _(
1489 "Valuation Rate for the Item {0}, is required to do accounting entries for {1} {2}."
1490 ).format(form_link, voucher_type, voucher_no)
Rohit Waghchaurebb3e5d02021-04-24 17:28:33 +05301491 message += "<br><br>" + _("Here are the options to proceed:")
Ankush Menat494bd9e2022-03-28 18:52:46 +05301492 solutions = (
1493 "<li>"
1494 + _(
1495 "If the item is transacting as a Zero Valuation Rate item in this entry, please enable 'Allow Zero Valuation Rate' in the {0} Item table."
1496 ).format(voucher_type)
1497 + "</li>"
1498 )
1499 solutions += (
1500 "<li>"
1501 + _("If not, you can Cancel / Submit this entry")
1502 + " {0} ".format(frappe.bold("after"))
1503 + _("performing either one below:")
1504 + "</li>"
1505 )
Marica97715f22020-05-11 20:45:37 +05301506 sub_solutions = "<ul><li>" + _("Create an incoming stock transaction for the Item.") + "</li>"
1507 sub_solutions += "<li>" + _("Mention Valuation Rate in the Item master.") + "</li></ul>"
1508 msg = message + solutions + sub_solutions + "</li>"
1509
1510 frappe.throw(msg=msg, title=_("Valuation Rate Missing"))
Nabin Haitfb6e4342014-10-15 11:34:40 +05301511
1512 return valuation_rate
Nabin Haita77b8c92020-12-21 14:45:50 +05301513
Ankush Menat494bd9e2022-03-28 18:52:46 +05301514
Ankush Menate7109c12021-08-26 16:40:45 +05301515def update_qty_in_future_sle(args, allow_negative_stock=False):
marination8418c4b2021-06-22 21:35:25 +05301516 """Recalculate Qty after Transaction in future SLEs based on current SLE."""
marination40389772021-07-02 17:13:45 +05301517 datetime_limit_condition = ""
marination8418c4b2021-06-22 21:35:25 +05301518 qty_shift = args.actual_qty
1519
Ankush Menat7c839c42022-05-06 12:09:08 +05301520 args["time_format"] = "%H:%i:%s"
1521
marination8418c4b2021-06-22 21:35:25 +05301522 # find difference/shift in qty caused by stock reconciliation
1523 if args.voucher_type == "Stock Reconciliation":
marination40389772021-07-02 17:13:45 +05301524 qty_shift = get_stock_reco_qty_shift(args)
1525
1526 # find the next nearest stock reco so that we only recalculate SLEs till that point
1527 next_stock_reco_detail = get_next_stock_reco(args)
1528 if next_stock_reco_detail:
1529 detail = next_stock_reco_detail[0]
Rohit Waghchaureef4bd772023-04-04 23:56:57 +05301530 if detail.batch_no:
1531 regenerate_sle_for_batch_stock_reco(detail)
1532
marination40389772021-07-02 17:13:45 +05301533 # add condition to update SLEs before this date & time
1534 datetime_limit_condition = get_datetime_limit_condition(detail)
marination8418c4b2021-06-22 21:35:25 +05301535
Ankush Menat494bd9e2022-03-28 18:52:46 +05301536 frappe.db.sql(
Ankush Menat7e2fbc02022-05-09 11:13:31 +05301537 f"""
Nabin Hait186a0452021-02-18 14:14:21 +05301538 update `tabStock Ledger Entry`
marination8418c4b2021-06-22 21:35:25 +05301539 set qty_after_transaction = qty_after_transaction + {qty_shift}
Nabin Hait186a0452021-02-18 14:14:21 +05301540 where
1541 item_code = %(item_code)s
1542 and warehouse = %(warehouse)s
1543 and voucher_no != %(voucher_no)s
1544 and is_cancelled = 0
Rohit Waghchaurea05c47e2022-12-22 10:24:04 +05301545 and (
1546 posting_date > %(posting_date)s or
1547 (
1548 posting_date = %(posting_date)s and
1549 time_format(posting_time, %(time_format)s) > time_format(%(posting_time)s, %(time_format)s)
1550 )
1551 )
marination40389772021-07-02 17:13:45 +05301552 {datetime_limit_condition}
Ankush Menat7e2fbc02022-05-09 11:13:31 +05301553 """,
Ankush Menat494bd9e2022-03-28 18:52:46 +05301554 args,
1555 )
Nabin Hait186a0452021-02-18 14:14:21 +05301556
1557 validate_negative_qty_in_future_sle(args, allow_negative_stock)
1558
Ankush Menat494bd9e2022-03-28 18:52:46 +05301559
Rohit Waghchaureef4bd772023-04-04 23:56:57 +05301560def regenerate_sle_for_batch_stock_reco(detail):
1561 doc = frappe.get_cached_doc("Stock Reconciliation", detail.voucher_no)
Rohit Waghchaureef4bd772023-04-04 23:56:57 +05301562 doc.recalculate_current_qty(detail.item_code, detail.batch_no)
Rohit Waghchauref2253dd2023-04-22 11:16:12 +05301563
1564 if not frappe.db.exists(
1565 "Repost Item Valuation", {"voucher_no": doc.name, "status": "Queued", "docstatus": "1"}
1566 ):
Rohit Waghchaure2d5ccc02023-05-01 21:17:18 +05301567 doc.repost_future_sle_and_gle(force=True)
Rohit Waghchaureef4bd772023-04-04 23:56:57 +05301568
1569
marination40389772021-07-02 17:13:45 +05301570def get_stock_reco_qty_shift(args):
1571 stock_reco_qty_shift = 0
1572 if args.get("is_cancelled"):
1573 if args.get("previous_qty_after_transaction"):
1574 # get qty (balance) that was set at submission
1575 last_balance = args.get("previous_qty_after_transaction")
1576 stock_reco_qty_shift = flt(args.qty_after_transaction) - flt(last_balance)
1577 else:
1578 stock_reco_qty_shift = flt(args.actual_qty)
1579 else:
1580 # reco is being submitted
Rohit Waghchaure6d513e22023-02-02 18:40:15 +05301581 last_balance = get_previous_sle_of_current_voucher(args, "<=", exclude_current_voucher=True).get(
Ankush Menat494bd9e2022-03-28 18:52:46 +05301582 "qty_after_transaction"
1583 )
marination40389772021-07-02 17:13:45 +05301584
1585 if last_balance is not None:
1586 stock_reco_qty_shift = flt(args.qty_after_transaction) - flt(last_balance)
1587 else:
1588 stock_reco_qty_shift = args.qty_after_transaction
1589
1590 return stock_reco_qty_shift
1591
Ankush Menat494bd9e2022-03-28 18:52:46 +05301592
Rohit Waghchaured9dd64b2023-04-14 13:00:12 +05301593def get_next_stock_reco(kwargs):
marination40389772021-07-02 17:13:45 +05301594 """Returns next nearest stock reconciliaton's details."""
1595
Rohit Waghchaured9dd64b2023-04-14 13:00:12 +05301596 sle = frappe.qb.DocType("Stock Ledger Entry")
1597
1598 query = (
1599 frappe.qb.from_(sle)
1600 .select(
1601 sle.name,
1602 sle.posting_date,
1603 sle.posting_time,
1604 sle.creation,
1605 sle.voucher_no,
1606 sle.item_code,
1607 sle.batch_no,
1608 sle.actual_qty,
1609 )
1610 .where(
1611 (sle.item_code == kwargs.get("item_code"))
1612 & (sle.warehouse == kwargs.get("warehouse"))
1613 & (sle.voucher_type == "Stock Reconciliation")
1614 & (sle.voucher_no != kwargs.get("voucher_no"))
1615 & (sle.is_cancelled == 0)
1616 & (
1617 (
1618 CombineDatetime(sle.posting_date, sle.posting_time)
1619 > CombineDatetime(kwargs.get("posting_date"), kwargs.get("posting_time"))
Rohit Waghchaure379b2152023-04-24 17:32:32 +05301620 )
1621 | (
1622 (
1623 CombineDatetime(sle.posting_date, sle.posting_time)
1624 == CombineDatetime(kwargs.get("posting_date"), kwargs.get("posting_time"))
Rohit Waghchaured9dd64b2023-04-14 13:00:12 +05301625 )
Rohit Waghchaure379b2152023-04-24 17:32:32 +05301626 & (sle.creation > kwargs.get("creation"))
marination40389772021-07-02 17:13:45 +05301627 )
1628 )
Rohit Waghchaured9dd64b2023-04-14 13:00:12 +05301629 )
Rohit Waghchaure6bccd862023-04-17 14:22:27 +05301630 .orderby(CombineDatetime(sle.posting_date, sle.posting_time))
1631 .orderby(sle.creation)
Rohit Waghchaurefcfa8842023-04-20 09:48:15 +05301632 .limit(1)
Ankush Menat494bd9e2022-03-28 18:52:46 +05301633 )
1634
Rohit Waghchaured9dd64b2023-04-14 13:00:12 +05301635 if kwargs.get("batch_no"):
Deepesh Gargea6eeac2023-04-20 12:48:44 +05301636 query = query.where(sle.batch_no == kwargs.get("batch_no"))
Rohit Waghchaured9dd64b2023-04-14 13:00:12 +05301637
1638 return query.run(as_dict=True)
1639
marination40389772021-07-02 17:13:45 +05301640
1641def get_datetime_limit_condition(detail):
marination40389772021-07-02 17:13:45 +05301642 return f"""
1643 and
1644 (timestamp(posting_date, posting_time) < timestamp('{detail.posting_date}', '{detail.posting_time}')
1645 or (
1646 timestamp(posting_date, posting_time) = timestamp('{detail.posting_date}', '{detail.posting_time}')
1647 and creation < '{detail.creation}'
1648 )
1649 )"""
1650
Ankush Menat494bd9e2022-03-28 18:52:46 +05301651
Ankush Menate7109c12021-08-26 16:40:45 +05301652def validate_negative_qty_in_future_sle(args, allow_negative_stock=False):
Ankush Menateb8b4242022-02-12 13:08:28 +05301653 if allow_negative_stock or is_negative_stock_allowed(item_code=args.item_code):
Ankush Menat5eba5752021-12-07 23:03:52 +05301654 return
1655 if not (args.actual_qty < 0 or args.voucher_type == "Stock Reconciliation"):
1656 return
Deepesh Gargb4be2922021-01-28 13:09:56 +05301657
Ankush Menat5eba5752021-12-07 23:03:52 +05301658 neg_sle = get_future_sle_with_negative_qty(args)
Maricad6078aa2022-06-17 15:13:13 +05301659
1660 if is_negative_with_precision(neg_sle):
Ankush Menat494bd9e2022-03-28 18:52:46 +05301661 message = _(
1662 "{0} units of {1} needed in {2} on {3} {4} for {5} to complete this transaction."
1663 ).format(
Ankush Menat5eba5752021-12-07 23:03:52 +05301664 abs(neg_sle[0]["qty_after_transaction"]),
Ankush Menat494bd9e2022-03-28 18:52:46 +05301665 frappe.get_desk_link("Item", args.item_code),
1666 frappe.get_desk_link("Warehouse", args.warehouse),
1667 neg_sle[0]["posting_date"],
1668 neg_sle[0]["posting_time"],
1669 frappe.get_desk_link(neg_sle[0]["voucher_type"], neg_sle[0]["voucher_no"]),
1670 )
Ankush Menat5eba5752021-12-07 23:03:52 +05301671
Ankush Menat494bd9e2022-03-28 18:52:46 +05301672 frappe.throw(message, NegativeStockError, title=_("Insufficient Stock"))
Ankush Menat5eba5752021-12-07 23:03:52 +05301673
1674 if not args.batch_no:
1675 return
1676
1677 neg_batch_sle = get_future_sle_with_negative_batch_qty(args)
Maricad6078aa2022-06-17 15:13:13 +05301678 if is_negative_with_precision(neg_batch_sle, is_batch=True):
Ankush Menat494bd9e2022-03-28 18:52:46 +05301679 message = _(
1680 "{0} units of {1} needed in {2} on {3} {4} for {5} to complete this transaction."
1681 ).format(
Ankush Menat5eba5752021-12-07 23:03:52 +05301682 abs(neg_batch_sle[0]["cumulative_total"]),
Ankush Menat494bd9e2022-03-28 18:52:46 +05301683 frappe.get_desk_link("Batch", args.batch_no),
1684 frappe.get_desk_link("Warehouse", args.warehouse),
1685 neg_batch_sle[0]["posting_date"],
1686 neg_batch_sle[0]["posting_time"],
1687 frappe.get_desk_link(neg_batch_sle[0]["voucher_type"], neg_batch_sle[0]["voucher_no"]),
1688 )
mergify[bot]7a3d3012022-03-08 10:42:30 +05301689 frappe.throw(message, NegativeStockError, title=_("Insufficient Stock for Batch"))
Ankush Menat5eba5752021-12-07 23:03:52 +05301690
Nabin Haita77b8c92020-12-21 14:45:50 +05301691
Maricad6078aa2022-06-17 15:13:13 +05301692def is_negative_with_precision(neg_sle, is_batch=False):
1693 """
1694 Returns whether system precision rounded qty is insufficient.
1695 E.g: -0.0003 in precision 3 (0.000) is sufficient for the user.
1696 """
1697
1698 if not neg_sle:
1699 return False
1700
1701 field = "cumulative_total" if is_batch else "qty_after_transaction"
1702 precision = cint(frappe.db.get_default("float_precision")) or 2
1703 qty_deficit = flt(neg_sle[0][field], precision)
1704
1705 return qty_deficit < 0 and abs(qty_deficit) > 0.0001
1706
1707
Nabin Haita77b8c92020-12-21 14:45:50 +05301708def get_future_sle_with_negative_qty(args):
Ankush Menat494bd9e2022-03-28 18:52:46 +05301709 return frappe.db.sql(
1710 """
Nabin Haita77b8c92020-12-21 14:45:50 +05301711 select
1712 qty_after_transaction, posting_date, posting_time,
1713 voucher_type, voucher_no
1714 from `tabStock Ledger Entry`
Deepesh Gargb4be2922021-01-28 13:09:56 +05301715 where
Nabin Haita77b8c92020-12-21 14:45:50 +05301716 item_code = %(item_code)s
1717 and warehouse = %(warehouse)s
1718 and voucher_no != %(voucher_no)s
1719 and timestamp(posting_date, posting_time) >= timestamp(%(posting_date)s, %(posting_time)s)
1720 and is_cancelled = 0
Nabin Hait186a0452021-02-18 14:14:21 +05301721 and qty_after_transaction < 0
Nabin Hait243d59b2021-02-02 16:55:13 +05301722 order by timestamp(posting_date, posting_time) asc
Nabin Haita77b8c92020-12-21 14:45:50 +05301723 limit 1
Ankush Menat494bd9e2022-03-28 18:52:46 +05301724 """,
1725 args,
1726 as_dict=1,
1727 )
Ankush Menat6a014d12021-04-12 20:21:27 +05301728
Ankush Menat5eba5752021-12-07 23:03:52 +05301729
1730def get_future_sle_with_negative_batch_qty(args):
Ankush Menat494bd9e2022-03-28 18:52:46 +05301731 return frappe.db.sql(
1732 """
Ankush Menat5eba5752021-12-07 23:03:52 +05301733 with batch_ledger as (
1734 select
1735 posting_date, posting_time, voucher_type, voucher_no,
1736 sum(actual_qty) over (order by posting_date, posting_time, creation) as cumulative_total
1737 from `tabStock Ledger Entry`
1738 where
1739 item_code = %(item_code)s
1740 and warehouse = %(warehouse)s
1741 and batch_no=%(batch_no)s
1742 and is_cancelled = 0
1743 order by posting_date, posting_time, creation
1744 )
1745 select * from batch_ledger
1746 where
1747 cumulative_total < 0.0
1748 and timestamp(posting_date, posting_time) >= timestamp(%(posting_date)s, %(posting_time)s)
1749 limit 1
Ankush Menat494bd9e2022-03-28 18:52:46 +05301750 """,
1751 args,
1752 as_dict=1,
1753 )
Ankush Menateb8b4242022-02-12 13:08:28 +05301754
1755
1756def is_negative_stock_allowed(*, item_code: Optional[str] = None) -> bool:
1757 if cint(frappe.db.get_single_value("Stock Settings", "allow_negative_stock", cache=True)):
1758 return True
1759 if item_code and cint(frappe.db.get_value("Item", item_code, "allow_negative_stock", cache=True)):
1760 return True
1761 return False
Rohit Waghchaure683a47f2022-10-11 15:11:39 +05301762
1763
1764def get_incoming_rate_for_inter_company_transfer(sle) -> float:
1765 """
1766 For inter company transfer, incoming rate is the average of the outgoing rate
1767 """
1768 rate = 0.0
1769
1770 field = "delivery_note_item" if sle.voucher_type == "Purchase Receipt" else "sales_invoice_item"
1771
1772 doctype = "Delivery Note Item" if sle.voucher_type == "Purchase Receipt" else "Sales Invoice Item"
1773
1774 reference_name = frappe.get_cached_value(sle.voucher_type + " Item", sle.voucher_detail_no, field)
1775
1776 if reference_name:
1777 rate = frappe.get_cached_value(
1778 doctype,
1779 reference_name,
1780 "incoming_rate",
1781 )
1782
1783 return rate
Rohit Waghchaureb5a2ccf2023-05-04 15:38:35 +05301784
1785
1786def is_internal_transfer(sle):
1787 data = frappe.get_cached_value(
1788 sle.voucher_type,
1789 sle.voucher_no,
1790 ["is_internal_supplier", "represents_company", "company"],
1791 as_dict=True,
1792 )
1793
1794 if data.is_internal_supplier and data.represents_company == data.company:
1795 return True