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