Browse Source

ADD contract_sale_generation module

pull/329/head
Angel Moya Pardo 7 years ago
committed by Sylvain Van Hoof
parent
commit
ed5637a392
  1. 57
      contract_sale_generation/README.rst
  2. 2
      contract_sale_generation/__init__.py
  3. 22
      contract_sale_generation/__manifest__.py
  4. 5
      contract_sale_generation/models/__init__.py
  5. 80
      contract_sale_generation/models/account_analytic_account.py
  6. 20
      contract_sale_generation/models/account_analytic_contract.py
  7. 5
      contract_sale_generation/tests/__init__.py
  8. 87
      contract_sale_generation/tests/test_contract_invoice.py
  9. 113
      contract_sale_generation/tests/test_contract_sale.py
  10. 39
      contract_sale_generation/views/account_analytic_account_view.xml
  11. 15
      contract_sale_generation/views/account_analytic_contract_view.xml
  12. 15
      contract_sale_generation/views/sale_view.xml

57
contract_sale_generation/README.rst

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

@ -0,0 +1,2 @@
# -*- coding: utf-8 -*-
from . import models

22
contract_sale_generation/__manifest__.py

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

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

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

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

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

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

@ -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)

39
contract_sale_generation/views/account_analytic_account_view.xml

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

15
contract_sale_generation/views/account_analytic_contract_view.xml

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

@ -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>
Loading…
Cancel
Save