Master chintan hiren mayank 13072018

This commit is contained in:
ChintAn Ambaliya 2018-07-13 09:51:12 +00:00 committed by Parthiv Patel
parent 604cf9e3e0
commit 7dd719a67f
194 changed files with 4249 additions and 1091 deletions

View File

@ -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:

View File

@ -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>

View File

@ -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>

View File

@ -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);
});
});

View File

@ -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"));
}

View File

@ -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();
});
});

View File

@ -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

View File

@ -0,0 +1,4 @@
# -*- coding: utf-8 -*-
# Part of Odoo, Flectra. See LICENSE file for full copyright and licensing details.
from . import main

View 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)

View 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)

View File

@ -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

View File

@ -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>

View File

@ -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,))

View File

@ -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'"
)

View File

@ -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',
}

View File

@ -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>

View File

@ -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">

View File

@ -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>

View File

@ -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])

View File

@ -9,7 +9,7 @@
"node":"*"
},
"author":{
"name":"Flectra S.A. - Hitesh Trivedi",
"name":"Odoo S.A. - Hitesh Trivedi",
"email":"thiteshm155@gmail.com"
}
}

View File

@ -17,4 +17,4 @@ module.exports = function(grunt) {
grunt.registerTask('default', ['jshint']);
};
};

View File

@ -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():

View File

@ -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)

View File

@ -170,4 +170,4 @@ hr {
[role="button"] {
cursor: pointer;
}
}

View 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>

View File

@ -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.

View File

@ -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);

View File

@ -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);
}
},
/**

View File

@ -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) {

View File

@ -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]);

View File

@ -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();

View File

@ -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

View File

@ -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;

View File

@ -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 &shy; characters when saving number
*
* @override
* @private
*/
_setValue: function (value, options) {
if (value) {
// remove possibly pasted &shy; 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

View File

@ -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)

View File

@ -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
//--------------------------------------------------------------------------

View File

@ -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

View File

@ -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,

View File

@ -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);
});

View File

@ -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

View File

@ -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;
}

View File

@ -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');
}

View File

@ -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);

View File

@ -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) {},
});

View File

@ -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 = [];

View File

@ -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;

View File

@ -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

View File

@ -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);
}
},
});
});

View File

@ -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();
});
},

View File

@ -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;
});
});

View File

@ -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.

View File

@ -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;

View File

@ -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) {

View File

@ -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");

View File

@ -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,

View File

@ -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;

View File

@ -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;
}

View File

@ -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

View File

@ -248,6 +248,10 @@
border-left-color: @gray-lighter;
}
}
.caret {
margin-top: -2px;
}
}
.o-status-more {

View File

@ -91,6 +91,7 @@
margin-left: 3%;
color: @headings-color;
text-align: right;
white-space: nowrap;
.o-transform-origin(right, center);
&.o_kanban_grow {

View File

@ -223,6 +223,7 @@
}
.o_favorite {
top: 3px;
left: 0;
right: auto;
}

View File

@ -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;
}

View File

@ -76,4 +76,4 @@
// ------------------------------------------------------------------
.o_pivot_measures_list {
.o-selected-li;
}
}

View File

@ -71,11 +71,6 @@
}
}
}
.o_target_date:not(.fc-today) {
background-color: @flectra-brand-primary;
opacity: 0.1;
}
}
.o_calendar_sidebar_container {

View File

@ -18,4 +18,4 @@ flectra.__DEBUG__.didLogInfo.then(function() {
});
})();
})();

View File

@ -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 &shy; 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

View File

@ -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();

View File

@ -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();
});
});
});

View File

@ -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();
});
});
});

View File

@ -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);

View File

@ -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();
});
});
});

View File

@ -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();
});
});});

View File

@ -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'])

View File

@ -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">

View File

@ -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"/>

View File

@ -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

View File

@ -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')]])

View File

@ -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

View File

@ -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());

View File

@ -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;
});

View File

@ -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.

View File

@ -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']) {

View File

@ -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();

View File

@ -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')

View File

@ -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

View File

@ -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;

View File

@ -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 = '&nbsp;';
}
// 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))) {

View File

@ -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',

View File

@ -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) {

View File

@ -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 {

View File

@ -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>

View File

@ -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);

View File

@ -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) {

View File

@ -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({

View File

@ -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

View File

@ -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);
},

View File

@ -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>

View File

@ -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]

View File

@ -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