mirror of
https://github.com/bringout/oca-ocb-accounting.git
synced 2026-04-22 02:42:08 +02:00
Initial commit: Accounting packages
This commit is contained in:
commit
4ef34c2317
2661 changed files with 1709616 additions and 0 deletions
|
|
@ -0,0 +1,84 @@
|
|||
/** @odoo-module **/
|
||||
|
||||
import { registry } from "@web/core/registry";
|
||||
import { createElement, append } from "@web/core/utils/xml";
|
||||
import { Notebook } from "@web/core/notebook/notebook";
|
||||
import { formView } from "@web/views/form/form_view";
|
||||
import { FormCompiler } from "@web/views/form/form_compiler";
|
||||
import { FormRenderer } from "@web/views/form/form_renderer";
|
||||
import { FormController } from '@web/views/form/form_controller';
|
||||
import { useService } from "@web/core/utils/hooks";
|
||||
|
||||
export class AccountMoveController extends FormController {
|
||||
setup() {
|
||||
super.setup();
|
||||
this.account_move_service = useService("account_move");
|
||||
}
|
||||
|
||||
async deleteRecord() {
|
||||
if ( !await this.account_move_service.addDeletionDialog(this, this.model.root.resId)) {
|
||||
return super.deleteRecord(...arguments);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export class AccountMoveFormNotebook extends Notebook {
|
||||
onAnchorClicked(ev) {
|
||||
if (ev.detail.detail.id === "#outstanding") {
|
||||
ev.preventDefault();
|
||||
ev.detail.detail.originalEv.preventDefault();
|
||||
}
|
||||
}
|
||||
|
||||
async changeTabTo(page_id) {
|
||||
if (this.props.onBeforeTabSwitch) {
|
||||
await this.props.onBeforeTabSwitch(page_id);
|
||||
}
|
||||
this.state.currentPage = page_id;
|
||||
}
|
||||
}
|
||||
AccountMoveFormNotebook.template = "account.AccountMoveFormNotebook";
|
||||
AccountMoveFormNotebook.props = {
|
||||
...Notebook.props,
|
||||
onBeforeTabSwitch: { type: Function, optional: true },
|
||||
}
|
||||
export class AccountMoveFormRenderer extends FormRenderer {
|
||||
async saveBeforeTabChange() {
|
||||
if (this.props.record.mode === "edit" && this.props.record.isDirty) {
|
||||
const contentEl = document.querySelector('.o_content');
|
||||
const scrollPos = contentEl.scrollTop;
|
||||
await this.props.record.save({
|
||||
stayInEdition: true,
|
||||
});
|
||||
if (scrollPos) {
|
||||
contentEl.scrollTop = scrollPos;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
AccountMoveFormRenderer.components = {
|
||||
...FormRenderer.components,
|
||||
AccountMoveFormNotebook: AccountMoveFormNotebook,
|
||||
}
|
||||
export class AccountMoveFormCompiler extends FormCompiler {
|
||||
compileNotebook(el, params) {
|
||||
const originalNoteBook = super.compileNotebook(...arguments);
|
||||
const noteBook = createElement("AccountMoveFormNotebook");
|
||||
for (const attr of originalNoteBook.attributes) {
|
||||
noteBook.setAttribute(attr.name, attr.value);
|
||||
}
|
||||
noteBook.setAttribute("onBeforeTabSwitch", "() => this.saveBeforeTabChange()");
|
||||
const slots = originalNoteBook.childNodes;
|
||||
append(noteBook, [...slots]);
|
||||
return noteBook;
|
||||
}
|
||||
}
|
||||
|
||||
export const AccountMoveFormView = {
|
||||
...formView,
|
||||
Renderer: AccountMoveFormRenderer,
|
||||
Compiler: AccountMoveFormCompiler,
|
||||
Controller: AccountMoveController,
|
||||
};
|
||||
|
||||
registry.category("views").add("account_move_form", AccountMoveFormView);
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
|
||||
<templates xml:space="preserve">
|
||||
<t t-name="account.AccountMoveFormNotebook" t-inherit="web.Notebook" t-inherit-mode="primary" owl="1">
|
||||
<xpath expr="//a[@class='nav-link']" position="attributes">
|
||||
<attribute name="t-on-click.prevent">() => this.changeTabTo(navItem[0])</attribute>
|
||||
</xpath>
|
||||
</t>
|
||||
</templates>
|
||||
|
|
@ -0,0 +1,28 @@
|
|||
/** @odoo-module **/
|
||||
|
||||
import { ConfirmationDialog } from "@web/core/confirmation_dialog/confirmation_dialog";
|
||||
import { escape } from "@web/core/utils/strings";
|
||||
import { markup } from "@odoo/owl";
|
||||
import { registry } from "@web/core/registry";
|
||||
|
||||
|
||||
export const accountMove = {
|
||||
dependencies: ["dialog", "orm"],
|
||||
start(env, { dialog, orm }) {
|
||||
return {
|
||||
async addDeletionDialog(component, moveIds) {
|
||||
const isMoveEndOfChain = await orm.call('account.move', 'check_move_sequence_chain', [moveIds]);
|
||||
if (!isMoveEndOfChain) {
|
||||
const message = env._t("This operation will create a gap in the sequence.");
|
||||
const confirmationDialogProps = component.deleteConfirmationDialogProps;
|
||||
confirmationDialogProps.body = markup(`<div class="text-danger">${escape(message)}</div>${escape(confirmationDialogProps.body)}`);
|
||||
dialog.add(ConfirmationDialog, confirmationDialogProps);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
registry.category("services").add("account_move", accountMove);
|
||||
|
|
@ -0,0 +1,103 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
|
||||
<templates xml:space="preserve">
|
||||
|
||||
<t t-name="account.AccountPaymentField" owl="1">
|
||||
<div>
|
||||
<t t-if="outstanding">
|
||||
<div>
|
||||
<strong class="float-start" id="outstanding" t-esc="title"/>
|
||||
</div>
|
||||
</t>
|
||||
<table style="width:100%;">
|
||||
<t t-foreach="lines" t-as="line" t-key="line_index">
|
||||
<tr>
|
||||
<t t-if="outstanding">
|
||||
<td>
|
||||
<a title="assign to invoice"
|
||||
role="button"
|
||||
class="oe_form_field btn btn-secondary outstanding_credit_assign"
|
||||
t-att-data-id="line.id"
|
||||
style="margin-right: 0px; padding-left: 5px; padding-right: 5px;"
|
||||
href="#"
|
||||
data-bs-toggle="tooltip"
|
||||
t-on-click.prevent="() => this.assignOutstandingCredit(line.id)">Add</a>
|
||||
</td>
|
||||
<td style="max-width: 11em;">
|
||||
<a t-att-title="line.date"
|
||||
role="button"
|
||||
class="oe_form_field btn btn-link open_account_move"
|
||||
t-on-click="() => this.openMove(line.move_id)"
|
||||
style="margin-right: 5px; text-overflow: ellipsis; overflow: hidden; white-space: nowrap; padding-left: 0px; width:100%; text-align:left;"
|
||||
data-bs-toggle="tooltip"
|
||||
t-att-payment-id="account_payment_id"
|
||||
t-esc="line.journal_name"/>
|
||||
</td>
|
||||
</t>
|
||||
<t t-if="!outstanding">
|
||||
<td>
|
||||
<a role="button" tabindex="0" class="js_payment_info fa fa-info-circle" t-att-index="line_index" style="margin-right:5px;" aria-label="Info" title="Journal Entry Info" data-bs-toggle="tooltip" t-on-click.stop="(ev) => this.onInfoClick(ev, line_index)"></a>
|
||||
</td>
|
||||
<td t-if="!line.is_exchange">
|
||||
<i class="o_field_widget text-start o_payment_label">Paid on <t t-esc="line.date"></t></i>
|
||||
</td>
|
||||
<td t-if="line.is_exchange" colspan="2">
|
||||
<i class="o_field_widget text-start text-muted text-start">
|
||||
<span class="oe_form_field oe_form_field_float oe_form_field_monetary fw-bold">
|
||||
<t t-esc="line.amount_formatted"/>
|
||||
</span>
|
||||
<span> Exchange Difference</span>
|
||||
</i>
|
||||
</td>
|
||||
</t>
|
||||
<td t-if="!line.is_exchange" style="text-align:right;">
|
||||
<span class="oe_form_field oe_form_field_float oe_form_field_monetary" style="margin-left: -10px;">
|
||||
<t t-esc="line.amount_formatted"/>
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
</t>
|
||||
</table>
|
||||
</div>
|
||||
</t>
|
||||
|
||||
<t t-name="account.AccountPaymentPopOver" owl="1">
|
||||
<div class="account_payment_popover">
|
||||
<h3 t-if="props.title" class="o_popover_header"><t t-esc="props.title"/></h3>
|
||||
<div class="px-2">
|
||||
<div>
|
||||
<table>
|
||||
<tr>
|
||||
<td><strong>Amount: </strong></td>
|
||||
<td>
|
||||
<t t-esc="props.amount_company_currency"></t>
|
||||
<t t-if="props.amount_foreign_currency">
|
||||
(<span class="fa fa-money"/> <t t-esc="props.amount_foreign_currency"/>)
|
||||
</t>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><strong>Memo: </strong></td>
|
||||
<td>
|
||||
<div style="width: 200px; word-wrap: break-word">
|
||||
<t t-esc="props.ref"/>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><strong>Date: </strong></td>
|
||||
<td><t t-esc="props.date"/></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><strong>Journal: </strong></td>
|
||||
<td><t t-esc="props.journal_name"/><span t-if="props.payment_method_name"> (<t t-esc="props.payment_method_name"/>)</span></td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
<button class="btn btn-sm btn-primary js_unreconcile_payment float-start" t-if="!props.is_exchange" style="margin-top:5px; margin-bottom:5px;" groups="account.group_account_invoice" t-on-click="() => props._onRemoveMoveReconcile(props.move_id, props.partial_id)">Unreconcile</button>
|
||||
<button class="btn btn-sm btn-secondary js_open_payment float-end" style="margin-top:5px; margin-bottom:5px;" t-on-click="() => props._onOpenMove(props.move_id)">View</button>
|
||||
</div>
|
||||
</div>
|
||||
</t>
|
||||
|
||||
</templates>
|
||||
|
|
@ -0,0 +1,93 @@
|
|||
/** @odoo-module **/
|
||||
|
||||
import { registry } from "@web/core/registry";
|
||||
import { usePopover } from "@web/core/popover/popover_hook";
|
||||
import { useService } from "@web/core/utils/hooks";
|
||||
import { localization } from "@web/core/l10n/localization";
|
||||
import { parseDate, formatDate } from "@web/core/l10n/dates";
|
||||
|
||||
import { formatMonetary } from "@web/views/fields/formatters";
|
||||
|
||||
const { Component, onWillUpdateProps } = owl;
|
||||
|
||||
class AccountPaymentPopOver extends Component {}
|
||||
AccountPaymentPopOver.template = "account.AccountPaymentPopOver";
|
||||
|
||||
export class AccountPaymentField extends Component {
|
||||
setup() {
|
||||
this.popover = usePopover();
|
||||
this.orm = useService("orm");
|
||||
this.action = useService("action");
|
||||
|
||||
this.formatData(this.props);
|
||||
onWillUpdateProps((nextProps) => this.formatData(nextProps));
|
||||
}
|
||||
|
||||
formatData(props) {
|
||||
const info = props.value || {
|
||||
content: [],
|
||||
outstanding: false,
|
||||
title: "",
|
||||
move_id: this.props.record.data.id,
|
||||
};
|
||||
for (let [key, value] of Object.entries(info.content)) {
|
||||
value.index = key;
|
||||
value.amount_formatted = formatMonetary(value.amount, { currencyId: value.currency_id });
|
||||
if (value.date) {
|
||||
// value.date is a string, parse to date and format to the users date format
|
||||
value.date = formatDate(parseDate(value.date));
|
||||
}
|
||||
}
|
||||
this.lines = info.content;
|
||||
this.outstanding = info.outstanding;
|
||||
this.title = info.title;
|
||||
this.move_id = info.move_id;
|
||||
}
|
||||
|
||||
onInfoClick(ev, idx) {
|
||||
if (this.popoverCloseFn) {
|
||||
this.closePopover();
|
||||
}
|
||||
this.popoverCloseFn = this.popover.add(
|
||||
ev.currentTarget,
|
||||
AccountPaymentPopOver,
|
||||
{
|
||||
title: this.env._t("Journal Entry Info"),
|
||||
...this.lines[idx],
|
||||
_onRemoveMoveReconcile: this.removeMoveReconcile.bind(this),
|
||||
_onOpenMove: this.openMove.bind(this),
|
||||
onClose: this.closePopover,
|
||||
},
|
||||
{
|
||||
position: localization.direction === "rtl" ? "bottom" : "left",
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
closePopover() {
|
||||
this.popoverCloseFn();
|
||||
this.popoverCloseFn = null;
|
||||
}
|
||||
|
||||
async assignOutstandingCredit(id) {
|
||||
await this.orm.call(this.props.record.resModel, 'js_assign_outstanding_line', [this.move_id, id], {});
|
||||
await this.props.record.model.root.load();
|
||||
this.props.record.model.notify();
|
||||
}
|
||||
|
||||
async removeMoveReconcile(moveId, partialId) {
|
||||
this.closePopover();
|
||||
await this.orm.call(this.props.record.resModel, 'js_remove_outstanding_partial', [moveId, partialId], {});
|
||||
await this.props.record.model.root.load();
|
||||
this.props.record.model.notify();
|
||||
}
|
||||
|
||||
async openMove(moveId) {
|
||||
const action = await this.orm.call(this.props.record.resModel, 'action_open_business_doc', [moveId], {});
|
||||
this.action.doAction(action);
|
||||
}
|
||||
}
|
||||
AccountPaymentField.template = "account.AccountPaymentField";
|
||||
AccountPaymentField.supportedTypes = ["char"];
|
||||
|
||||
registry.category("fields").add("payment", AccountPaymentField);
|
||||
|
|
@ -0,0 +1,27 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<templates>
|
||||
|
||||
<t t-name="account.ResequenceRenderer" owl="1" >
|
||||
<table t-if="data.changeLines.length" class="table table-sm">
|
||||
<thead><tr>
|
||||
<th>Date</th>
|
||||
<th>Before</th>
|
||||
<th>After</th>
|
||||
</tr></thead>
|
||||
<tbody>
|
||||
<t t-foreach="data.changeLines" t-as="changeLine" t-key="changeLine.id">
|
||||
<ChangeLine changeLine="changeLine" ordering="data.ordering"/>
|
||||
</t>
|
||||
</tbody>
|
||||
</table>
|
||||
</t>
|
||||
|
||||
<t t-name="account.ResequenceChangeLine" owl="1">
|
||||
<tr>
|
||||
<td t-esc="props.changeLine.date"/>
|
||||
<td t-esc="props.changeLine.current_name"/>
|
||||
<td t-if="props.ordering == 'keep'" t-esc="props.changeLine.new_by_name" t-attf-class="{{ props.changeLine.new_by_name != props.changeLine.new_by_date ? 'animate' : ''}}"/>
|
||||
<td t-else="" t-esc="props.changeLine.new_by_date" t-attf-class="{{ props.changeLine.new_by_name != props.changeLine.new_by_date ? 'animate' : ''}}"/>
|
||||
</tr>
|
||||
</t>
|
||||
</templates>
|
||||
|
|
@ -0,0 +1,24 @@
|
|||
/** @odoo-module */
|
||||
|
||||
import { registry } from "@web/core/registry";
|
||||
|
||||
const { Component, onWillUpdateProps } = owl;
|
||||
|
||||
class ChangeLine extends Component {}
|
||||
ChangeLine.template = "account.ResequenceChangeLine";
|
||||
ChangeLine.props = ["changeLine", "ordering"];
|
||||
|
||||
class ShowResequenceRenderer extends Component {
|
||||
setup() {
|
||||
this.formatData(this.props);
|
||||
onWillUpdateProps((nextProps) => this.formatData(nextProps));
|
||||
}
|
||||
|
||||
formatData(props) {
|
||||
this.data = props.value ? JSON.parse(props.value) : { changeLines: [], ordering: "date" };
|
||||
}
|
||||
}
|
||||
ShowResequenceRenderer.template = "account.ResequenceRenderer";
|
||||
ShowResequenceRenderer.components = { ChangeLine };
|
||||
|
||||
registry.category("fields").add("account_resequence_widget", ShowResequenceRenderer);
|
||||
|
|
@ -0,0 +1,23 @@
|
|||
/** @odoo-module **/
|
||||
|
||||
import { registry } from "@web/core/registry";
|
||||
import { SelectionField } from "@web/views/fields/selection/selection_field";
|
||||
|
||||
export class AccountTypeSelection extends SelectionField {
|
||||
get hierarchyOptions() {
|
||||
const opts = this.options;
|
||||
return [
|
||||
{ name: this.env._t('Balance Sheet') },
|
||||
{ name: this.env._t('Assets'), children: opts.filter(x => x[0] && x[0].startsWith('asset')) },
|
||||
{ name: this.env._t('Liabilities'), children: opts.filter(x => x[0] && x[0].startsWith('liability')) },
|
||||
{ name: this.env._t('Equity'), children: opts.filter(x => x[0] && x[0].startsWith('equity')) },
|
||||
{ name: this.env._t('Profit & Loss') },
|
||||
{ name: this.env._t('Income'), children: opts.filter(x => x[0] && x[0].startsWith('income')) },
|
||||
{ name: this.env._t('Expense'), children: opts.filter(x => x[0] && x[0].startsWith('expense')) },
|
||||
{ name: this.env._t('Other'), children: opts.filter(x => x[0] && x[0] === 'off_balance') },
|
||||
];
|
||||
}
|
||||
}
|
||||
AccountTypeSelection.template = "account.AccountTypeSelection";
|
||||
|
||||
registry.category("fields").add("account_type_selection", AccountTypeSelection);
|
||||
|
|
@ -0,0 +1,22 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
|
||||
<templates>
|
||||
|
||||
<t t-name="account.AccountTypeSelection" t-inherit="web.SelectionField" t-inherit-mode="primary" owl="1">
|
||||
<xpath expr="//t[@t-foreach='options']" position="replace">
|
||||
<t t-foreach="hierarchyOptions" t-as="group" t-key="group_index">
|
||||
<optgroup t-att-label="group.name">
|
||||
<t t-if="!!group.children">
|
||||
<t t-foreach="group.children" t-as="child" t-key="child[0]">
|
||||
<option
|
||||
t-att-selected="child[0] === value"
|
||||
t-att-value="stringify(child[0])"
|
||||
t-esc="child[1]"/>
|
||||
</t>
|
||||
</t>
|
||||
</optgroup>
|
||||
</t>
|
||||
</xpath>
|
||||
</t>
|
||||
|
||||
</templates>
|
||||
|
|
@ -0,0 +1,49 @@
|
|||
/** @odoo-module **/
|
||||
|
||||
import { registry } from "@web/core/registry";
|
||||
import { Many2ManyTagsField } from "@web/views/fields/many2many_tags/many2many_tags_field";
|
||||
|
||||
const { onWillUpdateProps } = owl;
|
||||
|
||||
export class AutosaveMany2ManyTagsField extends Many2ManyTagsField {
|
||||
setup() {
|
||||
super.setup();
|
||||
|
||||
onWillUpdateProps((nextProps) => this.willUpdateProps(nextProps));
|
||||
|
||||
this.lastBalance = this.props.record.data.balance;
|
||||
this.lastAccount = this.props.record.data.account_id;
|
||||
this.lastPartner = this.props.record.data.partner_id;
|
||||
|
||||
const super_update = this.update;
|
||||
this.update = (recordlist) => {
|
||||
super_update(recordlist);
|
||||
this._saveOnUpdate();
|
||||
};
|
||||
}
|
||||
|
||||
deleteTag(id) {
|
||||
super.deleteTag(id);
|
||||
this._saveOnUpdate();
|
||||
}
|
||||
|
||||
willUpdateProps(nextProps) {
|
||||
const line = this.props.record.data;
|
||||
if (line.tax_ids.records.length > 0) {
|
||||
if (line.balance !== this.lastBalance
|
||||
|| line.account_id[0] !== this.lastAccount[0]
|
||||
|| line.partner_id[0] !== this.lastPartner[0]) {
|
||||
this.lastBalance = line.balance;
|
||||
this.lastAccount = line.account_id;
|
||||
this.lastPartner = line.partner_id;
|
||||
this._saveOnUpdate();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async _saveOnUpdate() {
|
||||
await this.props.record.model.root.save({ stayInEdition: true });
|
||||
}
|
||||
}
|
||||
|
||||
registry.category("fields").add("autosave_many2many_tags", AutosaveMany2ManyTagsField);
|
||||
|
|
@ -0,0 +1,229 @@
|
|||
/** @odoo-module **/
|
||||
|
||||
import { _lt } from "@web/core/l10n/translation";
|
||||
import { registry } from "@web/core/registry";
|
||||
import { useService } from "@web/core/utils/hooks";
|
||||
import { listView } from "@web/views/list/list_view";
|
||||
import { ListRenderer } from "@web/views/list/list_renderer";
|
||||
import { ListController } from "@web/views/list/list_controller";
|
||||
import { kanbanView } from "@web/views/kanban/kanban_view";
|
||||
import { KanbanRenderer } from "@web/views/kanban/kanban_renderer";
|
||||
import { KanbanController } from "@web/views/kanban/kanban_controller";
|
||||
import { KanbanDropdownMenuWrapper } from "@web/views/kanban/kanban_dropdown_menu_wrapper";
|
||||
import { KanbanRecord } from "@web/views/kanban/kanban_record";
|
||||
import { FileUploader } from "@web/views/fields/file_handler";
|
||||
import { standardWidgetProps } from "@web/views/widgets/standard_widget_props";
|
||||
|
||||
import { Component, useState } from "@odoo/owl";
|
||||
|
||||
export class AccountFileUploader extends Component {
|
||||
setup() {
|
||||
this.orm = useService("orm");
|
||||
this.action = useService("action");
|
||||
this.notification = useService("notification");
|
||||
this.attachmentIdsToProcess = [];
|
||||
const rec = this.props.record ? this.props.record.data : false;
|
||||
this.extraContext = rec ? {
|
||||
default_journal_id: rec.id,
|
||||
default_move_type: (rec.type === 'sale' && 'out_invoice') || (rec.type === 'purchase' && 'in_invoice') || 'entry',
|
||||
} : this.props.extraContext || {}; //TODO remove this.props.extraContext
|
||||
}
|
||||
|
||||
async onFileUploaded(file) {
|
||||
const att_data = {
|
||||
name: file.name,
|
||||
mimetype: file.type,
|
||||
datas: file.data,
|
||||
};
|
||||
const att_id = await this.orm.create("ir.attachment", [att_data], {
|
||||
context: { ...this.extraContext, ...this.env.searchModel.context },
|
||||
});
|
||||
this.attachmentIdsToProcess.push(att_id);
|
||||
}
|
||||
|
||||
async onUploadComplete() {
|
||||
let action;
|
||||
try {
|
||||
action = await this.orm.call(
|
||||
"account.journal",
|
||||
"create_document_from_attachment",
|
||||
["", this.attachmentIdsToProcess],
|
||||
{ context: { ...this.extraContext, ...this.env.searchModel.context } },
|
||||
);
|
||||
} finally {
|
||||
// ensures attachments are cleared on success as well as on error
|
||||
this.attachmentIdsToProcess = [];
|
||||
}
|
||||
if (action.context && action.context.notifications) {
|
||||
for (let [file, msg] of Object.entries(action.context.notifications)) {
|
||||
this.notification.add(
|
||||
msg,
|
||||
{
|
||||
title: file,
|
||||
type: "info",
|
||||
sticky: true,
|
||||
});
|
||||
}
|
||||
delete action.context.notifications;
|
||||
}
|
||||
this.action.doAction(action);
|
||||
}
|
||||
}
|
||||
AccountFileUploader.components = {
|
||||
FileUploader,
|
||||
};
|
||||
AccountFileUploader.template = "account.AccountFileUploader";
|
||||
AccountFileUploader.extractProps = ({ attrs }) => ({
|
||||
togglerTemplate: attrs.template || "account.JournalUploadLink",
|
||||
btnClass: attrs.btnClass || "",
|
||||
linkText: attrs.linkText || attrs.title || _lt("Upload"), //TODO: remove linkText attr in master (not translatable)
|
||||
});
|
||||
AccountFileUploader.props = {
|
||||
...standardWidgetProps,
|
||||
record: { type: Object, optional: true},
|
||||
togglerTemplate: { type: String, optional: true },
|
||||
btnClass: { type: String, optional: true },
|
||||
linkText: { type: String, optional: true },
|
||||
slots: { type: Object, optional: true },
|
||||
extraContext: { type: Object, optional: true }, //this prop is only for stable databases with the old journal dashboard view, it should be deleted in master as it is not used
|
||||
}
|
||||
//when file uploader is used on account.journal (with a record)
|
||||
AccountFileUploader.fieldDependencies = {
|
||||
id: { type: "integer" },
|
||||
type: { type: "selection" },
|
||||
};
|
||||
|
||||
registry.category("view_widgets").add("account_file_uploader", AccountFileUploader);
|
||||
|
||||
export class AccountDropZone extends Component {
|
||||
setup() {
|
||||
this.notificationService = useService("notification");
|
||||
}
|
||||
|
||||
onDrop(ev) {
|
||||
const selector = '.account_file_uploader.o_input_file.o_hidden';
|
||||
// look for the closest uploader Input as it may have a context
|
||||
let uploadInput = ev.target.closest('.o_drop_area').parentElement.querySelector(selector) || document.querySelector(selector);
|
||||
let files = ev.dataTransfer ? ev.dataTransfer.files : false;
|
||||
if (uploadInput && !!files) {
|
||||
uploadInput.files = ev.dataTransfer.files;
|
||||
uploadInput.dispatchEvent(new Event("change"));
|
||||
} else {
|
||||
this.notificationService.add(
|
||||
this.env._t("Could not upload files"),
|
||||
{
|
||||
type: "danger",
|
||||
});
|
||||
}
|
||||
this.props.hideZone();
|
||||
}
|
||||
}
|
||||
AccountDropZone.defaultProps = {
|
||||
hideZone: () => {},
|
||||
};
|
||||
AccountDropZone.template = "account.DropZone";
|
||||
|
||||
// Account Move List View
|
||||
export class AccountMoveUploadListRenderer extends ListRenderer {
|
||||
setup() {
|
||||
super.setup();
|
||||
this.state.dropzoneVisible = false;
|
||||
}
|
||||
}
|
||||
AccountMoveUploadListRenderer.template = "account.ListRenderer";
|
||||
AccountMoveUploadListRenderer.components = {
|
||||
...ListRenderer.components,
|
||||
AccountDropZone,
|
||||
};
|
||||
|
||||
export class AccountMoveListController extends ListController {
|
||||
setup() {
|
||||
super.setup();
|
||||
this.account_move_service = useService("account_move");
|
||||
}
|
||||
|
||||
async onDeleteSelectedRecords() {
|
||||
const selectedResIds = await this.getSelectedResIds();
|
||||
if (this.props.resModel !== "account.move" || !await this.account_move_service.addDeletionDialog(this, selectedResIds)) {
|
||||
return super.onDeleteSelectedRecords(...arguments);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
AccountMoveListController.components = {
|
||||
...ListController.components,
|
||||
AccountFileUploader,
|
||||
};
|
||||
|
||||
export const AccountMoveUploadListView = {
|
||||
...listView,
|
||||
Controller: AccountMoveListController,
|
||||
Renderer: AccountMoveUploadListRenderer,
|
||||
buttonTemplate: "account.ListView.Buttons",
|
||||
};
|
||||
|
||||
// Account Move Kanban View
|
||||
export class AccountMoveUploadKanbanRenderer extends KanbanRenderer {
|
||||
setup() {
|
||||
super.setup();
|
||||
this.state.dropzoneVisible = false;
|
||||
}
|
||||
}
|
||||
AccountMoveUploadKanbanRenderer.template = "account.KanbanRenderer";
|
||||
AccountMoveUploadKanbanRenderer.components = {
|
||||
...KanbanRenderer.components,
|
||||
AccountDropZone,
|
||||
};
|
||||
|
||||
export class AccountMoveUploadKanbanController extends KanbanController {}
|
||||
AccountMoveUploadKanbanController.components = {
|
||||
...KanbanController.components,
|
||||
AccountFileUploader,
|
||||
};
|
||||
|
||||
export const AccountMoveUploadKanbanView = {
|
||||
...kanbanView,
|
||||
Controller: AccountMoveUploadKanbanController,
|
||||
Renderer: AccountMoveUploadKanbanRenderer,
|
||||
buttonTemplate: "account.KanbanView.Buttons",
|
||||
};
|
||||
|
||||
// Accounting Dashboard
|
||||
export class DashboardKanbanDropdownMenuWrapper extends KanbanDropdownMenuWrapper {
|
||||
onClick(ev) {
|
||||
// Keep the dropdown open as we need the fileuploader to remain in the dom
|
||||
if (!ev.target.tagName === "INPUT" && !ev.target.closest('.file_upload_kanban_action_a')) {
|
||||
super.onClick(ev);
|
||||
}
|
||||
}
|
||||
}
|
||||
export class DashboardKanbanRecord extends KanbanRecord {
|
||||
setup() {
|
||||
super.setup();
|
||||
this.state = useState({
|
||||
dropzoneVisible: false,
|
||||
});
|
||||
}
|
||||
}
|
||||
DashboardKanbanRecord.components = {
|
||||
...DashboardKanbanRecord.components,
|
||||
AccountDropZone,
|
||||
AccountFileUploader,
|
||||
KanbanDropdownMenuWrapper: DashboardKanbanDropdownMenuWrapper,
|
||||
};
|
||||
DashboardKanbanRecord.template = "account.DashboardKanbanRecord";
|
||||
|
||||
export class DashboardKanbanRenderer extends KanbanRenderer {}
|
||||
DashboardKanbanRenderer.components = {
|
||||
...KanbanRenderer.components,
|
||||
KanbanRecord: DashboardKanbanRecord,
|
||||
};
|
||||
|
||||
export const DashboardKanbanView = {
|
||||
...kanbanView,
|
||||
Renderer: DashboardKanbanRenderer,
|
||||
};
|
||||
|
||||
registry.category("views").add("account_tree", AccountMoveUploadListView);
|
||||
registry.category("views").add("account_documents_kanban", AccountMoveUploadKanbanView);
|
||||
registry.category("views").add("account_dashboard_kanban", DashboardKanbanView);
|
||||
|
|
@ -0,0 +1,27 @@
|
|||
.o_drop_area {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
position: absolute;
|
||||
background-color: #AAAA;
|
||||
z-index: 2;
|
||||
left: 0;
|
||||
top: 0;
|
||||
i {
|
||||
justify-content: center;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
height: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.file_upload_kanban_action_a {
|
||||
@include o-kanban-dashboard-dropdown-link($link-padding-gap: $o-kanban-dashboard-dropdown-complex-gap);
|
||||
}
|
||||
|
||||
.o_widget_account_file_uploader {
|
||||
.btn-primary.oe_kanban_action_button {
|
||||
a {
|
||||
color: $white;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,102 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
|
||||
<templates>
|
||||
<t t-name="account.DropZone" owl="1">
|
||||
<div t-if="props.visible"
|
||||
t-attf-class="o_drop_area"
|
||||
t-on-dragover.prevent="()=>{}"
|
||||
t-on-dragleave="props.hideZone"
|
||||
t-on-drop.prevent="onDrop">
|
||||
<i class="fa fa-upload fa-10x"></i>
|
||||
</div>
|
||||
</t>
|
||||
|
||||
<t t-name="account.AccountFileUploader" owl="1">
|
||||
<div t-att-class="props.record and props.record.data ? 'oe_kanban_color_' + props.record.data.color : ''">
|
||||
<FileUploader
|
||||
acceptedFileExtensions="props.acceptedFileExtensions"
|
||||
fileUploadClass="'account_file_uploader'"
|
||||
multiUpload="true"
|
||||
onUploaded.bind="onFileUploaded"
|
||||
onUploadComplete.bind="onUploadComplete">
|
||||
<t t-set-slot="toggler">
|
||||
<t t-if="props.togglerTemplate" t-call="{{ props.togglerTemplate }}"/>
|
||||
<t t-else="" t-slot="default"/>
|
||||
</t>
|
||||
<t t-slot="extra"/>
|
||||
</FileUploader>
|
||||
</div>
|
||||
</t>
|
||||
|
||||
<t t-name="account.ListRenderer" t-inherit="web.ListRenderer" t-inherit-mode="primary" owl="1">
|
||||
<xpath expr="//div[@t-ref='root']" position="before">
|
||||
<AccountDropZone
|
||||
visible="state.dropzoneVisible"
|
||||
hideZone="() => state.dropzoneVisible = false"/>
|
||||
</xpath>
|
||||
<xpath expr="//div[@t-ref='root']" position="attributes">
|
||||
<attribute name="t-on-dragenter.stop.prevent">() => state.dropzoneVisible = true</attribute>
|
||||
</xpath>
|
||||
</t>
|
||||
|
||||
<t t-name="account.KanbanRenderer" t-inherit="web.KanbanRenderer" t-inherit-mode="primary" owl="1">
|
||||
<xpath expr="//div[@t-ref='root']" position="before">
|
||||
<AccountDropZone
|
||||
visible="state.dropzoneVisible"
|
||||
hideZone="() => state.dropzoneVisible = false"/>
|
||||
</xpath>
|
||||
<xpath expr="//div[@t-ref='root']" position="attributes">
|
||||
<attribute name="t-on-dragenter.stop.prevent">() => state.dropzoneVisible = true</attribute>
|
||||
</xpath>
|
||||
</t>
|
||||
|
||||
<t t-name="account.ListView.Buttons" t-inherit="web.ListView.Buttons" t-inherit-mode="primary" owl="1">
|
||||
<xpath expr="//*[@class='btn btn-primary o_list_button_add']" position="after">
|
||||
<t t-call="account.AccountViewUploadButton"/>
|
||||
</xpath>
|
||||
</t>
|
||||
|
||||
<t t-name="account.KanbanView.Buttons" t-inherit="web.KanbanView.Buttons" t-inherit-mode="primary" owl="1">
|
||||
<xpath expr="//*[@class='btn btn-primary o-kanban-button-new']" position="after">
|
||||
<t t-call="account.AccountViewUploadButton"/>
|
||||
</xpath>
|
||||
</t>
|
||||
|
||||
<t t-name="account.AccountViewUploadButton" owl="1">
|
||||
<!-- No record is available so rely on the action context to contain the default_move_type -->
|
||||
<AccountFileUploader>
|
||||
<t t-set-slot="default">
|
||||
<button type="button" class="btn btn-secondary o_button_upload_bill">
|
||||
Upload
|
||||
</button>
|
||||
</t>
|
||||
</AccountFileUploader>
|
||||
</t>
|
||||
|
||||
<t t-name="account.DashboardKanbanRecord" owl="1">
|
||||
<div
|
||||
role="article"
|
||||
t-att-class="getRecordClasses()"
|
||||
t-att-data-id="props.canResequence and props.record.id"
|
||||
t-att-tabindex="props.record.model.useSampleModel ? -1 : 0"
|
||||
t-on-click="onGlobalClick"
|
||||
t-on-dragenter.stop.prevent="() => state.dropzoneVisible = true"
|
||||
t-ref="root">
|
||||
<AccountFileUploader record="props.record">
|
||||
<t t-set-slot="extra">
|
||||
<AccountDropZone
|
||||
visible="state.dropzoneVisible"
|
||||
hideZone="() => state.dropzoneVisible = false"/>
|
||||
<t t-call="{{ templates['kanban-box'] }}"/>
|
||||
</t>
|
||||
</AccountFileUploader>
|
||||
</div>
|
||||
</t>
|
||||
|
||||
<t t-name="account.JournalUploadLink" owl="1">
|
||||
<div t-att-class="props.btnClass" groups="account.group_account_invoice">
|
||||
<a href="#" t-out="props.linkText"/>
|
||||
</div>
|
||||
</t>
|
||||
|
||||
</templates>
|
||||
|
|
@ -0,0 +1,31 @@
|
|||
/** @odoo-module */
|
||||
|
||||
import { registry } from "@web/core/registry";
|
||||
|
||||
const { Component, onWillUpdateProps } = owl;
|
||||
|
||||
class ListItem extends Component {}
|
||||
ListItem.template = "account.GroupedItemTemplate";
|
||||
ListItem.props = ["item_vals", "options"];
|
||||
|
||||
class ListGroup extends Component {}
|
||||
ListGroup.template = "account.GroupedItemsTemplate";
|
||||
ListGroup.components = { ListItem };
|
||||
ListGroup.props = ["group_vals", "options"];
|
||||
|
||||
class ShowGroupedList extends Component {
|
||||
setup() {
|
||||
this.formatData(this.props);
|
||||
onWillUpdateProps((nextProps) => this.formatData(nextProps));
|
||||
}
|
||||
|
||||
formatData(props) {
|
||||
this.data = props.value
|
||||
? JSON.parse(props.value)
|
||||
: { groups_vals: [], options: { discarded_number: "", columns: [] } };
|
||||
}
|
||||
}
|
||||
ShowGroupedList.template = "account.GroupedListTemplate";
|
||||
ShowGroupedList.components = { ListGroup };
|
||||
|
||||
registry.category("fields").add("grouped_view_widget", ShowGroupedList);
|
||||
|
|
@ -0,0 +1,40 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<templates>
|
||||
<t t-name="account.GroupedListTemplate" owl="1">
|
||||
<table t-if="data.groups_vals.length" class="table table-sm o_list_table table table-sm table-hover table-striped o_list_table_grouped">
|
||||
<thead><tr>
|
||||
<t t-foreach="data.options.columns" t-as="col" t-key="col_index">
|
||||
<th t-esc="col['label']" t-attf-class="{{col['class']}}"/>
|
||||
</t>
|
||||
</tr></thead>
|
||||
<t t-foreach="data.groups_vals" t-as="group_vals" t-key="group_vals_index">
|
||||
<ListGroup group_vals="group_vals" options="data.options"/>
|
||||
</t>
|
||||
</table>
|
||||
<t t-if="data.options.discarded_number">
|
||||
<span><t t-esc="data.options.discarded_number"/> are not shown in the preview</span>
|
||||
</t>
|
||||
</t>
|
||||
|
||||
<tbody t-name="account.GroupedItemsTemplate" owl="1">
|
||||
<tr style="background-color: #dee2e6;">
|
||||
<td t-attf-colspan="{{props.options.columns.length}}">
|
||||
<t t-esc="props.group_vals.group_name"/>
|
||||
</td>
|
||||
</tr>
|
||||
<t t-foreach="props.group_vals.items_vals" t-as="item_vals" t-key="item_vals_index">
|
||||
<ListItem item_vals="item_vals[2]" options="props.options"/>
|
||||
</t>
|
||||
</tbody>
|
||||
|
||||
<tr t-name="account.GroupedItemTemplate" owl="1">
|
||||
<t t-foreach="props.options.columns" t-as="col" t-key="col_index">
|
||||
<td t-esc="props.item_vals[col['field']]" t-attf-class="{{col['class']}}"/>
|
||||
</t>
|
||||
</tr>
|
||||
|
||||
<t t-name="account.OpenMoveTemplate">
|
||||
<a href="#" t-esc="widget.value"/>
|
||||
</t>
|
||||
|
||||
</templates>
|
||||
|
|
@ -0,0 +1,48 @@
|
|||
/** @odoo-module **/
|
||||
|
||||
import { registry } from "@web/core/registry";
|
||||
import { useService } from "@web/core/utils/hooks";
|
||||
|
||||
const { Component } = owl;
|
||||
|
||||
export class JournalDashboardActivity extends Component {
|
||||
setup() {
|
||||
this.action = useService("action");
|
||||
this.MAX_ACTIVITY_DISPLAY = 5;
|
||||
this.formatData(this.props);
|
||||
}
|
||||
|
||||
formatData(props) {
|
||||
this.info = JSON.parse(this.props.value);
|
||||
this.info.more_activities = false;
|
||||
if (this.info.activities.length > this.MAX_ACTIVITY_DISPLAY) {
|
||||
this.info.more_activities = true;
|
||||
this.info.activities = this.info.activities.slice(0, this.MAX_ACTIVITY_DISPLAY);
|
||||
}
|
||||
}
|
||||
|
||||
async openActivity(activity) {
|
||||
this.action.doAction({
|
||||
type: 'ir.actions.act_window',
|
||||
name: this.env._t('Journal Entry'),
|
||||
target: 'current',
|
||||
res_id: activity.res_id,
|
||||
res_model: 'account.move',
|
||||
views: [[false, 'form']],
|
||||
});
|
||||
}
|
||||
|
||||
openAllActivities(e) {
|
||||
this.action.doAction({
|
||||
type: 'ir.actions.act_window',
|
||||
name: this.env._t('Journal Entries'),
|
||||
res_model: 'account.move',
|
||||
views: [[false, 'kanban'], [false, 'form']],
|
||||
search_view_id: [false],
|
||||
domain: [['journal_id', '=', this.props.record.resId], ['activity_ids', '!=', false]],
|
||||
});
|
||||
}
|
||||
}
|
||||
JournalDashboardActivity.template = "account.JournalDashboardActivity";
|
||||
|
||||
registry.category("fields").add("kanban_vat_activity", JournalDashboardActivity);
|
||||
|
|
@ -0,0 +1,24 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
|
||||
<templates>
|
||||
|
||||
<t t-name="account.JournalDashboardActivity" owl="1">
|
||||
<t t-foreach="info.activities" t-as="activity" t-key="activity_index">
|
||||
<div class="row">
|
||||
<div class="col-8 o_mail_activity">
|
||||
<a href="#"
|
||||
t-att-class="(activity.status == 'late' ? 'o_activity_color_overdue ' : ' ') + (activity.activity_category == 'tax_report' ? 'o_open_vat_report' : 'see_activity')"
|
||||
t-att-data-res-id="activity.res_id" t-att-data-id="activity.id" t-att-data-model="activity.res_model"
|
||||
t-on-click.stop.prevent="() => this.openActivity(activity)">
|
||||
<t t-esc="activity.name"/>
|
||||
</a>
|
||||
</div>
|
||||
<div class="col-4 text-end">
|
||||
<span><t t-esc="activity.date"/></span>
|
||||
</div>
|
||||
</div>
|
||||
</t>
|
||||
<a t-if="info.more_activities" class="float-end see_all_activities" href="#" t-on-click.stop.prevent="(ev) => this.openAllActivities(ev)">See all activities</a>
|
||||
</t>
|
||||
|
||||
</templates>
|
||||
|
|
@ -0,0 +1,17 @@
|
|||
/** @odoo-module **/
|
||||
|
||||
import { registry } from "@web/core/registry";
|
||||
import { Many2OneField } from "@web/views/fields/many2one/many2one_field";
|
||||
|
||||
class LineOpenMoveWidget extends Many2OneField {
|
||||
async openAction() {
|
||||
this.action.doActionButton({
|
||||
type: "object",
|
||||
resId: this.props.value[0],
|
||||
name: "action_open_business_doc",
|
||||
resModel: "account.move.line",
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
registry.category("fields").add("line_open_move_widget", LineOpenMoveWidget);
|
||||
|
|
@ -0,0 +1,25 @@
|
|||
/** @odoo-module **/
|
||||
|
||||
import { registry } from "@web/core/registry";
|
||||
import { useService } from "@web/core/utils/hooks";
|
||||
|
||||
const { Component } = owl;
|
||||
|
||||
class OpenMoveWidget extends Component {
|
||||
setup() {
|
||||
super.setup();
|
||||
this.action = useService("action");
|
||||
}
|
||||
|
||||
async openMove(ev) {
|
||||
this.action.doActionButton({
|
||||
type: "object",
|
||||
resId: this.props.record.resId,
|
||||
name: "action_open_business_doc",
|
||||
resModel: "account.move.line",
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
OpenMoveWidget.template = "account.OpenMoveWidget";
|
||||
registry.category("fields").add("open_move_widget", OpenMoveWidget);
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<templates>
|
||||
|
||||
<t t-name="account.OpenMoveWidget" owl="1">
|
||||
<a href="#" t-esc="props.value" t-on-click.prevent.stop="(ev) => this.openMove()"/>
|
||||
</t>
|
||||
|
||||
</templates>
|
||||
|
|
@ -0,0 +1,35 @@
|
|||
|
||||
// The goal of this file is to contain CSS hacks related to allowing
|
||||
// section and note on sale order and invoice.
|
||||
|
||||
table.o_section_and_note_list_view tr.o_data_row.o_is_line_note,
|
||||
table.o_section_and_note_list_view tr.o_data_row.o_is_line_note textarea[name="name"],
|
||||
div.oe_kanban_card.o_is_line_note {
|
||||
font-style: italic;
|
||||
}
|
||||
table.o_section_and_note_list_view tr.o_data_row.o_is_line_section,
|
||||
div.oe_kanban_card.o_is_line_section {
|
||||
font-weight: bold;
|
||||
background-color: #DDDDDD;
|
||||
}
|
||||
table.o_section_and_note_list_view tr.o_data_row.o_is_line_section {
|
||||
border-top: 1px solid #BBB;
|
||||
border-bottom: 1px solid #BBB;
|
||||
}
|
||||
|
||||
table.o_section_and_note_list_view tr.o_data_row {
|
||||
&.o_is_line_note,
|
||||
&.o_is_line_section {
|
||||
td {
|
||||
// There is an undeterministic CSS behaviour in Chrome related to
|
||||
// the combination of the row's and its children's borders.
|
||||
border: none !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.o_field_section_and_note_text {
|
||||
> span {
|
||||
white-space: pre-wrap !important;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,98 @@
|
|||
/** @odoo-module **/
|
||||
|
||||
import { registry } from "@web/core/registry";
|
||||
import { ListRenderer } from "@web/views/list/list_renderer";
|
||||
import { X2ManyField } from "@web/views/fields/x2many/x2many_field";
|
||||
import { TextField, ListTextField } from "@web/views/fields/text/text_field";
|
||||
import { CharField } from "@web/views/fields/char/char_field";
|
||||
|
||||
const { Component, useEffect } = owl;
|
||||
|
||||
export class SectionAndNoteListRenderer extends ListRenderer {
|
||||
/**
|
||||
* The purpose of this extension is to allow sections and notes in the one2many list
|
||||
* primarily used on Sales Orders and Invoices
|
||||
*
|
||||
* @override
|
||||
*/
|
||||
setup() {
|
||||
super.setup();
|
||||
this.titleField = "name";
|
||||
useEffect(
|
||||
() => this.focusToName(this.props.list.editedRecord),
|
||||
() => [this.props.list.editedRecord]
|
||||
)
|
||||
}
|
||||
|
||||
focusToName(editRec) {
|
||||
if (editRec && editRec.isVirtual && this.isSectionOrNote(editRec)) {
|
||||
const col = this.state.columns.find((c) => c.name === this.titleField);
|
||||
this.focusCell(col, null);
|
||||
}
|
||||
}
|
||||
|
||||
isSectionOrNote(record=null) {
|
||||
record = record || this.record;
|
||||
return ['line_section', 'line_note'].includes(record.data.display_type);
|
||||
}
|
||||
|
||||
getRowClass(record) {
|
||||
const existingClasses = super.getRowClass(record);
|
||||
return `${existingClasses} o_is_${record.data.display_type}`;
|
||||
}
|
||||
|
||||
getCellClass(column, record) {
|
||||
const classNames = super.getCellClass(column, record);
|
||||
if (this.isSectionOrNote(record) && column.widget !== "handle" && column.name !== this.titleField) {
|
||||
return `${classNames} o_hidden`;
|
||||
}
|
||||
return classNames;
|
||||
}
|
||||
|
||||
getColumns(record) {
|
||||
const columns = super.getColumns(record);
|
||||
if (this.isSectionOrNote(record)) {
|
||||
return this.getSectionColumns(columns);
|
||||
}
|
||||
return columns;
|
||||
}
|
||||
|
||||
getSectionColumns(columns) {
|
||||
const sectionCols = columns.filter((col) => col.widget === "handle" || col.type === "field" && col.name === this.titleField);
|
||||
return sectionCols.map((col) => {
|
||||
if (col.name === this.titleField) {
|
||||
return { ...col, colspan: columns.length - sectionCols.length + 1 };
|
||||
} else {
|
||||
return { ...col };
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
SectionAndNoteListRenderer.template = "account.sectionAndNoteListRenderer";
|
||||
|
||||
export class SectionAndNoteFieldOne2Many extends X2ManyField {}
|
||||
SectionAndNoteFieldOne2Many.additionalClasses = ['o_field_one2many'];
|
||||
SectionAndNoteFieldOne2Many.components = {
|
||||
...X2ManyField.components,
|
||||
ListRenderer: SectionAndNoteListRenderer,
|
||||
};
|
||||
|
||||
export class SectionAndNoteText extends Component {
|
||||
get componentToUse() {
|
||||
return this.props.record.data.display_type === 'line_section' ? CharField : TextField;
|
||||
}
|
||||
}
|
||||
SectionAndNoteText.template = "account.SectionAndNoteText";
|
||||
SectionAndNoteText.additionalClasses = ["o_field_text"];
|
||||
|
||||
export class ListSectionAndNoteText extends SectionAndNoteText {
|
||||
get componentToUse() {
|
||||
return this.props.record.data.display_type !== "line_section"
|
||||
? ListTextField
|
||||
: super.componentToUse;
|
||||
}
|
||||
}
|
||||
|
||||
registry.category("fields").add("section_and_note_one2many", SectionAndNoteFieldOne2Many);
|
||||
registry.category("fields").add("section_and_note_text", SectionAndNoteText);
|
||||
registry.category("fields").add("list.section_and_note_text", ListSectionAndNoteText);
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<templates>
|
||||
<t t-name="account.sectionAndNoteListRenderer" t-inherit="web.ListRenderer" t-inherit-mode="primary" owl="1">
|
||||
<xpath expr="//table" position="attributes">
|
||||
<attribute name="class" position="add" separator=" ">o_section_and_note_list_view</attribute>
|
||||
</xpath>
|
||||
</t>
|
||||
|
||||
<t t-name="account.SectionAndNoteText" owl="1">
|
||||
<t t-component="componentToUse" t-props="props"/>
|
||||
</t>
|
||||
|
||||
</templates>
|
||||
|
|
@ -0,0 +1,33 @@
|
|||
/** @odoo-module */
|
||||
|
||||
import { registry } from "@web/core/registry";
|
||||
import { useService } from "@web/core/utils/hooks";
|
||||
|
||||
import { ResConfigDevTool } from "@web/webclient/settings_form_view/widgets/res_config_dev_tool";
|
||||
|
||||
/**
|
||||
* Override of the widget in the settings that handles the "Developer Tools" section.
|
||||
* Provides a button to download XSD files for XML validation.
|
||||
*/
|
||||
class ResConfigDevToolDownloadXsd extends ResConfigDevTool {
|
||||
/**
|
||||
* Downloads every XSD file, based on installed localisations.
|
||||
*/
|
||||
setup() {
|
||||
super.setup();
|
||||
this.rpc = useService("rpc");
|
||||
}
|
||||
|
||||
async onClickDownloadXSD() {
|
||||
await this.rpc("/web/dataset/call_kw/ir.attachment/action_download_xsd_files", {
|
||||
model: 'ir.attachment',
|
||||
method: 'action_download_xsd_files',
|
||||
args: [],
|
||||
kwargs: {}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
ResConfigDevToolDownloadXsd.template = "res_config_dev_tool";
|
||||
|
||||
registry.category("view_widgets").add("res_config_dev_tool", ResConfigDevToolDownloadXsd, {force: true});
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<template>
|
||||
<div t-name='account.res_config_dev_tool' t-inherit="web.res_config_dev_tool" t-inherit-mode="extension" owl="1" primary="1">
|
||||
<xpath expr="//Setting/div" position="inside">
|
||||
<a t-if="isDebug" class="d-block" t-on-click.prevent="onClickDownloadXSD" href="#">Download XSD files (XML validation)</a>
|
||||
</xpath>
|
||||
</div>
|
||||
</template>
|
||||
|
|
@ -0,0 +1,24 @@
|
|||
.o_tax_group { width: 0% }
|
||||
|
||||
.o_tax_group_edit {
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.o_tax_group_edit:hover {
|
||||
color: #00A09D;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.o_tax_group_editable .o_tax_group_amount_value input {
|
||||
width: 65%;
|
||||
float: right;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.o_tax_group_editable .o_tax_group_amount_value::before {
|
||||
content: ' ';
|
||||
}
|
||||
|
||||
.o_tax_total_label{
|
||||
margin-bottom: 0 !important;
|
||||
}
|
||||
|
|
@ -0,0 +1,187 @@
|
|||
/** @odoo-module **/
|
||||
|
||||
import { formatFloat, formatMonetary } from "@web/views/fields/formatters";
|
||||
import { parseFloat } from "@web/views/fields/parsers";
|
||||
import { standardFieldProps } from "@web/views/fields/standard_field_props";
|
||||
import { registry } from "@web/core/registry";
|
||||
import { session } from "@web/session";
|
||||
import { useNumpadDecimal } from "@web/views/fields/numpad_decimal_hook";
|
||||
|
||||
const { Component, onPatched, onWillUpdateProps, useRef, useState } = owl;
|
||||
|
||||
/**
|
||||
A line of some TaxTotalsComponent, giving the values of a tax group.
|
||||
**/
|
||||
class TaxGroupComponent extends Component {
|
||||
setup() {
|
||||
this.inputTax = useRef("taxValueInput");
|
||||
this.state = useState({ value: "readonly" });
|
||||
onPatched(() => {
|
||||
if (this.state.value === "edit") {
|
||||
const { taxGroup, currency } = this.props;
|
||||
const newVal = formatFloat(taxGroup.tax_group_amount, { digits: (currency && currency.digits) });
|
||||
this.inputTax.el.value = newVal;
|
||||
this.inputTax.el.focus(); // Focus the input
|
||||
}
|
||||
});
|
||||
onWillUpdateProps(() => {
|
||||
this.setState("readonly");
|
||||
});
|
||||
useNumpadDecimal();
|
||||
}
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
// Main methods
|
||||
//--------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* The purpose of this method is to change the state of the component.
|
||||
* It can have one of the following three states:
|
||||
* - readonly: display in read-only mode of the field,
|
||||
* - edit: display with a html input field,
|
||||
* - disable: display with a html input field that is disabled.
|
||||
*
|
||||
* If a value other than one of these 3 states is passed as a parameter,
|
||||
* the component is set to readonly by default.
|
||||
*
|
||||
* @param {String} value
|
||||
*/
|
||||
setState(value) {
|
||||
if (["readonly", "edit", "disable"].includes(value)) {
|
||||
this.state.value = value;
|
||||
}
|
||||
else {
|
||||
this.state.value = "readonly";
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* This method handles the "_onChangeTaxValue" event. In this method,
|
||||
* we get the new value for the tax group, we format it and we call
|
||||
* the method to recalculate the tax lines. At the moment the method
|
||||
* is called, we disable the html input field.
|
||||
*
|
||||
* In case the value has not changed or the tax group is equal to 0,
|
||||
* the modification does not take place.
|
||||
*/
|
||||
_onChangeTaxValue() {
|
||||
this.setState("disable"); // Disable the input
|
||||
const oldValue = this.props.taxGroup.tax_group_amount;
|
||||
let newValue;
|
||||
try {
|
||||
newValue = parseFloat(this.inputTax.el.value); // Get the new value
|
||||
} catch (_err) {
|
||||
this.inputTax.el.value = oldValue;
|
||||
this.setState("edit");
|
||||
return;
|
||||
}
|
||||
// The newValue can"t be equals to 0
|
||||
if (newValue === oldValue || newValue === 0) {
|
||||
this.setState("readonly");
|
||||
return;
|
||||
}
|
||||
this.props.taxGroup.tax_group_amount = newValue;
|
||||
|
||||
this.props.onChangeTaxGroup({
|
||||
oldValue,
|
||||
newValue: newValue,
|
||||
taxGroupId: this.props.taxGroup.tax_group_id,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
TaxGroupComponent.props = {
|
||||
currency: { optional: true },
|
||||
taxGroup: { optional: true },
|
||||
onChangeTaxGroup: { optional: true },
|
||||
isReadonly: Boolean,
|
||||
invalidate: Function,
|
||||
};
|
||||
TaxGroupComponent.template = "account.TaxGroupComponent";
|
||||
|
||||
/**
|
||||
Widget used to display tax totals by tax groups for invoices, PO and SO,
|
||||
and possibly allowing editing them.
|
||||
|
||||
Note that this widget requires the object it is used on to have a
|
||||
currency_id field.
|
||||
**/
|
||||
export class TaxTotalsComponent extends Component {
|
||||
setup() {
|
||||
this.totals = {};
|
||||
this.formatData(this.props);
|
||||
onWillUpdateProps((nextProps) => {
|
||||
this.formatData(nextProps);
|
||||
});
|
||||
}
|
||||
|
||||
get readonly() {
|
||||
return this.props.readonly;
|
||||
}
|
||||
|
||||
get currencyId() {
|
||||
const recordCurrency = this.props.record.data.currency_id;
|
||||
return recordCurrency && recordCurrency[0];
|
||||
}
|
||||
|
||||
get currency() {
|
||||
return session.currencies[this.currencyId];
|
||||
}
|
||||
|
||||
invalidate() {
|
||||
return this.props.record.setInvalidField(this.props.name);
|
||||
}
|
||||
|
||||
/**
|
||||
* This method is the main function of the tax group widget.
|
||||
* It is called by the TaxGroupComponent and receives the newer tax value.
|
||||
*
|
||||
* It is responsible for triggering an event to notify the ORM of a change.
|
||||
*/
|
||||
_onChangeTaxValueByTaxGroup({ oldValue, newValue }) {
|
||||
if (oldValue === newValue) return;
|
||||
this.props.update(this.totals);
|
||||
this.totals.display_rounding = false;
|
||||
}
|
||||
|
||||
formatData(props) {
|
||||
let totals = JSON.parse(JSON.stringify(props.value));
|
||||
const currencyFmtOpts = { currencyId: props.record.data.currency_id && props.record.data.currency_id[0] };
|
||||
|
||||
let amount_untaxed = totals.amount_untaxed;
|
||||
let amount_tax = 0;
|
||||
let subtotals = [];
|
||||
for (let subtotal_title of totals.subtotals_order) {
|
||||
let amount_total = amount_untaxed + amount_tax;
|
||||
subtotals.push({
|
||||
'name': subtotal_title,
|
||||
'amount': amount_total,
|
||||
'formatted_amount': formatMonetary(amount_total, currencyFmtOpts),
|
||||
});
|
||||
let group = totals.groups_by_subtotal[subtotal_title];
|
||||
for (let i in group) {
|
||||
amount_tax = amount_tax + group[i].tax_group_amount;
|
||||
}
|
||||
}
|
||||
totals.subtotals = subtotals;
|
||||
let rounding_amount = totals.display_rounding && totals.rounding_amount || 0;
|
||||
let amount_total = amount_untaxed + amount_tax + rounding_amount;
|
||||
totals.amount_total = amount_total;
|
||||
totals.formatted_amount_total = formatMonetary(amount_total, currencyFmtOpts);
|
||||
for (let group_name of Object.keys(totals.groups_by_subtotal)) {
|
||||
let group = totals.groups_by_subtotal[group_name];
|
||||
for (let key in group) {
|
||||
group[key].formatted_tax_group_amount = formatMonetary(group[key].tax_group_amount, currencyFmtOpts);
|
||||
group[key].formatted_tax_group_base_amount = formatMonetary(group[key].tax_group_base_amount, currencyFmtOpts);
|
||||
}
|
||||
}
|
||||
this.totals = totals;
|
||||
}
|
||||
}
|
||||
TaxTotalsComponent.template = "account.TaxTotalsField";
|
||||
TaxTotalsComponent.components = { TaxGroupComponent };
|
||||
TaxTotalsComponent.props = {
|
||||
...standardFieldProps,
|
||||
};
|
||||
|
||||
registry.category("fields").add("account-tax-totals-field", TaxTotalsComponent);
|
||||
|
|
@ -0,0 +1,95 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<templates>
|
||||
|
||||
<t t-name="account.TaxGroupComponent" owl="1">
|
||||
<tr>
|
||||
<td class="o_td_label">
|
||||
<label class="o_form_label o_tax_total_label" t-esc="props.taxGroup.tax_group_name"/>
|
||||
</td>
|
||||
|
||||
<td class="o_tax_group">
|
||||
<t t-if="!props.isReadonly">
|
||||
<t t-if="['edit', 'disable'].includes(state.value)">
|
||||
<span class="o_tax_group_edit_input" t-ref="numpadDecimal">
|
||||
<input
|
||||
type="text"
|
||||
t-ref="taxValueInput"
|
||||
class="o_field_float
|
||||
o_field_number o_input"
|
||||
t-att-disabled="state.value === 'disable'"
|
||||
t-on-change.prevent="_onChangeTaxValue"
|
||||
t-on-blur="_onChangeTaxValue"/>
|
||||
</span>
|
||||
</t>
|
||||
<t t-else="">
|
||||
<span class="o_tax_group_edit" t-on-click.prevent="() => this.setState('edit')">
|
||||
<span class="o_tax_group_amount_value o_list_monetary">
|
||||
<i class="fa fa-pencil me-2"/> <t t-out="props.taxGroup.formatted_tax_group_amount"/>
|
||||
</span>
|
||||
</span>
|
||||
</t>
|
||||
</t>
|
||||
<t t-else="">
|
||||
<span class="o_tax_group_amount_value o_list_monetary">
|
||||
<t t-out="props.taxGroup.formatted_tax_group_amount" style="white-space: nowrap;"/>
|
||||
</span>
|
||||
</t>
|
||||
</td>
|
||||
</tr>
|
||||
</t>
|
||||
|
||||
<t t-name="account.TaxTotalsField" owl="1">
|
||||
<table t-if="totals" class="oe_right">
|
||||
<tbody>
|
||||
<t t-foreach="totals.subtotals" t-as="subtotal" t-key="subtotal['name']">
|
||||
<tr>
|
||||
<td class="o_td_label">
|
||||
<label class="o_form_label o_tax_total_label" t-esc="subtotal['name']"/>
|
||||
</td>
|
||||
|
||||
<td class="o_list_monetary">
|
||||
<span t-att-name="subtotal['name']" style="white-space: nowrap; font-weight: bold;" t-out="subtotal['formatted_amount']"/>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<t t-foreach="totals.groups_by_subtotal[subtotal['name']]" t-as="taxGroup" t-key="taxGroup.group_key">
|
||||
<TaxGroupComponent
|
||||
currency="currency"
|
||||
taxGroup="taxGroup"
|
||||
isReadonly="readonly"
|
||||
onChangeTaxGroup.bind="_onChangeTaxValueByTaxGroup"
|
||||
invalidate.bind="invalidate"
|
||||
/>
|
||||
</t>
|
||||
</t>
|
||||
|
||||
<tr t-if="'formatted_rounding_amount' in totals and totals.rounding_amount !== 0 and totals.display_rounding">
|
||||
<td class="o_td_label">
|
||||
<label class="o_form_label o_tax_total_label">Rounding</label>
|
||||
</td>
|
||||
<td class="o_list_monetary">
|
||||
<span
|
||||
t-out="totals.formatted_rounding_amount"
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<!-- Total amount with all taxes-->
|
||||
<tr>
|
||||
<td class="o_td_label">
|
||||
<label class="o_form_label o_tax_total_label">Total</label>
|
||||
</td>
|
||||
|
||||
<td class="o_list_monetary">
|
||||
<span
|
||||
name="amount_total"
|
||||
t-att-class="Object.keys(totals.groups_by_subtotal).length > 0 ? 'oe_subtotal_footer_separator' : ''"
|
||||
t-out="totals.formatted_amount_total"
|
||||
style="font-size: 1.3em; font-weight: bold; white-space: nowrap;"
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</t>
|
||||
</templates>
|
||||
Loading…
Add table
Add a link
Reference in a new issue