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.

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