diff options
Diffstat (limited to 'addons')
57 files changed, 817 insertions, 137 deletions
diff --git a/addons/account/report/account_invoice_report.py b/addons/account/report/account_invoice_report.py index 624a60bab11..3016dfa68c2 100644 --- a/addons/account/report/account_invoice_report.py +++ b/addons/account/report/account_invoice_report.py @@ -109,7 +109,7 @@ class AccountInvoiceReport(models.Model): 1 AS nbr, ai.type, ai.state, pt.categ_id, ai.date_due, ai.account_id, ail.account_id AS account_line_id, ai.partner_bank_id, - SUM ((invoice_type.sign * ail.quantity) / u.factor * u2.factor) AS product_qty, + SUM ((invoice_type.sign_qty * ail.quantity) / u.factor * u2.factor) AS product_qty, SUM(ail.price_subtotal_signed * invoice_type.sign) AS price_total, SUM(ABS(ail.price_subtotal_signed)) / CASE WHEN SUM(ail.quantity / u.factor * u2.factor) <> 0::numeric @@ -119,7 +119,7 @@ class AccountInvoiceReport(models.Model): ai.residual_company_signed / (SELECT count(*) FROM account_invoice_line l where invoice_id = ai.id) * count(*) * invoice_type.sign AS residual, ai.commercial_partner_id as commercial_partner_id, - partner.country_id + coalesce(partner.country_id, partner_ai.country_id) AS country_id """ return select_str @@ -128,6 +128,7 @@ class AccountInvoiceReport(models.Model): FROM account_invoice_line ail JOIN account_invoice ai ON ai.id = ail.invoice_id JOIN res_partner partner ON ai.commercial_partner_id = partner.id + JOIN res_partner partner_ai ON ai.partner_id = partner_ai.id LEFT JOIN product_product pr ON pr.id = ail.product_id left JOIN product_template pt ON pt.id = pr.product_tmpl_id LEFT JOIN uom_uom u ON u.id = ail.uom_id @@ -138,7 +139,11 @@ class AccountInvoiceReport(models.Model): WHEN ai.type::text = ANY (ARRAY['in_refund'::character varying::text, 'in_invoice'::character varying::text]) THEN -1 ELSE 1 - END) AS sign + END) AS sign,(CASE + WHEN ai.type::text = ANY (ARRAY['out_refund'::character varying::text, 'in_invoice'::character varying::text]) + THEN -1 + ELSE 1 + END) AS sign_qty FROM account_invoice ai ) AS invoice_type ON invoice_type.id = ai.id """ @@ -150,7 +155,7 @@ class AccountInvoiceReport(models.Model): ai.partner_id, ai.payment_term_id, u2.name, u2.id, ai.currency_id, ai.journal_id, ai.fiscal_position_id, ai.user_id, ai.company_id, ai.type, invoice_type.sign, ai.state, pt.categ_id, ai.date_due, ai.account_id, ail.account_id, ai.partner_bank_id, ai.residual_company_signed, - ai.amount_total_company_signed, ai.commercial_partner_id, partner.country_id + ai.amount_total_company_signed, ai.commercial_partner_id, coalesce(partner.country_id, partner_ai.country_id) """ return group_by_str diff --git a/addons/account/static/src/js/reconciliation/reconciliation_model.js b/addons/account/static/src/js/reconciliation/reconciliation_model.js index b8d2b8dd9c5..08256402b0d 100644 --- a/addons/account/static/src/js/reconciliation/reconciliation_model.js +++ b/addons/account/static/src/js/reconciliation/reconciliation_model.js @@ -874,8 +874,14 @@ var StatementModel = BasicModel.extend({ } } }); - total = Math.round(total*1000)/1000 || 0; - amount_currency = Math.round(amount_currency); + var company_currency = session.get_currency(line.st_line.currency_id); + var company_precision = company_currency && company_currency.digits[1] || 2; + total = utils.round_precision(total*1000, company_precision)/1000 || 0; + if(isOtherCurrencyId){ + var other_currency = session.get_currency(isOtherCurrencyId); + var other_precision = other_currency && other_currency.digits[1] || 2; + amount_currency = utils.round_precision(amount_currency, other_precision) + } line.balance = { amount: total, amount_str: field_utils.format.monetary(Math.abs(total), {}, formatOptions), diff --git a/addons/account_payment/models/payment.py b/addons/account_payment/models/payment.py index cfc90258289..4e00fce7054 100644 --- a/addons/account_payment/models/payment.py +++ b/addons/account_payment/models/payment.py @@ -50,7 +50,7 @@ class PaymentTransaction(models.Model): if not s2s_result or self.state != valid_state: _logger.warning( _("<%s> transaction (%s) invalid state : %s") % - (self.acquirer_id.provider, self.id, self.state_mesage)) + (self.acquirer_id.provider, self.id, self.state_message)) return 'pay_invoice_tx_state' try: diff --git a/addons/calendar/models/calendar.py b/addons/calendar/models/calendar.py index 8d95dada86c..713a208e7c2 100644 --- a/addons/calendar/models/calendar.py +++ b/addons/calendar/models/calendar.py @@ -1760,8 +1760,6 @@ class Meeting(models.Model): def _fix_rrule(self, values): rule_str = values.get('rrule') if rule_str: - rule = rrule.rrulestr(rule_str) - if not rule._until and not rule._count: - rule._count = 100 - rule_str = str(rule).split('RRULE:')[-1] + if 'UNTIL' not in rule_str and 'COUNT' not in rule_str: + rule_str += ';COUNT=100' return rule_str diff --git a/addons/delivery/models/delivery_carrier.py b/addons/delivery/models/delivery_carrier.py index dafb0e484b4..2644163e5e3 100644 --- a/addons/delivery/models/delivery_carrier.py +++ b/addons/delivery/models/delivery_carrier.py @@ -210,6 +210,12 @@ class DeliveryCarrier(models.Model): carrier.product_id.list_price = carrier.fixed_price def fixed_rate_shipment(self, order): + carrier = self._match_address(order.partner_shipping_id) + if not carrier: + return {'success': False, + 'price': 0.0, + 'error_message': _('Error: this delivery method is not available for this address.'), + 'warning_message': False} price = self.fixed_price if self.company_id.currency_id.id != order.currency_id.id: price = self.env['res.currency']._compute(self.company_id.currency_id, order.currency_id, price) diff --git a/addons/delivery/models/delivery_grid.py b/addons/delivery/models/delivery_grid.py index dddc300dfe2..5779684c8c0 100644 --- a/addons/delivery/models/delivery_grid.py +++ b/addons/delivery/models/delivery_grid.py @@ -47,7 +47,7 @@ class ProviderGrid(models.Model): if not carrier: return {'success': False, 'price': 0.0, - 'error_message': _('Error: no matching grid.'), + 'error_message': _('Error: this delivery method is not available for this address.'), 'warning_message': False} try: diff --git a/addons/hr_attendance/static/src/js/my_attendances.js b/addons/hr_attendance/static/src/js/my_attendances.js index 4a19b2b9b28..2e87833bfa1 100644 --- a/addons/hr_attendance/static/src/js/my_attendances.js +++ b/addons/hr_attendance/static/src/js/my_attendances.js @@ -25,12 +25,12 @@ var MyAttendances = AbstractAction.extend({ args: [[['user_id', '=', this.getSession().uid]], ['attendance_state', 'name']], }) .then(function (res) { + self.employee = res[0]; + self.$el.html(QWeb.render("HrAttendanceMyMainMenu", {widget: self})); if (_.isEmpty(res) ) { - self.$('.o_hr_attendance_employee').append(_t("Error : Could not find employee linked to user")); return; } - self.employee = res[0]; - self.$el.html(QWeb.render("HrAttendanceMyMainMenu", {widget: self})); + }); return $.when(def, this._super.apply(this, arguments)); diff --git a/addons/hr_gamification/wizard/gamification_badge_user_wizard.py b/addons/hr_gamification/wizard/gamification_badge_user_wizard.py index a2feb024688..5bbb6848bd3 100644 --- a/addons/hr_gamification/wizard/gamification_badge_user_wizard.py +++ b/addons/hr_gamification/wizard/gamification_badge_user_wizard.py @@ -2,14 +2,27 @@ # Part of Odoo. See LICENSE file for full copyright and licensing details. from odoo import api, fields, models, _ -from odoo.exceptions import UserError +from odoo.exceptions import UserError, AccessError class GamificationBadgeUserWizard(models.TransientModel): _inherit = 'gamification.badge.user.wizard' employee_id = fields.Many2one('hr.employee', string='Employee', required=True) - user_id = fields.Many2one('res.users', string='User', related='employee_id.user_id', store=True) + user_id = fields.Many2one('res.users', string='User', + related='employee_id.user_id', store=True, readonly=True) + + # TODO 12.0/master remove this hack by changing the model + @api.model + def create(self, values): + employee = self.env['hr.employee'].browse(values['employee_id']) + values['user_id'] = employee.user_id.id + try: + return super(GamificationBadgeUserWizard, self).create(values) + except AccessError: + # an employee can not write on another employee + # force sudo because of related + return super(GamificationBadgeUserWizard, self.sudo()).create(values) @api.multi def action_grant_badge(self): diff --git a/addons/hr_recruitment/views/hr_recruitment_views.xml b/addons/hr_recruitment/views/hr_recruitment_views.xml index 0d5664d6142..dba4df12325 100644 --- a/addons/hr_recruitment/views/hr_recruitment_views.xml +++ b/addons/hr_recruitment/views/hr_recruitment_views.xml @@ -855,7 +855,7 @@ <field name="inherit_id" ref="hr.view_employee_filter"/> <field name="arch" type="xml"> <xpath expr="//field[@name='department_id']" position="after"> - <filter name="newly_hired_employee" string="New Hired" domain="[('newly_hired_employee', '=', True)]"/> + <filter name="newly_hired_employee" string="New Hired" domain="[('newly_hired_employee', '=', True)]" groups="hr_recruitment.group_hr_recruitment_user"/> </xpath> </field> </record> diff --git a/addons/hr_timesheet/views/hr_timesheet_views.xml b/addons/hr_timesheet/views/hr_timesheet_views.xml index ccc13c433c1..24a8466218d 100644 --- a/addons/hr_timesheet/views/hr_timesheet_views.xml +++ b/addons/hr_timesheet/views/hr_timesheet_views.xml @@ -70,7 +70,7 @@ <pivot string="Timesheet"> <field name="employee_id" type="row"/> <field name="date" interval="month" type="col"/> - <field name="unit_amount" type="measure"/> + <field name="unit_amount" type="measure" widget="float_time"/> </pivot> </field> </record> diff --git a/addons/hw_posbox_homepage/controllers/main.py b/addons/hw_posbox_homepage/controllers/main.py index 6ba6c1ff62a..d5845003de2 100644 --- a/addons/hw_posbox_homepage/controllers/main.py +++ b/addons/hw_posbox_homepage/controllers/main.py @@ -58,7 +58,7 @@ index_template = """ </p> %s <p> - The PosBox software installed on this posbox is <b>version 17 BETA</b>, + The PosBox software installed on this posbox is <b>version 17</b>, the posbox version number is independent from Odoo. You can upgrade the software on the <a href='/hw_proxy/upgrade/'>upgrade page</a>. </p> diff --git a/addons/l10n_th/data/l10n_th_chart_data.xml b/addons/l10n_th/data/l10n_th_chart_data.xml index 94c094695f8..95cfd8d09ef 100644 --- a/addons/l10n_th/data/l10n_th_chart_data.xml +++ b/addons/l10n_th/data/l10n_th_chart_data.xml @@ -361,7 +361,7 @@ <field name="chart_template_id" ref="chart"/> <field name="name">Company Withholding Tax 1% (Transportation)</field> <field name="amount_type">percent</field> - <field name="amount" eval="1"/> + <field name="amount" eval="-1"/> <field name="account_id" ref="a_wht"/> <field name="refund_account_id" ref="a_wht"/> <field name="type_tax_use">purchase</field> @@ -373,7 +373,7 @@ <field name="chart_template_id" ref="chart"/> <field name="name">Company Withholding Tax 2% (Advertising)</field> <field name="amount_type">percent</field> - <field name="amount" eval="2"/> + <field name="amount" eval="-2"/> <field name="account_id" ref="a_wht"/> <field name="refund_account_id" ref="a_wht"/> <field name="type_tax_use">purchase</field> @@ -385,7 +385,7 @@ <field name="chart_template_id" ref="chart"/> <field name="name">Company Withholding Tax 3% (Service)</field> <field name="amount_type">percent</field> - <field name="amount" eval="3"/> + <field name="amount" eval="-3"/> <field name="account_id" ref="a_wht"/> <field name="refund_account_id" ref="a_wht"/> <field name="type_tax_use">purchase</field> @@ -397,7 +397,7 @@ <field name="chart_template_id" ref="chart"/> <field name="name">Company Withholding Tax 5% (Rental)</field> <field name="amount_type">percent</field> - <field name="amount" eval="5"/> + <field name="amount" eval="-5"/> <field name="account_id" ref="a_wht"/> <field name="refund_account_id" ref="a_wht"/> <field name="type_tax_use">purchase</field> @@ -409,7 +409,7 @@ <field name="chart_template_id" ref="chart"/> <field name="name">Personal Withholding Tax 1% (Transportation)</field> <field name="amount_type">percent</field> - <field name="amount" eval="1"/> + <field name="amount" eval="-1"/> <field name="account_id" ref="a_wht"/> <field name="refund_account_id" ref="a_wht"/> <field name="type_tax_use">purchase</field> @@ -421,7 +421,7 @@ <field name="chart_template_id" ref="chart"/> <field name="name">Personal Withholding Tax 2% (Advertising)</field> <field name="amount_type">percent</field> - <field name="amount" eval="2"/> + <field name="amount" eval="-2"/> <field name="account_id" ref="a_wht"/> <field name="refund_account_id" ref="a_wht"/> <field name="type_tax_use">purchase</field> @@ -433,7 +433,7 @@ <field name="chart_template_id" ref="chart"/> <field name="name">Personal Withholding Tax 3% (Service)</field> <field name="amount_type">percent</field> - <field name="amount" eval="3"/> + <field name="amount" eval="-3"/> <field name="account_id" ref="a_wht"/> <field name="refund_account_id" ref="a_wht"/> <field name="type_tax_use">purchase</field> @@ -445,7 +445,7 @@ <field name="chart_template_id" ref="chart"/> <field name="name">Personal Withholding Tax 5% (Rental)</field> <field name="amount_type">percent</field> - <field name="amount" eval="5"/> + <field name="amount" eval="-5"/> <field name="account_id" ref="a_wht"/> <field name="refund_account_id" ref="a_wht"/> <field name="type_tax_use">purchase</field> @@ -457,7 +457,7 @@ <field name="chart_template_id" ref="chart"/> <field name="name">Withholding Income Tax 1% (Transportation)</field> <field name="amount_type">percent</field> - <field name="amount" eval="1"/> + <field name="amount" eval="-1"/> <field name="account_id" ref="a_wht_income"/> <field name="refund_account_id" ref="a_wht_income"/> <field name="type_tax_use">sale</field> @@ -468,7 +468,7 @@ <field name="chart_template_id" ref="chart"/> <field name="name">Withholding Income Tax 2% (Advertising)</field> <field name="amount_type">percent</field> - <field name="amount" eval="2"/> + <field name="amount" eval="-2"/> <field name="account_id" ref="a_wht_income"/> <field name="refund_account_id" ref="a_wht_income"/> <field name="type_tax_use">sale</field> @@ -479,7 +479,7 @@ <field name="chart_template_id" ref="chart"/> <field name="name">Withholding Income Tax 3% (Service)</field> <field name="amount_type">percent</field> - <field name="amount" eval="3"/> + <field name="amount" eval="-3"/> <field name="account_id" ref="a_wht_income"/> <field name="refund_account_id" ref="a_wht_income"/> <field name="type_tax_use">sale</field> @@ -490,7 +490,7 @@ <field name="chart_template_id" ref="chart"/> <field name="name">Withholding Income Tax 5% (Rental)</field> <field name="amount_type">percent</field> - <field name="amount" eval="5"/> + <field name="amount" eval="-5"/> <field name="account_id" ref="a_wht_income"/> <field name="refund_account_id" ref="a_wht_income"/> <field name="type_tax_use">sale</field> diff --git a/addons/mail/models/mail_activity.py b/addons/mail/models/mail_activity.py index 3656f7e64e8..33c4b76d79d 100644 --- a/addons/mail/models/mail_activity.py +++ b/addons/mail/models/mail_activity.py @@ -146,7 +146,13 @@ class MailActivity(models.Model): def _onchange_activity_type_id(self): if self.activity_type_id: self.summary = self.activity_type_id.summary - self.date_deadline = (datetime.now() + timedelta(days=self.activity_type_id.days)) + tz = self.user_id.sudo().tz + if tz: + today_utc = pytz.UTC.localize(datetime.utcnow()) + today = today_utc.astimezone(pytz.timezone(tz)) + else: + today = datetime.now() + self.date_deadline = (today + timedelta(days=self.activity_type_id.days)) @api.onchange('previous_activity_type_id') def _onchange_previous_activity_type_id(self): diff --git a/addons/mass_mailing_event/models/event.py b/addons/mass_mailing_event/models/event.py index be37000e6f2..16b9c066862 100644 --- a/addons/mass_mailing_event/models/event.py +++ b/addons/mass_mailing_event/models/event.py @@ -8,6 +8,10 @@ class Event(models.Model): _inherit = "event.event" def action_mass_mailing_attendees(self): + if len(self) == 1: + domain = "[('event_id', '=', {})]".format(self.id) + else: + domain = "[('event_id', 'in', {})]".format(self.ids) mass_mailing_action = dict( name='Mass Mail Attendees', type='ir.actions.act_window', @@ -17,7 +21,7 @@ class Event(models.Model): target='current', context=dict( default_mailing_model_id=self.env.ref('event.model_event_registration').id, - default_mailing_domain="[('event_id', 'in', %s)]" % self.ids, # , ('state', 'in', ['draft', 'open', 'done']) + default_mailing_domain=domain, ), ) - return mass_mailing_action
\ No newline at end of file + return mass_mailing_action diff --git a/addons/mrp/views/mrp_workorder_views.xml b/addons/mrp/views/mrp_workorder_views.xml index 3fbb2ab111a..815034fff7c 100644 --- a/addons/mrp/views/mrp_workorder_views.xml +++ b/addons/mrp/views/mrp_workorder_views.xml @@ -155,7 +155,7 @@ <field name="active_move_line_ids" attrs="{'invisible': [('active_move_line_ids', '=', [])]}"> <tree editable="bottom" create="0" delete="0"> <field name="product_id"/> - <field name="product_uom_qty" readonly="1" string="To Consume"/> + <field name="product_uom_qty" readonly="1" invisible="1" string="To Consume"/> <field name="lot_id" domain="[('product_id', '=', product_id)]" context="{'default_product_id': product_id}"/> <field name="qty_done"/> <field name="product_uom_id" invisible="1"/> diff --git a/addons/mrp/wizard/mrp_product_produce.py b/addons/mrp/wizard/mrp_product_produce.py index 37e85825cdd..dfd4c9a268d 100644 --- a/addons/mrp/wizard/mrp_product_produce.py +++ b/addons/mrp/wizard/mrp_product_produce.py @@ -103,7 +103,7 @@ class MrpProductProduce(models.TransientModel): elif len(move._get_move_lines()) < 2: move.quantity_done += float_round(quantity * move.unit_factor, precision_rounding=rounding) else: - self._set_quantity_done(move, quantity * move.unit_factor) + move._set_quantity_done(quantity * move.unit_factor) for move in self.production_id.move_finished_ids: if move.product_id.tracking == 'none' and move.state not in ('done', 'cancel'): rounding = move.product_uom.rounding @@ -120,40 +120,6 @@ class MrpProductProduce(models.TransientModel): }) return {'type': 'ir.actions.act_window_close'} - def _set_quantity_done(self, move, qty): - """ - Set the given quantity as quantity done on the move through the move lines. The method is - able to handle move lines with a different UoM than the move (but honestly, this would be - looking for trouble...). - @param move: stock.move - @param qty: quantity in the UoM of move.product_uom - """ - for ml in move.move_line_ids: - # Convert move line qty into move uom - ml_qty = ml.product_uom_qty - ml.qty_done - if ml.product_uom_id != move.product_uom: - ml_qty = ml.product_uom_id._compute_quantity(ml_qty, move.product_uom, round=False) - - taken_qty = min(qty, ml_qty) - # Convert taken qty into move line uom - if ml.product_uom_id != move.product_uom: - taken_qty = move.product_uom._compute_quantity(ml_qty, ml.product_uom_id, round=False) - - # Assign qty_done and explicitly round to make sure there is no inconsistency between - # ml.qty_done and qty. - taken_qty = float_round(taken_qty, precision_rounding=ml.product_uom_id.rounding) - ml.qty_done += taken_qty - if ml.product_uom_id != move.product_uom: - taken_qty = ml.product_uom_id._compute_quantity(ml_qty, move.product_uom, round=False) - qty -= taken_qty - - if float_compare(qty, 0.0, precision_rounding=move.product_uom.rounding) <= 0: - break - if float_compare(qty, 0.0, precision_rounding=move.product_uom.rounding) > 0: - vals = move._prepare_move_line_vals(quantity=0) - vals['qty_done'] = qty - ml = self.env['stock.move.line'].create(vals) - @api.multi def check_finished_move_lots(self): produce_move = self.production_id.move_finished_ids.filtered(lambda x: x.product_id == self.product_id and x.state not in ('done', 'cancel')) diff --git a/addons/payment_authorize/models/authorize_request.py b/addons/payment_authorize/models/authorize_request.py index 138c270e35d..ba5a14f7edd 100644 --- a/addons/payment_authorize/models/authorize_request.py +++ b/addons/payment_authorize/models/authorize_request.py @@ -7,7 +7,7 @@ from uuid import uuid4 import pprint import logging -from odoo import _ +from odoo.addons.payment.models.payment_acquirer import _partner_split_name from odoo.exceptions import ValidationError, UserError from odoo import _ @@ -153,6 +153,12 @@ class AuthorizeAPI(): payment_profile = etree.SubElement(profile, "paymentProfiles") etree.SubElement(payment_profile, "customerType").text = 'business' if partner.is_company else 'individual' billTo = etree.SubElement(payment_profile, "billTo") + if partner.is_company: + etree.SubElement(billTo, "firstName").text = ' ' + etree.SubElement(billTo, "lastName").text = partner.name + else: + etree.SubElement(billTo, "firstName").text = _partner_split_name(partner.name)[0] + etree.SubElement(billTo, "lastName").text = _partner_split_name(partner.name)[1] etree.SubElement(billTo, "address").text = (partner.street or '' + (partner.street2 if partner.street2 else '')) or None missing_fields = [partner._fields[field].string for field in ['city', 'country_id'] if not partner[field]] diff --git a/addons/point_of_sale/models/pos_order.py b/addons/point_of_sale/models/pos_order.py index 4bca599d908..00f19a70b84 100644 --- a/addons/point_of_sale/models/pos_order.py +++ b/addons/point_of_sale/models/pos_order.py @@ -824,7 +824,10 @@ class PosOrder(models.Model): 'lot_id': lot_id, }) if not pack_lots and not float_is_zero(qty_done, precision_rounding=move.product_uom.rounding): - move.quantity_done = qty_done + if len(move._get_move_lines()) < 2: + move.quantity_done = qty_done + else: + move._set_quantity_done(qty_done) return has_wrong_lots def _prepare_bank_statement_line_payment_values(self, data): diff --git a/addons/point_of_sale/static/src/js/models.js b/addons/point_of_sale/static/src/js/models.js index 91e234dafc5..df20d976efd 100644 --- a/addons/point_of_sale/static/src/js/models.js +++ b/addons/point_of_sale/static/src/js/models.js @@ -1238,6 +1238,15 @@ exports.Product = Backbone.Model.extend({ var self = this; var date = moment().startOf('day'); + // In case of nested pricelists, it is necessary that all pricelists are made available in + // the POS. Display a basic alert to the user in this case. + if (pricelist === undefined) { + alert(_t( + 'An error occurred when loading product prices. ' + + 'Make sure all pricelists are available in the POS.' + )); + } + var category_ids = []; var category = this.categ; while (category) { diff --git a/addons/point_of_sale/static/src/js/screens.js b/addons/point_of_sale/static/src/js/screens.js index 95a7555e5d2..bb6e5a1f1cf 100644 --- a/addons/point_of_sale/static/src/js/screens.js +++ b/addons/point_of_sale/static/src/js/screens.js @@ -1547,7 +1547,12 @@ var ReceiptScreenWidget = ScreenWidget.extend({ }; }, print_web: function() { - window.print(); + if($.browser.safari){ + document.execCommand('print', false, null); + } + else{ + window.print(); + } this.pos.get_order()._printed = true; }, print_xml: function() { diff --git a/addons/point_of_sale/views/pos_config_view.xml b/addons/point_of_sale/views/pos_config_view.xml index 314b8030d82..dd98a420ed4 100644 --- a/addons/point_of_sale/views/pos_config_view.xml +++ b/addons/point_of_sale/views/pos_config_view.xml @@ -391,7 +391,7 @@ </div> </div> </div> - <div class="col-xs-12 col-md-6 o_setting_box" id="auto_printing" attrs="{'invisible': [('is_posbox', '=', False)]}"> + <div class="col-xs-12 col-md-6 o_setting_box" id="auto_printing"> <div class="o_setting_left_pane"> <field name="iface_print_auto"/> </div> @@ -400,7 +400,7 @@ <div class="text-muted"> Print receipts automatically once the payment is registered </div> - <div class="content-group mt16" attrs="{'invisible' : [('iface_print_auto', '=', False)]}"> + <div class="content-group mt16" attrs="{'invisible' : ['|', ('iface_print_auto', '=', False), ('is_posbox', '=', False)]}"> <div> <field name="iface_print_skip_screen" class="oe_inline"/><span class="oe_inline"><b>Skip Preview Screen</b></span> </div> diff --git a/addons/pos_sale/models/crm_team.py b/addons/pos_sale/models/crm_team.py index ffbfcd026c6..b8ed1c48710 100644 --- a/addons/pos_sale/models/crm_team.py +++ b/addons/pos_sale/models/crm_team.py @@ -77,7 +77,7 @@ class CrmTeam(models.Model): for data_point in order_data: result.append({'x_value': data_point.get('user_id')[0], 'y_value': data_point.get('price_total')}) - else: + elif self.dashboard_graph_group_pos in ['day', 'week', 'month']: # locale en_GB is used to be able to obtain the datetime from the string returned by read_group # /!\ do not use en_US as it's not ISO-standard and does not match datetime's library order_data = self.env['report.pos.order'].with_context(lang='en_GB').read_group( diff --git a/addons/project/controllers/portal.py b/addons/project/controllers/portal.py index 2e0f8528076..1ea74c36da4 100644 --- a/addons/project/controllers/portal.py +++ b/addons/project/controllers/portal.py @@ -104,7 +104,16 @@ class CustomerPortal(CustomerPortal): } # extends filterby criteria with project (criteria name is the project id) # Note: portal users can't view projects they don't follow - projects = request.env['project.project'].sudo().search([('privacy_visibility', '=', 'portal')]) + partner = request.env.user.partner_id + domain_projects = [ + '&', + ('privacy_visibility', '=', 'portal'), + '|', + ('message_partner_ids', 'child_of', [partner.commercial_partner_id.id]), + ('task_ids.message_partner_ids', 'child_of', [partner.commercial_partner_id.id]) + ] + + projects = request.env['project.project'].sudo().search(domain_projects) domain = [('project_id', 'in', projects.ids)] for proj in projects: searchbar_filters.update({ diff --git a/addons/project/models/project.py b/addons/project/models/project.py index c4121bb8b58..3b3030b26e4 100644 --- a/addons/project/models/project.py +++ b/addons/project/models/project.py @@ -531,7 +531,7 @@ class Task(models.Model): def _compute_attachment_ids(self): for task in self: attachment_ids = self.env['ir.attachment'].search([('res_id', '=', task.id), ('res_model', '=', 'project.task')]).ids - message_attachment_ids = self.mapped('message_ids.attachment_ids').ids # from mail_thread + message_attachment_ids = task.mapped('message_ids.attachment_ids').ids # from mail_thread task.attachment_ids = list(set(attachment_ids) - set(message_attachment_ids)) @api.multi diff --git a/addons/purchase/models/account_invoice.py b/addons/purchase/models/account_invoice.py index 25225bdc8f6..f8c36aeed09 100644 --- a/addons/purchase/models/account_invoice.py +++ b/addons/purchase/models/account_invoice.py @@ -107,6 +107,7 @@ class AccountInvoice(models.Model): purchase_ids = self.invoice_line_ids.mapped('purchase_id') if purchase_ids: self.origin = ', '.join(purchase_ids.mapped('name')) + self.reference = ', '.join(purchase_ids.filtered('partner_ref').mapped('partner_ref')) or self.reference @api.onchange('partner_id', 'company_id') def _onchange_partner_id(self): diff --git a/addons/sale/models/analytic.py b/addons/sale/models/analytic.py index 3a49bdee295..8cb0384df6b 100644 --- a/addons/sale/models/analytic.py +++ b/addons/sale/models/analytic.py @@ -107,7 +107,14 @@ class AccountAnalyticLine(models.Model): continue if sale_order.state != 'sale': - raise UserError(_('The Sales Order %s linked to the Analytic Account must be validated before registering expenses.') % sale_order.name) + message_unconfirmed = _('The Sales Order %s linked to the Analytic Account %s must be validated before registering expenses.') + messages = { + 'draft': message_unconfirmed, + 'sent': message_unconfirmed, + 'done': _('The Sales Order %s linked to the Analytic Account %s is currently locked. You cannot register an expense on a locked Sales Order. Please create a new SO linked to this Analytic Account.'), + 'cancel': _('The Sales Order %s linked to the Analytic Account %s is cancelled. You cannot register an expense on a cancelled Sales Order.'), + } + raise UserError(messages[sale_order.state] % (sale_order.name, analytic_line.account_id.name)) so_line = None price = analytic_line._sale_get_invoice_price(sale_order) diff --git a/addons/sale_payment/controllers/payment.py b/addons/sale_payment/controllers/payment.py index 01c7c2241de..db94060df40 100644 --- a/addons/sale_payment/controllers/payment.py +++ b/addons/sale_payment/controllers/payment.py @@ -27,6 +27,9 @@ class PaymentPortal(http.Controller): except: return False + # we check if the order need to create a payment token + save_token = save_token or order_sudo._get_payment_type() == "form_save" + if request.env.user == request.env.ref('base.public_user'): save_token = False diff --git a/addons/sale_stock/report/stock_report_deliveryslip.xml b/addons/sale_stock/report/stock_report_deliveryslip.xml index ea8ca1c0612..893b898fa3c 100644 --- a/addons/sale_stock/report/stock_report_deliveryslip.xml +++ b/addons/sale_stock/report/stock_report_deliveryslip.xml @@ -2,10 +2,10 @@ <odoo> <template id="report_delivery_document_inherit_sale_stock" inherit_id="stock.report_delivery_document"> <xpath expr="//div[@name='customer_address']" position="after"> - <div class="row" t-if="o.sale_id.client_order_ref"> + <div class="row" t-if="o.sudo().sale_id.client_order_ref"> <div class="col-xs-4 pull-right"> <strong>Customer Reference:</strong> - <p t-field="o.sale_id.client_order_ref"/> + <p t-field="o.sudo().sale_id.client_order_ref"/> </div> </div> </xpath> diff --git a/addons/stock/models/product.py b/addons/stock/models/product.py index b64574b6b3e..ffe8b54e3a5 100644 --- a/addons/stock/models/product.py +++ b/addons/stock/models/product.py @@ -132,17 +132,19 @@ class Product(models.Model): res = dict() for product in self.with_context(prefetch_fields=False): - res[product.id] = {} + product_id = product.id + rounding = product.uom_id.rounding + res[product_id] = {} if dates_in_the_past: - qty_available = quants_res.get(product.id, 0.0) - moves_in_res_past.get(product.id, 0.0) + moves_out_res_past.get(product.id, 0.0) + qty_available = quants_res.get(product_id, 0.0) - moves_in_res_past.get(product_id, 0.0) + moves_out_res_past.get(product_id, 0.0) else: - qty_available = quants_res.get(product.id, 0.0) - res[product.id]['qty_available'] = float_round(qty_available, precision_rounding=product.uom_id.rounding) - res[product.id]['incoming_qty'] = float_round(moves_in_res.get(product.id, 0.0), precision_rounding=product.uom_id.rounding) - res[product.id]['outgoing_qty'] = float_round(moves_out_res.get(product.id, 0.0), precision_rounding=product.uom_id.rounding) - res[product.id]['virtual_available'] = float_round( - qty_available + res[product.id]['incoming_qty'] - res[product.id]['outgoing_qty'], - precision_rounding=product.uom_id.rounding) + qty_available = quants_res.get(product_id, 0.0) + res[product_id]['qty_available'] = float_round(qty_available, precision_rounding=rounding) + res[product_id]['incoming_qty'] = float_round(moves_in_res.get(product_id, 0.0), precision_rounding=rounding) + res[product_id]['outgoing_qty'] = float_round(moves_out_res.get(product_id, 0.0), precision_rounding=rounding) + res[product_id]['virtual_available'] = float_round( + qty_available + res[product_id]['incoming_qty'] - res[product_id]['outgoing_qty'], + precision_rounding=rounding) return res diff --git a/addons/stock/models/stock_move.py b/addons/stock/models/stock_move.py index 656beb36c0c..47451caeb09 100644 --- a/addons/stock/models/stock_move.py +++ b/addons/stock/models/stock_move.py @@ -1250,3 +1250,36 @@ class StockMove(models.Model): return result else: return [(self.picking_id, self.product_id.responsible_id, visited)] + + def _set_quantity_done(self, qty): + """ + Set the given quantity as quantity done on the move through the move lines. The method is + able to handle move lines with a different UoM than the move (but honestly, this would be + looking for trouble...). + @param qty: quantity in the UoM of move.product_uom + """ + for ml in self.move_line_ids: + # Convert move line qty into move uom + ml_qty = ml.product_uom_qty - ml.qty_done + if ml.product_uom_id != self.product_uom: + ml_qty = ml.product_uom_id._compute_quantity(ml_qty, self.product_uom, round=False) + + taken_qty = min(qty, ml_qty) + # Convert taken qty into move line uom + if ml.product_uom_id != self.product_uom: + taken_qty = self.product_uom._compute_quantity(ml_qty, ml.product_uom_id, round=False) + + # Assign qty_done and explicitly round to make sure there is no inconsistency between + # ml.qty_done and qty. + taken_qty = float_round(taken_qty, precision_rounding=ml.product_uom_id.rounding) + ml.qty_done += taken_qty + if ml.product_uom_id != self.product_uom: + taken_qty = ml.product_uom_id._compute_quantity(ml_qty, self.product_uom, round=False) + qty -= taken_qty + + if float_compare(qty, 0.0, precision_rounding=self.product_uom.rounding) <= 0: + break + if float_compare(qty, 0.0, precision_rounding=self.product_uom.rounding) > 0: + vals = self._prepare_move_line_vals(quantity=0) + vals['qty_done'] = qty + ml = self.env['stock.move.line'].create(vals) diff --git a/addons/stock/report/report_deliveryslip.xml b/addons/stock/report/report_deliveryslip.xml index a5a9d7d0fe5..240d9d7523b 100644 --- a/addons/stock/report/report_deliveryslip.xml +++ b/addons/stock/report/report_deliveryslip.xml @@ -59,7 +59,15 @@ <tbody> <t t-set="lines" t-value="o.move_lines.filtered(lambda x: x.product_uom_qty)"/> <tr t-foreach="lines" t-as="move"> - <td><span t-field="move.product_id"/></td> + <td> + <span t-field="move.product_id"/> + <p t-if="o.picking_type_code == 'outgoing'"> + <span t-field="move.product_id.sudo().description_pickingout"/> + </p> + <p t-if="o.picking_type_code == 'incoming'"> + <span t-field="move.product_id.sudo().description_pickingin"/> + </p> + </td> <td> <span t-field="move.product_uom_qty"/> <span t-field="move.product_uom"/> diff --git a/addons/stock_account/views/stock_account_views.xml b/addons/stock_account/views/stock_account_views.xml index 77747cdea5c..ecf114226fb 100644 --- a/addons/stock_account/views/stock_account_views.xml +++ b/addons/stock_account/views/stock_account_views.xml @@ -73,7 +73,7 @@ <field name="model">product.product</field> <field name="arch" type="xml"> <tree> - <field name="name"/> + <field name="display_name"/> <field name="qty_at_date"/> <field name="uom_id" groups="product.group_uom"/> <field name="currency_id" invisible="1"/> diff --git a/addons/transifex/models/ir_translation.py b/addons/transifex/models/ir_translation.py index 316fbba47b1..c19f66c39c1 100644 --- a/addons/transifex/models/ir_translation.py +++ b/addons/transifex/models/ir_translation.py @@ -86,6 +86,6 @@ class IrTranslation(models.Model): 'module': translation.module, 'src': werkzeug.url_quote_plus( "text:'" + - translation.source[:50].replace("'", "\'") + + translation.source[:50].replace("'", "\\'") + "'"), } diff --git a/addons/web/controllers/main.py b/addons/web/controllers/main.py index c4fba082be2..35332e4fe79 100644 --- a/addons/web/controllers/main.py +++ b/addons/web/controllers/main.py @@ -1290,7 +1290,7 @@ class Export(http.Controller): 'relation_field': field.get('relation_field')} records.append(record) - if len(name.split('/')) < 3 and 'relation' in field: + if len(id.split('/')) < 3 and 'relation' in field: ref = field.pop('relation') record['value'] += '/id' record['params'] = {'model': ref, 'prefix': id, 'name': name} diff --git a/addons/web/static/src/js/fields/basic_fields.js b/addons/web/static/src/js/fields/basic_fields.js index 96209ee71ff..bb2130aa6b8 100644 --- a/addons/web/static/src/js/fields/basic_fields.js +++ b/addons/web/static/src/js/fields/basic_fields.js @@ -440,7 +440,7 @@ var FieldDate = InputField.extend({ this.datewidget = this._makeDatePicker(); this.datewidget.on('datetime_changed', this, function () { var value = this._getValue(); - if ((!value && this.value) || (value && !value.isSame(this.value))) { + if ((!value && this.value) || (value && !this._isSameValue(value))) { this._setValue(value); } }); @@ -2340,19 +2340,18 @@ var FieldDomain = AbstractField.extend({ // Convert char value to array value var value = this.value || "[]"; - var domain = Domain.prototype.stringToArray(value); // Create the domain selector or change the value of the current one... var def; if (!this.domainSelector) { - this.domainSelector = new DomainSelector(this, this._domainModel, domain, { + this.domainSelector = new DomainSelector(this, this._domainModel, value, { readonly: this.mode === "readonly" || this.inDialog, filters: this.fsFilters, debugMode: session.debug, }); def = this.domainSelector.prependTo(this.$el); } else { - def = this.domainSelector.setDomain(domain); + def = this.domainSelector.setDomain(value); } // ... then replace the other content (matched records, etc) return def.then(this._replaceContent.bind(this)); diff --git a/addons/web/static/src/js/fields/relational_fields.js b/addons/web/static/src/js/fields/relational_fields.js index 7e4e4973ced..faa6e82c3da 100644 --- a/addons/web/static/src/js/fields/relational_fields.js +++ b/addons/web/static/src/js/fields/relational_fields.js @@ -578,10 +578,11 @@ var FieldMany2One = AbstractField.extend({ * @param {OdooEvent} ev */ _onInputKeyup: function (ev) { - if (ev.which === $.ui.keyCode.ENTER) { - // If we pressed enter, we want to prevent _onInputFocusout from + if (ev.which === $.ui.keyCode.ENTER || ev.which === $.ui.keyCode.TAB) { + // If we pressed enter or tab, we want to prevent _onInputFocusout from // executing since it would open a M2O dialog to request // confirmation that the many2one is not properly set. + // It's a case that is already handled by the autocomplete lib. return; } this.isDirty = true; diff --git a/addons/web/static/src/js/views/basic/basic_model.js b/addons/web/static/src/js/views/basic/basic_model.js index d327fec4839..fe428af5e5e 100644 --- a/addons/web/static/src/js/views/basic/basic_model.js +++ b/addons/web/static/src/js/views/basic/basic_model.js @@ -2447,11 +2447,11 @@ var BasicModel = AbstractModel.extend({ } var def = $.Deferred(); - + var evalContext = this._getEvalContext(record); this._rpc({ model: domainModel, method: 'search_count', - args: [Domain.prototype.stringToArray(domainValue)], + args: [Domain.prototype.stringToArray(domainValue, evalContext)], context: context }) .then(_.identity, function (error, e) { diff --git a/addons/web/static/src/js/views/calendar/calendar_model.js b/addons/web/static/src/js/views/calendar/calendar_model.js index 63a35171a30..1f8c9423cac 100644 --- a/addons/web/static/src/js/views/calendar/calendar_model.js +++ b/addons/web/static/src/js/views/calendar/calendar_model.js @@ -405,6 +405,7 @@ return AbstractModel.extend({ dayNames: moment.weekdays(), dayNamesShort: moment.weekdaysShort(), firstDay: _t.database.parameters.week_start, + slotLabelFormat: _t.database.parameters.time_format.search("%H") != -1 ? 'H:mm': 'h(:mm)a', }; }, /** diff --git a/addons/web/static/src/js/views/calendar/calendar_renderer.js b/addons/web/static/src/js/views/calendar/calendar_renderer.js index a8d5d07ed0d..89be23c849b 100644 --- a/addons/web/static/src/js/views/calendar/calendar_renderer.js +++ b/addons/web/static/src/js/views/calendar/calendar_renderer.js @@ -388,7 +388,8 @@ return AbstractRenderer.extend({ if (!event.allDay) { var start = event.r_start || event.start; var end = event.r_end || event.end; - display_hour = start.format('HH:mm') + ' - ' + end.format('HH:mm'); + var timeFormat = _t.database.parameters.time_format.search("%H") != -1 ? 'HH:mm': 'h:mma'; + display_hour = start.format(timeFormat) + ' - ' + end.format(timeFormat); if (display_hour === '00:00 - 00:00') { display_hour = _t('All day'); } diff --git a/addons/web/static/src/js/views/kanban/kanban_model.js b/addons/web/static/src/js/views/kanban/kanban_model.js index 6c4ea1f133c..5d606324023 100644 --- a/addons/web/static/src/js/views/kanban/kanban_model.js +++ b/addons/web/static/src/js/views/kanban/kanban_model.js @@ -61,11 +61,15 @@ var KanbanModel = BasicModel.extend({ if (!groupByField || groupByField.type !== 'many2one') { return $.Deferred().reject(); // only supported when grouped on m2o } + // ignore action default which may be for another model + var context = _.pick(parent.context, function (value, key) { + return key.slice(0,8) !== 'default_'; + }); return this._rpc({ model: groupByField.relation, method: 'name_create', args: [name], - context: parent.context, // todo: combine with view context + context: context, // todo: combine with view context }) .then(function (result) { var newGroup = self._makeDataPoint({ @@ -272,6 +276,9 @@ var KanbanModel = BasicModel.extend({ options.groupBy = this.defaultGroupedBy; } var def = this._super(id, options); + if (options && options.loadMoreOffset) { + return def; + } return this._reloadProgressBarGroupFromRecord(id, def); }, /** diff --git a/addons/web/static/src/js/views/list/list_renderer.js b/addons/web/static/src/js/views/list/list_renderer.js index a72cd7008eb..2388b3a3707 100644 --- a/addons/web/static/src/js/views/list/list_renderer.js +++ b/addons/web/static/src/js/views/list/list_renderer.js @@ -193,7 +193,6 @@ var ListRenderer = BasicRenderer.extend({ reject = columnInvisibleFields[c.attrs.name]; } if (!reject && c.attrs.widget === 'handle') { - self.hasHandle = true; self.handleField = c.attrs.name; } return reject; @@ -659,6 +658,8 @@ var ListRenderer = BasicRenderer.extend({ this._computeAggregates(); $table.toggleClass('o_list_view_grouped', is_grouped); $table.toggleClass('o_list_view_ungrouped', !is_grouped); + this.hasHandle = this.state.orderedBy.length === 0 || + this.state.orderedBy[0].name === this.handleField; if (is_grouped) { $table .append(this._renderHeader(true)) diff --git a/addons/web/static/src/js/views/pivot/pivot_renderer.js b/addons/web/static/src/js/views/pivot/pivot_renderer.js index b5648d358a9..390ccfed6e1 100644 --- a/addons/web/static/src/js/views/pivot/pivot_renderer.js +++ b/addons/web/static/src/js/views/pivot/pivot_renderer.js @@ -14,6 +14,16 @@ var PivotRenderer = AbstractRenderer.extend({ 'hover td': '_onTdHover', }), + /** + * @override + * @param {Object} [params.widgets] mapping (fieldName -> widget) used to + * format the cells + */ + init: function (parent, state, params) { + this._super.apply(this, arguments); + this.fieldWidgets = params.widgets || {}; + }, + //-------------------------------------------------------------------------- // Private //-------------------------------------------------------------------------- @@ -141,7 +151,8 @@ var PivotRenderer = AbstractRenderer.extend({ if (value !== undefined) { name = this.state.measures[j % nbrMeasures]; measure = this.state.fields[name]; - value = field_utils.format[measureTypes[j % nbrMeasures]](value, measure); + var formatter = this.fieldWidgets[name] || measureTypes[j % nbrMeasures]; + value = field_utils.format[formatter](value, measure); } $cell = $('<td>') .data('id', rows[i].id) diff --git a/addons/web/static/src/js/views/pivot/pivot_view.js b/addons/web/static/src/js/views/pivot/pivot_view.js index e0b232b6310..aa12a3324a7 100644 --- a/addons/web/static/src/js/views/pivot/pivot_view.js +++ b/addons/web/static/src/js/views/pivot/pivot_view.js @@ -43,6 +43,7 @@ var PivotView = AbstractView.extend({ var measures = {}; var groupableFields = {}; + var widgets = {}; this.fields.__count = {string: _t("Count"), type: "integer"}; _.each(this.fields, function (field, name) { @@ -64,6 +65,10 @@ var PivotView = AbstractView.extend({ name += ':' + field.attrs.interval; } + if (field.attrs.widget) { + widgets[name] = field.attrs.widget; + } + // add active measures to the measure list. This is very rarely // necessary, but it can be useful if one is working with a // functional field non stored, but in a model with an overrided @@ -94,6 +99,8 @@ var PivotView = AbstractView.extend({ this.loadParams.rowGroupBys = rowGroupBys; this.loadParams.fields = this.fields; + this.rendererParams.widgets = widgets; + this.controllerParams.title = params.title || this.arch.attrs.string || _t("Untitled"); this.controllerParams.enableLinking = !this.arch.attrs.disable_linking; this.controllerParams.measures = measures; diff --git a/addons/web/static/src/js/widgets/domain_selector.js b/addons/web/static/src/js/widgets/domain_selector.js index c5a561b1b77..f7d5e02c5e5 100644 --- a/addons/web/static/src/js/widgets/domain_selector.js +++ b/addons/web/static/src/js/widgets/domain_selector.js @@ -177,18 +177,12 @@ var DomainTree = DomainNode.extend({ * operator from the domain. * @see DomainTree._addFlattenedChildren */ - init: function (parent, model, domain, options) { + init: function (parent, model, domain) { this._super.apply(this, arguments); - try { - domain = Domain.prototype.stringToArray(domain); - } catch (err) { - // TODO: domain could contain `parent` for example, which is - // currently not handled by the DomainSelector - this.invalidDomain = true; - this.children = []; - return; + var parsedDomain = this._parseDomain(domain); + if (parsedDomain) { + this._initialize(parsedDomain); } - this._initialize(domain); }, /** * @see DomainNode.start @@ -386,6 +380,23 @@ var DomainTree = DomainNode.extend({ }); }).bind(this)); }, + /** + * @param {string} domain + * @returns {Array[]} + */ + _parseDomain: function (domain) { + var parsedDomain = false; + try { + parsedDomain = Domain.prototype.stringToArray(domain); + this.invalidDomain = false; + } catch (err) { + // TODO: domain could contain `parent` for example, which is + // currently not handled by the DomainSelector + this.invalidDomain = true; + this.children = []; + } + return parsedDomain; + }, //-------------------------------------------------------------------------- // Handlers @@ -471,14 +482,17 @@ var DomainSelector = DomainTree.extend({ * If the internal domain value was already equal to the given one, this * does nothing. * - * @param {Array|string} domain + * @param {string} domain * @returns {Deferred} resolved when the rerendering is finished */ setDomain: function (domain) { - if (Domain.prototype.arrayToString(domain) === Domain.prototype.arrayToString(this.getDomain())) { + if (domain === Domain.prototype.arrayToString(this.getDomain())) { return $.when(); } - return this._redraw(domain); + var parsedDomain = this._parseDomain(domain); + if (parsedDomain) { + return this._redraw(domain); + } }, //-------------------------------------------------------------------------- diff --git a/addons/web/static/tests/fields/basic_fields_tests.js b/addons/web/static/tests/fields/basic_fields_tests.js index 5a6c4e4b0fd..e34579c5ec3 100644 --- a/addons/web/static/tests/fields/basic_fields_tests.js +++ b/addons/web/static/tests/fields/basic_fields_tests.js @@ -4418,6 +4418,33 @@ QUnit.module('basic_fields', { QUnit.module('FieldDomain'); + QUnit.test('The domain editor should not crash the view when given a dynamic filter', function (assert) { + //dynamic filters (containing variables, such as uid, parent or today) + //are not handled by the domain editor, but it shouldn't crash the view + assert.expect(1); + + this.data.partner.records[0].foo = '[["int_field", "=", uid]]'; + + var form = createView({ + View: FormView, + model: 'partner', + data: this.data, + arch: + '<form>' + + '<field name="foo" widget="domain" options="{\'model\': \'partner\'}"/>' + + '<field name="int_field" invisible="1"/>' + + '</form>', + res_id: 1, + session: { + user_context: {uid: 14}, + }, + }); + + assert.strictEqual(form.$('.o_read_mode').text(), "This domain is not supported.", + "The widget should not crash the view, but gracefully admit its failure."); + form.destroy(); + }); + QUnit.test('basic domain field usage is ok', function (assert) { assert.expect(6); diff --git a/addons/web/static/tests/fields/relational_fields_tests.js b/addons/web/static/tests/fields/relational_fields_tests.js index 5d9ee218711..b68ac239c13 100644 --- a/addons/web/static/tests/fields/relational_fields_tests.js +++ b/addons/web/static/tests/fields/relational_fields_tests.js @@ -2227,7 +2227,7 @@ QUnit.module('relational_fields', { }); }); - QUnit.test('pressing ENTER on a \'no_quick_create\' many2one should not trigger M2ODialog', function (assert) { + QUnit.test('pressing ENTER on a \'no_quick_create\' many2one should open a M2ODialog', function (assert) { var done = assert.async(); assert.expect(2); @@ -2278,6 +2278,68 @@ QUnit.module('relational_fields', { }); }); + QUnit.test('select a value by pressing TAB on a many2one with onchange', function (assert) { + var done = assert.async(); + assert.expect(3); + + this.data.partner.onchanges.trululu = function () {}; + + var M2O_DELAY = relationalFields.FieldMany2One.prototype.AUTOCOMPLETE_DELAY; + relationalFields.FieldMany2One.prototype.AUTOCOMPLETE_DELAY = 0; + var def = $.Deferred(); + + var form = createView({ + View: FormView, + model: 'partner', + data: this.data, + arch: '<form>' + + '<field name="trululu"/>' + + '<field name="display_name"/>' + + '</form>', + mockRPC: function (route, args) { + var result = this._super.apply(this, arguments); + if (args.method === 'onchange') { + return $.when(def).then(_.constant(result)); + } + return result; + }, + res_id: 1, + viewOptions: { + mode: 'edit', + }, + }); + + var $input = form.$('.o_field_many2one input'); + $input.val("first").trigger('input'); + concurrency.delay(0).then(function () { + var evOptions = { + which: $.ui.keyCode.TAB, + keyCode: $.ui.keyCode.TAB, + }; + $input.trigger($.Event('keydown', evOptions)); + $input.trigger($.Event('keypress', evOptions)); + $input.trigger($.Event('keyup', evOptions)); + + // simulate a focusout (e.g. because the user clicks outside) + // before the onchange returns + form.$('.o_field_char').focus(); + + assert.strictEqual($('.modal').length, 0, + "there shouldn't be any modal in body"); + + // unlock the onchange + def.resolve(); + + assert.strictEqual($input.val(), 'first record', + "first record should have been selected"); + assert.strictEqual($('.modal').length, 0, + "there shouldn't be any modal in body"); + relationalFields.FieldMany2One.prototype.AUTOCOMPLETE_DELAY = M2O_DELAY; + form.destroy(); + done(); + }); + }); + QUnit.test('many2one in editable list + onchange, with enter [REQUIRE FOCUS]', function (assert) { assert.expect(6); var done = assert.async(); diff --git a/addons/web/static/tests/helpers/mock_server.js b/addons/web/static/tests/helpers/mock_server.js index 52a225d1314..59147edb896 100644 --- a/addons/web/static/tests/helpers/mock_server.js +++ b/addons/web/static/tests/helpers/mock_server.js @@ -870,7 +870,7 @@ var MockServer = Class.extend({ var field = args.field ? args.field : 'sequence'; var records = this.data[args.model].records; for (var i in args.ids) { - var record = _.findWhere(this.data[args.model].records, {id: args.ids[i]}); + var record = _.findWhere(records, {id: args.ids[i]}); record[field] = Number(i) + offset; } }, diff --git a/addons/web/static/tests/views/calendar_tests.js b/addons/web/static/tests/views/calendar_tests.js index 9728d8a7c74..f9662308e22 100644 --- a/addons/web/static/tests/views/calendar_tests.js +++ b/addons/web/static/tests/views/calendar_tests.js @@ -456,7 +456,7 @@ QUnit.module('Views', { calendar.destroy(); }); - QUnit.test('create event with timezone in week mode', function (assert) { + QUnit.test('create event with timezone in week mode European locale', function (assert) { assert.expect(5); this.data.event.records = []; @@ -486,6 +486,9 @@ QUnit.module('Views', { return 120; }, }, + translateParameters: { // Avoid issues due to localization formats + time_format: "%H:%M:%S", + }, mockRPC: function (route, args) { if (args.method === "create") { assert.deepEqual(args.kwargs.context, { @@ -504,7 +507,7 @@ QUnit.module('Views', { var $view = $('#qunit-fixture').contents(); $view.prependTo('body'); // => select with click position - var top = calendar.$('.fc-axis:contains(8am)').offset().top + 5; + var top = calendar.$('.fc-axis:contains(8:00)').offset().top + 5; var left = calendar.$('.fc-day:eq(2)').offset().left + 5; try { @@ -550,7 +553,7 @@ QUnit.module('Views', { $view.remove(); }); - QUnit.test('create event with timezone in week mode with formViewDialog', function (assert) { + QUnit.test('create event with timezone in week mode with formViewDialog European locale', function (assert) { assert.expect(8); this.data.event.records = []; @@ -589,6 +592,9 @@ QUnit.module('Views', { return 120; }, }, + translateParameters: { // Avoid issues due to localization formats + time_format: "%H:%M:%S", + }, mockRPC: function (route, args) { if (args.method === "create") { assert.deepEqual(args.kwargs.context, { @@ -610,7 +616,7 @@ QUnit.module('Views', { var $view = $('#qunit-fixture').contents(); $view.prependTo('body'); // => select with click position - var top = calendar.$('.fc-axis:contains(8am)').offset().top + 5; + var top = calendar.$('.fc-axis:contains(8:00)').offset().top + 5; var left = calendar.$('.fc-day:eq(2)').offset().left + 5; try { @@ -699,6 +705,282 @@ QUnit.module('Views', { $view.remove(); }); + QUnit.test('create event with timezone in week mode American locale', function (assert) { + assert.expect(5); + + this.data.event.records = []; + + var calendar = createView({ + View: CalendarView, + model: 'event', + data: this.data, + arch: + '<calendar class="o_calendar_test" '+ + 'event_open_popup="true" '+ + 'date_start="start" '+ + 'date_stop="stop" '+ + 'all_day="allday" '+ + 'mode="week" '+ + 'readonly_form_view_id="1">'+ + '<field name="name"/>'+ + '<field name="start"/>'+ + '<field name="allday"/>'+ + '</calendar>', + archs: archs, + viewOptions: { + initialDate: initialDate, + }, + session: { + getTZOffset: function () { + return 120; + }, + }, + translateParameters: { // Avoid issues due to localization formats + time_format: "%I:%M:%S", + }, + mockRPC: function (route, args) { + if (args.method === "create") { + assert.deepEqual(args.kwargs.context, { + "default_name": null, + "default_start": "2016-12-13 06:00:00", + "default_stop": "2016-12-13 08:00:00", + "default_allday": null + }, + "should send the context to create events"); + } + return this._super(route, args); + }, + }); + + + var $view = $('#qunit-fixture').contents(); + $view.prependTo('body'); // => select with click position + + var top = calendar.$('.fc-axis:contains(8am)').offset().top + 5; + var left = calendar.$('.fc-day:eq(2)').offset().left + 5; + + try { + testUtils.triggerPositionalMouseEvent(left, top, "mousedown"); + } catch (e) { + calendar.destroy(); + $view.remove(); + throw new Error('The test fails to simulate a click in the screen. Your screen is probably too small or your dev tools is open.'); + } + + testUtils.triggerPositionalMouseEvent(left, top + 60, "mousemove"); + + assert.strictEqual(calendar.$('.fc-content .fc-time').text(), "8:00am - 10:00am", + "should display the time in the calendar sticker"); + + testUtils.triggerPositionalMouseEvent(left, top + 60, "mouseup"); + $('.modal input:first').val('new event').trigger('input'); + $('.modal button.btn:contains(Create)').trigger('click'); + var $newevent = calendar.$('.fc-event:contains(new event)'); + + assert.strictEqual($newevent.text().replace(/[\s\n\r]+/g, ''), "8:00am-10:00amnewevent12/13/201608:00:00False", + "should display the new event with time, title and additional fields"); + + assert.deepEqual($newevent.data('fcSeg').event.record, + { + display_name: "new event", + start: fieldUtils.parse.datetime("2016-12-13 06:00:00", this.data.event.fields.start, {isUTC: true}), + stop: fieldUtils.parse.datetime("2016-12-13 08:00:00", this.data.event.fields.stop, {isUTC: true}), + allday: false, + name: "new event", + id: 1 + }, + "the new record should have the utc datetime (quickCreate)"); + + // delete record + + $newevent.trigger('click'); + $('.modal button.btn-default:contains(Delete)').trigger('click'); + $('.modal button.btn-primary:contains(Ok)').trigger('click'); + assert.strictEqual(calendar.$('.fc-content').length, 0, "should delete the record"); + + calendar.destroy(); + $view.remove(); + }); + + QUnit.test('create event with timezone in week mode with formViewDialog American locale', function (assert) { + assert.expect(8); + + this.data.event.records = []; + this.data.event.onchanges = { + allday: function (obj) { + if (obj.allday) { + obj.start_date = obj.start && obj.start.split(' ')[0] || obj.start_date; + obj.stop_date = obj.stop && obj.stop.split(' ')[0] || obj.stop_date || obj.start_date; + } else { + obj.start = obj.start_date && (obj.start_date + ' 00:00:00') || obj.start; + obj.stop = obj.stop_date && (obj.stop_date + ' 00:00:00') || obj.stop || obj.start; + } + } + }; + + var calendar = createView({ + View: CalendarView, + model: 'event', + data: this.data, + arch: + '<calendar class="o_calendar_test" '+ + 'event_open_popup="true" '+ + 'date_start="start" '+ + 'date_stop="stop" '+ + 'all_day="allday" '+ + 'mode="week" '+ + 'readonly_form_view_id="1">'+ + '<field name="name"/>'+ + '</calendar>', + archs: archs, + viewOptions: { + initialDate: initialDate, + }, + session: { + getTZOffset: function () { + return 120; + }, + }, + translateParameters: { // Avoid issues due to localization formats + time_format: "%I:%M:%S", + }, + mockRPC: function (route, args) { + if (args.method === "create") { + assert.deepEqual(args.kwargs.context, { + "default_name": "new event", + "default_start": "2016-12-13 06:00:00", + "default_stop": "2016-12-13 08:00:00", + "default_allday": null + }, + "should send the context to create events"); + } + if (args.method === "write") { + assert.deepEqual(args.args[1], expectedEvent, + "should move the event"); + } + return this._super(route, args); + }, + }); + + var $view = $('#qunit-fixture').contents(); + $view.prependTo('body'); // => select with click position + + var top = calendar.$('.fc-axis:contains(8am)').offset().top + 5; + var left = calendar.$('.fc-day:eq(2)').offset().left + 5; + + try { + testUtils.triggerPositionalMouseEvent(left, top, "mousedown"); + } catch (e) { + calendar.destroy(); + $view.remove(); + throw new Error('The test fails to simulate a click in the screen. Your screen is probably too small or your dev tools is open.'); + } + testUtils.triggerPositionalMouseEvent(left, top + 60, "mousemove"); + testUtils.triggerPositionalMouseEvent(left, top + 60, "mouseup"); + $('.modal input:first').val('new event').trigger('input'); + $('.modal button.btn:contains(Edit)').trigger('click'); + + assert.strictEqual($('.o_field_widget[name="start"] input').val(), "12/13/2016 08:00:00", + "should display the datetime"); + + $('.modal-lg .o_field_boolean[name="allday"] input').trigger('click'); + + assert.strictEqual($('.o_field_widget[name="start_date"] input').val(), "12/13/2016", + "should display the date"); + + $('.modal-lg .o_field_boolean[name="allday"] input').trigger('click'); + + assert.strictEqual($('.o_field_widget[name="start"] input').val(), "12/13/2016 02:00:00", + "should display the datetime from the date with the timezone"); + + // use datepicker to enter a date: 12/13/2016 08:00:00 + $('.o_field_widget[name="start"] input').trigger('click'); + $('.bootstrap-datetimepicker-widget .picker-switch a[data-action="togglePicker"]').trigger('click'); + $('.bootstrap-datetimepicker-widget .timepicker .timepicker-hour').trigger('click'); + $('.bootstrap-datetimepicker-widget .timepicker-hours td.hour:contains(08)').trigger('click'); + $('.bootstrap-datetimepicker-widget .picker-switch a[data-action="close"]').trigger('click'); + + // use datepicker to enter a date: 12/13/2016 10:00:00 + $('.o_field_widget[name="stop"] input').trigger('click'); + $('.bootstrap-datetimepicker-widget .picker-switch a[data-action="togglePicker"]').trigger('click'); + $('.bootstrap-datetimepicker-widget .timepicker .timepicker-hour').trigger('click'); + $('.bootstrap-datetimepicker-widget .timepicker-hours td.hour:contains(10)').trigger('click'); + $('.bootstrap-datetimepicker-widget .picker-switch a[data-action="close"]').trigger('click'); + + $('.modal-lg button.btn:contains(Save)').trigger('click'); + var $newevent = calendar.$('.fc-event:contains(new event)'); + + assert.strictEqual($newevent.text().replace(/[\s\n\r]+/g, ''), "8:00am-10:00amnewevent", + "should display the new event with time and title"); + + assert.deepEqual($newevent.data('fcSeg').event.record, + { + display_name: "new event", + start: fieldUtils.parse.datetime("2016-12-13 06:00:00", this.data.event.fields.start, {isUTC: true}), + stop: fieldUtils.parse.datetime("2016-12-13 08:00:00", this.data.event.fields.stop, {isUTC: true}), + allday: false, + name: "new event", + id: 1 + }, + "the new record should have the utc datetime (formViewDialog)"); + + var pos = calendar.$('.fc-content').offset(); + left = pos.left + 5; + top = pos.top + 5; + + // Mode this event to another day + var expectedEvent = { + "allday": false, + "start": "2016-12-12 06:00:00", + "stop": "2016-12-12 08:00:00" + }; + testUtils.triggerPositionalMouseEvent(left, top, "mousedown"); + left = calendar.$('.fc-day:eq(1)').offset().left + 5; + testUtils.triggerPositionalMouseEvent(left, top, "mousemove"); + testUtils.triggerPositionalMouseEvent(left, top, "mouseup"); + + // Move to "All day" + expectedEvent = { + "allday": true, + "start": "2016-12-12 00:00:00", + "stop": "2016-12-12 00:00:00" + }; + testUtils.triggerPositionalMouseEvent(left, top, "mousedown"); + top = calendar.$('.fc-day:eq(1)').offset().top + 5; + testUtils.triggerPositionalMouseEvent(left, top, "mousemove"); + testUtils.triggerPositionalMouseEvent(left, top, "mouseup"); + + calendar.destroy(); + $view.remove(); + }); + + QUnit.test('check calendar week column timeformat and event content timeformat', function (assert) { + assert.expect(2); + + var calendar = createView({ + View: CalendarView, + model: 'event', + data: this.data, + arch: + '<calendar date_start="start">'+ + '<field name="name"/>'+ + '</calendar>', + archs: archs, + viewOptions: { + initialDate: initialDate, + }, + translateParameters: { + time_format: "%I:%M:%S", + }, + }); + + assert.strictEqual(calendar.$('.fc-axis:contains(8am)').length, 1, "calendar should show according to timeformat"); + assert.strictEqual(calendar.$('.fc-event:first:contains(12:00am)').length, 1, + "event time format should 12 hour"); + + calendar.destroy(); + }); + QUnit.test('create all day event in week mode', function (assert) { assert.expect(3); @@ -1601,12 +1883,15 @@ QUnit.module('Views', { viewOptions: { initialDate: initialDate, }, + translateParameters: { // Avoid issues due to localization formats + time_format: "%H:%M:%S", + }, }); // Click on Tuesday 12am var $view = $('#qunit-fixture').contents(); $view.prependTo('body'); - var top = calendar.$('.fc-axis:contains(12am)').offset().top + 5; + var top = calendar.$('.fc-axis:contains(0:00)').offset().top + 5; var left = calendar.$('.fc-day:eq(2)').offset().left + 5; try { testUtils.triggerPositionalMouseEvent(left, top, "mousedown"); diff --git a/addons/web/static/tests/views/kanban_tests.js b/addons/web/static/tests/views/kanban_tests.js index a1837111a19..49d65629c00 100644 --- a/addons/web/static/tests/views/kanban_tests.js +++ b/addons/web/static/tests/views/kanban_tests.js @@ -1110,6 +1110,43 @@ QUnit.module('Views', { kanban.destroy(); }); + QUnit.test('m2o group record created does not use action defaults', function (assert) { + assert.expect(1); + + var kanban = createView({ + View: KanbanView, + model: 'partner', + data: this.data, + arch: '<kanban class="o_kanban_test" on_create="quick_create">' + + '<templates><t t-name="kanban-box">' + + '<div>' + + '<field name="foo"/>' + + '</div>' + + '</t></templates>' + + '</kanban>', + groupBy: ['product_id'], + mockRPC: function (route, args) { + if (args.method === 'name_create') { + assert.notOk(_.has(args.kwargs.context, 'default_name'), + "default_* should be removed from context"); + } + return this._super.apply(this, arguments); + }, + viewOptions: { + context: { + default_name: 'default name partner', + }, + }, + }); + kanban.renderButtons(); + + kanban.$('.o_column_quick_create input').val('new product'); + kanban.$('.o_column_quick_create button.o_kanban_add').click(); + + kanban.destroy(); + }); + + QUnit.test('quick create record fail in grouped', function (assert) { assert.expect(7); @@ -3222,6 +3259,37 @@ QUnit.module('Views', { kanban.destroy(); }); + QUnit.test('column progressbars are working with load more', function (assert) { + assert.expect(1); + + var kanban = createView({ + View: KanbanView, + model: 'partner', + data: this.data, + domain: [['bar', '=', true]], + arch: + '<kanban limit="1">' + + '<progressbar field="foo" colors=\'{"yop": "success", "gnap": "warning", "blip": "danger"}\'/>' + + '<templates><t t-name="kanban-box">' + + '<div>' + + '<field name="id"/>' + + '</div>' + + '</t></templates>' + + '</kanban>', + groupBy: ['bar'], + }); + + // we have 1 record shown, load 2 more and check it worked + kanban.$('.o_kanban_group').find('.o_kanban_load_more').click(); + kanban.$('.o_kanban_group').find('.o_kanban_load_more').click(); + var shownIDs = _.map(kanban.$('.o_kanban_record'), function(record) { + return parseInt(record.innerText); + }); + assert.deepEqual(shownIDs, [1, 2, 3], "intended records are loaded"); + + kanban.destroy(); + }); + // XXX test deactivated in saas~11.2 as kanban archiving has been removed (and readded in saas~11.4) QUnit.skip('column progressbars on archiving records update counter', function (assert) { assert.expect(4); diff --git a/addons/web/static/tests/views/list_tests.js b/addons/web/static/tests/views/list_tests.js index 79efc03f52e..980b94e644c 100644 --- a/addons/web/static/tests/views/list_tests.js +++ b/addons/web/static/tests/views/list_tests.js @@ -3178,6 +3178,76 @@ QUnit.module('Views', { list.destroy(); }); + QUnit.test('editable list, handle widget locks and unlocks on sort', function (assert) { + assert.expect(6); + + // we need another sortable field to lock/unlock the handle + this.data.foo.fields.amount.sortable = true; + // resequence makes sense on a sequence field, not on arbitrary fields + this.data.foo.records[0].int_field = 0; + this.data.foo.records[1].int_field = 1; + this.data.foo.records[2].int_field = 2; + this.data.foo.records[3].int_field = 3; + + var list = createView({ + View: ListView, + model: 'foo', + data: this.data, + arch: '<tree editable="top" default_order="int_field">' + + '<field name="int_field" widget="handle"/>' + + '<field name="amount" widget="float"/>' + + '</tree>', + }); + + assert.strictEqual(list.$('tbody').text(), '1200.00500.00300.000.00', + "default should be sorted by int_field"); + + // Drag and drop the fourth line in second position + testUtils.dragAndDrop( + list.$('.ui-sortable-handle').eq(3), + list.$('tbody tr').first(), + {position: 'bottom'} + ); + + // Handle should be unlocked at this point + assert.strictEqual(list.$('tbody').text(), '1200.000.00500.00300.00', + "drag and drop should have succeeded, as the handle is unlocked"); + + // Sorting by a field different for int_field should lock the handle + list.$('.o_column_sortable').eq(1).click(); + + assert.strictEqual(list.$('tbody').text(), '0.00300.00500.001200.00', + "should have been sorted by amount"); + + // Drag and drop the fourth line in second position (not) + testUtils.dragAndDrop( + list.$('.ui-sortable-handle').eq(3), + list.$('tbody tr').first(), + {position: 'bottom'} + ); + + assert.strictEqual(list.$('tbody').text(), '0.00300.00500.001200.00', + "drag and drop should have failed as the handle is locked"); + + // Sorting by int_field should unlock the handle + list.$('.o_column_sortable').eq(0).click(); + + assert.strictEqual(list.$('tbody').text(), '1200.000.00500.00300.00', + "records should be ordered as per the previous resequence"); + + // Drag and drop the fourth line in second position + testUtils.dragAndDrop( + list.$('.ui-sortable-handle').eq(3), + list.$('tbody tr').first(), + {position: 'bottom'} + ); + + assert.strictEqual(list.$('tbody').text(), '1200.00300.000.00500.00', + "drag and drop should have worked as the handle is unlocked"); + + list.destroy(); + }); + QUnit.test('editable list with handle widget with slow network', function (assert) { assert.expect(15); diff --git a/addons/web/static/tests/views/pivot_tests.js b/addons/web/static/tests/views/pivot_tests.js index 56d228bbf30..7500531cdc8 100644 --- a/addons/web/static/tests/views/pivot_tests.js +++ b/addons/web/static/tests/views/pivot_tests.js @@ -105,6 +105,23 @@ QUnit.module('Views', { pivot.destroy(); }); + QUnit.test('pivot rendering with widget', function (assert) { + assert.expect(1); + + var pivot = createView({ + View: PivotView, + model: "partner", + data: this.data, + arch: '<pivot string="Partners">' + + '<field name="foo" type="measure" widget="float_time"/>' + + '</pivot>', + }); + + assert.strictEqual(pivot.$('td.o_pivot_cell_value:contains(32:00)').length, 1, + "should contain a pivot cell with the sum of all records"); + pivot.destroy(); + }); + QUnit.test('pivot view without "string" attribute', function (assert) { assert.expect(1); diff --git a/addons/web_editor/static/lib/summernote/src/js/EventHandler.js b/addons/web_editor/static/lib/summernote/src/js/EventHandler.js index efa57f8111c..578bcfd7a0c 100644 --- a/addons/web_editor/static/lib/summernote/src/js/EventHandler.js +++ b/addons/web_editor/static/lib/summernote/src/js/EventHandler.js @@ -207,11 +207,15 @@ define([ var layoutInfo = dom.makeLayoutInfo(target); /* ODOO: (start_modification */ var $editable = layoutInfo.editable(); - if (!event.isDefaultPrevented()) { - modules.editor.saveRange($editable); + if (event.setStyleInfoFromEditable) { + var styleInfo = modules.editor.styleFromNode($editable); + } else { + if (!event.isDefaultPrevented()) { + modules.editor.saveRange($editable); + } + var styleInfo = modules.editor.currentStyle(target); } /* ODOO: end_modification) */ - var styleInfo = modules.editor.currentStyle(target); self.updateStyleInfo(styleInfo, layoutInfo); }, 0); }; diff --git a/addons/web_editor/static/src/js/backend/fields.js b/addons/web_editor/static/src/js/backend/fields.js index 2bed78bf84f..7742f49bcde 100644 --- a/addons/web_editor/static/src/js/backend/fields.js +++ b/addons/web_editor/static/src/js/backend/fields.js @@ -225,7 +225,8 @@ var FieldTextHtmlSimple = basic_fields.DebouncedField.extend(TranslatableFieldMi this.$content = this.$('.note-editable:first'); this.$content.html(this._textToHtml(this.value)); // trigger a mouseup to refresh the editor toolbar - this.$content.trigger('mouseup'); + var mouseupEvent = $.Event('mouseup', {'setStyleInfoFromEditable': true}); + this.$content.trigger(mouseupEvent); if (this.nodeOptions['style-inline']) { transcoder.styleToClass(this.$content); transcoder.imgToFont(this.$content); diff --git a/addons/web_editor/static/src/js/editor/rte.js b/addons/web_editor/static/src/js/editor/rte.js index f4f7f82506b..bcc6a441e27 100644 --- a/addons/web_editor/static/src/js/editor/rte.js +++ b/addons/web_editor/static/src/js/editor/rte.js @@ -157,7 +157,7 @@ var History = function History($editable) { if (aUndo[pos]) { pos = Math.min(pos, aUndo.length); - aUndo.splice(Math.max(pos,1), aUndo.length); + aUndo.splice(pos, aUndo.length); } // => make a snap when the user change editable zone (because: don't make snap for each keydown) diff --git a/addons/web_editor/static/src/js/editor/summernote.js b/addons/web_editor/static/src/js/editor/summernote.js index 657a8bbac8c..ea6c5a8eee3 100644 --- a/addons/web_editor/static/src/js/editor/summernote.js +++ b/addons/web_editor/static/src/js/editor/summernote.js @@ -1077,7 +1077,14 @@ $.summernote.pluginEvents.untab = function (event, editor, layoutInfo) { $.summernote.pluginEvents.up = function (event, editor, layoutInfo) { var r = range.create(); var node = dom.firstChild(r.sc.childNodes[r.so] || r.sc); - if (!r.isOnCell() || (!dom.isCell(node) && dom.hasContentBefore(node) && (!dom.isBR(dom.hasContentBefore(node)) || !dom.isText(node) || dom.isVisibleText(node) || dom.hasContentBefore(dom.hasContentBefore(node))))) { + if (!r.isOnCell()) { + return; + } + // check if an ancestor between node and cell has content before + var ancestor = dom.ancestor(node, function (ancestorNode) { + return dom.hasContentBefore(ancestorNode) || dom.isCell(ancestorNode); + }); + if (!dom.isCell(ancestor) && (!dom.isBR(dom.hasContentBefore(ancestor)) || !dom.isText(node) || dom.isVisibleText(node) || dom.hasContentBefore(dom.hasContentBefore(ancestor)))) { return; } event.preventDefault(); @@ -1094,7 +1101,14 @@ $.summernote.pluginEvents.up = function (event, editor, layoutInfo) { $.summernote.pluginEvents.down = function (event, editor, layoutInfo) { var r = range.create(); var node = dom.firstChild(r.sc.childNodes[r.so] || r.sc); - if (!r.isOnCell() || (!dom.isCell(node) && dom.hasContentAfter(node) && (!dom.isBR(dom.hasContentAfter(node)) || !dom.isText(node) || dom.isVisibleText(node) || dom.hasContentAfter(dom.hasContentAfter(node))))) { + if (!r.isOnCell()) { + return; + } + // check if an ancestor between node and cell has content after + var ancestor = dom.ancestor(node, function (ancestorNode) { + return dom.hasContentAfter(ancestorNode) || dom.isCell(ancestorNode); + }); + if (!dom.isCell(ancestor) && (!dom.isBR(dom.hasContentAfter(ancestor)) || !dom.isText(node) || dom.isVisibleText(node) || dom.hasContentAfter(dom.hasContentAfter(ancestor)))) { return; } event.preventDefault(); diff --git a/addons/web_editor/static/src/js/widgets/widgets.js b/addons/web_editor/static/src/js/widgets/widgets.js index 00e02e6e032..3fe18ab503e 100644 --- a/addons/web_editor/static/src/js/widgets/widgets.js +++ b/addons/web_editor/static/src/js/widgets/widgets.js @@ -507,7 +507,7 @@ var ImageWidget = MediaWidget.extend({ self.$('.well > span').remove(); self.$('.well > div').show(); _.each(attachments, function (record) { - record.src = record.url || '/web/image/' + record.id; + record.src = record.url || _.str.sprintf('/web/image/%s/%s', record.id, encodeURI(record.name)); // Name is added for SEO purposes record.isDocument = !(/gif|jpe|jpg|png/.test(record.mimetype)); }); if (error || !attachments.length) { diff --git a/addons/website_event/views/event_views.xml b/addons/website_event/views/event_views.xml index a59b27ffa5d..4c542508fa6 100644 --- a/addons/website_event/views/event_views.xml +++ b/addons/website_event/views/event_views.xml @@ -47,12 +47,12 @@ <button name="action_open_badge_editor" type="object" states="confirm" - string="Edit Badges" + string="Preview Badges" class="oe_highlight"/> <button name="action_open_badge_editor" type="object" states="done" - string="Edit Badges"/> + string="Preview Badges"/> </xpath> </field> </record> |
