mirror of
https://github.com/bringout/oca-ocb-sale.git
synced 2026-04-27 19:12:06 +02:00
Initial commit: Sale packages
This commit is contained in:
commit
14e3d26998
6469 changed files with 2479670 additions and 0 deletions
|
|
@ -0,0 +1,11 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
from . import digest
|
||||
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
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
|
@ -0,0 +1,29 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo import fields, models, _
|
||||
from odoo.exceptions import AccessError
|
||||
|
||||
|
||||
class Digest(models.Model):
|
||||
_inherit = 'digest.digest'
|
||||
|
||||
kpi_all_sale_total = fields.Boolean('All Sales')
|
||||
kpi_all_sale_total_value = fields.Monetary(compute='_compute_kpi_sale_total_value')
|
||||
|
||||
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])
|
||||
|
||||
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
|
||||
return res
|
||||
|
|
@ -0,0 +1,15 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo import fields, models
|
||||
|
||||
|
||||
class ResCompany(models.Model):
|
||||
_inherit = "res.company"
|
||||
_check_company_auto = True
|
||||
|
||||
sale_order_template_id = fields.Many2one(
|
||||
"sale.order.template", string="Default Sale Template",
|
||||
domain="['|', ('company_id', '=', False), ('company_id', '=', id)]",
|
||||
check_company=True,
|
||||
)
|
||||
|
|
@ -0,0 +1,31 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo import fields, models, api
|
||||
|
||||
|
||||
class ResConfigSettings(models.TransientModel):
|
||||
_inherit = 'res.config.settings'
|
||||
|
||||
group_sale_order_template = fields.Boolean(
|
||||
"Quotation Templates", implied_group='sale_management.group_sale_order_template')
|
||||
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:
|
||||
if self.company_so_template_id:
|
||||
self.company_so_template_id = False
|
||||
companies = self.env['res.company'].sudo().search([
|
||||
('sale_order_template_id', '!=', False)
|
||||
])
|
||||
if companies:
|
||||
companies.sale_order_template_id = False
|
||||
super().set_values()
|
||||
|
|
@ -0,0 +1,172 @@
|
|||
# -*- 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.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'
|
||||
|
||||
sale_order_template_id = fields.Many2one(
|
||||
comodel_name='sale.order.template',
|
||||
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 ===#
|
||||
|
||||
# Do not make it depend on `company_id` field
|
||||
# It is triggered manually by the _onchange_company_id below iff the SO has not been saved.
|
||||
def _compute_sale_order_template_id(self):
|
||||
for order in self:
|
||||
company_template = order.company_id.sale_order_template_id
|
||||
if company_template and order.sale_order_template_id != company_template:
|
||||
if 'website_id' in self._fields and order.website_id:
|
||||
# don't apply quotation template for order created via eCommerce
|
||||
continue
|
||||
order.sale_order_template_id = order.company_id.sale_order_template_id.id
|
||||
|
||||
@api.depends('partner_id', 'sale_order_template_id')
|
||||
def _compute_note(self):
|
||||
super()._compute_note()
|
||||
for order in self.filtered('sale_order_template_id'):
|
||||
template = order.sale_order_template_id.with_context(lang=order.partner_id.lang)
|
||||
order.note = template.note if not is_html_empty(template.note) else order.note
|
||||
|
||||
@api.depends('sale_order_template_id')
|
||||
def _compute_require_signature(self):
|
||||
super()._compute_require_signature()
|
||||
for order in self.filtered('sale_order_template_id'):
|
||||
order.require_signature = order.sale_order_template_id.require_signature
|
||||
|
||||
@api.depends('sale_order_template_id')
|
||||
def _compute_require_payment(self):
|
||||
super()._compute_require_payment()
|
||||
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_validity_date(self):
|
||||
super()._compute_validity_date()
|
||||
for order in self.filtered('sale_order_template_id'):
|
||||
validity_days = order.sale_order_template_id.number_of_days
|
||||
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')),
|
||||
))
|
||||
|
||||
#=== ONCHANGE METHODS ===#
|
||||
|
||||
@api.onchange('company_id')
|
||||
def _onchange_company_id(self):
|
||||
"""Trigger quotation template recomputation on unsaved records company change"""
|
||||
if self._origin.id:
|
||||
return
|
||||
self._compute_sale_order_template_id()
|
||||
|
||||
@api.onchange('sale_order_template_id')
|
||||
def _onchange_sale_order_template_id(self):
|
||||
if not self.sale_order_template_id:
|
||||
return
|
||||
|
||||
sale_order_template = self.sale_order_template_id.with_context(lang=self.partner_id.lang)
|
||||
|
||||
order_lines_data = [fields.Command.clear()]
|
||||
order_lines_data += [
|
||||
fields.Command.create(line._prepare_order_line_values())
|
||||
for line in sale_order_template.sale_order_template_line_ids
|
||||
]
|
||||
|
||||
# set first line to sequence -99, so a resequence on first page doesn't cause following page
|
||||
# lines (that all have sequence 10 by default) to get mixed in the first page
|
||||
if len(order_lines_data) >= 2:
|
||||
order_lines_data[1][2]['sequence'] = -99
|
||||
|
||||
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."""
|
||||
if self._origin or not self.sale_order_template_id:
|
||||
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']
|
||||
)
|
||||
|
||||
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)),
|
||||
)):
|
||||
self._onchange_sale_order_template_id()
|
||||
|
||||
#=== ACTION METHODS ===#
|
||||
|
||||
def action_confirm(self):
|
||||
res = super().action_confirm()
|
||||
if self.env.su:
|
||||
self = self.with_user(SUPERUSER_ID)
|
||||
|
||||
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)
|
||||
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()
|
||||
|
|
@ -0,0 +1,29 @@
|
|||
# -*- 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"
|
||||
_description = "Sales Order Line"
|
||||
|
||||
sale_order_option_ids = fields.One2many('sale.order.option', 'line_id', 'Optional Products Lines')
|
||||
|
||||
@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:
|
||||
for template_line in line.order_id.sale_order_template_id.sale_order_template_line_ids:
|
||||
if line.product_id == template_line.product_id:
|
||||
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()
|
||||
|
|
@ -0,0 +1,150 @@
|
|||
# -*- 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()
|
||||
|
|
@ -0,0 +1,100 @@
|
|||
# -*- 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 ValidationError
|
||||
|
||||
|
||||
class SaleOrderTemplate(models.Model):
|
||||
_name = "sale.order.template"
|
||||
_description = "Quotation Template"
|
||||
|
||||
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')
|
||||
|
||||
name = fields.Char(string="Quotation Template", required=True)
|
||||
note = fields.Html(string="Terms and conditions", translate=True)
|
||||
|
||||
mail_template_id = fields.Many2one(
|
||||
comodel_name='mail.template',
|
||||
string="Confirmation Mail",
|
||||
domain=[('model', '=', 'sale.order')],
|
||||
help="This e-mail template will be sent on confirmation. Leave empty to send nothing.")
|
||||
number_of_days = fields.Integer(
|
||||
string="Quotation Duration",
|
||||
help="Number of days for the validity date computation of the quotation")
|
||||
|
||||
require_signature = fields.Boolean(
|
||||
string="Online Signature",
|
||||
compute='_compute_require_signature',
|
||||
store=True, readonly=False,
|
||||
help="Request a online signature to the customer in order to confirm orders automatically.")
|
||||
require_payment = fields.Boolean(
|
||||
string="Online Payment",
|
||||
compute='_compute_require_payment',
|
||||
store=True, readonly=False,
|
||||
help="Request an online payment to the customer in order to confirm orders automatically.")
|
||||
|
||||
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)
|
||||
|
||||
#=== COMPUTE METHODS ===#
|
||||
|
||||
@api.depends('company_id')
|
||||
def _compute_require_signature(self):
|
||||
for order in self:
|
||||
order.require_signature = (order.company_id or order.env.company).portal_confirmation_sign
|
||||
|
||||
@api.depends('company_id')
|
||||
def _compute_require_payment(self):
|
||||
for order in self:
|
||||
order.require_payment = (order.company_id or order.env.company).portal_confirmation_pay
|
||||
|
||||
#=== CONSTRAINT METHODS ===#
|
||||
|
||||
@api.constrains('company_id', 'sale_order_template_line_ids', 'sale_order_template_option_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:
|
||||
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')),
|
||||
template_company=template.company_id.display_name,
|
||||
))
|
||||
|
||||
#=== CRUD METHODS ===#
|
||||
|
||||
@api.model_create_multi
|
||||
def create(self, vals_list):
|
||||
records = super().create(vals_list)
|
||||
records._update_product_translations()
|
||||
return records
|
||||
|
||||
def write(self, vals):
|
||||
if 'active' in vals and not vals.get('active'):
|
||||
companies = self.env['res.company'].sudo().search([('sale_order_template_id', 'in', self.ids)])
|
||||
companies.sale_order_template_id = None
|
||||
result = super().write(vals)
|
||||
self._update_product_translations()
|
||||
return result
|
||||
|
||||
def _update_product_translations(self):
|
||||
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()
|
||||
|
|
@ -0,0 +1,108 @@
|
|||
# -*- 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 SaleOrderTemplateLine(models.Model):
|
||||
_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"),
|
||||
]
|
||||
|
||||
sale_order_template_id = fields.Many2one(
|
||||
comodel_name='sale.order.template',
|
||||
string='Quotation Template Reference',
|
||||
index=True, required=True,
|
||||
ondelete='cascade')
|
||||
sequence = fields.Integer(
|
||||
string="Sequence",
|
||||
help="Gives the sequence order when displaying a list of sale quote lines.",
|
||||
default=10)
|
||||
|
||||
company_id = fields.Many2one(
|
||||
related='sale_order_template_id.company_id', store=True, index=True)
|
||||
|
||||
product_id = fields.Many2one(
|
||||
comodel_name='product.product',
|
||||
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)
|
||||
|
||||
product_uom_id = fields.Many2one(
|
||||
comodel_name='uom.uom',
|
||||
string="Unit of Measure",
|
||||
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')
|
||||
product_uom_qty = fields.Float(
|
||||
string='Quantity',
|
||||
required=True,
|
||||
digits='Product Unit of Measure',
|
||||
default=1)
|
||||
|
||||
display_type = fields.Selection([
|
||||
('line_section', "Section"),
|
||||
('line_note', "Note")], default=False)
|
||||
|
||||
#=== 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_product_uom_id(self):
|
||||
for option in self:
|
||||
option.product_uom_id = option.product_id.uom_id
|
||||
|
||||
#=== CRUD METHODS ===#
|
||||
|
||||
@api.model_create_multi
|
||||
def create(self, vals_list):
|
||||
for vals in vals_list:
|
||||
if vals.get('display_type', self.default_get(['display_type'])['display_type']):
|
||||
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')):
|
||||
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)
|
||||
|
||||
#=== BUSINESS METHODS ===#
|
||||
|
||||
def _prepare_order_line_values(self):
|
||||
""" Give the values to create the corresponding order line.
|
||||
|
||||
:return: `sale.order.line` create values
|
||||
:rtype: dict
|
||||
"""
|
||||
self.ensure_one()
|
||||
return {
|
||||
'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,
|
||||
}
|
||||
|
|
@ -0,0 +1,74 @@
|
|||
# -*- 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