Skip to content

Commit

Permalink
[ADD] sale_project_reimbursement_cost: display provisions and reimbur…
Browse files Browse the repository at this point in the history
…sement costs in the Project Updates dashboard
  • Loading branch information
carlos-lopez-tecnativa committed Dec 5, 2024
1 parent 9769163 commit 5e093d2
Show file tree
Hide file tree
Showing 20 changed files with 1,365 additions and 0 deletions.
162 changes: 162 additions & 0 deletions sale_project_reimbursement_cost/README.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,162 @@
===============================
Sale project reimbursement cost
===============================

..
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
!! This file is generated by oca-gen-addon-readme !!
!! changes will be overwritten. !!
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
!! source digest: sha256:bad249e38e180277d68f401bdf0bd9385ca0561141736c76f646a7f277648b82
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
.. |badge1| image:: https://img.shields.io/badge/maturity-Beta-yellow.png
:target: https://odoo-community.org/page/development-status
:alt: Beta
.. |badge2| image:: https://img.shields.io/badge/licence-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%2Fproject-lightgray.png?logo=github
:target: https://github.com/OCA/project/tree/17.0/sale_project_reimbursement_cost
:alt: OCA/project
.. |badge4| image:: https://img.shields.io/badge/weblate-Translate%20me-F47D42.png
:target: https://translation.odoo-community.org/projects/project-17-0/project-17-0-sale_project_reimbursement_cost
: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/project&target_branch=17.0
:alt: Try me on Runboat

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

This module allows the display of provisions and reimbursement costs in
the ``Project Updates`` dashboard.

**Table of contents**

.. contents::
:local:

Use Cases / Context
===================

In businesses, especially those in services or consulting, where
expenses may be incurred on behalf of a client but later need to be
reimbursed by the client, Odoo’s functionality for
``Re-Invoice Expenses`` products (without using the hr_expenses module)
lacks visibility of this information in the project dashboard.

With this module, two sections are added to the project dashboard:

- **Provisions**: Values invoiced to the client in advance to cover any
necessary expenses. This section displays the analytic entries
generated from customer invoices for specific products.
- **Reimbursements cost:** Values generated from vendor bills that need
to be charged to the client. Using Odoo’s functionality, these are
the sales order lines automatically created from a vendor bill when
the product is configured as ``Re-Invoice Expenses``.

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

**To automatically create a project from a sale order:**

- Go to ``Sales > Products > Products``.
- Create a new product with the following options:

- Product Type: Service
- Invoicing Policy: ``Prepaid/Fixed Price``.
- Create on Order: Set an option other than ``Nothing``.

**For Provisions:**

- Go to ``Sales > Products > Products``.
- Create a new product of type Service.

**For Reimbursement:**

- Go to ``Sales > Products > Products``.
- Create a new product with the following options:

- ``Product Type``: Service
- Invoicing Policy: ``Based on Delivered Quantity (Manual)``.
- ``Re-Invoice Expenses``: At cost or Sales price
- Fill in the ``Provision Product`` field created in the previous
step.

Usage
=====

**To generate a new project from a sale order**:

- Go to ``Sales > Orders > Quotations``.
- Create a new sales order and select the first product configured.
- Confirm the sales order.

**To generate provisions**:

- Go to ``Invoicing > Customers > Invoices``.
- Create a new invoice and select the second product configured for
``Provisions``.
- In the invoice line, set the analytic distribution with the analytic
account for the project.
- Confirm the invoice.

**To generate reimbursement costs**:

- Go to ``Invoicing > Vendors > Bills``.
- Create a new bill and select the third product configured for
``Reimbursement``.
- In the invoice line, set the analytic distribution with the analytic
account for the project.
- Confirm the bill, and a new line will be added to the sale order with
the reimbursement cost.

**Project status dashboard**:

- Go to ``Project > Projects``.
- Search for the respective project.
- In the Kanban view, click the top-right icon to display project
settings, then click ``Reporting / Project Updates``.

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

Bugs are tracked on `GitHub Issues <https://github.com/OCA/project/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/project/issues/new?body=module:%20sale_project_reimbursement_cost%0Aversion:%2017.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
-------

* Tecnativa

Contributors
------------

- `Tecnativa <https://www.tecnativa.com>`__:

- Pedro M. Baeza
- Carlos López

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.

This module is part of the `OCA/project <https://github.com/OCA/project/tree/17.0/sale_project_reimbursement_cost>`_ 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 sale_project_reimbursement_cost/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
from . import models
22 changes: 22 additions & 0 deletions sale_project_reimbursement_cost/__manifest__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
# Copyright 2024 Tecnativa - Carlos López
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).
{
"name": "Sale project reimbursement cost",
"version": "17.0.1.0.0",
"summary": """Display provisions and reimbursement costs
in the Project Updates dashboard.""",
"author": "Tecnativa,Odoo Community Association (OCA)",
"website": "https://github.com/OCA/project",
"category": "Sales",
"depends": [
"sale_project",
],
"data": ["views/product_template_views.xml", "views/sale_order_line_views.xml"],
"assets": {
"web.assets_backend": [
"sale_project_reimbursement_cost/static/src/components/project_right_side_panel/**/*",
],
},
"installable": True,
"license": "AGPL-3",
}
2 changes: 2 additions & 0 deletions sale_project_reimbursement_cost/models/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
from . import product_template
from . import project_project
12 changes: 12 additions & 0 deletions sale_project_reimbursement_cost/models/product_template.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
# Copyright 2024 Tecnativa - Carlos López
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).
from odoo import fields, models


class ProductTemplate(models.Model):
_inherit = "product.template"

provision_product_id = fields.Many2one(
"product.product",
domain=[("type", "=", "service")],
)
151 changes: 151 additions & 0 deletions sale_project_reimbursement_cost/models/project_project.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
# Copyright 2024 Tecnativa - Carlos López
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).
from odoo import models
from odoo.osv import expression


class ProjectProject(models.Model):
_inherit = "project.project"

def get_panel_data(self):
panel_data = super().get_panel_data()
return {
**panel_data,
"reimbursement_items": self._get_reimbursement_items(),
"provision_items": self._get_provision_items(),
}

def _get_reimbursement_items(self, with_action=True):
domain = self._get_reimbursement_items_domain()
return {
"total": self.env["sale.order.line"].sudo().search_count(domain),
"data": self.get_reimbursement_items_data(
domain, limit=5, with_action=with_action
),
}

def get_reimbursement_items_data(
self, domain=None, offset=0, limit=None, with_action=True
):
if not self.user_has_groups("project.group_project_user"):
return []

Check warning on line 31 in sale_project_reimbursement_cost/models/project_project.py

View check run for this annotation

Codecov / codecov/patch

sale_project_reimbursement_cost/models/project_project.py#L31

Added line #L31 was not covered by tests
sols = (
self.env["sale.order.line"]
.sudo()
.search(
domain or self._get_reimbursement_items_domain(),
offset=offset,
limit=limit,
)
)

def get_action(res_id):
"""Return the action vals to call it in frontend to the sale lines"""
action_id = "sale_project_reimbursement_cost.sol_reimbursement_cost_action"
return {
"action": {
"name": action_id,
"resId": res_id,
}
}

return [
{
**sol_read,
**get_action(sol_read["id"]),
}
for sol_read in sols.with_context(with_price_unit=True).read(
[
"display_name",
"untaxed_amount_invoiced",
"untaxed_amount_to_invoice",
]
)
]

def _get_reimbursement_items_domain(self, additional_domain=None):
sale_items = self.sudo()._get_sale_order_items()
domain = [
("order_id", "in", sale_items.sudo().order_id.ids),
("is_expense", "=", True),
("qty_to_invoice", ">", 0),
("state", "=", "sale"),
("display_type", "=", False),
"|",
("project_id", "in", self.ids + [False]),
("id", "in", sale_items.ids),
]
if additional_domain:
domain = expression.AND([domain, additional_domain])

Check warning on line 79 in sale_project_reimbursement_cost/models/project_project.py

View check run for this annotation

Codecov / codecov/patch

sale_project_reimbursement_cost/models/project_project.py#L79

Added line #L79 was not covered by tests
return domain

def _get_provision_items(self, with_action=True):
domain = self._get_provision_items_domain()
return {
"total": self.env["account.analytic.line"].sudo().search_count(domain),
"data": self.get_provision_items_data(
domain, limit=5, with_action=with_action
),
}

def get_provision_items_data(
self, domain=None, offset=0, limit=None, with_action=True
):
if not self.user_has_groups("project.group_project_user"):
return []

Check warning on line 95 in sale_project_reimbursement_cost/models/project_project.py

View check run for this annotation

Codecov / codecov/patch

sale_project_reimbursement_cost/models/project_project.py#L95

Added line #L95 was not covered by tests
analytic_lines = (
self.env["account.analytic.line"]
.sudo()
.search(
domain or self._get_provision_items_domain(),
offset=offset,
limit=limit,
order="date,id",
)
)

def get_action(res_id):
"""Return the action vals to call it in frontend to the analytic lines"""
return {
"action": {
"name": "analytic.account_analytic_line_action_entries",
"resId": res_id,
}
}

return [
{
**analytic_line_read,
**get_action(analytic_line_read["id"]),
}
for analytic_line_read in analytic_lines.read(
[
"display_name",
"amount",
"date",
]
)
]

def _get_provision_items_domain(self, additional_domain=None):
provision_products = self.sudo()._get_provision_products()
domain = [
("product_id", "in", provision_products.ids),
("account_id", "=", self.analytic_account_id.id),
]
if additional_domain:
domain = expression.AND([domain, additional_domain])

Check warning on line 137 in sale_project_reimbursement_cost/models/project_project.py

View check run for this annotation

Codecov / codecov/patch

sale_project_reimbursement_cost/models/project_project.py#L137

Added line #L137 was not covered by tests
return domain

def _get_provision_products(self):
"""Get the provision products set in the all products"""
provision_products = self.env["product.template"]._read_group(
domain=[("provision_product_id", "!=", False)],
groupby=[],
aggregates=["provision_product_id:recordset"],
)
return (
provision_products[0][0]
if provision_products
else self.env["product.product"]
)
3 changes: 3 additions & 0 deletions sale_project_reimbursement_cost/pyproject.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
[build-system]
requires = ["whool"]
build-backend = "whool.buildapi"
23 changes: 23 additions & 0 deletions sale_project_reimbursement_cost/readme/CONFIGURE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
**To automatically create a project from a sale order:**

- Go to `Sales > Products > Products`.
- Create a new product with the following options:
- Product Type: Service
- Invoicing Policy: `Prepaid/Fixed Price`.
- Create on Order: Set an option other than `Nothing`.


**For Provisions:**

- Go to `Sales > Products > Products`.
- Create a new product of type Service.


**For Reimbursement:**

- Go to `Sales > Products > Products`.
- Create a new product with the following options:
- `Product Type`: Service
- Invoicing Policy: `Based on Delivered Quantity (Manual)`.
- `Re-Invoice Expenses`: At cost or Sales price
- Fill in the `Provision Product` field created in the previous step.
6 changes: 6 additions & 0 deletions sale_project_reimbursement_cost/readme/CONTEXT.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
In businesses, especially those in services or consulting, where expenses may be incurred on behalf of a client but later need to be reimbursed by the client, Odoo’s functionality for `Re-Invoice Expenses` products (without using the hr_expenses module) lacks visibility of this information in the project dashboard.

With this module, two sections are added to the project dashboard:

- **Provisions**: Values invoiced to the client in advance to cover any necessary expenses. This section displays the analytic entries generated from customer invoices for specific products.
- **Reimbursements cost:** Values generated from vendor bills that need to be charged to the client. Using Odoo’s functionality, these are the sales order lines automatically created from a vendor bill when the product is configured as `Re-Invoice Expenses`.
3 changes: 3 additions & 0 deletions sale_project_reimbursement_cost/readme/CONTRIBUTORS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
- [Tecnativa](https://www.tecnativa.com):
- Pedro M. Baeza
- Carlos López
Loading

0 comments on commit 5e093d2

Please sign in to comment.