Skip to content
Merged
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
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ Available addons
addon | version | maintainers | summary
--- | --- | --- | ---
[agreement_rebate_partner_company_group](agreement_rebate_partner_company_group/) | 18.0.1.0.0 | | Rebate agreements applied to all company group members
[contract](contract/) | 18.0.2.5.0 | | Recurring - Contracts Management
[contract](contract/) | 18.0.2.5.1 | | Recurring - Contracts Management
[contract_analytic_tag](contract_analytic_tag/) | 18.0.1.0.0 | <a href='https://github.com/victoralmau'><img src='https://github.com/victoralmau.png' width='32' height='32' style='border-radius:50%;' alt='victoralmau'/></a> | Contract Analytic Tag
[contract_forecast](contract_forecast/) | 18.0.1.0.1 | <a href='https://github.com/sbejaoui'><img src='https://github.com/sbejaoui.png' width='32' height='32' style='border-radius:50%;' alt='sbejaoui'/></a> | Contract Forecast
[contract_forecast_variable_quantity](contract_forecast_variable_quantity/) | 18.0.1.0.0 | | Contract Forecast Variable Quantity
Expand All @@ -42,6 +42,7 @@ addon | version | maintainers | summary
[contract_sale_mandate](contract_sale_mandate/) | 18.0.1.0.0 | | This module manages the banking mandate from the sale order to the contract.
[contract_sale_payment_mode](contract_sale_payment_mode/) | 18.0.1.0.0 | | This addon manages payment mode from sale order to contract.
[contract_sale_transmit_method](contract_sale_transmit_method/) | 18.0.1.0.0 | | Propagate transmit method (email, post, portal, ...) from sale orders to contracts.
[contract_service_dates](contract_service_dates/) | 18.0.1.0.0 | | This module adds Service Dates fields to contract lines
[contract_termination](contract_termination/) | 18.0.1.0.0 | | contract_termination
[contract_transmit_method](contract_transmit_method/) | 18.0.1.1.0 | | Set transmit method (email, post, portal, ...) in contracts and propagate it to invoices.
[contract_update_last_date_invoiced](contract_update_last_date_invoiced/) | 18.0.1.0.1 | <a href='https://github.com/rafaelbn'><img src='https://github.com/rafaelbn.png' width='32' height='32' style='border-radius:50%;' alt='rafaelbn'/></a> <a href='https://github.com/Andrii9090'><img src='https://github.com/Andrii9090.png' width='32' height='32' style='border-radius:50%;' alt='Andrii9090'/></a> | This module allows to update the last date invoiced if invoices are deleted.
Expand Down
2 changes: 1 addition & 1 deletion contract/README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ Recurring - Contracts Management
!! This file is generated by oca-gen-addon-readme !!
!! changes will be overwritten. !!
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
!! source digest: sha256:f31a70112d86e7b2f6237d0ad274d0a55e384fe483eb0b1a4fe4a0fca1b05c9a
!! source digest: sha256:18b03b2e2527c4875b06eb16fdea75963f902195518f9c5c755649aa89352108
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!

.. |badge1| image:: https://img.shields.io/badge/maturity-Production%2FStable-green.png
Expand Down
2 changes: 1 addition & 1 deletion contract/__manifest__.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@

{
"name": "Recurring - Contracts Management",
"version": "18.0.2.5.0",
"version": "18.0.2.5.1",
"category": "Contract Management",
"license": "AGPL-3",
"author": "Tecnativa, ACSONE SA/NV, Odoo Community Association (OCA)",
Expand Down
33 changes: 28 additions & 5 deletions contract/models/contract_line.py
Original file line number Diff line number Diff line change
Expand Up @@ -361,9 +361,32 @@ def _get_contract_line_total_value(self):
self.display_name,
)
)
quantity = self._get_quantity_to_invoice(
self.date_start,
self.date_end,
self.date_start,

period_length = self.get_relative_delta(
self.recurring_rule_type, self.recurring_interval
)
currency = (
self.contract_id.pricelist_id.currency_id
if self.contract_id.pricelist_id
else self.currency_id
)
return quantity * self.price_unit

current_period_start = self.date_start
total_amount = 0.0
while current_period_start <= self.date_end:
current_period_end = (
current_period_start + period_length - relativedelta(days=1)
)
if current_period_end > self.date_end:
current_period_end = self.date_end
qty = self._get_quantity_to_invoice(
current_period_start,
current_period_end,
current_period_start, # Mock an invoice date at period start
)
subtotal = qty * self.price_unit * (1 - (self.discount / 100.0))
if currency:
subtotal = currency.round(subtotal)
total_amount += subtotal
current_period_start += period_length
return total_amount
2 changes: 1 addition & 1 deletion contract/static/description/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -372,7 +372,7 @@ <h1>Recurring - Contracts Management</h1>
!! This file is generated by oca-gen-addon-readme !!
!! changes will be overwritten. !!
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
!! source digest: sha256:f31a70112d86e7b2f6237d0ad274d0a55e384fe483eb0b1a4fe4a0fca1b05c9a
!! source digest: sha256:18b03b2e2527c4875b06eb16fdea75963f902195518f9c5c755649aa89352108
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! -->
<p><a class="reference external image-reference" href="https://odoo-community.org/page/development-status"><img alt="Production/Stable" src="https://img.shields.io/badge/maturity-Production%2FStable-green.png" /></a> <a class="reference external image-reference" href="http://www.gnu.org/licenses/agpl-3.0-standalone.html"><img alt="License: AGPL-3" src="https://img.shields.io/badge/license-AGPL--3-blue.png" /></a> <a class="reference external image-reference" href="https://github.com/OCA/contract/tree/18.0/contract"><img alt="OCA/contract" src="https://img.shields.io/badge/github-OCA%2Fcontract-lightgray.png?logo=github" /></a> <a class="reference external image-reference" href="https://translation.odoo-community.org/projects/contract-18-0/contract-18-0-contract"><img alt="Translate me on Weblate" src="https://img.shields.io/badge/weblate-Translate%20me-F47D42.png" /></a> <a class="reference external image-reference" href="https://runboat.odoo-community.org/builds?repo=OCA/contract&amp;target_branch=18.0"><img alt="Try me on Runboat" src="https://img.shields.io/badge/runboat-Try%20me-875A7B.png" /></a></p>
<p>This module enables contracts management with recurring invoicing
Expand Down
67 changes: 61 additions & 6 deletions contract/tests/test_contract.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@

from odoo import Command, fields
from odoo.exceptions import UserError, ValidationError
from odoo.fields import Date
from odoo.tests import Form, common


Expand Down Expand Up @@ -1617,33 +1618,41 @@ def test_get_contract_line_total_value_basic(self):
{
"quantity": 3,
"price_unit": 400.0,
"discount": 0.0,
"date_start": "2026-01-01",
"date_end": "2026-12-31",
"recurring_next_date": "2026-01-01",
"recurring_interval": 1,
"recurring_rule_type": "monthly",
}
)
self.assertEqual(self.acct_line._get_contract_line_total_value(), 3 * 400.0)
# 12 months * (3 * 400.0) = 14,400.0
self.assertEqual(self.acct_line._get_contract_line_total_value(), 14400.0)

def test_get_contract_line_total_value_single_unit(self):
self.acct_line.write(
{
"quantity": 1,
"price_unit": 1200.0,
"discount": 0.0,
"date_start": "2026-01-01",
"date_end": "2026-12-31",
"recurring_next_date": "2026-01-01",
"recurring_interval": 1,
"recurring_rule_type": "monthly",
}
)
self.assertEqual(self.acct_line._get_contract_line_total_value(), 1200.0)
# 12 months * 1200.0 = 14,400.0
self.assertEqual(self.acct_line._get_contract_line_total_value(), 14400.0)

def test_get_contract_line_total_value_zero_price(self):
self.acct_line.write(
{
"quantity": 5,
"price_unit": 0.0,
"discount": 0.0,
"date_start": "2026-01-01",
"date_end": "2026-06-30",
"recurring_next_date": "2026-01-01",
"recurring_interval": 1,
"recurring_rule_type": "monthly",
}
)
self.assertEqual(self.acct_line._get_contract_line_total_value(), 0.0)
Expand All @@ -1657,8 +1666,8 @@ def test_get_contract_line_total_value_no_date_end_raises(self):
"name": "Test",
"quantity": 1,
"price_unit": 1000.0,
"discount": 0.0,
"date_start": to_date("2026-01-01"),
"recurring_next_date": to_date("2026-01-01"),
"recurring_interval": 1,
"recurring_rule_type": "monthly",
"recurring_invoicing_type": "pre-paid",
Expand All @@ -1668,3 +1677,49 @@ def test_get_contract_line_total_value_no_date_end_raises(self):
line.date_end = False
with self.assertRaises(UserError):
line._get_contract_line_total_value()

def test_get_contract_line_total_value_with_discount(self):
self.acct_line.write(
{
"quantity": 2,
"price_unit": 100.0,
"discount": 10.0,
"date_start": "2026-01-01",
"date_end": "2026-12-31",
"recurring_interval": 1,
"recurring_rule_type": "monthly",
}
)
# Per period: 2 * 100 * 0.90 = 180.0
# Total (12 periods): 2160.0
self.assertEqual(self.acct_line._get_contract_line_total_value(), 2160.0)

def test_contract_line_total_value_period_prorata(self):
"""Test a contract that terminates mid-way through an invoicing period."""
self.acct_line.write(
{
"quantity": 1,
"price_unit": 100.0,
"discount": 0.0,
"date_start": "2026-01-01",
"date_end": "2026-02-15",
"recurring_interval": 1,
"recurring_rule_type": "monthly",
}
)

expected_qty_period_1 = self.acct_line._get_quantity_to_invoice(
Date.to_date("2026-01-01"),
Date.to_date("2026-01-31"),
Date.to_date("2026-01-01"),
)
expected_qty_period_2 = self.acct_line._get_quantity_to_invoice(
Date.to_date("2026-02-01"),
Date.to_date("2026-02-15"),
Date.to_date("2026-02-01"),
)

expected_total = (expected_qty_period_1 + expected_qty_period_2) * 100.0
self.assertEqual(
self.acct_line._get_contract_line_total_value(), expected_total
)
108 changes: 108 additions & 0 deletions contract_service_dates/README.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
.. image:: https://odoo-community.org/readme-banner-image
:target: https://odoo-community.org/get-involved?utm_source=readme
:alt: Odoo Community Association

======================
Contract Service Dates
======================

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

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

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

This module adds **Service Start Date** and **Service End Date** fields
to contract lines, allowing you to track the actual service period
independently from the billing dates.

**Table of contents**

.. contents::
:local:

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

Contract lines in the base contract module carry a single start date and
end date that drive the recurring invoicing schedule. There is no
dedicated field to record the period during which the service is
actually delivered, which is often distinct from the billing period.
This module fills that gap.

Usage
=====

On a contract line form, a new **Service Dates** group is displayed in
the **Recurrence** tab, below the contract line start and end dates:

- **Service Start Date**: start of the service period. Populated
automatically from the contract line start date and kept in sync with
it unless manually overridden.
- **Service End Date**: end of the service period. Populated
automatically from the contract line end date and kept in sync with it
unless manually overridden.

Once a service date is set to a value different from its corresponding
contract line date, it is considered decoupled and will no longer be
updated automatically when the contract line date changes.

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

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

* ACSONE SA/NV

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

- Benoit Aimont benoit.aimont@acsone.eu (https://www.acsone.eu/)

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/contract <https://github.com/OCA/contract/tree/18.0/contract_service_dates>`_ 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 contract_service_dates/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
from . import models
18 changes: 18 additions & 0 deletions contract_service_dates/__manifest__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
# Copyright 2026 ACSONE SA/NV
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).

{
"name": "Contract Service Dates",
"summary": """This module adds Service Dates fields to contract lines""",
"version": "18.0.1.0.0",
"license": "AGPL-3",
"author": "ACSONE SA/NV, Odoo Community Association (OCA)",
"website": "https://github.com/OCA/contract",
"depends": [
"contract",
],
"data": [
"views/contract_line.xml",
],
"installable": True,
}
48 changes: 48 additions & 0 deletions contract_service_dates/i18n/contract_service_dates.pot
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
# Translation of Odoo Server.
# This file contains the translation of the following modules:
# * contract_service_dates
#
msgid ""
msgstr ""
"Project-Id-Version: Odoo Server 18.0\n"
"Report-Msgid-Bugs-To: \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: contract_service_dates
#: model:ir.model,name:contract_service_dates.model_contract_line
msgid "Contract Line"
msgstr ""

#. module: contract_service_dates
#: model:ir.model.fields,help:contract_service_dates.field_contract_line__service_end_date
msgid ""
"End date of the service period covered by this contract line. Defaults to "
"the contract line end date and follows it unless manually overridden."
msgstr ""

#. module: contract_service_dates
#: model_terms:ir.ui.view,arch_db:contract_service_dates.contract_line_form_view
msgid "Service Dates"
msgstr ""

#. module: contract_service_dates
#: model:ir.model.fields,field_description:contract_service_dates.field_contract_line__service_end_date
msgid "Service End Date"
msgstr ""

#. module: contract_service_dates
#: model:ir.model.fields,field_description:contract_service_dates.field_contract_line__service_start_date
msgid "Service Start Date"
msgstr ""

#. module: contract_service_dates
#: model:ir.model.fields,help:contract_service_dates.field_contract_line__service_start_date
msgid ""
"Start date of the service period covered by this contract line. Defaults to "
"the contract line start date and follows it unless manually overridden."
msgstr ""
1 change: 1 addition & 0 deletions contract_service_dates/models/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
from . import contract_line
Loading