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.

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