[MERGE] forward port branch saas-16 up to a2ea4ba095
This commit is contained in:
@@ -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);
|
||||
|
||||
|
||||
@@ -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'})
|
||||
|
||||
@@ -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']
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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')
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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});
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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'])
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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':
|
||||
|
||||
@@ -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
@@ -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:
|
||||
|
||||
Reference in New Issue
Block a user