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.

434 lines
17 KiB

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