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ů.
 
 
 
 

588 řádky
17 KiB

  1. # Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
  2. # License: GNU General Public License v3. See license.txt
  3. import functools
  4. import math
  5. import re
  6. import frappe
  7. from frappe import _
  8. from frappe.utils import add_days, add_months, cint, cstr, flt, formatdate, get_first_day, getdate
  9. from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import (
  10. get_accounting_dimensions,
  11. get_dimension_with_children,
  12. )
  13. from erpnext.accounts.report.utils import convert_to_presentation_currency, get_currency
  14. from erpnext.accounts.utils import get_fiscal_year
  15. def get_period_list(
  16. from_fiscal_year,
  17. to_fiscal_year,
  18. period_start_date,
  19. period_end_date,
  20. filter_based_on,
  21. periodicity,
  22. accumulated_values=False,
  23. company=None,
  24. reset_period_on_fy_change=True,
  25. ignore_fiscal_year=False,
  26. ):
  27. """Get a list of dict {"from_date": from_date, "to_date": to_date, "key": key, "label": label}
  28. Periodicity can be (Yearly, Quarterly, Monthly)"""
  29. if filter_based_on == "Fiscal Year":
  30. fiscal_year = get_fiscal_year_data(from_fiscal_year, to_fiscal_year)
  31. validate_fiscal_year(fiscal_year, from_fiscal_year, to_fiscal_year)
  32. year_start_date = getdate(fiscal_year.year_start_date)
  33. year_end_date = getdate(fiscal_year.year_end_date)
  34. else:
  35. validate_dates(period_start_date, period_end_date)
  36. year_start_date = getdate(period_start_date)
  37. year_end_date = getdate(period_end_date)
  38. months_to_add = {"Yearly": 12, "Half-Yearly": 6, "Quarterly": 3, "Monthly": 1}[periodicity]
  39. period_list = []
  40. start_date = year_start_date
  41. months = get_months(year_start_date, year_end_date)
  42. for i in range(cint(math.ceil(months / months_to_add))):
  43. period = frappe._dict({"from_date": start_date})
  44. if i == 0 and filter_based_on == "Date Range":
  45. to_date = add_months(get_first_day(start_date), months_to_add)
  46. else:
  47. to_date = add_months(start_date, months_to_add)
  48. start_date = to_date
  49. # Subtract one day from to_date, as it may be first day in next fiscal year or month
  50. to_date = add_days(to_date, -1)
  51. if to_date <= year_end_date:
  52. # the normal case
  53. period.to_date = to_date
  54. else:
  55. # if a fiscal year ends before a 12 month period
  56. period.to_date = year_end_date
  57. if not ignore_fiscal_year:
  58. period.to_date_fiscal_year = get_fiscal_year(period.to_date, company=company)[0]
  59. period.from_date_fiscal_year_start_date = get_fiscal_year(period.from_date, company=company)[1]
  60. period_list.append(period)
  61. if period.to_date == year_end_date:
  62. break
  63. # common processing
  64. for opts in period_list:
  65. key = opts["to_date"].strftime("%b_%Y").lower()
  66. if periodicity == "Monthly" and not accumulated_values:
  67. label = formatdate(opts["to_date"], "MMM YYYY")
  68. else:
  69. if not accumulated_values:
  70. label = get_label(periodicity, opts["from_date"], opts["to_date"])
  71. else:
  72. if reset_period_on_fy_change:
  73. label = get_label(periodicity, opts.from_date_fiscal_year_start_date, opts["to_date"])
  74. else:
  75. label = get_label(periodicity, period_list[0].from_date, opts["to_date"])
  76. opts.update(
  77. {
  78. "key": key.replace(" ", "_").replace("-", "_"),
  79. "label": label,
  80. "year_start_date": year_start_date,
  81. "year_end_date": year_end_date,
  82. }
  83. )
  84. return period_list
  85. def get_fiscal_year_data(from_fiscal_year, to_fiscal_year):
  86. fiscal_year = frappe.db.sql(
  87. """select min(year_start_date) as year_start_date,
  88. max(year_end_date) as year_end_date from `tabFiscal Year` where
  89. name between %(from_fiscal_year)s and %(to_fiscal_year)s""",
  90. {"from_fiscal_year": from_fiscal_year, "to_fiscal_year": to_fiscal_year},
  91. as_dict=1,
  92. )
  93. return fiscal_year[0] if fiscal_year else {}
  94. def validate_fiscal_year(fiscal_year, from_fiscal_year, to_fiscal_year):
  95. if not fiscal_year.get("year_start_date") or not fiscal_year.get("year_end_date"):
  96. frappe.throw(_("Start Year and End Year are mandatory"))
  97. if getdate(fiscal_year.get("year_end_date")) < getdate(fiscal_year.get("year_start_date")):
  98. frappe.throw(_("End Year cannot be before Start Year"))
  99. def validate_dates(from_date, to_date):
  100. if not from_date or not to_date:
  101. frappe.throw(_("From Date and To Date are mandatory"))
  102. if to_date < from_date:
  103. frappe.throw(_("To Date cannot be less than From Date"))
  104. def get_months(start_date, end_date):
  105. diff = (12 * end_date.year + end_date.month) - (12 * start_date.year + start_date.month)
  106. return diff + 1
  107. def get_label(periodicity, from_date, to_date):
  108. if periodicity == "Yearly":
  109. if formatdate(from_date, "YYYY") == formatdate(to_date, "YYYY"):
  110. label = formatdate(from_date, "YYYY")
  111. else:
  112. label = formatdate(from_date, "YYYY") + "-" + formatdate(to_date, "YYYY")
  113. else:
  114. label = formatdate(from_date, "MMM YY") + "-" + formatdate(to_date, "MMM YY")
  115. return label
  116. def get_data(
  117. company,
  118. root_type,
  119. balance_must_be,
  120. period_list,
  121. filters=None,
  122. accumulated_values=1,
  123. only_current_fiscal_year=True,
  124. ignore_closing_entries=False,
  125. ignore_accumulated_values_for_fy=False,
  126. total=True,
  127. ):
  128. accounts = get_accounts(company, root_type)
  129. if not accounts:
  130. return None
  131. accounts, accounts_by_name, parent_children_map = filter_accounts(accounts)
  132. company_currency = get_appropriate_currency(company, filters)
  133. gl_entries_by_account = {}
  134. for root in frappe.db.sql(
  135. """select lft, rgt from tabAccount
  136. where root_type=%s and ifnull(parent_account, '') = ''""",
  137. root_type,
  138. as_dict=1,
  139. ):
  140. set_gl_entries_by_account(
  141. company,
  142. period_list[0]["year_start_date"] if only_current_fiscal_year else None,
  143. period_list[-1]["to_date"],
  144. root.lft,
  145. root.rgt,
  146. filters,
  147. gl_entries_by_account,
  148. ignore_closing_entries=ignore_closing_entries,
  149. )
  150. calculate_values(
  151. accounts_by_name,
  152. gl_entries_by_account,
  153. period_list,
  154. accumulated_values,
  155. ignore_accumulated_values_for_fy,
  156. )
  157. accumulate_values_into_parents(accounts, accounts_by_name, period_list)
  158. out = prepare_data(accounts, balance_must_be, period_list, company_currency)
  159. out = filter_out_zero_value_rows(out, parent_children_map)
  160. if out and total:
  161. add_total_row(out, root_type, balance_must_be, period_list, company_currency)
  162. return out
  163. def get_appropriate_currency(company, filters=None):
  164. if filters and filters.get("presentation_currency"):
  165. return filters["presentation_currency"]
  166. else:
  167. return frappe.get_cached_value("Company", company, "default_currency")
  168. def calculate_values(
  169. accounts_by_name,
  170. gl_entries_by_account,
  171. period_list,
  172. accumulated_values,
  173. ignore_accumulated_values_for_fy,
  174. ):
  175. for entries in gl_entries_by_account.values():
  176. for entry in entries:
  177. d = accounts_by_name.get(entry.account)
  178. if not d:
  179. frappe.msgprint(
  180. _("Could not retrieve information for {0}.").format(entry.account),
  181. title="Error",
  182. raise_exception=1,
  183. )
  184. for period in period_list:
  185. # check if posting date is within the period
  186. if entry.posting_date <= period.to_date:
  187. if (accumulated_values or entry.posting_date >= period.from_date) and (
  188. not ignore_accumulated_values_for_fy or entry.fiscal_year == period.to_date_fiscal_year
  189. ):
  190. d[period.key] = d.get(period.key, 0.0) + flt(entry.debit) - flt(entry.credit)
  191. if entry.posting_date < period_list[0].year_start_date:
  192. d["opening_balance"] = d.get("opening_balance", 0.0) + flt(entry.debit) - flt(entry.credit)
  193. def accumulate_values_into_parents(accounts, accounts_by_name, period_list):
  194. """accumulate children's values in parent accounts"""
  195. for d in reversed(accounts):
  196. if d.parent_account:
  197. for period in period_list:
  198. accounts_by_name[d.parent_account][period.key] = accounts_by_name[d.parent_account].get(
  199. period.key, 0.0
  200. ) + d.get(period.key, 0.0)
  201. accounts_by_name[d.parent_account]["opening_balance"] = accounts_by_name[d.parent_account].get(
  202. "opening_balance", 0.0
  203. ) + d.get("opening_balance", 0.0)
  204. def prepare_data(accounts, balance_must_be, period_list, company_currency):
  205. data = []
  206. year_start_date = period_list[0]["year_start_date"].strftime("%Y-%m-%d")
  207. year_end_date = period_list[-1]["year_end_date"].strftime("%Y-%m-%d")
  208. for d in accounts:
  209. # add to output
  210. has_value = False
  211. total = 0
  212. row = frappe._dict(
  213. {
  214. "account": _(d.name),
  215. "parent_account": _(d.parent_account) if d.parent_account else "",
  216. "indent": flt(d.indent),
  217. "year_start_date": year_start_date,
  218. "year_end_date": year_end_date,
  219. "currency": company_currency,
  220. "include_in_gross": d.include_in_gross,
  221. "account_type": d.account_type,
  222. "is_group": d.is_group,
  223. "opening_balance": d.get("opening_balance", 0.0) * (1 if balance_must_be == "Debit" else -1),
  224. "account_name": (
  225. "%s - %s" % (_(d.account_number), _(d.account_name))
  226. if d.account_number
  227. else _(d.account_name)
  228. ),
  229. }
  230. )
  231. for period in period_list:
  232. if d.get(period.key) and balance_must_be == "Credit":
  233. # change sign based on Debit or Credit, since calculation is done using (debit - credit)
  234. d[period.key] *= -1
  235. row[period.key] = flt(d.get(period.key, 0.0), 3)
  236. if abs(row[period.key]) >= 0.005:
  237. # ignore zero values
  238. has_value = True
  239. total += flt(row[period.key])
  240. row["has_value"] = has_value
  241. row["total"] = total
  242. data.append(row)
  243. return data
  244. def filter_out_zero_value_rows(data, parent_children_map, show_zero_values=False):
  245. data_with_value = []
  246. for d in data:
  247. if show_zero_values or d.get("has_value"):
  248. data_with_value.append(d)
  249. else:
  250. # show group with zero balance, if there are balances against child
  251. children = [child.name for child in parent_children_map.get(d.get("account")) or []]
  252. if children:
  253. for row in data:
  254. if row.get("account") in children and row.get("has_value"):
  255. data_with_value.append(d)
  256. break
  257. return data_with_value
  258. def add_total_row(out, root_type, balance_must_be, period_list, company_currency):
  259. total_row = {
  260. "account_name": _("Total {0} ({1})").format(_(root_type), _(balance_must_be)),
  261. "account": _("Total {0} ({1})").format(_(root_type), _(balance_must_be)),
  262. "currency": company_currency,
  263. "opening_balance": 0.0,
  264. }
  265. for row in out:
  266. if not row.get("parent_account"):
  267. for period in period_list:
  268. total_row.setdefault(period.key, 0.0)
  269. total_row[period.key] += row.get(period.key, 0.0)
  270. row[period.key] = row.get(period.key, 0.0)
  271. total_row.setdefault("total", 0.0)
  272. total_row["total"] += flt(row["total"])
  273. total_row["opening_balance"] += row["opening_balance"]
  274. row["total"] = ""
  275. if "total" in total_row:
  276. out.append(total_row)
  277. # blank row after Total
  278. out.append({})
  279. def get_accounts(company, root_type):
  280. return frappe.db.sql(
  281. """
  282. select name, account_number, parent_account, lft, rgt, root_type, report_type, account_name, include_in_gross, account_type, is_group, lft, rgt
  283. from `tabAccount`
  284. where company=%s and root_type=%s order by lft""",
  285. (company, root_type),
  286. as_dict=True,
  287. )
  288. def filter_accounts(accounts, depth=20):
  289. parent_children_map = {}
  290. accounts_by_name = {}
  291. for d in accounts:
  292. accounts_by_name[d.name] = d
  293. parent_children_map.setdefault(d.parent_account or None, []).append(d)
  294. filtered_accounts = []
  295. def add_to_list(parent, level):
  296. if level < depth:
  297. children = parent_children_map.get(parent) or []
  298. sort_accounts(children, is_root=True if parent == None else False)
  299. for child in children:
  300. child.indent = level
  301. filtered_accounts.append(child)
  302. add_to_list(child.name, level + 1)
  303. add_to_list(None, 0)
  304. return filtered_accounts, accounts_by_name, parent_children_map
  305. def sort_accounts(accounts, is_root=False, key="name"):
  306. """Sort root types as Asset, Liability, Equity, Income, Expense"""
  307. def compare_accounts(a, b):
  308. if re.split(r"\W+", a[key])[0].isdigit():
  309. # if chart of accounts is numbered, then sort by number
  310. return int(a[key] > b[key]) - int(a[key] < b[key])
  311. elif is_root:
  312. if a.report_type != b.report_type and a.report_type == "Balance Sheet":
  313. return -1
  314. if a.root_type != b.root_type and a.root_type == "Asset":
  315. return -1
  316. if a.root_type == "Liability" and b.root_type == "Equity":
  317. return -1
  318. if a.root_type == "Income" and b.root_type == "Expense":
  319. return -1
  320. else:
  321. # sort by key (number) or name
  322. return int(a[key] > b[key]) - int(a[key] < b[key])
  323. return 1
  324. accounts.sort(key=functools.cmp_to_key(compare_accounts))
  325. def set_gl_entries_by_account(
  326. company,
  327. from_date,
  328. to_date,
  329. root_lft,
  330. root_rgt,
  331. filters,
  332. gl_entries_by_account,
  333. ignore_closing_entries=False,
  334. ):
  335. """Returns a dict like { "account": [gl entries], ... }"""
  336. additional_conditions = get_additional_conditions(from_date, ignore_closing_entries, filters)
  337. accounts = frappe.db.sql_list(
  338. """select name from `tabAccount`
  339. where lft >= %s and rgt <= %s and company = %s""",
  340. (root_lft, root_rgt, company),
  341. )
  342. if accounts:
  343. additional_conditions += " and account in ({})".format(
  344. ", ".join(frappe.db.escape(d) for d in accounts)
  345. )
  346. gl_filters = {
  347. "company": company,
  348. "from_date": from_date,
  349. "to_date": to_date,
  350. "finance_book": cstr(filters.get("finance_book")),
  351. }
  352. if filters.get("include_default_book_entries"):
  353. gl_filters["company_fb"] = frappe.db.get_value("Company", company, "default_finance_book")
  354. for key, value in filters.items():
  355. if value:
  356. gl_filters.update({key: value})
  357. gl_entries = frappe.db.sql(
  358. """
  359. select posting_date, account, debit, credit, is_opening, fiscal_year,
  360. debit_in_account_currency, credit_in_account_currency, account_currency from `tabGL Entry`
  361. where company=%(company)s
  362. {additional_conditions}
  363. and posting_date <= %(to_date)s
  364. and is_cancelled = 0""".format(
  365. additional_conditions=additional_conditions
  366. ),
  367. gl_filters,
  368. as_dict=True,
  369. )
  370. if filters and filters.get("presentation_currency"):
  371. convert_to_presentation_currency(gl_entries, get_currency(filters), filters.get("company"))
  372. for entry in gl_entries:
  373. gl_entries_by_account.setdefault(entry.account, []).append(entry)
  374. return gl_entries_by_account
  375. def get_additional_conditions(from_date, ignore_closing_entries, filters):
  376. additional_conditions = []
  377. accounting_dimensions = get_accounting_dimensions(as_list=False)
  378. if ignore_closing_entries:
  379. additional_conditions.append("ifnull(voucher_type, '')!='Period Closing Voucher'")
  380. if from_date:
  381. additional_conditions.append("posting_date >= %(from_date)s")
  382. if filters:
  383. if filters.get("project"):
  384. if not isinstance(filters.get("project"), list):
  385. filters.project = frappe.parse_json(filters.get("project"))
  386. additional_conditions.append("project in %(project)s")
  387. if filters.get("cost_center"):
  388. filters.cost_center = get_cost_centers_with_children(filters.cost_center)
  389. additional_conditions.append("cost_center in %(cost_center)s")
  390. if filters.get("include_default_book_entries"):
  391. if filters.get("finance_book"):
  392. if filters.get("company_fb") and cstr(filters.get("finance_book")) != cstr(
  393. filters.get("company_fb")
  394. ):
  395. frappe.throw(
  396. _("To use a different finance book, please uncheck 'Include Default Book Entries'")
  397. )
  398. else:
  399. additional_conditions.append("(finance_book in (%(finance_book)s) OR finance_book IS NULL)")
  400. else:
  401. additional_conditions.append("(finance_book in (%(company_fb)s) OR finance_book IS NULL)")
  402. else:
  403. if filters.get("finance_book"):
  404. additional_conditions.append("(finance_book in (%(finance_book)s) OR finance_book IS NULL)")
  405. else:
  406. additional_conditions.append("(finance_book IS NULL)")
  407. if accounting_dimensions:
  408. for dimension in accounting_dimensions:
  409. if filters.get(dimension.fieldname):
  410. if frappe.get_cached_value("DocType", dimension.document_type, "is_tree"):
  411. filters[dimension.fieldname] = get_dimension_with_children(
  412. dimension.document_type, filters.get(dimension.fieldname)
  413. )
  414. additional_conditions.append("{0} in %({0})s".format(dimension.fieldname))
  415. else:
  416. additional_conditions.append("{0} in %({0})s".format(dimension.fieldname))
  417. return " and {}".format(" and ".join(additional_conditions)) if additional_conditions else ""
  418. def get_cost_centers_with_children(cost_centers):
  419. if not isinstance(cost_centers, list):
  420. cost_centers = [d.strip() for d in cost_centers.strip().split(",") if d]
  421. all_cost_centers = []
  422. for d in cost_centers:
  423. if frappe.db.exists("Cost Center", d):
  424. lft, rgt = frappe.db.get_value("Cost Center", d, ["lft", "rgt"])
  425. children = frappe.get_all("Cost Center", filters={"lft": [">=", lft], "rgt": ["<=", rgt]})
  426. all_cost_centers += [c.name for c in children]
  427. else:
  428. frappe.throw(_("Cost Center: {0} does not exist").format(d))
  429. return list(set(all_cost_centers))
  430. def get_columns(periodicity, period_list, accumulated_values=1, company=None):
  431. columns = [
  432. {
  433. "fieldname": "account",
  434. "label": _("Account"),
  435. "fieldtype": "Link",
  436. "options": "Account",
  437. "width": 300,
  438. }
  439. ]
  440. if company:
  441. columns.append(
  442. {
  443. "fieldname": "currency",
  444. "label": _("Currency"),
  445. "fieldtype": "Link",
  446. "options": "Currency",
  447. "hidden": 1,
  448. }
  449. )
  450. for period in period_list:
  451. columns.append(
  452. {
  453. "fieldname": period.key,
  454. "label": period.label,
  455. "fieldtype": "Currency",
  456. "options": "currency",
  457. "width": 150,
  458. }
  459. )
  460. if periodicity != "Yearly":
  461. if not accumulated_values:
  462. columns.append(
  463. {"fieldname": "total", "label": _("Total"), "fieldtype": "Currency", "width": 150}
  464. )
  465. return columns
  466. def get_filtered_list_for_consolidated_report(filters, period_list):
  467. filtered_summary_list = []
  468. for period in period_list:
  469. if period == filters.get("company"):
  470. filtered_summary_list.append(period)
  471. return filtered_summary_list