[WIP][12.0][MIG] contract_sale_generation

This commit is contained in:
Murtuza Saleh
2020-09-30 22:01:22 +05:30
committed by Denis Roussel
parent f45535328e
commit 1fe5b39fc3
21 changed files with 952 additions and 291 deletions

View File

@@ -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 :target: http://www.gnu.org/licenses/agpl-3.0-standalone.html
:alt: License: AGPL-3 :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
============================= |badge1| |badge2| |badge3| |badge4| |badge5|
Contracts for recurrent sales
=============================
This module extends functionality of contracts to be able to generate sales This module extends functionality of contracts to be able to generate sales
orders instead of invoices. orders instead of invoices.
**Table of contents**
.. contents::
:local:
Usage Usage
===== =====
To use this module, you need to: To use this module, you need to:
#. Go to Accounting -> Contracts and select or create a new contract. #. Go to Sales -> Contracts and select or create a new contract.
#. Check *Generate recurring invoices automatically*.
#. Fill fields for selecting the recurrency and invoice parameters: #. Fill fields for selecting the recurrency and invoice parameters:
* Type defines document that contract will generate, can be "Sales" or "Invoices" * Type defines document that contract will generate, can be "Sales" or "Invoices"
* Sale Autoconfirm, validate Sales Orders if type is "Sales" * 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 Bug Tracker
=========== ===========
Bugs are tracked on `GitHub Issues Bugs are tracked on `GitHub Issues <https://github.com/OCA/contract/issues>`_.
<https://github.com/OCA/contract/issues>`_. In case of trouble, please In case of trouble, please check there if your issue has already been reported.
check there if your issue has already been reported. If you spotted it first, If you spotted it first, help us smashing it by providing a detailed and welcomed
help us smashing it by providing a detailed and welcomed feedback. `feedback <https://github.com/OCA/contract/issues/new?body=module:%20contract_sale_generation%0Aversion:%2012.0%0A%0A**Steps%20to%20reproduce**%0A-%20...%0A%0A**Current%20behavior**%0A%0A**Expected%20behavior**>`_.
Do not contact contributors directly about support or help with technical issues.
Credits Credits
======= =======
Authors
~~~~~~~
* PESOL
Contributors Contributors
------------ ~~~~~~~~~~~~
* Angel Moya <angel.moya@pesol.es> * Angel Moya <angel.moya@pesol.es>
* Florent THOMAS <florent.thomas@mind-and-go.com> * Florent THOMAS <florent.thomas@mind-and-go.com>
* Serpent Consulting Services Pvt. Ltd. <support@serpentcs.com>
Maintainer Maintainers
---------- ~~~~~~~~~~~
This module is maintained by the OCA.
.. image:: https://odoo-community.org/logo.png .. image:: https://odoo-community.org/logo.png
:alt: Odoo Community Association :alt: Odoo Community Association
:target: https://odoo-community.org :target: https://odoo-community.org
This module is maintained by the OCA.
OCA, or the Odoo Community Association, is a nonprofit organization whose OCA, or the Odoo Community Association, is a nonprofit organization whose
mission is to support the collaborative development of Odoo features and mission is to support the collaborative development of Odoo features and
promote its widespread use. promote its widespread use.
To contribute to this module, please visit https://odoo-community.org. This module is part of the `OCA/contract <https://github.com/OCA/contract/tree/12.0/contract_sale_generation>`_ project on GitHub.
You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute.

View File

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

View File

@@ -1,4 +1,3 @@
# -*- coding: utf-8 -*-
# Copyright 2017 Pesol (<http://pesol.es>) # Copyright 2017 Pesol (<http://pesol.es>)
# Copyright 2017 Angel Moya <angel.moya@pesol.es> # Copyright 2017 Angel Moya <angel.moya@pesol.es>
# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl.html) # License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl.html)
@@ -6,7 +5,7 @@
{ {
'name': 'Contracts Management - Recurring Sales', 'name': 'Contracts Management - Recurring Sales',
'version': '10.0.3.0.0', 'version': '12.0.1.0.0',
'category': 'Contract Management', 'category': 'Contract Management',
'license': 'AGPL-3', 'license': 'AGPL-3',
'author': "PESOL, " 'author': "PESOL, "
@@ -14,10 +13,9 @@
'website': 'https://github.com/oca/contract', 'website': 'https://github.com/oca/contract',
'depends': ['contract', 'sale'], 'depends': ['contract', 'sale'],
'data': [ 'data': [
'views/account_analytic_account_view.xml',
'views/account_analytic_contract_view.xml',
'views/sale_view.xml',
'data/contract_cron.xml', 'data/contract_cron.xml',
'views/contract.xml',
'views/contract_template.xml',
], ],
'installable': True, 'installable': True,
} }

View File

@@ -1,14 +1,15 @@
<?xml version="1.0" encoding='UTF-8'?>
<odoo> <odoo>
<record model="ir.cron" id="account_analytic_cron_for_sale"> <record id="contract_cron_for_sale" model="ir.cron">
<field name="name">Generate Recurring sales from Contracts</field> <field name="name">Generate Recurring sales from Contracts</field>
<field name="model_id" ref="model_contract_contract"/>
<field name="state">code</field>
<field name="code">model.cron_recurring_create_sale()</field>
<field name="user_id" ref="base.user_root" />
<field name="interval_number">1</field> <field name="interval_number">1</field>
<field name="interval_type">days</field> <field name="interval_type">days</field>
<field name="numbercall">-1</field> <field name="numbercall">-1</field>
<field name="model" eval="'account.analytic.account'"/> <field eval="False" name="doall" />
<field name="function" eval="'cron_recurring_create_sale'"/>
<field name="args" eval="'()'"/>
</record> </record>
</odoo> </odoo>

View File

@@ -1,5 +1,6 @@
# -*- coding: utf-8 -*-
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
from . import account_analytic_contract from . import abstract_contract
from . import account_analytic_account from . import contract
from . import sale_order_line
from . import contract_line

View File

@@ -1,4 +1,3 @@
# -*- coding: utf-8 -*-
# Copyright 2017 Pesol (<http://pesol.es>) # Copyright 2017 Pesol (<http://pesol.es>)
# Copyright 2017 Angel Moya <angel.moya@pesol.es> # Copyright 2017 Angel Moya <angel.moya@pesol.es>
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
@@ -6,15 +5,15 @@
from odoo import fields, models from odoo import fields, models
class AccountAnalyticContract(models.Model): class ContractAbstractContract(models.AbstractModel):
_inherit = 'account.analytic.contract' _inherit = 'contract.abstract.contract'
type = fields.Selection( type = fields.Selection(
string='Type', [('invoice', 'Invoice'),
selection=[('invoice', 'Invoice'),
('sale', 'Sale')], ('sale', 'Sale')],
string='Type',
default='invoice', default='invoice',
required=True, required=True,
) )
sale_autoconfirm = fields.Boolean( sale_autoconfirm = fields.Boolean(
string='Sale autoconfirm') string='Sale Autoconfirm')

View File

@@ -1,121 +0,0 @@
# -*- 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>
# Copyright 2018 Therp BV <https://therp.nl>.
# 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()

View File

@@ -0,0 +1,163 @@
# © 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>
# Copyright 2018 Therp BV <https://therp.nl>.
# 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: '
'<a href="#" data-oe-model="%s" data-oe-id="%s">'
'Sale Order'
'</a>'
)
% (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

View File

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

View File

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

View File

@@ -0,0 +1,3 @@
* Angel Moya <angel.moya@pesol.es>
* Florent THOMAS <florent.thomas@mind-and-go.com>
* Serpent Consulting Services Pvt. Ltd. <support@serpentcs.com>

View File

@@ -0,0 +1,2 @@
This module extends functionality of contracts to be able to generate sales
orders instead of invoices.

View File

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

View File

@@ -0,0 +1,435 @@
<?xml version="1.0" encoding="utf-8" ?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<meta name="generator" content="Docutils 0.15.1: http://docutils.sourceforge.net/" />
<title>Contracts Management - Recurring Sales</title>
<style type="text/css">
/*
:Author: David Goodger (goodger@python.org)
:Id: $Id: html4css1.css 7952 2016-07-26 18:15:59Z milde $
:Copyright: This stylesheet has been placed in the public domain.
Default cascading style sheet for the HTML output of Docutils.
See http://docutils.sf.net/docs/howto/html-stylesheets.html for how to
customize this style sheet.
*/
/* used to remove borders from tables and images */
.borderless, table.borderless td, table.borderless th {
border: 0 }
table.borderless td, table.borderless th {
/* Override padding for "table.docutils td" with "! important".
The right padding separates the table cells. */
padding: 0 0.5em 0 0 ! important }
.first {
/* Override more specific margin styles with "! important". */
margin-top: 0 ! important }
.last, .with-subtitle {
margin-bottom: 0 ! important }
.hidden {
display: none }
.subscript {
vertical-align: sub;
font-size: smaller }
.superscript {
vertical-align: super;
font-size: smaller }
a.toc-backref {
text-decoration: none ;
color: black }
blockquote.epigraph {
margin: 2em 5em ; }
dl.docutils dd {
margin-bottom: 0.5em }
object[type="image/svg+xml"], object[type="application/x-shockwave-flash"] {
overflow: hidden;
}
/* Uncomment (and remove this text!) to get bold-faced definition list terms
dl.docutils dt {
font-weight: bold }
*/
div.abstract {
margin: 2em 5em }
div.abstract p.topic-title {
font-weight: bold ;
text-align: center }
div.admonition, div.attention, div.caution, div.danger, div.error,
div.hint, div.important, div.note, div.tip, div.warning {
margin: 2em ;
border: medium outset ;
padding: 1em }
div.admonition p.admonition-title, div.hint p.admonition-title,
div.important p.admonition-title, div.note p.admonition-title,
div.tip p.admonition-title {
font-weight: bold ;
font-family: sans-serif }
div.attention p.admonition-title, div.caution p.admonition-title,
div.danger p.admonition-title, div.error p.admonition-title,
div.warning p.admonition-title, .code .error {
color: red ;
font-weight: bold ;
font-family: sans-serif }
/* Uncomment (and remove this text!) to get reduced vertical space in
compound paragraphs.
div.compound .compound-first, div.compound .compound-middle {
margin-bottom: 0.5em }
div.compound .compound-last, div.compound .compound-middle {
margin-top: 0.5em }
*/
div.dedication {
margin: 2em 5em ;
text-align: center ;
font-style: italic }
div.dedication p.topic-title {
font-weight: bold ;
font-style: normal }
div.figure {
margin-left: 2em ;
margin-right: 2em }
div.footer, div.header {
clear: both;
font-size: smaller }
div.line-block {
display: block ;
margin-top: 1em ;
margin-bottom: 1em }
div.line-block div.line-block {
margin-top: 0 ;
margin-bottom: 0 ;
margin-left: 1.5em }
div.sidebar {
margin: 0 0 0.5em 1em ;
border: medium outset ;
padding: 1em ;
background-color: #ffffee ;
width: 40% ;
float: right ;
clear: right }
div.sidebar p.rubric {
font-family: sans-serif ;
font-size: medium }
div.system-messages {
margin: 5em }
div.system-messages h1 {
color: red }
div.system-message {
border: medium outset ;
padding: 1em }
div.system-message p.system-message-title {
color: red ;
font-weight: bold }
div.topic {
margin: 2em }
h1.section-subtitle, h2.section-subtitle, h3.section-subtitle,
h4.section-subtitle, h5.section-subtitle, h6.section-subtitle {
margin-top: 0.4em }
h1.title {
text-align: center }
h2.subtitle {
text-align: center }
hr.docutils {
width: 75% }
img.align-left, .figure.align-left, object.align-left, table.align-left {
clear: left ;
float: left ;
margin-right: 1em }
img.align-right, .figure.align-right, object.align-right, table.align-right {
clear: right ;
float: right ;
margin-left: 1em }
img.align-center, .figure.align-center, object.align-center {
display: block;
margin-left: auto;
margin-right: auto;
}
table.align-center {
margin-left: auto;
margin-right: auto;
}
.align-left {
text-align: left }
.align-center {
clear: both ;
text-align: center }
.align-right {
text-align: right }
/* reset inner alignment in figures */
div.align-right {
text-align: inherit }
/* div.align-center * { */
/* text-align: left } */
.align-top {
vertical-align: top }
.align-middle {
vertical-align: middle }
.align-bottom {
vertical-align: bottom }
ol.simple, ul.simple {
margin-bottom: 1em }
ol.arabic {
list-style: decimal }
ol.loweralpha {
list-style: lower-alpha }
ol.upperalpha {
list-style: upper-alpha }
ol.lowerroman {
list-style: lower-roman }
ol.upperroman {
list-style: upper-roman }
p.attribution {
text-align: right ;
margin-left: 50% }
p.caption {
font-style: italic }
p.credits {
font-style: italic ;
font-size: smaller }
p.label {
white-space: nowrap }
p.rubric {
font-weight: bold ;
font-size: larger ;
color: maroon ;
text-align: center }
p.sidebar-title {
font-family: sans-serif ;
font-weight: bold ;
font-size: larger }
p.sidebar-subtitle {
font-family: sans-serif ;
font-weight: bold }
p.topic-title {
font-weight: bold }
pre.address {
margin-bottom: 0 ;
margin-top: 0 ;
font: inherit }
pre.literal-block, pre.doctest-block, pre.math, pre.code {
margin-left: 2em ;
margin-right: 2em }
pre.code .ln { color: grey; } /* line numbers */
pre.code, code { background-color: #eeeeee }
pre.code .comment, code .comment { color: #5C6576 }
pre.code .keyword, code .keyword { color: #3B0D06; font-weight: bold }
pre.code .literal.string, code .literal.string { color: #0C5404 }
pre.code .name.builtin, code .name.builtin { color: #352B84 }
pre.code .deleted, code .deleted { background-color: #DEB0A1}
pre.code .inserted, code .inserted { background-color: #A3D289}
span.classifier {
font-family: sans-serif ;
font-style: oblique }
span.classifier-delimiter {
font-family: sans-serif ;
font-weight: bold }
span.interpreted {
font-family: sans-serif }
span.option {
white-space: nowrap }
span.pre {
white-space: pre }
span.problematic {
color: red }
span.section-subtitle {
/* font-size relative to parent (h1..h6 element) */
font-size: 80% }
table.citation {
border-left: solid 1px gray;
margin-left: 1px }
table.docinfo {
margin: 2em 4em }
table.docutils {
margin-top: 0.5em ;
margin-bottom: 0.5em }
table.footnote {
border-left: solid 1px black;
margin-left: 1px }
table.docutils td, table.docutils th,
table.docinfo td, table.docinfo th {
padding-left: 0.5em ;
padding-right: 0.5em ;
vertical-align: top }
table.docutils th.field-name, table.docinfo th.docinfo-name {
font-weight: bold ;
text-align: left ;
white-space: nowrap ;
padding-left: 0 }
/* "booktabs" style (no vertical lines) */
table.docutils.booktabs {
border: 0px;
border-top: 2px solid;
border-bottom: 2px solid;
border-collapse: collapse;
}
table.docutils.booktabs * {
border: 0px;
}
table.docutils.booktabs th {
border-bottom: thin solid;
text-align: left;
}
h1 tt.docutils, h2 tt.docutils, h3 tt.docutils,
h4 tt.docutils, h5 tt.docutils, h6 tt.docutils {
font-size: 100% }
ul.auto-toc {
list-style-type: none }
</style>
</head>
<body>
<div class="document" id="contracts-management-recurring-sales">
<h1 class="title">Contracts Management - Recurring Sales</h1>
<!-- !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
!! This file is generated by oca-gen-addon-readme !!
!! changes will be overwritten. !!
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! -->
<p><a class="reference external" href="https://odoo-community.org/page/development-status"><img alt="Beta" src="https://img.shields.io/badge/maturity-Beta-yellow.png" /></a> <a class="reference external" href="http://www.gnu.org/licenses/agpl-3.0-standalone.html"><img alt="License: AGPL-3" src="https://img.shields.io/badge/licence-AGPL--3-blue.png" /></a> <a class="reference external" href="https://github.com/OCA/contract/tree/12.0/contract_sale_generation"><img alt="OCA/contract" src="https://img.shields.io/badge/github-OCA%2Fcontract-lightgray.png?logo=github" /></a> <a class="reference external" href="https://translation.odoo-community.org/projects/contract-12-0/contract-12-0-contract_sale_generation"><img alt="Translate me on Weblate" src="https://img.shields.io/badge/weblate-Translate%20me-F47D42.png" /></a> <a class="reference external" href="https://runbot.odoo-community.org/runbot/110/12.0"><img alt="Try me on Runbot" src="https://img.shields.io/badge/runbot-Try%20me-875A7B.png" /></a></p>
<p>This module extends functionality of contracts to be able to generate sales
orders instead of invoices.</p>
<p><strong>Table of contents</strong></p>
<div class="contents local topic" id="contents">
<ul class="simple">
<li><a class="reference internal" href="#usage" id="id1">Usage</a></li>
<li><a class="reference internal" href="#bug-tracker" id="id2">Bug Tracker</a></li>
<li><a class="reference internal" href="#credits" id="id3">Credits</a><ul>
<li><a class="reference internal" href="#authors" id="id4">Authors</a></li>
<li><a class="reference internal" href="#contributors" id="id5">Contributors</a></li>
<li><a class="reference internal" href="#maintainers" id="id6">Maintainers</a></li>
</ul>
</li>
</ul>
</div>
<div class="section" id="usage">
<h1><a class="toc-backref" href="#id1">Usage</a></h1>
<p>To use this module, you need to:</p>
<ol class="arabic simple">
<li>Go to Sales -&gt; Contracts and select or create a new contract.</li>
<li>Fill fields for selecting the recurrency and invoice parameters:<ul>
<li>Type defines document that contract will generate, can be “Sales” or “Invoices”</li>
<li>Sale Autoconfirm, validate Sales Orders if type is “Sales”</li>
</ul>
</li>
</ol>
</div>
<div class="section" id="bug-tracker">
<h1><a class="toc-backref" href="#id2">Bug Tracker</a></h1>
<p>Bugs are tracked on <a class="reference external" href="https://github.com/OCA/contract/issues">GitHub Issues</a>.
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
<a class="reference external" href="https://github.com/OCA/contract/issues/new?body=module:%20contract_sale_generation%0Aversion:%2012.0%0A%0A**Steps%20to%20reproduce**%0A-%20...%0A%0A**Current%20behavior**%0A%0A**Expected%20behavior**">feedback</a>.</p>
<p>Do not contact contributors directly about support or help with technical issues.</p>
</div>
<div class="section" id="credits">
<h1><a class="toc-backref" href="#id3">Credits</a></h1>
<div class="section" id="authors">
<h2><a class="toc-backref" href="#id4">Authors</a></h2>
<ul class="simple">
<li>PESOL</li>
</ul>
</div>
<div class="section" id="contributors">
<h2><a class="toc-backref" href="#id5">Contributors</a></h2>
<ul class="simple">
<li>Angel Moya &lt;<a class="reference external" href="mailto:angel.moya&#64;pesol.es">angel.moya&#64;pesol.es</a>&gt;</li>
<li>Florent THOMAS &lt;<a class="reference external" href="mailto:florent.thomas&#64;mind-and-go.com">florent.thomas&#64;mind-and-go.com</a>&gt;</li>
<li>Serpent Consulting Services Pvt. Ltd. &lt;<a class="reference external" href="mailto:support&#64;serpentcs.com">support&#64;serpentcs.com</a>&gt;</li>
</ul>
</div>
<div class="section" id="maintainers">
<h2><a class="toc-backref" href="#id6">Maintainers</a></h2>
<p>This module is maintained by the OCA.</p>
<a class="reference external image-reference" href="https://odoo-community.org"><img alt="Odoo Community Association" src="https://odoo-community.org/logo.png" /></a>
<p>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.</p>
<p>This module is part of the <a class="reference external" href="https://github.com/OCA/contract/tree/12.0/contract_sale_generation">OCA/contract</a> project on GitHub.</p>
<p>You are welcome to contribute. To learn how please visit <a class="reference external" href="https://odoo-community.org/page/Contribute">https://odoo-community.org/page/Contribute</a>.</p>
</div>
</div>
</div>
</body>
</html>

View File

@@ -1,4 +1,3 @@
# -*- coding: utf-8 -*-
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
from . import test_contract_sale from . import test_contract_sale

View File

@@ -1,108 +1,209 @@
# -*- coding: utf-8 -*-
# © 2016 Carlos Dauden <carlos.dauden@tecnativa.com> # © 2016 Carlos Dauden <carlos.dauden@tecnativa.com>
# Copyright 2017 Pesol (<http://pesol.es>) # Copyright 2017 Pesol (<http://pesol.es>)
# Copyright 2017 Angel Moya <angel.moya@pesol.es> # Copyright 2017 Angel Moya <angel.moya@pesol.es>
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
from odoo.exceptions import ValidationError from odoo.exceptions import ValidationError
from odoo import fields
from odoo.tests.common import TransactionCase from odoo.tests.common import TransactionCase
def to_date(date):
return fields.Date.to_date(date)
class TestContractSale(TransactionCase): class TestContractSale(TransactionCase):
# Use case : Prepare some data for current test case # Use case : Prepare some data for current test case
def setUp(self): def setUp(self):
super(TestContractSale, self).setUp() super(TestContractSale, self).setUp()
self.partner = self.env.ref('base.res_partner_2') self.pricelist = self.env['product.pricelist'].create({
self.product = self.env.ref('product.product_product_2') 'name': 'pricelist for contract test',
self.product.taxes_id += self.env['account.tax'].search( })
[('type_tax_use', '=', 'sale')], limit=1) self.partner = self.env['res.partner'].create({
self.product.description_sale = 'Test description sale' 'name': 'partner test contract',
self.template_vals = { '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_1.uom_id.id,
'price_unit': 100,
'discount': 50,
'recurring_rule_type': 'yearly', 'recurring_rule_type': 'yearly',
'recurring_interval': 1, 'recurring_interval': 1,
'name': 'Test Contract Template',
'type': 'sale',
'sale_autoconfirm': False
} }
self.template = self.env['account.analytic.contract'].create( self.template_vals = {
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
) )
self.contract = self.env['account.analytic.account'].create({ # 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', 'name': 'Test Contract',
'partner_id': self.partner.id, 'partner_id': self.partner.id,
'pricelist_id': self.partner.property_product_pricelist.id, 'pricelist_id': self.partner.property_product_pricelist.id,
'recurring_invoices': True, 'type': 'sale',
'date_start': '2016-02-15', 'sale_autoconfirm': False
'recurring_next_date': '2016-02-29', }
}) )
self.contract.contract_template_id = self.template self.line_vals = {
self.contract._onchange_contract_template_id() 'contract_id': self.contract.id,
self.contract_line = self.env['account.analytic.invoice.line'].create({ 'product_id': self.product_1.id,
'analytic_account_id': self.contract.id,
'product_id': self.product.id,
'name': 'Services from #START# to #END#', 'name': 'Services from #START# to #END#',
'quantity': 1, 'quantity': 1,
'uom_id': self.product.uom_id.id, 'uom_id': self.product_1.uom_id.id,
'price_unit': 100, 'price_unit': 100,
'discount': 50, '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): def test_check_discount(self):
with self.assertRaises(ValidationError): with self.assertRaises(ValidationError):
self.contract_line.write({'discount': 120}) self.contract_line.write({'discount': 120})
def test_contract(self): def test_contract(self):
recurring_next_date = to_date('2020-02-15')
self.assertAlmostEqual(self.contract_line.price_subtotal, 50.0) self.assertAlmostEqual(self.contract_line.price_subtotal, 50.0)
res = self.contract_line._onchange_product_id() res = self.contract_line._onchange_product_id()
self.assertIn('uom_id', res['domain']) self.assertIn('uom_id', res['domain'])
self.contract_line.price_unit = 100.0 self.contract_line.price_unit = 100.0
self.contract.partner_id = self.partner.id
self.contract.recurring_create_sale() self.contract.recurring_create_sale()
self.sale_monthly = self.env['sale.order'].search( self.sale_monthly = self.contract._get_related_sales()
[('project_id', '=', self.contract.id),
('state', '=', 'draft')])
self.assertTrue(self.sale_monthly) self.assertTrue(self.sale_monthly)
self.assertEqual(self.contract.recurring_next_date, '2017-02-28') self.assertEqual(
self.sale_line = self.sale_monthly.order_line[0] self.contract_line.recurring_next_date, recurring_next_date
self.assertAlmostEqual(self.sale_line.price_subtotal, 50.0) )
self.assertEqual(self.contract.partner_id.user_id, self.order_line = self.sale_monthly.order_line[0]
self.sale_monthly.user_id) 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): def test_contract_autoconfirm(self):
recurring_next_date = to_date('2020-02-15')
self.contract.sale_autoconfirm = True self.contract.sale_autoconfirm = True
self.assertAlmostEqual(self.contract_line.price_subtotal, 50.0) self.assertAlmostEqual(self.contract_line.price_subtotal, 50.0)
res = self.contract_line._onchange_product_id() res = self.contract_line._onchange_product_id()
self.assertIn('uom_id', res['domain']) self.assertIn('uom_id', res['domain'])
self.contract_line.price_unit = 100.0 self.contract_line.price_unit = 100.0
self.contract.partner_id = self.partner.id
self.contract.recurring_create_sale() self.contract.recurring_create_sale()
self.sale_monthly = self.env['sale.order'].search( self.sale_monthly = self.contract._get_related_sales()
[('project_id', '=', self.contract.id),
('state', '=', 'sale')])
self.assertTrue(self.sale_monthly) self.assertTrue(self.sale_monthly)
self.assertEqual(self.contract.recurring_next_date, '2017-02-28') self.assertEqual(
self.contract_line.recurring_next_date, recurring_next_date
self.sale_line = self.sale_monthly.order_line[0] )
self.assertAlmostEqual(self.sale_line.price_subtotal, 50.0) self.order_line = self.sale_monthly.order_line[0]
self.assertEqual(self.contract.partner_id.user_id, self.assertTrue(self.order_line.tax_id)
self.sale_monthly.user_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): 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.contract_template_id = self.template
self.contract._onchange_contract_template_id() self.contract._onchange_contract_template_id()
res = { res = {
'recurring_rule_type': self.contract.recurring_rule_type, 'contract_line_ids':
'recurring_interval': self.contract.recurring_interval, [(0, 0, {
'type': 'sale', 'product_id': self.product_1.id,
'sale_autoconfirm': False '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'] del self.template_vals['name']
self.assertDictEqual(res, self.template_vals) self.assertDictEqual(res, self.template_vals)
def test_check_cron_ended_contract(self): def test_contract_count_sale(self):
self.contract.recurring_next_date = '2016-02-29' self.contract.recurring_create_sale()
self.contract.recurring_rule_type = 'yearly' self.contract.recurring_create_sale()
self.contract.date_end = '2016-02-28' self.contract.recurring_create_sale()
sale_orders = self.contract.with_context( self.contract._compute_sale_count()
cron=True).recurring_create_sale() self.assertEqual(self.contract.sale_count, 3)
self.assertFalse(sale_orders)
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),
)

View File

@@ -1,39 +0,0 @@
<?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_sale"
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>

View File

@@ -1,15 +0,0 @@
<?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>

View File

@@ -0,0 +1,41 @@
<odoo>
<record id="contract_contract_form_view_recurring_sale_form" model="ir.ui.view">
<field name="name">contract.contract.form.recurring.sale.form</field>
<field name="model">contract.contract</field>
<field name="inherit_id" ref="contract.contract_contract_form_view"/>
<field name="arch" type="xml">
<xpath expr="//group[@name='recurring_invoices']" position="after">
<group name="recurring_sale_or_invoicing" string="Recurring Sales/Invoicing" col="4">
<field name="type"/>
<field name="sale_autoconfirm" attrs="{'invisible':[('type','!=', 'sale')]}" />
</group>
</xpath>
<xpath expr="//button[@name='recurring_create_invoice']" position="attributes">
<attribute name="attrs">{'invisible': ['|', ('create_invoice_visibility', '=', False),('type','!=','invoice')]}</attribute>
</xpath>
<xpath expr="//button[@name='recurring_create_invoice']" position="before">
<button name="recurring_create_sale"
type="object"
attrs="{'invisible': [('type','!=','sale')]}"
string="CREATE SALES"
class="oe_link"
groups="base.group_no_one"
/>
</xpath>
<xpath expr="//button[@name='action_show_invoices']" position="attributes">
<attribute name="attrs">{'invisible': [('type','!=','invoice')]}</attribute>
</xpath>
<xpath expr="//button[@name='action_show_invoices']" position="after">
<button name="action_show_sales"
type="object" icon="fa-list"
class="oe_stat_button" attrs="{'invisible': [('type','!=','sale')]}">
<field string="Sales Order"
name="sale_count"
widget="statinfo"/>
</button>
</xpath>
</field>
</record>
</odoo>

View File

@@ -0,0 +1,14 @@
<odoo>
<record id="contract_template_form_view_inherit_sale" model="ir.ui.view">
<field name="name">contract.template form view (in contract)</field>
<field name="model">contract.template</field>
<field name="inherit_id" ref="contract.contract_template_form_view"/>
<field name="arch" type="xml">
<xpath expr="//field[@name='pricelist_id']" position="after">
<field name="type"/>
</xpath>
</field>
</record>
</odoo>

View File

@@ -1,15 +0,0 @@
<?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>