first commit

This commit is contained in:
Suherdy Yacob 2026-05-28 10:07:53 +07:00
commit 98486e284e
20 changed files with 944 additions and 0 deletions

33
.gitignore vendored Normal file
View File

@ -0,0 +1,33 @@
# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
*$py.class
# C extensions
*.so
# Distribution / packaging
.Python
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
wheels/
share/python-wheels/
*.egg-info/
.installed.cfg
*.egg
# Editor & System Files
.vscode/
.idea/
*.swp
*~
.DS_Store

100
README.md Normal file
View File

@ -0,0 +1,100 @@
# POS Loyalty Marketing Access & Approvals
A custom Odoo 19 module introducing comprehensive security permissions, double-approval validation states, hierarchy-aware routing, and automated accounting mapping for Point of Sale Loyalty, Promotions, and Vouchers.
---
## 🌟 Key Features
### 1. Granular Security Groups
- **Marketing / User**:
- Full **Read / Create / Write** access to loyalty programs, discount programs, gift cards, and eWallets.
- Restricted from **deleting** marketing programs.
- Restricted from **generating vouchers** (coupons) directly.
- Strict **Read-only** access to Point of Sale Orders, Order Lines, Payments, POS Reports, Loyalty History, and Loyalty Mail.
- **Marketing / Manager**:
- Full user capabilities.
- Exclusive access to **Generate Vouchers** directly via the standard generation wizard.
### 2. Double-Approval validation Flow
- All newly created `loyalty.program` records (Loyalty, Discount, Promotion, Promo Code, Gift Card, eWallet) start in a **Draft** state.
- Marketing Users can edit draft programs and click **Submit for Approval** to put them into the **Pending Approval** state.
- The designated company-level approver (or any user in the **Marketing / Manager** group if no specific approver is configured) can click **Approve** to move the program into the **Active** state.
- When an active program is modified, its state automatically resets to **Draft** to ensure all edits are explicitly reviewed and re-approved before they can be used at the POS register.
### 3. Staged Voucher Generation Queues
- Restricts immediate voucher code generation for non-managers.
- When a user submits a request to generate coupon/voucher codes, the module automatically intercepts it and creates a **Voucher Generation Request** (`loyalty.voucher.generation.request`).
- This request stores the desired program, code count, prefix, point values, and expiration dates.
- Managers review the request queues and approve them to execute code generation safely in the background.
### 4. Hierarchy-Aware Company-Level Approvers
- Designated approvers are configured directly on each **Company** record via the new **Marketing Approvals** notebook tab (*Settings > Users & Companies > Companies*).
- Form fields available:
- **Marketing Program Approver**
- **Voucher Generation Approver**
- **Upward Tree Walking**: When a marketing program set on the parent company **OT** is verified:
- The module walks upward in the company hierarchy tree (`parent_id`) starting from the active/program company.
- Branch companies automatically inherit and share parent-level (**OT**) approvers.
- Local overrides can be set on any child branch company if different personnel are desired.
- Fallback logic authorizes any **Marketing / Manager** user if no designated approver exists in the tree.
### 5. Automated Discount Product Configuration
- When a marketing program is saved, Odoo standard generates an underlying service product used to represent the discount item in sales order lines.
- This module automatically intercepts that product's creation and:
- Assigns it to the product category **`OT / Saleable / PoS / Discounts`** (with fallback to any category named **`Discounts`**).
- Assigns both the **Income Account** and **Expense Account** to code **`412201`** for the corresponding active company.
---
## 📂 Technical Layout & Structure
```text
pos_loyalty_marketing_access/
├── __init__.py
├── __manifest__.py
├── data/
│ └── ir_sequence_data.xml
├── models/
│ ├── __init__.py
│ ├── loyalty_program.py
│ ├── loyalty_reward.py
│ ├── loyalty_voucher_generation_request.py
│ ├── pos_config.py
│ └── sale_order.py
├── security/
│ ├── ir.model.access.csv
│ └── pos_loyalty_marketing_security.xml
├── views/
│ ├── loyalty_program_views.xml
│ ├── loyalty_voucher_generation_request_views.xml
│ ├── menu_views.xml
│ ├── pos_config_views.xml
│ └── res_company_views.xml
├── wizards/
│ ├── __init__.py
│ └── loyalty_generate_wizard.py
└── README.md
```
---
## 🛠️ Installation & Upgrades
1. Place the `pos_loyalty_marketing_access` folder inside your custom addons directory.
2. Restart the Odoo server.
3. Update the module list in Odoo developer settings.
4. Search for `POS Loyalty Marketing Access & Approvals` and click **Install** or **Upgrade**.
Alternatively, upgrade the database schema directly via the command line:
```bash
python3 odoo-bin -c odoo.conf -d mapangroup_o19_7 -u pos_loyalty_marketing_access --stop-after-init
```
---
## 📝 License & Copyright
- **Author**: Suherdy Yacob
- **Version**: 1.0
- **Compatibility**: Odoo 19.0 (Community & Enterprise Editions)

4
__init__.py Normal file
View File

@ -0,0 +1,4 @@
# -*- coding: utf-8 -*-
from . import models
from . import wizards

36
__manifest__.py Normal file
View File

@ -0,0 +1,36 @@
# -*- coding: utf-8 -*-
{
'name': 'POS Loyalty Marketing Access & Approvals',
'version': '1.0',
'category': 'Sales/Point of Sale',
'summary': 'Custom Marketing security groups, access restrictions, and approval flows for loyalty programs and voucher generation.',
'description': """
POS Loyalty Marketing Access & Approvals
========================================
This module introduces:
1. Two new security groups: Marketing / User and Marketing / Manager.
2. Read-only POS orders and member transaction views for Marketing Users.
3. Marketing Program draft/approved states and an approval flow.
4. Voucher generation approval flow using a new Voucher Generation Requests model.
""",
'author': 'Suherdy Yacob',
'depends': [
'point_of_sale',
'loyalty',
'pos_loyalty',
'sale_loyalty',
],
'data': [
'security/pos_loyalty_marketing_security.xml',
'security/ir.model.access.csv',
'data/ir_sequence_data.xml',
'views/pos_config_views.xml',
'views/loyalty_program_views.xml',
'views/loyalty_voucher_generation_request_views.xml',
'views/res_company_views.xml',
'views/menu_views.xml',
],
'installable': True,
'application': False,
'license': 'LGPL-3',
}

14
data/ir_sequence_data.xml Normal file
View File

@ -0,0 +1,14 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<data noupdate="1">
<record id="seq_loyalty_voucher_generation_request" model="ir.sequence">
<field name="name">Voucher Generation Request Sequence</field>
<field name="code">loyalty.voucher.generation.request</field>
<field name="prefix">VGR/</field>
<field name="padding">5</field>
<field name="number_next">1</field>
<field name="number_increment">1</field>
<field name="company_id" eval="False"/>
</record>
</data>
</odoo>

8
models/__init__.py Normal file
View File

@ -0,0 +1,8 @@
# -*- coding: utf-8 -*-
from . import pos_config
from . import loyalty_program
from . import sale_order
from . import loyalty_voucher_generation_request
from . import loyalty_reward

70
models/loyalty_program.py Normal file
View File

@ -0,0 +1,70 @@
# -*- coding: utf-8 -*-
from odoo import _, api, fields, models
from odoo.exceptions import UserError
class LoyaltyProgram(models.Model):
_name = 'loyalty.program'
_inherit = ['loyalty.program', 'mail.thread', 'mail.activity.mixin']
state = fields.Selection([
('draft', 'Draft'),
('pending', 'Pending Approval'),
('approved', 'Approved')
], default='draft', string='Status', tracking=True)
def action_request_approval(self):
for program in self:
if program.state != 'draft':
continue
program.write({'state': 'pending'})
program.message_post(body=_("Approval requested for this marketing program."))
def action_approve(self):
for program in self:
if program.state != 'pending':
continue
# Check authorization
target_company = program.company_id or self.env.company
approver = target_company._get_marketing_program_approver()
if approver:
if self.env.user.id != approver.id:
raise UserError(_("Only the designated Marketing Program Approver (%s) can approve this program.") % approver.name)
else:
is_manager = self.env.user.has_group('pos_loyalty_marketing_access.group_marketing_manager')
if not is_manager:
raise UserError(_("Only a Marketing Manager can approve this program as no designated approver is configured."))
program.write({'state': 'approved'})
program.message_post(body=_("Marketing program has been approved and is now active."))
def action_reset_draft(self):
for program in self:
program.write({'state': 'draft'})
program.message_post(body=_("Marketing program reset to Draft."))
def write(self, vals):
# If we are only modifying state, skip the write reset checks
if len(vals) == 1 and 'state' in vals:
return super().write(vals)
# For modifications, check if they are in pending/approved state
for program in self:
if program.state in ['pending', 'approved']:
target_company = program.company_id or self.env.company
approver = target_company._get_marketing_program_approver()
if approver:
if self.env.user.id != approver.id:
vals['state'] = 'draft'
program.message_post(body=_("The approved marketing program was modified by a standard user and has been reset to Draft status for re-approval."))
else:
is_manager = self.env.user.has_group('pos_loyalty_marketing_access.group_marketing_manager')
if not is_manager:
vals['state'] = 'draft'
program.message_post(body=_("The approved marketing program was modified by a standard user and has been reset to Draft status for re-approval."))
return super().write(vals)

35
models/loyalty_reward.py Normal file
View File

@ -0,0 +1,35 @@
# -*- coding: utf-8 -*-
from odoo import api, fields, models
class LoyaltyReward(models.Model):
_inherit = 'loyalty.reward'
def _get_discount_product_values(self):
res = super()._get_discount_product_values()
# Search for the dedicated category "OT / Saleable / PoS / Discounts"
category = self.env['product.category'].sudo().search([
('complete_name', '=', 'OT / Saleable / PoS / Discounts')
], limit=1)
if not category:
# Fallback to any category named "Discounts" if not found
category = self.env['product.category'].sudo().search([
('name', '=', 'Discounts')
], limit=1)
for reward, vals in zip(self, res):
if category:
vals['categ_id'] = category.id
# Search for account "412201" matching the company of the reward
company = reward.company_id or self.env.company
account = self.env['account.account'].sudo().search([
('code', '=', '412201'),
('company_id', '=', company.id)
], limit=1)
if account:
vals['property_account_income_id'] = account.id
vals['property_account_expense_id'] = account.id
return res

View File

@ -0,0 +1,163 @@
# -*- coding: utf-8 -*-
from odoo import _, api, fields, models
from odoo.exceptions import UserError, ValidationError
from odoo.fields import Domain
class LoyaltyVoucherGenerationRequest(models.Model):
_name = 'loyalty.voucher.generation.request'
_description = 'Voucher Generation Request'
_inherit = ['mail.thread', 'mail.activity.mixin']
_order = 'id desc'
name = fields.Char(
string='Reference',
required=True,
copy=False,
readonly=True,
default=lambda self: _('New')
)
program_id = fields.Many2one(
'loyalty.program',
string='Program',
required=True,
domain="[('state', '=', 'approved')]"
)
mode = fields.Selection([
('anonymous', 'Anonymous Customers'),
('selected', 'Selected Customers')
], string='For', required=True, default='anonymous')
customer_ids = fields.Many2many('res.partner', string='Customers')
customer_tag_ids = fields.Many2many('res.partner.category', string='Customer Tags')
coupon_qty = fields.Integer('Quantity', default=1)
points_granted = fields.Float('Grant', default=1.0)
valid_until = fields.Date('Valid Until')
description = fields.Text('Description')
state = fields.Selection([
('draft', 'Draft'),
('pending', 'Pending Approval'),
('approved', 'Approved'),
('rejected', 'Rejected')
], default='draft', string='Status', tracking=True)
requester_id = fields.Many2one(
'res.users',
string='Requested By',
default=lambda self: self.env.user,
readonly=True
)
approver_id = fields.Many2one(
'res.users',
string='Approved By',
readonly=True
)
company_id = fields.Many2one(
'res.company',
string='Company',
default=lambda self: self.env.company
)
generated_card_ids = fields.Many2many(
'loyalty.card',
string='Generated Vouchers',
readonly=True
)
@api.model_create_multi
def create(self, vals_list):
for vals in vals_list:
if vals.get('name', _('New')) == _('New'):
vals['name'] = self.env['ir.sequence'].next_by_code('loyalty.voucher.generation.request') or _('New')
return super().create(vals_list)
def _get_partners(self):
self.ensure_one()
if self.mode != 'selected':
return self.env['res.partner']
domains = []
if self.customer_ids:
domains.append([('id', 'in', self.customer_ids.ids)])
if self.customer_tag_ids:
domains.append([('category_id', 'in', self.customer_tag_ids.ids)])
return self.env['res.partner'].search(Domain.OR(domains) if domains else Domain.TRUE)
def _get_coupon_values(self, partner):
self.ensure_one()
return {
'program_id': self.program_id.id,
'points': self.points_granted,
'expiration_date': self.valid_until,
'partner_id': partner.id if self.mode == 'selected' else False,
}
def action_request_approval(self):
for req in self:
if req.state != 'draft':
continue
req.write({'state': 'pending'})
req.message_post(body=_("Voucher generation approval requested."))
def action_approve(self):
for req in self:
if req.state != 'pending':
continue
# Check authorization
target_company = req.company_id or self.env.company
approver = target_company._get_voucher_generation_approver()
if approver:
if self.env.user.id != approver.id:
raise UserError(_("Only the designated Voucher Generation Approver (%s) can approve this request.") % approver.name)
else:
is_manager = self.env.user.has_group('pos_loyalty_marketing_access.group_marketing_manager')
if not is_manager:
raise UserError(_("Only a Marketing Manager can approve this request as no designated approver is configured."))
if req.coupon_qty <= 0:
raise ValidationError(_("Invalid quantity."))
# Execute voucher generation
coupon_create_vals = []
customers = req._get_partners() or range(req.coupon_qty)
for partner in customers:
coupon_create_vals.append(req._get_coupon_values(partner))
coupons = self.env['loyalty.card'].create(coupon_create_vals)
self.env['loyalty.history'].create([
{
'description': req.description or _("Gift For Customer"),
'card_id': coupon.id,
'issued': req.points_granted,
} for coupon in coupons
])
req.write({
'state': 'approved',
'approver_id': self.env.user.id,
'generated_card_ids': [(6, 0, coupons.ids)],
})
req.message_post(body=_("Voucher generation request approved. %s vouchers have been generated.") % len(coupons))
return {
'type': 'ir.actions.client',
'tag': 'display_notification',
'params': {
'title': _('Success'),
'message': _('Vouchers generated successfully.'),
'type': 'success',
'sticky': False,
}
}
def action_reject(self):
for req in self:
if req.state != 'pending':
continue
req.write({
'state': 'rejected',
'approver_id': self.env.user.id,
})
req.message_post(body=_("Voucher generation request rejected."))

60
models/pos_config.py Normal file
View File

@ -0,0 +1,60 @@
# -*- coding: utf-8 -*-
from odoo import api, fields, models
class ResCompany(models.Model):
_inherit = 'res.company'
marketing_program_approver_id = fields.Many2one(
'res.users',
string='Marketing Program Approver',
help='User authorized to approve marketing programs for this company.'
)
voucher_generation_approver_id = fields.Many2one(
'res.users',
string='Voucher Generation Approver',
help='User authorized to approve voucher generation requests for this company.'
)
def _get_marketing_program_approver(self):
self.ensure_one()
comp = self
while comp:
if comp.marketing_program_approver_id:
return comp.marketing_program_approver_id
comp = comp.parent_id
return self.env['res.users']
def _get_voucher_generation_approver(self):
self.ensure_one()
comp = self
while comp:
if comp.voucher_generation_approver_id:
return comp.voucher_generation_approver_id
comp = comp.parent_id
return self.env['res.users']
class PosConfig(models.Model):
_inherit = 'pos.config'
marketing_program_approver_id = fields.Many2one(
'res.users',
related='company_id.marketing_program_approver_id',
readonly=False,
string='Marketing Program Approver',
help='User authorized to approve marketing programs.'
)
voucher_generation_approver_id = fields.Many2one(
'res.users',
related='company_id.voucher_generation_approver_id',
readonly=False,
string='Voucher Generation Approver',
help='User authorized to approve voucher generation requests.'
)
def _get_program_ids(self):
res = super()._get_program_ids()
if not res:
return res
return res.filtered(lambda p: p.state == 'approved')

14
models/sale_order.py Normal file
View File

@ -0,0 +1,14 @@
# -*- coding: utf-8 -*-
from odoo import api, models
class SaleOrder(models.Model):
_inherit = 'sale.order'
def _get_program_domain(self):
domain = super()._get_program_domain()
return domain + [('state', '=', 'approved')]
def _get_trigger_domain(self):
domain = super()._get_trigger_domain()
return domain + [('state', '=', 'approved')]

View File

@ -0,0 +1,13 @@
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
access_marketing_user_loyalty_program,Access Loyalty Program Marketing User,loyalty.model_loyalty_program,group_marketing_user,1,1,1,0
access_marketing_user_loyalty_rule,Access Loyalty Rule Marketing User,loyalty.model_loyalty_rule,group_marketing_user,1,1,1,0
access_marketing_user_loyalty_reward,Access Loyalty Reward Marketing User,loyalty.model_loyalty_reward,group_marketing_user,1,1,1,0
access_marketing_user_loyalty_mail,Access Loyalty Mail Marketing User,loyalty.model_loyalty_mail,group_marketing_user,1,1,1,0
access_marketing_user_loyalty_card,Access Loyalty Card Marketing User,loyalty.model_loyalty_card,group_marketing_user,1,1,1,0
access_marketing_user_loyalty_history,Access Loyalty History Marketing User,loyalty.model_loyalty_history,group_marketing_user,1,0,0,0
access_marketing_user_pos_order,Access POS Order Marketing User,point_of_sale.model_pos_order,group_marketing_user,1,0,0,0
access_marketing_user_pos_order_line,Access POS Order Line Marketing User,point_of_sale.model_pos_order_line,group_marketing_user,1,0,0,0
access_marketing_user_pos_payment,Access POS Payment Marketing User,point_of_sale.model_pos_payment,group_marketing_user,1,0,0,0
access_marketing_user_report_pos_order,Access POS Report Marketing User,point_of_sale.model_report_pos_order,group_marketing_user,1,0,0,0
access_marketing_manager_loyalty_generate_wizard,Access Coupon Generation Wizard Marketing Manager,loyalty.model_loyalty_generate_wizard,group_marketing_manager,1,1,1,0
access_marketing_user_voucher_request,Access Voucher Request Marketing User,model_loyalty_voucher_generation_request,group_marketing_user,1,1,1,1
1 id name model_id:id group_id:id perm_read perm_write perm_create perm_unlink
2 access_marketing_user_loyalty_program Access Loyalty Program Marketing User loyalty.model_loyalty_program group_marketing_user 1 1 1 0
3 access_marketing_user_loyalty_rule Access Loyalty Rule Marketing User loyalty.model_loyalty_rule group_marketing_user 1 1 1 0
4 access_marketing_user_loyalty_reward Access Loyalty Reward Marketing User loyalty.model_loyalty_reward group_marketing_user 1 1 1 0
5 access_marketing_user_loyalty_mail Access Loyalty Mail Marketing User loyalty.model_loyalty_mail group_marketing_user 1 1 1 0
6 access_marketing_user_loyalty_card Access Loyalty Card Marketing User loyalty.model_loyalty_card group_marketing_user 1 1 1 0
7 access_marketing_user_loyalty_history Access Loyalty History Marketing User loyalty.model_loyalty_history group_marketing_user 1 0 0 0
8 access_marketing_user_pos_order Access POS Order Marketing User point_of_sale.model_pos_order group_marketing_user 1 0 0 0
9 access_marketing_user_pos_order_line Access POS Order Line Marketing User point_of_sale.model_pos_order_line group_marketing_user 1 0 0 0
10 access_marketing_user_pos_payment Access POS Payment Marketing User point_of_sale.model_pos_payment group_marketing_user 1 0 0 0
11 access_marketing_user_report_pos_order Access POS Report Marketing User point_of_sale.model_report_pos_order group_marketing_user 1 0 0 0
12 access_marketing_manager_loyalty_generate_wizard Access Coupon Generation Wizard Marketing Manager loyalty.model_loyalty_generate_wizard group_marketing_manager 1 1 1 0
13 access_marketing_user_voucher_request Access Voucher Request Marketing User model_loyalty_voucher_generation_request group_marketing_user 1 1 1 1

View File

@ -0,0 +1,24 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<data>
<!-- Define Privilege for Marketing Loyalty Access -->
<record id="privilege_marketing_loyalty_access" model="res.groups.privilege">
<field name="name">Marketing Loyalty Access</field>
<field name="category_id" ref="base.module_category_marketing"/>
</record>
<!-- Marketing / User Group -->
<record id="group_marketing_user" model="res.groups">
<field name="name">User</field>
<field name="privilege_id" ref="privilege_marketing_loyalty_access"/>
<field name="implied_ids" eval="[(4, ref('base.group_user'))]"/>
</record>
<!-- Marketing / Manager Group -->
<record id="group_marketing_manager" model="res.groups">
<field name="name">Manager</field>
<field name="privilege_id" ref="privilege_marketing_loyalty_access"/>
<field name="implied_ids" eval="[(4, ref('group_marketing_user'))]"/>
</record>
</data>
</odoo>

View File

@ -0,0 +1,61 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<!-- Inherit Loyalty Program Form View -->
<record id="loyalty_program_view_form_inherit_approval" model="ir.ui.view">
<field name="name">loyalty.program.view.form.inherit.approval</field>
<field name="model">loyalty.program</field>
<field name="inherit_id" ref="loyalty.loyalty_program_view_form"/>
<field name="arch" type="xml">
<!-- Add action buttons and statusbar in header -->
<xpath expr="//header" position="inside">
<button name="action_request_approval" string="Request Approval" type="object" class="btn-primary"
invisible="state != 'draft'"/>
<button name="action_approve" string="Approve" type="object" class="btn-success"
invisible="state != 'pending'"/>
<button name="action_reset_draft" string="Reset to Draft" type="object" class="btn-secondary"
invisible="state not in ['pending', 'approved']"/>
<field name="state" widget="statusbar" statusbar_visible="draft,pending,approved"/>
</xpath>
<!-- Add chatter support under the form sheet -->
<xpath expr="//sheet" position="after">
<chatter/>
</xpath>
</field>
</record>
<!-- Inherit Loyalty Program List View -->
<record id="loyalty_program_view_tree_inherit_state" model="ir.ui.view">
<field name="name">loyalty.program.view.list.inherit.state</field>
<field name="model">loyalty.program</field>
<field name="inherit_id" ref="loyalty.loyalty_program_view_tree"/>
<field name="arch" type="xml">
<xpath expr="//field[@name='program_type']" position="after">
<field name="state" widget="badge"
decoration-info="state == 'draft'"
decoration-warning="state == 'pending'"
decoration-success="state == 'approved'"/>
</xpath>
</field>
</record>
<!-- Inherit Loyalty Program Search View -->
<record id="loyalty_program_view_search_inherit" model="ir.ui.view">
<field name="name">loyalty.program.view.search.inherit</field>
<field name="model">loyalty.program</field>
<field name="inherit_id" ref="loyalty.loyalty_program_view_search"/>
<field name="arch" type="xml">
<xpath expr="//filter[@name='inactive']" position="before">
<filter string="Draft" name="state_draft" domain="[('state', '=', 'draft')]"/>
<filter string="Pending Approval" name="state_pending" domain="[('state', '=', 'pending')]"/>
<filter string="Approved" name="state_approved" domain="[('state', '=', 'approved')]"/>
<separator/>
</xpath>
<xpath expr="//search" position="inside">
<group>
<filter string="Status" name="group_by_state" context="{'group_by': 'state'}"/>
</group>
</xpath>
</field>
</record>
</odoo>

View File

@ -0,0 +1,121 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<record id="loyalty_voucher_generation_request_view_list" model="ir.ui.view">
<field name="name">loyalty.voucher.generation.request.list</field>
<field name="model">loyalty.voucher.generation.request</field>
<field name="arch" type="xml">
<list string="Voucher Generation Requests">
<field name="name"/>
<field name="program_id"/>
<field name="mode"/>
<field name="coupon_qty"/>
<field name="points_granted"/>
<field name="valid_until"/>
<field name="requester_id"/>
<field name="state" widget="badge"
decoration-info="state == 'draft'"
decoration-warning="state == 'pending'"
decoration-success="state == 'approved'"
decoration-danger="state == 'rejected'"/>
</list>
</field>
</record>
<record id="loyalty_voucher_generation_request_view_form" model="ir.ui.view">
<field name="name">loyalty.voucher.generation.request.form</field>
<field name="model">loyalty.voucher.generation.request</field>
<field name="arch" type="xml">
<form string="Voucher Generation Request">
<header>
<button name="action_request_approval" string="Request Approval" type="object" class="btn-primary"
invisible="state != 'draft'"/>
<button name="action_approve" string="Approve" type="object" class="btn-success"
invisible="state != 'pending'"/>
<button name="action_reject" string="Reject" type="object" class="btn-danger"
invisible="state != 'pending'"/>
<field name="state" widget="statusbar" statusbar_visible="draft,pending,approved,rejected"/>
</header>
<sheet>
<div class="oe_title">
<h1>
<field name="name"/>
</h1>
</div>
<group>
<group>
<field name="program_id" readonly="state != 'draft'"/>
<field name="mode" widget="radio" readonly="state != 'draft'"/>
<field name="coupon_qty" readonly="state != 'draft'"/>
<field name="points_granted" readonly="state != 'draft'"/>
</group>
<group>
<field name="valid_until" readonly="state != 'draft'"/>
<field name="requester_id"/>
<field name="approver_id" invisible="not approver_id"/>
<field name="company_id" groups="base.group_multi_company"/>
</group>
</group>
<notebook>
<page string="Target Customers" name="customers" invisible="mode != 'selected'">
<group>
<field name="customer_ids" widget="many2many_tags" readonly="state != 'draft'"/>
<field name="customer_tag_ids" widget="many2many_tags" readonly="state != 'draft'"/>
</group>
</page>
<page string="Generated Vouchers" name="generated_vouchers" invisible="state != 'approved'">
<field name="generated_card_ids" mode="list">
<list>
<field name="code"/>
<field name="partner_id"/>
<field name="points"/>
<field name="expiration_date"/>
</list>
</field>
</page>
<page string="Notes" name="notes">
<field name="description" placeholder="Specify details or reason for this request..." readonly="state != 'draft'"/>
</page>
</notebook>
</sheet>
<chatter/>
</form>
</field>
</record>
<record id="loyalty_voucher_generation_request_view_search" model="ir.ui.view">
<field name="name">loyalty.voucher.generation.request.search</field>
<field name="model">loyalty.voucher.generation.request</field>
<field name="arch" type="xml">
<search>
<field name="name"/>
<field name="program_id"/>
<field name="requester_id"/>
<filter string="Draft" name="draft" domain="[('state', '=', 'draft')]"/>
<filter string="Pending" name="pending" domain="[('state', '=', 'pending')]"/>
<filter string="Approved" name="approved" domain="[('state', '=', 'approved')]"/>
<filter string="Rejected" name="rejected" domain="[('state', '=', 'rejected')]"/>
<separator/>
<group>
<filter string="Program" name="group_by_program" context="{'group_by': 'program_id'}"/>
<filter string="Status" name="group_by_state" context="{'group_by': 'state'}"/>
<filter string="Requested By" name="group_by_requester" context="{'group_by': 'requester_id'}"/>
</group>
</search>
</field>
</record>
<record id="action_loyalty_voucher_generation_request" model="ir.actions.act_window">
<field name="name">Voucher Generation Requests</field>
<field name="res_model">loyalty.voucher.generation.request</field>
<field name="view_mode">list,form</field>
<field name="search_view_id" ref="loyalty_voucher_generation_request_view_search"/>
<field name="help" type="html">
<p class="o_view_nocontent_smiling_face">
Create a new Voucher Generation Request.
</p>
<p>
Requests must be approved by the designated approver before the coupons/vouchers are created in the system.
</p>
</field>
</record>
</odoo>

82
views/menu_views.xml Normal file
View File

@ -0,0 +1,82 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<!-- Add Marketing / User to Sales Root Menu -->
<record id="sale.sale_menu_root" model="ir.ui.menu">
<field name="group_ids" eval="[(4, ref('pos_loyalty_marketing_access.group_marketing_user'))]"/>
</record>
<!-- Add Marketing / User to Sales > Products Parent Menu (Catalog) -->
<record id="sale.product_menu_catalog" model="ir.ui.menu">
<field name="group_ids" eval="[(4, ref('pos_loyalty_marketing_access.group_marketing_user'))]"/>
</record>
<!-- Add Marketing / User to Sales > Products Menu -->
<record id="sale.menu_product_template_action" model="ir.ui.menu">
<field name="group_ids" eval="[(4, ref('pos_loyalty_marketing_access.group_marketing_user'))]"/>
</record>
<!-- Add Marketing / User to Sales > Discount & Loyalty -->
<record id="sale_loyalty.menu_discount_loyalty_type_config" model="ir.ui.menu">
<field name="group_ids" eval="[(4, ref('pos_loyalty_marketing_access.group_marketing_user'))]"/>
</record>
<!-- Add Marketing / User to Sales > Gift cards & eWallet -->
<record id="sale_loyalty.menu_gift_ewallet_type_config" model="ir.ui.menu">
<field name="group_ids" eval="[(4, ref('pos_loyalty_marketing_access.group_marketing_user'))]"/>
</record>
<!-- Add Marketing / User to POS Root Menu -->
<record id="point_of_sale.menu_point_root" model="ir.ui.menu">
<field name="group_ids" eval="[(4, ref('pos_loyalty_marketing_access.group_marketing_user'))]"/>
</record>
<!-- Add Marketing / User to POS > Orders Parent Menu -->
<record id="point_of_sale.menu_point_of_sale" model="ir.ui.menu">
<field name="group_ids" eval="[(4, ref('pos_loyalty_marketing_access.group_marketing_user'))]"/>
</record>
<!-- Add Marketing / User to POS > Orders Menu -->
<record id="point_of_sale.menu_point_ofsale" model="ir.ui.menu">
<field name="group_ids" eval="[(4, ref('pos_loyalty_marketing_access.group_marketing_user'))]"/>
</record>
<!-- Add Marketing / User to POS > Payments Menu -->
<record id="point_of_sale.menu_pos_payment" model="ir.ui.menu">
<field name="group_ids" eval="[(4, ref('pos_loyalty_marketing_access.group_marketing_user'))]"/>
</record>
<!-- Add Marketing / User to POS > Products Parent Menu (Catalog) -->
<record id="point_of_sale.pos_config_menu_catalog" model="ir.ui.menu">
<field name="group_ids" eval="[(4, ref('pos_loyalty_marketing_access.group_marketing_user'))]"/>
</record>
<!-- Add Marketing / User to POS > Products Menu -->
<record id="point_of_sale.menu_pos_products" model="ir.ui.menu">
<field name="group_ids" eval="[(4, ref('pos_loyalty_marketing_access.group_marketing_user'))]"/>
</record>
<!-- Add Marketing / User to POS > Discount & Loyalty -->
<record id="pos_loyalty.menu_discount_loyalty_type_config" model="ir.ui.menu">
<field name="group_ids" eval="[(4, ref('pos_loyalty_marketing_access.group_marketing_user'))]"/>
</record>
<!-- Add Marketing / User to POS > Gift cards & eWallet -->
<record id="pos_loyalty.menu_gift_ewallet_type_config" model="ir.ui.menu">
<field name="group_ids" eval="[(4, ref('pos_loyalty_marketing_access.group_marketing_user'))]"/>
</record>
<!-- Create Menu Items for Voucher Generation Requests under Sales and POS -->
<menuitem id="menu_loyalty_voucher_generation_request_sales"
name="Voucher Generation Requests"
parent="sale.product_menu_catalog"
action="action_loyalty_voucher_generation_request"
groups="pos_loyalty_marketing_access.group_marketing_user"
sequence="30"/>
<menuitem id="menu_loyalty_voucher_generation_request_pos"
name="Voucher Generation Requests"
parent="point_of_sale.pos_config_menu_catalog"
action="action_loyalty_voucher_generation_request"
groups="pos_loyalty_marketing_access.group_marketing_user"
sequence="30"/>
</odoo>

View File

@ -0,0 +1,22 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<record id="pos_config_view_form_inherit_marketing" model="ir.ui.view">
<field name="name">pos.config.form.view.inherit.marketing</field>
<field name="model">pos.config</field>
<field name="inherit_id" ref="point_of_sale.pos_config_view_form"/>
<field name="arch" type="xml">
<xpath expr="//notebook" position="inside">
<page string="Marketing Approvals" name="marketing_approvals">
<div class="row mt16 o_settings_container">
<setting string="Marketing Program Approver" help="Authorized user who approves draft marketing/loyalty programs (fallback: Marketing Managers).">
<field name="marketing_program_approver_id"/>
</setting>
<setting string="Voucher Generation Approver" help="Authorized user who approves coupon/voucher generation requests (fallback: Marketing Managers).">
<field name="voucher_generation_approver_id"/>
</setting>
</div>
</page>
</xpath>
</field>
</record>
</odoo>

View File

@ -0,0 +1,22 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<record id="view_company_form_marketing" model="ir.ui.view">
<field name="name">res.company.form.marketing</field>
<field name="model">res.company</field>
<field name="inherit_id" ref="base.view_company_form"/>
<field name="arch" type="xml">
<xpath expr="//notebook" position="inside">
<page string="Marketing Approvals" name="marketing_approvals" groups="pos_loyalty_marketing_access.group_marketing_user">
<group>
<group string="Program Approvals">
<field name="marketing_program_approver_id" options="{'no_create': True, 'no_open': True}"/>
</group>
<group string="Voucher Generation Approvals">
<field name="voucher_generation_approver_id" options="{'no_create': True, 'no_open': True}"/>
</group>
</group>
</page>
</xpath>
</field>
</record>
</odoo>

3
wizards/__init__.py Normal file
View File

@ -0,0 +1,3 @@
# -*- coding: utf-8 -*-
from . import loyalty_generate_wizard

View File

@ -0,0 +1,59 @@
# -*- coding: utf-8 -*-
from odoo import _, api, fields, models
from odoo.exceptions import UserError, ValidationError
class LoyaltyGenerateWizard(models.TransientModel):
_inherit = 'loyalty.generate.wizard'
def generate_coupons(self):
# 1. Validation check for Marketing User
if self.env.user.has_group('pos_loyalty_marketing_access.group_marketing_user') and not self.env.user.has_group('pos_loyalty_marketing_access.group_marketing_manager'):
raise UserError(_("Marketing Users are not authorized to generate vouchers."))
# 2. Intercept and create a Voucher Request instead of direct generation for Marketing group
is_marketing = self.env.user.has_group('pos_loyalty_marketing_access.group_marketing_user') or self.env.user.has_group('pos_loyalty_marketing_access.group_marketing_manager')
if is_marketing:
if any(not wizard.program_id for wizard in self):
raise ValidationError(_("Can not generate coupon, no program is set."))
if any(wizard.coupon_qty <= 0 for wizard in self):
raise ValidationError(_("Invalid quantity."))
requests = self.env['loyalty.voucher.generation.request']
for wizard in self:
req = self.env['loyalty.voucher.generation.request'].create({
'program_id': wizard.program_id.id,
'mode': wizard.mode,
'customer_ids': [(6, 0, wizard.customer_ids.ids)] if wizard.customer_ids else False,
'customer_tag_ids': [(6, 0, wizard.customer_tag_ids.ids)] if wizard.customer_tag_ids else False,
'coupon_qty': wizard.coupon_qty,
'points_granted': wizard.points_granted,
'valid_until': wizard.valid_until,
'description': wizard.description,
'state': 'pending',
})
requests |= req
action = {
'name': _('Voucher Generation Requests'),
'type': 'ir.actions.act_window',
'res_model': 'loyalty.voucher.generation.request',
'view_mode': 'form',
'res_id': requests[0].id,
'target': 'current',
}
return {
'type': 'ir.actions.client',
'tag': 'display_notification',
'params': {
'title': _('Request Submitted'),
'message': _('Your voucher generation request has been submitted and is pending approval.'),
'type': 'warning',
'sticky': True,
'next': action,
}
}
return super().generate_coupons()