mirror of
https://github.com/bringout/oca-ocb-sale.git
synced 2026-04-27 17:52:01 +02:00
19.0 vanilla
This commit is contained in:
parent
79f83631d5
commit
73afc09215
6267 changed files with 1534193 additions and 1130106 deletions
|
|
@ -4,5 +4,6 @@
|
|||
from . import loyalty_card
|
||||
from . import loyalty_program
|
||||
from . import loyalty_rule
|
||||
from . import product_product
|
||||
from . import sale_order_line
|
||||
from . import sale_order
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo import models
|
||||
|
||||
|
||||
class LoyaltyCard(models.Model):
|
||||
_inherit = 'loyalty.card'
|
||||
|
||||
|
|
|
|||
|
|
@ -1,13 +1,22 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo import fields, models
|
||||
from odoo import api, fields, models
|
||||
|
||||
|
||||
class LoyaltyProgram(models.Model):
|
||||
_name = 'loyalty.program'
|
||||
_inherit = ['loyalty.program', 'website.multi.mixin']
|
||||
|
||||
ecommerce_ok = fields.Boolean("Available on Website", default=True)
|
||||
show_non_published_product_warning = fields.Boolean(compute='_compute_show_non_published_product_warning')
|
||||
|
||||
@api.depends('program_type', 'trigger_product_ids.website_published')
|
||||
def _compute_show_non_published_product_warning(self):
|
||||
for program in self:
|
||||
program.show_non_published_product_warning = (
|
||||
program.program_type == 'ewallet'
|
||||
and any(not product.website_published for product in program.trigger_product_ids)
|
||||
)
|
||||
|
||||
def action_program_share(self):
|
||||
self.ensure_one()
|
||||
|
|
|
|||
|
|
@ -1,25 +1,27 @@
|
|||
# -*- 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 LoyaltyRule(models.Model):
|
||||
_inherit = 'loyalty.rule'
|
||||
|
||||
website_id = fields.Many2one(related='program_id.website_id', store=True)
|
||||
|
||||
# NOTE: is this sufficient?
|
||||
@api.constrains('code', 'website_id')
|
||||
@api.constrains('code', 'website_id', 'active')
|
||||
def _constrains_code(self):
|
||||
#Programs with the same code are allowed to coexist as long
|
||||
# as they are not both accessible from a website.
|
||||
with_code = self.filtered(lambda r: r.mode == 'with_code')
|
||||
with_code = self.filtered(lambda r: r.mode == 'with_code' and r.active)
|
||||
mapped_codes = with_code.mapped('code')
|
||||
read_result = self.env['loyalty.rule'].search_read(
|
||||
[('website_id', 'in', [False] + [w.id for w in self.website_id]),
|
||||
('mode', '=', 'with_code'), ('code', 'in', mapped_codes),
|
||||
('id', 'not in', with_code.ids)],
|
||||
('mode', '=', 'with_code'),
|
||||
('code', 'in', mapped_codes),
|
||||
('id', 'not in', with_code.ids),
|
||||
('active', '=', True)],
|
||||
fields=['code', 'website_id']) + [{'code': p.code, 'website_id': p.website_id} for p in with_code]
|
||||
existing_codes = set()
|
||||
for res in read_result:
|
||||
|
|
@ -30,5 +32,7 @@ class LoyaltyRule(models.Model):
|
|||
raise ValidationError(_('The promo code must be unique.'))
|
||||
existing_codes.add(val)
|
||||
# Prevent coupons and programs from sharing a code
|
||||
if self.env['loyalty.card'].search_count([('code', 'in', mapped_codes)]):
|
||||
if self.env['loyalty.card'].search_count([
|
||||
('code', 'in', mapped_codes), ('active', '=', True)
|
||||
]):
|
||||
raise ValidationError(_('A coupon with the same code was found.'))
|
||||
|
|
|
|||
|
|
@ -0,0 +1,41 @@
|
|||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo import models
|
||||
|
||||
|
||||
class ProductProduct(models.Model):
|
||||
_inherit = 'product.product'
|
||||
|
||||
def _can_return_content(self, field_name=None, access_token=None):
|
||||
""" Override of `orm` to give public users access to the unpublished product image.
|
||||
|
||||
Give access to the public users to the unpublished product images if they are linked to a
|
||||
reward.
|
||||
|
||||
:param field_name: The name of the field to check.
|
||||
:param access_token: The access token.
|
||||
:return: Whether to allow the access to the image.
|
||||
:rtype: bool
|
||||
"""
|
||||
if (
|
||||
field_name in ["image_%s" % size for size in [1920, 1024, 512, 256, 128]]
|
||||
and self.env['loyalty.reward'].sudo().search_count([
|
||||
('discount_line_product_id', '=', self.id),
|
||||
], limit=1)
|
||||
):
|
||||
return True
|
||||
return super()._can_return_content(field_name, access_token)
|
||||
|
||||
def _get_product_placeholder_filename(self):
|
||||
""" Override of `product` to set a default image for reward products. """
|
||||
# In sudo mode to allow eCommerce customers to see the placeholder
|
||||
if self.env['loyalty.reward'].sudo().search_count([
|
||||
('discount_line_product_id', '=', self.id),
|
||||
], limit=1):
|
||||
if self.env['loyalty.reward'].sudo().search_count([
|
||||
('program_type', '=', 'gift_card'),
|
||||
('discount_line_product_id', '=', self.id),
|
||||
], limit=1):
|
||||
return 'loyalty/static/img/gift_card.png'
|
||||
return 'loyalty/static/img/discount_placeholder_thumbnail.png'
|
||||
return super()._get_product_placeholder_filename()
|
||||
|
|
@ -1,15 +1,16 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from collections import defaultdict
|
||||
from datetime import timedelta
|
||||
|
||||
from odoo import api, fields, models
|
||||
from odoo.exceptions import UserError
|
||||
from odoo.osv import expression
|
||||
from odoo.fields import Domain
|
||||
from odoo.http import request
|
||||
|
||||
|
||||
class SaleOrder(models.Model):
|
||||
_inherit = "sale.order"
|
||||
_inherit = 'sale.order'
|
||||
|
||||
# List of disabled rewards for automatic claim
|
||||
disabled_auto_rewards = fields.Many2many("loyalty.reward", relation="sale_order_disabled_auto_rewards_rel")
|
||||
|
|
@ -22,7 +23,7 @@ class SaleOrder(models.Model):
|
|||
if leaf[0] != 'sale_ok':
|
||||
continue
|
||||
res[idx] = ('ecommerce_ok', '=', True)
|
||||
return expression.AND([res, [('website_id', 'in', (self.website_id.id, False))]])
|
||||
return Domain.AND([res, [('website_id', 'in', (self.website_id.id, False))]])
|
||||
return res
|
||||
|
||||
def _get_trigger_domain(self):
|
||||
|
|
@ -33,9 +34,12 @@ class SaleOrder(models.Model):
|
|||
if leaf[0] != 'program_id.sale_ok':
|
||||
continue
|
||||
res[idx] = ('program_id.ecommerce_ok', '=', True)
|
||||
return expression.AND([res, [('program_id.website_id', 'in', (self.website_id.id, False))]])
|
||||
return Domain.AND([res, [('program_id.website_id', 'in', (self.website_id.id, False))]])
|
||||
return res
|
||||
|
||||
def _get_program_timezone(self):
|
||||
return self.website_id.salesperson_id.tz or super()._get_program_timezone()
|
||||
|
||||
def _try_pending_coupon(self):
|
||||
if not request:
|
||||
return False
|
||||
|
|
@ -73,12 +77,15 @@ class SaleOrder(models.Model):
|
|||
claimed_reward_count = 0
|
||||
claimable_rewards = self._get_claimable_rewards()
|
||||
for coupon, rewards in claimable_rewards.items():
|
||||
if len(coupon.program_id.reward_ids) != 1 or\
|
||||
coupon.program_id.is_nominative or\
|
||||
(rewards.reward_type == 'product' and rewards.multi_product) or\
|
||||
rewards in self.disabled_auto_rewards or\
|
||||
rewards in self.order_line.reward_id:
|
||||
if (
|
||||
len(coupon.program_id.reward_ids) != 1
|
||||
or coupon.program_id.is_nominative
|
||||
or (rewards.reward_type == 'product' and rewards.multi_product)
|
||||
or rewards in self.disabled_auto_rewards
|
||||
or rewards in self.order_line.reward_id
|
||||
):
|
||||
continue
|
||||
|
||||
try:
|
||||
res = self._apply_program_reward(rewards, coupon)
|
||||
if 'error' not in res:
|
||||
|
|
@ -95,7 +102,7 @@ class SaleOrder(models.Model):
|
|||
products with different taxes.
|
||||
In this case, each taxes will have their own discount line. This is required
|
||||
to have correct amount of taxes according to the discount.
|
||||
But we wan't these lines to be `visually` merged into a single one in the
|
||||
But we want these lines to be `visually` merged into a single one in the
|
||||
e-commerce since the end user should only see one discount line.
|
||||
This is only possible since we don't show taxes in cart.
|
||||
eg:
|
||||
|
|
@ -120,14 +127,14 @@ class SaleOrder(models.Model):
|
|||
continue
|
||||
new_lines += self.env['sale.order.line'].new({
|
||||
'product_id': lines[0].product_id.id,
|
||||
'tax_id': False,
|
||||
'tax_ids': False,
|
||||
'price_unit': sum(lines.mapped('price_unit')),
|
||||
'price_subtotal': sum(lines.mapped('price_subtotal')),
|
||||
'price_total': sum(lines.mapped('price_total')),
|
||||
'discount': 0.0,
|
||||
'name': lines[0].name_short if lines.reward_id.reward_type != 'product' else lines[0].name,
|
||||
'product_uom_qty': 1,
|
||||
'product_uom': lines[0].product_uom.id,
|
||||
'product_uom_id': lines[0].product_uom_id.id,
|
||||
'order_id': order.id,
|
||||
'is_reward_line': True,
|
||||
'coupon_id': lines.coupon_id,
|
||||
|
|
@ -156,20 +163,36 @@ class SaleOrder(models.Model):
|
|||
request.session.pop('successful_code')
|
||||
return code
|
||||
|
||||
def _cart_update(self, product_id, line_id=None, add_qty=0, set_qty=0, **kwargs):
|
||||
def _set_delivery_method(self, *args, **kwargs):
|
||||
super()._set_delivery_method(*args, **kwargs)
|
||||
self._update_programs_and_rewards()
|
||||
|
||||
line = self.order_line.filtered(lambda sol: sol.product_id.id == product_id)[:1]
|
||||
reward_id = line.reward_id
|
||||
if set_qty == 0 and line.coupon_id and reward_id and reward_id.reward_type == 'discount':
|
||||
# Force the deletion of the line even if it's a temporary record created by new()
|
||||
line_id = line.id
|
||||
def _remove_delivery_line(self):
|
||||
super()._remove_delivery_line()
|
||||
self._update_programs_and_rewards()
|
||||
|
||||
res = super()._cart_update(
|
||||
product_id, line_id=line_id, add_qty=add_qty, set_qty=set_qty, **kwargs
|
||||
)
|
||||
def _cart_update_order_line(self, order_line, quantity, **kwargs):
|
||||
if (
|
||||
quantity <= 0
|
||||
and order_line.coupon_id
|
||||
and order_line.reward_id
|
||||
and order_line.reward_id.reward_type == 'discount'
|
||||
):
|
||||
# When a reward line is deleted we remove it from the auto claimable rewards
|
||||
order_line = order_line.with_context(website_sale_loyalty_delete=True)
|
||||
|
||||
return super()._cart_update_order_line(order_line, quantity, **kwargs)
|
||||
|
||||
def _verify_cart_after_update(self):
|
||||
super()._verify_cart_after_update()
|
||||
self._update_programs_and_rewards()
|
||||
self._auto_apply_rewards()
|
||||
return res
|
||||
if request: # In case the rewards application modifies the cart quantity
|
||||
request.session['website_sale_cart_quantity'] = self.cart_quantity
|
||||
|
||||
def _get_non_delivery_lines(self):
|
||||
"""Override of `website_sale` to exclude delivery reward lines."""
|
||||
return super()._get_non_delivery_lines() - self._get_free_shipping_lines()
|
||||
|
||||
def _get_free_shipping_lines(self):
|
||||
self.ensure_one()
|
||||
|
|
@ -185,7 +208,7 @@ class SaleOrder(models.Model):
|
|||
"""Remove coupons from abandonned ecommerce order."""
|
||||
ICP = self.env['ir.config_parameter']
|
||||
validity = ICP.get_param('website_sale_coupon.abandonned_coupon_validity', 4)
|
||||
validity = fields.Datetime.to_string(fields.datetime.now() - timedelta(days=int(validity)))
|
||||
validity = fields.Datetime.to_string(fields.Datetime.now() - timedelta(days=int(validity)))
|
||||
so_to_reset = self.env['sale.order'].search([
|
||||
('state', '=', 'draft'),
|
||||
('write_date', '<', validity),
|
||||
|
|
@ -196,20 +219,47 @@ class SaleOrder(models.Model):
|
|||
for so in so_to_reset:
|
||||
so._update_programs_and_rewards()
|
||||
|
||||
def _get_website_sale_extra_values(self):
|
||||
promo_code_success = self.get_promo_code_success_message(delete=False)
|
||||
promo_code_error = self.get_promo_code_error(delete=False)
|
||||
def _get_claimable_and_showable_rewards(self):
|
||||
self.ensure_one()
|
||||
res = self._get_claimable_rewards()
|
||||
loyality_cards = self.env['loyalty.card'].search([
|
||||
('partner_id', '=', self.partner_id.id),
|
||||
('program_id', 'any', self._get_program_domain()),
|
||||
'|',
|
||||
('program_id.trigger', '=', 'with_code'),
|
||||
'&', ('program_id.trigger', '=', 'auto'), ('program_id.applies_on', '=', 'future'),
|
||||
])
|
||||
total_is_zero = self.currency_id.is_zero(self.amount_total)
|
||||
global_discount_reward = self._get_applied_global_discount()
|
||||
for coupon in loyality_cards:
|
||||
points = self._get_real_points_for_coupon(coupon)
|
||||
for reward in coupon.program_id.reward_ids - self.order_line.reward_id:
|
||||
if (
|
||||
reward.is_global_discount
|
||||
and global_discount_reward
|
||||
and self._best_global_discount_already_applied(global_discount_reward, reward)
|
||||
):
|
||||
continue
|
||||
if reward.reward_type == 'discount' and total_is_zero:
|
||||
continue
|
||||
if coupon.expiration_date and coupon.expiration_date < fields.Date.today():
|
||||
continue
|
||||
if points >= reward.required_points:
|
||||
if coupon in res:
|
||||
res[coupon] |= reward
|
||||
else:
|
||||
res[coupon] = reward
|
||||
return res
|
||||
|
||||
return {
|
||||
'promo_code_success': promo_code_success,
|
||||
'promo_code_error': promo_code_error,
|
||||
}
|
||||
def _cart_find_product_line(self, *args, **kwargs):
|
||||
# Filter out reward lines, they shouldn't be modified by standard _cart_add logic.
|
||||
# This kind of lines is handled by _update_programs_and_rewards and _auto_apply_rewards.
|
||||
return super()._cart_find_product_line(*args, **kwargs).filtered(
|
||||
lambda sol: not sol.is_reward_line
|
||||
)
|
||||
|
||||
def _cart_find_product_line(self, product_id, line_id=None, **kwargs):
|
||||
""" Override to filter out reward lines from the cart lines.
|
||||
|
||||
These are handled by the _update_programs_and_rewards and _auto_apply_rewards methods.
|
||||
"""
|
||||
lines = super()._cart_find_product_line(product_id, line_id, **kwargs)
|
||||
lines = lines.filtered(lambda l: not l.is_reward_line) if not line_id else lines
|
||||
return lines
|
||||
def _recompute_cart(self):
|
||||
"""Recompute cart with loyalty programs and rewards applied."""
|
||||
self._update_programs_and_rewards()
|
||||
self._auto_apply_rewards()
|
||||
super()._recompute_cart()
|
||||
|
|
|
|||
|
|
@ -1,16 +1,26 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from collections import defaultdict
|
||||
|
||||
from odoo import models
|
||||
|
||||
|
||||
class SaleOrderLine(models.Model):
|
||||
_inherit = 'sale.order.line'
|
||||
|
||||
def _get_line_header(self):
|
||||
if self.is_reward_line:
|
||||
return self.name
|
||||
return super()._get_line_header()
|
||||
|
||||
def _show_in_cart(self):
|
||||
# Hide discount lines from website_order_line, see `order._compute_website_order_line`
|
||||
return self.reward_id.reward_type != 'discount' and super()._show_in_cart()
|
||||
|
||||
def _is_reorder_allowed(self):
|
||||
# Hide all types of rewards from reorder
|
||||
return not self.reward_id and super()._is_reorder_allowed()
|
||||
|
||||
def unlink(self):
|
||||
if self.env.context.get('website_sale_loyalty_delete', False):
|
||||
disabled_rewards_per_order = defaultdict(lambda: self.env['loyalty.reward'])
|
||||
|
|
@ -20,3 +30,17 @@ class SaleOrderLine(models.Model):
|
|||
for order, rewards in disabled_rewards_per_order.items():
|
||||
order.disabled_auto_rewards += rewards
|
||||
return super().unlink()
|
||||
|
||||
def _should_show_strikethrough_price(self):
|
||||
""" Override of `website_sale` to hide the strikethrough price for rewards. """
|
||||
return super()._should_show_strikethrough_price() and not self.is_reward_line
|
||||
|
||||
def _is_sellable(self):
|
||||
"""Override of `website_sale` to flag reward lines as not sellable.
|
||||
|
||||
:return: Whether the line is sellable or not.
|
||||
:rtype: bool
|
||||
"""
|
||||
return super()._is_sellable() and (
|
||||
not self.is_reward_line or self.reward_id.reward_type == 'product'
|
||||
)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue