Initial commit: OCA Report packages (45 packages)

This commit is contained in:
Ernad Husremovic 2025-08-29 15:43:05 +02:00
commit 2f4db400df
2543 changed files with 469120 additions and 0 deletions

View file

@ -0,0 +1,5 @@
# Copyright 2014 ACSONE SA/NV (<http://acsone.eu>)
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html).
from . import mis_report_instance_qweb
from . import mis_report_instance_xlsx

View file

@ -0,0 +1,27 @@
# Copyright 2014 ACSONE SA/NV (<http://acsone.eu>)
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html).
import logging
from odoo import models
_logger = logging.getLogger(__name__)
class Report(models.Model):
_inherit = "ir.actions.report"
def _render_qweb_pdf(self, report_ref, res_ids=None, data=None):
if (
self._get_report(report_ref).report_name
== "mis_builder.report_mis_report_instance"
):
if not res_ids:
res_ids = self.env.context.get("active_ids")
mis_report_instance = self.env["mis.report.instance"].browse(res_ids)[0]
# data=None, because it was there only to force Odoo
# to propagate context
return super(
Report, self.with_context(landscape=mis_report_instance.landscape_pdf)
)._render_qweb_pdf(report_ref, res_ids, data=None)
return super()._render_qweb_pdf(report_ref, res_ids, data)

View file

@ -0,0 +1,134 @@
<?xml version="1.0" encoding="UTF-8" ?>
<odoo>
<record id="qweb_pdf_export" model="ir.actions.report">
<field name="name">MIS report instance QWEB PDF report</field>
<field name="model">mis.report.instance</field>
<field name="type">ir.actions.report</field>
<field name="report_name">mis_builder.report_mis_report_instance</field>
<field name="report_type">qweb-pdf</field>
</record>
<!--
TODO we use divs with css table layout, but this has drawbacks:
(bad layout of first column, no colspan for first header row),
consider getting back to a plain HTML table.
-->
<template id="report_mis_report_instance">
<t t-call="web.html_container">
<t t-foreach="docs" t-as="o">
<t t-call="web.internal_layout">
<t t-set="matrix" t-value="o._compute_matrix()" />
<t t-set="notes" t-value="o.get_notes_by_cell_id()" />
<t t-set="style_obj" t-value="o.env['mis.report.style']" />
<div class="page">
<h3>
<span t-field="o.name" />
<span>-</span>
<t t-foreach="o.query_company_ids" t-as="company">
<span t-field="company.name" />
<span t-if="company != o.query_company_ids[-1]">,</span>
</t>
</h3>
<p>
<div class="mis_report_filers">
<t
t-foreach="o.get_filter_descriptions()"
t-as="filter_description"
>
<div>
<span t-out="filter_description" />
</div>
</t>
</div>
</p>
<div class="mis_table">
<div class="mis_thead">
<div class="mis_row">
<div class="mis_cell mis_collabel" />
<t t-foreach="matrix.iter_cols()" t-as="col">
<div class="mis_cell mis_collabel">
<t t-out="col.label" />
<t t-if="col.description">
<br />
<t t-out="col.description" />
</t>
</div>
<!-- add empty cells because we have no colspan with css tables -->
<t
t-foreach="list(col.iter_subcols())[1:]"
t-as="subcol"
>
<div class="mis_cell mis_collabel" />
</t>
</t>
</div>
<div class="mis_row">
<div class="mis_cell mis_collabel" />
<t t-foreach="matrix.iter_subcols()" t-as="subcol">
<div class="mis_cell mis_collabel">
<t t-out="subcol.label" />
<t t-if="subcol.description">
<br />
<t t-out="subcol.description" />
</t>
</div>
</t>
</div>
</div>
<div class="mis_tbody">
<t t-foreach="matrix.iter_rows()" t-as="row">
<div
t-if="not ((row.style_props.hide_empty and row.is_empty()) or row.style_props.hide_always)"
class="mis_row"
>
<div
t-att-style="style_obj.to_css_style(row.style_props)"
class="mis_cell mis_rowlabel"
>
<t t-out="row.label" />
<t t-if="row.description">
<br />
<t t-out="row.description" />
</t>
</div>
<t t-foreach="row.iter_cells()" t-as="cell">
<div
t-att-style="cell and style_obj.to_css_style(cell.style_props) or ''"
class="mis_cell mis_amount"
>
<t
t-out="cell and cell.val_rendered or ''"
/>
<span
class="oe_mis_builder_footnote"
t-if="cell"
>
<t
t-out="notes.get(cell.cell_id,{}).get('sequence','')"
/>
</span>
</div>
</t>
</div>
</t>
</div>
</div>
<!-- Foot notes -->
<div class="oe_mis_builder_footnote_div">
<table class="oe_mis_builder_footnote_table">
<t
t-foreach="sorted(notes.values(),key=lambda r:r['sequence'])"
t-as="note"
>
<tr>
<td><t t-out="note['sequence']" />. </td>
<td><t t-out="note['text']" /></td>
</tr>
</t>
</table>
</div>
</div>
</t>
</t>
</t>
</template>
</odoo>

View file

@ -0,0 +1,186 @@
# Copyright 2014 ACSONE SA/NV (<http://acsone.eu>)
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html).
import logging
import numbers
from collections import defaultdict
from datetime import datetime
from odoo import _, api, fields, models
from ..models.accounting_none import AccountingNone
from ..models.data_error import DataError
from ..models.mis_report_style import TYPE_STR
_logger = logging.getLogger(__name__)
ROW_HEIGHT = 15 # xlsxwriter units
COL_WIDTH = 0.9 # xlsxwriter units
MIN_COL_WIDTH = 10 # characters
MAX_COL_WIDTH = 50 # characters
class MisBuilderXlsx(models.AbstractModel):
_name = "report.mis_builder.mis_report_instance_xlsx"
_description = "MIS Builder XLSX report"
_inherit = "report.report_xlsx.abstract"
@api.model
def _mis_builder_add_annotation(self, sheet, cell, row_pos, col_pos, notes):
"""
Add anotation as a comment on cell in .xls
"""
if cell and (annotation := notes.get(cell.cell_id, {}).get("text")):
sheet.write_comment(row_pos, col_pos, annotation)
def generate_xlsx_report(self, workbook, data, objects):
# get the computed result of the report
matrix = objects._compute_matrix()
notes = objects.get_notes_by_cell_id()
style_obj = self.env["mis.report.style"]
# create worksheet
report_name = "{} - {}".format(
objects[0].name, ", ".join([a.name for a in objects[0].query_company_ids])
)
sheet = workbook.add_worksheet(report_name[:31])
row_pos = 0
col_pos = 0
# width of the labels column
label_col_width = MIN_COL_WIDTH
# {col_pos: max width in characters}
col_width = defaultdict(lambda: MIN_COL_WIDTH)
# document title
bold = workbook.add_format({"bold": True})
header_format = workbook.add_format(
{"bold": True, "align": "center", "bg_color": "#F0EEEE"}
)
sheet.write(row_pos, 0, report_name, bold)
row_pos += 2
# filters
filter_descriptions = objects.get_filter_descriptions()
if filter_descriptions:
for filter_description in objects.get_filter_descriptions():
sheet.write(row_pos, 0, filter_description)
row_pos += 1
row_pos += 1
# column headers
sheet.write(row_pos, 0, "", header_format)
col_pos = 1
for col in matrix.iter_cols():
label = col.label
if col.description:
label += "\n" + col.description
sheet.set_row(row_pos, ROW_HEIGHT * 2)
if col.colspan > 1:
sheet.merge_range(
row_pos,
col_pos,
row_pos,
col_pos + col.colspan - 1,
label,
header_format,
)
else:
sheet.write(row_pos, col_pos, label, header_format)
col_width[col_pos] = max(
col_width[col_pos], len(col.label or ""), len(col.description or "")
)
col_pos += col.colspan
row_pos += 1
# sub column headers
sheet.write(row_pos, 0, "", header_format)
col_pos = 1
for subcol in matrix.iter_subcols():
label = subcol.label
if subcol.description:
label += "\n" + subcol.description
sheet.set_row(row_pos, ROW_HEIGHT * 2)
sheet.write(row_pos, col_pos, label, header_format)
col_width[col_pos] = max(
col_width[col_pos],
len(subcol.label or ""),
len(subcol.description or ""),
)
col_pos += 1
row_pos += 1
# rows
for row in matrix.iter_rows():
if (
row.style_props.hide_empty and row.is_empty()
) or row.style_props.hide_always:
continue
row_xlsx_style = style_obj.to_xlsx_style(TYPE_STR, row.style_props)
row_format = workbook.add_format(row_xlsx_style)
col_pos = 0
label = row.label
if row.description:
label += "\n" + row.description
sheet.set_row(row_pos, ROW_HEIGHT * 2)
sheet.write(row_pos, col_pos, label, row_format)
label_col_width = max(
label_col_width, len(row.label or ""), len(row.description or "")
)
for cell in row.iter_cells():
col_pos += 1
self._mis_builder_add_annotation(sheet, cell, row_pos, col_pos, notes)
if not cell or cell.val is AccountingNone:
# TODO col/subcol format
sheet.write(row_pos, col_pos, "", row_format)
continue
cell_xlsx_style = style_obj.to_xlsx_style(
cell.val_type, cell.style_props, no_indent=True
)
cell_xlsx_style["align"] = "right"
cell_format = workbook.add_format(cell_xlsx_style)
if isinstance(cell.val, DataError):
val = cell.val.name
# TODO display cell.val.msg as Excel comment?
elif cell.val is None or cell.val is AccountingNone:
val = ""
else:
divider = float(cell.style_props.get("divider", 1))
if (
divider != 1
and isinstance(cell.val, numbers.Number)
and not cell.val_type == "pct"
):
val = cell.val / divider
else:
val = cell.val
sheet.write(row_pos, col_pos, val, cell_format)
col_width[col_pos] = max(
col_width[col_pos], len(cell.val_rendered or "")
)
row_pos += 1
# Add date/time footer
row_pos += 1
footer_format = workbook.add_format(
{"italic": True, "font_color": "#202020", "font_size": 9}
)
lang_model = self.env["res.lang"]
lang = lang_model._lang_get(self.env.user.lang)
now_tz = fields.Datetime.context_timestamp(
self.env["res.users"], datetime.now()
)
create_date = _(
"Generated on %(gen_date)s at %(gen_time)s",
gen_date=now_tz.strftime(lang.date_format),
gen_time=now_tz.strftime(lang.time_format),
)
sheet.write(row_pos, 0, create_date, footer_format)
# adjust col widths
sheet.set_column(0, 0, min(label_col_width, MAX_COL_WIDTH) * COL_WIDTH)
data_col_width = min(MAX_COL_WIDTH, max(col_width.values()))
min_col_pos = min(col_width.keys())
max_col_pos = max(col_width.keys())
sheet.set_column(min_col_pos, max_col_pos, data_col_width * COL_WIDTH)

View file

@ -0,0 +1,11 @@
<?xml version="1.0" encoding="UTF-8" ?>
<odoo>
<record id="xls_export" model="ir.actions.report">
<field name="name">MIS report instance XLS report</field>
<field name="model">mis.report.instance</field>
<field name="type">ir.actions.report</field>
<field name="report_name">mis_builder.mis_report_instance_xlsx</field>
<field name="report_type">xlsx</field>
<field name="report_file">mis_report_instance</field>
</record>
</odoo>