first commit
This commit is contained in:
commit
98486e284e
33
.gitignore
vendored
Normal file
33
.gitignore
vendored
Normal 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
100
README.md
Normal 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
4
__init__.py
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
from . import models
|
||||||
|
from . import wizards
|
||||||
36
__manifest__.py
Normal file
36
__manifest__.py
Normal 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
14
data/ir_sequence_data.xml
Normal 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
8
models/__init__.py
Normal 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
70
models/loyalty_program.py
Normal 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
35
models/loyalty_reward.py
Normal 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
|
||||||
163
models/loyalty_voucher_generation_request.py
Normal file
163
models/loyalty_voucher_generation_request.py
Normal 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
60
models/pos_config.py
Normal 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
14
models/sale_order.py
Normal 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')]
|
||||||
13
security/ir.model.access.csv
Normal file
13
security/ir.model.access.csv
Normal 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
|
||||||
|
24
security/pos_loyalty_marketing_security.xml
Normal file
24
security/pos_loyalty_marketing_security.xml
Normal 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>
|
||||||
61
views/loyalty_program_views.xml
Normal file
61
views/loyalty_program_views.xml
Normal 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>
|
||||||
121
views/loyalty_voucher_generation_request_views.xml
Normal file
121
views/loyalty_voucher_generation_request_views.xml
Normal 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
82
views/menu_views.xml
Normal 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>
|
||||||
22
views/pos_config_views.xml
Normal file
22
views/pos_config_views.xml
Normal 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>
|
||||||
22
views/res_company_views.xml
Normal file
22
views/res_company_views.xml
Normal 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
3
wizards/__init__.py
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
from . import loyalty_generate_wizard
|
||||||
59
wizards/loyalty_generate_wizard.py
Normal file
59
wizards/loyalty_generate_wizard.py
Normal 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()
|
||||||
Loading…
Reference in New Issue
Block a user