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
152 changes: 152 additions & 0 deletions sale_certification/README.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
==================
Sale Certification
==================

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

.. |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%2Fvertical--construction-lightgray.png?logo=github
:target: https://github.com/OCA/vertical-construction/tree/17.0/sale_certification
:alt: OCA/vertical-construction
.. |badge4| image:: https://img.shields.io/badge/weblate-Translate%20me-F47D42.png
:target: https://translation.odoo-community.org/projects/vertical-construction-17-0/vertical-construction-17-0-sale_certification
: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/vertical-construction&target_branch=17.0
:alt: Try me on Runboat

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

This module introduces a certification workflow between sales orders and
invoicing, commonly used in construction and large-scale contracting
industries.

Instead of invoicing directly from sales orders, this module allows the
creation of **certifications** that represent approved progress or
partial completion. These certifications can then generate invoices,
optionally applying **retention amounts** (withholding a percentage of
the payment until final project acceptance).

Key Features:

- Add a certification step between sales and invoicing.
- Generate invoices based on certified quantities.
- Support for configurable retention percentages per certification.
- Tracks invoiced vs. certified amounts for better financial oversight
at sale level.

**Table of contents**

.. contents::
:local:

Usage
=====

1. **Create a Sales Order** as usual with your products or services.

2. Enable the **Certifiable?** checkbox on the Sales Order.

3. Confirm the Sales Order.

4. Once confirmed:

- A **Certified Percentage** will be displayed at the Sales Order
level.
- A **Quantity Certified** field will appear at the line level.

5. Based on the invoice policy of your products, the same logic will
apply to the certification process. A new **Certification** button
will appear in the header.

6. Creating a new Certification offers three options:

- **Regular** – Automatically includes all available lines based on
the invoice policy.
- **Percentage** – Applies a percentage to the amounts from the
regular option (not the total sale).
- **Chapter** – Allows selection of specific sections (sale order
lines within sections) to include in the certification.

7. After creating a certification, you can freely adjust quantities or
remove lines. These changes will be reflected in the Sales Order,
following the same logic as invoices.

8. When you confirm the certification, the confirmation date will be
set.

9. Click **Create Invoice**. If you select **Apply Retention Monies**,
you will have two retention options:

- **Percentage** of the certified base amount.
- **Fixed Value** over the certified base amount.

This will:

- Create a negative line in the invoice subtracting the retention
amount.
- Generate a separate invoice to hold the retention amount (if no
draft retention invoice already exists), accessible from the Sales
Order view.

10. You can set a payment method or reminders on the retention invoice
to charge the client later or cancel the invoice if necessary.

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

Bugs are tracked on `GitHub Issues <https://github.com/OCA/vertical-construction/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/vertical-construction/issues/new?body=module:%20sale_certification%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
-------

* Binhex

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

- Christian Ramos <c.ramos@binhex.cloud>

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

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

|maintainer-Christian-RB|

This module is part of the `OCA/vertical-construction <https://github.com/OCA/vertical-construction/tree/17.0/sale_certification>`_ project on GitHub.

You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute.
4 changes: 4 additions & 0 deletions sale_certification/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
from . import models
from . import security
from . import views
from . import wizard
35 changes: 35 additions & 0 deletions sale_certification/__manifest__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
{
"name": "Sale Certification",
"summary": (
"This module allows to certificate sales orders " "in a construction context"
),
"version": "17.0.1.0.0",
"license": "AGPL-3",
"author": "Binhex,Odoo Community Association (OCA)",
"website": "https://github.com/OCA/vertical-construction",
"category": "Sales",
"depends": [
"sale",
],
"maintainers": ["Christian-RB"],
"data": [
"security/ir.model.access.csv",
"security/ir_rules.xml",
"wizard/certification_invoice_wizard_view.xml",
"wizard/certification_wizard_view.xml",
"views/view_order_certifications.xml",
"views/view_order_form_certify.xml",
"views/order_certifications_menu.xml",
"views/account_views.xml",
"report/certification_reports.xml",
"report/certification_templates.xml",
],
"assets": {
"web.report_assets_common": [
"sale_certification/static/src/css/report_certification.css",
],
"web.report_assets_pdf": [
"sale_certification/static/src/css/report_certification.css",
],
},
}
6 changes: 6 additions & 0 deletions sale_certification/models/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
from . import account_move
from . import account_move_line
from . import order_certification
from . import certification_line
from . import sale_order
from . import sale_order_line
41 changes: 41 additions & 0 deletions sale_certification/models/account_move.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
from odoo import fields, models


class AccountMove(models.Model):
_inherit = "account.move"

certification_count = fields.Integer(
compute="_compute_origin_certification_count",
)
retention_invoice = fields.Boolean(
help="Indicates if this invoice is a retention invoice.",
)

def _compute_origin_certification_count(self):
for move in self:
move.certification_count = len(
move.line_ids.certification_line_ids.certification_id
)

def action_view_source_certifications(self):
self.ensure_one()
certification_lines = self.line_ids.certification_line_ids
source_certifications = certification_lines.certification_id
result = self.env["ir.actions.act_window"]._for_xml_id(
"sale_certification.order_certification_action"
)
if len(source_certifications) > 1:
result["domain"] = [("id", "in", source_certifications.ids)]
elif len(source_certifications) == 1:
result["views"] = [
(
self.env.ref(
"sale_certification.view_order_certifications_form", False
).id,
"form",
)
]
result["res_id"] = source_certifications.id
else:
result = {"type": "ir.actions.act_window_close"}
return result
56 changes: 56 additions & 0 deletions sale_certification/models/account_move_line.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
from odoo import fields, models


class AccountMoveLine(models.Model):
_inherit = "account.move.line"

certification_line_ids = fields.Many2many(
"certification.line",
"certification_line_invoice_rel",
"invoice_line_id",
"certification_line_id",
string="Certification Lines",
readonly=True,
copy=False,
)
retention_invoice_line = fields.Many2one(
comodel_name="account.move.line",
help="The retention invoice line related to this line.",
copy=False,
ondelete="restrict",
)

def find_retention_invoice(self, certification):
retention_invoice = certification.mapped(
"order_id.invoice_ids.invoice_line_ids.retention_invoice_line.move_id"
).filtered(lambda move: move.state == "draft" and move.retention_invoice)
if retention_invoice:
return retention_invoice[0]
return False

def create_retention_invoice_line(self, certification):
self.ensure_one()
move_id = self.find_retention_invoice(certification)
if not move_id:
move_id = self.env["account.move"].create(
{
"move_type": "out_invoice",
"partner_id": self.move_id.partner_id.id,
"date": fields.Date.context_today(self),
"retention_invoice": True,
}
)
retention_vals = {
"display_type": "product",
"name": self.name,
"quantity": 1.0,
"move_id": move_id.id,
"price_unit": -1 * self.price_subtotal,
"balance": -1 * self.price_subtotal,
"retention_invoice_line": self.id,
"currency_id": move_id.currency_id.id,
}
self.retention_invoice_line = self.env["account.move.line"].create(
retention_vals
)
return move_id
Loading