mirror of
https://github.com/OCA/contract.git
synced 2025-02-13 17:57:24 +02:00
ADD contract_sale_generation module
This commit is contained in:
committed by
Denis Roussel
parent
d9eaf59fea
commit
e7fd5bb118
57
contract_sale_generation/README.rst
Normal file
57
contract_sale_generation/README.rst
Normal file
@@ -0,0 +1,57 @@
|
||||
.. image:: https://img.shields.io/badge/licence-AGPL--3-blue.svg
|
||||
:target: http://www.gnu.org/licenses/agpl-3.0-standalone.html
|
||||
:alt: License: AGPL-3
|
||||
|
||||
=============================
|
||||
Contracts for recurrent sales
|
||||
=============================
|
||||
|
||||
This module extends functionality of contracts to be able to generate sales
|
||||
orders instead of invoices.
|
||||
|
||||
Usage
|
||||
=====
|
||||
|
||||
To use this module, you need to:
|
||||
|
||||
#. Go to Accounting -> Contracts and select or create a new contract.
|
||||
#. Check *Generate recurring invoices automatically*.
|
||||
#. Fill fields for selecting the recurrency and invoice parameters:
|
||||
|
||||
* Type defines document that contract will generate, can be "Sales" or "Invoices"
|
||||
* Sale Autoconfirm, validate Sales Orders if type is "Sales"
|
||||
|
||||
.. image:: https://odoo-community.org/website/image/ir.attachment/5784_f2813bd/datas
|
||||
:alt: Try me on Runbot
|
||||
:target: https://runbot.odoo-community.org/runbot/110/10.0
|
||||
|
||||
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 smashing it by providing a detailed and welcomed feedback.
|
||||
|
||||
Credits
|
||||
=======
|
||||
|
||||
Contributors
|
||||
------------
|
||||
|
||||
* Angel Moya <angel.moya@pesol.es>
|
||||
|
||||
Maintainer
|
||||
----------
|
||||
|
||||
.. image:: https://odoo-community.org/logo.png
|
||||
:alt: Odoo Community Association
|
||||
:target: https://odoo-community.org
|
||||
|
||||
This module is maintained by the OCA.
|
||||
|
||||
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.
|
||||
|
||||
To contribute to this module, please visit https://odoo-community.org.
|
||||
2
contract_sale_generation/__init__.py
Normal file
2
contract_sale_generation/__init__.py
Normal file
@@ -0,0 +1,2 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from . import models
|
||||
22
contract_sale_generation/__manifest__.py
Normal file
22
contract_sale_generation/__manifest__.py
Normal file
@@ -0,0 +1,22 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright 2017 Pesol (<http://pesol.es>)
|
||||
# Copyright 2017 Angel Moya <angel.moya@pesol.es>
|
||||
# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl.html)
|
||||
|
||||
|
||||
{
|
||||
'name': 'Contracts Management - Recurring Sales',
|
||||
'version': '10.0.1.0.0',
|
||||
'category': 'Contract Management',
|
||||
'license': 'AGPL-3',
|
||||
'author': "PESOL, "
|
||||
"Odoo Community Association (OCA)",
|
||||
'website': 'https://github.com/oca/contract',
|
||||
'depends': ['contract', 'sale'],
|
||||
'data': [
|
||||
'views/account_analytic_account_view.xml',
|
||||
'views/account_analytic_contract_view.xml',
|
||||
'views/sale_view.xml',
|
||||
],
|
||||
'installable': True,
|
||||
}
|
||||
5
contract_sale_generation/models/__init__.py
Normal file
5
contract_sale_generation/models/__init__.py
Normal file
@@ -0,0 +1,5 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
|
||||
|
||||
from . import account_analytic_contract
|
||||
from . import account_analytic_account
|
||||
80
contract_sale_generation/models/account_analytic_account.py
Normal file
80
contract_sale_generation/models/account_analytic_account.py
Normal file
@@ -0,0 +1,80 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# © 2004-2010 OpenERP SA
|
||||
# © 2014 Angel Moya <angel.moya@domatix.com>
|
||||
# © 2015 Pedro M. Baeza <pedro.baeza@tecnativa.com>
|
||||
# © 2016 Carlos Dauden <carlos.dauden@tecnativa.com>
|
||||
# Copyright 2016-2017 LasLabs Inc.
|
||||
# Copyright 2017 Pesol (<http://pesol.es>)
|
||||
# Copyright 2017 Angel Moya <angel.moya@pesol.es>
|
||||
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
|
||||
|
||||
from odoo import api, models
|
||||
from odoo.exceptions import ValidationError
|
||||
from odoo.tools.translate import _
|
||||
|
||||
|
||||
class AccountAnalyticAccount(models.Model):
|
||||
_inherit = 'account.analytic.account'
|
||||
|
||||
@api.model
|
||||
def _prepare_sale_line(self, line, order_id):
|
||||
sale_line = self.env['sale.order.line'].new({
|
||||
'order_id': order_id,
|
||||
'product_id': line.product_id.id,
|
||||
'proudct_uom_qty': line.quantity,
|
||||
'proudct_uom_id': line.uom_id.id,
|
||||
})
|
||||
# Get other invoice line values from product onchange
|
||||
sale_line.product_id_change()
|
||||
sale_line_vals = sale_line._convert_to_write(sale_line._cache)
|
||||
# Insert markers
|
||||
name = line.name
|
||||
contract = line.analytic_account_id
|
||||
if 'old_date' in self.env.context and 'next_date' in self.env.context:
|
||||
lang_obj = self.env['res.lang']
|
||||
lang = lang_obj.search(
|
||||
[('code', '=', contract.partner_id.lang)])
|
||||
date_format = lang.date_format or '%m/%d/%Y'
|
||||
name = self._insert_markers(
|
||||
line, self.env.context['old_date'],
|
||||
self.env.context['next_date'], date_format)
|
||||
sale_line_vals.update({
|
||||
'name': name,
|
||||
'discount': line.discount,
|
||||
'price_unit': line.price_unit,
|
||||
})
|
||||
return sale_line_vals
|
||||
|
||||
@api.multi
|
||||
def _prepare_sale(self):
|
||||
self.ensure_one()
|
||||
if not self.partner_id:
|
||||
raise ValidationError(
|
||||
_("You must first select a Customer for Contract %s!") %
|
||||
self.name)
|
||||
sale = self.env['sale.order'].new({
|
||||
'partner_id': self.partner_id,
|
||||
'date_order': self.recurring_next_date,
|
||||
'origin': self.name,
|
||||
'company_id': self.company_id.id,
|
||||
'user_id': self.partner_id.user_id.id,
|
||||
'project_id': self.id
|
||||
})
|
||||
# Get other invoice values from partner onchange
|
||||
sale.onchange_partner_id()
|
||||
return sale._convert_to_write(sale._cache)
|
||||
|
||||
@api.multi
|
||||
def _create_invoice(self):
|
||||
self.ensure_one()
|
||||
if self.type == 'invoice':
|
||||
return super(AccountAnalyticAccount, self)._create_invoice()
|
||||
else:
|
||||
sale_vals = self._prepare_sale()
|
||||
sale = self.env['sale.order'].create(sale_vals)
|
||||
for line in self.recurring_invoice_line_ids:
|
||||
sale_line_vals = self._prepare_sale_line(line, sale.id)
|
||||
self.env['sale.order.line'].create(sale_line_vals)
|
||||
if self.sale_autoconfirm:
|
||||
sale.action_confirm()
|
||||
return sale
|
||||
20
contract_sale_generation/models/account_analytic_contract.py
Normal file
20
contract_sale_generation/models/account_analytic_contract.py
Normal file
@@ -0,0 +1,20 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright 2017 Pesol (<http://pesol.es>)
|
||||
# Copyright 2017 Angel Moya <angel.moya@pesol.es>
|
||||
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
|
||||
|
||||
from odoo import fields, models
|
||||
|
||||
|
||||
class AccountAnalyticContract(models.Model):
|
||||
_inherit = 'account.analytic.contract'
|
||||
|
||||
type = fields.Selection(
|
||||
string='Type',
|
||||
selection=[('invoice', 'Invoice'),
|
||||
('sale', 'Sale')],
|
||||
default='invoice',
|
||||
required=True,
|
||||
)
|
||||
sale_autoconfirm = fields.Boolean(
|
||||
string='Sale autoconfirm')
|
||||
5
contract_sale_generation/tests/__init__.py
Normal file
5
contract_sale_generation/tests/__init__.py
Normal file
@@ -0,0 +1,5 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
|
||||
|
||||
from . import test_contract_invoice
|
||||
from . import test_contract_sale
|
||||
87
contract_sale_generation/tests/test_contract_invoice.py
Normal file
87
contract_sale_generation/tests/test_contract_invoice.py
Normal file
@@ -0,0 +1,87 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# © 2016 Carlos Dauden <carlos.dauden@tecnativa.com>
|
||||
# Copyright 2017 Pesol (<http://pesol.es>)
|
||||
# Copyright 2017 Angel Moya <angel.moya@pesol.es>
|
||||
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
|
||||
|
||||
from odoo.exceptions import ValidationError
|
||||
from odoo.tests.common import TransactionCase
|
||||
|
||||
|
||||
class TestContractInvoice(TransactionCase):
|
||||
# Use case : Prepare some data for current test case
|
||||
|
||||
def setUp(self):
|
||||
super(TestContractInvoice, self).setUp()
|
||||
self.partner = self.env.ref('base.res_partner_2')
|
||||
self.product = self.env.ref('product.product_product_2')
|
||||
self.product.taxes_id += self.env['account.tax'].search(
|
||||
[('type_tax_use', '=', 'sale')], limit=1)
|
||||
self.product.description_sale = 'Test description sale'
|
||||
self.template_vals = {
|
||||
'recurring_rule_type': 'yearly',
|
||||
'recurring_interval': 1,
|
||||
'name': 'Test Contract Template',
|
||||
'type': 'invoice'
|
||||
}
|
||||
self.template = self.env['account.analytic.contract'].create(
|
||||
self.template_vals,
|
||||
)
|
||||
self.contract = self.env['account.analytic.account'].create({
|
||||
'name': 'Test Contract',
|
||||
'partner_id': self.partner.id,
|
||||
'pricelist_id': self.partner.property_product_pricelist.id,
|
||||
'recurring_invoices': True,
|
||||
'date_start': '2016-02-15',
|
||||
'recurring_next_date': '2016-02-29',
|
||||
})
|
||||
self.contract.contract_template_id = self.template
|
||||
self.contract._onchange_contract_template_id()
|
||||
self.contract_line = self.env['account.analytic.invoice.line'].create({
|
||||
'analytic_account_id': self.contract.id,
|
||||
'product_id': self.product.id,
|
||||
'name': 'Services from #START# to #END#',
|
||||
'quantity': 1,
|
||||
'uom_id': self.product.uom_id.id,
|
||||
'price_unit': 100,
|
||||
'discount': 50,
|
||||
})
|
||||
|
||||
def test_check_discount(self):
|
||||
with self.assertRaises(ValidationError):
|
||||
self.contract_line.write({'discount': 120})
|
||||
|
||||
def test_contract(self):
|
||||
self.assertAlmostEqual(self.contract_line.price_subtotal, 50.0)
|
||||
res = self.contract_line._onchange_product_id()
|
||||
self.assertIn('uom_id', res['domain'])
|
||||
self.contract_line.price_unit = 100.0
|
||||
|
||||
self.contract.partner_id = False
|
||||
with self.assertRaises(ValidationError):
|
||||
self.contract.recurring_create_invoice()
|
||||
self.contract.partner_id = self.partner.id
|
||||
|
||||
self.contract.recurring_create_invoice()
|
||||
self.invoice_monthly = self.env['account.invoice'].search(
|
||||
[('contract_id', '=', self.contract.id)])
|
||||
self.assertTrue(self.invoice_monthly)
|
||||
self.assertEqual(self.contract.recurring_next_date, '2017-02-28')
|
||||
|
||||
self.inv_line = self.invoice_monthly.invoice_line_ids[0]
|
||||
self.assertTrue(self.inv_line.invoice_line_tax_ids)
|
||||
self.assertAlmostEqual(self.inv_line.price_subtotal, 50.0)
|
||||
self.assertEqual(self.contract.partner_id.user_id,
|
||||
self.invoice_monthly.user_id)
|
||||
|
||||
def test_onchange_contract_template_id(self):
|
||||
""" It should change the contract values to match the template. """
|
||||
self.contract.contract_template_id = self.template
|
||||
self.contract._onchange_contract_template_id()
|
||||
res = {
|
||||
'recurring_rule_type': self.contract.recurring_rule_type,
|
||||
'recurring_interval': self.contract.recurring_interval,
|
||||
'type': 'invoice'
|
||||
}
|
||||
del self.template_vals['name']
|
||||
self.assertDictEqual(res, self.template_vals)
|
||||
113
contract_sale_generation/tests/test_contract_sale.py
Normal file
113
contract_sale_generation/tests/test_contract_sale.py
Normal file
@@ -0,0 +1,113 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# © 2016 Carlos Dauden <carlos.dauden@tecnativa.com>
|
||||
# Copyright 2017 Pesol (<http://pesol.es>)
|
||||
# Copyright 2017 Angel Moya <angel.moya@pesol.es>
|
||||
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
|
||||
|
||||
from odoo.exceptions import ValidationError
|
||||
from odoo.tests.common import TransactionCase
|
||||
|
||||
|
||||
class TestContractSale(TransactionCase):
|
||||
# Use case : Prepare some data for current test case
|
||||
|
||||
def setUp(self):
|
||||
super(TestContractSale, self).setUp()
|
||||
self.partner = self.env.ref('base.res_partner_2')
|
||||
self.product = self.env.ref('product.product_product_2')
|
||||
self.product.taxes_id += self.env['account.tax'].search(
|
||||
[('type_tax_use', '=', 'sale')], limit=1)
|
||||
self.product.description_sale = 'Test description sale'
|
||||
self.template_vals = {
|
||||
'recurring_rule_type': 'yearly',
|
||||
'recurring_interval': 1,
|
||||
'name': 'Test Contract Template',
|
||||
'type': 'sale',
|
||||
'sale_autoconfirm': False
|
||||
}
|
||||
self.template = self.env['account.analytic.contract'].create(
|
||||
self.template_vals,
|
||||
)
|
||||
self.contract = self.env['account.analytic.account'].create({
|
||||
'name': 'Test Contract',
|
||||
'partner_id': self.partner.id,
|
||||
'pricelist_id': self.partner.property_product_pricelist.id,
|
||||
'recurring_invoices': True,
|
||||
'date_start': '2016-02-15',
|
||||
'recurring_next_date': '2016-02-29',
|
||||
})
|
||||
self.contract.contract_template_id = self.template
|
||||
self.contract._onchange_contract_template_id()
|
||||
self.contract_line = self.env['account.analytic.invoice.line'].create({
|
||||
'analytic_account_id': self.contract.id,
|
||||
'product_id': self.product.id,
|
||||
'name': 'Services from #START# to #END#',
|
||||
'quantity': 1,
|
||||
'uom_id': self.product.uom_id.id,
|
||||
'price_unit': 100,
|
||||
'discount': 50,
|
||||
})
|
||||
|
||||
def test_check_discount(self):
|
||||
with self.assertRaises(ValidationError):
|
||||
self.contract_line.write({'discount': 120})
|
||||
|
||||
def test_contract(self):
|
||||
self.assertAlmostEqual(self.contract_line.price_subtotal, 50.0)
|
||||
res = self.contract_line._onchange_product_id()
|
||||
self.assertIn('uom_id', res['domain'])
|
||||
self.contract_line.price_unit = 100.0
|
||||
|
||||
self.contract.partner_id = False
|
||||
with self.assertRaises(ValidationError):
|
||||
self.contract.recurring_create_invoice()
|
||||
self.contract.partner_id = self.partner.id
|
||||
|
||||
self.contract.recurring_create_invoice()
|
||||
self.sale_monthly = self.env['sale.order'].search(
|
||||
[('project_id', '=', self.contract.id),
|
||||
('state', '=', 'draft')])
|
||||
self.assertTrue(self.sale_monthly)
|
||||
self.assertEqual(self.contract.recurring_next_date, '2017-02-28')
|
||||
|
||||
self.sale_line = self.sale_monthly.order_line[0]
|
||||
self.assertAlmostEqual(self.sale_line.price_subtotal, 50.0)
|
||||
self.assertEqual(self.contract.partner_id.user_id,
|
||||
self.sale_monthly.user_id)
|
||||
|
||||
def test_contract_autoconfirm(self):
|
||||
self.contract.sale_autoconfirm = True
|
||||
self.assertAlmostEqual(self.contract_line.price_subtotal, 50.0)
|
||||
res = self.contract_line._onchange_product_id()
|
||||
self.assertIn('uom_id', res['domain'])
|
||||
self.contract_line.price_unit = 100.0
|
||||
|
||||
self.contract.partner_id = False
|
||||
with self.assertRaises(ValidationError):
|
||||
self.contract.recurring_create_invoice()
|
||||
self.contract.partner_id = self.partner.id
|
||||
|
||||
self.contract.recurring_create_invoice()
|
||||
self.sale_monthly = self.env['sale.order'].search(
|
||||
[('project_id', '=', self.contract.id),
|
||||
('state', '=', 'sale')])
|
||||
self.assertTrue(self.sale_monthly)
|
||||
self.assertEqual(self.contract.recurring_next_date, '2017-02-28')
|
||||
|
||||
self.sale_line = self.sale_monthly.order_line[0]
|
||||
self.assertAlmostEqual(self.sale_line.price_subtotal, 50.0)
|
||||
self.assertEqual(self.contract.partner_id.user_id,
|
||||
self.sale_monthly.user_id)
|
||||
|
||||
def test_onchange_contract_template_id(self):
|
||||
""" It should change the contract values to match the template. """
|
||||
self.contract.contract_template_id = self.template
|
||||
self.contract._onchange_contract_template_id()
|
||||
res = {
|
||||
'recurring_rule_type': self.contract.recurring_rule_type,
|
||||
'recurring_interval': self.contract.recurring_interval,
|
||||
'type': 'sale',
|
||||
'sale_autoconfirm': False
|
||||
}
|
||||
del self.template_vals['name']
|
||||
self.assertDictEqual(res, self.template_vals)
|
||||
@@ -0,0 +1,39 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
|
||||
<record id="account_analytic_account_recurring_sale_form" model="ir.ui.view">
|
||||
<field name="name">account.analytic.account.invoice.recurring.sale.form</field>
|
||||
<field name="model">account.analytic.account</field>
|
||||
<field name="inherit_id" ref="contract.account_analytic_account_recurring_form_form"/>
|
||||
<field name="arch" type="xml">
|
||||
<xpath expr="//field[@name='recurring_invoicing_type']" position="before">
|
||||
<field name="type"/>
|
||||
<field name="sale_autoconfirm" attrs="{'invisible':[('type','!=', 'sale')]}" />
|
||||
</xpath>
|
||||
<xpath expr="//button[@name='recurring_create_invoice']" position="attributes">
|
||||
<attribute name="attrs">{'invisible': ['|',('recurring_invoices','!=',True),('type','!=','invoice')]}</attribute>
|
||||
</xpath>
|
||||
<xpath expr="//button[@name='recurring_create_invoice']" position="before">
|
||||
<button name="recurring_create_invoice"
|
||||
type="object"
|
||||
attrs="{'invisible': ['|',('recurring_invoices','!=',True),('type','!=','sale')]}"
|
||||
string="Create sales"
|
||||
class="oe_link"
|
||||
groups="base.group_no_one"
|
||||
/>
|
||||
</xpath>
|
||||
<xpath expr="//button[@name='contract.act_recurring_invoices']" position="attributes">
|
||||
<attribute name="attrs">{'invisible': ['|',('recurring_invoices','!=',True),('type','!=','invoice')]}</attribute>
|
||||
</xpath>
|
||||
<xpath expr="//button[@name='contract.act_recurring_invoices']" position="before">
|
||||
<button name="contract_sale_generation.act_recurring_sales"
|
||||
type="action"
|
||||
attrs="{'invisible': ['|',('recurring_invoices','!=',True),('type','!=','sale')]}"
|
||||
string="⇒ Show recurring sales"
|
||||
class="oe_link"
|
||||
/>
|
||||
</xpath>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
</odoo>
|
||||
@@ -0,0 +1,15 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
|
||||
<record id="account_analytic_contract_sale_view_form" model="ir.ui.view">
|
||||
<field name="name">Account Analytic Contract Sale Form View</field>
|
||||
<field name="model">account.analytic.contract</field>
|
||||
<field name="inherit_id" ref="contract.account_analytic_contract_view_form"/>
|
||||
<field name="arch" type="xml">
|
||||
<xpath expr="//field[@name='recurring_invoicing_type']" position="before">
|
||||
<field name="type"/>
|
||||
</xpath>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
</odoo>
|
||||
15
contract_sale_generation/views/sale_view.xml
Normal file
15
contract_sale_generation/views/sale_view.xml
Normal file
@@ -0,0 +1,15 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
|
||||
<record id="act_recurring_sales" model="ir.actions.act_window">
|
||||
<field name="context">{'search_default_project_id':
|
||||
[active_id],
|
||||
'default_project_id': active_id}
|
||||
</field>
|
||||
<field name="name">Sales</field>
|
||||
<field name="res_model">sale.order</field>
|
||||
<field name="view_id" ref="sale.view_order_tree" />
|
||||
<field name="search_view_id" ref="sale.sale_order_view_search_inherit_sale"/>
|
||||
</record>
|
||||
|
||||
</odoo>
|
||||
Reference in New Issue
Block a user