diff --git a/addons/barcodes/static/src/components/barcode_scanner.js b/addons/barcodes/static/src/components/barcode_scanner.js new file mode 100644 index 00000000000..f9f1a9cb383 --- /dev/null +++ b/addons/barcodes/static/src/components/barcode_scanner.js @@ -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 }, +}; diff --git a/addons/barcodes/static/src/components/barcode_scanner.scss b/addons/barcodes/static/src/components/barcode_scanner.scss new file mode 100644 index 00000000000..20fda86a787 --- /dev/null +++ b/addons/barcodes/static/src/components/barcode_scanner.scss @@ -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%; + } +} diff --git a/addons/barcodes/static/src/components/barcode_scanner.xml b/addons/barcodes/static/src/components/barcode_scanner.xml new file mode 100644 index 00000000000..0468e49a7be --- /dev/null +++ b/addons/barcodes/static/src/components/barcode_scanner.xml @@ -0,0 +1,15 @@ + + + + + + + + Tap to scan + + + + + + + diff --git a/addons/hr_attendance/__manifest__.py b/addons/hr_attendance/__manifest__.py index 1e3f0999316..18a0319e941 100644 --- a/addons/hr_attendance/__manifest__.py +++ b/addons/hr_attendance/__manifest__.py @@ -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', diff --git a/addons/hr_attendance/static/src/client_action/greeting_message/greeting_message.js b/addons/hr_attendance/static/src/client_action/greeting_message/greeting_message.js new file mode 100644 index 00000000000..66f910b8720 --- /dev/null +++ b/addons/hr_attendance/static/src/client_action/greeting_message/greeting_message.js @@ -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); diff --git a/addons/hr_attendance/static/src/client_action/greeting_message/greeting_message.xml b/addons/hr_attendance/static/src/client_action/greeting_message/greeting_message.xml new file mode 100644 index 00000000000..b753e7161cd --- /dev/null +++ b/addons/hr_attendance/static/src/client_action/greeting_message/greeting_message.xml @@ -0,0 +1,63 @@ + + + + + + + + + + + + + Goodbye ! + + + Checked out at + + + + Extra hours today: + + + + Total extra hours: + + + + Warning! Last check in was over 12 hours ago.If this isn't right, please contact Human Resource staff + + + + Welcome ! + + + Checked in at + + + + + + + Goodbye + OK + + + + + + + Invalid request + Please return to the main menu. + + + + + + Go back + + + + + + diff --git a/addons/hr_attendance/static/src/scss/hr_attendance.scss b/addons/hr_attendance/static/src/client_action/hr_attendance.scss similarity index 71% rename from addons/hr_attendance/static/src/scss/hr_attendance.scss rename to addons/hr_attendance/static/src/client_action/hr_attendance.scss index ae67df30e08..88668b0cf34 100644 --- a/addons/hr_attendance/static/src/scss/hr_attendance.scss +++ b/addons/hr_attendance/static/src/client_action/hr_attendance.scss @@ -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; diff --git a/addons/hr_attendance/static/src/client_action/kiosk_confirm/kiosk_confirm.js b/addons/hr_attendance/static/src/client_action/kiosk_confirm/kiosk_confirm.js new file mode 100644 index 00000000000..39fad9ac3f3 --- /dev/null +++ b/addons/hr_attendance/static/src/client_action/kiosk_confirm/kiosk_confirm.js @@ -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); diff --git a/addons/hr_attendance/static/src/client_action/kiosk_confirm/kiosk_confirm.xml b/addons/hr_attendance/static/src/client_action/kiosk_confirm/kiosk_confirm.xml new file mode 100644 index 00000000000..0f14e86f849 --- /dev/null +++ b/addons/hr_attendance/static/src/client_action/kiosk_confirm/kiosk_confirm.xml @@ -0,0 +1,65 @@ + + + + + + + + + + + + + + + + Go back + + + + + + + + + + + + + + Welcome!Want to check out? + Today's work hours: + + + + + Please enter your PIN to check outcheck in + + + + + + + + + + + + + + + + + + + + + + Error: could not find corresponding employee. + Please return to the main menu. + + + + + + diff --git a/addons/hr_attendance/static/src/client_action/kiosk_mode/kiosk_mode.js b/addons/hr_attendance/static/src/client_action/kiosk_mode/kiosk_mode.js new file mode 100644 index 00000000000..798d8eb04de --- /dev/null +++ b/addons/hr_attendance/static/src/client_action/kiosk_mode/kiosk_mode.js @@ -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); diff --git a/addons/hr_attendance/static/src/client_action/kiosk_mode/kiosk_mode.xml b/addons/hr_attendance/static/src/client_action/kiosk_mode/kiosk_mode.xml new file mode 100644 index 00000000000..421aecfe497 --- /dev/null +++ b/addons/hr_attendance/static/src/client_action/kiosk_mode/kiosk_mode.xml @@ -0,0 +1,33 @@ + + + + + + + + Welcome to + + + + + + + + + Scan your badge + + + + Identify Manually + + + + + Identify Manually + + + + + + + diff --git a/addons/hr_attendance/static/src/client_action/my_attendances/my_attendances.js b/addons/hr_attendance/static/src/client_action/my_attendances/my_attendances.js new file mode 100644 index 00000000000..c4cddace6fc --- /dev/null +++ b/addons/hr_attendance/static/src/client_action/my_attendances/my_attendances.js @@ -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); diff --git a/addons/hr_attendance/static/src/client_action/my_attendances/my_attendances.xml b/addons/hr_attendance/static/src/client_action/my_attendances/my_attendances.xml new file mode 100644 index 00000000000..79d5962181a --- /dev/null +++ b/addons/hr_attendance/static/src/client_action/my_attendances/my_attendances.xml @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + Welcome!Want to check out? + Today's work hours: + + + + + + Warning : Your user should be linked to an employee to use attendance. Please contact your administrator. + + + + + diff --git a/addons/hr_attendance/static/src/components/card_layout/card_layout.js b/addons/hr_attendance/static/src/components/card_layout/card_layout.js new file mode 100644 index 00000000000..d74af9ce0db --- /dev/null +++ b/addons/hr_attendance/static/src/components/card_layout/card_layout.js @@ -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: "", +}; diff --git a/addons/hr_attendance/static/src/components/card_layout/card_layout.xml b/addons/hr_attendance/static/src/components/card_layout/card_layout.xml new file mode 100644 index 00000000000..006f46900f2 --- /dev/null +++ b/addons/hr_attendance/static/src/components/card_layout/card_layout.xml @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/addons/hr_attendance/static/src/components/check_in_out/check_in_out.js b/addons/hr_attendance/static/src/components/check_in_out/check_in_out.js new file mode 100644 index 00000000000..9d4f52d05ef --- /dev/null +++ b/addons/hr_attendance/static/src/components/check_in_out/check_in_out.js @@ -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, +}; diff --git a/addons/hr_attendance/static/src/components/check_in_out/check_in_out.xml b/addons/hr_attendance/static/src/components/check_in_out/check_in_out.xml new file mode 100644 index 00000000000..4a5fe57bf83 --- /dev/null +++ b/addons/hr_attendance/static/src/components/check_in_out/check_in_out.xml @@ -0,0 +1,14 @@ + + + + + + + Check IN + + Check OUT + + + + + diff --git a/addons/hr_attendance/static/src/js/greeting_message.js b/addons/hr_attendance/static/src/js/greeting_message.js deleted file mode 100644 index d23dbeaea82..00000000000 --- a/addons/hr_attendance/static/src/js/greeting_message.js +++ /dev/null @@ -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("Warning! Last check in was over 12 hours ago.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; - -}); diff --git a/addons/hr_attendance/static/src/js/kiosk_confirm.js b/addons/hr_attendance/static/src/js/kiosk_confirm.js deleted file mode 100644 index 689ea90ad6b..00000000000 --- a/addons/hr_attendance/static/src/js/kiosk_confirm.js +++ /dev/null @@ -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; - -}); diff --git a/addons/hr_attendance/static/src/js/kiosk_mode.js b/addons/hr_attendance/static/src/js/kiosk_mode.js deleted file mode 100644 index b2e76f888f1..00000000000 --- a/addons/hr_attendance/static/src/js/kiosk_mode.js +++ /dev/null @@ -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; - -}); diff --git a/addons/hr_attendance/static/src/js/my_attendances.js b/addons/hr_attendance/static/src/js/my_attendances.js deleted file mode 100644 index 459549f54f3..00000000000 --- a/addons/hr_attendance/static/src/js/my_attendances.js +++ /dev/null @@ -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; - -}); diff --git a/addons/hr_attendance/static/src/xml/attendance.xml b/addons/hr_attendance/static/src/xml/attendance.xml deleted file mode 100644 index ad97b6b1182..00000000000 --- a/addons/hr_attendance/static/src/xml/attendance.xml +++ /dev/null @@ -1,205 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - Check IN - - Check OUT - - - - - - - o_barcode_main pt-5 - - Welcome to - - - - - Scan your badge - - - - Identify Manually - - - - - Identify Manually - - - - - - - - - - - - - - - - - - - - - Welcome!Want to check out? - Today's work hours: - - - - - - Warning : Your user should be linked to an employee to use attendance. Please contact your administrator. - - - - - - - - - - - - Go back - - - - - - - - - - - - - - Welcome!Want to check out? - Today's work hours: - - - - - Please enter your PIN to check outcheck in - - - - - - - - - - - - - - - - - - - - - - Error: could not find corresponding employee. - Please return to the main menu. - - - - - - - - - - - - - - - - - - - Goodbye ! - - - Checked out at - - - - Extra hours today: - - - - Total extra hours: - - - - - - Welcome ! - - - Checked in at - - - - - - - Goodbye - OK - - - - - - - Invalid request - Please return to the main menu. - - - - - - Go back - - - - - - - diff --git a/addons/hr_attendance/static/tests/client_action_tests.js b/addons/hr_attendance/static/tests/client_action_tests.js new file mode 100644 index 00000000000..b3e249ab58b --- /dev/null +++ b/addons/hr_attendance/static/tests/client_action_tests.js @@ -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" + ); + }); +}); diff --git a/addons/hr_attendance/static/tests/hr_attendance_tests.js b/addons/hr_attendance/static/tests/hr_attendance_tests.js deleted file mode 100644 index ce527dbf5a6..00000000000 --- a/addons/hr_attendance/static/tests/hr_attendance_tests.js +++ /dev/null @@ -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(); - }); - }); - -}); - -});
Please return to the main menu.