Initial commit: Pos packages

This commit is contained in:
Ernad Husremovic 2025-08-29 15:20:50 +02:00
commit 95dfb9edb0
1301 changed files with 264148 additions and 0 deletions

View file

@ -0,0 +1,127 @@
.pos .login-overlay{
position: fixed;
left: 0;
top: 0;
bottom: 0;
right: 0;
width: 100%;
height:100%;
z-index:1000;
background: linear-gradient(to right bottom, #77717e, #c9a8a9);
}
.pos .login-overlay:before {
content: '';
background-image: url(../../img/login-bg-overlay.svg);
background-color: rgba(0, 0, 0, 0.3);
position: fixed;
left: 0;
top: 0;
bottom: 0;
right: 0;
width: 100%;
height:100%;
}
.pos .screen-login{
position: absolute;
top: 0; left: 0; right: 0; bottom: 0;
margin: auto;
max-width: 550px;
width:100%;
height:300px;
text-align:center;
font-size:20px;
font-weight:bold;
background-color: #F0EEEE;
border-radius: 3px;
z-index:1200;
font-family: 'Lato';
}
.pos .login-title{
height: 40%;
vertical-align: middle;
line-height: 5;
font-size: larger;
}
.pos .login-body{
display: flex;
justify-content: center;
height:37%;
}
.pos .login-footer{
height:18%;
}
.pos .login-element{
float: left;
width: 40%;
height: 60%;
}
.pos .login-barcode-img{
width: 80px;
height: 55px;
background: white;
border: 0px;
}
.pos .login-barcode-text{
color: #999999;
font-size: 13px;
padding-top: 0.2em;
}
.pos .login-or{
font-size: 15px;
font-style: italic;
float: left;
width: 10%;
height: 100%;
line-height: 5;
}
.pos .login-button{
font-size: initial;
height: 100%;
color: #555555;
border-radius: 5px;
}
@media screen and (max-width: 576px) {
.pos .screen-login {
font-family: 'Lato', sans-serif;
width: 100%;
height: 100%;
text-align: center;
background-color: #fff;
overflow: hidden;
}
.pos .login-body {
flex-direction: column;
align-items: center;
}
.pos .login-button {
color: #fff;
background-color: #00A09D;
border-color: #00A09D;
height: 38px;
}
.pos .login-barcode-text {
color: #adb5bd;
margin-top: 8px;
margin-bottom: 0;
font-size: 15px;
font-weight: 400;
line-height: 1.2;
}
.pos .login-element .o_barcode_mobile_container .o_mobile_barcode {
top:0;
height: 55px;
}
}
.pos .pos-rightheader .header-button.lock-button {
font-size: 20px;
color: rgb(94, 185, 55);
transition: all 200ms ease-in-out;
width: 18px;
}
.pos .pos-rightheader .header-button.lock-button:hover {
color: rgb(197, 52, 0);
}

View file

@ -0,0 +1,31 @@
odoo.define('pos_hr.CashierName', function (require) {
'use strict';
const CashierName = require('point_of_sale.CashierName');
const Registries = require('point_of_sale.Registries');
const SelectCashierMixin = require('pos_hr.SelectCashierMixin');
const { useBarcodeReader } = require('point_of_sale.custom_hooks');
const PosHrCashierName = (CashierName) =>
class extends SelectCashierMixin(CashierName) {
setup() {
super.setup();
useBarcodeReader({ cashier: this.barcodeCashierAction });
}
//@Override
get avatar() {
if (this.env.pos.config.module_pos_hr) {
const cashier = this.env.pos.get_cashier();
if (!(cashier && cashier.id)) {
return '';
}
return `/web/image/hr.employee.public/${cashier.id}/avatar_128`;
}
return super.avatar;
}
};
Registries.Component.extend(CashierName, PosHrCashierName);
return CashierName;
});

View file

@ -0,0 +1,30 @@
odoo.define('pos_hr.chrome', function (require) {
'use strict';
const Chrome = require('point_of_sale.Chrome');
const Registries = require('point_of_sale.Registries');
const PosHrChrome = (Chrome) =>
class extends Chrome {
async start() {
await super.start();
if (this.env.pos.config.module_pos_hr) this.showTempScreen('LoginScreen');
}
get headerButtonIsShown() {
return !this.env.pos.config.module_pos_hr || this.env.pos.get_cashier().role == 'manager' || this.env.pos.get_cashier_user_id() === this.env.pos.user.id;
}
showCashMoveButton() {
return super.showCashMoveButton() && (!this.env.pos.cashier || this.env.pos.cashier.role == 'manager');
}
shouldShowCashControl() {
if (this.env.pos.config.module_pos_hr){
return super.shouldShowCashControl() && this.env.pos.hasLoggedIn;
}
return super.shouldShowCashControl();
}
};
Registries.Component.extend(Chrome, PosHrChrome);
return Chrome;
});

View file

@ -0,0 +1,28 @@
odoo.define('point_of_sale.HeaderLockButton', function(require) {
'use strict';
const PosComponent = require('point_of_sale.PosComponent');
const Registries = require('point_of_sale.Registries');
const { useState } = owl;
class HeaderLockButton extends PosComponent {
setup() {
super.setup();
this.state = useState({ isUnlockIcon: true, title: 'Unlocked' });
}
async showLoginScreen() {
this.env.pos.reset_cashier();
await this.showTempScreen('LoginScreen');
}
onMouseOver(isMouseOver) {
this.state.isUnlockIcon = !isMouseOver;
this.state.title = isMouseOver ? 'Lock' : 'Unlocked';
}
}
HeaderLockButton.template = "HeaderLockButton";
Registries.Component.add(HeaderLockButton);
return HeaderLockButton;
});

View file

@ -0,0 +1,43 @@
odoo.define('pos_hr.LoginScreen', function (require) {
'use strict';
const PosComponent = require('point_of_sale.PosComponent');
const Registries = require('point_of_sale.Registries');
const SelectCashierMixin = require('pos_hr.SelectCashierMixin');
const { useBarcodeReader } = require('point_of_sale.custom_hooks');
class LoginScreen extends SelectCashierMixin(PosComponent) {
setup() {
super.setup();
useBarcodeReader({cashier: this.barcodeCashierAction}, true);
}
async selectCashier() {
if (await super.selectCashier()) {
this.back();
}
}
async barcodeCashierAction(code) {
if (await super.barcodeCashierAction(code) && this.env.pos.get_cashier().id) {
this.back();
}
}
back() {
this.props.resolve({ confirmed: false, payload: false });
this.trigger('close-temp-screen');
this.env.pos.hasLoggedIn = true;
this.env.posbus.trigger('start-cash-control');
}
confirm() {
this.props.resolve({ confirmed: true, payload: true });
this.trigger('close-temp-screen');
}
get shopName() {
return this.env.pos.config.name;
}
}
LoginScreen.template = 'LoginScreen';
Registries.Component.add(LoginScreen);
return LoginScreen;
});

View file

@ -0,0 +1,18 @@
odoo.define('pos_hr.PaymentScreen', function (require) {
'use strict';
const PaymentScreen = require('point_of_sale.PaymentScreen');
const Registries = require('point_of_sale.Registries');
const PosHrPaymentScreen = (PaymentScreen_) =>
class extends PaymentScreen_ {
async _finalizeValidation() {
this.currentOrder.cashier = this.env.pos.get_cashier();
await super._finalizeValidation();
}
};
Registries.Component.extend(PaymentScreen, PosHrPaymentScreen);
return PaymentScreen;
});

View file

@ -0,0 +1,71 @@
/* global Sha1 */
odoo.define('pos_hr.SelectCashierMixin', function (require) {
'use strict';
const SelectCashierMixin = (PosComponent) => class ComponentWithSelectCashierMixin extends PosComponent {
async askPin(employee) {
const { confirmed, payload: inputPin } = await this.showPopup('NumberPopup', {
isPassword: true,
title: this.env._t('Password ?'),
startingValue: null,
});
if (!confirmed) return;
if (employee.pin === Sha1.hash(inputPin)) {
return employee;
} else {
await this.showPopup('ErrorPopup', {
title: this.env._t('Incorrect Password'),
});
return;
}
}
/**
* Select a cashier, the returning value will either be an object or nothing (undefined)
*/
async selectCashier() {
if (this.env.pos.config.module_pos_hr) {
const employeesList = this.env.pos.employees
.filter((employee) => employee.id !== this.env.pos.get_cashier().id)
.map((employee) => {
return {
id: employee.id,
item: employee,
label: employee.name,
isSelected: false,
};
});
let {confirmed, payload: employee} = await this.showPopup('SelectionPopup', {
title: this.env._t('Change Cashier'),
list: employeesList,
});
if (!confirmed) {
return;
}
if (employee && employee.pin) {
employee = await this.askPin(employee);
}
if (employee) {
this.env.pos.set_cashier(employee);
}
return employee;
}
}
async barcodeCashierAction(code) {
const employee = this.env.pos.employees.find(
(emp) => emp.barcode === Sha1.hash(code.code)
);
if (employee && employee !== this.env.pos.get_cashier() && (!employee.pin || (await this.askPin(employee)))) {
this.env.pos.set_cashier(employee);
}
return employee;
}
}
return SelectCashierMixin;
});

View file

@ -0,0 +1,82 @@
odoo.define('pos_hr.employees', function (require) {
"use strict";
var { PosGlobalState, Order } = require('point_of_sale.models');
const Registries = require('point_of_sale.Registries');
const PosHrPosGlobalState = (PosGlobalState) => class PosHrPosGlobalState extends PosGlobalState {
async _processData(loadedData) {
await super._processData(...arguments);
if (this.config.module_pos_hr) {
this.employees = loadedData['hr.employee'];
this.employee_by_id = loadedData['employee_by_id'];
this.reset_cashier();
}
}
async after_load_server_data() {
await super.after_load_server_data(...arguments);
if (this.config.module_pos_hr) {
this.hasLoggedIn = !this.config.module_pos_hr;
}
}
reset_cashier() {
this.cashier = {name: null, id: null, barcode: null, user_id: null, pin: null, role: null};
}
set_cashier(employee) {
this.cashier = employee;
const selectedOrder = this.get_order();
if (selectedOrder && !selectedOrder.get_orderlines().length) {
// Order without lines can be considered to be un-owned by any employee.
// We set the cashier on that order to the currently set employee.
selectedOrder.cashier = employee;
}
if (!this.cashierHasPriceControlRights() && this.numpadMode === 'price') {
this.numpadMode = 'quantity';
}
}
/**{name: null, id: null, barcode: null, user_id:null, pin:null}
* If pos_hr is activated, return {name: string, id: int, barcode: string, pin: string, user_id: int}
* @returns {null|*}
*/
get_cashier() {
if (this.config.module_pos_hr) {
return this.cashier;
}
return super.get_cashier();
}
get_cashier_user_id() {
if (this.config.module_pos_hr) {
return this.cashier.user_id ? this.cashier.user_id : null;
}
return super.get_cashier_user_id();
}
}
Registries.Model.extend(PosGlobalState, PosHrPosGlobalState);
const PosHrOrder = (Order) => class PosHrOrder extends Order {
constructor(obj, options) {
super(...arguments);
if (!options.json && this.pos.config.module_pos_hr) {
this.cashier = this.pos.get_cashier();
}
}
init_from_JSON(json) {
super.init_from_JSON(...arguments);
if (this.pos.config.module_pos_hr && json.employee_id) {
this.cashier = this.pos.employee_by_id[json.employee_id];
}
}
export_as_JSON() {
const json = super.export_as_JSON(...arguments);
if (this.pos.config.module_pos_hr) {
json.employee_id = this.cashier ? this.cashier.id : false;
}
return json;
}
}
Registries.Model.extend(Order, PosHrOrder);
});

View file

@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<templates id="template" xml:space="preserve">
<t t-name="CashierName" t-inherit="point_of_sale.CashierName" t-inherit-mode="extension" owl="1">
<xpath expr="//div" position="attributes">
<attribute name="t-on-click">selectCashier</attribute>
</xpath>
</t>
</templates>

View file

@ -0,0 +1,11 @@
<?xml version="1.0" encoding="UTF-8"?>
<templates id="template" xml:space="preserve">
<t t-name="Chrome" t-inherit="point_of_sale.Chrome" t-inherit-mode="extension" owl="1">
<xpath expr="//HeaderButton" position="replace">
<HeaderLockButton t-if="env.pos.config.module_pos_hr" />
<HeaderButton t-if="headerButtonIsShown" />
</xpath>
</t>
</templates>

View file

@ -0,0 +1,15 @@
<?xml version="1.0" encoding="UTF-8"?>
<templates id="template" xml:space="preserve">
<t t-name="HeaderLockButton" owl="1">
<div class="header-button lock-button" t-on-mouseover="() => this.onMouseOver(true)"
t-on-click="showLoginScreen" t-on-mouseout="() => this.onMouseOver(false)">
<span class="lock-button">
<i class="fa"
t-att-class="{ 'fa-unlock': state.isUnlockIcon, 'fa-lock': !state.isUnlockIcon }"
role="img" t-att-aria-label="state.title" t-att-title="state.title"></i>
</span>
</div>
</t>
</templates>

View file

@ -0,0 +1,24 @@
<?xml version="1.0" encoding="UTF-8"?>
<templates id="template" xml:space="preserve">
<t t-name="LoginScreen" owl="1">
<div class="login-overlay">
<div class="screen-login">
<div class="login-title"><small>Log in to </small>
<t t-esc="shopName" />
</div>
<div class="login-body">
<span class="login-element">
<img class="login-barcode-img"
src="/point_of_sale/static/img/barcode.png" />
<div class="login-barcode-text">Scan your badge</div>
</span>
<span class="login-or">or</span>
<span class="login-element">
<button class="login-button select-cashier"
t-on-click="selectCashier">Select Cashier</button>
</span>
</div>
</div>
</div>
</t>
</templates>