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.

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