Master chintan hiren mayank 13072018
This commit is contained in:
parent
604cf9e3e0
commit
7dd719a67f
@ -12,6 +12,9 @@ _logger = logging.getLogger(__name__)
|
||||
|
||||
try:
|
||||
import stdnum.eu.vat as stdnum_vat
|
||||
if not hasattr(stdnum_vat, "country_codes"):
|
||||
# stdnum version >= 1.9
|
||||
stdnum_vat.country_codes = stdnum_vat._country_codes
|
||||
except ImportError:
|
||||
_logger.warning('Python `stdnum` library not found, unable to call VIES service to detect address based on VAT number.')
|
||||
stdnum_vat = None
|
||||
@ -29,6 +32,11 @@ class ResPartner(models.Model):
|
||||
cp = lines.pop()
|
||||
city = lines.pop()
|
||||
return (cp, city)
|
||||
elif country == 'SE':
|
||||
result = re.match('([0-9]{3}\s?[0-9]{2})\s?([A-Z]+)', lines[-1])
|
||||
if result:
|
||||
lines.pop()
|
||||
return (result.group(1), result.group(2))
|
||||
else:
|
||||
result = re.match('((?:L-|AT-)?[0-9\-]+[A-Z]{,2}) (.+)', lines[-1])
|
||||
if result:
|
||||
|
@ -11,7 +11,7 @@
|
||||
<field name="arch" type="xml">
|
||||
<xpath expr="//field[@name='vat']" position="replace" />
|
||||
<field name="partner_id" position="after">
|
||||
<field name="vat" />
|
||||
<field name="vat" string="VAT"/>
|
||||
</field>
|
||||
</field>
|
||||
</record>
|
||||
|
@ -18,7 +18,7 @@
|
||||
<h3 class="oe_slogan">Gather all your Flectra reports in one app</h3>
|
||||
<div class="col-md-6">
|
||||
<p class="oe_mt32">
|
||||
After creating reports in each Odoo app, you can choose to make them as Favorite and adding those on your 'Dashboard'. Odoo Dashboard is the reporting module that gathers all your favorite graphs and tabs in one place. You company has no secret for you anymore, all your important figures are in front of you.
|
||||
After creating reports in each Flectra app, you can choose to make them as Favorite and adding those on your 'Dashboard'. Flectra Dashboard is the reporting module that gathers all your favorite graphs and tabs in one place. You company has no secret for you anymore, all your important figures are in front of you.
|
||||
</p>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
@ -40,7 +40,7 @@ After creating reports in each Odoo app, you can choose to make them as Favorite
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<p class="oe_mt32">
|
||||
In each Odoo App, you can create detailed reports and graphs in any format you like without the need of an external program. Get statistics on any numbers in your company, from fuel costs in projects all the way to revenues in sales channels, and with inventory, and everything in between.
|
||||
In each Flectra App, you can create detailed reports and graphs in any format you like without the need of an external program. Get statistics on any numbers in your company, from fuel costs in projects all the way to revenues in sales channels, and with inventory, and everything in between.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@ -52,7 +52,7 @@ In each Odoo App, you can create detailed reports and graphs in any format you l
|
||||
<h3 class="oe_slogan">Filter all results to fit your field of research</h3>
|
||||
<div class="col-md-12">
|
||||
<p class="oe_mt32">
|
||||
In each Odoo app, you'll need to create a report. In the reports you can filter and group each analysis using built-in filters, and create custom filters to gather only the information you are looking for. Save the filters you created in your favorites to access them anytime in just a click either from the concerned app, or in comparison with you other reports in the Odoo Dashboard app.
|
||||
In each Flectra app, you'll need to create a report. In the reports you can filter and group each analysis using built-in filters, and create custom filters to gather only the information you are looking for. Save the filters you created in your favorites to access them anytime in just a click either from the concerned app, or in comparison with you other reports in the Flectra Dashboard app.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -20,7 +20,7 @@ FormView.include({
|
||||
*/
|
||||
init: function (viewInfo) {
|
||||
this._super.apply(this, arguments);
|
||||
this.controllerParams.viewID = viewInfo.view_id;
|
||||
this.controllerParams.customViewID = viewInfo.custom_view_id;
|
||||
},
|
||||
});
|
||||
|
||||
@ -33,7 +33,7 @@ FormController.include({
|
||||
}),
|
||||
init: function (parent, model, renderer, params) {
|
||||
this._super.apply(this, arguments);
|
||||
this.viewID = params.viewID;
|
||||
this.customViewID = params.customViewID;
|
||||
},
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
@ -63,9 +63,9 @@ FormController.include({
|
||||
var board = this.renderer.getBoard();
|
||||
var arch = QWeb.render('DashBoard.xml', _.extend({}, board));
|
||||
return this._rpc({
|
||||
route: '/web/view/add_custom',
|
||||
route: '/web/view/edit_custom',
|
||||
params: {
|
||||
view_id: this.viewID,
|
||||
custom_id: this.customViewID,
|
||||
arch: arch,
|
||||
}
|
||||
}).then(dataManager.invalidate.bind(dataManager));
|
||||
@ -139,6 +139,31 @@ FormRenderer.include({
|
||||
this._super.apply(this, arguments);
|
||||
this.noContentHelp = params.noContentHelp;
|
||||
this.actionsDescr = {};
|
||||
this._boardSubcontrollers = []; // for board: controllers of subviews
|
||||
},
|
||||
/**
|
||||
* Call `on_attach_callback` for each subview
|
||||
*
|
||||
* @override
|
||||
*/
|
||||
on_attach_callback: function () {
|
||||
_.each(this._boardSubcontrollers, function (controller) {
|
||||
if ('on_attach_callback' in controller) {
|
||||
controller.on_attach_callback();
|
||||
}
|
||||
});
|
||||
},
|
||||
/**
|
||||
* Call `on_detach_callback` for each subview
|
||||
*
|
||||
* @override
|
||||
*/
|
||||
on_detach_callback: function () {
|
||||
_.each(this._boardSubcontrollers, function (controller) {
|
||||
if ('on_detach_callback' in controller) {
|
||||
controller.on_detach_callback();
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
@ -241,6 +266,7 @@ FormRenderer.include({
|
||||
hasSelectors: false,
|
||||
});
|
||||
return view.getController(self).then(function (controller) {
|
||||
self._boardSubcontrollers.push(controller);
|
||||
return controller.appendTo(params.$node);
|
||||
});
|
||||
});
|
||||
|
@ -94,7 +94,10 @@ FavoriteMenu.include({
|
||||
})
|
||||
.then(function (r) {
|
||||
if (r) {
|
||||
self.do_notify(_.str.sprintf(_t("'%s' added to dashboard"), name), '');
|
||||
self.do_notify(
|
||||
_.str.sprintf(_t("'%s' added to dashboard"), name),
|
||||
_t('Please refresh your browser for the changes to take effect.')
|
||||
);
|
||||
} else {
|
||||
self.do_warn(_t("Could not add filter to dashboard"));
|
||||
}
|
||||
|
@ -3,6 +3,7 @@ flectra.define('board.dashboard_tests', function (require) {
|
||||
|
||||
var testUtils = require('web.test_utils');
|
||||
var FormView = require('web.FormView');
|
||||
var ListRenderer = require('web.ListRenderer');
|
||||
|
||||
var createView = testUtils.createView;
|
||||
|
||||
@ -123,8 +124,8 @@ QUnit.test('basic functionality, with one sub action', function (assert) {
|
||||
if (route === '/web/dataset/search_read') {
|
||||
assert.deepEqual(args.domain, [['foo', '!=', 'False']], "the domain should be passed");
|
||||
}
|
||||
if (route === '/web/view/add_custom') {
|
||||
assert.step('add custom');
|
||||
if (route === '/web/view/edit_custom') {
|
||||
assert.step('edit custom');
|
||||
return $.when(true);
|
||||
}
|
||||
return this._super.apply(this, arguments);
|
||||
@ -155,7 +156,7 @@ QUnit.test('basic functionality, with one sub action', function (assert) {
|
||||
form.$('.oe_fold').click();
|
||||
|
||||
assert.ok(form.$('.oe_content').is(':visible'), "content is visible again");
|
||||
assert.verifySteps(['load action', 'add custom', 'add custom']);
|
||||
assert.verifySteps(['load action', 'edit custom', 'edit custom']);
|
||||
|
||||
assert.strictEqual($('.modal').length, 0, "should have no modal open");
|
||||
|
||||
@ -183,7 +184,7 @@ QUnit.test('basic functionality, with one sub action', function (assert) {
|
||||
assert.strictEqual($('.modal').length, 0, "should have no modal open");
|
||||
assert.strictEqual(form.$('.oe_action').length, 0, "should have no displayed action");
|
||||
|
||||
assert.verifySteps(['load action', 'add custom', 'add custom', 'add custom', 'add custom']);
|
||||
assert.verifySteps(['load action', 'edit custom', 'edit custom', 'edit custom', 'edit custom']);
|
||||
form.destroy();
|
||||
});
|
||||
|
||||
@ -292,8 +293,8 @@ QUnit.test('can drag and drop a view', function (assert) {
|
||||
views: [[4, 'list']],
|
||||
});
|
||||
}
|
||||
if (route === '/web/view/add_custom') {
|
||||
assert.step('add custom');
|
||||
if (route === '/web/view/edit_custom') {
|
||||
assert.step('edit custom');
|
||||
return $.when(true);
|
||||
}
|
||||
return this._super.apply(this, arguments);
|
||||
@ -339,8 +340,8 @@ QUnit.test('twice the same action in a dashboard', function (assert) {
|
||||
views: [[4, 'list'],[5, 'kanban']],
|
||||
});
|
||||
}
|
||||
if (route === '/web/view/add_custom') {
|
||||
assert.step('add custom');
|
||||
if (route === '/web/view/edit_custom') {
|
||||
assert.step('edit custom');
|
||||
return $.when(true);
|
||||
}
|
||||
return this._super.apply(this, arguments);
|
||||
@ -454,5 +455,48 @@ QUnit.test('clicking on a kanban\'s button should trigger the action', function
|
||||
form.destroy();
|
||||
});
|
||||
|
||||
QUnit.test('subviews are aware of attach in or detach from the DOM', function (assert) {
|
||||
assert.expect(2);
|
||||
|
||||
// patch list renderer `on_attach_callback` for the test only
|
||||
testUtils.patch(ListRenderer, {
|
||||
on_attach_callback: function () {
|
||||
assert.step('subview on_attach_callback');
|
||||
}
|
||||
});
|
||||
|
||||
var form = createView({
|
||||
View: FormView,
|
||||
model: 'board',
|
||||
data: this.data,
|
||||
arch: '<form string="My Dashboard">' +
|
||||
'<board style="2-1">' +
|
||||
'<column>' +
|
||||
'<action context="{}" view_mode="list" string="ABC" name="51" domain="[]"></action>' +
|
||||
'</column>' +
|
||||
'</board>' +
|
||||
'</form>',
|
||||
mockRPC: function (route) {
|
||||
if (route === '/web/action/load') {
|
||||
return $.when({
|
||||
res_model: 'partner',
|
||||
views: [[4, 'list']],
|
||||
});
|
||||
}
|
||||
return this._super.apply(this, arguments);
|
||||
},
|
||||
archs: {
|
||||
'partner,4,list':
|
||||
'<list string="Partner"><field name="foo"/></list>',
|
||||
},
|
||||
});
|
||||
|
||||
assert.verifySteps(['subview on_attach_callback']);
|
||||
|
||||
// restore on_attach_callback of ListRenderer
|
||||
testUtils.unpatch(ListRenderer);
|
||||
|
||||
form.destroy();
|
||||
});
|
||||
|
||||
});
|
||||
|
@ -1,4 +1,5 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo, Flectra. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from . import controllers
|
||||
from . import models
|
||||
|
4
addons/http_routing/controllers/__init__.py
Normal file
4
addons/http_routing/controllers/__init__.py
Normal file
@ -0,0 +1,4 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo, Flectra. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from . import main
|
23
addons/http_routing/controllers/main.py
Normal file
23
addons/http_routing/controllers/main.py
Normal file
@ -0,0 +1,23 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo, Flectra. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
import flectra
|
||||
|
||||
from flectra import http
|
||||
from flectra.http import request
|
||||
from flectra.osv import expression
|
||||
from flectra.addons.web.controllers.main import WebClient, Home
|
||||
|
||||
class Routing(Home):
|
||||
|
||||
@http.route('/website/translations', type='json', auth="public", website=True)
|
||||
def get_website_translations(self, lang, mods=None):
|
||||
Modules = request.env['ir.module.module'].sudo()
|
||||
IrHttp = request.env['ir.http'].sudo()
|
||||
domain = IrHttp._get_translation_frontend_modules_domain()
|
||||
modules = Modules.search(
|
||||
expression.AND([domain, [('state', '=', 'installed')]])
|
||||
).mapped('name')
|
||||
if mods:
|
||||
modules += mods
|
||||
return WebClient().translations(mods=modules, lang=lang)
|
64
addons/http_routing/geoipresolver.py
Normal file
64
addons/http_routing/geoipresolver.py
Normal file
@ -0,0 +1,64 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
import os.path
|
||||
|
||||
try:
|
||||
import GeoIP # Legacy
|
||||
except ImportError:
|
||||
GeoIP = None
|
||||
|
||||
try:
|
||||
import geoip2
|
||||
import geoip2.database
|
||||
except ImportError:
|
||||
geoip2 = None
|
||||
|
||||
class GeoIPResolver(object):
|
||||
def __init__(self, fname):
|
||||
self.fname = fname
|
||||
try:
|
||||
self._db = geoip2.database.Reader(fname)
|
||||
self.version = 2
|
||||
except Exception:
|
||||
try:
|
||||
self._db = GeoIP.open(fname, GeoIP.GEOIP_STANDARD)
|
||||
self.version = 1
|
||||
assert self._db.database_info is not None
|
||||
except Exception:
|
||||
raise ValueError('Invalid GeoIP database: %r' % fname)
|
||||
|
||||
def __del__(self):
|
||||
if self.version == 2:
|
||||
self._db.close()
|
||||
|
||||
@classmethod
|
||||
def open(cls, fname):
|
||||
if not GeoIP and not geoip2:
|
||||
return None
|
||||
if not os.path.exists(fname):
|
||||
return None
|
||||
return GeoIPResolver(fname)
|
||||
|
||||
def resolve(self, ip):
|
||||
if self.version == 1:
|
||||
return self._db.record_by_addr(ip) or {}
|
||||
elif self.version == 2:
|
||||
try:
|
||||
r = self._db.city(ip)
|
||||
except (ValueError, geoip2.errors.AddressNotFoundError):
|
||||
return {}
|
||||
# Compatibility with Legacy database.
|
||||
# Some ips cannot be located to a specific country. Legacy DB used to locate them in
|
||||
# continent instead of country. Do the same to not change behavior of existing code.
|
||||
country, attr = (r.country, 'iso_code') if r.country.geoname_id else (r.continent, 'code')
|
||||
return {
|
||||
'city': r.city.name,
|
||||
'country_code': getattr(country, attr),
|
||||
'country_name': country.name,
|
||||
'region': r.subdivisions[0].iso_code if r.subdivisions else None,
|
||||
'time_zone': r.location.time_zone,
|
||||
}
|
||||
|
||||
# compat
|
||||
def record_by_addr(self, addr):
|
||||
return self.resolve(addr)
|
@ -18,6 +18,8 @@ from flectra.addons.base.ir.ir_http import RequestUID, ModelConverter
|
||||
from flectra.http import request
|
||||
from flectra.tools import config, ustr, pycompat
|
||||
|
||||
from ..geoipresolver import GeoIPResolver
|
||||
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
# global resolver (GeoIP API is thread-safe, for multithreaded workers)
|
||||
@ -229,6 +231,13 @@ class IrHttp(models.AbstractModel):
|
||||
return request.env['res.lang'].search([('code', '=', lang_code)], limit=1)
|
||||
return request.env['res.lang'].search([], limit=1)
|
||||
|
||||
@classmethod
|
||||
def _get_translation_frontend_modules_domain(cls):
|
||||
""" Return a domain to list the domain adding web-translations and
|
||||
dynamic resources that may be used frontend views
|
||||
"""
|
||||
return []
|
||||
|
||||
bots = "bot|crawl|slurp|spider|curl|wget|facebookexternalhit".split("|")
|
||||
|
||||
@classmethod
|
||||
@ -258,25 +267,18 @@ class IrHttp(models.AbstractModel):
|
||||
# Lazy init of GeoIP resolver
|
||||
if flectra._geoip_resolver is not None:
|
||||
return
|
||||
geofile = config.get('geoip_database')
|
||||
try:
|
||||
import GeoIP
|
||||
# updated database can be downloaded on MaxMind website
|
||||
# http://dev.maxmind.com/geoip/legacy/install/city/
|
||||
geofile = config.get('geoip_database')
|
||||
if os.path.exists(geofile):
|
||||
flectra._geoip_resolver = GeoIP.open(geofile, GeoIP.GEOIP_STANDARD)
|
||||
else:
|
||||
flectra._geoip_resolver = False
|
||||
_logger.warning('GeoIP database file %r does not exists, apt-get install geoip-database-contrib or download it from http://dev.maxmind.com/geoip/legacy/install/city/', geofile)
|
||||
except ImportError:
|
||||
flectra._geoip_resolver = False
|
||||
flectra._geoip_resolver = GeoIPResolver.open(geofile) or False
|
||||
except Exception as e:
|
||||
_logger.warning('Cannot load GeoIP: %s', ustr(e))
|
||||
|
||||
@classmethod
|
||||
def _geoip_resolve(cls):
|
||||
if 'geoip' not in request.session:
|
||||
record = {}
|
||||
if flectra._geoip_resolver and request.httprequest.remote_addr:
|
||||
record = flectra._geoip_resolver.record_by_addr(request.httprequest.remote_addr) or {}
|
||||
record = flectra._geoip_resolver.resolve(request.httprequest.remote_addr) or {}
|
||||
request.session['geoip'] = record
|
||||
|
||||
@classmethod
|
||||
|
@ -8,6 +8,7 @@
|
||||
<field name="name">Pos Orders Inalterability Check</field>
|
||||
<field name="model_id" ref="point_of_sale.model_pos_order"/>
|
||||
<field name="type">ir.actions.server</field>
|
||||
<field name="state">code</field>
|
||||
<field name="code">
|
||||
action = env['pos.order']._check_hash_integrity(env.user.company_id.id)
|
||||
</field>
|
||||
|
@ -1,6 +1,6 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo, Flectra. See LICENSE file for full copyright and licensing details.
|
||||
from flectra import models
|
||||
from flectra import models, api
|
||||
from flectra.tools.translate import _
|
||||
from flectra.exceptions import UserError
|
||||
|
||||
@ -8,6 +8,7 @@ from flectra.exceptions import UserError
|
||||
class AccountBankStatement(models.Model):
|
||||
_inherit = 'account.bank.statement'
|
||||
|
||||
@api.multi
|
||||
def unlink(self):
|
||||
for statement in self.filtered(lambda s: s.company_id._is_accounting_unalterable() and s.journal_id.journal_user):
|
||||
raise UserError(_('You cannot modify anything on a bank statement (name: %s) that was created by point of sale operations.') % (statement.name,))
|
||||
@ -17,6 +18,7 @@ class AccountBankStatement(models.Model):
|
||||
class AccountBankStatementLine(models.Model):
|
||||
_inherit = 'account.bank.statement.line'
|
||||
|
||||
@api.multi
|
||||
def unlink(self):
|
||||
for line in self.filtered(lambda s: s.company_id._is_accounting_unalterable() and s.journal_id.journal_user):
|
||||
raise UserError(_('You cannot modify anything on a bank statement line (name: %s) that was created by point of sale operations.') % (line.name,))
|
||||
|
@ -1,2 +1,8 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo, Flectra. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
|
||||
def uninstall_hook(cr, registry):
|
||||
cr.execute(
|
||||
"DELETE FROM ir_model_data WHERE module = 'l10n_generic_coa'"
|
||||
)
|
||||
|
@ -29,4 +29,5 @@ Install some generic chart of accounts.
|
||||
'../account/demo/account_invoice_demo.yml',
|
||||
],
|
||||
'website': 'https://flectrahq.com/page/accounting',
|
||||
'uninstall_hook': 'uninstall_hook',
|
||||
}
|
||||
|
@ -98,7 +98,7 @@
|
||||
<t t-set="total_cess" t-value="total_cess + tax_data['amount']" t-options="{'widget': 'monetary', 'display_currency': o.currency_id}"/>
|
||||
</t>
|
||||
</t>
|
||||
<t t-esc="total_cess"/>
|
||||
<t t-esc="total_cess" t-options="{'widget': 'monetary', 'display_currency': o.currency_id}"/>
|
||||
</td>
|
||||
</t>
|
||||
</tr>
|
||||
|
@ -48,7 +48,7 @@
|
||||
</h3>
|
||||
<img class="oe_picture oe_screenshot" src="note_sc_03a.png">
|
||||
<p class="text-justify">
|
||||
Odoo Notes helps you stay organized and focused on your work with a structured and accessible system. Break down your tasks and projects into actionable work items and achieve your goals more efficiently
|
||||
Flectra Notes helps you stay organized and focused on your work with a structured and accessible system. Break down your tasks and projects into actionable work items and achieve your goals more efficiently
|
||||
</p>
|
||||
</div>
|
||||
<div class="col-md-4 oe_mt32">
|
||||
@ -78,7 +78,7 @@
|
||||
<h3 class="oe_slogan">Flectra Notes adapts to your needs and habits</h3>
|
||||
<div class="col-md-6">
|
||||
<p class="oe_mt32 text-justify">
|
||||
Odoo Notes' smart kanban view allows every user to customize their own steps in order to process their to-do lists and notes. Choose the default template or create your own steps/stages of the process.
|
||||
Flectra Notes' smart kanban view allows every user to customize their own steps in order to process their to-do lists and notes. Choose the default template or create your own steps/stages of the process.
|
||||
</p>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
|
@ -114,7 +114,7 @@
|
||||
<!-- title -->
|
||||
<field name="name"/>
|
||||
<div class="oe_kanban_bottom_right">
|
||||
<div class="o_kanban_inline_block mt16 mr4">
|
||||
<div class="o_kanban_inline_block mr4">
|
||||
<field name="activity_ids" widget="kanban_activity" />
|
||||
</div>
|
||||
</div>
|
||||
|
@ -10,7 +10,6 @@ import requests
|
||||
|
||||
from flectra import api, models, _
|
||||
from flectra.exceptions import UserError
|
||||
from flectra.tools import html2plaintext
|
||||
|
||||
from ..py_etherpad import EtherpadLiteClient
|
||||
|
||||
@ -76,7 +75,7 @@ class PadCommon(models.AbstractModel):
|
||||
@api.model
|
||||
def pad_get_content(self, url):
|
||||
company = self.env.user.sudo().company_id
|
||||
myPad = EtherpadLiteClient(company.pad_key, company.pad_server + '/api')
|
||||
myPad = EtherpadLiteClient(company.pad_key, (company.pad_server or '') + '/api')
|
||||
content = ''
|
||||
if url:
|
||||
split_url = url.split('/p/')
|
||||
@ -133,7 +132,7 @@ class PadCommon(models.AbstractModel):
|
||||
for k, field in self._fields.items():
|
||||
if hasattr(field, 'pad_content_field') and vals.get(field.pad_content_field) and self[k]:
|
||||
company = self.env.user.sudo().company_id
|
||||
myPad = EtherpadLiteClient(company.pad_key, company.pad_server + '/api')
|
||||
myPad = EtherpadLiteClient(company.pad_key, (company.pad_server or '') + '/api')
|
||||
path = self[k].split('/p/')[1]
|
||||
myPad.setHtmlFallbackText(path, vals[field.pad_content_field])
|
||||
|
||||
|
@ -9,7 +9,7 @@
|
||||
"node":"*"
|
||||
},
|
||||
"author":{
|
||||
"name":"Flectra S.A. - Hitesh Trivedi",
|
||||
"name":"Odoo S.A. - Hitesh Trivedi",
|
||||
"email":"thiteshm155@gmail.com"
|
||||
}
|
||||
}
|
||||
|
@ -17,4 +17,4 @@ module.exports = function(grunt) {
|
||||
|
||||
grunt.registerTask('default', ['jshint']);
|
||||
|
||||
};
|
||||
};
|
@ -28,6 +28,7 @@ import werkzeug.wsgi
|
||||
from collections import OrderedDict
|
||||
from werkzeug.urls import url_decode, iri_to_uri
|
||||
from xml.etree import ElementTree
|
||||
import unicodedata
|
||||
|
||||
|
||||
import flectra
|
||||
@ -1007,14 +1008,17 @@ class DataSet(http.Controller):
|
||||
|
||||
class View(http.Controller):
|
||||
|
||||
@http.route('/web/view/add_custom', type='json', auth="user")
|
||||
def add_custom(self, view_id, arch):
|
||||
CustomView = request.env['ir.ui.view.custom']
|
||||
CustomView.create({
|
||||
'user_id': request.session.uid,
|
||||
'ref_id': view_id,
|
||||
'arch': arch
|
||||
})
|
||||
@http.route('/web/view/edit_custom', type='json', auth="user")
|
||||
def edit_custom(self, custom_id, arch):
|
||||
"""
|
||||
Edit a custom view
|
||||
|
||||
:param int custom_id: the id of the edited custom view
|
||||
:param str arch: the edited arch of the custom view
|
||||
:returns: dict with acknowledged operation (result set to True)
|
||||
"""
|
||||
custom_view = request.env['ir.ui.view.custom'].browse(custom_id)
|
||||
custom_view.write({ 'arch': arch })
|
||||
return {'result': True}
|
||||
|
||||
class Binary(http.Controller):
|
||||
@ -1039,7 +1043,7 @@ class Binary(http.Controller):
|
||||
'/web/content/<string:model>/<int:id>/<string:field>/<string:filename>'], type='http', auth="public")
|
||||
def content_common(self, xmlid=None, model='ir.attachment', id=None, field='datas',
|
||||
filename=None, filename_field='datas_fname', unique=None, mimetype=None,
|
||||
download=None, data=None, token=None, access_token=None):
|
||||
download=None, data=None, token=None, access_token=None, **kw):
|
||||
status, headers, content = binary_content(
|
||||
xmlid=xmlid, model=model, id=id, field=field, unique=unique, filename=filename,
|
||||
filename_field=filename_field, download=download, mimetype=mimetype,
|
||||
@ -1089,8 +1093,12 @@ class Binary(http.Controller):
|
||||
elif status != 200 and download:
|
||||
return request.not_found()
|
||||
|
||||
height = int(height or 0)
|
||||
width = int(width or 0)
|
||||
if headers and dict(headers).get('Content-Type', '') == 'image/svg+xml': # we shan't resize svg images
|
||||
height = 0
|
||||
width = 0
|
||||
else:
|
||||
height = int(height or 0)
|
||||
width = int(width or 0)
|
||||
|
||||
if crop and (width or height):
|
||||
content = crop_image(content, type='center', size=(width, height), ratio=(1, 1))
|
||||
@ -1153,20 +1161,27 @@ class Binary(http.Controller):
|
||||
</script>"""
|
||||
args = []
|
||||
for ufile in files:
|
||||
|
||||
filename = ufile.filename
|
||||
if request.httprequest.user_agent.browser == 'safari':
|
||||
# Safari sends NFD UTF-8 (where é is composed by 'e' and [accent])
|
||||
# we need to send it the same stuff, otherwise it'll fail
|
||||
filename = unicodedata.normalize('NFD', ufile.filename)
|
||||
|
||||
try:
|
||||
attachment = Model.create({
|
||||
'name': ufile.filename,
|
||||
'name': filename,
|
||||
'datas': base64.encodestring(ufile.read()),
|
||||
'datas_fname': ufile.filename,
|
||||
'datas_fname': filename,
|
||||
'res_model': model,
|
||||
'res_id': int(id)
|
||||
})
|
||||
except Exception:
|
||||
args = args.append({'error': _("Something horrible happened")})
|
||||
args.append({'error': _("Something horrible happened")})
|
||||
_logger.exception("Fail to upload attachment %s" % ufile.filename)
|
||||
else:
|
||||
args.append({
|
||||
'filename': ufile.filename,
|
||||
'filename': filename,
|
||||
'mimetype': ufile.content_type,
|
||||
'id': attachment.id
|
||||
})
|
||||
@ -1428,7 +1443,7 @@ class ExportFormat(object):
|
||||
model, fields, ids, domain, import_compat = \
|
||||
operator.itemgetter('model', 'fields', 'ids', 'domain', 'import_compat')(params)
|
||||
|
||||
Model = request.env[model].with_context(**params.get('context', {}))
|
||||
Model = request.env[model].with_context(import_compat=import_compat, **params.get('context', {}))
|
||||
records = Model.browse(ids) or Model.search(domain, offset=0, limit=False, order=False)
|
||||
|
||||
if not Model._is_an_ordinary_table():
|
||||
|
@ -2,6 +2,7 @@
|
||||
# Part of Odoo, Flectra. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
import hashlib
|
||||
from collections import OrderedDict
|
||||
|
||||
from flectra import api, models
|
||||
from flectra.tools import pycompat
|
||||
@ -56,6 +57,24 @@ class Image(models.AbstractModel):
|
||||
elif options.get('zoom'):
|
||||
src_zoom = options['zoom']
|
||||
|
||||
img = '<img class="%s" src="%s" style="%s"%s%s/>' % \
|
||||
(classes, src, options.get('style', ''), ' alt="%s"' % alt if alt else '', ' data-zoom="1" data-zoom-image="%s"' % src_zoom if src_zoom else '')
|
||||
return pycompat.to_text(img)
|
||||
atts = OrderedDict()
|
||||
atts["src"] = src
|
||||
atts["class"] = classes
|
||||
atts["style"] = options.get('style')
|
||||
atts["alt"] = alt
|
||||
atts["data-zoom"] = src_zoom and u'1' or None
|
||||
atts["data-zoom-image"] = src_zoom
|
||||
|
||||
atts = self.env['ir.qweb']._post_processing_att('img', atts, options.get('template_options'))
|
||||
|
||||
img = ['<img']
|
||||
for name, value in atts.items():
|
||||
if value:
|
||||
img.append(' ')
|
||||
img.append(escape(pycompat.to_text(name)))
|
||||
img.append('="')
|
||||
img.append(escape(pycompat.to_text(value)))
|
||||
img.append('"')
|
||||
img.append('/>')
|
||||
|
||||
return u''.join(img)
|
||||
|
@ -170,4 +170,4 @@ hr {
|
||||
|
||||
[role="button"] {
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
26
addons/web/static/lib/qweb/qweb-test-widgets.xml
Normal file
26
addons/web/static/lib/qweb/qweb-test-widgets.xml
Normal file
@ -0,0 +1,26 @@
|
||||
<templates>
|
||||
<t t-name="date-simple"><t t-esc='value' /></t>
|
||||
<params id="date-simple">{"value": "1988-09-16"}</params>
|
||||
<result id="date-simple">1988-09-16</result>
|
||||
|
||||
<t t-name="datetime-simple"><t t-esc='value' /></t>
|
||||
<params id="datetime-simple">{"value": "1988-09-16 14:00:00"}</params>
|
||||
<result id="datetime-simple">1988-09-16 14:00:00</result>
|
||||
|
||||
<t t-name="datetime-widget-datetime"><t t-esc='value' t-options="{'widget': 'datetime'}" /></t>
|
||||
<params id="datetime-widget-datetime">{"value": "1988-09-16 14:00:00"}</params>
|
||||
<result id="datetime-widget-datetime">09/16/1988 16:00:00</result>
|
||||
|
||||
<t t-name="datetime-widget-date"><t t-esc='value' t-options="{'widget': 'date'}" /></t>
|
||||
<params id="datetime-widget-date">{"value": "1988-09-16 14:00:00"}</params>
|
||||
<result id="datetime-widget-date">09/16/1988</result>
|
||||
|
||||
<t t-name="datetime-widget-date-tz2"><t t-esc='value' t-options="{'widget': 'date'}" /></t>
|
||||
<params id="datetime-widget-date-tz2">{"value": "1988-09-16 01:00:00"}</params>
|
||||
<result id="datetime-widget-date-tz2">09/16/1988</result>
|
||||
|
||||
<t t-name="datetime-widget-date-tz"><t t-esc='value' t-options="{'widget': 'date'}" /></t>
|
||||
<params id="datetime-widget-date-tz">{"value": "1988-09-16 23:00:00"}</params>
|
||||
<result id="datetime-widget-date-tz">09/17/1988</result>
|
||||
|
||||
</templates>
|
@ -165,7 +165,7 @@ var AbstractWebClient = Widget.extend(mixins.ServiceProvider, {
|
||||
core.bus.on('connection_restored', this, this.on_connection_restored);
|
||||
|
||||
// crash manager integration
|
||||
session.on('error', crash_manager, crash_manager.rpc_error);
|
||||
core.bus.on('rpc_error', crash_manager, crash_manager.rpc_error);
|
||||
window.onerror = function (message, file, line, col, error) {
|
||||
// Scripts injected in DOM (eg: google API's js files) won't return a clean error on window.onerror.
|
||||
// The browser will just give you a 'Script error.' as message and nothing else for security issue.
|
||||
|
@ -26,9 +26,9 @@ var Loading = Widget.extend({
|
||||
this._super(parent);
|
||||
this.count = 0;
|
||||
this.blocked_ui = false;
|
||||
session.on("request", this, this.request_call);
|
||||
session.on("response", this, this.response_call);
|
||||
session.on("response_failed", this, this.response_call);
|
||||
core.bus.on('rpc_request', this, this.request_call);
|
||||
core.bus.on("rpc_response", this, this.response_call);
|
||||
core.bus.on("rpc_response_failed", this, this.response_call);
|
||||
},
|
||||
destroy: function() {
|
||||
this.on_rpc_event(-this.count);
|
||||
|
@ -267,11 +267,14 @@ var Menu = Widget.extend({
|
||||
* @param {Number} id the action_id to match
|
||||
* @param {Number} [menuID] a menu ID that may match with provided action
|
||||
*/
|
||||
open_action: function (id) {
|
||||
open_action: function (id, menuID) {
|
||||
var $menu = this.$el.add(this.$secondary_menus).find('a[data-action-id="' + id + '"]');
|
||||
var menu_id = $menu.data('menu');
|
||||
if (menu_id) {
|
||||
this.open_menu(menu_id);
|
||||
if (!(menuID && $menu.filter("[data-menu='" + menuID + "']").length)) {
|
||||
// menuID doesn't match action, so pick first menu_item
|
||||
menuID = $menu.data('menu');
|
||||
}
|
||||
if (menuID) {
|
||||
this.open_menu(menuID);
|
||||
}
|
||||
},
|
||||
/**
|
||||
|
@ -263,7 +263,7 @@ var ViewManager = Widget.extend(ControlPanelMixin, {
|
||||
|
||||
self.active_view = view;
|
||||
|
||||
if (!view.loaded) {
|
||||
if (!view.loaded || view.loaded.state() === 'rejected') {
|
||||
view_options = _.extend({}, view.options, view_options, self.env);
|
||||
view.loaded = $.Deferred();
|
||||
self.create_view(view, view_options).then(function(controller) {
|
||||
@ -638,7 +638,7 @@ var ViewManager = Widget.extend(ControlPanelMixin, {
|
||||
* active controller.
|
||||
*
|
||||
* @private
|
||||
* @param {OdooEvent} ev
|
||||
* @param {FlectraEvent} ev
|
||||
* @param {function} ev.data.callback used to send the requested context
|
||||
*/
|
||||
_onGetControllerContext: function (ev) {
|
||||
|
@ -103,6 +103,10 @@ return AbstractWebClient.extend({
|
||||
bind_hashchange: function() {
|
||||
var self = this;
|
||||
$(window).bind('hashchange', this.on_hashchange);
|
||||
var didHashChanged = false;
|
||||
$(window).one('hashchange', function () {
|
||||
didHashChanged = true;
|
||||
});
|
||||
|
||||
var state = $.bbq.getState(true);
|
||||
if (_.isEmpty(state) || state.action === "login") {
|
||||
@ -113,6 +117,9 @@ return AbstractWebClient.extend({
|
||||
args: [[session.uid], ['action_id']],
|
||||
})
|
||||
.done(function(result) {
|
||||
if (didHashChanged) {
|
||||
return;
|
||||
}
|
||||
var data = result[0];
|
||||
if(data.action_id) {
|
||||
self.action_manager.do_action(data.action_id[0]);
|
||||
|
@ -5,7 +5,12 @@ var core = require('web.core');
|
||||
var utils = require('web.utils');
|
||||
var time = require('web.time');
|
||||
|
||||
function genericJsonRpc (fct_name, params, fct) {
|
||||
function genericJsonRpc (fct_name, params, settings, fct) {
|
||||
var shadow = settings.shadow || false;
|
||||
delete settings.shadow;
|
||||
if (! shadow)
|
||||
core.bus.trigger('rpc_request');
|
||||
|
||||
var data = {
|
||||
jsonrpc: "2.0",
|
||||
method: fct_name,
|
||||
@ -30,11 +35,49 @@ function genericJsonRpc (fct_name, params, fct) {
|
||||
});
|
||||
// FIXME: jsonp?
|
||||
result.abort = function () { if (xhr.abort) xhr.abort(); };
|
||||
return result;
|
||||
|
||||
var p = result.then(function (result) {
|
||||
if (!shadow) {
|
||||
core.bus.trigger('rpc_response');
|
||||
}
|
||||
return result;
|
||||
}, function (type, error, textStatus, errorThrown) {
|
||||
if (type === "server") {
|
||||
if (!shadow) {
|
||||
core.bus.trigger('rpc_response');
|
||||
}
|
||||
if (error.code === 100) {
|
||||
core.bus.trigger('invalidate_session');
|
||||
}
|
||||
return $.Deferred().reject(error, $.Event());
|
||||
} else {
|
||||
if (!shadow) {
|
||||
core.bus.trigger('rpc_response_failed');
|
||||
}
|
||||
var nerror = {
|
||||
code: -32098,
|
||||
message: "XmlHttpRequestError " + errorThrown,
|
||||
data: {
|
||||
type: "xhr"+textStatus,
|
||||
debug: error.responseText,
|
||||
objects: [error, errorThrown]
|
||||
},
|
||||
};
|
||||
return $.Deferred().reject(nerror, $.Event());
|
||||
}
|
||||
});
|
||||
return p.fail(function () { // Allow deferred user to disable rpc_error call in fail
|
||||
p.fail(function (error, event) {
|
||||
if (!event.isDefaultPrevented()) {
|
||||
core.bus.trigger('rpc_error', error, event);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function jsonRpc(url, fct_name, params, settings) {
|
||||
return genericJsonRpc(fct_name, params, function(data) {
|
||||
settings = settings || {};
|
||||
return genericJsonRpc(fct_name, params, settings, function(data) {
|
||||
return $.ajax(url, _.extend({}, settings, {
|
||||
url: url,
|
||||
dataType: 'json',
|
||||
@ -47,7 +90,7 @@ function jsonRpc(url, fct_name, params, settings) {
|
||||
|
||||
function jsonpRpc(url, fct_name, params, settings) {
|
||||
settings = settings || {};
|
||||
return genericJsonRpc(fct_name, params, function(data) {
|
||||
return genericJsonRpc(fct_name, params, settings, function(data) {
|
||||
var payload_str = JSON.stringify(data, time.date_to_utc);
|
||||
var payload_url = $.param({r:payload_str});
|
||||
var force2step = settings.force2step || false;
|
||||
@ -260,8 +303,20 @@ function get_file(options) {
|
||||
if (options.error) {
|
||||
var body = this.contentDocument.body;
|
||||
var nodes = body.children.length === 0 ? body.childNodes : body.children;
|
||||
var node = nodes[1] || nodes[0];
|
||||
options.error(JSON.parse(node.textContent));
|
||||
var errorParams = {};
|
||||
|
||||
try { // Case of a serialized Flectra Exception: It is Json Parsable
|
||||
var node = nodes[1] || nodes[0];
|
||||
errorParams = JSON.parse(node.textContent);
|
||||
} catch (e) { // Arbitrary uncaught python side exception
|
||||
errorParams = {
|
||||
message: nodes.length > 1 ? nodes[1].textContent : '',
|
||||
data: {
|
||||
title: nodes.length > 0 ? nodes[0].textContent : '',
|
||||
}
|
||||
}
|
||||
}
|
||||
options.error(errorParams);
|
||||
}
|
||||
} finally {
|
||||
complete();
|
||||
|
@ -52,19 +52,19 @@ return {
|
||||
|
||||
if (options.method === 'read_group') {
|
||||
if (!(params.args && params.args[0] !== undefined)) {
|
||||
params.kwargs.domain = options.domain || params.domain || params.kwargs.domain || [];
|
||||
params.kwargs.domain = options.domain || params.domain || params.kwargs.domain || [];
|
||||
}
|
||||
if (!(params.args && params.args[1] !== undefined)) {
|
||||
params.kwargs.fields = options.fields || params.fields || params.kwargs.fields || [];
|
||||
params.kwargs.fields = options.fields || params.fields || params.kwargs.fields || [];
|
||||
}
|
||||
if (!(params.args && params.args[2] !== undefined)) {
|
||||
params.kwargs.groupby = options.groupBy || params.groupBy || params.kwargs.groupby || [];
|
||||
params.kwargs.groupby = options.groupBy || params.groupBy || params.kwargs.groupby || [];
|
||||
}
|
||||
params.kwargs.offset = options.offset || params.offset || params.kwargs.offset;
|
||||
params.kwargs.limit = options.limit || params.limit || params.kwargs.limit;
|
||||
params.kwargs.offset = options.offset || params.offset || params.kwargs.offset;
|
||||
params.kwargs.limit = options.limit || params.limit || params.kwargs.limit;
|
||||
// In kwargs, we look for "orderby" rather than "orderBy" (note the absence of capital B),
|
||||
// since the Python argument to the actual function is "orderby".
|
||||
var orderBy = options.orderBy || params.orderBy || params.kwargs.orderby;
|
||||
var orderBy = options.orderBy || params.orderBy || params.kwargs.orderby;
|
||||
params.kwargs.orderby = orderBy ? this._serializeSort(orderBy) : orderBy;
|
||||
params.kwargs.lazy = 'lazy' in options ? options.lazy : params.lazy;
|
||||
}
|
||||
@ -72,7 +72,7 @@ return {
|
||||
if (options.method === 'search_read') {
|
||||
// call the model method
|
||||
params.kwargs.domain = options.domain || params.domain || params.kwargs.domain;
|
||||
params.kwargs.fields = options.fields || params.fields || params.kwargs.fields;
|
||||
params.kwargs.fields = options.fields || params.fields || params.kwargs.fields;
|
||||
params.kwargs.offset = options.offset || params.offset || params.kwargs.offset;
|
||||
params.kwargs.limit = options.limit || params.limit || params.kwargs.limit;
|
||||
// In kwargs, we look for "order" rather than "orderBy" since the Python
|
||||
|
@ -49,6 +49,7 @@ var Session = core.Class.extend(mixins.EventDispatcherMixin, {
|
||||
this.qweb_mutex = new concurrency.Mutex();
|
||||
this.currencies = {};
|
||||
this._groups_def = {};
|
||||
core.bus.on('invalidate_session', this, this._onInvalidateSession);
|
||||
},
|
||||
setup: function (origin, options) {
|
||||
// must be able to customize server
|
||||
@ -327,23 +328,17 @@ var Session = core.Class.extend(mixins.EventDispatcherMixin, {
|
||||
rpc: function (url, params, options) {
|
||||
var self = this;
|
||||
options = _.clone(options || {});
|
||||
var shadow = options.shadow || false;
|
||||
options.headers = _.extend({}, options.headers);
|
||||
if (flectra.debug) {
|
||||
options.headers["X-Debug-Mode"] = $.deparam($.param.querystring()).debug;
|
||||
}
|
||||
|
||||
delete options.shadow;
|
||||
|
||||
return self.check_session_id().then(function () {
|
||||
// TODO: remove
|
||||
if (! _.isString(url)) {
|
||||
_.extend(options, url);
|
||||
url = url.url;
|
||||
}
|
||||
// TODO correct handling of timeouts
|
||||
if (! shadow)
|
||||
self.trigger('request');
|
||||
var fct;
|
||||
if (self.origin_server) {
|
||||
fct = ajax.jsonRpc;
|
||||
@ -362,37 +357,7 @@ var Session = core.Class.extend(mixins.EventDispatcherMixin, {
|
||||
url = self.url(url, null);
|
||||
options.session_id = self.session_id || '';
|
||||
}
|
||||
var p = fct(url, "call", params, options);
|
||||
p = p.then(function (result) {
|
||||
if (! shadow)
|
||||
self.trigger('response');
|
||||
return result;
|
||||
}, function (type, error, textStatus, errorThrown) {
|
||||
if (type === "server") {
|
||||
if (! shadow)
|
||||
self.trigger('response');
|
||||
if (error.code === 100) {
|
||||
self.uid = false;
|
||||
}
|
||||
return $.Deferred().reject(error, $.Event());
|
||||
} else {
|
||||
if (! shadow)
|
||||
self.trigger('response_failed');
|
||||
var nerror = {
|
||||
code: -32098,
|
||||
message: "XmlHttpRequestError " + errorThrown,
|
||||
data: {type: "xhr"+textStatus, debug: error.responseText, objects: [error, errorThrown] }
|
||||
};
|
||||
return $.Deferred().reject(nerror, $.Event());
|
||||
}
|
||||
});
|
||||
return p.fail(function () { // Allow deferred user to disable rpc_error call in fail
|
||||
p.fail(function (error, event) {
|
||||
if (!event.isDefaultPrevented()) {
|
||||
self.trigger('error', error, event);
|
||||
}
|
||||
});
|
||||
});
|
||||
return fct(url, "call", params, options);
|
||||
});
|
||||
},
|
||||
url: function (path, params) {
|
||||
@ -418,6 +383,17 @@ var Session = core.Class.extend(mixins.EventDispatcherMixin, {
|
||||
getTZOffset: function (date) {
|
||||
return -new Date(date).getTimezoneOffset();
|
||||
},
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
// Handlers
|
||||
//--------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
_onInvalidateSession: function () {
|
||||
this.uid = false;
|
||||
},
|
||||
});
|
||||
|
||||
return Session;
|
||||
|
@ -468,7 +468,13 @@ var FieldDate = InputField.extend({
|
||||
* @private
|
||||
*/
|
||||
_makeDatePicker: function () {
|
||||
return new datepicker.DateWidget(this, {defaultDate: this.value});
|
||||
return new datepicker.DateWidget(
|
||||
this,
|
||||
_.defaults(
|
||||
this.nodeOptions.datepicker || {},
|
||||
{defaultDate: this.value}
|
||||
)
|
||||
);
|
||||
},
|
||||
|
||||
/**
|
||||
@ -1013,6 +1019,20 @@ var FieldPhone = FieldEmail.extend({
|
||||
this.$el.removeClass('o_text_overflow');
|
||||
},
|
||||
|
||||
/**
|
||||
* Remove possibly present ­ characters when saving number
|
||||
*
|
||||
* @override
|
||||
* @private
|
||||
*/
|
||||
_setValue: function (value, options) {
|
||||
if (value) {
|
||||
// remove possibly pasted ­ characters
|
||||
value = value.replace(/\u00AD/g, '');
|
||||
}
|
||||
return this._super(value, options);
|
||||
},
|
||||
|
||||
/**
|
||||
* Phone fields are clickable in readonly on small screens ~= on phones.
|
||||
* This can be overriden by call-capable modules to display a clickable
|
||||
@ -1068,6 +1088,7 @@ var UrlWidget = InputField.extend({
|
||||
_renderReadonly: function () {
|
||||
this.$el.text(this.attrs.text || this.value)
|
||||
.addClass('o_form_uri o_text_overflow')
|
||||
.attr('target', '_blank')
|
||||
.attr('href', this.value);
|
||||
}
|
||||
});
|
||||
@ -1211,9 +1232,6 @@ var FieldBinaryImage = AbstractFieldBinary.extend({
|
||||
self.do_warn(_t("Image"), _t("Could not display the selected image."));
|
||||
});
|
||||
},
|
||||
isSet: function () {
|
||||
return true;
|
||||
},
|
||||
});
|
||||
|
||||
var FieldBinaryFile = AbstractFieldBinary.extend({
|
||||
@ -2412,6 +2430,21 @@ var AceEditor = DebouncedField.extend({
|
||||
// Private
|
||||
//--------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Format value
|
||||
*
|
||||
* Note: We have to overwrite this method to always return a string.
|
||||
* AceEditor works with string and not boolean value.
|
||||
*
|
||||
* @override
|
||||
* @private
|
||||
* @param {boolean|string} value
|
||||
* @returns {string}
|
||||
*/
|
||||
_formatValue: function (value) {
|
||||
return this._super.apply(this, arguments) || '';
|
||||
},
|
||||
|
||||
/**
|
||||
* @override
|
||||
* @private
|
||||
@ -2434,6 +2467,7 @@ var AceEditor = DebouncedField.extend({
|
||||
this.aceSession.setValue(newValue);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Starts the ace library on the given DOM element. This initializes the
|
||||
* ace editor option according to the edit/readonly mode and binds ace
|
||||
|
@ -26,6 +26,7 @@ registry
|
||||
.add('datetime', basic_fields.FieldDateTime)
|
||||
.add('domain', basic_fields.FieldDomain)
|
||||
.add('text', basic_fields.FieldText)
|
||||
.add('html', basic_fields.FieldText)
|
||||
.add('float', basic_fields.FieldFloat)
|
||||
.add('char', basic_fields.FieldChar)
|
||||
.add('link_button', basic_fields.LinkButton)
|
||||
|
@ -340,8 +340,8 @@ var FieldMany2One = AbstractField.extend({
|
||||
_renderReadonly: function () {
|
||||
var value = _.escape((this.m2o_value || "").trim()).split("\n").join("<br/>");
|
||||
this.$el.html(value);
|
||||
if (!this.nodeOptions.no_open) {
|
||||
this.$el.attr('href', '#');
|
||||
if (!this.nodeOptions.no_open && this.value) {
|
||||
this.$el.attr('href', _.str.sprintf('#id=%s&model=%s', this.value.res_id, this.field.relation));
|
||||
this.$el.addClass('o_form_uri');
|
||||
}
|
||||
},
|
||||
@ -1029,7 +1029,7 @@ var FieldX2Many = AbstractField.extend({
|
||||
*/
|
||||
_onEditLine: function (ev) {
|
||||
ev.stopPropagation();
|
||||
this.trigger_up('freeze_order', {id: this.value.id});
|
||||
this.trigger_up('edited_list', { id: this.value.id });
|
||||
var editedRecord = this.value.data[ev.data.index];
|
||||
this.renderer.setRowMode(editedRecord.id, 'edit')
|
||||
.done(ev.data.onSuccess);
|
||||
@ -1117,7 +1117,7 @@ var FieldX2Many = AbstractField.extend({
|
||||
_onResequence: function (event) {
|
||||
event.stopPropagation();
|
||||
var self = this;
|
||||
this.trigger_up('freeze_order', {id: this.value.id});
|
||||
this.trigger_up('edited_list', { id: this.value.id });
|
||||
var rowIDs = event.data.rowIDs.slice();
|
||||
var rowID = rowIDs.pop();
|
||||
var defs = _.map(rowIDs, function (rowID, index) {
|
||||
@ -1261,7 +1261,7 @@ var FieldOne2Many = FieldX2Many.extend({
|
||||
}
|
||||
} else if (!this.creatingRecord) {
|
||||
this.creatingRecord = true;
|
||||
this.trigger_up('freeze_order', {id: this.value.id});
|
||||
this.trigger_up('edited_list', { id: this.value.id });
|
||||
this._setValue({
|
||||
operation: 'CREATE',
|
||||
position: this.editable,
|
||||
@ -1291,12 +1291,24 @@ var FieldOne2Many = FieldX2Many.extend({
|
||||
// we don't want interference with the components upstream.
|
||||
ev.stopPropagation();
|
||||
|
||||
var self = this;
|
||||
var id = ev.data.id;
|
||||
// trigger an empty 'UPDATE' operation when the user clicks on 'Save' in
|
||||
// the dialog, to notify the main record that a subrecord of this
|
||||
// relational field has changed (those changes will be already stored on
|
||||
// that subrecord, thanks to the 'Save').
|
||||
var onSaved = this._setValue.bind(this, { operation: 'UPDATE', id: id }, {});
|
||||
var onSaved = function (record) {
|
||||
if (_.some(self.value.data, {id: record.id})) {
|
||||
// the record already exists in the relation, so trigger an
|
||||
// empty 'UPDATE' operation when the user clicks on 'Save' in
|
||||
// the dialog, to notify the main record that a subrecord of
|
||||
// this relational field has changed (those changes will be
|
||||
// already stored on that subrecord, thanks to the 'Save').
|
||||
self._setValue({ operation: 'UPDATE', id: record.id });
|
||||
} else {
|
||||
// the record isn't in the relation yet, so add it ; this can
|
||||
// happen if the user clicks on 'Save & New' in the dialog (the
|
||||
// opened record will be updated, and other records will be
|
||||
// created)
|
||||
self._setValue({ operation: 'ADD', id: record.id });
|
||||
}
|
||||
};
|
||||
this._openFormDialog({
|
||||
id: id,
|
||||
on_saved: onSaved,
|
||||
@ -1551,6 +1563,7 @@ var FieldMany2ManyBinaryMultiFiles = AbstractField.extend({
|
||||
|
||||
this.$('form.o_form_binary_form').submit();
|
||||
this.$('.oe_fileupload').hide();
|
||||
ev.target.value = "";
|
||||
},
|
||||
/**
|
||||
* @private
|
||||
@ -2265,6 +2278,21 @@ var FieldReference = FieldMany2One.extend({
|
||||
return this._super.apply(this, arguments);
|
||||
},
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
// Public
|
||||
//--------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* @override
|
||||
* @returns {jQuery}
|
||||
*/
|
||||
getFocusableElement: function () {
|
||||
if (this.mode === 'edit' && !this.field.relation) {
|
||||
return this.$('select');
|
||||
}
|
||||
return this._super.apply(this, arguments);
|
||||
},
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
// Private
|
||||
//--------------------------------------------------------------------------
|
||||
|
@ -92,10 +92,11 @@ var AbstractFieldUpgrade = {
|
||||
*/
|
||||
_render: function () {
|
||||
this._super.apply(this, arguments);
|
||||
this._insertEnterpriseLabel($("<span>", {
|
||||
text: "Enterprise",
|
||||
'class': "label label-primary oe_inline o_enterprise_label"
|
||||
}));
|
||||
// disable enterprise tags
|
||||
// this._insertEnterpriseLabel($("<span>", {
|
||||
// text: "Enterprise",
|
||||
// 'class': "label label-primary oe_inline o_enterprise_label"
|
||||
// }));
|
||||
},
|
||||
/**
|
||||
* This function is meant to be overriden to reset the $el to its initial
|
||||
|
@ -41,7 +41,8 @@ var CrashManager = core.Class.extend({
|
||||
core.bus.trigger('connection_lost');
|
||||
this.connection_lost = true;
|
||||
var timeinterval = setInterval(function() {
|
||||
ajax.jsonRpc('/web/webclient/version_info').then(function() {
|
||||
var options = {shadow: true};
|
||||
ajax.jsonRpc('/web/webclient/version_info', 'call', {}, options).then(function () {
|
||||
clearInterval(timeinterval);
|
||||
core.bus.trigger('connection_restored');
|
||||
self.connection_lost = false;
|
||||
@ -103,7 +104,7 @@ var CrashManager = core.Class.extend({
|
||||
if (!this.active) {
|
||||
return;
|
||||
}
|
||||
new Dialog(this, {
|
||||
return new Dialog(this, {
|
||||
size: 'medium',
|
||||
title: _.str.capitalize(error.type || error.message) || _t("Flectra Warning"),
|
||||
subtitle: error.data.title,
|
||||
|
@ -12,6 +12,7 @@ flectra.define('web.AbstractController', function (require) {
|
||||
* reading localstorage, ...) has to go through the controller.
|
||||
*/
|
||||
|
||||
var concurrency = require('web.concurrency');
|
||||
var Widget = require('web.Widget');
|
||||
|
||||
|
||||
@ -41,6 +42,9 @@ var AbstractController = Widget.extend({
|
||||
this.handle = params.handle;
|
||||
this.activeActions = params.activeActions;
|
||||
this.initialState = params.initialState;
|
||||
|
||||
// use a DropPrevious to correctly handle concurrent updates
|
||||
this.dp = new concurrency.DropPrevious();
|
||||
},
|
||||
/**
|
||||
* Simply renders and updates the url.
|
||||
@ -62,6 +66,18 @@ var AbstractController = Widget.extend({
|
||||
}
|
||||
return this._super.apply(this, arguments);
|
||||
},
|
||||
/**
|
||||
* Called each time the controller is attached into the DOM.
|
||||
*/
|
||||
on_attach_callback: function () {
|
||||
this.renderer.on_attach_callback();
|
||||
},
|
||||
/**
|
||||
* Called each time the controller is detached from the DOM.
|
||||
*/
|
||||
on_detach_callback: function () {
|
||||
this.renderer.on_detach_callback();
|
||||
},
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
// Public
|
||||
@ -194,11 +210,11 @@ var AbstractController = Widget.extend({
|
||||
var self = this;
|
||||
var shouldReload = (options && 'reload' in options) ? options.reload : true;
|
||||
var def = shouldReload ? this.model.reload(this.handle, params) : $.when();
|
||||
return def.then(function (handle) {
|
||||
return this.dp.add(def).then(function (handle) {
|
||||
self.handle = handle || self.handle; // update handle if we reloaded
|
||||
var state = self.model.get(self.handle);
|
||||
var localState = self.renderer.getLocalState();
|
||||
return self.renderer.updateState(state, params).then(function () {
|
||||
return self.dp.add(self.renderer.updateState(state, params)).then(function () {
|
||||
self.renderer.setLocalState(localState);
|
||||
self._update(state);
|
||||
});
|
||||
|
@ -36,6 +36,14 @@ return Widget.extend({
|
||||
this.$el.addClass(this.arch.attrs.class);
|
||||
return $.when(this._render(), this._super());
|
||||
},
|
||||
/**
|
||||
* Called each time the renderer is attached into the DOM.
|
||||
*/
|
||||
on_attach_callback: function () {},
|
||||
/**
|
||||
* Called each time the renderer is detached from the DOM.
|
||||
*/
|
||||
on_detach_callback: function () {},
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
// Public
|
||||
|
@ -224,6 +224,14 @@ var AbstractView = Class.extend({
|
||||
|
||||
_.each(this.loadParams.fieldsInfo.form, function (attrs, fieldName) {
|
||||
var field = fields[fieldName];
|
||||
if (!field) {
|
||||
// when a one2many record is opened in a form view, the fields
|
||||
// of the main one2many view (list or kanban) are added to the
|
||||
// fieldsInfo of its form view, but those fields aren't in the
|
||||
// loadParams.fields, as they are not displayed in the view, so
|
||||
// we can ignore them.
|
||||
return;
|
||||
}
|
||||
if (field.type !== 'one2many' && field.type !== 'many2many') {
|
||||
return;
|
||||
}
|
||||
|
@ -306,7 +306,7 @@ var BasicController = AbstractController.extend(FieldManagerMixin, {
|
||||
*
|
||||
* @private
|
||||
*/
|
||||
_disableButtons: function () {
|
||||
_disableButtons: function () {
|
||||
if (this.$buttons) {
|
||||
this.$buttons.find('button').attr('disabled', true);
|
||||
}
|
||||
@ -338,10 +338,8 @@ var BasicController = AbstractController.extend(FieldManagerMixin, {
|
||||
return;
|
||||
}
|
||||
self.model.discardChanges(recordID);
|
||||
if (self.model.isNew(recordID)) {
|
||||
if (self.model.canBeAbandoned(recordID)) {
|
||||
self._abandonRecord(recordID);
|
||||
}
|
||||
if (self.model.canBeAbandoned(recordID)) {
|
||||
self._abandonRecord(recordID);
|
||||
return;
|
||||
}
|
||||
return self._confirmSave(recordID);
|
||||
@ -352,7 +350,7 @@ var BasicController = AbstractController.extend(FieldManagerMixin, {
|
||||
*
|
||||
* @private
|
||||
*/
|
||||
_enableButtons: function () {
|
||||
_enableButtons: function () {
|
||||
if (this.$buttons) {
|
||||
this.$buttons.find('button').removeAttr('disabled');
|
||||
}
|
||||
|
@ -347,6 +347,7 @@ var BasicModel = AbstractModel.extend({
|
||||
var element = this.localData[id];
|
||||
var isNew = this.isNew(id);
|
||||
var rollback = 'rollback' in options ? options.rollback : isNew;
|
||||
var initialOffset = element.offset;
|
||||
this._visitChildren(element, function (elem) {
|
||||
if (rollback && elem._savePoint) {
|
||||
if (elem._savePoint instanceof Array) {
|
||||
@ -365,6 +366,7 @@ var BasicModel = AbstractModel.extend({
|
||||
delete elem.tempLimitIncrement;
|
||||
}
|
||||
});
|
||||
element.offset = initialOffset;
|
||||
},
|
||||
/**
|
||||
* Duplicate a record (by calling the 'copy' route)
|
||||
@ -375,11 +377,12 @@ var BasicModel = AbstractModel.extend({
|
||||
duplicateRecord: function (recordID) {
|
||||
var self = this;
|
||||
var record = this.localData[recordID];
|
||||
var context = this._getContext(record);
|
||||
return this._rpc({
|
||||
model: record.model,
|
||||
method: 'copy',
|
||||
args: [record.data.id],
|
||||
context: this._getContext(record),
|
||||
context: context,
|
||||
})
|
||||
.then(function (res_id) {
|
||||
var index = record.res_ids.indexOf(record.res_id);
|
||||
@ -391,6 +394,7 @@ var BasicModel = AbstractModel.extend({
|
||||
res_id: res_id,
|
||||
res_ids: record.res_ids.slice(0),
|
||||
viewType: record.viewType,
|
||||
context: context,
|
||||
});
|
||||
});
|
||||
},
|
||||
@ -567,10 +571,18 @@ var BasicModel = AbstractModel.extend({
|
||||
return _t("New");
|
||||
},
|
||||
/**
|
||||
* Returns true if a record can be abandoned from a list datapoint.
|
||||
* Returns true if a record can be abandoned.
|
||||
*
|
||||
* A record cannot be abandonned if it has been registered as "added"
|
||||
* in the parent's savepoint, otherwise it can be abandonned.
|
||||
* Case for not abandoning the record:
|
||||
*
|
||||
* 1. flagged as 'no abandon' (i.e. during a `default_get`, including any
|
||||
* `onchange` from a `default_get`)
|
||||
* 2. registered in a list on addition
|
||||
* 2.1. registered as non-new addition
|
||||
* 2.2. registered as new additon on update
|
||||
* 3. record is not new
|
||||
*
|
||||
* Otherwise, the record can be abandoned.
|
||||
*
|
||||
* This is useful when discarding changes on this record, as it means that
|
||||
* we must keep the record even if some fields are invalids (e.g. required
|
||||
@ -580,15 +592,29 @@ var BasicModel = AbstractModel.extend({
|
||||
* @returns {boolean}
|
||||
*/
|
||||
canBeAbandoned: function (id) {
|
||||
var data = this.localData[id];
|
||||
var parent = this.localData[data.parentID];
|
||||
var abandonable = true;
|
||||
if (parent) {
|
||||
abandonable = !_.some(parent._savePoint, function (entry) {
|
||||
return entry.operation === 'ADD' && entry.id === id;
|
||||
});
|
||||
// 1. no drop if flagged
|
||||
if (this.localData[id]._noAbandon) {
|
||||
return false;
|
||||
}
|
||||
return abandonable;
|
||||
// 2. no drop in a list on "ADD in some cases
|
||||
var record = this.localData[id];
|
||||
var parent = this.localData[record.parentID];
|
||||
if (parent) {
|
||||
var entry = _.findWhere(parent._savePoint, {operation: 'ADD', id: id});
|
||||
if (entry) {
|
||||
// 2.1. no drop on non-new addition in list
|
||||
if (!entry.isNew) {
|
||||
return false;
|
||||
}
|
||||
// 2.2. no drop on new addition on "UPDATE"
|
||||
var lastEntry = _.last(parent._savePoint);
|
||||
if (lastEntry.operation === 'UPDATE' && lastEntry.id === id) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
// 3. drop new records
|
||||
return this.isNew(id);
|
||||
},
|
||||
/**
|
||||
* Returns true if a record is dirty. A record is considered dirty if it has
|
||||
@ -874,6 +900,15 @@ var BasicModel = AbstractModel.extend({
|
||||
// x2Many case: the new record has been stored in _changes, as a
|
||||
// command so we remove the command(s) related to that record
|
||||
parent._changes = _.filter(parent._changes, function (change) {
|
||||
if (change.id === elementID &&
|
||||
change.operation === 'ADD' && // For now, only an ADD command increases limits
|
||||
parent.tempLimitIncrement) {
|
||||
// The record will be deleted from the _changes.
|
||||
// So we won't be passing into the logic of _applyX2ManyOperations anymore
|
||||
// implying that we have to cancel out the effects of an ADD command here
|
||||
parent.tempLimitIncrement--;
|
||||
parent.limit--;
|
||||
}
|
||||
return change.id !== elementID;
|
||||
});
|
||||
} else {
|
||||
@ -949,11 +984,11 @@ var BasicModel = AbstractModel.extend({
|
||||
* @returns {Deferred}
|
||||
* Resolved with the list of field names (whose value has been modified)
|
||||
*/
|
||||
save: function (record_id, options) {
|
||||
save: function (recordID, options) {
|
||||
var self = this;
|
||||
return this.mutex.exec(function () {
|
||||
options = options || {};
|
||||
var record = self.localData[record_id];
|
||||
var record = self.localData[recordID];
|
||||
if (options.savePoint) {
|
||||
self._visitChildren(record, function (rec) {
|
||||
var newValue = rec._changes || rec.data;
|
||||
@ -971,7 +1006,7 @@ var BasicModel = AbstractModel.extend({
|
||||
});
|
||||
}
|
||||
var shouldReload = 'reload' in options ? options.reload : true;
|
||||
var method = self.isNew(record_id) ? 'create' : 'write';
|
||||
var method = self.isNew(recordID) ? 'create' : 'write';
|
||||
if (record._changes) {
|
||||
// id never changes, and should not be written
|
||||
delete record._changes.id;
|
||||
@ -1053,7 +1088,13 @@ var BasicModel = AbstractModel.extend({
|
||||
addFieldsInfo: function (recordID, viewInfo) {
|
||||
var record = this.localData[recordID];
|
||||
record.fields = _.extend({}, record.fields, viewInfo.fields);
|
||||
record.fieldsInfo = _.extend({}, record.fieldsInfo, viewInfo.fieldsInfo);
|
||||
// complete the given fieldsInfo with the fields of the main view, so
|
||||
// that those field will be reloaded if a reload is triggered by the
|
||||
// secondary view
|
||||
var fieldsInfo = _.mapObject(viewInfo.fieldsInfo, function (fieldsInfo) {
|
||||
return _.defaults({}, fieldsInfo, record.fieldsInfo[record.viewType]);
|
||||
});
|
||||
record.fieldsInfo = _.extend({}, record.fieldsInfo, fieldsInfo);
|
||||
},
|
||||
/**
|
||||
* For list resources, this freezes the current records order.
|
||||
@ -1535,7 +1576,10 @@ var BasicModel = AbstractModel.extend({
|
||||
rec = self._makeDataPoint(params);
|
||||
list._cache[rec.res_id] = rec.id;
|
||||
}
|
||||
|
||||
// Do not abandon the record if it has been created
|
||||
// from `default_get`. The list has a savepoint only
|
||||
// after having fully executed `default_get`.
|
||||
rec._noAbandon = !list._savePoint;
|
||||
list._changes.push({operation: 'ADD', id: rec.id});
|
||||
if (command[0] === 1) {
|
||||
list._changes.push({operation: 'UPDATE', id: rec.id});
|
||||
@ -1953,6 +1997,7 @@ var BasicModel = AbstractModel.extend({
|
||||
var records = [];
|
||||
var ids = [];
|
||||
list = this._applyX2ManyOperations(list);
|
||||
|
||||
_.each(list.data, function (localId) {
|
||||
var record = self.localData[localId];
|
||||
var data = record._changes || record.data;
|
||||
@ -1963,16 +2008,22 @@ var BasicModel = AbstractModel.extend({
|
||||
ids.push(many2oneRecord.res_id);
|
||||
model = many2oneRecord.model;
|
||||
});
|
||||
if (!ids.length) {
|
||||
return $.when();
|
||||
}
|
||||
return this._rpc({
|
||||
model: model,
|
||||
method: 'name_get',
|
||||
args: [ids],
|
||||
args: [_.uniq(ids)],
|
||||
context: list.context,
|
||||
})
|
||||
.then(function (name_gets) {
|
||||
for (var i = 0; i < name_gets.length; i++) {
|
||||
records[i].data.display_name = name_gets[i][1];
|
||||
}
|
||||
_.each(records, function (record) {
|
||||
var nameGet = _.find(name_gets, function (nameGet) {
|
||||
return nameGet[0] === record.data.id;
|
||||
});
|
||||
record.data.display_name = nameGet[1];
|
||||
});
|
||||
});
|
||||
},
|
||||
/**
|
||||
@ -1990,7 +2041,8 @@ var BasicModel = AbstractModel.extend({
|
||||
*/
|
||||
_fetchRecord: function (record, options) {
|
||||
var self = this;
|
||||
var fieldNames = options && options.fieldNames || record.getFieldNames();
|
||||
options = options || {};
|
||||
var fieldNames = options.fieldNames || record.getFieldNames(options);
|
||||
fieldNames = _.uniq(fieldNames.concat(['display_name']));
|
||||
return this._rpc({
|
||||
model: record.model,
|
||||
@ -2457,7 +2509,16 @@ var BasicModel = AbstractModel.extend({
|
||||
var self = this;
|
||||
var def;
|
||||
if (list.static) {
|
||||
def = this._readUngroupedList(list);
|
||||
def = this._readUngroupedList(list).then(function () {
|
||||
if (list.parentID && self.isNew(list.parentID)) {
|
||||
// list from a default_get, so fetch display_name for many2one fields
|
||||
var many2ones = self._getMany2OneFieldNames(list);
|
||||
var defs = _.map(many2ones, function (name) {
|
||||
return self._fetchNameGets(list, name);
|
||||
});
|
||||
return $.when.apply($, defs);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
def = this._searchReadUngroupedList(list);
|
||||
}
|
||||
@ -2490,8 +2551,9 @@ var BasicModel = AbstractModel.extend({
|
||||
_fetchX2Manys: function (record, options) {
|
||||
var self = this;
|
||||
var defs = [];
|
||||
var fieldNames = options && options.fieldNames || record.getFieldNames();
|
||||
var viewType = options && options.viewType || record.viewType;
|
||||
options = options || {};
|
||||
var fieldNames = options.fieldNames || record.getFieldNames(options);
|
||||
var viewType = options.viewType || record.viewType;
|
||||
_.each(fieldNames, function (fieldName) {
|
||||
var field = record.fields[fieldName];
|
||||
if (field.type === 'one2many' || field.type === 'many2many') {
|
||||
@ -2516,6 +2578,11 @@ var BasicModel = AbstractModel.extend({
|
||||
relationField: field.relation_field,
|
||||
viewType: view ? view.type : fieldInfo.viewType,
|
||||
});
|
||||
// set existing changes to the list
|
||||
if (record._changes && record._changes[fieldName]) {
|
||||
list._changes = self.localData[record._changes[fieldName]]._changes;
|
||||
record._changes[fieldName] = list.id;
|
||||
}
|
||||
record.data[fieldName] = list.id;
|
||||
if (!fieldInfo.__no_fetch) {
|
||||
var def = self._readUngroupedList(list).then(function () {
|
||||
@ -3014,11 +3081,32 @@ var BasicModel = AbstractModel.extend({
|
||||
* default view type.
|
||||
*
|
||||
* @param {Object} element an element from the localData
|
||||
* @param {Object} [options]
|
||||
* @param {Object} [options.viewType] current viewType. If not set, we will
|
||||
* assume main viewType from the record
|
||||
* @returns {string[]} the list of field names
|
||||
*/
|
||||
_getFieldNames: function (element) {
|
||||
_getFieldNames: function (element, options) {
|
||||
var fieldsInfo = element.fieldsInfo;
|
||||
return Object.keys(fieldsInfo && fieldsInfo[element.viewType] || {});
|
||||
var viewType = options && options.viewType || element.viewType;
|
||||
return Object.keys(fieldsInfo && fieldsInfo[viewType] || {});
|
||||
},
|
||||
/**
|
||||
* Get many2one fields names in a datapoint. This is useful in order to
|
||||
* fetch their names in the case of a default_get.
|
||||
*
|
||||
* @private
|
||||
* @param {Object} datapoint a valid resource object
|
||||
* @returns {string[]} list of field names that are many2one
|
||||
*/
|
||||
_getMany2OneFieldNames: function (datapoint) {
|
||||
var many2ones = [];
|
||||
_.each(datapoint.fields, function (field, name) {
|
||||
if (field.type === 'many2one') {
|
||||
many2ones.push(name);
|
||||
}
|
||||
});
|
||||
return many2ones;
|
||||
},
|
||||
/**
|
||||
* Evaluate the record evaluation context. This method is supposed to be
|
||||
@ -3072,6 +3160,15 @@ var BasicModel = AbstractModel.extend({
|
||||
// when sent to the server, the x2manys values must be a list
|
||||
// of commands in a context, but the list of ids in a domain
|
||||
ids.toJSON = _generateX2ManyCommands.bind(null, fieldName);
|
||||
} else if (field.type === 'one2many') { // Ids are evaluated as a list of ids
|
||||
/* Filtering out virtual ids from the ids list
|
||||
* The server will crash if there are virtual ids in there
|
||||
* The webClient doesn't do literal id list comparison like ids == list
|
||||
* Only relevant in o2m: m2m does create actual records in db
|
||||
*/
|
||||
ids = _.filter(ids, function (id) {
|
||||
return typeof id !== 'string';
|
||||
});
|
||||
}
|
||||
context[fieldName] = ids;
|
||||
}
|
||||
@ -3166,6 +3263,9 @@ var BasicModel = AbstractModel.extend({
|
||||
* @param {Object} [options]
|
||||
* @param {string[]} [options.fieldNames] the fields to fetch for a record
|
||||
* @param {boolean} [options.onlyGroups=false]
|
||||
* @param {boolean} [options.keepEmptyGroups=false] if set, the groups not
|
||||
* present in the read_group anymore (empty groups) will stay in the
|
||||
* datapoint (used to mimic the kanban renderer behaviour for example)
|
||||
* @returns {Deferred}
|
||||
*/
|
||||
_load: function (dataPoint, options) {
|
||||
@ -3318,9 +3418,29 @@ var BasicModel = AbstractModel.extend({
|
||||
*/
|
||||
_makeDefaultRecord: function (modelName, params) {
|
||||
var self = this;
|
||||
|
||||
var determineExtraFields = function() {
|
||||
// Fields that are present in the originating view, that need to be initialized
|
||||
// Hence preventing their value to crash when getting back to the originating view
|
||||
var parentRecord = self.localData[params.parentID];
|
||||
|
||||
var originView = parentRecord && parentRecord.fieldsInfo;
|
||||
if (!originView || !originView[parentRecord.viewType])
|
||||
return [];
|
||||
|
||||
var fieldsFromOrigin = _.filter(Object.keys(originView[parentRecord.viewType]),
|
||||
function(fieldname) {
|
||||
return params.fields[fieldname] !== undefined;
|
||||
});
|
||||
|
||||
return fieldsFromOrigin;
|
||||
}
|
||||
|
||||
var fieldNames = Object.keys(params.fieldsInfo[params.viewType]);
|
||||
var fields_key = _.without(fieldNames, '__last_update');
|
||||
|
||||
var extraFields = determineExtraFields();
|
||||
|
||||
return this._rpc({
|
||||
model: modelName,
|
||||
method: 'default_get',
|
||||
@ -3338,7 +3458,7 @@ var BasicModel = AbstractModel.extend({
|
||||
viewType: params.viewType,
|
||||
});
|
||||
|
||||
return self.applyDefaultValues(record.id, result)
|
||||
return self.applyDefaultValues(record.id, result, {fieldNames: _.union(fieldNames, extraFields)})
|
||||
.then(function () {
|
||||
var def = $.Deferred();
|
||||
self._performOnChange(record, fields_key).always(function () {
|
||||
@ -3475,13 +3595,16 @@ var BasicModel = AbstractModel.extend({
|
||||
* @see _fetchRecord @see _makeDefaultRecord
|
||||
*
|
||||
* @param {Object} record
|
||||
* @param {Object} record
|
||||
* @param {Object} [options]
|
||||
* @param {Object} [options.viewType] current viewType. If not set, we will
|
||||
* assume main viewType from the record
|
||||
* @returns {Deferred<Object>} resolves to the finished resource
|
||||
*/
|
||||
_postprocess: function (record, options) {
|
||||
var self = this;
|
||||
var defs = [];
|
||||
_.each(record.getFieldNames(), function (name) {
|
||||
|
||||
_.each(record.getFieldNames(options), function (name) {
|
||||
var field = record.fields[name];
|
||||
var fieldInfo = record.fieldsInfo[record.viewType][name] || {};
|
||||
var options = fieldInfo.options || {};
|
||||
@ -3572,6 +3695,7 @@ var BasicModel = AbstractModel.extend({
|
||||
parentID: x2manyList.id,
|
||||
viewType: viewType,
|
||||
});
|
||||
r._noAbandon = true;
|
||||
x2manyList._changes.push({operation: 'ADD', id: r.id});
|
||||
x2manyList._cache[r.res_id] = r.id;
|
||||
|
||||
@ -3589,8 +3713,9 @@ var BasicModel = AbstractModel.extend({
|
||||
if (isFieldInView) {
|
||||
var field = r.fields[fieldName];
|
||||
var fieldType = field.type;
|
||||
var rec;
|
||||
if (fieldType === 'many2one') {
|
||||
var rec = self._makeDataPoint({
|
||||
rec = self._makeDataPoint({
|
||||
context: r.context,
|
||||
modelName: field.relation,
|
||||
data: {id: r._changes[fieldName]},
|
||||
@ -3598,9 +3723,21 @@ var BasicModel = AbstractModel.extend({
|
||||
});
|
||||
r._changes[fieldName] = rec.id;
|
||||
many2ones[fieldName] = true;
|
||||
} else if (fieldType === 'reference') {
|
||||
var reference = r._changes[fieldName].split(',');
|
||||
rec = self._makeDataPoint({
|
||||
context: r.context,
|
||||
modelName: reference[0],
|
||||
data: {id: parseInt(reference[1])},
|
||||
parentID: r.id,
|
||||
});
|
||||
r._changes[fieldName] = rec.id;
|
||||
many2ones[fieldName] = true;
|
||||
} else if (_.contains(['one2many', 'many2many'], fieldType)) {
|
||||
var x2mCommands = commands[0][2][fieldName];
|
||||
var x2mCommands = value[2][fieldName];
|
||||
defs.push(self._processX2ManyCommands(r, fieldName, x2mCommands));
|
||||
} else {
|
||||
r._changes[fieldName] = self._parseServerValue(field, r._changes[fieldName]);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -3795,12 +3932,28 @@ var BasicModel = AbstractModel.extend({
|
||||
defs.push(self._load(newGroup, options));
|
||||
}
|
||||
});
|
||||
if (options && options.keepEmptyGroups) {
|
||||
// Find the groups that were available in a previous
|
||||
// readGroup but are not there anymore.
|
||||
// Note that these groups are put after existing groups so
|
||||
// the order is not conserved. A sort *might* be useful.
|
||||
var emptyGroupsIDs = _.difference(_.pluck(previousGroups, 'id'), list.data);
|
||||
_.each(emptyGroupsIDs, function (groupID) {
|
||||
list.data.push(groupID);
|
||||
var emptyGroup = self.localData[groupID];
|
||||
// this attribute hasn't been updated in the previous
|
||||
// loop for empty groups
|
||||
emptyGroup.aggregateValues = {};
|
||||
});
|
||||
}
|
||||
return $.when.apply($, defs).then(function () {
|
||||
// generate the res_ids of the main list, being the concatenation
|
||||
// of the fetched res_ids in each group
|
||||
list.res_ids = _.flatten(_.map(arguments, function (group) {
|
||||
return group ? group.res_ids : [];
|
||||
}));
|
||||
if (!options || !options.onlyGroups) {
|
||||
// generate the res_ids of the main list, being the concatenation
|
||||
// of the fetched res_ids in each group
|
||||
list.res_ids = _.flatten(_.map(arguments, function (group) {
|
||||
return group ? group.res_ids : [];
|
||||
}));
|
||||
}
|
||||
return list;
|
||||
});
|
||||
});
|
||||
@ -4030,10 +4183,20 @@ var BasicModel = AbstractModel.extend({
|
||||
var r2 = self.localData[record2ID];
|
||||
var data1 = _.extend({}, r1.data, r1._changes);
|
||||
var data2 = _.extend({}, r2.data, r2._changes);
|
||||
if (data1[order.name] < data2[order.name]) {
|
||||
|
||||
// Default value to sort against: the value of the field
|
||||
var orderData1 = data1[order.name];
|
||||
var orderData2 = data2[order.name];
|
||||
|
||||
// If the field is a relation, sort on the display_name of those records
|
||||
if (list.fields[order.name].type === 'many2one') {
|
||||
orderData1 = orderData1 ? self.localData[orderData1].data.display_name : "";
|
||||
orderData2 = orderData2 ? self.localData[orderData2].data.display_name : "";
|
||||
}
|
||||
if (orderData1 < orderData2) {
|
||||
return order.asc ? -1 : 1;
|
||||
}
|
||||
if (data1[order.name] > data2[order.name]) {
|
||||
if (orderData1 > orderData2) {
|
||||
return order.asc ? 1 : -1;
|
||||
}
|
||||
return compareRecords(record1ID, record2ID, level + 1);
|
||||
|
@ -704,11 +704,11 @@ var BasicRenderer = AbstractRenderer.extend({
|
||||
/**
|
||||
* When someone presses the TAB/UP/DOWN/... key in a widget, it is nice to
|
||||
* be able to navigate in the view (default browser behaviors are disabled
|
||||
* by Odoo).
|
||||
* by Flectra).
|
||||
*
|
||||
* @abstract
|
||||
* @private
|
||||
* @param {OdooEvent} ev
|
||||
* @param {FlectraEvent} ev
|
||||
*/
|
||||
_onNavigationMove: function (ev) {},
|
||||
});
|
||||
|
@ -253,7 +253,10 @@ return AbstractModel.extend({
|
||||
* @param {Moment} start
|
||||
*/
|
||||
setDate: function (start) {
|
||||
this.data.start_date = this.data.end_date = this.data.target_date = this.data.highlight_date = start;
|
||||
// keep highlight/target_date in localtime
|
||||
this.data.highlight_date = this.data.target_date = start.clone();
|
||||
// set dates in UTC with timezone applied manually
|
||||
this.data.start_date = this.data.end_date = start;
|
||||
this.data.start_date.utc().add(this.getSession().getTZOffset(this.data.start_date), 'minutes');
|
||||
|
||||
switch (this.data.scale) {
|
||||
@ -386,7 +389,7 @@ return AbstractModel.extend({
|
||||
monthNamesShort: moment.monthsShort(),
|
||||
dayNames: moment.weekdays(),
|
||||
dayNamesShort: moment.weekdaysShort(),
|
||||
firstDay: moment().startOf('week').isoWeekday(),
|
||||
firstDay: moment()._locale._week.dow,
|
||||
};
|
||||
},
|
||||
/**
|
||||
@ -539,6 +542,7 @@ return AbstractModel.extend({
|
||||
});
|
||||
|
||||
var fs = [];
|
||||
var undefined_fs = [];
|
||||
_.each(events, function (event) {
|
||||
var data = event.record[fieldName];
|
||||
if (!_.contains(['many2many', 'one2many'], field.type)) {
|
||||
@ -548,15 +552,18 @@ return AbstractModel.extend({
|
||||
}
|
||||
_.each(data, function (_value) {
|
||||
var value = _.isArray(_value) ? _value[0] : _value;
|
||||
fs.push({
|
||||
var f = {
|
||||
'color_index': self.model_color === (field.relation || element.model) ? value : false,
|
||||
'value': value,
|
||||
'label': fieldUtils.format[field.type](_value, field),
|
||||
'label': fieldUtils.format[field.type](_value, field) || _t("Undefined"),
|
||||
'avatar_model': field.relation || element.model,
|
||||
});
|
||||
};
|
||||
// if field used as color does not have value then push filter in undefined_fs,
|
||||
// such filters should come last in filter list with Undefined string, later merge it with fs
|
||||
value ? fs.push(f) : undefined_fs.push(f);
|
||||
});
|
||||
});
|
||||
_.each(fs, function (f) {
|
||||
_.each(_.union(fs, undefined_fs), function (f) {
|
||||
var f1 = _.findWhere(filter.filters, f);
|
||||
if (f1) {
|
||||
f1.display = true;
|
||||
@ -611,7 +618,8 @@ return AbstractModel.extend({
|
||||
var date_start;
|
||||
var date_stop;
|
||||
var date_delay = evt[this.mapping.date_delay] || 1.0,
|
||||
all_day = this.mapping.all_day ? evt[this.mapping.all_day] : false,
|
||||
all_day = this.fields[this.mapping.date_start].type === 'date' ||
|
||||
this.mapping.all_day && evt[this.mapping.all_day] || false,
|
||||
the_title = '',
|
||||
attendees = [];
|
||||
|
||||
|
@ -112,8 +112,20 @@ var CalendarView = AbstractView.extend({
|
||||
//if quick_add = is NOT False and IS specified in view, we this one for widgets.QuickCreate'
|
||||
this.controllerParams.quickAddPop = (!('quick_add' in attrs) || utils.toBoolElse(attrs.quick_add+'', true));
|
||||
this.controllerParams.disableQuickCreate = params.disable_quick_create || !this.controllerParams.quickAddPop;
|
||||
// If this field is set ot true, we don't open the event in form view, but in a popup with the view_id passed by this parameter
|
||||
this.controllerParams.formViewId = !attrs.form_view_id || !utils.toBoolElse(attrs.form_view_id, true) ? false : attrs.form_view_id;
|
||||
|
||||
// If form_view_id is set, then the calendar view will open a form view
|
||||
// with this id, when it needs to edit or create an event.
|
||||
this.controllerParams.formViewId =
|
||||
attrs.form_view_id ? parseInt(attrs.form_view_id, 10) : false;
|
||||
if (!this.controllerParams.formViewId && params.action) {
|
||||
var formViewDescr = _.find(params.action.views, function (v) {
|
||||
return v[1] === 'form';
|
||||
});
|
||||
if (formViewDescr) {
|
||||
this.controllerParams.formViewId = formViewDescr[0];
|
||||
}
|
||||
}
|
||||
|
||||
this.controllerParams.readonlyFormViewId = !attrs.readonly_form_view_id || !utils.toBoolElse(attrs.readonly_form_view_id, true) ? false : attrs.readonly_form_view_id;
|
||||
this.controllerParams.eventOpenPopup = utils.toBoolElse(attrs.event_open_popup || '', false);
|
||||
this.controllerParams.mapping = mapping;
|
||||
|
@ -14,7 +14,7 @@ var FormController = BasicController.extend({
|
||||
custom_events: _.extend({}, BasicController.prototype.custom_events, {
|
||||
bounce_edit: '_onBounceEdit',
|
||||
button_clicked: '_onButtonClicked',
|
||||
freeze_order: '_onFreezeOrder',
|
||||
edited_list: '_onEditedList',
|
||||
open_one2many_record: '_onOpenOne2ManyRecord',
|
||||
open_record: '_onOpenRecord',
|
||||
toggle_column_order: '_onToggleColumnOrder',
|
||||
@ -100,6 +100,7 @@ var FormController = BasicController.extend({
|
||||
* @todo convert to new style
|
||||
*/
|
||||
on_attach_callback: function () {
|
||||
this._super.apply(this, arguments);
|
||||
this.autofocus();
|
||||
},
|
||||
/**
|
||||
@ -213,6 +214,16 @@ var FormController = BasicController.extend({
|
||||
return changedFields;
|
||||
});
|
||||
},
|
||||
/**
|
||||
* Overrides to force the viewType to 'form', so that we ensure that the
|
||||
* correct fields are reloaded (this is only useful for one2many form views).
|
||||
*
|
||||
* @override
|
||||
*/
|
||||
update: function (params, options) {
|
||||
params = _.extend({viewType: 'form'}, params);
|
||||
return this._super(params, options);
|
||||
},
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
// Private
|
||||
@ -467,11 +478,15 @@ var FormController = BasicController.extend({
|
||||
* in a x2many list view
|
||||
*
|
||||
* @private
|
||||
* @param {FlectraEvent} event
|
||||
* @param {FlectraEvent} ev
|
||||
* @param {integer} ev.id of the list to freeze while editing a line
|
||||
*/
|
||||
_onFreezeOrder: function (event) {
|
||||
event.stopPropagation();
|
||||
this.model.freezeOrder(event.data.id);
|
||||
_onEditedList: function (ev) {
|
||||
ev.stopPropagation();
|
||||
if (ev.data.id) {
|
||||
this.model.save(ev.data.id, {savePoint: true});
|
||||
}
|
||||
this.model.freezeOrder(ev.data.id);
|
||||
},
|
||||
/**
|
||||
* Opens a one2many record (potentially new) in a dialog. This handler is
|
||||
|
@ -43,6 +43,30 @@ return AbstractRenderer.extend({
|
||||
nv.utils.offWindowResize(this.to_remove);
|
||||
this._super();
|
||||
},
|
||||
/**
|
||||
* The graph view uses the nv(d3) lib to render the graph. This lib requires
|
||||
* that the rendering is done directly into the DOM (so that it can correctly
|
||||
* compute positions). However, the views are always rendered in fragments,
|
||||
* and appended to the DOM once ready (to prevent them from flickering). We
|
||||
* here use the on_attach_callback hook, called when the widget is attached
|
||||
* to the DOM, to perform the rendering. This ensures that the rendering is
|
||||
* always done in the DOM.
|
||||
*
|
||||
* @override
|
||||
*/
|
||||
on_attach_callback: function () {
|
||||
this._super.apply(this, arguments);
|
||||
this.isInDOM = true;
|
||||
this._renderGraph();
|
||||
},
|
||||
/**
|
||||
* @override
|
||||
*/
|
||||
on_detach_callback: function () {
|
||||
this._super.apply(this, arguments);
|
||||
this.isInDOM = false;
|
||||
},
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
// Private
|
||||
//--------------------------------------------------------------------------
|
||||
@ -51,10 +75,9 @@ return AbstractRenderer.extend({
|
||||
* Render the chart.
|
||||
*
|
||||
* Note that This method is synchronous, but the actual rendering is done
|
||||
* asynchronously (in a setTimeout). The reason for that is that nvd3/d3
|
||||
* needs to be in the DOM to correctly render itself. So, we trick Flectra by
|
||||
* returning immediately, then wait a tiny interval before actually
|
||||
* displaying the data.
|
||||
* asynchronously. The reason for that is that nvd3/d3 needs to be in the
|
||||
* DOM to correctly render itself. So, we trick Flectra by returning
|
||||
* immediately, then we render the chart when the widget is in the DOM.
|
||||
*
|
||||
* @returns {Deferred} The _super deferred is actually resolved immediately
|
||||
*/
|
||||
@ -76,21 +99,15 @@ return AbstractRenderer.extend({
|
||||
"Try to add some records, or make sure that " +
|
||||
"there is no active filter in the search bar."),
|
||||
}));
|
||||
} else {
|
||||
var self = this;
|
||||
setTimeout(function () {
|
||||
self.$el.empty();
|
||||
var chart = self['_render' + _.str.capitalize(self.state.mode) + 'Chart']();
|
||||
if (chart && chart.tooltip.chartContainer) {
|
||||
self.to_remove = chart.update;
|
||||
nv.utils.onWindowResize(chart.update);
|
||||
chart.tooltip.chartContainer(self.el);
|
||||
}
|
||||
}, 0);
|
||||
} else if (this.isInDOM) {
|
||||
// only render the graph if the widget is already in the DOM (this
|
||||
// happens typically after an update), otherwise, it will be
|
||||
// rendered when the widget will be attached to the DOM (see
|
||||
// 'on_attach_callback')
|
||||
this._renderGraph();
|
||||
}
|
||||
return this._super.apply(this, arguments);
|
||||
},
|
||||
|
||||
/**
|
||||
* Helper function to set up data properly for the multiBarChart model in
|
||||
* nvd3.
|
||||
@ -327,6 +344,21 @@ return AbstractRenderer.extend({
|
||||
chart(svg);
|
||||
return chart;
|
||||
},
|
||||
/**
|
||||
* Renders the graph according to its type. This function must be called
|
||||
* when the renderer is in the DOM (for nvd3 to render the graph correctly).
|
||||
*
|
||||
* @private
|
||||
*/
|
||||
_renderGraph: function () {
|
||||
this.$el.empty();
|
||||
var chart = this['_render' + _.str.capitalize(this.state.mode) + 'Chart']();
|
||||
if (chart && chart.tooltip.chartContainer) {
|
||||
this.to_remove = chart.update;
|
||||
nv.utils.onWindowResize(chart.update);
|
||||
chart.tooltip.chartContainer(this.el);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
});
|
||||
|
@ -10,6 +10,7 @@ flectra.define('web.KanbanController', function (require) {
|
||||
var BasicController = require('web.BasicController');
|
||||
var Context = require('web.Context');
|
||||
var core = require('web.core');
|
||||
var Domain = require('web.Domain');
|
||||
var view_dialogs = require('web.view_dialogs');
|
||||
|
||||
var _t = core._t;
|
||||
@ -106,6 +107,16 @@ var KanbanController = BasicController.extend({
|
||||
var groupedByM2o = groupByField && (groupByField.type === 'many2one');
|
||||
return groupedByM2o;
|
||||
},
|
||||
/**
|
||||
* @param {number[]} ids
|
||||
* @private
|
||||
* @returns {Deferred}
|
||||
*/
|
||||
_resequenceColumns: function (ids) {
|
||||
var state = this.model.get(this.handle, {raw: true});
|
||||
var model = state.fields[state.groupedBy[0]].relation;
|
||||
return this.model.resequence(model, ids, this.handle);
|
||||
},
|
||||
/**
|
||||
* This method calls the server to ask for a resequence. Note that this
|
||||
* does not rerender the user interface, because in most case, the
|
||||
@ -155,6 +166,10 @@ var KanbanController = BasicController.extend({
|
||||
_onAddColumn: function (event) {
|
||||
var self = this;
|
||||
this.model.createGroup(event.data.value, this.handle).then(function () {
|
||||
var state = self.model.get(self.handle, {raw: true});
|
||||
var ids = _.pluck(state.data, 'res_id').filter(_.isNumber);
|
||||
return self._resequenceColumns(ids);
|
||||
}).then(function () {
|
||||
return self.update({}, {reload: false});
|
||||
}).then(function () {
|
||||
self._updateButtons();
|
||||
@ -228,10 +243,57 @@ var KanbanController = BasicController.extend({
|
||||
resIDs: record.res_ids,
|
||||
},
|
||||
on_closed: function () {
|
||||
var recordModel = self.model.localData[record.id];
|
||||
var group = self.model.localData[recordModel.parentID];
|
||||
var parent = self.model.localData[group.parentID];
|
||||
|
||||
self.model.reload(record.id).then(function (db_id) {
|
||||
var data = self.model.get(db_id);
|
||||
var kanban_record = event.target;
|
||||
kanban_record.update(data);
|
||||
|
||||
// Check if we still need to display the record. Some fields of the domain are
|
||||
// not guaranteed to be in data. This is for example the case if the action
|
||||
// contains a domain on a field which is not in the Kanban view. Therefore,
|
||||
// we need to handle multiple cases based on 3 variables:
|
||||
// domInData: all domain fields are in the data
|
||||
// activeInDomain: 'active' is already in the domain
|
||||
// activeInData: 'active' is available in the data
|
||||
|
||||
var domain = (parent ? parent.domain : group.domain) || [];
|
||||
var domInData = _.every(domain, function (d) {
|
||||
return d[0] in data.data;
|
||||
});
|
||||
var activeInDomain = _.pluck(domain, 0).indexOf('active') !== -1;
|
||||
var activeInData = 'active' in data.data;
|
||||
|
||||
// Case # | domInData | activeInDomain | activeInData
|
||||
// 1 | true | true | true => no domain change
|
||||
// 2 | true | true | false => not possible
|
||||
// 3 | true | false | true => add active in domain
|
||||
// 4 | true | false | false => no domain change
|
||||
// 5 | false | true | true => no evaluation
|
||||
// 6 | false | true | false => no evaluation
|
||||
// 7 | false | false | true => replace domain
|
||||
// 8 | false | false | false => no evaluation
|
||||
|
||||
// There are 3 cases which cannot be evaluated since we don't have all the
|
||||
// necessary information. The complete solution would be to perform a RPC in
|
||||
// these cases, but this is out of scope. A simpler one is to do a try / catch.
|
||||
|
||||
if (domInData && !activeInDomain && activeInData) {
|
||||
domain = domain.concat([['active', '=', true]]);
|
||||
} else if (!domInData && !activeInDomain && activeInData) {
|
||||
domain = [['active', '=', true]];
|
||||
}
|
||||
try {
|
||||
var visible = new Domain(domain).compute(data.evalContext);
|
||||
} catch (e) {
|
||||
return;
|
||||
}
|
||||
if (!visible) {
|
||||
kanban_record.destroy();
|
||||
}
|
||||
});
|
||||
},
|
||||
});
|
||||
@ -380,9 +442,7 @@ var KanbanController = BasicController.extend({
|
||||
*/
|
||||
_onResequenceColumn: function (event) {
|
||||
var self = this;
|
||||
var state = this.model.get(this.handle, {raw: true});
|
||||
var model = state.fields[state.groupedBy[0]].relation;
|
||||
this.model.resequence(model, event.data.ids, this.handle).then(function () {
|
||||
this._resequenceColumns(event.data.ids).then(function () {
|
||||
self._updateEnv();
|
||||
});
|
||||
},
|
||||
|
@ -348,9 +348,8 @@ var KanbanModel = BasicModel.extend({
|
||||
return $.when();
|
||||
},
|
||||
/**
|
||||
* Reloads all progressbar data if the given id is a record's one. This is
|
||||
* done after given deferred and insures that the given deferred's result is
|
||||
* not lost.
|
||||
* Reloads all progressbar data. This is done after given deferred and
|
||||
* insures that the given deferred's result is not lost.
|
||||
*
|
||||
* @private
|
||||
* @param {string} recordID
|
||||
@ -359,7 +358,9 @@ var KanbanModel = BasicModel.extend({
|
||||
*/
|
||||
_reloadProgressBarGroupFromRecord: function (recordID, def) {
|
||||
var element = this.localData[recordID];
|
||||
if (element.type !== 'record') {
|
||||
if (element.type === 'list' && !element.parentID) {
|
||||
// we are reloading the whole view, so there is no need to manually
|
||||
// reload the progressbars
|
||||
return def;
|
||||
}
|
||||
|
||||
@ -369,7 +370,10 @@ var KanbanModel = BasicModel.extend({
|
||||
while (element) {
|
||||
if (element.progressBar) {
|
||||
return def.then(function (data) {
|
||||
return self._load(element, {onlyGroups: true}).then(function () {
|
||||
return self._load(element, {
|
||||
keepEmptyGroups: true,
|
||||
onlyGroups: true,
|
||||
}).then(function () {
|
||||
return data;
|
||||
});
|
||||
});
|
||||
|
@ -531,8 +531,13 @@ ListRenderer.include({
|
||||
* @returns {Deferred} this deferred is resolved immediately
|
||||
*/
|
||||
_renderView: function () {
|
||||
var self = this;
|
||||
this.currentRow = null;
|
||||
return this._super.apply(this, arguments);
|
||||
return this._super.apply(this, arguments).then(function () {
|
||||
if (self._isEditable()) {
|
||||
self.$('table').addClass('o_editable_list');
|
||||
}
|
||||
});
|
||||
},
|
||||
/**
|
||||
* Force the resequencing of the items in the list.
|
||||
|
@ -17,6 +17,7 @@ flectra.define('web.PivotModel', function (require) {
|
||||
*/
|
||||
|
||||
var AbstractModel = require('web.AbstractModel');
|
||||
var concurrency = require('web.concurrency');
|
||||
var core = require('web.core');
|
||||
var session = require('web.session');
|
||||
var utils = require('web.utils');
|
||||
@ -32,6 +33,7 @@ var PivotModel = AbstractModel.extend({
|
||||
this._super.apply(this, arguments);
|
||||
this.numbering = {};
|
||||
this.data = null;
|
||||
this._loadDataDropPrevious = new concurrency.DropPrevious();
|
||||
},
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
@ -250,6 +252,7 @@ var PivotModel = AbstractModel.extend({
|
||||
this.data.colGroupBys = params.context.pivot_column_groupby || this.data.colGroupBys;
|
||||
this.data.groupedBy = params.context.pivot_row_groupby || this.data.groupedBy;
|
||||
this.data.measures = this._processMeasures(params.context.pivot_measures) || this.data.measures;
|
||||
this.defaultGroupedBy = this.data.groupedBy.length ? this.data.groupedBy : this.defaultGroupedBy;
|
||||
}
|
||||
if ('domain' in params) {
|
||||
this.data.domain = params.domain;
|
||||
@ -278,7 +281,7 @@ var PivotModel = AbstractModel.extend({
|
||||
|
||||
self._updateTree(old_col_root, self.data.main_col.root);
|
||||
new_groupby_length = self._getHeaderDepth(self.data.main_col.root) - 1;
|
||||
self.data.main_row.groupbys = old_col_root.groupbys.slice(0, new_groupby_length);
|
||||
self.data.main_row.groupbys = old_row_root.groupbys.slice(0, new_groupby_length);
|
||||
});
|
||||
},
|
||||
/**
|
||||
@ -531,7 +534,7 @@ var PivotModel = AbstractModel.extend({
|
||||
}
|
||||
}
|
||||
|
||||
return $.when.apply(null, groupBys.map(function (groupBy) {
|
||||
return this._loadDataDropPrevious.add($.when.apply(null, groupBys.map(function (groupBy) {
|
||||
return self._rpc({
|
||||
model: self.modelName,
|
||||
method: 'read_group',
|
||||
@ -541,7 +544,7 @@ var PivotModel = AbstractModel.extend({
|
||||
groupBy: groupBy,
|
||||
lazy: false,
|
||||
});
|
||||
})).then(function () {
|
||||
}))).then(function () {
|
||||
var data = Array.prototype.slice.call(arguments);
|
||||
if (data[0][0].__count === 0) {
|
||||
self.data.has_data = false;
|
||||
|
@ -430,7 +430,7 @@ var SelectCreateDialog = ViewDialog.extend({
|
||||
* list controller.
|
||||
*
|
||||
* @private
|
||||
* @param {OdooEvent} ev
|
||||
* @param {FlectraEvent} ev
|
||||
* @param {function} ev.data.callback used to send the requested context
|
||||
*/
|
||||
_onGetControllerContext: function (ev) {
|
||||
|
@ -79,8 +79,15 @@ var DateWidget = Widget.extend({
|
||||
var oldValue = this.getValue();
|
||||
this._setValueFromUi();
|
||||
var newValue = this.getValue();
|
||||
|
||||
if (!oldValue !== !newValue || oldValue && newValue && !oldValue.isSame(newValue)) {
|
||||
var hasChanged = !oldValue !== !newValue;
|
||||
if (oldValue && newValue) {
|
||||
var formattedOldValue = oldValue.format(time.getLangDatetimeFormat());
|
||||
var formattedNewValue = newValue.format(time.getLangDatetimeFormat())
|
||||
if (formattedNewValue !== formattedOldValue) {
|
||||
hasChanged = true;
|
||||
}
|
||||
}
|
||||
if (hasChanged) {
|
||||
// The condition is strangely written; this is because the
|
||||
// values can be false/undefined
|
||||
this.trigger("datetime_changed");
|
||||
|
@ -178,6 +178,7 @@ var DebugManager = Widget.extend({
|
||||
})
|
||||
.then(function (views) {
|
||||
var view = views[0];
|
||||
view.type = view.type === 'tree' ? 'list' : view.type; // ignore tree view
|
||||
self.do_action({
|
||||
type: 'ir.actions.act_window',
|
||||
name: view.name,
|
||||
|
@ -176,6 +176,8 @@
|
||||
}
|
||||
}
|
||||
|
||||
@icon-font-path: "/web/static/lib/bootstrap/fonts/";
|
||||
|
||||
@btn-default-color: #fff;
|
||||
@btn-default-bg: @gray-light;
|
||||
@btn-default-border: transparent;
|
||||
|
@ -17,11 +17,12 @@
|
||||
|
||||
.o_datepicker_input {
|
||||
width: 100%;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.o_datepicker_button {
|
||||
.o-position-absolute(2px, 4px);
|
||||
cursor: pointer;
|
||||
pointer-events: none; // let click events go through the underlying input
|
||||
&:after {
|
||||
.o-caret-down;
|
||||
}
|
||||
|
@ -39,9 +39,7 @@
|
||||
|
||||
// Flex fields (inline)
|
||||
&.o_field_many2one, &.o_field_radio, &.o_field_many2manytags, &.o_field_percent_pie, &.o_field_monetary, &.o_field_binary_file {
|
||||
@media (min-width: @screen-sm-min) {
|
||||
.o-inline-flex-display();
|
||||
}
|
||||
.o-inline-flex-display();
|
||||
> span, > button {
|
||||
.o-flex(0, 0, auto);
|
||||
}
|
||||
@ -135,7 +133,7 @@
|
||||
display: inline-block;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
vertical-align: middle;
|
||||
vertical-align: baseline;
|
||||
> .o_priority_star {
|
||||
display: inline-block;
|
||||
font-size: 1.35em;
|
||||
@ -185,6 +183,9 @@
|
||||
|
||||
// Radio buttons
|
||||
&.o_field_radio {
|
||||
@media (max-width: @screen-xs-max) {
|
||||
display: inline-block;
|
||||
}
|
||||
.o_radio_input {
|
||||
outline: none;
|
||||
}
|
||||
@ -309,6 +310,10 @@
|
||||
&:hover .o_form_image_controls {
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
&.o_field_invalid > img {
|
||||
border: 1px solid @brand-danger;
|
||||
}
|
||||
}
|
||||
|
||||
// Input loading/file
|
||||
|
@ -248,6 +248,10 @@
|
||||
border-left-color: @gray-lighter;
|
||||
}
|
||||
}
|
||||
|
||||
.caret {
|
||||
margin-top: -2px;
|
||||
}
|
||||
}
|
||||
|
||||
.o-status-more {
|
||||
|
@ -91,6 +91,7 @@
|
||||
margin-left: 3%;
|
||||
color: @headings-color;
|
||||
text-align: right;
|
||||
white-space: nowrap;
|
||||
.o-transform-origin(right, center);
|
||||
|
||||
&.o_kanban_grow {
|
||||
|
@ -223,6 +223,7 @@
|
||||
}
|
||||
|
||||
.o_favorite {
|
||||
top: 3px;
|
||||
left: 0;
|
||||
right: auto;
|
||||
}
|
||||
|
@ -6,6 +6,14 @@
|
||||
background-color: @flectra-view-background-color;
|
||||
margin-bottom: 0px;
|
||||
|
||||
// Checkbox in editable list
|
||||
// should be clickable and activate the row
|
||||
&.o_editable_list .o_data_row:not(.o_selected_row) .o_data_cell {
|
||||
.o_checkbox:not(.o_readonly_modifier) {
|
||||
pointer-events: none;
|
||||
}
|
||||
}
|
||||
|
||||
&.table {
|
||||
td, th {
|
||||
vertical-align: middle;
|
||||
@ -37,12 +45,14 @@
|
||||
.user-select(none); // Prevent unwanted selection while sorting
|
||||
|
||||
&::after {
|
||||
margin-left: 6px;
|
||||
position: absolute;
|
||||
font-family: FontAwesome;
|
||||
content: "\f0d7";
|
||||
opacity: 0;
|
||||
}
|
||||
&:not(:empty)::after {
|
||||
margin-left: 6px;
|
||||
position: absolute;
|
||||
}
|
||||
&.o-sort-up {
|
||||
cursor: n-resize;
|
||||
&::after {
|
||||
@ -74,7 +84,6 @@
|
||||
padding: 0px;
|
||||
background-style: none;
|
||||
border-style: none;
|
||||
height: 0px;
|
||||
display: table-cell;
|
||||
}
|
||||
|
||||
|
@ -76,4 +76,4 @@
|
||||
// ------------------------------------------------------------------
|
||||
.o_pivot_measures_list {
|
||||
.o-selected-li;
|
||||
}
|
||||
}
|
@ -71,11 +71,6 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.o_target_date:not(.fc-today) {
|
||||
background-color: @flectra-brand-primary;
|
||||
opacity: 0.1;
|
||||
}
|
||||
}
|
||||
|
||||
.o_calendar_sidebar_container {
|
||||
|
@ -18,4 +18,4 @@ flectra.__DEBUG__.didLogInfo.then(function() {
|
||||
|
||||
});
|
||||
|
||||
})();
|
||||
})();
|
@ -1698,7 +1698,6 @@ QUnit.module('basic_fields', {
|
||||
assert.strictEqual(form.$('div[name="document"] > img').attr('width'), "90",
|
||||
"the image should correctly set its attributes");
|
||||
form.destroy();
|
||||
|
||||
});
|
||||
|
||||
QUnit.test('image fields in subviews are loaded correctly', function (assert) {
|
||||
@ -1750,6 +1749,34 @@ QUnit.module('basic_fields', {
|
||||
form.destroy();
|
||||
});
|
||||
|
||||
QUnit.test('image fields with required attribute', function (assert) {
|
||||
assert.expect(2);
|
||||
|
||||
var form = createView({
|
||||
View: FormView,
|
||||
model: 'partner',
|
||||
data: this.data,
|
||||
arch: '<form string="Partners">' +
|
||||
'<field name="document" required="1" widget="image"/>' +
|
||||
'</form>',
|
||||
mockRPC: function (route, args) {
|
||||
if (args.method === 'create') {
|
||||
throw new Error("Should not do a create RPC with unset required image field");
|
||||
}
|
||||
return this._super.apply(this, arguments);
|
||||
},
|
||||
});
|
||||
|
||||
form.$buttons.find('.o_form_button_save').click();
|
||||
|
||||
assert.ok(form.$('.o_form_view').hasClass('o_form_editable'),
|
||||
"form view should still be editable");
|
||||
assert.ok(form.$('.o_field_widget').hasClass('o_field_invalid'),
|
||||
"image field should be displayed as invalid");
|
||||
|
||||
form.destroy();
|
||||
});
|
||||
|
||||
QUnit.module('JournalDashboardGraph', {
|
||||
beforeEach: function () {
|
||||
_.extend(this.data.partner.fields, {
|
||||
@ -2262,6 +2289,38 @@ QUnit.module('basic_fields', {
|
||||
form.destroy();
|
||||
});
|
||||
|
||||
QUnit.test('datetime field in form view', function (assert) {
|
||||
assert.expect(1);
|
||||
|
||||
this.data.partner.fields.datetime.default = "2017-08-02 12:00:05";
|
||||
this.data.partner.fields.datetime.required = true;
|
||||
|
||||
var form = createView({
|
||||
View: FormView,
|
||||
model: 'partner',
|
||||
data: this.data,
|
||||
arch:'<form string="Partners"><field name="datetime"/></form>',
|
||||
res_id: 1,
|
||||
translateParameters: { // Avoid issues due to localization formats
|
||||
date_format: '%m/%d/%Y',
|
||||
time_format: '%H:%M',
|
||||
},
|
||||
});
|
||||
testUtils.patch(basicFields.FieldDate, {
|
||||
_setValue: function () {
|
||||
throw "The time format of the language must be taken into account."
|
||||
return this._super.apply(this, arguments);
|
||||
},
|
||||
});
|
||||
form.$buttons.find('.o_form_button_create').click();
|
||||
var expectedDateString = "08/02/2017 12:00"; // 10:00:00 without timezone
|
||||
assert.strictEqual(form.$('.o_field_date input').val(), expectedDateString,
|
||||
'the datetime should be correctly displayed in readonly');
|
||||
|
||||
testUtils.unpatch(basicFields.FieldDate);
|
||||
form.destroy();
|
||||
});
|
||||
|
||||
QUnit.test('datetime field in editable list view', function (assert) {
|
||||
assert.expect(8);
|
||||
|
||||
@ -3010,7 +3069,7 @@ QUnit.module('basic_fields', {
|
||||
QUnit.module('PhoneWidget');
|
||||
|
||||
QUnit.test('phone field in form view on extra small screens', function (assert) {
|
||||
assert.expect(7);
|
||||
assert.expect(8);
|
||||
|
||||
var form = createView({
|
||||
View: FormView,
|
||||
@ -3058,6 +3117,14 @@ QUnit.module('basic_fields', {
|
||||
assert.strictEqual($phoneLink.attr('href'), 'tel:new',
|
||||
"should still have proper tel prefix");
|
||||
|
||||
// save phone with ­ and verify it is removed
|
||||
form.$buttons.find('.o_form_button_edit').click();
|
||||
form.$('input[type="text"].o_field_widget').val('h\u00ADi').trigger('input');
|
||||
form.$buttons.find('.o_form_button_save').click();
|
||||
$phoneLink = form.$('a.o_form_uri.o_field_widget');
|
||||
assert.strictEqual($phoneLink.attr('href'), 'tel:hi',
|
||||
"U+00AD should have been removed");
|
||||
|
||||
form.destroy();
|
||||
});
|
||||
|
||||
|
File diff suppressed because it is too large
Load Diff
@ -110,14 +110,14 @@ var MockServer = Class.extend({
|
||||
console.log('%c[rpc] response' + route, 'color: blue; font-weight: bold;', JSON.parse(resultString));
|
||||
}
|
||||
return JSON.parse(resultString);
|
||||
}).fail(function (result) {
|
||||
}, function (result, event) {
|
||||
var errorString = JSON.stringify(result || false);
|
||||
if (logLevel === 1) {
|
||||
console.log('Mock: (ERROR)' + route, JSON.parse(errorString));
|
||||
} else if (logLevel === 2) {
|
||||
console.log('%c[rpc] response (error) ' + route, 'color: orange; font-weight: bold;', JSON.parse(errorString));
|
||||
}
|
||||
return JSON.parse(errorString);
|
||||
return $.Deferred().reject(errorString, event || $.Event());
|
||||
});
|
||||
},
|
||||
|
||||
@ -229,7 +229,6 @@ var MockServer = Class.extend({
|
||||
if (node.attrs.attrs) {
|
||||
var attrs = pyeval.py_eval(node.attrs.attrs);
|
||||
_.extend(modifiers, attrs);
|
||||
delete node.attrs.attrs;
|
||||
}
|
||||
if (node.attrs.states) {
|
||||
if (!modifiers.invisible) {
|
||||
@ -740,7 +739,7 @@ var MockServer = Class.extend({
|
||||
*/
|
||||
_mockReadProgressBar: function (model, kwargs) {
|
||||
var domain = kwargs.domain;
|
||||
var groupBy = kwargs.groupBy;
|
||||
var groupBy = kwargs.group_by;
|
||||
var progress_bar = kwargs.progress_bar;
|
||||
|
||||
var records = this._getRecords(model, domain || []);
|
||||
@ -921,6 +920,9 @@ var MockServer = Class.extend({
|
||||
|
||||
case '/web/dataset/search_read':
|
||||
return $.when(this._mockSearchReadController(args));
|
||||
|
||||
case '/web/dataset/resequence':
|
||||
return $.when();
|
||||
}
|
||||
if (route.indexOf('/web/image') >= 0 || _.contains(['.png', '.jpg'], route.substr(route.length - 4))) {
|
||||
return $.when();
|
||||
|
@ -146,7 +146,19 @@ QUnit.module('Views', {
|
||||
};
|
||||
|
||||
QUnit.test('simple calendar rendering', function (assert) {
|
||||
assert.expect(22);
|
||||
assert.expect(24);
|
||||
|
||||
this.data.event.records.push({
|
||||
id: 7,
|
||||
user_id: session.uid,
|
||||
partner_id: false,
|
||||
name: "event 7",
|
||||
start: "2016-12-18 09:00:00",
|
||||
stop: "2016-12-18 10:00:00",
|
||||
allday: false,
|
||||
partner_ids: [2],
|
||||
type: 1
|
||||
});
|
||||
|
||||
var calendar = createView({
|
||||
View: CalendarView,
|
||||
@ -187,7 +199,7 @@ QUnit.module('Views', {
|
||||
assert.strictEqual($sidebar.find('.o_selected_range').length, 1, "should highlight the target day in mini calendar");
|
||||
|
||||
calendar.$buttons.find('.o_calendar_button_month').trigger('click'); // display all the month
|
||||
assert.strictEqual(calendar.$('.fc-event').length, 6, "should display 6 events on the month (5 events + 2 week event - 1 'event 6' is filtered)");
|
||||
assert.strictEqual(calendar.$('.fc-event').length, 7, "should display 7 events on the month (5 events + 2 week event - 1 'event 6' is filtered + 1 'Undefined event')");
|
||||
assert.strictEqual($sidebar.find('.o_selected_range').length, 31, "month scale should highlight all days in mini calendar");
|
||||
|
||||
// test filters
|
||||
@ -196,15 +208,19 @@ QUnit.module('Views', {
|
||||
|
||||
var $typeFilter = $sidebar.find('.o_calendar_filter:has(h3:contains(user))');
|
||||
assert.ok($typeFilter.length, "should display 'user' filter");
|
||||
assert.strictEqual($typeFilter.find('.o_calendar_filter_item').length, 2, "should display 2 filter items for 'user'");
|
||||
assert.strictEqual($typeFilter.find('.o_calendar_filter_item').length, 3, "should display 3 filter items for 'user'");
|
||||
|
||||
// filters which has no value should show with string "Undefined" and should show at the last
|
||||
assert.strictEqual($typeFilter.find('.o_calendar_filter_item:last').data('value'), false, "filters having false value should be displayed at last in filter items");
|
||||
assert.strictEqual($typeFilter.find('.o_calendar_filter_item:last > span').text(), "Undefined", "filters having false value should display 'Undefined' string");
|
||||
|
||||
var $attendeesFilter = $sidebar.find('.o_calendar_filter:has(h3:contains(attendees))');
|
||||
assert.ok($attendeesFilter.length, "should display 'attendees' filter");
|
||||
assert.strictEqual($attendeesFilter.find('.o_calendar_filter_item').length, 3, "should display 3 filter items for 'attendees' who use write_model (2 saved + Everything)");
|
||||
assert.ok($attendeesFilter.find('.o_field_many2one').length, "should display one2many search bar for 'attendees' filter");
|
||||
|
||||
assert.strictEqual(calendar.$('.fc-event').length, 6,
|
||||
"should display 6 events ('event 5' counts for 2 because it spans two weeks and thus generate two fc-event elements)");
|
||||
assert.strictEqual(calendar.$('.fc-event').length, 7,
|
||||
"should display 7 events ('event 5' counts for 2 because it spans two weeks and thus generate two fc-event elements)");
|
||||
calendar.$('.o_calendar_filter .o_checkbox input').first().click(); // Disable first filter
|
||||
assert.strictEqual(calendar.$('.fc-event').length, 4, "should now only display 4 event");
|
||||
calendar.$('.o_calendar_filter .o_checkbox input').eq(1).click(); // Disable second filter
|
||||
@ -1715,6 +1731,136 @@ QUnit.module('Views', {
|
||||
|
||||
testUtils.unpatch(mixins.ParentedMixin);
|
||||
});
|
||||
|
||||
QUnit.test('timezone does not affect current day', function (assert) {
|
||||
assert.expect(2);
|
||||
|
||||
var calendar = createView({
|
||||
View: CalendarView,
|
||||
model: 'event',
|
||||
data: this.data,
|
||||
arch:
|
||||
'<calendar date_start="start_date">'+
|
||||
'<field name="name"/>'+
|
||||
'</calendar>',
|
||||
archs: archs,
|
||||
viewOptions: {
|
||||
initialDate: initialDate,
|
||||
},
|
||||
session: {
|
||||
getTZOffset: function () {
|
||||
return -2400; // 40 hours timezone
|
||||
},
|
||||
},
|
||||
|
||||
});
|
||||
|
||||
var $sidebar = calendar.$('.o_calendar_sidebar');
|
||||
|
||||
assert.strictEqual($sidebar.find('.ui-datepicker-current-day').text(), "12", "should highlight the target day");
|
||||
|
||||
// go to previous day
|
||||
$sidebar.find('.ui-datepicker-current-day').prev().click();
|
||||
|
||||
assert.strictEqual($sidebar.find('.ui-datepicker-current-day').text(), "11", "should highlight the selected day");
|
||||
|
||||
calendar.destroy();
|
||||
});
|
||||
|
||||
QUnit.test('form_view_id attribute works (for creating events)', function (assert) {
|
||||
assert.expect(1);
|
||||
|
||||
var calendar = createView({
|
||||
View: CalendarView,
|
||||
model: 'event',
|
||||
data: this.data,
|
||||
arch: '<calendar class="o_calendar_test" '+
|
||||
'date_start="start" '+
|
||||
'date_stop="stop" '+
|
||||
'mode="month" '+
|
||||
'form_view_id="42">'+
|
||||
'<field name="name"/>'+
|
||||
'</calendar>',
|
||||
archs: archs,
|
||||
viewOptions: {
|
||||
initialDate: initialDate,
|
||||
},
|
||||
mockRPC: function (route, args) {
|
||||
if (args.method === "create") {
|
||||
// we simulate here the case where a create call with just
|
||||
// the field name fails. This is a normal flow, the server
|
||||
// reject the create rpc (quick create), then the web client
|
||||
// fall back to a form view. This happens typically when a
|
||||
// model has required fields
|
||||
return $.Deferred().reject('None shall pass!');
|
||||
}
|
||||
return this._super(route, args);
|
||||
},
|
||||
intercepts: {
|
||||
do_action: function (event) {
|
||||
assert.strictEqual(event.data.action.views[0][0], 42,
|
||||
"should do a do_action with view id 42");
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
var $cell = calendar.$('.fc-day-grid .fc-row:eq(2) .fc-day:eq(2)');
|
||||
testUtils.triggerMouseEvent($cell, "mousedown");
|
||||
testUtils.triggerMouseEvent($cell, "mouseup");
|
||||
var $input = $('.modal-body input:first');
|
||||
$input.val("It's just a fleshwound").trigger('input');
|
||||
$('.modal button.btn:contains(Create)').trigger('click');
|
||||
|
||||
calendar.destroy();
|
||||
});
|
||||
|
||||
QUnit.test('calendar fallback to form view id in action if necessary', function (assert) {
|
||||
assert.expect(1);
|
||||
|
||||
var calendar = createView({
|
||||
View: CalendarView,
|
||||
model: 'event',
|
||||
data: this.data,
|
||||
arch: '<calendar class="o_calendar_test" '+
|
||||
'date_start="start" '+
|
||||
'date_stop="stop" '+
|
||||
'mode="month"> '+
|
||||
'<field name="name"/>'+
|
||||
'</calendar>',
|
||||
archs: archs,
|
||||
viewOptions: {
|
||||
initialDate: initialDate,
|
||||
action: {views: [[1, 'kanban'], [43, 'form']]}
|
||||
},
|
||||
mockRPC: function (route, args) {
|
||||
if (args.method === "create") {
|
||||
// we simulate here the case where a create call with just
|
||||
// the field name fails. This is a normal flow, the server
|
||||
// reject the create rpc (quick create), then the web client
|
||||
// fall back to a form view. This happens typically when a
|
||||
// model has required fields
|
||||
return $.Deferred().reject('None shall pass!');
|
||||
}
|
||||
return this._super(route, args);
|
||||
},
|
||||
intercepts: {
|
||||
do_action: function (event) {
|
||||
assert.strictEqual(event.data.action.views[0][0], 43,
|
||||
"should do a do_action with view id 43");
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
var $cell = calendar.$('.fc-day-grid .fc-row:eq(2) .fc-day:eq(2)');
|
||||
testUtils.triggerMouseEvent($cell, "mousedown");
|
||||
testUtils.triggerMouseEvent($cell, "mouseup");
|
||||
var $input = $('.modal-body input:first');
|
||||
$input.val("It's just a fleshwound").trigger('input');
|
||||
$('.modal button.btn:contains(Create)').trigger('click');
|
||||
|
||||
calendar.destroy();
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
});
|
||||
|
@ -1623,6 +1623,37 @@ QUnit.module('Views', {
|
||||
form.destroy();
|
||||
});
|
||||
|
||||
QUnit.test('duplicating a record preserve the context', function (assert) {
|
||||
assert.expect(2);
|
||||
|
||||
var form = createView({
|
||||
View: FormView,
|
||||
model: 'partner',
|
||||
data: this.data,
|
||||
arch: '<form string="Partners">' +
|
||||
'<field name="foo"/>' +
|
||||
'</form>',
|
||||
res_id: 1,
|
||||
viewOptions: {sidebar: true, context: {hey: 'hoy'}},
|
||||
mockRPC: function (route, args) {
|
||||
if (args.method === 'read') {
|
||||
// should have 2 read, one for initial load, second for
|
||||
// read after duplicating
|
||||
assert.strictEqual(args.kwargs.context.hey, 'hoy',
|
||||
"should have send the correct context");
|
||||
}
|
||||
if (args.method === 'search_read' && args.model === 'ir.attachment') {
|
||||
return $.when([]);
|
||||
}
|
||||
return this._super.apply(this, arguments);
|
||||
},
|
||||
});
|
||||
|
||||
form.sidebar.$('a:contains(Duplicate)').click();
|
||||
|
||||
form.destroy();
|
||||
});
|
||||
|
||||
QUnit.test('cannot duplicate a record', function (assert) {
|
||||
assert.expect(2);
|
||||
|
||||
@ -6494,5 +6525,35 @@ QUnit.module('Views', {
|
||||
testUtils.unpatch(mixins.ParentedMixin);
|
||||
});
|
||||
|
||||
QUnit.test('do not change pager when discarding current record', function (assert) {
|
||||
assert.expect(2);
|
||||
|
||||
var form = createView({
|
||||
View: FormView,
|
||||
model: 'partner',
|
||||
data: this.data,
|
||||
arch: '<form string="Partners">' +
|
||||
'<field name="foo"/>' +
|
||||
'</form>',
|
||||
viewOptions: {
|
||||
ids: [1, 2],
|
||||
index: 0,
|
||||
},
|
||||
res_id: 2,
|
||||
});
|
||||
|
||||
assert.strictEqual(form.pager.$('.o_pager_counter').text().trim(), '2 / 2',
|
||||
'pager should indicate that we are on second record');
|
||||
|
||||
form.$buttons.find('.o_form_button_edit').click();
|
||||
form.$buttons.find('.o_form_button_cancel').click();
|
||||
|
||||
assert.strictEqual(form.pager.$('.o_pager_counter').text().trim(), '2 / 2',
|
||||
'pager should not have changed');
|
||||
|
||||
form.destroy();
|
||||
});
|
||||
|
||||
|
||||
});
|
||||
});
|
||||
|
@ -1130,7 +1130,7 @@ QUnit.module('Views', {
|
||||
});
|
||||
|
||||
QUnit.test('create a column in grouped on m2o', function (assert) {
|
||||
assert.expect(13);
|
||||
assert.expect(14);
|
||||
|
||||
var nbRPCs = 0;
|
||||
var kanban = createView({
|
||||
@ -1149,6 +1149,11 @@ QUnit.module('Views', {
|
||||
if (args.method === 'name_create') {
|
||||
assert.ok(true, "should call name_create");
|
||||
}
|
||||
//Create column will call resequence to set column order
|
||||
if (route === '/web/dataset/resequence') {
|
||||
assert.ok(true, "should call resequence");
|
||||
return $.when(true);
|
||||
}
|
||||
return this._super(route, args);
|
||||
},
|
||||
});
|
||||
@ -1229,7 +1234,7 @@ QUnit.module('Views', {
|
||||
});
|
||||
|
||||
QUnit.test('delete a column in grouped on m2o', function (assert) {
|
||||
assert.expect(32);
|
||||
assert.expect(33);
|
||||
|
||||
testUtils.patch(KanbanRenderer, {
|
||||
_renderGrouped: function () {
|
||||
@ -1716,6 +1721,73 @@ QUnit.module('Views', {
|
||||
kanban.destroy();
|
||||
});
|
||||
|
||||
QUnit.test('button executes action and check domain', function (assert) {
|
||||
assert.expect(2);
|
||||
|
||||
var data = this.data;
|
||||
data.partner.fields.active = {string: "Active", type: "boolean", default: true};
|
||||
for (var k in this.data.partner.records) {
|
||||
data.partner.records[k].active = true;
|
||||
}
|
||||
|
||||
var kanban = createView({
|
||||
View: KanbanView,
|
||||
model: "partner",
|
||||
data: data,
|
||||
arch:
|
||||
'<kanban>' +
|
||||
'<templates><div t-name="kanban-box">' +
|
||||
'<field name="foo"/>' +
|
||||
'<field name="active"/>' +
|
||||
'<button type="object" name="a1" />' +
|
||||
'<button type="object" name="toggle_active" />' +
|
||||
'</div></templates>' +
|
||||
'</kanban>',
|
||||
});
|
||||
|
||||
testUtils.intercept(kanban, 'execute_action', function (event) {
|
||||
data.partner.records[0].active = false;
|
||||
event.data.on_closed();
|
||||
});
|
||||
|
||||
assert.strictEqual(kanban.$('.o_kanban_record:contains(yop)').length, 1, "should display 'yop' record");
|
||||
kanban.$('.o_kanban_record:contains(yop) button[data-name="toggle_active"]').click();
|
||||
assert.strictEqual(kanban.$('.o_kanban_record:contains(yop)').length, 0, "should remove 'yop' record from the view");
|
||||
|
||||
kanban.destroy();
|
||||
});
|
||||
|
||||
QUnit.test('button executes action with domain field not in view', function (assert) {
|
||||
assert.expect(1);
|
||||
|
||||
var kanban = createView({
|
||||
View: KanbanView,
|
||||
model: "partner",
|
||||
data: this.data,
|
||||
domain: [['bar', '=', true]],
|
||||
arch:
|
||||
'<kanban>' +
|
||||
'<templates><div t-name="kanban-box">' +
|
||||
'<field name="foo"/>' +
|
||||
'<button type="object" name="a1" />' +
|
||||
'<button type="object" name="toggle_action" />' +
|
||||
'</div></templates>' +
|
||||
'</kanban>',
|
||||
});
|
||||
|
||||
testUtils.intercept(kanban, 'execute_action', function (event) {
|
||||
event.data.on_closed();
|
||||
});
|
||||
|
||||
try {
|
||||
kanban.$('.o_kanban_record:contains(yop) button[data-name="toggle_action"]').click();
|
||||
assert.strictEqual(true, true, 'Everything went fine');
|
||||
} catch (e) {
|
||||
assert.strictEqual(true, false, 'Error triggered at action execution');
|
||||
}
|
||||
kanban.destroy();
|
||||
});
|
||||
|
||||
QUnit.test('rendering date and datetime', function (assert) {
|
||||
assert.expect(2);
|
||||
|
||||
@ -1948,7 +2020,7 @@ QUnit.module('Views', {
|
||||
});
|
||||
|
||||
QUnit.test('archive new kanban column', function (assert) {
|
||||
assert.expect(15);
|
||||
assert.expect(16);
|
||||
|
||||
this.data.partner.fields.active = {string: 'Active', type: 'char', default: true};
|
||||
|
||||
@ -1977,6 +2049,7 @@ QUnit.module('Views', {
|
||||
kanban.$('.o_column_quick_create input').val('new colum');
|
||||
kanban.$('.o_column_quick_create button.o_kanban_add').click();
|
||||
rpcs.push('/web/dataset/call_kw/product/name_create');
|
||||
rpcs.push('/web/dataset/resequence');
|
||||
assert.verifySteps(rpcs);
|
||||
|
||||
// add a record inside
|
||||
@ -2477,6 +2550,176 @@ QUnit.module('Views', {
|
||||
kanban.destroy();
|
||||
});
|
||||
|
||||
QUnit.test('column progressbars on archiving records update counter', function (assert) {
|
||||
assert.expect(4);
|
||||
|
||||
// add active field on partner model and make all records active
|
||||
this.data.partner.fields.active = {string: 'Active', type: 'char', default: true};
|
||||
|
||||
var kanban = createView({
|
||||
View: KanbanView,
|
||||
model: 'partner',
|
||||
data: this.data,
|
||||
arch:
|
||||
'<kanban>' +
|
||||
'<field name="active"/>' +
|
||||
'<field name="bar"/>' +
|
||||
'<field name="int_field"/>' +
|
||||
'<progressbar field="foo" colors=\'{"yop": "success", "gnap": "warning", "blip": "danger"}\' sum_field="int_field"/>' +
|
||||
'<templates><t t-name="kanban-box">' +
|
||||
'<div>' +
|
||||
'<field name="name"/>' +
|
||||
'</div>' +
|
||||
'</t></templates>' +
|
||||
'</kanban>',
|
||||
groupBy: ['bar'],
|
||||
});
|
||||
|
||||
assert.strictEqual(kanban.$('.o_kanban_group:eq(1) .o_kanban_counter_side').text(), "36",
|
||||
"counter should contain the correct value");
|
||||
assert.strictEqual(kanban.$('.o_kanban_group:eq(1) .o_kanban_counter_progress > .progress-bar:first').data('originalTitle'), "1 yop",
|
||||
"the counter progressbars should be correctly displayed");
|
||||
|
||||
// archive all records of the second columns
|
||||
kanban.$('.o_kanban_group:eq(1) .o_column_archive').click();
|
||||
|
||||
assert.strictEqual(kanban.$('.o_kanban_group:eq(1) .o_kanban_counter_side').text(), "0",
|
||||
"counter should contain the correct value");
|
||||
assert.strictEqual(kanban.$('.o_kanban_group:eq(1) .o_kanban_counter_progress > .progress-bar:first').data('originalTitle'), "0 yop",
|
||||
"the counter progressbars should have been correctly updated");
|
||||
|
||||
kanban.destroy();
|
||||
});
|
||||
|
||||
QUnit.test('kanban with progressbars: correctly update env when archiving records', function (assert) {
|
||||
assert.expect(3);
|
||||
|
||||
// add active field on partner model and make all records active
|
||||
this.data.partner.fields.active = {string: 'Active', type: 'char', default: true};
|
||||
|
||||
var kanban = createView({
|
||||
View: KanbanView,
|
||||
model: 'partner',
|
||||
data: this.data,
|
||||
arch:
|
||||
'<kanban>' +
|
||||
'<field name="active"/>' +
|
||||
'<field name="bar"/>' +
|
||||
'<field name="int_field"/>' +
|
||||
'<progressbar field="foo" colors=\'{"yop": "success", "gnap": "warning", "blip": "danger"}\' sum_field="int_field"/>' +
|
||||
'<templates><t t-name="kanban-box">' +
|
||||
'<div>' +
|
||||
'<field name="name"/>' +
|
||||
'</div>' +
|
||||
'</t></templates>' +
|
||||
'</kanban>',
|
||||
groupBy: ['bar'],
|
||||
intercepts: {
|
||||
env_updated: function (ev) {
|
||||
assert.step(ev.data.ids);
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
// archive all records of the first column
|
||||
kanban.$('.o_kanban_group:first .o_column_archive').click();
|
||||
|
||||
assert.verifySteps([
|
||||
[1, 2, 3, 4],
|
||||
[1, 2, 3],
|
||||
]);
|
||||
|
||||
kanban.destroy();
|
||||
});
|
||||
|
||||
QUnit.test('RPCs when (re)loading kanban view progressbars', function (assert) {
|
||||
assert.expect(9);
|
||||
|
||||
var kanban = createView({
|
||||
View: KanbanView,
|
||||
model: 'partner',
|
||||
data: this.data,
|
||||
arch:
|
||||
'<kanban>' +
|
||||
'<field name="bar"/>' +
|
||||
'<field name="int_field"/>' +
|
||||
'<progressbar field="foo" colors=\'{"yop": "success", "gnap": "warning", "blip": "danger"}\' sum_field="int_field"/>' +
|
||||
'<templates><t t-name="kanban-box">' +
|
||||
'<div>' +
|
||||
'<field name="name"/>' +
|
||||
'</div>' +
|
||||
'</t></templates>' +
|
||||
'</kanban>',
|
||||
groupBy: ['bar'],
|
||||
mockRPC: function (route, args) {
|
||||
assert.step(args.method || route);
|
||||
return this._super.apply(this, arguments);
|
||||
},
|
||||
});
|
||||
|
||||
kanban.reload();
|
||||
|
||||
assert.verifySteps([
|
||||
// initial load
|
||||
'read_group',
|
||||
'/web/dataset/search_read',
|
||||
'/web/dataset/search_read',
|
||||
'read_progress_bar',
|
||||
// reload
|
||||
'read_group',
|
||||
'/web/dataset/search_read',
|
||||
'/web/dataset/search_read',
|
||||
'read_progress_bar',
|
||||
]);
|
||||
|
||||
kanban.destroy();
|
||||
});
|
||||
|
||||
QUnit.test('drag & drop records grouped by m2o with progressbar', function (assert) {
|
||||
assert.expect(4);
|
||||
|
||||
this.data.partner.records[0].product_id = false;
|
||||
|
||||
var kanban = createView({
|
||||
View: KanbanView,
|
||||
model: 'partner',
|
||||
data: this.data,
|
||||
arch:
|
||||
'<kanban>' +
|
||||
'<progressbar field="foo" colors=\'{"yop": "success", "gnap": "warning", "blip": "danger"}\'/>' +
|
||||
'<templates><t t-name="kanban-box">' +
|
||||
'<div>' +
|
||||
'<field name="int_field"/>' +
|
||||
'</div>' +
|
||||
'</t></templates>' +
|
||||
'</kanban>',
|
||||
groupBy: ['product_id'],
|
||||
mockRPC: function (route, args) {
|
||||
if (route === '/web/dataset/resequence') {
|
||||
return $.when(true);
|
||||
}
|
||||
return this._super(route, args);
|
||||
},
|
||||
});
|
||||
|
||||
assert.strictEqual(kanban.$('.o_kanban_group:eq(0) .o_kanban_counter_side').text(), "1",
|
||||
"counter should contain the correct value");
|
||||
|
||||
testUtils.dragAndDrop(kanban.$('.o_kanban_group:eq(0) .o_kanban_record:eq(0)'), kanban.$('.o_kanban_group:eq(1)'));
|
||||
assert.strictEqual(kanban.$('.o_kanban_group:eq(0) .o_kanban_counter_side').text(), "0",
|
||||
"counter should contain the correct value");
|
||||
|
||||
testUtils.dragAndDrop(kanban.$('.o_kanban_group:eq(1) .o_kanban_record:eq(2)'), kanban.$('.o_kanban_group:eq(0)'));
|
||||
assert.strictEqual(kanban.$('.o_kanban_group:eq(0) .o_kanban_counter_side').text(), "1",
|
||||
"counter should contain the correct value");
|
||||
|
||||
testUtils.dragAndDrop(kanban.$('.o_kanban_group:eq(0) .o_kanban_record:eq(0)'), kanban.$('.o_kanban_group:eq(1)'));
|
||||
assert.strictEqual(kanban.$('.o_kanban_group:eq(0) .o_kanban_counter_side').text(), "0",
|
||||
"counter should contain the correct value");
|
||||
|
||||
kanban.destroy();
|
||||
});
|
||||
|
||||
QUnit.test('keep adding quickcreate in first column after a record from this column was moved', function (assert) {
|
||||
assert.expect(2);
|
||||
|
||||
|
@ -3310,7 +3310,6 @@ QUnit.module('Views', {
|
||||
list.destroy();
|
||||
});
|
||||
|
||||
|
||||
QUnit.test('basic support for widgets', function (assert) {
|
||||
assert.expect(1);
|
||||
|
||||
@ -3416,6 +3415,46 @@ QUnit.module('Views', {
|
||||
testUtils.unpatch(mixins.ParentedMixin);
|
||||
});
|
||||
|
||||
QUnit.test('concurrent reloads finishing in inverse order', function (assert) {
|
||||
assert.expect(3);
|
||||
|
||||
var blockSearchRead = false;
|
||||
var def = $.Deferred();
|
||||
var list = createView({
|
||||
View: ListView,
|
||||
model: 'foo',
|
||||
data: this.data,
|
||||
arch: '<tree><field name="foo"/></tree>',
|
||||
mockRPC: function (route) {
|
||||
var result = this._super.apply(this, arguments);
|
||||
if (route === '/web/dataset/search_read' && blockSearchRead) {
|
||||
return $.when(def).then(_.constant(result));
|
||||
}
|
||||
return result;
|
||||
},
|
||||
});
|
||||
|
||||
assert.strictEqual(list.$('.o_list_view .o_data_row').length, 4,
|
||||
"list view should contain 4 records");
|
||||
|
||||
// reload with a domain (this request is blocked)
|
||||
blockSearchRead = true;
|
||||
list.reload({domain: [['foo', '=', 'yop']]});
|
||||
|
||||
assert.strictEqual(list.$('.o_list_view .o_data_row').length, 4,
|
||||
"list view should still contain 4 records (search_read being blocked)");
|
||||
|
||||
// reload without the domain
|
||||
blockSearchRead = false;
|
||||
list.reload({domain: []});
|
||||
|
||||
// unblock the RPC
|
||||
def.resolve();
|
||||
assert.strictEqual(list.$('.o_list_view .o_data_row').length, 4,
|
||||
"list view should still contain 4 records");
|
||||
|
||||
list.destroy();
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
|
@ -960,4 +960,78 @@ QUnit.module('Views', {
|
||||
|
||||
pivot.destroy();
|
||||
});
|
||||
|
||||
QUnit.test('Row and column groupbys plus a domain', function (assert) {
|
||||
assert.expect(3);
|
||||
|
||||
var pivot = createView({
|
||||
View: PivotView,
|
||||
model: "partner",
|
||||
data: this.data,
|
||||
arch: '<pivot>' +
|
||||
'<field name="foo" type="measure"/>' +
|
||||
'</pivot>',
|
||||
});
|
||||
|
||||
// Set a column groupby
|
||||
pivot.$('thead .o_pivot_header_cell_closed').click();
|
||||
pivot.$('.o_field_selection li[data-field=customer] a').click();
|
||||
|
||||
// Set a Row groupby
|
||||
pivot.$('tbody .o_pivot_header_cell_closed').click();
|
||||
pivot.$('.o_pivot_field_menu li[data-field=product_id] a').click();
|
||||
|
||||
// Set a domain
|
||||
pivot.update({domain: [['product_id', '=', 41]]});
|
||||
|
||||
var expectedContext = {pivot_column_groupby: ['customer'],
|
||||
pivot_measures: ['foo'],
|
||||
pivot_row_groupby: ['product_id']};
|
||||
|
||||
// Mock 'save as favorite'
|
||||
assert.deepEqual(pivot.getContext(), expectedContext,
|
||||
'The pivot view should have the right context');
|
||||
|
||||
var $xpadHeader = pivot.$('tbody .o_pivot_header_cell_closed[data-original-title=Product]');
|
||||
assert.equal($xpadHeader.length, 1,
|
||||
'There should be only one product line because of the domain');
|
||||
|
||||
assert.equal($xpadHeader.text(), 'xpad',
|
||||
'The product should be the right one');
|
||||
|
||||
pivot.destroy();
|
||||
});
|
||||
|
||||
QUnit.test('parallel data loading should discard all but the last one', function (assert) {
|
||||
assert.expect(2);
|
||||
|
||||
var def;
|
||||
|
||||
var pivot = createView({
|
||||
View: PivotView,
|
||||
model: "partner",
|
||||
data: this.data,
|
||||
arch: '<pivot>' +
|
||||
'<field name="foo" type="measure"/>' +
|
||||
'</pivot>',
|
||||
mockRPC: function (route, args) {
|
||||
var result = this._super.apply(this, arguments);
|
||||
if (args.method === 'read_group') {
|
||||
return $.when(def).then(_.constant(result));
|
||||
}
|
||||
return result;
|
||||
},
|
||||
});
|
||||
|
||||
def = $.Deferred();
|
||||
pivot.update({groupBy: ['product_id']});
|
||||
pivot.update({groupBy: ['product_id', 'customer']});
|
||||
def.resolve();
|
||||
|
||||
assert.strictEqual(pivot.$('.o_pivot_cell_value').length, 6,
|
||||
"should have 6 cells");
|
||||
assert.strictEqual(pivot.$('tbody tr').length, 6,
|
||||
"should have 6 rows");
|
||||
pivot.destroy();
|
||||
});
|
||||
});});
|
||||
|
@ -33,6 +33,6 @@ class WebSuite(flectra.tests.HttpCase):
|
||||
filename = asset['filename']
|
||||
if not filename or asset['atype'] != 'text/javascript':
|
||||
continue
|
||||
with open(filename, 'r') as fp:
|
||||
if RE_ONLY.search(fp.read()):
|
||||
with open(filename, 'rb') as fp:
|
||||
if RE_ONLY.search(fp.read().decode('utf-8')):
|
||||
self.fail("`QUnit.only()` used in file %r" % asset['url'])
|
||||
|
@ -67,6 +67,12 @@
|
||||
<t t-call-assets="web_editor.summernote" t-css="false"/>
|
||||
<t t-call-assets="web_editor.assets_editor" t-css="false"/>
|
||||
<t t-call-assets="web.report_assets_editor" t-css="false"/>
|
||||
|
||||
<script type="text/javascript" src="/web/static/src/js/services/session.js"></script>
|
||||
<script type="text/javascript" src="/web_editor/static/src/js/content/body_manager.js"/>
|
||||
<script type="text/javascript" src="/web_editor/static/src/js/root_widget.js"/>
|
||||
<script type="text/javascript" src="/web_editor/static/src/js/iframe.js"></script>
|
||||
<script t-if="enable_editor and inline_mode" type="text/javascript" src="/web_editor/static/src/js/inline.js"></script>
|
||||
</t>
|
||||
</head>
|
||||
<body class="container">
|
||||
|
@ -400,7 +400,7 @@
|
||||
<t t-if="not disable_database_manager">
|
||||
<a class="" href="/web/database/manager">Manage Databases</a> <!--|-->
|
||||
</t>
|
||||
<!-- <a href="https://www.flectra.com" target="_blank">Powered by <span>Flectra</span></a> -->
|
||||
<!--<a href="https://www.flectra.com" target="_blank">Powered by <span>Flectra</span></a>-->
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -813,7 +813,7 @@
|
||||
</div>
|
||||
</nav>
|
||||
<div class="o_main">
|
||||
<div class="f_launcher">
|
||||
<div class="f_launcher" groups="base.group_user,base.group_portal">
|
||||
<t t-call="web.menu_launcher"/>
|
||||
</div>
|
||||
<div class="o_main_content"/>
|
||||
|
@ -160,7 +160,7 @@ class Web_Editor(http.Controller):
|
||||
'name': name,
|
||||
'type': 'url',
|
||||
'url': url,
|
||||
'public': True,
|
||||
'public': res_model == 'ir.ui.view',
|
||||
'res_id': res_id,
|
||||
'res_model': res_model,
|
||||
})
|
||||
@ -187,7 +187,7 @@ class Web_Editor(http.Controller):
|
||||
'name': c_file.filename,
|
||||
'datas': base64.b64encode(data),
|
||||
'datas_fname': c_file.filename,
|
||||
'public': True,
|
||||
'public': res_model == 'ir.ui.view',
|
||||
'res_id': res_id,
|
||||
'res_model': res_model,
|
||||
})
|
||||
@ -244,7 +244,7 @@ class Web_Editor(http.Controller):
|
||||
## @param bundles - True if the bundles views must be fetched (default to False)
|
||||
## @param bundles_restriction - Names of the bundle in which to look for less files (if empty, search in all of them)
|
||||
## @returns a dictionary with views info in the views key and style info in the less key
|
||||
@http.route("/web_editor/get_assets_editor_resources", type="json", auth="user")
|
||||
@http.route("/web_editor/get_assets_editor_resources", type="json", auth="user", website=True)
|
||||
def get_assets_editor_resources(self, key, get_views=True, get_less=True, bundles=False, bundles_restriction=[]):
|
||||
# Related views must be fetched if the user wants the views and/or the style
|
||||
views = request.env["ir.ui.view"].get_related_views(key, bundles=bundles)
|
||||
@ -361,16 +361,17 @@ class Web_Editor(http.Controller):
|
||||
|
||||
# Check if the file to save had already been modified
|
||||
custom_attachment = IrAttachment.search([("url", "=", custom_url)])
|
||||
datas = base64.b64encode((content or "\n").encode("utf-8"))
|
||||
if custom_attachment:
|
||||
# If it was already modified, simply override the corresponding attachment content
|
||||
custom_attachment.write({"datas": base64.b64encode(content.encode("utf-8"))})
|
||||
custom_attachment.write({"datas": datas})
|
||||
else:
|
||||
# If not, create a new attachment to copy the original LESS file content, with its modifications
|
||||
IrAttachment.create(dict(
|
||||
name = custom_url,
|
||||
type = "binary",
|
||||
mimetype = "text/less",
|
||||
datas = base64.b64encode(content.encode("utf-8")),
|
||||
datas = datas,
|
||||
datas_fname = url.split("/")[-1],
|
||||
url = custom_url, # Having an attachment of "binary" type with an non empty "url" field
|
||||
# is quite of an hack. This allows to fetch the "datas" field by adding
|
||||
|
@ -3,6 +3,7 @@
|
||||
|
||||
from flectra import models
|
||||
from flectra.http import request
|
||||
from flectra.osv import expression
|
||||
|
||||
|
||||
class IrHttp(models.AbstractModel):
|
||||
@ -19,3 +20,8 @@ class IrHttp(models.AbstractModel):
|
||||
context['translatable'] = True
|
||||
request.context = context
|
||||
return super(IrHttp, cls)._dispatch()
|
||||
|
||||
@classmethod
|
||||
def _get_translation_frontend_modules_domain(cls):
|
||||
domain = super(IrHttp, cls)._get_translation_frontend_modules_domain()
|
||||
return expression.OR([domain, [('name', '=', 'web_editor')]])
|
||||
|
@ -173,6 +173,7 @@ class Contact(models.AbstractModel):
|
||||
@api.model
|
||||
def attributes(self, record, field_name, options, values):
|
||||
attrs = super(Contact, self).attributes(record, field_name, options, values)
|
||||
options.pop('template_options') # remove options not specific to this widget
|
||||
attrs['data-oe-contact-options'] = json.dumps(options)
|
||||
return attrs
|
||||
|
||||
|
@ -472,7 +472,8 @@ define([
|
||||
onImageUpload: options.onImageUpload,
|
||||
onImageUploadError: options.onImageUploadError,
|
||||
onMediaDelete: options.onMediaDelete,
|
||||
onToolbarClick: options.onToolbarClick
|
||||
onToolbarClick: options.onToolbarClick,
|
||||
onUpload: options.onUpload,
|
||||
});
|
||||
|
||||
var styleInfo = modules.editor.styleFromNode(layoutInfo.editable());
|
||||
|
@ -1,256 +1,41 @@
|
||||
define([
|
||||
'summernote/core/list',
|
||||
'summernote/core/dom',
|
||||
'summernote/core/key',
|
||||
'summernote/core/agent',
|
||||
'summernote/core/range'
|
||||
'summernote/core/list',
|
||||
'summernote/core/dom',
|
||||
'summernote/core/key',
|
||||
'summernote/core/agent',
|
||||
'summernote/core/range'
|
||||
], function (list, dom, key, agent, range) {
|
||||
var Clipboard = function (handler) {
|
||||
var $paste;
|
||||
// FLECTRA override: use 0.8.10 version of this, adapted for the old summernote
|
||||
// version flectra is using
|
||||
var Clipboard = function (handler) {
|
||||
/**
|
||||
* paste by clipboard event
|
||||
*
|
||||
* @param {Event} event
|
||||
*/
|
||||
var pasteByEvent = function (event) {
|
||||
if (["INPUT", "TEXTAREA"].indexOf(event.target.tagName) !== -1) {
|
||||
// ODOO override: from old summernote version
|
||||
return;
|
||||
}
|
||||
|
||||
this.attach = function (layoutInfo) {
|
||||
// [workaround] getting image from clipboard
|
||||
// - IE11 and Firefox: CTRL+v hook
|
||||
// - Webkit: event.clipboardData
|
||||
if (agent.isMSIE && agent.browserVersion > 10) {
|
||||
$paste = $('<div />').attr('contenteditable', true).css({
|
||||
position : 'absolute',
|
||||
left : -100000,
|
||||
opacity : 0
|
||||
});
|
||||
var clipboardData = event.originalEvent.clipboardData;
|
||||
var layoutInfo = dom.makeLayoutInfo(event.currentTarget || event.target);
|
||||
var $editable = layoutInfo.editable();
|
||||
|
||||
layoutInfo.editable().on('keydown', function (e) {
|
||||
if (e.ctrlKey && e.keyCode === key.code.V) {
|
||||
handler.invoke('saveRange', layoutInfo.editable());
|
||||
$paste.focus();
|
||||
if (clipboardData && clipboardData.items && clipboardData.items.length) {
|
||||
var item = list.head(clipboardData.items);
|
||||
if (item.kind === 'file' && item.type.indexOf('image/') !== -1) {
|
||||
handler.insertImages(layoutInfo, [item.getAsFile()]);
|
||||
}
|
||||
handler.invoke('editor.afterCommand', $editable);
|
||||
}
|
||||
};
|
||||
|
||||
setTimeout(function () {
|
||||
pasteByHook(layoutInfo);
|
||||
}, 0);
|
||||
}
|
||||
});
|
||||
|
||||
layoutInfo.editable().before($paste);
|
||||
} else {
|
||||
layoutInfo.editable().on('paste', pasteByEvent);
|
||||
}
|
||||
this.attach = function (layoutInfo) {
|
||||
layoutInfo.editable().on('paste', pasteByEvent);
|
||||
};
|
||||
};
|
||||
|
||||
var pasteByHook = function (layoutInfo) {
|
||||
var $editable = layoutInfo.editable();
|
||||
var node = $paste[0].firstChild;
|
||||
|
||||
if (dom.isImg(node)) {
|
||||
var dataURI = node.src;
|
||||
var decodedData = atob(dataURI.split(',')[1]);
|
||||
var array = new Uint8Array(decodedData.length);
|
||||
for (var i = 0; i < decodedData.length; i++) {
|
||||
array[i] = decodedData.charCodeAt(i);
|
||||
}
|
||||
|
||||
var blob = new Blob([array], { type : 'image/png' });
|
||||
blob.name = 'clipboard.png';
|
||||
|
||||
handler.invoke('restoreRange', $editable);
|
||||
handler.invoke('focus', $editable);
|
||||
handler.insertImages(layoutInfo, [blob]);
|
||||
} else {
|
||||
var pasteContent = $('<div />').html($paste.html()).html();
|
||||
handler.invoke('restoreRange', $editable);
|
||||
handler.invoke('focus', $editable);
|
||||
|
||||
if (pasteContent) {
|
||||
handler.invoke('pasteHTML', $editable, pasteContent);
|
||||
}
|
||||
}
|
||||
|
||||
$paste.empty();
|
||||
};
|
||||
|
||||
/**
|
||||
* paste by clipboard event
|
||||
*
|
||||
* @param {Event} event
|
||||
*/
|
||||
var pasteByEvent = function (event) {
|
||||
var clipboardData = event.originalEvent.clipboardData;
|
||||
var layoutInfo = dom.makeLayoutInfo(event.currentTarget || event.target);
|
||||
var $editable = layoutInfo.editable();
|
||||
|
||||
if (["INPUT", "TEXTAREA"].indexOf(event.target.tagName) !== -1) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (clipboardData && clipboardData.items && clipboardData.items.length) {
|
||||
var item = list.head(clipboardData.items);
|
||||
if (item.kind === 'file' && item.type.indexOf('image/') !== -1) {
|
||||
handler.insertImages(layoutInfo, [item.getAsFile()]);
|
||||
}
|
||||
handler.invoke('editor.afterCommand', $editable);
|
||||
}
|
||||
|
||||
event.preventDefault();
|
||||
|
||||
var html = clipboardData.getData("text/html");
|
||||
var $node = $('<div/>').html(html);
|
||||
// if copying source did not provide html, default to plain text
|
||||
if(!html) {
|
||||
$node.text(clipboardData.getData("text/plain")).html(function(_, html){
|
||||
return html.replace(/\r?\n/g,'<br>');
|
||||
});
|
||||
}
|
||||
pasteContent($node, layoutInfo, $editable);
|
||||
};
|
||||
|
||||
/*
|
||||
remove undesirable tag
|
||||
filter classes and style attributes
|
||||
remove undesirable attributes
|
||||
*/
|
||||
var filter_tag = function ($nodes, $editable) {
|
||||
return $nodes.each(function() {
|
||||
var $node = $(this);
|
||||
|
||||
if ($node.attr('style')) {
|
||||
var style = _.filter(_.compact($node.attr('style').split(/\s*;\s*/)), function (style) {
|
||||
style = style.split(/\s*:\s*/);
|
||||
return /width|height|color|background-color|font-weight|text-align|font-style|text-decoration/i.test(style[0]) &&
|
||||
!(style[1] === 'initial' || style[1] === 'inherit' || $node.css(style[0]) === $editable.css(style[0]) ||
|
||||
(style[0] === 'background-color' && style[1] === 'rgb(255, 255, 255)') ||
|
||||
(style[0] === 'color' && style[1] === 'rgb(0, 0, 0)'));
|
||||
}).join(';');
|
||||
if (style.length) {
|
||||
$node.attr('style', style);
|
||||
} else {
|
||||
$node.removeAttr('style');
|
||||
}
|
||||
}
|
||||
|
||||
if ($node.attr('class')) {
|
||||
var classes = _.filter($node.attr('class').split(/\s+/), function (style) {
|
||||
return /(^|\s)(fa|pull|text|bg)(\s|-|$)/.test(style);
|
||||
}).join(' ');
|
||||
if (classes.length) {
|
||||
$node.attr('class', classes);
|
||||
} else {
|
||||
$node.removeAttr('class');
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
var pasteContent = function ($node, layoutInfo, $editable) {
|
||||
$node.find('meta, script, style').remove();
|
||||
filter_tag($node.find('*'), $editable).removeAttr('title', 'alt', 'id', 'contenteditable');
|
||||
|
||||
/*
|
||||
remove unless span and unwant font
|
||||
*/
|
||||
$node.find('span, font').filter(':not([class]):not([style])').each(function () {
|
||||
$(this).replaceWith($(this).contents());
|
||||
});
|
||||
$node.find('span + span').each(function () {
|
||||
|
||||
if (dom.isText(this.previousSibling)) {
|
||||
if (dom.isVisibleText(this.previousSibling)) {
|
||||
return;
|
||||
} else { // keep space between 2 tags, but can merge the both tags
|
||||
$(this).prev().append(this.previousSibling);
|
||||
}
|
||||
}
|
||||
if ($(this).attr('class') === $(this).prev().attr('class') && $(this).attr('style') === $(this).prev().attr('style')) {
|
||||
$(this).prev().append($(this).contents());
|
||||
$(this).remove();
|
||||
}
|
||||
});
|
||||
|
||||
// remove empty table row and td
|
||||
var $tdr;
|
||||
while(($tdr = $node.find('tr:empty, td:empty, th:empty, tbody:empty, t-head:empty, table:empty')) && $tdr.length) {
|
||||
$tdr.remove();
|
||||
}
|
||||
|
||||
/*
|
||||
reset architecture HTML node and add <p> tag
|
||||
*/
|
||||
var $arch = $('<div/>');
|
||||
var $last = $arch;
|
||||
$node.contents().each(function () {
|
||||
if (dom.isBR(this)) {
|
||||
$(this).remove();
|
||||
$last = $('<p/>');
|
||||
$arch.append($last);
|
||||
} else if (/h[0-9]+|li|table|p/i.test(this.tagName)) {
|
||||
$last = $('<p/>');
|
||||
$arch.append(this).append($last);
|
||||
} else if ($arch.is(':empty') && dom.isText(this)) {
|
||||
$last = $('<p/>').append(this);
|
||||
$arch.append($last);
|
||||
} else if (this.nodeType !== Node.COMMENT_NODE) {
|
||||
$last.append(this);
|
||||
}
|
||||
});
|
||||
$arch.find(':not([class]):not([style]):empty, p:empty').remove();
|
||||
|
||||
/*
|
||||
history
|
||||
*/
|
||||
$editable.data('NoteHistory').recordUndo($editable, "paste");
|
||||
|
||||
/*
|
||||
remove selected content
|
||||
*/
|
||||
var r = range.create();
|
||||
if (!r.isCollapsed()) {
|
||||
r = r.deleteContents();
|
||||
r.select();
|
||||
}
|
||||
|
||||
// If only pasting a <p/> element in an unique <p/> element, only paste
|
||||
// the <p/> element text
|
||||
var $p = $arch.children('p');
|
||||
var onlyAP = ($p.length === 1 && $arch.children().length === 1);
|
||||
if (onlyAP) {
|
||||
var $p1 = $(r.sc).closest('p');
|
||||
var $p2 = $(r.ec).closest('p');
|
||||
if ($p1.length && $p2.length && $p1[0] === $p2[0]) {
|
||||
$arch.html($p.text());
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
insert content
|
||||
*/
|
||||
var $nodes = $();
|
||||
$editable.on('DOMNodeInserted', function (event) {
|
||||
$nodes = $nodes.add(event.originalEvent.target);
|
||||
});
|
||||
window.document.execCommand('insertHTML', false, $arch.html());
|
||||
$editable.off('DOMNodeInserted');
|
||||
|
||||
/*
|
||||
clean insterted content
|
||||
*/
|
||||
var $span = $nodes.filter('span');
|
||||
$span = $span.first().add($span.last());
|
||||
$span = $span.add($span.prev('span'));
|
||||
$span = $span.add($span.next('span'));
|
||||
filter_tag($span, $editable);
|
||||
$span.not('[span], [style]').each(function () {
|
||||
_.each(this.childNodes, function (node) {
|
||||
$(node.parentNode).after(node);
|
||||
});
|
||||
$(this).remove();
|
||||
});
|
||||
r = range.create();
|
||||
if (!dom.isText(r.ec)) {
|
||||
r = range.create(r.sc.childNodes[r.so], dom.nodeLength(r.sc.childNodes[r.so]));
|
||||
}
|
||||
r.clean().select();
|
||||
|
||||
$editable.trigger('content_changed');
|
||||
};
|
||||
};
|
||||
|
||||
return Clipboard;
|
||||
return Clipboard;
|
||||
});
|
||||
|
@ -628,9 +628,9 @@ define([
|
||||
}
|
||||
|
||||
var anchors = [];
|
||||
// FLECTRA: adding this branch to modify existing anchor
|
||||
// FLECTRA: adding this branch to modify existing anchor if it fully contains the range
|
||||
var ancestor_anchor = dom.ancestor(rng.sc, dom.isAnchor);
|
||||
if(ancestor_anchor) {
|
||||
if(ancestor_anchor && ancestor_anchor === dom.ancestor(rng.ec, dom.isAnchor)) {
|
||||
anchors.push($(ancestor_anchor).html(linkText).get(0));
|
||||
} else if (isTextChanged) {
|
||||
// Create a new link when text changed.
|
||||
|
@ -83,12 +83,50 @@ var FieldTextHtmlSimple = basic_fields.DebouncedField.extend(TranslatableFieldMi
|
||||
// Private
|
||||
//--------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Returns the domain for attachments used in media dialog.
|
||||
* We look for attachments related to the current document. If there is a value for the model
|
||||
* field, it is used to search attachments, and the attachments from the current document are
|
||||
* filtered to display only user-created documents.
|
||||
* In the case of a wizard such as mail, we have the documents uploaded and those of the model
|
||||
*
|
||||
* @private
|
||||
* @returns {Array} "ir.attachment" odoo domain.
|
||||
*/
|
||||
_getAttachmentsDomain: function () {
|
||||
var domain = ['|', ['id', 'in', _.pluck(this.attachments, 'id')]];
|
||||
var attachedDocumentDomain = [
|
||||
'&',
|
||||
['res_model', '=', this.model],
|
||||
['res_id', '=', this.res_id|0]
|
||||
];
|
||||
// if the document is not yet created, do not see the documents of other users
|
||||
if (!this.res_id) {
|
||||
attachedDocumentDomain.unshift('&');
|
||||
attachedDocumentDomain.push(['create_uid', '=', session.uid]);
|
||||
}
|
||||
if (this.recordData.model) {
|
||||
var relatedDomain = ['&',
|
||||
['res_model', '=', this.recordData.model],
|
||||
['res_id', '=', this.recordData.res_id|0]];
|
||||
if (!this.recordData.res_id) {
|
||||
relatedDomain.unshift('&');
|
||||
relatedDomain.push(['create_uid', '=', session.uid]);
|
||||
}
|
||||
domain = domain.concat(['|'], attachedDocumentDomain, relatedDomain);
|
||||
} else {
|
||||
domain = domain.concat(attachedDocumentDomain);
|
||||
}
|
||||
return domain;
|
||||
},
|
||||
/**
|
||||
* @private
|
||||
* @returns {Object} the summernote configuration
|
||||
*/
|
||||
_getSummernoteConfig: function () {
|
||||
var summernoteConfig = {
|
||||
model: this.model,
|
||||
id: this.res_id,
|
||||
focus: false,
|
||||
height: 180,
|
||||
toolbar: [
|
||||
@ -107,6 +145,22 @@ var FieldTextHtmlSimple = basic_fields.DebouncedField.extend(TranslatableFieldMi
|
||||
lang: "flectra",
|
||||
onChange: this._doDebouncedAction.bind(this),
|
||||
};
|
||||
|
||||
var fieldNameAttachment =_.chain(this.recordData)
|
||||
.pairs()
|
||||
.find(function (value) {
|
||||
return _.isObject(value[1]) && value[1].model === "ir.attachment";
|
||||
})
|
||||
.first()
|
||||
.value();
|
||||
|
||||
if (fieldNameAttachment) {
|
||||
this.fieldNameAttachment = fieldNameAttachment;
|
||||
this.attachments = [];
|
||||
summernoteConfig.onUpload = this._onUpload.bind(this);
|
||||
summernoteConfig.getMediaDomain = this._getAttachmentsDomain.bind(this);
|
||||
}
|
||||
|
||||
if (config.debug) {
|
||||
summernoteConfig.toolbar.splice(7, 0, ['view', ['codeview']]);
|
||||
}
|
||||
@ -124,6 +178,34 @@ var FieldTextHtmlSimple = basic_fields.DebouncedField.extend(TranslatableFieldMi
|
||||
}
|
||||
return this.$content.html();
|
||||
},
|
||||
/**
|
||||
* trigger_up 'field_changed' add record into the "ir.attachment" field found in the view.
|
||||
* This method is called when an image is uploaded by the media dialog.
|
||||
*
|
||||
* For e.g. when sending email, this allows people to add attachments with the content
|
||||
* editor interface and that they appear in the attachment list.
|
||||
* The new documents being attached to the email, they will not be erased by the CRON
|
||||
* when closing the wizard.
|
||||
*
|
||||
* @private
|
||||
*/
|
||||
_onUpload: function (attachments) {
|
||||
var self = this;
|
||||
attachments = _.filter(attachments, function (attachment) {
|
||||
return !_.findWhere(self.attachments, {id: attachment.id});
|
||||
});
|
||||
if (!attachments.length) {
|
||||
return;
|
||||
}
|
||||
this.attachments = this.attachments.concat(attachments);
|
||||
this.trigger_up('field_changed', {
|
||||
dataPointID: this.dataPointID,
|
||||
changes: _.object([this.fieldNameAttachment], [{
|
||||
operation: 'ADD_M2M',
|
||||
ids: attachments
|
||||
}])
|
||||
});
|
||||
},
|
||||
/**
|
||||
* @override
|
||||
* @private
|
||||
@ -134,8 +216,6 @@ var FieldTextHtmlSimple = basic_fields.DebouncedField.extend(TranslatableFieldMi
|
||||
this.$textarea.summernote(this._getSummernoteConfig());
|
||||
this.$content = this.$('.note-editable:first');
|
||||
this.$content.html(this._textToHtml(this.value));
|
||||
this.$content.data('oe-id', this.recordData.res_id || this.res_id);
|
||||
this.$content.data('oe-model', this.recordData.model || this.model);
|
||||
// trigger a mouseup to refresh the editor toolbar
|
||||
this.$content.trigger('mouseup');
|
||||
if (this.nodeOptions['style-inline']) {
|
||||
|
@ -47,8 +47,8 @@ var EditorMenuBar = Widget.extend({
|
||||
var self = this;
|
||||
var defs = [this._super.apply(this, arguments)];
|
||||
|
||||
core.bus.on('editor_save_request', this, this._onSaveRequest);
|
||||
core.bus.on('editor_discard_request', this, this._onDiscardRequest);
|
||||
core.bus.on('editor_save_request', this, this.save);
|
||||
core.bus.on('editor_discard_request', this, this.cancel);
|
||||
|
||||
$('.dropdown-toggle').dropdown();
|
||||
|
||||
|
@ -281,7 +281,7 @@ var RTEWidget = Widget.extend({
|
||||
.each(function () {
|
||||
var $node = $(this);
|
||||
|
||||
// fallback for firefox iframe display:none see
|
||||
// fallback for firefox iframe display:none see https://github.com/odoo/odoo/pull/22610
|
||||
var computedStyles = window.getComputedStyle(this) || window.parent.getComputedStyle(this);
|
||||
// add class to display inline-block for empty t-field
|
||||
if (computedStyles.display === 'inline' && $node.data('oe-type') !== 'image') {
|
||||
@ -290,9 +290,12 @@ var RTEWidget = Widget.extend({
|
||||
});
|
||||
|
||||
// start element observation
|
||||
$(document).on('content_changed', '.o_editable', function (event) {
|
||||
self.trigger_up('rte_change', {target: event.target});
|
||||
$(this).addClass('o_dirty');
|
||||
$(document).on('content_changed', '.o_editable', function (ev) {
|
||||
self.trigger_up('rte_change', {target: ev.target});
|
||||
if (!ev.__isDirtyHandled) {
|
||||
$(this).addClass('o_dirty');
|
||||
ev.__isDirtyHandled = true;
|
||||
}
|
||||
});
|
||||
|
||||
$('#wrapwrap, .o_editable').on('click.rte', '*', this, this._onClick.bind(this));
|
||||
@ -376,10 +379,11 @@ var RTEWidget = Widget.extend({
|
||||
* @param {boolean} internal_history
|
||||
*/
|
||||
historyRecordUndo: function ($target, event, internal_history) {
|
||||
$target = $($target);
|
||||
var rng = range.create();
|
||||
var $editable = $(rng && rng.sc).closest('.o_editable');
|
||||
if (!rng || !$editable.length) {
|
||||
$editable = $($target).closest('.o_editable');
|
||||
$editable = $target.closest('.o_editable');
|
||||
rng = range.create($target.closest('*')[0],0);
|
||||
} else {
|
||||
rng = $editable.data('range') || rng;
|
||||
@ -407,6 +411,10 @@ var RTEWidget = Widget.extend({
|
||||
save: function (context) {
|
||||
var self = this;
|
||||
|
||||
$('.o_editable')
|
||||
.destroy()
|
||||
.removeClass('o_editable o_is_inline_editable');
|
||||
|
||||
var $dirty = $('.o_dirty');
|
||||
$dirty
|
||||
.removeAttr('contentEditable')
|
||||
|
@ -394,6 +394,9 @@ eventHandler.modules.imageDialog.showImageDialog = function ($editable) {
|
||||
core.bus.trigger('media_dialog_demand', {
|
||||
$editable: $editable,
|
||||
media: media,
|
||||
options : {
|
||||
onUpload: $editable.data('callbacks').onUpload,
|
||||
},
|
||||
});
|
||||
return new $.Deferred().reject();
|
||||
};
|
||||
@ -469,7 +472,7 @@ function prettify_html(html) {
|
||||
while (i--) space += ' ';
|
||||
return space;
|
||||
},
|
||||
reg = /^<\/?(a|span|font|strong|u|i|strong|b)(\s|>)/i,
|
||||
reg = /^<\/?(a|span|font|u|em|i|strong|b)(\s|>)/i,
|
||||
inline_level = Infinity,
|
||||
tokens = _.compact(_.flatten(_.map(html.split(/</), function (value) {
|
||||
value = value.replace(/\s+/g, ' ').split(/>/);
|
||||
@ -660,9 +663,17 @@ function summernote_mousedown(event) {
|
||||
}
|
||||
|
||||
// restore range if range lost after clicking on non-editable area
|
||||
r = range.create();
|
||||
try {
|
||||
r = range.create();
|
||||
} catch (e) {
|
||||
// If this code is running inside an iframe-editor and that the range
|
||||
// is outside of this iframe, this will fail as the iframe does not have
|
||||
// the permission to check the outside content this way. In that case,
|
||||
// we simply ignore the exception as it is as if there was no range.
|
||||
return;
|
||||
}
|
||||
var editables = $(".o_editable[contenteditable], .note-editable[contenteditable]");
|
||||
var r_editable = editables.has((r||{}).sc);
|
||||
var r_editable = editables.has((r||{}).sc).addBack(editables.filter((r||{}).sc));
|
||||
if (!r_editable.closest('.note-editor').is($editable) && !r_editable.filter('.o_editable').is(editables)) {
|
||||
var saved_editable = editables.has((remember_selection||{}).sc);
|
||||
if ($editable.length && !saved_editable.closest('.o_editable, .note-editor').is($editable)) {
|
||||
@ -784,6 +795,14 @@ eventHandler.attach = function (oLayoutInfo, options) {
|
||||
$(document).on("keyup", reRangeSelectKey);
|
||||
|
||||
var clone_data = false;
|
||||
|
||||
if (options.model) {
|
||||
oLayoutInfo.editable().data({'oe-model': options.model, 'oe-id': options.id});
|
||||
}
|
||||
if (options.getMediaDomain) {
|
||||
oLayoutInfo.editable().data('oe-media-domain', options.getMediaDomain);
|
||||
}
|
||||
|
||||
var $node = oLayoutInfo.editor();
|
||||
if ($node.data('oe-model') || $node.data('oe-translation-id')) {
|
||||
$node.on('content_changed', function () {
|
||||
@ -1085,6 +1104,7 @@ var SummernoteManager = Class.extend(mixins.EventDispatcherMixin, {
|
||||
_.extend({
|
||||
res_model: data.$editable.data('oe-model'),
|
||||
res_id: data.$editable.data('oe-id'),
|
||||
domain: data.$editable.data('oe-media-domain'),
|
||||
}, data.options),
|
||||
data.$editable,
|
||||
data.media
|
||||
|
@ -1800,12 +1800,17 @@ $.summernote.pluginEvents.indent = function (event, editor, layoutInfo, outdent)
|
||||
var $dom = $(ancestor);
|
||||
|
||||
if (!dom.isList(ancestor)) {
|
||||
// to indent a selection, we indent the child nodes of the common
|
||||
// ancestor that contains this selection
|
||||
$dom = $(dom.node(ancestor)).children();
|
||||
}
|
||||
if (!$dom.length) {
|
||||
$dom = $(dom.ancestor(r.sc, dom.isList) || dom.ancestor(r.sc, dom.isCell));
|
||||
if (!$dom.not('br').length) {
|
||||
// if selection is inside a list, we indent its list items
|
||||
$dom = $(dom.ancestor(r.sc, dom.isList));
|
||||
if (!$dom.length) {
|
||||
$dom = $(r.sc).closest(options.styleTags.join(','));
|
||||
// if the selection is contained in a single HTML node, we indent
|
||||
// the first ancestor 'content block' (P, H1, PRE, ...) or TD
|
||||
$dom = $(r.sc).closest(options.styleTags.join(',')+',td');
|
||||
}
|
||||
}
|
||||
|
||||
@ -1970,7 +1975,13 @@ eventHandler.modules.toolbar.button.updateRecentColor = function (elBtn, sEvent,
|
||||
};
|
||||
|
||||
$(document).on('click keyup', function () {
|
||||
var $popover = $((range.create()||{}).sc).closest('[contenteditable]');
|
||||
var current_range = {};
|
||||
try {
|
||||
current_range = range.create() || {};
|
||||
} catch (e) {
|
||||
// if range is on Restricted element ignore error
|
||||
}
|
||||
var $popover = $(current_range.sc).closest('[contenteditable]');
|
||||
var popover_history = ($popover.data()||{}).NoteHistory;
|
||||
if (!popover_history || popover_history === history) return;
|
||||
var editor = $popover.parent('.note-editor');
|
||||
@ -2257,7 +2268,7 @@ $.summernote.pluginEvents.backColor = function (event, editor, layoutInfo, backC
|
||||
};
|
||||
|
||||
options.onCreateLink = function (sLinkUrl) {
|
||||
if (sLinkUrl.indexOf('mailto:') === 0) {
|
||||
if (sLinkUrl.indexOf('mailto:') === 0 || sLinkUrl.indexOf('tel:') === 0) {
|
||||
// pass
|
||||
} else if (sLinkUrl.indexOf('@') !== -1 && sLinkUrl.indexOf(':') === -1) {
|
||||
sLinkUrl = 'mailto:' + sLinkUrl;
|
||||
|
@ -105,25 +105,52 @@ function getMatchedCSSRules(a) {
|
||||
delete style.display;
|
||||
}
|
||||
|
||||
_.each(['margin', 'padding'], function (p) {
|
||||
if (style[p+'-top'] || style[p+'-right'] || style[p+'-bottom'] || style[p+'-left']) {
|
||||
if (style[p+'-top'] === style[p+'-right'] && style[p+'-top'] === style[p+'-bottom'] && style[p+'-top'] === style[p+'-left']) {
|
||||
// The css generates all the attributes separately and not in simplified form.
|
||||
// In order to have a better compatibility (outlook for example) we simplify the css tags.
|
||||
// e.g. border-left-style: none; border-bottom-s .... will be simplified in border-style = none
|
||||
_.each([
|
||||
{property: 'margin'},
|
||||
{property: 'padding'},
|
||||
{property: 'border', propertyEnd: '-style', defaultValue: 'none'},
|
||||
], function (propertyInfo) {
|
||||
var p = propertyInfo.property;
|
||||
var e = propertyInfo.propertyEnd || '';
|
||||
var defVal = propertyInfo.defaultValue || 0;
|
||||
|
||||
if (style[p+'-top'+e] || style[p+'-right'+e] || style[p+'-bottom'+e] || style[p+'-left'+e]) {
|
||||
if (style[p+'-top'+e] === style[p+'-right'+e] && style[p+'-top'+e] === style[p+'-bottom'+e] && style[p+'-top'+e] === style[p+'-left'+e]) {
|
||||
// keep => property: [top/right/bottom/left value];
|
||||
style[p] = style[p+'-top'];
|
||||
style[p+e] = style[p+'-top'+e];
|
||||
}
|
||||
else {
|
||||
// keep => property: [top value] [right value] [bottom value] [left value];
|
||||
style[p] = (style[p+'-top'] || 0) + ' ' + (style[p+'-right'] || 0) + ' ' + (style[p+'-bottom'] || 0) + ' ' + (style[p+'-left'] || 0);
|
||||
if (style[p].indexOf('inherit') !== -1 || style[p].indexOf('initial') !== -1) {
|
||||
style[p+e] = (style[p+'-top'+e] || defVal) + ' ' + (style[p+'-right'+e] || defVal) + ' ' + (style[p+'-bottom'+e] || defVal) + ' ' + (style[p+'-left'+e] || defVal);
|
||||
if (style[p+e].indexOf('inherit') !== -1 || style[p+e].indexOf('initial') !== -1) {
|
||||
// keep => property-top: [top value]; property-right: [right value]; property-bottom: [bottom value]; property-left: [left value];
|
||||
delete style[p];
|
||||
delete style[p+e];
|
||||
return;
|
||||
}
|
||||
}
|
||||
delete style[p+'-top'];
|
||||
delete style[p+'-right'];
|
||||
delete style[p+'-bottom'];
|
||||
delete style[p+'-left'];
|
||||
delete style[p+'-top'+e];
|
||||
delete style[p+'-right'+e];
|
||||
delete style[p+'-bottom'+e];
|
||||
delete style[p+'-left'+e];
|
||||
}
|
||||
});
|
||||
|
||||
if (style['border-bottom-left-radius']) {
|
||||
style['border-radius'] = style['border-bottom-left-radius'];
|
||||
delete style['border-bottom-left-radius'];
|
||||
delete style['border-bottom-right-radius'];
|
||||
delete style['border-top-left-radius'];
|
||||
delete style['border-top-right-radius'];
|
||||
}
|
||||
|
||||
// if the border styling is initial we remove it to simplify the css tags for compatibility.
|
||||
// Also, since we do not send a css style tag, the initial value of the border is useless.
|
||||
_.each(_.keys(style), function (k) {
|
||||
if (k.indexOf('border') !== -1 && style[k] === 'initial') {
|
||||
delete style[k];
|
||||
}
|
||||
});
|
||||
|
||||
@ -205,6 +232,26 @@ function imgToFont($editable) {
|
||||
});
|
||||
}
|
||||
|
||||
/*
|
||||
* Utility function to apply function over descendants elements
|
||||
*
|
||||
* This is needed until the following issue of jQuery is solved:
|
||||
* https://github.com./jquery/sizzle/issues/403
|
||||
*
|
||||
* @param {Element} node The root Element node
|
||||
* @param {Function} func The function applied over descendants
|
||||
*/
|
||||
function applyOverDescendants(node, func) {
|
||||
node = node.firstChild;
|
||||
while (node) {
|
||||
if (node.nodeType === 1) {
|
||||
func(node);
|
||||
applyOverDescendants(node, func);
|
||||
}
|
||||
node = node.nextSibling;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts css style to inline style (leave the classes on elements but forces
|
||||
* the style they give as inline style).
|
||||
@ -215,9 +262,9 @@ function classToStyle($editable) {
|
||||
if (!rulesCache.length) {
|
||||
getMatchedCSSRules($editable[0]);
|
||||
}
|
||||
$editable.find('*').each(function () {
|
||||
var $target = $(this);
|
||||
var css = getMatchedCSSRules(this);
|
||||
applyOverDescendants($editable[0], function (node) {
|
||||
var $target = $(node);
|
||||
var css = getMatchedCSSRules(node);
|
||||
var style = $target.attr('style') || '';
|
||||
_.each(css, function (v,k) {
|
||||
if (!(new RegExp('(^|;)\s*' + k).test(style))) {
|
||||
@ -229,6 +276,34 @@ function classToStyle($editable) {
|
||||
} else {
|
||||
$target.attr('style', style);
|
||||
}
|
||||
// Apple Mail
|
||||
if (node.nodeName === 'TD' && !node.childNodes.length) {
|
||||
node.innerHTML = ' ';
|
||||
}
|
||||
|
||||
// Outlook
|
||||
if (node.nodeName === 'A' && $target.hasClass('btn') && !$target.hasClass('btn-link') && !$target.children().length) {
|
||||
var $hack = $('<table class="o_outlook_hack" style="display: inline-table;"><tr><td></td></tr></table>');
|
||||
$hack.find('td')
|
||||
.attr('height', $target.outerHeight())
|
||||
.css({
|
||||
'text-align': $target.parent().css('text-align'),
|
||||
'margin': $target.css('padding'),
|
||||
'border-radius': $target.css('border-radius'),
|
||||
'background-color': $target.css('background-color'),
|
||||
});
|
||||
$target.after($hack);
|
||||
$target.appendTo($hack.find('td'));
|
||||
// the space add a line when it's a table but it's invisible when it's a link
|
||||
node = $hack[0].previousSibling;
|
||||
if (node && node.nodeType === Node.TEXT_NODE && !node.textContent.match(/\S/)) {
|
||||
$(node).remove();
|
||||
}
|
||||
node = $hack[0].nextSibling;
|
||||
if (node && node.nodeType === Node.TEXT_NODE && !node.textContent.match(/\S/)) {
|
||||
$(node).remove();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@ -239,13 +314,18 @@ function classToStyle($editable) {
|
||||
* @param {jQuery} $editable
|
||||
*/
|
||||
function styleToClass($editable) {
|
||||
// Outlook revert
|
||||
$editable.find('table.o_outlook_hack').each(function () {
|
||||
$(this).after($('a', this));
|
||||
}).remove();
|
||||
|
||||
getMatchedCSSRules($editable[0]);
|
||||
|
||||
var $c = $('<span/>').appendTo(document.body);
|
||||
|
||||
$editable.find('*').each(function () {
|
||||
var $target = $(this);
|
||||
var css = getMatchedCSSRules(this);
|
||||
applyOverDescendants($editable[0], function (node) {
|
||||
var $target = $(node);
|
||||
var css = getMatchedCSSRules(node);
|
||||
var style = '';
|
||||
_.each(css, function (v,k) {
|
||||
if (!(new RegExp('(^|;)\s*' + k).test(style))) {
|
||||
|
@ -178,7 +178,7 @@ tour.register('rte', {
|
||||
content: "insert a link url",
|
||||
trigger: '#o_link_dialog_url_input',
|
||||
extra_trigger: 'a#link-preview.btn',
|
||||
run: "text http://www.flectra.com",
|
||||
run: "text http://www.flectrahq.com",
|
||||
}, {
|
||||
content: "change text label",
|
||||
trigger: '#o_link_dialog_label_input',
|
||||
|
@ -122,9 +122,11 @@ var ImageDialog = Widget.extend({
|
||||
this._super.apply(this, arguments);
|
||||
this.options = options || {};
|
||||
this.accept = this.options.accept || this.options.document ? "*/*" : "image/*";
|
||||
if (this.options.res_id) {
|
||||
if (options.domain) {
|
||||
this.domain = typeof options.domain === 'function' ? options.domain() : options.domain;
|
||||
} else if (options.res_id) {
|
||||
this.domain = ['|',
|
||||
'&', ['res_model', '=', this.options.res_model], ['res_id', '=', this.options.res_id],
|
||||
'&', ['res_model', '=', options.res_model], ['res_id', '=', options.res_id],
|
||||
['res_model', '=', 'ir.ui.view']];
|
||||
} else {
|
||||
this.domain = [['res_model', '=', 'ir.ui.view']];
|
||||
@ -188,8 +190,7 @@ var ImageDialog = Widget.extend({
|
||||
|
||||
var img = this.images[0];
|
||||
if (!img) {
|
||||
var id = this.$(".existing-attachments [data-src]:first").data('id');
|
||||
img = _.find(this.images, function (img) { return img.id === id;});
|
||||
return this.media;
|
||||
}
|
||||
|
||||
var def = $.when();
|
||||
@ -239,6 +240,11 @@ var ImageDialog = Widget.extend({
|
||||
var style = self.style;
|
||||
if (style) { $(self.media).css(style); }
|
||||
|
||||
if (self.options.onUpload) {
|
||||
// We consider that when selecting an image it is as if we upload it in the html content.
|
||||
self.options.onUpload([img]);
|
||||
}
|
||||
|
||||
return self.media;
|
||||
});
|
||||
},
|
||||
@ -290,6 +296,10 @@ var ImageDialog = Widget.extend({
|
||||
for (var i=0; i<attachments.length; i++) {
|
||||
self.file_selected(attachments[i], error);
|
||||
}
|
||||
|
||||
if (self.options.onUpload) {
|
||||
self.options.onUpload(attachments);
|
||||
}
|
||||
};
|
||||
},
|
||||
file_selection: function () {
|
||||
@ -1185,6 +1195,8 @@ var LinkDialog = Dialog.extend({
|
||||
if (dom.ancestor(nodes[i], dom.isImg)) {
|
||||
this.data.images.push(dom.ancestor(nodes[i], dom.isImg));
|
||||
text += '[IMG]';
|
||||
} else if (!is_link && nodes[i].nodeType === 1) {
|
||||
// just use text nodes from listBetween
|
||||
} else if (!is_link && i===0) {
|
||||
text += nodes[i].textContent.slice(so, Infinity);
|
||||
} else if (!is_link && i===nodes.length-1) {
|
||||
|
@ -5,13 +5,16 @@
|
||||
font-family: inherit !important;
|
||||
line-height: initial !important;
|
||||
color: initial !important;
|
||||
p {
|
||||
p, div {
|
||||
font-family: 'Lucida Grande', Helvetica, Verdana, Arial, sans-serif;
|
||||
font-size: 13px;
|
||||
}
|
||||
a, a:hover {
|
||||
color: initial;
|
||||
}
|
||||
ul > li > p {
|
||||
margin: 0px;
|
||||
}
|
||||
}
|
||||
.o_readonly {
|
||||
min-height: 1em;
|
||||
@ -20,6 +23,9 @@
|
||||
padding: 0;
|
||||
border: 0;
|
||||
word-wrap: break-word;
|
||||
ul > li > p {
|
||||
margin: 0px;
|
||||
}
|
||||
}
|
||||
}
|
||||
.oe_form_field_html iframe {
|
||||
|
@ -110,7 +110,7 @@
|
||||
<span class="text-muted">— or — </span>
|
||||
<label for="iamgeurl">Add an image URL</label>
|
||||
<div class="form-group btn-group">
|
||||
<input class="form-control url pull-left" id="iamgeurl" name="url" placeholder="https://www.flectra.com/logo.png" style="width: 320px;" type="text"/>
|
||||
<input class="form-control url pull-left" id="iamgeurl" name="url" placeholder="https://www.flectrahq.com/logo.png" style="width: 320px;" type="text"/>
|
||||
<button class="btn btn-default" type="submit">Add</button>
|
||||
</div>
|
||||
</div>
|
||||
@ -147,8 +147,9 @@
|
||||
<div class="existing-attachments">
|
||||
<div class="row mt16" t-as="row" t-foreach="rows">
|
||||
<div class="col-sm-2 o_existing_attachment_cell" t-as="attachment" t-foreach="row">
|
||||
<i class="fa fa-times o_existing_attachment_remove" t-att-data-id="attachment.id"/>
|
||||
<div class="o_attachment_border"><div t-att-data-src="attachment.src" t-att-data-url="attachment.url" t-att-alt="attachment.name" t-att-title="attachment.name" t-att-data-id="attachment.id" t-att-data-mimetype="attachment.mimetype" class="o_image"/></div>
|
||||
<i t-if="attachment.res_model === 'ir.ui.view'" class="fa fa-times o_existing_attachment_remove" title="This file is a public view attachment" t-att-data-id="attachment.id"/>
|
||||
<i t-else="" class="fa fa-times o_existing_attachment_remove" title="This file is attached to the current record" t-att-data-id="attachment.id"/>
|
||||
<div class="o_attachment_border" t-att-style="attachment.res_model === 'ir.ui.view' ? null : 'border: 1px solid #5cb85c;'"><div t-att-data-src="attachment.src" t-att-data-url="attachment.url" t-att-alt="attachment.name" t-att-title="attachment.name" t-att-data-id="attachment.id" t-att-data-mimetype="attachment.mimetype" class="o_image"/></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -211,7 +211,7 @@ QUnit.test('html_frame does not crash when saving in readonly', function (assert
|
||||
if (_.str.startsWith(route, '/logo')) {
|
||||
// manually call the callback to simulate that the iframe has
|
||||
// been loaded (note: just the content, not the editor)
|
||||
window.odoo[$.deparam(route).callback + '_content'].call();
|
||||
window.flectra[$.deparam(route).callback + '_content'].call();
|
||||
return $.when();
|
||||
}
|
||||
return this._super.apply(this, arguments);
|
||||
@ -248,7 +248,7 @@ QUnit.test('html_frame does not crash when saving in edit mode (editor not loade
|
||||
if (_.str.startsWith(route, '/logo')) {
|
||||
// manually call the callback to simulate that the iframe has
|
||||
// been partially loaded (just the content, not the editor)
|
||||
window.odoo[$.deparam(route).callback + '_content']();
|
||||
window.flectra[$.deparam(route).callback + '_content']();
|
||||
return $.when();
|
||||
}
|
||||
return this._super.apply(this, arguments);
|
||||
|
@ -16,6 +16,7 @@ var PlannerLauncher = planner.PlannerLauncher.extend({
|
||||
return this._rpc({
|
||||
model: 'web.planner',
|
||||
method: 'search_read',
|
||||
kwargs: {context: session.user_context},
|
||||
})
|
||||
.then(function (records) {
|
||||
_.each(records, function (planner) {
|
||||
|
@ -43,10 +43,6 @@ var PlannerDialog = Dialog.extend({
|
||||
e.preventDefault();
|
||||
this._display_page($(e.currentTarget).attr("href").replace("#", ""));
|
||||
},
|
||||
"click a[href^=\"#show_enterprise\"]": function (e) {
|
||||
e.preventDefault();
|
||||
this.show_enterprise();
|
||||
},
|
||||
},
|
||||
init: function (parent, options, planner) {
|
||||
this._super.apply(this, arguments);
|
||||
@ -363,36 +359,6 @@ var PlannerDialog = Dialog.extend({
|
||||
args: [this.planner.id, {'data': JSON.stringify(this.planner.data), 'progress': this.planner.progress}],
|
||||
});
|
||||
},
|
||||
show_enterprise: function () {
|
||||
var buttons = [{
|
||||
text: _t("Upgrade now"),
|
||||
classes: 'btn-primary',
|
||||
close: true,
|
||||
click: function () {
|
||||
rpc.query({
|
||||
model: "res.users",
|
||||
method: "search_count",
|
||||
args: [[["share", "=", false]]],
|
||||
})
|
||||
.then(function (data) {
|
||||
window.location = "https://www.flectra.com/flectra-enterprise/upgrade?utm_medium=community_upgrade&num_users=" + data;
|
||||
});
|
||||
},
|
||||
}, {
|
||||
text: _t("Cancel"),
|
||||
close: true,
|
||||
}];
|
||||
var dialog = new Dialog(this, {
|
||||
size: 'medium',
|
||||
buttons: buttons,
|
||||
$content: $('<div>', {
|
||||
html: QWeb.render('EnterpriseUpgrade'),
|
||||
}),
|
||||
title: _t("Flectra Enterprise"),
|
||||
}).open();
|
||||
|
||||
return dialog;
|
||||
},
|
||||
});
|
||||
|
||||
var PlannerLauncher = Widget.extend({
|
||||
|
@ -2,14 +2,14 @@
|
||||
# Part of Odoo, Flectra. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
{
|
||||
'name': 'Odoo Settings Dashboard',
|
||||
'name': 'Flectra Settings Dashboard',
|
||||
'author': 'Odoo S.A.',
|
||||
'version': '1.0',
|
||||
'summary': 'Quick actions for installing new app, adding users, completing planners, etc.',
|
||||
'category': 'Extra Tools',
|
||||
'description':
|
||||
"""
|
||||
Odoo dashboard
|
||||
Flectra dashboard
|
||||
==============
|
||||
* Quick access to install apps
|
||||
* Quick users add
|
||||
|
@ -231,7 +231,6 @@ var DashboardApps = Widget.extend({
|
||||
|
||||
events: {
|
||||
'click .o_browse_apps': 'on_new_apps',
|
||||
'click .o_confirm_upgrade': 'confirm_upgrade',
|
||||
},
|
||||
|
||||
init: function(parent, data){
|
||||
@ -242,18 +241,11 @@ var DashboardApps = Widget.extend({
|
||||
|
||||
start: function() {
|
||||
this._super.apply(this, arguments);
|
||||
if (flectra.db_info && _.last(flectra.db_info.server_version_info) !== 'e') {
|
||||
$(QWeb.render("DashboardEnterprise")).appendTo(this.$el);
|
||||
}
|
||||
},
|
||||
|
||||
on_new_apps: function(){
|
||||
this.do_action('base.open_module_tree');
|
||||
},
|
||||
|
||||
confirm_upgrade: function() {
|
||||
framework.redirect("https://www.flectra.com/flectra-enterprise/upgrade?num_users=" + (this.data.enterprise_users || 1));
|
||||
},
|
||||
});
|
||||
|
||||
var DashboardShare = Widget.extend({
|
||||
@ -268,7 +260,7 @@ var DashboardShare = Widget.extend({
|
||||
init: function(parent, data){
|
||||
this.data = data;
|
||||
this.parent = parent;
|
||||
this.share_url = 'https://www.flectra.com';
|
||||
this.share_url = 'https://www.flectrahq.com';
|
||||
this.share_text = encodeURIComponent("I am using #Flectra - Awesome open source business apps.");
|
||||
},
|
||||
|
||||
@ -283,7 +275,7 @@ var DashboardShare = Widget.extend({
|
||||
},
|
||||
|
||||
share_linkedin: function(){
|
||||
var popup_url = _.str.sprintf('http://www.linkedin.com/shareArticle?mini=true&url=%s&title=I am using flectra&summary=%s&source=www.flectra.com', encodeURIComponent(this.share_url), this.share_text);
|
||||
var popup_url = _.str.sprintf('http://www.linkedin.com/shareArticle?mini=true&url=%s&title=I am using flectra&summary=%s&source=www.flectrahq.com', encodeURIComponent(this.share_url), this.share_text);
|
||||
this.sharer(popup_url);
|
||||
},
|
||||
|
||||
|
@ -105,7 +105,7 @@
|
||||
<t t-set="planners" t-value="widget.planners"/>
|
||||
<t t-call="DashboardPlanner.PlannersList"/>
|
||||
<hr/>
|
||||
Need more help? <a target="_blank" href="https://www.flectra.com/documentation/user">Browse the documentation.</a>
|
||||
Need more help? <a target="_blank" href="https://www.flectrahq.com/documentation/user">Browse the documentation.</a>
|
||||
</div>
|
||||
</t>
|
||||
|
||||
@ -179,20 +179,6 @@
|
||||
</div>
|
||||
</t>
|
||||
|
||||
<t t-name="DashboardEnterprise">
|
||||
<hr class="mt16"/>
|
||||
<div class="text-center o_web_settings_dashboard_enterprise">
|
||||
<div class="text-center o_web_settings_dashboard_enterprise">
|
||||
<div class="text-center o_web_settings_dashboard_header">Flectra Enterprise</div>
|
||||
<div class="mb16"><a href="http://www.flectra.com/editions" target="_blank">Get more features with the Enterprise Edition!</a></div>
|
||||
<div><img class="img img-responsive" t-att-src='_s + "/web/static/src/img/enterprise_upgrade.jpg"'/></div>
|
||||
<div>
|
||||
<a class="btn btn-primary btn-block o_confirm_upgrade" role="button"><strong>Upgrade Now</strong></a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</t>
|
||||
|
||||
<t t-name="DashboardTranslations">
|
||||
<div class="text-center o_web_settings_dashboard_translations mt8">
|
||||
<i class="fa fa-globe fa-4x text-muted"></i>
|
||||
|
@ -18,8 +18,9 @@ from flectra import http, models, fields, _
|
||||
from flectra.http import request
|
||||
from flectra.tools import pycompat, OrderedSet
|
||||
from flectra.addons.http_routing.models.ir_http import slug, _guess_mimetype
|
||||
from flectra.addons.web.controllers.main import WebClient, Binary, Home
|
||||
from flectra.addons.web.controllers.main import WebClient, Binary
|
||||
from flectra.addons.portal.controllers.portal import pager as portal_pager
|
||||
from flectra.addons.portal.controllers.web import Home
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@ -306,17 +307,6 @@ class Website(Home):
|
||||
modules.button_immediate_upgrade()
|
||||
return request.redirect(redirect)
|
||||
|
||||
@http.route('/website/translations', type='json', auth="public", website=True)
|
||||
def get_website_translations(self, lang, mods=None):
|
||||
Modules = request.env['ir.module.module'].sudo()
|
||||
modules = Modules.search([
|
||||
'|', ('name', 'ilike', 'website'), ('name', '=', 'web_editor'),
|
||||
('state', '=', 'installed')
|
||||
]).mapped('name')
|
||||
if mods:
|
||||
modules += mods
|
||||
return WebClient().translations(mods=modules, lang=lang)
|
||||
|
||||
@http.route(['/website/publish'], type='json', auth="public", website=True)
|
||||
def publish(self, id, object):
|
||||
Model = request.env[object]
|
||||
|
@ -130,5 +130,118 @@ response = request.render("website.template_partner_comment", {
|
||||
<field name="name">Website 0.0.0.0</field>
|
||||
<field name="domain">0.0.0.0</field>
|
||||
</record>
|
||||
|
||||
<record id="website2_homepage" model="ir.ui.view">
|
||||
<field name="name">Home</field>
|
||||
<field name="type">qweb</field>
|
||||
<field name="key">website2.homepage</field>
|
||||
<field name="arch" type="xml">
|
||||
<t name="Home" priority="29" t-name="website2.homepage">
|
||||
<t t-call="website.layout">
|
||||
<div id="wrap" class="oe_structure oe_empty">
|
||||
<div class="carousel slide mb32" id="myCarousel0" style="height: 320px;">
|
||||
<ol class="carousel-indicators hidden">
|
||||
<li class="active" data-slide-to="0" data-target="#myCarousel0"/>
|
||||
</ol>
|
||||
<div class="carousel-inner">
|
||||
<div class="item image_text oe_img_bg active" style="background-image: url(http://0.0.0.0:8069/web/image/website.s_background_image_11);">
|
||||
<div class="container">
|
||||
<div class="row content">
|
||||
<div class="carousel-content col-md-6 col-sm-12">
|
||||
<h2>Homepage 0.0.0.0</h2>
|
||||
<h3>Click to customize this text</h3>
|
||||
<p>
|
||||
<a class="btn btn-success btn-large" href="/contactus">Contact us</a>
|
||||
</p>
|
||||
</div>
|
||||
<span class="carousel-img col-md-6 hidden-sm hidden-xs"> </span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="carousel-control left hidden" data-slide="prev" data-target="#myCarousel0" href="#myCarousel0" style="width: 10%">
|
||||
<i class="fa fa-chevron-left"/>
|
||||
</div>
|
||||
<div class="carousel-control right hidden" data-slide="next" data-target="#myCarousel0" href="#myCarousel0" style="width: 10%">
|
||||
<i class="fa fa-chevron-right"/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</t>
|
||||
</t>
|
||||
</field>
|
||||
</record>
|
||||
<record id="website2_homepage_page" model="website.page">
|
||||
<field name="website_published">True</field>
|
||||
<field name="url">/</field>
|
||||
<field name="view_id" ref="website2_homepage" />
|
||||
<field name="website_ids" eval="[(4, ref('website2'))]" />
|
||||
</record>
|
||||
|
||||
<record id="website2_contactus" model="ir.ui.view">
|
||||
<field name="name">Contact Us</field>
|
||||
<field name="type">qweb</field>
|
||||
<field name="key">website2.contactus</field>
|
||||
<field name="arch" type="xml">
|
||||
<t name="Contact Us" t-name="website2.contactus">
|
||||
<t t-call="website.layout">
|
||||
<div id="wrap">
|
||||
<div class="oe_structure"/>
|
||||
<div class="container">
|
||||
<h1>Contact us</h1>
|
||||
<div class="row">
|
||||
<div class="col-md-8">
|
||||
<div class="oe_structure">
|
||||
<div>
|
||||
<p>Contact us about anything related to our company or services.</p>
|
||||
<p>We'll do our best to get back to you as soon as possible.</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="text-center mt64" name="mail_button">
|
||||
<a t-attf-href="mailto:{{ res_company.email }}" class="btn btn-primary" id="o_contact_mail">Send us an email</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-4 mb32">
|
||||
<t t-call="website.company_description"/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="oe_structure"/>
|
||||
</div>
|
||||
</t>
|
||||
</t>
|
||||
</field>
|
||||
</record>
|
||||
<record id="website2_contactus_page" model="website.page">
|
||||
<field name="website_published">True</field>
|
||||
<field name="url">/contactus</field>
|
||||
<field name="view_id" ref="website2_contactus" />
|
||||
<field name="website_ids" eval="[(4, ref('website2'))]" />
|
||||
</record>
|
||||
|
||||
<!-- Menu & Homepage -->
|
||||
<record id="website2" model="website">
|
||||
<field name="homepage_id" ref="website2_homepage_page" />
|
||||
</record>
|
||||
<record id="website2_main_menu" model="website.menu">
|
||||
<field name="name">Top Menu</field>
|
||||
<field name="website_id" ref="website2"/>
|
||||
</record>
|
||||
<record id="website2_menu_homepage" model="website.menu">
|
||||
<field name="name">Home</field>
|
||||
<field name="url">/</field>
|
||||
<field name="parent_id" ref="website.website2_main_menu"/>
|
||||
<field name="sequence" type="int">10</field>
|
||||
<field name="website_id" ref="website2"/>
|
||||
<field name="page_id" ref="website2_homepage_page" />
|
||||
</record>
|
||||
<record id="website2_menu_contactus" model="website.menu">
|
||||
<field name="name">Contact us</field>
|
||||
<field name="url">/contactus</field>
|
||||
<field name="parent_id" ref="website.website2_main_menu"/>
|
||||
<field name="sequence" type="int">60</field>
|
||||
<field name="website_id" ref="website2"/>
|
||||
<field name="page_id" ref="website2_contactus_page" />
|
||||
</record>
|
||||
</data>
|
||||
</flectra>
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user