mirror of
https://github.com/bringout/oca-ocb-hr.git
synced 2026-04-24 09:52:02 +02:00
Initial commit: Hr packages
This commit is contained in:
commit
62531cd146
2820 changed files with 1432848 additions and 0 deletions
|
|
@ -0,0 +1,28 @@
|
|||
/** @odoo-module */
|
||||
|
||||
import { useService } from '@web/core/utils/hooks';
|
||||
import { formatMonetary } from "@web/views/fields/formatters";
|
||||
|
||||
const { Component, onWillStart, useState } = owl;
|
||||
|
||||
export class ExpenseDashboard extends Component {
|
||||
|
||||
setup() {
|
||||
super.setup();
|
||||
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) {
|
||||
return formatMonetary(value, { currencyId: currency_id});;
|
||||
}
|
||||
}
|
||||
ExpenseDashboard.template = 'hr_expense.ExpenseDashboard';
|
||||
|
|
@ -0,0 +1,19 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<templates xml:space="preserve">
|
||||
<t t-name="hr_expense.ExpenseDashboard" owl="1">
|
||||
<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 class="fa fa-question-circle-o flex-grow-0.5 d-flex flex-column p-3 border-bottom text-center" t-if="env.debug" data-tooltip="Numbers computed from your personal expenses."/>
|
||||
</div>
|
||||
</t>
|
||||
</templates>
|
||||
|
|
@ -0,0 +1,18 @@
|
|||
/** @odoo-module */
|
||||
|
||||
import { registry } from "@web/core/registry";
|
||||
import { Component } from "@odoo/owl";
|
||||
|
||||
const actionRegistry = registry.category("actions");
|
||||
|
||||
class QRModalComponent extends Component {
|
||||
setup() {
|
||||
this.url = _.str.sprintf(
|
||||
"/report/barcode/?barcode_type=QR&value=%s&width=256&height=256&humanreadable=1",
|
||||
this.props.action.params.url);
|
||||
}
|
||||
}
|
||||
|
||||
QRModalComponent.template = "hr_expense.QRModalComponent"
|
||||
|
||||
actionRegistry.add("expense_qr_code_modal", QRModalComponent);
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
<?xml version="1.0"?>
|
||||
<templates>
|
||||
<t t-name="hr_expense.QRModalComponent" owl="1">
|
||||
<div style="text-align:center;" class="o_expense_modal">
|
||||
<t t-if="url">
|
||||
<h3>Scan this QR code to get the Odoo app:</h3><br/><br/>
|
||||
<img class="border border-dark rounded" t-att-src="url"/>
|
||||
</t>
|
||||
</div>
|
||||
</t>
|
||||
</templates>
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 26 KiB |
|
|
@ -0,0 +1,82 @@
|
|||
odoo.define('hr_expense.tour', function(require) {
|
||||
"use strict";
|
||||
|
||||
const {_t} = require('web.core');
|
||||
const {Markup} = require('web.utils');
|
||||
var tour = require('web_tour.tour');
|
||||
|
||||
tour.register('hr_expense_tour' , {
|
||||
url: "/web",
|
||||
rainbowManMessage: _t("There you go - expense management in a nutshell!"),
|
||||
}, [tour.stepUtils.showAppsMenuItem(), {
|
||||
trigger: '.o_app[data-menu-xmlid="hr_expense.menu_hr_expense_root"]',
|
||||
content: _t("Wasting time recording your receipts? Let’s try a better way."),
|
||||
position: 'right',
|
||||
edition: 'community'
|
||||
}, {
|
||||
trigger: '.o_app[data-menu-xmlid="hr_expense.menu_hr_expense_root"]',
|
||||
content: _t("Wasting time recording your receipts? Let’s try a better way."),
|
||||
position: 'bottom',
|
||||
edition: 'enterprise'
|
||||
}, {
|
||||
trigger: '.o_list_button_add',
|
||||
extra_trigger: '.o_button_upload_expense',
|
||||
content: _t("It all begins here - let's go!"),
|
||||
position: 'bottom',
|
||||
mobile: false,
|
||||
}, {
|
||||
trigger: '.o-kanban-button-new',
|
||||
extra_trigger: '.o_button_upload_expense',
|
||||
content: _t("It all begins here - let's go!"),
|
||||
position: 'bottom',
|
||||
mobile: true,
|
||||
}, {
|
||||
trigger: '.o_field_widget[name="product_id"] .o_input_dropdown',
|
||||
extra_trigger: '.o_expense_form',
|
||||
content: _t("Enter a name then choose a category and configure the amount of your expense."),
|
||||
position: 'bottom',
|
||||
}, {
|
||||
trigger: '.o_form_status_indicator_dirty .o_form_button_save',
|
||||
extra_trigger: '.o_expense_form',
|
||||
content: Markup(_t("Ready? You can save it manually or discard modifications from here. You don't <em>need to save</em> - Odoo will save eveyrthing for you when you navigate.")),
|
||||
position: 'bottom',
|
||||
}, ...tour.stepUtils.statusbarButtonsSteps(_t("Attach Receipt"), _t("Attach a receipt - usually an image or a PDF file.")),
|
||||
...tour.stepUtils.statusbarButtonsSteps(_t("Create Report"), _t("Create a report to submit one or more expenses to your manager.")),
|
||||
...tour.stepUtils.statusbarButtonsSteps(_t("Submit to Manager"), Markup(_t('Once your <b>Expense Report</b> is ready, you can submit it to your manager and wait for approval.'))),
|
||||
...tour.stepUtils.goBackBreadcrumbsMobile(
|
||||
_t("Use the breadcrumbs to go back to the list of expenses."),
|
||||
undefined,
|
||||
".o_expense_form",
|
||||
),
|
||||
{
|
||||
trigger: '.breadcrumb > li.breadcrumb-item:first',
|
||||
extra_trigger: ".o_expense_form",
|
||||
content: _t("Let's go back to your expenses."),
|
||||
position: 'bottom',
|
||||
mobile: false,
|
||||
}, {
|
||||
trigger: '.o_expense_container',
|
||||
content: _t("The status of all your current expenses is visible from here."),
|
||||
position: 'bottom',
|
||||
},
|
||||
tour.stepUtils.openBuggerMenu(),
|
||||
{
|
||||
trigger: "[data-menu-xmlid='hr_expense.menu_hr_expense_report']",
|
||||
extra_trigger: '.o_main_navbar',
|
||||
content: _t("Let's check out where you can manage all your employees expenses"),
|
||||
position: "bottom"
|
||||
}, {
|
||||
trigger: '.o_list_renderer tbody tr[data-id]',
|
||||
content: _t('Managers can inspect all expenses from here.'),
|
||||
position: 'bottom',
|
||||
mobile: false,
|
||||
}, {
|
||||
trigger: '.o_kanban_renderer .oe_kanban_card',
|
||||
content: _t('Managers can inspect all expenses from here.'),
|
||||
position: 'bottom',
|
||||
mobile: true,
|
||||
},
|
||||
...tour.stepUtils.statusbarButtonsSteps(_t("Approve"), _t("Managers can approve the report here, then an accountant can post the accounting entries.")),
|
||||
]);
|
||||
|
||||
});
|
||||
|
|
@ -0,0 +1,116 @@
|
|||
/** @odoo-module */
|
||||
|
||||
import { useBus, useService } from '@web/core/utils/hooks';
|
||||
|
||||
const { useRef, useEffect, useState } = owl;
|
||||
|
||||
export const ExpenseDocumentDropZone = {
|
||||
setup() {
|
||||
this._super();
|
||||
this.dragState = useState({
|
||||
showDragZone: false,
|
||||
});
|
||||
this.root = useRef("root");
|
||||
|
||||
useEffect(
|
||||
(el) => {
|
||||
if (!el) {
|
||||
return;
|
||||
}
|
||||
const highlight = this.highlight.bind(this);
|
||||
const unhighlight = this.unhighlight.bind(this);
|
||||
const drop = this.onDrop.bind(this);
|
||||
el.addEventListener("dragover", highlight);
|
||||
el.addEventListener("dragleave", unhighlight);
|
||||
el.addEventListener("drop", drop);
|
||||
return () => {
|
||||
el.removeEventListener("dragover", highlight);
|
||||
el.removeEventListener("dragleave", unhighlight);
|
||||
el.removeEventListener("drop", drop);
|
||||
};
|
||||
},
|
||||
() => [document.querySelector('.o_content')]
|
||||
);
|
||||
},
|
||||
|
||||
highlight(ev) {
|
||||
ev.stopPropagation();
|
||||
ev.preventDefault();
|
||||
this.dragState.showDragZone = true;
|
||||
},
|
||||
|
||||
unhighlight(ev) {
|
||||
ev.stopPropagation();
|
||||
ev.preventDefault();
|
||||
this.dragState.showDragZone = false;
|
||||
},
|
||||
|
||||
async onDrop(ev) {
|
||||
ev.preventDefault();
|
||||
await this.env.bus.trigger("change_file_input", {
|
||||
files: ev.dataTransfer.files,
|
||||
});
|
||||
},
|
||||
};
|
||||
|
||||
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');
|
||||
this.root = useRef("root");
|
||||
this.isExpense = this.model.rootParams.resModel === "hr.expense";
|
||||
|
||||
useBus(this.env.bus, "change_file_input", async (ev) => {
|
||||
this.fileInput.el.files = ev.detail.files;
|
||||
await this.onChangeFileInput();
|
||||
});
|
||||
},
|
||||
|
||||
displayCreateReport() {
|
||||
return this.isExpense;
|
||||
},
|
||||
|
||||
async onCreateReportClick() {
|
||||
const records = this.model.root.selection;
|
||||
const recordIds = records.map((a) => a.resId);
|
||||
const action = await this.orm.call('hr.expense', 'get_expenses_to_submit', [recordIds]);
|
||||
this.actionService.doAction(action);
|
||||
},
|
||||
|
||||
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,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 });
|
||||
}
|
||||
}
|
||||
};
|
||||
|
|
@ -0,0 +1,50 @@
|
|||
.hr_expense {
|
||||
@include media-breakpoint-up(md) {
|
||||
&.o_list_view, &.o_kanban_renderer {
|
||||
min-height: auto;
|
||||
}
|
||||
}
|
||||
|
||||
.o_view_nocontent {
|
||||
top: 10%;
|
||||
|
||||
.o_view_nocontent_expense_receipt:before {
|
||||
@extend %o-nocontent-init-image;
|
||||
width: 300px;
|
||||
height: 230px;
|
||||
background: transparent url(/hr_expense/static/img/nocontent.png) no-repeat center;
|
||||
background-size: 300px 230px;
|
||||
margin-bottom: 0.75rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.o_kanban_view .o_cp_bottom_left:has(.o_button_create_report) {
|
||||
align-items: baseline;
|
||||
}
|
||||
|
||||
.o_expense_container {
|
||||
@include media-breakpoint-down(sm) {
|
||||
overflow: auto visible;
|
||||
}
|
||||
}
|
||||
|
||||
.o_dropzone {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
position: absolute;
|
||||
background-color: #AAAA;
|
||||
z-index: 2;
|
||||
left: 0;
|
||||
top: 0;
|
||||
i {
|
||||
justify-content: center;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
height: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.o_expense_categories td[name="description"] p:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
|
@ -0,0 +1,47 @@
|
|||
/** @odoo-module **/
|
||||
|
||||
import { useService } from "@web/core/utils/hooks";
|
||||
import { registry } from "@web/core/registry";
|
||||
import { FormController } from "@web/views/form/form_controller";
|
||||
import { formView } from "@web/views/form/form_view";
|
||||
|
||||
import { ConfirmationDialog } from "@web/core/confirmation_dialog/confirmation_dialog";
|
||||
|
||||
export class ExpenseFormController extends FormController {
|
||||
setup() {
|
||||
super.setup();
|
||||
this.dialogService = useService("dialog");
|
||||
this.orm = useService("orm");
|
||||
}
|
||||
|
||||
/**
|
||||
* @override
|
||||
*/
|
||||
async beforeExecuteActionButton(clickParams) {
|
||||
const record = this.model.root;
|
||||
if (
|
||||
clickParams.name === "action_submit_expenses" &&
|
||||
record.data.duplicate_expense_ids.count
|
||||
) {
|
||||
return new Promise((resolve) => {
|
||||
this.dialogService.add(ConfirmationDialog, {
|
||||
body: this.env._t("An expense of same category, amount and date already exists."),
|
||||
confirm: async () => {
|
||||
await this.orm.call("hr.expense", "action_approve_duplicates", [record.resId]);
|
||||
resolve(true);
|
||||
},
|
||||
}, {
|
||||
onClose: resolve.bind(null, false),
|
||||
});
|
||||
});
|
||||
}
|
||||
return super.beforeExecuteActionButton(...arguments);
|
||||
}
|
||||
}
|
||||
|
||||
export const ExpenseFormView = {
|
||||
...formView,
|
||||
Controller: ExpenseFormController,
|
||||
};
|
||||
|
||||
registry.category("views").add("hr_expense_form_view", ExpenseFormView);
|
||||
|
|
@ -0,0 +1,38 @@
|
|||
/** @odoo-module */
|
||||
|
||||
import { registry } from '@web/core/registry';
|
||||
import { patch } from '@web/core/utils/patch';
|
||||
|
||||
import { ExpenseDashboard } from '../components/expense_dashboard';
|
||||
import { ExpenseMobileQRCode } from '../mixins/qrcode';
|
||||
import { ExpenseDocumentUpload, ExpenseDocumentDropZone } 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);
|
||||
patch(ExpenseKanbanRenderer.prototype, 'expense_kanban_renderer_qrcode_dzone', ExpenseDocumentDropZone);
|
||||
ExpenseKanbanRenderer.template = 'hr_expense.KanbanRenderer';
|
||||
|
||||
export class ExpenseDashboardKanbanRenderer extends ExpenseKanbanRenderer {}
|
||||
ExpenseDashboardKanbanRenderer.components = { ...ExpenseDashboardKanbanRenderer.components, ExpenseDashboard};
|
||||
ExpenseDashboardKanbanRenderer.template = 'hr_expense.DashboardKanbanRenderer';
|
||||
|
||||
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,39 @@
|
|||
<?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 t-if="dragState.showDragZone" class="o_dropzone">
|
||||
<i class="fa fa-upload fa-10x"></i>
|
||||
</div>
|
||||
</xpath>
|
||||
</t>
|
||||
|
||||
<t t-name="hr_expense.DashboardKanbanRenderer" t-inherit="hr_expense.KanbanRenderer" t-inherit-mode="primary" owl="1">
|
||||
<xpath expr="//div[hasclass('o_kanban_renderer')]" position="before">
|
||||
<ExpenseDashboard/>
|
||||
</xpath>
|
||||
</t>
|
||||
|
||||
<t t-name="hr_expense.KanbanButtons" t-inherit="web.KanbanView.Buttons" t-inherit-mode="primary" owl="1">
|
||||
<!-- Remove class 'align-items-baseline' to ensure consistency with list buttons when adding a third button
|
||||
(Create Report) on mobile. Instead, align-items: baseline is added to parent div in css -->
|
||||
<xpath expr="//div[@t-if='props.showButtons']" position="attributes">
|
||||
<attribute name="class" remove="align-items-baseline" separator=" "/>
|
||||
</xpath>
|
||||
<xpath expr="//t[@t-if='canCreate']" position="after">
|
||||
<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>
|
||||
<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>
|
||||
<button t-if="displayCreateReport()" class="btn btn-secondary o_button_create_report" t-on-click="onCreateReportClick">
|
||||
Create Report
|
||||
</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>
|
||||
|
|
@ -0,0 +1,90 @@
|
|||
/** @odoo-module */
|
||||
|
||||
import { ExpenseDashboard } from '../components/expense_dashboard';
|
||||
import { ExpenseMobileQRCode } from '../mixins/qrcode';
|
||||
import { ExpenseDocumentUpload, ExpenseDocumentDropZone } from '../mixins/document_upload';
|
||||
|
||||
import { registry } from '@web/core/registry';
|
||||
import { patch } from '@web/core/utils/patch';
|
||||
import { useService } from '@web/core/utils/hooks';
|
||||
import { listView } from "@web/views/list/list_view";
|
||||
|
||||
import { ListController } from "@web/views/list/list_controller";
|
||||
import { ListRenderer } from "@web/views/list/list_renderer";
|
||||
|
||||
const { onWillStart } = owl;
|
||||
|
||||
export class ExpenseListController extends ListController {
|
||||
setup() {
|
||||
super.setup();
|
||||
this.orm = useService('orm');
|
||||
this.actionService = useService('action');
|
||||
this.rpc = useService("rpc");
|
||||
this.user = useService("user");
|
||||
this.isExpenseSheet = this.model.rootParams.resModel === "hr.expense.sheet";
|
||||
|
||||
onWillStart(async () => {
|
||||
this.is_expense_team_approver = await this.user.hasGroup("hr_expense.group_hr_expense_team_approver");
|
||||
this.is_account_invoicing = await this.user.hasGroup("account.group_account_invoice");
|
||||
});
|
||||
}
|
||||
|
||||
displaySubmit() {
|
||||
const records = this.model.root.selection;
|
||||
return records.length && records.every(record => record.data.state === 'draft') && this.isExpenseSheet;
|
||||
}
|
||||
|
||||
displayApprove() {
|
||||
const records = this.model.root.selection;
|
||||
return this.is_expense_team_approver && records.length && records.every(record => record.data.state === 'submit') && this.isExpenseSheet;
|
||||
}
|
||||
|
||||
displayPost() {
|
||||
const records = this.model.root.selection;
|
||||
return this.is_account_invoicing && records.length && records.every(record => record.data.state === 'approve') && this.isExpenseSheet;
|
||||
}
|
||||
|
||||
async onClick (action) {
|
||||
const records = this.model.root.selection;
|
||||
const recordIds = records.map((a) => a.resId);
|
||||
const model = this.model.rootParams.resModel;
|
||||
const context = {};
|
||||
if (action === 'approve_expense_sheets') {
|
||||
context['validate_analytic'] = true;
|
||||
}
|
||||
await this.orm.call(model, action, [recordIds], {context: context});
|
||||
// sgv note: we tried this.model.notify(); and does not work
|
||||
await this.model.root.load();
|
||||
this.render(true);
|
||||
}
|
||||
|
||||
}
|
||||
patch(ExpenseListController.prototype, 'expense_list_controller_upload', ExpenseDocumentUpload);
|
||||
|
||||
export class ExpenseListRenderer extends ListRenderer {
|
||||
setup() {
|
||||
super.setup()
|
||||
}
|
||||
}
|
||||
patch(ExpenseListRenderer.prototype, 'expense_list_renderer_qrcode', ExpenseMobileQRCode);
|
||||
patch(ExpenseListRenderer.prototype, 'expense_list_renderer_qrcode_dzone', ExpenseDocumentDropZone);
|
||||
ExpenseListRenderer.template = 'hr_expense.ListRenderer';
|
||||
|
||||
export class ExpenseDashboardListRenderer extends ExpenseListRenderer {}
|
||||
|
||||
ExpenseDashboardListRenderer.components = { ...ExpenseDashboardListRenderer.components, ExpenseDashboard};
|
||||
ExpenseDashboardListRenderer.template = 'hr_expense.DashboardListRenderer';
|
||||
|
||||
registry.category('views').add('hr_expense_tree', {
|
||||
...listView,
|
||||
buttonTemplate: 'hr_expense.ListButtons',
|
||||
Controller: ExpenseListController,
|
||||
Renderer: ExpenseListRenderer,
|
||||
});
|
||||
|
||||
registry.category('views').add('hr_expense_dashboard_tree', {
|
||||
...listView,
|
||||
buttonTemplate: 'hr_expense.ListButtons',
|
||||
Controller: ExpenseListController,
|
||||
Renderer: ExpenseDashboardListRenderer,
|
||||
});
|
||||
|
|
@ -0,0 +1,56 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<templates xml:space="preserve">
|
||||
<t t-name="hr_expense.ListButtons" t-inherit="web.ListView.Buttons" t-inherit-mode="primary" owl="1">
|
||||
|
||||
<!-- hr.expense and hr.expense.sheet -->
|
||||
<xpath expr="//button[hasclass('o_list_button_add')]" position="after">
|
||||
<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>
|
||||
<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>
|
||||
<button t-if="displayCreateReport()" class="btn btn-secondary o_button_create_report" t-on-click="onCreateReportClick">
|
||||
Create Report
|
||||
</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>
|
||||
|
||||
<!-- hr.expense.sheet -->
|
||||
<xpath expr="//button[hasclass('o_button_upload_expense')]" position="after">
|
||||
<button t-if="displaySubmit()" class="d-none d-md-block btn btn-secondary" t-on-click="() => this.onClick('action_submit_sheet')">
|
||||
Submit
|
||||
</button>
|
||||
</xpath>
|
||||
|
||||
<xpath expr="//button[hasclass('o_button_upload_expense')]" position="after">
|
||||
<button t-if="displayApprove()" class="d-none d-md-block btn btn-secondary" t-on-click="() => this.onClick('approve_expense_sheets')">
|
||||
Approve Report
|
||||
</button>
|
||||
</xpath>
|
||||
|
||||
<xpath expr="//button[hasclass('o_button_upload_expense')]" position="after">
|
||||
<button t-if="displayPost()" class="d-none d-md-block btn btn-secondary" t-on-click="() => this.onClick('action_sheet_move_create')">
|
||||
Post Entries
|
||||
</button>
|
||||
</xpath>
|
||||
</t>
|
||||
|
||||
<t t-name="hr_expense.ListRenderer" t-inherit="web.ListRenderer" t-inherit-mode="primary" owl="1">
|
||||
<xpath expr="//div[hasclass('o_list_renderer')]" position="before">
|
||||
<div t-if="dragState.showDragZone" class="o_dropzone">
|
||||
<i class="fa fa-upload fa-10x"></i>
|
||||
</div>
|
||||
</xpath>
|
||||
</t>
|
||||
|
||||
|
||||
<t t-name="hr_expense.DashboardListRenderer" t-inherit="hr_expense.ListRenderer" t-inherit-mode="primary" owl="1">
|
||||
<xpath expr="//div[hasclass('o_list_renderer')]" position="before">
|
||||
<ExpenseDashboard/>
|
||||
</xpath>
|
||||
</t>
|
||||
</templates>
|
||||
|
|
@ -0,0 +1,48 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
|
||||
<templates>
|
||||
<t t-name="hr.expense.DocumentsHiddenUploadForm">
|
||||
<div class="d-none o_expense_documents_upload">
|
||||
<t t-call="HiddenInputFile">
|
||||
<t t-set="multi_upload" t-value="true"/>
|
||||
<t t-set="fileupload_id" t-value="widget.fileUploadID"/>
|
||||
<t t-set="fileupload_action" t-translation="off">/web/binary/upload_attachment</t>
|
||||
<input type="hidden" name="model" t-att-value="'hr.expense'"/>
|
||||
<input type="hidden" name="id" t-att-value="0"/>
|
||||
</t>
|
||||
</div>
|
||||
</t>
|
||||
|
||||
<t t-name="hr.expense.DocumentDropZone">
|
||||
<div class="o_drop_area d-none">
|
||||
<i class="fa fa-upload fa-10x"></i>
|
||||
</div>
|
||||
</t>
|
||||
|
||||
<t t-extend="ListView.buttons" t-name="ExpensesListView.buttons">
|
||||
<t t-jquery="button.o_list_button_add" t-operation="after">
|
||||
<button type="button" t-att-class="'d-none d-md-block btn' + (!widget.isMobile ? ' btn-secondary' : '') + ' o_button_upload_expense'">
|
||||
Scan
|
||||
</button>
|
||||
</t>
|
||||
|
||||
<t t-jquery="button.o_list_button_add" t-operation="before">
|
||||
<button type="button" t-att-class="'d-block d-md-none btn' + (widget.isMobile ? ' btn-primary' : '') + ' o_button_upload_expense'">
|
||||
Scan
|
||||
</button>
|
||||
</t>
|
||||
|
||||
<!-- hr.expense buttons -->
|
||||
<t t-jquery="button.o_list_button_add" t-operation="after">
|
||||
<button type="button" t-att-class="'btn btn-secondary' + (widget.isExpense ? '' : ' d-none') + ' o_button_create_report'">
|
||||
Create Report
|
||||
</button>
|
||||
</t>
|
||||
|
||||
<t t-jquery="button.o_list_button_add" t-operation="replace">
|
||||
<button type="button" t-att-class="'btn' + (widget.isMobile ? ' btn-secondary' : ' btn-primary') + ' o_list_button_add'" title="Create record" accesskey="c">
|
||||
Create
|
||||
</button>
|
||||
</t>
|
||||
</t>
|
||||
</templates>
|
||||
|
|
@ -0,0 +1,17 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
|
||||
<templates id="template">
|
||||
<t t-name="hr_expense.dashboard_list_header">
|
||||
<div class="o_expense_container position-sticky start-0 d-flex o_form_statusbar">
|
||||
<t t-foreach="expenses" t-as="expense">
|
||||
<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="render_monetary_field(expenses[expense]['amount'], expenses[expense]['currency'])" class="h2 m-0 text-odoo"/>
|
||||
<b class="mx-2" t-esc="expenses[expense]['description']"/>
|
||||
</div>
|
||||
<div t-if="expense !== '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>
|
||||
</t>
|
||||
</templates>
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<templates id="template" xml:space="preserve">
|
||||
<t t-name="hr_expense_qr_code">
|
||||
<div style="text-align:center;" class="o_expense_modal">
|
||||
<t t-if="widget.url">
|
||||
<h3>Scan this QR code to get the Odoo app:</h3><br/><br/>
|
||||
<img class="border border-dark rounded" t-att-src="widget.url"/>
|
||||
</t>
|
||||
</div>
|
||||
</t>
|
||||
</templates>
|
||||
Loading…
Add table
Add a link
Reference in a new issue