diff options
| author | stephanchrst <stephanchrst@gmail.com> | 2022-05-10 21:51:50 +0700 |
|---|---|---|
| committer | stephanchrst <stephanchrst@gmail.com> | 2022-05-10 21:51:50 +0700 |
| commit | 3751379f1e9a4c215fb6eb898b4ccc67659b9ace (patch) | |
| tree | a44932296ef4a9b71d5f010906253d8c53727726 /addons/account/static | |
| parent | 0a15094050bfde69a06d6eff798e9a8ddf2b8c21 (diff) | |
initial commit 2
Diffstat (limited to 'addons/account/static')
38 files changed, 2213 insertions, 0 deletions
diff --git a/addons/account/static/description/icon.png b/addons/account/static/description/icon.png Binary files differnew file mode 100644 index 00000000..e3a38a76 --- /dev/null +++ b/addons/account/static/description/icon.png diff --git a/addons/account/static/src/css/account.css b/addons/account/static/src/css/account.css new file mode 100644 index 00000000..c3817370 --- /dev/null +++ b/addons/account/static/src/css/account.css @@ -0,0 +1,38 @@ +.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: 350px; +} + +@media (max-width: 767.98px) { + .oe_invoice_outstanding_credits_debits { + min-width: initial; + width: 100%; + } +} diff --git a/addons/account/static/src/css/account_bank_and_cash.css b/addons/account/static/src/css/account_bank_and_cash.css new file mode 100644 index 00000000..1d2a4fba --- /dev/null +++ b/addons/account/static/src/css/account_bank_and_cash.css @@ -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; +}
\ No newline at end of file diff --git a/addons/account/static/src/css/tax_group.css b/addons/account/static/src/css/tax_group.css new file mode 100644 index 00000000..0b617056 --- /dev/null +++ b/addons/account/static/src/css/tax_group.css @@ -0,0 +1,25 @@ +.oe_tax_group_editable { + width: 100%; +} + +.tax_group_edit { + white-space: nowrap; +} + +.tax_group_edit:hover { + color: #00A09D; + cursor: pointer; +} + +.oe_tax_group_name { + font-weight: bold; + min-width: 150px; + text-align: right; + padding-right: 20px; +} + +.oe_tax_group_editable .oe_tax_group_amount_value input { + width: 65%; + float: right; + text-align: right; +} diff --git a/addons/account/static/src/img/account_dashboard_onboarding_bg.jpg b/addons/account/static/src/img/account_dashboard_onboarding_bg.jpg Binary files differnew file mode 100644 index 00000000..163ffdd9 --- /dev/null +++ b/addons/account/static/src/img/account_dashboard_onboarding_bg.jpg diff --git a/addons/account/static/src/img/account_invoice_onboarding_bg.jpg b/addons/account/static/src/img/account_invoice_onboarding_bg.jpg Binary files differnew file mode 100644 index 00000000..7b7e1a52 --- /dev/null +++ b/addons/account/static/src/img/account_invoice_onboarding_bg.jpg diff --git a/addons/account/static/src/img/btn_paynowcc_lg.gif b/addons/account/static/src/img/btn_paynowcc_lg.gif Binary files differnew file mode 100644 index 00000000..092c5542 --- /dev/null +++ b/addons/account/static/src/img/btn_paynowcc_lg.gif diff --git a/addons/account/static/src/img/graph.png b/addons/account/static/src/img/graph.png Binary files differnew file mode 100644 index 00000000..eb0efc7b --- /dev/null +++ b/addons/account/static/src/img/graph.png diff --git a/addons/account/static/src/img/invoice-stamps.png b/addons/account/static/src/img/invoice-stamps.png Binary files differnew file mode 100644 index 00000000..83a67022 --- /dev/null +++ b/addons/account/static/src/img/invoice-stamps.png diff --git a/addons/account/static/src/js/account_dashboard_setup_bar.js b/addons/account/static/src/js/account_dashboard_setup_bar.js new file mode 100644 index 00000000..e69de29b --- /dev/null +++ b/addons/account/static/src/js/account_dashboard_setup_bar.js diff --git a/addons/account/static/src/js/account_payment_field.js b/addons/account/static/src/js/account_payment_field.js new file mode 100644 index 00000000..fa56d1a1 --- /dev/null +++ b/addons/account/static/src/js/account_payment_field.js @@ -0,0 +1,156 @@ +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', + }, AbstractField.prototype.events), + supportedFieldTypes: ['char'], + + //-------------------------------------------------------------------------- + // Public + //-------------------------------------------------------------------------- + + /** + * @override + * @returns {boolean} + */ + isSet: function() { + return true; + }, + + //-------------------------------------------------------------------------- + // Private + //-------------------------------------------------------------------------- + + /** + * @private + * @override + */ + _render: function() { + var self = this; + var info = JSON.parse(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)); + var unreconcile_button = $content.filter('.js_unreconcile_payment').on('click', self._onRemoveMoveReconcile.bind(self)); + + $content.filter('.js_open_payment').on('click', self._onOpenPayment.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 + */ + _onOpenPayment: function (event) { + var paymentId = parseInt($(event.target).attr('payment-id')); + var moveId = parseInt($(event.target).attr('move-id')); + var res_model; + var id; + if (paymentId !== undefined && !isNaN(paymentId)){ + res_model = "account.payment"; + id = paymentId; + } else if (moveId !== undefined && !isNaN(moveId)){ + res_model = "account.move"; + id = moveId; + } + //Open form view of account.move with id = move_id + if (res_model && id) { + this.do_action({ + type: 'ir.actions.act_window', + res_model: res_model, + res_id: id, + views: [[false, 'form']], + target: 'current' + }); + } + }, + /** + * @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: [JSON.parse(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 +}; + +}); diff --git a/addons/account/static/src/js/account_portal_sidebar.js b/addons/account/static/src/js/account_portal_sidebar.js new file mode 100644 index 00000000..58114e00 --- /dev/null +++ b/addons/account/static/src/js/account_portal_sidebar.js @@ -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); + }, +}); +}); diff --git a/addons/account/static/src/js/account_resequence_field.js b/addons/account/static/src/js/account_resequence_field.js new file mode 100644 index 00000000..57b57bfe --- /dev/null +++ b/addons/account/static/src/js/account_resequence_field.js @@ -0,0 +1,32 @@ +odoo.define('account.ShowResequenceRenderer', function (require) { +"use strict"; + +const { Component } = owl; +const { useState } = owl.hooks; +const AbstractFieldOwl = require('web.AbstractFieldOwl'); +const field_registry = require('web.field_registry_owl'); + +class ChangeLine extends Component { } +ChangeLine.template = 'account.ResequenceChangeLine'; +ChangeLine.props = ["changeLine", 'ordering']; + + +class ShowResequenceRenderer extends AbstractFieldOwl { + constructor(...args) { + super(...args); + this.data = this.value ? JSON.parse(this.value) : { + changeLines: [], + ordering: 'date', + }; + } + async willUpdateProps(nextProps) { + await super.willUpdateProps(nextProps); + Object.assign(this.data, JSON.parse(this.value)); + } +} +ShowResequenceRenderer.template = 'account.ResequenceRenderer'; +ShowResequenceRenderer.components = { ChangeLine } + +field_registry.add('account_resequence_widget', ShowResequenceRenderer); +return ShowResequenceRenderer; +}); diff --git a/addons/account/static/src/js/account_selection.js b/addons/account/static/src/js/account_selection.js new file mode 100644 index 00000000..feee1c17 --- /dev/null +++ b/addons/account/static/src/js/account_selection.js @@ -0,0 +1,80 @@ +odoo.define('account.hierarchy.selection', function (require) { +"use strict"; + + var core = require('web.core'); + var relational_fields = require('web.relational_fields'); + var _t = core._t; + var registry = require('web.field_registry'); + + + var FieldSelection = relational_fields.FieldSelection; + + var qweb = core.qweb; + + var HierarchySelection = FieldSelection.extend({ + _renderEdit: function () { + var self = this; + var prom = Promise.resolve() + if (!self.hierarchy_groups) { + prom = this._rpc({ + model: 'account.account.type', + method: 'search_read', + kwargs: { + domain: [], + fields: ['id', 'internal_group', 'display_name'], + }, + }).then(function(arg) { + self.values = _.map(arg, v => [v['id'], v['display_name']]) + self.hierarchy_groups = [ + { + 'name': _t('Balance Sheet'), + 'children': [ + {'name': _t('Assets'), 'ids': _.map(_.filter(arg, v => v['internal_group'] == 'asset'), v => v['id'])}, + {'name': _t('Liabilities'), 'ids': _.map(_.filter(arg, v => v['internal_group'] == 'liability'), v => v['id'])}, + {'name': _t('Equity'), 'ids': _.map(_.filter(arg, v => v['internal_group'] == 'equity'), v => v['id'])}, + ], + }, + { + 'name': _t('Profit & Loss'), + 'children': [ + {'name': _t('Income'), 'ids': _.map(_.filter(arg, v => v['internal_group'] == 'income'), v => v['id'])}, + {'name': _t('Expense'), 'ids': _.map(_.filter(arg, v => v['internal_group'] == 'expense'), v => v['id'])}, + ], + }, + {'name': _t('Other'), 'ids': _.map(_.filter(arg, v => !['asset', 'liability', 'equity', 'income', 'expense'].includes(v['internal_group'])), v => v['id'])}, + ] + }); + } + + Promise.resolve(prom).then(function() { + self.$el.empty(); + self._addHierarchy(self.$el, self.hierarchy_groups, 0); + var value = self.value; + if (self.field.type === 'many2one' && value) { + value = value.data.id; + } + self.$el.val(JSON.stringify(value)); + }); + }, + _addHierarchy: function(el, group, level) { + var self = this; + _.each(group, function(item) { + var optgroup = $('<optgroup/>').attr(({ + 'label': $('<div/>').html(' '.repeat(6 * level) + item['name']).text(), + })) + _.each(item['ids'], function(id) { + var value = _.find(self.values, v => v[0] == id) + optgroup.append($('<option/>', { + value: JSON.stringify(value[0]), + text: value[1], + })); + }) + el.append(optgroup) + if (item['children']) { + self._addHierarchy(el, item['children'], level + 1); + } + }) + } + }); + registry.add("account_hierarchy_selection", HierarchySelection); +}); diff --git a/addons/account/static/src/js/bank_statement.js b/addons/account/static/src/js/bank_statement.js new file mode 100644 index 00000000..3ab8d935 --- /dev/null +++ b/addons/account/static/src/js/bank_statement.js @@ -0,0 +1,21 @@ +odoo.define('account.bank_statement', function(require) { + "use strict"; + + var KanbanController = require("web.KanbanController"); + var ListController = require("web.ListController"); + + var includeDict = { + renderButtons: function () { + this._super.apply(this, arguments); + if (this.modelName === "account.bank.statement") { + var data = this.model.get(this.handle); + if (data.context.journal_type !== 'cash') { + this.$buttons.find('button.o_button_import').hide(); + } + } + } + }; + + KanbanController.include(includeDict); + ListController.include(includeDict); +});
\ No newline at end of file diff --git a/addons/account/static/src/js/bills_tree_upload.js b/addons/account/static/src/js/bills_tree_upload.js new file mode 100644 index 00000000..2894b868 --- /dev/null +++ b/addons/account/static/src/js/bills_tree_upload.js @@ -0,0 +1,127 @@ +odoo.define('account.upload.bill.mixin', function (require) { +"use strict"; + + var core = require('web.core'); + var _t = core._t; + + var qweb = core.qweb; + + var UploadBillMixin = { + + start: function () { + // define a unique uploadId and a callback method + this.fileUploadID = _.uniqueId('account_bill_file_upload'); + $(window).on(this.fileUploadID, this._onFileUploaded.bind(this)); + return this._super.apply(this, arguments); + }, + + _onAddAttachment: function (ev) { + // Auto submit form once we've selected an attachment + var $input = $(ev.currentTarget).find('input.o_input_file'); + if ($input.val() !== '') { + var $binaryForm = this.$('.o_vendor_bill_upload form.o_form_binary_form'); + $binaryForm.submit(); + } + }, + + _onFileUploaded: function () { + // Callback once attachment have been created, create a bill with attachment ids + var self = this; + var attachments = Array.prototype.slice.call(arguments, 1); + // Get id from result + var attachent_ids = attachments.reduce(function(filtered, record) { + if (record.id) { + filtered.push(record.id); + } + return filtered; + }, []); + return this._rpc({ + model: 'account.journal', + method: 'create_invoice_from_attachment', + args: ["", attachent_ids], + context: this.initialState.context, + }).then(function(result) { + self.do_action(result); + }); + }, + + _onUpload: function (event) { + var self = this; + // If hidden upload form don't exists, create it + var $formContainer = this.$('.o_content').find('.o_vendor_bill_upload'); + if (!$formContainer.length) { + $formContainer = $(qweb.render('account.BillsHiddenUploadForm', {widget: this})); + $formContainer.appendTo(this.$('.o_content')); + } + // Trigger the input to select a file + this.$('.o_vendor_bill_upload .o_input_file').click(); + }, + } + return UploadBillMixin; +}); + + +odoo.define('account.bills.tree', function (require) { +"use strict"; + var core = require('web.core'); + var ListController = require('web.ListController'); + var ListView = require('web.ListView'); + var UploadBillMixin = require('account.upload.bill.mixin'); + var viewRegistry = require('web.view_registry'); + + var BillsListController = ListController.extend(UploadBillMixin, { + buttons_template: 'BillsListView.buttons', + events: _.extend({}, ListController.prototype.events, { + 'click .o_button_upload_bill': '_onUpload', + 'change .o_vendor_bill_upload .o_form_binary_form': '_onAddAttachment', + }), + }); + + var BillsListView = ListView.extend({ + config: _.extend({}, ListView.prototype.config, { + Controller: BillsListController, + }), + }); + + viewRegistry.add('account_tree', BillsListView); +}); + +odoo.define('account.dashboard.kanban', function (require) { +"use strict"; + var core = require('web.core'); + var KanbanController = require('web.KanbanController'); + var KanbanView = require('web.KanbanView'); + var UploadBillMixin = require('account.upload.bill.mixin'); + var viewRegistry = require('web.view_registry'); + + var DashboardKanbanController = KanbanController.extend(UploadBillMixin, { + events: _.extend({}, KanbanController.prototype.events, { + 'click .o_button_upload_bill': '_onUpload', + 'change .o_vendor_bill_upload .o_form_binary_form': '_onAddAttachment', + }), + /** + * We override _onUpload (from the upload bill mixin) to pass default_journal_id + * and default_move_type in context. + * + * @override + */ + _onUpload: function (event) { + var kanbanRecord = $(event.currentTarget).closest('.o_kanban_record').data('record'); + this.initialState.context['default_journal_id'] = kanbanRecord.id; + if ($(event.currentTarget).attr('journal_type') == 'sale') { + this.initialState.context['default_move_type'] = 'out_invoice' + } else if ($(event.currentTarget).attr('journal_type') == 'purchase') { + this.initialState.context['default_move_type'] = 'in_invoice' + } + UploadBillMixin._onUpload.apply(this, arguments); + } + }); + + var DashboardKanbanView = KanbanView.extend({ + config: _.extend({}, KanbanView.prototype.config, { + Controller: DashboardKanbanController, + }), + }); + + viewRegistry.add('account_dashboard_kanban', DashboardKanbanView); +}); diff --git a/addons/account/static/src/js/grouped_view_widget.js b/addons/account/static/src/js/grouped_view_widget.js new file mode 100644 index 00000000..e1df30cf --- /dev/null +++ b/addons/account/static/src/js/grouped_view_widget.js @@ -0,0 +1,40 @@ +odoo.define('account.ShowGroupedList', function (require) { +"use strict"; + +const { Component } = owl; +const { useState } = owl.hooks; +const AbstractFieldOwl = require('web.AbstractFieldOwl'); +const field_registry = require('web.field_registry_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 AbstractFieldOwl { + constructor(...args) { + super(...args); + this.data = this.value ? JSON.parse(this.value) : { + groups_vals: [], + options: { + discarded_number: '', + columns: [], + }, + }; + } + async willUpdateProps(nextProps) { + await super.willUpdateProps(nextProps); + Object.assign(this.data, JSON.parse(this.value)); + } +} +ShowGroupedList.template = 'account.GroupedListTemplate'; +ShowGroupedList.components = { ListGroup } + +field_registry.add('grouped_view_widget', ShowGroupedList); +return ShowGroupedList; +}); diff --git a/addons/account/static/src/js/mail_activity.js b/addons/account/static/src/js/mail_activity.js new file mode 100644 index 00000000..8b84afda --- /dev/null +++ b/addons/account/static/src/js/mail_activity.js @@ -0,0 +1,69 @@ +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 self = this; + 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; +}); diff --git a/addons/account/static/src/js/section_and_note_fields_backend.js b/addons/account/static/src/js/section_and_note_fields_backend.js new file mode 100644 index 00000000..749d29fc --- /dev/null +++ b/addons/account/static/src/js/section_and_note_fields_backend.js @@ -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; +}); diff --git a/addons/account/static/src/js/tax_group.js b/addons/account/static/src/js/tax_group.js new file mode 100644 index 00000000..a9de7f9f --- /dev/null +++ b/addons/account/static/src/js/tax_group.js @@ -0,0 +1,171 @@ +odoo.define('account.tax_group', function (require) { + "use strict"; + + var core = require('web.core'); + var session = require('web.session'); + var fieldRegistry = require('web.field_registry'); + var AbstractField = require('web.AbstractField'); + var fieldUtils = require('web.field_utils'); + var QWeb = core.qweb; + + var TaxGroupCustomField = AbstractField.extend({ + events: { + 'click .tax_group_edit': '_onClick', + 'keydown .oe_tax_group_editable .tax_group_edit_input input': '_onKeydown', + 'blur .oe_tax_group_editable .tax_group_edit_input input': '_onBlur', + }, + + //-------------------------------------------------------------------------- + // Private + //-------------------------------------------------------------------------- + + /** + * This method is called by "_setTaxGroups". It is + * responsible for calculating taxes based on + * tax groups and triggering an event to + * notify the ORM of a change. + * + * @param {Id} taxGroupId + * @param {Float} deltaAmount + */ + _changeTaxValueByTaxGroup: function (taxGroupId, deltaAmount) { + var self = this; + // Search for the first tax line with the same tax group and modify its value + var line_id = self.record.data.line_ids.data.find(elem => elem.data.tax_group_id && elem.data.tax_group_id.data.id === taxGroupId); + + var debitAmount = 0; + var creditAmount = 0; + var amount_currency = 0; + if (line_id.data.currency_id) { // If multi currency enable + if (this.record.data.move_type === "in_invoice") { + amount_currency = line_id.data.amount_currency - deltaAmount; + } else { + amount_currency = line_id.data.amount_currency + deltaAmount; + } + } else { + var balance = line_id.data.price_subtotal; + balance -= deltaAmount; + if (this.record.data.move_type === "in_invoice") { // For vendor bill + if (balance > 0) { + debitAmount = balance; + } else if (balance < 0) { + creditAmount = -balance; + } + } else { // For refund + if (balance > 0) { + creditAmount = balance; + } else if (balance < 0) { + debitAmount = -balance; + } + } + } + // Trigger ORM + self.trigger_up('field_changed', { + dataPointID: self.record.id, + changes: { line_ids: { operation: "UPDATE", id: line_id.id, data: { amount_currency: amount_currency, debit: debitAmount, credit: creditAmount } } }, // account.move change + initialEvent: { dataPointID: line_id.id, changes: { amount_currency: amount_currency, debit: debitAmount, credit: creditAmount }, }, // account.move.line change + }); + }, + + /** + * This method checks that the document where the widget + * is located is of the "in_invoice" or "in_refund" type. + * This makes it possible to know if it is a purchase + * document. + * + * @returns boolean (true if the invoice is a purchase document) + */ + _isPurchaseDocument: function () { + return this.record.data.move_type === "in_invoice" || this.record.data.move_type === 'in_refund'; + }, + + /** + * This method is part of the widget life cycle and allows you to render + * the widget. + * + * @private + * @override + */ + _render: function () { + var self = this; + // Display the pencil and allow the event to click and edit only on purchase that are not posted and in edit mode. + // since the field is readonly its mode will always be readonly. Therefore we have to use a trick by checking the + // formRenderer (the parent) and check if it is in edit in order to know the correct mode. + var displayEditWidget = self._isPurchaseDocument() && this.record.data.state === 'draft' && this.getParent().mode === 'edit'; + this.$el.html($(QWeb.render('AccountTaxGroupTemplate', { + lines: self.value, + displayEditWidget: displayEditWidget, + }))); + }, + + //-------------------------------------------------------------------------- + // Handler + //-------------------------------------------------------------------------- + + /** + * This method is called when the user is in edit mode and + * leaves the <input> field. Then, we execute the code that + * modifies the information. + * + * @param {event} ev + */ + _onBlur: function (ev) { + ev.preventDefault(); + var $input = $(ev.target); + var newValue = $input.val(); + var currency = session.get_currency(this.record.data.currency_id.data.id); + 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) { + $input.addClass('o_field_invalid'); + return; + } + var oldValue = $input.data('originalValue'); + if (newValue === oldValue || newValue === 0) { + return this._render(); + } + var taxGroupId = $input.parents('.oe_tax_group_editable').data('taxGroupId'); + this._changeTaxValueByTaxGroup(taxGroupId, oldValue-newValue); + }, + + /** + * This method is called when the user clicks on a specific <td>. + * it will hide the edit button and display the field to be edited. + * + * @param {event} ev + */ + _onClick: function (ev) { + ev.preventDefault(); + var $taxGroupElement = $(ev.target).parents('.oe_tax_group_editable'); + // Show input and hide previous element + $taxGroupElement.find('.tax_group_edit').addClass('d-none'); + $taxGroupElement.find('.tax_group_edit_input').removeClass('d-none'); + var $input = $taxGroupElement.find('.tax_group_edit_input input'); + // Get original value and display it in user locale in the input + var formatedOriginalValue = fieldUtils.format.float($input.data('originalValue'), {}, {}); + $input.focus(); // Focus the input + $input.val(formatedOriginalValue); //add value in user locale to the input + }, + + /** + * This method is called when the user is in edit mode and pressing + * a key on his keyboard. If this key corresponds to ENTER or TAB, + * the code that modifies the information is executed. + * + * @param {event} ev + */ + _onKeydown: function (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(); + } + }, + + }); + fieldRegistry.add('tax-group-custom-field', TaxGroupCustomField) +}); diff --git a/addons/account/static/src/js/tours/account.js b/addons/account/static/src/js/tours/account.js new file mode 100644 index 00000000..e854f1c3 --- /dev/null +++ b/addons/account/static/src/js/tours/account.js @@ -0,0 +1,93 @@ +odoo.define('account.tour', function(require) { +"use strict"; + +var core = require('web.core'); +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", + }, { + 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: "left", + }, { + trigger: "a.o_onboarding_step_action[data-method=action_open_base_document_layout]", + content: _t("Customize your layout."), + position: "bottom", + }, { + 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: "left", + }, { + 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] input", + extra_trigger: "[name=move_type][raw-value=out_invoice]", + content: _t("Write a company name to <b>create one</b> or <b>see suggestions</b>."), + position: "bottom", + }, { + trigger: ".o_m2o_dropdown_option a:contains('Create')", + extra_trigger: "[name=move_type][raw-value=out_invoice]", + content: _t("Select first partner"), + auto: true, + }, { + trigger: ".modal-content button.btn-primary", + extra_trigger: "[name=move_type][raw-value=out_invoice]", + content: _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:not([data-context])", + extra_trigger: "[name=move_type][raw-value=out_invoice]", + content: _t("Add a line to your invoice"), + }, { + trigger: "div[name=invoice_line_ids] textarea[name=name]", + extra_trigger: "[name=move_type][raw-value=out_invoice]", + content: _t("Fill in the details of the line."), + position: "bottom", + }, { + trigger: "div[name=invoice_line_ids] input[name=price_unit]", + extra_trigger: "[name=move_type][raw-value=out_invoice]", + content: _t("Set a price"), + position: "bottom", + run: 'text 100', + }, { + trigger: "button[name=action_post]", + extra_trigger: "[name=move_type][raw-value=out_invoice]", + content: _t("Once your invoice is ready, press CONFIRM."), + }, { + trigger: "button[name=action_invoice_sent]", + extra_trigger: "[name=move_type][raw-value=out_invoice]", + content: _t("Send the invoice and check what the customer will receive."), + }, { + trigger: "input[name=email]", + extra_trigger: "[name=move_type][raw-value=out_invoice]", + content: _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", + extra_trigger: "[name=move_type][raw-value=out_invoice]", + content: _t("Validate."), + auto: true, + }, { + trigger: "button[name=send_and_print_action]", + extra_trigger: "[name=move_type][raw-value=out_invoice]", + content: _t("Let's send the invoice."), + position: "left" + } +]); + +}); diff --git a/addons/account/static/src/scss/account_activity.scss b/addons/account/static/src/scss/account_activity.scss new file mode 100644 index 00000000..b4514086 --- /dev/null +++ b/addons/account/static/src/scss/account_activity.scss @@ -0,0 +1,8 @@ +.o_journal_activity_kanban { + display: block; + .align_activity_center { + width: 100%; + align-items: center; + margin-bottom: 5px; + } +}
\ No newline at end of file diff --git a/addons/account/static/src/scss/account_dashboard.scss b/addons/account/static/src/scss/account_dashboard.scss new file mode 100644 index 00000000..b93e2d35 --- /dev/null +++ b/addons/account/static/src/scss/account_dashboard.scss @@ -0,0 +1,61 @@ +.o_kanban_view.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: gray('900'); + } + + &.o_action_done { + color: gray('900'); + background-color: gray('200'); + + &:after { + border-left-color: gray('200'); + } + + .fa-check { + color: theme-color('success'); + } + } + + &:last-of-type { + margin-left: $o-horizontal-padding; + padding-left: $o-horizontal-padding*.5; + border-left: 1px solid gray('300'); + } + } + } +} diff --git a/addons/account/static/src/scss/account_journal_dashboard.scss b/addons/account/static/src/scss/account_journal_dashboard.scss new file mode 100644 index 00000000..6c36e61a --- /dev/null +++ b/addons/account/static/src/scss/account_journal_dashboard.scss @@ -0,0 +1,68 @@ +.o_kanban_view.o_kanban_dashboard.o_account_kanban { + .o_kanban_record { + + @include media-breakpoint-up(sm) { + .oe_kanban_action_button { + display: block; + 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_kanban_ungrouped { + .o_kanban_record { + width: 450px; + } + } + + .o_kanban_group { + &:not(.o_column_folded) { + width: 450px + 2*$o-kanban-group-padding; + + @include media-breakpoint-down(sm) { + 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; +} diff --git a/addons/account/static/src/scss/account_reconciliation.scss b/addons/account/static/src/scss/account_reconciliation.scss new file mode 100644 index 00000000..d745f825 --- /dev/null +++ b/addons/account/static/src/scss/account_reconciliation.scss @@ -0,0 +1,367 @@ +.progress-reconciliation { + .progress-bar { + font-size: 1.08333333rem; + height: 14px; + background-color: $o-enterprise-color; + span { + display: contents; + } + } +} + +.o_reconciliation { + + .o_filter_input_wrapper { + position: relative; + width: 150px; + margin: 0.5rem !important; + .searchIcon { + position: absolute; + right: 10px; + } + .o_filter_input { + border: none; + border-bottom: 1px black solid; + } + } + + .import_to_suspense { + margin: 0.5rem !important; + } + + .notification_area { + clear: both; + } + + .o_view_noreconciliation { + max-width: none; + padding: 0 10%; + color: $o-main-color-muted; + font-size: 125%; + } + + .accounting_view { + width: 100%; + + .cell_left { + border-right: 1px solid #333; + padding-right: 5px; + } + .edit_amount { + margin-left: 20px; + color: #bbb; + } + .cell:hover .edit_amount { + color: #00A09D; + } + .strike_amount { + text-decoration: line-through; + } + tbody tr:hover .cell_account_code::before { + content: "\f068"; + font-family: FontAwesome; + position: relative; + margin-left: -17px; + left: -4px; + line-height: 0; + padding: 3px 2px 5px 5px; + } + + } + + .o_multi_currency { + margin-right: 5px; + &.o_multi_currency_color_0 { + color: #dd6666; + } + &.o_multi_currency_color_1 { + color: #aaaaaa; + } + &.o_multi_currency_color_2 { + color: #66dd66; + } + &.o_multi_currency_color_3 { + color: #6666dd; + } + &.o_multi_currency_color_4 { + color: #dddd66; + } + &.o_multi_currency_color_5 { + color: #dd66dd; + } + &.o_multi_currency_color_6 { + color: #66dddd; + } + &.o_multi_currency_color_7 { + color: #aaa333; + } + } + + .o_reconciliation_line { + margin-bottom: 30px; + table { + width: 100%; + vertical-align: top; + } + tbody tr { + cursor: pointer; + } + tr.already_reconciled { + color: $o-account-info-color; + } + tr.invalid { + text-decoration: line-through; + } + td { + padding: 1px 2px; + } + thead td { + border-top: $o-account-light-border; + padding-top: 4px; + padding-bottom: 5px; + background-color: $o-account-initial-line-background; + } + tfoot td { + color: #bbb; + } + + /* columns */ + + .cell_action { + width: 15px; + color: gray('700'); + background: #fff; + border: 0; + text-align: center; + .fa-add-remove:before { + content: ""; + } + } + tr:hover .cell_action .fa-add-remove:before { + content: "\f068"; + } + .is_tax .cell_action .fa-add-remove:before { + position: relative; + top: -18px; + } + .cell_account_code { + width: 80px; + padding-left: 5px; + } + .cell_due_date { + width: 100px; + } + .cell_label { + width: auto; + } + .cell_left { + padding-right: 5px; + } + .cell_right, .cell_left { + text-align: right; + width: 120px; + } + .cell_info_popover { + text-align: right; + width: 15px; + color: #ccc; + + &:empty { + padding: 0; + width: 0; + } + } + + table.accounting_view { + .cell_right, .cell_left, .cell_label, .cell_due_date, .cell_account_code,.cell_info_popover { + box-shadow: 0 1px 0 #EAEAEA; + } + } + /* info popover */ + .popover { + max-width: none; + } + + table.details { + vertical-align: top; + td:first-child { + vertical-align: top; + padding-right: 10px; + font-weight: bold; + } + } + + tr.one_line_info { + td { + padding-top: 10px; + text-align: center; + color: $o-account-info-color; + } + } + + /* Icons */ + + .toggle_match, .toggle_create { + transform: rotate(0deg); + transition: transform 300ms ease 0s; + } + .visible_toggle, &[data-mode="match"] .toggle_match, &[data-mode="create"] .toggle_create { + visibility: visible !important; + transform: rotate(90deg); + } + .toggle_create { + font-size: 10px; + } + + /* Match view & Create view */ + > .o_notebook { + display: none; + + > .o_notebook_headers { + margin-right: 0; + margin-left: 0; + } + } + + > .o_notebook > .tab-content > div { + border: 1px solid #ddd; + border-top: 0; + } + + > .o_notebook .match table tr:hover { + background-color: #eee; + } + + &:not([data-mode="inactive"]) > .o_notebook { + display: block; + } + + &:not(:focus-within) .o_web_accesskey_overlay { + display: none; + } + &:focus caption .o_buttons button { + outline: none; + box-shadow: 4px 4px 4px 0px $o-enterprise-color; + } + &:focus{ + outline: none; + box-shadow: 0 0 0 0; + } + } + + .o_reconcile_models .btn-primary { + margin: 0 2px 3px 0; + } + + /* Match view */ + + .match { + .cell_action .fa-add-remove:before { + content: ""; + } + tr:hover .cell_action .fa-add-remove:before { + content: "\f067"; + } + .match_controls { + padding: 5px 0 5px ($o-account-action-col-width+$o-account-main-table-borders-padding); + + .filter { + width: 240px; + display: inline-block; + } + + .fa-chevron-left, .fa-chevron-right { + display: inline-block; + cursor: pointer; + } + + .fa-chevron-left { + margin-right: 10px; + } + + .fa-chevron-left.disabled, .fa-chevron-right.disabled { + color: #ddd; + cursor: default; + } + } + .show_more { + display: inline-block; + margin-left: ($o-account-action-col-width+$o-account-main-table-borders-padding); + margin-top: 5px; + } + } + + /* Create view */ + .create { + > div > div.quick_add > .o_reconcile_models { + max-width: 100%; + max-height: 70px; + flex-wrap: wrap; + overflow: auto; + + & > * { + flex-grow: 0; + } + } + .quick_add { + margin-bottom: 7px; + padding: 0 8px; + } + .o_group table.o_group_col_6 { + width: 49%; + margin: 0; + vertical-align: top; + } + .o_group table.o_group_col_6:first-child { + margin-left: 8px; + } + .btn { + padding-top: 0; + padding-bottom: 0; + } + .add_line_container { + text-align: center; + clear: both; + color: $o-enterprise-primary-color; + cursor: pointer; + } + } + + .o_notebook .tab-content > .tab-pane { + padding: 5px 0; + } +} + +/*Manual Reconciliation*/ +.o_manual_statement { + .accounting_view { + td[colspan="3"] span:first-child { + width: 100%; + display: inline-block; + } + td[colspan="2"] { + border-bottom: 1px solid #333; + text-align: center; + width: 240px; + } + .do_partial_reconcile_true { + display: none; + } + } +} + +// This is rtl language specific fix +// It will flip the fa-fa play icon in left direction +.o_rtl { + .o_reconciliation { + .o_reconciliation_line { + .toggle_match, .toggle_create { + transform: rotate(180deg); + transition: transform 300ms; + } + .visible_toggle, &[data-mode="match"] .toggle_match, &[data-mode="create"] .toggle_create { + transform: rotate(270deg); + } + } + } +} diff --git a/addons/account/static/src/scss/account_searchpanel.scss b/addons/account/static/src/scss/account_searchpanel.scss new file mode 100644 index 00000000..621a283a --- /dev/null +++ b/addons/account/static/src/scss/account_searchpanel.scss @@ -0,0 +1,26 @@ +.o_search_panel.account_root { + flex: 0 0 50px; + padding: 6px; + 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 { + header { + margin-left: 0; + padding-left: 0; + } + .o_search_panel_category_value .o_toggle_fold { + width: 0.3rem; + } + } + &::-webkit-scrollbar { + width: 4px; + } + &::-webkit-scrollbar-thumb { + background: lightgray; + } +} diff --git a/addons/account/static/src/scss/section_and_note_backend.scss b/addons/account/static/src/scss/section_and_note_backend.scss new file mode 100644 index 00000000..93ce4dfd --- /dev/null +++ b/addons/account/static/src/scss/section_and_note_backend.scss @@ -0,0 +1,29 @@ + +// 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; + } + } +} diff --git a/addons/account/static/src/scss/variables.scss b/addons/account/static/src/scss/variables.scss new file mode 100644 index 00000000..3e79231c --- /dev/null +++ b/addons/account/static/src/scss/variables.scss @@ -0,0 +1,19 @@ +$o-account-action-col-width: 15px; +$o-account-main-table-borders-padding: 3px; +$o-account-light-border: 1px solid #bbb; +$o-account-initial-line-background: #f0f0f0; +$o-account-info-color: #44c; + + +@keyframes animate-red { + 0% { + color: red; + } + 100% { + color: inherit; + } +} + +.animate { + animation: animate-red 1s ease; +} diff --git a/addons/account/static/src/xml/account_journal_activity.xml b/addons/account/static/src/xml/account_journal_activity.xml new file mode 100644 index 00000000..e91b9d4c --- /dev/null +++ b/addons/account/static/src/xml/account_journal_activity.xml @@ -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-right"> + <span><t t-esc="activity.date"/></span> + </div> + </div> + </t> + <a t-if="more_activities" class="pull-right see_all_activities" href="#">See all activities</a> + </t> + +</templates> diff --git a/addons/account/static/src/xml/account_payment.xml b/addons/account/static/src/xml/account_payment.xml new file mode 100644 index 00000000..c752f838 --- /dev/null +++ b/addons/account/static/src/xml/account_payment.xml @@ -0,0 +1,85 @@ +<?xml version="1.0" encoding="UTF-8"?> + +<templates xml:space="preserve"> + + <t t-name="ShowPaymentInfo"> + <div> + <t t-if="outstanding"> + <div> + <strong class="float-left" id="outstanding"><t t-esc="title"></t></strong> + </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-link outstanding_credit_assign" t-att-data-id="line.id" style="margin-right: 10px;" href="#" data-toggle="tooltip">Add</a> + </td> + <td style="max-width: 30em;"> + <div class="oe_form_field" style="margin-right: 30px; text-overflow: ellipsis; overflow: hidden; white-space: nowrap;" t-att-title="line.date" data-toggle="tooltip"><t t-esc="line.journal_name"></t></div> + </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="Payment Info" data-toggle="tooltip"></a> + </td> + <td> + <i class="o_field_widget text-right o_payment_label">Paid on <t t-esc="line.date"></t></i> + </td> + </t> + <td 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 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-if="position === 'before'"> + <t t-esc="currency"/> + </t> + <t t-esc="amount"></t> + <t t-if="position === 'after'"> + <t t-esc="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>Payment 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-left" 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-right" t-att-payment-id="account_payment_id" t-att-move-id="move_id" style="margin-top:5px; margin-bottom:5px;">View</button> + </t> + +</templates> diff --git a/addons/account/static/src/xml/account_resequence.xml b/addons/account/static/src/xml/account_resequence.xml new file mode 100644 index 00000000..172a0e94 --- /dev/null +++ b/addons/account/static/src/xml/account_resequence.xml @@ -0,0 +1,25 @@ +<?xml version="1.0" encoding="utf-8"?> +<templates> + + <div t-name="account.ResequenceRenderer" owl="1" class="d-block"> + <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-foreach="data.changeLines" t-as="changeLine" t-key="changeLine.id"> + <ChangeLine changeLine="changeLine" ordering="data.ordering"/> + </tbody> + </table> + </div> + + <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> diff --git a/addons/account/static/src/xml/bills_tree_upload_views.xml b/addons/account/static/src/xml/bills_tree_upload_views.xml new file mode 100644 index 00000000..32505e12 --- /dev/null +++ b/addons/account/static/src/xml/bills_tree_upload_views.xml @@ -0,0 +1,23 @@ +<?xml version="1.0" encoding="UTF-8"?> + +<templates> + <t t-name="account.BillsHiddenUploadForm"> + <div class="d-none o_vendor_bill_upload"> + <t t-call="HiddenInputFile"> + <t t-set="multi_upload" t-value="true"/> + <t t-set="fileupload_id" t-value="widget.fileUploadID"/> + <t t-set="fileupload_action" t-translation="off">/web/binary/upload_attachment</t> + <input type="hidden" name="model" value=""/> + <input type="hidden" name="id" value="0"/> + </t> + </div> + </t> + + <t t-extend="ListView.buttons" t-name="BillsListView.buttons"> + <t t-jquery="button.o_list_button_add" t-operation="after"> + <button type="button" class="btn btn-secondary o_button_upload_bill"> + Upload + </button> + </t> + </t> +</templates> diff --git a/addons/account/static/src/xml/grouped_view_widget.xml b/addons/account/static/src/xml/grouped_view_widget.xml new file mode 100644 index 00000000..9863fc82 --- /dev/null +++ b/addons/account/static/src/xml/grouped_view_widget.xml @@ -0,0 +1,36 @@ +<?xml version="1.0" encoding="utf-8"?> +<templates> + <div t-name="account.GroupedListTemplate" owl="1" class="d-block"> + <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"> + <th t-esc="col['label']" t-attf-class="{{col['class']}}"/> + </t> + </tr></thead> + <t t-foreach="data.groups_vals" t-as="group_vals"> + <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> + </div> + + <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"> + <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"> + <td t-esc="props.item_vals[col['field']]" t-attf-class="{{col['class']}}"/> + </t> + </tr> + +</templates> diff --git a/addons/account/static/src/xml/tax_group.xml b/addons/account/static/src/xml/tax_group.xml new file mode 100644 index 00000000..807fcfc7 --- /dev/null +++ b/addons/account/static/src/xml/tax_group.xml @@ -0,0 +1,35 @@ +<?xml version='1.0' encoding='utf-8'?> + +<templates> + <t t-name="AccountTaxGroupTemplate"> + <table class="o_group o_inner_group oe_subtotal_footer border-0 my-0" style="min-width: 100%;"> + <tbody> + <t t-foreach="lines" t-as="line"> + <tr> + <td class="o_td_label oe_tax_group_name"> + <label class="o_form_label" t-esc="line[0]"/> + </td> + <td class="oe_tax_group_editable" t-att-data-tax-group-id="line[6]"> + <t t-if="displayEditWidget and line[1] !== 0"> + <span class="tax_group_edit"> + <i class="fa fa-pencil"></i> + <span class="oe_tax_group_amount_value"> + <t t-esc="line[3]"/> + </span> + </span> + <span class="tax_group_edit_input d-none"> + <input type="text" class="o_field_float o_field_number o_input" t-att-data-original-value="line[1]"/> + </span> + </t> + <t t-if="!displayEditWidget or line[1] === 0"> + <span class="oe_tax_group_amount_value"> + <t t-esc="line[3]"/> + </span> + </t> + </td> + </tr> + </t> + </tbody> + </table> + </t> +</templates> diff --git a/addons/account/static/tests/account_payment_field_tests.js b/addons/account/static/tests/account_payment_field_tests.js new file mode 100644 index 00000000..48609728 --- /dev/null +++ b/addons/account/static/tests/account_payment_field_tests.js @@ -0,0 +1,84 @@ +odoo.define('account.reconciliation_field_tests', function (require) { +"use strict"; + +var FormView = require('web.FormView'); +var testUtils = require('web.test_utils'); + +var createView = testUtils.createView; + +QUnit.module('account', { + beforeEach: function () { + this.data = { + 'account.move': { + fields: { + payments_widget: {string: "payments_widget data", type: "char"}, + outstanding_credits_debits_widget: {string: "outstanding_credits_debits_widget data", type: "char"}, + }, + records: [{ + id: 1, + payments_widget: '{"content": [{"digits": [69, 2], "currency": "$", "amount": 555.0, "name": "Customer Payment: INV/2017/0004", "date": "2017-04-25", "position": "before", "ref": "BNK1/2017/0003 (INV/2017/0004)", "payment_id": 22, "move_id": 10, "partial_id": 38, "journal_name": "Bank"}], "outstanding": false, "title": "Less Payment"}', + outstanding_credits_debits_widget: '{"content": [{"digits": [69, 2], "currency": "$", "amount": 100.0, "journal_name": "INV/2017/0004", "position": "before", "id": 20}], "move_id": 4, "outstanding": true, "title": "Outstanding credits"}', + }] + }, + }; + } +}, function () { + QUnit.module('Reconciliation'); + + QUnit.test('Reconciliation form field', async function (assert) { + assert.expect(5); + + var form = await createView({ + View: FormView, + model: 'account.move', + data: this.data, + arch: '<form>'+ + '<field name="outstanding_credits_debits_widget" widget="payment"/>'+ + '<field name="payments_widget" widget="payment"/>'+ + '</form>', + res_id: 1, + mockRPC: function (route, args) { + if (args.method === 'js_remove_outstanding_partial') { + assert.deepEqual(args.args, [10, 38], "should call js_remove_outstanding_partial {warning: required focus}"); + return Promise.resolve(); + } + if (args.method === 'js_assign_outstanding_line') { + assert.deepEqual(args.args, [4, 20], "should call js_assign_outstanding_line {warning: required focus}"); + return Promise.resolve(); + } + return this._super.apply(this, arguments); + }, + intercepts: { + do_action: function (event) { + assert.deepEqual(event.data.action, { + 'type': 'ir.actions.act_window', + 'res_model': 'account.move', + 'res_id': 10, + 'views': [[false, 'form']], + 'target': 'current' + }, + "should open the form view"); + }, + }, + }); + + assert.strictEqual(form.$('.o_field_widget[name="payments_widget"]').text().replace(/[\s\n\r]+/g, ' '), + " Paid on 04/25/2017 $ 555.00 ", + "should display payment information"); + + form.$('.o_field_widget[name="outstanding_credits_debits_widget"] .outstanding_credit_assign').trigger('click'); + + assert.strictEqual(form.$('.o_field_widget[name="outstanding_credits_debits_widget"]').text().replace(/[\s\n\r]+/g, ' '), + " Outstanding credits Add INV/2017/0004 $ 100.00 ", + "should display outstanding information"); + + form.$('.o_field_widget[name="payments_widget"] .js_payment_info').trigger('focus'); + form.$('.popover .js_open_payment').trigger('click'); + + form.$('.o_field_widget[name="payments_widget"] .js_payment_info').trigger('focus'); + form.$('.popover .js_unreconcile_payment').trigger('click'); + + form.destroy(); + }); +}); +}); diff --git a/addons/account/static/tests/section_and_note_tests.js b/addons/account/static/tests/section_and_note_tests.js new file mode 100644 index 00000000..7dcef00a --- /dev/null +++ b/addons/account/static/tests/section_and_note_tests.js @@ -0,0 +1,98 @@ +odoo.define('account.section_and_note_tests', function (require) { +"use strict"; + +var FormView = require('web.FormView'); +var testUtils = require('web.test_utils'); +var createView = testUtils.createView; + +QUnit.module('section_and_note', { + beforeEach: function () { + this.data = { + invoice: { + fields: { + invoice_line_ids: { + string: "Lines", + type: 'one2many', + relation: 'invoice_line', + relation_field: 'invoice_id' + }, + }, + records: [ + {id: 1, invoice_line_ids: [1, 2]}, + ], + }, + invoice_line: { + fields: { + display_type: { + string: 'Type', + type: 'selection', + selection: [['line_section', "Section"], ['line_note', "Note"]] + }, + invoice_id: { + string: "Invoice", + type: 'many2one', + relation: 'invoice' + }, + name: { + string: "Name", + type: 'text' + }, + }, + records: [ + {id: 1, display_type: false, invoice_id: 1, name: 'product\n2 lines'}, + {id: 2, display_type: 'line_section', invoice_id: 1, name: 'section'}, + ] + }, + }; + }, +}, function () { + QUnit.test('correct display of section and note fields', async function (assert) { + assert.expect(5); + var form = await createView({ + View: FormView, + model: 'invoice', + data: this.data, + arch: '<form>' + + '<field name="invoice_line_ids" widget="section_and_note_one2many"/>' + + '</form>', + archs: { + 'invoice_line,false,list': '<tree editable="bottom">' + + '<field name="display_type" invisible="1"/>' + + '<field name="name" widget="section_and_note_text"/>' + + '</tree>', + }, + res_id: 1, + }); + + assert.hasClass(form.$('[name="invoice_line_ids"] table'), 'o_section_and_note_list_view'); + + // section should be displayed correctly + var $tr0 = form.$('tr.o_data_row:eq(0)'); + + assert.doesNotHaveClass($tr0, 'o_is_line_section', + "should not have a section class"); + + var $tr1 = form.$('tr.o_data_row:eq(1)'); + + assert.hasClass($tr1, 'o_is_line_section', + "should have a section class"); + + // enter edit mode + await testUtils.form.clickEdit(form); + + // editing line should be textarea + $tr0 = form.$('tr.o_data_row:eq(0)'); + await testUtils.dom.click($tr0.find('td.o_data_cell')); + assert.containsOnce($tr0, 'td.o_data_cell textarea[name="name"]', + "editing line should be textarea"); + + // editing section should be input + $tr1 = form.$('tr.o_data_row:eq(1)'); + await testUtils.dom.click($tr1.find('td.o_data_cell')); + assert.containsOnce($tr1, 'td.o_data_cell input[name="name"]', + "editing section should be input"); + + form.destroy(); + }); +}); +}); diff --git a/addons/account/static/tests/tours/account_dashboard_setup_bar_tests.js b/addons/account/static/tests/tours/account_dashboard_setup_bar_tests.js new file mode 100644 index 00000000..0d11b027 --- /dev/null +++ b/addons/account/static/tests/tours/account_dashboard_setup_bar_tests.js @@ -0,0 +1,42 @@ +odoo.define('account.dashboard.setup.tour', function (require) { + "use strict"; + + var core = require('web.core'); + var tour = require('web_tour.tour'); + + var _t = core._t; + + tour.register('account_render_report', { + test: true, + url: '/web', + }, [tour.stepUtils.showAppsMenuItem(), + { + id: 'account_menu_click', + trigger: '.o_app[data-menu-xmlid="account.menu_finance"]', + position: 'bottom', + }, { + trigger: '.o_data_row:first', + extra_trigger: '.breadcrumb', + }, { + trigger: '.o_control_panel button:contains("' + _t('Print') + '")', + }, { + trigger: '.o_control_panel .o_dropdown_menu a:contains("' + _t('Invoices without Payment') + '")', + }, { + trigger: 'iframe .o_report_layout_standard h2', + content: 'Primary color is correct', + run: function () { + if (this.$anchor.css('color') !== "rgb(18, 52, 86)") { + console.error('The primary color should be the one set on the company.'); + } + }, + }, { + trigger: 'iframe .o_report_layout_standard #informations div strong', + content: 'Secondary color is correct', + run: function () { + if (this.$anchor.css('color') !== "rgb(120, 145, 1)") { + console.error('The secondary color should be the one set on the company.'); + } + }, + } + ]); +}); diff --git a/addons/account/static/tests/tours/tax_group_tests.js b/addons/account/static/tests/tours/tax_group_tests.js new file mode 100644 index 00000000..08cb218f --- /dev/null +++ b/addons/account/static/tests/tours/tax_group_tests.js @@ -0,0 +1,124 @@ +odoo.define('account.tax.group.tour.tests', function (require) { + "use strict"; + + var core = require('web.core'); + var tour = require('web_tour.tour'); + var _t = core._t; + + tour.register('account_tax_group', { + test: true, + url: "/web", + }, [tour.stepUtils.showAppsMenuItem(), + { + content: "Go to Invoicing", + trigger: '.o_app[data-menu-xmlid="account.menu_finance"]', + edition: 'community', + }, + { + content: "Go to Accounting", + trigger: '.o_app[data-menu-xmlid="account_accountant.menu_accounting"]', + edition: 'enterprise', + }, + { + content: "Go to Vendors", + trigger: 'a:contains("Vendors")', + }, + { + content: "Go to Bills", + trigger: 'span:contains("Bills")', + }, + { + extra_trigger: '.breadcrumb:contains("Bills")', + content: "Create new bill", + trigger: '.o_list_button_add', + }, + // Set a vendor + { + content: "Add vendor", + trigger: 'div.o_field_widget.o_field_many2one[name="partner_id"] div input', + run: 'text Azure Interior', + }, + { + content: "Valid vendor", + trigger: '.ui-menu-item a:contains("Azure Interior")', + }, + // Add First product + { + content: "Add items", + trigger: 'div[name="invoice_line_ids"] .o_field_x2many_list_row_add a:contains("Add a line")', + }, + { + content: "Select input", + trigger: 'div[name="invoice_line_ids"] .o_list_view .o_selected_row .o_list_many2one:first input', + }, + { + content: "Type item", + trigger: 'div[name="invoice_line_ids"] .o_list_view .o_selected_row .o_list_many2one:first input', + run: "text Large Desk", + }, + { + content: "Valid item", + trigger: '.ui-menu-item-wrapper:contains("Large Desk")', + }, + // Save account.move + { + content: "Save the account move", + trigger: '.o_form_button_save', + }, + // Edit account.move + { + content: "Edit the account move", + trigger: '.o_form_button_edit', + }, + // Edit tax group amount + { + content: "Edit tax group amount", + trigger: '.oe_tax_group_amount_value', + }, + { + content: "Modify the input value", + trigger: '.tax_group_edit_input input', + run: function (actions) { + $('.tax_group_edit_input input').val(200); + $('.tax_group_edit_input input').select(); + var keydownEvent = jQuery.Event('keydown'); + keydownEvent.which = 13; + this.$anchor.trigger(keydownEvent); + }, + }, + // Check new value for total (with modified tax_group_amount). + { + content: "Valid total amount", + trigger: 'span[name="amount_total"]:contains("1,499.00")', + }, + // Modify the quantity of the object + { + content: "Select item quantity", + trigger: 'div[name="invoice_line_ids"] .o_list_view tbody tr.o_data_row .o_list_number[title="1.000"]', + }, + { + content: "Change item quantity", + trigger: 'div[name="invoice_line_ids"] .o_list_view tbody tr.o_data_row .o_list_number[title="1.000"] input', + run: 'text 2', + }, + { + content: "Valid the new value", + trigger: 'div[name="invoice_line_ids"] .o_list_view tbody tr.o_data_row .o_list_number[title="1.000"] input', + run: function (actions) { + var keydownEvent = jQuery.Event('keydown'); + keydownEvent.which = 13; + this.$anchor.trigger(keydownEvent); + }, + }, + // Save form + { + content: "Save the account move", + trigger: '.o_form_button_save', + }, + // Check new tax group value + { + content: "Check new value of tax group", + trigger: '.oe_tax_group_amount_value:contains("389.70")', + }, + ]); +}); |
