Merge PR #967 into 14.0

Signed-off-by dreispt
This commit is contained in:
OCA-git-bot
2023-07-11 12:17:50 +00:00
18 changed files with 478 additions and 0 deletions

View File

View File

@@ -0,0 +1,3 @@
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
from . import models, wizard

View 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,
}

View File

@@ -0,0 +1,3 @@
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
from . import contract

View File

@@ -0,0 +1,78 @@
# Copyright 2023 Damien Crier - Foodles
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
from odoo import api, fields, models
class Contract(models.Model):
_inherit = "contract.contract"
original_contract_ids = fields.Many2many(
comodel_name="contract.contract",
relation="contract_split_contract_rel",
column1="contract_id",
column2="split_contract_id",
readonly=True,
)
@api.model
def _get_contract_split_name(self, split_wizard):
return split_wizard.main_contract_id.name
@api.model
def _get_values_create_split_contract(self, split_wizard):
return {
"name": self._get_contract_split_name(split_wizard),
"partner_id": split_wizard.partner_id.id,
"invoice_partner_id": split_wizard.invoice_partner_id.id,
"original_contract_ids": [split_wizard.main_contract_id.id],
"line_recurrence": True,
}
def _get_default_split_values(self) -> dict:
self.ensure_one()
return {
"main_contract_id": self.id,
"partner_id": self.partner_id.id,
"invoice_partner_id": self.invoice_partner_id.id,
"split_line_ids": [
(0, 0, line._get_default_split_line_values())
for line in self.contract_line_ids
],
}
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,
)
def _get_write_values_when_moving_line(self, new_contract):
self.ensure_one()
return {
"contract_id": new_contract.id,
"splitted_from_contract_id": self.contract_id.id,
}
def _get_write_values_when_splitting_and_moving_line(self, new_contract, qty):
self.ensure_one()
return {
"contract_id": new_contract.id,
"splitted_from_contract_id": self.contract_id.id,
"splitted_from_line_id": self.id,
"quantity": qty,
}
def _get_default_split_line_values(self) -> dict:
self.ensure_one()
return {
"original_contract_line_id": self.id,
"quantity_to_split": self.quantity,
}

View File

@@ -0,0 +1,3 @@
* `Foodles <https://www.foodles.co>`_:
* Damien Crier

View File

@@ -0,0 +1 @@
Adds a new button "Split" on a contract to be able to split lines across several contracts.

View File

@@ -0,0 +1 @@
* Allow to choose between move and plan successor (+ date)

View File

@@ -0,0 +1 @@
#. On a contract, hit the button "Split" and select which lines and/or quantities must be spitted to another contract.

View 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
1 id name model_id:id group_id:id perm_read perm_write perm_create perm_unlink
2 access_split_contract access_split_contract model_split_contract base.group_user 1 1 1 1
3 access_split_contract_line access_split_contract_line model_split_contract_line base.group_user 1 1 1 1

View File

@@ -0,0 +1 @@
from . import test_contract_split

View File

@@ -0,0 +1,150 @@
# 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_contract_default_get_method_1(self):
wizard = (
self.env["split.contract"]
.with_context(active_id=self.contract3.id)
.create({})
)
contract_split_name = self.contract3._get_contract_split_name(wizard)
self.assertEqual(contract_split_name, self.contract3.name)
expected_result = {
"main_contract_id": self.contract3.id,
"partner_id": self.contract3.partner_id.id,
"invoice_partner_id": self.contract3.invoice_partner_id.id,
"split_line_ids": [
(
0,
0,
{
"original_contract_line_id": line.id,
"quantity_to_split": line.quantity,
},
)
for line in self.contract3.contract_line_ids
],
}
self.assertEqual(self.contract3._get_default_split_values(), expected_result)
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
wizard.split_line_ids.quantity_to_split = 0
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
wizard.split_line_ids.quantity_to_split = 0
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.partner_2.id, new_contract.partner_id.id)
self.assertEqual(1, len(new_contract.contract_line_ids.ids))
self.assertEqual(
self.contract3,
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
wizard.split_line_ids.quantity_to_split = 0
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 partner_2 as partner_id
self.assertEqual(self.partner_2.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,
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
with self.assertRaises(ValidationError):
wizard.split_line_ids[0].quantity_to_split = (
wizard.split_line_ids[0].original_qty + 2
)

View 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>

View File

@@ -0,0 +1,3 @@
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
from . import wizard_split_contract

View File

@@ -0,0 +1,139 @@
# 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"
_description = "Contract split transient model"
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(contract._get_default_split_values())
return vals
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
):
contract_obj = self.env["contract.contract"]
new_contract = contract_obj.create(
contract_obj._get_values_create_split_contract(self)
)
# TODO: play onchange on partner_id. use onchange_helper from OCA ?
precision = self.env["decimal.precision"].precision_get(
"Product Unit of Measure"
)
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=line.uom_id and None or precision,
precision_rounding=line.uom_id.rounding or None,
)
== 0
):
# only move because new_qty = original_qty
original_line.write(
original_line._get_write_values_when_moving_line(new_contract)
)
elif (
float_compare(
line.quantity_to_split,
line.original_qty,
precision_digits=line.uom_id and None or precision,
precision_rounding=line.uom_id.rounding or None,
)
< 0
):
# need to split and move
new_line = original_line.copy()
new_line.write(
original_line._get_write_values_when_splitting_and_moving_line(
new_contract, line.quantity_to_split
)
)
original_line.quantity -= new_line.quantity
return new_contract
return True
class SplitContractLine(models.TransientModel):
_name = "split.contract.line"
_description = "Contract split line transient model"
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,
)
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):
precision = self.env["decimal.precision"].precision_get(
"Product Unit of Measure"
)
for rec in self:
if (
float_compare(
rec.quantity_to_split,
rec.original_qty,
precision_digits=rec.uom_id and None or precision,
precision_rounding=rec.uom_id.rounding or None,
)
> 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."
)
)

View 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>

View File

@@ -0,0 +1 @@
../../../../contract_split

View File

@@ -0,0 +1,6 @@
import setuptools
setuptools.setup(
setup_requires=['setuptools-odoo'],
odoo_addon=True,
)