diff --git a/addons/account/i18n/account.pot b/addons/account/i18n/account.pot index d60f0de59c6..ecb3a843a78 100644 --- a/addons/account/i18n/account.pot +++ b/addons/account/i18n/account.pot @@ -9471,6 +9471,12 @@ msgstr "" msgid "You cannot cancel an invoice which is partially paid. You need to unreconcile related payment entries first." msgstr "" +#. module: account +#: code:addons/account/models/company.py:118 +#, python-format +msgid "You cannot change the currency of the company since some journal items already exist" +msgstr "" + #. module: account #: code:addons/account/models/account.py:138 #, python-format diff --git a/addons/account/models/company.py b/addons/account/models/company.py index 3211571755b..8c6a21b4614 100644 --- a/addons/account/models/company.py +++ b/addons/account/models/company.py @@ -1,7 +1,7 @@ # -*- coding: utf-8 -*- from odoo import fields, models, api, _ -from odoo.exceptions import ValidationError +from odoo.exceptions import ValidationError, UserError from datetime import timedelta @@ -110,4 +110,10 @@ Best Regards,''') company.reflect_code_prefix_change(company.cash_account_code_prefix, new_cash_code, digits) if values.get('accounts_code_digits'): company.reflect_code_digits_change(digits) + + #forbid the change of currency_id if there are already some accounting entries existing + if 'currency_id' in values and values['currency_id'] != company.currency_id.id: + if self.env['account.move.line'].search([('company_id', '=', company.id)]): + raise UserError(_('You cannot change the currency of the company since some journal items already exist')) + return super(ResCompany, self).write(values) diff --git a/addons/account/tests/test_payment.py b/addons/account/tests/test_payment.py index 4fb9da15f8d..e048fbb7930 100644 --- a/addons/account/tests/test_payment.py +++ b/addons/account/tests/test_payment.py @@ -17,7 +17,9 @@ class TestPayment(AccountingTestCase): self.currency_chf_id = self.env.ref("base.CHF").id self.currency_usd_id = self.env.ref("base.USD").id self.currency_eur_id = self.env.ref("base.EUR").id - self.env.ref('base.main_company').write({'currency_id': self.currency_eur_id}) + + company = self.env.ref('base.main_company') + self.cr.execute("UPDATE res_company SET currency_id = %s WHERE id = %s", [self.currency_eur_id, company.id]) self.product = self.env.ref("product.product_product_4") self.payment_method_manual_in = self.env.ref("account.account_payment_method_manual_in") self.payment_method_manual_out = self.env.ref("account.account_payment_method_manual_out") diff --git a/addons/account/tests/test_reconciliation.py b/addons/account/tests/test_reconciliation.py index 3b0198b3f09..7d202628fba 100644 --- a/addons/account/tests/test_reconciliation.py +++ b/addons/account/tests/test_reconciliation.py @@ -24,7 +24,8 @@ class TestReconciliation(AccountingTestCase): self.currency_swiss_id = self.env.ref("base.CHF").id self.currency_usd_id = self.env.ref("base.USD").id self.currency_euro_id = self.env.ref("base.EUR").id - self.env.ref('base.main_company').write({'currency_id': self.currency_euro_id}) + company = self.env.ref('base.main_company') + self.cr.execute("UPDATE res_company SET currency_id = %s WHERE id = %s", [self.currency_euro_id, company.id]) self.account_rcv = partner_agrolait.property_account_receivable_id or self.env['account.account'].search([('user_type_id', '=', self.env.ref('account.data_account_type_receivable').id)], limit=1) self.account_rsa = partner_agrolait.property_account_payable_id or self.env['account.account'].search([('user_type_id', '=', self.env.ref('account.data_account_type_payable').id)], limit=1) self.product = self.env.ref("product.product_product_4") diff --git a/addons/crm/models/crm_lead.py b/addons/crm/models/crm_lead.py index 102ac1d02c5..11c9eab60dc 100644 --- a/addons/crm/models/crm_lead.py +++ b/addons/crm/models/crm_lead.py @@ -912,6 +912,8 @@ class Lead(models.Model): 'nb_opportunities': 0, } + today = fields.Date.from_string(fields.Date.context_today(self)) + opportunities = self.search([('type', '=', 'opportunity'), ('user_id', '=', self._uid)]) for opp in opportunities: @@ -919,28 +921,28 @@ class Lead(models.Model): if opp.activity_date_deadline: if opp.date_deadline: date_deadline = fields.Date.from_string(opp.date_deadline) - if date_deadline == date.today(): + if date_deadline == today: result['closing']['today'] += 1 - if date.today() <= date_deadline <= date.today() + timedelta(days=7): + if today <= date_deadline <= today + timedelta(days=7): result['closing']['next_7_days'] += 1 - if date_deadline < date.today() and not opp.date_closed: + if date_deadline < today and not opp.date_closed: result['closing']['overdue'] += 1 # Next activities for activity in opp.activity_ids: date_deadline = fields.Date.from_string(activity.date_deadline) - if date_deadline == date.today(): + if date_deadline == today: result['activity']['today'] += 1 - if date.today() <= date_deadline <= date.today() + timedelta(days=7): + if today <= date_deadline <= today + timedelta(days=7): result['activity']['next_7_days'] += 1 - if date_deadline < date.today() and not opp.date_closed: + if date_deadline < today and not opp.date_closed: result['activity']['overdue'] += 1 # Won in Opportunities if opp.date_closed and opp.stage_id.probability == 100: date_closed = fields.Date.from_string(opp.date_closed) - if date.today().replace(day=1) <= date_closed <= date.today(): + if today.replace(day=1) <= date_closed <= today: if opp.planned_revenue: result['won']['this_month'] += opp.planned_revenue - elif date.today() + relativedelta(months=-1, day=1) <= date_closed < date.today().replace(day=1): + elif today + relativedelta(months=-1, day=1) <= date_closed < today.replace(day=1): if opp.planned_revenue: result['won']['last_month'] += opp.planned_revenue @@ -965,9 +967,9 @@ class Lead(models.Model): for activity in activites_done: if activity['date']: date_act = fields.Date.from_string(activity['date']) - if date.today().replace(day=1) <= date_act <= date.today(): + if today.replace(day=1) <= date_act <= today: result['done']['this_month'] += 1 - elif date.today() + relativedelta(months=-1, day=1) <= date_act < date.today().replace(day=1): + elif today + relativedelta(months=-1, day=1) <= date_act < today.replace(day=1): result['done']['last_month'] += 1 # Meetings @@ -982,9 +984,9 @@ class Lead(models.Model): for meeting in meetings: if meeting['start']: start = datetime.strptime(meeting['start'], tools.DEFAULT_SERVER_DATETIME_FORMAT).date() - if start == date.today(): + if start == today: result['meeting']['today'] += 1 - if date.today() <= start <= date.today() + timedelta(days=7): + if today <= start <= today + timedelta(days=7): result['meeting']['next_7_days'] += 1 result['done']['target'] = self.env.user.target_sales_done diff --git a/addons/crm/static/src/js/sales_team_dashboard.js b/addons/crm/static/src/js/sales_team_dashboard.js index b2bec86a753..2f1df97bbb1 100644 --- a/addons/crm/static/src/js/sales_team_dashboard.js +++ b/addons/crm/static/src/js/sales_team_dashboard.js @@ -8,7 +8,7 @@ SalesTeamDashboardView.include({ fetch_data: function() { return new Model('crm.lead') - .call('retrieve_sales_dashboard', []); + .call('retrieve_sales_dashboard', [], {context: this.session.user_context || {}}); } }); diff --git a/addons/delivery/__manifest__.py b/addons/delivery/__manifest__.py index 4a3d32e9a1d..93363b5d092 100644 --- a/addons/delivery/__manifest__.py +++ b/addons/delivery/__manifest__.py @@ -16,6 +16,7 @@ invoices from picking, the system is able to add and compute the shipping line. 'depends': ['sale_stock'], 'data': [ 'security/ir.model.access.csv', + 'security/delivery_security.xml', 'views/delivery_view.xml', 'views/partner_view.xml', 'views/product_template_view.xml', diff --git a/addons/delivery/models/__init__.py b/addons/delivery/models/__init__.py index 33fb6434d71..140f3e93204 100644 --- a/addons/delivery/models/__init__.py +++ b/addons/delivery/models/__init__.py @@ -1,6 +1,7 @@ # -*- coding: utf-8 -*- # Part of Odoo. See LICENSE file for full copyright and licensing details. +from . import base_config_settings import delivery_carrier import delivery_price_rule import product_template diff --git a/addons/delivery/models/base_config_settings.py b/addons/delivery/models/base_config_settings.py new file mode 100644 index 00000000000..2ab67cbebdd --- /dev/null +++ b/addons/delivery/models/base_config_settings.py @@ -0,0 +1,17 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from odoo import api, models + + +class BaseConfigSettings(models.TransientModel): + _inherit = 'base.config.settings' + + @api.multi + def set_auth_company_share_product(self): + self.ensure_one() + res = super(BaseConfigSettings, self).set_auth_company_share_product() + delivery_rule = self.env.ref('delivery.delivery_comp_rule', False) + if delivery_rule: + delivery_rule.write({'active': not bool(self.company_share_product)}) + return res diff --git a/addons/delivery/security/delivery_security.xml b/addons/delivery/security/delivery_security.xml new file mode 100644 index 00000000000..ae588bc245c --- /dev/null +++ b/addons/delivery/security/delivery_security.xml @@ -0,0 +1,12 @@ + + + + + Delivery multi-company + + + ['|',('product_id.company_id','=',user.company_id.id),('product_id.company_id','=',False)] + + + + diff --git a/addons/sale_crm/models/crm_lead.py b/addons/sale_crm/models/crm_lead.py index 3d986566a72..55accdf7983 100644 --- a/addons/sale_crm/models/crm_lead.py +++ b/addons/sale_crm/models/crm_lead.py @@ -1,7 +1,6 @@ # -*- coding: utf-8 -*- # Part of Odoo. See LICENSE file for full copyright and licensing details. -from datetime import date from dateutil.relativedelta import relativedelta from odoo import api, fields, models @@ -31,7 +30,7 @@ class CrmLead(models.Model): @api.model def retrieve_sales_dashboard(self): res = super(CrmLead, self).retrieve_sales_dashboard() - date_today = date.today() + date_today = fields.Date.from_string(fields.Date.context_today(self)) res['invoiced'] = { 'this_month': 0, diff --git a/addons/survey/models/survey.py b/addons/survey/models/survey.py index 3d367b0364b..8f5ad20c2c5 100644 --- a/addons/survey/models/survey.py +++ b/addons/survey/models/survey.py @@ -683,6 +683,7 @@ class SurveyLabel(models.Model): value = fields.Char('Suggested value', translate=True, required=True) quizz_mark = fields.Float('Score for this choice', help="A positive score indicates a correct choice; a negative or null score indicates a wrong answer") + @api.one @api.constrains('question_id', 'question_id_2') def _check_question_not_empty(self): """Ensure that field question_id XOR field question_id_2 is not null""" diff --git a/addons/web/static/src/js/view_manager.js b/addons/web/static/src/js/view_manager.js index a351a813263..490b2d5cfba 100644 --- a/addons/web/static/src/js/view_manager.js +++ b/addons/web/static/src/js/view_manager.js @@ -15,6 +15,9 @@ var _t = core._t; var ViewManager = Widget.extend(ControlPanelMixin, { className: "o_view_manager_content", + custom_events: { + get_controller_context: '_onGetControllerContext', + }, /** * Called each time the view manager is attached into the DOM */ @@ -474,6 +477,25 @@ var ViewManager = Widget.extend(ControlPanelMixin, { } return this._super.apply(this, arguments); }, + + //-------------------------------------------------------------------------- + // Handlers + //-------------------------------------------------------------------------- + + // ONLY FORWARDPORT THIS COMMIT UP TO SAAS-15 + /** + * Handles a context request: provides to the caller the context of the + * active view. + * + * @private + * @param {OdooEvent} ev + * @param {function} ev.data.callback used to send the requested context + */ + _onGetControllerContext: function (ev) { + var controller = this.active_view && this.active_view.controller; + var context = controller && controller.get_context(); + ev.data.callback(context); + }, }); return ViewManager; diff --git a/addons/web/static/src/js/views/form_common.js b/addons/web/static/src/js/views/form_common.js index c562bebd83e..43a061aee74 100644 --- a/addons/web/static/src/js/views/form_common.js +++ b/addons/web/static/src/js/views/form_common.js @@ -976,6 +976,9 @@ var SelectCreateListView = ListView.extend({ * Search dialog (displays a list of records and permits to create a new one by switching to a form view) */ var SelectCreateDialog = ViewDialog.extend({ + custom_events: { + get_controller_context: '_onGetControllerContext', + }, /** * options: * - initial_ids @@ -1106,6 +1109,23 @@ var SelectCreateDialog = ViewDialog.extend({ return new FormViewDialog(this.__parentedParent, this.options).open(); }, on_view_list_loaded: function() {}, + + //-------------------------------------------------------------------------- + // Handlers + //-------------------------------------------------------------------------- + + /** + * Handles a context request: provides to the caller the context of the + * list view. + * + * @private + * @param {OdooEvent} ev + * @param {function} ev.data.callback used to send the requested context + */ + _onGetControllerContext: function (ev) { + var context = this.view_list.get_context(); + ev.data.callback(context); + }, }); return { diff --git a/addons/web/static/src/js/views/pivot_view.js b/addons/web/static/src/js/views/pivot_view.js index c40d17b41e2..395a07a1016 100644 --- a/addons/web/static/src/js/views/pivot_view.js +++ b/addons/web/static/src/js/views/pivot_view.js @@ -819,7 +819,7 @@ var PivotView = View.extend({ framework.blockUI(); var nbr_measures = this.active_measures.length, headers = this.compute_headers(), - measure_row = nbr_measures > 1 ? _.last(headers) : [], + measure_row = nbr_measures >= 1 ? _.last(headers) : [], rows = this.compute_rows(), i, j, value; headers[0].splice(0,1); @@ -845,7 +845,8 @@ var PivotView = View.extend({ title: this.title, }; if(table.measure_row.length + 1 > 256) { - c.show_message(_t("For Excel compatibility, data cannot be exported if there are more than 256 columns.\n\nTip: try to flip axis, filter further or reduce the number of measures.")); + crash_manager.show_message(_t("For Excel compatibility, data cannot be exported if there are more than 256 columns.\n\nTip: try to flip axis, filter further or reduce the number of measures.")); + framework.unblockUI(); return; } session.get_file({ diff --git a/addons/web/static/src/js/views/search_menus.js b/addons/web/static/src/js/views/search_menus.js index a8ce7274256..6c7069312aa 100644 --- a/addons/web/static/src/js/views/search_menus.js +++ b/addons/web/static/src/js/views/search_menus.js @@ -93,13 +93,15 @@ return Widget.extend({ this.$inputs.first().focus(); return; } - var search = this.searchview.build_search_data(), - view_manager = this.findAncestor(function (a) { - // HORRIBLE HACK. PLEASE SAVE ME FROM MYSELF (BUT IN A PAINLESS WAY IF POSSIBLE) - return 'active_view' in a; - }), - view_context = view_manager ? view_manager.active_view.controller.get_context() : {}, - results = pyeval.sync_eval_domains_and_contexts({ + var search = this.searchview.build_search_data(); + // ONLY FORWARDPORT THIS COMMIT UP TO SAAS-15 + var view_context; + this.trigger_up('get_controller_context', { + callback: function (ctx) { + view_context = ctx; + }, + }); + var results = pyeval.sync_eval_domains_and_contexts({ domains: search.domains, contexts: search.contexts.concat(view_context || []), group_by_seq: search.groupbys || [], diff --git a/addons/website_slides/models/slides.py b/addons/website_slides/models/slides.py index ad58b7af56a..d155f3855f1 100644 --- a/addons/website_slides/models/slides.py +++ b/addons/website_slides/models/slides.py @@ -351,7 +351,7 @@ class Slide(models.Model): base_url = self.env['ir.config_parameter'].sudo().get_param('web.base.url') for record in self: if record.datas and (not record.document_id or record.slide_type in ['document', 'presentation']): - record.embed_code = '' % (base_url, record.id, 315, 420) + record.embed_code = '' % (base_url, record.id, 315, 420) elif record.slide_type == 'video' and record.document_id: if not record.mime_type: # embed youtube video diff --git a/addons/website_slides/static/src/js/slides.js b/addons/website_slides/static/src/js/slides.js index 93ff942ab83..8f3ab9ea6c8 100644 --- a/addons/website_slides/static/src/js/slides.js +++ b/addons/website_slides/static/src/js/slides.js @@ -111,11 +111,12 @@ $(document).ready(function () { }, }); - $('iframe').ready(function() { + $('iframe.o_wslides_iframe_viewer').ready(function() { // TODO : make it work. For now, once the iframe is loaded, the value of #page_count is // still now set (the pdf is still loading) - var max_page = $('iframe').contents().find('#page_count').val(); - new SlideSocialEmbed($(this), max_page).setElement($('.oe_slide_js_embed_code_widget')); + var $iframe = $(this); + var max_page = $iframe.contents().find('#page_count').val(); + new SlideSocialEmbed($iframe, max_page).setElement($('.oe_slide_js_embed_code_widget')); }); diff --git a/odoo/addons/base/ir/ir_autovacuum.py b/odoo/addons/base/ir/ir_autovacuum.py index 774a6613592..1bbbd4ee096 100644 --- a/odoo/addons/base/ir/ir_autovacuum.py +++ b/odoo/addons/base/ir/ir_autovacuum.py @@ -17,7 +17,11 @@ class AutoVacuum(models.AbstractModel): for mname in self.env: model = self.env[mname] if model.is_transient(): - model._transient_vacuum(force=True) + try: + with cr.savepoint(): + model._transient_vacuum(force=True) + except Exception as e: + _logger.warning("Failed to clean transient model %s\n%s", model, str(e)) @api.model def _gc_user_logs(self):