diff --git a/addons/account/models/account.py b/addons/account/models/account.py index 32e90d115eb..718aef311e7 100644 --- a/addons/account/models/account.py +++ b/addons/account/models/account.py @@ -464,6 +464,12 @@ class AccountJournal(models.Model): res += [(journal.id, name)] return res + @api.model + def name_search(self, name='', args=None, operator='ilike', limit=100): + args = args or [] + recs = self.search(['|', ('code', operator, name), ('name', operator, name)] + args, limit=limit) + return recs.name_get() + @api.multi @api.depends('company_id') def _belong_to_company(self): diff --git a/addons/account/models/account_bank_statement.py b/addons/account/models/account_bank_statement.py index ab7fa0f307f..857ffc3e71d 100644 --- a/addons/account/models/account_bank_statement.py +++ b/addons/account/models/account_bank_statement.py @@ -403,8 +403,6 @@ class AccountBankStatementLine(models.Model): for line in self: if line.journal_entry_ids.ids: raise UserError(_('In order to delete a bank statement line, you must first cancel it to delete related journal items.')) - if line.move_name: - raise UserError(_('It is not allowed to delete a bank statement line that already created a journal entry since it would create a gap in the numbering. You should create the journal entry again and cancel it thanks to a regular revert.')) return super(AccountBankStatementLine, self).unlink() @api.multi diff --git a/addons/account/models/account_invoice.py b/addons/account/models/account_invoice.py index 458dfa2f34a..cb013f403db 100644 --- a/addons/account/models/account_invoice.py +++ b/addons/account/models/account_invoice.py @@ -481,6 +481,8 @@ class AccountInvoice(models.Model): payment_term_id = False fiscal_position = False bank_id = False + warning = {} + domain = {} company_id = self.company_id.id p = self.partner_id if not company_id else self.partner_id.with_context(force_company=company_id) type = self.type @@ -515,7 +517,6 @@ class AccountInvoice(models.Model): } if p.invoice_warn == 'block': self.partner_id = False - return {'warning': warning} self.account_id = account_id self.payment_term_id = payment_term_id @@ -525,8 +526,14 @@ class AccountInvoice(models.Model): bank_ids = p.commercial_partner_id.bank_ids bank_id = bank_ids[0].id if bank_ids else False self.partner_bank_id = bank_id - return {'domain': {'partner_bank_id': [('id', 'in', bank_ids.ids)]}} - return {} + domain = {'partner_bank_id': [('id', 'in', bank_ids.ids)]} + + res = {} + if warning: + res['warning'] = warning + if domain: + res['domain'] = domain + return res @api.multi def get_delivery_partner_id(self): @@ -1019,6 +1026,20 @@ class AccountInvoice(models.Model): result.append((0, 0, values)) return result + def _get_refund_common_fields(self): + return ['partner_id', 'payment_term_id', 'account_id', 'currency_id', 'journal_id'] + + def _get_refund_prepare_fields(self): + return ['name', 'reference', 'comment', 'date_due'] + + def _get_refund_modify_read_fields(self): + read_fields = ['type', 'number', 'invoice_line_ids', 'tax_line_ids', 'date'] + return self._get_refund_common_fields() + self._get_refund_prepare_fields() + read_fields + + def _get_refund_copy_fields(self): + copy_fields = ['company_id', 'user_id', 'fiscal_position_id'] + return self._get_refund_common_fields() + self._get_refund_prepare_fields() + copy_fields + @api.model def _prepare_refund(self, invoice, date_invoice=None, date=None, description=None, journal_id=None): """ Prepare the dict of values to create the new refund from the invoice. @@ -1034,8 +1055,7 @@ class AccountInvoice(models.Model): :return: dict of value to create() the refund """ values = {} - for field in ['name', 'reference', 'comment', 'date_due', 'partner_id', 'company_id', - 'account_id', 'currency_id', 'payment_term_id', 'user_id', 'fiscal_position_id']: + for field in self._get_refund_copy_fields(): if invoice._fields[field].type == 'many2one': values[field] = invoice[field].id else: @@ -1369,6 +1389,12 @@ class AccountInvoiceLine(models.Model): """ pass + @api.multi + def unlink(self): + if self.filtered(lambda r: r.invoice_id and r.invoice_id.state != 'draft'): + raise UserError(_('You can only delete an invoice line if the invoice is in draft state.')) + return super(AccountInvoiceLine, self).unlink() + class AccountInvoiceTax(models.Model): _name = "account.invoice.tax" _description = "Invoice Tax" diff --git a/addons/account/models/account_move.py b/addons/account/models/account_move.py index edd0a547b29..7ab0343ed2c 100644 --- a/addons/account/models/account_move.py +++ b/addons/account/models/account_move.py @@ -228,8 +228,8 @@ class AccountMove(models.Model): 'date': date, 'journal_id': journal_id.id if journal_id else self.journal_id.id, 'ref': _('reversal of: ') + self.name}) - for acm_line in reversed_move.line_ids: - acm_line.with_context(check_move_validity=False).write({ + for acm_line in reversed_move.line_ids.with_context(check_move_validity=False): + acm_line.write({ 'debit': acm_line.credit, 'credit': acm_line.debit, 'amount_currency': -acm_line.amount_currency diff --git a/addons/account/models/account_payment.py b/addons/account/models/account_payment.py index 38c5a2a9380..13705d23ca2 100644 --- a/addons/account/models/account_payment.py +++ b/addons/account/models/account_payment.py @@ -52,7 +52,7 @@ class account_abstract_payment(models.AbstractModel): @api.constrains('amount') def _check_amount(self): if not self.amount > 0.0: - raise ValidationError('The payment amount must be strictly positive.') + raise ValidationError(_('The payment amount must be strictly positive.')) @api.one @api.depends('payment_type', 'journal_id') @@ -410,8 +410,16 @@ class account_payment(models.Model): amount_wo = total_payment_company_signed - total_residual_company_signed else: amount_wo = total_residual_company_signed - total_payment_company_signed - debit_wo = amount_wo > 0 and amount_wo or 0.0 - credit_wo = amount_wo < 0 and -amount_wo or 0.0 + # Align the sign of the secondary currency writeoff amount with the sign of the writeoff + # amount in the company currency + if amount_wo > 0: + debit_wo = amount_wo + credit_wo = 0.0 + amount_currency_wo = abs(amount_currency_wo) + else: + debit_wo = 0.0 + credit_wo = -amount_wo + amount_currency_wo = -abs(amount_currency_wo) writeoff_line['name'] = self.writeoff_label writeoff_line['account_id'] = self.writeoff_account_id.id writeoff_line['debit'] = debit_wo diff --git a/addons/account/static/src/js/account_payment_widget.js b/addons/account/static/src/js/account_payment_widget.js index a7f2b64c413..2c9e947d32a 100644 --- a/addons/account/static/src/js/account_payment_widget.js +++ b/addons/account/static/src/js/account_payment_widget.js @@ -7,6 +7,7 @@ var formats = require('web.formats'); var Model = require('web.Model'); var QWeb = core.qweb; +var _t = core._t; var ShowPaymentLineWidget = form_common.AbstractField.extend({ render_value: function() { @@ -49,7 +50,7 @@ var ShowPaymentLineWidget = form_common.AbstractField.extend({ }), 'html': true, 'placement': 'left', - 'title': 'Payment Information', + 'title': _t('Payment Information'), 'trigger': 'focus', 'delay': { "show": 0, "hide": 100 }, }; @@ -89,4 +90,4 @@ var ShowPaymentLineWidget = form_common.AbstractField.extend({ core.form_widget_registry.add('payment', ShowPaymentLineWidget); -}); \ No newline at end of file +}); diff --git a/addons/account/views/account_invoice_view.xml b/addons/account/views/account_invoice_view.xml index 41f7de14856..35a95b5d45e 100644 --- a/addons/account/views/account_invoice_view.xml +++ b/addons/account/views/account_invoice_view.xml @@ -321,7 +321,7 @@ - + diff --git a/addons/account/views/account_view.xml b/addons/account/views/account_view.xml index fb34e891b0b..c880899b0fa 100644 --- a/addons/account/views/account_view.xml +++ b/addons/account/views/account_view.xml @@ -141,7 +141,7 @@ id="account.action_all_partner_invoices" name="All Invoices" res_model="account.invoice" - domain="[('partner_id', '=', active_id)]" + domain="[('partner_id', 'child_of', active_id)]" src_model="res.partner"/> account.journal.tree account.journal + 1 diff --git a/addons/account/wizard/account_invoice_refund.py b/addons/account/wizard/account_invoice_refund.py index bea13ffbf35..c449bbfcb6b 100644 --- a/addons/account/wizard/account_invoice_refund.py +++ b/addons/account/wizard/account_invoice_refund.py @@ -76,13 +76,7 @@ class AccountInvoiceRefund(models.TransientModel): to_reconcile_lines += tmpline to_reconcile_lines.filtered(lambda l: l.reconciled == False).reconcile() if mode == 'modify': - invoice = inv.read( - ['name', 'type', 'number', 'reference', - 'comment', 'date_due', 'partner_id', - 'partner_insite', 'partner_contact', - 'partner_ref', 'payment_term_id', 'account_id', - 'currency_id', 'invoice_line_ids', 'tax_line_ids', - 'journal_id', 'date']) + invoice = inv.read(inv_obj._get_refund_modify_read_fields()) invoice = invoice[0] del invoice['id'] invoice_lines = inv_line_obj.browse(invoice['invoice_line_ids']) @@ -100,9 +94,11 @@ class AccountInvoiceRefund(models.TransientModel): 'origin': inv.origin, 'fiscal_position_id': inv.fiscal_position_id.id, }) - for field in ('partner_id', 'account_id', 'currency_id', - 'payment_term_id', 'journal_id'): + for field in inv_obj._get_refund_common_fields(): + if inv_obj._fields[field].type == 'many2one': invoice[field] = invoice[field] and invoice[field][0] + else: + invoice[field] = invoice[field] or False inv_refund = inv_obj.create(invoice) if inv_refund.payment_term_id.id: inv_refund._onchange_payment_term_date_invoice() diff --git a/addons/account_bank_statement_import/account_bank_statement_import.py b/addons/account_bank_statement_import/account_bank_statement_import.py index 2d45a95612d..07ab4c7dc6e 100644 --- a/addons/account_bank_statement_import/account_bank_statement_import.py +++ b/addons/account_bank_statement_import/account_bank_statement_import.py @@ -43,6 +43,8 @@ class AccountBankStatementImport(models.TransientModel): if not journal: # The active_id is passed in context so the wizard can call import_file again once the journal is created return self.with_context(active_id=self.ids[0])._journal_creation_wizard(currency, account_number) + if not journal.default_debit_account_id or not journal.default_credit_account_id: + raise UserError(_('You have to set a Default Debit Account and a Default Credit Account for the journal: %s') % (journal.name,)) # Prepare statement data to be used for bank statements creation stmts_vals = self._complete_stmts_vals(stmts_vals, journal, account_number) # Create the bank statements @@ -220,6 +222,9 @@ class AccountBankStatementImport(models.TransientModel): filtered_st_lines.append(line_vals) else: ignored_statement_lines_import_ids.append(line_vals['unique_import_id']) + if 'balance_start' in st_vals: + st_vals['balance_start'] += line_vals['amount'] + if len(filtered_st_lines) > 0: # Remove values that won't be used to create records st_vals.pop('transactions', None) diff --git a/addons/account_bank_statement_import/i18n/account_bank_statement_import.pot b/addons/account_bank_statement_import/i18n/account_bank_statement_import.pot index 43cc6e38d59..32c6952c711 100644 --- a/addons/account_bank_statement_import/i18n/account_bank_statement_import.pot +++ b/addons/account_bank_statement_import/i18n/account_bank_statement_import.pot @@ -468,6 +468,12 @@ msgstr "" msgid "You have already imported that file." msgstr "" +#. module: account_bank_statement_import +#: code:addons/account_bank_statement_import/account_bank_statement_import.py:47 +#, python-format +msgid "You have to set a Default Debit Account and a Default Credit Account for the journal: %s" +msgstr "" + #. module: account_bank_statement_import #: model:ir.ui.view,arch_db:account_bank_statement_import.account_bank_statement_import_view msgid "_Import" diff --git a/addons/account_check_printing/models/account_payment.py b/addons/account_check_printing/models/account_payment.py index 13b9e39cb22..ef33dc05ae4 100644 --- a/addons/account_check_printing/models/account_payment.py +++ b/addons/account_check_printing/models/account_payment.py @@ -29,13 +29,7 @@ class AccountRegisterPayments(models.TransientModel): def _onchange_amount(self): if hasattr(super(AccountRegisterPayments, self), '_onchange_amount'): super(AccountRegisterPayments, self)._onchange_amount() - # TODO: merge, refactor and complete the amount_to_text and amount_to_text_en classes - check_amount_in_words = amount_to_text_en.amount_to_text(math.floor(self.amount), lang='en', currency='') - check_amount_in_words = check_amount_in_words.replace(' and Zero Cent', '') # Ugh - decimals = self.amount % 1 - if decimals >= 10**-2: - check_amount_in_words += _(' and %s/100') % str(int(round(float_round(decimals*100, precision_rounding=1)))) - self.check_amount_in_words = check_amount_in_words + self.check_amount_in_words = self.env['account.payment']._get_check_amount_in_words(self.amount) def get_payment_vals(self): res = super(AccountRegisterPayments, self).get_payment_vals() @@ -56,6 +50,15 @@ class AccountPayment(models.Model): help="The selected journal is configured to print check numbers. If your pre-printed check paper already has numbers " "or if the current numbering is wrong, you can change it in the journal configuration page.") + def _get_check_amount_in_words(self, amount): + # TODO: merge, refactor and complete the amount_to_text and amount_to_text_en classes + check_amount_in_words = amount_to_text_en.amount_to_text(math.floor(amount), lang='en', currency='') + check_amount_in_words = check_amount_in_words.replace(' and Zero Cent', '') # Ugh + decimals = amount % 1 + if decimals >= 10**-2: + check_amount_in_words += _(' and %s/100') % str(int(round(float_round(decimals*100, precision_rounding=1)))) + return check_amount_in_words + @api.onchange('journal_id') def _onchange_journal_id(self): if hasattr(super(AccountPayment, self), '_onchange_journal_id'): @@ -67,12 +70,7 @@ class AccountPayment(models.Model): def _onchange_amount(self): if hasattr(super(AccountPayment, self), '_onchange_amount'): super(AccountPayment, self)._onchange_amount() - check_amount_in_words = amount_to_text_en.amount_to_text(math.floor(self.amount), lang='en', currency='') - check_amount_in_words = check_amount_in_words.replace(' and Zero Cent', '') # Ugh - decimals = self.amount % 1 - if decimals >= 10**-2: - check_amount_in_words += _(' and %s/100') % str(int(round(float_round(decimals*100, precision_rounding=1)))) - self.check_amount_in_words = check_amount_in_words + self.check_amount_in_words = self._get_check_amount_in_words(self.amount) def _check_communication(self, payment_method_id, communication): super(AccountPayment, self)._check_communication(payment_method_id, communication) diff --git a/addons/analytic/views/analytic_account_views.xml b/addons/analytic/views/analytic_account_views.xml index f061e1c5133..b6269c89ca3 100644 --- a/addons/analytic/views/analytic_account_views.xml +++ b/addons/analytic/views/analytic_account_views.xml @@ -51,7 +51,7 @@ - {'search_default_group_date': 1, 'search_default_group_journal': 1, 'default_account_id': active_id} + {'search_default_group_date': 1, 'default_account_id': active_id} [('account_id','=', active_id)] Costs & Revenues account.analytic.line diff --git a/addons/barcodes/models/barcodes.py b/addons/barcodes/models/barcodes.py index 08482581bb5..6c3d228895e 100644 --- a/addons/barcodes/models/barcodes.py +++ b/addons/barcodes/models/barcodes.py @@ -44,8 +44,8 @@ class BarcodeNomenclature(models.Model): if len(code) != 8: return -1 - sum1 = ean[1] + ean[3] + ean[5] - sum2 = ean[0] + ean[2] + ean[4] + ean[6] + sum1 = int(ean[1]) + int(ean[3]) + int(ean[5]) + sum2 = int(ean[0]) + int(ean[2]) + int(ean[4]) + int(ean[6]) total = sum1 + 3 * sum2 return int((10 - total % 10) % 10) diff --git a/addons/barcodes/tests/__init__.py b/addons/barcodes/tests/__init__.py new file mode 100644 index 00000000000..6188e48fb99 --- /dev/null +++ b/addons/barcodes/tests/__init__.py @@ -0,0 +1 @@ +from . import test_barcode_nomenclature diff --git a/addons/barcodes/tests/test_barcode_nomenclature.py b/addons/barcodes/tests/test_barcode_nomenclature.py new file mode 100644 index 00000000000..d3ef2903d79 --- /dev/null +++ b/addons/barcodes/tests/test_barcode_nomenclature.py @@ -0,0 +1,15 @@ +# -*- coding: utf-8 -*- +from odoo.tests import common + + +class TestBarcodeNomenclature(common.TransactionCase): + + def test_ean8_checksum(self): + barcode_nomenclature = self.env['barcode.nomenclature'] + ean8 = "87111125" + checksum = barcode_nomenclature.ean8_checksum(ean8) + self.assertEqual(checksum, int(ean8[-1])) + checksum = barcode_nomenclature.ean8_checksum("8711112") + self.assertEqual(checksum, -1) + checksum = barcode_nomenclature.ean8_checksum("871111256") + self.assertEqual(checksum, -1) diff --git a/addons/base_vat/models/res_partner.py b/addons/base_vat/models/res_partner.py index 1d051bcfd82..4eed2ef8472 100644 --- a/addons/base_vat/models/res_partner.py +++ b/addons/base_vat/models/res_partner.py @@ -223,10 +223,14 @@ class ResPartner(models.Model): return True # Norway VAT validation, contributed by Rolv Råen (adEgo) + # Support for MVA suffix contributed by Bringsvor Consulting AS (bringsvor@bringsvor.com) def check_vat_no(self, vat): - ''' + """ Check Norway VAT number.See http://www.brreg.no/english/coordination/number.html - ''' + """ + if len(vat) == 12 and vat.upper().endswith('MVA'): + vat = vat[:-3] # Strictly speaking we should enforce the suffix MVA but... + if len(vat) != 9: return False try: diff --git a/addons/board/static/src/less/dashboard.less b/addons/board/static/src/less/dashboard.less index 36f0727c0ac..b0b41e65e34 100644 --- a/addons/board/static/src/less/dashboard.less +++ b/addons/board/static/src/less/dashboard.less @@ -139,4 +139,5 @@ // Favorites menu in control panel .o_add_to_dashboard { display: none; // hidden by default + .o-search-options-dropdown-custom-li; } diff --git a/addons/calendar/models/mail_message.py b/addons/calendar/models/mail_message.py index df6810ebbf3..4d5c15ead18 100644 --- a/addons/calendar/models/mail_message.py +++ b/addons/calendar/models/mail_message.py @@ -25,7 +25,7 @@ class Message(models.Model): @api.model def _find_allowed_model_wise(self, doc_model, doc_dict): if doc_model == 'calendar.event': - order = self._context.get('order', self._order) + order = self._context.get('order', self.env[doc_model]._order) for virtual_id in self.env[doc_model].browse(doc_dict.keys()).get_recurrent_ids([], order=order): doc_dict.setdefault(virtual_id, doc_dict[get_real_ids(virtual_id)]) return super(Message, self)._find_allowed_model_wise(doc_model, doc_dict) diff --git a/addons/crm/models/crm_lead.py b/addons/crm/models/crm_lead.py index 49d2738fbef..2d45d6b6801 100644 --- a/addons/crm/models/crm_lead.py +++ b/addons/crm/models/crm_lead.py @@ -99,7 +99,7 @@ class Lead(models.Model): user_id = fields.Many2one('res.users', string='Salesperson', index=True, track_visibility='onchange', default=lambda self: self.env.user) referred = fields.Char('Referred By') - date_open = fields.Datetime('Assigned', readonly=True) + date_open = fields.Datetime('Assigned', readonly=True, default=fields.Datetime.now) day_open = fields.Float(compute='_compute_day_open', string='Days to Assign', store=True) day_close = fields.Float(compute='_compute_day_close', string='Days to Close', store=True) date_last_stage_update = fields.Datetime(string='Last Stage Update', index=True, default=fields.Datetime.now) @@ -690,7 +690,7 @@ class Lead(models.Model): email_split = tools.email_split(self.email_from) values = { 'name': name, - 'user_id': self.user_id.id, + 'user_id': self.env.context.get('default_user_id') or self.user_id.id, 'comment': self.description, 'team_id': self.team_id.id, 'parent_id': parent_id, @@ -896,29 +896,30 @@ class Lead(models.Model): 'nb_opportunities': 0, } - opportunities = self.search([('type', '=', 'opportunity'), ('user_id', '=', self._uid), ('activity_date_deadline', '!=', False)]) + opportunities = self.search([('type', '=', 'opportunity'), ('user_id', '=', self._uid)]) for opp in opportunities: # Expected closing - if opp.date_deadline: - date_deadline = fields.Date.from_string(opp.date_deadline) - if date_deadline == date.today(): - result['closing']['today'] += 1 - if date.today() <= date_deadline <= date.today() + timedelta(days=7): - result['closing']['next_7_days'] += 1 - if date_deadline < date.today(): - 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(): - result['activity']['today'] += 1 - if date.today() <= date_deadline <= date.today() + timedelta(days=7): - result['activity']['next_7_days'] += 1 - if date_deadline < date.today(): - result['activity']['overdue'] += 1 + if opp.activity_date_deadline: + if opp.date_deadline: + date_deadline = fields.Date.from_string(opp.date_deadline) + if date_deadline == date.today(): + result['closing']['today'] += 1 + if date.today() <= date_deadline <= date.today() + timedelta(days=7): + result['closing']['next_7_days'] += 1 + if date_deadline < date.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(): + result['activity']['today'] += 1 + if date.today() <= date_deadline <= date.today() + timedelta(days=7): + result['activity']['next_7_days'] += 1 + if date_deadline < date.today(): + result['activity']['overdue'] += 1 # Won in Opportunities - if opp.date_closed: + 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 opp.planned_revenue: diff --git a/addons/crm/models/res_config.py b/addons/crm/models/res_config.py index b3bed2ba043..49f901b4140 100644 --- a/addons/crm/models/res_config.py +++ b/addons/crm/models/res_config.py @@ -59,7 +59,10 @@ class CRMSettings(models.TransientModel): @api.onchange('default_generate_lead_from_alias') def _onchange_default_generate_lead_from_alias(self): - self.alias_prefix = 'info' if self.default_generate_lead_from_alias else False + if self.default_generate_lead_from_alias: + self.alias_prefix = self.alias_prefix or 'info' + else: + self.alias_prefix = False @api.model def get_default_alias_prefix(self, fields): diff --git a/addons/crm/wizard/crm_lead_to_opportunity.py b/addons/crm/wizard/crm_lead_to_opportunity.py index 4a46498c881..5f7442002a6 100644 --- a/addons/crm/wizard/crm_lead_to_opportunity.py +++ b/addons/crm/wizard/crm_lead_to_opportunity.py @@ -95,7 +95,9 @@ class Lead2OpportunityPartner(models.TransientModel): leads = self.env['crm.lead'].browse(vals.get('lead_ids')) for lead in leads: - partner_id = self._create_partner(lead.id, self.action, vals.get('partner_id') or lead.partner_id.id) + self_def_user = self.with_context(default_user_id=self.user_id.id) + partner_id = self_def_user._create_partner( + lead.id, self.action, vals.get('partner_id') or lead.partner_id.id) res = lead.convert_opportunity(partner_id, [], False) user_ids = vals.get('user_ids') @@ -135,9 +137,6 @@ class Lead2OpportunityPartner(models.TransientModel): leads = self.env['crm.lead'].browse(self._context.get('active_ids', [])) values.update({'lead_ids': leads.ids, 'user_ids': [self.user_id.id]}) self._convert_opportunity(values) - for lead in leads: - if lead.partner_id and lead.partner_id.user_id != lead.user_id: - self.env['res.partner'].browse(lead.partner_id.id).write({'user_id': lead.user_id.id}) return leads[0].redirect_opportunity_view() diff --git a/addons/hr_expense/views/hr_expense_views.xml b/addons/hr_expense/views/hr_expense_views.xml index 0fc37dd778f..efed47fd890 100644 --- a/addons/hr_expense/views/hr_expense_views.xml +++ b/addons/hr_expense/views/hr_expense_views.xml @@ -461,7 +461,6 @@ - + + + + {'invisible':['|', ('type', 'not in', ['product', 'consu']), ('product_variant_count', '>', 1)]} + + + {'invisible':['|', ('type', 'not in', ['product', 'consu']), ('product_variant_count', '>', 1)]} + + + {'invisible':['|', ('type', 'not in', ['product', 'consu']), ('product_variant_count', '>', 1)]} + + + {'invisible':['|', ('type', 'not in', ['product', 'consu']), ('product_variant_count', '>', 1)]} + + diff --git a/addons/stock/views/stock_quant_views.xml b/addons/stock/views/stock_quant_views.xml index a33039ce7bd..67997c74854 100644 --- a/addons/stock/views/stock_quant_views.xml +++ b/addons/stock/views/stock_quant_views.xml @@ -86,7 +86,7 @@ - + diff --git a/addons/stock_account/models/account_invoice.py b/addons/stock_account/models/account_invoice.py index aacdd6c7177..6291da2922f 100644 --- a/addons/stock_account/models/account_invoice.py +++ b/addons/stock_account/models/account_invoice.py @@ -38,7 +38,7 @@ class AccountInvoice(models.Model): cacc = accounts['expense'].id if dacc and cacc: price_unit = i_line._get_anglo_saxon_price_unit() - if inv.currency_id.id != company_currency: + if inv.currency_id != company_currency: currency_id = inv.currency_id.id amount_currency = i_line._get_price(company_currency, price_unit) else: @@ -99,6 +99,6 @@ class AccountInvoiceLine(models.Model): def get_invoice_line_account(self, type, product, fpos, company): if company.anglo_saxon_accounting and type in ('in_invoice', 'in_refund') and product and product.type == 'product': accounts = product.product_tmpl_id.get_product_accounts(fiscal_pos=fpos) - if accounts['stock_input']: + if product.categ_id.property_valuation != 'manual_periodic' and accounts['stock_input']: return accounts['stock_input'] return super(AccountInvoiceLine, self).get_invoice_line_account(type, product, fpos, company) diff --git a/addons/web/static/src/js/views/list_view_editable.js b/addons/web/static/src/js/views/list_view_editable.js index 6beb6a7a044..42987a64af1 100644 --- a/addons/web/static/src/js/views/list_view_editable.js +++ b/addons/web/static/src/js/views/list_view_editable.js @@ -634,6 +634,7 @@ ListView.include(/** @lends instance.web.ListView# */{ * @return {*} */ _next: function (next_record, options) { + options = options || {}; next_record = next_record || 'succ'; var self = this; return this.save_edition().then(function (saveInfo) { diff --git a/addons/web/static/src/js/views/pivot_view.js b/addons/web/static/src/js/views/pivot_view.js index 95e8d13e5ff..c40d17b41e2 100644 --- a/addons/web/static/src/js/views/pivot_view.js +++ b/addons/web/static/src/js/views/pivot_view.js @@ -201,8 +201,8 @@ var PivotView = View.extend({ this.initial_col_groupby = context.pivot_col_groupby || this.initial_col_groupby; } this.main_row.groupbys = group_by.length ? group_by : (context.pivot_row_groupby || this.initial_row_groupby.slice(0)); - this.main_col.groupbys = context.pivot_column_groupby || this.initial_col_groupby.slice(0); - this.active_measures = context.pivot_measures || this.active_measures; + this.main_col.groupbys = context.col_group_by || context.pivot_column_groupby || this.initial_col_groupby.slice(0); + this.active_measures = context.measures || context.pivot_measures || this.active_measures; this.domain = domain; this.context = context; diff --git a/addons/web_editor/static/src/js/rte.summernote.js b/addons/web_editor/static/src/js/rte.summernote.js index 60c096603d8..4ab1e7bed06 100644 --- a/addons/web_editor/static/src/js/rte.summernote.js +++ b/addons/web_editor/static/src/js/rte.summernote.js @@ -511,7 +511,12 @@ function prettify_html(html) { level--; } - result += token.trim().replace(/\s+/, ' '); + // don't trim inline content (which could change appearance) + if (!inline) { + token = token.trim(); + } + + result += token.replace(/\s+/, ' '); if (inline_level > level) { result += '\n'; diff --git a/addons/web_editor/static/src/js/snippets.editor.js b/addons/web_editor/static/src/js/snippets.editor.js index 20a96a91e84..42f2103f95f 100644 --- a/addons/web_editor/static/src/js/snippets.editor.js +++ b/addons/web_editor/static/src/js/snippets.editor.js @@ -107,7 +107,7 @@ data.Class = Widget.extend({ return $from.closest(selector, parentNode); }, all: function ($from) { - return $from ? $from.find(selector) : $(selector); + return $from ? cssFind($from, selector) : $(selector); }, is: function ($from) { return $from.is(selector); @@ -130,15 +130,31 @@ data.Class = Widget.extend({ }); }, all: is_children ? function ($from) { - return ($from || self.$editable).find(selector); + return cssFind($from || self.$editable, selector); } : function ($from) { - return $from ? $from.find(selector) : self.$editable.filter(selector).add(self.$editable.find(selector)); + $from = $from || self.$editable; + return $from.filter(selector).add(cssFind($from, selector)); }, is: function ($from) { return $from.is(selector); } }; } + + /** + * jQuery find function behavior is: + * $('A').find('A B') <=> $('A A B') + * The searches behavior to find options' DOM needs to be + * $('A').find('A B') <=> $('A B') + * This is what this function does. + * + * @param {jQuery} $from - the jQuery element(s) from which to search + * @param {string} selector - the CSS selector to match + * @returns {jQuery} + */ + function cssFind($from, selector) { + return $from.find('*').filter(selector); + } }, fetch_snippet_templates: function () { diff --git a/addons/web_editor/static/src/js/transcoder.js b/addons/web_editor/static/src/js/transcoder.js index 05068972532..6b42e028b3a 100644 --- a/addons/web_editor/static/src/js/transcoder.js +++ b/addons/web_editor/static/src/js/transcoder.js @@ -121,6 +121,16 @@ var getMatchedCSSRules = function (a) { } }); + // text-decoration rule is decomposed in -line, -color and -style. This is + // however not supported by many browser/mail clients and the editor does + // not allow to change -color and -style rule anyway + if (style['text-decoration-line']) { + style['text-decoration'] = style['text-decoration-line']; + delete style['text-decoration-line']; + delete style['text-decoration-color']; + delete style['text-decoration-style']; + } + return style; }; diff --git a/addons/website/models/ir_http.py b/addons/website/models/ir_http.py index c62486d34a0..1e65fd256a8 100644 --- a/addons/website/models/ir_http.py +++ b/addons/website/models/ir_http.py @@ -179,8 +179,8 @@ class Http(models.AbstractModel): nearest_lang = not func and cls.get_nearest_lang(path[1]) url_lang = nearest_lang and path[1] preferred_lang = ((cook_lang if cook_lang in langs else False) - or (not is_a_bot and cls.get_nearest_lang(request.lang)) - or request.website.default_lang_code) + or request.website.default_lang_code + or (not is_a_bot and cls.get_nearest_lang(request.lang))) request.lang = context['lang'] = nearest_lang or preferred_lang # if lang in url but not the displayed or default language --> change or remove diff --git a/addons/website/static/src/less/website.snippets.less b/addons/website/static/src/less/website.snippets.less index 5f96bbac0e6..cde2501e605 100644 --- a/addons/website/static/src/less/website.snippets.less +++ b/addons/website/static/src/less/website.snippets.less @@ -391,6 +391,7 @@ div.carousel { // Parallax Theme .parallax { position: relative; + z-index: 0; .oe_structure { > :first-child { diff --git a/addons/website/static/src/less/website.ui.less b/addons/website/static/src/less/website.ui.less index 8f4732024b4..b515ad40096 100644 --- a/addons/website/static/src/less/website.ui.less +++ b/addons/website/static/src/less/website.ui.less @@ -47,4 +47,10 @@ body.o_connected_user { font-size: 20px; } } + @media only screen and (max-width: @screen-xs-max) { + #oe_applications { + position: inherit; + z-index: 1002; + } + } } diff --git a/addons/website_blog/static/src/js/contentshare.js b/addons/website_blog/static/src/js/contentshare.js index 47cd88fc4a7..cfc02470f7f 100644 --- a/addons/website_blog/static/src/js/contentshare.js +++ b/addons/website_blog/static/src/js/contentshare.js @@ -22,9 +22,12 @@ popover_content += ''; } if($('.blog_title, .blog_content').hasClass('js_tweet')){ - var current_url = window.location.href; - var tweet_selected_text = this.getSelection('string').substring(0,option.maxLength-(current_url.length+option.author_name.length+7)); - var text = encodeURIComponent(_.str.sprintf('\"%s\" --@%s %s', tweet_selected_text, option.author_name, current_url)); + var tweet = '"%s" - %s'; + var baseLength = tweet.replace(/%s/g, '').length; + // Shorten the selected text to match the tweet max length + // Note: all (non-localhost) urls in a tweet have 23 characters https://support.twitter.com/articles/78124 + var selectedText = this.getSelection('string').substring(0, option.maxLength - baseLength - 23); + var text = encodeURIComponent(_.str.sprintf(tweet, selectedText, window.location.href)); popover_content += ''; } return popover_content; diff --git a/addons/website_blog/static/src/js/website_blog.js b/addons/website_blog/static/src/js/website_blog.js index 5c2d82f7646..7985e0ebecc 100644 --- a/addons/website_blog/static/src/js/website_blog.js +++ b/addons/website_blog/static/src/js/website_blog.js @@ -58,7 +58,7 @@ function shareArticle(event){ $(document).ready(function() { if ($('.website_blog').length) { - $(".js_tweet, .js_comment").share({'author_name': $('#blog_author').text()}); + $(".js_tweet, .js_comment").share({}); $('.cover_footer').on('click', page_transist); $('a[href^="#blog_content"]').on('click', animate); $('.o_twitter, .o_facebook, .o_linkedin, .o_google, .o_twitter_complete, .o_facebook_complete, .o_linkedin_complete, .o_google_complete').on('click', shareArticle); diff --git a/addons/website_form/static/src/js/website_form.js b/addons/website_form/static/src/js/website_form.js index f94371261d9..38f37a27340 100644 --- a/addons/website_form/static/src/js/website_form.js +++ b/addons/website_form/static/src/js/website_form.js @@ -167,13 +167,15 @@ odoo.define('website_form.animation', function (require) { // Special cases for dates and datetimes } else if ($(input).hasClass('o_website_form_date')) { - return !self.is_datetime_valid(input.value, 'date'); + if (!self.is_datetime_valid(input.value, 'date')) { + return true; + } } else if ($(input).hasClass('o_website_form_datetime')) { - return !self.is_datetime_valid(input.value, 'datetime'); - - } else { - return !input.checkValidity(); + if (!self.is_datetime_valid(input.value, 'datetime')) { + return true; + } } + return !input.checkValidity(); }) // Update field color if invalid or erroneous diff --git a/addons/website_mail/controllers/main.py b/addons/website_mail/controllers/main.py index c6fc592e3d8..546def6890a 100644 --- a/addons/website_mail/controllers/main.py +++ b/addons/website_mail/controllers/main.py @@ -34,6 +34,7 @@ def _message_post_helper(res_model='', res_id=None, message='', token='', token_ else: if not author_id: raise NotFound() + kw.pop('csrf_token', None) return record.with_context(mail_create_nosubscribe=nosubscribe).message_post(body=message, message_type=kw.pop('message_type', "comment"), subtype=kw.pop('subtype', "mt_comment"), diff --git a/addons/website_portal_sale/controllers/main.py b/addons/website_portal_sale/controllers/main.py index a83a15ce6a4..79d5d424313 100644 --- a/addons/website_portal_sale/controllers/main.py +++ b/addons/website_portal_sale/controllers/main.py @@ -223,10 +223,10 @@ class website_account(website_account): partner = request.env['res.users'].browse(request.uid).partner_id invoices = request.env['account.invoice'].sudo().search_count([('partner_id', '=', partner.id), ('state', 'not in', ['draft', 'cancel'])]) if invoices: - if (data.get('vat', partner.vat) or False) != partner.vat: + if 'vat' in partner and (data['vat'] or False) != (partner.vat or False): error['vat'] = 'error' error_message.append(_('Changing VAT number is not allowed once invoices have been issued for your account. Please contact us directly for this operation.')) - if data.get('name', partner.name) != partner.name: + if 'name' in data and (data['name'] or False) != (partner.name or False): error['name'] = 'error' error_message.append(_('Changing your name is not allowed once invoices have been issued for your account. Please contact us directly for this operation.')) return error, error_message diff --git a/addons/website_quote/models/sale_order.py b/addons/website_quote/models/sale_order.py index d84b6b6766b..d5f3f2483ab 100644 --- a/addons/website_quote/models/sale_order.py +++ b/addons/website_quote/models/sale_order.py @@ -76,6 +76,13 @@ class SaleOrder(models.Model): (2, 'Immediate after website order validation and save a token'), ], 'Payment', help="Require immediate payment by the customer when validating the order from the website quote") + @api.multi + def copy(self, default=None): + if self.template_id and self.template_id.number_of_days > 0: + default = dict(default or {}) + default['validity_date'] = fields.Date.to_string(datetime.now() + timedelta(self.template_id.number_of_days)) + return super(SaleOrder, self).copy(default=default) + @api.one def _compute_amount_undiscounted(self): total = 0.0 diff --git a/addons/website_sale/controllers/main.py b/addons/website_sale/controllers/main.py index 407a2e7cd11..1208625bc9f 100644 --- a/addons/website_sale/controllers/main.py +++ b/addons/website_sale/controllers/main.py @@ -321,7 +321,8 @@ class WebsiteSale(http.Controller): @http.route(['/shop/change_pricelist/'], type='http', auth="public", website=True) def pricelist_change(self, pl_id, **post): - if request.website.is_pricelist_available(pl_id.id): + if (pl_id.selectable or pl_id == request.env.user.partner_id.property_product_pricelist) \ + and request.website.is_pricelist_available(pl_id.id): request.session['website_sale_current_pl'] = pl_id.id request.website.sale_get_order(force_pricelist=pl_id.id) return request.redirect(request.httprequest.referrer or '/shop') diff --git a/addons/website_sale/models/product.py b/addons/website_sale/models/product.py index d5a8f77a72d..d3e1c6b893a 100644 --- a/addons/website_sale/models/product.py +++ b/addons/website_sale/models/product.py @@ -19,7 +19,7 @@ class ProductPricelist(models.Model): return self.env['website'].search([], limit=1) website_id = fields.Many2one('website', string="website", default=_default_website) - code = fields.Char(string='E-commerce Promotional Code') + code = fields.Char(string='E-commerce Promotional Code', groups="base.group_user") selectable = fields.Boolean(help="Allow the end user to choose this price list") def clear_cache(self): diff --git a/addons/website_sale/models/sale_order.py b/addons/website_sale/models/sale_order.py index 7e4e3c42bcf..15030fc510e 100644 --- a/addons/website_sale/models/sale_order.py +++ b/addons/website_sale/models/sale_order.py @@ -71,7 +71,7 @@ class SaleOrder(models.Model): if order.pricelist_id and order.partner_id: order_line = order._cart_find_product_line(product.id) if order_line: - pu = self.env['account.tax']._fix_tax_included_price(pu, product.taxes_id, order_line.tax_id) + pu = self.env['account.tax']._fix_tax_included_price(pu, product.taxes_id, order_line[0].tax_id) return { 'product_id': product_id, @@ -228,7 +228,7 @@ class Website(models.Model): if not pricelists: # no pricelist for this country, or no GeoIP pricelists |= all_pl.filtered(lambda pl: not show_visible or pl.selectable or pl.id in (current_pl, order_pl)) else: - pricelists |= all_pl.filtered(lambda pl: not show_visible and pl.code) + pricelists |= all_pl.filtered(lambda pl: not show_visible and pl.sudo().code) # This method is cached, must not return records! See also #8795 return pricelists.ids @@ -397,6 +397,9 @@ class Website(models.Model): partner.write({'last_website_so_id': sale_order.id}) if sale_order: + # case when user emptied the cart + if not request.session.get('sale_order_id'): + request.session['sale_order_id'] = sale_order.id # check for change of pricelist with a coupon pricelist_id = pricelist_id or partner.property_product_pricelist.id @@ -435,7 +438,7 @@ class Website(models.Model): update_pricelist = True if code and code != sale_order.pricelist_id.code: - code_pricelist = self.env['product.pricelist'].search([('code', '=', code)], limit=1) + code_pricelist = self.env['product.pricelist'].sudo().search([('code', '=', code)], limit=1) if code_pricelist: pricelist_id = code_pricelist.id update_pricelist = True diff --git a/addons/website_sale/static/src/js/website_sale.js b/addons/website_sale/static/src/js/website_sale.js index 1df1fcd0e97..6ac948b4fe1 100644 --- a/addons/website_sale/static/src/js/website_sale.js +++ b/addons/website_sale/static/src/js/website_sale.js @@ -250,6 +250,7 @@ odoo.define('website_sale.website_sale', function (require) { if ($(".decimal_precision").length) { precision = parseInt($(".decimal_precision").last().data('precision')); + if (!precision) { precision = 0; } //todo: remove me in master/saas-17 } var formatted = _.str.sprintf('%.' + precision + 'f', price).split('.'); formatted[0] = utils.insert_thousand_seps(formatted[0]); diff --git a/addons/website_sale/views/templates.xml b/addons/website_sale/views/templates.xml index 6dcff342f5a..0387badeaa8 100644 --- a/addons/website_sale/views/templates.xml +++ b/addons/website_sale/views/templates.xml @@ -600,7 +600,7 @@ -

+

trans.src) gives the original value back value0 = field.translate(lambda term: None, record[fname]) value1 = field.translate({trans.src: trans.value}.get, value0) + # don't check the reverse if no translation happened + if value0 == value1: + continue value2 = field.translate({trans.value: trans.src}.get, value1) if value2 != value0: raise ValidationError(_("Translation is not valid:\n%s") % trans.value) diff --git a/odoo/addons/base/ir/ir_ui_view.py b/odoo/addons/base/ir/ir_ui_view.py index 8414d933f9b..4d890494aa8 100644 --- a/odoo/addons/base/ir/ir_ui_view.py +++ b/odoo/addons/base/ir/ir_ui_view.py @@ -457,7 +457,8 @@ actual arch. # cannot currently use relationships that are # not required. The root cause is the INNER JOIN # used to implement it. - views = self.search(conditions + [('model_ids.module', 'in', tuple(self.pool._init_modules))]) + modules = tuple(self.pool._init_modules) + (self._context.get('install_mode_data', {}).get('module'),) + views = self.search(conditions + [('model_ids.module', 'in', modules)]) views = self.search(conditions + [('id', 'in', list(self._context.get('check_view_ids') or (0,)) + map(int, views))]) else: views = self.search(conditions) diff --git a/odoo/addons/base/res/res.country.state.csv b/odoo/addons/base/res/res.country.state.csv index 877834a77ca..c48ed49141d 100644 --- a/odoo/addons/base/res/res.country.state.csv +++ b/odoo/addons/base/res/res.country.state.csv @@ -436,7 +436,7 @@ state_es_cu,es,"Cuenca","CU" state_es_gi,es,"Girona (Gerona)","GI" state_es_gr,es,"Granada","GR" state_es_gu,es,"Guadalajara","GU" -state_es_ss,es,"Guipuzkoa (Guipúzcoa)","SS" +state_es_ss,es,"Gipuzkoa (Guipúzcoa)","SS" state_es_h,es,"Huelva","H" state_es_hu,es,"Huesca","HU" state_es_j,es,"Jaén","J" diff --git a/odoo/addons/base/tests/test_translate.py b/odoo/addons/base/tests/test_translate.py index 1cd833c9df6..de607798a82 100644 --- a/odoo/addons/base/tests/test_translate.py +++ b/odoo/addons/base/tests/test_translate.py @@ -205,10 +205,16 @@ class TranslationToolsTestCase(unittest.TestCase): self.assertEquals(result, expect) def test_translate_html(self): + """ Test html_translate(). """ + source = """
A

B

C
""" + result = html_translate(lambda term: term, source) + self.assertEquals(result, source) + + def test_translate_html_i(self): """ Test xml_translate() and html_translate() with elements. """ - source = """""" + source = """

A B

""" result = xml_translate(lambda term: term, source) - self.assertEquals(result, """""") + self.assertEquals(result, """

A B

""") result = html_translate(lambda term: term, source) self.assertEquals(result, source) diff --git a/odoo/models.py b/odoo/models.py index cb9f32c9c94..be607bb72e2 100644 --- a/odoo/models.py +++ b/odoo/models.py @@ -3818,7 +3818,7 @@ class BaseModel(object): del vals['source'] # remove source to avoid triggering _set_src del vals['module'] # duplicated vals is not linked to any module vals['res_id'] = target_id - if vals['lang'] == old.env.lang: + if vals['lang'] == old.env.lang and field.translate is True: # 'source' to force the call to _set_src # 'value' needed if value is changed in copy(), want to see the new_value vals['source'] = old_wo_lang[name] diff --git a/odoo/tools/translate.py b/odoo/tools/translate.py index c947ae5e75d..fbef3443929 100644 --- a/odoo/tools/translate.py +++ b/odoo/tools/translate.py @@ -17,7 +17,7 @@ from datetime import datetime from os.path import join from babel.messages import extract -from lxml import etree +from lxml import etree, html import odoo from odoo.tools import config @@ -153,8 +153,13 @@ avoid_pattern = re.compile(r"\s*]*>(.*)", re.DOTALL | re.MULTILINE | re.UNICODE) -def translate_xml_node(node, callback, method, parser=None): - """ Return the translation of the given XML/HTML node. """ +def translate_xml_node(node, callback, parse, serialize): + """ Return the translation of the given XML/HTML node. + + :param callback: callback(text) returns translated text or None + :param parse: parse(text) returns a node (text is unicode) + :param serialize: serialize(node) returns unicode text + """ def nonspace(text): return bool(text) and not text.isspace() @@ -182,7 +187,7 @@ def translate_xml_node(node, callback, method, parser=None): def translate_content(node): """ Return ``node`` with its content translated inline. """ # serialize the node that contains the stuff to translate - text = etree.tostring(node, method=method, encoding='utf8').decode('utf8') + text = serialize(node) # retrieve the node's content and translate it match = node_pattern.match(text) trans = translate_text(match.group(1)) @@ -190,7 +195,7 @@ def translate_xml_node(node, callback, method, parser=None): # replace the content, and convert it back to an XML node text = text[:match.start(1)] + trans + text[match.end(1):] try: - node = etree.fromstring(encode(text), parser=parser) + node = parse(text) except etree.ParseError: # fallback: escape the translation as text node = etree.Element(node.tag, node.attrib, node.nsmap) @@ -275,6 +280,21 @@ def translate_xml_node(node, callback, method, parser=None): return node +def parse_xml(text): + return etree.fromstring(encode(text)) + +def serialize_xml(node): + return etree.tostring(node, method='xml', encoding='utf8').decode('utf8') + +_HTML_PARSER = etree.HTMLParser(encoding='utf8') + +def parse_html(text): + return html.fragment_fromstring(encode(text), parser=_HTML_PARSER) + +def serialize_html(node): + return etree.tostring(node, method='html', encoding='utf8').decode('utf8') + + def xml_translate(callback, value): """ Translate an XML value (string), using `callback` for translating text appearing in `value`. @@ -283,17 +303,15 @@ def xml_translate(callback, value): return value try: - root = etree.fromstring(encode(value)) - result = translate_xml_node(root, callback, 'xml') - return etree.tostring(result, method='xml', encoding='utf8').decode('utf8') + root = parse_xml(value) + result = translate_xml_node(root, callback, parse_xml, serialize_xml) + return serialize_xml(result) except etree.ParseError: # fallback for translated terms: use an HTML parser and wrap the term - wrapped = "
%s
" % encode(value) - root = etree.fromstring(wrapped, etree.HTMLParser(encoding='utf-8')) - # root is html > body > div; translate the div only - result = translate_xml_node(root[0][0], callback, 'xml') + root = parse_html("
%s
" % value) + result = translate_xml_node(root, callback, parse_xml, serialize_xml) # remove tags
and
from result - return etree.tostring(result, method='xml', encoding='utf8').decode('utf8')[5:-6] + return serialize_xml(result)[5:-6] def html_translate(callback, value): """ Translate an HTML value (string), using `callback` for translating text @@ -303,14 +321,11 @@ def html_translate(callback, value): return value try: - parser = etree.HTMLParser(encoding='utf-8') # value may be some HTML fragment, wrap it into a div - wrapped = "
%s
" % encode(value) - root = etree.fromstring(wrapped, parser) - # root is html > body > div; translate the div only - result = translate_xml_node(root[0][0], callback, 'html', parser) + root = parse_html("
%s
" % value) + result = translate_xml_node(root, callback, parse_html, serialize_html) # remove tags
and
from result - value = etree.tostring(result, method='html', encoding='utf8').decode('utf8')[5:-6] + value = serialize_html(result)[5:-6] except ValueError: _logger.exception("Cannot translate malformed HTML, using source value instead") diff --git a/requirements.txt b/requirements.txt index 51325155af2..9109a29a8c7 100644 --- a/requirements.txt +++ b/requirements.txt @@ -12,7 +12,7 @@ Mako==1.0.4 MarkupSafe==0.23 mock==2.0.0 num2words==0.5.4 -ofxparse==0.15 +ofxparse==0.16 passlib==1.6.5 Pillow==3.4.1 psutil==4.3.1