[MERGE] forward port branch saas-11.3 up to 21eb31d804
@@ -107,7 +107,7 @@ class AccountInvoice(models.Model):
|
|||||||
def _compute_residual(self):
|
def _compute_residual(self):
|
||||||
residual = 0.0
|
residual = 0.0
|
||||||
residual_company_signed = 0.0
|
residual_company_signed = 0.0
|
||||||
sign = self.type in ['in_invoice', 'out_refund'] and -1 or 1
|
sign = self.type in ['in_refund', 'out_refund'] and -1 or 1
|
||||||
for line in self.sudo().move_id.line_ids:
|
for line in self.sudo().move_id.line_ids:
|
||||||
if line.account_id == self.account_id:
|
if line.account_id == self.account_id:
|
||||||
residual_company_signed += line.amount_residual
|
residual_company_signed += line.amount_residual
|
||||||
|
|||||||
@@ -198,7 +198,7 @@ class account_abstract_payment(models.AbstractModel):
|
|||||||
total = 0.0
|
total = 0.0
|
||||||
groups = groupby(invoices, lambda i: i.currency_id)
|
groups = groupby(invoices, lambda i: i.currency_id)
|
||||||
for payment_currency, payment_invoices in groups:
|
for payment_currency, payment_invoices in groups:
|
||||||
amount_total = sum(i.residual_signed for i in payment_invoices)
|
amount_total = sum([MAP_INVOICE_TYPE_PAYMENT_SIGN[i.type] * i.residual_signed for i in payment_invoices])
|
||||||
if payment_currency == currency:
|
if payment_currency == currency:
|
||||||
total += amount_total
|
total += amount_total
|
||||||
else:
|
else:
|
||||||
|
|||||||
@@ -464,31 +464,6 @@ class TestPayment(AccountingTestCase):
|
|||||||
#check the invoice status
|
#check the invoice status
|
||||||
self.assertEqual(invoice.state, 'paid')
|
self.assertEqual(invoice.state, 'paid')
|
||||||
|
|
||||||
def test_payment_and_writeoff_out_refund(self):
|
|
||||||
# Use case:
|
|
||||||
# Company is in EUR, create a credit note for 100 EUR and register payment of 90.
|
|
||||||
# Mark invoice as fully paid with a write_off
|
|
||||||
# Check that all the aml are correctly created.
|
|
||||||
invoice = self.create_invoice(amount=100, type='out_refund', currency_id=self.currency_eur_id, partner=self.partner_agrolait.id)
|
|
||||||
# register payment on invoice
|
|
||||||
payment = self.payment_model.create({'payment_type': 'outbound',
|
|
||||||
'payment_method_id': self.env.ref('account.account_payment_method_manual_in').id,
|
|
||||||
'partner_type': 'customer',
|
|
||||||
'partner_id': self.partner_agrolait.id,
|
|
||||||
'amount': 90,
|
|
||||||
'payment_date': time.strftime('%Y') + '-07-15',
|
|
||||||
'payment_difference_handling': 'reconcile',
|
|
||||||
'writeoff_account_id': self.account_payable.id,
|
|
||||||
'journal_id': self.bank_journal_euro.id,
|
|
||||||
'invoice_ids': [(4, invoice.id, None)]
|
|
||||||
})
|
|
||||||
payment.post()
|
|
||||||
self.check_journal_items(payment.move_line_ids, [
|
|
||||||
{'account_id': self.account_eur.id, 'debit': 0.0, 'credit': 90.0, 'amount_currency': 0.0, 'currency_id': False},
|
|
||||||
{'account_id': self.account_payable.id, 'debit': 0.0, 'credit': 10.0, 'amount_currency': 0.0, 'currency_id': False},
|
|
||||||
{'account_id': self.account_receivable.id, 'debit': 100.0, 'credit': 0.0, 'amount_currency': 0.0, 'currency_id': False},
|
|
||||||
])
|
|
||||||
self.assertEqual(invoice.state, 'paid')
|
|
||||||
|
|
||||||
def test_payment_and_writeoff_in_other_currency_3(self):
|
def test_payment_and_writeoff_in_other_currency_3(self):
|
||||||
# Use case related in revision 20935462a0cabeb45480ce70114ff2f4e91eaf79
|
# Use case related in revision 20935462a0cabeb45480ce70114ff2f4e91eaf79
|
||||||
@@ -537,4 +512,4 @@ class TestPayment(AccountingTestCase):
|
|||||||
# Check the invoice status and the full reconciliation: the difference on the receivable account
|
# Check the invoice status and the full reconciliation: the difference on the receivable account
|
||||||
# should have been completed by an exchange rate difference entry
|
# should have been completed by an exchange rate difference entry
|
||||||
self.assertEqual(invoice.state, 'paid')
|
self.assertEqual(invoice.state, 'paid')
|
||||||
self.assertTrue(invoice.move_id.line_ids.filtered(lambda l: l.account_id == self.account_receivable)[0].full_reconcile_id)
|
self.assertTrue(invoice.move_id.line_ids.filtered(lambda l: l.account_id == self.account_receivable)[0].full_reconcile_id)
|
||||||
@@ -10,8 +10,12 @@ from odoo.tools.misc import mod10r
|
|||||||
def _is_l10n_ch_postal(account_ref):
|
def _is_l10n_ch_postal(account_ref):
|
||||||
""" Returns True iff the string account_ref is a valid postal account number,
|
""" Returns True iff the string account_ref is a valid postal account number,
|
||||||
i.e. it only contains ciphers and is last cipher is the result of a recursive
|
i.e. it only contains ciphers and is last cipher is the result of a recursive
|
||||||
modulo 10 operation ran over the rest of it.
|
modulo 10 operation ran over the rest of it. Shorten form with - is also accepted.
|
||||||
"""
|
"""
|
||||||
|
if re.match('^[0-9]{2}-[0-9]{1,6}-[0-9]$', account_ref or ''):
|
||||||
|
ref_subparts = account_ref.split('-')
|
||||||
|
account_ref = ref_subparts[0] + ref_subparts[1].rjust(6,'0') + ref_subparts[2]
|
||||||
|
|
||||||
if re.match('\d+$', account_ref or ''):
|
if re.match('\d+$', account_ref or ''):
|
||||||
account_ref_without_check = account_ref[:-1]
|
account_ref_without_check = account_ref[:-1]
|
||||||
return mod10r(account_ref_without_check) == account_ref
|
return mod10r(account_ref_without_check) == account_ref
|
||||||
|
|||||||
@@ -1807,7 +1807,7 @@
|
|||||||
</record>
|
</record>
|
||||||
|
|
||||||
<record id="pcg_4661" model="account.account.template">
|
<record id="pcg_4661" model="account.account.template">
|
||||||
<field name="name">>Datorii din operaţiuni de fiducie</field>
|
<field name="name">Datorii din operaţiuni de fiducie</field>
|
||||||
<field name="code">4661</field>
|
<field name="code">4661</field>
|
||||||
<field name="user_type_id" ref="account.data_account_type_current_liabilities"/>
|
<field name="user_type_id" ref="account.data_account_type_current_liabilities"/>
|
||||||
<field name="chart_template_id" ref="ro_chart_template"/>
|
<field name="chart_template_id" ref="ro_chart_template"/>
|
||||||
|
|||||||
@@ -81,8 +81,7 @@
|
|||||||
<field name="reconcile" eval="False"/>
|
<field name="reconcile" eval="False"/>
|
||||||
<field name="user_type_id" ref="account.data_account_type_current_assets"/>
|
<field name="user_type_id" ref="account.data_account_type_current_assets"/>
|
||||||
<field name="chart_template_id" ref="vn_template"/>
|
<field name="chart_template_id" ref="vn_template"/>
|
||||||
<field name="name">Thuế GTGT được khấu trừ của hàng hóa, dịch vụ
|
<field name="name">Thuế GTGT được khấu trừ của hàng hóa, dịch vụ</field>
|
||||||
</field>
|
|
||||||
</record>
|
</record>
|
||||||
<record id="chart1332" model="account.account.template">
|
<record id="chart1332" model="account.account.template">
|
||||||
<field name="code">1332</field>
|
<field name="code">1332</field>
|
||||||
@@ -140,7 +139,7 @@
|
|||||||
</record>
|
</record>
|
||||||
<record id="chart1388" model="account.account.template">
|
<record id="chart1388" model="account.account.template">
|
||||||
<field name="code">1388</field>
|
<field name="code">1388</field>
|
||||||
<field name="reconcile" eval="False"/>
|
<field name="reconcile" eval="True"/>
|
||||||
<field name="user_type_id" ref="account.data_account_type_current_assets"/>
|
<field name="user_type_id" ref="account.data_account_type_current_assets"/>
|
||||||
<field name="chart_template_id" ref="vn_template"/>
|
<field name="chart_template_id" ref="vn_template"/>
|
||||||
<field name="name">Phải thu khác</field>
|
<field name="name">Phải thu khác</field>
|
||||||
@@ -149,7 +148,7 @@
|
|||||||
|
|
||||||
<record id="chart141" model="account.account.template">
|
<record id="chart141" model="account.account.template">
|
||||||
<field name="code">141</field>
|
<field name="code">141</field>
|
||||||
<field name="reconcile" eval="False"/>
|
<field name="reconcile" eval="True"/>
|
||||||
<field name="user_type_id" ref="account.data_account_type_current_assets"/>
|
<field name="user_type_id" ref="account.data_account_type_current_assets"/>
|
||||||
<field name="chart_template_id" ref="vn_template"/>
|
<field name="chart_template_id" ref="vn_template"/>
|
||||||
<field name="name">Tạm ứng</field>
|
<field name="name">Tạm ứng</field>
|
||||||
@@ -322,8 +321,7 @@
|
|||||||
<field name="reconcile" eval="False"/>
|
<field name="reconcile" eval="False"/>
|
||||||
<field name="user_type_id" ref="account.data_account_type_non_current_assets"/>
|
<field name="user_type_id" ref="account.data_account_type_non_current_assets"/>
|
||||||
<field name="chart_template_id" ref="vn_template"/>
|
<field name="chart_template_id" ref="vn_template"/>
|
||||||
<field name="name">Cây lâu năm, súc vật làm việc và cho sản phẩm
|
<field name="name">Cây lâu năm, súc vật làm việc và cho sản phẩm</field>
|
||||||
</field>
|
|
||||||
</record>
|
</record>
|
||||||
<record id="chart2118" model="account.account.template">
|
<record id="chart2118" model="account.account.template">
|
||||||
<field name="code">2118</field>
|
<field name="code">2118</field>
|
||||||
@@ -449,7 +447,7 @@
|
|||||||
<field name="reconcile" eval="False"/>
|
<field name="reconcile" eval="False"/>
|
||||||
<field name="user_type_id" ref="account.data_account_type_non_current_assets"/>
|
<field name="user_type_id" ref="account.data_account_type_non_current_assets"/>
|
||||||
<field name="chart_template_id" ref="vn_template"/>
|
<field name="chart_template_id" ref="vn_template"/>
|
||||||
<field name="name">>Đầu tư vào công ty liên doanh</field>
|
<field name="name">Đầu tư vào công ty liên doanh</field>
|
||||||
</record>
|
</record>
|
||||||
|
|
||||||
|
|
||||||
@@ -459,7 +457,7 @@
|
|||||||
<field name="reconcile" eval="False"/>
|
<field name="reconcile" eval="False"/>
|
||||||
<field name="user_type_id" ref="account.data_account_type_non_current_assets"/>
|
<field name="user_type_id" ref="account.data_account_type_non_current_assets"/>
|
||||||
<field name="chart_template_id" ref="vn_template"/>
|
<field name="chart_template_id" ref="vn_template"/>
|
||||||
<field name="name">>Đầu tư góp vốn vào đơn vị khác</field>
|
<field name="name">Đầu tư góp vốn vào đơn vị khác</field>
|
||||||
</record>
|
</record>
|
||||||
|
|
||||||
<record id="chart2288" model="account.account.template">
|
<record id="chart2288" model="account.account.template">
|
||||||
@@ -651,14 +649,14 @@
|
|||||||
|
|
||||||
<record id="chart3341" model="account.account.template">
|
<record id="chart3341" model="account.account.template">
|
||||||
<field name="code">3341</field>
|
<field name="code">3341</field>
|
||||||
<field name="reconcile" eval="False"/>
|
<field name="reconcile" eval="True"/>
|
||||||
<field name="user_type_id" ref="account.data_account_type_current_liabilities"/>
|
<field name="user_type_id" ref="account.data_account_type_current_liabilities"/>
|
||||||
<field name="chart_template_id" ref="vn_template"/>
|
<field name="chart_template_id" ref="vn_template"/>
|
||||||
<field name="name">Phải trả công nhân viên</field>
|
<field name="name">Phải trả công nhân viên</field>
|
||||||
</record>
|
</record>
|
||||||
<record id="chart3348" model="account.account.template">
|
<record id="chart3348" model="account.account.template">
|
||||||
<field name="code">3348</field>
|
<field name="code">3348</field>
|
||||||
<field name="reconcile" eval="False"/>
|
<field name="reconcile" eval="True"/>
|
||||||
<field name="user_type_id" ref="account.data_account_type_current_liabilities"/>
|
<field name="user_type_id" ref="account.data_account_type_current_liabilities"/>
|
||||||
<field name="chart_template_id" ref="vn_template"/>
|
<field name="chart_template_id" ref="vn_template"/>
|
||||||
<field name="name">Phải trả người lao động khác</field>
|
<field name="name">Phải trả người lao động khác</field>
|
||||||
@@ -677,7 +675,7 @@
|
|||||||
<field name="reconcile" eval="False"/>
|
<field name="reconcile" eval="False"/>
|
||||||
<field name="user_type_id" ref="account.data_account_type_current_liabilities"/>
|
<field name="user_type_id" ref="account.data_account_type_current_liabilities"/>
|
||||||
<field name="chart_template_id" ref="vn_template"/>
|
<field name="chart_template_id" ref="vn_template"/>
|
||||||
<field name="name">Phải trả nội bộ về vốn kinh doanh </field>
|
<field name="name">Phải trả nội bộ về vốn kinh doanh</field>
|
||||||
</record>
|
</record>
|
||||||
|
|
||||||
<record id="chart3362" model="account.account.template">
|
<record id="chart3362" model="account.account.template">
|
||||||
@@ -763,7 +761,7 @@
|
|||||||
</record>
|
</record>
|
||||||
<record id="chart3388" model="account.account.template">
|
<record id="chart3388" model="account.account.template">
|
||||||
<field name="code">3388</field>
|
<field name="code">3388</field>
|
||||||
<field name="reconcile" eval="False"/>
|
<field name="reconcile" eval="True"/>
|
||||||
<field name="user_type_id" ref="account.data_account_type_current_liabilities"/>
|
<field name="user_type_id" ref="account.data_account_type_current_liabilities"/>
|
||||||
<field name="chart_template_id" ref="vn_template"/>
|
<field name="chart_template_id" ref="vn_template"/>
|
||||||
<field name="name">Phải trả, phải nộp khác</field>
|
<field name="name">Phải trả, phải nộp khác</field>
|
||||||
@@ -790,7 +788,7 @@
|
|||||||
<field name="reconcile" eval="False"/>
|
<field name="reconcile" eval="False"/>
|
||||||
<field name="user_type_id" ref="account.data_account_type_current_liabilities"/>
|
<field name="user_type_id" ref="account.data_account_type_current_liabilities"/>
|
||||||
<field name="chart_template_id" ref="vn_template"/>
|
<field name="chart_template_id" ref="vn_template"/>
|
||||||
<field name="name">Trái phiếu </field>
|
<field name="name">Trái phiếu</field>
|
||||||
</record>
|
</record>
|
||||||
<record id="chart34311" model="account.account.template">
|
<record id="chart34311" model="account.account.template">
|
||||||
<field name="code">34311</field>
|
<field name="code">34311</field>
|
||||||
|
|||||||
@@ -516,7 +516,9 @@ var ChatManager = AbstractService.extend({
|
|||||||
if ('ids' in options) {
|
if ('ids' in options) {
|
||||||
// get messages from their ids (chatter is the main use case)
|
// get messages from their ids (chatter is the main use case)
|
||||||
return this._fetchDocumentMessages(options.ids, options).then(function (result) {
|
return this._fetchDocumentMessages(options.ids, options).then(function (result) {
|
||||||
self.markAsRead(options.ids);
|
if (options.shouldMarkAsRead) {
|
||||||
|
self.markAsRead(options.ids);
|
||||||
|
}
|
||||||
return result;
|
return result;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ var ThreadField = AbstractField.extend({
|
|||||||
init: function () {
|
init: function () {
|
||||||
this._super.apply(this, arguments);
|
this._super.apply(this, arguments);
|
||||||
this.msgIDs = this.value.res_ids;
|
this.msgIDs = this.value.res_ids;
|
||||||
|
this._markAsReadOnRender = true; // DO NOT FORWARD-PORT
|
||||||
},
|
},
|
||||||
willStart: function () {
|
willStart: function () {
|
||||||
return this.alive(this.call('chat_manager', 'isReady'));
|
return this.alive(this.call('chat_manager', 'isReady'));
|
||||||
@@ -68,6 +69,7 @@ var ThreadField = AbstractField.extend({
|
|||||||
this.msgIDs = this.value.res_ids;
|
this.msgIDs = this.value.res_ids;
|
||||||
// the mail widgets being persistent, one need to update the res_id on reset
|
// the mail widgets being persistent, one need to update the res_id on reset
|
||||||
this.res_id = record.res_id;
|
this.res_id = record.res_id;
|
||||||
|
this._markAsReadOnRender = true; // DO NOT FORWARD-PORT
|
||||||
},
|
},
|
||||||
|
|
||||||
//--------------------------------------------------------------------------
|
//--------------------------------------------------------------------------
|
||||||
@@ -123,6 +125,10 @@ var ThreadField = AbstractField.extend({
|
|||||||
var self = this;
|
var self = this;
|
||||||
options = options || {};
|
options = options || {};
|
||||||
options.ids = ids;
|
options.ids = ids;
|
||||||
|
if (this._markAsReadOnRender) {
|
||||||
|
options.shouldMarkAsRead = true;
|
||||||
|
this._markAsReadOnRender = false;
|
||||||
|
}
|
||||||
var fetch_def = this.dp.add(this.call('chat_manager', 'getMessages', options));
|
var fetch_def = this.dp.add(this.call('chat_manager', 'getMessages', options));
|
||||||
return fetch_def.then(function (raw_messages) {
|
return fetch_def.then(function (raw_messages) {
|
||||||
var isCreateMode = false;
|
var isCreateMode = false;
|
||||||
|
|||||||
@@ -3606,11 +3606,12 @@ var BasicModel = AbstractModel.extend({
|
|||||||
*/
|
*/
|
||||||
_postprocess: function (record, options) {
|
_postprocess: function (record, options) {
|
||||||
var self = this;
|
var self = this;
|
||||||
|
var viewType = options && options.viewType || record.viewType;
|
||||||
var defs = [];
|
var defs = [];
|
||||||
|
|
||||||
_.each(record.getFieldNames(options), function (name) {
|
_.each(record.getFieldNames(options), function (name) {
|
||||||
var field = record.fields[name];
|
var field = record.fields[name];
|
||||||
var fieldInfo = record.fieldsInfo[record.viewType][name] || {};
|
var fieldInfo = record.fieldsInfo[viewType][name] || {};
|
||||||
var options = fieldInfo.options || {};
|
var options = fieldInfo.options || {};
|
||||||
if (options.always_reload) {
|
if (options.always_reload) {
|
||||||
if (record.fields[name].type === 'many2one' && record.data[name]) {
|
if (record.fields[name].type === 'many2one' && record.data[name]) {
|
||||||
@@ -3619,7 +3620,7 @@ var BasicModel = AbstractModel.extend({
|
|||||||
model: field.relation,
|
model: field.relation,
|
||||||
method: 'name_get',
|
method: 'name_get',
|
||||||
args: [element.data.id],
|
args: [element.data.id],
|
||||||
context: self._getContext(record, {fieldName: name}),
|
context: self._getContext(record, {fieldName: name, viewType: viewType}),
|
||||||
})
|
})
|
||||||
.then(function (result) {
|
.then(function (result) {
|
||||||
element.data.display_name = result[0][1];
|
element.data.display_name = result[0][1];
|
||||||
|
|||||||
@@ -150,6 +150,14 @@ var BasicView = AbstractView.extend({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// Many2one: context is not the same between the different views
|
||||||
|
// this means the result of a name_get could differ
|
||||||
|
if (fieldType === 'many2one') {
|
||||||
|
if (JSON.stringify(record.data[name].context) !==
|
||||||
|
JSON.stringify(fieldInfo.context)) {
|
||||||
|
fieldNames.push(name);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -268,7 +268,7 @@ var KanbanController = BasicController.extend({
|
|||||||
_onButtonNew: function () {
|
_onButtonNew: function () {
|
||||||
var state = this.model.get(this.handle, {raw: true});
|
var state = this.model.get(this.handle, {raw: true});
|
||||||
var quickCreateEnabled = this.quickCreateEnabled && viewUtils.isQuickCreateEnabled(state);
|
var quickCreateEnabled = this.quickCreateEnabled && viewUtils.isQuickCreateEnabled(state);
|
||||||
if (this.on_create === 'quick_create' && quickCreateEnabled) {
|
if (this.on_create === 'quick_create' && quickCreateEnabled && state.data.length) {
|
||||||
// Activate the quick create in the first column
|
// Activate the quick create in the first column
|
||||||
this.renderer.addQuickCreate();
|
this.renderer.addQuickCreate();
|
||||||
} else if (this.on_create && this.on_create !== 'quick_create') {
|
} else if (this.on_create && this.on_create !== 'quick_create') {
|
||||||
|
|||||||
@@ -380,6 +380,93 @@ QUnit.module('relational_fields', {
|
|||||||
form.destroy();
|
form.destroy();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
QUnit.test('show_address works in a view embedded in a view of another type', function (assert) {
|
||||||
|
assert.expect(1);
|
||||||
|
|
||||||
|
this.data.turtle.records[1].turtle_trululu = 2;
|
||||||
|
|
||||||
|
var form = createView({
|
||||||
|
View: FormView,
|
||||||
|
model: 'partner',
|
||||||
|
data: this.data,
|
||||||
|
arch: '<form string="Partners">' +
|
||||||
|
'<field name="display_name"/>' +
|
||||||
|
'<field name="turtles"/>' +
|
||||||
|
'</form>',
|
||||||
|
res_id: 1,
|
||||||
|
archs: {
|
||||||
|
"turtle,false,form": '<form string="T">' +
|
||||||
|
'<field name="display_name"/>' +
|
||||||
|
'<field name="turtle_trululu" context="{\'show_address\': 1}" options="{\'always_reload\': True}"/>' +
|
||||||
|
'</form>',
|
||||||
|
"turtle,false,list": '<tree editable="bottom">' +
|
||||||
|
'<field name="display_name"/>' +
|
||||||
|
'</tree>',
|
||||||
|
},
|
||||||
|
mockRPC: function (route, args) {
|
||||||
|
if (args.method === 'name_get') {
|
||||||
|
return this._super(route, args).then(function (result) {
|
||||||
|
if (args.model === 'partner' && args.kwargs.context.show_address) {
|
||||||
|
result[0][1] += '\nrue morgue\nparis 75013';
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return this._super(route, args);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
// click the turtle field, opens a modal with the turtle form view
|
||||||
|
form.$('.o_data_row:first td.o_data_cell').click();
|
||||||
|
|
||||||
|
assert.strictEqual($('[name="turtle_trululu"]').text(), "second recordrue morgueparis 75013",
|
||||||
|
"The partner's address should be displayed");
|
||||||
|
form.destroy();
|
||||||
|
});
|
||||||
|
|
||||||
|
QUnit.test('many2one data is reloaded if there is a context to take into account', function (assert) {
|
||||||
|
assert.expect(1);
|
||||||
|
|
||||||
|
this.data.turtle.records[1].turtle_trululu = 2;
|
||||||
|
|
||||||
|
var form = createView({
|
||||||
|
View: FormView,
|
||||||
|
model: 'partner',
|
||||||
|
data: this.data,
|
||||||
|
arch: '<form string="Partners">' +
|
||||||
|
'<field name="display_name"/>' +
|
||||||
|
'<field name="turtles"/>' +
|
||||||
|
'</form>',
|
||||||
|
res_id: 1,
|
||||||
|
archs: {
|
||||||
|
"turtle,false,form": '<form string="T">' +
|
||||||
|
'<field name="display_name"/>' +
|
||||||
|
'<field name="turtle_trululu" context="{\'show_address\': 1}" options="{\'always_reload\': True}"/>' +
|
||||||
|
'</form>',
|
||||||
|
"turtle,false,list": '<tree editable="bottom">' +
|
||||||
|
'<field name="display_name"/>' +
|
||||||
|
'<field name="turtle_trululu"/>' +
|
||||||
|
'</tree>',
|
||||||
|
},
|
||||||
|
mockRPC: function (route, args) {
|
||||||
|
if (args.method === 'name_get') {
|
||||||
|
return this._super(route, args).then(function (result) {
|
||||||
|
if (args.model === 'partner' && args.kwargs.context.show_address) {
|
||||||
|
result[0][1] += '\nrue morgue\nparis 75013';
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return this._super(route, args);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
// click the turtle field, opens a modal with the turtle form view
|
||||||
|
form.$('.o_data_row:first').click();
|
||||||
|
|
||||||
|
assert.strictEqual($('.modal [name=turtle_trululu]').text(), "second recordrue morgueparis 75013",
|
||||||
|
"The partner's address should be displayed");
|
||||||
|
form.destroy();
|
||||||
|
});
|
||||||
|
|
||||||
QUnit.test('many2ones in form views with search more', function (assert) {
|
QUnit.test('many2ones in form views with search more', function (assert) {
|
||||||
assert.expect(3);
|
assert.expect(3);
|
||||||
this.data.partner.records.push({
|
this.data.partner.records.push({
|
||||||
|
|||||||
@@ -4210,6 +4210,30 @@ QUnit.module('Views', {
|
|||||||
kanban.destroy();
|
kanban.destroy();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
QUnit.test('quick_create on grouped kanban without column', function (assert) {
|
||||||
|
assert.expect(1);
|
||||||
|
this.data.partner.records = [];
|
||||||
|
var kanban = createView({
|
||||||
|
View: KanbanView,
|
||||||
|
model: 'partner',
|
||||||
|
data: this.data,
|
||||||
|
arch: '<kanban class="o_kanban_test" on_create="quick_create"><templates><t t-name="kanban-box">' +
|
||||||
|
'<div>' +
|
||||||
|
'<field name="name"/>' +
|
||||||
|
'</div>' +
|
||||||
|
'</t></templates></kanban>',
|
||||||
|
groupBy: ['product_id'],
|
||||||
|
|
||||||
|
intercepts: {
|
||||||
|
switch_view: function (event) {
|
||||||
|
assert.ok(true, "switch_view was called instead of quick_create");
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
kanban.$buttons.find('.o-kanban-button-new').click();
|
||||||
|
kanban.destroy();
|
||||||
|
});
|
||||||
|
|
||||||
QUnit.test('keyboard navigation on kanban basic rendering', function (assert) {
|
QUnit.test('keyboard navigation on kanban basic rendering', function (assert) {
|
||||||
assert.expect(3);
|
assert.expect(3);
|
||||||
|
|
||||||
|
|||||||
@@ -72,6 +72,11 @@ class Website(models.Model):
|
|||||||
homepage_id = fields.Many2one('website.page', string='Homepage')
|
homepage_id = fields.Many2one('website.page', string='Homepage')
|
||||||
favicon = fields.Binary(string="Website Favicon", help="This field holds the image used to display a favicon on the website.")
|
favicon = fields.Binary(string="Website Favicon", help="This field holds the image used to display a favicon on the website.")
|
||||||
|
|
||||||
|
@api.onchange('language_ids')
|
||||||
|
def _onchange_language_ids(self):
|
||||||
|
if self.language_ids and self.default_lang_id not in self.language_ids:
|
||||||
|
self.default_lang_id = self.language_ids[0]
|
||||||
|
|
||||||
@api.multi
|
@api.multi
|
||||||
def _compute_menu(self):
|
def _compute_menu(self):
|
||||||
Menu = self.env['website.menu']
|
Menu = self.env['website.menu']
|
||||||
|
|||||||
@@ -81,8 +81,11 @@ Overview
|
|||||||
|
|
||||||
The Credits
|
The Credits
|
||||||
|
|
||||||
|
.. note:: The credits went from integer to float value starting **October 2018**
|
||||||
|
Integer values are still supported.
|
||||||
|
|
||||||
Every service provided through the In-App platform can be used by the
|
Every service provided through the In-App platform can be used by the
|
||||||
clients with tokens or *credits*. The credits are an integer unit and
|
clients with tokens or *credits*. The credits are an float unit and
|
||||||
their monetary value depends on the service and is decided by the
|
their monetary value depends on the service and is decided by the
|
||||||
provider. This could be:
|
provider. This could be:
|
||||||
|
|
||||||
@@ -157,7 +160,8 @@ The first step is to register your service on the IAP endpoint (production
|
|||||||
and/or test) before you can actually query user accounts. To create a service,
|
and/or test) before you can actually query user accounts. To create a service,
|
||||||
go to your *Portal Account* on the IAP endpoint (https://iap.odoo.com for
|
go to your *Portal Account* on the IAP endpoint (https://iap.odoo.com for
|
||||||
production, https://iap-sandbox.odoo.com for testing, the endpoints are
|
production, https://iap-sandbox.odoo.com for testing, the endpoints are
|
||||||
*independent* and *not synchronized*).
|
*independent* and *not synchronized*). Alternatively, you can go to your portal
|
||||||
|
on Odoo (https://iap.odoo.com/my/home) and select *In-App Services*.
|
||||||
|
|
||||||
.. note::
|
.. note::
|
||||||
|
|
||||||
@@ -166,33 +170,45 @@ production, https://iap-sandbox.odoo.com for testing, the endpoints are
|
|||||||
on sandbox to ease the tests.
|
on sandbox to ease the tests.
|
||||||
|
|
||||||
Log in then go to :menuselection:`My Account --> Your In-App Services`, click
|
Log in then go to :menuselection:`My Account --> Your In-App Services`, click
|
||||||
Create and provide the name of your service.
|
Create and provide the informations of your service.
|
||||||
|
|
||||||
|
|
||||||
The now created service has *two* important fields:
|
The service has *four* important fields:
|
||||||
|
|
||||||
* :samp:`name` - :class:`ServiceName`: this will identify your service in the
|
* :samp:`name` - :class:`ServiceName`: this will identify your service in the
|
||||||
client's :ref:`app <iap-odoo-app>` communicates directly with IAP.
|
client's :ref:`app <iap-odoo-app>` communicates directly with IAP, choose it
|
||||||
* :samp:`key` - :class:`ServiceKey`: the developer key that identifies you in
|
carefully!
|
||||||
IAP (see :ref:`your service <iap-service>`) and allows to draw credits from
|
|
||||||
the client's account.
|
|
||||||
|
|
||||||
.. warning::
|
.. warning::
|
||||||
The :class:`ServiceName` is unique and should usually match the name of your
|
The :class:`ServiceName` is unique and should usually match the name of your
|
||||||
Odoo App.
|
Odoo Client App.
|
||||||
|
|
||||||
|
* :samp:`Icon` - :class:`Icon`: A generic icon that will serve as default for your
|
||||||
|
:ref:`packages <iap-packages>`
|
||||||
|
|
||||||
|
* :samp:`key` - :class:`ServiceKey`: the developer key that identifies you in
|
||||||
|
IAP (see :ref:`your service <iap-service>`) and allows to draw credits from
|
||||||
|
the client's account. It will be shown only once upon creation of the service
|
||||||
|
and can be regenerated at will.
|
||||||
.. danger::
|
.. danger::
|
||||||
Your :class:`ServiceKey` *is a secret*, leaking your service key
|
Your :class:`ServiceKey` *is a secret*, leaking your service key
|
||||||
allows other application developers to draw credits bought for
|
allows other application developers to draw credits bought for
|
||||||
your service(s).
|
your service(s).
|
||||||
|
|
||||||
.. image:: images/service_select.png
|
* :samp:`privacy policy` - :class:`PrivacyPolicy`: This is an url to the privacy
|
||||||
|
policy of your service. This should explicitly mention the **information you collect**,
|
||||||
|
How you **use it, its relevance** to make your service work and inform the
|
||||||
|
client on how they can **access, update or delete their personal information**.
|
||||||
|
|
||||||
|
.. image:: images/menu.png
|
||||||
:align: center
|
:align: center
|
||||||
|
|
||||||
.. image:: images/service_create.png
|
.. image:: images/service_list.png
|
||||||
:align: center
|
:align: center
|
||||||
|
|
||||||
.. image:: images/service_packs.png
|
.. image:: images/creating_service.png
|
||||||
|
:align: center
|
||||||
|
|
||||||
|
.. image:: images/service_created.png
|
||||||
:align: center
|
:align: center
|
||||||
|
|
||||||
You can then create *credit packs* which clients can purchase in order to
|
You can then create *credit packs* which clients can purchase in order to
|
||||||
@@ -203,9 +219,10 @@ use your service.
|
|||||||
Packages
|
Packages
|
||||||
--------
|
--------
|
||||||
|
|
||||||
The credit packages are essentially a product with 4 characteristics.
|
The credit packages are essentially a product with 5 characteristics.
|
||||||
|
|
||||||
* Name: the name of the package,
|
* Name: the name of the package,
|
||||||
|
* Icon: A specific icon for the package. If not provided, it will fallback on the service icon
|
||||||
* Description: details on the package that will appear on the shop page as
|
* Description: details on the package that will appear on the shop page as
|
||||||
well as the invoice,
|
well as the invoice,
|
||||||
* Credits: the amount of credits the client is entitled to when buying the package,
|
* Credits: the amount of credits the client is entitled to when buying the package,
|
||||||
@@ -362,7 +379,6 @@ parameters we can use to make things clearer to the end-user:
|
|||||||
.. patch::
|
.. patch::
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
.. TODO:: how do you test your service?
|
.. TODO:: how do you test your service?
|
||||||
|
|
||||||
JSON-RPC2_ Transaction API
|
JSON-RPC2_ Transaction API
|
||||||
@@ -394,13 +410,13 @@ Authorize
|
|||||||
|
|
||||||
:param ServiceKey key:
|
:param ServiceKey key:
|
||||||
:param UserToken account_token:
|
:param UserToken account_token:
|
||||||
:param int credit:
|
:param float credit:
|
||||||
:param str description: optional, helps users identify the reason for
|
:param str description: optional, helps users identify the reason for
|
||||||
charges on their accounts.
|
charges on their accounts.
|
||||||
:returns: :class:`TransactionToken` if the authorization succeeded.
|
:returns: :class:`TransactionToken` if the authorization succeeded.
|
||||||
:raises: :class:`~odoo.exceptions.AccessError` if the service token is invalid
|
:raises: :class:`~odoo.exceptions.AccessError` if the service token is invalid
|
||||||
:raises: :class:`~odoo.addons.iap.models.iap.InsufficientCreditError` if the account does
|
:raises: :class:`~odoo.addons.iap.models.iap.InsufficientCreditError` if the account does
|
||||||
:raises: ``TypeError`` if the ``credit`` value is not an integer
|
:raises: ``TypeError`` if the ``credit`` value is not an integer or a float
|
||||||
|
|
||||||
.. code-block:: python
|
.. code-block:: python
|
||||||
|
|
||||||
@@ -434,7 +450,7 @@ Capture
|
|||||||
|
|
||||||
:param TransactionToken token:
|
:param TransactionToken token:
|
||||||
:param ServiceKey key:
|
:param ServiceKey key:
|
||||||
:param int credit_to_capture: (new - 15 Jan 2018) optional parameter to capture a smaller amount of credits than authorized
|
:param float credit_to_capture: (new - 15 Jan 2018) optional parameter to capture a smaller amount of credits than authorized
|
||||||
:raises: :class:`~odoo.exceptions.AccessError`
|
:raises: :class:`~odoo.exceptions.AccessError`
|
||||||
|
|
||||||
.. code-block:: python
|
.. code-block:: python
|
||||||
@@ -534,6 +550,33 @@ care how they are implemented
|
|||||||
|
|
||||||
Raised by any unexpeted behaviour at the discretion of the App developer (*you*).
|
Raised by any unexpeted behaviour at the discretion of the App developer (*you*).
|
||||||
|
|
||||||
|
|
||||||
|
Test the API
|
||||||
|
------------
|
||||||
|
|
||||||
|
In order to test the developped app, we propose a sandbox platform that allows you to:
|
||||||
|
|
||||||
|
1. Test the whole flow from the client's point of view - Actual services and transactions
|
||||||
|
that can be consulted. (again this requires to change the endpoint, see the danger note
|
||||||
|
in :ref:`Service <iap-service>`)
|
||||||
|
2. Test the API.
|
||||||
|
|
||||||
|
The latter consists in specific tokens that will work on **IAP-Sandbox only**.
|
||||||
|
|
||||||
|
* token ``000000``: represents a non-existing account. Returns
|
||||||
|
an :class:`~odoo.addons.iap.models.iap.InsufficientCreditError` on authorize attempt.
|
||||||
|
* token ``000111``: Represents an account without sufficient credits to perform any service.
|
||||||
|
Returns an :class:`~odoo.addons.iap.models.iap.InsufficientCreditError` on authorize attempt.
|
||||||
|
* token ``111111``: Represents an account with enough credits to perform any service.
|
||||||
|
An authorize attempt will return a dummy transacion token that is processed by the capture
|
||||||
|
and cancel routes.
|
||||||
|
|
||||||
|
.. note::
|
||||||
|
|
||||||
|
* Those tokens are only active on the IAP-Sanbox server.
|
||||||
|
* The service key is completely ignored with this flow, If you want to run a robust test
|
||||||
|
of your service, you should ignore these tokens.
|
||||||
|
|
||||||
Odoo Helpers
|
Odoo Helpers
|
||||||
============
|
============
|
||||||
|
|
||||||
@@ -548,7 +591,7 @@ Charging
|
|||||||
.. note::
|
.. note::
|
||||||
|
|
||||||
A new functionality was introduced to capture a different amount of credits than reserved.
|
A new functionality was introduced to capture a different amount of credits than reserved.
|
||||||
As this patch was added on the 15th of January 2018, you will need to upgrade your ``iap`` module in order to use it.
|
As this patch was added on the **15th of January 2018**, you will need to upgrade your ``iap`` module in order to use it.
|
||||||
The specifics of the new functionality are highlighted in the code.
|
The specifics of the new functionality are highlighted in the code.
|
||||||
|
|
||||||
.. class:: odoo.addons.iap.models.iap.charge(env, key, account_token, credit[, description, credit_template])
|
.. class:: odoo.addons.iap.models.iap.charge(env, key, account_token, credit[, description, credit_template])
|
||||||
@@ -567,12 +610,12 @@ Charging
|
|||||||
configuration key
|
configuration key
|
||||||
:param ServiceKey key:
|
:param ServiceKey key:
|
||||||
:param UserToken token:
|
:param UserToken token:
|
||||||
:param int credit:
|
:param float credit:
|
||||||
:param str description:
|
:param str description:
|
||||||
:param Qweb template credit_template:
|
:param Qweb template credit_template:
|
||||||
|
|
||||||
.. code-block:: python
|
.. code-block:: python
|
||||||
:emphasize-lines: 10,13,14,15
|
:emphasize-lines: 11,13,14,15
|
||||||
|
|
||||||
@route('/deathstar/superlaser', type='json')
|
@route('/deathstar/superlaser', type='json')
|
||||||
def superlaser(self, user_account,
|
def superlaser(self, user_account,
|
||||||
@@ -583,7 +626,8 @@ Charging
|
|||||||
0.0 is none, 1.0 is full power
|
0.0 is none, 1.0 is full power
|
||||||
"""
|
"""
|
||||||
credits = int(MAXIMUM_POWER * factor)
|
credits = int(MAXIMUM_POWER * factor)
|
||||||
with charge(request.env, SERVICE_KEY, user_account, credits) as transaction:
|
description = "We will demonstrate the power of this station on your home planet of Alderaan."
|
||||||
|
with charge(request.env, SERVICE_KEY, user_account, credits, description) as transaction:
|
||||||
# TODO: allow other targets
|
# TODO: allow other targets
|
||||||
transaction.credit = max(credits, 2)
|
transaction.credit = max(credits, 2)
|
||||||
# Sales ongoing one the energy price,
|
# Sales ongoing one the energy price,
|
||||||
|
|||||||
|
After Width: | Height: | Size: 77 KiB |
|
After Width: | Height: | Size: 30 KiB |
|
Before Width: | Height: | Size: 16 KiB After Width: | Height: | Size: 48 KiB |
|
Before Width: | Height: | Size: 11 KiB |
|
After Width: | Height: | Size: 68 KiB |
|
After Width: | Height: | Size: 26 KiB |
|
Before Width: | Height: | Size: 18 KiB |
|
Before Width: | Height: | Size: 11 KiB |
@@ -384,8 +384,6 @@ class ResConfigSettings(models.TransientModel, ResConfigModuleInstallationMixin)
|
|||||||
def copy(self, values):
|
def copy(self, values):
|
||||||
raise UserError(_("Cannot duplicate configuration!"), "")
|
raise UserError(_("Cannot duplicate configuration!"), "")
|
||||||
|
|
||||||
# TODO: Find replacement for 'onchange' attribute in view with dynamic
|
|
||||||
# api.onchange(...) and migrate the onchange_module(...) accordingly.
|
|
||||||
@api.model
|
@api.model
|
||||||
def fields_view_get(self, view_id=None, view_type='form',
|
def fields_view_get(self, view_id=None, view_type='form',
|
||||||
toolbar=False, submenu=False):
|
toolbar=False, submenu=False):
|
||||||
@@ -407,9 +405,6 @@ class ResConfigSettings(models.TransientModel, ResConfigModuleInstallationMixin)
|
|||||||
modifiers = json.loads(node.get("modifiers"))
|
modifiers = json.loads(node.get("modifiers"))
|
||||||
modifiers['readonly'] = True
|
modifiers['readonly'] = True
|
||||||
node.set("modifiers", json.dumps(modifiers))
|
node.set("modifiers", json.dumps(modifiers))
|
||||||
if 'on_change' not in node.attrib:
|
|
||||||
node.set("on_change",
|
|
||||||
"onchange_module(%s, '%s')" % (field, field))
|
|
||||||
|
|
||||||
ret_val['arch'] = etree.tostring(doc, encoding='unicode')
|
ret_val['arch'] = etree.tostring(doc, encoding='unicode')
|
||||||
return ret_val
|
return ret_val
|
||||||
@@ -433,6 +428,16 @@ class ResConfigSettings(models.TransientModel, ResConfigModuleInstallationMixin)
|
|||||||
}
|
}
|
||||||
return {}
|
return {}
|
||||||
|
|
||||||
|
def _register_hook(self):
|
||||||
|
""" Add an onchange method for each module field. """
|
||||||
|
def make_method(name):
|
||||||
|
return lambda self: self.onchange_module(self[name], name)
|
||||||
|
|
||||||
|
for name in self._fields:
|
||||||
|
if name.startswith('module_'):
|
||||||
|
method = make_method(name)
|
||||||
|
self._onchange_methods[name].append(method)
|
||||||
|
|
||||||
@api.model
|
@api.model
|
||||||
def _get_classified_fields(self):
|
def _get_classified_fields(self):
|
||||||
""" return a dictionary with the fields classified by category::
|
""" return a dictionary with the fields classified by category::
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ PyYAML==3.12 ; python_version < '3.7'
|
|||||||
PyYAML==3.13 ; python_version >= '3.7'
|
PyYAML==3.13 ; python_version >= '3.7'
|
||||||
qrcode==5.3
|
qrcode==5.3
|
||||||
reportlab==3.3.0
|
reportlab==3.3.0
|
||||||
requests==2.11.1
|
requests==2.20.0
|
||||||
suds-jurko==0.6
|
suds-jurko==0.6
|
||||||
vatnumber==1.2
|
vatnumber==1.2
|
||||||
vobject==0.9.3
|
vobject==0.9.3
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ pyusb==1.0.0
|
|||||||
PyYAML==3.12
|
PyYAML==3.12
|
||||||
qrcode==5.3
|
qrcode==5.3
|
||||||
reportlab>=3.3.0
|
reportlab>=3.3.0
|
||||||
requests==2.11.1
|
requests==2.20.0
|
||||||
six==1.10.0
|
six==1.10.0
|
||||||
suds-jurko==0.6
|
suds-jurko==0.6
|
||||||
vatnumber==1.2
|
vatnumber==1.2
|
||||||
|
|||||||