[MERGE] forward port branch saas-16 up to 64d56995e0

This commit is contained in:
Christophe Simonis
2017-09-06 13:29:05 +02:00
39 changed files with 601 additions and 102 deletions
+1 -1
View File
@@ -186,7 +186,7 @@ class AccountAccount(models.Model):
elif self.internal_type == 'receivable': elif self.internal_type == 'receivable':
action_context = {'show_mode_selector': False, 'mode': 'customers'} action_context = {'show_mode_selector': False, 'mode': 'customers'}
else: else:
action_context = {'show_mode_selector': False, 'account_ids': [self.id,]} action_context = {'show_mode_selector': False, 'mode': 'accounts', 'account_ids': [self.id,]}
return { return {
'type': 'ir.actions.client', 'type': 'ir.actions.client',
'tag': 'manual_reconciliation_view', 'tag': 'manual_reconciliation_view',
@@ -1073,7 +1073,7 @@ var ManualModel = StatementModel.extend({
return self._rpc({ return self._rpc({
model: 'account.move.line', model: 'account.move.line',
method: 'get_data_for_manual_reconciliation', method: 'get_data_for_manual_reconciliation',
args: ['account', self.account_ids], args: ['account', model.account_ids || self.account_ids],
context: context, context: context,
}) })
.then(function (result) { .then(function (result) {
+1 -1
View File
@@ -445,7 +445,7 @@ class Import(models.TransientModel):
:rtype: (None, None) | (list(str), dict(int: list(str))) :rtype: (None, None) | (list(str), dict(int: list(str)))
""" """
if not options.get('headers'): if not options.get('headers'):
return None, None return [], {}
headers = next(rows) headers = next(rows)
return headers, { return headers, {
+1 -1
View File
@@ -150,7 +150,7 @@ class TestMatchHeadersMultiple(TransactionCase):
def test_noheaders(self): def test_noheaders(self):
self.assertEqual( self.assertEqual(
self.env['base_import.import']._match_headers([], [], {}), (None, None) self.env['base_import.import']._match_headers([], [], {}), ([], {})
) )
def test_nomatch(self): def test_nomatch(self):
+1 -1
View File
@@ -966,7 +966,7 @@ class Lead(models.Model):
result['activity']['today'] += 1 result['activity']['today'] += 1
if date.today() <= date_deadline <= date.today() + timedelta(days=7): if date.today() <= date_deadline <= date.today() + timedelta(days=7):
result['activity']['next_7_days'] += 1 result['activity']['next_7_days'] += 1
if date_deadline < date.today(): if date_deadline < date.today() and not opp.date_closed:
result['activity']['overdue'] += 1 result['activity']['overdue'] += 1
# Won in Opportunities # Won in Opportunities
if opp.date_closed and opp.stage_id.probability == 100: if opp.date_closed and opp.stage_id.probability == 100:
+1 -1
View File
@@ -68,7 +68,7 @@ class HrAttendance(models.Model):
('check_in', '<=', attendance.check_in), ('check_in', '<=', attendance.check_in),
('id', '!=', attendance.id), ('id', '!=', attendance.id),
], order='check_in desc', limit=1) ], order='check_in desc', limit=1)
if last_attendance_before_check_in and last_attendance_before_check_in.check_out and last_attendance_before_check_in.check_out >= attendance.check_in: if last_attendance_before_check_in and last_attendance_before_check_in.check_out and last_attendance_before_check_in.check_out > attendance.check_in:
raise exceptions.ValidationError(_("Cannot create new attendance record for %(empl_name)s, the employee was already checked in on %(datetime)s") % { raise exceptions.ValidationError(_("Cannot create new attendance record for %(empl_name)s, the employee was already checked in on %(datetime)s") % {
'empl_name': attendance.employee_id.name, 'empl_name': attendance.employee_id.name,
'datetime': fields.Datetime.to_string(fields.Datetime.context_timestamp(self, fields.Datetime.from_string(attendance.check_in))), 'datetime': fields.Datetime.to_string(fields.Datetime.context_timestamp(self, fields.Datetime.from_string(attendance.check_in))),
+9 -2
View File
@@ -299,6 +299,13 @@ class HrExpense(models.Model):
}) })
return account_move return account_move
@api.multi
def unlink(self):
for expense in self:
if expense.state in ['done']:
raise UserError(_('You cannot delete a posted expense.'))
super(HrExpense, self).unlink()
@api.multi @api.multi
def action_get_attachment_view(self): def action_get_attachment_view(self):
self.ensure_one() self.ensure_one()
@@ -442,8 +449,8 @@ class HrExpenseSheet(models.Model):
@api.multi @api.multi
def unlink(self): def unlink(self):
for expense in self: for expense in self:
if expense.state == "post": if expense.state in ['post', 'done']:
raise UserError(_("You cannot delete a posted expense.")) raise UserError(_('You cannot delete a posted or paid expense.'))
super(HrExpenseSheet, self).unlink() super(HrExpenseSheet, self).unlink()
@api.multi @api.multi
-20
View File
@@ -26,26 +26,6 @@
<field eval="True" name="global"/> <field eval="True" name="global"/>
<field name="domain_force">['|',('company_id', '=', False), ('company_id', 'child_of', [user.company_id.id])]</field> <field name="domain_force">['|',('company_id', '=', False), ('company_id', 'child_of', [user.company_id.id])]</field>
</record> </record>
<record id="hr_expense_report_prevent_unlink" model="ir.rule">
<field name="name">Expense Report Prevent Delete Employee</field>
<field name="model_id" ref="model_hr_expense_sheet"/>
<field name="domain_force">[('state', '=', 'submit')]</field>
<field name="groups" eval="[(4, ref('base.group_user'))]"/>
<field name="perm_read" eval="False"/>
<field name="perm_write" eval="False"/>
<field name="perm_create" eval="False"/>
<field name="perm_unlink" eval="True"/>
</record>
<record id="hr_expense_report_auth_unlink" model="ir.rule">
<field name="name">Expense Report Authorize Delete Officer</field>
<field name="model_id" ref="model_hr_expense_sheet"/>
<field name="domain_force">[(1, '=', 1)]</field>
<field name="groups" eval="[(4, ref('hr_expense.group_hr_expense_user'))]"/>
<field name="perm_read" eval="False"/>
<field name="perm_write" eval="False"/>
<field name="perm_create" eval="False"/>
<field name="perm_unlink" eval="True"/>
</record>
</data> </data>
</odoo> </odoo>
@@ -6,14 +6,14 @@
<record id="group_hr_timesheet_user" model="res.groups"> <record id="group_hr_timesheet_user" model="res.groups">
<field name="name">User</field> <field name="name">User</field>
<field name="category_id" ref="base.module_category_hr_timesheet"/> <field name="category_id" ref="base.module_category_hr_timesheet"/>
<field name="implied_ids" eval="[(4, ref('hr.group_hr_user'))]"/> <field name="implied_ids" eval="[(4, ref('base.group_user'))]"/>
<field name="users" eval="[(4, ref('base.user_root'))]"/> <field name="users" eval="[(4, ref('base.user_root'))]"/>
</record> </record>
<record id="group_timesheet_manager" model="res.groups"> <record id="group_timesheet_manager" model="res.groups">
<field name="name">Manager</field> <field name="name">Manager</field>
<field name="category_id" ref="base.module_category_hr_timesheet"/> <field name="category_id" ref="base.module_category_hr_timesheet"/>
<field name="implied_ids" eval="[(4, ref('group_hr_timesheet_user'))]"/> <field name="implied_ids" eval="[(4, ref('group_hr_timesheet_user')), (4, ref('hr.group_hr_user'))]"/>
<field name="users" eval="[(4, ref('base.user_root'))]"/> <field name="users" eval="[(4, ref('base.user_root'))]"/>
</record> </record>
+1 -2
View File
@@ -37,10 +37,9 @@
<field name="name">hr.department.kanban.inherit</field> <field name="name">hr.department.kanban.inherit</field>
<field name="model">hr.department</field> <field name="model">hr.department</field>
<field name="inherit_id" ref="hr.hr_department_view_kanban"/> <field name="inherit_id" ref="hr.hr_department_view_kanban"/>
<field name="groups_id" eval="[(4,ref('hr_timesheet.group_hr_timesheet_user'))]"/> <field name="groups_id" eval="[(4,ref('hr_timesheet.group_timesheet_manager'))]"/>
<field name="arch" type="xml"> <field name="arch" type="xml">
<data> <data>
<xpath expr="//div[contains(@class, 'o_kanban_manage_reports')]" position="inside"> <xpath expr="//div[contains(@class, 'o_kanban_manage_reports')]" position="inside">
<div class="row"> <div class="row">
<div class="col-xs-12 text-left"> <div class="col-xs-12 text-left">
@@ -0,0 +1,7 @@
from openerp.modules.registry import RegistryManager
def migrate(cr, version):
registry = RegistryManager.get(cr.dbname)
from openerp.addons.account.models.chart_template import migrate_tags_on_taxes
migrate_tags_on_taxes(cr, registry)
@@ -0,0 +1,7 @@
from openerp.modules.registry import RegistryManager
def migrate(cr, version):
registry = RegistryManager.get(cr.dbname)
from openerp.addons.account.models.chart_template import migrate_set_tags_and_taxes_updatable
migrate_set_tags_and_taxes_updatable(cr, registry, 'l10n_fr')
+3
View File
@@ -77,7 +77,10 @@ class MailController(http.Controller):
record_action = record_sudo.get_access_action(access_uid=uid) record_action = record_sudo.get_access_action(access_uid=uid)
else: else:
record_action = record_sudo.get_access_action() record_action = record_sudo.get_access_action()
if record_action['type'] == 'ir.actions.act_url' and record_action.get('target_type') != 'public':
return cls._redirect_to_messaging()
record_action.pop('target_type', None)
# the record has an URL redirection: use it directly # the record has an URL redirection: use it directly
if record_action['type'] == 'ir.actions.act_url': if record_action['type'] == 'ir.actions.act_url':
return werkzeug.utils.redirect(record_action['url']) return werkzeug.utils.redirect(record_action['url'])
@@ -28,7 +28,7 @@ BasicModel.include({
var localID = (record._changes && fieldName in record._changes) ? var localID = (record._changes && fieldName in record._changes) ?
record._changes[fieldName] : record._changes[fieldName] :
record.data[fieldName]; record.data[fieldName];
var list = this.localData[localID]; var list = this._applyX2ManyOperations(this.localData[localID]);
var invalidPartnerIds = []; var invalidPartnerIds = [];
_.each(list.data, function (id) { _.each(list.data, function (id) {
var record = self.localData[id]; var record = self.localData[id];
+60
View File
@@ -1064,5 +1064,65 @@ QUnit.test('fieldmany2many tags email', function (assert) {
$('.modal-footer .btn-primary').click(); $('.modal-footer .btn-primary').click();
}); });
QUnit.test('fieldmany2many tags email (edition)', function (assert) {
assert.expect(15);
this.data.partner.records[0].timmy = [12];
var form = createView({
View: FormView,
model: 'partner',
data: this.data,
res_id: 1,
arch:'<form string="Partners">' +
'<sheet>' +
'<field name="display_name"/>' +
'<field name="timmy" widget="many2many_tags_email"/>' +
'</sheet>' +
'</form>',
viewOptions: {
mode: 'edit',
},
mockRPC: function (route, args) {
if (args.method ==='read' && args.model === 'partner_type') {
assert.step(args.args[0]);
assert.deepEqual(args.args[1] , ['display_name', 'email'], "should read the email");
}
return this._super.apply(this, arguments);
},
archs: {
'partner_type,false,form': '<form string="Types"><field name="display_name"/><field name="email"/></form>',
},
});
assert.verifySteps([[12]]);
assert.strictEqual(form.$('.o_field_many2manytags[name="timmy"] span.o_tag_color_10').length, 1,
"should contain one tag");
// add an other existing tag
var $input = form.$('.o_field_many2manytags input');
$input.click(); // opens the dropdown
$input.autocomplete('widget').find('li:first').click(); // add 'silver'
assert.strictEqual($('.modal-body.o_act_window').length, 1,
"there should be one modal opened to edit the empty email");
assert.strictEqual($('.modal-body.o_act_window input[name="display_name"]').val(), "silver",
"the opened modal should be a form view dialog with the partner_type 14");
assert.strictEqual($('.modal-body.o_act_window input[name="email"]').length, 1,
"there should be an email field in the modal");
// set the email and save the modal (will rerender the form view)
$('.modal-body.o_act_window input[name="email"]').val('coucou@petite.perruche').trigger('input');
$('.modal-footer .btn-primary').click();
assert.strictEqual(form.$('.o_field_many2manytags[name="timmy"] span.o_tag_color_10').length, 2,
"should contain the second tag");
// should have read [14] three times: when opening the dropdown, when opening the modal, and
// after the save
assert.verifySteps([[12], [14], [14], [14]]);
form.destroy();
});
}); });
}); });
+1
View File
@@ -338,6 +338,7 @@ class MrpProduction(models.Model):
source_location = self.location_src_id source_location = self.location_src_id
original_quantity = self.product_qty - self.qty_produced original_quantity = self.product_qty - self.qty_produced
data = { data = {
'sequence': bom_line.sequence,
'name': self.name, 'name': self.name,
'date': self.date_planned_start, 'date': self.date_planned_start,
'date_expected': self.date_planned_start, 'date_expected': self.date_planned_start,
+7
View File
@@ -477,6 +477,13 @@ var PosDB = core.Class.extend({
} }
return orders; return orders;
}, },
set_cashier: function(cashier) {
// Always update if the user is the same as before
this.save('cashier', cashier);
},
get_cashier: function() {
return this.load('cashier');
}
}); });
return PosDB; return PosDB;
+3 -1
View File
@@ -243,6 +243,7 @@ exports.PosModel = Backbone.Model.extend({
} }
self.db.set_uuid(self.config.uuid); self.db.set_uuid(self.config.uuid);
self.set_cachier(self.get_cashier());
var orders = self.db.get_orders(); var orders = self.db.get_orders();
for (var i = 0; i < orders.length; i++) { for (var i = 0; i < orders.length; i++) {
@@ -584,11 +585,12 @@ exports.PosModel = Backbone.Model.extend({
// returns the user who is currently the cashier for this point of sale // returns the user who is currently the cashier for this point of sale
get_cashier: function(){ get_cashier: function(){
return this.get('cashier') || this.user; return this.db.get_cashier() || this.get('cashier') || this.user;
}, },
// changes the current cashier // changes the current cashier
set_cashier: function(user){ set_cashier: function(user){
this.set('cashier', user); this.set('cashier', user);
this.db.set_cashier(this.cashier);
}, },
//creates a new empty order and sets it as the current order //creates a new empty order and sets it as the current order
add_new_order: function(){ add_new_order: function(){
+5 -3
View File
@@ -26,8 +26,8 @@ class AccountAnalyticLine(models.Model):
@api.multi @api.multi
def write(self, values): def write(self, values):
# prevent to update invoiced timesheets if product_id is not of type order # prevent to update invoiced timesheets if one line is of type delivery
if self.so_line.product_id.invoice_policy != "order" and self.filtered(lambda timesheet: timesheet.timesheet_invoice_id): if self.sudo().filtered(lambda aal: aal.so_line.product_id.invoice_policy == "delivery") and self.filtered(lambda timesheet: timesheet.timesheet_invoice_id):
if any([field_name in values for field_name in ['unit_amount', 'employee_id', 'task_id', 'timesheet_revenue', 'so_line', 'amount', 'date']]): if any([field_name in values for field_name in ['unit_amount', 'employee_id', 'task_id', 'timesheet_revenue', 'so_line', 'amount', 'date']]):
raise UserError(_('You can not modify already invoiced timesheets.')) raise UserError(_('You can not modify already invoiced timesheets.'))
@@ -123,7 +123,9 @@ class AccountAnalyticLine(models.Model):
revenue = analytic_account.currency_id.round(unit_amount * sale_price * (1-(so_line.discount/100))) revenue = analytic_account.currency_id.round(unit_amount * sale_price * (1-(so_line.discount/100)))
billable_type = 'billable_time' billable_type = 'billable_time'
elif so_line.product_id.invoice_policy == 'order' and so_line.product_id.track_service == 'task': elif so_line.product_id.invoice_policy == 'order' and so_line.product_id.track_service == 'task':
quantity_hour = so_line.product_uom._compute_quantity(so_line.product_uom_qty, timesheet_uom) quantity_hour = unit_amount
if so_line.product_uom.category_id == timesheet_uom.category_id:
quantity_hour = so_line.product_uom._compute_quantity(so_line.product_uom_qty, timesheet_uom)
# compute the total revenue the SO since we are in fixed price # compute the total revenue the SO since we are in fixed price
total_revenue_so = analytic_account.currency_id.round(quantity_hour * sale_price * (1-(so_line.discount/100))) total_revenue_so = analytic_account.currency_id.round(quantity_hour * sale_price * (1-(so_line.discount/100)))
# compute the total revenue already existing (without the current timesheet line) # compute the total revenue already existing (without the current timesheet line)
@@ -186,15 +186,20 @@ class TestSaleTimesheet(TestSale):
self.assertEquals(timesheet2.timesheet_invoice_id, invoice) self.assertEquals(timesheet2.timesheet_invoice_id, invoice)
self.assertEquals(timesheet3.timesheet_invoice_id, invoice) self.assertEquals(timesheet3.timesheet_invoice_id, invoice)
# check that analytic line for product 'ordered' can be altered # check that analytic line for product 'delivery' cannot be altered
with self.assertRaises(UserError): with self.assertRaises(UserError):
timesheet1.write(dict(unit_amount=10)) timesheet1.write(dict(unit_amount=10))
self.assertNotEquals(timesheet1.unit_amount, 10) self.assertNotEquals(timesheet1.unit_amount, 10)
# check that analytic line for product 'delivered' cannot be altered # check that analytic line for product 'ordered' can be altered
timesheet3.write(dict(unit_amount=10)) timesheet3.write(dict(unit_amount=10))
self.assertEquals(timesheet3.unit_amount, 10) self.assertEquals(timesheet3.unit_amount, 10)
# check that if at least 1 analytic line is for product 'delivery', it cannot be altered
with self.assertRaises(UserError):
(timesheet1 + timesheet3).write(dict(unit_amount=15))
self.assertNotEquals(timesheet1.unit_amount, 15)
self.assertNotEquals(timesheet3.unit_amount, 15)
def test_revenue_multi_currency(self): def test_revenue_multi_currency(self):
""" Create a SO with 2 lines : one for a delivered service, one for a ordered service. Confirm """ Create a SO with 2 lines : one for a delivered service, one for a ordered service. Confirm
@@ -356,8 +356,7 @@ var DateField = Field.extend(/** @lends instance.web.search.DateField# */{
var m = moment(v, t === 'datetime' ? 'YYYY-MM-DD HH:mm:ss' : 'YYYY-MM-DD'); var m = moment(v, t === 'datetime' ? 'YYYY-MM-DD HH:mm:ss' : 'YYYY-MM-DD');
if (!m.isValid()) { return $.when(null); } if (!m.isValid()) { return $.when(null); }
var d = m.toDate(); var date_string = field_utils.format[t](m, {type: t});
var date_string = field_utils.format[this.attrs.type](d, {type: t.attrs.type});
var label = _.str.sprintf(_.str.escapeHTML( var label = _.str.sprintf(_.str.escapeHTML(
_t("Search %(field)s at: %(value)s")), { _t("Search %(field)s at: %(value)s")), {
field: '<em>' + _.escape(this.attrs.string) + '</em>', field: '<em>' + _.escape(this.attrs.string) + '</em>',
@@ -367,7 +366,7 @@ var DateField = Field.extend(/** @lends instance.web.search.DateField# */{
facet: { facet: {
category: this.attrs.string, category: this.attrs.string,
field: this, field: this,
values: [{label: date_string, value: d}] values: [{label: date_string, value: m.toDate()}]
} }
}]); }]);
} }
+6 -7
View File
@@ -151,13 +151,12 @@ var Dialog = Widget.extend({
this.$modal.modal('hide'); this.$modal.modal('hide');
this.$modal.remove(); this.$modal.remove();
setTimeout(function () { // Keep class modal-open (deleted by bootstrap hide fnct) on body to allow scrolling inside the modal var modals = $('body > .modal').filter(':visible');
var modals = $('body > .modal').filter(':visible'); if (modals.length) {
if(modals.length) { modals.last().focus();
modals.last().focus(); // Keep class modal-open (deleted by bootstrap hide fnct) on body to allow scrolling inside the modal
$('body').addClass('modal-open'); $('body').addClass('modal-open');
} }
}, 0);
} }
}); });
@@ -305,18 +305,6 @@ return core.Class.extend({
viewType = viewType === 'tree' ? 'list' : viewType; viewType = viewType === 'tree' ? 'list' : viewType;
innerFieldsView.type = viewType; innerFieldsView.type = viewType;
attrs.views[viewType] = self._processFieldsView(_.extend({}, innerFieldsView)); attrs.views[viewType] = self._processFieldsView(_.extend({}, innerFieldsView));
// default_order is like:
// 'name,id desc'
// but we need it like:
// [{name: 'id', asc: false}, {name: 'name', asc: true}]
var defaultOrder = innerFieldsView.arch.attrs.default_order;
if (defaultOrder) {
attrs.orderedBy = _.map(defaultOrder.split(','), function (order) {
order = order.trim().split(' ');
return {name: order[0], asc: order[1] !== 'desc'};
});
}
}); });
delete field.views; delete field.views;
} }
@@ -346,6 +334,23 @@ return core.Class.extend({
} }
} }
attrs.mode = mode; attrs.mode = mode;
if (mode in attrs.views) {
var view = attrs.views[mode];
// process the default_order, which is like 'name,id desc'
// but we need it like [{name: 'name', asc: true}, {name: 'id', asc: false}]
var defaultOrder = view.arch.attrs.default_order;
if (defaultOrder) {
attrs.orderedBy = _.map(defaultOrder.split(','), function (order) {
order = order.trim().split(' ');
return {name: order[0], asc: order[1] !== 'desc'};
});
}
// detect editables list has they behave differently with respect
// to the sorting (changes are not sorted directly)
if (mode === 'list' && view.arch.attrs.editable) {
attrs.keepChangesUnsorted = true;
}
}
} }
if (attrs.Widget.prototype.fieldsToFetch) { if (attrs.Widget.prototype.fieldsToFetch) {
attrs.viewType = 'default'; attrs.viewType = 'default';
@@ -337,7 +337,6 @@ var BasicModel = AbstractModel.extend({
} }
if (element.type === 'record') { if (element.type === 'record') {
var data = _.extend({}, element.data, element._changes); var data = _.extend({}, element.data, element._changes);
var relDataPoint; var relDataPoint;
for (var fieldName in data) { for (var fieldName in data) {
@@ -411,16 +410,46 @@ var BasicModel = AbstractModel.extend({
} }
// here, type === 'list' // here, type === 'list'
// apply potential changes (only for x2many lists) // apply potential changes (only for x2many lists):
element = this._applyX2ManyOperations(element); // for list having the flag _keepChangesUnsorted set to true (typically,
var listData = _.map(element.data, function (elemID) { // editable lists), we sort before apply changes by default (to keep the
return self.get(elemID, options); // changes unsorted, i.e. the edited line at the same place). However, if
}); // the user forces a sort, all changes done so far are sorted (this is the
// purpose of the _keepChangesUnsortedCount key), and changes done later
// on won't be sorted.
var count;
if (element._keepChangesUnsorted) {
// only apply a subset (by default 0) of changes before sorting
count = element._keepChangesUnsortedCount || 0;
element = this._applyX2ManyOperations(element, {to: count, position: 'bottom'});
} else {
// apply all changes before sorting
element = this._applyX2ManyOperations(element, {position: 'bottom'});
}
this._sortList(element);
if (element._keepChangesUnsorted) {
// apply the remaining changes after the sort
element = this._applyX2ManyOperations(element, {from: count, position: 'bottom'});
}
if (element._changes) {
_.each(element._changes, function (change) {
if (change.operation === 'ADD' && change.isNew) {
element.data = _.without(element.data, change.id);
if (change.position === 'top') {
element.data.unshift(change.id);
} else {
element.data.push(change.id);
}
}
});
}
var list = { var list = {
aggregateValues: _.extend({}, element.aggregateValues), aggregateValues: _.extend({}, element.aggregateValues),
context: _.extend({}, element.context), context: _.extend({}, element.context),
count: element.count, count: element.count,
data: listData, data: _.map(element.data, function (elemID) {
return self.get(elemID, options);
}),
domain: element.domain.slice(0), domain: element.domain.slice(0),
fields: element.fields, fields: element.fields,
getContext: element.getContext, getContext: element.getContext,
@@ -434,7 +463,7 @@ var BasicModel = AbstractModel.extend({
offset: element.offset, offset: element.offset,
orderedBy: element.orderedBy, orderedBy: element.orderedBy,
res_id: element.res_id, res_id: element.res_id,
res_ids: element.res_ids, res_ids: element.res_ids.slice(0),
type: 'list', type: 'list',
value: element.value, value: element.value,
viewType: element.viewType, viewType: element.viewType,
@@ -442,7 +471,6 @@ var BasicModel = AbstractModel.extend({
if (element.fieldsInfo) { if (element.fieldsInfo) {
list.fieldsInfo = element.fieldsInfo; list.fieldsInfo = element.fieldsInfo;
} }
this._sortList(list);
return list; return list;
}, },
/** /**
@@ -684,6 +712,10 @@ var BasicModel = AbstractModel.extend({
if (!options.keepChanges) { if (!options.keepChanges) {
this.discardChanges(id, {rollback: false}); this.discardChanges(id, {rollback: false});
} }
} else if (element._changes) {
_.each(element._changes, function (change) {
delete change.isNew;
});
} }
if (options.context !== undefined) { if (options.context !== undefined) {
@@ -876,8 +908,15 @@ var BasicModel = AbstractModel.extend({
var list = this.localData[list_id]; var list = this.localData[list_id];
if (list.type === 'record') { if (list.type === 'record') {
return; return;
} else if (list._changes) {
_.each(list._changes, function (change) {
delete change.isNew;
});
} }
list.offset = 0; // the user manually selected a sort order, so we sort the list with all
// the changes he made so far, even if it is editable, however, changes
// he'll made after won't be sorted if the list is editable
list._keepChangesUnsortedCount = list._changes ? list._changes.length : 0;
if (list.orderedBy.length === 0) { if (list.orderedBy.length === 0) {
list.orderedBy.push({name: fieldName, asc: true}); list.orderedBy.push({name: fieldName, asc: true});
} else if (list.orderedBy[0].name === fieldName){ } else if (list.orderedBy[0].name === fieldName){
@@ -972,7 +1011,7 @@ var BasicModel = AbstractModel.extend({
}; };
return this._makeDefaultRecord(list.model, params).then(function (id) { return this._makeDefaultRecord(list.model, params).then(function (id) {
var position = options && options.position || 'top'; var position = options && options.position || 'top';
list._changes.push({operation: 'ADD', id: id, position: position}); list._changes.push({operation: 'ADD', id: id, position: position, isNew: true});
var record = self.localData[id]; var record = self.localData[id];
list._cache[record.res_id] = id; list._cache[record.res_id] = id;
return id; return id;
@@ -1366,11 +1405,17 @@ var BasicModel = AbstractModel.extend({
// no 'ADD' operation for that dataPoint, as it would mean // no 'ADD' operation for that dataPoint, as it would mean
// that the record wasn't in the relation yet // that the record wasn't in the relation yet
var idsToRemove = command.ids; var idsToRemove = command.ids;
list._changes = _.reject(list._changes, function (change) { list._changes = _.reject(list._changes, function (change, index) {
var idInCommands = _.contains(command.ids, change.id); var idInCommands = _.contains(command.ids, change.id);
if (idInCommands && change.operation === 'ADD') { if (idInCommands && change.operation === 'ADD') {
idsToRemove = _.without(idsToRemove, change.id); idsToRemove = _.without(idsToRemove, change.id);
} }
// decrement _keepChangesUnsortedCount if we filter out an
// operation whose index is smaller than the count (as there
// will be one operation less to apply before sorting)
if (idInCommands && index < list._keepChangesUnsortedCount) {
list._keepChangesUnsortedCount--;
}
return idInCommands; return idInCommands;
}); });
_.each(idsToRemove, function (id) { _.each(idsToRemove, function (id) {
@@ -1423,19 +1468,28 @@ var BasicModel = AbstractModel.extend({
* *
* @private * @private
* @param {Object} dataPoint of type list * @param {Object} dataPoint of type list
* @param {Object} [options] mostly contains the range of operations to apply
* @param {Object} [options.from=0] the index of the first operation to apply
* @param {Object} [options.to=length] the index of the last operation to apply
* @param {Object} [options.position] if set, each new operation will be set
* accordingly at the top or the bottom of the list
* @returns {Object} element of type list in which the commands have been * @returns {Object} element of type list in which the commands have been
* applied * applied
*/ */
_applyX2ManyOperations: function (list) { _applyX2ManyOperations: function (list, options) {
if (!list.static) { if (!list.static) {
// this function only applies on x2many lists // this function only applies on x2many lists
return list; return list;
} }
var self = this; var self = this;
list = _.extend({}, list); list = _.extend({}, list);
list.data = list.data.slice(0);
list.res_ids = list.res_ids.slice(0); list.res_ids = list.res_ids.slice(0);
_.each(list._changes, function (change) { var changes = list._changes || [];
if (options) {
var to = options.to === 0 ? 0 : (options.to || changes.length);
changes = changes.slice(options.from || 0, to);
}
_.each(changes, function (change) {
var relRecord; var relRecord;
if (change.id) { if (change.id) {
relRecord = self.localData[change.id]; relRecord = self.localData[change.id];
@@ -1444,7 +1498,7 @@ var BasicModel = AbstractModel.extend({
case 'ADD': case 'ADD':
list.count++; list.count++;
var resID = relRecord ? relRecord.res_id : change.resID; var resID = relRecord ? relRecord.res_id : change.resID;
if (change.position === 'top') { if (change.position === 'top' && (options ? options.position !== 'bottom' : true)) {
list.res_ids.unshift(resID); list.res_ids.unshift(resID);
} else { } else {
list.res_ids.push(resID); list.res_ids.push(resID);
@@ -2148,6 +2202,7 @@ var BasicModel = AbstractModel.extend({
rawContext: rawContext, rawContext: rawContext,
relationField: field.relation_field, relationField: field.relation_field,
viewType: view ? view.type : fieldInfo.viewType, viewType: view ? view.type : fieldInfo.viewType,
_keepChangesUnsorted: fieldInfo.keepChangesUnsorted,
}); });
record.data[fieldName] = list.id; record.data[fieldName] = list.id;
if (!fieldInfo.__no_fetch) { if (!fieldInfo.__no_fetch) {
@@ -2834,6 +2889,7 @@ var BasicModel = AbstractModel.extend({
offset: params.offset || (type === 'record' ? _.indexOf(res_ids, res_id) : 0), offset: params.offset || (type === 'record' ? _.indexOf(res_ids, res_id) : 0),
openGroupByDefault: params.openGroupByDefault, openGroupByDefault: params.openGroupByDefault,
orderedBy: params.orderedBy || [], orderedBy: params.orderedBy || [],
_keepChangesUnsorted: params._keepChangesUnsorted,
parentID: params.parentID, parentID: params.parentID,
rawContext: params.rawContext, rawContext: params.rawContext,
relationField: params.relationField, relationField: params.relationField,
@@ -3367,7 +3423,9 @@ var BasicModel = AbstractModel.extend({
change.id = dataPoint.id; change.id = dataPoint.id;
} }
}); });
list.data.push(dataPoint.id); if (_.contains(list.res_ids, id)) {
list.data.push(dataPoint.id);
}
}); });
return list; return list;
}); });
@@ -3458,20 +3516,32 @@ var BasicModel = AbstractModel.extend({
}, },
/** /**
* Do a in-memory sort of a list resource data points. This method assumes * Do a in-memory sort of a list resource data points. This method assumes
* that the list data has already been fetched, and that the changes have * that the list data has already been fetched, and that the changes that
* been applied. Its intended use is for static datasets, such as a one2many * need to be sorted have already been applied. Its intended use is for
* in a form view. * static datasets, such as a one2many in a form view.
* *
* @param {Object} list list dataPoint on which changes have been applied * @param {Object} list list dataPoint on which (some) changes might have
* been applied; it is a copy of an internal dataPoint, not the result of
* get
*/ */
_sortList: function (list) { _sortList: function (list) {
if (!list.static) {
// only sort x2many lists
return;
}
if (list.orderedBy.length) { if (list.orderedBy.length) {
var self = this;
// sort records according to ordered_by[0] // sort records according to ordered_by[0]
var order = list.orderedBy[0]; var order = list.orderedBy[0];
var data = list.data; var data = list.data;
data.sort(function (r1, r2) { var res_ids = list.res_ids;
var data1 = r1.data; data.sort(function (record1ID, record2ID) {
var data2 = r2.data; var r1 = self.localData[record1ID];
var r2 = self.localData[record2ID];
var data1 = _.extend({}, r1.data, r1._changes);
var data2 = _.extend({}, r2.data, r2._changes);
if (data1[order.name] < data2[order.name]) { if (data1[order.name] < data2[order.name]) {
return order.asc ? -1 : 1; return order.asc ? -1 : 1;
} }
@@ -3480,6 +3550,15 @@ var BasicModel = AbstractModel.extend({
} }
return 0; return 0;
}); });
// sort res_ids accordingly (only the current range of ids, the one
// mapping the data, needs to be sorted)
var preRangeIDs = res_ids.slice(0, list.offset); // resIDs before the range
var postRangeIDs = res_ids.slice(list.offset + list.limit); // resIDs after the range
var rangeIDs = _.map(data, function (dataPointID) {
return self.localData[dataPointID].res_id;
});
list.res_ids = preRangeIDs.concat(rangeIDs).concat(postRangeIDs);
} }
}, },
/** /**
@@ -252,6 +252,24 @@ return AbstractRenderer.extend({
this.color_map[key] = index; this.color_map[key] = index;
return index; return index;
}, },
/**
* @override
*/
getLocalState: function () {
var $fcScroller = this.$calendar.find('.fc-scroller');
return {
scrollPosition: $fcScroller.scrollTop(),
};
},
/**
* @override
*/
setLocalState: function (localState) {
if (localState.scrollPosition) {
var $fcScroller = this.$calendar.find('.fc-scroller');
$fcScroller.scrollTop(localState.scrollPosition);
}
},
//-------------------------------------------------------------------------- //--------------------------------------------------------------------------
// Private // Private
@@ -376,6 +394,7 @@ return AbstractRenderer.extend({
var $calendar = this.$calendar; var $calendar = this.$calendar;
var $fc_view = $calendar.find('.fc-view'); var $fc_view = $calendar.find('.fc-view');
var scrollPosition = $fc_view.scrollLeft(); var scrollPosition = $fc_view.scrollLeft();
var scrollTop = this.$calendar.find('.fc-scroller').scrollTop();
$fc_view.scrollLeft(0); $fc_view.scrollLeft(0);
$calendar.fullCalendar('unselect'); $calendar.fullCalendar('unselect');
@@ -420,7 +439,11 @@ return AbstractRenderer.extend({
this._renderFilters(); this._renderFilters();
this.$calendar.appendTo('body'); this.$calendar.appendTo('body');
this.$calendar.fullCalendar('render'); if (scrollTop) {
this.$calendar.fullCalendar('reinitView');
} else {
this.$calendar.fullCalendar('render');
}
this._renderEvents(); this._renderEvents();
this.$calendar.prependTo(this.$('.o_calendar_view')); this.$calendar.prependTo(this.$('.o_calendar_view'));
@@ -1474,6 +1474,7 @@ QUnit.module('relational_fields', {
assert.strictEqual($('.modal').length, 1, assert.strictEqual($('.modal').length, 1,
"should have one modal in body"); "should have one modal in body");
form.destroy(); form.destroy();
relationalFields.FieldMany2One.prototype.AUTOCOMPLETE_DELAY = M2O_DELAY;
done(); done();
}); });
}); });
@@ -5406,6 +5407,118 @@ QUnit.module('relational_fields', {
}); });
QUnit.test('editing tabbed one2many (editable=bottom)', function (assert) {
assert.expect(11);
this.data.partner.records[0].turtles = [];
for (var i = 0; i < 42; i++) {
var id = 100 + i;
this.data.turtle.records.push({id: id, turtle_foo: 'turtle' + (id-99)});
this.data.partner.records[0].turtles.push(id);
}
var form = createView({
View: FormView,
model: 'partner',
data: this.data,
arch:'<form string="Partners">' +
'<sheet>' +
'<field name="turtles">' +
'<tree editable="bottom">' +
'<field name="turtle_foo"/>' +
'</tree>' +
'</field>' +
'</sheet>' +
'</form>',
res_id: 1,
mockRPC: function (route, args) {
assert.step(args.method);
if (args.method === 'write') {
assert.deepEqual(args.args[1].turtles[42], [0, false, {turtle_foo: 'rainbow dash'}]);
}
return this._super.apply(this, arguments);
},
});
form.$buttons.find('.o_form_button_edit').click();
form.$('.o_field_x2many_list_row_add a').click();
assert.strictEqual(form.$('tr.o_data_row').length, 41,
"should have 41 data rows on the current page");
assert.ok(form.$('tr.o_data_row').last().hasClass('o_selected_row'),
"last row should be selected");
form.$('.o_data_row input[name="turtle_foo"]').val('rainbow dash').trigger('input');
form.$buttons.find('.o_form_button_save').click();
assert.strictEqual(form.$('tr.o_data_row').length, 40,
"should have 40 data rows on the current page");
assert.verifySteps(['read', 'read', 'default_get', 'write', 'read', 'read']);
form.destroy();
});
QUnit.test('editing tabbed one2many (editable=top)', function (assert) {
assert.expect(14);
this.data.partner.records[0].turtles = [];
this.data.turtle.fields.turtle_foo.default = "default foo";
for (var i = 0; i < 42; i++) {
var id = 100 + i;
this.data.turtle.records.push({id: id, turtle_foo: 'turtle' + (id-99)});
this.data.partner.records[0].turtles.push(id);
}
var form = createView({
View: FormView,
model: 'partner',
data: this.data,
arch:'<form string="Partners">' +
'<sheet>' +
'<field name="turtles">' +
'<tree editable="top">' +
'<field name="turtle_foo"/>' +
'</tree>' +
'</field>' +
'</sheet>' +
'</form>',
res_id: 1,
mockRPC: function (route, args) {
assert.step(args.method);
if (args.method === 'write') {
assert.deepEqual(args.args[1].turtles[0], [0, false, {turtle_foo: 'rainbow dash'}])
}
return this._super.apply(this, arguments);
},
});
form.$buttons.find('.o_form_button_edit').click();
form.$('.o_pager_next').click();
assert.strictEqual(form.$('tr.o_data_row').length, 2,
"should have 2 data rows on the current page");
form.$('.o_field_x2many_list_row_add a').click();
assert.strictEqual(form.$('tr.o_data_row').length, 3,
"should have 3 data rows on the current page");
assert.ok(form.$('tr.o_data_row').first().hasClass('o_selected_row'),
"first row should be selected");
assert.strictEqual(form.$('tr.o_data_row input').val(), 'default foo',
"selected input should have correct string");
form.$('.o_data_row input[name="turtle_foo"]').val('rainbow dash').trigger('input');
form.$buttons.find('.o_form_button_save').click();
assert.strictEqual(form.$('tr.o_data_row').length, 40,
"should have 40 data rows on the current page");
assert.verifySteps(['read', 'read', 'read', 'default_get', 'write', 'read', 'read']);
form.destroy();
});
QUnit.module('FieldMany2Many'); QUnit.module('FieldMany2Many');
@@ -2086,4 +2086,38 @@ QUnit.module('Views', {
model.destroy(); model.destroy();
}); });
QUnit.test('only x2many lists (static) should be sorted client-side', function (assert) {
assert.expect(1);
this.params.modelName = 'partner_type';
this.params.res_id = undefined;
this.params.orderedBy = [{name: 'display_name', asc: true}];
var model = createModel({
Model: BasicModel,
data: this.data,
mockRPC: function (route) {
if (route === '/web/dataset/search_read') {
// simulate randomn sort form the server
return $.when({
length: 3,
records: [
{id: 12, display_name: "gold", date: "2017-01-25"},
{id: 15, display_name: "bronze"},
{id: 14, display_name: "silver"},
],
});
}
return this._super.apply(this, arguments);
},
});
model.load(this.params).then(function (resultID) {
var list = model.get(resultID);
assert.deepEqual(_.map(list.data, 'res_id'), [12, 15, 14],
"should have kept the order from the server");
});
model.destroy();
});
});}); });});
+147
View File
@@ -773,6 +773,153 @@ QUnit.module('Views', {
list.destroy(); list.destroy();
}); });
QUnit.test('use default_order on editable tree: sort on save', function (assert) {
assert.expect(8);
this.data.foo.records[0].o2m = [1, 3];
var form = createView({
View: FormView,
model: 'foo',
data: this.data,
arch: '<form>' +
'<sheet>' +
'<field name="o2m">' +
'<tree editable="bottom" default_order="display_name">' +
'<field name="display_name"/>' +
'</tree>' +
'</field>' +
'</sheet>' +
'</form>',
res_id: 1,
});
form.$buttons.find('.o_form_button_edit').click();
assert.ok(form.$('tbody tr:first td:contains(Value 1)').length,
"Value 1 should be first");
assert.ok(form.$('tbody tr:eq(1) td:contains(Value 3)').length,
"Value 3 should be second");
var $o2m = form.$('.o_field_widget[name=o2m]');
form.$('.o_field_x2many_list_row_add a').click();
$o2m.find('.o_field_widget').val("Value 2").trigger('input');
assert.ok(form.$('tbody tr:first td:contains(Value 1)').length,
"Value 1 should be first");
assert.ok(form.$('tbody tr:eq(1) td:contains(Value 3)').length,
"Value 3 should be second");
assert.ok(form.$('tbody tr:eq(2) td input').val(),
"Value 2 should be third (shouldn't be sorted)");
form.$buttons.find('.o_form_button_save').click();
assert.ok(form.$('tbody tr:first td:contains(Value 1)').length,
"Value 1 should be first");
assert.ok(form.$('tbody tr:eq(1) td:contains(Value 2)').length,
"Value 2 should be second (should be sorted after saving)");
assert.ok(form.$('tbody tr:eq(2) td:contains(Value 3)').length,
"Value 3 should be third");
form.destroy();
});
QUnit.test('use default_order on editable tree: sort on demand', function (assert) {
assert.expect(8);
this.data.foo.records[0].o2m = [1, 3];
this.data.bar.fields = {name: {string: "Name", type: "char", sortable: true}};
this.data.bar.records[0].name = "Value 1";
this.data.bar.records[2].name = "Value 3";
var form = createView({
View: FormView,
model: 'foo',
data: this.data,
arch: '<form>' +
'<sheet>' +
'<field name="o2m">' +
'<tree editable="bottom" default_order="name">' +
'<field name="name"/>' +
'</tree>' +
'</field>' +
'</sheet>' +
'</form>',
res_id: 1,
});
form.$buttons.find('.o_form_button_edit').click();
assert.ok(form.$('tbody tr:first td:contains(Value 1)').length,
"Value 1 should be first");
assert.ok(form.$('tbody tr:eq(1) td:contains(Value 3)').length,
"Value 3 should be second");
var $o2m = form.$('.o_field_widget[name=o2m]');
form.$('.o_field_x2many_list_row_add a').click();
$o2m.find('.o_field_widget').val("Value 2").trigger('input');
assert.ok(form.$('tbody tr:first td:contains(Value 1)').length,
"Value 1 should be first");
assert.ok(form.$('tbody tr:eq(1) td:contains(Value 3)').length,
"Value 3 should be second");
assert.ok(form.$('tbody tr:eq(2) td input').val(),
"Value 2 should be third (shouldn't be sorted)");
$o2m.find('.o_column_sortable').click();
assert.ok(form.$('tbody tr:first td:contains(Value 3)').length,
"Value 3 should be first");
assert.ok(form.$('tbody tr:eq(1) td:contains(Value 2)').length,
"Value 2 should be second (should be sorted after saving)");
assert.ok(form.$('tbody tr:eq(2) td:contains(Value 1)').length,
"Value 1 should be third");
form.destroy();
});
QUnit.test('use default_order on editable tree: sort on demand in page', function (assert) {
assert.expect(4);
this.data.bar.fields = {name: {string: "Name", type: "char", sortable: true}};
var ids = [];
for (var i=0; i<45; i++) {
var id = 4 + i;
ids.push(id);
this.data.bar.records.push({
id: id,
name: "Value " + id,
});
}
this.data.foo.records[0].o2m = ids;
var form = createView({
View: FormView,
model: 'foo',
data: this.data,
arch: '<form>' +
'<sheet>' +
'<field name="o2m">' +
'<tree editable="bottom" default_order="name">' +
'<field name="name"/>' +
'</tree>' +
'</field>' +
'</sheet>' +
'</form>',
res_id: 1,
});
// Change page
form.$('.o_pager_next').click();
assert.ok(form.$('tbody tr:first td:contains(Value 44)').length,
"record 44 should be first");
assert.ok(form.$('tbody tr:eq(4) td:contains(Value 48)').length,
"record 48 should be last");
form.$('.o_column_sortable').click();
assert.ok(form.$('tbody tr:first td:contains(Value 48)').length,
"record 48 should be first");
assert.ok(form.$('tbody tr:eq(4) td:contains(Value 44)').length,
"record 44 should be first");
form.destroy();
});
QUnit.test('can display button in edit mode', function (assert) { QUnit.test('can display button in edit mode', function (assert) {
assert.expect(1); assert.expect(1);
+1 -1
View File
@@ -10,4 +10,4 @@ class WebSuite(odoo.tests.HttpCase):
at_install = False at_install = False
def test_01_js(self): def test_01_js(self):
self.phantom_js('/web/tests?mod=web', "", "", login='admin', timeout=240) self.phantom_js('/web/tests?mod=web', "", "", login='admin', timeout=300)
@@ -227,6 +227,7 @@ class BlogPost(models.Model):
'type': 'ir.actions.act_url', 'type': 'ir.actions.act_url',
'url': self.url, 'url': self.url,
'target': 'self', 'target': 'self',
'target_type': 'public',
'res_id': self.id, 'res_id': self.id,
} }
+1
View File
@@ -783,6 +783,7 @@ class Post(models.Model):
'type': 'ir.actions.act_url', 'type': 'ir.actions.act_url',
'url': '/forum/%s/question/%s' % (self.forum_id.id, self.id), 'url': '/forum/%s/question/%s' % (self.forum_id.id, self.id),
'target': 'self', 'target': 'self',
'target_type': 'public',
'res_id': self.id, 'res_id': self.id,
} }
+5 -5
View File
@@ -205,7 +205,7 @@
</tbody> </tbody>
</table> </table>
<t t-if="not bins"> <t t-if="not bins">
<div class="text-center text-muted"> <div class="text-center text-muted oe_product">
<h3 class="css_editable_display">No product defined.</h3> <h3 class="css_editable_display">No product defined.</h3>
<p groups="sales_team.group_sale_manager">Click <i>'New'</i> in the top-right corner to create your first product.</p> <p groups="sales_team.group_sale_manager">Click <i>'New'</i> in the top-right corner to create your first product.</p>
</div> </div>
@@ -417,8 +417,8 @@
<div id="o-carousel-product" class="carousel slide" data-ride="carousel" data-interval="0"> <div id="o-carousel-product" class="carousel slide" data-ride="carousel" data-interval="0">
<div class="carousel-outer"> <div class="carousel-outer">
<div class="carousel-inner"> <div class="carousel-inner">
<div class="item active" itemprop="image" t-field="product.product_variant_id.image" t-options="{'widget': 'image', 'class': 'product_detail_img js_variant_img', 'alt-field': 'name', 'zoom': 'image', 'unique': product['__last_update'] + product.product_variant_id['__last_update']}"/> <div class="item active" itemprop="image" t-field="product.image" t-options="{'widget': 'image', 'class': 'product_detail_img js_variant_img', 'alt-field': 'name', 'zoom': 'image', 'unique': product['__last_update']}"/>
<div t-if="variant_img" class="item" itemprop="image" t-field="product.image" t-options="{'widget': 'image', 'class': 'product_detail_img', 'alt-field': 'name', 'zoom': 'image', 'unique': product['__last_update'] + product.product_variant_id['__last_update']}"/> <div t-if="variant_img" class="item" itemprop="image" t-field="product.product_variant_id.image" t-options="{'widget': 'image', 'class': 'product_detail_img', 'alt-field': 'name', 'zoom': 'image', 'unique': product['__last_update'] + (product.product_variant_id['__last_update'] or '')}"/>
<t t-if="len(image_ids)" t-foreach="image_ids" t-as="pimg"> <t t-if="len(image_ids)" t-foreach="image_ids" t-as="pimg">
<div class="item" t-field="pimg.image" t-options='{"widget": "image", "class": "product_detail_img", "alt-field": "name", "zoom": "image" }'/> <div class="item" t-field="pimg.image" t-options='{"widget": "image", "class": "product_detail_img", "alt-field": "name", "zoom": "image" }'/>
</t> </t>
@@ -438,10 +438,10 @@
<ol class="carousel-indicators" t-if="len(image_ids) or variant_img"> <ol class="carousel-indicators" t-if="len(image_ids) or variant_img">
<li data-target="#o-carousel-product" data-slide-to="0" class="active"> <li data-target="#o-carousel-product" data-slide-to="0" class="active">
<img class="img img-responsive js_variant_img_small" t-attf-src="/website/image/product.product/{{product.product_variant_ids[0].id}}/image/90x90" t-att-alt="product.name"/> <img class="img img-responsive js_variant_img_small" t-attf-src="/website/image/product.template/{{product.id}}/image/90x90" t-att-alt="product.name"/>
</li> </li>
<li t-if="variant_img" data-target="#o-carousel-product" data-slide-to="1" class=""> <li t-if="variant_img" data-target="#o-carousel-product" data-slide-to="1" class="">
<img class="img img-responsive" t-attf-src="/website/image/product.template/{{product.id}}/image/90x90" t-att-alt="product.name"/> <img class="img img-responsive" t-attf-src="/website/image/product.product/{{product.product_variant_id.id}}/image/90x90" t-att-alt="product.name"/>
</li> </li>
<t t-if="len(image_ids)" t-foreach="image_ids" t-as="pimg"> <t t-if="len(image_ids)" t-foreach="image_ids" t-as="pimg">
<li data-target="#o-carousel-product" t-att-data-slide-to="pimg_index + (variant_img and 2 or 1)"> <li data-target="#o-carousel-product" t-att-data-slide-to="pimg_index + (variant_img and 2 or 1)">
@@ -1,9 +1,15 @@
'use strict'; 'use strict';
$(document).ready(function () { $(document).ready(function () {
// When choosing an delivery carrier, update the quotation and the acquirers var $pay_button = $('.oe_sale_acquirer_button button');
$pay_button.prop('disabled', false);
// When choosing an delivery carrier, update the quotation and the acquirers. Disable the 'Pay
// Now' button to avoid being redirected to payment acquier if the delivery carrier update is
// not over.
var $carrier = $("#delivery_carrier"); var $carrier = $("#delivery_carrier");
$carrier.find("input[name='delivery_type']").click(function (ev) { $carrier.find("input[name='delivery_type']").click(function (ev) {
$pay_button.prop('disabled', true);
var carrier_id = $(ev.currentTarget).val(); var carrier_id = $(ev.currentTarget).val();
window.location.href = '/shop/payment?carrier_id=' + carrier_id; window.location.href = '/shop/payment?carrier_id=' + carrier_id;
}); });
+1
View File
@@ -446,6 +446,7 @@ class Slide(models.Model):
'type': 'ir.actions.act_url', 'type': 'ir.actions.act_url',
'url': '%s' % self.website_url, 'url': '%s' % self.website_url,
'target': 'self', 'target': 'self',
'target_type': 'public',
'res_id': self.id, 'res_id': self.id,
} }
return super(Slide, self).get_access_action(access_uid) return super(Slide, self).get_access_action(access_uid)
+4
View File
@@ -213,6 +213,10 @@ Instead you must have a proxy redirecting requests whose URL starts with
``/longpolling/`` to the longpolling port. Other request should be proxied to ``/longpolling/`` to the longpolling port. Other request should be proxied to
the :option:`normal HTTP port <odoo-bin --xmlrpc-port>` the :option:`normal HTTP port <odoo-bin --xmlrpc-port>`
.. warning:: The livechat worker requires the ``psycogreen`` Python module,
which is not always included with all installation packages.
It can be manually installed with ``pip install psycogreen``.
Configuration sample Configuration sample
-------------------- --------------------
+1 -1
View File
@@ -50,7 +50,7 @@ CREATE TABLE ir_module_module (
website character varying(256), website character varying(256),
summary character varying(256), summary character varying(256),
name character varying(128) NOT NULL, name character varying(128) NOT NULL,
author character varying(128), author character varying,
icon varchar, icon varchar,
state character varying(16), state character varying(16),
latest_version character varying(64), latest_version character varying(64),
+3 -1
View File
@@ -1538,7 +1538,9 @@ class IrModelData(models.Model):
if model == 'ir.model.fields': if model == 'ir.model.fields':
# Don't remove the LOG_ACCESS_COLUMNS unless _log_access # Don't remove the LOG_ACCESS_COLUMNS unless _log_access
# has been turned off on the model. # has been turned off on the model.
field = self.env[model].browse(res_id) field = self.env[model].browse(res_id).with_context(
prefetch_fields=False,
)
if not field.exists(): if not field.exists():
_logger.info('Deleting orphan external_ids %s', external_ids) _logger.info('Deleting orphan external_ids %s', external_ids)
external_ids.unlink() external_ids.unlink()
+5 -4
View File
@@ -371,11 +371,12 @@ try:
# TODO when xlsxwriter bump to 0.9.8, add worksheet_class=None parameter instead of kw # TODO when xlsxwriter bump to 0.9.8, add worksheet_class=None parameter instead of kw
def add_worksheet(self, name=None, **kw): def add_worksheet(self, name=None, **kw):
# invalid Excel character: []:*?/\ if name:
name = re.sub(r'[\[\]:*?/\\]', '', name) # invalid Excel character: []:*?/\
name = re.sub(r'[\[\]:*?/\\]', '', name)
# maximum size is 31 characters # maximum size is 31 characters
name = name[:31] name = name[:31]
return super(PatchedXlsxWorkbook, self).add_worksheet(name, **kw) return super(PatchedXlsxWorkbook, self).add_worksheet(name, **kw)
xlsxwriter.Workbook = PatchedXlsxWorkbook xlsxwriter.Workbook = PatchedXlsxWorkbook
+7 -3
View File
@@ -23,14 +23,18 @@ _CONFDELTYPES = {
def existing_tables(cr, tablenames): def existing_tables(cr, tablenames):
""" Return the names of existing tables among ``tablenames``. """ """ Return the names of existing tables among ``tablenames``. """
query = """ SELECT table_name FROM information_schema.tables query = """
WHERE table_name IN %s AND table_schema != 'information_schema' """ SELECT relname
FROM pg_class
WHERE relname IN %s
AND relkind IN ('r', 'v', 'm')
"""
cr.execute(query, [tuple(tablenames)]) cr.execute(query, [tuple(tablenames)])
return [row[0] for row in cr.fetchall()] return [row[0] for row in cr.fetchall()]
def table_exists(cr, tablename): def table_exists(cr, tablename):
""" Return whether the given table exists. """ """ Return whether the given table exists. """
query = "SELECT 1 FROM information_schema.tables WHERE table_name=%s AND table_schema != 'information_schema'" query = "SELECT 1 FROM pg_class WHERE relkind IN ('r', 'v', 'm') AND relname=%s"
cr.execute(query, (tablename,)) cr.execute(query, (tablename,))
return cr.rowcount return cr.rowcount