mirror of
https://github.com/bringout/oca-ocb-sale.git
synced 2026-04-27 06:52:06 +02:00
19.0 vanilla
This commit is contained in:
parent
79f83631d5
commit
73afc09215
6267 changed files with 1534193 additions and 1130106 deletions
|
|
@ -5,7 +5,5 @@ from . import res_company
|
|||
from . import res_config_settings
|
||||
from . import sale_order
|
||||
from . import sale_order_line
|
||||
from . import sale_order_option
|
||||
from . import sale_order_template
|
||||
from . import sale_order_template_line
|
||||
from . import sale_order_template_option
|
||||
|
|
|
|||
|
|
@ -1,11 +1,10 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo import fields, models, _
|
||||
from odoo import _, fields, models
|
||||
from odoo.exceptions import AccessError
|
||||
|
||||
|
||||
class Digest(models.Model):
|
||||
class DigestDigest(models.Model):
|
||||
_inherit = 'digest.digest'
|
||||
|
||||
kpi_all_sale_total = fields.Boolean('All Sales')
|
||||
|
|
@ -14,16 +13,16 @@ class Digest(models.Model):
|
|||
def _compute_kpi_sale_total_value(self):
|
||||
if not self.env.user.has_group('sales_team.group_sale_salesman_all_leads'):
|
||||
raise AccessError(_("Do not have access, skip this data for user's digest email"))
|
||||
for record in self:
|
||||
start, end, company = record._get_kpi_compute_parameters()
|
||||
all_channels_sales = self.env['sale.report']._read_group([
|
||||
('date', '>=', start),
|
||||
('date', '<', end),
|
||||
('state', 'not in', ['draft', 'cancel', 'sent']),
|
||||
('company_id', '=', company.id)], ['price_total'], ['company_id'])
|
||||
record.kpi_all_sale_total_value = sum([channel_sale['price_total'] for channel_sale in all_channels_sales])
|
||||
|
||||
self._calculate_company_based_kpi(
|
||||
'sale.report',
|
||||
'kpi_all_sale_total_value',
|
||||
date_field='date',
|
||||
additional_domain=[('state', 'not in', ['draft', 'cancel', 'sent'])],
|
||||
sum_field='price_total',
|
||||
)
|
||||
|
||||
def _compute_kpis_actions(self, company, user):
|
||||
res = super(Digest, self)._compute_kpis_actions(company, user)
|
||||
res['kpi_all_sale_total'] = 'sale.report_all_channels_sales_action&menu_id=%s' % self.env.ref('sale.sale_menu_root').id
|
||||
res = super()._compute_kpis_actions(company, user)
|
||||
res['kpi_all_sale_total'] = 'sale.report_all_channels_sales_action?menu_id=%s' % self.env.ref('sale.sale_menu_root').id
|
||||
return res
|
||||
|
|
|
|||
|
|
@ -1,4 +1,3 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo import fields, models
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo import fields, models, api
|
||||
from odoo import fields, models
|
||||
|
||||
|
||||
class ResConfigSettings(models.TransientModel):
|
||||
|
|
@ -12,12 +11,6 @@ class ResConfigSettings(models.TransientModel):
|
|||
company_so_template_id = fields.Many2one(
|
||||
related="company_id.sale_order_template_id", string="Default Template", readonly=False,
|
||||
domain="['|', ('company_id', '=', False), ('company_id', '=', company_id)]")
|
||||
module_sale_quotation_builder = fields.Boolean("Quotation Builder")
|
||||
|
||||
@api.onchange('group_sale_order_template')
|
||||
def _onchange_group_sale_order_template(self):
|
||||
if not self.group_sale_order_template:
|
||||
self.module_sale_quotation_builder = False
|
||||
|
||||
def set_values(self):
|
||||
if not self.group_sale_order_template:
|
||||
|
|
|
|||
|
|
@ -1,15 +1,12 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from datetime import timedelta
|
||||
from itertools import chain, starmap, zip_longest
|
||||
|
||||
from odoo import SUPERUSER_ID, api, fields, models, _
|
||||
from odoo import _, api, fields, models
|
||||
from odoo.exceptions import ValidationError
|
||||
from odoo.tools import is_html_empty
|
||||
|
||||
from odoo.addons.sale.models.sale_order import READONLY_FIELD_STATES
|
||||
|
||||
|
||||
class SaleOrder(models.Model):
|
||||
_inherit = 'sale.order'
|
||||
|
|
@ -19,13 +16,7 @@ class SaleOrder(models.Model):
|
|||
string="Quotation Template",
|
||||
compute='_compute_sale_order_template_id',
|
||||
store=True, readonly=False, check_company=True, precompute=True,
|
||||
states=READONLY_FIELD_STATES,
|
||||
domain="['|', ('company_id', '=', False), ('company_id', '=', company_id)]")
|
||||
sale_order_option_ids = fields.One2many(
|
||||
comodel_name='sale.order.option', inverse_name='order_id',
|
||||
string="Optional Products Lines",
|
||||
states=READONLY_FIELD_STATES,
|
||||
copy=True)
|
||||
|
||||
#=== COMPUTE METHODS ===#
|
||||
|
||||
|
|
@ -59,6 +50,13 @@ class SaleOrder(models.Model):
|
|||
for order in self.filtered('sale_order_template_id'):
|
||||
order.require_payment = order.sale_order_template_id.require_payment
|
||||
|
||||
@api.depends('sale_order_template_id')
|
||||
def _compute_prepayment_percent(self):
|
||||
super()._compute_prepayment_percent()
|
||||
for order in self.filtered('sale_order_template_id'):
|
||||
if order.require_payment:
|
||||
order.prepayment_percent = order.sale_order_template_id.prepayment_percent
|
||||
|
||||
@api.depends('sale_order_template_id')
|
||||
def _compute_validity_date(self):
|
||||
super()._compute_validity_date()
|
||||
|
|
@ -67,26 +65,18 @@ class SaleOrder(models.Model):
|
|||
if validity_days > 0:
|
||||
order.validity_date = fields.Date.context_today(order) + timedelta(validity_days)
|
||||
|
||||
#=== CONSTRAINT METHODS ===#
|
||||
|
||||
@api.constrains('company_id', 'sale_order_option_ids')
|
||||
def _check_optional_product_company_id(self):
|
||||
for order in self:
|
||||
companies = order.sale_order_option_ids.product_id.company_id
|
||||
if companies and companies != order.company_id:
|
||||
bad_products = order.sale_order_option_ids.product_id.filtered(lambda p: p.company_id and p.company_id != order.company_id)
|
||||
raise ValidationError(_(
|
||||
"Your quotation contains products from company %(product_company)s whereas your quotation belongs to company %(quote_company)s. \n Please change the company of your quotation or remove the products from other companies (%(bad_products)s).",
|
||||
product_company=', '.join(companies.mapped('display_name')),
|
||||
quote_company=order.company_id.display_name,
|
||||
bad_products=', '.join(bad_products.mapped('display_name')),
|
||||
))
|
||||
@api.depends('sale_order_template_id')
|
||||
def _compute_journal_id(self):
|
||||
super()._compute_journal_id()
|
||||
for order in self.filtered('sale_order_template_id'):
|
||||
order.journal_id = order.sale_order_template_id.journal_id
|
||||
|
||||
#=== ONCHANGE METHODS ===#
|
||||
|
||||
@api.onchange('company_id')
|
||||
def _onchange_company_id(self):
|
||||
"""Trigger quotation template recomputation on unsaved records company change"""
|
||||
super()._onchange_company_id()
|
||||
if self._origin.id:
|
||||
return
|
||||
self._compute_sale_order_template_id()
|
||||
|
|
@ -111,14 +101,6 @@ class SaleOrder(models.Model):
|
|||
|
||||
self.order_line = order_lines_data
|
||||
|
||||
option_lines_data = [fields.Command.clear()]
|
||||
option_lines_data += [
|
||||
fields.Command.create(option._prepare_option_line_values())
|
||||
for option in sale_order_template.sale_order_template_option_ids
|
||||
]
|
||||
|
||||
self.sale_order_option_ids = option_lines_data
|
||||
|
||||
@api.onchange('partner_id')
|
||||
def _onchange_partner_id(self):
|
||||
"""Reload template for unsaved orders with unmodified lines & orders."""
|
||||
|
|
@ -126,47 +108,33 @@ class SaleOrder(models.Model):
|
|||
return
|
||||
|
||||
def line_eqv(line, t_line):
|
||||
return line and t_line and (
|
||||
line.product_id == t_line.product_id
|
||||
and line.display_type == t_line.display_type
|
||||
and line.product_uom == t_line.product_uom_id
|
||||
and line.product_uom_qty == t_line.product_uom_qty
|
||||
)
|
||||
|
||||
def option_eqv(option, t_option):
|
||||
return option and t_option and all(
|
||||
option[fname] == t_option[fname]
|
||||
for fname in ['product_id', 'uom_id', 'quantity']
|
||||
return line and t_line and all(
|
||||
line[fname] == t_line[fname]
|
||||
for fname in ['product_id', 'product_uom_id', 'product_uom_qty', 'display_type']
|
||||
)
|
||||
|
||||
lines = self.order_line
|
||||
options = self.sale_order_option_ids
|
||||
t_lines = self.sale_order_template_id.sale_order_template_line_ids
|
||||
t_options = self.sale_order_template_id.sale_order_template_option_ids
|
||||
|
||||
if all(chain(
|
||||
starmap(line_eqv, zip_longest(lines, t_lines)),
|
||||
starmap(option_eqv, zip_longest(options, t_options)),
|
||||
)):
|
||||
if all(starmap(line_eqv, zip_longest(lines, t_lines))):
|
||||
self._onchange_sale_order_template_id()
|
||||
|
||||
#=== ACTION METHODS ===#
|
||||
|
||||
def _get_confirmation_template(self):
|
||||
self.ensure_one()
|
||||
return self.sale_order_template_id.mail_template_id or super()._get_confirmation_template()
|
||||
|
||||
def action_confirm(self):
|
||||
res = super().action_confirm()
|
||||
if self.env.su:
|
||||
self = self.with_user(SUPERUSER_ID)
|
||||
|
||||
if self.env.context.get('send_email'):
|
||||
# Mail already sent in super method
|
||||
return res
|
||||
|
||||
# When an order is confirmed from backend (send_email=False), if the quotation template has
|
||||
# a specified mail template, send it as it's probably meant to share additional information.
|
||||
for order in self:
|
||||
if order.sale_order_template_id and order.sale_order_template_id.mail_template_id:
|
||||
order.sale_order_template_id.mail_template_id.send_mail(order.id)
|
||||
if order.sale_order_template_id.mail_template_id:
|
||||
order._send_order_notification_mail(order.sale_order_template_id.mail_template_id)
|
||||
return res
|
||||
|
||||
def _recompute_prices(self):
|
||||
super()._recompute_prices()
|
||||
# Special case: we want to overwrite the existing discount on _recompute_prices call
|
||||
# i.e. to make sure the discount is correctly reset
|
||||
# if pricelist discount_policy is different than when the price was first computed.
|
||||
self.sale_order_option_ids.discount = 0.0
|
||||
self.sale_order_option_ids._compute_price_unit()
|
||||
self.sale_order_option_ids._compute_discount()
|
||||
|
|
|
|||
|
|
@ -1,29 +1,59 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo import api, fields, models
|
||||
|
||||
|
||||
class SaleOrderLine(models.Model):
|
||||
_inherit = "sale.order.line"
|
||||
_inherit = 'sale.order.line'
|
||||
_description = "Sales Order Line"
|
||||
|
||||
sale_order_option_ids = fields.One2many('sale.order.option', 'line_id', 'Optional Products Lines')
|
||||
# Section-related fields
|
||||
is_optional = fields.Boolean(
|
||||
string="Optional Line",
|
||||
copy=True,
|
||||
default=False,
|
||||
) # Whether this section's lines are optional in the portal.
|
||||
|
||||
# === COMPUTE METHODS === #
|
||||
|
||||
@api.depends('product_id')
|
||||
def _compute_name(self):
|
||||
# Take the description on the order template if the product is present in it
|
||||
super()._compute_name()
|
||||
for line in self:
|
||||
if line.product_id and line.order_id.sale_order_template_id:
|
||||
if line.product_id and line.order_id.sale_order_template_id and line._use_template_name():
|
||||
for template_line in line.order_id.sale_order_template_id.sale_order_template_line_ids:
|
||||
if line.product_id == template_line.product_id:
|
||||
if line.product_id == template_line.product_id and template_line.name:
|
||||
# If a specific description was set on the template, use it
|
||||
# Otherwise the description is handled by the super call
|
||||
lang = line.order_id.partner_id.lang
|
||||
line.name = template_line.with_context(lang=lang).name + line.with_context(lang=lang)._get_sale_order_line_multiline_description_variants()
|
||||
break
|
||||
|
||||
def _compute_price_unit(self):
|
||||
# Avoid recomputing the price with pricelist rules, use the initial price
|
||||
# used in the optional product line.
|
||||
optional_product_lines = self.filtered('sale_order_option_ids')
|
||||
super(SaleOrderLine, self - optional_product_lines)._compute_price_unit()
|
||||
def _use_template_name(self):
|
||||
""" Allows overriding to avoid using the template lines descriptions for the sale order lines descriptions.
|
||||
This is typically useful for 'configured' products, such as event_ticket or event_booth, where we need to have
|
||||
specific configuration information inside description instead of the default values.
|
||||
"""
|
||||
self.ensure_one()
|
||||
return True
|
||||
|
||||
# === TOOLING ===#
|
||||
|
||||
def _is_line_optional(self):
|
||||
""" Returns whether the line is optional or not.
|
||||
|
||||
A line is optional if it is directly under an optional (sub)section, or under a subsection
|
||||
which is itself under an optional section.
|
||||
"""
|
||||
self.ensure_one()
|
||||
return (
|
||||
self.parent_id.is_optional
|
||||
or (
|
||||
self.parent_id.display_type == 'line_subsection'
|
||||
and self.parent_id.parent_id.is_optional
|
||||
)
|
||||
)
|
||||
|
||||
def _can_be_edited_on_portal(self):
|
||||
return super()._can_be_edited_on_portal() and self._is_line_optional()
|
||||
|
|
|
|||
|
|
@ -1,150 +0,0 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo import api, fields, models, _
|
||||
from odoo.exceptions import UserError
|
||||
|
||||
|
||||
class SaleOrderOption(models.Model):
|
||||
_name = 'sale.order.option'
|
||||
_description = "Sale Options"
|
||||
_order = 'sequence, id'
|
||||
|
||||
# FIXME ANVFE wtf is it not required ???
|
||||
# TODO related to order.company_id and restrict product choice based on company
|
||||
order_id = fields.Many2one('sale.order', 'Sales Order Reference', ondelete='cascade', index=True)
|
||||
|
||||
product_id = fields.Many2one(
|
||||
comodel_name='product.product',
|
||||
required=True,
|
||||
domain=[('sale_ok', '=', True)])
|
||||
line_id = fields.Many2one(
|
||||
comodel_name='sale.order.line', ondelete='set null', copy=False)
|
||||
sequence = fields.Integer(
|
||||
string='Sequence', help="Gives the sequence order when displaying a list of optional products.")
|
||||
|
||||
name = fields.Text(
|
||||
string="Description",
|
||||
compute='_compute_name',
|
||||
store=True, readonly=False,
|
||||
required=True, precompute=True)
|
||||
|
||||
quantity = fields.Float(
|
||||
string="Quantity",
|
||||
required=True,
|
||||
digits='Product Unit of Measure',
|
||||
default=1)
|
||||
uom_id = fields.Many2one(
|
||||
comodel_name='uom.uom',
|
||||
string="Unit of Measure",
|
||||
compute='_compute_uom_id',
|
||||
store=True, readonly=False,
|
||||
required=True, precompute=True,
|
||||
domain="[('category_id', '=', product_uom_category_id)]")
|
||||
product_uom_category_id = fields.Many2one(related='product_id.uom_id.category_id')
|
||||
|
||||
price_unit = fields.Float(
|
||||
string="Unit Price",
|
||||
digits='Product Price',
|
||||
compute='_compute_price_unit',
|
||||
store=True, readonly=False,
|
||||
required=True, precompute=True)
|
||||
discount = fields.Float(
|
||||
string="Discount (%)",
|
||||
digits='Discount',
|
||||
compute='_compute_discount',
|
||||
store=True, readonly=False, precompute=True)
|
||||
|
||||
is_present = fields.Boolean(
|
||||
string="Present on Quotation",
|
||||
compute='_compute_is_present',
|
||||
search='_search_is_present',
|
||||
help="This field will be checked if the option line's product is "
|
||||
"already present in the quotation.")
|
||||
|
||||
#=== COMPUTE METHODS ===#
|
||||
|
||||
@api.depends('product_id')
|
||||
def _compute_name(self):
|
||||
for option in self:
|
||||
if not option.product_id:
|
||||
continue
|
||||
product_lang = option.product_id.with_context(lang=option.order_id.partner_id.lang)
|
||||
option.name = product_lang.get_product_multiline_description_sale()
|
||||
|
||||
@api.depends('product_id')
|
||||
def _compute_uom_id(self):
|
||||
for option in self:
|
||||
if not option.product_id or option.uom_id:
|
||||
continue
|
||||
option.uom_id = option.product_id.uom_id
|
||||
|
||||
@api.depends('product_id', 'uom_id', 'quantity')
|
||||
def _compute_price_unit(self):
|
||||
for option in self:
|
||||
if not option.product_id:
|
||||
continue
|
||||
# To compute the price_unit a so line is created in cache
|
||||
values = option._get_values_to_add_to_order()
|
||||
new_sol = self.env['sale.order.line'].new(values)
|
||||
new_sol._compute_price_unit()
|
||||
option.price_unit = new_sol.price_unit
|
||||
# Drop the temporary record from the cache
|
||||
new_sol.invalidate_recordset(flush=False)
|
||||
|
||||
@api.depends('product_id', 'uom_id', 'quantity')
|
||||
def _compute_discount(self):
|
||||
for option in self:
|
||||
if not option.product_id:
|
||||
continue
|
||||
# To compute the discount a so line is created in cache
|
||||
values = option._get_values_to_add_to_order()
|
||||
new_sol = self.env['sale.order.line'].new(values)
|
||||
new_sol._compute_discount()
|
||||
option.discount = new_sol.discount
|
||||
# Drop the temporary record from the cache
|
||||
new_sol.invalidate_recordset(flush=False)
|
||||
|
||||
def _get_values_to_add_to_order(self):
|
||||
self.ensure_one()
|
||||
return {
|
||||
'order_id': self.order_id.id,
|
||||
'price_unit': self.price_unit,
|
||||
'name': self.name,
|
||||
'product_id': self.product_id.id,
|
||||
'product_uom_qty': self.quantity,
|
||||
'product_uom': self.uom_id.id,
|
||||
'discount': self.discount,
|
||||
}
|
||||
|
||||
@api.depends('line_id', 'order_id.order_line', 'product_id')
|
||||
def _compute_is_present(self):
|
||||
# NOTE: this field cannot be stored as the line_id is usually removed
|
||||
# through cascade deletion, which means the compute would be false
|
||||
for option in self:
|
||||
option.is_present = bool(option.order_id.order_line.filtered(lambda l: l.product_id == option.product_id))
|
||||
|
||||
def _search_is_present(self, operator, value):
|
||||
if (operator, value) in [('=', True), ('!=', False)]:
|
||||
return [('line_id', '=', False)]
|
||||
return [('line_id', '!=', False)]
|
||||
|
||||
#=== ACTION METHODS ===#
|
||||
|
||||
def button_add_to_order(self):
|
||||
self.add_option_to_order()
|
||||
|
||||
def add_option_to_order(self):
|
||||
self.ensure_one()
|
||||
|
||||
sale_order = self.order_id
|
||||
|
||||
if sale_order.state not in ['draft', 'sent']:
|
||||
raise UserError(_('You cannot add options to a confirmed order.'))
|
||||
|
||||
values = self._get_values_to_add_to_order()
|
||||
order_line = self.env['sale.order.line'].create(values)
|
||||
|
||||
self.write({'line_id': order_line.id})
|
||||
if sale_order:
|
||||
sale_order.add_option_to_order_with_taxcloud()
|
||||
|
|
@ -1,21 +1,23 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo import api, fields, models, _
|
||||
from odoo import _, api, fields, models
|
||||
from odoo.exceptions import ValidationError
|
||||
from odoo.fields import Command
|
||||
|
||||
|
||||
class SaleOrderTemplate(models.Model):
|
||||
_name = "sale.order.template"
|
||||
_name = 'sale.order.template'
|
||||
_description = "Quotation Template"
|
||||
_order = 'sequence, id'
|
||||
|
||||
active = fields.Boolean(
|
||||
default=True,
|
||||
help="If unchecked, it will allow you to hide the quotation template without removing it.")
|
||||
company_id = fields.Many2one(comodel_name='res.company')
|
||||
company_id = fields.Many2one(comodel_name='res.company', default=lambda self: self.env.company)
|
||||
|
||||
name = fields.Char(string="Quotation Template", required=True)
|
||||
note = fields.Html(string="Terms and conditions", translate=True)
|
||||
sequence = fields.Integer(default=10)
|
||||
|
||||
mail_template_id = fields.Many2one(
|
||||
comodel_name='mail.template',
|
||||
|
|
@ -36,15 +38,21 @@ class SaleOrderTemplate(models.Model):
|
|||
compute='_compute_require_payment',
|
||||
store=True, readonly=False,
|
||||
help="Request an online payment to the customer in order to confirm orders automatically.")
|
||||
prepayment_percent = fields.Float(
|
||||
string="Prepayment percentage",
|
||||
compute="_compute_prepayment_percent",
|
||||
store=True, readonly=False,
|
||||
help="The percentage of the amount needed to be paid to confirm quotations.")
|
||||
|
||||
sale_order_template_line_ids = fields.One2many(
|
||||
comodel_name='sale.order.template.line', inverse_name='sale_order_template_id',
|
||||
string="Lines",
|
||||
copy=True)
|
||||
sale_order_template_option_ids = fields.One2many(
|
||||
comodel_name='sale.order.template.option', inverse_name='sale_order_template_id',
|
||||
string="Optional Products",
|
||||
copy=True)
|
||||
journal_id = fields.Many2one(
|
||||
'account.journal', string="Invoicing Journal",
|
||||
domain=[('type', '=', 'sale')], company_dependent=True, check_company=True,
|
||||
help="If set, SO with this template will invoice in this journal; "
|
||||
"otherwise the sales journal with the lowest sequence is used.")
|
||||
|
||||
#=== COMPUTE METHODS ===#
|
||||
|
||||
|
|
@ -58,21 +66,69 @@ class SaleOrderTemplate(models.Model):
|
|||
for order in self:
|
||||
order.require_payment = (order.company_id or order.env.company).portal_confirmation_pay
|
||||
|
||||
@api.depends('company_id', 'require_payment')
|
||||
def _compute_prepayment_percent(self):
|
||||
for template in self:
|
||||
template.prepayment_percent = (
|
||||
template.company_id or template.env.company
|
||||
).prepayment_percent
|
||||
|
||||
#=== ONCHANGE METHODS ===#
|
||||
|
||||
@api.onchange('prepayment_percent')
|
||||
def _onchange_prepayment_percent(self):
|
||||
for template in self:
|
||||
if not template.prepayment_percent:
|
||||
template.require_payment = False
|
||||
|
||||
#=== CONSTRAINT METHODS ===#
|
||||
|
||||
@api.constrains('company_id', 'sale_order_template_line_ids', 'sale_order_template_option_ids')
|
||||
@api.constrains('company_id', 'sale_order_template_line_ids')
|
||||
def _check_company_id(self):
|
||||
for template in self:
|
||||
companies = template.mapped('sale_order_template_line_ids.product_id.company_id') | template.mapped('sale_order_template_option_ids.product_id.company_id')
|
||||
if len(companies) > 1:
|
||||
raise ValidationError(_("Your template cannot contain products from multiple companies."))
|
||||
elif companies and companies != template.company_id:
|
||||
restricted_products = template.sale_order_template_line_ids.product_id.filtered(
|
||||
'company_id'
|
||||
)
|
||||
if not restricted_products:
|
||||
continue
|
||||
|
||||
if not template.company_id:
|
||||
raise ValidationError(_(
|
||||
"Your template contains products from company %(product_company)s whereas your template belongs to company %(template_company)s. \n Please change the company of your template or remove the products from other companies.",
|
||||
product_company=', '.join(companies.mapped('display_name')),
|
||||
"Your template cannot contain products from specific companies if it's shared"
|
||||
" between companies. Please restrict the template access, or remove those"
|
||||
" products."
|
||||
))
|
||||
|
||||
authorized_products = restricted_products.filtered_domain(
|
||||
self.env['product.product']._check_company_domain(template.company_id)
|
||||
)
|
||||
if unauthorized_products := restricted_products - authorized_products:
|
||||
unaccessible_companies = unauthorized_products.company_id
|
||||
if len(unaccessible_companies) > 1:
|
||||
raise ValidationError(_(
|
||||
"Your template belongs to company %(template_company)s but contains"
|
||||
" products from other companies (%(product_company)s) that are not"
|
||||
" accessible to %(template_company)s.\nPlease change the company of your"
|
||||
" template or remove the products from other companies.",
|
||||
product_company=', '.join(unaccessible_companies.mapped('display_name')),
|
||||
template_company=template.company_id.display_name,
|
||||
))
|
||||
|
||||
raise ValidationError(_(
|
||||
"Your template belongs to company %(template_company)s but contains"
|
||||
" products from company (%(product_company)s) that are not"
|
||||
" accessible to %(template_company)s.\nPlease change the company of your"
|
||||
" template or remove the products from other companies.",
|
||||
product_company=unaccessible_companies.display_name,
|
||||
template_company=template.company_id.display_name,
|
||||
))
|
||||
|
||||
@api.constrains('prepayment_percent')
|
||||
def _check_prepayment_percent(self):
|
||||
for template in self:
|
||||
if template.require_payment and not (0 < template.prepayment_percent <= 1.0):
|
||||
raise ValidationError(_("Prepayment percentage must be a valid percentage."))
|
||||
|
||||
#=== CRUD METHODS ===#
|
||||
|
||||
@api.model_create_multi
|
||||
|
|
@ -90,11 +146,58 @@ class SaleOrderTemplate(models.Model):
|
|||
return result
|
||||
|
||||
def _update_product_translations(self):
|
||||
languages = self.env['res.lang'].search([('active', '=', 'true')])
|
||||
languages = self.env['res.lang'].search([('active', '=', True)])
|
||||
for lang in languages:
|
||||
for line in self.sale_order_template_line_ids:
|
||||
if line.name == line.product_id.get_product_multiline_description_sale():
|
||||
line.with_context(lang=lang.code).name = line.product_id.with_context(lang=lang.code).get_product_multiline_description_sale()
|
||||
for option in self.sale_order_template_option_ids:
|
||||
if option.name == option.product_id.get_product_multiline_description_sale():
|
||||
option.with_context(lang=lang.code).name = option.product_id.with_context(lang=lang.code).get_product_multiline_description_sale()
|
||||
|
||||
@api.model
|
||||
def _demo_configure_template(self):
|
||||
demo_template = self.env.ref(
|
||||
'sale_management.sale_order_template_1', raise_if_not_found=False
|
||||
)
|
||||
if not demo_template or demo_template.sale_order_template_line_ids:
|
||||
# Skip if template not found, or already configured
|
||||
return
|
||||
|
||||
acoustic_bloc_screen_product = self.env.ref(
|
||||
'product.product_template_acoustic_bloc_screens'
|
||||
).product_variant_id
|
||||
chair_protection_product = self.env.ref(
|
||||
'sale.product_product_1_product_template'
|
||||
).product_variant_id
|
||||
demo_template.sale_order_template_line_ids = [
|
||||
Command.create({
|
||||
'product_id': self.env.ref('product.consu_delivery_02').id,
|
||||
}),
|
||||
Command.create({
|
||||
'product_id': self.env.ref('product.product_delivery_01').id,
|
||||
'product_uom_qty': 8,
|
||||
}),
|
||||
Command.create({
|
||||
'product_id': acoustic_bloc_screen_product.id,
|
||||
}),
|
||||
Command.create({
|
||||
'product_id': chair_protection_product.id,
|
||||
'product_uom_qty': 8,
|
||||
}),
|
||||
Command.create({
|
||||
'name': self.env._("Optional Products Section"),
|
||||
'display_type': 'line_section',
|
||||
'is_optional': True,
|
||||
'product_uom_qty': 0,
|
||||
}),
|
||||
Command.create({
|
||||
'product_id': self.env.ref('product.product_product_16').id,
|
||||
'product_uom_qty': 0,
|
||||
}),
|
||||
Command.create({
|
||||
'product_id': self.env.ref('product.product_product_7').id,
|
||||
'product_uom_qty': 0,
|
||||
}),
|
||||
Command.create({
|
||||
'product_id': self.env.ref('product.product_product_12').id,
|
||||
'product_uom_qty': 0,
|
||||
}),
|
||||
]
|
||||
|
|
|
|||
|
|
@ -1,24 +1,22 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo import api, fields, models, _
|
||||
from odoo import _, api, fields, models
|
||||
from odoo.exceptions import UserError
|
||||
|
||||
|
||||
class SaleOrderTemplateLine(models.Model):
|
||||
_name = "sale.order.template.line"
|
||||
_name = 'sale.order.template.line'
|
||||
_description = "Quotation Template Line"
|
||||
_order = 'sale_order_template_id, sequence, id'
|
||||
|
||||
_sql_constraints = [
|
||||
('accountable_product_id_required',
|
||||
"CHECK(display_type IS NOT NULL OR (product_id IS NOT NULL AND product_uom_id IS NOT NULL))",
|
||||
"Missing required product and UoM on accountable sale quote line."),
|
||||
|
||||
('non_accountable_fields_null',
|
||||
"CHECK(display_type IS NULL OR (product_id IS NULL AND product_uom_qty = 0 AND product_uom_id IS NULL))",
|
||||
"Forbidden product, quantity and UoM on non-accountable sale quote line"),
|
||||
]
|
||||
_accountable_product_id_required = models.Constraint(
|
||||
'CHECK(display_type IS NOT NULL OR (product_id IS NOT NULL AND product_uom_id IS NOT NULL))',
|
||||
'Missing required product and UoM on accountable sale quote line.',
|
||||
)
|
||||
_non_accountable_fields_null = models.Constraint(
|
||||
'CHECK(display_type IS NULL OR (product_id IS NULL AND product_uom_qty = 0 AND product_uom_id IS NULL))',
|
||||
'Forbidden product, quantity and UoM on non-accountable sale quote line',
|
||||
)
|
||||
|
||||
sale_order_template_id = fields.Many2one(
|
||||
comodel_name='sale.order.template',
|
||||
|
|
@ -36,46 +34,76 @@ class SaleOrderTemplateLine(models.Model):
|
|||
product_id = fields.Many2one(
|
||||
comodel_name='product.product',
|
||||
check_company=True,
|
||||
domain="[('sale_ok', '=', True), ('company_id', 'in', [company_id, False])]")
|
||||
domain=lambda self: self._product_id_domain())
|
||||
|
||||
name = fields.Text(
|
||||
string="Description",
|
||||
compute='_compute_name',
|
||||
store=True, readonly=False, precompute=True,
|
||||
required=True,
|
||||
translate=True)
|
||||
translate=True,
|
||||
)
|
||||
|
||||
allowed_uom_ids = fields.Many2many('uom.uom', compute='_compute_allowed_uom_ids')
|
||||
product_uom_id = fields.Many2one(
|
||||
comodel_name='uom.uom',
|
||||
string="Unit of Measure",
|
||||
string="Unit",
|
||||
domain="[('id', 'in', allowed_uom_ids)]",
|
||||
compute='_compute_product_uom_id',
|
||||
store=True, readonly=False, precompute=True,
|
||||
domain="[('category_id', '=', product_uom_category_id)]")
|
||||
product_uom_category_id = fields.Many2one(related='product_id.uom_id.category_id')
|
||||
store=True, readonly=False, precompute=True)
|
||||
product_uom_qty = fields.Float(
|
||||
string='Quantity',
|
||||
required=True,
|
||||
digits='Product Unit of Measure',
|
||||
digits='Product Unit',
|
||||
default=1)
|
||||
|
||||
display_type = fields.Selection([
|
||||
('line_section', "Section"),
|
||||
('line_subsection', "Subsection"),
|
||||
('line_note', "Note")], default=False)
|
||||
|
||||
# Section-related fields
|
||||
parent_id = fields.Many2one(
|
||||
string="Parent Section Line",
|
||||
comodel_name='sale.order.template.line',
|
||||
compute='_compute_parent_id',
|
||||
)
|
||||
is_optional = fields.Boolean(
|
||||
string="Optional Line",
|
||||
copy=True,
|
||||
default=False,
|
||||
)
|
||||
|
||||
#=== COMPUTE METHODS ===#
|
||||
|
||||
@api.depends('product_id')
|
||||
def _compute_name(self):
|
||||
@api.depends('product_id', 'product_id.uom_id', 'product_id.uom_ids')
|
||||
def _compute_allowed_uom_ids(self):
|
||||
for option in self:
|
||||
if not option.product_id:
|
||||
continue
|
||||
option.name = option.product_id.get_product_multiline_description_sale()
|
||||
option.allowed_uom_ids = option.product_id.uom_id | option.product_id.uom_ids
|
||||
|
||||
@api.depends('product_id')
|
||||
def _compute_product_uom_id(self):
|
||||
for option in self:
|
||||
option.product_uom_id = option.product_id.uom_id
|
||||
|
||||
def _compute_parent_id(self):
|
||||
option_lines = set(self)
|
||||
for template, lines in self.grouped('sale_order_template_id').items():
|
||||
if not template:
|
||||
lines.parent_id = False
|
||||
continue
|
||||
last_section = False
|
||||
last_sub = False
|
||||
for line in template.sale_order_template_line_ids.sorted('sequence'):
|
||||
if line.display_type == 'line_section':
|
||||
last_section = line
|
||||
if line in option_lines:
|
||||
line.parent_id = False
|
||||
last_sub = False
|
||||
elif line.display_type == 'line_subsection':
|
||||
if line in option_lines:
|
||||
line.parent_id = last_section
|
||||
last_sub = line
|
||||
elif line in option_lines:
|
||||
line.parent_id = last_sub or last_section
|
||||
|
||||
#=== CRUD METHODS ===#
|
||||
|
||||
@api.model_create_multi
|
||||
|
|
@ -85,13 +113,18 @@ class SaleOrderTemplateLine(models.Model):
|
|||
vals.update(product_id=False, product_uom_qty=0, product_uom_id=False)
|
||||
return super().create(vals_list)
|
||||
|
||||
def write(self, values):
|
||||
if 'display_type' in values and self.filtered(lambda line: line.display_type != values.get('display_type')):
|
||||
def write(self, vals):
|
||||
if 'display_type' in vals and self.filtered(lambda line: line.display_type != vals.get('display_type')):
|
||||
raise UserError(_("You cannot change the type of a sale quote line. Instead you should delete the current line and create a new line of the proper type."))
|
||||
return super().write(values)
|
||||
return super().write(vals)
|
||||
|
||||
#=== BUSINESS METHODS ===#
|
||||
|
||||
@api.model
|
||||
def _product_id_domain(self):
|
||||
""" Returns the domain of the products that can be added to the template. """
|
||||
return [('sale_ok', '=', True), ('type', '!=', 'combo')]
|
||||
|
||||
def _prepare_order_line_values(self):
|
||||
""" Give the values to create the corresponding order line.
|
||||
|
||||
|
|
@ -99,10 +132,14 @@ class SaleOrderTemplateLine(models.Model):
|
|||
:rtype: dict
|
||||
"""
|
||||
self.ensure_one()
|
||||
return {
|
||||
vals = {
|
||||
'display_type': self.display_type,
|
||||
'name': self.name,
|
||||
'product_id': self.product_id.id,
|
||||
'product_uom_qty': self.product_uom_qty,
|
||||
'product_uom': self.product_uom_id.id,
|
||||
'product_uom_id': self.product_uom_id.id,
|
||||
'is_optional': self.is_optional,
|
||||
'sequence': self.sequence,
|
||||
}
|
||||
if self.name:
|
||||
vals['name'] = self.name
|
||||
return vals
|
||||
|
|
|
|||
|
|
@ -1,74 +0,0 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo import api, fields, models
|
||||
|
||||
|
||||
class SaleOrderTemplateOption(models.Model):
|
||||
_name = "sale.order.template.option"
|
||||
_description = "Quotation Template Option"
|
||||
_check_company_auto = True
|
||||
|
||||
sale_order_template_id = fields.Many2one(
|
||||
comodel_name='sale.order.template',
|
||||
string="Quotation Template Reference",
|
||||
index=True, required=True,
|
||||
ondelete='cascade')
|
||||
|
||||
company_id = fields.Many2one(
|
||||
related='sale_order_template_id.company_id', store=True, index=True)
|
||||
|
||||
product_id = fields.Many2one(
|
||||
comodel_name='product.product',
|
||||
required=True, check_company=True,
|
||||
domain="[('sale_ok', '=', True), ('company_id', 'in', [company_id, False])]")
|
||||
|
||||
name = fields.Text(
|
||||
string="Description",
|
||||
compute='_compute_name',
|
||||
store=True, readonly=False, precompute=True,
|
||||
required=True, translate=True)
|
||||
|
||||
uom_id = fields.Many2one(
|
||||
comodel_name='uom.uom',
|
||||
string="Unit of Measure",
|
||||
compute='_compute_uom_id',
|
||||
store=True, readonly=False,
|
||||
required=True, precompute=True,
|
||||
domain="[('category_id', '=', product_uom_category_id)]")
|
||||
product_uom_category_id = fields.Many2one(related='product_id.uom_id.category_id')
|
||||
quantity = fields.Float(
|
||||
string="Quantity",
|
||||
required=True,
|
||||
digits='Product Unit of Measure',
|
||||
default=1)
|
||||
|
||||
#=== COMPUTE METHODS ===#
|
||||
|
||||
@api.depends('product_id')
|
||||
def _compute_name(self):
|
||||
for option in self:
|
||||
if not option.product_id:
|
||||
continue
|
||||
option.name = option.product_id.get_product_multiline_description_sale()
|
||||
|
||||
@api.depends('product_id')
|
||||
def _compute_uom_id(self):
|
||||
for option in self:
|
||||
option.uom_id = option.product_id.uom_id
|
||||
|
||||
#=== BUSINESS METHODS ===#
|
||||
|
||||
def _prepare_option_line_values(self):
|
||||
""" Give the values to create the corresponding option line.
|
||||
|
||||
:return: `sale.order.option` create values
|
||||
:rtype: dict
|
||||
"""
|
||||
self.ensure_one()
|
||||
return {
|
||||
'name': self.name,
|
||||
'product_id': self.product_id.id,
|
||||
'quantity': self.quantity,
|
||||
'uom_id': self.uom_id.id,
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue