[MERGE] Forward port of saas-12.2 to saas-12.3 up to cf8e74a1a7
closes odoo/odoo#34907 Signed-off-by: Martin Trigaux (mat) <mat@odoo.com>
This commit is contained in:
@@ -440,7 +440,7 @@ class AccountInvoice(models.Model):
|
||||
vendor_display_name = _('From: ') + invoice.source_email
|
||||
invoice.invoice_icon = '@'
|
||||
else:
|
||||
vendor_display_name = ('Created by: ') + invoice.sudo().create_uid.name
|
||||
vendor_display_name = _('Created by: %s') % invoice.sudo().create_uid.name
|
||||
invoice.invoice_icon = '#'
|
||||
invoice.vendor_display_name = vendor_display_name
|
||||
|
||||
@@ -1635,6 +1635,8 @@ class AccountInvoice(models.Model):
|
||||
|
||||
values['type'] = TYPE2REFUND[invoice['type']]
|
||||
values['date_invoice'] = date_invoice or fields.Date.context_today(invoice)
|
||||
if values.get('date_due', False) and values['date_invoice'] > values['date_due']:
|
||||
values['date_due'] = values['date_invoice']
|
||||
values['state'] = 'draft'
|
||||
values['number'] = False
|
||||
values['origin'] = invoice.number
|
||||
|
||||
@@ -451,6 +451,7 @@ class ResPartner(models.Model):
|
||||
return {'domain': {'property_account_position_id': [('company_id', 'in', [company.id, False])]}}
|
||||
|
||||
def can_edit_vat(self):
|
||||
''' Can't edit `vat` if there is (non draft) issued invoices. '''
|
||||
can_edit_vat = super(ResPartner, self).can_edit_vat()
|
||||
if not can_edit_vat:
|
||||
return can_edit_vat
|
||||
|
||||
@@ -38,7 +38,7 @@
|
||||
<template id="portal_invoice_payment" name="Invoice Payment">
|
||||
<div class="row" t-if="not tx_ids and invoice.state == 'open' and invoice.amount_total" id="portal_pay">
|
||||
<div class="modal fade" id="pay_with" role="dialog">
|
||||
<div class="modal-dialog modal-content">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h3 class="modal-title">Pay with</h3>
|
||||
|
||||
@@ -353,14 +353,19 @@ class AccountVoucher(models.Model):
|
||||
temp['amount_currency'] = company_cur._convert(tax_vals['amount'], current_cur, line.company_id, self.account_date or fields.Date.today(), round=True)
|
||||
self.env['account.move.line'].create(temp)
|
||||
|
||||
self.env['account.move.line'].create(move_line)
|
||||
# When global rounding is activated, we must wait until all tax lines are computed to
|
||||
# merge them.
|
||||
if tax_calculation_rounding_method == 'round_globally':
|
||||
# _apply_taxes modifies the dict move_line in place to account for included/excluded taxes
|
||||
tax_lines_vals += self.env['account.move.line'].with_context(round=False)._apply_taxes(
|
||||
move_line,
|
||||
move_line.get('debit', 0.0) - move_line.get('credit', 0.0)
|
||||
)
|
||||
# rounding False means the move_line's amount are not rounded
|
||||
currency = self.env['res.currency'].browse(company_currency)
|
||||
move_line['debit'] = currency.round(move_line['debit'])
|
||||
move_line['credit'] = currency.round(move_line['credit'])
|
||||
self.env['account.move.line'].create(move_line)
|
||||
|
||||
# When round globally is set, we merge the tax lines
|
||||
if tax_calculation_rounding_method == 'round_globally':
|
||||
|
||||
@@ -17,6 +17,7 @@ class TimesheetAttendance(models.Model):
|
||||
|
||||
@api.model_cr
|
||||
def init(self):
|
||||
self._cr.execute("DROP VIEW IF EXISTS hr_timesheet_attendance_report")
|
||||
self._cr.execute("""CREATE OR REPLACE VIEW %s AS (
|
||||
SELECT
|
||||
max(id) AS id,
|
||||
@@ -31,7 +32,7 @@ class TimesheetAttendance(models.Model):
|
||||
resource_resource.user_id AS user_id,
|
||||
hr_attendance.worked_hours AS attendance,
|
||||
NULL AS timesheet,
|
||||
date_trunc('day', hr_attendance.check_in) AS date
|
||||
hr_attendance.check_in::date AS date
|
||||
FROM hr_attendance
|
||||
LEFT JOIN hr_employee ON hr_employee.id = hr_attendance.employee_id
|
||||
LEFT JOIN resource_resource on resource_resource.id = hr_employee.resource_id
|
||||
@@ -41,7 +42,7 @@ class TimesheetAttendance(models.Model):
|
||||
ts.user_id AS user_id,
|
||||
NULL AS attendance,
|
||||
ts.unit_amount AS timesheet,
|
||||
date_trunc('day', ts.date) AS date
|
||||
ts.date AS date
|
||||
FROM account_analytic_line AS ts
|
||||
WHERE ts.project_id IS NOT NULL
|
||||
) AS t
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
<field name="name">HR timesheet attendance report: Pivot</field>
|
||||
<field name="model">hr.timesheet.attendance.report</field>
|
||||
<field name="arch" type="xml">
|
||||
<pivot string="timesheet attendance">
|
||||
<pivot string="timesheet attendance" disable_linking="True">
|
||||
<field name="user_id" type="row"/>
|
||||
<field name="date" interval="day" type="col"/>
|
||||
<field name="total_difference" type="measure" widget="float_time"/>
|
||||
|
||||
@@ -55,7 +55,7 @@ def get_token():
|
||||
return read_file_first_line('token')
|
||||
|
||||
def get_version():
|
||||
return '19_04'
|
||||
return '19_07'
|
||||
|
||||
#----------------------------------------------------------
|
||||
# Controllers
|
||||
|
||||
@@ -91,7 +91,7 @@
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="heading">Version</td>
|
||||
<td>V19.04 <a class="btn btn-sm float-right" href='/hw_proxy/upgrade/'>update</a></td>
|
||||
<td>V19.07 <a class="btn btn-sm float-right" href='/hw_proxy/upgrade/'>update</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="heading">IP Address</td>
|
||||
|
||||
@@ -3,7 +3,8 @@
|
||||
<data noupdate="1">
|
||||
<!-- add VAT, codice fiscal and tax system for main company -->
|
||||
<record id="base.main_company" model="res.company">
|
||||
<field name="vat">IT00410123456</field>
|
||||
<field name="country_id" ref="base.it"/>
|
||||
<field name="vat">IT00410128888</field>
|
||||
<field name="l10n_it_codice_fiscale">0123456789987654</field>
|
||||
<field name="l10n_it_tax_system">RF01</field>
|
||||
<field name="zip">12345</field>
|
||||
|
||||
@@ -702,6 +702,10 @@ class MailThread(models.AbstractModel):
|
||||
params['token'] = token
|
||||
|
||||
link = '%s?%s' % (base_link, url_encode(params))
|
||||
|
||||
if self and hasattr(self, 'get_base_url'):
|
||||
link = self[0].get_base_url() + link
|
||||
|
||||
return link
|
||||
|
||||
@api.multi
|
||||
|
||||
@@ -22,7 +22,7 @@ class MassMailController(http.Controller):
|
||||
model_display = mailing.mailing_model_id.display_name
|
||||
blacklist_entry._message_log(description + " ({})".format(model_display))
|
||||
|
||||
@http.route(['/unsubscribe_from_list'], type='http', website=True, multilang=False, auth='public')
|
||||
@http.route(['/unsubscribe_from_list'], type='http', website=True, multilang=False, auth='public', sitemap=False)
|
||||
def unsubscribe_placeholder_link(self, **post):
|
||||
"""Dummy route so placeholder is not prefixed by language, MUST have multilang=False"""
|
||||
raise werkzeug.exceptions.NotFound()
|
||||
|
||||
@@ -557,12 +557,13 @@ class MrpProduction(models.Model):
|
||||
move[0]._recompute_state()
|
||||
move[0]._action_assign()
|
||||
move[0].unit_factor = quantity / move[0].raw_material_production_id.product_qty
|
||||
elif quantity < 0: # Do not remove 0 lines
|
||||
return move[0], old_qty, quantity
|
||||
else:
|
||||
if move[0].quantity_done > 0:
|
||||
raise UserError(_('Lines need to be deleted, but can not as you still have some quantities to consume in them. '))
|
||||
move[0]._action_cancel()
|
||||
move[0].unlink()
|
||||
return move[0], old_qty, quantity
|
||||
return self.env['stock.move'], old_qty, quantity
|
||||
else:
|
||||
move_values = self._get_move_raw_values(bom_line, line_data)
|
||||
move = self.env['stock.move'].create(move_values)
|
||||
@@ -848,15 +849,14 @@ class MrpProduction(models.Model):
|
||||
raise UserError(_('Work order %s is still running') % wo.name)
|
||||
self._check_lots()
|
||||
|
||||
# Cancel unfinished move
|
||||
move_to_cancel = self.move_raw_ids.filtered(lambda m: m.state not in ('done', 'cancel') and float_is_zero(m.quantity_done, precision_rounding=m.product_uom.rounding))
|
||||
move_to_cancel._action_cancel()
|
||||
|
||||
self.post_inventory()
|
||||
# Moves without quantity done are not posted => set them as done instead of canceling. In
|
||||
# case the user edits the MO later on and sets some consumed quantity on those, we do not
|
||||
# want the move lines to be canceled.
|
||||
(self.move_raw_ids | self.move_finished_ids).filtered(lambda x: x.state not in ('done', 'cancel')).write({'state': 'done'})
|
||||
(self.move_raw_ids | self.move_finished_ids).filtered(lambda x: x.state not in ('done', 'cancel')).write({
|
||||
'state': 'done',
|
||||
'product_uom_qty': 0.0,
|
||||
})
|
||||
self.write({'date_finished': fields.Datetime.now()})
|
||||
return True
|
||||
|
||||
|
||||
@@ -518,6 +518,51 @@ class TestMrpOrder(TestMrpCommon):
|
||||
self.assertEqual(mo.move_raw_ids.filtered(lambda m: m.product_id == p1).quantity_done, 20, 'Update the produce quantity should not impact already produced quantity.')
|
||||
mo.button_mark_done()
|
||||
|
||||
def test_product_produce_6(self):
|
||||
""" Plan 5 finished products, reserve and produce 3. Post the current production.
|
||||
Simulate an unlock and edit and, on the opened moves, set the consumed quantity
|
||||
to 3. Now, try to update the quantity to produce to 3. It should fail since there
|
||||
are consumed quantities. Unlock and edit, remove the consumed quantities and
|
||||
update the quantity to produce to 3."""
|
||||
self.stock_location = self.env.ref('stock.stock_location_stock')
|
||||
mo, bom, p_final, p1, p2 = self.generate_mo()
|
||||
self.assertEqual(len(mo), 1, 'MO should have been created')
|
||||
|
||||
self.env['stock.quant']._update_available_quantity(p1, self.stock_location, 20)
|
||||
|
||||
self.env['stock.quant']._update_available_quantity(p2, self.stock_location, 5)
|
||||
mo.action_assign()
|
||||
|
||||
produce_wizard = self.env['mrp.product.produce'].with_context({
|
||||
'active_id': mo.id,
|
||||
'active_ids': [mo.id],
|
||||
}).create({
|
||||
'product_qty': 3.0,
|
||||
})
|
||||
produce_wizard._onchange_product_qty()
|
||||
produce_wizard.do_produce()
|
||||
|
||||
mo.post_inventory()
|
||||
self.assertEqual(len(mo.move_raw_ids), 4)
|
||||
|
||||
mo.move_raw_ids.filtered(lambda m: m.state != 'done')[0].quantity_done = 3
|
||||
|
||||
update_quantity_wizard = self.env['change.production.qty'].create({
|
||||
'mo_id': mo.id,
|
||||
'product_qty': 3,
|
||||
})
|
||||
with self.assertRaises(UserError):
|
||||
update_quantity_wizard.change_prod_qty()
|
||||
|
||||
mo.move_raw_ids.filtered(lambda m: m.state != 'done')[0].quantity_done = 0
|
||||
update_quantity_wizard.change_prod_qty()
|
||||
|
||||
self.assertEqual(len(mo.move_raw_ids), 2)
|
||||
|
||||
mo.button_mark_done()
|
||||
self.assertTrue(all(s == 'done' for s in mo.move_raw_ids.mapped('state')))
|
||||
self.assertEqual(sum(mo.move_raw_ids.mapped('move_line_ids.product_uom_qty')), 0)
|
||||
|
||||
def test_product_produce_6(self):
|
||||
""" Checks that, for a BOM with two components, when creating a manufacturing order for one
|
||||
finished products and without reserving, the produce wizards proposes the corrects lines
|
||||
|
||||
@@ -60,15 +60,22 @@ class ChangeProductionQty(models.TransientModel):
|
||||
if line.child_bom_id and line.child_bom_id.type == 'phantom' or\
|
||||
line.product_id.type not in ['product', 'consu']:
|
||||
continue
|
||||
move, old_qty, new_qty = production._update_raw_move(line, line_data)
|
||||
move = production.move_raw_ids.filtered(lambda x: x.bom_line_id.id == line.id and x.state not in ('done', 'cancel'))
|
||||
if move:
|
||||
old_qty = move[0].product_uom_qty
|
||||
else:
|
||||
old_qty = 0
|
||||
iterate_key = production._get_document_iterate_key(move)
|
||||
if iterate_key:
|
||||
document = self.env['stock.picking']._log_activity_get_documents({move: (new_qty, old_qty)}, iterate_key, 'UP')
|
||||
document = self.env['stock.picking']._log_activity_get_documents({move: (line_data['qty'], old_qty)}, iterate_key, 'UP')
|
||||
for key, value in document.items():
|
||||
if documents.get(key):
|
||||
documents[key] += [value]
|
||||
else:
|
||||
documents[key] = [value]
|
||||
|
||||
production._update_raw_move(line, line_data)
|
||||
|
||||
production._log_manufacture_exception(documents)
|
||||
operation_bom_qty = {}
|
||||
for bom, bom_data in boms:
|
||||
|
||||
@@ -272,7 +272,7 @@ class WebsitePayment(http.Controller):
|
||||
tx_id = int(kw.get('tx_id', 0))
|
||||
if tx_id:
|
||||
tx = request.env['payment.transaction'].browse(tx_id)
|
||||
if tx.state == 'done':
|
||||
if tx.state in ['done', 'authorized']:
|
||||
status = 'success'
|
||||
message = tx.acquirer_id.done_msg
|
||||
elif tx.state == 'pending':
|
||||
|
||||
@@ -5,4 +5,5 @@ from . import account_invoice
|
||||
from . import res_partner
|
||||
from . import account_payment
|
||||
from . import chart_template
|
||||
from . import ir_http
|
||||
from . import res_company
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo import models
|
||||
from odoo.osv import expression
|
||||
|
||||
|
||||
class IrHttp(models.AbstractModel):
|
||||
_inherit = 'ir.http'
|
||||
|
||||
@classmethod
|
||||
def _get_translation_frontend_modules_domain(cls):
|
||||
domain = super(IrHttp, cls)._get_translation_frontend_modules_domain()
|
||||
return expression.OR([domain, [('name', '=', 'payment')]])
|
||||
@@ -259,7 +259,7 @@ class PosConfig(models.Model):
|
||||
" the Accounting application."))
|
||||
if self.invoice_journal_id.currency_id and self.invoice_journal_id.currency_id != self.currency_id:
|
||||
raise ValidationError(_("The invoice journal must be in the same currency as the Sales Journal or the company currency if that is not set."))
|
||||
if any(self.journal_ids.mapped(lambda journal: journal.currency_id and journal.currency_id != self.currency_id)):
|
||||
if any(self.journal_ids.mapped(lambda journal: self.currency_id not in (journal.company_id.currency_id, journal.currency_id))):
|
||||
raise ValidationError(_("All payment methods must be in the same currency as the Sales Journal or the company currency if that is not set."))
|
||||
|
||||
@api.constrains('company_id', 'available_pricelist_ids')
|
||||
|
||||
@@ -215,7 +215,7 @@ class PosSession(models.Model):
|
||||
'journal_id': journal.id,
|
||||
'user_id': self.env.user.id,
|
||||
'name': pos_name,
|
||||
'balance_start': self.env["account.bank.statement"]._get_opening_balance(journal.id)
|
||||
'balance_start': self.env["account.bank.statement"]._get_opening_balance(journal.id) if journal.type == 'cash' else 0
|
||||
}
|
||||
|
||||
statements.append(ABS.with_context(ctx).sudo(uid).create(st_values).id)
|
||||
|
||||
+41
@@ -0,0 +1,41 @@
|
||||
#!/bin/bash
|
||||
### BEGIN INIT INFO
|
||||
# Provides: timesyncd
|
||||
# Required-Start: $remote_fs $network
|
||||
# Required-Stop: $remote_fs
|
||||
# Default-Start: 2 3 4 5
|
||||
# Default-Stop: 0 1 6
|
||||
# Short-Description: Start systemd-timesyncd daemon at boot time
|
||||
# Description: Enable service provided by daemon.
|
||||
### END INIT INFO
|
||||
|
||||
start()
|
||||
{
|
||||
echo "start timesyncd"
|
||||
service systemd-timesyncd start
|
||||
}
|
||||
|
||||
stop()
|
||||
{
|
||||
echo "stop timesyncd"
|
||||
service systemd-timesyncd stop
|
||||
}
|
||||
|
||||
restart()
|
||||
{
|
||||
stop;
|
||||
sleep 1;
|
||||
start;
|
||||
}
|
||||
case $1 in
|
||||
start)
|
||||
start;;
|
||||
stop)
|
||||
stop;;
|
||||
restart)
|
||||
restart;;
|
||||
*)
|
||||
start;;
|
||||
esac
|
||||
|
||||
exit 0
|
||||
@@ -138,6 +138,7 @@ echo "* * * * * rm /var/run/odoo/sessions/*" | crontab -
|
||||
update-rc.d -f hostapd remove
|
||||
update-rc.d -f nginx remove
|
||||
update-rc.d -f dnsmasq remove
|
||||
update-rc.d timesyncd defaults
|
||||
|
||||
systemctl daemon-reload
|
||||
systemctl enable ramdisks.service
|
||||
|
||||
@@ -13,7 +13,7 @@ class MailThread(models.AbstractModel):
|
||||
_mail_post_token_field = 'access_token' # token field for external posts, to be overridden
|
||||
|
||||
website_message_ids = fields.One2many('mail.message', 'res_id', string='Website Messages',
|
||||
domain=lambda self: [('model', '=', self._name), ('message_type', '=', 'comment')], auto_join=True,
|
||||
domain=lambda self: [('model', '=', self._name), '|', ('message_type', '=', 'comment'), ('message_type', '=', 'email')], auto_join=True,
|
||||
help="Website communication history")
|
||||
|
||||
@api.multi
|
||||
|
||||
@@ -8,4 +8,7 @@ class ResPartner(models.Model):
|
||||
_inherit = 'res.partner'
|
||||
|
||||
def can_edit_vat(self):
|
||||
return True
|
||||
''' `vat` is a commercial field, synced between the parent (commercial
|
||||
entity) and the children. Only the commercial entity should be able to
|
||||
edit it (as in backend). '''
|
||||
return not self.parent_id
|
||||
|
||||
@@ -374,8 +374,11 @@
|
||||
<div class="clearfix" />
|
||||
<div t-attf-class="form-group #{error.get('company_name') and 'o_has_error' or ''} col-xl-6">
|
||||
<label class="col-form-label label-optional" for="company_name">Company Name</label>
|
||||
<p t-if="partner.commercial_partner_id.is_company" t-attf-class="form-control #{error.get('company_name') and 'is-invalid' or ''}" t-esc="partner.commercial_company_name"/>
|
||||
<input t-else="" type="text" name="company_name" t-attf-class="form-control #{error.get('company_name') and 'is-invalid' or ''}" t-att-value="company_name or partner.company_name" t-att-readonly="None if partner.can_edit_vat() else '1'" t-att-title="None if partner.can_edit_vat() else 'Changing company name is not allowed once document(s) have been issued for your account. Please contact us directly for this operation.'"/>
|
||||
<!-- The <input> is replace by a <p> to avoid sending an unauthorized value on form submit.
|
||||
The user might not have rights to change company_name but should still be able to see it.
|
||||
-->
|
||||
<p t-if="not partner.can_edit_vat()" t-attf-class="form-control" readonly="1" t-esc="partner.commercial_company_name" title="Changing company name is not allowed once document(s) have been issued for your account. Please contact us directly for this operation."/>
|
||||
<input t-else="" type="text" name="company_name" t-attf-class="form-control #{error.get('company_name') and 'is-invalid' or ''}" t-att-value="company_name or partner.commercial_company_name"/>
|
||||
</div>
|
||||
<div t-attf-class="form-group #{error.get('vat') and 'o_has_error' or ''} col-xl-6">
|
||||
<label class="col-form-label label-optional" for="vat">VAT Number</label>
|
||||
|
||||
@@ -34,7 +34,7 @@ class Partner(models.Model):
|
||||
# update at each change country, and so erase old pricelist
|
||||
if self.property_product_pricelist or (actual and default_for_country and default_for_country.id != actual.id):
|
||||
# keep the company of the current user before sudo
|
||||
self.env['ir.property'].with_context(force_company=self.env.user.company_id.id).sudo().set_multi(
|
||||
self.env['ir.property'].with_context(force_company=self._context.get('force_company', self.env.user.company_id.id)).sudo().set_multi(
|
||||
'property_product_pricelist',
|
||||
self._name,
|
||||
{self.id: self.property_product_pricelist or default_for_country.id},
|
||||
|
||||
@@ -398,6 +398,7 @@
|
||||
<field name="type">ir.actions.act_window</field>
|
||||
<field name="res_model">purchase.order</field>
|
||||
<field name="view_mode">tree,kanban,form,pivot,graph,calendar,activity</field>
|
||||
<field name="domain">[]</field>
|
||||
<field name="search_view_id" ref="view_purchase_order_filter"/>
|
||||
<field name="help" type="html">
|
||||
<p class="o_view_nocontent_smiling_face">
|
||||
|
||||
@@ -30,6 +30,7 @@ class ResPartner(models.Model):
|
||||
partner = partner.parent_id
|
||||
|
||||
def can_edit_vat(self):
|
||||
''' Can't edit `vat` if there is (non draft) issued SO. '''
|
||||
can_edit_vat = super(ResPartner, self).can_edit_vat()
|
||||
if not can_edit_vat:
|
||||
return can_edit_vat
|
||||
|
||||
@@ -209,9 +209,9 @@ class SaleOrder(models.Model):
|
||||
def _get_reward_line_values(self, program):
|
||||
self.ensure_one()
|
||||
if program.reward_type == 'discount':
|
||||
return self._get_reward_values_discount(program)
|
||||
return self._get_reward_values_discount(program.with_context(lang=self.partner_id.lang))
|
||||
elif program.reward_type == 'product':
|
||||
return [self._get_reward_values_product(program)]
|
||||
return [self._get_reward_values_product(program.with_context(lang=self.partner_id.lang))]
|
||||
|
||||
def _create_reward_line(self, program):
|
||||
self.write({'order_line': [(0, False, value) for value in self._get_reward_line_values(program)]})
|
||||
|
||||
@@ -499,7 +499,8 @@ class ProductTemplate(models.Model):
|
||||
|
||||
@api.depends(
|
||||
'product_variant_ids',
|
||||
'product_variant_ids.stock_quant_ids',
|
||||
'product_variant_ids.stock_move_ids.product_qty',
|
||||
'product_variant_ids.stock_move_ids.state',
|
||||
)
|
||||
def _compute_quantities(self):
|
||||
res = self._compute_quantities_dict()
|
||||
|
||||
@@ -93,7 +93,7 @@
|
||||
<td t-if="has_serial_number and move_line.lot_name" groups="stock.group_lot_on_delivery_slip">
|
||||
<span t-field="move_line.lot_name"/>
|
||||
</td>
|
||||
<td t-else="" groups="stock.group_lot_on_delivery_slip">
|
||||
<td t-elif="has_serial_number" groups="stock.group_lot_on_delivery_slip">
|
||||
<span t-field="move_line.lot_id.name"/>
|
||||
</td>
|
||||
<td class="text-center">
|
||||
|
||||
@@ -181,6 +181,8 @@
|
||||
}
|
||||
// checkboxes in editable lists should be clickable and activate the row
|
||||
.o_data_row:not(.o_selected_row) .o_data_cell {
|
||||
&.o_toggle_button_cell:not(.o_readonly_modifier) button, // registry: toggle_button
|
||||
&.o_boolean_favorite_cell:not(.o_readonly_modifier) .o_favorite, // registry: boolean_favorite
|
||||
.custom-checkbox:not(.o_readonly_modifier) {
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
@@ -1,13 +1,10 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
from odoo.tests.common import tagged
|
||||
import unittest
|
||||
|
||||
from odoo.tests.common import BaseCase
|
||||
from ..controllers import main
|
||||
|
||||
|
||||
@tagged('standard', 'at_install')
|
||||
class ActionMungerTest(unittest.TestCase):
|
||||
class ActionMungerTest(BaseCase):
|
||||
def test_actual_treeview(self):
|
||||
action = {
|
||||
"views": [[False, "tree"], [False, "form"],
|
||||
|
||||
@@ -2,9 +2,8 @@
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
import random
|
||||
import unittest
|
||||
|
||||
from odoo.tests.common import tagged
|
||||
from odoo.tests.common import BaseCase
|
||||
from odoo.tools import topological_sort
|
||||
|
||||
|
||||
@@ -14,8 +13,7 @@ def sample(population):
|
||||
random.randint(0, min(len(population), 5)))
|
||||
|
||||
|
||||
@tagged('standard', 'at_install')
|
||||
class TestModulesLoading(unittest.TestCase):
|
||||
class TestModulesLoading(BaseCase):
|
||||
def setUp(self):
|
||||
self.mods = [str(i) for i in range(1000)]
|
||||
|
||||
|
||||
@@ -1885,6 +1885,9 @@ var HelperPlugin = AbstractPlugin.extend({
|
||||
// Create empty text node to have a range into the node
|
||||
if (range.sc.tagName && !dom.isVoid(range.sc) && !range.sc.childNodes[range.so]) {
|
||||
$(range.sc).append(invisible);
|
||||
if (invisible.previousSibling && invisible.previousSibling.tagName === "BR") {
|
||||
invisible.previousSibling.remove();
|
||||
}
|
||||
range = this.context.invoke('editor.setRange', invisible, 0);
|
||||
}
|
||||
|
||||
|
||||
@@ -780,7 +780,10 @@ var TextPlugin = AbstractPlugin.extend({
|
||||
clipboardData = e.originalEvent.clipboardData.getData('text/plain');
|
||||
// get that text as an array of text nodes separated by <br> where needed
|
||||
var allNewlines = /\n/g;
|
||||
clipboardData = $('<p>' + clipboardData.replace(allNewlines, '<br>') + '</p>').contents().toArray();
|
||||
clipboardData = _.str.trim(clipboardData)
|
||||
.replace(/</g, '<').replace(/>/g, '>')
|
||||
.replace(allNewlines, '<br>');
|
||||
clipboardData = $('<p>' + clipboardData + '</p>').contents().toArray();
|
||||
}
|
||||
|
||||
// Delete selection
|
||||
|
||||
@@ -814,6 +814,18 @@ var keyboardTestsChar = [{
|
||||
start: "b:contents(0)->1",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "'a' in empty li",
|
||||
content: '<ul><li><p><br></p></li></ul>',
|
||||
steps: [{
|
||||
start: "p->1",
|
||||
key: 'a',
|
||||
}],
|
||||
test: {
|
||||
content: '<ul><li><p>a</p></li></ul>',
|
||||
start: "p:contents(0)->1",
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
QUnit.test('Char', function (assert) {
|
||||
@@ -2248,6 +2260,25 @@ var keyboardTestsComplex = [{
|
||||
start: "p:eq(1)->0",
|
||||
},
|
||||
},
|
||||
// List indent
|
||||
{
|
||||
name: "ENTER -> TAB -> a' at end of li > p (split -> indent -> write)",
|
||||
content: '<ul><li><p>a</p></li></ul>',
|
||||
steps: [{
|
||||
start: "p:contents()[0]->1",
|
||||
key: 'ENTER',
|
||||
},
|
||||
{
|
||||
key: 'TAB',
|
||||
},
|
||||
{
|
||||
key: 'a',
|
||||
}],
|
||||
test: {
|
||||
content: '<ul><li><p>a</p></li><li class="o_indent"><ul><li><p>a</p></li></ul></li></ul>',
|
||||
start: "p:eq(1):contents()[0]->1",
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
QUnit.test('Complex', function (assert) {
|
||||
|
||||
@@ -2,17 +2,16 @@
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
import textwrap
|
||||
import unittest
|
||||
|
||||
from lxml import etree, html
|
||||
from lxml.builder import E
|
||||
|
||||
from odoo.tests import common
|
||||
from odoo.tests.common import BaseCase
|
||||
from odoo.addons.web_editor.models.ir_qweb import html_to_text
|
||||
|
||||
|
||||
@common.tagged('standard', 'at_install')
|
||||
class TestHTMLToText(unittest.TestCase):
|
||||
class TestHTMLToText(BaseCase):
|
||||
def test_rawstring(self):
|
||||
self.assertEqual(
|
||||
"foobar",
|
||||
|
||||
@@ -22,8 +22,7 @@ class ResConfigSettings(models.TransientModel):
|
||||
language_count = fields.Integer(string='Number of languages', compute='_compute_language_count', readonly=True)
|
||||
website_default_lang_id = fields.Many2one(
|
||||
string='Default language', related='website_id.default_lang_id', readonly=False,
|
||||
relation='res.lang', required=False,
|
||||
oldname='default_lang_id')
|
||||
relation='res.lang', oldname='default_lang_id')
|
||||
website_default_lang_code = fields.Char(
|
||||
'Default language code', related='website_id.default_lang_code', readonly=False,
|
||||
oldname='default_lang_code')
|
||||
|
||||
@@ -1,13 +1,11 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
import unittest
|
||||
from odoo.addons.http_routing.models.ir_http import slugify, unslug
|
||||
from odoo.tests.common import tagged
|
||||
from odoo.tests.common import BaseCase
|
||||
|
||||
|
||||
@tagged('standard', 'at_install')
|
||||
class TestUnslug(unittest.TestCase):
|
||||
class TestUnslug(BaseCase):
|
||||
|
||||
def test_unslug(self):
|
||||
tests = {
|
||||
@@ -28,9 +26,7 @@ class TestUnslug(unittest.TestCase):
|
||||
for slug, expected in tests.items():
|
||||
self.assertEqual(unslug(slug), expected)
|
||||
|
||||
|
||||
@tagged('standard', 'at_install')
|
||||
class TestTitleToSlug(unittest.TestCase):
|
||||
class TestTitleToSlug(BaseCase):
|
||||
"""
|
||||
Those tests should pass with or without python-slugify
|
||||
See website/models/website.py slugify method
|
||||
|
||||
@@ -69,7 +69,7 @@
|
||||
</div>
|
||||
<div class="content-group">
|
||||
<div class="mt16">
|
||||
<field name="language_ids" widget="many2many_tags" options="{'no_create': True, 'no_open': True}"/>
|
||||
<field name="language_ids" widget="many2many_tags" options="{'no_create': True, 'no_open': True}" attrs="{'required': [('website_id', '!=', False)]}"/>
|
||||
</div>
|
||||
<field name="language_count" invisible="1"/>
|
||||
<div class="mt8" attrs="{'invisible':[('language_count', '<', 2)]}">
|
||||
|
||||
@@ -23,7 +23,10 @@ publicWidget.registry.js_get_posts = publicWidget.Widget.extend({
|
||||
this.$target.empty(); // Compatibility with db that saved content inside by mistake
|
||||
this.$target.attr('contenteditable', 'False'); // Prevent user edition
|
||||
|
||||
var domain = [['website_published', '=', true]];
|
||||
var domain = [
|
||||
['website_published', '=', true],
|
||||
['post_date', '<=', moment().utc().locale('en').format('YYYY-MM-DD HH:mm:ss')],
|
||||
];
|
||||
if (blogID) {
|
||||
domain.push(['blog_id', '=', parseInt(blogID)]);
|
||||
}
|
||||
|
||||
@@ -227,7 +227,9 @@ publicWidget.registry.crmPartnerAssign = publicWidget.Widget.extend({
|
||||
_onEditOppConfirm: function (ev) {
|
||||
ev.preventDefault();
|
||||
ev.stopPropagation();
|
||||
this._buttonExec($(ev.currentTarget), this._editOpportunity);
|
||||
if ($(".edit_opp_form")[0].checkValidity()) {
|
||||
this._buttonExec($(ev.currentTarget), this._editOpportunity);
|
||||
}
|
||||
},
|
||||
/**
|
||||
* @private
|
||||
|
||||
@@ -671,7 +671,7 @@
|
||||
<label>Expected Closing:</label>
|
||||
<div class="input-group date" id="exp_closing_div" data-target-input="nearest">
|
||||
<t t-set='date_formatted'><t t-options='{"widget": "date"}' t-esc="opportunity.date_deadline"/></t>
|
||||
<input type="text" name="date_deadline" t-att-value="date_formatted" class="datetimepicker-input form-control date_deadline" t-att-name="prefix"/>
|
||||
<input type="date" min="1900-01-01" name="date_deadline" t-att-value="date_formatted" class="datetimepicker-input form-control date_deadline" data-date-format="YYYY-MM-DD" t-att-name="prefix" placeholder="YYYY-MM-DD"/>
|
||||
<div class="input-group-append" data-target="#exp_closing_div" data-toggle="datetimepicker">
|
||||
<span class="input-group-text">
|
||||
<span class="fa fa-calendar" role="img" aria-label="Calendar"></span>
|
||||
@@ -697,7 +697,7 @@
|
||||
<label class="col-form-label" for="activity_date_deadline">Next Activity Date</label>
|
||||
<div class="input-group date" id="next_activity_div" data-target-input="nearest">
|
||||
<t t-set='date_formatted'><t t-options='{"widget": "date"}' t-esc="user_activity.date_deadline"/></t>
|
||||
<input type="text" name="activity_date_deadline" t-att-value="date_formatted" class="form-control activity_date_deadline datetimepicker-input" t-att-name="prefix"/>
|
||||
<input type="date" min="1900-01-01" name="activity_date_deadline" t-att-value="date_formatted" class="form-control activity_date_deadline datetimepicker-input" data-date-format="YYYY-MM-DD" t-att-name="prefix" placeholder="YYYY-MM-DD"/>
|
||||
<div class="input-group-append" data-target="#next_activity_div" data-toggle="datetimepicker">
|
||||
<span class="input-group-text">
|
||||
<span class="fa fa-calendar" role="img" aria-label="Calendar" title="Calendar"></span>
|
||||
|
||||
@@ -327,7 +327,7 @@ class Website(models.Model):
|
||||
if code_pricelist:
|
||||
pricelist_id = code_pricelist.id
|
||||
update_pricelist = True
|
||||
elif code is not None and sale_order.pricelist_id.code:
|
||||
elif code is not None and sale_order.pricelist_id.code and code != sale_order.pricelist_id.code:
|
||||
# code is not None when user removes code and click on "Apply"
|
||||
pricelist_id = partner.property_product_pricelist.id
|
||||
update_pricelist = True
|
||||
|
||||
@@ -1269,7 +1269,7 @@
|
||||
<t t-if="mode == ('new', 'billing') or (mode == ('edit', 'billing') and (can_edit_vat or 'vat' in checkout and checkout['vat']))">
|
||||
<div t-attf-class="form-group #{error.get('company_name') and 'o_has_error' or ''} col-lg-6">
|
||||
<label class="col-form-label font-weight-normal label-optional" for="company_name">Company Name</label>
|
||||
<input type="text" name="company_name" t-attf-class="form-control #{error.get('company_name') and 'is-invalid' or ''}" t-att-value="'company_name' in checkout and checkout['company_name']" t-att-readonly="'1' if 'vat' in checkout and checkout['vat'] and not can_edit_vat else None" t-att-title="'Changing company name is not allowed once document(s) have been issued for your account. Please contact us directly for this operation.' if 'vat' in checkout and checkout['vat'] and not can_edit_vat else None" />
|
||||
<input type="text" name="company_name" t-attf-class="form-control #{error.get('company_name') and 'is-invalid' or ''}" t-att-value="'commercial_company_name' in checkout and checkout['commercial_company_name'] or 'company_name' in checkout and checkout['company_name']" t-att-readonly="'1' if 'vat' in checkout and checkout['vat'] and not can_edit_vat else None" t-att-title="'Changing company name is not allowed once document(s) have been issued for your account. Please contact us directly for this operation.' if 'vat' in checkout and checkout['vat'] and not can_edit_vat else None" />
|
||||
</div>
|
||||
<div t-attf-class="form-group #{error.get('vat') and 'o_has_error' or ''} col-lg-6 div_vat">
|
||||
<label class="col-form-label font-weight-normal label-optional" for="vat">TIN / VAT </label>
|
||||
|
||||
@@ -22,12 +22,13 @@ class ChannelUsersRelation(models.Model):
|
||||
partner_id = fields.Many2one('res.partner', index=True, required=True)
|
||||
partner_email = fields.Char(related='partner_id.email', readonly=True)
|
||||
|
||||
@api.depends('channel_id.slide_partner_ids.partner_id', 'channel_id.slide_partner_ids.completed', 'partner_id')
|
||||
@api.depends('channel_id.slide_partner_ids.partner_id', 'channel_id.slide_partner_ids.completed', 'partner_id', 'channel_id.slide_partner_ids.slide_id.is_published')
|
||||
def _compute_completion(self):
|
||||
read_group_res = self.env['slide.slide.partner'].sudo().read_group(
|
||||
['&', '&', ('channel_id', 'in', self.mapped('channel_id').ids),
|
||||
('partner_id', 'in', self.mapped('partner_id').ids),
|
||||
('completed', '=', True)],
|
||||
('completed', '=', True),
|
||||
('slide_id.is_published', '=', True)],
|
||||
['channel_id', 'partner_id'],
|
||||
groupby=['channel_id', 'partner_id'], lazy=False)
|
||||
mapped_data = dict()
|
||||
|
||||
@@ -75,6 +75,15 @@ class TestStatistics(common.SlidesCase):
|
||||
self.assertEqual(channel_emp.completion, 100)
|
||||
self.assertTrue(channel_emp.completed)
|
||||
|
||||
self.slide_3.is_published = False
|
||||
self.assertEqual(channel_emp.completion, 100)
|
||||
self.assertTrue(channel_emp.completed)
|
||||
|
||||
self.slide_3.is_published = True
|
||||
self.slide_3.active = False
|
||||
self.assertEqual(channel_emp.completion, 100)
|
||||
self.assertTrue(channel_emp.completed)
|
||||
|
||||
@mute_logger('odoo.models')
|
||||
def test_channel_user_statistics_complete_check_member(self):
|
||||
(self.slide | self.slide_2).write({'is_preview': True})
|
||||
|
||||
@@ -3,6 +3,6 @@
|
||||
<li><a href="https://www.odoo.com/documentation/user/12.0/index.html">User</a></li>
|
||||
<li><a href="https://www.odoo.com/documentation/12.0/index.html">Developer</a></li>
|
||||
<li><a href="https://www.odoo.com/documentation/12.0/setup/install.html">Installation</a></li>
|
||||
<li><a href="http://training.odoo.com/courses/odoo-functional">Training center</a></li>
|
||||
<li><a href="https://odoo.com/slides">eLearning</a></li>
|
||||
<li><a href="https://www.odoo.com/page/odoo-white-paper">White Papers</a></li>
|
||||
<li><a href="https://www.odoo.com/page/legal">Legal</a></li>
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
Chile, 2019-07-12
|
||||
|
||||
Blanco Martín & Asociados agrees to the terms of the Odoo Corporate Contributor License
|
||||
Agreement v1.0.
|
||||
|
||||
I declare that I am authorized and able to make this agreement and sign this
|
||||
declaration.
|
||||
|
||||
Signed,
|
||||
|
||||
Daniel Blanco daniel@blancomartin.cl https://github.com/danisan
|
||||
|
||||
Corporation name: Blanco Martin & Asociados EIRL
|
||||
Corporation address: Av. Apoquindo 6410
|
||||
Of. 212 Las Condes, (RM)
|
||||
Country: Chile
|
||||
Point of contact: Daniel Blanco
|
||||
Title: Director - Representante Legal
|
||||
Email: daniel@blancomartin.cl
|
||||
Telephone: +56 2 28400990
|
||||
|
||||
List of contributors:
|
||||
|
||||
* Fernando de La Barrera fernando@blancomartin.cl https://github.com/bmya-fed
|
||||
* Alejandro Paciotti alejandro@blancomartin.cl https://github.com/alp-bmya
|
||||
* Albert Nieriz albert@blancomartin.cl https://github.com/aln-bmya
|
||||
* Bruno Figares bruno@blancomartin.cl https://github.com/brf-bmya
|
||||
* Susana Vazquez susana@blancomartin.cl https://github.com/suv-bmya
|
||||
@@ -9,7 +9,9 @@ declaration.
|
||||
Signed,
|
||||
|
||||
Baskhuu Lodoikhuu baskhuujacara@gmail.com https://github.com/jacara
|
||||
Baskhuu Lodoikhuu baskhuu@bumanit.mn https://github.com/bumanit
|
||||
|
||||
List of contributors:
|
||||
|
||||
Baskhuu Lodoikhuu baskhuujacara@gmail.com https://github.com/jacara
|
||||
Baskhuu Lodoikhuu baskhuu@bumanit.mn https://github.com/bumanit
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
France, 2019-07-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,
|
||||
|
||||
Valérian Dorcy valerian.dorcy@gmail.com https://github.com/dorcyv
|
||||
@@ -0,0 +1,9 @@
|
||||
France, 2019-07-11
|
||||
|
||||
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,
|
||||
|
||||
Grégory Schreiner gregory@mokatourisme.fr https://github.com/gregory-moka
|
||||
@@ -2,17 +2,15 @@
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
import ast
|
||||
import unittest
|
||||
|
||||
from odoo import SUPERUSER_ID
|
||||
from odoo.exceptions import UserError, ValidationError
|
||||
from odoo.tests.common import TransactionCase, tagged
|
||||
from odoo.tests.common import TransactionCase, BaseCase
|
||||
from odoo.tools import mute_logger
|
||||
from odoo.tools.safe_eval import safe_eval, const_eval
|
||||
|
||||
|
||||
@tagged('standard', 'at_install')
|
||||
class TestSafeEval(unittest.TestCase):
|
||||
class TestSafeEval(BaseCase):
|
||||
def test_const(self):
|
||||
# NB: True and False are names in Python 2 not consts
|
||||
expected = (1, {"a": {2.5}}, [None, u"foo"])
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
import unittest
|
||||
|
||||
import odoo
|
||||
from odoo.sql_db import TestCursor
|
||||
from odoo.tests import common
|
||||
from odoo.tests.common import BaseCase
|
||||
from odoo.tools.misc import mute_logger
|
||||
|
||||
ADMIN_USER_ID = common.ADMIN_USER_ID
|
||||
@@ -14,8 +14,7 @@ def registry():
|
||||
return odoo.registry(common.get_db_name())
|
||||
|
||||
|
||||
@common.tagged('standard', 'at_install')
|
||||
class TestExecute(unittest.TestCase):
|
||||
class TestExecute(BaseCase):
|
||||
""" Try cr.execute with wrong parameters """
|
||||
|
||||
@mute_logger('odoo.sql_db')
|
||||
|
||||
@@ -2,15 +2,13 @@
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
import functools
|
||||
import unittest
|
||||
|
||||
from odoo.tests.common import tagged
|
||||
from odoo.tests.common import BaseCase
|
||||
from odoo.tools import frozendict
|
||||
from odoo.tools.func import compose
|
||||
|
||||
|
||||
@tagged('standard', 'at_install')
|
||||
class TestCompose(unittest.TestCase):
|
||||
class TestCompose(BaseCase):
|
||||
def test_basic(self):
|
||||
str_add = compose(str, lambda a, b: a + b)
|
||||
self.assertEqual(str_add(1, 2), "3")
|
||||
@@ -25,8 +23,7 @@ class TestCompose(unittest.TestCase):
|
||||
self.assertEqual(mul(5, 42), u"210")
|
||||
|
||||
|
||||
@tagged('standard', 'at_install')
|
||||
class TestFrozendict(unittest.TestCase):
|
||||
class TestFrozendict(BaseCase):
|
||||
def test_frozendict_immutable(self):
|
||||
""" Ensure that a frozendict is immutable. """
|
||||
vals = {'name': 'Joe', 'age': 42}
|
||||
|
||||
@@ -2,13 +2,13 @@
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from contextlib import contextmanager
|
||||
import unittest
|
||||
|
||||
import psycopg2
|
||||
import psycopg2.errorcodes
|
||||
|
||||
import odoo
|
||||
from odoo.tests import common
|
||||
from odoo.tests.common import BaseCase
|
||||
|
||||
ADMIN_USER_ID = common.ADMIN_USER_ID
|
||||
|
||||
@@ -28,8 +28,7 @@ def drop_sequence(code):
|
||||
seq.unlink()
|
||||
|
||||
|
||||
@common.tagged('standard', 'at_install')
|
||||
class TestIrSequenceStandard(unittest.TestCase):
|
||||
class TestIrSequenceStandard(BaseCase):
|
||||
""" A few tests for a 'Standard' (i.e. PostgreSQL) sequence. """
|
||||
|
||||
def test_ir_sequence_create(self):
|
||||
@@ -67,8 +66,7 @@ class TestIrSequenceStandard(unittest.TestCase):
|
||||
drop_sequence('test_sequence_type')
|
||||
|
||||
|
||||
@common.tagged('standard', 'at_install')
|
||||
class TestIrSequenceNoGap(unittest.TestCase):
|
||||
class TestIrSequenceNoGap(BaseCase):
|
||||
""" Copy of the previous tests for a 'No gap' sequence. """
|
||||
|
||||
def test_ir_sequence_create_no_gap(self):
|
||||
@@ -105,8 +103,7 @@ class TestIrSequenceNoGap(unittest.TestCase):
|
||||
drop_sequence('test_sequence_type_2')
|
||||
|
||||
|
||||
@common.tagged('standard', 'at_install')
|
||||
class TestIrSequenceChangeImplementation(unittest.TestCase):
|
||||
class TestIrSequenceChangeImplementation(BaseCase):
|
||||
""" Create sequence objects and change their ``implementation`` field. """
|
||||
|
||||
def test_ir_sequence_1_create(self):
|
||||
@@ -143,8 +140,7 @@ class TestIrSequenceChangeImplementation(unittest.TestCase):
|
||||
drop_sequence('test_sequence_type_4')
|
||||
|
||||
|
||||
@common.tagged('standard', 'at_install')
|
||||
class TestIrSequenceGenerate(unittest.TestCase):
|
||||
class TestIrSequenceGenerate(BaseCase):
|
||||
""" Create sequence objects and generate some values. """
|
||||
|
||||
def test_ir_sequence_create(self):
|
||||
|
||||
@@ -1,15 +1,13 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
import unittest
|
||||
|
||||
from odoo.tests.common import tagged
|
||||
from odoo.tests.common import BaseCase
|
||||
from odoo.tools import html_sanitize, append_content_to_html, plaintext2html, email_split, misc
|
||||
from . import test_mail_examples
|
||||
|
||||
|
||||
@tagged('standard', 'at_install')
|
||||
class TestSanitizer(unittest.TestCase):
|
||||
class TestSanitizer(BaseCase):
|
||||
""" Test the html sanitizer that filters html to remove unwanted attributes """
|
||||
|
||||
def test_basic_sanitizer(self):
|
||||
@@ -285,8 +283,7 @@ class TestSanitizer(unittest.TestCase):
|
||||
# self.assertNotIn(ext, new_html)
|
||||
|
||||
|
||||
@tagged('standard', 'at_install')
|
||||
class TestHtmlTools(unittest.TestCase):
|
||||
class TestHtmlTools(BaseCase):
|
||||
""" Test some of our generic utility functions about html """
|
||||
|
||||
def test_plaintext2html(self):
|
||||
@@ -313,8 +310,7 @@ class TestHtmlTools(unittest.TestCase):
|
||||
self.assertEqual(append_content_to_html(html, content, plaintext_flag, preserve_flag, container_tag), expected, 'append_content_to_html is broken')
|
||||
|
||||
|
||||
@tagged('standard', 'at_install')
|
||||
class TestEmailTools(unittest.TestCase):
|
||||
class TestEmailTools(BaseCase):
|
||||
""" Test some of our generic utility functions for emails """
|
||||
|
||||
def test_email_split(self):
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import base64
|
||||
import unittest
|
||||
|
||||
from odoo.tests.common import tagged
|
||||
from odoo.tests.common import BaseCase
|
||||
from odoo.tools.mimetypes import guess_mimetype
|
||||
|
||||
PNG = b'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAIAAACQd1PeAAAADElEQVQI12P4//8/AAX+Av7czFnnAAAAAElFTkSuQmCC'
|
||||
@@ -23,8 +23,7 @@ hbnNsYXRlKDUwLDUwKSI+CiAgICA8cmVjdCB4PSIwIiB5PSIwIiB3aWR0aD0iMTUwIiBoZWlnaHQ9IjU
|
||||
pyZWQ7IiAvPgogIDwvZz4KCjwvc3ZnPgo="""
|
||||
|
||||
|
||||
@tagged('standard', 'at_install')
|
||||
class test_guess_mimetype(unittest.TestCase):
|
||||
class test_guess_mimetype(BaseCase):
|
||||
|
||||
def test_default_mimetype_empty(self):
|
||||
mimetype = guess_mimetype(b'')
|
||||
|
||||
@@ -4,14 +4,12 @@
|
||||
import datetime
|
||||
from dateutil.relativedelta import relativedelta
|
||||
import pytz
|
||||
import unittest
|
||||
|
||||
from odoo.tools import misc, date_utils
|
||||
from odoo.tests.common import TransactionCase, tagged
|
||||
from odoo.tests.common import TransactionCase, BaseCase
|
||||
|
||||
|
||||
@tagged('standard', 'at_install')
|
||||
class TestCountingStream(unittest.TestCase):
|
||||
class TestCountingStream(BaseCase):
|
||||
def test_empty_stream(self):
|
||||
s = misc.CountingStream(iter([]))
|
||||
self.assertEqual(s.index, -1)
|
||||
@@ -42,8 +40,7 @@ class TestCountingStream(unittest.TestCase):
|
||||
self.assertEqual(s.index, 0)
|
||||
|
||||
|
||||
@tagged('standard', 'at_install')
|
||||
class TestDateRangeFunction(unittest.TestCase):
|
||||
class TestDateRangeFunction(BaseCase):
|
||||
""" Test on date_range generator. """
|
||||
|
||||
def test_date_range_with_naive_datetimes(self):
|
||||
|
||||
@@ -1,14 +1,11 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
import unittest
|
||||
|
||||
from odoo.osv.query import Query
|
||||
from odoo.tests.common import tagged
|
||||
from odoo.tests.common import BaseCase
|
||||
|
||||
|
||||
@tagged('standard', 'at_install')
|
||||
class QueryTestCase(unittest.TestCase):
|
||||
class QueryTestCase(BaseCase):
|
||||
|
||||
def test_basic_query(self):
|
||||
query = Query()
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo.tests.common import TransactionCase, tagged, TagsSelector
|
||||
from odoo.tests.common import TransactionCase, tagged, TagsSelector, BaseCase
|
||||
|
||||
|
||||
@tagged('nodatabase')
|
||||
@@ -17,7 +17,8 @@ class TestSetTags(TransactionCase):
|
||||
fc = FakeClass()
|
||||
|
||||
self.assertTrue(hasattr(fc, 'test_tags'))
|
||||
self.assertEqual(fc.test_tags, {'at_install', 'standard', 'base'})
|
||||
self.assertEqual(fc.test_tags, {'at_install', 'standard'})
|
||||
self.assertEqual(fc.test_module, 'base')
|
||||
|
||||
def test_set_tags_not_decorated(self):
|
||||
"""Test that a TransactionCase has some test_tags by default"""
|
||||
@@ -28,7 +29,8 @@ class TestSetTags(TransactionCase):
|
||||
fc = FakeClass()
|
||||
|
||||
self.assertTrue(hasattr(fc, 'test_tags'))
|
||||
self.assertEqual(fc.test_tags, {'at_install', 'standard', 'base'})
|
||||
self.assertEqual(fc.test_tags, {'at_install', 'standard'})
|
||||
self.assertEqual(fc.test_module, 'base')
|
||||
|
||||
def test_set_tags_single_tag(self):
|
||||
"""Test the set_tags decorator with a single tag"""
|
||||
@@ -39,7 +41,8 @@ class TestSetTags(TransactionCase):
|
||||
|
||||
fc = FakeClass()
|
||||
|
||||
self.assertEqual(fc.test_tags, {'at_install', 'standard', 'base', 'slow'})
|
||||
self.assertEqual(fc.test_tags, {'at_install', 'standard', 'slow'})
|
||||
self.assertEqual(fc.test_module, 'base')
|
||||
|
||||
def test_set_tags_multiple_tags(self):
|
||||
"""Test the set_tags decorator with multiple tags"""
|
||||
@@ -50,7 +53,8 @@ class TestSetTags(TransactionCase):
|
||||
|
||||
fc = FakeClass()
|
||||
|
||||
self.assertEqual(fc.test_tags, {'at_install', 'standard', 'base', 'slow', 'nightly'})
|
||||
self.assertEqual(fc.test_tags, {'at_install', 'standard', 'slow', 'nightly'})
|
||||
self.assertEqual(fc.test_module, 'base')
|
||||
|
||||
def test_inheritance(self):
|
||||
"""Test inheritance when using the 'tagged' decorator"""
|
||||
@@ -64,13 +68,15 @@ class TestSetTags(TransactionCase):
|
||||
pass
|
||||
|
||||
fc = FakeClassB()
|
||||
self.assertEqual(fc.test_tags, {'at_install', 'standard', 'base', 'nightly'})
|
||||
self.assertEqual(fc.test_tags, {'at_install', 'standard', 'nightly'})
|
||||
self.assertEqual(fc.test_module, 'base')
|
||||
|
||||
class FakeClassC(FakeClassA):
|
||||
pass
|
||||
|
||||
fc = FakeClassC()
|
||||
self.assertEqual(fc.test_tags, {'at_install', 'standard', 'base'})
|
||||
self.assertEqual(fc.test_tags, {'at_install', 'standard'})
|
||||
self.assertEqual(fc.test_module, 'base')
|
||||
|
||||
def test_untagging(self):
|
||||
"""Test that one can remove the 'standard' tag"""
|
||||
@@ -80,7 +86,8 @@ class TestSetTags(TransactionCase):
|
||||
pass
|
||||
|
||||
fc = FakeClassA()
|
||||
self.assertEqual(fc.test_tags, {'at_install', 'base'})
|
||||
self.assertEqual(fc.test_tags, {'at_install'})
|
||||
self.assertEqual(fc.test_module, 'base')
|
||||
|
||||
@tagged('-standard', '-base', '-at_install')
|
||||
class FakeClassB(TransactionCase):
|
||||
@@ -104,61 +111,110 @@ class TestSelector(TransactionCase):
|
||||
"""Test the parser part of the TagsSelector class"""
|
||||
|
||||
tags = TagsSelector('+slow')
|
||||
self.assertEqual({'slow', }, tags.include)
|
||||
self.assertEqual({('slow', None, None, None),}, tags.include)
|
||||
self.assertEqual(set(), tags.exclude)
|
||||
|
||||
tags = TagsSelector('+slow,nightly')
|
||||
self.assertEqual({'slow', 'nightly'}, tags.include)
|
||||
self.assertEqual({('slow', None, None, None), ('nightly', None, None, None)}, tags.include)
|
||||
self.assertEqual(set(), tags.exclude)
|
||||
|
||||
tags = TagsSelector('+slow,-standard')
|
||||
self.assertEqual({'slow', }, tags.include)
|
||||
self.assertEqual({'standard', }, tags.exclude)
|
||||
self.assertEqual({('slow', None, None, None),}, tags.include)
|
||||
self.assertEqual({('standard', None, None, None),}, tags.exclude)
|
||||
|
||||
# same with space after the comma
|
||||
tags = TagsSelector('+slow, -standard')
|
||||
self.assertEqual({'slow', }, tags.include)
|
||||
self.assertEqual({'standard', }, tags.exclude)
|
||||
self.assertEqual({('slow', None, None, None),}, tags.include)
|
||||
self.assertEqual({('standard', None, None, None),}, tags.exclude)
|
||||
|
||||
# same with space befaore and after the comma
|
||||
# same with space before and after the comma
|
||||
tags = TagsSelector('+slow , -standard')
|
||||
self.assertEqual({'slow', }, tags.include)
|
||||
self.assertEqual({'standard', }, tags.exclude)
|
||||
self.assertEqual({('slow', None, None, None), }, tags.include)
|
||||
self.assertEqual({('standard', None, None, None), }, tags.exclude)
|
||||
|
||||
tags = TagsSelector('+slow ,-standard,+js')
|
||||
self.assertEqual({'slow', 'js', }, tags.include)
|
||||
self.assertEqual({'standard', }, tags.exclude)
|
||||
self.assertEqual({('slow', None, None, None),('js', None, None, None)}, tags.include)
|
||||
self.assertEqual({('standard', None, None, None),}, tags.exclude)
|
||||
|
||||
# without +
|
||||
tags = TagsSelector('slow, ')
|
||||
self.assertEqual({'slow', }, tags.include)
|
||||
self.assertEqual({('slow', None, None, None), }, tags.include)
|
||||
self.assertEqual(set(), tags.exclude)
|
||||
|
||||
# duplicates
|
||||
tags = TagsSelector('+slow,-standard, slow,-standard ')
|
||||
self.assertEqual({'slow', }, tags.include)
|
||||
self.assertEqual({'standard', }, tags.exclude)
|
||||
self.assertEqual({('slow', None, None, None), }, tags.include)
|
||||
self.assertEqual({('standard', None, None, None), }, tags.exclude)
|
||||
|
||||
tags = TagsSelector('')
|
||||
self.assertEqual(set(), tags.include)
|
||||
self.assertEqual(set(), tags.exclude)
|
||||
|
||||
tags = TagsSelector('/module') # all standard test of a module
|
||||
self.assertEqual({('standard', 'module', None, None), }, tags.include)
|
||||
self.assertEqual(set(), tags.exclude)
|
||||
|
||||
tags = TagsSelector('*/module') # all tests of a module
|
||||
self.assertEqual({(None, 'module', None, None), }, tags.include)
|
||||
self.assertEqual(set(), tags.exclude)
|
||||
|
||||
tags = TagsSelector(':class') # all standard test of a class
|
||||
self.assertEqual({('standard', None, 'class', None), }, tags.include)
|
||||
self.assertEqual(set(), tags.exclude)
|
||||
|
||||
tags = TagsSelector('.method')
|
||||
self.assertEqual({('standard', None, None, 'method'), }, tags.include)
|
||||
self.assertEqual(set(), tags.exclude)
|
||||
|
||||
tags = TagsSelector(':class.method')
|
||||
self.assertEqual({('standard', None, 'class', 'method'), }, tags.include)
|
||||
self.assertEqual(set(), tags.exclude)
|
||||
|
||||
tags = TagsSelector('/module:class.method') # only a specific test func in a module (standard)
|
||||
self.assertEqual({('standard', 'module', 'class', 'method'), }, tags.include)
|
||||
self.assertEqual(set(), tags.exclude)
|
||||
|
||||
tags = TagsSelector('*/module:class.method') # only a specific test func in a module
|
||||
self.assertEqual({(None, 'module', 'class', 'method'), }, tags.include)
|
||||
self.assertEqual(set(), tags.exclude)
|
||||
|
||||
tags = TagsSelector('-/module:class.method') # disable a specific test func in a module
|
||||
self.assertEqual({('standard', None, None, None), }, tags.include) # all strandard
|
||||
self.assertEqual({(None, 'module', 'class', 'method'), }, tags.exclude) # exept the test func
|
||||
|
||||
tags = TagsSelector('-*/module:class.method')
|
||||
self.assertEqual({('standard', None, None, None), }, tags.include)
|
||||
self.assertEqual({(None, 'module', 'class', 'method'), }, tags.exclude)
|
||||
|
||||
tags = TagsSelector('tag/module')
|
||||
self.assertEqual({('tag', 'module', None, None), }, tags.include)
|
||||
self.assertEqual(set(), tags.exclude)
|
||||
|
||||
tags = TagsSelector('tag.method')
|
||||
self.assertEqual({('tag', None, None, 'method'), }, tags.include)
|
||||
self.assertEqual(set(), tags.exclude)
|
||||
|
||||
tags = TagsSelector('*/module,-standard') # all non standard test of a module
|
||||
self.assertEqual({(None, 'module', None, None), }, tags.include) # all in module
|
||||
self.assertEqual({('standard', None, None, None), }, tags.exclude) # exept standard ones
|
||||
|
||||
@tagged('nodatabase')
|
||||
class TestSelectorSelection(TransactionCase):
|
||||
|
||||
def test_selector_selection(self):
|
||||
"""Test check_tags use cases"""
|
||||
class Test_A(TransactionCase):
|
||||
pass
|
||||
|
||||
@tagged('stock')
|
||||
class Test_B():
|
||||
class Test_B(BaseCase):
|
||||
pass
|
||||
|
||||
@tagged('stock', 'slow')
|
||||
class Test_C():
|
||||
class Test_C(BaseCase):
|
||||
pass
|
||||
|
||||
@tagged('standard', 'slow')
|
||||
class Test_D():
|
||||
class Test_D(BaseCase):
|
||||
pass
|
||||
|
||||
@tagged('-at_install', 'post_install')
|
||||
@@ -175,7 +231,6 @@ class TestSelectorSelection(TransactionCase):
|
||||
# considered tagged standard and they are run by default if
|
||||
# not explicitly deselected with '-standard' or if 'standard' is not
|
||||
# selectected along with another test tag
|
||||
|
||||
# same as "--test-tags=''" parameters:
|
||||
tags = TagsSelector('')
|
||||
self.assertFalse(tags.check(no_tags_obj))
|
||||
@@ -211,10 +266,10 @@ class TestSelectorSelection(TransactionCase):
|
||||
self.assertFalse(tags.check(stock_tag_obj))
|
||||
|
||||
tags = TagsSelector('standard')
|
||||
self.assertFalse(tags.check(stock_tag_obj))
|
||||
self.assertTrue(tags.check(stock_tag_obj))
|
||||
|
||||
tags = TagsSelector('slow,standard')
|
||||
self.assertFalse(tags.check(stock_tag_obj))
|
||||
self.assertTrue(tags.check(stock_tag_obj))
|
||||
|
||||
tags = TagsSelector('slow,-standard')
|
||||
self.assertFalse(tags.check(stock_tag_obj))
|
||||
|
||||
@@ -1,16 +1,12 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
import unittest
|
||||
|
||||
from odoo.tools import mute_logger
|
||||
from odoo.tools.translate import quote, unquote, xml_translate, html_translate
|
||||
from odoo.tests.common import TransactionCase, tagged
|
||||
from odoo.tests.common import TransactionCase, BaseCase
|
||||
from psycopg2 import IntegrityError
|
||||
|
||||
|
||||
@tagged('standard', 'at_install')
|
||||
class TranslationToolsTestCase(unittest.TestCase):
|
||||
class TranslationToolsTestCase(BaseCase):
|
||||
def assertItemsEqual(self, a, b, msg=None):
|
||||
self.assertEqual(sorted(a), sorted(b), msg)
|
||||
|
||||
|
||||
@@ -8,6 +8,8 @@ import unittest
|
||||
|
||||
from odoo import api, registry, SUPERUSER_ID
|
||||
from odoo.tests import common
|
||||
from odoo.tests.common import BaseCase
|
||||
|
||||
from odoo.modules.registry import Registry
|
||||
|
||||
|
||||
@@ -25,8 +27,7 @@ MODULE = 'test_uninstall'
|
||||
MODEL = 'test_uninstall.model'
|
||||
|
||||
|
||||
@common.tagged('standard', 'at_install')
|
||||
class TestUninstall(unittest.TestCase):
|
||||
class TestUninstall(BaseCase):
|
||||
"""
|
||||
Test the install/uninstall of a test module. The module is available in
|
||||
`odoo.tests` which should be present in the addons-path.
|
||||
|
||||
@@ -3,9 +3,8 @@
|
||||
|
||||
from lxml import etree
|
||||
import io
|
||||
import unittest
|
||||
|
||||
from odoo.tests.common import tagged
|
||||
from odoo.tests.common import BaseCase
|
||||
from odoo.tools.view_validation import (
|
||||
valid_page_in_book, valid_att_in_form, valid_type_in_colspan,
|
||||
valid_type_in_col, valid_att_in_field, valid_att_in_label,
|
||||
@@ -131,8 +130,7 @@ valid_tree = etree.parse(io.BytesIO(b'''\
|
||||
''')).getroot()
|
||||
|
||||
|
||||
@tagged('standard', 'at_install')
|
||||
class TestViewValidation(unittest.TestCase):
|
||||
class TestViewValidation(BaseCase):
|
||||
""" Test the view validation code (but not the views themselves). """
|
||||
|
||||
def test_page_validation(self):
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
import os.path
|
||||
import unittest
|
||||
|
||||
from odoo.tests.common import tagged
|
||||
from odoo.tests.common import BaseCase
|
||||
from odoo.tools.mimetypes import guess_mimetype
|
||||
|
||||
def contents(extension):
|
||||
@@ -14,8 +13,7 @@ def contents(extension):
|
||||
return f.read()
|
||||
|
||||
|
||||
@tagged('standard', 'at_install')
|
||||
class TestMimeGuessing(unittest.TestCase):
|
||||
class TestMimeGuessing(BaseCase):
|
||||
def test_doc(self):
|
||||
self.assertEqual(
|
||||
guess_mimetype(contents('doc')),
|
||||
|
||||
+83
-22
@@ -189,8 +189,9 @@ class MetaCase(type):
|
||||
super(MetaCase, cls).__init__(name, bases, attrs)
|
||||
# assign default test tags
|
||||
if cls.__module__.startswith('odoo.addons.'):
|
||||
module = cls.__module__.split('.')[2]
|
||||
cls.test_tags = {'standard', 'at_install', module}
|
||||
cls.test_tags = {'standard', 'at_install'}
|
||||
cls.test_module = cls.__module__.split('.')[2]
|
||||
cls.test_class = cls.__name__
|
||||
|
||||
|
||||
class BaseCase(TreeCase, MetaCase('DummyCase', (object,), {})):
|
||||
@@ -244,7 +245,10 @@ class BaseCase(TreeCase, MetaCase('DummyCase', (object,), {})):
|
||||
def _assertRaises(self, exception):
|
||||
""" Context manager that clears the environment upon failure. """
|
||||
with super(BaseCase, self).assertRaises(exception) as cm:
|
||||
with self.env.clear_upon_failure():
|
||||
if hasattr(self, 'env'):
|
||||
with self.env.clear_upon_failure():
|
||||
yield cm
|
||||
else:
|
||||
yield cm
|
||||
|
||||
def assertRaises(self, exception, func=None, *args, **kwargs):
|
||||
@@ -333,9 +337,23 @@ class BaseCase(TreeCase, MetaCase('DummyCase', (object,), {})):
|
||||
return False
|
||||
return True
|
||||
|
||||
def _repr_field_value(record, field_name):
|
||||
record_value = record[field_name]
|
||||
field_type = record._fields[field_name].type
|
||||
if field_type == 'monetary':
|
||||
currency_field_name = record._fields[field_name].currency_field
|
||||
record_currency = record[currency_field_name]
|
||||
return record_currency and record_currency.round(record_value) or record_value
|
||||
elif field_type in ('one2many', 'many2many'):
|
||||
return set(record_value.ids)
|
||||
elif field_type == 'many2one':
|
||||
return record_value.id
|
||||
else:
|
||||
return record_value
|
||||
|
||||
def _format_message(records, expected_values):
|
||||
''' Return a formatted representation of records/expected_values. '''
|
||||
all_records_values = records.read(list(expected_values[0].keys()), load=False)
|
||||
all_records_values = [{key: _repr_field_value(record, key) for key in expected_values[0]} for record in records]
|
||||
msg1 = '\n'.join(pprint.pformat(dic) for dic in all_records_values)
|
||||
msg2 = '\n'.join(pprint.pformat(dic) for dic in expected_values)
|
||||
return 'Current values:\n\n%s\n\nExpected values:\n\n%s' % (msg1, msg2)
|
||||
@@ -706,6 +724,8 @@ class ChromeBrowser():
|
||||
res = None
|
||||
if res and res.get('id') == ready_id:
|
||||
if res.get('result') == awaited_result:
|
||||
if has_exceeded:
|
||||
self._logger.info('The ready code tooks too much time : %s', tdiff)
|
||||
return True
|
||||
else:
|
||||
last_bad_res = res
|
||||
@@ -713,7 +733,6 @@ class ChromeBrowser():
|
||||
tdiff = time.time() - start_time
|
||||
if tdiff >= 2 and not has_exceeded:
|
||||
has_exceeded = True
|
||||
self._logger.warning('The ready code takes too much time : %s', tdiff)
|
||||
|
||||
self.take_screenshot(prefix='failed_ready')
|
||||
self._logger.info('Ready code last try result: %s', last_bad_res or res)
|
||||
@@ -1806,7 +1825,7 @@ def _get_node(view, f, *arg):
|
||||
|
||||
def tagged(*tags):
|
||||
"""
|
||||
A decorator to tag TestCase objects
|
||||
A decorator to tag BaseCase objects
|
||||
Tags are stored in a set that can be accessed from a 'test_tags' attribute
|
||||
A tag prefixed by '-' will remove the tag e.g. to remove the 'standard' tag
|
||||
By default, all Test classes from odoo.tests.common have a test_tags
|
||||
@@ -1816,33 +1835,75 @@ def tagged(*tags):
|
||||
def tags_decorator(obj):
|
||||
include = {t for t in tags if not t.startswith('-')}
|
||||
exclude = {t[1:] for t in tags if t.startswith('-')}
|
||||
obj.test_tags = (getattr(obj, 'test_tags', set()) | include) - exclude
|
||||
obj.test_tags = (getattr(obj, 'test_tags', set()) | include) - exclude # todo remove getattr in master since we want to limmit tagged to BaseCase and always have +standard tag
|
||||
return obj
|
||||
return tags_decorator
|
||||
|
||||
|
||||
class TagsSelector(object):
|
||||
""" Test selector based on tags. """
|
||||
filter_spec_re = re.compile(r'^([+-]?)(\*|\w*)(?:/(\w*))?(?::(\w*))?(?:\.(\w*))?$') # [-][tag][/module][:class][.method]
|
||||
|
||||
def __init__(self, spec):
|
||||
""" Parse the spec to determine tags to include and exclude. """
|
||||
clean_tags = {t.strip() for t in spec.split(',') if t.strip() != ''}
|
||||
self.exclude = {t[1:] for t in clean_tags if t.startswith('-')}
|
||||
self.include = {t.replace('+', '') for t in clean_tags if not t.startswith('-')}
|
||||
filter_specs = {t.strip() for t in spec.split(',') if t.strip()}
|
||||
self.exclude = set()
|
||||
self.include = set()
|
||||
|
||||
def check(self, arg):
|
||||
for filter_spec in filter_specs:
|
||||
match = self.filter_spec_re.match(filter_spec)
|
||||
if not match:
|
||||
_logger.error('Invalid tag %s', filter_spec)
|
||||
continue
|
||||
|
||||
sign, tag, module, klass, method = match.groups()
|
||||
is_include = sign != '-'
|
||||
|
||||
if not tag and is_include:
|
||||
# including /module:class.method implicitly requires 'standard'
|
||||
tag = 'standard'
|
||||
elif not tag or tag == '*':
|
||||
# '*' indicates all tests (instead of 'standard' tests only)
|
||||
tag = None
|
||||
test_filter = (tag, module, klass, method)
|
||||
|
||||
if is_include:
|
||||
self.include.add(test_filter)
|
||||
else:
|
||||
self.exclude.add(test_filter)
|
||||
|
||||
if self.exclude and not self.include:
|
||||
self.include.add(('standard', None, None, None))
|
||||
|
||||
def check(self, test):
|
||||
""" Return whether ``arg`` matches the specification: it must have at
|
||||
least one tag in ``self.include`` and none in ``self.exclude``.
|
||||
least one tag in ``self.include`` and none in ``self.exclude`` for each tag category.
|
||||
"""
|
||||
# handle the case where the Test does not inherit from TransactionCase
|
||||
tags = getattr(arg, 'test_tags', set())
|
||||
inter_no_test = self.exclude.intersection(tags)
|
||||
if inter_no_test:
|
||||
_logger.debug("Test '%s' not selected because it is tagged with : %s (exclusions: %s)", arg, inter_no_test, self.exclude)
|
||||
if not hasattr(test, 'test_tags'): # handle the case where the Test does not inherit from BaseCase and has no test_tags
|
||||
_logger.debug("Skipping test '%s' because no test_tag found.", test)
|
||||
return False
|
||||
inter_to_test = self.include.intersection(tags)
|
||||
if not inter_to_test:
|
||||
_logger.debug("Test '%s' not selected because it was not tagged with %s", arg, self.include)
|
||||
|
||||
test_module = getattr(test, 'test_module', None)
|
||||
test_class = getattr(test, 'test_class', None)
|
||||
test_tags = test.test_tags | {test_module} # module as test_tags deprecated, keep for retrocompatibility,
|
||||
test_method = getattr(test, '_testMethodName', None)
|
||||
|
||||
def _is_matching(test_filter):
|
||||
(tag, module, klass, method) = test_filter
|
||||
if tag and tag not in test_tags:
|
||||
return False
|
||||
elif module and module != test_module:
|
||||
return False
|
||||
elif klass and klass != test_class:
|
||||
return False
|
||||
elif method and test_method and method != test_method:
|
||||
return False
|
||||
return True
|
||||
|
||||
if any(_is_matching(test_filter) for test_filter in self.exclude):
|
||||
return False
|
||||
_logger.debug("Test '%s' selected: tagged with %s, exclusions: %s, inclusions: %s", arg, tags, self.exclude, self.include)
|
||||
return True
|
||||
|
||||
if any(_is_matching(test_filter) for test_filter in self.include):
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
@@ -160,7 +160,14 @@ class configmanager(object):
|
||||
dest='test_enable',
|
||||
help="Enable unit tests.")
|
||||
group.add_option("--test-tags", dest="test_tags",
|
||||
help="Comma separated list of tags to filter which tests to execute. Enable unit tests if set.")
|
||||
help="""Comma separated list of spec to filter which tests to execute. Enable unit tests if set.
|
||||
A filter spec has the format: [-][tag][/module][:class][.method]
|
||||
The '-' specifies if we want to include or exclude tests matching this spec.
|
||||
The tag will match tags added on a class with a @tagged decorator. By default tag value is 'standard' when not
|
||||
given on include mode. '*' will match all tags. Tag will also match module name (deprecated, use /module)
|
||||
The module, class, and method will respectively match the module name, test class name and test method name.
|
||||
examples: :TestClass.test_func,/test_module,external
|
||||
""")
|
||||
|
||||
parser.add_option_group(group)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user