first commit

This commit is contained in:
Suherdy Yacob 2026-05-12 13:15:13 +07:00
commit 5d13d172ad
15 changed files with 264 additions and 0 deletions

8
.gitignore vendored Normal file
View File

@ -0,0 +1,8 @@
__pycache__/
*.py[cod]
*$py.class
.DS_Store
.idea/
.vscode/
*.swp
*.swo

40
README.md Normal file
View 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
View File

@ -0,0 +1 @@
from . import models

24
__manifest__.py Normal file
View 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
View File

@ -0,0 +1,2 @@
from . import pos_order
from . import hr_employee

10
models/hr_employee.py Normal file
View 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
View 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

View 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>

View 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;
},
});

View 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>

View File

@ -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();
}
});

View File

@ -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>

View 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();
}
});

View 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
View 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>