diff --git a/contract_sale_generation/README.rst b/contract_sale_generation/README.rst index e7ccfec97..24e5d2283 100644 --- a/contract_sale_generation/README.rst +++ b/contract_sale_generation/README.rst @@ -1,58 +1,87 @@ -.. image:: https://img.shields.io/badge/licence-AGPL--3-blue.svg +====================================== +Contracts Management - Recurring Sales +====================================== + +.. !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! This file is generated by oca-gen-addon-readme !! + !! changes will be overwritten. !! + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + +.. |badge1| image:: https://img.shields.io/badge/maturity-Beta-yellow.png + :target: https://odoo-community.org/page/development-status + :alt: Beta +.. |badge2| image:: https://img.shields.io/badge/licence-AGPL--3-blue.png :target: http://www.gnu.org/licenses/agpl-3.0-standalone.html :alt: License: AGPL-3 +.. |badge3| image:: https://img.shields.io/badge/github-OCA%2Fcontract-lightgray.png?logo=github + :target: https://github.com/OCA/contract/tree/12.0/contract_sale_generation + :alt: OCA/contract +.. |badge4| image:: https://img.shields.io/badge/weblate-Translate%20me-F47D42.png + :target: https://translation.odoo-community.org/projects/contract-12-0/contract-12-0-contract_sale_generation + :alt: Translate me on Weblate +.. |badge5| image:: https://img.shields.io/badge/runbot-Try%20me-875A7B.png + :target: https://runbot.odoo-community.org/runbot/110/12.0 + :alt: Try me on Runbot -============================= -Contracts for recurrent sales -============================= +|badge1| |badge2| |badge3| |badge4| |badge5| This module extends functionality of contracts to be able to generate sales orders instead of invoices. +**Table of contents** + +.. contents:: + :local: + Usage ===== To use this module, you need to: -#. Go to Accounting -> Contracts and select or create a new contract. -#. Check *Generate recurring invoices automatically*. +#. Go to Sales -> Contracts and select or create a new contract. #. 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. +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 `_. + +Do not contact contributors directly about support or help with technical issues. Credits ======= +Authors +~~~~~~~ + +* PESOL + Contributors ------------- +~~~~~~~~~~~~ * Angel Moya * Florent THOMAS +* Serpent Consulting Services Pvt. Ltd. -Maintainer ----------- +Maintainers +~~~~~~~~~~~ + +This module is maintained by the OCA. .. 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. +This module is part of the `OCA/contract `_ project on GitHub. + +You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute. diff --git a/contract_sale_generation/__init__.py b/contract_sale_generation/__init__.py index a0fdc10fe..a9e337226 100644 --- a/contract_sale_generation/__init__.py +++ b/contract_sale_generation/__init__.py @@ -1,2 +1,2 @@ -# -*- coding: utf-8 -*- + from . import models diff --git a/contract_sale_generation/__manifest__.py b/contract_sale_generation/__manifest__.py index f20c1a7aa..0c68fd648 100644 --- a/contract_sale_generation/__manifest__.py +++ b/contract_sale_generation/__manifest__.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2017 Pesol () # Copyright 2017 Angel Moya # License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl.html) @@ -6,7 +5,7 @@ { 'name': 'Contracts Management - Recurring Sales', - 'version': '10.0.3.0.0', + 'version': '12.0.1.0.0', 'category': 'Contract Management', 'license': 'AGPL-3', 'author': "PESOL, " @@ -14,10 +13,9 @@ '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', 'data/contract_cron.xml', + 'views/contract.xml', + 'views/contract_template.xml', ], 'installable': True, } diff --git a/contract_sale_generation/data/contract_cron.xml b/contract_sale_generation/data/contract_cron.xml index d4d6f8fde..841f52971 100644 --- a/contract_sale_generation/data/contract_cron.xml +++ b/contract_sale_generation/data/contract_cron.xml @@ -1,14 +1,15 @@ - - + Generate Recurring sales from Contracts + + code + model.cron_recurring_create_sale() + 1 days -1 - - - + diff --git a/contract_sale_generation/models/__init__.py b/contract_sale_generation/models/__init__.py index a3782ea72..5a5b6e24b 100644 --- a/contract_sale_generation/models/__init__.py +++ b/contract_sale_generation/models/__init__.py @@ -1,5 +1,6 @@ -# -*- 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 +from . import abstract_contract +from . import contract +from . import sale_order_line +from . import contract_line diff --git a/contract_sale_generation/models/account_analytic_contract.py b/contract_sale_generation/models/abstract_contract.py similarity index 59% rename from contract_sale_generation/models/account_analytic_contract.py rename to contract_sale_generation/models/abstract_contract.py index 2db200daa..bda9742c1 100644 --- a/contract_sale_generation/models/account_analytic_contract.py +++ b/contract_sale_generation/models/abstract_contract.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2017 Pesol () # Copyright 2017 Angel Moya # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). @@ -6,15 +5,15 @@ from odoo import fields, models -class AccountAnalyticContract(models.Model): - _inherit = 'account.analytic.contract' +class ContractAbstractContract(models.AbstractModel): + _inherit = 'contract.abstract.contract' type = fields.Selection( + [('invoice', 'Invoice'), + ('sale', 'Sale')], string='Type', - selection=[('invoice', 'Invoice'), - ('sale', 'Sale')], default='invoice', required=True, ) sale_autoconfirm = fields.Boolean( - string='Sale autoconfirm') + string='Sale Autoconfirm') diff --git a/contract_sale_generation/models/account_analytic_account.py b/contract_sale_generation/models/account_analytic_account.py deleted file mode 100644 index 79405fd75..000000000 --- a/contract_sale_generation/models/account_analytic_account.py +++ /dev/null @@ -1,121 +0,0 @@ -# -*- 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 -# Copyright 2018 Therp BV . -# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). - -from odoo import api, models, fields -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, - 'product_qty': line.quantity, - 'product_uom_qty': line.quantity, - 'product_uom': line.uom_id.id, - }) - # Get other sale line values from product onchange - sale_line.product_id_change() - sale_line_vals = sale_line._convert_to_write(sale_line._cache) - # Insert markers - name = self._insert_markers(line.name) - 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): - """ - Create invoices - @param self: single record of account.invoice - @return: MUST return an invoice recordset - """ - self.ensure_one() - if self.type == 'invoice': - return super(AccountAnalyticAccount, self)._create_invoice() - else: - return self.env['account.invoice'] - - @api.multi - def _create_sale(self): - """ - Create Sale orders - @param self: single record of sale.order - @return: MUST return a sale.order recordset - """ - self.ensure_one() - if self.type == 'sale': - 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 - else: - return self.env['sale.order'] - - @api.multi - def recurring_create_sale(self): - """ - Create sales from contracts - :return: sales created - """ - sales = self.env['sale.order'] - for contract in self: - if not contract.check_dates_valid(): - continue - # Re-read contract with correct company - ctx = contract.get_invoice_context() - sales |= contract.with_context(ctx)._create_sale() - contract.write({ - 'recurring_next_date': fields.Date.to_string(ctx['next_date']) - }) - return sales - - @api.model - def cron_recurring_create_sale(self): - today = fields.Date.today() - contracts = self.search([ - ('recurring_invoices', '=', True), - ('recurring_next_date', '<=', today), - '|', - ('date_end', '=', False), - ('date_end', '>=', today), - ]) - return contracts.recurring_create_sale() diff --git a/contract_sale_generation/models/contract.py b/contract_sale_generation/models/contract.py new file mode 100644 index 000000000..17c622d12 --- /dev/null +++ b/contract_sale_generation/models/contract.py @@ -0,0 +1,163 @@ +# © 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 +# Copyright 2018 Therp BV . +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +from odoo import api, fields, models, _ + + +class ContractContract(models.Model): + _inherit = 'contract.contract' + + sale_count = fields.Integer(compute="_compute_sale_count") + + @api.multi + def _prepare_sale(self, date_ref): + self.ensure_one() + sale = self.env['sale.order'].new({ + 'partner_id': self.partner_id, + 'date_order': fields.Date.to_string(date_ref), + 'origin': self.name, + 'company_id': self.company_id.id, + 'user_id': self.partner_id.user_id.id, + }) + if self.payment_term_id: + sale.payment_term_id = self.payment_term_id.id + if self.fiscal_position_id: + sale.fiscal_position_id = self.fiscal_position_id.id + # Get other sale values from partner onchange + sale.onchange_partner_id() + return sale._convert_to_write(sale._cache) + + @api.multi + def _get_related_sales(self): + self.ensure_one() + sales = (self.env['sale.order.line'] + .search([('contract_line_id', 'in', + self.contract_line_ids.ids) + ]).mapped('order_id')) + return sales + + @api.multi + def _compute_sale_count(self): + for rec in self: + rec.sale_count = len(rec._get_related_sales()) + + @api.multi + def action_show_sales(self): + self.ensure_one() + tree_view = self.env.ref('sale.view_order_tree', + raise_if_not_found=False) + form_view = self.env.ref('sale.view_order_form', + raise_if_not_found=False) + action = { + 'type': 'ir.actions.act_window', + 'name': 'Sales Orders', + 'res_model': 'sale.order', + 'view_type': 'form', + 'view_mode': 'tree,kanban,form,calendar,pivot,graph,activity', + 'domain': [('id', 'in', self._get_related_sales().ids)], + } + if tree_view and form_view: + action['views'] = [(tree_view.id, 'tree'), (form_view.id, 'form')] + return action + + @api.multi + def recurring_create_sale(self): + """ + This method triggers the creation of the next sale order of the + contracts even if their next sale order date is in the future. + """ + sales = self._recurring_create_sale() + for sale_rec in sales: + self.message_post( + body=_( + 'Contract manually sale order: ' + '' + 'Sale Order' + '' + ) + % (sale_rec._name, sale_rec.id) + ) + return sales + + @api.multi + def _prepare_recurring_sales_values(self, date_ref=False): + """ + This method builds the list of sales values to create, based on + the lines to sale of the contracts in self. + !!! The date of next invoice (recurring_next_date) is updated here !!! + :return: list of dictionaries (invoices values) + """ + sales_values = [] + for contract in self: + if not date_ref: + date_ref = contract.recurring_next_date + if not date_ref: + # this use case is possible when recurring_create_invoice is + # called for a finished contract + continue + contract_lines = contract._get_lines_to_invoice(date_ref) + if not contract_lines: + continue + sale_values = contract._prepare_sale(date_ref) + for line in contract_lines: + sale_values.setdefault('order_line', []) + invoice_line_values = line._prepare_sale_line( + sale_values=sale_values, + ) + if invoice_line_values: + sale_values['order_line'].append( + (0, 0, invoice_line_values) + ) + sales_values.append(sale_values) + contract_lines._update_recurring_next_date() + return sales_values + + @api.multi + def _recurring_create_sale(self, date_ref=False): + sales_values = self._prepare_recurring_sales_values(date_ref) + so_rec = self.env["sale.order"].create(sales_values) + for rec in self.filtered(lambda c: c.sale_autoconfirm): + so_rec.action_confirm() + return so_rec + + @api.model + def cron_recurring_create_sale(self, date_ref=None): + if not date_ref: + date_ref = fields.Date.context_today(self) + domain = self._get_contracts_to_invoice_domain(date_ref) + domain.extend([('type', '=', 'sale')]) + sales = self.env["sale.order"] + # Sales by companies, so assignation emails get correct context + companies_to_sale = self.read_group( + domain, ["company_id"], ["company_id"]) + for row in companies_to_sale: + contracts_to_sale = self.search(row["__domain"]).with_context( + allowed_company_ids=[row["company_id"][0]] + ) + sales |= contracts_to_sale._recurring_create_sale(date_ref) + return sales + + @api.model + def cron_recurring_create_invoice(self, date_ref=None): + if not date_ref: + date_ref = fields.Date.context_today(self) + domain = self._get_contracts_to_invoice_domain(date_ref) + domain.extend([('type', '=', 'invoice')]) + invoices = self.env["account.invoice"] + # Invoice by companies, so assignation emails get correct context + companies_to_invoice = self.read_group( + domain, ["company_id"], ["company_id"]) + for row in companies_to_invoice: + contracts_to_invoice = self.search(row["__domain"]).with_context( + allowed_company_ids=[row["company_id"][0]] + ) + invoices |= contracts_to_invoice._recurring_create_invoice( + date_ref) + return invoices diff --git a/contract_sale_generation/models/contract_line.py b/contract_sale_generation/models/contract_line.py new file mode 100644 index 000000000..02dd76ad1 --- /dev/null +++ b/contract_sale_generation/models/contract_line.py @@ -0,0 +1,46 @@ +# Copyright (C) 2020 Open Source Integrators +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +from odoo import api, models + + +class ContractLine(models.Model): + _inherit = 'contract.line' + + @api.multi + def _prepare_sale_line(self, order_id=False, sale_values=False): + self.ensure_one() + dates = self._get_period_to_invoice( + self.last_date_invoiced, self.recurring_next_date + ) + sale_line_vals = { + 'product_id': self.product_id.id, + 'quantity': self._get_quantity_to_invoice(*dates), + 'uom_id': self.uom_id.id, + 'discount': self.discount, + 'contract_line_id': self.id, + } + if order_id: + sale_line_vals['order_id'] = order_id.id + order_line = self.env['sale.order.line'].with_context( + force_company=self.contract_id.company_id.id, + ).new(sale_line_vals) + if sale_values and not order_id: + sale = self.env['sale.order'].with_context( + force_company=self.contract_id.company_id.id, + ).new(sale_values) + order_line.order_id = sale + # Get other order line values from product onchange + order_line.product_id_change() + sale_line_vals = order_line._convert_to_write(order_line._cache) + # Insert markers + name = self._insert_markers(dates[0], dates[1]) + sale_line_vals.update( + { + 'sequence': self.sequence, + 'name': name, + 'analytic_tag_ids': [(6, 0, self.analytic_tag_ids.ids)], + 'price_unit': self.price_unit, + } + ) + return sale_line_vals diff --git a/contract_sale_generation/models/sale_order_line.py b/contract_sale_generation/models/sale_order_line.py new file mode 100644 index 000000000..08abb1332 --- /dev/null +++ b/contract_sale_generation/models/sale_order_line.py @@ -0,0 +1,12 @@ +# Copyright (C) 2020 Open Source Integrators +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +from odoo import fields, models + + +class SaleOrderLine(models.Model): + _inherit = 'sale.order.line' + + contract_line_id = fields.Many2one( + 'contract.line', string='Contract Line', index=True + ) diff --git a/contract_sale_generation/readme/CONTRIBUTORS.rst b/contract_sale_generation/readme/CONTRIBUTORS.rst new file mode 100644 index 000000000..4c26c0480 --- /dev/null +++ b/contract_sale_generation/readme/CONTRIBUTORS.rst @@ -0,0 +1,3 @@ +* Angel Moya +* Florent THOMAS +* Serpent Consulting Services Pvt. Ltd. diff --git a/contract_sale_generation/readme/DESCRIPTION.rst b/contract_sale_generation/readme/DESCRIPTION.rst new file mode 100644 index 000000000..2ea92d9eb --- /dev/null +++ b/contract_sale_generation/readme/DESCRIPTION.rst @@ -0,0 +1,2 @@ +This module extends functionality of contracts to be able to generate sales +orders instead of invoices. diff --git a/contract_sale_generation/readme/USAGE.rst b/contract_sale_generation/readme/USAGE.rst new file mode 100644 index 000000000..4f7623f66 --- /dev/null +++ b/contract_sale_generation/readme/USAGE.rst @@ -0,0 +1,7 @@ +To use this module, you need to: + +#. Go to Sales -> Contracts and select or create a new contract. +#. 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" diff --git a/contract_sale_generation/static/description/index.html b/contract_sale_generation/static/description/index.html new file mode 100644 index 000000000..74937050d --- /dev/null +++ b/contract_sale_generation/static/description/index.html @@ -0,0 +1,435 @@ + + + + + + +Contracts Management - Recurring Sales + + + +
+

Contracts Management - Recurring Sales

+ + +

Beta License: AGPL-3 OCA/contract Translate me on Weblate Try me on Runbot

+

This module extends functionality of contracts to be able to generate sales +orders instead of invoices.

+

Table of contents

+ +
+

Usage

+

To use this module, you need to:

+
    +
  1. Go to Sales -> Contracts and select or create a new contract.
  2. +
  3. 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”
    • +
    +
  4. +
+
+
+

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.

+

Do not contact contributors directly about support or help with technical issues.

+
+
+

Credits

+
+

Authors

+
    +
  • PESOL
  • +
+
+
+

Contributors

+ +
+
+

Maintainers

+

This module is maintained by the OCA.

+Odoo Community Association +

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.

+

This module is part of the OCA/contract project on GitHub.

+

You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute.

+
+
+
+ + diff --git a/contract_sale_generation/tests/__init__.py b/contract_sale_generation/tests/__init__.py index a76c76cbf..87f76cff7 100644 --- a/contract_sale_generation/tests/__init__.py +++ b/contract_sale_generation/tests/__init__.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). from . import test_contract_sale diff --git a/contract_sale_generation/tests/test_contract_sale.py b/contract_sale_generation/tests/test_contract_sale.py index 035ca8b97..1f83a4e93 100644 --- a/contract_sale_generation/tests/test_contract_sale.py +++ b/contract_sale_generation/tests/test_contract_sale.py @@ -1,108 +1,209 @@ -# -*- 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 import fields from odoo.tests.common import TransactionCase +def to_date(date): + return fields.Date.to_date(date) + + 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.pricelist = self.env['product.pricelist'].create({ + 'name': 'pricelist for contract test', }) - 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#', + self.partner = self.env['res.partner'].create({ + 'name': 'partner test contract', + 'property_product_pricelist': self.pricelist.id, + }) + self.product_1 = self.env.ref('product.product_product_1') + self.product_1.taxes_id += self.env['account.tax'].search( + [('type_tax_use', '=', 'sale')], limit=1 + ) + self.product_1.description_sale = 'Test description sale' + self.line_template_vals = { + 'product_id': self.product_1.id, + 'name': 'Test Contract Template', 'quantity': 1, - 'uom_id': self.product.uom_id.id, + 'uom_id': self.product_1.uom_id.id, 'price_unit': 100, 'discount': 50, - }) + 'recurring_rule_type': 'yearly', + 'recurring_interval': 1, + } + self.template_vals = { + 'name': 'Test Contract Template', + 'contract_line_ids': [ + (0, 0, self.line_template_vals), + ], + } + self.template = self.env['contract.template'].create( + self.template_vals + ) + # For being sure of the applied price + self.env['product.pricelist.item'].create( + { + 'pricelist_id': self.partner.property_product_pricelist.id, + 'product_id': self.product_1.id, + 'compute_price': 'formula', + 'base': 'list_price', + } + ) + self.contract = self.env['contract.contract'].create( + { + 'name': 'Test Contract', + 'partner_id': self.partner.id, + 'pricelist_id': self.partner.property_product_pricelist.id, + 'type': 'sale', + 'sale_autoconfirm': False + } + ) + self.line_vals = { + 'contract_id': self.contract.id, + 'product_id': self.product_1.id, + 'name': 'Services from #START# to #END#', + 'quantity': 1, + 'uom_id': self.product_1.uom_id.id, + 'price_unit': 100, + 'discount': 50, + 'recurring_rule_type': 'monthly', + 'recurring_interval': 1, + 'date_start': '2020-01-01', + 'recurring_next_date': '2020-01-15', + } + self.contract.contract_template_id = self.template + self.contract._onchange_contract_template_id() + self.contract_line = self.env['contract.line'].create( + self.line_vals + ) + self.contract2 = self.env['contract.contract'].create( + { + 'name': 'Test Contract 2', + 'type': 'sale', + 'partner_id': self.partner.id, + 'pricelist_id': self.partner.property_product_pricelist.id, + 'contract_type': 'purchase', + 'contract_line_ids': [ + ( + 0, + 0, + { + 'product_id': self.product_1.id, + 'name': 'Services from #START# to #END#', + 'quantity': 1, + 'uom_id': self.product_1.uom_id.id, + 'price_unit': 100, + 'discount': 50, + 'recurring_rule_type': 'monthly', + 'recurring_interval': 1, + 'date_start': '2018-02-15', + 'recurring_next_date': '2018-02-22', + }, + ) + ], + } + ) def test_check_discount(self): with self.assertRaises(ValidationError): self.contract_line.write({'discount': 120}) def test_contract(self): + recurring_next_date = to_date('2020-02-15') 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 = self.partner.id self.contract.recurring_create_sale() - self.sale_monthly = self.env['sale.order'].search( - [('project_id', '=', self.contract.id), - ('state', '=', 'draft')]) + self.sale_monthly = self.contract._get_related_sales() 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) + self.assertEqual( + self.contract_line.recurring_next_date, recurring_next_date + ) + self.order_line = self.sale_monthly.order_line[0] + self.assertTrue(self.order_line.tax_id) + self.assertAlmostEqual(self.order_line.price_subtotal, 50.0) + self.assertEqual(self.contract.user_id, self.sale_monthly.user_id) def test_contract_autoconfirm(self): + recurring_next_date = to_date('2020-02-15') 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 = self.partner.id self.contract.recurring_create_sale() - self.sale_monthly = self.env['sale.order'].search( - [('project_id', '=', self.contract.id), - ('state', '=', 'sale')]) + self.sale_monthly = self.contract._get_related_sales() 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) + self.assertEqual( + self.contract_line.recurring_next_date, recurring_next_date + ) + self.order_line = self.sale_monthly.order_line[0] + self.assertTrue(self.order_line.tax_id) + self.assertAlmostEqual(self.order_line.price_subtotal, 50.0) + self.assertEqual(self.contract.user_id, self.sale_monthly.user_id) def test_onchange_contract_template_id(self): - """ It should change the contract values to match the template. """ + """It should change the contract values to match the template.""" + self.contract.contract_template_id = False + self.contract._onchange_contract_template_id() 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 + 'contract_line_ids': + [(0, 0, { + 'product_id': self.product_1.id, + 'name': 'Test Contract Template', + 'quantity': 1, + 'uom_id': self.product_1.uom_id.id, + 'price_unit': 100, + 'discount': 50, + 'recurring_rule_type': 'yearly', + 'recurring_interval': 1, + })] } del self.template_vals['name'] self.assertDictEqual(res, self.template_vals) - def test_check_cron_ended_contract(self): - self.contract.recurring_next_date = '2016-02-29' - self.contract.recurring_rule_type = 'yearly' - self.contract.date_end = '2016-02-28' - sale_orders = self.contract.with_context( - cron=True).recurring_create_sale() - self.assertFalse(sale_orders) + def test_contract_count_sale(self): + self.contract.recurring_create_sale() + self.contract.recurring_create_sale() + self.contract.recurring_create_sale() + self.contract._compute_sale_count() + self.assertEqual(self.contract.sale_count, 3) + + def test_contract_count_sale_2(self): + orders = self.env['sale.order'] + orders |= self.contract.recurring_create_sale() + orders |= self.contract.recurring_create_sale() + orders |= self.contract.recurring_create_sale() + action = self.contract.action_show_sales() + self.assertEqual(set(action['domain'][0][2]), set(orders.ids)) + + def test_cron_recurring_create_sale(self): + self.contract_line.date_start = '2020-01-01' + self.contract_line.recurring_invoicing_type = 'post-paid' + self.contract_line.date_end = '2020-03-15' + self.contract_line._onchange_date_start() + contracts = self.contract2 + for _i in range(10): + contracts |= self.contract.copy({'type': 'sale'}) + self.env['contract.contract'].cron_recurring_create_sale() + order_lines = self.env['sale.order.line'].search( + [('contract_line_id', 'in', + contracts.mapped('contract_line_ids').ids)] + ) + self.assertEqual( + len(contracts.mapped('contract_line_ids')), + len(order_lines), + ) diff --git a/contract_sale_generation/views/account_analytic_account_view.xml b/contract_sale_generation/views/account_analytic_account_view.xml deleted file mode 100644 index 47a4ccec8..000000000 --- a/contract_sale_generation/views/account_analytic_account_view.xml +++ /dev/null @@ -1,39 +0,0 @@ - - - - - account.analytic.account.invoice.recurring.sale.form - account.analytic.account - - - - - - - - {'invisible': ['|',('recurring_invoices','!=',True),('type','!=','invoice')]} - - - + + + + + diff --git a/contract_sale_generation/views/contract_template.xml b/contract_sale_generation/views/contract_template.xml new file mode 100644 index 000000000..9a4707c5a --- /dev/null +++ b/contract_sale_generation/views/contract_template.xml @@ -0,0 +1,14 @@ + + + + contract.template form view (in contract) + contract.template + + + + + + + + + diff --git a/contract_sale_generation/views/sale_view.xml b/contract_sale_generation/views/sale_view.xml deleted file mode 100644 index e23bded0b..000000000 --- a/contract_sale_generation/views/sale_view.xml +++ /dev/null @@ -1,15 +0,0 @@ - - - - - {'search_default_project_id': - [active_id], - 'default_project_id': active_id} - - Sales - sale.order - - - - -