Nelze vybrat více než 25 témat Téma musí začínat písmenem nebo číslem, může obsahovat pomlčky („-“) a může být dlouhé až 35 znaků.
 
 
 
 

1749 řádky
52 KiB

  1. # Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
  2. # License: GNU General Public License v3. See license.txt
  3. from json import loads
  4. from typing import TYPE_CHECKING, List, Optional, Tuple
  5. import frappe
  6. import frappe.defaults
  7. from frappe import _, qb, throw
  8. from frappe.model.meta import get_field_precision
  9. from frappe.query_builder import AliasedQuery, Criterion, Table
  10. from frappe.query_builder.functions import Sum
  11. from frappe.query_builder.utils import DocType
  12. from frappe.utils import (
  13. cint,
  14. create_batch,
  15. cstr,
  16. flt,
  17. formatdate,
  18. get_number_format_info,
  19. getdate,
  20. now,
  21. nowdate,
  22. )
  23. from pypika import Order
  24. from pypika.terms import ExistsCriterion
  25. import erpnext
  26. # imported to enable erpnext.accounts.utils.get_account_currency
  27. from erpnext.accounts.doctype.account.account import get_account_currency # noqa
  28. from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import get_dimensions
  29. from erpnext.stock import get_warehouse_account_map
  30. from erpnext.stock.utils import get_stock_value_on
  31. if TYPE_CHECKING:
  32. from erpnext.stock.doctype.repost_item_valuation.repost_item_valuation import RepostItemValuation
  33. class FiscalYearError(frappe.ValidationError):
  34. pass
  35. class PaymentEntryUnlinkError(frappe.ValidationError):
  36. pass
  37. GL_REPOSTING_CHUNK = 100
  38. @frappe.whitelist()
  39. def get_fiscal_year(
  40. date=None, fiscal_year=None, label="Date", verbose=1, company=None, as_dict=False
  41. ):
  42. return get_fiscal_years(date, fiscal_year, label, verbose, company, as_dict=as_dict)[0]
  43. def get_fiscal_years(
  44. transaction_date=None, fiscal_year=None, label="Date", verbose=1, company=None, as_dict=False
  45. ):
  46. fiscal_years = frappe.cache().hget("fiscal_years", company) or []
  47. if not fiscal_years:
  48. # if year start date is 2012-04-01, year end date should be 2013-03-31 (hence subdate)
  49. FY = DocType("Fiscal Year")
  50. query = (
  51. frappe.qb.from_(FY)
  52. .select(FY.name, FY.year_start_date, FY.year_end_date)
  53. .where(FY.disabled == 0)
  54. )
  55. if fiscal_year:
  56. query = query.where(FY.name == fiscal_year)
  57. if company:
  58. FYC = DocType("Fiscal Year Company")
  59. query = query.where(
  60. ExistsCriterion(frappe.qb.from_(FYC).select(FYC.name).where(FYC.parent == FY.name)).negate()
  61. | ExistsCriterion(
  62. frappe.qb.from_(FYC)
  63. .select(FYC.company)
  64. .where(FYC.parent == FY.name)
  65. .where(FYC.company == company)
  66. )
  67. )
  68. query = query.orderby(FY.year_start_date, order=Order.desc)
  69. fiscal_years = query.run(as_dict=True)
  70. frappe.cache().hset("fiscal_years", company, fiscal_years)
  71. if not transaction_date and not fiscal_year:
  72. return fiscal_years
  73. if transaction_date:
  74. transaction_date = getdate(transaction_date)
  75. for fy in fiscal_years:
  76. matched = False
  77. if fiscal_year and fy.name == fiscal_year:
  78. matched = True
  79. if (
  80. transaction_date
  81. and getdate(fy.year_start_date) <= transaction_date
  82. and getdate(fy.year_end_date) >= transaction_date
  83. ):
  84. matched = True
  85. if matched:
  86. if as_dict:
  87. return (fy,)
  88. else:
  89. return ((fy.name, fy.year_start_date, fy.year_end_date),)
  90. error_msg = _("""{0} {1} is not in any active Fiscal Year""").format(
  91. label, formatdate(transaction_date)
  92. )
  93. if company:
  94. error_msg = _("""{0} for {1}""").format(error_msg, frappe.bold(company))
  95. if verbose == 1:
  96. frappe.msgprint(error_msg)
  97. raise FiscalYearError(error_msg)
  98. @frappe.whitelist()
  99. def get_fiscal_year_filter_field(company=None):
  100. field = {"fieldtype": "Select", "options": [], "operator": "Between", "query_value": True}
  101. fiscal_years = get_fiscal_years(company=company)
  102. for fiscal_year in fiscal_years:
  103. field["options"].append(
  104. {
  105. "label": fiscal_year.name,
  106. "value": fiscal_year.name,
  107. "query_value": [
  108. fiscal_year.year_start_date.strftime("%Y-%m-%d"),
  109. fiscal_year.year_end_date.strftime("%Y-%m-%d"),
  110. ],
  111. }
  112. )
  113. return field
  114. def validate_fiscal_year(date, fiscal_year, company, label="Date", doc=None):
  115. years = [f[0] for f in get_fiscal_years(date, label=_(label), company=company)]
  116. if fiscal_year not in years:
  117. if doc:
  118. doc.fiscal_year = years[0]
  119. else:
  120. throw(_("{0} '{1}' not in Fiscal Year {2}").format(label, formatdate(date), fiscal_year))
  121. @frappe.whitelist()
  122. def get_balance_on(
  123. account=None,
  124. date=None,
  125. party_type=None,
  126. party=None,
  127. company=None,
  128. in_account_currency=True,
  129. cost_center=None,
  130. ignore_account_permission=False,
  131. ):
  132. if not account and frappe.form_dict.get("account"):
  133. account = frappe.form_dict.get("account")
  134. if not date and frappe.form_dict.get("date"):
  135. date = frappe.form_dict.get("date")
  136. if not party_type and frappe.form_dict.get("party_type"):
  137. party_type = frappe.form_dict.get("party_type")
  138. if not party and frappe.form_dict.get("party"):
  139. party = frappe.form_dict.get("party")
  140. if not cost_center and frappe.form_dict.get("cost_center"):
  141. cost_center = frappe.form_dict.get("cost_center")
  142. cond = ["is_cancelled=0"]
  143. if date:
  144. cond.append("posting_date <= %s" % frappe.db.escape(cstr(date)))
  145. else:
  146. # get balance of all entries that exist
  147. date = nowdate()
  148. if account:
  149. acc = frappe.get_doc("Account", account)
  150. try:
  151. year_start_date = get_fiscal_year(date, company=company, verbose=0)[1]
  152. except FiscalYearError:
  153. if getdate(date) > getdate(nowdate()):
  154. # if fiscal year not found and the date is greater than today
  155. # get fiscal year for today's date and its corresponding year start date
  156. year_start_date = get_fiscal_year(nowdate(), verbose=1)[1]
  157. else:
  158. # this indicates that it is a date older than any existing fiscal year.
  159. # hence, assuming balance as 0.0
  160. return 0.0
  161. if account:
  162. report_type = acc.report_type
  163. else:
  164. report_type = ""
  165. if cost_center and report_type == "Profit and Loss":
  166. cc = frappe.get_doc("Cost Center", cost_center)
  167. if cc.is_group:
  168. cond.append(
  169. """ exists (
  170. select 1 from `tabCost Center` cc where cc.name = gle.cost_center
  171. and cc.lft >= %s and cc.rgt <= %s
  172. )"""
  173. % (cc.lft, cc.rgt)
  174. )
  175. else:
  176. cond.append("""gle.cost_center = %s """ % (frappe.db.escape(cost_center, percent=False),))
  177. if account:
  178. if not (frappe.flags.ignore_account_permission or ignore_account_permission):
  179. acc.check_permission("read")
  180. if report_type == "Profit and Loss":
  181. # for pl accounts, get balance within a fiscal year
  182. cond.append(
  183. "posting_date >= '%s' and voucher_type != 'Period Closing Voucher'" % year_start_date
  184. )
  185. # different filter for group and ledger - improved performance
  186. if acc.is_group:
  187. cond.append(
  188. """exists (
  189. select name from `tabAccount` ac where ac.name = gle.account
  190. and ac.lft >= %s and ac.rgt <= %s
  191. )"""
  192. % (acc.lft, acc.rgt)
  193. )
  194. # If group and currency same as company,
  195. # always return balance based on debit and credit in company currency
  196. if acc.account_currency == frappe.get_cached_value("Company", acc.company, "default_currency"):
  197. in_account_currency = False
  198. else:
  199. cond.append("""gle.account = %s """ % (frappe.db.escape(account, percent=False),))
  200. if party_type and party:
  201. cond.append(
  202. """gle.party_type = %s and gle.party = %s """
  203. % (frappe.db.escape(party_type), frappe.db.escape(party, percent=False))
  204. )
  205. if company:
  206. cond.append("""gle.company = %s """ % (frappe.db.escape(company, percent=False)))
  207. if account or (party_type and party):
  208. if in_account_currency:
  209. select_field = "sum(debit_in_account_currency) - sum(credit_in_account_currency)"
  210. else:
  211. select_field = "sum(debit) - sum(credit)"
  212. bal = frappe.db.sql(
  213. """
  214. SELECT {0}
  215. FROM `tabGL Entry` gle
  216. WHERE {1}""".format(
  217. select_field, " and ".join(cond)
  218. )
  219. )[0][0]
  220. # if bal is None, return 0
  221. return flt(bal)
  222. def get_count_on(account, fieldname, date):
  223. cond = ["is_cancelled=0"]
  224. if date:
  225. cond.append("posting_date <= %s" % frappe.db.escape(cstr(date)))
  226. else:
  227. # get balance of all entries that exist
  228. date = nowdate()
  229. try:
  230. year_start_date = get_fiscal_year(date, verbose=0)[1]
  231. except FiscalYearError:
  232. if getdate(date) > getdate(nowdate()):
  233. # if fiscal year not found and the date is greater than today
  234. # get fiscal year for today's date and its corresponding year start date
  235. year_start_date = get_fiscal_year(nowdate(), verbose=1)[1]
  236. else:
  237. # this indicates that it is a date older than any existing fiscal year.
  238. # hence, assuming balance as 0.0
  239. return 0.0
  240. if account:
  241. acc = frappe.get_doc("Account", account)
  242. if not frappe.flags.ignore_account_permission:
  243. acc.check_permission("read")
  244. # for pl accounts, get balance within a fiscal year
  245. if acc.report_type == "Profit and Loss":
  246. cond.append(
  247. "posting_date >= '%s' and voucher_type != 'Period Closing Voucher'" % year_start_date
  248. )
  249. # different filter for group and ledger - improved performance
  250. if acc.is_group:
  251. cond.append(
  252. """exists (
  253. select name from `tabAccount` ac where ac.name = gle.account
  254. and ac.lft >= %s and ac.rgt <= %s
  255. )"""
  256. % (acc.lft, acc.rgt)
  257. )
  258. else:
  259. cond.append("""gle.account = %s """ % (frappe.db.escape(account, percent=False),))
  260. entries = frappe.db.sql(
  261. """
  262. SELECT name, posting_date, account, party_type, party,debit,credit,
  263. voucher_type, voucher_no, against_voucher_type, against_voucher
  264. FROM `tabGL Entry` gle
  265. WHERE {0}""".format(
  266. " and ".join(cond)
  267. ),
  268. as_dict=True,
  269. )
  270. count = 0
  271. for gle in entries:
  272. if fieldname not in ("invoiced_amount", "payables"):
  273. count += 1
  274. else:
  275. dr_or_cr = "debit" if fieldname == "invoiced_amount" else "credit"
  276. cr_or_dr = "credit" if fieldname == "invoiced_amount" else "debit"
  277. select_fields = (
  278. "ifnull(sum(credit-debit),0)"
  279. if fieldname == "invoiced_amount"
  280. else "ifnull(sum(debit-credit),0)"
  281. )
  282. if (
  283. (not gle.against_voucher)
  284. or (gle.against_voucher_type in ["Sales Order", "Purchase Order"])
  285. or (gle.against_voucher == gle.voucher_no and gle.get(dr_or_cr) > 0)
  286. ):
  287. payment_amount = frappe.db.sql(
  288. """
  289. SELECT {0}
  290. FROM `tabGL Entry` gle
  291. WHERE docstatus < 2 and posting_date <= %(date)s and against_voucher = %(voucher_no)s
  292. and party = %(party)s and name != %(name)s""".format(
  293. select_fields
  294. ),
  295. {"date": date, "voucher_no": gle.voucher_no, "party": gle.party, "name": gle.name},
  296. )[0][0]
  297. outstanding_amount = flt(gle.get(dr_or_cr)) - flt(gle.get(cr_or_dr)) - payment_amount
  298. currency_precision = get_currency_precision() or 2
  299. if abs(flt(outstanding_amount)) > 0.1 / 10**currency_precision:
  300. count += 1
  301. return count
  302. @frappe.whitelist()
  303. def add_ac(args=None):
  304. from frappe.desk.treeview import make_tree_args
  305. if not args:
  306. args = frappe.local.form_dict
  307. args.doctype = "Account"
  308. args = make_tree_args(**args)
  309. ac = frappe.new_doc("Account")
  310. if args.get("ignore_permissions"):
  311. ac.flags.ignore_permissions = True
  312. args.pop("ignore_permissions")
  313. ac.update(args)
  314. if not ac.parent_account:
  315. ac.parent_account = args.get("parent")
  316. ac.old_parent = ""
  317. ac.freeze_account = "No"
  318. if cint(ac.get("is_root")):
  319. ac.parent_account = None
  320. ac.flags.ignore_mandatory = True
  321. ac.insert()
  322. return ac.name
  323. @frappe.whitelist()
  324. def add_cc(args=None):
  325. from frappe.desk.treeview import make_tree_args
  326. if not args:
  327. args = frappe.local.form_dict
  328. args.doctype = "Cost Center"
  329. args = make_tree_args(**args)
  330. if args.parent_cost_center == args.company:
  331. args.parent_cost_center = "{0} - {1}".format(
  332. args.parent_cost_center, frappe.get_cached_value("Company", args.company, "abbr")
  333. )
  334. cc = frappe.new_doc("Cost Center")
  335. cc.update(args)
  336. if not cc.parent_cost_center:
  337. cc.parent_cost_center = args.get("parent")
  338. cc.old_parent = ""
  339. cc.insert()
  340. return cc.name
  341. def reconcile_against_document(args, skip_ref_details_update_for_pe=False): # nosemgrep
  342. """
  343. Cancel PE or JV, Update against document, split if required and resubmit
  344. """
  345. # To optimize making GL Entry for PE or JV with multiple references
  346. reconciled_entries = {}
  347. for row in args:
  348. if not reconciled_entries.get((row.voucher_type, row.voucher_no)):
  349. reconciled_entries[(row.voucher_type, row.voucher_no)] = []
  350. reconciled_entries[(row.voucher_type, row.voucher_no)].append(row)
  351. for key, entries in reconciled_entries.items():
  352. voucher_type = key[0]
  353. voucher_no = key[1]
  354. # cancel advance entry
  355. doc = frappe.get_doc(voucher_type, voucher_no)
  356. frappe.flags.ignore_party_validation = True
  357. _delete_pl_entries(voucher_type, voucher_no)
  358. for entry in entries:
  359. check_if_advance_entry_modified(entry)
  360. validate_allocated_amount(entry)
  361. # update ref in advance entry
  362. if voucher_type == "Journal Entry":
  363. update_reference_in_journal_entry(entry, doc, do_not_save=True)
  364. else:
  365. update_reference_in_payment_entry(
  366. entry, doc, do_not_save=True, skip_ref_details_update_for_pe=skip_ref_details_update_for_pe
  367. )
  368. doc.save(ignore_permissions=True)
  369. # re-submit advance entry
  370. doc = frappe.get_doc(entry.voucher_type, entry.voucher_no)
  371. gl_map = doc.build_gl_map()
  372. create_payment_ledger_entry(gl_map, update_outstanding="No", cancel=0, adv_adj=1)
  373. # Only update outstanding for newly linked vouchers
  374. for entry in entries:
  375. update_voucher_outstanding(
  376. entry.against_voucher_type, entry.against_voucher, entry.account, entry.party_type, entry.party
  377. )
  378. frappe.flags.ignore_party_validation = False
  379. def check_if_advance_entry_modified(args):
  380. """
  381. check if there is already a voucher reference
  382. check if amount is same
  383. check if jv is submitted
  384. """
  385. if not args.get("unreconciled_amount"):
  386. args.update({"unreconciled_amount": args.get("unadjusted_amount")})
  387. ret = None
  388. if args.voucher_type == "Journal Entry":
  389. ret = frappe.db.sql(
  390. """
  391. select t2.{dr_or_cr} from `tabJournal Entry` t1, `tabJournal Entry Account` t2
  392. where t1.name = t2.parent and t2.account = %(account)s
  393. and t2.party_type = %(party_type)s and t2.party = %(party)s
  394. and (t2.reference_type is null or t2.reference_type in ('', 'Sales Order', 'Purchase Order'))
  395. and t1.name = %(voucher_no)s and t2.name = %(voucher_detail_no)s
  396. and t1.docstatus=1 """.format(
  397. dr_or_cr=args.get("dr_or_cr")
  398. ),
  399. args,
  400. )
  401. else:
  402. party_account_field = (
  403. "paid_from" if erpnext.get_party_account_type(args.party_type) == "Receivable" else "paid_to"
  404. )
  405. if args.voucher_detail_no:
  406. ret = frappe.db.sql(
  407. """select t1.name
  408. from `tabPayment Entry` t1, `tabPayment Entry Reference` t2
  409. where
  410. t1.name = t2.parent and t1.docstatus = 1
  411. and t1.name = %(voucher_no)s and t2.name = %(voucher_detail_no)s
  412. and t1.party_type = %(party_type)s and t1.party = %(party)s and t1.{0} = %(account)s
  413. and t2.reference_doctype in ('', 'Sales Order', 'Purchase Order')
  414. and t2.allocated_amount = %(unreconciled_amount)s
  415. """.format(
  416. party_account_field
  417. ),
  418. args,
  419. )
  420. else:
  421. ret = frappe.db.sql(
  422. """select name from `tabPayment Entry`
  423. where
  424. name = %(voucher_no)s and docstatus = 1
  425. and party_type = %(party_type)s and party = %(party)s and {0} = %(account)s
  426. and unallocated_amount = %(unreconciled_amount)s
  427. """.format(
  428. party_account_field
  429. ),
  430. args,
  431. )
  432. if not ret:
  433. throw(_("""Payment Entry has been modified after you pulled it. Please pull it again."""))
  434. def validate_allocated_amount(args):
  435. precision = args.get("precision") or frappe.db.get_single_value(
  436. "System Settings", "currency_precision"
  437. )
  438. if args.get("allocated_amount") < 0:
  439. throw(_("Allocated amount cannot be negative"))
  440. elif flt(args.get("allocated_amount"), precision) > flt(args.get("unadjusted_amount"), precision):
  441. throw(_("Allocated amount cannot be greater than unadjusted amount"))
  442. def update_reference_in_journal_entry(d, journal_entry, do_not_save=False):
  443. """
  444. Updates against document, if partial amount splits into rows
  445. """
  446. jv_detail = journal_entry.get("accounts", {"name": d["voucher_detail_no"]})[0]
  447. if flt(d["unadjusted_amount"]) - flt(d["allocated_amount"]) != 0:
  448. # adjust the unreconciled balance
  449. amount_in_account_currency = flt(d["unadjusted_amount"]) - flt(d["allocated_amount"])
  450. amount_in_company_currency = amount_in_account_currency * flt(jv_detail.exchange_rate)
  451. jv_detail.set(d["dr_or_cr"], amount_in_account_currency)
  452. jv_detail.set(
  453. "debit" if d["dr_or_cr"] == "debit_in_account_currency" else "credit",
  454. amount_in_company_currency,
  455. )
  456. else:
  457. journal_entry.remove(jv_detail)
  458. # new row with references
  459. new_row = journal_entry.append("accounts")
  460. new_row.update((frappe.copy_doc(jv_detail)).as_dict())
  461. new_row.set(d["dr_or_cr"], d["allocated_amount"])
  462. new_row.set(
  463. "debit" if d["dr_or_cr"] == "debit_in_account_currency" else "credit",
  464. d["allocated_amount"] * flt(jv_detail.exchange_rate),
  465. )
  466. new_row.set(
  467. "credit_in_account_currency"
  468. if d["dr_or_cr"] == "debit_in_account_currency"
  469. else "debit_in_account_currency",
  470. 0,
  471. )
  472. new_row.set("credit" if d["dr_or_cr"] == "debit_in_account_currency" else "debit", 0)
  473. new_row.set("reference_type", d["against_voucher_type"])
  474. new_row.set("reference_name", d["against_voucher"])
  475. new_row.against_account = cstr(jv_detail.against_account)
  476. new_row.is_advance = cstr(jv_detail.is_advance)
  477. new_row.docstatus = 1
  478. # will work as update after submit
  479. journal_entry.flags.ignore_validate_update_after_submit = True
  480. if not do_not_save:
  481. journal_entry.save(ignore_permissions=True)
  482. def update_reference_in_payment_entry(
  483. d, payment_entry, do_not_save=False, skip_ref_details_update_for_pe=False
  484. ):
  485. reference_details = {
  486. "reference_doctype": d.against_voucher_type,
  487. "reference_name": d.against_voucher,
  488. "total_amount": d.grand_total,
  489. "outstanding_amount": d.outstanding_amount,
  490. "allocated_amount": d.allocated_amount,
  491. "exchange_rate": d.exchange_rate
  492. if not d.exchange_gain_loss
  493. else payment_entry.get_exchange_rate(),
  494. "exchange_gain_loss": d.exchange_gain_loss, # only populated from invoice in case of advance allocation
  495. }
  496. if d.voucher_detail_no:
  497. existing_row = payment_entry.get("references", {"name": d["voucher_detail_no"]})[0]
  498. original_row = existing_row.as_dict().copy()
  499. existing_row.update(reference_details)
  500. if d.allocated_amount < original_row.allocated_amount:
  501. new_row = payment_entry.append("references")
  502. new_row.docstatus = 1
  503. for field in list(reference_details):
  504. new_row.set(field, original_row[field])
  505. new_row.allocated_amount = original_row.allocated_amount - d.allocated_amount
  506. else:
  507. new_row = payment_entry.append("references")
  508. new_row.docstatus = 1
  509. new_row.update(reference_details)
  510. payment_entry.flags.ignore_validate_update_after_submit = True
  511. payment_entry.setup_party_account_field()
  512. payment_entry.set_missing_values()
  513. payment_entry.set_amounts()
  514. if d.difference_amount and d.difference_account:
  515. account_details = {
  516. "account": d.difference_account,
  517. "cost_center": payment_entry.cost_center
  518. or frappe.get_cached_value("Company", payment_entry.company, "cost_center"),
  519. }
  520. if d.difference_amount:
  521. account_details["amount"] = d.difference_amount
  522. payment_entry.set_gain_or_loss(account_details=account_details)
  523. payment_entry.flags.ignore_validate_update_after_submit = True
  524. payment_entry.setup_party_account_field()
  525. payment_entry.set_missing_values()
  526. if not skip_ref_details_update_for_pe:
  527. payment_entry.set_missing_ref_details()
  528. payment_entry.set_amounts()
  529. if not do_not_save:
  530. payment_entry.save(ignore_permissions=True)
  531. def unlink_ref_doc_from_payment_entries(ref_doc):
  532. remove_ref_doc_link_from_jv(ref_doc.doctype, ref_doc.name)
  533. remove_ref_doc_link_from_pe(ref_doc.doctype, ref_doc.name)
  534. frappe.db.sql(
  535. """update `tabGL Entry`
  536. set against_voucher_type=null, against_voucher=null,
  537. modified=%s, modified_by=%s
  538. where against_voucher_type=%s and against_voucher=%s
  539. and voucher_no != ifnull(against_voucher, '')""",
  540. (now(), frappe.session.user, ref_doc.doctype, ref_doc.name),
  541. )
  542. ple = qb.DocType("Payment Ledger Entry")
  543. qb.update(ple).set(ple.against_voucher_type, ple.voucher_type).set(
  544. ple.against_voucher_no, ple.voucher_no
  545. ).set(ple.modified, now()).set(ple.modified_by, frappe.session.user).where(
  546. (ple.against_voucher_type == ref_doc.doctype)
  547. & (ple.against_voucher_no == ref_doc.name)
  548. & (ple.delinked == 0)
  549. ).run()
  550. if ref_doc.doctype in ("Sales Invoice", "Purchase Invoice"):
  551. ref_doc.set("advances", [])
  552. frappe.db.sql(
  553. """delete from `tab{0} Advance` where parent = %s""".format(ref_doc.doctype), ref_doc.name
  554. )
  555. def remove_ref_doc_link_from_jv(ref_type, ref_no):
  556. linked_jv = frappe.db.sql_list(
  557. """select parent from `tabJournal Entry Account`
  558. where reference_type=%s and reference_name=%s and docstatus < 2""",
  559. (ref_type, ref_no),
  560. )
  561. if linked_jv:
  562. frappe.db.sql(
  563. """update `tabJournal Entry Account`
  564. set reference_type=null, reference_name = null,
  565. modified=%s, modified_by=%s
  566. where reference_type=%s and reference_name=%s
  567. and docstatus < 2""",
  568. (now(), frappe.session.user, ref_type, ref_no),
  569. )
  570. frappe.msgprint(_("Journal Entries {0} are un-linked").format("\n".join(linked_jv)))
  571. def remove_ref_doc_link_from_pe(ref_type, ref_no):
  572. linked_pe = frappe.db.sql_list(
  573. """select parent from `tabPayment Entry Reference`
  574. where reference_doctype=%s and reference_name=%s and docstatus < 2""",
  575. (ref_type, ref_no),
  576. )
  577. if linked_pe:
  578. frappe.db.sql(
  579. """update `tabPayment Entry Reference`
  580. set allocated_amount=0, modified=%s, modified_by=%s
  581. where reference_doctype=%s and reference_name=%s
  582. and docstatus < 2""",
  583. (now(), frappe.session.user, ref_type, ref_no),
  584. )
  585. for pe in linked_pe:
  586. try:
  587. pe_doc = frappe.get_doc("Payment Entry", pe)
  588. pe_doc.set_amounts()
  589. pe_doc.clear_unallocated_reference_document_rows()
  590. pe_doc.validate_payment_type_with_outstanding()
  591. except Exception as e:
  592. msg = _("There were issues unlinking payment entry {0}.").format(pe_doc.name)
  593. msg += "<br>"
  594. msg += _("Please cancel payment entry manually first")
  595. frappe.throw(msg, exc=PaymentEntryUnlinkError, title=_("Payment Unlink Error"))
  596. frappe.db.sql(
  597. """update `tabPayment Entry` set total_allocated_amount=%s,
  598. base_total_allocated_amount=%s, unallocated_amount=%s, modified=%s, modified_by=%s
  599. where name=%s""",
  600. (
  601. pe_doc.total_allocated_amount,
  602. pe_doc.base_total_allocated_amount,
  603. pe_doc.unallocated_amount,
  604. now(),
  605. frappe.session.user,
  606. pe,
  607. ),
  608. )
  609. frappe.msgprint(_("Payment Entries {0} are un-linked").format("\n".join(linked_pe)))
  610. @frappe.whitelist()
  611. def get_company_default(company, fieldname, ignore_validation=False):
  612. value = frappe.get_cached_value("Company", company, fieldname)
  613. if not ignore_validation and not value:
  614. throw(
  615. _("Please set default {0} in Company {1}").format(
  616. frappe.get_meta("Company").get_label(fieldname), company
  617. )
  618. )
  619. return value
  620. def fix_total_debit_credit():
  621. vouchers = frappe.db.sql(
  622. """select voucher_type, voucher_no,
  623. sum(debit) - sum(credit) as diff
  624. from `tabGL Entry`
  625. group by voucher_type, voucher_no
  626. having sum(debit) != sum(credit)""",
  627. as_dict=1,
  628. )
  629. for d in vouchers:
  630. if abs(d.diff) > 0:
  631. dr_or_cr = d.voucher_type == "Sales Invoice" and "credit" or "debit"
  632. frappe.db.sql(
  633. """update `tabGL Entry` set %s = %s + %s
  634. where voucher_type = %s and voucher_no = %s and %s > 0 limit 1"""
  635. % (dr_or_cr, dr_or_cr, "%s", "%s", "%s", dr_or_cr),
  636. (d.diff, d.voucher_type, d.voucher_no),
  637. )
  638. def get_currency_precision():
  639. precision = cint(frappe.db.get_default("currency_precision"))
  640. if not precision:
  641. number_format = frappe.db.get_default("number_format") or "#,###.##"
  642. precision = get_number_format_info(number_format)[2]
  643. return precision
  644. def get_stock_rbnb_difference(posting_date, company):
  645. stock_items = frappe.db.sql_list(
  646. """select distinct item_code
  647. from `tabStock Ledger Entry` where company=%s""",
  648. company,
  649. )
  650. pr_valuation_amount = frappe.db.sql(
  651. """
  652. select sum(pr_item.valuation_rate * pr_item.qty * pr_item.conversion_factor)
  653. from `tabPurchase Receipt Item` pr_item, `tabPurchase Receipt` pr
  654. where pr.name = pr_item.parent and pr.docstatus=1 and pr.company=%s
  655. and pr.posting_date <= %s and pr_item.item_code in (%s)"""
  656. % ("%s", "%s", ", ".join(["%s"] * len(stock_items))),
  657. tuple([company, posting_date] + stock_items),
  658. )[0][0]
  659. pi_valuation_amount = frappe.db.sql(
  660. """
  661. select sum(pi_item.valuation_rate * pi_item.qty * pi_item.conversion_factor)
  662. from `tabPurchase Invoice Item` pi_item, `tabPurchase Invoice` pi
  663. where pi.name = pi_item.parent and pi.docstatus=1 and pi.company=%s
  664. and pi.posting_date <= %s and pi_item.item_code in (%s)"""
  665. % ("%s", "%s", ", ".join(["%s"] * len(stock_items))),
  666. tuple([company, posting_date] + stock_items),
  667. )[0][0]
  668. # Balance should be
  669. stock_rbnb = flt(pr_valuation_amount, 2) - flt(pi_valuation_amount, 2)
  670. # Balance as per system
  671. stock_rbnb_account = "Stock Received But Not Billed - " + frappe.get_cached_value(
  672. "Company", company, "abbr"
  673. )
  674. sys_bal = get_balance_on(stock_rbnb_account, posting_date, in_account_currency=False)
  675. # Amount should be credited
  676. return flt(stock_rbnb) + flt(sys_bal)
  677. def get_held_invoices(party_type, party):
  678. """
  679. Returns a list of names Purchase Invoices for the given party that are on hold
  680. """
  681. held_invoices = None
  682. if party_type == "Supplier":
  683. held_invoices = frappe.db.sql(
  684. "select name from `tabPurchase Invoice` where release_date IS NOT NULL and release_date > CURDATE()",
  685. as_dict=1,
  686. )
  687. held_invoices = set(d["name"] for d in held_invoices)
  688. return held_invoices
  689. def get_outstanding_invoices(
  690. party_type,
  691. party,
  692. account,
  693. common_filter=None,
  694. posting_date=None,
  695. min_outstanding=None,
  696. max_outstanding=None,
  697. accounting_dimensions=None,
  698. ):
  699. ple = qb.DocType("Payment Ledger Entry")
  700. outstanding_invoices = []
  701. precision = frappe.get_precision("Sales Invoice", "outstanding_amount") or 2
  702. if account:
  703. root_type, account_type = frappe.get_cached_value(
  704. "Account", account, ["root_type", "account_type"]
  705. )
  706. party_account_type = "Receivable" if root_type == "Asset" else "Payable"
  707. party_account_type = account_type or party_account_type
  708. else:
  709. party_account_type = erpnext.get_party_account_type(party_type)
  710. held_invoices = get_held_invoices(party_type, party)
  711. common_filter = common_filter or []
  712. common_filter.append(ple.account_type == party_account_type)
  713. common_filter.append(ple.account == account)
  714. common_filter.append(ple.party_type == party_type)
  715. common_filter.append(ple.party == party)
  716. ple_query = QueryPaymentLedger()
  717. invoice_list = ple_query.get_voucher_outstandings(
  718. common_filter=common_filter,
  719. posting_date=posting_date,
  720. min_outstanding=min_outstanding,
  721. max_outstanding=max_outstanding,
  722. get_invoices=True,
  723. accounting_dimensions=accounting_dimensions or [],
  724. )
  725. for d in invoice_list:
  726. payment_amount = d.invoice_amount_in_account_currency - d.outstanding_in_account_currency
  727. outstanding_amount = d.outstanding_in_account_currency
  728. if outstanding_amount > 0.5 / (10**precision):
  729. if (
  730. min_outstanding
  731. and max_outstanding
  732. and not (outstanding_amount >= min_outstanding and outstanding_amount <= max_outstanding)
  733. ):
  734. continue
  735. if not d.voucher_type == "Purchase Invoice" or d.voucher_no not in held_invoices:
  736. outstanding_invoices.append(
  737. frappe._dict(
  738. {
  739. "voucher_no": d.voucher_no,
  740. "voucher_type": d.voucher_type,
  741. "posting_date": d.posting_date,
  742. "invoice_amount": flt(d.invoice_amount_in_account_currency),
  743. "payment_amount": payment_amount,
  744. "outstanding_amount": outstanding_amount,
  745. "due_date": d.due_date,
  746. "currency": d.currency,
  747. }
  748. )
  749. )
  750. outstanding_invoices = sorted(
  751. outstanding_invoices, key=lambda k: k["due_date"] or getdate(nowdate())
  752. )
  753. return outstanding_invoices
  754. def get_account_name(
  755. account_type=None, root_type=None, is_group=None, account_currency=None, company=None
  756. ):
  757. """return account based on matching conditions"""
  758. return frappe.db.get_value(
  759. "Account",
  760. {
  761. "account_type": account_type or "",
  762. "root_type": root_type or "",
  763. "is_group": is_group or 0,
  764. "account_currency": account_currency or frappe.defaults.get_defaults().currency,
  765. "company": company or frappe.defaults.get_defaults().company,
  766. },
  767. "name",
  768. )
  769. @frappe.whitelist()
  770. def get_companies():
  771. """get a list of companies based on permission"""
  772. return [d.name for d in frappe.get_list("Company", fields=["name"], order_by="name")]
  773. @frappe.whitelist()
  774. def get_children(doctype, parent, company, is_root=False):
  775. from erpnext.accounts.report.financial_statements import sort_accounts
  776. parent_fieldname = "parent_" + doctype.lower().replace(" ", "_")
  777. fields = ["name as value", "is_group as expandable"]
  778. filters = [["docstatus", "<", 2]]
  779. filters.append(['ifnull(`{0}`,"")'.format(parent_fieldname), "=", "" if is_root else parent])
  780. if is_root:
  781. fields += ["root_type", "report_type", "account_currency"] if doctype == "Account" else []
  782. filters.append(["company", "=", company])
  783. else:
  784. fields += ["root_type", "account_currency"] if doctype == "Account" else []
  785. fields += [parent_fieldname + " as parent"]
  786. acc = frappe.get_list(doctype, fields=fields, filters=filters)
  787. if doctype == "Account":
  788. sort_accounts(acc, is_root, key="value")
  789. return acc
  790. @frappe.whitelist()
  791. def get_account_balances(accounts, company):
  792. if isinstance(accounts, str):
  793. accounts = loads(accounts)
  794. if not accounts:
  795. return []
  796. company_currency = frappe.get_cached_value("Company", company, "default_currency")
  797. for account in accounts:
  798. account["company_currency"] = company_currency
  799. account["balance"] = flt(
  800. get_balance_on(account["value"], in_account_currency=False, company=company)
  801. )
  802. if account["account_currency"] and account["account_currency"] != company_currency:
  803. account["balance_in_account_currency"] = flt(get_balance_on(account["value"], company=company))
  804. return accounts
  805. def create_payment_gateway_account(gateway, payment_channel="Email"):
  806. from erpnext.setup.setup_wizard.operations.install_fixtures import create_bank_account
  807. company = frappe.db.get_value("Global Defaults", None, "default_company")
  808. if not company:
  809. return
  810. # NOTE: we translate Payment Gateway account name because that is going to be used by the end user
  811. bank_account = frappe.db.get_value(
  812. "Account",
  813. {"account_name": _(gateway), "company": company},
  814. ["name", "account_currency"],
  815. as_dict=1,
  816. )
  817. if not bank_account:
  818. # check for untranslated one
  819. bank_account = frappe.db.get_value(
  820. "Account",
  821. {"account_name": gateway, "company": company},
  822. ["name", "account_currency"],
  823. as_dict=1,
  824. )
  825. if not bank_account:
  826. # try creating one
  827. bank_account = create_bank_account({"company_name": company, "bank_account": _(gateway)})
  828. if not bank_account:
  829. frappe.msgprint(_("Payment Gateway Account not created, please create one manually."))
  830. return
  831. # if payment gateway account exists, return
  832. if frappe.db.exists(
  833. "Payment Gateway Account",
  834. {"payment_gateway": gateway, "currency": bank_account.account_currency},
  835. ):
  836. return
  837. try:
  838. frappe.get_doc(
  839. {
  840. "doctype": "Payment Gateway Account",
  841. "is_default": 1,
  842. "payment_gateway": gateway,
  843. "payment_account": bank_account.name,
  844. "currency": bank_account.account_currency,
  845. "payment_channel": payment_channel,
  846. }
  847. ).insert(ignore_permissions=True, ignore_if_duplicate=True)
  848. except frappe.DuplicateEntryError:
  849. # already exists, due to a reinstall?
  850. pass
  851. @frappe.whitelist()
  852. def update_cost_center(docname, cost_center_name, cost_center_number, company, merge):
  853. """
  854. Renames the document by adding the number as a prefix to the current name and updates
  855. all transaction where it was present.
  856. """
  857. validate_field_number("Cost Center", docname, cost_center_number, company, "cost_center_number")
  858. if cost_center_number:
  859. frappe.db.set_value("Cost Center", docname, "cost_center_number", cost_center_number.strip())
  860. else:
  861. frappe.db.set_value("Cost Center", docname, "cost_center_number", "")
  862. frappe.db.set_value("Cost Center", docname, "cost_center_name", cost_center_name.strip())
  863. new_name = get_autoname_with_number(cost_center_number, cost_center_name, company)
  864. if docname != new_name:
  865. frappe.rename_doc("Cost Center", docname, new_name, force=1, merge=merge)
  866. return new_name
  867. def validate_field_number(doctype_name, docname, number_value, company, field_name):
  868. """Validate if the number entered isn't already assigned to some other document."""
  869. if number_value:
  870. filters = {field_name: number_value, "name": ["!=", docname]}
  871. if company:
  872. filters["company"] = company
  873. doctype_with_same_number = frappe.db.get_value(doctype_name, filters)
  874. if doctype_with_same_number:
  875. frappe.throw(
  876. _("{0} Number {1} is already used in {2} {3}").format(
  877. doctype_name, number_value, doctype_name.lower(), doctype_with_same_number
  878. )
  879. )
  880. def get_autoname_with_number(number_value, doc_title, company):
  881. """append title with prefix as number and suffix as company's abbreviation separated by '-'"""
  882. company_abbr = frappe.get_cached_value("Company", company, "abbr")
  883. parts = [doc_title.strip(), company_abbr]
  884. if cstr(number_value).strip():
  885. parts.insert(0, cstr(number_value).strip())
  886. return " - ".join(parts)
  887. @frappe.whitelist()
  888. def get_coa(doctype, parent, is_root, chart=None):
  889. from erpnext.accounts.doctype.account.chart_of_accounts.chart_of_accounts import (
  890. build_tree_from_json,
  891. )
  892. # add chart to flags to retrieve when called from expand all function
  893. chart = chart if chart else frappe.flags.chart
  894. frappe.flags.chart = chart
  895. parent = None if parent == _("All Accounts") else parent
  896. accounts = build_tree_from_json(chart) # returns alist of dict in a tree render-able form
  897. # filter out to show data for the selected node only
  898. accounts = [d for d in accounts if d["parent_account"] == parent]
  899. return accounts
  900. def update_gl_entries_after(
  901. posting_date,
  902. posting_time,
  903. for_warehouses=None,
  904. for_items=None,
  905. warehouse_account=None,
  906. company=None,
  907. ):
  908. stock_vouchers = get_future_stock_vouchers(
  909. posting_date, posting_time, for_warehouses, for_items, company
  910. )
  911. repost_gle_for_stock_vouchers(stock_vouchers, posting_date, company, warehouse_account)
  912. def repost_gle_for_stock_vouchers(
  913. stock_vouchers: List[Tuple[str, str]],
  914. posting_date: str,
  915. company: Optional[str] = None,
  916. warehouse_account=None,
  917. repost_doc: Optional["RepostItemValuation"] = None,
  918. ):
  919. from erpnext.accounts.general_ledger import toggle_debit_credit_if_negative
  920. if not stock_vouchers:
  921. return
  922. if not warehouse_account:
  923. warehouse_account = get_warehouse_account_map(company)
  924. stock_vouchers = sort_stock_vouchers_by_posting_date(stock_vouchers)
  925. if repost_doc and repost_doc.gl_reposting_index:
  926. # Restore progress
  927. stock_vouchers = stock_vouchers[cint(repost_doc.gl_reposting_index) :]
  928. precision = get_field_precision(frappe.get_meta("GL Entry").get_field("debit")) or 2
  929. for stock_vouchers_chunk in create_batch(stock_vouchers, GL_REPOSTING_CHUNK):
  930. gle = get_voucherwise_gl_entries(stock_vouchers_chunk, posting_date)
  931. for voucher_type, voucher_no in stock_vouchers_chunk:
  932. existing_gle = gle.get((voucher_type, voucher_no), [])
  933. voucher_obj = frappe.get_doc(voucher_type, voucher_no)
  934. # Some transactions post credit as negative debit, this is handled while posting GLE
  935. # but while comparing we need to make sure it's flipped so comparisons are accurate
  936. expected_gle = toggle_debit_credit_if_negative(voucher_obj.get_gl_entries(warehouse_account))
  937. if expected_gle:
  938. if not existing_gle or not compare_existing_and_expected_gle(
  939. existing_gle, expected_gle, precision
  940. ):
  941. _delete_accounting_ledger_entries(voucher_type, voucher_no)
  942. voucher_obj.make_gl_entries(gl_entries=expected_gle, from_repost=True)
  943. else:
  944. _delete_accounting_ledger_entries(voucher_type, voucher_no)
  945. if not frappe.flags.in_test:
  946. frappe.db.commit()
  947. if repost_doc:
  948. repost_doc.db_set(
  949. "gl_reposting_index",
  950. cint(repost_doc.gl_reposting_index) + len(stock_vouchers_chunk),
  951. )
  952. def _delete_pl_entries(voucher_type, voucher_no):
  953. ple = qb.DocType("Payment Ledger Entry")
  954. qb.from_(ple).delete().where(
  955. (ple.voucher_type == voucher_type) & (ple.voucher_no == voucher_no)
  956. ).run()
  957. def _delete_gl_entries(voucher_type, voucher_no):
  958. gle = qb.DocType("GL Entry")
  959. qb.from_(gle).delete().where(
  960. (gle.voucher_type == voucher_type) & (gle.voucher_no == voucher_no)
  961. ).run()
  962. def _delete_accounting_ledger_entries(voucher_type, voucher_no):
  963. """
  964. Remove entries from both General and Payment Ledger for specified Voucher
  965. """
  966. _delete_gl_entries(voucher_type, voucher_no)
  967. _delete_pl_entries(voucher_type, voucher_no)
  968. def sort_stock_vouchers_by_posting_date(
  969. stock_vouchers: List[Tuple[str, str]]
  970. ) -> List[Tuple[str, str]]:
  971. sle = frappe.qb.DocType("Stock Ledger Entry")
  972. voucher_nos = [v[1] for v in stock_vouchers]
  973. sles = (
  974. frappe.qb.from_(sle)
  975. .select(sle.voucher_type, sle.voucher_no, sle.posting_date, sle.posting_time, sle.creation)
  976. .where((sle.is_cancelled == 0) & (sle.voucher_no.isin(voucher_nos)))
  977. .groupby(sle.voucher_type, sle.voucher_no)
  978. .orderby(sle.posting_date)
  979. .orderby(sle.posting_time)
  980. .orderby(sle.creation)
  981. ).run(as_dict=True)
  982. sorted_vouchers = [(sle.voucher_type, sle.voucher_no) for sle in sles]
  983. unknown_vouchers = set(stock_vouchers) - set(sorted_vouchers)
  984. if unknown_vouchers:
  985. sorted_vouchers.extend(unknown_vouchers)
  986. return sorted_vouchers
  987. def get_future_stock_vouchers(
  988. posting_date, posting_time, for_warehouses=None, for_items=None, company=None
  989. ):
  990. values = []
  991. condition = ""
  992. if for_items:
  993. condition += " and item_code in ({})".format(", ".join(["%s"] * len(for_items)))
  994. values += for_items
  995. if for_warehouses:
  996. condition += " and warehouse in ({})".format(", ".join(["%s"] * len(for_warehouses)))
  997. values += for_warehouses
  998. if company:
  999. condition += " and company = %s"
  1000. values.append(company)
  1001. future_stock_vouchers = frappe.db.sql(
  1002. """select distinct sle.voucher_type, sle.voucher_no
  1003. from `tabStock Ledger Entry` sle
  1004. where
  1005. timestamp(sle.posting_date, sle.posting_time) >= timestamp(%s, %s)
  1006. and is_cancelled = 0
  1007. {condition}
  1008. order by timestamp(sle.posting_date, sle.posting_time) asc, creation asc for update""".format(
  1009. condition=condition
  1010. ),
  1011. tuple([posting_date, posting_time] + values),
  1012. as_dict=True,
  1013. )
  1014. return [(d.voucher_type, d.voucher_no) for d in future_stock_vouchers]
  1015. def get_voucherwise_gl_entries(future_stock_vouchers, posting_date):
  1016. """Get voucherwise list of GL entries.
  1017. Only fetches GLE fields required for comparing with new GLE.
  1018. Check compare_existing_and_expected_gle function below.
  1019. returns:
  1020. Dict[Tuple[voucher_type, voucher_no], List[GL Entries]]
  1021. """
  1022. gl_entries = {}
  1023. if not future_stock_vouchers:
  1024. return gl_entries
  1025. voucher_nos = [d[1] for d in future_stock_vouchers]
  1026. gles = frappe.db.sql(
  1027. """
  1028. select name, account, credit, debit, cost_center, project, voucher_type, voucher_no
  1029. from `tabGL Entry`
  1030. where
  1031. posting_date >= %s and voucher_no in (%s)"""
  1032. % ("%s", ", ".join(["%s"] * len(voucher_nos))),
  1033. tuple([posting_date] + voucher_nos),
  1034. as_dict=1,
  1035. )
  1036. for d in gles:
  1037. gl_entries.setdefault((d.voucher_type, d.voucher_no), []).append(d)
  1038. return gl_entries
  1039. def compare_existing_and_expected_gle(existing_gle, expected_gle, precision):
  1040. if len(existing_gle) != len(expected_gle):
  1041. return False
  1042. matched = True
  1043. for entry in expected_gle:
  1044. account_existed = False
  1045. for e in existing_gle:
  1046. if entry.account == e.account:
  1047. account_existed = True
  1048. if (
  1049. entry.account == e.account
  1050. and (not entry.cost_center or not e.cost_center or entry.cost_center == e.cost_center)
  1051. and (
  1052. flt(entry.debit, precision) != flt(e.debit, precision)
  1053. or flt(entry.credit, precision) != flt(e.credit, precision)
  1054. )
  1055. ):
  1056. matched = False
  1057. break
  1058. if not account_existed:
  1059. matched = False
  1060. break
  1061. return matched
  1062. def get_stock_accounts(company, voucher_type=None, voucher_no=None):
  1063. stock_accounts = [
  1064. d.name
  1065. for d in frappe.db.get_all(
  1066. "Account", {"account_type": "Stock", "company": company, "is_group": 0}
  1067. )
  1068. ]
  1069. if voucher_type and voucher_no:
  1070. if voucher_type == "Journal Entry":
  1071. stock_accounts = [
  1072. d.account
  1073. for d in frappe.db.get_all(
  1074. "Journal Entry Account", {"parent": voucher_no, "account": ["in", stock_accounts]}, "account"
  1075. )
  1076. ]
  1077. else:
  1078. stock_accounts = [
  1079. d.account
  1080. for d in frappe.db.get_all(
  1081. "GL Entry",
  1082. {"voucher_type": voucher_type, "voucher_no": voucher_no, "account": ["in", stock_accounts]},
  1083. "account",
  1084. )
  1085. ]
  1086. return stock_accounts
  1087. def get_stock_and_account_balance(account=None, posting_date=None, company=None):
  1088. if not posting_date:
  1089. posting_date = nowdate()
  1090. warehouse_account = get_warehouse_account_map(company)
  1091. account_balance = get_balance_on(
  1092. account, posting_date, in_account_currency=False, ignore_account_permission=True
  1093. )
  1094. related_warehouses = [
  1095. wh
  1096. for wh, wh_details in warehouse_account.items()
  1097. if wh_details.account == account and not wh_details.is_group
  1098. ]
  1099. total_stock_value = get_stock_value_on(related_warehouses, posting_date)
  1100. precision = frappe.get_precision("Journal Entry Account", "debit_in_account_currency")
  1101. return flt(account_balance, precision), flt(total_stock_value, precision), related_warehouses
  1102. def get_journal_entry(account, stock_adjustment_account, amount):
  1103. db_or_cr_warehouse_account = (
  1104. "credit_in_account_currency" if amount < 0 else "debit_in_account_currency"
  1105. )
  1106. db_or_cr_stock_adjustment_account = (
  1107. "debit_in_account_currency" if amount < 0 else "credit_in_account_currency"
  1108. )
  1109. return {
  1110. "accounts": [
  1111. {"account": account, db_or_cr_warehouse_account: abs(amount)},
  1112. {"account": stock_adjustment_account, db_or_cr_stock_adjustment_account: abs(amount)},
  1113. ]
  1114. }
  1115. def check_and_delete_linked_reports(report):
  1116. """Check if reports are referenced in Desktop Icon"""
  1117. icons = frappe.get_all("Desktop Icon", fields=["name"], filters={"_report": report})
  1118. if icons:
  1119. for icon in icons:
  1120. frappe.delete_doc("Desktop Icon", icon)
  1121. def get_payment_ledger_entries(gl_entries, cancel=0):
  1122. ple_map = []
  1123. if gl_entries:
  1124. ple = None
  1125. # companies
  1126. account = qb.DocType("Account")
  1127. companies = list(set([x.company for x in gl_entries]))
  1128. # receivable/payable account
  1129. accounts_with_types = (
  1130. qb.from_(account)
  1131. .select(account.name, account.account_type)
  1132. .where(
  1133. (account.account_type.isin(["Receivable", "Payable"]) & (account.company.isin(companies)))
  1134. )
  1135. .run(as_dict=True)
  1136. )
  1137. receivable_or_payable_accounts = [y.name for y in accounts_with_types]
  1138. def get_account_type(account):
  1139. for entry in accounts_with_types:
  1140. if entry.name == account:
  1141. return entry.account_type
  1142. dr_or_cr = 0
  1143. account_type = None
  1144. for gle in gl_entries:
  1145. if gle.account in receivable_or_payable_accounts:
  1146. account_type = get_account_type(gle.account)
  1147. if account_type == "Receivable":
  1148. dr_or_cr = gle.debit - gle.credit
  1149. dr_or_cr_account_currency = gle.debit_in_account_currency - gle.credit_in_account_currency
  1150. elif account_type == "Payable":
  1151. dr_or_cr = gle.credit - gle.debit
  1152. dr_or_cr_account_currency = gle.credit_in_account_currency - gle.debit_in_account_currency
  1153. if cancel:
  1154. dr_or_cr *= -1
  1155. dr_or_cr_account_currency *= -1
  1156. ple = frappe._dict(
  1157. doctype="Payment Ledger Entry",
  1158. posting_date=gle.posting_date,
  1159. company=gle.company,
  1160. account_type=account_type,
  1161. account=gle.account,
  1162. party_type=gle.party_type,
  1163. party=gle.party,
  1164. cost_center=gle.cost_center,
  1165. finance_book=gle.finance_book,
  1166. due_date=gle.due_date,
  1167. voucher_type=gle.voucher_type,
  1168. voucher_no=gle.voucher_no,
  1169. against_voucher_type=gle.against_voucher_type
  1170. if gle.against_voucher_type
  1171. else gle.voucher_type,
  1172. against_voucher_no=gle.against_voucher if gle.against_voucher else gle.voucher_no,
  1173. account_currency=gle.account_currency,
  1174. amount=dr_or_cr,
  1175. amount_in_account_currency=dr_or_cr_account_currency,
  1176. delinked=True if cancel else False,
  1177. remarks=gle.remarks,
  1178. )
  1179. dimensions_and_defaults = get_dimensions()
  1180. if dimensions_and_defaults:
  1181. for dimension in dimensions_and_defaults[0]:
  1182. ple[dimension.fieldname] = gle.get(dimension.fieldname)
  1183. ple_map.append(ple)
  1184. return ple_map
  1185. def create_payment_ledger_entry(
  1186. gl_entries, cancel=0, adv_adj=0, update_outstanding="Yes", from_repost=0
  1187. ):
  1188. if gl_entries:
  1189. ple_map = get_payment_ledger_entries(gl_entries, cancel=cancel)
  1190. for entry in ple_map:
  1191. ple = frappe.get_doc(entry)
  1192. if cancel:
  1193. delink_original_entry(ple)
  1194. ple.flags.ignore_permissions = 1
  1195. ple.flags.adv_adj = adv_adj
  1196. ple.flags.from_repost = from_repost
  1197. ple.flags.update_outstanding = update_outstanding
  1198. ple.submit()
  1199. def update_voucher_outstanding(voucher_type, voucher_no, account, party_type, party):
  1200. ple = frappe.qb.DocType("Payment Ledger Entry")
  1201. vouchers = [frappe._dict({"voucher_type": voucher_type, "voucher_no": voucher_no})]
  1202. common_filter = []
  1203. if account:
  1204. common_filter.append(ple.account == account)
  1205. if party_type:
  1206. common_filter.append(ple.party_type == party_type)
  1207. if party:
  1208. common_filter.append(ple.party == party)
  1209. ple_query = QueryPaymentLedger()
  1210. # on cancellation outstanding can be an empty list
  1211. voucher_outstanding = ple_query.get_voucher_outstandings(vouchers, common_filter=common_filter)
  1212. if (
  1213. voucher_type in ["Sales Invoice", "Purchase Invoice", "Fees"]
  1214. and party_type
  1215. and party
  1216. and voucher_outstanding
  1217. ):
  1218. outstanding = voucher_outstanding[0]
  1219. ref_doc = frappe.get_doc(voucher_type, voucher_no)
  1220. # Didn't use db_set for optimisation purpose
  1221. ref_doc.outstanding_amount = outstanding["outstanding_in_account_currency"] or 0.0
  1222. frappe.db.set_value(
  1223. voucher_type,
  1224. voucher_no,
  1225. "outstanding_amount",
  1226. outstanding["outstanding_in_account_currency"] or 0.0,
  1227. )
  1228. ref_doc.set_status(update=True)
  1229. def delink_original_entry(pl_entry):
  1230. if pl_entry:
  1231. ple = qb.DocType("Payment Ledger Entry")
  1232. query = (
  1233. qb.update(ple)
  1234. .set(ple.delinked, True)
  1235. .set(ple.modified, now())
  1236. .set(ple.modified_by, frappe.session.user)
  1237. .where(
  1238. (ple.company == pl_entry.company)
  1239. & (ple.account_type == pl_entry.account_type)
  1240. & (ple.account == pl_entry.account)
  1241. & (ple.party_type == pl_entry.party_type)
  1242. & (ple.party == pl_entry.party)
  1243. & (ple.voucher_type == pl_entry.voucher_type)
  1244. & (ple.voucher_no == pl_entry.voucher_no)
  1245. & (ple.against_voucher_type == pl_entry.against_voucher_type)
  1246. & (ple.against_voucher_no == pl_entry.against_voucher_no)
  1247. )
  1248. )
  1249. query.run()
  1250. class QueryPaymentLedger(object):
  1251. """
  1252. Helper Class for Querying Payment Ledger Entry
  1253. """
  1254. def __init__(self):
  1255. self.ple = qb.DocType("Payment Ledger Entry")
  1256. # query result
  1257. self.voucher_outstandings = []
  1258. # query filters
  1259. self.vouchers = []
  1260. self.common_filter = []
  1261. self.voucher_posting_date = []
  1262. self.min_outstanding = None
  1263. self.max_outstanding = None
  1264. def reset(self):
  1265. # clear filters
  1266. self.vouchers.clear()
  1267. self.common_filter.clear()
  1268. self.min_outstanding = self.max_outstanding = None
  1269. # clear result
  1270. self.voucher_outstandings.clear()
  1271. def query_for_outstanding(self):
  1272. """
  1273. Database query to fetch voucher amount and voucher outstanding using Common Table Expression
  1274. """
  1275. ple = self.ple
  1276. filter_on_voucher_no = []
  1277. filter_on_against_voucher_no = []
  1278. if self.vouchers:
  1279. voucher_types = set([x.voucher_type for x in self.vouchers])
  1280. voucher_nos = set([x.voucher_no for x in self.vouchers])
  1281. filter_on_voucher_no.append(ple.voucher_type.isin(voucher_types))
  1282. filter_on_voucher_no.append(ple.voucher_no.isin(voucher_nos))
  1283. filter_on_against_voucher_no.append(ple.against_voucher_type.isin(voucher_types))
  1284. filter_on_against_voucher_no.append(ple.against_voucher_no.isin(voucher_nos))
  1285. # build outstanding amount filter
  1286. filter_on_outstanding_amount = []
  1287. if self.min_outstanding:
  1288. if self.min_outstanding > 0:
  1289. filter_on_outstanding_amount.append(
  1290. Table("outstanding").amount_in_account_currency >= self.min_outstanding
  1291. )
  1292. else:
  1293. filter_on_outstanding_amount.append(
  1294. Table("outstanding").amount_in_account_currency <= self.min_outstanding
  1295. )
  1296. if self.max_outstanding:
  1297. if self.max_outstanding > 0:
  1298. filter_on_outstanding_amount.append(
  1299. Table("outstanding").amount_in_account_currency <= self.max_outstanding
  1300. )
  1301. else:
  1302. filter_on_outstanding_amount.append(
  1303. Table("outstanding").amount_in_account_currency >= self.max_outstanding
  1304. )
  1305. # build query for voucher amount
  1306. query_voucher_amount = (
  1307. qb.from_(ple)
  1308. .select(
  1309. ple.account,
  1310. ple.voucher_type,
  1311. ple.voucher_no,
  1312. ple.party_type,
  1313. ple.party,
  1314. ple.posting_date,
  1315. ple.due_date,
  1316. ple.account_currency.as_("currency"),
  1317. Sum(ple.amount).as_("amount"),
  1318. Sum(ple.amount_in_account_currency).as_("amount_in_account_currency"),
  1319. )
  1320. .where(ple.delinked == 0)
  1321. .where(Criterion.all(filter_on_voucher_no))
  1322. .where(Criterion.all(self.common_filter))
  1323. .where(Criterion.all(self.dimensions_filter))
  1324. .where(Criterion.all(self.voucher_posting_date))
  1325. .groupby(ple.voucher_type, ple.voucher_no, ple.party_type, ple.party)
  1326. )
  1327. # build query for voucher outstanding
  1328. query_voucher_outstanding = (
  1329. qb.from_(ple)
  1330. .select(
  1331. ple.account,
  1332. ple.against_voucher_type.as_("voucher_type"),
  1333. ple.against_voucher_no.as_("voucher_no"),
  1334. ple.party_type,
  1335. ple.party,
  1336. ple.posting_date,
  1337. ple.due_date,
  1338. ple.account_currency.as_("currency"),
  1339. Sum(ple.amount).as_("amount"),
  1340. Sum(ple.amount_in_account_currency).as_("amount_in_account_currency"),
  1341. )
  1342. .where(ple.delinked == 0)
  1343. .where(Criterion.all(filter_on_against_voucher_no))
  1344. .where(Criterion.all(self.common_filter))
  1345. .groupby(ple.against_voucher_type, ple.against_voucher_no, ple.party_type, ple.party)
  1346. )
  1347. # build CTE for combining voucher amount and outstanding
  1348. self.cte_query_voucher_amount_and_outstanding = (
  1349. qb.with_(query_voucher_amount, "vouchers")
  1350. .with_(query_voucher_outstanding, "outstanding")
  1351. .from_(AliasedQuery("vouchers"))
  1352. .left_join(AliasedQuery("outstanding"))
  1353. .on(
  1354. (AliasedQuery("vouchers").account == AliasedQuery("outstanding").account)
  1355. & (AliasedQuery("vouchers").voucher_type == AliasedQuery("outstanding").voucher_type)
  1356. & (AliasedQuery("vouchers").voucher_no == AliasedQuery("outstanding").voucher_no)
  1357. & (AliasedQuery("vouchers").party_type == AliasedQuery("outstanding").party_type)
  1358. & (AliasedQuery("vouchers").party == AliasedQuery("outstanding").party)
  1359. )
  1360. .select(
  1361. Table("vouchers").account,
  1362. Table("vouchers").voucher_type,
  1363. Table("vouchers").voucher_no,
  1364. Table("vouchers").party_type,
  1365. Table("vouchers").party,
  1366. Table("vouchers").posting_date,
  1367. Table("vouchers").amount.as_("invoice_amount"),
  1368. Table("vouchers").amount_in_account_currency.as_("invoice_amount_in_account_currency"),
  1369. Table("outstanding").amount.as_("outstanding"),
  1370. Table("outstanding").amount_in_account_currency.as_("outstanding_in_account_currency"),
  1371. (Table("vouchers").amount - Table("outstanding").amount).as_("paid_amount"),
  1372. (
  1373. Table("vouchers").amount_in_account_currency - Table("outstanding").amount_in_account_currency
  1374. ).as_("paid_amount_in_account_currency"),
  1375. Table("vouchers").due_date,
  1376. Table("vouchers").currency,
  1377. )
  1378. .where(Criterion.all(filter_on_outstanding_amount))
  1379. )
  1380. # build CTE filter
  1381. # only fetch invoices
  1382. if self.get_invoices:
  1383. self.cte_query_voucher_amount_and_outstanding = (
  1384. self.cte_query_voucher_amount_and_outstanding.having(
  1385. qb.Field("outstanding_in_account_currency") > 0
  1386. )
  1387. )
  1388. # only fetch payments
  1389. elif self.get_payments:
  1390. self.cte_query_voucher_amount_and_outstanding = (
  1391. self.cte_query_voucher_amount_and_outstanding.having(
  1392. qb.Field("outstanding_in_account_currency") < 0
  1393. )
  1394. )
  1395. # execute SQL
  1396. self.voucher_outstandings = self.cte_query_voucher_amount_and_outstanding.run(as_dict=True)
  1397. def get_voucher_outstandings(
  1398. self,
  1399. vouchers=None,
  1400. common_filter=None,
  1401. posting_date=None,
  1402. min_outstanding=None,
  1403. max_outstanding=None,
  1404. get_payments=False,
  1405. get_invoices=False,
  1406. accounting_dimensions=None,
  1407. ):
  1408. """
  1409. Fetch voucher amount and outstanding amount from Payment Ledger using Database CTE
  1410. vouchers - dict of vouchers to get
  1411. common_filter - array of criterions
  1412. min_outstanding - filter on minimum total outstanding amount
  1413. max_outstanding - filter on maximum total outstanding amount
  1414. get_invoices - only fetch vouchers(ledger entries with +ve outstanding)
  1415. get_payments - only fetch payments(ledger entries with -ve outstanding)
  1416. """
  1417. self.reset()
  1418. self.vouchers = vouchers
  1419. self.common_filter = common_filter or []
  1420. self.dimensions_filter = accounting_dimensions or []
  1421. self.voucher_posting_date = posting_date or []
  1422. self.min_outstanding = min_outstanding
  1423. self.max_outstanding = max_outstanding
  1424. self.get_payments = get_payments
  1425. self.get_invoices = get_invoices
  1426. self.query_for_outstanding()
  1427. return self.voucher_outstandings