diff options
| author | Christophe Simonis <chs@odoo.com> | 2019-05-28 15:36:23 +0200 |
|---|---|---|
| committer | Christophe Simonis <chs@odoo.com> | 2019-05-28 15:52:00 +0200 |
| commit | cffd147cfd4767f57dc6deeb0a956361904e1b1b (patch) | |
| tree | 8a5990635ca60e0f34fc59462ed694847714db5b /addons | |
| parent | e9350993ca0b81df17308766d7e26d458bc39520 (diff) | |
| parent | a20a31486cd0b99755f8e6c27281ac6ba4758b7a (diff) | |
[MERGE] forward port branch 11.0 up to a20a31486cd
Diffstat (limited to 'addons')
24 files changed, 135 insertions, 57 deletions
diff --git a/addons/account/static/src/js/reconciliation/reconciliation_action.js b/addons/account/static/src/js/reconciliation/reconciliation_action.js index 37844f00b3a..c4e0610ec52 100644 --- a/addons/account/static/src/js/reconciliation/reconciliation_action.js +++ b/addons/account/static/src/js/reconciliation/reconciliation_action.js @@ -295,9 +295,12 @@ var StatementAction = AbstractAction.extend(ControlPanelMixin, { 'context': self.model.getContext(), }); _.each(result.handles, function (handle) { - self._getWidget(handle).destroy(); - var index = _.findIndex(self.widgets, function (widget) {return widget.handle===handle;}); - self.widgets.splice(index, 1); + var widget = self._getWidget(handle); + if (widget) { + widget.destroy(); + var index = _.findIndex(self.widgets, function (widget) {return widget.handle===handle;}); + self.widgets.splice(index, 1); + } }); // Get number of widget and if less than constant and if there are more to laod, load until constant if (self.widgets.length < self.model.defaultDisplayQty diff --git a/addons/fleet/models/fleet_vehicle.py b/addons/fleet/models/fleet_vehicle.py index ae8fa16f858..f0edc5a3059 100644 --- a/addons/fleet/models/fleet_vehicle.py +++ b/addons/fleet/models/fleet_vehicle.py @@ -219,7 +219,7 @@ class FleetVehicle(models.Model): changes.append(_("License Plate: from '%s' to '%s'") % (old_license_plate, vals['license_plate'])) if len(changes) > 0: - self.message_post(body=", ".join(changes)) + vehicle.message_post(body=", ".join(changes)) return super(FleetVehicle, self).write(vals) diff --git a/addons/hr_expense/models/hr_expense.py b/addons/hr_expense/models/hr_expense.py index 1b08fa83d35..239b57ade7a 100644 --- a/addons/hr_expense/models/hr_expense.py +++ b/addons/hr_expense/models/hr_expense.py @@ -189,6 +189,27 @@ class HrExpense(models.Model): # ---------------------------------------- @api.multi + def _prepare_move_values(self): + """ + This function prepares move values related to an expense + """ + self.ensure_one() + journal = self.sheet_id.bank_journal_id if self.payment_mode == 'company_account' else self.sheet_id.journal_id + account_date = expense.sheet_id.accounting_date or expense.date + move_values = { + 'journal_id': journal.id, + 'company_id': self.env.user.company_id.id, + 'date': account_date, + 'ref': self.sheet_id.name, + # force the name to the default value, to avoid an eventual 'default_name' in the context + # to set it to '' which cause no number to be given to the account.move when posted. + 'name': '/', + } + return move_values + + @api.multi + + @api.multi def _get_account_move_by_sheet(self): """ Return a mapping between the expense sheet of current expense and its account move :returns dict where key is a sheet id, and value is an account move record @@ -196,18 +217,8 @@ class HrExpense(models.Model): move_grouped_by_sheet = {} for expense in self: # create the move that will contain the accounting entries - account_date = expense.sheet_id.accounting_date or expense.date - if expense.sheet_id.id not in move_grouped_by_sheet: - journal = expense.sheet_id.bank_journal_id if expense.payment_mode == 'company_account' else expense.sheet_id.journal_id - move = self.env['account.move'].create({ - 'journal_id': journal.id, - 'company_id': self.env.user.company_id.id, - 'date': account_date, - 'ref': expense.sheet_id.name, - # force the name to the default value, to avoid an eventual 'default_name' in the context - # to set it to '' which cause no number to be given to the account.move when posted. - 'name': '/', - }) + if expense.sheet_id.id not in move_group_by_sheet: + move = self.env['account.move'].create(expense._prepare_move_values()) move_grouped_by_sheet[expense.sheet_id.id] = move else: move = move_grouped_by_sheet[expense.sheet_id.id] diff --git a/addons/l10n_fr_fec/wizard/account_fr_fec.py b/addons/l10n_fr_fec/wizard/account_fr_fec.py index b9e8af84869..670109986d6 100644 --- a/addons/l10n_fr_fec/wizard/account_fr_fec.py +++ b/addons/l10n_fr_fec/wizard/account_fr_fec.py @@ -306,7 +306,7 @@ class AccountFrFec(models.TransientModel): END AS PieceRef, TO_CHAR(am.date, 'YYYYMMDD') AS PieceDate, - CASE WHEN aml.name IS NULL THEN '/' ELSE replace(replace(aml.name, '|', '/'), '\t', '') END AS EcritureLib, + CASE WHEN aml.name IS NULL OR aml.name = '' THEN '/' ELSE replace(replace(aml.name, '|', '/'), '\t', '') END AS EcritureLib, replace(CASE WHEN aml.debit = 0 THEN '0,00' ELSE to_char(aml.debit, '000000000000000D99') END, '.', ',') AS Debit, replace(CASE WHEN aml.credit = 0 THEN '0,00' ELSE to_char(aml.credit, '000000000000000D99') END, '.', ',') AS Credit, CASE WHEN rec.name IS NULL THEN '' ELSE rec.name END AS EcritureLet, @@ -389,7 +389,7 @@ class AccountFrFec(models.TransientModel): rows_length = len(rows) for i, row in enumerate(rows): if not i == rows_length - 1: - row.append(lineterminator) + row[-1] += lineterminator writer.writerow(row) fecvalue = fecfile.getvalue() diff --git a/addons/mail/wizard/mail_compose_message.py b/addons/mail/wizard/mail_compose_message.py index c22a8a10cc9..cef6496c352 100644 --- a/addons/mail/wizard/mail_compose_message.py +++ b/addons/mail/wizard/mail_compose_message.py @@ -391,6 +391,7 @@ class MailComposer(models.TransientModel): # ORM handle the assignation of command list on new onchange (api.v8), # this force the complete replacement of x2many field with # command and is compatible with onchange api.v7 + attachment_ids += values.pop('attachment_ids' , []) values = self._convert_to_write(values) if attachment_ids: values.update(attachment_ids=[(6, 0, attachment_ids)]) diff --git a/addons/payment/models/payment_acquirer.py b/addons/payment/models/payment_acquirer.py index 6026b62484c..b959ccc8ea0 100644 --- a/addons/payment/models/payment_acquirer.py +++ b/addons/payment/models/payment_acquirer.py @@ -650,7 +650,7 @@ class PaymentTransaction(models.Model): # custom create custom_method_name = '%s_create' % acquirer.provider - if hasattr(acquirer, custom_method_name): + if hasattr(self, custom_method_name): values.update(getattr(self, custom_method_name)(values)) # Default value of reference is diff --git a/addons/payment_authorize/models/authorize_request.py b/addons/payment_authorize/models/authorize_request.py index d9b87437577..109cf455f6d 100644 --- a/addons/payment_authorize/models/authorize_request.py +++ b/addons/payment_authorize/models/authorize_request.py @@ -224,6 +224,12 @@ class AuthorizeAPI(): etree.SubElement(customer, "email").text = partner.email or '' response = self._authorize_request(root) res = dict() + if not response.find('customerProfileId'): + _logger.warning( + 'Unable to create customer payment profile, data missing from transaction. Transaction_id: %s - Partner_id: %s' + % (transaction_id, partner) + ) + return res res['profile_id'] = response.find('customerProfileId').text res['payment_profile_id'] = response.find('customerPaymentProfileIdList/numericString').text root_profile = self._base_tree('getCustomerPaymentProfileRequest') diff --git a/addons/payment_authorize/models/payment.py b/addons/payment_authorize/models/payment.py index cf55effdd12..0c95a444c64 100644 --- a/addons/payment_authorize/models/payment.py +++ b/addons/payment_authorize/models/payment.py @@ -240,17 +240,18 @@ class TxAuthorize(models.Model): (self.type == 'form_save' or self.acquirer_id.save_token == 'always'): transaction = AuthorizeAPI(self.acquirer_id) res = transaction.create_customer_profile_from_tx(self.partner_id, self.acquirer_reference) - token_id = self.env['payment.token'].create({ - 'authorize_profile': res.get('profile_id'), - 'name': res.get('name'), - 'acquirer_ref': res.get('payment_profile_id'), - 'acquirer_id': self.acquirer_id.id, - 'partner_id': self.partner_id.id, - }) - self.payment_token_id = token_id + if res: + token_id = self.env['payment.token'].create({ + 'authorize_profile': res.get('profile_id'), + 'name': res.get('name'), + 'acquirer_ref': res.get('payment_profile_id'), + 'acquirer_id': self.acquirer_id.id, + 'partner_id': self.partner_id.id, + }) + self.payment_token_id = token_id - if self.payment_token_id: - self.payment_token_id.verified = True + if self.payment_token_id: + self.payment_token_id.verified = True return True elif status_code == self._authorize_pending_tx_status: self.write({ diff --git a/addons/point_of_sale/models/pos_order.py b/addons/point_of_sale/models/pos_order.py index b1fc92605e3..342a55fc9b9 100644 --- a/addons/point_of_sale/models/pos_order.py +++ b/addons/point_of_sale/models/pos_order.py @@ -857,7 +857,7 @@ class PosOrder(models.Model): # a serialnumber always has a quantity of 1 product, a lot number takes the full quantity of the order line qty = 1.0 if stock_production_lot.product_id.tracking == 'lot': - qty = pos_pack_lot.pos_order_line_id.qty + qty = abs(pos_pack_lot.pos_order_line_id.qty) qty_done += qty pack_lots.append({'lot_id': stock_production_lot.id, 'qty': qty}) else: diff --git a/addons/point_of_sale/static/src/js/models.js b/addons/point_of_sale/static/src/js/models.js index d39a8987d9d..b5ac7da464d 100644 --- a/addons/point_of_sale/static/src/js/models.js +++ b/addons/point_of_sale/static/src/js/models.js @@ -1473,7 +1473,7 @@ exports.Orderline = Backbone.Model.extend({ var lots_required = 1; if (this.product.tracking == 'serial') { - lots_required = this.quantity; + lots_required = Math.abs(this.quantity); } return lots_required; @@ -1876,8 +1876,11 @@ var PacklotlineCollection = Backbone.Collection.extend({ set_quantity_by_lot: function() { if (this.order_line.product.tracking == 'serial') { - var valid_lots = this.get_valid_lots(); - this.order_line.set_quantity(valid_lots.length); + var valid_lots_quantity = this.get_valid_lots().length; + if (this.order_line.quantity < 0){ + valid_lots_quantity = -valid_lots_quantity; + } + this.order_line.set_quantity(valid_lots_quantity); } } }); diff --git a/addons/purchase/models/purchase.py b/addons/purchase/models/purchase.py index b3bac6ad1de..2d22c43abc1 100644 --- a/addons/purchase/models/purchase.py +++ b/addons/purchase/models/purchase.py @@ -766,7 +766,9 @@ class PurchaseOrderLine(models.Model): for move in self.move_ids.filtered(lambda x: x.state != 'cancel' and not x.location_dest_id.usage == "supplier"): qty += move.product_uom._compute_quantity(move.product_uom_qty, self.product_uom, rounding_method='HALF-UP') template = { - 'name': self.name or '', + # truncate to 2000 to avoid triggering index limit error + # TODO: remove index in master? + 'name': (self.name or '')[:2000], 'product_id': self.product_id.id, 'product_uom': self.product_uom.id, 'date': self.order_id.date_approve, diff --git a/addons/sale/models/account_invoice.py b/addons/sale/models/account_invoice.py index 0a2406a0968..c705c793c93 100644 --- a/addons/sale/models/account_invoice.py +++ b/addons/sale/models/account_invoice.py @@ -41,7 +41,7 @@ class AccountInvoice(models.Model): self.partner_shipping_id = addr and addr.get('delivery') if self.env.context.get('type', 'out_invoice') == 'out_invoice': company = self.company_id or self.env.user.company_id - self.comment = company.with_context(lang=self.partner_id.lang).sale_note + self.comment = company.with_context(lang=self.partner_id.lang).sale_note or (self._origin.company_id == company and self.comment) @api.multi def action_invoice_paid(self): diff --git a/addons/sale_timesheet/security/sale_timesheet_security.xml b/addons/sale_timesheet/security/sale_timesheet_security.xml index b4b5dde9754..fecc5292e63 100644 --- a/addons/sale_timesheet/security/sale_timesheet_security.xml +++ b/addons/sale_timesheet/security/sale_timesheet_security.xml @@ -11,7 +11,7 @@ <field name="perm_create" eval="1"/> <field name="perm_write" eval="1"/> <field name="perm_unlink" eval="1"/> - <field name="perm_read" eval="0"/> + <field name="perm_read" eval="1"/> </record> </data> diff --git a/addons/sms/models/mail_thread.py b/addons/sms/models/mail_thread.py index 1fb62a72de4..a439487e4e9 100644 --- a/addons/sms/models/mail_thread.py +++ b/addons/sms/models/mail_thread.py @@ -39,8 +39,8 @@ class MailThread(models.AbstractModel): if not partners: partners = self._get_default_sms_recipients() - # Collect numbers, we will consider the message to be sent if at least one number can be found - numbers = list(set([i.mobile for i in partners if i.mobile])) + # Collect numbers, we will consider the message to be sent if at least one number can be found + numbers = list(set([i.mobile for i in partners if i.mobile])) if numbers: try: diff --git a/addons/sms/wizard/send_sms.py b/addons/sms/wizard/send_sms.py index 15d284e0879..f25bedb72ab 100644 --- a/addons/sms/wizard/send_sms.py +++ b/addons/sms/wizard/send_sms.py @@ -44,8 +44,8 @@ class SendSMS(models.TransientModel): return number if not phonenumbers.is_possible_number(phone_nbr) or not phonenumbers.is_valid_number(phone_nbr): return number - phone_fmt = phonenumbers.PhoneNumberFormat.INTERNATIONAL - return phonenumbers.format_number(phone_nbr, phone_fmt).replace(' ', '') + phone_fmt = phonenumbers.PhoneNumberFormat.E164 + return phonenumbers.format_number(phone_nbr, phone_fmt) else: return number @@ -61,12 +61,11 @@ class SendSMS(models.TransientModel): @api.model def default_get(self, fields): result = super(SendSMS, self).default_get(fields) - active_model = self.env.context.get('active_model') - model = self.env[active_model] - records = self._get_records(model) - if getattr(records, '_get_default_sms_recipients'): + if not self.env.context.get('default_recipients') and active_model and hasattr(self.env[active_model], '_get_default_sms_recipients'): + model = self.env[active_model] + records = self._get_records(model) partners = records._get_default_sms_recipients() phone_numbers = [] no_phone_partners = [] @@ -83,12 +82,12 @@ class SendSMS(models.TransientModel): return result def action_send_sms(self): - numbers = self.recipients.split(',') + numbers = [number.strip() for number in self.recipients.split(',') if number.strip()] active_model = self.env.context.get('active_model') - model = self.env[active_model] - records = self._get_records(model) - if getattr(records, 'message_post_send_sms'): + if active_model and hasattr(self.env[active_model], 'message_post_send_sms'): + model = self.env[active_model] + records = self._get_records(model) records.message_post_send_sms(self.message, numbers=numbers) else: self.env['sms.api']._send_sms(numbers, self.message) diff --git a/addons/stock/models/product.py b/addons/stock/models/product.py index 1dcd88e5cad..82301ecbdfc 100644 --- a/addons/stock/models/product.py +++ b/addons/stock/models/product.py @@ -436,6 +436,10 @@ class ProductTemplate(models.Model): def _is_cost_method_standard(self): return True + @api.depends( + 'product_variant_ids', + 'product_variant_ids.stock_quant_ids', + ) def _compute_quantities(self): res = self._compute_quantities_dict() for template in self: diff --git a/addons/stock/models/stock_move.py b/addons/stock/models/stock_move.py index f530d81cfcc..1eb1a6a3107 100644 --- a/addons/stock/models/stock_move.py +++ b/addons/stock/models/stock_move.py @@ -465,6 +465,7 @@ class StockMove(models.Model): else: view = self.env.ref('stock.view_stock_move_nosuggest_operations') + picking_type_id = self.picking_type_id or self.picking_id.picking_type_id return { 'name': _('Detailed Operations'), 'type': 'ir.actions.act_window', @@ -477,8 +478,8 @@ class StockMove(models.Model): 'res_id': self.id, 'context': dict( self.env.context, - show_lots_m2o=self.has_tracking != 'none' and (self.picking_type_id.use_existing_lots or self.state == 'done' or self.origin_returned_move_id.id), # able to create lots, whatever the value of ` use_create_lots`. - show_lots_text=self.has_tracking != 'none' and self.picking_type_id.use_create_lots and not self.picking_type_id.use_existing_lots and self.state != 'done' and not self.origin_returned_move_id.id, + show_lots_m2o=self.has_tracking != 'none' and (picking_type_id.use_existing_lots or self.state == 'done' or self.origin_returned_move_id.id), # able to create lots, whatever the value of ` use_create_lots`. + show_lots_text=self.has_tracking != 'none' and picking_type_id.use_create_lots and not picking_type_id.use_existing_lots and self.state != 'done' and not self.origin_returned_move_id.id, show_source_location=self.location_id.child_ids, show_destination_location=self.location_dest_id.child_ids, show_package=not self.location_id.usage == 'supplier', diff --git a/addons/stock/wizard/stock_picking_return_views.xml b/addons/stock/wizard/stock_picking_return_views.xml index cc4dd2de9a3..9339c14f5df 100644 --- a/addons/stock/wizard/stock_picking_return_views.xml +++ b/addons/stock/wizard/stock_picking_return_views.xml @@ -27,7 +27,7 @@ <group> <field name="parent_location_id" invisible="1"/> <field name="original_location_id" invisible="1"/> - <field name="location_id" options="{'no_create': True, 'no_open': True}" groups="stock.group_stock_multi_locations"/> + <field name="location_id" options="{'no_create': True, 'no_open': True}" groups="stock.group_stock_multi_locations" required="1"/> </group> <footer> <button name="create_returns" string="Return" type="object" class="btn-primary"/> diff --git a/addons/test_mail/tests/test_mail_template.py b/addons/test_mail/tests/test_mail_template.py index 388e669b730..55ddbb7b667 100644 --- a/addons/test_mail/tests/test_mail_template.py +++ b/addons/test_mail/tests/test_mail_template.py @@ -70,6 +70,46 @@ class TestMailTemplate(BaseFunctionalTest, MockEmails, TestRecipients): body_content=self.test_record.email_from, attachments=[('first.txt', b'My first attachment', 'text/plain'), ('second.txt', b'My second attachment', 'text/plain')]) + def test_composer_template_onchange_attachments(self): + """Tests that all attachments are added to the composer, + static attachments are not duplicated and while reports are re-generated, + and that intermediary attachments are dropped.""" + + composer = self.env['mail.compose.message'].create({}) + report_template = self.env.ref('web.action_report_externalpreview') + template_1 = self.email_template.copy({ + 'report_template': report_template.id, + }) + template_2 = self.email_template.copy({ + 'attachment_ids': False, + 'report_template': report_template.id, + }) + + onchange_templates = [template_1, template_2, template_1] + attachments_onchange = [] + # template_1 has two static attachments and one dynamically generated report, + # template_2 only has the report, so we should get 3, 1, 3 attachments + attachment_numbers = [3, 1, 3] + + for template in onchange_templates: + onchange = composer.onchange_template_id( + template.id, 'comment', 'mail.test', self.test_record.id + ) + values = composer._convert_to_record(composer._convert_to_cache(onchange['value'])) + attachments = values['attachment_ids'] + composer.attachment_ids = attachments # we apply the onchange + attachments_onchange.append(attachments) + + self.assertEqual( + [len(attachments) for attachments in attachments_onchange], + attachment_numbers, + ) + + self.assertTrue( + len(attachments_onchange[0] & attachments_onchange[2]) == 2, + "The two static attachments on the template should be common to the two onchanges" + ) + @mute_logger('odoo.addons.mail.models.mail_mail') def test_post_post_w_template(self): self.test_record.sudo(self.user_employee).message_post_with_template(self.email_template.id, composition_mode='comment') diff --git a/addons/web/static/src/js/core/ajax.js b/addons/web/static/src/js/core/ajax.js index f1f73bb5a28..cbd02e0755e 100644 --- a/addons/web/static/src/js/core/ajax.js +++ b/addons/web/static/src/js/core/ajax.js @@ -22,7 +22,13 @@ function genericJsonRpc (fct_name, params, settings, fct) { core.bus.trigger('rpc:result', data, result); if (result.error !== undefined) { if (result.error.data.arguments[0] !== "bus.Bus not available in test mode") { - console.error("Server application error", JSON.stringify(result.error)); + console.error( + "Server application error\n", + "Error code:", result.error.code, "\n", + "Error message:", result.error.message, "\n", + "Error data message:\n", result.error.data.message, "\n", + "Error data debug:\n", result.error.data.debug + ); } return $.Deferred().reject("server", result.error); } else { diff --git a/addons/web/static/src/js/views/calendar/calendar_controller.js b/addons/web/static/src/js/views/calendar/calendar_controller.js index d49c7c33f5c..4525a739df9 100644 --- a/addons/web/static/src/js/views/calendar/calendar_controller.js +++ b/addons/web/static/src/js/views/calendar/calendar_controller.js @@ -156,7 +156,7 @@ var CalendarController = AbstractController.extend({ * @returns {Deferred} */ _updateRecord: function (record) { - return this.model.updateRecord(record).then(this.reload.bind(this)); + return this.model.updateRecord(record).always(this.reload.bind(this)); }, //-------------------------------------------------------------------------- diff --git a/addons/web/static/src/js/views/list/list_controller.js b/addons/web/static/src/js/views/list/list_controller.js index 459f3db56ff..96a13c90b11 100644 --- a/addons/web/static/src/js/views/list/list_controller.js +++ b/addons/web/static/src/js/views/list/list_controller.js @@ -361,8 +361,9 @@ var ListController = BasicController.extend({ * @returns {Deferred} */ _update: function () { - this._toggleSidebar(); - return this._super.apply(this, arguments); + return this._super.apply(this, arguments) + .then(this._toggleSidebar.bind(this)) + .then(this._updateButtons.bind(this, 'readonly')); }, /** * This helper simply makes sure that the control panel buttons matches the diff --git a/addons/web/static/src/less/form_view_extra.less b/addons/web/static/src/less/form_view_extra.less index 61967ea96fe..465ef8e37ea 100644 --- a/addons/web/static/src/less/form_view_extra.less +++ b/addons/web/static/src/less/form_view_extra.less @@ -132,7 +132,7 @@ border-right: 1px solid #ddd; } .o_td_label + td { - padding: 2px 36px 2px 8px; + padding: 0px 36px 0px 8px; } .o_field_widget.o_text_overflow { width: 1px!important; // hack to make the table layout believe it is a small element (so that the table does not grow too much) ... diff --git a/addons/website_quote/models/sale_order.py b/addons/website_quote/models/sale_order.py index 40add3901c7..e5e24874615 100644 --- a/addons/website_quote/models/sale_order.py +++ b/addons/website_quote/models/sale_order.py @@ -120,7 +120,7 @@ class SaleOrder(models.Model): for line in template.quote_line: discount = 0 if self.pricelist_id: - price = self.pricelist_id.with_context(uom=line.product_uom_id.id).get_product_price(line.product_id, 1, False) + price = self.pricelist_id.with_context(uom=line.product_uom_id.id).get_product_price(line.product_id, line.product_uom_qty, False) if self.pricelist_id.discount_policy == 'without_discount' and line.price_unit: discount = (line.price_unit - price) / line.price_unit * 100 price = line.price_unit |
