[MERGE] forward port branch saas-14 up to 55ce6aff10

This commit is contained in:
Christophe Simonis
2018-05-02 14:49:58 +02:00
12 changed files with 139 additions and 260 deletions
+2 -1
View File
@@ -1090,8 +1090,9 @@ class AccountMoveLine(models.Model):
rec_move_ids += account_move_line.matched_credit_ids
if self.env.context.get('invoice_id'):
current_invoice = self.env['account.invoice'].browse(self.env.context['invoice_id'])
aml_to_keep = current_invoice.move_id.line_ids | current_invoice.move_id.line_ids.mapped('full_reconcile_id.exchange_move_id.line_ids')
rec_move_ids = rec_move_ids.filtered(
lambda r: (r.debit_move_id + r.credit_move_id) & current_invoice.move_id.line_ids
lambda r: (r.debit_move_id + r.credit_move_id) & aml_to_keep
)
return rec_move_ids.unlink()
@@ -656,3 +656,51 @@ class TestReconciliation(AccountingTestCase):
credit_aml.with_context(invoice_id=inv2.id).remove_move_reconcile()
self.assertAlmostEquals(inv1.residual, 10)
self.assertAlmostEquals(inv2.residual, 20)
def test_unreconcile_exchange(self):
# Use case:
# - Company currency in EUR
# - Create 2 rates for USD:
# 1.0 on 2018-01-01
# 0.5 on 2018-02-01
# - Create an invoice on 2018-01-02 of 111 USD
# - Register a payment on 2018-02-02 of 111 USD
# - Unreconcile the payment
self.env['res.currency.rate'].create({
'name': time.strftime('%Y') + '-07-01',
'rate': 1.0,
'currency_id': self.currency_usd_id,
'company_id': self.env.ref('base.main_company').id
})
self.env['res.currency.rate'].create({
'name': time.strftime('%Y') + '-08-01',
'rate': 0.5,
'currency_id': self.currency_usd_id,
'company_id': self.env.ref('base.main_company').id
})
inv = self.create_invoice(invoice_amount=111, currency_id=self.currency_usd_id)
payment = self.env['account.payment'].create({
'payment_type': 'inbound',
'payment_method_id': self.env.ref('account.account_payment_method_manual_in').id,
'partner_type': 'customer',
'partner_id': self.partner_agrolait_id,
'amount': 111,
'currency_id': self.currency_usd_id,
'journal_id': self.bank_journal_usd.id,
'payment_date': time.strftime('%Y') + '-08-01',
})
payment.post()
credit_aml = payment.move_line_ids.filtered('credit')
# Check residual before assignation
self.assertAlmostEquals(inv.residual, 111)
# Assign credit, check exchange move and residual
inv.assign_outstanding_credit(credit_aml.id)
self.assertEqual(len(payment.move_line_ids.mapped('full_reconcile_id').exchange_move_id), 1)
self.assertAlmostEquals(inv.residual, 0)
# Unreconcile invoice and check residual
credit_aml.with_context(invoice_id=inv.id).remove_move_reconcile()
self.assertAlmostEquals(inv.residual, 111)
@@ -35,7 +35,7 @@ class AccountRegisterPayments(models.TransientModel):
res = super(AccountRegisterPayments, self).get_payment_vals()
if self.payment_method_id == self.env.ref('account_check_printing.account_payment_method_check'):
res.update({
'check_manual_sequencing': self.check_manual_sequencing,
'check_amount_in_words': self.check_amount_in_words,
})
return res
@@ -146,7 +146,16 @@ function make_message (data) {
_.each(_.keys(emoji_substitutions), function (key) {
var escaped_key = String(key).replace(/([.*+?=^!:${}()|[\]\/\\])/g, '\\$1');
var regexp = new RegExp("(?:^|\\s|<[a-z]*>)(" + escaped_key + ")(?=\\s|$|</[a-z]*>)", "g");
var msg_bak = msg.body;
msg.body = msg.body.replace(regexp, ' <span class="o_mail_emoji">'+emoji_substitutions[key]+'</span> ');
// Idiot-proof limit. If the user had the amazing idea of copy-pasting thousands of emojis,
// the image rendering can lead to memory overflow errors on some browsers (e.g. Chrome).
// Set an arbitrary limit to 200 from which we simply don't replace them (anyway, they are
// already replaced by the unicode counterpart).
if (_.str.count(msg.body, 'o_mail_emoji') > 200) {
msg.body = msg_bak;
}
});
function property_descr(channel) {
+6 -1
View File
@@ -452,7 +452,12 @@ class ProductProduct(models.Model):
limit2 = (limit - len(products)) if limit else False
products += self.search(args + [('name', operator, name), ('id', 'not in', products.ids)], limit=limit2)
elif not products and operator in expression.NEGATIVE_TERM_OPERATORS:
products = self.search(args + ['&', ('default_code', operator, name), ('name', operator, name)], limit=limit)
domain = expression.OR([
['&', ('default_code', operator, name), ('name', operator, name)],
['&', ('default_code', '=', False), ('name', operator, name)],
])
domain = expression.AND([args, domain])
products = self.search(domain, limit=limit)
if not products and operator in positive_operators:
ptrn = re.compile('(\[(.*?)\])')
res = ptrn.search(name)
+9
View File
@@ -40,6 +40,15 @@ class TestVariantsSearch(TransactionCase):
self.assertIn(self.product_shirt_template, search_value,
'Shirt should be found searching L')
def test_name_search(self):
self.product_slip_template = self.env['product.template'].create({
'name': 'Slip',
})
res = self.env['product.product'].name_search('Shirt', [], 'not ilike', None)
res_ids = [r[0] for r in res]
self.assertIn(self.product_slip_template.product_variant_ids.id, res_ids,
'Slip should be found searching \'not ilike\'')
class TestVariants(common.TestProductCommon):
+1 -1
View File
@@ -988,7 +988,7 @@ class ProcurementOrder(models.Model):
self.ensure_one()
schedule_date = self._get_purchase_schedule_date()
purchase_date = self._get_purchase_order_date(partner, schedule_date)
fpos = self.env['account.fiscal.position'].with_context(company_id=self.company_id.id).get_fiscal_position(partner.id)
fpos = self.env['account.fiscal.position'].with_context(force_company=self.company_id.id).get_fiscal_position(partner.id)
gpo = self.rule_id.group_propagation_option
group = (gpo == 'fixed' and self.rule_id.group_id.id) or \
+6 -2
View File
@@ -252,8 +252,12 @@ 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));
options.error({
message: nodes.length > 1 ? nodes[1].textContent : '',
data: {
title: nodes.length > 0 ? nodes[0].textContent : '',
},
});
}
} finally {
complete();
@@ -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;
// ODOO override: use 0.8.10 version of this, adapted for the old summernote
// version odoo 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;
});
+22 -5
View File
@@ -194,8 +194,6 @@ var FieldTextHtml = widget.extend({
},
start: function () {
var self = this;
// Do not Forward port in >= 11.0
this.$formButtons = this.getParent().$buttons;
this.callback = _.uniqueId('FieldTextHtml_');
window.odoo[this.callback+"_editor"] = function (EditorBar) {
setTimeout(function () {
@@ -293,13 +291,32 @@ var FieldTextHtml = widget.extend({
src += "&datarecord="+ encodeURIComponent(JSON.stringify(datarecord));
return src;
},
_toggleFormButtons: function(enable) {
if (this.$formButtons) {
if (enable) {
this.$formButtons.find('button').removeClass('o_disabled').attr('disabled', false);
} else {
this.$formButtons.find('button').addClass('o_disabled').attr('disabled', true);
}
}
},
initialize_content: function () {
var self = this;
// Do not Forward port in >= 11.0
function getModalButtons() {
var $modal = self.getParent().getParent();
if ($modal && $modal.$footer) {
return $modal.$footer;
}
}
this.$formButtons = this.getParent().$buttons || getModalButtons();
this.$el.closest('.modal-body').css('max-height', 'none');
this.$iframe = this.$el.find('iframe');
// deactivate any button to avoid saving a not ready iframe
// Do not Forward port in >= 11.0
this.$formButtons.find('button').addClass('o_disabled').attr('disabled', true);
this._toggleFormButtons(false);
this.document = null;
this.$body = $();
this.$content = $();
@@ -327,7 +344,7 @@ var FieldTextHtml = widget.extend({
this.render_value();
// reactivate all the buttons when the field's content (the iframe) is loaded
// Do not Forward port in >= 11.0
this.$formButtons.find('button').removeClass('o_disabled').attr('disabled', false);
this._toggleFormButtons(true);
setTimeout(function () {
self.add_button();
setTimeout(self.resize,0);
@@ -426,7 +443,7 @@ var FieldTextHtml = widget.extend({
},
destroy: function () {
// Do not Forward port in >= 11.0
this.$formButtons.find('button').removeClass('o_disabled').attr('disabled', false);
this._toggleFormButtons(true);
$(window).off('resize', this.resize);
delete window.odoo[this.callback+"_editor"];
delete window.odoo[this.callback+"_content"];
@@ -676,7 +676,7 @@ function summernote_mousedown (event) {
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)) {
+1
View File
@@ -34,6 +34,7 @@ class Currency(models.Model):
]
@api.multi
@api.depends('rate_ids.rate')
def _compute_current_rate(self):
date = self._context.get('date') or fields.Datetime.now()
company_id = self._context.get('company_id') or self.env['res.users']._get_company().id