diff options
| author | Christophe Simonis <chs@odoo.com> | 2017-11-30 15:27:59 +0100 |
|---|---|---|
| committer | Christophe Simonis <chs@odoo.com> | 2017-11-30 15:27:59 +0100 |
| commit | ab084d580ff0edf9be1d12cec87aeffdd84459b7 (patch) | |
| tree | 6a19b4656315d94b65c88507bb471ac56d16f3ed /addons | |
| parent | 3f66814a0d01a81bef5456a07da4b1ac8e2281e7 (diff) | |
| parent | 98539336a5d40de748764ff0a3c16d8cda77b02f (diff) | |
[MERGE] forward port branch saas-15 up to 98539336a5d
Diffstat (limited to 'addons')
44 files changed, 261 insertions, 77 deletions
diff --git a/addons/account/models/account.py b/addons/account/models/account.py index cde4781888e..0fdb8213aab 100644 --- a/addons/account/models/account.py +++ b/addons/account/models/account.py @@ -525,7 +525,7 @@ class AccountTaxGroup(models.Model): class AccountTax(models.Model): _name = 'account.tax' _description = 'Tax' - _order = 'sequence' + _order = 'sequence,id' @api.model def _default_tax_group(self): diff --git a/addons/account/models/account_journal_dashboard.py b/addons/account/models/account_journal_dashboard.py index 54b05af4676..70710f65191 100644 --- a/addons/account/models/account_journal_dashboard.py +++ b/addons/account/models/account_journal_dashboard.py @@ -206,18 +206,19 @@ class account_journal(models.Model): number_late += 1 sum_late += cur.compute(result.get('amount_total'), currency) * factor + difference = currency.round(last_balance-account_sum) + 0.0 return { 'number_to_reconcile': number_to_reconcile, - 'account_balance': formatLang(self.env, account_sum, currency_obj=self.currency_id or self.company_id.currency_id), - 'last_balance': formatLang(self.env, last_balance, currency_obj=self.currency_id or self.company_id.currency_id), - 'difference': (last_balance-account_sum) and formatLang(self.env, last_balance-account_sum, currency_obj=self.currency_id or self.company_id.currency_id) or False, + 'account_balance': formatLang(self.env, currency.round(account_sum) + 0.0, currency_obj=currency), + 'last_balance': formatLang(self.env, currency.round(last_balance) + 0.0, currency_obj=currency), + 'difference': formatLang(self.env, difference, currency_obj=currency) if difference else False, 'number_draft': number_draft, 'number_waiting': number_waiting, 'number_late': number_late, - 'sum_draft': formatLang(self.env, sum_draft or 0.0, currency_obj=self.currency_id or self.company_id.currency_id), - 'sum_waiting': formatLang(self.env, sum_waiting or 0.0, currency_obj=self.currency_id or self.company_id.currency_id), - 'sum_late': formatLang(self.env, sum_late or 0.0, currency_obj=self.currency_id or self.company_id.currency_id), - 'currency_id': self.currency_id and self.currency_id.id or self.company_id.currency_id.id, + 'sum_draft': formatLang(self.env, currency.round(sum_draft) + 0.0, currency_obj=currency), + 'sum_waiting': formatLang(self.env, currency.round(sum_waiting) + 0.0, currency_obj=currency), + 'sum_late': formatLang(self.env, currency.round(sum_late) + 0.0, currency_obj=currency), + 'currency_id': currency.id, 'bank_statements_source': self.bank_statements_source, 'title': title, } diff --git a/addons/account/models/chart_template.py b/addons/account/models/chart_template.py index 4965b28e022..da20b325a3b 100644 --- a/addons/account/models/chart_template.py +++ b/addons/account/models/chart_template.py @@ -38,8 +38,7 @@ def migrate_tags_on_taxes(cr, registry): ('type_tax_use', '=', tax_template.type_tax_use), ('description', '=', tax_template.description) ]) - if len(tax_id.ids) == 1: - tax_id.sudo().write({'tag_ids': [(6, 0, tax_template.tag_ids.ids)]}) + tax_id.sudo().write({'tag_ids': [(6, 0, tax_template.tag_ids.ids)]}) def preserve_existing_tags_on_taxes(cr, registry, module): ''' This is a utility function used to preserve existing previous tags during upgrade of the module.''' diff --git a/addons/account/models/partner.py b/addons/account/models/partner.py index 9100dba6d19..a1042ff28a6 100644 --- a/addons/account/models/partner.py +++ b/addons/account/models/partner.py @@ -297,10 +297,16 @@ class ResPartner(models.Model): partner.total_invoiced = sum(price['total'] for price in price_totals if price['partner_id'] in child_ids) @api.multi - def _journal_item_count(self): + def _compute_journal_item_count(self): + AccountMoveLine = self.env['account.move.line'] for partner in self: - partner.journal_item_count = self.env['account.move.line'].search_count([('partner_id', '=', partner.id)]) - partner.contracts_count = self.env['account.analytic.account'].search_count([('partner_id', '=', partner.id)]) + partner.journal_item_count = AccountMoveLine.search_count([('partner_id', '=', partner.id)]) + + @api.multi + def _compute_contracts_count(self): + AccountAnalyticAccount = self.env['account.analytic.account'] + for partner in self: + partner.contracts_count = AccountAnalyticAccount.search_count([('partner_id', '=', partner.id)]) def get_followup_lines_domain(self, date, overdue_only=False, only_unblocked=False): domain = [('reconciled', '=', False), ('account_id.deprecated', '=', False), ('account_id.internal_type', '=', 'receivable'), '|', ('debit', '!=', 0), ('credit', '!=', 0), ('company_id', '=', self.env.user.company_id.id)] @@ -374,9 +380,8 @@ class ResPartner(models.Model): groups='account.group_account_invoice') currency_id = fields.Many2one('res.currency', compute='_get_company_currency', readonly=True, string="Currency", help='Utility field to express amount currency') - - contracts_count = fields.Integer(compute='_journal_item_count', string="Contracts", type='integer') - journal_item_count = fields.Integer(compute='_journal_item_count', string="Journal Items", type="integer") + contracts_count = fields.Integer(compute='_compute_contracts_count', string="Contracts", type='integer') + journal_item_count = fields.Integer(compute='_compute_journal_item_count', string="Journal Items", type="integer") property_account_payable_id = fields.Many2one('account.account', company_dependent=True, string="Account Payable", oldname="property_account_payable", domain="[('internal_type', '=', 'payable'), ('deprecated', '=', False)]", diff --git a/addons/event_sale/models/sale_order.py b/addons/event_sale/models/sale_order.py index 44801880267..7a2dcd8a745 100644 --- a/addons/event_sale/models/sale_order.py +++ b/addons/event_sale/models/sale_order.py @@ -42,7 +42,7 @@ class SaleOrderLine(models.Model): registrations linked to this line. This method update existing registrations and create new one for missing one. """ Registration = self.env['event.registration'] - registrations = Registration.search([('sale_order_line_id', 'in', self.ids)]) + registrations = Registration.search([('sale_order_line_id', 'in', self.ids), ('state', '!=', 'cancel')]) for so_line in self.filtered('event_id'): existing_registrations = registrations.filtered(lambda self: self.sale_order_line_id.id == so_line.id) if confirm: diff --git a/addons/hr_expense/models/hr_expense.py b/addons/hr_expense/models/hr_expense.py index c422e613d8d..194408a9118 100644 --- a/addons/hr_expense/models/hr_expense.py +++ b/addons/hr_expense/models/hr_expense.py @@ -576,4 +576,11 @@ class HrExpenseSheet(models.Model): def _check_employee(self): employee_ids = self.expense_line_ids.mapped('employee_id') if len(employee_ids) > 1 or (len(employee_ids) == 1 and employee_ids != self.employee_id): - raise ValidationError(_('You cannot add expense lines of another employee.'))
\ No newline at end of file + raise ValidationError(_('You cannot add expense lines of another employee.')) + + @api.one + @api.constrains('expense_line_ids') + def _check_payment_mode(self): + payment_mode = set(self.expense_line_ids.mapped('payment_mode')) + if len(payment_mode) > 1: + raise ValidationError(_('You cannot report expenses with different payment modes.'))
\ No newline at end of file diff --git a/addons/hr_expense/views/hr_expense_views.xml b/addons/hr_expense/views/hr_expense_views.xml index a8f4f045661..83ed8a014a6 100644 --- a/addons/hr_expense/views/hr_expense_views.xml +++ b/addons/hr_expense/views/hr_expense_views.xml @@ -465,9 +465,14 @@ <field name="attachment_number" string=" "/> <button name="action_get_attachment_view" string="View Attachments" type="object" icon="fa-paperclip"/> <field name="tax_ids" widget="many2many_tags"/> - <field name="total_amount" sum="Total Amount" widget="monetary"/> + <field name="total_amount" widget="monetary"/> </tree> </field> + <group> + <group class="oe_subtotal_footer oe_right" colspan="2" name="expense_total"> + <field name="total_amount" widget='monetary' options="{'currency_field': 'currency_id'}"/> + </group> + </group> </sheet> <div class="oe_chatter"> <field name="message_follower_ids" widget="mail_followers"/> diff --git a/addons/l10n_ch/models/res_bank.py b/addons/l10n_ch/models/res_bank.py index 0ddc98172a3..28abbc7b058 100644 --- a/addons/l10n_ch/models/res_bank.py +++ b/addons/l10n_ch/models/res_bank.py @@ -46,7 +46,8 @@ class ResPartnerBank(models.Model): for record in self: if record.acc_type == 'iban': record.l10n_ch_postal = record._retrieve_l10n_ch_postal(record.sanitized_acc_number) - record.l10n_ch_postal = record.sanitized_acc_number + else: + record.l10n_ch_postal = record.sanitized_acc_number def _retrieve_l10n_ch_postal(self, iban): """ Reads a swiss postal account number from a an IBAN and returns it as diff --git a/addons/l10n_fr_certification/models/account.py b/addons/l10n_fr_certification/models/account.py index 07d5fc04e41..94db3c46273 100644 --- a/addons/l10n_fr_certification/models/account.py +++ b/addons/l10n_fr_certification/models/account.py @@ -144,8 +144,9 @@ class AccountJournal(models.Model): @api.multi def write(self, vals): # restrict the operation in case we are trying to write a forbidden field - if self.company_id.country_id.code == 'FR' and vals.get('update_posted'): - raise UserError(ERR_MSG % (self._name, 'update_posted')) + for rec in self: + if rec.company_id.country_id.code == 'FR' and vals.get('update_posted'): + raise UserError(ERR_MSG % (self._name, 'update_posted')) return super(AccountJournal, self).write(vals) @api.model diff --git a/addons/mail/models/mail_thread.py b/addons/mail/models/mail_thread.py index 7a0ec8a65d0..b5aa6616794 100644 --- a/addons/mail/models/mail_thread.py +++ b/addons/mail/models/mail_thread.py @@ -1365,6 +1365,8 @@ class MailThread(models.AbstractModel): mail module, and should not contain security or generic html cleaning. Indeed those aspects should be covered by the html_sanitize method located in tools. """ + if not body: + return body, attachments root = lxml.html.fromstring(body) postprocessed = False to_remove = [] @@ -1399,7 +1401,7 @@ class MailThread(models.AbstractModel): # Content-Type: multipart/related; # boundary="_004_3f1e4da175f349248b8d43cdeb9866f1AMSPR06MB343eurprd06pro_"; # type="text/html" - if not message.is_multipart() or message.get('content-type', '').startswith("text/"): + if message.get_content_maintype() == 'text': encoding = message.get_content_charset() body = message.get_payload(decode=True) body = tools.ustr(body, encoding, errors='replace') diff --git a/addons/mail/models/res_users.py b/addons/mail/models/res_users.py index 059e0f521ad..ff9a3abba1c 100644 --- a/addons/mail/models/res_users.py +++ b/addons/mail/models/res_users.py @@ -3,6 +3,7 @@ from odoo import _, api, exceptions, fields, models from odoo.tools import pycompat +from odoo.addons.base.res.res_users import is_selection_groups class Users(models.Model): @@ -62,11 +63,14 @@ class Users(models.Model): @api.multi def write(self, vals): write_res = super(Users, self).write(vals) + sel_groups = [vals[k] for k in vals if is_selection_groups(k) and vals[k]] if vals.get('groups_id'): # form: {'group_ids': [(3, 10), (3, 3), (4, 10), (4, 3)]} or {'group_ids': [(6, 0, [ids]} user_group_ids = [command[1] for command in vals['groups_id'] if command[0] == 4] user_group_ids += [id for command in vals['groups_id'] if command[0] == 6 for id in command[2]] self.env['mail.channel'].search([('group_ids', 'in', user_group_ids)])._subscribe_users() + elif sel_groups: + self.env['mail.channel'].search([('group_ids', 'in', sel_groups)])._subscribe_users() return write_res def _create_welcome_message(self): diff --git a/addons/mail/security/ir.model.access.csv b/addons/mail/security/ir.model.access.csv index 5040f812d73..1b14cacee0e 100644 --- a/addons/mail/security/ir.model.access.csv +++ b/addons/mail/security/ir.model.access.csv @@ -22,7 +22,7 @@ access_mail_alias_all,mail.alias.all,model_mail_alias,,1,0,0,0 access_mail_alias_user,mail.alias.user,model_mail_alias,base.group_user,1,1,1,1
access_mail_alias_system,mail.alias.system,model_mail_alias,base.group_system,1,1,1,1
access_mail_message_subtype_all,mail.message.subtype.all,model_mail_message_subtype,,1,0,0,0
-access_mail_message_subtype_user,mail.message.subtype.user,model_mail_message_subtype,,1,1,1,1
+access_mail_message_subtype_user,mail.message.subtype.user,model_mail_message_subtype,base.group_user,1,1,1,1
access_mail_tracking_value_all,mail.tracking.value.all,model_mail_tracking_value,,0,0,0,0
access_mail_tracking_value_portal,mail.tracking.value.portal,model_mail_tracking_value,base.group_portal,0,0,0,0
access_mail_tracking_value_user,mail.tracking.value.user,model_mail_tracking_value,base.group_user,0,0,0,0
diff --git a/addons/mail/tests/test_mail_gateway.py b/addons/mail/tests/test_mail_gateway.py index 2a356d9ed13..48f32099592 100644 --- a/addons/mail/tests/test_mail_gateway.py +++ b/addons/mail/tests/test_mail_gateway.py @@ -176,6 +176,19 @@ Content-Type: text/html; --Apple-Mail=_9331E12B-8BD2-4EC7-B53E-01F3FBEC9227-- """ +MAIL_SINGLE_BINARY = """X-Original-To: raoul@grosbedon.fr +Delivered-To: raoul@grosbedon.fr +Received: by mail1.grosbedon.com (Postfix, from userid 10002) + id E8166BFACA; Fri, 23 Aug 2013 13:18:01 +0200 (CEST) +From: "Bruce Wayne" <bruce@wayneenterprises.com> +Content-Type: application/pdf; +Content-Disposition: filename=thetruth.pdf +Content-Transfer-Encoding: base64 +Message-Id: <6BB1FAB2-2104-438E-9447-07AE2C8C4A92@sexample.com> +Mime-Version: 1.0 (Mac OS X Mail 7.3 \(1878.6\)) + +SSBhbSB0aGUgQmF0TWFuCg==""" + MAIL_MULTIPART_IMAGE = """X-Original-To: raoul@example.com Delivered-To: micheline@example.com @@ -349,6 +362,10 @@ class TestMailgateway(TestMail): res.get('body', ''), 'message_parse: second part of the html version should be in body after parsing multipart/mixed') + res = self.env['mail.thread'].message_parse(MAIL_SINGLE_BINARY) + self.assertEqual(res['body'], '') + self.assertEqual(res['attachments'][0][0], 'thetruth.pdf') + @mute_logger('odoo.addons.mail.models.mail_thread') def test_message_process_cid(self): new_groups = self.format_and_process(MAIL_MULTIPART_IMAGE, subject='My Frogs', to='groups@example.com') diff --git a/addons/mrp_repair/report/mrp_repair_templates_repair_order.xml b/addons/mrp_repair/report/mrp_repair_templates_repair_order.xml index 433df24c6e1..37ac502fd3b 100644 --- a/addons/mrp_repair/report/mrp_repair_templates_repair_order.xml +++ b/addons/mrp_repair/report/mrp_repair_templates_repair_order.xml @@ -15,13 +15,13 @@ <strong t-if="o.address_id != o.partner_invoice_id">Invoice address: </strong> <div t-field="o.partner_invoice_id" t-options='{"widget": "contact", "fields": ["address", "name", "phone", "fax"], "no_marker": True, "phone_icons": True}'/> - <p t-if="o.partner_id.vat">VAT: <span t-field="o.partner_id.vat"/></p> + <p t-if="o.partner_invoice_id.vat">VAT: <span t-field="o.partner_invoice_id.vat"/></p> </div> <div t-if="o.address_id != o.partner_invoice_id"> <strong>Shipping address :</strong> <div t-field="o.address_id" t-options='{"widget": "contact", "fields": ["address", "name", "phone", "fax"], "no_marker": True, "phone_icons": True}'/> - <p t-if="o.partner_id.vat">VAT: <span t-field="o.partner_id.vat"/></p> + <p t-if="o.address_id.vat">VAT: <span t-field="o.address_id.vat"/></p> </div> </div> <div class="col-xs-5 col-xs-offset-1"> diff --git a/addons/payment_authorize/models/authorize_request.py b/addons/payment_authorize/models/authorize_request.py index 1ae0e71b6ed..f7aca95511d 100644 --- a/addons/payment_authorize/models/authorize_request.py +++ b/addons/payment_authorize/models/authorize_request.py @@ -29,6 +29,34 @@ def strip_ns(xml, ns): return it.root +def error_check(elem): + """Check if the response sent by Authorize.net contains an error. + + Errors can be a failure to try the transaction (in that case, the transasctionResponse + is empty, and the meaningful error message will be in message/code) or a failure to process + the transaction (in that case, the message/code content will be generic and the actual error + message is in transactionResponse/errors/error/errorText). + + :param etree._Element elem: the root element of the response that will be parsed + + :rtype: tuple (bool, str) + :return: tuple containnig a boolean indicating if the response should be considered + as an error and the most meaningful error message found in it. + """ + result_code = elem.find('messages/resultCode') + msg = 'No meaningful error message found, please check logs or the Authorize.net backend' + has_error = result_code is not None and result_code.text == 'Error' + if has_error: + # accumulate the most meangingful error + error = elem.find('transactionResponse/errors/error') + error = error if error is not None else elem.find('messages/message') + if error is not None: + code = error[0].text + text = error[1].text + msg = '%s: %s' % (code, text) + return (has_error, msg) + + class AuthorizeAPI(): """Authorize.net Gateway API integration. @@ -38,6 +66,9 @@ class AuthorizeAPI(): - Customer Profile/Payment Profile creation - Transaction authorization/capture/voiding """ + + AUTH_ERROR_STATUS = 3 + def __init__(self, acquirer): """Initiate the environment with the acquirer data. @@ -63,9 +94,6 @@ class AuthorizeAPI(): request.add_header('Content-Type', 'text/xml') response = urlopen(request).read() response = strip_ns(response, XMLNS) - if response.find('messages/resultCode').text == 'Error': - messages = map(lambda m: m.text, response.findall('messages/message/text')) - raise ValidationError(_('Authorize.net Error Message(s):\n %s') % '\n'.join(messages)) return response def _base_tree(self, requestType): @@ -190,6 +218,11 @@ class AuthorizeAPI(): etree.SubElement(order, "invoiceNumber").text = reference response = self._authorize_request(root) res = dict() + (has_error, error_msg) = error_check(response) + if has_error: + res['x_response_code'] = self.AUTH_ERROR_STATUS + res['x_response_reason_text'] = error_msg + return res res['x_response_code'] = response.find('transactionResponse/responseCode').text res['x_trans_id'] = response.find('transactionResponse/transId').text res['x_type'] = 'auth_capture' @@ -220,6 +253,11 @@ class AuthorizeAPI(): etree.SubElement(order, "invoiceNumber").text = reference response = self._authorize_request(root) res = dict() + (has_error, error_msg) = error_check(response) + if has_error: + res['x_response_code'] = self.AUTH_ERROR_STATUS + res['x_response_reason_text'] = error_msg + return res res['x_response_code'] = response.find('transactionResponse/responseCode').text res['x_trans_id'] = response.find('transactionResponse/transId').text res['x_type'] = 'auth_only' @@ -245,6 +283,11 @@ class AuthorizeAPI(): etree.SubElement(tx, "refTransId").text = transaction_id response = self._authorize_request(root) res = dict() + (has_error, error_msg) = error_check(response) + if has_error: + res['x_response_code'] = self.AUTH_ERROR_STATUS + res['x_response_reason_text'] = error_msg + return res res['x_response_code'] = response.find('transactionResponse/responseCode').text res['x_trans_id'] = response.find('transactionResponse/transId').text res['x_type'] = 'prior_auth_capture' @@ -265,6 +308,11 @@ class AuthorizeAPI(): etree.SubElement(tx, "refTransId").text = transaction_id response = self._authorize_request(root) res = dict() + (has_error, error_msg) = error_check(response) + if has_error: + res['x_response_code'] = self.AUTH_ERROR_STATUS + res['x_response_reason_text'] = error_msg + return res res['x_response_code'] = response.find('transactionResponse/responseCode').text res['x_trans_id'] = response.find('transactionResponse/transId').text res['x_type'] = 'void' diff --git a/addons/payment_authorize/tests/test_authorize.py b/addons/payment_authorize/tests/test_authorize.py index 50e4e95fdc4..35be8a99b17 100644 --- a/addons/payment_authorize/tests/test_authorize.py +++ b/addons/payment_authorize/tests/test_authorize.py @@ -209,7 +209,7 @@ class AuthorizeForm(AuthorizeCommon): 'acquirer_id': authorize.id, 'type': 'server2server', 'currency_id': self.currency_usd.id, - 'reference': 'test_ref_%s' % odoo.fields.Date.today(), + 'reference': 'test_ref_%s' % int(time.time()), 'payment_token_id': payment_token.id, 'partner_id': self.buyer_id, @@ -251,3 +251,20 @@ class AuthorizeForm(AuthorizeCommon): self.assertEqual(transaction.state, 'authorized') transaction.action_void() self.assertEqual(transaction.state, 'cancel') + + # try charging an unexisting profile + ghost_payment_token = payment_token.copy() + ghost_payment_token.authorize_profile = '99999999999' + # create normal s2s transaction + transaction = self.env['payment.transaction'].create({ + 'amount': 500, + 'acquirer_id': authorize.id, + 'type': 'server2server', + 'currency_id': self.currency_usd.id, + 'reference': 'test_ref_%s' % int(time.time()), + 'payment_token_id': ghost_payment_token.id, + 'partner_id': self.buyer_id, + + }) + transaction.authorize_s2s_do_transaction() + self.assertEqual(transaction.state, 'error') diff --git a/addons/point_of_sale/static/src/js/models.js b/addons/point_of_sale/static/src/js/models.js index 909a22096ca..d4aaf40f664 100644 --- a/addons/point_of_sale/static/src/js/models.js +++ b/addons/point_of_sale/static/src/js/models.js @@ -519,7 +519,7 @@ exports.PosModel = Backbone.Model.extend({ .then(function(){ load_model(index + 1); }, function(err){ loaded.reject(err); }); }catch(err){ - console.error(err.stack); + console.error(err.message, err.stack); loaded.reject(err); } },function(err){ diff --git a/addons/point_of_sale/static/src/js/screens.js b/addons/point_of_sale/static/src/js/screens.js index be7ebc43243..8cf2f5d9a7d 100644 --- a/addons/point_of_sale/static/src/js/screens.js +++ b/addons/point_of_sale/static/src/js/screens.js @@ -73,12 +73,21 @@ var ScreenWidget = PosBaseWidget.extend({ // - if there's a user with a matching barcode, put it as the active 'cashier', go to cashier mode, and return true // - else : do nothing and return false. You probably want to extend this to show and appropriate error popup... barcode_cashier_action: function(code){ + var self = this; var users = this.pos.users; for(var i = 0, len = users.length; i < len; i++){ if(users[i].barcode === code.code){ - this.pos.set_cashier(users[i]); - this.chrome.widget.username.renderElement(); - return true; + if (users[i].id !== this.pos.get_cashier().id && users[i].pos_security_pin) { + return this.gui.ask_password(users[i].pos_security_pin).then(function(){ + self.pos.set_cashier(users[i]); + self.chrome.widget.username.renderElement(); + return true; + }); + } else { + this.pos.set_cashier(users[i]); + this.chrome.widget.username.renderElement(); + return true; + } } } this.barcode_error_action(code); @@ -1045,10 +1054,10 @@ var ClientListScreenWidget = ScreenWidget.extend({ this.$('.searchbox input').on('keypress',function(event){ clearTimeout(search_timeout); - var query = this.value; + var searchbox = this; search_timeout = setTimeout(function(){ - self.perform_search(query,event.which === 13); + self.perform_search(searchbox.value, event.which === 13); },70); }); @@ -2090,4 +2099,3 @@ return { }; }); - diff --git a/addons/point_of_sale/tools/posbox/overwrite_before_init/etc/init_posbox_image.sh b/addons/point_of_sale/tools/posbox/overwrite_before_init/etc/init_posbox_image.sh index 00058bef68c..6856f9b4d06 100755 --- a/addons/point_of_sale/tools/posbox/overwrite_before_init/etc/init_posbox_image.sh +++ b/addons/point_of_sale/tools/posbox/overwrite_before_init/etc/init_posbox_image.sh @@ -17,8 +17,7 @@ export DEBIAN_FRONTEND=noninteractive echo "nameserver 8.8.8.8" >> /etc/resolv.conf -apt-get update -apt-get -y dist-upgrade +apt-get update && apt-get -y upgrade # Do not be too fast to upgrade to more recent firmware and kernel than 4.38 # Firmware 4.44 seems to prevent the LED mechanism from working @@ -39,6 +38,8 @@ rm -rf /usr/share/doc pip install pyusb==1.0.0b1 pip install qrcode pip install evdev +pip install simplejson +pip install unittest2 # --upgrade because websocket_client in wheezy is bad: # https://github.com/docker/compose/issues/1288 @@ -53,7 +54,6 @@ sudo -u postgres createuser -s pi mkdir /var/log/odoo chown pi:pi /var/log/odoo chown pi:pi -R /home/pi/odoo/ -chmod 770 -R /home/pi/odoo/ # logrotate is very picky when it comes to file permissions chown -R root:root /etc/logrotate.d/ diff --git a/addons/point_of_sale/tools/posbox/posbox_download_images.sh b/addons/point_of_sale/tools/posbox/posbox_download_images.sh index 09bd79b2103..33beddac041 100755 --- a/addons/point_of_sale/tools/posbox/posbox_download_images.sh +++ b/addons/point_of_sale/tools/posbox/posbox_download_images.sh @@ -1,5 +1,5 @@ #!/bin/sh -wget 'https://downloads.raspberrypi.org/raspbian_lite_latest' -O raspbian.img.zip +wget 'http://downloads.raspberrypi.org/raspbian_lite/images/raspbian_lite-2017-07-05/2017-07-05-raspbian-jessie-lite.zip' -O raspbian.img.zip unzip raspbian.img.zip wget 'https://github.com/dhruvvyas90/qemu-rpi-kernel/raw/master/kernel-qemu-4.4.13-jessie' -O kernel-qemu diff --git a/addons/pos_discount/static/src/js/discount.js b/addons/pos_discount/static/src/js/discount.js index 497573f5107..7ef38d6ffdb 100644 --- a/addons/pos_discount/static/src/js/discount.js +++ b/addons/pos_discount/static/src/js/discount.js @@ -1,14 +1,17 @@ odoo.define('pos_discount.pos_discount', function (require) { "use strict"; +var core = require('web.core'); var screens = require('point_of_sale.screens'); +var _t = core._t; + var DiscountButton = screens.ActionButtonWidget.extend({ template: 'DiscountButton', button_click: function(){ var self = this; this.gui.show_popup('number',{ - 'title': 'Discount Percentage', + 'title': _t('Discount Percentage'), 'value': this.pos.config.discount_pc, 'confirm': function(val) { val = Math.round(Math.max(0,Math.min(100,val))); diff --git a/addons/purchase/security/purchase_security.xml b/addons/purchase/security/purchase_security.xml index 1416745a8bb..4c409175bb4 100644 --- a/addons/purchase/security/purchase_security.xml +++ b/addons/purchase/security/purchase_security.xml @@ -51,5 +51,12 @@ <field name="domain_force">['|',('company_id','=',False),('company_id','child_of',[user.company_id.id])]</field> </record> + <record model="ir.rule" id="purchase_order_report_comp_rule"> + <field name="name">Purchase Order Report multi-company</field> + <field name="model_id" ref="model_purchase_report"/> + <field name="global" eval="True"/> + <field name="domain_force">['|',('company_id','=',False),('company_id','child_of',[user.company_id.id])]</field> + </record> + </data> </odoo> diff --git a/addons/report/static/src/less/layout_clean.less b/addons/report/static/src/less/layout_clean.less index 85e9c2d8491..869e1f4098f 100644 --- a/addons/report/static/src/less/layout_clean.less +++ b/addons/report/static/src/less/layout_clean.less @@ -70,7 +70,8 @@ /*Total table*/ .row > .col-xs-4 { width: 43%; - margin-top: -20px; + position: relative; + top: -20px; > table { tr:last-child td { border-top: 1px solid @gray-lighter !important; diff --git a/addons/stock/models/stock_picking.py b/addons/stock/models/stock_picking.py index d6616d38cbe..c1dbb75922c 100644 --- a/addons/stock/models/stock_picking.py +++ b/addons/stock/models/stock_picking.py @@ -637,7 +637,7 @@ class Picking(models.Model): move_dict = prod2move_ids[product_id][index] qty_on_link = min(move_dict['remaining_qty'], qty_to_assign) self.env['stock.move.operation.link'].create({'move_id': move_dict['move'].id, 'operation_id': operation_id, 'qty': qty_on_link, 'reserved_quant_id': quant_id}) - if move_dict['remaining_qty'] == qty_on_link: + if float_compare(move_dict['remaining_qty'], qty_on_link, precision_rounding=move_dict['move'].product_uom.rounding) == 0: prod2move_ids[product_id].pop(index) else: move_dict['remaining_qty'] -= qty_on_link diff --git a/addons/stock/wizard/stock_immediate_transfer.py b/addons/stock/wizard/stock_immediate_transfer.py index 365e9dbb382..2183e454023 100644 --- a/addons/stock/wizard/stock_immediate_transfer.py +++ b/addons/stock/wizard/stock_immediate_transfer.py @@ -33,4 +33,4 @@ class StockImmediateTransfer(models.TransientModel): pack.write({'qty_done': pack.product_qty}) else: pack.unlink() - self.pick_id.do_transfer() + return self.pick_id.do_transfer() diff --git a/addons/stock_account/models/stock.py b/addons/stock_account/models/stock.py index edace2f8fff..ee3a3603c52 100644 --- a/addons/stock_account/models/stock.py +++ b/addons/stock_account/models/stock.py @@ -71,7 +71,10 @@ class StockQuant(models.Model): valuation_update = newprice - quant.cost # this is where we post accounting entries for adjustment, if needed # If neg quant period already closed (likely with manual valuation), skip update - if not quant.company_id.currency_id.is_zero(valuation_update) and move._check_lock_date(): + lock_date = max(move.company_id.period_lock_date, move.company_id.fiscalyear_lock_date) + if self.user_has_groups('account.group_account_manager'): + lock_date = move.company_id.fiscalyear_lock_date + if not quant.company_id.currency_id.is_zero(valuation_update) and lock_date and move.date[:10] > lock_date: quant.with_context(force_valuation_amount=valuation_update)._account_entry_move(move) # update the standard price of the product, only if we would have diff --git a/addons/web/static/src/js/boot.js b/addons/web/static/src/js/boot.js index 9d299b1da84..c229029b944 100644 --- a/addons/web/static/src/js/boot.js +++ b/addons/web/static/src/js/boot.js @@ -10,7 +10,7 @@ * only when the deferred is resolved, and its value is equal to the resolved value. * The module can be rejected (unloaded). This will be logged in the console as info. * - * logs: + * logs: * Missing dependencies: * These modules do not appear in the page. It is possible that the * JavaScript file is not in the page or that the module name is wrong @@ -185,16 +185,27 @@ failed = odoo.__DEBUG__.get_failed_jobs(); var unloaded = _.filter(debug_jobs, function (job) { return job.missing; }); - var log = [(_.isEmpty(failed) ? (_.isEmpty(unloaded) ? 'info' : 'warning' ) : 'error') + ':', 'Some modules could not be started']; - if (missing.length) log.push('\nMissing dependencies: ', missing); - if (!_.isEmpty(failed)) log.push('\nFailed modules: ', _.pluck(failed, 'name')); - if (!_.isEmpty(rejected)) log.push('\nRejected modules: ', rejected); - if (!_.isEmpty(rejected_linked))log.push('\nRejected linked modules:', rejected_linked); - if (!_.isEmpty(unloaded)) log.push('\nNon loaded modules: ', _.pluck(unloaded, 'name')); - if (odoo.debug && !_.isEmpty(debug_jobs)) log.push('\nDebug: ', debug_jobs); - if (odoo.debug || !_.isEmpty(failed) || !_.isEmpty(unloaded)) { - console[_.isEmpty(failed) || _.isEmpty(unloaded) ? 'info' : 'error'].apply(console, log); + var log = console[_.isEmpty(failed) || _.isEmpty(unloaded) ? 'info' : 'error'].bind(console); + log((_.isEmpty(failed) ? (_.isEmpty(unloaded) ? 'info' : 'warning') : 'error') + ': Some modules could not be started'); + if (missing.length) { + log('Missing dependencies: ', missing); + } + if (!_.isEmpty(failed)) { + log('Failed modules: ', _.pluck(failed, 'name')); + } + if (!_.isEmpty(rejected)) { + log('Rejected modules: ', rejected); + } + if (!_.isEmpty(rejected_linked)) { + log('Rejected linked modules: ', rejected_linked); + } + if (!_.isEmpty(unloaded)) { + log('Non loaded modules: ', _.pluck(unloaded, 'name')); + } + if (odoo.debug && !_.isEmpty(debug_jobs)) { + log('Debug: ', debug_jobs); + } } } odoo.__DEBUG__.js_modules = { diff --git a/addons/web_editor/static/src/js/backend.js b/addons/web_editor/static/src/js/backend.js index 6b167c59edd..31e92228767 100644 --- a/addons/web_editor/static/src/js/backend.js +++ b/addons/web_editor/static/src/js/backend.js @@ -301,6 +301,8 @@ var FieldTextHtml = AbstractField.extend({ old_initialize_content: function () { this.$el.closest('.modal-body').css('max-height', 'none'); this.$iframe = this.$el.find('iframe'); + // deactivate any button to avoid saving a not ready iframe + $('.o_cp_buttons, .o_statusbar_buttons').find('button').addClass('o_disabled').attr('disabled', true); this.document = null; this.$body = $(); this.$content = $(); @@ -316,6 +318,8 @@ var FieldTextHtml = AbstractField.extend({ this.render(); this.add_button(); this.loaded = true; + // reactivate all the buttons when the field's content (the iframe) is loaded + $('.o_cp_buttons, .o_statusbar_buttons').find('button').removeClass('o_disabled').attr('disabled', false); setTimeout(self.resize, 0); }, on_editor_loaded: function (EditorBar) { diff --git a/addons/web_editor/static/src/js/widgets.js b/addons/web_editor/static/src/js/widgets.js index 0036a0783b7..5fc48b38071 100644 --- a/addons/web_editor/static/src/js/widgets.js +++ b/addons/web_editor/static/src/js/widgets.js @@ -617,7 +617,7 @@ var fontIconsDialog = Widget.extend({ e.stopPropagation(); this.$('#fa-icon').val(e.target.getAttribute('data-id')); - $(".font-icons-icon").removeClass("o_selected"); + this.$(".font-icons-icon").removeClass("o_selected"); $(e.target).addClass("o_selected"); }, 'dblclick .font-icons-icon': function () { @@ -743,7 +743,7 @@ var fontIconsDialog = Widget.extend({ continue; case '': continue; default: - $(".font-icons-icon").removeClass("o_selected").filter("[data-alias*=',"+cls+",']").addClass("o_selected"); + this.$(".font-icons-icon").removeClass("o_selected").filter("[data-alias*=',"+cls+",']").addClass("o_selected"); if (this.alias.indexOf(cls) !== -1) { this.$('#fa-icon').val(cls); } diff --git a/addons/website/controllers/main.py b/addons/website/controllers/main.py index 1940cba837c..ca2d3522ae6 100644 --- a/addons/website/controllers/main.py +++ b/addons/website/controllers/main.py @@ -64,7 +64,7 @@ class Website(Home): @http.route('/', type='http', auth="public", website=True) def index(self, **kw): page = 'homepage' - main_menu = request.env.ref('website.main_menu', raise_if_not_found=False) + main_menu = request.website.menu_id or request.env.ref('website.main_menu', raise_if_not_found=False) if main_menu: first_menu = main_menu.child_id and main_menu.child_id[0] if first_menu: diff --git a/addons/website/data/website_data.xml b/addons/website/data/website_data.xml index 9344ed70e4a..e2d702596a2 100644 --- a/addons/website/data/website_data.xml +++ b/addons/website/data/website_data.xml @@ -94,7 +94,7 @@ <record id="website.library_image_02" model="ir.attachment"> <field name="public" eval="True"/> <field name="name">Website Library Image 02</field> - <field name="datas_fname">deliver.png</field> + <field name="datas_fname">deliver.jpg</field> <field name="res_model">ir.ui.view</field> <field name="type">url</field> <field name="url">/website/static/src/img/library/deliver.jpg</field> @@ -139,7 +139,7 @@ <record id="website.library_image_07" model="ir.attachment"> <field name="public" eval="True"/> <field name="name">Website Library Image 07</field> - <field name="datas_fname">mobile_device.png</field> + <field name="datas_fname">mobile_device.jpg</field> <field name="res_model">ir.ui.view</field> <field name="type">url</field> <field name="url">/website/static/src/img/library/mobile_device.jpg</field> diff --git a/addons/website/models/res_partner.py b/addons/website/models/res_partner.py index d0f4a76b199..d3a3b1bc9f9 100644 --- a/addons/website/models/res_partner.py +++ b/addons/website/models/res_partner.py @@ -21,7 +21,7 @@ class Partner(models.Model): return False params = { 'center': '%s, %s %s, %s' % (self.street or '', self.city or '', self.zip or '', self.country_id and self.country_id.name_get()[0][1] or ''), - 'size': "%sx%s" % (height, width), + 'size': "%sx%s" % (width, height), 'zoom': zoom, 'sensor': 'false', 'key': google_maps_api_key, diff --git a/addons/website/static/src/js/website.snippets.animation.js b/addons/website/static/src/js/website.snippets.animation.js index d0547dcf9db..1213b986472 100644 --- a/addons/website/static/src/js/website.snippets.animation.js +++ b/addons/website/static/src/js/website.snippets.animation.js @@ -413,7 +413,11 @@ animation.registry.share = animation.Class.extend({ animation.registry.media_video = animation.Class.extend({ selector: ".media_iframe_video", start: function () { - if (!this.$target.has('> iframe').length) { + // TODO: this code should be refactored to make more sense and be better + // integrated with Odoo (this refactoring should be done in master). + this.$target.find('iframe').remove(); + + if (!this.$target.has('.media_iframe_video_size').length) { var editor = '<div class="css_editable_mode_display"> </div>'; var size = '<div class="media_iframe_video_size"> </div>'; this.$target.html(editor+size); diff --git a/addons/website_blog/views/website_blog_templates.xml b/addons/website_blog/views/website_blog_templates.xml index f6f513668ac..bcaa3a4c2aa 100644 --- a/addons/website_blog/views/website_blog_templates.xml +++ b/addons/website_blog/views/website_blog_templates.xml @@ -102,6 +102,7 @@ <t t-set="head"> <link t-att-href="'/blog/%s/feed' % (blog.id)" type="application/atom+xml" rel="alternate" title="Atom Feed"/> </t> + <div class="oe_structure"/> <div class="container"> <t t-call="website.pager" > <t t-set="classname">pull-right</t> diff --git a/addons/website_event/controllers/main.py b/addons/website_event/controllers/main.py index fe260e997f3..d4be8fb5132 100644 --- a/addons/website_event/controllers/main.py +++ b/addons/website_event/controllers/main.py @@ -149,7 +149,7 @@ class WebsiteEventController(http.Controller): # page not found values['path'] = re.sub(r"^website_event\.", '', page) values['from_template'] = 'website_event.default_page' # .strip('website_event.') - page = 'website.page_404' + page = 'website.%s' % (request.website.is_publisher() and 'page_404' or '404') return request.render(page, values) diff --git a/addons/website_event/views/event_templates.xml b/addons/website_event/views/event_templates.xml index 34fcee7f2ac..362ffc3cd0e 100644 --- a/addons/website_event/views/event_templates.xml +++ b/addons/website_event/views/event_templates.xml @@ -544,7 +544,7 @@ <h4 class="modal-title" id="myModalLabel"><strong>Attendees</strong></h4> </div> <div class="modal-body"> - <div class=""> + <div class="container"> <t t-set="counter_type" t-value="1"/> <t t-set="counter" t-value="0"/> <t t-foreach="tickets" t-as="ticket"> diff --git a/addons/website_portal/views/website_portal_templates.xml b/addons/website_portal/views/website_portal_templates.xml index b2fe4f5676e..6e3a1b3f271 100644 --- a/addons/website_portal/views/website_portal_templates.xml +++ b/addons/website_portal/views/website_portal_templates.xml @@ -198,7 +198,8 @@ <div class="clearfix" /> <div t-attf-class="form-group #{error.get('company_name') and 'has-error' or ''} col-lg-6"> <label class="control-label label-optional" for="company_name">Company Name</label> - <input type="text" name="company_name" class="form-control" t-att-value="company_name or partner.company_name"/> + <p t-if="partner.commercial_partner_id.is_company" class="form-control" t-esc="partner.commercial_company_name"/> + <input t-else="" type="text" name="company_name" class="form-control" t-att-value="company_name or partner.company_name"/> </div> <div t-if="has_check_vat" t-attf-class="form-group #{error.get('vat') and 'has-error' or ''} col-lg-6"> <label class="control-label label-optional" for="vat">VAT Number</label> diff --git a/addons/website_portal_sale/models/payment.py b/addons/website_portal_sale/models/payment.py index 3302ac87967..176f041847a 100644 --- a/addons/website_portal_sale/models/payment.py +++ b/addons/website_portal_sale/models/payment.py @@ -1,7 +1,7 @@ # -*- coding: utf-8 -*- # Part of Odoo. See LICENSE file for full copyright and licensing details. import logging -from odoo import api, fields, models +from odoo import _, api, fields, models from odoo.tools import float_compare _logger = logging.getLogger(__name__) @@ -81,4 +81,16 @@ class PaymentTransaction(models.Model): _logger.info('<%s> transaction pending/to confirm manually, sending quote email for order %s (ID %s)', acquirer_name, tx.sale_order_id.name, tx.sale_order_id.id) tx.sale_order_id.force_quotation_send() else: - _logger.warning('<%s> transaction MISMATCH for order %s (ID %s)', acquirer_name, tx.sale_order_id.name, tx.sale_order_id.id) + _logger.warning( + '<%s> transaction AMOUNT MISMATCH for order %s (ID %s): expected %r, got %r', + acquirer_name, tx.sale_order_id.name, tx.sale_order_id.id, + tx.sale_order_id.amount_total, tx.amount, + ) + tx.sale_order_id.message_post( + subject=_("Amount Mismatch (%s)") % acquirer_name, + body=_("The sale order was not confirmed despite response from the acquirer (%s): SO amount is %r but acquirer replied with %r.") % ( + acquirer_name, + tx.sale_order_id.amount_total, + tx.amount, + ) + ) diff --git a/addons/website_sale/controllers/main.py b/addons/website_sale/controllers/main.py index 5517b3ba508..d3235391fe2 100644 --- a/addons/website_sale/controllers/main.py +++ b/addons/website_sale/controllers/main.py @@ -373,8 +373,8 @@ class WebsiteSale(http.Controller): def cart_update(self, product_id, add_qty=1, set_qty=0, **kw): request.website.sale_get_order(force_create=1)._cart_update( product_id=int(product_id), - add_qty=float(add_qty), - set_qty=float(set_qty), + add_qty=add_qty, + set_qty=set_qty, attributes=self._filter_attributes(**kw), ) return request.redirect("/shop/cart") @@ -388,7 +388,6 @@ class WebsiteSale(http.Controller): if order.state != 'draft': request.website.sale_reset() return {} - value = order._cart_update(product_id=product_id, line_id=line_id, add_qty=add_qty, set_qty=set_qty) if not order.cart_quantity: request.website.sale_reset() diff --git a/addons/website_sale/models/sale_order.py b/addons/website_sale/models/sale_order.py index 335e9796b54..743871c5686 100644 --- a/addons/website_sale/models/sale_order.py +++ b/addons/website_sale/models/sale_order.py @@ -113,6 +113,17 @@ class SaleOrder(models.Model): """ Add or set product quantity, add_qty can be negative """ self.ensure_one() SaleOrderLineSudo = self.env['sale.order.line'].sudo() + + try: + if add_qty: + add_qty = float(add_qty) + except ValueError: + add_qty = 1 + try: + if set_qty: + set_qty = float(set_qty) + except ValueError: + set_qty = 0 quantity = 0 order_line = False if self.state != 'draft': diff --git a/addons/website_sale/static/src/js/website_sale.js b/addons/website_sale/static/src/js/website_sale.js index f7a8fa777bd..7d4d64d3932 100644 --- a/addons/website_sale/static/src/js/website_sale.js +++ b/addons/website_sale/static/src/js/website_sale.js @@ -133,6 +133,9 @@ odoo.define('website_sale.website_sale', function (require) { return; } var value = parseInt($input.val() || 0, 10); + if (isNaN(value)) { + value = 1; + } var $dom = $(this).closest('tr'); //var default_price = parseFloat($dom.find('.text-danger > span.oe_currency_value').text()); var $dom_optional = $dom.nextUntil(':not(.optional_product.info)'); @@ -151,7 +154,11 @@ odoo.define('website_sale.website_sale', function (require) { 'set_qty': value }).then(function (data) { $input.data('update_change', false); - if (value !== parseInt($input.val() || 0, 10)) { + var check_value = parseInt($input.val() || 0, 10); + if (isNaN(check_value)) { + check_value = 1; + } + if (value !== check_value) { $input.trigger('change'); return; } diff --git a/addons/website_sale/static/src/js/website_sale_validate.js b/addons/website_sale/static/src/js/website_sale_validate.js index dce80543dc3..614e8901484 100644 --- a/addons/website_sale/static/src/js/website_sale_validate.js +++ b/addons/website_sale/static/src/js/website_sale_validate.js @@ -16,8 +16,13 @@ $(document).ready(function () { return ajax.jsonRpc('/shop/payment/get_status/' + order_id, 'call', { }).then(function (result) { _poll_nbr += 1; - if(result.recall && _poll_nbr <= 5){ - setTimeout(function () { payment_transaction_poll_status(); }, 1000); + if(result.recall) { + if (_poll_nbr < 20){ + setTimeout(function () { payment_transaction_poll_status(); }, Math.ceil(_poll_nbr / 3) * 1000); + } + else { + result.message = "<i class='fa fa-warning' />"; + } } $('div.oe_website_sale_tx_status').html(result.message); }); diff --git a/addons/website_sale/views/templates.xml b/addons/website_sale/views/templates.xml index fcf24c81615..339645a46aa 100644 --- a/addons/website_sale/views/templates.xml +++ b/addons/website_sale/views/templates.xml @@ -1471,7 +1471,7 @@ <div class="container oe_website_sale"> <div class="row"> <div class="col-md-9"> - <h1 class="mb32">Order <em t-field="order.name" /> Confirmed</h1> + <h1 class="mb32">Order <em t-field="order.name" /></h1> <div class="thanks_msg"> <h2>Thank you for your order. <a class="btn btn-primary pull-right hidden-xs" href="/shop/print" target="_blank"><i class="fa fa-print"></i> Print diff --git a/addons/website_sale_options/controllers/main.py b/addons/website_sale_options/controllers/main.py index 4c9ba8e6502..1c84f9e460f 100644 --- a/addons/website_sale_options/controllers/main.py +++ b/addons/website_sale_options/controllers/main.py @@ -33,8 +33,8 @@ class WebsiteSaleOptions(WebsiteSale): if add_qty or set_qty: value = order._cart_update( product_id=int(product_id), - add_qty=int(add_qty), - set_qty=int(set_qty), + add_qty=add_qty, + set_qty=set_qty, attributes=attributes, optional_product_ids=optional_product_ids ) |
