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.

440 lines
17 KiB

5 years ago
  1. # Copyright 2018 Eficent Business and IT Consulting Services S.L.
  2. # (http://www.eficent.com)
  3. # License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html).
  4. from datetime import datetime, timedelta
  5. from odoo import _, api, fields, models
  6. from odoo.tools.misc import DEFAULT_SERVER_DATE_FORMAT
  7. class ReportStatementCommon(models.AbstractModel):
  8. """Abstract Report Statement for use in other models"""
  9. _name = "statement.common"
  10. _description = "Statement Reports Common"
  11. def _get_invoice_address(self, part):
  12. inv_addr_id = part.address_get(["invoice"]).get("invoice", part.id)
  13. return self.env["res.partner"].browse(inv_addr_id)
  14. def _format_date_to_partner_lang(
  15. self, date, date_format=DEFAULT_SERVER_DATE_FORMAT
  16. ):
  17. if isinstance(date, str):
  18. date = datetime.strptime(date, DEFAULT_SERVER_DATE_FORMAT)
  19. return date.strftime(date_format) if date else ""
  20. def _get_account_display_lines(
  21. self, company_id, partner_ids, date_start, date_end, account_type
  22. ):
  23. raise NotImplementedError
  24. def _get_account_initial_balance(
  25. self, company_id, partner_ids, date_start, account_type
  26. ):
  27. return {}
  28. def _show_buckets_sql_q1(self, partners, date_end, account_type):
  29. return str(
  30. self._cr.mogrify(
  31. """
  32. SELECT l.partner_id, l.currency_id, l.company_id, l.move_id,
  33. CASE WHEN l.balance > 0.0
  34. THEN l.balance - sum(coalesce(pd.amount, 0.0))
  35. ELSE l.balance + sum(coalesce(pc.amount, 0.0))
  36. END AS open_due,
  37. CASE WHEN l.balance > 0.0
  38. THEN l.amount_currency - sum(coalesce(pd.amount_currency, 0.0))
  39. ELSE l.amount_currency + sum(coalesce(pc.amount_currency, 0.0))
  40. END AS open_due_currency,
  41. CASE WHEN l.date_maturity is null
  42. THEN l.date
  43. ELSE l.date_maturity
  44. END as date_maturity
  45. FROM account_move_line l
  46. JOIN account_account_type at ON (at.id = l.user_type_id)
  47. JOIN account_move m ON (l.move_id = m.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.multi
  271. def _get_report_values(self, docids, data):
  272. """
  273. @return: returns a dict of parameters to pass to qweb report.
  274. the most important pair is {'data': res} which contains all
  275. the data for each partner. It is structured like:
  276. {partner_id: {
  277. 'start': date string,
  278. 'end': date_string,
  279. 'today': date_string
  280. 'currencies': {
  281. currency_id: {
  282. 'lines': [{'date': date string, ...}, ...],
  283. 'balance_forward': float,
  284. 'amount_due': float,
  285. 'buckets': {
  286. 'p1': float, 'p2': ...
  287. }
  288. }
  289. }
  290. }
  291. """
  292. company_id = data["company_id"]
  293. partner_ids = data["partner_ids"]
  294. date_start = data.get("date_start")
  295. if date_start and isinstance(date_start, str):
  296. date_start = datetime.strptime(
  297. date_start, DEFAULT_SERVER_DATE_FORMAT
  298. ).date()
  299. date_end = data["date_end"]
  300. if isinstance(date_end, str):
  301. date_end = datetime.strptime(date_end, DEFAULT_SERVER_DATE_FORMAT).date()
  302. account_type = data["account_type"]
  303. aging_type = data["aging_type"]
  304. today = fields.Date.today()
  305. amount_field = data.get("amount_field", "amount")
  306. # There should be relatively few of these, so to speed performance
  307. # we cache them - default needed if partner lang not set
  308. self._cr.execute(
  309. """
  310. SELECT p.id, l.date_format
  311. FROM res_partner p LEFT JOIN res_lang l ON p.lang=l.code
  312. WHERE p.id IN %(partner_ids)s
  313. """,
  314. {"partner_ids": tuple(partner_ids)},
  315. )
  316. date_formats = {r[0]: r[1] for r in self._cr.fetchall()}
  317. default_fmt = self.env["res.lang"]._lang_get(self.env.user.lang).date_format
  318. currencies = {x.id: x for x in self.env["res.currency"].search([])}
  319. res = {}
  320. # get base data
  321. lines = self._get_account_display_lines(
  322. company_id, partner_ids, date_start, date_end, account_type
  323. )
  324. balances_forward = self._get_account_initial_balance(
  325. company_id, partner_ids, date_start, account_type
  326. )
  327. if data["show_aging_buckets"]:
  328. buckets = self._get_account_show_buckets(
  329. company_id, partner_ids, date_end, account_type, aging_type
  330. )
  331. bucket_labels = self._get_bucket_labels(date_end, aging_type)
  332. else:
  333. bucket_labels = {}
  334. # organise and format for report
  335. format_date = self._format_date_to_partner_lang
  336. partners_to_remove = set()
  337. for partner_id in partner_ids:
  338. res[partner_id] = {
  339. "today": format_date(today, date_formats.get(partner_id, default_fmt)),
  340. "start": format_date(
  341. date_start, date_formats.get(partner_id, default_fmt)
  342. ),
  343. "end": format_date(date_end, date_formats.get(partner_id, default_fmt)),
  344. "currencies": {},
  345. }
  346. currency_dict = res[partner_id]["currencies"]
  347. for line in balances_forward.get(partner_id, []):
  348. (
  349. currency_dict[line["currency_id"]],
  350. currencies,
  351. ) = self._get_line_currency_defaults(
  352. line["currency_id"], currencies, line["balance"]
  353. )
  354. for line in lines[partner_id]:
  355. if line["currency_id"] not in currency_dict:
  356. (
  357. currency_dict[line["currency_id"]],
  358. currencies,
  359. ) = self._get_line_currency_defaults(
  360. line["currency_id"], currencies, 0.0
  361. )
  362. line_currency = currency_dict[line["currency_id"]]
  363. if not line["blocked"]:
  364. line_currency["amount_due"] += line[amount_field]
  365. line["balance"] = line_currency["amount_due"]
  366. line["date"] = format_date(
  367. line["date"], date_formats.get(partner_id, default_fmt)
  368. )
  369. line["date_maturity"] = format_date(
  370. line["date_maturity"], date_formats.get(partner_id, default_fmt)
  371. )
  372. line_currency["lines"].append(line)
  373. if data["show_aging_buckets"]:
  374. for line in buckets[partner_id]:
  375. if line["currency_id"] not in currency_dict:
  376. (
  377. currency_dict[line["currency_id"]],
  378. currencies,
  379. ) = self._get_line_currency_defaults(
  380. line["currency_id"], currencies, 0.0
  381. )
  382. line_currency = currency_dict[line["currency_id"]]
  383. line_currency["buckets"] = line
  384. if len(partner_ids) > 1:
  385. values = currency_dict.values()
  386. if not any([v["lines"] or v["balance_forward"] for v in values]):
  387. if data["filter_non_due_partners"]:
  388. partners_to_remove.add(partner_id)
  389. continue
  390. else:
  391. res[partner_id]["no_entries"] = True
  392. if data["filter_negative_balances"]:
  393. if not all([v["amount_due"] >= 0.0 for v in values]):
  394. partners_to_remove.add(partner_id)
  395. for partner in partners_to_remove:
  396. del res[partner]
  397. partner_ids.remove(partner)
  398. return {
  399. "doc_ids": partner_ids,
  400. "doc_model": "res.partner",
  401. "docs": self.env["res.partner"].browse(partner_ids),
  402. "data": res,
  403. "company": self.env["res.company"].browse(company_id),
  404. "Currencies": currencies,
  405. "account_type": account_type,
  406. "bucket_labels": bucket_labels,
  407. "get_inv_addr": self._get_invoice_address,
  408. }