mirror of
https://github.com/bringout/oca-ocb-hr.git
synced 2026-04-25 10:52:06 +02:00
19.0 vanilla
This commit is contained in:
parent
a1137a1456
commit
e1d89e11e3
2789 changed files with 1093187 additions and 605897 deletions
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
Before Width: | Height: | Size: 6.4 KiB After Width: | Height: | Size: 3 KiB |
|
|
@ -1,25 +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="98.162%" x2="0%" y1="1.838%" y2="100%">
|
||||
<stop offset="0%" stop-color="#797DA5"/>
|
||||
<stop offset="50.799%" stop-color="#6D7194"/>
|
||||
<stop offset="100%" stop-color="#626584"/>
|
||||
</linearGradient>
|
||||
<path id="icon-d" d="M21.2876355,40.2488327 C21.644877,38.8191569 22.7635563,37.6632882 24.2438835,37.2932064 L27.2616862,36.5387663 C29.7246615,38.3103548 33.5677441,38.8192676 36.7383138,36.5387663 L39.7561165,37.2932064 C41.5646061,37.7453288 42.8333333,39.3702441 42.8333333,41.2344238 L42.8333333,41.2959901 C45.2976729,38.7278585 46.8104101,35.2404382 46.8104101,31.39867 C46.8104101,23.4931273 40.4116554,17.1061635 32.5165079,17.1061635 C24.6101932,17.1061635 18.2226057,23.5042934 18.2226057,31.39867 C18.2226057,34.7434734 19.3680481,37.8164357 21.2876355,40.2488327 Z M57.5996449,51.3156444 C58.3876447,52.1120396 58.3876447,53.3998274 57.5911718,54.1962225 L55.1932801,56.5938801 C54.4052803,57.3902752 53.1173667,57.3902752 52.3208938,56.5938801 L43.8731973,48.1470086 C43.4919071,47.7657556 43.2800792,47.248946 43.2800792,46.7067195 L43.2800792,45.3257365 C40.2890694,47.6640881 36.5270059,49.0535434 32.434491,49.0535434 C22.6988809,49.0535434 14.8104101,41.1658429 14.8104101,31.4311835 C14.8104101,21.6965241 22.6988809,13.8088235 32.434491,13.8088235 C42.1701011,13.8088235 50.0585719,21.6965241 50.0585719,31.4311835 C50.0585719,35.5232988 48.6689809,39.2849949 46.3304009,42.2757127 L47.7115188,42.2757127 C48.2537982,42.2757127 48.7706583,42.4875199 49.1519485,42.8687729 L57.5996449,51.3156444 Z M32,23.1666667 C35.7394466,23.1666667 38.7708333,26.1980534 38.7708333,29.9375 C38.7708333,33.6769466 35.7394466,36.7083333 32,36.7083333 C28.2605534,36.7083333 25.2291667,33.6769466 25.2291667,29.9375 C25.2291667,26.1980534 28.2605534,23.1666667 32,23.1666667 Z"/>
|
||||
<path id="icon-e" d="M21.2876355,38.2488327 C21.644877,36.8191569 22.7635563,35.6632882 24.2438835,35.2932064 L27.2616862,34.5387663 C29.7246615,36.3103548 33.5677441,36.8192676 36.7383138,34.5387663 L39.7561165,35.2932064 C41.5646061,35.7453288 42.8333333,37.3702441 42.8333333,39.2344238 L42.8333333,39.2959901 C45.2976729,36.7278585 46.8104101,33.2404382 46.8104101,29.39867 C46.8104101,21.4931273 40.4116554,15.1061635 32.5165079,15.1061635 C24.6101932,15.1061635 18.2226057,21.5042934 18.2226057,29.39867 C18.2226057,32.7434734 19.3680481,35.8164357 21.2876355,38.2488327 Z M57.5996449,49.3156444 C58.3876447,50.1120396 58.3876447,51.3998274 57.5911718,52.1962225 L55.1932801,54.5938801 C54.4052803,55.3902752 53.1173667,55.3902752 52.3208938,54.5938801 L43.8731973,46.1470086 C43.4919071,45.7657556 43.2800792,45.248946 43.2800792,44.7067195 L43.2800792,43.3257365 C40.2890694,45.6640881 36.5270059,47.0535434 32.434491,47.0535434 C22.6988809,47.0535434 14.8104101,39.1658429 14.8104101,29.4311835 C14.8104101,19.6965241 22.6988809,11.8088235 32.434491,11.8088235 C42.1701011,11.8088235 50.0585719,19.6965241 50.0585719,29.4311835 C50.0585719,33.5232988 48.6689809,37.2849949 46.3304009,40.2757127 L47.7115188,40.2757127 C48.2537982,40.2757127 48.7706583,40.4875199 49.1519485,40.8687729 L57.5996449,49.3156444 Z M32,21.1666667 C35.7394466,21.1666667 38.7708333,24.1980534 38.7708333,27.9375 C38.7708333,31.6769466 35.7394466,34.7083333 32,34.7083333 C28.2605534,34.7083333 25.2291667,31.6769466 25.2291667,27.9375 C25.2291667,24.1980534 28.2605534,21.1666667 32,21.1666667 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="M44.7894737,57 L4,57 C2,57 -7.10542736e-15,56.8519481 0,52.8545455 L2.23548517e-16,28.151787 L18.9764771,5.20700378 L30.93373,0.994855357 L42.9934954,4.14545455 L48.0359957,15.0446694 L44.7894737,26.8973595 L57.8747796,38.6825776 L44.7894737,57 Z" opacity=".324" transform="translate(0 13)"/>
|
||||
<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="50" height="50" viewBox="0 0 50 50" xmlns="http://www.w3.org/2000/svg"><path d="M32 25a7 7 0 1 1-14 0 7 7 0 0 1 14 0Z" fill="#985184"/><path d="M25 46C13.402 46 4 36.598 4 25S13.402 4 25 4s21 9.402 21 21c0 5.799-2.35 11.049-6.15 14.85l-6.365-6.365A11.964 11.964 0 0 0 37 25c0-6.627-5.373-12-12-12s-12 5.373-12 12 5.373 12 12 12v9Z" fill="#1AD3BB"/><path fill-rule="evenodd" clip-rule="evenodd" d="M25 37a11.958 11.958 0 0 1-8.345-3.377A9 9 0 0 0 25 46h13a9 9 0 0 0-9-9h-4Z" fill="#985184"/></svg>
|
||||
|
|
|
|||
|
Before Width: | Height: | Size: 5.2 KiB After Width: | Height: | Size: 509 B |
Binary file not shown.
|
After Width: | Height: | Size: 18 KiB |
|
|
@ -1,11 +1,10 @@
|
|||
/** @odoo-module */
|
||||
|
||||
import { CharField } from "@web/views/fields/char/char_field";
|
||||
import { CharField, charField } from "@web/views/fields/char/char_field";
|
||||
import { registry } from "@web/core/registry";
|
||||
|
||||
import { useService } from "@web/core/utils/hooks";
|
||||
|
||||
export class ApplicantCharField extends CharField {
|
||||
static template = "hr_recruitment.ApplicantCharField";
|
||||
setup() {
|
||||
super.setup();
|
||||
|
||||
|
|
@ -14,11 +13,11 @@ export class ApplicantCharField extends CharField {
|
|||
|
||||
onClick() {
|
||||
const record = this.props.record.data;
|
||||
if (record.res_id !== undefined && record.res_model == 'hr.applicant') {
|
||||
if (record.res_id && record.res_model == 'hr.applicant') {
|
||||
this.action.doAction({
|
||||
type: 'ir.actions.act_window',
|
||||
res_model: 'hr.applicant',
|
||||
res_id: record.res_id,
|
||||
res_id: record.res_id.resId,
|
||||
views: [[false, "form"]],
|
||||
view_mode: "form",
|
||||
target: "current",
|
||||
|
|
@ -26,5 +25,10 @@ export class ApplicantCharField extends CharField {
|
|||
}
|
||||
}
|
||||
}
|
||||
ApplicantCharField.template = "hr_recruitment.ApplicantCharField";
|
||||
registry.category("fields").add("applicant_char", ApplicantCharField);
|
||||
|
||||
export const applicantCharField = {
|
||||
...charField,
|
||||
component: ApplicantCharField,
|
||||
};
|
||||
|
||||
registry.category("fields").add("applicant_char", applicantCharField);
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<templates xml:space="preserve">
|
||||
<t t-name="hr_recruitment.ApplicantCharField" t-inherit="web.CharField" t-inherit-mode="primary" owl="1">
|
||||
<t t-name="hr_recruitment.ApplicantCharField" t-inherit="web.CharField" t-inherit-mode="primary">
|
||||
<xpath expr="//span[@t-esc='formattedValue']" position="attributes">
|
||||
<attribute name="t-on-click.prevent.stop">onClick</attribute>
|
||||
</xpath>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,25 @@
|
|||
import { registry } from "@web/core/registry";
|
||||
import {
|
||||
StateSelectionField,
|
||||
stateSelectionField,
|
||||
} from "@web/views/fields/state_selection/state_selection_field";
|
||||
|
||||
const STATUS_COLORS = {
|
||||
blocked: "red",
|
||||
done: "green",
|
||||
waiting: "orange",
|
||||
};
|
||||
|
||||
export class HrApplicantStateSelectionField extends StateSelectionField {
|
||||
setup() {
|
||||
super.setup();
|
||||
this.colors = STATUS_COLORS;
|
||||
}
|
||||
}
|
||||
|
||||
export const hrApplicantStateSelectionField = {
|
||||
...stateSelectionField,
|
||||
component: HrApplicantStateSelectionField,
|
||||
};
|
||||
|
||||
registry.category("fields").add("hr_applicant_state_selection", hrApplicantStateSelectionField);
|
||||
|
|
@ -0,0 +1,21 @@
|
|||
import { CopyButton } from "@web/core/copy_button/copy_button";
|
||||
import { useService } from "@web/core/utils/hooks";
|
||||
|
||||
export class GenerateContentAndCopyButton extends CopyButton {
|
||||
static props = {
|
||||
...CopyButton.props,
|
||||
contentGenerationFunction: { type: Function, optional: true },
|
||||
};
|
||||
|
||||
setup() {
|
||||
super.setup();
|
||||
this.orm = useService("orm");
|
||||
}
|
||||
|
||||
async onClick() {
|
||||
if(this.props.contentGenerationFunction){
|
||||
this.props.content = await this.props.contentGenerationFunction();
|
||||
}
|
||||
await super.onClick();
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,34 @@
|
|||
|
||||
import { registry } from "@web/core/registry";
|
||||
import { useService } from "@web/core/utils/hooks";
|
||||
import { Many2ManyTagsField, many2ManyTagsField } from "@web/views/fields/many2many_tags/many2many_tags_field";
|
||||
|
||||
export class ApplicantLineMany2Many extends Many2ManyTagsField {
|
||||
setup() {
|
||||
super.setup(...arguments);
|
||||
this.orm = useService("orm");
|
||||
}
|
||||
|
||||
getTagProps(record){
|
||||
let applicant_name = record.data.display_name;
|
||||
let name = applicant_name;
|
||||
let job_name = record.data.job_id[1];
|
||||
if (job_name){
|
||||
name = `${job_name} - ${applicant_name}`;
|
||||
}
|
||||
return {...super.getTagProps(record), text: name};
|
||||
}
|
||||
}
|
||||
|
||||
export const applicantLineMany2Many = {
|
||||
...many2ManyTagsField,
|
||||
component: ApplicantLineMany2Many,
|
||||
relatedFields: (fieldInfo) => {
|
||||
return [
|
||||
...many2ManyTagsField.relatedFields(fieldInfo),
|
||||
{ name: "job_id", type: "many2one"},
|
||||
];
|
||||
},
|
||||
};
|
||||
|
||||
registry.category("fields").add("applicant_line_many2many", applicantLineMany2Many);
|
||||
|
|
@ -0,0 +1,51 @@
|
|||
import { CopyClipboardCharField, copyClipboardCharField } from "@web/views/fields/copy_clipboard/copy_clipboard_field";
|
||||
import { CharField } from "@web/views/fields/char/char_field";
|
||||
import { GenerateContentAndCopyButton } from "../../buttons/generate_content_and_copy_button";
|
||||
import { useService } from "@web/core/utils/hooks";
|
||||
import { omit } from "@web/core/utils/objects";
|
||||
import { _t } from "@web/core/l10n/translation";
|
||||
import { registry } from "@web/core/registry";
|
||||
|
||||
class RecruitmentCopyClipboardCharField extends CopyClipboardCharField {
|
||||
static template = "hr_recruitment.RecruitmentCopyClipboardCharField";
|
||||
static components = { Field: CharField, GenerateContentAndCopyButton };
|
||||
static props = {
|
||||
...CopyClipboardCharField.props,
|
||||
displayedValue: { type: String, optional: true },
|
||||
contentGenerationFunctionName: { type: String, optional: true },
|
||||
};
|
||||
|
||||
setup() {
|
||||
super.setup();
|
||||
this.orm = useService("orm");
|
||||
}
|
||||
|
||||
get fieldProps() {
|
||||
return omit(super.fieldProps, "displayedValue", "contentGenerationFunctionName");
|
||||
}
|
||||
|
||||
get contentGenerationFunction() {
|
||||
if(this.props.contentGenerationFunctionName) {
|
||||
return () => this.orm.call(
|
||||
this.props.record._config.resModel,
|
||||
this.props.contentGenerationFunctionName,
|
||||
[this.props.record.resId],
|
||||
);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export const recruitmentCopyClipboardCharField = {
|
||||
...copyClipboardCharField,
|
||||
component: RecruitmentCopyClipboardCharField,
|
||||
displayName: _t("Copy to Clipboard"),
|
||||
supportedTypes: ["char"],
|
||||
extractProps({ options }) {
|
||||
const props = copyClipboardCharField.extractProps(...arguments);
|
||||
props.displayedValue = options?.displayed_value;
|
||||
props.contentGenerationFunctionName = options?.content_generation_function_name;
|
||||
return props;
|
||||
},
|
||||
};
|
||||
registry.category("fields").add("RecruitmentCopyClipboardChar", recruitmentCopyClipboardCharField);
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<templates xml:space="preserve">
|
||||
|
||||
<t t-name="hr_recruitment.RecruitmentCopyClipboardCharField">
|
||||
<div class="d-flex">
|
||||
<span t-esc="props.displayedValue"></span>
|
||||
<GenerateContentAndCopyButton t-if="props.contentGenerationFunctionName" className="copyButtonClassName" disabled="props.record.isNew" contentGenerationFunction="contentGenerationFunction" icon="copyButtonIcon" successText="successText"/>
|
||||
<GenerateContentAndCopyButton t-else="" className="copyButtonClassName" content="props.record.data[props.name]" icon="copyButtonIcon" successText="successText"/>
|
||||
</div>
|
||||
</t>
|
||||
</templates>
|
||||
|
|
@ -0,0 +1,16 @@
|
|||
import { fields, Record } from "@mail/core/common/record";
|
||||
|
||||
export class HrApplicant extends Record {
|
||||
static _name = "hr.applicant";
|
||||
static id = "id";
|
||||
|
||||
partner_id = fields.One("res.partner", { inverse: "applicant_ids" });
|
||||
/**
|
||||
* The actual name of the partner (partner_id.name contains the email).
|
||||
*
|
||||
* @type {string}
|
||||
*/
|
||||
partner_name;
|
||||
}
|
||||
|
||||
HrApplicant.register();
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
import { ResPartner } from "@mail/core/common/res_partner_model";
|
||||
import { fields } from "@mail/core/common/record";
|
||||
|
||||
import { patch } from "@web/core/utils/patch";
|
||||
|
||||
patch(ResPartner.prototype, {
|
||||
/** @override */
|
||||
setup() {
|
||||
super.setup(...arguments);
|
||||
this.applicant_ids = fields.Many("hr.applicant", { inverse: "partner_id" });
|
||||
},
|
||||
});
|
||||
|
|
@ -1,109 +1,134 @@
|
|||
odoo.define('hr_recruitment.tour', function(require) {
|
||||
"use strict";
|
||||
import { _t } from "@web/core/l10n/translation";
|
||||
import { registry } from "@web/core/registry";
|
||||
import { stepUtils } from "@web_tour/tour_utils";
|
||||
import { markup } from "@odoo/owl";
|
||||
|
||||
const {_t} = require('web.core');
|
||||
const {Markup} = require('web.utils');
|
||||
var tour = require('web_tour.tour');
|
||||
|
||||
const { markup } = owl;
|
||||
|
||||
tour.register('hr_recruitment_tour',{
|
||||
url: "/web",
|
||||
rainbowManMessage: markup(_t("<div>Great job! You hired a new colleague!</div><div>Try the Website app to publish job offers online.</div>")),
|
||||
fadeout: 'very_slow',
|
||||
sequence: 230,
|
||||
}, [tour.stepUtils.showAppsMenuItem(), {
|
||||
registry.category("web_tour.tours").add('hr_recruitment_tour',{
|
||||
url: "/odoo",
|
||||
steps: () => [stepUtils.showAppsMenuItem(), {
|
||||
isActive: ["community"],
|
||||
trigger: '.o_app[data-menu-xmlid="hr_recruitment.menu_hr_recruitment_root"]',
|
||||
content: Markup(_t("Let's have a look at how to <b>improve</b> your <b>hiring process</b>.")),
|
||||
position: 'right',
|
||||
edition: 'community'
|
||||
content: markup(_t("Let's have a look at how to <b>improve</b> your <b>hiring process</b>.")),
|
||||
tooltipPosition: 'right',
|
||||
run: "click",
|
||||
}, {
|
||||
isActive: ["enterprise"],
|
||||
trigger: '.o_app[data-menu-xmlid="hr_recruitment.menu_hr_recruitment_root"]',
|
||||
content: Markup(_t("Let's have a look at how to <b>improve</b> your <b>hiring process</b>.")),
|
||||
position: 'bottom',
|
||||
edition: 'enterprise'
|
||||
content: markup(_t("Let's have a look at how to <b>improve</b> your <b>hiring process</b>.")),
|
||||
tooltipPosition: 'bottom',
|
||||
run: "click",
|
||||
}, {
|
||||
trigger: ".o-kanban-button-new",
|
||||
content: _t("Create your first Job Position."),
|
||||
position: "bottom",
|
||||
width: 195
|
||||
}, {
|
||||
tooltipPosition: "bottom",
|
||||
run: "click",
|
||||
},
|
||||
{
|
||||
trigger: ".o_hr_job_simple_form",
|
||||
},
|
||||
{
|
||||
trigger: ".o_job_name",
|
||||
extra_trigger: '.o_hr_job_simple_form',
|
||||
content: _t("What do you want to recruit today? Choose a job title..."),
|
||||
position: "right"
|
||||
}, {
|
||||
tooltipPosition: "right",
|
||||
run: "click",
|
||||
},
|
||||
{
|
||||
trigger: '.o_hr_job_simple_form',
|
||||
},
|
||||
{
|
||||
trigger: ".o_job_alias",
|
||||
extra_trigger: '.o_hr_job_simple_form',
|
||||
content: _t("Choose an application email."),
|
||||
position: "right",
|
||||
width: 195
|
||||
tooltipPosition: "right",
|
||||
run: "click",
|
||||
}, {
|
||||
trigger: '.o_create_job',
|
||||
content: _t('Let\'s create the position. An email will be setup for applications, and a public job description, if you use the Website app.'),
|
||||
position: 'bottom',
|
||||
run: function (actions) {
|
||||
actions.auto('.modal:visible .btn.btn-primary');
|
||||
},
|
||||
}, {
|
||||
trigger: ".oe_kanban_action_button",
|
||||
extra_trigger: '.o_hr_recruitment_kanban',
|
||||
content: _t("Let\'s have a look at the applications pipeline."),
|
||||
position: "bottom"
|
||||
tooltipPosition: 'bottom',
|
||||
run: "click .modal:visible .btn.btn-primary",
|
||||
}, {
|
||||
trigger: ".o_copy_paste_email",
|
||||
content: _t("Copy this email address, to paste it in your email composer, to apply."),
|
||||
position: "bottom"
|
||||
}, {
|
||||
tooltipPosition: "bottom",
|
||||
run: "click",
|
||||
},
|
||||
{
|
||||
trigger: ".o_kanban_applicant",
|
||||
},
|
||||
{
|
||||
trigger: ".breadcrumb-item:not(.active):last",
|
||||
extra_trigger: '.o_kanban_applicant',
|
||||
content: _t("Let’s go back to the dashboard."),
|
||||
position: "bottom",
|
||||
width: 195
|
||||
}, {
|
||||
trigger: ".oe_kanban_action_button",
|
||||
extra_trigger: '.o_hr_recruitment_kanban',
|
||||
content: Markup(_t("<b>Did you apply by sending an email?</b> Check incoming applications.")),
|
||||
position: "bottom"
|
||||
}, {
|
||||
trigger: ".oe_kanban_card",
|
||||
extra_trigger: '.o_kanban_applicant',
|
||||
content: Markup(_t("<b>Drag this card</b>, to qualify him for a first interview.")),
|
||||
position: "bottom",
|
||||
run: "drag_and_drop .o_kanban_group:eq(1) ",
|
||||
}, {
|
||||
trigger: ".oe_kanban_card",
|
||||
extra_trigger: '.o_kanban_applicant',
|
||||
content: Markup(_t("<b>Click to view</b> the application.")),
|
||||
position: "bottom",
|
||||
width: 195
|
||||
}, {
|
||||
trigger: ".o_Chatter .o_ChatterTopbar_buttonSendMessage",
|
||||
extra_trigger: '.o_applicant_form',
|
||||
content: Markup(_t("<div><b>Try to send an email</b> to the applicant.</div><div><i>Tips: All emails sent or received are saved in the history here</i>")),
|
||||
position: "bottom"
|
||||
}, {
|
||||
trigger: ".o_Chatter .o_Composer_buttonSend",
|
||||
extra_trigger: '.o_applicant_form',
|
||||
tooltipPosition: "bottom",
|
||||
run: "click",
|
||||
},
|
||||
{
|
||||
trigger: ".o_hr_recruitment_kanban",
|
||||
},
|
||||
{
|
||||
trigger: "button.oe_kanban_action",
|
||||
content: markup(_t("<b>Did you apply by sending an email?</b> Check incoming applications.")),
|
||||
tooltipPosition: "bottom",
|
||||
run: "click",
|
||||
},
|
||||
{
|
||||
trigger: ".o_kanban_applicant",
|
||||
},
|
||||
{
|
||||
trigger: ".o_kanban_record",
|
||||
content: markup(_t("<b>Drag this card</b>, to qualify him for a first interview.")),
|
||||
tooltipPosition: "bottom",
|
||||
run: "drag_and_drop(.o_kanban_group:eq(1))",
|
||||
},
|
||||
{
|
||||
trigger: ".o_kanban_applicant",
|
||||
},
|
||||
{
|
||||
trigger: ".o_kanban_record",
|
||||
content: markup(_t("<b>Click to view</b> the application.")),
|
||||
tooltipPosition: "bottom",
|
||||
run: "click",
|
||||
},
|
||||
{
|
||||
trigger: ".o_applicant_form",
|
||||
},
|
||||
{
|
||||
trigger: "button:contains(Send message)",
|
||||
content: markup(_t("<div><b>Try to send an email</b> to the applicant.</div><div><i>Tips: All emails sent or received are saved in the history here</i>")),
|
||||
tooltipPosition: "bottom",
|
||||
run: "click",
|
||||
},
|
||||
{
|
||||
trigger: ".o_applicant_form",
|
||||
},
|
||||
{
|
||||
trigger: ".o-mail-Chatter .o-mail-Composer button[aria-label='Send']",
|
||||
content: _t("Send your email. Followers will get a copy of the communication."),
|
||||
position: "bottom"
|
||||
}, {
|
||||
trigger: ".o_Chatter .o_ChatterTopbar_buttonLogNote",
|
||||
extra_trigger: '.o_applicant_form',
|
||||
tooltipPosition: "bottom",
|
||||
run: "click",
|
||||
},
|
||||
{
|
||||
trigger: ".o_applicant_form",
|
||||
},
|
||||
{
|
||||
trigger: "button:contains(Log note)",
|
||||
content: _t("Or talk about this applicant privately with your colleagues."),
|
||||
position: "bottom"
|
||||
}, {
|
||||
tooltipPosition: "bottom",
|
||||
run: "click",
|
||||
},
|
||||
{
|
||||
trigger: ".o_applicant_form",
|
||||
},
|
||||
{
|
||||
trigger: ".o_create_employee",
|
||||
extra_trigger: '.o_applicant_form',
|
||||
content: _t("Let’s create this new employee now."),
|
||||
position: "bottom",
|
||||
width: 225
|
||||
}, {
|
||||
tooltipPosition: "bottom",
|
||||
run: "click",
|
||||
},
|
||||
{
|
||||
trigger: ".o_hr_employee_form_view",
|
||||
},
|
||||
{
|
||||
trigger: ".o_form_button_save",
|
||||
extra_trigger: ".o_employee_form",
|
||||
content: _t("Save it !"),
|
||||
position: "bottom",
|
||||
width: 80
|
||||
}]);
|
||||
|
||||
});
|
||||
content: _t("Save it!"),
|
||||
tooltipPosition: "bottom",
|
||||
run: "click",
|
||||
}]});
|
||||
|
|
|
|||
|
|
@ -1,12 +1,12 @@
|
|||
.o_applicant_interviewer_form {
|
||||
.o_ChatterTopbar_buttonSendMessage,
|
||||
.o_ChatterTopbar_buttonLogNote,
|
||||
.o_MessageList_empty {
|
||||
.o-mail-Chatter-sendMessage,
|
||||
.o-mail-Chatter-logNote,
|
||||
.o-mail-Thread-empty {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.o_applicant_form {
|
||||
.o_form_view.o_applicant_form .o_form_renderer {
|
||||
&.o_form_readonly {
|
||||
.o_field_empty:empty {
|
||||
min-height: unset;
|
||||
|
|
@ -20,3 +20,8 @@
|
|||
}
|
||||
}
|
||||
|
||||
.o_hr_applicant_view_activity {
|
||||
.o_m2o_avatar {
|
||||
margin-right: 0px !important;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,108 +1,98 @@
|
|||
.o_kanban_renderer.o_kanban_dashboard {
|
||||
&.o_hr_recruitment_kanban {
|
||||
.o_hr_recruitment_kanban {
|
||||
.o_kanban_renderer {
|
||||
padding: 0px;
|
||||
border-top: 0px !important;
|
||||
|
||||
&.o_kanban_ungrouped .o_kanban_record {
|
||||
width: 450px;
|
||||
width: 100%;
|
||||
--KanbanRecord-padding-v: 4px !important;
|
||||
--KanbanRecord-padding-h: 0px !important;
|
||||
--Ribbon-font-size: 0.6rem;
|
||||
--Ribbon-wrapper-width: 5rem;
|
||||
--Ribbon-height: 1.9;
|
||||
|
||||
&:not(.o_kanban_ghost) {
|
||||
min-height: 220px;
|
||||
}
|
||||
}
|
||||
|
||||
.ribbon {
|
||||
&::before, &::after {
|
||||
display: none;
|
||||
}
|
||||
|
||||
span {
|
||||
padding: 5px;
|
||||
font-size: small;
|
||||
z-index: unset;
|
||||
height: auto;
|
||||
}
|
||||
}
|
||||
.ribbon-top-right {
|
||||
margin-top: -$o-kanban-dashboard-vpadding;
|
||||
|
||||
span {
|
||||
left: 0px;
|
||||
right: 30px;
|
||||
}
|
||||
}
|
||||
|
||||
.text_top_padding{
|
||||
padding-top: 0.375rem;
|
||||
}
|
||||
|
||||
.o_primary {
|
||||
display: flex !important;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding-right: 24px !important;
|
||||
|
||||
.fa {
|
||||
margin-left: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.o_job_activities {
|
||||
list-style-type: none;
|
||||
|
||||
.to-recruit {
|
||||
color: $o-enterprise-color;
|
||||
font-weight: bold;
|
||||
justify-content: center;
|
||||
}
|
||||
}
|
||||
|
||||
.o_kanban_card_header_title {
|
||||
min-height: 3rem;
|
||||
|
||||
.o_field_boolean_favorite {
|
||||
display: inline;
|
||||
}
|
||||
line-height: 1.3rem !important;
|
||||
margin-bottom: 4px !important;
|
||||
margin-top: 1px !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.o_kanban_renderer {
|
||||
&.o_kanban_applicant {
|
||||
.ribbon {
|
||||
&::before, &::after {
|
||||
display: none;
|
||||
}
|
||||
|
||||
span {
|
||||
padding: 5px;
|
||||
font-size: x-small;
|
||||
z-index: unset;
|
||||
height: auto;
|
||||
}
|
||||
.o_kanban_card_header {
|
||||
margin-bottom: 1px;
|
||||
}
|
||||
.ribbon-top-right {
|
||||
margin-top: -$o-kanban-dashboard-vpadding;
|
||||
|
||||
span {
|
||||
top: 10px;
|
||||
left: 20px;
|
||||
}
|
||||
.o_field_many2one_avatar_user .o_avatar{
|
||||
column-gap: 0.375rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.o_kanban_view {
|
||||
.oe_kanban_card {
|
||||
.o_kanban_state_with_padding {
|
||||
padding-left:7%;
|
||||
padding-bottom:5%;
|
||||
width: 12px;
|
||||
.o_kanban_renderer {
|
||||
.o_hr_recruitment_kanban_card_mobile_screen {
|
||||
min-height: 130px;
|
||||
}
|
||||
|
||||
&.o_kanban_grouped {
|
||||
.o_hr_recruitment_kanban_card_large_screen {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
&.o_kanban_ungrouped {
|
||||
@media only screen and (max-width: 768px) {
|
||||
.o_hr_recruitment_kanban_card_large_screen {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
@media only screen and (min-width: 768px) {
|
||||
.o_hr_recruitment_kanban_card_mobile_screen {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.o_view_sample_data .ribbon {
|
||||
display: none;
|
||||
.o_hr_recruitment_kanban .o_content .o_kanban_renderer .o_kanban_record {
|
||||
margin: 0px;
|
||||
}
|
||||
|
||||
.o_hr_recruitment_kanban_card_mobile_screen {
|
||||
min-height: 0px !important;
|
||||
}
|
||||
|
||||
.o_hr_recruitment_kanban {
|
||||
.o_m2o_avatar {
|
||||
margin-right: 0px !important;
|
||||
}
|
||||
|
||||
.number {
|
||||
font-size: 2rem !important;
|
||||
margin-top: 0.1rem;
|
||||
margin-bottom: 0rem;
|
||||
margin-left: 0rem;
|
||||
margin-right: 0.5rem;
|
||||
font-weight: bold;
|
||||
align-content: center;
|
||||
}
|
||||
}
|
||||
|
||||
.o_recruitment_list {
|
||||
.o_list_button {
|
||||
text-align: right;
|
||||
}
|
||||
.activity_badge {
|
||||
--badge-padding-x: .55em;
|
||||
}
|
||||
|
||||
.number_description {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.hr_recruitment_is_favorite_job .o_favorite a {
|
||||
display: flex;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,3 @@
|
|||
.o_status.o_status_orange {
|
||||
background-color: $o-warning;
|
||||
}
|
||||
|
|
@ -1,10 +1,7 @@
|
|||
/** @odoo-module */
|
||||
|
||||
import { registry } from '@web/core/registry';
|
||||
|
||||
import { formView } from '@web/views/form/form_view';
|
||||
import { FormController } from '@web/views/form/form_controller';
|
||||
import { session } from '@web/session';
|
||||
|
||||
export class InterviewerFormController extends FormController {
|
||||
|
||||
|
|
@ -18,7 +15,7 @@ export class InterviewerFormController extends FormController {
|
|||
return result;
|
||||
}
|
||||
result["o_applicant_interviewer_form"] = root.data.interviewer_ids.records.findIndex(
|
||||
interviewer => interviewer.data.id === session.uid) > -1;
|
||||
interviewer => interviewer.resId === root.data.user_id.id) > -1;
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,17 @@
|
|||
import { _t } from "@web/core/l10n/translation";
|
||||
import { FormController } from "@web/views/form/form_controller";
|
||||
|
||||
export class RecruitmentFormController extends FormController {
|
||||
/**
|
||||
* @override
|
||||
*/
|
||||
get archiveDialogProps() {
|
||||
const result = super.archiveDialogProps;
|
||||
result.body =
|
||||
this.model.root.data.all_application_count > 0
|
||||
? _t("This job position and all related applicants will be archived. Are you sure?")
|
||||
: _t("Are you sure that you want to archive this job position?");
|
||||
console.log(this.model.root.data.all_application_count);
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
import { registry } from "@web/core/registry";
|
||||
|
||||
import { formView } from "@web/views/form/form_view";
|
||||
import { RecruitmentFormController } from "@hr_recruitment/views/recruitment_form_controller";
|
||||
|
||||
export const RecruitmentFormView = {
|
||||
...formView,
|
||||
Controller: RecruitmentFormController,
|
||||
};
|
||||
|
||||
registry.category("views").add("recruitment_form_view", RecruitmentFormView);
|
||||
|
|
@ -0,0 +1,29 @@
|
|||
import { useService } from "@web/core/utils/hooks";
|
||||
import { user } from "@web/core/user";
|
||||
import { Component, onWillStart, useState } from "@odoo/owl";
|
||||
|
||||
export class RecruitmentActionHelper extends Component {
|
||||
static template = "hr_recruitment.RecruitmentActionHelper";
|
||||
static props = ["noContentHelp"];
|
||||
setup() {
|
||||
this.orm = useService("orm");
|
||||
this.actionService = useService("action");
|
||||
this.state = useState({
|
||||
hasDemoData: false,
|
||||
});
|
||||
onWillStart(async () => {
|
||||
const categoryTags = await this.orm.searchRead("hr.applicant.category", [], ["name"]);
|
||||
const demoTag = categoryTags.filter((tag) => tag.name === "Demo");
|
||||
this.state.hasDemoData = demoTag.length === 1;
|
||||
this.isRecruitmentUser = await user.hasGroup("hr_recruitment.group_hr_recruitment_user");
|
||||
});
|
||||
}
|
||||
|
||||
loadRecruitmentScenario() {
|
||||
this.actionService.doAction("hr_recruitment.action_load_demo_data");
|
||||
}
|
||||
|
||||
actionCreateJobPosition() {
|
||||
this.actionService.doAction("hr.action_create_job_position")
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,28 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<odoo>
|
||||
<t t-name="hr_recruitment.RecruitmentActionHelper">
|
||||
<div class="o_view_nocontent">
|
||||
<div class="o_nocontent_help">
|
||||
<p class="o_view_nocontent_smiling_face">
|
||||
Ready to recruit more efficiently?
|
||||
</p>
|
||||
<p>
|
||||
<a type="object" class="btn btn-link mt-3"
|
||||
t-on-click="() => this.actionCreateJobPosition()">
|
||||
Let's create a job position.
|
||||
</a>
|
||||
</p>
|
||||
<t t-if="!state.hasDemoData and isRecruitmentUser">
|
||||
<div class="d-flex gap-3 align-items-center or-separator">
|
||||
<hr class="flex-grow-1" /> or <hr class="flex-grow-1" />
|
||||
</div>
|
||||
|
||||
<a type="object" class="btn btn-secondary mt-3"
|
||||
t-on-click="() => this.loadRecruitmentScenario()">
|
||||
Load sample data
|
||||
</a>
|
||||
</t>
|
||||
</div>
|
||||
</div>
|
||||
</t>
|
||||
</odoo>
|
||||
|
|
@ -0,0 +1,44 @@
|
|||
import { _t } from "@web/core/l10n/translation";
|
||||
import { registry } from "@web/core/registry";
|
||||
import { ConfirmationDialog } from "@web/core/confirmation_dialog/confirmation_dialog";
|
||||
|
||||
import { kanbanView } from "@web/views/kanban/kanban_view";
|
||||
import { KanbanRenderer } from "@web/views/kanban/kanban_renderer";
|
||||
import { RecruitmentActionHelper } from "@hr_recruitment/views/recruitment_helper_view";
|
||||
|
||||
export class RecruitmentKanbanRenderer extends KanbanRenderer {
|
||||
static template = "hr_recruitment.RecruitmentKanbanRenderer";
|
||||
static components = {
|
||||
...KanbanRenderer.components,
|
||||
RecruitmentActionHelper,
|
||||
};
|
||||
|
||||
async archiveRecord(record, active) {
|
||||
if (active && record.data.application_count > 0) {
|
||||
this.dialog.add(ConfirmationDialog, {
|
||||
body: _t(
|
||||
"This job position and all related applicants will be archived. Are you sure?"
|
||||
),
|
||||
confirmLabel: _t("Archive"),
|
||||
confirm: () => {
|
||||
record.archive();
|
||||
this.props.list.load();
|
||||
},
|
||||
cancel: () => {},
|
||||
});
|
||||
} else if (active) {
|
||||
record.archive();
|
||||
this.props.list.load();
|
||||
} else {
|
||||
record.unarchive();
|
||||
this.props.list.load();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const RecruitmentKanbanView = {
|
||||
...kanbanView,
|
||||
Renderer: RecruitmentKanbanRenderer,
|
||||
};
|
||||
|
||||
registry.category("views").add("recruitment_kanban_view", RecruitmentKanbanView);
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<odoo>
|
||||
<t t-name="hr_recruitment.RecruitmentKanbanRenderer" t-inherit="web.KanbanRenderer" t-inherit-mode="primary">
|
||||
<ActionHelper position="replace">
|
||||
<t t-if="showNoContentHelper">
|
||||
<RecruitmentActionHelper noContentHelp="props.noContentHelp"/>
|
||||
</t>
|
||||
</ActionHelper>
|
||||
</t>
|
||||
</odoo>
|
||||
|
|
@ -0,0 +1,20 @@
|
|||
import { _t } from "@web/core/l10n/translation";
|
||||
import { ListController } from "@web/views/list/list_controller";
|
||||
|
||||
export class RecruitmentListController extends ListController {
|
||||
/**
|
||||
* @override
|
||||
*/
|
||||
get archiveDialogProps() {
|
||||
const result = super.archiveDialogProps;
|
||||
result.body =
|
||||
this.model.root.isDomainSelected || this.model.root.selection.length > 1
|
||||
? _t(
|
||||
"These job positions and all related applicants will be archived. Are you sure?"
|
||||
)
|
||||
: _t(
|
||||
"This job position and all related applicants will be archived. Are you sure?"
|
||||
);
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,22 @@
|
|||
import { registry } from "@web/core/registry";
|
||||
|
||||
import { listView } from "@web/views/list/list_view";
|
||||
import { ListRenderer } from "@web/views/list/list_renderer";
|
||||
import { RecruitmentActionHelper } from "@hr_recruitment/views/recruitment_helper_view";
|
||||
import { RecruitmentListController } from "@hr_recruitment/views/recruitment_list_controller";
|
||||
|
||||
export class RecruitmentListRenderer extends ListRenderer {
|
||||
static template = "hr_recruitment.RecruitmentListRenderer";
|
||||
static components = {
|
||||
...ListRenderer.components,
|
||||
RecruitmentActionHelper,
|
||||
};
|
||||
}
|
||||
|
||||
export const RecruitmentListView = {
|
||||
...listView,
|
||||
Controller: RecruitmentListController,
|
||||
Renderer: RecruitmentListRenderer,
|
||||
};
|
||||
|
||||
registry.category("views").add("recruitment_list_view", RecruitmentListView);
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<odoo>
|
||||
<t t-name="hr_recruitment.RecruitmentListRenderer" t-inherit="web.ListRenderer" t-inherit-mode="primary">
|
||||
<ActionHelper position="replace">
|
||||
<t t-if="showNoContentHelper">
|
||||
<RecruitmentActionHelper noContentHelp="props.noContentHelp"/>
|
||||
</t>
|
||||
</ActionHelper>
|
||||
</t>
|
||||
</odoo>
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
import { FormController } from "@web/views/form/form_controller";
|
||||
import { patch } from "@web/core/utils/patch";
|
||||
|
||||
patch(FormController.prototype, {
|
||||
getStaticActionMenuItems() {
|
||||
const menuItems = super.getStaticActionMenuItems();
|
||||
if (this.props.resModel === 'hr.applicant' && !this.model.root.data.job_id) {
|
||||
menuItems.addPropertyFieldValue.isAvailable = () => false;
|
||||
}
|
||||
return menuItems;
|
||||
},
|
||||
});
|
||||
|
|
@ -0,0 +1,52 @@
|
|||
import {
|
||||
click,
|
||||
defineMailModels,
|
||||
openView,
|
||||
registerArchs,
|
||||
start,
|
||||
startServer,
|
||||
} from "@mail/../tests/mail_test_helpers";
|
||||
import { expect, test } from "@odoo/hoot";
|
||||
import { animationFrame } from "@odoo/hoot-mock";
|
||||
import { defineModels, models } from "@web/../tests/web_test_helpers";
|
||||
|
||||
class HrApplicant extends models.ServerModel {
|
||||
_name = "hr.applicant";
|
||||
_records = [
|
||||
{
|
||||
id: 21,
|
||||
},
|
||||
];
|
||||
_views = {
|
||||
form: `<form><field name="id"/></form>`,
|
||||
};
|
||||
}
|
||||
defineMailModels();
|
||||
defineModels([HrApplicant]);
|
||||
|
||||
const newArchs = {
|
||||
"ir.attachment,false,list": `<list>
|
||||
<field name="res_id"/>
|
||||
<field name="res_model"/>
|
||||
<field name="res_name" widget="applicant_char"/>
|
||||
</list>`,
|
||||
};
|
||||
|
||||
test("Recruitment applicant_char widget on ir.attachment", async () => {
|
||||
const pyEnv = await startServer();
|
||||
pyEnv["ir.attachment"]._fields.res_id.model_field = "res_model";
|
||||
pyEnv["ir.attachment"].create([
|
||||
{ res_id: 21, res_model: "hr.applicant", res_name: "Someone" },
|
||||
{ res_id: false, res_model: "hr.applicant", res_name: "Nobody" },
|
||||
]);
|
||||
registerArchs(newArchs);
|
||||
await start();
|
||||
await openView({ res_model: "ir.attachment", views: [[false, "list"]] });
|
||||
await click(".o_field_applicant_char:last span");
|
||||
await animationFrame();
|
||||
expect(".o_field_applicant_char").toHaveCount(2);
|
||||
await click(".o_field_applicant_char:first span");
|
||||
await animationFrame();
|
||||
expect(".o_field_applicant_char").toHaveCount(0);
|
||||
expect('.o_field_widget[name="id"]:contains(21)').toHaveCount(1);
|
||||
});
|
||||
Loading…
Add table
Add a link
Reference in a new issue