19.0 vanilla

This commit is contained in:
Ernad Husremovic 2026-03-09 09:31:00 +01:00
parent a1137a1456
commit e1d89e11e3
2789 changed files with 1093187 additions and 605897 deletions

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.3 KiB

After

Width:  |  Height:  |  Size: 1.9 KiB

Before After
Before After

View file

@ -1,24 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="70" height="70" viewBox="0 0 70 70">
<defs>
<path id="icon-a" d="M4,5.35309892e-14 C36.4160122,9.87060235e-15 58.0836068,-3.97961823e-14 65,5.07020818e-14 C69,6.733808e-14 70,1 70,5 C70,43.0488877 70,62.4235458 70,65 C70,69 69,70 65,70 C61,70 9,70 4,70 C1,70 7.10542736e-15,69 7.10542736e-15,65 C7.25721566e-15,62.4676575 3.83358709e-14,41.8005206 3.60818146e-14,5 C-1.13686838e-13,1 1,5.75716207e-14 4,5.35309892e-14 Z"/>
<linearGradient id="icon-c" x1="100%" x2="0%" y1="0%" y2="100%">
<stop offset="0%" stop-color="#CDC484"/>
<stop offset="100%" stop-color="#B5AA59"/>
</linearGradient>
<path id="icon-d" d="M27.2777778,16.2348485 C32.4714536,16.2348485 36.681713,20.506348 36.681713,25.7755682 C36.681713,31.0447884 32.4714536,35.3162879 27.2777778,35.3162879 C22.0841019,35.3162879 17.8738426,31.0447884 17.8738426,25.7755682 C17.8738426,20.506348 22.0841019,16.2348485 27.2777778,16.2348485 Z M32,36 C32,35.5868456 24.1175854,37.5736818 20.6967864,35.0773525 L16.5053937,36.1404272 C13.9936026,36.7775087 12.2314815,39.0671622 12.2314815,41.6939608 L12.2314815,43.9029356 C12.2314815,45.4837136 13.4945475,46.7651515 15.052662,46.7651515 C25.363233,46.7651515 30.5185185,46.7651515 30.5185185,46.7651515 C28.6666667,40.4242424 32,36.4131544 32,36 Z M48.5116455,29.4292211 L49.8821338,32.7804044 C50.108418,33.3336802 50.7775488,33.5511019 51.2858154,33.2365172 L54.3643115,31.3309087 C55.1962939,30.8159288 56.2338818,31.569761 56.0011523,32.5201752 L55.140166,36.0368953 C54.9979932,36.61751 55.4115674,37.186738 56.0077051,37.230942 L59.6183691,37.4987982 C60.5941357,37.5712005 60.9904688,38.790921 60.2436182,39.4230444 L57.4800293,41.7619923 C57.0237549,42.1481736 57.0237549,42.8517324 57.4800293,43.2379137 L60.2436719,45.5769153 C60.9905762,46.2090387 60.5942432,47.4287592 59.6184229,47.5011615 L56.0077588,47.7690177 C55.4116211,47.8132217 54.9980469,48.3824497 55.1402197,48.9630644 L56.0012061,52.4797846 C56.2338818,53.4301987 55.1962939,54.1840309 54.3643652,53.669051 L51.2858691,51.7634425 C50.7776025,51.4488041 50.108418,51.6662258 49.8821875,52.2195553 L48.5116992,55.5707386 C48.1413086,56.4764115 46.8587988,56.4764115 46.4884082,55.5707386 L45.1179199,52.2195553 C44.8916357,51.6662795 44.2225049,51.4488578 43.7142383,51.7634425 L40.6356885,53.669051 C39.8037061,54.1840309 38.7661182,53.4301987 38.9988477,52.4797846 L39.859834,48.9630644 C40.0020068,48.3824497 39.5884326,47.8132217 38.9922949,47.7690177 L35.3816309,47.5011615 C34.4058643,47.4287592 34.0095313,46.2090387 34.7563818,45.5769153 L37.5199707,43.2379674 C37.9762451,42.8517862 37.9762451,42.1482273 37.5199707,41.762046 L34.7563281,39.4230444 C34.0094238,38.790921 34.4057568,37.5712005 35.3815771,37.4987982 L38.9922412,37.230942 C39.5883789,37.186738 40.0019531,36.61751 39.8597803,36.0368953 L38.9987939,32.5201752 C38.7661182,31.569761 39.8037061,30.8159288 40.6356348,31.3309087 L43.7141846,33.2365172 C44.2224512,33.5511556 44.891582,33.3337339 45.1178662,32.7804044 L46.4883545,29.4292211 C46.8587451,28.5236019 48.1412549,28.5236019 48.5116455,29.4292211 Z M54.8046875,42.4999799 C54.8046875,38.4721469 51.5277832,35.1952995 47.5,35.1952995 C43.4721631,35.1952995 40.1953125,38.4721469 40.1953125,42.4999799 C40.1953125,46.5278128 43.4721631,49.8046602 47.5,49.8046602 C51.5277832,49.8046602 54.8046875,46.5278128 54.8046875,42.4999799 Z M53.0859375,42.4999799 C53.0859375,45.5800843 50.5801074,48.0859119 47.5,48.0859119 C44.4198926,48.0859119 41.9140625,45.5800843 41.9140625,42.4999799 C41.9140625,39.4198754 44.4198926,36.9140478 47.5,36.9140478 C50.5801074,36.9140478 53.0859375,39.4198754 53.0859375,42.4999799 Z"/>
<path id="icon-e" d="M27.2777778,14.2348485 C32.4714536,14.2348485 36.681713,18.506348 36.681713,23.7755682 C36.681713,29.0447884 32.4714536,33.3162879 27.2777778,33.3162879 C22.0841019,33.3162879 17.8738426,29.0447884 17.8738426,23.7755682 C17.8738426,18.506348 22.0841019,14.2348485 27.2777778,14.2348485 Z M32,34 C32,33.5868456 24.1175854,35.5736818 20.6967864,33.0773525 L16.5053937,34.1404272 C13.9936026,34.7775087 12.2314815,37.0671622 12.2314815,39.6939608 L12.2314815,41.9029356 C12.2314815,43.4837136 13.4945475,44.7651515 15.052662,44.7651515 C25.363233,44.7651515 30.5185185,44.7651515 30.5185185,44.7651515 C28.6666667,38.4242424 32,34.4131544 32,34 Z M48.5116455,27.4292211 L49.8821338,30.7804044 C50.108418,31.3336802 50.7775488,31.5511019 51.2858154,31.2365172 L54.3643115,29.3309087 C55.1962939,28.8159288 56.2338818,29.569761 56.0011523,30.5201752 L55.140166,34.0368953 C54.9979932,34.61751 55.4115674,35.186738 56.0077051,35.230942 L59.6183691,35.4987982 C60.5941357,35.5712005 60.9904688,36.790921 60.2436182,37.4230444 L57.4800293,39.7619923 C57.0237549,40.1481736 57.0237549,40.8517324 57.4800293,41.2379137 L60.2436719,43.5769153 C60.9905762,44.2090387 60.5942432,45.4287592 59.6184229,45.5011615 L56.0077588,45.7690177 C55.4116211,45.8132217 54.9980469,46.3824497 55.1402197,46.9630644 L56.0012061,50.4797846 C56.2338818,51.4301987 55.1962939,52.1840309 54.3643652,51.669051 L51.2858691,49.7634425 C50.7776025,49.4488041 50.108418,49.6662258 49.8821875,50.2195553 L48.5116992,53.5707386 C48.1413086,54.4764115 46.8587988,54.4764115 46.4884082,53.5707386 L45.1179199,50.2195553 C44.8916357,49.6662795 44.2225049,49.4488578 43.7142383,49.7634425 L40.6356885,51.669051 C39.8037061,52.1840309 38.7661182,51.4301987 38.9988477,50.4797846 L39.859834,46.9630644 C40.0020068,46.3824497 39.5884326,45.8132217 38.9922949,45.7690177 L35.3816309,45.5011615 C34.4058643,45.4287592 34.0095313,44.2090387 34.7563818,43.5769153 L37.5199707,41.2379674 C37.9762451,40.8517862 37.9762451,40.1482273 37.5199707,39.762046 L34.7563281,37.4230444 C34.0094238,36.790921 34.4057568,35.5712005 35.3815771,35.4987982 L38.9922412,35.230942 C39.5883789,35.186738 40.0019531,34.61751 39.8597803,34.0368953 L38.9987939,30.5201752 C38.7661182,29.569761 39.8037061,28.8159288 40.6356348,29.3309087 L43.7141846,31.2365172 C44.2224512,31.5511556 44.891582,31.3337339 45.1178662,30.7804044 L46.4883545,27.4292211 C46.8587451,26.5236019 48.1412549,26.5236019 48.5116455,27.4292211 Z M54.8046875,40.4999799 C54.8046875,36.4721469 51.5277832,33.1952995 47.5,33.1952995 C43.4721631,33.1952995 40.1953125,36.4721469 40.1953125,40.4999799 C40.1953125,44.5278128 43.4721631,47.8046602 47.5,47.8046602 C51.5277832,47.8046602 54.8046875,44.5278128 54.8046875,40.4999799 Z M53.0859375,40.4999799 C53.0859375,43.5800843 50.5801074,46.0859119 47.5,46.0859119 C44.4198926,46.0859119 41.9140625,43.5800843 41.9140625,40.4999799 C41.9140625,37.4198754 44.4198926,34.9140478 47.5,34.9140478 C50.5801074,34.9140478 53.0859375,37.4198754 53.0859375,40.4999799 Z"/>
</defs>
<g fill="none" fill-rule="evenodd">
<mask id="icon-b" fill="#fff">
<use xlink:href="#icon-a"/>
</mask>
<g mask="url(#icon-b)">
<rect width="70" height="70" fill="url(#icon-c)"/>
<path fill="#FFF" fill-opacity=".383" d="M4,1.8 L65,1.8 C67.6666667,1.8 69.3333333,1.13333333 70,-0.2 C70,2.46666667 70,3.46666667 70,2.8 L1.10547097e-14,2.8 C-1.65952376e-14,3.46666667 -2.9161925e-14,2.46666667 -2.66453526e-14,-0.2 C0.666666667,1.13333333 2,1.8 4,1.8 Z" transform="matrix(1 0 0 -1 0 2.8)"/>
<path fill="#393939" d="M43.6444444,52 L4,52 C2,52 -7.10542736e-15,51.8514286 0,47.84 L2.21121142e-16,23.3197384 L20,0 L36,9.36 L30.6694253,16.3853185 L31.9690606,16.0436983 L28.4526012,19.3069352 L28.4526012,24.5134324 L39.2011791,11.8378465 L42.1902641,14.2434656 L46.8367307,9.36 L51,14.56 L60.3723121,27.0292074 L43.6444444,52 Z" opacity=".324" transform="translate(0 18)"/>
<path fill="#000" fill-opacity=".383" d="M4,4 L65,4 C67.6666667,4 69.3333333,3 70,1 C70,3.66666667 70,5 70,5 L1.77635684e-15,5 C1.77635684e-15,5 1.77635684e-15,3.66666667 1.77635684e-15,1 C0.666666667,3 2,4 4,4 Z" transform="translate(0 65)"/>
<use fill="#000" fill-rule="nonzero" opacity=".3" xlink:href="#icon-d"/>
<use fill="#FFF" fill-rule="nonzero" xlink:href="#icon-e"/>
</g>
</g>
</svg>
<svg width="51" height="50" viewBox="0 0 51 50" xmlns="http://www.w3.org/2000/svg"><path d="M9.096 43.956a25.151 25.151 0 0 1-1.58-1.483c-.726-.743-.67-1.924.065-2.66l25.95-25.95 1.415 1.415a2 2 0 0 1 0 2.828l-25.85 25.85Z" fill="#985184"/><path d="M15.849 15.808 7.363 7.322A25 25 0 0 1 30.948.708l-15.1 15.1Z" fill="#FBB945"/><path d="m15.848 15.807 15.1-15.1a25 25 0 0 1 11.77 6.614L25.04 25l-9.192-9.192Z" fill="#F78613"/><path d="M33.524 33.485 25.04 25 42.717 7.322a24.997 24.997 0 0 1 6.315 10.655L33.524 33.485Z" fill="#088BF5"/><path d="m33.525 33.485 15.508-15.508a25 25 0 0 1-4.96 23.231c-.715.84-1.988.836-2.77.055l-7.778-7.778Z" fill="#2EBCFA"/></svg>

Before

Width:  |  Height:  |  Size: 8 KiB

After

Width:  |  Height:  |  Size: 665 B

Before After
Before After

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

View file

@ -0,0 +1,5 @@
declare module "models" {
export interface ResPartner {
outOfOfficeDateEndText: Readonly<string>;
}
}

View file

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<templates xml:space="preserve">
<t t-inherit="mail.AvatarCardPopover" t-inherit-mode="extension">
<xpath expr="//span[hasclass('o-mail-avatar-card-name')]" position="after">
<span t-if="partner?.outOfOfficeDateEndText" class="text-warning me-1 smaller fw-bold opacity-75" t-esc="partner.outOfOfficeDateEndText"/>
</xpath>
</t>
</templates>

View file

@ -0,0 +1,20 @@
<?xml version="1.0" encoding="UTF-8"?>
<templates xml:space="preserve">
<t t-inherit="resource_mail.AvatarCardResourcePopover" t-inherit-mode="extension">
<xpath expr="//span[@name='icon'][hasclass('o_user_im_status')]/i[hasclass('fa-question-circle')]" position="before">
<i t-elif="record.im_status === 'leave_online'" class="fa fa-fw fa-plane text-success" title="Online" role="img" aria-label="User is online"/>
<i t-elif="record.im_status === 'leave_away'" class="fa fa-fw fa-plane text-warning" title="Idle" role="img" aria-label="User is idle"/>
<i t-elif="record.im_status === 'leave_busy'" class="fa fa-fw fa-plane text-danger" title="Busy" role="img" aria-label="User is busy"/>
<i t-elif="record.im_status === 'leave_offline'" class="fa fa-fw fa-plane text-500" title="Out of office" role="img" aria-label="User is out of office"/>
</xpath>
<xpath expr="//span[@name='icon'][hasclass('o_employee_presence_status')]" position="inside">
<!-- Employee is on time off but he is connected -->
<i t-if="record.hr_icon_display === 'presence_holiday_present'" class="fa fa-fw fa-plane text-success me-1" title="Active but on leave" role="img" aria-label="Active but on leave"/>
<!-- Employee is on time off and he is not connected -->
<i t-if="record.hr_icon_display === 'presence_holiday_absent'" class="fa fa-fw fa-plane text-warning me-1" title="On Time Off" role="img" aria-label="On Time Off"/>
</xpath>
<xpath expr="//span[hasclass('o-mail-avatar-card-name')]" position="after">
<span t-if="outOfOfficeDateEndText" class="text-warning me-1 small fw-bold" t-esc="outOfOfficeDateEndText"/>
</xpath>
</t>
</templates>

View file

@ -0,0 +1,17 @@
import { getOutOfOfficeDateEndText } from "@hr_holidays/res_partner_model_patch";
import { AvatarCardResourcePopover } from "@resource_mail/components/avatar_card_resource/avatar_card_resource_popover";
import { patch } from "@web/core/utils/patch";
patch(AvatarCardResourcePopover.prototype, {
get fieldNames() {
return [...super.fieldNames, "leave_date_to"];
},
get outOfOfficeDateEndText() {
if (!this.record.leave_date_to) {
return "";
}
return getOutOfOfficeDateEndText(this.record.leave_date_to);
},
});

View file

@ -0,0 +1,127 @@
import { Component, onWillStart, useState } from "@odoo/owl";
import { ConfirmationDialog } from "@web/core/confirmation_dialog/confirmation_dialog";
import { _t } from "@web/core/l10n/translation";
import { registry } from "@web/core/registry";
import { useService } from "@web/core/utils/hooks";
import { useRecordObserver } from "@web/model/relational_model/utils";
import { standardFieldProps } from "@web/views/fields/standard_field_props";
export class AccrualLevels extends Component {
static template = "hr_holidays.AccrualLevels";
static props = {
...standardFieldProps
};
setup() {
this.orm = useService("orm");
this.action = useService("action");
this.dialog = useService("dialog");
this.state = useState({});
useRecordObserver(async (record) => {
this.state.carryOverDate = this.getCarryOverDate(record);
this.state.data = await this.orm.read("hr.leave.accrual.level", record.data[this.props.name]._currentIds);
if (!this.state.newMilestoneIds.some(id => record.data[this.props.name]._currentIds.includes(id))) {
this.state.newMilestoneIds = record.data[this.props.name]._currentIds;
}
});
onWillStart(async () => {
this.state.newMilestoneIds = this.props.record.data[this.props.name]._currentIds;
});
}
get milestones() {
return this.props.record.data[this.props.name].records.map((record) => ({
id: record.id,
resId: record.evalContext.id,
data: this.state.data.filter((r) => r.id === record.evalContext.id)[0],
onDelete: () => this.deleteMilestone(record.id),
}));
}
getFullDay(day) {
return luxon.DateTime.fromFormat(day, "c", {
locale: this.env.model.config.context.lang.replace("_","-")}).toLocaleString({ weekday: "long" });
}
getFullMonth(month) {
return luxon.DateTime.fromFormat(month, "M", {
locale: this.env.model.config.context.lang.replace("_","-")}).toLocaleString({ month: "long" });
}
getNewMilestoneClass(id){
return this.state.newMilestoneIds.includes(id) ? "" : "new";
}
getCarryOverDate(planRecord) {
switch (planRecord._values.carryover_date) {
case "year_start":
return _t("start of the year");
case "allocation":
return _t("allocation date");
default:
return luxon.DateTime.fromFormat(
`${planRecord._values.carryover_day} ${planRecord._values.carryover_month} 2020`,
"d M y",
{locale: this.env.model.config.context.lang.replace("_","-")})
.toLocaleString({ day:"numeric", month: "long" });
}
}
async openMilestone(id) {
let action;
if (id) {
action = await this.orm.call("hr.leave.accrual.plan", "action_open_accrual_plan_level",
[this.props.record.evalContext.id], { level_id: id });
} else {
action = await this.orm.call("hr.leave.accrual.plan", "action_create_accrual_plan_level",
[this.props.record.evalContext.id]);
}
this.action.doAction(action, {
additionalContext: {
active_id: this.props.record.evalContext.id,
},
onClose: () => this.env.model.root.load(),
});
}
deleteMilestone(id) {
const milestoneRecord = this.props.record.data[this.props.name].records.find(
(record) => record.id === id
);
this.props.record.data[this.props.name].delete(milestoneRecord);
}
async editMilestone(id){
if (this.props.record.dirty) {
this.dialog.add(ConfirmationDialog, {
body: id ?
_t("Do you want to save the changes made to the accrual plan before editing this milestone?") :
_t("Do you want to save the changes made to the accrual plan before creating a new milestone?"),
confirmLabel: _t("Yes, save changes"),
cancelLabel: _t("No, keep the old version"),
cancel: async () => await this.openMilestone(id),
confirm: async () => {
await this.props.record.save({ reload: false });
await this.openMilestone(id);
},
});
} else {
await this.openMilestone(id);
}
}
}
export const accrualLevels = {
component: AccrualLevels,
fieldDependencies: [
{ name: "carryover_day", type: "integer" }
],
relatedFields: () => [{ name: "id", type: "integer" }],
};
registry.category("fields").add("accrual_levels", accrualLevels);

View file

@ -0,0 +1,128 @@
@media (min-width: 576px) {
.o_accrual_plan_settings .o_accrual {
grid-template-columns: 1fr 1fr !important;
}
.o_accrual_level_form .o_accrual {
grid-template-columns: max-content 1fr !important;
}
}
.o_accrual {
.o_field_accrual, .o_field_selection, .o_field_day_selection, .o_field_filterable_selection {
width: fit-content !important;
&:not(.o_readonly_modifier) > *:first-child {
min-width: 50px;
border-bottom: solid $border-width $border-color;
max-width: fit-content;
field-sizing: content;
}
&:not(.o_field_selection, .o_field_day_selection, .o_field_filterable_selection) > *:first-child {
max-width: 8ch;
}
}
.o_form_label {
font-weight: $o-font-weight-normal;
}
}
.o_accrual_plan_summary {
& div {
display: flex !important;
justify-content: center;
}
& p {
width: 70%;
padding: 8px;
font-weight: $o-font-weight-normal;
border: solid $border-width $border-color;
border-radius: $border-radius;
background-color: $o-webclient-background-color;
}
}
.o_accrual_plan_form {
.o_form_sheet_bg {
max-width: 100% !important;
margin: 0;
padding-bottom: 8px;
}
.o_accrual_plan_settings {
.oe_title {
.o_inner_group {
grid-template-columns: max-content 1fr;
}
}
}
.o_accrual_plan_levels {
background-color: $o-webclient-background-color;
& > .o_accrual_levels_scrollable {
overflow-y: auto;
box-sizing: border-box;
&:has(.o_view_nocontent) {
justify-content: center;
}
}
}
}
.o_accrual_level {
.new-text {
top: -20px;
right: 5px;
color: $o-brand-primary;
pointer-events: none;
}
.circle {
background-color: $border-color;
}
.new {
background-color: $o-brand-primary;
border-color: $o-brand-primary;
}
.arrow {
border: solid $border-color;
border-width: 0 $border-width*2 $border-width*2 0;
padding: 5px;
margin-top: -10px - $border-width*2;
transform: rotate(45deg);
}
.o_accrual_bg {
background-color: $o-view-background-color;
}
.delete span:hover {
background-color: $o-danger;
color: $o-white;
}
&:has(.content:hover, .time > span:hover) {
.content {
border-color: $o-brand-primary !important;
}
.time > span {
border-color: $o-brand-primary !important;
}
.timeline > .circle {
border-color: $o-brand-primary !important;
background-color: $o-brand-primary;
}
}
}

View file

@ -0,0 +1,122 @@
<?xml version="1.0" encoding="UTF-8"?>
<templates xml:space="preserve">
<t t-name="hr_holidays.AccrualLevels">
<div class="o_accrual_level w-100 d-flex vertical-align-middle">
<div class="d-flex flex-column align-items-end mx-2" style="width: 150px; min-width: 150px; align-self: center;"/>
<div class="timeline d-flex align-items-center flex-column" style="pointer-events: none;">
<h3 class="my-2" style="margin-left: -85%;">
Milestones
</h3>
</div>
</div>
<t t-foreach="milestones" t-as="level" t-key="level.id">
<div class="o_accrual_level w-100 d-flex vertical-align-middle" t-if="level.data">
<div class="time d-flex flex-column align-items-end mx-2" style="width: 150px; min-width: 150px; align-self: center;">
<span class="o_accrual_bg border w-100 position-relative text-center px-3 py-1 rounded-3" role="button" t-on-click="editMilestone.bind(this, level.data.id)">
<span class="new-text position-absolute fw-bold" t-if="getNewMilestoneClass(level.data.id)">New !</span>
<span class="text">
<t t-if="level.data.milestone_date == 'creation'">
Immediately
</t>
<t t-else="">
After <t t-esc="level.data.start_count"/> <t t-esc="level.data.start_type"/>(s)
</t>
</span>
</span>
</div>
<span class="timeline d-flex align-items-center flex-column" style="pointer-events: none;">
<span class="flex-grow-1 rounded border" style="width: 0;"/>
<span class="circle position-relative border rounded-circle border" style="z-index: 0; height: 15px; width: 15px;" t-att-class="getNewMilestoneClass(level.data.id)"/>
<span class="flex-grow-1 rounded border" style="width: 0;"/>
<i class="arrow down"/>
</span>
<div class="content o_accrual_bg align-self-center w-100 h-100 px-3 py-2 my-3 ms-2 border rounded-3" role="button" t-on-click="editMilestone.bind(this, level.data.id)">
<span class="text">
<h4 class="header m-0" name="header">
Accrual frequency : <t t-esc="level.data.added_value"/> <t t-esc="level.data.added_value_type"/>(s)
<t t-if="level.data.frequency == 'hourly'">
every hour.
<br/>
</t>
<t t-elif="level.data.frequency == 'daily'">
every day.
<br/>
</t>
<t t-elif="level.data.frequency == 'weekly'">
every week on <t t-esc="getFullDay(level.data.week_day)"/>.
<br/>
</t>
<t t-elif="level.data.frequency == 'bimonthly'">
twice a month on the <t t-esc="level.data.first_day"/>
and the
<t t-esc="level.data.second_day"/>
of the month.
<br/>
</t>
<t t-elif="level.data.frequency == 'monthly'">
every month on the <t t-esc="level.data.first_day"/>
of the month.
<br/>
</t>
<t t-elif="level.data.frequency == 'biyearly'">
twice a year on the <t t-esc="level.data.first_month_day"/>
of
<t t-esc="getFullMonth(level.data.first_month)"/>
and the
<t t-esc="level.data.second_month_day"/>
of <t t-esc="getFullMonth(level.data.second_month)"/>.
<br/>
</t>
<t t-elif="level.data.frequency == 'yearly'">
every year on the <t t-esc="level.data.yearly_day"/>
of <t t-esc="getFullMonth(level.data.yearly_month)"/>.
<br/>
</t>
</h4>
<div class="mt-1" t-if="level.data.can_be_carryover || level.data.cap_accrued_time_yearly || level.data.maximum_leave">
<t t-if="level.data.can_be_carryover">
<t t-if="level.data.action_with_unused_accruals == 'lost'">
Unused days will be lost.<br/>
</t>
<t t-elif="level.data.carryover_options == 'unlimited'">
Unused days will be transferred totally on each <t t-esc="state.carryOverDate"/>.
<br/>
</t>
<t t-else="">
Unused days will be transferred with a max of <t t-esc="level.data.postpone_max_days"/> <t t-esc="level.data.added_value_type"/>
on each <t t-esc="state.carryOverDate"/>.
<br/>
</t>
</t>
<t t-if="level.data.cap_accrued_time_yearly">
<t t-if="level.data.cap_accrued_time">
A yearly cap is set to <t t-esc="level.data.maximum_leave_yearly"/> <t t-esc="level.data.added_value_type"/>(s)
and a balance cap is set to <t t-esc="level.data.maximum_leave"/> <t t-esc="level.data.added_value_type"/>(s).
</t>
<t t-else="">
A yearly cap is set to <t t-esc="level.data.maximum_leave_yearly"/> <t t-esc="level.data.added_value_type"/>(s).
</t>
</t>
<t t-elif="level.data.maximum_leave">
A balance cap is set to <t t-esc="level.data.maximum_leave"/> <t t-esc="level.data.added_value_type"/>(s).
</t>
</div>
</span>
</div>
<div class="delete d-flex align-self-center">
<span class="o_accrual_bg d-flex justify-content-center align-items-center border rounded-circle mx-2" style="width: 30px; height: 30px;" t-on-click="level.onDelete.bind(this)" role="button">
<i class="fa fa-trash"/>
</span>
</div>
</div>
</t>
<div class="o_accrual_level w-100 d-flex vertical-align-middle">
<div class="d-flex flex-column align-items-end mx-2" style="width: 150px; min-width: 150px; align-self: center;"/>
<div class="timeline d-flex align-items-center flex-column" style="pointer-events: all;">
<button class="btn btn-secondary my-2" style="margin-left: -85%;" t-on-click="editMilestone.bind(this, false)">
Add a milestone
</button>
</div>
</div>
</t>
</templates>

View file

@ -0,0 +1,41 @@
import { registry } from "@web/core/registry";
import { SelectionField, selectionField } from "@web/views/fields/selection/selection_field";
export class DaySelectionField extends SelectionField {
static props = {
...SelectionField.props,
monthField: String,
};
/**
* @override
* return the available days in the carryover_month
* e.g. February -> [1, 29], april -> [1, 30]
*/
get options() {
let options = super.options;
const carryover_month = this.props.record.data[this.props.monthField];
// lastDay is the last day of the current_month for the leap year 2020
const lastDay = new Date(2020, carryover_month, 0).getDate();
options = options.filter((option) => option[0] <= lastDay);
return options;
}
}
export const daySelectionField = {
...selectionField,
component: DaySelectionField,
extractProps({ attrs }) {
return {
...selectionField.extractProps(...arguments),
monthField: attrs.month_field,
};
},
fieldDependencies: ({ attrs }) => [
{
name: attrs.month_field,
type: "selection",
},
],
};
registry.category("fields").add("day_selection", daySelectionField);

View file

@ -0,0 +1,95 @@
import { onWillStart, useState } from "@odoo/owl";
import { registry } from "@web/core/registry";
import { usePopover } from "@web/core/popover/popover_hook";
import { FloatTimeSelectionPopover } from "./float_time_selection_popover";
import { FloatTimeField, floatTimeField } from "@web/views/fields/float_time/float_time_field";
const { DateTime } = luxon;
function floatToHoursMinutes(floatValue) {
const hours = Math.floor(floatValue);
const minutes = Math.round((floatValue - hours) * 60);
return { hours: String(hours).padStart(2, "0"), minutes: String(minutes).padStart(2, "0") };
}
function hoursMinutesToFloat(hours, minutes) {
return parseInt(hours) + minutes / 60;
}
export class FloatTimeSelectionField extends FloatTimeField {
static template = "hr_holidays.FloatTimeSelectionField";
static props = {
...FloatTimeField.props,
};
setup() {
super.setup();
this.popover = usePopover(FloatTimeSelectionPopover, {
onClose: this.onClose.bind(this),
});
this.timeValues = useState({
hours: "00",
minutes: "00",
floatValue: 0,
});
onWillStart(() => {
const initialValue = this.props.record.data[this.props.name];
const { hours, minutes } = floatToHoursMinutes(initialValue);
this.timeValues.hours = hours;
this.timeValues.minutes = minutes;
this.timeValues.floatValue = initialValue;
});
}
get formattedValue() {
const unitAmount = super.formattedValue;
return DateTime
.fromFormat(unitAmount, 'hh:mm', { numberingSystem: 'latn', zone: 'default'})
.toLocaleString({ hour: 'numeric', minute: 'numeric'});
}
onCharHoursClick(ev) {
ev.preventDefault();
this.popover.open(ev.currentTarget, {
timeValues: this.timeValues,
onTimeChange: this.onTimeChange.bind(this),
});
setTimeout(() => {
this.inputFloatTimeRef.el.focus(); // focus on the input rather than the popover
}, 0);
}
onTimeChange(newTimeValues) {
this.timeValues.hours = newTimeValues.hours;
this.timeValues.minutes = newTimeValues.minutes;
this.timeValues.floatValue = parseInt(newTimeValues.hours) + newTimeValues.minutes / 60;
}
handleInputChange() {
this.popover.close();
const inputValue = this.inputFloatTimeRef.el.value;
const [hours, minutes] = inputValue.split(":").map(Number);
if (!isNaN(hours) && !isNaN(minutes)) {
this.timeValues.hours = String(hours).padStart(2, "0");
this.timeValues.minutes = String(minutes).padStart(2, "0");
this.timeValues.floatValue = hoursMinutesToFloat(hours, minutes);
} else {
const { hours, minutes } = floatToHoursMinutes(parseFloat(inputValue));
this.timeValues.hours = hours;
this.timeValues.minutes = minutes;
this.timeValues.floatValue = parseFloat(inputValue);
}
}
onClose() {
this.props.record.update({ [this.props.name]: this.timeValues.floatValue });
}
}
export const charHours = {
...floatTimeField,
component: FloatTimeSelectionField,
};
registry.category("fields").add("float_time_selection", charHours);

View file

@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<templates xml:space="preserve">
<t t-name="hr_holidays.FloatTimeSelectionField" t-inherit="web.FloatTimeField">
<xpath expr="//input" position="attributes">
<attribute name="t-on-input">handleInputChange</attribute>
<attribute name="t-on-click">onCharHoursClick</attribute>
</xpath>
</t>
</templates>

View file

@ -0,0 +1,39 @@
import { Component, useState } from "@odoo/owl";
const numberRange = (min, max) => [...Array(max - min)].map((_, i) => i + min);
const HOURS = numberRange(0, 24).map((hour) => [hour, String(hour)]);
const MINUTES = numberRange(0, 60).map((minute) => [minute, String(minute || 0).padStart(2, "0")]);
export class FloatTimeSelectionPopover extends Component {
static props = {
close: { type: Function },
onTimeChange: { type: Function },
timeValues: {
type: Object,
shape: {
hours: "00",
minutes: "00",
floatValue: 0,
},
},
};
static template = "hr_holidays.FloatTimeSelectionPopover";
setup() {
this.availableHours = HOURS;
this.availableMinutes = MINUTES;
this.state = useState({
selectedHours: this.props.timeValues.hours,
selectedMinutes: this.props.timeValues.minutes,
});
}
onTimeChange() {
this.props.onTimeChange({
hours: this.state.selectedHours,
minutes: this.state.selectedMinutes,
});
}
}

View file

@ -0,0 +1,18 @@
<?xml version="1.0" encoding="UTF-8"?>
<templates xml:space="preserve">
<t t-name="hr_holidays.FloatTimeSelectionPopover">
<div class="position-relative d-flex align-items-center flex-sm-row gap-1 gap-md-1 p-2">
<select class="o_hour_selection form-control form-control-sm w-auto" t-on-change="onTimeChange" t-model="state.selectedHours">
<t t-foreach="availableHours" t-as="unit" t-key="unit[0]">
<option class="text-center" t-att-value="unit[0]" t-esc="unit[1]" t-att-selected="unit[0] == state.selectedHours"/>
</t>
</select>
<span>:</span>
<select class="o_hour_selection form-control form-control-sm w-auto" t-on-change="onTimeChange" t-model="state.selectedMinutes">
<t t-foreach="availableMinutes" t-as="unit" t-key="unit[0]">
<option class="text-center" t-att-value="unit[0]" t-esc="unit[1]" t-att-selected="unit[0] == state.selectedMinutes"/>
</t>
</select>
</div>
</t>
</templates>

View file

@ -0,0 +1,118 @@
import { _t } from "@web/core/l10n/translation";
import { patch } from "@web/core/utils/patch";
import { HrPresenceStatus, hrPresenceStatus } from "@hr/components/hr_presence_status/hr_presence_status";
import { HrPresenceStatusPrivate, hrPresenceStatusPrivate } from "@hr/components/hr_presence_status_private/hr_presence_status_private";
import {
HrPresenceStatusPill,
hrPresenceStatusPill,
} from "@hr/components/hr_presence_status_pill/hr_presence_status_pill";
import {
HrPresenceStatusPrivatePill,
hrPresenceStatusPrivatePill,
} from "@hr/components/hr_presence_status_private_pill/hr_presence_status_private_pill";
const patchHrPresenceStatus = () => ({
get label() {
if (this.value.includes("holiday")) {
return _t("%(label)s, back on %(date)s",
{
label: super.label,
date: this.props.record.data['leave_date_to'].toLocaleString(
{
day: 'numeric',
month: 'short',
year: 'numeric',
}
)
}
)
}
return super.label
},
get icon() {
if (this.value.startsWith("presence_holiday")) {
return "fa-plane";
}
return super.icon;
},
get color() {
if (this.value.startsWith("presence_holiday")) {
return `${this.value === "presence_holiday_present" ? "text-success" : "o_icon_employee_absent"}`;
}
return super.color;
},
});
const patchHrPresenceStatusPill = () => ({
get color() {
if (this.value.startsWith("presence_holiday")) {
return this.value === "presence_holiday_present"
? "btn-outline-success"
: "btn-outline-warning";
}
return super.color;
},
});
// Applies common patch on both components
patch(HrPresenceStatus.prototype, patchHrPresenceStatus());
patch(HrPresenceStatusPrivate.prototype, patchHrPresenceStatus());
// Applies patch on one component and the other should be affected also, since it's extended from it.
patch(HrPresenceStatusPill.prototype, patchHrPresenceStatusPill());
const patchHrPresenceStatusPrivate = () => ({
get label() {
if (this.props.record.data.current_leave_id){
let label = this.props.record.data.current_leave_id.display_name;
if (this.props.record.data.leave_date_to) {
label += _t(", back on ") + this.props.record.data['leave_date_to'].toLocaleString(
{
day: 'numeric',
month: 'short',
year: 'numeric',
}
)
}
return label;
}
return super.label;
}
});
// Applies patch to hr_presence_status_private to display the time off type instead of default label
patch(HrPresenceStatusPrivate.prototype, patchHrPresenceStatusPrivate());
patch(HrPresenceStatusPrivatePill.prototype, patchHrPresenceStatusPrivate());
Object.assign(hrPresenceStatus, {
fieldDependencies: [
...hrPresenceStatus.fieldDependencies,
{ name: "leave_date_to", type: "date" },
],
});
Object.assign(hrPresenceStatusPrivate, {
fieldDependencies: [
...hrPresenceStatusPrivate.fieldDependencies,
...hrPresenceStatus.fieldDependencies,
{ name: "current_leave_id", type:"many2one"},
],
});
Object.assign(hrPresenceStatusPill, {
fieldDependencies: [
...hrPresenceStatusPill.fieldDependencies,
{ name: "leave_date_to", type: "date" },
],
});
Object.assign(hrPresenceStatusPrivatePill, {
fieldDependencies: [
...hrPresenceStatusPrivatePill.fieldDependencies,
...hrPresenceStatusPill.fieldDependencies,
{ name: "current_leave_id", type: "many2one" },
],
});

View file

@ -1,16 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<templates xml:space="preserve">
<t t-inherit="mail.PersonaImStatusIcon" t-inherit-mode="extension">
<xpath expr="//*[@name='root']" position="inside">
<t t-if="personaImStatusIconView.persona.im_status === 'leave_online'">
<i class="o_PersonaImStatusIcon_icon o-online fa fa-plane fa-stack-1x text-primary" title="Online" role="img" aria-label="User is online"/>
</t>
<t t-if="personaImStatusIconView.persona.im_status === 'leave_away'">
<i class="o_PersonaImStatusIcon_icon o-away fa fa-plane fa-stack-1x text-warning" title="Away" role="img" aria-label="User is away"/>
</t>
<t t-if="personaImStatusIconView.persona.im_status === 'leave_offline'">
<i class="o_PersonaImStatusIcon_icon o-offline fa fa-plane fa-stack-1x text-700" title="Out of office" role="img" aria-label="User is out of office"/>
</t>
</xpath>
</t>
</templates>

View file

@ -1,16 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<templates xml:space="preserve">
<t t-inherit="mail.ThreadIcon" t-inherit-mode="extension">
<xpath expr="//*[@name='noImStatusCondition']" position="before">
<t t-elif="thread.channel.correspondent.im_status === 'leave_online'">
<div class="o_ThreadIcon_online fa fa-fw fa-plane" title="Online"/>
</t>
<t t-elif="thread.channel.correspondent.im_status === 'leave_away'">
<div class="o_ThreadIcon_away fa fa-fw fa-plane text-warning" title="Away"/>
</t>
<t t-elif="thread.channel.correspondent.im_status === 'leave_offline'">
<div class="o_ThreadIcon_offline fa fa-fw fa-plane" title="Out of office"/>
</t>
</xpath>
</t>
</templates>

View file

@ -1,8 +0,0 @@
// -----------------------------------------------------------------------------
// Layout
// -----------------------------------------------------------------------------
.o_ThreadView_outOfOffice {
margin-top: 0;
margin-bottom: 0;
}

View file

@ -1,10 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<templates xml:space="preserve">
<t t-inherit="mail.ThreadView" t-inherit-mode="extension">
<xpath expr="//*[@name='loadingCondition']" position="before">
<t t-if="threadView.thread.channel and threadView.thread.channel.correspondent and threadView.thread.channel.correspondent.outOfOfficeText">
<div class="o_ThreadView_outOfOffice alert alert-primary" t-esc="threadView.thread.channel.correspondent.outOfOfficeText" role="alert"/>
</t>
</xpath>
</t>
</templates>

View file

@ -0,0 +1,8 @@
declare module "models" {
export interface HrEmployee {
leave_date_to: import("luxon").DateTime;
}
export interface ResUsers {
leave_date_to: string;
}
}

View file

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<templates xml:space="preserve">
<t t-inherit="mail.Composer.suggestionPartner" t-inherit-mode="extension">
<xpath expr="//span[@t-if='partner.email']" position="before">
<span t-if="partner.outOfOfficeDateEndText" class="text-warning me-1 smaller fw-bold opacity-75" t-esc="partner.outOfOfficeDateEndText"/>
</xpath>
</t>
</templates>

View file

@ -0,0 +1,10 @@
import { patch } from "@web/core/utils/patch";
import { fields } from "@mail/model/misc";
import { HrEmployee } from "@hr/core/common/hr_employee_model";
patch(HrEmployee.prototype, {
setup() {
super.setup();
this.leave_date_to = fields.Date();
},
});

View file

@ -0,0 +1,13 @@
import { ResUsers } from "@mail/core/common/res_users_model";
import { patch } from "@web/core/utils/patch";
/** @type {import("models").ResUsers} */
const resUsersPatch = {
setup() {
super.setup(...arguments);
/** @type {string} */
this.leave_date_to = undefined;
},
};
patch(ResUsers.prototype, resUsersPatch);

View file

@ -1,21 +1,195 @@
/* @odoo-module */
import { usePopover } from "@web/core/popover/popover_hook";
import { user } from "@web/core/user";
import { formatNumber, useNewAllocationRequest } from "@hr_holidays/views/hooks";
import { useService } from "@web/core/utils/hooks";
import { Component, onWillRender } from "@odoo/owl";
import Popover from "web.Popover";
export class TimeOffCardPopover extends Component {
static template = "hr_holidays.TimeOffCardPopover";
static props = [
"allocated",
"accrual_bonus",
"approved",
"planned",
"left",
"warning",
"closest",
"request_unit",
"exceeding_duration",
"close?",
"allows_negative",
"max_allowed_negative",
"onClickNewAllocationRequest?",
"errorLeaves",
"accrualExcess",
"timeOffType",
"employeeId",
"employeeCompany",
];
const { Component } = owl;
setup() {
this.actionService = useService("action");
}
export class TimeOffCardPopover extends Component {}
TimeOffCardPopover.components = { Popover };
async openLeaves() {
this.actionService.doAction({
type: "ir.actions.act_window",
res_model: "hr.leave",
views: [
[false, "list"],
[false, "form"],
],
domain: [["id", "in", this.props.errorLeaves]],
});
}
TimeOffCardPopover.template = 'hr_holidays.TimeOffCardPopover';
TimeOffCardPopover.props = ['allocated', 'approved', 'planned', 'left', 'usable'];
async allocatedLeaves() {
const { employeeId, timeOffType, employeeCompany } = this.props;
const today = new Date().toISOString().split('T')[0];
const isInHolidaysUserGroup = await user.hasGroup("hr_holidays.group_hr_holidays_user");
export class TimeOffCard extends Component {}
const resModel = "hr.leave.allocation"
const name = "My Allocations"
const context = isInHolidaysUserGroup ? {} : {
list_view_ref: "hr_holidays.hr_leave_allocation_view_tree_my",
form_view_ref: "hr_holidays.hr_leave_allocation_view_form",
}
const domain = [["holiday_status_id", "=", timeOffType], ['employee_company_id','=', employeeCompany],
'|', ["date_to", "=", false], ["date_to", ">=", today],
employeeId ? ['employee_id', '=', employeeId] : ['employee_id.user_id', '=', user.userId]]
TimeOffCard.components = { TimeOffCardPopover };
TimeOffCard.template = 'hr_holidays.TimeOffCard';
TimeOffCard.props = ['name', 'id', 'data', 'requires_allocation'];
openLeaveWindow(this.actionService, resModel, name, domain, context);
}
export class TimeOffCardMobile extends TimeOffCard {}
async navigateInfo(stateList) {
const { employeeId, timeOffType, employeeCompany } = this.props;
const isInHolidaysUserGroup = await user.hasGroup("hr_holidays.group_hr_holidays_user");
TimeOffCardMobile.template = 'hr_holidays.TimeOffCardMobile';
const resModel = "hr.leave"
const name = "My Time Off"
const domain = [
['state', 'in', stateList],
['holiday_status_id', '=', timeOffType], ['company_id','=', employeeCompany],
employeeId ? ['employee_id', '=', employeeId] : ['user_id', '=', user.userId]
];
const context = isInHolidaysUserGroup ? {
search_default_group_date_from: true
} : {
search_default_group_date_from: true,
list_view_ref: "hr_holidays.hr_leave_view_tree_my",
form_view_ref: "hr_holidays.hr_leave_view_form",
}
openLeaveWindow(this.actionService, resModel, name, domain, context);
}
}
export class TimeOffCard extends Component {
static template = "hr_holidays.TimeOffCard";
static props = ["name", "data", "requires_allocation", "employeeId", "holidayStatusId"];
setup() {
this.popover = usePopover(TimeOffCardPopover, {
position: "bottom",
popoverClass: "bg-view",
});
this.newAllocationRequest = useNewAllocationRequest();
this.actionService = useService("action");
this.lang = user.lang;
this.formatNumber = formatNumber;
const { data } = this.props;
this.errorLeaves = Object.values(data.virtual_excess_data).map((data) => data.leave_id);
this.errorLeavesDuration = Object.values(data.virtual_excess_data).reduce(
(acc, data) => acc + data.amount,
0
);
this.updateWarning();
onWillRender(this.updateWarning);
}
updateWarning() {
const { data } = this.props;
const errorLeavesSignificant = data.allows_negative
? this.errorLeavesDuration > data.max_allowed_negative
: this.errorLeavesDuration > 0;
const accrualExcess = this.getAccrualExcess(data);
const closeExpire =
data.closest_allocation_duration &&
data.closest_allocation_duration < data.virtual_remaining_leaves;
this.warning = errorLeavesSignificant || accrualExcess || closeExpire;
}
onClickInfo(ev) {
const { data, holidayStatusId, employeeId } = this.props;
this.popover.open(ev.target, {
allocated: formatNumber(this.lang, data.max_leaves),
accrual_bonus: formatNumber(this.lang, data.accrual_bonus),
approved: formatNumber(this.lang, data.leaves_approved),
planned: formatNumber(this.lang, data.leaves_requested),
left: formatNumber(this.lang, data.virtual_remaining_leaves),
warning: this.warning,
closest: data.closest_allocation_duration,
request_unit: data.request_unit,
exceeding_duration: data.exceeding_duration,
allows_negative: data.allows_negative,
max_allowed_negative: data.max_allowed_negative,
onClickNewAllocationRequest: this.newAllocationRequestFrom.bind(this),
errorLeaves: this.errorLeaves,
accrualExcess: this.getAccrualExcess(data),
timeOffType: holidayStatusId,
employeeId: employeeId,
employeeCompany: data.employee_company
});
}
getAccrualExcess(data) {
return data.allows_negative
? -data.exceeding_duration > data.max_allowed_negative
: -data.exceeding_duration > 0;
}
async newAllocationRequestFrom() {
this.popover.close();
await this.newAllocationRequest(this.props.employeeId, this.props.holidayStatusId);
}
async navigateTimeOffType() {
const { employeeId, holidayStatusId, data } = this.props;
const isInHolidaysUserGroup = await user.hasGroup("hr_holidays.group_hr_holidays_user");
const resModel = "hr.leave"
const name = "My Time Off"
const domain = [
['holiday_status_id', '=', holidayStatusId], ['company_id','=', data.employee_company],
employeeId ? ['employee_id', '=', employeeId] : ['user_id', '=', user.userId]
];
const context = isInHolidaysUserGroup ? {
search_default_group_date_from: true
} : {
list_view_ref: "hr_holidays.hr_leave_view_tree_my",
form_view_ref: "hr_holidays.hr_leave_view_form",
search_default_group_date_from: true
};
openLeaveWindow(this.actionService, resModel, name, domain, context);
}
}
function openLeaveWindow(actionService, resModel, name, domain, context) {
actionService.doAction({
type: "ir.actions.act_window",
name: name,
res_model: resModel,
views: [
[false, "list"],
[false, "form"]
],
domain: domain,
context: context
});
}
export class TimeOffCardMobile extends TimeOffCard {
static template = "hr_holidays.TimeOffCardMobile";
}

View file

@ -1,11 +1,15 @@
.o_timeoff_card {
flex-direction: column;
flex-grow: 1;
display: flex;
flex-direction: column;
justify-content: start;
flex: 1;
text-align: center;
border-right: 1px solid $border-color;
&:not(:last-child) {
border-right: 2px solid $border-color;
&>* {
width: fit-content;
margin-left: auto;
margin-right: auto;
}
span {
@ -18,9 +22,15 @@
margin-bottom: 3px;
}
.o_timeoff_name.btn-link:hover {
color: $btn-link-hover-color;
}
.o_timeoff_duration {
font-size: 30px;
font-weight: bold;
width: auto;
padding: auto 5px;
img {
height: 30px;
@ -32,43 +42,23 @@
font-size: 10px;
}
.o_timeoff_details {
width: fit-content;
margin: 0 auto;
cursor: pointer;
}
.o_timeoff_info {
display: inline-block;
margin-right: -10px;
span {
cursor: pointer;
padding-right: 10px;
}
}
}
.o_timeoff_card_mobile {
.o_timeoff_green {
color: $o-enterprise-primary-color;
}
}
.o_timeoff_popover {
background-color: $o-view-background-color;
font-size: 12px;
ul {
list-style-type: none;
margin-bottom: 0;
padding: 0;
}
li {
display: flex;
justify-content: space-between;
span {
padding-left: 3px;
}
padding-right: 10px;
}
}
.o_time_off_icon_types img {
filter: var(--timeOffCard-icon-filter);
}
.o_time_off_card_popover_warning {
max-width: 300px;
}

View file

@ -1,18 +1,23 @@
<?xml version="1.0" encoding="UTF-8" ?>
<templates xml:space="preserve">
<div t-name="hr_holidays.TimeOffCard" owl="1" class="o_timeoff_card py-3 text-odoo">
<div t-name="hr_holidays.TimeOffCard" class="o_timeoff_card py-3 text-primary">
<t t-set="data" t-value="props.data"/>
<t t-set="duration" t-value="props.requires_allocation ? data.virtual_remaining_leaves : data.virtual_leaves_taken"/>
<t t-set="parsed_duration" t-value="formatNumber(this.lang, duration)"/>
<t t-set="show_popover" t-value="true"/>
<strong class="o_timeoff_name"><t t-esc="props.name"/></strong>
<strong class="o_timeoff_name cursor-pointer btn-link" t-on-click="navigateTimeOffType"><t t-esc="props.name"/></strong>
<span class="o_timeoff_duration">
<t t-if="data and data.icon">
<img t-att-src="data.icon" />
</t>
<t t-esc="duration"/>
<span t-att-style="data.holds_changes ? 'color: #B6D369;' : ''">
<t t-esc="parsed_duration"/>
</span>
</span>
<div class="text-uppercase">
<span
t-att-class="'text-uppercase o_timeoff_details p-1' + (warning ? ' alert alert-warning' : '')"
t-on-click.stop="(ev) => (show_popover &amp;&amp; this.onClickInfo(ev))">
<t t-if="data.request_unit == 'hour'" name="duration_unit">hours</t>
<t t-else="">days</t>
<t t-if="props.requires_allocation" name="duration_type">
@ -21,15 +26,10 @@
<t t-else="">
taken
</t>
<t t-if="show_popover">
<TimeOffCardPopover
allocated="data.max_leaves"
approved="data.leaves_approved"
planned="data.leaves_requested"
left="data.virtual_remaining_leaves"
usable="data.usable_remaining_leaves" />
</t>
</div>
<span t-if="show_popover"
t-att-class="'o_timeoff_info fa' + (warning ? ' fa-exclamation-triangle' : ' fa-question-circle-o')"
/>
</span>
<span t-if="props.requires_allocation and data.closest_allocation_expire !== false" class="text-uppercase o_timeoff_validity">
<t t-if="data.closest_allocation_remaining != data.virtual_remaining_leaves">
(<t t-esc="data.closest_allocation_remaining"/> <t t-if="data.request_unit == 'hour'">hours</t><t t-else="">days</t>
@ -41,34 +41,51 @@
</span>
</div>
<t t-name="hr_holidays.TimeOffCardMobile" owl="1">
<t t-name="hr_holidays.TimeOffCardMobile">
<t t-set="data" t-value="props.data"/>
<t t-set="duration" t-value="props.requires_allocation ? data.virtual_remaining_leaves : data.virtual_leaves_taken" />
<span class="float-end o_timeoff_card_mobile">
<t t-if="props.requires_allocation" name="duration_type">
<strong t-esc="duration" class="o_timeoff_green"/> / <span t-esc="data.max_leaves"/> <t t-if="data.request_unit == 'hour'">Hours</t><t t-else="">Days</t> <span class="o_timeoff_green">Available</span>
<strong t-esc="duration" class="o_timeoff_green text-success"/> / <span t-esc="data.max_leaves"/> <t t-if="data.request_unit == 'hour'">Hours</t><t t-else="">Days</t> <span class="o_timeoff_green text-success">Available</span>
</t>
<t t-else="">
<strong t-esc="duration"/> <t t-if="data.request_unit == 'hour'">Hours</t><t t-else="">Days</t> <span class="text-odoo">Taken</span>
<strong t-esc="duration"/> <t t-if="data.request_unit == 'hour'">Hours</t><t t-else="">Days</t> <span class="text-primary">Taken</span>
</t>
</span>
</t>
<t t-name="hr_holidays.TimeOffCardPopover" owl="1">
<div class="o_timeoff_info">
<Popover position="'right'" popoverClass="'o_timeoff_popover'">
<span class="fa fa-question-circle-o"/>
<t t-set-slot="opened">
<ul>
<li>Allocated: <span t-esc="props.allocated"/></li>
<li>Approved: <span t-esc="props.approved"/></li>
<li style="border-bottom: 1px solid gray;">Planned: <span t-esc="props.planned"/></li>
<li>Left: <span t-esc="props.left"/></li>
<li t-if="props.left != props.usable">Usable: <span t-esc="props.usable"/></li>
</ul>
</t>
</Popover>
<t t-name="hr_holidays.TimeOffCardPopover">
<ul class="list-unstyled p-3 mb-0">
<li class="d-flex justify-content-between">
<span><span class="btn-link p-0 cursor-pointer" t-on-click="allocatedLeaves">Allocated</span> (<span class="btn-link p-0 cursor-pointer" t-on-click="props.onClickNewAllocationRequest">new request</span>):</span>
<span class="ps-1" t-esc="props.allocated"/>
</li>
<li class="d-flex justify-content-between" t-if="props.accrual_bonus != 0">
Accrual (Future): <span class="ps-1" t-esc="props.accrual_bonus"/>
</li>
<li class="d-flex justify-content-between btn-link cursor-pointer" t-on-click="() => this.navigateInfo(['validate'])">Approved: <span class="ps-1" t-esc="props.approved"/></li>
<li class="d-flex justify-content-between border-bottom btn-link cursor-pointer" t-on-click="() => this.navigateInfo(['confirm','validate1'])">Planned: <span class="ps-1" t-esc="props.planned"/></li>
<li class="d-flex justify-content-between">Available: <span class="ps-1" t-esc="props.left"/></li>
</ul>
<div t-if="props.warning" class="alert alert-warning mb-0 pb-0 o_time_off_card_popover_warning">
<span class="d-inline-block m-0 mb-3"
t-if="props.errorLeaves.length">
Some leaves cannot be linked to any allocation. To see those leaves,
<a t-on-click="() => this.openLeaves()" class="cursor-pointer">click here</a>.
</span>
<span class="d-inline-block m-0 mb-3"
t-if="props.closest &amp;&amp; props.closest &lt; props.left">
<i class="fa fa-warning"/> Only <t t-esc="props.closest"/>
<t t-if="props.request_unit == 'hour'"> hours</t>
<t t-else=""> days</t>
can be used before the allocation expires.
</span>
<span class="d-inline-block m-0 mb-3"
t-if="props.accrualExcess">
The leaves planned in the future are exceeding the maximum value of the allocation.
It will not be possible to take all of them.
</span>
</div>
</t>
</templates>

View file

@ -1,42 +1,73 @@
/* @odoo-module */
import { TimeOffCard } from './time_off_card';
import { TimeOffCard } from "./time_off_card";
import { useNewAllocationRequest } from "@hr_holidays/views/hooks";
import { useBus, useService } from "@web/core/utils/hooks";
const { Component, useState, onWillStart } = owl;
import { DateTimeInput } from "@web/core/datetime/datetime_input";
import { Component, useState, onWillStart } from "@odoo/owl";
export class TimeOffDashboard extends Component {
static components = { TimeOffCard, DateTimeInput };
static template = "hr_holidays.TimeOffDashboard";
static props = ["employeeId"];
setup() {
this.orm = useService("orm");
this.actionService = useService("action");
this.newRequest = useNewAllocationRequest();
this.state = useState({
date: luxon.DateTime.now(),
today: luxon.DateTime.now(),
holidays: [],
allocationRequests: 0,
});
useBus(this.env.timeOffBus, 'update_dashboard', async () => {
await this.loadDashboardData()
useBus(this.env.timeOffBus, "update_dashboard", async () => {
await this.loadDashboardData();
});
onWillStart(async () => {
await this.loadDashboardData();
this.loadDashboardData();
});
}
async loadDashboardData() {
const context = {};
if (this.props.employeeId !== null) {
context['employee_id'] = this.props.employeeId;
}
this.state.holidays = await this.orm.call(
'hr.leave.type',
'get_days_all_request',
[],
{
context: context
}
);
getContext() {
const context = { from_dashboard: true };
if (this.props && this.props.employeeId !== null) {
context["employee_id"] = this.props.employeeId;
}
return context;
}
async loadDashboardData(date = false) {
const context = this.getContext();
if (date) {
this.state.date = date;
}
const dashboardData = await this.orm.call(
"hr.employee",
"get_time_off_dashboard_data",
[this.state.date],
{ context }
)
this.state.holidays = dashboardData['allocation_data'];
this.state.allocationRequests = dashboardData['allocation_request_amount'];
this.hasAccrualAllocation = dashboardData['has_accrual_allocation'];
}
async newAllocationRequest() {
await this.newRequest(this.props.employeeId);
}
resetDate() {
this.state.date = luxon.DateTime.now();
this.loadDashboardData();
}
async openPendingRequests() {
if (!this.state.allocationRequests) {
return;
}
const action = await this.orm.call("hr.leave", "open_pending_requests", [], {
context: this.getContext(),
});
this.actionService.doAction(action);
}
}
TimeOffDashboard.components = { TimeOffCard };
TimeOffDashboard.template = 'hr_holidays.TimeOffDashboard';
TimeOffDashboard.props = ['employeeId'];

View file

@ -1,6 +1,16 @@
.o_timeoff_today_button {
border: none;
width: fit-content;
background: none;
transition: all 0.2s;
&:hover {
background-color: rgba(0, 0, 0, 0.1);
}
}
.o_timeoff_dashboard {
display: flex;
justify-content: space-between;
flex-direction: row;
height: auto;
box-shadow: inset 0 -1px 0 $border-color;
@ -9,3 +19,11 @@
z-index: 100;
background-color: $o-webclient-background-color;
}
.o_timeoff_dashboard_cards {
display: flex;
flex-direction: row;
& > * {
flex: 1;
}
}

View file

@ -1,8 +1,38 @@
<?xml version="1.0" encoding="UTF-8" ?>
<templates xml:space="preserve">
<div t-name="hr_holidays.TimeOffDashboard" owl="1" class="o_timeoff_dashboard">
<div t-name="hr_holidays.TimeOffDashboard" class="o_timeoff_dashboard">
<t t-foreach="state.holidays" t-as="holiday" t-key="holiday[3]">
<TimeOffCard name="holiday[0]" data="holiday[1]" requires_allocation="holiday[2] == 'yes'" id="holiday[3]"/>
<TimeOffCard
name="holiday[0]"
data="holiday[1]"
requires_allocation="holiday[2]"
holidayStatusId="holiday[3]"
employeeId="props.employeeId"/>
</t>
<div class="o_timeoff_card p-0 d-flex justify-content-around">
<div class="row justify-content-center align-items-center border-bottom h-25 w-100 p-0" t-if="hasAccrualAllocation">
Balance at the
<div class="p-1" style="max-width: 100px!important">
<DateTimeInput
type="'date'"
value="state.date"
onChange="(date) => this.loadDashboardData(date)"
minDate="state.today"
placeholder.translate="Today"/>
</div>
<button class="o_timeoff_today_button btn btn-secondary" t-on-click="resetDate">Today</button>
</div>
<div class="d-flex flex-column justify-content-center align-items-center w-100">
<strong class="o_timeoff_name">Pending Requests</strong>
<span t-on-click="openPendingRequests"
t-att-class="'o_timeoff_duration' + (state.allocationRequests ? ' cursor-pointer' : '')">
<t t-esc="state.allocationRequests"/>
</span>
<a class="text-uppercase o_timeoff_details p-1" t-on-click="newAllocationRequest">
<t t-if="employeeId">Grant Time</t>
<t t-else="">New Allocation Request</t>
</a>
</div>
</div>
</div>
</templates>

View file

@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<templates xml:space="preserve">
<t t-inherit="discuss.channel_member" t-inherit-mode="extension">
<xpath expr="//*[@t-ref='displayName']" position="inside">
<span t-if="member.partner_id?.outOfOfficeDateEndText" class="text-warning smaller ms-2 text-truncate fw-bold opacity-75">
<t t-esc="member.partner_id.outOfOfficeDateEndText"/>
</span>
</xpath>
</t>
</templates>

View file

@ -0,0 +1,9 @@
<?xml version="1.0" encoding="UTF-8"?>
<templates xml:space="preserve">
<t t-inherit="mail.DiscussCommand" t-inherit-mode="extension">
<xpath expr="//span[hasclass('o-mail-DiscussCommand-nameContainer')]" position="inside">
<span t-if="props.persona?.outOfOfficeDateEndText" class="text-warning smaller ms-2 fw-bold opacity-75" t-out="props.persona.outOfOfficeDateEndText"/>
</xpath>
</t>
</templates>

View file

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<templates xml:space="preserve">
<t t-inherit="mail.DiscussSidebarChannel.main" t-inherit-mode="extension">
<xpath expr="//span[hasclass('o-mail-DiscussSidebarChannel-itemName')]" position="inside">
<div t-if="thread.correspondent?.partner_id?.outOfOfficeDateEndText" class="text-warning smaller" t-out="thread.correspondent.persona.outOfOfficeDateEndText"/>
</xpath>
</t>
</templates>

View file

@ -0,0 +1,12 @@
<?xml version="1.0" encoding="UTF-8"?>
<templates xml:space="preserve">
<t t-inherit="mail.ImStatus" t-inherit-mode="extension">
<xpath expr="//*[@name='icon']" position="replace">
<i t-if="persona.im_status === 'leave_online'" class="fa fa-plane text-success" title="On Leave (Online)" role="img" aria-label="User is on leave and online"/>
<i t-elif="persona.im_status === 'leave_away'" class="fa fa-plane o-yellow" title="On Leave (Idle)" role="img" aria-label="User is on leave and idle"/>
<i t-elif="persona.im_status === 'leave_busy'" class="fa fa-fw fa-plane text-danger" title="On Leave (Busy)" role="img" aria-label="User is on leave and busy"/>
<i t-elif="persona.im_status === 'leave_offline'" class="fa fa-plane text-500" title="On Leave" role="img" aria-label="User is on leave"/>
<t t-else="">$0</t>
</xpath>
</t>
</templates>

View file

@ -1,16 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100">
<g id="Annual_Time_Off" data-name="Annual Time Off">
<g>
<rect x="13.34" y="24.39" width="73.32" height="59.4" rx="6.37" stroke-width="5" stroke="#875b7b" stroke-linecap="round" stroke-linejoin="round" fill="none"/>
<g>
<path d="M30.5,32.12V16.21a2.5,2.5,0,0,0-5,0V32.12a2.5,2.5,0,0,0,5,0Z" fill="#875b7b"/>
<path d="M52.5,32.12V16.21a2.5,2.5,0,0,0-5,0V32.12a2.5,2.5,0,1,0,5,0Z" fill="#875b7b"/>
<path d="M74.5,32.12V16.21a2.5,2.5,0,0,0-5,0V32.12a2.5,2.5,0,1,0,5,0Z" fill="#875b7b"/>
</g>
<g>
<path d="M86.65,40.82H13.35v36.6l3.32,6.21,63.63.16,7.62-3.5ZM38,70a2,2,0,0,1-2,2H28.56a2,2,0,0,1,0-4H34V64.47H28.56a2,2,0,0,1,0-4H34V57.08H28.57a2,2,0,0,1,0-4H36a2,2,0,0,1,2,2Zm15.72-9.49a2,2,0,0,1,2,2V70a2,2,0,0,1-2,2H46.29a2,2,0,0,1-2-2V55.08a2,2,0,0,1,2-2h7.45a2,2,0,0,1,0,4H48.29v3.39Zm17.71,0a2,2,0,0,1,2,2V70a2,2,0,0,1-2,2H64a2,2,0,1,1,0-4h5.44V64.47H64a2,2,0,0,1-2-2V55.08a2,2,0,0,1,2-2h7.44a2,2,0,0,1,0,4H66v3.39Z" fill="#875b7b"/>
<rect x="48.29" y="64.47" width="3.44" height="3.49" fill="#875b7b"/>
</g>
</g>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 1.1 KiB

View file

@ -1,20 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100">
<g id="Annual_Time_Off" data-name="Annual Time Off">
<g>
<g>
<path d="M30.5,32.12V16.21a2.5,2.5,0,0,0-5,0V32.12a2.5,2.5,0,0,0,5,0Z" fill="#875b7b"/>
<path d="M52.5,32.12V16.21a2.5,2.5,0,0,0-5,0V32.12a2.5,2.5,0,1,0,5,0Z" fill="#875b7b"/>
<path d="M74.5,32.12V16.21a2.5,2.5,0,0,0-5,0V32.12a2.5,2.5,0,1,0,5,0Z" fill="#875b7b"/>
</g>
<g>
<path d="M36,72H28.56a2,2,0,0,1,0-4H34V57.08H28.57a2,2,0,0,1,0-4H36a2,2,0,0,1,2,2V70A2,2,0,0,1,36,72Z" fill="#875b7b"/>
<g>
<path d="M35.54,64.47h-7a2,2,0,0,1,0-4h7a2,2,0,0,1,0,4Z" fill="#875b7b"/>
<path d="M53.73,72H46.29a2,2,0,0,1-2-2V55.08a2,2,0,0,1,2-2h7.45a2,2,0,0,1,0,4H48.29v3.39h5.44a2,2,0,0,1,2,2V70A2,2,0,0,1,53.73,72Zm-5.44-4h3.44V64.47H48.29Z" fill="#875b7b"/>
<path d="M71.44,72H64a2,2,0,1,1,0-4h5.44V64.47H64a2,2,0,0,1-2-2V55.08a2,2,0,0,1,2-2h7.44a2,2,0,0,1,0,4H66v3.39h5.44a2,2,0,0,1,2,2V70A2,2,0,0,1,71.44,72Z" fill="#875b7b"/>
</g>
</g>
<path d="M80.29,21.89H79.5V32.12a7.53,7.53,0,0,1-7.27,7.49h-.32a7.56,7.56,0,0,1-7.41-7.5V21.89h-7V32.12a7.53,7.53,0,0,1-7.27,7.49h-.32a7.56,7.56,0,0,1-7.41-7.5V21.89h-7V32.12a7.53,7.53,0,0,1-7.27,7.49h-.32a7.56,7.56,0,0,1-7.41-7.5V21.89h-.79a8.88,8.88,0,0,0-8.87,8.87V77.42a8.88,8.88,0,0,0,8.87,8.87H80.29a8.88,8.88,0,0,0,8.87-8.87V30.76A8.88,8.88,0,0,0,80.29,21.89Zm3.87,55.53a3.87,3.87,0,0,1-3.87,3.87H19.71a3.87,3.87,0,0,1-3.87-3.87V43.32H84.16Z" fill="#875b7b"/>
</g>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 1.5 KiB

View file

@ -1,15 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100">
<g id="Compensatory_Time_Off" data-name="Compensatory Time Off">
<g>
<line x1="23.12" y1="26.88" x2="76.88" y2="26.88" fill="none" stroke="#875b7b" stroke-linecap="round" stroke-linejoin="round" stroke-width="5"/>
<line x1="23.12" y1="84.74" x2="76.88" y2="84.74" fill="none" stroke="#875b7b" stroke-linecap="round" stroke-linejoin="round" stroke-width="5"/>
<line x1="50" y1="15.26" x2="50" y2="84.66" fill="none" stroke="#875b7b" stroke-linecap="round" stroke-linejoin="round" stroke-width="5"/>
<line x1="23.45" y1="27.4" x2="11.73" y2="61.24" fill="none" stroke="#875b7b" stroke-linecap="round" stroke-linejoin="round" stroke-width="5"/>
<line x1="35.24" y1="60.87" x2="23.52" y2="27.03" fill="none" stroke="#875b7b" stroke-linecap="round" stroke-linejoin="round" stroke-width="5"/>
<path d="M23.92,75.75a16.87,16.87,0,0,0,16.55-13.6,2.67,2.67,0,0,0-2.6-3.2H10a2.67,2.67,0,0,0-2.6,3.2A16.87,16.87,0,0,0,23.92,75.75Z" fill="#875b7b"/>
<line x1="75.62" y1="27.4" x2="63.9" y2="61.24" fill="none" stroke="#875b7b" stroke-linecap="round" stroke-linejoin="round" stroke-width="5"/>
<line x1="87.41" y1="60.87" x2="75.69" y2="27.03" fill="none" stroke="#875b7b" stroke-linecap="round" stroke-linejoin="round" stroke-width="5"/>
<path d="M76.08,75.75a16.87,16.87,0,0,0,16.56-13.6A2.67,2.67,0,0,0,90,59H62.13a2.67,2.67,0,0,0-2.6,3.2A16.87,16.87,0,0,0,76.08,75.75Z" fill="#875b7b"/>
</g>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 1.5 KiB

View file

@ -1,11 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100">
<g id="Compensatory_Time_Off" data-name="Compensatory Time Off">
<g>
<polyline points="23.12 66.91 23.12 29.88 76.88 19.88 76.88 59.29" fill="none" stroke="#875b7b" stroke-linecap="round" stroke-linejoin="round" stroke-width="5"/>
<line x1="23.12" y1="84.74" x2="76.88" y2="84.74" fill="none" stroke="#875b7b" stroke-linecap="round" stroke-linejoin="round" stroke-width="5"/>
<line x1="50" y1="15.26" x2="50" y2="84.66" fill="none" stroke="#875b7b" stroke-linecap="round" stroke-linejoin="round" stroke-width="5"/>
<path d="M23.92,75.75a16.87,16.87,0,0,0,16.55-13.6,2.67,2.67,0,0,0-2.6-3.2H10a2.67,2.67,0,0,0-2.6,3.2A16.87,16.87,0,0,0,23.92,75.75Z" fill="#875b7b"/>
<path d="M76.08,65.75a16.87,16.87,0,0,0,16.56-13.6A2.67,2.67,0,0,0,90,49H62.13a2.67,2.67,0,0,0-2.6,3.2A16.87,16.87,0,0,0,76.08,65.75Z" fill="#875b7b"/>
</g>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 940 B

View file

@ -1,11 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100">
<g id="Credit_Time" data-name="Credit Time">
<g>
<path d="M50,88.84A36.55,36.55,0,1,1,86.55,52.29,36.59,36.59,0,0,1,50,88.84Zm0-68.09A31.55,31.55,0,1,0,81.55,52.29,31.59,31.59,0,0,0,50,20.75Z" fill="#875b7b"/>
<path d="M41.35,16.66h17.3a2.5,2.5,0,0,0,0-5H41.35a2.5,2.5,0,0,0,0,5Z" fill="#875b7b"/>
<path d="M41.35,13.66h17.3a2.5,2.5,0,0,0,0-5H41.35a2.5,2.5,0,0,0,0,5Z" fill="#875b7b"/>
<path d="M70.66,22.86l2.23,2.23a1.92,1.92,0,0,0,.8.52,2.32,2.32,0,0,0,1.93,0,2,2,0,0,0,.81-.52l.39-.51a2.5,2.5,0,0,0,.34-1.26l-.09-.66a2.5,2.5,0,0,0-.64-1.1l-2.24-2.24a1.83,1.83,0,0,0-.8-.51,2.23,2.23,0,0,0-1.93,0,1.83,1.83,0,0,0-.8.51l-.39.51a2.43,2.43,0,0,0-.34,1.26l.08.67a2.52,2.52,0,0,0,.65,1.1Z" fill="#875b7b"/>
<path d="M60.78,36.56a13.49,13.49,0,0,0-8.6-3.94V29.87c0-3.21-5-3.22-5,0V33A11.57,11.57,0,0,0,38.43,43.3a11.44,11.44,0,0,0,8.75,11.9V68.42a8.48,8.48,0,0,1-4.42-2.36,2.18,2.18,0,0,1-.64-1.55V61.46A2.5,2.5,0,0,0,39.64,59h0a2.51,2.51,0,0,0-2.5,2.49v3a7.25,7.25,0,0,0,2.1,5.11,13.49,13.49,0,0,0,8,3.87v1.24c0,3.22,5,3.22,5,0V73.34a11.59,11.59,0,0,0,9.39-10.49,11.46,11.46,0,0,0-9.39-12V37.64a8.5,8.5,0,0,1,5.06,2.45,2.15,2.15,0,0,1,.64,1.55v3a2.49,2.49,0,0,0,2.49,2.51h0a2.51,2.51,0,0,0,2.5-2.49V41.66A7.21,7.21,0,0,0,60.78,36.56ZM45.16,48.49a6.4,6.4,0,0,1,2-10.24V50A6.52,6.52,0,0,1,45.16,48.49Zm11.42,14a6.5,6.5,0,0,1-4.4,5.63V56a6.49,6.49,0,0,1,4.4,6.58Z" fill="#875b7b"/>
</g>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 1.5 KiB

View file

@ -1,13 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100">
<g id="Credit_Time" data-name="Credit Time">
<path d="M50,88.84A36.55,36.55,0,1,1,86.55,52.29,36.59,36.59,0,0,1,50,88.84Zm0-68.09A31.55,31.55,0,1,0,81.55,52.29,31.59,31.59,0,0,0,50,20.75Z" fill="#875b7b"/>
<path d="M41.35,16.66h17.3a2.5,2.5,0,0,0,0-5H41.35a2.5,2.5,0,0,0,0,5Z" fill="#875b7b"/>
<path d="M41.35,13.66h17.3a2.5,2.5,0,0,0,0-5H41.35a2.5,2.5,0,0,0,0,5Z" fill="#875b7b"/>
<path d="M70.66,22.86l2.23,2.23a1.92,1.92,0,0,0,.8.52,2.32,2.32,0,0,0,1.93,0,2,2,0,0,0,.81-.52l.39-.51a2.5,2.5,0,0,0,.34-1.26l-.09-.66a2.5,2.5,0,0,0-.64-1.1l-2.24-2.24a1.83,1.83,0,0,0-.8-.51,2.23,2.23,0,0,0-1.93,0,1.83,1.83,0,0,0-.8.51l-.39.51a2.43,2.43,0,0,0-.34,1.26l.08.67a2.52,2.52,0,0,0,.65,1.1Z" fill="#875b7b"/>
<g>
<path d="M43.42,43.62A6.5,6.5,0,0,0,47.18,50V38.25A6.47,6.47,0,0,0,43.42,43.62Z" fill="#875b7b"/>
<path d="M52.18,68.16a6.44,6.44,0,0,0,0-12.21Z" fill="#875b7b"/>
<path d="M50,24.51A27.49,27.49,0,1,0,77.49,52,27.49,27.49,0,0,0,50,24.51Zm12.87,20.2a2.51,2.51,0,0,1-2.5,2.49h0a2.49,2.49,0,0,1-2.49-2.51v-3a2.15,2.15,0,0,0-.64-1.55,8.5,8.5,0,0,0-5.06-2.45V50.81a11.45,11.45,0,0,1,0,22.53v1.37c0,3.22-5,3.22-5,0V73.47a13.49,13.49,0,0,1-8-3.87,7.25,7.25,0,0,1-2.1-5.11v-3A2.51,2.51,0,0,1,39.63,59h0a2.5,2.5,0,0,1,2.49,2.51v3.05a2.18,2.18,0,0,0,.64,1.55,8.48,8.48,0,0,0,4.42,2.36V55.2a11.43,11.43,0,0,1,0-22.23v-3.1c0-3.22,5-3.21,5,0v2.75a13.49,13.49,0,0,1,8.6,3.94,7.21,7.21,0,0,1,2.1,5.1Z" fill="#875b7b"/>
</g>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 1.5 KiB

View file

@ -1,14 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100">
<g id="Extra_Time_Off" data-name="Extra Time Off">
<g>
<path d="M56.9,12.07a2.73,2.73,0,0,1,2.79,2.63l.46,9.13A2.77,2.77,0,0,0,64.5,26L72,20.75a2.71,2.71,0,0,1,1.58-.52A2.78,2.78,0,0,1,76,24.28l-4.16,8.13a2.77,2.77,0,0,0,2.45,4h.24l9.11-.76.26,0a2.77,2.77,0,0,1,1.47,5.1l-7.67,5a2.77,2.77,0,0,0,.33,4.83l8.26,3.89a2.76,2.76,0,0,1-1,5.26l-9.13.46A2.77,2.77,0,0,0,74,64.5L79.25,72A2.78,2.78,0,0,1,77,76.36,2.82,2.82,0,0,1,75.72,76l-8.13-4.16a2.82,2.82,0,0,0-1.26-.31,2.77,2.77,0,0,0-2.76,3l.76,9.11a2.74,2.74,0,0,1-2.79,3,2.67,2.67,0,0,1-2.29-1.27l-5-7.67A2.72,2.72,0,0,0,52,76.48a2.75,2.75,0,0,0-2.51,1.59l-3.89,8.26a2.66,2.66,0,0,1-2.47,1.6,2.73,2.73,0,0,1-2.79-2.63l-.46-9.13A2.77,2.77,0,0,0,35.5,74L28,79.25a2.71,2.71,0,0,1-1.58.52,2.78,2.78,0,0,1-2.46-4l4.16-8.13a2.77,2.77,0,0,0-2.45-4h-.24l-9.11.76-.26,0a2.77,2.77,0,0,1-1.47-5.1l7.67-5a2.77,2.77,0,0,0-.33-4.83l-8.26-3.89a2.76,2.76,0,0,1,1-5.26l9.13-.46A2.77,2.77,0,0,0,26,35.5L20.75,28A2.78,2.78,0,0,1,23,23.64a2.82,2.82,0,0,1,1.28.32l8.13,4.16a2.76,2.76,0,0,0,4-2.69l-.76-9.11a2.74,2.74,0,0,1,2.79-3,2.67,2.67,0,0,1,2.29,1.27l5,7.67A2.72,2.72,0,0,0,48,23.52a2.75,2.75,0,0,0,2.51-1.59l3.89-8.26a2.66,2.66,0,0,1,2.47-1.6m0-4h0A6.67,6.67,0,0,0,50.81,12l-3,6.26L44.1,12.41a6.69,6.69,0,0,0-5.64-3.09,6.88,6.88,0,0,0-5.05,2.2,6.67,6.67,0,0,0-1.73,5.14l.58,6.89L26.1,20.4a6.67,6.67,0,0,0-3.1-.76,6.78,6.78,0,0,0-6,3.67,6.65,6.65,0,0,0,.46,7L21.41,36l-6.91.35A6.77,6.77,0,0,0,12,49.19l6.26,3L12.41,55.9a6.77,6.77,0,0,0,3.65,12.45l.6,0,6.89-.58L20.4,73.9a6.68,6.68,0,0,0,.22,6.58,6.85,6.85,0,0,0,5.8,3.29,6.75,6.75,0,0,0,3.86-1.23l5.68-4,.35,6.91A6.76,6.76,0,0,0,49.19,88l3-6.26,3.76,5.81a6.69,6.69,0,0,0,5.64,3.09,6.88,6.88,0,0,0,5.05-2.2,6.68,6.68,0,0,0,1.73-5.14l-.58-6.89L73.9,79.6a6.67,6.67,0,0,0,3.1.76,6.78,6.78,0,0,0,6-3.67,6.65,6.65,0,0,0-.46-7l-4-5.68,6.91-.35A6.77,6.77,0,0,0,88,50.81l-6.26-3,5.81-3.76a6.77,6.77,0,0,0-3.65-12.45l-.6,0-6.89.58L79.6,26.1a6.68,6.68,0,0,0-.22-6.58,6.85,6.85,0,0,0-5.8-3.29,6.75,6.75,0,0,0-3.86,1.23L64,21.41l-.35-6.91A6.71,6.71,0,0,0,56.9,8.07Z" fill="#875b7b"/>
<g>
<path d="M34.89,56.11a1.26,1.26,0,0,0,.35-1.73A1.28,1.28,0,0,0,33.5,54l-2.75,1.84a1.23,1.23,0,0,0-.41.49,1.08,1.08,0,0,0-.06,1,1,1,0,0,0,.11.2s0,0,0,0l5.13,7.64a1.17,1.17,0,0,0,1.61.37,1.73,1.73,0,0,0,.24-.1L40,63.75a1.25,1.25,0,0,0-1.39-2.08l-1.72,1.15L35.66,61l1.69-1.13A1.25,1.25,0,0,0,36,57.75l-1.68,1.13-1.09-1.62Z" fill="#875b7b"/>
<path d="M41.05,49.06a1.51,1.51,0,0,0-1,1.85v0h0a1.5,1.5,0,0,0-1.84,2.36l3.22,2.52,1.1,3.93a1.5,1.5,0,0,0,1.85,1,1.48,1.48,0,0,0,1-1.85h0a1.5,1.5,0,0,0,1.76.06,1.32,1.32,0,0,0,.35-.32,1.51,1.51,0,0,0-.26-2.11L44,54,42.9,50.1A1.51,1.51,0,0,0,41.05,49.06Z" fill="#875b7b"/>
<path d="M48.13,44.21l-2.79,1.87A1.26,1.26,0,0,0,45,47.81a1.28,1.28,0,0,0,1.74.35l.31-.22,4.46,6.65a1.25,1.25,0,0,0,2.08-1.39l-4.46-6.65.39-.26a1.25,1.25,0,0,0-1.39-2.08Z" fill="#875b7b"/>
<path d="M58.44,45.63l.82-.55a1.29,1.29,0,0,0,.53-.8,1.25,1.25,0,0,0-.19-.94l-.07-.07-2.36-3.51a1.26,1.26,0,0,0-1.73-.34l-2.58,1.73a1.22,1.22,0,0,0-.63,1.63s0,0,0,0l.08.15,5.11,7.61a1.25,1.25,0,0,0,2.08-1.39l-.42-.63,1.74.65a1.23,1.23,0,0,0,1.6-.74,1.23,1.23,0,0,0-.73-1.6Zm-1.63-1.92-.67.45-1-1.52.66-.45Z" fill="#875b7b"/>
<path d="M62.5,34.6l-2.58,1.73a1.27,1.27,0,0,0-.48.66,1,1,0,0,0,0,.62.44.44,0,0,0,0,.1.76.76,0,0,0,0,.11.85.85,0,0,0,.13.25l5.1,7.6a1.25,1.25,0,0,0,2.08-1.39L64.83,41.4l.72-.49,2,2.93a1.25,1.25,0,0,0,2.08-1.4l-5.17-7.7A1.21,1.21,0,0,0,62.5,34.6Zm.93,4.72-1.08-1.61.73-.49,1.08,1.62Z" fill="#875b7b"/>
</g>
</g>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 3.6 KiB

View file

@ -1,9 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100">
<g id="Extra_Time_Off" data-name="Extra Time Off">
<g>
<path d="M62.35,37.71l1.08,1.61.73-.48-1.08-1.62Z" fill="#875b7b"/>
<path d="M55.13,42.64c.34.5.68,1,1,1.52l.67-.45-1-1.52Z" fill="#875b7b"/>
<path d="M86.33,54.43l-8.26-3.89a2.77,2.77,0,0,1-.33-4.83l7.67-5a2.76,2.76,0,0,0-1.73-5.08l-9.11.76a2.77,2.77,0,0,1-2.69-4L76,24.28a2.77,2.77,0,0,0-4-3.53L64.5,26a2.77,2.77,0,0,1-4.35-2.13l-.46-9.13a2.76,2.76,0,0,0-5.26-1l-3.89,8.26a2.77,2.77,0,0,1-4.83.33l-5-7.67a2.76,2.76,0,0,0-5.08,1.73l.76,9.11a2.77,2.77,0,0,1-4,2.69L24.28,24a2.77,2.77,0,0,0-3.53,4L26,35.5a2.77,2.77,0,0,1-2.13,4.35l-9.13.46a2.76,2.76,0,0,0-1,5.26l8.26,3.89a2.77,2.77,0,0,1,.33,4.83l-7.67,5a2.76,2.76,0,0,0,1.73,5.08l9.11-.76a2.77,2.77,0,0,1,2.69,4L24,75.72a2.77,2.77,0,0,0,4,3.53L35.5,74a2.77,2.77,0,0,1,4.35,2.13l.46,9.13a2.76,2.76,0,0,0,5.26,1l3.89-8.26a2.77,2.77,0,0,1,4.83-.33l5,7.67a2.76,2.76,0,0,0,5.08-1.73l-.76-9.11a2.77,2.77,0,0,1,4-2.69L75.72,76a2.77,2.77,0,0,0,3.53-4L74,64.5a2.77,2.77,0,0,1,2.13-4.35l9.13-.46A2.76,2.76,0,0,0,86.33,54.43ZM40,63.75l-2.64,1.77a1.73,1.73,0,0,1-.24.1,1.17,1.17,0,0,1-1.61-.37L30.4,57.61s0,0,0,0a1,1,0,0,1-.11-.2,1.08,1.08,0,0,1,.06-1,1.23,1.23,0,0,1,.41-.49L33.5,54a1.28,1.28,0,0,1,1.74.34,1.26,1.26,0,0,1-.35,1.73l-1.71,1.15,1.09,1.62L36,57.75a1.25,1.25,0,0,1,1.4,2.08L35.66,61l1.25,1.86,1.72-1.15A1.25,1.25,0,0,1,40,63.75Zm7.47-5.09a1.52,1.52,0,0,1-2.11.26h0a1.5,1.5,0,1,1-2.89.81l-1.1-3.93-3.22-2.52A1.5,1.5,0,0,1,40,50.92h0v0a1.5,1.5,0,1,1,2.89-.81L44,54l3.23,2.51A1.51,1.51,0,0,1,47.49,58.66Zm4-4.07c-1.49-2.21-3-4.43-4.46-6.65l-.31.22A1.28,1.28,0,0,1,45,47.81a1.26,1.26,0,0,1,.34-1.73l2.79-1.87a1.25,1.25,0,0,1,1.39,2.08l-.39.26c1.48,2.22,3,4.43,4.46,6.65A1.25,1.25,0,0,1,51.51,54.59Zm10.92-6.14a1.23,1.23,0,0,1-1.6.74l-1.74-.65.42.63a1.25,1.25,0,0,1-2.08,1.39L52.32,43l-.08-.15v0a1.22,1.22,0,0,1,.63-1.63l2.58-1.73a1.26,1.26,0,0,1,1.73.34l2.36,3.51.07.07a1.25,1.25,0,0,1,.19.94,1.29,1.29,0,0,1-.53.8l-.82.55,3.26,1.22A1.23,1.23,0,0,1,62.43,48.45Zm5.08-4.61-2-2.93-.72.49,1.93,2.88a1.25,1.25,0,0,1-2.08,1.39l-5.1-7.6a.85.85,0,0,1-.13-.25.76.76,0,0,1,0-.11.44.44,0,0,1,0-.1,1,1,0,0,1,0-.62,1.27,1.27,0,0,1,.48-.66L62.5,34.6a1.21,1.21,0,0,1,1.92.14l5.17,7.7A1.25,1.25,0,0,1,67.51,43.84Z" fill="#875b7b"/>
</g>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 2.3 KiB

View file

@ -1,17 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100">
<g id="Maternity_Time_Off" data-name="Maternity Time Off">
<g>
<g>
<path d="M63,11.81H53.58V38.42H20.75v2.35a24,24,0,0,0,24,24H63a24,24,0,0,0,24-24v-5A24,24,0,0,0,63,11.81Z" fill="none" stroke="#875b7b" stroke-linecap="round" stroke-linejoin="round" stroke-width="5"/>
<path d="M46.1,40.92H86.75c3.21,0,3.22-5,0-5H46.1c-3.22,0-3.23,5,0,5Z" fill="#875b7b"/>
<path d="M23.25,38.52V23.4a2.53,2.53,0,0,0-2.5-2.5H13a2.5,2.5,0,0,0,0,5h7.71l-2.5-2.5V38.52a2.5,2.5,0,0,0,5,0Z" fill="#875b7b"/>
<circle cx="28.65" cy="79.82" r="8.37" fill="none" stroke="#875b7b" stroke-linecap="round" stroke-linejoin="round" stroke-width="5"/>
<circle cx="78.1" cy="79.82" r="8.37" fill="none" stroke="#875b7b" stroke-linecap="round" stroke-linejoin="round" stroke-width="5"/>
<path d="M80.64,71.45V59.26a2.5,2.5,0,0,0-5,0V71.45a2.5,2.5,0,0,0,5,0Z" fill="#875b7b"/>
<path d="M31.25,71.45V59.68a2.5,2.5,0,0,0-5,0V71.45a2.5,2.5,0,0,0,5,0Z" fill="#875b7b"/>
</g>
<path d="M28.66,82.27c3.22,0,3.22-5,0-5s-3.22,5,0,5Z" fill="#875b7b"/>
<path d="M78.08,82.27c3.21,0,3.22-5,0-5s-3.23,5,0,5Z" fill="#875b7b"/>
</g>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 1.2 KiB

View file

@ -1,12 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100">
<g id="Maternity_Time_Off" data-name="Maternity Time Off">
<g>
<path d="M89.46,40.77v-5A26.51,26.51,0,0,0,63,9.31H53.58a2.5,2.5,0,0,0-2.5,2.5V35.92H23.25V23.4a2.53,2.53,0,0,0-2.5-2.5H13a2.5,2.5,0,0,0,0,5h5.21V40.77a26.41,26.41,0,0,0,8,18.94v9.51a10.89,10.89,0,1,0,5,0V63.55a26.38,26.38,0,0,0,13.48,3.7H63A26.25,26.25,0,0,0,75.64,64v5.22a10.87,10.87,0,1,0,5,0V60.48A26.41,26.41,0,0,0,89.46,40.77ZM63,14.31A21.49,21.49,0,0,1,84.46,35.79v.13H56.08V14.31ZM44.73,62.25A21.5,21.5,0,0,1,23.25,40.92h61.2a21.39,21.39,0,0,1-7.64,16.26,2.89,2.89,0,0,0-.47.38A21.33,21.33,0,0,1,63,62.25Z" fill="#875b7b"/>
<polygon points="21.36 39.59 22.89 51.45 32.66 62.33 55.54 65.12 78.42 60.8 86.94 47.4 86.94 37.63 21.36 39.59" fill="#875b7b"/>
<line x1="54.7" y1="36.38" x2="69.77" y2="13.5" fill="#875b7b" stroke="#875b7b" stroke-miterlimit="10" stroke-width="5"/>
<path d="M28.66,82.27c3.22,0,3.22-5,0-5s-3.22,5,0,5Z" fill="#875b7b"/>
<path d="M78.08,82.27c3.21,0,3.22-5,0-5s-3.23,5,0,5Z" fill="#875b7b"/>
</g>
<line x1="85.12" y1="24.8" x2="54.7" y2="36.38" fill="#875b7b" stroke="#875b7b" stroke-miterlimit="10" stroke-width="5"/>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 1.2 KiB

View file

@ -1,19 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100">
<g id="Maternity_Time_Off" data-name="Maternity Time Off">
<g>
<g>
<path d="M23.25,38.52V23.4a2.53,2.53,0,0,0-2.5-2.5H13a2.5,2.5,0,0,0,0,5h7.71l-2.5-2.5V38.52a2.5,2.5,0,0,0,5,0Z" fill="#875b7b"/>
<path d="M28.65,90.69A10.87,10.87,0,1,1,39.53,79.82,10.89,10.89,0,0,1,28.65,90.69Zm0-16.74a5.87,5.87,0,1,0,5.88,5.87A5.87,5.87,0,0,0,28.65,74Z" fill="#875b7b"/>
<path d="M78.1,90.69A10.87,10.87,0,1,1,89,79.82,10.89,10.89,0,0,1,78.1,90.69ZM78.1,74A5.87,5.87,0,1,0,84,79.82,5.88,5.88,0,0,0,78.1,74Z" fill="#875b7b"/>
<path d="M80.64,71.45V59.26a2.5,2.5,0,0,0-5,0V71.45a2.5,2.5,0,0,0,5,0Z" fill="#875b7b"/>
<path d="M31.25,71.45V59.68a2.5,2.5,0,0,0-5,0V71.45a2.5,2.5,0,0,0,5,0Z" fill="#875b7b"/>
<g>
<path d="M63,14.31H56.08V35.92H84.46v-.13A21.49,21.49,0,0,0,63,14.31Z" fill="none"/>
<path d="M89.46,35.79A26.51,26.51,0,0,0,63,9.31H53.58a2.5,2.5,0,0,0-2.5,2.5V35.92H20.75a2.5,2.5,0,0,0-2.5,2.5v2.35a26.36,26.36,0,0,0,4.39,14.59,7.65,7.65,0,0,1,5.88-3.17h.32a7.55,7.55,0,0,1,7.41,7.5v6.17a26.41,26.41,0,0,0,8.48,1.4H63a26.13,26.13,0,0,0,7.65-1.14V59.26a7.54,7.54,0,0,1,7.28-7.49h.32a7.49,7.49,0,0,1,6.53,4,26.31,26.31,0,0,0,4.69-15Zm-5,.13H56.08V14.31H63A21.49,21.49,0,0,1,84.46,35.79Z" fill="#875b7b"/>
</g>
</g>
<path d="M28.66,82.27c3.22,0,3.22-5,0-5s-3.22,5,0,5Z" fill="#875b7b"/>
<path d="M78.08,82.27c3.21,0,3.22-5,0-5s-3.23,5,0,5Z" fill="#875b7b"/>
</g>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 1.5 KiB

View file

@ -1,15 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100">
<g id="Paid_Time_Off" data-name="Paid Time Off">
<g>
<path d="M21.46,61.83a3,3,0,0,1,4.24-.27L39.5,73.7H78.9a3,3,0,0,1,0,6H74.37v4.81a3,3,0,0,1-6,0V79.7H45.48v4.81a3,3,0,0,1-6,0V79.7H38.37a3,3,0,0,1-2-.75L21.73,66.07A3,3,0,0,1,21.46,61.83Z" fill="#875b7b"/>
<g>
<rect x="46.5" y="67.63" width="6" height="0.07" fill="#875b7b"/>
<path d="M78.87,41.86c0-.18-.09-.36-.09-.54,0-32.94-58-32.94-58,0a5.58,5.58,0,0,1-.1.59c-.2,2.4,2.94,3.29,4.29,1.3,2.45-3.62,10.12-3.71,12.86-.29.57.71,1.84.95,2.26.14,1.88-3.62,17.81-3.54,18.69,0,.21.88,1.66.59,2.23-.11,2.83-3.48,10.92-3.39,13.51.24C76,45.18,79.12,44.25,78.87,41.86Z" fill="none" stroke="#875b7b" stroke-miterlimit="10" stroke-width="5"/>
<path d="M46.5,40.62v27h6v-27A23.17,23.17,0,0,0,46.5,40.62Z" fill="#875b7b"/>
<path d="M52.5,11.6a3.08,3.08,0,0,0-2.41-3.06,3,3,0,0,0-3.59,3v3h6Z" fill="#875b7b"/>
</g>
</g>
<path d="M21.77,43.82c-3.22-18.89,18.49-25.37,27.4-24.71C43.73,22.74,39,25.49,39,40.56,29.28,37.67,26.2,40.61,21.77,43.82Z" fill="#875b7b"/>
<path d="M76,43.91c3.32-19.85-17.83-26.36-26.84-24.8,5.44,3.63,9.12,7.5,9.12,22.57C65.64,39.83,69.39,41.65,76,43.91Z" fill="#875b7b"/>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 1.3 KiB

View file

@ -1,12 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100">
<g id="Paid_Time_Off" data-name="Paid Time Off">
<g>
<path d="M21.46,61.83a3,3,0,0,1,4.24-.27L39.5,73.7H78.9a3,3,0,0,1,0,6H74.37v4.81a3,3,0,0,1-6,0V79.7H45.48v4.81a3,3,0,0,1-6,0V79.7H38.37a3,3,0,0,1-2-.75L21.73,66.07A3,3,0,0,1,21.46,61.83Z" fill="#875b7b"/>
<g>
<rect x="46.5" y="67.63" width="6" height="0.07" fill="#875b7b"/>
<path d="M78.78,41.32A29.93,29.93,0,0,0,52.5,19V15.6a3.08,3.08,0,0,0-2.41-3.06,3,3,0,0,0-3.59,3V19A29.92,29.92,0,0,0,20.82,41.32a4.28,4.28,0,0,0-.1.59c-.2,2.4,2.94,3.29,4.29,1.3,2.45-3.62,10.12-3.71,12.86-.29a1.53,1.53,0,0,0,2.26.14c3.79-3.52,14.93-3.52,18.69,0A1.5,1.5,0,0,0,61.05,43c2.83-3.48,10.92-3.39,13.51.24,1.39,2,4.56,1,4.31-1.35A3.38,3.38,0,0,0,78.78,41.32Z" fill="#875b7b"/>
<rect x="46.5" y="38.79" width="6" height="28.84" fill="#875b7b"/>
</g>
</g>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 920 B

View file

@ -1,5 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100">
<g id="Parental_Time_Off" data-name="Parental Time Off">
<path d="M94.89,52.33,87.17,34.94a3.76,3.76,0,0,0-3.86-2.2,5.87,5.87,0,1,0-7.52.21,3.76,3.76,0,0,0-3.11.75,2,2,0,0,0-1.05,1L58.9,47.42A3.74,3.74,0,0,0,58,49.07l-4,1.81A3.74,3.74,0,0,0,52.19,50a4.28,4.28,0,1,0-5.52,0,3.8,3.8,0,0,0-1.89.72l-2.86-2a3.91,3.91,0,0,0-.82-1.22L27.65,34a3.75,3.75,0,0,0-2.81-1.09l-.19-.05a5.88,5.88,0,1,0-7.64,0,3.77,3.77,0,0,0-4.18,2.16L5.11,52.33A3.77,3.77,0,0,0,12,55.38l.4-.89A5.12,5.12,0,0,0,13,56.62V73.69a3.77,3.77,0,0,0,3.77,3.77h0a3.77,3.77,0,0,0,3.77-3.77V59.29h.15V73.67a3.77,3.77,0,1,0,7.54,0V56.41a5.19,5.19,0,0,0,.54-2.26V45.76l7,7a3.74,3.74,0,0,0,3.74.93l3.75,2.67v9.2a3.69,3.69,0,0,0,.48,1.8v7.44a2.75,2.75,0,1,0,5.49,0v-5.5h.11v5.49a2.75,2.75,0,0,0,5.49,0V67.19a3.64,3.64,0,0,0,.4-1.64V56.34l5.6-2.55a3.75,3.75,0,0,0,3.38-1l8.64-8.64.74,4.59a1.61,1.61,0,0,1-.2,1.07L65.7,63.28a1.86,1.86,0,0,0,1.89,2.65h4v7.76a3.77,3.77,0,0,0,7.54,0V65.93h.15v7.74a3.77,3.77,0,0,0,7.54,0V65.93h3.43a1.85,1.85,0,0,0,1.88-2.65l-5.47-9.66a3.78,3.78,0,0,0-.9-1.57l-1.21-2.14a1.62,1.62,0,0,1-.18-1.14l.22-1.07L88,55.38a3.77,3.77,0,0,0,6.89-3.05Z" fill="#875b7b"/>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 1.2 KiB

View file

@ -1,8 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100">
<g id="Recovery_Bank_Holiday" data-name="Recovery Bank Holiday">
<g>
<path d="M20,45a7.48,7.48,0,0,1,7.23,7.76c0,.27,0,.53,0,.8A22.91,22.91,0,1,0,57.55,31.86a7.74,7.74,0,0,1-.81,2.43,7.52,7.52,0,0,1-6.62,3.9,7.19,7.19,0,0,1-3.93-1.14l-8.87-5.72a7.44,7.44,0,0,1-4.07-7.59A34.22,34.22,0,0,0,16.93,45.55,7.42,7.42,0,0,1,19.79,45ZM40.1,53.24l12.52.07V43.74a2.5,2.5,0,0,1,5,0V55.28a2.34,2.34,0,0,1-.21,1,2.56,2.56,0,0,1-2.46,2L40.1,58.24a2.5,2.5,0,0,1,0-5Z" fill="#875b7b"/>
<path d="M50.16,20.63c-.65,0-1.3,0-1.95.08,1-1,1.92-1.93,2.87-2.9a2.5,2.5,0,1,0-3.53-3.53l-8.16,8.23a2.48,2.48,0,0,0,.35,4.43l9.16,5.91a2.52,2.52,0,0,0,3.42-.9,2.56,2.56,0,0,0-.89-3.42L47.2,25.81a26.57,26.57,0,0,1,3-.18,27.91,27.91,0,1,1-27.9,27.91c0-.33,0-.65,0-1A2.49,2.49,0,0,0,19.87,50a2.52,2.52,0,0,0-2.59,2.4c0,.39,0,.77,0,1.16a32.91,32.91,0,1,0,32.9-32.91Z" fill="#875b7b"/>
</g>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 956 B

View file

@ -1,12 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100">
<g id="Sick_Time_Off" data-name="Sick Time Off">
<g>
<path d="M44.38,10.28h6.89a3.1,3.1,0,0,1,3.1,3.1V38.33A21.07,21.07,0,0,1,33.3,59.4h0A21.08,21.08,0,0,1,12.23,38.33V13.39a3.1,3.1,0,0,1,3.11-3.1l6.6,0" fill="none" stroke="#875b7b" stroke-linecap="round" stroke-linejoin="round" stroke-width="5"/>
<path d="M32.63,59.93v7.84c0,29.27,44.79,29.27,44.79,0V57.82" fill="none" stroke="#875b7b" stroke-linecap="round" stroke-linejoin="round" stroke-width="5"/>
<g>
<circle cx="77.16" cy="45.77" r="10.6" fill="#875b7b" stroke="#875b7b" stroke-linecap="round" stroke-linejoin="round" stroke-width="5"/>
<path d="M77.19,48.27c3.21,0,3.22-5,0-5s-3.23,5,0,5Z" fill="#875b7b"/>
</g>
</g>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 805 B

View file

@ -1,10 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100">
<g id="Small_Unemployement" data-name="Small Unemployement">
<path d="M50,86.88A36.88,36.88,0,1,1,86.88,50,36.93,36.93,0,0,1,50,86.88Zm0-68.76A31.88,31.88,0,1,0,81.88,50,31.91,31.91,0,0,0,50,18.12Z" fill="#875b7b"/>
<g>
<path d="M34.9,64.53a2.14,2.14,0,0,1-2.14-2.14V54a49.9,49.9,0,0,0,13.75,3l-7.53-5a45.14,45.14,0,0,1-6.22-2V48l-4-2.63v17a6.14,6.14,0,0,0,6.14,6.14H64l-6.08-4Z" fill="#875b7b"/>
<path d="M45.37,35.47h9.26v3.38H46.16L52.49,43H65.1a2.14,2.14,0,0,1,2.14,2.14V50c-.89.33-1.8.63-2.71.91l6.71,4.41V45.15A6.14,6.14,0,0,0,65.1,39H58.63V33.47a2,2,0,0,0-2-2H43.37a2,2,0,0,0-2,2V35.7l4,2.63Z" fill="#875b7b"/>
</g>
<path d="M78.42,71.53a2.46,2.46,0,0,1-1.37-.41L20.59,34a2.5,2.5,0,0,1,2.74-4.18L79.8,66.94a2.5,2.5,0,0,1-1.38,4.59Z" fill="#875b7b"/>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 864 B

View file

@ -1,10 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100">
<g id="Small_Unemployement" data-name="Small Unemployement">
<path d="M50,86.88A36.88,36.88,0,1,1,86.88,50,36.92,36.92,0,0,1,50,86.88Zm0-68.76A31.88,31.88,0,1,0,81.88,50,31.91,31.91,0,0,0,50,18.12Z" fill="#875b7b"/>
<g>
<path d="M71.74,45.15a6.65,6.65,0,0,0-6.64-6.64h-6v-5a2.5,2.5,0,0,0-2.5-2.5H43.37a2.5,2.5,0,0,0-2.5,2.5v1.9L71.74,55.66Zm-17.61-6.8H45.87V36h8.26Z" fill="#875b7b"/>
<path d="M28.26,45.15V62.39A6.65,6.65,0,0,0,34.9,69H64.77L28.26,45Z" fill="#875b7b"/>
</g>
<path d="M78.42,71.53a2.46,2.46,0,0,1-1.37-.41L20.59,34a2.5,2.5,0,0,1,2.74-4.18L79.8,66.94a2.5,2.5,0,0,1-1.38,4.59Z" fill="#875b7b"/>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 717 B

View file

@ -1,11 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100">
<g id="Small_Unemployement" data-name="Small Unemployement">
<g>
<g>
<path d="M76.31,41.83a8.05,8.05,0,0,0-8-8H61.05v-6.1a3,3,0,0,0-3-3H42a3,3,0,0,0-3,3v6.1h-.74l38.1,25ZM55,33.6H45V30.72H55Z" fill="#875b7b"/>
<path d="M23.69,62.69a8,8,0,0,0,8,8H65.67l-42-27.59Z" fill="#875b7b"/>
</g>
<path d="M82.64,75.33a2.89,2.89,0,0,1-1.59-.47L15.77,31.94A2.89,2.89,0,1,1,19,27.11L84.23,70a2.89,2.89,0,0,1-1.59,5.3Z" fill="#875b7b"/>
</g>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 549 B

View file

@ -1,11 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100">
<g id="Training_Time_Off" data-name="Training Time Off">
<g>
<g>
<path d="M21.73,41.93v3c0,2.84,1.25,6.66,6.66,9.72V44.09Z" fill="#875b7b"/>
<path d="M49.7,50.18a16.43,16.43,0,0,1-5-.79L43.39,49v9.88a64.34,64.34,0,0,0,6.61.35c14.07,0,28.27-4.41,28.27-14.27V41.53L54.81,49.35A16.23,16.23,0,0,1,49.7,50.18Z" fill="#875b7b"/>
</g>
<path d="M92.65,26.69l-39-12.14a11.12,11.12,0,0,0-6.73,0L7.33,27.23a2.5,2.5,0,0,0,0,4.76l26.07,8.47V70a8.16,8.16,0,1,0,5,0V42.08l7.86,2.55a11.06,11.06,0,0,0,7,0L92.7,31.45a2.5,2.5,0,0,0,0-4.76Z" fill="#875b7b"/>
</g>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 666 B

View file

@ -1,15 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100">
<g id="Unpaid_Time_Off" data-name="Unpaid Time Off">
<g>
<path d="M61.24,34.26a13.53,13.53,0,0,0-8.6-3.93V27.58c0-3.22-5-3.22-5,0v3.09A11.57,11.57,0,0,0,38.89,41a11.44,11.44,0,0,0,8.75,11.9V66.13a8.5,8.5,0,0,1-4.43-2.36,2.19,2.19,0,0,1-.63-1.55v-3a2.5,2.5,0,0,0-2.49-2.51h0a2.5,2.5,0,0,0-2.5,2.49V62.2a7.21,7.21,0,0,0,2.1,5.11,13.52,13.52,0,0,0,8,3.87v1.24c0,3.22,5,3.22,5,0V71.05A11.58,11.58,0,0,0,62,60.56a11.46,11.46,0,0,0-9.38-12V35.35A8.5,8.5,0,0,1,57.7,37.8a2.2,2.2,0,0,1,.64,1.55v3a2.49,2.49,0,0,0,2.48,2.51h0a2.51,2.51,0,0,0,2.5-2.49V39.37A7.25,7.25,0,0,0,61.24,34.26Zm-13.6,13.4a6.56,6.56,0,0,1-2-1.46,6.4,6.4,0,0,1,2-10.24Zm7.66,7.71a6.42,6.42,0,0,1-2.66,10.5V53.66A6.38,6.38,0,0,1,55.3,55.37Z" fill="#875b7b"/>
<g>
<path d="M80.8,64,66.12,54.35A16.47,16.47,0,0,1,67,60.88a15.83,15.83,0,0,1-1.24,5.22l9.53,6.27a4.93,4.93,0,0,0,2.74.82A5,5,0,0,0,80.8,64Z" fill="#875b7b"/>
<path d="M33.9,40.68a16.07,16.07,0,0,1,1.78-6.37L24.42,26.9a5,5,0,0,0-5.5,8.35L34.25,45.34A16.79,16.79,0,0,1,33.9,40.68Z" fill="#875b7b"/>
</g>
<g>
<line x1="78.33" y1="31.08" x2="21.67" y2="68.47" fill="#875b7b"/>
<path d="M21.68,73.47a5,5,0,0,1-2.76-9.17L75.57,26.9a5,5,0,0,1,5.51,8.35L24.43,72.64A5,5,0,0,1,21.68,73.47Z" fill="#875b7b"/>
</g>
</g>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 1.3 KiB

View file

@ -1,5 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100">
<g id="Credit_Time" data-name="Credit Time">
<path d="M90.13,38.78a5.33,5.33,0,0,0-.28-1.44,1,1,0,0,0,0-.16c-.06-.18-.13-.35-.2-.51a6,6,0,0,0-.3-.56L89.21,36a4.82,4.82,0,0,0-4.15-2.26l-.43.06a4.3,4.3,0,0,0-.62,0H71a5,5,0,0,0,0,10H72.3L61.63,54.44,51.26,44.21s-.08-.06-.12-.1l-9-8.88a4.84,4.84,0,0,0-1.78-1.1,4.94,4.94,0,0,0-5.63.93L19.94,49.85v-2.7a5,5,0,0,0-10,0v13.1a3.93,3.93,0,0,0-.07,1,5.33,5.33,0,0,0,.28,1.44,1.09,1.09,0,0,0,0,.17c.06.17.13.34.2.5a6,6,0,0,0,.3.56l.09.14a4.82,4.82,0,0,0,4.15,2.26l.43-.06a4.3,4.3,0,0,0,.62,0H29a5,5,0,0,0,0-10H27.7L38.37,45.56,48.74,55.79s.08.06.12.1l9,8.88a4.84,4.84,0,0,0,1.78,1.1,4.94,4.94,0,0,0,5.63-.93L80.06,50.15v2.7a5,5,0,0,0,10,0V39.75A3.93,3.93,0,0,0,90.13,38.78Z" fill="none" stroke="#875b7b" stroke-miterlimit="10" stroke-width="5"/>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 869 B

View file

@ -1,5 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100">
<g id="Credit_Time" data-name="Credit Time">
<path d="M90.13,38.78a5.33,5.33,0,0,0-.28-1.44,1,1,0,0,0,0-.16c-.06-.18-.13-.35-.2-.51a6,6,0,0,0-.3-.56L89.21,36a4.82,4.82,0,0,0-4.15-2.26l-.43.06a4.3,4.3,0,0,0-.62,0H71a5,5,0,0,0,0,10H72.3L61.63,54.44,51.26,44.21s-.08-.06-.12-.1l-9-8.88a4.84,4.84,0,0,0-1.78-1.1,4.94,4.94,0,0,0-5.63.93L19.94,49.85v-2.7a5,5,0,0,0-10,0v13.1a3.93,3.93,0,0,0-.07,1,5.33,5.33,0,0,0,.28,1.44,1.09,1.09,0,0,0,0,.17c.06.17.13.34.2.5a6,6,0,0,0,.3.56l.09.14a4.82,4.82,0,0,0,4.15,2.26l.43-.06a4.3,4.3,0,0,0,.62,0H29a5,5,0,0,0,0-10H27.7L38.37,45.56,48.74,55.79s.08.06.12.1l9,8.88a4.84,4.84,0,0,0,1.78,1.1,4.94,4.94,0,0,0,5.63-.93L80.06,50.15v2.7a5,5,0,0,0,10,0V39.75A3.93,3.93,0,0,0,90.13,38.78Z" fill="#875b7b"/>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 815 B

View file

@ -1,11 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100">
<g id="Work_Accident_Time_Off" data-name="Work Accident Time Off">
<g>
<path d="M39,44.17H61A20.59,20.59,0,0,1,81.63,64.75V89.91a0,0,0,0,1,0,0H27.2a8.82,8.82,0,0,1-8.82-8.82V64.75A20.59,20.59,0,0,1,39,44.17Z" fill="none" stroke="#875b7b" stroke-linecap="round" stroke-linejoin="round" stroke-width="5"/>
<circle cx="50" cy="27.11" r="17.02" fill="none" stroke="#875b7b" stroke-linecap="round" stroke-linejoin="round" stroke-width="5"/>
<path d="M63.63,89.8c11.79,0,11.79-16.12,0-16.12H58.5" fill="none" stroke="#875b7b" stroke-linecap="round" stroke-linejoin="round" stroke-width="5"/>
<line x1="26.87" y1="48.15" x2="37.68" y2="88.75" fill="none" stroke="#875b7b" stroke-linecap="round" stroke-linejoin="round" stroke-width="5"/>
<line x1="38.96" y1="44.17" x2="67.63" y2="89.68" fill="none" stroke="#875b7b" stroke-linecap="round" stroke-linejoin="round" stroke-width="5"/>
</g>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 998 B

View file

@ -1,10 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100">
<g id="Work_Accident_Time_Off" data-name="Work Accident Time Off">
<g>
<path d="M39,44.17H61A20.59,20.59,0,0,1,81.63,64.75V89.91a0,0,0,0,1,0,0H27.2a8.82,8.82,0,0,1-8.82-8.82V64.75A20.59,20.59,0,0,1,39,44.17Z" fill="none" stroke="#875b7b" stroke-linecap="round" stroke-linejoin="round" stroke-width="5"/>
<circle cx="50" cy="27.11" r="17.02" fill="none" stroke="#875b7b" stroke-linecap="round" stroke-linejoin="round" stroke-width="5"/>
<path d="M34,73.68H63.63c11.79,0,11.79,16.12,0,16.12" fill="none" stroke="#875b7b" stroke-linecap="round" stroke-linejoin="round" stroke-width="5"/>
<polygon points="37.68 88.75 26.87 48.15 38.96 44.17 67.64 89.68 37.68 88.75" fill="#875b7b" stroke="#875b7b" stroke-linecap="round" stroke-linejoin="round" stroke-width="5"/>
</g>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 877 B

View file

@ -1,17 +0,0 @@
/** @odoo-module **/
import FieldRegistry from 'web.field_registry';
import basic_fields from 'web.basic_fields';
var FieldFloat = basic_fields.FieldFloat;
var FloatWithoutTrailingZeros = FieldFloat.extend({
_renderReadonly: function () {
var value = this._formatValue(this.value);
var parsed_value = parseFloat(value);
value = parsed_value.toString().replace(/\.0+$/, '');
this.$el.text(value);
}
});
FieldRegistry.add('float_without_trailing_zeros', FloatWithoutTrailingZeros);

View file

@ -1,105 +1,173 @@
/* @odoo-module */
import { useService } from '@web/core/utils/hooks';
import { registry } from '@web/core/registry'
import { formatDate } from "@web/core/l10n/dates";
const { Component, useState, onWillStart, onWillUpdateProps } = owl;
import { serializeDateTime } from "@web/core/l10n/dates";
import { registry } from "@web/core/registry";
import { useService } from "@web/core/utils/hooks";
import { useRecordObserver } from "@web/model/relational_model/utils";
import { formatFloatTime } from "@web/views/fields/formatters";
import { Component, useState, onWillStart } from "@odoo/owl";
import { standardWidgetProps } from "@web/views/widgets/standard_widget_props";
import { KanbanMany2OneAvatarEmployeeField } from "@hr/views/fields/many2one_avatar_employee_field/kanban_many2one_avatar_employee_field";
const { DateTime } = luxon;
export class LeaveStatsComponent extends Component {
static template = "hr_holidays.LeaveStatsComponent";
static components = {
KanbanMany2OneAvatarEmployeeField
};
static props = { ...standardWidgetProps};
setup() {
this.orm = useService('orm');
this.orm = useService("orm");
this.state = useState({
leaves: [],
departmentLeaves: [],
date: DateTime,
department: null,
employee: null,
type: null,
has_parent_department: null,
department_name: null,
});
this.date = this.props.record.data.date_from || DateTime.now();
this.department = this.props.record.data.department_id;
this.employee = this.props.record.data.employee_id;
this.date_format = {year: "numeric", month: "2-digit", day: "2-digit"};
this.hour_format = {hour: "2-digit", minute: "2-digit"};
this.state.date_from = this.props.record.data.date_from || DateTime.now();
this.state.date_to = this.props.record.data.date_to || DateTime.now();
this.state.employee = this.props.record.data.employee_id;
this.state.department = this.props.record.data.department_id;
onWillStart(async () => {
await this.loadLeaves(this.date, this.employee);
await this.loadDepartmentLeaves(this.date, this.department, this.employee);
await this.loadLeaves(this.state.employee);
await this.loadDepartmentLeaves(
this.state.department,
this.state.employee
);
});
onWillUpdateProps(async (nextProps) => {
const dateFrom = nextProps.record.data.date_from || DateTime.now();
const dateChanged = this.date !== dateFrom;
const employee = nextProps.record.data.employee_id;
const department = nextProps.record.data.department_id;
if (dateChanged || employee && (this.employee && this.employee[0]) !== employee[0]) {
await this.loadLeaves(dateFrom, employee);
useRecordObserver(async (record) => {
const dateFrom = record.data.date_from || DateTime.now();
const dateTo = record.data.date_to || DateTime.now();
const dateChanged = !this.state.date_from.equals(dateFrom) || !this.state.date_to.equals(dateTo);
this.state.date_from = dateFrom
this.state.date_to = dateTo
const employee = record.data.employee_id;
const department = record.data.department_id;
const proms = [];
if (
dateChanged ||
(employee && (this.state.employee && this.state.employee.id) !== employee.id)
) {
proms.push(this.loadLeaves(employee));
}
if (dateChanged || department && (this.department && this.department[0]) !== department[0]) {
await this.loadDepartmentLeaves(dateFrom, department, employee);
if (
dateChanged ||
(department &&
(this.state.department && this.state.department.id) !== department.id)
) {
proms.push(this.loadDepartmentLeaves(department, employee));
}
this.date = dateFrom;
this.employee = employee;
this.department = department;
})
await Promise.all(proms);
this.state.date_from = dateFrom;
this.state.employee = employee;
this.state.department = department;
if (this.state.department) {
const department_name_array = this.state.department.display_name.split('/');
this.state.department_name = department_name_array.pop();
this.state.has_parent_department = department_name_array.length > 0;
}
});
}
get thisYear() {
return this.date.toFormat('yyyy');
return this.state.date_from.toFormat("yyyy");
}
async loadDepartmentLeaves(date, department, employee) {
if (!(department && employee && date)) {
async loadDepartmentLeaves(department, employee) {
if (!(department && employee)) {
this.state.departmentLeaves = [];
return;
}
const dateFrom = date.startOf('month');
const dateTo = date.endOf('month');
const departmentLeaves = await this.orm.searchRead(
'hr.leave',
const dateFrom = serializeDateTime(this.state.date_from);
const dateTo = serializeDateTime(this.state.date_to);
const leaves = await this.orm.webSearchRead(
"hr.leave",
[
['department_id', '=', department[0]],
['state', '=', 'validate'],
['holiday_type', '=', 'employee'],
['date_from', '<=', dateTo],
['date_to', '>=', dateFrom],
["department_id", "=", department.id],
["state", "=", "validate"],
["employee_id", "!=", employee.id],
["date_from", "<=", dateTo],
["date_to", ">=", dateFrom],
],
['employee_id', 'date_from', 'date_to', 'number_of_days'],
{
specification: {
employee_id: { fields: { display_name: {} } },
date_from: {},
date_to: {},
number_of_days: {},
number_of_hours: {},
leave_type_request_unit: {},
},
}
);
this.state.departmentLeaves = departmentLeaves.map((leave) => {
return Object.assign({}, leave, {
dateFrom: formatDate(DateTime.fromSQL(leave.date_from, { zone: 'utc' }).toLocal()),
dateTo: formatDate(DateTime.fromSQL(leave.date_to, { zone: 'utc' }).toLocal()),
sameEmployee: leave.employee_id[0] === employee[0],
});
});
this.state.departmentLeaves = this.arrangeData(leaves.records)
}
async loadLeaves(date, employee) {
if (!(employee && date)) {
async loadLeaves(employee) {
if (!employee) {
this.state.leaves = [];
return;
}
const dateFrom = date.startOf('year');
const dateTo = date.endOf('year');
this.state.leaves = await this.orm.readGroup(
'hr.leave',
const dateFrom = serializeDateTime(this.state.date_from.startOf("year"));
const dateTo = serializeDateTime(this.state.date_from.endOf("year"));
const leaves = await this.orm.webSearchRead(
"hr.leave",
[
['employee_id', '=', employee[0]],
['state', '=', 'validate'],
['date_from', '<=', dateTo],
['date_to', '>=', dateFrom]
["employee_id", "=", employee.id],
["state", "=", "validate"],
["date_from", "<=", dateTo],
["date_to", ">=", dateFrom],
],
['holiday_status_id', 'number_of_days:sum'],
['holiday_status_id'],
{
specification: {
holiday_status_id: { fields: { display_name: {} } },
date_from: {},
date_to: {},
number_of_days: {},
number_of_hours: {},
leave_type_request_unit: {},
},
}
);
this.state.leaves = this.arrangeData(leaves.records);
}
arrangeData(leaves) {
leaves.forEach((leave) => {
const date_from = DateTime.fromSQL(leave.date_from, { zone: "utc" });
const date_to = DateTime.fromSQL(leave.date_to, { zone: "utc" });
const date_from_string = date_from.toLocal();
const date_to_string = date_to.toLocal();
leave.date_from = date_from_string.toLocaleString(this.date_format);
leave.hour_from = date_from_string.toLocaleString(this.hour_format);
leave.date_to = date_to_string.toLocaleString(this.date_format);
leave.hour_to = date_to_string.toLocaleString(this.hour_format);
leave.number_of_hours = formatFloatTime(Number(leave.number_of_hours.toFixed(2)));
leave.number_of_days = Number(leave.number_of_days.toFixed(2));
})
return leaves
}
}
LeaveStatsComponent.template = 'hr_holidays.LeaveStatsComponent';
registry.category('view_widgets').add('hr_leave_stats', LeaveStatsComponent);
export const leaveStatsComponent = {
component: LeaveStatsComponent,
fieldDependencies: [
{ name: "employee_id", type: "many2one" },
{ name: "date_from", type: "datetime" },
{ name: "date_to", type: "datetime" },
{ name: "department_id", type: "many2one" },
],
};
registry.category("view_widgets").add("hr_leave_stats", leaveStatsComponent);

View file

@ -1,29 +1,80 @@
<?xml version="1.0" encoding="UTF-8" ?>
<templates xml:space="preserve">
<div t-name="hr_holidays.LeaveStatsComponent" owl="1" class="o_leave_stats">
<div t-if="employee" id="o_leave_stats_employee">
<div class="o_hr_leave_subtitle">
<t t-esc="employee[1]"/> in <t t-esc="thisYear"/>
<div t-name="hr_holidays.LeaveStatsComponent" class="o_leave_stats">
<div t-if="state.employee and state.leaves.length !== 0" id="o_leave_stats_employee" class="sub_table px-3 py-2 mb-3">
<div>
<div class="d-flex">
<div class="mx-1">
<KanbanMany2OneAvatarEmployeeField
record="this.props.record"
name="'employee_id'"
/>
</div>
<div class="o_hr_leave_subtitle">
<t t-esc="state.employee.display_name"/>'s summary (<t t-esc="thisYear"/>)
</div>
</div>
</div>
<div t-if="state.leaves.length === 0">
None
</div>
<div t-foreach="state.leaves" t-as="leave" t-key="leave_index" class="d-flex flex-row justify-content-between">
<span t-esc="leave.holiday_status_id[1]"/>
<span><t t-esc="leave.number_of_days"/> day(s)</span>
<div class="d-flex flex-column mb-1">
<div t-foreach="state.leaves" t-as="leave" t-key="leave.id" class="d-flex mb-2 gap-1">
<span class="col-3 fw-bold" t-esc="leave.holiday_status_id.display_name"/>
<div t-if="leave.leave_type_request_unit == 'hour'" class="d-flex gap-2 col-6 flex-wrap">
<span t-esc="leave.date_from"/>
<span t-esc="leave.hour_from" class="fst-italic"/>
<i class="fa fa-long-arrow-right my-1" aria-label="Arrow icon" title="Arrow"/>
<span t-esc="leave.hour_to" class="fst-italic"/>
</div>
<div t-else="" class="col-6 d-flex gap-2 flex-wrap">
<span t-esc="leave.date_from"/>
<i class="fa fa-long-arrow-right my-1" aria-label="Arrow icon" title="Arrow"/>
<span t-esc="leave.date_to"/>
</div>
<div class="col-3 flex-wrap">
(
<span t-if="leave.leave_type_request_unit == 'hour'"><t t-esc="leave.number_of_hours"/> hours</span>
<span t-else=""><t t-esc="leave.number_of_days"/> days</span>
)
</div>
</div>
</div>
</div>
<div t-if="department" id="o_leave_stats_department">
<div class="o_horizontal_separator o_hr_leave_subtitle">
<t t-esc="department[1]"/>
<div t-if="state.department and state.departmentLeaves.length !== 0" id="o_leave_stats_department" class="sub_table px-3 py-2">
<div class="o_horizontal_separator o_hr_leave_subtitle d-flex align-items-center justify-content-between">
<div>
<span t-if="state.has_parent_department" class="mx-1">.../</span>
<span t-esc="state.department_name"/>
<span>'s summary in this period</span>
</div>
</div>
<div t-if="state.departmentLeaves.length === 0">
None
</div>
<div t-foreach="state.departmentLeaves" t-as="leave" t-key="leave_index" t-attf-class="d-flex flex-row justify-content-between {{leave.sameEmployee ? 'fw-bold': ''}}">
<span><t t-esc="leave.employee_id[1]"/>: <t t-esc="leave.number_of_days"/> day(s)</span>
<span><t t-esc="leave.dateFrom"/> - <t t-esc="leave.dateTo"/></span>
<div class="d-flex flex-column mb-1">
<div t-foreach="state.departmentLeaves" t-as="leave" t-key="leave.id" class="d-flex flex-column mb-2">
<div class=" d-flex my-1 gap-1">
<div class="col-3 d-flex">
<KanbanMany2OneAvatarEmployeeField
record="{'data' : {'employee_id': leave.employee_id}, 'fields': this.props.record.fields}"
name="'employee_id'" relation="'hr.employee.public'"
/>
<span t-esc="leave.employee_id.display_name" class="mx-1 fw-bold"/>
</div>
<div t-if="leave.leave_type_request_unit == 'hour'" class="d-flex gap-2 col-6 flex-wrap">
<span t-esc="leave.date_from"/>
<span t-esc="leave.hour_from" class="fst-italic"/>
<i class="fa fa-long-arrow-right my-1" aria-label="Arrow icon" title="Arrow"/>
<span t-esc="leave.hour_to" class="fst-italic"/>
</div>
<div t-else="" class="col-6 d-flex gap-2 flex-wrap">
<span t-esc="leave.date_from"/>
<i class="fa fa-long-arrow-right my-1" aria-label="Arrow icon" title="Arrow"/>
<span t-esc="leave.date_to"/>
</div>
<div class="col-3 flex-wrap">
(
<span t-if="leave.leave_type_request_unit == 'hour'"><t t-esc="leave.number_of_hours"/> hours</span>
<span t-else=""><t t-esc="leave.number_of_days"/> days</span>
)
</div>
</div>
</div>
</div>
</div>
</div>

View file

@ -1,52 +0,0 @@
/** @odoo-module **/
import { registerPatch } from '@mail/model/model_core';
import { attr } from '@mail/model/model_field';
import { clear } from '@mail/model/model_field_command';
import { str_to_date } from 'web.time';
registerPatch({
name: 'Partner',
fields: {
isOnline: {
compute() {
if (['leave_online', 'leave_away'].includes(this.im_status)) {
return true;
}
return this._super();
},
},
/**
* Date of end of the out of office period of the partner as string.
* String is expected to use Odoo's date string format
* (examples: '2011-12-01' or '2011-12-01').
*/
out_of_office_date_end: attr(),
/**
* Text shown when partner is out of office.
*/
outOfOfficeText: attr({
compute() {
if (!this.out_of_office_date_end) {
return clear();
}
if (!this.messaging.locale || !this.messaging.locale.language) {
return clear();
}
const currentDate = new Date();
const date = str_to_date(this.out_of_office_date_end);
const options = { day: 'numeric', month: 'short' };
if (currentDate.getFullYear() !== date.getFullYear()) {
options.year = 'numeric';
}
let localeCode = this.messaging.locale.language.replace(/_/g, '-');
if (localeCode === "sr@latin") {
localeCode = "sr-Latn-RS";
}
const formattedDate = date.toLocaleDateString(localeCode, options);
return _.str.sprintf(this.env._t("Out of office until %s"), formattedDate);
},
}),
},
});

View file

@ -1,14 +1,11 @@
/** @odoo-module **/
import { registry } from "@web/core/registry";
import { RadioField, preloadRadio } from "@web/views/fields/radio/radio_field";
import { RadioField, radioField } from "@web/views/fields/radio/radio_field";
class RadioImageField extends RadioField {}
RadioImageField.template = "hr_holidays.RadioImageField";
class RadioImageField extends RadioField {
static template = "hr_holidays.RadioImageField";
}
registry.category("fields").add("hr_holidays_radio_image", RadioImageField);
registry.category("preloadedData").add("hr_holidays_radio_image", {
loadOnTypes: ["many2one"],
preload: preloadRadio,
registry.category("fields").add("hr_holidays_radio_image", {
...radioField,
component: RadioImageField,
});

View file

@ -1,8 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<templates id="template" xml:space="preserve">
<t t-name="hr_holidays.RadioImageField" owl="1">
<div role="radiogroup" class="d-flex flex-wrap" t-att-aria-label="string">
<t t-name="hr_holidays.RadioImageField">
<div role="radiogroup" class="d-flex flex-wrap gap-4" t-att-aria-label="string">
<t t-if="props.readonly">
<t t-if="value !== false">
<div>

View file

@ -0,0 +1,28 @@
import { ResPartner } from "@mail/core/common/res_partner_model";
import { deserializeDateTime } from "@web/core/l10n/dates";
import { _t } from "@web/core/l10n/translation";
import { patch } from "@web/core/utils/patch";
const { DateTime } = luxon;
/** @param {string} datetime */
export function getOutOfOfficeDateEndText(datetime) {
const foptions = { ...DateTime.DATE_MED };
const dt = typeof datetime === "string" ? deserializeDateTime(datetime) : datetime;
if (dt.year === DateTime.now().year) {
foptions.year = undefined;
}
const fdate = dt.toLocaleString(foptions);
return _t("Back on %(date)s", { date: fdate });
}
patch(ResPartner.prototype, {
/** @returns {string} */
get outOfOfficeDateEndText() {
const employee_id = this.employee_id || this.main_user_id?.employee_id;
if (!employee_id?.leave_date_to) {
return "";
}
return getOutOfOfficeDateEndText(employee_id.leave_date_to);
},
});

View file

@ -1,115 +0,0 @@
$o-hr-holidays-border-color: map-get($grays, '300');
.o_hr_holidays_hierarchy {
margin-left: -$o-horizontal-padding;
margin-right: -$o-horizontal-padding;
@include media-breakpoint-up(lg, $o-extra-grid-breakpoints) {
margin-left: -$o-horizontal-padding*2;
margin-right: -$o-horizontal-padding*2;
}
margin-bottom:-24px;
padding: 10px 0px 24px 16px;
background-color: rgba(128, 128, 128, 0.15);
box-shadow: 0px 1px 1px rgba(17, 17, 17, 0.23);
overflow-x: auto;
.o_hr_holidays_title {
padding: 0px 0px 0px 86px;
font-size: 18px;
}
.o_hr_holidays_hierarchy_readonly {
padding: 40px 0px 0px 40px;
}
.o_hr_holidays_plan_level_container {
counter-reset: o-hr-holidays-accrual-plan-level-counter;
.o-kanban-button-new {
padding: 2px 12px;
margin: 0px 0px 0px 44px;
border-radius: 25px;
}
.o_legacy_kanban_view.o_kanban_ungrouped .o_kanban_record,
.o_kanban_renderer.o_kanban_ungrouped .o_kanban_record {
counter-increment: o-hr-holidays-accrual-plan-level-counter;
display: flex;
flex: 0 0 100%;
border: 0px;
padding: 0px 0px 0px 100px;
margin: 0px 0px 0px 0px;
background-color: transparent;
// Timeline Border
&:before {
content: '';
display: block;
@include o-position-absolute;
height: 100%;
margin-left: 8px;
border-left: 1px dashed darken($o-hr-holidays-border-color, 10%);
}
.o_hr_holidays_plan_level_level::before {
content: counter(o-hr-holidays-accrual-plan-level-counter);
}
// Whole record
.o_hr_holidays_body {
margin-left: 8px;
padding-top: 20px;
// Left side 'Level'
.o_hr_holidays_timeline {
@include o-position-absolute($top: 32px, $left: 6px);
width: 90px;
padding: 3px 0px;
border-radius: 3px;
background-color: $o-btn-secondary-bg;
box-shadow: 0 1px 2px rgba(0,0,0,.1);
}
// Actual kanban card
.o_hr_holidays_card {
position: relative;
margin-left: 22px;
margin-right: 2px;
width: 500px;
border-radius: 3px;
background-color: $o-btn-secondary-bg;
box-shadow: 0 1px 2px rgba(0,0,0,.1);
// Triangle
&:before {
content: '';
@include o-position-absolute($top: 12px, $left: -17px);
margin-left: 10px;
width: 14px;
height: 14px;
background-color: $o-btn-secondary-bg;
border-bottom: 1px solid $o-hr-holidays-border-color;
transform: rotate(45deg);
}
// Circle
&:after {
content: '';
@include o-position-absolute($top: 14px, $left: -36px);
width: 12px;
height: 12px;
border: 2px solid $o-brand-primary;
border-radius: 10px;
background: $o-btn-secondary-bg;
}
.content {
position: relative;
background-color: $o-btn-secondary-bg;
padding: 5px 7px;
font-size: 14px;
}
}
}
}
}
}

View file

@ -0,0 +1,8 @@
.o_hour_selection {
background: none;
&:focus,
&:hover {
border-color: var(--primary);
}
}

View file

@ -1,14 +1,35 @@
.o_hr_leave_form {
.o_form_sheet {
padding: 0!important;
overflow: hidden;
.ribbon-top-right {
top: 16px;
right: -8px;
.modal-content {
.o_hr_leave_form {
.o_form_sheet_bg {
padding-right: 0;
min-height: 160px;
overflow: hidden;
.ribbon {
& span {
--ribbon-font-size: 14px !important;
}
}
.o_form_statusbar {
padding-right: 16px;
}
}
.o_form_sheet {
padding-top: 0;
padding-right: 16px;
padding-bottom: 0;
}
.o_hr_leave_content {
padding-top: 8px;
}
}
}
.o_hr_leave_form {
.o_form_sheet {
padding-top: 0;
padding-right: 16px;
padding-bottom: 0;
}
.o_hr_leave_content {
margin: 0;
.o_group {
margin: 0;
}
@ -26,12 +47,16 @@
.o_hr_leave_subtitle {
font-size: 1.2rem;
}
.o_hr_leave_column {
padding: 16px;
.col_left {
padding-top: 16px;
}
.col_right {
background-color: map-get($grays, '200');
padding-top: 28px;
.sub_table {
background-color: map-get($grays, '200');
border-radius: 0.25rem;
}
padding-top: 16px;
padding-bottom: 16px;
}
.o_hr_leave_date {
display: flex;
@ -43,8 +68,94 @@
flex-direction: row;
}
}
.o_leave_stats {
margin-bottom: 16px;
}
}
.o_cw_popover_holidays {
max-width: 448px !important;
min-width: 250px;
width: fit-content;
}
.o_kanban_renderer {
.o_kanban_record.o_hr_holidays_kanban {
min-height: 105px;
.ribbon {
--Ribbon-wrapper-width: 6.7rem;
span {
--Ribbon-left-position-default: 0.3rem;
}
& .o_medium {
font-size: $font-size-sm !important;
}
}
}
}
.o_holidays_view_kanban .o_kanban_renderer{
.ribbon {
--Ribbon-wrapper-width: 6.77rem;
height: 5rem;
span {
--Ribbon-left-position-default: 0.3rem;
}
& .o_medium {
font-size: $font-size-sm !important;
}
}
&.o_kanban_grouped {
.o_hr_holidays_kanban {
display: none;
}
}
&.o_kanban_ungrouped {
padding: 0px;
@media only screen and (max-width:767px) {
.o_hr_holidays_kanban {
display: none;
}
}
@media only screen and (min-width:768px){
.o_kanban_record:not(.o_kanban_ghost) {
width: 100%;
max-height: 5.1rem;
margin: 0px;
justify-content: center;
}
.o_hr_holidays_kanban_mobile {
display: none;
}
.o_holidays_kanban_card {
display: flex;
flex-direction: column;
height: 100%;
.o_hr_holidays_value {
display: flex;
flex-direction: row;
.o_hr_holidays_name {
display: flex;
flex-direction: column;
gap: 2;
align-items: baseline;
}
.o_hr_holidays_card {
flex-direction: column;
display: flex;
flex-wrap: wrap;
align-items: baseline;
}
}
.o_hr_holidays_button {
display: flex;
flex-direction: row;
align-items: center;
justify-content: center;
gap: 1;
}
}
}
}
}

View file

@ -0,0 +1,11 @@
import { Store } from "@mail/core/common/store_service";
import { patch } from "@web/core/utils/patch";
/** @type {import("models").Store} */
const storeServicePatch = {
get onlineMemberStatuses() {
return [...super.onlineMemberStatuses, "leave_online", "leave_away", "leave_busy"];
},
};
patch(Store.prototype, storeServicePatch);

View file

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<templates xml:space="preserve">
<t t-inherit="mail.Thread" t-inherit-mode="extension">
<xpath expr="//*[hasclass('o-mail-Thread')]" position="before">
<div t-if="props.thread.model === 'discuss.channel' and props.thread.correspondent?.partner_id?.outOfOfficeDateEndText" class="alert alert-primary mb-0 smaller fw-bolder rounded-0 py-1 shadow-sm" t-esc="props.thread.correspondent.partner_id.outOfOfficeDateEndText" role="alert"/>
</xpath>
</t>
</templates>

View file

@ -1,89 +1,100 @@
/** @odoo-module **/
import tour from 'web_tour.tour';
import { _t } from 'web.core';
import { _t } from "@web/core/l10n/translation";
import { markup } from "@odoo/owl";
import { registry } from "@web/core/registry";
import { stepUtils } from "@web_tour/tour_utils";
const leaveType = "NotLimitedHR";
const leaveDateFrom = "01/17/2022";
const leaveDateTo = "01/17/2022";
const description = 'Days off';
tour.register('hr_holidays_tour', {
url: '/web',
rainbowManMessage: _t("Congrats, we can see that your request has been validated."),
test: false
}, [
tour.stepUtils.showAppsMenuItem(),
{
trigger: '.o_app[data-menu-xmlid="hr_holidays.menu_hr_holidays_root"]',
content: _t("Let's discover the Time Off application"),
position: 'bottom',
},
{
trigger: 'button.btn-time-off',
content: _t("Click on any date or on this button to request a time-off"),
position: 'bottom',
},
{
trigger: 'div[name="holiday_status_id"] input',
content: _t("Let's try to create a Sick Time Off, select it in the list"),
run: `text ${leaveType}`,
},
{
trigger: `.ui-autocomplete .ui-menu-item a:contains("${leaveType}")`,
run: "click",
auto: true,
in_modal: false,
},
{
trigger: '.o_field_widget[name="request_date_from"] input',
content: _t("You can select the period you need to take off, from start date to end date"),
position: 'right',
run: `text ${leaveDateFrom}`,
},
{
trigger: '.o_field_widget[name="request_date_to"] input',
content: _t("You can select the period you need to take off, from start date to end date"),
position: 'right',
run: `text ${leaveDateTo}`,
},
{
trigger: 'div[name="name"] textarea',
content: _t("Add some description for the people that will validate it"),
run: `text ${description}`,
position: 'right'
},
{
trigger: `button:contains(${_t('Save')})`,
content: _t("Submit your request"),
position: 'bottom',
},
{
trigger: 'button[data-menu-xmlid="hr_holidays.menu_hr_holidays_approvals"]',
content: _t("Let's go validate it"),
position: 'bottom'
},
{
trigger: 'a[data-menu-xmlid="hr_holidays.menu_open_department_leave_approve"]',
content: _t("Select Time Off"),
position: 'right'
},
{
trigger: 'table.o_list_table',
content: _t("Select the request you just created"),
position: 'bottom',
run: function(actions) {
const rows = this.$anchor.find('tr.o_data_row');
actions.click(rows[0]);
}
},
{
trigger: 'button[name="action_approve"]',
content: _t("Let's approve it"),
position: 'bottom'
},
{
trigger: 'a[data-menu-xmlid="hr_holidays.menu_hr_holidays_root"]',
content: _t("State is now confirmed. We can go back to the calendar"),
position: 'bottom'
}
]);
registry.category("web_tour.tours").add("hr_holidays_tour", {
url: "/odoo",
steps: () => [
stepUtils.showAppsMenuItem(),
{
trigger: '.o_app[data-menu-xmlid="hr_holidays.menu_hr_holidays_root"]',
content: markup(_t("Let's discover the <strong>Time Off</strong> app!")),
tooltipPosition: "bottom",
run: "click",
},
{
trigger: "button.btn-time-off",
content: _t("Click on this button to request a time-off"),
tooltipPosition: "bottom",
run: "click",
},
{
trigger: 'div[name="holiday_status_id"] input',
content: _t("Let's try to create a Sick Time Off, select it in the list"),
run: "click",
},
{
trigger: ".ui-autocomplete .ui-menu-item a:contains('Sick Time Off')",
tooltipPosition: "right",
run: "click",
},
{
trigger: "div[name=request_date_from] button",
content: _t("You can select the period you need to take off"),
tooltipPosition: "right",
run: "click",
},
{
content: _t("Click on the 22nd"),
trigger: ".o_date_item_cell:nth-child(31)",
run: "click",
},
{
content: _t("Click on the 25st"),
trigger: ".o_date_item_cell:nth-child(34)",
run: "click",
},
{
content: "click outside to go back to the time off record",
trigger: ".modal-content",
run: "click",
},
{
trigger: 'div[name="name"] textarea',
content: _t("Add some description for the people that will validate it"),
run: "click",
tooltipPosition: "right",
},
{
trigger: `button:contains(${_t("Submit Request")})`,
content: _t("Submit your request"),
tooltipPosition: "bottom",
run: "click",
},
{
trigger: 'button[data-menu-xmlid="hr_holidays.menu_hr_holidays_management"]',
content: _t("Let's go validate it"),
tooltipPosition: "bottom",
run: "click",
},
{
trigger: 'a[data-menu-xmlid="hr_holidays.menu_open_department_leave_approve"]',
content: _t("Select Time Off"),
tooltipPosition: "right",
run: "click",
},
{
content: "Switch to list view",
trigger: ".o_switch_view.o_list",
run: "click",
},
{
tooltipPosition: "bottom",
content: _t("Select the request you just created"),
trigger: "table.o_list_table tr.o_data_row:nth-child(1)",
run: "click",
},
{
trigger: 'button[name="action_approve"]',
content: _t("Let's approve it"),
tooltipPosition: "bottom",
run: "click",
},
{
isActive: ["auto"],
trigger: `tr.o_data_row:first:not(:has(button[name="action_approve"])),table tbody:not(tr.o_data_row)`,
content: "Verify leave is approved",
},
],
});

View file

@ -0,0 +1,194 @@
/** @odoo-module **/
import { registry } from "@web/core/registry";
import { stepUtils } from "@web_tour/tour_utils";
const leaveType1 = "leave_type_1";
const leaveType2 = "leave_type_2";
const leaveType3 = "leave_type_3";
const noRecords = "No Records";
const company2 = "company_2";
const firstLeaveDateFrom = "01/17/2022";
const firstLeaveDateTo = "01/17/2022";
const secondLeaveDateFrom = "01/18/2022";
const secondLeaveDateTo = "01/18/2022";
registry.category("web_tour.tours").add("hr_leave_type_tour", {
url: "/web",
steps: () => [
stepUtils.showAppsMenuItem(),
{
trigger: '.o_app[data-menu-xmlid="hr_holidays.menu_hr_holidays_root"]',
content: "Open the time-off application",
tooltipPosition: "bottom",
run: "click",
},
{
trigger: 'button[data-menu-xmlid="hr_holidays.menu_hr_holidays_management"]',
content: "Open the management menu",
tooltipPosition: "bottom",
run: "click",
},
{
trigger: 'a[data-menu-xmlid="hr_holidays.menu_open_department_leave_approve"]',
content: "Choose Time Off from the menu",
tooltipPosition: "right",
run: "click",
},
{
trigger: "button.o-kanban-button-new",
content: "Create a new time-off request",
tooltipPosition: "bottom",
run: "click",
},
// Check if a time-off could be requested using leave_type_1 as company_1 is selected by default.
{
trigger: 'div[name="holiday_status_id"] input',
content: "Create a time-off using leave_type_1. Select it from the list",
tooltipPosition: "bottom",
run: `edit ${leaveType1}`,
},
{
isActive: ["auto"],
trigger: `.ui-autocomplete .ui-menu-item a:contains("${leaveType1}")`,
run: "click",
},
{
trigger: `.o_field_widget[name='holiday_status_id'] input:value("${leaveType1}")`,
},
// Check that a time-off cannot be requested using leave_type_2 as company_2 is not selected.
{
trigger: 'div[name="holiday_status_id"] input',
content: "Try to select leave_type_2 from the list. It shouldn't be present",
tooltipPosition: "bottom",
run: `edit ${leaveType2}`,
},
{
trigger: `.ui-autocomplete .ui-menu-item span:contains('${noRecords}')`,
},
// Check if a time-off could be requested using leave_type_3
{
trigger: 'div[name="holiday_status_id"] input',
content: "Select leave_type_3 from the list",
tooltipPosition: "bottom",
run: `edit ${leaveType3}`,
},
{
isActive: ["auto"],
trigger: `.ui-autocomplete .ui-menu-item a:contains("${leaveType3}")`,
run: "click",
},
{
trigger: `.o_field_widget[name='holiday_status_id'] input:value("${leaveType3}")`,
},
{
trigger: "div[name=request_date_from] button",
content: "Let's change the start date of the leave",
run: "click",
},
{
trigger: "input[data-field=request_date_from]",
content: "Select the start date of the leave",
tooltipPosition: "right",
run: `edit ${firstLeaveDateFrom}`,
},
{
trigger: "button[data-field=request_date_to]",
content: "Let's change the end date of the leave",
run: "click",
},
{
trigger: "input[data-field=request_date_to]",
content: "Select the end date of the leave",
tooltipPosition: "right",
run: `edit ${firstLeaveDateTo} && press Enter`,
},
...stepUtils.saveForm(),
{
trigger: 'button[name="action_approve"]',
content: "Approve the leave",
tooltipPosition: "bottom",
run: "click",
},
{
trigger: 'button[name="action_cancel"]',
content:
"Make sure that the leave is approved by checking that the cancel button appears",
},
{
trigger: ".o_switch_company_menu button",
content: "Open the companies selection menu",
tooltipPosition: "bottom",
run: "click",
},
{
trigger: `.o_switch_company_item:contains("${company2}") [role=menuitemcheckbox]`,
content: "Select company_2",
tooltipPosition: "bottom",
run: "click",
},
{
trigger: ".o_switch_company_menu_buttons button:contains(Confirm)",
content: "Confirm the company selection",
tooltipPosition: "bottom",
run: "click",
expectUnloadPage: true,
},
{
trigger: "button.o_form_button_create",
content: "Create a new time-off request",
tooltipPosition: "bottom",
run: "click",
},
{
trigger: 'div[name="holiday_status_id"] input',
content:
"Select leave_type_2 from the list. It should be available now because company_2 is selected",
tooltipPosition: "bottom",
run: `edit ${leaveType2}`,
},
{
isActive: ["auto"],
trigger: `.ui-autocomplete .ui-menu-item a:contains("${leaveType2}")`,
run: "click",
},
{
trigger: `.o_field_widget[name='holiday_status_id'] input:value("${leaveType2}")`,
tooltipPosition: "bottom",
},
{
trigger: "div[name=request_date_from] button",
content: "Let's change the start date of the leave",
run: "click",
},
{
trigger: "input[data-field=request_date_from]",
content: "Select the start date of the leave",
tooltipPosition: "right",
run: `edit ${secondLeaveDateFrom}`,
},
{
trigger: "button[data-field=request_date_to]",
content: "Let's change the end date of the leave",
run: "click",
},
{
trigger: "input[data-field=request_date_to]",
content: "Select the end date of the leave",
tooltipPosition: "right",
run: `edit ${secondLeaveDateTo} && press Enter`,
},
...stepUtils.saveForm(),
{
trigger: 'button[name="action_approve"]',
content: "Approve the leave",
tooltipPosition: "bottom",
run: "click",
},
{
trigger: 'button[name="action_cancel"]',
content:
"Make sure that the leave is approved by checking that the cancel button appears",
},
],
});

View file

@ -1,18 +1,22 @@
/** @odoo-module */
import { _t } from "@web/core/l10n/translation";
import { ConfirmationDialog } from "@web/core/confirmation_dialog/confirmation_dialog";
import { CalendarController } from '@web/views/calendar/calendar_controller';
import { FormViewDialog } from '@web/views/view_dialogs/form_view_dialog';
import { CalendarController } from "@web/views/calendar/calendar_controller";
import { serializeDate } from "@web/core/l10n/dates";
import { TimeOffCalendarFilterPanel } from './filter_panel/calendar_filter_panel';
import { TimeOffFormViewDialog } from '../view_dialog/form_view_dialog';
import { useLeaveCancelWizard } from '../hooks';
const { EventBus, useSubEnv } = owl;
import { TimeOffCalendarSidePanel } from "./calendar_side_panel/calendar_side_panel";
import { TimeOffCalendarMobileFilterPanel } from "./calendar_filter_panel/calendar_mobile_filter_panel";
import { TimeOffFormViewDialog } from "../view_dialog/form_view_dialog";
import { useLeaveCancelWizard } from "../hooks";
import { EventBus, useSubEnv } from "@odoo/owl";
export class TimeOffCalendarController extends CalendarController {
static components = {
...CalendarController.components,
CalendarSidePanel: TimeOffCalendarSidePanel,
MobileFilterPanel: TimeOffCalendarMobileFilterPanel,
};
static template = "hr_holidays.CalendarController";
setup() {
super.setup();
useSubEnv({
@ -25,100 +29,96 @@ export class TimeOffCalendarController extends CalendarController {
return this.model.employeeId;
}
get filterPanelProps() {
return {
...super.filterPanelProps,
employee_id: this.employeeId,
};
}
newTimeOffRequest() {
const context = {};
if (this.employeeId) {
context['default_employee_id'] = this.employeeId;
if (this.props.context.active_id && this.props.context.active_model === "hr.employee") {
context["default_employee_id"] = this.props.context.active_id;
} else if (this.employeeId) {
context["default_employee_id"] = this.employeeId;
}
if (this.model.meta.scale == 'day') {
context['default_date_from'] = serializeDate(
this.model.data.range.start.set({ hours: 7 }), "datetime"
if (this.model.meta.scale == "day") {
context["default_date_from"] = serializeDate(
this.model.data.range.start.set({ hours: 7 }),
"datetime"
);
context['default_date_to'] = serializeDate(
this.model.data.range.end.set({ hours: 19 }), "datetime"
context["default_date_to"] = serializeDate(
this.model.data.range.end.set({ hours: 19 }),
"datetime"
);
}
this.displayDialog(FormViewDialog, {
resModel: 'hr.leave',
title: this.env._t('New Time Off'),
this.displayDialog(TimeOffFormViewDialog, {
resModel: "hr.leave",
title: _t("New Time Off"),
viewId: this.model.formViewId,
onRecordSaved: () => {
this.model.load();
this.env.timeOffBus.trigger('update_dashboard');
this.env.timeOffBus.trigger("update_dashboard");
},
onRecordDeleted: (record) => {},
onLeaveCancelled: (record) => {},
size: "md",
context: context,
});
}
newAllocationRequest() {
const context = {
'form_view_ref': 'hr_holidays.hr_leave_allocation_view_form_dashboard',
};
if (this.employeeId) {
context['default_employee_id'] = this.employeeId;
context['default_employee_ids'] = [this.employeeId];
context['form_view_ref'] = 'hr_holidays.hr_leave_allocation_view_form_manager_dashboard';
}
this.displayDialog(FormViewDialog, {
resModel: 'hr.leave.allocation',
title: this.env._t('New Allocation'),
context: context,
});
}
deleteRecord(record) {
if (!record.can_cancel) {
_deleteRecord(resId, canCancel) {
if (!canCancel) {
this.displayDialog(ConfirmationDialog, {
title: this.env._t("Confirmation"),
body: this.env._t("Are you sure you want to delete this record ?"),
title: _t("Confirmation"),
body: _t("Are you sure you want to delete this record?"),
confirm: async () => {
await this.model.unlinkRecord(record.id);
this.env.timeOffBus.trigger('update_dashboard');
await this.model.unlinkRecord(resId);
this.env.timeOffBus.trigger("update_dashboard");
},
cancel: () => {},
});
} else {
this.leaveCancelWizard(record.id, () => {
this.leaveCancelWizard(resId, () => {
this.model.load();
this.env.timeOffBus.trigger('update_dashboard');
this.env.timeOffBus.trigger("update_dashboard");
});
}
}
async editRecord(record, context = {}, shouldFetchFormViewId = true) {
deleteRecord(record) {
this._deleteRecord(record.id, record.rawRecord.can_cancel);
}
_editRecord(record, context, props = {}) {
const onDialogClosed = () => {
this.model.load();
this.env.timeOffBus.trigger('update_dashboard');
this.env.timeOffBus.trigger("update_dashboard");
};
return new Promise((resolve) => {
this.displayDialog(
TimeOffFormViewDialog, {
TimeOffFormViewDialog,
{
...props,
resModel: this.model.resModel,
resId: record.id || false,
context,
title: this.env._t("Time Off Request"),
title: _t("Time Off Request"),
viewId: this.model.formViewId,
onRecordSaved: onDialogClosed,
onRecordDeleted: (record) => this.deleteRecord(record),
onRecordDeleted: (record) =>
this._deleteRecord(record.resId, record.data.can_cancel),
onLeaveCancelled: onDialogClosed,
size: "md",
},
{ onClose: () => resolve() }
);
});
}
async editRecord(record, context = {}) {
return this._editRecord(record, context);
}
}
TimeOffCalendarController.template = "hr_holidays.CalendarController";
TimeOffCalendarController.components = {
...TimeOffCalendarController.components,
FilterPanel: TimeOffCalendarFilterPanel,
export class TimeOffReportCalendarController extends TimeOffCalendarController {
async editRecord(record, context = {}) {
return this._editRecord(record, context, { canExpand: false });
}
}

View file

@ -1,21 +1,24 @@
<?xml version="1.0" encoding="UTF-8"?>
<templates xml:space="preserve">
<t t-name="hr_holidays.CalendarController" t-inherit="web.CalendarController" t-inherit-mode="primary" owl="1">
<xpath expr="//DatePicker" position="replace"/>
</t>
<t t-name="hr_holidays.CalendarController.controlButtons" t-inherit="web.CalendarController.controlButtons" t-inherit-mode="primary" owl="1">
<xpath expr="//span[hasclass('o_calendar_scale_buttons')]" position="after">
<span class="o_timeoff_buttons">
<button class="btn btn-primary btn-time-off mx-1" t-on-click="newTimeOffRequest" type="button">
New Time Off
</button>
<button class="btn btn-secondary" t-on-click="newAllocationRequest" type="button">
<t t-if="employeeId">New</t> Allocation Request
</button>
</span>
<t t-name="hr_holidays.CalendarController" t-inherit="web.CalendarController" t-inherit-mode="primary">
<xpath expr="//Layout" position="inside">
<t t-if="props.archInfo.canCreate" t-set-slot="control-panel-create-button">
<span class="o_timeoff_buttons btn-group">
<div class="btn-group">
<button class="btn btn-primary btn-time-off " t-on-click="newTimeOffRequest" type="button">
New
</button>
</div>
</span>
</t>
</xpath>
</t>
<t t-name="hr_holidays.CalendarView.Buttons">
<button t-if="canCreateGroupTimeOff" class="btn btn-secondary" t-on-click="onNewGroupTimeOff">
New Group Time Off
</button>
</t>
</templates>

View file

@ -0,0 +1,47 @@
import { useState, onWillStart, onWillUpdateProps } from "@odoo/owl";
import { useService } from "@web/core/utils/hooks";
import { CalendarMobileFilterPanel } from "@web/views/calendar/mobile_filter_panel/calendar_mobile_filter_panel";
export class TimeOffCalendarMobileFilterPanel extends CalendarMobileFilterPanel {
static components = {
...CalendarMobileFilterPanel.components,
};
static template = "hr_holidays.TimeOffCalendarMobileFilterPanel";
setup() {
super.setup();
this.orm = useService("orm");
this.leaveState = useState({
holidays: [],
});
onWillStart(this.loadFilterData);
onWillUpdateProps(this.loadFilterData);
}
async loadFilterData() {
if (!this.env.isSmall) {
return;
}
const promises = [];
for (const section of this.props.model.filterSections){
if (section.fieldName !== "holiday_status_id") {
continue;
}
promises.push(
this.orm.call("hr.leave.type", "get_allocation_data_request", [])
);
}
const filterData = {};
const [data,] = await Promise.all(promises);
if(!data){
return;
}
data.forEach((leave) => {
filterData[leave[3]] = leave;
});
this.leaveState.holidays = filterData;
}
}

View file

@ -0,0 +1,21 @@
<?xml version="1.0" encoding="UTF-8"?>
<templates xml:space="preserve">
<t t-name="hr_holidays.TimeOffCalendarMobileFilterPanel" t-inherit="web.CalendarMobileFilterPanel" t-inherit-mode="primary">
<xpath expr="//i[@name='expand_filter_button']" position="replace">
<div class="ms-2 w-25">
Summary
<i t-attf-class="oi fa-fw oi-chevron-{{caretDirection}}" name="expand_filter_button"/>
</div>
</xpath>
<xpath expr="//span[@name='filter_label']" position="after">
<t t-set="holiday" t-value="this.leaveState.holidays[filter.value]"/>
<t t-if="holiday">
<t t-set="data" t-value="holiday[1]"/>
<t t-set="duration" t-value="holiday[2] ? data.virtual_remaining_leaves : data.virtual_leaves_taken"/>
<span class="fw-bold text-nowrap ms-1">
(<span class="o_timeoff_green text-success" t-esc="duration"/> / <span class="fw-bold text-nowrap" t-esc="data.max_leaves"/>)
</span>
</t>
</xpath>
</t>
</templates>

View file

@ -1,75 +1,139 @@
/** @odoo-module */
import { CalendarModel } from '@web/views/calendar/calendar_model';
import { deserializeDateTime, serializeDate, serializeDateTime } from "@web/core/l10n/dates";
import { CalendarModel } from "@web/views/calendar/calendar_model";
import {
deserializeDate,
deserializeDateTime,
serializeDate,
serializeDateTime,
} from "@web/core/l10n/dates";
import { Cache } from "@web/core/utils/cache";
const { DateTime } = luxon;
export class TimeOffCalendarModel extends CalendarModel {
setup(params, services) {
super.setup(params, services);
this.data.stressDays = {};
this.data.mandatoryDays = {};
if (this.env.isSmall) {
this.meta.scale = 'month';
this.meta.scale = "month";
}
this._mandatoryDaysCache = new Cache(
(data) => this.fetchMandatoryDays(data),
(data) => `${serializeDateTime(data.range.start)},${serializeDateTime(data.range.end)}`
);
}
/**
* @override
*/
normalizeRecord(rawRecord) {
let result = super.normalizeRecord(...arguments);
const result = super.normalizeRecord(...arguments);
if (rawRecord.employee_id) {
const employee = rawRecord.employee_id[1];
result.title = [employee, result.title].join(' ');
// If the employee's name isn't already included at the start of the title
if (!result.title.startsWith(employee)) {
result.title = [employee, result.title].join(" ");
}
}
if (rawRecord.date_from && rawRecord.date_to) {
const dateFrom = DateTime.fromSQL(rawRecord.date_from);
const dateTo = DateTime.fromSQL(rawRecord.date_to);
result.sameDay = dateFrom.hasSame(dateTo, 'day');
}
if (rawRecord.request_unit_half) {
result.requestDateFromPeriod = rawRecord.request_date_from_period;
result.requestDateToPeriod = rawRecord.request_date_to_period;
}
return result;
}
makeContextDefaults(record) {
const { scale } = this.meta;
const context = super.makeContextDefaults(record);
if (this.employeeId) {
context['default_employee_id'] = this.employeeId;
let default_employee_id = this.employeeId;
if(context['active_model'] === 'hr.employee') {
default_employee_id = context.active_id
}
if (default_employee_id) {
context["default_employee_id"] = default_employee_id
}
function deserialize(str) {
// "YYYY-MM-DD".length == 10
return str.length > 10 ? deserializeDateTime(str) : deserializeDate(str);
}
if (["week", "day"].includes(this.scale)) {
context["default_request_unit_hours"] = true;
const hour_from = deserialize(context['default_date_from']??this.date);
const hour_to = deserialize(context['default_date_to']??this.date);
context['default_request_hour_from'] = hour_from.hour + hour_from.minute / 60;
context['default_request_hour_to'] = hour_to.hour + hour_to.minute / 60;
}
if(['day', 'week'].includes(scale)) {
if ('default_date_from' in context) {
context['default_date_from'] = serializeDateTime(deserializeDateTime(context['default_date_from']).set({ hours: 7 }));
}
if ('default_date_to' in context) {
context['default_date_to'] = serializeDateTime(deserializeDateTime(context['default_date_from']).set({ hours: 19 }));
}
if ("default_date_from" in context) {
context["default_date_from"] = serializeDateTime(
deserialize(context["default_date_from"]).set({ hours: 7 })
);
}
if ("default_date_to" in context) {
context["default_date_to"] = serializeDateTime(
deserialize(context["default_date_to"]).set({ hours: 19 })
);
}
return context;
}
async updateData(data) {
await super.updateData(data);
data.stressDays = await this.fetchStressDays(data);
const prom = super.updateData(data);
data.mandatoryDays = await this._mandatoryDaysCache.read(data);
return prom;
}
async fetchStressDays(data) {
return this.orm.call("hr.employee", "get_stress_days", [
/**
* @override
*/
fetchUnusualDays(data) {
return this.orm.call(
this.meta.resModel,
"get_unusual_days",
[serializeDateTime(data.range.start), serializeDateTime(data.range.end)],
{
context: {
employee_id: this.employeeId,
},
}
);
}
async fetchMandatoryDays(data) {
return this.orm.call("hr.employee", "get_mandatory_days", [
this.employeeId,
serializeDate(data.range.start, "datetime"),
serializeDate(data.range.end, "datetime"),
]);
}
get stressDays() {
return this.data.stressDays;
get mandatoryDays() {
return this.data.mandatoryDays;
}
get employeeId() {
return this.meta.context.employee_id && this.meta.context.employee_id[0] || null;
return (
(this.meta.context.employee_id && this.meta.context.employee_id[0]) ||
(this.meta.context.active_model === "hr.employee" && this.meta.context.active_id) ||
null
);
}
fetchRecords(data) {
const { fieldNames, resModel } = this.meta;
const context = {};
if (!this.employeeId) {
context['short_name'] = 1;
context["short_name"] = 1;
}
return this.orm.searchRead(resModel, this.computeDomain(data), fieldNames, { context });
const fieldNamesToAdd = resModel === "hr.leave" ? ["request_unit_half", "request_date_from_period", "request_date_to_period", "request_unit_hours"] : [];
return this.orm.searchRead(resModel, this.computeDomain(data), [...fieldNames, ...fieldNamesToAdd], { context });
}
computeDomain(data) {
return [...super.computeDomain(data), ["state", "!=", "cancel"]];
}
}

View file

@ -1,5 +1,3 @@
/** @odoo-module */
import { CalendarRenderer } from '@web/views/calendar/calendar_renderer';
import { TimeOffCalendarCommonRenderer } from './common/calendar_common_renderer';
@ -8,6 +6,15 @@ import { TimeOffCalendarYearRenderer } from './year/calendar_year_renderer';
import { TimeOffDashboard } from '../../dashboard/time_off_dashboard';
export class TimeOffCalendarRenderer extends CalendarRenderer {
static template = "hr_holidays.CalendarRenderer";
static components = {
...TimeOffCalendarRenderer.components,
day: TimeOffCalendarCommonRenderer,
week: TimeOffCalendarCommonRenderer,
month: TimeOffCalendarCommonRenderer,
year: TimeOffCalendarYearRenderer,
TimeOffDashboard,
};
get employeeId() {
return this.props.model.employeeId;
}
@ -16,15 +23,6 @@ export class TimeOffCalendarRenderer extends CalendarRenderer {
return false;
}
}
TimeOffCalendarRenderer.template = 'hr_holidays.CalendarRenderer';
TimeOffCalendarRenderer.components = {
...TimeOffCalendarRenderer.components,
day: TimeOffCalendarCommonRenderer,
week: TimeOffCalendarCommonRenderer,
month: TimeOffCalendarCommonRenderer,
year: TimeOffCalendarYearRenderer,
TimeOffDashboard,
};
export class TimeOffDashboardCalendarRenderer extends TimeOffCalendarRenderer {
get showDashboard() {

View file

@ -1,20 +1,42 @@
.o_timeoff_calendar {
.o_calendar_renderer {
height: unset;
background-color: $o-view-background-color;
flex-grow: 1;
flex-basis: fit-content;
@for $size from 1 through length($o-colors) {
.o_calendar_widget {
.hr_stress_day_top_#{$size - 1}:not(.fc-disabled-day) {
.fc-day-number {
color: nth($o-colors, $size) !important;
font-weight: 600;
}
}
.o_calendar_renderer .o_calendar_widget {
.hr_mandatory_day {
.fc-daygrid-day-number {
font-weight: 600;
}
}
.fc-bgevent {
border-radius: 25px;
> .fc-view-container > .fc-dayGridYear-view .hr_mandatory_day:not(.fc-day-disabled):not(.fc-day-today) .fc-daygrid-day-top:not(:hover),
> .fc-view-harness > .fc-dayGridMonth-view .hr_mandatory_day:not(.fc-day-disabled):not(.fc-day-today) {
.fc-daygrid-day-number {
color: var(--mandatory-day-color) !important;
}
}
.fc-bg-event {
border-radius: $border-radius-pill;
}
.fc-dayGridMonth-view .fc-day.fc-day-today[class*="hr_mandatory_day_"] {
--o-cw-bg: var(--mandatory-day-color);
}
@for $size from 1 through length($o-colors) {
.hr_mandatory_day_#{$size - 1} {
--mandatory-day-color: #{nth($o-colors, $size)};
}
}
.o_event_half_left {
clip-path: polygon(0 0, 50% 0, 50% 100%, 0% 100%);
}
.o_event_half_right {
clip-path: polygon(100% 0, 50% 0, 50% 100%, 100% 100%);
}
}
}

View file

@ -1,8 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<templates xml:space="preserve">
<t t-name="hr_holidays.CalendarRenderer" owl="1">
<div class="o_timeoff_calendar">
<t t-name="hr_holidays.CalendarRenderer">
<div class="o_timeoff_calendar d-flex flex-column">
<TimeOffDashboard t-if="showDashboard" employeeId="employeeId"/>
<t t-call="web.CalendarRenderer"/>
</div>

View file

@ -0,0 +1,70 @@
import { CalendarSidePanel } from "@web/views/calendar/calendar_side_panel/calendar_side_panel";
import { serializeDate, serializeDateTime } from "@web/core/l10n/dates";
import { Cache } from "@web/core/utils/cache";
import { useService } from "@web/core/utils/hooks";
import { useState, onWillStart, onWillUpdateProps } from "@odoo/owl";
import { TimeOffCalendarFilterSection } from "../filter_section/calendar_filter_section";
export class TimeOffCalendarSidePanel extends CalendarSidePanel {
static components = {
...CalendarSidePanel.components,
FilterSection: TimeOffCalendarFilterSection,
};
static template = "hr_holidays.TimeOffCalendarSidePanel";
setup() {
super.setup();
this.orm = useService("orm");
this.getFormattedDateSpan = function getFormattedDateSpan(start, end) {
const n = "numeric";
const s = "short";
const isSameDay = start.hasSame(end, "days");
if (isSameDay) {
return start.toLocaleString({ month: s, day: n, year: n });
}
return start.toLocaleString({ month: s, day: n, year: n }) + " - " + end.toLocaleString({ month: s, day: n, year: n });
};;
this.leaveState = useState({
mandatoryDays: [],
bankHolidays: [],
});
this._specialDaysCache = new Cache(
(start, end) => this.fetchSpecialDays(start, end),
(start, end) => `${serializeDateTime(start)},${serializeDateTime(end)}`
);
onWillStart(this.updateSpecialDays);
onWillUpdateProps(this.updateSpecialDays);
}
fetchSpecialDays(start, end) {
const context = {
employee_id: this.props.model.employeeId,
};
return this.orm.call(
"hr.employee",
"get_special_days_data",
[serializeDate(start, "datetime"), serializeDate(end, "datetime")],
{
context: context,
}
);
}
async updateSpecialDays() {
const { rangeStart, rangeEnd } = this.props.model;
const specialDays = await this._specialDaysCache.read(rangeStart, rangeEnd);
specialDays["bankHolidays"].forEach((bankHoliday) => {
bankHoliday.start = luxon.DateTime.fromISO(bankHoliday.start);
bankHoliday.end = luxon.DateTime.fromISO(bankHoliday.end);
});
specialDays["mandatoryDays"].forEach((mandatoryDay) => {
mandatoryDay.start = luxon.DateTime.fromISO(mandatoryDay.start);
mandatoryDay.end = luxon.DateTime.fromISO(mandatoryDay.end);
});
this.leaveState.bankHolidays = specialDays["bankHolidays"];
this.leaveState.mandatoryDays = specialDays["mandatoryDays"];
}
}

View file

@ -0,0 +1,39 @@
<?xml version="1.0" encoding="UTF-8"?>
<templates xml:space="preserve">
<t t-name="hr_holidays.TimeOffCalendarSidePanel" t-inherit="web.CalendarSidePanel" t-inherit-mode="primary">
<xpath expr="//div[hasclass('o_calendar_sidebar')]" position="inside">
<div class="o_calendar_filter mt-4">
<h5>Legend</h5>
<div class="d-flex flex-column">
<span class="align-middle"><img class="o_calendar_filter_plain" src="/hr/static/src/img/icons/plain.svg"/> Validated</span>
<span class="align-middle"><img class="o_calendar_filter_hatched" src="/hr/static/src/img/icons/hatched.svg"/> To Approve</span>
<span class="align-middle"><img class="o_calendar_filter_line" src="/hr/static/src/img/icons/line.svg"/> Refused</span>
</div>
<div class="d-flex flex-column mt-4" t-if="leaveState.mandatoryDays.length">
<h5>Mandatory Days</h5>
<ul class="ps-0">
<li t-foreach="leaveState.mandatoryDays" t-as="mandatoryDay" t-key="mandatoryDay.id" class="mt-2 list-unstyled">
<strong
t-esc="getFormattedDateSpan(mandatoryDay.start, mandatoryDay.end)"
t-att-class="'hr_mandatory_day_'+mandatoryDay.colorIndex"/>
: <t t-esc="mandatoryDay.title"/>
</li>
</ul>
</div>
<div class="d-flex flex-column mt-4" t-if="leaveState.bankHolidays.length">
<h5>Public Holidays</h5>
<ul class="ps-0">
<li t-foreach="leaveState.bankHolidays" t-as="bankHoliday" t-key="bankHoliday.id" class="mt-2 list-unstyled">
<strong
t-esc="getFormattedDateSpan(bankHoliday.start, bankHoliday.end)"/>
: <t t-esc="bankHoliday.title"/>
</li>
</ul>
</div>
</div>
</xpath>
</t>
</templates>

View file

@ -1,12 +1,27 @@
/** @odoo-module */
import { calendarView } from '@web/views/calendar/calendar_view';
import { TimeOffCalendarController } from './calendar_controller';
import { TimeOffCalendarController, TimeOffReportCalendarController } from './calendar_controller';
import { TimeOffCalendarModel } from './calendar_model';
import { TimeOffCalendarRenderer, TimeOffDashboardCalendarRenderer } from './calendar_renderer';
import { registry } from '@web/core/registry';
import { user } from "@web/core/user";
import { useService } from "@web/core/utils/hooks";
import { onWillStart } from "@odoo/owl";
class TimeOffCalendarControllerHrLeave extends TimeOffCalendarController {
setup() {
super.setup();
this.actionService = useService("action");
onWillStart(async () => {
this.canCreateGroupTimeOff = await user.hasGroup("hr_holidays.group_hr_holidays_responsible");
});
}
async onNewGroupTimeOff() {
await this.actionService.doAction("hr_holidays.action_hr_leave_generate_multi_wizard");
}
}
const TimeOffCalendarView = {
...calendarView,
@ -14,12 +29,21 @@ const TimeOffCalendarView = {
Controller: TimeOffCalendarController,
Renderer: TimeOffCalendarRenderer,
Model: TimeOffCalendarModel,
}
buttonTemplate: "hr_holidays.CalendarController.controlButtons",
const TimeOffCalendarHrLeaveView = {
...TimeOffCalendarView,
Controller: TimeOffCalendarControllerHrLeave,
buttonTemplate: "hr_holidays.CalendarView.Buttons",
}
registry.category('views').add('time_off_calendar', TimeOffCalendarView);
registry.category('views').add('time_off_calendar_hr_leave', TimeOffCalendarHrLeaveView);
registry.category('views').add('time_off_calendar_dashboard', {
...TimeOffCalendarView,
Renderer: TimeOffDashboardCalendarRenderer,
});
registry.category('views').add('time_off_report_calendar', {
...TimeOffCalendarView,
Controller: TimeOffReportCalendarController,
})

View file

@ -1,25 +1,27 @@
/** @odoo-module */
import { CalendarCommonPopover } from "@web/views/calendar/calendar_common/calendar_common_popover";
import { useService } from "@web/core/utils/hooks";
export class TimeOffCalendarCommonPopover extends CalendarCommonPopover {
static subTemplates = {
...CalendarCommonPopover.subTemplates,
popover: "hr_holidays.TimeOffCalendarCommonPopover.popover",
};
setup() {
super.setup();
this.dialog = useService('dialog');
this.action = useService('action');
this.orm = useService("orm");
this.actionService = useService("action");
this.viewType = "calendar";
}
get isEventDeletable() {
const record = this.props.record.rawRecord;
const state = record.state;
return record.can_cancel || state && !['validate', 'refuse'].includes(state);
}
get isEventEditable() {
const state = this.props.record.rawRecord.state;
return state !== undefined;
onEditEvent() {
this.props.close()
this.actionService.doAction({
name: this.record.display_name,
type: "ir.actions.act_window",
res_model: this.props.model.resModel,
res_id: this.record.id,
views: [[false, "form"]],
});
}
}

View file

@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<templates xml:space="preserve">
<t t-name="hr_holidays.TimeOffCalendarCommonPopover.popover" t-inherit="web.CalendarCommonPopover.popover" t-inherit_mode="primary" owl="1">
<xpath expr="//span" position="attributes">
<attribute name="t-attf-class">flex-grow-1</attribute>
</xpath>
<xpath expr="//span[hasclass('o_cw_popover_close')]" position="attributes">
<attribute name="class">o_cw_popover_close ms-0 mt-2 me-2</attribute>
<attribute name="data-hotkey">x</attribute>
</xpath>
</t>
</templates>

View file

@ -1,23 +1,29 @@
/** @odoo-module */
import { onWillStart } from "@odoo/owl";
import { user } from "@web/core/user";
import { CalendarCommonRenderer } from '@web/views/calendar/calendar_common/calendar_common_renderer';
import { useStressDays } from '../../hooks';
import { useMandatoryDays } from '../../hooks';
import { TimeOffCalendarCommonPopover } from './calendar_common_popover';
export class TimeOffCalendarCommonRenderer extends CalendarCommonRenderer {
static components = {
...TimeOffCalendarCommonRenderer,
Popover: TimeOffCalendarCommonPopover,
};
setup() {
super.setup();
this.stressDays = useStressDays(this.props);
this.mandatoryDays = useMandatoryDays(this.props);
onWillStart(async () => {
this.isManager = (await user.hasGroup("hr_holidays.group_hr_holidays_user"));
});
}
onDayRender(info) {
super.onDayRender(info);
this.stressDays(info);
getDayCellClassNames(info) {
return [...super.getDayCellClassNames(info), ...this.mandatoryDays(info)];
}
onClick(info) {
// To open record view
return this.onDblClick(info)
}
}
TimeOffCalendarCommonRenderer.components = {
...TimeOffCalendarCommonRenderer,
Popover: TimeOffCalendarCommonPopover,
}

View file

@ -1,79 +0,0 @@
/** @odoo-module */
import { CalendarFilterPanel } from "@web/views/calendar/filter_panel/calendar_filter_panel";
import { TimeOffCardMobile } from "../../../dashboard/time_off_card";
import { getFormattedDateSpan } from '@web/views/calendar/utils';
import { useService } from "@web/core/utils/hooks";
import { serializeDate } from "@web/core/l10n/dates";
const { useState, onWillStart, onWillUpdateProps } = owl;
export class TimeOffCalendarFilterPanel extends CalendarFilterPanel {
setup() {
super.setup();
this.orm = useService('orm');
this.getFormattedDateSpan = getFormattedDateSpan;
this.leaveState = useState({
holidays: [],
stressDays: [],
bankHolidays: [],
});
onWillStart(async () => {
await this.loadFilterData();
await this.updateSpecialDays();
});
onWillUpdateProps(this.updateSpecialDays);
}
async updateSpecialDays() {
const context = {
'employee_id': this.props.employee_id,
}
const specialDays = await this.orm.call(
'hr.employee', 'get_special_days_data', [
serializeDate(this.props.model.rangeStart, "datetime"),
serializeDate(this.props.model.rangeEnd, "datetime"),
],
{
'context': context,
},
);
specialDays['bankHolidays'].forEach(bankHoliday => {
bankHoliday.start = luxon.DateTime.fromISO(bankHoliday.start)
bankHoliday.end = luxon.DateTime.fromISO(bankHoliday.end)
});
specialDays['stressDays'].forEach(stressDay => {
stressDay.start = luxon.DateTime.fromISO(stressDay.start)
stressDay.end = luxon.DateTime.fromISO(stressDay.end)
});
this.leaveState.bankHolidays = specialDays['bankHolidays'];
this.leaveState.stressDays = specialDays['stressDays'];
}
async loadFilterData() {
if(!this.env.isSmall) {
return;
}
const filterData = {};
const data = await this.orm.call(
'hr.leave.type', 'get_days_all_request', [],
);
data.forEach((leave) => {
filterData[leave[3]] = leave;
});
this.leaveState.holidays = filterData;
}
}
TimeOffCalendarFilterPanel.template = 'hr_holidays.CalendarFilterPanel';
TimeOffCalendarFilterPanel.components = {
...TimeOffCalendarFilterPanel.components,
TimeOffCardMobile,
}
TimeOffCalendarFilterPanel.subTemplates = {
filter: "hr_holidays.CalendarFilterPanel.filter",
}

View file

@ -1,45 +0,0 @@
.o_calendar_filter {
span {
vertical-align: middle;
}
& img {
width:30px;
&.o_calendar_filter_plain {
content:var(--calendarFilter-icon--plain);
}
&.o_calendar_filter_hatched {
content:var(--calendarFilter-icon--hatched);
}
&.o_calendar_filter_line {
content:var(--calendarFilter-icon--line);
}
}
.o_timeoff_legend {
display: inline-block;
width: 24px;
height: 30px;
margin: 0 3px;
padding: 3px 0;
text-align: center;
&_bankholiday {
background-color: $gray-200;
}
&_stressday {
font-weight: 600;
}
}
@for $size from 1 through length($o-colors) {
.hr_stress_day_#{$size - 1}:not(.fc-disabled-day) {
color: nth($o-colors, $size) !important;
}
}
}

View file

@ -1,56 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<templates xml:space="preserve">
<t t-name="hr_holidays.CalendarFilterPanel" t-inherit="web.CalendarFilterPanel" t-inherit-mode="primary" owl="1">
<xpath expr="//t[@t-foreach='props.model.filterSections']" position="after">
<div class="o_calendar_filter">
<h5>Legend</h5>
<div class="d-flex flex-column">
<span><img class="o_calendar_filter_plain" src="/hr/static/src/img/icons/plain.svg"/> Validated</span>
<span><img class="o_calendar_filter_hatched" src="/hr/static/src/img/icons/hatched.svg"/> To Approve</span>
<span><img class="o_calendar_filter_line" src="/hr/static/src/img/icons/line.svg"/> Refused</span>
<span><span class="o_timeoff_legend o_timeoff_legend_bankholiday">13</span> Public Holiday</span>
<span><span class="o_timeoff_legend o_timeoff_legend_stressday text-odoo">13</span> Stress Day</span>
</div>
<div class="d-flex flex-column mt-4" t-if="leaveState.stressDays.length">
<h5>Stress Days</h5>
<ul class="ps-2">
<li t-foreach="leaveState.stressDays" t-as="stressDay" t-key="stressDay.id" class="mt-2 list-unstyled">
<strong
t-esc="getFormattedDateSpan(stressDay.start, stressDay.end)"
t-att-class="'hr_stress_day_'+stressDay.colorIndex"/>
: <t t-esc="stressDay.title"/>
</li>
</ul>
</div>
<div class="d-flex flex-column mt-4" t-if="leaveState.bankHolidays.length">
<h5>Public Holidays</h5>
<ul class="ps-2">
<li t-foreach="leaveState.bankHolidays" t-as="bankHoliday" t-key="bankHoliday.id" class="mt-2 list-unstyled">
<strong
t-esc="getFormattedDateSpan(bankHoliday.start, bankHoliday.end)"/>
: <t t-esc="bankHoliday.title"/>
</li>
</ul>
</div>
</div>
</xpath>
</t>
<t t-name="hr_holidays.CalendarFilterPanel.filter" t-inherit="web.CalendarFilterPanel.filter" t-inherit-mode="primary" owl="1">
<xpath expr="//span[@t-esc='filter.label']" position="replace">
<span class="o_cw_filter_title text-truncate flex-grow">
<t t-esc="filter.label"/>
<t t-if="env.isSmall">
<t t-set="holiday" t-value="leaveState.holidays[filter.value]"/>
<TimeOffCardMobile t-if="holiday" name="holiday[0]" id="holiday[3]" data="holiday[1]" requires_allocation="holiday[2] === 'yes'" />
</t>
</span>
</xpath>
</t>
</templates>

View file

@ -0,0 +1,37 @@
import { CalendarFilterSection } from "@web/views/calendar/calendar_filter_section/calendar_filter_section";
import { TimeOffCardMobile } from "../../../dashboard/time_off_card";
import { useService } from "@web/core/utils/hooks";
import { useState, onWillStart } from "@odoo/owl";
export class TimeOffCalendarFilterSection extends CalendarFilterSection {
static components = {
...CalendarFilterSection.components,
TimeOffCardMobile,
};
static subTemplates = {
filter: "hr_holidays.CalendarFilterSection.filter",
};
setup() {
super.setup();
this.orm = useService("orm");
this.leaveState = useState({
holidays: [],
});
onWillStart(this.loadFilterData);
}
async loadFilterData() {
if (!this.env.isSmall || this.section.fieldName !== "holiday_status_id") {
return;
}
const filterData = {};
const data = await this.orm.call("hr.leave.type", "get_allocation_data_request", []);
data.forEach((leave) => {
filterData[leave[3]] = leave;
});
this.leaveState.holidays = filterData;
}
}

View file

@ -0,0 +1,25 @@
.o_calendar_filter {
img:not(.o_avatar) {
width: 30px;
&.o_calendar_filter_plain {
content:var(--calendarFilter-icon--plain);
}
&.o_calendar_filter_hatched {
content:var(--calendarFilter-icon--hatched);
}
&.o_calendar_filter_line {
content:var(--calendarFilter-icon--line);
}
}
@for $size from 1 through length($o-colors) {
.hr_mandatory_day_#{$size - 1}:not(.fc-disabled-day) {
color: nth($o-colors, $size) !important;
}
}
}

View file

@ -0,0 +1,15 @@
<?xml version="1.0" encoding="UTF-8"?>
<templates xml:space="preserve">
<t t-name="hr_holidays.CalendarFilterSection.filter" t-inherit="web.CalendarFilterSection.filter" t-inherit-mode="primary">
<xpath expr="//span[@t-esc='filter.label']" position="replace">
<span class="o_cw_filter_title flex-grow-1 text-truncate lh-base">
<t t-esc="filter.label"/>
<t t-if="env.isSmall">
<t t-set="holiday" t-value="leaveState.holidays[filter.value]"/>
<TimeOffCardMobile t-if="holiday" name="holiday[0]" id="holiday[3]" data="holiday[1]" requires_allocation="holiday[2]"/>
</t>
</span>
</xpath>
</t>
</templates>

View file

@ -1,13 +1,12 @@
/** @odoo-module **/
import { Dialog } from "@web/core/dialog/dialog";
import { CalendarYearPopover } from "@web/views/calendar/calendar_year/calendar_year_popover";
export class TimeOffCalendarYearPopover extends CalendarYearPopover {}
TimeOffCalendarYearPopover.components = { Dialog };
TimeOffCalendarYearPopover.template = "web.CalendarYearPopover";
TimeOffCalendarYearPopover.subTemplates = {
...CalendarYearPopover.subTemplates,
body: "hr_holidays.StressDayCalendarYearPopover.body",
};
export class TimeOffCalendarYearPopover extends CalendarYearPopover {
static components = { Dialog };
static template = "web.CalendarYearPopover";
static subTemplates = {
...CalendarYearPopover.subTemplates,
body: "hr_holidays.MandatoryDayCalendarYearPopover.body",
};
}

View file

@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<templates xml:space="preserve">
<t t-name="hr_holidays.StressDayCalendarYearPopover.body" owl="1">
<t t-name="hr_holidays.MandatoryDayCalendarYearPopover.body">
<t t-foreach="recordGroups" t-as="recordGroup" t-key="recordGroup.title">
<div class="fw-bold mt-2" t-esc="recordGroup.title" />
<t t-foreach="recordGroup.records" t-as="record" t-key="record.id">

View file

@ -1,71 +1,93 @@
/** @odoo-module */
import { CalendarYearRenderer } from '@web/views/calendar/calendar_year/calendar_year_renderer';
import { CalendarYearRenderer } from "@web/views/calendar/calendar_year/calendar_year_renderer";
import { useService } from "@web/core/utils/hooks";
import { useStressDays } from '../../hooks';
import { useCalendarPopover } from '@web/views/calendar/hooks';
import { TimeOffCalendarYearPopover } from './calendar_year_popover';
const { useEffect } = owl;
import { useMandatoryDays } from "../../hooks";
import { useCalendarPopover } from "@web/views/calendar/hooks/calendar_popover_hook";
import { TimeOffCalendarYearPopover } from "./calendar_year_popover";
export class TimeOffCalendarYearRenderer extends CalendarYearRenderer {
setup() {
super.setup();
this.orm = useService("orm");
this.stressDays = useStressDays(this.props);
this.stressDaysList = [];
this.stressDayPopover = useCalendarPopover(TimeOffCalendarYearPopover);
useEffect((el) => {
for (const week of el) {
const row = week.parentElement;
// Remove the week number if the week is empty.
// FullCalendar always displays 6 weeks even when empty.
if (!row.children[1].classList.length &&
!row.children[row.children.length - 1].classList.length) {
row.remove();
}
}
}, () => [this.rootRef.el && this.rootRef.el.querySelectorAll('.fc-content-skeleton td.fc-week-number')]);
this.mandatoryDays = useMandatoryDays(this.props);
this.mandatoryDaysList = [];
this.mandatoryDayPopover = useCalendarPopover(TimeOffCalendarYearPopover);
}
get options() {
return Object.assign(super.options, {
weekNumbers: true,
weekNumbersWithinDays: false,
weekLabel: this.env._t('Week'),
});
}
get customOptions() {
return {
...super.customOptions,
weekNumbersWithinDays: false,
};
}
/** @override **/
async onDateClick(info) {
const is_stress_day = [...info.dayEl.classList].some(elClass => elClass.startsWith('hr_stress_day_'))
this.stressDayPopover.close();
if (is_stress_day && !this.env.isSmall) {
const is_mandatory_day = [...info.dayEl.classList].some((elClass) =>
elClass.startsWith("hr_mandatory_day_")
);
this.mandatoryDayPopover.close();
if (is_mandatory_day && !this.env.isSmall) {
this.popover.close();
const date = luxon.DateTime.fromISO(info.dateStr);
const target = info.dayEl;
const stress_days_data = await this.orm.call("hr.employee", "get_stress_days_data", [date, date]);
stress_days_data.forEach(stress_day_data => {
stress_day_data['start'] = luxon.DateTime.fromISO(stress_day_data['start'])
stress_day_data['end'] = luxon.DateTime.fromISO(stress_day_data['end'])
const mandatory_days_data = await this.orm.call(
"hr.employee",
"get_mandatory_days_data",
[date, date]
);
mandatory_days_data.forEach((mandatory_day_data) => {
mandatory_day_data["start"] = luxon.DateTime.fromISO(mandatory_day_data["start"]);
mandatory_day_data["end"] = luxon.DateTime.fromISO(mandatory_day_data["end"]);
});
const records = Object.values(this.props.model.records).filter((r) =>
luxon.Interval.fromDateTimes(r.start.startOf("day"), r.end.endOf("day")).contains(date)
luxon.Interval.fromDateTimes(r.start.startOf("day"), r.end.endOf("day")).contains(
date
)
);
const props = this.getPopoverProps(date, records)
props['records'] = stress_days_data.concat(props['records'])
this.stressDayPopover.open(target, props, "o_cw_popover");
}
else {
const props = this.getPopoverProps(date, records);
props["records"] = mandatory_days_data.concat(props["records"]);
this.mandatoryDayPopover.open(target, props, "o_cw_popover_holidays o_cw_popover");
} else {
super.onDateClick(info);
}
}
onDayRender(info) {
super.onDayRender(info);
this.stressDaysList = this.stressDays(info);
openPopover(target, date, records) {
this.popover.open(target, this.getPopoverProps(date, records), "o_cw_popover_holidays o_cw_popover");
}
getDayCellClassNames(info) {
return [...super.getDayCellClassNames(info), ...this.mandatoryDays(info)];
}
/**
* @override
*/
eventClassNames({ event }) {
const classesToAdd = super.eventClassNames(...arguments);
const record = this.props.model.records[event.id];
if (record && record.requestDateFromPeriod && record.sameDay) {
if (record.requestDateFromPeriod === "am" && record.requestDateToPeriod === "am") {
classesToAdd.push("o_event_half_left")
} else if (record.requestDateFromPeriod === "pm" && record.requestDateToPeriod === "pm") {
classesToAdd.push("o_event_half_right")
}
}
// handling half pill UX for custom_hours
if (record?.rawRecord?.request_unit_hours && record.sameDay) {
if (record.end.c.hour < 12) {
classesToAdd.push("o_event_half_left");
} else if (record.end.c.hour >= 12 && record.start.c.hour >= 12) {
classesToAdd.push("o_event_half_right");
}
}
return classesToAdd;
}
}

View file

@ -1,14 +0,0 @@
.o_timeoff_calendar {
.o_calendar_widget {
.fc-dayGridYear-view {
.fc-week-number {
color: #adb5bd;
font-size: 0.85rem;
cursor: default;
vertical-align: middle;
line-height: unset;
}
}
}
}

View file

@ -0,0 +1,12 @@
<?xml version="1.0" encoding="UTF-8"?>
<templates xml:space="preserve">
<t t-name="hr_holidays.HrHolidaysGraphView.Buttons" t-inherit="web.GraphView.Buttons">
<!--Remove the line and pie chart views-->
<xpath expr="//button[@data-mode='line']" position="replace"/>
<xpath expr="//button[@data-mode='pie']" position="replace"/>
<!--The bar chart will always be stacked. So the stack button is of no use-->
<xpath expr="//div[hasclass('btn-group')][3]" position="replace"/>
</t>
</templates>

View file

@ -0,0 +1,74 @@
/** @odoo-module **/
import { GraphModel } from "@web/views/graph/graph_model";
import { sortBy } from "@web/core/utils/arrays";
export class HrHolidaysGraphModel extends GraphModel {
async load(searchParams) {
if (searchParams.groupBy.length != 0 && !searchParams.groupBy.includes('leave_type')){
searchParams.groupBy.push('leave_type');
}
await super.load(...arguments);
}
_getLineOverlayDataset() {
// Given that there are at least 2 stacks one for allocation and one for time off
// then there shouldn't be a lineOverlay.
return null;
}
/**
* Eventually filters and sort data points.
* @protected
* @returns {Object[]}
*/
_getProcessedDataPoints() {
const {fields, groupBy, mode, order } = this.metaData;
this.allocation_label = fields['leave_type'].selection.find((selection) => selection[0] === 'allocation')[1]
this.timeoff_label = fields['leave_type'].selection.find((selection) => selection[0] === 'request')[1]
let processedDataPoints = [];
if (mode === "bar") {
processedDataPoints = this.dataPoints.filter(
(dataPoint) => dataPoint.labels[0] !== this._getDefaultFilterLabel(groupBy[0])
);
if (order !== null && groupBy.length > 0) {
const groupedDataPoints = {};
for (const dataPt of processedDataPoints) {
const key = dataPt.labels[0]; // = x-axis value under the current assumptions
if (!groupedDataPoints[key]) {
groupedDataPoints[key] = [];
}
groupedDataPoints[key].push(dataPt);
}
const groups = Object.values(groupedDataPoints);
let datapointsWithAllocation = new Set(
groups.flat()
.map(dataPoint => {
if (dataPoint.labels[dataPoint.labels.length - 1] === this.allocation_label){
return dataPoint.labels.slice(0, -1).join('/');
}
return false;
})
.filter(label => label !== false)
)
const groupTotal = (group) => group.reduce((sum, dataPt) => {
if (datapointsWithAllocation.has(dataPt.labels.slice(0, -1).join('/'))){
return sum + dataPt.value;
}
return sum;
}, 0);
processedDataPoints = sortBy(groups, groupTotal, order.toLowerCase()).flat();
}
else {
processedDataPoints = super._getProcessedDataPoints();
}
return processedDataPoints;
}
return super._getProcessedDataPoints();
}
}

Some files were not shown because too many files have changed in this diff Show more