mirror of
https://github.com/bringout/oca-ocb-sale.git
synced 2026-04-27 20:12:04 +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,8 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from . import loyalty_card
|
||||
from . import loyalty_program
|
||||
from . import loyalty_rule
|
||||
from . import sale_order_line
|
||||
from . import sale_order
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
# -*- 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'
|
||||
|
||||
def action_coupon_share(self):
|
||||
self.ensure_one()
|
||||
return self.env['coupon.share'].create_share_action(coupon=self)
|
||||
|
|
@ -0,0 +1,14 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo import fields, models
|
||||
|
||||
class LoyaltyProgram(models.Model):
|
||||
_name = 'loyalty.program'
|
||||
_inherit = ['loyalty.program', 'website.multi.mixin']
|
||||
|
||||
ecommerce_ok = fields.Boolean("Available on Website", default=True)
|
||||
|
||||
def action_program_share(self):
|
||||
self.ensure_one()
|
||||
return self.env['coupon.share'].create_share_action(program=self)
|
||||
|
|
@ -0,0 +1,34 @@
|
|||
# -*- 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')
|
||||
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')
|
||||
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)],
|
||||
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:
|
||||
website_checks = (res['website_id'], False) if res['website_id'] else (False,)
|
||||
for website in website_checks:
|
||||
val = (res['code'], website)
|
||||
if val in existing_codes:
|
||||
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)]):
|
||||
raise ValidationError(_('A coupon with the same code was found.'))
|
||||
|
|
@ -0,0 +1,215 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
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.http import request
|
||||
|
||||
|
||||
class SaleOrder(models.Model):
|
||||
_inherit = "sale.order"
|
||||
|
||||
# List of disabled rewards for automatic claim
|
||||
disabled_auto_rewards = fields.Many2many("loyalty.reward", relation="sale_order_disabled_auto_rewards_rel")
|
||||
|
||||
def _get_program_domain(self):
|
||||
res = super()._get_program_domain()
|
||||
# Replace `sale_ok` leaf with `ecommerce_ok` if order is linked to a website
|
||||
if self.website_id:
|
||||
for idx, leaf in enumerate(res):
|
||||
if leaf[0] != 'sale_ok':
|
||||
continue
|
||||
res[idx] = ('ecommerce_ok', '=', True)
|
||||
return expression.AND([res, [('website_id', 'in', (self.website_id.id, False))]])
|
||||
return res
|
||||
|
||||
def _get_trigger_domain(self):
|
||||
res = super()._get_trigger_domain()
|
||||
# Replace `sale_ok` leaf with `ecommerce_ok` if order is linked to a website
|
||||
if self.website_id:
|
||||
for idx, leaf in enumerate(res):
|
||||
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 res
|
||||
|
||||
def _try_pending_coupon(self):
|
||||
if not request:
|
||||
return False
|
||||
|
||||
pending_coupon_code = request.session.get('pending_coupon_code')
|
||||
if pending_coupon_code:
|
||||
status = self._try_apply_code(pending_coupon_code)
|
||||
if 'error' not in status: # Returns an array if everything went right
|
||||
request.session.pop('pending_coupon_code')
|
||||
if len(status) == 1:
|
||||
coupon, rewards = next(iter(status.items()))
|
||||
if len(rewards) == 1 and not rewards.multi_product:
|
||||
self._apply_program_reward(rewards, coupon)
|
||||
return status
|
||||
return True
|
||||
|
||||
def _update_programs_and_rewards(self):
|
||||
for order in self:
|
||||
order._try_pending_coupon()
|
||||
return super()._update_programs_and_rewards()
|
||||
|
||||
def _auto_apply_rewards(self):
|
||||
"""
|
||||
Tries to auto apply claimable rewards.
|
||||
|
||||
It must answer to the following rules:
|
||||
- Must not be from a nominative program
|
||||
- The reward must be the only reward of the program
|
||||
- The reward may not be a multi product reward
|
||||
|
||||
Returns True if any reward was claimed else False
|
||||
"""
|
||||
self.ensure_one()
|
||||
|
||||
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:
|
||||
continue
|
||||
try:
|
||||
res = self._apply_program_reward(rewards, coupon)
|
||||
if 'error' not in res:
|
||||
claimed_reward_count += 1
|
||||
except UserError:
|
||||
pass
|
||||
|
||||
return bool(claimed_reward_count)
|
||||
|
||||
def _compute_website_order_line(self):
|
||||
""" This method will merge multiple discount lines generated by a same program
|
||||
into a single one (temporary line with `new()`).
|
||||
This case will only occur when the program is a discount applied on multiple
|
||||
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
|
||||
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:
|
||||
line 1: 10% discount on product with tax `A` - $15
|
||||
line 2: 10% discount on product with tax `B` - $11.5
|
||||
line 3: 10% discount on product with tax `C` - $10
|
||||
would be `hidden` and `replaced` by
|
||||
line 1: 10% discount - $36.5
|
||||
|
||||
Note: The line will be created without tax(es) and the amount will be computed
|
||||
depending if B2B or B2C is enabled.
|
||||
"""
|
||||
super()._compute_website_order_line()
|
||||
for order in self:
|
||||
grouped_order_lines = defaultdict(lambda: self.env['sale.order.line'])
|
||||
for line in order.order_line:
|
||||
if line.reward_id and line.coupon_id:
|
||||
grouped_order_lines[(line.reward_id, line.coupon_id, line.reward_identifier_code)] |= line
|
||||
new_lines = self.env['sale.order.line']
|
||||
for lines in grouped_order_lines.values():
|
||||
if lines.reward_id.reward_type != 'discount':
|
||||
continue
|
||||
new_lines += self.env['sale.order.line'].new({
|
||||
'product_id': lines[0].product_id.id,
|
||||
'tax_id': 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,
|
||||
'order_id': order.id,
|
||||
'is_reward_line': True,
|
||||
'coupon_id': lines.coupon_id,
|
||||
'reward_id': lines.reward_id,
|
||||
})
|
||||
if new_lines:
|
||||
order.website_order_line += new_lines
|
||||
|
||||
def _compute_cart_info(self):
|
||||
super(SaleOrder, self)._compute_cart_info()
|
||||
for order in self:
|
||||
reward_lines = order.website_order_line.filtered(lambda line: line.is_reward_line)
|
||||
order.cart_quantity -= int(sum(reward_lines.mapped('product_uom_qty')))
|
||||
|
||||
def get_promo_code_error(self, delete=True):
|
||||
error = request.session.get('error_promo_code')
|
||||
if error and delete:
|
||||
request.session.pop('error_promo_code')
|
||||
return error
|
||||
|
||||
def get_promo_code_success_message(self, delete=True):
|
||||
if not request.session.get('successful_code'):
|
||||
return False
|
||||
code = request.session.get('successful_code')
|
||||
if delete:
|
||||
request.session.pop('successful_code')
|
||||
return code
|
||||
|
||||
def _cart_update(self, product_id, line_id=None, add_qty=0, set_qty=0, **kwargs):
|
||||
|
||||
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
|
||||
|
||||
res = super()._cart_update(
|
||||
product_id, line_id=line_id, add_qty=add_qty, set_qty=set_qty, **kwargs
|
||||
)
|
||||
self._update_programs_and_rewards()
|
||||
self._auto_apply_rewards()
|
||||
return res
|
||||
|
||||
def _get_free_shipping_lines(self):
|
||||
self.ensure_one()
|
||||
return self.order_line.filtered(lambda l: l.reward_id.reward_type == 'shipping')
|
||||
|
||||
def _allow_nominative_programs(self):
|
||||
if not request or not hasattr(request, 'website'):
|
||||
return super()._allow_nominative_programs()
|
||||
return not request.website.is_public_user() and super()._allow_nominative_programs()
|
||||
|
||||
@api.autovacuum
|
||||
def _gc_abandoned_coupons(self, *args, **kwargs):
|
||||
"""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)))
|
||||
so_to_reset = self.env['sale.order'].search([
|
||||
('state', '=', 'draft'),
|
||||
('write_date', '<', validity),
|
||||
('website_id', '!=', False),
|
||||
('applied_coupon_ids', '!=', False),
|
||||
])
|
||||
so_to_reset.applied_coupon_ids = False
|
||||
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)
|
||||
|
||||
return {
|
||||
'promo_code_success': promo_code_success,
|
||||
'promo_code_error': promo_code_error,
|
||||
}
|
||||
|
||||
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
|
||||
|
|
@ -0,0 +1,22 @@
|
|||
# -*- 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 _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 unlink(self):
|
||||
if self.env.context.get('website_sale_loyalty_delete', False):
|
||||
disabled_rewards_per_order = defaultdict(lambda: self.env['loyalty.reward'])
|
||||
for line in self:
|
||||
if line.reward_id:
|
||||
disabled_rewards_per_order[line.order_id] |= line.reward_id
|
||||
for order, rewards in disabled_rewards_per_order.items():
|
||||
order.disabled_auto_rewards += rewards
|
||||
return super().unlink()
|
||||
Loading…
Add table
Add a link
Reference in a new issue