[MERGE] forward port branch 11.0 up to 36d96e0150
This commit is contained in:
@@ -588,6 +588,8 @@ class AccountBankStatementLine(models.Model):
|
|||||||
:returns: The journal entries with which the transaction was matched. If there was at least an entry in counterpart_aml_dicts or new_aml_dicts, this list contains
|
:returns: The journal entries with which the transaction was matched. If there was at least an entry in counterpart_aml_dicts or new_aml_dicts, this list contains
|
||||||
the move created by the reconciliation, containing entries for the statement.line (1), the counterpart move lines (0..*) and the new move lines (0..*).
|
the move created by the reconciliation, containing entries for the statement.line (1), the counterpart move lines (0..*) and the new move lines (0..*).
|
||||||
"""
|
"""
|
||||||
|
payable_account_type = self.env.ref('account.data_account_type_payable')
|
||||||
|
receivable_account_type = self.env.ref('account.data_account_type_receivable')
|
||||||
counterpart_aml_dicts = counterpart_aml_dicts or []
|
counterpart_aml_dicts = counterpart_aml_dicts or []
|
||||||
payment_aml_rec = payment_aml_rec or self.env['account.move.line']
|
payment_aml_rec = payment_aml_rec or self.env['account.move.line']
|
||||||
new_aml_dicts = new_aml_dicts or []
|
new_aml_dicts = new_aml_dicts or []
|
||||||
@@ -608,10 +610,16 @@ class AccountBankStatementLine(models.Model):
|
|||||||
raise UserError(_('A selected move line was already reconciled.'))
|
raise UserError(_('A selected move line was already reconciled.'))
|
||||||
if isinstance(aml_dict['move_line'], pycompat.integer_types):
|
if isinstance(aml_dict['move_line'], pycompat.integer_types):
|
||||||
aml_dict['move_line'] = aml_obj.browse(aml_dict['move_line'])
|
aml_dict['move_line'] = aml_obj.browse(aml_dict['move_line'])
|
||||||
|
|
||||||
|
account_types = self.env['account.account.type']
|
||||||
for aml_dict in (counterpart_aml_dicts + new_aml_dicts):
|
for aml_dict in (counterpart_aml_dicts + new_aml_dicts):
|
||||||
if aml_dict.get('tax_ids') and isinstance(aml_dict['tax_ids'][0], pycompat.integer_types):
|
if aml_dict.get('tax_ids') and isinstance(aml_dict['tax_ids'][0], pycompat.integer_types):
|
||||||
# Transform the value in the format required for One2many and Many2many fields
|
# Transform the value in the format required for One2many and Many2many fields
|
||||||
aml_dict['tax_ids'] = [(4, id, None) for id in aml_dict['tax_ids']]
|
aml_dict['tax_ids'] = [(4, id, None) for id in aml_dict['tax_ids']]
|
||||||
|
|
||||||
|
user_type_id = self.env['account.account'].browse(aml_dict.get('account_id')).user_type_id
|
||||||
|
if user_type_id in [payable_account_type, receivable_account_type] and user_type_id not in account_types:
|
||||||
|
account_types |= user_type_id
|
||||||
if any(line.journal_entry_ids for line in self):
|
if any(line.journal_entry_ids for line in self):
|
||||||
raise UserError(_('A selected statement line was already reconciled with an account move.'))
|
raise UserError(_('A selected statement line was already reconciled with an account move.'))
|
||||||
|
|
||||||
@@ -639,7 +647,9 @@ class AccountBankStatementLine(models.Model):
|
|||||||
if abs(total)>0.00001:
|
if abs(total)>0.00001:
|
||||||
partner_id = self.partner_id and self.partner_id.id or False
|
partner_id = self.partner_id and self.partner_id.id or False
|
||||||
partner_type = False
|
partner_type = False
|
||||||
if partner_id:
|
if partner_id and len(account_types) == 1:
|
||||||
|
partner_type = 'customer' if account_types == receivable_account_type else 'supplier'
|
||||||
|
if partner_id and not partner_type:
|
||||||
if total < 0:
|
if total < 0:
|
||||||
partner_type = 'supplier'
|
partner_type = 'supplier'
|
||||||
else:
|
else:
|
||||||
|
|||||||
@@ -148,8 +148,13 @@ class AccountInvoice(models.Model):
|
|||||||
amount_to_show = line.company_id.currency_id.with_context(date=line.date).compute(abs(line.amount_residual), self.currency_id)
|
amount_to_show = line.company_id.currency_id.with_context(date=line.date).compute(abs(line.amount_residual), self.currency_id)
|
||||||
if float_is_zero(amount_to_show, precision_rounding=self.currency_id.rounding):
|
if float_is_zero(amount_to_show, precision_rounding=self.currency_id.rounding):
|
||||||
continue
|
continue
|
||||||
|
if line.ref :
|
||||||
|
title = '%s : %s' % (line.move_id.name, line.ref)
|
||||||
|
else:
|
||||||
|
title = line.move_id.name
|
||||||
info['content'].append({
|
info['content'].append({
|
||||||
'journal_name': line.ref or line.move_id.name,
|
'journal_name': line.ref or line.move_id.name,
|
||||||
|
'title': title,
|
||||||
'amount': amount_to_show,
|
'amount': amount_to_show,
|
||||||
'currency': currency_id.symbol,
|
'currency': currency_id.symbol,
|
||||||
'id': line.id,
|
'id': line.id,
|
||||||
@@ -1277,6 +1282,20 @@ class AccountInvoice(models.Model):
|
|||||||
return result
|
return result
|
||||||
|
|
||||||
@api.model
|
@api.model
|
||||||
|
def _refund_tax_lines_account_change(self, lines, taxes_to_change):
|
||||||
|
# Let's change the account on tax lines when
|
||||||
|
# @param {list} lines: a list of orm commands
|
||||||
|
# @param {dict} taxes_to_change
|
||||||
|
# key: tax ID, value: refund account
|
||||||
|
|
||||||
|
if not taxes_to_change:
|
||||||
|
return lines
|
||||||
|
|
||||||
|
for line in lines:
|
||||||
|
if isinstance(line[2], dict) and line[2]['tax_id'] in taxes_to_change:
|
||||||
|
line[2]['account_id'] = taxes_to_change[line[2]['tax_id']]
|
||||||
|
return lines
|
||||||
|
|
||||||
def _get_refund_common_fields(self):
|
def _get_refund_common_fields(self):
|
||||||
return ['partner_id', 'payment_term_id', 'account_id', 'currency_id', 'journal_id']
|
return ['partner_id', 'payment_term_id', 'account_id', 'currency_id', 'journal_id']
|
||||||
|
|
||||||
@@ -1322,7 +1341,12 @@ class AccountInvoice(models.Model):
|
|||||||
values['invoice_line_ids'] = self._refund_cleanup_lines(invoice.invoice_line_ids)
|
values['invoice_line_ids'] = self._refund_cleanup_lines(invoice.invoice_line_ids)
|
||||||
|
|
||||||
tax_lines = invoice.tax_line_ids
|
tax_lines = invoice.tax_line_ids
|
||||||
values['tax_line_ids'] = self._refund_cleanup_lines(tax_lines)
|
taxes_to_change = {
|
||||||
|
line.tax_id.id: line.tax_id.refund_account_id.id
|
||||||
|
for line in tax_lines.filtered(lambda l: l.tax_id.refund_account_id != l.tax_id.account_id)
|
||||||
|
}
|
||||||
|
cleaned_tax_lines = self._refund_cleanup_lines(tax_lines)
|
||||||
|
values['tax_line_ids'] = self._refund_tax_lines_account_change(cleaned_tax_lines, taxes_to_change)
|
||||||
|
|
||||||
if journal_id:
|
if journal_id:
|
||||||
journal = self.env['account.journal'].browse(journal_id)
|
journal = self.env['account.journal'].browse(journal_id)
|
||||||
|
|||||||
@@ -16,8 +16,8 @@
|
|||||||
<td>
|
<td>
|
||||||
<a title="assign to invoice" role="button" class="oe_form_field outstanding_credit_assign" t-att-data-id="line.id" style="margin-right: 10px;">Add</a>
|
<a title="assign to invoice" role="button" class="oe_form_field outstanding_credit_assign" t-att-data-id="line.id" style="margin-right: 10px;">Add</a>
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td style="max-width: 10em;">
|
||||||
<span class="oe_form_field" style="margin-right: 30px;"><t t-esc="line.journal_name"></t></span>
|
<div class="oe_form_field" style="margin-right: 30px; text-overflow: ellipsis; overflow: hidden; white-space: nowrap;" t-att-title="line.title"><t t-esc="line.journal_name"></t></div>
|
||||||
</td>
|
</td>
|
||||||
</t>
|
</t>
|
||||||
<t t-if="!outstanding">
|
<t t-if="!outstanding">
|
||||||
|
|||||||
@@ -167,3 +167,59 @@ class TestAccountCustomerInvoice(AccountTestUsers):
|
|||||||
))
|
))
|
||||||
|
|
||||||
self.assertEquals(invoice.amount_untaxed, sum([x.base for x in invoice.tax_line_ids]))
|
self.assertEquals(invoice.amount_untaxed, sum([x.base for x in invoice.tax_line_ids]))
|
||||||
|
|
||||||
|
def test_customer_invoice_tax_refund(self):
|
||||||
|
company = self.env.user.company_id
|
||||||
|
tax_account = self.env['account.account'].create({
|
||||||
|
'name': 'TAX',
|
||||||
|
'code': 'TAX',
|
||||||
|
'user_type_id': self.env.ref('account.data_account_type_current_assets').id,
|
||||||
|
'company_id': company.id,
|
||||||
|
})
|
||||||
|
|
||||||
|
tax_refund_account = self.env['account.account'].create({
|
||||||
|
'name': 'TAX_REFUND',
|
||||||
|
'code': 'TAX_R',
|
||||||
|
'user_type_id': self.env.ref('account.data_account_type_current_assets').id,
|
||||||
|
'company_id': company.id,
|
||||||
|
})
|
||||||
|
|
||||||
|
journalrec = self.env['account.journal'].search([('type', '=', 'sale')])[0]
|
||||||
|
partner3 = self.env.ref('base.res_partner_3')
|
||||||
|
account_id = self.env['account.account'].search([('user_type_id', '=', self.env.ref('account.data_account_type_revenue').id)], limit=1).id
|
||||||
|
|
||||||
|
tax = self.env['account.tax'].create({
|
||||||
|
'name': 'Tax 15.0',
|
||||||
|
'amount': 15.0,
|
||||||
|
'amount_type': 'percent',
|
||||||
|
'type_tax_use': 'sale',
|
||||||
|
'account_id': tax_account.id,
|
||||||
|
'refund_account_id': tax_refund_account.id
|
||||||
|
})
|
||||||
|
|
||||||
|
invoice_line_data = [
|
||||||
|
(0, 0,
|
||||||
|
{
|
||||||
|
'product_id': self.env.ref('product.product_product_1').id,
|
||||||
|
'quantity': 40.0,
|
||||||
|
'account_id': account_id,
|
||||||
|
'name': 'product test 1',
|
||||||
|
'discount': 10.00,
|
||||||
|
'price_unit': 2.27,
|
||||||
|
'invoice_line_tax_ids': [(6, 0, [tax.id])],
|
||||||
|
}
|
||||||
|
)]
|
||||||
|
|
||||||
|
invoice = self.env['account.invoice'].create(dict(
|
||||||
|
name="Test Customer Invoice",
|
||||||
|
reference_type="none",
|
||||||
|
journal_id=journalrec.id,
|
||||||
|
partner_id=partner3.id,
|
||||||
|
invoice_line_ids=invoice_line_data
|
||||||
|
))
|
||||||
|
|
||||||
|
invoice.action_invoice_open()
|
||||||
|
|
||||||
|
refund = invoice.refund()
|
||||||
|
self.assertEqual(invoice.tax_line_ids.mapped('account_id'), tax_account)
|
||||||
|
self.assertEqual(refund.tax_line_ids.mapped('account_id'), tax_refund_account)
|
||||||
|
|||||||
@@ -239,7 +239,7 @@
|
|||||||
<record id="pcg_120" model="account.account.template">
|
<record id="pcg_120" model="account.account.template">
|
||||||
<field name="name">Résultat de l'exercice (bénéfice)</field>
|
<field name="name">Résultat de l'exercice (bénéfice)</field>
|
||||||
<field name="code">120</field>
|
<field name="code">120</field>
|
||||||
<field name="user_type_id" ref="account.data_unaffected_earnings"/>
|
<field name="user_type_id" ref="account.data_account_type_current_liabilities"/>
|
||||||
<field name="chart_template_id" ref="l10n_fr_pcg_chart_template"/>
|
<field name="chart_template_id" ref="l10n_fr_pcg_chart_template"/>
|
||||||
</record>
|
</record>
|
||||||
|
|
||||||
|
|||||||
@@ -1210,7 +1210,8 @@ class MailThread(models.AbstractModel):
|
|||||||
subtype = 'mail.mt_note'
|
subtype = 'mail.mt_note'
|
||||||
if message_dict.get('parent_id'):
|
if message_dict.get('parent_id'):
|
||||||
parent_message = self.env['mail.message'].sudo().browse(message_dict['parent_id'])
|
parent_message = self.env['mail.message'].sudo().browse(message_dict['parent_id'])
|
||||||
partner_ids = [(4, parent_message.author_id.id)]
|
if parent_message.author_id:
|
||||||
|
partner_ids = [(4, parent_message.author_id.id)]
|
||||||
else:
|
else:
|
||||||
subtype = 'mail.mt_comment'
|
subtype = 'mail.mt_comment'
|
||||||
new_msg = thread.message_post(subtype=subtype, partner_ids=partner_ids, **message_dict)
|
new_msg = thread.message_post(subtype=subtype, partner_ids=partner_ids, **message_dict)
|
||||||
|
|||||||
@@ -277,7 +277,7 @@ var Activity = AbstractActivityField.extend({
|
|||||||
$popover.find('#activity_feedback').val(self.feedbackValue[activity_id]);
|
$popover.find('#activity_feedback').val(self.feedbackValue[activity_id]);
|
||||||
$popover.on('click', '.o_activity_popover_done_next', function () {
|
$popover.on('click', '.o_activity_popover_done_next', function () {
|
||||||
var feedback = _.escape($popover.find('#activity_feedback').val());
|
var feedback = _.escape($popover.find('#activity_feedback').val());
|
||||||
var previous_activity_type_id = $popover_el.data('previous-activity-type-id');
|
var previous_activity_type_id = $popover_el.data('previous-activity-type-id') || false;
|
||||||
self._markActivityDone(activity_id, feedback)
|
self._markActivityDone(activity_id, feedback)
|
||||||
.then(self.scheduleActivity.bind(self, previous_activity_type_id));
|
.then(self.scheduleActivity.bind(self, previous_activity_type_id));
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -4,6 +4,7 @@
|
|||||||
from . import mass_mailing
|
from . import mass_mailing
|
||||||
from . import mass_mailing_stats
|
from . import mass_mailing_stats
|
||||||
from . import mail_mail
|
from . import mail_mail
|
||||||
|
from . import mail_template
|
||||||
from . import mail_thread
|
from . import mail_thread
|
||||||
from . import res_config_settings
|
from . import res_config_settings
|
||||||
from . import mass_mailing_report
|
from . import mass_mailing_report
|
||||||
|
|||||||
@@ -0,0 +1,22 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||||
|
|
||||||
|
from odoo import api, models
|
||||||
|
|
||||||
|
|
||||||
|
class MailTemplate(models.Model):
|
||||||
|
_inherit = "mail.template"
|
||||||
|
|
||||||
|
@api.model
|
||||||
|
def render_post_process(self, html):
|
||||||
|
# super will transform relative url to absolute
|
||||||
|
html = super(MailTemplate, self).render_post_process(html)
|
||||||
|
|
||||||
|
# apply shortener after
|
||||||
|
if self.env.context.get('post_convert_links'):
|
||||||
|
html = self.env['link.tracker'].convert_links(
|
||||||
|
html,
|
||||||
|
self.env.context['post_convert_links'],
|
||||||
|
blacklist=['/unsubscribe_from_list']
|
||||||
|
)
|
||||||
|
return html
|
||||||
@@ -643,6 +643,21 @@ class MassMailing(models.Model):
|
|||||||
_logger.info("Mass-mailing %s targets %s, no blacklist available", self, target._name)
|
_logger.info("Mass-mailing %s targets %s, no blacklist available", self, target._name)
|
||||||
return blacklist
|
return blacklist
|
||||||
|
|
||||||
|
def _get_convert_links(self):
|
||||||
|
self.ensure_one()
|
||||||
|
utm_mixin = self.mass_mailing_campaign_id if self.mass_mailing_campaign_id else self
|
||||||
|
vals = {'mass_mailing_id': self.id}
|
||||||
|
|
||||||
|
if self.mass_mailing_campaign_id:
|
||||||
|
vals['mass_mailing_campaign_id'] = self.mass_mailing_campaign_id.id
|
||||||
|
if utm_mixin.campaign_id:
|
||||||
|
vals['campaign_id'] = utm_mixin.campaign_id.id
|
||||||
|
if utm_mixin.source_id:
|
||||||
|
vals['source_id'] = utm_mixin.source_id.id
|
||||||
|
if utm_mixin.medium_id:
|
||||||
|
vals['medium_id'] = utm_mixin.medium_id.id
|
||||||
|
return vals
|
||||||
|
|
||||||
def _get_seen_list(self):
|
def _get_seen_list(self):
|
||||||
"""Returns a set of emails already targeted by current mailing/campaign (no duplicates)"""
|
"""Returns a set of emails already targeted by current mailing/campaign (no duplicates)"""
|
||||||
self.ensure_one()
|
self.ensure_one()
|
||||||
@@ -678,6 +693,7 @@ class MassMailing(models.Model):
|
|||||||
return {
|
return {
|
||||||
'mass_mailing_blacklist': self._get_blacklist(),
|
'mass_mailing_blacklist': self._get_blacklist(),
|
||||||
'mass_mailing_seen_list': self._get_seen_list(),
|
'mass_mailing_seen_list': self._get_seen_list(),
|
||||||
|
'post_convert_links': self._get_convert_links(),
|
||||||
}
|
}
|
||||||
|
|
||||||
def get_recipients(self):
|
def get_recipients(self):
|
||||||
@@ -719,13 +735,10 @@ class MassMailing(models.Model):
|
|||||||
if not res_ids:
|
if not res_ids:
|
||||||
raise UserError(_('Please select recipients.'))
|
raise UserError(_('Please select recipients.'))
|
||||||
|
|
||||||
# Convert links in absolute URLs before the application of the shortener
|
|
||||||
mailing.body_html = self.env['mail.thread']._replace_local_links(mailing.body_html)
|
|
||||||
|
|
||||||
composer_values = {
|
composer_values = {
|
||||||
'author_id': author_id,
|
'author_id': author_id,
|
||||||
'attachment_ids': [(4, attachment.id) for attachment in mailing.attachment_ids],
|
'attachment_ids': [(4, attachment.id) for attachment in mailing.attachment_ids],
|
||||||
'body': mailing.convert_links()[mailing.id],
|
'body': mailing.body_html,
|
||||||
'subject': mailing.name,
|
'subject': mailing.name,
|
||||||
'model': mailing.mailing_model_real,
|
'model': mailing.mailing_model_real,
|
||||||
'email_from': mailing.email_from,
|
'email_from': mailing.email_from,
|
||||||
|
|||||||
@@ -2,3 +2,4 @@
|
|||||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||||
|
|
||||||
from . import test_mass_mailing_list_merge
|
from . import test_mass_mailing_list_merge
|
||||||
|
from . import test_mass_mailing_shortener
|
||||||
|
|||||||
@@ -0,0 +1,132 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||||
|
|
||||||
|
from odoo.tests import common
|
||||||
|
from lxml import etree
|
||||||
|
|
||||||
|
try:
|
||||||
|
from unittest.mock import patch
|
||||||
|
except ImportError:
|
||||||
|
from mock import patch
|
||||||
|
|
||||||
|
|
||||||
|
class TestMassMailingShortener(common.TransactionCase):
|
||||||
|
def getHrefFor(self, html, id):
|
||||||
|
return html.xpath("*[@id='%s']" % id)[0].attrib.get('href')
|
||||||
|
|
||||||
|
def shorturl_to_link(self, short_url):
|
||||||
|
return self.env['link.tracker.code'].search([('code', '=', short_url.split('/r/')[-1])]).link_id
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
super(TestMassMailingShortener, self).setUp()
|
||||||
|
|
||||||
|
def _get_title_from_url(u):
|
||||||
|
return "Hello"
|
||||||
|
|
||||||
|
def _compute_favicon():
|
||||||
|
# 1px to avoid real request
|
||||||
|
return 'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8z8DwHwAFBQIAX8jx0gAAAABJRU5ErkJggg=='
|
||||||
|
|
||||||
|
patcher = patch('odoo.addons.link_tracker.models.link_tracker.link_tracker._compute_favicon', wraps=_compute_favicon)
|
||||||
|
patcher2 = patch('odoo.addons.link_tracker.models.link_tracker.link_tracker._get_title_from_url', wraps=_get_title_from_url)
|
||||||
|
patcher.start()
|
||||||
|
patcher2.start()
|
||||||
|
self.addCleanup(patcher.stop)
|
||||||
|
self.addCleanup(patcher2.stop)
|
||||||
|
|
||||||
|
def test_00_test_mass_mailing_shortener(self):
|
||||||
|
mailing_list_A = self.env['mail.mass_mailing.list'].create({
|
||||||
|
'name': 'A',
|
||||||
|
})
|
||||||
|
self.env['mail.mass_mailing.contact'].create({
|
||||||
|
'name': 'User 1', 'email': 'user1@example.com', 'list_ids': [(4, mailing_list_A.id)]
|
||||||
|
})
|
||||||
|
self.env['mail.mass_mailing.contact'].create({
|
||||||
|
'name': 'User 2', 'email': 'user2@example.com', 'list_ids': [(4, mailing_list_A.id)]
|
||||||
|
})
|
||||||
|
self.env['mail.mass_mailing.contact'].create({
|
||||||
|
'name': 'User 3', 'email': 'user3@example.com', 'list_ids': [(4, mailing_list_A.id)]
|
||||||
|
})
|
||||||
|
|
||||||
|
mass_mailing = self.env['mail.mass_mailing'].create({
|
||||||
|
"reply_to_mode": "email",
|
||||||
|
"reply_to": "Administrator <admin@yourcompany.example.com>",
|
||||||
|
"mailing_model_id": self.env.ref('mass_mailing.model_mail_mass_mailing_list').id,
|
||||||
|
"mailing_domain": "[('list_ids', 'in', [%d])]" % mailing_list_A.id,
|
||||||
|
"contact_list_ids": [[6, False, [mailing_list_A.id]]],
|
||||||
|
"mass_mailing_campaign_id": False,
|
||||||
|
"name": "sdf",
|
||||||
|
"body_html": """
|
||||||
|
Hi,
|
||||||
|
% set url = "www.odoo.com"
|
||||||
|
% set httpurl = "https://www.odoo.eu"
|
||||||
|
Website0: <a id="url0" href="https://www.odoo.tz/my/${object.name}">https://www.odoo.tz/my/${object.name}</h1>
|
||||||
|
Website1: <a id="url1" href="https://www.odoo.be">https://www.odoo.be</h1>
|
||||||
|
Website2: <a id="url2" href="https://${url}">https://${url}</h1>
|
||||||
|
Website3: <a id="url3" href="${httpurl}">${httpurl}</h1>
|
||||||
|
Email: <a id="url4" href="mailto:test@odoo.com">test@odoo.com</h1>
|
||||||
|
""",
|
||||||
|
"schedule_date": False,
|
||||||
|
"state": "draft",
|
||||||
|
"keep_archives": True,
|
||||||
|
})
|
||||||
|
|
||||||
|
mass_mailing.put_in_queue()
|
||||||
|
mass_mailing._process_mass_mailing_queue()
|
||||||
|
|
||||||
|
sent_mails = self.env['mail.mail'].search([('mailing_id', '=', mass_mailing.id)])
|
||||||
|
sent_messages = sent_mails.mapped('mail_message_id')
|
||||||
|
|
||||||
|
self.assertEqual(mailing_list_A.contact_nbr, len(sent_messages),
|
||||||
|
'Some message has not been sent')
|
||||||
|
|
||||||
|
xbody = etree.fromstring(sent_messages[0].body)
|
||||||
|
after_url0 = self.getHrefFor(xbody, 'url0')
|
||||||
|
after_url1 = self.getHrefFor(xbody, 'url1')
|
||||||
|
after_url2 = self.getHrefFor(xbody, 'url2')
|
||||||
|
after_url3 = self.getHrefFor(xbody, 'url3')
|
||||||
|
after_url4 = self.getHrefFor(xbody, 'url4')
|
||||||
|
|
||||||
|
self.assertTrue('/r/' in after_url0, 'URL0 should be shortened: %s' % after_url0)
|
||||||
|
self.assertTrue('/r/' in after_url1, 'URL1 should be shortened: %s' % after_url1)
|
||||||
|
self.assertTrue('/r/' in after_url2, 'URL2 should be shortened: %s' % after_url2)
|
||||||
|
self.assertTrue('/r/' in after_url3, 'URL3 should be shortened: %s' % after_url3)
|
||||||
|
self.assertEqual(after_url4, "mailto:test@odoo.com", 'mailto: has been converted')
|
||||||
|
|
||||||
|
short0 = self.shorturl_to_link(after_url0)
|
||||||
|
short1 = self.shorturl_to_link(after_url1)
|
||||||
|
short2 = self.shorturl_to_link(after_url2)
|
||||||
|
short3 = self.shorturl_to_link(after_url3)
|
||||||
|
|
||||||
|
self.assertTrue("https://www.odoo.tz/my/User" in short0.url, 'URL mismatch')
|
||||||
|
self.assertEqual(short1.url, "https://www.odoo.be", 'URL mismatch')
|
||||||
|
self.assertEqual(short2.url, "https://www.odoo.com", 'URL mismatch')
|
||||||
|
self.assertEqual(short3.url, "https://www.odoo.eu", 'URL mismatch')
|
||||||
|
|
||||||
|
_xbody = etree.fromstring(sent_messages[1].body)
|
||||||
|
_after_url0 = self.getHrefFor(_xbody, 'url0')
|
||||||
|
_after_url1 = self.getHrefFor(_xbody, 'url1')
|
||||||
|
_after_url2 = self.getHrefFor(_xbody, 'url2')
|
||||||
|
_after_url3 = self.getHrefFor(_xbody, 'url3')
|
||||||
|
_after_url4 = self.getHrefFor(_xbody, 'url4')
|
||||||
|
|
||||||
|
self.assertTrue('/r/' in _after_url0, 'URL0 should be shortened: %s' % _after_url0)
|
||||||
|
self.assertTrue('/r/' in _after_url1, 'URL1 should be shortened: %s' % _after_url1)
|
||||||
|
self.assertTrue('/r/' in _after_url2, 'URL2 should be shortened: %s' % _after_url2)
|
||||||
|
self.assertTrue('/r/' in _after_url3, 'URL3 should be shortened: %s' % _after_url3)
|
||||||
|
self.assertEqual(_after_url4, "mailto:test@odoo.com", 'mailto: has been converted')
|
||||||
|
|
||||||
|
_short0 = self.shorturl_to_link(_after_url0)
|
||||||
|
_short1 = self.shorturl_to_link(_after_url1)
|
||||||
|
_short2 = self.shorturl_to_link(_after_url2)
|
||||||
|
_short3 = self.shorturl_to_link(_after_url3)
|
||||||
|
|
||||||
|
self.assertTrue("https://www.odoo.tz/my/User" in _short0.url, 'URL mismatch')
|
||||||
|
self.assertEqual(_short1.url, "https://www.odoo.be", 'URL mismatch')
|
||||||
|
self.assertEqual(_short2.url, "https://www.odoo.com", 'URL mismatch')
|
||||||
|
self.assertEqual(_short3.url, "https://www.odoo.eu", 'URL mismatch')
|
||||||
|
|
||||||
|
self.assertNotEqual(short0.url, _short0.url)
|
||||||
|
self.assertEqual(short1.url, _short1.url)
|
||||||
|
self.assertEqual(short2.url, _short2.url)
|
||||||
|
self.assertEqual(short3.url, _short3.url)
|
||||||
@@ -5,7 +5,7 @@ from collections import defaultdict
|
|||||||
|
|
||||||
from odoo import api, fields, models, _
|
from odoo import api, fields, models, _
|
||||||
from odoo.addons import decimal_precision as dp
|
from odoo.addons import decimal_precision as dp
|
||||||
from odoo.exceptions import UserError
|
from odoo.exceptions import AccessError, UserError
|
||||||
from odoo.tools import float_compare, float_round
|
from odoo.tools import float_compare, float_round
|
||||||
|
|
||||||
class MrpProduction(models.Model):
|
class MrpProduction(models.Model):
|
||||||
@@ -30,6 +30,10 @@ class MrpProduction(models.Model):
|
|||||||
location = self.env['stock.picking.type'].browse(self.env.context['default_picking_type_id']).default_location_src_id
|
location = self.env['stock.picking.type'].browse(self.env.context['default_picking_type_id']).default_location_src_id
|
||||||
if not location:
|
if not location:
|
||||||
location = self.env.ref('stock.stock_location_stock', raise_if_not_found=False)
|
location = self.env.ref('stock.stock_location_stock', raise_if_not_found=False)
|
||||||
|
try:
|
||||||
|
location.check_access_rule('read')
|
||||||
|
except (AttributeError, AccessError):
|
||||||
|
location = self.env['stock.warehouse'].search([('company_id', '=', self.env.user.company_id.id)], limit=1).lot_stock_id
|
||||||
return location and location.id or False
|
return location and location.id or False
|
||||||
|
|
||||||
@api.model
|
@api.model
|
||||||
@@ -39,6 +43,10 @@ class MrpProduction(models.Model):
|
|||||||
location = self.env['stock.picking.type'].browse(self.env.context['default_picking_type_id']).default_location_dest_id
|
location = self.env['stock.picking.type'].browse(self.env.context['default_picking_type_id']).default_location_dest_id
|
||||||
if not location:
|
if not location:
|
||||||
location = self.env.ref('stock.stock_location_stock', raise_if_not_found=False)
|
location = self.env.ref('stock.stock_location_stock', raise_if_not_found=False)
|
||||||
|
try:
|
||||||
|
location.check_access_rule('read')
|
||||||
|
except (AttributeError, AccessError):
|
||||||
|
location = self.env['stock.warehouse'].search([('company_id', '=', self.env.user.company_id.id)], limit=1).lot_stock_id
|
||||||
return location and location.id or False
|
return location and location.id or False
|
||||||
|
|
||||||
name = fields.Char(
|
name = fields.Char(
|
||||||
@@ -295,6 +303,10 @@ class MrpProduction(models.Model):
|
|||||||
@api.onchange('picking_type_id')
|
@api.onchange('picking_type_id')
|
||||||
def onchange_picking_type(self):
|
def onchange_picking_type(self):
|
||||||
location = self.env.ref('stock.stock_location_stock')
|
location = self.env.ref('stock.stock_location_stock')
|
||||||
|
try:
|
||||||
|
location.check_access_rule('read')
|
||||||
|
except (AttributeError, AccessError):
|
||||||
|
location = self.env['stock.warehouse'].search([('company_id', '=', self.env.user.company_id.id)], limit=1).lot_stock_id
|
||||||
self.location_src_id = self.picking_type_id.default_location_src_id.id or location.id
|
self.location_src_id = self.picking_type_id.default_location_src_id.id or location.id
|
||||||
self.location_dest_id = self.picking_type_id.default_location_dest_id.id or location.id
|
self.location_dest_id = self.picking_type_id.default_location_dest_id.id or location.id
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||||
|
|
||||||
from odoo import api, fields, models, _
|
from odoo import api, fields, models, _
|
||||||
from odoo.exceptions import UserError
|
from odoo.exceptions import AccessError, UserError
|
||||||
from odoo.tools import float_compare
|
from odoo.tools import float_compare
|
||||||
|
|
||||||
|
|
||||||
@@ -13,10 +13,20 @@ class MrpUnbuild(models.Model):
|
|||||||
_order = 'id desc'
|
_order = 'id desc'
|
||||||
|
|
||||||
def _get_default_location_id(self):
|
def _get_default_location_id(self):
|
||||||
return self.env.ref('stock.stock_location_stock', raise_if_not_found=False)
|
stock_location = self.env.ref('stock.stock_location_stock', raise_if_not_found=False)
|
||||||
|
try:
|
||||||
|
stock_location.check_access_rule('read')
|
||||||
|
return stock_location.id
|
||||||
|
except (AttributeError, AccessError):
|
||||||
|
return self.env['stock.warehouse'].search([('company_id', '=', self.env.user.company_id.id)], limit=1).lot_stock_id.id
|
||||||
|
|
||||||
def _get_default_location_dest_id(self):
|
def _get_default_location_dest_id(self):
|
||||||
return self.env.ref('stock.stock_location_stock', raise_if_not_found=False)
|
stock_location = self.env.ref('stock.stock_location_stock', raise_if_not_found=False)
|
||||||
|
try:
|
||||||
|
stock_location.check_access_rule('read')
|
||||||
|
return stock_location.id
|
||||||
|
except (AttributeError, AccessError):
|
||||||
|
return self.env['stock.warehouse'].search([('company_id', '=', self.env.user.company_id.id)], limit=1).lot_stock_id.id
|
||||||
|
|
||||||
name = fields.Char('Reference', copy=False, readonly=True, default=lambda x: _('New'))
|
name = fields.Char('Reference', copy=False, readonly=True, default=lambda x: _('New'))
|
||||||
product_id = fields.Many2one(
|
product_id = fields.Many2one(
|
||||||
|
|||||||
@@ -128,8 +128,8 @@
|
|||||||
</group>
|
</group>
|
||||||
<group>
|
<group>
|
||||||
<field name="mo_id" domain="[('state','=','done')]"/>
|
<field name="mo_id" domain="[('state','=','done')]"/>
|
||||||
<field name="location_id" groups="stock.group_stock_multi_locations"/>
|
<field name="location_id" groups="stock.group_stock_multi_locations" domain="[('usage','=','internal')]"/>
|
||||||
<field name="location_dest_id" groups="stock.group_stock_multi_locations"/>
|
<field name="location_dest_id" groups="stock.group_stock_multi_locations" domain="[('usage','=','internal')]"/>
|
||||||
<field name="has_tracking" invisible="1"/>
|
<field name="has_tracking" invisible="1"/>
|
||||||
<field name="lot_id" attrs="{'invisible': [('has_tracking', '=', 'none')], 'required': [('has_tracking', '!=', 'none')]}" groups="stock.group_production_lot"/>
|
<field name="lot_id" attrs="{'invisible': [('has_tracking', '=', 'none')], 'required': [('has_tracking', '!=', 'none')]}" groups="stock.group_production_lot"/>
|
||||||
</group>
|
</group>
|
||||||
|
|||||||
@@ -893,12 +893,20 @@ exports.PosModel = Backbone.Model.extend({
|
|||||||
transfer.pipe(function(order_server_id){
|
transfer.pipe(function(order_server_id){
|
||||||
|
|
||||||
// generate the pdf and download it
|
// generate the pdf and download it
|
||||||
self.chrome.do_action('point_of_sale.pos_invoice_report',{additional_context:{
|
if (order_server_id.length) {
|
||||||
active_ids:order_server_id,
|
self.chrome.do_action('point_of_sale.pos_invoice_report',{additional_context:{
|
||||||
}}).done(function () {
|
active_ids:order_server_id,
|
||||||
invoiced.resolve();
|
}}).done(function () {
|
||||||
done.resolve();
|
invoiced.resolve();
|
||||||
});
|
done.resolve();
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
// The order has been pushed separately in batch when
|
||||||
|
// the connection came back.
|
||||||
|
// The user has to go to the backend to print the invoice
|
||||||
|
invoiced.reject({code:401, message:'Backend Invoice', data:{order: order}});
|
||||||
|
done.reject();
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
return done;
|
return done;
|
||||||
|
|||||||
@@ -2078,6 +2078,17 @@ var PaymentScreenWidget = ScreenWidget.extend({
|
|||||||
self.gui.show_screen('clientlist');
|
self.gui.show_screen('clientlist');
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
} else if (error.message === 'Backend Invoice') {
|
||||||
|
self.gui.show_popup('confirm',{
|
||||||
|
'title': _t('Please print the invoice from the backend'),
|
||||||
|
'body': _t('The order has been synchronized earlier. Please make the invoice from the backend for the order: ') + error.data.order.name,
|
||||||
|
confirm: function () {
|
||||||
|
this.gui.show_screen('receipt');
|
||||||
|
},
|
||||||
|
cancel: function () {
|
||||||
|
this.gui.show_screen('receipt');
|
||||||
|
},
|
||||||
|
});
|
||||||
} else if (error.code < 0) { // XmlHttpRequest Errors
|
} else if (error.code < 0) { // XmlHttpRequest Errors
|
||||||
self.gui.show_popup('error',{
|
self.gui.show_popup('error',{
|
||||||
'title': _t('The order could not be sent'),
|
'title': _t('The order could not be sent'),
|
||||||
|
|||||||
@@ -130,7 +130,8 @@ class StockScrap(models.Model):
|
|||||||
self.package_id,
|
self.package_id,
|
||||||
self.owner_id,
|
self.owner_id,
|
||||||
strict=True).mapped('quantity'))
|
strict=True).mapped('quantity'))
|
||||||
if float_compare(available_qty, self.scrap_qty, precision_digits=precision) >= 0:
|
scrap_qty = self.product_uom_id._compute_quantity(self.scrap_qty, self.product_id.uom_id)
|
||||||
|
if float_compare(available_qty, scrap_qty, precision_digits=precision) >= 0:
|
||||||
return self.do_scrap()
|
return self.do_scrap()
|
||||||
else:
|
else:
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -3574,6 +3574,24 @@ class StockMove(TransactionCase):
|
|||||||
self.assertEqual(scrap.state, 'done')
|
self.assertEqual(scrap.state, 'done')
|
||||||
self.assertEqual(move1.reserved_availability, 0.25)
|
self.assertEqual(move1.reserved_availability, 0.25)
|
||||||
|
|
||||||
|
def test_scrap_6(self):
|
||||||
|
""" Check that scrap correctly handle UoM. """
|
||||||
|
self.env['stock.quant']._update_available_quantity(self.product1, self.stock_location, 1)
|
||||||
|
scrap = self.env['stock.scrap'].create({
|
||||||
|
'product_id': self.product1.id,
|
||||||
|
'product_uom_id': self.uom_dozen.id,
|
||||||
|
'scrap_qty': 1,
|
||||||
|
})
|
||||||
|
warning_message = scrap.action_validate()
|
||||||
|
self.assertEqual(warning_message.get('res_model', 'Wrong Model'), 'stock.warn.insufficient.qty.scrap')
|
||||||
|
insufficient_qty_wizard = self.env['stock.warn.insufficient.qty.scrap'].create({
|
||||||
|
'product_id': self.product1.id,
|
||||||
|
'location_id': self.stock_location.id,
|
||||||
|
'scrap_id': scrap.id
|
||||||
|
})
|
||||||
|
insufficient_qty_wizard.action_done()
|
||||||
|
self.assertEqual(self.env['stock.quant']._gather(self.product1, self.stock_location).quantity, -11)
|
||||||
|
|
||||||
def test_in_date_1(self):
|
def test_in_date_1(self):
|
||||||
""" Check that moving a tracked quant keeps the incoming date.
|
""" Check that moving a tracked quant keeps the incoming date.
|
||||||
"""
|
"""
|
||||||
|
|||||||
@@ -212,7 +212,7 @@
|
|||||||
<button name="%(act_stock_return_picking)d" string="Return" attrs="{'invisible': ['|', ('state', '!=', 'done'), ('is_locked', '=', False)]}" type="action" groups="base.group_user"/>
|
<button name="%(act_stock_return_picking)d" string="Return" attrs="{'invisible': ['|', ('state', '!=', 'done'), ('is_locked', '=', False)]}" type="action" groups="base.group_user"/>
|
||||||
<button name="action_cancel" attrs="{'invisible': ['|', ('state', 'not in', ('assigned', 'confirmed', 'partially_available', 'draft', 'waiting')), ('is_locked', '=', False)]}" string="Cancel" groups="base.group_user" type="object"/>
|
<button name="action_cancel" attrs="{'invisible': ['|', ('state', 'not in', ('assigned', 'confirmed', 'partially_available', 'draft', 'waiting')), ('is_locked', '=', False)]}" string="Cancel" groups="base.group_user" type="object"/>
|
||||||
<field name="picking_type_code" invisible="1"/>
|
<field name="picking_type_code" invisible="1"/>
|
||||||
<button name="do_unreserve" string="Unreserve" groups="base.group_user" type="object" attrs="{'invisible': ['|', '|', ('picking_type_code', '=', 'incoming'), ('state', 'not in', ('assigned', 'partially_available')), ('is_locked', '=', False)]}"/>
|
<button name="do_unreserve" string="Unreserve" groups="base.group_user" type="object" attrs="{'invisible': ['|', '|', '|', ('picking_type_code', '=', 'incoming'), ('is_locked', '=', False), '&', ('state', 'not in', ('assigned', 'partially_available')), ('move_type', '!=', 'one'), '&', ('state', 'not in', ('assigned', 'partially_available', 'confirmed')), ('move_type', '=', 'one')]}"/>
|
||||||
<field name="show_operations" invisible="1" readonly="1"/>
|
<field name="show_operations" invisible="1" readonly="1"/>
|
||||||
<field name="move_line_exist" invisible="1"/>
|
<field name="move_line_exist" invisible="1"/>
|
||||||
<field name="has_packages" invisible="1"/>
|
<field name="has_packages" invisible="1"/>
|
||||||
|
|||||||
@@ -182,23 +182,38 @@ class ProductProduct(models.Model):
|
|||||||
StockMove = self.env['stock.move']
|
StockMove = self.env['stock.move']
|
||||||
to_date = self.env.context.get('to_date')
|
to_date = self.env.context.get('to_date')
|
||||||
|
|
||||||
self.env['account.move.line'].check_access_rights('read')
|
real_time_product_ids = [product.id for product in self if product.product_tmpl_id.valuation == 'real_time']
|
||||||
fifo_automated_values = {}
|
if real_time_product_ids:
|
||||||
query = """SELECT aml.product_id, aml.account_id, sum(aml.debit) - sum(aml.credit), sum(quantity), array_agg(aml.id)
|
self.env['account.move.line'].check_access_rights('read')
|
||||||
FROM account_move_line AS aml
|
fifo_automated_values = {}
|
||||||
WHERE aml.product_id IS NOT NULL AND aml.company_id=%%s %s
|
query = """SELECT aml.product_id, aml.account_id, sum(aml.debit) - sum(aml.credit), sum(quantity), array_agg(aml.id)
|
||||||
GROUP BY aml.product_id, aml.account_id"""
|
FROM account_move_line AS aml
|
||||||
params = (self.env.user.company_id.id,)
|
WHERE aml.product_id IN %%s AND aml.company_id=%%s %s
|
||||||
if to_date:
|
GROUP BY aml.product_id, aml.account_id"""
|
||||||
query = query % ('AND aml.date <= %s',)
|
params = (tuple(real_time_product_ids), self.env.user.company_id.id)
|
||||||
params = params + (to_date,)
|
if to_date:
|
||||||
else:
|
query = query % ('AND aml.date <= %s',)
|
||||||
query = query % ('',)
|
params = params + (to_date,)
|
||||||
self.env.cr.execute(query, params=params)
|
else:
|
||||||
|
query = query % ('',)
|
||||||
|
self.env.cr.execute(query, params=params)
|
||||||
|
|
||||||
res = self.env.cr.fetchall()
|
res = self.env.cr.fetchall()
|
||||||
for row in res:
|
for row in res:
|
||||||
fifo_automated_values[(row[0], row[1])] = (row[2], row[3], list(row[4]))
|
fifo_automated_values[(row[0], row[1])] = (row[2], row[3], list(row[4]))
|
||||||
|
|
||||||
|
product_values = {product: 0 for product in self}
|
||||||
|
product_move_ids = {product: [] for product in self}
|
||||||
|
if to_date:
|
||||||
|
domain = [('product_id', 'in', self.ids), ('date', '<=', to_date)] + StockMove._get_all_base_domain()
|
||||||
|
for move in StockMove.search(domain).with_context(prefetch_fields=False):
|
||||||
|
product_values[move.product_id] += move.value
|
||||||
|
product_move_ids[move.product_id].append(move.id)
|
||||||
|
else:
|
||||||
|
domain = [('product_id', 'in', self.ids)] + StockMove._get_all_base_domain()
|
||||||
|
for move in StockMove.search(domain).with_context(prefetch_fields=False):
|
||||||
|
product_values[move.product_id] += move.remaining_value
|
||||||
|
product_move_ids[move.product_id].append(move.id)
|
||||||
|
|
||||||
for product in self:
|
for product in self:
|
||||||
if product.cost_method in ['standard', 'average']:
|
if product.cost_method in ['standard', 'average']:
|
||||||
@@ -214,11 +229,9 @@ class ProductProduct(models.Model):
|
|||||||
elif product.cost_method == 'fifo':
|
elif product.cost_method == 'fifo':
|
||||||
if to_date:
|
if to_date:
|
||||||
if product.product_tmpl_id.valuation == 'manual_periodic':
|
if product.product_tmpl_id.valuation == 'manual_periodic':
|
||||||
domain = [('product_id', '=', product.id), ('date', '<=', to_date)] + StockMove._get_all_base_domain()
|
product.stock_value = product_values[product]
|
||||||
moves = StockMove.search(domain)
|
|
||||||
product.stock_value = sum(moves.mapped('value'))
|
|
||||||
product.qty_at_date = product.with_context(company_owned=True, owner_id=False).qty_available
|
product.qty_at_date = product.with_context(company_owned=True, owner_id=False).qty_available
|
||||||
product.stock_fifo_manual_move_ids = StockMove.browse(moves.ids)
|
product.stock_fifo_manual_move_ids = StockMove.browse(product_move_ids[product])
|
||||||
elif product.product_tmpl_id.valuation == 'real_time':
|
elif product.product_tmpl_id.valuation == 'real_time':
|
||||||
valuation_account_id = product.categ_id.property_stock_valuation_account_id.id
|
valuation_account_id = product.categ_id.property_stock_valuation_account_id.id
|
||||||
value, quantity, aml_ids = fifo_automated_values.get((product.id, valuation_account_id)) or (0, 0, [])
|
value, quantity, aml_ids = fifo_automated_values.get((product.id, valuation_account_id)) or (0, 0, [])
|
||||||
@@ -226,7 +239,7 @@ class ProductProduct(models.Model):
|
|||||||
product.qty_at_date = quantity
|
product.qty_at_date = quantity
|
||||||
product.stock_fifo_real_time_aml_ids = self.env['account.move.line'].browse(aml_ids)
|
product.stock_fifo_real_time_aml_ids = self.env['account.move.line'].browse(aml_ids)
|
||||||
else:
|
else:
|
||||||
product.stock_value, moves = product._sum_remaining_values()
|
product.stock_value, moves = product_values[product], StockMove.browse(product_move_ids[product])
|
||||||
product.qty_at_date = product.with_context(company_owned=True, owner_id=False).qty_available
|
product.qty_at_date = product.with_context(company_owned=True, owner_id=False).qty_available
|
||||||
if product.product_tmpl_id.valuation == 'manual_periodic':
|
if product.product_tmpl_id.valuation == 'manual_periodic':
|
||||||
product.stock_fifo_manual_move_ids = moves
|
product.stock_fifo_manual_move_ids = moves
|
||||||
|
|||||||
@@ -180,8 +180,10 @@ var DateTime = Field.extend({
|
|||||||
operators: [
|
operators: [
|
||||||
{value: "=", text: _lt("is equal to")},
|
{value: "=", text: _lt("is equal to")},
|
||||||
{value: "!=", text: _lt("is not equal to")},
|
{value: "!=", text: _lt("is not equal to")},
|
||||||
{value: ">=", text: _lt("is after")},
|
{value: ">", text: _lt("is after")},
|
||||||
{value: "<=", text: _lt("is before")},
|
{value: "<", text: _lt("is before")},
|
||||||
|
{value: ">=", text: _lt("is after or equal")},
|
||||||
|
{value: "<=", text: _lt("is before or equal")},
|
||||||
{value: "between", text: _lt("is between")},
|
{value: "between", text: _lt("is between")},
|
||||||
{value: "∃", text: _lt("is set")},
|
{value: "∃", text: _lt("is set")},
|
||||||
{value: "∄", text: _lt("is not set")}
|
{value: "∄", text: _lt("is not set")}
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ var map_title ={
|
|||||||
var CrashManager = core.Class.extend({
|
var CrashManager = core.Class.extend({
|
||||||
init: function() {
|
init: function() {
|
||||||
this.active = true;
|
this.active = true;
|
||||||
|
this.isConnected = true;
|
||||||
},
|
},
|
||||||
enable: function () {
|
enable: function () {
|
||||||
this.active = true;
|
this.active = true;
|
||||||
@@ -29,8 +30,29 @@ var CrashManager = core.Class.extend({
|
|||||||
disable: function () {
|
disable: function () {
|
||||||
this.active = false;
|
this.active = false;
|
||||||
},
|
},
|
||||||
rpc_error: function(error) {
|
handleLostConnection: function () {
|
||||||
var self = this;
|
var self = this;
|
||||||
|
if (!this.isConnected) {
|
||||||
|
// already handled, nothing to do. This can happen when several
|
||||||
|
// rpcs are done in parallel and fail because of a lost connection.
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.isConnected = false;
|
||||||
|
var delay = 2000;
|
||||||
|
core.bus.trigger('connection_lost');
|
||||||
|
|
||||||
|
setTimeout(function checkConnection() {
|
||||||
|
ajax.jsonRpc('/web/webclient/version_info', 'call', {}, {shadow:true}).then(function () {
|
||||||
|
core.bus.trigger('connection_restored');
|
||||||
|
self.isConnected = true;
|
||||||
|
}).fail(function () {
|
||||||
|
// exponential backoff, with some jitter
|
||||||
|
delay = (delay * 1.5) + 500*Math.random();
|
||||||
|
setTimeout(checkConnection, delay);
|
||||||
|
});
|
||||||
|
}, delay);
|
||||||
|
},
|
||||||
|
rpc_error: function(error) {
|
||||||
if (!this.active) {
|
if (!this.active) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -38,16 +60,7 @@ var CrashManager = core.Class.extend({
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (error.code === -32098) {
|
if (error.code === -32098) {
|
||||||
core.bus.trigger('connection_lost');
|
this.handleLostConnection();
|
||||||
this.connection_lost = true;
|
|
||||||
var timeinterval = setInterval(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;
|
|
||||||
});
|
|
||||||
}, 2000);
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
var handler = core.crash_registry.get(error.data.name, true);
|
var handler = core.crash_registry.get(error.data.name, true);
|
||||||
|
|||||||
@@ -155,7 +155,6 @@
|
|||||||
|
|
||||||
position: relative;
|
position: relative;
|
||||||
height: @odoo-navbar-height;
|
height: @odoo-navbar-height;
|
||||||
overflow: hidden;
|
|
||||||
|
|
||||||
> ul {
|
> ul {
|
||||||
> li {
|
> li {
|
||||||
@@ -171,6 +170,7 @@
|
|||||||
|
|
||||||
&.o_menu_sections {
|
&.o_menu_sections {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
display: none;
|
||||||
|
|
||||||
> li.open .dropdown-menu {
|
> li.open .dropdown-menu {
|
||||||
position: static;
|
position: static;
|
||||||
@@ -219,6 +219,10 @@
|
|||||||
.o_main_navbar {
|
.o_main_navbar {
|
||||||
height: 100%;
|
height: 100%;
|
||||||
overflow: auto;
|
overflow: auto;
|
||||||
|
|
||||||
|
.o_menu_sections {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -50,11 +50,16 @@ class Web_Editor(http.Controller):
|
|||||||
|
|
||||||
kwargs.update(trans)
|
kwargs.update(trans)
|
||||||
|
|
||||||
record = None
|
content = None
|
||||||
if model and kwargs.get('res_id'):
|
if model:
|
||||||
record = request.env[model].with_context(trans).browse(kwargs.get('res_id'))
|
Model = request.env[model].with_context(trans)
|
||||||
|
if kwargs.get('res_id'):
|
||||||
|
record = Model.browse(kwargs.get('res_id'))
|
||||||
|
content = record and getattr(record, field)
|
||||||
|
else:
|
||||||
|
content = Model.default_get([field]).get(field)
|
||||||
|
|
||||||
kwargs.update(content=record and getattr(record, field) or "")
|
kwargs.update(content=content or '')
|
||||||
|
|
||||||
return request.render(kwargs.get("template") or "web_editor.FieldTextHtml", kwargs, uid=request.uid)
|
return request.render(kwargs.get("template") or "web_editor.FieldTextHtml", kwargs, uid=request.uid)
|
||||||
|
|
||||||
|
|||||||
@@ -986,7 +986,7 @@ class WebsiteSale(http.Controller):
|
|||||||
|
|
||||||
@http.route(['/shop/get_unit_price'], type='json', auth="public", methods=['POST'], website=True)
|
@http.route(['/shop/get_unit_price'], type='json', auth="public", methods=['POST'], website=True)
|
||||||
def get_unit_price(self, product_ids, add_qty, **kw):
|
def get_unit_price(self, product_ids, add_qty, **kw):
|
||||||
products = request.env['product.product'].with_context({'quantity': add_qty}).browse(product_ids)
|
products = request.env['product.product'].with_context(quantity=add_qty).browse(product_ids)
|
||||||
return {product.id: product.website_price / add_qty for product in products}
|
return {product.id: product.website_price / add_qty for product in products}
|
||||||
|
|
||||||
# ------------------------------------------------------
|
# ------------------------------------------------------
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ from odoo import http, _
|
|||||||
from odoo.http import request
|
from odoo.http import request
|
||||||
from odoo.addons.website_sale.controllers.main import WebsiteSale
|
from odoo.addons.website_sale.controllers.main import WebsiteSale
|
||||||
import json
|
import json
|
||||||
|
from collections import OrderedDict
|
||||||
|
|
||||||
|
|
||||||
class WebsiteSaleProductComparison(WebsiteSale):
|
class WebsiteSaleProductComparison(WebsiteSale):
|
||||||
@@ -19,12 +20,12 @@ class WebsiteSaleProductComparison(WebsiteSale):
|
|||||||
|
|
||||||
res = {}
|
res = {}
|
||||||
for num, product in enumerate(products):
|
for num, product in enumerate(products):
|
||||||
for var in product.attribute_line_ids:
|
for var in product.attribute_line_ids.sorted(lambda x: x.attribute_id.sequence):
|
||||||
cat_name = var.attribute_id.category_id.name or _('Uncategorized')
|
cat_name = var.attribute_id.category_id.name or _('Uncategorized')
|
||||||
att_name = var.attribute_id.name
|
att_name = var.attribute_id.name
|
||||||
if not product.attribute_value_ids: # create_variant = False
|
if not product.attribute_value_ids: # create_variant = False
|
||||||
continue
|
continue
|
||||||
res.setdefault(cat_name, {}).setdefault(att_name, [' - '] * len(products))
|
res.setdefault(cat_name, OrderedDict()).setdefault(att_name, [' - '] * len(products))
|
||||||
val = product.attribute_value_ids.filtered(lambda x: x.attribute_id == var.attribute_id)
|
val = product.attribute_value_ids.filtered(lambda x: x.attribute_id == var.attribute_id)
|
||||||
res[cat_name][att_name][num] = val[0].name
|
res[cat_name][att_name][num] = val[0].name
|
||||||
values['specs'] = res
|
values['specs'] = res
|
||||||
|
|||||||
@@ -26,6 +26,6 @@ class ProductTemplate(models.Model):
|
|||||||
|
|
||||||
def get_variant_groups(self):
|
def get_variant_groups(self):
|
||||||
res = OrderedDict()
|
res = OrderedDict()
|
||||||
for var in self.attribute_line_ids:
|
for var in self.attribute_line_ids.sorted(lambda x: x.attribute_id.sequence):
|
||||||
res.setdefault(var.attribute_id.category_id.name or _('Uncategorized'), []).append(var)
|
res.setdefault(var.attribute_id.category_id.name or _('Uncategorized'), []).append(var)
|
||||||
return res
|
return res
|
||||||
|
|||||||
@@ -182,7 +182,7 @@ class WebsiteSlides(http.Controller):
|
|||||||
@http.route('''/slides/slide/<model("slide.slide"):slide>/download''', type='http', auth="public", website=True, sitemap=False)
|
@http.route('''/slides/slide/<model("slide.slide"):slide>/download''', type='http', auth="public", website=True, sitemap=False)
|
||||||
def slide_download(self, slide, **kw):
|
def slide_download(self, slide, **kw):
|
||||||
slide = slide.sudo()
|
slide = slide.sudo()
|
||||||
if slide.download_security == 'public' or (slide.download_security == 'user' and request.session.uid):
|
if slide.download_security == 'public' or (slide.download_security == 'user' and request.env.user and request.env.user != request.website.user_id):
|
||||||
filecontent = base64.b64decode(slide.datas)
|
filecontent = base64.b64decode(slide.datas)
|
||||||
disposition = 'attachment; filename=%s.pdf' % werkzeug.urls.url_quote(slide.name)
|
disposition = 'attachment; filename=%s.pdf' % werkzeug.urls.url_quote(slide.name)
|
||||||
return request.make_response(
|
return request.make_response(
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ from . import test_acl
|
|||||||
from . import test_api
|
from . import test_api
|
||||||
from . import test_base
|
from . import test_base
|
||||||
from . import test_basecase
|
from . import test_basecase
|
||||||
|
from . import test_cache
|
||||||
from . import test_db_cursor
|
from . import test_db_cursor
|
||||||
from . import test_expression
|
from . import test_expression
|
||||||
from . import test_float
|
from . import test_float
|
||||||
|
|||||||
@@ -0,0 +1,145 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||||
|
|
||||||
|
import os
|
||||||
|
import psutil
|
||||||
|
|
||||||
|
from odoo.tests.common import TransactionCase
|
||||||
|
|
||||||
|
|
||||||
|
class TestRecordCache(TransactionCase):
|
||||||
|
|
||||||
|
def test_cache(self):
|
||||||
|
""" Check the record cache object. """
|
||||||
|
Model = self.env['res.partner']
|
||||||
|
name = type(Model).name
|
||||||
|
ref = type(Model).ref
|
||||||
|
|
||||||
|
cache = self.env.cache
|
||||||
|
|
||||||
|
def check1(record, field, value):
|
||||||
|
# value is None means no value in cache
|
||||||
|
self.assertEqual(cache.contains(record, field), value is not None)
|
||||||
|
self.assertEqual(cache.contains_value(record, field), value is not None)
|
||||||
|
self.assertEqual(cache.get_value(record, field), value)
|
||||||
|
try:
|
||||||
|
self.assertEqual(cache.get(record, field), value)
|
||||||
|
self.assertIsNotNone(value)
|
||||||
|
except KeyError:
|
||||||
|
self.assertIsNone(value)
|
||||||
|
self.assertIsNone(cache.get_special(record, field))
|
||||||
|
self.assertEqual(field in cache.get_fields(record), value is not None)
|
||||||
|
self.assertEqual(record in cache.get_records(record, field), value is not None)
|
||||||
|
|
||||||
|
def check(record, name_val, ref_val):
|
||||||
|
""" check the values of fields 'name' and 'ref' on record. """
|
||||||
|
check1(record, name, name_val)
|
||||||
|
check1(record, ref, ref_val)
|
||||||
|
|
||||||
|
foo1, bar1 = Model.browse([1, 2])
|
||||||
|
foo2, bar2 = Model.sudo(self.env.ref('base.user_demo')).browse([1, 2])
|
||||||
|
self.assertNotEqual(foo1.env.uid, foo2.env.uid)
|
||||||
|
|
||||||
|
# cache is empty
|
||||||
|
cache.invalidate()
|
||||||
|
check(foo1, None, None)
|
||||||
|
check(foo2, None, None)
|
||||||
|
check(bar1, None, None)
|
||||||
|
check(bar2, None, None)
|
||||||
|
self.assertCountEqual(cache.get_missing_ids(foo1 + bar1, name), [1, 2])
|
||||||
|
self.assertCountEqual(cache.get_missing_ids(foo2 + bar2, name), [1, 2])
|
||||||
|
|
||||||
|
# set values in one environment only
|
||||||
|
for rec in [foo1, bar1]:
|
||||||
|
cache.set(rec, name, 'NAME1')
|
||||||
|
cache.set(rec, ref, 'REF1')
|
||||||
|
check(foo1, 'NAME1', 'REF1')
|
||||||
|
check(foo2, None, None)
|
||||||
|
check(bar1, 'NAME1', 'REF1')
|
||||||
|
check(bar2, None, None)
|
||||||
|
self.assertCountEqual(cache.get_missing_ids(foo1 + bar1, name), [])
|
||||||
|
self.assertCountEqual(cache.get_missing_ids(foo2 + bar2, name), [1, 2])
|
||||||
|
|
||||||
|
# set values in both environments
|
||||||
|
for rec in [foo2, bar2]:
|
||||||
|
cache.set(rec, name, 'NAME2')
|
||||||
|
cache.set(rec, ref, 'REF2')
|
||||||
|
check(foo1, 'NAME1', 'REF1')
|
||||||
|
check(foo2, 'NAME2', 'REF2')
|
||||||
|
check(bar1, 'NAME1', 'REF1')
|
||||||
|
check(bar2, 'NAME2', 'REF2')
|
||||||
|
self.assertCountEqual(cache.get_missing_ids(foo1 + bar1, name), [])
|
||||||
|
self.assertCountEqual(cache.get_missing_ids(foo2 + bar2, name), [])
|
||||||
|
|
||||||
|
# remove value in one environment
|
||||||
|
cache.remove(foo1, name)
|
||||||
|
check(foo1, None, 'REF1')
|
||||||
|
check(foo2, 'NAME2', 'REF2')
|
||||||
|
check(bar1, 'NAME1', 'REF1')
|
||||||
|
check(bar2, 'NAME2', 'REF2')
|
||||||
|
self.assertCountEqual(cache.get_missing_ids(foo1 + bar1, name), [1])
|
||||||
|
self.assertCountEqual(cache.get_missing_ids(foo2 + bar2, name), [])
|
||||||
|
|
||||||
|
# partial invalidation
|
||||||
|
cache.invalidate([(name, None), (ref, foo1.ids)])
|
||||||
|
check(foo1, None, None)
|
||||||
|
check(foo2, None, None)
|
||||||
|
check(bar1, None, 'REF1')
|
||||||
|
check(bar2, None, 'REF2')
|
||||||
|
|
||||||
|
# total invalidation
|
||||||
|
cache.invalidate()
|
||||||
|
check(foo1, None, None)
|
||||||
|
check(foo2, None, None)
|
||||||
|
check(bar1, None, None)
|
||||||
|
check(bar2, None, None)
|
||||||
|
|
||||||
|
# set a special value
|
||||||
|
cache.set_special(foo1, name, lambda: '42')
|
||||||
|
self.assertTrue(cache.contains(foo1, name))
|
||||||
|
self.assertFalse(cache.contains_value(foo1, name))
|
||||||
|
self.assertEqual(cache.get(foo1, name), '42')
|
||||||
|
self.assertIsNone(cache.get_value(foo1, name))
|
||||||
|
self.assertIsNotNone(cache.get_special(foo1, name))
|
||||||
|
|
||||||
|
# copy cache
|
||||||
|
for rec in [foo1, bar1]:
|
||||||
|
cache.set(rec, name, 'NAME1')
|
||||||
|
cache.set(rec, ref, 'REF1')
|
||||||
|
check(foo1, 'NAME1', 'REF1')
|
||||||
|
check(foo2, None, None)
|
||||||
|
check(bar1, 'NAME1', 'REF1')
|
||||||
|
check(bar2, None, None)
|
||||||
|
|
||||||
|
cache.copy(foo1 + bar1, foo2.env)
|
||||||
|
check(foo1, 'NAME1', 'REF1')
|
||||||
|
check(foo2, 'NAME1', 'REF1')
|
||||||
|
check(bar1, 'NAME1', 'REF1')
|
||||||
|
check(bar2, 'NAME1', 'REF1')
|
||||||
|
|
||||||
|
def test_memory(self):
|
||||||
|
""" Check memory consumption of the cache. """
|
||||||
|
NB_RECORDS = 100000
|
||||||
|
MAX_MEMORY = 100
|
||||||
|
|
||||||
|
cache = self.env.cache
|
||||||
|
model = self.env['res.partner']
|
||||||
|
records = [model.new() for index in range(NB_RECORDS)]
|
||||||
|
|
||||||
|
process = psutil.Process(os.getpid())
|
||||||
|
rss0 = process.memory_info().rss
|
||||||
|
|
||||||
|
char_names = [
|
||||||
|
'name', 'display_name', 'email', 'website', 'phone', 'mobile',
|
||||||
|
'street', 'street2', 'city', 'zip', 'vat', 'ref',
|
||||||
|
]
|
||||||
|
for name in char_names:
|
||||||
|
field = model._fields[name]
|
||||||
|
for record in records:
|
||||||
|
cache.set(record, field, 'test')
|
||||||
|
|
||||||
|
mem_usage = process.memory_info().rss - rss0
|
||||||
|
self.assertLess(
|
||||||
|
mem_usage, MAX_MEMORY * 1024 * 1024,
|
||||||
|
"Caching %s records must take less than %sMB of memory" % (NB_RECORDS, MAX_MEMORY),
|
||||||
|
)
|
||||||
+57
-51
@@ -741,6 +741,7 @@ class Environment(Mapping):
|
|||||||
self.cr, self.uid, self.context = self.args = (cr, uid, frozendict(context))
|
self.cr, self.uid, self.context = self.args = (cr, uid, frozendict(context))
|
||||||
self.registry = Registry(cr.dbname)
|
self.registry = Registry(cr.dbname)
|
||||||
self.cache = envs.cache
|
self.cache = envs.cache
|
||||||
|
self._cache_key = (cr, uid)
|
||||||
self._protected = StackMap() # {field: ids, ...}
|
self._protected = StackMap() # {field: ids, ...}
|
||||||
self.dirty = defaultdict(set) # {record: set(field_name), ...}
|
self.dirty = defaultdict(set) # {record: set(field_name), ...}
|
||||||
self.all = envs
|
self.all = envs
|
||||||
@@ -925,6 +926,12 @@ class Environment(Mapping):
|
|||||||
finally:
|
finally:
|
||||||
self.all.recompute = tmp
|
self.all.recompute = tmp
|
||||||
|
|
||||||
|
def cache_key(self, field):
|
||||||
|
""" Return the key to store the value of ``field`` in cache, the full
|
||||||
|
cache key being ``(key, field, record.id)``.
|
||||||
|
"""
|
||||||
|
return self if field.context_dependent else self._cache_key
|
||||||
|
|
||||||
|
|
||||||
class Environments(object):
|
class Environments(object):
|
||||||
""" A common object for all environments in a request. """
|
""" A common object for all environments in a request. """
|
||||||
@@ -947,52 +954,52 @@ class Environments(object):
|
|||||||
class Cache(object):
|
class Cache(object):
|
||||||
""" Implementation of the cache of records. """
|
""" Implementation of the cache of records. """
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
# {field: {record_id: {key: value}}}
|
# {key: {field: {record_id: value}}}
|
||||||
self._data = defaultdict(lambda: defaultdict(dict))
|
self._data = defaultdict(lambda: defaultdict(dict))
|
||||||
|
|
||||||
def contains(self, record, field):
|
def contains(self, record, field):
|
||||||
""" Return whether ``record`` has a value for ``field``. """
|
""" Return whether ``record`` has a value for ``field``. """
|
||||||
key = field.cache_key(record)
|
key = record.env.cache_key(field)
|
||||||
return key in self._data[field].get(record.id, ())
|
return record.id in self._data[key].get(field, ())
|
||||||
|
|
||||||
def get(self, record, field):
|
def get(self, record, field):
|
||||||
""" Return the value of ``field`` for ``record``. """
|
""" Return the value of ``field`` for ``record``. """
|
||||||
key = field.cache_key(record)
|
key = record.env.cache_key(field)
|
||||||
value = self._data[field][record.id][key]
|
value = self._data[key][field][record.id]
|
||||||
return value.get() if isinstance(value, SpecialValue) else value
|
return value.get() if isinstance(value, SpecialValue) else value
|
||||||
|
|
||||||
def set(self, record, field, value):
|
def set(self, record, field, value):
|
||||||
""" Set the value of ``field`` for ``record``. """
|
""" Set the value of ``field`` for ``record``. """
|
||||||
key = field.cache_key(record)
|
key = record.env.cache_key(field)
|
||||||
self._data[field][record.id][key] = value
|
self._data[key][field][record.id] = value
|
||||||
|
|
||||||
def remove(self, record, field):
|
def remove(self, record, field):
|
||||||
""" Remove the value of ``field`` for ``record``. """
|
""" Remove the value of ``field`` for ``record``. """
|
||||||
key = field.cache_key(record)
|
key = record.env.cache_key(field)
|
||||||
del self._data[field][record.id][key]
|
del self._data[key][field][record.id]
|
||||||
|
|
||||||
def contains_value(self, record, field):
|
def contains_value(self, record, field):
|
||||||
""" Return whether ``record`` has a regular value for ``field``. """
|
""" Return whether ``record`` has a regular value for ``field``. """
|
||||||
key = field.cache_key(record)
|
key = record.env.cache_key(field)
|
||||||
value = self._data[field][record.id].get(key, SpecialValue(None))
|
value = self._data[key][field].get(record.id, SpecialValue(None))
|
||||||
return not isinstance(value, SpecialValue)
|
return not isinstance(value, SpecialValue)
|
||||||
|
|
||||||
def get_value(self, record, field, default=None):
|
def get_value(self, record, field, default=None):
|
||||||
""" Return the regular value of ``field`` for ``record``. """
|
""" Return the regular value of ``field`` for ``record``. """
|
||||||
key = field.cache_key(record)
|
key = record.env.cache_key(field)
|
||||||
value = self._data[field][record.id].get(key, SpecialValue(None))
|
value = self._data[key][field].get(record.id, SpecialValue(None))
|
||||||
return default if isinstance(value, SpecialValue) else value
|
return default if isinstance(value, SpecialValue) else value
|
||||||
|
|
||||||
def get_special(self, record, field, default=None):
|
def get_special(self, record, field, default=None):
|
||||||
""" Return the special value of ``field`` for ``record``. """
|
""" Return the special value of ``field`` for ``record``. """
|
||||||
key = field.cache_key(record)
|
key = record.env.cache_key(field)
|
||||||
value = self._data[field][record.id].get(key)
|
value = self._data[key][field].get(record.id)
|
||||||
return value.get if isinstance(value, SpecialValue) else default
|
return value.get if isinstance(value, SpecialValue) else default
|
||||||
|
|
||||||
def set_special(self, record, field, getter):
|
def set_special(self, record, field, getter):
|
||||||
""" Set the value of ``field`` for ``record`` to return ``getter()``. """
|
""" Set the value of ``field`` for ``record`` to return ``getter()``. """
|
||||||
key = field.cache_key(record)
|
key = record.env.cache_key(field)
|
||||||
self._data[field][record.id][key] = SpecialValue(getter)
|
self._data[key][field][record.id] = SpecialValue(getter)
|
||||||
|
|
||||||
def set_failed(self, records, fields, exception):
|
def set_failed(self, records, fields, exception):
|
||||||
""" Mark ``fields`` on ``records`` with the given exception. """
|
""" Mark ``fields`` on ``records`` with the given exception. """
|
||||||
@@ -1005,67 +1012,66 @@ class Cache(object):
|
|||||||
def get_fields(self, record):
|
def get_fields(self, record):
|
||||||
""" Return the fields with a value for ``record``. """
|
""" Return the fields with a value for ``record``. """
|
||||||
for name, field in record._fields.items():
|
for name, field in record._fields.items():
|
||||||
key = field.cache_key(record)
|
key = record.env.cache_key(field)
|
||||||
if name != 'id' and key in self._data[field].get(record.id, ()):
|
if name != 'id' and record.id in self._data[key].get(field, ()):
|
||||||
yield field
|
yield field
|
||||||
|
|
||||||
def get_records(self, model, field):
|
def get_records(self, model, field):
|
||||||
""" Return the records of ``model`` that have a value for ``field``. """
|
""" Return the records of ``model`` that have a value for ``field``. """
|
||||||
key = field.cache_key(model)
|
key = model.env.cache_key(field)
|
||||||
# optimization: do not field.cache_key(record) for each record in cache
|
ids = list(self._data[key][field])
|
||||||
ids = [
|
|
||||||
record_id
|
|
||||||
for record_id, field_record_cache in self._data[field].items()
|
|
||||||
if key in field_record_cache
|
|
||||||
]
|
|
||||||
return model.browse(ids)
|
return model.browse(ids)
|
||||||
|
|
||||||
def get_missing_ids(self, records, field):
|
def get_missing_ids(self, records, field):
|
||||||
""" Return the ids of ``records`` that have no value for ``field``. """
|
""" Return the ids of ``records`` that have no value for ``field``. """
|
||||||
key = field.cache_key(records)
|
key = records.env.cache_key(field)
|
||||||
field_cache = self._data[field]
|
field_cache = self._data[key][field]
|
||||||
for record_id in records._ids:
|
for record_id in records._ids:
|
||||||
if key not in field_cache.get(record_id, ()):
|
if record_id not in field_cache:
|
||||||
yield record_id
|
yield record_id
|
||||||
|
|
||||||
def copy(self, records, env):
|
def copy(self, records, env):
|
||||||
""" Copy the cache of ``records`` to ``env``. """
|
""" Copy the cache of ``records`` to ``env``. """
|
||||||
src = records
|
src, dst = records.env, env
|
||||||
dst = records.with_env(env)
|
for src_key, dst_key in [(src, dst), (src._cache_key, dst._cache_key)]:
|
||||||
for field, field_cache in self._data.items():
|
if src_key == dst_key:
|
||||||
src_key = field.cache_key(src)
|
break
|
||||||
dst_key = field.cache_key(dst)
|
src_cache = self._data[src_key]
|
||||||
for record_cache in field_cache.values():
|
dst_cache = self._data[dst_key]
|
||||||
if src_key in record_cache and not isinstance(record_cache[src_key], SpecialValue):
|
for field, src_field_cache in src_cache.items():
|
||||||
# But not if it's a SpecialValue, which often is an access error
|
dst_field_cache = dst_cache[field]
|
||||||
# because the other environment (eg. sudo()) is well expected to have access.
|
for record_id, value in src_field_cache.items():
|
||||||
record_cache[dst_key] = record_cache[src_key]
|
if not isinstance(value, SpecialValue):
|
||||||
|
# But not if it's a SpecialValue, which often is an access error
|
||||||
|
# because the other environment (eg. sudo()) is well expected to have access.
|
||||||
|
dst_field_cache[record_id] = value
|
||||||
|
|
||||||
def invalidate(self, spec=None):
|
def invalidate(self, spec=None):
|
||||||
""" Invalidate the cache, partially or totally depending on ``spec``. """
|
""" Invalidate the cache, partially or totally depending on ``spec``. """
|
||||||
if spec is None:
|
if spec is None:
|
||||||
self._data.clear()
|
self._data.clear()
|
||||||
elif spec:
|
elif spec:
|
||||||
data = self._data
|
|
||||||
for field, ids in spec:
|
for field, ids in spec:
|
||||||
if ids is None:
|
if ids is None:
|
||||||
data.pop(field, None)
|
for data in self._data.values():
|
||||||
|
data.pop(field, None)
|
||||||
else:
|
else:
|
||||||
field_cache = data[field]
|
for data in self._data.values():
|
||||||
for id in ids:
|
field_cache = data.get(field)
|
||||||
field_cache.pop(id, None)
|
if field_cache:
|
||||||
|
for id in ids:
|
||||||
|
field_cache.pop(id, None)
|
||||||
|
|
||||||
def check(self, env):
|
def check(self, env):
|
||||||
""" Check the consistency of the cache for the given environment. """
|
""" Check the consistency of the cache for the given environment. """
|
||||||
# make a full copy of the cache, and invalidate it
|
# make a full copy of the cache, and invalidate it
|
||||||
dump = defaultdict(dict)
|
dump = defaultdict(dict)
|
||||||
for field, field_cache in self._data.items():
|
for key in [env, env._cache_key]:
|
||||||
browse = env[field.model_name].browse
|
key_cache = self._data[key]
|
||||||
for record_id, field_record_cache in field_cache.items():
|
for field, field_cache in key_cache.items():
|
||||||
if record_id:
|
for record_id, value in field_cache.items():
|
||||||
key = field.cache_key(browse(record_id))
|
if record_id:
|
||||||
if key in field_record_cache:
|
dump[field][record_id] = value
|
||||||
dump[field][record_id] = field_record_cache[key]
|
|
||||||
|
|
||||||
self.invalidate()
|
self.invalidate()
|
||||||
|
|
||||||
|
|||||||
@@ -769,16 +769,6 @@ class Field(MetaField('DummyField', (object,), {})):
|
|||||||
# Conversion of values
|
# Conversion of values
|
||||||
#
|
#
|
||||||
|
|
||||||
def cache_key(self, record):
|
|
||||||
""" Return the key to get/set the value of ``self`` on ``record`` in
|
|
||||||
cache, the full cache key being ``(self, record.id, key)``.
|
|
||||||
"""
|
|
||||||
env = record.env
|
|
||||||
# IMPORTANT: odoo.api.Cache.get_records() depends on the fact that the
|
|
||||||
# result does not depend on record.id. If you ever make the following
|
|
||||||
# dependent on record.id, don't forget to fix the other method!
|
|
||||||
return env if self.context_dependent else (env.cr, env.uid)
|
|
||||||
|
|
||||||
def null(self, record):
|
def null(self, record):
|
||||||
""" Return the null value for this field in the record format. """
|
""" Return the null value for this field in the record format. """
|
||||||
return False
|
return False
|
||||||
|
|||||||
Reference in New Issue
Block a user