diff --git a/mis_builder_analytic_source/README.rst b/mis_builder_analytic_source/README.rst new file mode 100644 index 0000000..a2861af --- /dev/null +++ b/mis_builder_analytic_source/README.rst @@ -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 `_ + +**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 `_. +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 `_. + +Do not contact contributors directly about support or help with technical issues. + +Credits +======= + +Authors +~~~~~~~ + +* KMEE + +Contributors +~~~~~~~~~~~~ + +* `KMEE `_: + + * Luis Felipe Mileo + +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 `__: + +|maintainer-mileo| + +This module is part of the `OCA/mis-builder-contrib `_ project on GitHub. + +You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute. diff --git a/mis_builder_analytic_source/__init__.py b/mis_builder_analytic_source/__init__.py new file mode 100644 index 0000000..0650744 --- /dev/null +++ b/mis_builder_analytic_source/__init__.py @@ -0,0 +1 @@ +from . import models diff --git a/mis_builder_analytic_source/__manifest__.py b/mis_builder_analytic_source/__manifest__.py new file mode 100644 index 0000000..56451f2 --- /dev/null +++ b/mis_builder_analytic_source/__manifest__.py @@ -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, +} diff --git a/mis_builder_analytic_source/models/__init__.py b/mis_builder_analytic_source/models/__init__.py new file mode 100644 index 0000000..f42ab96 --- /dev/null +++ b/mis_builder_analytic_source/models/__init__.py @@ -0,0 +1,3 @@ +from . import mis_analytic_move_line +from . import res_company +from . import res_config_settings diff --git a/mis_builder_analytic_source/models/mis_analytic_move_line.py b/mis_builder_analytic_source/models/mis_analytic_move_line.py new file mode 100644 index 0000000..6cc2526 --- /dev/null +++ b/mis_builder_analytic_source/models/mis_analytic_move_line.py @@ -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) diff --git a/mis_builder_analytic_source/models/res_company.py b/mis_builder_analytic_source/models/res_company.py new file mode 100644 index 0000000..4c24ca4 --- /dev/null +++ b/mis_builder_analytic_source/models/res_company.py @@ -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.", + ) diff --git a/mis_builder_analytic_source/models/res_config_settings.py b/mis_builder_analytic_source/models/res_config_settings.py new file mode 100644 index 0000000..8a208f9 --- /dev/null +++ b/mis_builder_analytic_source/models/res_config_settings.py @@ -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, + ) diff --git a/mis_builder_analytic_source/readme/CONFIGURE.rst b/mis_builder_analytic_source/readme/CONFIGURE.rst new file mode 100644 index 0000000..03cfc16 --- /dev/null +++ b/mis_builder_analytic_source/readme/CONFIGURE.rst @@ -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. diff --git a/mis_builder_analytic_source/readme/CONTRIBUTORS.rst b/mis_builder_analytic_source/readme/CONTRIBUTORS.rst new file mode 100644 index 0000000..66b6742 --- /dev/null +++ b/mis_builder_analytic_source/readme/CONTRIBUTORS.rst @@ -0,0 +1,3 @@ +* `KMEE `_: + + * Luis Felipe Mileo diff --git a/mis_builder_analytic_source/readme/DESCRIPTION.rst b/mis_builder_analytic_source/readme/DESCRIPTION.rst new file mode 100644 index 0000000..5dfe1f6 --- /dev/null +++ b/mis_builder_analytic_source/readme/DESCRIPTION.rst @@ -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. diff --git a/mis_builder_analytic_source/readme/ROADMAP.rst b/mis_builder_analytic_source/readme/ROADMAP.rst new file mode 100644 index 0000000..9d920cf --- /dev/null +++ b/mis_builder_analytic_source/readme/ROADMAP.rst @@ -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. diff --git a/mis_builder_analytic_source/readme/USAGE.rst b/mis_builder_analytic_source/readme/USAGE.rst new file mode 100644 index 0000000..befd1eb --- /dev/null +++ b/mis_builder_analytic_source/readme/USAGE.rst @@ -0,0 +1,11 @@ +#. 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. diff --git a/mis_builder_analytic_source/security/ir.model.access.csv b/mis_builder_analytic_source/security/ir.model.access.csv new file mode 100644 index 0000000..9544918 --- /dev/null +++ b/mis_builder_analytic_source/security/ir.model.access.csv @@ -0,0 +1,2 @@ +id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink +access_mis_analytic_move_line,mis.analytic.move.line read,model_mis_analytic_move_line,account.group_account_readonly,1,0,0,0 diff --git a/mis_builder_analytic_source/security/ir_rule.xml b/mis_builder_analytic_source/security/ir_rule.xml new file mode 100644 index 0000000..424a611 --- /dev/null +++ b/mis_builder_analytic_source/security/ir_rule.xml @@ -0,0 +1,10 @@ + + + + + MIS analytic move line multi-company + + [('company_id', 'in', company_ids)] + + diff --git a/mis_builder_analytic_source/static/description/index.html b/mis_builder_analytic_source/static/description/index.html new file mode 100644 index 0000000..cb608aa --- /dev/null +++ b/mis_builder_analytic_source/static/description/index.html @@ -0,0 +1,488 @@ + + + + + +MIS Builder Analytic Source + + + +
+ + + +Odoo Community Association + +
+

MIS Builder Analytic Source

+ +

Alpha License: AGPL-3 OCA/mis-builder-contrib Translate me on Weblate Try me on Runboat

+

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

+
+

Table of contents

+ +
+

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

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

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. +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.

+

Do not contact contributors directly about support or help with technical issues.

+
+
+

Credits

+
+

Authors

+
    +
  • KMEE
  • +
+
+
+

Contributors

+ +
+
+

Maintainers

+

This module is maintained by the OCA.

+ +Odoo Community Association + +

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.

+

Current maintainer:

+

mileo

+

This module is part of the OCA/mis-builder-contrib project on GitHub.

+

You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute.

+
+
+
+
+ + diff --git a/mis_builder_analytic_source/tests/__init__.py b/mis_builder_analytic_source/tests/__init__.py new file mode 100644 index 0000000..1c4a706 --- /dev/null +++ b/mis_builder_analytic_source/tests/__init__.py @@ -0,0 +1 @@ +from . import test_mis_analytic_move_line diff --git a/mis_builder_analytic_source/tests/test_mis_analytic_move_line.py b/mis_builder_analytic_source/tests/test_mis_analytic_move_line.py new file mode 100644 index 0000000..0af7312 --- /dev/null +++ b/mis_builder_analytic_source/tests/test_mis_analytic_move_line.py @@ -0,0 +1,149 @@ +# Copyright 2026 KMEE +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +from odoo import Command, fields +from odoo.tests import tagged +from odoo.tools.safe_eval import safe_eval + +from odoo.addons.account.tests.common import AccountTestInvoicingCommon +from odoo.addons.mis_builder.models.accounting_none import AccountingNone +from odoo.addons.mis_builder.models.aep import AccountingExpressionProcessor as AEP + + +@tagged("post_install", "-at_install") +class TestMisAnalyticMoveLine(AccountTestInvoicingCommon): + @classmethod + def setUpClass(cls, chart_template_ref=None): + super().setUpClass(chart_template_ref=chart_template_ref) + cls.company = cls.company_data["company"] + plan = cls.env["account.analytic.plan"].create({"name": "Projects"}) + cls.project_a, cls.project_b = cls.env["account.analytic.account"].create( + [ + {"name": "Project A", "plan_id": plan.id, "company_id": cls.company.id}, + {"name": "Project B", "plan_id": plan.id, "company_id": cls.company.id}, + ] + ) + cls.expense = cls.company_data["default_account_expense"] + cls.date = fields.Date.from_string("2026-03-15") + # one expense of 1000 split 60/40 between the two projects + cls.move = cls.env["account.move"].create( + { + "move_type": "entry", + "journal_id": cls.company_data["default_journal_misc"].id, + "date": cls.date, + "line_ids": [ + Command.create( + { + "name": "Shared cost", + "account_id": cls.expense.id, + "debit": 1000.0, + "analytic_distribution": { + str(cls.project_a.id): 60, + str(cls.project_b.id): 40, + }, + } + ), + Command.create( + { + "name": "Shared cost", + "account_id": cls.company_data[ + "default_account_payable" + ].id, + "credit": 1000.0, + } + ), + ], + } + ) + cls.move.action_post() + cls.View = cls.env["mis.analytic.move.line"] + + def _balp(self, aml_model, domain): + aep = AEP(self.company) + aep.parse_expr(f"balp[{self.expense.code}]") + aep.done_parsing() + aep.do_queries( + date_from=self.date, + date_to=self.date, + additional_move_line_filter=domain, + aml_model=aml_model, + ) + return safe_eval( + aep.replace_expr(f"balp[{self.expense.code}]"), + {"AccountingNone": AccountingNone}, + ) + + def test_prorated_lines(self): + """One view line per analytic account, with the distributed amount.""" + lines = self.View.search([("move_line_id.move_id", "=", self.move.id)]) + by_project = {line.analytic_account_id: line for line in lines} + self.assertEqual(set(by_project), {self.project_a, self.project_b}) + line_a = by_project[self.project_a] + self.assertEqual(line_a.account_id, self.expense) + self.assertEqual(line_a.line_origin, "journal") + self.assertEqual(line_a.parent_state, "posted") + self.assertAlmostEqual(line_a.debit, 600.0) + self.assertAlmostEqual(line_a.credit, 0.0) + self.assertAlmostEqual(line_a.balance, 600.0) + self.assertAlmostEqual(by_project[self.project_b].balance, 400.0) + + def test_analytic_filter_full_vs_prorated(self): + """The MIS analytic filter on journal items keeps the full balance. + + The same filter on this source gives the prorated amount, without changing + the domain written by mis_builder. + """ + domain = [("analytic_distribution_search", "in", [self.project_a.id])] + self.assertAlmostEqual(self._balp("account.move.line", domain), 1000.0) + self.assertAlmostEqual(self._balp("mis.analytic.move.line", domain), 600.0) + domain_b = [("analytic_account_id", "=", self.project_b.id)] + self.assertAlmostEqual(self._balp("mis.analytic.move.line", domain_b), 400.0) + + def test_manual_line_fallback_account(self): + """A line without financial account falls back to a cost account.""" + manual = self.env["account.analytic.line"].create( + { + "name": "Manual cost", + "account_id": self.project_a.id, + "amount": -50.0, + "date": self.date, + "company_id": self.company.id, + } + ) + line = self.View.search([("analytic_line_id", "=", manual.id)]) + self.assertEqual(line.line_origin, "manual") + self.assertAlmostEqual(line.debit, 50.0) + first_cost = self.env["account.account"].search( + [ + ("company_id", "=", self.company.id), + ("account_type", "=", "expense_direct_cost"), + ], + order="code, id", + limit=1, + ) + self.assertEqual(line.account_id, first_cost) + fallback = self.company_data["default_account_expense"] + self.company.mis_analytic_fallback_account_id = fallback + self.env.flush_all() + self.View.invalidate_model() + self.assertEqual(line.account_id, fallback) + + def test_multi_company_rule(self): + """Users only see the lines of their companies.""" + other = self.company_data_2["company"] + user = self.env["res.users"].create( + { + "name": "MIS reader", + "login": "mis_reader_analytic", + "company_id": other.id, + "company_ids": [Command.set(other.ids)], + "groups_id": [ + Command.set(self.env.ref("account.group_account_readonly").ids) + ], + } + ) + self.assertFalse( + self.View.with_user(user).search( + [("move_line_id.move_id", "=", self.move.id)] + ) + ) diff --git a/mis_builder_analytic_source/views/mis_analytic_move_line_views.xml b/mis_builder_analytic_source/views/mis_analytic_move_line_views.xml new file mode 100644 index 0000000..5797050 --- /dev/null +++ b/mis_builder_analytic_source/views/mis_analytic_move_line_views.xml @@ -0,0 +1,73 @@ + + + + + mis.analytic.move.line.tree + mis.analytic.move.line + + + + + + + + + + + + + + + + + + + + + + + mis.analytic.move.line.search + mis.analytic.move.line + + + + + + + + + + + + + + + + + + diff --git a/mis_builder_analytic_source/views/res_config_settings_views.xml b/mis_builder_analytic_source/views/res_config_settings_views.xml new file mode 100644 index 0000000..198c73b --- /dev/null +++ b/mis_builder_analytic_source/views/res_config_settings_views.xml @@ -0,0 +1,37 @@ + + + + + res.config.settings.form (mis_builder_analytic_source) + res.config.settings + + +
+
+
+
+
+
+
+
+
diff --git a/setup/mis_builder_analytic_source/odoo/addons/mis_builder_analytic_source b/setup/mis_builder_analytic_source/odoo/addons/mis_builder_analytic_source new file mode 120000 index 0000000..7fd361c --- /dev/null +++ b/setup/mis_builder_analytic_source/odoo/addons/mis_builder_analytic_source @@ -0,0 +1 @@ +../../../../mis_builder_analytic_source \ No newline at end of file diff --git a/setup/mis_builder_analytic_source/setup.py b/setup/mis_builder_analytic_source/setup.py new file mode 100644 index 0000000..28c57bb --- /dev/null +++ b/setup/mis_builder_analytic_source/setup.py @@ -0,0 +1,6 @@ +import setuptools + +setuptools.setup( + setup_requires=['setuptools-odoo'], + odoo_addon=True, +)