You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

441 lines
17 KiB

5 years ago
  1. # Copyright 2018 ForgeFlow, S.L. (https://www.forgeflow.com)
  2. # License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html).
  3. from datetime import datetime, timedelta
  4. from odoo import _, api, fields, models
  5. from odoo.tools.misc import DEFAULT_SERVER_DATE_FORMAT
  6. class ReportStatementCommon(models.AbstractModel):
  7. """Abstract Report Statement for use in other models"""
  8. _name = "statement.common"
  9. _description = "Statement Reports Common"
  10. def _get_invoice_address(self, part):
  11. inv_addr_id = part.address_get(["invoice"]).get("invoice", part.id)
  12. return self.env["res.partner"].browse(inv_addr_id)
  13. def _format_date_to_partner_lang(
  14. self, date, date_format=DEFAULT_SERVER_DATE_FORMAT
  15. ):
  16. if isinstance(date, str):
  17. date = datetime.strptime(date, DEFAULT_SERVER_DATE_FORMAT)
  18. return date.strftime(date_format) if date else ""
  19. def _get_account_display_lines(
  20. self, company_id, partner_ids, date_start, date_end, account_type
  21. ):
  22. raise NotImplementedError
  23. def _get_account_initial_balance(
  24. self, company_id, partner_ids, date_start, account_type
  25. ):
  26. return {}
  27. def _show_buckets_sql_q1(self, partners, date_end, account_type):
  28. return str(
  29. self._cr.mogrify(
  30. """
  31. SELECT l.partner_id, l.currency_id, l.company_id, l.move_id,
  32. CASE WHEN l.balance > 0.0
  33. THEN l.balance - sum(coalesce(pd.amount, 0.0))
  34. ELSE l.balance + sum(coalesce(pc.amount, 0.0))
  35. END AS open_due,
  36. CASE WHEN l.balance > 0.0
  37. THEN l.amount_currency - sum(coalesce(pd.debit_amount_currency, 0.0))
  38. ELSE l.amount_currency + sum(coalesce(pc.credit_amount_currency, 0.0))
  39. END AS open_due_currency,
  40. CASE WHEN l.date_maturity is null
  41. THEN l.date
  42. ELSE l.date_maturity
  43. END as date_maturity
  44. FROM account_move_line l
  45. JOIN account_move m ON (l.move_id = m.id)
  46. JOIN account_account aa ON (aa.id = l.account_id)
  47. JOIN account_account_type at ON (at.id = aa.user_type_id)
  48. LEFT JOIN (SELECT pr.*
  49. FROM account_partial_reconcile pr
  50. INNER JOIN account_move_line l2
  51. ON pr.credit_move_id = l2.id
  52. WHERE l2.date <= %(date_end)s
  53. ) as pd ON pd.debit_move_id = l.id
  54. LEFT JOIN (SELECT pr.*
  55. FROM account_partial_reconcile pr
  56. INNER JOIN account_move_line l2
  57. ON pr.debit_move_id = l2.id
  58. WHERE l2.date <= %(date_end)s
  59. ) as pc ON pc.credit_move_id = l.id
  60. WHERE l.partner_id IN %(partners)s AND at.type = %(account_type)s
  61. AND (
  62. (pd.id IS NOT NULL AND
  63. pd.max_date <= %(date_end)s) OR
  64. (pc.id IS NOT NULL AND
  65. pc.max_date <= %(date_end)s) OR
  66. (pd.id IS NULL AND pc.id IS NULL)
  67. ) AND l.date <= %(date_end)s AND not l.blocked
  68. AND m.state IN ('posted')
  69. GROUP BY l.partner_id, l.currency_id, l.date, l.date_maturity,
  70. l.amount_currency, l.balance, l.move_id,
  71. l.company_id, l.id
  72. """,
  73. locals(),
  74. ),
  75. "utf-8",
  76. )
  77. def _show_buckets_sql_q2(self, date_end, minus_30, minus_60, minus_90, minus_120):
  78. return str(
  79. self._cr.mogrify(
  80. """
  81. SELECT partner_id, currency_id, date_maturity, open_due,
  82. open_due_currency, move_id, company_id,
  83. CASE
  84. WHEN %(date_end)s <= date_maturity AND currency_id is null
  85. THEN open_due
  86. WHEN %(date_end)s <= date_maturity AND currency_id is not null
  87. THEN open_due_currency
  88. ELSE 0.0
  89. END as current,
  90. CASE
  91. WHEN %(minus_30)s < date_maturity
  92. AND date_maturity < %(date_end)s
  93. AND currency_id is null
  94. THEN open_due
  95. WHEN %(minus_30)s < date_maturity
  96. AND date_maturity < %(date_end)s
  97. AND currency_id is not null
  98. THEN open_due_currency
  99. ELSE 0.0
  100. END as b_1_30,
  101. CASE
  102. WHEN %(minus_60)s < date_maturity
  103. AND date_maturity <= %(minus_30)s
  104. AND currency_id is null
  105. THEN open_due
  106. WHEN %(minus_60)s < date_maturity
  107. AND date_maturity <= %(minus_30)s
  108. AND currency_id is not null
  109. THEN open_due_currency
  110. ELSE 0.0
  111. END as b_30_60,
  112. CASE
  113. WHEN %(minus_90)s < date_maturity
  114. AND date_maturity <= %(minus_60)s
  115. AND currency_id is null
  116. THEN open_due
  117. WHEN %(minus_90)s < date_maturity
  118. AND date_maturity <= %(minus_60)s
  119. AND currency_id is not null
  120. THEN open_due_currency
  121. ELSE 0.0
  122. END as b_60_90,
  123. CASE
  124. WHEN %(minus_120)s < date_maturity
  125. AND date_maturity <= %(minus_90)s
  126. AND currency_id is null
  127. THEN open_due
  128. WHEN %(minus_120)s < date_maturity
  129. AND date_maturity <= %(minus_90)s
  130. AND currency_id is not null
  131. THEN open_due_currency
  132. ELSE 0.0
  133. END as b_90_120,
  134. CASE
  135. WHEN date_maturity <= %(minus_120)s
  136. AND currency_id is null
  137. THEN open_due
  138. WHEN date_maturity <= %(minus_120)s
  139. AND currency_id is not null
  140. THEN open_due_currency
  141. ELSE 0.0
  142. END as b_over_120
  143. FROM Q1
  144. GROUP BY partner_id, currency_id, date_maturity, open_due,
  145. open_due_currency, move_id, company_id
  146. """,
  147. locals(),
  148. ),
  149. "utf-8",
  150. )
  151. def _show_buckets_sql_q3(self, company_id):
  152. return str(
  153. self._cr.mogrify(
  154. """
  155. SELECT Q2.partner_id, current, b_1_30, b_30_60, b_60_90, b_90_120,
  156. b_over_120,
  157. COALESCE(Q2.currency_id, c.currency_id) AS currency_id
  158. FROM Q2
  159. JOIN res_company c ON (c.id = Q2.company_id)
  160. WHERE c.id = %(company_id)s
  161. """,
  162. locals(),
  163. ),
  164. "utf-8",
  165. )
  166. def _show_buckets_sql_q4(self):
  167. return """
  168. SELECT partner_id, currency_id, sum(current) as current,
  169. sum(b_1_30) as b_1_30,
  170. sum(b_30_60) as b_30_60,
  171. sum(b_60_90) as b_60_90,
  172. sum(b_90_120) as b_90_120,
  173. sum(b_over_120) as b_over_120
  174. FROM Q3
  175. GROUP BY partner_id, currency_id
  176. """
  177. def _get_bucket_dates(self, date_end, aging_type):
  178. return getattr(
  179. self, "_get_bucket_dates_%s" % aging_type, self._get_bucket_dates_days
  180. )(date_end)
  181. def _get_bucket_dates_days(self, date_end):
  182. return {
  183. "date_end": date_end,
  184. "minus_30": date_end - timedelta(days=30),
  185. "minus_60": date_end - timedelta(days=60),
  186. "minus_90": date_end - timedelta(days=90),
  187. "minus_120": date_end - timedelta(days=120),
  188. }
  189. def _get_bucket_dates_months(self, date_end):
  190. res = {}
  191. d = date_end
  192. for k in ("date_end", "minus_30", "minus_60", "minus_90", "minus_120"):
  193. res[k] = d
  194. d = d.replace(day=1) - timedelta(days=1)
  195. return res
  196. def _get_account_show_buckets(
  197. self, company_id, partner_ids, date_end, account_type, aging_type
  198. ):
  199. buckets = dict(map(lambda x: (x, []), partner_ids))
  200. partners = tuple(partner_ids)
  201. full_dates = self._get_bucket_dates(date_end, aging_type)
  202. # pylint: disable=E8103
  203. # All input queries are properly escaped - false positive
  204. self.env.cr.execute(
  205. """
  206. WITH Q1 AS (%s),
  207. Q2 AS (%s),
  208. Q3 AS (%s),
  209. Q4 AS (%s)
  210. SELECT partner_id, currency_id, current, b_1_30, b_30_60, b_60_90,
  211. b_90_120, b_over_120,
  212. current+b_1_30+b_30_60+b_60_90+b_90_120+b_over_120
  213. AS balance
  214. FROM Q4
  215. GROUP BY partner_id, currency_id, current, b_1_30, b_30_60,
  216. b_60_90, b_90_120, b_over_120"""
  217. % (
  218. self._show_buckets_sql_q1(partners, date_end, account_type),
  219. self._show_buckets_sql_q2(
  220. full_dates["date_end"],
  221. full_dates["minus_30"],
  222. full_dates["minus_60"],
  223. full_dates["minus_90"],
  224. full_dates["minus_120"],
  225. ),
  226. self._show_buckets_sql_q3(company_id),
  227. self._show_buckets_sql_q4(),
  228. )
  229. )
  230. for row in self.env.cr.dictfetchall():
  231. buckets[row.pop("partner_id")].append(row)
  232. return buckets
  233. def _get_bucket_labels(self, date_end, aging_type):
  234. return getattr(
  235. self, "_get_bucket_labels_%s" % aging_type, self._get_bucket_dates_days
  236. )(date_end)
  237. def _get_bucket_labels_days(self, date_end):
  238. return [
  239. _("Current"),
  240. _("1 - 30 Days"),
  241. _("31 - 60 Days"),
  242. _("61 - 90 Days"),
  243. _("91 - 120 Days"),
  244. _("121 Days +"),
  245. _("Total"),
  246. ]
  247. def _get_bucket_labels_months(self, date_end):
  248. return [
  249. _("Current"),
  250. _("1 Month"),
  251. _("2 Months"),
  252. _("3 Months"),
  253. _("4 Months"),
  254. _("Older"),
  255. _("Total"),
  256. ]
  257. def _get_line_currency_defaults(self, currency_id, currencies, balance_forward):
  258. if currency_id not in currencies:
  259. # This will only happen if currency is inactive
  260. currencies[currency_id] = self.env["res.currency"].browse(currency_id)
  261. return (
  262. {
  263. "lines": [],
  264. "buckets": [],
  265. "balance_forward": balance_forward,
  266. "amount_due": balance_forward,
  267. },
  268. currencies,
  269. )
  270. @api.model
  271. def _get_report_values(self, docids, data=None):
  272. # flake8: noqa: C901
  273. """
  274. @return: returns a dict of parameters to pass to qweb report.
  275. the most important pair is {'data': res} which contains all
  276. the data for each partner. It is structured like:
  277. {partner_id: {
  278. 'start': date string,
  279. 'end': date_string,
  280. 'today': date_string
  281. 'currencies': {
  282. currency_id: {
  283. 'lines': [{'date': date string, ...}, ...],
  284. 'balance_forward': float,
  285. 'amount_due': float,
  286. 'buckets': {
  287. 'p1': float, 'p2': ...
  288. }
  289. }
  290. }
  291. }
  292. """
  293. company_id = data["company_id"]
  294. partner_ids = data["partner_ids"]
  295. date_start = data.get("date_start")
  296. if date_start and isinstance(date_start, str):
  297. date_start = datetime.strptime(
  298. date_start, DEFAULT_SERVER_DATE_FORMAT
  299. ).date()
  300. date_end = data["date_end"]
  301. if isinstance(date_end, str):
  302. date_end = datetime.strptime(date_end, DEFAULT_SERVER_DATE_FORMAT).date()
  303. account_type = data["account_type"]
  304. aging_type = data["aging_type"]
  305. today = fields.Date.today()
  306. amount_field = data.get("amount_field", "amount")
  307. # There should be relatively few of these, so to speed performance
  308. # we cache them - default needed if partner lang not set
  309. self._cr.execute(
  310. """
  311. SELECT p.id, l.date_format
  312. FROM res_partner p LEFT JOIN res_lang l ON p.lang=l.code
  313. WHERE p.id IN %(partner_ids)s
  314. """,
  315. {"partner_ids": tuple(partner_ids)},
  316. )
  317. date_formats = {r[0]: r[1] for r in self._cr.fetchall()}
  318. default_fmt = self.env["res.lang"]._lang_get(self.env.user.lang).date_format
  319. currencies = {x.id: x for x in self.env["res.currency"].search([])}
  320. res = {}
  321. # get base data
  322. lines = self._get_account_display_lines(
  323. company_id, partner_ids, date_start, date_end, account_type
  324. )
  325. balances_forward = self._get_account_initial_balance(
  326. company_id, partner_ids, date_start, account_type
  327. )
  328. if data["show_aging_buckets"]:
  329. buckets = self._get_account_show_buckets(
  330. company_id, partner_ids, date_end, account_type, aging_type
  331. )
  332. bucket_labels = self._get_bucket_labels(date_end, aging_type)
  333. else:
  334. bucket_labels = {}
  335. # organise and format for report
  336. format_date = self._format_date_to_partner_lang
  337. partners_to_remove = set()
  338. for partner_id in partner_ids:
  339. res[partner_id] = {
  340. "today": format_date(today, date_formats.get(partner_id, default_fmt)),
  341. "start": format_date(
  342. date_start, date_formats.get(partner_id, default_fmt)
  343. ),
  344. "end": format_date(date_end, date_formats.get(partner_id, default_fmt)),
  345. "currencies": {},
  346. }
  347. currency_dict = res[partner_id]["currencies"]
  348. for line in balances_forward.get(partner_id, []):
  349. (
  350. currency_dict[line["currency_id"]],
  351. currencies,
  352. ) = self._get_line_currency_defaults(
  353. line["currency_id"], currencies, line["balance"]
  354. )
  355. for line in lines[partner_id]:
  356. if line["currency_id"] not in currency_dict:
  357. (
  358. currency_dict[line["currency_id"]],
  359. currencies,
  360. ) = self._get_line_currency_defaults(
  361. line["currency_id"], currencies, 0.0
  362. )
  363. line_currency = currency_dict[line["currency_id"]]
  364. if not line["blocked"]:
  365. line_currency["amount_due"] += line[amount_field]
  366. line["balance"] = line_currency["amount_due"]
  367. line["date"] = format_date(
  368. line["date"], date_formats.get(partner_id, default_fmt)
  369. )
  370. line["date_maturity"] = format_date(
  371. line["date_maturity"], date_formats.get(partner_id, default_fmt)
  372. )
  373. line_currency["lines"].append(line)
  374. if data["show_aging_buckets"]:
  375. for line in buckets[partner_id]:
  376. if line["currency_id"] not in currency_dict:
  377. (
  378. currency_dict[line["currency_id"]],
  379. currencies,
  380. ) = self._get_line_currency_defaults(
  381. line["currency_id"], currencies, 0.0
  382. )
  383. line_currency = currency_dict[line["currency_id"]]
  384. line_currency["buckets"] = line
  385. if len(partner_ids) > 1:
  386. values = currency_dict.values()
  387. if not any([v["lines"] or v["balance_forward"] for v in values]):
  388. if data["filter_non_due_partners"]:
  389. partners_to_remove.add(partner_id)
  390. continue
  391. else:
  392. res[partner_id]["no_entries"] = True
  393. if data["filter_negative_balances"]:
  394. if not all([v["amount_due"] >= 0.0 for v in values]):
  395. partners_to_remove.add(partner_id)
  396. for partner in partners_to_remove:
  397. del res[partner]
  398. partner_ids.remove(partner)
  399. return {
  400. "doc_ids": partner_ids,
  401. "doc_model": "res.partner",
  402. "docs": self.env["res.partner"].browse(partner_ids),
  403. "data": res,
  404. "company": self.env["res.company"].browse(company_id),
  405. "Currencies": currencies,
  406. "account_type": account_type,
  407. "bucket_labels": bucket_labels,
  408. "get_inv_addr": self._get_invoice_address,
  409. }