diff --git a/addons/base_vat_autocomplete/models/res_partner.py b/addons/base_vat_autocomplete/models/res_partner.py
index 40bc80c1..a535514b 100644
--- a/addons/base_vat_autocomplete/models/res_partner.py
+++ b/addons/base_vat_autocomplete/models/res_partner.py
@@ -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:
diff --git a/addons/base_vat_autocomplete/views/res_company_view.xml b/addons/base_vat_autocomplete/views/res_company_view.xml
index f4b66169..2e5ab411 100644
--- a/addons/base_vat_autocomplete/views/res_company_view.xml
+++ b/addons/base_vat_autocomplete/views/res_company_view.xml
@@ -11,7 +11,7 @@
-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.
-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.
-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.
- 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
- 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.
tag - */ - var $arch = $('
'); - var $last = $arch; - $node.contents().each(function () { - if (dom.isBR(this)) { - $(this).remove(); - $last = $(''); - $arch.append($last); - } else if (/h[0-9]+|li|table|p/i.test(this.tagName)) { - $last = $(''); - $arch.append(this).append($last); - } else if ($arch.is(':empty') && dom.isText(this)) { - $last = $('').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 element in an unique element, only paste - // the 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; }); diff --git a/addons/web_editor/static/lib/summernote/src/js/module/Editor.js b/addons/web_editor/static/lib/summernote/src/js/module/Editor.js index ffa22941..373a9f1e 100644 --- a/addons/web_editor/static/lib/summernote/src/js/module/Editor.js +++ b/addons/web_editor/static/lib/summernote/src/js/module/Editor.js @@ -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. diff --git a/addons/web_editor/static/src/js/backend/fields.js b/addons/web_editor/static/src/js/backend/fields.js index 68da1248..0c30a52d 100644 --- a/addons/web_editor/static/src/js/backend/fields.js +++ b/addons/web_editor/static/src/js/backend/fields.js @@ -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']) { diff --git a/addons/web_editor/static/src/js/editor/editor.js b/addons/web_editor/static/src/js/editor/editor.js index e7c1e2d1..c2f9e18f 100644 --- a/addons/web_editor/static/src/js/editor/editor.js +++ b/addons/web_editor/static/src/js/editor/editor.js @@ -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(); diff --git a/addons/web_editor/static/src/js/editor/rte.js b/addons/web_editor/static/src/js/editor/rte.js index 13aebc70..07d56a80 100644 --- a/addons/web_editor/static/src/js/editor/rte.js +++ b/addons/web_editor/static/src/js/editor/rte.js @@ -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') diff --git a/addons/web_editor/static/src/js/editor/rte.summernote.js b/addons/web_editor/static/src/js/editor/rte.summernote.js index 635cbfaa..bcd33771 100644 --- a/addons/web_editor/static/src/js/editor/rte.summernote.js +++ b/addons/web_editor/static/src/js/editor/rte.summernote.js @@ -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 diff --git a/addons/web_editor/static/src/js/editor/summernote.js b/addons/web_editor/static/src/js/editor/summernote.js index ce7f2244..253e4179 100644 --- a/addons/web_editor/static/src/js/editor/summernote.js +++ b/addons/web_editor/static/src/js/editor/summernote.js @@ -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; diff --git a/addons/web_editor/static/src/js/editor/transcoder.js b/addons/web_editor/static/src/js/editor/transcoder.js index 8131762e..8172ea40 100644 --- a/addons/web_editor/static/src/js/editor/transcoder.js +++ b/addons/web_editor/static/src/js/editor/transcoder.js @@ -105,25 +105,52 @@ function getMatchedCSSRules(a) { delete style.display; } - _.each(['margin', 'padding'], function (p) { - if (style[p+'-top'] || style[p+'-right'] || style[p+'-bottom'] || style[p+'-left']) { - if (style[p+'-top'] === style[p+'-right'] && style[p+'-top'] === style[p+'-bottom'] && style[p+'-top'] === style[p+'-left']) { + // The css generates all the attributes separately and not in simplified form. + // In order to have a better compatibility (outlook for example) we simplify the css tags. + // e.g. border-left-style: none; border-bottom-s .... will be simplified in border-style = none + _.each([ + {property: 'margin'}, + {property: 'padding'}, + {property: 'border', propertyEnd: '-style', defaultValue: 'none'}, + ], function (propertyInfo) { + var p = propertyInfo.property; + var e = propertyInfo.propertyEnd || ''; + var defVal = propertyInfo.defaultValue || 0; + + if (style[p+'-top'+e] || style[p+'-right'+e] || style[p+'-bottom'+e] || style[p+'-left'+e]) { + if (style[p+'-top'+e] === style[p+'-right'+e] && style[p+'-top'+e] === style[p+'-bottom'+e] && style[p+'-top'+e] === style[p+'-left'+e]) { // keep => property: [top/right/bottom/left value]; - style[p] = style[p+'-top']; + style[p+e] = style[p+'-top'+e]; } else { // keep => property: [top value] [right value] [bottom value] [left value]; - style[p] = (style[p+'-top'] || 0) + ' ' + (style[p+'-right'] || 0) + ' ' + (style[p+'-bottom'] || 0) + ' ' + (style[p+'-left'] || 0); - if (style[p].indexOf('inherit') !== -1 || style[p].indexOf('initial') !== -1) { + style[p+e] = (style[p+'-top'+e] || defVal) + ' ' + (style[p+'-right'+e] || defVal) + ' ' + (style[p+'-bottom'+e] || defVal) + ' ' + (style[p+'-left'+e] || defVal); + if (style[p+e].indexOf('inherit') !== -1 || style[p+e].indexOf('initial') !== -1) { // keep => property-top: [top value]; property-right: [right value]; property-bottom: [bottom value]; property-left: [left value]; - delete style[p]; + delete style[p+e]; return; } } - delete style[p+'-top']; - delete style[p+'-right']; - delete style[p+'-bottom']; - delete style[p+'-left']; + delete style[p+'-top'+e]; + delete style[p+'-right'+e]; + delete style[p+'-bottom'+e]; + delete style[p+'-left'+e]; + } + }); + + if (style['border-bottom-left-radius']) { + style['border-radius'] = style['border-bottom-left-radius']; + delete style['border-bottom-left-radius']; + delete style['border-bottom-right-radius']; + delete style['border-top-left-radius']; + delete style['border-top-right-radius']; + } + + // if the border styling is initial we remove it to simplify the css tags for compatibility. + // Also, since we do not send a css style tag, the initial value of the border is useless. + _.each(_.keys(style), function (k) { + if (k.indexOf('border') !== -1 && style[k] === 'initial') { + delete style[k]; } }); @@ -205,6 +232,26 @@ function imgToFont($editable) { }); } +/* + * Utility function to apply function over descendants elements + * + * This is needed until the following issue of jQuery is solved: + * https://github.com./jquery/sizzle/issues/403 + * + * @param {Element} node The root Element node + * @param {Function} func The function applied over descendants + */ +function applyOverDescendants(node, func) { + node = node.firstChild; + while (node) { + if (node.nodeType === 1) { + func(node); + applyOverDescendants(node, func); + } + node = node.nextSibling; + } +} + /** * Converts css style to inline style (leave the classes on elements but forces * the style they give as inline style). @@ -215,9 +262,9 @@ function classToStyle($editable) { if (!rulesCache.length) { getMatchedCSSRules($editable[0]); } - $editable.find('*').each(function () { - var $target = $(this); - var css = getMatchedCSSRules(this); + applyOverDescendants($editable[0], function (node) { + var $target = $(node); + var css = getMatchedCSSRules(node); var style = $target.attr('style') || ''; _.each(css, function (v,k) { if (!(new RegExp('(^|;)\s*' + k).test(style))) { @@ -229,6 +276,34 @@ function classToStyle($editable) { } else { $target.attr('style', style); } + // Apple Mail + if (node.nodeName === 'TD' && !node.childNodes.length) { + node.innerHTML = ' '; + } + + // Outlook + if (node.nodeName === 'A' && $target.hasClass('btn') && !$target.hasClass('btn-link') && !$target.children().length) { + var $hack = $('Contact us about anything related to our company or services.
+We'll do our best to get back to you as soon as possible.
+