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.

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