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