Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
141 changes: 141 additions & 0 deletions mis_builder_analytic_source/README.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
.. image:: https://odoo-community.org/readme-banner-image
:target: https://odoo-community.org/get-involved?utm_source=readme
:alt: Odoo Community Association

===========================
MIS Builder Analytic Source
===========================

..
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
!! This file is generated by oca-gen-addon-readme !!
!! changes will be overwritten. !!
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
!! source digest: sha256:64495842204e2ceae230409ca441ed29402db0bbb009a98ca939e7d90aa1d61e
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!

.. |badge1| image:: https://img.shields.io/badge/maturity-Alpha-red.png
:target: https://odoo-community.org/page/development-status
:alt: Alpha
.. |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--contrib-lightgray.png?logo=github
:target: https://github.com/OCA/mis-builder-contrib/tree/16.0/mis_builder_analytic_source
:alt: OCA/mis-builder-contrib
.. |badge4| image:: https://img.shields.io/badge/weblate-Translate%20me-F47D42.png
:target: https://translation.odoo-community.org/projects/mis-builder-contrib-16-0/mis-builder-contrib-16-0-mis_builder_analytic_source
: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-contrib&target_branch=16.0
:alt: Try me on Runboat

|badge1| |badge2| |badge3| |badge4| |badge5|

Since Odoo 16 a journal item no longer has one analytic account: it has an
``analytic_distribution`` that can split it between several accounts. The
analytic filter of a MIS report on journal items keeps the **full** balance of
every item whose distribution contains the analytic account, so a P&L per
project, contract or department is overstated whenever an item is split.
Costs that only exist as analytic lines, such as timesheets, are not seen at
all.

This module adds ``mis.analytic.move.line``, a SQL view over
``account.analytic.line`` with the fields MIS Builder expects from a move lines
source (``account_id``, ``company_id``, ``date``, ``debit``, ``credit``,
``parent_state``, ``analytic_distribution``). Analytic lines already carry the
prorated amount, one line per analytic account, so a MIS report that uses this
model as its move lines source gives the exact figures per analytic account,
including timesheet costs.

.. IMPORTANT::
This is an alpha version, the data model and design can change at any time without warning.
Only for development or testing purpose, do not use in production.
`More details on development status <https://odoo-community.org/page/development-status>`_

**Table of contents**

.. contents::
:local:

Configuration
=============

Analytic lines without a financial account (timesheets, manual analytic
entries) are reported on the company's *MIS Analytic Fallback Account*
(Invoicing > Configuration > Settings, Analytics section). When it is not set,
the first *Cost of Revenue* account of the company (by code) is used.

Usage
=====

#. In the MIS report template, set *Move lines source* to *Analytic line as MIS
move line*.
#. Use the analytic filter of the report instance, or an analytic domain per
column, for example ``[("analytic_account_id", "=", 42)]``.
#. Expressions can also filter on the origin of the line, for example
``balp[5%][('line_origin', '=', 'timesheet')]`` or
``-balp[6%][('category', '=', 'invoice')]``.

The sign follows journal items: a positive analytic amount (revenue) is a
credit and a negative one (cost) is a debit, so the usual ``-balp[...]``
expressions for income keep working.

Known issues / Roadmap
======================

* Analytic lines are always in the company currency; amounts in another
currency are not converted.
* Draft journal items have no analytic lines, so the *posted and draft* target
move option shows the same figures as *posted only*. Manual analytic entries
and timesheets count as posted.

Bug Tracker
===========

Bugs are tracked on `GitHub Issues <https://github.com/OCA/mis-builder-contrib/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-contrib/issues/new?body=module:%20mis_builder_analytic_source%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
~~~~~~~

* KMEE

Contributors
~~~~~~~~~~~~

* `KMEE <https://www.kmee.com.br>`_:

* Luis Felipe Mileo <mileo@kmee.com.br>

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-mileo| image:: https://github.com/mileo.png?size=40px
:target: https://github.com/mileo
:alt: mileo

Current `maintainer <https://odoo-community.org/page/maintainer-role>`__:

|maintainer-mileo|

This module is part of the `OCA/mis-builder-contrib <https://github.com/OCA/mis-builder-contrib/tree/16.0/mis_builder_analytic_source>`_ project on GitHub.

You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute.
1 change: 1 addition & 0 deletions mis_builder_analytic_source/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
from . import models
22 changes: 22 additions & 0 deletions mis_builder_analytic_source/__manifest__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
# Copyright 2026 KMEE
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
{
"name": "MIS Builder Analytic Source",
"summary": "Analytic lines as the move lines source of MIS reports, so "
"analytic filters give the prorated amounts",
"version": "16.0.1.0.0",
"development_status": "Alpha",
"category": "Reporting",
"author": "KMEE, Odoo Community Association (OCA)",
"maintainers": ["mileo"],
"website": "https://github.com/OCA/mis-builder-contrib",
"license": "AGPL-3",
"depends": ["mis_builder", "account"],
"data": [
"security/ir.model.access.csv",
"security/ir_rule.xml",
"views/mis_analytic_move_line_views.xml",
"views/res_config_settings_views.xml",
],
"installable": True,
}
3 changes: 3 additions & 0 deletions mis_builder_analytic_source/models/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
from . import mis_analytic_move_line
from . import res_company
from . import res_config_settings
157 changes: 157 additions & 0 deletions mis_builder_analytic_source/models/mis_analytic_move_line.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,157 @@
# Copyright 2026 KMEE
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
"""SQL view exposing account.analytic.line with the shape MIS Builder expects from a
move lines source (account_id, company_id, date, debit, credit, parent_state and a
one-account analytic_distribution).

Why: MIS Builder's analytic filter on account.move.line keeps the FULL balance of any
line that merely contains the analytic account in its distribution. Analytic lines are
already prorated (one line per account, with the distributed amount), so a report whose
move_lines_source is this model gives exact figures per analytic account/project, and it
also sees costs that never hit the general ledger as separate lines (timesheets and
manual analytic entries).

Sign convention follows account.move.line: analytic amount > 0 (revenue) becomes credit,
amount < 0 (cost) becomes debit, so ``balance = debit - credit = -amount`` and the usual
``-balp[...]`` expressions for income keep working.
"""

from psycopg2 import sql

from odoo import fields, models, tools


class MisAnalyticMoveLine(models.Model):
_name = "mis.analytic.move.line"
_description = "Analytic line as MIS move line"
_inherit = ["analytic.mixin"]
_auto = False
_order = "date desc, id desc"

name = fields.Char(readonly=True)
date = fields.Date(readonly=True)
company_id = fields.Many2one("res.company", readonly=True)
company_currency_id = fields.Many2one(
"res.currency", related="company_id.currency_id", readonly=True
)
account_id = fields.Many2one(
"account.account", string="Financial Account", readonly=True
)
analytic_account_id = fields.Many2one("account.analytic.account", readonly=True)
plan_id = fields.Many2one("account.analytic.plan", readonly=True)
partner_id = fields.Many2one("res.partner", readonly=True)
product_id = fields.Many2one("product.product", readonly=True)
category = fields.Selection(
selection=lambda self: (
self.env["account.analytic.line"]._fields["category"].selection
),
readonly=True,
)
line_origin = fields.Selection(
selection=[
("journal", "Journal Item"),
("timesheet", "Timesheet"),
("manual", "Manual Analytic Entry"),
],
readonly=True,
help="Journal Item: analytic line generated by a posted journal item "
"(invoices, bills, entries). Timesheet: line linked to a project "
"(hr_timesheet). Manual: any other analytic line.",
)
is_timesheet = fields.Boolean(readonly=True)
move_line_id = fields.Many2one("account.move.line", readonly=True)
analytic_line_id = fields.Many2one("account.analytic.line", readonly=True)
parent_state = fields.Selection(
[("draft", "Draft"), ("posted", "Posted"), ("cancel", "Cancelled")],
readonly=True,
)
unit_amount = fields.Float(string="Quantity", readonly=True)
debit = fields.Monetary(currency_field="company_currency_id", readonly=True)
credit = fields.Monetary(currency_field="company_currency_id", readonly=True)
balance = fields.Monetary(currency_field="company_currency_id", readonly=True)
amount = fields.Monetary(
string="Analytic Amount", currency_field="company_currency_id", readonly=True
)

def _timesheet_sql(self):
"""Timesheets are analytic lines with a project (hr_timesheet). The column only
exists when hr_timesheet is installed, so build the expression defensively."""
self.env.cr.execute(
"""
SELECT 1 FROM information_schema.columns
WHERE table_name = 'account_analytic_line' AND column_name = 'project_id'
"""
)
if self.env.cr.fetchone():
return "aal.project_id IS NOT NULL"
return "FALSE"

def init(self):
cr = self.env.cr
tools.drop_view_if_exists(cr, self._table)
# In 16.0 models are initialized one by one (_auto_init + init), and this view
# may come before res.company: create the column read by the view up front
# (res.company's _auto_init then only adds the foreign key).
if not tools.column_exists(
cr, "res_company", "mis_analytic_fallback_account_id"
):
tools.create_column(
cr, "res_company", "mis_analytic_fallback_account_id", "int4"
)
# Lines without a general account (timesheets, manual entries) fall back to
# the company's configured account or, when not set, to its first
# "Cost of Revenue" account, so they still show up in a P&L.
query = sql.SQL(
"""
CREATE OR REPLACE VIEW {table} AS (
SELECT
aal.id AS id,
aal.name AS name,
aal.date AS date,
aal.company_id AS company_id,
COALESCE(
aal.general_account_id,
rc.mis_analytic_fallback_account_id,
(
SELECT a.id
FROM account_account a
WHERE a.company_id = aal.company_id
AND a.account_type = 'expense_direct_cost'
ORDER BY a.code, a.id
LIMIT 1
)
) AS account_id,
aal.account_id AS analytic_account_id,
aal.plan_id AS plan_id,
aal.partner_id AS partner_id,
aal.product_id AS product_id,
aal.category AS category,
CASE
WHEN aal.move_line_id IS NOT NULL THEN 'journal'
WHEN {timesheet} THEN 'timesheet'
ELSE 'manual'
END AS line_origin,
({timesheet}) AS is_timesheet,
aal.move_line_id AS move_line_id,
aal.id AS analytic_line_id,
COALESCE(am.state, 'posted') AS parent_state,
aal.unit_amount AS unit_amount,
CASE WHEN aal.amount < 0 THEN -aal.amount ELSE 0 END AS debit,
CASE WHEN aal.amount > 0 THEN aal.amount ELSE 0 END AS credit,
-aal.amount AS balance,
aal.amount AS amount,
jsonb_build_object(aal.account_id::text, 100)
AS analytic_distribution
FROM account_analytic_line aal
JOIN res_company rc ON rc.id = aal.company_id
LEFT JOIN account_move_line aml ON aml.id = aal.move_line_id
LEFT JOIN account_move am ON am.id = aml.move_id
WHERE aal.account_id IS NOT NULL
)
"""
).format(
table=sql.Identifier(self._table),
# constant expression chosen by _timesheet_sql(), never user input
timesheet=sql.SQL(self._timesheet_sql()),
)
cr.execute(query)
16 changes: 16 additions & 0 deletions mis_builder_analytic_source/models/res_company.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
# Copyright 2026 KMEE
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
from odoo import fields, models


class ResCompany(models.Model):
_inherit = "res.company"

mis_analytic_fallback_account_id = fields.Many2one(
comodel_name="account.account",
string="MIS Analytic Fallback Account",
domain="[('company_id', '=', id), ('deprecated', '=', False)]",
help="Financial account used by the MIS analytic source for analytic lines "
"without a general account (e.g. timesheets). When empty, the company's "
"first 'Cost of Revenue' account is used.",
)
12 changes: 12 additions & 0 deletions mis_builder_analytic_source/models/res_config_settings.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
# Copyright 2026 KMEE
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
from odoo import fields, models


class ResConfigSettings(models.TransientModel):
_inherit = "res.config.settings"

mis_analytic_fallback_account_id = fields.Many2one(
related="company_id.mis_analytic_fallback_account_id",
readonly=False,
)
4 changes: 4 additions & 0 deletions mis_builder_analytic_source/readme/CONFIGURE.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
Analytic lines without a financial account (timesheets, manual analytic
entries) are reported on the company's *MIS Analytic Fallback Account*
(Invoicing > Configuration > Settings, Analytics section). When it is not set,
the first *Cost of Revenue* account of the company (by code) is used.
3 changes: 3 additions & 0 deletions mis_builder_analytic_source/readme/CONTRIBUTORS.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
* `KMEE <https://www.kmee.com.br>`_:

* Luis Felipe Mileo <mileo@kmee.com.br>
15 changes: 15 additions & 0 deletions mis_builder_analytic_source/readme/DESCRIPTION.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
Since Odoo 16 a journal item no longer has one analytic account: it has an
``analytic_distribution`` that can split it between several accounts. The
analytic filter of a MIS report on journal items keeps the **full** balance of
every item whose distribution contains the analytic account, so a P&L per
project, contract or department is overstated whenever an item is split.
Costs that only exist as analytic lines, such as timesheets, are not seen at
all.

This module adds ``mis.analytic.move.line``, a SQL view over
``account.analytic.line`` with the fields MIS Builder expects from a move lines
source (``account_id``, ``company_id``, ``date``, ``debit``, ``credit``,
``parent_state``, ``analytic_distribution``). Analytic lines already carry the
prorated amount, one line per analytic account, so a MIS report that uses this
model as its move lines source gives the exact figures per analytic account,
including timesheet costs.
5 changes: 5 additions & 0 deletions mis_builder_analytic_source/readme/ROADMAP.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
* Analytic lines are always in the company currency; amounts in another
currency are not converted.
* Draft journal items have no analytic lines, so the *posted and draft* target
move option shows the same figures as *posted only*. Manual analytic entries
and timesheets count as posted.
Loading
Loading