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