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.

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