From 7c6bc684a9f4260b5b3e71febef978b2cadd9b67 Mon Sep 17 00:00:00 2001 From: eco-odoo Date: Thu, 31 May 2018 10:22:17 +0200 Subject: [PATCH 01/10] [IMP] base: some cleaning in general settings Use a new view to choose the document template. Remove layout fields from base company form. Reword Edit Header -> Edit Layout Set a default logo for a new company in data instead of demo data task: 60668 --- .../base_setup/models/res_config_settings.py | 2 +- .../views/res_config_settings_views.xml | 2 +- addons/web/static/src/scss/form_view.scss | 16 ++++++ odoo/addons/base/data/res_partner_data.xml | 1 + odoo/addons/base/data/res_users_demo.xml | 1 - odoo/addons/base/views/res_company_views.xml | 53 +++++++++++++++++-- 6 files changed, 69 insertions(+), 6 deletions(-) diff --git a/addons/base_setup/models/res_config_settings.py b/addons/base_setup/models/res_config_settings.py index ef42b700bb4..a2d4c5a4511 100644 --- a/addons/base_setup/models/res_config_settings.py +++ b/addons/base_setup/models/res_config_settings.py @@ -94,7 +94,7 @@ class ResConfigSettings(models.TransientModel): @api.multi def change_report_template(self): self.ensure_one() - template = self.env.ref('base.view_company_report_form') + template = self.env.ref('base.view_company_document_template_form') return { 'name': _('Choose Your Document Layout'), 'type': 'ir.actions.act_window', diff --git a/addons/base_setup/views/res_config_settings_views.xml b/addons/base_setup/views/res_config_settings_views.xml index 1a418fd251c..09be8139c9d 100644 --- a/addons/base_setup/views/res_config_settings_views.xml +++ b/addons/base_setup/views/res_config_settings_views.xml @@ -40,7 +40,7 @@
diff --git a/addons/web/static/src/scss/form_view.scss b/addons/web/static/src/scss/form_view.scss index ef4fb223b9a..f46de22e9db 100644 --- a/addons/web/static/src/scss/form_view.scss +++ b/addons/web/static/src/scss/form_view.scss @@ -603,6 +603,22 @@ margin-top: 32px !important; } } + &.o_company_document_layout { + .image_selection_container { + div { + width: 50%; + img { + margin-left: 0 !important; + } + } + } + td { + padding: 0 45px 0 0; + } + img[name="logo"] { + max-height: 300px; + } + } } // Overriden style when form view in modal 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/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 + +
+ + + + + + +
+
+
+
+
From 0dd6a0b2bf0be0698d21e039ad4ead33133d8267 Mon Sep 17 00:00:00 2001 From: eco-odoo Date: Tue, 3 Jul 2018 10:24:57 +0200 Subject: [PATCH 02/10] [IMP] web: core: ajax: loadCSS now returns a promise It is useful to know when the stylesheet has been loaded in order to prevent displaying unstyled content. CalendarView and the dashboard_graph widget (JournalDashboardGraph) use loadCSS through cssLibs / loadLibs so the tests now have to use createAsyncView instead of createView. --- .../static/tests/google_calendar_tests.js | 38 +++--- addons/web/static/src/js/core/ajax.js | 31 ++++- addons/web/static/tests/core/ajax_tests.js | 35 +++++ .../static/tests/fields/basic_fields_tests.js | 72 +++++------ .../tests/views/calendar_mobile_tests.js | 25 ++-- .../web/static/tests/views/calendar_tests.js | 120 +++++++++--------- addons/web/views/webclient_templates.xml | 1 + 7 files changed, 193 insertions(+), 129 deletions(-) create mode 100644 addons/web/static/tests/core/ajax_tests.js diff --git a/addons/google_calendar/static/tests/google_calendar_tests.js b/addons/google_calendar/static/tests/google_calendar_tests.js index 6edd786afd0..bba2286c163 100644 --- a/addons/google_calendar/static/tests/google_calendar_tests.js +++ b/addons/google_calendar/static/tests/google_calendar_tests.js @@ -4,7 +4,7 @@ odoo.define('google_calendar.calendar_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-12-12T08:00:00Z"); @@ -71,8 +71,9 @@ QUnit.module('Google Calendar', { QUnit.test('sync google calendar', function (assert) { assert.expect(6); + var done = assert.async(); - var calendar = createView({ + createAsyncView({ View: CalendarView, model: 'calendar.event', data: this.data, @@ -98,23 +99,24 @@ QUnit.module('Google Calendar', { } return this._super.apply(this, arguments); }, + }).then(function (calendar) { + assert.strictEqual(calendar.$('.fc-event').length, 2, "should display 2 events on the month"); + + var $sidebar = calendar.$('.o_calendar_sidebar'); + + calendar.$('.o_google_sync_button').click(); + + assert.verifySteps([ + '/web/dataset/call_kw/calendar.event/search_read', + '/google_calendar/sync_data', + '/web/dataset/call_kw/calendar.event/search_read', + ], 'should do a search_read before and after the call to sync_data'); + + assert.strictEqual(calendar.$('.fc-event').length, 3, "should now display 3 events on the month"); + + calendar.destroy(); + done(); }); - - assert.strictEqual(calendar.$('.fc-event').length, 2, "should display 2 events on the month"); - - var $sidebar = calendar.$('.o_calendar_sidebar'); - - calendar.$('.o_google_sync_button').click(); - - assert.verifySteps([ - '/web/dataset/call_kw/calendar.event/search_read', - '/google_calendar/sync_data', - '/web/dataset/call_kw/calendar.event/search_read', - ], 'should do a search_read before and after the call to sync_data'); - - assert.strictEqual(calendar.$('.fc-event').length, 3, "should now display 3 events on the month"); - - calendar.destroy(); }); }); }); diff --git a/addons/web/static/src/js/core/ajax.js b/addons/web/static/src/js/core/ajax.js index 81643e99430..35869b1949d 100644 --- a/addons/web/static/src/js/core/ajax.js +++ b/addons/web/static/src/js/core/ajax.js @@ -194,15 +194,34 @@ function realSetTimeout (fct, millis) { setTimeout(wait, millis); } -function loadCSS(url) { - if (!$('link[href="' + url + '"]').length) { - $('head').append($('', { +/** + * Load css asynchronously: fetch it from the url parameter and add a link tag + * to . + * If the url has already been requested and loaded, the promise will resolve immediately. + * + * @param {String} url of the css to be fetched + * @returns {Deferred} resolved when the css has been loaded. + */ +var loadCSS = (function () { + var urlDefs = Object.create(null); + + return function loadCSS(url) { + if (url in urlDefs) { + return urlDefs[url]; + } + var $link = $('', { 'href': url, 'rel': 'stylesheet', 'type': 'text/css' - })); - } -} + }); + urlDefs[url] = $.Deferred(); + $link.on('load', function () { + urlDefs[url].resolve(); + }); + $('head').append($link); + return urlDefs[url]; + }; +})(); var loadJS = (function () { var urls = []; diff --git a/addons/web/static/tests/core/ajax_tests.js b/addons/web/static/tests/core/ajax_tests.js new file mode 100644 index 00000000000..f58d7368d45 --- /dev/null +++ b/addons/web/static/tests/core/ajax_tests.js @@ -0,0 +1,35 @@ +odoo.define('web.ajax_tests', function (require) { +"use strict"; + +var ajax = require('web.ajax'); + +QUnit.module('core', function () { + + var test_css_url = '/test_assetsbundle/static/src/css/test_cssfile1.css'; + var test_link_selector = 'link[href="' + test_css_url + '"]'; + + QUnit.module('ajax', { + beforeEach: function () { + $(test_link_selector).remove(); + }, + afterEach: function () { + $(test_link_selector).remove(); + } + }); + + QUnit.test('loadCSS', function (assert) { + var done = assert.async(); + assert.expect(2); + ajax.loadCSS(test_css_url).then(function () { + var $links = $(test_link_selector); + assert.strictEqual($links.length, 1, "The css should be added to the dom."); + ajax.loadCSS(test_css_url).then(function () { + var $links = $(test_link_selector); + assert.strictEqual($links.length, 1, "The css should have been added only once."); + done(); + }); + }); + }); +}); + +}); diff --git a/addons/web/static/tests/fields/basic_fields_tests.js b/addons/web/static/tests/fields/basic_fields_tests.js index 98775ebab3c..8ced59a4686 100644 --- a/addons/web/static/tests/fields/basic_fields_tests.js +++ b/addons/web/static/tests/fields/basic_fields_tests.js @@ -12,6 +12,7 @@ var session = require('web.session'); var testUtils = require('web.test_utils'); var createView = testUtils.createView; +var createAsyncView = testUtils.createAsyncView; var DebouncedField = basicFields.DebouncedField; var _t = core._t; @@ -1957,7 +1958,7 @@ QUnit.module('basic_fields', { assert.expect(4); var graph_key = JSON.parse(this.data.partner.records[0].graph_data)[0].key; - var kanban = createView({ + createAsyncView({ View: KanbanView, model: 'partner', data: this.data, @@ -1970,42 +1971,41 @@ QUnit.module('basic_fields', { '' + '', 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) { 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..d57bad4e2e9 100644 --- a/addons/web/views/webclient_templates.xml +++ b/addons/web/views/webclient_templates.xml @@ -534,6 +534,7 @@ + From c1498ff36be4ce0b175f340e2f5690a37421ccd7 Mon Sep 17 00:00:00 2001 From: eco-odoo Date: Tue, 3 Jul 2018 10:28:44 +0200 Subject: [PATCH 03/10] [IMP] web: add option to display a banner above a view Add a banner_route attribute to AbstractView to allow fetching html from a controller route and displaying it above the view. The initial use case was the onboarding panels. Supported views are the ones inheriting both AbstractView and AbstractController. --- .../src/js/views/abstract_controller.js | 84 ++++++++++++++++++- .../web/static/src/js/views/abstract_view.js | 1 + addons/web/static/src/scss/banner.scss | 5 ++ .../tests/views/abstract_view_banner_tests.js | 82 ++++++++++++++++++ addons/web/views/webclient_templates.xml | 2 + doc/reference/views.rst | 39 +++++++++ odoo/addons/base/rng/tree_view.rng | 1 + 7 files changed, 210 insertions(+), 4 deletions(-) create mode 100644 addons/web/static/src/scss/banner.scss create mode 100644 addons/web/static/tests/views/abstract_view_banner_tests.js diff --git a/addons/web/static/src/js/views/abstract_controller.js b/addons/web/static/src/js/views/abstract_controller.js index bf4f3a616f3..f4f323b01ba 100644 --- a/addons/web/static/src/js/views/abstract_controller.js +++ b/addons/web/static/src/js/views/abstract_controller.js @@ -12,6 +12,7 @@ odoo.define('web.AbstractController', function (require) { * reading localstorage, ...) has to go through the controller. */ +var ajax = require('web.ajax'); var concurrency = require('web.concurrency'); var ControlPanelMixin = require('web.ControlPanelMixin'); var core = require('web.core'); @@ -54,7 +55,7 @@ var AbstractController = AbstractAction.extend(ControlPanelMixin, { this.activeActions = params.activeActions; this.controllerID = params.controllerID; this.initialState = params.initialState; - + this.bannerRoute = params.bannerRoute; // use a DropPrevious to correctly handle concurrent updates this.dp = new concurrency.DropPrevious(); // those arguments are temporary, they won't be necessary as soon as the @@ -280,6 +281,54 @@ var AbstractController = AbstractAction.extend(ControlPanelMixin, { state: state || {}, }); }, + /** + * Renders the html provided by the route specified by the + * bannerRoute attribute on the controller (banner_route in the template). + * Renders it before the view output and add a css class 'o_has_banner' to it. + * There can be only one banner displayed at a time. + * + * If the banner contains stylesheet links, they are moved to + * (and will only be fetched once). + * + * Route example: + * @http.route('/module/hello', auth='user', type='json') + * def hello(self): + * return {'html': '

hello, world

'} + * + * @private + * @returns {Deferred} + */ + _renderBanner: function () { + if (this.bannerRoute !== undefined) { + var self = this; + return this.dp + .add(this._rpc({route: this.bannerRoute})) + .then(function (response) { + if (!response.html) { + self.$el.removeClass('o_has_banner'); + return $.when(); + } + self.$el.addClass('o_has_banner'); + var $banner = $(response.html); + // we should only display one banner at a time + if (self._$banner && self._$banner.remove) { + self._$banner.remove(); + } + // Stylesheets are moved to and we wait for them to be loaded + // to prevent displaying unstyled content. + var defs = []; + $('link[rel="stylesheet"]', $banner).each(function (i, link) { + defs.push(ajax.loadCSS(link.href)); + link.remove(); + }); + return $.when.apply($, defs).then(function () { + $banner.prependTo(self.$el); + self._$banner = $banner; + }); + }); + } + return $.when(); + }, /** * Renders the control elements (buttons, pager and sidebar) of the current * view. @@ -370,7 +419,7 @@ var AbstractController = AbstractAction.extend(ControlPanelMixin, { }); this._pushState(); - return $.when(); + return this._renderBanner(); }, //-------------------------------------------------------------------------- @@ -405,12 +454,39 @@ var AbstractController = AbstractAction.extend(ControlPanelMixin, { * When a user clicks on an link with type="action", we need to actually * do the action. This kind of links is used a lot in no-content helpers. * + * The may have + * - a data-method and data-model attribute, in that case the corresponding + * rpc will be called. If that rpc returns an action it will be executed. + * - a data-reload-on-close attribute, in that case the view will be + * reloaded after the dialog has been closed. + * * @private * @param {OdooEvent} event */ _onActionClicked: function (event) { - event.preventDefault(); - this.do_action(event.currentTarget.name); + var $target = $(event.currentTarget); + var self = this; + var model = $target.data('model'); + var method = $target.data('method'); + + if (method !== undefined && model !== undefined) { + var options = {}; + if ($target.data('reload-on-close')) { + options.on_close = function () { + self.trigger_up('reload'); + }; + } + this.dp.add(this._rpc({ + model: model, + method: method, + })).then(function (action) { + if (action !== undefined) { + self.do_action(action, options); + } + }); + } else { + this.do_action($target.attr('name')); + } }, /** * Intercepts the 'switch_view' event to add the controllerID into the data, diff --git a/addons/web/static/src/js/views/abstract_view.js b/addons/web/static/src/js/views/abstract_view.js index fe95ada0a7f..7b1d21e6ea5 100644 --- a/addons/web/static/src/js/views/abstract_view.js +++ b/addons/web/static/src/js/views/abstract_view.js @@ -121,6 +121,7 @@ var AbstractView = Class.extend({ groupable: this.groupable, isEmbedded: this.isEmbedded, controllerID: params.controllerID, + bannerRoute: this.arch.attrs.banner_route, }; // AAB: these params won't be necessary as soon as the ControlPanel will // be instantiated by the View diff --git a/addons/web/static/src/scss/banner.scss b/addons/web/static/src/scss/banner.scss new file mode 100644 index 00000000000..b119d9ab273 --- /dev/null +++ b/addons/web/static/src/scss/banner.scss @@ -0,0 +1,5 @@ +.o_has_banner { + .o_view_nocontent { + top: 30%; + } +} 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/views/webclient_templates.xml b/addons/web/views/webclient_templates.xml index d57bad4e2e9..1ab7d27e26a 100644 --- a/addons/web/views/webclient_templates.xml +++ b/addons/web/views/webclient_templates.xml @@ -52,6 +52,7 @@ + @@ -528,6 +529,7 @@ + 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/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 @@ + From 872cf9160da2f260879b76eb9bd400a81f3e13a1 Mon Sep 17 00:00:00 2001 From: eco-odoo Date: Wed, 11 Jul 2018 10:40:16 +0200 Subject: [PATCH 04/10] [FIX] web: controller update now waits for _update to resolve Before this, the deffered returned by _update was not taken into account. --- addons/web/static/src/js/views/abstract_controller.js | 4 ++-- addons/web/static/tests/fields/basic_fields_tests.js | 3 +++ 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/addons/web/static/src/js/views/abstract_controller.js b/addons/web/static/src/js/views/abstract_controller.js index f4f323b01ba..155b1792dc6 100644 --- a/addons/web/static/src/js/views/abstract_controller.js +++ b/addons/web/static/src/js/views/abstract_controller.js @@ -91,7 +91,7 @@ var AbstractController = AbstractAction.extend(ControlPanelMixin, { this._super.apply(this, arguments), this.renderer.appendTo(this.$el) ).then(function () { - self._update(self.initialState); + return self._update(self.initialState); }); }, /** @@ -257,7 +257,7 @@ var AbstractController = AbstractAction.extend(ControlPanelMixin, { return; } self.renderer.setLocalState(localState); - self._update(state); + return self._update(state); }); }); }, diff --git a/addons/web/static/tests/fields/basic_fields_tests.js b/addons/web/static/tests/fields/basic_fields_tests.js index 8ced59a4686..e23483b4276 100644 --- a/addons/web/static/tests/fields/basic_fields_tests.js +++ b/addons/web/static/tests/fields/basic_fields_tests.js @@ -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, From 9744fecd058c707687a743c40fa4748f182a6275 Mon Sep 17 00:00:00 2001 From: eco-odoo Date: Fri, 25 May 2018 15:21:41 +0200 Subject: [PATCH 05/10] [IMP] base: add support for onboarding Add a base template file, html and stylesheet for the onboarding banners. Add a step for company onboarding. task: 60668 --- odoo/addons/base/__manifest__.py | 1 + odoo/addons/base/models/res_company.py | 15 + .../base/static/src/scss/onboarding.scss | 456 ++++++++++++++++++ odoo/addons/base/views/onboarding_views.xml | 177 +++++++ 4 files changed, 649 insertions(+) create mode 100644 odoo/addons/base/static/src/scss/onboarding.scss create mode 100644 odoo/addons/base/views/onboarding_views.xml 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/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/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 + +
From d1ec3306b8a75f8b961192ef5cf39b6f454f3792 Mon Sep 17 00:00:00 2001 From: eco-odoo Date: Mon, 28 May 2018 12:35:54 +0200 Subject: [PATCH 06/10] [ADD] account: onboarding panel for invoicing Add an onboarding panel for invoicing, it appears above the invoice list. After going through the steps the system should be all set to send real invoices. In the payment module, add a payment acquirer step and add it to the invoice onboarding panel. task: 60668 --- addons/account/__manifest__.py | 1 + addons/account/controllers/__init__.py | 1 + addons/account/controllers/onboarding.py | 21 +++ addons/account/models/account_invoice.py | 1 + addons/account/models/company.py | 112 ++++++++++++++- .../src/img/account_invoice_onboarding_bg.jpg | Bin 0 -> 23728 bytes addons/account/views/account_invoice_view.xml | 15 +- .../views/account_onboarding_templates.xml | 82 +++++++++++ .../mail/wizard/mail_compose_message_view.xml | 2 +- addons/payment/__init__.py | 3 +- addons/payment/__manifest__.py | 1 + addons/payment/models/__init__.py | 1 + addons/payment/models/res_company.py | 15 ++ .../payment_acquirer_onboarding_templates.xml | 135 ++++++++++++++++++ addons/payment/wizards/__init__.py | 1 + .../payment_acquirer_onboarding_wizard.py | 127 ++++++++++++++++ 16 files changed, 513 insertions(+), 5 deletions(-) create mode 100644 addons/account/controllers/onboarding.py create mode 100644 addons/account/static/src/img/account_invoice_onboarding_bg.jpg create mode 100644 addons/account/views/account_onboarding_templates.xml create mode 100644 addons/payment/models/res_company.py create mode 100644 addons/payment/views/payment_acquirer_onboarding_templates.xml create mode 100644 addons/payment/wizards/__init__.py create mode 100644 addons/payment/wizards/payment_acquirer_onboarding_wizard.py diff --git a/addons/account/__manifest__.py b/addons/account/__manifest__.py index 9654e47f01e..c65eed84e2f 100644 --- a/addons/account/__manifest__.py +++ b/addons/account/__manifest__.py @@ -63,6 +63,7 @@ Core mechanisms for the accounting modules. To display the menuitems, install th '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..ee51395add8 --- /dev/null +++ b/addons/account/controllers/onboarding.py @@ -0,0 +1,21 @@ +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, + }) + } 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..1206cd9ccd9 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,19 @@ 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) + @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 +411,100 @@ 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_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_invoice_onboarding_bg.jpg b/addons/account/static/src/img/account_invoice_onboarding_bg.jpg new file mode 100644 index 0000000000000000000000000000000000000000..7b7e1a524d5d4144381576c7ff6f486ee6189848 GIT binary patch literal 23728 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 literal 0 HcmV?d00001 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_onboarding_templates.xml b/addons/account/views/account_onboarding_templates.xml new file mode 100644 index 00000000000..f094a8a5bb9 --- /dev/null +++ b/addons/account/views/account_onboarding_templates.xml @@ -0,0 +1,82 @@ + + + + + + + + + + Configure your document template + ir.actions.act_window + res.company + form + form + + new + + + + mail.compose.message.form.account_invoice_onboarding_sample_invoice + + mail.compose.message + + 1000 + primary + + + 1 + + + 1 + + + 1 + + + + + Send a sample invoice + ir.actions.act_window + mail.compose.message + form + form + + new + + diff --git a/addons/mail/wizard/mail_compose_message_view.xml b/addons/mail/wizard/mail_compose_message_view.xml index 5d3a7a910dd..9c349c36bc2 100644 --- a/addons/mail/wizard/mail_compose_message_view.xml +++ b/addons/mail/wizard/mail_compose_message_view.xml @@ -52,7 +52,7 @@ the selected records the current search filter. - Followers of the document and + Followers of the document and diff --git a/addons/payment/__init__.py b/addons/payment/__init__.py index b75bfd8cfe4..4a05b41804c 100644 --- a/addons/payment/__init__.py +++ b/addons/payment/__init__.py @@ -2,4 +2,5 @@ # Part of Odoo. See LICENSE file for full copyright and licensing details. from . import models -from . import controllers \ No newline at end of file +from . import controllers +from . import wizards diff --git a/addons/payment/__manifest__.py b/addons/payment/__manifest__.py index 182e3667fc7..2df0e234ab5 100644 --- a/addons/payment/__manifest__.py +++ b/addons/payment/__manifest__.py @@ -14,6 +14,7 @@ 'views/payment_views.xml', 'views/account_payment_views.xml', 'views/account_invoice_views.xml', + 'views/payment_acquirer_onboarding_templates.xml', 'views/payment_templates.xml', 'views/payment_portal_templates.xml', 'views/assets.xml', diff --git a/addons/payment/models/__init__.py b/addons/payment/models/__init__.py index 9758f10d9c9..93f5083aca8 100644 --- a/addons/payment/models/__init__.py +++ b/addons/payment/models/__init__.py @@ -5,3 +5,4 @@ from . import account_invoice from . import res_partner from . import account_payment from . import chart_template +from . import res_company diff --git a/addons/payment/models/res_company.py b/addons/payment/models/res_company.py new file mode 100644 index 00000000000..c2b52486563 --- /dev/null +++ b/addons/payment/models/res_company.py @@ -0,0 +1,15 @@ +from odoo import _, api, fields, models + + +class ResCompany(models.Model): + _inherit = 'res.company' + + # Step is marked as done if one payment module is installed or + # `custom` payment has been changed or `skip this step` has been clicked + payment_acquirer_onboarding_done = fields.Boolean('Payment acquirer onboarding done', default=False) + + @api.model + def action_open_payment_onboarding_payment_acquirer(self): + """ Called by onboarding panel above the customer invoice list.""" + action = self.env.ref('payment.action_open_payment_onboarding_payment_acquirer_wizard').read()[0] + return action diff --git a/addons/payment/views/payment_acquirer_onboarding_templates.xml b/addons/payment/views/payment_acquirer_onboarding_templates.xml new file mode 100644 index 00000000000..f2d0274a11c --- /dev/null +++ b/addons/payment/views/payment_acquirer_onboarding_templates.xml @@ -0,0 +1,135 @@ + + + + + + + + + payment.acquirer.onboarding.wizard.form + payment.acquirer.onboarding.wizard + +
+ +
+
+ +
+
+
+
+
+
+ PayPal + is the easiest way to accept payments. It's easy to configure and it offers + + a transparent fee structure. + + + + + + + +
+
+
+
+ +
+
+
+
+
+
+ Stripe + is a payment provider with + + a simple fee structure and no hidden costs. + + No other gateway or merchant account is required. We recommend this solution + to manage credit card payments. + + + + + Get my Stripe keys + + + +
+
+
+
+ +
+
+
+
+
+
+ Provide customers with instructions to pay outside of your platform (wire + transfer, cash on delivery, etc.). + + + + +
+
+
+
+ You can skip this step if invoices are pre-paid. +
+
+
+ + + + Choose a payment method + ir.actions.act_window + payment.acquirer.onboarding.wizard + form + form + + new + + diff --git a/addons/payment/wizards/__init__.py b/addons/payment/wizards/__init__.py new file mode 100644 index 00000000000..84b24dc0131 --- /dev/null +++ b/addons/payment/wizards/__init__.py @@ -0,0 +1 @@ +from . import payment_acquirer_onboarding_wizard diff --git a/addons/payment/wizards/payment_acquirer_onboarding_wizard.py b/addons/payment/wizards/payment_acquirer_onboarding_wizard.py new file mode 100644 index 00000000000..679facc45a9 --- /dev/null +++ b/addons/payment/wizards/payment_acquirer_onboarding_wizard.py @@ -0,0 +1,127 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from odoo import api, models, fields, _ +from odoo.exceptions import UserError + + +class PaymentWizard(models.TransientModel): + _name = 'payment.acquirer.onboarding.wizard' + + paypal_checked = fields.Boolean('Paypal', default=lambda self: self._get_default_payment_acquirer_onboarding_value('paypal_checked')) + paypal_email_account = fields.Char('Paypal Email ID', default=lambda self: self._get_default_payment_acquirer_onboarding_value('paypal_email_account')) + paypal_seller_account = fields.Char('Paypal Merchant ID', default=lambda self: self._get_default_payment_acquirer_onboarding_value('paypal_seller_account')) + paypal_pdt_token = fields.Char('Paypal PDT Token', default=lambda self: self._get_default_payment_acquirer_onboarding_value('paypal_pdt_token')) + + stripe_checked = fields.Boolean('Credit Card (via Stripe)', default=lambda self: self._get_default_payment_acquirer_onboarding_value('stripe_checked')) + stripe_secret_key = fields.Char(default=lambda self: self._get_default_payment_acquirer_onboarding_value('stripe_secret_key')) + stripe_publishable_key = fields.Char(default=lambda self: self._get_default_payment_acquirer_onboarding_value('stripe_publishable_key')) + + manual_checked = fields.Boolean('Manual Payment', default=lambda self: self._get_default_payment_acquirer_onboarding_value('manual_checked')) + manual_name = fields.Char('Method name', default=lambda self: self._get_default_payment_acquirer_onboarding_value('manual_name')) + manual_post_msg = fields.Html('Payment instructions', default=lambda self: self._get_default_payment_acquirer_onboarding_value('manual_post_msg')) + + _payment_acquirer_onboarding_cache = {} + _data_fetched = False + + def _get_manual_payment(self, env=None): + if env is None: + env = self.env + return env['payment.acquirer'].search( + [('provider', '=', 'transfer'), + ('sequence', '=', 2), + ('company_id', '=', self.env.user.company_id.id)], limit=1) + + def _get_default_payment_acquirer_onboarding_value(self, key): + if not self.env.user._is_admin(): + raise UserError(_('Only administators can access this data.')) + + if self._data_fetched: + return self._payment_acquirer_onboarding_cache.get(key, '') + + self._data_fetched = True + + installed_modules = self.env['ir.module.module'].sudo().search([ + ('name', 'in', ('payment_paypal', 'payment_stripe')), + ('state', '=', 'installed'), + ]).mapped('name') + + if 'payment_paypal' in installed_modules: + self._payment_acquirer_onboarding_cache['paypal_checked'] = True + acquirer = self.env.ref('payment.payment_acquirer_paypal') + self._payment_acquirer_onboarding_cache['paypal_email_account'] = acquirer['paypal_email_account'] + self._payment_acquirer_onboarding_cache['paypal_seller_account'] = acquirer['paypal_seller_account'] + self._payment_acquirer_onboarding_cache['paypal_pdt_token'] = acquirer['paypal_pdt_token'] + + if 'payment_stripe' in installed_modules: + self._payment_acquirer_onboarding_cache['stripe_checked'] = True + acquirer = self.env.ref('payment.payment_acquirer_stripe') + self._payment_acquirer_onboarding_cache['stripe_secret_key'] = acquirer['stripe_secret_key'] + self._payment_acquirer_onboarding_cache['stripe_publishable_key'] = acquirer['stripe_publishable_key'] + + manual_payment = self._get_manual_payment() + # manual payment is checked if the user has renamed it + self._payment_acquirer_onboarding_cache['manual_checked'] = \ + (manual_payment['name'] != 'Wire Transfer') + self._payment_acquirer_onboarding_cache['manual_name'] = manual_payment['name'] + self._payment_acquirer_onboarding_cache['manual_post_msg'] = manual_payment['post_msg'] + + return self._payment_acquirer_onboarding_cache.get(key, '') + + def _install_module(self, module_name): + module = self.env['ir.module.module'].sudo().search([('name', '=', module_name)]) + if module.state not in ('installed', 'to install', 'to upgrade'): + module.button_immediate_install() + + def _hook_on_save_onboarding_payment_acquirer(self): + # if any payment method is selected activate "Online Payment" in invoicing settings + self._install_module('account_payment') + + @api.multi + def add_payment_methods(self): + if self.paypal_checked is True: + self._install_module('payment_paypal') + + if self.stripe_checked is True: + self._install_module('payment_stripe') + + if self.paypal_checked is True or self.stripe_checked is True or self.manual_checked is True: + + # the wizard is inherited in `sale` + self._hook_on_save_onboarding_payment_acquirer() + + # create a new env including the freshly installed module(s) + new_env = api.Environment(self.env.cr, self.env.uid, self.env.context) + + if self.paypal_checked is True: + new_env.ref('payment.payment_acquirer_paypal').write({ + 'paypal_email_account': self.paypal_email_account, + 'paypal_seller_account': self.paypal_seller_account, + 'paypal_pdt_token': self.paypal_pdt_token, + 'website_published': True, + }) + if self.stripe_checked is True: + new_env.ref('payment.payment_acquirer_stripe').write({ + 'stripe_secret_key': self.stripe_secret_key, + 'stripe_publishable_key': self.stripe_publishable_key, + 'website_published': True, + }) + if self.manual_checked is True: + self._get_manual_payment(new_env).write({ + 'name': self.manual_name, + 'post_msg': self.manual_post_msg, + 'website_published': True, + }) + self._set_payment_acquirer_onboarding_step_done() + # delete wizard data immediately to get rid of residual credentials + self.unlink() + + return {'type': 'ir.actions.act_window_close'} + + @api.multi + def action_skip_payment_onboarding_payment_acquirer_step(self): + self._set_payment_acquirer_onboarding_step_done() + return {'type': 'ir.actions.act_window_close'} + + def _set_payment_acquirer_onboarding_step_done(self): + self.env.user.company_id.write({'payment_acquirer_onboarding_done': True}) From 15f498ebbdec5cdda56dfabb5a189b774fee0fe5 Mon Sep 17 00:00:00 2001 From: eco-odoo Date: Fri, 6 Jul 2018 10:22:10 +0200 Subject: [PATCH 07/10] [IMP] sale: disable logo check before printing When the logo was not set and you tried to print a quotation, a modal form opened and offered you to edit the company data. You then had to save, close the modal and retry to print. Now the quotation is printed without the logo. --- addons/sale/models/sale.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/addons/sale/models/sale.py b/addons/sale/models/sale.py index 2bd90b3a86b..c4e39ab3768 100644 --- a/addons/sale/models/sale.py +++ b/addons/sale/models/sale.py @@ -397,7 +397,9 @@ class SaleOrder(models.Model): @api.multi def print_quotation(self): self.filtered(lambda s: s.state == 'draft').write({'state': 'sent'}) - return self.env.ref('sale.action_report_saleorder').report_action(self) + + return self.env.ref('sale.action_report_saleorder')\ + .with_context({'discard_logo_check': True}).report_action(self) @api.multi def action_view_invoice(self): From f5a58effdd66e3e8473e961462e682f79b8c5fe3 Mon Sep 17 00:00:00 2001 From: eco-odoo Date: Fri, 6 Jul 2018 10:30:59 +0200 Subject: [PATCH 08/10] [ADD] sale: add onboarding panel for quotations Help users configure the system in order to send the first quotation to a customer. This panel appears above the sale quotations list. task: 1839195 --- addons/payment/__manifest__.py | 1 + addons/payment/wizards/__init__.py | 3 + addons/sale/__manifest__.py | 1 + addons/sale/controllers/__init__.py | 1 + addons/sale/controllers/onboarding.py | 24 ++++ addons/sale/models/res_company.py | 60 +++++++++- .../src/img/sale_quotation_onboarding_bg.jpg | Bin 0 -> 35910 bytes addons/sale/views/sale_onboarding_views.xml | 107 ++++++++++++++++++ addons/sale/views/sale_views.xml | 32 +++++- addons/sale/wizard/__init__.py | 1 + .../payment_acquirer_onboarding_wizard.py | 18 +++ 11 files changed, 244 insertions(+), 4 deletions(-) create mode 100644 addons/sale/controllers/onboarding.py create mode 100644 addons/sale/static/src/img/sale_quotation_onboarding_bg.jpg create mode 100644 addons/sale/views/sale_onboarding_views.xml create mode 100644 addons/sale/wizard/payment_acquirer_onboarding_wizard.py diff --git a/addons/payment/__manifest__.py b/addons/payment/__manifest__.py index 2df0e234ab5..c353f9b0ad3 100644 --- a/addons/payment/__manifest__.py +++ b/addons/payment/__manifest__.py @@ -1,4 +1,5 @@ # -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. { 'name': 'Payment Acquirer', diff --git a/addons/payment/wizards/__init__.py b/addons/payment/wizards/__init__.py index 84b24dc0131..ca76ffc555b 100644 --- a/addons/payment/wizards/__init__.py +++ b/addons/payment/wizards/__init__.py @@ -1 +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/sale/__manifest__.py b/addons/sale/__manifest__.py index 1f007b22e11..a5ff679d745 100644 --- a/addons/sale/__manifest__.py +++ b/addons/sale/__manifest__.py @@ -28,6 +28,7 @@ This module contains all the common features of Sales Management and eCommerce. 'views/mail_activity_views.xml', 'views/assets.xml', 'views/sale_portal_templates.xml', + 'views/sale_onboarding_views.xml', 'views/res_config_settings_views.xml', 'views/res_users_views.xml', ], diff --git a/addons/sale/controllers/__init__.py b/addons/sale/controllers/__init__.py index e5a6112c48f..6d24304de46 100644 --- a/addons/sale/controllers/__init__.py +++ b/addons/sale/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/sale/controllers/onboarding.py b/addons/sale/controllers/onboarding.py new file mode 100644 index 00000000000..6fa8950ae66 --- /dev/null +++ b/addons/sale/controllers/onboarding.py @@ -0,0 +1,24 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from odoo import http +from odoo.http import request + + +class OnboardingController(http.Controller): + + @http.route('/sales/onboarding_quotation_panel', auth='user', type='json') + def sale_quotation_onboarding(self): + """ Returns the `banner` for the sale 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.sale_quotation_onboarding_closed: + return {} + + return { + 'html': request.env.ref('sale.onboarding_quotation_panel').render({ + 'company': request.env.user.company_id, + }) + } diff --git a/addons/sale/models/res_company.py b/addons/sale/models/res_company.py index 6ad140861ea..01b2e7c1fdd 100644 --- a/addons/sale/models/res_company.py +++ b/addons/sale/models/res_company.py @@ -1,7 +1,7 @@ # -*- coding: utf-8 -*- # Part of Odoo. See LICENSE file for full copyright and licensing details. -from odoo import fields, models +from odoo import api, fields, models class ResCompany(models.Model): @@ -10,3 +10,61 @@ class ResCompany(models.Model): sale_note = fields.Text(string='Default Terms and Conditions', translate=True) portal_confirmation_sign = fields.Boolean(string='Digital Signature') portal_confirmation_pay = fields.Boolean(string='Electronic Payment') + + @api.depends('portal_confirmation_sign') + def _get_sale_onboarding_signature_radio(self): + """ Convert portal_confirmation_sign from boolean to radio. """ + for record in self: + record.portal_confirmation_sign_radio = 'yes' if record.portal_confirmation_sign else 'no' + + @api.depends('portal_confirmation_sign') + def _set_sale_onboarding_signature_radio(self): + """ Convert portal_confirmation_sign from radio to boolean. """ + for record in self: + record.portal_confirmation_sign = record.portal_confirmation_sign_radio == 'yes' + + # the target field, portal_confirmation_sign, is a boolean but we would like to render it as a radio button + portal_confirmation_sign_radio = fields.Selection([('yes', "Yes"), ('no', "No")], "Digital Signature (radio)", + compute=_get_sale_onboarding_signature_radio, inverse=_set_sale_onboarding_signature_radio) + + # sale quotation onboarding + sale_quotation_onboarding_closed = fields.Boolean( + string="Onboarding sale quotation closed", + help="Refers to the sale quotation onboarding panel closed state.") + sale_quotation_onboarding_folded = fields.Boolean( + string="Onboarding sale quotation folded", + help="Refers to the sale quotation onboarding panel folded state.") + sale_onboarding_quotation_layout_done = fields.Boolean("Onboarding quotation layout step done", + compute="_compute_sale_onboarding_quotation_layout_done") + sale_onboarding_payment_acquirer_done = fields.Boolean("Sale onboarding payment acquirer step done", + default=False) + sale_onboarding_signature_done = fields.Boolean("Sale onboarding signature step done", + default=False) + + @api.model + def action_toggle_fold_sale_quotation_onboarding(self): + """ Toggle the onboarding panel `folded` state. """ + self.env.user.company_id.sale_quotation_onboarding_folded =\ + not self.env.user.company_id.sale_quotation_onboarding_folded + + @api.model + def action_close_sale_quotation_onboarding(self): + """ Mark the onboarding panel as closed. """ + self.env.user.company_id.sale_quotation_onboarding_closed = True + + @api.model + def action_open_sale_onboarding_signature(self): + """ Called by onboarding panel above the quotation list.""" + action = self.env.ref('sale.action_open_sale_onboarding_signature').read()[0] + action['res_id'] = self.env.user.company_id.id + return action + + @api.model + def action_open_sale_onboarding_payment_acquirer(self): + """ Called by onboarding panel above the quotation list.""" + action = self.env.ref('sale.action_open_sale_onboarding_payment_acquirer_wizard').read()[0] + return action + + @api.multi + def action_save_onboarding_signature(self): + self.sale_onboarding_signature_done = True diff --git a/addons/sale/static/src/img/sale_quotation_onboarding_bg.jpg b/addons/sale/static/src/img/sale_quotation_onboarding_bg.jpg new file mode 100644 index 0000000000000000000000000000000000000000..3c0f7a340c709ec95017d99da281ae8e1a9cf446 GIT binary patch literal 35910 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 ch$JYQ@Z&)}MI>x? + + + + + + + + + + sale.onboarding.signature.form + res.company + +

+
+ Quotations can be confirmed automatically based on a customer signature, in + order to automate the delivery process. This functionality is convenient for all + the parties and offers as much legal guarantee as a paper signature. +
+
+ Do you want to activate the digital signature? +
+
+ +
+
+
+
+ + + + Request a digital signature + ir.actions.act_window + res.company + form + form + + new + + + + sale.onboarding.payment.acquirer.form + sale.payment.acquirer.onboarding.wizard + + primary + 1000 + + +
+ You can skip this step if you don't manage payments on quotations. +
+
+
+
+ + Choose a payment method + ir.actions.act_window + sale.payment.acquirer.onboarding.wizard + form + form + + new + + diff --git a/addons/sale/views/sale_views.xml b/addons/sale/views/sale_views.xml index 529a5a2db51..3272aab3f4e 100644 --- a/addons/sale/views/sale_views.xml +++ b/addons/sale/views/sale_views.xml @@ -194,6 +194,17 @@ + + sale.order.tree + sale.order + + primary + + + /sales/onboarding_quotation_panel + + + sale.order.form @@ -706,6 +717,23 @@ sequence="5" active="False"/> + + Quotations + ir.actions.act_window + sale.order + form + + tree,kanban,form,calendar,pivot,graph + + +

+ Create a new quotation, the first step of a new sale! +

+ Once the quotation is confirmed by the customer, it becomes a sales order.
You will be able to create an invoice and collect the payment. +

+
+
+ Quotations ir.actions.act_window @@ -724,7 +752,7 @@ @@ -985,7 +1013,6 @@ }
- Quotations ir.actions.act_window @@ -1009,7 +1036,6 @@

- Invoices account.invoice diff --git a/addons/sale/wizard/__init__.py b/addons/sale/wizard/__init__.py index 46b1c608a3d..09f069f85ed 100644 --- a/addons/sale/wizard/__init__.py +++ b/addons/sale/wizard/__init__.py @@ -1,4 +1,5 @@ # -*- coding: utf-8 -*- # Part of Odoo. See LICENSE file for full copyright and licensing details. +from . import payment_acquirer_onboarding_wizard from . import sale_make_invoice_advance diff --git a/addons/sale/wizard/payment_acquirer_onboarding_wizard.py b/addons/sale/wizard/payment_acquirer_onboarding_wizard.py new file mode 100644 index 00000000000..6bc92ec825e --- /dev/null +++ b/addons/sale/wizard/payment_acquirer_onboarding_wizard.py @@ -0,0 +1,18 @@ +# -*- 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 = 'sale.payment.acquirer.onboarding.wizard' + + def _hook_on_save_onboarding_payment_acquirer(self): + """ Override. Activate the option in sales settings. """ + self.env.user.company_id.portal_confirmation_pay = True + + def _set_payment_acquirer_onboarding_step_done(self): + """ Override. """ + self.env.user.company_id.sale_onboarding_payment_acquirer_done = True From 6671d3d72b6db0990485c3fe1a52f611345db998 Mon Sep 17 00:00:00 2001 From: eco-odoo Date: Mon, 16 Jul 2018 11:30:43 +0200 Subject: [PATCH 09/10] [IMP] account: convert dashboard onboarding to new design Convert the current design of the account dashboard setup bar in order to uniformize the design of the onboarding panels. task: 1858542 --- addons/account/__manifest__.py | 1 - addons/account/controllers/onboarding.py | 16 ++++++++ addons/account/models/company.py | 19 +++++++++ .../img/account_dashboard_onboarding_bg.jpg | Bin 0 -> 35204 bytes addons/account/views/account.xml | 1 - .../views/account_journal_dashboard_view.xml | 2 +- .../views/account_onboarding_templates.xml | 38 +++++++++++++++++- addons/account/wizard/setup_wizards.py | 12 ++---- addons/account/wizard/setup_wizards_view.xml | 8 ++-- 9 files changed, 80 insertions(+), 17 deletions(-) create mode 100644 addons/account/static/src/img/account_dashboard_onboarding_bg.jpg diff --git a/addons/account/__manifest__.py b/addons/account/__manifest__.py index c65eed84e2f..3b8a9fa18fa 100644 --- a/addons/account/__manifest__.py +++ b/addons/account/__manifest__.py @@ -62,7 +62,6 @@ 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', diff --git a/addons/account/controllers/onboarding.py b/addons/account/controllers/onboarding.py index ee51395add8..1fd91294489 100644 --- a/addons/account/controllers/onboarding.py +++ b/addons/account/controllers/onboarding.py @@ -19,3 +19,19 @@ class OnboardingController(http.Controller): '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/company.py b/addons/account/models/company.py index 1206cd9ccd9..efde6a5c4cc 100644 --- a/addons/account/models/company.py +++ b/addons/account/models/company.py @@ -80,6 +80,14 @@ Best Regards,''')) 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 @@ -432,6 +440,17 @@ Best Regards,''')) """ 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. """ 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 0000000000000000000000000000000000000000..163ffdd9e312a24b4b077a66a15a37ee3bd74c67 GIT binary patch literal 35204 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 literal 0 HcmV?d00001 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_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 -
- -
From 19775de2617c1d74aa9668db81730751e51fa447 Mon Sep 17 00:00:00 2001 From: eco-odoo Date: Wed, 18 Jul 2018 15:24:25 +0200 Subject: [PATCH 10/10] [ADD] website_sale: onboarding steps Add new onboarding steps: payment and theme selector. There are initially used on the website_sale_dashboard onboarding panel. task: 38876 --- addons/website_sale/__init__.py | 1 + addons/website_sale/__manifest__.py | 1 + addons/website_sale/models/__init__.py | 1 + addons/website_sale/models/res_company.py | 17 ++++++++++ .../website_sale/views/onboarding_views.xml | 31 +++++++++++++++++++ addons/website_sale/wizard/__init__.py | 4 +++ .../payment_acquirer_onboarding_wizard.py | 13 ++++++++ addons/website_theme_install/__init__.py | 5 ++- .../website_theme_install/models/__init__.py | 6 +++- .../models/res_company.py | 28 +++++++++++++++++ addons/website_theme_install/views/views.xml | 11 +++++++ 11 files changed, 116 insertions(+), 2 deletions(-) create mode 100644 addons/website_sale/models/res_company.py create mode 100644 addons/website_sale/views/onboarding_views.xml create mode 100644 addons/website_sale/wizard/__init__.py create mode 100644 addons/website_sale/wizard/payment_acquirer_onboarding_wizard.py create mode 100644 addons/website_theme_install/models/res_company.py 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 + +