[MERGE] forward port branch saas-16 up to a2ea4ba095

This commit is contained in:
Christophe Simonis
2017-12-12 18:39:43 +01:00
25 changed files with 396 additions and 78 deletions
+1 -4
View File
@@ -248,7 +248,7 @@ class AccountInvoice(models.Model):
move_name = fields.Char(string='Journal Entry Name', readonly=False,
default=False, copy=False,
help="Technical field holding the number given to the invoice, automatically set when the invoice is validated then stored to set the same number again if the invoice is cancelled, set to draft and re-validated.")
reference = fields.Char(string='Vendor Reference',
reference = fields.Char(string='Vendor Reference', copy=False,
help="The partner reference of this invoice.", readonly=True, states={'draft': [('readonly', False)]})
reference_type = fields.Selection('_get_reference_type', string='Payment Reference',
required=True, readonly=True, states={'draft': [('readonly', False)]},
@@ -1582,9 +1582,6 @@ class AccountInvoiceLine(models.Model):
self.price_unit = 0.0
domain['uom_id'] = []
else:
# Use the purchase uom by default
self.uom_id = self.product_id.uom_po_id
if part.lang:
product = self.product_id.with_context(lang=part.lang)
else:
@@ -658,6 +658,98 @@ QUnit.module('account', {
clientAction.destroy();
});
QUnit.test('Reconciliation validate without proposition', function (assert) {
assert.expect(1);
// Test added to prevent this issue happening again: https://github.com/odoo/odoo/commit/3549688b21eb65e16b9c3f2b6462eb8d8b52cd47
var clientAction = new ReconciliationClientAction.StatementAction(null, this.params.options);
testUtils.addMockEnvironment(clientAction, {
data: this.params.data,
session: {
currencies: {
3: {
digits: [69, 2],
position: "before",
symbol: "$"
}
}
},
});
clientAction.appendTo($('#qunit-fixture'));
var widget = clientAction.widgets[0];
// Ensure that when we validate a line without any selection, it is the same
// as when we manually create a line with the line.balance and that only one
// line is send back to server.
testUtils.intercept(clientAction, 'call_service', function (event) {
assert.deepEqual(event.data.args[1].args,
[[5],[{partner_id: 8, counterpart_aml_dicts: [],
payment_aml_ids: [], new_aml_dicts: [{
account_id: 287,
credit: 1175,
debit: 0,
name: 'SAJ/2014/002 and SAJ/2014/003'
}]}]],
"Should call process_reconciliations with ids");
});
// click on validate button
widget.$('button.o_validate:not(:hidden)').trigger('click');
clientAction.destroy();
});
QUnit.test('Reconciliation validate with proposition', function (assert) {
assert.expect(1);
// Test added to check this functionality: https://github.com/odoo/odoo/commit/2f3b469dee6f18cbccce1cdf2a81cfe57960c533
var clientAction = new ReconciliationClientAction.StatementAction(null, this.params.options);
testUtils.addMockEnvironment(clientAction, {
data: this.params.data,
session: {
currencies: {
3: {
digits: [69, 2],
position: "before",
symbol: "$"
}
}
},
});
clientAction.appendTo($('#qunit-fixture'));
var widget = clientAction.widgets[0];
// Add a line as proposition
// open the first line
widget.$('.accounting_view thead td:first').trigger('click');
// select propositions
widget.$('.match .cell_account_code:first').trigger('click');
// Ensure that when we validate a line with propositions and that there is a remaining balance
// We also create a line which is the open balance.
testUtils.intercept(clientAction, 'call_service', function (event) {
assert.deepEqual(event.data.args[1].args,
[[5],[{partner_id: 8,
counterpart_aml_dicts: [{
counterpart_aml_id: 109,
credit: 650,
debit: 0,
name: 'INV/2017/0002'
}],
payment_aml_ids: [],
new_aml_dicts: [{
account_id: 287,
credit: 525,
debit: 0,
name: 'SAJ/2014/002 and SAJ/2014/003 : Open balance'
}]}]],
"Should call process_reconciliations with ids");
});
// click on validate button
widget.$('button.o_validate:not(:hidden)').trigger('click');
clientAction.destroy();
});
QUnit.test('Reconciliation partial', function (assert) {
assert.expect(10);
+4 -3
View File
@@ -20,13 +20,14 @@ class TestReconciliation(AccountingTestCase):
self.res_currency_model = self.registry('res.currency')
self.res_currency_rate_model = self.registry('res.currency.rate')
self.partner_agrolait_id = self.env.ref("base.res_partner_2").id
partner_agrolait = self.env.ref("base.res_partner_2")
self.partner_agrolait_id = partner_agrolait.id
self.currency_swiss_id = self.env.ref("base.CHF").id
self.currency_usd_id = self.env.ref("base.USD").id
self.currency_euro_id = self.env.ref("base.EUR").id
self.env.ref('base.main_company').write({'currency_id': self.currency_euro_id})
self.account_rcv = self.env['account.account'].search([('user_type_id', '=', self.env.ref('account.data_account_type_receivable').id)], limit=1)
self.account_rsa = self.env['account.account'].search([('user_type_id', '=', self.env.ref('account.data_account_type_payable').id)], limit=1)
self.account_rcv = partner_agrolait.property_account_receivable_id or self.env['account.account'].search([('user_type_id', '=', self.env.ref('account.data_account_type_receivable').id)], limit=1)
self.account_rsa = partner_agrolait.property_account_payable_id or self.env['account.account'].search([('user_type_id', '=', self.env.ref('account.data_account_type_payable').id)], limit=1)
self.product = self.env.ref("product.product_product_4")
self.bank_journal_euro = self.env['account.journal'].create({'name': 'Bank', 'type': 'bank', 'code': 'BNK67'})
+4 -1
View File
@@ -204,8 +204,11 @@ class Team(models.Model):
def _get_graph(self):
graph_datas = super(Team, self)._get_graph()
if self.dashboard_graph_model == 'crm.opportunity.report' and self.dashboard_graph_group_pipeline == 'stage':
stage_data = self.env['crm.stage'].browse([d['label'] for d in graph_datas[0]['values']]).read(['sequence', 'name'])
stage_ids = [d['label'] for d in graph_datas[0]['values'] if d['label'] is not None]
stage_data = self.env['crm.stage'].browse(stage_ids).read(['sequence', 'name'])
stage_data = {d['id']: {'name': d['name'], 'sequence': d['sequence']} for d in stage_data}
# use "Undefined" stage for unset stage records
stage_data[None] = {'name': _('Undefined'), 'sequence': -1}
graph_datas[0]['values'] = sorted(graph_datas[0]['values'], key=lambda el: stage_data[el['label']]['sequence'])
for gdata in graph_datas[0]['values']:
gdata['label'] = stage_data[gdata['label']]['name']
+2 -1
View File
@@ -16,7 +16,8 @@ class HrAttendance(models.Model):
return self.env['hr.employee'].search([('user_id', '=', self.env.uid)], limit=1)
employee_id = fields.Many2one('hr.employee', string="Employee", default=_default_employee, required=True, ondelete='cascade', index=True)
department_id = fields.Many2one('hr.department', string="Department", related="employee_id.department_id")
department_id = fields.Many2one('hr.department', string="Department", related="employee_id.department_id",
readonly=True)
check_in = fields.Datetime(string="Check In", default=fields.Datetime.now, required=True)
check_out = fields.Datetime(string="Check Out")
worked_hours = fields.Float(string='Worked Hours', compute='_compute_worked_hours', store=True, readonly=True)
+3 -3
View File
@@ -39,16 +39,16 @@ class HrContract(models.Model):
@api.multi
def get_attribute(self, code, attribute):
return getattr(self.env['hr.contract.advantage.template'].search([('code', '=', code)], limit=1), attribute)
return self.env['hr.contract.advantage.template'].search([('code', '=', code)], limit=1)[attribute]
@api.multi
def set_attribute_value(self, code, active):
for contract in self:
if active:
value = self.env['hr.contract.advantage.template'].search([('code', '=', code)], limit=1).default_value
setattr(contract, code, value)
contract[code] = value
else:
setattr(contract, code, 0.0)
contract[code] = 0.0
class HrContractAdvandageTemplate(models.Model):
+1
View File
@@ -16,6 +16,7 @@ class WebsitePayment(http.Controller):
values = {
'pms': payment_tokens,
'acquirers': acquirers,
'error_message': [kwargs['error']] if kwargs.get('error') else False,
'return_url': return_url,
'bootstrap_formatting': True,
'partner_id': partner.id
+25 -3
View File
@@ -21,8 +21,18 @@ class StripeController(http.Controller):
def stripe_s2s_create(self, **post):
acquirer_id = int(post.get('acquirer_id'))
acquirer = request.env['payment.acquirer'].browse(acquirer_id)
acquirer.s2s_process(post)
return werkzeug.utils.redirect(post.get('return_url', '/'))
error = None
try:
acquirer.s2s_process(post)
except Exception as e:
error = e.message
return_url = post.get('return_url', '/')
if error:
separator = '?' if werkzeug.urls.url_parse(return_url).query == '' else '&'
return_url += '{}{}'.format(separator, werkzeug.urls.url_encode({'error': error}))
return werkzeug.utils.redirect(return_url)
@http.route(['/payment/stripe/s2s/create_json_3ds'], type='json', auth='public', csrf=False)
def stripe_s2s_create_json_3ds(self, verify_validity=False, **kwargs):
@@ -63,7 +73,19 @@ class StripeController(http.Controller):
tx = TX.sudo().browse(int(tx_id))
if not tx:
raise werkzeug.exceptions.NotFound()
response = tx._create_stripe_charge(tokenid=post['tokenid'], email=post['email'])
stripe_token = post['token']
response = None
if tx.type == 'form_save' and tx.partner_id:
payment_token_id = request.env['payment.token'].sudo().create({
'acquirer_id': tx.acquirer_id.id,
'partner_id': tx.partner_id.id,
'stripe_token': stripe_token
})
tx.payment_token_id = payment_token_id
response = tx._create_stripe_charge(acquirer_ref=payment_token_id.acquirer_ref, email=stripe_token['email'])
else:
response = tx._create_stripe_charge(tokenid=stripe_token['id'], email=stripe_token['email'])
_logger.info('Stripe: entering form_feedback with post data %s', pprint.pformat(response))
if response:
request.env['payment.transaction'].sudo().with_context(lang=None).form_feedback(response, 'stripe')
+54 -23
View File
@@ -2,6 +2,7 @@
import logging
import requests
import pprint
from odoo import api, fields, models, _
from odoo.addons.payment.models.payment_acquirer import ValidationError
@@ -227,11 +228,12 @@ class PaymentTokenStripe(models.Model):
@api.model
def stripe_create(self, values):
res = {}
token = values.get('stripe_token')
description = None
payment_acquirer = self.env['payment.acquirer'].browse(values.get('acquirer_id'))
url_token = 'https://%s/tokens' % payment_acquirer._get_stripe_api_url()
url_customer = 'https://%s/customers' % payment_acquirer._get_stripe_api_url()
# when asking to create a token on Stripe servers
if values.get('cc_number'):
url_token = 'https://%s/tokens' % payment_acquirer._get_stripe_api_url()
payment_params = {
'card[number]': values['cc_number'].replace(' ', ''),
'card[exp_month]': str(values['cc_expiry'][:2]),
@@ -244,27 +246,56 @@ class PaymentTokenStripe(models.Model):
params=payment_params,
headers=STRIPE_HEADERS)
token = r.json()
if token.get('id'):
customer_params = {
'source': token['id'],
'description': values['cc_holder_name']
}
if values.get('partner_id'):
partner = self.env['res.partner'].browse(values['partner_id'])
customer_params['email'] = partner.email and partner.email.strip()
r = requests.post(url_customer,
auth=(payment_acquirer.stripe_secret_key, ''),
params=customer_params,
headers=STRIPE_HEADERS)
customer = r.json()
res = {
'acquirer_ref': customer['id'],
'name': 'XXXXXXXXXXXX%s - %s' % (values['cc_number'][-4:], values['cc_holder_name'])
}
elif token.get('error'):
raise UserError(token['error']['message'])
description = values['cc_holder_name']
else:
partner_id = self.env['res.partner'].browse(values['partner_id'])
description = 'Partner: %s (id: %s)' % (partner_id.name, partner_id.id)
if not token:
raise Exception('stripe_create: No token provided!')
res = self._stripe_create_customer(token, description, payment_acquirer.id)
# pop credit card info to info sent to create
for field_name in ["cc_number", "cvc", "cc_holder_name", "cc_expiry", "cc_brand"]:
for field_name in ["cc_number", "cvc", "cc_holder_name", "cc_expiry", "cc_brand", "stripe_token"]:
values.pop(field_name, None)
return res
def _stripe_create_customer(self, token, description=None, acquirer_id=None):
if token['object'] != 'token':
_logger.error('payment.token.stripe_create_customer: Cannot create a customer for object type "%s"', token.get('object'))
raise Exception('We are unable to process your credit card information.')
if token['type'] != 'card':
_logger.error('payment.token.stripe_create_customer: Cannot create a customer for token type "%s"', token.get('type'))
raise Exception('We are unable to process your credit card information.')
if token.get('error'):
_logger.error('payment.token.stripe_create_customer: Token error:\n%s', pprint.pformat(token['error']))
raise Exception(token['error']['message'])
payment_acquirer = self.env['payment.acquirer'].browse(acquirer_id or self.acquirer_id.id)
url_customer = 'https://%s/customers' % payment_acquirer._get_stripe_api_url()
customer_params = {
'source': token['id'],
'description': description or token["card"]["name"]
}
r = requests.post(url_customer,
auth=(payment_acquirer.stripe_secret_key, ''),
params=customer_params,
headers=STRIPE_HEADERS)
customer = r.json()
if customer.get('error'):
_logger.error('payment.token.stripe_create_customer: Customer error:\n%s', pprint.pformat(customer['error']))
raise Exception(customer['error']['message'])
values = {
'acquirer_ref': customer['id'],
'name': 'XXXXXXXXXXXX%s - %s' % (token['card']['last4'], customer_params["description"])
}
return values
@@ -38,8 +38,9 @@ odoo.define('payment_stripe.stripe', function(require) {
});
}
ajax.jsonRpc("/payment/stripe/create_charge", 'call', {
tokenid: token.id,
email: token.email,
tokenid: token.id, // TBE TODO: for backward compatibility, remove on master
email: token.email, // TBE TODO: for backward compatibility, remove on master
token: token,
amount: $("input[name='amount']").val(),
acquirer_id: $("#acquirer_stripe").val(),
currency: $("input[name='currency']").val(),
+1
View File
@@ -1030,6 +1030,7 @@ class ReportSaleDetails(models.AbstractModel):
payments = []
return {
'currency_precision': user_currency.decimal_places,
'total_paid': user_currency.round(total),
'payments': payments,
'company_name': self.env.user.company_id.name,
@@ -5,6 +5,7 @@ var core = require('web.core');
var mixins = require('web.mixins');
var rpc = require('web.rpc');
var Session = require('web.Session');
var PosBaseWidget = require('point_of_sale.BaseWidget');
var QWeb = core.qweb;
var _t = core._t;
@@ -447,6 +448,7 @@ var ProxyDevice = core.Class.extend(mixins.PropertiesMixin,{
})
.then(function(result){
var env = {
widget: new PosBaseWidget(self),
company: self.pos.company,
pos: self.pos,
products: result.products,
@@ -1022,6 +1022,9 @@ var ProductScreenWidget = ScreenWidget.extend({
this.product_categories_widget.reset_category();
this.numpad.state.reset();
}
if (this.pos.config.iface_vkeyboard && this.chrome.widget.keyboard) {
this.chrome.widget.keyboard.connect($(this.el.querySelector('.searchbox input')));
}
},
close: function(){
@@ -1375,6 +1378,7 @@ var ClientListScreenWidget = ScreenWidget.extend({
// to maintain consistent scroll.
display_client_details: function(visibility,partner,clickpos){
var self = this;
var searchbox = this.$('.searchbox input');
var contents = this.$('.client-details-contents');
var parent = this.$('.client-list').parent();
var scroll = parent.scrollTop();
@@ -1411,6 +1415,19 @@ var ClientListScreenWidget = ScreenWidget.extend({
this.details_visible = true;
this.toggle_save_button();
} else if (visibility === 'edit') {
// Connect the keyboard to the edited field
if (this.pos.config.iface_vkeyboard && this.chrome.widget.keyboard) {
contents.off('click', '.detail');
searchbox.off('click');
contents.on('click', '.detail', function(ev){
self.chrome.widget.keyboard.connect(ev.target);
self.chrome.widget.keyboard.show();
});
searchbox.on('click', function() {
self.chrome.widget.keyboard.connect($(this));
});
}
this.editing_client = true;
contents.empty();
contents.append($(QWeb.render('ClientDetailsEdit',{widget:this,partner:partner})));
@@ -1453,6 +1470,9 @@ var ClientListScreenWidget = ScreenWidget.extend({
},
close: function(){
this._super();
if (this.pos.config.iface_vkeyboard && this.chrome.widget.keyboard) {
this.chrome.widget.keyboard.hide();
}
},
});
gui.define_screen({name:'clientlist', widget: ClientListScreenWidget});
+3 -3
View File
@@ -941,7 +941,7 @@
<line t-foreach='payments' t-as='payment'>
<left><t t-esc='payment.name' /></left>
<right>
<t t-esc='payment.total'/>
<t t-esc='widget.format_currency_no_symbol(payment.total)'/>
</right>
</line>
@@ -955,7 +955,7 @@
<line t-foreach='taxes' t-as='taxe'>
<left><t t-esc='taxe.name' /></left>
<right>
<t t-esc='taxe.total'/>
<t t-esc='widget.format_currency_no_symbol(taxe.total)'/>
</right>
</line>
@@ -966,7 +966,7 @@
<line>
<left>Total:</left>
<right>
<t t-esc='total_paid'/>
<t t-esc='widget.format_currency_no_symbol(total_paid)'/>
</right>
</line>
@@ -50,7 +50,7 @@
<tbody>
<tr t-foreach='payments' t-as='payment'>
<td><t t-esc="payment['name']" /></td>
<td><t t-esc="payment['total']" /></td>
<td><t t-esc="payment['total']" t-options="{'widget': 'float', 'precision': currency_precision}"/></td>
</tr>
</tbody>
</table>
@@ -67,8 +67,8 @@
<tbody>
<tr t-foreach='taxes' t-as='tax'>
<td><t t-esc="tax['name']" /></td>
<td><t t-esc="tax['tax_amount']" /></td>
<td><t t-esc="tax['base_amount']" /></td>
<td><t t-esc="tax['tax_amount']" t-options="{'widget': 'float', 'precision': currency_precision}"/></td>
<td><t t-esc="tax['base_amount']" t-options="{'widget': 'float', 'precision': currency_precision}"/></td>
</tr>
</tbody>
</table>
@@ -76,7 +76,7 @@
<br/>
<br/>
<strong>Total: <t t-esc='total_paid'/></strong>
<strong>Total: <t t-esc='total_paid' t-options="{'widget': 'float', 'precision': currency_precision}"/></strong>
</div>
</t>
+2
View File
@@ -200,6 +200,8 @@ class CustomerPortal(Controller):
# vat validation
partner = request.env["res.partner"]
if data.get("vat") and hasattr(partner, "check_vat"):
if data.get("country_id"):
data["vat"] = request.env["res.partner"].fix_eu_vat_number(int(data.get("country_id")), data.get("vat"))
partner_dummy = partner.new({
'vat': data['vat'],
'country_id': (int(data['country_id'])
+6 -4
View File
@@ -41,10 +41,12 @@ class ProductProduct(models.Model):
# No attribute_value_ids means the bom line is not variant specific
price += sbom.product_id.uom_id._compute_price(sbom.product_id.standard_price, sbom.product_uom_id) * sbom_data['qty']
if bom.routing_id:
total_cost = 0.0
for order in bom.routing_id.operation_ids:
total_cost += (order.time_cycle/60) * order.workcenter_id.costs_hour
price += bom.product_uom_id._compute_price(total_cost, bom.product_id.uom_id)
# FIXME master: remove me
if hasattr(self.env['mrp.workcenter'], 'costs_hour'):
total_cost = 0.0
for order in bom.routing_id.operation_ids:
total_cost += (order.time_cycle/60) * order.workcenter_id.costs_hour
price += bom.product_uom_id._compute_price(total_cost, bom.product_id.uom_id)
# Convert on product UoM quantities
if price > 0:
price = bom.product_uom_id._compute_price(price / bom.product_qty, self.uom_id)
@@ -63,6 +63,13 @@ class AccountInvoice(models.Model):
data['account_id'] = account.id
return data
def _onchange_product_id(self):
domain = super(AccountInvoice, self)._onchange_product_id()
if self.purchase_id:
# Use the purchase uom by default
self.uom_id = self.product_id.uom_po_id
return domain
# Load all unsold PO lines
@api.onchange('purchase_id')
def purchase_order_change(self):
+5 -7
View File
@@ -71,15 +71,13 @@ class Location(models.Model):
_sql_constraints = [('barcode_company_uniq', 'unique (barcode,company_id)', 'The barcode for a location must be unique per company !')]
@api.one
@api.depends('name', 'location_id.name')
@api.depends('name', 'location_id.complete_name')
def _compute_complete_name(self):
""" Forms complete name of location from parent location to child location. """
name = self.name
current = self
while current.location_id:
current = current.location_id
name = '%s/%s' % (current.name, name)
self.complete_name = name
if self.location_id.complete_name:
self.complete_name = '%s/%s' % (self.location_id.complete_name, self.name)
else:
self.complete_name = self.name
def write(self, values):
if 'usage' in values and values['usage'] == 'view':
+6 -3
View File
@@ -265,14 +265,17 @@ class StockMove(models.Model):
detect errors. """
raise UserError(_('The requested operation cannot be processed because of a programming error setting the `product_qty` field instead of the `product_uom_qty`.'))
@api.one
@api.multi
@api.depends('move_line_ids.product_qty')
def _compute_reserved_availability(self):
""" Fill the `availability` field on a stock move, which is the actual reserved quantity
and is represented by the aggregated `product_qty` on the linked move lines. If the move
is force assigned, the value will be 0.
"""
self.reserved_availability = self.product_id.uom_id._compute_quantity(sum(self.move_line_ids.mapped('product_qty')), self.product_uom, rounding_method='HALF-UP')
result = {data['move_id'][0]: data['product_qty'] for data in
self.env['stock.move.line'].read_group([('move_id', 'in', self.ids)], ['move_id','product_qty'], ['move_id'])}
for rec in self:
rec.reserved_availability = rec.product_id.uom_id._compute_quantity(result.get(rec.id, 0.0), rec.product_uom, rounding_method='HALF-UP')
@api.one
@api.depends('state', 'product_id', 'product_qty', 'location_id')
@@ -402,7 +405,7 @@ class StockMove(models.Model):
if propagated_date_field:
current_date = datetime.strptime(move.date_expected, DEFAULT_SERVER_DATETIME_FORMAT)
new_date = datetime.strptime(vals.get(propagated_date_field), DEFAULT_SERVER_DATETIME_FORMAT)
delta = new_date - current_date
delta = relativedelta.relativedelta(new_date, current_date)
if abs(delta.days) >= move.company_id.propagation_minimum_delta:
old_move_date = datetime.strptime(move.move_dest_ids[0].date_expected, DEFAULT_SERVER_DATETIME_FORMAT)
new_move_date = (old_move_date + relativedelta.relativedelta(days=delta.days or 0)).strftime(DEFAULT_SERVER_DATETIME_FORMAT)
@@ -95,6 +95,7 @@ var FieldMany2One = AbstractField.extend({
template: 'FieldMany2One',
custom_events: _.extend({}, AbstractField.prototype.custom_events, {
'closed_unset': '_onDialogClosedUnset',
'field_changed': '_onFieldChanged',
'quick_create': '_onQuickCreate',
'search_create_popup': '_onSearchCreatePopup',
}),
@@ -124,6 +125,12 @@ var FieldMany2One = AbstractField.extend({
// 'recordParams' is a dict of params used when calling functions
// 'getDomain' and 'getContext' on this.record
this.recordParams = {fieldName: this.name, viewType: this.viewType};
// We need to know if the widget is dirty (i.e. if the user has changed
// the value, and those changes haven't been acknowledged yet by the
// environment), to prevent erasing that new value on a reset (e.g.
// coming by an onchange on another field)
this.isDirty = false;
this.lastChangeEvent = undefined;
},
start: function () {
// booleean indicating that the content of the input isn't synchronized
@@ -151,9 +158,31 @@ var FieldMany2One = AbstractField.extend({
* TODO
*/
reinitialize: function (value) {
this.isDirty = false;
this.floating = false;
this._setValue(value);
},
/**
* Re-renders the widget if it isn't dirty. The widget is dirty if the user
* changed the value, and that change hasn't been acknowledged yet by the
* environment. For example, another field with an onchange has been updated
* and this field is updated before the onchange returns. Two '_setValue'
* are done (this is sequential), the first one returns and this widget is
* reset. However, it has pending changes, so we don't re-render.
*
* @override
*/
reset: function (record, event) {
this._reset(record, event);
if (!event || event === this.lastChangeEvent) {
this.isDirty = false;
}
if (this.isDirty) {
return $.when();
} else {
return this._render();
}
},
//--------------------------------------------------------------------------
// Private
@@ -237,6 +266,16 @@ var FieldMany2One = AbstractField.extend({
_getDisplayName: function (value) {
return value.split('\n')[0];
},
/**
* Listens to events 'field_changed' to keep track of the last event that
* has been trigerred. This allows to detect that all changes have been
* acknowledged by the environment.
*
* @param {OdooEvent} event 'field_changed' event
*/
_onFieldChanged: function (event) {
this.lastChangeEvent = event;
},
/**
* @private
* @param {string} name
@@ -476,6 +515,7 @@ var FieldMany2One = AbstractField.extend({
* @private
*/
_onDialogClosedUnset: function () {
this.isDirty = false;
this.floating = false;
this._render();
},
@@ -544,6 +584,7 @@ var FieldMany2One = AbstractField.extend({
// confirmation that the many2one is not properly set.
return;
}
this.isDirty = true;
if (this.$input.val() === "") {
this.reinitialize(false);
} else if (this._getDisplayName(this.m2o_value) !== this.$input.val()) {
@@ -1495,7 +1495,8 @@ var BasicModel = AbstractModel.extend({
def = this._rpc({
model: list.model,
method: 'read',
args: [_.pluck(data, 'id'), fieldNames]
args: [_.pluck(data, 'id'), fieldNames],
context: record.context,
}).then(function (records) {
_.each(records, function (record) {
list_records[record.id].data = record;
@@ -1743,22 +1744,21 @@ var BasicModel = AbstractModel.extend({
/**
* Fetch all name_gets for the many2ones in a group
*
* @param {Object} group a valid resource object
* @param {Object[]} groups a list of object with context and record sub keys
* @returns {Deferred}
*/
_fetchMany2OneGroup: function (group) {
var ids = _.uniq(_.pluck(group, 'res_id'));
_fetchMany2OneGroup: function (groups) {
var ids = _.uniq(_.pluck(_.pluck(groups, 'record'), 'res_id'));
return this._rpc({
model: group[0].model,
model: groups[0].record.model,
method: 'name_get',
args: [ids],
context: group[0].context
context: groups[0].context
})
.then(function (name_gets) {
_.each(group, function (record) {
var nameGet = _.find(name_gets, function (n) { return n[0] === record.res_id;});
record.data.display_name = nameGet[1];
_.each(groups, function (obj) {
var nameGet = _.find(name_gets, function (n) { return n[0] === obj.record.res_id;});
obj.record.data.display_name = nameGet[1];
});
});
},
@@ -2019,14 +2019,17 @@ var BasicModel = AbstractModel.extend({
if (!relatedRecord) {
return;
}
toBeFetched.push(relatedRecord);
toBeFetched.push({
context: record.getContext({fieldName: name, viewType: record.viewType}),
record: relatedRecord
});
}
});
// group them by model and context. Using the context as key is
// necessary to make sure the correct context is used for the rpc;
var groups = _.groupBy(toBeFetched, function (rec) {
return [rec.model, JSON.stringify(rec.context)].join();
var groups = _.groupBy(toBeFetched, function (elem) {
return [elem.record.model, JSON.stringify(elem.context)].join();
});
return $.when.apply($, _.map(groups, this._fetchMany2OneGroup.bind(this)));
@@ -2416,7 +2419,7 @@ var BasicModel = AbstractModel.extend({
model: field.relation,
method: 'read',
args: [ids, fieldNames],
context: {}, // FIXME
context: list.getContext() || {},
});
} else {
def = $.when(_.map(ids, function (id) {
@@ -258,6 +258,31 @@ QUnit.module('relational_fields', {
form.destroy();
});
QUnit.test('context in many2one and default get', function (assert) {
assert.expect(1);
this.data.partner.fields.int_field.default = 14;
this.data.partner.fields.trululu.default = 2;
var form = createView({
View: FormView,
model: 'partner',
data: this.data,
arch: '<form string="Partners">' +
'<field name="int_field"/>' +
'<field name="trululu" context="{\'blip\':int_field}"/>' +
'</form>',
mockRPC: function (route, args) {
if (args.method === 'name_get') {
assert.strictEqual(args.kwargs.context.blip, 14,
'context should have been properly sent to the nameget rpc');
}
return this._super(route, args);
},
});
form.destroy();
});
QUnit.test('editing a many2one (with form view opened with external button)', function (assert) {
assert.expect(1);
var form = createView({
@@ -6713,6 +6738,63 @@ QUnit.module('relational_fields', {
form.destroy();
});
QUnit.test('one2many field: change value before pending onchange returns', function (assert) {
var done = assert.async();
assert.expect(2);
var M2O_DELAY = relationalFields.FieldMany2One.prototype.AUTOCOMPLETE_DELAY;
relationalFields.FieldMany2One.prototype.AUTOCOMPLETE_DELAY = 0;
this.data.partner.onchanges = {
int_field: function () {}
};
var def;
var form = createView({
View: FormView,
model: 'partner',
data: this.data,
arch:'<form string="Partners">' +
'<field name="p">' +
'<tree editable="bottom">' +
'<field name="int_field"/>' +
'<field name="trululu"/>' +
'</tree>' +
'</field>' +
'</form>',
mockRPC: function (route, args) {
var result = this._super.apply(this, arguments);
if (args.method === 'onchange') {
// delay the onchange RPC
return $.when(def).then(_.constant(result));
}
return result;
},
});
form.$('.o_field_x2many_list_row_add a').click();
def = $.Deferred();
form.$('.o_field_widget[name=int_field]')
.val('44')
.trigger('input');
var $dropdown = form.$('.o_field_many2one input').autocomplete('widget');
// set trululu before onchange
form.$('.o_field_many2one input').val('first').trigger('keydown').trigger('keyup');
// complete the onchange
def.resolve();
assert.strictEqual(form.$('.o_field_many2one input').val(), 'first',
'should have kept the new value');
concurrency.delay(0).then(function () {
// check name_search result
assert.strictEqual($dropdown.find('li:not(.o_m2o_dropdown_option)').length, 1,
'autocomplete should contains 1 suggestion');
relationalFields.FieldMany2One.prototype.AUTOCOMPLETE_DELAY = M2O_DELAY;
form.destroy();
done();
});
});
QUnit.test('focus is correctly reset after an onchange in an x2many', function (assert) {
assert.expect(2);
@@ -8622,7 +8704,7 @@ QUnit.module('relational_fields', {
});
QUnit.test('fieldmany2many tags in editable list', function (assert) {
assert.expect(4);
assert.expect(7);
this.data.partner.records[0].timmy = [12];
@@ -8630,10 +8712,18 @@ QUnit.module('relational_fields', {
View: ListView,
model: 'partner',
data: this.data,
context: {take: 'five'},
arch:'<tree editable="bottom">' +
'<field name="foo"/>' +
'<field name="timmy" widget="many2many_tags"/>' +
'</tree>',
mockRPC: function (route, args) {
if (args.method === 'read' && args.model === 'partner_type') {
assert.deepEqual(args.kwargs.context, {take: 'five'},
'The context should be passed to the RPC');
}
return this._super.apply(this, arguments);
}
});
assert.strictEqual(list.$('.o_data_row:first .o_field_many2manytags .badge').length, 1,
@@ -654,7 +654,7 @@ var VideoDialog = Widget.extend({
var $media = $(this.media);
if ($media.hasClass('media_iframe_video')) {
var src = $media.data('src') || '';
var src = $media.data('oe-expression') || $media.data('src') || '';
this.$('textarea#o_video_text').val(src);
this.$('input#o_video_autoplay').prop('checked', src.indexOf('autoplay=1') >= 0);
+1 -1
View File
@@ -974,7 +974,7 @@ class Field(MetaField('DummyField', (object,), {})):
if env.in_onchange:
for invf in record._field_inverses[self]:
invf._update(record[self.name], record)
record._set_dirty(self.name)
env.dirty[record].add(self.name)
# determine more dependent fields, and invalidate them
if self.relational: