137 lines
6.1 KiB
Python
137 lines
6.1 KiB
Python
# -*- 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
|
||
from odoo.tools import float_compare
|
||
from datetime import datetime, time
|
||
import pytz
|
||
|
||
class PosOrder(models.Model):
|
||
_inherit = 'pos.order'
|
||
|
||
def validate_coupon_programs(self, point_changes, new_codes):
|
||
point_changes_int = {int(k): v for k, v in point_changes.items()}
|
||
subscription_cards = self.env['loyalty.card'].browse(point_changes_int.keys()).exists().filtered(
|
||
lambda c: c.program_id.program_type == 'subscription'
|
||
)
|
||
|
||
# Validate dates and limits for subscription cards
|
||
for card in subscription_cards:
|
||
today = fields.Date.today()
|
||
if card.subscription_start_date and today < card.subscription_start_date:
|
||
return {
|
||
'successful': False,
|
||
'payload': {
|
||
'message': _('The subscription for %s is not active yet (Starts on %s).', card.partner_id.name, card.subscription_start_date),
|
||
}
|
||
}
|
||
if card.subscription_end_date and today > card.subscription_end_date:
|
||
return {
|
||
'successful': False,
|
||
'payload': {
|
||
'message': _('The subscription for %s has expired (Expired on %s).', card.partner_id.name, card.subscription_end_date),
|
||
}
|
||
}
|
||
|
||
# Check usage limit today in user local timezone
|
||
user_tz = pytz.timezone(self.env.user.tz or 'UTC')
|
||
now_utc = fields.Datetime.now()
|
||
now_local = pytz.utc.localize(now_utc).astimezone(user_tz)
|
||
midnight_local = now_local.replace(hour=0, minute=0, second=0, microsecond=0)
|
||
start_date = midnight_local.astimezone(pytz.utc).replace(tzinfo=None)
|
||
|
||
history_count = self.env['loyalty.history'].sudo().search_count([
|
||
('card_id', '=', card.id),
|
||
('create_date', '>=', start_date),
|
||
('used', '>', 0),
|
||
])
|
||
if history_count >= 1:
|
||
return {
|
||
'successful': False,
|
||
'payload': {
|
||
'message': _('Customer %s has already claimed their subscription free product today.', card.partner_id.name),
|
||
}
|
||
}
|
||
|
||
# Bypass the points validation by temporarily mocking subscription cards points to a high value.
|
||
original_points = {card: card.points for card in subscription_cards}
|
||
for card in subscription_cards:
|
||
card.points = 9999.0
|
||
|
||
try:
|
||
res = super().validate_coupon_programs(point_changes, new_codes)
|
||
finally:
|
||
for card, points in original_points.items():
|
||
card.points = points
|
||
|
||
return res
|
||
|
||
def _check_existing_loyalty_cards(self, coupon_data):
|
||
"""Extend core to also match existing subscription cards so they are
|
||
reused (point-updated) rather than triggering new-card creation."""
|
||
# Run the standard matching first (covers 'loyalty' and 'ewallet').
|
||
super()._check_existing_loyalty_cards(coupon_data)
|
||
|
||
# Now handle 'subscription' type programs the same way.
|
||
coupon_key_to_modify = []
|
||
for coupon_id, coupon_vals in coupon_data.items():
|
||
partner_id = coupon_vals.get('partner_id', False)
|
||
if not partner_id:
|
||
continue
|
||
program = self.env['loyalty.program'].browse(coupon_vals['program_id']).exists()
|
||
if not program or program.program_type != 'subscription':
|
||
continue
|
||
existing = self.env['loyalty.card'].search([
|
||
('partner_id', '=', partner_id),
|
||
('program_id', '=', coupon_vals['program_id']),
|
||
], limit=1)
|
||
if existing:
|
||
coupon_vals['coupon_id'] = existing.id
|
||
coupon_key_to_modify.append([coupon_id, existing.id])
|
||
|
||
for old_key, new_key in coupon_key_to_modify:
|
||
coupon_data[new_key] = coupon_data.pop(old_key)
|
||
|
||
def confirm_coupon_programs(self, coupon_data):
|
||
"""Strip any to-be-created (negative-id) coupon_data entries that
|
||
target a subscription program before handing off to core.
|
||
|
||
Background: the POS JS adds a {points: 0} couponPointChange for every
|
||
is_nominative program (applies_on='both') whenever a partner is on the
|
||
order – including Subscription Direksi. Core would then create a fresh
|
||
loyalty.card for that partner. Subscription cards must only be created
|
||
manually, so we intercept and remove these phantom entries here.
|
||
"""
|
||
coupon_data_int = {int(k): v for k, v in coupon_data.items()}
|
||
|
||
# Collect negative (new-card) ids that belong to subscription programs.
|
||
keys_to_remove = []
|
||
for coupon_id, coupon_vals in coupon_data_int.items():
|
||
if coupon_id >= 0:
|
||
# Positive id = existing card; let core handle it normally.
|
||
continue
|
||
program = self.env['loyalty.program'].browse(
|
||
coupon_vals.get('program_id')
|
||
).exists()
|
||
if program and program.program_type == 'subscription':
|
||
keys_to_remove.append(coupon_id)
|
||
|
||
# Remove the ghost entries (work on the original string-key dict).
|
||
for key in keys_to_remove:
|
||
coupon_data.pop(key, None)
|
||
coupon_data.pop(str(key), None)
|
||
|
||
# Run super to process normal workflow
|
||
res = super().confirm_coupon_programs(coupon_data)
|
||
|
||
# After points calculations/deductions are processed, reset the points
|
||
# back to 0.0 for any subscription cards that were actually applied.
|
||
coupon_data_int = {int(k): v for k, v in coupon_data.items()}
|
||
for card_id in coupon_data_int.keys():
|
||
card = self.env['loyalty.card'].browse(card_id).exists()
|
||
if card and card.program_id.program_type == 'subscription':
|
||
card.points = 0.0
|
||
|
||
return res
|