[REF] hr_attendance: convert legacy client actions to owl
The goal of this commit is to convert the last legacy client actions from
/hr_attendance to owl.
Client actions:
hr_attendance_greeting_message
hr_attendance_kiosk_confirm
hr_attendance_kiosk_mode
hr_attendance_my_attendances
closes odoo/odoo#110095
Taskid: 3138068
Related: odoo/enterprise#35884
Related: odoo/upgrade#4296
Signed-off-by: Adrien Dieudonné (adr) <adr@odoo.com>
This commit is contained in:
@@ -0,0 +1,42 @@
|
||||
/** @odoo-module **/
|
||||
|
||||
import { isBarcodeScannerSupported, scanBarcode } from "@web/webclient/barcode/barcode_scanner";
|
||||
import { Component } from "@odoo/owl";
|
||||
import { useService } from "@web/core/utils/hooks";
|
||||
|
||||
export class BarcodeScanner extends Component {
|
||||
setup() {
|
||||
this.notification = useService("notification");
|
||||
this.isBarcodeScannerSupported = isBarcodeScannerSupported();
|
||||
}
|
||||
|
||||
get facingMode() {
|
||||
return "environment";
|
||||
}
|
||||
|
||||
async openMobileScanner() {
|
||||
let error = null;
|
||||
let barcode = null;
|
||||
try {
|
||||
barcode = await scanBarcode(this.facingMode);
|
||||
} catch (err) {
|
||||
error = err.error.message;
|
||||
}
|
||||
|
||||
if (barcode) {
|
||||
this.props.onBarcodeScanned(barcode);
|
||||
if ("vibrate" in window.navigator) {
|
||||
window.navigator.vibrate(100);
|
||||
}
|
||||
} else {
|
||||
this.notification.add(error || this.env._t("Please, Scan again !"), {
|
||||
type: "warning",
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
BarcodeScanner.template = "barcodes.BarcodeScanner";
|
||||
BarcodeScanner.props = {
|
||||
onBarcodeScanned: { type: Function },
|
||||
};
|
||||
@@ -0,0 +1,66 @@
|
||||
.o_barcode_mobile_container {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
font-family: 'Lato', sans-serif;
|
||||
font-size: 1.08333333rem;
|
||||
font-weight: 400;
|
||||
line-height: 1.5;
|
||||
color: #fff;
|
||||
|
||||
img, .o_mobile_barcode {
|
||||
width: 115px;
|
||||
height: 60px;
|
||||
}
|
||||
|
||||
.o_mobile_barcode {
|
||||
width: 100%;
|
||||
bottom: 0;
|
||||
position: absolute;
|
||||
opacity: 0.75;
|
||||
padding-top: 5px;
|
||||
|
||||
.o_barcode_mobile_camera {
|
||||
margin: 5px;
|
||||
font-size: 1em;
|
||||
}
|
||||
|
||||
div {
|
||||
font-size: 12px;
|
||||
}
|
||||
}
|
||||
|
||||
.o_barcode_laser {
|
||||
// avoid dependencies to web file in pos
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: -15px;
|
||||
bottom: auto;
|
||||
right: -15px;
|
||||
|
||||
height: 5px;
|
||||
background: rgba(red, 0.6);
|
||||
box-shadow: 0 1px 10px 1px rgba(red, 0.8);
|
||||
animation: o_barcode_scanner_intro 1s cubic-bezier(0.6, -0.28, 0.735, 0.045) 0.4s;
|
||||
}
|
||||
|
||||
@keyframes o_barcode_scanner_intro {
|
||||
25% {
|
||||
top: 75%;
|
||||
}
|
||||
50% {
|
||||
top: 0;
|
||||
}
|
||||
75% {
|
||||
top: 100%;
|
||||
}
|
||||
100% {
|
||||
top: 50%;
|
||||
}
|
||||
}
|
||||
}
|
||||
@media screen and (max-width: 576px) {
|
||||
.o_event_barcode_bg.o_home_menu_background {
|
||||
background: white;
|
||||
height: 100%;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<templates xml:space="preserve">
|
||||
|
||||
<t t-name="barcodes.BarcodeScanner" owl="1">
|
||||
<div class="o_barcode_mobile_container">
|
||||
<a t-if="isBarcodeScannerSupported" role="button" class="btn btn-primary o_mobile_barcode" t-on-click="openMobileScanner">
|
||||
<i class="fa fa-camera fa-2x o_barcode_mobile_camera"/>
|
||||
Tap to scan
|
||||
</a>
|
||||
<img src="/barcodes/static/img/barcode.png" alt="Barcode"/>
|
||||
<span class="o_barcode_laser"/>
|
||||
</div>
|
||||
</t>
|
||||
|
||||
</templates>
|
||||
@@ -35,10 +35,9 @@ actions(Check in/Check out) performed by them.
|
||||
'assets': {
|
||||
'web.assets_backend': [
|
||||
'hr_attendance/static/src/**/*',
|
||||
'hr_attendance/static/src/xml/**/*',
|
||||
],
|
||||
'web.qunit_suite_tests': [
|
||||
'hr_attendance/static/tests/hr_attendance_tests.js',
|
||||
'hr_attendance/static/tests/client_action_tests.js',
|
||||
],
|
||||
},
|
||||
'license': 'LGPL-3',
|
||||
|
||||
@@ -0,0 +1,226 @@
|
||||
/** @odoo-module **/
|
||||
|
||||
import { CardLayout } from "@hr_attendance/components/card_layout/card_layout";
|
||||
import { Component, onWillStart, onWillUnmount, useState } from "@odoo/owl";
|
||||
import { deserializeDateTime } from "@web/core/l10n/dates";
|
||||
import { registry } from "@web/core/registry";
|
||||
import { useBus, useService } from "@web/core/utils/hooks";
|
||||
import { sprintf } from "@web/core/utils/strings";
|
||||
|
||||
const { Duration } = luxon;
|
||||
|
||||
export class GreetingMessage extends Component {
|
||||
setup() {
|
||||
const { action } = this.props;
|
||||
|
||||
this.actionService = useService("action");
|
||||
this.notificationService = useService("notification");
|
||||
this.orm = useService("orm");
|
||||
this.user = useService("user");
|
||||
|
||||
this.kioskDelay = action.kiosk_delay;
|
||||
this.state = useState({
|
||||
message: "",
|
||||
randomMessage: "",
|
||||
showWarningMessage: false,
|
||||
});
|
||||
|
||||
onWillStart(this.onWillStart);
|
||||
onWillUnmount(() => this.return_to_main_menu && clearTimeout(this.return_to_main_menu));
|
||||
|
||||
let activeBarcode = true;
|
||||
if (!this.props.action.attendance) {
|
||||
activeBarcode = false;
|
||||
return;
|
||||
}
|
||||
|
||||
this.next_action =
|
||||
action.next_action || "hr_attendance.hr_attendance_action_my_attendances";
|
||||
// no listening to barcode scans if we aren't coming from the kiosk mode (and thus not going back to it with next_action)
|
||||
if (
|
||||
this.next_action !== "hr_attendance.hr_attendance_action_kiosk_mode" &&
|
||||
this.next_action.tag !== "hr_attendance_kiosk_mode"
|
||||
) {
|
||||
activeBarcode = false;
|
||||
}
|
||||
|
||||
this.attendance = action.attendance;
|
||||
// We receive the check in/out times in UTC
|
||||
// This widget only deals with display, which should be in browser's TimeZone
|
||||
this.attendance.check_in =
|
||||
this.attendance.check_in && deserializeDateTime(this.attendance.check_in);
|
||||
this.attendance.check_out =
|
||||
this.attendance.check_out && deserializeDateTime(this.attendance.check_out);
|
||||
this.previous_attendance_change_date =
|
||||
action.previous_attendance_change_date &&
|
||||
deserializeDateTime(action.previous_attendance_change_date);
|
||||
|
||||
const formatDateTime = registry.category("formatters").get("datetime");
|
||||
this.attendance.check_in_time =
|
||||
this.attendance.check_in && formatDateTime(this.attendance.check_in);
|
||||
this.attendance.check_out_time =
|
||||
this.attendance.check_out && formatDateTime(this.attendance.check_out);
|
||||
|
||||
const formatFloatTime = registry.category("formatters").get("float_time");
|
||||
// extra hours amount displayed in the greeting message template.
|
||||
this.total_overtime_float = action.total_overtime; // Used for comparison in template
|
||||
this.total_overtime = formatFloatTime(this.total_overtime_float);
|
||||
this.today_overtime_float = action.overtime_today;
|
||||
this.today_overtime = formatFloatTime(this.today_overtime_float);
|
||||
|
||||
if (action.hours_today) {
|
||||
const duration = Duration.fromObject({ hours: action.hours_today })
|
||||
.toFormat("hh-mm")
|
||||
.split("-");
|
||||
|
||||
this.hours_today = sprintf(this.env._t("%(hours)s hours, %(minutes)s minutes"), {
|
||||
hours: duration[0],
|
||||
minutes: duration[1],
|
||||
});
|
||||
}
|
||||
|
||||
this.employee_name = action.employee_name;
|
||||
this.attendanceBarcode = action.barcode;
|
||||
|
||||
const barcode = useService("barcode");
|
||||
this.lockScanner = false;
|
||||
if (activeBarcode) {
|
||||
useBus(barcode.bus, "barcode_scanned", this.onBarcodeScanned.bind(this));
|
||||
}
|
||||
}
|
||||
|
||||
async onBarcodeScanned(ev) {
|
||||
if (this.lockScanner) {
|
||||
return;
|
||||
}
|
||||
const { barcode } = ev.detail;
|
||||
if (this.attendanceBarcode !== barcode) {
|
||||
this.lockScanner = true;
|
||||
if (this.return_to_main_menu) {
|
||||
// in case of multiple scans in the greeting message view, delete the timer, a new one will be created.
|
||||
clearTimeout(this.return_to_main_menu);
|
||||
}
|
||||
|
||||
const result = await this.orm.call("hr.employee", "attendance_scan", [barcode]);
|
||||
if (result.action) {
|
||||
this.actionService.doAction(result.action);
|
||||
} else if (result.warning) {
|
||||
this.notificationService.add(result.warning, { type: "danger" });
|
||||
this.setTimeoutNextAction();
|
||||
this.lockScanner = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onClickDismiss() {
|
||||
this.actionService.doAction(this.next_action, { clearBreadcrumbs: true });
|
||||
}
|
||||
|
||||
async onWillStart() {
|
||||
// if no correct action given (due to an erroneous back or refresh from the browser), we set the dismiss button to return
|
||||
// to the (likely) appropriate menu, according to the user access rights
|
||||
if (!this.attendance) {
|
||||
const hasGroup = await this.user.hasGroup("hr_attendance.group_hr_attendance_user");
|
||||
if (hasGroup) {
|
||||
this.next_action = "hr_attendance.hr_attendance_action_kiosk_mode";
|
||||
} else {
|
||||
this.next_action = "hr_attendance.hr_attendance_action_my_attendances";
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
this.setTimeoutNextAction();
|
||||
this.attendance.check_out ? this.setFarewellMessage() : this.setWelcomeMessage();
|
||||
}
|
||||
|
||||
setWelcomeMessage() {
|
||||
const { _t } = this.env;
|
||||
const now = this.attendance.check_in;
|
||||
if (now.hour < 5) {
|
||||
this.state.message = _t("Good night");
|
||||
} else if (now.hour < 12) {
|
||||
if (now.hour < 8 && Math.random() < 0.3) {
|
||||
if (Math.random() < 0.75) {
|
||||
this.state.message = _t("The early bird catches the worm");
|
||||
} else {
|
||||
this.state.message = _t("First come, first served");
|
||||
}
|
||||
} else {
|
||||
this.state.message = _t("Good morning");
|
||||
}
|
||||
} else if (now.hour < 17) {
|
||||
this.state.message = _t("Good afternoon");
|
||||
} else if (now.hour < 23) {
|
||||
this.state.message = _t("Good evening");
|
||||
} else {
|
||||
this.state.message = _t("Good night");
|
||||
}
|
||||
if (this.previous_attendance_change_date) {
|
||||
const last_check_out_date = this.previous_attendance_change_date;
|
||||
if (now - last_check_out_date > 24 * 7 * 60 * 60 * 1000) {
|
||||
this.state.randomMessage = _t("Glad to have you back, it's been a while!");
|
||||
} else {
|
||||
if (Math.random() < 0.02) {
|
||||
this.state.randomMessage = _t(
|
||||
"If a job is worth doing, it is worth doing well!"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
setFarewellMessage() {
|
||||
const { _t } = this.env;
|
||||
const now = this.attendance.check_out;
|
||||
|
||||
if (this.previous_attendance_change_date) {
|
||||
const last_check_in_date = this.previous_attendance_change_date;
|
||||
if (now - last_check_in_date > 1000 * 60 * 60 * 12) {
|
||||
this.state.showWarningMessage = true;
|
||||
if (this.return_to_main_menu) {
|
||||
clearTimeout(this.return_to_main_menu);
|
||||
}
|
||||
this.lockScanner = true;
|
||||
} else if (now - last_check_in_date > 1000 * 60 * 60 * 8) {
|
||||
this.state.randomMessage = _t("Another good day's work! See you soon!");
|
||||
}
|
||||
}
|
||||
|
||||
if (now.hour < 12) {
|
||||
this.state.message = _t("Have a good day!");
|
||||
} else if (now.hour < 14) {
|
||||
this.state.message = _t("Have a nice lunch!");
|
||||
if (Math.random() < 0.05) {
|
||||
this.state.randomMessage = _t(
|
||||
"Eat breakfast as a king, lunch as a merchant and supper as a beggar"
|
||||
);
|
||||
} else if (Math.random() < 0.06) {
|
||||
this.state.randomMessage = _t("An apple a day keeps the doctor away");
|
||||
}
|
||||
} else if (now.hour < 17) {
|
||||
this.state.message = _t("Have a good afternoon");
|
||||
} else {
|
||||
if (now.hour < 18 && Math.random() < 0.2) {
|
||||
this.state.message = _t(
|
||||
"Early to bed and early to rise, makes a man healthy, wealthy and wise"
|
||||
);
|
||||
} else {
|
||||
this.state.message = _t("Have a good evening");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
setTimeoutNextAction() {
|
||||
if (this.kioskDelay > 0) {
|
||||
this.return_to_main_menu = setTimeout(() => {
|
||||
this.actionService.doAction(this.next_action, { clearBreadcrumbs: true });
|
||||
}, this.kioskDelay);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
GreetingMessage.template = "hr_attendance.GreetingMessage";
|
||||
GreetingMessage.components = { CardLayout };
|
||||
|
||||
registry.category("actions").add("hr_attendance_greeting_message", GreetingMessage);
|
||||
@@ -0,0 +1,63 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<templates xml:space="preserve">
|
||||
|
||||
<t t-name="hr_attendance.GreetingMessage" owl="1">
|
||||
<CardLayout>
|
||||
<t t-if="attendance">
|
||||
<t t-call="hr_attendance.UserBadge">
|
||||
<t t-set="userId" t-value="attendance.employee_id[0]"/>
|
||||
<t t-set="userName" t-value="employee_name"/>
|
||||
</t>
|
||||
|
||||
<div t-if="attendance.check_out" class="flex-grow-1">
|
||||
<h1 class="mt-5">Goodbye <t t-esc="employee_name"/>!</h1>
|
||||
<h2 class="o_hr_attendance_message_message mt4 mb24" t-esc="state.message"/>
|
||||
<div class="alert alert-info fs-2 mx-3" role="status">
|
||||
Checked out at <b><t t-esc="attendance.check_out_time"/></b>
|
||||
<br/><b><t t-esc="hours_today"/></b>
|
||||
</div>
|
||||
<div t-att-class="'alert ' + (today_overtime_float >= 0 ? 'alert-success' : 'alert-danger') + ' h3 mx-3'" role="status">
|
||||
Extra hours today:
|
||||
<span t-esc="today_overtime"/>
|
||||
</div>
|
||||
<t t-if="total_overtime_float > 0">
|
||||
Total extra hours: <span t-esc="total_overtime"/>
|
||||
</t>
|
||||
<h3 class="o_hr_attendance_random_message fst-italic mb24" t-esc="state.randomMessage"/>
|
||||
<div t-if="showWarningMessage" class="o_hr_attendance_warning_message mt24 alert alert-warning" role="alert">
|
||||
<b>Warning! Last check in was over 12 hours ago.</b><br/>If this isn't right, please contact Human Resource staff
|
||||
</div>
|
||||
</div>
|
||||
<div t-else="" class="flex-grow-1">
|
||||
<h1 class="mt-5 mb0">Welcome <t t-esc="employee_name"/>!</h1>
|
||||
<h2 class="o_hr_attendance_message_message mt4 mb24" t-esc="state.message"/>
|
||||
<div class="alert alert-info fs-2 mx-3" role="status">
|
||||
Checked in at <b><t t-esc="attendance.check_in_time"/></b>
|
||||
</div>
|
||||
<h3 class="o_hr_attendance_random_message mb24" t-esc="state.randomMessage"/>
|
||||
<div class="o_hr_attendance_warning_message mt24 alert alert-warning" style="display:none" role="alert"/>
|
||||
</div>
|
||||
<div class="flex-grow-1">
|
||||
<button class="o_hr_attendance_button_dismiss align-self-center btn btn-primary btn-lg px-5 py-3" t-on-click="() => this.onClickDismiss()">
|
||||
<span class="fs-2" t-if="attendance.check_out">Goodbye</span>
|
||||
<span class="fs-2" t-else="">OK</span>
|
||||
</button>
|
||||
</div>
|
||||
</t>
|
||||
<t t-else="">
|
||||
<div class="flex-grow-1">
|
||||
<div class="alert alert-warning mt-5 mx-3" role="alert">
|
||||
<h4 class="alert-heading">Invalid request</h4>
|
||||
<p>Please return to the main menu.</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex-grow-1">
|
||||
<button class="o_hr_attendance_button_dismiss btn btn-primary btn-lg fs-2 px-5 py-3" t-on-click="() => this.onClickDismiss()">
|
||||
<i class="fa fa-chevron-left me-2"/>
|
||||
<span class="fs-2">Go back</span>
|
||||
</button>
|
||||
</div>
|
||||
</t>
|
||||
</CardLayout>
|
||||
|
||||
</templates>
|
||||
+1
-10
@@ -1,19 +1,10 @@
|
||||
|
||||
.o_hr_attendance_kiosk_mode, .o_hr_attendance_clock {
|
||||
.o_hr_attendance_kiosk_mode {
|
||||
@include media-breakpoint-down(md) {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.o_hr_attendance_clock {
|
||||
position: relative;
|
||||
display: none;
|
||||
|
||||
@include media-breakpoint-up(md) {
|
||||
@include o-position-absolute(0, 0); // Fine-tuned by margin classes
|
||||
}
|
||||
}
|
||||
|
||||
.o_hr_attendance_kiosk_mode {
|
||||
@include media-breakpoint-up(md) {
|
||||
min-width: map-get($grid-breakpoints, 'md') * .6;
|
||||
@@ -0,0 +1,80 @@
|
||||
/** @odoo-module **/
|
||||
|
||||
import { CardLayout } from "@hr_attendance/components/card_layout/card_layout";
|
||||
import { CheckInOut } from "@hr_attendance/components/check_in_out/check_in_out";
|
||||
import { Component, onWillStart, useState } from "@odoo/owl";
|
||||
import { browser } from "@web/core/browser/browser";
|
||||
import { registry } from "@web/core/registry";
|
||||
import { useService } from "@web/core/utils/hooks";
|
||||
|
||||
export class KioskConfirm extends Component {
|
||||
setup() {
|
||||
this.actionService = useService("action");
|
||||
this.notification = useService("notification");
|
||||
this.orm = useService("orm");
|
||||
this.user = useService("user");
|
||||
|
||||
this.padButtons = [
|
||||
...Array.from({ length: 9 }, (_, i) => [i + 1]), // [[1], ..., [9]]
|
||||
["C", "btn-warning"],
|
||||
[0],
|
||||
["OK", "btn-primary"],
|
||||
];
|
||||
this.state = useState({
|
||||
codePin: "",
|
||||
});
|
||||
this.lockPad = false;
|
||||
|
||||
this.nextAction = "hr_attendance.hr_attendance_action_kiosk_mode";
|
||||
const { employee_id, employee_name, employee_state, employee_hours_today } =
|
||||
this.props.action;
|
||||
this.employeeId = employee_id;
|
||||
this.employeeName = employee_name;
|
||||
this.checkedIn = employee_state === "checked_in";
|
||||
this.employeeHoursToday = registry.category("formatters").get("float_time")(
|
||||
employee_hours_today
|
||||
);
|
||||
onWillStart(this.onWillStart);
|
||||
}
|
||||
|
||||
async onWillStart() {
|
||||
this.use_pin = await this.user.hasGroup("hr_attendance.group_hr_attendance_use_pin");
|
||||
}
|
||||
|
||||
onClickBack() {
|
||||
this.actionService.doAction(this.nextAction, { clearBreadcrumbs: true });
|
||||
}
|
||||
|
||||
async onClickPadButton(value) {
|
||||
if (this.lockPad) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (value === "C") {
|
||||
this.state.codePin = "";
|
||||
} else if (value === "OK") {
|
||||
this.lockPad = true;
|
||||
const result = await this.orm.call("hr.employee", "attendance_manual", [
|
||||
[this.employeeId],
|
||||
this.nextAction,
|
||||
this.state.codePin,
|
||||
]);
|
||||
if (result.action) {
|
||||
this.actionService.doAction(result.action);
|
||||
} else if (result.warning) {
|
||||
this.notification.add(result.warning, { type: "danger" });
|
||||
this.state.codePin = "";
|
||||
browser.setTimeout(() => {
|
||||
this.lockPad = false;
|
||||
}, 500);
|
||||
}
|
||||
} else {
|
||||
this.state.codePin += value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
KioskConfirm.template = "hr_attendance.KioskConfirm";
|
||||
KioskConfirm.components = { CardLayout, CheckInOut };
|
||||
|
||||
registry.category("actions").add("hr_attendance_kiosk_confirm", KioskConfirm);
|
||||
@@ -0,0 +1,65 @@
|
||||
|
||||
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<templates xml:space="preserve">
|
||||
|
||||
<t t-name="hr_attendance.UserBadge" owl="1">
|
||||
<div class="o_hr_attendance_user_badge o_home_menu_background d-flex align-items-end justify-content-center flex-grow-1 pt-5 pt-md-4 bg-odoo">
|
||||
<img class="img rounded-circle mb-n5" t-attf-src="/web/image?model=hr.employee.public&field=avatar_128&id=#{userId}" t-att-title="userName" height="80" t-att-alt="userName"/>
|
||||
</div>
|
||||
</t>
|
||||
|
||||
<t t-name="hr_attendance.KioskConfirm" owl="1">
|
||||
<CardLayout>
|
||||
|
||||
<button class="o_hr_attendance_back_button btn btn-block btn-secondary btn-lg d-block d-md-none py-5">
|
||||
<i class="fa fa-chevron-left me-2"/> Go back
|
||||
</button>
|
||||
|
||||
<t t-if="employeeId" t-call="hr_attendance.UserBadge">
|
||||
<t t-set="userId" t-value="employeeId"/>
|
||||
<t t-set="userName" t-value="employeeName"/>
|
||||
</t>
|
||||
|
||||
<button t-on-click="() => this.onClickBack()" class="o_hr_attendance_back_button o_hr_attendance_back_button_md btn btn-secondary d-none d-md-inline-flex align-items-center position-absolute top-0 start-0 rounded-circle">
|
||||
<i class="fa fa-2x fa-fw fa-chevron-left me-1" role="img" aria-label="Go back" title="Go back"/>
|
||||
</button>
|
||||
|
||||
<div t-if="employeeId" class="flex-grow-1">
|
||||
<h1 class="mt-5 mb8" t-esc="employeeName"/>
|
||||
<h3 class="mt8 mb24"><t t-if="!checkedIn">Welcome!</t><t t-else="">Want to check out?</t></h3>
|
||||
<h4 class="mt0 mb0 text-muted" t-if="checkedIn">Today's work hours: <span t-esc="employeeHoursToday"/></h4>
|
||||
|
||||
<CheckInOut t-if="!use_pin" checkedIn="checkedIn" employeeId="employeeId" nextAction="nextAction"/>
|
||||
|
||||
<t t-else="">
|
||||
<h3 class="mt-4 mb0 text-muted">Please enter your PIN to <b t-if="checkedIn">check out</b><b t-else="">check in</b></h3>
|
||||
<div class="row">
|
||||
<div class="col-md-8 offset-md-2 o_hr_attendance_pin_pad">
|
||||
<div class="row g-0" >
|
||||
<div class="col-12 mb8 mt8">
|
||||
<input t-att-value="state.codePin" class="o_hr_attendance_PINbox border-0 bg-white fs-1 text-center" type="password" disabled="true"/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row g-0">
|
||||
<t t-foreach="padButtons" t-as="btn" t-key="btn[0]">
|
||||
<div class="col-4 p-1">
|
||||
<a href="#" t-on-click="() => this.onClickPadButton(btn[0])" t-attf-class="o_hr_attendance_PINbox_button btn {{btn[1]? btn[1] : 'btn-secondary border'}} btn-block btn-lg d-flex align-items-center justify-content-center">
|
||||
<t t-esc="btn[0]"/>
|
||||
</a>
|
||||
</div>
|
||||
</t>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</t>
|
||||
</div>
|
||||
<div t-else="" class="alert alert-danger mx-3" role="alert">
|
||||
<h4 class="alert-heading">Error: could not find corresponding employee.</h4>
|
||||
<p>Please return to the main menu.</p>
|
||||
</div>
|
||||
<a role="button" class="oe_attendance_sign_in_out" aria-label="Sign out" title="Sign out"/>
|
||||
</CardLayout>
|
||||
</t>
|
||||
|
||||
</templates>
|
||||
@@ -0,0 +1,93 @@
|
||||
/** @odoo-module **/
|
||||
|
||||
import { BarcodeScanner } from "@barcodes/components/barcode_scanner";
|
||||
import { CardLayout } from "@hr_attendance/components/card_layout/card_layout";
|
||||
import { Component, onWillStart } from "@odoo/owl";
|
||||
import { browser } from "@web/core/browser/browser";
|
||||
import { registry } from "@web/core/registry";
|
||||
import { useBus, useService } from "@web/core/utils/hooks";
|
||||
import { url } from "@web/core/utils/urls";
|
||||
|
||||
export class KioskMode extends Component {
|
||||
setup() {
|
||||
this.actionService = useService("action");
|
||||
this.companyService = useService("company");
|
||||
this.notification = useService("notification");
|
||||
this.rpc = useService("rpc");
|
||||
this.orm = useService("orm");
|
||||
|
||||
const barcode = useService("barcode");
|
||||
useBus(barcode.bus, "barcode_scanned", (ev) => this.onBarcodeScanned(ev.detail.barcode));
|
||||
|
||||
this.lockScanner = false;
|
||||
|
||||
onWillStart(this.onWillStart);
|
||||
// Make a RPC call every day to keep the session alive
|
||||
browser.setInterval(() => this.callServer(), 60 * 60 * 1000 * 24);
|
||||
}
|
||||
|
||||
async onWillStart() {
|
||||
const companyId = this.companyService.currentCompany.id;
|
||||
const currentCompany = (
|
||||
await this.orm.searchRead(
|
||||
"res.company",
|
||||
[["id", "=", companyId]],
|
||||
["name", "attendance_kiosk_mode", "attendance_barcode_source"][
|
||||
("employee_id", "date_from", "date_to", "number_of_days")
|
||||
]
|
||||
)
|
||||
)[0];
|
||||
|
||||
this.companyName = currentCompany.name;
|
||||
this.companyImageUrl = url("/web/image", {
|
||||
model: "res.company",
|
||||
id: companyId,
|
||||
field: "logo",
|
||||
});
|
||||
this.kioskMode = currentCompany.attendance_kiosk_mode;
|
||||
this.barcodeSource = currentCompany.attendance_barcode_source;
|
||||
}
|
||||
|
||||
onClickAttendanceEmployees() {
|
||||
this.actionService.doAction("hr_attendance.hr_employee_attendance_action_kanban", {
|
||||
additionalContext: { no_group_by: true },
|
||||
});
|
||||
}
|
||||
|
||||
async onBarcodeScanned(barcode) {
|
||||
if (this.lockScanner) {
|
||||
return;
|
||||
}
|
||||
this.lockScanner = true;
|
||||
const result = await this.orm.call("hr.employee", "attendance_scan", [barcode]);
|
||||
if (result.action) {
|
||||
this.actionService.doAction(result.action);
|
||||
} else if (result.warning) {
|
||||
this.notification.add(result.warning, { type: "danger" });
|
||||
this.lockScanner = false;
|
||||
}
|
||||
}
|
||||
|
||||
callServer() {
|
||||
// Make a call to the database to avoid the auto close of the session
|
||||
this.rpc("/hr_attendance/kiosk_keepalive", {});
|
||||
}
|
||||
}
|
||||
|
||||
class KioskModeBarcodeScanner extends BarcodeScanner {
|
||||
get facingMode() {
|
||||
if (this.props.barcodeSource == "front") {
|
||||
return "user";
|
||||
}
|
||||
return super.facingMode;
|
||||
}
|
||||
}
|
||||
KioskModeBarcodeScanner.props = {
|
||||
...BarcodeScanner.props,
|
||||
barcodeSource: String,
|
||||
};
|
||||
|
||||
KioskMode.template = "hr_attendance.KioskMode";
|
||||
KioskMode.components = { CardLayout, BarcodeScanner: KioskModeBarcodeScanner };
|
||||
|
||||
registry.category("actions").add("hr_attendance_kiosk_mode", KioskMode);
|
||||
@@ -0,0 +1,33 @@
|
||||
|
||||
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<templates xml:space="preserve">
|
||||
|
||||
<t t-name="hr_attendance.KioskMode" owl="1">
|
||||
<CardLayout kioskModeClasses="'o_barcode_main pt-5'">
|
||||
<h2 class="mb-2"><small>Welcome to</small> <t t-esc="companyName"/></h2>
|
||||
<img t-att-src="companyImageUrl" alt="Company Logo" class="o_hr_attendance_kiosk_company_image align-self-center img img-fluid mb-3" width="200"/>
|
||||
<div class="o_hr_attendance_kiosk_welcome_row d-flex flex-column pb-5">
|
||||
<div class="col-md-5 mt-5 mb-5 mb-md-0 align-self-center" t-if="kioskMode != 'manual'">
|
||||
<BarcodeScanner t-if="barcodeSource !== 'scanner'" onBarcodeScanned="(ev) => this.onBarcodeScanned(ev)" barcodeSource="barcodeSource"/>
|
||||
<div t-else="" class="o_barcode_mobile_container">
|
||||
<img src="/barcodes/static/img/barcode.png" alt="Barcode"/>
|
||||
<span class="o_barcode_laser"/>
|
||||
</div>
|
||||
<h6 class="mt-2 text-muted">Scan your badge</h6>
|
||||
</div>
|
||||
<div class="mt-5 align-self-end" t-if="kioskMode == 'barcode_manual'">
|
||||
<button class="o_hr_attendance_button_employees btn btn-link" t-on-click="() => this.onClickAttendanceEmployees()">
|
||||
Identify Manually
|
||||
</button>
|
||||
</div>
|
||||
<div class="mt-5 align-self-center" t-if="kioskMode == 'manual'">
|
||||
<button class="o_hr_attendance_button_employees btn btn-primary px-5 py-3 mt-4 mb-2" t-on-click="() => this.onClickAttendanceEmployees()">
|
||||
<span class="fs-2">Identify Manually</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</CardLayout>
|
||||
</t>
|
||||
|
||||
</templates>
|
||||
@@ -0,0 +1,40 @@
|
||||
/** @odoo-module **/
|
||||
|
||||
import { CardLayout } from "@hr_attendance/components/card_layout/card_layout";
|
||||
import { CheckInOut } from "@hr_attendance/components/check_in_out/check_in_out";
|
||||
import { Component, onWillStart } from "@odoo/owl";
|
||||
import { registry } from "@web/core/registry";
|
||||
import { useService } from "@web/core/utils/hooks";
|
||||
|
||||
export class MyAttendances extends Component {
|
||||
setup() {
|
||||
this.orm = useService("orm");
|
||||
this.user = useService("user");
|
||||
|
||||
this.nextAction = "hr_attendance.hr_attendance_action_my_attendances";
|
||||
this.employee = false;
|
||||
this.checkedIn = false;
|
||||
|
||||
onWillStart(this.onWillStart);
|
||||
}
|
||||
|
||||
async onWillStart() {
|
||||
const result = await this.orm.searchRead(
|
||||
"hr.employee",
|
||||
[["user_id", "=", this.user.userId]],
|
||||
["attendance_state", "name", "hours_today"]
|
||||
);
|
||||
this.employee = result[0];
|
||||
if (this.employee) {
|
||||
this.hoursToday = registry.category("formatters").get("float_time")(
|
||||
this.employee.hours_today
|
||||
);
|
||||
this.checkedIn = this.employee.attendance_state === "checked_in";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
MyAttendances.template = "hr_attendance.MyAttendances";
|
||||
MyAttendances.components = { CardLayout, CheckInOut };
|
||||
|
||||
registry.category("actions").add("hr_attendance_my_attendances", MyAttendances);
|
||||
@@ -0,0 +1,26 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<templates xml:space="preserve">
|
||||
|
||||
<t t-name="hr_attendance.MyAttendances" owl="1">
|
||||
<CardLayout>
|
||||
<t t-if="employee">
|
||||
<t t-call="hr_attendance.UserBadge">
|
||||
<t t-set="userId" t-value="employee.id"/>
|
||||
<t t-set="userName" t-value="employee.name"/>
|
||||
</t>
|
||||
|
||||
<div class="flex-grow-1">
|
||||
<h1 class="mt-5" t-esc="employee.name"/>
|
||||
<h3><t t-if="!checkedIn">Welcome!</t><t t-else="">Want to check out?</t></h3>
|
||||
<h4 class="mt0 mb0 text-muted" t-if="checkedIn">Today's work hours: <span t-esc="hoursToday"/></h4>
|
||||
</div>
|
||||
|
||||
<CheckInOut checkedIn="checkedIn" employeeId="employee.id" nextAction="nextAction"/>
|
||||
</t>
|
||||
<div t-else="" class="alert alert-warning">
|
||||
<b>Warning</b> : Your user should be linked to an employee to use attendance.<br/> Please contact your administrator.
|
||||
</div>
|
||||
</CardLayout>
|
||||
</t>
|
||||
|
||||
</templates>
|
||||
@@ -0,0 +1,14 @@
|
||||
/** @odoo-module **/
|
||||
|
||||
import { Component } from "@odoo/owl";
|
||||
|
||||
export class CardLayout extends Component {}
|
||||
|
||||
CardLayout.template = "hr_attendance.CardLayout";
|
||||
CardLayout.props = {
|
||||
kioskModeClasses: { type: String, optional: true },
|
||||
slots: Object,
|
||||
};
|
||||
CardLayout.defaultProps = {
|
||||
kioskModeClasses: "",
|
||||
};
|
||||
@@ -0,0 +1,15 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<templates xml:space="preserve">
|
||||
|
||||
<t t-name="hr_attendance.CardLayout" owl="1">
|
||||
<div class="o_hr_attendance_kiosk_mode_container o_home_menu_background d-flex flex-column align-items-center justify-content-center h-100 text-center position-relative">
|
||||
<span class="o_hr_attendance_kiosk_backdrop position-absolute top-0 start-0 end-0 bottom-0 bg-black-25"/>
|
||||
<div t-attf-class="o_hr_attendance_kiosk_mode flex-grow-1 flex-md-grow-0 card pb-3 px-0 px-lg-5 {{props.kioskModeClasses}}">
|
||||
<div class="card-body d-flex flex-column p-0 p-md-4">
|
||||
<t t-slot="default" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</t>
|
||||
|
||||
</templates>
|
||||
@@ -0,0 +1,34 @@
|
||||
/** @odoo-module **/
|
||||
|
||||
import { Component } from "@odoo/owl";
|
||||
import { useService } from "@web/core/utils/hooks";
|
||||
import { useDebounced } from "@web/core/utils/timing";
|
||||
|
||||
export class CheckInOut extends Component {
|
||||
setup() {
|
||||
this.actionService = useService("action");
|
||||
this.orm = useService("orm");
|
||||
this.notification = useService("notification");
|
||||
|
||||
this.onClickSignInOut = useDebounced(this.signInOut, 200, true);
|
||||
}
|
||||
|
||||
async signInOut() {
|
||||
const result = await this.orm.call("hr.employee", "attendance_manual", [
|
||||
[this.props.employeeId],
|
||||
this.props.nextAction,
|
||||
]);
|
||||
if (result.action) {
|
||||
this.actionService.doAction(result.action);
|
||||
} else if (result.warning) {
|
||||
this.notification.add(result.warning, { type: "danger" });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
CheckInOut.template = "hr_attendance.CheckInOut";
|
||||
CheckInOut.props = {
|
||||
checkedIn: Boolean,
|
||||
employeeId: Number,
|
||||
nextAction: String,
|
||||
};
|
||||
@@ -0,0 +1,14 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<templates xml:space="preserve">
|
||||
|
||||
<t t-name="hr_attendance.CheckInOut" owl="1">
|
||||
<div class="flex-grow-1">
|
||||
<button t-on-click="() => this.onClickSignInOut()" t-attf-class="o_hr_attendance_sign_in_out_icon btn btn-{{ props.checkedIn ? 'warning' : 'success' }} align-self-center px-5 py-3 mt-4 mb-2">
|
||||
<span class="align-middle fs-2 me-3 text-white" t-if="!props.checkedIn">Check IN</span>
|
||||
<i t-attf-class="fa fa-4x fa-sign-{{ props.checkedIn ? 'out' : 'in' }} align-middle"/>
|
||||
<span class="align-middle fs-2 ms-3" t-if="props.checkedIn">Check OUT</span>
|
||||
</button>
|
||||
</div>
|
||||
</t>
|
||||
|
||||
</templates>
|
||||
@@ -1,201 +0,0 @@
|
||||
odoo.define('hr_attendance.greeting_message', function (require) {
|
||||
"use strict";
|
||||
|
||||
var AbstractAction = require('web.AbstractAction');
|
||||
var core = require('web.core');
|
||||
var time = require('web.time');
|
||||
var field_utils = require('web.field_utils');
|
||||
|
||||
var _t = core._t;
|
||||
|
||||
|
||||
var GreetingMessage = AbstractAction.extend({
|
||||
contentTemplate: 'HrAttendanceGreetingMessage',
|
||||
|
||||
events: {
|
||||
"click .o_hr_attendance_button_dismiss": function() { this.do_action(this.next_action, {clear_breadcrumbs: true}); },
|
||||
},
|
||||
|
||||
init: function(parent, action) {
|
||||
var self = this;
|
||||
this._super.apply(this, arguments);
|
||||
this.activeBarcode = true;
|
||||
this.kioskDelay = action.kiosk_delay;
|
||||
|
||||
// if no correct action given (due to an erroneous back or refresh from the browser), we set the dismiss button to return
|
||||
// to the (likely) appropriate menu, according to the user access rights
|
||||
if(!action.attendance) {
|
||||
this.activeBarcode = false;
|
||||
this.getSession().user_has_group('hr_attendance.group_hr_attendance_user').then(function(has_group) {
|
||||
if(has_group) {
|
||||
self.next_action = 'hr_attendance.hr_attendance_action_kiosk_mode';
|
||||
} else {
|
||||
self.next_action = 'hr_attendance.hr_attendance_action_my_attendances';
|
||||
}
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
this.next_action = action.next_action || 'hr_attendance.hr_attendance_action_my_attendances';
|
||||
// no listening to barcode scans if we aren't coming from the kiosk mode (and thus not going back to it with next_action)
|
||||
if (this.next_action != 'hr_attendance.hr_attendance_action_kiosk_mode' && this.next_action.tag != 'hr_attendance_kiosk_mode') {
|
||||
this.activeBarcode = false;
|
||||
}
|
||||
|
||||
this.attendance = action.attendance;
|
||||
// We receive the check in/out times in UTC
|
||||
// This widget only deals with display, which should be in browser's TimeZone
|
||||
this.attendance.check_in = this.attendance.check_in && moment.utc(this.attendance.check_in).local();
|
||||
this.attendance.check_out = this.attendance.check_out && moment.utc(this.attendance.check_out).local();
|
||||
this.previous_attendance_change_date = action.previous_attendance_change_date && moment.utc(action.previous_attendance_change_date).local();
|
||||
|
||||
// check in/out times displayed in the greeting message template.
|
||||
this.format_time = time.getLangTimeFormat();
|
||||
this.attendance.check_in_time = this.attendance.check_in && this.attendance.check_in.format(this.format_time);
|
||||
this.attendance.check_out_time = this.attendance.check_out && this.attendance.check_out.format(this.format_time);
|
||||
|
||||
// extra hours amount displayed in the greeting message template.
|
||||
this.total_overtime_float = action.total_overtime; // Used for comparison in template
|
||||
this.total_overtime = field_utils.format.float_time(this.total_overtime_float);
|
||||
this.today_overtime_float = action.overtime_today;
|
||||
this.today_overtime = field_utils.format.float_time(this.today_overtime_float);
|
||||
|
||||
if (action.hours_today) {
|
||||
var duration = moment.duration(action.hours_today, "hours");
|
||||
this.hours_today = duration.hours() + ' hours, ' + duration.minutes() + ' minutes';
|
||||
}
|
||||
|
||||
this.employee_name = action.employee_name;
|
||||
this.attendanceBarcode = action.barcode;
|
||||
},
|
||||
|
||||
start: function() {
|
||||
if (this.attendance) {
|
||||
this.attendance.check_out ? this.farewell_message() : this.welcome_message();
|
||||
}
|
||||
if (this.activeBarcode) {
|
||||
core.bus.on('barcode_scanned', this, this._onBarcodeScanned);
|
||||
}
|
||||
return this._super.apply(this, arguments);
|
||||
},
|
||||
|
||||
welcome_message: function() {
|
||||
var self = this;
|
||||
var now = this.attendance.check_in.clone();
|
||||
if (this.kioskDelay > 0) {
|
||||
this.return_to_main_menu = setTimeout( function() { self.do_action(self.next_action, {clear_breadcrumbs: true}); }, this.kioskDelay);
|
||||
}
|
||||
|
||||
if (now.hours() < 5) {
|
||||
this.$('.o_hr_attendance_message_message').append(_t("Good night"));
|
||||
} else if (now.hours() < 12) {
|
||||
if (now.hours() < 8 && Math.random() < 0.3) {
|
||||
if (Math.random() < 0.75) {
|
||||
this.$('.o_hr_attendance_message_message').append(_t("The early bird catches the worm"));
|
||||
} else {
|
||||
this.$('.o_hr_attendance_message_message').append(_t("First come, first served"));
|
||||
}
|
||||
} else {
|
||||
this.$('.o_hr_attendance_message_message').append(_t("Good morning"));
|
||||
}
|
||||
} else if (now.hours() < 17){
|
||||
this.$('.o_hr_attendance_message_message').append(_t("Good afternoon"));
|
||||
} else if (now.hours() < 23){
|
||||
this.$('.o_hr_attendance_message_message').append(_t("Good evening"));
|
||||
} else {
|
||||
this.$('.o_hr_attendance_message_message').append(_t("Good night"));
|
||||
}
|
||||
if(this.previous_attendance_change_date){
|
||||
var last_check_out_date = this.previous_attendance_change_date.clone();
|
||||
if(now - last_check_out_date > 24*7*60*60*1000){
|
||||
this.$('.o_hr_attendance_random_message').html(_t("Glad to have you back, it's been a while!"));
|
||||
} else {
|
||||
if(Math.random() < 0.02){
|
||||
this.$('.o_hr_attendance_random_message').html(_t("If a job is worth doing, it is worth doing well!"));
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
farewell_message: function() {
|
||||
var self = this;
|
||||
var now = this.attendance.check_out.clone();
|
||||
if (this.kioskDelay > 0) {
|
||||
this.return_to_main_menu = setTimeout( function() { self.do_action(self.next_action, {clear_breadcrumbs: true}); }, this.kioskDelay);
|
||||
}
|
||||
|
||||
if(this.previous_attendance_change_date){
|
||||
var last_check_in_date = this.previous_attendance_change_date.clone();
|
||||
if(now - last_check_in_date > 1000*60*60*12){
|
||||
this.$('.o_hr_attendance_warning_message').show().append(_t("<b>Warning! Last check in was over 12 hours ago.</b><br/>If this isn't right, please contact Human Resource staff"));
|
||||
if (this.return_to_main_menu) {
|
||||
clearTimeout(this.return_to_main_menu);
|
||||
}
|
||||
this.activeBarcode = false;
|
||||
} else if(now - last_check_in_date > 1000*60*60*8){
|
||||
this.$('.o_hr_attendance_random_message').html(_t("Another good day's work! See you soon!"));
|
||||
}
|
||||
}
|
||||
|
||||
if (now.hours() < 12) {
|
||||
this.$('.o_hr_attendance_message_message').append(_t("Have a good day!"));
|
||||
} else if (now.hours() < 14) {
|
||||
this.$('.o_hr_attendance_message_message').append(_t("Have a nice lunch!"));
|
||||
if (Math.random() < 0.05) {
|
||||
this.$('.o_hr_attendance_random_message').html(_t("Eat breakfast as a king, lunch as a merchant and supper as a beggar"));
|
||||
} else if (Math.random() < 0.06) {
|
||||
this.$('.o_hr_attendance_random_message').html(_t("An apple a day keeps the doctor away"));
|
||||
}
|
||||
} else if (now.hours() < 17) {
|
||||
this.$('.o_hr_attendance_message_message').append(_t("Have a good afternoon"));
|
||||
} else {
|
||||
if (now.hours() < 18 && Math.random() < 0.2) {
|
||||
this.$('.o_hr_attendance_message_message').append(_t("Early to bed and early to rise, makes a man healthy, wealthy and wise"));
|
||||
} else {
|
||||
this.$('.o_hr_attendance_message_message').append(_t("Have a good evening"));
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
_onBarcodeScanned: function(barcode) {
|
||||
var self = this;
|
||||
if (this.attendanceBarcode !== barcode){
|
||||
if (this.return_to_main_menu) { // in case of multiple scans in the greeting message view, delete the timer, a new one will be created.
|
||||
clearTimeout(this.return_to_main_menu);
|
||||
}
|
||||
core.bus.off('barcode_scanned', this, this._onBarcodeScanned);
|
||||
const kioskDelay = this.kioskDelay;
|
||||
this._rpc({
|
||||
model: 'hr.employee',
|
||||
method: 'attendance_scan',
|
||||
args: [barcode, ],
|
||||
})
|
||||
.then(function (result) {
|
||||
if (result.action) {
|
||||
self.do_action(result.action);
|
||||
} else if (result.warning) {
|
||||
self.displayNotification({ title: result.warning, type: 'danger' });
|
||||
if (kioskDelay > 0) {
|
||||
setTimeout( function() { self.do_action(self.next_action, {clear_breadcrumbs: true}); }, kioskDelay);
|
||||
}
|
||||
}
|
||||
}, function () {
|
||||
if (kioskDelay > 0) {
|
||||
setTimeout( function() { self.do_action(self.next_action, {clear_breadcrumbs: true}); }, kioskDelay);
|
||||
}
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
destroy: function () {
|
||||
core.bus.off('barcode_scanned', this, this._onBarcodeScanned);
|
||||
clearTimeout(this.return_to_main_menu);
|
||||
this._super.apply(this, arguments);
|
||||
},
|
||||
});
|
||||
|
||||
core.action_registry.add('hr_attendance_greeting_message', GreetingMessage);
|
||||
|
||||
return GreetingMessage;
|
||||
|
||||
});
|
||||
@@ -1,98 +0,0 @@
|
||||
odoo.define('hr_attendance.kiosk_confirm', function (require) {
|
||||
"use strict";
|
||||
|
||||
var AbstractAction = require('web.AbstractAction');
|
||||
var core = require('web.core');
|
||||
var field_utils = require('web.field_utils');
|
||||
var QWeb = core.qweb;
|
||||
|
||||
const session = require('web.session');
|
||||
|
||||
|
||||
var KioskConfirm = AbstractAction.extend({
|
||||
events: {
|
||||
"click .o_hr_attendance_back_button": function () { this.do_action(this.next_action, {clear_breadcrumbs: true}); },
|
||||
"click .o_hr_attendance_sign_in_out_icon": _.debounce(function () {
|
||||
var self = this;
|
||||
this._rpc({
|
||||
model: 'hr.employee',
|
||||
method: 'attendance_manual',
|
||||
args: [[this.employee_id], this.next_action],
|
||||
context: session.user_context,
|
||||
})
|
||||
.then(function(result) {
|
||||
if (result.action) {
|
||||
self.do_action(result.action);
|
||||
} else if (result.warning) {
|
||||
self.displayNotification({ title: result.warning, type: 'danger' });
|
||||
}
|
||||
});
|
||||
}, 200, true),
|
||||
'click .o_hr_attendance_pin_pad_button_0': function() { this.$('.o_hr_attendance_PINbox').val(this.$('.o_hr_attendance_PINbox').val() + 0); },
|
||||
'click .o_hr_attendance_pin_pad_button_1': function() { this.$('.o_hr_attendance_PINbox').val(this.$('.o_hr_attendance_PINbox').val() + 1); },
|
||||
'click .o_hr_attendance_pin_pad_button_2': function() { this.$('.o_hr_attendance_PINbox').val(this.$('.o_hr_attendance_PINbox').val() + 2); },
|
||||
'click .o_hr_attendance_pin_pad_button_3': function() { this.$('.o_hr_attendance_PINbox').val(this.$('.o_hr_attendance_PINbox').val() + 3); },
|
||||
'click .o_hr_attendance_pin_pad_button_4': function() { this.$('.o_hr_attendance_PINbox').val(this.$('.o_hr_attendance_PINbox').val() + 4); },
|
||||
'click .o_hr_attendance_pin_pad_button_5': function() { this.$('.o_hr_attendance_PINbox').val(this.$('.o_hr_attendance_PINbox').val() + 5); },
|
||||
'click .o_hr_attendance_pin_pad_button_6': function() { this.$('.o_hr_attendance_PINbox').val(this.$('.o_hr_attendance_PINbox').val() + 6); },
|
||||
'click .o_hr_attendance_pin_pad_button_7': function() { this.$('.o_hr_attendance_PINbox').val(this.$('.o_hr_attendance_PINbox').val() + 7); },
|
||||
'click .o_hr_attendance_pin_pad_button_8': function() { this.$('.o_hr_attendance_PINbox').val(this.$('.o_hr_attendance_PINbox').val() + 8); },
|
||||
'click .o_hr_attendance_pin_pad_button_9': function() { this.$('.o_hr_attendance_PINbox').val(this.$('.o_hr_attendance_PINbox').val() + 9); },
|
||||
'click .o_hr_attendance_pin_pad_button_C': function() { this.$('.o_hr_attendance_PINbox').val(''); },
|
||||
'click .o_hr_attendance_pin_pad_button_ok': _.debounce(function() {
|
||||
var self = this;
|
||||
this.$('.o_hr_attendance_pin_pad_button_ok').attr("disabled", "disabled");
|
||||
this._rpc({
|
||||
model: 'hr.employee',
|
||||
method: 'attendance_manual',
|
||||
args: [[this.employee_id], this.next_action, this.$('.o_hr_attendance_PINbox').val()],
|
||||
context: session.user_context,
|
||||
})
|
||||
.then(function(result) {
|
||||
if (result.action) {
|
||||
self.do_action(result.action);
|
||||
} else if (result.warning) {
|
||||
self.displayNotification({ title: result.warning, type: 'danger' });
|
||||
self.$('.o_hr_attendance_PINbox').val('');
|
||||
setTimeout( function() { self.$('.o_hr_attendance_pin_pad_button_ok').removeAttr("disabled"); }, 500);
|
||||
}
|
||||
});
|
||||
}, 200, true),
|
||||
},
|
||||
|
||||
init: function (parent, action) {
|
||||
this._super.apply(this, arguments);
|
||||
this.next_action = 'hr_attendance.hr_attendance_action_kiosk_mode';
|
||||
this.employee_id = action.employee_id;
|
||||
this.employee_name = action.employee_name;
|
||||
this.employee_state = action.employee_state;
|
||||
this.employee_hours_today = field_utils.format.float_time(action.employee_hours_today);
|
||||
},
|
||||
|
||||
start: function () {
|
||||
var self = this;
|
||||
this.getSession().user_has_group('hr_attendance.group_hr_attendance_use_pin').then(function(has_group){
|
||||
self.use_pin = has_group;
|
||||
self.$el.html(QWeb.render("HrAttendanceKioskConfirm", {widget: self}));
|
||||
self.start_clock();
|
||||
});
|
||||
return self._super.apply(this, arguments);
|
||||
},
|
||||
|
||||
start_clock: function () {
|
||||
this.clock_start = setInterval(function() {this.$(".o_hr_attendance_clock").text(new Date().toLocaleTimeString(navigator.language, {hour: '2-digit', minute:'2-digit', second:'2-digit'}));}, 500);
|
||||
// First clock refresh before interval to avoid delay
|
||||
this.$(".o_hr_attendance_clock").show().text(new Date().toLocaleTimeString(navigator.language, {hour: '2-digit', minute:'2-digit', second:'2-digit'}));
|
||||
},
|
||||
|
||||
destroy: function () {
|
||||
clearInterval(this.clock_start);
|
||||
this._super.apply(this, arguments);
|
||||
},
|
||||
});
|
||||
|
||||
core.action_registry.add('hr_attendance_kiosk_confirm', KioskConfirm);
|
||||
|
||||
return KioskConfirm;
|
||||
|
||||
});
|
||||
@@ -1,94 +0,0 @@
|
||||
odoo.define('hr_attendance.kiosk_mode', function (require) {
|
||||
"use strict";
|
||||
|
||||
var AbstractAction = require('web.AbstractAction');
|
||||
var ajax = require('web.ajax');
|
||||
var core = require('web.core');
|
||||
var Session = require('web.session');
|
||||
|
||||
var QWeb = core.qweb;
|
||||
|
||||
|
||||
var KioskMode = AbstractAction.extend({
|
||||
events: {
|
||||
"click .o_hr_attendance_button_employees": function() {
|
||||
this.do_action('hr_attendance.hr_employee_attendance_action_kanban', {
|
||||
additional_context: {'no_group_by': true},
|
||||
});
|
||||
},
|
||||
},
|
||||
|
||||
start: function () {
|
||||
var self = this;
|
||||
core.bus.on('barcode_scanned', this, this._onBarcodeScanned);
|
||||
self.session = Session;
|
||||
const company_id = this.session.user_context.allowed_company_ids[0];
|
||||
var def = this._rpc({
|
||||
model: 'res.company',
|
||||
method: 'search_read',
|
||||
args: [[['id', '=', company_id]], ['name', 'attendance_kiosk_mode', 'attendance_barcode_source']],
|
||||
})
|
||||
.then(function (companies){
|
||||
self.company_name = companies[0].name;
|
||||
self.company_image_url = self.session.url('/web/image', {model: 'res.company', id: company_id, field: 'logo',});
|
||||
self.kiosk_mode = companies[0].attendance_kiosk_mode;
|
||||
self.barcode_source = companies[0].attendance_barcode_source;
|
||||
self.$el.html(QWeb.render("HrAttendanceKioskMode", {widget: self}));
|
||||
self.start_clock();
|
||||
});
|
||||
// Make a RPC call every day to keep the session alive
|
||||
self._interval = window.setInterval(this._callServer.bind(this), (60*60*1000*24));
|
||||
return Promise.all([def, this._super.apply(this, arguments)]);
|
||||
},
|
||||
|
||||
on_attach_callback: function () {
|
||||
// Stop the bus_service to avoid notifications in kiosk mode
|
||||
this.call('bus_service', 'stop');
|
||||
$('body').find('.o_ChatWindowHeaderView_commandClose').click();
|
||||
},
|
||||
|
||||
_onBarcodeScanned: function(barcode) {
|
||||
var self = this;
|
||||
core.bus.off('barcode_scanned', this, this._onBarcodeScanned);
|
||||
this._rpc({
|
||||
model: 'hr.employee',
|
||||
method: 'attendance_scan',
|
||||
args: [barcode, ],
|
||||
})
|
||||
.then(function (result) {
|
||||
if (result.action) {
|
||||
self.do_action(result.action);
|
||||
} else if (result.warning) {
|
||||
self.displayNotification({ title: result.warning, type: 'danger' });
|
||||
core.bus.on('barcode_scanned', self, self._onBarcodeScanned);
|
||||
}
|
||||
}, function () {
|
||||
core.bus.on('barcode_scanned', self, self._onBarcodeScanned);
|
||||
});
|
||||
},
|
||||
|
||||
start_clock: function() {
|
||||
this.clock_start = setInterval(function() {this.$(".o_hr_attendance_clock").text(new Date().toLocaleTimeString(navigator.language, {hour: '2-digit', minute:'2-digit', second:'2-digit'}));}, 500);
|
||||
// First clock refresh before interval to avoid delay
|
||||
this.$(".o_hr_attendance_clock").show().text(new Date().toLocaleTimeString(navigator.language, {hour: '2-digit', minute:'2-digit', second:'2-digit'}));
|
||||
},
|
||||
|
||||
destroy: function () {
|
||||
core.bus.off('barcode_scanned', this, this._onBarcodeScanned);
|
||||
clearInterval(this.clock_start);
|
||||
clearInterval(this._interval);
|
||||
this._super.apply(this, arguments);
|
||||
},
|
||||
|
||||
_callServer: function () {
|
||||
// Make a call to the database to avoid the auto close of the session
|
||||
return ajax.rpc("/hr_attendance/kiosk_keepalive", {});
|
||||
},
|
||||
|
||||
});
|
||||
|
||||
core.action_registry.add('hr_attendance_kiosk_mode', KioskMode);
|
||||
|
||||
return KioskMode;
|
||||
|
||||
});
|
||||
@@ -1,59 +0,0 @@
|
||||
odoo.define('hr_attendance.my_attendances', function (require) {
|
||||
"use strict";
|
||||
|
||||
var AbstractAction = require('web.AbstractAction');
|
||||
var core = require('web.core');
|
||||
var field_utils = require('web.field_utils');
|
||||
|
||||
const session = require('web.session');
|
||||
|
||||
var MyAttendances = AbstractAction.extend({
|
||||
contentTemplate: 'HrAttendanceMyMainMenu',
|
||||
events: {
|
||||
"click .o_hr_attendance_sign_in_out_icon": _.debounce(function() {
|
||||
this.update_attendance();
|
||||
}, 200, true),
|
||||
},
|
||||
|
||||
willStart: function () {
|
||||
var self = this;
|
||||
|
||||
var def = this._rpc({
|
||||
model: 'hr.employee',
|
||||
method: 'search_read',
|
||||
args: [[['user_id', '=', this.getSession().uid]], ['attendance_state', 'name', 'hours_today']],
|
||||
context: session.user_context,
|
||||
})
|
||||
.then(function (res) {
|
||||
self.employee = res.length && res[0];
|
||||
if (res.length) {
|
||||
self.hours_today = field_utils.format.float_time(self.employee.hours_today);
|
||||
}
|
||||
});
|
||||
|
||||
return Promise.all([def, this._super.apply(this, arguments)]);
|
||||
},
|
||||
|
||||
update_attendance: function () {
|
||||
var self = this;
|
||||
this._rpc({
|
||||
model: 'hr.employee',
|
||||
method: 'attendance_manual',
|
||||
args: [[self.employee.id], 'hr_attendance.hr_attendance_action_my_attendances'],
|
||||
context: session.user_context,
|
||||
})
|
||||
.then(function(result) {
|
||||
if (result.action) {
|
||||
self.do_action(result.action);
|
||||
} else if (result.warning) {
|
||||
self.displayNotification({ title: result.warning, type: 'danger' });
|
||||
}
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
core.action_registry.add('hr_attendance_my_attendances', MyAttendances);
|
||||
|
||||
return MyAttendances;
|
||||
|
||||
});
|
||||
@@ -1,205 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<template xml:space="preserve">
|
||||
<t t-name="PresenceIndicator">
|
||||
<div id="oe_hr_attendance_status" class="fa fa-circle me-1 text-400 " role="img" aria-label="Available" title="Available">
|
||||
</div>
|
||||
</t>
|
||||
|
||||
<t t-name="HrAttendanceCardLayout">
|
||||
<div class="o_hr_attendance_kiosk_mode_container o_home_menu_background d-flex flex-column align-items-center justify-content-center h-100 text-center">
|
||||
<span class="o_hr_attendance_kiosk_backdrop position-absolute top-0 start-0 end-0 bottom-0 bg-black-25"/>
|
||||
<div class="o_hr_attendance_clock bg-black-50 p-3 py-md-2 m-0 mt-md-5 me-md-5 h2 text-white font-monospace"/>
|
||||
<div t-attf-class="o_hr_attendance_kiosk_mode flex-grow-1 flex-md-grow-0 card pb-3 px-0 px-lg-5 {{kioskModeClasses ? kioskModeClasses : '' }}">
|
||||
<div class="card-body d-flex flex-column p-0 p-md-4">
|
||||
<t t-out="bodyContent"></t>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</t>
|
||||
|
||||
<t t-name="HrAttendanceUserBadge">
|
||||
<div class="o_hr_attendance_user_badge o_home_menu_background d-flex align-items-end justify-content-center flex-grow-1 pt-5 pt-md-4 bg-odoo">
|
||||
<img class="img rounded-circle mb-n5" t-attf-src="/web/image?model=hr.employee.public&field=avatar_128&id=#{userId}" t-att-title="userName" height="80" t-att-alt="userName"/>
|
||||
</div>
|
||||
</t>
|
||||
|
||||
<t t-name="HrAttendanceCheckInOutButtons">
|
||||
<div class="flex-grow-1">
|
||||
<button t-attf-class="o_hr_attendance_sign_in_out_icon btn btn-{{ checked_in ? 'warning' : 'success' }} align-self-center px-5 py-3 mt-4 mb-2">
|
||||
<span class="align-middle fs-2 me-3 text-white" t-if="!checked_in">Check IN</span>
|
||||
<i t-attf-class="fa fa-4x fa-sign-{{ checked_in ? 'out' : 'in' }} align-middle"/>
|
||||
<span class="align-middle fs-2 ms-3" t-if="checked_in">Check OUT</span>
|
||||
</button>
|
||||
</div>
|
||||
</t>
|
||||
|
||||
<t t-name="HrAttendanceKioskMode">
|
||||
<t t-call="HrAttendanceCardLayout">
|
||||
<t t-set="kioskModeClasses">o_barcode_main pt-5</t>
|
||||
<t t-set="bodyContent">
|
||||
<h2 class="mb-2"><small>Welcome to</small> <t t-esc="widget.company_name"/></h2>
|
||||
<img t-attf-src="{{widget.company_image_url}}" alt="Company Logo" class="o_hr_attendance_kiosk_company_image align-self-center img img-fluid mb-3" width="200"/>
|
||||
<div class="o_hr_attendance_kiosk_welcome_row d-flex flex-column pb-5">
|
||||
<div class="col-md-5 mt-5 mb-5 mb-md-0 align-self-center" t-if="widget.kiosk_mode != 'manual'">
|
||||
<img src="/barcodes/static/img/barcode.png" alt="Barcode" style="width: 115px;height: 60px"/>
|
||||
<h6 class="mt-2 text-muted">Scan your badge</h6>
|
||||
</div>
|
||||
<div class="mt-5 align-self-end" t-if="widget.kiosk_mode == 'barcode_manual'">
|
||||
<button class="o_hr_attendance_button_employees btn btn-link">
|
||||
Identify Manually
|
||||
</button>
|
||||
</div>
|
||||
<div class="mt-5 align-self-center" t-if="widget.kiosk_mode == 'manual'">
|
||||
<button class="o_hr_attendance_button_employees btn btn-primary px-5 py-3 mt-4 mb-2">
|
||||
<span class="fs-2">Identify Manually</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</t>
|
||||
</t>
|
||||
</t>
|
||||
|
||||
<t t-name="HrAttendanceMyMainMenu">
|
||||
<t t-call="HrAttendanceCardLayout">
|
||||
<t t-set="bodyContent">
|
||||
<t t-if="widget.employee">
|
||||
<t t-set="checked_in" t-value="widget.employee.attendance_state=='checked_in'"/>
|
||||
|
||||
<t t-call="HrAttendanceUserBadge">
|
||||
<t t-set="userId" t-value="widget.employee.id"/>
|
||||
<t t-set="userName" t-value="widget.employee.name"/>
|
||||
</t>
|
||||
|
||||
<div class="flex-grow-1">
|
||||
<h1 class="mt-5" t-esc="widget.employee.name"/>
|
||||
<h3><t t-if="!checked_in">Welcome!</t><t t-else="">Want to check out?</t></h3>
|
||||
<h4 class="mt0 mb0 text-muted" t-if="checked_in">Today's work hours: <span t-esc="widget.hours_today"/></h4>
|
||||
</div>
|
||||
|
||||
<t t-call="HrAttendanceCheckInOutButtons"/>
|
||||
</t>
|
||||
<div class="alert alert-warning" t-else="">
|
||||
<b>Warning</b> : Your user should be linked to an employee to use attendance.<br/> Please contact your administrator.
|
||||
</div>
|
||||
</t>
|
||||
</t>
|
||||
</t>
|
||||
|
||||
<t t-name="HrAttendanceKioskConfirm">
|
||||
<t t-call="HrAttendanceCardLayout">
|
||||
<t t-set="bodyContent">
|
||||
<t t-set="checked_in" t-value="widget.employee_state=='checked_in'"/>
|
||||
|
||||
<button class="o_hr_attendance_back_button btn btn-block btn-secondary btn-lg d-block d-md-none py-5">
|
||||
<i class="fa fa-chevron-left me-2"/> Go back
|
||||
</button>
|
||||
|
||||
<t t-if="widget.employee_id" t-call="HrAttendanceUserBadge">
|
||||
<t t-set="userId" t-value="widget.employee_id"/>
|
||||
<t t-set="userName" t-value="widget.employee_name"/>
|
||||
</t>
|
||||
|
||||
<button class="o_hr_attendance_back_button o_hr_attendance_back_button_md btn btn-secondary d-none d-md-inline-flex align-items-center position-absolute top-0 start-0 rounded-circle">
|
||||
<i class="fa fa-2x fa-fw fa-chevron-left me-1" role="img" aria-label="Go back" title="Go back"/>
|
||||
</button>
|
||||
|
||||
<div t-if="widget.employee_id" class="flex-grow-1">
|
||||
<h1 class="mt-5 mb8"><t t-esc="widget.employee_name"/></h1>
|
||||
<h3 class="mt8 mb24"><t t-if="!checked_in">Welcome!</t><t t-else="">Want to check out?</t></h3>
|
||||
<h4 class="mt0 mb0 text-muted" t-if="checked_in">Today's work hours: <span t-esc="widget.employee_hours_today"/></h4>
|
||||
|
||||
<t t-if="!widget.use_pin" t-call="HrAttendanceCheckInOutButtons"/>
|
||||
|
||||
<t t-else="">
|
||||
<h3 class="mt-4 mb0 text-muted">Please enter your PIN to <b t-if="checked_in">check out</b><b t-else="">check in</b></h3>
|
||||
<div class="row">
|
||||
<div class="col-md-8 offset-md-2 o_hr_attendance_pin_pad">
|
||||
<div class="row g-0" >
|
||||
<div class="col-12 mb8 mt8">
|
||||
<input class="o_hr_attendance_PINbox border-0 bg-white fs-1 text-center" type="password" disabled="true"/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row g-0">
|
||||
<t t-foreach="['1', '2', '3', '4', '5', '6', '7', '8', '9', ['C', 'btn-warning'], '0', ['ok', 'btn-primary']]" t-as="btn_name">
|
||||
<div class="col-4 p-1">
|
||||
<a href="#" t-attf-class="o_hr_attendance_PINbox_button btn {{btn_name[1]? btn_name[1] : 'btn-secondary border'}} btn-block btn-lg {{ 'o_hr_attendance_pin_pad_button_' + btn_name[0] }} d-flex align-items-center justify-content-center">
|
||||
<t t-esc="btn_name[0]"/>
|
||||
</a>
|
||||
</div>
|
||||
</t>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</t>
|
||||
</div>
|
||||
<div t-else="" class="alert alert-danger mx-3" role="alert">
|
||||
<h4 class="alert-heading">Error: could not find corresponding employee.</h4>
|
||||
<p>Please return to the main menu.</p>
|
||||
</div>
|
||||
<a role="button" class="oe_attendance_sign_in_out" aria-label="Sign out" title="Sign out"/>
|
||||
</t>
|
||||
</t>
|
||||
</t>
|
||||
|
||||
<t t-name="HrAttendanceGreetingMessage">
|
||||
<t t-call="HrAttendanceCardLayout">
|
||||
<t t-set="bodyContent">
|
||||
<t t-set="checked_in" t-value="widget.employee_state=='checked_in'"/>
|
||||
|
||||
<t t-if="widget.attendance">
|
||||
<t t-call="HrAttendanceUserBadge">
|
||||
<t t-set="userId" t-value="widget.attendance.employee_id[0]"/>
|
||||
<t t-set="userName" t-value="widget.employee_name"/>
|
||||
</t>
|
||||
|
||||
<div t-if="widget.attendance.check_out" class="flex-grow-1">
|
||||
<h1 class="mt-5">Goodbye <t t-esc="widget.employee_name"/>!</h1>
|
||||
<h2 class="o_hr_attendance_message_message mt4 mb24"/>
|
||||
<div class="alert alert-info fs-2 mx-3" role="status">
|
||||
Checked out at <b><t t-esc="widget.attendance.check_out_time"/></b>
|
||||
<br/><b><t t-esc="widget.hours_today"/></b>
|
||||
</div>
|
||||
<div t-att-class="'alert ' + (widget.today_overtime_float >= 0 ? 'alert-success' : 'alert-danger') + ' h3 mx-3'" role="status">
|
||||
Extra hours today:
|
||||
<span t-esc="widget.today_overtime"/>
|
||||
</div>
|
||||
<t t-if="widget.total_overtime_float > 0">
|
||||
Total extra hours: <span t-esc="widget.total_overtime"/>
|
||||
</t>
|
||||
<h3 class="o_hr_attendance_random_message fst-italic mb24"/>
|
||||
<div class="o_hr_attendance_warning_message mt24 alert alert-warning" style="display:none" role="alert"/>
|
||||
</div>
|
||||
<div t-else="" class="flex-grow-1">
|
||||
<h1 class="mt-5 mb0">Welcome <t t-esc="widget.employee_name"/>!</h1>
|
||||
<h2 class="o_hr_attendance_message_message mt4 mb24"/>
|
||||
<div class="alert alert-info fs-2 mx-3" role="status">
|
||||
Checked in at <b><t t-esc="widget.attendance.check_in_time"/></b>
|
||||
</div>
|
||||
<h3 class="o_hr_attendance_random_message mb24"/>
|
||||
<div class="o_hr_attendance_warning_message mt24 alert alert-warning" style="display:none" role="alert"/>
|
||||
</div>
|
||||
<div class="flex-grow-1">
|
||||
<button class="o_hr_attendance_button_dismiss align-self-center btn btn-primary btn-lg px-5 py-3">
|
||||
<span class="fs-2" t-if="widget.attendance.check_out">Goodbye</span>
|
||||
<span class="fs-2" t-else="">OK</span>
|
||||
</button>
|
||||
</div>
|
||||
</t>
|
||||
<t t-else="">
|
||||
<div class="flex-grow-1">
|
||||
<div class="alert alert-warning mt-5 mx-3" role="alert">
|
||||
<h4 class="alert-heading">Invalid request</h4>
|
||||
<p>Please return to the main menu.</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex-grow-1">
|
||||
<button class="o_hr_attendance_button_dismiss btn btn-primary btn-lg fs-2 px-5 py-3">
|
||||
<i class="fa fa-chevron-left me-2"/>
|
||||
<span class="fs-2">Go back</span>
|
||||
</button>
|
||||
</div>
|
||||
</t>
|
||||
</t>
|
||||
</t>
|
||||
</t>
|
||||
</template>
|
||||
@@ -0,0 +1,229 @@
|
||||
/** @odoo-module **/
|
||||
|
||||
import { barcodeService } from "@barcodes/barcode_service";
|
||||
import { getFixture, makeDeferred, nextTick, patchWithCleanup } from "@web/../tests/helpers/utils";
|
||||
import { createWebClient, doAction } from "@web/../tests/webclient/helpers";
|
||||
import { registry } from "@web/core/registry";
|
||||
import { session } from "@web/session";
|
||||
import { companyService } from "@web/webclient/company_service";
|
||||
|
||||
const serviceRegistry = registry.category("services");
|
||||
|
||||
let serverData;
|
||||
let target;
|
||||
|
||||
QUnit.module("Client Actions", (hooks) => {
|
||||
hooks.beforeEach(() => {
|
||||
// serverData = getActionManagerServerData();
|
||||
serverData = {
|
||||
models: {
|
||||
"hr.employee": {
|
||||
fields: {
|
||||
name: { string: "Name", type: "char" },
|
||||
attendance_state: {
|
||||
string: "State",
|
||||
type: "selection",
|
||||
selection: [
|
||||
["checked_in", "In"],
|
||||
["checked_out", "Out"],
|
||||
],
|
||||
default: 1,
|
||||
},
|
||||
user_id: { string: "user ID", type: "integer" },
|
||||
barcode: { string: "barcode", type: "integer" },
|
||||
hours_today: { string: "Hours today", type: "float" },
|
||||
overtime: { string: "Overtime", type: "float" },
|
||||
},
|
||||
records: [
|
||||
{
|
||||
id: 1,
|
||||
name: "Employee A",
|
||||
attendance_state: "checked_out",
|
||||
user_id: 1,
|
||||
barcode: 1,
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
name: "Employee B",
|
||||
attendance_state: "checked_out",
|
||||
user_id: 2,
|
||||
barcode: 2,
|
||||
},
|
||||
],
|
||||
},
|
||||
"res.company": {
|
||||
fields: {
|
||||
name: { string: "Name", type: "char" },
|
||||
attendance_kiosk_mode: { type: "char" },
|
||||
attendance_barcode_source: { type: "char" },
|
||||
},
|
||||
records: [
|
||||
{
|
||||
id: 1,
|
||||
name: "Company A",
|
||||
attendance_kiosk_mode: "barcode_manual",
|
||||
attendance_barcode_source: "front",
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
|
||||
actions: {
|
||||
"hr_attendance.hr_attendance_action_kiosk_mode": {
|
||||
type: "ir.actions.client",
|
||||
tag: "hr_attendance_kiosk_mode",
|
||||
},
|
||||
},
|
||||
};
|
||||
target = getFixture();
|
||||
|
||||
serviceRegistry.add("barcode", barcodeService);
|
||||
serviceRegistry.add("company", companyService);
|
||||
});
|
||||
|
||||
QUnit.test("My attendances: simple rendering", async function (assert) {
|
||||
patchWithCleanup(session, {
|
||||
uid: 1,
|
||||
});
|
||||
|
||||
const webClient = await createWebClient({ serverData });
|
||||
await doAction(webClient, {
|
||||
type: "ir.actions.client",
|
||||
tag: "hr_attendance_my_attendances",
|
||||
});
|
||||
|
||||
assert.strictEqual(
|
||||
target.querySelector(".o_hr_attendance_kiosk_mode h1").textContent,
|
||||
"Employee A",
|
||||
"should have rendered the client action without crashing"
|
||||
);
|
||||
});
|
||||
|
||||
QUnit.test("Kiosk Mode: simple rendering", async function (assert) {
|
||||
patchWithCleanup(session, {
|
||||
uid: 1,
|
||||
user_companies: {
|
||||
current_company: 1,
|
||||
allowed_companies: {
|
||||
1: { id: 1 },
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
let rpcCount = 0;
|
||||
const mockRPC = async (route, args) => {
|
||||
if (args.method === "attendance_scan" && args.model === "hr.employee") {
|
||||
rpcCount++;
|
||||
return serverData.models["hr.employee"].records.find((r) => r.id === args.args[0]);
|
||||
}
|
||||
};
|
||||
const webClient = await createWebClient({ serverData, mockRPC });
|
||||
await doAction(webClient, {
|
||||
type: "ir.actions.client",
|
||||
tag: "hr_attendance_kiosk_mode",
|
||||
});
|
||||
|
||||
const { barcode } = webClient.env.services;
|
||||
barcode.bus.trigger("barcode_scanned", { barcode: 1 });
|
||||
barcode.bus.trigger("barcode_scanned", { barcode: 1 });
|
||||
assert.strictEqual(rpcCount, 1, "RPC call should have been done only once.");
|
||||
|
||||
barcode.bus.trigger("barcode_scanned", { barcode: 2 });
|
||||
assert.strictEqual(rpcCount, 1, "RPC call should have been done only once.");
|
||||
});
|
||||
|
||||
QUnit.test("Attendance Greeting Message Test", async function (assert) {
|
||||
patchWithCleanup(session, {
|
||||
uid: 1,
|
||||
user_companies: {
|
||||
current_company: 1,
|
||||
allowed_companies: {
|
||||
1: { id: 1 },
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
let rpcCount = 0;
|
||||
let def;
|
||||
|
||||
const actions = [
|
||||
{
|
||||
type: "ir.actions.client",
|
||||
tag: "hr_attendance_greeting_message",
|
||||
attendance: {
|
||||
check_in: "2018-09-20 13:41:13",
|
||||
employee_id: [1],
|
||||
},
|
||||
next_action: "hr_attendance.hr_attendance_action_kiosk_mode",
|
||||
},
|
||||
{
|
||||
type: "ir.actions.client",
|
||||
tag: "hr_attendance_kiosk_mode",
|
||||
},
|
||||
{
|
||||
type: "ir.actions.client",
|
||||
tag: "hr_attendance_greeting_message",
|
||||
attendance: {
|
||||
check_in: "2018-09-20 13:41:13",
|
||||
check_out: "2018-09-20 14:41:13",
|
||||
employee_id: [1],
|
||||
},
|
||||
next_action: "hr_attendance.hr_attendance_action_kiosk_mode",
|
||||
},
|
||||
];
|
||||
|
||||
const mockRPC = async (route, args) => {
|
||||
if (args.method === "attendance_scan" && args.model === "hr.employee") {
|
||||
await def;
|
||||
return {
|
||||
action: actions[rpcCount++],
|
||||
};
|
||||
}
|
||||
};
|
||||
const webClient = await createWebClient({ serverData, mockRPC });
|
||||
await doAction(webClient, {
|
||||
type: "ir.actions.client",
|
||||
tag: "hr_attendance_kiosk_mode",
|
||||
});
|
||||
assert.containsOnce(target, ".o_hr_attendance_kiosk_welcome_row");
|
||||
|
||||
const { barcode } = webClient.env.services;
|
||||
barcode.bus.trigger("barcode_scanned", { barcode: 1 });
|
||||
await nextTick();
|
||||
assert.strictEqual(rpcCount, 1, "RPC call should have been done only once.");
|
||||
assert.containsNone(target, ".o_hr_attendance_kiosk_welcome_row");
|
||||
assert.strictEqual(
|
||||
target.querySelector(".o_hr_attendance_message_message").textContent,
|
||||
"Good afternoon"
|
||||
);
|
||||
assert.strictEqual(
|
||||
target
|
||||
.querySelector(".o_hr_attendance_kiosk_mode div[role='status']")
|
||||
.textContent.trim(),
|
||||
"Checked in at 09/20/2018 14:41:13"
|
||||
);
|
||||
|
||||
def = makeDeferred();
|
||||
barcode.bus.trigger("barcode_scanned", { barcode: 1 });
|
||||
barcode.bus.trigger("barcode_scanned", { barcode: 1 });
|
||||
def.resolve();
|
||||
await nextTick();
|
||||
assert.strictEqual(rpcCount, 2, "RPC call should have been done only once.");
|
||||
assert.containsOnce(target, ".o_hr_attendance_kiosk_mode");
|
||||
|
||||
barcode.bus.trigger("barcode_scanned", { barcode: 1 });
|
||||
await nextTick();
|
||||
assert.strictEqual(rpcCount, 3, "RPC call should have been done only once.");
|
||||
assert.containsNone(target, ".o_hr_attendance_kiosk_welcome_row");
|
||||
assert.strictEqual(
|
||||
target.querySelector(".o_hr_attendance_message_message").textContent,
|
||||
"Have a good afternoon"
|
||||
);
|
||||
assert.strictEqual(
|
||||
target
|
||||
.querySelector(".o_hr_attendance_kiosk_mode div[role='status']")
|
||||
.textContent.trim(),
|
||||
"Checked out at 09/20/2018 15:41:13"
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -1,197 +0,0 @@
|
||||
odoo.define('hr_attendance.tests', function (require) {
|
||||
"use strict";
|
||||
|
||||
var testUtils = require('web.test_utils');
|
||||
var core = require('web.core');
|
||||
|
||||
var MyAttendances = require('hr_attendance.my_attendances');
|
||||
var KioskMode = require('hr_attendance.kiosk_mode');
|
||||
var GreetingMessage = require('hr_attendance.greeting_message');
|
||||
|
||||
|
||||
QUnit.module('HR Attendance', {
|
||||
beforeEach: function () {
|
||||
this.data = {
|
||||
'hr.employee': {
|
||||
fields: {
|
||||
name: {string: 'Name', type: 'char'},
|
||||
attendance_state: {
|
||||
string: 'State',
|
||||
type: 'selection',
|
||||
selection: [['checked_in', "In"], ['checked_out', "Out"]],
|
||||
default: 1,
|
||||
},
|
||||
user_id: {string: 'user ID', type: 'integer'},
|
||||
barcode: {string:'barcode', type: 'integer'},
|
||||
hours_today: {string:'Hours today', type: 'float'},
|
||||
overtime: {string: 'Overtime', type: 'float'},
|
||||
},
|
||||
records: [{
|
||||
id: 1,
|
||||
name: "Employee A",
|
||||
attendance_state: 'checked_out',
|
||||
user_id: 1,
|
||||
barcode: 1,
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
name: "Employee B",
|
||||
attendance_state: 'checked_out',
|
||||
user_id: 2,
|
||||
barcode: 2,
|
||||
}],
|
||||
},
|
||||
'res.company': {
|
||||
fields: {
|
||||
name: {string: 'Name', type: 'char'},
|
||||
attendance_kiosk_mode: {type: 'char'},
|
||||
attendance_barcode_source: {type: 'char'},
|
||||
},
|
||||
records: [{
|
||||
id: 1,
|
||||
name: "Company A",
|
||||
attendance_kiosk_mode: 'barcode_manual',
|
||||
attendance_barcode_source: 'front',
|
||||
}],
|
||||
},
|
||||
};
|
||||
},
|
||||
}, function () {
|
||||
QUnit.module('My attendances (client action)');
|
||||
|
||||
QUnit.test('simple rendering', async function (assert) {
|
||||
assert.expect(1);
|
||||
|
||||
var $target = $('#qunit-fixture');
|
||||
var clientAction = new MyAttendances(null, {});
|
||||
await testUtils.mock.addMockEnvironment(clientAction, {
|
||||
data: this.data,
|
||||
session: {
|
||||
uid: 1,
|
||||
},
|
||||
});
|
||||
await clientAction.appendTo($target);
|
||||
|
||||
assert.strictEqual(clientAction.$('.o_hr_attendance_kiosk_mode h1').text(), 'Employee A',
|
||||
"should have rendered the client action without crashing");
|
||||
|
||||
clientAction.destroy();
|
||||
});
|
||||
|
||||
QUnit.test('Attendance Kiosk Mode Test', async function (assert) {
|
||||
assert.expect(2);
|
||||
|
||||
var $target = $('#qunit-fixture');
|
||||
var self = this;
|
||||
var rpcCount = 0;
|
||||
var clientAction = new KioskMode(null, {});
|
||||
await testUtils.mock.addMockEnvironment(clientAction, {
|
||||
data: this.data,
|
||||
session: {
|
||||
uid: 1,
|
||||
user_context: {
|
||||
allowed_company_ids: [1],
|
||||
}
|
||||
},
|
||||
mockRPC: function(route, args) {
|
||||
if (args.method === 'attendance_scan' && args.model === 'hr.employee') {
|
||||
|
||||
rpcCount++;
|
||||
return Promise.resolve(self.data['hr.employee'].records[0]);
|
||||
}
|
||||
return this._super(route, args);
|
||||
},
|
||||
});
|
||||
await clientAction.appendTo($target);
|
||||
core.bus.trigger('barcode_scanned', 1);
|
||||
core.bus.trigger('barcode_scanned', 1);
|
||||
assert.strictEqual(rpcCount, 1, 'RPC call should have been done only once.');
|
||||
|
||||
core.bus.trigger('barcode_scanned', 2);
|
||||
assert.strictEqual(rpcCount, 1, 'RPC call should have been done only once.');
|
||||
|
||||
clientAction.destroy();
|
||||
});
|
||||
|
||||
QUnit.test('Attendance Greeting Message Test', async function (assert) {
|
||||
assert.expect(10);
|
||||
|
||||
var $target = $('#qunit-fixture');
|
||||
var self = this;
|
||||
var rpcCount = 0;
|
||||
|
||||
var clientActions = [];
|
||||
let greetingMessageCreated;
|
||||
async function createGreetingMessage (target, barcode){
|
||||
var action = {
|
||||
attendance: {
|
||||
check_in: "2018-09-20 13:41:13",
|
||||
employee_id: [barcode],
|
||||
},
|
||||
next_action: "hr_attendance.hr_attendance_action_kiosk_mode",
|
||||
barcode: barcode,
|
||||
};
|
||||
var clientAction = new GreetingMessage(null, action);
|
||||
await testUtils.mock.addMockEnvironment(clientAction, {
|
||||
data: self.data,
|
||||
session: {
|
||||
uid: 1,
|
||||
company_id: 1,
|
||||
},
|
||||
mockRPC: function(route, args) {
|
||||
if (args.method === 'attendance_scan' && args.model === 'hr.employee') {
|
||||
rpcCount++;
|
||||
action.attendance.employee_id = [args.args[0], 'Employee'];
|
||||
/*
|
||||
if rpc have been made, a new instance is created to simulate the same behaviour
|
||||
as functional flow.
|
||||
*/
|
||||
greetingMessageCreated = createGreetingMessage (target, args.args[0]);
|
||||
return Promise.resolve({action: action});
|
||||
}
|
||||
return this._super(route, args);
|
||||
},
|
||||
});
|
||||
await clientAction.appendTo(target);
|
||||
|
||||
clientActions.push(clientAction);
|
||||
}
|
||||
|
||||
// init - mock coming from kiosk
|
||||
await createGreetingMessage ($target, 1);
|
||||
await testUtils.nextMicrotaskTick();
|
||||
assert.strictEqual(clientActions.length, 1, 'Number of clientAction must = 1.');
|
||||
|
||||
core.bus.trigger('barcode_scanned', 1);
|
||||
/*
|
||||
As action is given when instantiate GreetingMessage, we simulate that we come from the KioskMode
|
||||
So rescanning the same barcode won't lead to another RPC.
|
||||
*/
|
||||
assert.strictEqual(clientActions.length, 1, 'Number of clientActions must = 1.');
|
||||
assert.strictEqual(rpcCount, 0, 'RPC call should not have been done.');
|
||||
|
||||
core.bus.trigger('barcode_scanned', 2);
|
||||
await greetingMessageCreated;
|
||||
assert.strictEqual(clientActions.length, 2, 'Number of clientActions must = 2.');
|
||||
assert.strictEqual(rpcCount, 1, 'RPC call should have been done only once.');
|
||||
core.bus.trigger('barcode_scanned', 2);
|
||||
await testUtils.nextMicrotaskTick();
|
||||
assert.strictEqual(clientActions.length, 2, 'Number of clientActions must = 2.');
|
||||
assert.strictEqual(rpcCount, 1, 'RPC call should have been done only once.');
|
||||
|
||||
core.bus.trigger('barcode_scanned', 1);
|
||||
await greetingMessageCreated;
|
||||
assert.strictEqual(clientActions.length, 3, 'Number of clientActions must = 3.');
|
||||
core.bus.trigger('barcode_scanned', 1);
|
||||
await testUtils.nextMicrotaskTick();
|
||||
assert.strictEqual(clientActions.length, 3, 'Number of clientActions must = 3.');
|
||||
assert.strictEqual(rpcCount, 2, 'RPC call should have been done only twice.');
|
||||
|
||||
_.each(clientActions.reverse(), function(clientAction) {
|
||||
clientAction.destroy();
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
});
|
||||
Reference in New Issue
Block a user