# -*- 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