mirror of
https://github.com/bringout/oca-ocb-accounting.git
synced 2026-04-21 19:42:00 +02:00
19.0 vanilla
This commit is contained in:
parent
ba20ce7443
commit
768b70e05e
2357 changed files with 1057103 additions and 712486 deletions
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
Before Width: | Height: | Size: 6.4 KiB After Width: | Height: | Size: 1.9 KiB |
|
|
@ -0,0 +1 @@
|
|||
<svg width="50" height="50" viewBox="0 0 50 50" xmlns="http://www.w3.org/2000/svg"><path d="M38 46H12c-4.418 0-8-3.498-8-7.814V4h30c2.21 0 4 1.75 4 3.907V46Z" fill="#2EBCFA"/><path d="M12 46h26a7.999 7.999 0 0 0 8-8H20a8 8 0 0 1-8 8Z" fill="#144496"/><path d="M38 8C23.408 8 10.687 16.014 4 27.88V4h30a4 4 0 0 1 4 4Z" fill="#088BF5"/><path d="M20.425 34h1.181v-1.707c3.23-.198 5.394-1.791 5.394-4.435v-.021c0-2.332-1.495-3.498-4.442-4.143l-.952-.198v-3.02c1.035.167 1.715.74 1.86 1.708l.011.02 3.283-.01.01-.01c-.115-2.53-2.007-4.216-5.164-4.435V16h-1.18v1.739c-3.074.145-5.28 1.728-5.28 4.33v.021c0 2.343 1.547 3.602 4.338 4.216l.941.198v3.082c-1.223-.115-1.934-.625-2.101-1.54l-.01-.022-3.293.01-.021.011c.084 2.665 2.237 4.133 5.425 4.258V34Zm-1.777-12.19v-.022c0-.75.606-1.228 1.777-1.332v2.769c-1.213-.323-1.777-.729-1.777-1.416Zm4.85 6.423v.02c0 .802-.68 1.229-1.892 1.333v-2.821c1.36.343 1.892.697 1.892 1.468Z" fill="#fff"/></svg>
|
||||
|
After Width: | Height: | Size: 939 B |
Binary file not shown.
|
After Width: | Height: | Size: 10 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 4.5 KiB |
|
|
@ -0,0 +1 @@
|
|||
<svg width="50" height="50" viewBox="0 0 50 50" xmlns="http://www.w3.org/2000/svg"><g clip-path="url(#o_icon_l10n__a)"><path fill-rule="evenodd" clip-rule="evenodd" d="M25 0c13.807 0 25 11.193 25 25S38.807 50 25 50 0 38.807 0 25 11.193 0 25 0Zm0 48c2.105 0 4.598-1.844 6.682-6.188 1.047-2.181 1.902-4.832 2.481-7.812H15.837c.58 2.98 1.434 5.631 2.48 7.812C20.403 46.156 22.896 48 25 48Zm-9.503-16h19.006c.322-2.202.497-4.552.497-7 0-2.448-.175-4.798-.497-7H15.497A48.566 48.566 0 0 0 15 25c0 2.448.175 4.798.497 7Zm.34-16c.58-2.98 1.434-5.631 2.48-7.812C20.403 3.845 22.896 2 25 2c2.105 0 4.598 1.845 6.682 6.188 1.047 2.181 1.902 4.832 2.481 7.812H15.837Zm20.686 2c.31 2.221.477 4.57.477 7 0 2.43-.166 4.779-.477 7h10.392A22.987 22.987 0 0 0 48 25c0-2.44-.38-4.793-1.085-7H36.523Zm9.65-2h-9.974c-1.08-5.826-3.177-10.599-5.837-13.372C37.502 4.333 43.35 9.368 46.172 16ZM13.8 16H3.828c2.822-6.632 8.67-11.667 15.81-13.372-2.66 2.773-4.757 7.546-5.837 13.372ZM3.085 18h10.392A50.707 50.707 0 0 0 13 25c0 2.43.166 4.779.477 7H3.085A22.986 22.986 0 0 1 2 25c0-2.44.38-4.793 1.085-7Zm27.277 29.372c2.66-2.773 4.757-7.546 5.837-13.372h9.974c-2.823 6.632-8.67 11.667-15.811 13.372Zm-10.724 0C12.498 45.667 6.65 40.632 3.828 34H13.8c1.08 5.826 3.177 10.599 5.837 13.372Z" fill="#1AD3BB"/></g><defs><clipPath id="o_icon_l10n__a"><path fill="#fff" d="M0 0h50v50H0z"/></clipPath></defs></svg>
|
||||
|
After Width: | Height: | Size: 1.3 KiB |
|
|
@ -0,0 +1,21 @@
|
|||
import {Component} from "@odoo/owl";
|
||||
import {registry} from "@web/core/registry";
|
||||
import { standardFieldProps } from "@web/views/fields/standard_field_props";
|
||||
|
||||
export class AccountBatchSendingSummary extends Component {
|
||||
static template = "account.BatchSendingSummary";
|
||||
static props = {
|
||||
...standardFieldProps,
|
||||
};
|
||||
|
||||
setup() {
|
||||
super.setup();
|
||||
this.data = this.props.record.data[this.props.name];
|
||||
}
|
||||
}
|
||||
|
||||
export const accountBatchSendingSummary = {
|
||||
component: AccountBatchSendingSummary,
|
||||
}
|
||||
|
||||
registry.category("fields").add("account_batch_sending_summary", accountBatchSendingSummary);
|
||||
|
|
@ -0,0 +1,15 @@
|
|||
<?xml version="1.0" encoding="UTF-8" ?>
|
||||
<template>
|
||||
|
||||
<t t-name="account.BatchSendingSummary">
|
||||
<p>You are about to send</p>
|
||||
<ul>
|
||||
<li t-foreach="this.data" t-as="summary_entry" t-key="summary_entry">
|
||||
<t t-out="summary_entry_value.count"/> invoice(s)
|
||||
<t t-out="summary_entry_value.label"/>
|
||||
<t t-if="summary_entry_value.extra" t-out="summary_entry_value.extra"/>
|
||||
</li>
|
||||
</ul>
|
||||
</t>
|
||||
|
||||
</template>
|
||||
|
|
@ -0,0 +1,48 @@
|
|||
import { _t } from "@web/core/l10n/translation";
|
||||
import { registry } from "@web/core/registry";
|
||||
import { DocumentFileUploader } from "../document_file_uploader/document_file_uploader";
|
||||
|
||||
export class AccountFileUploader extends DocumentFileUploader {
|
||||
static template = "account.AccountFileUploader";
|
||||
static props = {
|
||||
...DocumentFileUploader.props,
|
||||
btnClass: { type: String, optional: true },
|
||||
linkText: { type: String, optional: true },
|
||||
togglerTemplate: { type: String, optional: true },
|
||||
};
|
||||
|
||||
getExtraContext() {
|
||||
const extraContext = super.getExtraContext();
|
||||
const record_data = this.props.record ? this.props.record.data : false;
|
||||
return record_data ? {
|
||||
...extraContext,
|
||||
default_journal_id: record_data.id,
|
||||
default_move_type: (
|
||||
(record_data.type === 'sale' && 'out_invoice')
|
||||
|| (record_data.type === 'purchase' && 'in_invoice')
|
||||
|| 'entry'
|
||||
),
|
||||
} : extraContext;
|
||||
|
||||
}
|
||||
|
||||
getResModel() {
|
||||
return "account.journal";
|
||||
}
|
||||
}
|
||||
|
||||
//when file uploader is used on account.journal (with a record)
|
||||
export const accountFileUploader = {
|
||||
component: AccountFileUploader,
|
||||
extractProps: ({ attrs }) => ({
|
||||
togglerTemplate: attrs.template || "account.JournalUploadLink",
|
||||
btnClass: attrs.btnClass || "",
|
||||
linkText: attrs.title || _t("Upload"),
|
||||
}),
|
||||
fieldDependencies: [
|
||||
{ name: "id", type: "integer" },
|
||||
{ name: "type", type: "selection" },
|
||||
],
|
||||
};
|
||||
|
||||
registry.category("view_widgets").add("account_file_uploader", accountFileUploader);
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
.o_widget_account_file_uploader {
|
||||
button.oe_kanban_action {
|
||||
a {
|
||||
color: var(--btn-color);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,26 @@
|
|||
<templates>
|
||||
|
||||
<t t-name="account.AccountFileUploader" t-inherit="account.DocumentFileUploader" t-inherit-mode="primary">
|
||||
<xpath expr="//t[@t-slot='toggler']" position="replace">
|
||||
<t t-if="props.togglerTemplate" t-call="{{ props.togglerTemplate }}"/>
|
||||
<t t-else="" t-slot="toggler"/>
|
||||
</xpath>
|
||||
</t>
|
||||
|
||||
<t t-name="account.AccountViewUploadButton">
|
||||
<AccountFileUploader>
|
||||
<t t-set-slot="toggler">
|
||||
<button type="button" class="btn btn-secondary o_button_upload_bill" data-hotkey="shift+i">
|
||||
Upload
|
||||
</button>
|
||||
</t>
|
||||
</AccountFileUploader>
|
||||
</t>
|
||||
|
||||
<t t-name="account.JournalUploadLink">
|
||||
<t groups="account.group_account_invoice">
|
||||
<a t-att-class="props.btnClass" href="#" t-out="props.linkText" draggable="false"/>
|
||||
</t>
|
||||
</t>
|
||||
|
||||
</templates>
|
||||
|
|
@ -0,0 +1,61 @@
|
|||
import { registry } from "@web/core/registry";
|
||||
import {
|
||||
SectionAndNoteListRenderer,
|
||||
SectionAndNoteFieldOne2Many,
|
||||
sectionAndNoteFieldOne2Many,
|
||||
} from "../section_and_note_fields_backend/section_and_note_fields_backend";
|
||||
|
||||
export class AccountMergeWizardLinesRenderer extends SectionAndNoteListRenderer {
|
||||
setup() {
|
||||
super.setup();
|
||||
this.titleField = "info";
|
||||
}
|
||||
|
||||
getCellClass(column, record) {
|
||||
const classNames = super.getCellClass(column, record);
|
||||
// Even though the `is_selected` field is invisible for section lines, we should
|
||||
// keep its column (which would be hidden by the call to super.getCellClass)
|
||||
// in order to align the section header name with the account names.
|
||||
if (this.isSectionOrNote(record) && column.name === "is_selected") {
|
||||
return classNames.replace(" o_hidden", "");
|
||||
}
|
||||
return classNames;
|
||||
}
|
||||
|
||||
/** @override **/
|
||||
getSectionColumns(columns) {
|
||||
const sectionCols = columns.filter(
|
||||
(col) =>
|
||||
col.type === "field" && (col.name === this.titleField || col.name === "is_selected")
|
||||
);
|
||||
return sectionCols.map((col) => {
|
||||
if (col.name === this.titleField) {
|
||||
return { ...col, colspan: columns.length - sectionCols.length + 1 };
|
||||
} else {
|
||||
return { ...col };
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/** @override */
|
||||
isSortable(column) {
|
||||
// Don't allow sorting columns, as that doesn't make sense in the wizard view.
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
export class AccountMergeWizardLinesOne2Many extends SectionAndNoteFieldOne2Many {
|
||||
static components = {
|
||||
...SectionAndNoteFieldOne2Many.components,
|
||||
ListRenderer: AccountMergeWizardLinesRenderer,
|
||||
};
|
||||
}
|
||||
|
||||
export const accountMergeWizardLinesOne2Many = {
|
||||
...sectionAndNoteFieldOne2Many,
|
||||
component: AccountMergeWizardLinesOne2Many,
|
||||
};
|
||||
|
||||
registry
|
||||
.category("fields")
|
||||
.add("account_merge_wizard_lines_one2many", accountMergeWizardLinesOne2Many);
|
||||
|
|
@ -1,5 +1,3 @@
|
|||
/** @odoo-module **/
|
||||
|
||||
import { registry } from "@web/core/registry";
|
||||
import { createElement, append } from "@web/core/utils/xml";
|
||||
import { Notebook } from "@web/core/notebook/notebook";
|
||||
|
|
@ -8,27 +6,43 @@ 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";
|
||||
import { deleteConfirmationMessage } from "@web/core/confirmation_dialog/confirmation_dialog";
|
||||
import {_t} from "@web/core/l10n/translation";
|
||||
|
||||
export class AccountMoveController extends FormController {
|
||||
|
||||
export class AccountMoveFormController 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);
|
||||
}
|
||||
get cogMenuProps() {
|
||||
return {
|
||||
...super.cogMenuProps,
|
||||
printDropdownTitle: _t("Print"),
|
||||
loadExtraPrintItems: this.loadExtraPrintItems.bind(this),
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
async loadExtraPrintItems() {
|
||||
const items = await this.orm.call("account.move", "get_extra_print_items", [this.model.root.resId]);
|
||||
return items.filter((item) => item.key !== "download_all");
|
||||
}
|
||||
|
||||
|
||||
async deleteRecord() {
|
||||
const deleteConfirmationDialogProps = this.deleteConfirmationDialogProps;
|
||||
deleteConfirmationDialogProps.body = await this.account_move_service.getDeletionDialogBody(deleteConfirmationMessage, this.model.root.resId);
|
||||
this.deleteRecordsWithConfirmation(deleteConfirmationDialogProps, [this.model.root]);
|
||||
}
|
||||
}
|
||||
|
||||
export class AccountMoveFormNotebook extends Notebook {
|
||||
onAnchorClicked(ev) {
|
||||
if (ev.detail.detail.id === "#outstanding") {
|
||||
ev.preventDefault();
|
||||
ev.detail.detail.originalEv.preventDefault();
|
||||
}
|
||||
}
|
||||
static template = "account.AccountMoveFormNotebook";
|
||||
static props = {
|
||||
...Notebook.props,
|
||||
onBeforeTabSwitch: { type: Function, optional: true },
|
||||
};
|
||||
|
||||
async changeTabTo(page_id) {
|
||||
if (this.props.onBeforeTabSwitch) {
|
||||
|
|
@ -37,29 +51,25 @@ export class AccountMoveFormNotebook extends Notebook {
|
|||
this.state.currentPage = page_id;
|
||||
}
|
||||
}
|
||||
AccountMoveFormNotebook.template = "account.AccountMoveFormNotebook";
|
||||
AccountMoveFormNotebook.props = {
|
||||
...Notebook.props,
|
||||
onBeforeTabSwitch: { type: Function, optional: true },
|
||||
}
|
||||
|
||||
export class AccountMoveFormRenderer extends FormRenderer {
|
||||
static components = {
|
||||
...FormRenderer.components,
|
||||
AccountMoveFormNotebook: AccountMoveFormNotebook,
|
||||
};
|
||||
|
||||
async saveBeforeTabChange() {
|
||||
if (this.props.record.mode === "edit" && this.props.record.isDirty) {
|
||||
if (this.props.record.isInEdition && await this.props.record.isDirty()) {
|
||||
const contentEl = document.querySelector('.o_content');
|
||||
const scrollPos = contentEl.scrollTop;
|
||||
await this.props.record.save({
|
||||
stayInEdition: true,
|
||||
});
|
||||
await this.props.record.save();
|
||||
if (scrollPos) {
|
||||
contentEl.scrollTop = scrollPos;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
AccountMoveFormRenderer.components = {
|
||||
...FormRenderer.components,
|
||||
AccountMoveFormNotebook: AccountMoveFormNotebook,
|
||||
}
|
||||
|
||||
export class AccountMoveFormCompiler extends FormCompiler {
|
||||
compileNotebook(el, params) {
|
||||
const originalNoteBook = super.compileNotebook(...arguments);
|
||||
|
|
@ -67,7 +77,7 @@ export class AccountMoveFormCompiler extends FormCompiler {
|
|||
for (const attr of originalNoteBook.attributes) {
|
||||
noteBook.setAttribute(attr.name, attr.value);
|
||||
}
|
||||
noteBook.setAttribute("onBeforeTabSwitch", "() => this.saveBeforeTabChange()");
|
||||
noteBook.setAttribute("onBeforeTabSwitch", "() => __comp__.saveBeforeTabChange()");
|
||||
const slots = originalNoteBook.childNodes;
|
||||
append(noteBook, [...slots]);
|
||||
return noteBook;
|
||||
|
|
@ -78,7 +88,7 @@ export const AccountMoveFormView = {
|
|||
...formView,
|
||||
Renderer: AccountMoveFormRenderer,
|
||||
Compiler: AccountMoveFormCompiler,
|
||||
Controller: AccountMoveController,
|
||||
Controller: AccountMoveFormController,
|
||||
};
|
||||
|
||||
registry.category("views").add("account_move_form", AccountMoveFormView);
|
||||
|
|
|
|||
|
|
@ -1,9 +1,10 @@
|
|||
<?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">
|
||||
<t t-name="account.AccountMoveFormNotebook" t-inherit="web.Notebook" t-inherit-mode="primary">
|
||||
<xpath expr="//a[@class='nav-link']" position="attributes">
|
||||
<attribute name="t-on-click.prevent">() => this.changeTabTo(navItem[0])</attribute>
|
||||
<attribute name="tabindex">-1</attribute>
|
||||
</xpath>
|
||||
</t>
|
||||
</templates>
|
||||
|
|
|
|||
|
|
@ -1,28 +0,0 @@
|
|||
/** @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);
|
||||
|
|
@ -2,95 +2,112 @@
|
|||
|
||||
<templates xml:space="preserve">
|
||||
|
||||
<t t-name="account.AccountPaymentField" owl="1">
|
||||
<t t-name="account.AccountPaymentField">
|
||||
<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-set="info" t-value="this.getInfo()"/>
|
||||
<div class="d-flex flex-column align-items-end">
|
||||
<table class="w-auto">
|
||||
<t t-if="info.outstanding">
|
||||
<tr>
|
||||
<td colspan="2" class="text-start">
|
||||
<strong id="outstanding" t-out="info.title"/>
|
||||
</td>
|
||||
</tr>
|
||||
</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>
|
||||
<t t-foreach="info.lines" t-as="line" t-key="line_index">
|
||||
<tr>
|
||||
<t t-if="info.outstanding">
|
||||
<td t-out="line.formattedDate"/>
|
||||
<td style="max-width: 9rem;">
|
||||
<a t-att-title="(line.bank_label ? line.bank_label + ' - ' : '') + (line.move_ref ? line.move_ref : '')"
|
||||
role="button"
|
||||
class="open_account_move oe_form_field btn btn-link w-100 text-start"
|
||||
t-on-click="() => this.openMove(line.move_id)"
|
||||
data-bs-toggle="tooltip"
|
||||
t-att-payment-id="account_payment_id"
|
||||
t-out="line.journal_name"/>
|
||||
</td>
|
||||
<td class="ps-2">
|
||||
<a title="assign to invoice"
|
||||
role="button"
|
||||
class="oe_form_field btn btn-secondary outstanding_credit_assign d-print-none text-truncate w-100 text-start"
|
||||
t-att-data-id="line.id"
|
||||
href="#"
|
||||
data-bs-toggle="tooltip"
|
||||
t-on-click.prevent="() => this.assignOutstandingCredit(info.moveId, line.id)">Add</a>
|
||||
</td>
|
||||
</t>
|
||||
<t t-if="!info.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)"></a>
|
||||
</td>
|
||||
<td t-if="!line.is_exchange">
|
||||
<i class="o_field_widget text-start o_payment_label">
|
||||
<t t-if="line.is_refund">Reversed on </t>
|
||||
<t t-else="">Paid on </t>
|
||||
<t t-out="line.formattedDate"></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-out="line.amount_formatted"/>
|
||||
</span>
|
||||
<span> Exchange Difference</span>
|
||||
</i>
|
||||
</td>
|
||||
</t>
|
||||
<td t-if="!line.is_exchange" class="text-end ps-2 text-nowrap">
|
||||
<span class="oe_form_field oe_form_field_float oe_form_field_monetary">
|
||||
<t t-out="line.amount_formatted"/>
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
</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>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</t>
|
||||
|
||||
<t t-name="account.AccountPaymentPopOver" owl="1">
|
||||
<t t-name="account.AccountPaymentPopOver">
|
||||
<div class="account_payment_popover">
|
||||
<h3 t-if="props.title" class="o_popover_header"><t t-esc="props.title"/></h3>
|
||||
<h3 t-if="props.title" class="popover-header"><t t-out="props.title"/></h3>
|
||||
<div class="px-2">
|
||||
<div>
|
||||
<table>
|
||||
<tr>
|
||||
<td><strong>Amount: </strong></td>
|
||||
<td>
|
||||
<t t-esc="props.amount_company_currency"></t>
|
||||
<td class="fw-bolder">Amount:</td>
|
||||
<td class="ps-1">
|
||||
<t t-out="props.amount_company_currency"></t>
|
||||
<t t-if="props.amount_foreign_currency">
|
||||
(<span class="fa fa-money"/> <t t-esc="props.amount_foreign_currency"/>)
|
||||
(<span class="fa fa-money"/> <t t-out="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"/>
|
||||
<td class="fw-bolder align-top">Memo:</td>
|
||||
<td class="ps-1">
|
||||
<div class="o_memo_content" t-att-data-tooltip="props.ref" data-tooltip-position="left">
|
||||
<t t-out="props.ref"/>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><strong>Date: </strong></td>
|
||||
<td><t t-esc="props.date"/></td>
|
||||
<td class="fw-bolder">Date:</td>
|
||||
<td class="ps-1"><t t-out="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>
|
||||
<td class="fw-bolder">Journal:</td>
|
||||
<td class="ps-1">
|
||||
<t t-out="props.journal_name"/>
|
||||
<span t-if="props.payment_method_name">
|
||||
(<t t-out="props.payment_method_name"/>)
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
<tr t-if="props.company_name">
|
||||
<td class="fw-bolder">Branch:</td>
|
||||
<td class="ps-1"><t t-out="props.company_name"/></td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,85 +1,73 @@
|
|||
/** @odoo-module **/
|
||||
|
||||
import { _t } from "@web/core/l10n/translation";
|
||||
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 { formatDate, deserializeDate } from "@web/core/l10n/dates";
|
||||
|
||||
import { formatMonetary } from "@web/views/fields/formatters";
|
||||
import { standardFieldProps } from "@web/views/fields/standard_field_props";
|
||||
import { Component } from "@odoo/owl";
|
||||
|
||||
const { Component, onWillUpdateProps } = owl;
|
||||
|
||||
class AccountPaymentPopOver extends Component {}
|
||||
AccountPaymentPopOver.template = "account.AccountPaymentPopOver";
|
||||
class AccountPaymentPopOver extends Component {
|
||||
static props = { "*": { optional: true } };
|
||||
static template = "account.AccountPaymentPopOver";
|
||||
}
|
||||
|
||||
export class AccountPaymentField extends Component {
|
||||
static props = { ...standardFieldProps };
|
||||
static template = "account.AccountPaymentField";
|
||||
|
||||
setup() {
|
||||
this.popover = usePopover();
|
||||
const position = localization.direction === "rtl" ? "bottom" : "left";
|
||||
this.popover = usePopover(AccountPaymentPopOver, { position });
|
||||
this.orm = useService("orm");
|
||||
this.action = useService("action");
|
||||
|
||||
this.formatData(this.props);
|
||||
onWillUpdateProps((nextProps) => this.formatData(nextProps));
|
||||
}
|
||||
|
||||
formatData(props) {
|
||||
const info = props.value || {
|
||||
getInfo() {
|
||||
const info = this.props.record.data[this.props.name] || {
|
||||
content: [],
|
||||
outstanding: false,
|
||||
title: "",
|
||||
move_id: this.props.record.data.id,
|
||||
move_id: this.props.record.resId,
|
||||
};
|
||||
for (let [key, value] of Object.entries(info.content)) {
|
||||
for (const [key, value] of Object.entries(info.content)) {
|
||||
value.index = key;
|
||||
value.amount_formatted = formatMonetary(value.amount, { currencyId: value.currency_id });
|
||||
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));
|
||||
value.formattedDate = formatDate(deserializeDate(value.date))
|
||||
}
|
||||
}
|
||||
this.lines = info.content;
|
||||
this.outstanding = info.outstanding;
|
||||
this.title = info.title;
|
||||
this.move_id = info.move_id;
|
||||
return {
|
||||
lines: info.content,
|
||||
outstanding: info.outstanding,
|
||||
title: info.title,
|
||||
moveId: 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",
|
||||
},
|
||||
);
|
||||
onInfoClick(ev, line) {
|
||||
this.popover.open(ev.currentTarget, {
|
||||
title: _t("Journal Entry Info"),
|
||||
...line,
|
||||
_onRemoveMoveReconcile: this.removeMoveReconcile.bind(this),
|
||||
_onOpenMove: this.openMove.bind(this),
|
||||
});
|
||||
}
|
||||
|
||||
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], {});
|
||||
async assignOutstandingCredit(moveId, id) {
|
||||
await this.orm.call(this.props.record.resModel, 'js_assign_outstanding_line', [moveId, id], {});
|
||||
await this.props.record.model.root.load();
|
||||
this.props.record.model.notify();
|
||||
}
|
||||
|
||||
async removeMoveReconcile(moveId, partialId) {
|
||||
this.closePopover();
|
||||
this.popover.close();
|
||||
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) {
|
||||
|
|
@ -87,7 +75,10 @@ export class AccountPaymentField extends Component {
|
|||
this.action.doAction(action);
|
||||
}
|
||||
}
|
||||
AccountPaymentField.template = "account.AccountPaymentField";
|
||||
AccountPaymentField.supportedTypes = ["char"];
|
||||
|
||||
registry.category("fields").add("payment", AccountPaymentField);
|
||||
export const accountPaymentField = {
|
||||
component: AccountPaymentField,
|
||||
supportedTypes: ["binary"],
|
||||
};
|
||||
|
||||
registry.category("fields").add("payment", accountPaymentField);
|
||||
|
|
|
|||
|
|
@ -0,0 +1,23 @@
|
|||
import { Component } from "@odoo/owl";
|
||||
import { registry } from "@web/core/registry";
|
||||
import { standardFieldProps } from "@web/views/fields/standard_field_props";
|
||||
|
||||
class AccountPaymentRegisterHtmlField extends Component {
|
||||
static props = standardFieldProps;
|
||||
static template = "account.AccountPaymentRegisterHtmlField";
|
||||
|
||||
get value() {
|
||||
return this.props.record.data[this.props.name];
|
||||
}
|
||||
|
||||
switchInstallmentsAmount(ev) {
|
||||
if (ev.srcElement.classList.contains("installments_switch_button")) {
|
||||
const root = this.env.model.root;
|
||||
root.update({ amount: root.data.installments_switch_amount });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const accountPaymentRegisterHtmlField = { component: AccountPaymentRegisterHtmlField };
|
||||
|
||||
registry.category("fields").add("account_payment_register_html", accountPaymentRegisterHtmlField);
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<templates xml:space="preserve">
|
||||
<t t-name="account.AccountPaymentRegisterHtmlField">
|
||||
<div t-out="value" t-on-click="switchInstallmentsAmount"/>
|
||||
</t>
|
||||
</templates>
|
||||
|
|
@ -0,0 +1,25 @@
|
|||
import { registry } from "@web/core/registry";
|
||||
|
||||
import { X2ManyField, x2ManyField } from "@web/views/fields/x2many/x2many_field";
|
||||
import { useAddInlineRecord } from "@web/views/fields/relational_utils";
|
||||
|
||||
export class PaymentTermLineIdsOne2Many extends X2ManyField {
|
||||
setup() {
|
||||
super.setup();
|
||||
// Overloads the addInLine method to mark all new records as 'dirty' by calling update with an empty object.
|
||||
// This prevents the records from being abandoned if the user clicks globally or on an existing record.
|
||||
this.addInLine = useAddInlineRecord({
|
||||
addNew: async (...args) => {
|
||||
const newRecord = await this.list.addNewRecord(...args);
|
||||
newRecord.update({});
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export const PaymentTermLineIds = {
|
||||
...x2ManyField,
|
||||
component: PaymentTermLineIdsOne2Many,
|
||||
}
|
||||
|
||||
registry.category("fields").add("payment_term_line_ids", PaymentTermLineIds);
|
||||
|
|
@ -0,0 +1,44 @@
|
|||
import { Component } from "@odoo/owl";
|
||||
import { registry } from "@web/core/registry";
|
||||
import { useDateTimePicker } from "@web/core/datetime/datetime_picker_hook";
|
||||
import { useService } from "@web/core/utils/hooks";
|
||||
import { today } from "@web/core/l10n/dates";
|
||||
import { standardWidgetProps } from "@web/views/widgets/standard_widget_props";
|
||||
|
||||
|
||||
export class AccountPickCurrencyDate extends Component {
|
||||
static template = "account.AccountPickCurrencyDate";
|
||||
static props = {
|
||||
...standardWidgetProps,
|
||||
record: { type: Object, optional: true },
|
||||
};
|
||||
|
||||
setup() {
|
||||
this.orm = useService("orm");
|
||||
this.dateTimePicker = useDateTimePicker({
|
||||
target: 'datetime-picker-target',
|
||||
onApply: async (date) => {
|
||||
const record = this.props.record
|
||||
const rate = await this.orm.call(
|
||||
'account.move',
|
||||
'get_currency_rate',
|
||||
[record.resId, record.data.company_id.id, record.data.currency_id.id, date.toISODate()],
|
||||
);
|
||||
this.props.record.update({ invoice_currency_rate: rate });
|
||||
await this.props.record.save();
|
||||
},
|
||||
get pickerProps() {
|
||||
return {
|
||||
type: 'date',
|
||||
value: today(),
|
||||
};
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export const accountPickCurrencyDate = {
|
||||
component: AccountPickCurrencyDate,
|
||||
}
|
||||
|
||||
registry.category("view_widgets").add("account_pick_currency_date", accountPickCurrencyDate);
|
||||
|
|
@ -0,0 +1,15 @@
|
|||
<?xml version="1.0" encoding="UTF-8" ?>
|
||||
<template>
|
||||
<t t-name="account.AccountPickCurrencyDate">
|
||||
<button
|
||||
type="button"
|
||||
t-on-click.prevent="() => this.dateTimePicker.open()"
|
||||
class="btn btn-link text-dark p-0"
|
||||
title="Pick the rate on a certain date"
|
||||
t-ref="datetime-picker-target"
|
||||
>
|
||||
<i class="fa fa-calendar"/>
|
||||
</button>
|
||||
</t>
|
||||
|
||||
</template>
|
||||
|
|
@ -1,27 +1,28 @@
|
|||
<?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">
|
||||
<t t-name="account.ResequenceRenderer" >
|
||||
<t t-set="value" t-value="this.getValue()"/>
|
||||
<table t-if="value.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 t-foreach="value.changeLines" t-as="changeLine" t-key="changeLine.id">
|
||||
<ChangeLine changeLine="changeLine" ordering="value.ordering"/>
|
||||
</t>
|
||||
</tbody>
|
||||
</table>
|
||||
</t>
|
||||
|
||||
<t t-name="account.ResequenceChangeLine" owl="1">
|
||||
<t t-name="account.ResequenceChangeLine">
|
||||
<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' : ''}}"/>
|
||||
<td t-out="props.changeLine.date"/>
|
||||
<td t-out="props.changeLine.current_name"/>
|
||||
<td t-if="props.ordering == 'keep'" t-out="props.changeLine.new_by_name" t-attf-class="{{ props.changeLine.new_by_name != props.changeLine.new_by_date ? 'animate' : ''}}"/>
|
||||
<td t-else="" t-out="props.changeLine.new_by_date" t-attf-class="{{ props.changeLine.new_by_name != props.changeLine.new_by_date ? 'animate' : ''}}"/>
|
||||
</tr>
|
||||
</t>
|
||||
</templates>
|
||||
|
|
|
|||
|
|
@ -1,24 +1,22 @@
|
|||
/** @odoo-module */
|
||||
|
||||
import { registry } from "@web/core/registry";
|
||||
import { Component } from "@odoo/owl";
|
||||
import { standardFieldProps } from "@web/views/fields/standard_field_props";
|
||||
|
||||
const { Component, onWillUpdateProps } = owl;
|
||||
|
||||
class ChangeLine extends Component {}
|
||||
ChangeLine.template = "account.ResequenceChangeLine";
|
||||
ChangeLine.props = ["changeLine", "ordering"];
|
||||
class ChangeLine extends Component {
|
||||
static template = "account.ResequenceChangeLine";
|
||||
static 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" };
|
||||
static template = "account.ResequenceRenderer";
|
||||
static components = { ChangeLine };
|
||||
static props = { ...standardFieldProps };
|
||||
getValue() {
|
||||
const value = this.props.record.data[this.props.name];
|
||||
return value ? JSON.parse(value) : { changeLines: [], ordering: "date" };
|
||||
}
|
||||
}
|
||||
ShowResequenceRenderer.template = "account.ResequenceRenderer";
|
||||
ShowResequenceRenderer.components = { ChangeLine };
|
||||
|
||||
registry.category("fields").add("account_resequence_widget", ShowResequenceRenderer);
|
||||
registry.category("fields").add("account_resequence_widget", {
|
||||
component: ShowResequenceRenderer,
|
||||
});
|
||||
|
|
|
|||
|
|
@ -0,0 +1,25 @@
|
|||
import { _t } from "@web/core/l10n/translation";
|
||||
import { registry } from "@web/core/registry";
|
||||
import { statusBarField, StatusBarField } from "@web/views/fields/statusbar/statusbar_field";
|
||||
|
||||
export class AccountMoveStatusBarSecuredField extends StatusBarField {
|
||||
static template = "account.MoveStatusBarSecuredField";
|
||||
|
||||
get isSecured() {
|
||||
return this.props.record.data['secured'];
|
||||
}
|
||||
|
||||
get currentItem() {
|
||||
return this.getAllItems().find((item) => item.isSelected);
|
||||
}
|
||||
}
|
||||
|
||||
export const accountMoveStatusBarSecuredField = {
|
||||
...statusBarField,
|
||||
component: AccountMoveStatusBarSecuredField,
|
||||
displayName: _t("Status with secured indicator for Journal Entries"),
|
||||
supportedTypes: ["selection"],
|
||||
additionalClasses: ["o_field_statusbar"],
|
||||
};
|
||||
|
||||
registry.category("fields").add("account_move_statusbar_secured", accountMoveStatusBarSecuredField);
|
||||
|
|
@ -0,0 +1,35 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<templates xml:space="preserve">
|
||||
<!-- Add "secured" indicator to the posted state -->
|
||||
|
||||
<t t-name="account.MoveStatusBarSecuredField.ItemLabel">
|
||||
<span t-esc="item.label" />
|
||||
<t t-if="item.value == 'posted'">
|
||||
<i t-attf-class="fa fa-fw ms-1 #{isSecured ? 'fa-lock text-success' : 'fa-unlock text-warning'}"/>
|
||||
</t>
|
||||
</t>
|
||||
|
||||
<t t-name="account.MoveStatusBarSecuredField.Dropdown" t-inherit="web.StatusBarField.Dropdown" t-inherit-mode="primary">
|
||||
<xpath expr="//span[@t-esc='item.label']" position="replace">
|
||||
<t t-call="account.MoveStatusBarSecuredField.ItemLabel"/>
|
||||
</xpath>
|
||||
</t>
|
||||
|
||||
<t t-name="account.MoveStatusBarSecuredField" t-inherit="web.StatusBarField" t-inherit-mode="primary">
|
||||
<xpath expr="//*[@t-call='web.StatusBarField.Dropdown']" position="attributes">
|
||||
<attribute name="t-call">account.MoveStatusBarSecuredField.Dropdown</attribute>
|
||||
</xpath>
|
||||
|
||||
<xpath expr="//*[@t-esc='item.label']" position="inside">
|
||||
<t t-call="account.MoveStatusBarSecuredField.ItemLabel"/>
|
||||
</xpath>
|
||||
<xpath expr="//*[@t-esc='item.label']" position="attributes">
|
||||
<attribute name="t-esc" />
|
||||
</xpath>
|
||||
|
||||
<xpath expr="//*[@t-out='getCurrentLabel()']" position="replace">
|
||||
<t t-set="item" t-value="currentItem"/>
|
||||
<t t-call="account.MoveStatusBarSecuredField.ItemLabel"/>
|
||||
</xpath>
|
||||
</t>
|
||||
</templates>
|
||||
|
|
@ -0,0 +1,54 @@
|
|||
import { FloatField, floatField } from "@web/views/fields/float/float_field";
|
||||
import { roundPrecision } from "@web/core/utils/numbers";
|
||||
import {registry} from "@web/core/registry";
|
||||
|
||||
export class AccountTaxRepartitionLineFactorPercent extends FloatField {
|
||||
static defaultProps = {
|
||||
...FloatField.defaultProps,
|
||||
digits: [16, 12],
|
||||
};
|
||||
|
||||
/*
|
||||
* @override
|
||||
* We don't want to display all amounts with 12 digits behind so we remove the trailing 0
|
||||
* as much as possible.
|
||||
*/
|
||||
get formattedValue() {
|
||||
const value = super.formattedValue;
|
||||
const trailingNumbersMatch = value.match(/(\d+)$/);
|
||||
if (!trailingNumbersMatch) {
|
||||
return value;
|
||||
}
|
||||
const trailingZeroMatch = trailingNumbersMatch[1].match(/(0+)$/);
|
||||
if (!trailingZeroMatch) {
|
||||
return value;
|
||||
}
|
||||
const nbTrailingZeroToRemove = Math.min(trailingZeroMatch[1].length, trailingNumbersMatch[1].length - 2);
|
||||
return value.substring(0, value.length - nbTrailingZeroToRemove);
|
||||
}
|
||||
|
||||
/*
|
||||
* @override
|
||||
* Prevent the users of showing a rounding at 12 digits on the screen but
|
||||
* getting an unrounded value after typing "= 2/3" on the field when saving.
|
||||
*/
|
||||
parse(value) {
|
||||
const parsedValue = super.parse(value);
|
||||
try {
|
||||
Number(parsedValue);
|
||||
} catch {
|
||||
return parsedValue;
|
||||
}
|
||||
const precisionRounding = Number(`1e-${this.props.digits[1]}`);
|
||||
return roundPrecision(parsedValue, precisionRounding);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
export const accountTaxRepartitionLineFactorPercent = {
|
||||
...floatField,
|
||||
component: AccountTaxRepartitionLineFactorPercent,
|
||||
};
|
||||
|
||||
|
||||
registry.category("fields").add("account_tax_repartition_line_factor_percent", accountTaxRepartitionLineFactorPercent);
|
||||
|
|
@ -1,23 +1,62 @@
|
|||
/** @odoo-module **/
|
||||
|
||||
import { _t } from "@web/core/l10n/translation";
|
||||
import { registry } from "@web/core/registry";
|
||||
import { SelectionField } from "@web/views/fields/selection/selection_field";
|
||||
import { SelectionField, 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') },
|
||||
static template = "account.AccountTypeSelection";
|
||||
setup() {
|
||||
super.setup();
|
||||
const getChoicesForGroup = (group) => {
|
||||
return this.choices.filter(x => x.value.startsWith(group));
|
||||
}
|
||||
this.sections = [
|
||||
{
|
||||
label: _t('Balance Sheet'),
|
||||
name: "balance_sheet"
|
||||
},
|
||||
{
|
||||
label: _t('Profit & Loss'),
|
||||
name: "profit_and_loss"
|
||||
},
|
||||
]
|
||||
this.groups = [
|
||||
{
|
||||
label: _t('Assets'),
|
||||
choices: getChoicesForGroup('asset'),
|
||||
section: "balance_sheet",
|
||||
},
|
||||
{
|
||||
label: _t('Liabilities'),
|
||||
choices: getChoicesForGroup('liability'),
|
||||
section: "balance_sheet",
|
||||
},
|
||||
{
|
||||
label: _t('Equity'),
|
||||
choices: getChoicesForGroup('equity'),
|
||||
section: "balance_sheet",
|
||||
},
|
||||
{
|
||||
label: _t('Income'),
|
||||
choices: getChoicesForGroup('income'),
|
||||
section: "profit_and_loss",
|
||||
},
|
||||
{
|
||||
label: _t('Expense'),
|
||||
choices: getChoicesForGroup('expense'),
|
||||
section: "profit_and_loss",
|
||||
},
|
||||
{
|
||||
label: _t('Other'),
|
||||
choices: getChoicesForGroup('off_balance'),
|
||||
section: "profit_and_loss",
|
||||
},
|
||||
];
|
||||
}
|
||||
}
|
||||
AccountTypeSelection.template = "account.AccountTypeSelection";
|
||||
|
||||
registry.category("fields").add("account_type_selection", AccountTypeSelection);
|
||||
export const accountTypeSelection = {
|
||||
...selectionField,
|
||||
component: AccountTypeSelection,
|
||||
};
|
||||
|
||||
registry.category("fields").add("account_type_selection", accountTypeSelection);
|
||||
|
|
|
|||
|
|
@ -2,20 +2,11 @@
|
|||
|
||||
<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>
|
||||
<t t-name="account.AccountTypeSelection" t-inherit="web.SelectionField" t-inherit-mode="primary">
|
||||
<xpath expr="//SelectMenu" position="attributes">
|
||||
<attribute name="choices"></attribute>
|
||||
<attribute name="groups">groups</attribute>
|
||||
<attribute name="sections">sections</attribute>
|
||||
</xpath>
|
||||
</t>
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,57 @@
|
|||
import { registry } from "@web/core/registry";
|
||||
import { Component } from "@odoo/owl";
|
||||
import { standardFieldProps } from "@web/views/fields/standard_field_props";
|
||||
import { useService } from "@web/core/utils/hooks";
|
||||
|
||||
const WARNING_TYPE_ORDER = ["danger", "warning", "info"];
|
||||
|
||||
export class ActionableErrors extends Component {
|
||||
static props = { errorData: {type: Object} };
|
||||
static template = "account.ActionableErrors";
|
||||
|
||||
setup() {
|
||||
super.setup();
|
||||
this.actionService = useService("action");
|
||||
this.orm = useService("orm");
|
||||
}
|
||||
|
||||
get errorData() {
|
||||
return this.props.errorData;
|
||||
}
|
||||
|
||||
async handleOnClick(errorData){
|
||||
if (errorData.action?.view_mode) {
|
||||
// view_mode is not handled JS side
|
||||
errorData.action['views'] = errorData.action.view_mode.split(',').map(mode => [false, mode]);
|
||||
delete errorData.action['view_mode'];
|
||||
}
|
||||
if (errorData.action_call) {
|
||||
const [model, method, args] = errorData.action_call;
|
||||
await this.orm.call(model, method, [args]);
|
||||
this.env.model.action.doAction("soft_reload");
|
||||
} else {
|
||||
this.env.model.action.doAction(errorData.action);
|
||||
}
|
||||
}
|
||||
|
||||
get sortedActionableErrors() {
|
||||
return this.errorData && Object.fromEntries(
|
||||
Object.entries(this.errorData).sort(
|
||||
(a, b) =>
|
||||
WARNING_TYPE_ORDER.indexOf(a[1]["level"] || "warning") -
|
||||
WARNING_TYPE_ORDER.indexOf(b[1]["level"] || "warning"),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export class ActionableErrorsField extends ActionableErrors {
|
||||
static props = { ...standardFieldProps };
|
||||
|
||||
get errorData() {
|
||||
return this.props.record.data[this.props.name];
|
||||
}
|
||||
}
|
||||
|
||||
export const actionableErrorsField = {component: ActionableErrorsField};
|
||||
registry.category("fields").add("actionable_errors", actionableErrorsField);
|
||||
|
|
@ -0,0 +1,26 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<templates>
|
||||
<t t-name="account.ActionableErrors">
|
||||
<t t-if="this.sortedActionableErrors">
|
||||
<div class="mb-2 rounded-2 overflow-hidden d-grid gap-2">
|
||||
<t t-foreach="this.sortedActionableErrors" t-as="error" t-key="error">
|
||||
<t t-set="level" t-value="error_value.level || 'warning'"/>
|
||||
<div t-att-class="`alert alert-${level} m-0 p-1 ps-3`" role="alert">
|
||||
<div t-att-name="error" style="white-space: pre-wrap;">
|
||||
<t t-out="error_value.message"/>
|
||||
<a class="fw-bold"
|
||||
t-if="error_value.action or error_value.action_call"
|
||||
href="#"
|
||||
t-on-click.prevent="() => this.handleOnClick(error_value)"
|
||||
>
|
||||
<i class="oi oi-arrow-right ms-1"/>
|
||||
<span class="ms-1" t-out="error_value.action_text"/>
|
||||
<i t-if="level === 'danger'" class="fa fa-warning ms-1"/>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</t>
|
||||
</div>
|
||||
</t>
|
||||
</t>
|
||||
</templates>
|
||||
|
|
@ -0,0 +1,17 @@
|
|||
import { registry } from "@web/core/registry";
|
||||
import { X2ManyField, x2ManyField } from "@web/views/fields/x2many/x2many_field";
|
||||
|
||||
|
||||
export class AutoSaveResPartnerField extends X2ManyField {
|
||||
async onAdd({ context, editable } = {}) {
|
||||
await this.props.record.model.root.save();
|
||||
await super.onAdd({ context, editable });
|
||||
}
|
||||
}
|
||||
|
||||
export const autoSaveResPartnerField = {
|
||||
...x2ManyField,
|
||||
component: AutoSaveResPartnerField,
|
||||
};
|
||||
|
||||
registry.category("fields").add("auto_save_res_partner", autoSaveResPartnerField);
|
||||
|
|
@ -1,49 +0,0 @@
|
|||
/** @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,53 @@
|
|||
import { registry } from "@web/core/registry";
|
||||
import { useRecordObserver } from "@web/model/relational_model/utils";
|
||||
import {
|
||||
Many2ManyTaxTagsField,
|
||||
many2ManyTaxTagsField
|
||||
} from "@account/components/many2x_tax_tags/many2x_tax_tags";
|
||||
|
||||
export class AutosaveMany2ManyTaxTagsField extends Many2ManyTaxTagsField {
|
||||
setup() {
|
||||
super.setup();
|
||||
|
||||
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();
|
||||
};
|
||||
useRecordObserver(this.onRecordChange.bind(this));
|
||||
}
|
||||
|
||||
async deleteTag(id) {
|
||||
await super.deleteTag(id);
|
||||
await this._saveOnUpdate();
|
||||
}
|
||||
|
||||
onRecordChange(record) {
|
||||
const line = record.data;
|
||||
if (line.tax_ids.records.length > 0) {
|
||||
if (line.balance !== this.lastBalance
|
||||
|| line.account_id.id !== this.lastAccount.id
|
||||
|| line.partner_id.id !== this.lastPartner.id) {
|
||||
this.lastBalance = line.balance;
|
||||
this.lastAccount = line.account_id;
|
||||
this.lastPartner = line.partner_id;
|
||||
return record.model.root.save();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async _saveOnUpdate() {
|
||||
await this.props.record.model.root.save();
|
||||
}
|
||||
}
|
||||
|
||||
export const autosaveMany2ManyTaxTagsField = {
|
||||
...many2ManyTaxTagsField,
|
||||
component: AutosaveMany2ManyTaxTagsField,
|
||||
};
|
||||
|
||||
registry.category("fields").add("autosave_many2many_tax_tags", autosaveMany2ManyTaxTagsField);
|
||||
|
|
@ -0,0 +1,69 @@
|
|||
import { registry } from "@web/core/registry";
|
||||
import { useService } from "@web/core/utils/hooks";
|
||||
import { DocumentFileUploader } from "../document_file_uploader/document_file_uploader";
|
||||
|
||||
import { Component, onWillStart } from "@odoo/owl";
|
||||
|
||||
export class BillGuide extends Component {
|
||||
static template = "account.BillGuide";
|
||||
static components = {
|
||||
DocumentFileUploader,
|
||||
};
|
||||
static props = ["*"]; // could contain view_widget props
|
||||
|
||||
setup() {
|
||||
this.orm = useService("orm");
|
||||
this.action = useService("action");
|
||||
this.context = null;
|
||||
this.alias = null;
|
||||
this.showSampleAction = false;
|
||||
onWillStart(this.onWillStart);
|
||||
}
|
||||
|
||||
async onWillStart() {
|
||||
const rec = this.props.record;
|
||||
const ctx = this.env.searchModel.context;
|
||||
if (rec) {
|
||||
// prepare context from journal record
|
||||
this.context = {
|
||||
default_journal_id: rec.resId,
|
||||
default_move_type: (rec.data.type === 'sale' && 'out_invoice') || (rec.data.type === 'purchase' && 'in_invoice') || 'entry',
|
||||
active_model: rec.resModel,
|
||||
active_ids: [rec.resId],
|
||||
}
|
||||
this.alias = rec.data.alias_domain_id && rec.data.alias_id[1] || false;
|
||||
} else if (!ctx?.default_journal_id && ctx?.active_id) {
|
||||
this.context = {
|
||||
default_journal_id: ctx.active_id,
|
||||
}
|
||||
}
|
||||
this.showSampleAction = await this.orm.call("account.journal", "is_sample_action_available");
|
||||
}
|
||||
|
||||
handleButtonClick(action, model="account.journal") {
|
||||
this.action.doActionButton({
|
||||
resModel: model,
|
||||
name: action,
|
||||
context: this.context || this.env.searchModel.context,
|
||||
type: 'object',
|
||||
});
|
||||
}
|
||||
|
||||
openVendorBill() {
|
||||
return this.action.doAction({
|
||||
type: "ir.actions.act_window",
|
||||
res_model: "account.move",
|
||||
views: [[false, "form"]],
|
||||
context: {
|
||||
default_move_type: "in_invoice",
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
export const billGuide = {
|
||||
component: BillGuide,
|
||||
};
|
||||
|
||||
registry.category("view_widgets").add("bill_upload_guide", billGuide);
|
||||
|
|
@ -0,0 +1,35 @@
|
|||
.o_view_nocontent {
|
||||
.o_nocontent_help:has(> .bill_guide_container) {
|
||||
min-width: 65vw;
|
||||
}
|
||||
}
|
||||
|
||||
.bill_guide_container {
|
||||
@include media-breakpoint-up(sm) {
|
||||
min-width: 400px;
|
||||
}
|
||||
|
||||
.bill_guide_left, .bill_guide_right {
|
||||
width: 45%;
|
||||
}
|
||||
|
||||
.separator_wrapper {
|
||||
width: 10%;
|
||||
}
|
||||
}
|
||||
|
||||
.bill-guide-img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: contain;
|
||||
}
|
||||
|
||||
.mb-9 {
|
||||
margin-bottom: 9rem !important;
|
||||
}
|
||||
|
||||
.account_drag_drop_btn {
|
||||
border-style: dashed !important;
|
||||
border-color: $o-brand-primary;
|
||||
background-color: mix($o-brand-primary, $o-view-background-color, 15%);
|
||||
}
|
||||
|
|
@ -0,0 +1,82 @@
|
|||
<templates>
|
||||
|
||||
<t t-name="account.BillGuide">
|
||||
<div class="d-flex flex-row bill_guide_container mb-3" t-att-class="{ 'mb-9': props.largeIcons }">
|
||||
<div class="bill_guide_left d-flex align-items-center justify-content-center py-3">
|
||||
<div>
|
||||
<div class="text-center">
|
||||
<img t-att-class="{ 'bill-guide-img': props.largeIcons }" src="/web/static/img/folder.svg"/>
|
||||
</div>
|
||||
<div class="text-center mt-2">
|
||||
<span class="btn account_drag_drop_btn pe-none" t-att-class="{ 'btn-lg': props.largeIcons }">Drag & drop</span>
|
||||
<div t-if="showSampleAction">
|
||||
<span class="btn pe-none px-1 fw-normal">or</span>
|
||||
<a class="btn btn-link px-0 fw-normal"
|
||||
t-att-class="{ 'btn-lg': props.largeIcons }"
|
||||
href="#"
|
||||
type="object"
|
||||
name="action_create_vendor_bill"
|
||||
journal_type="purchase"
|
||||
groups="account.group_account_invoice"
|
||||
t-on-click="() => this.handleButtonClick('action_create_vendor_bill')"
|
||||
>
|
||||
try our sample
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="separator_wrapper d-flex justify-content-center flex-shrink-1">
|
||||
<div class="word-separator d-flex flex-column align-items-center">
|
||||
<div class="vertical-line border-start flex-grow-1 mt-2"/>
|
||||
<div class="m-2">
|
||||
or
|
||||
</div>
|
||||
<div class="vertical-line border-start flex-grow-1 mb-2"/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="bill_guide_right d-flex align-items-center justify-content-center py-3">
|
||||
<div t-if="alias">
|
||||
<div class="text-center">
|
||||
<img t-att-class="{ 'bill-guide-img': props.largeIcons }" src="/account/static/src/img/bill.svg" alt="Email bills"/>
|
||||
</div>
|
||||
<div class="text-center mt-2">
|
||||
<div class="">
|
||||
<span class="btn pe-none px-1 fw-normal">Send a bill to</span>
|
||||
<a class="btn btn-link px-0 fw-normal" t-attf-href="mailto:{{alias}}" t-out="alias"></a>
|
||||
</div>
|
||||
<div>
|
||||
<span class="btn pe-none px-1 fw-normal">or</span>
|
||||
<a href="#"
|
||||
type="object"
|
||||
class="btn btn-link px-0 fw-normal"
|
||||
t-on-click="() => this.handleButtonClick('action_create_new')"
|
||||
groups="account.group_account_invoice"
|
||||
>
|
||||
Create manually
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div t-else="">
|
||||
<div class="text-center">
|
||||
<img t-att-class="{ 'bill-guide-img': props.largeIcons }" src="/web/static/img/bill.svg" alt="Create bill manually"/>
|
||||
</div>
|
||||
<div class="text-center mt-2">
|
||||
<div class="">
|
||||
<a href="#"
|
||||
type="object"
|
||||
class="o_invoice_new"
|
||||
t-on-click="() => this.openVendorBill()"
|
||||
groups="account.group_account_invoice"
|
||||
>
|
||||
Create a bill manually
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</t>
|
||||
|
||||
</templates>
|
||||
|
|
@ -1,229 +0,0 @@
|
|||
/** @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);
|
||||
|
|
@ -1,27 +0,0 @@
|
|||
.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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,102 +0,0 @@
|
|||
<?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,22 @@
|
|||
import { registry } from "@web/core/registry";
|
||||
import { CharField, charField } from "@web/views/fields/char/char_field";
|
||||
|
||||
// Ensure that in Hoot tests, this module is loaded after `@mail/js/onchange_on_keydown`
|
||||
// (needed because that module patches `charField`).
|
||||
import "@mail/js/onchange_on_keydown";
|
||||
|
||||
export class CharWithPlaceholderField extends CharField {
|
||||
static template = "account.CharWithPlaceholderField";
|
||||
|
||||
/** Override **/
|
||||
get formattedValue() {
|
||||
return super.formattedValue || this.props.placeholder;
|
||||
}
|
||||
}
|
||||
|
||||
export const charWithPlaceholderField = {
|
||||
...charField,
|
||||
component: CharWithPlaceholderField,
|
||||
};
|
||||
|
||||
registry.category("fields").add("char_with_placeholder_field", charWithPlaceholderField);
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<templates xml:space="preserve">
|
||||
|
||||
<t t-name="account.CharWithPlaceholderField" t-inherit="web.CharField">
|
||||
<xpath expr="//span" position="attributes">
|
||||
<attribute name="t-att-class">{'text-muted': !this.props.record.data[props.name]}</attribute>
|
||||
</xpath>
|
||||
</t>
|
||||
|
||||
</templates>
|
||||
|
|
@ -0,0 +1,14 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<templates xml:space="preserve">
|
||||
|
||||
<t t-name="account.CharWithPlaceholderFieldToCheck" t-inherit="account.CharWithPlaceholderField" t-inherit-mode="extension">
|
||||
<xpath expr="//span" position="after">
|
||||
<span t-if="props.record.data.checked === false and props.record.data.state === 'posted'"
|
||||
groups="account.group_account_user"
|
||||
class="badge rounded-pill text-bg-info mx-2 d-inline-flex">
|
||||
To review
|
||||
</span>
|
||||
</xpath>
|
||||
</t>
|
||||
|
||||
</templates>
|
||||
|
|
@ -0,0 +1,16 @@
|
|||
import { registry } from "@web/core/registry";
|
||||
import {
|
||||
charWithPlaceholderField,
|
||||
CharWithPlaceholderField
|
||||
} from "../char_with_placeholder_field/char_with_placeholder_field";
|
||||
|
||||
export class CharWithPlaceholderFieldToCheck extends CharWithPlaceholderField {
|
||||
static template = "account.CharWithPlaceholderField";
|
||||
}
|
||||
|
||||
export const charWithPlaceholderFieldToCheck = {
|
||||
...charWithPlaceholderField,
|
||||
component: CharWithPlaceholderFieldToCheck,
|
||||
};
|
||||
|
||||
registry.category("fields").add("char_with_placeholder_field_to_check", charWithPlaceholderFieldToCheck);
|
||||
|
|
@ -0,0 +1,43 @@
|
|||
/** @odoo-module **/
|
||||
|
||||
import { _t } from "@web/core/l10n/translation";
|
||||
import { ConfirmationDialog } from "@web/core/confirmation_dialog/confirmation_dialog";
|
||||
import { registry } from "@web/core/registry";
|
||||
import { FormController } from "@web/views/form/form_controller";
|
||||
import { formView } from "@web/views/form/form_view";
|
||||
|
||||
export class CurrencyFormController extends FormController {
|
||||
|
||||
async onWillSaveRecord(record) {
|
||||
if (record.data.display_rounding_warning &&
|
||||
record._values.rounding !== undefined &&
|
||||
record.data.rounding < record._values.rounding
|
||||
) {
|
||||
return new Promise((resolve) => {
|
||||
this.dialogService.add(ConfirmationDialog, {
|
||||
title: _t("Confirmation Warning"),
|
||||
body: _t(
|
||||
"You're about to permanently change the decimals for all prices in your database.\n" +
|
||||
"This change cannot be undone without technical support."
|
||||
),
|
||||
confirmLabel: _t("Confirm"),
|
||||
cancelLabel: _t("Cancel"),
|
||||
confirm: () => resolve(true),
|
||||
cancel: () => {
|
||||
record.discard();
|
||||
resolve(false);
|
||||
},
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
export const currencyFormView = {
|
||||
...formView,
|
||||
Controller: CurrencyFormController,
|
||||
};
|
||||
|
||||
registry.category("views").add("currency_form", currencyFormView);
|
||||
|
|
@ -0,0 +1,24 @@
|
|||
/** @odoo-module **/
|
||||
|
||||
import { registry } from "@web/core/registry";
|
||||
import { standardFieldProps } from "@web/views/fields/standard_field_props";
|
||||
import { useService } from "@web/core/utils/hooks";
|
||||
import { Component } from "@odoo/owl";
|
||||
|
||||
class OpenDecimalPrecisionButton extends Component {
|
||||
static template = "account.OpenDecimalPrecisionButton";
|
||||
static props = { ...standardFieldProps };
|
||||
|
||||
setup() {
|
||||
this.action = useService("action");
|
||||
}
|
||||
|
||||
async discardAndOpen() {
|
||||
await this.props.record.discard();
|
||||
this.action.doAction("base.action_decimal_precision_form");
|
||||
}
|
||||
}
|
||||
|
||||
registry.category("fields").add("open_decimal_precision_button", {
|
||||
component: OpenDecimalPrecisionButton,
|
||||
});
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<templates>
|
||||
<t t-name="account.OpenDecimalPrecisionButton">
|
||||
<button type="button"
|
||||
class="btn btn-link p-0"
|
||||
t-on-click="discardAndOpen">
|
||||
<i class="fa fa-arrow-right text-muted me-1"/>
|
||||
More precision on Product Prices
|
||||
</button>
|
||||
</t>
|
||||
</templates>
|
||||
|
|
@ -0,0 +1,78 @@
|
|||
import { useService } from "@web/core/utils/hooks";
|
||||
import { FileUploader } from "@web/views/fields/file_handler";
|
||||
import { standardWidgetProps } from "@web/views/widgets/standard_widget_props";
|
||||
|
||||
import { Component, markup } from "@odoo/owl";
|
||||
|
||||
export class DocumentFileUploader extends Component {
|
||||
static template = "account.DocumentFileUploader";
|
||||
static components = {
|
||||
FileUploader,
|
||||
};
|
||||
static props = {
|
||||
...standardWidgetProps,
|
||||
record: { type: Object, optional: true },
|
||||
slots: { type: Object, optional: true },
|
||||
resModel: { type: String, optional: true },
|
||||
};
|
||||
|
||||
setup() {
|
||||
this.orm = useService("orm");
|
||||
this.action = useService("action");
|
||||
this.notification = useService("notification");
|
||||
this.attachmentIdsToProcess = [];
|
||||
this.extraContext = this.getExtraContext();
|
||||
}
|
||||
|
||||
// To pass extra context while creating record
|
||||
getExtraContext() {
|
||||
return {};
|
||||
}
|
||||
|
||||
async onFileUploaded(file) {
|
||||
const att_data = {
|
||||
name: file.name,
|
||||
mimetype: file.type,
|
||||
datas: file.data,
|
||||
};
|
||||
// clean the context to ensure the `create` call doesn't fail from unknown `default_*` context
|
||||
const cleanContext = Object.fromEntries(Object.entries(this.env.searchModel.context).filter(([key]) => !key.startsWith('default_')));
|
||||
const [att_id] = await this.orm.create("ir.attachment", [att_data], {context: cleanContext});
|
||||
this.attachmentIdsToProcess.push(att_id);
|
||||
}
|
||||
|
||||
// To define specific resModal from another model
|
||||
getResModel() {
|
||||
return this.props.resModel;
|
||||
}
|
||||
|
||||
async onUploadComplete() {
|
||||
const resModal = this.getResModel();
|
||||
let action;
|
||||
try {
|
||||
action = await this.orm.call(
|
||||
resModal,
|
||||
"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 (const [file, msg] of Object.entries(action.context.notifications)) {
|
||||
this.notification.add(msg, {
|
||||
title: file,
|
||||
type: "info",
|
||||
sticky: true,
|
||||
});
|
||||
}
|
||||
delete action.context.notifications;
|
||||
}
|
||||
if (action.help?.length) {
|
||||
action.help = markup(action.help);
|
||||
}
|
||||
this.action.doAction(action);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,29 @@
|
|||
<templates>
|
||||
|
||||
<t t-name="account.DocumentFileUploader">
|
||||
<FileUploader
|
||||
acceptedFileExtensions="props.acceptedFileExtensions"
|
||||
fileUploadClass="'document_file_uploader'"
|
||||
multiUpload="true"
|
||||
onUploaded.bind="onFileUploaded"
|
||||
onUploadComplete.bind="onUploadComplete">
|
||||
<t t-set-slot="toggler">
|
||||
<t t-slot="toggler"/>
|
||||
</t>
|
||||
<t t-slot="default"/>
|
||||
</FileUploader>
|
||||
</t>
|
||||
|
||||
<t t-name="account.DocumentViewUploadButton">
|
||||
<DocumentFileUploader resModel="props.resModel">
|
||||
<t t-set-slot="toggler">
|
||||
<t t-if="!hideUploadButton">
|
||||
<button type="button" class="btn btn-secondary" data-hotkey="shift+i">
|
||||
Upload
|
||||
</button>
|
||||
</t>
|
||||
</t>
|
||||
</DocumentFileUploader>
|
||||
</t>
|
||||
|
||||
</templates>
|
||||
|
|
@ -0,0 +1,69 @@
|
|||
import { _t } from "@web/core/l10n/translation";
|
||||
import { registry } from "@web/core/registry";
|
||||
import { useService } from "@web/core/utils/hooks";
|
||||
|
||||
import { SelectionField, selectionField } from "@web/views/fields/selection/selection_field";
|
||||
|
||||
import { Component } from "@odoo/owl";
|
||||
|
||||
export class DocumentStatePopover extends Component {
|
||||
static template = "account.DocumentStatePopover";
|
||||
static props = {
|
||||
close: Function,
|
||||
onClose: Function,
|
||||
copyText: Function,
|
||||
message: String,
|
||||
};
|
||||
}
|
||||
|
||||
export class DocumentState extends SelectionField {
|
||||
static template = "account.DocumentState";
|
||||
|
||||
setup() {
|
||||
super.setup();
|
||||
this.popover = useService("popover");
|
||||
this.notification = useService("notification");
|
||||
}
|
||||
|
||||
get message() {
|
||||
return this.props.record.data.message;
|
||||
}
|
||||
|
||||
copyText() {
|
||||
navigator.clipboard.writeText(this.message);
|
||||
this.notification.add(_t("Text copied"), { type: "success" });
|
||||
this.popoverCloseFn();
|
||||
this.popoverCloseFn = null;
|
||||
}
|
||||
|
||||
showMessagePopover(ev) {
|
||||
const close = () => {
|
||||
this.popoverCloseFn();
|
||||
this.popoverCloseFn = null;
|
||||
};
|
||||
|
||||
if (this.popoverCloseFn) {
|
||||
close();
|
||||
return;
|
||||
}
|
||||
|
||||
this.popoverCloseFn = this.popover.add(
|
||||
ev.currentTarget,
|
||||
DocumentStatePopover,
|
||||
{
|
||||
message: this.message,
|
||||
copyText: this.copyText.bind(this),
|
||||
onClose: close,
|
||||
},
|
||||
{
|
||||
closeOnClickAway: true,
|
||||
position: "top",
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
registry.category("fields").add("account_document_state", {
|
||||
...selectionField,
|
||||
component: DocumentState,
|
||||
});
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
.account_document_state_popover {
|
||||
width: 500px;
|
||||
}
|
||||
|
||||
.account_document_state_popover_clone {
|
||||
&:hover {
|
||||
color: $o-enterprise-action-color !important;
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,20 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<templates>
|
||||
<t t-name="account.DocumentStatePopover">
|
||||
<div class="row m-2 mt-4 justify-content-between account_document_state_popover">
|
||||
<span class="col-10" t-out="props.message" style="white-space: pre-wrap;"/>
|
||||
<button class="col-2 btn p-0 account_document_state_popover_clone" t-on-click="() => props.copyText()">
|
||||
<i class="fa fa-clipboard"/>
|
||||
</button>
|
||||
</div>
|
||||
</t>
|
||||
|
||||
<t t-name="account.DocumentState" t-inherit="web.SelectionField" t-inherit-mode="primary">
|
||||
<span position="after">
|
||||
<span t-if="message"> </span>
|
||||
<a t-if="message"
|
||||
t-on-click="(ev) => this.showMessagePopover(ev)"
|
||||
class="fa fa-info-circle"/>
|
||||
</span>
|
||||
</t>
|
||||
</templates>
|
||||
|
|
@ -0,0 +1,63 @@
|
|||
/** @odoo-module **/
|
||||
|
||||
import { registry } from "@web/core/registry";
|
||||
import { SelectionField, selectionField } from "@web/views/fields/selection/selection_field";
|
||||
|
||||
export class DynamicSelectionField extends SelectionField {
|
||||
|
||||
static props = {
|
||||
...SelectionField.props,
|
||||
available_field: { type: String },
|
||||
}
|
||||
|
||||
get availableOptions() {
|
||||
return this.props.record.data[this.props.available_field]?.split(",") || [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Filter the options with the accepted available options.
|
||||
* @override
|
||||
*/
|
||||
get options() {
|
||||
const availableOptions = this.availableOptions;
|
||||
return super.options.filter(x => availableOptions.includes(x[0]));
|
||||
}
|
||||
|
||||
/**
|
||||
* In dynamic selection field, sometimes we can have no options available.
|
||||
* This override handles that case by adding optional chaining when accessing the found options.
|
||||
* @override
|
||||
*/
|
||||
get string() {
|
||||
if (this.type === "selection") {
|
||||
return this.props.record.data[this.props.name] !== false
|
||||
? this.options.find((o) => o[0] === this.props.record.data[this.props.name])?.[1]
|
||||
: "";
|
||||
}
|
||||
return super.string;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/*
|
||||
EXAMPLE USAGE:
|
||||
|
||||
In python:
|
||||
the_available_field = fields.Char() # string of comma separated available selection field keys
|
||||
the_selection_field = fields.Selection([ ... ])
|
||||
|
||||
In the views:
|
||||
<field name="the_available_field" column_invisible="1"/>
|
||||
<field name="the_selection_field"
|
||||
widget="dynamic_selection"
|
||||
options="{'available_field': 'the_available_field'}"/>
|
||||
*/
|
||||
|
||||
registry.category("fields").add("dynamic_selection", {
|
||||
...selectionField,
|
||||
component: DynamicSelectionField,
|
||||
extractProps: (fieldInfo, dynamicInfo) => ({
|
||||
...selectionField.extractProps(fieldInfo, dynamicInfo),
|
||||
available_field: fieldInfo.options.available_field,
|
||||
}),
|
||||
})
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
<?xml version="1.0" encoding="utf-8" ?>
|
||||
<templates id="template" xml:space="preserve">
|
||||
<t t-name="account.FetchEInvoices">
|
||||
<DropdownItem onSelected.bind="fetchEInvoices">
|
||||
<i class="fa fa-fw fa-refresh me-1" aria-hidden="true"></i><t t-esc="this.buttonLabel" />
|
||||
</DropdownItem>
|
||||
</t>
|
||||
</templates>
|
||||
|
|
@ -0,0 +1,59 @@
|
|||
import { Component } from "@odoo/owl";
|
||||
import { DropdownItem } from "@web/core/dropdown/dropdown_item";
|
||||
import { registry } from "@web/core/registry";
|
||||
import { useService } from "@web/core/utils/hooks";
|
||||
import { _t } from "@web/core/l10n/translation";
|
||||
import { ACTIONS_GROUP_NUMBER } from "@web/search/action_menus/action_menus";
|
||||
|
||||
const cogMenuRegistry = registry.category("cogMenu");
|
||||
|
||||
export class FetchEInvoices extends Component {
|
||||
static template = "account.FetchEInvoices";
|
||||
static props = {};
|
||||
static components = { DropdownItem };
|
||||
|
||||
setup() {
|
||||
super.setup();
|
||||
this.action = useService("action");
|
||||
}
|
||||
|
||||
get buttonAction() {
|
||||
return this.env.searchModel.globalContext.show_fetch_in_einvoices_button
|
||||
? "button_fetch_in_einvoices"
|
||||
: "button_refresh_out_einvoices_status";
|
||||
}
|
||||
|
||||
get buttonLabel() {
|
||||
return this.env.searchModel.globalContext.show_fetch_in_einvoices_button
|
||||
? _t("Fetch e-Invoices")
|
||||
: _t("Refresh e-Invoices Status");
|
||||
}
|
||||
|
||||
fetchEInvoices() {
|
||||
const journalId = this.env.searchModel.globalContext.default_journal_id;
|
||||
if (!journalId) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.action.doActionButton({
|
||||
type: "object",
|
||||
resId: journalId,
|
||||
name: this.buttonAction,
|
||||
resModel: "account.journal",
|
||||
onClose: () => window.location.reload(),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export const fetchEInvoicesActionMenu = {
|
||||
Component: FetchEInvoices,
|
||||
groupNumber: ACTIONS_GROUP_NUMBER,
|
||||
isDisplayed: ({ config, searchModel }) =>
|
||||
searchModel.resModel === "account.move" &&
|
||||
(searchModel.globalContext.default_journal_id || false) &&
|
||||
(searchModel.globalContext.show_fetch_in_einvoices_button ||
|
||||
searchModel.globalContext.show_refresh_out_einvoices_status_button ||
|
||||
false),
|
||||
};
|
||||
|
||||
cogMenuRegistry.add("account-fetch-e-invoices", fetchEInvoicesActionMenu, { sequence: 11 });
|
||||
|
|
@ -1,31 +1,30 @@
|
|||
/** @odoo-module */
|
||||
|
||||
import { registry } from "@web/core/registry";
|
||||
import { Component } from "@odoo/owl";
|
||||
import { standardFieldProps } from "@web/views/fields/standard_field_props";
|
||||
|
||||
const { Component, onWillUpdateProps } = owl;
|
||||
class ListItem extends Component {
|
||||
static template = "account.GroupedItemTemplate";
|
||||
static props = ["item_vals", "options"];
|
||||
}
|
||||
|
||||
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 ListGroup extends Component {
|
||||
static template = "account.GroupedItemsTemplate";
|
||||
static components = { ListItem };
|
||||
static 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)
|
||||
static template = "account.GroupedListTemplate";
|
||||
static components = { ListGroup };
|
||||
static props = {...standardFieldProps};
|
||||
getValue() {
|
||||
const value = this.props.record.data[this.props.name];
|
||||
return value
|
||||
? JSON.parse(value)
|
||||
: { groups_vals: [], options: { discarded_number: "", columns: [] } };
|
||||
}
|
||||
}
|
||||
ShowGroupedList.template = "account.GroupedListTemplate";
|
||||
ShowGroupedList.components = { ListGroup };
|
||||
|
||||
registry.category("fields").add("grouped_view_widget", ShowGroupedList);
|
||||
registry.category("fields").add("grouped_view_widget", {
|
||||
component: ShowGroupedList,
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,25 +1,26 @@
|
|||
<?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">
|
||||
<t t-name="account.GroupedListTemplate">
|
||||
<t t-set="value" t-value="this.getValue()"/>
|
||||
<table t-if="value.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 t-foreach="value.options.columns" t-as="col" t-key="col_index">
|
||||
<th t-out="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 t-foreach="value.groups_vals" t-as="group_vals" t-key="group_vals_index">
|
||||
<ListGroup group_vals="group_vals" options="value.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-if="value.options.discarded_number">
|
||||
<span><t t-out="value.options.discarded_number"/> are not shown in the preview</span>
|
||||
</t>
|
||||
</t>
|
||||
|
||||
<tbody t-name="account.GroupedItemsTemplate" owl="1">
|
||||
<tbody t-name="account.GroupedItemsTemplate">
|
||||
<tr style="background-color: #dee2e6;">
|
||||
<td t-attf-colspan="{{props.options.columns.length}}">
|
||||
<t t-esc="props.group_vals.group_name"/>
|
||||
<t t-out="props.group_vals.group_name"/>
|
||||
</td>
|
||||
</tr>
|
||||
<t t-foreach="props.group_vals.items_vals" t-as="item_vals" t-key="item_vals_index">
|
||||
|
|
@ -27,14 +28,14 @@
|
|||
</t>
|
||||
</tbody>
|
||||
|
||||
<tr t-name="account.GroupedItemTemplate" owl="1">
|
||||
<tr t-name="account.GroupedItemTemplate">
|
||||
<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']}}"/>
|
||||
<td t-out="props.item_vals[col['field']]" t-attf-class="{{col['class']}}"/>
|
||||
</t>
|
||||
</tr>
|
||||
|
||||
<t t-name="account.OpenMoveTemplate">
|
||||
<a href="#" t-esc="widget.value"/>
|
||||
<a href="#" t-out="widget.value"/>
|
||||
</t>
|
||||
|
||||
</templates>
|
||||
|
|
|
|||
|
|
@ -1,48 +0,0 @@
|
|||
/** @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);
|
||||
|
|
@ -1,24 +0,0 @@
|
|||
<?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,74 @@
|
|||
import { registry } from "@web/core/registry";
|
||||
import { useService } from "@web/core/utils/hooks";
|
||||
import { FileInput } from "@web/core/file_input/file_input";
|
||||
import { Component, onWillUnmount } from "@odoo/owl";
|
||||
import { standardFieldProps } from "@web/views/fields/standard_field_props";
|
||||
|
||||
export class MailAttachments extends Component {
|
||||
static template = "account.mail_attachments";
|
||||
static components = { FileInput };
|
||||
static props = {...standardFieldProps};
|
||||
|
||||
setup() {
|
||||
this.orm = useService("orm");
|
||||
this.notification = useService("notification");
|
||||
this.attachmentIdsToUnlink = new Set();
|
||||
|
||||
onWillUnmount(this.onWillUnmount);
|
||||
}
|
||||
|
||||
get attachments() {
|
||||
return this.props.record.data[this.props.name] || [];
|
||||
}
|
||||
|
||||
get renderedAttachments() {
|
||||
const attachments = JSON.parse(JSON.stringify(this.attachments));
|
||||
const attachmentsNotSupported = this.props.record.data.attachments_not_supported || {};
|
||||
for (const attachment of attachments) {
|
||||
if (attachment.id && attachment.id in attachmentsNotSupported) {
|
||||
attachment.tooltip = attachmentsNotSupported[attachment.id];
|
||||
}
|
||||
}
|
||||
return attachments;
|
||||
}
|
||||
|
||||
onFileRemove(deleteId) {
|
||||
const newValue = [];
|
||||
|
||||
for (let item of this.attachments) {
|
||||
if (item.id === deleteId) {
|
||||
if (item.placeholder || item.protect_from_deletion) {
|
||||
const copyItem = Object.assign({ skip: true }, item);
|
||||
newValue.push(copyItem);
|
||||
} else {
|
||||
this.attachmentIdsToUnlink.add(item.id);
|
||||
}
|
||||
} else {
|
||||
newValue.push(item);
|
||||
}
|
||||
}
|
||||
|
||||
this.props.record.update({ [this.props.name]: newValue });
|
||||
}
|
||||
|
||||
async onWillUnmount() {
|
||||
// Unlink added attachments if the wizard is not saved.
|
||||
if (!this.props.record.resId) {
|
||||
this.attachments.forEach((item) => {
|
||||
if (item.manual) {
|
||||
this.attachmentIdsToUnlink.add(item.id);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (this.attachmentIdsToUnlink.size) {
|
||||
await this.orm.unlink("ir.attachment", Array.from(this.attachmentIdsToUnlink));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const mailAttachments = {
|
||||
component: MailAttachments,
|
||||
};
|
||||
|
||||
registry.category("fields").add("mail_attachments", mailAttachments);
|
||||
|
|
@ -0,0 +1,20 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<templates id="template" xml:space="preserve">
|
||||
<t t-name="account.mail_attachments">
|
||||
<ul class="list-unstyled m-0">
|
||||
<t t-foreach="renderedAttachments" t-as="attachment" t-key="attachment.id">
|
||||
<t t-if="!attachment.skip">
|
||||
<li class="d-flex align-items-center bg-200 p-1 ps-3 my-2">
|
||||
<span t-out="attachment.name" class="flex-grow-1 text-truncate"/>
|
||||
|
||||
<button class="btn flex-shrink-0" t-on-click.stop="() => this.onFileRemove(attachment.id)">
|
||||
<i class="fa fa-fw fa-times"/>
|
||||
</button>
|
||||
|
||||
<i class="fa fa-fw o_button_icon fa-warning" t-if="attachment.tooltip" t-att-data-tooltip="attachment.tooltip"></i>
|
||||
</li>
|
||||
</t>
|
||||
</t>
|
||||
</ul>
|
||||
</t>
|
||||
</templates>
|
||||
|
|
@ -0,0 +1,51 @@
|
|||
import { registry } from "@web/core/registry";
|
||||
import { useService } from "@web/core/utils/hooks";
|
||||
import { FileUploader } from "@web/views/fields/file_handler";
|
||||
import { Component } from "@odoo/owl";
|
||||
import { standardFieldProps } from "@web/views/fields/standard_field_props";
|
||||
import { useX2ManyCrud } from "@web/views/fields/relational_utils";
|
||||
import { dataUrlToBlob } from "@mail/core/common/attachment_uploader_hook";
|
||||
|
||||
export class MailAttachments extends Component {
|
||||
static template = "mail.MailComposerAttachmentSelector";
|
||||
static components = { FileUploader };
|
||||
static props = {...standardFieldProps};
|
||||
|
||||
setup() {
|
||||
this.mailStore = useService("mail.store");
|
||||
this.attachmentUploadService = useService("mail.attachment_upload");
|
||||
this.operations = useX2ManyCrud(() => {
|
||||
return this.props.record.data["attachment_ids"];
|
||||
}, true);
|
||||
}
|
||||
|
||||
get attachments() {
|
||||
return this.props.record.data[this.props.name] || [];
|
||||
}
|
||||
|
||||
async onFileUploaded({ name, data, type }) {
|
||||
const resIds = JSON.parse(this.props.record.data.res_ids);
|
||||
const thread = await this.mailStore.Thread.insert({
|
||||
model: this.props.record.data.model,
|
||||
id: resIds[0],
|
||||
});
|
||||
|
||||
const file = new File([dataUrlToBlob(data, type)], name, { type });
|
||||
const attachment = await this.attachmentUploadService.upload(thread, thread.composer, file);
|
||||
|
||||
let fileDict = {
|
||||
id: attachment.id,
|
||||
name: attachment.name,
|
||||
mimetype: attachment.mimetype,
|
||||
placeholder: false,
|
||||
manual: true,
|
||||
};
|
||||
this.props.record.update({ [this.props.name]: this.attachments.concat([fileDict]) });
|
||||
}
|
||||
}
|
||||
|
||||
export const mailAttachments = {
|
||||
component: MailAttachments,
|
||||
};
|
||||
|
||||
registry.category("fields").add("mail_attachments_selector", mailAttachments);
|
||||
|
|
@ -0,0 +1,80 @@
|
|||
import {
|
||||
many2ManyTagsFieldColorEditable,
|
||||
Many2ManyTagsFieldColorEditable,
|
||||
} from "@web/views/fields/many2many_tags/many2many_tags_field";
|
||||
import { useService } from "@web/core/utils/hooks";
|
||||
import { registry } from "@web/core/registry";
|
||||
import { TagsList } from "@web/core/tags_list/tags_list";
|
||||
import { _t } from "@web/core/l10n/translation";
|
||||
import { onMounted } from "@odoo/owl";
|
||||
|
||||
export class FieldMany2ManyTagsBanksTagsList extends TagsList {
|
||||
static template = "FieldMany2ManyTagsBanksTagsList";
|
||||
}
|
||||
|
||||
export class FieldMany2ManyTagsBanks extends Many2ManyTagsFieldColorEditable {
|
||||
static template = "account.FieldMany2ManyTagsBanks";
|
||||
static components = {
|
||||
...FieldMany2ManyTagsBanks.components,
|
||||
TagsList: FieldMany2ManyTagsBanksTagsList,
|
||||
};
|
||||
|
||||
setup() {
|
||||
super.setup();
|
||||
this.actionService = useService("action");
|
||||
onMounted(async () => {
|
||||
// Needed when you create a partner (from a move for example), we want the partner to be saved to be able
|
||||
// to have it as account holder
|
||||
const isDirty = await this.props.record.model.root.isDirty();
|
||||
if (isDirty) {
|
||||
this.props.record.model.root.save();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
getTagProps(record) {
|
||||
return {
|
||||
...super.getTagProps(record),
|
||||
allowOutPayment: record.data?.allow_out_payment,
|
||||
};
|
||||
}
|
||||
|
||||
openBanksListView() {
|
||||
this.actionService.doAction({
|
||||
type: "ir.actions.act_window",
|
||||
name: _t("Banks"),
|
||||
res_model: this.relation,
|
||||
views: [
|
||||
[false, "list"],
|
||||
[false, "form"],
|
||||
],
|
||||
domain: this.getDomain(),
|
||||
target: "current",
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export const fieldMany2ManyTagsBanks = {
|
||||
...many2ManyTagsFieldColorEditable,
|
||||
component: FieldMany2ManyTagsBanks,
|
||||
supportedOptions: [
|
||||
...(many2ManyTagsFieldColorEditable.supportedOptions || []),
|
||||
{
|
||||
label: _t("Allows out payments"),
|
||||
name: "allow_out_payment_field",
|
||||
type: "boolean",
|
||||
},
|
||||
],
|
||||
additionalClasses: [
|
||||
...(many2ManyTagsFieldColorEditable.additionalClasses || []),
|
||||
"o_field_many2many_tags",
|
||||
],
|
||||
relatedFields: ({ options }) => {
|
||||
return [
|
||||
...many2ManyTagsFieldColorEditable.relatedFields({ options }),
|
||||
{ name: options.allow_out_payment_field, type: "boolean", readonly: false },
|
||||
];
|
||||
},
|
||||
};
|
||||
|
||||
registry.category("fields").add("many2many_tags_banks", fieldMany2ManyTagsBanks);
|
||||
|
|
@ -0,0 +1,25 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<templates xml:space="preserve">
|
||||
<t t-name="FieldMany2ManyTagsBanksTagsList" t-inherit="web.TagsList" t-inherit-mode="primary">
|
||||
<xpath expr="//div[hasclass('o_tag_badge_text')]" position="before">
|
||||
<span class="me-1">
|
||||
<i t-if="tag.allowOutPayment" class="fa fa-shield text-success" data-tooltip="Trusted"/>
|
||||
<i t-else="" class="fa fa-exclamation-circle text-danger" data-tooltip="Untrusted"/>
|
||||
</span>
|
||||
</xpath>
|
||||
</t>
|
||||
|
||||
<t t-name="account.FieldMany2ManyTagsBanks" t-inherit="web.Many2ManyTagsField" t-inherit-mode="primary">
|
||||
<xpath expr="//div[hasclass('o_field_many2many_selection')]" position="inside">
|
||||
<button
|
||||
aria-label="Internal link"
|
||||
class="btn btn-link text-action o_dropdown_button px-1 py-0 oi oi-arrow-right"
|
||||
data-tooltip="Internal link"
|
||||
draggable="false"
|
||||
tabindex="-1"
|
||||
type="button"
|
||||
t-on-click="this.openBanksListView"
|
||||
/>
|
||||
</xpath>
|
||||
</t>
|
||||
</templates>
|
||||
|
|
@ -0,0 +1,56 @@
|
|||
import { registry } from "@web/core/registry";
|
||||
import {
|
||||
Many2ManyTagsField,
|
||||
many2ManyTagsField,
|
||||
} from "@web/views/fields/many2many_tags/many2many_tags_field";
|
||||
import { Many2XAutocomplete } from "@web/views/fields/relational_utils";
|
||||
|
||||
export class Many2ManyTagsJournalsMany2xAutocomplete extends Many2XAutocomplete {
|
||||
static template = "account.Many2ManyTagsJournalsMany2xAutocomplete";
|
||||
static props = {
|
||||
...Many2XAutocomplete.props,
|
||||
group_company_id: { type: Number, optional: true },
|
||||
};
|
||||
|
||||
get searchSpecification() {
|
||||
return {
|
||||
...super.searchSpecification,
|
||||
company_id: {
|
||||
fields: {
|
||||
display_name: {},
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export class Many2ManyTagsJournals extends Many2ManyTagsField {
|
||||
static template = "account.Many2ManyTagsJournals";
|
||||
static components = {
|
||||
...Many2ManyTagsField.components,
|
||||
Many2XAutocomplete: Many2ManyTagsJournalsMany2xAutocomplete,
|
||||
};
|
||||
|
||||
getTagProps(record) {
|
||||
const group_company_id = this.props.record.data["company_id"];
|
||||
|
||||
const text = group_company_id
|
||||
? record.data.display_name
|
||||
: `${record.data.company_id.display_name} - ${record.data.display_name}`;
|
||||
return {
|
||||
...super.getTagProps(record),
|
||||
text,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export const fieldMany2ManyTagsJournals = {
|
||||
...many2ManyTagsField,
|
||||
component: Many2ManyTagsJournals,
|
||||
relatedFields: (fieldInfo) => [
|
||||
...many2ManyTagsField.relatedFields(fieldInfo),
|
||||
{ name: "company_id", type: "many2one", relation: "res.company" },
|
||||
],
|
||||
};
|
||||
|
||||
registry.category("fields").add("many2many_tags_journals", fieldMany2ManyTagsJournals);
|
||||
|
|
@ -0,0 +1,17 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<templates xml:space="preserve">
|
||||
<t t-name="account.Many2ManyTagsJournals" t-inherit="web.Many2ManyTagsField">
|
||||
<xpath expr="//Many2XAutocomplete" position="attributes">
|
||||
<attribute name="group_company_id">this.props.record.data['company_id'].id</attribute>
|
||||
</xpath>
|
||||
</t>
|
||||
<t t-name="account.Many2ManyTagsJournalsMany2xAutocomplete" t-inherit="web.Many2XAutocomplete" t-inherit-mode="primary">
|
||||
<xpath expr="//t[@t-set-slot='option']/t" position="after">
|
||||
<t t-if="optionScope.data.record and !props.group_company_id">
|
||||
<span class="text-muted ms-3 fst-italic">
|
||||
<t t-out="optionScope.data.record.company_id.display_name"/>
|
||||
</span>
|
||||
</t>
|
||||
</xpath>
|
||||
</t>
|
||||
</templates>
|
||||
|
|
@ -0,0 +1,65 @@
|
|||
import { _t } from "@web/core/l10n/translation";
|
||||
import { registry } from "@web/core/registry";
|
||||
import { Many2XAutocomplete } from "@web/views/fields/relational_utils";
|
||||
import {
|
||||
Many2ManyTagsField,
|
||||
many2ManyTagsField,
|
||||
} from "@web/views/fields/many2many_tags/many2many_tags_field";
|
||||
|
||||
export class Many2XTaxTagsAutocomplete extends Many2XAutocomplete {
|
||||
static components = {
|
||||
...Many2XAutocomplete.components,
|
||||
};
|
||||
|
||||
async loadOptionsSource(request) {
|
||||
// Always include Search More
|
||||
let options = await super.loadOptionsSource(...arguments);
|
||||
if (!options.slice(-1)[0]?.cssClass?.includes("o_m2o_dropdown_option_search_more")) {
|
||||
options.push({
|
||||
label: this.SearchMoreButtonLabel,
|
||||
onSelect: this.onSearchMore.bind(this, request),
|
||||
cssClass: "o_m2o_dropdown_option o_m2o_dropdown_option_search_more",
|
||||
});
|
||||
}
|
||||
return options;
|
||||
}
|
||||
|
||||
async onSearchMore(request) {
|
||||
const { getDomain, context, fieldString } = this.props;
|
||||
|
||||
const domain = getDomain();
|
||||
let dynamicFilters = [];
|
||||
if (request.length) {
|
||||
dynamicFilters = [
|
||||
{
|
||||
description: _t("Quick search: %s", request),
|
||||
domain: [["name", "ilike", request]],
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
const title = _t("Search: %s", fieldString);
|
||||
this.selectCreate({
|
||||
domain,
|
||||
context,
|
||||
filters: dynamicFilters,
|
||||
title,
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export class Many2ManyTaxTagsField extends Many2ManyTagsField {
|
||||
static components = {
|
||||
...Many2ManyTagsField.components,
|
||||
Many2XAutocomplete: Many2XTaxTagsAutocomplete,
|
||||
};
|
||||
}
|
||||
|
||||
export const many2ManyTaxTagsField = {
|
||||
...many2ManyTagsField,
|
||||
component: Many2ManyTaxTagsField,
|
||||
additionalClasses: ['o_field_many2many_tags']
|
||||
};
|
||||
|
||||
registry.category("fields").add("many2many_tax_tags", many2ManyTaxTagsField);
|
||||
|
|
@ -0,0 +1,35 @@
|
|||
import { registry } from "@web/core/registry";
|
||||
import { standardWidgetProps } from "@web/views/widgets/standard_widget_props";
|
||||
import { useService } from "@web/core/utils/hooks";
|
||||
|
||||
import { Component } from "@odoo/owl";
|
||||
|
||||
class AccountOnboardingWidget extends Component {
|
||||
static template = "account.Onboarding";
|
||||
static props = {
|
||||
...standardWidgetProps,
|
||||
};
|
||||
setup() {
|
||||
this.action = useService("action");
|
||||
this.orm = useService("orm");
|
||||
}
|
||||
|
||||
get recordOnboardingSteps() {
|
||||
return JSON.parse(this.props.record.data.kanban_dashboard).onboarding?.steps;
|
||||
}
|
||||
|
||||
async onboardingLinkClicked(step) {
|
||||
const action = await this.orm.call("onboarding.onboarding.step", step.action, [], {
|
||||
context: {
|
||||
journal_id: this.props.record.resId,
|
||||
}
|
||||
});
|
||||
this.action.doAction(action);
|
||||
}
|
||||
}
|
||||
|
||||
export const accountOnboarding = {
|
||||
component: AccountOnboardingWidget,
|
||||
}
|
||||
|
||||
registry.category("view_widgets").add("account_onboarding", accountOnboarding);
|
||||
|
|
@ -0,0 +1,23 @@
|
|||
<?xml version="1.0" encoding="UTF-8" ?>
|
||||
<template>
|
||||
|
||||
<t t-name="account.Onboarding">
|
||||
<div class="">
|
||||
<div class="col-auto my-1" t-foreach="recordOnboardingSteps" t-as="step" t-key="step.id">
|
||||
<i class="fa me-2 fs-5" t-att-class="{
|
||||
'fa-circle text-secondary': step.state == 'not_done',
|
||||
'fa-check-circle text-success': step.state != 'not_done',
|
||||
}"/>
|
||||
<a href="#"
|
||||
t-att-data-method="step.action"
|
||||
data-model="onboarding.onboarding.step"
|
||||
t-out="step.title"
|
||||
t-att-title="step.description"
|
||||
t-on-click.stop.prevent="() => this.onboardingLinkClicked(step)"
|
||||
/>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</t>
|
||||
|
||||
</template>
|
||||
|
|
@ -1,17 +1,35 @@
|
|||
/** @odoo-module **/
|
||||
|
||||
import { Component } from "@odoo/owl";
|
||||
import { registry } from "@web/core/registry";
|
||||
import { Many2OneField } from "@web/views/fields/many2one/many2one_field";
|
||||
import { useService } from "@web/core/utils/hooks";
|
||||
import { computeM2OProps, Many2One } from "@web/views/fields/many2one/many2one";
|
||||
import { buildM2OFieldDescription, Many2OneField } from "@web/views/fields/many2one/many2one_field";
|
||||
|
||||
class LineOpenMoveWidget extends Component {
|
||||
static template = "account.LineOpenMoveWidget";
|
||||
static components = { Many2One };
|
||||
static props = { ...Many2OneField.props };
|
||||
|
||||
setup() {
|
||||
this.action = useService("action");
|
||||
}
|
||||
|
||||
get m2oProps() {
|
||||
return {
|
||||
...computeM2OProps(this.props),
|
||||
openRecordAction: () => this.openAction(),
|
||||
};
|
||||
}
|
||||
|
||||
class LineOpenMoveWidget extends Many2OneField {
|
||||
async openAction() {
|
||||
this.action.doActionButton({
|
||||
return this.action.doActionButton({
|
||||
type: "object",
|
||||
resId: this.props.value[0],
|
||||
resId: this.props.record.data[this.props.name].id,
|
||||
name: "action_open_business_doc",
|
||||
resModel: "account.move.line",
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
registry.category("fields").add("line_open_move_widget", LineOpenMoveWidget);
|
||||
registry.category("fields").add("line_open_move_widget", {
|
||||
...buildM2OFieldDescription(LineOpenMoveWidget),
|
||||
});
|
||||
|
|
|
|||
|
|
@ -0,0 +1,8 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<templates xml:space="preserve">
|
||||
|
||||
<t t-name="account.LineOpenMoveWidget">
|
||||
<Many2One t-props="m2oProps"/>
|
||||
</t>
|
||||
|
||||
</templates>
|
||||
|
|
@ -1,11 +1,12 @@
|
|||
/** @odoo-module **/
|
||||
|
||||
import { registry } from "@web/core/registry";
|
||||
import { useService } from "@web/core/utils/hooks";
|
||||
|
||||
const { Component } = owl;
|
||||
import { standardFieldProps } from "@web/views/fields/standard_field_props";
|
||||
import { Component } from "@odoo/owl";
|
||||
|
||||
class OpenMoveWidget extends Component {
|
||||
static template = "account.OpenMoveWidget";
|
||||
static props = { ...standardFieldProps };
|
||||
|
||||
setup() {
|
||||
super.setup();
|
||||
this.action = useService("action");
|
||||
|
|
@ -16,10 +17,11 @@ class OpenMoveWidget extends Component {
|
|||
type: "object",
|
||||
resId: this.props.record.resId,
|
||||
name: "action_open_business_doc",
|
||||
resModel: "account.move.line",
|
||||
resModel: this.props.record.resModel,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
OpenMoveWidget.template = "account.OpenMoveWidget";
|
||||
registry.category("fields").add("open_move_widget", OpenMoveWidget);
|
||||
registry.category("fields").add("open_move_widget", {
|
||||
component: OpenMoveWidget,
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,8 +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 t-name="account.OpenMoveWidget">
|
||||
<a href="#" t-out="props.record.data[props.name] || '/'" t-on-click.prevent.stop="(ev) => this.openMove()"/>
|
||||
</t>
|
||||
|
||||
</templates>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,8 @@
|
|||
import { ProductCatalogOrderLine } from "@product/product_catalog/order_line/order_line";
|
||||
|
||||
export class ProductCatalogAccountMoveLine extends ProductCatalogOrderLine {
|
||||
static props = {
|
||||
...ProductCatalogOrderLine.props,
|
||||
min_qty: { type: Number, optional: true },
|
||||
};
|
||||
}
|
||||
|
|
@ -0,0 +1,19 @@
|
|||
import { ProductCatalogKanbanController } from "@product/product_catalog/kanban_controller";
|
||||
import { patch } from "@web/core/utils/patch";
|
||||
import { _t } from "@web/core/l10n/translation";
|
||||
|
||||
patch(ProductCatalogKanbanController.prototype, {
|
||||
get stateFiels() {
|
||||
return this.orderResModel === "account.move" ? ["state", "move_type"] : super.stateFiels;
|
||||
},
|
||||
|
||||
_defineButtonContent() {
|
||||
if (this.orderStateInfo.move_type === "out_invoice") {
|
||||
this.buttonString = _t("Back to Invoice");
|
||||
} else if (this.orderStateInfo.move_type === "in_invoice") {
|
||||
this.buttonString = _t("Back to Bill");
|
||||
} else {
|
||||
super._defineButtonContent();
|
||||
}
|
||||
},
|
||||
});
|
||||
|
|
@ -0,0 +1,26 @@
|
|||
import { ProductCatalogKanbanModel } from "@product/product_catalog/kanban_model";
|
||||
import { patch } from "@web/core/utils/patch";
|
||||
|
||||
patch(ProductCatalogKanbanModel.prototype, {
|
||||
async _loadData(params) {
|
||||
const selectedSection = this.env.searchModel.selectedSection;
|
||||
if (selectedSection.filtered) {
|
||||
params = {
|
||||
...params,
|
||||
domain: [...(params.domain || []), ['is_in_selected_section_of_order', '=', true]],
|
||||
context: {
|
||||
...params.context,
|
||||
section_id: selectedSection.sectionId,
|
||||
},
|
||||
};
|
||||
}
|
||||
return await super._loadData(params);
|
||||
},
|
||||
|
||||
_getOrderLinesInfoParams(params, productIds) {
|
||||
return {
|
||||
...super._getOrderLinesInfoParams(params, productIds),
|
||||
section_id: this.env.searchModel.selectedSection.sectionId,
|
||||
};
|
||||
}
|
||||
})
|
||||
|
|
@ -0,0 +1,52 @@
|
|||
import { useSubEnv } from "@odoo/owl";
|
||||
import { ProductCatalogKanbanRecord } from "@product/product_catalog/kanban_record";
|
||||
import { ProductCatalogAccountMoveLine } from "./account_move_line";
|
||||
import { patch } from "@web/core/utils/patch";
|
||||
|
||||
patch(ProductCatalogKanbanRecord.prototype, {
|
||||
setup() {
|
||||
super.setup();
|
||||
|
||||
useSubEnv({
|
||||
...this.env,
|
||||
selectedSectionId: this.env.searchModel.selectedSection?.sectionId,
|
||||
});
|
||||
},
|
||||
|
||||
get orderLineComponent() {
|
||||
if (this.env.orderResModel === "account.move") {
|
||||
return ProductCatalogAccountMoveLine;
|
||||
}
|
||||
return super.orderLineComponent;
|
||||
},
|
||||
|
||||
_getUpdateQuantityAndGetPriceParams() {
|
||||
return {
|
||||
...super._getUpdateQuantityAndGetPriceParams(),
|
||||
section_id: this.env.searchModel.selectedSection.sectionId || false,
|
||||
};
|
||||
},
|
||||
|
||||
addProduct(qty = 1) {
|
||||
if (this.productCatalogData.quantity === 0 && qty < this.productCatalogData.min_qty) {
|
||||
qty = this.productCatalogData.min_qty; // Take seller's minimum if trying to add less
|
||||
}
|
||||
super.addProduct(qty);
|
||||
},
|
||||
|
||||
updateQuantity(quantity) {
|
||||
const lineCountChange = (quantity > 0) - (this.productCatalogData.quantity > 0);
|
||||
if (lineCountChange !== 0) {
|
||||
this.notifyLineCountChange(lineCountChange);
|
||||
}
|
||||
|
||||
super.updateQuantity(quantity);
|
||||
},
|
||||
|
||||
notifyLineCountChange(lineCountChange) {
|
||||
this.env.searchModel.trigger('section-line-count-change', {
|
||||
sectionId: this.env.selectedSectionId,
|
||||
lineCountChange: lineCountChange,
|
||||
});
|
||||
},
|
||||
})
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
import { productCatalogKanbanView } from "@product/product_catalog/kanban_view";
|
||||
import { patch } from "@web/core/utils/patch";
|
||||
import { AccountProductCatalogSearchModel } from "./search/search_model";
|
||||
import { AccountProductCatalogSearchPanel} from "./search/search_panel";
|
||||
|
||||
patch(productCatalogKanbanView, {
|
||||
SearchModel: AccountProductCatalogSearchModel,
|
||||
SearchPanel: AccountProductCatalogSearchPanel,
|
||||
});
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
import { SearchModel } from "@web/search/search_model";
|
||||
|
||||
export class AccountProductCatalogSearchModel extends SearchModel {
|
||||
setup() {
|
||||
super.setup(...arguments);
|
||||
this.selectedSection = {sectionId: null, filtered: false};
|
||||
}
|
||||
|
||||
setSelectedSection(sectionId, filtered) {
|
||||
this.selectedSection = {sectionId, filtered};
|
||||
this._notify();
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,172 @@
|
|||
import { onWillStart, useState } from '@odoo/owl';
|
||||
import { getActiveHotkey } from '@web/core/hotkeys/hotkey_service';
|
||||
import { rpc } from '@web/core/network/rpc';
|
||||
import { useBus } from '@web/core/utils/hooks';
|
||||
import { SearchPanel } from '@web/search/search_panel/search_panel';
|
||||
|
||||
|
||||
export class AccountProductCatalogSearchPanel extends SearchPanel {
|
||||
static template = 'account.ProductCatalogSearchPanel';
|
||||
|
||||
setup() {
|
||||
super.setup();
|
||||
|
||||
this.state = useState({
|
||||
...this.state,
|
||||
sections: new Map(),
|
||||
isAddingSection: '',
|
||||
newSectionName: "",
|
||||
});
|
||||
|
||||
useBus(this.env.searchModel, 'section-line-count-change', this.updateSectionLineCount);
|
||||
|
||||
onWillStart(async () => await this.loadSections());
|
||||
}
|
||||
|
||||
updateActiveValues() {
|
||||
super.updateActiveValues();
|
||||
this.state.sidebarExpanded ||= this.showSections;
|
||||
}
|
||||
|
||||
get showSections() {
|
||||
return this.env.model.config.context.show_sections;
|
||||
}
|
||||
|
||||
get selectedSection() {
|
||||
return this.env.searchModel.selectedSection;
|
||||
}
|
||||
|
||||
onDragStart(sectionId, ev) {
|
||||
ev.dataTransfer.setData('section_id', sectionId);
|
||||
}
|
||||
|
||||
onDragOver(ev) {
|
||||
ev.preventDefault();
|
||||
}
|
||||
|
||||
onDrop(targetSecId, ev) {
|
||||
ev.preventDefault();
|
||||
const moveSecId = parseInt(ev.dataTransfer.getData('section_id'));
|
||||
if (moveSecId !== targetSecId) this.reorderSections(moveSecId, targetSecId);
|
||||
}
|
||||
|
||||
enableSectionInput(isAddingSection) {
|
||||
this.state.isAddingSection = isAddingSection;
|
||||
setTimeout(() => document.querySelector('.o_section_input')?.focus(), 100);
|
||||
}
|
||||
|
||||
onSectionInputKeydown(ev) {
|
||||
const hotkey = getActiveHotkey(ev);
|
||||
if (hotkey === 'enter') {
|
||||
this.createSection();
|
||||
} else if (hotkey === 'escape') {
|
||||
Object.assign(this.state, {
|
||||
isAddingSection: '',
|
||||
newSectionName: "",
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
setSelectedSection(sectionId=null, filtered=false) {
|
||||
this.env.searchModel.setSelectedSection(sectionId, filtered);
|
||||
}
|
||||
|
||||
async createSection() {
|
||||
const sectionName = this.state.newSectionName.trim();
|
||||
if (!sectionName) return this.state.isAddingSection = '';
|
||||
|
||||
const position = this.state.isAddingSection;
|
||||
const section = await rpc('/product/catalog/create_section',
|
||||
this._getSectionInfoParams({
|
||||
name: sectionName,
|
||||
position: position,
|
||||
})
|
||||
);
|
||||
|
||||
if (section) {
|
||||
const sections = this.state.sections;
|
||||
let newLineCount = 0;
|
||||
|
||||
if (position === 'top') {
|
||||
newLineCount = sections.get(false).line_count;
|
||||
sections.delete(false);
|
||||
}
|
||||
sections.set(section.id, {
|
||||
name: this.state.newSectionName,
|
||||
sequence: section.sequence,
|
||||
line_count: newLineCount,
|
||||
});
|
||||
this._sortSectionsBySequence(sections);
|
||||
this.setSelectedSection(section.id);
|
||||
}
|
||||
Object.assign(this.state, {
|
||||
isAddingSection: '',
|
||||
newSectionName: "",
|
||||
});
|
||||
}
|
||||
|
||||
async loadSections() {
|
||||
if (!this.showSections) return;
|
||||
const sections = await rpc('/product/catalog/get_sections', this._getSectionInfoParams());
|
||||
|
||||
const sectionMap = new Map();
|
||||
for (const {id, name, sequence, line_count} of sections) {
|
||||
sectionMap.set(id, {name, sequence, line_count});
|
||||
}
|
||||
this.state.sections = sectionMap;
|
||||
this.setSelectedSection(sectionMap.size > 0 ? [...sectionMap.keys()][0] : null);
|
||||
}
|
||||
|
||||
async reorderSections(moveId, targetId) {
|
||||
const sections = this.state.sections;
|
||||
const moveSection = sections.get(moveId);
|
||||
const targetSection = sections.get(targetId);
|
||||
|
||||
if (!moveSection || !targetSection) return;
|
||||
|
||||
const updatedSequences = await rpc('/product/catalog/resequence_sections',
|
||||
this._getSectionInfoParams({
|
||||
sections: [
|
||||
{ id: moveId, sequence: moveSection.sequence },
|
||||
{ id: targetId, sequence: targetSection.sequence },
|
||||
],
|
||||
})
|
||||
);
|
||||
for (const [id, sequence] of Object.entries(updatedSequences)) {
|
||||
const section = sections.get(parseInt(id));
|
||||
section && (section.sequence = sequence);
|
||||
}
|
||||
const noSection = sections.get(false);
|
||||
noSection && (noSection.sequence = 0); // Reset the sequence of the "No Section"
|
||||
this._sortSectionsBySequence(sections);
|
||||
}
|
||||
|
||||
updateSectionLineCount({detail: {sectionId, lineCountChange}}) {
|
||||
const sections = this.state.sections;
|
||||
const section = sections.get(sectionId);
|
||||
if (!section) return;
|
||||
|
||||
section.line_count = Math.max(0, section.line_count + lineCountChange);
|
||||
|
||||
if (section.line_count === 0 && sectionId === false && sections.size > 1) {
|
||||
sections.delete(sectionId);
|
||||
this.setSelectedSection(sections.size > 0 ? [...sections.keys()][0] : null);
|
||||
}
|
||||
}
|
||||
|
||||
_getSectionInfoParams(extra = {}) {
|
||||
const ctx = this.env.model.config.context;
|
||||
return {
|
||||
res_model: ctx.product_catalog_order_model,
|
||||
order_id: ctx.order_id,
|
||||
child_field: ctx.child_field,
|
||||
...extra,
|
||||
};
|
||||
}
|
||||
|
||||
_sortSectionsBySequence(sections) {
|
||||
this.state.sections = new Map(
|
||||
[...sections].sort((a, b) => a[1].sequence - b[1].sequence)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,22 @@
|
|||
.o_section_input {
|
||||
border: none;
|
||||
outline: none;
|
||||
background: transparent;
|
||||
border-bottom: 2px solid var(--primary);
|
||||
}
|
||||
|
||||
.o_selected_section {
|
||||
background-color: var(--list-group-active-bg);
|
||||
|
||||
.o_section_name {
|
||||
font-weight: bold;
|
||||
}
|
||||
}
|
||||
|
||||
.o_row_handle {
|
||||
@include o-grab-cursor;
|
||||
color: #adb5bd;
|
||||
&:hover {
|
||||
color: #666666;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,105 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
|
||||
<templates id="template" xml:space="preserve">
|
||||
<t
|
||||
t-name="account.ProductCatalogSearchPanelContent"
|
||||
t-inherit="web.SearchPanelContent"
|
||||
t-inherit-mode="primary"
|
||||
>
|
||||
<section position="before">
|
||||
<section t-if="showSections" class="o_search_panel_sections mt-5">
|
||||
<header class="d-flex align-items-center cursor-default gap-2 mb-3">
|
||||
<i class="fa fa-filter"/>
|
||||
<span class="text-uppercase fw-bold">Sections</span>
|
||||
<div class="d-flex align-items-center ms-auto">
|
||||
<span class="me-2">Filter</span>
|
||||
<div class="form-check form-switch mb-0">
|
||||
<input
|
||||
class="form-check-input"
|
||||
type="checkbox"
|
||||
role="switch"
|
||||
t-att-checked="selectedSection.filtered ? true : false"
|
||||
t-on-click="() => this.setSelectedSection(
|
||||
selectedSection.sectionId, !selectedSection.filtered
|
||||
)"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
<ul class="list-group d-block">
|
||||
<li
|
||||
draggable="true"
|
||||
t-foreach="this.state.sections.keys()"
|
||||
t-as="sectionId"
|
||||
t-key="sectionId"
|
||||
t-att-class="'list-group-item p-0 mb-1 border-0 cursor-pointer ' + (
|
||||
selectedSection.sectionId == sectionId ? 'o_selected_section' : ''
|
||||
)"
|
||||
t-on-dragstart="(e) => onDragStart(sectionId, e)"
|
||||
t-on-dragover="(e) => onDragOver(e)"
|
||||
t-on-drop="(e) => this.onDrop(sectionId, e)"
|
||||
>
|
||||
<div class="d-flex align-items-center">
|
||||
<t t-set="section" t-value="this.state.sections.get(sectionId)"/>
|
||||
<span
|
||||
t-if="sectionId"
|
||||
class="o_row_handle oi oi-draggable ui-sortable-handle"
|
||||
/>
|
||||
<i
|
||||
t-else=""
|
||||
class="fa fa-pencil"
|
||||
t-on-click="() => this.enableSectionInput('top')"
|
||||
/>
|
||||
<input
|
||||
t-if="sectionId === false and this.state.isAddingSection === 'top'"
|
||||
class="ms-2 o_section_input"
|
||||
type="text"
|
||||
t-model="this.state.newSectionName"
|
||||
t-on-keydown="onSectionInputKeydown"
|
||||
t-on-blur="createSection"
|
||||
/>
|
||||
<div
|
||||
t-else=""
|
||||
class="w-100 ms-2 d-flex align-items-center justify-content-between overflow-hidden"
|
||||
t-on-click="() => this.setSelectedSection(
|
||||
sectionId, selectedSection.filtered
|
||||
)"
|
||||
t-att-title="section.name"
|
||||
>
|
||||
<span class="o_section_name text-truncate" t-out="section.name"/>
|
||||
<span class="mx-2 text-muted" t-out="section.line_count"/>
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
<input
|
||||
t-if="this.state.isAddingSection === 'bottom'"
|
||||
class="o_section_input py-1"
|
||||
type="text"
|
||||
placeholder="Enter a description"
|
||||
t-model="this.state.newSectionName"
|
||||
t-on-keydown="onSectionInputKeydown"
|
||||
t-on-blur="createSection"
|
||||
/>
|
||||
<button
|
||||
t-else=""
|
||||
class="btn btn-link px-0 py-1"
|
||||
type="button"
|
||||
t-on-click="() => this.enableSectionInput('bottom')"
|
||||
>
|
||||
+ Add Section
|
||||
</button>
|
||||
</section>
|
||||
</section>
|
||||
|
||||
<div class="o_search_panel_empty_state me-3" position="attributes">
|
||||
<attribute name="class" add="d-none" separator=" "/>
|
||||
</div>
|
||||
</t>
|
||||
|
||||
<t t-name="account.ProductCatalogSearchPanel" t-inherit="web.SearchPanel" t-inherit-mode="primary">
|
||||
<t t-call="web.SearchPanel.Regular" position="attributes">
|
||||
<attribute name="t-call">account.ProductCatalogSearchPanelContent</attribute>
|
||||
</t>
|
||||
</t>
|
||||
</templates>
|
||||
|
|
@ -0,0 +1,86 @@
|
|||
import { _t } from "@web/core/l10n/translation";
|
||||
import { buildM2OFieldDescription, extractM2OFieldProps, m2oSupportedOptions } from "@web/views/fields/many2one/many2one_field";
|
||||
import { registry } from "@web/core/registry";
|
||||
import { ProductNameAndDescriptionField } from "@product/product_name_and_description/product_name_and_description";
|
||||
|
||||
export class ProductLabelSectionAndNoteField extends ProductNameAndDescriptionField {
|
||||
static template = "account.ProductLabelSectionAndNoteField";
|
||||
static props = {
|
||||
...super.props,
|
||||
show_label_warning: { type: Boolean, optional: true, default: false },
|
||||
};
|
||||
|
||||
static descriptionColumn = "name";
|
||||
|
||||
get sectionAndNoteClasses() {
|
||||
return {
|
||||
"fw-bolder": this.isSection,
|
||||
"fw-bold": this.isSubSection,
|
||||
"fst-italic": this.isNote(),
|
||||
"text-warning": this.shouldShowWarning(),
|
||||
};
|
||||
}
|
||||
|
||||
get sectionAndNoteIsReadonly() {
|
||||
return (
|
||||
this.props.readonly
|
||||
&& this.isProductClickable
|
||||
&& (["cancel", "posted"].includes(this.props.record.evalContext.parent.state)
|
||||
|| this.props.record.evalContext.parent.locked)
|
||||
)
|
||||
}
|
||||
|
||||
get isSection() {
|
||||
return this.props.record.data.display_type === "line_section";
|
||||
}
|
||||
|
||||
get isSubSection() {
|
||||
return this.props.record.data.display_type === "line_subsection";
|
||||
}
|
||||
|
||||
get isSectionOrSubSection() {
|
||||
return this.isSection || this.isSubSection;
|
||||
}
|
||||
|
||||
isNote(record = null) {
|
||||
record = record || this.props.record;
|
||||
return record.data.display_type === "line_note";
|
||||
}
|
||||
|
||||
parseLabel(value) {
|
||||
return (this.productName && value && this.productName.concat("\n", value))
|
||||
|| (this.productName && !value && this.productName)
|
||||
|| (value || "");
|
||||
}
|
||||
|
||||
shouldShowWarning() {
|
||||
return (
|
||||
!this.productName &&
|
||||
this.props.show_label_warning &&
|
||||
!this.isSectionOrSubSection &&
|
||||
!this.isNote()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export const productLabelSectionAndNoteField = {
|
||||
...buildM2OFieldDescription(ProductLabelSectionAndNoteField),
|
||||
listViewWidth: [240, 400],
|
||||
supportedOptions: [
|
||||
...m2oSupportedOptions,
|
||||
{
|
||||
label: _t("Show Label Warning"),
|
||||
name: "show_label_warning",
|
||||
type: "boolean",
|
||||
default: false
|
||||
},
|
||||
],
|
||||
extractProps({ options }) {
|
||||
const props = extractM2OFieldProps(...arguments);
|
||||
props.show_label_warning = options.show_label_warning;
|
||||
return props;
|
||||
},
|
||||
};
|
||||
registry
|
||||
.category("fields")
|
||||
.add("product_label_section_and_note_field", productLabelSectionAndNoteField);
|
||||
|
|
@ -0,0 +1,14 @@
|
|||
.o_field_product_label_section_and_note_cell {
|
||||
|
||||
textarea {
|
||||
resize: none;
|
||||
}
|
||||
|
||||
div.o_input {
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
|
||||
@include media-only(print) {
|
||||
height: auto !important;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,66 @@
|
|||
<?xml version="1.0" encoding="UTF-8" ?>
|
||||
<templates>
|
||||
|
||||
<t t-name="account.ProductLabelSectionAndNoteField">
|
||||
<div class="o_field_product_label_section_and_note_cell">
|
||||
<t t-if="isNote()">
|
||||
<textarea
|
||||
class="o_input d-print-none border-0 fst-italic"
|
||||
placeholder="Enter a description"
|
||||
rows="1"
|
||||
t-att-class="sectionAndNoteClasses"
|
||||
t-att-readonly="sectionAndNoteIsReadonly"
|
||||
t-att-value="label"
|
||||
t-ref="labelNodeRef"
|
||||
t-key="props.readonly"
|
||||
/>
|
||||
</t>
|
||||
<t t-elif="isSectionOrSubSection">
|
||||
<input
|
||||
type="text"
|
||||
class="o_input text-wrap border-0 w-100"
|
||||
placeholder="Enter a description"
|
||||
t-att-class="sectionAndNoteClasses"
|
||||
t-att-readonly="sectionAndNoteIsReadonly"
|
||||
t-att-value="label"
|
||||
t-ref="labelNodeRef"
|
||||
t-key="props.readonly"
|
||||
/>
|
||||
</t>
|
||||
<t t-else="">
|
||||
<div class="d-flex align-items-center gap-1">
|
||||
<Many2One t-props="this.m2oProps" cssClass="'w-100'" t-on-keydown="onM2oInputKeydown"/>
|
||||
<t t-if="showLabelVisibilityToggler">
|
||||
<button
|
||||
class="btn fa fa-bars text-start o_external_button px-1"
|
||||
type="button"
|
||||
id="labelVisibilityButtonId"
|
||||
data-tooltip="Click or press enter to add a description"
|
||||
t-on-click="() => this.switchLabelVisibility()"
|
||||
/>
|
||||
</t>
|
||||
</div>
|
||||
<div
|
||||
t-if="props.readonly"
|
||||
class="o_input d-print-none border-0 fst-italic"
|
||||
t-att-class="{ ...sectionAndNoteClasses, 'd-none': !(columnIsProductAndLabel.value and label) }"
|
||||
t-out="label"
|
||||
/>
|
||||
<textarea
|
||||
t-else=""
|
||||
class="o_input d-print-none border-0 fst-italic"
|
||||
placeholder="Enter a description"
|
||||
rows="1"
|
||||
type="text"
|
||||
t-att-class="{ ...sectionAndNoteClasses, 'd-none': !(columnIsProductAndLabel.value and (label or labelVisibility.value)) }"
|
||||
t-att-value="label"
|
||||
t-ref="labelNodeRef"
|
||||
/>
|
||||
</t>
|
||||
<t t-if="isPrintMode.value">
|
||||
<div class="d-none d-print-block text-wrap" t-out="label"/>
|
||||
</t>
|
||||
</div>
|
||||
</t>
|
||||
|
||||
</templates>
|
||||
|
|
@ -0,0 +1,75 @@
|
|||
import {
|
||||
SectionAndNoteFieldOne2Many,
|
||||
sectionAndNoteFieldOne2Many,
|
||||
SectionAndNoteListRenderer,
|
||||
} from "@account/components/section_and_note_fields_backend/section_and_note_fields_backend";
|
||||
import { ProductNameAndDescriptionListRendererMixin } from "@product/product_name_and_description/product_name_and_description";
|
||||
import { registry } from "@web/core/registry";
|
||||
import { patch } from "@web/core/utils/patch";
|
||||
|
||||
export class ProductLabelSectionAndNoteListRender extends SectionAndNoteListRenderer {
|
||||
setup() {
|
||||
super.setup();
|
||||
this.descriptionColumn = "name";
|
||||
this.productColumns = ["product_id", "product_template_id"];
|
||||
this.conditionalColumns = ["product_id", "quantity", "product_uom_id"];
|
||||
}
|
||||
|
||||
processAllColumn(allColumns, list) {
|
||||
allColumns = allColumns.map((column) => {
|
||||
if (column["optional"] === "conditional" && this.conditionalColumns.includes(column["name"])) {
|
||||
/**
|
||||
* The preference should be different whether:
|
||||
* - It's a Vendor Bill or an Invoice
|
||||
* - Sale module is installed
|
||||
* Vendor Bills -> Product should be hidden by default
|
||||
* Invoices -> conditionalColumns should be hidden by default if Sale module is not installed
|
||||
*/
|
||||
const isBill = ["in_invoice", "in_refund", "in_receipt"].includes(this.props.list.evalContext.parent.move_type);
|
||||
const isInvoice = ["out_invoice", "out_refund", "out_receipt"].includes(this.props.list.evalContext.parent.move_type);
|
||||
const isSaleInstalled = this.props.list.evalContext.parent.is_sale_installed;
|
||||
column["optional"] = "show";
|
||||
if (isBill && column["name"] === "product_id") {
|
||||
column["optional"] = "hide";
|
||||
}
|
||||
else if (isInvoice && !isSaleInstalled) {
|
||||
column["optional"] = "hide";
|
||||
}
|
||||
}
|
||||
return column;
|
||||
});
|
||||
return super.processAllColumn(allColumns, list);
|
||||
}
|
||||
|
||||
isCellReadonly(column, record) {
|
||||
if (![...this.productColumns, "name"].includes(column.name)) {
|
||||
return super.isCellReadonly(column, record);
|
||||
}
|
||||
// The isCellReadonly method from the ListRenderer is used to determine the classes to apply to the cell.
|
||||
// We need this override to make sure some readonly classes are not applied to the cell if it is still editable.
|
||||
let isReadonly = super.isCellReadonly(column, record);
|
||||
return (
|
||||
isReadonly
|
||||
&& (["cancel", "posted"].includes(record.evalContext.parent.state)
|
||||
|| record.evalContext.parent.locked)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
patch(ProductLabelSectionAndNoteListRender.prototype, ProductNameAndDescriptionListRendererMixin);
|
||||
|
||||
export class ProductLabelSectionAndNoteOne2Many extends SectionAndNoteFieldOne2Many {
|
||||
static components = {
|
||||
...super.components,
|
||||
ListRenderer: ProductLabelSectionAndNoteListRender,
|
||||
};
|
||||
}
|
||||
|
||||
export const productLabelSectionAndNoteOne2Many = {
|
||||
...sectionAndNoteFieldOne2Many,
|
||||
component: ProductLabelSectionAndNoteOne2Many,
|
||||
};
|
||||
|
||||
registry
|
||||
.category("fields")
|
||||
.add("product_label_section_and_note_field_o2m", productLabelSectionAndNoteOne2Many);
|
||||
|
|
@ -0,0 +1,93 @@
|
|||
import { _t } from "@web/core/l10n/translation";
|
||||
import { registry } from "@web/core/registry";
|
||||
import { radioField, RadioField } from "@web/views/fields/radio/radio_field";
|
||||
import { onWillStart, useState } from "@odoo/owl";
|
||||
import { useService } from "@web/core/utils/hooks";
|
||||
import { deepCopy } from "@web/core/utils/objects";
|
||||
|
||||
|
||||
const labels = {
|
||||
'in_invoice': _t("Bill"),
|
||||
'out_invoice': _t("Invoice"),
|
||||
'in_receipt': _t("Receipt"),
|
||||
'out_receipt': _t("Receipt"),
|
||||
};
|
||||
|
||||
const in_move_types = ['in_invoice', 'in_receipt']
|
||||
const out_move_types = ['out_invoice', 'out_receipt']
|
||||
|
||||
|
||||
export class ReceiptSelector extends RadioField {
|
||||
static template = "account.ReceiptSelector";
|
||||
static props = {
|
||||
...RadioField.props,
|
||||
};
|
||||
|
||||
setup() {
|
||||
super.setup();
|
||||
this.lazySession = useService("lazy_session");
|
||||
this.show_sale_receipts = useState({ value: false });
|
||||
onWillStart(()=> {
|
||||
this.lazySession.getValue("show_sale_receipts", (show_sale_receipts) => {
|
||||
this.show_sale_receipts.value = show_sale_receipts;
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove the unwanted options and update the English labels
|
||||
* @override
|
||||
*/
|
||||
get items() {
|
||||
const original_items = super.items;
|
||||
if ( this.type !== 'selection' ) {
|
||||
return original_items;
|
||||
}
|
||||
|
||||
// Use a copy to avoid updating the original selection labels
|
||||
let items = deepCopy(original_items)
|
||||
|
||||
let allowedValues = [];
|
||||
if ( in_move_types.includes(this.value) ) {
|
||||
allowedValues = in_move_types
|
||||
} else if (out_move_types.includes(this.value) && this.show_sale_receipts.value ) {
|
||||
allowedValues = out_move_types
|
||||
}
|
||||
|
||||
if ( allowedValues.length > 1 ) {
|
||||
// Filter only the wanted items
|
||||
items = items.filter((item) => {
|
||||
return (allowedValues.includes(item[0]));
|
||||
});
|
||||
|
||||
// Update the label of the wanted items
|
||||
items.forEach((item) => {
|
||||
if (item[0] in labels) {
|
||||
item[1] = labels[item[0]];
|
||||
}
|
||||
});
|
||||
}
|
||||
return items;
|
||||
}
|
||||
|
||||
get string() {
|
||||
if ( this.type === 'selection' ) {
|
||||
// Use the original labels and not the modified ones
|
||||
return this.value !== false
|
||||
? this.props.record.fields[this.props.name].selection.find((i) => i[0] === this.value)[1]
|
||||
: "";
|
||||
}
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
export const receiptSelector = {
|
||||
...radioField,
|
||||
additionalClasses: ['o_field_radio'],
|
||||
component: ReceiptSelector,
|
||||
extractProps() {
|
||||
return radioField.extractProps(...arguments);
|
||||
},
|
||||
};
|
||||
|
||||
registry.category("fields").add("receipt_selector", receiptSelector);
|
||||
|
|
@ -0,0 +1,15 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<templates xml:space="preserve">
|
||||
|
||||
<t t-name="account.ReceiptSelector">
|
||||
<div>
|
||||
<t t-if="props.readonly || ['in_refund', 'out_refund'].includes(value) || (!show_sale_receipts.value and ['out_invoice', 'out_receipt'].includes(value))">
|
||||
<span t-esc="string" t-att-raw-value="value" />
|
||||
</t>
|
||||
<t t-else="">
|
||||
<t t-call="web.RadioField"/>
|
||||
</t>
|
||||
</div>
|
||||
</t>
|
||||
|
||||
</templates>
|
||||
|
|
@ -2,29 +2,76 @@
|
|||
// 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 {
|
||||
--o-SectionAndNote-border-color: #{$gray-500};
|
||||
$_SectionAndNote-transition: 0.1s ease-in-out;
|
||||
|
||||
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;
|
||||
tr {
|
||||
&.o_is_line_note {
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
&.o_is_line_section, &.o_is_line_subsection {
|
||||
--table-striped-bg: var(--table-bg-type);
|
||||
|
||||
.o_list_section_options {
|
||||
width: 1px; // to prevent the column to expand
|
||||
}
|
||||
|
||||
.o_field_product_label_section_and_note_cell .o_input {
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
}
|
||||
|
||||
&.o_is_line_section {
|
||||
--table-hover-bg: #{rgba($body-emphasis-color, .08)};
|
||||
--table-bg-type: #{rgba($body-emphasis-color, .06)};
|
||||
--ListRenderer-data-row-focused-striped-bg: var(--table-bg-type);
|
||||
|
||||
font-weight: $font-weight-bolder;
|
||||
|
||||
&:where(:not(:empty)) {
|
||||
// When dragging, an empty tr is created we don't want
|
||||
// a border here.
|
||||
border-top-color: var(--o-SectionAndNote-border-color);
|
||||
}
|
||||
|
||||
&.o_dragged {
|
||||
border-width: $border-width 0;
|
||||
}
|
||||
}
|
||||
|
||||
&.o_is_line_subsection {
|
||||
--table-hover-bg: #{rgba($body-emphasis-color, .05)};
|
||||
--table-bg-type: #{rgba($body-emphasis-color, .03)};
|
||||
|
||||
font-weight: $font-weight-bold;
|
||||
|
||||
&.o_dragged {
|
||||
// We rely on td borders, when dragged we need to remove
|
||||
// the borders on .o_data_row to avoid double borders
|
||||
--ListRenderer-data-row-border-bottom-width: 0;
|
||||
}
|
||||
|
||||
td {
|
||||
border-bottom-width: $border-width;
|
||||
|
||||
&:not(.o_handle_cell) {
|
||||
border-color: var(--o-SectionAndNote-border-color);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Check if next sibling is a section to apply a border-color.
|
||||
&:where(:not(.o_dragged):has(+ .o_is_line_section:not(.o_dragged, .d-table-row))) {
|
||||
border-color: var(--o-SectionAndNote-border-color);
|
||||
}
|
||||
}
|
||||
|
||||
// Check if first tr in the table is a section to apply a border-color on
|
||||
// the <th> els.
|
||||
&:where(:has(tbody > tr:first-child.o_is_line_section:not(.o_dragged, .d-table-row))) th {
|
||||
border-color: var(--o-SectionAndNote-border-color);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,14 +1,88 @@
|
|||
/** @odoo-module **/
|
||||
|
||||
import { Component, useEffect } from "@odoo/owl";
|
||||
import { _t } from "@web/core/l10n/translation";
|
||||
import { x2ManyCommands } from "@web/core/orm_service";
|
||||
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";
|
||||
import { standardFieldProps } from "@web/views/fields/standard_field_props";
|
||||
import { ListTextField, TextField } from "@web/views/fields/text/text_field";
|
||||
import { X2ManyField, x2ManyField } from "@web/views/fields/x2many/x2many_field";
|
||||
import { ListRenderer } from "@web/views/list/list_renderer";
|
||||
|
||||
const { Component, useEffect } = owl;
|
||||
const SHOW_ALL_ITEMS_TOOLTIP = _t("Some lines can be on the next page, display them to unlock actions on section.");
|
||||
const DISABLED_MOVE_DOWN_ITEM_TOOLTIP = _t("Some lines of the next section can be on the next page, display them to unlock the action.");
|
||||
|
||||
const DISPLAY_TYPES = {
|
||||
NOTE: "line_note",
|
||||
SECTION: "line_section",
|
||||
SUBSECTION: "line_subsection",
|
||||
};
|
||||
|
||||
export function getParentSectionRecord(list, record) {
|
||||
const { sectionIndex } = getRecordsUntilSection(list, record, false, record.data.display_type !== DISPLAY_TYPES.SUBSECTION);
|
||||
return list.records[sectionIndex];
|
||||
}
|
||||
|
||||
function getPreviousSectionRecords(list, record) {
|
||||
const { sectionRecords } = getRecordsUntilSection(list, record, false);
|
||||
return sectionRecords;
|
||||
}
|
||||
|
||||
export function getSectionRecords(list, record, subSection) {
|
||||
const { sectionRecords } = getRecordsUntilSection(list, record, true, subSection);
|
||||
return sectionRecords;
|
||||
}
|
||||
|
||||
function hasNextSection(list, record) {
|
||||
const { sectionIndex } = getRecordsUntilSection(list, record, true);
|
||||
return sectionIndex < list.records.length && list.records[sectionIndex].data.display_type === record.data.display_type;
|
||||
}
|
||||
|
||||
function hasPreviousSection(list, record) {
|
||||
const { sectionIndex } = getRecordsUntilSection(list, record, false);
|
||||
return sectionIndex >= 0 && list.records[sectionIndex].data.display_type === record.data.display_type;
|
||||
}
|
||||
|
||||
function getRecordsUntilSection(list, record, asc, subSection) {
|
||||
const stopAtTypes = [DISPLAY_TYPES.SECTION];
|
||||
if (subSection ?? record.data.display_type === DISPLAY_TYPES.SUBSECTION) {
|
||||
stopAtTypes.push(DISPLAY_TYPES.SUBSECTION);
|
||||
}
|
||||
|
||||
const sectionRecords = [];
|
||||
let index = list.records.findIndex(listRecord => listRecord.id === record.id);
|
||||
if (asc) {
|
||||
sectionRecords.push(list.records[index]);
|
||||
index++;
|
||||
while (index < list.records.length && !stopAtTypes.includes(list.records[index].data.display_type)) {
|
||||
sectionRecords.push(list.records[index]);
|
||||
index++;
|
||||
}
|
||||
} else {
|
||||
index--;
|
||||
while (index >= 0 && !stopAtTypes.includes(list.records[index].data.display_type)) {
|
||||
sectionRecords.unshift(list.records[index]);
|
||||
index--;
|
||||
}
|
||||
sectionRecords.unshift(list.records[index]);
|
||||
}
|
||||
|
||||
return {
|
||||
sectionRecords,
|
||||
sectionIndex: index,
|
||||
};
|
||||
}
|
||||
|
||||
export class SectionAndNoteListRenderer extends ListRenderer {
|
||||
static template = "account.SectionAndNoteListRenderer";
|
||||
static recordRowTemplate = "account.SectionAndNoteListRenderer.RecordRow";
|
||||
static props = [
|
||||
...super.props,
|
||||
"aggregatedFields",
|
||||
"subsections",
|
||||
"hidePrices",
|
||||
"hideComposition",
|
||||
];
|
||||
|
||||
/**
|
||||
* The purpose of this extension is to allow sections and notes in the one2many list
|
||||
* primarily used on Sales Orders and Invoices
|
||||
|
|
@ -18,47 +92,335 @@ export class SectionAndNoteListRenderer extends ListRenderer {
|
|||
setup() {
|
||||
super.setup();
|
||||
this.titleField = "name";
|
||||
this.priceColumns = [...this.props.aggregatedFields, "price_unit"];
|
||||
// invisible fields to force copy when duplicating a section
|
||||
this.copyFields = ["display_type", "collapse_composition", "collapse_prices"];
|
||||
useEffect(
|
||||
() => this.focusToName(this.props.list.editedRecord),
|
||||
() => [this.props.list.editedRecord]
|
||||
)
|
||||
(editedRecord) => this.focusToName(editedRecord),
|
||||
() => [this.editedRecord]
|
||||
);
|
||||
}
|
||||
|
||||
get disabledMoveDownItemTooltip() {
|
||||
return DISABLED_MOVE_DOWN_ITEM_TOOLTIP;
|
||||
}
|
||||
|
||||
get showAllItemsTooltip() {
|
||||
return SHOW_ALL_ITEMS_TOOLTIP;
|
||||
}
|
||||
|
||||
get hidePrices() {
|
||||
return this.record.data.collapse_prices;
|
||||
}
|
||||
|
||||
get hideComposition() {
|
||||
return this.record.data.collapse_composition;
|
||||
}
|
||||
|
||||
get disablePricesButton() {
|
||||
return this.shouldCollapse(this.record, 'collapse_prices') || this.disableCompositionButton;
|
||||
}
|
||||
|
||||
get disableCompositionButton() {
|
||||
return this.shouldCollapse(this.record, 'collapse_composition');
|
||||
}
|
||||
|
||||
async toggleCollapse(record, fieldName) {
|
||||
// We don't want to have 'collapse_prices' & 'collapse_composition' set to True at the same time
|
||||
const reverseFieldName = fieldName === 'collapse_prices' ? 'collapse_composition' : 'collapse_prices';
|
||||
const changes = {
|
||||
[fieldName]: !record.data[fieldName],
|
||||
[reverseFieldName]: false,
|
||||
};
|
||||
await record.update(changes);
|
||||
}
|
||||
|
||||
async addRowAfterSection(record, addSubSection) {
|
||||
const canProceed = await this.props.list.leaveEditMode({ canAbandon: false });
|
||||
if (!canProceed) {
|
||||
return;
|
||||
}
|
||||
|
||||
const index =
|
||||
this.props.list.records.indexOf(record) +
|
||||
getSectionRecords(this.props.list, record).length -
|
||||
1;
|
||||
const context = {
|
||||
default_display_type: addSubSection ? DISPLAY_TYPES.SUBSECTION : DISPLAY_TYPES.SECTION,
|
||||
};
|
||||
await this.props.list.addNewRecordAtIndex(index, { context });
|
||||
}
|
||||
|
||||
async addNoteInSection(record) {
|
||||
const canProceed = await this.props.list.leaveEditMode({ canAbandon: false });
|
||||
if (!canProceed) {
|
||||
return;
|
||||
}
|
||||
|
||||
const index =
|
||||
this.props.list.records.indexOf(record) +
|
||||
getSectionRecords(this.props.list, record, true).length -
|
||||
1;
|
||||
const context = {
|
||||
default_display_type: DISPLAY_TYPES.NOTE,
|
||||
};
|
||||
await this.props.list.addNewRecordAtIndex(index, { context });
|
||||
}
|
||||
|
||||
async addRowInSection(record, addSubSection) {
|
||||
const canProceed = await this.props.list.leaveEditMode({ canAbandon: false });
|
||||
if (!canProceed) {
|
||||
return;
|
||||
}
|
||||
|
||||
const index =
|
||||
this.props.list.records.indexOf(record) +
|
||||
getSectionRecords(this.props.list, record, !addSubSection).length -
|
||||
1;
|
||||
const context = this.getInsertLineContext(record, addSubSection);
|
||||
if (addSubSection) {
|
||||
context["default_display_type"] = DISPLAY_TYPES.SUBSECTION;
|
||||
}
|
||||
await this.props.list.addNewRecordAtIndex(index, { context });
|
||||
}
|
||||
|
||||
/**
|
||||
* Hook for other modules to conditionally specify defaults for new lines
|
||||
*/
|
||||
getInsertLineContext(_record, _addSubSection) {
|
||||
return {};
|
||||
}
|
||||
|
||||
canUseFormatter(column, record) {
|
||||
if (
|
||||
this.isSection(record) &&
|
||||
this.props.aggregatedFields.includes(column.name)
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
return super.canUseFormatter(column, record);
|
||||
}
|
||||
|
||||
async deleteSection(record) {
|
||||
if (this.editedRecord && this.editedRecord !== record) {
|
||||
const left = await this.props.list.leaveEditMode({ canAbandon: false });
|
||||
if (!left) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
if (this.activeActions.onDelete) {
|
||||
const method = this.activeActions.unlink ? "unlink" : "delete";
|
||||
const commands = [];
|
||||
const sectionRecords = getSectionRecords(this.props.list, record);
|
||||
for (const sectionRecord of sectionRecords) {
|
||||
commands.push(
|
||||
x2ManyCommands[method](sectionRecord.resId || sectionRecord._virtualId)
|
||||
);
|
||||
}
|
||||
await this.props.list.applyCommands(commands);
|
||||
}
|
||||
}
|
||||
|
||||
async duplicateSection(record) {
|
||||
const left = await this.props.list.leaveEditMode();
|
||||
if (!left) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { sectionRecords, sectionIndex } = getRecordsUntilSection(this.props.list, record, true)
|
||||
const recordsToDuplicate = sectionRecords.filter((record) => {
|
||||
return this.shouldDuplicateSectionItem(record);
|
||||
});
|
||||
await this.props.list.duplicateRecords(recordsToDuplicate, {
|
||||
targetIndex: sectionIndex,
|
||||
copyFields: this.copyFields,
|
||||
});
|
||||
}
|
||||
|
||||
async editNextRecord(record, group) {
|
||||
const canProceed = await this.props.list.leaveEditMode({ validate: true });
|
||||
if (!canProceed) {
|
||||
return;
|
||||
}
|
||||
|
||||
const iter = getRecordsUntilSection(this.props.list, record, true, true);
|
||||
if (this.isSection(record) || iter.sectionRecords.length === 1) {
|
||||
return this.props.list.addNewRecordAtIndex(iter.sectionIndex - 1);
|
||||
} else {
|
||||
return super.editNextRecord(record, group);
|
||||
}
|
||||
}
|
||||
|
||||
expandPager() {
|
||||
return this.props.list.load({ limit: this.props.list.count });
|
||||
}
|
||||
|
||||
focusToName(editRec) {
|
||||
if (editRec && editRec.isVirtual && this.isSectionOrNote(editRec)) {
|
||||
const col = this.state.columns.find((c) => c.name === this.titleField);
|
||||
if (editRec && editRec.isNew && this.isSectionOrNote(editRec)) {
|
||||
const col = this.columns.find((c) => c.name === this.titleField);
|
||||
this.focusCell(col, null);
|
||||
}
|
||||
}
|
||||
|
||||
isSectionOrNote(record=null) {
|
||||
hasNextSection(record) {
|
||||
return hasNextSection(this.props.list, record);
|
||||
}
|
||||
|
||||
hasPreviousSection(record) {
|
||||
return hasPreviousSection(this.props.list, record);
|
||||
}
|
||||
|
||||
isNextSectionInPage(record) {
|
||||
if (this.props.list.count <= this.props.list.offset + this.props.list.limit) {
|
||||
// if last page
|
||||
return true;
|
||||
}
|
||||
const sectionRecords = getSectionRecords(this.props.list, record);
|
||||
const index = this.props.list.records.indexOf(record) + sectionRecords.length;
|
||||
if (index >= this.props.list.limit) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const { sectionIndex } = getRecordsUntilSection(this.props.list, this.props.list.records[index], true);
|
||||
return sectionIndex < this.props.list.limit;
|
||||
}
|
||||
|
||||
isSectionOrNote(record = null) {
|
||||
record = record || this.record;
|
||||
return ['line_section', 'line_note'].includes(record.data.display_type);
|
||||
return [DISPLAY_TYPES.SECTION, DISPLAY_TYPES.SUBSECTION, DISPLAY_TYPES.NOTE].includes(
|
||||
record.data.display_type
|
||||
);
|
||||
}
|
||||
|
||||
isSection(record = null) {
|
||||
record = record || this.record;
|
||||
return [DISPLAY_TYPES.SECTION, DISPLAY_TYPES.SUBSECTION].includes(record.data.display_type);
|
||||
}
|
||||
|
||||
isSectionInPage(record) {
|
||||
if (this.props.list.count <= this.props.list.offset + this.props.list.limit) {
|
||||
// if last page
|
||||
return true;
|
||||
}
|
||||
const { sectionIndex } = getRecordsUntilSection(this.props.list, record, true);
|
||||
return sectionIndex < this.props.list.limit;
|
||||
}
|
||||
|
||||
isSortable() {
|
||||
return false;
|
||||
}
|
||||
|
||||
isTopSection(record) {
|
||||
return record.data.display_type === DISPLAY_TYPES.SECTION;
|
||||
}
|
||||
|
||||
isSubSection(record) {
|
||||
return record.data.display_type === DISPLAY_TYPES.SUBSECTION;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines whether the line should be collapsed.
|
||||
* - If the parent is a section: use the parent’s field.
|
||||
* - If the parent is a subsection: use parent subsection OR its section.
|
||||
* @param {object} record
|
||||
* @param {string} fieldName
|
||||
* @param {boolean} checkSection - if true, also evaluates the collapse state for section or
|
||||
* subsection records
|
||||
* @returns {boolean}
|
||||
*/
|
||||
shouldCollapse(record, fieldName, checkSection = false) {
|
||||
const parentSection = getParentSectionRecord(this.props.list, record);
|
||||
|
||||
// --- For sections ---
|
||||
if (this.isSection(record) && checkSection) {
|
||||
if (this.isTopSection(record)) {
|
||||
return record.data[fieldName];
|
||||
}
|
||||
if (this.isSubSection(record)) {
|
||||
return record.data[fieldName] || parentSection?.data[fieldName];
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// `line_section` never collapses unless explicitly checked above
|
||||
if (this.isTopSection(record)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!parentSection) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// --- For regular lines ---
|
||||
if (this.isSubSection(parentSection)) {
|
||||
const grandParent = getParentSectionRecord(this.props.list, parentSection);
|
||||
return parentSection.data[fieldName] || grandParent?.data[fieldName];
|
||||
}
|
||||
|
||||
return !!parentSection.data[fieldName];
|
||||
}
|
||||
|
||||
getRowClass(record) {
|
||||
const existingClasses = super.getRowClass(record);
|
||||
return `${existingClasses} o_is_${record.data.display_type}`;
|
||||
let newClasses = `${existingClasses} o_is_${record.data.display_type}`;
|
||||
if (this.props.hideComposition && this.shouldCollapse(record, 'collapse_composition')) {
|
||||
newClasses += " text-muted";
|
||||
}
|
||||
return newClasses;
|
||||
}
|
||||
|
||||
getCellClass(column, record) {
|
||||
const classNames = super.getCellClass(column, record);
|
||||
if (this.isSectionOrNote(record) && column.widget !== "handle" && column.name !== this.titleField) {
|
||||
let classNames = super.getCellClass(column, record);
|
||||
// For hiding columnns of section and note
|
||||
if (
|
||||
this.isSectionOrNote(record)
|
||||
&& column.widget !== "handle"
|
||||
&& ![column.name, ...this.props.aggregatedFields].includes(column.name)
|
||||
) {
|
||||
return `${classNames} o_hidden`;
|
||||
}
|
||||
// For muting the price columns
|
||||
if (
|
||||
this.props.hidePrices
|
||||
&& this.shouldCollapse(record, 'collapse_prices')
|
||||
&& this.priceColumns.includes(column.name)
|
||||
) {
|
||||
classNames += " text-muted";
|
||||
}
|
||||
|
||||
return classNames;
|
||||
}
|
||||
|
||||
getColumns(record) {
|
||||
const columns = super.getColumns(record);
|
||||
if (this.isSectionOrNote(record)) {
|
||||
return this.getSectionColumns(columns);
|
||||
return this.getSectionColumns(columns, record);
|
||||
}
|
||||
return columns;
|
||||
}
|
||||
|
||||
getSectionColumns(columns) {
|
||||
const sectionCols = columns.filter((col) => col.widget === "handle" || col.type === "field" && col.name === this.titleField);
|
||||
getFormattedValue(column, record) {
|
||||
if (this.isSection(record) && this.props.aggregatedFields.includes(column.name)) {
|
||||
const total = getSectionRecords(this.props.list, record)
|
||||
.filter((record) => !this.isSection(record))
|
||||
.reduce((total, record) => total + record.data[column.name], 0);
|
||||
const formatter = registry.category("formatters").get(column.fieldType, (val) => val);
|
||||
return formatter(total, {
|
||||
...formatter.extractOptions?.(column),
|
||||
data: record.data,
|
||||
field: record.fields[column.name],
|
||||
});
|
||||
}
|
||||
return super.getFormattedValue(column, record);
|
||||
}
|
||||
|
||||
getSectionColumns(columns, record) {
|
||||
const sectionCols = columns.filter(
|
||||
(col) =>
|
||||
col.widget === "handle"
|
||||
|| col.name === this.titleField
|
||||
|| (this.isSection(record) && this.props.aggregatedFields.includes(col.name))
|
||||
);
|
||||
return sectionCols.map((col) => {
|
||||
if (col.name === this.titleField) {
|
||||
return { ...col, colspan: columns.length - sectionCols.length + 1 };
|
||||
|
|
@ -67,23 +429,119 @@ export class SectionAndNoteListRenderer extends ListRenderer {
|
|||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
SectionAndNoteListRenderer.template = "account.sectionAndNoteListRenderer";
|
||||
|
||||
export class SectionAndNoteFieldOne2Many extends X2ManyField {}
|
||||
SectionAndNoteFieldOne2Many.additionalClasses = ['o_field_one2many'];
|
||||
SectionAndNoteFieldOne2Many.components = {
|
||||
...X2ManyField.components,
|
||||
ListRenderer: SectionAndNoteListRenderer,
|
||||
};
|
||||
async moveSectionDown(record) {
|
||||
const canProceed = await this.props.list.leaveEditMode({ canAbandon: false });
|
||||
if (!canProceed) {
|
||||
return;
|
||||
}
|
||||
|
||||
export class SectionAndNoteText extends Component {
|
||||
get componentToUse() {
|
||||
return this.props.record.data.display_type === 'line_section' ? CharField : TextField;
|
||||
const sectionRecords = getSectionRecords(this.props.list, record);
|
||||
const index = this.props.list.records.indexOf(record) + sectionRecords.length;
|
||||
const nextSectionRecords = getSectionRecords(this.props.list, this.props.list.records[index]);
|
||||
return this.swapSections(sectionRecords, nextSectionRecords);
|
||||
}
|
||||
|
||||
async moveSectionUp(record) {
|
||||
const canProceed = await this.props.list.leaveEditMode({ canAbandon: false });
|
||||
if (!canProceed) {
|
||||
return;
|
||||
}
|
||||
|
||||
const previousSectionRecords = getPreviousSectionRecords(this.props.list, record);
|
||||
const sectionRecords = getSectionRecords(this.props.list, record);
|
||||
return this.swapSections(previousSectionRecords, sectionRecords);
|
||||
}
|
||||
|
||||
shouldDuplicateSectionItem(record) {
|
||||
return true;
|
||||
}
|
||||
|
||||
async swapSections(sectionRecords1, sectionRecords2) {
|
||||
const commands = [];
|
||||
let sequence = sectionRecords1[0].data[this.props.list.handleField];
|
||||
for (const record of sectionRecords2) {
|
||||
commands.push(x2ManyCommands.update(record.resId || record._virtualId, {
|
||||
[this.props.list.handleField]: sequence++,
|
||||
}));
|
||||
}
|
||||
for (const record of sectionRecords1) {
|
||||
commands.push(x2ManyCommands.update(record.resId || record._virtualId, {
|
||||
[this.props.list.handleField]: sequence++,
|
||||
}));
|
||||
}
|
||||
await this.props.list.applyCommands(commands, { sort: true });
|
||||
}
|
||||
|
||||
/**
|
||||
* @override
|
||||
* Reset the values of `collapse_` fields of the subsection if it is dragged
|
||||
*/
|
||||
async sortDrop(dataRowId, dataGroupId, options) {
|
||||
await super.sortDrop(dataRowId, dataGroupId, options);
|
||||
|
||||
const record = this.props.list.records.find(r => r.id === dataRowId);
|
||||
const parentSection = getParentSectionRecord(this.props.list, record);
|
||||
const commands = [];
|
||||
|
||||
if (this.resetOnResequence(record, parentSection)) {
|
||||
commands.push(x2ManyCommands.update(record.resId || record._virtualId, {
|
||||
...this.fieldsToReset(),
|
||||
}));
|
||||
}
|
||||
|
||||
await this.props.list.applyCommands(commands);
|
||||
}
|
||||
|
||||
resetOnResequence(record, parentSection) {
|
||||
return (
|
||||
this.isSubSection(record)
|
||||
&& parentSection?.data.collapse_composition
|
||||
&& (record.data.collapse_composition || record.data.collapse_prices)
|
||||
);
|
||||
}
|
||||
|
||||
fieldsToReset() {
|
||||
return {
|
||||
...(this.props.hideComposition && { collapse_composition: false }),
|
||||
...(this.props.hidePrices && { collapse_prices: false }),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export class SectionAndNoteFieldOne2Many extends X2ManyField {
|
||||
static components = {
|
||||
...super.components,
|
||||
ListRenderer: SectionAndNoteListRenderer,
|
||||
};
|
||||
static props = {
|
||||
...super.props,
|
||||
aggregatedFields: Array,
|
||||
hideComposition: Boolean,
|
||||
hidePrices: Boolean,
|
||||
subsections: Boolean,
|
||||
};
|
||||
|
||||
get rendererProps() {
|
||||
const rp = super.rendererProps;
|
||||
if (this.props.viewMode === "list") {
|
||||
rp.aggregatedFields = this.props.aggregatedFields;
|
||||
rp.hideComposition = this.props.hideComposition;
|
||||
rp.hidePrices = this.props.hidePrices;
|
||||
rp.subsections = this.props.subsections;
|
||||
}
|
||||
return rp;
|
||||
}
|
||||
}
|
||||
|
||||
export class SectionAndNoteText extends Component {
|
||||
static template = "account.SectionAndNoteText";
|
||||
static props = { ...standardFieldProps };
|
||||
|
||||
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() {
|
||||
|
|
@ -93,6 +551,33 @@ export class ListSectionAndNoteText extends SectionAndNoteText {
|
|||
}
|
||||
}
|
||||
|
||||
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);
|
||||
export const sectionAndNoteFieldOne2Many = {
|
||||
...x2ManyField,
|
||||
component: SectionAndNoteFieldOne2Many,
|
||||
additionalClasses: [...(x2ManyField.additionalClasses || []), "o_field_one2many"],
|
||||
extractProps: (staticInfo, dynamicInfo) => {
|
||||
return {
|
||||
...x2ManyField.extractProps(staticInfo, dynamicInfo),
|
||||
aggregatedFields: staticInfo.attrs.aggregated_fields
|
||||
? staticInfo.attrs.aggregated_fields.split(/\s*,\s*/)
|
||||
: [],
|
||||
hideComposition: staticInfo.options?.hide_composition ?? false,
|
||||
hidePrices: staticInfo.options?.hide_prices ?? false,
|
||||
subsections: staticInfo.options?.subsections ?? false,
|
||||
};
|
||||
},
|
||||
};
|
||||
|
||||
export const sectionAndNoteText = {
|
||||
component: SectionAndNoteText,
|
||||
additionalClasses: ["o_field_text"],
|
||||
};
|
||||
|
||||
export const listSectionAndNoteText = {
|
||||
...sectionAndNoteText,
|
||||
component: ListSectionAndNoteText,
|
||||
};
|
||||
|
||||
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);
|
||||
|
|
|
|||
|
|
@ -1,12 +1,85 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<templates>
|
||||
<t t-name="account.sectionAndNoteListRenderer" t-inherit="web.ListRenderer" t-inherit-mode="primary" owl="1">
|
||||
<t t-name="account.SectionAndNoteListRenderer" t-inherit="web.ListRenderer" t-inherit-mode="primary">
|
||||
<xpath expr="//table" position="attributes">
|
||||
<attribute name="class" position="add" separator=" ">o_section_and_note_list_view</attribute>
|
||||
<attribute name="class" add="o_section_and_note_list_view" separator=" "/>
|
||||
</xpath>
|
||||
</t>
|
||||
<t t-name="account.SectionAndNoteListRenderer.RecordRow" t-inherit="web.ListRenderer.RecordRow">
|
||||
<xpath expr="//td[hasclass('o_list_record_remove')]" position="attributes">
|
||||
<attribute name="t-if">!isSection(record)</attribute>
|
||||
</xpath>
|
||||
<xpath expr="//td[hasclass('o_list_record_remove')]" position="after">
|
||||
<td t-else="" class="o_list_section_options w-print-0 p-print-0 text-center">
|
||||
<Dropdown position="'bottom-end'" t-if="!props.readonly">
|
||||
<button class="btn d-table-cell border-0 py-0 px-1 cursor-pointer">
|
||||
<i class="fa fa-ellipsis-v"/>
|
||||
</button>
|
||||
<t t-set-slot="content">
|
||||
<t t-if="this.isSectionInPage(record)">
|
||||
<DropdownItem onSelected="() => this.addRowInSection(record, false)">
|
||||
<i class="me-1 fa fa-fw fa-plus"/><span>Add a line</span>
|
||||
</DropdownItem>
|
||||
<t t-if="this.isTopSection(record)">
|
||||
<t t-if="props.subsections">
|
||||
<DropdownItem onSelected="() => this.addRowInSection(record, true)">
|
||||
<i class="me-1 fa fa-fw fa-level-down"/><span>Add a subsection</span>
|
||||
</DropdownItem>
|
||||
</t>
|
||||
</t>
|
||||
<DropdownItem
|
||||
t-if="props.hidePrices"
|
||||
onSelected="() => this.toggleCollapse(record, 'collapse_prices')"
|
||||
attrs="{ 'class': disablePricesButton ? 'disabled' : '' }"
|
||||
>
|
||||
<i class="me-1 fa fa-fw" t-att-class="hidePrices ? 'fa-eye' : 'fa-eye-slash'"/>
|
||||
<span t-if="hidePrices">Show Prices</span>
|
||||
<span t-else="">Hide Prices</span>
|
||||
</DropdownItem>
|
||||
<t t-name="composition_button">
|
||||
<DropdownItem
|
||||
t-if="props.hideComposition"
|
||||
onSelected="() => this.toggleCollapse(record, 'collapse_composition')"
|
||||
attrs="{ 'class': disableCompositionButton ? 'disabled' : '' }"
|
||||
>
|
||||
<i class="me-1 fa fa-fw" t-att-class="hideComposition ? 'fa-eye' : 'fa-eye-slash'"/>
|
||||
<span t-if="hideComposition">Show Composition</span>
|
||||
<span t-else="">Hide Composition</span>
|
||||
</DropdownItem>
|
||||
</t>
|
||||
<DropdownItem onSelected="() => this.addNoteInSection(record)">
|
||||
<i class="me-1 fa fa-fw fa-sticky-note-o"/><span>Add a note</span>
|
||||
</DropdownItem>
|
||||
<DropdownItem t-if="this.hasPreviousSection(record)" onSelected="() => this.moveSectionUp(record)">
|
||||
<i class="me-1 fa fa-fw fa-arrow-up"/><span>Move Up</span>
|
||||
</DropdownItem>
|
||||
<t t-set="nextSectionInPage" t-value="this.isNextSectionInPage(record)"/>
|
||||
<t t-set="moveDownItemAttrs" t-value="nextSectionInPage ? {} : { 'data-tooltip': this.disabledMoveDownItemTooltip }"/>
|
||||
<t t-set="moveDownItemDefaultProps" t-value="{ attrs: moveDownItemAttrs, class: { 'text-muted': !nextSectionInPage } }"/>
|
||||
<DropdownItem t-if="this.hasNextSection(record)" t-props="moveDownItemDefaultProps" onSelected="() => this.moveSectionDown(record)">
|
||||
<i class="me-1 fa fa-fw fa-arrow-down"/><span>Move Down</span>
|
||||
</DropdownItem>
|
||||
<DropdownItem onSelected="() => this.duplicateSection(record)">
|
||||
<i class="me-1 fa fa-fw fa-clone"/><span>Duplicate</span>
|
||||
</DropdownItem>
|
||||
<t t-if="hasDeleteButton">
|
||||
<DropdownItem onSelected="() => this.deleteSection(record)" class="'text-danger'">
|
||||
<i class="me-1 fa fa-fw fa-trash"/><span>Delete</span>
|
||||
</DropdownItem>
|
||||
</t>
|
||||
</t>
|
||||
<t t-else="">
|
||||
<DropdownItem onSelected="() => this.expandPager()" attrs="{ 'data-tooltip': this.showAllItemsTooltip }">
|
||||
<i class="me-1 fa fa-fw fa-expand"/><span>Show all lines</span>
|
||||
</DropdownItem>
|
||||
</t>
|
||||
</t>
|
||||
</Dropdown>
|
||||
</td>
|
||||
</xpath>
|
||||
</t>
|
||||
|
||||
<t t-name="account.SectionAndNoteText" owl="1">
|
||||
<t t-name="account.SectionAndNoteText">
|
||||
<t t-component="componentToUse" t-props="props"/>
|
||||
</t>
|
||||
|
||||
|
|
|
|||
|
|
@ -1,33 +0,0 @@
|
|||
/** @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});
|
||||
|
|
@ -1,8 +0,0 @@
|
|||
<?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,16 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<templates xml:space="preserve">
|
||||
<t t-name="account.TaxAutoComplete" t-inherit="web.AutoComplete">
|
||||
<xpath expr="//t[@t-out='option.label']" position="replace">
|
||||
<t t-if="option.data.record.tax_scope">
|
||||
<div class="tax_autocomplete_grid">
|
||||
<div t-out="option.label"/>
|
||||
<div t-esc="option.data.record.tax_scope" class="text-muted"/>
|
||||
</div>
|
||||
</t>
|
||||
<t t-else="">
|
||||
<span t-out="option.label"/>
|
||||
</t>
|
||||
</xpath>
|
||||
</t>
|
||||
</templates>
|
||||
|
|
@ -1,25 +1,40 @@
|
|||
/** @odoo-module **/
|
||||
|
||||
import { formatFloat, formatMonetary } from "@web/views/fields/formatters";
|
||||
import { formatMonetary } from "@web/views/fields/formatters";
|
||||
import { formatFloat } from "@web/core/utils/numbers";
|
||||
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 {
|
||||
Component,
|
||||
onPatched,
|
||||
onWillUpdateProps,
|
||||
onWillRender,
|
||||
toRaw,
|
||||
useRef,
|
||||
useState,
|
||||
} from "@odoo/owl";
|
||||
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 {
|
||||
static props = {
|
||||
totals: { optional: true },
|
||||
subtotal: { optional: true },
|
||||
taxGroup: { optional: true },
|
||||
onChangeTaxGroup: { optional: true },
|
||||
isReadonly: Boolean,
|
||||
invalidate: Function,
|
||||
};
|
||||
static template = "account.TaxGroupComponent";
|
||||
|
||||
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) });
|
||||
const { taxGroup } = this.props;
|
||||
const newVal = formatFloat(taxGroup.tax_amount_currency, { digits: this.props.totals.currency_pd });
|
||||
this.inputTax.el.value = newVal;
|
||||
this.inputTax.el.focus(); // Focus the input
|
||||
}
|
||||
|
|
@ -30,6 +45,10 @@ class TaxGroupComponent extends Component {
|
|||
useNumpadDecimal();
|
||||
}
|
||||
|
||||
formatMonetary(value) {
|
||||
return formatMonetary(value, {currencyId: this.props.totals.currency_id});
|
||||
}
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
// Main methods
|
||||
//--------------------------------------------------------------------------
|
||||
|
|
@ -66,11 +85,11 @@ class TaxGroupComponent extends Component {
|
|||
*/
|
||||
_onChangeTaxValue() {
|
||||
this.setState("disable"); // Disable the input
|
||||
const oldValue = this.props.taxGroup.tax_group_amount;
|
||||
const oldValue = this.props.taxGroup.tax_amount_currency;
|
||||
let newValue;
|
||||
try {
|
||||
newValue = parseFloat(this.inputTax.el.value); // Get the new value
|
||||
} catch (_err) {
|
||||
} catch {
|
||||
this.inputTax.el.value = oldValue;
|
||||
this.setState("edit");
|
||||
return;
|
||||
|
|
@ -80,25 +99,20 @@ class TaxGroupComponent extends Component {
|
|||
this.setState("readonly");
|
||||
return;
|
||||
}
|
||||
this.props.taxGroup.tax_group_amount = newValue;
|
||||
const deltaValue = newValue - oldValue;
|
||||
this.props.taxGroup.tax_amount_currency += deltaValue;
|
||||
this.props.subtotal.tax_amount_currency += deltaValue;
|
||||
this.props.totals.tax_amount_currency += deltaValue;
|
||||
this.props.totals.total_amount_currency += deltaValue;
|
||||
|
||||
this.props.onChangeTaxGroup({
|
||||
oldValue,
|
||||
newValue: newValue,
|
||||
taxGroupId: this.props.taxGroup.tax_group_id,
|
||||
taxGroupId: this.props.taxGroup.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.
|
||||
|
|
@ -107,31 +121,30 @@ TaxGroupComponent.template = "account.TaxGroupComponent";
|
|||
currency_id field.
|
||||
**/
|
||||
export class TaxTotalsComponent extends Component {
|
||||
static template = "account.TaxTotalsField";
|
||||
static components = { TaxGroupComponent };
|
||||
static props = {
|
||||
...standardFieldProps,
|
||||
};
|
||||
|
||||
setup() {
|
||||
this.totals = {};
|
||||
this.formatData(this.props);
|
||||
onWillUpdateProps((nextProps) => {
|
||||
this.formatData(nextProps);
|
||||
});
|
||||
onWillRender(() => this.formatData(this.props));
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
formatMonetary(value) {
|
||||
return formatMonetary(value, {currencyId: this.totals.currency_id});
|
||||
}
|
||||
|
||||
/**
|
||||
* This method is the main function of the tax group widget.
|
||||
* It is called by the TaxGroupComponent and receives the newer tax value.
|
||||
|
|
@ -140,48 +153,21 @@ export class TaxTotalsComponent extends Component {
|
|||
*/
|
||||
_onChangeTaxValueByTaxGroup({ oldValue, newValue }) {
|
||||
if (oldValue === newValue) return;
|
||||
this.props.update(this.totals);
|
||||
this.totals.display_rounding = false;
|
||||
this.props.record.update({ [this.props.name]: this.totals });
|
||||
delete this.totals.cash_rounding_base_amount_currency;
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
let totals = JSON.parse(JSON.stringify(toRaw(props.record.data[this.props.name])));
|
||||
if (!totals) {
|
||||
return;
|
||||
}
|
||||
this.totals = totals;
|
||||
}
|
||||
}
|
||||
TaxTotalsComponent.template = "account.TaxTotalsField";
|
||||
TaxTotalsComponent.components = { TaxGroupComponent };
|
||||
TaxTotalsComponent.props = {
|
||||
...standardFieldProps,
|
||||
|
||||
export const taxTotalsComponent = {
|
||||
component: TaxTotalsComponent,
|
||||
};
|
||||
|
||||
registry.category("fields").add("account-tax-totals-field", TaxTotalsComponent);
|
||||
registry.category("fields").add("account-tax-totals-field", taxTotalsComponent);
|
||||
|
|
|
|||
|
|
@ -1,10 +1,10 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<templates>
|
||||
|
||||
<t t-name="account.TaxGroupComponent" owl="1">
|
||||
<t t-name="account.TaxGroupComponent">
|
||||
<tr>
|
||||
<td class="o_td_label">
|
||||
<label class="o_form_label o_tax_total_label" t-esc="props.taxGroup.tax_group_name"/>
|
||||
<label class="o_form_label o_tax_total_label" t-out="props.taxGroup.group_name"/>
|
||||
</td>
|
||||
|
||||
<td class="o_tax_group">
|
||||
|
|
@ -24,37 +24,40 @@
|
|||
<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"/>
|
||||
<i class="fa fa-pencil me-2"/> <t t-out="formatMonetary(props.taxGroup.tax_amount_currency)"/>
|
||||
</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;"/>
|
||||
<t t-out="formatMonetary(props.taxGroup.tax_amount_currency)" style="white-space: nowrap;"/>
|
||||
</span>
|
||||
</t>
|
||||
</td>
|
||||
</tr>
|
||||
</t>
|
||||
|
||||
<t t-name="account.TaxTotalsField" owl="1">
|
||||
<table t-if="totals" class="oe_right">
|
||||
<t t-name="account.TaxTotalsField">
|
||||
<table t-if="totals" class="float-end">
|
||||
<tbody>
|
||||
<t t-foreach="totals.subtotals" t-as="subtotal" t-key="subtotal['name']">
|
||||
<t t-foreach="totals.subtotals" t-as="subtotal" t-key="subtotal_index">
|
||||
<tr>
|
||||
<td class="o_td_label">
|
||||
<label class="o_form_label o_tax_total_label" t-esc="subtotal['name']"/>
|
||||
<label class="o_form_label o_tax_total_label" t-out="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']"/>
|
||||
<span t-att-name="subtotal.name"
|
||||
style="white-space: nowrap; font-weight: bold;"
|
||||
t-out="formatMonetary(subtotal.base_amount_currency)"/>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<t t-foreach="totals.groups_by_subtotal[subtotal['name']]" t-as="taxGroup" t-key="taxGroup.group_key">
|
||||
<t t-foreach="subtotal.tax_groups" t-as="taxGroup" t-key="taxGroup_index">
|
||||
<TaxGroupComponent
|
||||
currency="currency"
|
||||
totals="totals"
|
||||
subtotal="subtotal"
|
||||
taxGroup="taxGroup"
|
||||
isReadonly="readonly"
|
||||
onChangeTaxGroup.bind="_onChangeTaxValueByTaxGroup"
|
||||
|
|
@ -63,13 +66,13 @@
|
|||
</t>
|
||||
</t>
|
||||
|
||||
<tr t-if="'formatted_rounding_amount' in totals and totals.rounding_amount !== 0 and totals.display_rounding">
|
||||
<tr t-if="'cash_rounding_base_amount_currency' in totals">
|
||||
<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"
|
||||
t-out="formatMonetary(totals.cash_rounding_base_amount_currency)"
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
|
|
@ -83,8 +86,8 @@
|
|||
<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"
|
||||
t-att-class="{'oe_subtotal_footer_separator': totals.has_tax_groups}"
|
||||
t-out="formatMonetary(totals.total_amount_currency)"
|
||||
style="font-size: 1.3em; font-weight: bold; white-space: nowrap;"
|
||||
/>
|
||||
</td>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,6 @@
|
|||
<?xml version="1.0"?>
|
||||
<templates>
|
||||
<t t-name="account.TestsSharedJsPython">
|
||||
<button t-attf-class="#{state.done ? 'text-success' : ''}" t-on-click="processTests">Test</button>
|
||||
</t>
|
||||
</templates>
|
||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue