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

581 行
17 KiB

  1. # Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
  2. # License: GNU General Public License v3. See license.txt
  3. import copy
  4. import frappe
  5. from frappe import _
  6. from frappe.model.meta import get_field_precision
  7. from frappe.utils import cint, cstr, flt, formatdate, getdate, now
  8. import erpnext
  9. from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import (
  10. get_accounting_dimensions,
  11. )
  12. from erpnext.accounts.doctype.budget.budget import validate_expense_against_budget
  13. from erpnext.accounts.utils import create_payment_ledger_entry
  14. class ClosedAccountingPeriod(frappe.ValidationError):
  15. pass
  16. def make_gl_entries(
  17. gl_map,
  18. cancel=False,
  19. adv_adj=False,
  20. merge_entries=True,
  21. update_outstanding="Yes",
  22. from_repost=False,
  23. ):
  24. if gl_map:
  25. if not cancel:
  26. validate_accounting_period(gl_map)
  27. validate_disabled_accounts(gl_map)
  28. gl_map = process_gl_map(gl_map, merge_entries)
  29. if gl_map and len(gl_map) > 1:
  30. create_payment_ledger_entry(
  31. gl_map,
  32. cancel=0,
  33. adv_adj=adv_adj,
  34. update_outstanding=update_outstanding,
  35. from_repost=from_repost,
  36. )
  37. save_entries(gl_map, adv_adj, update_outstanding, from_repost)
  38. # Post GL Map proccess there may no be any GL Entries
  39. elif gl_map:
  40. frappe.throw(
  41. _(
  42. "Incorrect number of General Ledger Entries found. You might have selected a wrong Account in the transaction."
  43. )
  44. )
  45. else:
  46. make_reverse_gl_entries(gl_map, adv_adj=adv_adj, update_outstanding=update_outstanding)
  47. def validate_disabled_accounts(gl_map):
  48. accounts = [d.account for d in gl_map if d.account]
  49. Account = frappe.qb.DocType("Account")
  50. disabled_accounts = (
  51. frappe.qb.from_(Account)
  52. .where(Account.name.isin(accounts) & Account.disabled == 1)
  53. .select(Account.name, Account.disabled)
  54. ).run(as_dict=True)
  55. if disabled_accounts:
  56. account_list = "<br>"
  57. account_list += ", ".join([frappe.bold(d.name) for d in disabled_accounts])
  58. frappe.throw(
  59. _("Cannot create accounting entries against disabled accounts: {0}").format(account_list),
  60. title=_("Disabled Account Selected"),
  61. )
  62. def validate_accounting_period(gl_map):
  63. accounting_periods = frappe.db.sql(
  64. """ SELECT
  65. ap.name as name
  66. FROM
  67. `tabAccounting Period` ap, `tabClosed Document` cd
  68. WHERE
  69. ap.name = cd.parent
  70. AND ap.company = %(company)s
  71. AND cd.closed = 1
  72. AND cd.document_type = %(voucher_type)s
  73. AND %(date)s between ap.start_date and ap.end_date
  74. """,
  75. {
  76. "date": gl_map[0].posting_date,
  77. "company": gl_map[0].company,
  78. "voucher_type": gl_map[0].voucher_type,
  79. },
  80. as_dict=1,
  81. )
  82. if accounting_periods:
  83. frappe.throw(
  84. _(
  85. "You cannot create or cancel any accounting entries with in the closed Accounting Period {0}"
  86. ).format(frappe.bold(accounting_periods[0].name)),
  87. ClosedAccountingPeriod,
  88. )
  89. def process_gl_map(gl_map, merge_entries=True, precision=None):
  90. if not gl_map:
  91. return []
  92. gl_map = distribute_gl_based_on_cost_center_allocation(gl_map, precision)
  93. if merge_entries:
  94. gl_map = merge_similar_entries(gl_map, precision)
  95. gl_map = toggle_debit_credit_if_negative(gl_map)
  96. return gl_map
  97. def distribute_gl_based_on_cost_center_allocation(gl_map, precision=None):
  98. cost_center_allocation = get_cost_center_allocation_data(
  99. gl_map[0]["company"], gl_map[0]["posting_date"]
  100. )
  101. if not cost_center_allocation:
  102. return gl_map
  103. new_gl_map = []
  104. for d in gl_map:
  105. cost_center = d.get("cost_center")
  106. # Validate budget against main cost center
  107. validate_expense_against_budget(
  108. d, expense_amount=flt(d.debit, precision) - flt(d.credit, precision)
  109. )
  110. if cost_center and cost_center_allocation.get(cost_center):
  111. for sub_cost_center, percentage in cost_center_allocation.get(cost_center, {}).items():
  112. gle = copy.deepcopy(d)
  113. gle.cost_center = sub_cost_center
  114. for field in ("debit", "credit", "debit_in_account_currency", "credit_in_account_currency"):
  115. gle[field] = flt(flt(d.get(field)) * percentage / 100, precision)
  116. new_gl_map.append(gle)
  117. else:
  118. new_gl_map.append(d)
  119. return new_gl_map
  120. def get_cost_center_allocation_data(company, posting_date):
  121. par = frappe.qb.DocType("Cost Center Allocation")
  122. child = frappe.qb.DocType("Cost Center Allocation Percentage")
  123. records = (
  124. frappe.qb.from_(par)
  125. .inner_join(child)
  126. .on(par.name == child.parent)
  127. .select(par.main_cost_center, child.cost_center, child.percentage)
  128. .where(par.docstatus == 1)
  129. .where(par.company == company)
  130. .where(par.valid_from <= posting_date)
  131. .orderby(par.valid_from, order=frappe.qb.desc)
  132. ).run(as_dict=True)
  133. cc_allocation = frappe._dict()
  134. for d in records:
  135. cc_allocation.setdefault(d.main_cost_center, frappe._dict()).setdefault(
  136. d.cost_center, d.percentage
  137. )
  138. return cc_allocation
  139. def merge_similar_entries(gl_map, precision=None):
  140. merged_gl_map = []
  141. accounting_dimensions = get_accounting_dimensions()
  142. for entry in gl_map:
  143. # if there is already an entry in this account then just add it
  144. # to that entry
  145. same_head = check_if_in_list(entry, merged_gl_map, accounting_dimensions)
  146. if same_head:
  147. same_head.debit = flt(same_head.debit) + flt(entry.debit)
  148. same_head.debit_in_account_currency = flt(same_head.debit_in_account_currency) + flt(
  149. entry.debit_in_account_currency
  150. )
  151. same_head.credit = flt(same_head.credit) + flt(entry.credit)
  152. same_head.credit_in_account_currency = flt(same_head.credit_in_account_currency) + flt(
  153. entry.credit_in_account_currency
  154. )
  155. else:
  156. merged_gl_map.append(entry)
  157. company = gl_map[0].company if gl_map else erpnext.get_default_company()
  158. company_currency = erpnext.get_company_currency(company)
  159. if not precision:
  160. precision = get_field_precision(frappe.get_meta("GL Entry").get_field("debit"), company_currency)
  161. # filter zero debit and credit entries
  162. merged_gl_map = filter(
  163. lambda x: flt(x.debit, precision) != 0
  164. or flt(x.credit, precision) != 0
  165. or (
  166. x.voucher_type == "Journal Entry"
  167. and frappe.get_cached_value("Journal Entry", x.voucher_no, "voucher_type")
  168. == "Exchange Gain Or Loss"
  169. ),
  170. merged_gl_map,
  171. )
  172. merged_gl_map = list(merged_gl_map)
  173. return merged_gl_map
  174. def check_if_in_list(gle, gl_map, dimensions=None):
  175. account_head_fieldnames = [
  176. "voucher_detail_no",
  177. "party",
  178. "against_voucher",
  179. "cost_center",
  180. "against_voucher_type",
  181. "party_type",
  182. "project",
  183. "finance_book",
  184. ]
  185. if dimensions:
  186. account_head_fieldnames = account_head_fieldnames + dimensions
  187. for e in gl_map:
  188. same_head = True
  189. if e.account != gle.account:
  190. same_head = False
  191. continue
  192. for fieldname in account_head_fieldnames:
  193. if cstr(e.get(fieldname)) != cstr(gle.get(fieldname)):
  194. same_head = False
  195. break
  196. if same_head:
  197. return e
  198. def toggle_debit_credit_if_negative(gl_map):
  199. for entry in gl_map:
  200. # toggle debit, credit if negative entry
  201. if flt(entry.debit) < 0:
  202. entry.credit = flt(entry.credit) - flt(entry.debit)
  203. entry.debit = 0.0
  204. if flt(entry.debit_in_account_currency) < 0:
  205. entry.credit_in_account_currency = flt(entry.credit_in_account_currency) - flt(
  206. entry.debit_in_account_currency
  207. )
  208. entry.debit_in_account_currency = 0.0
  209. if flt(entry.credit) < 0:
  210. entry.debit = flt(entry.debit) - flt(entry.credit)
  211. entry.credit = 0.0
  212. if flt(entry.credit_in_account_currency) < 0:
  213. entry.debit_in_account_currency = flt(entry.debit_in_account_currency) - flt(
  214. entry.credit_in_account_currency
  215. )
  216. entry.credit_in_account_currency = 0.0
  217. update_net_values(entry)
  218. return gl_map
  219. def update_net_values(entry):
  220. # In some scenarios net value needs to be shown in the ledger
  221. # This method updates net values as debit or credit
  222. if entry.post_net_value and entry.debit and entry.credit:
  223. if entry.debit > entry.credit:
  224. entry.debit = entry.debit - entry.credit
  225. entry.debit_in_account_currency = (
  226. entry.debit_in_account_currency - entry.credit_in_account_currency
  227. )
  228. entry.credit = 0
  229. entry.credit_in_account_currency = 0
  230. else:
  231. entry.credit = entry.credit - entry.debit
  232. entry.credit_in_account_currency = (
  233. entry.credit_in_account_currency - entry.debit_in_account_currency
  234. )
  235. entry.debit = 0
  236. entry.debit_in_account_currency = 0
  237. def save_entries(gl_map, adv_adj, update_outstanding, from_repost=False):
  238. if not from_repost:
  239. validate_cwip_accounts(gl_map)
  240. process_debit_credit_difference(gl_map)
  241. if gl_map:
  242. check_freezing_date(gl_map[0]["posting_date"], adv_adj)
  243. for entry in gl_map:
  244. make_entry(entry, adv_adj, update_outstanding, from_repost)
  245. def make_entry(args, adv_adj, update_outstanding, from_repost=False):
  246. gle = frappe.new_doc("GL Entry")
  247. gle.update(args)
  248. gle.flags.ignore_permissions = 1
  249. gle.flags.from_repost = from_repost
  250. gle.flags.adv_adj = adv_adj
  251. gle.flags.update_outstanding = update_outstanding or "Yes"
  252. gle.flags.notify_update = False
  253. gle.submit()
  254. if not from_repost and gle.voucher_type != "Period Closing Voucher":
  255. validate_expense_against_budget(args)
  256. def validate_cwip_accounts(gl_map):
  257. """Validate that CWIP account are not used in Journal Entry"""
  258. if gl_map and gl_map[0].voucher_type != "Journal Entry":
  259. return
  260. cwip_enabled = any(
  261. cint(ac.enable_cwip_accounting)
  262. for ac in frappe.db.get_all("Asset Category", "enable_cwip_accounting")
  263. )
  264. if cwip_enabled:
  265. cwip_accounts = [
  266. d[0]
  267. for d in frappe.db.sql(
  268. """select name from tabAccount
  269. where account_type = 'Capital Work in Progress' and is_group=0"""
  270. )
  271. ]
  272. for entry in gl_map:
  273. if entry.account in cwip_accounts:
  274. frappe.throw(
  275. _(
  276. "Account: <b>{0}</b> is capital Work in progress and can not be updated by Journal Entry"
  277. ).format(entry.account)
  278. )
  279. def process_debit_credit_difference(gl_map):
  280. precision = get_field_precision(
  281. frappe.get_meta("GL Entry").get_field("debit"),
  282. currency=frappe.get_cached_value("Company", gl_map[0].company, "default_currency"),
  283. )
  284. voucher_type = gl_map[0].voucher_type
  285. voucher_no = gl_map[0].voucher_no
  286. allowance = get_debit_credit_allowance(voucher_type, precision)
  287. debit_credit_diff = get_debit_credit_difference(gl_map, precision)
  288. if abs(debit_credit_diff) > allowance:
  289. if not (
  290. voucher_type == "Journal Entry"
  291. and frappe.get_cached_value("Journal Entry", voucher_no, "voucher_type")
  292. == "Exchange Gain Or Loss"
  293. ):
  294. raise_debit_credit_not_equal_error(debit_credit_diff, voucher_type, voucher_no)
  295. elif abs(debit_credit_diff) >= (1.0 / (10**precision)):
  296. make_round_off_gle(gl_map, debit_credit_diff, precision)
  297. debit_credit_diff = get_debit_credit_difference(gl_map, precision)
  298. if abs(debit_credit_diff) > allowance:
  299. if not (
  300. voucher_type == "Journal Entry"
  301. and frappe.get_cached_value("Journal Entry", voucher_no, "voucher_type")
  302. == "Exchange Gain Or Loss"
  303. ):
  304. raise_debit_credit_not_equal_error(debit_credit_diff, voucher_type, voucher_no)
  305. def get_debit_credit_difference(gl_map, precision):
  306. debit_credit_diff = 0.0
  307. for entry in gl_map:
  308. entry.debit = flt(entry.debit, precision)
  309. entry.credit = flt(entry.credit, precision)
  310. debit_credit_diff += entry.debit - entry.credit
  311. debit_credit_diff = flt(debit_credit_diff, precision)
  312. return debit_credit_diff
  313. def get_debit_credit_allowance(voucher_type, precision):
  314. if voucher_type in ("Journal Entry", "Payment Entry"):
  315. allowance = 5.0 / (10**precision)
  316. else:
  317. allowance = 0.5
  318. return allowance
  319. def raise_debit_credit_not_equal_error(debit_credit_diff, voucher_type, voucher_no):
  320. frappe.throw(
  321. _("Debit and Credit not equal for {0} #{1}. Difference is {2}.").format(
  322. voucher_type, voucher_no, debit_credit_diff
  323. )
  324. )
  325. def make_round_off_gle(gl_map, debit_credit_diff, precision):
  326. round_off_account, round_off_cost_center = get_round_off_account_and_cost_center(
  327. gl_map[0].company, gl_map[0].voucher_type, gl_map[0].voucher_no
  328. )
  329. round_off_gle = frappe._dict()
  330. round_off_account_exists = False
  331. if gl_map[0].voucher_type != "Period Closing Voucher":
  332. for d in gl_map:
  333. if d.account == round_off_account:
  334. round_off_gle = d
  335. if d.debit:
  336. debit_credit_diff -= flt(d.debit) - flt(d.credit)
  337. else:
  338. debit_credit_diff += flt(d.credit)
  339. round_off_account_exists = True
  340. if round_off_account_exists and abs(debit_credit_diff) < (1.0 / (10**precision)):
  341. gl_map.remove(round_off_gle)
  342. return
  343. if not round_off_gle:
  344. for k in ["voucher_type", "voucher_no", "company", "posting_date", "remarks"]:
  345. round_off_gle[k] = gl_map[0][k]
  346. round_off_gle.update(
  347. {
  348. "account": round_off_account,
  349. "debit_in_account_currency": abs(debit_credit_diff) if debit_credit_diff < 0 else 0,
  350. "credit_in_account_currency": debit_credit_diff if debit_credit_diff > 0 else 0,
  351. "debit": abs(debit_credit_diff) if debit_credit_diff < 0 else 0,
  352. "credit": debit_credit_diff if debit_credit_diff > 0 else 0,
  353. "cost_center": round_off_cost_center,
  354. "party_type": None,
  355. "party": None,
  356. "is_opening": "No",
  357. "against_voucher_type": None,
  358. "against_voucher": None,
  359. }
  360. )
  361. update_accounting_dimensions(round_off_gle)
  362. if not round_off_account_exists:
  363. gl_map.append(round_off_gle)
  364. def update_accounting_dimensions(round_off_gle):
  365. dimensions = get_accounting_dimensions()
  366. meta = frappe.get_meta(round_off_gle["voucher_type"])
  367. has_all_dimensions = True
  368. for dimension in dimensions:
  369. if not meta.has_field(dimension):
  370. has_all_dimensions = False
  371. if dimensions and has_all_dimensions:
  372. dimension_values = frappe.db.get_value(
  373. round_off_gle["voucher_type"], round_off_gle["voucher_no"], dimensions, as_dict=1
  374. )
  375. for dimension in dimensions:
  376. round_off_gle[dimension] = dimension_values.get(dimension)
  377. def get_round_off_account_and_cost_center(
  378. company, voucher_type, voucher_no, use_company_default=False
  379. ):
  380. round_off_account, round_off_cost_center = frappe.get_cached_value(
  381. "Company", company, ["round_off_account", "round_off_cost_center"]
  382. ) or [None, None]
  383. meta = frappe.get_meta(voucher_type)
  384. # Give first preference to parent cost center for round off GLE
  385. if not use_company_default and meta.has_field("cost_center"):
  386. parent_cost_center = frappe.db.get_value(voucher_type, voucher_no, "cost_center")
  387. if parent_cost_center:
  388. round_off_cost_center = parent_cost_center
  389. if not round_off_account:
  390. frappe.throw(_("Please mention Round Off Account in Company"))
  391. if not round_off_cost_center:
  392. frappe.throw(_("Please mention Round Off Cost Center in Company"))
  393. return round_off_account, round_off_cost_center
  394. def make_reverse_gl_entries(
  395. gl_entries=None, voucher_type=None, voucher_no=None, adv_adj=False, update_outstanding="Yes"
  396. ):
  397. """
  398. Get original gl entries of the voucher
  399. and make reverse gl entries by swapping debit and credit
  400. """
  401. if not gl_entries:
  402. gl_entry = frappe.qb.DocType("GL Entry")
  403. gl_entries = (
  404. frappe.qb.from_(gl_entry)
  405. .select("*")
  406. .where(gl_entry.voucher_type == voucher_type)
  407. .where(gl_entry.voucher_no == voucher_no)
  408. .where(gl_entry.is_cancelled == 0)
  409. .for_update()
  410. ).run(as_dict=1)
  411. if gl_entries:
  412. create_payment_ledger_entry(
  413. gl_entries, cancel=1, adv_adj=adv_adj, update_outstanding=update_outstanding
  414. )
  415. validate_accounting_period(gl_entries)
  416. check_freezing_date(gl_entries[0]["posting_date"], adv_adj)
  417. set_as_cancel(gl_entries[0]["voucher_type"], gl_entries[0]["voucher_no"])
  418. for entry in gl_entries:
  419. new_gle = copy.deepcopy(entry)
  420. new_gle["name"] = None
  421. debit = new_gle.get("debit", 0)
  422. credit = new_gle.get("credit", 0)
  423. debit_in_account_currency = new_gle.get("debit_in_account_currency", 0)
  424. credit_in_account_currency = new_gle.get("credit_in_account_currency", 0)
  425. new_gle["debit"] = credit
  426. new_gle["credit"] = debit
  427. new_gle["debit_in_account_currency"] = credit_in_account_currency
  428. new_gle["credit_in_account_currency"] = debit_in_account_currency
  429. new_gle["remarks"] = "On cancellation of " + new_gle["voucher_no"]
  430. new_gle["is_cancelled"] = 1
  431. if new_gle["debit"] or new_gle["credit"]:
  432. make_entry(new_gle, adv_adj, "Yes")
  433. def check_freezing_date(posting_date, adv_adj=False):
  434. """
  435. Nobody can do GL Entries where posting date is before freezing date
  436. except authorized person
  437. Administrator has all the roles so this check will be bypassed if any role is allowed to post
  438. Hence stop admin to bypass if accounts are freezed
  439. """
  440. if not adv_adj:
  441. acc_frozen_upto = frappe.db.get_value("Accounts Settings", None, "acc_frozen_upto")
  442. if acc_frozen_upto:
  443. frozen_accounts_modifier = frappe.db.get_value(
  444. "Accounts Settings", None, "frozen_accounts_modifier"
  445. )
  446. if getdate(posting_date) <= getdate(acc_frozen_upto) and (
  447. frozen_accounts_modifier not in frappe.get_roles() or frappe.session.user == "Administrator"
  448. ):
  449. frappe.throw(
  450. _("You are not authorized to add or update entries before {0}").format(
  451. formatdate(acc_frozen_upto)
  452. )
  453. )
  454. def set_as_cancel(voucher_type, voucher_no):
  455. """
  456. Set is_cancelled=1 in all original gl entries for the voucher
  457. """
  458. frappe.db.sql(
  459. """UPDATE `tabGL Entry` SET is_cancelled = 1,
  460. modified=%s, modified_by=%s
  461. where voucher_type=%s and voucher_no=%s and is_cancelled = 0""",
  462. (now(), frappe.session.user, voucher_type, voucher_no),
  463. )