commit 5d13d172ad7f2dbfed8f8697636d2ea4d42db94f Author: Suherdy Yacob Date: Tue May 12 13:15:13 2026 +0700 first commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..d1ba00d --- /dev/null +++ b/.gitignore @@ -0,0 +1,8 @@ +__pycache__/ +*.py[cod] +*$py.class +.DS_Store +.idea/ +.vscode/ +*.swp +*.swo diff --git a/README.md b/README.md new file mode 100644 index 0000000..0bc5a61 --- /dev/null +++ b/README.md @@ -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. diff --git a/__init__.py b/__init__.py new file mode 100644 index 0000000..0650744 --- /dev/null +++ b/__init__.py @@ -0,0 +1 @@ +from . import models diff --git a/__manifest__.py b/__manifest__.py new file mode 100644 index 0000000..a1f4c1c --- /dev/null +++ b/__manifest__.py @@ -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', +} diff --git a/models/__init__.py b/models/__init__.py new file mode 100644 index 0000000..3037fd5 --- /dev/null +++ b/models/__init__.py @@ -0,0 +1,2 @@ +from . import pos_order +from . import hr_employee diff --git a/models/hr_employee.py b/models/hr_employee.py new file mode 100644 index 0000000..582d922 --- /dev/null +++ b/models/hr_employee.py @@ -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 diff --git a/models/pos_order.py b/models/pos_order.py new file mode 100644 index 0000000..cff0b76 --- /dev/null +++ b/models/pos_order.py @@ -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 diff --git a/static/src/app/components/order_tabs/order_tabs.xml b/static/src/app/components/order_tabs/order_tabs.xml new file mode 100644 index 0000000..335da2f --- /dev/null +++ b/static/src/app/components/order_tabs/order_tabs.xml @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/static/src/app/models/pos_order.js b/static/src/app/models/pos_order.js new file mode 100644 index 0000000..cded56a --- /dev/null +++ b/static/src/app/models/pos_order.js @@ -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; + }, +}); diff --git a/static/src/app/screens/floor_screen/floor_screen.xml b/static/src/app/screens/floor_screen/floor_screen.xml new file mode 100644 index 0000000..4bcab6e --- /dev/null +++ b/static/src/app/screens/floor_screen/floor_screen.xml @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/static/src/app/screens/product_screen/action_pad/action_pad.js b/static/src/app/screens/product_screen/action_pad/action_pad.js new file mode 100644 index 0000000..0456175 --- /dev/null +++ b/static/src/app/screens/product_screen/action_pad/action_pad.js @@ -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(); + } +}); diff --git a/static/src/app/screens/product_screen/action_pad/action_pad.xml b/static/src/app/screens/product_screen/action_pad/action_pad.xml new file mode 100644 index 0000000..4288f51 --- /dev/null +++ b/static/src/app/screens/product_screen/action_pad/action_pad.xml @@ -0,0 +1,16 @@ + + + + + () => this.onClickPay() + pos.canPay + + + + + + () => this.onClickPay() + !pos.scanning and pos.canPay + + + diff --git a/static/src/app/screens/product_screen/product_screen.js b/static/src/app/screens/product_screen/product_screen.js new file mode 100644 index 0000000..4510c64 --- /dev/null +++ b/static/src/app/screens/product_screen/product_screen.js @@ -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(); + } +}); diff --git a/static/src/app/services/pos_store.js b/static/src/app/services/pos_store.js new file mode 100644 index 0000000..3d76f11 --- /dev/null +++ b/static/src/app/services/pos_store.js @@ -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; + }, +}); diff --git a/views/pos_order_views.xml b/views/pos_order_views.xml new file mode 100644 index 0000000..3a4eccb --- /dev/null +++ b/views/pos_order_views.xml @@ -0,0 +1,14 @@ + + + + pos.order.form.inherit.custom.access + pos.order + + + + + + + + +