[MERGE] forward port branch saas-14 up to 55ce6aff10
This commit is contained in:
@@ -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) {
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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):
|
||||
|
||||
|
||||
@@ -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 \
|
||||
|
||||
@@ -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;
|
||||
});
|
||||
|
||||
@@ -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)) {
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user