diff --git a/addons/account/models/account_move.py b/addons/account/models/account_move.py index 92ea5f493f6..6c9b9a48409 100644 --- a/addons/account/models/account_move.py +++ b/addons/account/models/account_move.py @@ -290,7 +290,7 @@ class AccountMoveLine(models.Model): if not cr.fetchone(): cr.execute('CREATE INDEX account_move_line_partner_id_ref_idx ON account_move_line (partner_id, ref)') - @api.depends('debit', 'credit', 'amount_currency', 'currency_id', 'matched_debit_ids', 'matched_credit_ids', 'matched_debit_ids.amount', 'matched_credit_ids.amount', 'account_id.currency_id', 'move_id.state') + @api.depends('debit', 'credit', 'amount_currency', 'currency_id', 'matched_debit_ids', 'matched_credit_ids', 'matched_debit_ids.amount', 'matched_credit_ids.amount', 'move_id.state') def _amount_residual(self): """ Computes the residual amount of a move line from a reconciliable account in the company currency and the line's currency. This amount will be 0 for fully reconciled lines or lines from a non-reconciliable account, the original line amount diff --git a/addons/account/models/company.py b/addons/account/models/company.py index 6aabee9118c..d3042f466a9 100644 --- a/addons/account/models/company.py +++ b/addons/account/models/company.py @@ -4,7 +4,7 @@ from datetime import timedelta, datetime import calendar from odoo import fields, models, api, _ -from odoo.exceptions import ValidationError +from odoo.exceptions import ValidationError, UserError from odoo.exceptions import UserError from odoo.tools.float_utils import float_round, float_is_zero @@ -136,6 +136,12 @@ 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) @api.model diff --git a/addons/account/tests/test_payment.py b/addons/account/tests/test_payment.py index 46a75581fc2..9855452e209 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 b91d4748954..bda31562874 100644 --- a/addons/account/tests/test_reconciliation.py +++ b/addons/account/tests/test_reconciliation.py @@ -25,7 +25,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 07afb0c079f..06655894e4d 100644 --- a/addons/crm/models/crm_lead.py +++ b/addons/crm/models/crm_lead.py @@ -968,6 +968,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: @@ -975,28 +977,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(): + if date_deadline < today: 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 @@ -1021,9 +1023,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 @@ -1038,9 +1040,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/delivery/models/__init__.py b/addons/delivery/models/__init__.py index 5229017a189..9e162e76592 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 res_config_settings from . import delivery_carrier from . import delivery_grid from . import product_template diff --git a/addons/delivery/models/res_config_settings.py b/addons/delivery/models/res_config_settings.py new file mode 100644 index 00000000000..070f8dcdb29 --- /dev/null +++ b/addons/delivery/models/res_config_settings.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 ResConfigSettings(models.TransientModel): + _inherit = 'res.config.settings' + + def set_values(self): + super(ResConfigSettings, self).set_values() + rule = self.env.ref('delivery.delivery_carrier_comp_rule', False) + if rule: + rule.write({'active': not bool(self.company_share_product)}) diff --git a/addons/delivery/security/delivery_carrier_security.xml b/addons/delivery/security/delivery_carrier_security.xml index 8fad0891676..db718fbf361 100644 --- a/addons/delivery/security/delivery_carrier_security.xml +++ b/addons/delivery/security/delivery_carrier_security.xml @@ -6,6 +6,7 @@ ['|',('company_id','=',user.company_id.id),('company_id','=',False)] + 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/im_livechat/models/im_livechat_channel.py b/addons/im_livechat/models/im_livechat_channel.py index c2741fcef92..783fbd417ef 100644 --- a/addons/im_livechat/models/im_livechat_channel.py +++ b/addons/im_livechat/models/im_livechat_channel.py @@ -238,7 +238,7 @@ class ImLivechatChannelRule(models.Model): """ def _match(rules): for rule in rules: - if re.search(rule.regex_url, url): + if re.search(rule.regex_url or '', url): return rule return False # first, search the country specific rules (the first match is returned) diff --git a/addons/sale_crm/models/crm_lead.py b/addons/sale_crm/models/crm_lead.py index 7409fdccaa5..b4eeb43f565 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/stock/models/procurement.py b/addons/stock/models/procurement.py index 7aaf942b0e9..6f8402c1fe7 100644 --- a/addons/stock/models/procurement.py +++ b/addons/stock/models/procurement.py @@ -237,7 +237,7 @@ class ProcurementGroup(models.Model): self.sudo()._procure_orderpoint_confirm(use_new_cursor=use_new_cursor, company_id=company_id) # Search all confirmed stock_moves and try to assign them - confirmed_moves = self.env['stock.move'].search([('state', '=', 'confirmed')], limit=None, order='priority desc, date_expected asc') + confirmed_moves = self.env['stock.move'].search([('state', '=', 'confirmed'), ('product_uom_qty', '!=', 0.0)], limit=None, order='priority desc, date_expected asc') for moves_chunk in split_every(100, confirmed_moves.ids): self.env['stock.move'].browse(moves_chunk)._action_assign() if use_new_cursor: diff --git a/addons/survey/models/survey.py b/addons/survey/models/survey.py index 99faaaa5b8e..d5d249fe128 100644 --- a/addons/survey/models/survey.py +++ b/addons/survey/models/survey.py @@ -680,6 +680,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/views/pivot/pivot_controller.js b/addons/web/static/src/js/views/pivot/pivot_controller.js index 731183e7efe..5f31c7103c7 100644 --- a/addons/web/static/src/js/views/pivot/pivot_controller.js +++ b/addons/web/static/src/js/views/pivot/pivot_controller.js @@ -126,6 +126,7 @@ var PivotController = AbstractController.extend({ var table = this.model.exportData(); if(table.measure_row.length + 1 > 256) { 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; } framework.blockUI(); diff --git a/addons/web/static/src/js/views/pivot/pivot_model.js b/addons/web/static/src/js/views/pivot/pivot_model.js index 99a5e4ee334..1c76c41ab71 100644 --- a/addons/web/static/src/js/views/pivot/pivot_model.js +++ b/addons/web/static/src/js/views/pivot/pivot_model.js @@ -135,7 +135,7 @@ var PivotModel = AbstractModel.extend({ exportData: function () { var measureNbr = this.data.measures.length; var headers = this._computeHeaders(); - var measureRow = measureNbr > 1 ? _.last(headers) : []; + var measureRow = measureNbr >= 1 ? _.last(headers) : []; var rows = this._computeRows(); var i, j, value; headers[0].splice(0,1); diff --git a/addons/web_editor/static/src/js/widgets/widgets.js b/addons/web_editor/static/src/js/widgets/widgets.js index a7046eb11e8..50759d3be0a 100644 --- a/addons/web_editor/static/src/js/widgets/widgets.js +++ b/addons/web_editor/static/src/js/widgets/widgets.js @@ -1293,6 +1293,7 @@ var LinkDialog = Dialog.extend({ this.$('input.email-address').val(match = /mailto:(.+)/.exec(href) ? match[1] : ''); } else { this.$('input.url').val(href); + this.$('input.window-new').closest("div").show(); } } this.preview(); diff --git a/addons/website_form/controllers/main.py b/addons/website_form/controllers/main.py index fd42ef8da0b..fad638c48dd 100644 --- a/addons/website_form/controllers/main.py +++ b/addons/website_form/controllers/main.py @@ -171,7 +171,8 @@ class WebsiteForm(http.Controller): return data def insert_record(self, request, model, values, custom, meta=None): - record = request.env[model.model].sudo().with_context(mail_create_nosubscribe=True).create(values) + model_name = model.sudo().model + record = request.env[model_name].sudo().with_context(mail_create_nosubscribe=True).create(values) if custom or meta: default_field = model.website_form_default_field_id @@ -183,13 +184,13 @@ class WebsiteForm(http.Controller): # If there is a default field configured for this model, use it. # If there isn't, put the custom data in a message instead if default_field.name: - if default_field.ttype == 'html' or model.model == 'mail.mail': + if default_field.ttype == 'html' or model_name == 'mail.mail': custom_content = nl2br(custom_content) record.update({default_field.name: custom_content}) else: values = { 'body': nl2br(custom_content), - 'model': model.model, + 'model': model_name, 'message_type': 'comment', 'no_auto_thread': False, 'res_id': record.id, @@ -201,7 +202,8 @@ class WebsiteForm(http.Controller): # Link all files attached on the form def insert_attachment(self, model, id_record, files): orphan_attachment_ids = [] - record = model.env[model.model].browse(id_record) + model_name = model.sudo().model + record = model.env[model_name].browse(id_record) authorized_fields = model.sudo()._get_form_writable_fields() for file in files: custom_field = file.field_name not in authorized_fields @@ -209,7 +211,7 @@ class WebsiteForm(http.Controller): 'name': file.field_name if custom_field else file.filename, 'datas': base64.encodestring(file.read()), 'datas_fname': file.filename, - 'res_model': model.model, + 'res_model': model_name, 'res_id': record.id, } attachment_id = request.env['ir.attachment'].sudo().create(attachment_value) @@ -221,10 +223,10 @@ class WebsiteForm(http.Controller): # If some attachments didn't match a field on the model, # we create a mail.message to link them to the record if orphan_attachment_ids: - if model.model != 'mail.mail': + if model_name != 'mail.mail': values = { 'body': _('

Attached files :

'), - 'model': model.model, + 'model': model_name, 'message_type': 'comment', 'no_auto_thread': False, 'res_id': id_record, diff --git a/addons/website_sale_delivery/models/res_country.py b/addons/website_sale_delivery/models/res_country.py index 8b4648a7105..601ac731787 100644 --- a/addons/website_sale_delivery/models/res_country.py +++ b/addons/website_sale_delivery/models/res_country.py @@ -35,5 +35,7 @@ class ResCountry(models.Model): states = res break states |= carrier.state_ids + if not states: + states = states.search([('country_id', '=', self.id)]) res = res & states return res diff --git a/addons/website_slides/models/slides.py b/addons/website_slides/models/slides.py index b9d4aa2d757..764d8acfa6a 100644 --- a/addons/website_slides/models/slides.py +++ b/addons/website_slides/models/slides.py @@ -348,7 +348,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 279c256c5ac..a3b578a3e48 100644 --- a/addons/website_slides/static/src/js/slides.js +++ b/addons/website_slides/static/src/js/slides.js @@ -109,11 +109,12 @@ var page_widgets = {}; }, }); - $('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/doc/cla/corporate/odoogap.md b/doc/cla/corporate/odoogap.md new file mode 100644 index 00000000000..08a10ddd683 --- /dev/null +++ b/doc/cla/corporate/odoogap.md @@ -0,0 +1,18 @@ +PromptEQUATION, 2017-09-03 + +PromptEquation/ OdooGap agrees to the terms of the Odoo Corporate Contributor License +Agreement v1.0. + +I declare that I am authorized and able to make this agreement and sign this +declaration. + +Signed, + +Diogo Duarte dduarte@odoogap.com https://github.com/dduarte-odoogap + +List of contributors: + +Diogo Duarte dduarte@odoogap.com https://github.com/dduarte-odoogap +Diogo Duarte diogocarvalhoduarte@gmail.com https://github.com/dduarte-odoogap +Carlos Fonseca cfonseca@odoogap.com https://github.com/cfonseca-odoogap +Carlos Fonseca carlosmigueldafonseca@gmail.com https://github.com/cpintofonseca diff --git a/doc/cla/individual/hilarak.md b/doc/cla/individual/hilarak.md new file mode 100644 index 00000000000..4e164fe88d1 --- /dev/null +++ b/doc/cla/individual/hilarak.md @@ -0,0 +1,11 @@ +India, 18-02-2018 + +I hereby agree to the terms of the Odoo Individual Contributor License +Agreement v1.0. + +I declare that I am authorized and able to make this agreement and sign this +declaration. + +Signed, + +Hilar AK hilarak@gmail.com https://github.com/hilarak diff --git a/doc/cla/individual/lbelorgey.md b/doc/cla/individual/lbelorgey.md index 6cdacb5cdf1..c8db490ec4d 100644 --- a/doc/cla/individual/lbelorgey.md +++ b/doc/cla/individual/lbelorgey.md @@ -8,4 +8,4 @@ declaration. Signed, -Laurent Bélorgey lb@lalieutenante.com https://github.com/lbelorgey \ No newline at end of file +Laurent BĂ©lorgey lb@lalieutenante.com https://github.com/lbelorgey \ No newline at end of file diff --git a/odoo/addons/base/ir/ir_autovacuum.py b/odoo/addons/base/ir/ir_autovacuum.py index 804f7bc827d..8ffdb0b1b3e 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 self._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):