[MERGE] forward port branch saas-11.3 up to 8a0e819d0b
This commit is contained in:
@@ -305,9 +305,12 @@ var StatementAction = AbstractAction.extend(ControlPanelMixin, {
|
||||
'context': self.model.getContext(),
|
||||
});
|
||||
_.each(result.handles, function (handle) {
|
||||
self._getWidget(handle).destroy();
|
||||
var index = _.findIndex(self.widgets, function (widget) {return widget.handle===handle;});
|
||||
self.widgets.splice(index, 1);
|
||||
var widget = self._getWidget(handle);
|
||||
if (widget) {
|
||||
widget.destroy();
|
||||
var index = _.findIndex(self.widgets, function (widget) {return widget.handle===handle;});
|
||||
self.widgets.splice(index, 1);
|
||||
}
|
||||
});
|
||||
// Get number of widget and if less than constant and if there are more to laod, load until constant
|
||||
if (self.widgets.length < self.model.defaultDisplayQty
|
||||
|
||||
@@ -207,6 +207,25 @@ class HrExpense(models.Model):
|
||||
# Business
|
||||
# ----------------------------------------
|
||||
|
||||
@api.multi
|
||||
def _prepare_move_values(self):
|
||||
"""
|
||||
This function prepares move values related to an expense
|
||||
"""
|
||||
self.ensure_one()
|
||||
journal = self.sheet_id.bank_journal_id if self.payment_mode == 'company_account' else self.sheet_id.journal_id
|
||||
account_date = self.sheet_id.accounting_date or self.date
|
||||
move_values = {
|
||||
'journal_id': journal.id,
|
||||
'company_id': self.env.user.company_id.id,
|
||||
'date': account_date,
|
||||
'ref': self.sheet_id.name,
|
||||
# force the name to the default value, to avoid an eventual 'default_name' in the context
|
||||
# to set it to '' which cause no number to be given to the account.move when posted.
|
||||
'name': '/',
|
||||
}
|
||||
return move_values
|
||||
|
||||
@api.multi
|
||||
def _get_account_move_by_sheet(self):
|
||||
""" Return a mapping between the expense sheet of current expense and its account move
|
||||
@@ -215,18 +234,8 @@ class HrExpense(models.Model):
|
||||
move_grouped_by_sheet = {}
|
||||
for expense in self:
|
||||
# create the move that will contain the accounting entries
|
||||
account_date = expense.sheet_id.accounting_date or expense.date
|
||||
if expense.sheet_id.id not in move_grouped_by_sheet:
|
||||
journal = expense.sheet_id.bank_journal_id if expense.payment_mode == 'company_account' else expense.sheet_id.journal_id
|
||||
move = self.env['account.move'].create({
|
||||
'journal_id': journal.id,
|
||||
'company_id': self.env.user.company_id.id,
|
||||
'date': account_date,
|
||||
'ref': expense.sheet_id.name,
|
||||
# force the name to the default value, to avoid an eventual 'default_name' in the context
|
||||
# to set it to '' which cause no number to be given to the account.move when posted.
|
||||
'name': '/',
|
||||
})
|
||||
move = self.env['account.move'].create(expense._prepare_move_values())
|
||||
move_grouped_by_sheet[expense.sheet_id.id] = move
|
||||
else:
|
||||
move = move_grouped_by_sheet[expense.sheet_id.id]
|
||||
|
||||
@@ -305,7 +305,7 @@ class AccountFrFec(models.TransientModel):
|
||||
END
|
||||
AS PieceRef,
|
||||
TO_CHAR(am.date, 'YYYYMMDD') AS PieceDate,
|
||||
CASE WHEN aml.name IS NULL THEN '/' ELSE replace(replace(aml.name, '|', '/'), '\t', '') END AS EcritureLib,
|
||||
CASE WHEN aml.name IS NULL OR aml.name = '' THEN '/' ELSE replace(replace(aml.name, '|', '/'), '\t', '') END AS EcritureLib,
|
||||
replace(CASE WHEN aml.debit = 0 THEN '0,00' ELSE to_char(aml.debit, '000000000000000D99') END, '.', ',') AS Debit,
|
||||
replace(CASE WHEN aml.credit = 0 THEN '0,00' ELSE to_char(aml.credit, '000000000000000D99') END, '.', ',') AS Credit,
|
||||
CASE WHEN rec.name IS NULL THEN '' ELSE rec.name END AS EcritureLet,
|
||||
@@ -388,7 +388,7 @@ class AccountFrFec(models.TransientModel):
|
||||
rows_length = len(rows)
|
||||
for i, row in enumerate(rows):
|
||||
if not i == rows_length - 1:
|
||||
row.append(lineterminator)
|
||||
row[-1] += lineterminator
|
||||
writer.writerow(row)
|
||||
|
||||
fecvalue = fecfile.getvalue()
|
||||
|
||||
@@ -897,7 +897,7 @@ class PaymentTransaction(models.Model):
|
||||
|
||||
# custom create
|
||||
custom_method_name = '%s_create' % acquirer.provider
|
||||
if hasattr(acquirer, custom_method_name):
|
||||
if hasattr(self, custom_method_name):
|
||||
values.update(getattr(self, custom_method_name)(values))
|
||||
|
||||
if not values.get('reference'):
|
||||
|
||||
@@ -226,6 +226,12 @@ class AuthorizeAPI():
|
||||
etree.SubElement(customer, "email").text = partner.email or ''
|
||||
response = self._authorize_request(root)
|
||||
res = dict()
|
||||
if not response.find('customerProfileId'):
|
||||
_logger.warning(
|
||||
'Unable to create customer payment profile, data missing from transaction. Transaction_id: %s - Partner_id: %s'
|
||||
% (transaction_id, partner)
|
||||
)
|
||||
return res
|
||||
res['profile_id'] = response.find('customerProfileId').text
|
||||
res['payment_profile_id'] = response.find('customerPaymentProfileIdList/numericString').text
|
||||
root_profile = self._base_tree('getCustomerPaymentProfileRequest')
|
||||
|
||||
@@ -228,17 +228,18 @@ class TxAuthorize(models.Model):
|
||||
(self.type == 'form_save' or self.acquirer_id.save_token == 'always'):
|
||||
transaction = AuthorizeAPI(self.acquirer_id)
|
||||
res = transaction.create_customer_profile_from_tx(self.partner_id, self.acquirer_reference)
|
||||
token_id = self.env['payment.token'].create({
|
||||
'authorize_profile': res.get('profile_id'),
|
||||
'name': res.get('name'),
|
||||
'acquirer_ref': res.get('payment_profile_id'),
|
||||
'acquirer_id': self.acquirer_id.id,
|
||||
'partner_id': self.partner_id.id,
|
||||
})
|
||||
self.payment_token_id = token_id
|
||||
if res:
|
||||
token_id = self.env['payment.token'].create({
|
||||
'authorize_profile': res.get('profile_id'),
|
||||
'name': res.get('name'),
|
||||
'acquirer_ref': res.get('payment_profile_id'),
|
||||
'acquirer_id': self.acquirer_id.id,
|
||||
'partner_id': self.partner_id.id,
|
||||
})
|
||||
self.payment_token_id = token_id
|
||||
|
||||
if self.payment_token_id:
|
||||
self.payment_token_id.verified = True
|
||||
if self.payment_token_id:
|
||||
self.payment_token_id.verified = True
|
||||
return True
|
||||
elif status_code == self._authorize_pending_tx_status:
|
||||
self.write({'acquirer_reference': data.get('x_trans_id')})
|
||||
|
||||
@@ -867,7 +867,7 @@ class PosOrder(models.Model):
|
||||
# a serialnumber always has a quantity of 1 product, a lot number takes the full quantity of the order line
|
||||
qty = 1.0
|
||||
if stock_production_lot.product_id.tracking == 'lot':
|
||||
qty = pos_pack_lot.pos_order_line_id.qty
|
||||
qty = abs(pos_pack_lot.pos_order_line_id.qty)
|
||||
qty_done += qty
|
||||
pack_lots.append({'lot_id': stock_production_lot.id, 'qty': qty})
|
||||
else:
|
||||
|
||||
@@ -1473,7 +1473,7 @@ exports.Orderline = Backbone.Model.extend({
|
||||
var lots_required = 1;
|
||||
|
||||
if (this.product.tracking == 'serial') {
|
||||
lots_required = this.quantity;
|
||||
lots_required = Math.abs(this.quantity);
|
||||
}
|
||||
|
||||
return lots_required;
|
||||
@@ -1878,8 +1878,11 @@ var PacklotlineCollection = Backbone.Collection.extend({
|
||||
|
||||
set_quantity_by_lot: function() {
|
||||
if (this.order_line.product.tracking == 'serial') {
|
||||
var valid_lots = this.get_valid_lots();
|
||||
this.order_line.set_quantity(valid_lots.length);
|
||||
var valid_lots_quantity = this.get_valid_lots().length;
|
||||
if (this.order_line.quantity < 0){
|
||||
valid_lots_quantity = -valid_lots_quantity;
|
||||
}
|
||||
this.order_line.set_quantity(valid_lots_quantity);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@@ -312,7 +312,9 @@ class PurchaseOrderLine(models.Model):
|
||||
for move in self.move_ids.filtered(lambda x: x.state != 'cancel' and not x.location_dest_id.usage == "supplier"):
|
||||
qty += move.product_uom._compute_quantity(move.product_uom_qty, self.product_uom, rounding_method='HALF-UP')
|
||||
template = {
|
||||
'name': self.name or '',
|
||||
# truncate to 2000 to avoid triggering index limit error
|
||||
# TODO: remove index in master?
|
||||
'name': (self.name or '')[:2000],
|
||||
'product_id': self.product_id.id,
|
||||
'product_uom': self.product_uom.id,
|
||||
'date': self.order_id.date_order,
|
||||
|
||||
@@ -49,7 +49,7 @@ class AccountInvoice(models.Model):
|
||||
inv_type = self.type or self.env.context.get('type', 'out_invoice')
|
||||
if inv_type == 'out_invoice':
|
||||
company = self.company_id or self.env.user.company_id
|
||||
self.comment = company.with_context(lang=self.partner_id.lang).sale_note
|
||||
self.comment = company.with_context(lang=self.partner_id.lang).sale_note or (self._origin.company_id == company and self.comment)
|
||||
|
||||
@api.multi
|
||||
def action_invoice_open(self):
|
||||
|
||||
@@ -39,8 +39,8 @@ class MailThread(models.AbstractModel):
|
||||
if not partners:
|
||||
partners = self._get_default_sms_recipients()
|
||||
|
||||
# Collect numbers, we will consider the message to be sent if at least one number can be found
|
||||
numbers = list(set([i.mobile for i in partners if i.mobile]))
|
||||
# Collect numbers, we will consider the message to be sent if at least one number can be found
|
||||
numbers = list(set([i.mobile for i in partners if i.mobile]))
|
||||
|
||||
if numbers:
|
||||
try:
|
||||
|
||||
@@ -45,8 +45,8 @@ class SendSMS(models.TransientModel):
|
||||
return number
|
||||
if not phonenumbers.is_possible_number(phone_nbr) or not phonenumbers.is_valid_number(phone_nbr):
|
||||
return number
|
||||
phone_fmt = phonenumbers.PhoneNumberFormat.INTERNATIONAL
|
||||
return phonenumbers.format_number(phone_nbr, phone_fmt).replace(' ', '')
|
||||
phone_fmt = phonenumbers.PhoneNumberFormat.E164
|
||||
return phonenumbers.format_number(phone_nbr, phone_fmt)
|
||||
else:
|
||||
return number
|
||||
|
||||
@@ -62,12 +62,11 @@ class SendSMS(models.TransientModel):
|
||||
@api.model
|
||||
def default_get(self, fields):
|
||||
result = super(SendSMS, self).default_get(fields)
|
||||
|
||||
active_model = self.env.context.get('active_model')
|
||||
model = self.env[active_model]
|
||||
|
||||
records = self._get_records(model)
|
||||
if getattr(records, '_get_default_sms_recipients'):
|
||||
if not self.env.context.get('default_recipients') and active_model and hasattr(self.env[active_model], '_get_default_sms_recipients'):
|
||||
model = self.env[active_model]
|
||||
records = self._get_records(model)
|
||||
partners = records._get_default_sms_recipients()
|
||||
phone_numbers = []
|
||||
no_phone_partners = []
|
||||
@@ -84,12 +83,12 @@ class SendSMS(models.TransientModel):
|
||||
return result
|
||||
|
||||
def action_send_sms(self):
|
||||
numbers = self.recipients.split(',')
|
||||
numbers = [number.strip() for number in self.recipients.split(',') if number.strip()]
|
||||
|
||||
active_model = self.env.context.get('active_model')
|
||||
model = self.env[active_model]
|
||||
records = self._get_records(model)
|
||||
if getattr(records, 'message_post_send_sms'):
|
||||
if active_model and hasattr(self.env[active_model], 'message_post_send_sms'):
|
||||
model = self.env[active_model]
|
||||
records = self._get_records(model)
|
||||
records.message_post_send_sms(self.message, numbers=numbers)
|
||||
else:
|
||||
self.env['sms.api']._send_sms(numbers, self.message)
|
||||
|
||||
@@ -458,6 +458,10 @@ class ProductTemplate(models.Model):
|
||||
def _is_cost_method_standard(self):
|
||||
return True
|
||||
|
||||
@api.depends(
|
||||
'product_variant_ids',
|
||||
'product_variant_ids.stock_quant_ids',
|
||||
)
|
||||
def _compute_quantities(self):
|
||||
res = self._compute_quantities_dict()
|
||||
for template in self:
|
||||
|
||||
@@ -462,6 +462,7 @@ class StockMove(models.Model):
|
||||
else:
|
||||
view = self.env.ref('stock.view_stock_move_nosuggest_operations')
|
||||
|
||||
picking_type_id = self.picking_type_id or self.picking_id.picking_type_id
|
||||
return {
|
||||
'name': _('Detailed Operations'),
|
||||
'type': 'ir.actions.act_window',
|
||||
@@ -474,8 +475,8 @@ class StockMove(models.Model):
|
||||
'res_id': self.id,
|
||||
'context': dict(
|
||||
self.env.context,
|
||||
show_lots_m2o=self.has_tracking != 'none' and (self.picking_type_id.use_existing_lots or self.state == 'done' or self.origin_returned_move_id.id), # able to create lots, whatever the value of ` use_create_lots`.
|
||||
show_lots_text=self.has_tracking != 'none' and self.picking_type_id.use_create_lots and not self.picking_type_id.use_existing_lots and self.state != 'done' and not self.origin_returned_move_id.id,
|
||||
show_lots_m2o=self.has_tracking != 'none' and (picking_type_id.use_existing_lots or self.state == 'done' or self.origin_returned_move_id.id), # able to create lots, whatever the value of ` use_create_lots`.
|
||||
show_lots_text=self.has_tracking != 'none' and picking_type_id.use_create_lots and not picking_type_id.use_existing_lots and self.state != 'done' and not self.origin_returned_move_id.id,
|
||||
show_source_location=self.location_id.child_ids and self.picking_type_id.code != 'incoming',
|
||||
show_destination_location=self.location_dest_id.child_ids and self.picking_type_id.code != 'outgoing',
|
||||
show_package=not self.location_id.usage == 'supplier',
|
||||
|
||||
@@ -27,7 +27,7 @@
|
||||
<group>
|
||||
<field name="parent_location_id" invisible="1"/>
|
||||
<field name="original_location_id" invisible="1"/>
|
||||
<field name="location_id" options="{'no_create': True, 'no_open': True}" groups="stock.group_stock_multi_locations"/>
|
||||
<field name="location_id" options="{'no_create': True, 'no_open': True}" groups="stock.group_stock_multi_locations" required="1"/>
|
||||
</group>
|
||||
<footer>
|
||||
<button name="create_returns" string="Return" type="object" class="btn-primary"/>
|
||||
|
||||
@@ -72,6 +72,46 @@ class TestMailTemplate(BaseFunctionalTest, MockEmails, TestRecipients):
|
||||
body_content=self.test_record.email_from,
|
||||
attachments=[('first.txt', b'My first attachment', 'text/plain'), ('second.txt', b'My second attachment', 'text/plain')])
|
||||
|
||||
def test_composer_template_onchange_attachments(self):
|
||||
"""Tests that all attachments are added to the composer,
|
||||
static attachments are not duplicated and while reports are re-generated,
|
||||
and that intermediary attachments are dropped."""
|
||||
|
||||
composer = self.env['mail.compose.message'].with_context(default_attachment_ids=[]).create({})
|
||||
report_template = self.env.ref('web.action_report_externalpreview')
|
||||
template_1 = self.email_template.copy({
|
||||
'report_template': report_template.id,
|
||||
})
|
||||
template_2 = self.email_template.copy({
|
||||
'attachment_ids': False,
|
||||
'report_template': report_template.id,
|
||||
})
|
||||
|
||||
onchange_templates = [template_1, template_2, template_1, False]
|
||||
attachments_onchange = [composer.attachment_ids]
|
||||
# template_1 has two static attachments and one dynamically generated report,
|
||||
# template_2 only has the report, so we should get 3, 1, 3 attachments
|
||||
attachment_numbers = [0, 3, 1, 3, 0]
|
||||
|
||||
with self.env.do_in_onchange():
|
||||
for template in onchange_templates:
|
||||
onchange = composer.onchange_template_id(
|
||||
template.id if template else False, 'comment', self.test_record._name, self.test_record.id
|
||||
)
|
||||
values = composer._convert_to_record(composer._convert_to_cache(onchange['value']))
|
||||
attachments_onchange.append(values['attachment_ids'])
|
||||
composer.update(onchange['value'])
|
||||
|
||||
self.assertEqual(
|
||||
[len(attachments) for attachments in attachments_onchange],
|
||||
attachment_numbers,
|
||||
)
|
||||
|
||||
self.assertTrue(
|
||||
len(attachments_onchange[1] & attachments_onchange[3]) == 2,
|
||||
"The two static attachments on the template should be common to the two onchanges"
|
||||
)
|
||||
|
||||
@mute_logger('odoo.addons.mail.models.mail_mail')
|
||||
def test_post_post_w_template(self):
|
||||
self.test_record.sudo(self.user_employee).message_post_with_template(self.email_template.id, composition_mode='comment')
|
||||
|
||||
@@ -47,7 +47,10 @@ ActionManager.include({
|
||||
* @param {string} [state.view_type]
|
||||
*/
|
||||
loadState: function (state) {
|
||||
var callersArguments = arguments;
|
||||
var self = this;
|
||||
var action;
|
||||
var def;
|
||||
var options = {
|
||||
clear_breadcrumbs: true,
|
||||
pushState: false,
|
||||
@@ -102,11 +105,20 @@ ActionManager.include({
|
||||
action = lastAction;
|
||||
options.viewType = state.view_type;
|
||||
}
|
||||
} else if (state.sa) {
|
||||
def = this._rpc({
|
||||
route: '/web/session/get_session_action',
|
||||
params: {key: state.sa},
|
||||
}).then(function (sessionAction) {
|
||||
action = sessionAction;
|
||||
});
|
||||
}
|
||||
if (action) {
|
||||
return this.doAction(action, options);
|
||||
}
|
||||
return this._super.apply(this, arguments);
|
||||
return $.when(def).then(function () {
|
||||
if (action) {
|
||||
return self.doAction(action, options);
|
||||
}
|
||||
return self._super.apply(self, callersArguments);
|
||||
});
|
||||
},
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
|
||||
@@ -29,11 +29,17 @@ function genericJsonRpc (fct_name, params, settings, fct) {
|
||||
core.bus.trigger('rpc:result', data, result);
|
||||
if (result.error !== undefined) {
|
||||
if (result.error.data.arguments[0] !== "bus.Bus not available in test mode") {
|
||||
var func = console.error;
|
||||
if (result.error.data.exception_type === "user_error") {
|
||||
console.log("Server application error", JSON.stringify(result.error));
|
||||
} else {
|
||||
console.error("Server application error", JSON.stringify(result.error));
|
||||
func = console.log;
|
||||
}
|
||||
func(
|
||||
"Server application error\n",
|
||||
"Error code:", result.error.code, "\n",
|
||||
"Error message:", result.error.message, "\n",
|
||||
"Error data message:\n", result.error.data.message, "\n",
|
||||
"Error data debug:\n", result.error.data.debug
|
||||
);
|
||||
}
|
||||
return $.Deferred().reject("server", result.error);
|
||||
} else {
|
||||
|
||||
@@ -160,7 +160,7 @@ var CalendarController = AbstractController.extend({
|
||||
* @returns {Deferred}
|
||||
*/
|
||||
_updateRecord: function (record) {
|
||||
return this.model.updateRecord(record).then(this.reload.bind(this));
|
||||
return this.model.updateRecord(record).always(this.reload.bind(this));
|
||||
},
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
|
||||
@@ -373,8 +373,9 @@ var ListController = BasicController.extend({
|
||||
* @returns {Deferred}
|
||||
*/
|
||||
_update: function () {
|
||||
this._toggleSidebar();
|
||||
return this._super.apply(this, arguments);
|
||||
return this._super.apply(this, arguments)
|
||||
.then(this._toggleSidebar.bind(this))
|
||||
.then(this._updateButtons.bind(this, 'readonly'));
|
||||
},
|
||||
/**
|
||||
* This helper simply makes sure that the control panel buttons matches the
|
||||
|
||||
@@ -638,48 +638,48 @@ ListRenderer.include({
|
||||
_resequence: function (event, ui) {
|
||||
var self = this;
|
||||
var movedRecordID = ui.item.data('id');
|
||||
var rows = this.state.data;
|
||||
var row = _.findWhere(rows, {id: movedRecordID});
|
||||
var index0 = rows.indexOf(row);
|
||||
var index1 = ui.item.index();
|
||||
var lower = Math.min(index0, index1);
|
||||
var upper = Math.max(index0, index1) + 1;
|
||||
self.unselectRow().then(function () {
|
||||
var rows = self.state.data;
|
||||
var row = _.findWhere(rows, {id: movedRecordID});
|
||||
var index0 = rows.indexOf(row);
|
||||
var index1 = ui.item.index();
|
||||
var lower = Math.min(index0, index1);
|
||||
var upper = Math.max(index0, index1) + 1;
|
||||
|
||||
var order = _.findWhere(self.state.orderedBy, {name: self.handleField});
|
||||
var asc = !order || order.asc;
|
||||
var reorderAll = false;
|
||||
var sequence = (asc ? -1 : 1) * Infinity;
|
||||
var order = _.findWhere(self.state.orderedBy, {name: self.handleField});
|
||||
var asc = !order || order.asc;
|
||||
var reorderAll = false;
|
||||
var sequence = (asc ? -1 : 1) * Infinity;
|
||||
|
||||
// determine if we need to reorder all lines
|
||||
_.each(rows, function (row, index) {
|
||||
if ((index < lower || index >= upper) &&
|
||||
((asc && sequence >= row.data[self.handleField]) ||
|
||||
(!asc && sequence <= row.data[self.handleField]))) {
|
||||
reorderAll = true;
|
||||
}
|
||||
sequence = row.data[self.handleField];
|
||||
});
|
||||
// determine if we need to reorder all lines
|
||||
_.each(rows, function (row, index) {
|
||||
if ((index < lower || index >= upper) &&
|
||||
((asc && sequence >= row.data[self.handleField]) ||
|
||||
(!asc && sequence <= row.data[self.handleField]))) {
|
||||
reorderAll = true;
|
||||
}
|
||||
sequence = row.data[self.handleField];
|
||||
});
|
||||
|
||||
if (reorderAll) {
|
||||
rows = _.without(rows, row);
|
||||
rows.splice(index1, 0, row);
|
||||
} else {
|
||||
rows = rows.slice(lower, upper);
|
||||
rows = _.without(rows, row);
|
||||
if (index0 > index1) {
|
||||
rows.unshift(row);
|
||||
if (reorderAll) {
|
||||
rows = _.without(rows, row);
|
||||
rows.splice(index1, 0, row);
|
||||
} else {
|
||||
rows.push(row);
|
||||
rows = rows.slice(lower, upper);
|
||||
rows = _.without(rows, row);
|
||||
if (index0 > index1) {
|
||||
rows.unshift(row);
|
||||
} else {
|
||||
rows.push(row);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var sequences = _.pluck(_.pluck(rows, 'data'), self.handleField);
|
||||
var rowIDs = _.pluck(rows, 'id');
|
||||
var sequences = _.pluck(_.pluck(rows, 'data'), self.handleField);
|
||||
var rowIDs = _.pluck(rows, 'id');
|
||||
|
||||
if (!asc) {
|
||||
rowIDs.reverse();
|
||||
}
|
||||
this.unselectRow().then(function () {
|
||||
if (!asc) {
|
||||
rowIDs.reverse();
|
||||
}
|
||||
self.trigger_up('resequence', {
|
||||
rowIDs: rowIDs,
|
||||
offset: _.min(sequences),
|
||||
|
||||
@@ -140,7 +140,7 @@
|
||||
border-right: 1px solid #ddd;
|
||||
}
|
||||
.o_td_label + td {
|
||||
padding: 2px 36px 2px 8px;
|
||||
padding: 0px 36px 0px 8px;
|
||||
}
|
||||
.o_field_widget.o_text_overflow {
|
||||
width: 1px!important; // hack to make the table layout believe it is a small element (so that the table does not grow too much) ...
|
||||
|
||||
@@ -1143,6 +1143,30 @@ QUnit.module('ActionManager', {
|
||||
actionManager.destroy();
|
||||
});
|
||||
|
||||
QUnit.test('state.sa should load action from session', function (assert) {
|
||||
assert.expect(1);
|
||||
|
||||
var actionManager = createActionManager({
|
||||
actions: this.actions,
|
||||
archs: this.archs,
|
||||
data: this.data,
|
||||
mockRPC: function (route, args) {
|
||||
if (route === '/web/session/get_session_action') {
|
||||
return $.when(1);
|
||||
}
|
||||
return this._super.apply(this, arguments);
|
||||
},
|
||||
});
|
||||
actionManager.loadState({
|
||||
sa: 1,
|
||||
});
|
||||
|
||||
assert.strictEqual(actionManager.$('.o_kanban_view').length, 1,
|
||||
"should have rendered a kanban view");
|
||||
|
||||
actionManager.destroy();
|
||||
});
|
||||
|
||||
QUnit.module('Concurrency management');
|
||||
|
||||
QUnit.test('drop previous actions if possible', function (assert) {
|
||||
|
||||
@@ -7982,6 +7982,68 @@ QUnit.module('Views', {
|
||||
.trigger($.Event('keydown', {which: $.ui.keyCode.ESCAPE}));
|
||||
form.destroy();
|
||||
});
|
||||
|
||||
QUnit.test('resequence list lines when discardable lines are present', function (assert) {
|
||||
assert.expect(8);
|
||||
|
||||
var onchangeNum = 0;
|
||||
|
||||
this.data.partner.onchanges = {
|
||||
p: function (obj) {
|
||||
onchangeNum++;
|
||||
obj.foo = obj.p.length.toString();
|
||||
},
|
||||
};
|
||||
|
||||
var form = createView({
|
||||
View: FormView,
|
||||
model: 'partner',
|
||||
data: this.data,
|
||||
arch: '<form>' +
|
||||
'<field name="foo"/>' +
|
||||
'<field name="p"/>' +
|
||||
'</form>',
|
||||
archs: {
|
||||
'partner,false,list':
|
||||
'<tree editable="bottom">' +
|
||||
'<field name="int_field" widget="handle"/>' +
|
||||
'<field name="display_name" required="1"/>' +
|
||||
'</tree>',
|
||||
},
|
||||
});
|
||||
|
||||
assert.strictEqual(onchangeNum, 1, "one onchange happens when form is opened");
|
||||
assert.strictEqual(form.$('[name="foo"]').val(), "0", "onchange worked there is 0 line");
|
||||
|
||||
// Add one line
|
||||
form.$('.o_field_x2many_list_row_add a').click();
|
||||
form.$('.o_field_one2many input:first').focus();
|
||||
form.$('.o_field_one2many input:first').val('first line').trigger('input');
|
||||
form.$('input[name="foo"]').click();
|
||||
assert.strictEqual(onchangeNum, 2, "one onchange happens when a line is added");
|
||||
assert.strictEqual(form.$('[name="foo"]').val(), "1", "onchange worked there is 1 line");
|
||||
|
||||
// Drag and drop second line before first one (with 1 draft and invalid line)
|
||||
form.$('.o_field_x2many_list_row_add a').click();
|
||||
testUtils.dragAndDrop(
|
||||
form.$('.ui-sortable-handle').eq(0),
|
||||
form.$('.o_data_row').last(),
|
||||
{position: 'bottom'}
|
||||
);
|
||||
assert.strictEqual(onchangeNum, 3, "one onchange happens when lines are resequenced")
|
||||
assert.strictEqual(form.$('[name="foo"]').val(), "1", "onchange worked there is 1 line");
|
||||
|
||||
// Add a second line
|
||||
form.$('.o_field_x2many_list_row_add a').click();
|
||||
form.$('.o_field_one2many input:first').focus();
|
||||
form.$('.o_field_one2many input:first').val('second line').trigger('input');
|
||||
form.$('input[name="foo"]').click();
|
||||
assert.strictEqual(onchangeNum, 4, "one onchange happens when a line is added");
|
||||
assert.strictEqual(form.$('[name="foo"]').val(), "2", "onchange worked there is 2 lines");
|
||||
|
||||
form.destroy();
|
||||
});
|
||||
|
||||
QUnit.test('if the focus is on the discard button, hitting ESCAPE should discard', function (assert) {
|
||||
assert.expect(1);
|
||||
|
||||
|
||||
@@ -168,9 +168,9 @@ sAnimations.registry.ProductWishlist = sAnimations.Class.extend(ProductConfigura
|
||||
wSaleUtils.animateClone($('#my_cart'), tr, 25, 40);
|
||||
|
||||
if ($('#b2b_wish').is(':checked')) {
|
||||
return this._addToCart(product, tr.find('qty').val() || 1);
|
||||
return this._addToCart(product, tr.find('add_qty').val() || 1);
|
||||
} else {
|
||||
var adding_deffered = this._addToCart(product, tr.find('qty').val() || 1);
|
||||
var adding_deffered = this._addToCart(product, tr.find('add_qty').val() || 1);
|
||||
this._removeWish(e, adding_deffered);
|
||||
return adding_deffered;
|
||||
}
|
||||
|
||||
@@ -27,3 +27,4 @@ Thomas Binsfeld thomas.binsfeld@acsone.eu https://github.com/ThomasBinsfeld
|
||||
Benjamin Willig benjamin.willig@acsone.eu https://github.com/benwillig
|
||||
François Honoré francois.honore@acsone.eu https://github.com/acsonefho
|
||||
Denis Robinet denis.robinet@acsone.eu https://github.com/RobinetDenisAcsone
|
||||
Benoit Aimont benoit.aimont@acsone.eu https://github.com/baimont
|
||||
|
||||
@@ -44,4 +44,5 @@ Alexander Olivares alexander@vauxoo.com https://github.com/alxolivares
|
||||
Jose Manuel Robles josemanuel@vauxoo.com https://github.com/keylor2906
|
||||
Erick Birbe erick@vauxoo.com https://github.com/ebirbe
|
||||
Tomas Alvarez tomas@vauxoo.com https://github.com/tomeyro
|
||||
Carmen Liliana Miranda González carmen@vauxoo.com https://github.com/CarmenMiranda
|
||||
Arturo Flores arturo@vauxoo.com https://github.com/umiphos
|
||||
|
||||
@@ -251,11 +251,12 @@ class TestFields(common.TransactionCase):
|
||||
self.assertEqual(c.display_name, 'B / C')
|
||||
self.assertEqual(d.display_name, 'B / C / D')
|
||||
|
||||
b.name = 'X'
|
||||
# rename several records to trigger several recomputations at once
|
||||
(d + c + b).write({'name': 'X'})
|
||||
self.assertEqual(a.display_name, 'A')
|
||||
self.assertEqual(b.display_name, 'X')
|
||||
self.assertEqual(c.display_name, 'X / C')
|
||||
self.assertEqual(d.display_name, 'X / C / D')
|
||||
self.assertEqual(c.display_name, 'X / X')
|
||||
self.assertEqual(d.display_name, 'X / X / X')
|
||||
|
||||
# delete b; both c and d are deleted in cascade; c should also be marked
|
||||
# to recompute, but recomputation should not fail...
|
||||
|
||||
@@ -1069,6 +1069,8 @@ class Field(MetaField('DummyField', (object,), {})):
|
||||
recs = record._recompute_check(self)
|
||||
if recs:
|
||||
# recompute the value (only in cache)
|
||||
if self.recursive:
|
||||
recs = record
|
||||
self.compute_value(recs)
|
||||
# HACK: if result is in the wrong cache, copy values
|
||||
if recs.env != env:
|
||||
|
||||
+28
-16
@@ -5388,6 +5388,19 @@ class BaseModel(MetaModel('DummyModel', (object,), {'_register': False})):
|
||||
if subnames else record[name]
|
||||
)
|
||||
|
||||
def has_changed(self, name):
|
||||
""" Return whether a field on record has changed. """
|
||||
record = self['<record>']
|
||||
subnames = self['<tree>'][name]
|
||||
if not subnames:
|
||||
return self[name] != record[name]
|
||||
else:
|
||||
return len(self[name]) != len(record[name]) or any(
|
||||
line_snapshot.has_changed(subname)
|
||||
for line_snapshot in self[name]
|
||||
for subname in subnames
|
||||
)
|
||||
|
||||
def diff(self, other):
|
||||
""" Return the values in ``self`` that differ from ``other``.
|
||||
Requires record cache invalidation for correct output!
|
||||
@@ -5465,25 +5478,24 @@ class BaseModel(MetaModel('DummyModel', (object,), {'_register': False})):
|
||||
|
||||
result = {'warnings': OrderedSet()}
|
||||
|
||||
# process names in order (or the keys of values if no name given)
|
||||
while todo:
|
||||
name = todo.pop(0)
|
||||
if name in done:
|
||||
continue
|
||||
done.add(name)
|
||||
|
||||
with env.do_in_onchange():
|
||||
# process names in order
|
||||
with env.do_in_onchange():
|
||||
while todo:
|
||||
# apply field-specific onchange methods
|
||||
if field_onchange.get(name):
|
||||
record._onchange_eval(name, field_onchange[name], result)
|
||||
for name in todo:
|
||||
if field_onchange.get(name):
|
||||
record._onchange_eval(name, field_onchange[name], result)
|
||||
done.add(name)
|
||||
|
||||
# make a snapshot (this forces evaluation of computed fields)
|
||||
snapshot1 = Snapshot(record, nametree)
|
||||
# determine which fields to process for the next pass
|
||||
todo = [
|
||||
name
|
||||
for name in nametree
|
||||
if name not in done and snapshot0.has_changed(name)
|
||||
]
|
||||
|
||||
# determine which fields have been modified
|
||||
for name in nametree:
|
||||
if snapshot1[name] != snapshot0[name]:
|
||||
todo.append(name)
|
||||
# make the snapshot with the final values of record
|
||||
snapshot1 = Snapshot(record, nametree)
|
||||
|
||||
# determine values that have changed by comparing snapshots
|
||||
self.invalidate_cache()
|
||||
|
||||
@@ -964,8 +964,11 @@ def trans_generate(lang, modules, cr):
|
||||
extra_comments = extra_comments or []
|
||||
if not module: return
|
||||
src_file = open(fabsolutepath, 'rb')
|
||||
options = {}
|
||||
if extract_method == 'python':
|
||||
options['encoding'] = 'UTF-8'
|
||||
try:
|
||||
for extracted in extract.extract(extract_method, src_file, keywords=extract_keywords):
|
||||
for extracted in extract.extract(extract_method, src_file, keywords=extract_keywords, options=options):
|
||||
# Babel 0.9.6 yields lineno, message, comments
|
||||
# Babel 1.3 yields lineno, message, comments, context
|
||||
lineno, message, comments = extracted[:3]
|
||||
|
||||
Reference in New Issue
Block a user