mirror of
https://github.com/bringout/oca-ocb-accounting.git
synced 2026-04-21 02:02:02 +02:00
Initial commit: Accounting packages
This commit is contained in:
commit
4ef34c2317
2661 changed files with 1709616 additions and 0 deletions
|
|
@ -0,0 +1,84 @@
|
|||
/** @odoo-module **/
|
||||
|
||||
import { registry } from "@web/core/registry";
|
||||
import { createElement, append } from "@web/core/utils/xml";
|
||||
import { Notebook } from "@web/core/notebook/notebook";
|
||||
import { formView } from "@web/views/form/form_view";
|
||||
import { FormCompiler } from "@web/views/form/form_compiler";
|
||||
import { FormRenderer } from "@web/views/form/form_renderer";
|
||||
import { FormController } from '@web/views/form/form_controller';
|
||||
import { useService } from "@web/core/utils/hooks";
|
||||
|
||||
export class AccountMoveController extends FormController {
|
||||
setup() {
|
||||
super.setup();
|
||||
this.account_move_service = useService("account_move");
|
||||
}
|
||||
|
||||
async deleteRecord() {
|
||||
if ( !await this.account_move_service.addDeletionDialog(this, this.model.root.resId)) {
|
||||
return super.deleteRecord(...arguments);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export class AccountMoveFormNotebook extends Notebook {
|
||||
onAnchorClicked(ev) {
|
||||
if (ev.detail.detail.id === "#outstanding") {
|
||||
ev.preventDefault();
|
||||
ev.detail.detail.originalEv.preventDefault();
|
||||
}
|
||||
}
|
||||
|
||||
async changeTabTo(page_id) {
|
||||
if (this.props.onBeforeTabSwitch) {
|
||||
await this.props.onBeforeTabSwitch(page_id);
|
||||
}
|
||||
this.state.currentPage = page_id;
|
||||
}
|
||||
}
|
||||
AccountMoveFormNotebook.template = "account.AccountMoveFormNotebook";
|
||||
AccountMoveFormNotebook.props = {
|
||||
...Notebook.props,
|
||||
onBeforeTabSwitch: { type: Function, optional: true },
|
||||
}
|
||||
export class AccountMoveFormRenderer extends FormRenderer {
|
||||
async saveBeforeTabChange() {
|
||||
if (this.props.record.mode === "edit" && this.props.record.isDirty) {
|
||||
const contentEl = document.querySelector('.o_content');
|
||||
const scrollPos = contentEl.scrollTop;
|
||||
await this.props.record.save({
|
||||
stayInEdition: true,
|
||||
});
|
||||
if (scrollPos) {
|
||||
contentEl.scrollTop = scrollPos;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
AccountMoveFormRenderer.components = {
|
||||
...FormRenderer.components,
|
||||
AccountMoveFormNotebook: AccountMoveFormNotebook,
|
||||
}
|
||||
export class AccountMoveFormCompiler extends FormCompiler {
|
||||
compileNotebook(el, params) {
|
||||
const originalNoteBook = super.compileNotebook(...arguments);
|
||||
const noteBook = createElement("AccountMoveFormNotebook");
|
||||
for (const attr of originalNoteBook.attributes) {
|
||||
noteBook.setAttribute(attr.name, attr.value);
|
||||
}
|
||||
noteBook.setAttribute("onBeforeTabSwitch", "() => this.saveBeforeTabChange()");
|
||||
const slots = originalNoteBook.childNodes;
|
||||
append(noteBook, [...slots]);
|
||||
return noteBook;
|
||||
}
|
||||
}
|
||||
|
||||
export const AccountMoveFormView = {
|
||||
...formView,
|
||||
Renderer: AccountMoveFormRenderer,
|
||||
Compiler: AccountMoveFormCompiler,
|
||||
Controller: AccountMoveController,
|
||||
};
|
||||
|
||||
registry.category("views").add("account_move_form", AccountMoveFormView);
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
|
||||
<templates xml:space="preserve">
|
||||
<t t-name="account.AccountMoveFormNotebook" t-inherit="web.Notebook" t-inherit-mode="primary" owl="1">
|
||||
<xpath expr="//a[@class='nav-link']" position="attributes">
|
||||
<attribute name="t-on-click.prevent">() => this.changeTabTo(navItem[0])</attribute>
|
||||
</xpath>
|
||||
</t>
|
||||
</templates>
|
||||
|
|
@ -0,0 +1,28 @@
|
|||
/** @odoo-module **/
|
||||
|
||||
import { ConfirmationDialog } from "@web/core/confirmation_dialog/confirmation_dialog";
|
||||
import { escape } from "@web/core/utils/strings";
|
||||
import { markup } from "@odoo/owl";
|
||||
import { registry } from "@web/core/registry";
|
||||
|
||||
|
||||
export const accountMove = {
|
||||
dependencies: ["dialog", "orm"],
|
||||
start(env, { dialog, orm }) {
|
||||
return {
|
||||
async addDeletionDialog(component, moveIds) {
|
||||
const isMoveEndOfChain = await orm.call('account.move', 'check_move_sequence_chain', [moveIds]);
|
||||
if (!isMoveEndOfChain) {
|
||||
const message = env._t("This operation will create a gap in the sequence.");
|
||||
const confirmationDialogProps = component.deleteConfirmationDialogProps;
|
||||
confirmationDialogProps.body = markup(`<div class="text-danger">${escape(message)}</div>${escape(confirmationDialogProps.body)}`);
|
||||
dialog.add(ConfirmationDialog, confirmationDialogProps);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
registry.category("services").add("account_move", accountMove);
|
||||
|
|
@ -0,0 +1,103 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
|
||||
<templates xml:space="preserve">
|
||||
|
||||
<t t-name="account.AccountPaymentField" owl="1">
|
||||
<div>
|
||||
<t t-if="outstanding">
|
||||
<div>
|
||||
<strong class="float-start" id="outstanding" t-esc="title"/>
|
||||
</div>
|
||||
</t>
|
||||
<table style="width:100%;">
|
||||
<t t-foreach="lines" t-as="line" t-key="line_index">
|
||||
<tr>
|
||||
<t t-if="outstanding">
|
||||
<td>
|
||||
<a title="assign to invoice"
|
||||
role="button"
|
||||
class="oe_form_field btn btn-secondary outstanding_credit_assign"
|
||||
t-att-data-id="line.id"
|
||||
style="margin-right: 0px; padding-left: 5px; padding-right: 5px;"
|
||||
href="#"
|
||||
data-bs-toggle="tooltip"
|
||||
t-on-click.prevent="() => this.assignOutstandingCredit(line.id)">Add</a>
|
||||
</td>
|
||||
<td style="max-width: 11em;">
|
||||
<a t-att-title="line.date"
|
||||
role="button"
|
||||
class="oe_form_field btn btn-link open_account_move"
|
||||
t-on-click="() => this.openMove(line.move_id)"
|
||||
style="margin-right: 5px; text-overflow: ellipsis; overflow: hidden; white-space: nowrap; padding-left: 0px; width:100%; text-align:left;"
|
||||
data-bs-toggle="tooltip"
|
||||
t-att-payment-id="account_payment_id"
|
||||
t-esc="line.journal_name"/>
|
||||
</td>
|
||||
</t>
|
||||
<t t-if="!outstanding">
|
||||
<td>
|
||||
<a role="button" tabindex="0" class="js_payment_info fa fa-info-circle" t-att-index="line_index" style="margin-right:5px;" aria-label="Info" title="Journal Entry Info" data-bs-toggle="tooltip" t-on-click.stop="(ev) => this.onInfoClick(ev, line_index)"></a>
|
||||
</td>
|
||||
<td t-if="!line.is_exchange">
|
||||
<i class="o_field_widget text-start o_payment_label">Paid on <t t-esc="line.date"></t></i>
|
||||
</td>
|
||||
<td t-if="line.is_exchange" colspan="2">
|
||||
<i class="o_field_widget text-start text-muted text-start">
|
||||
<span class="oe_form_field oe_form_field_float oe_form_field_monetary fw-bold">
|
||||
<t t-esc="line.amount_formatted"/>
|
||||
</span>
|
||||
<span> Exchange Difference</span>
|
||||
</i>
|
||||
</td>
|
||||
</t>
|
||||
<td t-if="!line.is_exchange" style="text-align:right;">
|
||||
<span class="oe_form_field oe_form_field_float oe_form_field_monetary" style="margin-left: -10px;">
|
||||
<t t-esc="line.amount_formatted"/>
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
</t>
|
||||
</table>
|
||||
</div>
|
||||
</t>
|
||||
|
||||
<t t-name="account.AccountPaymentPopOver" owl="1">
|
||||
<div class="account_payment_popover">
|
||||
<h3 t-if="props.title" class="o_popover_header"><t t-esc="props.title"/></h3>
|
||||
<div class="px-2">
|
||||
<div>
|
||||
<table>
|
||||
<tr>
|
||||
<td><strong>Amount: </strong></td>
|
||||
<td>
|
||||
<t t-esc="props.amount_company_currency"></t>
|
||||
<t t-if="props.amount_foreign_currency">
|
||||
(<span class="fa fa-money"/> <t t-esc="props.amount_foreign_currency"/>)
|
||||
</t>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><strong>Memo: </strong></td>
|
||||
<td>
|
||||
<div style="width: 200px; word-wrap: break-word">
|
||||
<t t-esc="props.ref"/>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><strong>Date: </strong></td>
|
||||
<td><t t-esc="props.date"/></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><strong>Journal: </strong></td>
|
||||
<td><t t-esc="props.journal_name"/><span t-if="props.payment_method_name"> (<t t-esc="props.payment_method_name"/>)</span></td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
<button class="btn btn-sm btn-primary js_unreconcile_payment float-start" t-if="!props.is_exchange" style="margin-top:5px; margin-bottom:5px;" groups="account.group_account_invoice" t-on-click="() => props._onRemoveMoveReconcile(props.move_id, props.partial_id)">Unreconcile</button>
|
||||
<button class="btn btn-sm btn-secondary js_open_payment float-end" style="margin-top:5px; margin-bottom:5px;" t-on-click="() => props._onOpenMove(props.move_id)">View</button>
|
||||
</div>
|
||||
</div>
|
||||
</t>
|
||||
|
||||
</templates>
|
||||
|
|
@ -0,0 +1,93 @@
|
|||
/** @odoo-module **/
|
||||
|
||||
import { registry } from "@web/core/registry";
|
||||
import { usePopover } from "@web/core/popover/popover_hook";
|
||||
import { useService } from "@web/core/utils/hooks";
|
||||
import { localization } from "@web/core/l10n/localization";
|
||||
import { parseDate, formatDate } from "@web/core/l10n/dates";
|
||||
|
||||
import { formatMonetary } from "@web/views/fields/formatters";
|
||||
|
||||
const { Component, onWillUpdateProps } = owl;
|
||||
|
||||
class AccountPaymentPopOver extends Component {}
|
||||
AccountPaymentPopOver.template = "account.AccountPaymentPopOver";
|
||||
|
||||
export class AccountPaymentField extends Component {
|
||||
setup() {
|
||||
this.popover = usePopover();
|
||||
this.orm = useService("orm");
|
||||
this.action = useService("action");
|
||||
|
||||
this.formatData(this.props);
|
||||
onWillUpdateProps((nextProps) => this.formatData(nextProps));
|
||||
}
|
||||
|
||||
formatData(props) {
|
||||
const info = props.value || {
|
||||
content: [],
|
||||
outstanding: false,
|
||||
title: "",
|
||||
move_id: this.props.record.data.id,
|
||||
};
|
||||
for (let [key, value] of Object.entries(info.content)) {
|
||||
value.index = key;
|
||||
value.amount_formatted = formatMonetary(value.amount, { currencyId: value.currency_id });
|
||||
if (value.date) {
|
||||
// value.date is a string, parse to date and format to the users date format
|
||||
value.date = formatDate(parseDate(value.date));
|
||||
}
|
||||
}
|
||||
this.lines = info.content;
|
||||
this.outstanding = info.outstanding;
|
||||
this.title = info.title;
|
||||
this.move_id = info.move_id;
|
||||
}
|
||||
|
||||
onInfoClick(ev, idx) {
|
||||
if (this.popoverCloseFn) {
|
||||
this.closePopover();
|
||||
}
|
||||
this.popoverCloseFn = this.popover.add(
|
||||
ev.currentTarget,
|
||||
AccountPaymentPopOver,
|
||||
{
|
||||
title: this.env._t("Journal Entry Info"),
|
||||
...this.lines[idx],
|
||||
_onRemoveMoveReconcile: this.removeMoveReconcile.bind(this),
|
||||
_onOpenMove: this.openMove.bind(this),
|
||||
onClose: this.closePopover,
|
||||
},
|
||||
{
|
||||
position: localization.direction === "rtl" ? "bottom" : "left",
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
closePopover() {
|
||||
this.popoverCloseFn();
|
||||
this.popoverCloseFn = null;
|
||||
}
|
||||
|
||||
async assignOutstandingCredit(id) {
|
||||
await this.orm.call(this.props.record.resModel, 'js_assign_outstanding_line', [this.move_id, id], {});
|
||||
await this.props.record.model.root.load();
|
||||
this.props.record.model.notify();
|
||||
}
|
||||
|
||||
async removeMoveReconcile(moveId, partialId) {
|
||||
this.closePopover();
|
||||
await this.orm.call(this.props.record.resModel, 'js_remove_outstanding_partial', [moveId, partialId], {});
|
||||
await this.props.record.model.root.load();
|
||||
this.props.record.model.notify();
|
||||
}
|
||||
|
||||
async openMove(moveId) {
|
||||
const action = await this.orm.call(this.props.record.resModel, 'action_open_business_doc', [moveId], {});
|
||||
this.action.doAction(action);
|
||||
}
|
||||
}
|
||||
AccountPaymentField.template = "account.AccountPaymentField";
|
||||
AccountPaymentField.supportedTypes = ["char"];
|
||||
|
||||
registry.category("fields").add("payment", AccountPaymentField);
|
||||
|
|
@ -0,0 +1,27 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<templates>
|
||||
|
||||
<t t-name="account.ResequenceRenderer" owl="1" >
|
||||
<table t-if="data.changeLines.length" class="table table-sm">
|
||||
<thead><tr>
|
||||
<th>Date</th>
|
||||
<th>Before</th>
|
||||
<th>After</th>
|
||||
</tr></thead>
|
||||
<tbody>
|
||||
<t t-foreach="data.changeLines" t-as="changeLine" t-key="changeLine.id">
|
||||
<ChangeLine changeLine="changeLine" ordering="data.ordering"/>
|
||||
</t>
|
||||
</tbody>
|
||||
</table>
|
||||
</t>
|
||||
|
||||
<t t-name="account.ResequenceChangeLine" owl="1">
|
||||
<tr>
|
||||
<td t-esc="props.changeLine.date"/>
|
||||
<td t-esc="props.changeLine.current_name"/>
|
||||
<td t-if="props.ordering == 'keep'" t-esc="props.changeLine.new_by_name" t-attf-class="{{ props.changeLine.new_by_name != props.changeLine.new_by_date ? 'animate' : ''}}"/>
|
||||
<td t-else="" t-esc="props.changeLine.new_by_date" t-attf-class="{{ props.changeLine.new_by_name != props.changeLine.new_by_date ? 'animate' : ''}}"/>
|
||||
</tr>
|
||||
</t>
|
||||
</templates>
|
||||
|
|
@ -0,0 +1,24 @@
|
|||
/** @odoo-module */
|
||||
|
||||
import { registry } from "@web/core/registry";
|
||||
|
||||
const { Component, onWillUpdateProps } = owl;
|
||||
|
||||
class ChangeLine extends Component {}
|
||||
ChangeLine.template = "account.ResequenceChangeLine";
|
||||
ChangeLine.props = ["changeLine", "ordering"];
|
||||
|
||||
class ShowResequenceRenderer extends Component {
|
||||
setup() {
|
||||
this.formatData(this.props);
|
||||
onWillUpdateProps((nextProps) => this.formatData(nextProps));
|
||||
}
|
||||
|
||||
formatData(props) {
|
||||
this.data = props.value ? JSON.parse(props.value) : { changeLines: [], ordering: "date" };
|
||||
}
|
||||
}
|
||||
ShowResequenceRenderer.template = "account.ResequenceRenderer";
|
||||
ShowResequenceRenderer.components = { ChangeLine };
|
||||
|
||||
registry.category("fields").add("account_resequence_widget", ShowResequenceRenderer);
|
||||
|
|
@ -0,0 +1,23 @@
|
|||
/** @odoo-module **/
|
||||
|
||||
import { registry } from "@web/core/registry";
|
||||
import { SelectionField } from "@web/views/fields/selection/selection_field";
|
||||
|
||||
export class AccountTypeSelection extends SelectionField {
|
||||
get hierarchyOptions() {
|
||||
const opts = this.options;
|
||||
return [
|
||||
{ name: this.env._t('Balance Sheet') },
|
||||
{ name: this.env._t('Assets'), children: opts.filter(x => x[0] && x[0].startsWith('asset')) },
|
||||
{ name: this.env._t('Liabilities'), children: opts.filter(x => x[0] && x[0].startsWith('liability')) },
|
||||
{ name: this.env._t('Equity'), children: opts.filter(x => x[0] && x[0].startsWith('equity')) },
|
||||
{ name: this.env._t('Profit & Loss') },
|
||||
{ name: this.env._t('Income'), children: opts.filter(x => x[0] && x[0].startsWith('income')) },
|
||||
{ name: this.env._t('Expense'), children: opts.filter(x => x[0] && x[0].startsWith('expense')) },
|
||||
{ name: this.env._t('Other'), children: opts.filter(x => x[0] && x[0] === 'off_balance') },
|
||||
];
|
||||
}
|
||||
}
|
||||
AccountTypeSelection.template = "account.AccountTypeSelection";
|
||||
|
||||
registry.category("fields").add("account_type_selection", AccountTypeSelection);
|
||||
|
|
@ -0,0 +1,22 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
|
||||
<templates>
|
||||
|
||||
<t t-name="account.AccountTypeSelection" t-inherit="web.SelectionField" t-inherit-mode="primary" owl="1">
|
||||
<xpath expr="//t[@t-foreach='options']" position="replace">
|
||||
<t t-foreach="hierarchyOptions" t-as="group" t-key="group_index">
|
||||
<optgroup t-att-label="group.name">
|
||||
<t t-if="!!group.children">
|
||||
<t t-foreach="group.children" t-as="child" t-key="child[0]">
|
||||
<option
|
||||
t-att-selected="child[0] === value"
|
||||
t-att-value="stringify(child[0])"
|
||||
t-esc="child[1]"/>
|
||||
</t>
|
||||
</t>
|
||||
</optgroup>
|
||||
</t>
|
||||
</xpath>
|
||||
</t>
|
||||
|
||||
</templates>
|
||||
|
|
@ -0,0 +1,49 @@
|
|||
/** @odoo-module **/
|
||||
|
||||
import { registry } from "@web/core/registry";
|
||||
import { Many2ManyTagsField } from "@web/views/fields/many2many_tags/many2many_tags_field";
|
||||
|
||||
const { onWillUpdateProps } = owl;
|
||||
|
||||
export class AutosaveMany2ManyTagsField extends Many2ManyTagsField {
|
||||
setup() {
|
||||
super.setup();
|
||||
|
||||
onWillUpdateProps((nextProps) => this.willUpdateProps(nextProps));
|
||||
|
||||
this.lastBalance = this.props.record.data.balance;
|
||||
this.lastAccount = this.props.record.data.account_id;
|
||||
this.lastPartner = this.props.record.data.partner_id;
|
||||
|
||||
const super_update = this.update;
|
||||
this.update = (recordlist) => {
|
||||
super_update(recordlist);
|
||||
this._saveOnUpdate();
|
||||
};
|
||||
}
|
||||
|
||||
deleteTag(id) {
|
||||
super.deleteTag(id);
|
||||
this._saveOnUpdate();
|
||||
}
|
||||
|
||||
willUpdateProps(nextProps) {
|
||||
const line = this.props.record.data;
|
||||
if (line.tax_ids.records.length > 0) {
|
||||
if (line.balance !== this.lastBalance
|
||||
|| line.account_id[0] !== this.lastAccount[0]
|
||||
|| line.partner_id[0] !== this.lastPartner[0]) {
|
||||
this.lastBalance = line.balance;
|
||||
this.lastAccount = line.account_id;
|
||||
this.lastPartner = line.partner_id;
|
||||
this._saveOnUpdate();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async _saveOnUpdate() {
|
||||
await this.props.record.model.root.save({ stayInEdition: true });
|
||||
}
|
||||
}
|
||||
|
||||
registry.category("fields").add("autosave_many2many_tags", AutosaveMany2ManyTagsField);
|
||||
|
|
@ -0,0 +1,229 @@
|
|||
/** @odoo-module **/
|
||||
|
||||
import { _lt } from "@web/core/l10n/translation";
|
||||
import { registry } from "@web/core/registry";
|
||||
import { useService } from "@web/core/utils/hooks";
|
||||
import { listView } from "@web/views/list/list_view";
|
||||
import { ListRenderer } from "@web/views/list/list_renderer";
|
||||
import { ListController } from "@web/views/list/list_controller";
|
||||
import { kanbanView } from "@web/views/kanban/kanban_view";
|
||||
import { KanbanRenderer } from "@web/views/kanban/kanban_renderer";
|
||||
import { KanbanController } from "@web/views/kanban/kanban_controller";
|
||||
import { KanbanDropdownMenuWrapper } from "@web/views/kanban/kanban_dropdown_menu_wrapper";
|
||||
import { KanbanRecord } from "@web/views/kanban/kanban_record";
|
||||
import { FileUploader } from "@web/views/fields/file_handler";
|
||||
import { standardWidgetProps } from "@web/views/widgets/standard_widget_props";
|
||||
|
||||
import { Component, useState } from "@odoo/owl";
|
||||
|
||||
export class AccountFileUploader extends Component {
|
||||
setup() {
|
||||
this.orm = useService("orm");
|
||||
this.action = useService("action");
|
||||
this.notification = useService("notification");
|
||||
this.attachmentIdsToProcess = [];
|
||||
const rec = this.props.record ? this.props.record.data : false;
|
||||
this.extraContext = rec ? {
|
||||
default_journal_id: rec.id,
|
||||
default_move_type: (rec.type === 'sale' && 'out_invoice') || (rec.type === 'purchase' && 'in_invoice') || 'entry',
|
||||
} : this.props.extraContext || {}; //TODO remove this.props.extraContext
|
||||
}
|
||||
|
||||
async onFileUploaded(file) {
|
||||
const att_data = {
|
||||
name: file.name,
|
||||
mimetype: file.type,
|
||||
datas: file.data,
|
||||
};
|
||||
const att_id = await this.orm.create("ir.attachment", [att_data], {
|
||||
context: { ...this.extraContext, ...this.env.searchModel.context },
|
||||
});
|
||||
this.attachmentIdsToProcess.push(att_id);
|
||||
}
|
||||
|
||||
async onUploadComplete() {
|
||||
let action;
|
||||
try {
|
||||
action = await this.orm.call(
|
||||
"account.journal",
|
||||
"create_document_from_attachment",
|
||||
["", this.attachmentIdsToProcess],
|
||||
{ context: { ...this.extraContext, ...this.env.searchModel.context } },
|
||||
);
|
||||
} finally {
|
||||
// ensures attachments are cleared on success as well as on error
|
||||
this.attachmentIdsToProcess = [];
|
||||
}
|
||||
if (action.context && action.context.notifications) {
|
||||
for (let [file, msg] of Object.entries(action.context.notifications)) {
|
||||
this.notification.add(
|
||||
msg,
|
||||
{
|
||||
title: file,
|
||||
type: "info",
|
||||
sticky: true,
|
||||
});
|
||||
}
|
||||
delete action.context.notifications;
|
||||
}
|
||||
this.action.doAction(action);
|
||||
}
|
||||
}
|
||||
AccountFileUploader.components = {
|
||||
FileUploader,
|
||||
};
|
||||
AccountFileUploader.template = "account.AccountFileUploader";
|
||||
AccountFileUploader.extractProps = ({ attrs }) => ({
|
||||
togglerTemplate: attrs.template || "account.JournalUploadLink",
|
||||
btnClass: attrs.btnClass || "",
|
||||
linkText: attrs.linkText || attrs.title || _lt("Upload"), //TODO: remove linkText attr in master (not translatable)
|
||||
});
|
||||
AccountFileUploader.props = {
|
||||
...standardWidgetProps,
|
||||
record: { type: Object, optional: true},
|
||||
togglerTemplate: { type: String, optional: true },
|
||||
btnClass: { type: String, optional: true },
|
||||
linkText: { type: String, optional: true },
|
||||
slots: { type: Object, optional: true },
|
||||
extraContext: { type: Object, optional: true }, //this prop is only for stable databases with the old journal dashboard view, it should be deleted in master as it is not used
|
||||
}
|
||||
//when file uploader is used on account.journal (with a record)
|
||||
AccountFileUploader.fieldDependencies = {
|
||||
id: { type: "integer" },
|
||||
type: { type: "selection" },
|
||||
};
|
||||
|
||||
registry.category("view_widgets").add("account_file_uploader", AccountFileUploader);
|
||||
|
||||
export class AccountDropZone extends Component {
|
||||
setup() {
|
||||
this.notificationService = useService("notification");
|
||||
}
|
||||
|
||||
onDrop(ev) {
|
||||
const selector = '.account_file_uploader.o_input_file.o_hidden';
|
||||
// look for the closest uploader Input as it may have a context
|
||||
let uploadInput = ev.target.closest('.o_drop_area').parentElement.querySelector(selector) || document.querySelector(selector);
|
||||
let files = ev.dataTransfer ? ev.dataTransfer.files : false;
|
||||
if (uploadInput && !!files) {
|
||||
uploadInput.files = ev.dataTransfer.files;
|
||||
uploadInput.dispatchEvent(new Event("change"));
|
||||
} else {
|
||||
this.notificationService.add(
|
||||
this.env._t("Could not upload files"),
|
||||
{
|
||||
type: "danger",
|
||||
});
|
||||
}
|
||||
this.props.hideZone();
|
||||
}
|
||||
}
|
||||
AccountDropZone.defaultProps = {
|
||||
hideZone: () => {},
|
||||
};
|
||||
AccountDropZone.template = "account.DropZone";
|
||||
|
||||
// Account Move List View
|
||||
export class AccountMoveUploadListRenderer extends ListRenderer {
|
||||
setup() {
|
||||
super.setup();
|
||||
this.state.dropzoneVisible = false;
|
||||
}
|
||||
}
|
||||
AccountMoveUploadListRenderer.template = "account.ListRenderer";
|
||||
AccountMoveUploadListRenderer.components = {
|
||||
...ListRenderer.components,
|
||||
AccountDropZone,
|
||||
};
|
||||
|
||||
export class AccountMoveListController extends ListController {
|
||||
setup() {
|
||||
super.setup();
|
||||
this.account_move_service = useService("account_move");
|
||||
}
|
||||
|
||||
async onDeleteSelectedRecords() {
|
||||
const selectedResIds = await this.getSelectedResIds();
|
||||
if (this.props.resModel !== "account.move" || !await this.account_move_service.addDeletionDialog(this, selectedResIds)) {
|
||||
return super.onDeleteSelectedRecords(...arguments);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
AccountMoveListController.components = {
|
||||
...ListController.components,
|
||||
AccountFileUploader,
|
||||
};
|
||||
|
||||
export const AccountMoveUploadListView = {
|
||||
...listView,
|
||||
Controller: AccountMoveListController,
|
||||
Renderer: AccountMoveUploadListRenderer,
|
||||
buttonTemplate: "account.ListView.Buttons",
|
||||
};
|
||||
|
||||
// Account Move Kanban View
|
||||
export class AccountMoveUploadKanbanRenderer extends KanbanRenderer {
|
||||
setup() {
|
||||
super.setup();
|
||||
this.state.dropzoneVisible = false;
|
||||
}
|
||||
}
|
||||
AccountMoveUploadKanbanRenderer.template = "account.KanbanRenderer";
|
||||
AccountMoveUploadKanbanRenderer.components = {
|
||||
...KanbanRenderer.components,
|
||||
AccountDropZone,
|
||||
};
|
||||
|
||||
export class AccountMoveUploadKanbanController extends KanbanController {}
|
||||
AccountMoveUploadKanbanController.components = {
|
||||
...KanbanController.components,
|
||||
AccountFileUploader,
|
||||
};
|
||||
|
||||
export const AccountMoveUploadKanbanView = {
|
||||
...kanbanView,
|
||||
Controller: AccountMoveUploadKanbanController,
|
||||
Renderer: AccountMoveUploadKanbanRenderer,
|
||||
buttonTemplate: "account.KanbanView.Buttons",
|
||||
};
|
||||
|
||||
// Accounting Dashboard
|
||||
export class DashboardKanbanDropdownMenuWrapper extends KanbanDropdownMenuWrapper {
|
||||
onClick(ev) {
|
||||
// Keep the dropdown open as we need the fileuploader to remain in the dom
|
||||
if (!ev.target.tagName === "INPUT" && !ev.target.closest('.file_upload_kanban_action_a')) {
|
||||
super.onClick(ev);
|
||||
}
|
||||
}
|
||||
}
|
||||
export class DashboardKanbanRecord extends KanbanRecord {
|
||||
setup() {
|
||||
super.setup();
|
||||
this.state = useState({
|
||||
dropzoneVisible: false,
|
||||
});
|
||||
}
|
||||
}
|
||||
DashboardKanbanRecord.components = {
|
||||
...DashboardKanbanRecord.components,
|
||||
AccountDropZone,
|
||||
AccountFileUploader,
|
||||
KanbanDropdownMenuWrapper: DashboardKanbanDropdownMenuWrapper,
|
||||
};
|
||||
DashboardKanbanRecord.template = "account.DashboardKanbanRecord";
|
||||
|
||||
export class DashboardKanbanRenderer extends KanbanRenderer {}
|
||||
DashboardKanbanRenderer.components = {
|
||||
...KanbanRenderer.components,
|
||||
KanbanRecord: DashboardKanbanRecord,
|
||||
};
|
||||
|
||||
export const DashboardKanbanView = {
|
||||
...kanbanView,
|
||||
Renderer: DashboardKanbanRenderer,
|
||||
};
|
||||
|
||||
registry.category("views").add("account_tree", AccountMoveUploadListView);
|
||||
registry.category("views").add("account_documents_kanban", AccountMoveUploadKanbanView);
|
||||
registry.category("views").add("account_dashboard_kanban", DashboardKanbanView);
|
||||
|
|
@ -0,0 +1,27 @@
|
|||
.o_drop_area {
|
||||
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%;
|
||||
}
|
||||
}
|
||||
|
||||
.file_upload_kanban_action_a {
|
||||
@include o-kanban-dashboard-dropdown-link($link-padding-gap: $o-kanban-dashboard-dropdown-complex-gap);
|
||||
}
|
||||
|
||||
.o_widget_account_file_uploader {
|
||||
.btn-primary.oe_kanban_action_button {
|
||||
a {
|
||||
color: $white;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,102 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
|
||||
<templates>
|
||||
<t t-name="account.DropZone" owl="1">
|
||||
<div t-if="props.visible"
|
||||
t-attf-class="o_drop_area"
|
||||
t-on-dragover.prevent="()=>{}"
|
||||
t-on-dragleave="props.hideZone"
|
||||
t-on-drop.prevent="onDrop">
|
||||
<i class="fa fa-upload fa-10x"></i>
|
||||
</div>
|
||||
</t>
|
||||
|
||||
<t t-name="account.AccountFileUploader" owl="1">
|
||||
<div t-att-class="props.record and props.record.data ? 'oe_kanban_color_' + props.record.data.color : ''">
|
||||
<FileUploader
|
||||
acceptedFileExtensions="props.acceptedFileExtensions"
|
||||
fileUploadClass="'account_file_uploader'"
|
||||
multiUpload="true"
|
||||
onUploaded.bind="onFileUploaded"
|
||||
onUploadComplete.bind="onUploadComplete">
|
||||
<t t-set-slot="toggler">
|
||||
<t t-if="props.togglerTemplate" t-call="{{ props.togglerTemplate }}"/>
|
||||
<t t-else="" t-slot="default"/>
|
||||
</t>
|
||||
<t t-slot="extra"/>
|
||||
</FileUploader>
|
||||
</div>
|
||||
</t>
|
||||
|
||||
<t t-name="account.ListRenderer" t-inherit="web.ListRenderer" t-inherit-mode="primary" owl="1">
|
||||
<xpath expr="//div[@t-ref='root']" position="before">
|
||||
<AccountDropZone
|
||||
visible="state.dropzoneVisible"
|
||||
hideZone="() => state.dropzoneVisible = false"/>
|
||||
</xpath>
|
||||
<xpath expr="//div[@t-ref='root']" position="attributes">
|
||||
<attribute name="t-on-dragenter.stop.prevent">() => state.dropzoneVisible = true</attribute>
|
||||
</xpath>
|
||||
</t>
|
||||
|
||||
<t t-name="account.KanbanRenderer" t-inherit="web.KanbanRenderer" t-inherit-mode="primary" owl="1">
|
||||
<xpath expr="//div[@t-ref='root']" position="before">
|
||||
<AccountDropZone
|
||||
visible="state.dropzoneVisible"
|
||||
hideZone="() => state.dropzoneVisible = false"/>
|
||||
</xpath>
|
||||
<xpath expr="//div[@t-ref='root']" position="attributes">
|
||||
<attribute name="t-on-dragenter.stop.prevent">() => state.dropzoneVisible = true</attribute>
|
||||
</xpath>
|
||||
</t>
|
||||
|
||||
<t t-name="account.ListView.Buttons" t-inherit="web.ListView.Buttons" t-inherit-mode="primary" owl="1">
|
||||
<xpath expr="//*[@class='btn btn-primary o_list_button_add']" position="after">
|
||||
<t t-call="account.AccountViewUploadButton"/>
|
||||
</xpath>
|
||||
</t>
|
||||
|
||||
<t t-name="account.KanbanView.Buttons" t-inherit="web.KanbanView.Buttons" t-inherit-mode="primary" owl="1">
|
||||
<xpath expr="//*[@class='btn btn-primary o-kanban-button-new']" position="after">
|
||||
<t t-call="account.AccountViewUploadButton"/>
|
||||
</xpath>
|
||||
</t>
|
||||
|
||||
<t t-name="account.AccountViewUploadButton" owl="1">
|
||||
<!-- No record is available so rely on the action context to contain the default_move_type -->
|
||||
<AccountFileUploader>
|
||||
<t t-set-slot="default">
|
||||
<button type="button" class="btn btn-secondary o_button_upload_bill">
|
||||
Upload
|
||||
</button>
|
||||
</t>
|
||||
</AccountFileUploader>
|
||||
</t>
|
||||
|
||||
<t t-name="account.DashboardKanbanRecord" owl="1">
|
||||
<div
|
||||
role="article"
|
||||
t-att-class="getRecordClasses()"
|
||||
t-att-data-id="props.canResequence and props.record.id"
|
||||
t-att-tabindex="props.record.model.useSampleModel ? -1 : 0"
|
||||
t-on-click="onGlobalClick"
|
||||
t-on-dragenter.stop.prevent="() => state.dropzoneVisible = true"
|
||||
t-ref="root">
|
||||
<AccountFileUploader record="props.record">
|
||||
<t t-set-slot="extra">
|
||||
<AccountDropZone
|
||||
visible="state.dropzoneVisible"
|
||||
hideZone="() => state.dropzoneVisible = false"/>
|
||||
<t t-call="{{ templates['kanban-box'] }}"/>
|
||||
</t>
|
||||
</AccountFileUploader>
|
||||
</div>
|
||||
</t>
|
||||
|
||||
<t t-name="account.JournalUploadLink" owl="1">
|
||||
<div t-att-class="props.btnClass" groups="account.group_account_invoice">
|
||||
<a href="#" t-out="props.linkText"/>
|
||||
</div>
|
||||
</t>
|
||||
|
||||
</templates>
|
||||
|
|
@ -0,0 +1,31 @@
|
|||
/** @odoo-module */
|
||||
|
||||
import { registry } from "@web/core/registry";
|
||||
|
||||
const { Component, onWillUpdateProps } = owl;
|
||||
|
||||
class ListItem extends Component {}
|
||||
ListItem.template = "account.GroupedItemTemplate";
|
||||
ListItem.props = ["item_vals", "options"];
|
||||
|
||||
class ListGroup extends Component {}
|
||||
ListGroup.template = "account.GroupedItemsTemplate";
|
||||
ListGroup.components = { ListItem };
|
||||
ListGroup.props = ["group_vals", "options"];
|
||||
|
||||
class ShowGroupedList extends Component {
|
||||
setup() {
|
||||
this.formatData(this.props);
|
||||
onWillUpdateProps((nextProps) => this.formatData(nextProps));
|
||||
}
|
||||
|
||||
formatData(props) {
|
||||
this.data = props.value
|
||||
? JSON.parse(props.value)
|
||||
: { groups_vals: [], options: { discarded_number: "", columns: [] } };
|
||||
}
|
||||
}
|
||||
ShowGroupedList.template = "account.GroupedListTemplate";
|
||||
ShowGroupedList.components = { ListGroup };
|
||||
|
||||
registry.category("fields").add("grouped_view_widget", ShowGroupedList);
|
||||
|
|
@ -0,0 +1,40 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<templates>
|
||||
<t t-name="account.GroupedListTemplate" owl="1">
|
||||
<table t-if="data.groups_vals.length" class="table table-sm o_list_table table table-sm table-hover table-striped o_list_table_grouped">
|
||||
<thead><tr>
|
||||
<t t-foreach="data.options.columns" t-as="col" t-key="col_index">
|
||||
<th t-esc="col['label']" t-attf-class="{{col['class']}}"/>
|
||||
</t>
|
||||
</tr></thead>
|
||||
<t t-foreach="data.groups_vals" t-as="group_vals" t-key="group_vals_index">
|
||||
<ListGroup group_vals="group_vals" options="data.options"/>
|
||||
</t>
|
||||
</table>
|
||||
<t t-if="data.options.discarded_number">
|
||||
<span><t t-esc="data.options.discarded_number"/> are not shown in the preview</span>
|
||||
</t>
|
||||
</t>
|
||||
|
||||
<tbody t-name="account.GroupedItemsTemplate" owl="1">
|
||||
<tr style="background-color: #dee2e6;">
|
||||
<td t-attf-colspan="{{props.options.columns.length}}">
|
||||
<t t-esc="props.group_vals.group_name"/>
|
||||
</td>
|
||||
</tr>
|
||||
<t t-foreach="props.group_vals.items_vals" t-as="item_vals" t-key="item_vals_index">
|
||||
<ListItem item_vals="item_vals[2]" options="props.options"/>
|
||||
</t>
|
||||
</tbody>
|
||||
|
||||
<tr t-name="account.GroupedItemTemplate" owl="1">
|
||||
<t t-foreach="props.options.columns" t-as="col" t-key="col_index">
|
||||
<td t-esc="props.item_vals[col['field']]" t-attf-class="{{col['class']}}"/>
|
||||
</t>
|
||||
</tr>
|
||||
|
||||
<t t-name="account.OpenMoveTemplate">
|
||||
<a href="#" t-esc="widget.value"/>
|
||||
</t>
|
||||
|
||||
</templates>
|
||||
|
|
@ -0,0 +1,48 @@
|
|||
/** @odoo-module **/
|
||||
|
||||
import { registry } from "@web/core/registry";
|
||||
import { useService } from "@web/core/utils/hooks";
|
||||
|
||||
const { Component } = owl;
|
||||
|
||||
export class JournalDashboardActivity extends Component {
|
||||
setup() {
|
||||
this.action = useService("action");
|
||||
this.MAX_ACTIVITY_DISPLAY = 5;
|
||||
this.formatData(this.props);
|
||||
}
|
||||
|
||||
formatData(props) {
|
||||
this.info = JSON.parse(this.props.value);
|
||||
this.info.more_activities = false;
|
||||
if (this.info.activities.length > this.MAX_ACTIVITY_DISPLAY) {
|
||||
this.info.more_activities = true;
|
||||
this.info.activities = this.info.activities.slice(0, this.MAX_ACTIVITY_DISPLAY);
|
||||
}
|
||||
}
|
||||
|
||||
async openActivity(activity) {
|
||||
this.action.doAction({
|
||||
type: 'ir.actions.act_window',
|
||||
name: this.env._t('Journal Entry'),
|
||||
target: 'current',
|
||||
res_id: activity.res_id,
|
||||
res_model: 'account.move',
|
||||
views: [[false, 'form']],
|
||||
});
|
||||
}
|
||||
|
||||
openAllActivities(e) {
|
||||
this.action.doAction({
|
||||
type: 'ir.actions.act_window',
|
||||
name: this.env._t('Journal Entries'),
|
||||
res_model: 'account.move',
|
||||
views: [[false, 'kanban'], [false, 'form']],
|
||||
search_view_id: [false],
|
||||
domain: [['journal_id', '=', this.props.record.resId], ['activity_ids', '!=', false]],
|
||||
});
|
||||
}
|
||||
}
|
||||
JournalDashboardActivity.template = "account.JournalDashboardActivity";
|
||||
|
||||
registry.category("fields").add("kanban_vat_activity", JournalDashboardActivity);
|
||||
|
|
@ -0,0 +1,24 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
|
||||
<templates>
|
||||
|
||||
<t t-name="account.JournalDashboardActivity" owl="1">
|
||||
<t t-foreach="info.activities" t-as="activity" t-key="activity_index">
|
||||
<div class="row">
|
||||
<div class="col-8 o_mail_activity">
|
||||
<a href="#"
|
||||
t-att-class="(activity.status == 'late' ? 'o_activity_color_overdue ' : ' ') + (activity.activity_category == 'tax_report' ? 'o_open_vat_report' : 'see_activity')"
|
||||
t-att-data-res-id="activity.res_id" t-att-data-id="activity.id" t-att-data-model="activity.res_model"
|
||||
t-on-click.stop.prevent="() => this.openActivity(activity)">
|
||||
<t t-esc="activity.name"/>
|
||||
</a>
|
||||
</div>
|
||||
<div class="col-4 text-end">
|
||||
<span><t t-esc="activity.date"/></span>
|
||||
</div>
|
||||
</div>
|
||||
</t>
|
||||
<a t-if="info.more_activities" class="float-end see_all_activities" href="#" t-on-click.stop.prevent="(ev) => this.openAllActivities(ev)">See all activities</a>
|
||||
</t>
|
||||
|
||||
</templates>
|
||||
|
|
@ -0,0 +1,17 @@
|
|||
/** @odoo-module **/
|
||||
|
||||
import { registry } from "@web/core/registry";
|
||||
import { Many2OneField } from "@web/views/fields/many2one/many2one_field";
|
||||
|
||||
class LineOpenMoveWidget extends Many2OneField {
|
||||
async openAction() {
|
||||
this.action.doActionButton({
|
||||
type: "object",
|
||||
resId: this.props.value[0],
|
||||
name: "action_open_business_doc",
|
||||
resModel: "account.move.line",
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
registry.category("fields").add("line_open_move_widget", LineOpenMoveWidget);
|
||||
|
|
@ -0,0 +1,25 @@
|
|||
/** @odoo-module **/
|
||||
|
||||
import { registry } from "@web/core/registry";
|
||||
import { useService } from "@web/core/utils/hooks";
|
||||
|
||||
const { Component } = owl;
|
||||
|
||||
class OpenMoveWidget extends Component {
|
||||
setup() {
|
||||
super.setup();
|
||||
this.action = useService("action");
|
||||
}
|
||||
|
||||
async openMove(ev) {
|
||||
this.action.doActionButton({
|
||||
type: "object",
|
||||
resId: this.props.record.resId,
|
||||
name: "action_open_business_doc",
|
||||
resModel: "account.move.line",
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
OpenMoveWidget.template = "account.OpenMoveWidget";
|
||||
registry.category("fields").add("open_move_widget", OpenMoveWidget);
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<templates>
|
||||
|
||||
<t t-name="account.OpenMoveWidget" owl="1">
|
||||
<a href="#" t-esc="props.value" t-on-click.prevent.stop="(ev) => this.openMove()"/>
|
||||
</t>
|
||||
|
||||
</templates>
|
||||
|
|
@ -0,0 +1,35 @@
|
|||
|
||||
// The goal of this file is to contain CSS hacks related to allowing
|
||||
// section and note on sale order and invoice.
|
||||
|
||||
table.o_section_and_note_list_view tr.o_data_row.o_is_line_note,
|
||||
table.o_section_and_note_list_view tr.o_data_row.o_is_line_note textarea[name="name"],
|
||||
div.oe_kanban_card.o_is_line_note {
|
||||
font-style: italic;
|
||||
}
|
||||
table.o_section_and_note_list_view tr.o_data_row.o_is_line_section,
|
||||
div.oe_kanban_card.o_is_line_section {
|
||||
font-weight: bold;
|
||||
background-color: #DDDDDD;
|
||||
}
|
||||
table.o_section_and_note_list_view tr.o_data_row.o_is_line_section {
|
||||
border-top: 1px solid #BBB;
|
||||
border-bottom: 1px solid #BBB;
|
||||
}
|
||||
|
||||
table.o_section_and_note_list_view tr.o_data_row {
|
||||
&.o_is_line_note,
|
||||
&.o_is_line_section {
|
||||
td {
|
||||
// There is an undeterministic CSS behaviour in Chrome related to
|
||||
// the combination of the row's and its children's borders.
|
||||
border: none !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.o_field_section_and_note_text {
|
||||
> span {
|
||||
white-space: pre-wrap !important;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,98 @@
|
|||
/** @odoo-module **/
|
||||
|
||||
import { registry } from "@web/core/registry";
|
||||
import { ListRenderer } from "@web/views/list/list_renderer";
|
||||
import { X2ManyField } from "@web/views/fields/x2many/x2many_field";
|
||||
import { TextField, ListTextField } from "@web/views/fields/text/text_field";
|
||||
import { CharField } from "@web/views/fields/char/char_field";
|
||||
|
||||
const { Component, useEffect } = owl;
|
||||
|
||||
export class SectionAndNoteListRenderer extends ListRenderer {
|
||||
/**
|
||||
* The purpose of this extension is to allow sections and notes in the one2many list
|
||||
* primarily used on Sales Orders and Invoices
|
||||
*
|
||||
* @override
|
||||
*/
|
||||
setup() {
|
||||
super.setup();
|
||||
this.titleField = "name";
|
||||
useEffect(
|
||||
() => this.focusToName(this.props.list.editedRecord),
|
||||
() => [this.props.list.editedRecord]
|
||||
)
|
||||
}
|
||||
|
||||
focusToName(editRec) {
|
||||
if (editRec && editRec.isVirtual && this.isSectionOrNote(editRec)) {
|
||||
const col = this.state.columns.find((c) => c.name === this.titleField);
|
||||
this.focusCell(col, null);
|
||||
}
|
||||
}
|
||||
|
||||
isSectionOrNote(record=null) {
|
||||
record = record || this.record;
|
||||
return ['line_section', 'line_note'].includes(record.data.display_type);
|
||||
}
|
||||
|
||||
getRowClass(record) {
|
||||
const existingClasses = super.getRowClass(record);
|
||||
return `${existingClasses} o_is_${record.data.display_type}`;
|
||||
}
|
||||
|
||||
getCellClass(column, record) {
|
||||
const classNames = super.getCellClass(column, record);
|
||||
if (this.isSectionOrNote(record) && column.widget !== "handle" && column.name !== this.titleField) {
|
||||
return `${classNames} o_hidden`;
|
||||
}
|
||||
return classNames;
|
||||
}
|
||||
|
||||
getColumns(record) {
|
||||
const columns = super.getColumns(record);
|
||||
if (this.isSectionOrNote(record)) {
|
||||
return this.getSectionColumns(columns);
|
||||
}
|
||||
return columns;
|
||||
}
|
||||
|
||||
getSectionColumns(columns) {
|
||||
const sectionCols = columns.filter((col) => col.widget === "handle" || col.type === "field" && col.name === this.titleField);
|
||||
return sectionCols.map((col) => {
|
||||
if (col.name === this.titleField) {
|
||||
return { ...col, colspan: columns.length - sectionCols.length + 1 };
|
||||
} else {
|
||||
return { ...col };
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
SectionAndNoteListRenderer.template = "account.sectionAndNoteListRenderer";
|
||||
|
||||
export class SectionAndNoteFieldOne2Many extends X2ManyField {}
|
||||
SectionAndNoteFieldOne2Many.additionalClasses = ['o_field_one2many'];
|
||||
SectionAndNoteFieldOne2Many.components = {
|
||||
...X2ManyField.components,
|
||||
ListRenderer: SectionAndNoteListRenderer,
|
||||
};
|
||||
|
||||
export class SectionAndNoteText extends Component {
|
||||
get componentToUse() {
|
||||
return this.props.record.data.display_type === 'line_section' ? CharField : TextField;
|
||||
}
|
||||
}
|
||||
SectionAndNoteText.template = "account.SectionAndNoteText";
|
||||
SectionAndNoteText.additionalClasses = ["o_field_text"];
|
||||
|
||||
export class ListSectionAndNoteText extends SectionAndNoteText {
|
||||
get componentToUse() {
|
||||
return this.props.record.data.display_type !== "line_section"
|
||||
? ListTextField
|
||||
: super.componentToUse;
|
||||
}
|
||||
}
|
||||
|
||||
registry.category("fields").add("section_and_note_one2many", SectionAndNoteFieldOne2Many);
|
||||
registry.category("fields").add("section_and_note_text", SectionAndNoteText);
|
||||
registry.category("fields").add("list.section_and_note_text", ListSectionAndNoteText);
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<templates>
|
||||
<t t-name="account.sectionAndNoteListRenderer" t-inherit="web.ListRenderer" t-inherit-mode="primary" owl="1">
|
||||
<xpath expr="//table" position="attributes">
|
||||
<attribute name="class" position="add" separator=" ">o_section_and_note_list_view</attribute>
|
||||
</xpath>
|
||||
</t>
|
||||
|
||||
<t t-name="account.SectionAndNoteText" owl="1">
|
||||
<t t-component="componentToUse" t-props="props"/>
|
||||
</t>
|
||||
|
||||
</templates>
|
||||
|
|
@ -0,0 +1,33 @@
|
|||
/** @odoo-module */
|
||||
|
||||
import { registry } from "@web/core/registry";
|
||||
import { useService } from "@web/core/utils/hooks";
|
||||
|
||||
import { ResConfigDevTool } from "@web/webclient/settings_form_view/widgets/res_config_dev_tool";
|
||||
|
||||
/**
|
||||
* Override of the widget in the settings that handles the "Developer Tools" section.
|
||||
* Provides a button to download XSD files for XML validation.
|
||||
*/
|
||||
class ResConfigDevToolDownloadXsd extends ResConfigDevTool {
|
||||
/**
|
||||
* Downloads every XSD file, based on installed localisations.
|
||||
*/
|
||||
setup() {
|
||||
super.setup();
|
||||
this.rpc = useService("rpc");
|
||||
}
|
||||
|
||||
async onClickDownloadXSD() {
|
||||
await this.rpc("/web/dataset/call_kw/ir.attachment/action_download_xsd_files", {
|
||||
model: 'ir.attachment',
|
||||
method: 'action_download_xsd_files',
|
||||
args: [],
|
||||
kwargs: {}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
ResConfigDevToolDownloadXsd.template = "res_config_dev_tool";
|
||||
|
||||
registry.category("view_widgets").add("res_config_dev_tool", ResConfigDevToolDownloadXsd, {force: true});
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<template>
|
||||
<div t-name='account.res_config_dev_tool' t-inherit="web.res_config_dev_tool" t-inherit-mode="extension" owl="1" primary="1">
|
||||
<xpath expr="//Setting/div" position="inside">
|
||||
<a t-if="isDebug" class="d-block" t-on-click.prevent="onClickDownloadXSD" href="#">Download XSD files (XML validation)</a>
|
||||
</xpath>
|
||||
</div>
|
||||
</template>
|
||||
|
|
@ -0,0 +1,24 @@
|
|||
.o_tax_group { width: 0% }
|
||||
|
||||
.o_tax_group_edit {
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.o_tax_group_edit:hover {
|
||||
color: #00A09D;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.o_tax_group_editable .o_tax_group_amount_value input {
|
||||
width: 65%;
|
||||
float: right;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.o_tax_group_editable .o_tax_group_amount_value::before {
|
||||
content: ' ';
|
||||
}
|
||||
|
||||
.o_tax_total_label{
|
||||
margin-bottom: 0 !important;
|
||||
}
|
||||
|
|
@ -0,0 +1,187 @@
|
|||
/** @odoo-module **/
|
||||
|
||||
import { formatFloat, formatMonetary } from "@web/views/fields/formatters";
|
||||
import { parseFloat } from "@web/views/fields/parsers";
|
||||
import { standardFieldProps } from "@web/views/fields/standard_field_props";
|
||||
import { registry } from "@web/core/registry";
|
||||
import { session } from "@web/session";
|
||||
import { useNumpadDecimal } from "@web/views/fields/numpad_decimal_hook";
|
||||
|
||||
const { Component, onPatched, onWillUpdateProps, useRef, useState } = owl;
|
||||
|
||||
/**
|
||||
A line of some TaxTotalsComponent, giving the values of a tax group.
|
||||
**/
|
||||
class TaxGroupComponent extends Component {
|
||||
setup() {
|
||||
this.inputTax = useRef("taxValueInput");
|
||||
this.state = useState({ value: "readonly" });
|
||||
onPatched(() => {
|
||||
if (this.state.value === "edit") {
|
||||
const { taxGroup, currency } = this.props;
|
||||
const newVal = formatFloat(taxGroup.tax_group_amount, { digits: (currency && currency.digits) });
|
||||
this.inputTax.el.value = newVal;
|
||||
this.inputTax.el.focus(); // Focus the input
|
||||
}
|
||||
});
|
||||
onWillUpdateProps(() => {
|
||||
this.setState("readonly");
|
||||
});
|
||||
useNumpadDecimal();
|
||||
}
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
// Main methods
|
||||
//--------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* The purpose of this method is to change the state of the component.
|
||||
* It can have one of the following three states:
|
||||
* - readonly: display in read-only mode of the field,
|
||||
* - edit: display with a html input field,
|
||||
* - disable: display with a html input field that is disabled.
|
||||
*
|
||||
* If a value other than one of these 3 states is passed as a parameter,
|
||||
* the component is set to readonly by default.
|
||||
*
|
||||
* @param {String} value
|
||||
*/
|
||||
setState(value) {
|
||||
if (["readonly", "edit", "disable"].includes(value)) {
|
||||
this.state.value = value;
|
||||
}
|
||||
else {
|
||||
this.state.value = "readonly";
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* This method handles the "_onChangeTaxValue" event. In this method,
|
||||
* we get the new value for the tax group, we format it and we call
|
||||
* the method to recalculate the tax lines. At the moment the method
|
||||
* is called, we disable the html input field.
|
||||
*
|
||||
* In case the value has not changed or the tax group is equal to 0,
|
||||
* the modification does not take place.
|
||||
*/
|
||||
_onChangeTaxValue() {
|
||||
this.setState("disable"); // Disable the input
|
||||
const oldValue = this.props.taxGroup.tax_group_amount;
|
||||
let newValue;
|
||||
try {
|
||||
newValue = parseFloat(this.inputTax.el.value); // Get the new value
|
||||
} catch (_err) {
|
||||
this.inputTax.el.value = oldValue;
|
||||
this.setState("edit");
|
||||
return;
|
||||
}
|
||||
// The newValue can"t be equals to 0
|
||||
if (newValue === oldValue || newValue === 0) {
|
||||
this.setState("readonly");
|
||||
return;
|
||||
}
|
||||
this.props.taxGroup.tax_group_amount = newValue;
|
||||
|
||||
this.props.onChangeTaxGroup({
|
||||
oldValue,
|
||||
newValue: newValue,
|
||||
taxGroupId: this.props.taxGroup.tax_group_id,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
TaxGroupComponent.props = {
|
||||
currency: { optional: true },
|
||||
taxGroup: { optional: true },
|
||||
onChangeTaxGroup: { optional: true },
|
||||
isReadonly: Boolean,
|
||||
invalidate: Function,
|
||||
};
|
||||
TaxGroupComponent.template = "account.TaxGroupComponent";
|
||||
|
||||
/**
|
||||
Widget used to display tax totals by tax groups for invoices, PO and SO,
|
||||
and possibly allowing editing them.
|
||||
|
||||
Note that this widget requires the object it is used on to have a
|
||||
currency_id field.
|
||||
**/
|
||||
export class TaxTotalsComponent extends Component {
|
||||
setup() {
|
||||
this.totals = {};
|
||||
this.formatData(this.props);
|
||||
onWillUpdateProps((nextProps) => {
|
||||
this.formatData(nextProps);
|
||||
});
|
||||
}
|
||||
|
||||
get readonly() {
|
||||
return this.props.readonly;
|
||||
}
|
||||
|
||||
get currencyId() {
|
||||
const recordCurrency = this.props.record.data.currency_id;
|
||||
return recordCurrency && recordCurrency[0];
|
||||
}
|
||||
|
||||
get currency() {
|
||||
return session.currencies[this.currencyId];
|
||||
}
|
||||
|
||||
invalidate() {
|
||||
return this.props.record.setInvalidField(this.props.name);
|
||||
}
|
||||
|
||||
/**
|
||||
* This method is the main function of the tax group widget.
|
||||
* It is called by the TaxGroupComponent and receives the newer tax value.
|
||||
*
|
||||
* It is responsible for triggering an event to notify the ORM of a change.
|
||||
*/
|
||||
_onChangeTaxValueByTaxGroup({ oldValue, newValue }) {
|
||||
if (oldValue === newValue) return;
|
||||
this.props.update(this.totals);
|
||||
this.totals.display_rounding = false;
|
||||
}
|
||||
|
||||
formatData(props) {
|
||||
let totals = JSON.parse(JSON.stringify(props.value));
|
||||
const currencyFmtOpts = { currencyId: props.record.data.currency_id && props.record.data.currency_id[0] };
|
||||
|
||||
let amount_untaxed = totals.amount_untaxed;
|
||||
let amount_tax = 0;
|
||||
let subtotals = [];
|
||||
for (let subtotal_title of totals.subtotals_order) {
|
||||
let amount_total = amount_untaxed + amount_tax;
|
||||
subtotals.push({
|
||||
'name': subtotal_title,
|
||||
'amount': amount_total,
|
||||
'formatted_amount': formatMonetary(amount_total, currencyFmtOpts),
|
||||
});
|
||||
let group = totals.groups_by_subtotal[subtotal_title];
|
||||
for (let i in group) {
|
||||
amount_tax = amount_tax + group[i].tax_group_amount;
|
||||
}
|
||||
}
|
||||
totals.subtotals = subtotals;
|
||||
let rounding_amount = totals.display_rounding && totals.rounding_amount || 0;
|
||||
let amount_total = amount_untaxed + amount_tax + rounding_amount;
|
||||
totals.amount_total = amount_total;
|
||||
totals.formatted_amount_total = formatMonetary(amount_total, currencyFmtOpts);
|
||||
for (let group_name of Object.keys(totals.groups_by_subtotal)) {
|
||||
let group = totals.groups_by_subtotal[group_name];
|
||||
for (let key in group) {
|
||||
group[key].formatted_tax_group_amount = formatMonetary(group[key].tax_group_amount, currencyFmtOpts);
|
||||
group[key].formatted_tax_group_base_amount = formatMonetary(group[key].tax_group_base_amount, currencyFmtOpts);
|
||||
}
|
||||
}
|
||||
this.totals = totals;
|
||||
}
|
||||
}
|
||||
TaxTotalsComponent.template = "account.TaxTotalsField";
|
||||
TaxTotalsComponent.components = { TaxGroupComponent };
|
||||
TaxTotalsComponent.props = {
|
||||
...standardFieldProps,
|
||||
};
|
||||
|
||||
registry.category("fields").add("account-tax-totals-field", TaxTotalsComponent);
|
||||
|
|
@ -0,0 +1,95 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<templates>
|
||||
|
||||
<t t-name="account.TaxGroupComponent" owl="1">
|
||||
<tr>
|
||||
<td class="o_td_label">
|
||||
<label class="o_form_label o_tax_total_label" t-esc="props.taxGroup.tax_group_name"/>
|
||||
</td>
|
||||
|
||||
<td class="o_tax_group">
|
||||
<t t-if="!props.isReadonly">
|
||||
<t t-if="['edit', 'disable'].includes(state.value)">
|
||||
<span class="o_tax_group_edit_input" t-ref="numpadDecimal">
|
||||
<input
|
||||
type="text"
|
||||
t-ref="taxValueInput"
|
||||
class="o_field_float
|
||||
o_field_number o_input"
|
||||
t-att-disabled="state.value === 'disable'"
|
||||
t-on-change.prevent="_onChangeTaxValue"
|
||||
t-on-blur="_onChangeTaxValue"/>
|
||||
</span>
|
||||
</t>
|
||||
<t t-else="">
|
||||
<span class="o_tax_group_edit" t-on-click.prevent="() => this.setState('edit')">
|
||||
<span class="o_tax_group_amount_value o_list_monetary">
|
||||
<i class="fa fa-pencil me-2"/> <t t-out="props.taxGroup.formatted_tax_group_amount"/>
|
||||
</span>
|
||||
</span>
|
||||
</t>
|
||||
</t>
|
||||
<t t-else="">
|
||||
<span class="o_tax_group_amount_value o_list_monetary">
|
||||
<t t-out="props.taxGroup.formatted_tax_group_amount" style="white-space: nowrap;"/>
|
||||
</span>
|
||||
</t>
|
||||
</td>
|
||||
</tr>
|
||||
</t>
|
||||
|
||||
<t t-name="account.TaxTotalsField" owl="1">
|
||||
<table t-if="totals" class="oe_right">
|
||||
<tbody>
|
||||
<t t-foreach="totals.subtotals" t-as="subtotal" t-key="subtotal['name']">
|
||||
<tr>
|
||||
<td class="o_td_label">
|
||||
<label class="o_form_label o_tax_total_label" t-esc="subtotal['name']"/>
|
||||
</td>
|
||||
|
||||
<td class="o_list_monetary">
|
||||
<span t-att-name="subtotal['name']" style="white-space: nowrap; font-weight: bold;" t-out="subtotal['formatted_amount']"/>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<t t-foreach="totals.groups_by_subtotal[subtotal['name']]" t-as="taxGroup" t-key="taxGroup.group_key">
|
||||
<TaxGroupComponent
|
||||
currency="currency"
|
||||
taxGroup="taxGroup"
|
||||
isReadonly="readonly"
|
||||
onChangeTaxGroup.bind="_onChangeTaxValueByTaxGroup"
|
||||
invalidate.bind="invalidate"
|
||||
/>
|
||||
</t>
|
||||
</t>
|
||||
|
||||
<tr t-if="'formatted_rounding_amount' in totals and totals.rounding_amount !== 0 and totals.display_rounding">
|
||||
<td class="o_td_label">
|
||||
<label class="o_form_label o_tax_total_label">Rounding</label>
|
||||
</td>
|
||||
<td class="o_list_monetary">
|
||||
<span
|
||||
t-out="totals.formatted_rounding_amount"
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<!-- Total amount with all taxes-->
|
||||
<tr>
|
||||
<td class="o_td_label">
|
||||
<label class="o_form_label o_tax_total_label">Total</label>
|
||||
</td>
|
||||
|
||||
<td class="o_list_monetary">
|
||||
<span
|
||||
name="amount_total"
|
||||
t-att-class="Object.keys(totals.groups_by_subtotal).length > 0 ? 'oe_subtotal_footer_separator' : ''"
|
||||
t-out="totals.formatted_amount_total"
|
||||
style="font-size: 1.3em; font-weight: bold; white-space: nowrap;"
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</t>
|
||||
</templates>
|
||||
|
|
@ -0,0 +1,61 @@
|
|||
.openerp div.oe_account_help {
|
||||
background : #D6EBFF;
|
||||
width: 100%;
|
||||
padding: 10px;
|
||||
border: 3px solid #C1D4E6;
|
||||
}
|
||||
|
||||
.openerp p.oe_account_font_help{
|
||||
text-align: left;
|
||||
font-weight: bold;
|
||||
margin: 0px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.openerp p.oe_account_font_content{
|
||||
margin-left: 30px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.openerp p.oe_account_font_title{
|
||||
margin-top: 7px;
|
||||
font-size: 15px;
|
||||
font-style: italic;
|
||||
color: grey;
|
||||
}
|
||||
|
||||
.oe_invoice_outstanding_credits_debits {
|
||||
clear: both;
|
||||
float: right;
|
||||
min-width: 260px;
|
||||
padding-top: 20px;
|
||||
}
|
||||
|
||||
.oe_account_terms {
|
||||
flex: auto !important;
|
||||
}
|
||||
|
||||
@media (max-width: 991.98px) {
|
||||
/* The purpose is to put the narration below the totals in the tab 'Invoice Lines'
|
||||
instead of above for the mobile view */
|
||||
.o_form_view .oe_invoice_lines_tab {
|
||||
display: flex;
|
||||
flex-direction: column-reverse;
|
||||
}
|
||||
|
||||
.o_form_view .oe_invoice_lines_tab .oe_invoice_outstanding_credits_debits {
|
||||
min-width: initial;
|
||||
width: 50%;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 767.98px) {
|
||||
.o_form_view .oe_invoice_lines_tab .oe_invoice_outstanding_credits_debits {
|
||||
min-width: initial;
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.o_field_account_resequence_widget {
|
||||
width: 100%;
|
||||
}
|
||||
|
|
@ -0,0 +1,31 @@
|
|||
.openerp .oe_force_bold {
|
||||
font-weight: bold !important;
|
||||
}
|
||||
.openerp label.oe_open_balance{
|
||||
margin-right: -18px;
|
||||
}
|
||||
.openerp label.oe_subtotal_footer_separator{
|
||||
float:right;
|
||||
width: 184px !important;
|
||||
}
|
||||
.openerp label.oe_mini_subtotal_footer_separator{
|
||||
margin-right: -14px;
|
||||
}
|
||||
.openerp .oe_account_total, .openerp .oe_pos_total {
|
||||
margin-left: -2px;
|
||||
}
|
||||
.openerp label.oe_real_closing_balance{
|
||||
min-width: 184px !important;
|
||||
}
|
||||
.openerp label.oe_difference, .openerp label.oe_pos_difference {
|
||||
margin-right: -10px;
|
||||
padding-left: 10px !important;
|
||||
min-width: 195px !important;
|
||||
}
|
||||
.openerp .oe_opening_total{
|
||||
margin-right: 4px;
|
||||
}
|
||||
|
||||
.o_payment_label{
|
||||
padding-right: 20px;
|
||||
}
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 34 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 23 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 2.8 KiB |
BIN
odoo-bringout-oca-ocb-account/account/static/src/img/graph.png
Normal file
BIN
odoo-bringout-oca-ocb-account/account/static/src/img/graph.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.7 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 6 KiB |
|
|
@ -0,0 +1,13 @@
|
|||
/** @odoo-module */
|
||||
|
||||
import publicWidget from 'web.public.widget';
|
||||
import "portal.portal"; // force dependencies
|
||||
|
||||
publicWidget.registry.PortalHomeCounters.include({
|
||||
/**
|
||||
* @override
|
||||
*/
|
||||
_getCountersAlwaysDisplayed() {
|
||||
return this._super(...arguments).concat(['invoice_count']);
|
||||
},
|
||||
});
|
||||
|
|
@ -0,0 +1,73 @@
|
|||
odoo.define('account.AccountPortalSidebar', function (require) {
|
||||
'use strict';
|
||||
|
||||
const dom = require('web.dom');
|
||||
var publicWidget = require('web.public.widget');
|
||||
var PortalSidebar = require('portal.PortalSidebar');
|
||||
var utils = require('web.utils');
|
||||
|
||||
publicWidget.registry.AccountPortalSidebar = PortalSidebar.extend({
|
||||
selector: '.o_portal_invoice_sidebar',
|
||||
events: {
|
||||
'click .o_portal_invoice_print': '_onPrintInvoice',
|
||||
},
|
||||
|
||||
/**
|
||||
* @override
|
||||
*/
|
||||
start: function () {
|
||||
var def = this._super.apply(this, arguments);
|
||||
|
||||
var $invoiceHtml = this.$el.find('iframe#invoice_html');
|
||||
var updateIframeSize = this._updateIframeSize.bind(this, $invoiceHtml);
|
||||
|
||||
$(window).on('resize', updateIframeSize);
|
||||
|
||||
var iframeDoc = $invoiceHtml[0].contentDocument || $invoiceHtml[0].contentWindow.document;
|
||||
if (iframeDoc.readyState === 'complete') {
|
||||
updateIframeSize();
|
||||
} else {
|
||||
$invoiceHtml.on('load', updateIframeSize);
|
||||
}
|
||||
|
||||
return def;
|
||||
},
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
// Handlers
|
||||
//--------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Called when the iframe is loaded or the window is resized on customer portal.
|
||||
* The goal is to expand the iframe height to display the full report without scrollbar.
|
||||
*
|
||||
* @private
|
||||
* @param {object} $el: the iframe
|
||||
*/
|
||||
_updateIframeSize: function ($el) {
|
||||
var $wrapwrap = $el.contents().find('div#wrapwrap');
|
||||
// Set it to 0 first to handle the case where scrollHeight is too big for its content.
|
||||
$el.height(0);
|
||||
$el.height($wrapwrap[0].scrollHeight);
|
||||
|
||||
// scroll to the right place after iframe resize
|
||||
if (!utils.isValidAnchor(window.location.hash)) {
|
||||
return;
|
||||
}
|
||||
var $target = $(window.location.hash);
|
||||
if (!$target.length) {
|
||||
return;
|
||||
}
|
||||
dom.scrollTo($target[0], {duration: 0});
|
||||
},
|
||||
/**
|
||||
* @private
|
||||
* @param {MouseEvent} ev
|
||||
*/
|
||||
_onPrintInvoice: function (ev) {
|
||||
ev.preventDefault();
|
||||
var href = $(ev.currentTarget).attr('href');
|
||||
this._printIframeContent(href);
|
||||
},
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,149 @@
|
|||
odoo.define('account.payment', function (require) {
|
||||
"use strict";
|
||||
|
||||
var AbstractField = require('web.AbstractField');
|
||||
var core = require('web.core');
|
||||
var field_registry = require('web.field_registry');
|
||||
var field_utils = require('web.field_utils');
|
||||
|
||||
var QWeb = core.qweb;
|
||||
var _t = core._t;
|
||||
|
||||
var ShowPaymentLineWidget = AbstractField.extend({
|
||||
events: _.extend({
|
||||
'click .outstanding_credit_assign': '_onOutstandingCreditAssign',
|
||||
'click .open_account_move': '_onOpenPaymentOrMove',
|
||||
}, AbstractField.prototype.events),
|
||||
supportedFieldTypes: ['char'],
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
// Public
|
||||
//--------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* @override
|
||||
* @returns {boolean}
|
||||
*/
|
||||
isSet: function() {
|
||||
return true;
|
||||
},
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
// Private
|
||||
//--------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* @private
|
||||
* @override
|
||||
*/
|
||||
_render: function() {
|
||||
this.viewAlreadyOpened = false;
|
||||
var self = this;
|
||||
var info = this.value;
|
||||
if (!info) {
|
||||
this.$el.html('');
|
||||
return;
|
||||
}
|
||||
_.each(info.content, function (k, v){
|
||||
k.index = v;
|
||||
k.amount = field_utils.format.float(k.amount, {digits: k.digits});
|
||||
if (k.date){
|
||||
k.date = field_utils.format.date(field_utils.parse.date(k.date, {}, {isUTC: true}));
|
||||
}
|
||||
});
|
||||
this.$el.html(QWeb.render('ShowPaymentInfo', {
|
||||
lines: info.content,
|
||||
outstanding: info.outstanding,
|
||||
title: info.title
|
||||
}));
|
||||
_.each(this.$('.js_payment_info'), function (k, v){
|
||||
var isRTL = _t.database.parameters.direction === "rtl";
|
||||
var content = info.content[v];
|
||||
var options = {
|
||||
content: function () {
|
||||
var $content = $(QWeb.render('PaymentPopOver', content));
|
||||
$content.filter('.js_unreconcile_payment').on('click', self._onRemoveMoveReconcile.bind(self));
|
||||
|
||||
$content.filter('.js_open_payment').on('click', self._onOpenPaymentOrMove.bind(self));
|
||||
return $content;
|
||||
},
|
||||
html: true,
|
||||
placement: isRTL ? 'bottom' : 'left',
|
||||
title: 'Payment Information',
|
||||
trigger: 'focus',
|
||||
delay: { "show": 0, "hide": 100 },
|
||||
container: $(k).parent(), // FIXME Ugly, should use the default body container but system & tests to adapt to properly destroy the popover
|
||||
};
|
||||
$(k).popover(options);
|
||||
});
|
||||
},
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
// Handlers
|
||||
//--------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* @private
|
||||
* @override
|
||||
* @param {MouseEvent} event
|
||||
*/
|
||||
_onOpenPaymentOrMove: function (event) {
|
||||
var moveId = parseInt($(event.target).attr('move-id'));
|
||||
if (!this.viewAlreadyOpened && moveId !== undefined && !isNaN(moveId)) {
|
||||
this.viewAlreadyOpened = true;
|
||||
var self = this;
|
||||
this._rpc({
|
||||
model: 'account.move',
|
||||
method: 'action_open_business_doc',
|
||||
args: [moveId],
|
||||
}).then(function (actionData) {
|
||||
return self.do_action(actionData);
|
||||
});
|
||||
}
|
||||
},
|
||||
/**
|
||||
* @private
|
||||
* @override
|
||||
* @param {MouseEvent} event
|
||||
*/
|
||||
_onOutstandingCreditAssign: function (event) {
|
||||
event.stopPropagation();
|
||||
event.preventDefault();
|
||||
var self = this;
|
||||
var id = $(event.target).data('id') || false;
|
||||
this._rpc({
|
||||
model: 'account.move',
|
||||
method: 'js_assign_outstanding_line',
|
||||
args: [this.value.move_id, id],
|
||||
}).then(function () {
|
||||
self.trigger_up('reload');
|
||||
});
|
||||
},
|
||||
/**
|
||||
* @private
|
||||
* @override
|
||||
* @param {MouseEvent} event
|
||||
*/
|
||||
_onRemoveMoveReconcile: function (event) {
|
||||
var self = this;
|
||||
var moveId = parseInt($(event.target).attr('move-id'));
|
||||
var partialId = parseInt($(event.target).attr('partial-id'));
|
||||
if (partialId !== undefined && !isNaN(partialId)){
|
||||
this._rpc({
|
||||
model: 'account.move',
|
||||
method: 'js_remove_outstanding_partial',
|
||||
args: [moveId, partialId],
|
||||
}).then(function () {
|
||||
self.trigger_up('reload');
|
||||
});
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
field_registry.add('payment', ShowPaymentLineWidget);
|
||||
|
||||
return {
|
||||
ShowPaymentLineWidget: ShowPaymentLineWidget
|
||||
};
|
||||
|
||||
});
|
||||
|
|
@ -0,0 +1,31 @@
|
|||
/** @odoo-module **/
|
||||
|
||||
import { qweb, _t } from 'web.core';
|
||||
import fieldRegistry from 'web.field_registry';
|
||||
import { FieldSelection } from 'web.relational_fields';
|
||||
|
||||
|
||||
export const HierarchySelection = FieldSelection.extend({
|
||||
|
||||
init: function () {
|
||||
this._super.apply(this, arguments);
|
||||
this.hierarchyGroups = [
|
||||
{name: _t('Balance Sheet')},
|
||||
{name: _t('Assets'), children: this.values.filter(x => x[0] && x[0].startsWith('asset'))},
|
||||
{name: _t('Liabilities'), children: this.values.filter(x => x[0] && x[0].startsWith('liability'))},
|
||||
{name: _t('Equity'), children: this.values.filter(x => x[0] && x[0].startsWith('equity'))},
|
||||
{name: _t('Profit & Loss')},
|
||||
{name: _t('Income'), children: this.values.filter(x => x[0] && x[0].startsWith('income'))},
|
||||
{name: _t('Expense'), children: this.values.filter(x => x[0] && x[0].startsWith('expense'))},
|
||||
{name: _t('Other'), children: this.values.filter(x => x[0] && x[0] == 'off_balance')},
|
||||
];
|
||||
},
|
||||
|
||||
_renderEdit: function () {
|
||||
this.$el.empty();
|
||||
this.$el.append(qweb.render('accountTypeSelection', {widget: this}));
|
||||
this.$el.val(JSON.stringify(this._getRawValue()));
|
||||
}
|
||||
});
|
||||
|
||||
fieldRegistry.add("account_type_selection", HierarchySelection);
|
||||
|
|
@ -0,0 +1,68 @@
|
|||
odoo.define('account.activity', function (require) {
|
||||
"use strict";
|
||||
|
||||
var AbstractField = require('web.AbstractField');
|
||||
var core = require('web.core');
|
||||
var field_registry = require('web.field_registry');
|
||||
|
||||
var QWeb = core.qweb;
|
||||
var _t = core._t;
|
||||
|
||||
var VatActivity = AbstractField.extend({
|
||||
className: 'o_journal_activity_kanban',
|
||||
events: {
|
||||
'click .see_all_activities': '_onOpenAll',
|
||||
'click .see_activity': '_onOpenActivity',
|
||||
},
|
||||
init: function () {
|
||||
this.MAX_ACTIVITY_DISPLAY = 5;
|
||||
this._super.apply(this, arguments);
|
||||
},
|
||||
//------------------------------------------------------------
|
||||
// Private
|
||||
//------------------------------------------------------------
|
||||
_render: function () {
|
||||
var info = JSON.parse(this.value);
|
||||
if (!info) {
|
||||
this.$el.html('');
|
||||
return;
|
||||
}
|
||||
info.more_activities = false;
|
||||
if (info.activities.length > this.MAX_ACTIVITY_DISPLAY) {
|
||||
info.more_activities = true;
|
||||
info.activities = info.activities.slice(0, this.MAX_ACTIVITY_DISPLAY);
|
||||
}
|
||||
this.$el.html(QWeb.render('accountJournalDashboardActivity', info));
|
||||
},
|
||||
|
||||
_onOpenActivity: function(e) {
|
||||
e.preventDefault();
|
||||
var self = this;
|
||||
self.do_action({
|
||||
type: 'ir.actions.act_window',
|
||||
name: _t('Journal Entry'),
|
||||
target: 'current',
|
||||
res_id: $(e.target).data('resId'),
|
||||
res_model: 'account.move',
|
||||
views: [[false, 'form']],
|
||||
});
|
||||
},
|
||||
|
||||
_onOpenAll: function(e) {
|
||||
e.preventDefault();
|
||||
var self = this;
|
||||
self.do_action({
|
||||
type: 'ir.actions.act_window',
|
||||
name: _t('Journal Entries'),
|
||||
res_model: 'account.move',
|
||||
views: [[false, 'kanban'], [false, 'form']],
|
||||
search_view_id: [false],
|
||||
domain: [['journal_id', '=', self.res_id], ['activity_ids', '!=', false]],
|
||||
});
|
||||
}
|
||||
})
|
||||
|
||||
field_registry.add('kanban_vat_activity', VatActivity);
|
||||
|
||||
return VatActivity;
|
||||
});
|
||||
|
|
@ -0,0 +1,23 @@
|
|||
/** @odoo-module **/
|
||||
import fieldRegistry from 'web.field_registry';
|
||||
import { FieldChar } from 'web.basic_fields';
|
||||
|
||||
const OpenMoveWidget = FieldChar.extend({
|
||||
template: 'account.OpenMoveTemplate',
|
||||
events: Object.assign({}, FieldChar.prototype.events, {
|
||||
'click': '_onOpenMove',
|
||||
}),
|
||||
_onOpenMove: function(ev) {
|
||||
ev.preventDefault();
|
||||
ev.stopPropagation();
|
||||
var self = this;
|
||||
this._rpc({
|
||||
model: 'account.move.line',
|
||||
method: 'action_open_business_doc',
|
||||
args: [this.res_id],
|
||||
}).then(function (actionData){
|
||||
return self.do_action(actionData);
|
||||
});
|
||||
},
|
||||
});
|
||||
fieldRegistry.add('open_move_widget', OpenMoveWidget);
|
||||
|
|
@ -0,0 +1,106 @@
|
|||
|
||||
odoo.define('account.section_and_note_backend', function (require) {
|
||||
// The goal of this file is to contain JS hacks related to allowing
|
||||
// section and note on sale order and invoice.
|
||||
|
||||
// [UPDATED] now also allows configuring products on sale order.
|
||||
|
||||
"use strict";
|
||||
var FieldChar = require('web.basic_fields').FieldChar;
|
||||
var FieldOne2Many = require('web.relational_fields').FieldOne2Many;
|
||||
var fieldRegistry = require('web.field_registry');
|
||||
var ListFieldText = require('web.basic_fields').ListFieldText;
|
||||
var ListRenderer = require('web.ListRenderer');
|
||||
|
||||
var SectionAndNoteListRenderer = ListRenderer.extend({
|
||||
/**
|
||||
* We want section and note to take the whole line (except handle and trash)
|
||||
* to look better and to hide the unnecessary fields.
|
||||
*
|
||||
* @override
|
||||
*/
|
||||
_renderBodyCell: function (record, node, index, options) {
|
||||
var $cell = this._super.apply(this, arguments);
|
||||
|
||||
var isSection = record.data.display_type === 'line_section';
|
||||
var isNote = record.data.display_type === 'line_note';
|
||||
|
||||
if (isSection || isNote) {
|
||||
if (node.attrs.widget === "handle") {
|
||||
return $cell;
|
||||
} else if (node.attrs.name === "name") {
|
||||
var nbrColumns = this._getNumberOfCols();
|
||||
if (this.handleField) {
|
||||
nbrColumns--;
|
||||
}
|
||||
if (this.addTrashIcon) {
|
||||
nbrColumns--;
|
||||
}
|
||||
$cell.attr('colspan', nbrColumns);
|
||||
} else {
|
||||
$cell.removeClass('o_invisible_modifier');
|
||||
return $cell.addClass('o_hidden');
|
||||
}
|
||||
}
|
||||
|
||||
return $cell;
|
||||
},
|
||||
/**
|
||||
* We add the o_is_{display_type} class to allow custom behaviour both in JS and CSS.
|
||||
*
|
||||
* @override
|
||||
*/
|
||||
_renderRow: function (record, index) {
|
||||
var $row = this._super.apply(this, arguments);
|
||||
|
||||
if (record.data.display_type) {
|
||||
$row.addClass('o_is_' + record.data.display_type);
|
||||
}
|
||||
|
||||
return $row;
|
||||
},
|
||||
/**
|
||||
* We want to add .o_section_and_note_list_view on the table to have stronger CSS.
|
||||
*
|
||||
* @override
|
||||
* @private
|
||||
*/
|
||||
_renderView: function () {
|
||||
var self = this;
|
||||
return this._super.apply(this, arguments).then(function () {
|
||||
self.$('.o_list_table').addClass('o_section_and_note_list_view');
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// We create a custom widget because this is the cleanest way to do it:
|
||||
// to be sure this custom code will only impact selected fields having the widget
|
||||
// and not applied to any other existing ListRenderer.
|
||||
var SectionAndNoteFieldOne2Many = FieldOne2Many.extend({
|
||||
/**
|
||||
* We want to use our custom renderer for the list.
|
||||
*
|
||||
* @override
|
||||
*/
|
||||
_getRenderer: function () {
|
||||
if (this.view.arch.tag === 'tree') {
|
||||
return SectionAndNoteListRenderer;
|
||||
}
|
||||
return this._super.apply(this, arguments);
|
||||
},
|
||||
});
|
||||
|
||||
// This is a merge between a FieldText and a FieldChar.
|
||||
// We want a FieldChar for section,
|
||||
// and a FieldText for the rest (product and note).
|
||||
var SectionAndNoteFieldText = function (parent, name, record, options) {
|
||||
var isSection = record.data.display_type === 'line_section';
|
||||
var Constructor = isSection ? FieldChar : ListFieldText;
|
||||
return new Constructor(parent, name, record, options);
|
||||
};
|
||||
|
||||
fieldRegistry.add('section_and_note_one2many', SectionAndNoteFieldOne2Many);
|
||||
fieldRegistry.add('section_and_note_text', SectionAndNoteFieldText);
|
||||
|
||||
return SectionAndNoteListRenderer;
|
||||
});
|
||||
|
|
@ -0,0 +1,199 @@
|
|||
/** @odoo-module alias=account.tax_group_owl **/
|
||||
"use strict";
|
||||
|
||||
import session from 'web.session';
|
||||
import AbstractFieldOwl from 'web.AbstractFieldOwl';
|
||||
import fieldUtils from 'web.field_utils';
|
||||
import field_registry from 'web.field_registry_owl';
|
||||
import { LegacyComponent } from "@web/legacy/legacy_component";
|
||||
|
||||
const { onPatched, onWillUpdateProps, useRef, useState } = owl;
|
||||
|
||||
/**
|
||||
A line of some LegacyTaxTotalsComponent, giving the values of a tax group.
|
||||
**/
|
||||
class LegacyTaxGroupComponent extends LegacyComponent {
|
||||
setup() {
|
||||
this.inputTax = useRef('taxValueInput');
|
||||
this.state = useState({value: 'readonly'});
|
||||
onPatched(this.onPatched);
|
||||
onWillUpdateProps(this.onWillUpdateProps);
|
||||
}
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
// Life cycle methods
|
||||
//--------------------------------------------------------------------------
|
||||
|
||||
onWillUpdateProps(nextProps) {
|
||||
this.setState('readonly'); // If props are edited, we set the state to readonly
|
||||
}
|
||||
|
||||
onPatched() {
|
||||
if (this.state.value === 'edit') {
|
||||
let newValue = this.props.taxGroup.tax_group_amount;
|
||||
let currency = session.get_currency(this.props.record.data.currency_id.data.id);
|
||||
|
||||
newValue = fieldUtils.format.float(newValue, null, {digits: currency.digits});
|
||||
this.inputTax.el.focus(); // Focus the input
|
||||
this.inputTax.el.value = newValue;
|
||||
}
|
||||
}
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
// Main methods
|
||||
//--------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* The purpose of this method is to change the state of the component.
|
||||
* It can have one of the following three states:
|
||||
* - readonly: display in read-only mode of the field,
|
||||
* - edit: display with a html input field,
|
||||
* - disable: display with a html input field that is disabled.
|
||||
*
|
||||
* If a value other than one of these 3 states is passed as a parameter,
|
||||
* the component is set to readonly by default.
|
||||
*
|
||||
* @param {String} value
|
||||
*/
|
||||
setState(value) {
|
||||
if (['readonly', 'edit', 'disable'].includes(value)) {
|
||||
this.state.value = value;
|
||||
}
|
||||
else {
|
||||
this.state.value = 'readonly';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* This method handles the "_onChangeTaxValue" event. In this method,
|
||||
* we get the new value for the tax group, we format it and we call
|
||||
* the method to recalculate the tax lines. At the moment the method
|
||||
* is called, we disable the html input field.
|
||||
*
|
||||
* In case the value has not changed or the tax group is equal to 0,
|
||||
* the modification does not take place.
|
||||
*/
|
||||
_onChangeTaxValue() {
|
||||
this.setState('disable'); // Disable the input
|
||||
let newValue = this.inputTax.el.value; // Get the new value
|
||||
let currency = session.get_currency(this.props.record.data.currency_id.data.id); // The records using this widget must have a currency_id field.
|
||||
try {
|
||||
newValue = fieldUtils.parse.float(newValue); // Need a float for format the value
|
||||
newValue = fieldUtils.format.float(newValue, null, {digits: currency.digits}); // Return a string rounded to currency precision
|
||||
newValue = fieldUtils.parse.float(newValue); // Convert back to Float to compare with oldValue to know if value has changed
|
||||
} catch (_err) {
|
||||
$(this.inputTax.el).addClass('o_field_invalid');
|
||||
this.setState('edit');
|
||||
return;
|
||||
}
|
||||
// The newValue can't be equals to 0
|
||||
if (newValue === this.props.taxGroup.tax_group_amount || newValue === 0) {
|
||||
this.setState('readonly');
|
||||
return;
|
||||
}
|
||||
this.props.taxGroup.tax_group_amount = newValue;
|
||||
this.props.onChangeTaxGroup({
|
||||
oldValue: this.props.taxGroup.tax_group_amount,
|
||||
newValue: newValue,
|
||||
taxGroupId: this.props.taxGroup.tax_group_id
|
||||
});
|
||||
}
|
||||
}
|
||||
LegacyTaxGroupComponent.props = ['taxGroup', 'readonly', 'record', 'onChangeTaxGroup'];
|
||||
LegacyTaxGroupComponent.template = 'account.LegacyTaxGroupComponent';
|
||||
|
||||
/**
|
||||
Widget used to display tax totals by tax groups for invoices, PO and SO,
|
||||
and possibly allowing editing them.
|
||||
|
||||
Note that this widget requires the object it is used on to have a
|
||||
currency_id field.
|
||||
**/
|
||||
class LegacyTaxTotalsComponent extends AbstractFieldOwl {
|
||||
setup() {
|
||||
super.setup();
|
||||
this.totals = useState({value: this.value ? this.value : null});
|
||||
this._computeTotalsFormat()
|
||||
this.readonly = this.mode == 'readonly' || this.record.evalModifiers(this.attrs.modifiers).readonly;
|
||||
onWillUpdateProps(this.onWillUpdateProps);
|
||||
}
|
||||
|
||||
onWillUpdateProps(nextProps) {
|
||||
// We only reformat tax groups if there are changed
|
||||
this.totals.value = nextProps.record.data[this.props.fieldName];
|
||||
this._computeTotalsFormat()
|
||||
}
|
||||
|
||||
_onKeydown(ev) {
|
||||
switch (ev.which) {
|
||||
// Trigger only if the user clicks on ENTER or on TAB.
|
||||
case $.ui.keyCode.ENTER:
|
||||
case $.ui.keyCode.TAB:
|
||||
// trigger blur to prevent the code being executed twice
|
||||
$(ev.target).blur();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* This method is the main function of the tax group widget.
|
||||
* It is called by an event trigger (from the LegacyTaxGroupComponent) and receives
|
||||
* a particular payload.
|
||||
*
|
||||
* It is responsible for calculating taxes based on tax groups and triggering
|
||||
* an event to notify the ORM of a change.
|
||||
*/
|
||||
_onChangeTaxValueByTaxGroup(ev) {
|
||||
this._computeTotalsFormat();
|
||||
this.trigger('field-changed', {
|
||||
dataPointID: this.record.id,
|
||||
changes: { tax_totals: this.totals.value }
|
||||
})
|
||||
}
|
||||
|
||||
_format(amount) {
|
||||
if (this.props.record.data.currency_id.data) {
|
||||
const currency = session.get_currency(this.props.record.data.currency_id.data.id);
|
||||
return fieldUtils.format.monetary(amount, null, {currency: currency});
|
||||
}
|
||||
return fieldUtils.format.monetary(amount);
|
||||
}
|
||||
|
||||
_computeTotalsFormat() {
|
||||
if (!this.totals.value) // Misc journal entry
|
||||
return;
|
||||
let amount_untaxed = this.totals.value.amount_untaxed;
|
||||
let amount_tax = 0;
|
||||
let subtotals = [];
|
||||
for (let subtotal_title of this.totals.value.subtotals_order) {
|
||||
let amount_total = amount_untaxed + amount_tax;
|
||||
subtotals.push({
|
||||
'name': subtotal_title,
|
||||
'amount': amount_total,
|
||||
'formatted_amount': this._format(amount_total),
|
||||
});
|
||||
let group = this.totals.value.groups_by_subtotal[subtotal_title];
|
||||
for (let i in group) {
|
||||
amount_tax = amount_tax + group[i].tax_group_amount;
|
||||
}
|
||||
}
|
||||
this.totals.value.subtotals = subtotals;
|
||||
let amount_total = amount_untaxed + amount_tax;
|
||||
this.totals.value.amount_total = amount_total;
|
||||
this.totals.value.formatted_amount_total = this._format(amount_total);
|
||||
for (let group_name of Object.keys(this.totals.value.groups_by_subtotal)) {
|
||||
let group = this.totals.value.groups_by_subtotal[group_name];
|
||||
for (let i in group) {
|
||||
group[i].formatted_tax_group_amount = this._format(group[i].tax_group_amount);
|
||||
group[i].formatted_tax_group_base_amount = this._format(group[i].tax_group_base_amount);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
LegacyTaxTotalsComponent.template = 'account.LegacyTaxTotalsField';
|
||||
LegacyTaxTotalsComponent.components = { LegacyTaxGroupComponent };
|
||||
|
||||
|
||||
field_registry.add('account-tax-totals-field', LegacyTaxTotalsComponent);
|
||||
|
||||
export default LegacyTaxTotalsComponent
|
||||
|
|
@ -0,0 +1,141 @@
|
|||
odoo.define('account.tour', function(require) {
|
||||
"use strict";
|
||||
|
||||
var core = require('web.core');
|
||||
const {Markup} = require('web.utils');
|
||||
var tour = require('web_tour.tour');
|
||||
|
||||
var _t = core._t;
|
||||
|
||||
tour.register('account_tour', {
|
||||
url: "/web",
|
||||
sequence: 60,
|
||||
}, [
|
||||
...tour.stepUtils.goToAppSteps('account.menu_finance', _t('Send invoices to your customers in no time with the <b>Invoicing app</b>.')),
|
||||
{
|
||||
trigger: "a.o_onboarding_step_action[data-method=action_open_base_onboarding_company]",
|
||||
content: _t("Start by checking your company's data."),
|
||||
position: "bottom",
|
||||
skip_trigger: 'a[data-method=action_open_base_onboarding_company].o_onboarding_step_action__done',
|
||||
}, {
|
||||
trigger: "button[name=action_save_onboarding_company_step]",
|
||||
extra_trigger: "a.o_onboarding_step_action[data-method=action_open_base_onboarding_company]",
|
||||
content: _t("Looks good. Let's continue."),
|
||||
position: "bottom",
|
||||
skip_trigger: 'a[data-method=action_open_base_onboarding_company].o_onboarding_step_action__done',
|
||||
}, {
|
||||
trigger: "a.o_onboarding_step_action[data-method=action_open_base_document_layout]",
|
||||
content: _t("Customize your layout."),
|
||||
position: "bottom",
|
||||
skip_trigger: 'a[data-method=action_open_base_document_layout].o_onboarding_step_action__done',
|
||||
}, {
|
||||
trigger: "button[name=document_layout_save]",
|
||||
extra_trigger: "a.o_onboarding_step_action[data-method=action_open_base_document_layout]",
|
||||
content: _t("Once everything is as you want it, validate."),
|
||||
position: "top",
|
||||
skip_trigger: 'a[data-method=action_open_base_document_layout].o_onboarding_step_action__done',
|
||||
}, {
|
||||
trigger: "a.o_onboarding_step_action[data-method=action_open_account_onboarding_create_invoice]",
|
||||
content: _t("Now, we'll create your first invoice."),
|
||||
position: "bottom",
|
||||
}, {
|
||||
trigger: "div[name=partner_id] .o_input_dropdown",
|
||||
// FIXME WOWL: this selector needs to work in both legacy and non-legacy views
|
||||
// because account_invoice_extracts *adds* a js_class on the base view which forces
|
||||
// the use of a legacy view in enterprise only
|
||||
extra_trigger: "[name=move_type] [raw-value=out_invoice], [name=move_type][raw-value=out_invoice]",
|
||||
content: Markup(_t("Write a company name to <b>create one</b> or <b>see suggestions</b>.")),
|
||||
position: "right",
|
||||
}, {
|
||||
trigger: "div[name=partner_id] input",
|
||||
auto: true,
|
||||
}, {
|
||||
trigger: ".o_m2o_dropdown_option a:contains('Create')",
|
||||
// FIXME WOWL: this selector needs to work in both legacy and non-legacy views
|
||||
// because account_invoice_extracts *adds* a js_class on the base view which forces
|
||||
// the use of a legacy view in enterprise only
|
||||
extra_trigger: "[name=move_type] [raw-value=out_invoice], [name=move_type][raw-value=out_invoice]",
|
||||
content: _t("Select first partner"),
|
||||
auto: true,
|
||||
}, {
|
||||
trigger: ".modal-content button.btn-primary",
|
||||
// FIXME WOWL: this selector needs to work in both legacy and non-legacy views
|
||||
// because account_invoice_extracts *adds* a js_class on the base view which forces
|
||||
// the use of a legacy view in enterprise only
|
||||
extra_trigger: "[name=move_type] [raw-value=out_invoice], [name=move_type][raw-value=out_invoice]",
|
||||
content: Markup(_t("Once everything is set, you are good to continue. You will be able to edit this later in the <b>Customers</b> menu.")),
|
||||
auto: true,
|
||||
}, {
|
||||
trigger: "div[name=invoice_line_ids] .o_field_x2many_list_row_add a",
|
||||
// FIXME WOWL: this selector needs to work in both legacy and non-legacy views
|
||||
// because account_invoice_extracts *adds* a js_class on the base view which forces
|
||||
// the use of a legacy view in enterprise only
|
||||
extra_trigger: "[name=move_type] [raw-value=out_invoice], [name=move_type][raw-value=out_invoice]",
|
||||
content: _t("Add a line to your invoice"),
|
||||
}, {
|
||||
trigger: "div[name=invoice_line_ids] div[name=name] textarea",
|
||||
// FIXME WOWL: this selector needs to work in both legacy and non-legacy views
|
||||
// because account_invoice_extracts *adds* a js_class on the base view which forces
|
||||
// the use of a legacy view in enterprise only
|
||||
extra_trigger: "[name=move_type] [raw-value=out_invoice], [name=move_type][raw-value=out_invoice]",
|
||||
content: _t("Fill in the details of the line."),
|
||||
position: "bottom",
|
||||
}, {
|
||||
trigger: "div[name=invoice_line_ids] div[name=price_unit] input",
|
||||
// FIXME WOWL: this selector needs to work in both legacy and non-legacy views
|
||||
// because account_invoice_extracts *adds* a js_class on the base view which forces
|
||||
// the use of a legacy view in enterprise only
|
||||
extra_trigger: "[name=move_type] [raw-value=out_invoice], [name=move_type][raw-value=out_invoice]",
|
||||
content: _t("Set a price"),
|
||||
position: "bottom",
|
||||
run: 'text 100',
|
||||
},
|
||||
...tour.stepUtils.saveForm(),
|
||||
{
|
||||
trigger: "button[name=action_post]",
|
||||
extra_trigger: "button.o_form_button_create",
|
||||
content: _t("Once your invoice is ready, press CONFIRM."),
|
||||
}, {
|
||||
trigger: "button[name=action_invoice_sent]",
|
||||
// FIXME WOWL: this selector needs to work in both legacy and non-legacy views
|
||||
// because account_invoice_extracts *adds* a js_class on the base view which forces
|
||||
// the use of a legacy view in enterprise only
|
||||
extra_trigger: "[name=move_type] [raw-value=out_invoice], [name=move_type][raw-value=out_invoice]",
|
||||
content: _t("Send the invoice and check what the customer will receive."),
|
||||
}, {
|
||||
trigger: ".o_field_widget[name=email] input, input[name=email]",
|
||||
// FIXME WOWL: this selector needs to work in both legacy and non-legacy views
|
||||
// because account_invoice_extracts *adds* a js_class on the base view which forces
|
||||
// the use of a legacy view in enterprise only
|
||||
extra_trigger: "[name=move_type] [raw-value=out_invoice], [name=move_type][raw-value=out_invoice]",
|
||||
content: Markup(_t("Write here <b>your own email address</b> to test the flow.")),
|
||||
run: 'text customer@example.com',
|
||||
auto: true,
|
||||
}, {
|
||||
trigger: ".modal-content button.btn-primary",
|
||||
// FIXME WOWL: this selector needs to work in both legacy and non-legacy views
|
||||
// because account_invoice_extracts *adds* a js_class on the base view which forces
|
||||
// the use of a legacy view in enterprise only
|
||||
extra_trigger: "[name=move_type] [raw-value=out_invoice], [name=move_type][raw-value=out_invoice]",
|
||||
content: _t("Validate."),
|
||||
auto: true,
|
||||
}, {
|
||||
trigger: "button[name=send_and_print_action]",
|
||||
// FIXME WOWL: this selector needs to work in both legacy and non-legacy views
|
||||
// because account_invoice_extracts *adds* a js_class on the base view which forces
|
||||
// the use of a legacy view in enterprise only
|
||||
extra_trigger: "[name=move_type] [raw-value=out_invoice], [name=move_type][raw-value=out_invoice]",
|
||||
content: _t("Let's send the invoice."),
|
||||
position: "top"
|
||||
}, {
|
||||
trigger: "button[name=action_invoice_sent].btn-secondary",
|
||||
content: _t("The invoice having been sent, the button has changed priority."),
|
||||
run() {},
|
||||
}, {
|
||||
trigger: "button[name=action_register_payment]",
|
||||
content: _t("The next step is payment registration."),
|
||||
run() {},
|
||||
}
|
||||
]);
|
||||
|
||||
});
|
||||
|
|
@ -0,0 +1,61 @@
|
|||
.o_kanban_dashboard.o_account_kanban {
|
||||
|
||||
&.o_kanban_ungrouped .o_account_dashboard_header {
|
||||
margin: (0 - $o-kanban-record-margin) ($o-kanban-record-margin - $o-horizontal-padding) $o-kanban-record-margin;
|
||||
}
|
||||
|
||||
.o_account_dashboard_header {
|
||||
flex: 1 0 100%;
|
||||
flex-flow: column nowrap;
|
||||
align-self: flex-start;
|
||||
width: 100%;
|
||||
height: auto; // cancel o_form_view height 100%, which hides the help tip message at the bottom of the screen
|
||||
min-height: 0%; // cancel o_form_view min-height 100%, which hides the help tip message at the bottom of the screen
|
||||
background-color: $o-view-background-color;
|
||||
|
||||
.o_form_statusbar {
|
||||
padding-right: $o-horizontal-padding;
|
||||
}
|
||||
|
||||
h4 {
|
||||
font-size: $font-size-base;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.fa-gift {
|
||||
color: #eeeeee;
|
||||
&:hover {
|
||||
color: #555555;
|
||||
}
|
||||
}
|
||||
|
||||
.o_arrow_button.btn-secondary {
|
||||
color: $text-muted;
|
||||
text-transform: none;
|
||||
font-weight: 500;
|
||||
|
||||
.o_account_dashboard_index {
|
||||
color: map-get($grays, '900');
|
||||
}
|
||||
|
||||
&.o_action_done {
|
||||
color: map-get($grays, '900');
|
||||
background-color: map-get($grays, '200');
|
||||
|
||||
&:after {
|
||||
border-left-color: map-get($grays, '200');
|
||||
}
|
||||
|
||||
.fa-check {
|
||||
@extend .text-success;
|
||||
}
|
||||
}
|
||||
|
||||
&:last-of-type {
|
||||
margin-left: $o-horizontal-padding;
|
||||
padding-left: $o-horizontal-padding*.5;
|
||||
border-left: 1px solid map-get($grays, '300');
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,70 @@
|
|||
.o_kanban_dashboard.o_account_kanban {
|
||||
.o_kanban_record {
|
||||
|
||||
@include media-breakpoint-up(sm) {
|
||||
.oe_kanban_action_button {
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
}
|
||||
|
||||
.o_kanban_card_settings {
|
||||
padding-top: $o-horizontal-padding/2;
|
||||
padding-bottom: $o-horizontal-padding/2;
|
||||
|
||||
border-top: 1px solid;
|
||||
border-color: $o-brand-lightsecondary;
|
||||
}
|
||||
.o_dashboard_star {
|
||||
font-size: 12px;
|
||||
|
||||
&.fa-star-o {
|
||||
color: $o-main-color-muted;
|
||||
&:hover {
|
||||
color: gold;
|
||||
}
|
||||
}
|
||||
&.fa-star {
|
||||
color: gold;
|
||||
}
|
||||
}
|
||||
|
||||
.o_dashboard_graph {
|
||||
margin-bottom: -$o-horizontal-padding/2;
|
||||
}
|
||||
|
||||
.o_field_widget.o_field_kanban_vat_activity {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
|
||||
&.o_kanban_ungrouped {
|
||||
.o_kanban_record {
|
||||
width: 450px;
|
||||
}
|
||||
}
|
||||
|
||||
.o_kanban_group {
|
||||
&:not(.o_column_folded) {
|
||||
--KanbanGroup-width: 450px;
|
||||
@include media-breakpoint-down(md) {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Style for the widget "dashboard_graph"
|
||||
.o_dashboard_graph {
|
||||
position: relative;
|
||||
margin: 16px -16px;
|
||||
|
||||
canvas {
|
||||
height: 75px;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
.o_sample_data .o_dashboard_graph.o_graph_linechart > svg g.nv-linesWrap g.nv-group.nv-series-0 {
|
||||
fill: gray !important;
|
||||
opacity: 0.1;
|
||||
}
|
||||
|
|
@ -0,0 +1,32 @@
|
|||
.o_search_panel.account_root {
|
||||
flex: 0 0 50px;
|
||||
padding: 6px !important; // need to override bootstrap ps-4 rule
|
||||
scrollbar-width: thin;
|
||||
.o_search_panel_section_header {
|
||||
display: none;
|
||||
}
|
||||
.list-group-item span.o_search_panel_label_title {
|
||||
display: contents;
|
||||
}
|
||||
.o_search_panel_category_value {
|
||||
padding-right: 0 !important;
|
||||
header {
|
||||
margin-left: 0;
|
||||
}
|
||||
.o_toggle_fold:empty{
|
||||
width: 0;
|
||||
}
|
||||
.o_search_panel_category_value {
|
||||
padding-left: 1.2rem !important; // need to override bootstrap ps-4 rule
|
||||
.o_toggle_fold {
|
||||
width: 0.3rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
&::-webkit-scrollbar {
|
||||
width: 4px;
|
||||
}
|
||||
&::-webkit-scrollbar-thumb {
|
||||
background: lightgray;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
.o_journal_activity_kanban {
|
||||
display: block;
|
||||
.align_activity_center {
|
||||
width: 100%;
|
||||
align-items: center;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
@keyframes animate-red {
|
||||
0% {
|
||||
color: red;
|
||||
}
|
||||
100% {
|
||||
color: inherit;
|
||||
}
|
||||
}
|
||||
|
||||
.animate {
|
||||
animation: animate-red 1s ease;
|
||||
}
|
||||
|
|
@ -0,0 +1,21 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
|
||||
<templates>
|
||||
|
||||
<t t-name="accountJournalDashboardActivity">
|
||||
<t t-foreach="activities" t-as="activity">
|
||||
<div class="row">
|
||||
<div class="col-8 o_mail_activity">
|
||||
<a href="#" t-att-class="(activity.status == 'late' ? 'o_activity_color_overdue ' : ' ') + (activity.activity_category == 'tax_report' ? 'o_open_vat_report' : 'see_activity')" t-att-data-res-id="activity.res_id" t-att-data-id="activity.id" t-att-data-model="activity.res_model">
|
||||
<t t-esc="activity.name"/>
|
||||
</a>
|
||||
</div>
|
||||
<div class="col-4 text-end">
|
||||
<span><t t-esc="activity.date"/></span>
|
||||
</div>
|
||||
</div>
|
||||
</t>
|
||||
<a t-if="more_activities" class="float-end see_all_activities" href="#">See all activities</a>
|
||||
</t>
|
||||
|
||||
</templates>
|
||||
|
|
@ -0,0 +1,109 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
|
||||
<templates xml:space="preserve">
|
||||
|
||||
<t t-name="ShowPaymentInfo">
|
||||
<div>
|
||||
<t t-if="outstanding">
|
||||
<div>
|
||||
<strong class="float-start" id="outstanding" t-esc="title"/>
|
||||
</div>
|
||||
</t>
|
||||
<table style="width:100%;">
|
||||
<t t-foreach="lines" t-as="line">
|
||||
<tr>
|
||||
<t t-if="outstanding">
|
||||
<td>
|
||||
<a title="assign to invoice"
|
||||
role="button"
|
||||
class="oe_form_field btn btn-secondary outstanding_credit_assign"
|
||||
t-att-data-id="line.id"
|
||||
style="margin-right: 0px; padding-left: 5px; padding-right: 5px;"
|
||||
href="#"
|
||||
data-bs-toggle="tooltip">Add</a>
|
||||
</td>
|
||||
<td style="max-width: 11em;">
|
||||
<a t-att-title="line.date"
|
||||
role="button"
|
||||
class="oe_form_field btn btn-link open_account_move"
|
||||
t-att-move-id="line.move_id"
|
||||
style="margin-right: 5px; text-overflow: ellipsis; overflow: hidden; white-space: nowrap; padding-left: 0px; width:100%; text-align:left;"
|
||||
data-bs-toggle="tooltip"
|
||||
t-att-payment-id="account_payment_id"
|
||||
t-esc="line.journal_name"/>
|
||||
</td>
|
||||
</t>
|
||||
<t t-if="!outstanding">
|
||||
<td>
|
||||
<a role="button" tabindex="0" class="js_payment_info fa fa-info-circle" t-att-index="line.index" style="margin-right:5px;" aria-label="Info" title="Journal Entry Info" data-bs-toggle="tooltip"></a>
|
||||
</td>
|
||||
<td t-if="!line.is_exchange">
|
||||
<i class="o_field_widget text-start o_payment_label">Paid on <t t-esc="line.date"></t></i>
|
||||
</td>
|
||||
<td t-if="line.is_exchange" colspan="2">
|
||||
<i class="o_field_widget text-start text-muted text-start">
|
||||
<span class="oe_form_field oe_form_field_float oe_form_field_monetary fw-bold">
|
||||
<t t-if="line.position === 'before'">
|
||||
<t t-esc="line.currency"/>
|
||||
</t>
|
||||
<t t-esc="line.amount"/>
|
||||
<t t-if="line.position === 'after'">
|
||||
<t t-esc="line.currency"/>
|
||||
</t>
|
||||
</span>
|
||||
<span>Exchange Difference</span>
|
||||
</i>
|
||||
</td>
|
||||
</t>
|
||||
<td t-if="!line.is_exchange" style="text-align:right;">
|
||||
<span class="oe_form_field oe_form_field_float oe_form_field_monetary" style="margin-left: -10px;">
|
||||
<t t-if="line.position === 'before'">
|
||||
<t t-esc="line.currency"/>
|
||||
</t>
|
||||
<t t-esc="line.amount"/>
|
||||
<t t-if="line.position === 'after'">
|
||||
<t t-esc="line.currency"/>
|
||||
</t>
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
</t>
|
||||
</table>
|
||||
</div>
|
||||
</t>
|
||||
|
||||
<t t-name="PaymentPopOver">
|
||||
<div>
|
||||
<table>
|
||||
<tr>
|
||||
<td><strong>Amount: </strong></td>
|
||||
<td>
|
||||
<t t-esc="amount_company_currency"></t>
|
||||
<t t-if="amount_foreign_currency">
|
||||
(<span class="fa fa-money"/> <t t-esc="amount_foreign_currency"/>)
|
||||
</t>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><strong>Memo: </strong></td>
|
||||
<td>
|
||||
<div style="width: 200px; word-wrap: break-word">
|
||||
<t t-esc="ref"/>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><strong>Date: </strong></td>
|
||||
<td><t t-esc="date"/></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><strong>Journal: </strong></td>
|
||||
<td><t t-esc="journal_name"/><span t-if="payment_method_name"> (<t t-esc="payment_method_name"/>)</span></td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
<button class="btn btn-sm btn-primary js_unreconcile_payment float-start" t-if="!is_exchange" t-att-partial-id="partial_id" t-att-payment-id="payment_id" t-att-move-id="move_id" style="margin-top:5px; margin-bottom:5px;" groups="account.group_account_invoice">Unreconcile</button>
|
||||
<button class="btn btn-sm btn-secondary js_open_payment float-end" t-att-move-id="move_id" style="margin-top:5px; margin-bottom:5px;">View</button>
|
||||
</t>
|
||||
|
||||
</templates>
|
||||
|
|
@ -0,0 +1,17 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
|
||||
<templates>
|
||||
|
||||
<t t-name="accountTypeSelection">
|
||||
<t t-foreach="widget.hierarchyGroups" t-as="group">
|
||||
<optgroup t-att-label="group.name">
|
||||
<t t-if="group.children">
|
||||
<t t-foreach="group.children" t-as="child">
|
||||
<option t-att-value="JSON.stringify(child[0])" t-out="child[1]"/>
|
||||
</t>
|
||||
</t>
|
||||
</optgroup>
|
||||
</t>
|
||||
</t>
|
||||
|
||||
</templates>
|
||||
|
|
@ -0,0 +1,83 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<templates>
|
||||
|
||||
<t t-name="account.LegacyTaxGroupComponent" owl="1">
|
||||
<tr>
|
||||
<td class="o_td_label">
|
||||
<label class="o_form_label o_tax_total_label" t-esc="props.taxGroup.tax_group_name"/>
|
||||
</td>
|
||||
|
||||
<td>
|
||||
<t t-if="!props.readonly">
|
||||
<t t-if="['edit', 'disable'].includes(state.value)">
|
||||
<span class="o_tax_group_edit_input">
|
||||
<input
|
||||
type="text"
|
||||
t-ref="taxValueInput"
|
||||
class="o_field_float
|
||||
o_field_number o_input"
|
||||
t-att-disabled="state.value === 'disable'"
|
||||
t-on-blur.prevent="_onChangeTaxValue"/>
|
||||
</span>
|
||||
</t>
|
||||
<t t-else="">
|
||||
<span class="o_tax_group_edit" t-on-click.prevent="() => this.setState('edit')">
|
||||
<i class="fa fa-pencil"/>
|
||||
<span class="o_tax_group_amount_value">
|
||||
<t t-out="props.taxGroup.formatted_tax_group_amount"/>
|
||||
</span>
|
||||
</span>
|
||||
</t>
|
||||
</t>
|
||||
<t t-else="">
|
||||
<span class="o_tax_group_amount_value">
|
||||
<t t-out="props.taxGroup.formatted_tax_group_amount" style="white-space: nowrap;"/>
|
||||
</span>
|
||||
</t>
|
||||
</td>
|
||||
</tr>
|
||||
</t>
|
||||
|
||||
<div t-name="account.LegacyTaxTotalsField" owl="1">
|
||||
<table t-if="totals.value" class="oe_right">
|
||||
<tbody>
|
||||
<t t-foreach="totals.value.subtotals" t-as="subtotal" t-key="subtotal['name']">
|
||||
<tr>
|
||||
<td class="o_td_label">
|
||||
<label class="o_form_label o_tax_total_label" t-esc="subtotal['name']"/>
|
||||
</td>
|
||||
|
||||
<td>
|
||||
<span t-att-name="subtotal['name']" style="white-space: nowrap; font-weight: bold;" t-out="subtotal['formatted_amount']"/>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<t t-foreach="totals.value.groups_by_subtotal[subtotal['name']]" t-as="taxGroup" t-key="taxGroup.group_key">
|
||||
<LegacyTaxGroupComponent
|
||||
taxGroup="taxGroup"
|
||||
record="record"
|
||||
readonly="readonly"
|
||||
onChangeTaxGroup.bind="_onChangeTaxValueByTaxGroup"
|
||||
/>
|
||||
</t>
|
||||
</t>
|
||||
|
||||
<!-- Total amount with all taxes-->
|
||||
<tr>
|
||||
<td class="o_td_label">
|
||||
<label class="o_form_label o_tax_total_label">Total</label>
|
||||
</td>
|
||||
|
||||
<td>
|
||||
<span
|
||||
name="amount_total"
|
||||
t-att-class="Object.keys(totals.value.groups_by_subtotal).length > 0 ? 'oe_subtotal_footer_separator' : ''"
|
||||
t-out="totals.value.formatted_amount_total"
|
||||
style="white-space: nowrap; font-weight: bold; font-size: 1.3em;"
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</templates>
|
||||
Loading…
Add table
Add a link
Reference in a new issue