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.

460 lines
18 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 datetime import date, datetime, timedelta
  5. import pandas as pd
  6. from odoo import api, models
  7. from odoo.tools import float_is_zero
  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, "code": journal.code}})
  40. return journals_data
  41. def _get_accounts_data(self, accounts_ids):
  42. accounts = self.env["account.account"].browse(accounts_ids)
  43. accounts_data = {}
  44. for account in accounts:
  45. accounts_data.update(
  46. {
  47. account.id: {
  48. "id": account.id,
  49. "code": account.code,
  50. "name": account.name,
  51. }
  52. }
  53. )
  54. return accounts_data
  55. @api.model
  56. def _get_move_lines_domain(
  57. self, company_id, account_ids, partner_ids, only_posted_moves
  58. ):
  59. domain = [
  60. ("account_id", "in", account_ids),
  61. ("company_id", "=", company_id),
  62. ("reconciled", "=", False),
  63. ]
  64. if partner_ids:
  65. domain += [("partner_id", "in", partner_ids)]
  66. if only_posted_moves:
  67. domain += [("move_id.state", "=", "posted")]
  68. return domain
  69. @api.model
  70. def _calculate_amounts(
  71. self, ag_pb_data, acc_id, prt_id, residual, due_date, date_at_object
  72. ):
  73. ag_pb_data[acc_id]["residual"] += residual
  74. ag_pb_data[acc_id][prt_id]["residual"] += residual
  75. today = date_at_object
  76. if not due_date or today <= due_date:
  77. ag_pb_data[acc_id]["current"] += residual
  78. ag_pb_data[acc_id][prt_id]["current"] += residual
  79. elif today <= due_date + timedelta(days=30):
  80. ag_pb_data[acc_id]["30_days"] += residual
  81. ag_pb_data[acc_id][prt_id]["30_days"] += residual
  82. elif today <= due_date + timedelta(days=60):
  83. ag_pb_data[acc_id]["60_days"] += residual
  84. ag_pb_data[acc_id][prt_id]["60_days"] += residual
  85. elif today <= due_date + timedelta(days=90):
  86. ag_pb_data[acc_id]["90_days"] += residual
  87. ag_pb_data[acc_id][prt_id]["90_days"] += residual
  88. elif today <= due_date + timedelta(days=120):
  89. ag_pb_data[acc_id]["120_days"] += residual
  90. ag_pb_data[acc_id][prt_id]["120_days"] += residual
  91. else:
  92. ag_pb_data[acc_id]["older"] += residual
  93. ag_pb_data[acc_id][prt_id]["older"] += residual
  94. return ag_pb_data
  95. def _get_account_partial_reconciled(self, company_id, date_at_object):
  96. domain = [("max_date", ">=", date_at_object), ("company_id", "=", company_id)]
  97. fields = ["debit_move_id", "credit_move_id", "amount"]
  98. accounts_partial_reconcile = self.env["account.partial.reconcile"].search_read(
  99. domain=domain, fields=fields
  100. )
  101. debit_amount = {}
  102. credit_amount = {}
  103. for account_partial_reconcile_data in accounts_partial_reconcile:
  104. debit_move_id = account_partial_reconcile_data["debit_move_id"][0]
  105. credit_move_id = account_partial_reconcile_data["credit_move_id"][0]
  106. if debit_move_id not in debit_amount.keys():
  107. debit_amount[debit_move_id] = 0.0
  108. debit_amount[debit_move_id] += account_partial_reconcile_data["amount"]
  109. if credit_move_id not in credit_amount.keys():
  110. credit_amount[credit_move_id] = 0.0
  111. credit_amount[credit_move_id] += account_partial_reconcile_data["amount"]
  112. account_partial_reconcile_data.update(
  113. {"debit_move_id": debit_move_id, "credit_move_id": credit_move_id,}
  114. )
  115. return accounts_partial_reconcile, debit_amount, credit_amount
  116. @api.model
  117. def _get_new_move_lines_domain(
  118. self, new_ml_ids, account_ids, company_id, partner_ids, only_posted_moves
  119. ):
  120. domain = [
  121. ("account_id", "in", account_ids),
  122. ("company_id", "=", company_id),
  123. ("id", "in", new_ml_ids),
  124. ]
  125. if partner_ids:
  126. domain += [("partner_id", "in", partner_ids)]
  127. if only_posted_moves:
  128. domain += [("move_id.state", "=", "posted")]
  129. return domain
  130. def _recalculate_move_lines(
  131. self,
  132. move_lines,
  133. debit_ids,
  134. credit_ids,
  135. debit_amount,
  136. credit_amount,
  137. ml_ids,
  138. account_ids,
  139. company_id,
  140. partner_ids,
  141. only_posted_moves,
  142. ):
  143. reconciled_ids = list(debit_ids) + list(credit_ids)
  144. new_ml_ids = []
  145. for reconciled_id in reconciled_ids:
  146. if reconciled_id not in ml_ids and reconciled_id not in new_ml_ids:
  147. new_ml_ids += [reconciled_id]
  148. new_domain = self._get_new_move_lines_domain(
  149. new_ml_ids, account_ids, company_id, partner_ids, only_posted_moves
  150. )
  151. ml_fields = [
  152. "id",
  153. "name",
  154. "date",
  155. "move_id",
  156. "journal_id",
  157. "account_id",
  158. "partner_id",
  159. "amount_residual",
  160. "date_maturity",
  161. "ref",
  162. "reconciled",
  163. ]
  164. new_move_lines = self.env["account.move.line"].search_read(
  165. domain=new_domain, fields=ml_fields
  166. )
  167. move_lines = move_lines + new_move_lines
  168. for move_line in move_lines:
  169. ml_id = move_line["id"]
  170. if ml_id in debit_ids:
  171. move_line["amount_residual"] += debit_amount[ml_id]
  172. if ml_id in credit_ids:
  173. move_line["amount_residual"] -= credit_amount[ml_id]
  174. return move_lines
  175. def _get_move_lines_data(
  176. self,
  177. company_id,
  178. account_ids,
  179. partner_ids,
  180. date_at_object,
  181. only_posted_moves,
  182. show_move_line_details,
  183. ):
  184. domain = self._get_move_lines_domain(
  185. company_id, account_ids, partner_ids, only_posted_moves
  186. )
  187. ml_fields = [
  188. "id",
  189. "name",
  190. "date",
  191. "move_id",
  192. "journal_id",
  193. "account_id",
  194. "partner_id",
  195. "amount_residual",
  196. "date_maturity",
  197. "ref",
  198. "reconciled",
  199. ]
  200. move_lines = self.env["account.move.line"].search_read(
  201. domain=domain, fields=ml_fields
  202. )
  203. ml_ids = set(pd.DataFrame(move_lines).id.to_list())
  204. journals_ids = set()
  205. partners_ids = set()
  206. partners_data = {}
  207. ag_pb_data = {}
  208. if date_at_object < date.today():
  209. (
  210. acc_partial_rec,
  211. debit_amount,
  212. credit_amount,
  213. ) = self._get_account_partial_reconciled(company_id, date_at_object)
  214. if acc_partial_rec:
  215. acc_partial_rec_data = pd.DataFrame(acc_partial_rec)
  216. debit_ids = set(acc_partial_rec_data.debit_move_id.to_list())
  217. credit_ids = set(acc_partial_rec_data.credit_move_id.to_list())
  218. move_lines = self._recalculate_move_lines(
  219. move_lines,
  220. debit_ids,
  221. credit_ids,
  222. debit_amount,
  223. credit_amount,
  224. ml_ids,
  225. account_ids,
  226. company_id,
  227. partner_ids,
  228. only_posted_moves,
  229. )
  230. moves_lines_to_remove = []
  231. for move_line in move_lines:
  232. if move_line["date"] > date_at_object or float_is_zero(
  233. move_line["amount_residual"], precision_digits=2
  234. ):
  235. moves_lines_to_remove.append(move_line)
  236. if len(moves_lines_to_remove) > 0:
  237. for move_line_to_remove in moves_lines_to_remove:
  238. move_lines.remove(move_line_to_remove)
  239. for move_line in move_lines:
  240. journals_ids.add(move_line["journal_id"][0])
  241. acc_id = move_line["account_id"][0]
  242. if move_line["partner_id"]:
  243. prt_id = move_line["partner_id"][0]
  244. prt_name = move_line["partner_id"][1]
  245. else:
  246. prt_id = 0
  247. prt_name = ""
  248. if prt_id not in partners_ids:
  249. partners_data.update({prt_id: {"id": prt_id, "name": prt_name}})
  250. partners_ids.add(prt_id)
  251. if acc_id not in ag_pb_data.keys():
  252. ag_pb_data = self._initialize_account(ag_pb_data, acc_id)
  253. if prt_id not in ag_pb_data[acc_id]:
  254. ag_pb_data = self._initialize_partner(ag_pb_data, acc_id, prt_id)
  255. move_line_data = {}
  256. if show_move_line_details:
  257. move_line_data.update(
  258. {
  259. "date": move_line["date"],
  260. "entry": move_line["move_id"][1],
  261. "jnl_id": move_line["journal_id"][0],
  262. "acc_id": acc_id,
  263. "partner": prt_name,
  264. "ref": move_line["ref"],
  265. "due_date": move_line["date_maturity"],
  266. "residual": move_line["amount_residual"],
  267. }
  268. )
  269. ag_pb_data[acc_id][prt_id]["move_lines"].append(move_line_data)
  270. ag_pb_data = self._calculate_amounts(
  271. ag_pb_data,
  272. acc_id,
  273. prt_id,
  274. move_line["amount_residual"],
  275. move_line["date_maturity"],
  276. date_at_object,
  277. )
  278. journals_data = self._get_journals_data(list(journals_ids))
  279. accounts_data = self._get_accounts_data(ag_pb_data.keys())
  280. return ag_pb_data, accounts_data, partners_data, journals_data
  281. @api.model
  282. def _compute_maturity_date(self, ml, date_at_object):
  283. ml.update(
  284. {
  285. "current": 0.0,
  286. "30_days": 0.0,
  287. "60_days": 0.0,
  288. "90_days": 0.0,
  289. "120_days": 0.0,
  290. "older": 0.0,
  291. }
  292. )
  293. due_date = ml["due_date"]
  294. amount = ml["residual"]
  295. today = date_at_object
  296. if not due_date or today <= due_date:
  297. ml["current"] += amount
  298. elif today <= due_date + timedelta(days=30):
  299. ml["30_days"] += amount
  300. elif today <= due_date + timedelta(days=60):
  301. ml["60_days"] += amount
  302. elif today <= due_date + timedelta(days=90):
  303. ml["90_days"] += amount
  304. elif today <= due_date + timedelta(days=120):
  305. ml["120_days"] += amount
  306. else:
  307. ml["older"] += amount
  308. def _create_account_list(
  309. self,
  310. ag_pb_data,
  311. accounts_data,
  312. partners_data,
  313. journals_data,
  314. show_move_line_details,
  315. date_at_oject,
  316. ):
  317. aged_partner_data = []
  318. for account in accounts_data.values():
  319. acc_id = account["id"]
  320. account.update(
  321. {
  322. "residual": ag_pb_data[acc_id]["residual"],
  323. "current": ag_pb_data[acc_id]["current"],
  324. "30_days": ag_pb_data[acc_id]["30_days"],
  325. "60_days": ag_pb_data[acc_id]["60_days"],
  326. "90_days": ag_pb_data[acc_id]["90_days"],
  327. "120_days": ag_pb_data[acc_id]["120_days"],
  328. "older": ag_pb_data[acc_id]["older"],
  329. "partners": [],
  330. }
  331. )
  332. for prt_id in ag_pb_data[acc_id]:
  333. if isinstance(prt_id, int):
  334. partner = {
  335. "name": partners_data[prt_id]["name"],
  336. "residual": ag_pb_data[acc_id][prt_id]["residual"],
  337. "current": ag_pb_data[acc_id][prt_id]["current"],
  338. "30_days": ag_pb_data[acc_id][prt_id]["30_days"],
  339. "60_days": ag_pb_data[acc_id][prt_id]["60_days"],
  340. "90_days": ag_pb_data[acc_id][prt_id]["90_days"],
  341. "120_days": ag_pb_data[acc_id][prt_id]["120_days"],
  342. "older": ag_pb_data[acc_id][prt_id]["older"],
  343. }
  344. if show_move_line_details:
  345. move_lines = []
  346. for ml in ag_pb_data[acc_id][prt_id]["move_lines"]:
  347. ml.update(
  348. {
  349. "journal": journals_data[ml["jnl_id"]]["code"],
  350. "account": accounts_data[ml["acc_id"]]["code"],
  351. }
  352. )
  353. self._compute_maturity_date(ml, date_at_oject)
  354. move_lines.append(ml)
  355. partner.update({"move_lines": move_lines})
  356. account["partners"].append(partner)
  357. aged_partner_data.append(account)
  358. return aged_partner_data
  359. @api.model
  360. def _calculate_percent(self, aged_partner_data):
  361. for account in aged_partner_data:
  362. if abs(account["residual"]) > 0.01:
  363. total = account["residual"]
  364. account.update(
  365. {
  366. "percent_current": abs(
  367. round((account["current"] / total) * 100, 2)
  368. ),
  369. "percent_30_days": abs(
  370. round((account["30_days"] / total) * 100, 2)
  371. ),
  372. "percent_60_days": abs(
  373. round((account["60_days"] / total) * 100, 2)
  374. ),
  375. "percent_90_days": abs(
  376. round((account["90_days"] / total) * 100, 2)
  377. ),
  378. "percent_120_days": abs(
  379. round((account["120_days"] / total) * 100, 2)
  380. ),
  381. "percent_older": abs(
  382. round((account["older"] / total) * 100, 2)
  383. ),
  384. }
  385. )
  386. else:
  387. account.update(
  388. {
  389. "percent_current": 0.0,
  390. "percent_30_days": 0.0,
  391. "percent_60_days": 0.0,
  392. "percent_90_days": 0.0,
  393. "percent_120_days": 0.0,
  394. "percent_older": 0.0,
  395. }
  396. )
  397. return aged_partner_data
  398. @api.multi
  399. def _get_report_values(self, docids, data):
  400. wizard_id = data["wizard_id"]
  401. company = self.env["res.company"].browse(data["company_id"])
  402. company_id = data["company_id"]
  403. account_ids = data["account_ids"]
  404. partner_ids = data["partner_ids"]
  405. date_at = data["date_at"]
  406. date_at_object = datetime.strptime(date_at, "%Y-%m-%d").date()
  407. only_posted_moves = data["only_posted_moves"]
  408. show_move_line_details = data["show_move_line_details"]
  409. (
  410. ag_pb_data,
  411. accounts_data,
  412. partners_data,
  413. journals_data,
  414. ) = self._get_move_lines_data(
  415. company_id,
  416. account_ids,
  417. partner_ids,
  418. date_at_object,
  419. only_posted_moves,
  420. show_move_line_details,
  421. )
  422. aged_partner_data = self._create_account_list(
  423. ag_pb_data,
  424. accounts_data,
  425. partners_data,
  426. journals_data,
  427. show_move_line_details,
  428. date_at_object,
  429. )
  430. aged_partner_data = self._calculate_percent(aged_partner_data)
  431. return {
  432. "doc_ids": [wizard_id],
  433. "doc_model": "open.items.report.wizard",
  434. "docs": self.env["open.items.report.wizard"].browse(wizard_id),
  435. "company_name": company.display_name,
  436. "currency_name": company.currency_id.name,
  437. "date_at": date_at,
  438. "only_posted_moves": only_posted_moves,
  439. "aged_partner_balance": aged_partner_data,
  440. "show_move_lines_details": show_move_line_details,
  441. }