Du kan inte välja fler än 25 ämnen Ämnen måste starta med en bokstav eller siffra, kan innehålla bindestreck ('-') och vara max 35 tecken långa.
 
 
 
 

650 rader
18 KiB

  1. import frappe
  2. from frappe import _
  3. from frappe.email import sendmail_to_system_managers
  4. from frappe.utils import (
  5. add_days,
  6. add_months,
  7. cint,
  8. date_diff,
  9. flt,
  10. get_first_day,
  11. get_last_day,
  12. get_link_to_form,
  13. getdate,
  14. rounded,
  15. today,
  16. )
  17. from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import (
  18. get_accounting_dimensions,
  19. )
  20. from erpnext.accounts.utils import get_account_currency
  21. def validate_service_stop_date(doc):
  22. """Validates service_stop_date for Purchase Invoice and Sales Invoice"""
  23. enable_check = (
  24. "enable_deferred_revenue" if doc.doctype == "Sales Invoice" else "enable_deferred_expense"
  25. )
  26. old_stop_dates = {}
  27. old_doc = frappe.db.get_all(
  28. "{0} Item".format(doc.doctype), {"parent": doc.name}, ["name", "service_stop_date"]
  29. )
  30. for d in old_doc:
  31. old_stop_dates[d.name] = d.service_stop_date or ""
  32. for item in doc.items:
  33. if not item.get(enable_check):
  34. continue
  35. if item.service_stop_date:
  36. if date_diff(item.service_stop_date, item.service_start_date) < 0:
  37. frappe.throw(_("Service Stop Date cannot be before Service Start Date"))
  38. if date_diff(item.service_stop_date, item.service_end_date) > 0:
  39. frappe.throw(_("Service Stop Date cannot be after Service End Date"))
  40. if (
  41. old_stop_dates
  42. and old_stop_dates.get(item.name)
  43. and item.service_stop_date != old_stop_dates.get(item.name)
  44. ):
  45. frappe.throw(_("Cannot change Service Stop Date for item in row {0}").format(item.idx))
  46. def build_conditions(process_type, account, company):
  47. conditions = ""
  48. deferred_account = (
  49. "item.deferred_revenue_account" if process_type == "Income" else "item.deferred_expense_account"
  50. )
  51. if account:
  52. conditions += "AND %s='%s'" % (deferred_account, account)
  53. elif company:
  54. conditions += f"AND p.company = {frappe.db.escape(company)}"
  55. return conditions
  56. def convert_deferred_expense_to_expense(
  57. deferred_process, start_date=None, end_date=None, conditions=""
  58. ):
  59. # book the expense/income on the last day, but it will be trigger on the 1st of month at 12:00 AM
  60. if not start_date:
  61. start_date = add_months(today(), -1)
  62. if not end_date:
  63. end_date = add_days(today(), -1)
  64. # check for the purchase invoice for which GL entries has to be done
  65. invoices = frappe.db.sql_list(
  66. """
  67. select distinct item.parent
  68. from `tabPurchase Invoice Item` item, `tabPurchase Invoice` p
  69. where item.service_start_date<=%s and item.service_end_date>=%s
  70. and item.enable_deferred_expense = 1 and item.parent=p.name
  71. and item.docstatus = 1 and ifnull(item.amount, 0) > 0
  72. {0}
  73. """.format(
  74. conditions
  75. ),
  76. (end_date, start_date),
  77. ) # nosec
  78. # For each invoice, book deferred expense
  79. for invoice in invoices:
  80. doc = frappe.get_doc("Purchase Invoice", invoice)
  81. book_deferred_income_or_expense(doc, deferred_process, end_date)
  82. if frappe.flags.deferred_accounting_error:
  83. send_mail(deferred_process)
  84. def convert_deferred_revenue_to_income(
  85. deferred_process, start_date=None, end_date=None, conditions=""
  86. ):
  87. # book the expense/income on the last day, but it will be trigger on the 1st of month at 12:00 AM
  88. if not start_date:
  89. start_date = add_months(today(), -1)
  90. if not end_date:
  91. end_date = add_days(today(), -1)
  92. # check for the sales invoice for which GL entries has to be done
  93. invoices = frappe.db.sql_list(
  94. """
  95. select distinct item.parent
  96. from `tabSales Invoice Item` item, `tabSales Invoice` p
  97. where item.service_start_date<=%s and item.service_end_date>=%s
  98. and item.enable_deferred_revenue = 1 and item.parent=p.name
  99. and item.docstatus = 1 and ifnull(item.amount, 0) > 0
  100. {0}
  101. """.format(
  102. conditions
  103. ),
  104. (end_date, start_date),
  105. ) # nosec
  106. for invoice in invoices:
  107. doc = frappe.get_doc("Sales Invoice", invoice)
  108. book_deferred_income_or_expense(doc, deferred_process, end_date)
  109. if frappe.flags.deferred_accounting_error:
  110. send_mail(deferred_process)
  111. def get_booking_dates(doc, item, posting_date=None):
  112. if not posting_date:
  113. posting_date = add_days(today(), -1)
  114. last_gl_entry = False
  115. deferred_account = (
  116. "deferred_revenue_account" if doc.doctype == "Sales Invoice" else "deferred_expense_account"
  117. )
  118. prev_gl_entry = frappe.db.sql(
  119. """
  120. select name, posting_date from `tabGL Entry` where company=%s and account=%s and
  121. voucher_type=%s and voucher_no=%s and voucher_detail_no=%s
  122. and is_cancelled = 0
  123. order by posting_date desc limit 1
  124. """,
  125. (doc.company, item.get(deferred_account), doc.doctype, doc.name, item.name),
  126. as_dict=True,
  127. )
  128. prev_gl_via_je = frappe.db.sql(
  129. """
  130. SELECT p.name, p.posting_date FROM `tabJournal Entry` p, `tabJournal Entry Account` c
  131. WHERE p.name = c.parent and p.company=%s and c.account=%s
  132. and c.reference_type=%s and c.reference_name=%s
  133. and c.reference_detail_no=%s and c.docstatus < 2 order by posting_date desc limit 1
  134. """,
  135. (doc.company, item.get(deferred_account), doc.doctype, doc.name, item.name),
  136. as_dict=True,
  137. )
  138. if prev_gl_via_je:
  139. if (not prev_gl_entry) or (
  140. prev_gl_entry and prev_gl_entry[0].posting_date < prev_gl_via_je[0].posting_date
  141. ):
  142. prev_gl_entry = prev_gl_via_je
  143. if prev_gl_entry:
  144. start_date = getdate(add_days(prev_gl_entry[0].posting_date, 1))
  145. else:
  146. start_date = item.service_start_date
  147. end_date = get_last_day(start_date)
  148. if end_date >= item.service_end_date:
  149. end_date = item.service_end_date
  150. last_gl_entry = True
  151. elif item.service_stop_date and end_date >= item.service_stop_date:
  152. end_date = item.service_stop_date
  153. last_gl_entry = True
  154. if end_date > getdate(posting_date):
  155. end_date = posting_date
  156. if getdate(start_date) <= getdate(end_date):
  157. return start_date, end_date, last_gl_entry
  158. else:
  159. return None, None, None
  160. def calculate_monthly_amount(
  161. doc, item, last_gl_entry, start_date, end_date, total_days, total_booking_days, account_currency
  162. ):
  163. amount, base_amount = 0, 0
  164. if not last_gl_entry:
  165. total_months = (
  166. (item.service_end_date.year - item.service_start_date.year) * 12
  167. + (item.service_end_date.month - item.service_start_date.month)
  168. + 1
  169. )
  170. prorate_factor = flt(date_diff(item.service_end_date, item.service_start_date)) / flt(
  171. date_diff(get_last_day(item.service_end_date), get_first_day(item.service_start_date))
  172. )
  173. actual_months = rounded(total_months * prorate_factor, 1)
  174. already_booked_amount, already_booked_amount_in_account_currency = get_already_booked_amount(
  175. doc, item
  176. )
  177. base_amount = flt(item.base_net_amount / actual_months, item.precision("base_net_amount"))
  178. if base_amount + already_booked_amount > item.base_net_amount:
  179. base_amount = item.base_net_amount - already_booked_amount
  180. if account_currency == doc.company_currency:
  181. amount = base_amount
  182. else:
  183. amount = flt(item.net_amount / actual_months, item.precision("net_amount"))
  184. if amount + already_booked_amount_in_account_currency > item.net_amount:
  185. amount = item.net_amount - already_booked_amount_in_account_currency
  186. if not (get_first_day(start_date) == start_date and get_last_day(end_date) == end_date):
  187. partial_month = flt(date_diff(end_date, start_date)) / flt(
  188. date_diff(get_last_day(end_date), get_first_day(start_date))
  189. )
  190. base_amount = rounded(partial_month, 1) * base_amount
  191. amount = rounded(partial_month, 1) * amount
  192. else:
  193. already_booked_amount, already_booked_amount_in_account_currency = get_already_booked_amount(
  194. doc, item
  195. )
  196. base_amount = flt(
  197. item.base_net_amount - already_booked_amount, item.precision("base_net_amount")
  198. )
  199. if account_currency == doc.company_currency:
  200. amount = base_amount
  201. else:
  202. amount = flt(
  203. item.net_amount - already_booked_amount_in_account_currency, item.precision("net_amount")
  204. )
  205. return amount, base_amount
  206. def calculate_amount(doc, item, last_gl_entry, total_days, total_booking_days, account_currency):
  207. amount, base_amount = 0, 0
  208. if not last_gl_entry:
  209. base_amount = flt(
  210. item.base_net_amount * total_booking_days / flt(total_days), item.precision("base_net_amount")
  211. )
  212. if account_currency == doc.company_currency:
  213. amount = base_amount
  214. else:
  215. amount = flt(
  216. item.net_amount * total_booking_days / flt(total_days), item.precision("net_amount")
  217. )
  218. else:
  219. already_booked_amount, already_booked_amount_in_account_currency = get_already_booked_amount(
  220. doc, item
  221. )
  222. base_amount = flt(
  223. item.base_net_amount - already_booked_amount, item.precision("base_net_amount")
  224. )
  225. if account_currency == doc.company_currency:
  226. amount = base_amount
  227. else:
  228. amount = flt(
  229. item.net_amount - already_booked_amount_in_account_currency, item.precision("net_amount")
  230. )
  231. return amount, base_amount
  232. def get_already_booked_amount(doc, item):
  233. if doc.doctype == "Sales Invoice":
  234. total_credit_debit, total_credit_debit_currency = "debit", "debit_in_account_currency"
  235. deferred_account = "deferred_revenue_account"
  236. else:
  237. total_credit_debit, total_credit_debit_currency = "credit", "credit_in_account_currency"
  238. deferred_account = "deferred_expense_account"
  239. gl_entries_details = frappe.db.sql(
  240. """
  241. select sum({0}) as total_credit, sum({1}) as total_credit_in_account_currency, voucher_detail_no
  242. from `tabGL Entry` where company=%s and account=%s and voucher_type=%s and voucher_no=%s and voucher_detail_no=%s
  243. and is_cancelled = 0
  244. group by voucher_detail_no
  245. """.format(
  246. total_credit_debit, total_credit_debit_currency
  247. ),
  248. (doc.company, item.get(deferred_account), doc.doctype, doc.name, item.name),
  249. as_dict=True,
  250. )
  251. journal_entry_details = frappe.db.sql(
  252. """
  253. SELECT sum(c.{0}) as total_credit, sum(c.{1}) as total_credit_in_account_currency, reference_detail_no
  254. FROM `tabJournal Entry` p , `tabJournal Entry Account` c WHERE p.name = c.parent and
  255. p.company = %s and c.account=%s and c.reference_type=%s and c.reference_name=%s and c.reference_detail_no=%s
  256. and p.docstatus < 2 group by reference_detail_no
  257. """.format(
  258. total_credit_debit, total_credit_debit_currency
  259. ),
  260. (doc.company, item.get(deferred_account), doc.doctype, doc.name, item.name),
  261. as_dict=True,
  262. )
  263. already_booked_amount = gl_entries_details[0].total_credit if gl_entries_details else 0
  264. already_booked_amount += journal_entry_details[0].total_credit if journal_entry_details else 0
  265. if doc.currency == doc.company_currency:
  266. already_booked_amount_in_account_currency = already_booked_amount
  267. else:
  268. already_booked_amount_in_account_currency = (
  269. gl_entries_details[0].total_credit_in_account_currency if gl_entries_details else 0
  270. )
  271. already_booked_amount_in_account_currency += (
  272. journal_entry_details[0].total_credit_in_account_currency if journal_entry_details else 0
  273. )
  274. return already_booked_amount, already_booked_amount_in_account_currency
  275. def book_deferred_income_or_expense(doc, deferred_process, posting_date=None):
  276. enable_check = (
  277. "enable_deferred_revenue" if doc.doctype == "Sales Invoice" else "enable_deferred_expense"
  278. )
  279. accounts_frozen_upto = frappe.get_cached_value("Accounts Settings", "None", "acc_frozen_upto")
  280. def _book_deferred_revenue_or_expense(
  281. item, via_journal_entry, submit_journal_entry, book_deferred_entries_based_on
  282. ):
  283. start_date, end_date, last_gl_entry = get_booking_dates(doc, item, posting_date=posting_date)
  284. if not (start_date and end_date):
  285. return
  286. account_currency = get_account_currency(item.expense_account or item.income_account)
  287. if doc.doctype == "Sales Invoice":
  288. against, project = doc.customer, doc.project
  289. credit_account, debit_account = item.income_account, item.deferred_revenue_account
  290. else:
  291. against, project = doc.supplier, item.project
  292. credit_account, debit_account = item.deferred_expense_account, item.expense_account
  293. total_days = date_diff(item.service_end_date, item.service_start_date) + 1
  294. total_booking_days = date_diff(end_date, start_date) + 1
  295. if book_deferred_entries_based_on == "Months":
  296. amount, base_amount = calculate_monthly_amount(
  297. doc,
  298. item,
  299. last_gl_entry,
  300. start_date,
  301. end_date,
  302. total_days,
  303. total_booking_days,
  304. account_currency,
  305. )
  306. else:
  307. amount, base_amount = calculate_amount(
  308. doc, item, last_gl_entry, total_days, total_booking_days, account_currency
  309. )
  310. if not amount:
  311. return
  312. # check if books nor frozen till endate:
  313. if accounts_frozen_upto and getdate(end_date) <= getdate(accounts_frozen_upto):
  314. end_date = get_last_day(add_days(accounts_frozen_upto, 1))
  315. if via_journal_entry:
  316. book_revenue_via_journal_entry(
  317. doc,
  318. credit_account,
  319. debit_account,
  320. amount,
  321. base_amount,
  322. end_date,
  323. project,
  324. account_currency,
  325. item.cost_center,
  326. item,
  327. deferred_process,
  328. submit_journal_entry,
  329. )
  330. else:
  331. make_gl_entries(
  332. doc,
  333. credit_account,
  334. debit_account,
  335. against,
  336. amount,
  337. base_amount,
  338. end_date,
  339. project,
  340. account_currency,
  341. item.cost_center,
  342. item,
  343. deferred_process,
  344. )
  345. # Returned in case of any errors because it tries to submit the same record again and again in case of errors
  346. if frappe.flags.deferred_accounting_error:
  347. return
  348. if getdate(end_date) < getdate(posting_date) and not last_gl_entry:
  349. _book_deferred_revenue_or_expense(
  350. item, via_journal_entry, submit_journal_entry, book_deferred_entries_based_on
  351. )
  352. via_journal_entry = cint(
  353. frappe.db.get_singles_value("Accounts Settings", "book_deferred_entries_via_journal_entry")
  354. )
  355. submit_journal_entry = cint(
  356. frappe.db.get_singles_value("Accounts Settings", "submit_journal_entries")
  357. )
  358. book_deferred_entries_based_on = frappe.db.get_singles_value(
  359. "Accounts Settings", "book_deferred_entries_based_on"
  360. )
  361. for item in doc.get("items"):
  362. if item.get(enable_check):
  363. _book_deferred_revenue_or_expense(
  364. item, via_journal_entry, submit_journal_entry, book_deferred_entries_based_on
  365. )
  366. def process_deferred_accounting(posting_date=None):
  367. """Converts deferred income/expense into income/expense
  368. Executed via background jobs on every month end"""
  369. if not posting_date:
  370. posting_date = today()
  371. if not cint(
  372. frappe.db.get_singles_value(
  373. "Accounts Settings", "automatically_process_deferred_accounting_entry"
  374. )
  375. ):
  376. return
  377. start_date = add_months(today(), -1)
  378. end_date = add_days(today(), -1)
  379. companies = frappe.get_all("Company")
  380. for company in companies:
  381. for record_type in ("Income", "Expense"):
  382. doc = frappe.get_doc(
  383. dict(
  384. doctype="Process Deferred Accounting",
  385. company=company.name,
  386. posting_date=posting_date,
  387. start_date=start_date,
  388. end_date=end_date,
  389. type=record_type,
  390. )
  391. )
  392. doc.insert()
  393. doc.submit()
  394. def make_gl_entries(
  395. doc,
  396. credit_account,
  397. debit_account,
  398. against,
  399. amount,
  400. base_amount,
  401. posting_date,
  402. project,
  403. account_currency,
  404. cost_center,
  405. item,
  406. deferred_process=None,
  407. ):
  408. # GL Entry for crediting the amount in the deferred expense
  409. from erpnext.accounts.general_ledger import make_gl_entries
  410. if amount == 0:
  411. return
  412. gl_entries = []
  413. gl_entries.append(
  414. doc.get_gl_dict(
  415. {
  416. "account": credit_account,
  417. "against": against,
  418. "credit": base_amount,
  419. "credit_in_account_currency": amount,
  420. "cost_center": cost_center,
  421. "voucher_detail_no": item.name,
  422. "posting_date": posting_date,
  423. "project": project,
  424. "against_voucher_type": "Process Deferred Accounting",
  425. "against_voucher": deferred_process,
  426. },
  427. account_currency,
  428. item=item,
  429. )
  430. )
  431. # GL Entry to debit the amount from the expense
  432. gl_entries.append(
  433. doc.get_gl_dict(
  434. {
  435. "account": debit_account,
  436. "against": against,
  437. "debit": base_amount,
  438. "debit_in_account_currency": amount,
  439. "cost_center": cost_center,
  440. "voucher_detail_no": item.name,
  441. "posting_date": posting_date,
  442. "project": project,
  443. "against_voucher_type": "Process Deferred Accounting",
  444. "against_voucher": deferred_process,
  445. },
  446. account_currency,
  447. item=item,
  448. )
  449. )
  450. if gl_entries:
  451. try:
  452. make_gl_entries(gl_entries, cancel=(doc.docstatus == 2), merge_entries=True)
  453. frappe.db.commit()
  454. except Exception as e:
  455. if frappe.flags.in_test:
  456. doc.log_error(f"Error while processing deferred accounting for Invoice {doc.name}")
  457. raise e
  458. else:
  459. frappe.db.rollback()
  460. doc.log_error(f"Error while processing deferred accounting for Invoice {doc.name}")
  461. frappe.flags.deferred_accounting_error = True
  462. def send_mail(deferred_process):
  463. title = _("Error while processing deferred accounting for {0}").format(deferred_process)
  464. link = get_link_to_form("Process Deferred Accounting", deferred_process)
  465. content = _("Deferred accounting failed for some invoices:") + "\n"
  466. content += _(
  467. "Please check Process Deferred Accounting {0} and submit manually after resolving errors."
  468. ).format(link)
  469. sendmail_to_system_managers(title, content)
  470. def book_revenue_via_journal_entry(
  471. doc,
  472. credit_account,
  473. debit_account,
  474. amount,
  475. base_amount,
  476. posting_date,
  477. project,
  478. account_currency,
  479. cost_center,
  480. item,
  481. deferred_process=None,
  482. submit="No",
  483. ):
  484. if amount == 0:
  485. return
  486. journal_entry = frappe.new_doc("Journal Entry")
  487. journal_entry.posting_date = posting_date
  488. journal_entry.company = doc.company
  489. journal_entry.voucher_type = (
  490. "Deferred Revenue" if doc.doctype == "Sales Invoice" else "Deferred Expense"
  491. )
  492. journal_entry.process_deferred_accounting = deferred_process
  493. debit_entry = {
  494. "account": credit_account,
  495. "credit": base_amount,
  496. "credit_in_account_currency": amount,
  497. "account_currency": account_currency,
  498. "reference_name": doc.name,
  499. "reference_type": doc.doctype,
  500. "reference_detail_no": item.name,
  501. "cost_center": cost_center,
  502. "project": project,
  503. }
  504. credit_entry = {
  505. "account": debit_account,
  506. "debit": base_amount,
  507. "debit_in_account_currency": amount,
  508. "account_currency": account_currency,
  509. "reference_name": doc.name,
  510. "reference_type": doc.doctype,
  511. "reference_detail_no": item.name,
  512. "cost_center": cost_center,
  513. "project": project,
  514. }
  515. for dimension in get_accounting_dimensions():
  516. debit_entry.update({dimension: item.get(dimension)})
  517. credit_entry.update({dimension: item.get(dimension)})
  518. journal_entry.append("accounts", debit_entry)
  519. journal_entry.append("accounts", credit_entry)
  520. try:
  521. journal_entry.save()
  522. if submit:
  523. journal_entry.submit()
  524. frappe.db.commit()
  525. except Exception:
  526. frappe.db.rollback()
  527. doc.log_error(f"Error while processing deferred accounting for Invoice {doc.name}")
  528. frappe.flags.deferred_accounting_error = True
  529. def get_deferred_booking_accounts(doctype, voucher_detail_no, dr_or_cr):
  530. if doctype == "Sales Invoice":
  531. credit_account, debit_account = frappe.db.get_value(
  532. "Sales Invoice Item",
  533. {"name": voucher_detail_no},
  534. ["income_account", "deferred_revenue_account"],
  535. )
  536. else:
  537. credit_account, debit_account = frappe.db.get_value(
  538. "Purchase Invoice Item",
  539. {"name": voucher_detail_no},
  540. ["deferred_expense_account", "expense_account"],
  541. )
  542. if dr_or_cr == "Debit":
  543. return debit_account
  544. else:
  545. return credit_account