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.

687 lines
29 KiB

  1. # Author: Julien Coux
  2. # Copyright 2016 Camptocamp SA
  3. # Copyright 2021 Tecnativa - João Marques
  4. # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).
  5. from odoo import models
  6. class AbstractReportXslx(models.AbstractModel):
  7. _name = "report.account_financial_report.abstract_report_xlsx"
  8. _description = "Abstract XLSX Account Financial Report"
  9. _inherit = "report.report_xlsx.abstract"
  10. def get_workbook_options(self):
  11. vals = super().get_workbook_options()
  12. vals.update({"constant_memory": True})
  13. return vals
  14. def generate_xlsx_report(self, workbook, data, objects):
  15. # Initialize report variables
  16. report_data = {
  17. "workbook": None,
  18. "sheet": None, # main sheet which will contains report
  19. "columns": None, # columns of the report
  20. "row_pos": None, # row_pos must be incremented at each writing lines
  21. "formats": None,
  22. }
  23. self._define_formats(workbook, report_data)
  24. # Get report data
  25. report_name = self._get_report_name(objects, data=data)
  26. report_footer = self._get_report_footer()
  27. filters = self._get_report_filters(objects)
  28. report_data["columns"] = self._get_report_columns(objects)
  29. report_data["workbook"] = workbook
  30. report_data["sheet"] = workbook.add_worksheet(report_name[:31])
  31. self._set_column_width(report_data)
  32. # Fill report
  33. report_data["row_pos"] = 0
  34. self._write_report_title(report_name, report_data)
  35. self._write_filters(filters, report_data)
  36. self._generate_report_content(workbook, objects, data, report_data)
  37. self._write_report_footer(report_footer, report_data)
  38. def _define_formats(self, workbook, report_data):
  39. """Add cell formats to current workbook.
  40. Those formats can be used on all cell.
  41. Available formats are :
  42. * format_bold
  43. * format_right
  44. * format_right_bold_italic
  45. * format_header_left
  46. * format_header_center
  47. * format_header_right
  48. * format_header_amount
  49. * format_amount
  50. * format_percent_bold_italic
  51. """
  52. currency_id = self.env["res.company"]._default_currency_id()
  53. report_data["formats"] = {
  54. "format_bold": workbook.add_format({"bold": True}),
  55. "format_right": workbook.add_format({"align": "right"}),
  56. "format_left": workbook.add_format({"align": "left"}),
  57. "format_right_bold_italic": workbook.add_format(
  58. {"align": "right", "bold": True, "italic": True}
  59. ),
  60. "format_header_left": workbook.add_format(
  61. {"bold": True, "border": True, "bg_color": "#FFFFCC"}
  62. ),
  63. "format_header_center": workbook.add_format(
  64. {"bold": True, "align": "center", "border": True, "bg_color": "#FFFFCC"}
  65. ),
  66. "format_header_right": workbook.add_format(
  67. {"bold": True, "align": "right", "border": True, "bg_color": "#FFFFCC"}
  68. ),
  69. "format_header_amount": workbook.add_format(
  70. {"bold": True, "border": True, "bg_color": "#FFFFCC"}
  71. ).set_num_format("#,##0." + "0" * currency_id.decimal_places),
  72. "format_amount": workbook.add_format().set_num_format(
  73. "#,##0." + "0" * currency_id.decimal_places
  74. ),
  75. "format_amount_bold": workbook.add_format({"bold": True}).set_num_format(
  76. "#,##0." + "0" * currency_id.decimal_places
  77. ),
  78. "format_percent_bold_italic": workbook.add_format(
  79. {"bold": True, "italic": True}
  80. ).set_num_format("#,##0.00%"),
  81. }
  82. def _set_column_width(self, report_data):
  83. """Set width for all defined columns.
  84. Columns are defined with `_get_report_columns` method.
  85. """
  86. for position, column in report_data["columns"].items():
  87. report_data["sheet"].set_column(position, position, column["width"])
  88. def _write_report_title(self, title, report_data):
  89. """Write report title on current line using all defined columns width.
  90. Columns are defined with `_get_report_columns` method.
  91. """
  92. report_data["sheet"].merge_range(
  93. report_data["row_pos"],
  94. 0,
  95. report_data["row_pos"],
  96. len(report_data["columns"]) - 1,
  97. title,
  98. report_data["formats"]["format_bold"],
  99. )
  100. report_data["row_pos"] += 3
  101. def _write_report_footer(self, footer, report_data):
  102. """Write report footer .
  103. Columns are defined with `_get_report_columns` method.
  104. """
  105. if footer:
  106. report_data["row_pos"] += 1
  107. report_data["sheet"].merge_range(
  108. report_data["row_pos"],
  109. 0,
  110. report_data["row_pos"],
  111. len(report_data["columns"]) - 1,
  112. footer,
  113. report_data["formats"]["format_left"],
  114. )
  115. report_data["row_pos"] += 1
  116. def _write_filters(self, filters, report_data):
  117. """Write one line per filters on starting on current line.
  118. Columns number for filter name is defined
  119. with `_get_col_count_filter_name` method.
  120. Columns number for filter value is define
  121. with `_get_col_count_filter_value` method.
  122. """
  123. col_name = 1
  124. col_count_filter_name = self._get_col_count_filter_name()
  125. col_count_filter_value = self._get_col_count_filter_value()
  126. col_value = col_name + col_count_filter_name + 1
  127. for title, value in filters:
  128. report_data["sheet"].merge_range(
  129. report_data["row_pos"],
  130. col_name,
  131. report_data["row_pos"],
  132. col_name + col_count_filter_name - 1,
  133. title,
  134. report_data["formats"]["format_header_left"],
  135. )
  136. report_data["sheet"].merge_range(
  137. report_data["row_pos"],
  138. col_value,
  139. report_data["row_pos"],
  140. col_value + col_count_filter_value - 1,
  141. value,
  142. )
  143. report_data["row_pos"] += 1
  144. report_data["row_pos"] += 2
  145. def write_array_title(self, title, report_data):
  146. """Write array title on current line using all defined columns width.
  147. Columns are defined with `_get_report_columns` method.
  148. """
  149. report_data["sheet"].merge_range(
  150. report_data["row_pos"],
  151. 0,
  152. report_data["row_pos"],
  153. len(report_data["columns"]) - 1,
  154. title,
  155. report_data["formats"]["format_bold"],
  156. )
  157. report_data["row_pos"] += 1
  158. def write_array_header(self, report_data):
  159. """Write array header on current line using all defined columns name.
  160. Columns are defined with `_get_report_columns` method.
  161. """
  162. for col_pos, column in report_data["columns"].items():
  163. report_data["sheet"].write(
  164. report_data["row_pos"],
  165. col_pos,
  166. column["header"],
  167. report_data["formats"]["format_header_center"],
  168. )
  169. report_data["row_pos"] += 1
  170. def write_line(self, line_object, report_data):
  171. """Write a line on current line using all defined columns field name.
  172. Columns are defined with `_get_report_columns` method.
  173. """
  174. for col_pos, column in report_data["columns"].items():
  175. value = getattr(line_object, column["field"])
  176. cell_type = column.get("type", "string")
  177. if cell_type == "many2one":
  178. report_data["sheet"].write_string(
  179. report_data["row_pos"],
  180. col_pos,
  181. value.name or "",
  182. report_data["formats"]["format_right"],
  183. )
  184. elif cell_type == "string":
  185. if (
  186. hasattr(line_object, "account_group_id")
  187. and line_object.account_group_id
  188. ):
  189. report_data["sheet"].write_string(
  190. report_data["row_pos"],
  191. col_pos,
  192. value or "",
  193. report_data["formats"]["format_bold"],
  194. )
  195. else:
  196. report_data["sheet"].write_string(
  197. report_data["row_pos"], col_pos, value or ""
  198. )
  199. elif cell_type == "amount":
  200. if (
  201. hasattr(line_object, "account_group_id")
  202. and line_object.account_group_id
  203. ):
  204. cell_format = report_data["formats"]["format_amount_bold"]
  205. else:
  206. cell_format = report_data["formats"]["format_amount"]
  207. report_data["sheet"].write_number(
  208. report_data["row_pos"], col_pos, float(value), cell_format
  209. )
  210. elif cell_type == "amount_currency":
  211. if line_object.currency_id:
  212. format_amt = self._get_currency_amt_format(line_object, report_data)
  213. report_data["sheet"].write_number(
  214. report_data["row_pos"], col_pos, float(value), format_amt
  215. )
  216. report_data["row_pos"] += 1
  217. def write_line_from_dict(self, line_dict, report_data):
  218. """Write a line on current line"""
  219. for col_pos, column in report_data["columns"].items():
  220. value = line_dict.get(column["field"], False)
  221. cell_type = column.get("type", "string")
  222. if cell_type == "string":
  223. if (
  224. line_dict.get("account_group_id", False)
  225. and line_dict["account_group_id"]
  226. ):
  227. report_data["sheet"].write_string(
  228. report_data["row_pos"],
  229. col_pos,
  230. value or "",
  231. report_data["formats"]["format_bold"],
  232. )
  233. else:
  234. if (
  235. not isinstance(value, str)
  236. and not isinstance(value, bool)
  237. and not isinstance(value, int)
  238. ):
  239. value = value and value.strftime("%d/%m/%Y")
  240. report_data["sheet"].write_string(
  241. report_data["row_pos"], col_pos, value or ""
  242. )
  243. elif cell_type == "amount":
  244. if (
  245. line_dict.get("account_group_id", False)
  246. and line_dict["account_group_id"]
  247. ):
  248. cell_format = report_data["formats"]["format_amount_bold"]
  249. else:
  250. cell_format = report_data["formats"]["format_amount"]
  251. report_data["sheet"].write_number(
  252. report_data["row_pos"], col_pos, float(value), cell_format
  253. )
  254. elif cell_type == "amount_currency":
  255. if line_dict.get("currency_name", False):
  256. format_amt = self._get_currency_amt_format_dict(
  257. line_dict, report_data
  258. )
  259. report_data["sheet"].write_number(
  260. report_data["row_pos"], col_pos, float(value), format_amt
  261. )
  262. elif cell_type == "currency_name":
  263. report_data["sheet"].write_string(
  264. report_data["row_pos"],
  265. col_pos,
  266. value or "",
  267. report_data["formats"]["format_right"],
  268. )
  269. report_data["row_pos"] += 1
  270. def write_initial_balance(self, my_object, label, report_data):
  271. """Write a specific initial balance line on current line
  272. using defined columns field_initial_balance name.
  273. Columns are defined with `_get_report_columns` method.
  274. """
  275. col_pos_label = self._get_col_pos_initial_balance_label()
  276. report_data["sheet"].write(
  277. report_data["row_pos"],
  278. col_pos_label,
  279. label,
  280. report_data["formats"]["format_right"],
  281. )
  282. for col_pos, column in report_data["columns"].items():
  283. if column.get("field_initial_balance"):
  284. value = getattr(my_object, column["field_initial_balance"])
  285. cell_type = column.get("type", "string")
  286. if cell_type == "string":
  287. report_data["sheet"].write_string(
  288. report_data["row_pos"], col_pos, value or ""
  289. )
  290. elif cell_type == "amount":
  291. report_data["sheet"].write_number(
  292. report_data["row_pos"],
  293. col_pos,
  294. float(value),
  295. report_data["formats"]["format_amount"],
  296. )
  297. elif cell_type == "amount_currency":
  298. if my_object.currency_id:
  299. format_amt = self._get_currency_amt_format(
  300. my_object, report_data
  301. )
  302. report_data["sheet"].write_number(
  303. report_data["row_pos"], col_pos, float(value), format_amt
  304. )
  305. elif column.get("field_currency_balance"):
  306. value = getattr(my_object, column["field_currency_balance"])
  307. cell_type = column.get("type", "string")
  308. if cell_type == "many2one":
  309. if my_object.currency_id:
  310. report_data["sheet"].write_string(
  311. report_data["row_pos"],
  312. col_pos,
  313. value.name or "",
  314. report_data["formats"]["format_right"],
  315. )
  316. report_data["row_pos"] += 1
  317. def write_initial_balance_from_dict(self, my_object, label, report_data):
  318. """Write a specific initial balance line on current line
  319. using defined columns field_initial_balance name.
  320. Columns are defined with `_get_report_columns` method.
  321. """
  322. col_pos_label = self._get_col_pos_initial_balance_label()
  323. report_data["sheet"].write(
  324. report_data["row_pos"],
  325. col_pos_label,
  326. label,
  327. report_data["formats"]["format_right"],
  328. )
  329. for col_pos, column in report_data["columns"].items():
  330. if column.get("field_initial_balance"):
  331. value = my_object.get(column["field_initial_balance"], False)
  332. cell_type = column.get("type", "string")
  333. if cell_type == "string":
  334. report_data["sheet"].write_string(
  335. report_data["row_pos"], col_pos, value or ""
  336. )
  337. elif cell_type == "amount":
  338. report_data["sheet"].write_number(
  339. report_data["row_pos"],
  340. col_pos,
  341. float(value),
  342. report_data["formats"]["format_amount"],
  343. )
  344. elif cell_type == "amount_currency":
  345. if my_object["currency_id"]:
  346. format_amt = self._get_currency_amt_format(
  347. my_object, report_data
  348. )
  349. report_data["sheet"].write_number(
  350. report_data["row_pos"], col_pos, float(value), format_amt
  351. )
  352. elif column.get("field_currency_balance"):
  353. value = my_object.get(column["field_currency_balance"], False)
  354. cell_type = column.get("type", "string")
  355. if cell_type == "many2one":
  356. if my_object["currency_id"]:
  357. report_data["sheet"].write_string(
  358. report_data["row_pos"],
  359. col_pos,
  360. value.name or "",
  361. report_data["formats"]["format_right"],
  362. )
  363. report_data["row_pos"] += 1
  364. def write_ending_balance(self, my_object, name, label, report_data):
  365. """Write a specific ending balance line on current line
  366. using defined columns field_final_balance name.
  367. Columns are defined with `_get_report_columns` method.
  368. """
  369. for i in range(0, len(report_data["columns"])):
  370. report_data["sheet"].write(
  371. report_data["row_pos"],
  372. i,
  373. "",
  374. report_data["formats"]["format_header_right"],
  375. )
  376. row_count_name = self._get_col_count_final_balance_name()
  377. col_pos_label = self._get_col_pos_final_balance_label()
  378. report_data["sheet"].merge_range(
  379. report_data["row_pos"],
  380. 0,
  381. report_data["row_pos"],
  382. row_count_name - 1,
  383. name,
  384. report_data["formats"]["format_header_left"],
  385. )
  386. report_data["sheet"].write(
  387. report_data["row_pos"],
  388. col_pos_label,
  389. label,
  390. report_data["formats"]["format_header_right"],
  391. )
  392. for col_pos, column in report_data["columns"].items():
  393. if column.get("field_final_balance"):
  394. value = getattr(my_object, column["field_final_balance"])
  395. cell_type = column.get("type", "string")
  396. if cell_type == "string":
  397. report_data["sheet"].write_string(
  398. report_data["row_pos"],
  399. col_pos,
  400. value or "",
  401. report_data["formats"]["format_header_right"],
  402. )
  403. elif cell_type == "amount":
  404. report_data["sheet"].write_number(
  405. report_data["row_pos"],
  406. col_pos,
  407. float(value),
  408. report_data["formats"]["format_header_amount"],
  409. )
  410. elif cell_type == "amount_currency":
  411. if my_object.currency_id:
  412. format_amt = self._get_currency_amt_header_format(
  413. my_object, report_data
  414. )
  415. report_data["sheet"].write_number(
  416. report_data["row_pos"], col_pos, float(value), format_amt
  417. )
  418. elif column.get("field_currency_balance"):
  419. value = getattr(my_object, column["field_currency_balance"])
  420. cell_type = column.get("type", "string")
  421. if cell_type == "many2one":
  422. if my_object.currency_id:
  423. report_data["sheet"].write_string(
  424. report_data["row_pos"],
  425. col_pos,
  426. value.name or "",
  427. report_data["formats"]["format_header_right"],
  428. )
  429. report_data["row_pos"] += 1
  430. def write_ending_balance_from_dict(self, my_object, name, label, report_data):
  431. """Write a specific ending balance line on current line
  432. using defined columns field_final_balance name.
  433. Columns are defined with `_get_report_columns` method.
  434. """
  435. for i in range(0, len(report_data["columns"])):
  436. report_data["sheet"].write(
  437. report_data["row_pos"],
  438. i,
  439. "",
  440. report_data["formats"]["format_header_right"],
  441. )
  442. row_count_name = self._get_col_count_final_balance_name()
  443. col_pos_label = self._get_col_pos_final_balance_label()
  444. report_data["sheet"].merge_range(
  445. report_data["row_pos"],
  446. 0,
  447. report_data["row_pos"],
  448. row_count_name - 1,
  449. name,
  450. report_data["formats"]["format_header_left"],
  451. )
  452. report_data["sheet"].write(
  453. report_data["row_pos"],
  454. col_pos_label,
  455. label,
  456. report_data["formats"]["format_header_right"],
  457. )
  458. for col_pos, column in report_data["columns"].items():
  459. if column.get("field_final_balance"):
  460. value = my_object.get(column["field_final_balance"], False)
  461. cell_type = column.get("type", "string")
  462. if cell_type == "string":
  463. report_data["sheet"].write_string(
  464. report_data["row_pos"],
  465. col_pos,
  466. value or "",
  467. report_data["formats"]["format_header_right"],
  468. )
  469. elif cell_type == "amount":
  470. report_data["sheet"].write_number(
  471. report_data["row_pos"],
  472. col_pos,
  473. float(value),
  474. report_data["formats"]["format_header_amount"],
  475. )
  476. elif cell_type == "amount_currency":
  477. if my_object["currency_id"] and value:
  478. format_amt = self._get_currency_amt_format_dict(
  479. my_object, report_data
  480. )
  481. report_data["sheet"].write_number(
  482. report_data["row_pos"], col_pos, float(value), format_amt
  483. )
  484. elif column.get("field_currency_balance"):
  485. value = my_object.get(column["field_currency_balance"], False)
  486. cell_type = column.get("type", "string")
  487. if cell_type == "many2one":
  488. if my_object["currency_id"]:
  489. report_data["sheet"].write_string(
  490. report_data["row_pos"],
  491. col_pos,
  492. value or "",
  493. report_data["formats"]["format_header_right"],
  494. )
  495. elif cell_type == "currency_name":
  496. report_data["sheet"].write_string(
  497. report_data["row_pos"],
  498. col_pos,
  499. value or "",
  500. report_data["formats"]["format_header_right"],
  501. )
  502. report_data["row_pos"] += 1
  503. def _get_currency_amt_format(self, line_object, report_data):
  504. """ Return amount format specific for each currency. """
  505. if "account_group_id" in line_object and line_object["account_group_id"]:
  506. format_amt = report_data["formats"]["format_amount_bold"]
  507. field_prefix = "format_amount_bold"
  508. else:
  509. format_amt = report_data["formats"]["format_amount"]
  510. field_prefix = "format_amount"
  511. if "currency_id" in line_object and line_object.get("currency_id", False):
  512. field_name = "{}_{}".format(field_prefix, line_object["currency_id"].name)
  513. if hasattr(self, field_name):
  514. format_amt = getattr(self, field_name)
  515. else:
  516. format_amt = report_data["workbook"].add_format()
  517. report_data["field_name"] = format_amt
  518. format_amount = "#,##0." + (
  519. "0" * line_object["currency_id"].decimal_places
  520. )
  521. format_amt.set_num_format(format_amount)
  522. return format_amt
  523. def _get_currency_amt_format_dict(self, line_dict, report_data):
  524. """ Return amount format specific for each currency. """
  525. if line_dict.get("account_group_id", False) and line_dict["account_group_id"]:
  526. format_amt = report_data["formats"]["format_amount_bold"]
  527. field_prefix = "format_amount_bold"
  528. else:
  529. format_amt = report_data["formats"]["format_amount"]
  530. field_prefix = "format_amount"
  531. if line_dict.get("currency_id", False) and line_dict["currency_id"]:
  532. if isinstance(line_dict["currency_id"], int):
  533. currency = self.env["res.currency"].browse(line_dict["currency_id"])
  534. else:
  535. currency = line_dict["currency_id"]
  536. field_name = "{}_{}".format(field_prefix, currency.name)
  537. if hasattr(self, field_name):
  538. format_amt = getattr(self, field_name)
  539. else:
  540. format_amt = report_data["workbook"].add_format()
  541. report_data["field_name"] = format_amt
  542. format_amount = "#,##0." + ("0" * currency.decimal_places)
  543. format_amt.set_num_format(format_amount)
  544. return format_amt
  545. def _get_currency_amt_header_format(self, line_object, report_data):
  546. """ Return amount header format for each currency. """
  547. format_amt = report_data["formats"]["format_header_amount"]
  548. if line_object.currency_id:
  549. field_name = "format_header_amount_%s" % line_object.currency_id.name
  550. if hasattr(self, field_name):
  551. format_amt = getattr(self, field_name)
  552. else:
  553. format_amt = report_data["workbook"].add_format(
  554. {"bold": True, "border": True, "bg_color": "#FFFFCC"}
  555. )
  556. report_data["field_name"] = format_amt
  557. format_amount = "#,##0." + (
  558. "0" * line_object.currency_id.decimal_places
  559. )
  560. format_amt.set_num_format(format_amount)
  561. return format_amt
  562. def _get_currency_amt_header_format_dict(self, line_object, report_data):
  563. """ Return amount header format for each currency. """
  564. format_amt = report_data["formats"]["format_header_amount"]
  565. if line_object["currency_id"]:
  566. field_name = "format_header_amount_%s" % line_object["currency_name"]
  567. if hasattr(self, field_name):
  568. format_amt = getattr(self, field_name)
  569. else:
  570. format_amt = report_data["workbook"].add_format(
  571. {"bold": True, "border": True, "bg_color": "#FFFFCC"}
  572. )
  573. report_data["field_name"] = format_amt
  574. format_amount = "#,##0." + (
  575. "0" * line_object["currency_id"].decimal_places
  576. )
  577. format_amt.set_num_format(format_amount)
  578. return format_amt
  579. def _generate_report_content(self, workbook, report, data, report_data):
  580. """
  581. Allow to fetch report content to be displayed.
  582. """
  583. raise NotImplementedError()
  584. def _get_report_complete_name(self, report, prefix, data=None):
  585. if report.company_id:
  586. suffix = " - {} - {}".format(
  587. report.company_id.name, report.company_id.currency_id.name
  588. )
  589. return prefix + suffix
  590. return prefix
  591. def _get_report_name(self, report, data=False):
  592. """
  593. Allow to define the report name.
  594. Report name will be used as sheet name and as report title.
  595. :return: the report name
  596. """
  597. raise NotImplementedError()
  598. def _get_report_footer(self):
  599. """
  600. Allow to define the report footer.
  601. :return: the report footer
  602. """
  603. return False
  604. def _get_report_columns(self, report):
  605. """
  606. Allow to define the report columns
  607. which will be used to generate report.
  608. :return: the report columns as dict
  609. :Example:
  610. {
  611. 0: {'header': 'Simple column',
  612. 'field': 'field_name_on_my_object',
  613. 'width': 11},
  614. 1: {'header': 'Amount column',
  615. 'field': 'field_name_on_my_object',
  616. 'type': 'amount',
  617. 'width': 14},
  618. }
  619. """
  620. raise NotImplementedError()
  621. def _get_report_filters(self, report):
  622. """
  623. :return: the report filters as list
  624. :Example:
  625. [
  626. ['first_filter_name', 'first_filter_value'],
  627. ['second_filter_name', 'second_filter_value']
  628. ]
  629. """
  630. raise NotImplementedError()
  631. def _get_col_count_filter_name(self):
  632. """
  633. :return: the columns number used for filter names.
  634. """
  635. raise NotImplementedError()
  636. def _get_col_count_filter_value(self):
  637. """
  638. :return: the columns number used for filter values.
  639. """
  640. raise NotImplementedError()
  641. def _get_col_pos_initial_balance_label(self):
  642. """
  643. :return: the columns position used for initial balance label.
  644. """
  645. raise NotImplementedError()
  646. def _get_col_count_final_balance_name(self):
  647. """
  648. :return: the columns number used for final balance name.
  649. """
  650. raise NotImplementedError()
  651. def _get_col_pos_final_balance_label(self):
  652. """
  653. :return: the columns position used for final balance label.
  654. """
  655. raise NotImplementedError()