[MIG] project_link_from_invoice: migrate to 18.0
This commit is contained in:
60
project_link_from_invoice/README.md
Normal file
60
project_link_from_invoice/README.md
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
# Project Link From Invoice
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
Easily access your projects directly from invoices:
|
||||||
|
|
||||||
|
- Displays project name(s) in the invoice list view.
|
||||||
|
- Adds a smart button on invoice form view to open the related project(s).
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
This module depends on:
|
||||||
|
|
||||||
|
- `project` (Odoo core)
|
||||||
|
- `account` (Odoo core)
|
||||||
|
- `sale_project` (Odoo core)
|
||||||
|
|
||||||
|
Use Odoo's normal procedure to install add-ons.
|
||||||
|
|
||||||
|
## Configuration
|
||||||
|
|
||||||
|
No specific configuration is required.
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
Open an invoice that is linked to a sale order. If the sale order has tasks
|
||||||
|
in a project, a "Project(s)" button appears in the header of the invoice form.
|
||||||
|
Clicking it opens the related project(s) in form or list view.
|
||||||
|
|
||||||
|
The invoice list view also displays the project name(s) in a dedicated column.
|
||||||
|
|
||||||
|
## Known issues / Roadmap
|
||||||
|
|
||||||
|
None yet.
|
||||||
|
|
||||||
|
## Bug Tracker
|
||||||
|
|
||||||
|
Bugs are tracked on [our issues website](https://github.com/elabore-coop/project-tools/issues).
|
||||||
|
In case of trouble, please check there if your issue has already been reported.
|
||||||
|
If you spotted it first, help us smash it by providing detailed and welcomed feedback.
|
||||||
|
|
||||||
|
## Credits
|
||||||
|
|
||||||
|
### Images
|
||||||
|
|
||||||
|
- Elabore: [Icon](https://elabore.coop/web/image/res.company/1/logo)
|
||||||
|
|
||||||
|
### Contributors
|
||||||
|
|
||||||
|
- Clément Thomas
|
||||||
|
|
||||||
|
### Funders
|
||||||
|
|
||||||
|
The development of this module has been financially supported by:
|
||||||
|
|
||||||
|
- Elabore (<https://elabore.coop>)
|
||||||
|
|
||||||
|
### Maintainer
|
||||||
|
|
||||||
|
This module is maintained by Elabore.
|
||||||
2
project_link_from_invoice/__init__.py
Normal file
2
project_link_from_invoice/__init__.py
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
|
||||||
|
from . import models
|
||||||
34
project_link_from_invoice/__manifest__.py
Normal file
34
project_link_from_invoice/__manifest__.py
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
# Copyright 2022 Stéphan Sainléger (Elabore)
|
||||||
|
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
|
||||||
|
|
||||||
|
{
|
||||||
|
"name": "project_link_from_invoice",
|
||||||
|
"version": "18.0.1.0.0",
|
||||||
|
"author": "Elabore",
|
||||||
|
"website": "https://git.elabore.coop/elabore/project-tools",
|
||||||
|
"maintainer": "Clément Thomas",
|
||||||
|
"license": "AGPL-3",
|
||||||
|
"category": "Tools",
|
||||||
|
"summary": "Add link from invoice to project",
|
||||||
|
# any module necessary for this one to work correctly
|
||||||
|
"depends": [
|
||||||
|
"base",
|
||||||
|
"project",
|
||||||
|
"account",
|
||||||
|
"sale_project",
|
||||||
|
],
|
||||||
|
"external_dependencies": {
|
||||||
|
"python": [],
|
||||||
|
},
|
||||||
|
# always loaded
|
||||||
|
"data": [
|
||||||
|
"views/account_move_view.xml",
|
||||||
|
],
|
||||||
|
# only loaded in demonstration mode
|
||||||
|
"demo": [],
|
||||||
|
"installable": True,
|
||||||
|
# Install this module automatically if all dependency have been previously
|
||||||
|
# and independently installed. Used for synergetic or glue modules.
|
||||||
|
"auto_install": False,
|
||||||
|
"application": False,
|
||||||
|
}
|
||||||
58
project_link_from_invoice/i18n/fr.po
Normal file
58
project_link_from_invoice/i18n/fr.po
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
# Translation of Odoo Server.
|
||||||
|
# This file contains the translation of the following modules:
|
||||||
|
# * project_link_from_invoice
|
||||||
|
#
|
||||||
|
msgid ""
|
||||||
|
msgstr ""
|
||||||
|
"Project-Id-Version: Odoo Server 14.0\n"
|
||||||
|
"Report-Msgid-Bugs-To: \n"
|
||||||
|
"POT-Creation-Date: 2023-04-27 12:47+0000\n"
|
||||||
|
"PO-Revision-Date: 2023-04-27 12:47+0000\n"
|
||||||
|
"Last-Translator: \n"
|
||||||
|
"Language-Team: \n"
|
||||||
|
"MIME-Version: 1.0\n"
|
||||||
|
"Content-Type: text/plain; charset=UTF-8\n"
|
||||||
|
"Content-Transfer-Encoding: \n"
|
||||||
|
"Plural-Forms: \n"
|
||||||
|
|
||||||
|
#. module: project_link_from_invoice
|
||||||
|
#: model:ir.model.fields,field_description:project_link_from_invoice.field_account_move__display_name
|
||||||
|
msgid "Display Name"
|
||||||
|
msgstr "Nom affiché"
|
||||||
|
|
||||||
|
#. module: project_link_from_invoice
|
||||||
|
#: model:ir.model.fields,field_description:project_link_from_invoice.field_account_move__id
|
||||||
|
msgid "ID"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#. module: project_link_from_invoice
|
||||||
|
#: model:ir.model,name:project_link_from_invoice.model_account_move
|
||||||
|
msgid "Journal Entry"
|
||||||
|
msgstr "Pièce comptable"
|
||||||
|
|
||||||
|
#. module: project_link_from_invoice
|
||||||
|
#: model:ir.model.fields,field_description:project_link_from_invoice.field_account_move____last_update
|
||||||
|
msgid "Last Modified on"
|
||||||
|
msgstr "Dernière modification le"
|
||||||
|
|
||||||
|
#. module: project_link_from_invoice
|
||||||
|
#: model:ir.model.fields,field_description:project_link_from_invoice.field_account_bank_statement_line__project_ids
|
||||||
|
#: model:ir.model.fields,field_description:project_link_from_invoice.field_account_move__project_ids
|
||||||
|
#: model:ir.model.fields,field_description:project_link_from_invoice.field_account_payment__project_ids
|
||||||
|
msgid "Project"
|
||||||
|
msgstr "Projet"
|
||||||
|
|
||||||
|
#. module: project_link_from_invoice
|
||||||
|
#: model:ir.model.fields,field_description:project_link_from_invoice.field_account_bank_statement_line__project_count
|
||||||
|
#: model:ir.model.fields,field_description:project_link_from_invoice.field_account_move__project_count
|
||||||
|
#: model:ir.model.fields,field_description:project_link_from_invoice.field_account_payment__project_count
|
||||||
|
msgid "Project Count"
|
||||||
|
msgstr "Numbre de projets"
|
||||||
|
|
||||||
|
#. module: project_link_from_invoice
|
||||||
|
#: model_terms:ir.ui.view,arch_db:project_link_from_invoice.view_move_form_project_link_from_invoice_inherit
|
||||||
|
#: model:ir.model.fields,field_description:project_link_from_invoice.field_account_bank_statement_line__projects_name
|
||||||
|
#: model:ir.model.fields,field_description:project_link_from_invoice.field_account_move__projects_name
|
||||||
|
#: model:ir.model.fields,field_description:project_link_from_invoice.field_account_payment__projects_name
|
||||||
|
msgid "Project(s)"
|
||||||
|
msgstr "Projet(s)"
|
||||||
2
project_link_from_invoice/models/__init__.py
Normal file
2
project_link_from_invoice/models/__init__.py
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
|
||||||
|
from . import account_move
|
||||||
49
project_link_from_invoice/models/account_move.py
Normal file
49
project_link_from_invoice/models/account_move.py
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
# Copyright 2022 Stéphan Sainléger (Elabore)
|
||||||
|
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
|
||||||
|
|
||||||
|
from odoo import api, fields, models
|
||||||
|
|
||||||
|
|
||||||
|
class AccountMove(models.Model):
|
||||||
|
_inherit = "account.move"
|
||||||
|
|
||||||
|
project_ids = fields.Many2many(
|
||||||
|
"project.project", string="Projects", compute="_get_related_project_ids"
|
||||||
|
)
|
||||||
|
project_count = fields.Integer("Project Count", compute="_get_related_project_ids")
|
||||||
|
projects_name = fields.Char("Project(s)", compute="_get_related_project_ids")
|
||||||
|
|
||||||
|
def action_open_projects(self):
|
||||||
|
"""Open related projects, in form or list view depending on project numbers."""
|
||||||
|
project_ids = self.project_ids.ids
|
||||||
|
action = self.env["ir.actions.actions"]._for_xml_id(
|
||||||
|
"project.open_view_project_all"
|
||||||
|
)
|
||||||
|
|
||||||
|
if self.project_count == 1:
|
||||||
|
action["res_id"] = project_ids[0]
|
||||||
|
action["views"] = [[False, "form"]]
|
||||||
|
else:
|
||||||
|
action["views"] = [[False, "list"], [False, "form"]]
|
||||||
|
|
||||||
|
action["domain"] = [("id", "in", project_ids)]
|
||||||
|
|
||||||
|
del action["target"] # to display breadcrumbs
|
||||||
|
|
||||||
|
return action
|
||||||
|
|
||||||
|
@api.depends("line_ids.sale_line_ids")
|
||||||
|
def _get_related_project_ids(self):
|
||||||
|
for move in self:
|
||||||
|
projects = self.env["project.task"].search(
|
||||||
|
[
|
||||||
|
(
|
||||||
|
"sale_order_id",
|
||||||
|
"in",
|
||||||
|
move.line_ids.sale_line_ids.order_id.ids,
|
||||||
|
)
|
||||||
|
]
|
||||||
|
).project_id
|
||||||
|
move.project_ids = projects.ids
|
||||||
|
move.projects_name = " ; ".join([p.name for p in projects])
|
||||||
|
move.project_count = len(projects)
|
||||||
4
project_link_from_invoice/tests/__init__.py
Normal file
4
project_link_from_invoice/tests/__init__.py
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
# Copyright 2022 Stéphan Sainléger (Elabore)
|
||||||
|
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
|
||||||
|
|
||||||
|
from . import test_account_move
|
||||||
193
project_link_from_invoice/tests/test_account_move.py
Normal file
193
project_link_from_invoice/tests/test_account_move.py
Normal file
@@ -0,0 +1,193 @@
|
|||||||
|
# Copyright 2022 Stéphan Sainléger (Elabore)
|
||||||
|
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
|
||||||
|
|
||||||
|
from odoo import Command
|
||||||
|
from odoo.tests import tagged
|
||||||
|
from odoo.addons.sale_project.tests.common import TestSaleProjectCommon
|
||||||
|
|
||||||
|
|
||||||
|
@tagged("post_install", "-at_install")
|
||||||
|
class TestProjectLinkFromInvoice(TestSaleProjectCommon):
|
||||||
|
"""Tests for the project_link_from_invoice module.
|
||||||
|
|
||||||
|
Verifies that invoices correctly expose related project(s) via
|
||||||
|
the computed fields project_ids, project_count and projects_name.
|
||||||
|
"""
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def setUpClass(cls):
|
||||||
|
super().setUpClass()
|
||||||
|
cls.env = cls.env(context=dict(cls.env.context, tracking_disable=True))
|
||||||
|
|
||||||
|
# Service product that creates a task in a new project on SO confirmation
|
||||||
|
cls.service_product = cls.env["product.product"].create(
|
||||||
|
{
|
||||||
|
"name": "Service with Project",
|
||||||
|
"type": "service",
|
||||||
|
"invoice_policy": "order",
|
||||||
|
"service_tracking": "task_in_project",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
# Partner used for orders and invoices
|
||||||
|
cls.partner = cls.env["res.partner"].create({"name": "Test Partner"})
|
||||||
|
|
||||||
|
def _confirm_sale_order(self, product, qty=1):
|
||||||
|
"""Create and confirm a sale order with one service line."""
|
||||||
|
so = self.env["sale.order"].create(
|
||||||
|
{
|
||||||
|
"partner_id": self.partner.id,
|
||||||
|
"order_line": [
|
||||||
|
Command.create(
|
||||||
|
{
|
||||||
|
"product_id": product.id,
|
||||||
|
"product_uom_qty": qty,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
],
|
||||||
|
}
|
||||||
|
)
|
||||||
|
so.action_confirm()
|
||||||
|
return so
|
||||||
|
|
||||||
|
def _invoice_sale_order(self, sale_order):
|
||||||
|
"""Create and post an invoice from a confirmed sale order."""
|
||||||
|
invoice_wizard = (
|
||||||
|
self.env["sale.advance.payment.inv"]
|
||||||
|
.with_context(
|
||||||
|
active_model="sale.order",
|
||||||
|
active_ids=sale_order.ids,
|
||||||
|
active_id=sale_order.id,
|
||||||
|
)
|
||||||
|
.create({"advance_payment_method": "delivered"})
|
||||||
|
)
|
||||||
|
invoices = invoice_wizard._create_invoices(sale_order)
|
||||||
|
invoices.action_post()
|
||||||
|
return invoices
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
# Tests
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
|
||||||
|
def test_no_project_on_unrelated_invoice(self):
|
||||||
|
"""An invoice not linked to any sale order has no related project."""
|
||||||
|
invoice = self.env["account.move"].create(
|
||||||
|
{
|
||||||
|
"move_type": "out_invoice",
|
||||||
|
"partner_id": self.partner.id,
|
||||||
|
"invoice_line_ids": [
|
||||||
|
Command.create(
|
||||||
|
{
|
||||||
|
"name": "Manual line",
|
||||||
|
"quantity": 1,
|
||||||
|
"price_unit": 100.0,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
],
|
||||||
|
}
|
||||||
|
)
|
||||||
|
self.assertEqual(invoice.project_count, 0)
|
||||||
|
self.assertFalse(invoice.project_ids)
|
||||||
|
self.assertEqual(invoice.projects_name, "")
|
||||||
|
|
||||||
|
def test_single_project_linked_to_invoice(self):
|
||||||
|
"""An invoice from a SO that created a task has one related project."""
|
||||||
|
so = self._confirm_sale_order(self.service_product)
|
||||||
|
# The SO must have generated a task in a project
|
||||||
|
task = so.tasks_ids[:1]
|
||||||
|
self.assertTrue(task, "Sale order confirmation should have created a task")
|
||||||
|
project = task.project_id
|
||||||
|
self.assertTrue(project, "Task should belong to a project")
|
||||||
|
|
||||||
|
invoice = self._invoice_sale_order(so)
|
||||||
|
self.assertEqual(len(invoice), 1)
|
||||||
|
self.assertEqual(invoice.project_count, 1)
|
||||||
|
self.assertIn(project, invoice.project_ids)
|
||||||
|
self.assertIn(project.name, invoice.projects_name)
|
||||||
|
|
||||||
|
def test_multiple_projects_linked_to_invoice(self):
|
||||||
|
"""An invoice from two SOs with different projects shows both projects."""
|
||||||
|
so1 = self._confirm_sale_order(self.service_product)
|
||||||
|
so2 = self._confirm_sale_order(self.service_product)
|
||||||
|
|
||||||
|
project1 = so1.tasks_ids[:1].project_id
|
||||||
|
project2 = so2.tasks_ids[:1].project_id
|
||||||
|
self.assertTrue(project1 and project2)
|
||||||
|
# Ensure the two SOs created different projects
|
||||||
|
self.assertNotEqual(project1, project2)
|
||||||
|
|
||||||
|
# Create a single invoice covering both sale orders
|
||||||
|
invoice_wizard = (
|
||||||
|
self.env["sale.advance.payment.inv"]
|
||||||
|
.with_context(
|
||||||
|
active_model="sale.order",
|
||||||
|
active_ids=(so1 + so2).ids,
|
||||||
|
)
|
||||||
|
.create({"advance_payment_method": "delivered"})
|
||||||
|
)
|
||||||
|
invoices = invoice_wizard._create_invoices(so1 + so2)
|
||||||
|
invoices.action_post()
|
||||||
|
|
||||||
|
# Find the invoice that covers both SO lines
|
||||||
|
combined_invoice = invoices[:1]
|
||||||
|
self.assertGreaterEqual(combined_invoice.project_count, 2)
|
||||||
|
self.assertIn(project1, combined_invoice.project_ids)
|
||||||
|
self.assertIn(project2, combined_invoice.project_ids)
|
||||||
|
|
||||||
|
def test_action_open_projects_single(self):
|
||||||
|
"""action_open_projects returns a form view when there is one project."""
|
||||||
|
so = self._confirm_sale_order(self.service_product)
|
||||||
|
invoice = self._invoice_sale_order(so)
|
||||||
|
self.assertEqual(invoice.project_count, 1)
|
||||||
|
|
||||||
|
action = invoice.action_open_projects()
|
||||||
|
self.assertEqual(action["res_id"], invoice.project_ids.id)
|
||||||
|
view_types = [v[1] for v in action["views"]]
|
||||||
|
self.assertIn("form", view_types)
|
||||||
|
self.assertNotIn("list", view_types)
|
||||||
|
|
||||||
|
def test_action_open_projects_multiple(self):
|
||||||
|
"""action_open_projects returns a list view when there are multiple projects."""
|
||||||
|
so1 = self._confirm_sale_order(self.service_product)
|
||||||
|
so2 = self._confirm_sale_order(self.service_product)
|
||||||
|
|
||||||
|
invoice_wizard = (
|
||||||
|
self.env["sale.advance.payment.inv"]
|
||||||
|
.with_context(
|
||||||
|
active_model="sale.order",
|
||||||
|
active_ids=(so1 + so2).ids,
|
||||||
|
)
|
||||||
|
.create({"advance_payment_method": "delivered"})
|
||||||
|
)
|
||||||
|
invoices = invoice_wizard._create_invoices(so1 + so2)
|
||||||
|
invoices.action_post()
|
||||||
|
|
||||||
|
combined_invoice = invoices[:1]
|
||||||
|
if combined_invoice.project_count < 2:
|
||||||
|
self.skipTest("Combined invoice does not span multiple projects")
|
||||||
|
|
||||||
|
action = combined_invoice.action_open_projects()
|
||||||
|
view_types = [v[1] for v in action["views"]]
|
||||||
|
self.assertIn("list", view_types)
|
||||||
|
self.assertIn("form", view_types)
|
||||||
|
self.assertNotIn("target", action)
|
||||||
|
|
||||||
|
def test_projects_name_separator(self):
|
||||||
|
"""projects_name joins multiple project names with ' ; '."""
|
||||||
|
so1 = self._confirm_sale_order(self.service_product)
|
||||||
|
so2 = self._confirm_sale_order(self.service_product)
|
||||||
|
|
||||||
|
invoice_wizard = (
|
||||||
|
self.env["sale.advance.payment.inv"]
|
||||||
|
.with_context(
|
||||||
|
active_model="sale.order",
|
||||||
|
active_ids=(so1 + so2).ids,
|
||||||
|
)
|
||||||
|
.create({"advance_payment_method": "delivered"})
|
||||||
|
)
|
||||||
|
invoices = invoice_wizard._create_invoices(so1 + so2)
|
||||||
|
invoices.action_post()
|
||||||
|
|
||||||
|
combined_invoice = invoices[:1]
|
||||||
|
if combined_invoice.project_count >= 2:
|
||||||
|
self.assertIn(" ; ", combined_invoice.projects_name)
|
||||||
35
project_link_from_invoice/views/account_move_view.xml
Normal file
35
project_link_from_invoice/views/account_move_view.xml
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<odoo>
|
||||||
|
<record id="view_move_form_project_link_from_invoice_inherit" model="ir.ui.view">
|
||||||
|
<field name="name">account.move.form.project.link</field>
|
||||||
|
<field name="model">account.move</field>
|
||||||
|
<field name="inherit_id" ref="account.view_move_form" />
|
||||||
|
<field name="priority" eval="99" />
|
||||||
|
<field name="arch" type="xml">
|
||||||
|
<xpath expr="//div[@name='button_box']" position="inside">
|
||||||
|
<button
|
||||||
|
name="action_open_projects"
|
||||||
|
class="oe_stat_button"
|
||||||
|
icon="fa-puzzle-piece"
|
||||||
|
type="object"
|
||||||
|
invisible="project_count == 0"
|
||||||
|
>
|
||||||
|
<field name="project_count" widget="statinfo" string="Project(s)" />
|
||||||
|
</button>
|
||||||
|
</xpath>
|
||||||
|
</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<record id="view_invoice_tree_project_link_from_invoice_inherit" model="ir.ui.view">
|
||||||
|
<field name="name">account.invoice.list.project.link</field>
|
||||||
|
<field name="model">account.move</field>
|
||||||
|
<field name="inherit_id" ref="account.view_invoice_tree"/>
|
||||||
|
<field name="arch" type="xml">
|
||||||
|
<xpath expr="//list/field[@name='invoice_date']" position="before">
|
||||||
|
<field name="projects_name" />
|
||||||
|
</xpath>
|
||||||
|
</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
</odoo>
|
||||||
|
|
||||||
Reference in New Issue
Block a user