You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 

1008 lines
32 KiB

  1. # Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
  2. # License: GNU General Public License v3. See license.txt
  3. import json
  4. from collections import defaultdict
  5. from typing import List, Tuple
  6. import frappe
  7. from frappe import _
  8. from frappe.utils import cint, cstr, flt, get_link_to_form, getdate
  9. import erpnext
  10. from erpnext.accounts.general_ledger import (
  11. make_gl_entries,
  12. make_reverse_gl_entries,
  13. process_gl_map,
  14. )
  15. from erpnext.accounts.utils import get_fiscal_year
  16. from erpnext.controllers.accounts_controller import AccountsController
  17. from erpnext.stock import get_warehouse_account_map
  18. from erpnext.stock.doctype.inventory_dimension.inventory_dimension import (
  19. get_evaluated_inventory_dimension,
  20. )
  21. from erpnext.stock.stock_ledger import get_items_to_be_repost
  22. class QualityInspectionRequiredError(frappe.ValidationError):
  23. pass
  24. class QualityInspectionRejectedError(frappe.ValidationError):
  25. pass
  26. class QualityInspectionNotSubmittedError(frappe.ValidationError):
  27. pass
  28. class BatchExpiredError(frappe.ValidationError):
  29. pass
  30. class StockController(AccountsController):
  31. def validate(self):
  32. super(StockController, self).validate()
  33. if not self.get("is_return"):
  34. self.validate_inspection()
  35. self.validate_serialized_batch()
  36. self.clean_serial_nos()
  37. self.validate_customer_provided_item()
  38. self.set_rate_of_stock_uom()
  39. self.validate_internal_transfer()
  40. self.validate_putaway_capacity()
  41. def make_gl_entries(self, gl_entries=None, from_repost=False):
  42. if self.docstatus == 2:
  43. make_reverse_gl_entries(voucher_type=self.doctype, voucher_no=self.name)
  44. provisional_accounting_for_non_stock_items = cint(
  45. frappe.get_cached_value(
  46. "Company", self.company, "enable_provisional_accounting_for_non_stock_items"
  47. )
  48. )
  49. if (
  50. cint(erpnext.is_perpetual_inventory_enabled(self.company))
  51. or provisional_accounting_for_non_stock_items
  52. ):
  53. warehouse_account = get_warehouse_account_map(self.company)
  54. if self.docstatus == 1:
  55. if not gl_entries:
  56. gl_entries = self.get_gl_entries(warehouse_account)
  57. make_gl_entries(gl_entries, from_repost=from_repost)
  58. elif self.doctype in ["Purchase Receipt", "Purchase Invoice"] and self.docstatus == 1:
  59. gl_entries = []
  60. gl_entries = self.get_asset_gl_entry(gl_entries)
  61. make_gl_entries(gl_entries, from_repost=from_repost)
  62. def validate_serialized_batch(self):
  63. from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos
  64. is_material_issue = False
  65. if self.doctype == "Stock Entry" and self.purpose == "Material Issue":
  66. is_material_issue = True
  67. for d in self.get("items"):
  68. if hasattr(d, "serial_no") and hasattr(d, "batch_no") and d.serial_no and d.batch_no:
  69. serial_nos = frappe.get_all(
  70. "Serial No",
  71. fields=["batch_no", "name", "warehouse"],
  72. filters={"name": ("in", get_serial_nos(d.serial_no))},
  73. )
  74. for row in serial_nos:
  75. if row.warehouse and row.batch_no != d.batch_no:
  76. frappe.throw(
  77. _("Row #{0}: Serial No {1} does not belong to Batch {2}").format(
  78. d.idx, row.name, d.batch_no
  79. )
  80. )
  81. if is_material_issue:
  82. continue
  83. if flt(d.qty) > 0.0 and d.get("batch_no") and self.get("posting_date") and self.docstatus < 2:
  84. expiry_date = frappe.get_cached_value("Batch", d.get("batch_no"), "expiry_date")
  85. if expiry_date and getdate(expiry_date) < getdate(self.posting_date):
  86. frappe.throw(
  87. _("Row #{0}: The batch {1} has already expired.").format(
  88. d.idx, get_link_to_form("Batch", d.get("batch_no"))
  89. ),
  90. BatchExpiredError,
  91. )
  92. def clean_serial_nos(self):
  93. from erpnext.stock.doctype.serial_no.serial_no import clean_serial_no_string
  94. for row in self.get("items"):
  95. if hasattr(row, "serial_no") and row.serial_no:
  96. # remove extra whitespace and store one serial no on each line
  97. row.serial_no = clean_serial_no_string(row.serial_no)
  98. for row in self.get("packed_items") or []:
  99. if hasattr(row, "serial_no") and row.serial_no:
  100. # remove extra whitespace and store one serial no on each line
  101. row.serial_no = clean_serial_no_string(row.serial_no)
  102. def get_gl_entries(
  103. self, warehouse_account=None, default_expense_account=None, default_cost_center=None
  104. ):
  105. if not warehouse_account:
  106. warehouse_account = get_warehouse_account_map(self.company)
  107. sle_map = self.get_stock_ledger_details()
  108. voucher_details = self.get_voucher_details(default_expense_account, default_cost_center, sle_map)
  109. gl_list = []
  110. warehouse_with_no_account = []
  111. precision = self.get_debit_field_precision()
  112. for item_row in voucher_details:
  113. sle_list = sle_map.get(item_row.name)
  114. sle_rounding_diff = 0.0
  115. if sle_list:
  116. for sle in sle_list:
  117. if warehouse_account.get(sle.warehouse):
  118. # from warehouse account
  119. sle_rounding_diff += flt(sle.stock_value_difference)
  120. self.check_expense_account(item_row)
  121. # expense account/ target_warehouse / source_warehouse
  122. if item_row.get("target_warehouse"):
  123. warehouse = item_row.get("target_warehouse")
  124. expense_account = warehouse_account[warehouse]["account"]
  125. else:
  126. expense_account = item_row.expense_account
  127. gl_list.append(
  128. self.get_gl_dict(
  129. {
  130. "account": warehouse_account[sle.warehouse]["account"],
  131. "against": expense_account,
  132. "cost_center": item_row.cost_center,
  133. "project": item_row.project or self.get("project"),
  134. "remarks": self.get("remarks") or _("Accounting Entry for Stock"),
  135. "debit": flt(sle.stock_value_difference, precision),
  136. "is_opening": item_row.get("is_opening") or self.get("is_opening") or "No",
  137. },
  138. warehouse_account[sle.warehouse]["account_currency"],
  139. item=item_row,
  140. )
  141. )
  142. gl_list.append(
  143. self.get_gl_dict(
  144. {
  145. "account": expense_account,
  146. "against": warehouse_account[sle.warehouse]["account"],
  147. "cost_center": item_row.cost_center,
  148. "remarks": self.get("remarks") or _("Accounting Entry for Stock"),
  149. "debit": -1 * flt(sle.stock_value_difference, precision),
  150. "project": item_row.get("project") or self.get("project"),
  151. "is_opening": item_row.get("is_opening") or self.get("is_opening") or "No",
  152. },
  153. item=item_row,
  154. )
  155. )
  156. elif sle.warehouse not in warehouse_with_no_account:
  157. warehouse_with_no_account.append(sle.warehouse)
  158. if abs(sle_rounding_diff) > (1.0 / (10**precision)) and self.is_internal_transfer():
  159. warehouse_asset_account = ""
  160. if self.get("is_internal_customer"):
  161. warehouse_asset_account = warehouse_account[item_row.get("target_warehouse")]["account"]
  162. elif self.get("is_internal_supplier"):
  163. warehouse_asset_account = warehouse_account[item_row.get("warehouse")]["account"]
  164. expense_account = frappe.get_cached_value("Company", self.company, "default_expense_account")
  165. gl_list.append(
  166. self.get_gl_dict(
  167. {
  168. "account": expense_account,
  169. "against": warehouse_asset_account,
  170. "cost_center": item_row.cost_center,
  171. "project": item_row.project or self.get("project"),
  172. "remarks": _("Rounding gain/loss Entry for Stock Transfer"),
  173. "debit": sle_rounding_diff,
  174. "is_opening": item_row.get("is_opening") or self.get("is_opening") or "No",
  175. },
  176. warehouse_account[sle.warehouse]["account_currency"],
  177. item=item_row,
  178. )
  179. )
  180. gl_list.append(
  181. self.get_gl_dict(
  182. {
  183. "account": warehouse_asset_account,
  184. "against": expense_account,
  185. "cost_center": item_row.cost_center,
  186. "remarks": _("Rounding gain/loss Entry for Stock Transfer"),
  187. "credit": sle_rounding_diff,
  188. "project": item_row.get("project") or self.get("project"),
  189. "is_opening": item_row.get("is_opening") or self.get("is_opening") or "No",
  190. },
  191. item=item_row,
  192. )
  193. )
  194. if warehouse_with_no_account:
  195. for wh in warehouse_with_no_account:
  196. if frappe.get_cached_value("Warehouse", wh, "company"):
  197. frappe.throw(
  198. _(
  199. "Warehouse {0} is not linked to any account, please mention the account in the warehouse record or set default inventory account in company {1}."
  200. ).format(wh, self.company)
  201. )
  202. return process_gl_map(gl_list, precision=precision)
  203. def get_debit_field_precision(self):
  204. if not frappe.flags.debit_field_precision:
  205. frappe.flags.debit_field_precision = frappe.get_precision(
  206. "GL Entry", "debit_in_account_currency"
  207. )
  208. return frappe.flags.debit_field_precision
  209. def get_voucher_details(self, default_expense_account, default_cost_center, sle_map):
  210. if self.doctype == "Stock Reconciliation":
  211. reconciliation_purpose = frappe.db.get_value(self.doctype, self.name, "purpose")
  212. is_opening = "Yes" if reconciliation_purpose == "Opening Stock" else "No"
  213. details = []
  214. for voucher_detail_no in sle_map:
  215. details.append(
  216. frappe._dict(
  217. {
  218. "name": voucher_detail_no,
  219. "expense_account": default_expense_account,
  220. "cost_center": default_cost_center,
  221. "is_opening": is_opening,
  222. }
  223. )
  224. )
  225. return details
  226. else:
  227. details = self.get("items")
  228. if default_expense_account or default_cost_center:
  229. for d in details:
  230. if default_expense_account and not d.get("expense_account"):
  231. d.expense_account = default_expense_account
  232. if default_cost_center and not d.get("cost_center"):
  233. d.cost_center = default_cost_center
  234. return details
  235. def get_items_and_warehouses(self) -> Tuple[List[str], List[str]]:
  236. """Get list of items and warehouses affected by a transaction"""
  237. if not (hasattr(self, "items") or hasattr(self, "packed_items")):
  238. return [], []
  239. item_rows = (self.get("items") or []) + (self.get("packed_items") or [])
  240. items = {d.item_code for d in item_rows if d.item_code}
  241. warehouses = set()
  242. for d in item_rows:
  243. if d.get("warehouse"):
  244. warehouses.add(d.warehouse)
  245. if self.doctype == "Stock Entry":
  246. if d.get("s_warehouse"):
  247. warehouses.add(d.s_warehouse)
  248. if d.get("t_warehouse"):
  249. warehouses.add(d.t_warehouse)
  250. return list(items), list(warehouses)
  251. def get_stock_ledger_details(self):
  252. stock_ledger = {}
  253. stock_ledger_entries = frappe.db.sql(
  254. """
  255. select
  256. name, warehouse, stock_value_difference, valuation_rate,
  257. voucher_detail_no, item_code, posting_date, posting_time,
  258. actual_qty, qty_after_transaction
  259. from
  260. `tabStock Ledger Entry`
  261. where
  262. voucher_type=%s and voucher_no=%s and is_cancelled = 0
  263. """,
  264. (self.doctype, self.name),
  265. as_dict=True,
  266. )
  267. for sle in stock_ledger_entries:
  268. stock_ledger.setdefault(sle.voucher_detail_no, []).append(sle)
  269. return stock_ledger
  270. def make_batches(self, warehouse_field):
  271. """Create batches if required. Called before submit"""
  272. for d in self.items:
  273. if d.get(warehouse_field) and not d.batch_no:
  274. has_batch_no, create_new_batch = frappe.get_cached_value(
  275. "Item", d.item_code, ["has_batch_no", "create_new_batch"]
  276. )
  277. if has_batch_no and create_new_batch:
  278. d.batch_no = (
  279. frappe.get_doc(
  280. dict(
  281. doctype="Batch",
  282. item=d.item_code,
  283. supplier=getattr(self, "supplier", None),
  284. reference_doctype=self.doctype,
  285. reference_name=self.name,
  286. )
  287. )
  288. .insert()
  289. .name
  290. )
  291. def check_expense_account(self, item):
  292. if not item.get("expense_account"):
  293. msg = _("Please set an Expense Account in the Items table")
  294. frappe.throw(
  295. _("Row #{0}: Expense Account not set for the Item {1}. {2}").format(
  296. item.idx, frappe.bold(item.item_code), msg
  297. ),
  298. title=_("Expense Account Missing"),
  299. )
  300. else:
  301. is_expense_account = (
  302. frappe.get_cached_value("Account", item.get("expense_account"), "report_type")
  303. == "Profit and Loss"
  304. )
  305. if (
  306. self.doctype
  307. not in (
  308. "Purchase Receipt",
  309. "Purchase Invoice",
  310. "Stock Reconciliation",
  311. "Stock Entry",
  312. "Subcontracting Receipt",
  313. )
  314. and not is_expense_account
  315. ):
  316. frappe.throw(
  317. _("Expense / Difference account ({0}) must be a 'Profit or Loss' account").format(
  318. item.get("expense_account")
  319. )
  320. )
  321. if is_expense_account and not item.get("cost_center"):
  322. frappe.throw(
  323. _("{0} {1}: Cost Center is mandatory for Item {2}").format(
  324. _(self.doctype), self.name, item.get("item_code")
  325. )
  326. )
  327. def delete_auto_created_batches(self):
  328. for d in self.items:
  329. if not d.batch_no:
  330. continue
  331. frappe.db.set_value(
  332. "Serial No", {"batch_no": d.batch_no, "status": "Inactive"}, "batch_no", None
  333. )
  334. d.batch_no = None
  335. d.db_set("batch_no", None)
  336. for data in frappe.get_all(
  337. "Batch", {"reference_name": self.name, "reference_doctype": self.doctype}
  338. ):
  339. frappe.delete_doc("Batch", data.name)
  340. def get_sl_entries(self, d, args):
  341. sl_dict = frappe._dict(
  342. {
  343. "item_code": d.get("item_code", None),
  344. "warehouse": d.get("warehouse", None),
  345. "posting_date": self.posting_date,
  346. "posting_time": self.posting_time,
  347. "fiscal_year": get_fiscal_year(self.posting_date, company=self.company)[0],
  348. "voucher_type": self.doctype,
  349. "voucher_no": self.name,
  350. "voucher_detail_no": d.name,
  351. "actual_qty": (self.docstatus == 1 and 1 or -1) * flt(d.get("stock_qty")),
  352. "stock_uom": frappe.get_cached_value(
  353. "Item", args.get("item_code") or d.get("item_code"), "stock_uom"
  354. ),
  355. "incoming_rate": 0,
  356. "company": self.company,
  357. "batch_no": cstr(d.get("batch_no")).strip(),
  358. "serial_no": d.get("serial_no"),
  359. "project": d.get("project") or self.get("project"),
  360. "is_cancelled": 1 if self.docstatus == 2 else 0,
  361. }
  362. )
  363. sl_dict.update(args)
  364. self.update_inventory_dimensions(d, sl_dict)
  365. return sl_dict
  366. def update_inventory_dimensions(self, row, sl_dict) -> None:
  367. # To handle delivery note and sales invoice
  368. if row.get("item_row"):
  369. row = row.get("item_row")
  370. dimensions = get_evaluated_inventory_dimension(row, sl_dict, parent_doc=self)
  371. for dimension in dimensions:
  372. if not dimension:
  373. continue
  374. if row.get(dimension.source_fieldname):
  375. sl_dict[dimension.target_fieldname] = row.get(dimension.source_fieldname)
  376. if not sl_dict.get(dimension.target_fieldname) and dimension.fetch_from_parent:
  377. sl_dict[dimension.target_fieldname] = self.get(dimension.fetch_from_parent)
  378. # Get value based on doctype name
  379. if not sl_dict.get(dimension.target_fieldname):
  380. fieldname = next(
  381. (
  382. field.fieldname
  383. for field in frappe.get_meta(self.doctype).fields
  384. if field.options == dimension.fetch_from_parent
  385. ),
  386. None,
  387. )
  388. if fieldname and self.get(fieldname):
  389. sl_dict[dimension.target_fieldname] = self.get(fieldname)
  390. if sl_dict[dimension.target_fieldname] and self.docstatus == 1:
  391. row.db_set(dimension.source_fieldname, sl_dict[dimension.target_fieldname])
  392. def make_sl_entries(self, sl_entries, allow_negative_stock=False, via_landed_cost_voucher=False):
  393. from erpnext.stock.stock_ledger import make_sl_entries
  394. make_sl_entries(sl_entries, allow_negative_stock, via_landed_cost_voucher)
  395. def make_gl_entries_on_cancel(self):
  396. if frappe.db.sql(
  397. """select name from `tabGL Entry` where voucher_type=%s
  398. and voucher_no=%s""",
  399. (self.doctype, self.name),
  400. ):
  401. self.make_gl_entries()
  402. def get_serialized_items(self):
  403. serialized_items = []
  404. item_codes = list(set(d.item_code for d in self.get("items")))
  405. if item_codes:
  406. serialized_items = frappe.db.sql_list(
  407. """select name from `tabItem`
  408. where has_serial_no=1 and name in ({})""".format(
  409. ", ".join(["%s"] * len(item_codes))
  410. ),
  411. tuple(item_codes),
  412. )
  413. return serialized_items
  414. def validate_warehouse(self):
  415. from erpnext.stock.utils import validate_disabled_warehouse, validate_warehouse_company
  416. warehouses = list(set(d.warehouse for d in self.get("items") if getattr(d, "warehouse", None)))
  417. target_warehouses = list(
  418. set([d.target_warehouse for d in self.get("items") if getattr(d, "target_warehouse", None)])
  419. )
  420. warehouses.extend(target_warehouses)
  421. from_warehouse = list(
  422. set([d.from_warehouse for d in self.get("items") if getattr(d, "from_warehouse", None)])
  423. )
  424. warehouses.extend(from_warehouse)
  425. for w in warehouses:
  426. validate_disabled_warehouse(w)
  427. validate_warehouse_company(w, self.company)
  428. def update_billing_percentage(self, update_modified=True):
  429. target_ref_field = "amount"
  430. if self.doctype == "Delivery Note":
  431. target_ref_field = "amount - (returned_qty * rate)"
  432. self._update_percent_field(
  433. {
  434. "target_dt": self.doctype + " Item",
  435. "target_parent_dt": self.doctype,
  436. "target_parent_field": "per_billed",
  437. "target_ref_field": target_ref_field,
  438. "target_field": "billed_amt",
  439. "name": self.name,
  440. },
  441. update_modified,
  442. )
  443. def validate_inspection(self):
  444. """Checks if quality inspection is set/ is valid for Items that require inspection."""
  445. inspection_fieldname_map = {
  446. "Purchase Receipt": "inspection_required_before_purchase",
  447. "Purchase Invoice": "inspection_required_before_purchase",
  448. "Sales Invoice": "inspection_required_before_delivery",
  449. "Delivery Note": "inspection_required_before_delivery",
  450. }
  451. inspection_required_fieldname = inspection_fieldname_map.get(self.doctype)
  452. # return if inspection is not required on document level
  453. if (
  454. (not inspection_required_fieldname and self.doctype != "Stock Entry")
  455. or (self.doctype == "Stock Entry" and not self.inspection_required)
  456. or (self.doctype in ["Sales Invoice", "Purchase Invoice"] and not self.update_stock)
  457. ):
  458. return
  459. for row in self.get("items"):
  460. qi_required = False
  461. if inspection_required_fieldname and frappe.db.get_value(
  462. "Item", row.item_code, inspection_required_fieldname
  463. ):
  464. qi_required = True
  465. elif self.doctype == "Stock Entry" and row.t_warehouse:
  466. qi_required = True # inward stock needs inspection
  467. if qi_required: # validate row only if inspection is required on item level
  468. self.validate_qi_presence(row)
  469. if self.docstatus == 1:
  470. self.validate_qi_submission(row)
  471. self.validate_qi_rejection(row)
  472. def validate_qi_presence(self, row):
  473. """Check if QI is present on row level. Warn on save and stop on submit if missing."""
  474. if not row.quality_inspection:
  475. msg = f"Row #{row.idx}: Quality Inspection is required for Item {frappe.bold(row.item_code)}"
  476. if self.docstatus == 1:
  477. frappe.throw(_(msg), title=_("Inspection Required"), exc=QualityInspectionRequiredError)
  478. else:
  479. frappe.msgprint(_(msg), title=_("Inspection Required"), indicator="blue")
  480. def validate_qi_submission(self, row):
  481. """Check if QI is submitted on row level, during submission"""
  482. action = frappe.db.get_single_value(
  483. "Stock Settings", "action_if_quality_inspection_is_not_submitted"
  484. )
  485. qa_docstatus = frappe.db.get_value("Quality Inspection", row.quality_inspection, "docstatus")
  486. if not qa_docstatus == 1:
  487. link = frappe.utils.get_link_to_form("Quality Inspection", row.quality_inspection)
  488. msg = (
  489. f"Row #{row.idx}: Quality Inspection {link} is not submitted for the item: {row.item_code}"
  490. )
  491. if action == "Stop":
  492. frappe.throw(_(msg), title=_("Inspection Submission"), exc=QualityInspectionNotSubmittedError)
  493. else:
  494. frappe.msgprint(_(msg), alert=True, indicator="orange")
  495. def validate_qi_rejection(self, row):
  496. """Check if QI is rejected on row level, during submission"""
  497. action = frappe.db.get_single_value("Stock Settings", "action_if_quality_inspection_is_rejected")
  498. qa_status = frappe.db.get_value("Quality Inspection", row.quality_inspection, "status")
  499. if qa_status == "Rejected":
  500. link = frappe.utils.get_link_to_form("Quality Inspection", row.quality_inspection)
  501. msg = f"Row #{row.idx}: Quality Inspection {link} was rejected for item {row.item_code}"
  502. if action == "Stop":
  503. frappe.throw(_(msg), title=_("Inspection Rejected"), exc=QualityInspectionRejectedError)
  504. else:
  505. frappe.msgprint(_(msg), alert=True, indicator="orange")
  506. def update_blanket_order(self):
  507. blanket_orders = list(set([d.blanket_order for d in self.items if d.blanket_order]))
  508. for blanket_order in blanket_orders:
  509. frappe.get_doc("Blanket Order", blanket_order).update_ordered_qty()
  510. def validate_customer_provided_item(self):
  511. for d in self.get("items"):
  512. # Customer Provided parts will have zero valuation rate
  513. if frappe.get_cached_value("Item", d.item_code, "is_customer_provided_item"):
  514. d.allow_zero_valuation_rate = 1
  515. def set_rate_of_stock_uom(self):
  516. if self.doctype in [
  517. "Purchase Receipt",
  518. "Purchase Invoice",
  519. "Purchase Order",
  520. "Sales Invoice",
  521. "Sales Order",
  522. "Delivery Note",
  523. "Quotation",
  524. ]:
  525. for d in self.get("items"):
  526. d.stock_uom_rate = d.rate / (d.conversion_factor or 1)
  527. def validate_internal_transfer(self):
  528. if (
  529. self.doctype in ("Sales Invoice", "Delivery Note", "Purchase Invoice", "Purchase Receipt")
  530. and self.is_internal_transfer()
  531. ):
  532. self.validate_in_transit_warehouses()
  533. self.validate_multi_currency()
  534. self.validate_packed_items()
  535. def validate_in_transit_warehouses(self):
  536. if (
  537. self.doctype == "Sales Invoice" and self.get("update_stock")
  538. ) or self.doctype == "Delivery Note":
  539. for item in self.get("items"):
  540. if not item.target_warehouse:
  541. frappe.throw(
  542. _("Row {0}: Target Warehouse is mandatory for internal transfers").format(item.idx)
  543. )
  544. if (
  545. self.doctype == "Purchase Invoice" and self.get("update_stock")
  546. ) or self.doctype == "Purchase Receipt":
  547. for item in self.get("items"):
  548. if not item.from_warehouse:
  549. frappe.throw(
  550. _("Row {0}: From Warehouse is mandatory for internal transfers").format(item.idx)
  551. )
  552. def validate_multi_currency(self):
  553. if self.currency != self.company_currency:
  554. frappe.throw(_("Internal transfers can only be done in company's default currency"))
  555. def validate_packed_items(self):
  556. if self.doctype in ("Sales Invoice", "Delivery Note Item") and self.get("packed_items"):
  557. frappe.throw(_("Packed Items cannot be transferred internally"))
  558. def validate_putaway_capacity(self):
  559. # if over receipt is attempted while 'apply putaway rule' is disabled
  560. # and if rule was applied on the transaction, validate it.
  561. from erpnext.stock.doctype.putaway_rule.putaway_rule import get_available_putaway_capacity
  562. valid_doctype = self.doctype in (
  563. "Purchase Receipt",
  564. "Stock Entry",
  565. "Purchase Invoice",
  566. "Stock Reconciliation",
  567. )
  568. if self.doctype == "Purchase Invoice" and self.get("update_stock") == 0:
  569. valid_doctype = False
  570. if valid_doctype:
  571. rule_map = defaultdict(dict)
  572. for item in self.get("items"):
  573. warehouse_field = "t_warehouse" if self.doctype == "Stock Entry" else "warehouse"
  574. rule = frappe.db.get_value(
  575. "Putaway Rule",
  576. {"item_code": item.get("item_code"), "warehouse": item.get(warehouse_field)},
  577. ["name", "disable"],
  578. as_dict=True,
  579. )
  580. if rule:
  581. if rule.get("disabled"):
  582. continue # dont validate for disabled rule
  583. if self.doctype == "Stock Reconciliation":
  584. stock_qty = flt(item.qty)
  585. else:
  586. stock_qty = flt(item.transfer_qty) if self.doctype == "Stock Entry" else flt(item.stock_qty)
  587. rule_name = rule.get("name")
  588. if not rule_map[rule_name]:
  589. rule_map[rule_name]["warehouse"] = item.get(warehouse_field)
  590. rule_map[rule_name]["item"] = item.get("item_code")
  591. rule_map[rule_name]["qty_put"] = 0
  592. rule_map[rule_name]["capacity"] = get_available_putaway_capacity(rule_name)
  593. rule_map[rule_name]["qty_put"] += flt(stock_qty)
  594. for rule, values in rule_map.items():
  595. if flt(values["qty_put"]) > flt(values["capacity"]):
  596. message = self.prepare_over_receipt_message(rule, values)
  597. frappe.throw(msg=message, title=_("Over Receipt"))
  598. def prepare_over_receipt_message(self, rule, values):
  599. message = _(
  600. "{0} qty of Item {1} is being received into Warehouse {2} with capacity {3}."
  601. ).format(
  602. frappe.bold(values["qty_put"]),
  603. frappe.bold(values["item"]),
  604. frappe.bold(values["warehouse"]),
  605. frappe.bold(values["capacity"]),
  606. )
  607. message += "<br><br>"
  608. rule_link = frappe.utils.get_link_to_form("Putaway Rule", rule)
  609. message += _("Please adjust the qty or edit {0} to proceed.").format(rule_link)
  610. return message
  611. def repost_future_sle_and_gle(self, force=False):
  612. args = frappe._dict(
  613. {
  614. "posting_date": self.posting_date,
  615. "posting_time": self.posting_time,
  616. "voucher_type": self.doctype,
  617. "voucher_no": self.name,
  618. "company": self.company,
  619. }
  620. )
  621. if force or future_sle_exists(args) or repost_required_for_queue(self):
  622. item_based_reposting = cint(
  623. frappe.db.get_single_value("Stock Reposting Settings", "item_based_reposting")
  624. )
  625. if item_based_reposting:
  626. create_item_wise_repost_entries(voucher_type=self.doctype, voucher_no=self.name)
  627. else:
  628. create_repost_item_valuation_entry(args)
  629. def add_gl_entry(
  630. self,
  631. gl_entries,
  632. account,
  633. cost_center,
  634. debit,
  635. credit,
  636. remarks,
  637. against_account,
  638. debit_in_account_currency=None,
  639. credit_in_account_currency=None,
  640. account_currency=None,
  641. project=None,
  642. voucher_detail_no=None,
  643. item=None,
  644. posting_date=None,
  645. ):
  646. gl_entry = {
  647. "account": account,
  648. "cost_center": cost_center,
  649. "debit": debit,
  650. "credit": credit,
  651. "against": against_account,
  652. "remarks": remarks,
  653. }
  654. if voucher_detail_no:
  655. gl_entry.update({"voucher_detail_no": voucher_detail_no})
  656. if debit_in_account_currency:
  657. gl_entry.update({"debit_in_account_currency": debit_in_account_currency})
  658. if credit_in_account_currency:
  659. gl_entry.update({"credit_in_account_currency": credit_in_account_currency})
  660. if posting_date:
  661. gl_entry.update({"posting_date": posting_date})
  662. gl_entries.append(self.get_gl_dict(gl_entry, item=item))
  663. def repost_required_for_queue(doc: StockController) -> bool:
  664. """check if stock document contains repeated item-warehouse with queue based valuation.
  665. if queue exists for repeated items then SLEs need to reprocessed in background again.
  666. """
  667. consuming_sles = frappe.db.get_all(
  668. "Stock Ledger Entry",
  669. filters={
  670. "voucher_type": doc.doctype,
  671. "voucher_no": doc.name,
  672. "actual_qty": ("<", 0),
  673. "is_cancelled": 0,
  674. },
  675. fields=["item_code", "warehouse", "stock_queue"],
  676. )
  677. item_warehouses = [(sle.item_code, sle.warehouse) for sle in consuming_sles]
  678. unique_item_warehouses = set(item_warehouses)
  679. if len(unique_item_warehouses) == len(item_warehouses):
  680. return False
  681. for sle in consuming_sles:
  682. if sle.stock_queue != "[]": # using FIFO/LIFO valuation
  683. return True
  684. return False
  685. @frappe.whitelist()
  686. def make_quality_inspections(doctype, docname, items):
  687. if isinstance(items, str):
  688. items = json.loads(items)
  689. inspections = []
  690. for item in items:
  691. if flt(item.get("sample_size")) > flt(item.get("qty")):
  692. frappe.throw(
  693. _(
  694. "{item_name}'s Sample Size ({sample_size}) cannot be greater than the Accepted Quantity ({accepted_quantity})"
  695. ).format(
  696. item_name=item.get("item_name"),
  697. sample_size=item.get("sample_size"),
  698. accepted_quantity=item.get("qty"),
  699. )
  700. )
  701. quality_inspection = frappe.get_doc(
  702. {
  703. "doctype": "Quality Inspection",
  704. "inspection_type": "Incoming",
  705. "inspected_by": frappe.session.user,
  706. "reference_type": doctype,
  707. "reference_name": docname,
  708. "item_code": item.get("item_code"),
  709. "description": item.get("description"),
  710. "sample_size": flt(item.get("sample_size")),
  711. "item_serial_no": item.get("serial_no").split("\n")[0] if item.get("serial_no") else None,
  712. "batch_no": item.get("batch_no"),
  713. }
  714. ).insert()
  715. quality_inspection.save()
  716. inspections.append(quality_inspection.name)
  717. return inspections
  718. def is_reposting_pending():
  719. return frappe.db.exists(
  720. "Repost Item Valuation", {"docstatus": 1, "status": ["in", ["Queued", "In Progress"]]}
  721. )
  722. def future_sle_exists(args, sl_entries=None):
  723. key = (args.voucher_type, args.voucher_no)
  724. if not hasattr(frappe.local, "future_sle"):
  725. frappe.local.future_sle = {}
  726. if validate_future_sle_not_exists(args, key, sl_entries):
  727. return False
  728. elif get_cached_data(args, key):
  729. return True
  730. if not sl_entries:
  731. sl_entries = get_sle_entries_against_voucher(args)
  732. if not sl_entries:
  733. return
  734. or_conditions = get_conditions_to_validate_future_sle(sl_entries)
  735. data = frappe.db.sql(
  736. """
  737. select item_code, warehouse, count(name) as total_row
  738. from `tabStock Ledger Entry` force index (item_warehouse)
  739. where
  740. ({})
  741. and timestamp(posting_date, posting_time)
  742. >= timestamp(%(posting_date)s, %(posting_time)s)
  743. and voucher_no != %(voucher_no)s
  744. and is_cancelled = 0
  745. GROUP BY
  746. item_code, warehouse
  747. """.format(
  748. " or ".join(or_conditions)
  749. ),
  750. args,
  751. as_dict=1,
  752. )
  753. for d in data:
  754. frappe.local.future_sle[key][(d.item_code, d.warehouse)] = d.total_row
  755. return len(data)
  756. def validate_future_sle_not_exists(args, key, sl_entries=None):
  757. item_key = ""
  758. if args.get("item_code"):
  759. item_key = (args.get("item_code"), args.get("warehouse"))
  760. if not sl_entries and hasattr(frappe.local, "future_sle"):
  761. if key not in frappe.local.future_sle:
  762. return False
  763. if not frappe.local.future_sle.get(key) or (
  764. item_key and item_key not in frappe.local.future_sle.get(key)
  765. ):
  766. return True
  767. def get_cached_data(args, key):
  768. if key not in frappe.local.future_sle:
  769. frappe.local.future_sle[key] = frappe._dict({})
  770. if args.get("item_code"):
  771. item_key = (args.get("item_code"), args.get("warehouse"))
  772. count = frappe.local.future_sle[key].get(item_key)
  773. return True if (count or count == 0) else False
  774. else:
  775. return frappe.local.future_sle[key]
  776. def get_sle_entries_against_voucher(args):
  777. return frappe.get_all(
  778. "Stock Ledger Entry",
  779. filters={"voucher_type": args.voucher_type, "voucher_no": args.voucher_no},
  780. fields=["item_code", "warehouse"],
  781. order_by="creation asc",
  782. )
  783. def get_conditions_to_validate_future_sle(sl_entries):
  784. warehouse_items_map = {}
  785. for entry in sl_entries:
  786. if entry.warehouse not in warehouse_items_map:
  787. warehouse_items_map[entry.warehouse] = set()
  788. warehouse_items_map[entry.warehouse].add(entry.item_code)
  789. or_conditions = []
  790. for warehouse, items in warehouse_items_map.items():
  791. or_conditions.append(
  792. f"""warehouse = {frappe.db.escape(warehouse)}
  793. and item_code in ({', '.join(frappe.db.escape(item) for item in items)})"""
  794. )
  795. return or_conditions
  796. def create_repost_item_valuation_entry(args):
  797. args = frappe._dict(args)
  798. repost_entry = frappe.new_doc("Repost Item Valuation")
  799. repost_entry.based_on = args.based_on
  800. if not args.based_on:
  801. repost_entry.based_on = "Transaction" if args.voucher_no else "Item and Warehouse"
  802. repost_entry.voucher_type = args.voucher_type
  803. repost_entry.voucher_no = args.voucher_no
  804. repost_entry.item_code = args.item_code
  805. repost_entry.warehouse = args.warehouse
  806. repost_entry.posting_date = args.posting_date
  807. repost_entry.posting_time = args.posting_time
  808. repost_entry.company = args.company
  809. repost_entry.allow_zero_rate = args.allow_zero_rate
  810. repost_entry.flags.ignore_links = True
  811. repost_entry.flags.ignore_permissions = True
  812. repost_entry.save()
  813. repost_entry.submit()
  814. def create_item_wise_repost_entries(voucher_type, voucher_no, allow_zero_rate=False):
  815. """Using a voucher create repost item valuation records for all item-warehouse pairs."""
  816. stock_ledger_entries = get_items_to_be_repost(voucher_type, voucher_no)
  817. distinct_item_warehouses = set()
  818. repost_entries = []
  819. for sle in stock_ledger_entries:
  820. item_wh = (sle.item_code, sle.warehouse)
  821. if item_wh in distinct_item_warehouses:
  822. continue
  823. distinct_item_warehouses.add(item_wh)
  824. repost_entry = frappe.new_doc("Repost Item Valuation")
  825. repost_entry.based_on = "Item and Warehouse"
  826. repost_entry.voucher_type = voucher_type
  827. repost_entry.voucher_no = voucher_no
  828. repost_entry.item_code = sle.item_code
  829. repost_entry.warehouse = sle.warehouse
  830. repost_entry.posting_date = sle.posting_date
  831. repost_entry.posting_time = sle.posting_time
  832. repost_entry.allow_zero_rate = allow_zero_rate
  833. repost_entry.flags.ignore_links = True
  834. repost_entry.flags.ignore_permissions = True
  835. repost_entry.submit()
  836. repost_entries.append(repost_entry)
  837. return repost_entries