feat: implement CMS modules for carousel, promos, and app config, and enable HTML support for push notifications.
This commit is contained in:
parent
b38484a343
commit
2e189dbe6a
@ -1,14 +1,19 @@
|
|||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
{
|
{
|
||||||
'name': "Mapan Loyalty Push Notifications",
|
'name': "Mapan Loyalty Push Notifications",
|
||||||
'summary': "Send push notifications to the Loyalty Flutter App",
|
'summary': "Mobile App CMS: Push notifications, carousel, promos and app configuration",
|
||||||
'description': """
|
'description': """
|
||||||
Integrates Odoo with Firebase Cloud Messaging (FCM) to send push notifications directly
|
Content Management System for the Mie Mapan Loyalty Flutter App.
|
||||||
to customers' Android devices. Maps FCM tokens to res_partner records.
|
Features:
|
||||||
|
- Send push notifications to loyalty app users
|
||||||
|
- Manage carousel banner slides (home screen)
|
||||||
|
- Manage promo highlight cards (home screen, with rich text detail)
|
||||||
|
- Configure App Settings (About Us URL, Contact Us URL)
|
||||||
|
- API endpoints for Flutter app: notifications, CMS content, order history, branches
|
||||||
""",
|
""",
|
||||||
'author': "Suherdy Yacob",
|
'author': "Suherdy Yacob",
|
||||||
'category': 'Marketing',
|
'category': 'Marketing',
|
||||||
'version': '1.0',
|
'version': '1.1',
|
||||||
'depends': ['base', 'loyalty', 'pos_loyalty', 'sale_loyalty'],
|
'depends': ['base', 'loyalty', 'pos_loyalty', 'sale_loyalty'],
|
||||||
'data': [
|
'data': [
|
||||||
'security/mapan_loyalty_push_security.xml',
|
'security/mapan_loyalty_push_security.xml',
|
||||||
@ -16,6 +21,9 @@
|
|||||||
'wizard/push_wizard_views.xml',
|
'wizard/push_wizard_views.xml',
|
||||||
'views/res_partner_views.xml',
|
'views/res_partner_views.xml',
|
||||||
'views/app_notification_views.xml',
|
'views/app_notification_views.xml',
|
||||||
|
'views/app_carousel_views.xml',
|
||||||
|
'views/app_promo_views.xml',
|
||||||
|
'views/app_cms_config_views.xml',
|
||||||
],
|
],
|
||||||
'installable': True,
|
'installable': True,
|
||||||
'application': False,
|
'application': False,
|
||||||
|
|||||||
@ -2,6 +2,7 @@
|
|||||||
from odoo import http
|
from odoo import http
|
||||||
from odoo.http import request
|
from odoo.http import request
|
||||||
|
|
||||||
|
|
||||||
def normalize_phone_search(phone):
|
def normalize_phone_search(phone):
|
||||||
if not phone:
|
if not phone:
|
||||||
return []
|
return []
|
||||||
@ -20,6 +21,7 @@ def normalize_phone_search(phone):
|
|||||||
candidates.append('62' + digits)
|
candidates.append('62' + digits)
|
||||||
return list(set(candidates))
|
return list(set(candidates))
|
||||||
|
|
||||||
|
|
||||||
class AppNotificationController(http.Controller):
|
class AppNotificationController(http.Controller):
|
||||||
|
|
||||||
@http.route('/api/loyalty/fetch_notifications', type='jsonrpc', auth='user', methods=['POST'], csrf=False)
|
@http.route('/api/loyalty/fetch_notifications', type='jsonrpc', auth='user', methods=['POST'], csrf=False)
|
||||||
@ -28,6 +30,7 @@ class AppNotificationController(http.Controller):
|
|||||||
Endpoint for the Flutter app Background Task and In-App notification center.
|
Endpoint for the Flutter app Background Task and In-App notification center.
|
||||||
Returns image_128 (base64 thumbnail) for list display.
|
Returns image_128 (base64 thumbnail) for list display.
|
||||||
The Flutter detail screen loads the full image via /web/image/ with session cookie.
|
The Flutter detail screen loads the full image via /web/image/ with session cookie.
|
||||||
|
Body is now HTML from the rich text editor — returned as-is.
|
||||||
"""
|
"""
|
||||||
import base64 as b64mod
|
import base64 as b64mod
|
||||||
user = request.env.user
|
user = request.env.user
|
||||||
@ -58,21 +61,144 @@ class AppNotificationController(http.Controller):
|
|||||||
elif not img:
|
elif not img:
|
||||||
notif['image_128'] = None
|
notif['image_128'] = None
|
||||||
notif['has_image'] = bool(notif['image_128'])
|
notif['has_image'] = bool(notif['image_128'])
|
||||||
|
# body is HTML string — pass through as-is
|
||||||
|
if notif.get('body') is False:
|
||||||
|
notif['body'] = ''
|
||||||
|
|
||||||
return {
|
return {
|
||||||
'status': 'success',
|
'status': 'success',
|
||||||
'data': notifications
|
'data': notifications
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@http.route('/api/loyalty/cms_content', type='jsonrpc', auth='public', methods=['POST'], csrf=False)
|
||||||
|
def fetch_cms_content(self, **kw):
|
||||||
|
"""
|
||||||
|
Public endpoint to fetch carousel slides and promo highlights for the Flutter app home screen.
|
||||||
|
"""
|
||||||
|
import base64 as b64mod
|
||||||
|
from datetime import date
|
||||||
|
|
||||||
|
today = date.today()
|
||||||
|
|
||||||
|
# --- Carousel Slides ---
|
||||||
|
carousel_domain = [('is_active', '=', True)]
|
||||||
|
carousel_items = request.env['mapan.app.carousel'].sudo().search_read(
|
||||||
|
carousel_domain,
|
||||||
|
['id', 'name', 'image', 'image_url', 'link_url', 'sequence'],
|
||||||
|
order='sequence, id'
|
||||||
|
)
|
||||||
|
for item in carousel_items:
|
||||||
|
img = item.get('image')
|
||||||
|
if img and isinstance(img, bytes):
|
||||||
|
item['image'] = img.decode('utf-8')
|
||||||
|
elif not img:
|
||||||
|
item['image'] = None
|
||||||
|
|
||||||
|
# --- Promo Highlights ---
|
||||||
|
promo_domain = [('is_active', '=', True)]
|
||||||
|
promos = request.env['mapan.app.promo'].sudo().search_read(
|
||||||
|
promo_domain,
|
||||||
|
['id', 'name', 'body', 'image_128', 'date_start', 'date_end', 'sequence'],
|
||||||
|
order='sequence, id'
|
||||||
|
)
|
||||||
|
active_promos = []
|
||||||
|
for promo in promos:
|
||||||
|
# Filter by validity date client-side after fetch
|
||||||
|
ds = promo.get('date_start')
|
||||||
|
de = promo.get('date_end')
|
||||||
|
if ds and ds > today:
|
||||||
|
continue
|
||||||
|
if de and de < today:
|
||||||
|
continue
|
||||||
|
img = promo.get('image_128')
|
||||||
|
if img and isinstance(img, bytes):
|
||||||
|
promo['image_128'] = img.decode('utf-8')
|
||||||
|
elif not img:
|
||||||
|
promo['image_128'] = None
|
||||||
|
# Convert date objects to string
|
||||||
|
promo['date_start'] = str(promo['date_start']) if promo.get('date_start') else None
|
||||||
|
promo['date_end'] = str(promo['date_end']) if promo.get('date_end') else None
|
||||||
|
if promo.get('body') is False:
|
||||||
|
promo['body'] = ''
|
||||||
|
active_promos.append(promo)
|
||||||
|
|
||||||
|
return {
|
||||||
|
'status': 'success',
|
||||||
|
'carousel': carousel_items,
|
||||||
|
'promos': active_promos,
|
||||||
|
}
|
||||||
|
|
||||||
|
@http.route('/api/loyalty/app_config', type='jsonrpc', auth='public', methods=['POST'], csrf=False)
|
||||||
|
def fetch_app_config(self, **kw):
|
||||||
|
"""
|
||||||
|
Public endpoint to fetch app configuration (About Us URL, Contact Us URL).
|
||||||
|
"""
|
||||||
|
config = request.env['mapan.app.config'].sudo().search([], limit=1)
|
||||||
|
if not config:
|
||||||
|
config = request.env['mapan.app.config'].sudo().create({'name': 'App Configuration'})
|
||||||
|
return {
|
||||||
|
'status': 'success',
|
||||||
|
'about_us_url': config.about_us_url or '',
|
||||||
|
'contact_us_url': config.contact_us_url or '',
|
||||||
|
}
|
||||||
|
|
||||||
|
@http.route('/api/loyalty/order_history', type='jsonrpc', auth='user', methods=['POST'], csrf=False)
|
||||||
|
def fetch_order_history(self, **kw):
|
||||||
|
"""
|
||||||
|
Authenticated endpoint to fetch loyalty point history for the current user.
|
||||||
|
Queries loyalty.history which tracks earn/spend events.
|
||||||
|
Positive points = earned, negative points = spent.
|
||||||
|
"""
|
||||||
|
user = request.env.user
|
||||||
|
partner = user.partner_id
|
||||||
|
|
||||||
|
# Get all loyalty cards for this partner
|
||||||
|
cards = request.env['loyalty.card'].sudo().search([('partner_id', '=', partner.id)])
|
||||||
|
card_ids = cards.ids
|
||||||
|
|
||||||
|
if not card_ids:
|
||||||
|
return {'status': 'success', 'data': []}
|
||||||
|
|
||||||
|
history_records = request.env['loyalty.history'].sudo().search_read(
|
||||||
|
[('card_id', 'in', card_ids)],
|
||||||
|
['id', 'card_id', 'points', 'date', 'order_id'],
|
||||||
|
order='date desc',
|
||||||
|
limit=100
|
||||||
|
)
|
||||||
|
|
||||||
|
result = []
|
||||||
|
for rec in history_records:
|
||||||
|
points = rec.get('points') or 0
|
||||||
|
point_type = 'earn' if points >= 0 else 'spend'
|
||||||
|
|
||||||
|
order_ref = ''
|
||||||
|
order_id = rec.get('order_id')
|
||||||
|
if order_id and isinstance(order_id, (list, tuple)) and len(order_id) > 1:
|
||||||
|
order_ref = str(order_id[1])
|
||||||
|
elif order_id:
|
||||||
|
order_ref = str(order_id)
|
||||||
|
|
||||||
|
result.append({
|
||||||
|
'id': rec['id'],
|
||||||
|
'date': str(rec.get('date') or ''),
|
||||||
|
'points': round(float(points), 2),
|
||||||
|
'type': point_type,
|
||||||
|
'order_ref': order_ref,
|
||||||
|
'card_id': rec['card_id'][0] if isinstance(rec.get('card_id'), (list, tuple)) else rec.get('card_id'),
|
||||||
|
})
|
||||||
|
|
||||||
|
return {'status': 'success', 'data': result}
|
||||||
|
|
||||||
@http.route('/api/loyalty/branches', type='jsonrpc', auth='public', methods=['POST'], csrf=False)
|
@http.route('/api/loyalty/branches', type='jsonrpc', auth='public', methods=['POST'], csrf=False)
|
||||||
def fetch_branches(self, **kw):
|
def fetch_branches(self, **kw):
|
||||||
"""
|
"""
|
||||||
Public endpoint for the Flutter app to get branches without exposing API keys.
|
Public endpoint for the Flutter app to get branches without exposing API keys.
|
||||||
|
Includes latitude/longitude for geolocation-based distance sorting on the client.
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
branches = request.env['res.company'].sudo().search_read(
|
branches = request.env['res.company'].sudo().search_read(
|
||||||
[('parent_id', '!=', False)],
|
[('parent_id', '!=', False)],
|
||||||
['name', 'street', 'city', 'phone'],
|
['name', 'street', 'city', 'phone', 'partner_latitude', 'partner_longitude'],
|
||||||
limit=50
|
limit=50
|
||||||
)
|
)
|
||||||
return {'status': 'success', 'data': branches}
|
return {'status': 'success', 'data': branches}
|
||||||
|
|||||||
@ -1,7 +1,8 @@
|
|||||||
from . import res_partner
|
from . import res_partner
|
||||||
from . import app_notification
|
from . import app_notification
|
||||||
|
from . import app_carousel
|
||||||
|
from . import app_promo
|
||||||
|
from . import app_cms_config
|
||||||
from . import res_users
|
from . import res_users
|
||||||
from . import loyalty_card
|
from . import loyalty_card
|
||||||
from . import loyalty_verification_otp
|
from . import loyalty_verification_otp
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
32
models/app_carousel.py
Normal file
32
models/app_carousel.py
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
from odoo import models, fields
|
||||||
|
|
||||||
|
|
||||||
|
class AppCarousel(models.Model):
|
||||||
|
"""Carousel slides managed from the Odoo backend, displayed on the Flutter app home screen."""
|
||||||
|
_name = 'mapan.app.carousel'
|
||||||
|
_description = 'Mobile App Carousel Slide'
|
||||||
|
_order = 'sequence, id'
|
||||||
|
|
||||||
|
name = fields.Char(string='Slide Title', required=True)
|
||||||
|
sequence = fields.Integer(string='Sequence', default=10)
|
||||||
|
is_active = fields.Boolean(string='Active', default=True)
|
||||||
|
|
||||||
|
# Image: uploaded from Odoo backend
|
||||||
|
image = fields.Image(
|
||||||
|
string='Banner Image',
|
||||||
|
max_width=1920,
|
||||||
|
max_height=720,
|
||||||
|
help='Banner image for the carousel slide (recommended ratio 16:6).'
|
||||||
|
)
|
||||||
|
|
||||||
|
# External image URL (alternative to uploaded image)
|
||||||
|
image_url = fields.Char(
|
||||||
|
string='External Image URL',
|
||||||
|
help='If set and no image is uploaded, the app will load the image from this URL.'
|
||||||
|
)
|
||||||
|
|
||||||
|
link_url = fields.Char(
|
||||||
|
string='Tap URL',
|
||||||
|
help='URL to open when the user taps this carousel slide. Leave empty for no action.'
|
||||||
|
)
|
||||||
27
models/app_cms_config.py
Normal file
27
models/app_cms_config.py
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
from odoo import models, fields, api
|
||||||
|
|
||||||
|
|
||||||
|
class AppCmsConfig(models.Model):
|
||||||
|
"""Singleton model for Mobile App global configuration."""
|
||||||
|
_name = 'mapan.app.config'
|
||||||
|
_description = 'Mobile App Configuration'
|
||||||
|
|
||||||
|
name = fields.Char(default='App Configuration', readonly=True)
|
||||||
|
|
||||||
|
about_us_url = fields.Char(
|
||||||
|
string='About Us URL',
|
||||||
|
help='URL to open when user taps "About Us" in the app account menu.'
|
||||||
|
)
|
||||||
|
contact_us_url = fields.Char(
|
||||||
|
string='Contact Us URL',
|
||||||
|
help='URL to open when user taps "Contact Us" in the app account menu.'
|
||||||
|
)
|
||||||
|
|
||||||
|
@api.model
|
||||||
|
def get_config(self):
|
||||||
|
"""Always return the single config record, creating it if it does not exist."""
|
||||||
|
config = self.search([], limit=1)
|
||||||
|
if not config:
|
||||||
|
config = self.create({'name': 'App Configuration'})
|
||||||
|
return config
|
||||||
@ -6,7 +6,7 @@ class AppNotification(models.Model):
|
|||||||
_order = 'create_date desc'
|
_order = 'create_date desc'
|
||||||
|
|
||||||
title = fields.Char(string='Notification Title', required=True)
|
title = fields.Char(string='Notification Title', required=True)
|
||||||
body = fields.Text(string='Notification Body', required=True)
|
body = fields.Html(string='Notification Body', required=True)
|
||||||
|
|
||||||
# Optional promotional image — Odoo auto-generates image_128 thumbnail
|
# Optional promotional image — Odoo auto-generates image_128 thumbnail
|
||||||
image = fields.Image(
|
image = fields.Image(
|
||||||
|
|||||||
42
models/app_promo.py
Normal file
42
models/app_promo.py
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
from odoo import models, fields
|
||||||
|
|
||||||
|
|
||||||
|
class AppPromo(models.Model):
|
||||||
|
"""Highlight Promo items managed from the Odoo backend.
|
||||||
|
|
||||||
|
Displayed as a horizontal scrollable row below the carousel on the Flutter app home screen.
|
||||||
|
When tapped, the app shows a detail screen with title, image, and the rich-text body.
|
||||||
|
"""
|
||||||
|
_name = 'mapan.app.promo'
|
||||||
|
_description = 'Mobile App Promo Highlight'
|
||||||
|
_order = 'sequence, id'
|
||||||
|
|
||||||
|
name = fields.Char(string='Promo Title', required=True)
|
||||||
|
sequence = fields.Integer(string='Sequence', default=10)
|
||||||
|
is_active = fields.Boolean(string='Active', default=True)
|
||||||
|
|
||||||
|
# Rich text body (like the notification body)
|
||||||
|
body = fields.Html(
|
||||||
|
string='Promo Detail Content',
|
||||||
|
help='Rich text content shown when the user taps on this promo highlight.'
|
||||||
|
)
|
||||||
|
|
||||||
|
# Thumbnail image for the list/card view
|
||||||
|
image = fields.Image(
|
||||||
|
string='Promo Image',
|
||||||
|
max_width=800,
|
||||||
|
max_height=800,
|
||||||
|
help='Image shown on the promo card tile. Square format recommended.'
|
||||||
|
)
|
||||||
|
image_128 = fields.Image(
|
||||||
|
string='Thumbnail',
|
||||||
|
related='image',
|
||||||
|
max_width=128,
|
||||||
|
max_height=128,
|
||||||
|
store=True,
|
||||||
|
readonly=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
date_start = fields.Date(string='Valid From', help='Leave empty for no start restriction.')
|
||||||
|
date_end = fields.Date(string='Valid Until', help='Leave empty for no expiry.')
|
||||||
@ -3,6 +3,15 @@ access_mapan_push_wizard_manager,mapan_push_wizard_manager,model_mapan_push_wiza
|
|||||||
access_mapan_app_notification_user,mapan_app_notification_user,model_mapan_app_notification,mapan_loyalty_push.group_mapan_loyalty_push_user,1,0,0,0
|
access_mapan_app_notification_user,mapan_app_notification_user,model_mapan_app_notification,mapan_loyalty_push.group_mapan_loyalty_push_user,1,0,0,0
|
||||||
access_mapan_app_notification_manager,mapan_app_notification_manager,model_mapan_app_notification,mapan_loyalty_push.group_mapan_loyalty_push_manager,1,1,1,1
|
access_mapan_app_notification_manager,mapan_app_notification_manager,model_mapan_app_notification,mapan_loyalty_push.group_mapan_loyalty_push_manager,1,1,1,1
|
||||||
access_mapan_app_notification_portal,mapan_app_notification_portal,model_mapan_app_notification,base.group_portal,1,0,0,0
|
access_mapan_app_notification_portal,mapan_app_notification_portal,model_mapan_app_notification,base.group_portal,1,0,0,0
|
||||||
|
access_mapan_app_carousel_user,mapan_app_carousel_user,model_mapan_app_carousel,mapan_loyalty_push.group_mapan_loyalty_push_user,1,0,0,0
|
||||||
|
access_mapan_app_carousel_manager,mapan_app_carousel_manager,model_mapan_app_carousel,mapan_loyalty_push.group_mapan_loyalty_push_manager,1,1,1,1
|
||||||
|
access_mapan_app_carousel_portal,mapan_app_carousel_portal,model_mapan_app_carousel,base.group_portal,1,0,0,0
|
||||||
|
access_mapan_app_promo_user,mapan_app_promo_user,model_mapan_app_promo,mapan_loyalty_push.group_mapan_loyalty_push_user,1,0,0,0
|
||||||
|
access_mapan_app_promo_manager,mapan_app_promo_manager,model_mapan_app_promo,mapan_loyalty_push.group_mapan_loyalty_push_manager,1,1,1,1
|
||||||
|
access_mapan_app_promo_portal,mapan_app_promo_portal,model_mapan_app_promo,base.group_portal,1,0,0,0
|
||||||
|
access_mapan_app_config_user,mapan_app_config_user,model_mapan_app_config,mapan_loyalty_push.group_mapan_loyalty_push_user,1,0,0,0
|
||||||
|
access_mapan_app_config_manager,mapan_app_config_manager,model_mapan_app_config,mapan_loyalty_push.group_mapan_loyalty_push_manager,1,1,1,1
|
||||||
|
access_mapan_app_config_portal,mapan_app_config_portal,model_mapan_app_config,base.group_portal,1,0,0,0
|
||||||
access_loyalty_card_portal,loyalty.card.portal,loyalty.model_loyalty_card,base.group_portal,1,0,0,0
|
access_loyalty_card_portal,loyalty.card.portal,loyalty.model_loyalty_card,base.group_portal,1,0,0,0
|
||||||
access_loyalty_program_portal,loyalty.program.portal,loyalty.model_loyalty_program,base.group_portal,1,0,0,0
|
access_loyalty_program_portal,loyalty.program.portal,loyalty.model_loyalty_program,base.group_portal,1,0,0,0
|
||||||
access_loyalty_history_portal,loyalty.history.portal,loyalty.model_loyalty_history,base.group_portal,1,0,0,0
|
access_loyalty_history_portal,loyalty.history.portal,loyalty.model_loyalty_history,base.group_portal,1,0,0,0
|
||||||
@ -10,4 +19,3 @@ access_loyalty_reward_portal,loyalty.reward.portal,loyalty.model_loyalty_reward,
|
|||||||
access_loyalty_rule_portal,loyalty.rule.portal,loyalty.model_loyalty_rule,base.group_portal,1,0,0,0
|
access_loyalty_rule_portal,loyalty.rule.portal,loyalty.model_loyalty_rule,base.group_portal,1,0,0,0
|
||||||
access_loyalty_verification_otp_admin,loyalty.verification.otp.admin,model_loyalty_verification_otp,base.group_system,1,1,1,1
|
access_loyalty_verification_otp_admin,loyalty.verification.otp.admin,model_loyalty_verification_otp,base.group_system,1,1,1,1
|
||||||
access_loyalty_verification_otp_portal,loyalty.verification.otp.portal,model_loyalty_verification_otp,base.group_portal,1,1,1,1
|
access_loyalty_verification_otp_portal,loyalty.verification.otp.portal,model_loyalty_verification_otp,base.group_portal,1,1,1,1
|
||||||
|
|
||||||
|
|||||||
|
78
views/app_carousel_views.xml
Normal file
78
views/app_carousel_views.xml
Normal file
@ -0,0 +1,78 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<odoo>
|
||||||
|
<!-- List View -->
|
||||||
|
<record id="view_mapan_app_carousel_tree" model="ir.ui.view">
|
||||||
|
<field name="name">mapan.app.carousel.tree</field>
|
||||||
|
<field name="model">mapan.app.carousel</field>
|
||||||
|
<field name="arch" type="xml">
|
||||||
|
<list string="Carousel Slides" editable="top">
|
||||||
|
<field name="sequence" widget="handle"/>
|
||||||
|
<field name="name"/>
|
||||||
|
<field name="image" widget="image" options="{'size': [48, 48]}"/>
|
||||||
|
<field name="image_url"/>
|
||||||
|
<field name="link_url"/>
|
||||||
|
<field name="is_active"/>
|
||||||
|
</list>
|
||||||
|
</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<!-- Form View -->
|
||||||
|
<record id="view_mapan_app_carousel_form" model="ir.ui.view">
|
||||||
|
<field name="name">mapan.app.carousel.form</field>
|
||||||
|
<field name="model">mapan.app.carousel</field>
|
||||||
|
<field name="arch" type="xml">
|
||||||
|
<form string="Carousel Slide">
|
||||||
|
<sheet>
|
||||||
|
<div class="oe_button_box" name="button_box"/>
|
||||||
|
<div class="oe_title">
|
||||||
|
<h1>
|
||||||
|
<field name="name" placeholder="Slide Title"/>
|
||||||
|
</h1>
|
||||||
|
</div>
|
||||||
|
<group>
|
||||||
|
<group>
|
||||||
|
<field name="sequence"/>
|
||||||
|
<field name="is_active"/>
|
||||||
|
<field name="link_url" placeholder="https://..."/>
|
||||||
|
</group>
|
||||||
|
<group>
|
||||||
|
<field name="image" widget="image" options="{'size': [0, 200]}"/>
|
||||||
|
</group>
|
||||||
|
</group>
|
||||||
|
<group string="External Image (Alternative to Uploaded Image)">
|
||||||
|
<field name="image_url"
|
||||||
|
placeholder="https://example.com/banner.jpg"
|
||||||
|
help="If no image is uploaded above, the app will load the image from this URL."/>
|
||||||
|
</group>
|
||||||
|
</sheet>
|
||||||
|
</form>
|
||||||
|
</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<!-- Action -->
|
||||||
|
<record id="action_mapan_app_carousel" model="ir.actions.act_window">
|
||||||
|
<field name="name">Carousel Slides</field>
|
||||||
|
<field name="res_model">mapan.app.carousel</field>
|
||||||
|
<field name="view_mode">list,form</field>
|
||||||
|
<field name="help" type="html">
|
||||||
|
<p class="o_view_nocontent_smiling_face">
|
||||||
|
No carousel slides yet!
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
Add slides to display on the app home screen banner carousel.
|
||||||
|
Drag the sequence handle to reorder slides.
|
||||||
|
</p>
|
||||||
|
</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<menuitem id="menu_mapan_app_carousel"
|
||||||
|
name="Carousel Slides"
|
||||||
|
parent="menu_mapan_mobile_app_root"
|
||||||
|
action="action_mapan_app_carousel"
|
||||||
|
groups="mapan_loyalty_push.group_mapan_loyalty_push_manager"
|
||||||
|
sequence="30"/>
|
||||||
|
|
||||||
|
<record id="menu_mapan_app_carousel" model="ir.ui.menu">
|
||||||
|
<field name="group_ids" eval="[(6, 0, [ref('mapan_loyalty_push.group_mapan_loyalty_push_manager')])]"/>
|
||||||
|
</record>
|
||||||
|
</odoo>
|
||||||
46
views/app_cms_config_views.xml
Normal file
46
views/app_cms_config_views.xml
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<odoo>
|
||||||
|
<!-- Form View (singleton-style) -->
|
||||||
|
<record id="view_mapan_app_config_form" model="ir.ui.view">
|
||||||
|
<field name="name">mapan.app.config.form</field>
|
||||||
|
<field name="model">mapan.app.config</field>
|
||||||
|
<field name="arch" type="xml">
|
||||||
|
<form string="App Settings">
|
||||||
|
<sheet>
|
||||||
|
<div class="oe_title">
|
||||||
|
<h1>Mobile App Settings</h1>
|
||||||
|
</div>
|
||||||
|
<group string="Account Menu Links">
|
||||||
|
<field name="about_us_url"
|
||||||
|
placeholder="https://yourwebsite.com/about"
|
||||||
|
help="URL opened when the user taps About Us in the app."/>
|
||||||
|
<field name="contact_us_url"
|
||||||
|
placeholder="https://yourwebsite.com/contact"
|
||||||
|
help="URL opened when the user taps Contact Us in the app."/>
|
||||||
|
</group>
|
||||||
|
</sheet>
|
||||||
|
</form>
|
||||||
|
</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<!-- Singleton action: always opens the single config record -->
|
||||||
|
<record id="action_mapan_app_config" model="ir.actions.act_window">
|
||||||
|
<field name="name">App Settings</field>
|
||||||
|
<field name="res_model">mapan.app.config</field>
|
||||||
|
<field name="view_mode">form</field>
|
||||||
|
<field name="target">inline</field>
|
||||||
|
<!-- Open existing record if it exists -->
|
||||||
|
<field name="domain">[]</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<menuitem id="menu_mapan_app_config"
|
||||||
|
name="App Settings"
|
||||||
|
parent="menu_mapan_mobile_app_root"
|
||||||
|
action="action_mapan_app_config"
|
||||||
|
groups="mapan_loyalty_push.group_mapan_loyalty_push_manager"
|
||||||
|
sequence="50"/>
|
||||||
|
|
||||||
|
<record id="menu_mapan_app_config" model="ir.ui.menu">
|
||||||
|
<field name="group_ids" eval="[(6, 0, [ref('mapan_loyalty_push.group_mapan_loyalty_push_manager')])]"/>
|
||||||
|
</record>
|
||||||
|
</odoo>
|
||||||
@ -5,10 +5,9 @@
|
|||||||
<field name="name">mapan.app.notification.tree</field>
|
<field name="name">mapan.app.notification.tree</field>
|
||||||
<field name="model">mapan.app.notification</field>
|
<field name="model">mapan.app.notification</field>
|
||||||
<field name="arch" type="xml">
|
<field name="arch" type="xml">
|
||||||
<list string="App Notifications" default_order="create_date desc" create="false" edit="false" delete="false">
|
<list string="App Notifications" default_order="create_date desc">
|
||||||
<field name="create_date" string="Sent On"/>
|
<field name="create_date" string="Sent On"/>
|
||||||
<field name="title"/>
|
<field name="title"/>
|
||||||
<field name="body"/>
|
|
||||||
<field name="is_global"/>
|
<field name="is_global"/>
|
||||||
</list>
|
</list>
|
||||||
</field>
|
</field>
|
||||||
@ -19,27 +18,32 @@
|
|||||||
<field name="name">mapan.app.notification.form</field>
|
<field name="name">mapan.app.notification.form</field>
|
||||||
<field name="model">mapan.app.notification</field>
|
<field name="model">mapan.app.notification</field>
|
||||||
<field name="arch" type="xml">
|
<field name="arch" type="xml">
|
||||||
<form string="App Notification" create="false" edit="false" delete="false">
|
<form string="App Notification">
|
||||||
<sheet>
|
<sheet>
|
||||||
<div class="oe_title">
|
<div class="oe_title">
|
||||||
<h1>
|
<h1>
|
||||||
<field name="title" readonly="1"/>
|
<field name="title" placeholder="Notification Title"/>
|
||||||
</h1>
|
</h1>
|
||||||
</div>
|
</div>
|
||||||
<group>
|
<group>
|
||||||
<group>
|
<group>
|
||||||
<field name="is_global" readonly="1"/>
|
<field name="is_global"/>
|
||||||
</group>
|
|
||||||
<group>
|
|
||||||
<field name="create_date" string="Sent On" readonly="1"/>
|
<field name="create_date" string="Sent On" readonly="1"/>
|
||||||
</group>
|
</group>
|
||||||
</group>
|
|
||||||
<group>
|
<group>
|
||||||
<field name="body" readonly="1"/>
|
<field name="image" widget="image" options="{'size': [0, 160]}"/>
|
||||||
|
</group>
|
||||||
</group>
|
</group>
|
||||||
<group string="Sent To" invisible="is_global == True">
|
<group string="Sent To" invisible="is_global == True">
|
||||||
<field name="partner_ids" widget="many2many_tags" readonly="1" nolabel="1"/>
|
<field name="partner_ids" widget="many2many_tags" nolabel="1"
|
||||||
|
options="{'no_create': True, 'no_open': True}"/>
|
||||||
</group>
|
</group>
|
||||||
|
<notebook>
|
||||||
|
<page string="Message Content">
|
||||||
|
<field name="body" widget="html"
|
||||||
|
placeholder="Enter the notification message content here..."/>
|
||||||
|
</page>
|
||||||
|
</notebook>
|
||||||
</sheet>
|
</sheet>
|
||||||
</form>
|
</form>
|
||||||
</field>
|
</field>
|
||||||
|
|||||||
80
views/app_promo_views.xml
Normal file
80
views/app_promo_views.xml
Normal file
@ -0,0 +1,80 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<odoo>
|
||||||
|
<!-- List View -->
|
||||||
|
<record id="view_mapan_app_promo_tree" model="ir.ui.view">
|
||||||
|
<field name="name">mapan.app.promo.tree</field>
|
||||||
|
<field name="model">mapan.app.promo</field>
|
||||||
|
<field name="arch" type="xml">
|
||||||
|
<list string="Promo Highlights" editable="top">
|
||||||
|
<field name="sequence" widget="handle"/>
|
||||||
|
<field name="name"/>
|
||||||
|
<field name="image" widget="image" options="{'size': [48, 48]}"/>
|
||||||
|
<field name="date_start"/>
|
||||||
|
<field name="date_end"/>
|
||||||
|
<field name="is_active"/>
|
||||||
|
</list>
|
||||||
|
</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<!-- Form View -->
|
||||||
|
<record id="view_mapan_app_promo_form" model="ir.ui.view">
|
||||||
|
<field name="name">mapan.app.promo.form</field>
|
||||||
|
<field name="model">mapan.app.promo</field>
|
||||||
|
<field name="arch" type="xml">
|
||||||
|
<form string="Promo Highlight">
|
||||||
|
<sheet>
|
||||||
|
<field name="image" widget="image" options="{'size': [0, 180]}"
|
||||||
|
class="oe_avatar" style="max-width:200px"/>
|
||||||
|
<div class="oe_title">
|
||||||
|
<h1>
|
||||||
|
<field name="name" placeholder="Promo Title"/>
|
||||||
|
</h1>
|
||||||
|
</div>
|
||||||
|
<group>
|
||||||
|
<group>
|
||||||
|
<field name="sequence"/>
|
||||||
|
<field name="is_active"/>
|
||||||
|
</group>
|
||||||
|
<group>
|
||||||
|
<field name="date_start"/>
|
||||||
|
<field name="date_end"/>
|
||||||
|
</group>
|
||||||
|
</group>
|
||||||
|
<notebook>
|
||||||
|
<page string="Detail Content">
|
||||||
|
<field name="body" widget="html"
|
||||||
|
placeholder="Enter the promo detail content shown when the user taps this card..."/>
|
||||||
|
</page>
|
||||||
|
</notebook>
|
||||||
|
</sheet>
|
||||||
|
</form>
|
||||||
|
</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<!-- Action -->
|
||||||
|
<record id="action_mapan_app_promo" model="ir.actions.act_window">
|
||||||
|
<field name="name">Promo Highlights</field>
|
||||||
|
<field name="res_model">mapan.app.promo</field>
|
||||||
|
<field name="view_mode">list,form</field>
|
||||||
|
<field name="help" type="html">
|
||||||
|
<p class="o_view_nocontent_smiling_face">
|
||||||
|
No promo highlights yet!
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
Add promo items to display below the carousel on the app home screen.
|
||||||
|
Users can tap a promo to read the full detail.
|
||||||
|
</p>
|
||||||
|
</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<menuitem id="menu_mapan_app_promo"
|
||||||
|
name="Promo Highlights"
|
||||||
|
parent="menu_mapan_mobile_app_root"
|
||||||
|
action="action_mapan_app_promo"
|
||||||
|
groups="mapan_loyalty_push.group_mapan_loyalty_push_manager"
|
||||||
|
sequence="40"/>
|
||||||
|
|
||||||
|
<record id="menu_mapan_app_promo" model="ir.ui.menu">
|
||||||
|
<field name="group_ids" eval="[(6, 0, [ref('mapan_loyalty_push.group_mapan_loyalty_push_manager')])]"/>
|
||||||
|
</record>
|
||||||
|
</odoo>
|
||||||
@ -6,7 +6,7 @@ class PushNotificationWizard(models.TransientModel):
|
|||||||
_description = 'Send Mobile App Notification'
|
_description = 'Send Mobile App Notification'
|
||||||
|
|
||||||
title = fields.Char(string='Notification Title', required=True)
|
title = fields.Char(string='Notification Title', required=True)
|
||||||
body = fields.Text(string='Notification Body', required=True)
|
body = fields.Html(string='Notification Body', required=True)
|
||||||
image = fields.Image(
|
image = fields.Image(
|
||||||
string='Notification Image (optional)',
|
string='Notification Image (optional)',
|
||||||
max_width=1920,
|
max_width=1920,
|
||||||
|
|||||||
@ -7,7 +7,8 @@
|
|||||||
<form string="Send Push Notification">
|
<form string="Send Push Notification">
|
||||||
<group>
|
<group>
|
||||||
<field name="title"/>
|
<field name="title"/>
|
||||||
<field name="body"/>
|
<field name="body" widget="html"
|
||||||
|
placeholder="Enter notification message content..."/>
|
||||||
<field name="image" widget="image" options="{'size': [0, 200]}"
|
<field name="image" widget="image" options="{'size': [0, 200]}"
|
||||||
help="Optional promotional image (JPG/PNG). Displayed in the notification detail screen."/>
|
help="Optional promotional image (JPG/PNG). Displayed in the notification detail screen."/>
|
||||||
<field name="recipient_type" widget="radio" options="{'horizontal': true}"/>
|
<field name="recipient_type" widget="radio" options="{'horizontal': true}"/>
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user