mirror of
https://github.com/bringout/oca-ocb-sale.git
synced 2026-04-27 13:32:03 +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,13 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from . import account_move
|
||||
from . import product
|
||||
from . import event_booth_registration
|
||||
from . import event_booth
|
||||
from . import event_booth_category
|
||||
from . import event_type_booth
|
||||
from . import sale_order
|
||||
from . import sale_order_line
|
||||
from . import sale_order_template_line
|
||||
from . import sale_order_template_option
|
||||
|
|
@ -0,0 +1,16 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo import models
|
||||
|
||||
|
||||
class AccountMove(models.Model):
|
||||
_inherit = 'account.move'
|
||||
|
||||
def _invoice_paid_hook(self):
|
||||
""" When an invoice linked to a sales order selling registrations is
|
||||
paid, update booths accordingly as they are booked when invoice is paid.
|
||||
"""
|
||||
res = super(AccountMove, self)._invoice_paid_hook()
|
||||
self.mapped('line_ids.sale_line_ids')._update_event_booths(set_paid=True)
|
||||
return res
|
||||
|
|
@ -0,0 +1,49 @@
|
|||
# -*- 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 EventBooth(models.Model):
|
||||
_inherit = 'event.booth'
|
||||
|
||||
# registrations
|
||||
event_booth_registration_ids = fields.One2many('event.booth.registration', 'event_booth_id')
|
||||
# sale information
|
||||
sale_order_line_registration_ids = fields.Many2many(
|
||||
'sale.order.line', 'event_booth_registration',
|
||||
'event_booth_id', 'sale_order_line_id', string='SO Lines with reservations',
|
||||
groups='sales_team.group_sale_salesman', copy=False)
|
||||
sale_order_line_id = fields.Many2one(
|
||||
'sale.order.line', string='Final Sale Order Line', ondelete='set null',
|
||||
readonly=True, states={'available': [('readonly', False)]},
|
||||
groups='sales_team.group_sale_salesman', copy=False)
|
||||
sale_order_id = fields.Many2one(
|
||||
related='sale_order_line_id.order_id', store='True', readonly=True,
|
||||
groups='sales_team.group_sale_salesman')
|
||||
is_paid = fields.Boolean('Is Paid', copy=False)
|
||||
|
||||
@api.ondelete(at_uninstall=False)
|
||||
def _unlink_except_linked_sale_order(self):
|
||||
booth_with_so = self.sudo().filtered('sale_order_id')
|
||||
if booth_with_so:
|
||||
raise UserError(_(
|
||||
'You can\'t delete the following booths as they are linked to sales orders: '
|
||||
'%(booths)s', booths=', '.join(booth_with_so.mapped('name'))))
|
||||
|
||||
def action_set_paid(self):
|
||||
self.write({'is_paid': True})
|
||||
|
||||
def action_view_sale_order(self):
|
||||
self.sale_order_id.ensure_one()
|
||||
action = self.env['ir.actions.actions']._for_xml_id('sale.action_orders')
|
||||
action['views'] = [(False, 'form')]
|
||||
action['res_id'] = self.sale_order_id.id
|
||||
return action
|
||||
|
||||
def _get_booth_multiline_description(self):
|
||||
return '%s : \n%s' % (
|
||||
self.event_id.display_name,
|
||||
'\n'.join(['- %s' % booth.name for booth in self])
|
||||
)
|
||||
|
|
@ -0,0 +1,107 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
import logging
|
||||
|
||||
from odoo import api, fields, models
|
||||
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class EventBoothCategory(models.Model):
|
||||
_inherit = 'event.booth.category'
|
||||
|
||||
def _default_product_id(self):
|
||||
return self.env.ref('event_booth_sale.product_product_event_booth', raise_if_not_found=False)
|
||||
|
||||
product_id = fields.Many2one(
|
||||
'product.product', string='Product', required=True,
|
||||
domain=[('detailed_type', '=', 'event_booth')], default=_default_product_id,
|
||||
groups="event.group_event_registration_desk")
|
||||
price = fields.Float(
|
||||
string='Price', compute='_compute_price', digits='Product Price', readonly=False,
|
||||
store=True, groups="event.group_event_registration_desk")
|
||||
currency_id = fields.Many2one(related='product_id.currency_id', groups="event.group_event_registration_desk")
|
||||
price_reduce = fields.Float(
|
||||
string='Price Reduce', compute='_compute_price_reduce',
|
||||
compute_sudo=True, digits='Product Price', groups="event.group_event_registration_desk")
|
||||
price_reduce_taxinc = fields.Float(
|
||||
string='Price Reduce Tax inc', compute='_compute_price_reduce_taxinc',
|
||||
compute_sudo=True
|
||||
)
|
||||
image_1920 = fields.Image(compute='_compute_image_1920', readonly=False, store=True)
|
||||
|
||||
@api.depends('product_id')
|
||||
def _compute_image_1920(self):
|
||||
for category in self:
|
||||
category.image_1920 = category.image_1920 if category.image_1920 else category.product_id.image_1920
|
||||
|
||||
@api.depends('product_id')
|
||||
def _compute_price(self):
|
||||
""" By default price comes from category but can be changed by event
|
||||
people as product may be shared across various categories. """
|
||||
for category in self:
|
||||
if category.product_id and category.product_id.list_price:
|
||||
category.price = category.product_id.list_price + category.product_id.price_extra
|
||||
|
||||
@api.depends_context('pricelist', 'quantity')
|
||||
@api.depends('product_id', 'price')
|
||||
def _compute_price_reduce(self):
|
||||
for category in self:
|
||||
product = category.product_id
|
||||
pricelist = product.product_tmpl_id._get_contextual_pricelist()
|
||||
lst_price = product.currency_id._convert(
|
||||
product.lst_price,
|
||||
pricelist.currency_id,
|
||||
self.env.company,
|
||||
fields.Datetime.now(),
|
||||
round=False,
|
||||
)
|
||||
discount = (lst_price - product._get_contextual_price()) / lst_price if lst_price else 0.0
|
||||
category.price_reduce = (1.0 - discount) * category.price
|
||||
|
||||
@api.depends_context('pricelist', 'quantity')
|
||||
@api.depends('product_id', 'price_reduce')
|
||||
def _compute_price_reduce_taxinc(self):
|
||||
for category in self:
|
||||
tax_ids = category.product_id.taxes_id
|
||||
taxes = tax_ids.compute_all(category.price_reduce, category.currency_id, 1.0, product=category.product_id)
|
||||
category.price_reduce_taxinc = taxes['total_included']
|
||||
|
||||
def _init_column(self, column_name):
|
||||
""" Initialize product_id for existing columns when installing sale
|
||||
bridge, to ensure required attribute is fulfilled. """
|
||||
if column_name != "product_id":
|
||||
return super(EventBoothCategory, self)._init_column(column_name)
|
||||
|
||||
# fetch void columns
|
||||
self.env.cr.execute("SELECT id FROM %s WHERE product_id IS NULL" % self._table)
|
||||
booth_category_ids = self.env.cr.fetchall()
|
||||
if not booth_category_ids:
|
||||
return
|
||||
|
||||
# update existing columns
|
||||
_logger.debug("Table '%s': setting default value of new column %s to unique values for each row",
|
||||
self._table, column_name)
|
||||
default_booth_product = self._default_product_id()
|
||||
if default_booth_product:
|
||||
product_id = default_booth_product.id
|
||||
else:
|
||||
product_id = self.env['product.product'].create({
|
||||
'name': 'Generic Event Booth Product',
|
||||
'categ_id': self.env.ref('event_sale.product_category_events').id,
|
||||
'list_price': 100,
|
||||
'standard_price': 0,
|
||||
'detailed_type': 'event_booth',
|
||||
'invoice_policy': 'order',
|
||||
}).id
|
||||
self.env['ir.model.data'].create({
|
||||
'name': 'product_product_event_booth',
|
||||
'module': 'event_booth_sale',
|
||||
'model': 'product.product',
|
||||
'res_id': product_id,
|
||||
})
|
||||
self.env.cr._obj.execute(
|
||||
f'UPDATE {self._table} SET product_id = %s WHERE id IN %s;',
|
||||
(product_id, tuple(booth_category_ids))
|
||||
)
|
||||
|
|
@ -0,0 +1,78 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo import api, fields, models, _
|
||||
|
||||
|
||||
class EventBoothRegistration(models.Model):
|
||||
"""event.booth.registrations are used to allow multiple partners to book the same booth.
|
||||
Whenever a partner has paid their registration all the others linked to the booth will be deleted."""
|
||||
|
||||
_name = 'event.booth.registration'
|
||||
_description = 'Event Booth Registration'
|
||||
|
||||
sale_order_line_id = fields.Many2one('sale.order.line', string='Sale Order Line', required=True, ondelete='cascade')
|
||||
event_booth_id = fields.Many2one('event.booth', string='Booth', required=True)
|
||||
partner_id = fields.Many2one(
|
||||
'res.partner', related='sale_order_line_id.order_partner_id', store=True)
|
||||
contact_name = fields.Char(string='Contact Name', compute='_compute_contact_name', readonly=False, store=True)
|
||||
contact_email = fields.Char(string='Contact Email', compute='_compute_contact_email', readonly=False, store=True)
|
||||
contact_phone = fields.Char(string='Contact Phone', compute='_compute_contact_phone', readonly=False, store=True)
|
||||
contact_mobile = fields.Char(string='Contact Mobile', compute='_compute_contact_mobile', readonly=False, store=True)
|
||||
|
||||
_sql_constraints = [('unique_registration', 'unique(sale_order_line_id, event_booth_id)',
|
||||
'There can be only one registration for a booth by sale order line')]
|
||||
|
||||
@api.depends('partner_id')
|
||||
def _compute_contact_name(self):
|
||||
for registration in self:
|
||||
if not registration.contact_name:
|
||||
registration.contact_name = registration.partner_id.name or False
|
||||
|
||||
@api.depends('partner_id')
|
||||
def _compute_contact_email(self):
|
||||
for registration in self:
|
||||
if not registration.contact_email:
|
||||
registration.contact_email = registration.partner_id.email or False
|
||||
|
||||
@api.depends('partner_id')
|
||||
def _compute_contact_phone(self):
|
||||
for registration in self:
|
||||
if not registration.contact_phone:
|
||||
registration.contact_phone = registration.partner_id.phone or False
|
||||
|
||||
@api.depends('partner_id')
|
||||
def _compute_contact_mobile(self):
|
||||
for registration in self:
|
||||
if not registration.contact_mobile:
|
||||
registration.contact_mobile = registration.partner_id.mobile or False
|
||||
|
||||
@api.model
|
||||
def _get_fields_for_booth_confirmation(self):
|
||||
return ['sale_order_line_id', 'partner_id', 'contact_name', 'contact_email', 'contact_phone', 'contact_mobile']
|
||||
|
||||
def action_confirm(self):
|
||||
for registration in self:
|
||||
values = {
|
||||
field: registration[field].id if isinstance(registration[field], models.BaseModel) else registration[field]
|
||||
for field in self._get_fields_for_booth_confirmation()
|
||||
}
|
||||
registration.event_booth_id.action_confirm(values)
|
||||
self._cancel_pending_registrations()
|
||||
|
||||
def _cancel_pending_registrations(self):
|
||||
body = '<p>%(message)s: <ul>%(booth_names)s</ul></p>' % {
|
||||
'message': _('Your order has been cancelled because the following booths have been reserved'),
|
||||
'booth_names': ''.join('<li>%s</li>' % booth.display_name for booth in self.event_booth_id)
|
||||
}
|
||||
other_registrations = self.search([
|
||||
('event_booth_id', 'in', self.event_booth_id.ids),
|
||||
('id', 'not in', self.ids)
|
||||
])
|
||||
for order in other_registrations.sale_order_line_id.order_id:
|
||||
order.sudo().message_post(
|
||||
body=body,
|
||||
partner_ids=order.user_id.partner_id.ids,
|
||||
)
|
||||
order.sudo()._action_cancel()
|
||||
other_registrations.unlink()
|
||||
|
|
@ -0,0 +1,17 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo import api, fields, models
|
||||
|
||||
|
||||
class EventTypeBooth(models.Model):
|
||||
_inherit = 'event.type.booth'
|
||||
|
||||
product_id = fields.Many2one(related='booth_category_id.product_id')
|
||||
price = fields.Float(related='booth_category_id.price', store=True)
|
||||
currency_id = fields.Many2one(related='booth_category_id.currency_id')
|
||||
|
||||
@api.model
|
||||
def _get_event_booth_fields_whitelist(self):
|
||||
res = super(EventTypeBooth, self)._get_event_booth_fields_whitelist()
|
||||
return res + ['product_id', 'price']
|
||||
|
|
@ -0,0 +1,31 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo import api, fields, models
|
||||
|
||||
|
||||
class ProductTemplate(models.Model):
|
||||
_inherit = 'product.template'
|
||||
|
||||
detailed_type = fields.Selection(selection_add=[
|
||||
('event_booth', 'Event Booth'),
|
||||
], ondelete={'event_booth': 'set service'})
|
||||
|
||||
@api.onchange('detailed_type')
|
||||
def _onchange_type_event_booth(self):
|
||||
if self.detailed_type == 'event_booth':
|
||||
self.invoice_policy = 'order'
|
||||
|
||||
def _detailed_type_mapping(self):
|
||||
type_mapping = super()._detailed_type_mapping()
|
||||
type_mapping['event_booth'] = 'service'
|
||||
return type_mapping
|
||||
|
||||
|
||||
class Product(models.Model):
|
||||
_inherit = 'product.product'
|
||||
|
||||
@api.onchange('detailed_type')
|
||||
def _onchange_type_event_booth(self):
|
||||
if self.detailed_type == 'event_booth':
|
||||
self.invoice_policy = 'order'
|
||||
|
|
@ -0,0 +1,35 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo import api, fields, models
|
||||
|
||||
|
||||
class SaleOrder(models.Model):
|
||||
_inherit = 'sale.order'
|
||||
|
||||
event_booth_ids = fields.One2many('event.booth', 'sale_order_id', string='Booths')
|
||||
event_booth_count = fields.Integer(string='Booth Count', compute='_compute_event_booth_count')
|
||||
|
||||
@api.depends('event_booth_ids')
|
||||
def _compute_event_booth_count(self):
|
||||
if self.ids:
|
||||
slot_data = self.env['event.booth']._read_group(
|
||||
[('sale_order_id', 'in', self.ids)],
|
||||
['sale_order_id'], ['sale_order_id']
|
||||
)
|
||||
slot_mapped = dict((data['sale_order_id'][0], data['sale_order_id_count']) for data in slot_data)
|
||||
else:
|
||||
slot_mapped = dict()
|
||||
for so in self:
|
||||
so.event_booth_count = slot_mapped.get(so.id, 0)
|
||||
|
||||
def action_confirm(self):
|
||||
res = super(SaleOrder, self).action_confirm()
|
||||
for so in self:
|
||||
so.order_line._update_event_booths()
|
||||
return res
|
||||
|
||||
def action_view_booth_list(self):
|
||||
action = self.env['ir.actions.act_window']._for_xml_id('event_booth.event_booth_action')
|
||||
action['domain'] = [('sale_order_id', 'in', self.ids)]
|
||||
return action
|
||||
|
|
@ -0,0 +1,116 @@
|
|||
# -*- 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 SaleOrderLine(models.Model):
|
||||
_inherit = 'sale.order.line'
|
||||
|
||||
event_booth_category_id = fields.Many2one('event.booth.category', string='Booths Category', ondelete='set null')
|
||||
event_booth_pending_ids = fields.Many2many(
|
||||
'event.booth', string='Pending Booths', search='_search_event_booth_pending_ids',
|
||||
compute='_compute_event_booth_pending_ids', inverse='_inverse_event_booth_pending_ids',
|
||||
help='Used to create registration when providing the desired event booth.'
|
||||
)
|
||||
event_booth_registration_ids = fields.One2many(
|
||||
'event.booth.registration', 'sale_order_line_id', string='Confirmed Registration')
|
||||
event_booth_ids = fields.One2many('event.booth', 'sale_order_line_id', string='Confirmed Booths')
|
||||
is_event_booth = fields.Boolean(compute='_compute_is_event_booth')
|
||||
|
||||
@api.depends('product_id.type')
|
||||
def _compute_is_event_booth(self):
|
||||
for record in self:
|
||||
record.is_event_booth = record.product_id.detailed_type == 'event_booth'
|
||||
|
||||
@api.depends('event_booth_ids')
|
||||
def _compute_name_short(self):
|
||||
wbooth = self.filtered(lambda line: line.event_booth_pending_ids)
|
||||
for record in wbooth:
|
||||
record.name_short = record.event_booth_pending_ids.event_id.name
|
||||
super(SaleOrderLine, self - wbooth)._compute_name_short()
|
||||
|
||||
@api.depends('event_booth_registration_ids')
|
||||
def _compute_event_booth_pending_ids(self):
|
||||
for so_line in self:
|
||||
so_line.event_booth_pending_ids = so_line.event_booth_registration_ids.event_booth_id
|
||||
|
||||
def _inverse_event_booth_pending_ids(self):
|
||||
""" This method will take care of creating the event.booth.registrations based on selected booths.
|
||||
It will also unlink ones that are de-selected. """
|
||||
|
||||
for so_line in self:
|
||||
existing_booths = so_line.event_booth_registration_ids.event_booth_id or self.env[
|
||||
'event.booth']
|
||||
selected_booths = so_line.event_booth_pending_ids
|
||||
|
||||
so_line.event_booth_registration_ids.filtered(
|
||||
lambda reg: reg.event_booth_id not in selected_booths).unlink()
|
||||
|
||||
self.env['event.booth.registration'].create([{
|
||||
'event_booth_id': booth.id,
|
||||
'sale_order_line_id': so_line.id,
|
||||
'partner_id': so_line.order_id.partner_id.id
|
||||
} for booth in selected_booths - existing_booths])
|
||||
|
||||
def _search_event_booth_pending_ids(self, operator, value):
|
||||
return [('event_booth_registration_ids.event_booth_id', operator, value)]
|
||||
|
||||
@api.constrains('event_booth_registration_ids')
|
||||
def _check_event_booth_registration_ids(self):
|
||||
if len(self.event_booth_registration_ids.event_booth_id.event_id) > 1:
|
||||
raise ValidationError(_('Registrations from the same Order Line must belong to a single event.'))
|
||||
|
||||
@api.onchange('product_id')
|
||||
def _onchange_product_id_booth(self):
|
||||
"""We reset the event when the selected product doesn't belong to any pending booths."""
|
||||
if self.event_id and (not self.product_id or self.product_id not in self.event_booth_pending_ids.product_id):
|
||||
self.event_id = None
|
||||
|
||||
@api.onchange('event_id')
|
||||
def _onchange_event_id_booth(self):
|
||||
"""We reset the pending booths when the event changes to avoid inconsistent state."""
|
||||
if self.event_booth_pending_ids and (not self.event_id or self.event_id != self.event_booth_pending_ids.event_id):
|
||||
self.event_booth_pending_ids = None
|
||||
|
||||
@api.depends('event_booth_registration_ids.event_booth_id')
|
||||
def _compute_name(self):
|
||||
"""Override to add the compute dependency.
|
||||
|
||||
The custom name logic can be found below in _get_sale_order_line_multiline_description_sale.
|
||||
"""
|
||||
super()._compute_name()
|
||||
|
||||
def _update_event_booths(self, set_paid=False):
|
||||
for so_line in self.filtered('is_event_booth'):
|
||||
if so_line.event_booth_pending_ids and not so_line.event_booth_ids:
|
||||
unavailable = so_line.event_booth_pending_ids.filtered(lambda booth: not booth.is_available)
|
||||
if unavailable:
|
||||
raise ValidationError(
|
||||
_('The following booths are unavailable, please remove them to continue : %(booth_names)s',
|
||||
booth_names=''.join('\n\t- %s' % booth.display_name for booth in unavailable)))
|
||||
so_line.event_booth_registration_ids.sudo().action_confirm()
|
||||
if so_line.event_booth_ids and set_paid:
|
||||
so_line.event_booth_ids.sudo().action_set_paid()
|
||||
return True
|
||||
|
||||
def _get_sale_order_line_multiline_description_sale(self):
|
||||
if self.event_booth_pending_ids:
|
||||
return self.event_booth_pending_ids._get_booth_multiline_description()
|
||||
return super()._get_sale_order_line_multiline_description_sale()
|
||||
|
||||
def _get_display_price(self):
|
||||
if self.event_booth_pending_ids and self.event_id:
|
||||
company = self.event_id.company_id or self.env.company
|
||||
currency = company.currency_id
|
||||
pricelist = self.order_id.pricelist_id
|
||||
if pricelist.discount_policy == "with_discount":
|
||||
total_price = sum([booth.booth_category_id.with_context(pricelist=pricelist.id).price_reduce for booth in self.event_booth_pending_ids])
|
||||
else:
|
||||
total_price = sum([booth.price for booth in self.event_booth_pending_ids])
|
||||
return currency._convert(
|
||||
total_price, self.order_id.currency_id,
|
||||
self.order_id.company_id or self.env.company.id,
|
||||
self.order_id.date_order or fields.Date.today())
|
||||
return super()._get_display_price()
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
from odoo import fields, models
|
||||
|
||||
class SaleOrderTemplateLine(models.Model):
|
||||
_inherit = "sale.order.template.line"
|
||||
|
||||
product_id = fields.Many2one(domain="[('sale_ok', '=', True), ('detailed_type', 'not in', ['event', 'event_booth']), ('company_id', 'in', [company_id, False])]")
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
from odoo import fields, models
|
||||
|
||||
class SaleOrderTemplateOption(models.Model):
|
||||
_inherit = "sale.order.template.option"
|
||||
|
||||
product_id = fields.Many2one(domain="[('sale_ok', '=', True), ('detailed_type', 'not in', ['event', 'event_booth']), ('company_id', 'in', [company_id, False])]")
|
||||
Loading…
Add table
Add a link
Reference in a new issue