[MERGE] forward port branch 11.0 up to 9a7e3c8b49
This commit is contained in:
@@ -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:
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
@@ -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', '>', 1)]}"/>
|
<field name="lst_price" widget='monetary' options="{'currency_field': 'currency_id', 'field_digits': True}" attrs="{'readonly': [('product_variant_count', '>', 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'])]}">
|
||||||
|
|||||||
@@ -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],
|
||||||
|
|||||||
@@ -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():
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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';
|
||||||
|
|||||||
@@ -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])
|
||||||
|
|||||||
@@ -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'})
|
||||||
|
)
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -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
|
||||||
@@ -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
@@ -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)
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user