[MERGE] forward port branch 11.0 up to 950d9b2369
This commit is contained in:
@@ -350,7 +350,7 @@ class AccountInvoice(models.Model):
|
|||||||
payment_move_line_ids = fields.Many2many('account.move.line', string='Payment Move Lines', compute='_compute_payments', store=True)
|
payment_move_line_ids = fields.Many2many('account.move.line', string='Payment Move Lines', compute='_compute_payments', store=True)
|
||||||
user_id = fields.Many2one('res.users', string='Salesperson', track_visibility='onchange',
|
user_id = fields.Many2one('res.users', string='Salesperson', track_visibility='onchange',
|
||||||
readonly=True, states={'draft': [('readonly', False)]},
|
readonly=True, states={'draft': [('readonly', False)]},
|
||||||
default=lambda self: self.env.user)
|
default=lambda self: self.env.user, copy=False)
|
||||||
fiscal_position_id = fields.Many2one('account.fiscal.position', string='Fiscal Position', oldname='fiscal_position',
|
fiscal_position_id = fields.Many2one('account.fiscal.position', string='Fiscal Position', oldname='fiscal_position',
|
||||||
readonly=True, states={'draft': [('readonly', False)]})
|
readonly=True, states={'draft': [('readonly', False)]})
|
||||||
commercial_partner_id = fields.Many2one('res.partner', string='Commercial Entity', compute_sudo=True,
|
commercial_partner_id = fields.Many2one('res.partner', string='Commercial Entity', compute_sudo=True,
|
||||||
@@ -998,8 +998,6 @@ class AccountInvoice(models.Model):
|
|||||||
'invoice_id': self.id,
|
'invoice_id': self.id,
|
||||||
'analytic_tag_ids': analytic_tag_ids
|
'analytic_tag_ids': analytic_tag_ids
|
||||||
}
|
}
|
||||||
if line['account_analytic_id']:
|
|
||||||
move_line_dict['analytic_line_ids'] = [(0, 0, line._get_analytic_line())]
|
|
||||||
res.append(move_line_dict)
|
res.append(move_line_dict)
|
||||||
return res
|
return res
|
||||||
|
|
||||||
@@ -1445,21 +1443,6 @@ class AccountInvoiceLine(models.Model):
|
|||||||
_description = "Invoice Line"
|
_description = "Invoice Line"
|
||||||
_order = "invoice_id,sequence,id"
|
_order = "invoice_id,sequence,id"
|
||||||
|
|
||||||
@api.multi
|
|
||||||
def _get_analytic_line(self):
|
|
||||||
ref = self.invoice_id.number
|
|
||||||
return {
|
|
||||||
'name': self.name,
|
|
||||||
'date': self.invoice_id.date_invoice,
|
|
||||||
'account_id': self.account_analytic_id.id,
|
|
||||||
'unit_amount': self.quantity,
|
|
||||||
'amount': self.price_subtotal_signed,
|
|
||||||
'product_id': self.product_id.id,
|
|
||||||
'product_uom_id': self.uom_id.id,
|
|
||||||
'general_account_id': self.account_id.id,
|
|
||||||
'ref': ref,
|
|
||||||
}
|
|
||||||
|
|
||||||
@api.one
|
@api.one
|
||||||
@api.depends('price_unit', 'discount', 'invoice_line_tax_ids', 'quantity',
|
@api.depends('price_unit', 'discount', 'invoice_line_tax_ids', 'quantity',
|
||||||
'product_id', 'invoice_id.partner_id', 'invoice_id.currency_id', 'invoice_id.company_id',
|
'product_id', 'invoice_id.partner_id', 'invoice_id.currency_id', 'invoice_id.company_id',
|
||||||
|
|||||||
@@ -174,7 +174,6 @@ Best Regards,'''))
|
|||||||
def setting_init_fiscal_year_action(self):
|
def setting_init_fiscal_year_action(self):
|
||||||
""" Called by the 'Fiscal Year Opening' button of the setup bar."""
|
""" Called by the 'Fiscal Year Opening' button of the setup bar."""
|
||||||
company = self.env.user.company_id
|
company = self.env.user.company_id
|
||||||
company.create_op_move_if_non_existant()
|
|
||||||
new_wizard = self.env['account.financial.year.op'].create({'company_id': company.id})
|
new_wizard = self.env['account.financial.year.op'].create({'company_id': company.id})
|
||||||
view_id = self.env.ref('account.setup_financial_year_opening_form').id
|
view_id = self.env.ref('account.setup_financial_year_opening_form').id
|
||||||
|
|
||||||
@@ -265,7 +264,7 @@ Best Regards,'''))
|
|||||||
default_journal = self.env['account.journal'].search([('type', '=', 'general'), ('company_id', '=', self.id)], limit=1)
|
default_journal = self.env['account.journal'].search([('type', '=', 'general'), ('company_id', '=', self.id)], limit=1)
|
||||||
|
|
||||||
if not default_journal:
|
if not default_journal:
|
||||||
raise UserError(_("No miscellaneous journal could be found. Please create one before proceeding."))
|
raise UserError(_("Please install a chart of accounts or create a miscellaneous journal before proceeding."))
|
||||||
|
|
||||||
self.account_opening_move_id = self.env['account.move'].create({
|
self.account_opening_move_id = self.env['account.move'].create({
|
||||||
'name': _('Opening Journal Entry'),
|
'name': _('Opening Journal Entry'),
|
||||||
|
|||||||
@@ -179,7 +179,7 @@
|
|||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr t-foreach="range(max(5-len(invoice.invoice_line_ids),0))" t-as="l">
|
<tr t-foreach="range(max(5-len(invoice.invoice_line_ids),0))" t-as="l">
|
||||||
<td>&nbsp;</td>
|
<td t-translation="off">&nbsp;</td>
|
||||||
<td class="hidden"></td>
|
<td class="hidden"></td>
|
||||||
<td></td>
|
<td></td>
|
||||||
<td></td>
|
<td></td>
|
||||||
|
|||||||
@@ -88,7 +88,7 @@
|
|||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr t-foreach="range(max(5-len(o.invoice_line_ids),0))" t-as="l">
|
<tr t-foreach="range(max(5-len(o.invoice_line_ids),0))" t-as="l">
|
||||||
<td>&nbsp;</td>
|
<td t-translation="off">&nbsp;</td>
|
||||||
<td class="hidden"></td>
|
<td class="hidden"></td>
|
||||||
<td></td>
|
<td></td>
|
||||||
<td></td>
|
<td></td>
|
||||||
@@ -112,18 +112,18 @@
|
|||||||
<t t-foreach="o._get_tax_amount_by_group()" t-as="amount_by_group">
|
<t t-foreach="o._get_tax_amount_by_group()" t-as="amount_by_group">
|
||||||
<tr style="border-bottom:1px solid #dddddd;">
|
<tr style="border-bottom:1px solid #dddddd;">
|
||||||
<t t-if="len(o.tax_line_ids) == 1 and o.amount_untaxed == amount_by_group[2]">
|
<t t-if="len(o.tax_line_ids) == 1 and o.amount_untaxed == amount_by_group[2]">
|
||||||
<td><span t-esc="amount_by_group[0]"/></td>
|
|
||||||
<td class="text-right">
|
|
||||||
<span t-esc="amount_by_group[3]"/>
|
|
||||||
</td>
|
|
||||||
</t>
|
|
||||||
<t t-else="">
|
|
||||||
<td>
|
<td>
|
||||||
<span t-esc="amount_by_group[0]"/>
|
<span t-esc="amount_by_group[0]"/>
|
||||||
<span>&nbsp;<span>on</span>
|
<span>&nbsp;<span>on</span>
|
||||||
<t t-esc="amount_by_group[4]"/>
|
<t t-esc="amount_by_group[4]"/>
|
||||||
</span>
|
</span>
|
||||||
</td>
|
</td>
|
||||||
|
<td class="text-right">
|
||||||
|
<span t-esc="amount_by_group[3]"/>
|
||||||
|
</td>
|
||||||
|
</t>
|
||||||
|
<t t-else="">
|
||||||
|
<td><span t-esc="amount_by_group[0]"/></td>
|
||||||
<td class="text-right">
|
<td class="text-right">
|
||||||
<span t-esc="amount_by_group[3]" />
|
<span t-esc="amount_by_group[3]" />
|
||||||
</td>
|
</td>
|
||||||
|
|||||||
@@ -168,6 +168,10 @@
|
|||||||
<label for="currency_exchange_journal_id" class="col-md-3 o_light_label"/>
|
<label for="currency_exchange_journal_id" class="col-md-3 o_light_label"/>
|
||||||
<field name="currency_exchange_journal_id"/>
|
<field name="currency_exchange_journal_id"/>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="row mt16">
|
||||||
|
<label for="currency_exchange_journal_id" class="col-md-3 o_light_label"/>
|
||||||
|
<field name="currency_exchange_journal_id"/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -402,5 +402,57 @@ QUnit.test('non-existing action in a dashboard', function (assert) {
|
|||||||
form.destroy();
|
form.destroy();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
QUnit.test('clicking on a kanban\'s button should trigger the action', function (assert) {
|
||||||
|
assert.expect(2);
|
||||||
|
|
||||||
|
var form = createView({
|
||||||
|
View: FormView,
|
||||||
|
model: 'board',
|
||||||
|
data: this.data,
|
||||||
|
arch: '<form string="My Dashboard">' +
|
||||||
|
'<board style="2-1">' +
|
||||||
|
'<column>' +
|
||||||
|
'<action name="149" string="Partner" view_mode="kanban" id="action_0_1"></action>' +
|
||||||
|
'</column>' +
|
||||||
|
'</board>' +
|
||||||
|
'</form>',
|
||||||
|
archs: {
|
||||||
|
'partner,false,kanban':
|
||||||
|
'<kanban class="o_kanban_test"><templates><t t-name="kanban-box">' +
|
||||||
|
'<div>' +
|
||||||
|
'<field name="foo"/>' +
|
||||||
|
'</div>' +
|
||||||
|
'<div><button name="sitting_on_a_park_bench" type="object">Eying little girls with bad intent</button>' +
|
||||||
|
'</div>' +
|
||||||
|
'</t></templates></kanban>',
|
||||||
|
},
|
||||||
|
intercepts: {
|
||||||
|
execute_action: function (event) {
|
||||||
|
var data = event.data;
|
||||||
|
assert.strictEqual(data.env.model, 'partner', "should have correct model");
|
||||||
|
assert.strictEqual(data.action_data.name, 'sitting_on_a_park_bench',
|
||||||
|
"should call correct method");
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
mockRPC: function (route) {
|
||||||
|
if (route === '/board/static/src/img/layout_1-1-1.png') {
|
||||||
|
return $.when();
|
||||||
|
}
|
||||||
|
if (route === '/web/action/load') {
|
||||||
|
return $.when({res_model: 'partner', view_mode: 'kanban', views: [[false, 'kanban']]});
|
||||||
|
}
|
||||||
|
if (route === '/web/dataset/search_read') {
|
||||||
|
return $.when({records: [{foo: 'aqualung'}]});
|
||||||
|
}
|
||||||
|
return this._super.apply(this, arguments);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
form.$('.o_kanban_test').find('button:first').click();
|
||||||
|
|
||||||
|
form.destroy();
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -122,10 +122,10 @@
|
|||||||
<field name="stop" attrs="{'invisible': True}"/>
|
<field name="stop" attrs="{'invisible': True}"/>
|
||||||
<field name="id" attrs="{'invisible': True}"/>
|
<field name="id" attrs="{'invisible': True}"/>
|
||||||
|
|
||||||
<field name="start_date" string="Starting at" attrs="{'invisible': [('allday','=',False)], 'readonly': [('id', '!=', False), ('recurrency','=',True)]}"/>
|
<field name="start_date" string="Starting at" attrs="{'required': [('allday','=',True)], 'invisible': [('allday','=',False)], 'readonly': [('id', '!=', False), ('recurrency','=',True)]}"/>
|
||||||
<field name="stop_date" string="Ending at" attrs="{'invisible': [('allday','=',False)], 'readonly': [('id', '!=', False), ('recurrency','=',True)]}"/>
|
<field name="stop_date" string="Ending at" attrs="{'required': [('allday','=',True)],'invisible': [('allday','=',False)], 'readonly': [('id', '!=', False), ('recurrency','=',True)]}"/>
|
||||||
|
|
||||||
<field name="start_datetime" string="Starting at" attrs="{'invisible': [('allday','=',True)], 'readonly': [('id', '!=', False), ('recurrency','=',True)]}"/>
|
<field name="start_datetime" string="Starting at" attrs="{'required': [('allday','=',False)], 'invisible': [('allday','=',True)], 'readonly': [('id', '!=', False), ('recurrency','=',True)]}"/>
|
||||||
<field name="stop_datetime" invisible="1"/>
|
<field name="stop_datetime" invisible="1"/>
|
||||||
<label for="duration" attrs="{'invisible': [('allday','=',True)]}"/>
|
<label for="duration" attrs="{'invisible': [('allday','=',True)]}"/>
|
||||||
<div attrs="{'invisible': [('allday','=',True)]}">
|
<div attrs="{'invisible': [('allday','=',True)]}">
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import werkzeug.urls
|
|||||||
|
|
||||||
from odoo import api, fields, models
|
from odoo import api, fields, models
|
||||||
from odoo.exceptions import RedirectWarning, UserError
|
from odoo.exceptions import RedirectWarning, UserError
|
||||||
|
from odoo.tools import pycompat
|
||||||
from odoo.tools.safe_eval import safe_eval
|
from odoo.tools.safe_eval import safe_eval
|
||||||
from odoo.tools.translate import _
|
from odoo.tools.translate import _
|
||||||
|
|
||||||
@@ -153,6 +154,9 @@ class GoogleDrive(models.Model):
|
|||||||
a length of 1 element only (batch processing is not supported in the code, though nothing really prevent it)
|
a length of 1 element only (batch processing is not supported in the code, though nothing really prevent it)
|
||||||
:return: the config id and config name
|
:return: the config id and config name
|
||||||
'''
|
'''
|
||||||
|
# TO DO in master: fix my signature and my model
|
||||||
|
if isinstance(res_model, pycompat.string_types):
|
||||||
|
res_model = self.env['ir.model'].search([('model', '=', res_model)]).id
|
||||||
if not res_id:
|
if not res_id:
|
||||||
raise UserError(_("Creating google drive may only be done by one at a time."))
|
raise UserError(_("Creating google drive may only be done by one at a time."))
|
||||||
# check if a model is configured with a template
|
# check if a model is configured with a template
|
||||||
|
|||||||
@@ -1,78 +1,109 @@
|
|||||||
odoo.define('google_drive.google_drive', function (require) {
|
odoo.define('google_drive.sidebar', function (require) {
|
||||||
"use strict";
|
"use strict";
|
||||||
|
|
||||||
var data = require('web.data');
|
/**
|
||||||
|
* The purpose of this file is to include the Sidebar widget to add Google
|
||||||
|
* Drive related items.
|
||||||
|
*/
|
||||||
|
|
||||||
var Sidebar = require('web.Sidebar');
|
var Sidebar = require('web.Sidebar');
|
||||||
|
|
||||||
|
|
||||||
Sidebar.include({
|
Sidebar.include({
|
||||||
init: function () {
|
// TO DO: clean me in master
|
||||||
var self = this;
|
/**
|
||||||
var ids;
|
* @override
|
||||||
this._super.apply(this, arguments);
|
*/
|
||||||
var view = self.getParent();
|
start: function () {
|
||||||
var result;
|
var def;
|
||||||
if (view.fields_view && view.fields_view.type === "form") {
|
if (this.options.viewType === "form") {
|
||||||
ids = [];
|
def = this._addGoogleDocItems(this.env.model, this.env.activeIds[0]);
|
||||||
view.on("load_record", self, function (r) {
|
|
||||||
ids = [r.id];
|
|
||||||
self.add_gdoc_items(view, r.id);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
return $.when(def).then(this._super.bind(this));
|
||||||
},
|
},
|
||||||
add_gdoc_items: function (view, res_id) {
|
|
||||||
|
//--------------------------------------------------------------------------
|
||||||
|
// Private
|
||||||
|
//--------------------------------------------------------------------------
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @private
|
||||||
|
* @param {string} model
|
||||||
|
* @param {integer} resID
|
||||||
|
* @returns {Deferred}
|
||||||
|
*/
|
||||||
|
_addGoogleDocItems: function (model, resID) {
|
||||||
var self = this;
|
var self = this;
|
||||||
|
if (!resID) {
|
||||||
|
return $.when();
|
||||||
|
}
|
||||||
var gdoc_item = _.indexOf(_.pluck(self.items.other, 'classname'), 'oe_share_gdoc');
|
var gdoc_item = _.indexOf(_.pluck(self.items.other, 'classname'), 'oe_share_gdoc');
|
||||||
if (gdoc_item !== -1) {
|
if (gdoc_item !== -1) {
|
||||||
self.items.other.splice(gdoc_item, 1);
|
self.items.other.splice(gdoc_item, 1);
|
||||||
}
|
}
|
||||||
if (res_id) {
|
return this._rpc({
|
||||||
view.sidebar_eval_context().done(function (context) {
|
args: [this.env.model, resID],
|
||||||
var ds = new data.DataSet(this, 'google.drive.config', context);
|
context: this.env.context,
|
||||||
ds.call('get_google_drive_config', [view.dataset.model, res_id, context]).done(function (r) {
|
method: 'get_google_drive_config',
|
||||||
if (!_.isEmpty(r)) {
|
model: 'google.drive.config',
|
||||||
_.each(r, function (res) {
|
}).then(function (r) {
|
||||||
var already_there = false;
|
if (!_.isEmpty(r)) {
|
||||||
for (var i = 0;i < self.items.other.length;i++){
|
_.each(r, function (res) {
|
||||||
if (self.items.other[i].classname === "oe_share_gdoc" && self.items.other[i].label.indexOf(res.name) > -1){
|
var already_there = false;
|
||||||
already_there = true;
|
for (var i = 0; i < self.items.other.length; i++) {
|
||||||
break;
|
var item = self.items.other[i];
|
||||||
}
|
if (item.classname === 'oe_share_gdoc' && item.label.indexOf(res.name) > -1) {
|
||||||
}
|
already_there = true;
|
||||||
if (!already_there){
|
break;
|
||||||
self.add_items('other', [{
|
}
|
||||||
label: res.name,
|
}
|
||||||
config_id: res.id,
|
if (!already_there) {
|
||||||
res_id: res_id,
|
self._addItems('other', [{
|
||||||
res_model: view.dataset.model,
|
callback: self._onGoogleDocItemClicked.bind(self, res.id, resID),
|
||||||
callback: self.on_google_doc,
|
classname: 'oe_share_gdoc',
|
||||||
classname: 'oe_share_gdoc'
|
config_id: res.id,
|
||||||
},
|
label: res.name,
|
||||||
]);
|
res_id: resID,
|
||||||
}
|
res_model: model,
|
||||||
});
|
}]);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
}
|
||||||
}
|
});
|
||||||
},
|
},
|
||||||
on_google_doc: function (doc_item) {
|
|
||||||
|
//--------------------------------------------------------------------------
|
||||||
|
// Handlers
|
||||||
|
//--------------------------------------------------------------------------
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @private
|
||||||
|
* @param {integer} configID
|
||||||
|
* @param {integer} resID
|
||||||
|
*/
|
||||||
|
_onGoogleDocItemClicked: function (configID, resID) {
|
||||||
var self = this;
|
var self = this;
|
||||||
var domain = [['id', '=', doc_item.config_id]];
|
var domain = [['id', '=', configID]];
|
||||||
var fields = ['google_drive_resource_id', 'google_drive_client_id'];
|
var fields = ['google_drive_resource_id', 'google_drive_client_id'];
|
||||||
this._rpc({
|
this._rpc({
|
||||||
|
args: [domain, fields],
|
||||||
|
method: 'search_read',
|
||||||
|
model: 'google.drive.config',
|
||||||
|
}).then(function (configs) {
|
||||||
|
self._rpc({
|
||||||
|
args: [configID, resID, configs[0].google_drive_resource_id],
|
||||||
|
context: self.env.context,
|
||||||
|
method: 'get_google_drive_url',
|
||||||
model: 'google.drive.config',
|
model: 'google.drive.config',
|
||||||
method: 'search_read',
|
}).then(function (url) {
|
||||||
args: [domain, fields],
|
if (url){
|
||||||
})
|
window.open(url, '_blank');
|
||||||
.then(function (configs) {
|
}
|
||||||
var ds = new data.DataSet(self, 'google.drive.config');
|
|
||||||
ds.call('get_google_drive_url', [doc_item.config_id, doc_item.res_id,configs[0].google_drive_resource_id, self.dataset.context]).done(function (url) {
|
|
||||||
if (url){
|
|
||||||
window.open(url, '_blank');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
});
|
||||||
},
|
},
|
||||||
});
|
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
return Sidebar;
|
||||||
|
});
|
||||||
|
|||||||
@@ -0,0 +1,112 @@
|
|||||||
|
odoo.define('google_drive.gdrive_integration', function (require) {
|
||||||
|
"use strict";
|
||||||
|
//rebuild
|
||||||
|
var FormView = require('web.FormView');
|
||||||
|
var testUtils = require('web.test_utils');
|
||||||
|
var GoogleDriveSideBar = require('google_drive.sidebar');
|
||||||
|
|
||||||
|
var createView = testUtils.createView;
|
||||||
|
|
||||||
|
/*
|
||||||
|
* @override
|
||||||
|
* Avoid breaking other tests because of the new route
|
||||||
|
* that the module introduces
|
||||||
|
*/
|
||||||
|
var _addGoogleDocItemsOriginal = GoogleDriveSideBar.prototype._addGoogleDocItems;
|
||||||
|
|
||||||
|
var _addGoogleDocItemsMocked = function (model, resID) {
|
||||||
|
return $.when();
|
||||||
|
};
|
||||||
|
|
||||||
|
GoogleDriveSideBar.prototype._addGoogleDocItems = _addGoogleDocItemsMocked;
|
||||||
|
|
||||||
|
QUnit.module('gdrive_integration', {
|
||||||
|
beforeEach: function () {
|
||||||
|
// For our test to work, the _addGoogleDocItems function needs to be the original
|
||||||
|
GoogleDriveSideBar.prototype._addGoogleDocItems = _addGoogleDocItemsOriginal;
|
||||||
|
|
||||||
|
this.data = {
|
||||||
|
partner: {
|
||||||
|
fields: {
|
||||||
|
display_name: {string: "Displayed name", type: "char", searchable: true},
|
||||||
|
},
|
||||||
|
records: [{
|
||||||
|
id: 1,
|
||||||
|
display_name: "Locomotive Breath",
|
||||||
|
}],
|
||||||
|
},
|
||||||
|
'google.drive.config': {
|
||||||
|
fields: {
|
||||||
|
model_id: {string: 'Model', type: 'int'},
|
||||||
|
name: {string: 'Name', type: 'char'},
|
||||||
|
google_drive_resource_id: {string: 'Resource ID', type: 'char'},
|
||||||
|
},
|
||||||
|
records: [{
|
||||||
|
id: 27,
|
||||||
|
name: 'Cyberdyne Systems',
|
||||||
|
model_id: 1,
|
||||||
|
google_drive_resource_id: 'T1000',
|
||||||
|
}],
|
||||||
|
},
|
||||||
|
'ir.attachment': {
|
||||||
|
fields: {
|
||||||
|
name: {string: 'Name', type:'char'}
|
||||||
|
},
|
||||||
|
records: [],
|
||||||
|
}
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
afterEach: function() {
|
||||||
|
GoogleDriveSideBar.prototype._addGoogleDocItems = _addGoogleDocItemsMocked;
|
||||||
|
}
|
||||||
|
|
||||||
|
}, function () {
|
||||||
|
QUnit.module('Google Drive Sidebar');
|
||||||
|
|
||||||
|
QUnit.test('rendering of the google drive attachments in Sidebar', function (assert) {
|
||||||
|
assert.expect(3);
|
||||||
|
|
||||||
|
var form = createView({
|
||||||
|
View: FormView,
|
||||||
|
model: 'partner',
|
||||||
|
data: this.data,
|
||||||
|
arch: '<form string="Partners">' +
|
||||||
|
'<field name="display_name"/>' +
|
||||||
|
'</form>',
|
||||||
|
res_id: 1,
|
||||||
|
viewOptions: {sidebar: true},
|
||||||
|
mockRPC: function (route, args) {
|
||||||
|
if (route === '/web/dataset/call_kw/google.drive.config/get_google_drive_config') {
|
||||||
|
assert.deepEqual(args.args, ['partner', 1],
|
||||||
|
'The route to get google drive config should have been called');
|
||||||
|
return $.when([{id: 27, name: 'Cyberdyne Systems'}]);
|
||||||
|
}
|
||||||
|
if (route === '/web/dataset/call_kw/google.drive.config/search_read'){
|
||||||
|
return $.when([{google_drive_resource_id: "T1000",
|
||||||
|
google_drive_client_id: "cyberdyne.org",
|
||||||
|
id: 1}]);
|
||||||
|
}
|
||||||
|
if (route === '/web/dataset/call_kw/google.drive.config/get_google_drive_url') {
|
||||||
|
assert.deepEqual(args.args, [27, 1, 'T1000'],
|
||||||
|
'The route to get the Google url should have been called');
|
||||||
|
// We don't return anything useful, otherwise it will open a new tab
|
||||||
|
return $.when();
|
||||||
|
}
|
||||||
|
return this._super.apply(this, arguments);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
var google_action = form.sidebar.$('.oe_share_gdoc');
|
||||||
|
|
||||||
|
assert.strictEqual(google_action.length, 1,
|
||||||
|
'The button to the google action should be present');
|
||||||
|
|
||||||
|
// Trigger opening of the dynamic link
|
||||||
|
google_action.find('a:first').click();
|
||||||
|
|
||||||
|
form.destroy();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
||||||
@@ -8,4 +8,9 @@
|
|||||||
</xpath>
|
</xpath>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
<template id="qunit_suite" name="google_drive tests" inherit_id="web.qunit_suite">
|
||||||
|
<xpath expr="//t[@t-set='head']" position="inside">
|
||||||
|
<script type="text/javascript" src="/google_drive/static/tests/gdrive_test.js"></script>
|
||||||
|
</xpath>
|
||||||
|
</template>
|
||||||
</odoo>
|
</odoo>
|
||||||
|
|||||||
@@ -27,13 +27,13 @@
|
|||||||
<field eval="25" name="priority"/>
|
<field eval="25" name="priority"/>
|
||||||
<field name="arch" type="xml">
|
<field name="arch" type="xml">
|
||||||
<form string="Expenses" class="o_expense_form">
|
<form string="Expenses" class="o_expense_form">
|
||||||
<header>
|
<header invisible="context.get('expense_adding_line')">
|
||||||
<button name="submit_expenses" states="draft" string="Submit to Manager" type="object" class="oe_highlight o_expense_submit"/>
|
<button name="submit_expenses" states="draft" string="Submit to Manager" type="object" class="oe_highlight o_expense_submit"/>
|
||||||
<field name="state" widget="statusbar" statusbar_visible="draft,reported,done"/>
|
<field name="state" widget="statusbar" statusbar_visible="draft,reported,done"/>
|
||||||
<button name="view_sheet" type="object" string="View Report" class="oe_highlight" attrs="{'invisible': [('state', '=','draft')]}"/>
|
<button name="view_sheet" type="object" string="View Report" class="oe_highlight" attrs="{'invisible': [('state', '=','draft')]}"/>
|
||||||
</header>
|
</header>
|
||||||
<sheet>
|
<sheet>
|
||||||
<div class="oe_button_box">
|
<div class="oe_button_box" invisible="context.get('expense_adding_line')">
|
||||||
<button name="action_get_attachment_view"
|
<button name="action_get_attachment_view"
|
||||||
class="oe_stat_button"
|
class="oe_stat_button"
|
||||||
icon="fa-book"
|
icon="fa-book"
|
||||||
@@ -419,7 +419,7 @@
|
|||||||
</group>
|
</group>
|
||||||
<notebook>
|
<notebook>
|
||||||
<page string="Expense Lines">
|
<page string="Expense Lines">
|
||||||
<field name="expense_line_ids" nolabel="1" widget="many2many" domain="[('state', '=', 'draft')]" options="{'not_delete': True, 'reload_on_button': True, 'no_create': True}">
|
<field name="expense_line_ids" nolabel="1" widget="many2many" domain="[('state', '=', 'draft')]" options="{'not_delete': True, 'reload_on_button': True, 'no_create': True}" context="{'expense_adding_line': True}">
|
||||||
<tree decoration-danger="is_refused">
|
<tree decoration-danger="is_refused">
|
||||||
<field name="date"/>
|
<field name="date"/>
|
||||||
<field name="name"/>
|
<field name="name"/>
|
||||||
|
|||||||
@@ -31,4 +31,4 @@ class HolidaysSummaryDept(models.TransientModel):
|
|||||||
'model': 'hr.department',
|
'model': 'hr.department',
|
||||||
'form': data
|
'form': data
|
||||||
}
|
}
|
||||||
return self.env.ref('hr_holidays.action_report_holidayssummary').report_action(departments, data=datas)
|
return self.env.ref('hr_holidays.action_report_holidayssummary').with_context(from_transient_model=True).report_action(departments, data=datas)
|
||||||
|
|||||||
@@ -102,6 +102,7 @@ class HrPayslip(models.Model):
|
|||||||
def refund_sheet(self):
|
def refund_sheet(self):
|
||||||
for payslip in self:
|
for payslip in self:
|
||||||
copied_payslip = payslip.copy({'credit_note': True, 'name': _('Refund: ') + payslip.name})
|
copied_payslip = payslip.copy({'credit_note': True, 'name': _('Refund: ') + payslip.name})
|
||||||
|
copied_payslip.compute_sheet()
|
||||||
copied_payslip.action_payslip_done()
|
copied_payslip.action_payslip_done()
|
||||||
formview_ref = self.env.ref('hr_payroll.view_hr_payslip_form', False)
|
formview_ref = self.env.ref('hr_payroll.view_hr_payslip_form', False)
|
||||||
treeview_ref = self.env.ref('hr_payroll.view_hr_payslip_tree', False)
|
treeview_ref = self.env.ref('hr_payroll.view_hr_payslip_tree', False)
|
||||||
|
|||||||
@@ -59,3 +59,18 @@ class Task(models.Model):
|
|||||||
vals['parent_id'] = context.pop('default_parent_id', None)
|
vals['parent_id'] = context.pop('default_parent_id', None)
|
||||||
task = super(Task, self.with_context(context)).create(vals)
|
task = super(Task, self.with_context(context)).create(vals)
|
||||||
return task
|
return task
|
||||||
|
|
||||||
|
@api.multi
|
||||||
|
def write(self, values):
|
||||||
|
result = super(Task, self).write(values)
|
||||||
|
# reassign project_id on related timesheet lines
|
||||||
|
if 'project_id' in values:
|
||||||
|
project_id = values.get('project_id')
|
||||||
|
# a timesheet must have an analytic account (and a project)
|
||||||
|
if not project_id:
|
||||||
|
raise UserError(_('This task must have a project since they are linked to timesheets.'))
|
||||||
|
self.sudo().mapped('timesheet_ids').write({
|
||||||
|
'project_id': project_id,
|
||||||
|
'account_id': self.env['project.project'].browse(project_id).sudo().analytic_account_id.id
|
||||||
|
})
|
||||||
|
return result
|
||||||
@@ -48,7 +48,7 @@
|
|||||||
<field name="perm_create" eval="1"/>
|
<field name="perm_create" eval="1"/>
|
||||||
<field name="perm_write" eval="1"/>
|
<field name="perm_write" eval="1"/>
|
||||||
<field name="perm_unlink" eval="1"/>
|
<field name="perm_unlink" eval="1"/>
|
||||||
<field name="perm_read" eval="1"/>
|
<field name="perm_read" eval="0"/>
|
||||||
</record>
|
</record>
|
||||||
|
|
||||||
<record id="timesheet_rule_portal" model="ir.rule">
|
<record id="timesheet_rule_portal" model="ir.rule">
|
||||||
|
|||||||
@@ -2,13 +2,13 @@
|
|||||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||||
|
|
||||||
from odoo.tests.common import TransactionCase
|
from odoo.tests.common import TransactionCase
|
||||||
from odoo.exceptions import AccessError
|
from odoo.exceptions import AccessError, UserError
|
||||||
|
|
||||||
|
|
||||||
class TestTimesheet(TransactionCase):
|
class TestCommonTimesheet(TransactionCase):
|
||||||
|
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
super(TestTimesheet, self).setUp()
|
super(TestCommonTimesheet, self).setUp()
|
||||||
|
|
||||||
# customer partner
|
# customer partner
|
||||||
self.partner = self.env['res.partner'].create({
|
self.partner = self.env['res.partner'].create({
|
||||||
@@ -69,6 +69,9 @@ class TestTimesheet(TransactionCase):
|
|||||||
'user_id': self.user_manager.id,
|
'user_id': self.user_manager.id,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
|
class TestTimesheet(TestCommonTimesheet):
|
||||||
|
|
||||||
def test_log_timesheet(self):
|
def test_log_timesheet(self):
|
||||||
""" Test when log timesheet : check analytic account, user and employee are correctly set. """
|
""" Test when log timesheet : check analytic account, user and employee are correctly set. """
|
||||||
Timesheet = self.env['account.analytic.line']
|
Timesheet = self.env['account.analytic.line']
|
||||||
@@ -134,3 +137,42 @@ class TestTimesheet(TransactionCase):
|
|||||||
'employee_id': self.empl_employee2.id,
|
'employee_id': self.empl_employee2.id,
|
||||||
})
|
})
|
||||||
self.assertEquals(timesheet1.user_id, self.user_employee2, 'Changing timesheet employee should change the related user')
|
self.assertEquals(timesheet1.user_id, self.user_employee2, 'Changing timesheet employee should change the related user')
|
||||||
|
|
||||||
|
def test_transfert_project(self):
|
||||||
|
""" Test transfert task with timesheet to another project """
|
||||||
|
Timesheet = self.env['account.analytic.line']
|
||||||
|
# create a second project
|
||||||
|
self.project_customer2 = self.env['project.project'].create({
|
||||||
|
'name': 'Project NUMBER DEUX',
|
||||||
|
'allow_timesheets': True,
|
||||||
|
})
|
||||||
|
# employee 1 log some timesheet on task 1
|
||||||
|
Timesheet.create({
|
||||||
|
'project_id': self.project_customer.id,
|
||||||
|
'task_id': self.task1.id,
|
||||||
|
'name': 'my first timesheet',
|
||||||
|
'unit_amount': 4,
|
||||||
|
})
|
||||||
|
|
||||||
|
timesheet_count1 = Timesheet.search_count([('project_id', '=', self.project_customer.id)])
|
||||||
|
timesheet_count2 = Timesheet.search_count([('project_id', '=', self.project_customer2.id)])
|
||||||
|
self.assertEquals(timesheet_count1, 1, "One timesheet in project 1")
|
||||||
|
self.assertEquals(timesheet_count2, 0, "No timesheet in project 2")
|
||||||
|
self.assertEquals(len(self.task1.timesheet_ids), 1, "The timesheet should be linked to task 1")
|
||||||
|
|
||||||
|
# change project of task 1
|
||||||
|
self.task1.write({
|
||||||
|
'project_id': self.project_customer2.id
|
||||||
|
})
|
||||||
|
|
||||||
|
timesheet_count1 = Timesheet.search_count([('project_id', '=', self.project_customer.id)])
|
||||||
|
timesheet_count2 = Timesheet.search_count([('project_id', '=', self.project_customer2.id)])
|
||||||
|
self.assertEquals(timesheet_count1, 0, "No timesheet in project 1")
|
||||||
|
self.assertEquals(timesheet_count2, 1, "One timesheet in project 2")
|
||||||
|
self.assertEquals(len(self.task1.timesheet_ids), 1, "The timesheet should be linked to task 1")
|
||||||
|
|
||||||
|
# it is forbidden to set a task with timesheet without project
|
||||||
|
with self.assertRaises(UserError):
|
||||||
|
self.task1.write({
|
||||||
|
'project_id': False
|
||||||
|
})
|
||||||
|
|||||||
@@ -72,6 +72,11 @@ def jsonrpc(url, method='call', params=None):
|
|||||||
#----------------------------------------------------------
|
#----------------------------------------------------------
|
||||||
# Helpers for proxy
|
# Helpers for proxy
|
||||||
#----------------------------------------------------------
|
#----------------------------------------------------------
|
||||||
|
class IapTransaction(object):
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
self.credit = None
|
||||||
|
|
||||||
@contextlib.contextmanager
|
@contextlib.contextmanager
|
||||||
def charge(env, key, account_token, credit, description=None, credit_template=None):
|
def charge(env, key, account_token, credit, description=None, credit_template=None):
|
||||||
"""
|
"""
|
||||||
@@ -107,7 +112,9 @@ def charge(env, key, account_token, credit, description=None, credit_template=No
|
|||||||
e.args = (json.dumps(arguments),)
|
e.args = (json.dumps(arguments),)
|
||||||
raise e
|
raise e
|
||||||
try:
|
try:
|
||||||
yield
|
transaction = IapTransaction()
|
||||||
|
transaction.credit = credit
|
||||||
|
yield transaction
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
params = {
|
params = {
|
||||||
'token': transaction_token,
|
'token': transaction_token,
|
||||||
@@ -119,6 +126,7 @@ def charge(env, key, account_token, credit, description=None, credit_template=No
|
|||||||
params = {
|
params = {
|
||||||
'token': transaction_token,
|
'token': transaction_token,
|
||||||
'key': key,
|
'key': key,
|
||||||
|
'credit_to_capture': transaction.credit,
|
||||||
}
|
}
|
||||||
r = jsonrpc(endpoint + '/iap/1/capture', params=params) # noqa
|
r = jsonrpc(endpoint + '/iap/1/capture', params=params) # noqa
|
||||||
|
|
||||||
|
|||||||
@@ -67,7 +67,7 @@ class HrContract(models.Model):
|
|||||||
help="Number of days of paid leaves the employee gets per year.")
|
help="Number of days of paid leaves the employee gets per year.")
|
||||||
holidays_editable = fields.Boolean(string="Editable Leaves", default=True)
|
holidays_editable = fields.Boolean(string="Editable Leaves", default=True)
|
||||||
holidays_compensation = fields.Monetary(compute='_compute_holidays_compensation', string="Holidays Compensation")
|
holidays_compensation = fields.Monetary(compute='_compute_holidays_compensation', string="Holidays Compensation")
|
||||||
wage_with_holidays = fields.Monetary(compute='_compute_wage_with_holidays', string="Wage update with holidays retenues")
|
wage_with_holidays = fields.Monetary(compute='_compute_wage_with_holidays', inverse='_inverse_wage_with_holidays', string="Wage update with holidays retenues")
|
||||||
additional_net_amount = fields.Monetary(string="Net Supplements",
|
additional_net_amount = fields.Monetary(string="Net Supplements",
|
||||||
help="Monthly net amount the employee receives.")
|
help="Monthly net amount the employee receives.")
|
||||||
retained_net_amount = fields.Monetary(sting="Net Retained",
|
retained_net_amount = fields.Monetary(sting="Net Retained",
|
||||||
@@ -76,7 +76,7 @@ class HrContract(models.Model):
|
|||||||
default=lambda self: self.get_attribute('eco_checks', 'default_value'),
|
default=lambda self: self.get_attribute('eco_checks', 'default_value'),
|
||||||
help="Yearly amount the employee receives in the form of eco vouchers.")
|
help="Yearly amount the employee receives in the form of eco vouchers.")
|
||||||
|
|
||||||
@api.depends('holidays', 'wage')
|
@api.depends('holidays', 'wage', 'final_yearly_costs')
|
||||||
def _compute_wage_with_holidays(self):
|
def _compute_wage_with_holidays(self):
|
||||||
for contract in self:
|
for contract in self:
|
||||||
if contract.holidays > 20.0:
|
if contract.holidays > 20.0:
|
||||||
@@ -85,6 +85,23 @@ class HrContract(models.Model):
|
|||||||
else:
|
else:
|
||||||
contract.wage_with_holidays = contract.wage
|
contract.wage_with_holidays = contract.wage
|
||||||
|
|
||||||
|
def _inverse_wage_with_holidays(self):
|
||||||
|
for contract in self:
|
||||||
|
if contract.holidays > 20.0:
|
||||||
|
remaining_for_gross = contract.wage_with_holidays * (13.0 + 13.0 * 0.3507 + 0.92)
|
||||||
|
yearly_cost = remaining_for_gross \
|
||||||
|
+ 12.0 * contract.representation_fees \
|
||||||
|
+ 12.0 * contract.fuel_card \
|
||||||
|
+ 12.0 * contract.internet \
|
||||||
|
+ 12.0 * (contract.mobile + contract.mobile_plus) \
|
||||||
|
+ 12.0 * contract.transport_employer_cost \
|
||||||
|
+ (1.326 / 1.05 * 12.0) * contract.commission_on_target \
|
||||||
|
+ 220.0 * contract.meal_voucher_paid_by_employer
|
||||||
|
contract.final_yearly_costs = yearly_cost / (1.0 - (contract.holidays - 20.0) / 231.0)
|
||||||
|
contract.wage = contract._get_gross_from_employer_costs(contract.final_yearly_costs)
|
||||||
|
else:
|
||||||
|
contract.wage = contract.wage_with_holidays
|
||||||
|
|
||||||
@api.depends('transport_mode', 'company_car_total_depreciated_cost',
|
@api.depends('transport_mode', 'company_car_total_depreciated_cost',
|
||||||
'public_transport_reimbursed_amount', 'others_reimbursed_amount')
|
'public_transport_reimbursed_amount', 'others_reimbursed_amount')
|
||||||
def _compute_transport_employer_cost(self):
|
def _compute_transport_employer_cost(self):
|
||||||
|
|||||||
@@ -36,13 +36,13 @@ class AccountMove(models.Model):
|
|||||||
_('An error occured when computing the inalterability. Impossible to get the unique previous posted journal entry.'))
|
_('An error occured when computing the inalterability. Impossible to get the unique previous posted journal entry.'))
|
||||||
|
|
||||||
#build and return the hash
|
#build and return the hash
|
||||||
return self._compute_hash(prev_move.l10n_fr_hash if prev_move else '')
|
return self._compute_hash(prev_move.l10n_fr_hash if prev_move else u'')
|
||||||
|
|
||||||
def _compute_hash(self, previous_hash):
|
def _compute_hash(self, previous_hash):
|
||||||
""" Computes the hash of the browse_record given as self, based on the hash
|
""" Computes the hash of the browse_record given as self, based on the hash
|
||||||
of the previous record in the company's securisation sequence given as parameter"""
|
of the previous record in the company's securisation sequence given as parameter"""
|
||||||
self.ensure_one()
|
self.ensure_one()
|
||||||
hash_string = sha256(previous_hash + self.l10n_fr_string_to_hash)
|
hash_string = sha256((previous_hash + self.l10n_fr_string_to_hash).encode('utf-8'))
|
||||||
return hash_string.hexdigest()
|
return hash_string.hexdigest()
|
||||||
|
|
||||||
def _compute_string_to_hash(self):
|
def _compute_string_to_hash(self):
|
||||||
@@ -63,7 +63,7 @@ class AccountMove(models.Model):
|
|||||||
values[k] = _getattrstring(line, field)
|
values[k] = _getattrstring(line, field)
|
||||||
#make the json serialization canonical
|
#make the json serialization canonical
|
||||||
# (https://tools.ietf.org/html/draft-staykov-hu-json-canonical-form-00)
|
# (https://tools.ietf.org/html/draft-staykov-hu-json-canonical-form-00)
|
||||||
move.l10n_fr_string_to_hash = dumps(values, sort_keys=True, encoding="utf-8",
|
move.l10n_fr_string_to_hash = dumps(values, sort_keys=True,
|
||||||
ensure_ascii=True, indent=None,
|
ensure_ascii=True, indent=None,
|
||||||
separators=(',',':'))
|
separators=(',',':'))
|
||||||
|
|
||||||
@@ -118,7 +118,7 @@ class AccountMove(models.Model):
|
|||||||
|
|
||||||
if not moves:
|
if not moves:
|
||||||
raise UserError(_('There isn\'t any journal entry flagged for data inalterability yet for the company %s. This mechanism only runs for journal entries generated after the installation of the module France - Certification CGI 286 I-3 bis.') % self.env.user.company_id.name)
|
raise UserError(_('There isn\'t any journal entry flagged for data inalterability yet for the company %s. This mechanism only runs for journal entries generated after the installation of the module France - Certification CGI 286 I-3 bis.') % self.env.user.company_id.name)
|
||||||
previous_hash = ''
|
previous_hash = u''
|
||||||
start_move_info = []
|
start_move_info = []
|
||||||
for move in moves:
|
for move in moves:
|
||||||
if move.l10n_fr_hash != move._compute_hash(previous_hash=previous_hash):
|
if move.l10n_fr_hash != move._compute_hash(previous_hash=previous_hash):
|
||||||
|
|||||||
@@ -79,13 +79,13 @@ class pos_order(models.Model):
|
|||||||
_('An error occured when computing the inalterability. Impossible to get the unique previous posted point of sale order.'))
|
_('An error occured when computing the inalterability. Impossible to get the unique previous posted point of sale order.'))
|
||||||
|
|
||||||
#build and return the hash
|
#build and return the hash
|
||||||
return self._compute_hash(prev_order.l10n_fr_hash if prev_order else '')
|
return self._compute_hash(prev_order.l10n_fr_hash if prev_order else u'')
|
||||||
|
|
||||||
def _compute_hash(self, previous_hash):
|
def _compute_hash(self, previous_hash):
|
||||||
""" Computes the hash of the browse_record given as self, based on the hash
|
""" Computes the hash of the browse_record given as self, based on the hash
|
||||||
of the previous record in the company's securisation sequence given as parameter"""
|
of the previous record in the company's securisation sequence given as parameter"""
|
||||||
self.ensure_one()
|
self.ensure_one()
|
||||||
hash_string = sha256(previous_hash + self.l10n_fr_string_to_hash)
|
hash_string = sha256((previous_hash + self.l10n_fr_string_to_hash).encode('utf-8'))
|
||||||
return hash_string.hexdigest()
|
return hash_string.hexdigest()
|
||||||
|
|
||||||
def _compute_string_to_hash(self):
|
def _compute_string_to_hash(self):
|
||||||
@@ -108,7 +108,7 @@ class pos_order(models.Model):
|
|||||||
values[k] = _getattrstring(line, field)
|
values[k] = _getattrstring(line, field)
|
||||||
#make the json serialization canonical
|
#make the json serialization canonical
|
||||||
# (https://tools.ietf.org/html/draft-staykov-hu-json-canonical-form-00)
|
# (https://tools.ietf.org/html/draft-staykov-hu-json-canonical-form-00)
|
||||||
order.l10n_fr_string_to_hash = dumps(values, sort_keys=True, encoding="utf-8",
|
order.l10n_fr_string_to_hash = dumps(values, sort_keys=True,
|
||||||
ensure_ascii=True, indent=None,
|
ensure_ascii=True, indent=None,
|
||||||
separators=(',',':'))
|
separators=(',',':'))
|
||||||
|
|
||||||
@@ -155,7 +155,7 @@ class pos_order(models.Model):
|
|||||||
|
|
||||||
if not orders:
|
if not orders:
|
||||||
raise UserError(_('There isn\'t any order flagged for data inalterability yet for the company %s. This mechanism only runs for point of sale orders generated after the installation of the module France - Certification CGI 286 I-3 bis. - POS') % self.env.user.company_id.name)
|
raise UserError(_('There isn\'t any order flagged for data inalterability yet for the company %s. This mechanism only runs for point of sale orders generated after the installation of the module France - Certification CGI 286 I-3 bis. - POS') % self.env.user.company_id.name)
|
||||||
previous_hash = ''
|
previous_hash = u''
|
||||||
start_order_info = []
|
start_order_info = []
|
||||||
for order in orders:
|
for order in orders:
|
||||||
if order.l10n_fr_hash != order._compute_hash(previous_hash=previous_hash):
|
if order.l10n_fr_hash != order._compute_hash(previous_hash=previous_hash):
|
||||||
|
|||||||
@@ -148,7 +148,7 @@ class Message(models.Model):
|
|||||||
given, restrict to messages written in one of those channels. """
|
given, restrict to messages written in one of those channels. """
|
||||||
partner_id = self.env.user.partner_id.id
|
partner_id = self.env.user.partner_id.id
|
||||||
delete_mode = not self.env.user.share # delete employee notifs, keep customer ones
|
delete_mode = not self.env.user.share # delete employee notifs, keep customer ones
|
||||||
if domain is None and delete_mode:
|
if not domain and delete_mode:
|
||||||
query = "DELETE FROM mail_message_res_partner_needaction_rel WHERE res_partner_id IN %s"
|
query = "DELETE FROM mail_message_res_partner_needaction_rel WHERE res_partner_id IN %s"
|
||||||
args = [(partner_id,)]
|
args = [(partner_id,)]
|
||||||
if channel_ids:
|
if channel_ids:
|
||||||
@@ -468,6 +468,7 @@ class Message(models.Model):
|
|||||||
- if author_id == pid, uid is the author, OR
|
- if author_id == pid, uid is the author, OR
|
||||||
- uid belongs to a notified channel, OR
|
- uid belongs to a notified channel, OR
|
||||||
- uid is in the specified recipients, OR
|
- uid is in the specified recipients, OR
|
||||||
|
- uid has a notification on the message, OR
|
||||||
- uid have read access to the related document is model, res_id
|
- uid have read access to the related document is model, res_id
|
||||||
- otherwise: remove the id
|
- otherwise: remove the id
|
||||||
"""
|
"""
|
||||||
@@ -495,17 +496,22 @@ class Message(models.Model):
|
|||||||
# check read access rights before checking the actual rules on the given ids
|
# check read access rights before checking the actual rules on the given ids
|
||||||
super(Message, self.sudo(access_rights_uid or self._uid)).check_access_rights('read')
|
super(Message, self.sudo(access_rights_uid or self._uid)).check_access_rights('read')
|
||||||
|
|
||||||
self._cr.execute("""SELECT DISTINCT m.id, m.model, m.res_id, m.author_id, partner_rel.res_partner_id, channel_partner.channel_id as channel_id
|
self._cr.execute("""
|
||||||
|
SELECT DISTINCT m.id, m.model, m.res_id, m.author_id,
|
||||||
|
COALESCE(partner_rel.res_partner_id, needaction_rel.res_partner_id),
|
||||||
|
channel_partner.channel_id as channel_id
|
||||||
FROM "%s" m
|
FROM "%s" m
|
||||||
LEFT JOIN "mail_message_res_partner_rel" partner_rel
|
LEFT JOIN "mail_message_res_partner_rel" partner_rel
|
||||||
ON partner_rel.mail_message_id = m.id AND partner_rel.res_partner_id = (%%s)
|
ON partner_rel.mail_message_id = m.id AND partner_rel.res_partner_id = %%(pid)s
|
||||||
|
LEFT JOIN "mail_message_res_partner_needaction_rel" needaction_rel
|
||||||
|
ON needaction_rel.mail_message_id = m.id AND needaction_rel.res_partner_id = %%(pid)s
|
||||||
LEFT JOIN "mail_message_mail_channel_rel" channel_rel
|
LEFT JOIN "mail_message_mail_channel_rel" channel_rel
|
||||||
ON channel_rel.mail_message_id = m.id
|
ON channel_rel.mail_message_id = m.id
|
||||||
LEFT JOIN "mail_channel" channel
|
LEFT JOIN "mail_channel" channel
|
||||||
ON channel.id = channel_rel.mail_channel_id
|
ON channel.id = channel_rel.mail_channel_id
|
||||||
LEFT JOIN "mail_channel_partner" channel_partner
|
LEFT JOIN "mail_channel_partner" channel_partner
|
||||||
ON channel_partner.channel_id = channel.id AND channel_partner.partner_id = (%%s)
|
ON channel_partner.channel_id = channel.id AND channel_partner.partner_id = %%(pid)s
|
||||||
WHERE m.id = ANY (%%s)""" % self._table, (pid, pid, ids,))
|
WHERE m.id = ANY (%%(ids)s)""" % self._table, dict(pid=pid, ids=ids))
|
||||||
for id, rmod, rid, author_id, partner_id, channel_id in self._cr.fetchall():
|
for id, rmod, rid, author_id, partner_id, channel_id in self._cr.fetchall():
|
||||||
if author_id == pid:
|
if author_id == pid:
|
||||||
author_ids.add(id)
|
author_ids.add(id)
|
||||||
@@ -533,6 +539,7 @@ class Message(models.Model):
|
|||||||
- read: if
|
- read: if
|
||||||
- author_id == pid, uid is the author OR
|
- author_id == pid, uid is the author OR
|
||||||
- uid is in the recipients (partner_ids) OR
|
- uid is in the recipients (partner_ids) OR
|
||||||
|
- uid has been notified (needaction) OR
|
||||||
- uid is member of a listern channel (channel_ids.partner_ids) OR
|
- uid is member of a listern channel (channel_ids.partner_ids) OR
|
||||||
- uid have read access to the related document if model, res_id
|
- uid have read access to the related document if model, res_id
|
||||||
- otherwise: raise
|
- otherwise: raise
|
||||||
@@ -583,17 +590,22 @@ class Message(models.Model):
|
|||||||
message_values = dict((res_id, {}) for res_id in self.ids)
|
message_values = dict((res_id, {}) for res_id in self.ids)
|
||||||
|
|
||||||
if operation in ['read', 'write']:
|
if operation in ['read', 'write']:
|
||||||
self._cr.execute("""SELECT DISTINCT m.id, m.model, m.res_id, m.author_id, m.parent_id, partner_rel.res_partner_id, channel_partner.channel_id as channel_id
|
self._cr.execute("""
|
||||||
|
SELECT DISTINCT m.id, m.model, m.res_id, m.author_id, m.parent_id,
|
||||||
|
COALESCE(partner_rel.res_partner_id, needaction_rel.res_partner_id),
|
||||||
|
channel_partner.channel_id as channel_id
|
||||||
FROM "%s" m
|
FROM "%s" m
|
||||||
LEFT JOIN "mail_message_res_partner_rel" partner_rel
|
LEFT JOIN "mail_message_res_partner_rel" partner_rel
|
||||||
ON partner_rel.mail_message_id = m.id AND partner_rel.res_partner_id = (%%s)
|
ON partner_rel.mail_message_id = m.id AND partner_rel.res_partner_id = %%(pid)s
|
||||||
|
LEFT JOIN "mail_message_res_partner_needaction_rel" needaction_rel
|
||||||
|
ON needaction_rel.mail_message_id = m.id AND needaction_rel.res_partner_id = %%(pid)s
|
||||||
LEFT JOIN "mail_message_mail_channel_rel" channel_rel
|
LEFT JOIN "mail_message_mail_channel_rel" channel_rel
|
||||||
ON channel_rel.mail_message_id = m.id
|
ON channel_rel.mail_message_id = m.id
|
||||||
LEFT JOIN "mail_channel" channel
|
LEFT JOIN "mail_channel" channel
|
||||||
ON channel.id = channel_rel.mail_channel_id
|
ON channel.id = channel_rel.mail_channel_id
|
||||||
LEFT JOIN "mail_channel_partner" channel_partner
|
LEFT JOIN "mail_channel_partner" channel_partner
|
||||||
ON channel_partner.channel_id = channel.id AND channel_partner.partner_id = (%%s)
|
ON channel_partner.channel_id = channel.id AND channel_partner.partner_id = %%(pid)s
|
||||||
WHERE m.id = ANY (%%s)""" % self._table, (self.env.user.partner_id.id, self.env.user.partner_id.id, self.ids,))
|
WHERE m.id = ANY (%%(ids)s)""" % self._table, dict(pid=self.env.user.partner_id.id, ids=self.ids))
|
||||||
for mid, rmod, rid, author_id, parent_id, partner_id, channel_id in self._cr.fetchall():
|
for mid, rmod, rid, author_id, parent_id, partner_id, channel_id in self._cr.fetchall():
|
||||||
message_values[mid] = {
|
message_values[mid] = {
|
||||||
'model': rmod,
|
'model': rmod,
|
||||||
|
|||||||
@@ -125,7 +125,7 @@ class Users(models.Model):
|
|||||||
|
|
||||||
@api.model
|
@api.model
|
||||||
def activity_user_count(self):
|
def activity_user_count(self):
|
||||||
query = """SELECT m.name, count(*), act.res_model as model,
|
query = """SELECT m.id, count(*), act.res_model as model,
|
||||||
CASE
|
CASE
|
||||||
WHEN now()::date - act.date_deadline::date = 0 Then 'today'
|
WHEN now()::date - act.date_deadline::date = 0 Then 'today'
|
||||||
WHEN now()::date - act.date_deadline::date > 0 Then 'overdue'
|
WHEN now()::date - act.date_deadline::date > 0 Then 'overdue'
|
||||||
@@ -134,16 +134,18 @@ class Users(models.Model):
|
|||||||
FROM mail_activity AS act
|
FROM mail_activity AS act
|
||||||
JOIN ir_model AS m ON act.res_model_id = m.id
|
JOIN ir_model AS m ON act.res_model_id = m.id
|
||||||
WHERE user_id = %s
|
WHERE user_id = %s
|
||||||
GROUP BY m.name, states, act.res_model;
|
GROUP BY m.id, states, act.res_model;
|
||||||
"""
|
"""
|
||||||
self.env.cr.execute(query, [self.env.uid])
|
self.env.cr.execute(query, [self.env.uid])
|
||||||
activity_data = self.env.cr.dictfetchall()
|
activity_data = self.env.cr.dictfetchall()
|
||||||
|
model_ids = [a['id'] for a in activity_data]
|
||||||
|
model_names = {n[0]:n[1] for n in self.env['ir.model'].browse(model_ids).name_get()}
|
||||||
|
|
||||||
user_activities = {}
|
user_activities = {}
|
||||||
for activity in activity_data:
|
for activity in activity_data:
|
||||||
if not user_activities.get(activity['model']):
|
if not user_activities.get(activity['model']):
|
||||||
user_activities[activity['model']] = {
|
user_activities[activity['model']] = {
|
||||||
'name': activity['name'],
|
'name': model_names[activity['id']],
|
||||||
'model': activity['model'],
|
'model': activity['model'],
|
||||||
'icon': modules.module.get_module_icon(self.env[activity['model']]._original_module),
|
'icon': modules.module.get_module_icon(self.env[activity['model']]._original_module),
|
||||||
'total_count': 0, 'today_count': 0, 'overdue_count': 0, 'planned_count': 0,
|
'total_count': 0, 'today_count': 0, 'overdue_count': 0, 'planned_count': 0,
|
||||||
|
|||||||
@@ -258,7 +258,7 @@ var Chatter = Widget.extend(chat_mixin, {
|
|||||||
var suggested_partners = [];
|
var suggested_partners = [];
|
||||||
var thread_recipients = result[self.context.default_res_id];
|
var thread_recipients = result[self.context.default_res_id];
|
||||||
_.each(thread_recipients, function (recipient) {
|
_.each(thread_recipients, function (recipient) {
|
||||||
var parsed_email = utils.parse_email(recipient[1]);
|
var parsed_email = recipient[1] && utils.parse_email(recipient[1]);
|
||||||
suggested_partners.push({
|
suggested_partners.push({
|
||||||
checked: true,
|
checked: true,
|
||||||
partner_id: recipient[0],
|
partner_id: recipient[0],
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ odoo.define('mail.systray', function (require) {
|
|||||||
|
|
||||||
var config = require('web.config');
|
var config = require('web.config');
|
||||||
var core = require('web.core');
|
var core = require('web.core');
|
||||||
var framework = require('web.framework');
|
|
||||||
var session = require('web.session');
|
var session = require('web.session');
|
||||||
var SystrayMenu = require('web.SystrayMenu');
|
var SystrayMenu = require('web.SystrayMenu');
|
||||||
var Widget = require('web.Widget');
|
var Widget = require('web.Widget');
|
||||||
@@ -175,6 +174,9 @@ var ActivityMenu = Widget.extend({
|
|||||||
return self._rpc({
|
return self._rpc({
|
||||||
model: 'res.users',
|
model: 'res.users',
|
||||||
method: 'activity_user_count',
|
method: 'activity_user_count',
|
||||||
|
kwargs: {
|
||||||
|
context: session.user_context,
|
||||||
|
},
|
||||||
}).then(function (data) {
|
}).then(function (data) {
|
||||||
self.activities = data;
|
self.activities = data;
|
||||||
self.activityCounter = _.reduce(data, function(total_count, p_data){ return total_count + p_data.total_count; }, 0);
|
self.activityCounter = _.reduce(data, function(total_count, p_data){ return total_count + p_data.total_count; }, 0);
|
||||||
|
|||||||
@@ -100,15 +100,17 @@ function inline (node, transform_children) {
|
|||||||
|
|
||||||
// Parses text to find email: Tagada <address@mail.fr> -> [Tagada, address@mail.fr] or False
|
// Parses text to find email: Tagada <address@mail.fr> -> [Tagada, address@mail.fr] or False
|
||||||
function parse_email (text) {
|
function parse_email (text) {
|
||||||
var result = text.match(/(.*)<(.*@.*)>/);
|
if (text){
|
||||||
if (result) {
|
var result = text.match(/(.*)<(.*@.*)>/);
|
||||||
return [_.str.trim(result[1]), _.str.trim(result[2])];
|
if (result) {
|
||||||
|
return [_.str.trim(result[1]), _.str.trim(result[2])];
|
||||||
|
}
|
||||||
|
result = text.match(/(.*@.*)/);
|
||||||
|
if (result) {
|
||||||
|
return [_.str.trim(result[1]), _.str.trim(result[1])];
|
||||||
|
}
|
||||||
|
return [text, false];
|
||||||
}
|
}
|
||||||
result = text.match(/(.*@.*)/);
|
|
||||||
if (result) {
|
|
||||||
return [_.str.trim(result[1]), _.str.trim(result[1])];
|
|
||||||
}
|
|
||||||
return [text, false];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Replaces textarea text into html text (add <p>, <a>)
|
// Replaces textarea text into html text (add <p>, <a>)
|
||||||
|
|||||||
@@ -0,0 +1,105 @@
|
|||||||
|
odoo.define('mail.client_action_mobile_tests', function (require) {
|
||||||
|
"use strict";
|
||||||
|
|
||||||
|
var session = require('web.session');
|
||||||
|
var testUtils = require('web.test_utils');
|
||||||
|
var Widget = require('web.Widget');
|
||||||
|
|
||||||
|
var ChatAction = require('mail.chat_client_action');
|
||||||
|
var chat_manager = require('mail.chat_manager');
|
||||||
|
|
||||||
|
QUnit.module('mail', {}, function () {
|
||||||
|
|
||||||
|
QUnit.module('Discuss client action in mobile', {
|
||||||
|
beforeEach: function () {
|
||||||
|
this.data = {
|
||||||
|
'mail.message': {
|
||||||
|
fields: {},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
this.createChatAction = function (params) {
|
||||||
|
var Parent = Widget.extend({
|
||||||
|
do_push_state: function () {},
|
||||||
|
});
|
||||||
|
var parent = new Parent();
|
||||||
|
testUtils.addMockEnvironment(parent, {
|
||||||
|
data: this.data,
|
||||||
|
archs: {
|
||||||
|
'mail.message,false,search': '<search/>',
|
||||||
|
},
|
||||||
|
session: params.session || {},
|
||||||
|
intercepts: params.intercepts || {},
|
||||||
|
});
|
||||||
|
var chatAction = new ChatAction(parent, params);
|
||||||
|
chatAction.set_cp_bus(new Widget());
|
||||||
|
chatAction.appendTo($('#qunit-fixture'));
|
||||||
|
|
||||||
|
return chatAction;
|
||||||
|
};
|
||||||
|
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
QUnit.test('mobile basic rendering', function (assert) {
|
||||||
|
// This is a very basic first test for the client action. However, with
|
||||||
|
// the chat_manager, it is hard to override RPCs (for instance, the
|
||||||
|
// /mail/client_action route is always called when the test suite is
|
||||||
|
// launched), and we must wait for this RPC to be done before starting to
|
||||||
|
// test the interface. This should be refactored to facilitate the testing.
|
||||||
|
assert.expect(9);
|
||||||
|
var done = assert.async();
|
||||||
|
|
||||||
|
var rpc = session.rpc;
|
||||||
|
|
||||||
|
var chatAction = this.createChatAction({
|
||||||
|
id: 1,
|
||||||
|
context: {},
|
||||||
|
params: {},
|
||||||
|
intercepts: {
|
||||||
|
get_session: function (ev) {
|
||||||
|
ev.data.callback({});
|
||||||
|
},
|
||||||
|
},
|
||||||
|
session: {
|
||||||
|
rpc: function (route, args) {
|
||||||
|
if (args.method === 'message_fetch') {
|
||||||
|
return $.when([]);
|
||||||
|
}
|
||||||
|
return rpc.apply(this, arguments);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
chat_manager.is_ready.then(function () {
|
||||||
|
// test basic rendering in mobile
|
||||||
|
assert.strictEqual(chatAction.$('.o_mail_chat_mobile_control_panel').length, 1,
|
||||||
|
"should have rendered a control panel");
|
||||||
|
assert.strictEqual(chatAction.$('.o_mail_chat_content .o_mail_no_content').length, 1,
|
||||||
|
"should display the no content message");
|
||||||
|
assert.strictEqual(chatAction.$('.o_mail_mobile_tabs').length, 1,
|
||||||
|
"should have rendered the tabs");
|
||||||
|
assert.ok(chatAction.$('.o_mail_mobile_tab[data-type=channel_inbox]').hasClass('active'),
|
||||||
|
"should be in inbox tab");
|
||||||
|
assert.strictEqual(chatAction.$('.o_mail_chat_mobile_inbox_buttons:visible').length, 1,
|
||||||
|
"inbox/starred buttons should be visible");
|
||||||
|
assert.ok(chatAction.$('.o_mail_chat_mobile_inbox_buttons .o_channel_inbox_item[data-type=channel_inbox]').hasClass('btn-primary'),
|
||||||
|
"should be in inbox");
|
||||||
|
|
||||||
|
// move to DMs tab
|
||||||
|
chatAction.$('.o_mail_mobile_tab[data-type=dm]').click();
|
||||||
|
assert.ok(chatAction.$('.o_mail_mobile_tab[data-type=dm]').hasClass('active'),
|
||||||
|
"should be in DMs tab");
|
||||||
|
assert.strictEqual(chatAction.$('.o_mail_chat_content .o_mail_no_content').length, 0,
|
||||||
|
"should display the no content message");
|
||||||
|
chatAction.$('.o_mail_chat_button_dm').click(); // click to add a channel
|
||||||
|
assert.strictEqual(chatAction.$('.o_mail_add_channel input:visible').length, 1,
|
||||||
|
"should display the input to add a channel");
|
||||||
|
|
||||||
|
chatAction.destroy();
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
||||||
@@ -1,88 +0,0 @@
|
|||||||
odoo.define('mail.client_action_test', function (require) {
|
|
||||||
"use strict";
|
|
||||||
|
|
||||||
var testUtils = require('web.test_utils');
|
|
||||||
var Widget = require('web.Widget');
|
|
||||||
|
|
||||||
var ChatAction = require("mail.chat_client_action");
|
|
||||||
|
|
||||||
QUnit.module('mail', {}, function () {
|
|
||||||
|
|
||||||
QUnit.module('Discuss client action', {
|
|
||||||
beforeEach: function () {
|
|
||||||
this.data = {
|
|
||||||
'mail.message': {
|
|
||||||
fields: {},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
this.createChatAction = function (params) {
|
|
||||||
var parent = new Widget();
|
|
||||||
testUtils.addMockEnvironment(parent, {
|
|
||||||
data: this.data,
|
|
||||||
archs: {
|
|
||||||
'mail.message,false,search': '<search/>',
|
|
||||||
},
|
|
||||||
config: {device: {isMobile: true}},
|
|
||||||
});
|
|
||||||
var chatAction = new ChatAction(parent, params);
|
|
||||||
chatAction.set_cp_bus(new Widget());
|
|
||||||
chatAction.appendTo($('#qunit-fixture'));
|
|
||||||
|
|
||||||
return chatAction;
|
|
||||||
};
|
|
||||||
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
QUnit.skip('mobile basic rendering', function (assert) {
|
|
||||||
// Unfortunately, this test is skipped for now because there is no way to
|
|
||||||
// execute the whole test suite in mobile (it only works test by test), so
|
|
||||||
// as the client action include for mobile is rejected when we are not in
|
|
||||||
// mobile, it isn't possible to test it
|
|
||||||
// Moreover, RPCs done by the chat_manager (e.g. message_fetch) should be
|
|
||||||
// properly mocked.
|
|
||||||
assert.expect(11);
|
|
||||||
|
|
||||||
var parent = new Widget();
|
|
||||||
testUtils.addMockEnvironment(parent, {
|
|
||||||
data: this.data,
|
|
||||||
archs: {
|
|
||||||
'mail.message,false,search': '<search/>',
|
|
||||||
},
|
|
||||||
config: {device: {isMobile: true}},
|
|
||||||
});
|
|
||||||
|
|
||||||
var params = {
|
|
||||||
id: 1,
|
|
||||||
context: {},
|
|
||||||
params: {},
|
|
||||||
};
|
|
||||||
var chatAction = this.createChatAction(params);
|
|
||||||
|
|
||||||
// test basic rendering in mobile
|
|
||||||
assert.equal(chatAction.$(".o_mail_chat_mobile_control_panel").length, 1, "Mobile control panel created");
|
|
||||||
assert.equal(chatAction.$(".o_mail_mobile_tab").length, 4, "Four mobile tabs created");
|
|
||||||
assert.equal(chatAction.$('.o_mail_chat_content').length, 1, "One default chat content pane created");
|
|
||||||
assert.equal(chatAction.$(".o_mail_chat_tab_pane").length, 3, "Three mobile tab panes created");
|
|
||||||
|
|
||||||
// Inbox
|
|
||||||
assert.equal(chatAction.activeMobileTab, "channel_inbox", "'channel_inbox' is default active tab");
|
|
||||||
assert.ok(chatAction.$(".o_channel_inbox_item:nth(0)").hasClass("btn-primary"), "Showing 'Inbox'");
|
|
||||||
|
|
||||||
// Starred
|
|
||||||
chatAction.$(".o_channel_inbox_item[data-type='channel_starred']").click();
|
|
||||||
assert.ok(chatAction.$(".o_channel_inbox_item:nth(1)").hasClass("btn-primary"), "Clicked on 'Starred'");
|
|
||||||
|
|
||||||
assert.ok(chatAction.$(".o_mail_chat_content").is(":visible"), "Default main content pane visible");
|
|
||||||
|
|
||||||
chatAction.$(".o_mail_mobile_tab[data-type='dm']").click();
|
|
||||||
assert.equal(chatAction.activeMobileTab, "dm", "After click on 'Conversation', is now active tab");
|
|
||||||
|
|
||||||
assert.ok(!chatAction.$(".o_mail_chat_content").is(":visible"), "none", "'Main' content pane is invisible");
|
|
||||||
assert.ok(chatAction.$(".o_mail_chat_tab_pane:nth(0)").is(":visible"), "'Conversation' pane is visible");
|
|
||||||
|
|
||||||
chatAction.destroy();
|
|
||||||
});
|
|
||||||
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -43,13 +43,18 @@
|
|||||||
|
|
||||||
<template id="qunit_suite" name="mail_tests" inherit_id="web.qunit_suite">
|
<template id="qunit_suite" name="mail_tests" inherit_id="web.qunit_suite">
|
||||||
<xpath expr="//t[@t-set='head']" position="inside">
|
<xpath expr="//t[@t-set='head']" position="inside">
|
||||||
<script type="text/javascript" src="/mail/static/tests/client_action_tests.js"></script>
|
|
||||||
<script type="text/javascript" src="/mail/static/tests/chatter_tests.js"></script>
|
<script type="text/javascript" src="/mail/static/tests/chatter_tests.js"></script>
|
||||||
<script type="text/javascript" src="/mail/static/tests/systray_tests.js"></script>
|
<script type="text/javascript" src="/mail/static/tests/systray_tests.js"></script>
|
||||||
<script type="text/javascript" src="/mail/static/tests/chat_window_test.js"></script>
|
<script type="text/javascript" src="/mail/static/tests/chat_window_test.js"></script>
|
||||||
</xpath>
|
</xpath>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
<template id="qunit_mobile_suite" name="mail_mobile_tests" inherit_id="web.qunit_mobile_suite">
|
||||||
|
<xpath expr="//t[@t-set='head']" position="inside">
|
||||||
|
<script type="text/javascript" src="/mail/static/tests/client_action_mobile_tests.js"></script>
|
||||||
|
</xpath>
|
||||||
|
</template>
|
||||||
|
|
||||||
<template id="message_origin_link">
|
<template id="message_origin_link">
|
||||||
<p>This <t t-esc="self._description.lower()"/> has been <span t-if="edit">modified</span><span t-if="not edit">created</span> from:
|
<p>This <t t-esc="self._description.lower()"/> has been <span t-if="edit">modified</span><span t-if="not edit">created</span> from:
|
||||||
<t t-foreach="origin" t-as="o">
|
<t t-foreach="origin" t-as="o">
|
||||||
|
|||||||
@@ -213,6 +213,15 @@ class MaintenanceEquipment(models.Model):
|
|||||||
self.message_subscribe_users(user_ids=[vals['owner_user_id']])
|
self.message_subscribe_users(user_ids=[vals['owner_user_id']])
|
||||||
return super(MaintenanceEquipment, self).write(vals)
|
return super(MaintenanceEquipment, self).write(vals)
|
||||||
|
|
||||||
|
@api.model
|
||||||
|
def _message_get_auto_subscribe_fields(self, updated_fields, auto_follow_fields=None):
|
||||||
|
""" mail.thread override so user_id which has no special access allowance is not
|
||||||
|
automatically subscribed.
|
||||||
|
"""
|
||||||
|
if auto_follow_fields is None:
|
||||||
|
auto_follow_fields = []
|
||||||
|
return super(MaintenanceEquipment, self)._message_get_auto_subscribe_fields(updated_fields, auto_follow_fields)
|
||||||
|
|
||||||
@api.model
|
@api.model
|
||||||
def _read_group_category_ids(self, categories, domain, order):
|
def _read_group_category_ids(self, categories, domain, order):
|
||||||
""" Read group customization in order to display all the categories in
|
""" Read group customization in order to display all the categories in
|
||||||
|
|||||||
@@ -585,14 +585,15 @@ class MassMailing(models.Model):
|
|||||||
self.ensure_one()
|
self.ensure_one()
|
||||||
mass_mailing_copy = self.copy()
|
mass_mailing_copy = self.copy()
|
||||||
if mass_mailing_copy:
|
if mass_mailing_copy:
|
||||||
|
context = dict(self.env.context)
|
||||||
|
context['form_view_initial_mode'] = 'edit'
|
||||||
return {
|
return {
|
||||||
'type': 'ir.actions.act_window',
|
'type': 'ir.actions.act_window',
|
||||||
'view_type': 'form',
|
'view_type': 'form',
|
||||||
'view_mode': 'form',
|
'view_mode': 'form',
|
||||||
'res_model': 'mail.mass_mailing',
|
'res_model': 'mail.mass_mailing',
|
||||||
'res_id': mass_mailing_copy.id,
|
'res_id': mass_mailing_copy.id,
|
||||||
'context': self.env.context,
|
'context': context,
|
||||||
'flags': {'initial_mode': 'edit'},
|
|
||||||
}
|
}
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
|||||||
@@ -88,6 +88,10 @@ class MrpUnbuild(models.Model):
|
|||||||
if self.product_id.tracking != 'none' and not self.lot_id.id:
|
if self.product_id.tracking != 'none' and not self.lot_id.id:
|
||||||
raise UserError(_('Should have a lot for the finished product'))
|
raise UserError(_('Should have a lot for the finished product'))
|
||||||
|
|
||||||
|
if self.mo_id:
|
||||||
|
if self.mo_id.state != 'done':
|
||||||
|
raise UserError(_('You cannot unbuild a undone manufacturing order.'))
|
||||||
|
|
||||||
consume_move = self._generate_consume_moves()[0]
|
consume_move = self._generate_consume_moves()[0]
|
||||||
produce_moves = self._generate_produce_moves()
|
produce_moves = self._generate_produce_moves()
|
||||||
|
|
||||||
@@ -156,12 +160,31 @@ class MrpUnbuild(models.Model):
|
|||||||
def _generate_produce_moves(self):
|
def _generate_produce_moves(self):
|
||||||
moves = self.env['stock.move']
|
moves = self.env['stock.move']
|
||||||
for unbuild in self:
|
for unbuild in self:
|
||||||
factor = unbuild.product_uom_id._compute_quantity(unbuild.product_qty, unbuild.bom_id.product_uom_id) / unbuild.bom_id.product_qty
|
if unbuild.mo_id:
|
||||||
boms, lines = unbuild.bom_id.explode(unbuild.product_id, factor, picking_type=unbuild.bom_id.picking_type_id)
|
raw_moves = unbuild.mo_id.move_raw_ids.filtered(lambda move: move.state == 'done')
|
||||||
for line, line_data in lines:
|
factor = unbuild.product_qty / unbuild.mo_id.product_uom_id._compute_quantity(unbuild.mo_id.product_qty, unbuild.product_uom_id)
|
||||||
moves += unbuild._generate_move_from_bom_line(line, line_data['qty'])
|
for raw_move in raw_moves:
|
||||||
|
moves += unbuild._generate_move_from_raw_moves(raw_move, factor)
|
||||||
|
else:
|
||||||
|
factor = unbuild.product_uom_id._compute_quantity(unbuild.product_qty, unbuild.bom_id.product_uom_id) / unbuild.bom_id.product_qty
|
||||||
|
boms, lines = unbuild.bom_id.explode(unbuild.product_id, factor, picking_type=unbuild.bom_id.picking_type_id)
|
||||||
|
for line, line_data in lines:
|
||||||
|
moves += unbuild._generate_move_from_bom_line(line, line_data['qty'])
|
||||||
return moves
|
return moves
|
||||||
|
|
||||||
|
def _generate_move_from_raw_moves(self, raw_move, factor):
|
||||||
|
return self.env['stock.move'].create({
|
||||||
|
'name': self.name,
|
||||||
|
'date': self.create_date,
|
||||||
|
'product_id': raw_move.product_id.id,
|
||||||
|
'product_uom_qty': raw_move.product_uom_qty * factor,
|
||||||
|
'product_uom': raw_move.product_uom.id,
|
||||||
|
'procure_method': 'make_to_stock',
|
||||||
|
'location_dest_id': self.location_dest_id.id,
|
||||||
|
'location_id': raw_move.location_dest_id.id,
|
||||||
|
'unbuild_id': self.id,
|
||||||
|
})
|
||||||
|
|
||||||
def _generate_move_from_bom_line(self, bom_line, quantity):
|
def _generate_move_from_bom_line(self, bom_line, quantity):
|
||||||
return self.env['stock.move'].create({
|
return self.env['stock.move'].create({
|
||||||
'name': self.name,
|
'name': self.name,
|
||||||
|
|||||||
@@ -195,6 +195,12 @@ class StockMove(models.Model):
|
|||||||
move_lines = self.move_line_ids.filtered(lambda ml: ml.lot_id == lot and not ml.lot_produced_id)
|
move_lines = self.move_line_ids.filtered(lambda ml: ml.lot_id == lot and not ml.lot_produced_id)
|
||||||
else:
|
else:
|
||||||
move_lines = self.move_line_ids.filtered(lambda ml: not ml.lot_id and not ml.lot_produced_id)
|
move_lines = self.move_line_ids.filtered(lambda ml: not ml.lot_id and not ml.lot_produced_id)
|
||||||
|
|
||||||
|
# Sanity check: if the product is a serial number and `lot` is already present in the other
|
||||||
|
# consumed move lines, raise.
|
||||||
|
if lot and self.product_id.tracking == 'serial' and lot in self.move_line_ids.filtered(lambda ml: ml.qty_done).mapped('lot_id'):
|
||||||
|
raise UserError(_('You cannot consume the same serial number twice. Please correct the serial numbers encoded.'))
|
||||||
|
|
||||||
for ml in move_lines:
|
for ml in move_lines:
|
||||||
rounding = ml.product_uom_id.rounding
|
rounding = ml.product_uom_id.rounding
|
||||||
if float_compare(qty_to_add, 0, precision_rounding=rounding) <= 0:
|
if float_compare(qty_to_add, 0, precision_rounding=rounding) <= 0:
|
||||||
|
|||||||
@@ -122,7 +122,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</group>
|
</group>
|
||||||
<group>
|
<group>
|
||||||
<field name="mo_id" required="True"/>
|
<field name="mo_id" domain="[('state','=','done')]"/>
|
||||||
<field name="location_id" groups="stock.group_stock_multi_locations"/>
|
<field name="location_id" groups="stock.group_stock_multi_locations"/>
|
||||||
<field name="location_dest_id" groups="stock.group_stock_multi_locations"/>
|
<field name="location_dest_id" groups="stock.group_stock_multi_locations"/>
|
||||||
<field name="has_tracking" invisible="1"/>
|
<field name="has_tracking" invisible="1"/>
|
||||||
|
|||||||
@@ -125,6 +125,8 @@ class MrpProductProduce(models.TransientModel):
|
|||||||
raise UserError(_('You need to provide a lot for the finished product'))
|
raise UserError(_('You need to provide a lot for the finished product'))
|
||||||
existing_move_line = produce_move.move_line_ids.filtered(lambda x: x.lot_id == self.lot_id)
|
existing_move_line = produce_move.move_line_ids.filtered(lambda x: x.lot_id == self.lot_id)
|
||||||
if existing_move_line:
|
if existing_move_line:
|
||||||
|
if self.product_id.tracking == 'serial':
|
||||||
|
raise UserError(_('You cannot produce the same serial number twice.'))
|
||||||
existing_move_line.product_uom_qty += self.product_qty
|
existing_move_line.product_uom_qty += self.product_qty
|
||||||
existing_move_line.qty_done += self.product_qty
|
existing_move_line.qty_done += self.product_qty
|
||||||
else:
|
else:
|
||||||
@@ -181,27 +183,25 @@ class MrpProductProduceLine(models.TransientModel):
|
|||||||
|
|
||||||
@api.onchange('lot_id')
|
@api.onchange('lot_id')
|
||||||
def _onchange_lot_id(self):
|
def _onchange_lot_id(self):
|
||||||
|
""" When the user is encoding a produce line for a tracked product, we apply some logic to
|
||||||
|
help him. This onchange will automatically switch `qty_done` to 1.0.
|
||||||
|
"""
|
||||||
res = {}
|
res = {}
|
||||||
if self.product_id.tracking == 'serial':
|
if self.product_id.tracking == 'serial':
|
||||||
self.qty_done = 1
|
self.qty_done = 1
|
||||||
return res
|
return res
|
||||||
|
|
||||||
@api.constrains('lot_id')
|
@api.onchange('qty_done')
|
||||||
def _check_lot_id(self):
|
def _onchange_qty_done(self):
|
||||||
for ml in self:
|
""" When the user is encoding a produce line for a tracked product, we apply some logic to
|
||||||
if ml.product_id.tracking == 'serial':
|
help him. This onchange will warn him if he set `qty_done` to a non-supported value.
|
||||||
produce_lines_to_check = ml.product_produce_id.produce_line_ids.filtered(lambda l: l.product_id == ml.product_id and l.lot_id)
|
"""
|
||||||
message = produce_lines_to_check._check_for_duplicated_serial_numbers()
|
res = {}
|
||||||
if message:
|
if self.product_id.tracking == 'serial':
|
||||||
raise ValidationError(message)
|
if float_compare(self.qty_done, 1.0, precision_rounding=self.move_id.product_id.uom_id.rounding) != 0:
|
||||||
|
message = _('You can only process 1.0 %s for products with unique serial number.') % self.product_id.uom_id.name
|
||||||
def _check_for_duplicated_serial_numbers(self):
|
res['warning'] = {'title': _('Warning'), 'message': message}
|
||||||
if self.mapped('lot_id'):
|
return res
|
||||||
lots_map = [(ml.product_id.id, ml.lot_id.name) for ml in self]
|
|
||||||
recorded_serials_counter = Counter(lots_map)
|
|
||||||
for (product_id, lot_id), occurrences in recorded_serials_counter.items():
|
|
||||||
if occurrences > 1 and lot_id is not False:
|
|
||||||
return _('You cannot consume the same serial number twice. Please correct the serial numbers encoded.')
|
|
||||||
|
|
||||||
@api.onchange('product_id')
|
@api.onchange('product_id')
|
||||||
def _onchange_product_id(self):
|
def _onchange_product_id(self):
|
||||||
|
|||||||
@@ -51,7 +51,10 @@ class WebsitePayment(http.Controller):
|
|||||||
'amount': float(amount),
|
'amount': float(amount),
|
||||||
'payment_form': payment_form,
|
'payment_form': payment_form,
|
||||||
}
|
}
|
||||||
return request.render('payment.pay', values)
|
# TODO: remove return when payment is implemented with refactored payments, i.e. use of
|
||||||
|
# payment.payment_tokens_list in the template.
|
||||||
|
# return request.render('payment.pay', values)
|
||||||
|
return request.render('website.404')
|
||||||
|
|
||||||
@http.route(['/website_payment/transaction'], type='json', auth="public", website=True)
|
@http.route(['/website_payment/transaction'], type='json', auth="public", website=True)
|
||||||
def transaction(self, reference, amount, currency_id, acquirer_id):
|
def transaction(self, reference, amount, currency_id, acquirer_id):
|
||||||
|
|||||||
@@ -127,7 +127,7 @@ class PaymentAcquirerAuthorize(models.Model):
|
|||||||
for field_name in mandatory_fields:
|
for field_name in mandatory_fields:
|
||||||
if not data.get(field_name):
|
if not data.get(field_name):
|
||||||
error[field_name] = 'missing'
|
error[field_name] = 'missing'
|
||||||
if data['cc_expiry'] and datetime.now().strftime('%y%M') > datetime.strptime(data['cc_expiry'], '%M / %y').strftime('%y%M'):
|
if data['cc_expiry'] and datetime.now().strftime('%y%m') > datetime.strptime(data['cc_expiry'], '%m / %y').strftime('%y%m'):
|
||||||
return False
|
return False
|
||||||
return False if error else True
|
return False if error else True
|
||||||
|
|
||||||
|
|||||||
@@ -92,7 +92,7 @@ odoo.define('payment_stripe.stripe', function(require) {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
var access_token = $("input[name='access_token']").val() || $("input[name='token']").val();
|
var access_token = $("input[name='access_token']").val() || $("input[name='token']").val() || '';
|
||||||
var so_id = $("input[name='return_url']").val().match(/quote\/([0-9]+)/) || undefined;
|
var so_id = $("input[name='return_url']").val().match(/quote\/([0-9]+)/) || undefined;
|
||||||
if (so_id) {
|
if (so_id) {
|
||||||
so_id = parseInt(so_id[1]);
|
so_id = parseInt(so_id[1]);
|
||||||
|
|||||||
@@ -43,11 +43,10 @@ try:
|
|||||||
else:
|
else:
|
||||||
_logger.warning(_('Unable to format %s:\n%s') % number, e)
|
_logger.warning(_('Unable to format %s:\n%s') % number, e)
|
||||||
return number
|
return number
|
||||||
if always_international and phone_nbr.country_code != country_phone_code:
|
if always_international or phone_nbr.country_code != country_phone_code:
|
||||||
phone_fmt = phonenumbers.PhoneNumberFormat.INTERNATIONAL
|
phone_fmt = phonenumbers.PhoneNumberFormat.INTERNATIONAL
|
||||||
else:
|
else:
|
||||||
phone_fmt = phonenumbers.PhoneNumberFormat.NATIONAL
|
phone_fmt = phonenumbers.PhoneNumberFormat.NATIONAL
|
||||||
|
|
||||||
return phonenumbers.format_number(phone_nbr, phone_fmt)
|
return phonenumbers.format_number(phone_nbr, phone_fmt)
|
||||||
|
|
||||||
except ImportError:
|
except ImportError:
|
||||||
|
|||||||
@@ -1,8 +1,4 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<!--
|
|
||||||
The purpose of this view is to move the VAT field at the top of the
|
|
||||||
company form. That way, the user will fill it first
|
|
||||||
-->
|
|
||||||
<odoo>
|
<odoo>
|
||||||
<record id="res_company_view_form_phone_validation" model="ir.ui.view">
|
<record id="res_company_view_form_phone_validation" model="ir.ui.view">
|
||||||
<field name="name">res.company.view.form.phone_validation</field>
|
<field name="name">res.company.view.form.phone_validation</field>
|
||||||
|
|||||||
@@ -128,6 +128,15 @@
|
|||||||
<filter string="Archived" name="inactive" domain="[('active','=',False)]"/>
|
<filter string="Archived" name="inactive" domain="[('active','=',False)]"/>
|
||||||
<field string="Attributes" name="attribute_line_ids" groups="product.group_product_variant"/>
|
<field string="Attributes" name="attribute_line_ids" groups="product.group_product_variant"/>
|
||||||
<field name="pricelist_id" widget="selection" context="{'pricelist': self}" filter_domain="[]" groups="product.group_sale_pricelist"/> <!-- Keep widget=selection on this field to pass numeric `self` value, which is not the case for regular m2o widgets! -->
|
<field name="pricelist_id" widget="selection" context="{'pricelist': self}" filter_domain="[]" groups="product.group_sale_pricelist"/> <!-- Keep widget=selection on this field to pass numeric `self` value, which is not the case for regular m2o widgets! -->
|
||||||
|
<separator/>
|
||||||
|
<filter string="Late Activities" name="activities_overdue"
|
||||||
|
domain="[('activity_ids.date_deadline', '<', context_today().strftime('%Y-%m-%d'))]"
|
||||||
|
help="Show all records which has next action date is before today"/>
|
||||||
|
<filter string="Today Activities" name="activities_today"
|
||||||
|
domain="[('activity_ids.date_deadline', '=', context_today().strftime('%Y-%m-%d'))]"/>
|
||||||
|
<filter string="Future Activities" name="activities_upcoming_all"
|
||||||
|
domain="[('activity_ids.date_deadline', '>', context_today().strftime('%Y-%m-%d'))
|
||||||
|
]"/>
|
||||||
</search>
|
</search>
|
||||||
</field>
|
</field>
|
||||||
</record>
|
</record>
|
||||||
|
|||||||
@@ -44,7 +44,7 @@ class StockProductionLot(models.Model):
|
|||||||
# Assign dates according to products data
|
# Assign dates according to products data
|
||||||
@api.model
|
@api.model
|
||||||
def create(self, vals):
|
def create(self, vals):
|
||||||
dates = self._get_dates(vals.get('product_id'))
|
dates = self._get_dates(vals.get('product_id') or self.env.context.get('default_product_id'))
|
||||||
for d in dates:
|
for d in dates:
|
||||||
if not vals.get(d):
|
if not vals.get(d):
|
||||||
vals[d] = dates[d]
|
vals[d] = dates[d]
|
||||||
|
|||||||
@@ -512,19 +512,19 @@ class Task(models.Model):
|
|||||||
partner_id = fields.Many2one('res.partner',
|
partner_id = fields.Many2one('res.partner',
|
||||||
string='Customer',
|
string='Customer',
|
||||||
default=_get_default_partner)
|
default=_get_default_partner)
|
||||||
manager_id = fields.Many2one('res.users', string='Project Manager', related='project_id.user_id', readonly=True)
|
manager_id = fields.Many2one('res.users', string='Project Manager', related='project_id.user_id', readonly=True, related_sudo=False)
|
||||||
company_id = fields.Many2one('res.company',
|
company_id = fields.Many2one('res.company',
|
||||||
string='Company',
|
string='Company',
|
||||||
default=lambda self: self.env['res.company']._company_default_get())
|
default=lambda self: self.env['res.company']._company_default_get())
|
||||||
color = fields.Integer(string='Color Index')
|
color = fields.Integer(string='Color Index')
|
||||||
user_email = fields.Char(related='user_id.email', string='User Email', readonly=True)
|
user_email = fields.Char(related='user_id.email', string='User Email', readonly=True, related_sudo=False)
|
||||||
attachment_ids = fields.One2many('ir.attachment', compute='_compute_attachment_ids', string="Main Attachments",
|
attachment_ids = fields.One2many('ir.attachment', compute='_compute_attachment_ids', string="Main Attachments",
|
||||||
help="Attachment that don't come from message.")
|
help="Attachment that don't come from message.")
|
||||||
# In the domain of displayed_image_id, we couln't use attachment_ids because a one2many is represented as a list of commands so we used res_model & res_id
|
# In the domain of displayed_image_id, we couln't use attachment_ids because a one2many is represented as a list of commands so we used res_model & res_id
|
||||||
displayed_image_id = fields.Many2one('ir.attachment', domain="[('res_model', '=', 'project.task'), ('res_id', '=', id), ('mimetype', 'ilike', 'image')]", string='Cover Image')
|
displayed_image_id = fields.Many2one('ir.attachment', domain="[('res_model', '=', 'project.task'), ('res_id', '=', id), ('mimetype', 'ilike', 'image')]", string='Cover Image')
|
||||||
legend_blocked = fields.Char(related='stage_id.legend_blocked', string='Kanban Blocked Explanation', readonly=True)
|
legend_blocked = fields.Char(related='stage_id.legend_blocked', string='Kanban Blocked Explanation', readonly=True, related_sudo=False)
|
||||||
legend_done = fields.Char(related='stage_id.legend_done', string='Kanban Valid Explanation', readonly=True)
|
legend_done = fields.Char(related='stage_id.legend_done', string='Kanban Valid Explanation', readonly=True, related_sudo=False)
|
||||||
legend_normal = fields.Char(related='stage_id.legend_normal', string='Kanban Ongoing Explanation', readonly=True)
|
legend_normal = fields.Char(related='stage_id.legend_normal', string='Kanban Ongoing Explanation', readonly=True, related_sudo=False)
|
||||||
parent_id = fields.Many2one('project.task', string='Parent Task')
|
parent_id = fields.Many2one('project.task', string='Parent Task')
|
||||||
child_ids = fields.One2many('project.task', 'parent_id', string="Sub-tasks")
|
child_ids = fields.One2many('project.task', 'parent_id', string="Sub-tasks")
|
||||||
subtask_project_id = fields.Many2one('project.project', related="project_id.subtask_project_id", string='Sub-task Project', readonly=True)
|
subtask_project_id = fields.Many2one('project.project', related="project_id.subtask_project_id", string='Sub-task Project', readonly=True)
|
||||||
|
|||||||
@@ -60,7 +60,7 @@ class Holidays(models.Model):
|
|||||||
work_hours_data = [item for item in holiday.employee_id.iter_work_hours_count(fields.Datetime.from_string(holiday.date_from), fields.Datetime.from_string(holiday.date_to))]
|
work_hours_data = [item for item in holiday.employee_id.iter_work_hours_count(fields.Datetime.from_string(holiday.date_from), fields.Datetime.from_string(holiday.date_to))]
|
||||||
for index, (day_date, work_hours_count) in enumerate(work_hours_data):
|
for index, (day_date, work_hours_count) in enumerate(work_hours_data):
|
||||||
self.env['account.analytic.line'].create({
|
self.env['account.analytic.line'].create({
|
||||||
'name': "%s (%s/%s)" % (holiday.name, index + 1, len(work_hours_data)),
|
'name': "%s (%s/%s)" % (holiday.name or '', index + 1, len(work_hours_data)),
|
||||||
'project_id': holiday_project.id,
|
'project_id': holiday_project.id,
|
||||||
'task_id': holiday_task.id,
|
'task_id': holiday_task.id,
|
||||||
'account_id': holiday_project.analytic_account_id.id,
|
'account_id': holiday_project.analytic_account_id.id,
|
||||||
@@ -77,6 +77,7 @@ class Holidays(models.Model):
|
|||||||
def action_refuse(self):
|
def action_refuse(self):
|
||||||
""" Remove the timesheets linked to the refused holidays """
|
""" Remove the timesheets linked to the refused holidays """
|
||||||
result = super(Holidays, self).action_refuse()
|
result = super(Holidays, self).action_refuse()
|
||||||
self.mapped('timesheet_ids').write({'holiday_id': False})
|
timesheets = self.sudo().mapped('timesheet_ids')
|
||||||
self.mapped('timesheet_ids').unlink()
|
timesheets.write({'holiday_id': False})
|
||||||
|
timesheets.unlink()
|
||||||
return result
|
return result
|
||||||
|
|||||||
@@ -98,7 +98,9 @@ class AccountInvoice(models.Model):
|
|||||||
def _onchange_currency_id(self):
|
def _onchange_currency_id(self):
|
||||||
if self.currency_id:
|
if self.currency_id:
|
||||||
for line in self.invoice_line_ids.filtered(lambda r: r.purchase_line_id):
|
for line in self.invoice_line_ids.filtered(lambda r: r.purchase_line_id):
|
||||||
line.price_unit = line.purchase_id.currency_id.with_context(date=self.date_invoice).compute(line.purchase_line_id.price_unit, self.currency_id, round=False)
|
if line.purchase_id.currency_id != self.currency_id:
|
||||||
|
line.price_unit = line.purchase_id.currency_id.with_context(date=self.date_invoice)\
|
||||||
|
.compute(line.purchase_line_id.price_unit, self.currency_id, round=False)
|
||||||
|
|
||||||
@api.onchange('invoice_line_ids')
|
@api.onchange('invoice_line_ids')
|
||||||
def _onchange_origin(self):
|
def _onchange_origin(self):
|
||||||
|
|||||||
@@ -776,10 +776,10 @@ class PurchaseOrderLine(models.Model):
|
|||||||
self.product_uom = self.product_id.uom_po_id or self.product_id.uom_id
|
self.product_uom = self.product_id.uom_po_id or self.product_id.uom_id
|
||||||
result['domain'] = {'product_uom': [('category_id', '=', self.product_id.uom_id.category_id.id)]}
|
result['domain'] = {'product_uom': [('category_id', '=', self.product_id.uom_id.category_id.id)]}
|
||||||
|
|
||||||
product_lang = self.product_id.with_context({
|
product_lang = self.product_id.with_context(
|
||||||
'lang': self.partner_id.lang,
|
lang=self.partner_id.lang,
|
||||||
'partner_id': self.partner_id.id,
|
partner_id=self.partner_id.id,
|
||||||
})
|
)
|
||||||
self.name = product_lang.display_name
|
self.name = product_lang.display_name
|
||||||
if product_lang.description_purchase:
|
if product_lang.description_purchase:
|
||||||
self.name += '\n' + product_lang.description_purchase
|
self.name += '\n' + product_lang.description_purchase
|
||||||
|
|||||||
@@ -61,7 +61,7 @@
|
|||||||
<record id="portal_purchase_order_user_rule" model="ir.rule">
|
<record id="portal_purchase_order_user_rule" model="ir.rule">
|
||||||
<field name="name">Portal Purchase Orders</field>
|
<field name="name">Portal Purchase Orders</field>
|
||||||
<field name="model_id" ref="purchase.model_purchase_order"/>
|
<field name="model_id" ref="purchase.model_purchase_order"/>
|
||||||
<field name="domain_force">[('message_partner_ids','child_of',[user.commercial_partner_id.id])]</field>
|
<field name="domain_force">['|', ('message_partner_ids','child_of',[user.commercial_partner_id.id]),('partner_id', 'child_of', [user.commercial_partner_id.id])]</field>
|
||||||
<field name="groups" eval="[(4, ref('base.group_portal'))]"/>
|
<field name="groups" eval="[(4, ref('base.group_portal'))]"/>
|
||||||
<field name="perm_unlink" eval="1"/>
|
<field name="perm_unlink" eval="1"/>
|
||||||
<field name="perm_write" eval="1"/>
|
<field name="perm_write" eval="1"/>
|
||||||
@@ -72,7 +72,7 @@
|
|||||||
<record id="portal_purchase_order_line_rule" model="ir.rule">
|
<record id="portal_purchase_order_line_rule" model="ir.rule">
|
||||||
<field name="name">Portal Purhcase Orders Line</field>
|
<field name="name">Portal Purhcase Orders Line</field>
|
||||||
<field name="model_id" ref="purchase.model_purchase_order_line"/>
|
<field name="model_id" ref="purchase.model_purchase_order_line"/>
|
||||||
<field name="domain_force">[('order_id.message_partner_ids','child_of',[user.commercial_partner_id.id])]</field>
|
<field name="domain_force">['|',('order_id.message_partner_ids','child_of',[user.commercial_partner_id.id]),('order_id.partner_id','child_of',[user.commercial_partner_id.id])]</field>
|
||||||
<field name="groups" eval="[(4, ref('base.group_portal'))]"/>
|
<field name="groups" eval="[(4, ref('base.group_portal'))]"/>
|
||||||
</record>
|
</record>
|
||||||
</data>
|
</data>
|
||||||
|
|||||||
@@ -128,11 +128,13 @@ class AccountAnalyticLine(models.Model):
|
|||||||
raise UserError(_('The Sales Order %s linked to the Analytic Account must be validated before registering expenses.') % sale_order.name)
|
raise UserError(_('The Sales Order %s linked to the Analytic Account must be validated before registering expenses.') % sale_order.name)
|
||||||
|
|
||||||
price = analytic_line._sale_get_invoice_price(sale_order)
|
price = analytic_line._sale_get_invoice_price(sale_order)
|
||||||
so_line = self.env['sale.order.line'].search([
|
so_line = None
|
||||||
('order_id', '=', sale_order.id),
|
if analytic_line.product_id.expense_policy == 'sales_price' and analytic_line.product_id.invoice_policy == 'delivery':
|
||||||
('price_unit', '=', price),
|
so_line = self.env['sale.order.line'].search([
|
||||||
('product_id', '=', self.product_id.id)
|
('order_id', '=', sale_order.id),
|
||||||
], limit=1)
|
('price_unit', '=', price),
|
||||||
|
('product_id', '=', self.product_id.id)
|
||||||
|
], limit=1)
|
||||||
|
|
||||||
if not so_line:
|
if not so_line:
|
||||||
# generate a new SO line
|
# generate a new SO line
|
||||||
@@ -142,7 +144,7 @@ class AccountAnalyticLine(models.Model):
|
|||||||
so_line = self.env['sale.order.line'].create(so_line_values)
|
so_line = self.env['sale.order.line'].create(so_line_values)
|
||||||
so_line._compute_tax_id()
|
so_line._compute_tax_id()
|
||||||
else:
|
else:
|
||||||
so_line.write({'qty_delivered': analytic_line.unit_amount})
|
so_line.write({'qty_delivered': so_line.qty_delivered + analytic_line.unit_amount})
|
||||||
|
|
||||||
if so_line: # if so line found or created, then update AAL (this will trigger the recomputation of qty delivered on SO line)
|
if so_line: # if so line found or created, then update AAL (this will trigger the recomputation of qty delivered on SO line)
|
||||||
analytic_line.with_context(sale_analytic_norecompute=True).write({'so_line': so_line.id})
|
analytic_line.with_context(sale_analytic_norecompute=True).write({'so_line': so_line.id})
|
||||||
|
|||||||
@@ -150,7 +150,10 @@
|
|||||||
<t t-foreach="doc._get_tax_amount_by_group()" t-as="amount_by_group">
|
<t t-foreach="doc._get_tax_amount_by_group()" t-as="amount_by_group">
|
||||||
<tr style="border-bottom:1px solid #dddddd;">
|
<tr style="border-bottom:1px solid #dddddd;">
|
||||||
<t t-if="amount_by_group[3] == 1 and doc.amount_untaxed == amount_by_group[2]">
|
<t t-if="amount_by_group[3] == 1 and doc.amount_untaxed == amount_by_group[2]">
|
||||||
<td><span t-esc="amount_by_group[0]"/></td>
|
<td>
|
||||||
|
<span t-esc="amount_by_group[0]"/>
|
||||||
|
<span>&nbsp;<span>on</span>&nbsp;<t t-esc="amount_by_group[2]" t-options='{"widget": "monetary", "display_currency": doc.pricelist_id.currency_id}'/></span>
|
||||||
|
</td>
|
||||||
<td class="text-right">
|
<td class="text-right">
|
||||||
<span t-esc="amount_by_group[1]"
|
<span t-esc="amount_by_group[1]"
|
||||||
t-options='{"widget": "monetary", "display_currency": doc.pricelist_id.currency_id}'/>
|
t-options='{"widget": "monetary", "display_currency": doc.pricelist_id.currency_id}'/>
|
||||||
@@ -158,7 +161,7 @@
|
|||||||
</t>
|
</t>
|
||||||
<t t-else ="">
|
<t t-else ="">
|
||||||
<td>
|
<td>
|
||||||
<span t-esc="amount_by_group[0]"/><span>&nbsp;<span>on</span>&nbsp;<t t-esc="amount_by_group[2]" t-options='{"widget": "monetary", "display_currency": doc.pricelist_id.currency_id}'/></span>
|
<span t-esc="amount_by_group[0]"/>
|
||||||
</td>
|
</td>
|
||||||
<td class="text-right">
|
<td class="text-right">
|
||||||
<span t-esc="amount_by_group[1]"
|
<span t-esc="amount_by_group[1]"
|
||||||
|
|||||||
@@ -9,6 +9,16 @@ from odoo.addons.iap.models import iap
|
|||||||
|
|
||||||
_logger = logging.getLogger(__name__)
|
_logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
try:
|
||||||
|
import phonenumbers
|
||||||
|
_sms_phonenumbers_lib_imported = True
|
||||||
|
|
||||||
|
except ImportError:
|
||||||
|
_sms_phonenumbers_lib_imported = False
|
||||||
|
_logger.warning(
|
||||||
|
"The `phonenumbers` Python module is not installed. Try: pip install phonenumbers."
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class SendSMS(models.TransientModel):
|
class SendSMS(models.TransientModel):
|
||||||
_name = 'sms.send_sms'
|
_name = 'sms.send_sms'
|
||||||
@@ -16,6 +26,27 @@ class SendSMS(models.TransientModel):
|
|||||||
recipients = fields.Char('Recipients', required=True)
|
recipients = fields.Char('Recipients', required=True)
|
||||||
message = fields.Text('Message', required=True)
|
message = fields.Text('Message', required=True)
|
||||||
|
|
||||||
|
def _phone_get_country(self, partner):
|
||||||
|
if 'country_id' in partner:
|
||||||
|
return partner.country_id
|
||||||
|
return self.env.user.company_id.country_id
|
||||||
|
|
||||||
|
def _sms_sanitization(self, partner, field_name):
|
||||||
|
number = partner[field_name]
|
||||||
|
if _sms_phonenumbers_lib_imported:
|
||||||
|
country = self._phone_get_country(partner)
|
||||||
|
country_code = country.code if country else None
|
||||||
|
try:
|
||||||
|
phone_nbr = phonenumbers.parse(number, region=country_code, keep_raw_input=True)
|
||||||
|
except phonenumbers.phonenumberutil.NumberParseException:
|
||||||
|
return number
|
||||||
|
if not phonenumbers.is_possible_number(phone_nbr) or not phonenumbers.is_valid_number(phone_nbr):
|
||||||
|
return number
|
||||||
|
phone_fmt = phonenumbers.PhoneNumberFormat.INTERNATIONAL
|
||||||
|
return phonenumbers.format_number(phone_nbr, phone_fmt).replace(' ', '')
|
||||||
|
else:
|
||||||
|
return number
|
||||||
|
|
||||||
def _get_records(self, model):
|
def _get_records(self, model):
|
||||||
if self.env.context.get('active_domain'):
|
if self.env.context.get('active_domain'):
|
||||||
records = model.search(self.env.context.get('active_domain'))
|
records = model.search(self.env.context.get('active_domain'))
|
||||||
@@ -38,7 +69,8 @@ class SendSMS(models.TransientModel):
|
|||||||
phone_numbers = []
|
phone_numbers = []
|
||||||
no_phone_partners = []
|
no_phone_partners = []
|
||||||
for partner in partners:
|
for partner in partners:
|
||||||
number = partner.mobile
|
default_field_name = 'mobile' if partner.mobile else 'phone'
|
||||||
|
number = self._sms_sanitization(partner, self.env.context('field_name') or default_field_name)
|
||||||
if number:
|
if number:
|
||||||
phone_numbers.append(number)
|
phone_numbers.append(number)
|
||||||
else:
|
else:
|
||||||
|
|||||||
@@ -183,10 +183,6 @@ class Route(models.Model):
|
|||||||
'domain': [('route_ids', 'in', self.ids)],
|
'domain': [('route_ids', 'in', self.ids)],
|
||||||
}
|
}
|
||||||
|
|
||||||
@api.onchange('warehouse_selectable')
|
|
||||||
def _onchange_warehouse_selectable(self):
|
|
||||||
if not self.warehouse_selectable:
|
|
||||||
self.warehouse_ids = [(5, 0, 0)]
|
|
||||||
|
|
||||||
class PushedFlow(models.Model):
|
class PushedFlow(models.Model):
|
||||||
_name = "stock.location.path"
|
_name = "stock.location.path"
|
||||||
|
|||||||
@@ -1000,8 +1000,16 @@ class StockMove(models.Model):
|
|||||||
|
|
||||||
moves = self.filtered(lambda x: x.state not in ('done', 'cancel'))
|
moves = self.filtered(lambda x: x.state not in ('done', 'cancel'))
|
||||||
moves_todo = self.env['stock.move']
|
moves_todo = self.env['stock.move']
|
||||||
# Create extra moves where necessary
|
|
||||||
|
# Cancel moves where necessary ; we should do it before creating the extra moves because
|
||||||
|
# this operation could trigger a merge of moves.
|
||||||
for move in moves:
|
for move in moves:
|
||||||
|
if move.quantity_done <= 0:
|
||||||
|
if float_compare(move.product_uom_qty, 0.0, precision_rounding=move.product_uom.rounding) == 0:
|
||||||
|
move._action_cancel()
|
||||||
|
|
||||||
|
# Create extra moves where necessary
|
||||||
|
for move in moves.filtered(lambda m: m.state != 'cancel'):
|
||||||
# Here, the `quantity_done` was already rounded to the product UOM by the `do_produce` wizard. However,
|
# Here, the `quantity_done` was already rounded to the product UOM by the `do_produce` wizard. However,
|
||||||
# it is possible that the user changed the value before posting the inventory by a value that should be
|
# it is possible that the user changed the value before posting the inventory by a value that should be
|
||||||
# rounded according to the move's UOM. In this specific case, we chose to round up the value, because it
|
# rounded according to the move's UOM. In this specific case, we chose to round up the value, because it
|
||||||
@@ -1010,10 +1018,6 @@ class StockMove(models.Model):
|
|||||||
# FIXME: move rounding to move line
|
# FIXME: move rounding to move line
|
||||||
# rounding = move.product_uom.rounding
|
# rounding = move.product_uom.rounding
|
||||||
# move.quantity_done = float_round(move.quantity_done, precision_rounding=rounding, rounding_method ='UP')
|
# move.quantity_done = float_round(move.quantity_done, precision_rounding=rounding, rounding_method ='UP')
|
||||||
if move.quantity_done <= 0:
|
|
||||||
if float_compare(move.product_uom_qty, 0.0, precision_rounding=move.product_uom.rounding) == 0:
|
|
||||||
move._action_cancel()
|
|
||||||
continue
|
|
||||||
moves_todo |= move
|
moves_todo |= move
|
||||||
moves_todo |= move._create_extra_move()
|
moves_todo |= move._create_extra_move()
|
||||||
# Split moves where necessary and move quants
|
# Split moves where necessary and move quants
|
||||||
|
|||||||
@@ -109,36 +109,49 @@ class StockMoveLine(models.Model):
|
|||||||
|
|
||||||
@api.onchange('lot_name', 'lot_id')
|
@api.onchange('lot_name', 'lot_id')
|
||||||
def onchange_serial_number(self):
|
def onchange_serial_number(self):
|
||||||
|
""" When the user is encoding a move line for a tracked product, we apply some logic to
|
||||||
|
help him. This includes:
|
||||||
|
- automatically switch `qty_done` to 1.0
|
||||||
|
- warn if he has already encoded `lot_name` in another move line
|
||||||
|
"""
|
||||||
res = {}
|
res = {}
|
||||||
if self.product_id.tracking == 'serial':
|
if self.product_id.tracking == 'serial':
|
||||||
self.qty_done = 1
|
if not self.qty_done:
|
||||||
move_lines_to_check = self._get_similar_move_lines() - self
|
self.qty_done = 1
|
||||||
message = move_lines_to_check._check_for_duplicated_serial_numbers()
|
|
||||||
|
message = None
|
||||||
|
if self.lot_name or self.lot_id:
|
||||||
|
move_lines_to_check = self._get_similar_move_lines() - self
|
||||||
|
if self.lot_name:
|
||||||
|
counter = Counter(move_lines_to_check.mapped('lot_name'))
|
||||||
|
if counter.get(self.lot_name) and counter[self.lot_name] > 1:
|
||||||
|
message = _('You cannot use the same serial number twice. Please correct the serial numbers encoded.')
|
||||||
|
elif self.lot_id:
|
||||||
|
counter = Counter(move_lines_to_check.mapped('lot_id.id'))
|
||||||
|
if counter.get(self.lot_id.id) and counter[self.lot_id.id] > 1:
|
||||||
|
message = _('You cannot use the same serial number twice. Please correct the serial numbers encoded.')
|
||||||
|
|
||||||
if message:
|
if message:
|
||||||
res['warning'] = {'title': _('Warning'), 'message': message}
|
res['warning'] = {'title': _('Warning'), 'message': message}
|
||||||
return res
|
return res
|
||||||
|
|
||||||
|
@api.onchange('qty_done')
|
||||||
|
def _onchange_qty_done(self):
|
||||||
|
""" When the user is encoding a move line for a tracked product, we apply some logic to
|
||||||
|
help him. This onchange will warn him if he set `qty_done` to a non-supported value.
|
||||||
|
"""
|
||||||
|
res = {}
|
||||||
|
if self.product_id.tracking == 'serial':
|
||||||
|
if float_compare(self.qty_done, 1.0, precision_rounding=self.move_id.product_id.uom_id.rounding) != 0:
|
||||||
|
message = _('You can only process 1.0 %s for products with unique serial number.') % self.product_id.uom_id.name
|
||||||
|
res['warning'] = {'title': _('Warning'), 'message': message}
|
||||||
|
return res
|
||||||
|
|
||||||
@api.constrains('qty_done')
|
@api.constrains('qty_done')
|
||||||
def _check_positive_qty_done(self):
|
def _check_positive_qty_done(self):
|
||||||
if any([ml.qty_done < 0 for ml in self]):
|
if any([ml.qty_done < 0 for ml in self]):
|
||||||
raise ValidationError(_('You can not enter negative quantities!'))
|
raise ValidationError(_('You can not enter negative quantities!'))
|
||||||
|
|
||||||
@api.constrains('lot_id', 'lot_name', 'qty_done')
|
|
||||||
def _check_unique_serial_number(self):
|
|
||||||
for ml in self.filtered(lambda ml: ml.move_id.product_id.tracking == 'serial' and (ml.lot_id or ml.lot_name)):
|
|
||||||
move_lines_to_check = ml._get_similar_move_lines()
|
|
||||||
message = move_lines_to_check._check_for_duplicated_serial_numbers()
|
|
||||||
if message:
|
|
||||||
raise ValidationError(message)
|
|
||||||
if float_compare(ml.qty_done, 1.0, precision_rounding=ml.move_id.product_id.uom_id.rounding) == 1:
|
|
||||||
raise UserError(_(
|
|
||||||
'You can only process 1.0 %s for products with unique serial number.') % ml.product_id.uom_id.name)
|
|
||||||
if ml.lot_name:
|
|
||||||
already_exist = self.env['stock.production.lot'].search(
|
|
||||||
[('name', '=', ml.lot_name), ('product_id', '=', ml.product_id.id)])
|
|
||||||
if already_exist:
|
|
||||||
return _('You have already assigned this serial number to this product. Please correct the serial numbers encoded.')
|
|
||||||
|
|
||||||
def _get_similar_move_lines(self):
|
def _get_similar_move_lines(self):
|
||||||
self.ensure_one()
|
self.ensure_one()
|
||||||
lines = self.env['stock.move.line']
|
lines = self.env['stock.move.line']
|
||||||
@@ -147,25 +160,6 @@ class StockMoveLine(models.Model):
|
|||||||
lines |= picking_id.move_line_ids.filtered(lambda ml: ml.product_id == self.product_id and (ml.lot_id or ml.lot_name))
|
lines |= picking_id.move_line_ids.filtered(lambda ml: ml.product_id == self.product_id and (ml.lot_id or ml.lot_name))
|
||||||
return lines
|
return lines
|
||||||
|
|
||||||
def _check_for_duplicated_serial_numbers(self):
|
|
||||||
""" This method is used in _check_unique_serial_number and in onchange_serial_number to check that a same serial number is not used twice amongst the recordset passed.
|
|
||||||
|
|
||||||
:return: an error message directed to the user if needed else False
|
|
||||||
"""
|
|
||||||
if self.mapped('lot_id'):
|
|
||||||
lots_map = [(ml.product_id.id, ml.lot_id.name) for ml in self]
|
|
||||||
recorded_serials_counter = Counter(lots_map)
|
|
||||||
for (product_id, lot_id), occurrences in recorded_serials_counter.items():
|
|
||||||
if occurrences > 1 and lot_id is not False:
|
|
||||||
return _('You cannot use the same serial number twice. Please correct the serial numbers encoded.')
|
|
||||||
elif self.mapped('lot_name'):
|
|
||||||
lots_map = [(ml.product_id.id, ml.lot_name) for ml in self]
|
|
||||||
recorded_serials_counter = Counter(lots_map)
|
|
||||||
for (product_id, lot_id), occurrences in recorded_serials_counter.items():
|
|
||||||
if occurrences > 1 and lot_id is not False:
|
|
||||||
return _('You cannot use the same serial number twice. Please correct the serial numbers encoded.')
|
|
||||||
return False
|
|
||||||
|
|
||||||
@api.model
|
@api.model
|
||||||
def create(self, vals):
|
def create(self, vals):
|
||||||
vals['ordered_qty'] = vals.get('product_uom_qty')
|
vals['ordered_qty'] = vals.get('product_uom_qty')
|
||||||
|
|||||||
@@ -246,7 +246,7 @@
|
|||||||
</group>
|
</group>
|
||||||
<group>
|
<group>
|
||||||
<field name="scheduled_date"/>
|
<field name="scheduled_date"/>
|
||||||
<field name="origin" placeholder="e.g. PO0032" readonly="1"/>
|
<field name="origin" placeholder="e.g. PO0032"/>
|
||||||
<field name="owner_id" groups="stock.group_tracking_owner"/>
|
<field name="owner_id" groups="stock.group_tracking_owner"/>
|
||||||
<div groups="stock.group_tracking_owner" colspan="2" col="2">
|
<div groups="stock.group_tracking_owner" colspan="2" col="2">
|
||||||
<button name="action_assign_owner" string="Assign Owner" type="object" attrs="{'invisible': ['|',('move_line_exist', '=', False),('state', 'not in', ('draft','assigned','confirmed'))]}"
|
<button name="action_assign_owner" string="Assign Owner" type="object" attrs="{'invisible': ['|',('move_line_exist', '=', False),('state', 'not in', ('draft','assigned','confirmed'))]}"
|
||||||
|
|||||||
@@ -171,9 +171,13 @@ class LandedCost(models.Model):
|
|||||||
val_line_values.update({'cost_id': cost.id, 'cost_line_id': cost_line.id})
|
val_line_values.update({'cost_id': cost.id, 'cost_line_id': cost_line.id})
|
||||||
self.env['stock.valuation.adjustment.lines'].create(val_line_values)
|
self.env['stock.valuation.adjustment.lines'].create(val_line_values)
|
||||||
total_qty += val_line_values.get('quantity', 0.0)
|
total_qty += val_line_values.get('quantity', 0.0)
|
||||||
total_cost += val_line_values.get('former_cost', 0.0)
|
|
||||||
total_weight += val_line_values.get('weight', 0.0)
|
total_weight += val_line_values.get('weight', 0.0)
|
||||||
total_volume += val_line_values.get('volume', 0.0)
|
total_volume += val_line_values.get('volume', 0.0)
|
||||||
|
|
||||||
|
former_cost = val_line_values.get('former_cost', 0.0)
|
||||||
|
# round this because former_cost on the valuation lines is also rounded
|
||||||
|
total_cost += tools.float_round(former_cost, precision_digits=digits[1]) if digits else former_cost
|
||||||
|
|
||||||
total_line += 1
|
total_line += 1
|
||||||
|
|
||||||
for line in cost.cost_lines:
|
for line in cost.cost_lines:
|
||||||
|
|||||||
@@ -8,7 +8,7 @@
|
|||||||
<field name="arch" type="xml">
|
<field name="arch" type="xml">
|
||||||
<form string="Add pickings to batch">
|
<form string="Add pickings to batch">
|
||||||
<separator string="Select a batch"/>
|
<separator string="Select a batch"/>
|
||||||
<field name="batch_id" class="oe_inline"/>
|
<field name="batch_id" class="oe_inline" options="{'no_create_edit': True, 'no_open': True}"/>
|
||||||
<footer>
|
<footer>
|
||||||
<button name="attach_pickings" type="object" string="Add to Batch" class="btn-primary"/>
|
<button name="attach_pickings" type="object" string="Add to Batch" class="btn-primary"/>
|
||||||
<button string="Cancel" class="btn-default" special="cancel"/>
|
<button string="Cancel" class="btn-default" special="cancel"/>
|
||||||
|
|||||||
@@ -274,3 +274,71 @@ class TestMessageAccess(common.BaseFunctionalTest, common.MockEmails):
|
|||||||
def test_mail_message_access_create_reply(self):
|
def test_mail_message_access_create_reply(self):
|
||||||
self.message.write({'partner_ids': [(4, self.user_employee.partner_id.id)]})
|
self.message.write({'partner_ids': [(4, self.user_employee.partner_id.id)]})
|
||||||
self.env['mail.message'].sudo(self.user_employee).create({'model': 'mail.channel', 'res_id': self.group_private.id, 'body': 'Test', 'parent_id': self.message.id})
|
self.env['mail.message'].sudo(self.user_employee).create({'model': 'mail.channel', 'res_id': self.group_private.id, 'body': 'Test', 'parent_id': self.message.id})
|
||||||
|
|
||||||
|
|
||||||
|
@mute_logger('openerp.addons.mail.models.mail_mail')
|
||||||
|
def test_mark_all_as_read(self):
|
||||||
|
self.user_employee.notification_type = 'inbox'
|
||||||
|
emp_partner = self.user_employee.partner_id.sudo(self.user_employee.id)
|
||||||
|
|
||||||
|
group_private = self.env['mail.channel'].with_context({
|
||||||
|
'mail_create_nolog': True,
|
||||||
|
'mail_create_nosubscribe': True,
|
||||||
|
'mail_channel_noautofollow': True,
|
||||||
|
}).create({
|
||||||
|
'name': 'Private',
|
||||||
|
'description': 'Private James R.',
|
||||||
|
'public': 'private',
|
||||||
|
'alias_name': 'private',
|
||||||
|
'alias_contact': 'followers'}
|
||||||
|
).with_context({'mail_create_nosubscribe': False})
|
||||||
|
|
||||||
|
# mark all as read clear needactions
|
||||||
|
group_private.message_post(body='Test', message_type='comment', subtype='mail.mt_comment', partner_ids=[emp_partner.id])
|
||||||
|
emp_partner.env['mail.message'].mark_all_as_read(channel_ids=[], domain=[])
|
||||||
|
na_count = emp_partner.get_needaction_count()
|
||||||
|
self.assertEqual(na_count, 0, "mark all as read should conclude all needactions")
|
||||||
|
|
||||||
|
# mark all as read also clear inaccessible needactions
|
||||||
|
new_msg = group_private.message_post(body='Zest', message_type='comment', subtype='mail.mt_comment', partner_ids=[emp_partner.id])
|
||||||
|
needaction_accessible = len(emp_partner.env['mail.message'].search([['needaction', '=', True]]))
|
||||||
|
self.assertEqual(needaction_accessible, 1, "a new message to a partner is readable to that partner")
|
||||||
|
|
||||||
|
new_msg.sudo().partner_ids = self.env['res.partner']
|
||||||
|
emp_partner.env['mail.message'].search([['needaction', '=', True]])
|
||||||
|
needaction_length = len(emp_partner.env['mail.message'].search([['needaction', '=', True]]))
|
||||||
|
self.assertEqual(needaction_length, 1, "message should still be readable when notified")
|
||||||
|
|
||||||
|
na_count = emp_partner.get_needaction_count()
|
||||||
|
self.assertEqual(na_count, 1, "message not accessible is currently still counted")
|
||||||
|
|
||||||
|
emp_partner.env['mail.message'].mark_all_as_read(channel_ids=[], domain=[])
|
||||||
|
na_count = emp_partner.get_needaction_count()
|
||||||
|
self.assertEqual(na_count, 0, "mark all read should conclude all needactions even inacessible ones")
|
||||||
|
|
||||||
|
@mute_logger('openerp.addons.mail.models.mail_mail')
|
||||||
|
def test_mark_all_as_read_share(self):
|
||||||
|
self.user_portal.notification_type = 'inbox'
|
||||||
|
portal_partner = self.user_portal.partner_id.sudo(self.user_portal.id)
|
||||||
|
|
||||||
|
# mark all as read clear needactions
|
||||||
|
self.group_pigs.message_post(body='Test', message_type='comment', subtype='mail.mt_comment', partner_ids=[portal_partner.id])
|
||||||
|
portal_partner.env['mail.message'].mark_all_as_read(channel_ids=[], domain=[])
|
||||||
|
na_count = portal_partner.get_needaction_count()
|
||||||
|
self.assertEqual(na_count, 0, "mark all as read should conclude all needactions")
|
||||||
|
|
||||||
|
# mark all as read also clear inaccessible needactions
|
||||||
|
new_msg = self.group_pigs.message_post(body='Zest', message_type='comment', subtype='mail.mt_comment', partner_ids=[portal_partner.id])
|
||||||
|
needaction_accessible = len(portal_partner.env['mail.message'].search([['needaction', '=', True]]))
|
||||||
|
self.assertEqual(needaction_accessible, 1, "a new message to a partner is readable to that partner")
|
||||||
|
|
||||||
|
new_msg.sudo().partner_ids = self.env['res.partner']
|
||||||
|
needaction_length = len(portal_partner.env['mail.message'].search([['needaction', '=', True]]))
|
||||||
|
self.assertEqual(needaction_length, 1, "message should still be readable when notified")
|
||||||
|
|
||||||
|
na_count = portal_partner.get_needaction_count()
|
||||||
|
self.assertEqual(na_count, 1, "message not accessible is currently still counted")
|
||||||
|
|
||||||
|
portal_partner.env['mail.message'].mark_all_as_read(channel_ids=[], domain=[])
|
||||||
|
na_count = portal_partner.get_needaction_count()
|
||||||
|
self.assertEqual(na_count, 0, "mark all read should conclude all needactions even inacessible ones")
|
||||||
@@ -44,11 +44,6 @@ var AbstractField = Widget.extend({
|
|||||||
navigation_move: '_onNavigationMove',
|
navigation_move: '_onNavigationMove',
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
|
||||||
* fields can extend the context, e.g. binary fields add {bin_size: true}
|
|
||||||
*/
|
|
||||||
context: null,
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* An object representing fields to be fetched by the model eventhough not present in the view
|
* An object representing fields to be fetched by the model eventhough not present in the view
|
||||||
* This object contains "field name" as key and an object as value.
|
* This object contains "field name" as key and an object as value.
|
||||||
|
|||||||
@@ -1094,7 +1094,6 @@ var AbstractFieldBinary = AbstractField.extend({
|
|||||||
},
|
},
|
||||||
'click .o_clear_file_button': 'on_clear',
|
'click .o_clear_file_button': 'on_clear',
|
||||||
}),
|
}),
|
||||||
context: { bin_size: true },
|
|
||||||
init: function (parent, name, record) {
|
init: function (parent, name, record) {
|
||||||
this._super.apply(this, arguments);
|
this._super.apply(this, arguments);
|
||||||
this.fields = record.fields;
|
this.fields = record.fields;
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ var _t = core._t;
|
|||||||
/**
|
/**
|
||||||
* Convert binary to bin_size
|
* Convert binary to bin_size
|
||||||
*
|
*
|
||||||
* @param {string} value base64 representation of the binary (might be already a bin_size!)
|
* @param {string} [value] base64 representation of the binary (might be already a bin_size!)
|
||||||
* @param {Object} [field]
|
* @param {Object} [field]
|
||||||
* a description of the field (note: this parameter is ignored)
|
* a description of the field (note: this parameter is ignored)
|
||||||
* @param {Object} [options] additional options (note: this parameter is ignored)
|
* @param {Object} [options] additional options (note: this parameter is ignored)
|
||||||
@@ -38,6 +38,9 @@ var _t = core._t;
|
|||||||
* @returns {string} bin_size (which is human-readable)
|
* @returns {string} bin_size (which is human-readable)
|
||||||
*/
|
*/
|
||||||
function formatBinary(value, field, options) {
|
function formatBinary(value, field, options) {
|
||||||
|
if (!value) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
return utils.binaryToBinsize(value);
|
return utils.binaryToBinsize(value);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -387,10 +387,6 @@ return core.Class.extend({
|
|||||||
attrs.fieldDependencies = attrs.Widget.prototype.fieldDependencies;
|
attrs.fieldDependencies = attrs.Widget.prototype.fieldDependencies;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (attrs.Widget.prototype.context) {
|
|
||||||
attrs.context = attrs.Widget.prototype.context;
|
|
||||||
}
|
|
||||||
|
|
||||||
return attrs;
|
return attrs;
|
||||||
},
|
},
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -1854,7 +1854,7 @@ var BasicModel = AbstractModel.extend({
|
|||||||
model: record.model,
|
model: record.model,
|
||||||
method: 'read',
|
method: 'read',
|
||||||
args: [[record.res_id], fieldNames],
|
args: [[record.res_id], fieldNames],
|
||||||
context: record.getContext(),
|
context: _.extend({}, record.getContext(), {bin_size: true}),
|
||||||
})
|
})
|
||||||
.then(function (result) {
|
.then(function (result) {
|
||||||
if (result.length === 0) {
|
if (result.length === 0) {
|
||||||
@@ -2374,7 +2374,7 @@ var BasicModel = AbstractModel.extend({
|
|||||||
});
|
});
|
||||||
record.data[fieldName] = list.id;
|
record.data[fieldName] = list.id;
|
||||||
if (!fieldInfo.__no_fetch) {
|
if (!fieldInfo.__no_fetch) {
|
||||||
var def = self._readUngroupedList(list).then(function (list) {
|
var def = self._readUngroupedList(list).then(function () {
|
||||||
return $.when(
|
return $.when(
|
||||||
self._fetchX2ManysBatched(list),
|
self._fetchX2ManysBatched(list),
|
||||||
self._fetchReferencesBatched(list)
|
self._fetchReferencesBatched(list)
|
||||||
@@ -2403,6 +2403,7 @@ var BasicModel = AbstractModel.extend({
|
|||||||
var fields = view ? view.fields : fieldInfo.relatedFields;
|
var fields = view ? view.fields : fieldInfo.relatedFields;
|
||||||
var viewType = view ? view.type : fieldInfo.viewType;
|
var viewType = view ? view.type : fieldInfo.viewType;
|
||||||
list = this._applyX2ManyOperations(list);
|
list = this._applyX2ManyOperations(list);
|
||||||
|
this._sortList(list);
|
||||||
var x2mRecords = [];
|
var x2mRecords = [];
|
||||||
|
|
||||||
// step 1: collect ids
|
// step 1: collect ids
|
||||||
@@ -2640,7 +2641,7 @@ var BasicModel = AbstractModel.extend({
|
|||||||
var relRecordAdded = [];
|
var relRecordAdded = [];
|
||||||
var relRecordUpdated = [];
|
var relRecordUpdated = [];
|
||||||
_.each(list._changes, function (change) {
|
_.each(list._changes, function (change) {
|
||||||
if (change.operation === 'ADD') {
|
if (change.operation === 'ADD' && change.id) {
|
||||||
relRecordAdded.push(self.localData[change.id]);
|
relRecordAdded.push(self.localData[change.id]);
|
||||||
} else if (change.operation === 'UPDATE' && !self.isNew(change.id)) {
|
} else if (change.operation === 'UPDATE' && !self.isNew(change.id)) {
|
||||||
// ignore new records that would have been updated
|
// ignore new records that would have been updated
|
||||||
@@ -2699,6 +2700,10 @@ var BasicModel = AbstractModel.extend({
|
|||||||
} else if (_.contains(addedIds, list.res_ids[i])) {
|
} else if (_.contains(addedIds, list.res_ids[i])) {
|
||||||
// this is a new id (maybe existing in DB, but new in JS)
|
// this is a new id (maybe existing in DB, but new in JS)
|
||||||
relRecord = _.findWhere(relRecordAdded, {res_id: list.res_ids[i]});
|
relRecord = _.findWhere(relRecordAdded, {res_id: list.res_ids[i]});
|
||||||
|
if (!relRecord) {
|
||||||
|
commands[fieldName].push(x2ManyCommands.link_to(list.res_ids[i]));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
changes = this._generateChanges(relRecord, options);
|
changes = this._generateChanges(relRecord, options);
|
||||||
if (changes.id) {
|
if (changes.id) {
|
||||||
// the subrecord already exists in db
|
// the subrecord already exists in db
|
||||||
@@ -3092,23 +3097,13 @@ var BasicModel = AbstractModel.extend({
|
|||||||
id: {type: 'integer'},
|
id: {type: 'integer'},
|
||||||
}, params.fields);
|
}, params.fields);
|
||||||
|
|
||||||
// datapoint context is extended by fields widgets
|
|
||||||
var context = _.extend({}, params.context);
|
|
||||||
if (params.fieldsInfo && params.viewType) {
|
|
||||||
_.each(params.fieldsInfo[params.viewType], function (field) {
|
|
||||||
if (field.context) {
|
|
||||||
_.extend(context, field.context);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
var dataPoint = {
|
var dataPoint = {
|
||||||
_cache: type === 'list' ? {} : undefined,
|
_cache: type === 'list' ? {} : undefined,
|
||||||
_changes: null,
|
_changes: null,
|
||||||
_domains: {},
|
_domains: {},
|
||||||
_rawChanges: {},
|
_rawChanges: {},
|
||||||
aggregateValues: params.aggregateValues || {},
|
aggregateValues: params.aggregateValues || {},
|
||||||
context: context,
|
context: params.context,
|
||||||
count: params.count || res_ids.length,
|
count: params.count || res_ids.length,
|
||||||
data: data,
|
data: data,
|
||||||
domain: params.domain || [],
|
domain: params.domain || [],
|
||||||
|
|||||||
@@ -167,6 +167,13 @@ var KanbanColumn = Widget.extend({
|
|||||||
* Adds the quick create record to the top of the column.
|
* Adds the quick create record to the top of the column.
|
||||||
*/
|
*/
|
||||||
addQuickCreate: function () {
|
addQuickCreate: function () {
|
||||||
|
if (this.folded) {
|
||||||
|
// first open the column, and then add the quick create
|
||||||
|
this.trigger_up('column_toggle_fold', {
|
||||||
|
openQuickCreate: true,
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (this.quickCreateWidget) {
|
if (this.quickCreateWidget) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -207,6 +207,7 @@ var KanbanController = BasicController.extend({
|
|||||||
* @param {OdooEvent} event
|
* @param {OdooEvent} event
|
||||||
*/
|
*/
|
||||||
_onButtonClicked: function (event) {
|
_onButtonClicked: function (event) {
|
||||||
|
event.stopPropagation();
|
||||||
var self = this;
|
var self = this;
|
||||||
var attrs = event.data.attrs;
|
var attrs = event.data.attrs;
|
||||||
var record = event.data.record;
|
var record = event.data.record;
|
||||||
@@ -388,13 +389,18 @@ var KanbanController = BasicController.extend({
|
|||||||
/**
|
/**
|
||||||
* @private
|
* @private
|
||||||
* @param {OdooEvent} event
|
* @param {OdooEvent} event
|
||||||
|
* @param {boolean} [event.data.openQuickCreate=false] if true, opens the
|
||||||
|
* QuickCreate in the toggled column (it assumes that we are opening it)
|
||||||
*/
|
*/
|
||||||
_onToggleColumn: function (event) {
|
_onToggleColumn: function (event) {
|
||||||
var self = this;
|
var self = this;
|
||||||
var column = event.target;
|
var column = event.target;
|
||||||
this.model.toggleGroup(column.db_id).then(function (db_id) {
|
this.model.toggleGroup(column.db_id).then(function (db_id) {
|
||||||
var data = self.model.get(db_id);
|
var data = self.model.get(db_id);
|
||||||
self.renderer.updateColumn(db_id, data);
|
var options = {
|
||||||
|
openQuickCreate: !!event.data.openQuickCreate,
|
||||||
|
};
|
||||||
|
self.renderer.updateColumn(db_id, data, options);
|
||||||
self._updateEnv();
|
self._updateEnv();
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ var KanbanModel = BasicModel.extend({
|
|||||||
* @returns {Deferred<string>} resolves to the local id of the new record
|
* @returns {Deferred<string>} resolves to the local id of the new record
|
||||||
*/
|
*/
|
||||||
addRecordToGroup: function (groupID, resId) {
|
addRecordToGroup: function (groupID, resId) {
|
||||||
|
var self = this;
|
||||||
var group = this.localData[groupID];
|
var group = this.localData[groupID];
|
||||||
var new_record = this._makeDataPoint({
|
var new_record = this._makeDataPoint({
|
||||||
res_id: resId,
|
res_id: resId,
|
||||||
@@ -31,15 +32,16 @@ var KanbanModel = BasicModel.extend({
|
|||||||
viewType: group.viewType,
|
viewType: group.viewType,
|
||||||
parentID: groupID,
|
parentID: groupID,
|
||||||
});
|
});
|
||||||
group.data.unshift(new_record.id);
|
|
||||||
group.res_ids.unshift(resId);
|
|
||||||
group.count++;
|
|
||||||
|
|
||||||
// update the res_ids and count of the parent
|
|
||||||
this.localData[group.parentID].count++;
|
|
||||||
this._updateParentResIDs(group);
|
|
||||||
|
|
||||||
return this._fetchRecord(new_record).then(function (result) {
|
return this._fetchRecord(new_record).then(function (result) {
|
||||||
|
group.data.unshift(new_record.id);
|
||||||
|
group.res_ids.unshift(resId);
|
||||||
|
group.count++;
|
||||||
|
|
||||||
|
// update the res_ids and count of the parent
|
||||||
|
self.localData[group.parentID].count++;
|
||||||
|
self._updateParentResIDs(group);
|
||||||
|
|
||||||
return result.id;
|
return result.id;
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -6916,6 +6916,7 @@ QUnit.module('relational_fields', {
|
|||||||
if (args.method === 'read' && args.model === 'partner') {
|
if (args.method === 'read' && args.model === 'partner') {
|
||||||
assert.deepEqual(args.kwargs.context, {
|
assert.deepEqual(args.kwargs.context, {
|
||||||
active_field: 2,
|
active_field: 2,
|
||||||
|
bin_size: true,
|
||||||
someKey: 'some value',
|
someKey: 'some value',
|
||||||
}, "sent context should be correct");
|
}, "sent context should be correct");
|
||||||
}
|
}
|
||||||
@@ -7639,6 +7640,74 @@ QUnit.module('relational_fields', {
|
|||||||
form.destroy();
|
form.destroy();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
QUnit.test('one2many with multiple pages and sequence field, part2', function (assert) {
|
||||||
|
assert.expect(1);
|
||||||
|
|
||||||
|
this.data.partner.records[0].turtles = [3, 2, 1];
|
||||||
|
this.data.partner.onchanges.turtles = function () {};
|
||||||
|
|
||||||
|
var form = createView({
|
||||||
|
View: FormView,
|
||||||
|
model: 'partner',
|
||||||
|
data: this.data,
|
||||||
|
arch:'<form string="Partners">' +
|
||||||
|
'<field name="turtles">' +
|
||||||
|
'<tree limit="2">' +
|
||||||
|
'<field name="turtle_int" widget="handle"/>' +
|
||||||
|
'<field name="turtle_foo"/>' +
|
||||||
|
'<field name="partner_ids" invisible="1"/>' +
|
||||||
|
'</tree>' +
|
||||||
|
'</field>' +
|
||||||
|
'</form>',
|
||||||
|
res_id: 1,
|
||||||
|
mockRPC: function (route, args) {
|
||||||
|
if (args.method === 'onchange') {
|
||||||
|
return $.when({value: { turtles: [
|
||||||
|
[5],
|
||||||
|
[1, 1, {turtle_foo: "from onchange id2", partner_ids: [[5]]}],
|
||||||
|
[1, 3, {turtle_foo: "from onchange id3", partner_ids: [[5]]}],
|
||||||
|
]}});
|
||||||
|
}
|
||||||
|
return this._super(route, args);
|
||||||
|
},
|
||||||
|
viewOptions: {
|
||||||
|
mode: 'edit',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
form.$('span[name="delete"]').first().click();
|
||||||
|
assert.strictEqual(form.$('.o_data_row').text(), 'from onchange id2from onchange id3',
|
||||||
|
'onchange has been properly applied');
|
||||||
|
form.destroy();
|
||||||
|
});
|
||||||
|
|
||||||
|
QUnit.test('new record, with one2many with more default values than limit', function (assert) {
|
||||||
|
assert.expect(2);
|
||||||
|
|
||||||
|
var form = createView({
|
||||||
|
View: FormView,
|
||||||
|
model: 'partner',
|
||||||
|
data: this.data,
|
||||||
|
arch:'<form string="Partners">' +
|
||||||
|
'<field name="turtles">' +
|
||||||
|
'<tree limit="2">' +
|
||||||
|
'<field name="turtle_foo"/>' +
|
||||||
|
'</tree>' +
|
||||||
|
'</field>' +
|
||||||
|
'</form>',
|
||||||
|
context: { default_turtles: [1,2,3]},
|
||||||
|
viewOptions: {
|
||||||
|
mode: 'edit',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
assert.strictEqual(form.$('.o_data_row').text(), 'yopblip',
|
||||||
|
'data has been properly loaded');
|
||||||
|
form.$buttons.find('.o_form_button_save').click();
|
||||||
|
|
||||||
|
assert.strictEqual(form.$('.o_data_row').text(), 'yopblip',
|
||||||
|
'data has been properly saved');
|
||||||
|
form.destroy();
|
||||||
|
});
|
||||||
|
|
||||||
QUnit.module('FieldMany2Many');
|
QUnit.module('FieldMany2Many');
|
||||||
|
|
||||||
QUnit.test('many2many kanban: edition', function (assert) {
|
QUnit.test('many2many kanban: edition', function (assert) {
|
||||||
|
|||||||
@@ -74,6 +74,7 @@ QUnit.module('Views', {
|
|||||||
mockRPC: function (route, args) {
|
mockRPC: function (route, args) {
|
||||||
assert.deepEqual(args.kwargs.context, {
|
assert.deepEqual(args.kwargs.context, {
|
||||||
active_field: 2,
|
active_field: 2,
|
||||||
|
bin_size: true,
|
||||||
someKey: 'some value',
|
someKey: 'some value',
|
||||||
}, "should have sent the correct context");
|
}, "should have sent the correct context");
|
||||||
return this._super.apply(this, arguments);
|
return this._super.apply(this, arguments);
|
||||||
|
|||||||
@@ -199,7 +199,7 @@ QUnit.module('Views', {
|
|||||||
});
|
});
|
||||||
|
|
||||||
QUnit.test('only necessary fields are fetched with correct context', function (assert) {
|
QUnit.test('only necessary fields are fetched with correct context', function (assert) {
|
||||||
assert.expect(1);
|
assert.expect(2);
|
||||||
|
|
||||||
var form = createView({
|
var form = createView({
|
||||||
View: FormView,
|
View: FormView,
|
||||||
@@ -214,6 +214,8 @@ QUnit.module('Views', {
|
|||||||
// field, not sure why. Maybe this test should be modified.
|
// field, not sure why. Maybe this test should be modified.
|
||||||
assert.deepEqual(args.args[1], ["foo", "display_name"],
|
assert.deepEqual(args.args[1], ["foo", "display_name"],
|
||||||
"should only fetch requested fields");
|
"should only fetch requested fields");
|
||||||
|
assert.deepEqual(args.kwargs.context, {bin_size: true},
|
||||||
|
"bin_size should always be in the context");
|
||||||
return this._super(route, args);
|
return this._super(route, args);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -33,9 +33,10 @@ QUnit.module('Views', {
|
|||||||
state: { string: "State", type: "selection", selection: [["abc", "ABC"], ["def", "DEF"], ["ghi", "GHI"]]},
|
state: { string: "State", type: "selection", selection: [["abc", "ABC"], ["def", "DEF"], ["ghi", "GHI"]]},
|
||||||
date: {string: "Date Field", type: 'date'},
|
date: {string: "Date Field", type: 'date'},
|
||||||
datetime: {string: "Datetime Field", type: 'datetime'},
|
datetime: {string: "Datetime Field", type: 'datetime'},
|
||||||
|
image: {string: "Image", type: "binary"},
|
||||||
},
|
},
|
||||||
records: [
|
records: [
|
||||||
{id: 1, bar: true, foo: "yop", int_field: 10, qux: 0.4, product_id: 3, state: "abc", category_ids: []},
|
{id: 1, bar: true, foo: "yop", int_field: 10, qux: 0.4, product_id: 3, state: "abc", category_ids: [], 'image': 'R0lGODlhAQABAAD/ACwAAAAAAQABAAACAA=='},
|
||||||
{id: 2, bar: true, foo: "blip", int_field: 9, qux: 13, product_id: 5, state: "def", category_ids: [6]},
|
{id: 2, bar: true, foo: "blip", int_field: 9, qux: 13, product_id: 5, state: "def", category_ids: [6]},
|
||||||
{id: 3, bar: true, foo: "gnap", int_field: 17, qux: -3, product_id: 3, state: "ghi", category_ids: [7]},
|
{id: 3, bar: true, foo: "gnap", int_field: 17, qux: -3, product_id: 3, state: "ghi", category_ids: [7]},
|
||||||
{id: 4, bar: false, foo: "blip", int_field: -4, qux: 9, product_id: 5, state: "ghi", category_ids: []},
|
{id: 4, bar: false, foo: "blip", int_field: -4, qux: 9, product_id: 5, state: "ghi", category_ids: []},
|
||||||
@@ -440,6 +441,50 @@ QUnit.module('Views', {
|
|||||||
kanban.destroy();
|
kanban.destroy();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
QUnit.test('quick create when first column is folded', function (assert) {
|
||||||
|
assert.expect(6);
|
||||||
|
|
||||||
|
var kanban = createView({
|
||||||
|
View: KanbanView,
|
||||||
|
model: 'partner',
|
||||||
|
data: this.data,
|
||||||
|
arch: '<kanban on_create="quick_create">' +
|
||||||
|
'<field name="bar"/>' +
|
||||||
|
'<templates><t t-name="kanban-box">' +
|
||||||
|
'<div><field name="foo"/></div>' +
|
||||||
|
'</t></templates></kanban>',
|
||||||
|
groupBy: ['bar'],
|
||||||
|
});
|
||||||
|
|
||||||
|
assert.notOk(kanban.$('.o_kanban_group:first').hasClass('o_column_folded'),
|
||||||
|
"first column should not be folded");
|
||||||
|
|
||||||
|
// fold the first column
|
||||||
|
kanban.$('.o_kanban_group:first .o_kanban_toggle_fold').click();
|
||||||
|
|
||||||
|
assert.ok(kanban.$('.o_kanban_group:first').hasClass('o_column_folded'),
|
||||||
|
"first column should be folded");
|
||||||
|
|
||||||
|
// click on 'Create' to open the quick create in the first column
|
||||||
|
kanban.$buttons.find('.o-kanban-button-new').click();
|
||||||
|
|
||||||
|
assert.notOk(kanban.$('.o_kanban_group:first').hasClass('o_column_folded'),
|
||||||
|
"first column should no longer be folded");
|
||||||
|
var $quickCreate = kanban.$('.o_kanban_group:first .o_kanban_quick_create');
|
||||||
|
assert.strictEqual($quickCreate.length, 1,
|
||||||
|
"should have added a quick create element in first column");
|
||||||
|
|
||||||
|
// fold again the first column
|
||||||
|
kanban.$('.o_kanban_group:first .o_kanban_toggle_fold').click();
|
||||||
|
|
||||||
|
assert.ok(kanban.$('.o_kanban_group:first').hasClass('o_column_folded'),
|
||||||
|
"first column should be folded");
|
||||||
|
assert.strictEqual(kanban.$('.o_kanban_quick_create').length, 0,
|
||||||
|
"there should be no more quick create");
|
||||||
|
|
||||||
|
kanban.destroy();
|
||||||
|
});
|
||||||
|
|
||||||
QUnit.test('quick create and edit in grouped mode', function (assert) {
|
QUnit.test('quick create and edit in grouped mode', function (assert) {
|
||||||
assert.expect(6);
|
assert.expect(6);
|
||||||
|
|
||||||
@@ -545,6 +590,80 @@ QUnit.module('Views', {
|
|||||||
kanban.destroy();
|
kanban.destroy();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
QUnit.test('quick create several records on slow network', function (assert) {
|
||||||
|
assert.expect(6);
|
||||||
|
|
||||||
|
var self = this;
|
||||||
|
var def;
|
||||||
|
var kanban = createView({
|
||||||
|
View: KanbanView,
|
||||||
|
model: 'partner',
|
||||||
|
data: this.data,
|
||||||
|
arch: '<kanban class="o_kanban_test" on_create="quick_create">' +
|
||||||
|
'<field name="bar"/>' +
|
||||||
|
'<templates><t t-name="kanban-box">' +
|
||||||
|
'<div><field name="foo"/></div>' +
|
||||||
|
'</t></templates></kanban>',
|
||||||
|
groupBy: ['bar'],
|
||||||
|
mockRPC: function (route, args) {
|
||||||
|
var result = this._super.apply(this, arguments);
|
||||||
|
if (args.method === 'name_create') {
|
||||||
|
return result.then(function (result) {
|
||||||
|
// simulate a dynamic default value for foo based on display_name,
|
||||||
|
// so that we can check that the record has been fetched before
|
||||||
|
// being displayed
|
||||||
|
var newRec = _.findWhere(self.data.partner.records, {id: result[0]});
|
||||||
|
newRec.foo = newRec.display_name;
|
||||||
|
return result;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (args.method === 'read') {
|
||||||
|
return $.when(def).then(_.constant(result));
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
assert.strictEqual(kanban.$('.o_kanban_group:first .o_kanban_record').length, 1,
|
||||||
|
"first column should contain one record");
|
||||||
|
|
||||||
|
// click to add a record, and add two in a row (first one will be delayed)
|
||||||
|
kanban.$('.o_kanban_header .o_kanban_quick_add i').first().click();
|
||||||
|
|
||||||
|
assert.strictEqual(kanban.$('.o_kanban_quick_create').length, 1,
|
||||||
|
"the quick create should be open");
|
||||||
|
|
||||||
|
def = $.Deferred();
|
||||||
|
kanban.$('.o_kanban_quick_create input')
|
||||||
|
.val('new partner 1')
|
||||||
|
.trigger($.Event('keypress', {
|
||||||
|
which: $.ui.keyCode.ENTER,
|
||||||
|
keyCode: $.ui.keyCode.ENTER,
|
||||||
|
}));
|
||||||
|
var firstDef = def;
|
||||||
|
def = null;
|
||||||
|
kanban.$('.o_kanban_quick_create input')
|
||||||
|
.val('new partner 2')
|
||||||
|
.trigger($.Event('keypress', {
|
||||||
|
which: $.ui.keyCode.ENTER,
|
||||||
|
keyCode: $.ui.keyCode.ENTER,
|
||||||
|
}));
|
||||||
|
|
||||||
|
assert.strictEqual(kanban.$('.o_kanban_group:first .o_kanban_record').length, 2,
|
||||||
|
"first column should now contain two records");
|
||||||
|
assert.strictEqual(kanban.$('.o_kanban_group:first .o_kanban_record:first').text(),
|
||||||
|
'new partner 2', "new record should have been fetched");
|
||||||
|
|
||||||
|
firstDef.resolve();
|
||||||
|
|
||||||
|
assert.strictEqual(kanban.$('.o_kanban_group:first .o_kanban_record').length, 3,
|
||||||
|
"first column should now contain three records");
|
||||||
|
assert.strictEqual(kanban.$('.o_kanban_group:first .o_kanban_record:first').text(),
|
||||||
|
'new partner 1', "new record should have been fetched");
|
||||||
|
|
||||||
|
kanban.destroy();
|
||||||
|
});
|
||||||
|
|
||||||
QUnit.test('quick create fail in grouped', function (assert) {
|
QUnit.test('quick create fail in grouped', function (assert) {
|
||||||
assert.expect(7);
|
assert.expect(7);
|
||||||
|
|
||||||
@@ -2339,6 +2458,32 @@ QUnit.module('Views', {
|
|||||||
"quick create should have been added in the first column");
|
"quick create should have been added in the first column");
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
QUnit.test('test displaying image', function (assert) {
|
||||||
|
assert.expect(2);
|
||||||
|
|
||||||
|
var kanban = createView({
|
||||||
|
View: KanbanView,
|
||||||
|
model: 'partner',
|
||||||
|
data: this.data,
|
||||||
|
arch: '<kanban class="o_kanban_test">' +
|
||||||
|
'<field name="id"/>' +
|
||||||
|
'<field name="image"/>' +
|
||||||
|
'<templates><t t-name="kanban-box"><div>' +
|
||||||
|
'<img t-att-src="kanban_image(\'partner\', \'image\', record.id.raw_value)"/>' +
|
||||||
|
'</div></t></templates>' +
|
||||||
|
'</kanban>',
|
||||||
|
});
|
||||||
|
|
||||||
|
var imageOnRecord = kanban.$('img[src*="/web/image"][src*="&id=1"]');
|
||||||
|
assert.strictEqual(imageOnRecord.length, 1, "partner with image display image");
|
||||||
|
|
||||||
|
var placeholders = kanban.$('img[src$="/web/static/src/img/placeholder.png"]');
|
||||||
|
assert.strictEqual(placeholders.length, this.data.partner.records.length - 1,
|
||||||
|
"partner with image should displaiy");
|
||||||
|
|
||||||
|
kanban.destroy();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ class WebSuite(odoo.tests.HttpCase):
|
|||||||
|
|
||||||
def test_01_js(self):
|
def test_01_js(self):
|
||||||
# webclient desktop test suite
|
# webclient desktop test suite
|
||||||
self.phantom_js('/web/tests?mod=web', "", "", login='admin', timeout=300)
|
self.phantom_js('/web/tests?mod=web', "", "", login='admin', timeout=360)
|
||||||
|
|
||||||
def test_02_js(self):
|
def test_02_js(self):
|
||||||
# webclient mobile test suite
|
# webclient mobile test suite
|
||||||
|
|||||||
@@ -374,10 +374,11 @@ var RTEWidget = Widget.extend({
|
|||||||
* @param {boolean} internal_history
|
* @param {boolean} internal_history
|
||||||
*/
|
*/
|
||||||
historyRecordUndo: function ($target, event, internal_history) {
|
historyRecordUndo: function ($target, event, internal_history) {
|
||||||
|
$target = $($target);
|
||||||
var rng = range.create();
|
var rng = range.create();
|
||||||
var $editable = $(rng && rng.sc).closest('.o_editable');
|
var $editable = $(rng && rng.sc).closest('.o_editable');
|
||||||
if (!rng || !$editable.length) {
|
if (!rng || !$editable.length) {
|
||||||
$editable = $($target).closest('.o_editable');
|
$editable = $target.closest('.o_editable');
|
||||||
rng = range.create($target.closest('*')[0],0);
|
rng = range.create($target.closest('*')[0],0);
|
||||||
} else {
|
} else {
|
||||||
rng = $editable.data('range') || rng;
|
rng = $editable.data('range') || rng;
|
||||||
|
|||||||
@@ -846,37 +846,67 @@ range.WrappedRange.prototype.reRange = function (keep_end, isNotBreakable) {
|
|||||||
|
|
||||||
return new range.WrappedRange(sc, so, ec, eo);
|
return new range.WrappedRange(sc, so, ec, eo);
|
||||||
};
|
};
|
||||||
// isOnImg: judge whether range is an image node or not
|
/**
|
||||||
|
* Returns the image the range is in or matches (if any, false otherwise).
|
||||||
|
*
|
||||||
|
* @todo this implementation may not cover all corner cases but should do the
|
||||||
|
* trick for all reproductible ones
|
||||||
|
* @returns {DOMElement|boolean}
|
||||||
|
*/
|
||||||
range.WrappedRange.prototype.isOnImg = function () {
|
range.WrappedRange.prototype.isOnImg = function () {
|
||||||
|
// If not a selection but a cursor position, just check if a point's
|
||||||
|
// ancestor is an image or not
|
||||||
|
if (this.sc === this.ec && this.so === this.eo) {
|
||||||
|
return dom.ancestor(this.sc, dom.isImg);
|
||||||
|
}
|
||||||
|
|
||||||
|
var startPoint = {node: this.sc, offset: this.so};
|
||||||
|
var endPoint = {node: this.ec, offset: this.eo};
|
||||||
|
|
||||||
var nb = 0;
|
var nb = 0;
|
||||||
var image;
|
var image;
|
||||||
var startPoint = {node: this.sc.childNodes.length && this.sc.childNodes[this.so] || this.sc};
|
var textNode;
|
||||||
startPoint.offset = startPoint.node === this.sc ? this.so : 0;
|
|
||||||
var endPoint = {node: this.ec.childNodes.length && this.ec.childNodes[this.eo] || this.ec};
|
|
||||||
endPoint.offset = endPoint.node === this.ec ? this.eo : 0;
|
|
||||||
|
|
||||||
if (dom.isImg(startPoint.node)) {
|
|
||||||
nb ++;
|
|
||||||
image = startPoint.node;
|
|
||||||
}
|
|
||||||
dom.walkPoint(startPoint, endPoint, function (point) {
|
dom.walkPoint(startPoint, endPoint, function (point) {
|
||||||
if (!dom.isText(endPoint.node) && point.node === endPoint.node && point.offset === endPoint.offset) {
|
// If the element has children (not a text node and not empty node),
|
||||||
|
// the element cannot be considered as selected (these children will
|
||||||
|
// be processed to determine that)
|
||||||
|
if (dom.hasChildren(point.node)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
var node = point.node.childNodes.length && point.node.childNodes[point.offset] || point.node;
|
|
||||||
var offset = node === point.node ? point.offset : 0;
|
// Check if an ancestor of the current point is an image
|
||||||
var isImg = dom.ancestor(node, dom.isImg);
|
var pointImg = dom.ancestor(point.node, dom.isImg);
|
||||||
if (!isImg && ((!dom.isBR(node) && !dom.isText(node)) || (offset && node.textContent.length !== offset && node.textContent.match(/\S|\u00A0/)))) {
|
var isText = dom.isText(point.node);
|
||||||
|
|
||||||
|
// Check if a visible element is selected, i.e.
|
||||||
|
// - If an ancestor of the current is an image we did not see yet
|
||||||
|
// - If the point is not in a br or a text (so a node with no children)
|
||||||
|
// - If the point is in a non empty text node we already saw
|
||||||
|
if (pointImg ?
|
||||||
|
(image !== pointImg) :
|
||||||
|
((!dom.isBR(point.node) && !isText) || (textNode === point.node && point.node.textContent.match(/\S|\u00A0/)))) {
|
||||||
nb++;
|
nb++;
|
||||||
}
|
}
|
||||||
if (isImg && image !== isImg) {
|
|
||||||
image = isImg;
|
// If an ancestor of the current point is an image, then save it as the
|
||||||
nb ++;
|
// image we are looking for
|
||||||
|
if (pointImg) {
|
||||||
|
image = pointImg;
|
||||||
|
}
|
||||||
|
// If the current point is a text node save it as the last text node
|
||||||
|
// seen (if we see it again, this might mean it is selected)
|
||||||
|
if (isText) {
|
||||||
|
textNode = point.node;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
return nb === 1 && image;
|
return nb === 1 && image;
|
||||||
};
|
};
|
||||||
range.WrappedRange.prototype.deleteContents = function (towrite) {
|
range.WrappedRange.prototype.deleteContents = function (towrite) {
|
||||||
|
if (this.sc === this.ec && this.so === this.eo) {
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
var prevBP = dom.removeBetween(this.sc, this.so, this.ec, this.eo, towrite);
|
var prevBP = dom.removeBetween(this.sc, this.so, this.ec, this.eo, towrite);
|
||||||
|
|
||||||
$(dom.node(prevBP.sc)).trigger("click"); // trigger click to disable and reanable editor and image handler
|
$(dom.node(prevBP.sc)).trigger("click"); // trigger click to disable and reanable editor and image handler
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ odoo.define('website_form.animation', function (require) {
|
|||||||
|
|
||||||
start: function () {
|
start: function () {
|
||||||
var self = this;
|
var self = this;
|
||||||
qweb.add_template('/website_form/static/src/xml/website_form.xml');
|
this.templates_loaded = ajax.loadXML('/website_form/static/src/xml/website_form.xml', qweb);
|
||||||
this.$target.find('.o_website_form_send').on('click',function (e) {self.send(e);});
|
this.$target.find('.o_website_form_send').on('click',function (e) {self.send(e);});
|
||||||
|
|
||||||
// Initialize datetimepickers
|
// Initialize datetimepickers
|
||||||
@@ -233,7 +233,10 @@ odoo.define('website_form.animation', function (require) {
|
|||||||
if (status !== 'success') { // Restore send button behavior if result is an error
|
if (status !== 'success') { // Restore send button behavior if result is an error
|
||||||
this.$target.find('.o_website_form_send').on('click',function (e) {self.send(e);});
|
this.$target.find('.o_website_form_send').on('click',function (e) {self.send(e);});
|
||||||
}
|
}
|
||||||
this.$target.find('#o_website_form_result').replaceWith(qweb.render("website_form.status_" + status));
|
var $result = this.$('#o_website_form_result');
|
||||||
|
this.templates_loaded.done(function () {
|
||||||
|
$result.replaceWith(qweb.render("website_form.status_" + status));
|
||||||
|
});
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -21,7 +21,10 @@ $(document).ready(function () {
|
|||||||
setTimeout(function () { payment_transaction_poll_status(); }, Math.ceil(_poll_nbr / 3) * 1000);
|
setTimeout(function () { payment_transaction_poll_status(); }, Math.ceil(_poll_nbr / 3) * 1000);
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
result.message = "<i class='fa fa-warning' />";
|
var $message = $(result.message);
|
||||||
|
$message.find('span:first').prepend($(
|
||||||
|
"<i title='We are waiting the confirmation of the bank or payment provider' class='fa fa-warning' style='margin-right:10px;'>"));
|
||||||
|
result.message = $message.html();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
$('div.oe_website_sale_tx_status').html(result.message);
|
$('div.oe_website_sale_tx_status').html(result.message);
|
||||||
|
|||||||
@@ -684,6 +684,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<template id="wizard_checkout" name="Wizard Checkout">
|
<template id="wizard_checkout" name="Wizard Checkout">
|
||||||
|
<t t-set="website_sale_order" t-value="website.sale_get_order()"/>
|
||||||
<ul class="wizard pull-right hidden-xs">
|
<ul class="wizard pull-right hidden-xs">
|
||||||
<li t-att-class="step==10 and 'text-primary' or 'text-muted'" id="step10">
|
<li t-att-class="step==10 and 'text-primary' or 'text-muted'" id="step10">
|
||||||
<t t-set="step_review_order" t-value="step>10 and step<50"/>
|
<t t-set="step_review_order" t-value="step>10 and step<50"/>
|
||||||
|
|||||||
@@ -11,12 +11,14 @@ odoo.define('website_sale_delivery.checkout', function (require) {
|
|||||||
var $amount_delivery = $('#order_delivery span.oe_currency_value');
|
var $amount_delivery = $('#order_delivery span.oe_currency_value');
|
||||||
var $amount_untaxed = $('#order_total_untaxed span.oe_currency_value');
|
var $amount_untaxed = $('#order_total_untaxed span.oe_currency_value');
|
||||||
var $amount_tax = $('#order_total_taxes span.oe_currency_value');
|
var $amount_tax = $('#order_total_taxes span.oe_currency_value');
|
||||||
|
var $amount_total = $('#order_total span.oe_currency_value');
|
||||||
var $carrier_badge = $('#delivery_carrier input[name="delivery_type"][value=' + result.carrier_id + '] ~ .badge.hidden');
|
var $carrier_badge = $('#delivery_carrier input[name="delivery_type"][value=' + result.carrier_id + '] ~ .badge.hidden');
|
||||||
var $compute_badge = $('#delivery_carrier input[name="delivery_type"][value=' + result.carrier_id + '] ~ .o_delivery_compute');
|
var $compute_badge = $('#delivery_carrier input[name="delivery_type"][value=' + result.carrier_id + '] ~ .o_delivery_compute');
|
||||||
if (result.status === true) {
|
if (result.status === true) {
|
||||||
$amount_delivery.text(result.new_amount_delivery);
|
$amount_delivery.text(result.new_amount_delivery);
|
||||||
$amount_untaxed.text(result.new_amount_untaxed);
|
$amount_untaxed.text(result.new_amount_untaxed);
|
||||||
$amount_tax.text(result.new_amount_tax);
|
$amount_tax.text(result.new_amount_tax);
|
||||||
|
$amount_total.text(result.new_amount_delivery + result.new_amount_untaxed + result.new_amount_tax);
|
||||||
$carrier_badge.children('span').text(result.new_amount_delivery);
|
$carrier_badge.children('span').text(result.new_amount_delivery);
|
||||||
$carrier_badge.removeClass('hidden');
|
$carrier_badge.removeClass('hidden');
|
||||||
$compute_badge.addClass('hidden');
|
$compute_badge.addClass('hidden');
|
||||||
|
|||||||
@@ -7,12 +7,12 @@ var core = require('web.core');
|
|||||||
var time = require('web.time');
|
var time = require('web.time');
|
||||||
var Widget = require('web.Widget');
|
var Widget = require('web.Widget');
|
||||||
var local_storage = require('web.local_storage');
|
var local_storage = require('web.local_storage');
|
||||||
|
var websiteRootInstance = require('website.WebsiteRoot.instance');
|
||||||
|
|
||||||
var _t = core._t;
|
var _t = core._t;
|
||||||
var page_widgets = {};
|
var page_widgets = {};
|
||||||
|
|
||||||
$(document).ready(function () {
|
(function () {
|
||||||
|
|
||||||
var widget_parent = $('body');
|
var widget_parent = $('body');
|
||||||
|
|
||||||
|
|
||||||
@@ -23,7 +23,7 @@ $(document).ready(function () {
|
|||||||
// then return fix formate string else timeago
|
// then return fix formate string else timeago
|
||||||
display_str = "";
|
display_str = "";
|
||||||
if (datetime_obj && new Date().getTime() - datetime_obj.getTime() > 7 * 24 * 60 * 60 * 1000) {
|
if (datetime_obj && new Date().getTime() - datetime_obj.getTime() > 7 * 24 * 60 * 60 * 1000) {
|
||||||
display_str = datetime_obj.toDateString();
|
display_str = moment(datetime_obj).format('ll');
|
||||||
} else {
|
} else {
|
||||||
display_str = moment(datetime_obj).fromNow();
|
display_str = moment(datetime_obj).fromNow();
|
||||||
}
|
}
|
||||||
@@ -216,7 +216,7 @@ $(document).ready(function () {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
})();
|
||||||
|
|
||||||
return {
|
return {
|
||||||
page_widgets: page_widgets,
|
page_widgets: page_widgets,
|
||||||
|
|||||||
@@ -112,11 +112,6 @@
|
|||||||
<i class="fa fa-cloud-upload"></i> Upload
|
<i class="fa fa-cloud-upload"></i> Upload
|
||||||
</a>
|
</a>
|
||||||
</t>
|
</t>
|
||||||
<t t-if="channel.can_upload and is_public_user">
|
|
||||||
<a class="btn btn-primary" title="Upload Presentation" t-attf-href="/web/login?redirect=/slides/#{slug(channel)}">
|
|
||||||
<i class="fa fa-cloud-upload"></i> Upload
|
|
||||||
</a>
|
|
||||||
</t>
|
|
||||||
</h3>
|
</h3>
|
||||||
</div>
|
</div>
|
||||||
<!-- Search box -->
|
<!-- Search box -->
|
||||||
|
|||||||
@@ -1,16 +0,0 @@
|
|||||||
Netherlands, 8-2-2015
|
|
||||||
|
|
||||||
BAS Solutions agrees to the terms of the Odoo Corporate Contributor License
|
|
||||||
Agreement v1.0.
|
|
||||||
|
|
||||||
I declare that I am authorized and able to make this agreement and sign this
|
|
||||||
declaration.
|
|
||||||
|
|
||||||
Signed,
|
|
||||||
|
|
||||||
Erwin van der Ploeg erwin@bas-solutions.nl https://github.com/erwin-bas-solutions
|
|
||||||
|
|
||||||
List of contributors:
|
|
||||||
|
|
||||||
Erwin van der Ploeg erwin@bas-solutions.nl https://github.com/erwin-bas-solutions
|
|
||||||
Ruchir Shukla ruchir@bas-solutions.nl https://github.com/ruchir-bas-solutions
|
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
Netherlands, 18-1-2018
|
||||||
|
|
||||||
|
Odoo Experts agrees to the terms of the Odoo Corporate Contributor License
|
||||||
|
Agreement v1.0.
|
||||||
|
|
||||||
|
I declare that I am authorized and able to make this agreement and sign this
|
||||||
|
declaration.
|
||||||
|
|
||||||
|
Signed,
|
||||||
|
|
||||||
|
Erwin van der Ploeg erwin@bas-solutions.nl https://github.com/erwin-bas-solutions
|
||||||
|
|
||||||
|
List of contributors:
|
||||||
|
|
||||||
|
Erwin van der Ploeg erwin@odooexperts.nl https://github.com/ploegvde
|
||||||
|
Yenthe Van Ginneken yenthe@odooexperts.nl https://github.com/Yenthe666
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
Vietnam, 2018-01-16,
|
||||||
|
|
||||||
|
I hereby agree to the terms of the Odoo Individual Contributor License
|
||||||
|
Agreement v1.0.
|
||||||
|
|
||||||
|
I declare that I am authorized and able to make this agreement and sign this
|
||||||
|
declaration.
|
||||||
|
|
||||||
|
Signed,
|
||||||
|
|
||||||
|
Nguyen Duc Tam nguyenductamlhp@gmail.com https://github.com/nguyenductamlhp
|
||||||
@@ -360,13 +360,6 @@ Source installation requires manually installing dependencies:
|
|||||||
Install ``psycopg`` using the installer here
|
Install ``psycopg`` using the installer here
|
||||||
http://www.stickpeople.com/projects/python/win-psycopg/
|
http://www.stickpeople.com/projects/python/win-psycopg/
|
||||||
|
|
||||||
Then edit the requirements.txt file:
|
|
||||||
|
|
||||||
- remove ``psycopg2`` as you already have it.
|
|
||||||
- remove the optional ``python-ldap``, ``gevent`` and ``psutil`` because
|
|
||||||
they require compilation.
|
|
||||||
- add ``pypiwin32`` because it's needed under windows.
|
|
||||||
|
|
||||||
Then use pip to install the dependencies using the following
|
Then use pip to install the dependencies using the following
|
||||||
command from a cmd.exe prompt (replace ``\YourOdooPath`` by the actual
|
command from a cmd.exe prompt (replace ``\YourOdooPath`` by the actual
|
||||||
path where you downloaded Odoo):
|
path where you downloaded Odoo):
|
||||||
|
|||||||
+38
-8
@@ -82,8 +82,9 @@ Overview
|
|||||||
The Credits
|
The Credits
|
||||||
|
|
||||||
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 monetary value of a credit depends
|
clients with tokens or *credits*. The credits are an integer unit and
|
||||||
on the service and is decided by the provider. This could be:
|
their monetary value depends on the service and is decided by the
|
||||||
|
provider. This could be:
|
||||||
|
|
||||||
* for an sms service: 1 credit = 1 sms,
|
* for an sms service: 1 credit = 1 sms,
|
||||||
* for an add service: 1 credit = 1 add,
|
* for an add service: 1 credit = 1 add,
|
||||||
@@ -210,14 +211,20 @@ The credit packages are essentially a product with 4 characteristics.
|
|||||||
* 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,
|
||||||
* Price: the price in *EUROS* for the time being (USD support is planned).
|
* Price: the price in *EUROS* for the time being (USD support is planned).
|
||||||
|
|
||||||
.. image:: images/package.png
|
.. note::
|
||||||
:align: center
|
|
||||||
|
Odoo takes a 25% commission on all package sales. Adjust your selling price accordingly.
|
||||||
|
|
||||||
|
|
||||||
.. note::
|
.. note::
|
||||||
|
|
||||||
Depending on the strategy, the price per credit can vary from one
|
Depending on the strategy, the price per credit can vary from one
|
||||||
package to another.
|
package to another.
|
||||||
|
|
||||||
|
|
||||||
|
.. image:: images/package.png
|
||||||
|
:align: center
|
||||||
|
|
||||||
.. _iap-odoo-app:
|
.. _iap-odoo-app:
|
||||||
|
|
||||||
Odoo App
|
Odoo App
|
||||||
@@ -309,15 +316,22 @@ perform the service within:
|
|||||||
|
|
||||||
.. todo:: "My Account" > "Your InApp Services"?
|
.. todo:: "My Account" > "Your InApp Services"?
|
||||||
|
|
||||||
|
|
||||||
The :class:`~odoo.addons.iap.models.iap.charge` helper will:
|
The :class:`~odoo.addons.iap.models.iap.charge` helper will:
|
||||||
|
|
||||||
|
.. note::
|
||||||
|
|
||||||
|
Since the 15th of January 2018, a new functionality that allows one to capture a different amount than autorized has been added.
|
||||||
|
See :ref:`Charging <iap-charging>`
|
||||||
|
|
||||||
1. authorize (create) a transaction with the specified number of credits,
|
1. authorize (create) a transaction with the specified number of credits,
|
||||||
if the account does not have enough credits it will raise the relevant
|
if the account does not have enough credits it will raise the relevant
|
||||||
error
|
error
|
||||||
2. execute the body of the ``with`` statement
|
2. execute the body of the ``with`` statement
|
||||||
3. if the body of the ``with`` executes succesfully, capture (confirm) the
|
3. (NEW) if the body of the ``with`` executes succesfully, update the price
|
||||||
transaction
|
of the transaction if needed
|
||||||
4. otherwise if an error is raised from the body of the ``with`` cancel the
|
4. capture (confirm) the transaction
|
||||||
|
5. otherwise if an error is raised from the body of the ``with`` cancel the
|
||||||
transaction (and release the hold on the credits)
|
transaction (and release the hold on the credits)
|
||||||
|
|
||||||
.. danger::
|
.. danger::
|
||||||
@@ -420,9 +434,11 @@ 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
|
||||||
:raises: :class:`~odoo.exceptions.AccessError`
|
:raises: :class:`~odoo.exceptions.AccessError`
|
||||||
|
|
||||||
.. code-block:: python
|
.. code-block:: python
|
||||||
|
:emphasize-lines: 8
|
||||||
|
|
||||||
r2 = requests.post(ODOO + '/iap/1/capture', json={
|
r2 = requests.post(ODOO + '/iap/1/capture', json={
|
||||||
'jsonrpc': '2.0',
|
'jsonrpc': '2.0',
|
||||||
@@ -431,6 +447,7 @@ Capture
|
|||||||
'params': {
|
'params': {
|
||||||
'token': tx,
|
'token': tx,
|
||||||
'key': SERVICE_KEY,
|
'key': SERVICE_KEY,
|
||||||
|
'credit_to_capture': credit or False,
|
||||||
}
|
}
|
||||||
}).json()
|
}).json()
|
||||||
if 'error' in r:
|
if 'error' in r:
|
||||||
@@ -523,9 +540,17 @@ Odoo Helpers
|
|||||||
For convenience, if you are implementing your service using Odoo the ``iap``
|
For convenience, if you are implementing your service using Odoo the ``iap``
|
||||||
module provides a few helpers to make IAP flow even simpler:
|
module provides a few helpers to make IAP flow even simpler:
|
||||||
|
|
||||||
|
.. _iap-charging:
|
||||||
|
|
||||||
Charging
|
Charging
|
||||||
--------
|
--------
|
||||||
|
|
||||||
|
.. note::
|
||||||
|
|
||||||
|
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.
|
||||||
|
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])
|
||||||
|
|
||||||
A *context manager* for authorizing and automatically capturing or
|
A *context manager* for authorizing and automatically capturing or
|
||||||
@@ -544,8 +569,10 @@ Charging
|
|||||||
:param UserToken token:
|
:param UserToken token:
|
||||||
:param int credit:
|
:param int credit:
|
||||||
:param str description:
|
:param str description:
|
||||||
|
:param Qweb template credit_template:
|
||||||
|
|
||||||
.. code-block:: python
|
.. code-block:: python
|
||||||
|
:emphasize-lines: 10,13,14,15
|
||||||
|
|
||||||
@route('/deathstar/superlaser', type='json')
|
@route('/deathstar/superlaser', type='json')
|
||||||
def superlaser(self, user_account,
|
def superlaser(self, user_account,
|
||||||
@@ -556,8 +583,11 @@ 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):
|
with charge(request.env, SERVICE_KEY, user_account, credits) as transaction:
|
||||||
# TODO: allow other targets
|
# TODO: allow other targets
|
||||||
|
transaction.credit = max(credits, 2)
|
||||||
|
# Sales ongoing one the energy price,
|
||||||
|
# a maximum of 2 credits will be charged/captured.
|
||||||
self.env['systems.planets'].search([
|
self.env['systems.planets'].search([
|
||||||
('grid', '=', 'M-10'),
|
('grid', '=', 'M-10'),
|
||||||
('name', '=', 'Alderaan'),
|
('name', '=', 'Alderaan'),
|
||||||
|
|||||||
@@ -1597,7 +1597,7 @@
|
|||||||
<field name="code">tl</field>
|
<field name="code">tl</field>
|
||||||
<field file="base/static/img/country_flags/tl.png" name="image" type="base64" />
|
<field file="base/static/img/country_flags/tl.png" name="image" type="base64" />
|
||||||
<field name="currency_id" ref="USD" />
|
<field name="currency_id" ref="USD" />
|
||||||
<field eval="0" name="phone_code" />
|
<field eval="670" name="phone_code" />
|
||||||
</record>
|
</record>
|
||||||
<record id="tr" model="res.country">
|
<record id="tr" model="res.country">
|
||||||
<field name="name">Turkey</field>
|
<field name="name">Turkey</field>
|
||||||
|
|||||||
@@ -678,7 +678,7 @@ class IrActionsReport(models.Model):
|
|||||||
"""
|
"""
|
||||||
discard_logo_check = self.env.context.get('discard_logo_check')
|
discard_logo_check = self.env.context.get('discard_logo_check')
|
||||||
if (self.env.uid == SUPERUSER_ID) and ((not self.env.user.company_id.external_report_layout) or (not discard_logo_check and not self.env.user.company_id.logo)) and config:
|
if (self.env.uid == SUPERUSER_ID) and ((not self.env.user.company_id.external_report_layout) or (not discard_logo_check and not self.env.user.company_id.logo)) and config:
|
||||||
template = self.env.ref('base.view_company_report_form_with_print')
|
template = self.env.ref('base.view_company_report_form_with_print') if self.env.context.get('from_transient_model', False) else self.env.ref('base.view_company_report_form')
|
||||||
return {
|
return {
|
||||||
'name': _('Choose Your Document Layout'),
|
'name': _('Choose Your Document Layout'),
|
||||||
'type': 'ir.actions.act_window',
|
'type': 'ir.actions.act_window',
|
||||||
|
|||||||
+9
-8
@@ -3,22 +3,23 @@ decorator==4.0.10
|
|||||||
docutils==0.12
|
docutils==0.12
|
||||||
ebaysdk==2.1.5
|
ebaysdk==2.1.5
|
||||||
feedparser==5.2.1
|
feedparser==5.2.1
|
||||||
gevent==1.1.2
|
gevent==1.1.2 ; sys_platform != 'win32'
|
||||||
greenlet==0.4.10
|
greenlet==0.4.10
|
||||||
html2text==2016.9.19
|
html2text==2016.9.19
|
||||||
Jinja2==2.8
|
Jinja2==2.8
|
||||||
lxml==3.5.0
|
lxml==3.7.1 ; sys_platform != 'win32'
|
||||||
|
lxml ; sys_platform == 'win32'
|
||||||
Mako==1.0.4
|
Mako==1.0.4
|
||||||
MarkupSafe==0.23
|
MarkupSafe==0.23
|
||||||
mock==2.0.0
|
mock==2.0.0
|
||||||
num2words==0.5.4
|
num2words==0.5.4
|
||||||
ofxparse==0.16
|
ofxparse==0.16
|
||||||
passlib==1.6.5
|
passlib==1.6.5
|
||||||
Pillow==3.4.1
|
Pillow==4.0.0
|
||||||
psutil==4.3.1
|
psutil==4.3.1; sys_platform != 'win32'
|
||||||
psycopg2==2.7.3.1
|
psycopg2==2.7.3.1; sys_platform != 'win32'
|
||||||
pydot==1.2.3
|
pydot==1.2.3
|
||||||
pyldap==2.4.28
|
pyldap==2.4.28; sys_platform != 'win32'
|
||||||
pyparsing==2.1.10
|
pyparsing==2.1.10
|
||||||
PyPDF2==1.26.0
|
PyPDF2==1.26.0
|
||||||
pyserial==3.1.1
|
pyserial==3.1.1
|
||||||
@@ -29,11 +30,11 @@ PyYAML==3.12
|
|||||||
qrcode==5.3
|
qrcode==5.3
|
||||||
reportlab==3.3.0
|
reportlab==3.3.0
|
||||||
requests==2.11.1
|
requests==2.11.1
|
||||||
six==1.10.0
|
|
||||||
suds-jurko==0.6
|
suds-jurko==0.6
|
||||||
vatnumber==1.2
|
vatnumber==1.2
|
||||||
vobject==0.9.3
|
vobject==0.9.3
|
||||||
Werkzeug==0.11.11
|
Werkzeug==0.11.15
|
||||||
XlsxWriter==0.9.3
|
XlsxWriter==0.9.3
|
||||||
xlwt==1.3.*
|
xlwt==1.3.*
|
||||||
xlrd==1.0.0
|
xlrd==1.0.0
|
||||||
|
pypiwin32 ; sys_platform == 'win32'
|
||||||
|
|||||||
Reference in New Issue
Block a user