mirror of
https://github.com/OCA/contract.git
synced 2025-02-13 17:57:24 +02:00
[14.0][ADD] contract_split module
This commit is contained in:
0
contract_split/README.rst
Normal file
0
contract_split/README.rst
Normal file
3
contract_split/__init__.py
Normal file
3
contract_split/__init__.py
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
|
||||||
|
|
||||||
|
from . import models, wizard
|
||||||
20
contract_split/__manifest__.py
Normal file
20
contract_split/__manifest__.py
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
# Copyright 2023 Damien Crier - Foodles
|
||||||
|
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
|
||||||
|
|
||||||
|
|
||||||
|
{
|
||||||
|
"name": "Contract Split",
|
||||||
|
"version": "14.0.1.0.0",
|
||||||
|
"category": "Sales",
|
||||||
|
"license": "AGPL-3",
|
||||||
|
"summary": "Split contract",
|
||||||
|
"depends": ["contract"],
|
||||||
|
"author": "Foodles, Odoo Community Association (OCA)",
|
||||||
|
"website": "https://github.com/OCA/contract",
|
||||||
|
"data": [
|
||||||
|
"security/ir.model.access.csv",
|
||||||
|
"wizard/wizard_split_contract.xml",
|
||||||
|
"views/contract.xml",
|
||||||
|
],
|
||||||
|
"installable": True,
|
||||||
|
}
|
||||||
3
contract_split/models/__init__.py
Normal file
3
contract_split/models/__init__.py
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
|
||||||
|
|
||||||
|
from . import contract
|
||||||
26
contract_split/models/contract.py
Normal file
26
contract_split/models/contract.py
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
# Copyright 2023 Damien Crier - Foodles
|
||||||
|
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
|
||||||
|
|
||||||
|
from odoo import fields, models
|
||||||
|
|
||||||
|
|
||||||
|
class Contract(models.Model):
|
||||||
|
_inherit = "contract.contract"
|
||||||
|
|
||||||
|
original_contract_id = fields.Many2one(
|
||||||
|
comodel_name="contract.contract",
|
||||||
|
readonly=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class ContractLine(models.Model):
|
||||||
|
_inherit = "contract.line"
|
||||||
|
|
||||||
|
splitted_from_line_id = fields.Many2one(
|
||||||
|
comodel_name="contract.line",
|
||||||
|
readonly=True,
|
||||||
|
)
|
||||||
|
splitted_from_contract_id = fields.Many2one(
|
||||||
|
comodel_name="contract.contract",
|
||||||
|
readonly=True,
|
||||||
|
)
|
||||||
3
contract_split/readme/CONTRIBUTORS.rst
Normal file
3
contract_split/readme/CONTRIBUTORS.rst
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
* `Foodles <https://www.foodles.co>`_:
|
||||||
|
|
||||||
|
* Damien Crier
|
||||||
1
contract_split/readme/DESCRIPTION.rst
Normal file
1
contract_split/readme/DESCRIPTION.rst
Normal file
@@ -0,0 +1 @@
|
|||||||
|
Adds a new button "Split" on a contract to be able to split lines across several contracts.
|
||||||
1
contract_split/readme/USAGE.rst
Normal file
1
contract_split/readme/USAGE.rst
Normal file
@@ -0,0 +1 @@
|
|||||||
|
#. On a contract, hit the button "Split" and select which lines and/or quantities must be spitted to another contract.
|
||||||
3
contract_split/security/ir.model.access.csv
Normal file
3
contract_split/security/ir.model.access.csv
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
|
||||||
|
access_split_contract,access_split_contract,model_split_contract,base.group_user,1,1,1,1
|
||||||
|
access_split_contract_line,access_split_contract_line,model_split_contract_line,base.group_user,1,1,1,1
|
||||||
|
0
contract_split/tests/__init__.py
Normal file
0
contract_split/tests/__init__.py
Normal file
123
contract_split/tests/test_contract_split.py
Normal file
123
contract_split/tests/test_contract_split.py
Normal file
@@ -0,0 +1,123 @@
|
|||||||
|
# Copyright 2023 Foodles (https://www.foodles.com/)
|
||||||
|
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).
|
||||||
|
from odoo.exceptions import ValidationError
|
||||||
|
from odoo.tests import tagged
|
||||||
|
|
||||||
|
from odoo.addons.contract.tests.test_contract import TestContractBase
|
||||||
|
|
||||||
|
|
||||||
|
@tagged("post_install", "-at_install")
|
||||||
|
class TestContractSplit(TestContractBase):
|
||||||
|
@classmethod
|
||||||
|
def setUpClass(cls):
|
||||||
|
super().setUpClass()
|
||||||
|
|
||||||
|
def test_default_get(self):
|
||||||
|
wizard = (
|
||||||
|
self.env["split.contract"]
|
||||||
|
.with_context(active_id=self.contract3.id)
|
||||||
|
.create({})
|
||||||
|
)
|
||||||
|
self.assertEqual(self.contract3.partner_id.id, wizard.partner_id.id)
|
||||||
|
self.assertEqual(
|
||||||
|
self.contract3.invoice_partner_id.id, wizard.invoice_partner_id.id
|
||||||
|
)
|
||||||
|
self.assertEqual(self.contract3.id, wizard.main_contract_id.id)
|
||||||
|
self.assertEqual(3, len(wizard.split_line_ids.ids))
|
||||||
|
|
||||||
|
def test_no_split_because_no_qty_set(self):
|
||||||
|
wizard = (
|
||||||
|
self.env["split.contract"]
|
||||||
|
.with_context(active_id=self.contract3.id)
|
||||||
|
.create({})
|
||||||
|
)
|
||||||
|
wizard.partner_id = self.partner_2.id
|
||||||
|
initial_contracts_length = self.env["contract.contract"].search_count([])
|
||||||
|
# confirm wizard without setting to_split quantities
|
||||||
|
wizard.action_split_contract()
|
||||||
|
# nothing should have changed. No new contract created and original
|
||||||
|
# contract remains untouched
|
||||||
|
self.assertEqual(3, len(self.contract3.contract_line_ids.ids))
|
||||||
|
self.assertEqual(
|
||||||
|
initial_contracts_length, self.env["contract.contract"].search_count([])
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_split_one_line_full_qty(self):
|
||||||
|
wizard = (
|
||||||
|
self.env["split.contract"]
|
||||||
|
.with_context(active_id=self.contract3.id)
|
||||||
|
.create({})
|
||||||
|
)
|
||||||
|
wizard.partner_id = self.partner_2.id
|
||||||
|
initial_contracts_length = self.env["contract.contract"].search_count([])
|
||||||
|
# set quantity to split in the wizard
|
||||||
|
wizard.split_line_ids[0].quantity_to_split = wizard.split_line_ids[
|
||||||
|
0
|
||||||
|
].original_qty
|
||||||
|
# confirm wizard with setting to_split quantities
|
||||||
|
new_contract = wizard.action_split_contract()
|
||||||
|
# A new contract must have been created.
|
||||||
|
self.assertEqual(
|
||||||
|
initial_contracts_length + 1, self.env["contract.contract"].search_count([])
|
||||||
|
)
|
||||||
|
# new contract has now the splitted line
|
||||||
|
self.assertEqual(self.partner2.id, new_contract.partner_id.id)
|
||||||
|
self.assertEqual(1, len(new_contract.contract_line_ids.ids))
|
||||||
|
self.assertEqual(
|
||||||
|
self.contract3.id,
|
||||||
|
new_contract.contract_line_ids.mapped("splitted_from_contract_id"),
|
||||||
|
)
|
||||||
|
# Original contract has now only 2 lines (3 at the beginning)
|
||||||
|
self.assertEqual(2, len(self.contract3.contract_line_ids.ids))
|
||||||
|
|
||||||
|
def test_split_one_line_one_qty(self):
|
||||||
|
# Set a qty = 2 in one line of a contract
|
||||||
|
self.contract3.contract_line_ids.filtered(
|
||||||
|
lambda line: line.name == "Line"
|
||||||
|
).quantity = 2
|
||||||
|
wizard = (
|
||||||
|
self.env["split.contract"]
|
||||||
|
.with_context(active_id=self.contract3.id)
|
||||||
|
.create({})
|
||||||
|
)
|
||||||
|
wizard.partner_id = self.partner_2.id
|
||||||
|
initial_contracts_length = self.env["contract.contract"].search_count([])
|
||||||
|
# set quantity to split in the wizard
|
||||||
|
wizard.split_line_ids.filtered(lambda l: l.name == "Line").quantity_to_split = 1
|
||||||
|
# confirm wizard with setting to_split quantities
|
||||||
|
new_contract = wizard.action_split_contract()
|
||||||
|
# A new contract must have been created.
|
||||||
|
self.assertEqual(
|
||||||
|
initial_contracts_length + 1, self.env["contract.contract"].search_count([])
|
||||||
|
)
|
||||||
|
# new contract has partner2 as partner_id
|
||||||
|
self.assertEqual(self.partner2.id, new_contract.partner_id.id)
|
||||||
|
# new contract has now the splitted line with a qty of one
|
||||||
|
self.assertEqual(1, len(new_contract.contract_line_ids.ids))
|
||||||
|
self.assertEqual(1, new_contract.contract_line_ids.quantity)
|
||||||
|
self.assertEqual(
|
||||||
|
self.contract3.id,
|
||||||
|
new_contract.contract_line_ids.mapped("splitted_from_contract_id"),
|
||||||
|
)
|
||||||
|
# Original contract still has 3 lines but with a qty=1 in the last line named "Line"
|
||||||
|
self.assertEqual(3, len(self.contract3.contract_line_ids.ids))
|
||||||
|
self.assertEqual(
|
||||||
|
1,
|
||||||
|
self.contract3.contract_line_ids.filtered(
|
||||||
|
lambda l: l.name == "Line"
|
||||||
|
).quantity,
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_split_with_more_quantity_should_raise_error(self):
|
||||||
|
wizard = (
|
||||||
|
self.env["split.contract"]
|
||||||
|
.with_context(active_id=self.contract3.id)
|
||||||
|
.create({})
|
||||||
|
)
|
||||||
|
# set quantity to split in the wizard
|
||||||
|
wizard.split_line_ids[0].quantity_to_split = (
|
||||||
|
wizard.split_line_ids[0].original_qty + 2
|
||||||
|
)
|
||||||
|
# confirm wizard with setting to_split quantities that should raise an error
|
||||||
|
with self.assertRaises(ValidationError):
|
||||||
|
wizard.action_split_contract()
|
||||||
19
contract_split/views/contract.xml
Normal file
19
contract_split/views/contract.xml
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8" ?>
|
||||||
|
<odoo>
|
||||||
|
<record id="view_contract_contract_form" model="ir.ui.view">
|
||||||
|
<field name="name">contract.contract.form</field>
|
||||||
|
<field name="model">contract.contract</field>
|
||||||
|
<field name="inherit_id" ref="contract.contract_contract_form_view" />
|
||||||
|
<field name="arch" type="xml">
|
||||||
|
<header position="inside">
|
||||||
|
<button
|
||||||
|
name="%(contract_split.split_contract_wizard_action)d"
|
||||||
|
type="action"
|
||||||
|
string="Split"
|
||||||
|
class="oe_highlight"
|
||||||
|
context="{'default_contract_id': active_id}"
|
||||||
|
/>
|
||||||
|
</header>
|
||||||
|
</field>
|
||||||
|
</record>
|
||||||
|
</odoo>
|
||||||
3
contract_split/wizard/__init__.py
Normal file
3
contract_split/wizard/__init__.py
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
|
||||||
|
|
||||||
|
from . import wizard_split_contract
|
||||||
171
contract_split/wizard/wizard_split_contract.py
Normal file
171
contract_split/wizard/wizard_split_contract.py
Normal file
@@ -0,0 +1,171 @@
|
|||||||
|
# Copyright 2023 Damien Crier - Foodles
|
||||||
|
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
|
||||||
|
|
||||||
|
from odoo import _, api, fields, models
|
||||||
|
from odoo.exceptions import ValidationError
|
||||||
|
from odoo.tools import float_compare
|
||||||
|
|
||||||
|
|
||||||
|
class SplitContract(models.TransientModel):
|
||||||
|
_name = "split.contract"
|
||||||
|
|
||||||
|
split_line_ids = fields.One2many(
|
||||||
|
comodel_name="split.contract.line", inverse_name="split_contract_id"
|
||||||
|
)
|
||||||
|
main_contract_id = fields.Many2one(comodel_name="contract.contract")
|
||||||
|
partner_id = fields.Many2one(comodel_name="res.partner")
|
||||||
|
invoice_partner_id = fields.Many2one(comodel_name="res.partner")
|
||||||
|
|
||||||
|
@api.model
|
||||||
|
def default_get(self, fields) -> dict:
|
||||||
|
vals = super().default_get(fields)
|
||||||
|
contract_id = self.env.context.get("active_id")
|
||||||
|
contract = self.env["contract.contract"].browse(contract_id)
|
||||||
|
vals.update(self._get_default_values_from_contract(contract))
|
||||||
|
return vals
|
||||||
|
|
||||||
|
def _get_default_values_from_contract(self, contract) -> dict:
|
||||||
|
return {
|
||||||
|
"main_contract_id": contract.id,
|
||||||
|
"partner_id": contract.partner_id.id,
|
||||||
|
"invoice_partner_id": contract.invoice_partner_id.id,
|
||||||
|
"split_line_ids": [
|
||||||
|
(0, 0, self._get_default_split_line_values(line))
|
||||||
|
for line in contract.contract_line_ids
|
||||||
|
],
|
||||||
|
}
|
||||||
|
|
||||||
|
def _get_default_split_line_values(self, line) -> list:
|
||||||
|
return {
|
||||||
|
"original_contract_line_id": line.id,
|
||||||
|
}
|
||||||
|
|
||||||
|
def _get_contract_name(self):
|
||||||
|
return self.main_contract_id.name
|
||||||
|
|
||||||
|
def _get_values_create_contract(self):
|
||||||
|
self.ensure_one()
|
||||||
|
return {
|
||||||
|
"name": self._get_contract_name(),
|
||||||
|
"partner_id": self.partner_id.id,
|
||||||
|
"invoice_partner_id": self.invoice_partner_id.id,
|
||||||
|
"original_contract_id": self.main_contract_id.id,
|
||||||
|
"line_recurrence": True,
|
||||||
|
}
|
||||||
|
|
||||||
|
def action_split_contract(self):
|
||||||
|
"""
|
||||||
|
If lines exists in the wizard, create a new contract <CONTRACT>
|
||||||
|
For all lines that are kept in the wizard lines :
|
||||||
|
- check if it needs to be split or only moved to the new contract <CONTRACT>
|
||||||
|
- if original_qty == qty_to_split: just move the contract_id
|
||||||
|
- if original_qty < qty_to_split: split the line
|
||||||
|
(eg: duplicate and change qties)
|
||||||
|
- if qty_to_split == 0: do nothing
|
||||||
|
"""
|
||||||
|
self.ensure_one()
|
||||||
|
if self.split_line_ids and any(
|
||||||
|
line.quantity_to_split for line in self.split_line_ids
|
||||||
|
):
|
||||||
|
new_contract = self.env["contract.contract"].create(
|
||||||
|
self._get_values_create_contract()
|
||||||
|
)
|
||||||
|
# TODO: play onchange on partner_id. use onchange_helper from OCA ?
|
||||||
|
for line in self.split_line_ids:
|
||||||
|
original_line = line.original_contract_line_id
|
||||||
|
if not line.quantity_to_split:
|
||||||
|
continue
|
||||||
|
if (
|
||||||
|
float_compare(
|
||||||
|
line.quantity_to_split, line.original_qty, precision_digits=2
|
||||||
|
)
|
||||||
|
== 0
|
||||||
|
):
|
||||||
|
# only move because new_qty = original_qty
|
||||||
|
original_line.write(
|
||||||
|
line._get_write_values_when_moving_line(new_contract)
|
||||||
|
)
|
||||||
|
elif (
|
||||||
|
float_compare(
|
||||||
|
line.quantity_to_split, line.original_qty, precision_digits=2
|
||||||
|
)
|
||||||
|
< 0
|
||||||
|
):
|
||||||
|
# need to split and move
|
||||||
|
new_line = original_line.copy()
|
||||||
|
new_line.write(
|
||||||
|
line._get_write_values_when_splitting_and_moving_line(
|
||||||
|
new_contract, line
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
original_line.quantity -= new_line.quantity
|
||||||
|
return new_contract
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
class SplitContractLine(models.TransientModel):
|
||||||
|
_name = "split.contract.line"
|
||||||
|
|
||||||
|
split_contract_id = fields.Many2one(comodel_name="split.contract")
|
||||||
|
original_contract_line_id = fields.Many2one(comodel_name="contract.line")
|
||||||
|
original_qty = fields.Float(
|
||||||
|
related="original_contract_line_id.quantity",
|
||||||
|
readonly=True,
|
||||||
|
store=False,
|
||||||
|
)
|
||||||
|
original_contract_id = fields.Many2one(
|
||||||
|
comodel_name="contract.contract",
|
||||||
|
related="original_contract_line_id.contract_id",
|
||||||
|
readonly=True,
|
||||||
|
store=False,
|
||||||
|
)
|
||||||
|
product_id = fields.Many2one(
|
||||||
|
comodel_name="product.product",
|
||||||
|
related="original_contract_line_id.product_id",
|
||||||
|
readonly=True,
|
||||||
|
store=False,
|
||||||
|
)
|
||||||
|
uom_id = fields.Many2one(
|
||||||
|
comodel_name="uom.uom",
|
||||||
|
related="original_contract_line_id.uom_id",
|
||||||
|
readonly=True,
|
||||||
|
store=False,
|
||||||
|
)
|
||||||
|
name = fields.Text(
|
||||||
|
comodel_name="product.product",
|
||||||
|
related="original_contract_line_id.name",
|
||||||
|
readonly=True,
|
||||||
|
)
|
||||||
|
quantity_to_split = fields.Float(string="Quantity to move", default=0)
|
||||||
|
|
||||||
|
@api.constrains("quantity_to_split")
|
||||||
|
def _check_quantity_to_move(self):
|
||||||
|
for rec in self:
|
||||||
|
if (
|
||||||
|
float_compare(
|
||||||
|
rec.quantity_to_split, rec.original_qty, precision_digits=2
|
||||||
|
)
|
||||||
|
> 0
|
||||||
|
):
|
||||||
|
# we try to move more qty than present in the initial contract line
|
||||||
|
raise ValidationError(
|
||||||
|
_(
|
||||||
|
"You cannot split more quantities than the "
|
||||||
|
"original quantity of the initial contract line."
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
def _get_write_values_when_moving_line(self, new_contract):
|
||||||
|
return {
|
||||||
|
"contract_id": new_contract.id,
|
||||||
|
"splitted_from_contract_id": self.original_contract_id.id,
|
||||||
|
}
|
||||||
|
|
||||||
|
def _get_write_values_when_splitting_and_moving_line(self, new_contract, line):
|
||||||
|
return {
|
||||||
|
"contract_id": new_contract.id,
|
||||||
|
"splitted_from_contract_id": self.original_contract_id.id,
|
||||||
|
"splitted_from_line_id": self.original_contract_line_id.id,
|
||||||
|
"quantity": line.quantity_to_split,
|
||||||
|
}
|
||||||
46
contract_split/wizard/wizard_split_contract.xml
Normal file
46
contract_split/wizard/wizard_split_contract.xml
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8" ?>
|
||||||
|
<odoo>
|
||||||
|
<record id="view_split_contract_form" model="ir.ui.view">
|
||||||
|
<field name="name">split.contract.form</field>
|
||||||
|
<field name="model">split.contract</field>
|
||||||
|
<field name="arch" type="xml">
|
||||||
|
<form>
|
||||||
|
<group>
|
||||||
|
<field name="main_contract_id" />
|
||||||
|
<field name="partner_id" />
|
||||||
|
<field name="invoice_partner_id" />
|
||||||
|
</group>
|
||||||
|
<notebook>
|
||||||
|
<page name="lines" string="Lines">
|
||||||
|
<field name="split_line_ids" nolabel="1">
|
||||||
|
<tree>
|
||||||
|
<field name="original_contract_line_id" />
|
||||||
|
<field name="product_id" />
|
||||||
|
<field name="name" />
|
||||||
|
<field name="original_qty" />
|
||||||
|
<field name="uom_id" />
|
||||||
|
<field name="quantity_to_split" />
|
||||||
|
</tree>
|
||||||
|
</field>
|
||||||
|
</page>
|
||||||
|
</notebook>
|
||||||
|
<footer>
|
||||||
|
<button
|
||||||
|
string="Split"
|
||||||
|
name="action_split_contract"
|
||||||
|
type="object"
|
||||||
|
class="btn-primary"
|
||||||
|
/>
|
||||||
|
<button string="Cancel" class="btn-secondary" special="cancel" />
|
||||||
|
</footer>
|
||||||
|
</form>
|
||||||
|
</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<record id="split_contract_wizard_action" model="ir.actions.act_window">
|
||||||
|
<field name="name">Split Contract</field>
|
||||||
|
<field name="res_model">split.contract</field>
|
||||||
|
<field name="view_mode">form</field>
|
||||||
|
<field name="target">new</field>
|
||||||
|
</record>
|
||||||
|
</odoo>
|
||||||
1
setup/contract_split/odoo/addons/contract_split
Symbolic link
1
setup/contract_split/odoo/addons/contract_split
Symbolic link
@@ -0,0 +1 @@
|
|||||||
|
../../../../contract_split
|
||||||
6
setup/contract_split/setup.py
Normal file
6
setup/contract_split/setup.py
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
import setuptools
|
||||||
|
|
||||||
|
setuptools.setup(
|
||||||
|
setup_requires=['setuptools-odoo'],
|
||||||
|
odoo_addon=True,
|
||||||
|
)
|
||||||
Reference in New Issue
Block a user