diff --git a/addons/account/__manifest__.py b/addons/account/__manifest__.py index 9654e47f01e..3b8a9fa18fa 100644 --- a/addons/account/__manifest__.py +++ b/addons/account/__manifest__.py @@ -62,7 +62,7 @@ Core mechanisms for the accounting modules. To display the menuitems, install th 'report/account_report_payment_receipt_templates.xml', 'data/payment_receipt_data.xml', 'wizard/setup_wizards_view.xml', - 'views/account_dashboard_setup_bar.xml', + 'views/account_onboarding_templates.xml', 'wizard/account_report_tax_view.xml', 'views/report_tax.xml', 'views/res_users_views.xml', diff --git a/addons/account/controllers/__init__.py b/addons/account/controllers/__init__.py index e5a6112c48f..6d24304de46 100644 --- a/addons/account/controllers/__init__.py +++ b/addons/account/controllers/__init__.py @@ -2,4 +2,5 @@ # Part of Odoo. See LICENSE file for full copyright and licensing details. from . import mail +from . import onboarding from . import portal diff --git a/addons/account/controllers/onboarding.py b/addons/account/controllers/onboarding.py new file mode 100644 index 00000000000..1fd91294489 --- /dev/null +++ b/addons/account/controllers/onboarding.py @@ -0,0 +1,37 @@ +from odoo import http +from odoo.http import request + + +class OnboardingController(http.Controller): + + @http.route('/account/account_invoice_onboarding', auth='user', type='json') + def account_invoice_onboarding(self): + """ Returns the `banner` for the account invoice onboarding panel. + It can be empty if the user has closed it or if he doesn't have + the permission to see it. """ + + if not request.env.user._is_admin() or \ + request.env.user.company_id.account_invoice_onboarding_closed: + return {} + + return { + 'html': request.env.ref('account.account_invoice_onboarding_panel').render({ + 'company': request.env.user.company_id, + }) + } + + @http.route('/account/account_dashboard_onboarding', auth='user', type='json') + def account_dashboard_onboarding(self): + """ Returns the `banner` for the account dashboard onboarding panel. + It can be empty if the user has closed it or if he doesn't have + the permission to see it. """ + + if not request.env.user._is_admin() or \ + request.env.user.company_id.account_dashboard_onboarding_closed: + return {} + + return { + 'html': request.env.ref('account.account_dashboard_onboarding_panel').render({ + 'company': request.env.user.company_id, + }) + } \ No newline at end of file diff --git a/addons/account/models/account_invoice.py b/addons/account/models/account_invoice.py index 190640cb34d..a38e33ebac1 100644 --- a/addons/account/models/account_invoice.py +++ b/addons/account/models/account_invoice.py @@ -621,6 +621,7 @@ class AccountInvoice(models.Model): def message_post(self, **kwargs): if self.env.context.get('mark_invoice_as_sent'): self.filtered(lambda inv: not inv.sent).write({'sent': True}) + self.env.user.company_id.account_onboarding_sample_invoice_sent = True return super(AccountInvoice, self.with_context(mail_post_autofollow=True)).message_post(**kwargs) @api.model diff --git a/addons/account/models/company.py b/addons/account/models/company.py index 65162b07490..efde6a5c4cc 100644 --- a/addons/account/models/company.py +++ b/addons/account/models/company.py @@ -6,7 +6,7 @@ import time from dateutil.relativedelta import relativedelta from odoo import fields, models, api, _ -from odoo.exceptions import ValidationError, UserError +from odoo.exceptions import ValidationError, UserError, RedirectWarning from odoo.tools.misc import DEFAULT_SERVER_DATE_FORMAT from odoo.tools.float_utils import float_round, float_is_zero from odoo.tools import DEFAULT_SERVER_DATE_FORMAT, date_utils @@ -67,6 +67,27 @@ Best Regards,''')) account_setup_coa_done = fields.Boolean(string='Chart of Account Checked', help="Technical field holding the status of the chart of account setup step.") account_setup_bar_closed = fields.Boolean(string='Setup Bar Closed', help="Technical field set to True when setup bar has been closed by the user.") + # account invoice onboarding + account_invoice_onboarding_closed = fields.Boolean( + string="Account invoice onboarding panel closed", + help="Refers to the account invoice onboarding panel closed state.") + account_invoice_onboarding_folded = fields.Boolean( + string="Account invoice onboarding panel folded", + help="Refers to the account invoice onboarding panel folded state.") + + account_onboarding_invoice_layout_done = fields.Boolean("Onboarding invoice layout step done", + compute="_compute_account_onboarding_invoice_layout_done") + account_onboarding_sample_invoice_sent = fields.Boolean( + "Onboarding sample invoice step completed", default=False) + + # account dashboard onboarding + account_dashboard_onboarding_closed = fields.Boolean( + string="Account dashboard onboarding panel closed", + help="Refers to the account dashboard onboarding panel closed state.") + account_dashboard_onboarding_folded = fields.Boolean( + string="Account dashboard onboarding panel folded", + help="Refers to the account dashboard onboarding panel folded state.") + @api.multi def _check_lock_dates(self, vals): '''Check the lock dates for the current companies. This can't be done in a api.constrains because we need @@ -398,3 +419,111 @@ Best Regards,''')) 'debit': credit_diff, 'credit': debit_diff, }) + + @api.depends('logo', 'account_invoice_onboarding_closed') + def _compute_account_onboarding_invoice_layout_done(self): + """ The invoice onboarding step is marked as done if logo is filled + and different from the default one. """ + for record in self: + record.account_onboarding_invoice_layout_done = \ + record.account_invoice_onboarding_closed or ( + bool(record.logo) and record.logo != record._get_logo()) + + @api.model + def action_toggle_fold_account_invoice_onboarding(self): + """ Toggle the onboarding panel `folded` state. """ + self.env.user.company_id.account_invoice_onboarding_folded =\ + not self.env.user.company_id.account_invoice_onboarding_folded + + @api.model + def action_close_account_invoice_onboarding(self): + """ Mark the onboarding panel as closed. """ + self.env.user.company_id.account_invoice_onboarding_closed = True + + @api.model + def action_toggle_fold_account_dashboard_onboarding(self): + """ Toggle the dashboard onboarding panel `folded` state. """ + self.env.user.company_id.account_dashboard_onboarding_folded =\ + not self.env.user.company_id.account_dashboard_onboarding_folded + + @api.model + def action_close_account_dashboard_onboarding(self): + """ Mark the dashboard onboarding panel as closed. """ + self.env.user.company_id.account_dashboard_onboarding_closed = True + + @api.model + def action_open_account_onboarding_invoice_layout(self): + """ Onboarding step for the invoice layout. """ + action = self.env.ref('account.action_open_account_onboarding_invoice_layout').read()[0] + action['res_id'] = self.env.user.company_id.id + return action + + @api.model + def _get_sample_invoice(self): + """ Get a sample invoice or create one if it does not exist. """ + # use current user as partner + partner = self.env.user.partner_id + + company_id = self.env.user.company_id.id + # try to find an existing sample invoice + sample_invoice = self.env['account.invoice'].search( + [('company_id', '=', company_id), + ('partner_id', '=', partner.id)], limit=1) + + if len(sample_invoice) == 0: + # If there are no existing accounts or no journal, fail + account = self.env['account.account'].search([('company_id', '=', company_id)], limit=1) + if len(account) == 0: + action = self.env.ref('account.action_account_config') + msg = _( + "We cannot find a chart of accounts for this company, you should configure it. \n" + "Please go to Account Configuration and select or install a fiscal localization.") + raise RedirectWarning(msg, action.id, _("Go to the configuration panel")) + + journal = self.env['account.journal'].search([('company_id', '=', company_id)], limit=1) + if len(journal) == 0: + action = self.env.ref('account.action_account_journal_form') + msg = _("We cannot find any journal for this company. You should create one." + "\nPlease go to Configuration > Journals.") + raise RedirectWarning(msg, action.id, _("Go to the journal configuration")) + + sample_invoice = self.env['account.invoice'].create({ + 'name': _("Sample invoice"), + 'journal_id': journal.id, + 'partner_id': partner.id, + }) + # sample invoice lines + self.env['account.invoice.line'].create({ + 'name': _("Sample invoice line name"), + 'invoice_id': sample_invoice.id, + 'account_id': account.id, + 'price_unit': 199.99, + 'quantity': 2, + }) + self.env['account.invoice.line'].create({ + 'name': _("Sample invoice line name 2"), + 'invoice_id': sample_invoice.id, + 'account_id': account.id, + 'price_unit': 25, + 'quantity': 1, + }) + return sample_invoice + + @api.model + def action_open_account_onboarding_sample_invoice(self): + """ Onboarding step for sending a sample invoice. Open a window to compose an email, + with the edi_invoice_template message loaded by default. """ + sample_invoice = self._get_sample_invoice() + template = self.env.ref('account.email_template_edi_invoice', False) + action = self.env.ref('account.action_open_account_onboarding_sample_invoice').read()[0] + action['context'] = { + 'default_res_id': sample_invoice.id, + 'default_use_template': bool(template), + 'default_template_id': template and template.id or False, + 'default_model': 'account.invoice', + 'default_composition_mode': 'comment', + 'mark_invoice_as_sent': True, + 'custom_layout': 'mail.mail_notification_borders', + 'force_email': True, + } + return action diff --git a/addons/account/static/src/img/account_dashboard_onboarding_bg.jpg b/addons/account/static/src/img/account_dashboard_onboarding_bg.jpg new file mode 100644 index 00000000000..163ffdd9e31 Binary files /dev/null and b/addons/account/static/src/img/account_dashboard_onboarding_bg.jpg differ diff --git a/addons/account/static/src/img/account_invoice_onboarding_bg.jpg b/addons/account/static/src/img/account_invoice_onboarding_bg.jpg new file mode 100644 index 00000000000..7b7e1a524d5 Binary files /dev/null and b/addons/account/static/src/img/account_invoice_onboarding_bg.jpg differ diff --git a/addons/account/views/account.xml b/addons/account/views/account.xml index ca67c3a11d3..a6da2f17b5f 100644 --- a/addons/account/views/account.xml +++ b/addons/account/views/account.xml @@ -38,7 +38,6 @@ - diff --git a/addons/account/views/account_invoice_view.xml b/addons/account/views/account_invoice_view.xml index 147e0d56ac7..e6e57606a70 100644 --- a/addons/account/views/account_invoice_view.xml +++ b/addons/account/views/account_invoice_view.xml @@ -167,6 +167,18 @@ + + account.invoice.tree.with.onboarding + account.invoice + + primary + + + /account/account_invoice_onboarding + + + + account.invoice.kanban account.invoice @@ -727,11 +739,10 @@ - tree - + diff --git a/addons/account/views/account_journal_dashboard_view.xml b/addons/account/views/account_journal_dashboard_view.xml index 346e7793d1f..d56d51cb2b4 100644 --- a/addons/account/views/account_journal_dashboard_view.xml +++ b/addons/account/views/account_journal_dashboard_view.xml @@ -3,7 +3,7 @@ account.journal.dashboard.kanban account.journal - + ', domain: [['id', 'in', [1, 2]]], + }).then(function (kanban) { + // nvd3 seems to do a setTimeout(0) each time the addGraph function is + // called, which is done twice in this case as there are 2 records. + // for that reason, we need to do two setTimeout(0) as well here to ensure + // that both graphs are rendered before starting to check if the rendering + // is correct. + concurrency.delay(0).then(function () { + return concurrency.delay(0); + }).then(function () { + assert.strictEqual(kanban.$('.o_kanban_record:first() .o_graph_barchart').length, 1, + "graph of first record should be a barchart"); + assert.strictEqual(kanban.$('.o_kanban_record:nth(1) .o_graph_linechart').length, 1, + "graph of second record should be a linechart"); + + var evt = document.createEvent("MouseEvents"); //taken ref from https://developer.mozilla.org/en-US/docs/Web/API/MouseEvent/initMouseEvent + evt.initMouseEvent("mouseover", true, true, window, 0, 0, 0, 80, 20, false, false, false, false, 0, null); + $('.discreteBar')[0].dispatchEvent(evt); + var tooltip = $('.nvtooltip').find('table').find('.key')[0].innerText; + assert.equal(tooltip, graph_key, "graph tooltip should be generated "); + $('.nvtooltip').remove(); + + // force a re-rendering of the first record (to check if the + // previous rendered graph is correctly removed from the DOM) + var firstRecordState = kanban.model.get(kanban.handle).data[0]; + return kanban.renderer.updateRecord(firstRecordState); + }).then(function () { + return concurrency.delay(0); // one graph is re-rendered + }).then(function () { + assert.strictEqual(kanban.$('.o_kanban_record:first() svg').length, 1, + "there should be only one rendered graph by record"); + + kanban.destroy(); + done(); + }); }); - - // nvd3 seems to do a setTimeout(0) each time the addGraph function is - // called, which is done twice in this case as there are 2 records. - // for that reason, we need to do two setTimeout(0) as well here to ensure - // that both graphs are rendered before starting to check if the rendering - // is correct. - concurrency.delay(0).then(function () { - return concurrency.delay(0); - }).then(function () { - assert.strictEqual(kanban.$('.o_kanban_record:first() .o_graph_barchart').length, 1, - "graph of first record should be a barchart"); - assert.strictEqual(kanban.$('.o_kanban_record:nth(1) .o_graph_linechart').length, 1, - "graph of second record should be a linechart"); - - var evt = document.createEvent("MouseEvents"); //taken ref from https://developer.mozilla.org/en-US/docs/Web/API/MouseEvent/initMouseEvent - evt.initMouseEvent("mouseover", true, true, window, 0, 0, 0, 80, 20, false, false, false, false, 0, null); - $('.discreteBar')[0].dispatchEvent(evt); - var tooltip = $('.nvtooltip').find('table').find('.key')[0].innerText; - assert.equal(tooltip, graph_key, "graph tooltip should be generated "); - $('.nvtooltip').remove(); - - // force a re-rendering of the first record (to check if the - // previous rendered graph is correctly removed from the DOM) - var firstRecordState = kanban.model.get(kanban.handle).data[0]; - return kanban.renderer.updateRecord(firstRecordState); - }).then(function () { - return concurrency.delay(0); // one graph is re-rendered - }).then(function () { - assert.strictEqual(kanban.$('.o_kanban_record:first() svg').length, 1, - "there should be only one rendered graph by record"); - - kanban.destroy(); - done(); - }); - }); QUnit.test('graph dashboard widget does not need nv to be destroyed', function (assert) { @@ -3690,6 +3690,9 @@ QUnit.module('basic_fields', { '' + '', res_id: 1, + viewOptions: { + disable_autofocus: true, + }, }); assert.strictEqual(form.$('.o_field_widget.o_selection > a span.o_status.o_status_red').length, 1, diff --git a/addons/web/static/tests/views/abstract_view_banner_tests.js b/addons/web/static/tests/views/abstract_view_banner_tests.js new file mode 100644 index 00000000000..21532ed60d9 --- /dev/null +++ b/addons/web/static/tests/views/abstract_view_banner_tests.js @@ -0,0 +1,82 @@ +odoo.define('web.abstract_view_banner_tests', function (require) { +"use strict"; + +var AbstractRenderer = require('web.AbstractRenderer'); +var AbstractView = require('web.AbstractView'); + +var testUtils = require('web.test_utils'); +var createAsyncView = testUtils.createAsyncView; + +var TestRenderer = AbstractRenderer.extend({ + _renderView: function () { + this.$el.addClass('test_content'); + return this._super(); + }, +}); + +var TestView = AbstractView.extend({ + type: 'test', + config: _.extend({}, AbstractView.prototype.config, { + Renderer: TestRenderer + }), +}); + +var test_css_url = '/test_assetsbundle/static/src/css/test_cssfile1.css'; + +QUnit.module('Views', { + beforeEach: function () { + this.data = { + test_model: { + fields: {}, + records: [], + }, + }; + }, + afterEach: function () { + $('head link[href$="' + test_css_url + '"]').remove(); + } + }, function () { + QUnit.module('BasicRenderer'); + + QUnit.test("The banner should be fetched from the route", function (assert) { + var done = assert.async(); + assert.expect(5); + + var banner_html = + '
' + + '' + + '
' + + '
'; + + createAsyncView({ + View: TestView, + model: 'test_model', + data: this.data, + arch: '', + mockRPC: function (route, args) { + if (route === '/module/hello_banner') { + assert.step(route); + return $.when({html: banner_html}); + } + return this._super(route, args); + }, + }).then(function (view) { + var $banner = view.$('.hello_banner'); + assert.strictEqual($banner.length, 1, + "The view should contain the response from the controller."); + assert.verifySteps(['/module/hello_banner'], "The banner should be fetched."); + + var $head_link = $('head link[href$="' + test_css_url + '"]'); + assert.strictEqual($head_link.length, 1, + "The stylesheet should have been added to head."); + + var $banner_link = $('link[href$="' + test_css_url + '"]', $banner); + assert.strictEqual($banner_link.length, 0, + "The stylesheet should have been removed from the banner."); + view.destroy(); + done(); + }); + }); + } +); +}); diff --git a/addons/web/static/tests/views/calendar_mobile_tests.js b/addons/web/static/tests/views/calendar_mobile_tests.js index b8a0353960e..969476b0081 100644 --- a/addons/web/static/tests/views/calendar_mobile_tests.js +++ b/addons/web/static/tests/views/calendar_mobile_tests.js @@ -4,7 +4,7 @@ odoo.define('web.calendar_mobile_tests', function (require) { var CalendarView = require('web.CalendarView'); var testUtils = require('web.test_utils'); -var createView = testUtils.createView; +var createAsyncView = testUtils.createAsyncView; var initialDate = new Date(2016, 11, 12, 8, 0, 0); initialDate = new Date(initialDate.getTime() - initialDate.getTimezoneOffset()*60*1000); @@ -35,8 +35,9 @@ QUnit.module('Views', { QUnit.test('simple calendar rendering in mobile', function (assert) { assert.expect(3); + var done = assert.async(); - var calendar = createView({ + createAsyncView({ View: CalendarView, model: 'event', data: this.data, @@ -46,16 +47,18 @@ QUnit.module('Views', { viewOptions: { initialDate: initialDate, }, + }).then(function (calendar) { + + assert.notOk(calendar.$buttons.find('.o_calendar_button_prev').is(':visible'), + "prev button should be hidden"); + assert.notOk(calendar.$buttons.find('.o_calendar_button_next').is(':visible'), + "next button should be hidden"); + assert.ok($('.o_control_panel .o_cp_pager .o_calendar_button_today').is(':visible'), + "today button should be visible in the pager area (bottom right corner)"); + + calendar.destroy(); + done(); }); - - assert.notOk(calendar.$buttons.find('.o_calendar_button_prev').is(':visible'), - "prev button should be hidden"); - assert.notOk(calendar.$buttons.find('.o_calendar_button_next').is(':visible'), - "next button should be hidden"); - assert.ok($('.o_control_panel .o_cp_pager .o_calendar_button_today').is(':visible'), - "today button should be visible in the pager area (bottom right corner)"); - - calendar.destroy(); }); }); diff --git a/addons/web/static/tests/views/calendar_tests.js b/addons/web/static/tests/views/calendar_tests.js index 2bab1f42437..9a8d0e9f043 100644 --- a/addons/web/static/tests/views/calendar_tests.js +++ b/addons/web/static/tests/views/calendar_tests.js @@ -23,6 +23,7 @@ CalendarRenderer.include({ var createView = testUtils.createView; +var createAsyncView = testUtils.createAsyncView; var initialDate = new Date(2016, 11, 12, 8, 0, 0); initialDate = new Date(initialDate.getTime() - initialDate.getTimezoneOffset()*60*1000); @@ -125,6 +126,7 @@ QUnit.module('Views', { QUnit.test('simple calendar rendering', function (assert) { assert.expect(24); + var done = assert.async(); this.data.event.records.push({ id: 7, @@ -138,7 +140,7 @@ QUnit.module('Views', { type: 1 }); - var calendar = createView({ + createAsyncView({ View: CalendarView, model: 'event', data: this.data, @@ -156,67 +158,69 @@ QUnit.module('Views', { ''+ '', archs: archs, + viewOptions: { initialDate: initialDate, }, + }).then(function (calendar) { + + assert.ok(calendar.$('.o_calendar_view').find('.fc-view-container').length, "should instance of fullcalendar"); + + var $sidebar = calendar.$('.o_calendar_sidebar'); + + assert.strictEqual($sidebar.find('.ui-state-active').text(), "12", "should highlight the target day"); + + // test view scales + + assert.strictEqual(calendar.$('.fc-event').length, 9, "should display 9 events on the week (4 event + 5 days event)"); + assert.strictEqual($sidebar.find('.o_selected_range').length, 7, "week scale should highlight 7 days in mini calendar"); + + calendar.$buttons.find('.o_calendar_button_day').trigger('click'); // display only one day + assert.strictEqual(calendar.$('.fc-event').length, 2, "should display 2 events on the day"); + assert.strictEqual($sidebar.find('.o_selected_range').length, 1, "should highlight the target day in mini calendar"); + + calendar.$buttons.find('.o_calendar_button_month').trigger('click'); // display all the month + assert.strictEqual(calendar.$('.fc-event').length, 7, "should display 7 events on the month (5 events + 2 week event - 1 'event 6' is filtered + 1 'Undefined event')"); + assert.strictEqual($sidebar.find('.o_selected_range').length, 31, "month scale should highlight all days in mini calendar"); + + // test filters + + assert.strictEqual($sidebar.find('.o_calendar_filter').length, 2, "should display 3 filters"); + + var $typeFilter = $sidebar.find('.o_calendar_filter:has(h3:contains(user))'); + assert.ok($typeFilter.length, "should display 'user' filter"); + assert.strictEqual($typeFilter.find('.o_calendar_filter_item').length, 3, "should display 3 filter items for 'user'"); + + // filters which has no value should show with string "Undefined" and should show at the last + assert.strictEqual($typeFilter.find('.o_calendar_filter_item:last').data('value'), false, "filters having false value should be displayed at last in filter items"); + assert.strictEqual($typeFilter.find('.o_calendar_filter_item:last > span').text(), "Undefined", "filters having false value should display 'Undefined' string"); + + var $attendeesFilter = $sidebar.find('.o_calendar_filter:has(h3:contains(attendees))'); + assert.ok($attendeesFilter.length, "should display 'attendees' filter"); + assert.strictEqual($attendeesFilter.find('.o_calendar_filter_item').length, 3, "should display 3 filter items for 'attendees' who use write_model (2 saved + Everything)"); + assert.ok($attendeesFilter.find('.o_field_many2one').length, "should display one2many search bar for 'attendees' filter"); + + assert.strictEqual(calendar.$('.fc-event').length, 7, + "should display 7 events ('event 5' counts for 2 because it spans two weeks and thus generate two fc-event elements)"); + calendar.$('.o_calendar_filter .o_checkbox input').first().click(); // Disable first filter + assert.strictEqual(calendar.$('.fc-event').length, 4, "should now only display 4 event"); + calendar.$('.o_calendar_filter .o_checkbox input').eq(1).click(); // Disable second filter + assert.strictEqual(calendar.$('.fc-event').length, 0, "should not display any event anymore"); + + // test search bar in filter + $sidebar.find('input[type="text"]').trigger('click'); + assert.strictEqual($('ul.ui-autocomplete li:not(.o_m2o_dropdown_option)').length, 2, "should display 2 choices in one2many autocomplete"); // TODO: remove :not(.o_m2o_dropdown_option) because can't have "create & edit" choice + $('ul.ui-autocomplete li:first').trigger('click'); + assert.strictEqual($sidebar.find('.o_calendar_filter:has(h3:contains(attendees)) .o_calendar_filter_item').length, 4, "should display 4 filter items for 'attendees'"); + $sidebar.find('input[type="text"]').trigger('click'); + assert.strictEqual($('ul.ui-autocomplete li:not(.o_m2o_dropdown_option)').text(), "partner 4", "should display the last choice in one2many autocomplete"); // TODO: remove :not(.o_m2o_dropdown_option) because can't have "create & edit" choice + $sidebar.find('.o_calendar_filter_item .o_remove').first().trigger('click'); + assert.ok($('.modal-footer button.btn:contains(Ok)').length, "should display the confirm message"); + $('.modal-footer button.btn:contains(Ok)').trigger('click'); + assert.strictEqual($sidebar.find('.o_calendar_filter:has(h3:contains(attendees)) .o_calendar_filter_item').length, 3, "click on remove then should display 3 filter items for 'attendees'"); + calendar.destroy(); + done(); }); - - assert.ok(calendar.$('.o_calendar_view').find('.fc-view-container').length, "should instance of fullcalendar"); - - var $sidebar = calendar.$('.o_calendar_sidebar'); - - assert.strictEqual($sidebar.find('.ui-state-active').text(), "12", "should highlight the target day"); - - // test view scales - - assert.strictEqual(calendar.$('.fc-event').length, 9, "should display 9 events on the week (4 event + 5 days event)"); - assert.strictEqual($sidebar.find('.o_selected_range').length, 7, "week scale should highlight 7 days in mini calendar"); - - calendar.$buttons.find('.o_calendar_button_day').trigger('click'); // display only one day - assert.strictEqual(calendar.$('.fc-event').length, 2, "should display 2 events on the day"); - assert.strictEqual($sidebar.find('.o_selected_range').length, 1, "should highlight the target day in mini calendar"); - - calendar.$buttons.find('.o_calendar_button_month').trigger('click'); // display all the month - assert.strictEqual(calendar.$('.fc-event').length, 7, "should display 7 events on the month (5 events + 2 week event - 1 'event 6' is filtered + 1 'Undefined event')"); - assert.strictEqual($sidebar.find('.o_selected_range').length, 31, "month scale should highlight all days in mini calendar"); - - // test filters - - assert.strictEqual($sidebar.find('.o_calendar_filter').length, 2, "should display 3 filters"); - - var $typeFilter = $sidebar.find('.o_calendar_filter:has(h3:contains(user))'); - assert.ok($typeFilter.length, "should display 'user' filter"); - assert.strictEqual($typeFilter.find('.o_calendar_filter_item').length, 3, "should display 3 filter items for 'user'"); - - // filters which has no value should show with string "Undefined" and should show at the last - assert.strictEqual($typeFilter.find('.o_calendar_filter_item:last').data('value'), false, "filters having false value should be displayed at last in filter items"); - assert.strictEqual($typeFilter.find('.o_calendar_filter_item:last > span').text(), "Undefined", "filters having false value should display 'Undefined' string"); - - var $attendeesFilter = $sidebar.find('.o_calendar_filter:has(h3:contains(attendees))'); - assert.ok($attendeesFilter.length, "should display 'attendees' filter"); - assert.strictEqual($attendeesFilter.find('.o_calendar_filter_item').length, 3, "should display 3 filter items for 'attendees' who use write_model (2 saved + Everything)"); - assert.ok($attendeesFilter.find('.o_field_many2one').length, "should display one2many search bar for 'attendees' filter"); - - assert.strictEqual(calendar.$('.fc-event').length, 7, - "should display 7 events ('event 5' counts for 2 because it spans two weeks and thus generate two fc-event elements)"); - calendar.$('.o_calendar_filter .o_checkbox input').first().click(); // Disable first filter - assert.strictEqual(calendar.$('.fc-event').length, 4, "should now only display 4 event"); - calendar.$('.o_calendar_filter .o_checkbox input').eq(1).click(); // Disable second filter - assert.strictEqual(calendar.$('.fc-event').length, 0, "should not display any event anymore"); - - // test search bar in filter - - $sidebar.find('input[type="text"]').trigger('click'); - assert.strictEqual($('ul.ui-autocomplete li:not(.o_m2o_dropdown_option)').length, 2, "should display 2 choices in one2many autocomplete"); // TODO: remove :not(.o_m2o_dropdown_option) because can't have "create & edit" choice - $('ul.ui-autocomplete li:first').trigger('click'); - assert.strictEqual($sidebar.find('.o_calendar_filter:has(h3:contains(attendees)) .o_calendar_filter_item').length, 4, "should display 4 filter items for 'attendees'"); - $sidebar.find('input[type="text"]').trigger('click'); - assert.strictEqual($('ul.ui-autocomplete li:not(.o_m2o_dropdown_option)').text(), "partner 4", "should display the last choice in one2many autocomplete"); // TODO: remove :not(.o_m2o_dropdown_option) because can't have "create & edit" choice - $sidebar.find('.o_calendar_filter_item .o_remove').first().trigger('click'); - assert.ok($('.modal-footer button.btn:contains(Ok)').length, "should display the confirm message"); - $('.modal-footer button.btn:contains(Ok)').trigger('click'); - assert.strictEqual($sidebar.find('.o_calendar_filter:has(h3:contains(attendees)) .o_calendar_filter_item').length, 3, "click on remove then should display 3 filter items for 'attendees'"); - calendar.destroy(); }); QUnit.test('breadcrumbs are updated with the displayed period', function (assert) { diff --git a/addons/web/views/webclient_templates.xml b/addons/web/views/webclient_templates.xml index bf123e8dbfa..1ab7d27e26a 100644 --- a/addons/web/views/webclient_templates.xml +++ b/addons/web/views/webclient_templates.xml @@ -52,6 +52,7 @@ + @@ -528,12 +529,14 @@ + + diff --git a/addons/website_sale/__init__.py b/addons/website_sale/__init__.py index 7d34c7c054a..88710707227 100644 --- a/addons/website_sale/__init__.py +++ b/addons/website_sale/__init__.py @@ -3,3 +3,4 @@ from . import controllers from . import models +from . import wizard diff --git a/addons/website_sale/__manifest__.py b/addons/website_sale/__manifest__.py index dd68c7046d5..c4136d11a15 100644 --- a/addons/website_sale/__manifest__.py +++ b/addons/website_sale/__manifest__.py @@ -15,6 +15,7 @@ 'data/digest_data.xml', 'views/product_views.xml', 'views/account_views.xml', + 'views/onboarding_views.xml', 'views/sale_report_views.xml', 'views/sale_order_views.xml', 'views/crm_team_views.xml', diff --git a/addons/website_sale/models/__init__.py b/addons/website_sale/models/__init__.py index 2ed1e1dbe42..ad98431fbaf 100644 --- a/addons/website_sale/models/__init__.py +++ b/addons/website_sale/models/__init__.py @@ -12,3 +12,4 @@ from . import sale_order from . import website from . import res_config_settings from . import digest +from . import res_company diff --git a/addons/website_sale/models/res_company.py b/addons/website_sale/models/res_company.py new file mode 100644 index 00000000000..c82e478ab26 --- /dev/null +++ b/addons/website_sale/models/res_company.py @@ -0,0 +1,17 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from odoo import api, fields, models + + +class ResCompany(models.Model): + _inherit = 'res.company' + + website_sale_onboarding_payment_acquirer_done =\ + fields.Boolean("Website sale onboarding payment acquirer step done", default=False) + + @api.model + def action_open_website_sale_onboarding_payment_acquirer(self): + """ Called by onboarding panel above the quotation list.""" + action = self.env.ref('website_sale.action_open_website_sale_onboarding_payment_acquirer_wizard').read()[0] + return action diff --git a/addons/website_sale/views/onboarding_views.xml b/addons/website_sale/views/onboarding_views.xml new file mode 100644 index 00000000000..6004dfab302 --- /dev/null +++ b/addons/website_sale/views/onboarding_views.xml @@ -0,0 +1,31 @@ + + + + + website.sale.onboarding.payment.acquirer.form + website.sale.payment.acquirer.onboarding.wizard + + primary + 1000 + + + + + + Choose a payment method + ir.actions.act_window + website.sale.payment.acquirer.onboarding.wizard + form + form + + new + + diff --git a/addons/website_sale/wizard/__init__.py b/addons/website_sale/wizard/__init__.py new file mode 100644 index 00000000000..ca76ffc555b --- /dev/null +++ b/addons/website_sale/wizard/__init__.py @@ -0,0 +1,4 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from . import payment_acquirer_onboarding_wizard diff --git a/addons/website_sale/wizard/payment_acquirer_onboarding_wizard.py b/addons/website_sale/wizard/payment_acquirer_onboarding_wizard.py new file mode 100644 index 00000000000..688b740d72e --- /dev/null +++ b/addons/website_sale/wizard/payment_acquirer_onboarding_wizard.py @@ -0,0 +1,13 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from odoo import models + + +class PaymentWizard(models.TransientModel): + _inherit = 'payment.acquirer.onboarding.wizard' + _name = 'website.sale.payment.acquirer.onboarding.wizard' + + def _set_payment_acquirer_onboarding_step_done(self): + """ Override. """ + self.env.user.company_id.website_sale_onboarding_payment_acquirer_done = True diff --git a/addons/website_theme_install/__init__.py b/addons/website_theme_install/__init__.py index 9a7e03eded3..dc5e6b693d1 100644 --- a/addons/website_theme_install/__init__.py +++ b/addons/website_theme_install/__init__.py @@ -1 +1,4 @@ -from . import models \ No newline at end of file +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from . import models diff --git a/addons/website_theme_install/models/__init__.py b/addons/website_theme_install/models/__init__.py index c32871d604e..480a1b847c7 100644 --- a/addons/website_theme_install/models/__init__.py +++ b/addons/website_theme_install/models/__init__.py @@ -1 +1,5 @@ -from . import ir_module_module \ No newline at end of file +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from . import ir_module_module +from . import res_company diff --git a/addons/website_theme_install/models/res_company.py b/addons/website_theme_install/models/res_company.py new file mode 100644 index 00000000000..ce1e3e719a7 --- /dev/null +++ b/addons/website_theme_install/models/res_company.py @@ -0,0 +1,28 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from odoo import api, fields, models +from ast import literal_eval + + +class ResCompany(models.Model): + _inherit = 'res.company' + + def _compute_website_theme_onboarding_done(self): + """ The step is marked as done if one theme is installed. """ + # we need the same domain as the existing action + action = self.env.ref('website_theme_install.theme_install_kanban_action').read()[0] + domain = literal_eval(action['domain']) + domain.append(('state', '=', 'installed')) + installed_themes_count = self.env['ir.module.module'].sudo().search_count(domain) + for record in self: + record.website_theme_onboarding_done = (installed_themes_count > 0) + + website_theme_onboarding_done = fields.Boolean("Onboarding website theme step done", + compute='_compute_website_theme_onboarding_done') + + @api.model + def action_open_website_theme_selector(self): + action = self.env.ref('website_theme_install.theme_install_kanban_action').read()[0] + action['target'] = 'new' + return action diff --git a/addons/website_theme_install/views/views.xml b/addons/website_theme_install/views/views.xml index e78b76a53fb..1522b751936 100644 --- a/addons/website_theme_install/views/views.xml +++ b/addons/website_theme_install/views/views.xml @@ -95,4 +95,15 @@ action = {
See themes here + + diff --git a/doc/reference/views.rst b/doc/reference/views.rst index 08848604926..8a733dedee4 100644 --- a/doc/reference/views.rst +++ b/doc/reference/views.rst @@ -44,6 +44,45 @@ otherwise. ``application`` website feature defining togglable views. By default, views are always applied +``banner_route`` + a route address to be fetched and prepended to the view. + + If this attribute is set, the + :ref:`controller route url` will be fetched and + displayed above the view. The json response from the controller should + contain an "html" key. + + If the html contains a stylesheet tag, it will be + removed and appended to . + + To interact with the backend you can use tags. Please take + a look at the documentation of the _onActionClicked method of + AbstractController (*addons/web/static/src/js/views/abstract_controller.js*) + for more details. + + Only views extending AbstractView and AbstractController can use this + attribute, like :ref:`reference/views/form`, :ref:`reference/views/kanban`, + :ref:`reference/views/list`, ... + + Example: + + .. code-block:: xml + + + + .. code-block:: python + + class MyController(odoo.http.Controller): + @http.route('/module_name/hello', auth='user', type='json') + def hello(self): + return { + 'html': """ +
+ +

hello, world

+
""" + } .. _reference/views/inheritance: diff --git a/odoo/addons/base/__manifest__.py b/odoo/addons/base/__manifest__.py index 27844454c0c..637656cb70a 100644 --- a/odoo/addons/base/__manifest__.py +++ b/odoo/addons/base/__manifest__.py @@ -68,6 +68,7 @@ The kernel of Odoo, needed for all installation. 'views/ir_property_views.xml', 'views/res_config_settings_views.xml', 'views/report_paperformat_views.xml', + 'views/onboarding_views.xml', 'security/ir.model.access.csv', ], 'demo': [ diff --git a/odoo/addons/base/data/res_partner_data.xml b/odoo/addons/base/data/res_partner_data.xml index 7e3830a0bd5..f83a9dceab4 100644 --- a/odoo/addons/base/data/res_partner_data.xml +++ b/odoo/addons/base/data/res_partner_data.xml @@ -12,6 +12,7 @@ info@yourcompany.com www.yourcompany.com + diff --git a/odoo/addons/base/data/res_users_demo.xml b/odoo/addons/base/data/res_users_demo.xml index d5977f63173..6801ef989fe 100644 --- a/odoo/addons/base/data/res_users_demo.xml +++ b/odoo/addons/base/data/res_users_demo.xml @@ -25,7 +25,6 @@ +1 555 123 8069 info@yourcompany.example.com www.example.com - diff --git a/odoo/addons/base/models/res_company.py b/odoo/addons/base/models/res_company.py index e87c850672a..e63f3aeafde 100644 --- a/odoo/addons/base/models/res_company.py +++ b/odoo/addons/base/models/res_company.py @@ -66,6 +66,8 @@ class Company(models.Model): _sql_constraints = [ ('name_uniq', 'unique (name)', 'The company name must be unique !') ] + base_onboarding_company_done = fields.Boolean("Onboarding company step done", + compute="_compute_base_onboarding_company_done") @api.model_cr def init(self): @@ -263,3 +265,16 @@ class Company(models.Model): .report_action(docids)) else: return res + + @api.depends('street') + def _compute_base_onboarding_company_done(self): + """ The company onboarding step is marked as done if street1 is filled. """ + for record in self: + record.base_onboarding_company_done = bool(record.street) + + @api.model + def action_open_base_onboarding_company(self): + """ Onboarding step for company basic information. """ + action = self.env.ref('base.action_open_base_onboarding_company').read()[0] + action['res_id'] = self.env.user.company_id.id + return action diff --git a/odoo/addons/base/rng/tree_view.rng b/odoo/addons/base/rng/tree_view.rng index 068f88d909a..ff758cb7ed3 100644 --- a/odoo/addons/base/rng/tree_view.rng +++ b/odoo/addons/base/rng/tree_view.rng @@ -30,6 +30,7 @@ + diff --git a/odoo/addons/base/static/src/scss/onboarding.scss b/odoo/addons/base/static/src/scss/onboarding.scss new file mode 100644 index 00000000000..56c23c27576 --- /dev/null +++ b/odoo/addons/base/static/src/scss/onboarding.scss @@ -0,0 +1,456 @@ +$o-onboarding-dot-size: 20px; +$o-onboarding-dot-timing: 0.5s; + +$o-onboarding-blue-bg-1: #60ABC3; +$o-onboarding-blue-bg-2: #4187AE; +$o-onboarding-blue-ui-1: #F39D9B; +$o-onboarding-blue-ui-2: #FBEEC8; + +$o-onboarding-orange-bg-1: #DA9D77; +$o-onboarding-orange-bg-2: #B9684F; +$o-onboarding-orange-ui-1: #599FF1; +$o-onboarding-orange-ui-2: #68B3C9; + +$o-onboarding-violet-bg-1: #6070C3; +$o-onboarding-violet-bg-2: #475DA4; +$o-onboarding-violet-ui-1: #F39D9B; +$o-onboarding-violet-ui-2: #FBEEC8; + +.o_onboarding { + position: relative; + overflow: hidden; + background: white center; + background-size: cover; + background-position: center top; + border-bottom: 1px solid $gray-lighter-dark; + + .o_onboarding_wrap { + padding: $o-horizontal-padding*2 $o-horizontal-padding; + } + + .o_onboarding_folded_title { + display: none; + } + + .o_onboarding_btn_fold, .o_onboarding_btn_close { + @include o-position-absolute(auto,0,0); + padding: 6px $o-horizontal-padding + 6px; + cursor: pointer; + } + + .o_onboarding_btn_close { + top:0 + } + + .modal-body, .modal-title { + color: black; + } + + .o_onboarding_steps, .o_onboarding_step { + @include o-flex-display(); + } + + .o_onboarding_steps { + @include o-flex-flow(row, nowrap); + @include o-justify-content(initial); + + @media (max-width: $screen-xs-max) { + @include o-flex-flow(column, nowrap); + + .o_onboarding_folded_title { + border-right: none; + } + } + } + + .o_onboarding_step { + @include o-flex-flow(column, nowrap); + @include o-flex(1, 1, auto); + @include o-align-items(center); + text-align: center; + position: relative; + opacity: 0.6; + + @media (max-width: $screen-xs-max) { + text-align: left; + margin-bottom: 16px; + @include o-align-items(baseline); + + .o_onboarding_step_content { + max-width: none; + } + } + + &:first-of-type { + .o_onboarding_progress { + display: none; + } + + .o_onboarding_dot { + @include transition-delay(0s); + } + } + + .o_onboarding_step_content { + max-width: 230px; + padding: 0 10px; + + .o_onboarding_step_action { + border: 1px solid $gray-lighter; + display: none; + } + + p { + opacity: 0.8; + } + } + + .o_onboarding_progress { + @include o-position-absolute($o-onboarding-dot-size*0.5 - 2px, calc(50% + 10px)); + @include size(calc(100% - 20px), 4px); + background-color: $gray-lighter; + + .progress-bar { + @include o-linear-gradient($deg: 90deg, $start-color: $o-brand-optional, $end-color: $o-brand-primary); + @include o-transition(opacity, $o-onboarding-dot-timing); + } + } + + .o_onboarding_dot { + @include square($o-onboarding-dot-size); + color: transparent; + padding-top: 5px; + font-size: 11px; + display: inline-block; + border-radius: 100%; + background-color: $gray-lighter; + position: relative; + @include o-transition(background, $o-onboarding-dot-timing, ease, $o-onboarding-dot-timing); + } + + .o_onboarding_all_done { + display: none; + } + + // Steps' Status + &.o_onboarding_current, &.o_onboarding_done { + opacity: 1; + + .o_onboarding_progress .progress-bar { + width: 100%; + box-shadow: none; + @include o-transition(width, $o-onboarding-dot-timing); + } + } + + &.o_onboarding_current { + .o_onboarding_step_action, .o_onboarding_dot { + @include o-animation(bounceIn, $duration: $o-onboarding-dot-timing*2); + } + + .o_onboarding_step_action { + display: inline-block; + } + + .o_onboarding_dot { + background: $o-brand-primary; + } + } + + &.o_onboarding_done { + .o_onboarding_dot { + background: $o-brand-optional; + color: white; + } + + .o_onboarding_all_done { + @include o-animation(bounceIn, $duration: $o-onboarding-dot-timing*2); + display: block; + } + + .o_onboarding_step_content_info { + @include o-animation(o_onb_fadeIn, $duration: $o-onboarding-dot-timing*3); + } + + + .o_onboarding_done { + .o_onboarding_dot { + @include transition-delay(0s); + } + + .o_onboarding_progress { + background: $o-brand-optional; + } + + .progress-bar { + opacity: 0; + } + } + } + } + + @keyframes o_onb_fadeIn { + 0% {opacity: 0} + 33% {opacity: 0} + 100% {opacity: 1} + } + + // ========= Transition ========= + &.collapsing { + @include o-transition(all, .25s); + min-height: 30px; + } + + // ========= Folded Design ========= + // ====================================== + + &.collapse:not(.in) { + min-height: 30px; + background-image: none!important; + display: block; + + .o_onboarding_hide_folded { + display: none!important; + } + + .o_onboarding_btn_fold { + top: 0; + left: 0; + @include o-transform(scaleY(-1)); + } + + .o_onboarding_wrap { + padding: 0 $o-horizontal-padding; + + .o_onboarding_steps { + padding: 0; + @include o-justify-content(baseline); + } + + .o_onboarding_folded_title { + padding: 6px 10px 6px 0; + + @media (min-width: $screen-sm-min) { + border-right: 1px solid $gray-lighter-dark; + } + } + + .o_onboarding_folded_title, .o_onboarding_step { + @include o-inline-flex-display(); + } + + .o_onboarding_step { + @include o-align-items(flex-start); + @include o-flex(0,1,auto); + padding-right: 12px; + + &:not(:last-of-type) { + &:before, &:after { + content: " "; + @include o-position-absolute(0, 0); + + border-top: floor($o-statusbar-height/2) solid transparent; + border-bottom: ceil($o-statusbar-height/2) solid transparent; + border-right: none; + border-left: $o-statusbar-arrow-width solid $o-view-background-color; + -moz-transform: scale(0.9999); // Smooth the triangle on firefox + } + + &:before { + right: -1px; + border-left-color: $gray-lighter-dark; + } + } + + .o_onboarding_step_content { + padding: 0; + @include o-flex-display(); + } + + .o_onboarding_step_content_info .o_onboarding_step_title { + margin: 7px!important; + font-size: $font-size-base; + } + + .o_onboarding_all_done { + margin: 6px 10px 0 -4px!important; + + > i:before { + content: "\f00c"; + } + } + } + } + } + + // ========= Color Variations ========= + // ====================================== + + // ========= Variation Mixin ========== + @mixin o-onboarding-variation($bg1, $bg2, $ui1, $ui2) { + &, .o_onboarding_btn_fold, .o_onboarding_step_title, .o_onboarding_step_action, a.o_onboarding_all_done, .o_onboarding_all_done > .fa, .o_onboarding_btn_close { + color: white; + } + + .o_onboarding_wrap { + @include o-vertical-gradient(fade-out($bg1, 0.5), $bg2); + box-shadow: inset 0 -7px 20px -5px fade-out(black, 0.7); + } + + .o_onboarding_dot, .o_onboarding_progress { + background-color: fade-out(white, 0.5); + } + + .o_onboarding_step_action { + border-color: fade-out(white, 0.5); + } + + .progress-bar { + @include o-linear-gradient($deg: 90deg, $start-color: $ui1, $end-color: $ui2); + } + + .o_onboarding_step { + &.o_onboarding_done, .o_onboarding_done + .o_onboarding_done { + .o_onboarding_dot, .o_onboarding_progress { + background: $ui1; + } + } + + &.o_onboarding_current .o_onboarding_dot { + background: $ui2; + } + } + } + + // ========= Variation Templates ========== + &.o_onboarding_blue.collapse.in, &.o_onboarding_blue.collapsing { + @include o-onboarding-variation($o-onboarding-blue-bg-1, $o-onboarding-blue-bg-2, $o-onboarding-blue-ui-1, $o-onboarding-blue-ui-2); + } + + &.o_onboarding_orange.collapse.in, &.o_onboarding_orange.collapsing { + @include o-onboarding-variation($o-onboarding-orange-bg-1, $o-onboarding-orange-bg-2, $o-onboarding-orange-ui-1, $o-onboarding-orange-ui-2); + } + + &.o_onboarding_violet.collapse.in, &.o_onboarding_violet.collapsing { + @include o-onboarding-variation($o-onboarding-violet-bg-1, $o-onboarding-violet-bg-2, $o-onboarding-violet-ui-1, $o-onboarding-violet-ui-2); + } + + // ========= "Cannon Confetti" animation ========== + .o_onboarding_confetti { + @include o-animation(cannon-explosion, $o-onboarding-dot-timing*3, ease-out, 0s, 1, normal, both); + @include o-position-absolute(0, 50%, 30px); + @include o-transform-origin(50%, 100%); + display: none; + width: 1px; + + @keyframes cannon-explosion { + 0% { + @include o-transform(scale(0)); + } + 70% { + opacity: 1; + } + 100% { + opacity: 0; + } + } + + > div { + @include o-position-absolute(auto, 0, 5px,0); + @include o-transform-origin(50%, 100%); + @include o-flex-display(); + @include o-flex-flow(column, nowrap); + @include o-justify-content(space-around); + @include o-align-content(center); + padding-bottom: 30%; + + &:nth-child(1), &:nth-child(5) { + height: 90%; + @include o-transform(rotate(-20deg)); + } + &:nth-child(2), &:nth-child(4) { + height: 95%; + @include o-transform(rotate(-10deg)); + } + &:nth-child(3) { + height: 100%; + } + &:nth-child(4) { + @include o-transform(rotate(10deg)); + } + &:nth-child(5) { + @include o-transform(rotate(20deg)); + } + + > div { + @include o-flex(0,1,auto); + @include o-animation(confetti-rotate-l, $o-onboarding-dot-timing*3, ease-out, 0s, 1, normal, both); + + &:nth-child(odd) { + @include o-animation(confetti-rotate-r, $o-onboarding-dot-timing*3, ease-out, 0s, 1, normal, both); + } + + @keyframes confetti-rotate-l { + 0% { + @include o-transform(rotate(0) scaleY(1)); + } + 50% { + @include o-transform(rotate(2turn) scaleY(1.5)); + opacity: 1; + } + 100% { + @include o-transform(rotate(2.05turn) scaleY(1)); + opacity: 0; + } + } + + @keyframes confetti-rotate-r { + 0% { + @include o-transform(rotate(0) scaleY(1)); + } + 50% { + @include o-transform(rotate(-2turn) scaleY(1.5)); + opacity: 1; + } + 100% { + @include o-transform(rotate(-2.05turn) scaleY(1)); + opacity: 0; + } + } + + &.ribbon { + @include size(.3em, .7em); + border-radius: .1em; + } + + &.flake { + @include square(.3em); + border-radius: 50%; + } + + // Confetti Colors + &.color-1 { + background-color: #feb535; + } + + &.color-2 { + background-color: #bea4ff; + } + + &.color-3 { + background-color: #ff6e83; + } + + &.color-4 { + background-color: #58cafe; + } + } + } + } + + .o_onboarding_done { + // Activate confetti cannon when step is done + .o_onboarding_confetti { + display: block; + } + } +} \ No newline at end of file diff --git a/odoo/addons/base/views/onboarding_views.xml b/odoo/addons/base/views/onboarding_views.xml new file mode 100644 index 00000000000..f8bca87b31f --- /dev/null +++ b/odoo/addons/base/views/onboarding_views.xml @@ -0,0 +1,177 @@ + + + + + + + + + + + base.company.onboarding.form + res.company + + primary + 1000 + + + {'invisible': 1} + + +
+
+
+
+
+
+ + Set your company data + ir.actions.act_window + res.company + form + form + + new + +
diff --git a/odoo/addons/base/views/res_company_views.xml b/odoo/addons/base/views/res_company_views.xml index 90e4a0b2d97..e6a76289242 100644 --- a/odoo/addons/base/views/res_company_views.xml +++ b/odoo/addons/base/views/res_company_views.xml @@ -28,7 +28,6 @@
- @@ -39,7 +38,6 @@ - @@ -182,6 +180,55 @@ - + + res.company.document.template.form + res.company + primary + 2048 + +
+ + + + + + + +
+
+