first commit
This commit is contained in:
commit
5d13d172ad
8
.gitignore
vendored
Normal file
8
.gitignore
vendored
Normal file
@ -0,0 +1,8 @@
|
||||
__pycache__/
|
||||
*.py[cod]
|
||||
*$py.class
|
||||
.DS_Store
|
||||
.idea/
|
||||
.vscode/
|
||||
*.swp
|
||||
*.swo
|
||||
40
README.md
Normal file
40
README.md
Normal file
@ -0,0 +1,40 @@
|
||||
# POS Custom Employee Access
|
||||
|
||||
This Odoo 19 module enhances the security and auditability of the Point of Sale system by enforcing PIN-based authentication for critical workflows and tracking detailed order attribution.
|
||||
|
||||
## Features
|
||||
|
||||
- **Mandatory PIN on Table Selection**: Prevents unauthorized access to tables. Every table selection triggers a PIN prompt to identify the employee taking the order.
|
||||
- **Mandatory PIN on Payment**: Requires PIN authentication before processing payments to ensure the transaction is handled by an authorized employee.
|
||||
- **Role-Based Payment Gating**: Cross-checks employee roles (using `pos_employee_role`) to prevent roles like 'waiter' from processing payments.
|
||||
- **Order Attribution**:
|
||||
- `Order Taker`: Records the employee who first authenticated for the table.
|
||||
- `Cashier`: Records the employee who authenticated for the payment.
|
||||
- **UI Streamlining**: Removes manual "+ New Order" buttons to enforce a table-based workflow.
|
||||
|
||||
## Dependencies
|
||||
|
||||
- `point_of_sale`
|
||||
- `pos_restaurant`
|
||||
- `pos_hr`
|
||||
- `pos_employee_role`
|
||||
|
||||
## Installation
|
||||
|
||||
1. Place the `pos_custom_access` directory in your Odoo `customaddons` folder.
|
||||
2. Restart the Odoo server.
|
||||
3. Update the App List in the Odoo backend.
|
||||
4. Install the `POS Custom Employee Access` module.
|
||||
|
||||
## Configuration
|
||||
|
||||
Ensure that employees have PINs set in their HR Employee profile and that they are assigned appropriate roles if using `pos_employee_role`.
|
||||
|
||||
## Audit Fields
|
||||
|
||||
The following fields are added or modified on the `pos.order` model:
|
||||
- `creator_id`: "Order Taker" - Employee who took the order.
|
||||
- `payer_id`: "Cashier" - Employee who processed the payment.
|
||||
- `employee_id`: Renamed to "Cash Open by" - Employee who opened the cash register (standard `pos_hr` field).
|
||||
|
||||
These fields can be viewed in the POS Order backend list and form views.
|
||||
1
__init__.py
Normal file
1
__init__.py
Normal file
@ -0,0 +1 @@
|
||||
from . import models
|
||||
24
__manifest__.py
Normal file
24
__manifest__.py
Normal file
@ -0,0 +1,24 @@
|
||||
{
|
||||
'name': 'POS Custom Employee Access',
|
||||
'version': '19.0.0.0.1',
|
||||
'category': 'Point of Sale',
|
||||
'summary': 'Enhanced POS access control and employee tracking',
|
||||
'description': """
|
||||
- Remove "+ New Order" button on Floor Screen
|
||||
- Enforce PIN entry on table selection
|
||||
- Enforce PIN entry on payment with role check
|
||||
- Record order creator and payer employees
|
||||
""",
|
||||
'author': 'Suherdy Yacob',
|
||||
'depends': ['pos_restaurant', 'pos_hr', 'pos_employee_role'],
|
||||
'data': [
|
||||
'views/pos_order_views.xml',
|
||||
],
|
||||
'assets': {
|
||||
'point_of_sale._assets_pos': [
|
||||
'pos_custom_access/static/src/app/**/*',
|
||||
],
|
||||
},
|
||||
'installable': True,
|
||||
'license': 'LGPL-3',
|
||||
}
|
||||
2
models/__init__.py
Normal file
2
models/__init__.py
Normal file
@ -0,0 +1,2 @@
|
||||
from . import pos_order
|
||||
from . import hr_employee
|
||||
10
models/hr_employee.py
Normal file
10
models/hr_employee.py
Normal file
@ -0,0 +1,10 @@
|
||||
from odoo import api, models
|
||||
|
||||
class HrEmployee(models.Model):
|
||||
_inherit = 'hr.employee'
|
||||
|
||||
@api.model
|
||||
def _load_pos_data_fields(self, config):
|
||||
fields = super()._load_pos_data_fields(config)
|
||||
fields.append('pos_role')
|
||||
return fields
|
||||
19
models/pos_order.py
Normal file
19
models/pos_order.py
Normal file
@ -0,0 +1,19 @@
|
||||
from odoo import models, fields
|
||||
|
||||
class PosOrder(models.Model):
|
||||
_inherit = 'pos.order'
|
||||
|
||||
creator_id = fields.Many2one('hr.employee', string='Order Taker', help='Employee who took the order')
|
||||
payer_id = fields.Many2one('hr.employee', string='Cashier', help='Employee who processed the payment')
|
||||
employee_id = fields.Many2one('hr.employee', string='Cash Open by', help='Employee who opened the cash register')
|
||||
|
||||
def _export_for_ui(self, order):
|
||||
result = super()._export_for_ui(order)
|
||||
result['creator_id'] = order.creator_id.id
|
||||
result['payer_id'] = order.payer_id.id
|
||||
return result
|
||||
|
||||
def _get_fields_for_draft_order(self):
|
||||
fields = super()._get_fields_for_draft_order()
|
||||
fields.extend(['creator_id', 'payer_id'])
|
||||
return fields
|
||||
8
static/src/app/components/order_tabs/order_tabs.xml
Normal file
8
static/src/app/components/order_tabs/order_tabs.xml
Normal file
@ -0,0 +1,8 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<templates id="template" xml:space="preserve">
|
||||
<t t-name="pos_custom_access.OrderTabs" t-inherit="point_of_sale.OrderTabs" t-inherit-mode="extension">
|
||||
<xpath expr="//ListContainer" position="attributes">
|
||||
<attribute name="onClickPlus"></attribute>
|
||||
</xpath>
|
||||
</t>
|
||||
</templates>
|
||||
16
static/src/app/models/pos_order.js
Normal file
16
static/src/app/models/pos_order.js
Normal file
@ -0,0 +1,16 @@
|
||||
import { patch } from "@web/core/utils/patch";
|
||||
import { PosOrder } from "@point_of_sale/app/models/pos_order";
|
||||
|
||||
patch(PosOrder.prototype, {
|
||||
setup(vals) {
|
||||
super.setup(...arguments);
|
||||
this.creator_id = vals.creator_id || false;
|
||||
this.payer_id = vals.payer_id || false;
|
||||
},
|
||||
serializeForORM() {
|
||||
const result = super.serializeForORM(...arguments);
|
||||
result.creator_id = this.creator_id;
|
||||
result.payer_id = this.payer_id;
|
||||
return result;
|
||||
},
|
||||
});
|
||||
8
static/src/app/screens/floor_screen/floor_screen.xml
Normal file
8
static/src/app/screens/floor_screen/floor_screen.xml
Normal file
@ -0,0 +1,8 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<templates id="template" xml:space="preserve">
|
||||
<t t-name="pos_custom_access.FloorScreen" t-inherit="pos_restaurant.FloorScreen" t-inherit-mode="extension">
|
||||
<xpath expr="//button[hasclass('new-order')]" position="replace">
|
||||
<!-- Removed "+ New Order" button -->
|
||||
</xpath>
|
||||
</t>
|
||||
</templates>
|
||||
@ -0,0 +1,15 @@
|
||||
import { ActionpadWidget } from "@point_of_sale/app/screens/product_screen/action_pad/action_pad";
|
||||
import { patch } from "@web/core/utils/patch";
|
||||
|
||||
patch(ActionpadWidget.prototype, {
|
||||
async onClickPay() {
|
||||
if (this.currentOrder.lines.length === 0) {
|
||||
return;
|
||||
}
|
||||
const cashier = this.pos.getCashier();
|
||||
if (cashier) {
|
||||
this.currentOrder.payer_id = cashier.id;
|
||||
}
|
||||
await this.pos.pay();
|
||||
}
|
||||
});
|
||||
@ -0,0 +1,16 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<templates id="template" xml:space="preserve">
|
||||
<t t-name="pos_custom_access.ActionpadWidget" t-inherit="point_of_sale.ActionpadWidget" t-inherit-mode="extension">
|
||||
<xpath expr="//button[hasclass('pay-order-button')]" position="attributes">
|
||||
<attribute name="t-on-click">() => this.onClickPay()</attribute>
|
||||
<attribute name="t-if">pos.canPay</attribute>
|
||||
</xpath>
|
||||
</t>
|
||||
|
||||
<t t-name="pos_custom_access.ProductScreen" t-inherit="point_of_sale.ProductScreen" t-inherit-mode="extension">
|
||||
<xpath expr="//button[hasclass('pay-button')]" position="attributes">
|
||||
<attribute name="t-on-click">() => this.onClickPay()</attribute>
|
||||
<attribute name="t-if">!pos.scanning and pos.canPay</attribute>
|
||||
</xpath>
|
||||
</t>
|
||||
</templates>
|
||||
15
static/src/app/screens/product_screen/product_screen.js
Normal file
15
static/src/app/screens/product_screen/product_screen.js
Normal file
@ -0,0 +1,15 @@
|
||||
import { ProductScreen } from "@point_of_sale/app/screens/product_screen/product_screen";
|
||||
import { patch } from "@web/core/utils/patch";
|
||||
|
||||
patch(ProductScreen.prototype, {
|
||||
async onClickPay() {
|
||||
if (this.currentOrder.lines.length === 0) {
|
||||
return;
|
||||
}
|
||||
const cashier = this.pos.getCashier();
|
||||
if (cashier) {
|
||||
this.currentOrder.payer_id = cashier.id;
|
||||
}
|
||||
await this.pos.pay();
|
||||
}
|
||||
});
|
||||
68
static/src/app/services/pos_store.js
Normal file
68
static/src/app/services/pos_store.js
Normal file
@ -0,0 +1,68 @@
|
||||
/* global Sha1 */
|
||||
import { patch } from "@web/core/utils/patch";
|
||||
import { PosStore } from "@point_of_sale/app/services/pos_store";
|
||||
import { _t } from "@web/core/l10n/translation";
|
||||
import { NumberPopup } from "@point_of_sale/app/components/popups/number_popup/number_popup";
|
||||
import { makeAwaitable } from "@point_of_sale/app/utils/make_awaitable_dialog";
|
||||
|
||||
patch(PosStore.prototype, {
|
||||
async setTableFromUi(table, orderUuid = null) {
|
||||
if (!this.config.module_pos_restaurant || this.isOrderTransferMode) {
|
||||
return super.setTableFromUi(...arguments);
|
||||
}
|
||||
|
||||
const cashier = await this._selectCashierByPin();
|
||||
if (!cashier) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.setCashier(cashier);
|
||||
await super.setTableFromUi(...arguments);
|
||||
|
||||
const order = this.getOrder();
|
||||
if (order && !order.creator_id) {
|
||||
order.creator_id = cashier.id;
|
||||
}
|
||||
},
|
||||
|
||||
async _selectCashierByPin() {
|
||||
if (!this.config.module_pos_hr) {
|
||||
return this.getCashier();
|
||||
}
|
||||
|
||||
const inputPin = await makeAwaitable(this.dialog, NumberPopup, {
|
||||
formatDisplayedValue: (x) => x.replace(/./g, "•"),
|
||||
title: _t("Enter PIN"),
|
||||
});
|
||||
|
||||
if (!inputPin) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const hashedPin = Sha1.hash(inputPin);
|
||||
const allEmployees = this.models["hr.employee"].getAll();
|
||||
const employee = allEmployees.find(emp => emp._pin === hashedPin);
|
||||
|
||||
if (!employee) {
|
||||
this.notification.add(_t("Invalid PIN"), {
|
||||
type: "danger",
|
||||
});
|
||||
return false;
|
||||
}
|
||||
|
||||
return employee;
|
||||
},
|
||||
|
||||
get canPay() {
|
||||
const cashier = this.getCashier();
|
||||
if (!cashier) {
|
||||
return false;
|
||||
}
|
||||
// If pos_employee_role is installed, check role
|
||||
if (cashier.pos_role) {
|
||||
return ['cashier', 'outlet_manager', 'area_manager'].includes(cashier.pos_role);
|
||||
}
|
||||
// Default to true if no role system
|
||||
return true;
|
||||
},
|
||||
});
|
||||
14
views/pos_order_views.xml
Normal file
14
views/pos_order_views.xml
Normal file
@ -0,0 +1,14 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<record id="view_pos_pos_form_inherit_custom_access" model="ir.ui.view">
|
||||
<field name="name">pos.order.form.inherit.custom.access</field>
|
||||
<field name="model">pos.order</field>
|
||||
<field name="inherit_id" ref="point_of_sale.view_pos_pos_form"/>
|
||||
<field name="arch" type="xml">
|
||||
<xpath expr="//field[@name='user_id']" position="after">
|
||||
<field name="creator_id"/>
|
||||
<field name="payer_id"/>
|
||||
</xpath>
|
||||
</field>
|
||||
</record>
|
||||
</odoo>
|
||||
Loading…
Reference in New Issue
Block a user