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.

374 lines
17 KiB

  1. # © 2016 Julien Coux (Camptocamp)
  2. # Copyright 2020 ForgeFlow S.L. (https://www.forgeflow.com)
  3. # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).
  4. from odoo import models, api
  5. from odoo.tools import float_is_zero
  6. from datetime import date, datetime, timedelta
  7. import pandas as pd
  8. class AgedPartnerBalanceReport(models.AbstractModel):
  9. _name = 'report.account_financial_report.aged_partner_balance'
  10. @api.model
  11. def _initialize_account(self, ag_pb_data, acc_id):
  12. ag_pb_data[acc_id] = {}
  13. ag_pb_data[acc_id]['id'] = acc_id
  14. ag_pb_data[acc_id]['residual'] = 0.0
  15. ag_pb_data[acc_id]['current'] = 0.0
  16. ag_pb_data[acc_id]['30_days'] = 0.0
  17. ag_pb_data[acc_id]['60_days'] = 0.0
  18. ag_pb_data[acc_id]['90_days'] = 0.0
  19. ag_pb_data[acc_id]['120_days'] = 0.0
  20. ag_pb_data[acc_id]['older'] = 0.0
  21. return ag_pb_data
  22. @api.model
  23. def _initialize_partner(self, ag_pb_data, acc_id, prt_id):
  24. ag_pb_data[acc_id][prt_id] = {}
  25. ag_pb_data[acc_id][prt_id]['id'] = acc_id
  26. ag_pb_data[acc_id][prt_id]['residual'] = 0.0
  27. ag_pb_data[acc_id][prt_id]['current'] = 0.0
  28. ag_pb_data[acc_id][prt_id]['30_days'] = 0.0
  29. ag_pb_data[acc_id][prt_id]['60_days'] = 0.0
  30. ag_pb_data[acc_id][prt_id]['90_days'] = 0.0
  31. ag_pb_data[acc_id][prt_id]['120_days'] = 0.0
  32. ag_pb_data[acc_id][prt_id]['older'] = 0.0
  33. ag_pb_data[acc_id][prt_id]['move_lines'] = []
  34. return ag_pb_data
  35. def _get_journals_data(self, journals_ids):
  36. journals = self.env['account.journal'].browse(journals_ids)
  37. journals_data = {}
  38. for journal in journals:
  39. journals_data.update({journal.id: {'id': journal.id,
  40. 'code': journal.code}})
  41. return journals_data
  42. def _get_accounts_data(self, accounts_ids):
  43. accounts = self.env['account.account'].browse(accounts_ids)
  44. accounts_data = {}
  45. for account in accounts:
  46. accounts_data.update({account.id: {'id': account.id,
  47. 'code': account.code,
  48. 'name': account.name}})
  49. return accounts_data
  50. @api.model
  51. def _get_move_lines_domain(self, company_id, account_ids, partner_ids,
  52. only_posted_moves):
  53. domain = [('account_id', 'in', account_ids),
  54. ('company_id', '=', company_id),
  55. ('reconciled', '=', False)]
  56. if partner_ids:
  57. domain += [('partner_id', 'in', partner_ids)]
  58. if only_posted_moves:
  59. domain += [('move_id.state', '=', 'posted')]
  60. return domain
  61. @api.model
  62. def _calculate_amounts(self, ag_pb_data, acc_id, prt_id, residual,
  63. due_date, date_at_object):
  64. ag_pb_data[acc_id]['residual'] += residual
  65. ag_pb_data[acc_id][prt_id]['residual'] += residual
  66. today = date_at_object
  67. if not due_date or today <= due_date:
  68. ag_pb_data[acc_id]['current'] += residual
  69. ag_pb_data[acc_id][prt_id]['current'] += residual
  70. elif today <= due_date + timedelta(days=30):
  71. ag_pb_data[acc_id]['30_days'] += residual
  72. ag_pb_data[acc_id][prt_id]['30_days'] += residual
  73. elif today <= due_date + timedelta(days=60):
  74. ag_pb_data[acc_id]['60_days'] += residual
  75. ag_pb_data[acc_id][prt_id]['60_days'] += residual
  76. elif today <= due_date + timedelta(days=90):
  77. ag_pb_data[acc_id]['90_days'] += residual
  78. ag_pb_data[acc_id][prt_id]['90_days'] += residual
  79. elif today <= due_date + timedelta(days=120):
  80. ag_pb_data[acc_id]['120_days'] += residual
  81. ag_pb_data[acc_id][prt_id]['120_days'] += residual
  82. else:
  83. ag_pb_data[acc_id]['older'] += residual
  84. ag_pb_data[acc_id][prt_id]['older'] += residual
  85. return ag_pb_data
  86. def _get_account_partial_reconciled(self, company_id, date_at_object):
  87. domain = [('max_date', '>=', date_at_object),
  88. ('company_id', '=', company_id)]
  89. fields = ['debit_move_id', 'credit_move_id', 'amount']
  90. accounts_partial_reconcile = \
  91. self.env['account.partial.reconcile'].search_read(
  92. domain=domain,
  93. fields=fields
  94. )
  95. debit_amount = {}
  96. credit_amount = {}
  97. for account_partial_reconcile_data in accounts_partial_reconcile:
  98. debit_move_id = account_partial_reconcile_data['debit_move_id'][0]
  99. credit_move_id = account_partial_reconcile_data['credit_move_id'][0]
  100. if debit_move_id not in debit_amount.keys():
  101. debit_amount[debit_move_id] = 0.0
  102. debit_amount[debit_move_id] += \
  103. account_partial_reconcile_data['amount']
  104. if credit_move_id not in credit_amount.keys():
  105. credit_amount[credit_move_id] = 0.0
  106. credit_amount[credit_move_id] += \
  107. account_partial_reconcile_data['amount']
  108. account_partial_reconcile_data.update({
  109. 'debit_move_id': debit_move_id,
  110. 'credit_move_id': credit_move_id,
  111. })
  112. return accounts_partial_reconcile, debit_amount, credit_amount
  113. @api.model
  114. def _get_new_move_lines_domain(self, new_ml_ids, account_ids, company_id,
  115. partner_ids, only_posted_moves):
  116. domain = [('account_id', 'in', account_ids),
  117. ('company_id', '=', company_id),
  118. ('id', 'in', new_ml_ids)]
  119. if partner_ids:
  120. domain += [('partner_id', 'in', partner_ids)]
  121. if only_posted_moves:
  122. domain += [('move_id.state', '=', 'posted')]
  123. return domain
  124. def _recalculate_move_lines(self, move_lines, debit_ids, credit_ids,
  125. debit_amount, credit_amount, ml_ids,
  126. account_ids, company_id, partner_ids,
  127. only_posted_moves):
  128. reconciled_ids = list(debit_ids) + list(credit_ids)
  129. new_ml_ids = []
  130. for reconciled_id in reconciled_ids:
  131. if reconciled_id not in ml_ids and reconciled_id not in new_ml_ids:
  132. new_ml_ids += [reconciled_id]
  133. new_domain = self._get_new_move_lines_domain(new_ml_ids, account_ids,
  134. company_id, partner_ids,
  135. only_posted_moves)
  136. ml_fields = [
  137. 'id', 'name', 'date', 'move_id', 'journal_id', 'account_id',
  138. 'partner_id', 'amount_residual', 'date_maturity', 'ref',
  139. 'reconciled']
  140. new_move_lines = self.env['account.move.line'].search_read(
  141. domain=new_domain, fields=ml_fields
  142. )
  143. move_lines = move_lines + new_move_lines
  144. for move_line in move_lines:
  145. ml_id = move_line['id']
  146. if ml_id in debit_ids:
  147. move_line['amount_residual'] += debit_amount[ml_id]
  148. if ml_id in credit_ids:
  149. move_line['amount_residual'] -= credit_amount[ml_id]
  150. return move_lines
  151. def _get_move_lines_data(
  152. self, company_id, account_ids, partner_ids, date_at_object,
  153. only_posted_moves, show_move_line_details):
  154. domain = self._get_move_lines_domain(company_id, account_ids,
  155. partner_ids, only_posted_moves)
  156. ml_fields = [
  157. 'id', 'name', 'date', 'move_id', 'journal_id', 'account_id',
  158. 'partner_id', 'amount_residual', 'date_maturity', 'ref',
  159. 'reconciled']
  160. move_lines = self.env['account.move.line'].search_read(
  161. domain=domain, fields=ml_fields
  162. )
  163. ml_ids = set(pd.DataFrame(move_lines).id.to_list())
  164. journals_ids = set()
  165. partners_ids = set()
  166. partners_data = {}
  167. ag_pb_data = {}
  168. if date_at_object < date.today():
  169. acc_partial_rec, debit_amount, credit_amount = \
  170. self._get_account_partial_reconciled(company_id, date_at_object)
  171. if acc_partial_rec:
  172. acc_partial_rec_data = pd.DataFrame(acc_partial_rec)
  173. debit_ids = set(acc_partial_rec_data.debit_move_id.to_list())
  174. credit_ids = set(acc_partial_rec_data.credit_move_id.to_list())
  175. move_lines = self._recalculate_move_lines(
  176. move_lines, debit_ids, credit_ids,
  177. debit_amount, credit_amount, ml_ids, account_ids,
  178. company_id, partner_ids, only_posted_moves
  179. )
  180. moves_lines_to_remove = []
  181. for move_line in move_lines:
  182. if move_line['date'] > date_at_object or \
  183. float_is_zero(move_line['amount_residual'],
  184. precision_digits=2):
  185. moves_lines_to_remove.append(move_line)
  186. if len(moves_lines_to_remove) > 0:
  187. for move_line_to_remove in moves_lines_to_remove:
  188. move_lines.remove(move_line_to_remove)
  189. for move_line in move_lines:
  190. journals_ids.add(move_line['journal_id'][0])
  191. acc_id = move_line['account_id'][0]
  192. if move_line['partner_id']:
  193. prt_id = move_line['partner_id'][0]
  194. prt_name = move_line['partner_id'][1]
  195. else:
  196. prt_id = 0
  197. prt_name = ""
  198. if prt_id not in partners_ids:
  199. partners_data.update({
  200. prt_id: {'id': prt_id, 'name': prt_name}
  201. })
  202. partners_ids.add(prt_id)
  203. if acc_id not in ag_pb_data.keys():
  204. ag_pb_data = self._initialize_account(ag_pb_data, acc_id)
  205. if prt_id not in ag_pb_data[acc_id]:
  206. ag_pb_data = self._initialize_partner(ag_pb_data, acc_id,
  207. prt_id)
  208. move_line_data = {}
  209. if show_move_line_details:
  210. move_line_data.update({
  211. 'date': move_line['date'],
  212. 'entry': move_line['move_id'][1],
  213. 'jnl_id': move_line['journal_id'][0],
  214. 'acc_id': acc_id,
  215. 'partner': prt_name,
  216. 'ref': move_line['ref'],
  217. 'due_date': move_line['date_maturity'],
  218. 'residual': move_line['amount_residual'],
  219. })
  220. ag_pb_data[acc_id][prt_id]['move_lines'].append(move_line_data)
  221. ag_pb_data = self._calculate_amounts(
  222. ag_pb_data, acc_id, prt_id, move_line['amount_residual'],
  223. move_line['date_maturity'], date_at_object)
  224. journals_data = self._get_journals_data(list(journals_ids))
  225. accounts_data = self._get_accounts_data(ag_pb_data.keys())
  226. return ag_pb_data, accounts_data, partners_data, journals_data
  227. @api.model
  228. def _compute_maturity_date(self, ml, date_at_object):
  229. ml.update({
  230. 'current': 0.0,
  231. '30_days': 0.0,
  232. '60_days': 0.0,
  233. '90_days': 0.0,
  234. '120_days': 0.0,
  235. 'older': 0.0,
  236. })
  237. due_date = ml['due_date']
  238. amount = ml['residual']
  239. today = date_at_object
  240. if not due_date or today <= due_date:
  241. ml['current'] += amount
  242. elif today <= due_date + timedelta(days=30):
  243. ml['30_days'] += amount
  244. elif today <= due_date + timedelta(days=60):
  245. ml['60_days'] += amount
  246. elif today <= due_date + timedelta(days=90):
  247. ml['90_days'] += amount
  248. elif today <= due_date + timedelta(days=120):
  249. ml['120_days'] += amount
  250. else:
  251. ml['older'] += amount
  252. def _create_account_list(
  253. self, ag_pb_data, accounts_data, partners_data, journals_data,
  254. show_move_line_details, date_at_oject):
  255. aged_partner_data = []
  256. for account in accounts_data.values():
  257. acc_id = account['id']
  258. account.update({
  259. 'residual': ag_pb_data[acc_id]['residual'],
  260. 'current': ag_pb_data[acc_id]['current'],
  261. '30_days': ag_pb_data[acc_id]['30_days'],
  262. '60_days': ag_pb_data[acc_id]['60_days'],
  263. '90_days': ag_pb_data[acc_id]['90_days'],
  264. '120_days': ag_pb_data[acc_id]['120_days'],
  265. 'older': ag_pb_data[acc_id]['older'],
  266. 'partners': [],
  267. })
  268. for prt_id in ag_pb_data[acc_id]:
  269. if isinstance(prt_id, int):
  270. partner = {
  271. 'name': partners_data[prt_id]['name'],
  272. 'residual': ag_pb_data[acc_id][prt_id]['residual'],
  273. 'current': ag_pb_data[acc_id][prt_id]['current'],
  274. '30_days': ag_pb_data[acc_id][prt_id]['30_days'],
  275. '60_days': ag_pb_data[acc_id][prt_id]['60_days'],
  276. '90_days': ag_pb_data[acc_id][prt_id]['90_days'],
  277. '120_days': ag_pb_data[acc_id][prt_id]['120_days'],
  278. 'older': ag_pb_data[acc_id][prt_id]['older'],
  279. }
  280. if show_move_line_details:
  281. move_lines = []
  282. for ml in ag_pb_data[acc_id][prt_id]['move_lines']:
  283. ml.update({
  284. 'journal': journals_data[ml['jnl_id']]['code'],
  285. 'account': accounts_data[ml['acc_id']]['code'],
  286. })
  287. self._compute_maturity_date(ml, date_at_oject)
  288. move_lines.append(ml)
  289. partner.update({
  290. 'move_lines': move_lines
  291. })
  292. account['partners'].append(partner)
  293. aged_partner_data.append(account)
  294. return aged_partner_data
  295. @api.model
  296. def _calculate_percent(self, aged_partner_data):
  297. for account in aged_partner_data:
  298. if abs(account['residual']) > 0.01:
  299. total = account['residual']
  300. account.update({
  301. 'percent_current': abs(
  302. round((account['current'] / total) * 100, 2)),
  303. 'percent_30_days': abs(
  304. round((account['30_days'] / total) * 100,
  305. 2)),
  306. 'percent_60_days': abs(
  307. round((account['60_days'] / total) * 100,
  308. 2)),
  309. 'percent_90_days': abs(
  310. round((account['90_days'] / total) * 100,
  311. 2)),
  312. 'percent_120_days': abs(
  313. round((account['120_days'] / total) * 100,
  314. 2)),
  315. 'percent_older': abs(
  316. round((account['older'] / total) * 100, 2)),
  317. })
  318. else:
  319. account.update({
  320. 'percent_current': 0.0,
  321. 'percent_30_days': 0.0,
  322. 'percent_60_days': 0.0,
  323. 'percent_90_days': 0.0,
  324. 'percent_120_days': 0.0,
  325. 'percent_older': 0.0,
  326. })
  327. return aged_partner_data
  328. @api.multi
  329. def _get_report_values(self, docids, data):
  330. wizard_id = data['wizard_id']
  331. company = self.env['res.company'].browse(data['company_id'])
  332. company_id = data['company_id']
  333. account_ids = data['account_ids']
  334. partner_ids = data['partner_ids']
  335. date_at = data['date_at']
  336. date_at_object = datetime.strptime(date_at, '%Y-%m-%d').date()
  337. only_posted_moves = data['only_posted_moves']
  338. show_move_line_details = data['show_move_line_details']
  339. ag_pb_data, accounts_data, partners_data, \
  340. journals_data = self._get_move_lines_data(
  341. company_id, account_ids, partner_ids, date_at_object,
  342. only_posted_moves, show_move_line_details)
  343. aged_partner_data = self._create_account_list(
  344. ag_pb_data, accounts_data, partners_data, journals_data,
  345. show_move_line_details, date_at_object)
  346. aged_partner_data = self._calculate_percent(aged_partner_data)
  347. return {
  348. 'doc_ids': [wizard_id],
  349. 'doc_model': 'open.items.report.wizard',
  350. 'docs': self.env['open.items.report.wizard'].browse(wizard_id),
  351. 'company_name': company.display_name,
  352. 'currency_name': company.currency_id.name,
  353. 'date_at': date_at,
  354. 'only_posted_moves': only_posted_moves,
  355. 'aged_partner_balance': aged_partner_data,
  356. 'show_move_lines_details': show_move_line_details,
  357. }