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 -*-
|
||||
{
|
||||
'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': """
|
||||
Integrates Odoo with Firebase Cloud Messaging (FCM) to send push notifications directly
|
||||
to customers' Android devices. Maps FCM tokens to res_partner records.
|
||||
Content Management System for the Mie Mapan Loyalty Flutter App.
|
||||
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",
|
||||
'category': 'Marketing',
|
||||
'version': '1.0',
|
||||
'version': '1.1',
|
||||
'depends': ['base', 'loyalty', 'pos_loyalty', 'sale_loyalty'],
|
||||
'data': [
|
||||
'security/mapan_loyalty_push_security.xml',
|
||||
@ -16,6 +21,9 @@
|
||||
'wizard/push_wizard_views.xml',
|
||||
'views/res_partner_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,
|
||||
'application': False,
|
||||
|
||||
@ -2,6 +2,7 @@
|
||||
from odoo import http
|
||||
from odoo.http import request
|
||||
|
||||
|
||||
def normalize_phone_search(phone):
|
||||
if not phone:
|
||||
return []
|
||||
@ -20,6 +21,7 @@ def normalize_phone_search(phone):
|
||||
candidates.append('62' + digits)
|
||||
return list(set(candidates))
|
||||
|
||||
|
||||
class AppNotificationController(http.Controller):
|
||||
|
||||
@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.
|
||||
Returns image_128 (base64 thumbnail) for list display.
|
||||
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
|
||||
user = request.env.user
|
||||
@ -58,21 +61,144 @@ class AppNotificationController(http.Controller):
|
||||
elif not img:
|
||||
notif['image_128'] = None
|
||||
notif['has_image'] = bool(notif['image_128'])
|
||||
# body is HTML string — pass through as-is
|
||||
if notif.get('body') is False:
|
||||
notif['body'] = ''
|
||||
|
||||
return {
|
||||
'status': 'success',
|
||||
'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)
|
||||
def fetch_branches(self, **kw):
|
||||
"""
|
||||
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:
|
||||
branches = request.env['res.company'].sudo().search_read(
|
||||
[('parent_id', '!=', False)],
|
||||
['name', 'street', 'city', 'phone'],
|
||||
['name', 'street', 'city', 'phone', 'partner_latitude', 'partner_longitude'],
|
||||
limit=50
|
||||
)
|
||||
return {'status': 'success', 'data': branches}
|
||||
|
||||
@ -1,7 +1,8 @@
|
||||
from . import res_partner
|
||||
from . import app_notification
|
||||
from . import app_carousel
|
||||
from . import app_promo
|
||||
from . import app_cms_config
|
||||
from . import res_users
|
||||
from . import loyalty_card
|
||||
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'
|
||||
|
||||
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
|
||||
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_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_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_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
|
||||
@ -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_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
|
||||
|
||||
|
||||
|
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="model">mapan.app.notification</field>
|
||||
<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="title"/>
|
||||
<field name="body"/>
|
||||
<field name="is_global"/>
|
||||
</list>
|
||||
</field>
|
||||
@ -19,27 +18,32 @@
|
||||
<field name="name">mapan.app.notification.form</field>
|
||||
<field name="model">mapan.app.notification</field>
|
||||
<field name="arch" type="xml">
|
||||
<form string="App Notification" create="false" edit="false" delete="false">
|
||||
<form string="App Notification">
|
||||
<sheet>
|
||||
<div class="oe_title">
|
||||
<h1>
|
||||
<field name="title" readonly="1"/>
|
||||
<field name="title" placeholder="Notification Title"/>
|
||||
</h1>
|
||||
</div>
|
||||
<group>
|
||||
<group>
|
||||
<field name="is_global" readonly="1"/>
|
||||
</group>
|
||||
<group>
|
||||
<field name="is_global"/>
|
||||
<field name="create_date" string="Sent On" readonly="1"/>
|
||||
</group>
|
||||
</group>
|
||||
<group>
|
||||
<field name="body" readonly="1"/>
|
||||
<field name="image" widget="image" options="{'size': [0, 160]}"/>
|
||||
</group>
|
||||
</group>
|
||||
<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>
|
||||
<notebook>
|
||||
<page string="Message Content">
|
||||
<field name="body" widget="html"
|
||||
placeholder="Enter the notification message content here..."/>
|
||||
</page>
|
||||
</notebook>
|
||||
</sheet>
|
||||
</form>
|
||||
</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'
|
||||
|
||||
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(
|
||||
string='Notification Image (optional)',
|
||||
max_width=1920,
|
||||
|
||||
@ -7,7 +7,8 @@
|
||||
<form string="Send Push Notification">
|
||||
<group>
|
||||
<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]}"
|
||||
help="Optional promotional image (JPG/PNG). Displayed in the notification detail screen."/>
|
||||
<field name="recipient_type" widget="radio" options="{'horizontal': true}"/>
|
||||
|
||||
Loading…
Reference in New Issue
Block a user