您最多选择25个主题 主题必须以字母或数字开头,可以包含连字符 (-),并且长度不得超过35个字符
 
 
 
 

829 行
27 KiB

  1. # Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
  2. # License: GNU General Public License v3. See license.txt
  3. import frappe
  4. from frappe import ValidationError, _, msgprint
  5. from frappe.contacts.doctype.address.address import get_address_display
  6. from frappe.utils import cint, cstr, flt, getdate
  7. from frappe.utils.data import nowtime
  8. from erpnext.accounts.doctype.budget.budget import validate_expense_against_budget
  9. from erpnext.accounts.party import get_party_details
  10. from erpnext.buying.utils import update_last_purchase_rate, validate_for_items
  11. from erpnext.controllers.sales_and_purchase_return import get_rate_for_return
  12. from erpnext.controllers.subcontracting_controller import SubcontractingController
  13. from erpnext.stock.get_item_details import get_conversion_factor
  14. from erpnext.stock.utils import get_incoming_rate
  15. class QtyMismatchError(ValidationError):
  16. pass
  17. class BuyingController(SubcontractingController):
  18. def __setup__(self):
  19. self.flags.ignore_permlevel_for_fields = ["buying_price_list", "price_list_currency"]
  20. def get_feed(self):
  21. if self.get("supplier_name"):
  22. return _("From {0} | {1} {2}").format(self.supplier_name, self.currency, self.grand_total)
  23. def validate(self):
  24. super(BuyingController, self).validate()
  25. if getattr(self, "supplier", None) and not self.supplier_name:
  26. self.supplier_name = frappe.db.get_value("Supplier", self.supplier, "supplier_name")
  27. self.validate_items()
  28. self.set_qty_as_per_stock_uom()
  29. self.validate_stock_or_nonstock_items()
  30. self.validate_warehouse()
  31. self.validate_from_warehouse()
  32. self.set_supplier_address()
  33. self.validate_asset_return()
  34. self.validate_auto_repeat_subscription_dates()
  35. if self.doctype == "Purchase Invoice":
  36. self.validate_purchase_receipt_if_update_stock()
  37. if self.doctype == "Purchase Receipt" or (
  38. self.doctype == "Purchase Invoice" and self.update_stock
  39. ):
  40. # self.validate_purchase_return()
  41. self.validate_rejected_warehouse()
  42. self.validate_accepted_rejected_qty()
  43. validate_for_items(self)
  44. # sub-contracting
  45. self.validate_for_subcontracting()
  46. if self.get("is_old_subcontracting_flow"):
  47. self.create_raw_materials_supplied()
  48. self.set_landed_cost_voucher_amount()
  49. if self.doctype in ("Purchase Receipt", "Purchase Invoice"):
  50. self.update_valuation_rate()
  51. def onload(self):
  52. super(BuyingController, self).onload()
  53. self.set_onload(
  54. "backflush_based_on",
  55. frappe.db.get_single_value(
  56. "Buying Settings", "backflush_raw_materials_of_subcontract_based_on"
  57. ),
  58. )
  59. def set_missing_values(self, for_validate=False):
  60. super(BuyingController, self).set_missing_values(for_validate)
  61. self.set_supplier_from_item_default()
  62. self.set_price_list_currency("Buying")
  63. # set contact and address details for supplier, if they are not mentioned
  64. if getattr(self, "supplier", None):
  65. self.update_if_missing(
  66. get_party_details(
  67. self.supplier,
  68. party_type="Supplier",
  69. doctype=self.doctype,
  70. company=self.company,
  71. party_address=self.get("supplier_address"),
  72. shipping_address=self.get("shipping_address"),
  73. company_address=self.get("billing_address"),
  74. fetch_payment_terms_template=not self.get("ignore_default_payment_terms_template"),
  75. ignore_permissions=self.flags.ignore_permissions,
  76. )
  77. )
  78. self.set_missing_item_details(for_validate)
  79. def set_supplier_from_item_default(self):
  80. if self.meta.get_field("supplier") and not self.supplier:
  81. for d in self.get("items"):
  82. supplier = frappe.db.get_value(
  83. "Item Default", {"parent": d.item_code, "company": self.company}, "default_supplier"
  84. )
  85. if supplier:
  86. self.supplier = supplier
  87. else:
  88. item_group = frappe.db.get_value("Item", d.item_code, "item_group")
  89. supplier = frappe.db.get_value(
  90. "Item Default", {"parent": item_group, "company": self.company}, "default_supplier"
  91. )
  92. if supplier:
  93. self.supplier = supplier
  94. break
  95. def validate_stock_or_nonstock_items(self):
  96. if self.meta.get_field("taxes") and not self.get_stock_items() and not self.get_asset_items():
  97. msg = _('Tax Category has been changed to "Total" because all the Items are non-stock items')
  98. self.update_tax_category(msg)
  99. def update_tax_category(self, msg):
  100. tax_for_valuation = [
  101. d for d in self.get("taxes") if d.category in ["Valuation", "Valuation and Total"]
  102. ]
  103. if tax_for_valuation:
  104. for d in tax_for_valuation:
  105. d.category = "Total"
  106. msgprint(msg)
  107. def validate_asset_return(self):
  108. if self.doctype not in ["Purchase Receipt", "Purchase Invoice"] or not self.is_return:
  109. return
  110. purchase_doc_field = (
  111. "purchase_receipt" if self.doctype == "Purchase Receipt" else "purchase_invoice"
  112. )
  113. not_cancelled_asset = [
  114. d.name
  115. for d in frappe.db.get_all("Asset", {purchase_doc_field: self.return_against, "docstatus": 1})
  116. ]
  117. if self.is_return and len(not_cancelled_asset):
  118. frappe.throw(
  119. _(
  120. "{} has submitted assets linked to it. You need to cancel the assets to create purchase return."
  121. ).format(self.return_against),
  122. title=_("Not Allowed"),
  123. )
  124. def get_asset_items(self):
  125. if self.doctype not in ["Purchase Order", "Purchase Invoice", "Purchase Receipt"]:
  126. return []
  127. return [d.item_code for d in self.items if d.is_fixed_asset]
  128. def set_landed_cost_voucher_amount(self):
  129. for d in self.get("items"):
  130. lc_voucher_data = frappe.db.sql(
  131. """select sum(applicable_charges), cost_center
  132. from `tabLanded Cost Item`
  133. where docstatus = 1 and purchase_receipt_item = %s""",
  134. d.name,
  135. )
  136. d.landed_cost_voucher_amount = lc_voucher_data[0][0] if lc_voucher_data else 0.0
  137. if not d.cost_center and lc_voucher_data and lc_voucher_data[0][1]:
  138. d.db_set("cost_center", lc_voucher_data[0][1])
  139. def validate_from_warehouse(self):
  140. for item in self.get("items"):
  141. if item.get("from_warehouse") and (item.get("from_warehouse") == item.get("warehouse")):
  142. frappe.throw(
  143. _("Row #{0}: Accepted Warehouse and Supplier Warehouse cannot be same").format(item.idx)
  144. )
  145. if item.get("from_warehouse") and self.get("is_subcontracted"):
  146. frappe.throw(
  147. _(
  148. "Row #{0}: Cannot select Supplier Warehouse while suppling raw materials to subcontractor"
  149. ).format(item.idx)
  150. )
  151. def set_supplier_address(self):
  152. address_dict = {
  153. "supplier_address": "address_display",
  154. "shipping_address": "shipping_address_display",
  155. }
  156. for address_field, address_display_field in address_dict.items():
  157. if self.get(address_field):
  158. self.set(address_display_field, get_address_display(self.get(address_field)))
  159. def set_total_in_words(self):
  160. from frappe.utils import money_in_words
  161. if self.meta.get_field("base_in_words"):
  162. if self.meta.get_field("base_rounded_total") and not self.is_rounded_total_disabled():
  163. amount = abs(self.base_rounded_total)
  164. else:
  165. amount = abs(self.base_grand_total)
  166. self.base_in_words = money_in_words(amount, self.company_currency)
  167. if self.meta.get_field("in_words"):
  168. if self.meta.get_field("rounded_total") and not self.is_rounded_total_disabled():
  169. amount = abs(self.rounded_total)
  170. else:
  171. amount = abs(self.grand_total)
  172. self.in_words = money_in_words(amount, self.currency)
  173. # update valuation rate
  174. def update_valuation_rate(self, reset_outgoing_rate=True):
  175. """
  176. item_tax_amount is the total tax amount applied on that item
  177. stored for valuation
  178. TODO: rename item_tax_amount to valuation_tax_amount
  179. """
  180. stock_and_asset_items = []
  181. stock_and_asset_items = self.get_stock_items() + self.get_asset_items()
  182. stock_and_asset_items_qty, stock_and_asset_items_amount = 0, 0
  183. last_item_idx = 1
  184. for d in self.get("items"):
  185. if d.item_code and d.item_code in stock_and_asset_items:
  186. stock_and_asset_items_qty += flt(d.qty)
  187. stock_and_asset_items_amount += flt(d.base_net_amount)
  188. last_item_idx = d.idx
  189. total_valuation_amount = sum(
  190. flt(d.base_tax_amount_after_discount_amount)
  191. for d in self.get("taxes")
  192. if d.category in ["Valuation", "Valuation and Total"]
  193. )
  194. valuation_amount_adjustment = total_valuation_amount
  195. for i, item in enumerate(self.get("items")):
  196. if item.item_code and item.qty and item.item_code in stock_and_asset_items:
  197. item_proportion = (
  198. flt(item.base_net_amount) / stock_and_asset_items_amount
  199. if stock_and_asset_items_amount
  200. else flt(item.qty) / stock_and_asset_items_qty
  201. )
  202. if i == (last_item_idx - 1):
  203. item.item_tax_amount = flt(
  204. valuation_amount_adjustment, self.precision("item_tax_amount", item)
  205. )
  206. else:
  207. item.item_tax_amount = flt(
  208. item_proportion * total_valuation_amount, self.precision("item_tax_amount", item)
  209. )
  210. valuation_amount_adjustment -= item.item_tax_amount
  211. self.round_floats_in(item)
  212. if flt(item.conversion_factor) == 0.0:
  213. item.conversion_factor = (
  214. get_conversion_factor(item.item_code, item.uom).get("conversion_factor") or 1.0
  215. )
  216. qty_in_stock_uom = flt(item.qty * item.conversion_factor)
  217. if self.get("is_old_subcontracting_flow"):
  218. item.rm_supp_cost = self.get_supplied_items_cost(item.name, reset_outgoing_rate)
  219. item.valuation_rate = (
  220. item.base_net_amount
  221. + item.item_tax_amount
  222. + item.rm_supp_cost
  223. + flt(item.landed_cost_voucher_amount)
  224. ) / qty_in_stock_uom
  225. else:
  226. item.valuation_rate = (
  227. item.base_net_amount
  228. + item.item_tax_amount
  229. + flt(item.landed_cost_voucher_amount)
  230. + flt(item.get("rate_difference_with_purchase_invoice"))
  231. ) / qty_in_stock_uom
  232. else:
  233. item.valuation_rate = 0.0
  234. def set_incoming_rate(self):
  235. if self.doctype not in ("Purchase Receipt", "Purchase Invoice", "Purchase Order"):
  236. return
  237. if not self.is_internal_transfer():
  238. return
  239. ref_doctype_map = {
  240. "Purchase Order": "Sales Order Item",
  241. "Purchase Receipt": "Delivery Note Item",
  242. "Purchase Invoice": "Sales Invoice Item",
  243. }
  244. ref_doctype = ref_doctype_map.get(self.doctype)
  245. items = self.get("items")
  246. for d in items:
  247. if not cint(self.get("is_return")):
  248. # Get outgoing rate based on original item cost based on valuation method
  249. if not d.get(frappe.scrub(ref_doctype)):
  250. posting_time = self.get("posting_time")
  251. if not posting_time and self.doctype == "Purchase Order":
  252. posting_time = nowtime()
  253. outgoing_rate = get_incoming_rate(
  254. {
  255. "item_code": d.item_code,
  256. "warehouse": d.get("from_warehouse"),
  257. "posting_date": self.get("posting_date") or self.get("transation_date"),
  258. "posting_time": posting_time,
  259. "qty": -1 * flt(d.get("stock_qty")),
  260. "serial_no": d.get("serial_no"),
  261. "batch_no": d.get("batch_no"),
  262. "company": self.company,
  263. "voucher_type": self.doctype,
  264. "voucher_no": self.name,
  265. "allow_zero_valuation": d.get("allow_zero_valuation"),
  266. },
  267. raise_error_if_no_rate=False,
  268. )
  269. rate = flt(outgoing_rate * (d.conversion_factor or 1), d.precision("rate"))
  270. else:
  271. field = "incoming_rate" if self.get("is_internal_supplier") else "rate"
  272. rate = flt(
  273. frappe.db.get_value(ref_doctype, d.get(frappe.scrub(ref_doctype)), field)
  274. * (d.conversion_factor or 1),
  275. d.precision("rate"),
  276. )
  277. if self.is_internal_transfer():
  278. if self.doctype == "Purchase Receipt" or self.get("update_stock"):
  279. if rate != d.rate:
  280. d.rate = rate
  281. frappe.msgprint(
  282. _(
  283. "Row {0}: Item rate has been updated as per valuation rate since its an internal stock transfer"
  284. ).format(d.idx),
  285. alert=1,
  286. )
  287. d.discount_percentage = 0.0
  288. d.discount_amount = 0.0
  289. d.margin_rate_or_amount = 0.0
  290. def validate_for_subcontracting(self):
  291. if self.is_subcontracted and self.get("is_old_subcontracting_flow"):
  292. if self.doctype in ["Purchase Receipt", "Purchase Invoice"] and not self.supplier_warehouse:
  293. frappe.throw(_("Supplier Warehouse mandatory for sub-contracted {0}").format(self.doctype))
  294. for item in self.get("items"):
  295. if item in self.sub_contracted_items and not item.bom:
  296. frappe.throw(_("Please select BOM in BOM field for Item {0}").format(item.item_code))
  297. if self.doctype != "Purchase Order":
  298. return
  299. for row in self.get("supplied_items"):
  300. if not row.reserve_warehouse:
  301. msg = f"Reserved Warehouse is mandatory for the Item {frappe.bold(row.rm_item_code)} in Raw Materials supplied"
  302. frappe.throw(_(msg))
  303. else:
  304. for item in self.get("items"):
  305. if item.get("bom"):
  306. item.bom = None
  307. def set_qty_as_per_stock_uom(self):
  308. for d in self.get("items"):
  309. if d.meta.get_field("stock_qty"):
  310. # Check if item code is present
  311. # Conversion factor should not be mandatory for non itemized items
  312. if not d.conversion_factor and d.item_code:
  313. frappe.throw(_("Row {0}: Conversion Factor is mandatory").format(d.idx))
  314. d.stock_qty = flt(d.qty) * flt(d.conversion_factor)
  315. if self.doctype == "Purchase Receipt" and d.meta.get_field("received_stock_qty"):
  316. # Set Received Qty in Stock UOM
  317. d.received_stock_qty = flt(d.received_qty) * flt(
  318. d.conversion_factor, d.precision("conversion_factor")
  319. )
  320. def validate_purchase_return(self):
  321. for d in self.get("items"):
  322. if self.is_return and flt(d.rejected_qty) != 0:
  323. frappe.throw(_("Row #{0}: Rejected Qty can not be entered in Purchase Return").format(d.idx))
  324. # validate rate with ref PR
  325. def validate_rejected_warehouse(self):
  326. for d in self.get("items"):
  327. if flt(d.rejected_qty) and not d.rejected_warehouse:
  328. if self.rejected_warehouse:
  329. d.rejected_warehouse = self.rejected_warehouse
  330. if not d.rejected_warehouse:
  331. frappe.throw(
  332. _("Row #{0}: Rejected Warehouse is mandatory against rejected Item {1}").format(
  333. d.idx, d.item_code
  334. )
  335. )
  336. # validate accepted and rejected qty
  337. def validate_accepted_rejected_qty(self):
  338. for d in self.get("items"):
  339. self.validate_negative_quantity(d, ["received_qty", "qty", "rejected_qty"])
  340. if not flt(d.received_qty) and (flt(d.qty) or flt(d.rejected_qty)):
  341. d.received_qty = flt(d.qty) + flt(d.rejected_qty)
  342. # Check Received Qty = Accepted Qty + Rejected Qty
  343. val = flt(d.qty) + flt(d.rejected_qty)
  344. if flt(val, d.precision("received_qty")) != flt(d.received_qty, d.precision("received_qty")):
  345. message = _(
  346. "Row #{0}: Received Qty must be equal to Accepted + Rejected Qty for Item {1}"
  347. ).format(d.idx, d.item_code)
  348. frappe.throw(msg=message, title=_("Mismatch"), exc=QtyMismatchError)
  349. def validate_negative_quantity(self, item_row, field_list):
  350. if self.is_return:
  351. return
  352. item_row = item_row.as_dict()
  353. for fieldname in field_list:
  354. if flt(item_row[fieldname]) < 0:
  355. frappe.throw(
  356. _("Row #{0}: {1} can not be negative for item {2}").format(
  357. item_row["idx"],
  358. frappe.get_meta(item_row.doctype).get_label(fieldname),
  359. item_row["item_code"],
  360. )
  361. )
  362. def check_for_on_hold_or_closed_status(self, ref_doctype, ref_fieldname):
  363. for d in self.get("items"):
  364. if d.get(ref_fieldname):
  365. status = frappe.db.get_value(ref_doctype, d.get(ref_fieldname), "status")
  366. if status in ("Closed", "On Hold"):
  367. frappe.throw(_("{0} {1} is {2}").format(ref_doctype, d.get(ref_fieldname), status))
  368. def update_stock_ledger(self, allow_negative_stock=False, via_landed_cost_voucher=False):
  369. self.update_ordered_and_reserved_qty()
  370. sl_entries = []
  371. stock_items = self.get_stock_items()
  372. for d in self.get("items"):
  373. if d.item_code not in stock_items:
  374. continue
  375. if d.warehouse:
  376. pr_qty = flt(d.qty) * flt(d.conversion_factor)
  377. if pr_qty:
  378. if d.from_warehouse and (
  379. (not cint(self.is_return) and self.docstatus == 1)
  380. or (cint(self.is_return) and self.docstatus == 2)
  381. ):
  382. from_warehouse_sle = self.get_sl_entries(
  383. d,
  384. {
  385. "actual_qty": -1 * pr_qty,
  386. "warehouse": d.from_warehouse,
  387. "outgoing_rate": d.rate,
  388. "recalculate_rate": 1,
  389. "dependant_sle_voucher_detail_no": d.name,
  390. },
  391. )
  392. sl_entries.append(from_warehouse_sle)
  393. sle = self.get_sl_entries(
  394. d, {"actual_qty": flt(pr_qty), "serial_no": cstr(d.serial_no).strip()}
  395. )
  396. if self.is_return:
  397. outgoing_rate = get_rate_for_return(
  398. self.doctype, self.name, d.item_code, self.return_against, item_row=d
  399. )
  400. sle.update({"outgoing_rate": outgoing_rate, "recalculate_rate": 1})
  401. if d.from_warehouse:
  402. sle.dependant_sle_voucher_detail_no = d.name
  403. else:
  404. val_rate_db_precision = 6 if cint(self.precision("valuation_rate", d)) <= 6 else 9
  405. incoming_rate = flt(d.valuation_rate, val_rate_db_precision)
  406. sle.update(
  407. {
  408. "incoming_rate": incoming_rate,
  409. "recalculate_rate": 1
  410. if (self.is_subcontracted and (d.bom or d.fg_item)) or d.from_warehouse
  411. else 0,
  412. }
  413. )
  414. sl_entries.append(sle)
  415. if d.from_warehouse and (
  416. (not cint(self.is_return) and self.docstatus == 2)
  417. or (cint(self.is_return) and self.docstatus == 1)
  418. ):
  419. from_warehouse_sle = self.get_sl_entries(
  420. d, {"actual_qty": -1 * pr_qty, "warehouse": d.from_warehouse, "recalculate_rate": 1}
  421. )
  422. sl_entries.append(from_warehouse_sle)
  423. if flt(d.rejected_qty) != 0:
  424. sl_entries.append(
  425. self.get_sl_entries(
  426. d,
  427. {
  428. "warehouse": d.rejected_warehouse,
  429. "actual_qty": flt(d.rejected_qty) * flt(d.conversion_factor),
  430. "serial_no": cstr(d.rejected_serial_no).strip(),
  431. "incoming_rate": 0.0,
  432. },
  433. )
  434. )
  435. if self.get("is_old_subcontracting_flow"):
  436. self.make_sl_entries_for_supplier_warehouse(sl_entries)
  437. self.make_sl_entries(
  438. sl_entries,
  439. allow_negative_stock=allow_negative_stock,
  440. via_landed_cost_voucher=via_landed_cost_voucher,
  441. )
  442. def update_ordered_and_reserved_qty(self):
  443. po_map = {}
  444. for d in self.get("items"):
  445. if self.doctype == "Purchase Receipt" and d.purchase_order:
  446. po_map.setdefault(d.purchase_order, []).append(d.purchase_order_item)
  447. elif self.doctype == "Purchase Invoice" and d.purchase_order and d.po_detail:
  448. po_map.setdefault(d.purchase_order, []).append(d.po_detail)
  449. for po, po_item_rows in po_map.items():
  450. if po and po_item_rows:
  451. po_obj = frappe.get_doc("Purchase Order", po)
  452. if po_obj.status in ["Closed", "Cancelled"]:
  453. frappe.throw(
  454. _("{0} {1} is cancelled or closed").format(_("Purchase Order"), po),
  455. frappe.InvalidStatusError,
  456. )
  457. po_obj.update_ordered_qty(po_item_rows)
  458. if self.get("is_old_subcontracting_flow"):
  459. po_obj.update_reserved_qty_for_subcontract()
  460. def on_submit(self):
  461. if self.get("is_return"):
  462. return
  463. if self.doctype in ["Purchase Receipt", "Purchase Invoice"]:
  464. field = "purchase_invoice" if self.doctype == "Purchase Invoice" else "purchase_receipt"
  465. self.process_fixed_asset()
  466. self.update_fixed_asset(field)
  467. if self.doctype in ["Purchase Order", "Purchase Receipt"] and not frappe.db.get_single_value(
  468. "Buying Settings", "disable_last_purchase_rate"
  469. ):
  470. update_last_purchase_rate(self, is_submit=1)
  471. def on_cancel(self):
  472. super(BuyingController, self).on_cancel()
  473. if self.get("is_return"):
  474. return
  475. if self.doctype in ["Purchase Order", "Purchase Receipt"] and not frappe.db.get_single_value(
  476. "Buying Settings", "disable_last_purchase_rate"
  477. ):
  478. update_last_purchase_rate(self, is_submit=0)
  479. if self.doctype in ["Purchase Receipt", "Purchase Invoice"]:
  480. field = "purchase_invoice" if self.doctype == "Purchase Invoice" else "purchase_receipt"
  481. self.delete_linked_asset()
  482. self.update_fixed_asset(field, delete_asset=True)
  483. def validate_budget(self):
  484. if self.docstatus == 1:
  485. for data in self.get("items"):
  486. args = data.as_dict()
  487. args.update(
  488. {
  489. "doctype": self.doctype,
  490. "company": self.company,
  491. "posting_date": (
  492. self.schedule_date if self.doctype == "Material Request" else self.transaction_date
  493. ),
  494. }
  495. )
  496. validate_expense_against_budget(args)
  497. def process_fixed_asset(self):
  498. if self.doctype == "Purchase Invoice" and not self.update_stock:
  499. return
  500. asset_items = self.get_asset_items()
  501. if asset_items:
  502. self.auto_make_assets(asset_items)
  503. def auto_make_assets(self, asset_items):
  504. items_data = get_asset_item_details(asset_items)
  505. messages = []
  506. for d in self.items:
  507. if d.is_fixed_asset:
  508. item_data = items_data.get(d.item_code)
  509. if item_data.get("auto_create_assets"):
  510. # If asset has to be auto created
  511. # Check for asset naming series
  512. if item_data.get("asset_naming_series"):
  513. created_assets = []
  514. if item_data.get("is_grouped_asset"):
  515. asset = self.make_asset(d, is_grouped_asset=True)
  516. created_assets.append(asset)
  517. else:
  518. for qty in range(cint(d.qty)):
  519. asset = self.make_asset(d)
  520. created_assets.append(asset)
  521. if len(created_assets) > 5:
  522. # dont show asset form links if more than 5 assets are created
  523. messages.append(
  524. _("{} Assets created for {}").format(len(created_assets), frappe.bold(d.item_code))
  525. )
  526. else:
  527. assets_link = list(map(lambda d: frappe.utils.get_link_to_form("Asset", d), created_assets))
  528. assets_link = frappe.bold(",".join(assets_link))
  529. is_plural = "s" if len(created_assets) != 1 else ""
  530. messages.append(
  531. _("Asset{} {assets_link} created for {}").format(
  532. is_plural, frappe.bold(d.item_code), assets_link=assets_link
  533. )
  534. )
  535. else:
  536. frappe.throw(
  537. _("Row {}: Asset Naming Series is mandatory for the auto creation for item {}").format(
  538. d.idx, frappe.bold(d.item_code)
  539. )
  540. )
  541. else:
  542. messages.append(
  543. _("Assets not created for {0}. You will have to create asset manually.").format(
  544. frappe.bold(d.item_code)
  545. )
  546. )
  547. for message in messages:
  548. frappe.msgprint(message, title="Success", indicator="green")
  549. def make_asset(self, row, is_grouped_asset=False):
  550. if not row.asset_location:
  551. frappe.throw(_("Row {0}: Enter location for the asset item {1}").format(row.idx, row.item_code))
  552. item_data = frappe.db.get_value(
  553. "Item", row.item_code, ["asset_naming_series", "asset_category"], as_dict=1
  554. )
  555. if is_grouped_asset:
  556. purchase_amount = flt(row.base_amount + row.item_tax_amount)
  557. else:
  558. purchase_amount = flt(row.base_rate + row.item_tax_amount)
  559. asset = frappe.get_doc(
  560. {
  561. "doctype": "Asset",
  562. "item_code": row.item_code,
  563. "asset_name": row.item_name,
  564. "naming_series": item_data.get("asset_naming_series") or "AST",
  565. "asset_category": item_data.get("asset_category"),
  566. "location": row.asset_location,
  567. "company": self.company,
  568. "supplier": self.supplier,
  569. "purchase_date": self.posting_date,
  570. "calculate_depreciation": 1,
  571. "purchase_receipt_amount": purchase_amount,
  572. "gross_purchase_amount": purchase_amount,
  573. "asset_quantity": row.qty if is_grouped_asset else 0,
  574. "purchase_receipt": self.name if self.doctype == "Purchase Receipt" else None,
  575. "purchase_invoice": self.name if self.doctype == "Purchase Invoice" else None,
  576. }
  577. )
  578. asset.flags.ignore_validate = True
  579. asset.flags.ignore_mandatory = True
  580. asset.set_missing_values()
  581. asset.insert()
  582. return asset.name
  583. def update_fixed_asset(self, field, delete_asset=False):
  584. for d in self.get("items"):
  585. if d.is_fixed_asset:
  586. is_auto_create_enabled = frappe.db.get_value("Item", d.item_code, "auto_create_assets")
  587. assets = frappe.db.get_all("Asset", filters={field: self.name, "item_code": d.item_code})
  588. for asset in assets:
  589. asset = frappe.get_doc("Asset", asset.name)
  590. if delete_asset and is_auto_create_enabled:
  591. # need to delete movements to delete assets otherwise throws link exists error
  592. movements = frappe.db.sql(
  593. """SELECT asm.name
  594. FROM `tabAsset Movement` asm, `tabAsset Movement Item` asm_item
  595. WHERE asm_item.parent=asm.name and asm_item.asset=%s""",
  596. asset.name,
  597. as_dict=1,
  598. )
  599. for movement in movements:
  600. frappe.delete_doc("Asset Movement", movement.name, force=1)
  601. frappe.delete_doc("Asset", asset.name, force=1)
  602. continue
  603. if self.docstatus in [0, 1] and not asset.get(field):
  604. asset.set(field, self.name)
  605. asset.purchase_date = self.posting_date
  606. asset.supplier = self.supplier
  607. elif self.docstatus == 2:
  608. if asset.docstatus == 2:
  609. continue
  610. if asset.docstatus == 0:
  611. asset.set(field, None)
  612. asset.supplier = None
  613. if asset.docstatus == 1 and delete_asset:
  614. frappe.throw(
  615. _(
  616. "Cannot cancel this document as it is linked with submitted asset {0}. Please cancel it to continue."
  617. ).format(frappe.utils.get_link_to_form("Asset", asset.name))
  618. )
  619. asset.flags.ignore_validate_update_after_submit = True
  620. asset.flags.ignore_mandatory = True
  621. if asset.docstatus == 0:
  622. asset.flags.ignore_validate = True
  623. asset.save()
  624. def delete_linked_asset(self):
  625. if self.doctype == "Purchase Invoice" and not self.get("update_stock"):
  626. return
  627. frappe.db.sql("delete from `tabAsset Movement` where reference_name=%s", self.name)
  628. def validate_schedule_date(self):
  629. if not self.get("items"):
  630. return
  631. if any(d.schedule_date for d in self.get("items")):
  632. # Select earliest schedule_date.
  633. self.schedule_date = min(
  634. d.schedule_date for d in self.get("items") if d.schedule_date is not None
  635. )
  636. if self.schedule_date:
  637. for d in self.get("items"):
  638. if not d.schedule_date:
  639. d.schedule_date = self.schedule_date
  640. if (
  641. d.schedule_date
  642. and self.transaction_date
  643. and getdate(d.schedule_date) < getdate(self.transaction_date)
  644. ):
  645. frappe.throw(_("Row #{0}: Reqd by Date cannot be before Transaction Date").format(d.idx))
  646. else:
  647. frappe.throw(_("Please enter Reqd by Date"))
  648. def validate_items(self):
  649. # validate items to see if they have is_purchase_item or is_subcontracted_item enabled
  650. if self.doctype == "Material Request":
  651. return
  652. if self.get("is_old_subcontracting_flow"):
  653. validate_item_type(self, "is_sub_contracted_item", "subcontracted")
  654. else:
  655. validate_item_type(self, "is_purchase_item", "purchase")
  656. def get_asset_item_details(asset_items):
  657. asset_items_data = {}
  658. for d in frappe.get_all(
  659. "Item",
  660. fields=["name", "auto_create_assets", "asset_naming_series", "is_grouped_asset"],
  661. filters={"name": ("in", asset_items)},
  662. ):
  663. asset_items_data.setdefault(d.name, d)
  664. return asset_items_data
  665. def validate_item_type(doc, fieldname, message):
  666. # iterate through items and check if they are valid sales or purchase items
  667. items = [d.item_code for d in doc.items if d.item_code]
  668. # No validation check inase of creating transaction using 'Opening Invoice Creation Tool'
  669. if not items:
  670. return
  671. item_list = ", ".join(["%s" % frappe.db.escape(d) for d in items])
  672. invalid_items = [
  673. d[0]
  674. for d in frappe.db.sql(
  675. """
  676. select item_code from tabItem where name in ({0}) and {1}=0
  677. """.format(
  678. item_list, fieldname
  679. ),
  680. as_list=True,
  681. )
  682. ]
  683. if invalid_items:
  684. items = ", ".join([d for d in invalid_items])
  685. if len(invalid_items) > 1:
  686. error_message = _(
  687. "Following items {0} are not marked as {1} item. You can enable them as {1} item from its Item master"
  688. ).format(items, message)
  689. else:
  690. error_message = _(
  691. "Following item {0} is not marked as {1} item. You can enable them as {1} item from its Item master"
  692. ).format(items, message)
  693. frappe.throw(error_message)