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.

608 lines
24 KiB

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