initial commit
This commit is contained in:
commit
ad46c2d8fd
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
@ -0,0 +1 @@
|
|||||||
|
__pycache__
|
||||||
33
README.md
Normal file
33
README.md
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
# POS Loyalty Expiration Custom
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
This module enhances the Odoo 19 Point of Sale (POS) loyalty capabilities by introducing a strict expiration control mechanism. It provides an isolated, custom expiration date field (`custom_end_date`) that overrides default point earning and reward claiming logic in the Point of Sale UI and backend.
|
||||||
|
|
||||||
|
When a Loyalty Card passes this `custom_end_date`, the system securely locks the card out of POS transactions, preventing both the accumulation of new reward points and the claiming of previously earned rewards.
|
||||||
|
|
||||||
|
## Key Features
|
||||||
|
* **Dedicated Custom End Date:** Formally declares and injects the `custom_end_date` directly into the `loyalty.card` model and UI (no Odoo Studio required).
|
||||||
|
* **Earn Point Blocking:** Halts any calculation or recording of points for an order when an expired loyalty card is used.
|
||||||
|
* **Reward Claim Blocking:** Filters and completely hides any claimable rewards associated with an expired loyalty card from the POS frontend interface.
|
||||||
|
* **Strict Backend Validation:** Expands on Odoo's internal coupon code checks to raise descriptive validation errors if an expired code is manually inputted.
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
1. Place the `pos_loyalty_expiration_custom` directory inside your Odoo `addons` or `custom` folder.
|
||||||
|
2. Restart the Odoo service.
|
||||||
|
3. Log in as an Administrator, activate Developer Mode, and click **Update Apps List**.
|
||||||
|
4. Search for `POS Loyalty Expiration Custom` and click **Install**.
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
1. Navigate to **Point of Sale > Products > Loyalty Programs**.
|
||||||
|
2. Select a program and view its related **Loyalty Cards** (or go directly to **Loyalty Cards**).
|
||||||
|
3. On the card form, locate the **End Date** field (placed next to the standard `expiration_date`).
|
||||||
|
4. Set an expiration date.
|
||||||
|
5. In an active POS Session:
|
||||||
|
* Selecting a customer or scanning a code linked to an expired card will yield no points for their purchase.
|
||||||
|
* Selecting the "Rewards" button will not present any options related to the expired card.
|
||||||
|
|
||||||
|
## Technical Notes
|
||||||
|
* Extending Python model `loyalty.card` to inject `custom_end_date` into `_load_pos_data_fields`.
|
||||||
|
* Patching `LoyaltyCard.prototype.isExpired()` in `pos_order.js` to parse and check the custom date constraint.
|
||||||
|
* Patching `Order.prototype.getClaimableRewards` to proactively filter out expired cards.
|
||||||
|
* Patching `PosStore.prototype._updatePrograms` to gracefully ignore and purge any points calculated against expired cards.
|
||||||
1
__init__.py
Normal file
1
__init__.py
Normal file
@ -0,0 +1 @@
|
|||||||
|
from . import models
|
||||||
38
__manifest__.py
Normal file
38
__manifest__.py
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
{
|
||||||
|
'name': 'POS Loyalty Expiration Custom',
|
||||||
|
'version': '1.0',
|
||||||
|
'category': 'Sales/Point of Sale',
|
||||||
|
'summary': 'Enforce custom expiration dates on POS Loyalty Cards',
|
||||||
|
'author': 'Abdul Aziz Amrullah',
|
||||||
|
'description': """
|
||||||
|
POS Loyalty Expiration Custom
|
||||||
|
=============================
|
||||||
|
|
||||||
|
This module extends the core Point of Sale loyalty functionality to enforce a strict expiration logic based on a `custom_end_date` field.
|
||||||
|
|
||||||
|
Key Features:
|
||||||
|
-------------
|
||||||
|
* **Custom End Date Field:** Adds a custom "End Date" (`custom_end_date`) to the Loyalty Card view (backend).
|
||||||
|
* **Strict POS Validation:** When a loyalty card's custom end date has passed, the Point of Sale system will immediately block the card.
|
||||||
|
* **Halt Point Earning:** Expired cards are blocked from accumulating new reward points for any purchases made.
|
||||||
|
* **Block Reward Claims:** Rewards linked to expired loyalty cards are completely hidden and cannot be claimed by the user.
|
||||||
|
|
||||||
|
This ensures seamless and autonomous handling of promotional periods or membership validity strictly through an isolated date field without affecting the core functionality of regular expiration parameters.
|
||||||
|
""",
|
||||||
|
'depends': ['pos_loyalty'],
|
||||||
|
'data': [
|
||||||
|
'views/loyalty_card_views.xml',
|
||||||
|
],
|
||||||
|
'assets': {
|
||||||
|
'point_of_sale._assets_pos': [
|
||||||
|
'pos_loyalty_expiration_custom/static/src/app/models/loyalty_card.js',
|
||||||
|
'pos_loyalty_expiration_custom/static/src/app/models/pos_order.js',
|
||||||
|
'pos_loyalty_expiration_custom/static/src/app/services/pos_store.js',
|
||||||
|
],
|
||||||
|
},
|
||||||
|
'installable': True,
|
||||||
|
'application': False,
|
||||||
|
'auto_install': False,
|
||||||
|
'license': 'LGPL-3',
|
||||||
|
}
|
||||||
2
models/__init__.py
Normal file
2
models/__init__.py
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
from . import loyalty_card
|
||||||
|
from . import pos_config
|
||||||
25
models/loyalty_card.py
Normal file
25
models/loyalty_card.py
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
from odoo import api, fields, models
|
||||||
|
|
||||||
|
class LoyaltyCard(models.Model):
|
||||||
|
_inherit = 'loyalty.card'
|
||||||
|
|
||||||
|
# Declare the field so the ORM is aware of it and it can be loaded in _load_pos_data_fields.
|
||||||
|
custom_end_date = fields.Date(string="End Date", help="Custom End Date for Loyalty Card")
|
||||||
|
|
||||||
|
@api.model
|
||||||
|
def _load_pos_data_fields(self, config):
|
||||||
|
fields_list = super()._load_pos_data_fields(config)
|
||||||
|
if 'custom_end_date' not in fields_list:
|
||||||
|
fields_list.append('custom_end_date')
|
||||||
|
return fields_list
|
||||||
|
|
||||||
|
@api.model
|
||||||
|
def get_gift_card_status(self, gift_code, config_id):
|
||||||
|
res = super().get_gift_card_status(gift_code, config_id)
|
||||||
|
if res.get('status') and res.get('data') and res['data'].get('loyalty.card'):
|
||||||
|
card_data = res['data']['loyalty.card'][0]
|
||||||
|
end_date = card_data.get('custom_end_date')
|
||||||
|
if end_date and fields.Date.from_string(end_date) < fields.Date.today():
|
||||||
|
res['status'] = False
|
||||||
|
return res
|
||||||
22
models/pos_config.py
Normal file
22
models/pos_config.py
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
from odoo import models, fields, _
|
||||||
|
|
||||||
|
class PosConfig(models.Model):
|
||||||
|
_inherit = 'pos.config'
|
||||||
|
|
||||||
|
def use_coupon_code(self, code, creation_date, partner_id, pricelist_id):
|
||||||
|
res = super().use_coupon_code(code, creation_date, partner_id, pricelist_id)
|
||||||
|
if res.get('successful'):
|
||||||
|
# Fetch the coupon to check custom_end_date
|
||||||
|
coupon_id = res['payload'].get('coupon_id')
|
||||||
|
if coupon_id:
|
||||||
|
coupon = self.env['loyalty.card'].browse(coupon_id)
|
||||||
|
check_date = fields.Date.from_string(creation_date[:11])
|
||||||
|
if coupon.custom_end_date and coupon.custom_end_date < check_date:
|
||||||
|
return {
|
||||||
|
'successful': False,
|
||||||
|
'payload': {
|
||||||
|
'error_message': _("This coupon's custom end date is expired (%s).", code),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
return res
|
||||||
21
static/src/app/models/loyalty_card.js
Normal file
21
static/src/app/models/loyalty_card.js
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
/** @odoo-module **/
|
||||||
|
|
||||||
|
import { LoyaltyCard } from "@pos_loyalty/app/models/loyalty_card";
|
||||||
|
import { patch } from "@web/core/utils/patch";
|
||||||
|
|
||||||
|
const { DateTime } = luxon;
|
||||||
|
|
||||||
|
patch(LoyaltyCard.prototype, {
|
||||||
|
isExpired() {
|
||||||
|
const isSuperExpired = super.isExpired(...arguments);
|
||||||
|
if (isSuperExpired) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.custom_end_date) {
|
||||||
|
return DateTime.fromISO(this.custom_end_date).toMillis() < DateTime.now().toMillis();
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
});
|
||||||
14
static/src/app/models/pos_order.js
Normal file
14
static/src/app/models/pos_order.js
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
/** @odoo-module **/
|
||||||
|
|
||||||
|
import { PosOrder } from "@point_of_sale/app/models/pos_order";
|
||||||
|
import { patch } from "@web/core/utils/patch";
|
||||||
|
|
||||||
|
patch(PosOrder.prototype, {
|
||||||
|
getClaimableRewards() {
|
||||||
|
const claimableRewards = super.getClaimableRewards(...arguments);
|
||||||
|
return claimableRewards.filter(cr => {
|
||||||
|
const coupon = this.models['loyalty.card'].get(cr.coupon_id);
|
||||||
|
return coupon ? !coupon.isExpired() : true;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
19
static/src/app/services/pos_store.js
Normal file
19
static/src/app/services/pos_store.js
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
/** @odoo-module **/
|
||||||
|
|
||||||
|
import { PosStore } from "@point_of_sale/app/services/pos_store";
|
||||||
|
import { patch } from "@web/core/utils/patch";
|
||||||
|
|
||||||
|
patch(PosStore.prototype, {
|
||||||
|
async _updatePrograms() {
|
||||||
|
await super._updatePrograms(...arguments);
|
||||||
|
const order = this.getOrder();
|
||||||
|
if (order && order.uiState && order.uiState.couponPointChanges) {
|
||||||
|
for (const [key, pe] of Object.entries(order.uiState.couponPointChanges)) {
|
||||||
|
const coupon = this.models['loyalty.card'].get(pe.coupon_id);
|
||||||
|
if (coupon && coupon.isExpired()) {
|
||||||
|
delete order.uiState.couponPointChanges[key];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
24
views/loyalty_card_views.xml
Normal file
24
views/loyalty_card_views.xml
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<odoo>
|
||||||
|
<record id="loyalty_card_view_form_inherit_custom_end_date" model="ir.ui.view">
|
||||||
|
<field name="name">loyalty.card.view.form.inherit.custom.end.date</field>
|
||||||
|
<field name="model">loyalty.card</field>
|
||||||
|
<field name="inherit_id" ref="loyalty.loyalty_card_view_form"/>
|
||||||
|
<field name="arch" type="xml">
|
||||||
|
<xpath expr="//field[@name='expiration_date']" position="after">
|
||||||
|
<field name="custom_end_date"/>
|
||||||
|
</xpath>
|
||||||
|
</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<record id="loyalty_card_view_tree_inherit_custom_end_date" model="ir.ui.view">
|
||||||
|
<field name="name">loyalty.card.view.list.inherit.custom.end.date</field>
|
||||||
|
<field name="model">loyalty.card</field>
|
||||||
|
<field name="inherit_id" ref="loyalty.loyalty_card_view_tree"/>
|
||||||
|
<field name="arch" type="xml">
|
||||||
|
<xpath expr="//field[@name='expiration_date']" position="after">
|
||||||
|
<field name="custom_end_date" optional="show"/>
|
||||||
|
</xpath>
|
||||||
|
</field>
|
||||||
|
</record>
|
||||||
|
</odoo>
|
||||||
Loading…
Reference in New Issue
Block a user