[MERGE] forward port branch saas-11.3 up to 21eb31d804

This commit is contained in:
Christophe Simonis
2018-10-31 20:43:03 +01:00
26 changed files with 235 additions and 76 deletions
+1 -1
View File
@@ -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
+1 -1
View File
@@ -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:
+1 -26
View File
@@ -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)
+5 -1
View File
@@ -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
+1 -1
View File
@@ -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"/>
+11 -13
View File
@@ -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);
+5
View File
@@ -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']
+66 -22
View File
@@ -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,
Binary file not shown.

After

Width:  |  Height:  |  Size: 77 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

After

Width:  |  Height:  |  Size: 48 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 68 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

+10 -5
View File
@@ -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::
+1 -1
View File
@@ -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
+1 -1
View File
@@ -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