Skip to content

Commit

Permalink
ADD contract_sale_generation module
Browse files Browse the repository at this point in the history
  • Loading branch information
angelmoya authored and Bastian Guenther committed Jan 12, 2023
1 parent d1551c0 commit 06fb3c0
Show file tree
Hide file tree
Showing 12 changed files with 460 additions and 0 deletions.
57 changes: 57 additions & 0 deletions contract_sale_generation/README.rst
Original file line number Diff line number Diff line change
@@ -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 changes: 2 additions & 0 deletions contract_sale_generation/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
# -*- coding: utf-8 -*-
from . import models
22 changes: 22 additions & 0 deletions contract_sale_generation/__manifest__.py
Original file line number Diff line number Diff line change
@@ -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 changes: 5 additions & 0 deletions contract_sale_generation/models/__init__.py
Original file line number Diff line number Diff line change
@@ -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 changes: 80 additions & 0 deletions contract_sale_generation/models/account_analytic_account.py
Original file line number Diff line number Diff line change
@@ -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 changes: 20 additions & 0 deletions contract_sale_generation/models/account_analytic_contract.py
Original file line number Diff line number Diff line change
@@ -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 changes: 5 additions & 0 deletions contract_sale_generation/tests/__init__.py
Original file line number Diff line number Diff line change
@@ -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 changes: 87 additions & 0 deletions contract_sale_generation/tests/test_contract_invoice.py
Original file line number Diff line number Diff line change
@@ -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 changes: 113 additions & 0 deletions contract_sale_generation/tests/test_contract_sale.py
Original file line number Diff line number Diff line change
@@ -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)
Loading

0 comments on commit 06fb3c0

Please sign in to comment.