[IMP] hr,hr_expense: adapt kanban views to OWL

task-2900536

closes odoo/odoo#95317

Related: odoo/enterprise#29116
Signed-off-by: Kevin Baptiste <kba@odoo.com>
This commit is contained in:
Kevin Baptiste
2022-07-12 13:21:07 +02:00
parent 7ab3254b59
commit ecf07e1043
24 changed files with 294 additions and 90 deletions
+5
View File
@@ -52,6 +52,10 @@
'hr/static/src/models/*.js',
],
'web.assets_backend': [
'hr/static/src/mixins/*.js',
'hr/static/src/views/**/*.js',
'hr/static/src/components/**/*.js',
'hr/static/src/components/**/*.scss',
'hr/static/src/scss/hr.scss',
'hr/static/src/js/chat_mixin.js',
'hr/static/src/js/hr_employee.js',
@@ -62,6 +66,7 @@
'hr/static/src/js/work_permit_upload.js',
],
'web.assets_qweb': [
'hr/static/src/components/**/*.xml',
'hr/static/src/xml/*.xml',
],
'web.qunit_suite_tests': [
@@ -0,0 +1,10 @@
/** @odoo-module */
import { registry } from '@web/core/registry';
import { ImageField } from '@web/views/fields/image/image_field';
export class BackgroundImageField extends ImageField {}
BackgroundImageField.template = 'hr.BackgroundImage';
registry.category("fields").add("background_image", BackgroundImageField);
@@ -0,0 +1,9 @@
.o_field_background_image {
> div {
background-position: center;
background-repeat: no-repeat;
background-size: cover;
width: 100%;
height: 100%;
}
}
@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<templates id="template" xml:space="preserve">
<t t-name="hr.BackgroundImage" owl="1">
<div
t-att-data-tooltip-template="hasTooltip and tooltipAttributes.template"
t-att-data-tooltip-info="hasTooltip and tooltipAttributes.info"
t-att-data-tooltip-delay="hasTooltip and props.zoomDelay"
t-attf-style="background-image: url('#{getUrl(props.previewImage or props.name)}');"/>
</t>
</templates>
-23
View File
@@ -9,11 +9,6 @@ odoo.define('hr.employee_chat', function (require) {
const ListController = require('web.ListController');
const ListView = require('web.ListView');
var KanbanController = require('web.KanbanController');
var KanbanView = require('web.KanbanView');
var KanbanRenderer = require('web.KanbanRenderer');
var KanbanRecord = require('web.KanbanRecord');
const ChatMixin = require('hr.chat_mixin');
@@ -138,22 +133,4 @@ odoo.define('hr.employee_chat', function (require) {
})
viewRegistry.add('hr_employee_list', EmployeeListView);
// USAGE OF CHAT MIXIN IN KANBAN VIEWS
var EmployeeKanbanRecord = KanbanRecord.extend(ChatMixin);
var EmployeeKanbanRenderer = KanbanRenderer.extend({
config: Object.assign({}, KanbanRenderer.prototype.config, {
KanbanRecord: EmployeeKanbanRecord,
}),
});
var EmployeeKanbanView = KanbanView.extend({
config: _.extend({}, KanbanView.prototype.config, {
Controller: KanbanController,
Renderer: EmployeeKanbanRenderer
}),
});
viewRegistry.add('hr_employee_kanban', EmployeeKanbanView);
});
+10
View File
@@ -0,0 +1,10 @@
/** @odoo-module */
const { Component } = owl;
export const EmployeeChatMixin = {
async openChat(employeeId) {
const messaging = await Component.env.services.messaging.get();
messaging.openChat({ employeeId });
},
}
+20
View File
@@ -0,0 +1,20 @@
/** @odoo-module */
import { registry } from '@web/core/registry';
import { patch } from '@web/core/utils/patch';
import { kanbanView } from '@web/views/kanban/kanban_view';
import { KanbanModel } from '@web/views/kanban/kanban_model';
import { EmployeeChatMixin } from '../mixins/chat_mixin';
export class EmployeeKanbanRecord extends KanbanModel.Record {}
patch(EmployeeKanbanRecord.prototype, 'employee_kanban_record_mixin', EmployeeChatMixin);
export class EmployeeKanbanModel extends KanbanModel {}
EmployeeKanbanModel.Record = EmployeeKanbanRecord;
registry.category('views').add('hr_employee_kanban', {
...kanbanView,
Model: EmployeeKanbanModel,
});
+3 -2
View File
@@ -128,7 +128,8 @@
<templates>
<t t-name="kanban-box">
<div class="oe_kanban_global_click o_kanban_record_has_image_fill o_hr_kanban_record">
<field name="avatar_128" widget="image" class="o_kanban_image_fill_left" options="{'zoom': true, 'zoom_delay': 1000, 'background': true, 'preventClicks': false}"/>
<field name="image_1024" class="o_kanban_image_fill_left" preview_image="image_128"
widget="background_image" options="{'zoom': true, 'zoom_delay': 1000}"/>
<div class="oe_kanban_details">
<div class="o_kanban_record_top">
@@ -178,7 +179,7 @@
<div class="o_kanban_record_bottom">
<div class="oe_kanban_bottom_left"/>
<div class="oe_kanban_bottom_right">
<a title="Chat" icon="fa-comments" href="#" class="ml8 o_employee_chat_btn" attrs="{'invisible': [('user_id','=', False)]}" role="button"><i class="fa fa-comments"/></a>
<a title="Chat" icon="fa-comments" href="#" t-on-click.prevent="() => props.record.openChat(props.record.resId)" class="ml8" attrs="{'invisible': [('user_id','=', False)]}" role="button"><i class="fa fa-comments"/></a>
</div>
</div>
</div>
+7 -5
View File
@@ -247,6 +247,7 @@
<field name="coach_id" invisible="1"/>
<field name="active" invisible="1"/>
<field name="category_ids" widget="many2many_tags" options="{'color_field': 'color'}" optional="hide"/>
<field name="country_id" optional="hide"/>
</tree>
</field>
</record>
@@ -266,7 +267,8 @@
<templates>
<t t-name="kanban-box">
<div class="oe_kanban_global_click o_kanban_record_has_image_fill o_hr_kanban_record">
<field name="avatar_128" widget="image" class="o_kanban_image_fill_left" options="{'zoom': true, 'zoom_delay': 1000, 'background': true, 'preventClicks': false}"/>
<field name="image_1024" class="o_kanban_image_fill_left" preview_image="image_128"
widget="background_image" options="{'zoom': true, 'zoom_delay': 1000}"/>
<div class="oe_kanban_details">
<div class="o_kanban_record_top">
@@ -324,10 +326,10 @@
<div class="o_kanban_record_bottom">
<div class="oe_kanban_bottom_left"/>
<div class="oe_kanban_bottom_right float-end">
<a title="Chat" icon="fa-comments" href="#" class="ml8 o_employee_chat_btn" attrs="{'invisible': [('user_id','=', False)]}" role="button"><i class="fa fa-comments"/></a>
<div class="hr_activity_container">
<field name="activity_ids" widget="kanban_activity"/>
</div>
<a title="Chat" icon="fa-comments" href="#" t-on-click.prevent="() => props.record.openChat(props.record.resId)" class="ml8" attrs="{'invisible': [('user_id','=', False)]}" role="button"><i class="fa fa-comments"/></a>
<div class="hr_activity_container">
<field name="activity_ids" widget="kanban_activity"/>
</div>
</div>
</div>
</div>
+3
View File
@@ -52,6 +52,8 @@ This module also uses analytic accounting and is compatible with the invoice on
'application': True,
'assets': {
'web.assets_backend': [
'hr_expense/static/src/mixins/*.js',
'hr_expense/static/src/views/*.js',
'hr_expense/static/src/js/expense_views.js',
'hr_expense/static/src/js/expense_form_view.js',
'hr_expense/static/src/js/expense_qr_code_action.js',
@@ -63,6 +65,7 @@ This module also uses analytic accounting and is compatible with the invoice on
'hr_expense/static/tests/tours/expense_upload_tours.js',
],
'web.assets_qweb': [
'hr_expense/static/src/views/*.xml',
'hr_expense/static/src/xml/**/*',
],
},
+1 -1
View File
@@ -335,7 +335,7 @@ class HrExpense(models.Model):
expense=expense.name, product=expense.product_id.name,
))
def create_expense_from_attachments(self, attachment_ids=None, view_type='tree'):
def create_expense_from_attachments(self, attachment_ids=None, view_type='list'):
''' Create the expenses from files.
:return: An action redirecting to hr.expense tree view.
'''
@@ -1,14 +1,11 @@
odoo.define('hr_expense.expenses.tree', function (require) {
"use strict";
var DocumentUploadMixin = require('hr_expense.documents.upload.mixin');
var KanbanController = require('web.KanbanController');
var KanbanView = require('web.KanbanView');
var ListController = require('web.ListController');
var ListView = require('web.ListView');
var viewRegistry = require('web.view_registry');
var core = require('web.core');
var ListRenderer = require('web.ListRenderer');
var KanbanRenderer = require('web.KanbanRenderer');
var session = require('web.session');
const config = require('web.config');
@@ -129,34 +126,6 @@ odoo.define('hr_expense.expenses.tree', function (require) {
})
});
var ExpensesKanbanController = KanbanController.extend(DocumentUploadMixin, {
buttons_template: 'ExpensesKanbanView.buttons',
events: _.extend({}, KanbanController.prototype.events, {
'click .o_button_upload_expense': '_onUpload',
'change .o_expense_documents_upload .o_form_binary_form': '_onAddAttachment',
}),
});
var ExpenseKanbanRenderer = KanbanRenderer.extend(ExpenseQRCodeMixin);
var ExpenseKanbanRendererHeader = ExpenseKanbanRenderer.extend(ExpenseDashboardMixin);
// The kanban view
var ExpensesKanbanView = KanbanView.extend({
config: _.extend({}, KanbanView.prototype.config, {
Controller: ExpensesKanbanController,
Renderer: ExpenseKanbanRenderer,
}),
});
// The kanban view with the Header
// Used in "My Expenses to Report", "All My Expenses" & "My Repo
var ExpensesKanbanViewHeader = ExpensesKanbanView.extend({
config: _.extend({}, ExpensesKanbanView.prototype.config, {
Renderer: ExpenseKanbanRendererHeader,
})
});
viewRegistry.add('hr_expense_tree_dashboard_upload', ExpensesListViewDashboardUpload);
// Tree view with the header.
// Used in "My Expenses to Report" & "All My Expenses"
@@ -165,8 +134,4 @@ odoo.define('hr_expense.expenses.tree', function (require) {
// Tree view with the header.
// Used in "My Reports"
viewRegistry.add('hr_expense_tree_dashboard_header', ExpensesListViewDashboardHeader);
viewRegistry.add('hr_expense_kanban', ExpensesKanbanView);
// Kanban view with the header.
// Used in "My Expenses to Report", "All My Expenses" & "My Reports"
viewRegistry.add('hr_expense_kanban_header', ExpensesKanbanViewHeader);
});
@@ -0,0 +1,50 @@
/** @odoo-module */
import { useService } from '@web/core/utils/hooks';
const { useRef } = owl;
export const ExpenseDocumentUpload = {
setup() {
this._super();
this.actionService = useService('action');
this.notification = useService('notification');
this.orm = useService('orm');
this.http = useService('http');
this.fileInput = useRef('fileInput');
},
uploadDocument() {
this.fileInput.el.click();
},
async onChangeFileInput() {
const params = {
csrf_token: odoo.csrf_token,
ufile: [...this.fileInput.el.files],
model: 'hr.expense',
id: 0,
};
const fileData = await this.http.post('/web/binary/upload_attachment', params, "text");
const attachments = JSON.parse(fileData);
if (attachments.error) {
throw new Error(attachments.error);
}
this.onUpload(attachments);
},
async onUpload(attachments) {
const attachmentIds = attachments.map((a) => a.id);
if (!attachmentIds.length) {
this.notification.add(
this.env._t('An error occurred during the upload')
);
return;
}
const action = await this.orm.call('hr.expense', 'create_expense_from_attachments', ["", attachmentIds]);
this.actionService.doAction(action);
},
};
@@ -0,0 +1,36 @@
/** @odoo-module */
import { useService } from '@web/core/utils/hooks';
import session from 'web.session';
const { onWillStart, useState } = owl;
export const ExpenseDashboardMixin = {
setup() {
this._super();
this.orm = useService('orm');
this.state = useState({
expenses: {}
});
onWillStart(async () => {
const expense_states = await this.orm.call("hr.expense", 'get_expense_dashboard', []);
this.state.expenses = expense_states;
});
},
renderMonetaryField(value, currency_id) {
value = value.toFixed(2);
const currency = session.get_currency(currency_id);
if (currency) {
if (currency.position === "after") {
value += currency.symbol;
} else {
value = currency.symbol + value;
}
}
return value;
}
}
@@ -0,0 +1,46 @@
/** @odoo-module */
import { useService } from "@web/core/utils/hooks";
const { onMounted, onPatched, useRef } = owl;
export const ExpenseMobileQRCode = {
setup() {
this._super();
this.root = useRef('root');
this.actionService = useService('action');
onMounted(this.bindAppsIcons);
onPatched(this.bindAppsIcons);
},
bindAppsIcons() {
const apps = this.root.el.querySelectorAll('.o_expense_mobile_app');
if (!apps) {
return;
}
const handler = this.handleClick.bind(this);
for (const app of apps) {
app.addEventListener('click', handler);
}
},
handleClick(ev) {
ev.preventDefault();
ev.stopPropagation();
const url = ev.currentTarget && ev.currentTarget.href;
if (!this.env.isSmall) {
this.actionService.doAction({
name: this.env._t("Download our App"),
type: "ir.actions.client",
tag: 'expense_qr_code_modal',
target: "new",
params: { url },
});
} else {
this.actionService.doAction({ type: "ir.actions.act_url", url });
}
}
};
@@ -1,6 +1,6 @@
.hr_expense {
@include media-breakpoint-up(md) {
&.o_list_view, &.o_legacy_kanban_view {
&.o_list_view, &.o_kanban_renderer {
min-height: auto;
}
}
@@ -0,0 +1,36 @@
/** @odoo-module */
import { registry } from '@web/core/registry';
import { patch } from '@web/core/utils/patch';
import { ExpenseDashboardMixin } from '../mixins/expense_dashboard';
import { ExpenseMobileQRCode } from '../mixins/qrcode';
import { ExpenseDocumentUpload } from '../mixins/document_upload';
import { kanbanView } from '@web/views/kanban/kanban_view';
import { KanbanController } from '@web/views/kanban/kanban_controller';
import { KanbanRenderer } from '@web/views/kanban/kanban_renderer';
export class ExpenseKanbanController extends KanbanController {}
patch(ExpenseKanbanController.prototype, 'expense_kanban_controller_upload', ExpenseDocumentUpload);
export class ExpenseKanbanRenderer extends KanbanRenderer {}
patch(ExpenseKanbanRenderer.prototype, 'expense_kanban_renderer_qrcode', ExpenseMobileQRCode);
ExpenseKanbanRenderer.template = 'hr_expense.KanbanRenderer';
export class ExpenseDashboardKanbanRenderer extends ExpenseKanbanRenderer {}
patch(ExpenseDashboardKanbanRenderer.prototype, 'expense_kanban_renderer_dashboard', ExpenseDashboardMixin);
registry.category('views').add('hr_expense_kanban', {
...kanbanView,
buttonTemplate: 'hr_expense.KanbanButtons',
Controller: ExpenseKanbanController,
Renderer: ExpenseKanbanRenderer,
});
registry.category('views').add('hr_expense_dashboard_kanban', {
...kanbanView,
buttonTemplate: 'hr_expense.KanbanButtons',
Controller: ExpenseKanbanController,
Renderer: ExpenseDashboardKanbanRenderer,
});
@@ -0,0 +1,38 @@
<?xml version="1.0" encoding="UTF-8"?>
<templates xml:space="preserve">
<t t-name="hr_expense.KanbanRenderer" t-inherit="web.KanbanRenderer" t-inherit-mode="primary" owl="1">
<xpath expr="//div[hasclass('o_kanban_renderer')]" position="before">
<div class="o_expense_container position-sticky start-0 d-flex o_form_statusbar">
<t t-foreach="Object.entries(state.expenses)" t-as="expense" t-key="expense[0]">
<t t-set="name" t-value="expense[0]"/>
<t t-set="data" t-value="expense[1]"/>
<div t-attf-class="o_expense_card o_arrow_button flex-grow-1 d-flex flex-column p-3 border-bottom text-center">
<span t-esc="renderMonetaryField(data['amount'], data['currency'])" class="h2 m-0 text-odoo"/>
<b class="mx-2" t-esc="data['description']"/>
</div>
<div t-if="name !== 'approved'" t-attf-class="o_expense_card o_arrow_button flex-grow-1 d-flex flex-column p-3 border-bottom text-center">
<i class="fa fa-angle-right fa-3x"/>
</div>
</t>
</div>
</xpath>
</t>
<t t-name="hr_expense.KanbanButtons" t-inherit="web.KanbanView.Buttons" t-inherit-mode="primary" owl="1">
<xpath expr="//t[@t-if='canCreate']" position="after">
<button type="button" class="d-none d-md-inline o_button_upload_expense btn btn-primary mx-1" t-on-click.prevent="uploadDocument">
Upload
</button>
</xpath>
<xpath expr="//t[@t-if='canCreate']" position="before">
<button type="button" class="d-inline d-md-none o_button_upload_expense btn btn-primary mx-1" t-on-click.prevent="uploadDocument">
Scan
</button>
</xpath>
<xpath expr="//div" position="inside">
<input type="file" name="ufile" class="d-none" t-ref="fileInput" multiple="1" accept="*" t-on-change="onChangeFileInput" />
</xpath>
</t>
</templates>
@@ -32,18 +32,4 @@
</button>
</t>
</t>
<t t-extend="KanbanView.buttons" t-name="ExpensesKanbanView.buttons">
<t t-jquery="button:last" t-operation="after">
<button type="button" class="d-none d-md-inline o_button_upload_expense btn btn-primary mx-1">
Upload
</button>
</t>
<t t-jquery="button:first" t-operation="before">
<button type="button" class="d-inline d-md-none o_button_upload_expense btn btn-primary mx-1">
Scan
</button>
</t>
</t>
</templates>
+6 -6
View File
@@ -290,7 +290,7 @@
<field name="mode">primary</field>
<field name="arch" type="xml">
<xpath expr="//kanban" position="attributes">
<attribute name="js_class">hr_expense_kanban_header</attribute>
<attribute name="js_class">hr_expense_dashboard_kanban</attribute>
</xpath>
</field>
</record>
@@ -429,10 +429,10 @@
</p>
<p>Snap pictures of your receipts and let Odoo<br/> automatically create expenses for you.</p>
<p class="d-none d-md-block">
<a href="https://apps.apple.com/be/app/odoo/id1272543640" target="_blank">
<a href="https://apps.apple.com/be/app/odoo/id1272543640" target="_blank" class="o_expense_mobile_app">
<img alt="Apple App Store" class="img img-fluid h-100 o_expense_apple_store" src="/hr_expense/static/img/app_store.png"/>
</a>
<a href="https://play.google.com/store/apps/details?id=com.odoo.mobile" target="_blank" class="o_expense_google_store">
<a href="https://play.google.com/store/apps/details?id=com.odoo.mobile" target="_blank" class="o_expense_mobile_app">
<img alt="Google Play Store" class="img img-fluid h-100 o_expense_google_store" src="/hr_expense/static/img/play_store.png"/>
</a>
</p>
@@ -466,10 +466,10 @@
</p>
<p>Snap pictures of your receipts and let Odoo<br/> automatically create expenses for you.</p>
<p class="d-none d-md-block">
<a href="https://apps.apple.com/be/app/odoo/id1272543640" target="_blank">
<a href="https://apps.apple.com/be/app/odoo/id1272543640" target="_blank" class="o_expense_mobile_app">
<img alt="Apple App Store" class="img img-fluid h-100 o_expense_apple_store" src="/hr_expense/static/img/app_store.png"/>
</a>
<a href="https://play.google.com/store/apps/details?id=com.odoo.mobile" target="_blank" class="o_expense_google_store">
<a href="https://play.google.com/store/apps/details?id=com.odoo.mobile" target="_blank" class="o_expense_mobile_app">
<img alt="Google Play Store" class="img img-fluid h-100 o_expense_google_store" src="/hr_expense/static/img/play_store.png"/>
</a>
</p>
@@ -868,7 +868,7 @@
<field name="mode">primary</field>
<field name="arch" type="xml">
<xpath expr="//kanban" position="attributes">
<attribute name="js_class">hr_expense_kanban_header</attribute>
<attribute name="js_class">hr_expense_dashboard_kanban</attribute>
</xpath>
</field>
</record>
@@ -30,6 +30,7 @@ $o-hr-holidays-border-color: map-get($grays, '300');
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;
@@ -105,7 +105,6 @@
</p>
</div>
<field name="level_ids" mode="kanban" nolabel="1"
class="o_hr_holidays_plan_level_container o_hr_holidays_plan_level_hierarchy"
add-label="Add a new level"
>
+1 -1
View File
@@ -96,7 +96,7 @@
<field name="name">hr.leave.view.kanban</field>
<field name="model">hr.leave</field>
<field name="arch" type="xml">
<kanban class="o_kanban_mobile" js_class="time_off_kanban" create="0" sample="1">
<kanban class="o_kanban_mobile" create="0" sample="1">
<field name="employee_id"/>
<field name="date_from"/>
<field name="date_to"/>
+1 -1
View File
@@ -136,7 +136,7 @@
<xpath expr="//li[@id='last_login']" position="inside">
<span t-if="record.current_leave_id.raw_value" style="font-size: 100%"
t-att-class="record.current_leave_state.raw_value=='validate'?'oe_kanban_button oe_kanban_color_3':'oe_kanban_button oe_kanban_color_2'"
t-att-title="moment(record.leave_date_from.raw_value).format('ddd Do MMM') + ' - ' + moment(record.leave_date_to.raw_value).format('ddd Do MMM')">
t-att-title="luxon.DateTime.fromJSDate(record.leave_date_from.raw_value).toFormat('ccc d MMM') + ' - ' + luxon.DateTime.fromJSDate(record.leave_date_to.raw_value).toFormat('ccc d MMM')">
<field name="current_leave_id"/>
</span>
</xpath>