[MERGE] forward port branch saas-11.3 up to 387d5cff90

closes odoo/odoo#30566
This commit is contained in:
Christophe Simonis
2019-01-25 17:29:34 +00:00
32 changed files with 386 additions and 63 deletions
+13 -4
View File
@@ -419,9 +419,6 @@ class AccountMove(models.Model):
journal_id=journal_id,
auto=auto)
reversed_moves |= reversed_move
#unreconcile all lines reversed
aml = ac_move.line_ids.filtered(lambda x: x.account_id.reconcile or x.account_id.internal_type == 'liquidity')
aml.remove_move_reconcile()
#reconcile together the reconcilable (or the liquidity aml) and their newly created counterpart
for account in set([x.account_id for x in aml]):
to_rec = aml.filtered(lambda y: y.account_id == account)
@@ -438,6 +435,18 @@ class AccountMove(models.Model):
def open_reconcile_view(self):
return self.line_ids.open_reconcile_view()
# FIXME: Clarify me and change me in master
@api.multi
def action_duplicate(self):
self.ensure_one()
action = self.env.ref('account.action_move_journal_line').read()[0]
action['target'] = 'inline'
action['context'] = dict(self.env.context)
action['context']['view_no_maturity'] = False
action['views'] = [(self.env.ref('account.view_move_form').id, 'form')]
action['res_id'] = self.copy().id
return action
@api.model
def _run_reverses_entries(self):
''' This method is called from a cron job. '''
@@ -627,7 +636,7 @@ class AccountMoveLine(models.Model):
index=True, store=True, copy=False) # related is required
blocked = fields.Boolean(string='No Follow-up', default=False,
help="You can check this box to mark this journal item as a litigation with the associated partner")
date_maturity = fields.Date(string='Due date', index=True, required=True,
date_maturity = fields.Date(string='Due date', index=True, required=True, copy=False,
help="This field is used for payable and receivable journal entries. You can put the limit date for the payment of this line.")
date = fields.Date(related='move_id.date', string='Date', index=True, store=True, copy=False, readonly=False) # related is required
analytic_line_ids = fields.One2many('account.analytic.line', 'move_id', string='Analytic lines', oldname="analytic_lines")
+1 -1
View File
@@ -204,7 +204,7 @@ class ResPartner(models.Model):
@api.multi
def _credit_debit_get(self):
tables, where_clause, where_params = self.env['account.move.line']._query_get()
tables, where_clause, where_params = self.env['account.move.line'].with_context(company_id=self.env.user.company_id.id)._query_get()
where_params = [tuple(self.ids)] + where_params
if where_clause:
where_clause = 'AND ' + where_clause
@@ -162,4 +162,8 @@ var ShowPaymentLineWidget = AbstractField.extend({
field_registry.add('payment', ShowPaymentLineWidget);
return {
ShowPaymentLineWidget: ShowPaymentLineWidget
};
});
+2 -1
View File
@@ -1556,10 +1556,11 @@
<field name="name">account.move.form</field>
<field name="model">account.move</field>
<field name="arch" type="xml">
<form string="Account Entry">
<form string="Account Entry" duplicate="false">
<header>
<button name="action_post" states="draft" string="Post" type="object" class="oe_highlight" groups="account.group_account_invoice"/>
<button name="%(action_view_account_move_reversal)d" states="posted" string="Reverse Entry" type="action" groups="account.group_account_invoice"/>
<button name="action_duplicate" string="Duplicate" type="object" groups="account.group_account_invoice"/>
<field name="state" widget="statusbar"/>
</header>
<sheet>
+3 -2
View File
@@ -15,13 +15,14 @@ class ResPartner(models.Model):
self.env.cr.execute("""
SELECT
U.partner_id as id,
CASE WHEN age(now() AT TIME ZONE 'UTC', B.last_poll) > interval %s THEN 'offline'
WHEN age(now() AT TIME ZONE 'UTC', B.last_presence) > interval %s THEN 'away'
CASE WHEN age(now() AT TIME ZONE 'UTC', max(B.last_poll)) > interval %s THEN 'offline'
WHEN age(now() AT TIME ZONE 'UTC', max(B.last_presence)) > interval %s THEN 'away'
ELSE 'online'
END as status
FROM bus_presence B
JOIN res_users U ON B.user_id = U.id
WHERE U.partner_id IN %s AND U.active = 't'
GROUP BY U.partner_id
""", ("%s seconds" % DISCONNECTION_TIMER, "%s seconds" % AWAY_TIMER, tuple(self.ids)))
res = dict(((status['id'], status['status']) for status in self.env.cr.dictfetchall()))
for partner in self:
+1 -1
View File
@@ -70,7 +70,7 @@ class DeliveryCarrier(models.Model):
def install_more_provider(self):
return {
'name': 'New Providers',
'view_mode': 'kanban',
'view_mode': 'kanban,form',
'res_model': 'ir.module.module',
'domain': [['name', 'ilike', 'delivery_']],
'type': 'ir.actions.act_window',
+1
View File
@@ -59,6 +59,7 @@ class FleetVehicle(models.Model):
fuel_type = fields.Selection([
('gasoline', 'Gasoline'),
('diesel', 'Diesel'),
('lpg', 'LPG'),
('electric', 'Electric'),
('hybrid', 'Hybrid')
], 'Fuel Type', help='Fuel Used by the vehicle')
+20 -11
View File
@@ -45,15 +45,21 @@ class FleetVehicle(models.Model):
elif contract.cost_frequency == "yearly":
car.total_cost += contract.cost_generated / 12.0
def _get_co2_fee(self, co2):
return max((((co2 * 9.0) - 600.0) * 1.2488) / 12.0, 0.0)
def _get_co2_fee(self, co2, fuel_type):
fuel_coefficient = {'diesel': 600, 'gasoline': 768, 'lpg': 990, 'electric': 0, 'hybrid': 600}
co2_fee = 0
if fuel_type and fuel_type != 'electric':
if not co2:
co2 = 165 if fuel_type in ['diesel', 'hybrid'] else 182
co2_fee = (((co2 * 9.0) - fuel_coefficient.get(fuel_type, 0)) * 144.97 / 114.08) / 12.0
return max(co2_fee, 26.47)
@api.depends('co2')
@api.depends('co2', 'fuel_type')
def _compute_co2_fee(self):
for car in self:
car.co2_fee = self._get_co2_fee(car.co2)
car.co2_fee = self._get_co2_fee(car.co2, car.fuel_type)
@api.depends('fuel_type', 'car_value', 'acquisition_date')
@api.depends('fuel_type', 'car_value', 'acquisition_date', 'co2')
def _compute_car_atn(self):
for car in self:
car.atn = car._get_car_atn(car.acquisition_date, car.car_value, car.fuel_type, car.co2)
@@ -111,15 +117,18 @@ class FleetVehicle(models.Model):
atn = 0.0
else:
if fuel_type in ['diesel', 'hybrid']:
reference = 87.0
reference = 88.0
else:
reference = 105.0
reference = 107.0
if not co2:
co2 = 195 if fuel_type in ['diesel', 'hybrid'] else 205
if co2 <= reference:
atn = car_value * max(0.04, (0.055 - 0.001 * (reference - co2))) * magic_coeff
else:
atn = car_value * min(0.18, (0.055 + 0.001 * (co2 - reference))) * magic_coeff
return max(1280, atn) / 12.0
return max(1310, atn) / 12.0
@api.onchange('model_id')
def _onchange_model_id(self):
@@ -140,7 +149,7 @@ class FleetVehicleModel(models.Model):
default_recurring_cost_amount_depreciated = fields.Float(string="Cost (Depreciated)",
help="Default recurring cost amount that should be applied to a new car from this model")
default_co2 = fields.Float(string="CO2 emissions")
default_fuel_type = fields.Selection([('gasoline', 'Gasoline'), ('diesel', 'Diesel'), ('electric', 'Electric'), ('hybrid', 'Hybrid')], 'Fuel Type', help='Fuel Used by the vehicle')
default_fuel_type = fields.Selection([('gasoline', 'Gasoline'), ('diesel', 'Diesel'), ('lpg', 'LPG'), ('electric', 'Electric'), ('hybrid', 'Hybrid')], 'Fuel Type', help='Fuel Used by the vehicle')
default_car_value = fields.Float(string="Catalog Value (VAT Incl.)")
can_be_requested = fields.Boolean(string="Can be requested", help="Can be requested on a contract as a new car")
default_atn = fields.Float(compute='_compute_atn', string="ATN")
@@ -158,7 +167,7 @@ class FleetVehicleModel(models.Model):
for model in self:
model.default_total_depreciated_cost = model.co2_fee + model.default_recurring_cost_amount_depreciated
@api.depends('default_co2')
@api.depends('default_co2', 'default_fuel_type')
def _compute_co2_fee(self):
for model in self:
model.co2_fee = self.env['fleet.vehicle']._get_co2_fee(model.default_co2)
model.co2_fee = self.env['fleet.vehicle']._get_co2_fee(model.default_co2, model.default_fuel_type)
+1
View File
@@ -266,6 +266,7 @@ var Chatter = Widget.extend({
recordName: this.recordName,
defaultBody: oldComposer && oldComposer.$input && oldComposer.$input.val(),
defaultMentionSelections: oldComposer && oldComposer.getMentionListenerSelections(),
attachmentIds: (oldComposer && oldComposer.get('attachment_ids')) || [],
});
this._composer.on('input_focused', this, function () {
this._composer.mentionSetPrefetchedPartners(this._mentionSuggestions || []);
@@ -56,7 +56,7 @@ var BasicComposer = Widget.extend({
// Attachments
this._attachmentDataSet = new data.DataSetSearch(this, 'ir.attachment', this.context);
this.fileuploadID = _.uniqueId('o_chat_fileupload');
this.set('attachment_ids', []);
this.set('attachment_ids', options.attachmentIds || []);
// Mention
this._mentionManager = new MentionManager(this);
@@ -118,6 +118,7 @@ var BasicComposer = Widget.extend({
});
// Attachments
this._renderAttachments();
$(window).on(this.fileuploadID, this._onAttachmentLoaded.bind(this));
this.on('change:attachment_ids', this, this._renderAttachments);
+5 -1
View File
@@ -8,7 +8,7 @@ class MailMessage(models.Model):
_inherit = 'mail.message'
rating_ids = fields.One2many('rating.rating', 'message_id', string='Related ratings')
rating_value = fields.Float("Rating Value", compute='_compute_rating_value', store=False)
rating_value = fields.Float("Rating Value", compute='_compute_rating_value', store=False, search='_search_rating_value')
@api.multi
@api.depends('rating_ids', 'rating_ids.rating')
@@ -17,3 +17,7 @@ class MailMessage(models.Model):
mapping = dict((r.message_id.id, r.rating) for r in ratings)
for message in self:
message.rating_value = mapping.get(message.id, 0.0)
@api.multi
def _search_rating_value(self, operator, operand):
return [('rating_ids.rating', operator, operand)]
+3 -2
View File
@@ -19,7 +19,7 @@ class AccountAnalyticLine(models.Model):
@api.model
def create(self, values):
result = super(AccountAnalyticLine, self).create(values)
if 'so_line' not in values and not result.so_line and result.product_id and result.product_id.expense_policy != 'no' and result.amount <= 0: # allow to force a False value for so_line
if 'so_line' not in values and not result.so_line and result.product_id and result.product_id.expense_policy not in [False, 'no'] and result.amount <= 0: # allow to force a False value for so_line
result.sudo()._sale_determine_order_line()
return result
@@ -28,7 +28,8 @@ class AccountAnalyticLine(models.Model):
result = super(AccountAnalyticLine, self).write(values)
if 'so_line' not in values: # allow to force a False value for so_line
# only take the AAL from expense or vendor bill, meaning having a negative amount
self.sudo().filtered(lambda aal: not aal.so_line and aal.product_id and aal.product_id.expense_policy != 'no' and aal.amount <= 0)._sale_determine_order_line()
self.sudo().filtered(lambda aal: not aal.so_line and aal.product_id and aal.product_id.expense_policy not in [False, 'no'] and aal.amount <= 0)._sale_determine_order_line()
return result
# ----------------------------------------------------------
# Vendor Bill / Expense : determine the Sale Order to reinvoice
+22 -13
View File
@@ -47,8 +47,8 @@ class SaleOrder(models.Model):
amount_untaxed += line.price_subtotal
amount_tax += line.price_tax
order.update({
'amount_untaxed': order.pricelist_id.currency_id.round(amount_untaxed),
'amount_tax': order.pricelist_id.currency_id.round(amount_tax),
'amount_untaxed': amount_untaxed,
'amount_tax': amount_tax,
'amount_total': amount_untaxed + amount_tax,
})
@@ -66,22 +66,31 @@ class SaleOrder(models.Model):
for possible refunds created directly from existing invoices. This is necessary since such a
refund is not directly linked to the SO.
"""
# Ignore the status of the deposit product
deposit_product_id = self.env['sale.advance.payment.inv']._default_product_id()
line_invoice_status_all = [(d['order_id'][0], d['invoice_status']) for d in self.env['sale.order.line'].read_group([('order_id', 'in', self.ids), ('product_id', '!=', deposit_product_id.id)], ['order_id', 'invoice_status'], ['order_id', 'invoice_status'], lazy=False)]
for order in self:
invoice_ids = order.order_line.mapped('invoice_lines').mapped('invoice_id').filtered(lambda r: r.type in ['out_invoice', 'out_refund'])
# Search for invoices which have been 'cancelled' (filter_refund = 'modify' in
# 'account.invoice.refund')
# use like as origin may contains multiple references (e.g. 'SO01, SO02')
refunds = invoice_ids.search([('origin', 'like', order.name), ('company_id', '=', order.company_id.id)]).filtered(lambda r: r.type in ['out_invoice', 'out_refund'])
refunds = invoice_ids.search([('origin', 'like', order.name), ('company_id', '=', order.company_id.id), ('type', 'in', ('out_invoice', 'out_refund'))])
invoice_ids |= refunds.filtered(lambda r: order.name in [origin.strip() for origin in r.origin.split(',')])
# Search for refunds as well
refund_ids = self.env['account.invoice'].browse()
if invoice_ids:
for inv in invoice_ids:
refund_ids += refund_ids.search([('type', '=', 'out_refund'), ('origin', '=', inv.number), ('origin', '!=', False), ('journal_id', '=', inv.journal_id.id)])
# Ignore the status of the deposit product
deposit_product_id = self.env['sale.advance.payment.inv']._default_product_id()
line_invoice_status = [line.invoice_status for line in order.order_line if line.product_id != deposit_product_id]
# Search for refunds as well
domain_inv = expression.OR([
['&', ('origin', '=', inv.number), ('journal_id', '=', inv.journal_id.id)]
for inv in invoice_ids if inv.number
])
if domain_inv:
refund_ids = self.env['account.invoice'].search(expression.AND([
['&', ('type', '=', 'out_refund'), ('origin', '!=', False)],
domain_inv
]))
else:
refund_ids = self.env['account.invoice'].browse()
line_invoice_status = [d[1] for d in line_invoice_status_all if d[0] == order.id]
if order.state not in ('sale', 'done'):
invoice_status = 'no'
@@ -662,7 +671,7 @@ class SaleOrder(models.Model):
# create an analytic account if at least an expense product
for order in self:
if any([expense_policy != 'no' for expense_policy in order.order_line.mapped('product_id.expense_policy')]):
if any([expense_policy not in [False, 'no'] for expense_policy in order.order_line.mapped('product_id.expense_policy')]):
if not order.analytic_account_id:
order._create_analytic_account()
@@ -1026,7 +1035,7 @@ class SaleOrderLine(models.Model):
msg = _("Extra line with %s ") % (line.product_id.display_name,)
line.order_id.message_post(body=msg)
# create an analytic account if at least an expense product
if line.product_id.expense_policy != 'no' and not self.order_id.analytic_account_id:
if line.product_id.expense_policy not in [False, 'no'] and not self.order_id.analytic_account_id:
self.order_id._create_analytic_account()
return line
+5
View File
@@ -314,6 +314,7 @@
<field name="qty_delivered_manual" invisible="1"/>
<field name="qty_delivered_method" invisible="1"/>
<field name="price_total" invisible="1"/>
<field name="price_tax" invisible="1"/>
<field name="price_subtotal" invisible="1"/>
<label for="product_uom_qty" string="Ordered Quantity"/>
<div>
@@ -322,6 +323,7 @@
name="product_uom_qty" class="oe_inline"/>
<field
name="product_uom"
force_save="1"
groups="uom.group_uom"
class="oe_inline oe_no_button"
attrs="{
@@ -442,6 +444,7 @@
<field name="qty_to_invoice" invisible="1"/>
<field
name="product_uom"
force_save="1"
attrs="{
'readonly': [('state', 'in', ('sale','done', 'cancel'))],
'required': [('display_type', '=', False)],
@@ -473,6 +476,7 @@
<field name="invoice_status" invisible="1"/>
<field name="customer_lead" invisible="1"/>
<field name="currency_id" invisible="1"/>
<field name="price_tax" invisible="1"/>
</tree>
<kanban class="o_kanban_mobile">
<field name="name"/>
@@ -480,6 +484,7 @@
<field name="product_uom_qty"/>
<field name="product_uom" groups="uom.group_uom"/>
<field name="price_subtotal"/>
<field name="price_tax" invisible="1"/>
<field name="price_total" invisible="1"/>
<field name="price_unit"/>
<field name="display_type"/>
+1 -1
View File
@@ -10,7 +10,7 @@ class AccountAnalyticLine(models.Model):
@api.multi
def _sale_determine_order(self):
mapping = super(AccountAnalyticLine, self)._sale_determine_order()
for analytic_line in self.sudo().filtered(lambda aal: not aal.so_line and aal.product_id and aal.product_id.expense_policy != 'no'):
for analytic_line in self.sudo().filtered(lambda aal: not aal.so_line and aal.product_id and aal.product_id.expense_policy not in [False, 'no']):
if analytic_line.move_id.expense_id.sale_order_id:
mapping[analytic_line.id] = analytic_line.move_id.expense_id.sale_order_id
return mapping
+1 -2
View File
@@ -387,7 +387,7 @@ class StockMove(models.Model):
product_tot_qty_available = move.product_id.qty_available + tmpl_dict[move.product_id.id]
rounding = move.product_id.uom_id.rounding
qty_done = 0.0
qty_done = move.product_uom._compute_quantity(move.quantity_done, move.product_id.uom_id)
if float_is_zero(product_tot_qty_available, precision_rounding=rounding):
new_std_price = move._get_price_unit()
elif float_is_zero(product_tot_qty_available + move.product_qty, precision_rounding=rounding) or \
@@ -396,7 +396,6 @@ class StockMove(models.Model):
else:
# Get the standard price
amount_unit = std_price_update.get((move.company_id.id, move.product_id.id)) or move.product_id.standard_price
qty_done = move.product_uom._compute_quantity(move.quantity_done, move.product_id.uom_id)
qty = forced_qty or qty_done
new_std_price = ((amount_unit * product_tot_qty_available) + (move._get_price_unit() * qty)) / (product_tot_qty_available + qty_done)
@@ -2587,6 +2587,40 @@ class TestStockValuation(TransactionCase):
self.assertAlmostEqual(self.product1.qty_at_date, 0.0)
self.assertAlmostEqual(self.product1.stock_value, 0.0)
def test_average_perpetual_6(self):
self.product1.product_tmpl_id.cost_method = 'average'
move1 = self.env['stock.move'].create({
'name': 'Receive 1 unit at 10',
'location_id': self.supplier_location.id,
'location_dest_id': self.stock_location.id,
'product_id': self.product1.id,
'product_uom': self.uom_unit.id,
'product_uom_qty': 1.0,
'price_unit': 10,
})
move1._action_confirm()
move1._action_assign()
move1.move_line_ids.qty_done = 1.0
move2 = self.env['stock.move'].create({
'name': 'Receive 1 units at 5',
'location_id': self.supplier_location.id,
'location_dest_id': self.stock_location.id,
'product_id': self.product1.id,
'product_uom': self.uom_unit.id,
'product_uom_qty': 1.0,
'price_unit': 5,
})
move2._action_confirm()
move2._action_assign()
move2.move_line_ids.qty_done = 1.0
# Receive both at the same time
(move1 | move2)._action_done()
self.assertAlmostEqual(self.product1.standard_price, 7.5)
def test_average_negative_1(self):
""" Test edit in the past. Receive 10, send 20, edit the second move to only send 10.
"""
@@ -307,6 +307,9 @@ class AdjustmentLines(models.Model):
return False
accounts = self.product_id.product_tmpl_id.get_product_accounts()
debit_account_id = accounts.get('stock_valuation') and accounts['stock_valuation'].id or False
# If the stock move is dropshipped move we need to get the cost account instead the stock valuation account
if self.move_id._is_dropshipped():
debit_account_id = accounts.get('expense') and accounts['expense'].id or False
already_out_account_id = accounts['stock_output'].id
credit_account_id = self.cost_line_id.account_id.id or cost_product.property_account_expense_id.id or cost_product.categ_id.property_account_expense_categ_id.id
@@ -240,6 +240,12 @@ var FieldMany2One = AbstractField.extend({
*/
_bindAutoComplete: function () {
var self = this;
// avoid ignoring autocomplete="off" by obfuscating placeholder, see #30439
if (this.$input.attr('placeholder')) {
this.$input.attr('placeholder', function (index, val) {
return val.split('').join('\ufeff');
});
}
this.$input.autocomplete({
source: function (req, resp) {
_.each(self._autocompleteSources, function (source) {
@@ -228,17 +228,16 @@ var FormController = BasicController.extend({
// are displayed with an alert
var fields = self.renderer.state.fields;
var data = self.renderer.state.data;
var alertFields = [];
var alertFields = {};
for (var k = 0; k < changedFields.length; k++) {
var field = fields[changedFields[k]];
var fieldData = data[changedFields[k]];
if (field.translate && fieldData) {
alertFields.push(field);
alertFields[changedFields[k]] = field;
}
}
if (alertFields.length) {
self.renderer.alertFields = alertFields;
self.renderer.displayTranslationAlert();
if (!_.isEmpty(alertFields)) {
self.renderer.updateAlertFields(alertFields);
}
}
return changedFields;
@@ -13,6 +13,7 @@ var FormRenderer = BasicRenderer.extend({
className: "o_form_view",
events: _.extend({}, BasicRenderer.prototype.events, {
'click .o_notification_box .oe_field_translate': '_onTranslate',
'click .o_notification_box .close': '_onTranslateNotificationClose',
'click .oe_title, .o_inner_group': '_onClick',
}),
custom_events: _.extend({}, BasicRenderer.prototype.custom_events, {
@@ -30,6 +31,7 @@ var FormRenderer = BasicRenderer.extend({
this._super.apply(this, arguments);
this.idsForLabels = {};
this.lastActivatedFieldIndex = -1;
this.alertFields = {};
},
/**
* @override
@@ -103,23 +105,32 @@ var FormRenderer = BasicRenderer.extend({
});
return fieldNames;
},
/*
* Updates translation alert fields for the current state and display updated fields
*
* @param {Object} alertFields
*/
updateAlertFields: function (alertFields) {
this.alertFields[this.state.res_id] = _.extend(this.alertFields[this.state.res_id] || {}, alertFields);
this.displayTranslationAlert();
},
/**
* Show a warning message if the user modified a translated field. For each
* field, the notification provides a link to edit the field's translations.
*
* @param {Object[]} alertFields field list
*/
displayTranslationAlert: function () {
this.$('.o_notification_box').remove();
var $notification = $(qweb.render('notification-box', {type: 'info'}))
.append(qweb.render('translation-alert', {
fields: this.alertFields,
lang: _t.database.parameters.name
}));
if (this.$('.o_form_statusbar').length) {
this.$('.o_form_statusbar').after($notification);
} else {
this.$el.prepend($notification);
if (this.alertFields[this.state.res_id]) {
var $notification = $(qweb.render('notification-box', {type: 'info'}))
.append(qweb.render('translation-alert', {
fields: this.alertFields[this.state.res_id],
lang: _t.database.parameters.name
}));
if (this.$('.o_form_statusbar').length) {
this.$('.o_form_statusbar').after($notification);
} else {
this.$el.prepend($notification);
}
}
},
/**
@@ -905,6 +916,9 @@ var FormRenderer = BasicRenderer.extend({
return $.when.apply($, defs).then(function () {
self._updateView($form.contents());
if (self.state.res_id in self.alertFields) {
self.displayTranslationAlert();
}
}, function () {
$form.remove();
}).then(function(){
@@ -1015,6 +1029,15 @@ var FormRenderer = BasicRenderer.extend({
event.preventDefault();
this.trigger_up('translate', {fieldName: event.target.name, id: this.state.id});
},
/**
* remove alert fields of record from alertFields object
*
* @private
* @param {MouseEvent} ev
*/
_onTranslateNotificationClose: function(ev) {
delete this.alertFields[this.state.res_id];
},
});
return FormRenderer;
+1 -1
View File
@@ -1525,7 +1525,7 @@
<div role="alertdialog">
Please update translations of :
<t t-foreach="fields" t-as="field">
<strong><a class="oe_field_translate" t-att-name="field.name" href="#"><t t-esc="field.string"/><t t-if="!field_last">, </t></a></strong>
<strong><a class="oe_field_translate" t-att-name="field" href="#"><t t-esc="field_value.string"/><t t-if="field_index &lt; _.size(fields)-1">, </t></a></strong>
</t>
</div>
</t>
+145 -2
View File
@@ -16,6 +16,7 @@ var Widget = require('web.Widget');
var _t = core._t;
var createView = testUtils.createView;
var createActionManager = testUtils.createActionManager;
var createAsyncView = testUtils.createAsyncView;
var createActionManager = testUtils.createActionManager;
@@ -5647,9 +5648,10 @@ QUnit.module('Views', {
});
QUnit.test('display translation alert', function (assert) {
assert.expect(1);
assert.expect(2);
this.data.partner.fields.foo.translate = true;
this.data.partner.fields.display_name.translate = true;
var multi_lang = _t.database.multi_lang;
_t.database.multi_lang = true;
@@ -5662,6 +5664,7 @@ QUnit.module('Views', {
'<sheet>' +
'<group>' +
'<field name="foo"/>' +
'<field name="display_name"/>' +
'</group>' +
'</sheet>' +
'</form>',
@@ -5671,14 +5674,154 @@ QUnit.module('Views', {
form.$buttons.find('.o_form_button_edit').click();
form.$('input[name="foo"]').val("test").trigger("input");
form.$buttons.find('.o_form_button_save').click();
assert.strictEqual(form.$('.o_form_view > .alert > div .oe_field_translate').length, 1,
"should have single translation alert");
assert.strictEqual(form.$('.o_form_view > .alert > div').length, 1,"should have a translation alert");
form.$buttons.find('.o_form_button_edit').click();
form.$('input[name="display_name"]').val("test2").trigger("input");
form.$buttons.find('.o_form_button_save').click();
assert.strictEqual(form.$('.o_form_view > .alert > div .oe_field_translate').length, 2,
"should have two translate fields in translation alert");
form.destroy();
_t.database.multi_lang = multi_lang;
});
QUnit.test('translation alerts are preserved on pager change', function (assert) {
assert.expect(5);
this.data.partner.fields.foo.translate = true;
var multi_lang = _t.database.multi_lang;
_t.database.multi_lang = true;
var form = createView({
View: FormView,
model: 'partner',
data: this.data,
arch: '<form string="Partners">' +
'<sheet>' +
'<field name="foo"/>' +
'</sheet>' +
'</form>',
viewOptions: {
ids: [1, 2],
index: 0,
},
res_id: 1,
});
form.$buttons.find('.o_form_button_edit').click();
form.$('input[name="foo"]').val("test").trigger("input");
form.$buttons.find('.o_form_button_save').click();
assert.strictEqual(form.$('.o_form_view > .alert > div').length, 1,
"should have a translation alert");
// click on the pager to switch to the next record
form.pager.$('.o_pager_next').click();
assert.strictEqual(form.$('.o_form_view > .alert > div').length, 0,
"should not have a translation alert");
// click on the pager to switch back to the previous record
form.pager.$('.o_pager_previous').click();
assert.strictEqual(form.$('.o_form_view > .alert > div').length, 1,
"should have a translation alert");
// remove translation alert by click X and check alert even after form reload
form.$('.o_form_view > .alert > .close').click();
assert.strictEqual(form.$('.o_form_view > .alert > div').length, 0,
"should not have a translation alert");
form.reload();
assert.strictEqual(form.$('.o_form_view > .alert > div').length, 0,
"should not have a translation alert after reload");
form.destroy();
_t.database.multi_lang = multi_lang;
});
QUnit.test('translation alerts preseved on reverse breadcrumb', function (assert) {
assert.expect(2);
this.data['ir.translation'] = {
fields: {
name: { string: "name", type: "char" },
source: {string: "Source", type: "char"},
value: {string: "Value", type: "char"},
},
records: [],
};
this.data.partner.fields.foo.translate = true;
var multi_lang = _t.database.multi_lang;
_t.database.multi_lang = true;
var archs = {
'partner,false,form': '<form string="Partners">' +
'<sheet>' +
'<field name="foo"/>' +
'</sheet>' +
'</form>',
'partner,false,search': '<search></search>',
'ir.translation,false,list': '<tree>' +
'<field name="name"/>' +
'<field name="source"/>' +
'<field name="value"/>' +
'</tree>',
'ir.translation,false,search': '<search></search>',
};
var actions = [{
id: 1,
name: 'Partner',
res_model: 'partner',
type: 'ir.actions.act_window',
views: [[false, 'form']],
}, {
id: 2,
name: 'Translate',
res_model: 'ir.translation',
type: 'ir.actions.act_window',
views: [[false, 'list']],
target: 'current',
flags: {'search_view': true, 'action_buttons': true},
}];
var actionManager = createActionManager({
actions: actions,
archs: archs,
data: this.data,
});
actionManager.doAction(1);
actionManager.controlPanel.$el.find('.o_form_button_edit').click();
actionManager.$('input[name="foo"]').val("test").trigger("input");
actionManager.controlPanel.$el.find('.o_form_button_save').click();
assert.strictEqual(actionManager.$('.o_form_view > .alert > div').length, 1,
"should have a translation alert");
var currentController = actionManager.getCurrentController().widget;
actionManager.doAction(2, {
on_reverse_breadcrumb: function () {
if (!_.isEmpty(currentController.renderer.alertFields)) {
currentController.renderer.displayTranslationAlert();
}
return false;
},
});
$('.o_control_panel .breadcrumb a:first').click();
assert.strictEqual(actionManager.$('.o_form_view > .alert > div').length, 1,
"should have a translation alert");
actionManager.destroy();
_t.database.multi_lang = multi_lang;
});
QUnit.test('translate event correctly handled with multiple controllers', function (assert) {
assert.expect(3);
+1 -1
View File
@@ -424,7 +424,7 @@ class Web_Editor(http.Controller):
(note: searching for the URL string is not enough as it could appear in a comment or an xpath expression.)
"""
return bool(etree.XML(view.arch).xpath("link[@href='{}']".format(url)))
return bool(etree.XML(view.arch).xpath("//link[@href='{}']".format(url)))
view_to_xpath = IrUiView.get_related_views(bundle_xmlid, bundles=True).filtered(views_linking_url)
+3
View File
@@ -9,9 +9,12 @@ declaration.
Signed,
Roberto Fichera roberto.fichera@levelprime.com https://github.com/robyf70
Filippo Iovine - filippo.iovine@levelprime.com - https://github.com/FilippoIOVINE
List of contributors:
Roberto Fichera roberto.fichera@levelprime.com https://github.com/robyf70
Roberto Fichera robyf@tekno-soft.it https://github.com/robyf70
Filippo Iovine - filippo.iovine@levelprime.com - https://github.com/FilippoIOVINE
Filippo Iovine - filippo.jovine@gmail.com - https://github.com/FilippoIOVINE
+11
View File
@@ -0,0 +1,11 @@
china, 2019-01-16
I hereby agree to the terms of the Odoo Individual Contributor License
Agreement v1.0.
I declare that I am authorized and able to make this agreement and sign this
declaration.
Signed,
liweijie 674416404@qq.com https://github.com/liweijie0812
+11
View File
@@ -0,0 +1,11 @@
Yemen, 2019-01-22
I hereby agree to the terms of the Odoo Individual Contributor License
Agreement v1.0.
I declare that I am authorized and able to make this agreement and sign this
declaration.
Signed,
Waleed Mohsen Mohsen.Waleed@gmail.com https://github.com/MohsenWaleed
+11
View File
@@ -0,0 +1,11 @@
Mexico, 18-01-2018
I hereby agree to the terms of the Odoo Individual Contributor License
Agreement v1.0.
I declare that I am authorized and able to make this agreement and sign this
declaration.
Signed,
José Francisco Martínez Salgado admin@jofranmtz.com https://github.com/xyvs
-2
View File
@@ -180,8 +180,6 @@ todo_include_todos = False
intersphinx_mapping = {
'python': ('https://docs.python.org/3/', None),
'werkzeug': ('http://werkzeug.pocoo.org/docs/', None),
'sqlalchemy': ('http://docs.sqlalchemy.org/en/rel_0_9/', None),
'django': ('https://django.readthedocs.org/en/latest/', None),
}
github_user = 'odoo'
+5
View File
@@ -403,6 +403,11 @@ class IrModelFields(models.Model):
raise UserError(_("The Selection Options expression is not a valid Pythonic expression. "
"Please provide an expression in the [('key','Label'), ...] format."))
@api.constrains('domain')
def _check_domain(self):
for field in self:
safe_eval(field.domain or '[]')
@api.constrains('name', 'state')
def _check_name(self):
for field in self:
+4 -1
View File
@@ -394,7 +394,10 @@ actual arch.
def _compute_defaults(self, values):
if 'inherit_id' in values:
values.setdefault('mode', 'extension' if values['inherit_id'] else 'primary')
# Do not automatically change the mode if the view already has an inherit_id,
# and the user change it to another.
if not values['inherit_id'] or all(not view.inherit_id for view in self):
values.setdefault('mode', 'extension' if values['inherit_id'] else 'primary')
return values
@api.model_create_multi
+28
View File
@@ -1573,6 +1573,12 @@ class ViewModeField(ViewCase):
})
self.assertEqual(view2.mode, 'extension')
view2.write({'inherit_id': None})
self.assertEqual(view2.mode, 'primary')
view2.write({'inherit_id': view.id})
self.assertEqual(view2.mode, 'extension')
@mute_logger('odoo.sql_db')
def testModeExplicit(self):
view = self.View.create({
@@ -1585,6 +1591,7 @@ class ViewModeField(ViewCase):
'arch': '<qweb/>'
})
self.assertEqual(view.mode, 'primary')
self.assertEqual(view2.mode, 'primary')
with self.assertRaises(IntegrityError):
self.View.create({
@@ -1636,6 +1643,27 @@ class ViewModeField(ViewCase):
view.write({'mode': 'primary'})
def testChangeInheritOfPrimary(self):
"""
A primary view with an inherit_id must remain primary when changing the inherit_id
"""
base1 = self.View.create({
'inherit_id': None,
'arch': '<qweb/>',
})
base2 = self.View.create({
'inherit_id': None,
'arch': '<qweb/>',
})
view = self.View.create({
'mode': 'primary',
'inherit_id': base1.id,
'arch': '<qweb/>',
})
self.assertEqual(view.mode, 'primary')
view.write({'inherit_id': base2.id})
self.assertEqual(view.mode, 'primary')
class TestDefaultView(ViewCase):
def testDefaultViewBase(self):