diff --git a/addons/account/models/account_payment.py b/addons/account/models/account_payment.py index 0da4a3da3fe..dccf457a22a 100644 --- a/addons/account/models/account_payment.py +++ b/addons/account/models/account_payment.py @@ -185,14 +185,15 @@ class account_abstract_payment(models.AbstractModel): :param currency: If not specified, search a default currency on wizard/journal. :return: The total amount to pay the invoices. ''' - # Get the payment currency - if not currency: - currency = self.currency_id or self.journal_id.currency_id or self.journal_id.company_id.currency_id # Get the payment invoices if not invoices: invoices = self.invoice_ids + # Get the payment currency + if not currency: + currency = self.currency_id or self.journal_id.currency_id or self.journal_id.company_id.currency_id or invoices and invoices[0].currency_id + # Avoid currency rounding issues by summing the amounts according to the company_currency_id before total = 0.0 groups = groupby(invoices, lambda i: i.currency_id) @@ -210,6 +211,14 @@ class account_register_payments(models.TransientModel): _inherit = 'account.abstract.payment' _description = "Register payments on multiple invoices" + @api.onchange('journal_id') + def _onchange_journal(self): + res = super(account_register_payments, self)._onchange_journal() + active_ids = self._context.get('active_ids') + invoices = self.env['account.invoice'].browse(active_ids) + self.amount = abs(self._compute_payment_amount(invoices)) + return res + @api.model def default_get(self, fields): rec = super(account_register_payments, self).default_get(fields) diff --git a/addons/account_asset/models/account_asset.py b/addons/account_asset/models/account_asset.py index 36a4ee9ba25..b7b401b5e73 100644 --- a/addons/account_asset/models/account_asset.py +++ b/addons/account_asset/models/account_asset.py @@ -351,7 +351,9 @@ class AccountAssetAsset(models.Model): def set_to_close(self): move_ids = self._get_disposal_moves() if move_ids: - self._return_disposal_view(move_ids) + return self._return_disposal_view(move_ids) + # Fallback, as if we just clicked on the smartbutton + return self.open_entries() @api.multi def set_to_draft(self): diff --git a/addons/barcodes/README.rst b/addons/barcodes/README.rst index 696ab62fbc5..62739b10fe4 100644 --- a/addons/barcodes/README.rst +++ b/addons/barcodes/README.rst @@ -106,7 +106,7 @@ The purpose of this component is to listen to keypresses to detect barcodes, the on core.bus inside a 'barcode_event'. All keypress events are buffered until there is no more keypress during 50ms or a carriage return / tab is inputted (because most barcode scanners use this as a suffix). -If the buffered keys looks like a barcode (match the the regexp /.{3,}[\n\r\t]*), an event is triggered : +If the buffered keys looks like a barcode, an event is triggered : core.bus.trigger('barcode_scanned', barcode); Otherwise, the keypresses are 'resent'. However, for security reasons, a keypress event programmatically crafted doesn't trigger native browser behaviors. For this reason, BarcodeEvents doesn't intercept keypresses diff --git a/addons/board/static/src/js/board_view.js b/addons/board/static/src/js/board_view.js index aa0613e9785..b2d57903a3d 100644 --- a/addons/board/static/src/js/board_view.js +++ b/addons/board/static/src/js/board_view.js @@ -248,7 +248,7 @@ var BoardRenderer = FormRenderer.extend({ } var view = _.find(action.views, function (descr) { return descr[1] === params.viewType; - }); + }) || [false, params.viewType]; return self.loadViews(action.res_model, context, [view]) .then(function (viewsInfo) { var viewInfo = viewsInfo[params.viewType]; diff --git a/addons/calendar/models/mail_activity.py b/addons/calendar/models/mail_activity.py index 4cf142ed42d..afa39cbbc35 100644 --- a/addons/calendar/models/mail_activity.py +++ b/addons/calendar/models/mail_activity.py @@ -24,7 +24,7 @@ class MailActivity(models.Model): 'default_res_id': self.env.context.get('default_res_id'), 'default_res_model': self.env.context.get('default_res_model'), 'default_name': self.summary or self.res_name, - 'default_description': self.note and tools.html2plaintext(self.note) or '', + 'default_description': self.note and tools.html2plaintext(self.note).strip() or '', 'default_activity_ids': [(6, 0, self.ids)], } return action diff --git a/addons/crm/models/crm_team.py b/addons/crm/models/crm_team.py index d37d72544bc..657665a86df 100644 --- a/addons/crm/models/crm_team.py +++ b/addons/crm/models/crm_team.py @@ -149,14 +149,22 @@ class Team(models.Model): tree_view_id = self.env.ref('crm.crm_case_tree_view_oppor').id form_view_id = self.env.ref('crm.crm_case_form_view_oppor').id kanb_view_id = self.env.ref('crm.crm_case_kanban_view_leads').id - action['views'] = [ - [kanb_view_id, 'kanban'], - [tree_view_id, 'tree'], - [form_view_id, 'form'], - [False, 'graph'], - [False, 'calendar'], - [False, 'pivot'] - ] + + mode_string = action.get('view_mode', '') + view_mode = mode_string.split(',') if mode_string else ['kanban', 'tree', 'form', 'graph', 'calendar', 'pivot'] + dict_views = { + 'kanban': kanb_view_id, + 'tree': tree_view_id, + 'form': form_view_id, + 'graph': False, + 'calendar': False, + 'pivot': False, + } + + # Sort the views according to the view mode of the action + # which can be modified via studio, or technical menu for that matter + action['views'] = [[dict_views.get(view_type, False), view_type] for view_type in view_mode] + action['context'] = action_context return action diff --git a/addons/hr_timesheet/models/hr_timesheet.py b/addons/hr_timesheet/models/hr_timesheet.py index 9aca9f0f0f5..f46a5464015 100644 --- a/addons/hr_timesheet/models/hr_timesheet.py +++ b/addons/hr_timesheet/models/hr_timesheet.py @@ -81,7 +81,7 @@ class AccountAnalyticLine(models.Model): vals['account_id'] = project.analytic_account_id.id vals['company_id'] = project.analytic_account_id.company_id.id if not project.analytic_account_id.active: - raise UserError(_('The project you are timesheeting on is not linked to a active analytic account. Please the project configuration.')) + raise UserError(_('The project you are timesheeting on is not linked to an active analytic account. Set one on the project configuration.')) # employee implies user if vals.get('employee_id') and not vals.get('user_id'): employee = self.env['hr.employee'].browse(vals['employee_id']) diff --git a/addons/hr_timesheet/models/project.py b/addons/hr_timesheet/models/project.py index 6a68665dc04..2047c4309a9 100644 --- a/addons/hr_timesheet/models/project.py +++ b/addons/hr_timesheet/models/project.py @@ -48,7 +48,7 @@ class Project(models.Model): allow_timesheets = values['allow_timesheets'] if 'allow_timesheets' in values else self.default_get(['allow_timesheets'])['allow_timesheets'] if allow_timesheets and not values.get('analytic_account_id'): analytic_account = self.env['account.analytic.account'].create({ - 'name': values.get('name', _('Unkwon Analytic Account')), + 'name': values.get('name', _('Unknown Analytic Account')), 'company_id': values.get('company_id', self.env.user.company_id.id), 'partner_id': values.get('partner_id'), 'active': True, @@ -96,7 +96,7 @@ class Task(models.Model): analytic_account_id = fields.Many2one('account.analytic.account', string="Analytic Account", related='project_id.analytic_account_id', readonly=True) allow_timesheets = fields.Boolean("Allow timesheets", compute='_compute_allow_timesheets', help="Timesheets can be logged on this task.") - remaining_hours = fields.Float("Remaining Hours", compute='_compute_remaining_hours', inverse='_inverse_remaining_hours', help="Total remaining time, can be re-estimated periodically by the assignee of the task.") + remaining_hours = fields.Float("Remaining Hours", compute='_compute_remaining_hours', store=True, readonly=True, help="Total remaining time, can be re-estimated periodically by the assignee of the task.") effective_hours = fields.Float("Hours Spent", compute='_compute_effective_hours', compute_sudo=True, store=True, help="Computed using the sum of the task work done.") total_hours_spent = fields.Float("Total Hours", compute='_compute_total_hours_spent', store=True, help="Computed as: Time Spent + Sub-tasks Hours.") progress = fields.Float("Progress", compute='_compute_progress_hours', store=True, group_operator="avg", help="Display progress of current task.") @@ -130,11 +130,6 @@ class Task(models.Model): for task in self: task.remaining_hours = task.planned_hours - task.effective_hours - task.subtask_effective_hours - @api.onchange('remaining_hours') - def _inverse_remaining_hours(self): - for task in self: - task.planned_hours = task.remaining_hours + task.effective_hours + task.subtask_effective_hours - @api.depends('effective_hours', 'subtask_effective_hours') def _compute_total_hours_spent(self): for task in self: diff --git a/addons/mass_mailing_event/models/__init__.py b/addons/mass_mailing_event/models/__init__.py index 80862024115..85b632eb8a3 100644 --- a/addons/mass_mailing_event/models/__init__.py +++ b/addons/mass_mailing_event/models/__init__.py @@ -2,3 +2,4 @@ # Part of Odoo. See LICENSE file for full copyright and licensing details. from . import event +from . import mass_mailing diff --git a/addons/mass_mailing_event/models/mass_mailing.py b/addons/mass_mailing_event/models/mass_mailing.py new file mode 100644 index 00000000000..a9849ad8deb --- /dev/null +++ b/addons/mass_mailing_event/models/mass_mailing.py @@ -0,0 +1,12 @@ +from odoo import models, api + + +class MassMailingCampaign(models.Model): + _inherit = "mail.mass_mailing" + + @api.onchange('mailing_model_id', 'contact_list_ids') + def _onchange_model_and_list(self): + result = super(MassMailingCampaign, self)._onchange_model_and_list() + if self.mailing_model_name == 'event.registration' and self.mailing_domain == '[]': + self.mailing_domain = self.env.context.get('default_mailing_domain', '[]') + return result diff --git a/addons/product/views/product_views.xml b/addons/product/views/product_views.xml index c6c498927bd..246bd1d282c 100644 --- a/addons/product/views/product_views.xml +++ b/addons/product/views/product_views.xml @@ -73,6 +73,11 @@ + + + + +
@@ -109,11 +114,6 @@ - - - - -
diff --git a/addons/purchase_requisition/models/purchase_requisition.py b/addons/purchase_requisition/models/purchase_requisition.py index 03196f5d9f8..85e9b6238ca 100644 --- a/addons/purchase_requisition/models/purchase_requisition.py +++ b/addons/purchase_requisition/models/purchase_requisition.py @@ -119,7 +119,7 @@ class PurchaseRequisition(models.Model): def action_in_progress(self): self.ensure_one() if not all(obj.line_ids for obj in self): - raise UserError(_('You cannot confirm agreement because there is no product line.')) + raise UserError(_("You cannot confirm agreement '%s' because there is no product line.") % self.name) if self.type_id.quantity_copy == 'none' and self.vendor_id: for requisition_line in self.line_ids: if requisition_line.price_unit <= 0.0: diff --git a/addons/purchase_stock/models/stock.py b/addons/purchase_stock/models/stock.py index b3d336ceddc..5d4271f603f 100644 --- a/addons/purchase_stock/models/stock.py +++ b/addons/purchase_stock/models/stock.py @@ -78,6 +78,10 @@ class StockMove(models.Model): vals['purchase_line_id'] = self.purchase_line_id.id return vals + def _clean_merged(self): + super(StockMove, self)._clean_merged() + self.write({'created_purchase_line_id': False}) + def _action_done(self): res = super(StockMove, self)._action_done() self.mapped('purchase_line_id').sudo()._update_received_qty() diff --git a/addons/sale_stock/tests/test_sale_stock.py b/addons/sale_stock/tests/test_sale_stock.py index 0d35dc00190..f16565d27d9 100644 --- a/addons/sale_stock/tests/test_sale_stock.py +++ b/addons/sale_stock/tests/test_sale_stock.py @@ -173,14 +173,14 @@ class TestSaleStock(TestSale): # Check invoice self.assertEqual(self.so.invoice_status, 'to invoice', 'Sale Stock: so invoice_status should be "to invoice" instead of "%s" after picking return' % self.so.invoice_status) - self.assertAlmostEqual(self.so.order_line[0].qty_delivered, 3.0, 'Sale Stock: delivered quantity should be 3.0 instead of "%s" after picking return' % self.so.order_line[0].qty_delivered) + self.assertAlmostEqual(self.so.order_line[0].qty_delivered, 3.0, msg='Sale Stock: delivered quantity should be 3.0 instead of "%s" after picking return' % self.so.order_line[0].qty_delivered) # let's do an invoice with refunds adv_wiz = self.env['sale.advance.payment.inv'].with_context(active_ids=[self.so.id]).create({ 'advance_payment_method': 'all', }) adv_wiz.with_context(open_invoices=True).create_invoices() self.inv_2 = self.so.invoice_ids.filtered(lambda r: r.state == 'draft') - self.assertAlmostEqual(self.inv_2.invoice_line_ids[0].quantity, 2.0, 'Sale Stock: refund quantity on the invoice should be 2.0 instead of "%s".' % self.inv_2.invoice_line_ids[0].quantity) + self.assertAlmostEqual(self.inv_2.invoice_line_ids[0].quantity, 2.0, msg='Sale Stock: refund quantity on the invoice should be 2.0 instead of "%s".' % self.inv_2.invoice_line_ids[0].quantity) self.assertEqual(self.so.invoice_status, 'no', 'Sale Stock: so invoice_status should be "no" instead of "%s" after invoicing the return' % self.so.invoice_status) def test_03_sale_stock_delivery_partial(self): diff --git a/addons/stock/models/procurement.py b/addons/stock/models/procurement.py index fd08b362949..64929fa0084 100644 --- a/addons/stock/models/procurement.py +++ b/addons/stock/models/procurement.py @@ -1,7 +1,7 @@ # -*- coding: utf-8 -*- # Part of Odoo. See LICENSE file for full copyright and licensing details. -from collections import defaultdict +from collections import OrderedDict from datetime import datetime from dateutil.relativedelta import relativedelta from odoo.tools.misc import split_every @@ -318,9 +318,18 @@ class ProcurementGroup(models.Model): orderpoints_noprefetch = orderpoints_noprefetch[1000:] # Calculate groups that can be executed together - location_data = defaultdict(lambda: dict(products=self.env['product.product'], orderpoints=self.env['stock.warehouse.orderpoint'], groups=list())) + location_data = OrderedDict() + + def makedefault(): + return { + 'products': self.env['product.product'], + 'orderpoints': self.env['stock.warehouse.orderpoint'], + 'groups': [] + } + for orderpoint in orderpoints: key = self._procurement_from_orderpoint_get_grouping_key([orderpoint.id]) + location_data[key] = makedefault() location_data[key]['products'] += orderpoint.product_id location_data[key]['orderpoints'] += orderpoint location_data[key]['groups'] = self._procurement_from_orderpoint_get_groups([orderpoint.id]) diff --git a/addons/stock/models/stock_move.py b/addons/stock/models/stock_move.py index dbc6b131d49..43d9785d1a1 100644 --- a/addons/stock/models/stock_move.py +++ b/addons/stock/models/stock_move.py @@ -565,6 +565,10 @@ class StockMove(models.Model): move.package_level_id.id ] + def _clean_merged(self): + """Cleanup hook used when merging moves""" + self.write({'propagate': False}) + def _merge_moves(self, merge_into=False): """ This method will, for each move in `self`, go up in their linked picking and try to find in their existing moves a candidate into which we can merge the move. @@ -602,7 +606,7 @@ class StockMove(models.Model): if moves_to_unlink: # We are using propagate to False in order to not cancel destination moves merged in moves[0] - moves_to_unlink.write({'propagate': False}) + moves_to_unlink._clean_merged() moves_to_unlink._action_cancel() moves_to_unlink.sudo().unlink() return (self | self.env['stock.move'].concat(*moves_to_merge)) - moves_to_unlink diff --git a/addons/web/static/src/js/views/kanban/kanban_column_progressbar.js b/addons/web/static/src/js/views/kanban/kanban_column_progressbar.js index b597ccf1c60..7e1f16c9fb0 100644 --- a/addons/web/static/src/js/views/kanban/kanban_column_progressbar.js +++ b/addons/web/static/src/js/views/kanban/kanban_column_progressbar.js @@ -142,6 +142,7 @@ var KanbanColumnProgressBar = Widget.extend({ // Display and animate the progress bars var barNumber = 0; + var barMinWidth = 6; // In % _.each(self.colors, function (val, key) { var $bar = self.$bars[val]; var count = self.subgroupCounts && self.subgroupCounts[key] || 0; @@ -168,16 +169,15 @@ var KanbanColumnProgressBar = Widget.extend({ $bar.addClass('o_bar_has_records'); // Make sure every bar that has records has some space // and that everything adds up to 100% - var minWidth = 6*barNumber; - var maxWidth = 100 - minWidth; + var maxWidth = 100 - barMinWidth * barNumber; self.$('.progress-bar.o_bar_has_records').css('max-width', maxWidth + '%'); - self.$('.progress-bar.o_bar_has_records').css('min-width', minWidth + '%'); $bar.css('width', (count * 100 / self.groupCount) + '%'); barNumber++; } else { $bar.css('width', ''); } }); + this.$('.progress-bar.o_bar_has_records').css('min-width', barMinWidth + '%'); // Display and animate the counter number var start = this.prevTotalCounterValue; 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 b45f7a9ffdf..54f469bbc19 100644 --- a/addons/web/static/src/js/views/pivot/pivot_model.js +++ b/addons/web/static/src/js/views/pivot/pivot_model.js @@ -541,7 +541,7 @@ var PivotModel = AbstractModel.extend({ _loadData: function () { var self = this; var groupBys = []; - var rowGroupBys = this.data.groupedBy.length ? this.data.groupedBy : this.initialRowGroupBys; + var rowGroupBys = !_.isEmpty(this.data.groupedBy) ? this.data.groupedBy : this.initialRowGroupBys; var colGroupBys = this.data.colGroupBys; var measures = _.map(this.data.measures, function(measure) { if (self.fields[measure].type === 'many2one') { @@ -624,7 +624,7 @@ var PivotModel = AbstractModel.extend({ }); var index = 0; - var rowGroupBys = this.data.groupedBy.length ? this.data.groupedBy : this.initialRowGroupBys; + var rowGroupBys = !_.isEmpty(this.data.groupedBy) ? this.data.groupedBy : this.initialRowGroupBys; var colGroupBys = this.data.colGroupBys; var datapt, row, col, attrs, cell_value; var main_row_header, main_col_header; diff --git a/addons/website_sale_digital/controllers/main.py b/addons/website_sale_digital/controllers/main.py index c53f27d7336..39d0492260a 100644 --- a/addons/website_sale_digital/controllers/main.py +++ b/addons/website_sale_digital/controllers/main.py @@ -44,7 +44,7 @@ class WebsiteSaleDigital(CustomerPortal): product_id = product.id template = product.product_tmpl_id att = Attachment.search_read( - domain=['|', '&', ('res_model', '=', product._name), ('res_id', '=', product_id), '&', ('res_model', '=', template._name), '&', ('res_id', '=', template.id), ('product_downloadable', '=', True)], + domain=['|', '&', ('res_model', '=', product._name), ('res_id', '=', product_id), '&', ('res_model', '=', template._name), ('res_id', '=', template.id), ('product_downloadable', '=', True)], fields=['name', 'write_date'], order='write_date desc', ) diff --git a/doc/cla/corporate/international-business-hub.md b/doc/cla/corporate/international-business-hub.md new file mode 100644 index 00000000000..b2989af2cc2 --- /dev/null +++ b/doc/cla/corporate/international-business-hub.md @@ -0,0 +1,17 @@ +Switzerland, 2018-07-31 + +International Business Hub 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, + +Sandro Pignataro pignatarosandro@gmail.com https://github.com/Pigna74 + +List of contributors: + +Guido Notari guido.notari@gmail.com https://github.com/gnotari +Matteo Bilotta byloth@gmail.com https://github.com/Byloth + diff --git a/odoo/service/db.py b/odoo/service/db.py index 8f3c24d7d8a..95723f36690 100644 --- a/odoo/service/db.py +++ b/odoo/service/db.py @@ -71,7 +71,10 @@ def _initialize_db(id, db_name, demo, lang, user_password, login='admin', countr if country_code: countries = env['res.country'].search([('code', 'ilike', country_code)]) if countries: - env['res.company'].browse(1).country_id = countries[0] + comp_local = {'country_id': countries[0].id} + if countries[0].currency_id: + comp_local['currency_id'] = countries[0].currency_id.id + env['res.company'].browse(1).write(comp_local) # update admin's password and lang and login values = {'password': user_password, 'lang': lang}