mirror of
https://github.com/bringout/oca-report.git
synced 2026-04-19 23:22:00 +02:00
remove 16.0-only packages from 19.0 branch
Keep only modules available in OCA reporting-engine 19.0:
bi_sql_editor, report_csv, report_qweb_element_page_visibility,
report_xlsx, report_xlsx_helper, report_xml, sql_request_abstract
🤖 assisted by claude
This commit is contained in:
parent
32a4fa90f7
commit
05df50b41d
2066 changed files with 0 additions and 366555 deletions
|
|
@ -1,47 +0,0 @@
|
|||
# MIS Builder
|
||||
|
||||
Odoo addon: mis_builder
|
||||
|
||||
## Installation
|
||||
|
||||
```bash
|
||||
pip install odoo-bringout-oca-mis-builder-mis_builder
|
||||
```
|
||||
|
||||
## Dependencies
|
||||
|
||||
This addon depends on:
|
||||
- account
|
||||
- board
|
||||
- report_xlsx
|
||||
- date_range
|
||||
|
||||
## Manifest Information
|
||||
|
||||
- **Name**: MIS Builder
|
||||
- **Version**: 16.0.5.5.1
|
||||
- **Category**: Reporting
|
||||
- **License**: AGPL-3
|
||||
- **Installable**: True
|
||||
|
||||
## Source
|
||||
|
||||
Based on [OCA/mis-builder](https://github.com/OCA/mis-builder) branch 16.0, addon `mis_builder`.
|
||||
|
||||
## License
|
||||
|
||||
This package maintains the original AGPL-3 license from the upstream Odoo project.
|
||||
|
||||
## Documentation
|
||||
|
||||
- Overview: doc/OVERVIEW.md
|
||||
- Architecture: doc/ARCHITECTURE.md
|
||||
- Models: doc/MODELS.md
|
||||
- Controllers: doc/CONTROLLERS.md
|
||||
- Wizards: doc/WIZARDS.md
|
||||
- Install: doc/INSTALL.md
|
||||
- Usage: doc/USAGE.md
|
||||
- Configuration: doc/CONFIGURATION.md
|
||||
- Dependencies: doc/DEPENDENCIES.md
|
||||
- Troubleshooting: doc/TROUBLESHOOTING.md
|
||||
- FAQ: doc/FAQ.md
|
||||
|
|
@ -1,32 +0,0 @@
|
|||
# Architecture
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
U[Users] -->|HTTP| V[Views and QWeb Templates]
|
||||
V --> C[Controllers]
|
||||
V --> W[Wizards – Transient Models]
|
||||
C --> M[Models and ORM]
|
||||
W --> M
|
||||
M --> R[Reports]
|
||||
DX[Data XML] --> M
|
||||
S[Security – ACLs and Groups] -. enforces .-> M
|
||||
|
||||
subgraph Mis_builder Module - mis_builder
|
||||
direction LR
|
||||
M:::layer
|
||||
W:::layer
|
||||
C:::layer
|
||||
V:::layer
|
||||
R:::layer
|
||||
S:::layer
|
||||
DX:::layer
|
||||
end
|
||||
|
||||
classDef layer fill:#eef8ff,stroke:#6ea8fe,stroke-width:1px
|
||||
```
|
||||
|
||||
Notes
|
||||
- Views include tree/form/kanban templates and report templates.
|
||||
- Controllers provide website/portal routes when present.
|
||||
- Wizards are UI flows implemented with `models.TransientModel`.
|
||||
- Data XML loads data/demo records; Security defines groups and access.
|
||||
|
|
@ -1,3 +0,0 @@
|
|||
# Configuration
|
||||
|
||||
Refer to Odoo settings for mis_builder. Configure related models, access rights, and options as needed.
|
||||
|
|
@ -1,3 +0,0 @@
|
|||
# Controllers
|
||||
|
||||
This module does not define custom HTTP controllers.
|
||||
|
|
@ -1,8 +0,0 @@
|
|||
# Dependencies
|
||||
|
||||
This addon depends on:
|
||||
|
||||
- [account](https://github.com/bringout/oca-ocb-accounting/tree/ddf6c0d80189f2cd640968f14b2d1346fca52a9f/odoo-bringout-oca-ocb-account)
|
||||
- [board](https://github.com/bringout/oca-ocb-core/tree/5d1ce43101a4d83b4ac660942e4a7a462823262f/odoo-bringout-oca-ocb-board)
|
||||
- [report_xlsx](https://github.com/bringout/oca-report)
|
||||
- [date_range](https://github.com/bringout/oca-technical)
|
||||
|
|
@ -1,4 +0,0 @@
|
|||
# FAQ
|
||||
|
||||
- Q: Which Odoo version? A: 16.0 (OCA/OCB packaged).
|
||||
- Q: How to enable? A: Start server with --addon mis_builder or install in UI.
|
||||
|
|
@ -1,7 +0,0 @@
|
|||
# Install
|
||||
|
||||
```bash
|
||||
pip install odoo-bringout-oca-mis-builder-mis_builder"
|
||||
# or
|
||||
uv pip install odoo-bringout-oca-mis-builder-mis_builder"
|
||||
```
|
||||
|
|
@ -1,24 +0,0 @@
|
|||
# Models
|
||||
|
||||
Detected core models and extensions in mis_builder.
|
||||
|
||||
```mermaid
|
||||
classDiagram
|
||||
class mis_kpi_data
|
||||
class mis_report
|
||||
class mis_report_instance
|
||||
class mis_report_instance_annotation
|
||||
class mis_report_instance_period
|
||||
class mis_report_instance_period_sum
|
||||
class mis_report_kpi
|
||||
class mis_report_kpi_expression
|
||||
class mis_report_query
|
||||
class mis_report_style
|
||||
class mis_report_subkpi
|
||||
class mis_report_subreport
|
||||
class prorata_read_group_mixin
|
||||
```
|
||||
|
||||
Notes
|
||||
- Classes show model technical names; fields omitted for brevity.
|
||||
- Items listed under _inherit are extensions of existing models.
|
||||
|
|
@ -1,6 +0,0 @@
|
|||
# Overview
|
||||
|
||||
Packaged Odoo addon: mis_builder. Provides features documented in upstream Odoo 16 under this addon.
|
||||
|
||||
- Source: OCA/OCB 16.0, addon mis_builder
|
||||
- License: LGPL-3
|
||||
|
|
@ -1,32 +0,0 @@
|
|||
# Reports
|
||||
|
||||
Report definitions and templates in mis_builder.
|
||||
|
||||
```mermaid
|
||||
classDiagram
|
||||
class Report
|
||||
Model <|-- Report
|
||||
class MisBuilderXlsx
|
||||
AbstractModel <|-- MisBuilderXlsx
|
||||
```
|
||||
|
||||
## Available Reports
|
||||
|
||||
### PDF/Document Reports
|
||||
- **MIS report instance QWEB PDF report** (PDF/Print)
|
||||
- **MIS report instance XLS report** (PDF/Print)
|
||||
|
||||
|
||||
## Report Files
|
||||
|
||||
- **__init__.py** (Python logic)
|
||||
- **mis_report_instance_qweb.py** (Python logic)
|
||||
- **mis_report_instance_qweb.xml** (XML template/definition)
|
||||
- **mis_report_instance_xlsx.py** (Python logic)
|
||||
- **mis_report_instance_xlsx.xml** (XML template/definition)
|
||||
|
||||
## Notes
|
||||
- Named reports above are accessible through Odoo's reporting menu
|
||||
- Python files define report logic and data processing
|
||||
- XML files contain report templates, definitions, and formatting
|
||||
- Reports are integrated with Odoo's printing and email systems
|
||||
|
|
@ -1,45 +0,0 @@
|
|||
# Security
|
||||
|
||||
Access control and security definitions in mis_builder.
|
||||
|
||||
## Access Control Lists (ACLs)
|
||||
|
||||
Model access permissions defined in:
|
||||
- **[ir.model.access.csv](../mis_builder/security/ir.model.access.csv)**
|
||||
- 23 model access rules
|
||||
|
||||
## Record Rules
|
||||
|
||||
Row-level security rules defined in:
|
||||
|
||||
## Security Groups & Configuration
|
||||
|
||||
Security groups and permissions defined in:
|
||||
- **[mis_builder_security.xml](../mis_builder/security/mis_builder_security.xml)**
|
||||
- **[res_groups.xml](../mis_builder/security/res_groups.xml)**
|
||||
- 2 security groups defined
|
||||
|
||||
```mermaid
|
||||
graph TB
|
||||
subgraph "Security Layers"
|
||||
A[Users] --> B[Groups]
|
||||
B --> C[Access Control Lists]
|
||||
C --> D[Models]
|
||||
B --> E[Record Rules]
|
||||
E --> F[Individual Records]
|
||||
end
|
||||
```
|
||||
|
||||
Security files overview:
|
||||
- **[ir.model.access.csv](../mis_builder/security/ir.model.access.csv)**
|
||||
- Model access permissions (CRUD rights)
|
||||
- **[mis_builder_security.xml](../mis_builder/security/mis_builder_security.xml)**
|
||||
- Security groups, categories, and XML-based rules
|
||||
- **[res_groups.xml](../mis_builder/security/res_groups.xml)**
|
||||
- Security groups, categories, and XML-based rules
|
||||
|
||||
Notes
|
||||
- Access Control Lists define which groups can access which models
|
||||
- Record Rules provide row-level security (filter records by user/group)
|
||||
- Security groups organize users and define permission sets
|
||||
- All security is enforced at the ORM level by Odoo
|
||||
|
|
@ -1,5 +0,0 @@
|
|||
# Troubleshooting
|
||||
|
||||
- Ensure Python and Odoo environment matches repo guidance.
|
||||
- Check database connectivity and logs if startup fails.
|
||||
- Validate that dependent addons listed in DEPENDENCIES.md are installed.
|
||||
|
|
@ -1,7 +0,0 @@
|
|||
# Usage
|
||||
|
||||
Start Odoo including this addon (from repo root):
|
||||
|
||||
```bash
|
||||
python3 scripts/nix_odoo_web_server.py --db-name mydb --addon mis_builder
|
||||
```
|
||||
|
|
@ -1,8 +0,0 @@
|
|||
# Wizards
|
||||
|
||||
Transient models exposed as UI wizards in mis_builder.
|
||||
|
||||
```mermaid
|
||||
classDiagram
|
||||
class AddMisReportInstanceDashboard
|
||||
```
|
||||
|
|
@ -1,719 +0,0 @@
|
|||
.. image:: https://odoo-community.org/readme-banner-image
|
||||
:target: https://odoo-community.org/get-involved?utm_source=readme
|
||||
:alt: Odoo Community Association
|
||||
|
||||
===========
|
||||
MIS Builder
|
||||
===========
|
||||
|
||||
..
|
||||
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
|
||||
!! This file is generated by oca-gen-addon-readme !!
|
||||
!! changes will be overwritten. !!
|
||||
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
|
||||
!! source digest: sha256:9284d72ac55aea402b2ee7dbcbf9e3bcd406892939230843fdb5ddf37dffebee
|
||||
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
|
||||
|
||||
.. |badge1| image:: https://img.shields.io/badge/maturity-Production%2FStable-green.png
|
||||
:target: https://odoo-community.org/page/development-status
|
||||
:alt: Production/Stable
|
||||
.. |badge2| image:: https://img.shields.io/badge/license-AGPL--3-blue.png
|
||||
:target: http://www.gnu.org/licenses/agpl-3.0-standalone.html
|
||||
:alt: License: AGPL-3
|
||||
.. |badge3| image:: https://img.shields.io/badge/github-OCA%2Fmis--builder-lightgray.png?logo=github
|
||||
:target: https://github.com/OCA/mis-builder/tree/16.0/mis_builder
|
||||
:alt: OCA/mis-builder
|
||||
.. |badge4| image:: https://img.shields.io/badge/weblate-Translate%20me-F47D42.png
|
||||
:target: https://translation.odoo-community.org/projects/mis-builder-16-0/mis-builder-16-0-mis_builder
|
||||
:alt: Translate me on Weblate
|
||||
.. |badge5| image:: https://img.shields.io/badge/runboat-Try%20me-875A7B.png
|
||||
:target: https://runboat.odoo-community.org/builds?repo=OCA/mis-builder&target_branch=16.0
|
||||
:alt: Try me on Runboat
|
||||
|
||||
|badge1| |badge2| |badge3| |badge4| |badge5|
|
||||
|
||||
This module allows you to build Management Information Systems dashboards.
|
||||
Such style of reports presents KPI in rows and time periods in columns.
|
||||
Reports mainly fetch data from account moves, but can also combine data coming
|
||||
from arbitrary Odoo models. Reports can be exported to PDF, Excel and they
|
||||
can be added to Odoo dashboards.
|
||||
|
||||
**Table of contents**
|
||||
|
||||
.. contents::
|
||||
:local:
|
||||
|
||||
Installation
|
||||
============
|
||||
|
||||
Your preferred way to install addons will work with MIS Builder.
|
||||
|
||||
An easy way to install it with all its dependencies is using pip:
|
||||
|
||||
* ``pip install --pre odoo12-addon-mis_builder``
|
||||
* then restart Odoo, update the addons list in your database, and install
|
||||
the MIS Builder application.
|
||||
|
||||
Usage
|
||||
=====
|
||||
|
||||
To configure this module, you need to:
|
||||
|
||||
* Go to Accounting > Configuration > MIS Reporting > MIS Report Templates where
|
||||
you can create report templates by defining KPI's. KPI's constitute the rows of your
|
||||
reports. Such report templates are time independent.
|
||||
|
||||
.. figure:: https://raw.githubusercontent.com/OCA/mis-builder/10.0/mis_builder/static/description/ex_report_template.png
|
||||
:alt: Sample report template
|
||||
:width: 80 %
|
||||
:align: center
|
||||
|
||||
* Then in Accounting > Reports > MIS Reporting > MIS Reports you can create report instance by
|
||||
binding the templates to time periods, hence defining the columns of your reports.
|
||||
|
||||
.. figure:: https://raw.githubusercontent.com/OCA/mis-builder/10.0/mis_builder/static/description/ex_report_settings.png
|
||||
:alt: Sample report configuration
|
||||
:width: 80 %
|
||||
:align: center
|
||||
|
||||
* From the MIS Reports view, you can preview the report, add it to and Odoo dashboard,
|
||||
and export it to PDF or Excel.
|
||||
|
||||
.. figure:: https://raw.githubusercontent.com/OCA/mis-builder/10.0/mis_builder/static/description/ex_report_preview.png
|
||||
:alt: Sample preview
|
||||
:width: 80 %
|
||||
:align: center
|
||||
|
||||
Development
|
||||
===========
|
||||
|
||||
A typical extension is to provide a mechanism to filter reports on analytic dimensions
|
||||
or operational units. To implement this, you can override _get_additional_move_line_filter
|
||||
and _get_additional_filter to further filter move lines or queries based on a user
|
||||
selection. A typical use case could be to add an analytic account field on mis.report.instance,
|
||||
or even on mis.report.instance.period if you want different columns to show different
|
||||
analytic accounts.
|
||||
|
||||
Known issues / Roadmap
|
||||
======================
|
||||
|
||||
The mis_builder `roadmap <https://github.com/OCA/mis-builder/issues?q=is%3Aopen+is%3Aissue+label%3Aenhancement>`_
|
||||
and `known issues <https://github.com/OCA/mis-builder/issues?q=is%3Aopen+is%3Aissue+label%3Abug>`_ can
|
||||
be found on GitHub.
|
||||
|
||||
Changelog
|
||||
=========
|
||||
|
||||
16.0.5.1.9 (2024-02-09)
|
||||
~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
**Bugfixes**
|
||||
|
||||
- Restore compatibility with python 3.9 (`#590 <https://github.com/OCA/mis-builder/issues/590>`_)
|
||||
|
||||
|
||||
16.0.5.1.8 (2024-02-08)
|
||||
~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
**Bugfixes**
|
||||
|
||||
- Resolve a permission issue when creating report periods with a user without admin rights. (`#596 <https://github.com/OCA/mis-builder/issues/596>`_)
|
||||
|
||||
|
||||
16.0.5.1.0 (2023-04-04)
|
||||
~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
**Features**
|
||||
|
||||
- Improve UX by adding the option to edit the pivot date directly on the view.
|
||||
|
||||
16.0.5.0.0 (2023-04-01)
|
||||
~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
**Features**
|
||||
|
||||
- Migration to 16.0
|
||||
|
||||
- Addition of a generic filter domain on reports and columns.
|
||||
- Addition of a search bar to the widget. The corresponding search view is configurable
|
||||
per report.
|
||||
- Huge improvement of the widget style. This was long overdue.
|
||||
- Make the MIS Report menu accessible to the Billing Administrator group
|
||||
(instead of the hidden Show Full Accounting Features), to align with the access rules
|
||||
and avoid giving a false sense of security. This also makes the menu discoverable to
|
||||
new users.
|
||||
- Removal of analytic fetures because the upstream ``analytic_distribution`` mechanism
|
||||
is not compatible; support may be introduced in separate module, depending on use
|
||||
cases.
|
||||
- Abandon the ``mis_report_filters`` context key which had security implication.
|
||||
It is replaced by a ``mis_analytic_domain`` context key which is ANDed with other
|
||||
report-defined filters. (`#472 <https://github.com/OCA/mis-builder/issues/472>`_)
|
||||
- Rename the ``get_filter_descriptions_from_context`` method to
|
||||
``get_filter_descriptions``. This method may be overridden to provide additional
|
||||
subtitles on the PDF or XLS report, representing user-selected filters.
|
||||
- The ``hide_analytic_filters`` has been replaced by ``widget_show_filters``.
|
||||
- The visibility of the settings button on the widget is now controlled by a
|
||||
``show_settings_button``. Before it was visible only for the ``account_user`` group
|
||||
but this was not flexible enough.
|
||||
- The widget configuration settings are now grouped in a dedicated ``Widget`` tab in
|
||||
the report configuration form.
|
||||
|
||||
**Bugfixes**
|
||||
|
||||
- Fix access error when previewing or printing report. (`#415 <https://github.com/OCA/mis-builder/issues/415>`_)
|
||||
|
||||
|
||||
15.0.4.0.5 (2022-07-19)
|
||||
~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
**Bugfixes**
|
||||
|
||||
- Support users without timezone. (`#388 <https://github.com/OCA/mis-builder/issues/388>`_)
|
||||
|
||||
|
||||
15.0.4.0.4 (2022-07-19)
|
||||
~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
**Bugfixes**
|
||||
|
||||
- Allow deleting a report that has subreports. (`#431 <https://github.com/OCA/mis-builder/issues/431>`_)
|
||||
|
||||
|
||||
15.0.4.0.2 (2022-02-16)
|
||||
~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
**Bugfixes**
|
||||
|
||||
- Fix access right issue when clicking the "Save" button on a MIS Report Instance form. (`#410 <https://github.com/OCA/mis-builder/issues/410>`_)
|
||||
|
||||
|
||||
14.0.4.0.0 (2022-01-08)
|
||||
~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
**Features**
|
||||
|
||||
- Remove various field size limits. (`#332 <https://github.com/OCA/mis-builder/issues/332>`_)
|
||||
|
||||
|
||||
**Bugfixes**
|
||||
|
||||
- Support for the Odoo 13+ multi-company model. In multi-company mode, several allowed
|
||||
companies can be declared on MIS Report instances, and the report operates on the
|
||||
intersection of report companies and companies selected in the user context. (`#327 <https://github.com/OCA/mis-builder/issues/327>`_)
|
||||
- The ``get_additional_query_filter`` argument of ``evaluate()`` is now propagated
|
||||
correctly. (`#375 <https://github.com/OCA/mis-builder/issues/375>`_)
|
||||
- Use the ``parent_state`` field of ``account.move.line`` to filter entries in ``posted``
|
||||
and ``draft`` state only. Before, when reporting in draft mode, all entries were used
|
||||
(i.e. there was no filter), and that started including the cancelled entries/invoices in
|
||||
Odoo 13.+.
|
||||
|
||||
This change also contains a **breaking change** in the internal API. For quite a while
|
||||
the ``target_move argument`` of AEP and other methods was not used by MIS Builder itself
|
||||
and was kept for backward compatibility. To avoid rippling effects of the necessary
|
||||
change to use ``parent_state``, we now remove this argument. (`#377 <https://github.com/OCA/mis-builder/issues/377>`_)
|
||||
|
||||
|
||||
14.0.3.6.7 (2021-06-02)
|
||||
~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
**Bugfixes**
|
||||
|
||||
- When on a MIS Report Instance, if you wanted to generate a new line of type comparison, you couldn't currently select any existing period to compare.
|
||||
This happened because the field domain was searching in a NewId context, thus not finding a correct period.
|
||||
Changing the domain and making it use a computed field with a search for the _origin record solves the problem. (`#361 <https://github.com/OCA/mis-builder/issues/361>`_)
|
||||
|
||||
|
||||
14.0.3.6.6 (2021-04-23)
|
||||
~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
**Bugfixes**
|
||||
|
||||
- Fix drilldown action name when the account model has been customized. (`#350 <https://github.com/OCA/mis-builder/issues/350>`_)
|
||||
|
||||
|
||||
14.0.3.6.5 (2021-04-23)
|
||||
~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
**Bugfixes**
|
||||
|
||||
- While duplicating a MIS report instance, comparison columns are ignored because
|
||||
they would raise an error otherwise, as they keep the old source_cmpcol_from_id
|
||||
and source_cmpcol_to_id from the original record. (`#343 <https://github.com/OCA/mis-builder/issues/343>`_)
|
||||
|
||||
|
||||
14.0.3.6.4 (2021-04-06)
|
||||
~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
**Features**
|
||||
|
||||
- The drilldown action name displayed on the breadcrumb has been revised.
|
||||
The kpi description and the account ``display_name`` are shown instead
|
||||
of the kpi's technical definition. (`#304 <https://github.com/OCA/mis-builder/issues/304>`_)
|
||||
- Add analytic group filters on report instance, periods and in the interactive
|
||||
view. (`#320 <https://github.com/OCA/mis-builder/issues/320>`_)
|
||||
|
||||
|
||||
13.0.3.6.3 (2020-08-28)
|
||||
~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
**Bugfixes**
|
||||
|
||||
- Having a "Compare columns" added on a KPI with an associated style using a
|
||||
Factor/Divider did lead to the said factor being applied on the percentages
|
||||
when exporting to XLSX. (`#300 <https://github.com/OCA/mis-builder/issues/300>`_)
|
||||
|
||||
|
||||
**Misc**
|
||||
|
||||
- `#280 <https://github.com/OCA/mis-builder/issues/280>`_, `#296 <https://github.com/OCA/mis-builder/issues/296>`_
|
||||
|
||||
|
||||
13.0.3.6.2 (2020-04-22)
|
||||
~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
**Bugfixes**
|
||||
|
||||
- The "Settings" button is now displayed for users with the "Show full accounting features" right when previewing a report. (`#281 <https://github.com/OCA/mis-builder/issues/281>`_)
|
||||
|
||||
|
||||
13.0.3.6.1 (2020-04-22)
|
||||
~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
**Bugfixes**
|
||||
|
||||
- Fix ``TypeError: 'module' object is not iterable`` when using
|
||||
budgets by account. (`#276 <https://github.com/OCA/mis-builder/issues/276>`_)
|
||||
|
||||
|
||||
13.0.3.6.0 (2020-03-28)
|
||||
~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
**Features**
|
||||
|
||||
- Add column-level filters on analytic account and analytic tags.
|
||||
These filters are combined with a AND with the report-level filters
|
||||
and cannot be modified in the preview. (`#138 <https://github.com/OCA/mis-builder/issues/138>`_)
|
||||
- Access to KPI from other reports in KPI expressions, aka subreports. In a
|
||||
report template, one can list named "subreports" (other report templates). When
|
||||
evaluating expressions, you can access KPI's of subreports with a dot-prefix
|
||||
notation. Example: you can define a MIS Report for a "Balance Sheet", and then
|
||||
have another MIS Report "Balance Sheet Ratios" that fetches KPI's from "Balance
|
||||
Sheet" to create new KPI's for the ratios (e.g. balance_sheet.current_assets /
|
||||
balance_sheet.total_assets). (`#155 <https://github.com/OCA/mis-builder/issues/155>`_)
|
||||
|
||||
|
||||
13.0.3.5.0 (2020-01-??)
|
||||
~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Migration to odoo 13.0.
|
||||
|
||||
12.0.3.5.0 (2019-10-26)
|
||||
~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
**Features**
|
||||
|
||||
- The ``account_id`` field of the model selected in 'Move lines source'
|
||||
in the Period form can now be a Many2one
|
||||
relationship with any model that has a ``code`` field (not only with
|
||||
``account.account`` model). To this end, the model to be used for Actuals
|
||||
move lines can be configured on the report template. It can be something else
|
||||
than move lines and the only constraint is that its ``account_id`` field
|
||||
has a ``code`` field. (`#149 <https://github.com/oca/mis-builder/issues/149>`_)
|
||||
- Add ``source_aml_model_name`` field so extension modules providing
|
||||
alternative data sources can more easily customize their data source. (`#214 <https://github.com/oca/mis-builder/issues/214>`_)
|
||||
- Support analytic tag filters in the backend view and preview widget.
|
||||
Selecting several tags in the filter means filtering on move lines which
|
||||
have *all* these tags set. This is to support the most common use case of
|
||||
using tags for different dimensions. The filter also makes a AND with the
|
||||
analytic account filter. (`#228 <https://github.com/oca/mis-builder/issues/228>`_)
|
||||
- Display company in account details rows in multi-company mode. (`#242 <https://github.com/oca/mis-builder/issues/242>`_)
|
||||
|
||||
|
||||
**Bugfixes**
|
||||
|
||||
- Propagate context to xlsx report, so the analytic account filter
|
||||
works when exporting to xslx too. This also requires a fix to
|
||||
``report_xlsx`` (see https://github.com/OCA/reporting-engine/pull/259). (`#178 <https://github.com/oca/mis-builder/issues/178>`_)
|
||||
- In columns of type Sum, preserve styles for KPIs that are not summable
|
||||
(eg percentage values). Before this fix, such cells were displayed without
|
||||
style. (`#219 <https://github.com/oca/mis-builder/issues/219>`_)
|
||||
- In Excel export, keep the percentage point suffix (pp) instead of replacing it with %. (`#220 <https://github.com/oca/mis-builder/issues/220>`_)
|
||||
|
||||
|
||||
12.0.3.4.0 (2019-07-09)
|
||||
~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
**Features**
|
||||
|
||||
- New year-to-date mode for defining periods. (`#165 <https://github.com/oca/mis-builder/issues/165>`_)
|
||||
- Add support for move lines with negative debit or credit.
|
||||
Used by some for storno accounting. Not officially supported. (`#175 <https://github.com/oca/mis-builder/issues/175>`_)
|
||||
- In Excel export, use a number format with thousands separator. The
|
||||
specific separator used depends on the Excel configuration (eg regional
|
||||
settings). (`#190 <https://github.com/oca/mis-builder/issues/190>`_)
|
||||
- Add generation date/time at the end of the XLS export. (`#191 <https://github.com/oca/mis-builder/issues/191>`_)
|
||||
- In presence of Sub KPIs, report more informative user errors when
|
||||
non-multi expressions yield tuples of incorrect lenght. (`#196 <https://github.com/oca/mis-builder/issues/196>`_)
|
||||
|
||||
|
||||
**Bugfixes**
|
||||
|
||||
- Fix rendering of percentage types in Excel export. (`#192 <https://github.com/oca/mis-builder/issues/192>`_)
|
||||
|
||||
|
||||
12.0.3.3.0 (2019-01-26)
|
||||
~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
**Features**
|
||||
|
||||
*Dynamic analytic filters in report preview are not yet available in 11,
|
||||
this requires an update to the JS widget that proved difficult to implement
|
||||
so far. Help welcome.*
|
||||
|
||||
- Analytic account filters. On a report, an analytic
|
||||
account can be selected for filtering. The filter will
|
||||
be applied to move lines queries. A filter box is also
|
||||
available in the widget to let the user select the analytic
|
||||
account during report preview. (`#15 <https://github.com/oca/mis-builder/issues/15>`_)
|
||||
- Control visibility of analytic filter combo box in widget.
|
||||
This is useful to hide the analytic filters on reports where
|
||||
they do not make sense, such as balance sheet reports. (`#42 <https://github.com/oca/mis-builder/issues/42>`_)
|
||||
- Display analytic filters in the header of exported pdf and xls. (`#44 <https://github.com/oca/mis-builder/issues/44>`_)
|
||||
- Replace the last old gtk icons with fontawesome icons. (`#104 <https://github.com/oca/mis-builder/issues/104>`_)
|
||||
- Use active_test=False in AEP queries.
|
||||
This is important for reports involving inactive taxes.
|
||||
This should not negatively effect existing reports, because
|
||||
an accounting report must take into account all existing move lines
|
||||
even if they reference objects such as taxes, journals, accounts types
|
||||
that have been deactivated since their creation. (`#107 <https://github.com/oca/mis-builder/issues/107>`_)
|
||||
- int(), float() and round() support for AccountingNone. (`#108 <https://github.com/oca/mis-builder/issues/108>`_)
|
||||
- Allow referencing subkpis by name by writing `kpi_x.subkpi_y` in expressions. (`#114 <https://github.com/oca/mis-builder/issues/114>`_)
|
||||
- Add an option to control the display of the start/end dates in the
|
||||
column headers. It is disabled by default (this is a change compared
|
||||
to previous behaviour). (`#118 <https://github.com/oca/mis-builder/issues/118>`_)
|
||||
- Add evaluate method to mis.report. This is a simplified
|
||||
method to evaluate kpis of a report over a time period,
|
||||
without creating a mis.report.instance. (`#123 <https://github.com/oca/mis-builder/issues/123>`_)
|
||||
|
||||
**Bugs**
|
||||
|
||||
- In the style form, hide the "Hide always" checkbox when "Hide always inherit"
|
||||
is checked, as for all other syle elements. (`#121 <https://github.com/OCA/mis-builder/pull/121>_`)
|
||||
|
||||
**Upgrading from 3.2 (breaking changes)**
|
||||
|
||||
If you use ``Actuals (alternative)`` data source in combination with analytic
|
||||
filters, the underlying model must now have an ``analytic_account_id`` field.
|
||||
|
||||
|
||||
11.0.3.2.2 (2018-06-30)
|
||||
~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
* [FIX] Fix bug in company_default_get call returning
|
||||
id instead of recordset
|
||||
(`#103 <https://github.com/OCA/mis-builder/pull/103>`_)
|
||||
* [IMP] add "hide always" style property to make hidden KPI's
|
||||
(for KPI that serve as basis for other formulas, but do not
|
||||
need to be displayed).
|
||||
(`#46 <https://github.com/OCA/mis-builder/issues/46>`_)
|
||||
|
||||
11.0.3.2.1 (2018-05-29)
|
||||
~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
* [FIX] Missing comparison operator for AccountingNone
|
||||
leading to errors in pbal computations
|
||||
(`#93 <https://github.com/OCA/mis-builder/issue/93>`_)
|
||||
|
||||
10.0.3.2.0 (2018-05-02)
|
||||
~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
* [FIX] make subkpi ordering deterministic
|
||||
(`#71 <https://github.com/OCA/mis-builder/issues/71>`_)
|
||||
* [ADD] report instance level option to disable account expansion,
|
||||
enabling the creation of detailed templates while deferring the decision
|
||||
of rendering the details or not to the report instance
|
||||
(`#74 <https://github.com/OCA/mis-builder/issues/74>`_)
|
||||
* [ADD] pbal and nbal accounting expressions, to sum positive
|
||||
and negative balances respectively (ie ignoring accounts with negative,
|
||||
resp positive balances)
|
||||
(`#86 <https://github.com/OCA/mis-builder/issues/86>`_)
|
||||
|
||||
11.0.3.1.2 (2018-02-04)
|
||||
~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Migration to Odoo 11. No new feature.
|
||||
(`#67 <https://github.com/OCA/mis-builder/pull/67>`_)
|
||||
|
||||
10.0.3.1.1 (2017-11-14)
|
||||
~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
New features:
|
||||
|
||||
* [ADD] month and year relative periods, easier to use than
|
||||
date ranges for the most common case.
|
||||
(`#2 <https://github.com/OCA/mis-builder/issues/2>`_)
|
||||
* [ADD] multi-company consolidation support, with currency conversion
|
||||
(the conversion rate date is the end of the reporting period)
|
||||
(`#7 <https://github.com/OCA/mis-builder/issues/7>`_,
|
||||
`#3 <https://github.com/OCA/mis-builder/issues/3>`_)
|
||||
* [ADD] provide ref, datetime, dateutil, time, user in the evaluation
|
||||
context of move line domains; among other things, this allows using
|
||||
references to xml ids (such as account types or tax tags) when
|
||||
querying move lines
|
||||
(`#26 <https://github.com/OCA/mis-builder/issues/26>`_).
|
||||
* [ADD] extended account selectors: you can now select accounts using
|
||||
any domain on account.account, not only account codes
|
||||
``balp[('account_type', '=', 'asset_receivable')]``
|
||||
(`#4 <https://github.com/OCA/mis-builder/issues/4>`_).
|
||||
* [IMP] in the report instance configuration form, the filters are
|
||||
now grouped in a notebook page, this improves readability and
|
||||
extensibility
|
||||
(`#39 <https://github.com/OCA/mis-builder/issues/39>`_).
|
||||
|
||||
Bug fixes:
|
||||
|
||||
* [FIX] fix error when saving periods in comparison mode on newly
|
||||
created (not yet saved) report instances.
|
||||
`#50 <https://github.com/OCA/mis-builder/pull/50>`_
|
||||
* [FIX] improve display of Base Date report instance view.
|
||||
`#51 <https://github.com/OCA/mis-builder/pull/51>`_
|
||||
|
||||
Upgrading from 3.0 (breaking changes):
|
||||
|
||||
* Alternative move line data sources must have a company_id field.
|
||||
|
||||
10.0.3.0.4 (2017-10-14)
|
||||
~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Bug fix:
|
||||
|
||||
* [FIX] issue with initial balance rounding.
|
||||
`#30 <https://github.com/OCA/mis-builder/issues/30>`_
|
||||
|
||||
10.0.3.0.3 (2017-10-03)
|
||||
~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Bug fix:
|
||||
|
||||
* [FIX] fix error saving KPI on newly created reports.
|
||||
`#18 <https://github.com/OCA/mis-builder/issues/18>`_
|
||||
|
||||
10.0.3.0.2 (2017-10-01)
|
||||
~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
New features:
|
||||
|
||||
* [ADD] Alternative move line source per report column.
|
||||
This makes mis buidler accounting expressions work on any model
|
||||
that has debit, credit, account_id and date fields. Provided you can
|
||||
expose, say, committed purchases, or your budget as a view with
|
||||
debit, credit and account_id, this opens up a lot of possibilities
|
||||
* [ADD] Comparison column source (more flexible than the previous,
|
||||
now deprecated, comparison mechanism).
|
||||
CAVEAT: there is no automated migration to the new mechanism.
|
||||
* [ADD] Sum column source, to create columns that add/subtract
|
||||
other columns.
|
||||
* [ADD] mis.kpi.data abstract model as a basis for manual KPI values
|
||||
supporting automatic ajustment to the reporting time period (the basis
|
||||
for budget item, but could also server other purposes, such as manually
|
||||
entering some KPI values, such as number of employee)
|
||||
* [ADD] mis_builder_budget module providing a new budget data source
|
||||
* [ADD] new "hide empty" style property
|
||||
* [IMP] new AEP method to get accounts involved in an expression
|
||||
(this is useful to find which KPI relate to a given P&L
|
||||
acount, to implement budget control)
|
||||
* [IMP] many UI improvements
|
||||
* [IMP] many code style improvements and some refactoring
|
||||
* [IMP] add the column date_from, date_to in expression evaluation context,
|
||||
as well as time, datetime and dateutil modules
|
||||
|
||||
Main bug fixes:
|
||||
|
||||
* [FIX] deletion of templates and reports (cascade and retricts)
|
||||
(https://github.com/OCA/account-financial-reporting/issues/281)
|
||||
* [FIX] copy of reports
|
||||
(https://github.com/OCA/account-financial-reporting/issues/282)
|
||||
* [FIX] better error message when periods have wrong/missing dates
|
||||
(https://github.com/OCA/account-financial-reporting/issues/283)
|
||||
* [FIX] xlsx export of string types KPI
|
||||
(https://github.com/OCA/account-financial-reporting/issues/285)
|
||||
* [FIX] sorting of detail by account
|
||||
* [FIX] computation bug in detail by account when multiple accounting
|
||||
expressions were used in a KPI
|
||||
* [FIX] permission issue when adding report to dashboard with non admin user
|
||||
|
||||
10.0.2.0.3 (unreleased)
|
||||
~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
* [IMP] more robust behaviour in presence of missing expressions
|
||||
* [FIX] indent style
|
||||
* [FIX] local variable 'ctx' referenced before assignment when generating
|
||||
reports with no objects
|
||||
* [IMP] use fontawesome icons
|
||||
* [MIG] migrate to 10.0
|
||||
* [FIX] unicode error when exporting to Excel
|
||||
* [IMP] provide full access to mis builder style for group Adviser.
|
||||
|
||||
9.0.2.0.2 (2016-09-27)
|
||||
~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
* [IMP] Add refresh button in mis report preview.
|
||||
* [IMP] Widget code changes to allow to add fields in the widget more easily.
|
||||
|
||||
9.0.2.0.1 (2016-05-26)
|
||||
~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
* [IMP] remove unused argument in declare_and_compute_period()
|
||||
for a cleaner API. This is a breaking API changing merged in
|
||||
urgency before it is used by other modules.
|
||||
|
||||
9.0.2.0.0 (2016-05-24)
|
||||
~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Part of the work for this release has been done at the Sorrento sprint
|
||||
April 26-29, 2016. The rest (ie a major refactoring) has been done in
|
||||
the weeks after.
|
||||
|
||||
* [IMP] hide button box in edit mode on the report instance settings form
|
||||
* [FIX] Fix sum aggregation of non-stored fields
|
||||
(https://github.com/OCA/account-financial-reporting/issues/178)
|
||||
* [IMP] There is now a default style at the report level
|
||||
* [CHG] Number display properties (rounding, prefix, suffix, factor) are
|
||||
now defined in styles
|
||||
* [CHG] Percentage difference are rounded to 1 digit instead of the kpi's
|
||||
rounding, as the KPI rounding does not make sense in this case
|
||||
* [CHG] The divider suffix (k, M, etc) is not inserted automatically anymore
|
||||
because it is inconsistent when working with prefixes; you need to add it
|
||||
manually in the suffix
|
||||
* [IMP] AccountingExpressionProcessor now supports 'balu' expressions
|
||||
to obtain the unallocated profit/loss of previous fiscal years;
|
||||
get_unallocated_pl is the corresponding convenience method
|
||||
* [IMP] AccountingExpressionProcessor now has easy methods to obtain
|
||||
balances by account: get_balances_initial, get_balances_end,
|
||||
get_balances_variation
|
||||
* [IMP] there is now an auto-expand feature to automatically display
|
||||
a detail by account for selected kpis
|
||||
* [IMP] the kpi and period lists are now manipulated through forms instead
|
||||
of directly in the tree views
|
||||
* [IMP] it is now possible to create a report through a wizard, such
|
||||
reports are deemed temporary and available through a "Last Reports Generated"
|
||||
menu, they are garbaged collected automatically, unless saved permanently,
|
||||
which can be done using a Save button
|
||||
* [IMP] there is now a beginner mode to configure simple reports with
|
||||
only one period
|
||||
* [IMP] it is now easier to configure periods with fixed start/end dates
|
||||
* [IMP] the new sub-kpi mechanism allows the creation of columns
|
||||
with multiple values, or columns with different values
|
||||
* [IMP] thanks to the new style model, the Excel export is now styled
|
||||
* [IMP] a new style model is now used to centralize style configuration
|
||||
* [FIX] use =like instead of like to search for accounts, because
|
||||
the % are added by the user in the expressions
|
||||
* [FIX] Correctly compute the initial balance of income and expense account
|
||||
based on the start of the fiscal year
|
||||
* [IMP] Support date ranges (from OCA/server-tools/date_range) as a more
|
||||
flexible alternative to fiscal periods
|
||||
* v9 migration: fiscal periods are removed, account charts are removed,
|
||||
consolidation accounts have been removed
|
||||
|
||||
8.0.1.0.0 (2016-04-27)
|
||||
~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
* The copy of a MIS Report Instance now copies period.
|
||||
https://github.com/OCA/account-financial-reporting/pull/181
|
||||
* The copy of a MIS Report Template now copies KPIs and queries.
|
||||
https://github.com/OCA/account-financial-reporting/pull/177
|
||||
* Usability: the default view for MIS Report instances is now the rendered preview,
|
||||
and the settings are accessible through a gear icon in the list view and
|
||||
a button in the preview.
|
||||
https://github.com/OCA/account-financial-reporting/pull/170
|
||||
* Display blank cells instead of 0.0 when there is no data.
|
||||
https://github.com/OCA/account-financial-reporting/pull/169
|
||||
* Usability: better layout of the MIS Report periods settings on small screens.
|
||||
https://github.com/OCA/account-financial-reporting/pull/167
|
||||
* Include the download buttons inside the MIS Builder widget, and refactor
|
||||
the widget to open the door to analytic filtering in the previews.
|
||||
https://github.com/OCA/account-financial-reporting/pull/151
|
||||
* Add KPI rendering prefixes (so you can print $ in front of the value).
|
||||
https://github.com/OCA/account-financial-reporting/pull/158
|
||||
* Add hooks for analytic filtering.
|
||||
https://github.com/OCA/account-financial-reporting/pull/128
|
||||
https://github.com/OCA/account-financial-reporting/pull/131
|
||||
|
||||
8.0.0.2.0
|
||||
~~~~~~~~~
|
||||
|
||||
Pre-history. Or rather, you need to look at the git log.
|
||||
|
||||
Bug Tracker
|
||||
===========
|
||||
|
||||
Bugs are tracked on `GitHub Issues <https://github.com/OCA/mis-builder/issues>`_.
|
||||
In case of trouble, please check there if your issue has already been reported.
|
||||
If you spotted it first, help us to smash it by providing a detailed and welcomed
|
||||
`feedback <https://github.com/OCA/mis-builder/issues/new?body=module:%20mis_builder%0Aversion:%2016.0%0A%0A**Steps%20to%20reproduce**%0A-%20...%0A%0A**Current%20behavior**%0A%0A**Expected%20behavior**>`_.
|
||||
|
||||
Do not contact contributors directly about support or help with technical issues.
|
||||
|
||||
Credits
|
||||
=======
|
||||
|
||||
Authors
|
||||
~~~~~~~
|
||||
|
||||
* ACSONE SA/NV
|
||||
|
||||
Contributors
|
||||
~~~~~~~~~~~~
|
||||
|
||||
* Stéphane Bidoul <stephane.bidoul@acsone.eu>
|
||||
* Laetitia Gangloff <laetitia.gangloff@acsone.eu>
|
||||
* Adrien Peiffer <adrien.peiffer@acsone.eu>
|
||||
* Alexis de Lattre <alexis.delattre@akretion.com>
|
||||
* Alexandre Fayolle <alexandre.fayolle@camptocamp.com>
|
||||
* Jordi Ballester <jordi.ballester@eficent.com>
|
||||
* Thomas Binsfeld <thomas.binsfeld@gmail.com>
|
||||
* Giovanni Capalbo <giovanni@therp.nl>
|
||||
* Marco Calcagni <mcalcagni@dinamicheaziendali.it>
|
||||
* Sébastien Beau <sebastien.beau@akretion.com>
|
||||
* Laurent Mignon <laurent.mignon@acsone.eu>
|
||||
* Luc De Meyer <luc.demeyer@noviat.com>
|
||||
* Benjamin Willig <benjamin.willig@acsone.eu>
|
||||
* Martronic SA <info@martronic.ch>
|
||||
* nicomacr <nmr@adhoc.com.ar>
|
||||
* Juan Jose Scarafia <jjs@adhoc.com.ar>
|
||||
* Richard deMeester <richard@willowit.com.au>
|
||||
* Eric Caudal <eric.caudal@elico-corp.com>
|
||||
* Andrea Stirpe <a.stirpe@onestein.nl>
|
||||
* Maxence Groine <mgroine@fiefmanage.ch>
|
||||
* Arnaud Pineux <arnaud.pineux@acsone.eu>
|
||||
* Ernesto Tejeda <ernesto.tejeda@tecnativa.com>
|
||||
* Pedro M. Baeza <pedro.baeza@tecnativa.com>
|
||||
* Alexey Pelykh <alexey.pelykh@corphub.eu>
|
||||
* Jairo Llopis (https://www.moduon.team/)
|
||||
* Dzung Tran <dungtd@trobz.com>
|
||||
* Hoang Diep <hoang@trobz.com>
|
||||
|
||||
Maintainers
|
||||
~~~~~~~~~~~
|
||||
|
||||
This module is maintained by the OCA.
|
||||
|
||||
.. image:: https://odoo-community.org/logo.png
|
||||
:alt: Odoo Community Association
|
||||
:target: https://odoo-community.org
|
||||
|
||||
OCA, or the Odoo Community Association, is a nonprofit organization whose
|
||||
mission is to support the collaborative development of Odoo features and
|
||||
promote its widespread use.
|
||||
|
||||
.. |maintainer-sbidoul| image:: https://github.com/sbidoul.png?size=40px
|
||||
:target: https://github.com/sbidoul
|
||||
:alt: sbidoul
|
||||
|
||||
Current `maintainer <https://odoo-community.org/page/maintainer-role>`__:
|
||||
|
||||
|maintainer-sbidoul|
|
||||
|
||||
This module is part of the `OCA/mis-builder <https://github.com/OCA/mis-builder/tree/16.0/mis_builder>`_ project on GitHub.
|
||||
|
||||
You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute.
|
||||
|
|
@ -1,6 +0,0 @@
|
|||
# Copyright 2014 ACSONE SA/NV (<http://acsone.eu>)
|
||||
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html).
|
||||
|
||||
from . import models
|
||||
from . import wizard
|
||||
from . import report
|
||||
|
|
@ -1,47 +0,0 @@
|
|||
# Copyright 2014-2018 ACSONE SA/NV (<http://acsone.eu>)
|
||||
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html).
|
||||
|
||||
{
|
||||
"name": "MIS Builder",
|
||||
"version": "16.0.5.5.1",
|
||||
"category": "Reporting",
|
||||
"summary": """
|
||||
Build 'Management Information System' Reports and Dashboards
|
||||
""",
|
||||
"author": "ACSONE SA/NV, " "Odoo Community Association (OCA)",
|
||||
"website": "https://github.com/OCA/mis-builder",
|
||||
"depends": [
|
||||
"account",
|
||||
"board",
|
||||
"report_xlsx", # OCA/reporting-engine
|
||||
"date_range", # OCA/server-ux
|
||||
],
|
||||
"data": [
|
||||
"security/res_groups.xml",
|
||||
"wizard/mis_builder_dashboard.xml",
|
||||
"views/mis_report.xml",
|
||||
"views/mis_report_instance.xml",
|
||||
"views/mis_report_style.xml",
|
||||
"datas/ir_cron.xml",
|
||||
"security/ir.model.access.csv",
|
||||
"security/mis_builder_security.xml",
|
||||
"report/mis_report_instance_qweb.xml",
|
||||
"report/mis_report_instance_xlsx.xml",
|
||||
],
|
||||
"assets": {
|
||||
"web.assets_backend": [
|
||||
"mis_builder/static/src/components/mis_report_widget.esm.js",
|
||||
"mis_builder/static/src/components/mis_report_widget.xml",
|
||||
"mis_builder/static/src/components/mis_report_widget.css",
|
||||
],
|
||||
"web.report_assets_common": [
|
||||
"/mis_builder/static/src/css/report.css",
|
||||
],
|
||||
},
|
||||
"qweb": ["static/src/xml/mis_report_widget.xml"],
|
||||
"installable": True,
|
||||
"application": True,
|
||||
"license": "AGPL-3",
|
||||
"development_status": "Production/Stable",
|
||||
"maintainers": ["sbidoul"],
|
||||
}
|
||||
|
|
@ -1,13 +0,0 @@
|
|||
<?xml version="1.0" encoding="UTF-8" ?>
|
||||
<odoo noupdate="1">
|
||||
<record id="ir_cron_vacuum_temp_reports" model="ir.cron">
|
||||
<field name="name">Vacuum temporary reports</field>
|
||||
<field name="interval_number">4</field>
|
||||
<field name="interval_type">hours</field>
|
||||
<field name="numbercall">-1</field>
|
||||
<field eval="False" name="doall" />
|
||||
<field ref="model_mis_report_instance" name="model_id" />
|
||||
<field name="code">model._vacuum_report()</field>
|
||||
<field name="active" eval="True" />
|
||||
</record>
|
||||
</odoo>
|
||||
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
|
|
@ -1,25 +0,0 @@
|
|||
# Copyright 2024 Tecnativa - Víctor Martínez
|
||||
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).
|
||||
|
||||
from openupgradelib import openupgrade
|
||||
|
||||
|
||||
@openupgrade.migrate()
|
||||
def migrate(env, version):
|
||||
"""Set the value of the analytic_domain field."""
|
||||
openupgrade.logged_query(
|
||||
env.cr,
|
||||
"""
|
||||
UPDATE mis_report_instance_period
|
||||
SET analytic_domain = CONCAT('[("analytic_distribution_search", "in", [', analytic_account_id::VARCHAR, '])]')
|
||||
WHERE analytic_account_id IS NOT NULL
|
||||
""", # noqa: E501
|
||||
)
|
||||
openupgrade.logged_query(
|
||||
env.cr,
|
||||
"""
|
||||
UPDATE mis_report_instance
|
||||
SET analytic_domain = CONCAT('[("analytic_distribution_search", "in", [', analytic_account_id::VARCHAR, '])]')
|
||||
WHERE analytic_account_id IS NOT NULL
|
||||
""", # noqa: E501
|
||||
)
|
||||
|
|
@ -1,10 +0,0 @@
|
|||
# Copyright 2023 ACSONE SA/NV (<http://acsone.eu>)
|
||||
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html).
|
||||
|
||||
import odoo
|
||||
from odoo import api
|
||||
|
||||
|
||||
def migrate(cr, installed_version):
|
||||
env = api.Environment(cr, odoo.SUPERUSER_ID, {})
|
||||
env["mis.report.instance.period"].search([])._compute_source_aml_model_id()
|
||||
|
|
@ -1,11 +0,0 @@
|
|||
# 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
|
||||
from . import mis_report_subreport
|
||||
from . import mis_report_instance
|
||||
from . import mis_report_style
|
||||
from . import aep
|
||||
from . import mis_kpi_data
|
||||
from . import prorata_read_group_mixin
|
||||
from . import mis_report_instance_annotation
|
||||
|
|
@ -1,215 +0,0 @@
|
|||
# Copyright 2016 Thomas Binsfeld
|
||||
# Copyright 2016 ACSONE SA/NV (<http://acsone.eu>)
|
||||
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html).
|
||||
"""
|
||||
Provides the AccountingNone singleton.
|
||||
|
||||
AccountingNone is a null value that dissolves in basic arithmetic operations,
|
||||
as illustrated in the examples below. In comparisons, AccountingNone behaves
|
||||
the same as zero.
|
||||
|
||||
>>> 1 + 1
|
||||
2
|
||||
>>> 1 + AccountingNone
|
||||
1
|
||||
>>> AccountingNone + 1
|
||||
1
|
||||
>>> AccountingNone + None
|
||||
AccountingNone
|
||||
>>> None + AccountingNone
|
||||
AccountingNone
|
||||
>>> +AccountingNone
|
||||
AccountingNone
|
||||
>>> -AccountingNone
|
||||
AccountingNone
|
||||
>>> -(AccountingNone)
|
||||
AccountingNone
|
||||
>>> AccountingNone - 1
|
||||
-1
|
||||
>>> 1 - AccountingNone
|
||||
1
|
||||
>>> abs(AccountingNone)
|
||||
AccountingNone
|
||||
>>> AccountingNone - None
|
||||
AccountingNone
|
||||
>>> None - AccountingNone
|
||||
AccountingNone
|
||||
>>> AccountingNone / 2
|
||||
0.0
|
||||
>>> 2 / AccountingNone
|
||||
Traceback (most recent call last):
|
||||
...
|
||||
ZeroDivisionError
|
||||
>>> AccountingNone / AccountingNone
|
||||
AccountingNone
|
||||
>>> AccountingNone // 2
|
||||
0.0
|
||||
>>> 2 // AccountingNone
|
||||
Traceback (most recent call last):
|
||||
...
|
||||
ZeroDivisionError
|
||||
>>> AccountingNone // AccountingNone
|
||||
AccountingNone
|
||||
>>> AccountingNone * 2
|
||||
0.0
|
||||
>>> 2 * AccountingNone
|
||||
0.0
|
||||
>>> AccountingNone * AccountingNone
|
||||
AccountingNone
|
||||
>>> AccountingNone * None
|
||||
AccountingNone
|
||||
>>> None * AccountingNone
|
||||
AccountingNone
|
||||
>>> str(AccountingNone)
|
||||
''
|
||||
>>> bool(AccountingNone)
|
||||
False
|
||||
>>> AccountingNone > 0
|
||||
False
|
||||
>>> AccountingNone < 0
|
||||
False
|
||||
>>> AccountingNone < 1
|
||||
True
|
||||
>>> AccountingNone > 1
|
||||
False
|
||||
>>> 0 < AccountingNone
|
||||
False
|
||||
>>> 0 > AccountingNone
|
||||
False
|
||||
>>> 1 < AccountingNone
|
||||
False
|
||||
>>> 1 > AccountingNone
|
||||
True
|
||||
>>> AccountingNone == 0
|
||||
True
|
||||
>>> AccountingNone == 0.0
|
||||
True
|
||||
>>> AccountingNone == None
|
||||
True
|
||||
>>> AccountingNone >= AccountingNone
|
||||
True
|
||||
>>> AccountingNone <= AccountingNone
|
||||
True
|
||||
>>> round(AccountingNone, 2)
|
||||
0.0
|
||||
>>> float(AccountingNone)
|
||||
0.0
|
||||
>>> int(AccountingNone)
|
||||
0
|
||||
"""
|
||||
|
||||
__all__ = ["AccountingNone"]
|
||||
|
||||
|
||||
class AccountingNoneType:
|
||||
def __add__(self, other):
|
||||
if other is None:
|
||||
return AccountingNone
|
||||
return other
|
||||
|
||||
__radd__ = __add__
|
||||
|
||||
def __sub__(self, other):
|
||||
if other is None:
|
||||
return AccountingNone
|
||||
return -other
|
||||
|
||||
def __rsub__(self, other):
|
||||
if other is None:
|
||||
return AccountingNone
|
||||
return other
|
||||
|
||||
def __iadd__(self, other):
|
||||
if other is None:
|
||||
return AccountingNone
|
||||
return other
|
||||
|
||||
def __isub__(self, other):
|
||||
if other is None:
|
||||
return AccountingNone
|
||||
return -other
|
||||
|
||||
def __abs__(self):
|
||||
return self
|
||||
|
||||
def __pos__(self):
|
||||
return self
|
||||
|
||||
def __neg__(self):
|
||||
return self
|
||||
|
||||
def __div__(self, other):
|
||||
if other is AccountingNone:
|
||||
return AccountingNone
|
||||
return 0.0
|
||||
|
||||
def __rdiv__(self, other):
|
||||
raise ZeroDivisionError
|
||||
|
||||
def __floordiv__(self, other):
|
||||
if other is AccountingNone:
|
||||
return AccountingNone
|
||||
return 0.0
|
||||
|
||||
def __rfloordiv__(self, other):
|
||||
raise ZeroDivisionError
|
||||
|
||||
def __truediv__(self, other):
|
||||
if other is AccountingNone:
|
||||
return AccountingNone
|
||||
return 0.0
|
||||
|
||||
def __rtruediv__(self, other):
|
||||
raise ZeroDivisionError
|
||||
|
||||
def __mul__(self, other):
|
||||
if other is None or other is AccountingNone:
|
||||
return AccountingNone
|
||||
return 0.0
|
||||
|
||||
__rmul__ = __mul__
|
||||
|
||||
def __repr__(self):
|
||||
return "AccountingNone"
|
||||
|
||||
def __str__(self):
|
||||
return ""
|
||||
|
||||
def __nonzero__(self):
|
||||
return False
|
||||
|
||||
def __bool__(self):
|
||||
return False
|
||||
|
||||
def __eq__(self, other):
|
||||
return other == 0 or other is None or other is AccountingNone
|
||||
|
||||
def __lt__(self, other):
|
||||
return other > 0
|
||||
|
||||
def __gt__(self, other):
|
||||
return other < 0
|
||||
|
||||
def __le__(self, other):
|
||||
return other >= 0
|
||||
|
||||
def __ge__(self, other):
|
||||
return other <= 0
|
||||
|
||||
def __float__(self):
|
||||
return 0.0
|
||||
|
||||
def __int__(self):
|
||||
return 0
|
||||
|
||||
def __round__(self, ndigits):
|
||||
return 0.0
|
||||
|
||||
|
||||
AccountingNone = AccountingNoneType()
|
||||
|
||||
|
||||
if __name__ == "__main__": # pragma: no cover
|
||||
import doctest
|
||||
|
||||
doctest.testmod()
|
||||
|
|
@ -1,660 +0,0 @@
|
|||
# Copyright 2014 ACSONE SA/NV (<http://acsone.eu>)
|
||||
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html).
|
||||
|
||||
import logging
|
||||
import re
|
||||
from collections import defaultdict
|
||||
|
||||
from odoo import _, fields
|
||||
from odoo.exceptions import UserError
|
||||
from odoo.models import expression
|
||||
from odoo.tools.float_utils import float_is_zero
|
||||
from odoo.tools.safe_eval import datetime, dateutil, safe_eval, time
|
||||
|
||||
from .accounting_none import AccountingNone
|
||||
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
_DOMAIN_START_RE = re.compile(r"\(|(['\"])[!&|]\1")
|
||||
|
||||
|
||||
def _is_domain(s):
|
||||
"""Test if a string looks like an Odoo domain"""
|
||||
return _DOMAIN_START_RE.match(s)
|
||||
|
||||
|
||||
class Accumulator:
|
||||
"""A simple class to accumulate debit, credit and custom field values.
|
||||
|
||||
>>> acc1 = Accumulator(["f1", "f2"])
|
||||
>>> acc1.debit
|
||||
AccountingNone
|
||||
>>> acc1.credit
|
||||
AccountingNone
|
||||
>>> acc1.custom_fields
|
||||
{'f1': AccountingNone, 'f2': AccountingNone}
|
||||
>>> acc1.add_debit_credit(10, 20)
|
||||
>>> acc1.debit, acc1.credit
|
||||
(10, 20)
|
||||
>>> acc1.add_custom_field("f1", 10)
|
||||
>>> acc1.custom_fields
|
||||
{'f1': 10, 'f2': AccountingNone}
|
||||
>>> acc2 = Accumulator(["f1", "f2"])
|
||||
>>> acc2.add_debit_credit(21, 31)
|
||||
>>> acc2.add_custom_field("f2", 41)
|
||||
>>> acc1 += acc2
|
||||
>>> acc1.debit, acc1.credit
|
||||
(31, 51)
|
||||
>>> acc1.custom_fields
|
||||
{'f1': 10, 'f2': 41}
|
||||
"""
|
||||
|
||||
def __init__(self, custom_field_names=()):
|
||||
self.debit = AccountingNone
|
||||
self.credit = AccountingNone
|
||||
self.custom_fields = {
|
||||
custom_field: AccountingNone for custom_field in custom_field_names
|
||||
}
|
||||
|
||||
def has_data(self):
|
||||
return (
|
||||
self.debit is not AccountingNone
|
||||
or self.credit is not AccountingNone
|
||||
or any(v is not AccountingNone for v in self.custom_fields.values())
|
||||
)
|
||||
|
||||
def add_debit_credit(self, debit, credit):
|
||||
self.debit += debit
|
||||
self.credit += credit
|
||||
|
||||
def add_custom_field(self, field, value):
|
||||
self.custom_fields[field] += value
|
||||
|
||||
def __iadd__(self, other):
|
||||
self.debit += other.debit
|
||||
self.credit += other.credit
|
||||
for field in self.custom_fields:
|
||||
self.custom_fields[field] += other.custom_fields[field]
|
||||
return self
|
||||
|
||||
|
||||
class AccountingExpressionProcessor:
|
||||
"""Processor for accounting expressions.
|
||||
|
||||
Expressions of the form
|
||||
<field><mode>(.fieldname)?[accounts][optional move line domain]
|
||||
are supported, where:
|
||||
* field is bal, crd, deb, pbal (positive balances only),
|
||||
nbal (negative balance only), fld (custom field)
|
||||
* mode is i (initial balance), e (ending balance),
|
||||
p (moves over period)
|
||||
* .fieldname is used only with fldp and specifies the field name to sum
|
||||
* there is also a special u mode (unallocated P&L) which computes
|
||||
the sum from the beginning until the beginning of the fiscal year
|
||||
of the period; it is only meaningful for P&L accounts
|
||||
* accounts is a list of accounts, possibly containing % wildcards,
|
||||
or a domain expression on account.account
|
||||
* an optional domain on move lines allowing filters on eg analytic
|
||||
accounts or journal
|
||||
|
||||
Examples:
|
||||
* bal[70]: variation of the balance of moves on account 70
|
||||
over the period (it is the same as balp[70]);
|
||||
* bali[70,60]: balance of accounts 70 and 60 at the start of period;
|
||||
* bale[1%]: balance of accounts starting with 1 at end of period.
|
||||
* fldp.quantity[60%]: sum of the quantity field of moves on accounts 60
|
||||
|
||||
How to use:
|
||||
* repeatedly invoke parse_expr() for each expression containing
|
||||
accounting variables as described above; this lets the processor
|
||||
group domains and modes and accounts;
|
||||
* when all expressions have been parsed, invoke done_parsing()
|
||||
to notify the processor that it can prepare to query (mainly
|
||||
search all accounts - children, consolidation - that will need to
|
||||
be queried;
|
||||
* for each period, call do_queries(), then call replace_expr() for each
|
||||
expression to replace accounting variables with their resulting value
|
||||
for the given period.
|
||||
|
||||
How it works:
|
||||
* by accumulating the expressions before hand, it ensures to do the
|
||||
strict minimum number of queries to the database (for each period,
|
||||
one query per domain and mode);
|
||||
* it queries using the orm read_group which reduces to a query with
|
||||
sum on debit and credit and group by on account_id and company_id,
|
||||
(note: it seems the orm then does one query per account to fetch
|
||||
the account name...);
|
||||
* additionally, one query per view/consolidation account is done to
|
||||
discover the children accounts.
|
||||
"""
|
||||
|
||||
MODE_VARIATION = "p"
|
||||
MODE_INITIAL = "i"
|
||||
MODE_END = "e"
|
||||
MODE_UNALLOCATED = "u"
|
||||
|
||||
_ACC_RE = re.compile(
|
||||
r"(?P<field>\bbal|\bpbal|\bnbal|\bcrd|\bdeb|\bfld)"
|
||||
r"(?P<mode>[piseu])?"
|
||||
r"(?P<fld_name>\.[a-zA-Z0-9_]+)?"
|
||||
r"\s*"
|
||||
r"(?P<account_sel>_[a-zA-Z0-9]+|\[.*?\])"
|
||||
r"\s*"
|
||||
r"(?P<ml_domain>\[.*?\])?"
|
||||
)
|
||||
|
||||
def __init__(self, companies, currency=None, account_model="account.account"):
|
||||
self.env = companies.env
|
||||
self.companies = companies
|
||||
if not currency:
|
||||
self.currency = companies.mapped("currency_id")
|
||||
if len(self.currency) > 1:
|
||||
raise UserError(
|
||||
_(
|
||||
"If currency_id is not provided, "
|
||||
"all companies must have the same currency."
|
||||
)
|
||||
)
|
||||
else:
|
||||
self.currency = currency
|
||||
self.dp = self.currency.decimal_places
|
||||
# before done_parsing: {(ml_domain, mode): set(acc_domain)}
|
||||
# after done_parsing: {(ml_domain, mode): list(account_ids)}
|
||||
self._map_account_ids = defaultdict(set)
|
||||
# {account_domain: set(account_ids)}
|
||||
self._account_ids_by_acc_domain = defaultdict(set)
|
||||
# smart ending balance (returns AccountingNone if there
|
||||
# are no moves in period and 0 initial balance), implies
|
||||
# a first query to get the initial balance and another
|
||||
# to get the variation, so it's a bit slower
|
||||
self.smart_end = True
|
||||
# custom field to query and sum
|
||||
self._custom_fields = set()
|
||||
# Account model
|
||||
self._account_model = self.env[account_model].with_context(active_test=False)
|
||||
|
||||
def _account_codes_to_domain(self, account_codes):
|
||||
"""Convert a comma separated list of account codes
|
||||
(possibly with wildcards) to a domain on account.account.
|
||||
"""
|
||||
elems = []
|
||||
for account_code in account_codes.split(","):
|
||||
account_code = account_code.strip()
|
||||
if "%" in account_code:
|
||||
elems.append([("code", "=like", account_code)])
|
||||
else:
|
||||
elems.append([("code", "=", account_code)])
|
||||
return tuple(expression.OR(elems))
|
||||
|
||||
def _parse_match_object(self, mo):
|
||||
"""Split a match object corresponding to an accounting variable
|
||||
|
||||
Returns field, mode, fld_name, account domain, move line domain.
|
||||
"""
|
||||
domain_eval_context = {
|
||||
"ref": self.env.ref,
|
||||
"user": self.env.user,
|
||||
"time": time,
|
||||
"datetime": datetime,
|
||||
"dateutil": dateutil,
|
||||
}
|
||||
field, mode, fld_name, account_sel, ml_domain = mo.groups()
|
||||
# handle some legacy modes
|
||||
if not mode:
|
||||
mode = self.MODE_VARIATION
|
||||
elif mode == "s":
|
||||
mode = self.MODE_END
|
||||
# custom fields
|
||||
if fld_name:
|
||||
assert fld_name[0] == "."
|
||||
fld_name = fld_name[1:] # strip leading dot
|
||||
# convert account selector to account domain
|
||||
if account_sel.startswith("_"):
|
||||
# legacy bal_NNN%
|
||||
acc_domain = self._account_codes_to_domain(account_sel[1:])
|
||||
else:
|
||||
assert account_sel[0] == "[" and account_sel[-1] == "]"
|
||||
inner_account_sel = account_sel[1:-1].strip()
|
||||
if not inner_account_sel:
|
||||
# empty selector: select all accounts
|
||||
acc_domain = tuple()
|
||||
elif _is_domain(inner_account_sel):
|
||||
# account selector is a domain
|
||||
acc_domain = tuple(safe_eval(account_sel, domain_eval_context))
|
||||
else:
|
||||
# account selector is a list of account codes
|
||||
acc_domain = self._account_codes_to_domain(inner_account_sel)
|
||||
# move line domain
|
||||
if ml_domain:
|
||||
assert ml_domain[0] == "[" and ml_domain[-1] == "]"
|
||||
ml_domain = tuple(safe_eval(ml_domain, domain_eval_context))
|
||||
else:
|
||||
ml_domain = tuple()
|
||||
return field, mode, fld_name, acc_domain, ml_domain
|
||||
|
||||
def parse_expr(self, expr):
|
||||
"""Parse an expression, extracting accounting variables.
|
||||
|
||||
Move line domains and account selectors are extracted and
|
||||
stored in the map so when all expressions have been parsed,
|
||||
we know which account domains to query for each move line domain
|
||||
and mode.
|
||||
"""
|
||||
for mo in self._ACC_RE.finditer(expr):
|
||||
field, mode, fld_name, acc_domain, ml_domain = self._parse_match_object(mo)
|
||||
if mode == self.MODE_END and self.smart_end:
|
||||
modes = (self.MODE_INITIAL, self.MODE_VARIATION, self.MODE_END)
|
||||
else:
|
||||
modes = (mode,)
|
||||
for mode in modes:
|
||||
key = (ml_domain, mode)
|
||||
self._map_account_ids[key].add(acc_domain)
|
||||
if field == "fld":
|
||||
if mode != self.MODE_VARIATION:
|
||||
raise UserError(
|
||||
_(
|
||||
"`fld` can only be used with mode `p` (variation) "
|
||||
"in expression %s",
|
||||
expr,
|
||||
)
|
||||
)
|
||||
if not fld_name:
|
||||
raise UserError(
|
||||
_("`fld` must have a field name in exression %s", expr)
|
||||
)
|
||||
self._custom_fields.add(fld_name)
|
||||
else:
|
||||
if fld_name:
|
||||
raise UserError(
|
||||
_(
|
||||
"`%(field)s` cannot have a field name "
|
||||
"in expression %(expr)s",
|
||||
field=field,
|
||||
expr=expr,
|
||||
)
|
||||
)
|
||||
|
||||
def done_parsing(self):
|
||||
"""Replace account domains by account ids in map"""
|
||||
for key, acc_domains in self._map_account_ids.items():
|
||||
all_account_ids = set()
|
||||
for acc_domain in acc_domains:
|
||||
acc_domain_with_company = expression.AND(
|
||||
[acc_domain, [("company_id", "in", self.companies.ids)]]
|
||||
)
|
||||
account_ids = self._account_model.search(acc_domain_with_company).ids
|
||||
self._account_ids_by_acc_domain[acc_domain].update(account_ids)
|
||||
all_account_ids.update(account_ids)
|
||||
self._map_account_ids[key] = list(all_account_ids)
|
||||
|
||||
@classmethod
|
||||
def has_account_var(cls, expr):
|
||||
"""Test if an string contains an accounting variable."""
|
||||
return bool(cls._ACC_RE.search(expr))
|
||||
|
||||
def get_account_ids_for_expr(self, expr):
|
||||
"""Get a set of account ids that are involved in an expression.
|
||||
|
||||
Prerequisite: done_parsing() must have been invoked.
|
||||
"""
|
||||
account_ids = set()
|
||||
for mo in self._ACC_RE.finditer(expr):
|
||||
_, _, _, acc_domain, _ = self._parse_match_object(mo)
|
||||
account_ids.update(self._account_ids_by_acc_domain[acc_domain])
|
||||
return account_ids
|
||||
|
||||
def get_aml_domain_for_expr(self, expr, date_from, date_to, account_id=None):
|
||||
"""Get a domain on account.move.line for an expression.
|
||||
|
||||
Prerequisite: done_parsing() must have been invoked.
|
||||
|
||||
Returns a domain that can be used to search on account.move.line.
|
||||
"""
|
||||
aml_domains = []
|
||||
date_domain_by_mode = {}
|
||||
for mo in self._ACC_RE.finditer(expr):
|
||||
field, mode, fld_name, acc_domain, ml_domain = self._parse_match_object(mo)
|
||||
aml_domain = list(ml_domain)
|
||||
account_ids = set()
|
||||
account_ids.update(self._account_ids_by_acc_domain[acc_domain])
|
||||
if not account_id:
|
||||
aml_domain.append(("account_id", "in", tuple(account_ids)))
|
||||
else:
|
||||
# filter on account_id
|
||||
if account_id in account_ids:
|
||||
aml_domain.append(("account_id", "=", account_id))
|
||||
else:
|
||||
continue
|
||||
if field == "crd":
|
||||
aml_domain.append(("credit", "<>", 0.0))
|
||||
elif field == "deb":
|
||||
aml_domain.append(("debit", "<>", 0.0))
|
||||
elif fld_name:
|
||||
aml_domain.append((fld_name, "!=", False))
|
||||
aml_domains.append(expression.normalize_domain(aml_domain))
|
||||
if mode not in date_domain_by_mode:
|
||||
date_domain_by_mode[mode] = self.get_aml_domain_for_dates(
|
||||
date_from, date_to, mode
|
||||
)
|
||||
assert aml_domains
|
||||
# TODO we could do this for more precision:
|
||||
# AND(OR(aml_domains[mode]), date_domain[mode]) for each mode
|
||||
return expression.OR(aml_domains) + expression.OR(date_domain_by_mode.values())
|
||||
|
||||
def get_aml_domain_for_dates(self, date_from, date_to, mode):
|
||||
if mode == self.MODE_VARIATION:
|
||||
domain = [("date", ">=", date_from), ("date", "<=", date_to)]
|
||||
elif mode in (self.MODE_INITIAL, self.MODE_END):
|
||||
# for income and expense account, sum from the beginning
|
||||
# of the current fiscal year only, for balance sheet accounts
|
||||
# sum from the beginning of time
|
||||
date_from_date = fields.Date.from_string(date_from)
|
||||
# TODO this takes the fy from the first company
|
||||
# make that user controllable (nice to have)?
|
||||
fy_date_from = self.companies[0].compute_fiscalyear_dates(date_from_date)[
|
||||
"date_from"
|
||||
]
|
||||
domain = [
|
||||
"|",
|
||||
("date", ">=", fields.Date.to_string(fy_date_from)),
|
||||
("account_id.include_initial_balance", "=", True),
|
||||
]
|
||||
if mode == self.MODE_INITIAL:
|
||||
domain.append(("date", "<", date_from))
|
||||
elif mode == self.MODE_END:
|
||||
domain.append(("date", "<=", date_to))
|
||||
elif mode == self.MODE_UNALLOCATED:
|
||||
date_from_date = fields.Date.from_string(date_from)
|
||||
# TODO this takes the fy from the first company
|
||||
# make that user controllable (nice to have)?
|
||||
fy_date_from = self.companies[0].compute_fiscalyear_dates(date_from_date)[
|
||||
"date_from"
|
||||
]
|
||||
domain = [
|
||||
("date", "<", fields.Date.to_string(fy_date_from)),
|
||||
("account_id.include_initial_balance", "=", False),
|
||||
]
|
||||
return expression.normalize_domain(domain)
|
||||
|
||||
def _get_company_rates(self, date):
|
||||
# get exchange rates for each company with its rouding
|
||||
company_rates = {}
|
||||
target_rate = self.currency.with_context(date=date).rate
|
||||
for company in self.companies:
|
||||
if company.currency_id != self.currency:
|
||||
rate = target_rate / company.currency_id.with_context(date=date).rate
|
||||
else:
|
||||
rate = 1.0
|
||||
company_rates[company.id] = (rate, company.currency_id.decimal_places)
|
||||
return company_rates
|
||||
|
||||
def do_queries(
|
||||
self,
|
||||
date_from,
|
||||
date_to,
|
||||
additional_move_line_filter=None,
|
||||
aml_model=None,
|
||||
):
|
||||
"""Query sums of debit and credit for all accounts and domains
|
||||
used in expressions.
|
||||
|
||||
This method must be executed after done_parsing().
|
||||
"""
|
||||
if not aml_model:
|
||||
aml_model = self.env["account.move.line"]
|
||||
else:
|
||||
aml_model = self.env[aml_model]
|
||||
aml_model = aml_model.with_context(active_test=False)
|
||||
company_rates = self._get_company_rates(date_to)
|
||||
# {(domain, mode): {account_id: Accumulator}}
|
||||
self._data = defaultdict(
|
||||
lambda: defaultdict(
|
||||
lambda: Accumulator(self._custom_fields),
|
||||
)
|
||||
)
|
||||
domain_by_mode = {}
|
||||
ends = []
|
||||
for key in self._map_account_ids:
|
||||
domain, mode = key
|
||||
if mode == self.MODE_END and self.smart_end:
|
||||
# postpone computation of ending balance
|
||||
ends.append((domain, mode))
|
||||
continue
|
||||
if mode not in domain_by_mode:
|
||||
domain_by_mode[mode] = self.get_aml_domain_for_dates(
|
||||
date_from, date_to, mode
|
||||
)
|
||||
domain = list(domain) + domain_by_mode[mode]
|
||||
domain.append(("account_id", "in", self._map_account_ids[key]))
|
||||
if additional_move_line_filter:
|
||||
domain.extend(additional_move_line_filter)
|
||||
# fetch sum of debit/credit, grouped by account_id
|
||||
_logger.debug("read_group domain: %s", domain)
|
||||
try:
|
||||
accs = aml_model.read_group(
|
||||
domain,
|
||||
[
|
||||
"debit",
|
||||
"credit",
|
||||
"account_id",
|
||||
"company_id",
|
||||
*self._custom_fields,
|
||||
],
|
||||
["account_id", "company_id"],
|
||||
lazy=False,
|
||||
)
|
||||
except ValueError as e:
|
||||
raise UserError(
|
||||
_(
|
||||
'Error while querying move line source "%(model_name)s". '
|
||||
"This is likely due to a filter or expression referencing "
|
||||
"a field that does not exist in the model.\n\n"
|
||||
"The technical error message is: %(exception)s. "
|
||||
)
|
||||
% dict(
|
||||
model_name=aml_model._description,
|
||||
exception=e,
|
||||
)
|
||||
) from e
|
||||
for acc in accs:
|
||||
rate, dp = company_rates[acc["company_id"][0]]
|
||||
debit = acc["debit"] or 0.0
|
||||
credit = acc["credit"] or 0.0
|
||||
if mode in (self.MODE_INITIAL, self.MODE_UNALLOCATED) and float_is_zero(
|
||||
debit - credit, precision_digits=self.dp
|
||||
):
|
||||
# in initial mode, ignore accounts with 0 balance
|
||||
continue
|
||||
# due to branches, it's possible to have multiple groups
|
||||
# with the same account_id, because multiple companies can
|
||||
# use the same account
|
||||
account_data = self._data[key][acc["account_id"][0]]
|
||||
account_data.add_debit_credit(debit * rate, credit * rate)
|
||||
for field_name in self._custom_fields:
|
||||
account_data.add_custom_field(
|
||||
field_name, acc[field_name] or AccountingNone
|
||||
)
|
||||
# compute ending balances by summing initial and variation
|
||||
for key in ends:
|
||||
domain, mode = key
|
||||
initial_data = self._data[(domain, self.MODE_INITIAL)]
|
||||
variation_data = self._data[(domain, self.MODE_VARIATION)]
|
||||
account_ids = set(initial_data.keys()) | set(variation_data.keys())
|
||||
for account_id in account_ids:
|
||||
self._data[key][account_id] += initial_data[account_id]
|
||||
self._data[key][account_id] += variation_data[account_id]
|
||||
|
||||
def replace_expr(self, expr):
|
||||
"""Replace accounting variables in an expression by their amount.
|
||||
|
||||
Returns a new expression string.
|
||||
|
||||
This method must be executed after do_queries().
|
||||
"""
|
||||
|
||||
def f(mo):
|
||||
field, mode, fld_name, acc_domain, ml_domain = self._parse_match_object(mo)
|
||||
key = (ml_domain, mode)
|
||||
account_ids_data = self._data[key]
|
||||
v = AccountingNone
|
||||
account_ids = self._account_ids_by_acc_domain[acc_domain]
|
||||
for account_id in account_ids:
|
||||
entry = account_ids_data[account_id]
|
||||
debit = entry.debit
|
||||
credit = entry.credit
|
||||
if field == "bal":
|
||||
v += debit - credit
|
||||
elif field == "pbal":
|
||||
if debit >= credit:
|
||||
v += debit - credit
|
||||
elif field == "nbal":
|
||||
if debit < credit:
|
||||
v += debit - credit
|
||||
elif field == "deb":
|
||||
v += debit
|
||||
elif field == "crd":
|
||||
v += credit
|
||||
else:
|
||||
assert field == "fld"
|
||||
v += entry.custom_fields[fld_name]
|
||||
# in initial balance mode, assume 0 is None
|
||||
# as it does not make sense to distinguish 0 from "no data"
|
||||
if (
|
||||
v is not AccountingNone
|
||||
and mode in (self.MODE_INITIAL, self.MODE_UNALLOCATED)
|
||||
and float_is_zero(v, precision_digits=self.dp)
|
||||
):
|
||||
v = AccountingNone
|
||||
return "(" + repr(v) + ")"
|
||||
|
||||
return self._ACC_RE.sub(f, expr)
|
||||
|
||||
def replace_exprs_by_account_id(self, exprs):
|
||||
"""Replace accounting variables in a list of expression
|
||||
by their amount, iterating by accounts involved in the expression.
|
||||
|
||||
yields account_id, replaced_expr
|
||||
|
||||
This method must be executed after do_queries().
|
||||
"""
|
||||
|
||||
def f(mo):
|
||||
field, mode, fld_name, acc_domain, ml_domain = self._parse_match_object(mo)
|
||||
key = (ml_domain, mode)
|
||||
# first check if account_id is involved in
|
||||
# the current expression part
|
||||
if account_id not in self._account_ids_by_acc_domain[acc_domain]:
|
||||
return "(AccountingNone)"
|
||||
# here we know account_id is involved in acc_domain
|
||||
account_ids_data = self._data[key]
|
||||
entry = account_ids_data[account_id]
|
||||
debit = entry.debit
|
||||
credit = entry.credit
|
||||
if field == "bal":
|
||||
v = debit - credit
|
||||
elif field == "pbal":
|
||||
if debit >= credit:
|
||||
v = debit - credit
|
||||
else:
|
||||
v = AccountingNone
|
||||
elif field == "nbal":
|
||||
if debit < credit:
|
||||
v = debit - credit
|
||||
else:
|
||||
v = AccountingNone
|
||||
elif field == "deb":
|
||||
v = debit
|
||||
elif field == "crd":
|
||||
v = credit
|
||||
else:
|
||||
assert field == "fld"
|
||||
v = entry.custom_fields[fld_name]
|
||||
# in initial balance mode, assume 0 is None
|
||||
# as it does not make sense to distinguish 0 from "no data"
|
||||
if (
|
||||
v is not AccountingNone
|
||||
and mode in (self.MODE_INITIAL, self.MODE_UNALLOCATED)
|
||||
and float_is_zero(v, precision_digits=self.dp)
|
||||
):
|
||||
v = AccountingNone
|
||||
return "(" + repr(v) + ")"
|
||||
|
||||
account_ids = set()
|
||||
for expr in exprs:
|
||||
for mo in self._ACC_RE.finditer(expr):
|
||||
_, mode, _, acc_domain, ml_domain = self._parse_match_object(mo)
|
||||
key = (ml_domain, mode)
|
||||
account_ids_data = self._data[key]
|
||||
for account_id in self._account_ids_by_acc_domain[acc_domain]:
|
||||
if account_ids_data[account_id].has_data():
|
||||
account_ids.add(account_id)
|
||||
|
||||
for account_id in account_ids:
|
||||
yield account_id, [self._ACC_RE.sub(f, expr) for expr in exprs]
|
||||
|
||||
@classmethod
|
||||
def _get_balances(cls, mode, companies, date_from, date_to):
|
||||
expr = f"deb{mode}[], crd{mode}[]"
|
||||
aep = AccountingExpressionProcessor(companies)
|
||||
# disable smart_end to have the data at once, instead
|
||||
# of initial + variation
|
||||
aep.smart_end = False
|
||||
aep.parse_expr(expr)
|
||||
aep.done_parsing()
|
||||
aep.do_queries(date_from, date_to)
|
||||
return {k: (v.debit, v.credit) for k, v in aep._data[((), mode)].items()}
|
||||
|
||||
@classmethod
|
||||
def get_balances_initial(cls, companies, date):
|
||||
"""A convenience method to obtain the initial balances of all accounts
|
||||
at a given date.
|
||||
|
||||
It is the same as get_balances_end(date-1).
|
||||
|
||||
:param companies:
|
||||
:param date:
|
||||
|
||||
Returns a dictionary: {account_id, (debit, credit)}
|
||||
"""
|
||||
return cls._get_balances(cls.MODE_INITIAL, companies, date, date)
|
||||
|
||||
@classmethod
|
||||
def get_balances_end(cls, companies, date):
|
||||
"""A convenience method to obtain the ending balances of all accounts
|
||||
at a given date.
|
||||
|
||||
It is the same as get_balances_initial(date+1).
|
||||
|
||||
:param companies:
|
||||
:param date:
|
||||
|
||||
Returns a dictionary: {account_id, (debit, credit)}
|
||||
"""
|
||||
return cls._get_balances(cls.MODE_END, companies, date, date)
|
||||
|
||||
@classmethod
|
||||
def get_balances_variation(cls, companies, date_from, date_to):
|
||||
"""A convenience method to obtain the variation of the
|
||||
balances of all accounts over a period.
|
||||
|
||||
:param companies:
|
||||
:param date:
|
||||
|
||||
Returns a dictionary: {account_id, (debit, credit)}
|
||||
"""
|
||||
return cls._get_balances(cls.MODE_VARIATION, companies, date_from, date_to)
|
||||
|
||||
@classmethod
|
||||
def get_unallocated_pl(cls, companies, date):
|
||||
"""A convenience method to obtain the unallocated profit/loss
|
||||
of the previous fiscal years at a given date.
|
||||
|
||||
:param companies:
|
||||
:param date:
|
||||
|
||||
Returns a tuple (debit, credit)
|
||||
"""
|
||||
# TODO shoud we include here the accounts of type "unaffected"
|
||||
# or leave that to the caller?
|
||||
bals = cls._get_balances(cls.MODE_UNALLOCATED, companies, date, date)
|
||||
return tuple(map(sum, zip(*bals.values()))) # noqa: B905
|
||||
|
|
@ -1,129 +0,0 @@
|
|||
# Copyright 2014 ACSONE SA/NV (<http://acsone.eu>)
|
||||
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html).
|
||||
|
||||
|
||||
def _sum(lst):
|
||||
"""Same as stdlib sum but returns None instead of 0
|
||||
in case of empty sequence.
|
||||
|
||||
>>> sum([1])
|
||||
1
|
||||
>>> _sum([1])
|
||||
1
|
||||
>>> sum([1, 2])
|
||||
3
|
||||
>>> _sum([1, 2])
|
||||
3
|
||||
>>> sum([])
|
||||
0
|
||||
>>> _sum([])
|
||||
"""
|
||||
if not lst:
|
||||
return None
|
||||
return sum(lst)
|
||||
|
||||
|
||||
def _avg(lst):
|
||||
"""Arithmetic mean of a sequence. Returns None in case of empty sequence.
|
||||
|
||||
>>> _avg([1])
|
||||
1.0
|
||||
>>> _avg([1, 2])
|
||||
1.5
|
||||
>>> _avg([])
|
||||
"""
|
||||
if not lst:
|
||||
return None
|
||||
return sum(lst) / float(len(lst))
|
||||
|
||||
|
||||
def _min(*args):
|
||||
"""Same as stdlib min but returns None instead of exception
|
||||
in case of empty sequence.
|
||||
|
||||
>>> min(1, 2)
|
||||
1
|
||||
>>> _min(1, 2)
|
||||
1
|
||||
>>> min([1, 2])
|
||||
1
|
||||
>>> _min([1, 2])
|
||||
1
|
||||
>>> min(1)
|
||||
Traceback (most recent call last):
|
||||
File "<stdin>", line 1, in ?
|
||||
TypeError: 'int' object is not iterable
|
||||
>>> _min(1)
|
||||
Traceback (most recent call last):
|
||||
File "<stdin>", line 1, in ?
|
||||
TypeError: 'int' object is not iterable
|
||||
>>> min([1])
|
||||
1
|
||||
>>> _min([1])
|
||||
1
|
||||
>>> min()
|
||||
Traceback (most recent call last):
|
||||
File "<stdin>", line 1, in ?
|
||||
TypeError: min expected at least 1 argument, got 0
|
||||
>>> _min()
|
||||
Traceback (most recent call last):
|
||||
File "<stdin>", line 1, in ?
|
||||
TypeError: min expected at least 1 argument, got 0
|
||||
>>> min([])
|
||||
Traceback (most recent call last):
|
||||
File "<stdin>", line 1, in ?
|
||||
ValueError: min() arg is an empty sequence
|
||||
>>> _min([])
|
||||
"""
|
||||
if len(args) == 1 and not args[0]:
|
||||
return None
|
||||
return min(*args)
|
||||
|
||||
|
||||
def _max(*args):
|
||||
"""Same as stdlib max but returns None instead of exception
|
||||
in case of empty sequence.
|
||||
|
||||
>>> max(1, 2)
|
||||
2
|
||||
>>> _max(1, 2)
|
||||
2
|
||||
>>> max([1, 2])
|
||||
2
|
||||
>>> _max([1, 2])
|
||||
2
|
||||
>>> max(1)
|
||||
Traceback (most recent call last):
|
||||
File "<stdin>", line 1, in ?
|
||||
TypeError: 'int' object is not iterable
|
||||
>>> _max(1)
|
||||
Traceback (most recent call last):
|
||||
File "<stdin>", line 1, in ?
|
||||
TypeError: 'int' object is not iterable
|
||||
>>> max([1])
|
||||
1
|
||||
>>> _max([1])
|
||||
1
|
||||
>>> max()
|
||||
Traceback (most recent call last):
|
||||
File "<stdin>", line 1, in ?
|
||||
TypeError: max expected at least 1 argument, got 0
|
||||
>>> _max()
|
||||
Traceback (most recent call last):
|
||||
File "<stdin>", line 1, in ?
|
||||
TypeError: max expected at least 1 argument, got 0
|
||||
>>> max([])
|
||||
Traceback (most recent call last):
|
||||
File "<stdin>", line 1, in ?
|
||||
ValueError: max() arg is an empty sequence
|
||||
>>> _max([])
|
||||
"""
|
||||
if len(args) == 1 and not args[0]:
|
||||
return None
|
||||
return max(*args)
|
||||
|
||||
|
||||
if __name__ == "__main__": # pragma: no cover
|
||||
import doctest
|
||||
|
||||
doctest.testmod()
|
||||
|
|
@ -1,17 +0,0 @@
|
|||
# Copyright 2016 ACSONE SA/NV (<http://acsone.eu>)
|
||||
# Copyright 2016 Akretion (<http://akretion.com>)
|
||||
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html).
|
||||
|
||||
|
||||
class DataError(Exception):
|
||||
def __init__(self, name, msg):
|
||||
super().__init__()
|
||||
self.name = name
|
||||
self.msg = msg
|
||||
|
||||
def __repr__(self):
|
||||
return f"{self.__class__.__name__}({repr(self.name)})"
|
||||
|
||||
|
||||
class NameDataError(DataError):
|
||||
pass
|
||||
|
|
@ -1,68 +0,0 @@
|
|||
# Copyright 2020 ACSONE SA/NV (<http://acsone.eu>)
|
||||
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html).
|
||||
|
||||
from .mis_safe_eval import NameDataError, mis_safe_eval
|
||||
|
||||
|
||||
class ExpressionEvaluator:
|
||||
def __init__(
|
||||
self,
|
||||
aep,
|
||||
date_from,
|
||||
date_to,
|
||||
additional_move_line_filter=None,
|
||||
aml_model=None,
|
||||
):
|
||||
self.aep = aep
|
||||
self.date_from = date_from
|
||||
self.date_to = date_to
|
||||
self.additional_move_line_filter = additional_move_line_filter
|
||||
self.aml_model = aml_model
|
||||
self._aep_queries_done = False
|
||||
|
||||
def aep_do_queries(self):
|
||||
if self.aep and not self._aep_queries_done:
|
||||
self.aep.do_queries(
|
||||
self.date_from,
|
||||
self.date_to,
|
||||
self.additional_move_line_filter,
|
||||
self.aml_model,
|
||||
)
|
||||
self._aep_queries_done = True
|
||||
|
||||
def eval_expressions(self, expressions, locals_dict):
|
||||
vals = []
|
||||
drilldown_args = []
|
||||
name_error = False
|
||||
for expression in expressions:
|
||||
expr = expression and expression.name or "AccountingNone"
|
||||
if self.aep:
|
||||
replaced_expr = self.aep.replace_expr(expr)
|
||||
else:
|
||||
replaced_expr = expr
|
||||
val = mis_safe_eval(replaced_expr, locals_dict)
|
||||
vals.append(val)
|
||||
if isinstance(val, NameDataError):
|
||||
name_error = True
|
||||
if replaced_expr != expr:
|
||||
drilldown_args.append({"expr": expr})
|
||||
else:
|
||||
drilldown_args.append(None)
|
||||
return vals, drilldown_args, name_error
|
||||
|
||||
def eval_expressions_by_account(self, expressions, locals_dict):
|
||||
if not self.aep:
|
||||
return
|
||||
exprs = [e and e.name or "AccountingNone" for e in expressions]
|
||||
for account_id, replaced_exprs in self.aep.replace_exprs_by_account_id(exprs):
|
||||
vals = []
|
||||
drilldown_args = []
|
||||
name_error = False
|
||||
for expr, replaced_expr in zip(exprs, replaced_exprs): # noqa: B905
|
||||
val = mis_safe_eval(replaced_expr, locals_dict)
|
||||
vals.append(val)
|
||||
if replaced_expr != expr:
|
||||
drilldown_args.append({"expr": expr, "account_id": account_id})
|
||||
else:
|
||||
drilldown_args.append(None)
|
||||
yield account_id, vals, drilldown_args, name_error
|
||||
|
|
@ -1,576 +0,0 @@
|
|||
# Copyright 2014 ACSONE SA/NV (<http://acsone.eu>)
|
||||
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html).
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
from collections import OrderedDict, defaultdict
|
||||
|
||||
from odoo import _
|
||||
from odoo.exceptions import UserError
|
||||
|
||||
from .accounting_none import AccountingNone
|
||||
from .mis_kpi_data import ACC_SUM
|
||||
from .mis_safe_eval import DataError, mis_safe_eval
|
||||
from .simple_array import SimpleArray
|
||||
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class KpiMatrixRow:
|
||||
# TODO: ultimately, the kpi matrix will become ignorant of KPI's and
|
||||
# accounts and know about rows, columns, sub columns and styles only.
|
||||
# It is already ignorant of period and only knowns about columns.
|
||||
# This will require a correct abstraction for expanding row details.
|
||||
|
||||
def __init__(self, matrix, kpi, account_id=None, parent_row=None):
|
||||
self._matrix = matrix
|
||||
self.kpi = kpi
|
||||
self.account_id = account_id
|
||||
self.description = ""
|
||||
self.parent_row = parent_row
|
||||
if not self.account_id:
|
||||
self.style_props = self._matrix._style_model.merge(
|
||||
[self.kpi.report_id.style_id, self.kpi.style_id]
|
||||
)
|
||||
else:
|
||||
self.style_props = self._matrix._style_model.merge(
|
||||
[self.kpi.report_id.style_id, self.kpi.auto_expand_accounts_style_id]
|
||||
)
|
||||
|
||||
@property
|
||||
def label(self):
|
||||
if not self.account_id:
|
||||
return self.kpi.description
|
||||
else:
|
||||
return self._matrix.get_account_name(self.account_id)
|
||||
|
||||
@property
|
||||
def row_id(self):
|
||||
self._matrix._make_row_id(self.kpi.id, self.account_id)
|
||||
|
||||
def iter_cell_tuples(self, cols=None):
|
||||
if cols is None:
|
||||
cols = self._matrix.iter_cols()
|
||||
for col in cols:
|
||||
yield col.get_cell_tuple_for_row(self)
|
||||
|
||||
def iter_cells(self, subcols=None):
|
||||
if subcols is None:
|
||||
subcols = self._matrix.iter_subcols()
|
||||
for subcol in subcols:
|
||||
yield subcol.get_cell_for_row(self)
|
||||
|
||||
def is_empty(self):
|
||||
for cell in self.iter_cells():
|
||||
if cell and cell.val not in (AccountingNone, None):
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
class KpiMatrixCol:
|
||||
def __init__(self, key, label, description, locals_dict, subkpis):
|
||||
self.key = key
|
||||
self.label = label
|
||||
self.description = description
|
||||
self.locals_dict = locals_dict
|
||||
self.colspan = subkpis and len(subkpis) or 1
|
||||
self._subcols = []
|
||||
self.subkpis = subkpis
|
||||
if not subkpis:
|
||||
subcol = KpiMatrixSubCol(self, "", "", 0)
|
||||
self._subcols.append(subcol)
|
||||
else:
|
||||
for i, subkpi in enumerate(subkpis):
|
||||
subcol = KpiMatrixSubCol(self, subkpi.description, "", i)
|
||||
self._subcols.append(subcol)
|
||||
self._cell_tuples_by_row = {} # {row: (cells tuple)}
|
||||
|
||||
def _set_cell_tuple(self, row, cell_tuple):
|
||||
self._cell_tuples_by_row[row] = cell_tuple
|
||||
|
||||
def iter_subcols(self):
|
||||
return self._subcols
|
||||
|
||||
def iter_cell_tuples(self):
|
||||
return self._cell_tuples_by_row.values()
|
||||
|
||||
def get_cell_tuple_for_row(self, row):
|
||||
return self._cell_tuples_by_row.get(row)
|
||||
|
||||
|
||||
class KpiMatrixSubCol:
|
||||
def __init__(self, col, label, description, index=0):
|
||||
self.col = col
|
||||
self.label = label
|
||||
self.description = description
|
||||
self.index = index
|
||||
|
||||
@property
|
||||
def subkpi(self):
|
||||
if self.col.subkpis:
|
||||
return self.col.subkpis[self.index]
|
||||
|
||||
def iter_cells(self):
|
||||
for cell_tuple in self.col.iter_cell_tuples():
|
||||
yield cell_tuple[self.index]
|
||||
|
||||
def get_cell_for_row(self, row):
|
||||
cell_tuple = self.col.get_cell_tuple_for_row(row)
|
||||
if cell_tuple is None:
|
||||
return None
|
||||
return cell_tuple[self.index]
|
||||
|
||||
|
||||
class KpiMatrixCell: # noqa: B903 (immutable data class)
|
||||
def __init__(
|
||||
self,
|
||||
row,
|
||||
subcol,
|
||||
val,
|
||||
val_rendered,
|
||||
val_comment,
|
||||
style_props,
|
||||
drilldown_arg,
|
||||
val_type,
|
||||
):
|
||||
self.row = row
|
||||
self.subcol = subcol
|
||||
self.val = val
|
||||
self.val_rendered = val_rendered
|
||||
self.val_comment = val_comment
|
||||
self.style_props = style_props
|
||||
self.drilldown_arg = drilldown_arg
|
||||
self.val_type = val_type
|
||||
self.cell_id = KpiMatrix._pack_cell_id(self)
|
||||
|
||||
|
||||
class KpiMatrix:
|
||||
def __init__(self, env, multi_company=False, account_model="account.account"):
|
||||
# cache language id for faster rendering
|
||||
lang_model = env["res.lang"]
|
||||
self.lang = lang_model._lang_get(env.user.lang)
|
||||
self._style_model = env["mis.report.style"]
|
||||
self._account_model = env[account_model]
|
||||
# data structures
|
||||
# { kpi: KpiMatrixRow }
|
||||
self._kpi_rows = OrderedDict()
|
||||
# { kpi: {account_id: KpiMatrixRow} }
|
||||
self._detail_rows = {}
|
||||
# { col_key: KpiMatrixCol }
|
||||
self._cols = OrderedDict()
|
||||
# { col_key (left of comparison): [(col_key, base_col_key)] }
|
||||
self._comparison_todo = defaultdict(list)
|
||||
# { col_key (left of sum): (col_key, [(sign, sum_col_key)])
|
||||
self._sum_todo = {}
|
||||
# { account_id: account_name }
|
||||
self._account_names = {}
|
||||
self._multi_company = multi_company
|
||||
|
||||
def declare_kpi(self, kpi):
|
||||
"""Declare a new kpi (row) in the matrix.
|
||||
|
||||
Invoke this first for all kpi, in display order.
|
||||
"""
|
||||
self._kpi_rows[kpi] = KpiMatrixRow(self, kpi)
|
||||
self._detail_rows[kpi] = {}
|
||||
|
||||
def declare_col(self, col_key, label, description, locals_dict, subkpis):
|
||||
"""Declare a new column, giving it an identifier (key).
|
||||
|
||||
Invoke the declare_* methods in display order.
|
||||
"""
|
||||
col = KpiMatrixCol(col_key, label, description, locals_dict, subkpis)
|
||||
self._cols[col_key] = col
|
||||
return col
|
||||
|
||||
def declare_comparison(
|
||||
self, cmpcol_key, col_key, base_col_key, label, description=None
|
||||
):
|
||||
"""Declare a new comparison column.
|
||||
|
||||
Invoke the declare_* methods in display order.
|
||||
"""
|
||||
self._comparison_todo[cmpcol_key] = (col_key, base_col_key, label, description)
|
||||
self._cols[cmpcol_key] = None # reserve slot in insertion order
|
||||
|
||||
def declare_sum(
|
||||
self, sumcol_key, col_to_sum_keys, label, description=None, sum_accdet=False
|
||||
):
|
||||
"""Declare a new summation column.
|
||||
|
||||
Invoke the declare_* methods in display order.
|
||||
:param col_to_sum_keys: [(sign, col_key)]
|
||||
"""
|
||||
self._sum_todo[sumcol_key] = (col_to_sum_keys, label, description, sum_accdet)
|
||||
self._cols[sumcol_key] = None # reserve slot in insertion order
|
||||
|
||||
def set_values(self, kpi, col_key, vals, drilldown_args, tooltips=True):
|
||||
"""Set values for a kpi and a colum.
|
||||
|
||||
Invoke this after declaring the kpi and the column.
|
||||
"""
|
||||
self.set_values_detail_account(
|
||||
kpi, col_key, None, vals, drilldown_args, tooltips
|
||||
)
|
||||
|
||||
def set_values_detail_account(
|
||||
self, kpi, col_key, account_id, vals, drilldown_args, tooltips=True
|
||||
):
|
||||
"""Set values for a kpi and a column and a detail account.
|
||||
|
||||
Invoke this after declaring the kpi and the column.
|
||||
"""
|
||||
if not account_id:
|
||||
row = self._kpi_rows[kpi]
|
||||
else:
|
||||
kpi_row = self._kpi_rows[kpi]
|
||||
if account_id in self._detail_rows[kpi]:
|
||||
row = self._detail_rows[kpi][account_id]
|
||||
else:
|
||||
row = KpiMatrixRow(self, kpi, account_id, parent_row=kpi_row)
|
||||
self._detail_rows[kpi][account_id] = row
|
||||
col = self._cols[col_key]
|
||||
cell_tuple = []
|
||||
assert len(vals) == col.colspan
|
||||
assert len(drilldown_args) == col.colspan
|
||||
for val, drilldown_arg, subcol in zip(vals, drilldown_args, col.iter_subcols()): # noqa: B905
|
||||
if isinstance(val, DataError):
|
||||
val_rendered = val.name
|
||||
val_comment = val.msg
|
||||
else:
|
||||
val_rendered = self._style_model.render(
|
||||
self.lang, row.style_props, kpi.type, val
|
||||
)
|
||||
if row.kpi.multi and subcol.subkpi:
|
||||
val_comment = "{}.{} = {}".format(
|
||||
row.kpi.name,
|
||||
subcol.subkpi.name,
|
||||
row.kpi._get_expression_str_for_subkpi(subcol.subkpi),
|
||||
)
|
||||
else:
|
||||
val_comment = f"{row.kpi.name} = {row.kpi.expression}"
|
||||
cell_style_props = row.style_props
|
||||
if row.kpi.style_expression:
|
||||
# evaluate style expression
|
||||
try:
|
||||
style_name = mis_safe_eval(
|
||||
row.kpi.style_expression, col.locals_dict
|
||||
)
|
||||
except Exception:
|
||||
_logger.error(
|
||||
"Error evaluating style expression <%s>",
|
||||
row.kpi.style_expression,
|
||||
exc_info=True,
|
||||
)
|
||||
if style_name:
|
||||
style = self._style_model.search([("name", "=", style_name)])
|
||||
if style:
|
||||
cell_style_props = self._style_model.merge(
|
||||
[row.style_props, style[0]]
|
||||
)
|
||||
else:
|
||||
_logger.error("Style '%s' not found.", style_name)
|
||||
cell = KpiMatrixCell(
|
||||
row,
|
||||
subcol,
|
||||
val,
|
||||
val_rendered,
|
||||
tooltips and val_comment or None,
|
||||
cell_style_props,
|
||||
drilldown_arg,
|
||||
kpi.type,
|
||||
)
|
||||
cell_tuple.append(cell)
|
||||
assert len(cell_tuple) == col.colspan
|
||||
col._set_cell_tuple(row, cell_tuple)
|
||||
|
||||
def _common_subkpis(self, cols):
|
||||
if not cols:
|
||||
return set()
|
||||
common_subkpis = set(cols[0].subkpis)
|
||||
for col in cols[1:]:
|
||||
common_subkpis = common_subkpis & set(col.subkpis)
|
||||
return common_subkpis
|
||||
|
||||
def compute_comparisons(self):
|
||||
"""Compute comparisons.
|
||||
|
||||
Invoke this after setting all values.
|
||||
"""
|
||||
for (
|
||||
cmpcol_key,
|
||||
(col_key, base_col_key, label, description),
|
||||
) in self._comparison_todo.items():
|
||||
col = self._cols[col_key]
|
||||
base_col = self._cols[base_col_key]
|
||||
common_subkpis = self._common_subkpis([col, base_col])
|
||||
if (col.subkpis or base_col.subkpis) and not common_subkpis:
|
||||
raise UserError(
|
||||
_(
|
||||
"Columns %(descr)s and %(base_descr)s are not comparable",
|
||||
descr=col.description,
|
||||
base_descr=base_col.description,
|
||||
)
|
||||
)
|
||||
if not label:
|
||||
label = f"{col.label} vs {base_col.label}"
|
||||
comparison_col = KpiMatrixCol(
|
||||
cmpcol_key,
|
||||
label,
|
||||
description,
|
||||
{},
|
||||
sorted(common_subkpis, key=lambda s: s.sequence),
|
||||
)
|
||||
self._cols[cmpcol_key] = comparison_col
|
||||
for row in self.iter_rows():
|
||||
cell_tuple = col.get_cell_tuple_for_row(row)
|
||||
base_cell_tuple = base_col.get_cell_tuple_for_row(row)
|
||||
if cell_tuple is None and base_cell_tuple is None:
|
||||
continue
|
||||
if cell_tuple is None:
|
||||
vals = [AccountingNone] * (len(common_subkpis) or 1)
|
||||
else:
|
||||
vals = [
|
||||
cell.val
|
||||
for cell in cell_tuple
|
||||
if not common_subkpis or cell.subcol.subkpi in common_subkpis
|
||||
]
|
||||
if base_cell_tuple is None:
|
||||
base_vals = [AccountingNone] * (len(common_subkpis) or 1)
|
||||
else:
|
||||
base_vals = [
|
||||
cell.val
|
||||
for cell in base_cell_tuple
|
||||
if not common_subkpis or cell.subcol.subkpi in common_subkpis
|
||||
]
|
||||
comparison_cell_tuple = []
|
||||
for val, base_val, comparison_subcol in zip( # noqa: B905
|
||||
vals,
|
||||
base_vals,
|
||||
comparison_col.iter_subcols(),
|
||||
):
|
||||
# TODO FIXME average factors
|
||||
comparison = self._style_model.compare_and_render(
|
||||
self.lang,
|
||||
row.style_props,
|
||||
row.kpi.type,
|
||||
row.kpi.compare_method,
|
||||
val,
|
||||
base_val,
|
||||
1,
|
||||
1,
|
||||
)
|
||||
delta, delta_r, delta_style, delta_type = comparison
|
||||
comparison_cell_tuple.append(
|
||||
KpiMatrixCell(
|
||||
row,
|
||||
comparison_subcol,
|
||||
delta,
|
||||
delta_r,
|
||||
None,
|
||||
delta_style,
|
||||
None,
|
||||
delta_type,
|
||||
)
|
||||
)
|
||||
comparison_col._set_cell_tuple(row, comparison_cell_tuple)
|
||||
|
||||
def compute_sums(self):
|
||||
"""Compute comparisons.
|
||||
|
||||
Invoke this after setting all values.
|
||||
"""
|
||||
for (
|
||||
sumcol_key,
|
||||
(col_to_sum_keys, label, description, sum_accdet),
|
||||
) in self._sum_todo.items():
|
||||
sumcols = [self._cols[k] for (sign, k) in col_to_sum_keys]
|
||||
# TODO check all sumcols are resolved; we need a kind of
|
||||
# recompute queue here so we don't depend on insertion
|
||||
# order
|
||||
common_subkpis = self._common_subkpis(sumcols)
|
||||
if any(c.subkpis for c in sumcols) and not common_subkpis:
|
||||
raise UserError(
|
||||
_(
|
||||
"Sum cannot be computed in column {} "
|
||||
"because the columns to sum have no "
|
||||
"common subkpis"
|
||||
).format(label)
|
||||
)
|
||||
sum_col = KpiMatrixCol(
|
||||
sumcol_key,
|
||||
label,
|
||||
description,
|
||||
{},
|
||||
sorted(common_subkpis, key=lambda s: s.sequence),
|
||||
)
|
||||
self._cols[sumcol_key] = sum_col
|
||||
for row in self.iter_rows():
|
||||
acc = SimpleArray([AccountingNone] * (len(common_subkpis) or 1))
|
||||
if row.kpi.accumulation_method == ACC_SUM and not (
|
||||
row.account_id and not sum_accdet
|
||||
):
|
||||
for sign, col_to_sum in col_to_sum_keys:
|
||||
cell_tuple = self._cols[col_to_sum].get_cell_tuple_for_row(row)
|
||||
if cell_tuple is None:
|
||||
vals = [AccountingNone] * (len(common_subkpis) or 1)
|
||||
else:
|
||||
vals = [
|
||||
cell.val
|
||||
for cell in cell_tuple
|
||||
if not common_subkpis
|
||||
or cell.subcol.subkpi in common_subkpis
|
||||
]
|
||||
if sign == "+":
|
||||
acc += SimpleArray(vals)
|
||||
else:
|
||||
acc -= SimpleArray(vals)
|
||||
self.set_values_detail_account(
|
||||
row.kpi,
|
||||
sumcol_key,
|
||||
row.account_id,
|
||||
acc,
|
||||
[None] * (len(common_subkpis) or 1),
|
||||
tooltips=False,
|
||||
)
|
||||
|
||||
def iter_rows(self):
|
||||
"""Iterate rows in display order.
|
||||
|
||||
yields KpiMatrixRow.
|
||||
"""
|
||||
for kpi_row in self._kpi_rows.values():
|
||||
yield kpi_row
|
||||
detail_rows = self._detail_rows[kpi_row.kpi].values()
|
||||
detail_rows = sorted(detail_rows, key=lambda r: r.label)
|
||||
yield from detail_rows
|
||||
|
||||
def iter_cols(self):
|
||||
"""Iterate columns in display order.
|
||||
|
||||
yields KpiMatrixCol: one for each column or comparison.
|
||||
"""
|
||||
for _col_key, col in self._cols.items():
|
||||
yield col
|
||||
|
||||
def iter_subcols(self):
|
||||
"""Iterate sub columns in display order.
|
||||
|
||||
yields KpiMatrixSubCol: one for each subkpi in each column
|
||||
and comparison.
|
||||
"""
|
||||
for col in self.iter_cols():
|
||||
yield from col.iter_subcols()
|
||||
|
||||
def _load_account_names(self):
|
||||
account_ids = set()
|
||||
for detail_rows in self._detail_rows.values():
|
||||
account_ids.update(detail_rows.keys())
|
||||
accounts = self._account_model.search([("id", "in", list(account_ids))])
|
||||
self._account_names = {a.id: self._get_account_name(a) for a in accounts}
|
||||
|
||||
def _get_account_name(self, account):
|
||||
result = f"{account.code} {account.name}"
|
||||
if self._multi_company:
|
||||
result = f"{result} [{account.company_id.name}]"
|
||||
return result
|
||||
|
||||
def get_account_name(self, account_id):
|
||||
if account_id not in self._account_names:
|
||||
self._load_account_names()
|
||||
return self._account_names[account_id]
|
||||
|
||||
def as_dict(self):
|
||||
header = [{"cols": []}, {"cols": []}]
|
||||
for col in self.iter_cols():
|
||||
header[0]["cols"].append(
|
||||
{
|
||||
"label": col.label,
|
||||
"description": col.description,
|
||||
"colspan": col.colspan,
|
||||
}
|
||||
)
|
||||
for subcol in col.iter_subcols():
|
||||
header[1]["cols"].append(
|
||||
{
|
||||
"label": subcol.label,
|
||||
"description": subcol.description,
|
||||
"colspan": 1,
|
||||
}
|
||||
)
|
||||
|
||||
body = []
|
||||
for row in self.iter_rows():
|
||||
if (
|
||||
row.style_props.hide_empty and row.is_empty()
|
||||
) or row.style_props.hide_always:
|
||||
continue
|
||||
row_data = {
|
||||
"row_id": row.row_id,
|
||||
"parent_row_id": (row.parent_row and row.parent_row.row_id or None),
|
||||
"label": row.label,
|
||||
"description": row.description,
|
||||
"style": self._style_model.to_css_style(row.style_props),
|
||||
"cells": [],
|
||||
}
|
||||
for cell in row.iter_cells():
|
||||
if cell is None:
|
||||
# TODO use subcol style here
|
||||
row_data["cells"].append({})
|
||||
else:
|
||||
if cell.val is AccountingNone or isinstance(cell.val, DataError):
|
||||
val = None
|
||||
else:
|
||||
val = cell.val
|
||||
col_data = {
|
||||
"cell_id": cell.cell_id,
|
||||
"val": val,
|
||||
"val_r": cell.val_rendered,
|
||||
"val_c": cell.val_comment,
|
||||
"style": self._style_model.to_css_style(
|
||||
cell.style_props, no_indent=True
|
||||
),
|
||||
# notes can not be added on 'details by account' lines
|
||||
"can_be_annotated": not cell.row.account_id,
|
||||
}
|
||||
if cell.drilldown_arg:
|
||||
col_data["drilldown_arg"] = cell.drilldown_arg
|
||||
row_data["cells"].append(col_data)
|
||||
body.append(row_data)
|
||||
|
||||
return {"header": header, "body": body}
|
||||
|
||||
# Logic to convert semantic coordinates (period, kpi, subkpi)
|
||||
# to visual coordinates (cell id) and back. The rendering logic musn't know
|
||||
# about semantic concepts such as periods and kpis. Having these well identified
|
||||
# methods allow us to easily spot where the conversion between the rendering and
|
||||
# semantic domain occur.
|
||||
|
||||
@classmethod
|
||||
def _make_row_id(cls, kpi_id: int, account_id: int | None) -> str:
|
||||
return f"{kpi_id}:{account_id or ''}"
|
||||
|
||||
@classmethod
|
||||
def _make_cell_id(
|
||||
cls, kpi_id: int, account_id: int | None, period_id: int, subkpi_id: int | None
|
||||
) -> str:
|
||||
return f"{kpi_id}#{account_id or ''}#{period_id}#{subkpi_id or ''}"
|
||||
|
||||
@classmethod
|
||||
def _pack_cell_id(cls, cell: KpiMatrixCell) -> str:
|
||||
return cls._make_cell_id(
|
||||
cell.row.kpi.id,
|
||||
cell.row.account_id,
|
||||
cell.subcol.col.key,
|
||||
cell.subcol.subkpi and cell.subcol.subkpi.id,
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def _unpack_cell_id(cls, cell_id: str) -> tuple[int, int | None, int, int | None]:
|
||||
kpi_id, account_id, col_key, subkpi_id = cell_id.split("#")
|
||||
kpi_id = int(kpi_id)
|
||||
account_id = int(account_id) if account_id else None
|
||||
period_id = int(col_key)
|
||||
subkpi_id = int(subkpi_id) if subkpi_id else None
|
||||
return kpi_id, account_id, period_id, subkpi_id
|
||||
|
|
@ -1,115 +0,0 @@
|
|||
# Copyright 2017 ACSONE SA/NV
|
||||
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
|
||||
|
||||
from collections import defaultdict
|
||||
|
||||
from odoo import _, api, fields, models
|
||||
from odoo.exceptions import UserError
|
||||
from odoo.osv import expression
|
||||
|
||||
ACC_SUM = "sum"
|
||||
ACC_AVG = "avg"
|
||||
ACC_NONE = "none"
|
||||
|
||||
|
||||
def intersect_days(item_dt_from, item_dt_to, dt_from, dt_to):
|
||||
item_days = (item_dt_to - item_dt_from).days + 1.0
|
||||
i_dt_from = max(dt_from, item_dt_from)
|
||||
i_dt_to = min(dt_to, item_dt_to)
|
||||
i_days = (i_dt_to - i_dt_from).days + 1.0
|
||||
return i_days, item_days
|
||||
|
||||
|
||||
class MisKpiData(models.AbstractModel):
|
||||
"""Abstract class for manually entered KPI values."""
|
||||
|
||||
_name = "mis.kpi.data"
|
||||
_description = "MIS Kpi Data Abtract class"
|
||||
|
||||
name = fields.Char(compute="_compute_name", required=False, readonly=True)
|
||||
kpi_expression_id = fields.Many2one(
|
||||
comodel_name="mis.report.kpi.expression",
|
||||
required=True,
|
||||
ondelete="restrict",
|
||||
string="KPI",
|
||||
)
|
||||
date_from = fields.Date(required=True, string="From")
|
||||
date_to = fields.Date(required=True, string="To")
|
||||
amount = fields.Float()
|
||||
seq1 = fields.Integer(
|
||||
related="kpi_expression_id.kpi_id.sequence",
|
||||
store=True,
|
||||
readonly=True,
|
||||
string="KPI Sequence",
|
||||
)
|
||||
seq2 = fields.Integer(
|
||||
related="kpi_expression_id.subkpi_id.sequence",
|
||||
store=True,
|
||||
readonly=True,
|
||||
string="Sub-KPI Sequence",
|
||||
)
|
||||
|
||||
@api.depends(
|
||||
"kpi_expression_id.subkpi_id.name",
|
||||
"kpi_expression_id.kpi_id.name",
|
||||
"date_from",
|
||||
"date_to",
|
||||
)
|
||||
def _compute_name(self):
|
||||
for rec in self:
|
||||
subkpi_name = rec.kpi_expression_id.subkpi_id.name
|
||||
if subkpi_name:
|
||||
subkpi_name = "." + subkpi_name
|
||||
else:
|
||||
subkpi_name = ""
|
||||
rec.name = "{}{}: {} - {}".format(
|
||||
rec.kpi_expression_id.kpi_id.name,
|
||||
subkpi_name,
|
||||
rec.date_from,
|
||||
rec.date_to,
|
||||
)
|
||||
|
||||
@api.model
|
||||
def _intersect_days(self, item_dt_from, item_dt_to, dt_from, dt_to):
|
||||
return intersect_days(item_dt_from, item_dt_to, dt_from, dt_to)
|
||||
|
||||
@api.model
|
||||
def _query_kpi_data(self, date_from, date_to, base_domain):
|
||||
"""Query mis.kpi.data over a time period.
|
||||
|
||||
Returns {mis.report.kpi.expression: amount}
|
||||
"""
|
||||
dt_from = fields.Date.from_string(date_from)
|
||||
dt_to = fields.Date.from_string(date_to)
|
||||
# all data items within or overlapping [date_from, date_to]
|
||||
date_domain = [("date_from", "<=", date_to), ("date_to", ">=", date_from)]
|
||||
domain = expression.AND([date_domain, base_domain])
|
||||
res = defaultdict(float)
|
||||
res_avg = defaultdict(list)
|
||||
for item in self.search(domain):
|
||||
item_dt_from = fields.Date.from_string(item.date_from)
|
||||
item_dt_to = fields.Date.from_string(item.date_to)
|
||||
i_days, item_days = self._intersect_days(
|
||||
item_dt_from, item_dt_to, dt_from, dt_to
|
||||
)
|
||||
if item.kpi_expression_id.kpi_id.accumulation_method == ACC_SUM:
|
||||
# accumulate pro-rata overlap between item and reporting period
|
||||
res[item.kpi_expression_id] += item.amount * i_days / item_days
|
||||
elif item.kpi_expression_id.kpi_id.accumulation_method == ACC_AVG:
|
||||
# memorize the amount and number of days overlapping
|
||||
# the reporting period (used as weight in average)
|
||||
res_avg[item.kpi_expression_id].append((i_days, item.amount))
|
||||
else:
|
||||
raise UserError(
|
||||
_(
|
||||
"Unexpected accumulation method %(method)s for %(name)s.",
|
||||
method=item.kpi_expression_id.kpi_id.accumulation_method,
|
||||
name=item.name,
|
||||
)
|
||||
)
|
||||
# compute weighted average for ACC_AVG
|
||||
for kpi_expression, amounts in res_avg.items():
|
||||
res[kpi_expression] = sum(d * a for d, a in amounts) / sum(
|
||||
d for d, a in amounts
|
||||
)
|
||||
return res
|
||||
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
|
|
@ -1,113 +0,0 @@
|
|||
# Copyright 2025 ACSONE SA/NV
|
||||
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
|
||||
|
||||
|
||||
from odoo import _, api, fields, models
|
||||
from odoo.exceptions import AccessError
|
||||
|
||||
from .kpimatrix import KpiMatrix
|
||||
|
||||
|
||||
class MisReportInstanceAnnotation(models.Model):
|
||||
_name = "mis.report.instance.annotation"
|
||||
_description = "Mis Report Instance Annotation"
|
||||
|
||||
period_id = fields.Many2one(
|
||||
comodel_name="mis.report.instance.period",
|
||||
ondelete="cascade",
|
||||
required=True,
|
||||
)
|
||||
kpi_id = fields.Many2one(
|
||||
comodel_name="mis.report.kpi",
|
||||
ondelete="cascade",
|
||||
required=True,
|
||||
)
|
||||
subkpi_id = fields.Many2one(
|
||||
comodel_name="mis.report.subkpi",
|
||||
ondelete="cascade",
|
||||
)
|
||||
note = fields.Char()
|
||||
annotation_context = fields.Json(
|
||||
help="""
|
||||
Context used when adding annotation
|
||||
"""
|
||||
)
|
||||
|
||||
def init(self):
|
||||
self.env.cr.execute(
|
||||
"""
|
||||
CREATE INDEX IF NOT EXISTS
|
||||
mis_report_instance_annotation_period_id_kpi_id_subkpi_id_idx
|
||||
ON mis_report_instance_annotation(period_id,kpi_id,subkpi_id);
|
||||
"""
|
||||
)
|
||||
|
||||
@api.model
|
||||
def _get_first_matching_annotation(self, cell_id, instance_id):
|
||||
"""
|
||||
Return first annoation
|
||||
matching exactly the period,kpi,subkpi and annotation context
|
||||
"""
|
||||
|
||||
kpi_id, _, period_id, subkpi_id = KpiMatrix._unpack_cell_id(cell_id)
|
||||
|
||||
annotations = self.env["mis.report.instance.annotation"].search(
|
||||
[
|
||||
("period_id", "=", period_id),
|
||||
("kpi_id", "=", kpi_id),
|
||||
("subkpi_id", "=", subkpi_id),
|
||||
],
|
||||
)
|
||||
annotation_context = (
|
||||
self.env["mis.report.instance"]
|
||||
.browse(instance_id)
|
||||
._get_annotation_context()
|
||||
)
|
||||
annotation = fields.first(
|
||||
annotations.filtered(
|
||||
lambda rec: rec.annotation_context == annotation_context
|
||||
)
|
||||
)
|
||||
return annotation
|
||||
|
||||
@api.model
|
||||
def set_annotation(self, cell_id, instance_id, note):
|
||||
if (
|
||||
not self.env["mis.report.instance"]
|
||||
.browse(instance_id)
|
||||
.user_can_edit_annotation
|
||||
):
|
||||
raise AccessError(_("You do not have the rights to edit annotations"))
|
||||
|
||||
annotation = self._get_first_matching_annotation(cell_id, instance_id)
|
||||
|
||||
if annotation:
|
||||
annotation.note = note
|
||||
else:
|
||||
kpi_id, _account_id, period_id, subkpi_id = KpiMatrix._unpack_cell_id(
|
||||
cell_id
|
||||
)
|
||||
self.env["mis.report.instance.annotation"].create(
|
||||
{
|
||||
"period_id": period_id,
|
||||
"kpi_id": kpi_id,
|
||||
"subkpi_id": subkpi_id,
|
||||
"note": note,
|
||||
"annotation_context": self.env["mis.report.instance"]
|
||||
.browse(instance_id)
|
||||
._get_annotation_context(),
|
||||
}
|
||||
)
|
||||
|
||||
@api.model
|
||||
def remove_annotation(self, cell_id, instance_id):
|
||||
if (
|
||||
not self.env["mis.report.instance"]
|
||||
.browse(instance_id)
|
||||
.user_can_edit_annotation
|
||||
):
|
||||
raise AccessError(_("You do not have the rights to edit annotations"))
|
||||
|
||||
annotation = self._get_first_matching_annotation(cell_id, instance_id)
|
||||
if annotation:
|
||||
annotation.unlink()
|
||||
|
|
@ -1,314 +0,0 @@
|
|||
# Copyright 2016 Therp BV (<http://therp.nl>)
|
||||
# Copyright 2016 ACSONE SA/NV (<http://acsone.eu>)
|
||||
# Copyright 2020 CorporateHub (https://corporatehub.eu)
|
||||
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html).
|
||||
|
||||
import sys
|
||||
|
||||
from odoo import _, api, fields, models
|
||||
from odoo.exceptions import ValidationError
|
||||
|
||||
from .accounting_none import AccountingNone
|
||||
from .data_error import DataError
|
||||
|
||||
if sys.version_info.major >= 3:
|
||||
unicode = str
|
||||
|
||||
|
||||
class PropertyDict(dict):
|
||||
def __getattr__(self, name):
|
||||
return self.get(name)
|
||||
|
||||
def copy(self): # pylint: disable=copy-wo-api-one,method-required-super
|
||||
return PropertyDict(self)
|
||||
|
||||
|
||||
PROPS = [
|
||||
"color",
|
||||
"background_color",
|
||||
"font_style",
|
||||
"font_weight",
|
||||
"font_size",
|
||||
"indent_level",
|
||||
"prefix",
|
||||
"suffix",
|
||||
"dp",
|
||||
"divider",
|
||||
"hide_empty",
|
||||
"hide_always",
|
||||
]
|
||||
|
||||
TYPE_NUM = "num"
|
||||
TYPE_PCT = "pct"
|
||||
TYPE_STR = "str"
|
||||
|
||||
CMP_DIFF = "diff"
|
||||
CMP_PCT = "pct"
|
||||
CMP_NONE = "none"
|
||||
|
||||
|
||||
class MisReportKpiStyle(models.Model):
|
||||
_name = "mis.report.style"
|
||||
_description = "MIS Report Style"
|
||||
|
||||
@api.constrains("indent_level")
|
||||
def check_positive_val(self):
|
||||
for record in self:
|
||||
if record.indent_level < 0:
|
||||
raise ValidationError(
|
||||
_("Indent level must be greater than " "or equal to 0")
|
||||
)
|
||||
|
||||
_font_style_selection = [("normal", "Normal"), ("italic", "Italic")]
|
||||
|
||||
_font_weight_selection = [("nornal", "Normal"), ("bold", "Bold")]
|
||||
|
||||
_font_size_selection = [
|
||||
("medium", "medium"),
|
||||
("xx-small", "xx-small"),
|
||||
("x-small", "x-small"),
|
||||
("small", "small"),
|
||||
("large", "large"),
|
||||
("x-large", "x-large"),
|
||||
("xx-large", "xx-large"),
|
||||
]
|
||||
|
||||
_font_size_to_xlsx_size = {
|
||||
"medium": 11,
|
||||
"xx-small": 5,
|
||||
"x-small": 7,
|
||||
"small": 9,
|
||||
"large": 13,
|
||||
"x-large": 15,
|
||||
"xx-large": 17,
|
||||
}
|
||||
|
||||
# style name
|
||||
name = fields.Char(string="Style name", required=True)
|
||||
|
||||
# color
|
||||
color_inherit = fields.Boolean(default=True)
|
||||
color = fields.Char(
|
||||
string="Text color",
|
||||
help="Text color in valid RGB code (from #000000 to #FFFFFF)",
|
||||
default="#000000",
|
||||
)
|
||||
background_color_inherit = fields.Boolean(default=True)
|
||||
background_color = fields.Char(
|
||||
help="Background color in valid RGB code (from #000000 to #FFFFFF)",
|
||||
default="#FFFFFF",
|
||||
)
|
||||
# font
|
||||
font_style_inherit = fields.Boolean(default=True)
|
||||
font_style = fields.Selection(selection=_font_style_selection)
|
||||
font_weight_inherit = fields.Boolean(default=True)
|
||||
font_weight = fields.Selection(selection=_font_weight_selection)
|
||||
font_size_inherit = fields.Boolean(default=True)
|
||||
font_size = fields.Selection(selection=_font_size_selection)
|
||||
# indent
|
||||
indent_level_inherit = fields.Boolean(default=True)
|
||||
indent_level = fields.Integer()
|
||||
# number format
|
||||
prefix_inherit = fields.Boolean(default=True)
|
||||
prefix = fields.Char()
|
||||
suffix_inherit = fields.Boolean(default=True)
|
||||
suffix = fields.Char()
|
||||
dp_inherit = fields.Boolean(default=True)
|
||||
dp = fields.Integer(string="Rounding", default=0)
|
||||
divider_inherit = fields.Boolean(default=True)
|
||||
divider = fields.Selection(
|
||||
[
|
||||
("1e-6", _("µ")),
|
||||
("1e-3", _("m")),
|
||||
("1", _("1")),
|
||||
("1e3", _("k")),
|
||||
("1e6", _("M")),
|
||||
],
|
||||
string="Factor",
|
||||
default="1",
|
||||
)
|
||||
hide_empty_inherit = fields.Boolean(default=True)
|
||||
hide_empty = fields.Boolean(default=False)
|
||||
hide_always_inherit = fields.Boolean(default=True)
|
||||
hide_always = fields.Boolean(default=False)
|
||||
|
||||
_sql_constraints = [
|
||||
("style_name_uniq", "unique(name)", "Style name should be unique")
|
||||
]
|
||||
|
||||
@api.model
|
||||
def merge(self, styles):
|
||||
"""Merge several styles, giving priority to the last.
|
||||
|
||||
Returns a PropertyDict of style properties.
|
||||
"""
|
||||
r = PropertyDict()
|
||||
for style in styles:
|
||||
if not style:
|
||||
continue
|
||||
if isinstance(style, dict):
|
||||
r.update(style)
|
||||
else:
|
||||
for prop in PROPS:
|
||||
inherit = getattr(style, prop + "_inherit", None)
|
||||
if not inherit:
|
||||
value = getattr(style, prop)
|
||||
r[prop] = value
|
||||
return r
|
||||
|
||||
@api.model
|
||||
def render(self, lang, style_props, var_type, value, sign="-"):
|
||||
if var_type == TYPE_NUM:
|
||||
return self.render_num(
|
||||
lang,
|
||||
value,
|
||||
style_props.divider,
|
||||
style_props.dp,
|
||||
style_props.prefix,
|
||||
style_props.suffix,
|
||||
sign=sign,
|
||||
)
|
||||
elif var_type == TYPE_PCT:
|
||||
return self.render_pct(lang, value, style_props.dp, sign=sign)
|
||||
else:
|
||||
return self.render_str(lang, value)
|
||||
|
||||
@api.model
|
||||
def render_num(
|
||||
self, lang, value, divider=1.0, dp=0, prefix=None, suffix=None, sign="-"
|
||||
):
|
||||
# format number following user language
|
||||
if value is None or value is AccountingNone:
|
||||
return ""
|
||||
value = round(value / float(divider or 1), dp or 0) or 0
|
||||
r = lang.format("%%%s.%df" % (sign, dp or 0), value, grouping=True)
|
||||
r = r.replace("-", "\N{NON-BREAKING HYPHEN}")
|
||||
if prefix:
|
||||
r = prefix + "\N{NO-BREAK SPACE}" + r
|
||||
if suffix:
|
||||
r = r + "\N{NO-BREAK SPACE}" + suffix
|
||||
return r
|
||||
|
||||
@api.model
|
||||
def render_pct(self, lang, value, dp=1, sign="-"):
|
||||
return self.render_num(lang, value, divider=0.01, dp=dp, suffix="%", sign=sign)
|
||||
|
||||
@api.model
|
||||
def render_str(self, lang, value):
|
||||
if value is None or value is AccountingNone:
|
||||
return ""
|
||||
return unicode(value)
|
||||
|
||||
@api.model
|
||||
def compare_and_render(
|
||||
self,
|
||||
lang,
|
||||
style_props,
|
||||
var_type,
|
||||
compare_method,
|
||||
value,
|
||||
base_value,
|
||||
average_value=1,
|
||||
average_base_value=1,
|
||||
):
|
||||
"""
|
||||
:param lang: res.lang record
|
||||
:param style_props: PropertyDict with style properties
|
||||
:param var_type: num, pct or str
|
||||
:param compare_method: diff, pct, none
|
||||
:param value: value to compare (value - base_value)
|
||||
:param base_value: value compared with (value - base_value)
|
||||
:param average_value: value = value / average_value
|
||||
:param average_base_value: base_value = base_value / average_base_value
|
||||
:return: tuple with 4 elements
|
||||
- delta = comparison result (Float or AccountingNone)
|
||||
- delta_r = delta rendered in formatted string (String)
|
||||
- delta_style = PropertyDict with style properties
|
||||
- delta_type = Type of the comparison result (num or pct)
|
||||
"""
|
||||
delta = AccountingNone
|
||||
delta_r = ""
|
||||
delta_style = style_props.copy()
|
||||
delta_type = TYPE_NUM
|
||||
if isinstance(value, DataError) or isinstance(base_value, DataError):
|
||||
return AccountingNone, "", delta_style, delta_type
|
||||
if value is None:
|
||||
value = AccountingNone
|
||||
if base_value is None:
|
||||
base_value = AccountingNone
|
||||
if var_type == TYPE_PCT:
|
||||
delta = value - base_value
|
||||
if delta and round(delta, (style_props.dp or 0) + 2) != 0:
|
||||
delta_style.update(divider=0.01, prefix="", suffix=_("pp"))
|
||||
else:
|
||||
delta = AccountingNone
|
||||
elif var_type == TYPE_NUM:
|
||||
if value and average_value:
|
||||
# pylint: disable=redefined-variable-type
|
||||
value = value / float(average_value)
|
||||
if base_value and average_base_value:
|
||||
# pylint: disable=redefined-variable-type
|
||||
base_value = base_value / float(average_base_value)
|
||||
if compare_method == CMP_DIFF:
|
||||
delta = value - base_value
|
||||
if delta and round(delta, style_props.dp or 0) != 0:
|
||||
pass
|
||||
else:
|
||||
delta = AccountingNone
|
||||
elif compare_method == CMP_PCT:
|
||||
if base_value and round(base_value, style_props.dp or 0) != 0:
|
||||
delta = (value - base_value) / abs(base_value)
|
||||
if delta and round(delta, 3) != 0:
|
||||
delta_style.update(dp=1)
|
||||
delta_type = TYPE_PCT
|
||||
else:
|
||||
delta = AccountingNone
|
||||
if delta is not AccountingNone:
|
||||
delta_r = self.render(lang, delta_style, delta_type, delta, sign="+")
|
||||
return delta, delta_r, delta_style, delta_type
|
||||
|
||||
@api.model
|
||||
def to_xlsx_style(self, var_type, props, no_indent=False):
|
||||
xlsx_attributes = [
|
||||
("italic", props.font_style == "italic"),
|
||||
("bold", props.font_weight == "bold"),
|
||||
("font_size", self._font_size_to_xlsx_size.get(props.font_size, 11)),
|
||||
("font_color", props.color),
|
||||
("bg_color", props.background_color),
|
||||
]
|
||||
if var_type == TYPE_NUM:
|
||||
num_format = "#,##0"
|
||||
if props.dp:
|
||||
num_format += "."
|
||||
num_format += "0" * props.dp
|
||||
if props.prefix:
|
||||
num_format = f'"{props.prefix} "{num_format}'
|
||||
if props.suffix:
|
||||
num_format = f'{num_format}" {props.suffix}"'
|
||||
xlsx_attributes.append(("num_format", num_format))
|
||||
elif var_type == TYPE_PCT:
|
||||
num_format = "0"
|
||||
if props.dp:
|
||||
num_format += "."
|
||||
num_format += "0" * props.dp
|
||||
num_format += "%"
|
||||
xlsx_attributes.append(("num_format", num_format))
|
||||
if props.indent_level is not None and not no_indent:
|
||||
xlsx_attributes.append(("indent", props.indent_level))
|
||||
return dict([a for a in xlsx_attributes if a[1] is not None])
|
||||
|
||||
@api.model
|
||||
def to_css_style(self, props, no_indent=False):
|
||||
css_attributes = [
|
||||
("font-style", props.font_style),
|
||||
("font-weight", props.font_weight),
|
||||
("font-size", props.font_size),
|
||||
("color", props.color),
|
||||
("background-color", props.background_color),
|
||||
]
|
||||
if props.indent_level is not None and not no_indent:
|
||||
css_attributes.append(("text-indent", f"{props.indent_level}em"))
|
||||
return (
|
||||
"; ".join(["{}: {}".format(*a) for a in css_attributes if a[1] is not None])
|
||||
or None
|
||||
)
|
||||
|
|
@ -1,74 +0,0 @@
|
|||
# Copyright 2020 ACSONE SA/NV (<http://acsone.eu>)
|
||||
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html).
|
||||
|
||||
from odoo import _, api, fields, models
|
||||
from odoo.exceptions import ValidationError
|
||||
|
||||
from .mis_report import _is_valid_python_var
|
||||
|
||||
|
||||
class ParentLoopError(ValidationError):
|
||||
pass
|
||||
|
||||
|
||||
class InvalidNameError(ValidationError):
|
||||
pass
|
||||
|
||||
|
||||
class MisReportSubReport(models.Model):
|
||||
_name = "mis.report.subreport"
|
||||
_description = "MIS Report - Sub Reports Relation"
|
||||
|
||||
name = fields.Char(required=True)
|
||||
report_id = fields.Many2one(
|
||||
comodel_name="mis.report",
|
||||
required=True,
|
||||
ondelete="cascade",
|
||||
)
|
||||
subreport_id = fields.Many2one(
|
||||
comodel_name="mis.report",
|
||||
required=True,
|
||||
ondelete="restrict",
|
||||
)
|
||||
|
||||
_sql_constraints = [
|
||||
(
|
||||
"name_unique",
|
||||
"unique(name, report_id)",
|
||||
"Subreport name should be unique by report",
|
||||
),
|
||||
(
|
||||
"subreport_unique",
|
||||
"unique(subreport_id, report_id)",
|
||||
"Should not include the same report more than once as sub report "
|
||||
"of a given report",
|
||||
),
|
||||
]
|
||||
|
||||
@api.constrains("name")
|
||||
def _check_name(self):
|
||||
for rec in self:
|
||||
if not _is_valid_python_var(rec.name):
|
||||
raise InvalidNameError(
|
||||
_("Subreport name ({}) must be a valid python identifier").format(
|
||||
rec.name
|
||||
)
|
||||
)
|
||||
|
||||
@api.constrains("report_id", "subreport_id")
|
||||
def _check_loop(self):
|
||||
def _has_subreport(reports, report):
|
||||
if not reports:
|
||||
return False
|
||||
if report in reports:
|
||||
return True
|
||||
return any(
|
||||
_has_subreport(r.subreport_ids.mapped("subreport_id"), report)
|
||||
for r in reports
|
||||
)
|
||||
|
||||
for rec in self:
|
||||
if _has_subreport(rec.subreport_id, rec.report_id):
|
||||
raise ParentLoopError(_("Subreport loop detected"))
|
||||
|
||||
# TODO check subkpi compatibility in subreports
|
||||
|
|
@ -1,33 +0,0 @@
|
|||
# Copyright 2016 ACSONE SA/NV (<http://acsone.eu>)
|
||||
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html).
|
||||
|
||||
import traceback
|
||||
|
||||
from odoo.tools.safe_eval import _BUILTINS, _SAFE_OPCODES, test_expr
|
||||
|
||||
from .data_error import DataError, NameDataError
|
||||
|
||||
__all__ = ["mis_safe_eval"]
|
||||
|
||||
|
||||
def mis_safe_eval(expr, locals_dict):
|
||||
"""Evaluate an expression using safe_eval
|
||||
|
||||
Returns the evaluated value or DataError.
|
||||
|
||||
Raises NameError if the evaluation depends on a variable that is not
|
||||
present in local_dict.
|
||||
"""
|
||||
try:
|
||||
c = test_expr(expr, _SAFE_OPCODES, mode="eval")
|
||||
globals_dict = {"__builtins__": _BUILTINS}
|
||||
# pylint: disable=eval-used,eval-referenced
|
||||
val = eval(c, globals_dict, locals_dict)
|
||||
except NameError:
|
||||
val = NameDataError("#NAME", traceback.format_exc())
|
||||
except ZeroDivisionError:
|
||||
# pylint: disable=redefined-variable-type
|
||||
val = DataError("#DIV/0", traceback.format_exc())
|
||||
except Exception:
|
||||
val = DataError("#ERR", traceback.format_exc())
|
||||
return val
|
||||
|
|
@ -1,96 +0,0 @@
|
|||
# Copyright 2020 ACSONE SA/NV
|
||||
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
|
||||
|
||||
from odoo import _, api, fields, models
|
||||
from odoo.exceptions import UserError
|
||||
from odoo.fields import Date
|
||||
|
||||
from .mis_kpi_data import intersect_days
|
||||
|
||||
|
||||
class ProRataReadGroupMixin(models.AbstractModel):
|
||||
_name = "prorata.read_group.mixin"
|
||||
_description = "Adapt model with date_from/date_to for pro-rata temporis read_group"
|
||||
|
||||
date_from = fields.Date(required=True)
|
||||
date_to = fields.Date(required=True)
|
||||
date = fields.Date(
|
||||
compute=lambda self: None,
|
||||
search="_search_date",
|
||||
help=(
|
||||
"Dummy field that adapts searches on date "
|
||||
"to searches on date_from/date_to."
|
||||
),
|
||||
)
|
||||
|
||||
def _search_date(self, operator, value):
|
||||
if operator in (">=", ">"):
|
||||
return [("date_to", operator, value)]
|
||||
elif operator in ("<=", "<"):
|
||||
return [("date_from", operator, value)]
|
||||
raise UserError(
|
||||
_("Unsupported operator %s for searching on date") % (operator,)
|
||||
)
|
||||
|
||||
@api.model
|
||||
def _intersect_days(self, item_dt_from, item_dt_to, dt_from, dt_to):
|
||||
return intersect_days(item_dt_from, item_dt_to, dt_from, dt_to)
|
||||
|
||||
@api.model
|
||||
def read_group(
|
||||
self, domain, fields, groupby, offset=0, limit=None, orderby=False, lazy=True
|
||||
):
|
||||
"""Override read_group to perform pro-rata temporis adjustments.
|
||||
|
||||
When read_group is invoked with a domain that filters on
|
||||
a time period (date >= from and date <= to, or
|
||||
date_from <= to and date_to >= from), adjust the accumulated
|
||||
values pro-rata temporis.
|
||||
"""
|
||||
date_from = None
|
||||
date_to = None
|
||||
assert isinstance(domain, list)
|
||||
for domain_item in domain:
|
||||
if isinstance(domain_item, list | tuple):
|
||||
field, op, value = domain_item
|
||||
if field == "date" and op == ">=":
|
||||
date_from = value
|
||||
elif field == "date_to" and op == ">=":
|
||||
date_from = value
|
||||
elif field == "date" and op == "<=":
|
||||
date_to = value
|
||||
elif field == "date_from" and op == "<=":
|
||||
date_to = value
|
||||
if (
|
||||
date_from is not None
|
||||
and date_to is not None
|
||||
and not any(":" in f for f in fields)
|
||||
):
|
||||
dt_from = Date.from_string(date_from)
|
||||
dt_to = Date.from_string(date_to)
|
||||
res = {}
|
||||
sum_fields = set(fields) - set(groupby)
|
||||
read_fields = set(fields + ["date_from", "date_to"])
|
||||
for item in self.search(domain).read(read_fields):
|
||||
key = tuple(item[k] for k in groupby)
|
||||
if key not in res:
|
||||
res[key] = {k: item[k] for k in groupby}
|
||||
res[key].update({k: 0.0 for k in sum_fields})
|
||||
res_item = res[key]
|
||||
for sum_field in sum_fields:
|
||||
item_dt_from = Date.from_string(item["date_from"])
|
||||
item_dt_to = Date.from_string(item["date_to"])
|
||||
i_days, item_days = self._intersect_days(
|
||||
item_dt_from, item_dt_to, dt_from, dt_to
|
||||
)
|
||||
res_item[sum_field] += item[sum_field] * i_days / item_days
|
||||
return res.values()
|
||||
return super().read_group(
|
||||
domain,
|
||||
fields,
|
||||
groupby,
|
||||
offset=offset,
|
||||
limit=limit,
|
||||
orderby=orderby,
|
||||
lazy=lazy,
|
||||
)
|
||||
|
|
@ -1,184 +0,0 @@
|
|||
# Copyright 2014 ACSONE SA/NV (<http://acsone.eu>)
|
||||
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html).
|
||||
""" A trivial immutable array that supports basic arithmetic operations.
|
||||
|
||||
>>> a = SimpleArray((1.0, 2.0, 3.0))
|
||||
>>> b = SimpleArray((4.0, 5.0, 6.0))
|
||||
>>> t = (4.0, 5.0, 6.0)
|
||||
>>> +a
|
||||
SimpleArray((1.0, 2.0, 3.0))
|
||||
>>> -a
|
||||
SimpleArray((-1.0, -2.0, -3.0))
|
||||
>>> a + b
|
||||
SimpleArray((5.0, 7.0, 9.0))
|
||||
>>> b + a
|
||||
SimpleArray((5.0, 7.0, 9.0))
|
||||
>>> a + t
|
||||
SimpleArray((5.0, 7.0, 9.0))
|
||||
>>> t + a
|
||||
SimpleArray((5.0, 7.0, 9.0))
|
||||
>>> a - b
|
||||
SimpleArray((-3.0, -3.0, -3.0))
|
||||
>>> a - t
|
||||
SimpleArray((-3.0, -3.0, -3.0))
|
||||
>>> t - a
|
||||
SimpleArray((3.0, 3.0, 3.0))
|
||||
>>> a * b
|
||||
SimpleArray((4.0, 10.0, 18.0))
|
||||
>>> b * a
|
||||
SimpleArray((4.0, 10.0, 18.0))
|
||||
>>> a * t
|
||||
SimpleArray((4.0, 10.0, 18.0))
|
||||
>>> t * a
|
||||
SimpleArray((4.0, 10.0, 18.0))
|
||||
>>> a / b
|
||||
SimpleArray((0.25, 0.4, 0.5))
|
||||
>>> b / a
|
||||
SimpleArray((4.0, 2.5, 2.0))
|
||||
>>> a / t
|
||||
SimpleArray((0.25, 0.4, 0.5))
|
||||
>>> t / a
|
||||
SimpleArray((4.0, 2.5, 2.0))
|
||||
>>> b / 2
|
||||
SimpleArray((2.0, 2.5, 3.0))
|
||||
>>> 2 * b
|
||||
SimpleArray((8.0, 10.0, 12.0))
|
||||
>>> 1 - b
|
||||
SimpleArray((-3.0, -4.0, -5.0))
|
||||
>>> b += 2 ; b
|
||||
SimpleArray((6.0, 7.0, 8.0))
|
||||
>>> a / ((1.0, 0.0, 1.0))
|
||||
SimpleArray((1.0, DataError('#DIV/0'), 3.0))
|
||||
>>> a / 0.0
|
||||
SimpleArray((DataError('#DIV/0'), DataError('#DIV/0'), DataError('#DIV/0')))
|
||||
>>> a * ((1.0, 'a', 1.0))
|
||||
SimpleArray((1.0, DataError('#ERR'), 3.0))
|
||||
>>> 6.0 / a
|
||||
SimpleArray((6.0, 3.0, 2.0))
|
||||
>>> Vector = named_simple_array('Vector', ('x', 'y'))
|
||||
>>> p1 = Vector((1, 2))
|
||||
>>> print(p1.x, p1.y, p1)
|
||||
1 2 Vector((1, 2))
|
||||
>>> p2 = Vector((2, 3))
|
||||
>>> print(p2.x, p2.y, p2)
|
||||
2 3 Vector((2, 3))
|
||||
>>> p3 = p1 + p2
|
||||
>>> print(p3.x, p3.y, p3)
|
||||
3 5 Vector((3, 5))
|
||||
>>> p4 = (4, 5) + p2
|
||||
>>> print(p4.x, p4.y, p4)
|
||||
6 8 Vector((6, 8))
|
||||
>>> p1 * 2
|
||||
Vector((2, 4))
|
||||
>>> 2 * p1
|
||||
Vector((2, 4))
|
||||
>>> p1 - 1
|
||||
Vector((0, 1))
|
||||
>>> 1 - p1
|
||||
Vector((0, -1))
|
||||
>>> p1 / 2.0
|
||||
Vector((0.5, 1.0))
|
||||
>>> v = 2.0 / p1
|
||||
>>> print(v.x, v.y, v)
|
||||
2.0 1.0 Vector((2.0, 1.0))
|
||||
"""
|
||||
|
||||
import itertools
|
||||
import operator
|
||||
import traceback
|
||||
|
||||
from .data_error import DataError
|
||||
|
||||
__all__ = ["SimpleArray", "named_simple_array"]
|
||||
|
||||
|
||||
class SimpleArray(tuple):
|
||||
def _op(self, op, other):
|
||||
def _o2(x, y):
|
||||
try:
|
||||
return op(x, y)
|
||||
except ZeroDivisionError:
|
||||
return DataError("#DIV/0", traceback.format_exc())
|
||||
except Exception:
|
||||
return DataError("#ERR", traceback.format_exc())
|
||||
|
||||
if isinstance(other, tuple):
|
||||
if len(other) != len(self):
|
||||
raise TypeError("tuples must have same length for %s" % op)
|
||||
return self.__class__(map(_o2, self, other))
|
||||
else:
|
||||
return self.__class__(_o2(z, other) for z in self)
|
||||
|
||||
def _cast(self, other):
|
||||
if isinstance(other, self.__class__):
|
||||
return other
|
||||
elif isinstance(other, tuple):
|
||||
return self.__class__(other)
|
||||
else:
|
||||
# other is a scalar
|
||||
return self.__class__(itertools.repeat(other, len(self)))
|
||||
|
||||
def __add__(self, other):
|
||||
return self._op(operator.add, other)
|
||||
|
||||
__radd__ = __add__
|
||||
|
||||
def __pos__(self):
|
||||
return self.__class__(map(operator.pos, self))
|
||||
|
||||
def __neg__(self):
|
||||
return self.__class__(map(operator.neg, self))
|
||||
|
||||
def __sub__(self, other):
|
||||
return self._op(operator.sub, other)
|
||||
|
||||
def __rsub__(self, other):
|
||||
return self._cast(other)._op(operator.sub, self)
|
||||
|
||||
def __mul__(self, other):
|
||||
return self._op(operator.mul, other)
|
||||
|
||||
__rmul__ = __mul__
|
||||
|
||||
def __div__(self, other):
|
||||
return self._op(operator.div, other)
|
||||
|
||||
def __floordiv__(self, other):
|
||||
return self._op(operator.floordiv, other)
|
||||
|
||||
def __truediv__(self, other):
|
||||
return self._op(operator.truediv, other)
|
||||
|
||||
def __rdiv__(self, other):
|
||||
return self._cast(other)._op(operator.div, self)
|
||||
|
||||
def __rfloordiv__(self, other):
|
||||
return self._cast(other)._op(operator.floordiv, self)
|
||||
|
||||
def __rtruediv__(self, other):
|
||||
return self._cast(other)._op(operator.truediv, self)
|
||||
|
||||
def __repr__(self):
|
||||
return f"{self.__class__.__name__}({tuple.__repr__(self)})"
|
||||
|
||||
|
||||
def named_simple_array(typename, field_names):
|
||||
"""Return a subclass of SimpleArray, with named properties.
|
||||
|
||||
This method is to SimpleArray what namedtuple is to tuple.
|
||||
It's less sophisticated than namedtuple so some namedtuple
|
||||
advanced use cases may not work, but it's good enough for
|
||||
our needs in mis_builder, ie referring to subkpi values
|
||||
by name.
|
||||
"""
|
||||
props = {
|
||||
field_name: property(operator.itemgetter(i))
|
||||
for i, field_name in enumerate(field_names)
|
||||
}
|
||||
return type(typename, (SimpleArray,), props)
|
||||
|
||||
|
||||
if __name__ == "__main__": # pragma: no cover
|
||||
import doctest
|
||||
|
||||
doctest.testmod()
|
||||
|
|
@ -1,3 +0,0 @@
|
|||
[build-system]
|
||||
requires = ["whool"]
|
||||
build-backend = "whool.buildapi"
|
||||
|
|
@ -1,27 +0,0 @@
|
|||
* Stéphane Bidoul <stephane.bidoul@acsone.eu>
|
||||
* Laetitia Gangloff <laetitia.gangloff@acsone.eu>
|
||||
* Adrien Peiffer <adrien.peiffer@acsone.eu>
|
||||
* Alexis de Lattre <alexis.delattre@akretion.com>
|
||||
* Alexandre Fayolle <alexandre.fayolle@camptocamp.com>
|
||||
* Jordi Ballester <jordi.ballester@eficent.com>
|
||||
* Thomas Binsfeld <thomas.binsfeld@gmail.com>
|
||||
* Giovanni Capalbo <giovanni@therp.nl>
|
||||
* Marco Calcagni <mcalcagni@dinamicheaziendali.it>
|
||||
* Sébastien Beau <sebastien.beau@akretion.com>
|
||||
* Laurent Mignon <laurent.mignon@acsone.eu>
|
||||
* Luc De Meyer <luc.demeyer@noviat.com>
|
||||
* Benjamin Willig <benjamin.willig@acsone.eu>
|
||||
* Martronic SA <info@martronic.ch>
|
||||
* nicomacr <nmr@adhoc.com.ar>
|
||||
* Juan Jose Scarafia <jjs@adhoc.com.ar>
|
||||
* Richard deMeester <richard@willowit.com.au>
|
||||
* Eric Caudal <eric.caudal@elico-corp.com>
|
||||
* Andrea Stirpe <a.stirpe@onestein.nl>
|
||||
* Maxence Groine <mgroine@fiefmanage.ch>
|
||||
* Arnaud Pineux <arnaud.pineux@acsone.eu>
|
||||
* Ernesto Tejeda <ernesto.tejeda@tecnativa.com>
|
||||
* Pedro M. Baeza <pedro.baeza@tecnativa.com>
|
||||
* Alexey Pelykh <alexey.pelykh@corphub.eu>
|
||||
* Jairo Llopis (https://www.moduon.team/)
|
||||
* Dzung Tran <dungtd@trobz.com>
|
||||
* Hoang Diep <hoang@trobz.com>
|
||||
|
|
@ -1,5 +0,0 @@
|
|||
This module allows you to build Management Information Systems dashboards.
|
||||
Such style of reports presents KPI in rows and time periods in columns.
|
||||
Reports mainly fetch data from account moves, but can also combine data coming
|
||||
from arbitrary Odoo models. Reports can be exported to PDF, Excel and they
|
||||
can be added to Odoo dashboards.
|
||||
|
|
@ -1,6 +0,0 @@
|
|||
A typical extension is to provide a mechanism to filter reports on analytic dimensions
|
||||
or operational units. To implement this, you can override _get_additional_move_line_filter
|
||||
and _get_additional_filter to further filter move lines or queries based on a user
|
||||
selection. A typical use case could be to add an analytic account field on mis.report.instance,
|
||||
or even on mis.report.instance.period if you want different columns to show different
|
||||
analytic accounts.
|
||||
|
|
@ -1,539 +0,0 @@
|
|||
16.0.5.1.9 (2024-02-09)
|
||||
~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
**Bugfixes**
|
||||
|
||||
- Restore compatibility with python 3.9 (`#590 <https://github.com/OCA/mis-builder/issues/590>`_)
|
||||
|
||||
|
||||
16.0.5.1.8 (2024-02-08)
|
||||
~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
**Bugfixes**
|
||||
|
||||
- Resolve a permission issue when creating report periods with a user without admin rights. (`#596 <https://github.com/OCA/mis-builder/issues/596>`_)
|
||||
|
||||
|
||||
16.0.5.1.0 (2023-04-04)
|
||||
~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
**Features**
|
||||
|
||||
- Improve UX by adding the option to edit the pivot date directly on the view.
|
||||
|
||||
16.0.5.0.0 (2023-04-01)
|
||||
~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
**Features**
|
||||
|
||||
- Migration to 16.0
|
||||
|
||||
- Addition of a generic filter domain on reports and columns.
|
||||
- Addition of a search bar to the widget. The corresponding search view is configurable
|
||||
per report.
|
||||
- Huge improvement of the widget style. This was long overdue.
|
||||
- Make the MIS Report menu accessible to the Billing Administrator group
|
||||
(instead of the hidden Show Full Accounting Features), to align with the access rules
|
||||
and avoid giving a false sense of security. This also makes the menu discoverable to
|
||||
new users.
|
||||
- Removal of analytic fetures because the upstream ``analytic_distribution`` mechanism
|
||||
is not compatible; support may be introduced in separate module, depending on use
|
||||
cases.
|
||||
- Abandon the ``mis_report_filters`` context key which had security implication.
|
||||
It is replaced by a ``mis_analytic_domain`` context key which is ANDed with other
|
||||
report-defined filters. (`#472 <https://github.com/OCA/mis-builder/issues/472>`_)
|
||||
- Rename the ``get_filter_descriptions_from_context`` method to
|
||||
``get_filter_descriptions``. This method may be overridden to provide additional
|
||||
subtitles on the PDF or XLS report, representing user-selected filters.
|
||||
- The ``hide_analytic_filters`` has been replaced by ``widget_show_filters``.
|
||||
- The visibility of the settings button on the widget is now controlled by a
|
||||
``show_settings_button``. Before it was visible only for the ``account_user`` group
|
||||
but this was not flexible enough.
|
||||
- The widget configuration settings are now grouped in a dedicated ``Widget`` tab in
|
||||
the report configuration form.
|
||||
|
||||
**Bugfixes**
|
||||
|
||||
- Fix access error when previewing or printing report. (`#415 <https://github.com/OCA/mis-builder/issues/415>`_)
|
||||
|
||||
|
||||
15.0.4.0.5 (2022-07-19)
|
||||
~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
**Bugfixes**
|
||||
|
||||
- Support users without timezone. (`#388 <https://github.com/OCA/mis-builder/issues/388>`_)
|
||||
|
||||
|
||||
15.0.4.0.4 (2022-07-19)
|
||||
~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
**Bugfixes**
|
||||
|
||||
- Allow deleting a report that has subreports. (`#431 <https://github.com/OCA/mis-builder/issues/431>`_)
|
||||
|
||||
|
||||
15.0.4.0.2 (2022-02-16)
|
||||
~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
**Bugfixes**
|
||||
|
||||
- Fix access right issue when clicking the "Save" button on a MIS Report Instance form. (`#410 <https://github.com/OCA/mis-builder/issues/410>`_)
|
||||
|
||||
|
||||
14.0.4.0.0 (2022-01-08)
|
||||
~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
**Features**
|
||||
|
||||
- Remove various field size limits. (`#332 <https://github.com/OCA/mis-builder/issues/332>`_)
|
||||
|
||||
|
||||
**Bugfixes**
|
||||
|
||||
- Support for the Odoo 13+ multi-company model. In multi-company mode, several allowed
|
||||
companies can be declared on MIS Report instances, and the report operates on the
|
||||
intersection of report companies and companies selected in the user context. (`#327 <https://github.com/OCA/mis-builder/issues/327>`_)
|
||||
- The ``get_additional_query_filter`` argument of ``evaluate()`` is now propagated
|
||||
correctly. (`#375 <https://github.com/OCA/mis-builder/issues/375>`_)
|
||||
- Use the ``parent_state`` field of ``account.move.line`` to filter entries in ``posted``
|
||||
and ``draft`` state only. Before, when reporting in draft mode, all entries were used
|
||||
(i.e. there was no filter), and that started including the cancelled entries/invoices in
|
||||
Odoo 13.+.
|
||||
|
||||
This change also contains a **breaking change** in the internal API. For quite a while
|
||||
the ``target_move argument`` of AEP and other methods was not used by MIS Builder itself
|
||||
and was kept for backward compatibility. To avoid rippling effects of the necessary
|
||||
change to use ``parent_state``, we now remove this argument. (`#377 <https://github.com/OCA/mis-builder/issues/377>`_)
|
||||
|
||||
|
||||
14.0.3.6.7 (2021-06-02)
|
||||
~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
**Bugfixes**
|
||||
|
||||
- When on a MIS Report Instance, if you wanted to generate a new line of type comparison, you couldn't currently select any existing period to compare.
|
||||
This happened because the field domain was searching in a NewId context, thus not finding a correct period.
|
||||
Changing the domain and making it use a computed field with a search for the _origin record solves the problem. (`#361 <https://github.com/OCA/mis-builder/issues/361>`_)
|
||||
|
||||
|
||||
14.0.3.6.6 (2021-04-23)
|
||||
~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
**Bugfixes**
|
||||
|
||||
- Fix drilldown action name when the account model has been customized. (`#350 <https://github.com/OCA/mis-builder/issues/350>`_)
|
||||
|
||||
|
||||
14.0.3.6.5 (2021-04-23)
|
||||
~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
**Bugfixes**
|
||||
|
||||
- While duplicating a MIS report instance, comparison columns are ignored because
|
||||
they would raise an error otherwise, as they keep the old source_cmpcol_from_id
|
||||
and source_cmpcol_to_id from the original record. (`#343 <https://github.com/OCA/mis-builder/issues/343>`_)
|
||||
|
||||
|
||||
14.0.3.6.4 (2021-04-06)
|
||||
~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
**Features**
|
||||
|
||||
- The drilldown action name displayed on the breadcrumb has been revised.
|
||||
The kpi description and the account ``display_name`` are shown instead
|
||||
of the kpi's technical definition. (`#304 <https://github.com/OCA/mis-builder/issues/304>`_)
|
||||
- Add analytic group filters on report instance, periods and in the interactive
|
||||
view. (`#320 <https://github.com/OCA/mis-builder/issues/320>`_)
|
||||
|
||||
|
||||
13.0.3.6.3 (2020-08-28)
|
||||
~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
**Bugfixes**
|
||||
|
||||
- Having a "Compare columns" added on a KPI with an associated style using a
|
||||
Factor/Divider did lead to the said factor being applied on the percentages
|
||||
when exporting to XLSX. (`#300 <https://github.com/OCA/mis-builder/issues/300>`_)
|
||||
|
||||
|
||||
**Misc**
|
||||
|
||||
- `#280 <https://github.com/OCA/mis-builder/issues/280>`_, `#296 <https://github.com/OCA/mis-builder/issues/296>`_
|
||||
|
||||
|
||||
13.0.3.6.2 (2020-04-22)
|
||||
~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
**Bugfixes**
|
||||
|
||||
- The "Settings" button is now displayed for users with the "Show full accounting features" right when previewing a report. (`#281 <https://github.com/OCA/mis-builder/issues/281>`_)
|
||||
|
||||
|
||||
13.0.3.6.1 (2020-04-22)
|
||||
~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
**Bugfixes**
|
||||
|
||||
- Fix ``TypeError: 'module' object is not iterable`` when using
|
||||
budgets by account. (`#276 <https://github.com/OCA/mis-builder/issues/276>`_)
|
||||
|
||||
|
||||
13.0.3.6.0 (2020-03-28)
|
||||
~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
**Features**
|
||||
|
||||
- Add column-level filters on analytic account and analytic tags.
|
||||
These filters are combined with a AND with the report-level filters
|
||||
and cannot be modified in the preview. (`#138 <https://github.com/OCA/mis-builder/issues/138>`_)
|
||||
- Access to KPI from other reports in KPI expressions, aka subreports. In a
|
||||
report template, one can list named "subreports" (other report templates). When
|
||||
evaluating expressions, you can access KPI's of subreports with a dot-prefix
|
||||
notation. Example: you can define a MIS Report for a "Balance Sheet", and then
|
||||
have another MIS Report "Balance Sheet Ratios" that fetches KPI's from "Balance
|
||||
Sheet" to create new KPI's for the ratios (e.g. balance_sheet.current_assets /
|
||||
balance_sheet.total_assets). (`#155 <https://github.com/OCA/mis-builder/issues/155>`_)
|
||||
|
||||
|
||||
13.0.3.5.0 (2020-01-??)
|
||||
~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Migration to odoo 13.0.
|
||||
|
||||
12.0.3.5.0 (2019-10-26)
|
||||
~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
**Features**
|
||||
|
||||
- The ``account_id`` field of the model selected in 'Move lines source'
|
||||
in the Period form can now be a Many2one
|
||||
relationship with any model that has a ``code`` field (not only with
|
||||
``account.account`` model). To this end, the model to be used for Actuals
|
||||
move lines can be configured on the report template. It can be something else
|
||||
than move lines and the only constraint is that its ``account_id`` field
|
||||
has a ``code`` field. (`#149 <https://github.com/oca/mis-builder/issues/149>`_)
|
||||
- Add ``source_aml_model_name`` field so extension modules providing
|
||||
alternative data sources can more easily customize their data source. (`#214 <https://github.com/oca/mis-builder/issues/214>`_)
|
||||
- Support analytic tag filters in the backend view and preview widget.
|
||||
Selecting several tags in the filter means filtering on move lines which
|
||||
have *all* these tags set. This is to support the most common use case of
|
||||
using tags for different dimensions. The filter also makes a AND with the
|
||||
analytic account filter. (`#228 <https://github.com/oca/mis-builder/issues/228>`_)
|
||||
- Display company in account details rows in multi-company mode. (`#242 <https://github.com/oca/mis-builder/issues/242>`_)
|
||||
|
||||
|
||||
**Bugfixes**
|
||||
|
||||
- Propagate context to xlsx report, so the analytic account filter
|
||||
works when exporting to xslx too. This also requires a fix to
|
||||
``report_xlsx`` (see https://github.com/OCA/reporting-engine/pull/259). (`#178 <https://github.com/oca/mis-builder/issues/178>`_)
|
||||
- In columns of type Sum, preserve styles for KPIs that are not summable
|
||||
(eg percentage values). Before this fix, such cells were displayed without
|
||||
style. (`#219 <https://github.com/oca/mis-builder/issues/219>`_)
|
||||
- In Excel export, keep the percentage point suffix (pp) instead of replacing it with %. (`#220 <https://github.com/oca/mis-builder/issues/220>`_)
|
||||
|
||||
|
||||
12.0.3.4.0 (2019-07-09)
|
||||
~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
**Features**
|
||||
|
||||
- New year-to-date mode for defining periods. (`#165 <https://github.com/oca/mis-builder/issues/165>`_)
|
||||
- Add support for move lines with negative debit or credit.
|
||||
Used by some for storno accounting. Not officially supported. (`#175 <https://github.com/oca/mis-builder/issues/175>`_)
|
||||
- In Excel export, use a number format with thousands separator. The
|
||||
specific separator used depends on the Excel configuration (eg regional
|
||||
settings). (`#190 <https://github.com/oca/mis-builder/issues/190>`_)
|
||||
- Add generation date/time at the end of the XLS export. (`#191 <https://github.com/oca/mis-builder/issues/191>`_)
|
||||
- In presence of Sub KPIs, report more informative user errors when
|
||||
non-multi expressions yield tuples of incorrect lenght. (`#196 <https://github.com/oca/mis-builder/issues/196>`_)
|
||||
|
||||
|
||||
**Bugfixes**
|
||||
|
||||
- Fix rendering of percentage types in Excel export. (`#192 <https://github.com/oca/mis-builder/issues/192>`_)
|
||||
|
||||
|
||||
12.0.3.3.0 (2019-01-26)
|
||||
~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
**Features**
|
||||
|
||||
*Dynamic analytic filters in report preview are not yet available in 11,
|
||||
this requires an update to the JS widget that proved difficult to implement
|
||||
so far. Help welcome.*
|
||||
|
||||
- Analytic account filters. On a report, an analytic
|
||||
account can be selected for filtering. The filter will
|
||||
be applied to move lines queries. A filter box is also
|
||||
available in the widget to let the user select the analytic
|
||||
account during report preview. (`#15 <https://github.com/oca/mis-builder/issues/15>`_)
|
||||
- Control visibility of analytic filter combo box in widget.
|
||||
This is useful to hide the analytic filters on reports where
|
||||
they do not make sense, such as balance sheet reports. (`#42 <https://github.com/oca/mis-builder/issues/42>`_)
|
||||
- Display analytic filters in the header of exported pdf and xls. (`#44 <https://github.com/oca/mis-builder/issues/44>`_)
|
||||
- Replace the last old gtk icons with fontawesome icons. (`#104 <https://github.com/oca/mis-builder/issues/104>`_)
|
||||
- Use active_test=False in AEP queries.
|
||||
This is important for reports involving inactive taxes.
|
||||
This should not negatively effect existing reports, because
|
||||
an accounting report must take into account all existing move lines
|
||||
even if they reference objects such as taxes, journals, accounts types
|
||||
that have been deactivated since their creation. (`#107 <https://github.com/oca/mis-builder/issues/107>`_)
|
||||
- int(), float() and round() support for AccountingNone. (`#108 <https://github.com/oca/mis-builder/issues/108>`_)
|
||||
- Allow referencing subkpis by name by writing `kpi_x.subkpi_y` in expressions. (`#114 <https://github.com/oca/mis-builder/issues/114>`_)
|
||||
- Add an option to control the display of the start/end dates in the
|
||||
column headers. It is disabled by default (this is a change compared
|
||||
to previous behaviour). (`#118 <https://github.com/oca/mis-builder/issues/118>`_)
|
||||
- Add evaluate method to mis.report. This is a simplified
|
||||
method to evaluate kpis of a report over a time period,
|
||||
without creating a mis.report.instance. (`#123 <https://github.com/oca/mis-builder/issues/123>`_)
|
||||
|
||||
**Bugs**
|
||||
|
||||
- In the style form, hide the "Hide always" checkbox when "Hide always inherit"
|
||||
is checked, as for all other syle elements. (`#121 <https://github.com/OCA/mis-builder/pull/121>_`)
|
||||
|
||||
**Upgrading from 3.2 (breaking changes)**
|
||||
|
||||
If you use ``Actuals (alternative)`` data source in combination with analytic
|
||||
filters, the underlying model must now have an ``analytic_account_id`` field.
|
||||
|
||||
|
||||
11.0.3.2.2 (2018-06-30)
|
||||
~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
* [FIX] Fix bug in company_default_get call returning
|
||||
id instead of recordset
|
||||
(`#103 <https://github.com/OCA/mis-builder/pull/103>`_)
|
||||
* [IMP] add "hide always" style property to make hidden KPI's
|
||||
(for KPI that serve as basis for other formulas, but do not
|
||||
need to be displayed).
|
||||
(`#46 <https://github.com/OCA/mis-builder/issues/46>`_)
|
||||
|
||||
11.0.3.2.1 (2018-05-29)
|
||||
~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
* [FIX] Missing comparison operator for AccountingNone
|
||||
leading to errors in pbal computations
|
||||
(`#93 <https://github.com/OCA/mis-builder/issue/93>`_)
|
||||
|
||||
10.0.3.2.0 (2018-05-02)
|
||||
~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
* [FIX] make subkpi ordering deterministic
|
||||
(`#71 <https://github.com/OCA/mis-builder/issues/71>`_)
|
||||
* [ADD] report instance level option to disable account expansion,
|
||||
enabling the creation of detailed templates while deferring the decision
|
||||
of rendering the details or not to the report instance
|
||||
(`#74 <https://github.com/OCA/mis-builder/issues/74>`_)
|
||||
* [ADD] pbal and nbal accounting expressions, to sum positive
|
||||
and negative balances respectively (ie ignoring accounts with negative,
|
||||
resp positive balances)
|
||||
(`#86 <https://github.com/OCA/mis-builder/issues/86>`_)
|
||||
|
||||
11.0.3.1.2 (2018-02-04)
|
||||
~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Migration to Odoo 11. No new feature.
|
||||
(`#67 <https://github.com/OCA/mis-builder/pull/67>`_)
|
||||
|
||||
10.0.3.1.1 (2017-11-14)
|
||||
~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
New features:
|
||||
|
||||
* [ADD] month and year relative periods, easier to use than
|
||||
date ranges for the most common case.
|
||||
(`#2 <https://github.com/OCA/mis-builder/issues/2>`_)
|
||||
* [ADD] multi-company consolidation support, with currency conversion
|
||||
(the conversion rate date is the end of the reporting period)
|
||||
(`#7 <https://github.com/OCA/mis-builder/issues/7>`_,
|
||||
`#3 <https://github.com/OCA/mis-builder/issues/3>`_)
|
||||
* [ADD] provide ref, datetime, dateutil, time, user in the evaluation
|
||||
context of move line domains; among other things, this allows using
|
||||
references to xml ids (such as account types or tax tags) when
|
||||
querying move lines
|
||||
(`#26 <https://github.com/OCA/mis-builder/issues/26>`_).
|
||||
* [ADD] extended account selectors: you can now select accounts using
|
||||
any domain on account.account, not only account codes
|
||||
``balp[('account_type', '=', 'asset_receivable')]``
|
||||
(`#4 <https://github.com/OCA/mis-builder/issues/4>`_).
|
||||
* [IMP] in the report instance configuration form, the filters are
|
||||
now grouped in a notebook page, this improves readability and
|
||||
extensibility
|
||||
(`#39 <https://github.com/OCA/mis-builder/issues/39>`_).
|
||||
|
||||
Bug fixes:
|
||||
|
||||
* [FIX] fix error when saving periods in comparison mode on newly
|
||||
created (not yet saved) report instances.
|
||||
`#50 <https://github.com/OCA/mis-builder/pull/50>`_
|
||||
* [FIX] improve display of Base Date report instance view.
|
||||
`#51 <https://github.com/OCA/mis-builder/pull/51>`_
|
||||
|
||||
Upgrading from 3.0 (breaking changes):
|
||||
|
||||
* Alternative move line data sources must have a company_id field.
|
||||
|
||||
10.0.3.0.4 (2017-10-14)
|
||||
~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Bug fix:
|
||||
|
||||
* [FIX] issue with initial balance rounding.
|
||||
`#30 <https://github.com/OCA/mis-builder/issues/30>`_
|
||||
|
||||
10.0.3.0.3 (2017-10-03)
|
||||
~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Bug fix:
|
||||
|
||||
* [FIX] fix error saving KPI on newly created reports.
|
||||
`#18 <https://github.com/OCA/mis-builder/issues/18>`_
|
||||
|
||||
10.0.3.0.2 (2017-10-01)
|
||||
~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
New features:
|
||||
|
||||
* [ADD] Alternative move line source per report column.
|
||||
This makes mis buidler accounting expressions work on any model
|
||||
that has debit, credit, account_id and date fields. Provided you can
|
||||
expose, say, committed purchases, or your budget as a view with
|
||||
debit, credit and account_id, this opens up a lot of possibilities
|
||||
* [ADD] Comparison column source (more flexible than the previous,
|
||||
now deprecated, comparison mechanism).
|
||||
CAVEAT: there is no automated migration to the new mechanism.
|
||||
* [ADD] Sum column source, to create columns that add/subtract
|
||||
other columns.
|
||||
* [ADD] mis.kpi.data abstract model as a basis for manual KPI values
|
||||
supporting automatic ajustment to the reporting time period (the basis
|
||||
for budget item, but could also server other purposes, such as manually
|
||||
entering some KPI values, such as number of employee)
|
||||
* [ADD] mis_builder_budget module providing a new budget data source
|
||||
* [ADD] new "hide empty" style property
|
||||
* [IMP] new AEP method to get accounts involved in an expression
|
||||
(this is useful to find which KPI relate to a given P&L
|
||||
acount, to implement budget control)
|
||||
* [IMP] many UI improvements
|
||||
* [IMP] many code style improvements and some refactoring
|
||||
* [IMP] add the column date_from, date_to in expression evaluation context,
|
||||
as well as time, datetime and dateutil modules
|
||||
|
||||
Main bug fixes:
|
||||
|
||||
* [FIX] deletion of templates and reports (cascade and retricts)
|
||||
(https://github.com/OCA/account-financial-reporting/issues/281)
|
||||
* [FIX] copy of reports
|
||||
(https://github.com/OCA/account-financial-reporting/issues/282)
|
||||
* [FIX] better error message when periods have wrong/missing dates
|
||||
(https://github.com/OCA/account-financial-reporting/issues/283)
|
||||
* [FIX] xlsx export of string types KPI
|
||||
(https://github.com/OCA/account-financial-reporting/issues/285)
|
||||
* [FIX] sorting of detail by account
|
||||
* [FIX] computation bug in detail by account when multiple accounting
|
||||
expressions were used in a KPI
|
||||
* [FIX] permission issue when adding report to dashboard with non admin user
|
||||
|
||||
10.0.2.0.3 (unreleased)
|
||||
~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
* [IMP] more robust behaviour in presence of missing expressions
|
||||
* [FIX] indent style
|
||||
* [FIX] local variable 'ctx' referenced before assignment when generating
|
||||
reports with no objects
|
||||
* [IMP] use fontawesome icons
|
||||
* [MIG] migrate to 10.0
|
||||
* [FIX] unicode error when exporting to Excel
|
||||
* [IMP] provide full access to mis builder style for group Adviser.
|
||||
|
||||
9.0.2.0.2 (2016-09-27)
|
||||
~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
* [IMP] Add refresh button in mis report preview.
|
||||
* [IMP] Widget code changes to allow to add fields in the widget more easily.
|
||||
|
||||
9.0.2.0.1 (2016-05-26)
|
||||
~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
* [IMP] remove unused argument in declare_and_compute_period()
|
||||
for a cleaner API. This is a breaking API changing merged in
|
||||
urgency before it is used by other modules.
|
||||
|
||||
9.0.2.0.0 (2016-05-24)
|
||||
~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Part of the work for this release has been done at the Sorrento sprint
|
||||
April 26-29, 2016. The rest (ie a major refactoring) has been done in
|
||||
the weeks after.
|
||||
|
||||
* [IMP] hide button box in edit mode on the report instance settings form
|
||||
* [FIX] Fix sum aggregation of non-stored fields
|
||||
(https://github.com/OCA/account-financial-reporting/issues/178)
|
||||
* [IMP] There is now a default style at the report level
|
||||
* [CHG] Number display properties (rounding, prefix, suffix, factor) are
|
||||
now defined in styles
|
||||
* [CHG] Percentage difference are rounded to 1 digit instead of the kpi's
|
||||
rounding, as the KPI rounding does not make sense in this case
|
||||
* [CHG] The divider suffix (k, M, etc) is not inserted automatically anymore
|
||||
because it is inconsistent when working with prefixes; you need to add it
|
||||
manually in the suffix
|
||||
* [IMP] AccountingExpressionProcessor now supports 'balu' expressions
|
||||
to obtain the unallocated profit/loss of previous fiscal years;
|
||||
get_unallocated_pl is the corresponding convenience method
|
||||
* [IMP] AccountingExpressionProcessor now has easy methods to obtain
|
||||
balances by account: get_balances_initial, get_balances_end,
|
||||
get_balances_variation
|
||||
* [IMP] there is now an auto-expand feature to automatically display
|
||||
a detail by account for selected kpis
|
||||
* [IMP] the kpi and period lists are now manipulated through forms instead
|
||||
of directly in the tree views
|
||||
* [IMP] it is now possible to create a report through a wizard, such
|
||||
reports are deemed temporary and available through a "Last Reports Generated"
|
||||
menu, they are garbaged collected automatically, unless saved permanently,
|
||||
which can be done using a Save button
|
||||
* [IMP] there is now a beginner mode to configure simple reports with
|
||||
only one period
|
||||
* [IMP] it is now easier to configure periods with fixed start/end dates
|
||||
* [IMP] the new sub-kpi mechanism allows the creation of columns
|
||||
with multiple values, or columns with different values
|
||||
* [IMP] thanks to the new style model, the Excel export is now styled
|
||||
* [IMP] a new style model is now used to centralize style configuration
|
||||
* [FIX] use =like instead of like to search for accounts, because
|
||||
the % are added by the user in the expressions
|
||||
* [FIX] Correctly compute the initial balance of income and expense account
|
||||
based on the start of the fiscal year
|
||||
* [IMP] Support date ranges (from OCA/server-tools/date_range) as a more
|
||||
flexible alternative to fiscal periods
|
||||
* v9 migration: fiscal periods are removed, account charts are removed,
|
||||
consolidation accounts have been removed
|
||||
|
||||
8.0.1.0.0 (2016-04-27)
|
||||
~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
* The copy of a MIS Report Instance now copies period.
|
||||
https://github.com/OCA/account-financial-reporting/pull/181
|
||||
* The copy of a MIS Report Template now copies KPIs and queries.
|
||||
https://github.com/OCA/account-financial-reporting/pull/177
|
||||
* Usability: the default view for MIS Report instances is now the rendered preview,
|
||||
and the settings are accessible through a gear icon in the list view and
|
||||
a button in the preview.
|
||||
https://github.com/OCA/account-financial-reporting/pull/170
|
||||
* Display blank cells instead of 0.0 when there is no data.
|
||||
https://github.com/OCA/account-financial-reporting/pull/169
|
||||
* Usability: better layout of the MIS Report periods settings on small screens.
|
||||
https://github.com/OCA/account-financial-reporting/pull/167
|
||||
* Include the download buttons inside the MIS Builder widget, and refactor
|
||||
the widget to open the door to analytic filtering in the previews.
|
||||
https://github.com/OCA/account-financial-reporting/pull/151
|
||||
* Add KPI rendering prefixes (so you can print $ in front of the value).
|
||||
https://github.com/OCA/account-financial-reporting/pull/158
|
||||
* Add hooks for analytic filtering.
|
||||
https://github.com/OCA/account-financial-reporting/pull/128
|
||||
https://github.com/OCA/account-financial-reporting/pull/131
|
||||
|
||||
8.0.0.2.0
|
||||
~~~~~~~~~
|
||||
|
||||
Pre-history. Or rather, you need to look at the git log.
|
||||
|
|
@ -1,7 +0,0 @@
|
|||
Your preferred way to install addons will work with MIS Builder.
|
||||
|
||||
An easy way to install it with all its dependencies is using pip:
|
||||
|
||||
* ``pip install --pre odoo12-addon-mis_builder``
|
||||
* then restart Odoo, update the addons list in your database, and install
|
||||
the MIS Builder application.
|
||||
|
|
@ -1,3 +0,0 @@
|
|||
The mis_builder `roadmap <https://github.com/OCA/mis-builder/issues?q=is%3Aopen+is%3Aissue+label%3Aenhancement>`_
|
||||
and `known issues <https://github.com/OCA/mis-builder/issues?q=is%3Aopen+is%3Aissue+label%3Abug>`_ can
|
||||
be found on GitHub.
|
||||
|
|
@ -1,26 +0,0 @@
|
|||
To configure this module, you need to:
|
||||
|
||||
* Go to Accounting > Configuration > MIS Reporting > MIS Report Templates where
|
||||
you can create report templates by defining KPI's. KPI's constitute the rows of your
|
||||
reports. Such report templates are time independent.
|
||||
|
||||
.. figure:: https://raw.githubusercontent.com/OCA/mis-builder/10.0/mis_builder/static/description/ex_report_template.png
|
||||
:alt: Sample report template
|
||||
:width: 80 %
|
||||
:align: center
|
||||
|
||||
* Then in Accounting > Reports > MIS Reporting > MIS Reports you can create report instance by
|
||||
binding the templates to time periods, hence defining the columns of your reports.
|
||||
|
||||
.. figure:: https://raw.githubusercontent.com/OCA/mis-builder/10.0/mis_builder/static/description/ex_report_settings.png
|
||||
:alt: Sample report configuration
|
||||
:width: 80 %
|
||||
:align: center
|
||||
|
||||
* From the MIS Reports view, you can preview the report, add it to and Odoo dashboard,
|
||||
and export it to PDF or Excel.
|
||||
|
||||
.. figure:: https://raw.githubusercontent.com/OCA/mis-builder/10.0/mis_builder/static/description/ex_report_preview.png
|
||||
:alt: Sample preview
|
||||
:width: 80 %
|
||||
:align: center
|
||||
|
|
@ -1,5 +0,0 @@
|
|||
# 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
|
||||
|
|
@ -1,27 +0,0 @@
|
|||
# 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)
|
||||
|
|
@ -1,134 +0,0 @@
|
|||
<?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>
|
||||
|
|
@ -1,186 +0,0 @@
|
|||
# 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)
|
||||
|
|
@ -1,11 +0,0 @@
|
|||
<?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>
|
||||
|
|
@ -1,24 +0,0 @@
|
|||
"id","name","model_id:id","group_id:id","perm_read","perm_write","perm_create","perm_unlink"
|
||||
manage_mis_report_kpi,manage_mis_report_kpi,model_mis_report_kpi,account.group_account_manager,1,1,1,1
|
||||
access_mis_report_kpi,access_mis_report_kpi,model_mis_report_kpi,base.group_user,1,0,0,0
|
||||
manage_mis_report_query,manage_mis_report_query,model_mis_report_query,account.group_account_manager,1,1,1,1
|
||||
access_mis_report_query,access_mis_report_query,model_mis_report_query,base.group_user,1,0,0,0
|
||||
manage_mis_report,manage_mis_report,model_mis_report,account.group_account_manager,1,1,1,1
|
||||
access_mis_report,access_mis_report,model_mis_report,base.group_user,1,0,0,0
|
||||
manage_mis_report_instance_period,manage_mis_report_instance_period,model_mis_report_instance_period,account.group_account_manager,1,1,1,1
|
||||
access_mis_report_instance_period,access_mis_report_instance_period,model_mis_report_instance_period,base.group_user,1,0,0,0
|
||||
manage_mis_report_instance_period_sum,manage_mis_report_instance_period_sum,model_mis_report_instance_period_sum,account.group_account_manager,1,1,1,1
|
||||
access_mis_report_instance_period_sum,access_mis_report_instance_period_sum,model_mis_report_instance_period_sum,base.group_user,1,0,0,0
|
||||
manage_mis_report_instance,manage_mis_report_instance,model_mis_report_instance,account.group_account_manager,1,1,1,1
|
||||
access_mis_report_instance,access_mis_report_instance,model_mis_report_instance,base.group_user,1,0,0,0
|
||||
manage_mis_report_subkpi,access_mis_report_subkpi,model_mis_report_subkpi,account.group_account_manager,1,1,1,1
|
||||
access_mis_report_subkpi,access_mis_report_subkpi,model_mis_report_subkpi,base.group_user,1,0,0,0
|
||||
manage_mis_report_kpi_expression,access_mis_report_kpi_expression,model_mis_report_kpi_expression,account.group_account_manager,1,1,1,1
|
||||
access_mis_report_kpi_expression,access_mis_report_kpi_expression,model_mis_report_kpi_expression,base.group_user,1,0,0,0
|
||||
manage_mis_report_subreport,access_mis_report_subreport,model_mis_report_subreport,account.group_account_manager,1,1,1,1
|
||||
access_mis_report_subreport,access_mis_report_subreport,model_mis_report_subreport,base.group_user,1,0,0,0
|
||||
manage_mis_report_style,access_mis_report_style,model_mis_report_style,account.group_account_manager,1,1,1,1
|
||||
access_mis_report_style,access_mis_report_style,model_mis_report_style,base.group_user,1,0,0,0
|
||||
access_add_to_dashboard_wizard,access_add_to_dashboard_wizard,model_add_mis_report_instance_dashboard_wizard,base.group_user,1,1,1,0
|
||||
access_read_mis_report_annotation, access_read_mis_report_annotation,model_mis_report_instance_annotation,mis_builder.group_read_annotation,1,0,0,0
|
||||
access_edit_mis_report_annotation, access_edit_mis_report_annotation,model_mis_report_instance_annotation,mis_builder.group_edit_annotation,1,1,1,1
|
||||
|
|
|
@ -1,11 +0,0 @@
|
|||
<?xml version="1.0" encoding="utf-8" ?>
|
||||
<odoo>
|
||||
<record id="mis_builder_multi_company_rule" model="ir.rule">
|
||||
<field name="name">Mis Report Instance multi company</field>
|
||||
<field name="model_id" ref="model_mis_report_instance" />
|
||||
<field name="domain_force">
|
||||
['|',('company_id','=',False),('company_id','in',company_ids), '|',
|
||||
('company_ids', '=', False), ('company_ids', 'in', company_ids)]
|
||||
</field>
|
||||
</record>
|
||||
</odoo>
|
||||
|
|
@ -1,17 +0,0 @@
|
|||
<?xml version="1.0" encoding="utf-8" ?>
|
||||
<odoo>
|
||||
<record model="res.groups" id="group_read_annotation">
|
||||
<field name="name">MIS Report: view annotations</field>
|
||||
</record>
|
||||
<record model="res.groups" id="group_edit_annotation">
|
||||
<field name="name">MIS Report: add annotations</field>
|
||||
<field
|
||||
name="implied_ids"
|
||||
eval="[Command.link(ref('mis_builder.group_read_annotation'))]"
|
||||
/>
|
||||
<field
|
||||
name="users"
|
||||
eval="[(4, ref('base.user_root')), (4, ref('base.user_admin'))]"
|
||||
/>
|
||||
</record>
|
||||
</odoo>
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 94 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 101 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 98 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 4.7 KiB |
File diff suppressed because it is too large
Load diff
|
|
@ -1,108 +0,0 @@
|
|||
.o_web_client .mis_builder_amount {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.o_web_client .mis_builder_collabel {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.o_web_client .mis_builder_rowlabel {
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.o_web_client .mis_builder a {
|
||||
/* we don't want the link color, to respect user styles */
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
.o_web_client .mis_builder a:hover {
|
||||
/* underline links on hover to give a visual cue */
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.oe_mis_builder_content {
|
||||
}
|
||||
|
||||
.oe_mis_builder_report_wide_sheet {
|
||||
max-width: 95% !important;
|
||||
}
|
||||
|
||||
/* style for the control panel (search box and buttons) */
|
||||
|
||||
.oe_mis_builder_cp {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
padding-bottom: 20px;
|
||||
}
|
||||
|
||||
.oe_mis_builder_cp_left {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
.oe_mis_builder_cp_right {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex-grow: 2;
|
||||
max-width: 1280px;
|
||||
}
|
||||
|
||||
.oe_mis_builder_cp_right_top_right {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
}
|
||||
|
||||
.oe_mis_builder_cp_right_top {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
}
|
||||
|
||||
.oe_mis_builder_cp_right_bottom {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
}
|
||||
|
||||
.oe_mis_builder_filter_buttons {
|
||||
display: flex;
|
||||
flex-grow: 1;
|
||||
justify-content: flex-start;
|
||||
}
|
||||
|
||||
.oe_mis_builder_action_buttons {
|
||||
display: flex;
|
||||
flex-grow: 1;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.oe_mis_builder_dropdown {
|
||||
overflow: visible !important;
|
||||
}
|
||||
|
||||
.oe_mis_builder_footnote {
|
||||
font-size: 80%;
|
||||
color: red;
|
||||
position: relative;
|
||||
bottom: 1ex;
|
||||
width: 1em;
|
||||
display: inline-block;
|
||||
padding-right: 1px;
|
||||
}
|
||||
|
||||
.oe_mis_builder_footnote_table {
|
||||
list-style: none;
|
||||
white-space: pre-wrap;
|
||||
display: inline-block;
|
||||
|
||||
td {
|
||||
vertical-align: top;
|
||||
}
|
||||
}
|
||||
|
||||
.oe_mis_builder_footnote_div {
|
||||
padding-top: 1em;
|
||||
}
|
||||
|
||||
.oe_mis_builder_menu_disabled {
|
||||
color: gainsboro;
|
||||
}
|
||||
|
|
@ -1,287 +0,0 @@
|
|||
/** @odoo-module **/
|
||||
|
||||
import Dialog from "web.Dialog";
|
||||
import {Component, onMounted, onWillStart, useState, useSubEnv} from "@odoo/owl";
|
||||
import {DatePicker} from "@web/core/datepicker/datepicker";
|
||||
import {FilterMenu} from "@web/search/filter_menu/filter_menu";
|
||||
import {SearchBar} from "@web/search/search_bar/search_bar";
|
||||
import {SearchModel} from "@web/search/search_model";
|
||||
import {parseDate} from "@web/core/l10n/dates";
|
||||
import {qweb} from "web.core";
|
||||
import {registry} from "@web/core/registry";
|
||||
import {useBus, useService} from "@web/core/utils/hooks";
|
||||
|
||||
export class MisReportWidget extends Component {
|
||||
setup() {
|
||||
super.setup();
|
||||
this.orm = useService("orm");
|
||||
this.user = useService("user");
|
||||
this.action = useService("action");
|
||||
this.view = useService("view");
|
||||
this.JSON = JSON;
|
||||
this.state = useState({
|
||||
mis_report_data: {header: [], body: [], notes: {}},
|
||||
pivot_date: null,
|
||||
can_edit_annotation: false,
|
||||
can_read_annotation: false,
|
||||
});
|
||||
this.searchModel = new SearchModel(this.env, {
|
||||
user: this.user,
|
||||
orm: this.orm,
|
||||
view: this.view,
|
||||
});
|
||||
useSubEnv({searchModel: this.searchModel});
|
||||
useBus(this.env.searchModel, "update", async () => {
|
||||
await this.env.searchModel.sectionsPromise;
|
||||
this.refresh();
|
||||
});
|
||||
onWillStart(this.willStart);
|
||||
|
||||
onMounted(this._onMounted);
|
||||
}
|
||||
|
||||
// Lifecycle
|
||||
async willStart() {
|
||||
const [result] = await this.orm.read(
|
||||
"mis.report.instance",
|
||||
[this._instanceId()],
|
||||
[
|
||||
"source_aml_model_name",
|
||||
"widget_show_filters",
|
||||
"widget_show_settings_button",
|
||||
"widget_search_view_id",
|
||||
"pivot_date",
|
||||
"widget_show_pivot_date",
|
||||
"user_can_read_annotation",
|
||||
"user_can_edit_annotation",
|
||||
"wide_display_by_default",
|
||||
],
|
||||
{context: this.context}
|
||||
);
|
||||
this.source_aml_model_name = result.source_aml_model_name;
|
||||
this.widget_show_filters = result.widget_show_filters;
|
||||
this.widget_show_settings_button = result.widget_show_settings_button;
|
||||
this.widget_search_view_id =
|
||||
result.widget_search_view_id && result.widget_search_view_id[0];
|
||||
this.state.pivot_date = parseDate(result.pivot_date);
|
||||
this.widget_show_pivot_date = result.widget_show_pivot_date;
|
||||
if (this.showSearchBar) {
|
||||
// Initialize the search model
|
||||
await this.searchModel.load({
|
||||
resModel: this.source_aml_model_name,
|
||||
searchViewId: this.widget_search_view_id,
|
||||
});
|
||||
}
|
||||
|
||||
this.wide_display = result.wide_display_by_default;
|
||||
|
||||
// Compute the report
|
||||
this.refresh();
|
||||
this.state.can_edit_annotation = result.user_can_edit_annotation;
|
||||
this.state.can_read_annotation = result.user_can_read_annotation;
|
||||
}
|
||||
|
||||
async _onMounted() {
|
||||
this.resize_sheet();
|
||||
}
|
||||
|
||||
get showSearchBar() {
|
||||
return (
|
||||
this.source_aml_model_name &&
|
||||
this.widget_show_filters &&
|
||||
this.widget_search_view_id
|
||||
);
|
||||
}
|
||||
|
||||
get showPivotDate() {
|
||||
return this.widget_show_pivot_date;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the id of the mis.report.instance to which the widget is
|
||||
* bound.
|
||||
*
|
||||
* @returns int
|
||||
*/
|
||||
_instanceId() {
|
||||
if (this.props.value) {
|
||||
return this.props.value;
|
||||
}
|
||||
|
||||
/*
|
||||
* This trick is needed because in a dashboard the view does
|
||||
* not seem to be bound to an instance: it seems to be a limitation
|
||||
* of Odoo dashboards that are not designed to contain forms but
|
||||
* rather tree views or charts.
|
||||
*/
|
||||
var context = this.props.record.context;
|
||||
if (context.active_model === "mis.report.instance") {
|
||||
return context.active_id;
|
||||
}
|
||||
}
|
||||
|
||||
get context() {
|
||||
var ctx = super.context;
|
||||
if (this.showSearchBar) {
|
||||
ctx = {
|
||||
...ctx,
|
||||
mis_analytic_domain: this.searchModel.searchDomain,
|
||||
};
|
||||
}
|
||||
if (this.showPivotDate && this.state.pivot_date) {
|
||||
ctx = {
|
||||
...ctx,
|
||||
mis_pivot_date: this.state.pivot_date,
|
||||
};
|
||||
}
|
||||
return ctx;
|
||||
}
|
||||
|
||||
async drilldown(event) {
|
||||
const drilldown = JSON.parse(event.target.dataset.drilldown);
|
||||
const action = await this.orm.call(
|
||||
"mis.report.instance",
|
||||
"drilldown",
|
||||
[this._instanceId(), drilldown],
|
||||
{context: this.context}
|
||||
);
|
||||
this.action.doAction(action);
|
||||
}
|
||||
|
||||
async refresh() {
|
||||
this.state.mis_report_data = await this.orm.call(
|
||||
"mis.report.instance",
|
||||
"compute",
|
||||
[this._instanceId()],
|
||||
{context: this.context}
|
||||
);
|
||||
}
|
||||
|
||||
async refresh_annotation() {
|
||||
this.state.mis_report_data.notes = await this.orm.call(
|
||||
"mis.report.instance",
|
||||
"get_notes_by_cell_id",
|
||||
[this._instanceId()],
|
||||
{context: this.context}
|
||||
);
|
||||
}
|
||||
|
||||
async printPdf() {
|
||||
const action = await this.orm.call(
|
||||
"mis.report.instance",
|
||||
"print_pdf",
|
||||
[this._instanceId()],
|
||||
{context: this.context}
|
||||
);
|
||||
this.action.doAction(action);
|
||||
}
|
||||
|
||||
async exportXls() {
|
||||
const action = await this.orm.call(
|
||||
"mis.report.instance",
|
||||
"export_xls",
|
||||
[this._instanceId()],
|
||||
{context: this.context}
|
||||
);
|
||||
this.action.doAction(action);
|
||||
}
|
||||
|
||||
async displaySettings() {
|
||||
const action = await this.orm.call(
|
||||
"mis.report.instance",
|
||||
"display_settings",
|
||||
[this._instanceId()],
|
||||
{context: this.context}
|
||||
);
|
||||
this.action.doAction(action);
|
||||
}
|
||||
|
||||
async _remove_annotation(cell_id) {
|
||||
await this.orm.call(
|
||||
"mis.report.instance.annotation",
|
||||
"remove_annotation",
|
||||
[cell_id, this._instanceId()],
|
||||
{context: this.context}
|
||||
);
|
||||
this.refresh_annotation();
|
||||
}
|
||||
|
||||
async _save_annotation(cell_id) {
|
||||
const text = document.querySelector(".o_mis_builder_annotation_text").value;
|
||||
await this.orm.call(
|
||||
"mis.report.instance.annotation",
|
||||
"set_annotation",
|
||||
[cell_id, this._instanceId(), text],
|
||||
{context: this.context}
|
||||
);
|
||||
await this.refresh_annotation();
|
||||
}
|
||||
|
||||
async annotate(event) {
|
||||
const cell_id = event.target.dataset.cellId;
|
||||
const note = this.state.mis_report_data.notes[cell_id];
|
||||
const note_text = (note && note.text) || "";
|
||||
var buttons = [
|
||||
{
|
||||
text: this.env._t("Save"),
|
||||
classes: "btn-primary",
|
||||
close: true,
|
||||
click: this._save_annotation.bind(this, cell_id),
|
||||
},
|
||||
{
|
||||
text: this.env._t("Cancel"),
|
||||
close: true,
|
||||
},
|
||||
];
|
||||
if (typeof note !== "undefined") {
|
||||
buttons.push({
|
||||
text: this.env._t("Remove"),
|
||||
classes: "btn-secondary",
|
||||
close: true,
|
||||
click: this._remove_annotation.bind(this, cell_id),
|
||||
});
|
||||
}
|
||||
|
||||
new Dialog(this, {
|
||||
title: "Annotate",
|
||||
size: "medium",
|
||||
$content: $(
|
||||
qweb.render("mis_builder.annotation_dialog", {
|
||||
text: note_text,
|
||||
})
|
||||
),
|
||||
buttons: buttons,
|
||||
}).open();
|
||||
}
|
||||
|
||||
async remove_annotation(event) {
|
||||
const cell_id = event.target.dataset.cellId;
|
||||
this._remove_annotation(cell_id);
|
||||
}
|
||||
|
||||
onDateTimeChanged(ev) {
|
||||
this.state.pivot_date = ev;
|
||||
this.refresh();
|
||||
}
|
||||
|
||||
async toggle_wide_display() {
|
||||
this.wide_display = !this.wide_display;
|
||||
this.resize_sheet();
|
||||
}
|
||||
|
||||
async resize_sheet() {
|
||||
var sheet_element = document.getElementsByClassName("o_form_sheet")[0];
|
||||
sheet_element.classList.toggle(
|
||||
"oe_mis_builder_report_wide_sheet",
|
||||
this.wide_display
|
||||
);
|
||||
var button_resize_element = document.getElementById("icon_resize");
|
||||
button_resize_element.classList.toggle("fa-expand", !this.wide_display);
|
||||
button_resize_element.classList.toggle("fa-compress", this.wide_display);
|
||||
}
|
||||
}
|
||||
|
||||
MisReportWidget.components = {FilterMenu, SearchBar, DatePicker};
|
||||
MisReportWidget.template = "mis_builder.MisReportWidget";
|
||||
|
||||
registry.category("fields").add("mis_report_widget", MisReportWidget);
|
||||
|
|
@ -1,206 +0,0 @@
|
|||
<?xml version="1.0" encoding="utf-8" ?>
|
||||
<templates>
|
||||
|
||||
<t t-name="mis_builder.MisReportWidget" owl="1">
|
||||
<div class="oe_mis_builder_content">
|
||||
<t t-if="state.mis_report_data">
|
||||
<t t-set="notes" t-value="state.mis_report_data.notes" />
|
||||
<div class="oe_mis_builder_cp">
|
||||
<div class="oe_mis_builder_cp_left">
|
||||
</div>
|
||||
<div class="oe_mis_builder_cp_right">
|
||||
<div class="oe_mis_builder_cp_right_top_right">
|
||||
<div class="oe_mis_builder_action_buttons">
|
||||
<button
|
||||
t-on-click="toggle_wide_display"
|
||||
class="btn btn-secondary"
|
||||
>
|
||||
<i id="icon_resize" class="fa" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="oe_mis_builder_cp_right_top">
|
||||
<SearchBar t-if="showSearchBar" />
|
||||
</div>
|
||||
<div class="oe_mis_builder_cp_right_bottom">
|
||||
<div class="oe_mis_builder_filter_buttons">
|
||||
<FilterMenu t-if="showSearchBar" />
|
||||
<DatePicker
|
||||
date="state.pivot_date"
|
||||
onDateTimeChanged="onDateTimeChanged.bind(this)"
|
||||
placeholder="'Base date...'"
|
||||
t-if="showPivotDate"
|
||||
/>
|
||||
</div>
|
||||
<div class="oe_mis_builder_action_buttons">
|
||||
<button t-on-click="refresh" class="btn">
|
||||
<span class="fa fa-refresh" /> Refresh </button>
|
||||
<button t-on-click="printPdf" class="btn">
|
||||
<span class="fa fa-print" /> Print </button>
|
||||
<button t-on-click="exportXls" class="btn">
|
||||
<span class="fa fa-download" /> Export </button>
|
||||
<button
|
||||
t-on-click="displaySettings"
|
||||
t-if="widget_show_settings_button"
|
||||
class="btn"
|
||||
>
|
||||
<span class="fa fa-cog" /> Settings </button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="o_list_renderer o_renderer table-responsive">
|
||||
<table
|
||||
class="o_list_table table table-sm table-hover table-striped mis_builder"
|
||||
>
|
||||
<thead>
|
||||
<tr
|
||||
t-foreach="state.mis_report_data.header"
|
||||
t-as="row"
|
||||
t-key="row_index"
|
||||
class="oe_list_header_columns"
|
||||
>
|
||||
<th class="oe_list_header_char">
|
||||
|
||||
</th>
|
||||
<th
|
||||
t-foreach="row.cols"
|
||||
t-as="col"
|
||||
t-key="col_index"
|
||||
class="oe_list_header_char mis_builder_collabel"
|
||||
t-att-colspan="col.colspan"
|
||||
>
|
||||
<t t-esc="col.label" />
|
||||
<t t-if="col.description">
|
||||
<br />
|
||||
<t t-esc="col.description" />
|
||||
</t>
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr
|
||||
t-foreach="state.mis_report_data.body"
|
||||
t-as="row"
|
||||
t-key="row_index"
|
||||
>
|
||||
<td t-att="{'style': row.style}">
|
||||
<t t-esc="row.label" />
|
||||
<t t-if="row.description">
|
||||
<br />
|
||||
<t t-esc="row.description" />
|
||||
</t>
|
||||
</td>
|
||||
<td
|
||||
t-foreach="row.cells"
|
||||
t-as="cell"
|
||||
t-key="cell_index"
|
||||
t-att="{'style': cell.style, 'title': cell.val_c}"
|
||||
class="mis_builder_amount oe_mis_builder_dropdown"
|
||||
>
|
||||
<div>
|
||||
<t t-if="cell.drilldown_arg">
|
||||
<a
|
||||
href="javascript:void(0)"
|
||||
class="mis_builder_drilldown"
|
||||
t-on-click="drilldown"
|
||||
t-att-data-drilldown="JSON.stringify(cell.drilldown_arg)"
|
||||
>
|
||||
<t t-esc="cell.val_r" />
|
||||
</a>
|
||||
</t>
|
||||
<t t-else="">
|
||||
<t t-esc="cell.val_r" />
|
||||
</t>
|
||||
<span class="oe_mis_builder_footnote">
|
||||
<div t-if="notes[cell.cell_id]">
|
||||
<a
|
||||
t-att-id="'note_'+notes[cell.cell_id].sequence"
|
||||
t-out="notes[cell.cell_id] and notes[cell.cell_id].sequence"
|
||||
t-att="{'title': notes[cell.cell_id].text}"
|
||||
href="#footnotes"
|
||||
/>
|
||||
</div>
|
||||
</span>
|
||||
|
||||
<div id="dropdown_menu" class="btn-group">
|
||||
<div
|
||||
class="dropdown"
|
||||
t-if="state.can_edit_annotation and cell.can_be_annotated"
|
||||
>
|
||||
<div
|
||||
data-bs-toggle="dropdown"
|
||||
t-attf-class="dropdown-toggle"
|
||||
/>
|
||||
|
||||
<div
|
||||
class="dropdown-menu o_filter_menu"
|
||||
role="menu"
|
||||
>
|
||||
<a
|
||||
href="javascript:void(0)"
|
||||
t-on-click="annotate"
|
||||
t-att-data-cell-id="cell.cell_id"
|
||||
role="menuitem"
|
||||
class="dropdown-item js_tag"
|
||||
>
|
||||
Annotate
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<!-- show menu as disabled -->
|
||||
<div
|
||||
t-else=""
|
||||
class="dropdown-toggle oe_mis_builder_menu_disabled"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
<tfoot>
|
||||
<tr />
|
||||
</tfoot>
|
||||
</table>
|
||||
</div>
|
||||
<!-- Adding notes -->
|
||||
<div class="oe_mis_builder_footnote_div" id="footnotes">
|
||||
<table class="oe_mis_builder_footnote_table">
|
||||
<t
|
||||
t-foreach="state.mis_report_data.notes"
|
||||
t-as="cell_id"
|
||||
t-key="cell_id"
|
||||
>
|
||||
<tr>
|
||||
<td><a
|
||||
t-out="notes[cell_id].sequence"
|
||||
t-att-href="'#note_'+notes[cell_id].sequence"
|
||||
/>. </td>
|
||||
<td><t t-out="notes[cell_id].text" /></td>
|
||||
<td><i
|
||||
href="javascript:void(0)"
|
||||
t-on-click="remove_annotation"
|
||||
t-att-data-cell-id="cell_id"
|
||||
class="btn fa fa-trash-o"
|
||||
t-if="state.can_edit_annotation"
|
||||
/></td>
|
||||
</tr>
|
||||
</t>
|
||||
</table>
|
||||
</div>
|
||||
</t>
|
||||
</div>
|
||||
</t>
|
||||
|
||||
<t t-name="mis_builder.annotation_dialog">
|
||||
<form role="form">
|
||||
<textarea
|
||||
class="o_mis_builder_annotation_text"
|
||||
name="note"
|
||||
rows='4'
|
||||
placeholder="Insert note here"
|
||||
><t t-out="text" t-att-data-textnote="text" /></textarea>
|
||||
</form>
|
||||
</t>
|
||||
|
||||
</templates>
|
||||
|
|
@ -1,68 +0,0 @@
|
|||
.mis_table {
|
||||
display: table;
|
||||
width: 100%;
|
||||
table-layout: fixed;
|
||||
}
|
||||
.mis_row {
|
||||
display: table-row;
|
||||
page-break-inside: avoid;
|
||||
}
|
||||
.mis_cell {
|
||||
display: table-cell;
|
||||
page-break-inside: avoid;
|
||||
}
|
||||
.mis_thead {
|
||||
display: table-header-group;
|
||||
}
|
||||
.mis_tbody {
|
||||
display: table-row-group;
|
||||
}
|
||||
.mis_table,
|
||||
.mis_table .mis_row {
|
||||
border-left: 0px;
|
||||
border-right: 0px;
|
||||
text-align: left;
|
||||
padding-right: 3px;
|
||||
padding-left: 3px;
|
||||
padding-top: 2px;
|
||||
padding-bottom: 2px;
|
||||
border-collapse: collapse;
|
||||
}
|
||||
.mis_table .mis_row {
|
||||
border-color: grey;
|
||||
border-bottom: 1px solid lightGrey;
|
||||
}
|
||||
.mis_table .mis_cell.mis_collabel {
|
||||
font-weight: bold;
|
||||
background-color: #f0f0f0;
|
||||
text-align: center;
|
||||
}
|
||||
.mis_table .mis_cell.mis_rowlabel {
|
||||
text-align: left;
|
||||
/*white-space: nowrap;*/
|
||||
}
|
||||
.mis_table .mis_cell.mis_amount {
|
||||
text-align: right;
|
||||
}
|
||||
.oe_mis_builder_footnote {
|
||||
font-size: 70%;
|
||||
color: red;
|
||||
position: relative;
|
||||
bottom: 1ex;
|
||||
width: 1em;
|
||||
display: inline-block;
|
||||
padding-right: 1px;
|
||||
}
|
||||
.oe_mis_builder_footnote_div {
|
||||
padding-top: 1em;
|
||||
}
|
||||
|
||||
.oe_mis_builder_footnote_table {
|
||||
list-style: none;
|
||||
white-space: pre-wrap;
|
||||
display: inline-block;
|
||||
|
||||
td {
|
||||
vertical-align: top;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,17 +0,0 @@
|
|||
# Copyright 2014 ACSONE SA/NV (<http://acsone.eu>)
|
||||
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html).
|
||||
|
||||
from . import test_accounting_none
|
||||
from . import test_aep
|
||||
from . import test_multi_company_aep
|
||||
from . import test_aggregate
|
||||
from . import test_data_sources
|
||||
from . import test_kpi_data
|
||||
from . import test_mis_report_instance
|
||||
from . import test_mis_safe_eval
|
||||
from . import test_period_dates
|
||||
from . import test_render
|
||||
from . import test_simple_array
|
||||
from . import test_target_move
|
||||
from . import test_utc_midnight
|
||||
from . import test_mis_report_instance_annotation
|
||||
|
|
@ -1,67 +0,0 @@
|
|||
# Copyright 2017 ACSONE SA/NV (<http://acsone.eu>)
|
||||
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html).
|
||||
|
||||
import doctest
|
||||
|
||||
from odoo.tests import BaseCase, tagged
|
||||
|
||||
|
||||
def _zip(iter1, iter2):
|
||||
i = 0
|
||||
iter1 = iter(iter1)
|
||||
iter2 = iter(iter2)
|
||||
while True:
|
||||
i1 = next(iter1, None)
|
||||
i2 = next(iter2, None)
|
||||
if i1 is None and i2 is None:
|
||||
return
|
||||
yield i, i1, i2
|
||||
i += 1
|
||||
|
||||
|
||||
def assert_matrix(matrix, expected):
|
||||
for i, row, expected_row in _zip(matrix.iter_rows(), expected):
|
||||
if row is None and expected_row is not None:
|
||||
raise AssertionError("not enough rows")
|
||||
if row is not None and expected_row is None:
|
||||
raise AssertionError("too many rows")
|
||||
for j, cell, expected_val in _zip(row.iter_cells(), expected_row):
|
||||
assert (
|
||||
cell and cell.val
|
||||
) == expected_val, "{} != {} in row {} col {}".format(
|
||||
cell and cell.val, expected_val, i, j
|
||||
)
|
||||
|
||||
|
||||
@tagged("doctest")
|
||||
class OdooDocTestCase(BaseCase):
|
||||
"""
|
||||
We need a custom DocTestCase class in order to:
|
||||
- define test_tags to run as part of standard tests
|
||||
- output a more meaningful test name than default "DocTestCase.runTest"
|
||||
"""
|
||||
|
||||
__qualname__ = "doctests for "
|
||||
|
||||
def __init__(self, test):
|
||||
self.__test = test
|
||||
self.__name = test._dt_test.name
|
||||
super().__init__(self.__name)
|
||||
|
||||
def __getattr__(self, item):
|
||||
if item == self.__name:
|
||||
return self.__test
|
||||
|
||||
|
||||
def load_doctests(module):
|
||||
"""
|
||||
Generates a tests loading method for the doctests of the given module
|
||||
https://docs.python.org/3/library/unittest.html#load-tests-protocol
|
||||
"""
|
||||
|
||||
def load_tests(loader, tests, ignore):
|
||||
for test in doctest.DocTestSuite(module):
|
||||
tests.addTest(OdooDocTestCase(test))
|
||||
return tests
|
||||
|
||||
return load_tests
|
||||
|
|
@ -1,7 +0,0 @@
|
|||
from odoo import models
|
||||
|
||||
|
||||
class MisKpiDataTestItem(models.Model):
|
||||
_name = "mis.kpi.data.test.item"
|
||||
_inherit = "mis.kpi.data"
|
||||
_description = "MIS Kpi Data test item"
|
||||
|
|
@ -1,8 +0,0 @@
|
|||
# Copyright 2014 ACSONE SA/NV (<http://acsone.eu>)
|
||||
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html).
|
||||
|
||||
|
||||
from ..models import accounting_none
|
||||
from .common import load_doctests
|
||||
|
||||
load_tests = load_doctests(accounting_none)
|
||||
|
|
@ -1,467 +0,0 @@
|
|||
# Copyright 2014 ACSONE SA/NV (<http://acsone.eu>)
|
||||
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html).
|
||||
|
||||
import datetime
|
||||
import time
|
||||
|
||||
import odoo.tests.common as common
|
||||
from odoo import fields
|
||||
from odoo.exceptions import UserError
|
||||
from odoo.tools.safe_eval import safe_eval
|
||||
|
||||
from ..models import aep
|
||||
from ..models.accounting_none import AccountingNone
|
||||
from ..models.aep import AccountingExpressionProcessor as AEP
|
||||
from ..models.aep import _is_domain
|
||||
from .common import load_doctests
|
||||
|
||||
load_tests = load_doctests(aep)
|
||||
|
||||
|
||||
class TestAEP(common.TransactionCase):
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
self.res_company = self.env["res.company"]
|
||||
self.account_model = self.env["account.account"]
|
||||
self.move_model = self.env["account.move"]
|
||||
self.journal_model = self.env["account.journal"]
|
||||
self.curr_year = datetime.date.today().year
|
||||
self.prev_year = self.curr_year - 1
|
||||
# create company
|
||||
self.company = self.res_company.create({"name": "AEP Company"})
|
||||
# create receivable bs account
|
||||
self.account_ar = self.account_model.create(
|
||||
{
|
||||
"company_id": self.company.id,
|
||||
"code": "400AR",
|
||||
"name": "Receivable",
|
||||
"account_type": "asset_receivable",
|
||||
"reconcile": True,
|
||||
}
|
||||
)
|
||||
# create income pl account
|
||||
self.account_in = self.account_model.create(
|
||||
{
|
||||
"company_id": self.company.id,
|
||||
"code": "700IN",
|
||||
"name": "Income",
|
||||
"account_type": "income",
|
||||
}
|
||||
)
|
||||
self.account_in_no_data = self.account_model.create(
|
||||
{
|
||||
"company_id": self.company.id,
|
||||
"code": "700INNODATA",
|
||||
"name": "Income (no data)",
|
||||
"account_type": "income",
|
||||
}
|
||||
)
|
||||
# create journal
|
||||
self.journal = self.journal_model.create(
|
||||
{
|
||||
"company_id": self.company.id,
|
||||
"name": "Sale journal",
|
||||
"code": "VEN",
|
||||
"type": "sale",
|
||||
}
|
||||
)
|
||||
# create move in December last year
|
||||
self._create_move(
|
||||
date=datetime.date(self.prev_year, 12, 1),
|
||||
amount=100,
|
||||
debit_acc=self.account_ar,
|
||||
credit_acc=self.account_in,
|
||||
)
|
||||
# create move in January this year
|
||||
self._create_move(
|
||||
date=datetime.date(self.curr_year, 1, 1),
|
||||
amount=300,
|
||||
debit_acc=self.account_ar,
|
||||
credit_acc=self.account_in,
|
||||
credit_quantity=3,
|
||||
)
|
||||
# create move in March this year
|
||||
self._create_move(
|
||||
date=datetime.date(self.curr_year, 3, 1),
|
||||
amount=500,
|
||||
debit_acc=self.account_ar,
|
||||
credit_acc=self.account_in,
|
||||
)
|
||||
# create the AEP, and prepare the expressions we'll need
|
||||
self.aep = AEP(self.company)
|
||||
self.aep.parse_expr("bali[]")
|
||||
self.aep.parse_expr("bale[]")
|
||||
self.aep.parse_expr("balp[]")
|
||||
self.aep.parse_expr("balu[]")
|
||||
self.aep.parse_expr("bali[700IN]")
|
||||
self.aep.parse_expr("bale[700IN]")
|
||||
self.aep.parse_expr("balp[700IN]")
|
||||
self.aep.parse_expr("balp[700NA]") # account that does not exist
|
||||
self.aep.parse_expr("bali[400AR]")
|
||||
self.aep.parse_expr("bale[400AR]")
|
||||
self.aep.parse_expr("balp[400AR]")
|
||||
self.aep.parse_expr("debp[400A%]")
|
||||
self.aep.parse_expr("crdp[700I%]")
|
||||
self.aep.parse_expr("bali[400%]")
|
||||
self.aep.parse_expr("bale[700%]")
|
||||
self.aep.parse_expr("balp[700I%]")
|
||||
self.aep.parse_expr("fldp.quantity[700%]")
|
||||
self.aep.parse_expr("balp[]" "[('account_id.code', '=', '400AR')]")
|
||||
self.aep.parse_expr(
|
||||
"balp[]" "[('account_id.account_type', '=', " " 'asset_receivable')]"
|
||||
)
|
||||
self.aep.parse_expr("balp[('account_type', '=', " " 'asset_receivable')]")
|
||||
self.aep.parse_expr(
|
||||
"balp['&', "
|
||||
" ('account_type', '=', "
|
||||
" 'asset_receivable'), "
|
||||
" ('code', '=', '400AR')]"
|
||||
)
|
||||
self.aep.parse_expr("bal_700IN") # deprecated
|
||||
self.aep.parse_expr("bals[700IN]") # deprecated
|
||||
|
||||
def _create_move(
|
||||
self, date, amount, debit_acc, credit_acc, post=True, credit_quantity=0
|
||||
):
|
||||
move = self.move_model.create(
|
||||
{
|
||||
"journal_id": self.journal.id,
|
||||
"date": fields.Date.to_string(date),
|
||||
"line_ids": [
|
||||
(
|
||||
0,
|
||||
0,
|
||||
{
|
||||
"name": "/",
|
||||
"debit": amount,
|
||||
"account_id": debit_acc.id,
|
||||
},
|
||||
),
|
||||
(
|
||||
0,
|
||||
0,
|
||||
{
|
||||
"name": "/",
|
||||
"credit": amount,
|
||||
"account_id": credit_acc.id,
|
||||
"quantity": credit_quantity,
|
||||
},
|
||||
),
|
||||
],
|
||||
}
|
||||
)
|
||||
if post:
|
||||
move._post()
|
||||
return move
|
||||
|
||||
def _do_queries(self, date_from, date_to):
|
||||
self.aep.do_queries(
|
||||
date_from=fields.Date.to_string(date_from),
|
||||
date_to=fields.Date.to_string(date_to),
|
||||
)
|
||||
|
||||
def _eval(self, expr):
|
||||
eval_dict = {"AccountingNone": AccountingNone}
|
||||
return safe_eval(self.aep.replace_expr(expr), eval_dict)
|
||||
|
||||
def _eval_by_account_id(self, expr):
|
||||
res = {}
|
||||
eval_dict = {"AccountingNone": AccountingNone}
|
||||
for account_id, replaced_exprs in self.aep.replace_exprs_by_account_id([expr]):
|
||||
res[account_id] = safe_eval(replaced_exprs[0], eval_dict)
|
||||
return res
|
||||
|
||||
def test_sanity_check(self):
|
||||
self.assertEqual(self.company.fiscalyear_last_day, 31)
|
||||
self.assertEqual(self.company.fiscalyear_last_month, "12")
|
||||
|
||||
def test_parse_expr_error_handling(self):
|
||||
aep = AEP(self.company)
|
||||
with self.assertRaises(UserError) as cm:
|
||||
aep.parse_expr("fldi.quantity[700%]")
|
||||
self.assertIn(
|
||||
"`fld` can only be used with mode `p` (variation)", str(cm.exception)
|
||||
)
|
||||
with self.assertRaises(UserError) as cm:
|
||||
aep.parse_expr("fldp[700%]")
|
||||
self.assertIn("`fld` must have a field name", str(cm.exception))
|
||||
with self.assertRaises(UserError) as cm:
|
||||
aep.parse_expr("balp.quantity[700%]")
|
||||
self.assertIn("`bal` cannot have a field name", str(cm.exception))
|
||||
|
||||
def test_aep_basic(self):
|
||||
self.aep.done_parsing()
|
||||
# let's query for december
|
||||
self._do_queries(
|
||||
datetime.date(self.prev_year, 12, 1), datetime.date(self.prev_year, 12, 31)
|
||||
)
|
||||
# initial balance must be None
|
||||
self.assertIs(self._eval("bali[400AR]"), AccountingNone)
|
||||
self.assertIs(self._eval("bali[700IN]"), AccountingNone)
|
||||
# check variation
|
||||
self.assertEqual(self._eval("balp[400AR]"), 100)
|
||||
self.assertEqual(self._eval("balp[][('account_id.code', '=', '400AR')]"), 100)
|
||||
self.assertEqual(
|
||||
self._eval(
|
||||
"balp[]" "[('account_id.account_type', '=', " " 'asset_receivable')]"
|
||||
),
|
||||
100,
|
||||
)
|
||||
self.assertEqual(
|
||||
self._eval("balp[('account_type', '=', " " 'asset_receivable')]"),
|
||||
100,
|
||||
)
|
||||
self.assertEqual(
|
||||
self._eval(
|
||||
"balp['&', "
|
||||
" ('account_type', '=', "
|
||||
" 'asset_receivable'), "
|
||||
" ('code', '=', '400AR')]"
|
||||
),
|
||||
100,
|
||||
)
|
||||
self.assertEqual(self._eval("balp[700IN]"), -100)
|
||||
# check ending balance
|
||||
self.assertEqual(self._eval("bale[400AR]"), 100)
|
||||
self.assertEqual(self._eval("bale[700IN]"), -100)
|
||||
|
||||
# let's query for January
|
||||
self._do_queries(
|
||||
datetime.date(self.curr_year, 1, 1), datetime.date(self.curr_year, 1, 31)
|
||||
)
|
||||
# initial balance is None for income account (it's not carried over)
|
||||
self.assertEqual(self._eval("bali[400AR]"), 100)
|
||||
self.assertIs(self._eval("bali[700IN]"), AccountingNone)
|
||||
# check variation
|
||||
self.assertEqual(self._eval("balp[400AR]"), 300)
|
||||
self.assertEqual(self._eval("balp[700IN]"), -300)
|
||||
# check ending balance
|
||||
self.assertEqual(self._eval("bale[400AR]"), 400)
|
||||
self.assertEqual(self._eval("bale[700IN]"), -300)
|
||||
# check result for non existing account
|
||||
self.assertIs(self._eval("bale[700NA]"), AccountingNone)
|
||||
# check fldp.quantity
|
||||
self.assertEqual(self._eval("fldp.quantity[700%]"), 3)
|
||||
|
||||
# let's query for March
|
||||
self._do_queries(
|
||||
datetime.date(self.curr_year, 3, 1), datetime.date(self.curr_year, 3, 31)
|
||||
)
|
||||
# initial balance is the ending balance fo January
|
||||
self.assertEqual(self._eval("bali[400AR]"), 400)
|
||||
self.assertEqual(self._eval("bali[700IN]"), -300)
|
||||
self.assertEqual(self._eval("pbali[400AR]"), 400)
|
||||
self.assertEqual(self._eval("nbali[400AR]"), 0)
|
||||
self.assertEqual(self._eval("nbali[700IN]"), -300)
|
||||
self.assertEqual(self._eval("pbali[700IN]"), 0)
|
||||
# check variation
|
||||
self.assertEqual(self._eval("balp[400AR]"), 500)
|
||||
self.assertEqual(self._eval("balp[700IN]"), -500)
|
||||
self.assertEqual(self._eval("nbalp[400AR]"), 0)
|
||||
self.assertEqual(self._eval("pbalp[400AR]"), 500)
|
||||
self.assertEqual(self._eval("nbalp[700IN]"), -500)
|
||||
self.assertEqual(self._eval("pbalp[700IN]"), 0)
|
||||
# check ending balance
|
||||
self.assertEqual(self._eval("bale[400AR]"), 900)
|
||||
self.assertEqual(self._eval("nbale[400AR]"), 0)
|
||||
self.assertEqual(self._eval("pbale[400AR]"), 900)
|
||||
self.assertEqual(self._eval("bale[700IN]"), -800)
|
||||
self.assertEqual(self._eval("nbale[700IN]"), -800)
|
||||
self.assertEqual(self._eval("pbale[700IN]"), 0)
|
||||
# check some variant expressions, for coverage
|
||||
self.assertEqual(self._eval("crdp[700I%]"), 500)
|
||||
self.assertEqual(self._eval("debp[400A%]"), 500)
|
||||
self.assertEqual(self._eval("bal_700IN"), -500)
|
||||
self.assertEqual(self._eval("bals[700IN]"), -800)
|
||||
# check fldp.quantity
|
||||
self.assertEqual(self._eval("fldp.quantity[700%]"), 0)
|
||||
|
||||
# unallocated p&l from previous year
|
||||
self.assertEqual(self._eval("balu[]"), -100)
|
||||
# TODO allocate profits, and then...
|
||||
|
||||
# let's query for December where there is no data
|
||||
self._do_queries(
|
||||
datetime.date(self.curr_year, 12, 1), datetime.date(self.curr_year, 12, 31)
|
||||
)
|
||||
self.assertIs(self._eval("balp[700IN]"), AccountingNone)
|
||||
|
||||
def test_aep_by_account(self):
|
||||
self.aep.done_parsing()
|
||||
self._do_queries(
|
||||
datetime.date(self.curr_year, 3, 1), datetime.date(self.curr_year, 3, 31)
|
||||
)
|
||||
variation = self._eval_by_account_id("balp[]")
|
||||
self.assertEqual(variation, {self.account_ar.id: 500, self.account_in.id: -500})
|
||||
variation = self._eval_by_account_id("pbalp[]")
|
||||
self.assertEqual(
|
||||
variation, {self.account_ar.id: 500, self.account_in.id: AccountingNone}
|
||||
)
|
||||
variation = self._eval_by_account_id("nbalp[]")
|
||||
self.assertEqual(
|
||||
variation, {self.account_ar.id: AccountingNone, self.account_in.id: -500}
|
||||
)
|
||||
variation = self._eval_by_account_id("balp[700IN]")
|
||||
self.assertEqual(variation, {self.account_in.id: -500})
|
||||
variation = self._eval_by_account_id("crdp[700IN] - debp[400AR]")
|
||||
self.assertEqual(variation, {self.account_ar.id: -500, self.account_in.id: 500})
|
||||
end = self._eval_by_account_id("bale[]")
|
||||
self.assertEqual(end, {self.account_ar.id: 900, self.account_in.id: -800})
|
||||
|
||||
def test_aep_by_account_no_data(self):
|
||||
"""Test that accounts with no data are not returned."""
|
||||
self.aep.done_parsing()
|
||||
self._do_queries(
|
||||
datetime.date(self.curr_year, 3, 1), datetime.date(self.curr_year, 3, 31)
|
||||
)
|
||||
variation = self._eval("balp[700I%]")
|
||||
self.assertEqual(variation, -500)
|
||||
variation_by_account = self._eval_by_account_id("balp[700I%]")
|
||||
self.assertEqual(variation_by_account, {self.account_in.id: -500})
|
||||
|
||||
def test_aep_convenience_methods(self):
|
||||
initial = AEP.get_balances_initial(self.company, time.strftime("%Y") + "-03-01")
|
||||
self.assertEqual(
|
||||
initial, {self.account_ar.id: (400, 0), self.account_in.id: (0, 300)}
|
||||
)
|
||||
variation = AEP.get_balances_variation(
|
||||
self.company,
|
||||
time.strftime("%Y") + "-03-01",
|
||||
time.strftime("%Y") + "-03-31",
|
||||
)
|
||||
self.assertEqual(
|
||||
variation, {self.account_ar.id: (500, 0), self.account_in.id: (0, 500)}
|
||||
)
|
||||
end = AEP.get_balances_end(self.company, time.strftime("%Y") + "-03-31")
|
||||
self.assertEqual(
|
||||
end, {self.account_ar.id: (900, 0), self.account_in.id: (0, 800)}
|
||||
)
|
||||
unallocated = AEP.get_unallocated_pl(
|
||||
self.company, time.strftime("%Y") + "-03-15"
|
||||
)
|
||||
self.assertEqual(unallocated, (0, 100))
|
||||
|
||||
def test_float_is_zero(self):
|
||||
dp = self.company.currency_id.decimal_places
|
||||
self.assertEqual(dp, 2)
|
||||
# make initial balance at Jan 1st equal to 0.01
|
||||
self._create_move(
|
||||
date=datetime.date(self.prev_year, 12, 1),
|
||||
amount=100.01,
|
||||
debit_acc=self.account_in,
|
||||
credit_acc=self.account_ar,
|
||||
)
|
||||
initial = AEP.get_balances_initial(self.company, time.strftime("%Y") + "-01-01")
|
||||
self.assertEqual(initial, {self.account_ar.id: (100.00, 100.01)})
|
||||
# make initial balance at Jan 1st equal to 0.001
|
||||
self._create_move(
|
||||
date=datetime.date(self.prev_year, 12, 1),
|
||||
amount=0.009,
|
||||
debit_acc=self.account_ar,
|
||||
credit_acc=self.account_in,
|
||||
)
|
||||
initial = AEP.get_balances_initial(self.company, time.strftime("%Y") + "-01-01")
|
||||
# epsilon initial balances is reported as empty
|
||||
self.assertEqual(initial, {})
|
||||
|
||||
def test_get_account_ids_for_expr(self):
|
||||
self.aep.done_parsing()
|
||||
expr = "balp[700IN]"
|
||||
account_ids = self.aep.get_account_ids_for_expr(expr)
|
||||
self.assertEqual(account_ids, {self.account_in.id})
|
||||
expr = "balp[700%]"
|
||||
account_ids = self.aep.get_account_ids_for_expr(expr)
|
||||
self.assertEqual(account_ids, {self.account_in.id, self.account_in_no_data.id})
|
||||
expr = "bali[400%], bale[700%]" # subkpis combined expression
|
||||
account_ids = self.aep.get_account_ids_for_expr(expr)
|
||||
self.assertEqual(
|
||||
account_ids,
|
||||
{self.account_in.id, self.account_ar.id, self.account_in_no_data.id},
|
||||
)
|
||||
|
||||
def test_get_aml_domain_for_expr(self):
|
||||
self.aep.done_parsing()
|
||||
expr = "balp[700IN]"
|
||||
domain = self.aep.get_aml_domain_for_expr(expr, "2017-01-01", "2017-03-31")
|
||||
self.assertEqual(
|
||||
domain,
|
||||
[
|
||||
("account_id", "in", (self.account_in.id,)),
|
||||
"&",
|
||||
("date", ">=", "2017-01-01"),
|
||||
("date", "<=", "2017-03-31"),
|
||||
],
|
||||
)
|
||||
expr = "debi[700IN] - crdi[400AR]"
|
||||
domain = self.aep.get_aml_domain_for_expr(expr, "2017-02-01", "2017-03-31")
|
||||
self.assertEqual(
|
||||
domain,
|
||||
[
|
||||
"|",
|
||||
# debi[700IN]
|
||||
"&",
|
||||
("account_id", "in", (self.account_in.id,)),
|
||||
("debit", "<>", 0.0),
|
||||
# crdi[400AR]
|
||||
"&",
|
||||
("account_id", "in", (self.account_ar.id,)),
|
||||
("credit", "<>", 0.0),
|
||||
"&",
|
||||
# for P&L accounts, only after fy start
|
||||
"|",
|
||||
("date", ">=", "2017-01-01"),
|
||||
("account_id.include_initial_balance", "=", True),
|
||||
# everything must be before from_date for initial balance
|
||||
("date", "<", "2017-02-01"),
|
||||
],
|
||||
)
|
||||
|
||||
def test_is_domain(self):
|
||||
self.assertTrue(_is_domain("('a', '=' 1)"))
|
||||
self.assertTrue(_is_domain("'&', ('a', '=' 1), ('b', '=', 1)"))
|
||||
self.assertTrue(_is_domain("'|', ('a', '=' 1), ('b', '=', 1)"))
|
||||
self.assertTrue(_is_domain("'!', ('a', '=' 1), ('b', '=', 1)"))
|
||||
self.assertTrue(_is_domain("\"&\", ('a', '=' 1), ('b', '=', 1)"))
|
||||
self.assertTrue(_is_domain("\"|\", ('a', '=' 1), ('b', '=', 1)"))
|
||||
self.assertTrue(_is_domain("\"!\", ('a', '=' 1), ('b', '=', 1)"))
|
||||
self.assertFalse(_is_domain("123%"))
|
||||
self.assertFalse(_is_domain("123%,456"))
|
||||
self.assertFalse(_is_domain(""))
|
||||
|
||||
def test_inactive_tax(self):
|
||||
expr = 'balp[][("tax_ids.name", "=", "test tax")]'
|
||||
self.aep.parse_expr(expr)
|
||||
self.aep.done_parsing()
|
||||
|
||||
tax = self.env["account.tax"].create(
|
||||
dict(name="test tax", active=True, amount=0, company_id=self.company.id)
|
||||
)
|
||||
move = self._create_move(
|
||||
date=datetime.date(self.prev_year, 12, 1),
|
||||
amount=100,
|
||||
debit_acc=self.account_ar,
|
||||
credit_acc=self.account_in,
|
||||
post=False,
|
||||
)
|
||||
for ml in move.line_ids:
|
||||
if ml.credit:
|
||||
ml.write(dict(tax_ids=[(6, 0, [tax.id])]))
|
||||
tax.active = False
|
||||
move._post()
|
||||
# let's query for december 1st
|
||||
self._do_queries(
|
||||
datetime.date(self.prev_year, 12, 1), datetime.date(self.prev_year, 12, 1)
|
||||
)
|
||||
# let's see if there was a match
|
||||
self.assertEqual(self._eval(expr), -100)
|
||||
|
||||
def test_invalid_field(self):
|
||||
expr = 'balp[][("invalid_field", "=", "...")]'
|
||||
self.aep.parse_expr(expr)
|
||||
self.aep.done_parsing()
|
||||
with self.assertRaises(UserError) as cm:
|
||||
self._do_queries(
|
||||
datetime.date(self.prev_year, 12, 1),
|
||||
datetime.date(self.prev_year, 12, 1),
|
||||
)
|
||||
assert "Error while querying move line source" in str(cm.exception)
|
||||
|
|
@ -1,7 +0,0 @@
|
|||
# Copyright 2014 ACSONE SA/NV (<http://acsone.eu>)
|
||||
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html).
|
||||
|
||||
from ..models import aggregate
|
||||
from .common import load_doctests
|
||||
|
||||
load_tests = load_doctests(aggregate)
|
||||
|
|
@ -1,227 +0,0 @@
|
|||
# Copyright 2016 ACSONE SA/NV (<http://acsone.eu>)
|
||||
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html).
|
||||
|
||||
import odoo.tests.common as common
|
||||
|
||||
from ..models.accounting_none import AccountingNone
|
||||
from ..models.mis_report import CMP_DIFF
|
||||
from ..models.mis_report_instance import (
|
||||
MODE_NONE,
|
||||
SRC_ACTUALS_ALT,
|
||||
SRC_CMPCOL,
|
||||
SRC_SUMCOL,
|
||||
)
|
||||
from .common import assert_matrix
|
||||
|
||||
|
||||
class TestMisReportInstanceDataSources(common.TransactionCase):
|
||||
"""Test sum and comparison data source."""
|
||||
|
||||
def _create_move(self, date, amount, debit_acc, credit_acc):
|
||||
move = self.move_model.create(
|
||||
{
|
||||
"journal_id": self.journal.id,
|
||||
"date": date,
|
||||
"line_ids": [
|
||||
(0, 0, {"name": "/", "debit": amount, "account_id": debit_acc.id}),
|
||||
(
|
||||
0,
|
||||
0,
|
||||
{"name": "/", "credit": amount, "account_id": credit_acc.id},
|
||||
),
|
||||
],
|
||||
}
|
||||
)
|
||||
move._post()
|
||||
return move
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
super().setUpClass()
|
||||
# Perform the tests with a brand new company to avoid intrusive data from other
|
||||
# modules added to the default company
|
||||
cls.company = cls.env["res.company"].create({"name": "Company Test"})
|
||||
cls.env.user.company_id = cls.company
|
||||
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
self.account_model = self.env["account.account"]
|
||||
self.move_model = self.env["account.move"]
|
||||
self.journal_model = self.env["account.journal"]
|
||||
# create receivable bs account
|
||||
self.account_ar = self.account_model.create(
|
||||
{
|
||||
"company_id": self.env.user.company_id.id,
|
||||
"code": "400AR",
|
||||
"name": "Receivable",
|
||||
"account_type": "asset_receivable",
|
||||
"reconcile": True,
|
||||
}
|
||||
)
|
||||
# create income account
|
||||
self.account_in = self.account_model.create(
|
||||
{
|
||||
"company_id": self.env.user.company_id.id,
|
||||
"code": "700IN",
|
||||
"name": "Income",
|
||||
"account_type": "income",
|
||||
}
|
||||
)
|
||||
self.account_in2 = self.account_model.create(
|
||||
{
|
||||
"company_id": self.env.user.company_id.id,
|
||||
"code": "700IN2",
|
||||
"name": "Income",
|
||||
"account_type": "income",
|
||||
}
|
||||
)
|
||||
# create journal
|
||||
self.journal = self.journal_model.create(
|
||||
{
|
||||
"company_id": self.env.user.company_id.id,
|
||||
"name": "Sale journal",
|
||||
"code": "VEN",
|
||||
"type": "sale",
|
||||
}
|
||||
)
|
||||
# create move
|
||||
self._create_move(
|
||||
date="2017-01-01",
|
||||
amount=11,
|
||||
debit_acc=self.account_ar,
|
||||
credit_acc=self.account_in,
|
||||
)
|
||||
# create move
|
||||
self._create_move(
|
||||
date="2017-02-01",
|
||||
amount=13,
|
||||
debit_acc=self.account_ar,
|
||||
credit_acc=self.account_in,
|
||||
)
|
||||
self._create_move(
|
||||
date="2017-02-01",
|
||||
amount=17,
|
||||
debit_acc=self.account_ar,
|
||||
credit_acc=self.account_in2,
|
||||
)
|
||||
# create report
|
||||
self.report = self.env["mis.report"].create(dict(name="test report"))
|
||||
self.kpi1 = self.env["mis.report.kpi"].create(
|
||||
dict(
|
||||
report_id=self.report.id,
|
||||
name="k1",
|
||||
description="kpi 1",
|
||||
expression="-balp[700IN]",
|
||||
compare_method=CMP_DIFF,
|
||||
)
|
||||
)
|
||||
self.expr1 = self.kpi1.expression_ids[0]
|
||||
self.kpi2 = self.env["mis.report.kpi"].create(
|
||||
dict(
|
||||
report_id=self.report.id,
|
||||
name="k2",
|
||||
description="kpi 2",
|
||||
expression="-balp[700%]",
|
||||
compare_method=CMP_DIFF,
|
||||
auto_expand_accounts=True,
|
||||
)
|
||||
)
|
||||
self.instance = self.env["mis.report.instance"].create(
|
||||
dict(name="test instance", report_id=self.report.id, comparison_mode=True)
|
||||
)
|
||||
self.p1 = self.env["mis.report.instance.period"].create(
|
||||
dict(
|
||||
name="p1",
|
||||
report_instance_id=self.instance.id,
|
||||
manual_date_from="2017-01-01",
|
||||
manual_date_to="2017-01-31",
|
||||
)
|
||||
)
|
||||
self.p2 = self.env["mis.report.instance.period"].create(
|
||||
dict(
|
||||
name="p2",
|
||||
report_instance_id=self.instance.id,
|
||||
manual_date_from="2017-02-01",
|
||||
manual_date_to="2017-02-28",
|
||||
)
|
||||
)
|
||||
|
||||
def test_sum(self):
|
||||
self.psum = self.env["mis.report.instance.period"].create(
|
||||
dict(
|
||||
name="psum",
|
||||
report_instance_id=self.instance.id,
|
||||
mode=MODE_NONE,
|
||||
source=SRC_SUMCOL,
|
||||
source_sumcol_ids=[
|
||||
(0, 0, dict(period_to_sum_id=self.p1.id, sign="+")),
|
||||
(0, 0, dict(period_to_sum_id=self.p2.id, sign="+")),
|
||||
],
|
||||
)
|
||||
)
|
||||
matrix = self.instance._compute_matrix()
|
||||
# None in last col because account details are not summed by default
|
||||
assert_matrix(
|
||||
matrix,
|
||||
[
|
||||
[11, 13, 24],
|
||||
[11, 30, 41],
|
||||
[11, 13, AccountingNone],
|
||||
[AccountingNone, 17, AccountingNone],
|
||||
],
|
||||
)
|
||||
|
||||
def test_sum_diff(self):
|
||||
self.psum = self.env["mis.report.instance.period"].create(
|
||||
dict(
|
||||
name="psum",
|
||||
report_instance_id=self.instance.id,
|
||||
mode=MODE_NONE,
|
||||
source=SRC_SUMCOL,
|
||||
source_sumcol_ids=[
|
||||
(0, 0, dict(period_to_sum_id=self.p1.id, sign="+")),
|
||||
(0, 0, dict(period_to_sum_id=self.p2.id, sign="-")),
|
||||
],
|
||||
source_sumcol_accdet=True,
|
||||
)
|
||||
)
|
||||
matrix = self.instance._compute_matrix()
|
||||
assert_matrix(
|
||||
matrix,
|
||||
[[11, 13, -2], [11, 30, -19], [11, 13, -2], [AccountingNone, 17, -17]],
|
||||
)
|
||||
|
||||
def test_cmp(self):
|
||||
self.pcmp = self.env["mis.report.instance.period"].create(
|
||||
dict(
|
||||
name="pcmp",
|
||||
report_instance_id=self.instance.id,
|
||||
mode=MODE_NONE,
|
||||
source=SRC_CMPCOL,
|
||||
source_cmpcol_from_id=self.p1.id,
|
||||
source_cmpcol_to_id=self.p2.id,
|
||||
)
|
||||
)
|
||||
matrix = self.instance._compute_matrix()
|
||||
assert_matrix(
|
||||
matrix, [[11, 13, 2], [11, 30, 19], [11, 13, 2], [AccountingNone, 17, 17]]
|
||||
)
|
||||
|
||||
def test_actuals(self):
|
||||
matrix = self.instance._compute_matrix()
|
||||
assert_matrix(matrix, [[11, 13], [11, 30], [11, 13], [AccountingNone, 17]])
|
||||
|
||||
def test_actuals_disable_auto_expand_accounts(self):
|
||||
self.instance.no_auto_expand_accounts = True
|
||||
matrix = self.instance._compute_matrix()
|
||||
assert_matrix(matrix, [[11, 13], [11, 30]])
|
||||
|
||||
def test_actuals_alt(self):
|
||||
aml_model = self.env["ir.model"].search([("name", "=", "account.move.line")])
|
||||
self.kpi2.auto_expand_accounts = False
|
||||
self.p1.source = SRC_ACTUALS_ALT
|
||||
self.p1.source_aml_model_id = aml_model.id
|
||||
self.p2.source = SRC_ACTUALS_ALT
|
||||
self.p1.source_aml_model_id = aml_model.id
|
||||
matrix = self.instance._compute_matrix()
|
||||
assert_matrix(matrix, [[11, 13], [11, 30]])
|
||||
|
|
@ -1,142 +0,0 @@
|
|||
# Copyright 2017 ACSONE SA/NV (<http://acsone.eu>)
|
||||
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html).
|
||||
|
||||
from odoo_test_helper import FakeModelLoader
|
||||
|
||||
from odoo.tests.common import TransactionCase
|
||||
|
||||
from ..models.mis_kpi_data import ACC_AVG, ACC_SUM
|
||||
|
||||
|
||||
class TestKpiData(TransactionCase):
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
super().setUpClass()
|
||||
|
||||
cls.loader = FakeModelLoader(cls.env, cls.__module__)
|
||||
cls.loader.backup_registry()
|
||||
from .fake_models import MisKpiDataTestItem
|
||||
|
||||
cls.loader.update_registry((MisKpiDataTestItem,))
|
||||
|
||||
report = cls.env["mis.report"].create(dict(name="test report"))
|
||||
cls.kpi1 = cls.env["mis.report.kpi"].create(
|
||||
dict(
|
||||
report_id=report.id,
|
||||
name="k1",
|
||||
description="kpi 1",
|
||||
expression="AccountingNone",
|
||||
)
|
||||
)
|
||||
cls.expr1 = cls.kpi1.expression_ids[0]
|
||||
cls.kpi2 = cls.env["mis.report.kpi"].create(
|
||||
dict(
|
||||
report_id=report.id,
|
||||
name="k2",
|
||||
description="kpi 2",
|
||||
expression="AccountingNone",
|
||||
)
|
||||
)
|
||||
cls.expr2 = cls.kpi2.expression_ids[0]
|
||||
cls.kd11 = cls.env["mis.kpi.data.test.item"].create(
|
||||
dict(
|
||||
kpi_expression_id=cls.expr1.id,
|
||||
date_from="2017-05-01",
|
||||
date_to="2017-05-10",
|
||||
amount=10,
|
||||
)
|
||||
)
|
||||
cls.kd12 = cls.env["mis.kpi.data.test.item"].create(
|
||||
dict(
|
||||
kpi_expression_id=cls.expr1.id,
|
||||
date_from="2017-05-11",
|
||||
date_to="2017-05-20",
|
||||
amount=20,
|
||||
)
|
||||
)
|
||||
cls.kd13 = cls.env["mis.kpi.data.test.item"].create(
|
||||
dict(
|
||||
kpi_expression_id=cls.expr1.id,
|
||||
date_from="2017-05-21",
|
||||
date_to="2017-05-25",
|
||||
amount=30,
|
||||
)
|
||||
)
|
||||
cls.kd21 = cls.env["mis.kpi.data.test.item"].create(
|
||||
dict(
|
||||
kpi_expression_id=cls.expr2.id,
|
||||
date_from="2017-06-01",
|
||||
date_to="2017-06-30",
|
||||
amount=3,
|
||||
)
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def tearDownClass(cls):
|
||||
cls.loader.restore_registry()
|
||||
return super().tearDownClass()
|
||||
|
||||
def test_kpi_data_name(self):
|
||||
self.assertEqual(self.kd11.name, "k1: 2017-05-01 - 2017-05-10")
|
||||
self.assertEqual(self.kd12.name, "k1: 2017-05-11 - 2017-05-20")
|
||||
|
||||
def test_kpi_data_sum(self):
|
||||
self.assertEqual(self.kpi1.accumulation_method, ACC_SUM)
|
||||
# one full
|
||||
r = self.env["mis.kpi.data.test.item"]._query_kpi_data(
|
||||
"2017-05-01", "2017-05-10", []
|
||||
)
|
||||
self.assertEqual(r, {self.expr1: 10})
|
||||
# one half
|
||||
r = self.env["mis.kpi.data.test.item"]._query_kpi_data(
|
||||
"2017-05-01", "2017-05-05", []
|
||||
)
|
||||
self.assertEqual(r, {self.expr1: 5})
|
||||
# two full
|
||||
r = self.env["mis.kpi.data.test.item"]._query_kpi_data(
|
||||
"2017-05-01", "2017-05-20", []
|
||||
)
|
||||
self.assertEqual(r, {self.expr1: 30})
|
||||
# two half
|
||||
r = self.env["mis.kpi.data.test.item"]._query_kpi_data(
|
||||
"2017-05-06", "2017-05-15", []
|
||||
)
|
||||
self.assertEqual(r, {self.expr1: 15})
|
||||
# more than covered range
|
||||
r = self.env["mis.kpi.data.test.item"]._query_kpi_data(
|
||||
"2017-01-01", "2017-05-31", []
|
||||
)
|
||||
self.assertEqual(r, {self.expr1: 60})
|
||||
# two kpis
|
||||
r = self.env["mis.kpi.data.test.item"]._query_kpi_data(
|
||||
"2017-05-21", "2017-06-30", []
|
||||
)
|
||||
self.assertEqual(r, {self.expr1: 30, self.expr2: 3})
|
||||
|
||||
def test_kpi_data_avg(self):
|
||||
self.kpi1.accumulation_method = ACC_AVG
|
||||
# one full
|
||||
r = self.env["mis.kpi.data.test.item"]._query_kpi_data(
|
||||
"2017-05-01", "2017-05-10", []
|
||||
)
|
||||
self.assertEqual(r, {self.expr1: 10})
|
||||
# one half
|
||||
r = self.env["mis.kpi.data.test.item"]._query_kpi_data(
|
||||
"2017-05-01", "2017-05-05", []
|
||||
)
|
||||
self.assertEqual(r, {self.expr1: 10})
|
||||
# two full
|
||||
r = self.env["mis.kpi.data.test.item"]._query_kpi_data(
|
||||
"2017-05-01", "2017-05-20", []
|
||||
)
|
||||
self.assertEqual(r, {self.expr1: (10 * 10 + 20 * 10) / 20})
|
||||
# two half
|
||||
r = self.env["mis.kpi.data.test.item"]._query_kpi_data(
|
||||
"2017-05-06", "2017-05-15", []
|
||||
)
|
||||
self.assertEqual(r, {self.expr1: (10 * 5 + 20 * 5) / 10})
|
||||
# more than covered range
|
||||
r = self.env["mis.kpi.data.test.item"]._query_kpi_data(
|
||||
"2017-01-01", "2017-05-31", []
|
||||
)
|
||||
self.assertEqual(r, {self.expr1: (10 * 10 + 20 * 10 + 30 * 5) / 25})
|
||||
|
|
@ -1,635 +0,0 @@
|
|||
# Copyright 2016 ACSONE SA/NV (<http://acsone.eu>)
|
||||
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html).
|
||||
|
||||
import odoo.tests.common as common
|
||||
from odoo.tools import test_reports
|
||||
|
||||
from ..models.accounting_none import AccountingNone
|
||||
from ..models.mis_report import TYPE_STR, SubKPITupleLengthError, SubKPIUnknownTypeError
|
||||
|
||||
|
||||
class TestMisReportInstance(common.HttpCase):
|
||||
"""Basic integration test to exercise mis.report.instance.
|
||||
|
||||
We don't check the actual results here too much as computation correctness
|
||||
should be covered by lower level unit tests.
|
||||
"""
|
||||
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
partner_model_id = self.env.ref("base.model_res_partner").id
|
||||
partner_create_date_field_id = self.env.ref(
|
||||
"base.field_res_partner__create_date"
|
||||
).id
|
||||
partner_debit_field_id = self.env.ref("account.field_res_partner__debit").id
|
||||
# create a report with 2 subkpis and one query
|
||||
self.report = self.env["mis.report"].create(
|
||||
dict(
|
||||
name="test report",
|
||||
subkpi_ids=[
|
||||
(0, 0, dict(name="sk1", description="subkpi 1", sequence=1)),
|
||||
(0, 0, dict(name="sk2", description="subkpi 2", sequence=2)),
|
||||
],
|
||||
query_ids=[
|
||||
(
|
||||
0,
|
||||
0,
|
||||
dict(
|
||||
name="partner",
|
||||
model_id=partner_model_id,
|
||||
field_ids=[(4, partner_debit_field_id, None)],
|
||||
date_field=partner_create_date_field_id,
|
||||
aggregate="sum",
|
||||
),
|
||||
)
|
||||
],
|
||||
)
|
||||
)
|
||||
# create another report with 2 subkpis, no query
|
||||
self.report_2 = self.env["mis.report"].create(
|
||||
dict(
|
||||
name="another test report",
|
||||
subkpi_ids=[
|
||||
(
|
||||
0,
|
||||
0,
|
||||
dict(
|
||||
name="subkpi1_report2",
|
||||
description="subkpi 1, report 2",
|
||||
sequence=1,
|
||||
),
|
||||
),
|
||||
(
|
||||
0,
|
||||
0,
|
||||
dict(
|
||||
name="subkpi2_report2",
|
||||
description="subkpi 2, report 2",
|
||||
sequence=2,
|
||||
),
|
||||
),
|
||||
],
|
||||
)
|
||||
)
|
||||
# Third report, 2 subkpis, no query
|
||||
self.report_3 = self.env["mis.report"].create(
|
||||
dict(
|
||||
name="test report 3",
|
||||
subkpi_ids=[
|
||||
(
|
||||
0,
|
||||
0,
|
||||
dict(
|
||||
name="subkpi1_report3",
|
||||
description="subkpi 1, report 3",
|
||||
sequence=1,
|
||||
),
|
||||
),
|
||||
(
|
||||
0,
|
||||
0,
|
||||
dict(
|
||||
name="subkpi2_report3",
|
||||
description="subkpi 2, report 3",
|
||||
sequence=2,
|
||||
),
|
||||
),
|
||||
],
|
||||
)
|
||||
)
|
||||
# kpi with accounting formulas
|
||||
self.kpi1 = self.env["mis.report.kpi"].create(
|
||||
dict(
|
||||
report_id=self.report.id,
|
||||
description="kpi 1",
|
||||
name="k1",
|
||||
multi=True,
|
||||
expression_ids=[
|
||||
(
|
||||
0,
|
||||
0,
|
||||
dict(name="bale[200%]", subkpi_id=self.report.subkpi_ids[0].id),
|
||||
),
|
||||
(
|
||||
0,
|
||||
0,
|
||||
dict(name="balp[200%]", subkpi_id=self.report.subkpi_ids[1].id),
|
||||
),
|
||||
],
|
||||
)
|
||||
)
|
||||
# kpi with accounting formula and query
|
||||
self.kpi2 = self.env["mis.report.kpi"].create(
|
||||
dict(
|
||||
report_id=self.report.id,
|
||||
description="kpi 2",
|
||||
name="k2",
|
||||
multi=True,
|
||||
expression_ids=[
|
||||
(
|
||||
0,
|
||||
0,
|
||||
dict(name="balp[200%]", subkpi_id=self.report.subkpi_ids[0].id),
|
||||
),
|
||||
(
|
||||
0,
|
||||
0,
|
||||
dict(
|
||||
name="partner.debit", subkpi_id=self.report.subkpi_ids[1].id
|
||||
),
|
||||
),
|
||||
],
|
||||
)
|
||||
)
|
||||
# kpi with a simple expression summing other multi-valued kpis
|
||||
self.env["mis.report.kpi"].create(
|
||||
dict(
|
||||
report_id=self.report.id,
|
||||
description="kpi 4",
|
||||
name="k4",
|
||||
multi=False,
|
||||
expression="k1 + k2 + k3",
|
||||
)
|
||||
)
|
||||
# kpi with 2 constants
|
||||
self.env["mis.report.kpi"].create(
|
||||
dict(
|
||||
report_id=self.report.id,
|
||||
description="kpi 3",
|
||||
name="k3",
|
||||
multi=True,
|
||||
expression_ids=[
|
||||
(
|
||||
0,
|
||||
0,
|
||||
dict(
|
||||
name="AccountingNone",
|
||||
subkpi_id=self.report.subkpi_ids[0].id,
|
||||
),
|
||||
),
|
||||
(0, 0, dict(name="1.0", subkpi_id=self.report.subkpi_ids[1].id)),
|
||||
],
|
||||
)
|
||||
)
|
||||
# kpi with a NameError (x not defined)
|
||||
self.env["mis.report.kpi"].create(
|
||||
dict(
|
||||
report_id=self.report.id,
|
||||
description="kpi 5",
|
||||
name="k5",
|
||||
multi=True,
|
||||
expression_ids=[
|
||||
(0, 0, dict(name="x", subkpi_id=self.report.subkpi_ids[0].id)),
|
||||
(0, 0, dict(name="1.0", subkpi_id=self.report.subkpi_ids[1].id)),
|
||||
],
|
||||
)
|
||||
)
|
||||
# string-type kpi
|
||||
self.env["mis.report.kpi"].create(
|
||||
dict(
|
||||
report_id=self.report.id,
|
||||
description="kpi 6",
|
||||
name="k6",
|
||||
multi=True,
|
||||
type=TYPE_STR,
|
||||
expression_ids=[
|
||||
(0, 0, dict(name='"bla"', subkpi_id=self.report.subkpi_ids[0].id)),
|
||||
(
|
||||
0,
|
||||
0,
|
||||
dict(name='"blabla"', subkpi_id=self.report.subkpi_ids[1].id),
|
||||
),
|
||||
],
|
||||
)
|
||||
)
|
||||
# kpi that references another subkpi by name
|
||||
self.env["mis.report.kpi"].create(
|
||||
dict(
|
||||
report_id=self.report.id,
|
||||
description="kpi 7",
|
||||
name="k7",
|
||||
multi=True,
|
||||
expression_ids=[
|
||||
(0, 0, dict(name="k3.sk1", subkpi_id=self.report.subkpi_ids[0].id)),
|
||||
(0, 0, dict(name="k3.sk2", subkpi_id=self.report.subkpi_ids[1].id)),
|
||||
],
|
||||
)
|
||||
)
|
||||
# Report 2 : kpi with AccountingNone value
|
||||
self.env["mis.report.kpi"].create(
|
||||
dict(
|
||||
report_id=self.report_2.id,
|
||||
description="AccountingNone kpi",
|
||||
name="AccountingNoneKPI",
|
||||
multi=False,
|
||||
)
|
||||
)
|
||||
# Report 2 : 'classic' kpi with values for each sub-KPI
|
||||
self.env["mis.report.kpi"].create(
|
||||
dict(
|
||||
report_id=self.report_2.id,
|
||||
description="Classic kpi",
|
||||
name="classic_kpi_r2",
|
||||
multi=True,
|
||||
expression_ids=[
|
||||
(
|
||||
0,
|
||||
0,
|
||||
dict(
|
||||
name="bale[200%]", subkpi_id=self.report_2.subkpi_ids[0].id
|
||||
),
|
||||
),
|
||||
(
|
||||
0,
|
||||
0,
|
||||
dict(
|
||||
name="balp[200%]", subkpi_id=self.report_2.subkpi_ids[1].id
|
||||
),
|
||||
),
|
||||
],
|
||||
)
|
||||
)
|
||||
# Report 3 : kpi with wrong tuple length
|
||||
self.env["mis.report.kpi"].create(
|
||||
dict(
|
||||
report_id=self.report_3.id,
|
||||
description="Wrong tuple length kpi",
|
||||
name="wrongTupleLen",
|
||||
multi=False,
|
||||
expression="('hello', 'does', 'this', 'work')",
|
||||
)
|
||||
)
|
||||
# Report 3 : 'classic' kpi
|
||||
self.env["mis.report.kpi"].create(
|
||||
dict(
|
||||
report_id=self.report_3.id,
|
||||
description="Classic kpi",
|
||||
name="classic_kpi_r2",
|
||||
multi=True,
|
||||
expression_ids=[
|
||||
(
|
||||
0,
|
||||
0,
|
||||
dict(
|
||||
name="bale[200%]", subkpi_id=self.report_3.subkpi_ids[0].id
|
||||
),
|
||||
),
|
||||
(
|
||||
0,
|
||||
0,
|
||||
dict(
|
||||
name="balp[200%]", subkpi_id=self.report_3.subkpi_ids[1].id
|
||||
),
|
||||
),
|
||||
],
|
||||
)
|
||||
)
|
||||
# create a report instance
|
||||
self.report_instance = self.env["mis.report.instance"].create(
|
||||
dict(
|
||||
name="test instance",
|
||||
report_id=self.report.id,
|
||||
company_id=self.env.ref("base.main_company").id,
|
||||
period_ids=[
|
||||
(
|
||||
0,
|
||||
0,
|
||||
dict(
|
||||
name="p1",
|
||||
mode="relative",
|
||||
type="d",
|
||||
subkpi_ids=[(4, self.report.subkpi_ids[0].id, None)],
|
||||
),
|
||||
),
|
||||
(
|
||||
0,
|
||||
0,
|
||||
dict(
|
||||
name="p2",
|
||||
mode="fix",
|
||||
manual_date_from="2014-01-01",
|
||||
manual_date_to="2014-12-31",
|
||||
),
|
||||
),
|
||||
],
|
||||
)
|
||||
)
|
||||
# same for report 2
|
||||
self.report_instance_2 = self.env["mis.report.instance"].create(
|
||||
dict(
|
||||
name="test instance 2",
|
||||
report_id=self.report_2.id,
|
||||
company_id=self.env.ref("base.main_company").id,
|
||||
period_ids=[
|
||||
(
|
||||
0,
|
||||
0,
|
||||
dict(
|
||||
name="p3",
|
||||
mode="fix",
|
||||
manual_date_from="2019-01-01",
|
||||
manual_date_to="2019-12-31",
|
||||
),
|
||||
)
|
||||
],
|
||||
)
|
||||
)
|
||||
# and for report 3
|
||||
self.report_instance_3 = self.env["mis.report.instance"].create(
|
||||
dict(
|
||||
name="test instance 3",
|
||||
report_id=self.report_3.id,
|
||||
company_id=self.env.ref("base.main_company").id,
|
||||
period_ids=[
|
||||
(
|
||||
0,
|
||||
0,
|
||||
dict(
|
||||
name="p4",
|
||||
mode="fix",
|
||||
manual_date_from="2019-01-01",
|
||||
manual_date_to="2019-12-31",
|
||||
),
|
||||
)
|
||||
],
|
||||
)
|
||||
)
|
||||
|
||||
def test_compute(self):
|
||||
matrix = self.report_instance._compute_matrix()
|
||||
for row in matrix.iter_rows():
|
||||
vals = [c.val for c in row.iter_cells()]
|
||||
if row.kpi.name == "k3":
|
||||
# k3 is constant
|
||||
self.assertEqual(vals, [AccountingNone, AccountingNone, 1.0])
|
||||
elif row.kpi.name == "k6":
|
||||
# k6 is a string kpi
|
||||
self.assertEqual(vals, ["bla", "bla", "blabla"])
|
||||
elif row.kpi.name == "k7":
|
||||
# k7 references k3 via subkpi names
|
||||
self.assertEqual(vals, [AccountingNone, AccountingNone, 1.0])
|
||||
|
||||
def test_multi_company_compute(self):
|
||||
self.report_instance.write(
|
||||
{
|
||||
"multi_company": True,
|
||||
"company_ids": [(6, 0, self.report_instance.company_id.ids)],
|
||||
}
|
||||
)
|
||||
self.report_instance.report_id.kpi_ids.write({"auto_expand_accounts": True})
|
||||
matrix = self.report_instance._compute_matrix()
|
||||
for row in matrix.iter_rows():
|
||||
if row.account_id:
|
||||
account = self.env["account.account"].browse(row.account_id)
|
||||
self.assertEqual(
|
||||
row.label,
|
||||
f"{account.code} {account.name} [{account.company_id.name}]",
|
||||
)
|
||||
self.report_instance.write({"multi_company": False})
|
||||
matrix = self.report_instance._compute_matrix()
|
||||
for row in matrix.iter_rows():
|
||||
if row.account_id:
|
||||
account = self.env["account.account"].browse(row.account_id)
|
||||
self.assertEqual(row.label, f"{account.code} {account.name}")
|
||||
|
||||
def test_evaluate(self):
|
||||
company = self.env.ref("base.main_company")
|
||||
aep = self.report._prepare_aep(company)
|
||||
r = self.report.evaluate(aep, date_from="2014-01-01", date_to="2014-12-31")
|
||||
self.assertEqual(r["k3"], (AccountingNone, 1.0))
|
||||
self.assertEqual(r["k6"], ("bla", "blabla"))
|
||||
self.assertEqual(r["k7"], (AccountingNone, 1.0))
|
||||
|
||||
def test_json(self):
|
||||
self.report_instance.compute()
|
||||
|
||||
def test_drilldown(self):
|
||||
action = self.report_instance.drilldown(
|
||||
dict(expr="balp[200%]", period_id=self.report_instance.period_ids[0].id)
|
||||
)
|
||||
account_ids = (
|
||||
self.env["account.account"]
|
||||
.search(
|
||||
[
|
||||
("code", "=like", "200%"),
|
||||
("company_id", "=", self.env.ref("base.main_company").id),
|
||||
]
|
||||
)
|
||||
.ids
|
||||
)
|
||||
self.assertTrue(("account_id", "in", tuple(account_ids)) in action["domain"])
|
||||
self.assertEqual(action["res_model"], "account.move.line")
|
||||
|
||||
def test_drilldown_action_name_with_account(self):
|
||||
period = self.report_instance.period_ids[0]
|
||||
account = self.env["account.account"].search([], limit=1)
|
||||
args = {
|
||||
"period_id": period.id,
|
||||
"kpi_id": self.kpi1.id,
|
||||
"account_id": account.id,
|
||||
}
|
||||
action_name = self.report_instance._get_drilldown_action_name(args)
|
||||
expected_name = "{kpi} - {account} - {period}".format(
|
||||
kpi=self.kpi1.description,
|
||||
account=account.display_name,
|
||||
period=period.display_name,
|
||||
)
|
||||
assert action_name == expected_name
|
||||
|
||||
def test_drilldown_action_name_without_account(self):
|
||||
period = self.report_instance.period_ids[0]
|
||||
args = {
|
||||
"period_id": period.id,
|
||||
"kpi_id": self.kpi1.id,
|
||||
}
|
||||
action_name = self.report_instance._get_drilldown_action_name(args)
|
||||
expected_name = f"{self.kpi1.description} - {period.display_name}"
|
||||
assert action_name == expected_name
|
||||
|
||||
def test_drilldown_views(self):
|
||||
IrUiView = self.env["ir.ui.view"]
|
||||
model_name = "account.move.line"
|
||||
IrUiView.search([("model", "=", model_name)]).unlink()
|
||||
IrUiView.create(
|
||||
[
|
||||
{
|
||||
"name": "mis_report_test_drilldown_views_chart",
|
||||
"model": model_name,
|
||||
"arch": "<graph><field name='name'/></graph>",
|
||||
},
|
||||
{
|
||||
"name": "mis_report_test_drilldown_views_tree",
|
||||
"model": model_name,
|
||||
"arch": "<pivot><field name='name'/></pivot>",
|
||||
},
|
||||
]
|
||||
)
|
||||
action = self.report_instance.drilldown(
|
||||
dict(expr="balp[200%]", period_id=self.report_instance.period_ids[0].id)
|
||||
)
|
||||
self.assertEqual(action["view_mode"], "pivot,graph")
|
||||
self.assertEqual(action["views"], [[False, "pivot"], [False, "graph"]])
|
||||
IrUiView.create(
|
||||
[
|
||||
{
|
||||
"name": "mis_report_test_drilldown_views_form",
|
||||
"model": model_name,
|
||||
"arch": "<form><field name='name'/></form>",
|
||||
},
|
||||
{
|
||||
"name": "mis_report_test_drilldown_views_tree",
|
||||
"model": model_name,
|
||||
"arch": "<tree><field name='name'/></tree>",
|
||||
},
|
||||
]
|
||||
)
|
||||
action = self.report_instance.drilldown(
|
||||
dict(expr="balp[200%]", period_id=self.report_instance.period_ids[0].id)
|
||||
)
|
||||
self.assertEqual(action["view_mode"], "tree,form,pivot,graph")
|
||||
self.assertEqual(
|
||||
action["views"],
|
||||
[[False, "tree"], [False, "form"], [False, "pivot"], [False, "graph"]],
|
||||
)
|
||||
|
||||
def test_qweb(self):
|
||||
self.report_instance.print_pdf() # get action
|
||||
test_reports.try_report(
|
||||
self.env.cr,
|
||||
self.env.uid,
|
||||
"mis_builder.report_mis_report_instance",
|
||||
[self.report_instance.id],
|
||||
report_type="qweb-pdf",
|
||||
)
|
||||
|
||||
def test_xlsx(self):
|
||||
self.report_instance.export_xls() # get action
|
||||
test_reports.try_report(
|
||||
self.env.cr,
|
||||
self.env.uid,
|
||||
"mis_builder.mis_report_instance_xlsx",
|
||||
[self.report_instance.id],
|
||||
report_type="xlsx",
|
||||
)
|
||||
|
||||
def test_get_kpis_by_account_id(self):
|
||||
account_ids = (
|
||||
self.env["account.account"]
|
||||
.search(
|
||||
[
|
||||
("code", "=like", "200%"),
|
||||
("company_id", "=", self.env.ref("base.main_company").id),
|
||||
]
|
||||
)
|
||||
.ids
|
||||
)
|
||||
kpi200 = {self.kpi1, self.kpi2}
|
||||
res = self.report.get_kpis_by_account_id(self.env.ref("base.main_company"))
|
||||
for account_id in account_ids:
|
||||
self.assertTrue(account_id in res)
|
||||
self.assertEqual(res[account_id], kpi200)
|
||||
|
||||
def test_kpi_name_get_name_search(self):
|
||||
r = self.env["mis.report.kpi"].name_search("k1")
|
||||
self.assertEqual(len(r), 1)
|
||||
self.assertEqual(r[0][0], self.kpi1.id)
|
||||
self.assertEqual(r[0][1], "kpi 1 (k1)")
|
||||
r = self.env["mis.report.kpi"].name_search("kpi 1")
|
||||
self.assertEqual(len(r), 1)
|
||||
self.assertEqual(r[0][0], self.kpi1.id)
|
||||
self.assertEqual(r[0][1], "kpi 1 (k1)")
|
||||
|
||||
def test_kpi_expr_name_get_name_search(self):
|
||||
r = self.env["mis.report.kpi.expression"].name_search("k1")
|
||||
self.assertEqual(
|
||||
[i[1] for i in r],
|
||||
["kpi 1 / subkpi 1 (k1.sk1)", "kpi 1 / subkpi 2 (k1.sk2)"],
|
||||
)
|
||||
r = self.env["mis.report.kpi.expression"].name_search("k1.sk1")
|
||||
self.assertEqual([i[1] for i in r], ["kpi 1 / subkpi 1 (k1.sk1)"])
|
||||
r = self.env["mis.report.kpi.expression"].name_search("k4")
|
||||
self.assertEqual([i[1] for i in r], ["kpi 4 (k4)"])
|
||||
|
||||
def test_query_company_ids(self):
|
||||
# sanity check single company mode
|
||||
assert not self.report_instance.multi_company
|
||||
assert self.report_instance.company_id
|
||||
assert self.report_instance.query_company_ids == self.report_instance.company_id
|
||||
# create a second company
|
||||
c1 = self.report_instance.company_id
|
||||
c2 = self.env["res.company"].create(
|
||||
dict(
|
||||
name="company 2",
|
||||
)
|
||||
)
|
||||
self.report_instance.write(dict(multi_company=True, company_id=False))
|
||||
self.report_instance.company_ids |= c1
|
||||
self.report_instance.company_ids |= c2
|
||||
assert len(self.report_instance.company_ids) == 2
|
||||
self.assertFalse(self.report_instance.query_company_ids - self.env.companies)
|
||||
# In a user context where there is only one company, ensure
|
||||
# query_company_ids only has one company too.
|
||||
assert (
|
||||
self.report_instance.with_context(
|
||||
allowed_company_ids=(c1.id,)
|
||||
).query_company_ids
|
||||
== c1
|
||||
)
|
||||
|
||||
def test_multi_company_onchange(self):
|
||||
# not multi company
|
||||
self.assertTrue(self.report_instance.company_id)
|
||||
self.assertFalse(self.report_instance.multi_company)
|
||||
self.assertFalse(self.report_instance.company_ids)
|
||||
self.assertEqual(
|
||||
self.report_instance.query_company_ids[0], self.report_instance.company_id
|
||||
)
|
||||
# create a child company
|
||||
self.env["res.company"].create(
|
||||
dict(name="company 2", parent_id=self.report_instance.company_id.id)
|
||||
)
|
||||
self.report_instance.multi_company = True
|
||||
# multi company, company_ids not set
|
||||
self.assertEqual(self.report_instance.query_company_ids, self.env.companies)
|
||||
# set company_ids
|
||||
previous_company = self.report_instance.company_id
|
||||
self.report_instance._onchange_company()
|
||||
self.assertFalse(self.report_instance.company_id)
|
||||
self.assertTrue(self.report_instance.multi_company)
|
||||
self.assertEqual(self.report_instance.company_ids, previous_company)
|
||||
self.assertEqual(self.report_instance.query_company_ids, previous_company)
|
||||
# reset single company mode
|
||||
self.report_instance.multi_company = False
|
||||
self.report_instance._onchange_company()
|
||||
self.assertEqual(
|
||||
self.report_instance.query_company_ids[0], self.report_instance.company_id
|
||||
)
|
||||
self.assertFalse(self.report_instance.company_ids)
|
||||
|
||||
def test_mis_report_analytic_filters(self):
|
||||
# Check that matrix has no values when using a filter with a non existing value
|
||||
matrix = self.report_instance.with_context(
|
||||
analytic_domain=[("partner_id", "=", -1)]
|
||||
)._compute_matrix()
|
||||
for row in matrix.iter_rows():
|
||||
vals = [c.val for c in row.iter_cells()]
|
||||
if row.kpi.name == "k1":
|
||||
self.assertEqual(vals, [AccountingNone, AccountingNone, AccountingNone])
|
||||
elif row.kpi.name == "k2":
|
||||
self.assertEqual(vals, [AccountingNone, AccountingNone, None])
|
||||
elif row.kpi.name == "k4":
|
||||
self.assertEqual(vals, [AccountingNone, AccountingNone, 1.0])
|
||||
|
||||
def test_raise_when_unknown_kpi_value_type(self):
|
||||
with self.assertRaises(SubKPIUnknownTypeError):
|
||||
self.report_instance_2.compute()
|
||||
|
||||
def test_raise_when_wrong_tuple_length_with_subkpis(self):
|
||||
with self.assertRaises(SubKPITupleLengthError):
|
||||
self.report_instance_3.compute()
|
||||
|
||||
def test_unprivileged(self):
|
||||
test_user = common.new_test_user(
|
||||
self.env, "mis_you", groups="base.group_user,account.group_account_readonly"
|
||||
)
|
||||
self.report_instance.with_user(test_user).compute()
|
||||
|
|
@ -1,154 +0,0 @@
|
|||
# Copyright 2025 ACSONE SA/NV
|
||||
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
|
||||
|
||||
from odoo import Command
|
||||
from odoo.tests.common import TransactionCase
|
||||
|
||||
|
||||
class TestMisReportInstanceAnnotation(TransactionCase):
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
self.report = self.env["mis.report"].create(
|
||||
dict(
|
||||
name="test report",
|
||||
subkpi_ids=[
|
||||
Command.create(
|
||||
dict(
|
||||
name="subkpi1_report2",
|
||||
description="subkpi 1, report 2",
|
||||
sequence=1,
|
||||
)
|
||||
),
|
||||
Command.create(
|
||||
dict(
|
||||
name="subkpi2_report2",
|
||||
description="subkpi 2, report 2",
|
||||
sequence=2,
|
||||
),
|
||||
),
|
||||
],
|
||||
)
|
||||
)
|
||||
|
||||
self.kpi = self.env["mis.report.kpi"].create(
|
||||
dict(
|
||||
report_id=self.report.id,
|
||||
description="kpi 1",
|
||||
name="k1",
|
||||
multi=True,
|
||||
expression_ids=[
|
||||
Command.create(
|
||||
dict(name="bale[200%]", subkpi_id=self.report.subkpi_ids[0].id),
|
||||
),
|
||||
Command.create(
|
||||
dict(name="balp[200%]", subkpi_id=self.report.subkpi_ids[1].id),
|
||||
),
|
||||
],
|
||||
)
|
||||
)
|
||||
|
||||
self.report_instance = self.env["mis.report.instance"].create(
|
||||
dict(
|
||||
name="test instance",
|
||||
report_id=self.report.id,
|
||||
company_id=self.env.ref("base.main_company").id,
|
||||
period_ids=[
|
||||
Command.create(
|
||||
dict(
|
||||
name="p1",
|
||||
mode="fix",
|
||||
manual_date_from="2013-01-01",
|
||||
manual_date_to="2013-12-31",
|
||||
sequence=1,
|
||||
),
|
||||
),
|
||||
Command.create(
|
||||
dict(
|
||||
name="p2",
|
||||
mode="fix",
|
||||
manual_date_from="2014-01-01",
|
||||
manual_date_to="2014-12-31",
|
||||
sequence=2,
|
||||
),
|
||||
),
|
||||
],
|
||||
)
|
||||
)
|
||||
|
||||
def test_adding_note(self):
|
||||
notes = self.report_instance.get_notes_by_cell_id()
|
||||
|
||||
self.assertEqual({}, notes)
|
||||
|
||||
# report with 4 cells, 2 periods and 2 subkpis
|
||||
matrix = self.report_instance._compute_matrix()
|
||||
cell_ids = [c.cell_id for row in matrix.iter_rows() for c in row.iter_cells()]
|
||||
self.assertEqual(len(cell_ids), 4)
|
||||
|
||||
first_cell_id, second_cell_id, third_cell_id, _fourth_cell_id = cell_ids
|
||||
|
||||
# adding one note
|
||||
self.env["mis.report.instance.annotation"].set_annotation(
|
||||
first_cell_id, self.report_instance.id, "This is a note"
|
||||
)
|
||||
notes = self.report_instance.get_notes_by_cell_id()
|
||||
self.assertDictEqual(
|
||||
{first_cell_id: {"text": "This is a note", "sequence": 1}}, notes
|
||||
)
|
||||
|
||||
# adding another note
|
||||
self.env["mis.report.instance.annotation"].set_annotation(
|
||||
third_cell_id, self.report_instance.id, "This is another note"
|
||||
)
|
||||
notes = self.report_instance.get_notes_by_cell_id()
|
||||
self.assertDictEqual(
|
||||
{
|
||||
first_cell_id: {"text": "This is a note", "sequence": 1},
|
||||
third_cell_id: {"text": "This is another note", "sequence": 2},
|
||||
},
|
||||
notes,
|
||||
)
|
||||
|
||||
self.env["mis.report.instance.annotation"].set_annotation(
|
||||
second_cell_id, self.report_instance.id, "This is third note"
|
||||
)
|
||||
|
||||
notes = self.report_instance.get_notes_by_cell_id()
|
||||
# Last note added should have a sequence of
|
||||
# 2 since it is deplayed in the second cell
|
||||
self.assertDictEqual(
|
||||
{
|
||||
first_cell_id: {"text": "This is a note", "sequence": 1},
|
||||
second_cell_id: {"text": "This is third note", "sequence": 2},
|
||||
third_cell_id: {"text": "This is another note", "sequence": 3},
|
||||
},
|
||||
notes,
|
||||
)
|
||||
|
||||
def test_remove_note(self):
|
||||
notes = self.report_instance.get_notes_by_cell_id()
|
||||
|
||||
self.assertEqual({}, notes)
|
||||
|
||||
# report with 4 cells, 2 periods and 2 subkpis
|
||||
matrix = self.report_instance._compute_matrix()
|
||||
cell_ids = [c.cell_id for row in matrix.iter_rows() for c in row.iter_cells()]
|
||||
self.assertEqual(len(cell_ids), 4)
|
||||
|
||||
first_cell_id = cell_ids[0]
|
||||
|
||||
# adding one note
|
||||
self.env["mis.report.instance.annotation"].set_annotation(
|
||||
first_cell_id, self.report_instance.id, "This is a note"
|
||||
)
|
||||
notes = self.report_instance.get_notes_by_cell_id()
|
||||
self.assertDictEqual(
|
||||
{first_cell_id: {"text": "This is a note", "sequence": 1}}, notes
|
||||
)
|
||||
|
||||
# remove note
|
||||
self.env["mis.report.instance.annotation"].remove_annotation(
|
||||
first_cell_id, self.report_instance.id
|
||||
)
|
||||
notes = self.report_instance.get_notes_by_cell_id()
|
||||
self.assertEqual({}, notes)
|
||||
|
|
@ -1,25 +0,0 @@
|
|||
# Copyright 2016 ACSONE SA/NV (<http://acsone.eu>)
|
||||
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html).
|
||||
|
||||
import odoo.tests.common as common
|
||||
|
||||
from ..models.mis_safe_eval import DataError, NameDataError, mis_safe_eval
|
||||
|
||||
|
||||
class TestMisSafeEval(common.TransactionCase):
|
||||
def test_nominal(self):
|
||||
val = mis_safe_eval("a + 1", {"a": 1})
|
||||
self.assertEqual(val, 2)
|
||||
|
||||
def test_exceptions(self):
|
||||
val = mis_safe_eval("1/0", {}) # division by zero
|
||||
self.assertTrue(isinstance(val, DataError))
|
||||
self.assertEqual(val.name, "#DIV/0")
|
||||
val = mis_safe_eval("1a", {}) # syntax error
|
||||
self.assertTrue(isinstance(val, DataError))
|
||||
self.assertEqual(val.name, "#ERR")
|
||||
|
||||
def test_name_error(self):
|
||||
val = mis_safe_eval("a + 1", {})
|
||||
self.assertTrue(isinstance(val, NameDataError))
|
||||
self.assertEqual(val.name, "#NAME")
|
||||
|
|
@ -1,208 +0,0 @@
|
|||
# Copyright 2014 ACSONE SA/NV (<http://acsone.eu>)
|
||||
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html).
|
||||
|
||||
import datetime
|
||||
|
||||
import odoo.tests.common as common
|
||||
from odoo import fields
|
||||
from odoo.tools.safe_eval import safe_eval
|
||||
|
||||
from ..models.accounting_none import AccountingNone
|
||||
from ..models.aep import AccountingExpressionProcessor as AEP
|
||||
|
||||
|
||||
class TestMultiCompanyAEP(common.TransactionCase):
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
self.res_company = self.env["res.company"]
|
||||
self.account_model = self.env["account.account"]
|
||||
self.move_model = self.env["account.move"]
|
||||
self.journal_model = self.env["account.journal"]
|
||||
self.currency_model = self.env["res.currency"]
|
||||
self.curr_year = datetime.date.today().year
|
||||
self.prev_year = self.curr_year - 1
|
||||
self.usd = self.currency_model.with_context(active_test=False).search(
|
||||
[("name", "=", "USD")]
|
||||
)
|
||||
self.eur = self.currency_model.with_context(active_test=False).search(
|
||||
[("name", "=", "EUR")]
|
||||
)
|
||||
# create company A and B
|
||||
self.company_eur = self.res_company.create(
|
||||
{"name": "CYEUR", "currency_id": self.eur.id}
|
||||
)
|
||||
self.company_usd = self.res_company.create(
|
||||
{"name": "CYUSD", "currency_id": self.usd.id}
|
||||
)
|
||||
self.env["res.currency.rate"].search([]).unlink()
|
||||
for company, divider in [(self.company_eur, 1.0), (self.company_usd, 2.0)]:
|
||||
# create receivable bs account
|
||||
company_key = company.name
|
||||
setattr(
|
||||
self,
|
||||
"account_ar_" + company_key,
|
||||
self.account_model.create(
|
||||
{
|
||||
"company_id": company.id,
|
||||
"code": "400AR",
|
||||
"name": "Receivable",
|
||||
"account_type": "asset_receivable",
|
||||
"reconcile": True,
|
||||
}
|
||||
),
|
||||
)
|
||||
# create income pl account
|
||||
setattr(
|
||||
self,
|
||||
"account_in_" + company_key,
|
||||
self.account_model.create(
|
||||
{
|
||||
"company_id": company.id,
|
||||
"code": "700IN",
|
||||
"name": "Income",
|
||||
"account_type": "income",
|
||||
}
|
||||
),
|
||||
)
|
||||
# create journal
|
||||
setattr(
|
||||
self,
|
||||
"journal" + company_key,
|
||||
self.journal_model.create(
|
||||
{
|
||||
"company_id": company.id,
|
||||
"name": "Sale journal",
|
||||
"code": "VEN",
|
||||
"type": "sale",
|
||||
}
|
||||
),
|
||||
)
|
||||
# create move in december last year
|
||||
self._create_move(
|
||||
journal=getattr(self, "journal" + company_key),
|
||||
date=datetime.date(self.prev_year, 12, 1),
|
||||
amount=100 / divider,
|
||||
debit_acc=getattr(self, "account_ar_" + company_key),
|
||||
credit_acc=getattr(self, "account_in_" + company_key),
|
||||
)
|
||||
# create move in january this year
|
||||
self._create_move(
|
||||
journal=getattr(self, "journal" + company_key),
|
||||
date=datetime.date(self.curr_year, 1, 1),
|
||||
amount=300 / divider,
|
||||
debit_acc=getattr(self, "account_ar_" + company_key),
|
||||
credit_acc=getattr(self, "account_in_" + company_key),
|
||||
)
|
||||
# create move in february this year
|
||||
self._create_move(
|
||||
journal=getattr(self, "journal" + company_key),
|
||||
date=datetime.date(self.curr_year, 3, 1),
|
||||
amount=500 / divider,
|
||||
debit_acc=getattr(self, "account_ar_" + company_key),
|
||||
credit_acc=getattr(self, "account_in_" + company_key),
|
||||
)
|
||||
|
||||
def _create_move(self, journal, date, amount, debit_acc, credit_acc):
|
||||
move = self.move_model.create(
|
||||
{
|
||||
"journal_id": journal.id,
|
||||
"date": fields.Date.to_string(date),
|
||||
"line_ids": [
|
||||
(0, 0, {"name": "/", "debit": amount, "account_id": debit_acc.id}),
|
||||
(
|
||||
0,
|
||||
0,
|
||||
{"name": "/", "credit": amount, "account_id": credit_acc.id},
|
||||
),
|
||||
],
|
||||
}
|
||||
)
|
||||
move._post()
|
||||
return move
|
||||
|
||||
def _do_queries(self, companies, currency, date_from, date_to):
|
||||
# create the AEP, and prepare the expressions we'll need
|
||||
aep = AEP(companies, currency)
|
||||
aep.parse_expr("bali[]")
|
||||
aep.parse_expr("bale[]")
|
||||
aep.parse_expr("balp[]")
|
||||
aep.parse_expr("balu[]")
|
||||
aep.parse_expr("bali[700IN]")
|
||||
aep.parse_expr("bale[700IN]")
|
||||
aep.parse_expr("balp[700IN]")
|
||||
aep.parse_expr("bali[400AR]")
|
||||
aep.parse_expr("bale[400AR]")
|
||||
aep.parse_expr("balp[400AR]")
|
||||
aep.parse_expr("debp[400A%]")
|
||||
aep.parse_expr("crdp[700I%]")
|
||||
aep.parse_expr("bali[400%]")
|
||||
aep.parse_expr("bale[700%]")
|
||||
aep.done_parsing()
|
||||
aep.do_queries(
|
||||
date_from=fields.Date.to_string(date_from),
|
||||
date_to=fields.Date.to_string(date_to),
|
||||
)
|
||||
return aep
|
||||
|
||||
def _eval(self, aep, expr):
|
||||
eval_dict = {"AccountingNone": AccountingNone}
|
||||
return safe_eval(aep.replace_expr(expr), eval_dict)
|
||||
|
||||
def _eval_by_account_id(self, aep, expr):
|
||||
res = {}
|
||||
eval_dict = {"AccountingNone": AccountingNone}
|
||||
for account_id, replaced_exprs in aep.replace_exprs_by_account_id([expr]):
|
||||
res[account_id] = safe_eval(replaced_exprs[0], eval_dict)
|
||||
return res
|
||||
|
||||
def test_aep_basic(self):
|
||||
# let's query for december, one company
|
||||
aep = self._do_queries(
|
||||
self.company_eur,
|
||||
None,
|
||||
datetime.date(self.prev_year, 12, 1),
|
||||
datetime.date(self.prev_year, 12, 31),
|
||||
)
|
||||
self.assertEqual(self._eval(aep, "balp[700IN]"), -100)
|
||||
aep = self._do_queries(
|
||||
self.company_usd,
|
||||
None,
|
||||
datetime.date(self.prev_year, 12, 1),
|
||||
datetime.date(self.prev_year, 12, 31),
|
||||
)
|
||||
self.assertEqual(self._eval(aep, "balp[700IN]"), -50)
|
||||
# let's query for december, two companies
|
||||
aep = self._do_queries(
|
||||
self.company_eur | self.company_usd,
|
||||
self.eur,
|
||||
datetime.date(self.prev_year, 12, 1),
|
||||
datetime.date(self.prev_year, 12, 31),
|
||||
)
|
||||
self.assertEqual(self._eval(aep, "balp[700IN]"), -150)
|
||||
|
||||
def test_aep_multi_currency(self):
|
||||
date_from = datetime.date(self.prev_year, 12, 1)
|
||||
date_to = datetime.date(self.prev_year, 12, 31)
|
||||
today = datetime.date.today()
|
||||
self.env["res.currency.rate"].create(
|
||||
dict(currency_id=self.usd.id, name=date_to, rate=1.1)
|
||||
)
|
||||
self.env["res.currency.rate"].create(
|
||||
dict(currency_id=self.usd.id, name=today, rate=1.2)
|
||||
)
|
||||
# let's query for december, one company, default currency = eur
|
||||
aep = self._do_queries(self.company_eur, None, date_from, date_to)
|
||||
self.assertEqual(self._eval(aep, "balp[700IN]"), -100)
|
||||
# let's query for december, two companies
|
||||
aep = self._do_queries(
|
||||
self.company_eur | self.company_usd, self.eur, date_from, date_to
|
||||
)
|
||||
self.assertAlmostEqual(self._eval(aep, "balp[700IN]"), -100 - 50 / 1.1)
|
||||
# let's query for december, one company, currency = usd
|
||||
aep = self._do_queries(self.company_eur, self.usd, date_from, date_to)
|
||||
self.assertAlmostEqual(self._eval(aep, "balp[700IN]"), -100 * 1.1)
|
||||
# let's query for december, two companies, currency = usd
|
||||
aep = self._do_queries(
|
||||
self.company_eur | self.company_usd, self.usd, date_from, date_to
|
||||
)
|
||||
self.assertAlmostEqual(self._eval(aep, "balp[700IN]"), -100 * 1.1 - 50)
|
||||
|
|
@ -1,159 +0,0 @@
|
|||
# Copyright 2017 ACSONE SA/NV (<http://acsone.eu>)
|
||||
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html).
|
||||
|
||||
import odoo.tests.common as common
|
||||
from odoo import fields
|
||||
|
||||
from ..models.mis_report_instance import (
|
||||
MODE_FIX,
|
||||
MODE_NONE,
|
||||
MODE_REL,
|
||||
SRC_SUMCOL,
|
||||
DateFilterForbidden,
|
||||
DateFilterRequired,
|
||||
)
|
||||
from .common import assert_matrix
|
||||
|
||||
|
||||
class TestPeriodDates(common.TransactionCase):
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
self.report_obj = self.env["mis.report"]
|
||||
self.instance_obj = self.env["mis.report.instance"]
|
||||
self.period_obj = self.env["mis.report.instance.period"]
|
||||
self.report = self.report_obj.create(dict(name="test-report"))
|
||||
self.instance = self.instance_obj.create(
|
||||
dict(name="test-instance", report_id=self.report.id, comparison_mode=False)
|
||||
)
|
||||
self.assertEqual(len(self.instance.period_ids), 1)
|
||||
self.period = self.instance.period_ids[0]
|
||||
|
||||
def assertDateEqual(self, first, second, msg=None):
|
||||
self.assertEqual(first, fields.Date.from_string(second), msg)
|
||||
|
||||
def test_date_filter_constraints(self):
|
||||
self.instance.comparison_mode = True
|
||||
with self.assertRaises(DateFilterRequired):
|
||||
self.period.write(dict(mode=MODE_NONE))
|
||||
with self.assertRaises(DateFilterForbidden):
|
||||
self.period.write(dict(mode=MODE_FIX, source=SRC_SUMCOL))
|
||||
|
||||
def test_simple_mode(self):
|
||||
# not comparison_mode
|
||||
self.assertFalse(self.instance.comparison_mode)
|
||||
period = self.instance.period_ids[0]
|
||||
self.assertEqual(period.date_from, self.instance.date_from)
|
||||
self.assertEqual(period.date_to, self.instance.date_to)
|
||||
|
||||
def tests_mode_none(self):
|
||||
self.instance.comparison_mode = True
|
||||
self.period.write(dict(mode=MODE_NONE, source=SRC_SUMCOL))
|
||||
self.assertFalse(self.period.date_from)
|
||||
self.assertFalse(self.period.date_to)
|
||||
self.assertTrue(self.period.valid)
|
||||
|
||||
def tests_mode_fix(self):
|
||||
self.instance.comparison_mode = True
|
||||
self.period.write(
|
||||
dict(
|
||||
mode=MODE_FIX,
|
||||
manual_date_from="2017-01-01",
|
||||
manual_date_to="2017-12-31",
|
||||
)
|
||||
)
|
||||
self.assertDateEqual(self.period.date_from, "2017-01-01")
|
||||
self.assertDateEqual(self.period.date_to, "2017-12-31")
|
||||
self.assertTrue(self.period.valid)
|
||||
|
||||
def test_rel_day(self):
|
||||
self.instance.write(dict(comparison_mode=True, date="2017-01-01"))
|
||||
self.period.write(dict(mode=MODE_REL, type="d", offset="-2"))
|
||||
self.assertDateEqual(self.period.date_from, "2016-12-30")
|
||||
self.assertDateEqual(self.period.date_to, "2016-12-30")
|
||||
self.assertTrue(self.period.valid)
|
||||
|
||||
def test_rel_day_ytd(self):
|
||||
self.instance.write(dict(comparison_mode=True, date="2019-05-03"))
|
||||
self.period.write(dict(mode=MODE_REL, type="d", offset="-2", is_ytd=True))
|
||||
self.assertDateEqual(self.period.date_from, "2019-01-01")
|
||||
self.assertDateEqual(self.period.date_to, "2019-05-01")
|
||||
self.assertTrue(self.period.valid)
|
||||
|
||||
def test_rel_week(self):
|
||||
self.instance.write(dict(comparison_mode=True, date="2016-12-30"))
|
||||
self.period.write(dict(mode=MODE_REL, type="w", offset="1", duration=2))
|
||||
# from Monday to Sunday, the week after 2016-12-30
|
||||
self.assertDateEqual(self.period.date_from, "2017-01-02")
|
||||
self.assertDateEqual(self.period.date_to, "2017-01-15")
|
||||
self.assertTrue(self.period.valid)
|
||||
|
||||
def test_rel_week_ytd(self):
|
||||
self.instance.write(dict(comparison_mode=True, date="2019-05-27"))
|
||||
self.period.write(
|
||||
dict(mode=MODE_REL, type="w", offset="1", duration=2, is_ytd=True)
|
||||
)
|
||||
self.assertDateEqual(self.period.date_from, "2019-01-01")
|
||||
self.assertDateEqual(self.period.date_to, "2019-06-16")
|
||||
self.assertTrue(self.period.valid)
|
||||
|
||||
def test_rel_month(self):
|
||||
self.instance.write(dict(comparison_mode=True, date="2017-01-05"))
|
||||
self.period.write(dict(mode=MODE_REL, type="m", offset="1"))
|
||||
self.assertDateEqual(self.period.date_from, "2017-02-01")
|
||||
self.assertDateEqual(self.period.date_to, "2017-02-28")
|
||||
self.assertTrue(self.period.valid)
|
||||
|
||||
def test_rel_month_ytd(self):
|
||||
self.instance.write(dict(comparison_mode=True, date="2019-05-15"))
|
||||
self.period.write(dict(mode=MODE_REL, type="m", offset="-1", is_ytd=True))
|
||||
self.assertDateEqual(self.period.date_from, "2019-01-01")
|
||||
self.assertDateEqual(self.period.date_to, "2019-04-30")
|
||||
self.assertTrue(self.period.valid)
|
||||
|
||||
def test_rel_year(self):
|
||||
self.instance.write(dict(comparison_mode=True, date="2017-05-06"))
|
||||
self.period.write(dict(mode=MODE_REL, type="y", offset="1"))
|
||||
self.assertDateEqual(self.period.date_from, "2018-01-01")
|
||||
self.assertDateEqual(self.period.date_to, "2018-12-31")
|
||||
self.assertTrue(self.period.valid)
|
||||
|
||||
def test_rel_date_range(self):
|
||||
# create a few date ranges
|
||||
date_range_type = self.env["date.range.type"].create(dict(name="Year"))
|
||||
for year in (2016, 2017, 2018):
|
||||
self.env["date.range"].create(
|
||||
dict(
|
||||
type_id=date_range_type.id,
|
||||
name="%d" % year,
|
||||
date_start="%d-01-01" % year,
|
||||
date_end="%d-12-31" % year,
|
||||
company_id=date_range_type.company_id.id,
|
||||
)
|
||||
)
|
||||
self.instance.write(dict(comparison_mode=True, date="2017-06-15"))
|
||||
self.period.write(
|
||||
dict(
|
||||
mode=MODE_REL,
|
||||
type="date_range",
|
||||
date_range_type_id=date_range_type.id,
|
||||
offset="-1",
|
||||
duration=3,
|
||||
)
|
||||
)
|
||||
self.assertDateEqual(self.period.date_from, "2016-01-01")
|
||||
self.assertDateEqual(self.period.date_to, "2018-12-31")
|
||||
self.assertTrue(self.period.valid)
|
||||
|
||||
def test_dates_in_expr(self):
|
||||
self.env["mis.report.kpi"].create(
|
||||
dict(
|
||||
report_id=self.report.id,
|
||||
name="k1",
|
||||
description="kpi 1",
|
||||
expression="(date_to - date_from).days + 1",
|
||||
)
|
||||
)
|
||||
self.instance.date_from = "2017-01-01"
|
||||
self.instance.date_to = "2017-01-31"
|
||||
matrix = self.instance._compute_matrix()
|
||||
assert_matrix(matrix, [[31]])
|
||||
|
|
@ -1,315 +0,0 @@
|
|||
# Copyright 2016 ACSONE SA/NV (<http://acsone.eu>)
|
||||
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html).
|
||||
|
||||
import odoo.tests.common as common
|
||||
|
||||
from ..models.accounting_none import AccountingNone
|
||||
from ..models.data_error import DataError
|
||||
from ..models.mis_report_style import CMP_DIFF, CMP_PCT, TYPE_NUM, TYPE_PCT, TYPE_STR
|
||||
|
||||
|
||||
class TestRendering(common.TransactionCase):
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
self.style_obj = self.env["mis.report.style"]
|
||||
self.kpi_obj = self.env["mis.report.kpi"]
|
||||
self.style = self.style_obj.create(dict(name="teststyle"))
|
||||
self.lang = (
|
||||
self.env["res.lang"]
|
||||
.with_context(active_test=False)
|
||||
.search([("code", "=", "en_US")])[0]
|
||||
)
|
||||
|
||||
def _render(self, value, var_type=TYPE_NUM):
|
||||
style_props = self.style_obj.merge([self.style])
|
||||
return self.style_obj.render(self.lang, style_props, var_type, value)
|
||||
|
||||
def _compare_and_render(
|
||||
self, value, base_value, var_type=TYPE_NUM, compare_method=CMP_PCT
|
||||
):
|
||||
style_props = self.style_obj.merge([self.style])
|
||||
r = self.style_obj.compare_and_render(
|
||||
self.lang, style_props, var_type, compare_method, value, base_value
|
||||
)[:2]
|
||||
if r[0]:
|
||||
return (round(r[0], 8), r[1])
|
||||
else:
|
||||
return r
|
||||
|
||||
def test_render(self):
|
||||
self.assertEqual("1", self._render(1))
|
||||
self.assertEqual("1", self._render(1.1))
|
||||
self.assertEqual("2", self._render(1.6))
|
||||
self.style.dp_inherit = False
|
||||
self.style.dp = 2
|
||||
self.assertEqual("1.00", self._render(1))
|
||||
self.assertEqual("1.10", self._render(1.1))
|
||||
self.assertEqual("1.60", self._render(1.6))
|
||||
self.assertEqual("1.61", self._render(1.606))
|
||||
self.assertEqual("12,345.67", self._render(12345.67))
|
||||
|
||||
def test_render_negative(self):
|
||||
# non breaking hyphen
|
||||
self.assertEqual("\u20111", self._render(-1))
|
||||
|
||||
def test_render_zero(self):
|
||||
self.assertEqual("0", self._render(0))
|
||||
self.assertEqual("", self._render(None))
|
||||
self.assertEqual("", self._render(AccountingNone))
|
||||
|
||||
def test_render_suffix(self):
|
||||
self.style.suffix_inherit = False
|
||||
self.style.suffix = "€"
|
||||
self.assertEqual("1\xa0€", self._render(1))
|
||||
self.style.suffix = "k€"
|
||||
self.style.divider_inherit = False
|
||||
self.style.divider = "1e3"
|
||||
self.assertEqual("1\xa0k€", self._render(1000))
|
||||
|
||||
def test_render_prefix(self):
|
||||
self.style.prefix_inherit = False
|
||||
self.style.prefix = "$"
|
||||
self.assertEqual("$\xa01", self._render(1))
|
||||
self.style.prefix = "k$"
|
||||
self.style.divider_inherit = False
|
||||
self.style.divider = "1e3"
|
||||
self.assertEqual("k$\xa01", self._render(1000))
|
||||
|
||||
def test_render_divider(self):
|
||||
self.style.divider_inherit = False
|
||||
self.style.divider = "1e3"
|
||||
self.style.dp_inherit = False
|
||||
self.style.dp = 0
|
||||
self.assertEqual("1", self._render(1000))
|
||||
self.style.divider = "1e6"
|
||||
self.style.dp = 3
|
||||
self.assertEqual("0.001", self._render(1000))
|
||||
self.style.divider = "1e-3"
|
||||
self.style.dp = 0
|
||||
self.assertEqual("1,000", self._render(1))
|
||||
self.style.divider = "1e-6"
|
||||
self.style.dp = 0
|
||||
self.assertEqual("1,000,000", self._render(1))
|
||||
|
||||
def test_render_pct(self):
|
||||
self.assertEqual("100\xa0%", self._render(1, TYPE_PCT))
|
||||
self.assertEqual("50\xa0%", self._render(0.5, TYPE_PCT))
|
||||
self.style.dp_inherit = False
|
||||
self.style.dp = 2
|
||||
self.assertEqual("51.23\xa0%", self._render(0.5123, TYPE_PCT))
|
||||
|
||||
def test_render_string(self):
|
||||
self.assertEqual("", self._render("", TYPE_STR))
|
||||
self.assertEqual("", self._render(None, TYPE_STR))
|
||||
self.assertEqual("abcdé", self._render("abcdé", TYPE_STR))
|
||||
|
||||
def test_compare_num_pct(self):
|
||||
self.assertEqual((1.0, "+100.0\xa0%"), self._compare_and_render(100, 50))
|
||||
self.assertEqual((0.5, "+50.0\xa0%"), self._compare_and_render(75, 50))
|
||||
self.assertEqual((0.5, "+50.0\xa0%"), self._compare_and_render(-25, -50))
|
||||
self.assertEqual((1.0, "+100.0\xa0%"), self._compare_and_render(0, -50))
|
||||
self.assertEqual((2.0, "+200.0\xa0%"), self._compare_and_render(50, -50))
|
||||
self.assertEqual((-0.5, "\u201150.0\xa0%"), self._compare_and_render(25, 50))
|
||||
self.assertEqual((-1.0, "\u2011100.0\xa0%"), self._compare_and_render(0, 50))
|
||||
self.assertEqual((-2.0, "\u2011200.0\xa0%"), self._compare_and_render(-50, 50))
|
||||
self.assertEqual((-0.5, "\u201150.0\xa0%"), self._compare_and_render(-75, -50))
|
||||
self.assertEqual(
|
||||
(AccountingNone, ""), self._compare_and_render(50, AccountingNone)
|
||||
)
|
||||
self.assertEqual((AccountingNone, ""), self._compare_and_render(50, None))
|
||||
self.assertEqual((AccountingNone, ""), self._compare_and_render(50, 50))
|
||||
self.assertEqual((0.002, "+0.2\xa0%"), self._compare_and_render(50.1, 50))
|
||||
self.assertEqual((AccountingNone, ""), self._compare_and_render(50.01, 50))
|
||||
self.assertEqual(
|
||||
(-1.0, "\u2011100.0\xa0%"), self._compare_and_render(AccountingNone, 50)
|
||||
)
|
||||
self.assertEqual((-1.0, "\u2011100.0\xa0%"), self._compare_and_render(None, 50))
|
||||
self.assertEqual(
|
||||
(AccountingNone, ""), self._compare_and_render(DataError("#ERR", "."), 1)
|
||||
)
|
||||
self.assertEqual(
|
||||
(AccountingNone, ""), self._compare_and_render(1, DataError("#ERR", "."))
|
||||
)
|
||||
|
||||
def test_compare_num_diff(self):
|
||||
self.assertEqual(
|
||||
(25, "+25"), self._compare_and_render(75, 50, TYPE_NUM, CMP_DIFF)
|
||||
)
|
||||
self.assertEqual(
|
||||
(-25, "\u201125"), self._compare_and_render(25, 50, TYPE_NUM, CMP_DIFF)
|
||||
)
|
||||
self.style.suffix_inherit = False
|
||||
self.style.suffix = "€"
|
||||
self.assertEqual(
|
||||
(-25, "\u201125\xa0€"),
|
||||
self._compare_and_render(25, 50, TYPE_NUM, CMP_DIFF),
|
||||
)
|
||||
self.style.suffix = ""
|
||||
self.assertEqual(
|
||||
(50.0, "+50"),
|
||||
self._compare_and_render(50, AccountingNone, TYPE_NUM, CMP_DIFF),
|
||||
)
|
||||
self.assertEqual(
|
||||
(50.0, "+50"), self._compare_and_render(50, None, TYPE_NUM, CMP_DIFF)
|
||||
)
|
||||
self.assertEqual(
|
||||
(-50.0, "\u201150"),
|
||||
self._compare_and_render(AccountingNone, 50, TYPE_NUM, CMP_DIFF),
|
||||
)
|
||||
self.assertEqual(
|
||||
(-50.0, "\u201150"), self._compare_and_render(None, 50, TYPE_NUM, CMP_DIFF)
|
||||
)
|
||||
self.style.dp_inherit = False
|
||||
self.style.dp = 2
|
||||
self.assertEqual(
|
||||
(0.1, "+0.10"), self._compare_and_render(1.1, 1.0, TYPE_NUM, CMP_DIFF)
|
||||
)
|
||||
self.assertEqual(
|
||||
(AccountingNone, ""),
|
||||
self._compare_and_render(1.001, 1.0, TYPE_NUM, CMP_DIFF),
|
||||
)
|
||||
|
||||
def test_compare_pct(self):
|
||||
self.assertEqual(
|
||||
(0.25, "+25\xa0pp"), self._compare_and_render(0.75, 0.50, TYPE_PCT)
|
||||
)
|
||||
self.assertEqual(
|
||||
(AccountingNone, ""), self._compare_and_render(0.751, 0.750, TYPE_PCT)
|
||||
)
|
||||
|
||||
def test_compare_pct_result_type(self):
|
||||
style_props = self.style_obj.merge([self.style])
|
||||
result = self.style_obj.compare_and_render(
|
||||
self.lang, style_props, TYPE_PCT, CMP_DIFF, 0.75, 0.50
|
||||
)
|
||||
self.assertEqual(result[3], TYPE_NUM)
|
||||
|
||||
def test_merge(self):
|
||||
self.style.color = "#FF0000"
|
||||
self.style.color_inherit = False
|
||||
style_props = self.style_obj.merge([self.style])
|
||||
self.assertEqual(style_props, {"color": "#FF0000"})
|
||||
style_dict = {"color": "#00FF00", "dp": 0}
|
||||
style_props = self.style_obj.merge([self.style, style_dict])
|
||||
self.assertEqual(style_props, {"color": "#00FF00", "dp": 0})
|
||||
style2 = self.style_obj.create(
|
||||
dict(
|
||||
name="teststyle2",
|
||||
dp_inherit=False,
|
||||
dp=1,
|
||||
# color_inherit=True: will not be applied
|
||||
color="#0000FF",
|
||||
)
|
||||
)
|
||||
style_props = self.style_obj.merge([self.style, style_dict, style2])
|
||||
self.assertEqual(style_props, {"color": "#00FF00", "dp": 1})
|
||||
|
||||
def test_css(self):
|
||||
self.style.color_inherit = False
|
||||
self.style.color = "#FF0000"
|
||||
self.style.background_color_inherit = False
|
||||
self.style.background_color = "#0000FF"
|
||||
self.style.suffix_inherit = False
|
||||
self.style.suffix = "s"
|
||||
self.style.prefix_inherit = False
|
||||
self.style.prefix = "p"
|
||||
self.style.font_style_inherit = False
|
||||
self.style.font_style = "italic"
|
||||
self.style.font_weight_inherit = False
|
||||
self.style.font_weight = "bold"
|
||||
self.style.font_size_inherit = False
|
||||
self.style.font_size = "small"
|
||||
self.style.indent_level_inherit = False
|
||||
self.style.indent_level = 2
|
||||
style_props = self.style_obj.merge([self.style])
|
||||
css = self.style_obj.to_css_style(style_props)
|
||||
self.assertEqual(
|
||||
css,
|
||||
"font-style: italic; "
|
||||
"font-weight: bold; "
|
||||
"font-size: small; "
|
||||
"color: #FF0000; "
|
||||
"background-color: #0000FF; "
|
||||
"text-indent: 2em",
|
||||
)
|
||||
css = self.style_obj.to_css_style(style_props, no_indent=True)
|
||||
self.assertEqual(
|
||||
css,
|
||||
"font-style: italic; "
|
||||
"font-weight: bold; "
|
||||
"font-size: small; "
|
||||
"color: #FF0000; "
|
||||
"background-color: #0000FF",
|
||||
)
|
||||
|
||||
def test_xslx(self):
|
||||
self.style.color_inherit = False
|
||||
self.style.color = "#FF0000"
|
||||
self.style.background_color_inherit = False
|
||||
self.style.background_color = "#0000FF"
|
||||
self.style.suffix_inherit = False
|
||||
self.style.suffix = "s"
|
||||
self.style.prefix_inherit = False
|
||||
self.style.prefix = "p"
|
||||
self.style.dp_inherit = False
|
||||
self.style.dp = 2
|
||||
self.style.font_style_inherit = False
|
||||
self.style.font_style = "italic"
|
||||
self.style.font_weight_inherit = False
|
||||
self.style.font_weight = "bold"
|
||||
self.style.font_size_inherit = False
|
||||
self.style.font_size = "small"
|
||||
self.style.indent_level_inherit = False
|
||||
self.style.indent_level = 2
|
||||
style_props = self.style_obj.merge([self.style])
|
||||
xlsx = self.style_obj.to_xlsx_style(TYPE_NUM, style_props)
|
||||
self.assertEqual(
|
||||
xlsx,
|
||||
{
|
||||
"italic": True,
|
||||
"bold": True,
|
||||
"font_size": 9,
|
||||
"font_color": "#FF0000",
|
||||
"bg_color": "#0000FF",
|
||||
"num_format": '"p "#,##0.00" s"',
|
||||
"indent": 2,
|
||||
},
|
||||
)
|
||||
xlsx = self.style_obj.to_xlsx_style(TYPE_NUM, style_props, no_indent=True)
|
||||
self.assertEqual(
|
||||
xlsx,
|
||||
{
|
||||
"italic": True,
|
||||
"bold": True,
|
||||
"font_size": 9,
|
||||
"font_color": "#FF0000",
|
||||
"bg_color": "#0000FF",
|
||||
"num_format": '"p "#,##0.00" s"',
|
||||
},
|
||||
)
|
||||
# percent type ignore prefix and suffix
|
||||
xlsx = self.style_obj.to_xlsx_style(TYPE_PCT, style_props, no_indent=True)
|
||||
self.assertEqual(
|
||||
xlsx,
|
||||
{
|
||||
"italic": True,
|
||||
"bold": True,
|
||||
"font_size": 9,
|
||||
"font_color": "#FF0000",
|
||||
"bg_color": "#0000FF",
|
||||
"num_format": "0.00%",
|
||||
},
|
||||
)
|
||||
|
||||
# str type have no num_format style
|
||||
xlsx = self.style_obj.to_xlsx_style(TYPE_STR, style_props, no_indent=True)
|
||||
self.assertEqual(
|
||||
xlsx,
|
||||
{
|
||||
"italic": True,
|
||||
"bold": True,
|
||||
"font_size": 9,
|
||||
"font_color": "#FF0000",
|
||||
"bg_color": "#0000FF",
|
||||
},
|
||||
)
|
||||
|
|
@ -1,7 +0,0 @@
|
|||
# Copyright 2014 ACSONE SA/NV (<http://acsone.eu>)
|
||||
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html).
|
||||
|
||||
from ..models import simple_array
|
||||
from .common import load_doctests
|
||||
|
||||
load_tests = load_doctests(simple_array)
|
||||
|
|
@ -1,96 +0,0 @@
|
|||
# Copyright 2020 ACSONE SA/NV (<http://acsone.eu>)
|
||||
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html).
|
||||
|
||||
from odoo.tests.common import TransactionCase
|
||||
|
||||
from odoo.addons.mis_builder.models.expression_evaluator import ExpressionEvaluator
|
||||
from odoo.addons.mis_builder.models.mis_report_subreport import (
|
||||
InvalidNameError,
|
||||
ParentLoopError,
|
||||
)
|
||||
|
||||
|
||||
class TestMisSubreport(TransactionCase):
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
super().setUpClass()
|
||||
# create report
|
||||
cls.subreport = cls.env["mis.report"].create(dict(name="test subreport"))
|
||||
cls.subreport_kpi1 = cls.env["mis.report.kpi"].create(
|
||||
dict(
|
||||
report_id=cls.subreport.id,
|
||||
name="sk1",
|
||||
description="subreport kpi 1",
|
||||
expression="11",
|
||||
)
|
||||
)
|
||||
cls.report = cls.env["mis.report"].create(
|
||||
dict(
|
||||
name="test report",
|
||||
subreport_ids=[
|
||||
(0, 0, dict(name="subreport", subreport_id=cls.subreport.id))
|
||||
],
|
||||
)
|
||||
)
|
||||
cls.report_kpi1 = cls.env["mis.report.kpi"].create(
|
||||
dict(
|
||||
report_id=cls.report.id,
|
||||
name="k1",
|
||||
description="report kpi 1",
|
||||
expression="subreport.sk1 + 1",
|
||||
)
|
||||
)
|
||||
cls.parent_report = cls.env["mis.report"].create(
|
||||
dict(
|
||||
name="parent report",
|
||||
subreport_ids=[(0, 0, dict(name="report", subreport_id=cls.report.id))],
|
||||
)
|
||||
)
|
||||
cls.parent_report_kpi1 = cls.env["mis.report.kpi"].create(
|
||||
dict(
|
||||
report_id=cls.parent_report.id,
|
||||
name="pk1",
|
||||
description="parent report kpi 1",
|
||||
expression="report.k1 + 1",
|
||||
)
|
||||
)
|
||||
|
||||
def test_basic(self):
|
||||
ee = ExpressionEvaluator(aep=None, date_from="2017-01-01", date_to="2017-01-16")
|
||||
d = self.report._evaluate(ee)
|
||||
assert d["k1"] == 12
|
||||
|
||||
def test_two_levels(self):
|
||||
ee = ExpressionEvaluator(aep=None, date_from="2017-01-01", date_to="2017-01-16")
|
||||
d = self.parent_report._evaluate(ee)
|
||||
assert d["pk1"] == 13
|
||||
|
||||
def test_detect_loop(self):
|
||||
with self.assertRaises(ParentLoopError):
|
||||
self.report.write(
|
||||
dict(
|
||||
subreport_ids=[
|
||||
(
|
||||
0,
|
||||
0,
|
||||
dict(name="preport1", subreport_id=self.parent_report.id),
|
||||
)
|
||||
]
|
||||
)
|
||||
)
|
||||
with self.assertRaises(ParentLoopError):
|
||||
self.report.write(
|
||||
dict(
|
||||
subreport_ids=[
|
||||
(
|
||||
0,
|
||||
0,
|
||||
dict(name="preport2", subreport_id=self.report.id),
|
||||
)
|
||||
]
|
||||
)
|
||||
)
|
||||
|
||||
def test_invalid_name(self):
|
||||
with self.assertRaises(InvalidNameError):
|
||||
self.report.subreport_ids[0].name = "ab c"
|
||||
|
|
@ -1,36 +0,0 @@
|
|||
# Copyright 2016 ACSONE SA/NV (<http://acsone.eu>)
|
||||
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html).
|
||||
|
||||
import odoo.tests.common as common
|
||||
|
||||
|
||||
class TestMisReportInstance(common.TransactionCase):
|
||||
def test_supports_target_move_filter(self):
|
||||
self.assertTrue(
|
||||
self.env["mis.report"]._supports_target_move_filter("account.move.line")
|
||||
)
|
||||
|
||||
def test_supports_target_move_filter_no_parent_state(self):
|
||||
self.assertFalse(
|
||||
self.env["mis.report"]._supports_target_move_filter("account.move")
|
||||
)
|
||||
|
||||
def test_target_move_domain_posted(self):
|
||||
self.assertEqual(
|
||||
self.env["mis.report"]._get_target_move_domain(
|
||||
"posted", "account.move.line"
|
||||
),
|
||||
[("parent_state", "=", "posted")],
|
||||
)
|
||||
|
||||
def test_target_move_domain_all(self):
|
||||
self.assertEqual(
|
||||
self.env["mis.report"]._get_target_move_domain("all", "account.move.line"),
|
||||
[("parent_state", "in", ("posted", "draft"))],
|
||||
)
|
||||
|
||||
def test_target_move_domain_no_parent_state(self):
|
||||
"""Test get_target_move_domain on a model that has no parent_state."""
|
||||
self.assertEqual(
|
||||
self.env["mis.report"]._get_target_move_domain("all", "account.move"), []
|
||||
)
|
||||
|
|
@ -1,19 +0,0 @@
|
|||
# Copyright 2014 ACSONE SA/NV (<http://acsone.eu>)
|
||||
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html).
|
||||
|
||||
import odoo.tests.common as common
|
||||
|
||||
from ..models.mis_report import _utc_midnight
|
||||
|
||||
|
||||
class TestUtcMidnight(common.TransactionCase):
|
||||
def test_utc_midnight(self):
|
||||
date_to_convert = "2014-07-05"
|
||||
date_time_convert = _utc_midnight(date_to_convert, "Europe/Brussels")
|
||||
self.assertEqual(date_time_convert, "2014-07-04 22:00:00")
|
||||
date_time_convert = _utc_midnight(date_to_convert, "Europe/Brussels", add_day=1)
|
||||
self.assertEqual(date_time_convert, "2014-07-05 22:00:00")
|
||||
date_time_convert = _utc_midnight(date_to_convert, "US/Pacific")
|
||||
self.assertEqual(date_time_convert, "2014-07-05 07:00:00")
|
||||
date_time_convert = _utc_midnight(date_to_convert, "US/Pacific", add_day=1)
|
||||
self.assertEqual(date_time_convert, "2014-07-06 07:00:00")
|
||||
|
|
@ -1,306 +0,0 @@
|
|||
<?xml version="1.0" encoding="UTF-8" ?>
|
||||
<odoo>
|
||||
<record model="ir.ui.view" id="mis_report_view_tree">
|
||||
<field name="name">mis.report.view.tree</field>
|
||||
<field name="model">mis.report</field>
|
||||
<field name="arch" type="xml">
|
||||
<tree>
|
||||
<field name="name" />
|
||||
<field name="description" />
|
||||
</tree>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record model="ir.ui.view" id="mis_report_view_form">
|
||||
<field name="name">mis.report.view.form</field>
|
||||
<field name="model">mis.report</field>
|
||||
<field name="arch" type="xml">
|
||||
<form string="MIS Report">
|
||||
<sheet>
|
||||
<group col="2">
|
||||
<field name="name" />
|
||||
<field name="description" />
|
||||
<field name="style_id" />
|
||||
<field name="move_lines_source" options="{'no_open': true}" />
|
||||
</group>
|
||||
<notebook>
|
||||
<page string="KPI's">
|
||||
<field
|
||||
name="kpi_ids"
|
||||
nolabel="1"
|
||||
colspan="2"
|
||||
context="{'default_report_id': id}"
|
||||
>
|
||||
<tree>
|
||||
<field name="sequence" widget="handle" />
|
||||
<field name="description" />
|
||||
<field name="name" />
|
||||
<field name="type" />
|
||||
<field
|
||||
name="compare_method"
|
||||
attrs="{'invisible': [('type', '=', 'str')]}"
|
||||
/>
|
||||
<field
|
||||
name="accumulation_method"
|
||||
attrs="{'invisible': [('type', '=', 'str')]}"
|
||||
/>
|
||||
<field name="expression" />
|
||||
</tree>
|
||||
</field>
|
||||
</page>
|
||||
<page string="Queries">
|
||||
<field
|
||||
name="query_ids"
|
||||
nolabel="1"
|
||||
colspan="2"
|
||||
context="{'default_report_id': id}"
|
||||
>
|
||||
<tree editable="bottom">
|
||||
<field name="name" />
|
||||
<field name="model_id" />
|
||||
<field
|
||||
name="field_ids"
|
||||
domain="[('model_id', '=', model_id)]"
|
||||
widget="many2many_tags"
|
||||
/>
|
||||
<field name="field_names" />
|
||||
<field name="aggregate" />
|
||||
<field
|
||||
name="date_field"
|
||||
domain="[('model_id', '=', model_id), ('ttype', 'in', ('date', 'datetime'))]"
|
||||
/>
|
||||
<field name="domain" />
|
||||
</tree>
|
||||
</field>
|
||||
</page>
|
||||
<page string="Sub KPI's">
|
||||
<field name="subkpi_ids" nolabel="1" colspan="2">
|
||||
<tree editable="bottom">
|
||||
<field name="sequence" widget="handle" />
|
||||
<field name="description" />
|
||||
<field name="name" />
|
||||
</tree>
|
||||
</field>
|
||||
</page>
|
||||
<page string="Sub Reports">
|
||||
<field
|
||||
name="subreport_ids"
|
||||
nolabel="1"
|
||||
colspan="2"
|
||||
context="{'default_report_id': id}"
|
||||
>
|
||||
<tree editable="bottom">
|
||||
<field name="name" />
|
||||
<field
|
||||
name="subreport_id"
|
||||
domain="[('id', '!=', parent.id)]"
|
||||
/>
|
||||
</tree>
|
||||
</field>
|
||||
</page>
|
||||
</notebook>
|
||||
</sheet>
|
||||
</form>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="mis_report_view_kpi_form" model="ir.ui.view">
|
||||
<field name="name">mis.report.view.kpi.form</field>
|
||||
<field name="model">mis.report.kpi</field>
|
||||
<field name="arch" type="xml">
|
||||
<form string="MIS Report KPI">
|
||||
<group col="4">
|
||||
<field name="description" />
|
||||
<field name="name" />
|
||||
<field name="type" />
|
||||
<newline />
|
||||
<field name="compare_method" />
|
||||
<field name="accumulation_method" />
|
||||
<field name="style_id" />
|
||||
<field name="style_expression" />
|
||||
<field name='id' invisible='1' />
|
||||
<field
|
||||
name="report_id"
|
||||
invisible="1"
|
||||
attrs="{'required': [('id', '!=', False)]}"
|
||||
/>
|
||||
</group>
|
||||
<notebook>
|
||||
<page string="Expressions">
|
||||
<group col="2">
|
||||
<field name="multi" />
|
||||
<newline />
|
||||
<field
|
||||
name="expression_ids"
|
||||
colspan="2"
|
||||
nolabel="1"
|
||||
attrs="{'invisible': [('multi', '=', False)]}"
|
||||
>
|
||||
<tree editable="bottom">
|
||||
<field
|
||||
name="subkpi_id"
|
||||
domain="[('report_id', '=', parent.report_id)]"
|
||||
/>
|
||||
<field name="name" />
|
||||
</tree>
|
||||
</field>
|
||||
<field
|
||||
name="expression"
|
||||
colspan="2"
|
||||
nolabel="1"
|
||||
attrs="{'invisible': [('multi', '=', True)],
|
||||
'readonly': [('multi', '=', True)]}"
|
||||
placeholder="Enter expression here, for example balp[70%]. See also help tab."
|
||||
/>
|
||||
</group>
|
||||
<group col="4" string="Auto expand">
|
||||
<field name="auto_expand_accounts" />
|
||||
<field
|
||||
name="auto_expand_accounts_style_id"
|
||||
attrs="{'invisible': [('auto_expand_accounts', '!=', True)]}"
|
||||
/>
|
||||
</group>
|
||||
</page>
|
||||
<page string="Help (for KPI expressions)">
|
||||
<div style="display: flex; width: 100;">
|
||||
<div>
|
||||
<p>
|
||||
Expressions can be any valid python expressions.
|
||||
</p>
|
||||
<p
|
||||
> The following special elements are recognized in the expressions
|
||||
to compute accounting data: <code
|
||||
>{bal|crd|deb|pbal|nbal|fld}{pieu}(.fieldname)[account
|
||||
selector][journal items domain]</code>. </p>
|
||||
<ul>
|
||||
<li>
|
||||
<code>bal</code>, <code>crd</code>, <code
|
||||
>deb</code>, <code>
|
||||
pbal</code>, <code>nbal</code>, <code
|
||||
>fld</code> : balance, debit, credit,
|
||||
positive balance, negative balance,
|
||||
other numerical field. </li>
|
||||
<li>
|
||||
<code>p</code>, <code>i</code>, <code
|
||||
>e</code> : respectively variation over the period,
|
||||
initial balance, ending balance </li>
|
||||
<li>when <code
|
||||
>fld</code> is used : a field name specifier
|
||||
must be provided (e.g. <code
|
||||
>fldp.quantity</code></li>
|
||||
<li> The <b
|
||||
>account selector</b> is a like expression on the
|
||||
account code (eg <code
|
||||
>70%</code>, etc), or a domain over accounts
|
||||
(eg <code
|
||||
>[('code', 'like', '60%')]</code>). </li>
|
||||
<li> The <b
|
||||
>journal items domain</b> is an Odoo domain filter on
|
||||
journal items. </li>
|
||||
<li>
|
||||
<code>balu[]</code> : (<code
|
||||
>u</code> for unallocated) is a special expression
|
||||
that shows the unallocated profit/loss of previous fiscal
|
||||
years. </li>
|
||||
</ul>
|
||||
<p>
|
||||
Expressions can involve other KPI, sub KPI and
|
||||
query results by name (eg <code>kpi1 + kpi2</code>,
|
||||
<code>kpi2.subkpi1</code>, <code
|
||||
>query1.field1</code>).
|
||||
</p>
|
||||
<p>
|
||||
Additionally following variables are available
|
||||
in the evaluation context:
|
||||
</p>
|
||||
<ul>
|
||||
<li>
|
||||
<code>sum</code>, <code>min</code>,
|
||||
<code>max</code>, <code>len</code>,
|
||||
<code>avg</code> : behave as expected, very
|
||||
similar to the python builtins. </li>
|
||||
<li>
|
||||
<code>datetime</code>, <code
|
||||
>datetime</code>, <code
|
||||
>dateutil</code> : the python modules. </li>
|
||||
<li>
|
||||
<code>date_from</code>, <code
|
||||
>date_to</code> : beginning and end date of the
|
||||
period. </li>
|
||||
<li>
|
||||
<code
|
||||
>AccountingNone</code> : a null value that behaves as 0 in
|
||||
arithmetic operations. </li>
|
||||
</ul>
|
||||
</div>
|
||||
<div>
|
||||
<p>Examples:</p>
|
||||
<ul>
|
||||
<li>
|
||||
<code
|
||||
>bal[70]</code> : variation of the balance of account 70 over
|
||||
the period (it is the same as <code
|
||||
>balp[70]</code>. </li>
|
||||
<li>
|
||||
<code
|
||||
>bali[70,60]</code> : initial balance of accounts 70 and 60. </li>
|
||||
<li>
|
||||
<code
|
||||
>bale[1%%]</code> : balance of accounts starting with 1 at
|
||||
end of period. </li>
|
||||
<li>
|
||||
<code
|
||||
>crdp[40%]</code> : sum of all credits on accounts starting
|
||||
with 40 during the period. </li>
|
||||
<li>
|
||||
<code>
|
||||
debp[55%][('journal_id.code', '=',
|
||||
'BNK1')]
|
||||
</code>
|
||||
: sum of all debits on accounts 55 and journal BNK1 during
|
||||
the period. </li>
|
||||
<li>
|
||||
<code>
|
||||
balp[('user_type_id', '=',
|
||||
ref('account.
|
||||
data_account_type_receivable').id)][]
|
||||
</code>
|
||||
: variation of the balance of all receivable accounts over
|
||||
the period. </li>
|
||||
<li>
|
||||
<code>
|
||||
balp[][('tax_line_id.tag_ids', '=', ref('l10n_be.tax_tag_56').id)]
|
||||
</code>
|
||||
: balance of move lines related to tax grid 56. </li>
|
||||
<li>
|
||||
<code
|
||||
>pbale[55%]</code> : sum of all ending balances of accounts
|
||||
starting with 55 whose ending balance is positive. </li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</page>
|
||||
</notebook>
|
||||
</form>
|
||||
</field>
|
||||
</record>
|
||||
<record model="ir.actions.act_window" id="mis_report_view_action">
|
||||
<field name="name">MIS Report Templates</field>
|
||||
<field name="view_id" ref="mis_report_view_tree" />
|
||||
<field name="res_model">mis.report</field>
|
||||
<field name="view_mode">tree,form</field>
|
||||
</record>
|
||||
<menuitem
|
||||
id="mis_report_conf_menu"
|
||||
parent="account.menu_finance_configuration"
|
||||
name="MIS Reporting"
|
||||
sequence="90"
|
||||
/>
|
||||
<menuitem
|
||||
id="mis_report_view_menu"
|
||||
parent="mis_report_conf_menu"
|
||||
name="MIS Report Templates"
|
||||
action="mis_report_view_action"
|
||||
sequence="21"
|
||||
/>
|
||||
</odoo>
|
||||
|
|
@ -1,423 +0,0 @@
|
|||
<?xml version="1.0" encoding="UTF-8" ?>
|
||||
<odoo>
|
||||
<record model="ir.ui.view" id="mis_report_instance_result_view_form">
|
||||
<field name="name">mis.report.instance.result.view.form</field>
|
||||
<field name="model">mis.report.instance</field>
|
||||
<field name="priority" eval="20 " />
|
||||
<field name="arch" type="xml">
|
||||
<form
|
||||
string="MIS Report Preview"
|
||||
edit="false"
|
||||
create="false"
|
||||
delete="false"
|
||||
>
|
||||
<sheet>
|
||||
<field
|
||||
name="id"
|
||||
widget="mis_report_widget"
|
||||
nolabel="1"
|
||||
style="width:100%"
|
||||
/>
|
||||
</sheet>
|
||||
</form>
|
||||
</field>
|
||||
</record>
|
||||
<record model="ir.ui.view" id="mis_report_instance_view_tree">
|
||||
<field name="name">mis.report.instance.view.tree</field>
|
||||
<field name="model">mis.report.instance</field>
|
||||
<field name="arch" type="xml">
|
||||
<tree>
|
||||
<field name="sequence" widget="handle" />
|
||||
<button
|
||||
type="object"
|
||||
name="preview"
|
||||
string="Preview"
|
||||
icon="fa-search"
|
||||
/>
|
||||
<button type="object" name="print_pdf" string="Print" icon="fa-print" />
|
||||
<button
|
||||
type="object"
|
||||
name="export_xls"
|
||||
string="Export"
|
||||
icon="fa-download"
|
||||
/>
|
||||
<field name="name" />
|
||||
<field name="report_id" string="Template" />
|
||||
<field name="company_id" groups="base.group_multi_company" />
|
||||
<field name="multi_company" groups="base.group_multi_company" />
|
||||
<field name="currency_id" groups="base.group_multi_currency" />
|
||||
<field name="target_move" />
|
||||
<field name="pivot_date" />
|
||||
</tree>
|
||||
</field>
|
||||
</record>
|
||||
<record model="ir.ui.view" id="mis_report_instance_view_form">
|
||||
<field name="name">mis.report.instance.view.form</field>
|
||||
<field name="model">mis.report.instance</field>
|
||||
<field name="priority" eval="15" />
|
||||
<field name="arch" type="xml">
|
||||
<form string="MIS Report Instance">
|
||||
<sheet>
|
||||
<field name="temporary" invisible="1" />
|
||||
<div class="oe_right oe_button_box" name="buttons">
|
||||
<button
|
||||
type="object"
|
||||
name="preview"
|
||||
string="Preview"
|
||||
icon="fa-search"
|
||||
/>
|
||||
<button
|
||||
type="object"
|
||||
name="print_pdf"
|
||||
string="Print"
|
||||
icon="fa-print"
|
||||
/>
|
||||
<button
|
||||
type="object"
|
||||
name="export_xls"
|
||||
string="Export"
|
||||
icon="fa-download"
|
||||
/>
|
||||
<button
|
||||
type="action"
|
||||
name="%(mis_report_instance_add_to_dashboard_action)d"
|
||||
string="Add to dashboard"
|
||||
icon="fa-plus"
|
||||
attrs="{'invisible': [('temporary', '=', True)]}"
|
||||
/>
|
||||
<button
|
||||
type="object"
|
||||
name="save_report"
|
||||
string="Save"
|
||||
icon="fa-save"
|
||||
attrs="{'invisible': [('temporary', '=', False)]}"
|
||||
/>
|
||||
</div>
|
||||
<div class="oe_title">
|
||||
<div class="oe_edit_only">
|
||||
<label for="name" />
|
||||
</div>
|
||||
<h1>
|
||||
<field name="name" placeholder="Name" />
|
||||
</h1>
|
||||
<field name="description" />
|
||||
</div>
|
||||
<group>
|
||||
<group>
|
||||
<field name="report_id" string="Template" />
|
||||
<field
|
||||
name="currency_id"
|
||||
groups="base.group_multi_currency"
|
||||
/>
|
||||
<field name="comparison_mode" />
|
||||
</group>
|
||||
<group>
|
||||
<group
|
||||
name="simple_mode"
|
||||
attrs="{'invisible': [('comparison_mode', '=', True)]}"
|
||||
colspan="4"
|
||||
>
|
||||
<field name="date_range_id" />
|
||||
<field
|
||||
name="date_from"
|
||||
attrs="{'required': [('comparison_mode', '=', False)]}"
|
||||
/>
|
||||
<field
|
||||
name="date_to"
|
||||
attrs="{'required': [('comparison_mode', '=', False)]}"
|
||||
/>
|
||||
</group>
|
||||
</group>
|
||||
</group>
|
||||
<notebook>
|
||||
<page
|
||||
string="Columns"
|
||||
attrs="{'invisible': [('comparison_mode', '=', False)]}"
|
||||
>
|
||||
<group>
|
||||
<group>
|
||||
<field name="date" />
|
||||
</group>
|
||||
<group>
|
||||
|
||||
</group>
|
||||
<field
|
||||
name="period_ids"
|
||||
nolabel="1"
|
||||
colspan="4"
|
||||
attrs="{'required': [('comparison_mode', '=', True)]}"
|
||||
context="{'default_report_instance_id': id}"
|
||||
>
|
||||
<tree decoration-danger="not valid">
|
||||
<field name="sequence" widget="handle" />
|
||||
<field name="name" />
|
||||
<field name="source" />
|
||||
<field name="source_aml_model_id" />
|
||||
<field name="date_from" />
|
||||
<field name="date_to" />
|
||||
<field name="valid" invisible="1" />
|
||||
</tree>
|
||||
</field>
|
||||
</group>
|
||||
</page>
|
||||
<page string="Filters">
|
||||
<group name="filters">
|
||||
<field name="target_move" widget="radio" />
|
||||
<field
|
||||
name="multi_company"
|
||||
groups="base.group_multi_company"
|
||||
/>
|
||||
<field
|
||||
name="company_id"
|
||||
groups="base.group_multi_company"
|
||||
attrs="{'required': [('multi_company', '=', False)], 'invisible': [('multi_company', '=', True)]}"
|
||||
/>
|
||||
<field
|
||||
name="company_ids"
|
||||
groups="base.group_multi_company"
|
||||
widget="many2many_tags"
|
||||
attrs="{'invisible': [('multi_company', '=', False)]}"
|
||||
/>
|
||||
<field
|
||||
name="query_company_ids"
|
||||
groups="base.group_multi_company"
|
||||
widget="many2many_tags"
|
||||
/>
|
||||
<field name="source_aml_model_name" invisible="1" />
|
||||
<field
|
||||
name="analytic_domain"
|
||||
widget="domain"
|
||||
options="{'model': 'source_aml_model_name'}"
|
||||
/>
|
||||
</group>
|
||||
</page>
|
||||
<page string="Layout">
|
||||
<group name="layout">
|
||||
<field name="landscape_pdf" />
|
||||
<field name="no_auto_expand_accounts" />
|
||||
<field name="display_columns_description" />
|
||||
<field name="wide_display_by_default" />
|
||||
</group>
|
||||
</page>
|
||||
<page string="Widget">
|
||||
<group name="widget">
|
||||
<field name="widget_show_filters" />
|
||||
<field
|
||||
name="widget_search_view_id"
|
||||
attrs="{'invisible': [('widget_show_filters', '=', False)]}"
|
||||
/>
|
||||
<field name="widget_show_settings_button" />
|
||||
<field name="widget_show_pivot_date" />
|
||||
</group>
|
||||
</page>
|
||||
</notebook>
|
||||
</sheet>
|
||||
</form>
|
||||
</field>
|
||||
</record>
|
||||
<record model="ir.actions.act_window" id="mis_report_instance_view_action">
|
||||
<field name="name">MIS Reports</field>
|
||||
<field name="view_id" ref="mis_report_instance_view_tree" />
|
||||
<field name="res_model">mis.report.instance</field>
|
||||
<field name="view_mode">tree,form</field>
|
||||
<field name="domain">[('temporary', '=', False)]</field>
|
||||
</record>
|
||||
<menuitem
|
||||
id="mis_report_finance_menu"
|
||||
parent="account.menu_finance_reports"
|
||||
name="MIS Reporting"
|
||||
sequence="101"
|
||||
groups="account.group_account_manager"
|
||||
/>
|
||||
<menuitem
|
||||
id="mis_report_instance_view_menu"
|
||||
parent="mis_report_finance_menu"
|
||||
name="MIS Reports"
|
||||
action="mis_report_instance_view_action"
|
||||
sequence="10"
|
||||
/>
|
||||
<record id="wizard_mis_report_instance_view_form" model="ir.ui.view">
|
||||
<field name="model">mis.report.instance</field>
|
||||
<field name="priority">99</field>
|
||||
<field name="inherit_id" ref="mis_builder.mis_report_instance_view_form" />
|
||||
<field name="mode">primary</field>
|
||||
<field name="arch" type="xml">
|
||||
<field name="name" position="attributes">
|
||||
<attribute name="readonly">1</attribute>
|
||||
</field>
|
||||
<label for="name" position="replace" />
|
||||
<field name="report_id" position="attributes">
|
||||
<attribute name="invisible">1</attribute>
|
||||
</field>
|
||||
<div name="buttons" position="attributes">
|
||||
<attribute name="invisible">1</attribute>
|
||||
</div>
|
||||
<sheet position="after">
|
||||
<footer>
|
||||
<button
|
||||
type="object"
|
||||
name="save_report"
|
||||
string="Save"
|
||||
icon="fa-save"
|
||||
/>
|
||||
<button
|
||||
type="object"
|
||||
name="preview"
|
||||
string="Preview"
|
||||
icon="fa-search"
|
||||
/>
|
||||
<button
|
||||
type="object"
|
||||
name="print_pdf"
|
||||
string="Print"
|
||||
icon="fa-print"
|
||||
/>
|
||||
<button
|
||||
type="object"
|
||||
name="export_xls"
|
||||
string="Export"
|
||||
icon="fa-download"
|
||||
/> or <button string="Cancel" class="oe_link" special="cancel" />
|
||||
</footer>
|
||||
</sheet>
|
||||
</field>
|
||||
</record>
|
||||
<record model="ir.actions.act_window" id="last_mis_report_instance_view_action">
|
||||
<field name="name">Last Reports Generated</field>
|
||||
<field name="view_id" ref="mis_report_instance_view_tree" />
|
||||
<field name="res_model">mis.report.instance</field>
|
||||
<field name="view_mode">tree,form</field>
|
||||
<field name="domain">[('temporary', '=', True)]</field>
|
||||
</record>
|
||||
<menuitem
|
||||
id="last_wizard_mis_report_instance_view_menu"
|
||||
parent="mis_report_finance_menu"
|
||||
name="Last Reports Generated"
|
||||
action="last_mis_report_instance_view_action"
|
||||
sequence="20"
|
||||
/>
|
||||
<record model="ir.ui.view" id="mis_report_instance_period_view_form">
|
||||
<field name="model">mis.report.instance.period</field>
|
||||
<field name="priority" eval="16" />
|
||||
<field name="arch" type="xml">
|
||||
<form>
|
||||
<group col="4">
|
||||
<field name="name" placeholder="Name" />
|
||||
<field
|
||||
name="subkpi_ids"
|
||||
domain="[('report_id', '=', parent.report_id)]"
|
||||
widget="many2many_tags"
|
||||
options="{'no_create': True}"
|
||||
/>
|
||||
<field name="valid" invisible="1" />
|
||||
<field
|
||||
name="report_instance_id"
|
||||
invisible="1"
|
||||
attrs="{'required': [('id', '!=', False)]}"
|
||||
/>
|
||||
<field name="report_id" invisible="1" />
|
||||
<field name="id" invisible="1" />
|
||||
</group>
|
||||
<group string="Source" col="4">
|
||||
<group colspan="2" name="source">
|
||||
<field name="source" />
|
||||
</group>
|
||||
<group col="2" colspan="2" name="source_data">
|
||||
<field
|
||||
name="source_aml_model_id"
|
||||
attrs="{'invisible': [('source', '!=', 'actuals_alt')], 'required': [('source', '==', 'actuals_alt')]}"
|
||||
/>
|
||||
<field name="source_aml_model_name" invisible="1" />
|
||||
<field
|
||||
name="source_sumcol_ids"
|
||||
attrs="{'invisible': [('source', '!=', 'sumcol')]}"
|
||||
nolabel="1"
|
||||
colspan="2"
|
||||
>
|
||||
<tree editable="bottom">
|
||||
<field name="sign" />
|
||||
<field
|
||||
name="period_to_sum_id"
|
||||
domain="[('report_instance_id', '=', parent.report_instance_id), ('id', '!=', parent.id)]"
|
||||
options="{'no_create': True, 'no_open': True}"
|
||||
/>
|
||||
</tree>
|
||||
</field>
|
||||
<field
|
||||
name="source_sumcol_accdet"
|
||||
attrs="{'invisible': [('source', '!=', 'sumcol')]}"
|
||||
/>
|
||||
<field name="allowed_cmpcol_ids" invisible="1" />
|
||||
<field
|
||||
name="source_cmpcol_to_id"
|
||||
attrs="{'invisible': [('source', '!=', 'cmpcol')], 'required': [('source', '=', 'cmpcol')]}"
|
||||
domain="[('id', 'in', allowed_cmpcol_ids)]"
|
||||
options="{'no_create': True, 'no_open': True}"
|
||||
/>
|
||||
<field
|
||||
name="source_cmpcol_from_id"
|
||||
attrs="{'invisible': [('source', '!=', 'cmpcol')], 'required': [('source', '=', 'cmpcol')]}"
|
||||
domain="[('id', 'in', allowed_cmpcol_ids)]"
|
||||
options="{'no_create': True, 'no_open': True}"
|
||||
/>
|
||||
</group>
|
||||
</group>
|
||||
<group string="Dates">
|
||||
<group colspan="4">
|
||||
<field name="mode" widget="radio" />
|
||||
</group>
|
||||
<group
|
||||
name="relative"
|
||||
attrs="{'invisible': [('mode', '!=', 'relative')]}"
|
||||
colspan="4"
|
||||
>
|
||||
<group>
|
||||
<field
|
||||
name="type"
|
||||
attrs="{'required': [('mode', '=', 'relative')]}"
|
||||
/>
|
||||
<field name="is_ytd" />
|
||||
<field
|
||||
name="date_range_type_id"
|
||||
attrs="{'invisible': [('type', '!=', 'date_range')], 'required': [('type', '=', 'date_range'), ('mode', '=', 'relative')]}"
|
||||
/>
|
||||
<field name="offset" />
|
||||
<field name="duration" />
|
||||
</group>
|
||||
<group>
|
||||
<field name="date_from" />
|
||||
<field name="date_to" />
|
||||
</group>
|
||||
</group>
|
||||
<group
|
||||
name="fix"
|
||||
attrs="{'invisible': [('mode', '!=', 'fix')]}"
|
||||
colspan="4"
|
||||
>
|
||||
<group>
|
||||
<field name="date_range_id" />
|
||||
</group>
|
||||
<group>
|
||||
<field
|
||||
name="manual_date_from"
|
||||
attrs="{'required': [('mode', '=', 'fix')]}"
|
||||
/>
|
||||
<field
|
||||
name="manual_date_to"
|
||||
attrs="{'required': [('mode', '=', 'fix')]}"
|
||||
/>
|
||||
</group>
|
||||
</group>
|
||||
</group>
|
||||
<group string="Filters">
|
||||
<field
|
||||
name="analytic_domain"
|
||||
widget="domain"
|
||||
options="{'model': 'source_aml_model_name'}"
|
||||
attrs="{'invisible': [('source_aml_model_name', '=', False)]}"
|
||||
/>
|
||||
</group>
|
||||
</form>
|
||||
</field>
|
||||
</record>
|
||||
</odoo>
|
||||
|
|
@ -1,110 +0,0 @@
|
|||
<?xml version="1.0" encoding="UTF-8" ?>
|
||||
<odoo>
|
||||
<record model="ir.ui.view" id="mis_report_style_view_tree">
|
||||
<field name="name">mis.report.style.view.tree</field>
|
||||
<field name="model">mis.report.style</field>
|
||||
<field name="arch" type="xml">
|
||||
<tree>
|
||||
<field name="name" />
|
||||
</tree>
|
||||
</field>
|
||||
</record>
|
||||
<record id="mis_report_style_view_form" model="ir.ui.view">
|
||||
<field name="name">mis.report.style.view.form</field>
|
||||
<field name="model">mis.report.style</field>
|
||||
<field name="arch" type="xml">
|
||||
<form string="MIS Report Style">
|
||||
<sheet>
|
||||
<group string="Style" col="2">
|
||||
<field name="name" />
|
||||
</group>
|
||||
<group string="Number" col="4">
|
||||
<field name="dp_inherit" string="Rounding inherit" />
|
||||
<field
|
||||
name="dp"
|
||||
attrs="{'invisible': [('dp_inherit', '=', True)]}"
|
||||
/>
|
||||
<field name="divider_inherit" string="Factor inherit" />
|
||||
<field
|
||||
name="divider"
|
||||
attrs="{'invisible': [('divider_inherit', '=', True)]}"
|
||||
/>
|
||||
<field name="prefix_inherit" />
|
||||
<field
|
||||
name="prefix"
|
||||
attrs="{'invisible': [('prefix_inherit', '=', True)]}"
|
||||
/>
|
||||
<field name="suffix_inherit" />
|
||||
<field
|
||||
name="suffix"
|
||||
attrs="{'invisible': [('suffix_inherit', '=', True)]}"
|
||||
/>
|
||||
</group>
|
||||
<group string="Color" col="4">
|
||||
<field name="color_inherit" />
|
||||
<field
|
||||
name="color"
|
||||
attrs="{'invisible': [('color_inherit', '=', True)]}"
|
||||
widget="color"
|
||||
/>
|
||||
<field name="background_color_inherit" />
|
||||
<field
|
||||
name="background_color"
|
||||
attrs="{'invisible': [('background_color_inherit', '=', True)]}"
|
||||
widget="color"
|
||||
/>
|
||||
</group>
|
||||
<group string="Font" col="4">
|
||||
<field name="font_style_inherit" />
|
||||
<field
|
||||
name="font_style"
|
||||
attrs="{'invisible': [('font_style_inherit', '=', True)]}"
|
||||
/>
|
||||
<field name="font_weight_inherit" />
|
||||
<field
|
||||
name="font_weight"
|
||||
attrs="{'invisible': [('font_weight_inherit', '=', True)]}"
|
||||
/>
|
||||
<field name="font_size_inherit" />
|
||||
<field
|
||||
name="font_size"
|
||||
attrs="{'invisible': [('font_size_inherit', '=', True)]}"
|
||||
/>
|
||||
</group>
|
||||
<group string="Indent" col="4">
|
||||
<field name="indent_level_inherit" />
|
||||
<field
|
||||
name="indent_level"
|
||||
attrs="{'invisible': [('indent_level_inherit', '=', True)]}"
|
||||
/>
|
||||
</group>
|
||||
<group string="Visibility" col="4">
|
||||
<field name="hide_empty_inherit" />
|
||||
<field
|
||||
name="hide_empty"
|
||||
attrs="{'invisible': [('hide_empty_inherit', '=', True)]}"
|
||||
/>
|
||||
<field name="hide_always_inherit" />
|
||||
<field
|
||||
name="hide_always"
|
||||
attrs="{'invisible': [('hide_always_inherit', '=', True)]}"
|
||||
/>
|
||||
</group>
|
||||
</sheet>
|
||||
</form>
|
||||
</field>
|
||||
</record>
|
||||
<record model="ir.actions.act_window" id="mis_report_style_view_action">
|
||||
<field name="name">MIS Report Styles</field>
|
||||
<field name="view_id" ref="mis_report_style_view_tree" />
|
||||
<field name="res_model">mis.report.style</field>
|
||||
<field name="view_mode">tree,form</field>
|
||||
</record>
|
||||
<menuitem
|
||||
id="mis_report_style_view_menu"
|
||||
parent="mis_report_conf_menu"
|
||||
name="MIS Report Styles"
|
||||
action="mis_report_style_view_action"
|
||||
sequence="22"
|
||||
/>
|
||||
</odoo>
|
||||
|
|
@ -1,4 +0,0 @@
|
|||
# Copyright 2014 ACSONE SA/NV (<http://acsone.eu>)
|
||||
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html).
|
||||
|
||||
from . import mis_builder_dashboard
|
||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue