diff --git a/contract_sale_generation/README.rst b/contract_sale_generation/README.rst new file mode 100644 index 00000000..dda50022 --- /dev/null +++ b/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 +`_. 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 + +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. diff --git a/contract_sale_generation/__init__.py b/contract_sale_generation/__init__.py new file mode 100644 index 00000000..a0fdc10f --- /dev/null +++ b/contract_sale_generation/__init__.py @@ -0,0 +1,2 @@ +# -*- coding: utf-8 -*- +from . import models diff --git a/contract_sale_generation/__manifest__.py b/contract_sale_generation/__manifest__.py new file mode 100644 index 00000000..072fa461 --- /dev/null +++ b/contract_sale_generation/__manifest__.py @@ -0,0 +1,22 @@ +# -*- coding: utf-8 -*- +# Copyright 2017 Pesol () +# Copyright 2017 Angel Moya +# 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, +} diff --git a/contract_sale_generation/models/__init__.py b/contract_sale_generation/models/__init__.py new file mode 100644 index 00000000..a3782ea7 --- /dev/null +++ b/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 diff --git a/contract_sale_generation/models/account_analytic_account.py b/contract_sale_generation/models/account_analytic_account.py new file mode 100644 index 00000000..b29b4afd --- /dev/null +++ b/contract_sale_generation/models/account_analytic_account.py @@ -0,0 +1,80 @@ +# -*- coding: utf-8 -*- +# © 2004-2010 OpenERP SA +# © 2014 Angel Moya +# © 2015 Pedro M. Baeza +# © 2016 Carlos Dauden +# Copyright 2016-2017 LasLabs Inc. +# Copyright 2017 Pesol () +# Copyright 2017 Angel Moya +# 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 diff --git a/contract_sale_generation/models/account_analytic_contract.py b/contract_sale_generation/models/account_analytic_contract.py new file mode 100644 index 00000000..2db200da --- /dev/null +++ b/contract_sale_generation/models/account_analytic_contract.py @@ -0,0 +1,20 @@ +# -*- coding: utf-8 -*- +# Copyright 2017 Pesol () +# Copyright 2017 Angel Moya +# 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') diff --git a/contract_sale_generation/tests/__init__.py b/contract_sale_generation/tests/__init__.py new file mode 100644 index 00000000..dc292d5e --- /dev/null +++ b/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 diff --git a/contract_sale_generation/tests/test_contract_invoice.py b/contract_sale_generation/tests/test_contract_invoice.py new file mode 100644 index 00000000..5974b6bb --- /dev/null +++ b/contract_sale_generation/tests/test_contract_invoice.py @@ -0,0 +1,87 @@ +# -*- coding: utf-8 -*- +# © 2016 Carlos Dauden +# Copyright 2017 Pesol () +# Copyright 2017 Angel Moya +# 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) diff --git a/contract_sale_generation/tests/test_contract_sale.py b/contract_sale_generation/tests/test_contract_sale.py new file mode 100644 index 00000000..02aacac5 --- /dev/null +++ b/contract_sale_generation/tests/test_contract_sale.py @@ -0,0 +1,113 @@ +# -*- coding: utf-8 -*- +# © 2016 Carlos Dauden +# Copyright 2017 Pesol () +# Copyright 2017 Angel Moya +# 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) diff --git a/contract_sale_generation/views/account_analytic_account_view.xml b/contract_sale_generation/views/account_analytic_account_view.xml new file mode 100644 index 00000000..91578633 --- /dev/null +++ b/contract_sale_generation/views/account_analytic_account_view.xml @@ -0,0 +1,39 @@ + + + + + account.analytic.account.invoice.recurring.sale.form + account.analytic.account + + + + + + + + {'invisible': ['|',('recurring_invoices','!=',True),('type','!=','invoice')]} + + +