Angel Moya Pardo
7 years ago
committed by
Sylvain Van Hoof
12 changed files with 460 additions and 0 deletions
-
57contract_sale_generation/README.rst
-
2contract_sale_generation/__init__.py
-
22contract_sale_generation/__manifest__.py
-
5contract_sale_generation/models/__init__.py
-
80contract_sale_generation/models/account_analytic_account.py
-
20contract_sale_generation/models/account_analytic_contract.py
-
5contract_sale_generation/tests/__init__.py
-
87contract_sale_generation/tests/test_contract_invoice.py
-
113contract_sale_generation/tests/test_contract_sale.py
-
39contract_sale_generation/views/account_analytic_account_view.xml
-
15contract_sale_generation/views/account_analytic_contract_view.xml
-
15contract_sale_generation/views/sale_view.xml
@ -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. |
@ -0,0 +1,2 @@ |
|||||
|
# -*- coding: utf-8 -*- |
||||
|
from . import models |
@ -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, |
||||
|
} |
@ -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 |
@ -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 |
@ -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') |
@ -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 |
@ -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) |
@ -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> |
@ -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> |
Write
Preview
Loading…
Cancel
Save
Reference in new issue