first commit
This commit is contained in:
commit
075d5055a7
5
.gitignore
vendored
Normal file
5
.gitignore
vendored
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
*.pyc
|
||||||
|
*.pyo
|
||||||
|
__pycache__/
|
||||||
|
*.swp
|
||||||
|
.DS_Store
|
||||||
20
README.md
Normal file
20
README.md
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
# POS Loyalty Subscription
|
||||||
|
|
||||||
|
This module introduces a new "Subscription" loyalty program type in Odoo.
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
- New "Subscription" loyalty program type.
|
||||||
|
- Support for subscription start and end dates on loyalty cards.
|
||||||
|
- Enforces a limit of one claim per day for subscription rewards.
|
||||||
|
- Automatic integration with POS loyalty.
|
||||||
|
|
||||||
|
## Configuration
|
||||||
|
|
||||||
|
1. Install the module.
|
||||||
|
2. In the Odoo backend, go to Point of Sale -> Products -> Loyalty Programs.
|
||||||
|
3. Create a new loyalty program and select "Subscription" as the program type.
|
||||||
|
4. Set up the reward (for example, a free product).
|
||||||
|
5. Go to Loyalty Cards and create a card for your customer.
|
||||||
|
6. Set the "Subscription Start Date" and "Subscription End Date" on the customer's card.
|
||||||
|
7. Open the POS, select the customer, and add the product. The reward will be automatically applied for free.
|
||||||
4
__init__.py
Normal file
4
__init__.py
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||||
|
|
||||||
|
from . import models
|
||||||
27
__manifest__.py
Normal file
27
__manifest__.py
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
{
|
||||||
|
'name': 'POS Loyalty Subscription',
|
||||||
|
'version': '1.0',
|
||||||
|
'category': 'Sales/Point of Sale',
|
||||||
|
'summary': 'Allows subscription-based loyalty programs (e.g. daily claim limits) with validity dates.',
|
||||||
|
'author': 'Suherdy Yacob',
|
||||||
|
'description': """
|
||||||
|
POS Loyalty Subscription
|
||||||
|
========================
|
||||||
|
Introduces a new "Subscription" loyalty program type in Odoo.
|
||||||
|
Allows customers to join subscription programs where they can claim a free product
|
||||||
|
once per day within a specified subscription start and end date.
|
||||||
|
""",
|
||||||
|
'depends': ['point_of_sale', 'pos_loyalty', 'pos_loyalty_multi_level'],
|
||||||
|
'data': [
|
||||||
|
'views/loyalty_card_views.xml',
|
||||||
|
],
|
||||||
|
'assets': {
|
||||||
|
'point_of_sale._assets_pos': [
|
||||||
|
'pos_loyalty_subscription/static/src/app/**/*',
|
||||||
|
],
|
||||||
|
},
|
||||||
|
'installable': True,
|
||||||
|
'application': False,
|
||||||
|
'license': 'LGPL-3',
|
||||||
|
}
|
||||||
6
models/__init__.py
Normal file
6
models/__init__.py
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||||
|
|
||||||
|
from . import loyalty_program
|
||||||
|
from . import loyalty_card
|
||||||
|
from . import pos_order
|
||||||
37
models/loyalty_card.py
Normal file
37
models/loyalty_card.py
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||||
|
|
||||||
|
import pytz
|
||||||
|
from odoo import api, fields, models
|
||||||
|
|
||||||
|
class LoyaltyCard(models.Model):
|
||||||
|
_inherit = 'loyalty.card'
|
||||||
|
|
||||||
|
subscription_start_date = fields.Date(string="Subscription Start Date")
|
||||||
|
subscription_end_date = fields.Date(string="Subscription End Date")
|
||||||
|
subscription_usage_count = fields.Integer(compute='_compute_subscription_usage_count', string="Subscription Usage Today")
|
||||||
|
|
||||||
|
def _compute_subscription_usage_count(self):
|
||||||
|
for card in self:
|
||||||
|
if card.program_id.program_type != 'subscription':
|
||||||
|
card.subscription_usage_count = 0
|
||||||
|
continue
|
||||||
|
|
||||||
|
domain = [('card_id', '=', card.id), ('used', '>', 0)]
|
||||||
|
|
||||||
|
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)
|
||||||
|
domain.append(('create_date', '>=', start_date))
|
||||||
|
|
||||||
|
# Use sudo() to bypass potential multi-company record access rules on loyalty.history
|
||||||
|
card.subscription_usage_count = self.env['loyalty.history'].sudo().search_count(domain)
|
||||||
|
|
||||||
|
@api.model
|
||||||
|
def _load_pos_data_fields(self, config):
|
||||||
|
fields_list = super()._load_pos_data_fields(config)
|
||||||
|
fields_list.extend(['subscription_start_date', 'subscription_end_date', 'subscription_usage_count'])
|
||||||
|
return fields_list
|
||||||
51
models/loyalty_program.py
Normal file
51
models/loyalty_program.py
Normal file
@ -0,0 +1,51 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||||
|
|
||||||
|
from odoo import api, fields, models, _
|
||||||
|
|
||||||
|
class LoyaltyProgram(models.Model):
|
||||||
|
_inherit = 'loyalty.program'
|
||||||
|
|
||||||
|
program_type = fields.Selection(
|
||||||
|
selection_add=[('subscription', 'Subscription')],
|
||||||
|
ondelete={'subscription': 'set default'}
|
||||||
|
)
|
||||||
|
|
||||||
|
@api.model
|
||||||
|
def _program_items_name(self):
|
||||||
|
res = super()._program_items_name()
|
||||||
|
res['subscription'] = _("Subscriptions")
|
||||||
|
return res
|
||||||
|
|
||||||
|
@api.model
|
||||||
|
def _program_type_default_values(self):
|
||||||
|
res = super()._program_type_default_values()
|
||||||
|
res['subscription'] = {
|
||||||
|
'applies_on': 'both',
|
||||||
|
'trigger': 'auto',
|
||||||
|
'portal_visible': True,
|
||||||
|
'portal_point_name': _("Subscription claim(s)"),
|
||||||
|
'manual_membership': True,
|
||||||
|
'rule_ids': [(5, 0, 0)],
|
||||||
|
'reward_ids': [(5, 0, 0)],
|
||||||
|
'communication_plan_ids': [(5, 0, 0)],
|
||||||
|
}
|
||||||
|
return res
|
||||||
|
|
||||||
|
@api.model_create_multi
|
||||||
|
def create(self, vals_list):
|
||||||
|
for vals in vals_list:
|
||||||
|
if vals.get('program_type') == 'subscription':
|
||||||
|
vals['manual_membership'] = True
|
||||||
|
vals['trigger'] = 'auto'
|
||||||
|
vals['applies_on'] = 'both'
|
||||||
|
vals['portal_visible'] = True
|
||||||
|
return super().create(vals_list)
|
||||||
|
|
||||||
|
def write(self, vals):
|
||||||
|
if vals.get('program_type') == 'subscription' or (not vals.get('program_type') and any(p.program_type == 'subscription' for p in self)):
|
||||||
|
vals['manual_membership'] = True
|
||||||
|
vals['trigger'] = 'auto'
|
||||||
|
vals['applies_on'] = 'both'
|
||||||
|
vals['portal_visible'] = True
|
||||||
|
return super().write(vals)
|
||||||
81
models/pos_order.py
Normal file
81
models/pos_order.py
Normal file
@ -0,0 +1,81 @@
|
|||||||
|
# -*- 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 confirm_coupon_programs(self, coupon_data):
|
||||||
|
# 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 subscription cards
|
||||||
|
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
|
||||||
111
static/src/app/models/pos_order.js
Normal file
111
static/src/app/models/pos_order.js
Normal file
@ -0,0 +1,111 @@
|
|||||||
|
/** @odoo-module **/
|
||||||
|
|
||||||
|
import { PosOrder } from "@point_of_sale/app/models/pos_order";
|
||||||
|
import { patch } from "@web/core/utils/patch";
|
||||||
|
|
||||||
|
const { DateTime } = luxon;
|
||||||
|
|
||||||
|
function resolveManyToOneId(value) {
|
||||||
|
if (!value && value !== 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
if (Array.isArray(value)) {
|
||||||
|
return parseInt(value[0], 10);
|
||||||
|
}
|
||||||
|
if (typeof value === 'object') {
|
||||||
|
return parseInt(value.id, 10);
|
||||||
|
}
|
||||||
|
return parseInt(value, 10);
|
||||||
|
}
|
||||||
|
|
||||||
|
patch(PosOrder.prototype, {
|
||||||
|
_programIsApplicable(program) {
|
||||||
|
const isApplicable = super._programIsApplicable(...arguments);
|
||||||
|
if (!isApplicable) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (program.program_type === 'subscription') {
|
||||||
|
const partner = this.getPartner();
|
||||||
|
if (!partner) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const allCards = this.models['loyalty.card']?.getAll() || [];
|
||||||
|
const card = allCards.find((c) => {
|
||||||
|
const cardPartnerId = resolveManyToOneId(c.partner_id);
|
||||||
|
const cardProgramId = resolveManyToOneId(c.program_id);
|
||||||
|
return cardPartnerId === partner.id && cardProgramId === program.id;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!card || !card.active) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const today = DateTime.now().startOf('day');
|
||||||
|
if (card.subscription_start_date) {
|
||||||
|
const startDate = DateTime.fromISO(card.subscription_start_date).startOf('day');
|
||||||
|
if (today < startDate) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (card.subscription_end_date) {
|
||||||
|
const endDate = DateTime.fromISO(card.subscription_end_date).startOf('day');
|
||||||
|
if (today > endDate) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return isApplicable;
|
||||||
|
},
|
||||||
|
|
||||||
|
_getRealCouponPoints(coupon_id) {
|
||||||
|
const dbCoupon = this.models['loyalty.card'].get(coupon_id);
|
||||||
|
const programId = dbCoupon ? resolveManyToOneId(dbCoupon.program_id) : null;
|
||||||
|
const program = programId ? this.models['loyalty.program'].get(programId) : null;
|
||||||
|
|
||||||
|
if (program && program.program_type === 'subscription') {
|
||||||
|
// Subscription program rewards cost points, but they are free for the customer.
|
||||||
|
// We return enough points to cover the reward cost, limited by 1 claim per day.
|
||||||
|
const rewardPoints = (program.reward_ids || []).reduce((max, r) => Math.max(max, r.required_points || 0), 1);
|
||||||
|
|
||||||
|
let todayClaims = 0;
|
||||||
|
|
||||||
|
// 1. Count claims in the current order
|
||||||
|
for (const line of this.getOrderlines()) {
|
||||||
|
if (line.is_reward_line && line.coupon_id?.id === coupon_id) {
|
||||||
|
todayClaims++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Count claims in other orders in this POS session (that are paid or finalized)
|
||||||
|
const orders = this.models['pos.order'].getAll();
|
||||||
|
for (const order of orders) {
|
||||||
|
if (order === this) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (!order.is_paid && order.state !== 'paid' && order.state !== 'done' && order.state !== 'invoiced' && !order.finalized) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
for (const line of order._get_reward_lines() || []) {
|
||||||
|
if (line.coupon_id && line.coupon_id.id === coupon_id) {
|
||||||
|
todayClaims++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. Count claims already recorded in the backend today
|
||||||
|
const backendClaims = dbCoupon.subscription_usage_count || 0;
|
||||||
|
const totalClaimsToday = backendClaims + todayClaims;
|
||||||
|
|
||||||
|
if (totalClaimsToday >= 1) {
|
||||||
|
return 0; // Exceeded limit of 1 claim per day
|
||||||
|
} else {
|
||||||
|
return rewardPoints; // Enough points to claim
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return super._getRealCouponPoints(...arguments);
|
||||||
|
}
|
||||||
|
});
|
||||||
4
tests/__init__.py
Normal file
4
tests/__init__.py
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||||
|
|
||||||
|
from . import test_subscription
|
||||||
118
tests/test_subscription.py
Normal file
118
tests/test_subscription.py
Normal file
@ -0,0 +1,118 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
from odoo.tests.common import TransactionCase
|
||||||
|
from odoo.fields import Date, Datetime
|
||||||
|
import pytz
|
||||||
|
|
||||||
|
class TestPOSLoyaltySubscription(TransactionCase):
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
super().setUp()
|
||||||
|
|
||||||
|
# Create a product for the reward
|
||||||
|
self.reward_product = self.env['product.product'].create({
|
||||||
|
'name': 'Test Free Coffee',
|
||||||
|
'type': 'consu',
|
||||||
|
})
|
||||||
|
|
||||||
|
# Create a subscription program
|
||||||
|
self.program = self.env['loyalty.program'].create({
|
||||||
|
'name': 'Makan Pagi Gratis',
|
||||||
|
'program_type': 'subscription',
|
||||||
|
})
|
||||||
|
|
||||||
|
# Create a reward for the program
|
||||||
|
self.reward = self.env['loyalty.reward'].create({
|
||||||
|
'program_id': self.program.id,
|
||||||
|
'reward_type': 'product',
|
||||||
|
'required_points': 1,
|
||||||
|
'reward_product_id': self.reward_product.id,
|
||||||
|
})
|
||||||
|
|
||||||
|
# Create a partner
|
||||||
|
self.partner = self.env['res.partner'].create({
|
||||||
|
'name': 'John Doe',
|
||||||
|
})
|
||||||
|
|
||||||
|
# Create a subscription card
|
||||||
|
self.card = self.env['loyalty.card'].create({
|
||||||
|
'program_id': self.program.id,
|
||||||
|
'partner_id': self.partner.id,
|
||||||
|
'subscription_start_date': Date.today(),
|
||||||
|
'subscription_end_date': Date.today(),
|
||||||
|
})
|
||||||
|
|
||||||
|
# Find or create a POS config and session
|
||||||
|
pos_config = self.env['pos.config'].search([], limit=1)
|
||||||
|
if not pos_config:
|
||||||
|
pos_config = self.env['pos.config'].create({
|
||||||
|
'name': 'Test POS Config',
|
||||||
|
})
|
||||||
|
self.pos_session = self.env['pos.session'].create({
|
||||||
|
'config_id': pos_config.id,
|
||||||
|
'user_id': self.env.uid,
|
||||||
|
})
|
||||||
|
|
||||||
|
def test_program_default_values(self):
|
||||||
|
"""Verify that default values for subscription program are correctly applied"""
|
||||||
|
self.assertTrue(self.program.manual_membership, "Subscription program should default to manual membership")
|
||||||
|
self.assertEqual(self.program.trigger, 'auto', "Subscription program should trigger automatically")
|
||||||
|
|
||||||
|
def test_validation_date_ranges(self):
|
||||||
|
"""Verify date validity checks in validate_coupon_programs"""
|
||||||
|
pos_order = self.env['pos.order'].create({
|
||||||
|
'name': 'Test POS Order',
|
||||||
|
'partner_id': self.partner.id,
|
||||||
|
'session_id': self.pos_session.id,
|
||||||
|
'amount_tax': 0.0,
|
||||||
|
'amount_total': 0.0,
|
||||||
|
'amount_paid': 0.0,
|
||||||
|
'amount_return': 0.0,
|
||||||
|
})
|
||||||
|
|
||||||
|
# 1. Valid case (today is within range)
|
||||||
|
point_changes = {str(self.card.id): -1}
|
||||||
|
res = pos_order.validate_coupon_programs(point_changes, [])
|
||||||
|
self.assertTrue(res.get('successful', False), "Validation should succeed within valid date ranges")
|
||||||
|
|
||||||
|
# 2. Start date in future
|
||||||
|
self.card.subscription_start_date = Date.add(Date.today(), days=1)
|
||||||
|
res = pos_order.validate_coupon_programs(point_changes, [])
|
||||||
|
self.assertFalse(res.get('successful', True), "Validation should fail if subscription starts in future")
|
||||||
|
|
||||||
|
# Reset start date
|
||||||
|
self.card.subscription_start_date = Date.today()
|
||||||
|
|
||||||
|
# 3. End date in past
|
||||||
|
self.card.subscription_end_date = Date.subtract(Date.today(), days=1)
|
||||||
|
res = pos_order.validate_coupon_programs(point_changes, [])
|
||||||
|
self.assertFalse(res.get('successful', True), "Validation should fail if subscription has expired")
|
||||||
|
|
||||||
|
def test_validation_daily_limit(self):
|
||||||
|
"""Verify that daily claim limit is enforced"""
|
||||||
|
pos_order = self.env['pos.order'].create({
|
||||||
|
'name': 'Test POS Order',
|
||||||
|
'partner_id': self.partner.id,
|
||||||
|
'session_id': self.pos_session.id,
|
||||||
|
'amount_tax': 0.0,
|
||||||
|
'amount_total': 0.0,
|
||||||
|
'amount_paid': 0.0,
|
||||||
|
'amount_return': 0.0,
|
||||||
|
})
|
||||||
|
|
||||||
|
point_changes = {str(self.card.id): -1}
|
||||||
|
|
||||||
|
# Succeeds initially
|
||||||
|
res = pos_order.validate_coupon_programs(point_changes, [])
|
||||||
|
self.assertTrue(res.get('successful', False))
|
||||||
|
|
||||||
|
# Create history entry today
|
||||||
|
self.env['loyalty.history'].create({
|
||||||
|
'card_id': self.card.id,
|
||||||
|
'used': 1,
|
||||||
|
'description': 'Test Claim',
|
||||||
|
'create_date': Datetime.now(),
|
||||||
|
})
|
||||||
|
|
||||||
|
# Should fail now
|
||||||
|
res = pos_order.validate_coupon_programs(point_changes, [])
|
||||||
|
self.assertFalse(res.get('successful', True), "Validation should fail if already claimed today")
|
||||||
16
views/loyalty_card_views.xml
Normal file
16
views/loyalty_card_views.xml
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<odoo>
|
||||||
|
<record id="loyalty_card_view_form_inherit_subscription" model="ir.ui.view">
|
||||||
|
<field name="name">loyalty.card.view.form.inherit.subscription</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="program_type" invisible="1"/>
|
||||||
|
<field name="subscription_start_date" invisible="program_type != 'subscription'"/>
|
||||||
|
<field name="subscription_end_date" invisible="program_type != 'subscription'"/>
|
||||||
|
<field name="subscription_usage_count" invisible="program_type != 'subscription'" readonly="1"/>
|
||||||
|
</xpath>
|
||||||
|
</field>
|
||||||
|
</record>
|
||||||
|
</odoo>
|
||||||
Loading…
Reference in New Issue
Block a user