summaryrefslogtreecommitdiff
path: root/addons
diff options
context:
space:
mode:
Diffstat (limited to 'addons')
-rw-r--r--addons/account/report/account_invoice_report.py13
-rw-r--r--addons/account/static/src/js/reconciliation/reconciliation_model.js10
-rw-r--r--addons/account_payment/models/payment.py2
-rw-r--r--addons/calendar/models/calendar.py6
-rw-r--r--addons/delivery/models/delivery_carrier.py6
-rw-r--r--addons/delivery/models/delivery_grid.py2
-rw-r--r--addons/hr_attendance/static/src/js/my_attendances.js6
-rw-r--r--addons/hr_gamification/wizard/gamification_badge_user_wizard.py17
-rw-r--r--addons/hr_recruitment/views/hr_recruitment_views.xml2
-rw-r--r--addons/hr_timesheet/views/hr_timesheet_views.xml2
-rw-r--r--addons/hw_posbox_homepage/controllers/main.py2
-rw-r--r--addons/l10n_th/data/l10n_th_chart_data.xml24
-rw-r--r--addons/mail/models/mail_activity.py8
-rw-r--r--addons/mass_mailing_event/models/event.py8
-rw-r--r--addons/mrp/views/mrp_workorder_views.xml2
-rw-r--r--addons/mrp/wizard/mrp_product_produce.py36
-rw-r--r--addons/payment_authorize/models/authorize_request.py8
-rw-r--r--addons/point_of_sale/models/pos_order.py5
-rw-r--r--addons/point_of_sale/static/src/js/models.js9
-rw-r--r--addons/point_of_sale/static/src/js/screens.js7
-rw-r--r--addons/point_of_sale/views/pos_config_view.xml4
-rw-r--r--addons/pos_sale/models/crm_team.py2
-rw-r--r--addons/project/controllers/portal.py11
-rw-r--r--addons/project/models/project.py2
-rw-r--r--addons/purchase/models/account_invoice.py1
-rw-r--r--addons/sale/models/analytic.py9
-rw-r--r--addons/sale_payment/controllers/payment.py3
-rw-r--r--addons/sale_stock/report/stock_report_deliveryslip.xml4
-rw-r--r--addons/stock/models/product.py20
-rw-r--r--addons/stock/models/stock_move.py33
-rw-r--r--addons/stock/report/report_deliveryslip.xml10
-rw-r--r--addons/stock_account/views/stock_account_views.xml2
-rw-r--r--addons/transifex/models/ir_translation.py2
-rw-r--r--addons/web/controllers/main.py2
-rw-r--r--addons/web/static/src/js/fields/basic_fields.js7
-rw-r--r--addons/web/static/src/js/fields/relational_fields.js5
-rw-r--r--addons/web/static/src/js/views/basic/basic_model.js4
-rw-r--r--addons/web/static/src/js/views/calendar/calendar_model.js1
-rw-r--r--addons/web/static/src/js/views/calendar/calendar_renderer.js3
-rw-r--r--addons/web/static/src/js/views/kanban/kanban_model.js9
-rw-r--r--addons/web/static/src/js/views/list/list_renderer.js3
-rw-r--r--addons/web/static/src/js/views/pivot/pivot_renderer.js13
-rw-r--r--addons/web/static/src/js/views/pivot/pivot_view.js7
-rw-r--r--addons/web/static/src/js/widgets/domain_selector.js40
-rw-r--r--addons/web/static/tests/fields/basic_fields_tests.js27
-rw-r--r--addons/web/static/tests/fields/relational_fields_tests.js64
-rw-r--r--addons/web/static/tests/helpers/mock_server.js2
-rw-r--r--addons/web/static/tests/views/calendar_tests.js295
-rw-r--r--addons/web/static/tests/views/kanban_tests.js68
-rw-r--r--addons/web/static/tests/views/list_tests.js70
-rw-r--r--addons/web/static/tests/views/pivot_tests.js17
-rw-r--r--addons/web_editor/static/lib/summernote/src/js/EventHandler.js10
-rw-r--r--addons/web_editor/static/src/js/backend/fields.js3
-rw-r--r--addons/web_editor/static/src/js/editor/rte.js2
-rw-r--r--addons/web_editor/static/src/js/editor/summernote.js18
-rw-r--r--addons/web_editor/static/src/js/widgets/widgets.js2
-rw-r--r--addons/website_event/views/event_views.xml4
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>