Initial commit: Accounting packages

This commit is contained in:
Ernad Husremovic 2025-08-29 15:20:47 +02:00
commit 4ef34c2317
2661 changed files with 1709616 additions and 0 deletions

View file

@ -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);

View file

@ -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>

View file

@ -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);

View file

@ -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>

View file

@ -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);

View file

@ -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>

View file

@ -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);

View file

@ -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);

View file

@ -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>

View file

@ -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);

View file

@ -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);

View file

@ -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;
}
}
}

View file

@ -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>

View file

@ -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);

View file

@ -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>

View file

@ -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);

View file

@ -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>

View file

@ -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);

View file

@ -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);

View file

@ -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>

View file

@ -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;
}
}

View file

@ -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);

View file

@ -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>

View file

@ -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});

View file

@ -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>

View file

@ -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;
}

View file

@ -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);

View file

@ -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>

View file

@ -0,0 +1,61 @@
.openerp div.oe_account_help {
background : #D6EBFF;
width: 100%;
padding: 10px;
border: 3px solid #C1D4E6;
}
.openerp p.oe_account_font_help{
text-align: left;
font-weight: bold;
margin: 0px;
font-size: 14px;
}
.openerp p.oe_account_font_content{
margin-left: 30px;
font-size: 14px;
}
.openerp p.oe_account_font_title{
margin-top: 7px;
font-size: 15px;
font-style: italic;
color: grey;
}
.oe_invoice_outstanding_credits_debits {
clear: both;
float: right;
min-width: 260px;
padding-top: 20px;
}
.oe_account_terms {
flex: auto !important;
}
@media (max-width: 991.98px) {
/* The purpose is to put the narration below the totals in the tab 'Invoice Lines'
instead of above for the mobile view */
.o_form_view .oe_invoice_lines_tab {
display: flex;
flex-direction: column-reverse;
}
.o_form_view .oe_invoice_lines_tab .oe_invoice_outstanding_credits_debits {
min-width: initial;
width: 50%;
}
}
@media (max-width: 767.98px) {
.o_form_view .oe_invoice_lines_tab .oe_invoice_outstanding_credits_debits {
min-width: initial;
width: 100%;
}
}
.o_field_account_resequence_widget {
width: 100%;
}

View file

@ -0,0 +1,31 @@
.openerp .oe_force_bold {
font-weight: bold !important;
}
.openerp label.oe_open_balance{
margin-right: -18px;
}
.openerp label.oe_subtotal_footer_separator{
float:right;
width: 184px !important;
}
.openerp label.oe_mini_subtotal_footer_separator{
margin-right: -14px;
}
.openerp .oe_account_total, .openerp .oe_pos_total {
margin-left: -2px;
}
.openerp label.oe_real_closing_balance{
min-width: 184px !important;
}
.openerp label.oe_difference, .openerp label.oe_pos_difference {
margin-right: -10px;
padding-left: 10px !important;
min-width: 195px !important;
}
.openerp .oe_opening_total{
margin-right: 4px;
}
.o_payment_label{
padding-right: 20px;
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6 KiB

View file

@ -0,0 +1,13 @@
/** @odoo-module */
import publicWidget from 'web.public.widget';
import "portal.portal"; // force dependencies
publicWidget.registry.PortalHomeCounters.include({
/**
* @override
*/
_getCountersAlwaysDisplayed() {
return this._super(...arguments).concat(['invoice_count']);
},
});

View file

@ -0,0 +1,73 @@
odoo.define('account.AccountPortalSidebar', function (require) {
'use strict';
const dom = require('web.dom');
var publicWidget = require('web.public.widget');
var PortalSidebar = require('portal.PortalSidebar');
var utils = require('web.utils');
publicWidget.registry.AccountPortalSidebar = PortalSidebar.extend({
selector: '.o_portal_invoice_sidebar',
events: {
'click .o_portal_invoice_print': '_onPrintInvoice',
},
/**
* @override
*/
start: function () {
var def = this._super.apply(this, arguments);
var $invoiceHtml = this.$el.find('iframe#invoice_html');
var updateIframeSize = this._updateIframeSize.bind(this, $invoiceHtml);
$(window).on('resize', updateIframeSize);
var iframeDoc = $invoiceHtml[0].contentDocument || $invoiceHtml[0].contentWindow.document;
if (iframeDoc.readyState === 'complete') {
updateIframeSize();
} else {
$invoiceHtml.on('load', updateIframeSize);
}
return def;
},
//--------------------------------------------------------------------------
// Handlers
//--------------------------------------------------------------------------
/**
* Called when the iframe is loaded or the window is resized on customer portal.
* The goal is to expand the iframe height to display the full report without scrollbar.
*
* @private
* @param {object} $el: the iframe
*/
_updateIframeSize: function ($el) {
var $wrapwrap = $el.contents().find('div#wrapwrap');
// Set it to 0 first to handle the case where scrollHeight is too big for its content.
$el.height(0);
$el.height($wrapwrap[0].scrollHeight);
// scroll to the right place after iframe resize
if (!utils.isValidAnchor(window.location.hash)) {
return;
}
var $target = $(window.location.hash);
if (!$target.length) {
return;
}
dom.scrollTo($target[0], {duration: 0});
},
/**
* @private
* @param {MouseEvent} ev
*/
_onPrintInvoice: function (ev) {
ev.preventDefault();
var href = $(ev.currentTarget).attr('href');
this._printIframeContent(href);
},
});
});

View file

@ -0,0 +1,149 @@
odoo.define('account.payment', function (require) {
"use strict";
var AbstractField = require('web.AbstractField');
var core = require('web.core');
var field_registry = require('web.field_registry');
var field_utils = require('web.field_utils');
var QWeb = core.qweb;
var _t = core._t;
var ShowPaymentLineWidget = AbstractField.extend({
events: _.extend({
'click .outstanding_credit_assign': '_onOutstandingCreditAssign',
'click .open_account_move': '_onOpenPaymentOrMove',
}, AbstractField.prototype.events),
supportedFieldTypes: ['char'],
//--------------------------------------------------------------------------
// Public
//--------------------------------------------------------------------------
/**
* @override
* @returns {boolean}
*/
isSet: function() {
return true;
},
//--------------------------------------------------------------------------
// Private
//--------------------------------------------------------------------------
/**
* @private
* @override
*/
_render: function() {
this.viewAlreadyOpened = false;
var self = this;
var info = this.value;
if (!info) {
this.$el.html('');
return;
}
_.each(info.content, function (k, v){
k.index = v;
k.amount = field_utils.format.float(k.amount, {digits: k.digits});
if (k.date){
k.date = field_utils.format.date(field_utils.parse.date(k.date, {}, {isUTC: true}));
}
});
this.$el.html(QWeb.render('ShowPaymentInfo', {
lines: info.content,
outstanding: info.outstanding,
title: info.title
}));
_.each(this.$('.js_payment_info'), function (k, v){
var isRTL = _t.database.parameters.direction === "rtl";
var content = info.content[v];
var options = {
content: function () {
var $content = $(QWeb.render('PaymentPopOver', content));
$content.filter('.js_unreconcile_payment').on('click', self._onRemoveMoveReconcile.bind(self));
$content.filter('.js_open_payment').on('click', self._onOpenPaymentOrMove.bind(self));
return $content;
},
html: true,
placement: isRTL ? 'bottom' : 'left',
title: 'Payment Information',
trigger: 'focus',
delay: { "show": 0, "hide": 100 },
container: $(k).parent(), // FIXME Ugly, should use the default body container but system & tests to adapt to properly destroy the popover
};
$(k).popover(options);
});
},
//--------------------------------------------------------------------------
// Handlers
//--------------------------------------------------------------------------
/**
* @private
* @override
* @param {MouseEvent} event
*/
_onOpenPaymentOrMove: function (event) {
var moveId = parseInt($(event.target).attr('move-id'));
if (!this.viewAlreadyOpened && moveId !== undefined && !isNaN(moveId)) {
this.viewAlreadyOpened = true;
var self = this;
this._rpc({
model: 'account.move',
method: 'action_open_business_doc',
args: [moveId],
}).then(function (actionData) {
return self.do_action(actionData);
});
}
},
/**
* @private
* @override
* @param {MouseEvent} event
*/
_onOutstandingCreditAssign: function (event) {
event.stopPropagation();
event.preventDefault();
var self = this;
var id = $(event.target).data('id') || false;
this._rpc({
model: 'account.move',
method: 'js_assign_outstanding_line',
args: [this.value.move_id, id],
}).then(function () {
self.trigger_up('reload');
});
},
/**
* @private
* @override
* @param {MouseEvent} event
*/
_onRemoveMoveReconcile: function (event) {
var self = this;
var moveId = parseInt($(event.target).attr('move-id'));
var partialId = parseInt($(event.target).attr('partial-id'));
if (partialId !== undefined && !isNaN(partialId)){
this._rpc({
model: 'account.move',
method: 'js_remove_outstanding_partial',
args: [moveId, partialId],
}).then(function () {
self.trigger_up('reload');
});
}
},
});
field_registry.add('payment', ShowPaymentLineWidget);
return {
ShowPaymentLineWidget: ShowPaymentLineWidget
};
});

View file

@ -0,0 +1,31 @@
/** @odoo-module **/
import { qweb, _t } from 'web.core';
import fieldRegistry from 'web.field_registry';
import { FieldSelection } from 'web.relational_fields';
export const HierarchySelection = FieldSelection.extend({
init: function () {
this._super.apply(this, arguments);
this.hierarchyGroups = [
{name: _t('Balance Sheet')},
{name: _t('Assets'), children: this.values.filter(x => x[0] && x[0].startsWith('asset'))},
{name: _t('Liabilities'), children: this.values.filter(x => x[0] && x[0].startsWith('liability'))},
{name: _t('Equity'), children: this.values.filter(x => x[0] && x[0].startsWith('equity'))},
{name: _t('Profit & Loss')},
{name: _t('Income'), children: this.values.filter(x => x[0] && x[0].startsWith('income'))},
{name: _t('Expense'), children: this.values.filter(x => x[0] && x[0].startsWith('expense'))},
{name: _t('Other'), children: this.values.filter(x => x[0] && x[0] == 'off_balance')},
];
},
_renderEdit: function () {
this.$el.empty();
this.$el.append(qweb.render('accountTypeSelection', {widget: this}));
this.$el.val(JSON.stringify(this._getRawValue()));
}
});
fieldRegistry.add("account_type_selection", HierarchySelection);

View file

@ -0,0 +1,68 @@
odoo.define('account.activity', function (require) {
"use strict";
var AbstractField = require('web.AbstractField');
var core = require('web.core');
var field_registry = require('web.field_registry');
var QWeb = core.qweb;
var _t = core._t;
var VatActivity = AbstractField.extend({
className: 'o_journal_activity_kanban',
events: {
'click .see_all_activities': '_onOpenAll',
'click .see_activity': '_onOpenActivity',
},
init: function () {
this.MAX_ACTIVITY_DISPLAY = 5;
this._super.apply(this, arguments);
},
//------------------------------------------------------------
// Private
//------------------------------------------------------------
_render: function () {
var info = JSON.parse(this.value);
if (!info) {
this.$el.html('');
return;
}
info.more_activities = false;
if (info.activities.length > this.MAX_ACTIVITY_DISPLAY) {
info.more_activities = true;
info.activities = info.activities.slice(0, this.MAX_ACTIVITY_DISPLAY);
}
this.$el.html(QWeb.render('accountJournalDashboardActivity', info));
},
_onOpenActivity: function(e) {
e.preventDefault();
var self = this;
self.do_action({
type: 'ir.actions.act_window',
name: _t('Journal Entry'),
target: 'current',
res_id: $(e.target).data('resId'),
res_model: 'account.move',
views: [[false, 'form']],
});
},
_onOpenAll: function(e) {
e.preventDefault();
var self = this;
self.do_action({
type: 'ir.actions.act_window',
name: _t('Journal Entries'),
res_model: 'account.move',
views: [[false, 'kanban'], [false, 'form']],
search_view_id: [false],
domain: [['journal_id', '=', self.res_id], ['activity_ids', '!=', false]],
});
}
})
field_registry.add('kanban_vat_activity', VatActivity);
return VatActivity;
});

View file

@ -0,0 +1,23 @@
/** @odoo-module **/
import fieldRegistry from 'web.field_registry';
import { FieldChar } from 'web.basic_fields';
const OpenMoveWidget = FieldChar.extend({
template: 'account.OpenMoveTemplate',
events: Object.assign({}, FieldChar.prototype.events, {
'click': '_onOpenMove',
}),
_onOpenMove: function(ev) {
ev.preventDefault();
ev.stopPropagation();
var self = this;
this._rpc({
model: 'account.move.line',
method: 'action_open_business_doc',
args: [this.res_id],
}).then(function (actionData){
return self.do_action(actionData);
});
},
});
fieldRegistry.add('open_move_widget', OpenMoveWidget);

View file

@ -0,0 +1,106 @@
odoo.define('account.section_and_note_backend', function (require) {
// The goal of this file is to contain JS hacks related to allowing
// section and note on sale order and invoice.
// [UPDATED] now also allows configuring products on sale order.
"use strict";
var FieldChar = require('web.basic_fields').FieldChar;
var FieldOne2Many = require('web.relational_fields').FieldOne2Many;
var fieldRegistry = require('web.field_registry');
var ListFieldText = require('web.basic_fields').ListFieldText;
var ListRenderer = require('web.ListRenderer');
var SectionAndNoteListRenderer = ListRenderer.extend({
/**
* We want section and note to take the whole line (except handle and trash)
* to look better and to hide the unnecessary fields.
*
* @override
*/
_renderBodyCell: function (record, node, index, options) {
var $cell = this._super.apply(this, arguments);
var isSection = record.data.display_type === 'line_section';
var isNote = record.data.display_type === 'line_note';
if (isSection || isNote) {
if (node.attrs.widget === "handle") {
return $cell;
} else if (node.attrs.name === "name") {
var nbrColumns = this._getNumberOfCols();
if (this.handleField) {
nbrColumns--;
}
if (this.addTrashIcon) {
nbrColumns--;
}
$cell.attr('colspan', nbrColumns);
} else {
$cell.removeClass('o_invisible_modifier');
return $cell.addClass('o_hidden');
}
}
return $cell;
},
/**
* We add the o_is_{display_type} class to allow custom behaviour both in JS and CSS.
*
* @override
*/
_renderRow: function (record, index) {
var $row = this._super.apply(this, arguments);
if (record.data.display_type) {
$row.addClass('o_is_' + record.data.display_type);
}
return $row;
},
/**
* We want to add .o_section_and_note_list_view on the table to have stronger CSS.
*
* @override
* @private
*/
_renderView: function () {
var self = this;
return this._super.apply(this, arguments).then(function () {
self.$('.o_list_table').addClass('o_section_and_note_list_view');
});
}
});
// We create a custom widget because this is the cleanest way to do it:
// to be sure this custom code will only impact selected fields having the widget
// and not applied to any other existing ListRenderer.
var SectionAndNoteFieldOne2Many = FieldOne2Many.extend({
/**
* We want to use our custom renderer for the list.
*
* @override
*/
_getRenderer: function () {
if (this.view.arch.tag === 'tree') {
return SectionAndNoteListRenderer;
}
return this._super.apply(this, arguments);
},
});
// This is a merge between a FieldText and a FieldChar.
// We want a FieldChar for section,
// and a FieldText for the rest (product and note).
var SectionAndNoteFieldText = function (parent, name, record, options) {
var isSection = record.data.display_type === 'line_section';
var Constructor = isSection ? FieldChar : ListFieldText;
return new Constructor(parent, name, record, options);
};
fieldRegistry.add('section_and_note_one2many', SectionAndNoteFieldOne2Many);
fieldRegistry.add('section_and_note_text', SectionAndNoteFieldText);
return SectionAndNoteListRenderer;
});

View file

@ -0,0 +1,199 @@
/** @odoo-module alias=account.tax_group_owl **/
"use strict";
import session from 'web.session';
import AbstractFieldOwl from 'web.AbstractFieldOwl';
import fieldUtils from 'web.field_utils';
import field_registry from 'web.field_registry_owl';
import { LegacyComponent } from "@web/legacy/legacy_component";
const { onPatched, onWillUpdateProps, useRef, useState } = owl;
/**
A line of some LegacyTaxTotalsComponent, giving the values of a tax group.
**/
class LegacyTaxGroupComponent extends LegacyComponent {
setup() {
this.inputTax = useRef('taxValueInput');
this.state = useState({value: 'readonly'});
onPatched(this.onPatched);
onWillUpdateProps(this.onWillUpdateProps);
}
//--------------------------------------------------------------------------
// Life cycle methods
//--------------------------------------------------------------------------
onWillUpdateProps(nextProps) {
this.setState('readonly'); // If props are edited, we set the state to readonly
}
onPatched() {
if (this.state.value === 'edit') {
let newValue = this.props.taxGroup.tax_group_amount;
let currency = session.get_currency(this.props.record.data.currency_id.data.id);
newValue = fieldUtils.format.float(newValue, null, {digits: currency.digits});
this.inputTax.el.focus(); // Focus the input
this.inputTax.el.value = newValue;
}
}
//--------------------------------------------------------------------------
// Main methods
//--------------------------------------------------------------------------
/**
* The purpose of this method is to change the state of the component.
* It can have one of the following three states:
* - readonly: display in read-only mode of the field,
* - edit: display with a html input field,
* - disable: display with a html input field that is disabled.
*
* If a value other than one of these 3 states is passed as a parameter,
* the component is set to readonly by default.
*
* @param {String} value
*/
setState(value) {
if (['readonly', 'edit', 'disable'].includes(value)) {
this.state.value = value;
}
else {
this.state.value = 'readonly';
}
}
/**
* This method handles the "_onChangeTaxValue" event. In this method,
* we get the new value for the tax group, we format it and we call
* the method to recalculate the tax lines. At the moment the method
* is called, we disable the html input field.
*
* In case the value has not changed or the tax group is equal to 0,
* the modification does not take place.
*/
_onChangeTaxValue() {
this.setState('disable'); // Disable the input
let newValue = this.inputTax.el.value; // Get the new value
let currency = session.get_currency(this.props.record.data.currency_id.data.id); // The records using this widget must have a currency_id field.
try {
newValue = fieldUtils.parse.float(newValue); // Need a float for format the value
newValue = fieldUtils.format.float(newValue, null, {digits: currency.digits}); // Return a string rounded to currency precision
newValue = fieldUtils.parse.float(newValue); // Convert back to Float to compare with oldValue to know if value has changed
} catch (_err) {
$(this.inputTax.el).addClass('o_field_invalid');
this.setState('edit');
return;
}
// The newValue can't be equals to 0
if (newValue === this.props.taxGroup.tax_group_amount || newValue === 0) {
this.setState('readonly');
return;
}
this.props.taxGroup.tax_group_amount = newValue;
this.props.onChangeTaxGroup({
oldValue: this.props.taxGroup.tax_group_amount,
newValue: newValue,
taxGroupId: this.props.taxGroup.tax_group_id
});
}
}
LegacyTaxGroupComponent.props = ['taxGroup', 'readonly', 'record', 'onChangeTaxGroup'];
LegacyTaxGroupComponent.template = 'account.LegacyTaxGroupComponent';
/**
Widget used to display tax totals by tax groups for invoices, PO and SO,
and possibly allowing editing them.
Note that this widget requires the object it is used on to have a
currency_id field.
**/
class LegacyTaxTotalsComponent extends AbstractFieldOwl {
setup() {
super.setup();
this.totals = useState({value: this.value ? this.value : null});
this._computeTotalsFormat()
this.readonly = this.mode == 'readonly' || this.record.evalModifiers(this.attrs.modifiers).readonly;
onWillUpdateProps(this.onWillUpdateProps);
}
onWillUpdateProps(nextProps) {
// We only reformat tax groups if there are changed
this.totals.value = nextProps.record.data[this.props.fieldName];
this._computeTotalsFormat()
}
_onKeydown(ev) {
switch (ev.which) {
// Trigger only if the user clicks on ENTER or on TAB.
case $.ui.keyCode.ENTER:
case $.ui.keyCode.TAB:
// trigger blur to prevent the code being executed twice
$(ev.target).blur();
}
}
/**
* This method is the main function of the tax group widget.
* It is called by an event trigger (from the LegacyTaxGroupComponent) and receives
* a particular payload.
*
* It is responsible for calculating taxes based on tax groups and triggering
* an event to notify the ORM of a change.
*/
_onChangeTaxValueByTaxGroup(ev) {
this._computeTotalsFormat();
this.trigger('field-changed', {
dataPointID: this.record.id,
changes: { tax_totals: this.totals.value }
})
}
_format(amount) {
if (this.props.record.data.currency_id.data) {
const currency = session.get_currency(this.props.record.data.currency_id.data.id);
return fieldUtils.format.monetary(amount, null, {currency: currency});
}
return fieldUtils.format.monetary(amount);
}
_computeTotalsFormat() {
if (!this.totals.value) // Misc journal entry
return;
let amount_untaxed = this.totals.value.amount_untaxed;
let amount_tax = 0;
let subtotals = [];
for (let subtotal_title of this.totals.value.subtotals_order) {
let amount_total = amount_untaxed + amount_tax;
subtotals.push({
'name': subtotal_title,
'amount': amount_total,
'formatted_amount': this._format(amount_total),
});
let group = this.totals.value.groups_by_subtotal[subtotal_title];
for (let i in group) {
amount_tax = amount_tax + group[i].tax_group_amount;
}
}
this.totals.value.subtotals = subtotals;
let amount_total = amount_untaxed + amount_tax;
this.totals.value.amount_total = amount_total;
this.totals.value.formatted_amount_total = this._format(amount_total);
for (let group_name of Object.keys(this.totals.value.groups_by_subtotal)) {
let group = this.totals.value.groups_by_subtotal[group_name];
for (let i in group) {
group[i].formatted_tax_group_amount = this._format(group[i].tax_group_amount);
group[i].formatted_tax_group_base_amount = this._format(group[i].tax_group_base_amount);
}
}
}
}
LegacyTaxTotalsComponent.template = 'account.LegacyTaxTotalsField';
LegacyTaxTotalsComponent.components = { LegacyTaxGroupComponent };
field_registry.add('account-tax-totals-field', LegacyTaxTotalsComponent);
export default LegacyTaxTotalsComponent

View file

@ -0,0 +1,141 @@
odoo.define('account.tour', function(require) {
"use strict";
var core = require('web.core');
const {Markup} = require('web.utils');
var tour = require('web_tour.tour');
var _t = core._t;
tour.register('account_tour', {
url: "/web",
sequence: 60,
}, [
...tour.stepUtils.goToAppSteps('account.menu_finance', _t('Send invoices to your customers in no time with the <b>Invoicing app</b>.')),
{
trigger: "a.o_onboarding_step_action[data-method=action_open_base_onboarding_company]",
content: _t("Start by checking your company's data."),
position: "bottom",
skip_trigger: 'a[data-method=action_open_base_onboarding_company].o_onboarding_step_action__done',
}, {
trigger: "button[name=action_save_onboarding_company_step]",
extra_trigger: "a.o_onboarding_step_action[data-method=action_open_base_onboarding_company]",
content: _t("Looks good. Let's continue."),
position: "bottom",
skip_trigger: 'a[data-method=action_open_base_onboarding_company].o_onboarding_step_action__done',
}, {
trigger: "a.o_onboarding_step_action[data-method=action_open_base_document_layout]",
content: _t("Customize your layout."),
position: "bottom",
skip_trigger: 'a[data-method=action_open_base_document_layout].o_onboarding_step_action__done',
}, {
trigger: "button[name=document_layout_save]",
extra_trigger: "a.o_onboarding_step_action[data-method=action_open_base_document_layout]",
content: _t("Once everything is as you want it, validate."),
position: "top",
skip_trigger: 'a[data-method=action_open_base_document_layout].o_onboarding_step_action__done',
}, {
trigger: "a.o_onboarding_step_action[data-method=action_open_account_onboarding_create_invoice]",
content: _t("Now, we'll create your first invoice."),
position: "bottom",
}, {
trigger: "div[name=partner_id] .o_input_dropdown",
// FIXME WOWL: this selector needs to work in both legacy and non-legacy views
// because account_invoice_extracts *adds* a js_class on the base view which forces
// the use of a legacy view in enterprise only
extra_trigger: "[name=move_type] [raw-value=out_invoice], [name=move_type][raw-value=out_invoice]",
content: Markup(_t("Write a company name to <b>create one</b> or <b>see suggestions</b>.")),
position: "right",
}, {
trigger: "div[name=partner_id] input",
auto: true,
}, {
trigger: ".o_m2o_dropdown_option a:contains('Create')",
// FIXME WOWL: this selector needs to work in both legacy and non-legacy views
// because account_invoice_extracts *adds* a js_class on the base view which forces
// the use of a legacy view in enterprise only
extra_trigger: "[name=move_type] [raw-value=out_invoice], [name=move_type][raw-value=out_invoice]",
content: _t("Select first partner"),
auto: true,
}, {
trigger: ".modal-content button.btn-primary",
// FIXME WOWL: this selector needs to work in both legacy and non-legacy views
// because account_invoice_extracts *adds* a js_class on the base view which forces
// the use of a legacy view in enterprise only
extra_trigger: "[name=move_type] [raw-value=out_invoice], [name=move_type][raw-value=out_invoice]",
content: Markup(_t("Once everything is set, you are good to continue. You will be able to edit this later in the <b>Customers</b> menu.")),
auto: true,
}, {
trigger: "div[name=invoice_line_ids] .o_field_x2many_list_row_add a",
// FIXME WOWL: this selector needs to work in both legacy and non-legacy views
// because account_invoice_extracts *adds* a js_class on the base view which forces
// the use of a legacy view in enterprise only
extra_trigger: "[name=move_type] [raw-value=out_invoice], [name=move_type][raw-value=out_invoice]",
content: _t("Add a line to your invoice"),
}, {
trigger: "div[name=invoice_line_ids] div[name=name] textarea",
// FIXME WOWL: this selector needs to work in both legacy and non-legacy views
// because account_invoice_extracts *adds* a js_class on the base view which forces
// the use of a legacy view in enterprise only
extra_trigger: "[name=move_type] [raw-value=out_invoice], [name=move_type][raw-value=out_invoice]",
content: _t("Fill in the details of the line."),
position: "bottom",
}, {
trigger: "div[name=invoice_line_ids] div[name=price_unit] input",
// FIXME WOWL: this selector needs to work in both legacy and non-legacy views
// because account_invoice_extracts *adds* a js_class on the base view which forces
// the use of a legacy view in enterprise only
extra_trigger: "[name=move_type] [raw-value=out_invoice], [name=move_type][raw-value=out_invoice]",
content: _t("Set a price"),
position: "bottom",
run: 'text 100',
},
...tour.stepUtils.saveForm(),
{
trigger: "button[name=action_post]",
extra_trigger: "button.o_form_button_create",
content: _t("Once your invoice is ready, press CONFIRM."),
}, {
trigger: "button[name=action_invoice_sent]",
// FIXME WOWL: this selector needs to work in both legacy and non-legacy views
// because account_invoice_extracts *adds* a js_class on the base view which forces
// the use of a legacy view in enterprise only
extra_trigger: "[name=move_type] [raw-value=out_invoice], [name=move_type][raw-value=out_invoice]",
content: _t("Send the invoice and check what the customer will receive."),
}, {
trigger: ".o_field_widget[name=email] input, input[name=email]",
// FIXME WOWL: this selector needs to work in both legacy and non-legacy views
// because account_invoice_extracts *adds* a js_class on the base view which forces
// the use of a legacy view in enterprise only
extra_trigger: "[name=move_type] [raw-value=out_invoice], [name=move_type][raw-value=out_invoice]",
content: Markup(_t("Write here <b>your own email address</b> to test the flow.")),
run: 'text customer@example.com',
auto: true,
}, {
trigger: ".modal-content button.btn-primary",
// FIXME WOWL: this selector needs to work in both legacy and non-legacy views
// because account_invoice_extracts *adds* a js_class on the base view which forces
// the use of a legacy view in enterprise only
extra_trigger: "[name=move_type] [raw-value=out_invoice], [name=move_type][raw-value=out_invoice]",
content: _t("Validate."),
auto: true,
}, {
trigger: "button[name=send_and_print_action]",
// FIXME WOWL: this selector needs to work in both legacy and non-legacy views
// because account_invoice_extracts *adds* a js_class on the base view which forces
// the use of a legacy view in enterprise only
extra_trigger: "[name=move_type] [raw-value=out_invoice], [name=move_type][raw-value=out_invoice]",
content: _t("Let's send the invoice."),
position: "top"
}, {
trigger: "button[name=action_invoice_sent].btn-secondary",
content: _t("The invoice having been sent, the button has changed priority."),
run() {},
}, {
trigger: "button[name=action_register_payment]",
content: _t("The next step is payment registration."),
run() {},
}
]);
});

View file

@ -0,0 +1,61 @@
.o_kanban_dashboard.o_account_kanban {
&.o_kanban_ungrouped .o_account_dashboard_header {
margin: (0 - $o-kanban-record-margin) ($o-kanban-record-margin - $o-horizontal-padding) $o-kanban-record-margin;
}
.o_account_dashboard_header {
flex: 1 0 100%;
flex-flow: column nowrap;
align-self: flex-start;
width: 100%;
height: auto; // cancel o_form_view height 100%, which hides the help tip message at the bottom of the screen
min-height: 0%; // cancel o_form_view min-height 100%, which hides the help tip message at the bottom of the screen
background-color: $o-view-background-color;
.o_form_statusbar {
padding-right: $o-horizontal-padding;
}
h4 {
font-size: $font-size-base;
font-weight: 500;
}
.fa-gift {
color: #eeeeee;
&:hover {
color: #555555;
}
}
.o_arrow_button.btn-secondary {
color: $text-muted;
text-transform: none;
font-weight: 500;
.o_account_dashboard_index {
color: map-get($grays, '900');
}
&.o_action_done {
color: map-get($grays, '900');
background-color: map-get($grays, '200');
&:after {
border-left-color: map-get($grays, '200');
}
.fa-check {
@extend .text-success;
}
}
&:last-of-type {
margin-left: $o-horizontal-padding;
padding-left: $o-horizontal-padding*.5;
border-left: 1px solid map-get($grays, '300');
}
}
}
}

View file

@ -0,0 +1,70 @@
.o_kanban_dashboard.o_account_kanban {
.o_kanban_record {
@include media-breakpoint-up(sm) {
.oe_kanban_action_button {
margin-bottom: 5px;
}
}
.o_kanban_card_settings {
padding-top: $o-horizontal-padding/2;
padding-bottom: $o-horizontal-padding/2;
border-top: 1px solid;
border-color: $o-brand-lightsecondary;
}
.o_dashboard_star {
font-size: 12px;
&.fa-star-o {
color: $o-main-color-muted;
&:hover {
color: gold;
}
}
&.fa-star {
color: gold;
}
}
.o_dashboard_graph {
margin-bottom: -$o-horizontal-padding/2;
}
.o_field_widget.o_field_kanban_vat_activity {
display: block;
}
}
&.o_kanban_ungrouped {
.o_kanban_record {
width: 450px;
}
}
.o_kanban_group {
&:not(.o_column_folded) {
--KanbanGroup-width: 450px;
@include media-breakpoint-down(md) {
width: 100%;
}
}
}
}
// Style for the widget "dashboard_graph"
.o_dashboard_graph {
position: relative;
margin: 16px -16px;
canvas {
height: 75px;
}
}
.o_sample_data .o_dashboard_graph.o_graph_linechart > svg g.nv-linesWrap g.nv-group.nv-series-0 {
fill: gray !important;
opacity: 0.1;
}

View file

@ -0,0 +1,32 @@
.o_search_panel.account_root {
flex: 0 0 50px;
padding: 6px !important; // need to override bootstrap ps-4 rule
scrollbar-width: thin;
.o_search_panel_section_header {
display: none;
}
.list-group-item span.o_search_panel_label_title {
display: contents;
}
.o_search_panel_category_value {
padding-right: 0 !important;
header {
margin-left: 0;
}
.o_toggle_fold:empty{
width: 0;
}
.o_search_panel_category_value {
padding-left: 1.2rem !important; // need to override bootstrap ps-4 rule
.o_toggle_fold {
width: 0.3rem;
}
}
}
&::-webkit-scrollbar {
width: 4px;
}
&::-webkit-scrollbar-thumb {
background: lightgray;
}
}

View file

@ -0,0 +1,8 @@
.o_journal_activity_kanban {
display: block;
.align_activity_center {
width: 100%;
align-items: center;
margin-bottom: 5px;
}
}

View file

@ -0,0 +1,12 @@
@keyframes animate-red {
0% {
color: red;
}
100% {
color: inherit;
}
}
.animate {
animation: animate-red 1s ease;
}

View file

@ -0,0 +1,21 @@
<?xml version="1.0" encoding="UTF-8"?>
<templates>
<t t-name="accountJournalDashboardActivity">
<t t-foreach="activities" t-as="activity">
<div class="row">
<div class="col-8 o_mail_activity">
<a href="#" t-att-class="(activity.status == 'late' ? 'o_activity_color_overdue ' : ' ') + (activity.activity_category == 'tax_report' ? 'o_open_vat_report' : 'see_activity')" t-att-data-res-id="activity.res_id" t-att-data-id="activity.id" t-att-data-model="activity.res_model">
<t t-esc="activity.name"/>
</a>
</div>
<div class="col-4 text-end">
<span><t t-esc="activity.date"/></span>
</div>
</div>
</t>
<a t-if="more_activities" class="float-end see_all_activities" href="#">See all activities</a>
</t>
</templates>

View file

@ -0,0 +1,109 @@
<?xml version="1.0" encoding="UTF-8"?>
<templates xml:space="preserve">
<t t-name="ShowPaymentInfo">
<div>
<t t-if="outstanding">
<div>
<strong class="float-start" id="outstanding" t-esc="title"/>
</div>
</t>
<table style="width:100%;">
<t t-foreach="lines" t-as="line">
<tr>
<t t-if="outstanding">
<td>
<a title="assign to invoice"
role="button"
class="oe_form_field btn btn-secondary outstanding_credit_assign"
t-att-data-id="line.id"
style="margin-right: 0px; padding-left: 5px; padding-right: 5px;"
href="#"
data-bs-toggle="tooltip">Add</a>
</td>
<td style="max-width: 11em;">
<a t-att-title="line.date"
role="button"
class="oe_form_field btn btn-link open_account_move"
t-att-move-id="line.move_id"
style="margin-right: 5px; text-overflow: ellipsis; overflow: hidden; white-space: nowrap; padding-left: 0px; width:100%; text-align:left;"
data-bs-toggle="tooltip"
t-att-payment-id="account_payment_id"
t-esc="line.journal_name"/>
</td>
</t>
<t t-if="!outstanding">
<td>
<a role="button" tabindex="0" class="js_payment_info fa fa-info-circle" t-att-index="line.index" style="margin-right:5px;" aria-label="Info" title="Journal Entry Info" data-bs-toggle="tooltip"></a>
</td>
<td t-if="!line.is_exchange">
<i class="o_field_widget text-start o_payment_label">Paid on <t t-esc="line.date"></t></i>
</td>
<td t-if="line.is_exchange" colspan="2">
<i class="o_field_widget text-start text-muted text-start">
<span class="oe_form_field oe_form_field_float oe_form_field_monetary fw-bold">
<t t-if="line.position === 'before'">
<t t-esc="line.currency"/>
</t>
<t t-esc="line.amount"/>
<t t-if="line.position === 'after'">
<t t-esc="line.currency"/>
</t>
</span>
<span>Exchange Difference</span>
</i>
</td>
</t>
<td t-if="!line.is_exchange" style="text-align:right;">
<span class="oe_form_field oe_form_field_float oe_form_field_monetary" style="margin-left: -10px;">
<t t-if="line.position === 'before'">
<t t-esc="line.currency"/>
</t>
<t t-esc="line.amount"/>
<t t-if="line.position === 'after'">
<t t-esc="line.currency"/>
</t>
</span>
</td>
</tr>
</t>
</table>
</div>
</t>
<t t-name="PaymentPopOver">
<div>
<table>
<tr>
<td><strong>Amount: </strong></td>
<td>
<t t-esc="amount_company_currency"></t>
<t t-if="amount_foreign_currency">
(<span class="fa fa-money"/> <t t-esc="amount_foreign_currency"/>)
</t>
</td>
</tr>
<tr>
<td><strong>Memo: </strong></td>
<td>
<div style="width: 200px; word-wrap: break-word">
<t t-esc="ref"/>
</div>
</td>
</tr>
<tr>
<td><strong>Date: </strong></td>
<td><t t-esc="date"/></td>
</tr>
<tr>
<td><strong>Journal: </strong></td>
<td><t t-esc="journal_name"/><span t-if="payment_method_name"> (<t t-esc="payment_method_name"/>)</span></td>
</tr>
</table>
</div>
<button class="btn btn-sm btn-primary js_unreconcile_payment float-start" t-if="!is_exchange" t-att-partial-id="partial_id" t-att-payment-id="payment_id" t-att-move-id="move_id" style="margin-top:5px; margin-bottom:5px;" groups="account.group_account_invoice">Unreconcile</button>
<button class="btn btn-sm btn-secondary js_open_payment float-end" t-att-move-id="move_id" style="margin-top:5px; margin-bottom:5px;">View</button>
</t>
</templates>

View file

@ -0,0 +1,17 @@
<?xml version="1.0" encoding="UTF-8"?>
<templates>
<t t-name="accountTypeSelection">
<t t-foreach="widget.hierarchyGroups" t-as="group">
<optgroup t-att-label="group.name">
<t t-if="group.children">
<t t-foreach="group.children" t-as="child">
<option t-att-value="JSON.stringify(child[0])" t-out="child[1]"/>
</t>
</t>
</optgroup>
</t>
</t>
</templates>

View file

@ -0,0 +1,83 @@
<?xml version="1.0" encoding="utf-8"?>
<templates>
<t t-name="account.LegacyTaxGroupComponent" owl="1">
<tr>
<td class="o_td_label">
<label class="o_form_label o_tax_total_label" t-esc="props.taxGroup.tax_group_name"/>
</td>
<td>
<t t-if="!props.readonly">
<t t-if="['edit', 'disable'].includes(state.value)">
<span class="o_tax_group_edit_input">
<input
type="text"
t-ref="taxValueInput"
class="o_field_float
o_field_number o_input"
t-att-disabled="state.value === 'disable'"
t-on-blur.prevent="_onChangeTaxValue"/>
</span>
</t>
<t t-else="">
<span class="o_tax_group_edit" t-on-click.prevent="() => this.setState('edit')">
<i class="fa fa-pencil"/>
<span class="o_tax_group_amount_value">
<t t-out="props.taxGroup.formatted_tax_group_amount"/>
</span>
</span>
</t>
</t>
<t t-else="">
<span class="o_tax_group_amount_value">
<t t-out="props.taxGroup.formatted_tax_group_amount" style="white-space: nowrap;"/>
</span>
</t>
</td>
</tr>
</t>
<div t-name="account.LegacyTaxTotalsField" owl="1">
<table t-if="totals.value" class="oe_right">
<tbody>
<t t-foreach="totals.value.subtotals" t-as="subtotal" t-key="subtotal['name']">
<tr>
<td class="o_td_label">
<label class="o_form_label o_tax_total_label" t-esc="subtotal['name']"/>
</td>
<td>
<span t-att-name="subtotal['name']" style="white-space: nowrap; font-weight: bold;" t-out="subtotal['formatted_amount']"/>
</td>
</tr>
<t t-foreach="totals.value.groups_by_subtotal[subtotal['name']]" t-as="taxGroup" t-key="taxGroup.group_key">
<LegacyTaxGroupComponent
taxGroup="taxGroup"
record="record"
readonly="readonly"
onChangeTaxGroup.bind="_onChangeTaxValueByTaxGroup"
/>
</t>
</t>
<!-- Total amount with all taxes-->
<tr>
<td class="o_td_label">
<label class="o_form_label o_tax_total_label">Total</label>
</td>
<td>
<span
name="amount_total"
t-att-class="Object.keys(totals.value.groups_by_subtotal).length > 0 ? 'oe_subtotal_footer_separator' : ''"
t-out="totals.value.formatted_amount_total"
style="white-space: nowrap; font-weight: bold; font-size: 1.3em;"
/>
</td>
</tr>
</tbody>
</table>
</div>
</templates>