blob: 353bfa452b62ac09b6de7ac7d034f3fac69b3587 [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 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 Menateb8b4242022-02-12 13:08:28 +05306from typing import Optional
Chillar Anand915b3432021-09-02 16:44:59 +05307
8import frappe
9from frappe import _
10from frappe.model.meta import get_field_precision
Ankush Menat102fff22022-02-19 15:51:04 +053011from frappe.query_builder.functions import Sum
Ankush Menatcef84c22021-12-03 12:18:59 +053012from frappe.utils import cint, cstr, flt, get_link_to_form, getdate, now, nowdate
Ankush Menat102fff22022-02-19 15:51:04 +053013from pypika import CustomFunction
Achilles Rasquinha361366e2018-02-14 17:08:59 +053014
Chillar Anand915b3432021-09-02 16:44:59 +053015import erpnext
Ankush Menatcef84c22021-12-03 12:18:59 +053016from erpnext.stock.doctype.bin.bin import update_qty as update_bin_qty
Chillar Anand915b3432021-09-02 16:44:59 +053017from erpnext.stock.utils import (
Chillar Anand915b3432021-09-02 16:44:59 +053018 get_incoming_outgoing_rate_for_cancel,
Deepesh Garg6f107da2021-10-12 20:15:55 +053019 get_or_make_bin,
Chillar Anand915b3432021-09-02 16:44:59 +053020 get_valuation_method,
21)
Ankush Menatb534fee2022-02-19 20:58:36 +053022from erpnext.stock.valuation import FIFOValuation, LIFOValuation, round_off_if_near_zero
Chillar Anand915b3432021-09-02 16:44:59 +053023
Nabin Hait97bce3a2021-07-12 13:24:43 +053024
Rushabh Mehta793ba6b2014-02-14 15:47:51 +053025class NegativeStockError(frappe.ValidationError): pass
Rohit Waghchaurebb3e5d02021-04-24 17:28:33 +053026class SerialNoExistsInFutureTransaction(frappe.ValidationError):
27 pass
Nabin Hait902e8602013-01-08 18:29:24 +053028
Anand Doshi5b004ff2013-09-25 19:55:41 +053029
Deepesh Garg2a9c5ba2020-04-30 10:38:58 +053030def make_sl_entries(sl_entries, allow_negative_stock=False, via_landed_cost_voucher=False):
Ankush Menateb8495a2022-03-02 12:01:51 +053031 """ Create SL entries from SL entry dicts
32
33 args:
34 - allow_negative_stock: disable negative stock valiations if true
35 - via_landed_cost_voucher: landed cost voucher cancels and reposts
36 entries of purchase document. This flag is used to identify if
37 cancellation and repost is happening via landed cost voucher, in
38 such cases certain validations need to be ignored (like negative
39 stock)
40 """
Rohit Waghchaure4d81d452021-06-15 10:21:44 +053041 from erpnext.controllers.stock_controller import future_sle_exists
Nabin Haitca775742013-09-26 16:16:44 +053042 if sl_entries:
Deepesh Garg2a9c5ba2020-04-30 10:38:58 +053043 cancel = sl_entries[0].get("is_cancelled")
Nabin Haitca775742013-09-26 16:16:44 +053044 if cancel:
Nabin Hait186a0452021-02-18 14:14:21 +053045 validate_cancellation(sl_entries)
Deepesh Garg2a9c5ba2020-04-30 10:38:58 +053046 set_as_cancel(sl_entries[0].get('voucher_type'), sl_entries[0].get('voucher_no'))
Nabin Haitdc82d4f2014-04-07 12:02:57 +053047
Rohit Waghchaure4d81d452021-06-15 10:21:44 +053048 args = get_args_for_future_sle(sl_entries[0])
49 future_sle_exists(args, sl_entries)
50
Nabin Haitca775742013-09-26 16:16:44 +053051 for sle in sl_entries:
Ankush Menatefc4b942022-03-02 11:19:12 +053052 if sle.serial_no and not via_landed_cost_voucher:
Rohit Waghchaurebb3e5d02021-04-24 17:28:33 +053053 validate_serial_no(sle)
54
Nabin Haita77b8c92020-12-21 14:45:50 +053055 if cancel:
56 sle['actual_qty'] = -flt(sle.get('actual_qty'))
Deepesh Garg2a9c5ba2020-04-30 10:38:58 +053057
Nabin Haita77b8c92020-12-21 14:45:50 +053058 if sle['actual_qty'] < 0 and not sle.get('outgoing_rate'):
59 sle['outgoing_rate'] = get_incoming_outgoing_rate_for_cancel(sle.item_code,
60 sle.voucher_type, sle.voucher_no, sle.voucher_detail_no)
61 sle['incoming_rate'] = 0.0
Deepesh Garg2a9c5ba2020-04-30 10:38:58 +053062
Nabin Haita77b8c92020-12-21 14:45:50 +053063 if sle['actual_qty'] > 0 and not sle.get('incoming_rate'):
64 sle['incoming_rate'] = get_incoming_outgoing_rate_for_cancel(sle.item_code,
65 sle.voucher_type, sle.voucher_no, sle.voucher_detail_no)
66 sle['outgoing_rate'] = 0.0
Nabin Haitdc82d4f2014-04-07 12:02:57 +053067
Nabin Hait5288bde2014-11-03 15:08:21 +053068 if sle.get("actual_qty") or sle.get("voucher_type")=="Stock Reconciliation":
Nabin Haita77b8c92020-12-21 14:45:50 +053069 sle_doc = make_entry(sle, allow_negative_stock, via_landed_cost_voucher)
Deepesh Gargb4be2922021-01-28 13:09:56 +053070
Nabin Haita77b8c92020-12-21 14:45:50 +053071 args = sle_doc.as_dict()
marination40389772021-07-02 17:13:45 +053072
73 if sle.get("voucher_type") == "Stock Reconciliation":
74 # preserve previous_qty_after_transaction for qty reposting
75 args.previous_qty_after_transaction = sle.get("previous_qty_after_transaction")
76
Ankush Menatcef84c22021-12-03 12:18:59 +053077 is_stock_item = frappe.get_cached_value('Item', args.get("item_code"), 'is_stock_item')
78 if is_stock_item:
79 bin_name = get_or_make_bin(args.get("item_code"), args.get("warehouse"))
Ankush Menatcef84c22021-12-03 12:18:59 +053080 repost_current_voucher(args, allow_negative_stock, via_landed_cost_voucher)
Ankush Menatff9a6e82021-12-20 15:07:41 +053081 update_bin_qty(bin_name, args)
Ankush Menatcef84c22021-12-03 12:18:59 +053082 else:
83 frappe.msgprint(_("Item {0} ignored since it is not a stock item").format(args.get("item_code")))
84
85def repost_current_voucher(args, allow_negative_stock=False, via_landed_cost_voucher=False):
86 if args.get("actual_qty") or args.get("voucher_type") == "Stock Reconciliation":
87 if not args.get("posting_date"):
88 args["posting_date"] = nowdate()
89
90 if args.get("is_cancelled") and via_landed_cost_voucher:
91 return
92
93 # Reposts only current voucher SL Entries
94 # Updates valuation rate, stock value, stock queue for current transaction
95 update_entries_after({
96 "item_code": args.get('item_code'),
97 "warehouse": args.get('warehouse'),
98 "posting_date": args.get("posting_date"),
99 "posting_time": args.get("posting_time"),
100 "voucher_type": args.get("voucher_type"),
101 "voucher_no": args.get("voucher_no"),
102 "sle_id": args.get('name'),
103 "creation": args.get('creation')
104 }, allow_negative_stock=allow_negative_stock, via_landed_cost_voucher=via_landed_cost_voucher)
105
106 # update qty in future sle and Validate negative qty
107 update_qty_in_future_sle(args, allow_negative_stock)
108
Nabin Haitadeb9762014-10-06 11:53:52 +0530109
Rohit Waghchaure4d81d452021-06-15 10:21:44 +0530110def get_args_for_future_sle(row):
111 return frappe._dict({
112 'voucher_type': row.get('voucher_type'),
113 'voucher_no': row.get('voucher_no'),
114 'posting_date': row.get('posting_date'),
115 'posting_time': row.get('posting_time')
116 })
117
Rohit Waghchaurebb3e5d02021-04-24 17:28:33 +0530118def validate_serial_no(sle):
119 from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos
Ankush Menat66bf21f2022-01-16 20:45:59 +0530120
Rohit Waghchaurebb3e5d02021-04-24 17:28:33 +0530121 for sn in get_serial_nos(sle.serial_no):
122 args = copy.deepcopy(sle)
123 args.serial_no = sn
124 args.warehouse = ''
125
126 vouchers = []
127 for row in get_stock_ledger_entries(args, '>'):
128 voucher_type = frappe.bold(row.voucher_type)
129 voucher_no = frappe.bold(get_link_to_form(row.voucher_type, row.voucher_no))
130 vouchers.append(f'{voucher_type} {voucher_no}')
131
132 if vouchers:
133 serial_no = frappe.bold(sn)
134 msg = (f'''The serial no {serial_no} has been used in the future transactions so you need to cancel them first.
135 The list of the transactions are as below.''' + '<br><br><ul><li>')
136
137 msg += '</li><li>'.join(vouchers)
138 msg += '</li></ul>'
139
140 title = 'Cannot Submit' if not sle.get('is_cancelled') else 'Cannot Cancel'
141 frappe.throw(_(msg), title=_(title), exc=SerialNoExistsInFutureTransaction)
142
Nabin Hait186a0452021-02-18 14:14:21 +0530143def validate_cancellation(args):
144 if args[0].get("is_cancelled"):
145 repost_entry = frappe.db.get_value("Repost Item Valuation", {
146 'voucher_type': args[0].voucher_type,
147 'voucher_no': args[0].voucher_no,
148 'docstatus': 1
149 }, ['name', 'status'], as_dict=1)
150
151 if repost_entry:
152 if repost_entry.status == 'In Progress':
153 frappe.throw(_("Cannot cancel the transaction. Reposting of item valuation on submission is not completed yet."))
154 if repost_entry.status == 'Queued':
Nabin Haitd46b2362021-02-23 16:38:52 +0530155 doc = frappe.get_doc("Repost Item Valuation", repost_entry.name)
Ankush Menataa024fc2021-11-18 12:51:26 +0530156 doc.flags.ignore_permissions = True
Nabin Haitd46b2362021-02-23 16:38:52 +0530157 doc.cancel()
158 doc.delete()
Nabin Haitdc82d4f2014-04-07 12:02:57 +0530159
Nabin Hait9653f602013-08-20 15:37:33 +0530160def set_as_cancel(voucher_type, voucher_no):
Deepesh Garg2a9c5ba2020-04-30 10:38:58 +0530161 frappe.db.sql("""update `tabStock Ledger Entry` set is_cancelled=1,
Nabin Hait9653f602013-08-20 15:37:33 +0530162 modified=%s, modified_by=%s
Deepesh Garg2a9c5ba2020-04-30 10:38:58 +0530163 where voucher_type=%s and voucher_no=%s and is_cancelled = 0""",
Rushabh Mehta793ba6b2014-02-14 15:47:51 +0530164 (now(), frappe.session.user, voucher_type, voucher_no))
Nabin Haitdc82d4f2014-04-07 12:02:57 +0530165
Nabin Hait54c865e2015-03-27 15:38:31 +0530166def make_entry(args, allow_negative_stock=False, via_landed_cost_voucher=False):
Saqib Ansaric7fc6092021-10-12 13:30:40 +0530167 args["doctype"] = "Stock Ledger Entry"
Rushabh Mehtaa504f062014-04-04 12:16:26 +0530168 sle = frappe.get_doc(args)
Anand Doshi6dfd4302015-02-10 14:41:27 +0530169 sle.flags.ignore_permissions = 1
Nabin Hait4ccd8d32015-01-23 12:18:01 +0530170 sle.allow_negative_stock=allow_negative_stock
Nabin Hait54c865e2015-03-27 15:38:31 +0530171 sle.via_landed_cost_voucher = via_landed_cost_voucher
Nabin Haitaeba24e2013-08-23 15:17:36 +0530172 sle.submit()
Nabin Haita77b8c92020-12-21 14:45:50 +0530173 return sle
Nabin Haitdc82d4f2014-04-07 12:02:57 +0530174
rohitwaghchaure31fe5f52021-08-02 11:01:30 +0530175def repost_future_sle(args=None, voucher_type=None, voucher_no=None, allow_negative_stock=None, via_landed_cost_voucher=False, doc=None):
Nabin Haita77b8c92020-12-21 14:45:50 +0530176 if not args and voucher_type and voucher_no:
rohitwaghchaure31fe5f52021-08-02 11:01:30 +0530177 args = get_items_to_be_repost(voucher_type, voucher_no, doc)
Deepesh Gargb4be2922021-01-28 13:09:56 +0530178
rohitwaghchaure31fe5f52021-08-02 11:01:30 +0530179 distinct_item_warehouses = get_distinct_item_warehouse(args, doc)
Nabin Haita77b8c92020-12-21 14:45:50 +0530180
rohitwaghchaure31fe5f52021-08-02 11:01:30 +0530181 i = get_current_index(doc) or 0
Nabin Haita77b8c92020-12-21 14:45:50 +0530182 while i < len(args):
rohitwaghchaure31fe5f52021-08-02 11:01:30 +0530183 validate_item_warehouse(args[i])
184
Nabin Haita77b8c92020-12-21 14:45:50 +0530185 obj = update_entries_after({
rohitwaghchaure31fe5f52021-08-02 11:01:30 +0530186 'item_code': args[i].get('item_code'),
187 'warehouse': args[i].get('warehouse'),
188 'posting_date': args[i].get('posting_date'),
189 'posting_time': args[i].get('posting_time'),
190 'creation': args[i].get('creation'),
191 'distinct_item_warehouses': distinct_item_warehouses
Nabin Haita77b8c92020-12-21 14:45:50 +0530192 }, allow_negative_stock=allow_negative_stock, via_landed_cost_voucher=via_landed_cost_voucher)
193
rohitwaghchaure31fe5f52021-08-02 11:01:30 +0530194 distinct_item_warehouses[(args[i].get('item_code'), args[i].get('warehouse'))].reposting_status = True
Deepesh Gargb4be2922021-01-28 13:09:56 +0530195
Nabin Hait97bce3a2021-07-12 13:24:43 +0530196 if obj.new_items_found:
Ankush Menat8fe5feb2021-11-04 19:48:32 +0530197 for item_wh, data in distinct_item_warehouses.items():
Nabin Hait97bce3a2021-07-12 13:24:43 +0530198 if ('args_idx' not in data and not data.reposting_status) or (data.sle_changed and data.reposting_status):
199 data.args_idx = len(args)
200 args.append(data.sle)
201 elif data.sle_changed and not data.reposting_status:
202 args[data.args_idx] = data.sle
rohitwaghchaure31fe5f52021-08-02 11:01:30 +0530203
Nabin Hait97bce3a2021-07-12 13:24:43 +0530204 data.sle_changed = False
Nabin Haita77b8c92020-12-21 14:45:50 +0530205 i += 1
206
rohitwaghchaure31fe5f52021-08-02 11:01:30 +0530207 if doc and i % 2 == 0:
208 update_args_in_repost_item_valuation(doc, i, args, distinct_item_warehouses)
209
210 if doc and args:
211 update_args_in_repost_item_valuation(doc, i, args, distinct_item_warehouses)
212
213def validate_item_warehouse(args):
214 for field in ['item_code', 'warehouse', 'posting_date', 'posting_time']:
215 if not args.get(field):
216 validation_msg = f'The field {frappe.unscrub(args.get(field))} is required for the reposting'
217 frappe.throw(_(validation_msg))
218
219def update_args_in_repost_item_valuation(doc, index, args, distinct_item_warehouses):
220 frappe.db.set_value(doc.doctype, doc.name, {
221 'items_to_be_repost': json.dumps(args, default=str),
222 'distinct_item_and_warehouse': json.dumps({str(k): v for k,v in distinct_item_warehouses.items()}, default=str),
223 'current_index': index
224 })
225
226 frappe.db.commit()
227
228 frappe.publish_realtime('item_reposting_progress', {
229 'name': doc.name,
230 'items_to_be_repost': json.dumps(args, default=str),
231 'current_index': index
232 })
233
234def get_items_to_be_repost(voucher_type, voucher_no, doc=None):
235 if doc and doc.items_to_be_repost:
236 return json.loads(doc.items_to_be_repost) or []
237
Nabin Haita77b8c92020-12-21 14:45:50 +0530238 return frappe.db.get_all("Stock Ledger Entry",
239 filters={"voucher_type": voucher_type, "voucher_no": voucher_no},
Nabin Hait186a0452021-02-18 14:14:21 +0530240 fields=["item_code", "warehouse", "posting_date", "posting_time", "creation"],
Nabin Haita77b8c92020-12-21 14:45:50 +0530241 order_by="creation asc",
242 group_by="item_code, warehouse"
243 )
Nabin Hait74c281c2013-08-19 16:17:18 +0530244
rohitwaghchaure31fe5f52021-08-02 11:01:30 +0530245def get_distinct_item_warehouse(args=None, doc=None):
246 distinct_item_warehouses = {}
247 if doc and doc.distinct_item_and_warehouse:
248 distinct_item_warehouses = json.loads(doc.distinct_item_and_warehouse)
249 distinct_item_warehouses = {frappe.safe_eval(k): frappe._dict(v) for k, v in distinct_item_warehouses.items()}
250 else:
251 for i, d in enumerate(args):
252 distinct_item_warehouses.setdefault((d.item_code, d.warehouse), frappe._dict({
253 "reposting_status": False,
254 "sle": d,
255 "args_idx": i
256 }))
257
258 return distinct_item_warehouses
259
260def get_current_index(doc=None):
261 if doc and doc.current_index:
262 return doc.current_index
263
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530264class update_entries_after(object):
Nabin Hait902e8602013-01-08 18:29:24 +0530265 """
Nabin Haitdc82d4f2014-04-07 12:02:57 +0530266 update valution rate and qty after transaction
Nabin Hait902e8602013-01-08 18:29:24 +0530267 from the current time-bucket onwards
Nabin Haitdc82d4f2014-04-07 12:02:57 +0530268
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530269 :param args: args as dict
270
271 args = {
272 "item_code": "ABC",
273 "warehouse": "XYZ",
274 "posting_date": "2012-12-12",
275 "posting_time": "12:00"
276 }
Nabin Hait902e8602013-01-08 18:29:24 +0530277 """
Anand Doshi0dc79f42015-04-06 12:59:34 +0530278 def __init__(self, args, allow_zero_rate=False, allow_negative_stock=None, via_landed_cost_voucher=False, verbose=1):
Nabin Haita77b8c92020-12-21 14:45:50 +0530279 self.exceptions = {}
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530280 self.verbose = verbose
281 self.allow_zero_rate = allow_zero_rate
Anand Doshi0dc79f42015-04-06 12:59:34 +0530282 self.via_landed_cost_voucher = via_landed_cost_voucher
Ankush Menateb8b4242022-02-12 13:08:28 +0530283 self.item_code = args.get("item_code")
284 self.allow_negative_stock = allow_negative_stock or is_negative_stock_allowed(item_code=self.item_code)
Nabin Haitdc82d4f2014-04-07 12:02:57 +0530285
Nabin Haita77b8c92020-12-21 14:45:50 +0530286 self.args = frappe._dict(args)
Nabin Haita77b8c92020-12-21 14:45:50 +0530287 if self.args.sle_id:
288 self.args['name'] = self.args.sle_id
Nabin Haitd46b2362021-02-23 16:38:52 +0530289
Nabin Haita77b8c92020-12-21 14:45:50 +0530290 self.company = frappe.get_cached_value("Warehouse", self.args.warehouse, "company")
291 self.get_precision()
292 self.valuation_method = get_valuation_method(self.item_code)
Nabin Hait97bce3a2021-07-12 13:24:43 +0530293
294 self.new_items_found = False
295 self.distinct_item_warehouses = args.get("distinct_item_warehouses", frappe._dict())
Nabin Haita77b8c92020-12-21 14:45:50 +0530296
297 self.data = frappe._dict()
298 self.initialize_previous_data(self.args)
Nabin Haita77b8c92020-12-21 14:45:50 +0530299 self.build()
Deepesh Gargb4be2922021-01-28 13:09:56 +0530300
Nabin Haita77b8c92020-12-21 14:45:50 +0530301 def get_precision(self):
302 company_base_currency = frappe.get_cached_value('Company', self.company, "default_currency")
303 self.precision = get_field_precision(frappe.get_meta("Stock Ledger Entry").get_field("stock_value"),
304 currency=company_base_currency)
305
306 def initialize_previous_data(self, args):
307 """
308 Get previous sl entries for current item for each related warehouse
309 and assigns into self.data dict
310
311 :Data Structure:
312
313 self.data = {
314 warehouse1: {
315 'previus_sle': {},
316 'qty_after_transaction': 10,
317 'valuation_rate': 100,
318 'stock_value': 1000,
319 'prev_stock_value': 1000,
320 'stock_queue': '[[10, 100]]',
321 'stock_value_difference': 1000
322 }
323 }
324
325 """
Ankush Menatc1d986a2021-08-31 19:43:42 +0530326 self.data.setdefault(args.warehouse, frappe._dict())
327 warehouse_dict = self.data[args.warehouse]
marination8418c4b2021-06-22 21:35:25 +0530328 previous_sle = get_previous_sle_of_current_voucher(args)
Ankush Menatc1d986a2021-08-31 19:43:42 +0530329 warehouse_dict.previous_sle = previous_sle
Nabin Haitbb777562013-08-29 18:19:37 +0530330
Ankush Menatc1d986a2021-08-31 19:43:42 +0530331 for key in ("qty_after_transaction", "valuation_rate", "stock_value"):
332 setattr(warehouse_dict, key, flt(previous_sle.get(key)))
333
334 warehouse_dict.update({
Nabin Haita77b8c92020-12-21 14:45:50 +0530335 "prev_stock_value": previous_sle.stock_value or 0.0,
336 "stock_queue": json.loads(previous_sle.stock_queue or "[]"),
337 "stock_value_difference": 0.0
338 })
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530339
Nabin Haita77b8c92020-12-21 14:45:50 +0530340 def build(self):
Sagar Vorae50324a2021-03-31 12:44:03 +0530341 from erpnext.controllers.stock_controller import future_sle_exists
Nabin Hait186a0452021-02-18 14:14:21 +0530342
Nabin Haita77b8c92020-12-21 14:45:50 +0530343 if self.args.get("sle_id"):
Nabin Hait186a0452021-02-18 14:14:21 +0530344 self.process_sle_against_current_timestamp()
Sagar Vorae50324a2021-03-31 12:44:03 +0530345 if not future_sle_exists(self.args):
Nabin Hait186a0452021-02-18 14:14:21 +0530346 self.update_bin()
Deepesh Garg2a9c5ba2020-04-30 10:38:58 +0530347 else:
Nabin Haita77b8c92020-12-21 14:45:50 +0530348 entries_to_fix = self.get_future_entries_to_fix()
349
350 i = 0
351 while i < len(entries_to_fix):
352 sle = entries_to_fix[i]
353 i += 1
354
Deepesh Garg2a9c5ba2020-04-30 10:38:58 +0530355 self.process_sle(sle)
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530356
Nabin Haita77b8c92020-12-21 14:45:50 +0530357 if sle.dependant_sle_voucher_detail_no:
Nabin Hait243d59b2021-02-02 16:55:13 +0530358 entries_to_fix = self.get_dependent_entries_to_fix(entries_to_fix, sle)
Nabin Haitd46b2362021-02-23 16:38:52 +0530359
Nabin Hait186a0452021-02-18 14:14:21 +0530360 self.update_bin()
Nabin Haita77b8c92020-12-21 14:45:50 +0530361
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530362 if self.exceptions:
363 self.raise_exceptions()
364
Nabin Hait186a0452021-02-18 14:14:21 +0530365 def process_sle_against_current_timestamp(self):
Nabin Haita77b8c92020-12-21 14:45:50 +0530366 sl_entries = self.get_sle_against_current_voucher()
367 for sle in sl_entries:
368 self.process_sle(sle)
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530369
Nabin Haita77b8c92020-12-21 14:45:50 +0530370 def get_sle_against_current_voucher(self):
Nabin Haitf2be0802021-02-15 19:27:49 +0530371 self.args['time_format'] = '%H:%i:%s'
372
Nabin Haita77b8c92020-12-21 14:45:50 +0530373 return frappe.db.sql("""
374 select
375 *, timestamp(posting_date, posting_time) as "timestamp"
376 from
377 `tabStock Ledger Entry`
378 where
379 item_code = %(item_code)s
380 and warehouse = %(warehouse)s
rohitwaghchaurefe4540d2021-08-26 12:52:36 +0530381 and is_cancelled = 0
Nabin Hait186a0452021-02-18 14:14:21 +0530382 and timestamp(posting_date, time_format(posting_time, %(time_format)s)) = timestamp(%(posting_date)s, time_format(%(posting_time)s, %(time_format)s))
383
Nabin Haita77b8c92020-12-21 14:45:50 +0530384 order by
385 creation ASC
386 for update
387 """, self.args, as_dict=1)
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530388
Nabin Haita77b8c92020-12-21 14:45:50 +0530389 def get_future_entries_to_fix(self):
390 # includes current entry!
391 args = self.data[self.args.warehouse].previous_sle \
392 or frappe._dict({"item_code": self.item_code, "warehouse": self.args.warehouse})
Deepesh Gargb4be2922021-01-28 13:09:56 +0530393
Nabin Haita77b8c92020-12-21 14:45:50 +0530394 return list(self.get_sle_after_datetime(args))
Rushabh Mehta538607e2016-06-12 11:03:00 +0530395
Nabin Haita77b8c92020-12-21 14:45:50 +0530396 def get_dependent_entries_to_fix(self, entries_to_fix, sle):
397 dependant_sle = get_sle_by_voucher_detail_no(sle.dependant_sle_voucher_detail_no,
398 excluded_sle=sle.name)
Deepesh Gargb4be2922021-01-28 13:09:56 +0530399
Nabin Haita77b8c92020-12-21 14:45:50 +0530400 if not dependant_sle:
Nabin Hait243d59b2021-02-02 16:55:13 +0530401 return entries_to_fix
Nabin Haita77b8c92020-12-21 14:45:50 +0530402 elif dependant_sle.item_code == self.item_code and dependant_sle.warehouse == self.args.warehouse:
Nabin Hait243d59b2021-02-02 16:55:13 +0530403 return entries_to_fix
404 elif dependant_sle.item_code != self.item_code:
Nabin Hait97bce3a2021-07-12 13:24:43 +0530405 self.update_distinct_item_warehouses(dependant_sle)
Nabin Hait243d59b2021-02-02 16:55:13 +0530406 return entries_to_fix
407 elif dependant_sle.item_code == self.item_code and dependant_sle.warehouse in self.data:
408 return entries_to_fix
Nabin Hait97bce3a2021-07-12 13:24:43 +0530409 else:
410 return self.append_future_sle_for_dependant(dependant_sle, entries_to_fix)
411
412 def update_distinct_item_warehouses(self, dependant_sle):
413 key = (dependant_sle.item_code, dependant_sle.warehouse)
414 val = frappe._dict({
415 "sle": dependant_sle
416 })
417 if key not in self.distinct_item_warehouses:
418 self.distinct_item_warehouses[key] = val
419 self.new_items_found = True
420 else:
421 existing_sle_posting_date = self.distinct_item_warehouses[key].get("sle", {}).get("posting_date")
422 if getdate(dependant_sle.posting_date) < getdate(existing_sle_posting_date):
423 val.sle_changed = True
424 self.distinct_item_warehouses[key] = val
425 self.new_items_found = True
426
427 def append_future_sle_for_dependant(self, dependant_sle, entries_to_fix):
Nabin Haita77b8c92020-12-21 14:45:50 +0530428 self.initialize_previous_data(dependant_sle)
429
430 args = self.data[dependant_sle.warehouse].previous_sle \
431 or frappe._dict({"item_code": self.item_code, "warehouse": dependant_sle.warehouse})
432 future_sle_for_dependant = list(self.get_sle_after_datetime(args))
433
434 entries_to_fix.extend(future_sle_for_dependant)
Nabin Hait243d59b2021-02-02 16:55:13 +0530435 return sorted(entries_to_fix, key=lambda k: k['timestamp'])
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530436
437 def process_sle(self, sle):
Ankush Menat66bf21f2022-01-16 20:45:59 +0530438 from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos
439
Nabin Haita77b8c92020-12-21 14:45:50 +0530440 # previous sle data for this warehouse
441 self.wh_data = self.data[sle.warehouse]
442
Anand Doshi0dc79f42015-04-06 12:59:34 +0530443 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 +0530444 # validate negative stock for serialized items, fifo valuation
Nabin Hait902e8602013-01-08 18:29:24 +0530445 # or when negative stock is not allowed for moving average
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530446 if not self.validate_negative_stock(sle):
Nabin Haita77b8c92020-12-21 14:45:50 +0530447 self.wh_data.qty_after_transaction += flt(sle.actual_qty)
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530448 return
Nabin Haitb96c0142014-10-07 11:25:04 +0530449
Nabin Haita77b8c92020-12-21 14:45:50 +0530450 # Get dynamic incoming/outgoing rate
Ankush Menat701878f2022-03-01 18:08:29 +0530451 if not self.args.get("sle_id"):
452 self.get_dynamic_incoming_outgoing_rate(sle)
Deepesh Gargb4be2922021-01-28 13:09:56 +0530453
Ankush Menat66bf21f2022-01-16 20:45:59 +0530454 if get_serial_nos(sle.serial_no):
Rushabh Mehta2a21bc92015-02-25 15:08:42 +0530455 self.get_serialized_values(sle)
Nabin Haita77b8c92020-12-21 14:45:50 +0530456 self.wh_data.qty_after_transaction += flt(sle.actual_qty)
Rohit Waghchaure05d3bcb2019-04-28 18:39:18 +0530457 if sle.voucher_type == "Stock Reconciliation":
Nabin Haita77b8c92020-12-21 14:45:50 +0530458 self.wh_data.qty_after_transaction = sle.qty_after_transaction
Rohit Waghchaure05d3bcb2019-04-28 18:39:18 +0530459
Nabin Haita77b8c92020-12-21 14:45:50 +0530460 self.wh_data.stock_value = flt(self.wh_data.qty_after_transaction) * flt(self.wh_data.valuation_rate)
Ankush Menatce0514c2022-02-15 11:41:41 +0530461 elif sle.batch_no and frappe.db.get_value("Batch", sle.batch_no, "use_batchwise_valuation", cache=True):
462 self.update_batched_values(sle)
Nabin Haitb96c0142014-10-07 11:25:04 +0530463 else:
Rohit Waghchaure66aa37f2019-05-24 16:53:51 +0530464 if sle.voucher_type=="Stock Reconciliation" and not sle.batch_no:
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530465 # assert
Nabin Haita77b8c92020-12-21 14:45:50 +0530466 self.wh_data.valuation_rate = sle.valuation_rate
467 self.wh_data.qty_after_transaction = sle.qty_after_transaction
Nabin Haita77b8c92020-12-21 14:45:50 +0530468 self.wh_data.stock_value = flt(self.wh_data.qty_after_transaction) * flt(self.wh_data.valuation_rate)
Ankush Menatb0cf6192022-01-16 13:02:23 +0530469 if self.valuation_method != "Moving Average":
470 self.wh_data.stock_queue = [[self.wh_data.qty_after_transaction, self.wh_data.valuation_rate]]
Nabin Haitb96c0142014-10-07 11:25:04 +0530471 else:
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530472 if self.valuation_method == "Moving Average":
473 self.get_moving_average_values(sle)
Nabin Haita77b8c92020-12-21 14:45:50 +0530474 self.wh_data.qty_after_transaction += flt(sle.actual_qty)
475 self.wh_data.stock_value = flt(self.wh_data.qty_after_transaction) * flt(self.wh_data.valuation_rate)
Nabin Haitb96c0142014-10-07 11:25:04 +0530476 else:
Ankush Menatf089d392022-02-02 12:51:21 +0530477 self.update_queue_values(sle)
Nabin Haitb96c0142014-10-07 11:25:04 +0530478
Rushabh Mehta54047782013-12-26 11:07:46 +0530479 # rounding as per precision
Nabin Haita77b8c92020-12-21 14:45:50 +0530480 self.wh_data.stock_value = flt(self.wh_data.stock_value, self.precision)
Ankush Menat609d2fc2022-02-20 11:35:53 +0530481 if not self.wh_data.qty_after_transaction:
482 self.wh_data.stock_value = 0.0
Nabin Haita77b8c92020-12-21 14:45:50 +0530483 stock_value_difference = self.wh_data.stock_value - self.wh_data.prev_stock_value
484 self.wh_data.prev_stock_value = self.wh_data.stock_value
Nabin Haitdc82d4f2014-04-07 12:02:57 +0530485
Nabin Hait902e8602013-01-08 18:29:24 +0530486 # update current sle
Nabin Haita77b8c92020-12-21 14:45:50 +0530487 sle.qty_after_transaction = self.wh_data.qty_after_transaction
488 sle.valuation_rate = self.wh_data.valuation_rate
489 sle.stock_value = self.wh_data.stock_value
490 sle.stock_queue = json.dumps(self.wh_data.stock_queue)
Rushabh Mehta2e0e7112015-02-18 11:38:05 +0530491 sle.stock_value_difference = stock_value_difference
Rushabh Mehta8bb6e532015-02-18 20:22:59 +0530492 sle.doctype="Stock Ledger Entry"
493 frappe.get_doc(sle).db_update()
Nabin Haitdc82d4f2014-04-07 12:02:57 +0530494
Ankush Menat701878f2022-03-01 18:08:29 +0530495 if not self.args.get("sle_id"):
496 self.update_outgoing_rate_on_transaction(sle)
Nabin Haita77b8c92020-12-21 14:45:50 +0530497
Ankush Menatce0514c2022-02-15 11:41:41 +0530498
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530499 def validate_negative_stock(self, sle):
500 """
501 validate negative stock for entries current datetime onwards
502 will not consider cancelled entries
503 """
Nabin Haita77b8c92020-12-21 14:45:50 +0530504 diff = self.wh_data.qty_after_transaction + flt(sle.actual_qty)
Nabin Haitdc82d4f2014-04-07 12:02:57 +0530505
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530506 if diff < 0 and abs(diff) > 0.0001:
507 # negative stock!
508 exc = sle.copy().update({"diff": diff})
Nabin Haita77b8c92020-12-21 14:45:50 +0530509 self.exceptions.setdefault(sle.warehouse, []).append(exc)
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530510 return False
Nabin Hait902e8602013-01-08 18:29:24 +0530511 else:
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530512 return True
513
Nabin Haita77b8c92020-12-21 14:45:50 +0530514 def get_dynamic_incoming_outgoing_rate(self, sle):
515 # Get updated incoming/outgoing rate from transaction
516 if sle.recalculate_rate:
517 rate = self.get_incoming_outgoing_rate_from_transaction(sle)
518
519 if flt(sle.actual_qty) >= 0:
520 sle.incoming_rate = rate
521 else:
522 sle.outgoing_rate = rate
523
524 def get_incoming_outgoing_rate_from_transaction(self, sle):
525 rate = 0
526 # Material Transfer, Repack, Manufacturing
527 if sle.voucher_type == "Stock Entry":
Nabin Hait97bce3a2021-07-12 13:24:43 +0530528 self.recalculate_amounts_in_stock_entry(sle.voucher_no)
Nabin Haita77b8c92020-12-21 14:45:50 +0530529 rate = frappe.db.get_value("Stock Entry Detail", sle.voucher_detail_no, "valuation_rate")
530 # Sales and Purchase Return
531 elif sle.voucher_type in ("Purchase Receipt", "Purchase Invoice", "Delivery Note", "Sales Invoice"):
532 if frappe.get_cached_value(sle.voucher_type, sle.voucher_no, "is_return"):
Chillar Anand915b3432021-09-02 16:44:59 +0530533 from erpnext.controllers.sales_and_purchase_return import (
534 get_rate_for_return, # don't move this import to top
535 )
rohitwaghchaurece6c3b52021-04-13 20:55:52 +0530536 rate = get_rate_for_return(sle.voucher_type, sle.voucher_no, sle.item_code,
537 voucher_detail_no=sle.voucher_detail_no, sle = sle)
Nabin Haita77b8c92020-12-21 14:45:50 +0530538 else:
539 if sle.voucher_type in ("Purchase Receipt", "Purchase Invoice"):
Deepesh Gargb4be2922021-01-28 13:09:56 +0530540 rate_field = "valuation_rate"
Nabin Haita77b8c92020-12-21 14:45:50 +0530541 else:
542 rate_field = "incoming_rate"
543
544 # check in item table
545 item_code, incoming_rate = frappe.db.get_value(sle.voucher_type + " Item",
546 sle.voucher_detail_no, ["item_code", rate_field])
547
548 if item_code == sle.item_code:
549 rate = incoming_rate
550 else:
551 if sle.voucher_type in ("Delivery Note", "Sales Invoice"):
552 ref_doctype = "Packed Item"
553 else:
554 ref_doctype = "Purchase Receipt Item Supplied"
Deepesh Gargb4be2922021-01-28 13:09:56 +0530555
Nabin Haita77b8c92020-12-21 14:45:50 +0530556 rate = frappe.db.get_value(ref_doctype, {"parent_detail_docname": sle.voucher_detail_no,
557 "item_code": sle.item_code}, rate_field)
558
559 return rate
560
561 def update_outgoing_rate_on_transaction(self, sle):
562 """
563 Update outgoing rate in Stock Entry, Delivery Note, Sales Invoice and Sales Return
564 In case of Stock Entry, also calculate FG Item rate and total incoming/outgoing amount
565 """
566 if sle.actual_qty and sle.voucher_detail_no:
567 outgoing_rate = abs(flt(sle.stock_value_difference)) / abs(sle.actual_qty)
568
569 if flt(sle.actual_qty) < 0 and sle.voucher_type == "Stock Entry":
570 self.update_rate_on_stock_entry(sle, outgoing_rate)
571 elif sle.voucher_type in ("Delivery Note", "Sales Invoice"):
572 self.update_rate_on_delivery_and_sales_return(sle, outgoing_rate)
573 elif flt(sle.actual_qty) < 0 and sle.voucher_type in ("Purchase Receipt", "Purchase Invoice"):
574 self.update_rate_on_purchase_receipt(sle, outgoing_rate)
575
576 def update_rate_on_stock_entry(self, sle, outgoing_rate):
577 frappe.db.set_value("Stock Entry Detail", sle.voucher_detail_no, "basic_rate", outgoing_rate)
578
Ankush Menat701878f2022-03-01 18:08:29 +0530579 # Update outgoing item's rate, recalculate FG Item's rate and total incoming/outgoing amount
580 if not sle.dependant_sle_voucher_detail_no:
581 self.recalculate_amounts_in_stock_entry(sle.voucher_no)
Nabin Hait97bce3a2021-07-12 13:24:43 +0530582
583 def recalculate_amounts_in_stock_entry(self, voucher_no):
584 stock_entry = frappe.get_doc("Stock Entry", voucher_no, for_update=True)
Nabin Haita77b8c92020-12-21 14:45:50 +0530585 stock_entry.calculate_rate_and_amount(reset_outgoing_rate=False, raise_error_if_no_rate=False)
586 stock_entry.db_update()
587 for d in stock_entry.items:
588 d.db_update()
Deepesh Gargb4be2922021-01-28 13:09:56 +0530589
Nabin Haita77b8c92020-12-21 14:45:50 +0530590 def update_rate_on_delivery_and_sales_return(self, sle, outgoing_rate):
591 # Update item's incoming rate on transaction
592 item_code = frappe.db.get_value(sle.voucher_type + " Item", sle.voucher_detail_no, "item_code")
593 if item_code == sle.item_code:
594 frappe.db.set_value(sle.voucher_type + " Item", sle.voucher_detail_no, "incoming_rate", outgoing_rate)
595 else:
596 # packed item
597 frappe.db.set_value("Packed Item",
598 {"parent_detail_docname": sle.voucher_detail_no, "item_code": sle.item_code},
599 "incoming_rate", outgoing_rate)
600
601 def update_rate_on_purchase_receipt(self, sle, outgoing_rate):
602 if frappe.db.exists(sle.voucher_type + " Item", sle.voucher_detail_no):
603 frappe.db.set_value(sle.voucher_type + " Item", sle.voucher_detail_no, "base_net_rate", outgoing_rate)
604 else:
605 frappe.db.set_value("Purchase Receipt Item Supplied", sle.voucher_detail_no, "rate", outgoing_rate)
606
607 # Recalculate subcontracted item's rate in case of subcontracted purchase receipt/invoice
Rohit Waghchaure4d81d452021-06-15 10:21:44 +0530608 if frappe.get_cached_value(sle.voucher_type, sle.voucher_no, "is_subcontracted") == 'Yes':
Rohit Waghchauree5fb2392021-06-18 20:37:42 +0530609 doc = frappe.get_doc(sle.voucher_type, sle.voucher_no)
Nabin Haita77b8c92020-12-21 14:45:50 +0530610 doc.update_valuation_rate(reset_outgoing_rate=False)
611 for d in (doc.items + doc.supplied_items):
612 d.db_update()
613
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530614 def get_serialized_values(self, sle):
615 incoming_rate = flt(sle.incoming_rate)
616 actual_qty = flt(sle.actual_qty)
Nabin Hait328c4f92020-01-02 19:00:32 +0530617 serial_nos = cstr(sle.serial_no).split("\n")
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530618
619 if incoming_rate < 0:
620 # wrong incoming rate
Nabin Haita77b8c92020-12-21 14:45:50 +0530621 incoming_rate = self.wh_data.valuation_rate
Rushabh Mehta538607e2016-06-12 11:03:00 +0530622
Nabin Hait2620bf42016-02-29 11:30:27 +0530623 stock_value_change = 0
Ankush Menatb9642a12021-12-21 16:49:41 +0530624 if actual_qty > 0:
Nabin Hait2620bf42016-02-29 11:30:27 +0530625 stock_value_change = actual_qty * incoming_rate
Ankush Menatb9642a12021-12-21 16:49:41 +0530626 else:
Nabin Hait2620bf42016-02-29 11:30:27 +0530627 # In case of delivery/stock issue, get average purchase rate
628 # of serial nos of current entry
Nabin Haita77b8c92020-12-21 14:45:50 +0530629 if not sle.is_cancelled:
630 outgoing_value = self.get_incoming_value_for_serial_nos(sle, serial_nos)
631 stock_value_change = -1 * outgoing_value
632 else:
633 stock_value_change = actual_qty * sle.outgoing_rate
Rushabh Mehta2a21bc92015-02-25 15:08:42 +0530634
Nabin Haita77b8c92020-12-21 14:45:50 +0530635 new_stock_qty = self.wh_data.qty_after_transaction + actual_qty
rohitwaghchaure0fe6ced2018-07-27 10:33:30 +0530636
Nabin Hait2620bf42016-02-29 11:30:27 +0530637 if new_stock_qty > 0:
Nabin Haita77b8c92020-12-21 14:45:50 +0530638 new_stock_value = (self.wh_data.qty_after_transaction * self.wh_data.valuation_rate) + stock_value_change
rohitwaghchaure0fe6ced2018-07-27 10:33:30 +0530639 if new_stock_value >= 0:
Nabin Hait2620bf42016-02-29 11:30:27 +0530640 # calculate new valuation rate only if stock value is positive
641 # else it remains the same as that of previous entry
Nabin Haita77b8c92020-12-21 14:45:50 +0530642 self.wh_data.valuation_rate = new_stock_value / new_stock_qty
Rushabh Mehtacca33b22016-07-08 18:24:46 +0530643
Nabin Haita77b8c92020-12-21 14:45:50 +0530644 if not self.wh_data.valuation_rate and sle.voucher_detail_no:
rohitwaghchaureb1ac9792017-12-01 16:09:02 +0530645 allow_zero_rate = self.check_if_allow_zero_valuation_rate(sle.voucher_type, sle.voucher_detail_no)
646 if not allow_zero_rate:
Ankush Menatd7ca83e2022-02-19 19:35:33 +0530647 self.wh_data.valuation_rate = self.get_fallback_rate(sle)
rohitwaghchaureb1ac9792017-12-01 16:09:02 +0530648
Nabin Hait328c4f92020-01-02 19:00:32 +0530649 def get_incoming_value_for_serial_nos(self, sle, serial_nos):
650 # get rate from serial nos within same company
651 all_serial_nos = frappe.get_all("Serial No",
652 fields=["purchase_rate", "name", "company"],
653 filters = {'name': ('in', serial_nos)})
654
Ankush Menat98917802021-06-11 18:40:22 +0530655 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 +0530656
657 # Get rate for serial nos which has been transferred to other company
658 invalid_serial_nos = [d.name for d in all_serial_nos if d.company!=sle.company]
659 for serial_no in invalid_serial_nos:
660 incoming_rate = frappe.db.sql("""
661 select incoming_rate
662 from `tabStock Ledger Entry`
663 where
664 company = %s
665 and actual_qty > 0
Ankush Menat82ea9582022-01-16 20:19:04 +0530666 and is_cancelled = 0
Nabin Hait328c4f92020-01-02 19:00:32 +0530667 and (serial_no = %s
668 or serial_no like %s
669 or serial_no like %s
670 or serial_no like %s
671 )
672 order by posting_date desc
673 limit 1
674 """, (sle.company, serial_no, serial_no+'\n%', '%\n'+serial_no, '%\n'+serial_no+'\n%'))
675
676 incoming_values += flt(incoming_rate[0][0]) if incoming_rate else 0
677
678 return incoming_values
679
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530680 def get_moving_average_values(self, sle):
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530681 actual_qty = flt(sle.actual_qty)
Nabin Haita77b8c92020-12-21 14:45:50 +0530682 new_stock_qty = flt(self.wh_data.qty_after_transaction) + actual_qty
Nabin Hait6dfc78b2016-06-24 12:28:55 +0530683 if new_stock_qty >= 0:
684 if actual_qty > 0:
Nabin Haita77b8c92020-12-21 14:45:50 +0530685 if flt(self.wh_data.qty_after_transaction) <= 0:
686 self.wh_data.valuation_rate = sle.incoming_rate
Nabin Hait6dfc78b2016-06-24 12:28:55 +0530687 else:
Nabin Haita77b8c92020-12-21 14:45:50 +0530688 new_stock_value = (self.wh_data.qty_after_transaction * self.wh_data.valuation_rate) + \
Nabin Hait6dfc78b2016-06-24 12:28:55 +0530689 (actual_qty * sle.incoming_rate)
Rushabh Mehta14a908b2015-10-15 12:28:20 +0530690
Nabin Haita77b8c92020-12-21 14:45:50 +0530691 self.wh_data.valuation_rate = new_stock_value / new_stock_qty
Rushabh Mehta14a908b2015-10-15 12:28:20 +0530692
Nabin Hait6dfc78b2016-06-24 12:28:55 +0530693 elif sle.outgoing_rate:
694 if new_stock_qty:
Nabin Haita77b8c92020-12-21 14:45:50 +0530695 new_stock_value = (self.wh_data.qty_after_transaction * self.wh_data.valuation_rate) + \
Nabin Hait6dfc78b2016-06-24 12:28:55 +0530696 (actual_qty * sle.outgoing_rate)
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530697
Nabin Haita77b8c92020-12-21 14:45:50 +0530698 self.wh_data.valuation_rate = new_stock_value / new_stock_qty
Nabin Hait6dfc78b2016-06-24 12:28:55 +0530699 else:
Nabin Haita77b8c92020-12-21 14:45:50 +0530700 self.wh_data.valuation_rate = sle.outgoing_rate
Nabin Hait6dfc78b2016-06-24 12:28:55 +0530701 else:
Nabin Haita77b8c92020-12-21 14:45:50 +0530702 if flt(self.wh_data.qty_after_transaction) >= 0 and sle.outgoing_rate:
703 self.wh_data.valuation_rate = sle.outgoing_rate
Rushabh Mehta14a908b2015-10-15 12:28:20 +0530704
Nabin Haita77b8c92020-12-21 14:45:50 +0530705 if not self.wh_data.valuation_rate and actual_qty > 0:
706 self.wh_data.valuation_rate = sle.incoming_rate
Rushabh Mehtacc8b2b22017-03-31 12:44:29 +0530707
Rushabh Mehtaaedaac62017-05-04 09:35:19 +0530708 # Get valuation rate from previous SLE or Item master, if item does not have the
Javier Wong9b11d9b2017-04-14 18:24:04 +0800709 # allow zero valuration rate flag set
Nabin Haita77b8c92020-12-21 14:45:50 +0530710 if not self.wh_data.valuation_rate and sle.voucher_detail_no:
Javier Wong9b11d9b2017-04-14 18:24:04 +0800711 allow_zero_valuation_rate = self.check_if_allow_zero_valuation_rate(sle.voucher_type, sle.voucher_detail_no)
712 if not allow_zero_valuation_rate:
Ankush Menatd7ca83e2022-02-19 19:35:33 +0530713 self.wh_data.valuation_rate = self.get_fallback_rate(sle)
Rushabh Mehtacc8b2b22017-03-31 12:44:29 +0530714
Ankush Menatf089d392022-02-02 12:51:21 +0530715 def update_queue_values(self, sle):
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530716 incoming_rate = flt(sle.incoming_rate)
717 actual_qty = flt(sle.actual_qty)
Nabin Haitada485f2015-07-17 15:09:56 +0530718 outgoing_rate = flt(sle.outgoing_rate)
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530719
Ankush Menatb534fee2022-02-19 20:58:36 +0530720 self.wh_data.qty_after_transaction = round_off_if_near_zero(self.wh_data.qty_after_transaction + actual_qty)
721
Ankush Menat97e18a12022-01-15 17:42:25 +0530722 if self.valuation_method == "LIFO":
723 stock_queue = LIFOValuation(self.wh_data.stock_queue)
724 else:
725 stock_queue = FIFOValuation(self.wh_data.stock_queue)
726
Ankush Menatb534fee2022-02-19 20:58:36 +0530727 _prev_qty, prev_stock_value = stock_queue.get_total_stock_and_value()
728
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530729 if actual_qty > 0:
Ankush Menat97e18a12022-01-15 17:42:25 +0530730 stock_queue.add_stock(qty=actual_qty, rate=incoming_rate)
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530731 else:
Ankush Menat4b29fb62021-12-18 18:40:22 +0530732 def rate_generator() -> float:
733 allow_zero_valuation_rate = self.check_if_allow_zero_valuation_rate(sle.voucher_type, sle.voucher_detail_no)
734 if not allow_zero_valuation_rate:
Ankush Menatd7ca83e2022-02-19 19:35:33 +0530735 return self.get_fallback_rate(sle)
Nabin Haitada485f2015-07-17 15:09:56 +0530736 else:
Ankush Menat4b29fb62021-12-18 18:40:22 +0530737 return 0.0
Rushabh Mehtacca33b22016-07-08 18:24:46 +0530738
Ankush Menat97e18a12022-01-15 17:42:25 +0530739 stock_queue.remove_stock(qty=abs(actual_qty), outgoing_rate=outgoing_rate, rate_generator=rate_generator)
Nabin Haitdc82d4f2014-04-07 12:02:57 +0530740
Ankush Menatb534fee2022-02-19 20:58:36 +0530741 _qty, stock_value = stock_queue.get_total_stock_and_value()
742
743 stock_value_difference = stock_value - prev_stock_value
Nabin Haitdc82d4f2014-04-07 12:02:57 +0530744
Ankush Menat97e18a12022-01-15 17:42:25 +0530745 self.wh_data.stock_queue = stock_queue.state
Ankush Menatb534fee2022-02-19 20:58:36 +0530746 self.wh_data.stock_value = round_off_if_near_zero(self.wh_data.stock_value + stock_value_difference)
Rushabh Mehtacca33b22016-07-08 18:24:46 +0530747
Nabin Haita77b8c92020-12-21 14:45:50 +0530748 if not self.wh_data.stock_queue:
749 self.wh_data.stock_queue.append([0, sle.incoming_rate or sle.outgoing_rate or self.wh_data.valuation_rate])
Rushabh Mehtacc8b2b22017-03-31 12:44:29 +0530750
Ankush Menatb534fee2022-02-19 20:58:36 +0530751 if self.wh_data.qty_after_transaction:
752 self.wh_data.valuation_rate = self.wh_data.stock_value / self.wh_data.qty_after_transaction
753
Ankush Menatce0514c2022-02-15 11:41:41 +0530754 def update_batched_values(self, sle):
755 incoming_rate = flt(sle.incoming_rate)
756 actual_qty = flt(sle.actual_qty)
Ankush Menat4b29fb62021-12-18 18:40:22 +0530757
Ankush Menat35483242022-02-19 22:22:27 +0530758 self.wh_data.qty_after_transaction = round_off_if_near_zero(self.wh_data.qty_after_transaction + actual_qty)
Ankush Menatce0514c2022-02-15 11:41:41 +0530759
760 if actual_qty > 0:
761 stock_value_difference = incoming_rate * actual_qty
Ankush Menatce0514c2022-02-15 11:41:41 +0530762 else:
Ankush Menat35483242022-02-19 22:22:27 +0530763 outgoing_rate = get_batch_incoming_rate(item_code=sle.item_code,
764 warehouse=sle.warehouse, batch_no=sle.batch_no, posting_date=sle.posting_date,
765 posting_time=sle.posting_time, creation=sle.creation)
Ankush Menataba7a7c2022-02-19 19:36:28 +0530766 if outgoing_rate is None:
767 # This can *only* happen if qty available for the batch is zero.
768 # in such case fall back various other rates.
769 # future entries will correct the overall accounting as each
770 # batch individually uses moving average rates.
771 outgoing_rate = self.get_fallback_rate(sle)
Ankush Menatce0514c2022-02-15 11:41:41 +0530772 stock_value_difference = outgoing_rate * actual_qty
Ankush Menatce0514c2022-02-15 11:41:41 +0530773
Ankush Menat35483242022-02-19 22:22:27 +0530774 self.wh_data.stock_value = round_off_if_near_zero(self.wh_data.stock_value + stock_value_difference)
Ankush Menatce0514c2022-02-15 11:41:41 +0530775 if self.wh_data.qty_after_transaction:
776 self.wh_data.valuation_rate = self.wh_data.stock_value / self.wh_data.qty_after_transaction
Ankush Menat4b29fb62021-12-18 18:40:22 +0530777
Javier Wong9b11d9b2017-04-14 18:24:04 +0800778 def check_if_allow_zero_valuation_rate(self, voucher_type, voucher_detail_no):
deepeshgarg007f9c0ef32019-07-30 18:49:19 +0530779 ref_item_dt = ""
780
781 if voucher_type == "Stock Entry":
782 ref_item_dt = voucher_type + " Detail"
783 elif voucher_type in ["Purchase Invoice", "Sales Invoice", "Delivery Note", "Purchase Receipt"]:
784 ref_item_dt = voucher_type + " Item"
785
786 if ref_item_dt:
787 return frappe.db.get_value(ref_item_dt, voucher_detail_no, "allow_zero_valuation_rate")
788 else:
789 return 0
Rushabh Mehtacc8b2b22017-03-31 12:44:29 +0530790
Ankush Menatd7ca83e2022-02-19 19:35:33 +0530791 def get_fallback_rate(self, sle) -> float:
792 """When exact incoming rate isn't available use any of other "average" rates as fallback.
793 This should only get used for negative stock."""
794 return get_valuation_rate(sle.item_code, sle.warehouse,
795 sle.voucher_type, sle.voucher_no, self.allow_zero_rate,
796 currency=erpnext.get_company_currency(sle.company), company=sle.company, batch_no=sle.batch_no)
797
Nabin Haita77b8c92020-12-21 14:45:50 +0530798 def get_sle_before_datetime(self, args):
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530799 """get previous stock ledger entry before current time-bucket"""
Nabin Haita77b8c92020-12-21 14:45:50 +0530800 sle = get_stock_ledger_entries(args, "<", "desc", "limit 1", for_update=False)
801 sle = sle[0] if sle else frappe._dict()
802 return sle
Deepesh Garg2a9c5ba2020-04-30 10:38:58 +0530803
Nabin Haita77b8c92020-12-21 14:45:50 +0530804 def get_sle_after_datetime(self, args):
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530805 """get Stock Ledger Entries after a particular datetime, for reposting"""
Nabin Haita77b8c92020-12-21 14:45:50 +0530806 return get_stock_ledger_entries(args, ">", "asc", for_update=True, check_serial_no=False)
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530807
808 def raise_exceptions(self):
Nabin Haita77b8c92020-12-21 14:45:50 +0530809 msg_list = []
Ankush Menat8fe5feb2021-11-04 19:48:32 +0530810 for warehouse, exceptions in self.exceptions.items():
Nabin Haita77b8c92020-12-21 14:45:50 +0530811 deficiency = min(e["diff"] for e in exceptions)
Rushabh Mehta538607e2016-06-12 11:03:00 +0530812
Nabin Haita77b8c92020-12-21 14:45:50 +0530813 if ((exceptions[0]["voucher_type"], exceptions[0]["voucher_no"]) in
814 frappe.local.flags.currently_saving):
Nabin Hait3edefb12016-07-20 16:13:18 +0530815
Nabin Haita77b8c92020-12-21 14:45:50 +0530816 msg = _("{0} units of {1} needed in {2} to complete this transaction.").format(
Nabin Hait243d59b2021-02-02 16:55:13 +0530817 abs(deficiency), frappe.get_desk_link('Item', exceptions[0]["item_code"]),
Nabin Haita77b8c92020-12-21 14:45:50 +0530818 frappe.get_desk_link('Warehouse', warehouse))
819 else:
820 msg = _("{0} units of {1} needed in {2} on {3} {4} for {5} to complete this transaction.").format(
Nabin Hait243d59b2021-02-02 16:55:13 +0530821 abs(deficiency), frappe.get_desk_link('Item', exceptions[0]["item_code"]),
Nabin Haita77b8c92020-12-21 14:45:50 +0530822 frappe.get_desk_link('Warehouse', warehouse),
823 exceptions[0]["posting_date"], exceptions[0]["posting_time"],
824 frappe.get_desk_link(exceptions[0]["voucher_type"], exceptions[0]["voucher_no"]))
Rushabh Mehta538607e2016-06-12 11:03:00 +0530825
Nabin Haita77b8c92020-12-21 14:45:50 +0530826 if msg:
827 msg_list.append(msg)
828
829 if msg_list:
830 message = "\n\n".join(msg_list)
831 if self.verbose:
mergify[bot]7a3d3012022-03-08 10:42:30 +0530832 frappe.throw(message, NegativeStockError, title=_('Insufficient Stock'))
Nabin Haita77b8c92020-12-21 14:45:50 +0530833 else:
834 raise NegativeStockError(message)
Deepesh Gargb4be2922021-01-28 13:09:56 +0530835
Nabin Haita77b8c92020-12-21 14:45:50 +0530836 def update_bin(self):
837 # update bin for each warehouse
Ankush Menat8fe5feb2021-11-04 19:48:32 +0530838 for warehouse, data in self.data.items():
Ankush Menat97060c42021-12-03 11:50:38 +0530839 bin_name = get_or_make_bin(self.item_code, warehouse)
Deepesh Garg6f107da2021-10-12 20:15:55 +0530840
Ankush Menat7dd10362022-03-10 17:07:57 +0530841 updated_values = {
Nabin Haita77b8c92020-12-21 14:45:50 +0530842 "actual_qty": data.qty_after_transaction,
843 "stock_value": data.stock_value
Ankush Menat7dd10362022-03-10 17:07:57 +0530844 }
845 if data.valuation_rate is not None:
846 updated_values["valuation_rate"] = data.valuation_rate
847 frappe.db.set_value('Bin', bin_name, updated_values)
Nabin Haitdc82d4f2014-04-07 12:02:57 +0530848
marination8418c4b2021-06-22 21:35:25 +0530849
850def get_previous_sle_of_current_voucher(args, exclude_current_voucher=False):
851 """get stock ledger entries filtered by specific posting datetime conditions"""
852
853 args['time_format'] = '%H:%i:%s'
854 if not args.get("posting_date"):
855 args["posting_date"] = "1900-01-01"
856 if not args.get("posting_time"):
857 args["posting_time"] = "00:00"
858
859 voucher_condition = ""
860 if exclude_current_voucher:
861 voucher_no = args.get("voucher_no")
862 voucher_condition = f"and voucher_no != '{voucher_no}'"
863
864 sle = frappe.db.sql("""
865 select *, timestamp(posting_date, posting_time) as "timestamp"
866 from `tabStock Ledger Entry`
867 where item_code = %(item_code)s
868 and warehouse = %(warehouse)s
869 and is_cancelled = 0
870 {voucher_condition}
871 and timestamp(posting_date, time_format(posting_time, %(time_format)s)) < timestamp(%(posting_date)s, time_format(%(posting_time)s, %(time_format)s))
872 order by timestamp(posting_date, posting_time) desc, creation desc
873 limit 1
874 for update""".format(voucher_condition=voucher_condition), args, as_dict=1)
875
876 return sle[0] if sle else frappe._dict()
877
Anand Doshi4dc7caa2013-01-11 11:44:49 +0530878def get_previous_sle(args, for_update=False):
Anand Doshi1b531862013-01-10 19:29:51 +0530879 """
Nabin Haitdc82d4f2014-04-07 12:02:57 +0530880 get the last sle on or before the current time-bucket,
Anand Doshi1b531862013-01-10 19:29:51 +0530881 to get actual qty before transaction, this function
882 is called from various transaction like stock entry, reco etc
Nabin Haitdc82d4f2014-04-07 12:02:57 +0530883
Anand Doshi1b531862013-01-10 19:29:51 +0530884 args = {
885 "item_code": "ABC",
886 "warehouse": "XYZ",
887 "posting_date": "2012-12-12",
888 "posting_time": "12:00",
889 "sle": "name of reference Stock Ledger Entry"
890 }
891 """
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530892 args["name"] = args.get("sle", None) or ""
893 sle = get_stock_ledger_entries(args, "<=", "desc", "limit 1", for_update=for_update)
Pratik Vyas16371b72013-09-18 18:31:03 +0530894 return sle and sle[0] or {}
Nabin Haitfb6e4342014-10-15 11:34:40 +0530895
Rohit Waghchaure66aa37f2019-05-24 16:53:51 +0530896def get_stock_ledger_entries(previous_sle, operator=None,
897 order="desc", limit=None, for_update=False, debug=False, check_serial_no=True):
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530898 """get stock ledger entries filtered by specific posting datetime conditions"""
Nabin Haitb9ce1042018-02-01 14:58:50 +0530899 conditions = " and timestamp(posting_date, posting_time) {0} timestamp(%(posting_date)s, %(posting_time)s)".format(operator)
900 if previous_sle.get("warehouse"):
901 conditions += " and warehouse = %(warehouse)s"
902 elif previous_sle.get("warehouse_condition"):
903 conditions += " and " + previous_sle.get("warehouse_condition")
904
Rohit Waghchaure66aa37f2019-05-24 16:53:51 +0530905 if check_serial_no and previous_sle.get("serial_no"):
Rohit Waghchaurebb3e5d02021-04-24 17:28:33 +0530906 # conditions += " and serial_no like {}".format(frappe.db.escape('%{0}%'.format(previous_sle.get("serial_no"))))
907 serial_no = previous_sle.get("serial_no")
908 conditions += (""" and
909 (
910 serial_no = {0}
911 or serial_no like {1}
912 or serial_no like {2}
913 or serial_no like {3}
914 )
915 """).format(frappe.db.escape(serial_no), frappe.db.escape('{}\n%'.format(serial_no)),
916 frappe.db.escape('%\n{}'.format(serial_no)), frappe.db.escape('%\n{}\n%'.format(serial_no)))
Rohit Waghchaure05d3bcb2019-04-28 18:39:18 +0530917
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530918 if not previous_sle.get("posting_date"):
919 previous_sle["posting_date"] = "1900-01-01"
920 if not previous_sle.get("posting_time"):
921 previous_sle["posting_time"] = "00:00"
922
923 if operator in (">", "<=") and previous_sle.get("name"):
924 conditions += " and name!=%(name)s"
925
Deepesh Garg2a9c5ba2020-04-30 10:38:58 +0530926 return frappe.db.sql("""
927 select *, timestamp(posting_date, posting_time) as "timestamp"
928 from `tabStock Ledger Entry`
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530929 where item_code = %%(item_code)s
Nabin Haita77b8c92020-12-21 14:45:50 +0530930 and is_cancelled = 0
Nabin Haitb9ce1042018-02-01 14:58:50 +0530931 %(conditions)s
Aditya Hase0c164242019-01-07 22:07:13 +0530932 order by timestamp(posting_date, posting_time) %(order)s, creation %(order)s
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530933 %(limit)s %(for_update)s""" % {
934 "conditions": conditions,
935 "limit": limit or "",
936 "for_update": for_update and "for update" or "",
937 "order": order
Rushabh Mehta50dc4e92015-02-19 20:05:45 +0530938 }, previous_sle, as_dict=1, debug=debug)
Rushabh Mehtadf9e80c2015-02-17 19:55:17 +0530939
Nabin Haita77b8c92020-12-21 14:45:50 +0530940def get_sle_by_voucher_detail_no(voucher_detail_no, excluded_sle=None):
941 return frappe.db.get_value('Stock Ledger Entry',
942 {'voucher_detail_no': voucher_detail_no, 'name': ['!=', excluded_sle]},
943 ['item_code', 'warehouse', 'posting_date', 'posting_time', 'timestamp(posting_date, posting_time) as timestamp'],
944 as_dict=1)
Deepesh Garg2a9c5ba2020-04-30 10:38:58 +0530945
Ankush Menatab926522022-02-19 15:37:03 +0530946def get_batch_incoming_rate(item_code, warehouse, batch_no, posting_date, posting_time, creation=None):
Ankush Menatce0514c2022-02-15 11:41:41 +0530947
Ankush Menat102fff22022-02-19 15:51:04 +0530948 Timestamp = CustomFunction('timestamp', ['date', 'time'])
949
950 sle = frappe.qb.DocType("Stock Ledger Entry")
951
952 timestamp_condition = (Timestamp(sle.posting_date, sle.posting_time) < Timestamp(posting_date, posting_time))
953 if creation:
954 timestamp_condition |= (
955 (Timestamp(sle.posting_date, sle.posting_time) == Timestamp(posting_date, posting_time))
956 & (sle.creation < creation)
Ankush Menatce0514c2022-02-15 11:41:41 +0530957 )
Ankush Menat102fff22022-02-19 15:51:04 +0530958
959 batch_details = (
960 frappe.qb
961 .from_(sle)
962 .select(
963 Sum(sle.stock_value_difference).as_("batch_value"),
964 Sum(sle.actual_qty).as_("batch_qty")
965 )
966 .where(
967 (sle.item_code == item_code)
968 & (sle.warehouse == warehouse)
969 & (sle.batch_no == batch_no)
970 & (sle.is_cancelled == 0)
971 )
972 .where(timestamp_condition)
973 ).run(as_dict=True)
Ankush Menatce0514c2022-02-15 11:41:41 +0530974
975 if batch_details and batch_details[0].batch_qty:
976 return batch_details[0].batch_value / batch_details[0].batch_qty
977
978
Rushabh Mehtacc8b2b22017-03-31 12:44:29 +0530979def get_valuation_rate(item_code, warehouse, voucher_type, voucher_no,
Ankush Menat342d09a2022-02-19 14:28:51 +0530980 allow_zero_rate=False, currency=None, company=None, raise_error_if_no_rate=True, batch_no=None):
Rohit Waghchaurea5f40942017-06-16 15:21:36 +0530981
Ankush Menatf7ffe042021-11-01 13:21:14 +0530982 if not company:
983 company = frappe.get_cached_value("Warehouse", warehouse, "company")
984
Ankush Menat342d09a2022-02-19 14:28:51 +0530985 last_valuation_rate = None
986
987 # Get moving average rate of a specific batch number
988 if warehouse and batch_no and frappe.db.get_value("Batch", batch_no, "use_batchwise_valuation"):
989 last_valuation_rate = frappe.db.sql("""
990 select sum(stock_value_difference) / sum(actual_qty)
991 from `tabStock Ledger Entry`
992 where
993 item_code = %s
994 AND warehouse = %s
995 AND batch_no = %s
996 AND is_cancelled = 0
997 AND NOT (voucher_no = %s AND voucher_type = %s)
998 """,
999 (item_code, warehouse, batch_no, voucher_no, voucher_type))
1000
Ankush Menatf7ffe042021-11-01 13:21:14 +05301001 # Get valuation rate from last sle for the same item and warehouse
Ankush Menat342d09a2022-02-19 14:28:51 +05301002 if not last_valuation_rate or last_valuation_rate[0][0] is None:
1003 last_valuation_rate = frappe.db.sql("""select valuation_rate
1004 from `tabStock Ledger Entry` force index (item_warehouse)
1005 where
1006 item_code = %s
1007 AND warehouse = %s
1008 AND valuation_rate >= 0
1009 AND is_cancelled = 0
1010 AND NOT (voucher_no = %s AND voucher_type = %s)
1011 order by posting_date desc, posting_time desc, name desc limit 1""", (item_code, warehouse, voucher_no, voucher_type))
Nabin Haitfb6e4342014-10-15 11:34:40 +05301012
1013 if not last_valuation_rate:
Nabin Haita0b967f2017-01-18 18:35:58 +05301014 # Get valuation rate from last sle for the item against any warehouse
Nabin Haitfb6e4342014-10-15 11:34:40 +05301015 last_valuation_rate = frappe.db.sql("""select valuation_rate
Deepesh Garg6f107da2021-10-12 20:15:55 +05301016 from `tabStock Ledger Entry` force index (item_code)
Mangesh-Khairnar0df51342019-08-19 10:04:52 +05301017 where
1018 item_code = %s
1019 AND valuation_rate > 0
Ankush Menat82ea9582022-01-16 20:19:04 +05301020 AND is_cancelled = 0
Mangesh-Khairnar0df51342019-08-19 10:04:52 +05301021 AND NOT(voucher_no = %s AND voucher_type = %s)
1022 order by posting_date desc, posting_time desc, name desc limit 1""", (item_code, voucher_no, voucher_type))
Nabin Haitfb6e4342014-10-15 11:34:40 +05301023
Nabin Haita645f362018-03-01 10:31:24 +05301024 if last_valuation_rate:
Nabin Haita77b8c92020-12-21 14:45:50 +05301025 return flt(last_valuation_rate[0][0])
Nabin Haita645f362018-03-01 10:31:24 +05301026
1027 # If negative stock allowed, and item delivered without any incoming entry,
1028 # system does not found any SLE, then take valuation rate from Item
1029 valuation_rate = frappe.db.get_value("Item", item_code, "valuation_rate")
Nabin Haitfb6e4342014-10-15 11:34:40 +05301030
1031 if not valuation_rate:
Nabin Haita645f362018-03-01 10:31:24 +05301032 # try Item Standard rate
1033 valuation_rate = frappe.db.get_value("Item", item_code, "standard_rate")
Nabin Haitfb6e4342014-10-15 11:34:40 +05301034
Rushabh Mehtaaedaac62017-05-04 09:35:19 +05301035 if not valuation_rate:
Nabin Haita645f362018-03-01 10:31:24 +05301036 # try in price list
1037 valuation_rate = frappe.db.get_value('Item Price',
1038 dict(item_code=item_code, buying=1, currency=currency),
1039 'price_list_rate')
Rushabh Mehtacc8b2b22017-03-31 12:44:29 +05301040
Nabin Hait7ba092e2018-02-01 10:51:27 +05301041 if not allow_zero_rate and not valuation_rate and raise_error_if_no_rate \
Rohit Waghchauree9ff1912017-06-19 12:54:59 +05301042 and cint(erpnext.is_perpetual_inventory_enabled(company)):
Neil Trini Lasrado193c8912017-03-28 17:39:34 +05301043 frappe.local.message_log = []
Rohit Waghchaurebb3e5d02021-04-24 17:28:33 +05301044 form_link = get_link_to_form("Item", item_code)
Marica97715f22020-05-11 20:45:37 +05301045
1046 message = _("Valuation Rate for the Item {0}, is required to do accounting entries for {1} {2}.").format(form_link, voucher_type, voucher_no)
Rohit Waghchaurebb3e5d02021-04-24 17:28:33 +05301047 message += "<br><br>" + _("Here are the options to proceed:")
Marica97715f22020-05-11 20:45:37 +05301048 solutions = "<li>" + _("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.").format(voucher_type) + "</li>"
Rohit Waghchaurebb3e5d02021-04-24 17:28:33 +05301049 solutions += "<li>" + _("If not, you can Cancel / Submit this entry") + " {0} ".format(frappe.bold("after")) + _("performing either one below:") + "</li>"
Marica97715f22020-05-11 20:45:37 +05301050 sub_solutions = "<ul><li>" + _("Create an incoming stock transaction for the Item.") + "</li>"
1051 sub_solutions += "<li>" + _("Mention Valuation Rate in the Item master.") + "</li></ul>"
1052 msg = message + solutions + sub_solutions + "</li>"
1053
1054 frappe.throw(msg=msg, title=_("Valuation Rate Missing"))
Nabin Haitfb6e4342014-10-15 11:34:40 +05301055
1056 return valuation_rate
Nabin Haita77b8c92020-12-21 14:45:50 +05301057
Ankush Menate7109c12021-08-26 16:40:45 +05301058def update_qty_in_future_sle(args, allow_negative_stock=False):
marination8418c4b2021-06-22 21:35:25 +05301059 """Recalculate Qty after Transaction in future SLEs based on current SLE."""
marination40389772021-07-02 17:13:45 +05301060 datetime_limit_condition = ""
marination8418c4b2021-06-22 21:35:25 +05301061 qty_shift = args.actual_qty
1062
1063 # find difference/shift in qty caused by stock reconciliation
1064 if args.voucher_type == "Stock Reconciliation":
marination40389772021-07-02 17:13:45 +05301065 qty_shift = get_stock_reco_qty_shift(args)
1066
1067 # find the next nearest stock reco so that we only recalculate SLEs till that point
1068 next_stock_reco_detail = get_next_stock_reco(args)
1069 if next_stock_reco_detail:
1070 detail = next_stock_reco_detail[0]
1071 # add condition to update SLEs before this date & time
1072 datetime_limit_condition = get_datetime_limit_condition(detail)
marination8418c4b2021-06-22 21:35:25 +05301073
Nabin Hait186a0452021-02-18 14:14:21 +05301074 frappe.db.sql("""
1075 update `tabStock Ledger Entry`
marination8418c4b2021-06-22 21:35:25 +05301076 set qty_after_transaction = qty_after_transaction + {qty_shift}
Nabin Hait186a0452021-02-18 14:14:21 +05301077 where
1078 item_code = %(item_code)s
1079 and warehouse = %(warehouse)s
1080 and voucher_no != %(voucher_no)s
1081 and is_cancelled = 0
1082 and (timestamp(posting_date, posting_time) > timestamp(%(posting_date)s, %(posting_time)s)
1083 or (
1084 timestamp(posting_date, posting_time) = timestamp(%(posting_date)s, %(posting_time)s)
1085 and creation > %(creation)s
1086 )
1087 )
marination40389772021-07-02 17:13:45 +05301088 {datetime_limit_condition}
1089 """.format(qty_shift=qty_shift, datetime_limit_condition=datetime_limit_condition), args)
Nabin Hait186a0452021-02-18 14:14:21 +05301090
1091 validate_negative_qty_in_future_sle(args, allow_negative_stock)
1092
marination40389772021-07-02 17:13:45 +05301093def get_stock_reco_qty_shift(args):
1094 stock_reco_qty_shift = 0
1095 if args.get("is_cancelled"):
1096 if args.get("previous_qty_after_transaction"):
1097 # get qty (balance) that was set at submission
1098 last_balance = args.get("previous_qty_after_transaction")
1099 stock_reco_qty_shift = flt(args.qty_after_transaction) - flt(last_balance)
1100 else:
1101 stock_reco_qty_shift = flt(args.actual_qty)
1102 else:
1103 # reco is being submitted
1104 last_balance = get_previous_sle_of_current_voucher(args,
1105 exclude_current_voucher=True).get("qty_after_transaction")
1106
1107 if last_balance is not None:
1108 stock_reco_qty_shift = flt(args.qty_after_transaction) - flt(last_balance)
1109 else:
1110 stock_reco_qty_shift = args.qty_after_transaction
1111
1112 return stock_reco_qty_shift
1113
1114def get_next_stock_reco(args):
1115 """Returns next nearest stock reconciliaton's details."""
1116
1117 return frappe.db.sql("""
1118 select
1119 name, posting_date, posting_time, creation, voucher_no
1120 from
marination8c441262021-07-02 17:46:05 +05301121 `tabStock Ledger Entry`
marination40389772021-07-02 17:13:45 +05301122 where
1123 item_code = %(item_code)s
1124 and warehouse = %(warehouse)s
1125 and voucher_type = 'Stock Reconciliation'
1126 and voucher_no != %(voucher_no)s
1127 and is_cancelled = 0
1128 and (timestamp(posting_date, posting_time) > timestamp(%(posting_date)s, %(posting_time)s)
1129 or (
1130 timestamp(posting_date, posting_time) = timestamp(%(posting_date)s, %(posting_time)s)
1131 and creation > %(creation)s
1132 )
1133 )
1134 limit 1
1135 """, args, as_dict=1)
1136
1137def get_datetime_limit_condition(detail):
marination40389772021-07-02 17:13:45 +05301138 return f"""
1139 and
1140 (timestamp(posting_date, posting_time) < timestamp('{detail.posting_date}', '{detail.posting_time}')
1141 or (
1142 timestamp(posting_date, posting_time) = timestamp('{detail.posting_date}', '{detail.posting_time}')
1143 and creation < '{detail.creation}'
1144 )
1145 )"""
1146
Ankush Menate7109c12021-08-26 16:40:45 +05301147def validate_negative_qty_in_future_sle(args, allow_negative_stock=False):
Ankush Menateb8b4242022-02-12 13:08:28 +05301148 if allow_negative_stock or is_negative_stock_allowed(item_code=args.item_code):
Ankush Menat5eba5752021-12-07 23:03:52 +05301149 return
1150 if not (args.actual_qty < 0 or args.voucher_type == "Stock Reconciliation"):
1151 return
Deepesh Gargb4be2922021-01-28 13:09:56 +05301152
Ankush Menat5eba5752021-12-07 23:03:52 +05301153 neg_sle = get_future_sle_with_negative_qty(args)
1154 if neg_sle:
1155 message = _("{0} units of {1} needed in {2} on {3} {4} for {5} to complete this transaction.").format(
1156 abs(neg_sle[0]["qty_after_transaction"]),
1157 frappe.get_desk_link('Item', args.item_code),
1158 frappe.get_desk_link('Warehouse', args.warehouse),
1159 neg_sle[0]["posting_date"], neg_sle[0]["posting_time"],
1160 frappe.get_desk_link(neg_sle[0]["voucher_type"], neg_sle[0]["voucher_no"]))
1161
mergify[bot]7a3d3012022-03-08 10:42:30 +05301162 frappe.throw(message, NegativeStockError, title=_('Insufficient Stock'))
Ankush Menat5eba5752021-12-07 23:03:52 +05301163
1164
1165 if not args.batch_no:
1166 return
1167
1168 neg_batch_sle = get_future_sle_with_negative_batch_qty(args)
1169 if neg_batch_sle:
1170 message = _("{0} units of {1} needed in {2} on {3} {4} for {5} to complete this transaction.").format(
1171 abs(neg_batch_sle[0]["cumulative_total"]),
1172 frappe.get_desk_link('Batch', args.batch_no),
1173 frappe.get_desk_link('Warehouse', args.warehouse),
1174 neg_batch_sle[0]["posting_date"], neg_batch_sle[0]["posting_time"],
1175 frappe.get_desk_link(neg_batch_sle[0]["voucher_type"], neg_batch_sle[0]["voucher_no"]))
mergify[bot]7a3d3012022-03-08 10:42:30 +05301176 frappe.throw(message, NegativeStockError, title=_("Insufficient Stock for Batch"))
Ankush Menat5eba5752021-12-07 23:03:52 +05301177
Nabin Haita77b8c92020-12-21 14:45:50 +05301178
1179def get_future_sle_with_negative_qty(args):
1180 return frappe.db.sql("""
1181 select
1182 qty_after_transaction, posting_date, posting_time,
1183 voucher_type, voucher_no
1184 from `tabStock Ledger Entry`
Deepesh Gargb4be2922021-01-28 13:09:56 +05301185 where
Nabin Haita77b8c92020-12-21 14:45:50 +05301186 item_code = %(item_code)s
1187 and warehouse = %(warehouse)s
1188 and voucher_no != %(voucher_no)s
1189 and timestamp(posting_date, posting_time) >= timestamp(%(posting_date)s, %(posting_time)s)
1190 and is_cancelled = 0
Nabin Hait186a0452021-02-18 14:14:21 +05301191 and qty_after_transaction < 0
Nabin Hait243d59b2021-02-02 16:55:13 +05301192 order by timestamp(posting_date, posting_time) asc
Nabin Haita77b8c92020-12-21 14:45:50 +05301193 limit 1
Sagar Vorae50324a2021-03-31 12:44:03 +05301194 """, args, as_dict=1)
Ankush Menat6a014d12021-04-12 20:21:27 +05301195
Ankush Menat5eba5752021-12-07 23:03:52 +05301196
1197def get_future_sle_with_negative_batch_qty(args):
1198 return frappe.db.sql("""
1199 with batch_ledger as (
1200 select
1201 posting_date, posting_time, voucher_type, voucher_no,
1202 sum(actual_qty) over (order by posting_date, posting_time, creation) as cumulative_total
1203 from `tabStock Ledger Entry`
1204 where
1205 item_code = %(item_code)s
1206 and warehouse = %(warehouse)s
1207 and batch_no=%(batch_no)s
1208 and is_cancelled = 0
1209 order by posting_date, posting_time, creation
1210 )
1211 select * from batch_ledger
1212 where
1213 cumulative_total < 0.0
1214 and timestamp(posting_date, posting_time) >= timestamp(%(posting_date)s, %(posting_time)s)
1215 limit 1
1216 """, args, as_dict=1)
Ankush Menateb8b4242022-02-12 13:08:28 +05301217
1218
1219def is_negative_stock_allowed(*, item_code: Optional[str] = None) -> bool:
1220 if cint(frappe.db.get_single_value("Stock Settings", "allow_negative_stock", cache=True)):
1221 return True
1222 if item_code and cint(frappe.db.get_value("Item", item_code, "allow_negative_stock", cache=True)):
1223 return True
1224 return False