[MERGE] forward port branch 11.0 up to 9a7e3c8b49

This commit is contained in:
Christophe Simonis
2019-04-08 17:45:00 +02:00
27 changed files with 242 additions and 46 deletions
+19 -9
View File
@@ -99,6 +99,11 @@ class AccountInvoice(models.Model):
def _get_reference_type(self): def _get_reference_type(self):
return [('none', _('Free Reference'))] return [('none', _('Free Reference'))]
def _get_aml_for_amount_residual(self):
""" Get the aml to consider to compute the amount residual of invoices """
self.ensure_one()
return self.sudo().move_id.line_ids.filtered(lambda l: l.account_id == self.account_id)
@api.one @api.one
@api.depends( @api.depends(
'state', 'currency_id', 'invoice_line_ids.price_subtotal', 'state', 'currency_id', 'invoice_line_ids.price_subtotal',
@@ -108,14 +113,13 @@ class AccountInvoice(models.Model):
residual = 0.0 residual = 0.0
residual_company_signed = 0.0 residual_company_signed = 0.0
sign = self.type in ['in_refund', 'out_refund'] and -1 or 1 sign = self.type in ['in_refund', 'out_refund'] and -1 or 1
for line in self.sudo().move_id.line_ids: for line in self._get_aml_for_amount_residual():
if line.account_id == self.account_id: residual_company_signed += line.amount_residual
residual_company_signed += line.amount_residual if line.currency_id == self.currency_id:
if line.currency_id == self.currency_id: residual += line.amount_residual_currency if line.currency_id else line.amount_residual
residual += line.amount_residual_currency if line.currency_id else line.amount_residual else:
else: from_currency = (line.currency_id and line.currency_id.with_context(date=line.date)) or line.company_id.currency_id.with_context(date=line.date)
from_currency = (line.currency_id and line.currency_id.with_context(date=line.date)) or line.company_id.currency_id.with_context(date=line.date) residual += from_currency.compute(line.amount_residual, self.currency_id)
residual += from_currency.compute(line.amount_residual, self.currency_id)
self.residual_company_signed = abs(residual_company_signed) * sign self.residual_company_signed = abs(residual_company_signed) * sign
self.residual_signed = abs(residual) * sign self.residual_signed = abs(residual) * sign
self.residual = abs(residual) self.residual = abs(residual)
@@ -929,12 +933,18 @@ class AccountInvoice(models.Model):
tax_grouped[key]['base'] += round_curr(val['base']) tax_grouped[key]['base'] += round_curr(val['base'])
return tax_grouped return tax_grouped
@api.multi
def _get_aml_for_register_payment(self):
""" Get the aml to consider to reconcile in register payment """
self.ensure_one()
return self.move_id.line_ids.filtered(lambda r: not r.reconciled and r.account_id.internal_type in ('payable', 'receivable'))
@api.multi @api.multi
def register_payment(self, payment_line, writeoff_acc_id=False, writeoff_journal_id=False): def register_payment(self, payment_line, writeoff_acc_id=False, writeoff_journal_id=False):
""" Reconcile payable/receivable lines from the invoice with payment_line """ """ Reconcile payable/receivable lines from the invoice with payment_line """
line_to_reconcile = self.env['account.move.line'] line_to_reconcile = self.env['account.move.line']
for inv in self: for inv in self:
line_to_reconcile += inv.move_id.line_ids.filtered(lambda r: not r.reconciled and r.account_id.internal_type in ('payable', 'receivable')) line_to_reconcile += inv._get_aml_for_register_payment()
return (line_to_reconcile + payment_line).reconcile(writeoff_acc_id, writeoff_journal_id) return (line_to_reconcile + payment_line).reconcile(writeoff_acc_id, writeoff_journal_id)
@api.multi @api.multi
@@ -682,7 +682,7 @@ class AccountReconciliation(models.AbstractModel):
liquidity_amt_clause = currency and '%(amount)s::numeric' or 'abs(%(amount)s::numeric)' liquidity_amt_clause = currency and '%(amount)s::numeric' or 'abs(%(amount)s::numeric)'
sql_query = st_line._get_common_sql_query(excluded_ids=excluded_ids) + \ sql_query = st_line._get_common_sql_query(excluded_ids=excluded_ids) + \
" AND (" + field + " = %(amount)s::numeric OR (acc.internal_type = 'liquidity' AND " + liquidity_field + " = " + liquidity_amt_clause + ")) \ " AND (" + field + " = %(amount)s::numeric OR (acc.internal_type = 'liquidity' AND " + liquidity_field + " = " + liquidity_amt_clause + ")) \
ORDER BY date_maturity desc, aml.id desc LIMIT 1" ORDER BY date_maturity asc, aml.id desc LIMIT 1"
self.env.cr.execute(sql_query, params) self.env.cr.execute(sql_query, params)
results = self.env.cr.fetchone() results = self.env.cr.fetchone()
if results: if results:
+2 -1
View File
@@ -1199,6 +1199,7 @@
<field name="invoice_id" attrs="{'invisible': [('invoice_id','=',False)]}"/> <field name="invoice_id" attrs="{'invisible': [('invoice_id','=',False)]}"/>
</group> </group>
<group string="Dates"> <group string="Dates">
<field name="date"/>
<field name="date_maturity"/> <field name="date_maturity"/>
</group> </group>
@@ -1206,7 +1207,7 @@
<field name="tax_line_id" readonly="1" attrs="{'invisible': [('tax_line_id','=',False)]}"/> <field name="tax_line_id" readonly="1" attrs="{'invisible': [('tax_line_id','=',False)]}"/>
<field name="tax_ids" widget="many2many_tags" readonly="1" attrs="{'invisible': [('tax_ids','=',[])]}"/> <field name="tax_ids" widget="many2many_tags" readonly="1" attrs="{'invisible': [('tax_ids','=',[])]}"/>
</group> </group>
<group string="Matching" attrs="{'invisible':[('full_reconcile_id','=',False)]}"> <group string="Matching" attrs="{'invisible':[('full_reconcile_id','=',False), ('matched_debit_ids', '=', []),('matched_credit_ids', '=', [])]}">
<label for="full_reconcile_id"/> <label for="full_reconcile_id"/>
<div> <div>
<field name="full_reconcile_id"/> <field name="full_reconcile_id"/>
@@ -178,7 +178,7 @@
<field name="salvage_value" widget="monetary" options="{'currency_field': 'currency_id'}" attrs="{'invisible': [('type','=','sale')]}"/> <field name="salvage_value" widget="monetary" options="{'currency_field': 'currency_id'}" attrs="{'invisible': [('type','=','sale')]}"/>
<field name="value_residual" widget="monetary" options="{'currency_field': 'currency_id'}"/> <field name="value_residual" widget="monetary" options="{'currency_field': 'currency_id'}"/>
<field name="partner_id" string="Vendor" domain="[('supplier', '=', True)]"/> <field name="partner_id" string="Vendor" domain="[('supplier', '=', True)]"/>
<field name="invoice_id" string="Invoice" options="{'no_create': True}"/> <field name="invoice_id" string="Invoice" attrs="{'readonly': [('state', '!=', 'draft')]}" options="{'no_create': True}"/>
</group> </group>
</group> </group>
<notebook colspan="4"> <notebook colspan="4">
@@ -71,7 +71,7 @@
<h4 class="mb24">Pay with</h4> <h4 class="mb24">Pay with</h4>
<t t-call="payment.payment_tokens_list"> <t t-call="payment.payment_tokens_list">
<t t-set="mode" t-value="'payment'"/> <t t-set="mode" t-value="'payment'"/>
<t t-set="partner_id" t-value="invoice.partner_id.id"/> <t t-set="partner_id" t-value="request.env.user.partner_id.id"/>
<t t-set="success_url" t-value="'/my/invoices/%s%s' % (invoice.id, ('?access_token=%s' % (access_token)) if access_token else '')"/> <t t-set="success_url" t-value="'/my/invoices/%s%s' % (invoice.id, ('?access_token=%s' % (access_token)) if access_token else '')"/>
<t t-set="error_url" t-value="'/my/invoices/%s%s' % (invoice.id, ('?access_token=%s' % (access_token)) if access_token else '')"/> <t t-set="error_url" t-value="'/my/invoices/%s%s' % (invoice.id, ('?access_token=%s' % (access_token)) if access_token else '')"/>
<t t-set="access_token" t-value="access_token or ''"/> <t t-set="access_token" t-value="access_token or ''"/>
@@ -31,13 +31,13 @@ class Partner(models.Model):
replacement_xml = """ replacement_xml = """
<div> <div>
<field name="country_enforce_cities" invisible="1"/> <field name="country_enforce_cities" invisible="1"/>
<field name='city' placeholder="%(placeholder)s" <field name='city' placeholder="%(placeholder)s" class="o_address_city"
attrs="{ attrs="{
'invisible': [('country_enforce_cities', '=', True), ('city_id', '!=', False)], 'invisible': [('country_enforce_cities', '=', True), ('city_id', '!=', False)],
'readonly': [('type', '=', 'contact')%(parent_condition)s] 'readonly': [('type', '=', 'contact')%(parent_condition)s]
}" }"
/> />
<field name='city_id' placeholder="%(placeholder)s" string="%(placeholder)s" <field name='city_id' placeholder="%(placeholder)s" string="%(placeholder)s" class="o_address_city"
context="{'default_country_id': country_id, context="{'default_country_id': country_id,
'default_name': city, 'default_name': city,
'default_zipcode': zip, 'default_zipcode': zip,
+5 -1
View File
@@ -164,7 +164,7 @@ def is_multilang_url(local_url, langs=None):
local_url = '/'.join(spath) local_url = '/'.join(spath)
try: try:
# Try to match an endpoint in werkzeug's routing table # Try to match an endpoint in werkzeug's routing table
url = local_url.split('?') url = local_url.partition('#')[0].split('?')
path = url[0] path = url[0]
query_string = url[1] if len(url) > 1 else None query_string = url[1] if len(url) > 1 else None
router = request.httprequest.app.get_db_router(request.db).bind('') router = request.httprequest.app.get_db_router(request.db).bind('')
@@ -173,6 +173,10 @@ def is_multilang_url(local_url, langs=None):
return (func.routing.get('website', False) and return (func.routing.get('website', False) and
func.routing.get('multilang', func.routing['type'] == 'http')) func.routing.get('multilang', func.routing['type'] == 'http'))
except werkzeug.exceptions.NotFound: except werkzeug.exceptions.NotFound:
# Consider /static/ files as non-multilang
static_index = path.find('/static/', 1)
if static_index != -1 and static_index == path.find('/', 1):
return False
return True return True
except Exception as e: except Exception as e:
return False return False
+1 -1
View File
@@ -256,7 +256,7 @@
<field name="type_tax_use">sale</field> <field name="type_tax_use">sale</field>
<field name="refund_account_id" ref="chart22030200"/> <field name="refund_account_id" ref="chart22030200"/>
<field name="account_id" ref="chart22030200"/> <field name="account_id" ref="chart22030200"/>
<field name="tag_ids" eval="[ref('l10n_pl.tag_pl_08'), ref('l10n_pl.tag_pl_29')]"/> <field name="tag_ids" eval="[(6,0,[ref('l10n_pl.tag_pl_08'), ref('l10n_pl.tag_pl_29')])]"/>
<field name="tax_group_id" ref="tax_group_vat_7"/> <field name="tax_group_id" ref="tax_group_vat_7"/>
</record> </record>
@@ -62,7 +62,8 @@ class MailComposeMessage(models.TransientModel):
if mail_values.get('email_to'): if mail_values.get('email_to'):
recips = tools.email_split(mail_values['email_to']) recips = tools.email_split(mail_values['email_to'])
else: else:
recips = tools.email_split(partners_email.get(res_id)) partner_id = (mail_values.get('recipient_ids') or [(False, '')])[0][1]
recips = tools.email_split(partners_email.get(partner_id))
mail_to = recips[0].lower() if recips else False mail_to = recips[0].lower() if recips else False
if (blacklist and mail_to in blacklist) or (seen_list and mail_to in seen_list): if (blacklist and mail_to in blacklist) or (seen_list and mail_to in seen_list):
# prevent sending to blocked addresses that were included by mistake # prevent sending to blocked addresses that were included by mistake
+7 -1
View File
@@ -506,7 +506,13 @@ class PosOrder(models.Model):
'|', '|',
('credit_move_id.move_id', '=', move.id), ('credit_move_id.move_id', '=', move.id),
('debit_move_id.move_id', '=', move.id)], limit=1) ('debit_move_id.move_id', '=', move.id)], limit=1)
partial_reconcile.create_tax_cash_basis_entry(cash_basis_percentage_before_rec[move]) if partial_reconcile:
# In case none of the order debit move lines have been reconciled
# there is no need to create the tax cash basis entries as nothing has been reconciled
# a known case is when the the bank journal credit account is set to a receivable account,
# which has as effect to fully reconcile the payment line with its counterpart,
# leaving no payment lines to reconcile with the order debit lines.
partial_reconcile.create_tax_cash_basis_entry(cash_basis_percentage_before_rec[move])
def _filtered_for_reconciliation(self): def _filtered_for_reconciliation(self):
filter_states = ['invoiced', 'done'] filter_states = ['invoiced', 'done']
@@ -742,9 +742,6 @@ var Chrome = PosBaseWidget.extend(AbstractAction.prototype, {
if(err.message === 'XmlHttpRequestError '){ if(err.message === 'XmlHttpRequestError '){
title = 'Network Failure (XmlHttpRequestError)'; title = 'Network Failure (XmlHttpRequestError)';
body = 'The Point of Sale could not be loaded due to a network problem.\n Please check your internet connection.'; body = 'The Point of Sale could not be loaded due to a network problem.\n Please check your internet connection.';
}else if(err.message === 'TLSError'){
title = 'Https connection to IoT Box failed';
body = 'Make sure you are using IoT Box v18.10 or higher.\n\n Navigate to ' + err.url + ' to accept the certificate of your IoT Box.';
}else if(err.code === 200){ }else if(err.code === 200){
title = err.data.message; title = err.data.message;
body = err.data.debug; body = err.data.debug;
+10 -5
View File
@@ -135,11 +135,16 @@ exports.PosModel = Backbone.Model.extend({
done.resolve(); done.resolve();
}, },
function(statusText, url){ function(statusText, url){
if (statusText == 'error' && window.location.protocol == 'https:') { var show_loading_error = (self.gui.current_screen === null);
var error = {message: 'TLSError', url: url}; done.resolve();
self.chrome.loading_error(error); if (show_loading_error && statusText == 'error' && window.location.protocol == 'https:') {
} else { self.gui.show_popup('alert', {
done.resolve(); title: _t('HTTPS connection to IoT Box failed'),
body: _.str.sprintf(
_t('Make sure you are using IoT Box v18.12 or higher. Navigate to %s to accept the certificate of your IoT Box.'),
url
),
});
} }
}); });
return done; return done;
+5 -1
View File
@@ -12,6 +12,7 @@ from odoo.addons import decimal_precision as dp
from odoo.tools import float_compare, pycompat from odoo.tools import float_compare, pycompat
class ProductCategory(models.Model): class ProductCategory(models.Model):
_name = "product.category" _name = "product.category"
_description = "Product Category" _description = "Product Category"
@@ -508,7 +509,10 @@ class ProductProduct(models.Model):
precision = self.env['decimal.precision'].precision_get('Product Unit of Measure') precision = self.env['decimal.precision'].precision_get('Product Unit of Measure')
res = self.env['product.supplierinfo'] res = self.env['product.supplierinfo']
for seller in self._prepare_sellers(params): sellers = self._prepare_sellers(params)
if self.env.context.get('force_company'):
sellers = sellers.filtered(lambda s: not s.company_id or s.company_id.id == self.env.context['force_company'])
for seller in sellers:
# Set quantity in UoM of seller # Set quantity in UoM of seller
quantity_uom_seller = quantity quantity_uom_seller = quantity
if quantity_uom_seller and uom_id and uom_id != seller.product_uom: if quantity_uom_seller and uom_id and uom_id != seller.product_uom:
+2 -1
View File
@@ -237,8 +237,9 @@
<group name="pricing" string="Pricing"> <group name="pricing" string="Pricing">
<field name="product_variant_count" invisible="1"/> <field name="product_variant_count" invisible="1"/>
<field name="lst_price" widget='monetary' options="{'currency_field': 'currency_id', 'field_digits': True}" attrs="{'readonly': [('product_variant_count', '&gt;', 1)]}"/> <field name="lst_price" widget='monetary' options="{'currency_field': 'currency_id', 'field_digits': True}" attrs="{'readonly': [('product_variant_count', '&gt;', 1)]}"/>
<field name="standard_price" widget='monetary' options="{'currency_field': 'currency_id', 'field_digits': True}"/> <field name="standard_price" widget='monetary' options="{'currency_field': 'cost_currency_id', 'field_digits': True}"/>
<field name="currency_id" invisible='1'/> <field name="currency_id" invisible='1'/>
<field name="cost_currency_id" invisible="1"/>
</group> </group>
<group name="weight"> <group name="weight">
<group name="weight" string="Weights" attrs="{'invisible':[('type', 'not in', ['product', 'consu'])]}"> <group name="weight" string="Weights" attrs="{'invisible':[('type', 'not in', ['product', 'consu'])]}">
+2 -2
View File
@@ -999,7 +999,7 @@ class ProcurementRule(models.Model):
def _get_purchase_order_date(self, product_id, product_qty, product_uom, values, partner, schedule_date): def _get_purchase_order_date(self, product_id, product_qty, product_uom, values, partner, schedule_date):
"""Return the datetime value to use as Order Date (``date_order``) for the """Return the datetime value to use as Order Date (``date_order``) for the
Purchase Order created to satisfy the given procurement. """ Purchase Order created to satisfy the given procurement. """
seller = product_id._select_seller( seller = product_id.with_context(force_company=values['company_id'].id)._select_seller(
partner_id=partner, partner_id=partner,
quantity=product_qty, quantity=product_qty,
date=fields.Date.to_string(schedule_date), date=fields.Date.to_string(schedule_date),
@@ -1009,7 +1009,7 @@ class ProcurementRule(models.Model):
def _update_purchase_order_line(self, product_id, product_qty, product_uom, values, line, partner): def _update_purchase_order_line(self, product_id, product_qty, product_uom, values, line, partner):
procurement_uom_po_qty = product_uom._compute_quantity(product_qty, product_id.uom_po_id) procurement_uom_po_qty = product_uom._compute_quantity(product_qty, product_id.uom_po_id)
seller = product_id._select_seller( seller = product_id.with_context(force_company=values['company_id'].id)._select_seller(
partner_id=partner, partner_id=partner,
quantity=line.product_qty + procurement_uom_po_qty, quantity=line.product_qty + procurement_uom_po_qty,
date=line.order_id.date_order and line.order_id.date_order[:10], date=line.order_id.date_order and line.order_id.date_order[:10],
+6 -6
View File
@@ -3,7 +3,7 @@
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 UserError, ValidationError
from odoo.tools import float_utils from odoo.tools import float_utils
@@ -148,13 +148,13 @@ class Inventory(models.Model):
if self.filter == 'none' and self.product_id and self.location_id and self.lot_id: if self.filter == 'none' and self.product_id and self.location_id and self.lot_id:
return return
if self.filter not in ('product', 'product_owner') and self.product_id: if self.filter not in ('product', 'product_owner') and self.product_id:
raise UserError(_('The selected inventory options are not coherent.')) raise ValidationError(_('The selected inventory options are not coherent.'))
if self.filter != 'lot' and self.lot_id: if self.filter != 'lot' and self.lot_id:
raise UserError(_('The selected inventory options are not coherent.')) raise ValidationError(_('The selected inventory options are not coherent.'))
if self.filter not in ('owner', 'product_owner') and self.partner_id: if self.filter not in ('owner', 'product_owner') and self.partner_id:
raise UserError(_('The selected inventory options are not coherent.')) raise ValidationError(_('The selected inventory options are not coherent.'))
if self.filter != 'pack' and self.package_id: if self.filter != 'pack' and self.package_id:
raise UserError(_('The selected inventory options are not coherent.')) raise ValidationError(_('The selected inventory options are not coherent.'))
def action_reset_product_qty(self): def action_reset_product_qty(self):
self.mapped('line_ids').write({'product_qty': 0}) self.mapped('line_ids').write({'product_qty': 0})
@@ -387,7 +387,7 @@ class InventoryLine(models.Model):
""" """
for line in self: for line in self:
if line.product_id.type != 'product': if line.product_id.type != 'product':
raise UserError(_("You can only adjust stockable products.") + '\n\n%s -> %s' % (line.product_id.display_name, line.product_id.type)) raise ValidationError(_("You can only adjust stockable products.") + '\n\n%s -> %s' % (line.product_id.display_name, line.product_id.type))
def _get_quants(self): def _get_quants(self):
return self.env['stock.quant'].search([ return self.env['stock.quant'].search([
@@ -22,7 +22,7 @@ class StockImmediateTransfer(models.TransientModel):
picking.action_assign() picking.action_assign()
if picking.state != 'assigned': if picking.state != 'assigned':
raise UserError(_("Could not reserve all requested products. Please use the \'Mark as Todo\' button to handle the reservation manually.")) raise UserError(_("Could not reserve all requested products. Please use the \'Mark as Todo\' button to handle the reservation manually."))
for move in picking.move_lines: for move in picking.move_lines.filtered(lambda m: m.state not in ['done', 'cancel']):
for move_line in move.move_line_ids: for move_line in move.move_line_ids:
move_line.qty_done = move_line.product_uom_qty move_line.qty_done = move_line.product_uom_qty
if picking._check_backorder(): if picking._check_backorder():
+1 -1
View File
@@ -303,7 +303,7 @@ class StockMove(models.Model):
if qty_to_take_on_candidates == 0: if qty_to_take_on_candidates == 0:
move.write({ move.write({
'value': -tmp_value if not quantity else move.value or -tmp_value, # outgoing move are valued negatively 'value': -tmp_value if not quantity else move.value or -tmp_value, # outgoing move are valued negatively
'price_unit': -tmp_value / move.product_qty, 'price_unit': -tmp_value / (move.product_qty or quantity),
}) })
elif qty_to_take_on_candidates > 0: elif qty_to_take_on_candidates > 0:
last_fifo_price = new_standard_price or move.product_id.standard_price last_fifo_price = new_standard_price or move.product_id.standard_price
+1 -1
View File
@@ -97,7 +97,7 @@ if(!the_form.length) {
} }
// checkboxes and radios // checkboxes and radios
the_form.find("input[name^=" + key + "][type!='text']").each(function(){ the_form.find("input[name=" + key + "][type!='text']").each(function(){
$(this).val(value); $(this).val(value);
}); });
}); });
@@ -883,7 +883,10 @@ eventHandler.attach = function (oLayoutInfo, options) {
show_tooltip = true; show_tooltip = true;
setTimeout(function () { setTimeout(function () {
if (!show_tooltip) return; // Do not show tooltip on double-click and if there is already one
if (!show_tooltip || $target.attr('title') !== undefined) {
return;
}
$target.tooltip({title: _t('Double-click to edit'), trigger: 'manuel', container: 'body'}).tooltip('show'); $target.tooltip({title: _t('Double-click to edit'), trigger: 'manuel', container: 'body'}).tooltip('show');
setTimeout(function () { setTimeout(function () {
$target.tooltip('destroy'); $target.tooltip('destroy');
@@ -900,7 +900,7 @@ var VideoWidget = MediaWidget.extend({
options = options || {}; options = options || {};
// Video url patterns(youtube, instagram, vimeo, dailymotion, youku, ...) // Video url patterns(youtube, instagram, vimeo, dailymotion, youku, ...)
var ytRegExp = /^(?:(?:https?:)?\/\/)?(?:www\.)?(?:youtu\.be\/|youtube\.com\/(?:embed\/|v\/|watch\?v=|watch\?.+&v=))((\w|-){11})(?:\S+)?$/; var ytRegExp = /^(?:(?:https?:)?\/\/)?(?:www\.)?(?:youtu\.be\/|youtube(-nocookie)?\.com\/(?:embed\/|v\/|watch\?v=|watch\?.+&v=))((?:\w|-){11})(?:\S+)?$/;
var ytMatch = url.match(ytRegExp); var ytMatch = url.match(ytRegExp);
var insRegExp = /(.*)instagram.com\/p\/(.[a-zA-Z0-9]*)/; var insRegExp = /(.*)instagram.com\/p\/(.[a-zA-Z0-9]*)/;
@@ -927,8 +927,8 @@ var VideoWidget = MediaWidget.extend({
var autoplay = options.autoplay ? '?autoplay=1' : '?autoplay=0'; var autoplay = options.autoplay ? '?autoplay=1' : '?autoplay=0';
if (ytMatch && ytMatch[1].length === 11) { if (ytMatch && ytMatch[2].length === 11) {
$video.attr('src', '//www.youtube.com/embed/' + ytMatch[1] + autoplay); $video.attr('src', '//www.youtube' + (ytMatch[1] || '') + '.com/embed/' + ytMatch[2] + autoplay);
} else if (insMatch && insMatch[2].length) { } else if (insMatch && insMatch[2].length) {
$video.attr('src', '//www.instagram.com/p/' + insMatch[2] + '/embed/'); $video.attr('src', '//www.instagram.com/p/' + insMatch[2] + '/embed/');
videoType = 'ins'; videoType = 'ins';
+8 -1
View File
@@ -4,6 +4,7 @@ import re
from collections import OrderedDict from collections import OrderedDict
from odoo import models from odoo import models
from odoo.addons.http_routing.models.ir_http import url_for
from odoo.http import request from odoo.http import request
@@ -37,10 +38,16 @@ class QWeb(models.AbstractModel):
if not website and options.get('website_id'): if not website and options.get('website_id'):
website = self.env['website'].browse(options['website_id']) website = self.env['website'].browse(options['website_id'])
if not website or not website.cdn_activated: if not website:
return atts return atts
name = self.URL_ATTRS.get(tagName) name = self.URL_ATTRS.get(tagName)
if request and name and name in atts:
atts[name] = url_for(atts[name])
if not website.cdn_activated:
return atts
if name and name in atts: if name and name in atts:
atts = OrderedDict(atts) atts = OrderedDict(atts)
atts[name] = website.get_cdn_url(atts[name]) atts[name] = website.get_cdn_url(atts[name])
+122
View File
@@ -2,7 +2,9 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details. # Part of Odoo. See LICENSE file for full copyright and licensing details.
import re import re
import werkzeug
import odoo
from odoo import tools from odoo import tools
from odoo.modules.module import get_module_resource from odoo.modules.module import get_module_resource
from odoo.tests.common import TransactionCase from odoo.tests.common import TransactionCase
@@ -63,3 +65,123 @@ class TestQweb(TransactionCase):
"css": attachments[1].url, "css": attachments[1].url,
"user_id": demo.id, "user_id": demo.id,
}).encode('utf8')) }).encode('utf8'))
class MockObject(object):
_log_call = []
def __init__(self, *args, **kwargs):
self.__dict__ = kwargs
def __call__(self, *args, **kwargs):
self._log_call.append((args, kwargs))
return self
def __getitem__(self, index):
return self
def werkzeugRaiseNotFound(*args, **kwargs):
raise werkzeug.exceptions.NotFound()
class MockRequest(object):
""" Class with context manager mocking odoo.http.request for tests """
def __init__(self, env, website=None, context=None, multilang=True, routing=True):
app = MockObject(routing={
'type': 'http',
'website': True,
'multilang': multilang,
})
app.get_db_router = app.bind = app.match = app
if not routing:
app.match = werkzeugRaiseNotFound
self.request = MockObject(
env=env, context=context or {}, db=None, debug=False,
website=website, httprequest=MockObject(
path='/hello/',
app=app
)
)
odoo.http._request_stack.push(self.request)
def __enter__(self):
return self.request
def __exit__(self, exc_type, exc_value, traceback):
odoo.http._request_stack.pop()
class TestQwebProcessAtt(TransactionCase):
def setUp(self):
super(TestQwebProcessAtt, self).setUp()
self.website = self.env['website'].browse(1)
self.website.language_ids = self.env.ref('base.lang_en') + self.env.ref('base.lang_fr')
self.website.default_lang_id = self.env.ref('base.lang_en')
self.website.cdn_activated = True
self.website.cdn_url = "http://test.cdn"
self.website.cdn_filters = "\n".join(["^(/[a-z]{2}_[A-Z]{2})?/a$", "^/b$"])
def _test_att(self, url, expect, tag='a', attribute='href'):
self.assertEqual(
self.env['ir.qweb']._post_processing_att(tag, {attribute: url}, {}),
expect
)
def test_process_att_no_request(self):
# no request so no URL rewriting
self._test_att('/', {'href': '/'})
self._test_att('/en_US/', {'href': '/en_US/'})
self._test_att('/fr_FR/', {'href': '/fr_FR/'})
# no URL rewritting for CDN
self._test_att('/a', {'href': '/a'})
def test_process_att_no_website(self):
with MockRequest(self.env) as request:
# no website so URL rewriting
self._test_att('/', {'href': '/'})
self._test_att('/en_US/', {'href': '/en_US/'})
self._test_att('/fr_FR/', {'href': '/fr_FR/'})
# no URL rewritting for CDN
self._test_att('/a', {'href': '/a'})
def test_process_att_monolang_route(self):
with MockRequest(self.env, website=self.website, multilang=False) as request:
# lang not changed in URL but CDN enabled
self._test_att('/a', {'href': 'http://test.cdn/a'})
self._test_att('/en_US/a', {'href': 'http://test.cdn/en_US/a'})
self._test_att('/b', {'href': 'http://test.cdn/b'})
self._test_att('/en_US/b', {'href': '/en_US/b'})
def test_process_att_no_request_lang(self):
with MockRequest(self.env, self.website) as request:
self._test_att('/', {'href': '/'})
self._test_att('/en_US/', {'href': '/'})
self._test_att('/fr_FR/', {'href': '/fr_FR/'})
def test_process_att_with_request_lang(self):
with MockRequest(self.env, self.website, context={'lang': 'fr_FR'}) as request:
self._test_att('/', {'href': '/fr_FR/'})
self._test_att('/en_US/', {'href': '/'})
self._test_att('/fr_FR/', {'href': '/fr_FR/'})
def test_process_att_matching_cdn_and_lang(self):
with MockRequest(self.env, self.website) as request:
# lang prefix is added before CDN
self._test_att('/a', {'href': 'http://test.cdn/a'})
self._test_att('/en_US/a', {'href': 'http://test.cdn/a'})
self._test_att('/fr_FR/a', {'href': 'http://test.cdn/fr_FR/a'})
self._test_att('/b', {'href': 'http://test.cdn/b'})
self._test_att('/en_US/b', {'href': 'http://test.cdn/b'})
self._test_att('/fr_FR/b', {'href': '/fr_FR/b'})
def test_process_att_no_route(self):
with MockRequest(self.env, self.website, context={'lang': 'fr_FR'}, routing=False) as request:
# default on multilang=True if route is not /{module}/static/
self._test_att('/web/static/hi', {'href': '/web/static/hi'})
self._test_att('/my-page', {'href': '/fr_FR/my-page'})
def test_process_att_url_crap(self):
with MockRequest(self.env, self.website) as request:
# #{fragment} is stripped from URL when testing route
self._test_att('/x#y?z', {'href': '/x#y?z'})
self.assertEqual(
request.httprequest.app._log_call[-1],
(('/x',), {'method': 'POST', 'query_args': None})
)
self._test_att('/x?y#z', {'href': '/x?y#z'})
self.assertEqual(
request.httprequest.app._log_call[-1],
(('/x',), {'method': 'POST', 'query_args': 'y'})
)
+11
View File
@@ -0,0 +1,11 @@
Switzerland, 2019-03-25
I hereby agree to the terms of the Odoo Individual Contributor License
Agreement v1.0.
I declare that I am authorized and able to make this agreement and sign this
declaration.
Signed,
Bernard Letourmy bernard.letourmy@gmail.com https://github.com/bletourmy
+11
View File
@@ -0,0 +1,11 @@
USA, 2019-04-04
I hereby agree to the terms of the Odoo Individual Contributor License
Agreement v1.0.
I declare that I am authorized and able to make this agreement and sign this
declaration.
Signed,
Matthew Watkins mwatkins.bulk@gmail.com https://github.com/matthewdwatkins
+9
View File
@@ -21,6 +21,7 @@ from docutils.core import publish_string
from docutils.transforms import Transform, writer_aux from docutils.transforms import Transform, writer_aux
from docutils.writers.html4css1 import Writer from docutils.writers.html4css1 import Writer
import lxml.html import lxml.html
import psycopg2
import odoo import odoo
from odoo import api, fields, models, modules, tools, _ from odoo import api, fields, models, modules, tools, _
@@ -528,6 +529,14 @@ class Module(models.Model):
@api.multi @api.multi
def _button_immediate_function(self, function): def _button_immediate_function(self, function):
try:
# This is done because the installation/uninstallation/upgrade can modify a currently
# running cron job and prevent it from finishing, and since the ir_cron table is locked
# during execution, the lock won't be released until timeout.
self._cr.execute("SELECT * FROM ir_cron FOR UPDATE NOWAIT")
except psycopg2.OperationalError:
raise UserError(_("The server is busy right now, module operations are not possible at"
" this time, please try again later."))
function(self) function(self)
self._cr.commit() self._cr.commit()
+5 -1
View File
@@ -922,7 +922,11 @@ class Field(MetaField('DummyField', (object,), {})):
""" """
indexname = '%s_%s_index' % (model._table, self.name) indexname = '%s_%s_index' % (model._table, self.name)
if self.index: if self.index:
sql.create_index(model._cr, indexname, model._table, ['"%s"' % self.name]) try:
with model._cr.savepoint():
sql.create_index(model._cr, indexname, model._table, ['"%s"' % self.name])
except psycopg2.OperationalError:
_schema.error("Unable to add index for %s", self)
else: else:
sql.drop_index(model._cr, indexname, model._table) sql.drop_index(model._cr, indexname, model._table)