[MERGE] forward port branch saas-12.3 up to 6f55fd65da
This commit is contained in:
@@ -2173,9 +2173,9 @@ class AccountMoveLine(models.Model):
|
|||||||
help="The bank statement used for bank reconciliation")
|
help="The bank statement used for bank reconciliation")
|
||||||
|
|
||||||
# ==== Tax fields ====
|
# ==== Tax fields ====
|
||||||
tax_ids = fields.Many2many('account.tax', string='Taxes')
|
tax_ids = fields.Many2many('account.tax', string='Taxes', help="Taxes that apply on the base amount")
|
||||||
tax_line_id = fields.Many2one('account.tax', string='Originator tax', ondelete='restrict', store=True,
|
tax_line_id = fields.Many2one('account.tax', string='Originator tax', ondelete='restrict', store=True,
|
||||||
compute='_compute_tax_line_id')
|
compute='_compute_tax_line_id', help="Indicates that this journal item is a tax line")
|
||||||
tax_base_amount = fields.Monetary(string="Base Amount", store=True,
|
tax_base_amount = fields.Monetary(string="Base Amount", store=True,
|
||||||
currency_field='company_currency_id')
|
currency_field='company_currency_id')
|
||||||
tax_exigible = fields.Boolean(string='Appears in VAT report', default=True,
|
tax_exigible = fields.Boolean(string='Appears in VAT report', default=True,
|
||||||
|
|||||||
@@ -454,7 +454,10 @@ class account_payment(models.Model):
|
|||||||
move.line_ids.remove_move_reconcile()
|
move.line_ids.remove_move_reconcile()
|
||||||
move.button_cancel()
|
move.button_cancel()
|
||||||
move.unlink()
|
move.unlink()
|
||||||
rec.state = 'cancelled'
|
rec.write({
|
||||||
|
'state': 'cancelled',
|
||||||
|
'move_name': False,
|
||||||
|
})
|
||||||
|
|
||||||
@api.multi
|
@api.multi
|
||||||
def unlink(self):
|
def unlink(self):
|
||||||
|
|||||||
@@ -580,6 +580,8 @@ class AccountReconcileModel(models.Model):
|
|||||||
'''
|
'''
|
||||||
if not self.match_total_amount:
|
if not self.match_total_amount:
|
||||||
return True
|
return True
|
||||||
|
if not candidates:
|
||||||
|
return False
|
||||||
|
|
||||||
# Match total residual amount.
|
# Match total residual amount.
|
||||||
total_residual = 0.0
|
total_residual = 0.0
|
||||||
|
|||||||
@@ -331,7 +331,7 @@
|
|||||||
<field name="module_account_sepa_direct_debit" class="oe_inline" widget="upgrade_boolean"/>
|
<field name="module_account_sepa_direct_debit" class="oe_inline" widget="upgrade_boolean"/>
|
||||||
</div>
|
</div>
|
||||||
<div class="o_setting_right_pane" name="sepa_direct_debit_right_pane">
|
<div class="o_setting_right_pane" name="sepa_direct_debit_right_pane">
|
||||||
<label string="SEPA Direct Debit (SDD)" for="module_account_sepa"/>
|
<label string="SEPA Direct Debit (SDD)" for="module_account_sepa_direct_debit"/>
|
||||||
<span class="fa fa-lg fa-building-o" title="Values set here are company-specific." aria-label="Values set here are company-specific." groups="base.group_multi_company" role="img"/>
|
<span class="fa fa-lg fa-building-o" title="Values set here are company-specific." aria-label="Values set here are company-specific." groups="base.group_multi_company" role="img"/>
|
||||||
<div class="text-muted">
|
<div class="text-muted">
|
||||||
Collect customer payments in one-click using Euro SEPA Service
|
Collect customer payments in one-click using Euro SEPA Service
|
||||||
|
|||||||
@@ -163,6 +163,7 @@ _map_iban_template = {
|
|||||||
'sm': 'SMkk KBBB BBSS SSSC CCCC CCCC CCC', # San Marino
|
'sm': 'SMkk KBBB BBSS SSSC CCCC CCCC CCC', # San Marino
|
||||||
'tn': 'TNkk BBSS SCCC CCCC CCCC CCCC', # Tunisia
|
'tn': 'TNkk BBSS SCCC CCCC CCCC CCCC', # Tunisia
|
||||||
'tr': 'TRkk BBBB BRCC CCCC CCCC CCCC CC', # Turkey
|
'tr': 'TRkk BBBB BRCC CCCC CCCC CCCC CC', # Turkey
|
||||||
|
'ua': 'UAkk BBBB BBCC CCCC CCCC CCCC CCCC C', # Ukraine
|
||||||
'vg': 'VGkk BBBB CCCC CCCC CCCC CCCC', # Virgin Islands
|
'vg': 'VGkk BBBB CCCC CCCC CCCC CCCC', # Virgin Islands
|
||||||
'xk': 'XKkk BBBB CCCC CCCC CCCC', # Kosovo
|
'xk': 'XKkk BBBB CCCC CCCC CCCC', # Kosovo
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -446,6 +446,9 @@ class Import(models.TransientModel):
|
|||||||
# Or a date/datetime if it matches the pattern
|
# Or a date/datetime if it matches the pattern
|
||||||
date_patterns = [options['date_format']] if options.get(
|
date_patterns = [options['date_format']] if options.get(
|
||||||
'date_format') else []
|
'date_format') else []
|
||||||
|
user_date_format = self.env['res.lang']._lang_get(self.env.user.lang).date_format
|
||||||
|
if user_date_format:
|
||||||
|
date_patterns.append(user_date_format)
|
||||||
date_patterns.extend(DATE_PATTERNS)
|
date_patterns.extend(DATE_PATTERNS)
|
||||||
match = check_patterns(date_patterns, preview_values)
|
match = check_patterns(date_patterns, preview_values)
|
||||||
if match:
|
if match:
|
||||||
|
|||||||
@@ -14,6 +14,13 @@ except ImportError:
|
|||||||
"Install it to support more countries, for example with `easy_install vatnumber`.")
|
"Install it to support more countries, for example with `easy_install vatnumber`.")
|
||||||
vatnumber = None
|
vatnumber = None
|
||||||
|
|
||||||
|
# Although stdnum is a dependency of vatnumber, the import of the latter is surrounded by a try/except
|
||||||
|
# if it is not installed. Therefore, we cannot be sure stdnum is installed in all cases.
|
||||||
|
try:
|
||||||
|
import stdnum
|
||||||
|
except ImportError:
|
||||||
|
stdnum = None
|
||||||
|
|
||||||
from odoo import api, models, _
|
from odoo import api, models, _
|
||||||
from odoo.tools.misc import ustr
|
from odoo.tools.misc import ustr
|
||||||
from odoo.exceptions import ValidationError
|
from odoo.exceptions import ValidationError
|
||||||
@@ -63,6 +70,8 @@ _ref_vat = {
|
|||||||
'be': 'BE0477472701',
|
'be': 'BE0477472701',
|
||||||
'bg': 'BG1234567892',
|
'bg': 'BG1234567892',
|
||||||
'ch': 'CHE-123.456.788 TVA or CH TVA 123456', # Swiss by Yannick Vaucher @ Camptocamp
|
'ch': 'CHE-123.456.788 TVA or CH TVA 123456', # Swiss by Yannick Vaucher @ Camptocamp
|
||||||
|
'cl': 'CL76086428-5',
|
||||||
|
'co': 'CO213123432-1 or CO213.123.432-1',
|
||||||
'cy': 'CY12345678F',
|
'cy': 'CY12345678F',
|
||||||
'cz': 'CZ12345679',
|
'cz': 'CZ12345679',
|
||||||
'de': 'DE123456788',
|
'de': 'DE123456788',
|
||||||
@@ -398,6 +407,12 @@ class ResPartner(models.Model):
|
|||||||
except ImportError:
|
except ImportError:
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
def check_vat_cl(self, vat):
|
||||||
|
return stdnum.util.get_cc_module('cl', 'vat').is_valid(vat) if stdnum else True
|
||||||
|
|
||||||
|
def check_vat_co(self, vat):
|
||||||
|
return stdnum.util.get_cc_module('co', 'vat').is_valid(vat) if stdnum else True
|
||||||
|
|
||||||
def default_compact(self, vat):
|
def default_compact(self, vat):
|
||||||
return vat
|
return vat
|
||||||
|
|
||||||
|
|||||||
@@ -122,8 +122,8 @@
|
|||||||
<field name="stop" attrs="{'invisible': True}"/>
|
<field name="stop" attrs="{'invisible': True}"/>
|
||||||
<field name="id" attrs="{'invisible': True}"/>
|
<field name="id" attrs="{'invisible': True}"/>
|
||||||
|
|
||||||
<field name="start_date" string="Starting at" attrs="{'required': [('allday','=',True)], 'invisible': [('allday','=',False)], 'readonly': [('id', '!=', False), ('recurrency','=',True)]}"/>
|
<field name="start_date" string="Starting at" attrs="{'required': [('allday','=',True)], 'invisible': [('allday','=',False)], 'readonly': [('id', '!=', False), ('recurrency','=',True)]}" force_save="1"/>
|
||||||
<field name="stop_date" string="Ending at" attrs="{'required': [('allday','=',True)],'invisible': [('allday','=',False)], 'readonly': [('id', '!=', False), ('recurrency','=',True)]}"/>
|
<field name="stop_date" string="Ending at" attrs="{'required': [('allday','=',True)],'invisible': [('allday','=',False)], 'readonly': [('id', '!=', False), ('recurrency','=',True)]}" force_save="1"/>
|
||||||
|
|
||||||
<field name="start_datetime" string="Starting at" attrs="{'required': [('allday','=',False)], 'invisible': [('allday','=',True)], 'readonly': [('id', '!=', False), ('recurrency','=',True)]}"/>
|
<field name="start_datetime" string="Starting at" attrs="{'required': [('allday','=',False)], 'invisible': [('allday','=',True)], 'readonly': [('id', '!=', False), ('recurrency','=',True)]}"/>
|
||||||
<field name="stop_datetime" invisible="1"/>
|
<field name="stop_datetime" invisible="1"/>
|
||||||
@@ -132,7 +132,7 @@
|
|||||||
<field name="duration" widget="float_time" string="Duration" class="oe_inline" attrs="{'readonly': [('id', '!=', False), ('recurrency','=',True)]}"/>
|
<field name="duration" widget="float_time" string="Duration" class="oe_inline" attrs="{'readonly': [('id', '!=', False), ('recurrency','=',True)]}"/>
|
||||||
<span> hours</span>
|
<span> hours</span>
|
||||||
</div>
|
</div>
|
||||||
<field name="allday" attrs="{'readonly': [('id', '!=', False), ('recurrency','=',True)]}"/>
|
<field name="allday" attrs="{'readonly': [('id', '!=', False), ('recurrency','=',True)]}" force_save="1"/>
|
||||||
</group>
|
</group>
|
||||||
<group>
|
<group>
|
||||||
<field name="categ_ids" widget="many2many_tags" options="
|
<field name="categ_ids" widget="many2many_tags" options="
|
||||||
|
|||||||
@@ -391,7 +391,7 @@
|
|||||||
<div t-attf-class="#{kanban_color(record.color.raw_value)} oe_kanban_global_click">
|
<div t-attf-class="#{kanban_color(record.color.raw_value)} oe_kanban_global_click">
|
||||||
<div class="o_dropdown_kanban dropdown">
|
<div class="o_dropdown_kanban dropdown">
|
||||||
|
|
||||||
<a class="dropdown-toggle o-no-caret btn" role="button" data-toggle="dropdown" href="#" aria-label="Dropdown menu" title="Dropdown menu">
|
<a class="dropdown-toggle o-no-caret btn" role="button" data-toggle="dropdown" data-display="static" href="#" aria-label="Dropdown menu" title="Dropdown menu">
|
||||||
<span class="fa fa-ellipsis-v"/>
|
<span class="fa fa-ellipsis-v"/>
|
||||||
</a>
|
</a>
|
||||||
<div class="dropdown-menu" role="menu">
|
<div class="dropdown-menu" role="menu">
|
||||||
|
|||||||
@@ -318,7 +318,7 @@
|
|||||||
<div t-attf-class="oe_kanban_color_#{kanban_getcolor(record.color.raw_value)} oe_kanban_card oe_kanban_global_click">
|
<div t-attf-class="oe_kanban_color_#{kanban_getcolor(record.color.raw_value)} oe_kanban_card oe_kanban_global_click">
|
||||||
<div class="o_dropdown_kanban dropdown">
|
<div class="o_dropdown_kanban dropdown">
|
||||||
|
|
||||||
<a class="dropdown-toggle o-no-caret btn" role="button" data-toggle="dropdown" href="#" aria-label="Dropdown menu" title="Dropdown menu">
|
<a class="dropdown-toggle o-no-caret btn" role="button" data-toggle="dropdown" data-display="static" href="#" aria-label="Dropdown menu" title="Dropdown menu">
|
||||||
<span class="fa fa-ellipsis-v"/>
|
<span class="fa fa-ellipsis-v"/>
|
||||||
</a>
|
</a>
|
||||||
<div class="dropdown-menu" role="menu">
|
<div class="dropdown-menu" role="menu">
|
||||||
|
|||||||
@@ -192,7 +192,7 @@
|
|||||||
<t t-name="kanban-box">
|
<t t-name="kanban-box">
|
||||||
<div class="oe_kanban_card oe_kanban_global_click">
|
<div class="oe_kanban_card oe_kanban_global_click">
|
||||||
<div class="o_dropdown_kanban dropdown" groups="base.group_user">
|
<div class="o_dropdown_kanban dropdown" groups="base.group_user">
|
||||||
<a class="dropdown-toggle o-no-caret btn" role="button" data-toggle="dropdown" href="#" aria-label="Dropdown menu" title="Dropdown menu">
|
<a class="dropdown-toggle o-no-caret btn" role="button" data-toggle="dropdown" data-display="static" href="#" aria-label="Dropdown menu" title="Dropdown menu">
|
||||||
<span class="fa fa-ellipsis-v"/>
|
<span class="fa fa-ellipsis-v"/>
|
||||||
</a>
|
</a>
|
||||||
<div class="dropdown-menu" role="menu">
|
<div class="dropdown-menu" role="menu">
|
||||||
|
|||||||
@@ -893,24 +893,21 @@ class Escpos:
|
|||||||
self._raw(PAPER_FULL_CUT)
|
self._raw(PAPER_FULL_CUT)
|
||||||
|
|
||||||
|
|
||||||
def cashdraw(self, pin, tries=5):
|
def cashdraw(self, pin):
|
||||||
""" Send pulse to kick the cash drawer
|
""" Send pulse to kick the cash drawer
|
||||||
|
|
||||||
With some printers the drawer will not open after one pulse, for this reason we will check
|
For some reason, with some printers (ex: Epson TM-m30), the cash drawer
|
||||||
the drawer status up to 'tries' times and send a new pulse if the drawer is still closed. If the
|
only opens 50% of the time if you just send the pulse. But if you read
|
||||||
drawer status is open, we will stop sending pulses.
|
the status afterwards, it opens all the time.
|
||||||
"""
|
"""
|
||||||
for i in range(tries):
|
if pin == 2:
|
||||||
if pin == 2:
|
self._raw(CD_KICK_2)
|
||||||
self._raw(CD_KICK_2)
|
elif pin == 5:
|
||||||
elif pin == 5:
|
self._raw(CD_KICK_5)
|
||||||
self._raw(CD_KICK_5)
|
else:
|
||||||
else:
|
raise CashDrawerError()
|
||||||
raise CashDrawerError()
|
|
||||||
|
|
||||||
if i != tries - 1 and not self.get_printer_status()['printer']['drawer_pin_high']:
|
|
||||||
break
|
|
||||||
|
|
||||||
|
self.get_printer_status()
|
||||||
|
|
||||||
def hw(self, hw):
|
def hw(self, hw):
|
||||||
""" Hardware operations """
|
""" Hardware operations """
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import uuid
|
|||||||
import werkzeug.urls
|
import werkzeug.urls
|
||||||
import requests
|
import requests
|
||||||
|
|
||||||
from odoo import api, fields, models, exceptions
|
from odoo import api, fields, models, exceptions, _
|
||||||
from odoo.tools import pycompat
|
from odoo.tools import pycompat
|
||||||
|
|
||||||
_logger = logging.getLogger(__name__)
|
_logger = logging.getLogger(__name__)
|
||||||
@@ -68,7 +68,9 @@ def jsonrpc(url, method='call', params=None, timeout=15):
|
|||||||
raise e
|
raise e
|
||||||
return response.get('result')
|
return response.get('result')
|
||||||
except (ValueError, requests.exceptions.ConnectionError, requests.exceptions.MissingSchema, requests.exceptions.Timeout, requests.exceptions.HTTPError) as e:
|
except (ValueError, requests.exceptions.ConnectionError, requests.exceptions.MissingSchema, requests.exceptions.Timeout, requests.exceptions.HTTPError) as e:
|
||||||
raise exceptions.AccessError('The url that this service requested returned an error. Please contact the author of the app. The url it tried to contact was ' + url)
|
raise exceptions.AccessError(
|
||||||
|
_('The url that this service requested returned an error. Please contact the author of the app. The url it tried to contact was %s') % url
|
||||||
|
)
|
||||||
|
|
||||||
#----------------------------------------------------------
|
#----------------------------------------------------------
|
||||||
# Helpers for proxy
|
# Helpers for proxy
|
||||||
|
|||||||
@@ -828,6 +828,17 @@
|
|||||||
<field name="tax_group_id" ref="ae_tax_group_5"/>
|
<field name="tax_group_id" ref="ae_tax_group_5"/>
|
||||||
<field name="chart_template_id" ref="uae_chart_template_standard"/>
|
<field name="chart_template_id" ref="uae_chart_template_standard"/>
|
||||||
</record>
|
</record>
|
||||||
|
<record id="uae_purchase_tax_5_child_reverse" model="account.tax.template">
|
||||||
|
<field name="name">Reverse Charge TVA +5%(Purchase)</field>
|
||||||
|
<field name="type_tax_use">none</field>
|
||||||
|
<field name="amount">5</field>
|
||||||
|
<field name="amount_type">percent</field>
|
||||||
|
<field name="description">TVA 5%</field>
|
||||||
|
<field name="account_id" ref="uae_account_3726"/>
|
||||||
|
<field name="refund_account_id" ref="uae_account_3726"/>
|
||||||
|
<field name="tax_group_id" ref="ae_tax_group_5"/>
|
||||||
|
<field name="chart_template_id" ref="uae_chart_template_standard"/>
|
||||||
|
</record>
|
||||||
<record id="uae_purchase_tax_reverse_charge" model="account.tax.template">
|
<record id="uae_purchase_tax_reverse_charge" model="account.tax.template">
|
||||||
<field name="name">Reverse Charge Provision</field>
|
<field name="name">Reverse Charge Provision</field>
|
||||||
<field name="type_tax_use">purchase</field>
|
<field name="type_tax_use">purchase</field>
|
||||||
|
|||||||
@@ -95,7 +95,7 @@ class AccountMove(models.Model):
|
|||||||
|
|
||||||
# <1.2.1.8>
|
# <1.2.1.8>
|
||||||
if not seller.l10n_it_tax_system:
|
if not seller.l10n_it_tax_system:
|
||||||
raise UserError("The seller's company must have a tax system.")
|
raise UserError(_("The seller's company must have a tax system."))
|
||||||
|
|
||||||
# <1.2.2>
|
# <1.2.2>
|
||||||
if not seller.street and not seller.street2:
|
if not seller.street and not seller.street2:
|
||||||
@@ -110,7 +110,7 @@ class AccountMove(models.Model):
|
|||||||
raise UserError(_("%s must have a country.") % (seller.display_name))
|
raise UserError(_("%s must have a country.") % (seller.display_name))
|
||||||
|
|
||||||
# <1.4.1>
|
# <1.4.1>
|
||||||
if not buyer.vat and not buyer.l10n_it_codice_fiscale:
|
if not buyer.vat and not buyer.l10n_it_codice_fiscale and buyer.country_id.code != 'IT':
|
||||||
raise UserError(_("The buyer, %s, or his company must have either a VAT number either a tax code (Codice Fiscale).") % (buyer.display_name))
|
raise UserError(_("The buyer, %s, or his company must have either a VAT number either a tax code (Codice Fiscale).") % (buyer.display_name))
|
||||||
|
|
||||||
# <1.4.2>
|
# <1.4.2>
|
||||||
@@ -730,7 +730,7 @@ class AccountTax(models.Model):
|
|||||||
for tax in self:
|
for tax in self:
|
||||||
if tax.l10n_it_has_exoneration:
|
if tax.l10n_it_has_exoneration:
|
||||||
if not tax.l10n_it_kind_exoneration or not tax.l10n_it_law_reference or tax.amount != 0:
|
if not tax.l10n_it_kind_exoneration or not tax.l10n_it_law_reference or tax.amount != 0:
|
||||||
raise ValidationError("If the tax has exoneration, you must enter a kind of exoneration, a law reference and the amount of the tax must be 0.0.")
|
raise ValidationError(_("If the tax has exoneration, you must enter a kind of exoneration, a law reference and the amount of the tax must be 0.0."))
|
||||||
if tax.l10n_it_kind_exoneration == 'N6' and tax.l10n_it_vat_due_date == 'S':
|
if tax.l10n_it_kind_exoneration == 'N6' and tax.l10n_it_vat_due_date == 'S':
|
||||||
raise UserError(_("'Scissione dei pagamenti' is not compatible with exoneration of kind 'N6'"))
|
raise UserError(_("'Scissione dei pagamenti' is not compatible with exoneration of kind 'N6'"))
|
||||||
|
|
||||||
|
|||||||
@@ -35,7 +35,7 @@ class FetchmailServer(models.Model):
|
|||||||
def _check_pec(self):
|
def _check_pec(self):
|
||||||
for record in self:
|
for record in self:
|
||||||
if record.l10n_it_is_pec and record.type != 'imap':
|
if record.l10n_it_is_pec and record.type != 'imap':
|
||||||
raise ValidationError("PEC mail server must be of type IMAP.")
|
raise ValidationError(_("PEC mail server must be of type IMAP."))
|
||||||
|
|
||||||
@api.multi
|
@api.multi
|
||||||
def fetch_mail(self):
|
def fetch_mail(self):
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
# -*- coding:utf-8 -*-
|
# -*- coding:utf-8 -*-
|
||||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||||
|
|
||||||
from odoo import api, fields, models
|
from odoo import api, fields, models, _
|
||||||
from odoo.exceptions import ValidationError
|
from odoo.exceptions import ValidationError
|
||||||
|
|
||||||
TAX_SYSTEM = [
|
TAX_SYSTEM = [
|
||||||
@@ -94,7 +94,7 @@ class ResCompany(models.Model):
|
|||||||
or not record.l10n_it_eco_index_share_capital\
|
or not record.l10n_it_eco_index_share_capital\
|
||||||
or not record.l10n_it_eco_index_sole_shareholder\
|
or not record.l10n_it_eco_index_sole_shareholder\
|
||||||
or not record.l10n_it_eco_index_liquidation_state:
|
or not record.l10n_it_eco_index_liquidation_state:
|
||||||
raise ValidationError("All fields about the Economic and Administrative Index must be completed.")
|
raise ValidationError(_("All fields about the Economic and Administrative Index must be completed."))
|
||||||
|
|
||||||
@api.constrains('l10n_it_has_tax_representative',
|
@api.constrains('l10n_it_has_tax_representative',
|
||||||
'l10n_it_tax_representative_partner_id')
|
'l10n_it_tax_representative_partner_id')
|
||||||
@@ -103,8 +103,8 @@ class ResCompany(models.Model):
|
|||||||
if not record.l10n_it_has_tax_representative:
|
if not record.l10n_it_has_tax_representative:
|
||||||
continue
|
continue
|
||||||
if not record.l10n_it_tax_representative_partner_id:
|
if not record.l10n_it_tax_representative_partner_id:
|
||||||
raise ValidationError("You must select a tax representative.")
|
raise ValidationError(_("You must select a tax representative."))
|
||||||
if not record.l10n_it_tax_representative_partner_id.vat:
|
if not record.l10n_it_tax_representative_partner_id.vat:
|
||||||
raise ValidationError("Your tax representative partner must have a tax number.")
|
raise ValidationError(_("Your tax representative partner must have a tax number."))
|
||||||
if not record.l10n_it_tax_representative_partner_id.country_id:
|
if not record.l10n_it_tax_representative_partner_id.country_id:
|
||||||
raise ValidationError("Your tax representative partner must have a country.")
|
raise ValidationError(_("Your tax representative partner must have a country."))
|
||||||
|
|||||||
@@ -151,23 +151,23 @@ class Channel(models.Model):
|
|||||||
@api.constrains('moderator_ids')
|
@api.constrains('moderator_ids')
|
||||||
def _check_moderator_email(self):
|
def _check_moderator_email(self):
|
||||||
if any(not moderator.email for channel in self for moderator in channel.moderator_ids):
|
if any(not moderator.email for channel in self for moderator in channel.moderator_ids):
|
||||||
raise ValidationError("Moderators must have an email address.")
|
raise ValidationError(_("Moderators must have an email address."))
|
||||||
|
|
||||||
@api.constrains('moderator_ids', 'channel_partner_ids', 'channel_last_seen_partner_ids')
|
@api.constrains('moderator_ids', 'channel_partner_ids', 'channel_last_seen_partner_ids')
|
||||||
def _check_moderator_is_member(self):
|
def _check_moderator_is_member(self):
|
||||||
for channel in self:
|
for channel in self:
|
||||||
if not (channel.mapped('moderator_ids.partner_id') <= channel.sudo().channel_partner_ids):
|
if not (channel.mapped('moderator_ids.partner_id') <= channel.sudo().channel_partner_ids):
|
||||||
raise ValidationError("Moderators should be members of the channel they moderate.")
|
raise ValidationError(_("Moderators should be members of the channel they moderate."))
|
||||||
|
|
||||||
@api.constrains('moderation', 'email_send')
|
@api.constrains('moderation', 'email_send')
|
||||||
def _check_moderation_parameters(self):
|
def _check_moderation_parameters(self):
|
||||||
if any(not channel.email_send and channel.moderation for channel in self):
|
if any(not channel.email_send and channel.moderation for channel in self):
|
||||||
raise ValidationError('Only mailing lists can be moderated.')
|
raise ValidationError(_('Only mailing lists can be moderated.'))
|
||||||
|
|
||||||
@api.constrains('moderator_ids')
|
@api.constrains('moderator_ids')
|
||||||
def _check_moderator_existence(self):
|
def _check_moderator_existence(self):
|
||||||
if any(not channel.moderator_ids for channel in self if channel.moderation):
|
if any(not channel.moderator_ids for channel in self if channel.moderation):
|
||||||
raise ValidationError('Moderated channels must have moderators.')
|
raise ValidationError(_('Moderated channels must have moderators.'))
|
||||||
|
|
||||||
@api.multi
|
@api.multi
|
||||||
def _compute_is_member(self):
|
def _compute_is_member(self):
|
||||||
@@ -254,7 +254,7 @@ class Channel(models.Model):
|
|||||||
# First checks if user tries to modify moderation fields and has not the right to do it.
|
# First checks if user tries to modify moderation fields and has not the right to do it.
|
||||||
if any(key for key in MODERATION_FIELDS if vals.get(key)) and any(self.env.user not in channel.moderator_ids for channel in self if channel.moderation):
|
if any(key for key in MODERATION_FIELDS if vals.get(key)) and any(self.env.user not in channel.moderator_ids for channel in self if channel.moderation):
|
||||||
if not self.env.user.has_group('base.group_system'):
|
if not self.env.user.has_group('base.group_system'):
|
||||||
raise UserError("You do not possess the rights to modify fields related to moderation on one of the channels you are modifying.")
|
raise UserError(_("You do not have the rights to modify fields related to moderation on one of the channels you are modifying."))
|
||||||
|
|
||||||
tools.image_resize_images(vals)
|
tools.image_resize_images(vals)
|
||||||
result = super(Channel, self).write(vals)
|
result = super(Channel, self).write(vals)
|
||||||
@@ -426,9 +426,9 @@ class Channel(models.Model):
|
|||||||
if self.env.user in self.moderator_ids or self.env.user.has_group('base.group_system'):
|
if self.env.user in self.moderator_ids or self.env.user.has_group('base.group_system'):
|
||||||
success = self._send_guidelines(self.channel_partner_ids)
|
success = self._send_guidelines(self.channel_partner_ids)
|
||||||
if not success:
|
if not success:
|
||||||
raise UserError('View "mail.mail_channel_send_guidelines" was not found. No email has been sent. Please contact an administrator to fix this issue.')
|
raise UserError(_('View "mail.mail_channel_send_guidelines" was not found. No email has been sent. Please contact an administrator to fix this issue.'))
|
||||||
else:
|
else:
|
||||||
raise UserError("Only an administrator or a moderator can send guidelines to channel members!")
|
raise UserError(_("Only an administrator or a moderator can send guidelines to channel members!"))
|
||||||
|
|
||||||
@api.multi
|
@api.multi
|
||||||
def _send_guidelines(self, partners):
|
def _send_guidelines(self, partners):
|
||||||
|
|||||||
@@ -937,7 +937,7 @@ class MailThread(models.AbstractModel):
|
|||||||
|
|
||||||
# 1. Check if message is a reply on a thread
|
# 1. Check if message is a reply on a thread
|
||||||
msg_references = [ref for ref in tools.mail_header_msgid_re.findall(thread_references) if 'reply_to' not in ref]
|
msg_references = [ref for ref in tools.mail_header_msgid_re.findall(thread_references) if 'reply_to' not in ref]
|
||||||
mail_messages = MailMessage.sudo().search([('message_id', 'in', msg_references)], limit=1)
|
mail_messages = MailMessage.sudo().search([('message_id', 'in', msg_references)], limit=1, order='id desc, message_id')
|
||||||
is_a_reply = bool(mail_messages)
|
is_a_reply = bool(mail_messages)
|
||||||
|
|
||||||
# 1.1 Handle forward to an alias with a different model: do not consider it as a reply
|
# 1.1 Handle forward to an alias with a different model: do not consider it as a reply
|
||||||
@@ -1365,7 +1365,7 @@ class MailThread(models.AbstractModel):
|
|||||||
# Very unusual situation, be we should be fault-tolerant here
|
# Very unusual situation, be we should be fault-tolerant here
|
||||||
message_id = "<%s@localhost>" % time.time()
|
message_id = "<%s@localhost>" % time.time()
|
||||||
_logger.debug('Parsing Message without message-id, generating a random one: %s', message_id)
|
_logger.debug('Parsing Message without message-id, generating a random one: %s', message_id)
|
||||||
msg_dict['message_id'] = message_id
|
msg_dict['message_id'] = message_id.strip()
|
||||||
|
|
||||||
if message.get('Subject'):
|
if message.get('Subject'):
|
||||||
msg_dict['subject'] = tools.decode_smtp_header(message.get('Subject'))
|
msg_dict['subject'] = tools.decode_smtp_header(message.get('Subject'))
|
||||||
|
|||||||
@@ -97,7 +97,6 @@ var DocumentThread = Thread.extend({
|
|||||||
fetchMessages: function (options) {
|
fetchMessages: function (options) {
|
||||||
var self = this;
|
var self = this;
|
||||||
return this._fetchMessages(options).then(function () {
|
return this._fetchMessages(options).then(function () {
|
||||||
self.call('mail_service', 'markMessagesAsRead', self._messageIDs);
|
|
||||||
return self._messages;
|
return self._messages;
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -85,7 +85,7 @@ var MailManager = AbstractService.extend({
|
|||||||
addMessage: function (data, options) {
|
addMessage: function (data, options) {
|
||||||
options = options || {};
|
options = options || {};
|
||||||
var message = this.getMessage(data.id);
|
var message = this.getMessage(data.id);
|
||||||
var prom = Promise.resolve();
|
var prom;
|
||||||
if (!message) {
|
if (!message) {
|
||||||
prom = this._addNewMessage(data, options);
|
prom = this._addNewMessage(data, options);
|
||||||
} else {
|
} else {
|
||||||
@@ -95,6 +95,7 @@ var MailManager = AbstractService.extend({
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
this._addMessageToThreads(message, options);
|
this._addMessageToThreads(message, options);
|
||||||
|
prom = Promise.resolve(message);
|
||||||
}
|
}
|
||||||
return prom;
|
return prom;
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -106,7 +106,7 @@ MailManager.include({
|
|||||||
// This may happen due to concurrent calls to this method from
|
// This may happen due to concurrent calls to this method from
|
||||||
// messaging menu preview click and handling of longpolling chat
|
// messaging menu preview click and handling of longpolling chat
|
||||||
// window state.
|
// window state.
|
||||||
return;
|
return Promise.resolve();
|
||||||
}
|
}
|
||||||
var threadWindow = this._getThreadWindow(threadID);
|
var threadWindow = this._getThreadWindow(threadID);
|
||||||
var prom = Promise.resolve();
|
var prom = Promise.resolve();
|
||||||
|
|||||||
@@ -108,6 +108,7 @@ var ActivityMenu = Widget.extend({
|
|||||||
*/
|
*/
|
||||||
_onActivityActionClick: function (ev) {
|
_onActivityActionClick: function (ev) {
|
||||||
ev.stopPropagation();
|
ev.stopPropagation();
|
||||||
|
this.$('.dropdown-toggle').dropdown('toggle');
|
||||||
var targetAction = $(ev.currentTarget);
|
var targetAction = $(ev.currentTarget);
|
||||||
var actionXmlid = targetAction.data('action_xmlid');
|
var actionXmlid = targetAction.data('action_xmlid');
|
||||||
if (actionXmlid) {
|
if (actionXmlid) {
|
||||||
|
|||||||
@@ -110,6 +110,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.o_mail_thread {
|
.o_mail_thread {
|
||||||
|
position: relative;
|
||||||
flex: 1 0 0;
|
flex: 1 0 0;
|
||||||
overflow: auto;
|
overflow: auto;
|
||||||
-webkit-overflow-scrolling: touch; // smooth scrolling in iOS app (Safari)
|
-webkit-overflow-scrolling: touch; // smooth scrolling in iOS app (Safari)
|
||||||
|
|||||||
@@ -1144,6 +1144,71 @@ QUnit.test('chatter: access document with some notifs', async function (assert)
|
|||||||
form.destroy();
|
form.destroy();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
QUnit.test('chatter: new message notification from document', async function (assert) {
|
||||||
|
// when receiving new messages from a document that is open, it should
|
||||||
|
// not mark the messages are read
|
||||||
|
assert.expect(4);
|
||||||
|
|
||||||
|
var form = await createView({
|
||||||
|
View: FormView,
|
||||||
|
model: 'partner',
|
||||||
|
data: this.data,
|
||||||
|
services: this.services,
|
||||||
|
arch: '<form string="Partners">' +
|
||||||
|
'<sheet>' +
|
||||||
|
'<field name="foo"/>' +
|
||||||
|
'</sheet>' +
|
||||||
|
'<div class="oe_chatter">' +
|
||||||
|
'<field name="message_ids" widget="mail_thread"/>' +
|
||||||
|
'</div>' +
|
||||||
|
'</form>',
|
||||||
|
res_id: 2,
|
||||||
|
session: {
|
||||||
|
partner_id: 3,
|
||||||
|
},
|
||||||
|
mockRPC: function (route, args) {
|
||||||
|
if (args.method === 'set_message_done') {
|
||||||
|
throw new Error("should not mark message as read");
|
||||||
|
}
|
||||||
|
return this._super.apply(this, arguments);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
var thread = form.call('mail_service', 'getDocumentThread', 'partner', 2);
|
||||||
|
assert.strictEqual(
|
||||||
|
thread.getUnreadCounter(),
|
||||||
|
0,
|
||||||
|
"document thread should have no unread messages initially");
|
||||||
|
assert.containsNone(
|
||||||
|
form,
|
||||||
|
'.o_thread_message',
|
||||||
|
"should have no message in the chatter");
|
||||||
|
|
||||||
|
// Simulate received needaction`` message on this document
|
||||||
|
var message = {
|
||||||
|
author_id: [1, "Me"],
|
||||||
|
body: '<p>test</p>',
|
||||||
|
channel_ids: [],
|
||||||
|
id: 2,
|
||||||
|
model: 'partner',
|
||||||
|
needaction_partner_ids: [3],
|
||||||
|
res_id: 2,
|
||||||
|
};
|
||||||
|
const notifications = [ [['myDB', 'ir.needaction'], message] ];
|
||||||
|
form.call('bus_service', 'trigger', 'notification', notifications);
|
||||||
|
await testUtils.nextTick();
|
||||||
|
assert.strictEqual(
|
||||||
|
thread.getUnreadCounter(),
|
||||||
|
1,
|
||||||
|
"document thread should have one unread message (not marked as read)");
|
||||||
|
assert.containsOnce(
|
||||||
|
form,
|
||||||
|
'.o_thread_message',
|
||||||
|
"should have one message in the chatter");
|
||||||
|
|
||||||
|
form.destroy();
|
||||||
|
});
|
||||||
|
|
||||||
QUnit.test('chatter: post a message and switch in edit mode', async function (assert) {
|
QUnit.test('chatter: post a message and switch in edit mode', async function (assert) {
|
||||||
assert.expect(5);
|
assert.expect(5);
|
||||||
|
|
||||||
|
|||||||
@@ -59,6 +59,7 @@ QUnit.module('Discuss', {
|
|||||||
type: 'integer',
|
type: 'integer',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
records: [],
|
||||||
},
|
},
|
||||||
'res.partner': {
|
'res.partner': {
|
||||||
fields: {
|
fields: {
|
||||||
@@ -1490,6 +1491,70 @@ QUnit.test('custom-named DM conversation', async function (assert) {
|
|||||||
discuss.destroy();
|
discuss.destroy();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
QUnit.test('receive channel message notification then delayed needaction notification', async function (assert) {
|
||||||
|
assert.expect(3);
|
||||||
|
|
||||||
|
const message = {
|
||||||
|
author_id: [5, 'Demo User'],
|
||||||
|
body: '<p>test</p>',
|
||||||
|
channel_ids: [1],
|
||||||
|
id: 100,
|
||||||
|
model: 'mail.channel',
|
||||||
|
needaction: true,
|
||||||
|
needaction_partner_ids: [3],
|
||||||
|
res_id: 1,
|
||||||
|
};
|
||||||
|
this.data.initMessaging = {
|
||||||
|
channel_slots: {
|
||||||
|
channel_channel: [{
|
||||||
|
id: 1,
|
||||||
|
channel_type: 'channel',
|
||||||
|
name: "general",
|
||||||
|
}],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const discuss = await createDiscuss({
|
||||||
|
context: {},
|
||||||
|
params: {},
|
||||||
|
data: this.data,
|
||||||
|
services: this.services,
|
||||||
|
session: {
|
||||||
|
partner_id: 3
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const $inbox = discuss.$('.o_mail_discuss_item[data-thread-id="mailbox_inbox"]');
|
||||||
|
assert.hasClass(
|
||||||
|
$inbox,
|
||||||
|
'o_active',
|
||||||
|
"'Inbox' should be the currently active thread");
|
||||||
|
assert.containsNone(
|
||||||
|
discuss,
|
||||||
|
'.o_thread_message',
|
||||||
|
"inbox should contain no messages initially");
|
||||||
|
|
||||||
|
// simulate new needaction message posted on channnel
|
||||||
|
this.data['mail.message'].records.push(message);
|
||||||
|
// simulate receiving channel notification
|
||||||
|
discuss.call('bus_service', 'trigger', 'notification', [
|
||||||
|
[['myDB', 'mail.channel', 1], message]
|
||||||
|
]);
|
||||||
|
// short delay after receiving needaction notification
|
||||||
|
await testUtils.nextTick();
|
||||||
|
// simulate receiving needaction message notification after a short delay
|
||||||
|
discuss.call('bus_service', 'trigger', 'notification', [
|
||||||
|
[['myDB', 'ir.needaction', 3], message]
|
||||||
|
]);
|
||||||
|
await testUtils.nextTick();
|
||||||
|
assert.containsOnce(
|
||||||
|
discuss,
|
||||||
|
'.o_thread_message',
|
||||||
|
"inbox should contain one message");
|
||||||
|
|
||||||
|
discuss.destroy();
|
||||||
|
});
|
||||||
|
|
||||||
QUnit.test('input not cleared on unresolved message_post rpc', async function (assert) {
|
QUnit.test('input not cleared on unresolved message_post rpc', async function (assert) {
|
||||||
assert.expect(2);
|
assert.expect(2);
|
||||||
|
|
||||||
|
|||||||
@@ -153,7 +153,7 @@ QUnit.test('activity menu widget: activity menu with 3 records', async function
|
|||||||
});
|
});
|
||||||
|
|
||||||
QUnit.test('activity menu widget: activity view icon', async function (assert) {
|
QUnit.test('activity menu widget: activity view icon', async function (assert) {
|
||||||
assert.expect(10);
|
assert.expect(12);
|
||||||
var self = this;
|
var self = this;
|
||||||
var activityMenu = new ActivityMenu();
|
var activityMenu = new ActivityMenu();
|
||||||
testUtils.mock.addMockEnvironment(activityMenu, {
|
testUtils.mock.addMockEnvironment(activityMenu, {
|
||||||
@@ -193,9 +193,15 @@ QUnit.test('activity menu widget: activity view icon', async function (assert) {
|
|||||||
|
|
||||||
// click on the "Issue" activity icon
|
// click on the "Issue" activity icon
|
||||||
await testUtils.dom.click(activityMenu.$('.dropdown-toggle'));
|
await testUtils.dom.click(activityMenu.$('.dropdown-toggle'));
|
||||||
|
assert.hasClass(activityMenu.$('.dropdown-menu'), 'show',
|
||||||
|
"dropdown should be expanded");
|
||||||
|
|
||||||
await testUtils.dom.click(activityMenu.$(".o_mail_activity_action[data-model_name='Issue']"));
|
await testUtils.dom.click(activityMenu.$(".o_mail_activity_action[data-model_name='Issue']"));
|
||||||
|
assert.doesNotHaveClass(activityMenu.$('.dropdown-menu'), 'show',
|
||||||
|
"dropdown should be collapsed");
|
||||||
|
|
||||||
// click on the "Note" activity icon
|
// click on the "Note" activity icon
|
||||||
|
await testUtils.dom.click(activityMenu.$('.dropdown-toggle'));
|
||||||
await testUtils.dom.click(activityMenu.$(".o_mail_activity_action[data-model_name='Note']"));
|
await testUtils.dom.click(activityMenu.$(".o_mail_activity_action[data-model_name='Note']"));
|
||||||
|
|
||||||
assert.verifySteps([
|
assert.verifySteps([
|
||||||
|
|||||||
@@ -920,5 +920,62 @@ QUnit.test('messaging menu widget: expand on thread preview', async function (as
|
|||||||
messagingMenu.destroy();
|
messagingMenu.destroy();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
QUnit.test('messaging menu widget: click twice preview on slow message_fetch should open chat window once', async function (assert) {
|
||||||
|
// This test assumes that a condition for opening chat window is to
|
||||||
|
// successfully fetch messages beforehand.
|
||||||
|
assert.expect(1);
|
||||||
|
|
||||||
|
const self = this;
|
||||||
|
// Used to pause `message_fetch` after opening the messaging menu.
|
||||||
|
// This is necessary `message_fetch` on mailbox_inbox is required to
|
||||||
|
// display the previews.
|
||||||
|
let lockMessageFetch = false;
|
||||||
|
const messageFetchProm = testUtils.makeTestPromise();
|
||||||
|
|
||||||
|
const messagingMenu = new MessagingMenu();
|
||||||
|
testUtils.addMockEnvironment(messagingMenu, {
|
||||||
|
services: this.services,
|
||||||
|
data: this.data,
|
||||||
|
session: { partner_id: 1 },
|
||||||
|
async mockRPC(route, args) {
|
||||||
|
if (args.method === 'message_fetch' && lockMessageFetch) {
|
||||||
|
const _super = this._super.bind(this);
|
||||||
|
await messageFetchProm;
|
||||||
|
return _super(route, args);
|
||||||
|
}
|
||||||
|
if (args.method === 'channel_minimize') {
|
||||||
|
// called to detach thread in chat window
|
||||||
|
// simulate longpolling response with new chat window state
|
||||||
|
const channelInfo = {
|
||||||
|
...self.data['mail.channel'].records[0],
|
||||||
|
is_minimized: true,
|
||||||
|
state: 'open',
|
||||||
|
};
|
||||||
|
const notifications = [ [['myDB', 'res.partner'], channelInfo] ];
|
||||||
|
messagingMenu.call('bus_service', 'trigger', 'notification', notifications);
|
||||||
|
}
|
||||||
|
return this._super(...arguments);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
await messagingMenu.appendTo($('#qunit-fixture'));
|
||||||
|
|
||||||
|
// Opening chat window 1st time from messaging menu (pending from `messageFetchDef`)
|
||||||
|
await testUtils.dom.click(messagingMenu.$('.dropdown-toggle'));
|
||||||
|
lockMessageFetch = true;
|
||||||
|
await testUtils.dom.click(messagingMenu.$('.o_mail_preview'));
|
||||||
|
// Click again on preview to open chat window
|
||||||
|
await testUtils.dom.click(messagingMenu.$('.dropdown-toggle'));
|
||||||
|
await testUtils.dom.click(messagingMenu.$('.o_mail_preview'));
|
||||||
|
messageFetchProm.resolve();
|
||||||
|
await testUtils.nextTick();
|
||||||
|
|
||||||
|
assert.containsOnce(
|
||||||
|
$,
|
||||||
|
'.o_thread_window',
|
||||||
|
"should only display a single chat window");
|
||||||
|
|
||||||
|
messagingMenu.destroy();
|
||||||
|
});
|
||||||
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -48,7 +48,7 @@ class MailResendMessage(models.TransientModel):
|
|||||||
rec['mail_message_id'] = mail_message_id.id
|
rec['mail_message_id'] = mail_message_id.id
|
||||||
rec['partner_ids'] = partner_ids
|
rec['partner_ids'] = partner_ids
|
||||||
else:
|
else:
|
||||||
raise UserError('No message_id found in context')
|
raise UserError(_('No message_id found in context'))
|
||||||
return rec
|
return rec
|
||||||
|
|
||||||
@api.multi
|
@api.multi
|
||||||
|
|||||||
@@ -43,7 +43,7 @@ class MaintenanceEquipmentCategory(models.Model):
|
|||||||
alias_id = fields.Many2one(
|
alias_id = fields.Many2one(
|
||||||
'mail.alias', 'Alias', ondelete='restrict', required=True,
|
'mail.alias', 'Alias', ondelete='restrict', required=True,
|
||||||
help="Email alias for this equipment category. New emails will automatically "
|
help="Email alias for this equipment category. New emails will automatically "
|
||||||
"create new maintenance request for this equipment category.")
|
"create a new equipment under this category.")
|
||||||
fold = fields.Boolean(string='Folded in Maintenance Pipe', compute='_compute_fold', store=True)
|
fold = fields.Boolean(string='Folded in Maintenance Pipe', compute='_compute_fold', store=True)
|
||||||
|
|
||||||
@api.multi
|
@api.multi
|
||||||
|
|||||||
@@ -136,7 +136,7 @@
|
|||||||
<div t-attf-class="oe_kanban_color_#{kanban_getcolor(record.color.raw_value)} oe_kanban_card oe_kanban_global_click oe_semantic_html_override">
|
<div t-attf-class="oe_kanban_color_#{kanban_getcolor(record.color.raw_value)} oe_kanban_card oe_kanban_global_click oe_semantic_html_override">
|
||||||
<div class="o_dropdown_kanban dropdown">
|
<div class="o_dropdown_kanban dropdown">
|
||||||
|
|
||||||
<a role="button" class="dropdown-toggle o-no-caret btn" data-toggle="dropdown" href="#" aria-label="Dropdown menu" title="Dropdown menu">
|
<a role="button" class="dropdown-toggle o-no-caret btn" data-toggle="dropdown" data-display="static" href="#" aria-label="Dropdown menu" title="Dropdown menu">
|
||||||
<span class="fa fa-ellipsis-v"/>
|
<span class="fa fa-ellipsis-v"/>
|
||||||
</a>
|
</a>
|
||||||
<div class="dropdown-menu" role="menu">
|
<div class="dropdown-menu" role="menu">
|
||||||
@@ -435,7 +435,7 @@
|
|||||||
<div t-attf-class="oe_kanban_color_#{kanban_getcolor(record.color.raw_value)} oe_kanban_card oe_kanban_global_click">
|
<div t-attf-class="oe_kanban_color_#{kanban_getcolor(record.color.raw_value)} oe_kanban_card oe_kanban_global_click">
|
||||||
<div class="o_dropdown_kanban dropdown">
|
<div class="o_dropdown_kanban dropdown">
|
||||||
|
|
||||||
<a role="button" class="dropdown-toggle o-no-caret btn" data-toggle="dropdown" href="#" aria-label="Dropdown menu" title="Dropdown menu">
|
<a role="button" class="dropdown-toggle o-no-caret btn" data-toggle="dropdown" data-display="static" href="#" aria-label="Dropdown menu" title="Dropdown menu">
|
||||||
<span class="fa fa-ellipsis-v"/>
|
<span class="fa fa-ellipsis-v"/>
|
||||||
</a>
|
</a>
|
||||||
<div class="dropdown-menu" role="menu">
|
<div class="dropdown-menu" role="menu">
|
||||||
|
|||||||
@@ -252,7 +252,7 @@ class MassMailingContact(models.Model):
|
|||||||
contacts = self.env['mail.mass_mailing.list_contact_rel'].search([('list_id', '=', active_list_id)])
|
contacts = self.env['mail.mass_mailing.list_contact_rel'].search([('list_id', '=', active_list_id)])
|
||||||
return [('id', 'in', [record.contact_id.id for record in contacts if record.opt_out == value])]
|
return [('id', 'in', [record.contact_id.id for record in contacts if record.opt_out == value])]
|
||||||
else:
|
else:
|
||||||
raise UserError('Search opt out cannot be executed without a unique and valid active mailing list context.')
|
raise UserError(_('Search opt out cannot be executed without a unique and valid active mailing list context.'))
|
||||||
|
|
||||||
@api.depends('subscription_list_ids')
|
@api.depends('subscription_list_ids')
|
||||||
def _compute_opt_out(self):
|
def _compute_opt_out(self):
|
||||||
|
|||||||
@@ -386,7 +386,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="o_kanban_card_manage_pane">
|
<div class="o_kanban_card_manage_pane">
|
||||||
<div class="o_kanban_card_manage_section o_dropdown_kanban dropdown">
|
<div class="o_kanban_card_manage_section o_dropdown_kanban dropdown">
|
||||||
<a role="button" class="dropdown-toggle o-no-caret btn" data-toggle="dropdown" href="#" aria-label="Dropdown menu" title="Dropdown menu">
|
<a role="button" class="dropdown-toggle o-no-caret btn" data-toggle="dropdown" data-display="static" href="#" aria-label="Dropdown menu" title="Dropdown menu">
|
||||||
<span class="fa fa-ellipsis-v"/>
|
<span class="fa fa-ellipsis-v"/>
|
||||||
</a>
|
</a>
|
||||||
<div class="dropdown-menu" role="menu">
|
<div class="dropdown-menu" role="menu">
|
||||||
@@ -685,7 +685,7 @@
|
|||||||
<t t-name="kanban-box">
|
<t t-name="kanban-box">
|
||||||
<div t-attf-class="oe_kanban_color_#{kanban_getcolor(record.color.raw_value)} oe_kanban_card oe_kanban_global_click oe_kanban_mass_mailing">
|
<div t-attf-class="oe_kanban_color_#{kanban_getcolor(record.color.raw_value)} oe_kanban_card oe_kanban_global_click oe_kanban_mass_mailing">
|
||||||
<div class="o_dropdown_kanban dropdown">
|
<div class="o_dropdown_kanban dropdown">
|
||||||
<a role="button" class="dropdown-toggle o-no-caret btn" data-toggle="dropdown" href="#" aria-label="Dropdown menu" title="Dropdown menu">
|
<a role="button" class="dropdown-toggle o-no-caret btn" data-toggle="dropdown" href="#" data-display="static" aria-label="Dropdown menu" title="Dropdown menu">
|
||||||
<span class="fa fa-ellipsis-v"/>
|
<span class="fa fa-ellipsis-v"/>
|
||||||
</a>
|
</a>
|
||||||
<div class="dropdown-menu" role="menu">
|
<div class="dropdown-menu" role="menu">
|
||||||
@@ -1041,7 +1041,7 @@
|
|||||||
<div t-attf-class="oe_kanban_color_#{kanban_getcolor(record.color.raw_value)} oe_kanban_card oe_kanban_global_click oe_kanban_mass_mailing_campaign">
|
<div t-attf-class="oe_kanban_color_#{kanban_getcolor(record.color.raw_value)} oe_kanban_card oe_kanban_global_click oe_kanban_mass_mailing_campaign">
|
||||||
<div class="o_dropdown_kanban dropdown">
|
<div class="o_dropdown_kanban dropdown">
|
||||||
|
|
||||||
<a role="button" class="dropdown-toggle o-no-caret btn" data-toggle="dropdown" href="#" aria-label="Dropdown menu" title="Dropdown menu">
|
<a role="button" class="dropdown-toggle o-no-caret btn" data-toggle="dropdown" href="#" data-display="static" aria-label="Dropdown menu" title="Dropdown menu">
|
||||||
<span class="fa fa-ellipsis-v"/>
|
<span class="fa fa-ellipsis-v"/>
|
||||||
</a>
|
</a>
|
||||||
<div class="dropdown-menu" role="menu">
|
<div class="dropdown-menu" role="menu">
|
||||||
|
|||||||
@@ -58,7 +58,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<h2>Mailing Lists</h2>
|
<h2 id="mailing_list_settings_title" invisible="1">Mailing Lists</h2>
|
||||||
<div id="mailing_list" class="row mt16 o_settings_container"/>
|
<div id="mailing_list" class="row mt16 o_settings_container"/>
|
||||||
</div>
|
</div>
|
||||||
</xpath>
|
</xpath>
|
||||||
|
|||||||
@@ -249,6 +249,11 @@ class MrpWorkorder(models.Model):
|
|||||||
for order in (self - late_orders):
|
for order in (self - late_orders):
|
||||||
order.color = 2
|
order.color = 2
|
||||||
|
|
||||||
|
@api.onchange('date_planned_start', 'duration_expected')
|
||||||
|
def _onchange_date_planned_finished(self):
|
||||||
|
if self.date_planned_start and self.duration_expected:
|
||||||
|
self.date_planned_finished = self.date_planned_start + relativedelta(minutes=self.duration_expected)
|
||||||
|
|
||||||
@api.multi
|
@api.multi
|
||||||
def write(self, values):
|
def write(self, values):
|
||||||
if list(values.keys()) != ['time_ids'] and any(workorder.state == 'done' for workorder in self):
|
if list(values.keys()) != ['time_ids'] and any(workorder.state == 'done' for workorder in self):
|
||||||
|
|||||||
@@ -98,7 +98,7 @@
|
|||||||
<field name="tag_ids" widget="many2many_tags" options="{'color_field': 'color'}"/>
|
<field name="tag_ids" widget="many2many_tags" options="{'color_field': 'color'}"/>
|
||||||
|
|
||||||
<div class="o_dropdown_kanban dropdown">
|
<div class="o_dropdown_kanban dropdown">
|
||||||
<a role="button" class="dropdown-toggle o-no-caret btn" data-toggle="dropdown" href="#" aria-label="Dropdown menu" title="Dropdown menu">
|
<a role="button" class="dropdown-toggle o-no-caret btn" data-toggle="dropdown" data-display="static" href="#" aria-label="Dropdown menu" title="Dropdown menu">
|
||||||
<span class="fa fa-ellipsis-v"/>
|
<span class="fa fa-ellipsis-v"/>
|
||||||
</a>
|
</a>
|
||||||
<div class="dropdown-menu" role="menu">
|
<div class="dropdown-menu" role="menu">
|
||||||
|
|||||||
@@ -110,7 +110,7 @@ class ResPartner(models.Model):
|
|||||||
if suggestions:
|
if suggestions:
|
||||||
results = []
|
results = []
|
||||||
for suggestion in suggestions:
|
for suggestion in suggestions:
|
||||||
results.append(suggestion)
|
results.append(self._format_data_company(suggestion))
|
||||||
return results
|
return results
|
||||||
else:
|
else:
|
||||||
return []
|
return []
|
||||||
|
|||||||
@@ -76,7 +76,10 @@ var ScreenWidget = PosBaseWidget.extend({
|
|||||||
barcode_client_action: function(code){
|
barcode_client_action: function(code){
|
||||||
var partner = this.pos.db.get_partner_by_barcode(code.code);
|
var partner = this.pos.db.get_partner_by_barcode(code.code);
|
||||||
if(partner){
|
if(partner){
|
||||||
this.pos.get_order().set_client(partner);
|
if (this.pos.get_order().get_client() !== partner) {
|
||||||
|
this.pos.get_order().set_client(partner);
|
||||||
|
this.pos.get_order().set_pricelist(_.findWhere(this.pos.pricelists, {'id': partner.property_product_pricelist[0]}) || this.pos.default_pricelist);
|
||||||
|
}
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
this.barcode_error_action(code);
|
this.barcode_error_action(code);
|
||||||
|
|||||||
@@ -10,7 +10,7 @@
|
|||||||
<field name="end_date"/>
|
<field name="end_date"/>
|
||||||
</group>
|
</group>
|
||||||
<group>
|
<group>
|
||||||
<field name="pos_config_ids" colspan="4" nolabel="1" />
|
<field name="pos_config_ids" mode="tree" colspan="4" nolabel="1" />
|
||||||
</group>
|
</group>
|
||||||
<footer>
|
<footer>
|
||||||
<button name="generate_report" string="Print" type="object" class="btn-primary"/>
|
<button name="generate_report" string="Print" type="object" class="btn-primary"/>
|
||||||
|
|||||||
@@ -244,7 +244,7 @@ class CustomerPortal(Controller):
|
|||||||
document = request.env[model_name].browse([document_id])
|
document = request.env[model_name].browse([document_id])
|
||||||
document_sudo = document.sudo().exists()
|
document_sudo = document.sudo().exists()
|
||||||
if not document_sudo:
|
if not document_sudo:
|
||||||
raise MissingError("This document does not exist.")
|
raise MissingError(_("This document does not exist."))
|
||||||
try:
|
try:
|
||||||
document.check_access_rights('read')
|
document.check_access_rights('read')
|
||||||
document.check_access_rule('read')
|
document.check_access_rule('read')
|
||||||
@@ -279,12 +279,12 @@ class CustomerPortal(Controller):
|
|||||||
|
|
||||||
def _show_report(self, model, report_type, report_ref, download=False):
|
def _show_report(self, model, report_type, report_ref, download=False):
|
||||||
if report_type not in ('html', 'pdf', 'text'):
|
if report_type not in ('html', 'pdf', 'text'):
|
||||||
raise UserError("Invalid report type: %s" % report_type)
|
raise UserError(_("Invalid report type: %s") % report_type)
|
||||||
|
|
||||||
report_sudo = request.env.ref(report_ref).sudo()
|
report_sudo = request.env.ref(report_ref).sudo()
|
||||||
|
|
||||||
if not isinstance(report_sudo, type(request.env['ir.actions.report'])):
|
if not isinstance(report_sudo, type(request.env['ir.actions.report'])):
|
||||||
raise UserError("%s is not the reference of a report" % report_ref)
|
raise UserError(_("%s is not the reference of a report") % report_ref)
|
||||||
|
|
||||||
method_name = 'render_qweb_%s' % (report_type)
|
method_name = 'render_qweb_%s' % (report_type)
|
||||||
report = getattr(report_sudo, method_name)([model.id], data={'report_type': report_type})[0]
|
report = getattr(report_sudo, method_name)([model.id], data={'report_type': report_type})[0]
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<odoo>
|
<odoo>
|
||||||
<record id="pos_config_view_form_inherit_pos_mercury" model="ir.ui.view">
|
<record id="res_config_view_form_inherit_pos_mercury" model="ir.ui.view">
|
||||||
<field name="name">pos.config.form.inherit.mercury</field>
|
<field name="name">res.config.form.inherit.mercury</field>
|
||||||
<field name="model">res.config.settings</field>
|
<field name="model">res.config.settings</field>
|
||||||
<field name="inherit_id" ref="point_of_sale.res_config_settings_view_form"/>
|
<field name="inherit_id" ref="point_of_sale.res_config_settings_view_form"/>
|
||||||
<field name="arch" type="xml">
|
<field name="arch" type="xml">
|
||||||
|
|||||||
@@ -1,6 +1,11 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<odoo>
|
<odoo>
|
||||||
<record id="pos_config_view_form_inherit_pos_mercury" model="ir.ui.view">
|
<!-- TODO the inherit for
|
||||||
|
point_of_sale.res_config_settings_view_form in this module
|
||||||
|
uses the pos_config_view_form_inherit_pos_mercury xml id
|
||||||
|
already. It should be renamed to
|
||||||
|
res_config_settings_view_form_inherit_pos_mercury. -->
|
||||||
|
<record id="pos_config_view_form_inherit_pos_mercury_2" model="ir.ui.view">
|
||||||
<field name="name">pos.config.form.view.inherit.mercury</field>
|
<field name="name">pos.config.form.view.inherit.mercury</field>
|
||||||
<field name="model">pos.config</field>
|
<field name="model">pos.config</field>
|
||||||
<field name="inherit_id" ref="point_of_sale.pos_config_view_form"/>
|
<field name="inherit_id" ref="point_of_sale.pos_config_view_form"/>
|
||||||
|
|||||||
@@ -577,7 +577,7 @@
|
|||||||
</small>
|
</small>
|
||||||
</div>
|
</div>
|
||||||
<div class="o_dropdown_kanban dropdown" groups="base.group_user">
|
<div class="o_dropdown_kanban dropdown" groups="base.group_user">
|
||||||
<a role="button" class="dropdown-toggle o-no-caret btn" data-toggle="dropdown" href="#" aria-label="Dropdown menu" title="Dropdown menu">
|
<a role="button" class="dropdown-toggle o-no-caret btn" data-toggle="dropdown" data-display="static" href="#" aria-label="Dropdown menu" title="Dropdown menu">
|
||||||
<span class="fa fa-ellipsis-v"/>
|
<span class="fa fa-ellipsis-v"/>
|
||||||
</a>
|
</a>
|
||||||
<div class="dropdown-menu" role="menu">
|
<div class="dropdown-menu" role="menu">
|
||||||
|
|||||||
@@ -405,6 +405,8 @@ class PurchaseOrder(models.Model):
|
|||||||
'default_type': 'in_invoice',
|
'default_type': 'in_invoice',
|
||||||
'default_purchase_id': self.id,
|
'default_purchase_id': self.id,
|
||||||
}
|
}
|
||||||
|
if self.user_id:
|
||||||
|
result['context']['default_user_id'] = self.user_id.id
|
||||||
# choose the view_mode accordingly
|
# choose the view_mode accordingly
|
||||||
if len(self.invoice_ids) > 1 and not create_bill:
|
if len(self.invoice_ids) > 1 and not create_bill:
|
||||||
result['domain'] = "[('id', 'in', " + str(self.invoice_ids.ids) + ")]"
|
result['domain'] = "[('id', 'in', " + str(self.invoice_ids.ids) + ")]"
|
||||||
|
|||||||
@@ -199,7 +199,8 @@ class ProductionLot(models.Model):
|
|||||||
stock_moves = self.env['stock.move.line'].search([
|
stock_moves = self.env['stock.move.line'].search([
|
||||||
('lot_id', '=', lot.id),
|
('lot_id', '=', lot.id),
|
||||||
('state', '=', 'done')
|
('state', '=', 'done')
|
||||||
]).mapped('move_id').filtered(
|
]).mapped('move_id')
|
||||||
|
stock_moves = stock_moves.search([('id', 'in', stock_moves.ids)]).filtered(
|
||||||
lambda move: move.picking_id.location_id.usage == 'supplier' and move.state == 'done')
|
lambda move: move.picking_id.location_id.usage == 'supplier' and move.state == 'done')
|
||||||
lot.purchase_order_ids = stock_moves.mapped('purchase_line_id.order_id')
|
lot.purchase_order_ids = stock_moves.mapped('purchase_line_id.order_id')
|
||||||
lot.purchase_order_count = len(lot.purchase_order_ids)
|
lot.purchase_order_count = len(lot.purchase_order_ids)
|
||||||
|
|||||||
@@ -314,6 +314,68 @@ class TestCreatePicking(common.TestProductCommon):
|
|||||||
|
|
||||||
self.assertEqual(sum(customer_picking.move_lines.mapped('reserved_availability')), 100.0, 'The total quantity for the customer move should be available and reserved.')
|
self.assertEqual(sum(customer_picking.move_lines.mapped('reserved_availability')), 100.0, 'The total quantity for the customer move should be available and reserved.')
|
||||||
|
|
||||||
|
def test_04_rounding(self):
|
||||||
|
""" We set the Unit(s) rounding to 1.0 and ensure buying 1.2 units in a PO is rounded to 1.0
|
||||||
|
at reception.
|
||||||
|
"""
|
||||||
|
uom_unit = self.env.ref('uom.product_uom_unit')
|
||||||
|
uom_unit.rounding = 1.0
|
||||||
|
|
||||||
|
# buy a dozen
|
||||||
|
po = self.env['purchase.order'].create(self.po_vals)
|
||||||
|
|
||||||
|
po.order_line.product_qty = 1.2
|
||||||
|
po.button_confirm()
|
||||||
|
|
||||||
|
# the move should be 1.0 units
|
||||||
|
move1 = po.picking_ids.move_lines[0]
|
||||||
|
self.assertEqual(move1.product_uom_qty, 1.0)
|
||||||
|
self.assertEqual(move1.product_uom.id, uom_unit.id)
|
||||||
|
self.assertEqual(move1.product_qty, 1.0)
|
||||||
|
|
||||||
|
# edit the so line, buy 2.4 units, the move should now be 2.0 units
|
||||||
|
po.order_line.product_qty = 2.0
|
||||||
|
self.assertEqual(move1.product_uom_qty, 2.0)
|
||||||
|
self.assertEqual(move1.product_uom.id, uom_unit.id)
|
||||||
|
self.assertEqual(move1.product_qty, 2.0)
|
||||||
|
|
||||||
|
# deliver everything
|
||||||
|
move1.quantity_done = 2.0
|
||||||
|
po.picking_ids.button_validate()
|
||||||
|
|
||||||
|
# check the delivered quantity
|
||||||
|
self.assertEqual(po.order_line.qty_received, 2.0)
|
||||||
|
|
||||||
|
def test_05_uom_rounding(self):
|
||||||
|
""" We set the Unit(s) and Dozen(s) rounding to 1.0 and ensure buying 1.3 dozens in a PO is
|
||||||
|
rounded to 1.0 at reception.
|
||||||
|
"""
|
||||||
|
uom_unit = self.env.ref('uom.product_uom_unit')
|
||||||
|
uom_dozen = self.env.ref('uom.product_uom_dozen')
|
||||||
|
uom_unit.rounding = 1.0
|
||||||
|
uom_dozen.rounding = 1.0
|
||||||
|
|
||||||
|
# buy 1.3 dozen
|
||||||
|
po = self.env['purchase.order'].create(self.po_vals)
|
||||||
|
|
||||||
|
po.order_line.product_qty = 1.3
|
||||||
|
po.order_line.product_uom = uom_dozen.id
|
||||||
|
po.button_confirm()
|
||||||
|
|
||||||
|
# the move should be 16.0 units
|
||||||
|
move1 = po.picking_ids.move_lines[0]
|
||||||
|
self.assertEqual(move1.product_uom_qty, 16.0)
|
||||||
|
self.assertEqual(move1.product_uom.id, uom_unit.id)
|
||||||
|
self.assertEqual(move1.product_qty, 16.0)
|
||||||
|
|
||||||
|
# force the propagation of the uom, buy 2.6 dozens, the move 2 should have 2 dozens
|
||||||
|
self.env['ir.config_parameter'].sudo().set_param('stock.propagate_uom', '1')
|
||||||
|
po.order_line.product_qty = 2.6
|
||||||
|
move2 = po.picking_ids.move_lines.filtered(lambda m: m.product_uom.id == uom_dozen.id)
|
||||||
|
self.assertEqual(move2.product_uom_qty, 2)
|
||||||
|
self.assertEqual(move2.product_uom.id, uom_dozen.id)
|
||||||
|
self.assertEqual(move2.product_qty, 24)
|
||||||
|
|
||||||
def create_delivery_order(self, propagate_date, propagate_date_minimum_delta):
|
def create_delivery_order(self, propagate_date, propagate_date_minimum_delta):
|
||||||
stock_location = self.env['ir.model.data'].xmlid_to_object('stock.stock_location_stock')
|
stock_location = self.env['ir.model.data'].xmlid_to_object('stock.stock_location_stock')
|
||||||
customer_location = self.env['ir.model.data'].xmlid_to_object('stock.stock_location_customers')
|
customer_location = self.env['ir.model.data'].xmlid_to_object('stock.stock_location_customers')
|
||||||
|
|||||||
@@ -1083,7 +1083,7 @@ class SaleOrderLine(models.Model):
|
|||||||
@api.multi
|
@api.multi
|
||||||
def write(self, values):
|
def write(self, values):
|
||||||
if 'display_type' in values and self.filtered(lambda line: line.display_type != values.get('display_type')):
|
if 'display_type' in values and self.filtered(lambda line: line.display_type != values.get('display_type')):
|
||||||
raise UserError("You cannot change the type of a sale order line. Instead you should delete the current line and create a new line of the proper type.")
|
raise UserError(_("You cannot change the type of a sale order line. Instead you should delete the current line and create a new line of the proper type."))
|
||||||
|
|
||||||
if 'product_uom_qty' in values:
|
if 'product_uom_qty' in values:
|
||||||
precision = self.env['decimal.precision'].precision_get('Product Unit of Measure')
|
precision = self.env['decimal.precision'].precision_get('Product Unit of Measure')
|
||||||
|
|||||||
@@ -305,14 +305,14 @@ class SaleOrder(models.Model):
|
|||||||
# Check commit 6bb42904a03 for next if/else
|
# Check commit 6bb42904a03 for next if/else
|
||||||
# Remove reward line if price or qty equal to 0
|
# Remove reward line if price or qty equal to 0
|
||||||
if values['product_uom_qty'] and values['price_unit']:
|
if values['product_uom_qty'] and values['price_unit']:
|
||||||
order.write({'order_line': [(1, line.id, values) for line in lines]})
|
lines.write(values)
|
||||||
else:
|
else:
|
||||||
if program.reward_type != 'free_shipping':
|
if program.reward_type != 'free_shipping':
|
||||||
# Can't remove the lines directly as we might be in a recordset loop
|
# Can't remove the lines directly as we might be in a recordset loop
|
||||||
lines_to_remove += lines
|
lines_to_remove += lines
|
||||||
else:
|
else:
|
||||||
values.update(price_unit=0.0)
|
values.update(price_unit=0.0)
|
||||||
order.write({'order_line': [(1, line.id, values) for line in lines]})
|
lines.write(values)
|
||||||
return lines_to_remove
|
return lines_to_remove
|
||||||
|
|
||||||
self.ensure_one()
|
self.ensure_one()
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||||
|
|
||||||
from odoo import api, fields, models
|
from odoo import api, fields, models, _
|
||||||
from odoo.addons import decimal_precision as dp
|
from odoo.addons import decimal_precision as dp
|
||||||
from odoo.exceptions import UserError
|
from odoo.exceptions import UserError
|
||||||
|
|
||||||
@@ -36,7 +36,7 @@ class SaleOrderTemplate(models.Model):
|
|||||||
template_id = self.env['ir.default'].get('sale.order', 'sale_order_template_id')
|
template_id = self.env['ir.default'].get('sale.order', 'sale_order_template_id')
|
||||||
for template in self:
|
for template in self:
|
||||||
if template_id and template_id == template.id:
|
if template_id and template_id == template.id:
|
||||||
raise UserError('Before archiving "%s" please select another default template in the settings.' % template.name)
|
raise UserError(_('Before archiving "%s" please select another default template in the settings.') % template.name)
|
||||||
return super(SaleOrderTemplate, self).write(vals)
|
return super(SaleOrderTemplate, self).write(vals)
|
||||||
|
|
||||||
|
|
||||||
@@ -87,7 +87,7 @@ class SaleOrderTemplateLine(models.Model):
|
|||||||
@api.multi
|
@api.multi
|
||||||
def write(self, values):
|
def write(self, values):
|
||||||
if 'display_type' in values and self.filtered(lambda line: line.display_type != values.get('display_type')):
|
if 'display_type' in values and self.filtered(lambda line: line.display_type != values.get('display_type')):
|
||||||
raise UserError("You cannot change the type of a sale quote line. Instead you should delete the current line and create a new line of the proper type.")
|
raise UserError(_("You cannot change the type of a sale quote line. Instead you should delete the current line and create a new line of the proper type."))
|
||||||
return super(SaleOrderTemplateLine, self).write(values)
|
return super(SaleOrderTemplateLine, self).write(values)
|
||||||
|
|
||||||
_sql_constraints = [
|
_sql_constraints = [
|
||||||
|
|||||||
@@ -98,12 +98,13 @@
|
|||||||
</field>
|
</field>
|
||||||
</record>
|
</record>
|
||||||
|
|
||||||
|
<!-- TODO remove me after saas-12.4 to be replaced by sale.prod_config_main -->
|
||||||
<menuitem id="menu_product_settings" name="Products" sequence="80"
|
<menuitem id="menu_product_settings" name="Products" sequence="80"
|
||||||
parent="sale.menu_sale_config"/>
|
parent="sale.menu_sale_config"/>
|
||||||
<menuitem id="menu_product_attribute_action"
|
<menuitem id="menu_product_attribute_action"
|
||||||
action="product.attribute_action"
|
action="product.attribute_action"
|
||||||
parent="menu_product_settings" groups="product.group_product_variant" sequence="1"/>
|
parent="sale.prod_config_main" groups="product.group_product_variant" sequence="1"/>
|
||||||
<menuitem id="menu_catalog_variants_action"
|
<menuitem id="menu_catalog_variants_action"
|
||||||
action="product.variants_action"
|
action="product.variants_action"
|
||||||
parent="menu_product_settings" groups="product.group_product_variant" sequence="2"/>
|
parent="sale.prod_config_main" groups="product.group_product_variant" sequence="2"/>
|
||||||
</odoo>
|
</odoo>
|
||||||
|
|||||||
@@ -123,7 +123,8 @@ class ProductionLot(models.Model):
|
|||||||
stock_moves = self.env['stock.move.line'].search([
|
stock_moves = self.env['stock.move.line'].search([
|
||||||
('lot_id', '=', lot.id),
|
('lot_id', '=', lot.id),
|
||||||
('state', '=', 'done')
|
('state', '=', 'done')
|
||||||
]).mapped('move_id').filtered(
|
]).mapped('move_id')
|
||||||
|
stock_moves = stock_moves.search([('id', 'in', stock_moves.ids)]).filtered(
|
||||||
lambda move: move.picking_id.location_dest_id.usage == 'customer' and move.state == 'done')
|
lambda move: move.picking_id.location_dest_id.usage == 'customer' and move.state == 'done')
|
||||||
lot.sale_order_ids = stock_moves.mapped('sale_line_id.order_id')
|
lot.sale_order_ids = stock_moves.mapped('sale_line_id.order_id')
|
||||||
lot.sale_order_count = len(lot.sale_order_ids)
|
lot.sale_order_count = len(lot.sale_order_ids)
|
||||||
|
|||||||
@@ -63,7 +63,7 @@ class AccountMoveLine(models.Model):
|
|||||||
sale_line_delivery = line.sale_line_ids.filtered(lambda sol: sol.product_id.invoice_policy == 'delivery' and sol.product_id.service_type == 'timesheet')
|
sale_line_delivery = line.sale_line_ids.filtered(lambda sol: sol.product_id.invoice_policy == 'delivery' and sol.product_id.service_type == 'timesheet')
|
||||||
if sale_line_delivery:
|
if sale_line_delivery:
|
||||||
domain = self._timesheet_domain_get_invoiced_lines(sale_line_delivery)
|
domain = self._timesheet_domain_get_invoiced_lines(sale_line_delivery)
|
||||||
timesheets = self.env['account.analytic.line'].search(domain)
|
timesheets = self.env['account.analytic.line'].search(domain).sudo()
|
||||||
timesheets.write({
|
timesheets.write({
|
||||||
'timesheet_invoice_id': line.move_id.id,
|
'timesheet_invoice_id': line.move_id.id,
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -90,7 +90,8 @@ class SaleOrder(models.Model):
|
|||||||
|
|
||||||
task_projects = self.tasks_ids.mapped('project_id')
|
task_projects = self.tasks_ids.mapped('project_id')
|
||||||
if len(task_projects) == 1 and len(self.tasks_ids) > 1: # redirect to task of the project (with kanban stage, ...)
|
if len(task_projects) == 1 and len(self.tasks_ids) > 1: # redirect to task of the project (with kanban stage, ...)
|
||||||
action = self.env.ref('project.act_project_project_2_project_task_all').read()[0]
|
action = self.with_context(active_id=task_projects.id).env.ref(
|
||||||
|
'project.act_project_project_2_project_task_all').read()[0]
|
||||||
if action.get('context'):
|
if action.get('context'):
|
||||||
eval_context = self.env['ir.actions.actions']._get_eval_context()
|
eval_context = self.env['ir.actions.actions']._get_eval_context()
|
||||||
eval_context.update({'active_id': task_projects.id})
|
eval_context.update({'active_id': task_projects.id})
|
||||||
|
|||||||
@@ -751,7 +751,9 @@ class UoM(models.Model):
|
|||||||
procurement_uom = self
|
procurement_uom = self
|
||||||
computed_qty = qty
|
computed_qty = qty
|
||||||
get_param = self.env['ir.config_parameter'].sudo().get_param
|
get_param = self.env['ir.config_parameter'].sudo().get_param
|
||||||
if procurement_uom.id != quant_uom.id and get_param('stock.propagate_uom') != '1':
|
if get_param('stock.propagate_uom') != '1':
|
||||||
computed_qty = self._compute_quantity(qty, quant_uom, rounding_method='HALF-UP')
|
computed_qty = self._compute_quantity(qty, quant_uom, rounding_method='HALF-UP')
|
||||||
procurement_uom = quant_uom
|
procurement_uom = quant_uom
|
||||||
|
else:
|
||||||
|
computed_qty = self._compute_quantity(qty, procurement_uom, rounding_method='HALF-UP')
|
||||||
return (computed_qty, procurement_uom)
|
return (computed_qty, procurement_uom)
|
||||||
|
|||||||
@@ -1,8 +1,7 @@
|
|||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||||
|
|
||||||
from odoo import api, fields, models, _
|
from odoo import api, fields, models
|
||||||
from odoo.exceptions import UserError
|
|
||||||
|
|
||||||
|
|
||||||
class ResConfigSettings(models.TransientModel):
|
class ResConfigSettings(models.TransientModel):
|
||||||
@@ -54,13 +53,6 @@ class ResConfigSettings(models.TransientModel):
|
|||||||
def _onchange_group_stock_production_lot(self):
|
def _onchange_group_stock_production_lot(self):
|
||||||
if not self.group_stock_production_lot:
|
if not self.group_stock_production_lot:
|
||||||
self.group_lot_on_delivery_slip = False
|
self.group_lot_on_delivery_slip = False
|
||||||
tracked_products = self.env['product.template'].search([('tracking', 'in', ['lot', 'serial']),])
|
|
||||||
if not self.group_stock_production_lot and tracked_products:
|
|
||||||
names = ", ".join(tracked_products.mapped('display_name') if len(tracked_products) <= 10
|
|
||||||
else tracked_products[:10].mapped('display_name') + ["..."])
|
|
||||||
raise UserError(_("You should not remove the 'lots and serial numbers' "
|
|
||||||
"option while the following products are still tracked by lot "
|
|
||||||
"or serial number:\n %s") % names)
|
|
||||||
|
|
||||||
@api.onchange('group_stock_adv_location')
|
@api.onchange('group_stock_adv_location')
|
||||||
def onchange_adv_location(self):
|
def onchange_adv_location(self):
|
||||||
|
|||||||
@@ -54,7 +54,7 @@ class ProductionLot(models.Model):
|
|||||||
@api.multi
|
@api.multi
|
||||||
def write(self, vals):
|
def write(self, vals):
|
||||||
if 'product_id' in vals and any([vals['product_id'] != lot.product_id.id for lot in self]):
|
if 'product_id' in vals and any([vals['product_id'] != lot.product_id.id for lot in self]):
|
||||||
move_lines = self.env['stock.move.line'].search([('lot_id', 'in', self.ids)])
|
move_lines = self.env['stock.move.line'].search([('lot_id', 'in', self.ids), ('product_id', '!=', vals['product_id'])])
|
||||||
if move_lines:
|
if move_lines:
|
||||||
raise UserError(_(
|
raise UserError(_(
|
||||||
'You are not allowed to change the product linked to a serial or lot number ' +
|
'You are not allowed to change the product linked to a serial or lot number ' +
|
||||||
|
|||||||
@@ -32,7 +32,7 @@
|
|||||||
<span t-field="pick.name"/>
|
<span t-field="pick.name"/>
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<img t-att-src="'/report/barcode/?type=%s&value=%s&width=%s&height=%s' % ('Code128', pick.name, 300, 50)" alt="Barcode"/>
|
<img t-att-src="'/report/barcode/?type=%s&value=%s&width=%s&height=%s' % ('Code128', pick.name, 600, 100)" style="width:300px;height:50px" alt="Barcode"/>
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<span t-field="pick.state"/>
|
<span t-field="pick.state"/>
|
||||||
|
|||||||
@@ -15,12 +15,15 @@
|
|||||||
<h2>
|
<h2>
|
||||||
<span t-esc="user_input.partner_id.name or user_input.email"/>
|
<span t-esc="user_input.partner_id.name or user_input.email"/>
|
||||||
</h2>
|
</h2>
|
||||||
<p>Successfully achieved</p>
|
<!-- You should not print a certificate if failed :) -->
|
||||||
|
<p t-if="user_input.quizz_passed">Successfully achieved</p>
|
||||||
|
<p t-if="not user_input.quizz_passed">Successfully failed</p>
|
||||||
<h3>
|
<h3>
|
||||||
<span t-field="user_input.survey_id.display_name"/>
|
<span t-field="user_input.survey_id.display_name"/>
|
||||||
</h3>
|
</h3>
|
||||||
<p>
|
<p>
|
||||||
<strong>Date of Certification:</strong>
|
<strong t-if="user_input.quizz_passed">Date of Certification:</strong>
|
||||||
|
<strong t-if="not user_input.quizz_passed">Date of Failure:</strong>
|
||||||
<span t-field="user_input.create_date"/>
|
<span t-field="user_input.create_date"/>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -146,7 +146,7 @@
|
|||||||
<div t-name="kanban-box" t-attf-class="oe_kanban_color_#{kanban_getcolor(record.color.raw_value)} oe_kanban_card oe_kanban_global_click o_kanban_card_survey">
|
<div t-name="kanban-box" t-attf-class="oe_kanban_color_#{kanban_getcolor(record.color.raw_value)} oe_kanban_card oe_kanban_global_click o_kanban_card_survey">
|
||||||
<div class="o_dropdown_kanban dropdown" t-if="widget.editable">
|
<div class="o_dropdown_kanban dropdown" t-if="widget.editable">
|
||||||
|
|
||||||
<a role="button" class="dropdown-toggle o-no-caret btn" data-toggle="dropdown" href="#" aria-label="Dropdown menu" title="Dropdown menu">
|
<a role="button" class="dropdown-toggle o-no-caret btn" data-toggle="dropdown" data-display="static" href="#" aria-label="Dropdown menu" title="Dropdown menu">
|
||||||
<span class="fa fa-ellipsis-v"/>
|
<span class="fa fa-ellipsis-v"/>
|
||||||
</a>
|
</a>
|
||||||
<div class="dropdown-menu" role="menu">
|
<div class="dropdown-menu" role="menu">
|
||||||
|
|||||||
@@ -522,7 +522,7 @@ var utils = {
|
|||||||
}
|
}
|
||||||
var normalized_value = value / precision;
|
var normalized_value = value / precision;
|
||||||
var epsilon_magnitude = Math.log(Math.abs(normalized_value))/Math.log(2);
|
var epsilon_magnitude = Math.log(Math.abs(normalized_value))/Math.log(2);
|
||||||
var epsilon = Math.pow(2, epsilon_magnitude - 53);
|
var epsilon = Math.pow(2, epsilon_magnitude - 52);
|
||||||
normalized_value += normalized_value >= 0 ? epsilon : -epsilon;
|
normalized_value += normalized_value >= 0 ? epsilon : -epsilon;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -1362,9 +1362,9 @@ var BasicModel = AbstractModel.extend({
|
|||||||
// apply changes to local data
|
// apply changes to local data
|
||||||
for (var fieldName in changes) {
|
for (var fieldName in changes) {
|
||||||
field = record.fields[fieldName];
|
field = record.fields[fieldName];
|
||||||
if (field.type === 'one2many' || field.type === 'many2many') {
|
if (field && (field.type === 'one2many' || field.type === 'many2many')) {
|
||||||
defs.push(this._applyX2ManyChange(record, fieldName, changes[fieldName], options));
|
defs.push(this._applyX2ManyChange(record, fieldName, changes[fieldName], options));
|
||||||
} else if (field.type === 'many2one' || field.type === 'reference') {
|
} else if (field && (field.type === 'many2one' || field.type === 'reference')) {
|
||||||
defs.push(this._applyX2OneChange(record, fieldName, changes[fieldName]));
|
defs.push(this._applyX2OneChange(record, fieldName, changes[fieldName]));
|
||||||
} else {
|
} else {
|
||||||
record._changes[fieldName] = changes[fieldName];
|
record._changes[fieldName] = changes[fieldName];
|
||||||
@@ -1379,7 +1379,7 @@ var BasicModel = AbstractModel.extend({
|
|||||||
var onChangeFields = []; // the fields that have changed and that have an on_change
|
var onChangeFields = []; // the fields that have changed and that have an on_change
|
||||||
for (var fieldName in changes) {
|
for (var fieldName in changes) {
|
||||||
field = record.fields[fieldName];
|
field = record.fields[fieldName];
|
||||||
if (field.onChange) {
|
if (field && field.onChange) {
|
||||||
var isX2Many = field.type === 'one2many' || field.type === 'many2many';
|
var isX2Many = field.type === 'one2many' || field.type === 'many2many';
|
||||||
if (!isX2Many || (self._isX2ManyValid(record._changes[fieldName] || record.data[fieldName]))) {
|
if (!isX2Many || (self._isX2ManyValid(record._changes[fieldName] || record.data[fieldName]))) {
|
||||||
onChangeFields.push(fieldName);
|
onChangeFields.push(fieldName);
|
||||||
|
|||||||
@@ -91,10 +91,15 @@ KanbanRenderer.include({
|
|||||||
var index = _.findIndex(this.widgets, {db_id: localID});
|
var index = _.findIndex(this.widgets, {db_id: localID});
|
||||||
var $column = this.widgets[index].$el;
|
var $column = this.widgets[index].$el;
|
||||||
var left = $column.css('left');
|
var left = $column.css('left');
|
||||||
|
var right = $column.css('right');
|
||||||
var scrollTop = $column.scrollTop();
|
var scrollTop = $column.scrollTop();
|
||||||
return this._super.apply(this, arguments).then(function () {
|
return this._super.apply(this, arguments).then(function () {
|
||||||
$column = self.widgets[index].$el;
|
$column = self.widgets[index].$el;
|
||||||
$column.css({left: left});
|
if (_t.database.parameters.direction === 'rtl') {
|
||||||
|
$column.css({right: right});
|
||||||
|
} else {
|
||||||
|
$column.css({left: left});
|
||||||
|
}
|
||||||
$column.scrollTop(scrollTop); // required when clicking on 'Load More'
|
$column.scrollTop(scrollTop); // required when clicking on 'Load More'
|
||||||
self._enableSwipe();
|
self._enableSwipe();
|
||||||
});
|
});
|
||||||
@@ -115,19 +120,40 @@ KanbanRenderer.include({
|
|||||||
var self = this;
|
var self = this;
|
||||||
var moveToIndex = this.activeColumnIndex;
|
var moveToIndex = this.activeColumnIndex;
|
||||||
var updateFunc = animate ? 'animate' : 'css';
|
var updateFunc = animate ? 'animate' : 'css';
|
||||||
|
var rtl = _t.database.parameters.direction === 'rtl';
|
||||||
_.each(this.widgets, function (column, index) {
|
_.each(this.widgets, function (column, index) {
|
||||||
var columnID = column.id || column.db_id;
|
var columnID = column.id || column.db_id;
|
||||||
var $column = self.$('.o_kanban_group[data-id="' + columnID + '"]');
|
var $column = self.$('.o_kanban_group[data-id="' + columnID + '"]');
|
||||||
if (index === moveToIndex - 1) {
|
if (index === moveToIndex - 1) {
|
||||||
$column[updateFunc]({left: '-100%'});
|
if (rtl) {
|
||||||
|
$column[updateFunc]({right: '-100%'});
|
||||||
|
} else {
|
||||||
|
$column[updateFunc]({left: '-100%'});
|
||||||
|
}
|
||||||
} else if (index === moveToIndex + 1) {
|
} else if (index === moveToIndex + 1) {
|
||||||
$column[updateFunc]({left: '100%'});
|
if (rtl) {
|
||||||
|
$column[updateFunc]({right: '100%'});
|
||||||
|
} else {
|
||||||
|
$column[updateFunc]({left: '100%'});
|
||||||
|
}
|
||||||
} else if (index === moveToIndex) {
|
} else if (index === moveToIndex) {
|
||||||
$column[updateFunc]({left: '0%'});
|
if (rtl) {
|
||||||
|
$column[updateFunc]({right: '0%'});
|
||||||
|
} else {
|
||||||
|
$column[updateFunc]({left: '0%'});
|
||||||
|
}
|
||||||
} else if (index < moveToIndex) {
|
} else if (index < moveToIndex) {
|
||||||
$column.css({left: '-100%'});
|
if (rtl) {
|
||||||
|
$column.css({right: '-100%'});
|
||||||
|
} else {
|
||||||
|
$column.css({left: '-100%'});
|
||||||
|
}
|
||||||
} else if (index > moveToIndex) {
|
} else if (index > moveToIndex) {
|
||||||
$column.css({left: '100%'});
|
if (rtl) {
|
||||||
|
$column.css({right: '100%'});
|
||||||
|
} else {
|
||||||
|
$column.css({left: '100%'});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -184,6 +210,7 @@ KanbanRenderer.include({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Apply the scroll x on the tabs
|
// Apply the scroll x on the tabs
|
||||||
|
// XXX in case of RTL, should we use scrollRight?
|
||||||
this.$('.o_kanban_mobile_tabs').scrollLeft(scrollToLeft);
|
this.$('.o_kanban_mobile_tabs').scrollLeft(scrollToLeft);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -213,16 +240,17 @@ KanbanRenderer.include({
|
|||||||
*/
|
*/
|
||||||
_enableSwipe: function () {
|
_enableSwipe: function () {
|
||||||
var self = this;
|
var self = this;
|
||||||
|
var step = _t.database.parameters.direction === 'rtl' ? -1 : 1;
|
||||||
this.$el.swipe({
|
this.$el.swipe({
|
||||||
excludedElements: ".o_kanban_mobile_tabs",
|
excludedElements: ".o_kanban_mobile_tabs",
|
||||||
swipeLeft: function () {
|
swipeLeft: function () {
|
||||||
var moveToIndex = self.activeColumnIndex + 1;
|
var moveToIndex = self.activeColumnIndex + step;
|
||||||
if (moveToIndex < self.widgets.length) {
|
if (moveToIndex < self.widgets.length) {
|
||||||
self._moveToGroup(moveToIndex, self.ANIMATE);
|
self._moveToGroup(moveToIndex, self.ANIMATE);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
swipeRight: function () {
|
swipeRight: function () {
|
||||||
var moveToIndex = self.activeColumnIndex - 1;
|
var moveToIndex = self.activeColumnIndex - step;
|
||||||
if (moveToIndex > -1) {
|
if (moveToIndex > -1) {
|
||||||
self._moveToGroup(moveToIndex, self.ANIMATE);
|
self._moveToGroup(moveToIndex, self.ANIMATE);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -502,16 +502,16 @@ ListRenderer.include({
|
|||||||
switch (field.type) {
|
switch (field.type) {
|
||||||
case 'binary': return 1;
|
case 'binary': return 1;
|
||||||
case 'boolean': return '40px';
|
case 'boolean': return '40px';
|
||||||
case 'char': return 1;
|
case 'char': return 2;
|
||||||
case 'date': return '100px';
|
case 'date': return '92px';
|
||||||
case 'datetime': return '150px';
|
case 'datetime': return '146px';
|
||||||
case 'float': return '100px';
|
case 'float': return '92px';
|
||||||
case 'html': return 3;
|
case 'html': return 3;
|
||||||
case 'integer': return '80px';
|
case 'integer': return '74px';
|
||||||
case 'many2many': return 2;
|
case 'many2many': return 2;
|
||||||
case 'many2one': return 2;
|
case 'many2one': return 2;
|
||||||
case 'monetary': return 1.2;
|
case 'monetary': return '104px';
|
||||||
case 'one2many': return 2.5;
|
case 'one2many': return 2;
|
||||||
case 'reference': return 1.5;
|
case 'reference': return 1.5;
|
||||||
case 'selection': return 1.5;
|
case 'selection': return 1.5;
|
||||||
case 'text': return 2.5;
|
case 'text': return 2.5;
|
||||||
|
|||||||
@@ -38,6 +38,9 @@
|
|||||||
&.o_list_many2one {
|
&.o_list_many2one {
|
||||||
white-space: pre-wrap;
|
white-space: pre-wrap;
|
||||||
}
|
}
|
||||||
|
&.o_list_text {
|
||||||
|
word-break: break-word;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
tfoot {
|
tfoot {
|
||||||
|
|||||||
@@ -1,10 +1,12 @@
|
|||||||
odoo.define('web.kanban_mobile_tests', function (require) {
|
odoo.define('web.kanban_mobile_tests', function (require) {
|
||||||
"use strict";
|
"use strict";
|
||||||
|
|
||||||
|
var core = require('web.core');
|
||||||
var KanbanView = require('web.KanbanView');
|
var KanbanView = require('web.KanbanView');
|
||||||
var testUtils = require('web.test_utils');
|
var testUtils = require('web.test_utils');
|
||||||
|
|
||||||
var createView = testUtils.createView;
|
var createView = testUtils.createView;
|
||||||
|
var _t = core._t;
|
||||||
|
|
||||||
QUnit.module('Views', {
|
QUnit.module('Views', {
|
||||||
beforeEach: function () {
|
beforeEach: function () {
|
||||||
@@ -119,6 +121,35 @@ QUnit.module('Views', {
|
|||||||
kanban.destroy();
|
kanban.destroy();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
QUnit.test('mobile grouped rendering in rtl direction', async function (assert) {
|
||||||
|
assert.expect(2);
|
||||||
|
|
||||||
|
var direction = _t.database.parameters.direction;
|
||||||
|
_t.database.parameters.direction = 'rtl';
|
||||||
|
|
||||||
|
var kanban = await createView({
|
||||||
|
View: KanbanView,
|
||||||
|
model: 'partner',
|
||||||
|
data: this.data,
|
||||||
|
arch: '<kanban class="o_kanban_test o_kanban_small_column" on_create="quick_create">' +
|
||||||
|
'<templates><t t-name="kanban-box">' +
|
||||||
|
'<div><field name="foo"/></div>' +
|
||||||
|
'</t></templates>' +
|
||||||
|
'</kanban>',
|
||||||
|
domain: [['product_id', '!=', false]],
|
||||||
|
groupBy: ['product_id'],
|
||||||
|
});
|
||||||
|
|
||||||
|
assert.strictEqual(kanban.$('.o_kanban_group:first')[0].style.right, '0%',
|
||||||
|
"first tab should have 50% right");
|
||||||
|
assert.strictEqual(kanban.$('.o_kanban_group:nth(1)')[0].style.right, '100%',
|
||||||
|
"second tab should have 100% right");
|
||||||
|
|
||||||
|
kanban.destroy();
|
||||||
|
_t.database.parameters.direction = direction;
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
QUnit.test('mobile grouped with undefined column', async function (assert) {
|
QUnit.test('mobile grouped with undefined column', async function (assert) {
|
||||||
assert.expect(5);
|
assert.expect(5);
|
||||||
|
|
||||||
|
|||||||
@@ -1432,10 +1432,10 @@ QUnit.module('Views', {
|
|||||||
QUnit.test('width of some of the fields should be hardcoded', async function (assert) {
|
QUnit.test('width of some of the fields should be hardcoded', async function (assert) {
|
||||||
const assertions = [
|
const assertions = [
|
||||||
{ field: 'bar', expected: 40, type: 'Boolean' },
|
{ field: 'bar', expected: 40, type: 'Boolean' },
|
||||||
{ field: 'int_field', expected: 80, type: 'Integer' },
|
{ field: 'int_field', expected: 74, type: 'Integer' },
|
||||||
{ field: 'qux', expected: 100, type: 'Float' },
|
{ field: 'qux', expected: 92, type: 'Float' },
|
||||||
{ field: 'date', expected: 100, type: 'Date' },
|
{ field: 'date', expected: 92, type: 'Date' },
|
||||||
{ field: 'datetime', expected: 150, type: 'Datetime' },
|
{ field: 'datetime', expected: 146, type: 'Datetime' },
|
||||||
];
|
];
|
||||||
assert.expect(assertions.length + 1);
|
assert.expect(assertions.length + 1);
|
||||||
|
|
||||||
@@ -1497,9 +1497,8 @@ QUnit.module('Views', {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// the width is hardcoded to make sure we have the same condition
|
// the width is hardcoded to make sure we have the same condition
|
||||||
// between debug mode and non debug mode (#qunit-fixture is limited to
|
// between debug mode and non debug mode
|
||||||
// 1000px)
|
list.$el.width('1200px');
|
||||||
list.$el.width('1000px');
|
|
||||||
var startHeight = list.$('.o_data_row:first').height();
|
var startHeight = list.$('.o_data_row:first').height();
|
||||||
|
|
||||||
// start edition of first row
|
// start edition of first row
|
||||||
|
|||||||
@@ -103,7 +103,12 @@ class WebsiteMultiMixin(models.AbstractModel):
|
|||||||
_name = 'website.multi.mixin'
|
_name = 'website.multi.mixin'
|
||||||
_description = 'Multi Website Mixin'
|
_description = 'Multi Website Mixin'
|
||||||
|
|
||||||
website_id = fields.Many2one('website', string='Website', help='Restrict publishing to this website.')
|
website_id = fields.Many2one(
|
||||||
|
"website",
|
||||||
|
string="Website",
|
||||||
|
ondelete="restrict",
|
||||||
|
help="Restrict publishing to this website.",
|
||||||
|
)
|
||||||
|
|
||||||
@api.multi
|
@api.multi
|
||||||
def can_access_from_current_website(self, website_id=False):
|
def can_access_from_current_website(self, website_id=False):
|
||||||
|
|||||||
@@ -170,6 +170,19 @@ class Website(models.Model):
|
|||||||
if 'favicon' in vals:
|
if 'favicon' in vals:
|
||||||
vals['favicon'] = tools.image_process(vals['favicon'], size=(256, 256), crop='center', output_format='ICO')
|
vals['favicon'] = tools.image_process(vals['favicon'], size=(256, 256), crop='center', output_format='ICO')
|
||||||
|
|
||||||
|
@api.multi
|
||||||
|
def unlink(self):
|
||||||
|
# Do not delete invoices, delete what's strictly necessary
|
||||||
|
attachments_to_unlink = self.env['ir.attachment'].search([
|
||||||
|
('website_id', 'in', self.ids),
|
||||||
|
'|', '|',
|
||||||
|
('key', '!=', False), # theme attachment
|
||||||
|
('url', 'ilike', '.custom.'), # customized theme attachment
|
||||||
|
('url', 'ilike', '.assets\\_'),
|
||||||
|
])
|
||||||
|
attachments_to_unlink.unlink()
|
||||||
|
return super(Website, self).unlink()
|
||||||
|
|
||||||
# ----------------------------------------------------------
|
# ----------------------------------------------------------
|
||||||
# Page Management
|
# Page Management
|
||||||
# ----------------------------------------------------------
|
# ----------------------------------------------------------
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ class Menu(models.Model):
|
|||||||
page_id = fields.Many2one('website.page', 'Related Page', ondelete='cascade')
|
page_id = fields.Many2one('website.page', 'Related Page', ondelete='cascade')
|
||||||
new_window = fields.Boolean('New Window')
|
new_window = fields.Boolean('New Window')
|
||||||
sequence = fields.Integer(default=_default_sequence)
|
sequence = fields.Integer(default=_default_sequence)
|
||||||
website_id = fields.Many2one('website', 'Website')
|
website_id = fields.Many2one('website', 'Website', ondelete='cascade')
|
||||||
parent_id = fields.Many2one('website.menu', 'Parent Menu', index=True, ondelete="cascade")
|
parent_id = fields.Many2one('website.menu', 'Parent Menu', index=True, ondelete="cascade")
|
||||||
child_id = fields.One2many('website.menu', 'parent_id', string='Child Menus')
|
child_id = fields.One2many('website.menu', 'parent_id', string='Child Menus')
|
||||||
parent_path = fields.Char(index=True)
|
parent_path = fields.Char(index=True)
|
||||||
|
|||||||
@@ -5,6 +5,57 @@ import odoo
|
|||||||
import odoo.tests
|
import odoo.tests
|
||||||
|
|
||||||
|
|
||||||
|
@odoo.tests.tagged('-at_install', 'post_install')
|
||||||
|
class TestUiCustomizeTheme(odoo.tests.HttpCase):
|
||||||
|
def test_01_attachment_website_unlink(self):
|
||||||
|
''' Some ir.attachment needs to be unlinked when a website is unlink,
|
||||||
|
otherwise some flows will just crash. That's the case when 2 website
|
||||||
|
have their theme color customized. Removing a website will make its
|
||||||
|
customized attachment generic, thus having 2 attachments with the
|
||||||
|
same URL available for other websites, leading to singleton errors
|
||||||
|
(among other).
|
||||||
|
|
||||||
|
But no all attachment should be deleted, eg we don't want to delete
|
||||||
|
a SO or invoice PDF coming from an ecommerce order.
|
||||||
|
'''
|
||||||
|
Website = self.env['website']
|
||||||
|
Page = self.env['website.page']
|
||||||
|
Attachment = self.env['ir.attachment']
|
||||||
|
|
||||||
|
website_default = Website.browse(1)
|
||||||
|
website_test = Website.create({'name': 'Website Test'})
|
||||||
|
|
||||||
|
# simulate attachment state when editing 2 theme through customize
|
||||||
|
custom_url = '/TEST/website/static/src/scss/options/colors/user_theme_color_palette.custom.web.assets_common.scss'
|
||||||
|
scss_attachment = Attachment.create({
|
||||||
|
'name': custom_url,
|
||||||
|
'type': 'binary',
|
||||||
|
'mimetype': 'text/scss',
|
||||||
|
'datas': '',
|
||||||
|
'url': custom_url,
|
||||||
|
'website_id': website_default.id
|
||||||
|
})
|
||||||
|
scss_attachment.copy({'website_id': website_test.id})
|
||||||
|
|
||||||
|
# simulate PDF from ecommerce order
|
||||||
|
# Note: it will only have its website_id flag if the website has a domain
|
||||||
|
# equal to the current URL (fallback or get_current_website())
|
||||||
|
so_attachment = Attachment.create({
|
||||||
|
'name': 'SO036.pdf',
|
||||||
|
'type': 'binary',
|
||||||
|
'mimetype': 'application/pdf',
|
||||||
|
'datas': '',
|
||||||
|
'website_id': website_test.id
|
||||||
|
})
|
||||||
|
|
||||||
|
# avoid sql error on page website_id restrict
|
||||||
|
Page.search([('website_id', '=', website_test.id)]).unlink()
|
||||||
|
website_test.unlink()
|
||||||
|
self.assertEqual(Attachment.search_count([('url', '=', custom_url)]), 1, 'Should not left duplicates when deleting a website')
|
||||||
|
self.assertTrue(so_attachment.exists(), 'Most attachment should not be deleted')
|
||||||
|
self.assertFalse(so_attachment.website_id, 'Website should be removed')
|
||||||
|
|
||||||
|
|
||||||
@odoo.tests.tagged('-at_install', 'post_install')
|
@odoo.tests.tagged('-at_install', 'post_install')
|
||||||
class TestUiHtmlEditor(odoo.tests.HttpCase):
|
class TestUiHtmlEditor(odoo.tests.HttpCase):
|
||||||
def test_html_editor_multiple_templates(self):
|
def test_html_editor_multiple_templates(self):
|
||||||
|
|||||||
@@ -12,12 +12,22 @@ publicWidget.registry.visitor = publicWidget.Widget.extend({
|
|||||||
start: function () {
|
start: function () {
|
||||||
var defs = [this._super.apply(this, arguments)];
|
var defs = [this._super.apply(this, arguments)];
|
||||||
var self = this;
|
var self = this;
|
||||||
|
var $eventList = this.$('.country_events_list');
|
||||||
|
this._originalContent = $eventList[0].outerHTML;
|
||||||
defs.push(this._rpc({route: '/event/get_country_event_list'}).then(function (data) {
|
defs.push(this._rpc({route: '/event/get_country_event_list'}).then(function (data) {
|
||||||
if (data) {
|
if (data) {
|
||||||
self.$('.country_events_list').replaceWith(data);
|
self._$loadedContent = $(data);
|
||||||
|
$eventList.replaceWith(self._$loadedContent);
|
||||||
}
|
}
|
||||||
}));
|
}));
|
||||||
return Promise.all(defs);
|
return Promise.all(defs);
|
||||||
},
|
},
|
||||||
|
/**
|
||||||
|
* @override
|
||||||
|
*/
|
||||||
|
destroy: function () {
|
||||||
|
this._super.apply(this, arguments);
|
||||||
|
this._$loadedContent.replaceWith(this._originalContent);
|
||||||
|
},
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -5,6 +5,9 @@
|
|||||||
<field name="name">res.config.settings.view.form.inherit.website.mass.mailing</field>
|
<field name="name">res.config.settings.view.form.inherit.website.mass.mailing</field>
|
||||||
<field name="model">res.config.settings</field>
|
<field name="model">res.config.settings</field>
|
||||||
<field name="arch" type="xml">
|
<field name="arch" type="xml">
|
||||||
|
<xpath expr="//h2[@id='mailing_list_settings_title']" position="attributes">
|
||||||
|
<attribute name="invisible">0</attribute>
|
||||||
|
</xpath>
|
||||||
<xpath expr="//div[@id='mailing_list']" position="inside">
|
<xpath expr="//div[@id='mailing_list']" position="inside">
|
||||||
<div class="col-lg-6 o_setting_box col-12" title="This adds a new Newsletter Popup snippet to drag & drop on your website pages. It triggers a popup when visitors land on the page. This popup prompts them to enter their email address to join a mailing list.">
|
<div class="col-lg-6 o_setting_box col-12" title="This adds a new Newsletter Popup snippet to drag & drop on your website pages. It triggers a popup when visitors land on the page. This popup prompts them to enter their email address to join a mailing list.">
|
||||||
<div class="o_setting_left_pane">
|
<div class="o_setting_left_pane">
|
||||||
|
|||||||
@@ -6,4 +6,4 @@ from odoo import fields, models
|
|||||||
class PaymentAcquirer(models.Model):
|
class PaymentAcquirer(models.Model):
|
||||||
_inherit = 'payment.acquirer'
|
_inherit = 'payment.acquirer'
|
||||||
|
|
||||||
website_id = fields.Many2one('website')
|
website_id = fields.Many2one('website', ondelete='restrict')
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ class ProductPricelist(models.Model):
|
|||||||
domain = [('company_id', '=', company_id)]
|
domain = [('company_id', '=', company_id)]
|
||||||
return self.env['website'].search(domain, limit=1)
|
return self.env['website'].search(domain, limit=1)
|
||||||
|
|
||||||
website_id = fields.Many2one('website', string="Website", default=_default_website)
|
website_id = fields.Many2one('website', string="Website", ondelete='restrict', default=_default_website)
|
||||||
code = fields.Char(string='E-commerce Promotional Code', groups="base.group_user")
|
code = fields.Char(string='E-commerce Promotional Code', groups="base.group_user")
|
||||||
selectable = fields.Boolean(help="Allow the end user to choose this price list")
|
selectable = fields.Boolean(help="Allow the end user to choose this price list")
|
||||||
|
|
||||||
|
|||||||
@@ -18,14 +18,15 @@ class WebsiteSaleProductComparison(WebsiteSale):
|
|||||||
products = request.env['product.product'].search([('id', 'in', product_ids)])
|
products = request.env['product.product'].search([('id', 'in', product_ids)])
|
||||||
values['products'] = products.with_context(display_default_code=False)
|
values['products'] = products.with_context(display_default_code=False)
|
||||||
|
|
||||||
res = {}
|
res = OrderedDict()
|
||||||
|
attrs = products.mapped('product_tmpl_id.valid_product_template_attribute_line_ids.attribute_id').filtered(lambda x: x.create_variant)
|
||||||
|
for attr in attrs.sorted(lambda att: (att.category_id.sequence, att.sequence)):
|
||||||
|
cat_name = attr.category_id.name or _('Uncategorized')
|
||||||
|
res.setdefault(cat_name, OrderedDict()).setdefault(attr.name, [' - '] * len(products))
|
||||||
for num, product in enumerate(products):
|
for num, product in enumerate(products):
|
||||||
for var in product.product_tmpl_id._get_valid_product_template_attribute_lines():
|
for var in product.product_tmpl_id.valid_product_template_attribute_line_ids.filtered(lambda x: x.attribute_id.create_variant):
|
||||||
cat_name = var.attribute_id.category_id.name or _('Uncategorized')
|
cat_name = var.attribute_id.category_id.name or _('Uncategorized')
|
||||||
att_name = var.attribute_id.name
|
att_name = var.attribute_id.name
|
||||||
if not var.attribute_id.create_variant:
|
|
||||||
continue
|
|
||||||
res.setdefault(cat_name, OrderedDict()).setdefault(att_name, [' - '] * len(products))
|
|
||||||
val = product.attribute_value_ids.filtered(lambda x: x.attribute_id == var.attribute_id)
|
val = product.attribute_value_ids.filtered(lambda x: x.attribute_id == var.attribute_id)
|
||||||
if val:
|
if val:
|
||||||
res[cat_name][att_name][num] = val[0].name
|
res[cat_name][att_name][num] = val[0].name
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ class ProductWishlist(models.Model):
|
|||||||
currency_id = fields.Many2one('res.currency', related='pricelist_id.currency_id', readonly=True)
|
currency_id = fields.Many2one('res.currency', related='pricelist_id.currency_id', readonly=True)
|
||||||
pricelist_id = fields.Many2one('product.pricelist', string='Pricelist', help='Pricelist when added')
|
pricelist_id = fields.Many2one('product.pricelist', string='Pricelist', help='Pricelist when added')
|
||||||
price = fields.Monetary(digits=0, currency_field='currency_id', string='Price', help='Price of the product when it has been added in the wishlist')
|
price = fields.Monetary(digits=0, currency_field='currency_id', string='Price', help='Price of the product when it has been added in the wishlist')
|
||||||
website_id = fields.Many2one('website', required=True)
|
website_id = fields.Many2one('website', ondelete='cascade', required=True)
|
||||||
active = fields.Boolean(default=True, required=True)
|
active = fields.Boolean(default=True, required=True)
|
||||||
|
|
||||||
@api.model
|
@api.model
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ class WebsiteTwitterTweet(models.Model):
|
|||||||
_name = 'website.twitter.tweet'
|
_name = 'website.twitter.tweet'
|
||||||
_description = 'Website Twitter'
|
_description = 'Website Twitter'
|
||||||
|
|
||||||
website_id = fields.Many2one('website', string='Website')
|
website_id = fields.Many2one('website', string='Website', ondelete='cascade')
|
||||||
screen_name = fields.Char(string='Screen Name')
|
screen_name = fields.Char(string='Screen Name')
|
||||||
tweet = fields.Text(string='Tweets')
|
tweet = fields.Text(string='Tweets')
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,11 @@
|
|||||||
|
Ukraine, 2019-08-12
|
||||||
|
|
||||||
|
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,
|
||||||
|
|
||||||
|
Igor Ishchenko igor@cruxlab.com https://github.com/igoris
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
Portugal, 2019-08-09
|
||||||
|
|
||||||
|
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,
|
||||||
|
|
||||||
|
Reinaldo Ramos 42942314+reinaldoramosarxi@users.noreply.github.com https://github.com/reinaldoramosarxi
|
||||||
@@ -458,12 +458,16 @@ template inheritance
|
|||||||
''''''''''''''''''''
|
''''''''''''''''''''
|
||||||
|
|
||||||
Template inheritance is used to alter existing templates in-place, e.g. to
|
Template inheritance is used to alter existing templates in-place, e.g. to
|
||||||
add information to templates created by an other modules.
|
add information to templates created by other modules.
|
||||||
|
|
||||||
Template inheritance is performed via the ``t-extend`` directive which takes
|
Template inheritance is performed via the ``t-extend`` directive which takes
|
||||||
the name of the template to alter as parameter.
|
the name of the template to alter as parameter.
|
||||||
|
|
||||||
The alteration is then performed with any number of ``t-jquery``
|
When ``t-extend`` is combined with ``t-name`` a new template with the given name
|
||||||
|
is created. In this case the extended template is not altered, instead the
|
||||||
|
directives define how to create the new template.
|
||||||
|
|
||||||
|
In both cases the alteration is then performed with any number of ``t-jquery``
|
||||||
sub-directives::
|
sub-directives::
|
||||||
|
|
||||||
<t t-extend="base.template">
|
<t t-extend="base.template">
|
||||||
|
|||||||
@@ -62,6 +62,25 @@ import time
|
|||||||
if hasattr(time, 'tzset'):
|
if hasattr(time, 'tzset'):
|
||||||
time.tzset()
|
time.tzset()
|
||||||
|
|
||||||
|
#----------------------------------------------------------
|
||||||
|
# PyPDF2 hack
|
||||||
|
# ensure that zlib does not throw error -5 when decompressing
|
||||||
|
# because some pdf won't fit into allocated memory
|
||||||
|
# https://docs.python.org/3/library/zlib.html#zlib.decompressobj
|
||||||
|
# ----------------------------------------------------------
|
||||||
|
import PyPDF2
|
||||||
|
|
||||||
|
try:
|
||||||
|
import zlib
|
||||||
|
|
||||||
|
def _decompress(data):
|
||||||
|
zobj = zlib.decompressobj()
|
||||||
|
return zobj.decompress(data)
|
||||||
|
|
||||||
|
PyPDF2.filters.decompress = _decompress
|
||||||
|
except ImportError:
|
||||||
|
pass # no fix required
|
||||||
|
|
||||||
#----------------------------------------------------------
|
#----------------------------------------------------------
|
||||||
# Shortcuts
|
# Shortcuts
|
||||||
#----------------------------------------------------------
|
#----------------------------------------------------------
|
||||||
|
|||||||
@@ -51,6 +51,7 @@ base.lang_my,"Burmese / ဗမာစာ",my_MM,my,"Left-to-Right","[3,3]",".",",
|
|||||||
"base.lang_nb_NO","Norwegian Bokmål / Norsk bokmål","nb_NO","nb_NO","Left-to-Right","[3,0]",","," ","%d. %b %Y","%H:%M:%S","True","1"
|
"base.lang_nb_NO","Norwegian Bokmål / Norsk bokmål","nb_NO","nb_NO","Left-to-Right","[3,0]",","," ","%d. %b %Y","%H:%M:%S","True","1"
|
||||||
"base.lang_fa_IR","Persian / فارس","fa_IR","fa","Right-to-Left","[3,0]",".",",","%Y/%m/%d","%H:%M:%S","True","6"
|
"base.lang_fa_IR","Persian / فارس","fa_IR","fa","Right-to-Left","[3,0]",".",",","%Y/%m/%d","%H:%M:%S","True","6"
|
||||||
"base.lang_pl","Polish / Język polski","pl_PL","pl","Left-to-Right","[]",",",,"%d.%m.%Y","%H:%M:%S","True","1"
|
"base.lang_pl","Polish / Język polski","pl_PL","pl","Left-to-Right","[]",",",,"%d.%m.%Y","%H:%M:%S","True","1"
|
||||||
|
"base.lang_pt_AO","Portuguese (AO) / Português (AO)","pt_AO","pt_AO","Left-to-Right","[]",",",,"%d-%m-%Y","%H:%M:%S","True","1"
|
||||||
"base.lang_pt_BR","Portuguese (BR) / Português (BR)","pt_BR","pt_BR","Left-to-Right","[3,0]",",",".","%d/%m/%Y","%H:%M:%S","True","7"
|
"base.lang_pt_BR","Portuguese (BR) / Português (BR)","pt_BR","pt_BR","Left-to-Right","[3,0]",",",".","%d/%m/%Y","%H:%M:%S","True","7"
|
||||||
"base.lang_pt","Portuguese / Português","pt_PT","pt","Left-to-Right","[]",",",,"%d-%m-%Y","%H:%M:%S","True","1"
|
"base.lang_pt","Portuguese / Português","pt_PT","pt","Left-to-Right","[]",",",,"%d-%m-%Y","%H:%M:%S","True","1"
|
||||||
"base.lang_ro","Romanian / română","ro_RO","ro","Left-to-Right","[3,0]",",",".","%d.%m.%Y","%H:%M:%S","True","1"
|
"base.lang_ro","Romanian / română","ro_RO","ro","Left-to-Right","[3,0]",",",".","%d.%m.%Y","%H:%M:%S","True","1"
|
||||||
|
|||||||
|
@@ -48,8 +48,10 @@ class IrRule(models.Model):
|
|||||||
by the user with the switch company menu. These companies are
|
by the user with the switch company menu. These companies are
|
||||||
filtered and trusted.
|
filtered and trusted.
|
||||||
"""
|
"""
|
||||||
|
# use an empty context for 'user' to make the domain evaluation
|
||||||
|
# independent from the context
|
||||||
return {
|
return {
|
||||||
'user': self.env.user,
|
'user': self.env.user.with_context({}),
|
||||||
'time': time,
|
'time': time,
|
||||||
'company_ids': self.env.companies.ids,
|
'company_ids': self.env.companies.ids,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -883,7 +883,7 @@ class Users(models.Model):
|
|||||||
cfg = self.env['ir.config_parameter'].sudo()
|
cfg = self.env['ir.config_parameter'].sudo()
|
||||||
min_failures = int(cfg.get_param('base.login_cooldown_after', 5))
|
min_failures = int(cfg.get_param('base.login_cooldown_after', 5))
|
||||||
if min_failures == 0:
|
if min_failures == 0:
|
||||||
return True
|
return False
|
||||||
|
|
||||||
delay = int(cfg.get_param('base.login_cooldown_duration', 60))
|
delay = int(cfg.get_param('base.login_cooldown_duration', 60))
|
||||||
return failures >= min_failures and (datetime.datetime.now() - previous) < datetime.timedelta(seconds=delay)
|
return failures >= min_failures and (datetime.datetime.now() - previous) < datetime.timedelta(seconds=delay)
|
||||||
|
|||||||
@@ -27,6 +27,8 @@ class TestFloatPrecision(TransactionCase):
|
|||||||
try_round(0.0049,'0.00') # 0.0049 is closer to 0 than to 0.01, so should round down
|
try_round(0.0049,'0.00') # 0.0049 is closer to 0 than to 0.01, so should round down
|
||||||
try_round(0.005,'0.01') # the rule is to round half away from zero
|
try_round(0.005,'0.01') # the rule is to round half away from zero
|
||||||
try_round(-0.005,'-0.01') # the rule is to round half away from zero
|
try_round(-0.005,'-0.01') # the rule is to round half away from zero
|
||||||
|
try_round(6.6 * 0.175, '1.16') # 6.6 * 0.175 is rounded to 1.15 with epsilon = 53
|
||||||
|
try_round(-6.6 * 0.175, '-1.16')
|
||||||
|
|
||||||
def try_zero(amount, expected):
|
def try_zero(amount, expected):
|
||||||
self.assertEqual(currency.is_zero(amount), expected,
|
self.assertEqual(currency.is_zero(amount), expected,
|
||||||
|
|||||||
@@ -165,7 +165,7 @@
|
|||||||
<div class="oe_module_vignette">
|
<div class="oe_module_vignette">
|
||||||
<t t-set="installed" t-value="record.state.raw_value == 'installed'"/>
|
<t t-set="installed" t-value="record.state.raw_value == 'installed'"/>
|
||||||
<div class="o_dropdown_kanban dropdown" tabindex="-1">
|
<div class="o_dropdown_kanban dropdown" tabindex="-1">
|
||||||
<a class="dropdown-toggle o-no-caret btn" data-toggle="dropdown" href="#" role="button" aria-label="Dropdown menu" title="Dropdown menu">
|
<a class="dropdown-toggle o-no-caret btn" data-toggle="dropdown" data-display="static" href="#" role="button" aria-label="Dropdown menu" title="Dropdown menu">
|
||||||
<span class="fa fa-ellipsis-v"/>
|
<span class="fa fa-ellipsis-v"/>
|
||||||
</a>
|
</a>
|
||||||
<div class="dropdown-menu" role="menu" aria-labelledby="dLabel">
|
<div class="dropdown-menu" role="menu" aria-labelledby="dLabel">
|
||||||
|
|||||||
@@ -2,3 +2,4 @@ id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
|
|||||||
access_test_access_right_some_obj,access_test_access_right_some_obj,model_test_access_right_some_obj,,1,1,1,1
|
access_test_access_right_some_obj,access_test_access_right_some_obj,model_test_access_right_some_obj,,1,1,1,1
|
||||||
access_test_access_right_container,access_test_access_right_container,model_test_access_right_container,,1,1,1,1
|
access_test_access_right_container,access_test_access_right_container,model_test_access_right_container,,1,1,1,1
|
||||||
access_test_access_right_parent,access_test_access_right_parent,model_test_access_right_parent,,1,1,1,1
|
access_test_access_right_parent,access_test_access_right_parent,model_test_access_right_parent,,1,1,1,1
|
||||||
|
access_test_access_right_obj_categ,access_test_access_right_obj_categ,model_test_access_right_obj_categ,,1,1,1,1
|
||||||
|
|||||||
|
@@ -8,6 +8,7 @@ class SomeObj(models.Model):
|
|||||||
_description = 'Object For Test Access Right'
|
_description = 'Object For Test Access Right'
|
||||||
|
|
||||||
val = fields.Integer()
|
val = fields.Integer()
|
||||||
|
categ_id = fields.Many2one('test_access_right.obj_categ')
|
||||||
company_id = fields.Many2one('res.company')
|
company_id = fields.Many2one('res.company')
|
||||||
forbidden = fields.Integer(
|
forbidden = fields.Integer(
|
||||||
groups='test_access_rights.test_group,!base.group_no_one,base.group_user,!base.group_public',
|
groups='test_access_rights.test_group,!base.group_no_one,base.group_user,!base.group_public',
|
||||||
@@ -28,3 +29,14 @@ class Parent(models.Model):
|
|||||||
_inherits = {'test_access_right.some_obj': 'obj_id'}
|
_inherits = {'test_access_right.some_obj': 'obj_id'}
|
||||||
|
|
||||||
obj_id = fields.Many2one('test_access_right.some_obj', required=True, ondelete='restrict')
|
obj_id = fields.Many2one('test_access_right.some_obj', required=True, ondelete='restrict')
|
||||||
|
|
||||||
|
class ObjCateg(models.Model):
|
||||||
|
_name = 'test_access_right.obj_categ'
|
||||||
|
_description = "Context dependent searchable model"
|
||||||
|
|
||||||
|
name = fields.Char(required=True)
|
||||||
|
|
||||||
|
def search(self, args, **kwargs):
|
||||||
|
if self.env.context.get('only_media'):
|
||||||
|
args += [('name', '=', 'Media')]
|
||||||
|
return super(ObjCateg, self).search(args, **kwargs)
|
||||||
|
|||||||
@@ -8,10 +8,11 @@ class TestRules(TransactionCase):
|
|||||||
def setUp(self):
|
def setUp(self):
|
||||||
super(TestRules, self).setUp()
|
super(TestRules, self).setUp()
|
||||||
|
|
||||||
self.id1 = self.env['test_access_right.some_obj']\
|
ObjCateg = self.env['test_access_right.obj_categ']
|
||||||
.create({'val': 1}).id
|
SomeObj = self.env['test_access_right.some_obj']
|
||||||
self.id2 = self.env['test_access_right.some_obj']\
|
self.categ1 = ObjCateg.create({'name': 'Food'}).id
|
||||||
.create({'val': -1}).id
|
self.id1 = SomeObj.create({'val': 1, 'categ_id': self.categ1}).id
|
||||||
|
self.id2 = SomeObj.create({'val': -1, 'categ_id': self.categ1}).id
|
||||||
# create a global rule forbidding access to records with a negative
|
# create a global rule forbidding access to records with a negative
|
||||||
# (or zero) val
|
# (or zero) val
|
||||||
self.env['ir.rule'].create({
|
self.env['ir.rule'].create({
|
||||||
@@ -19,6 +20,13 @@ class TestRules(TransactionCase):
|
|||||||
'model_id': self.browse_ref('test_access_rights.model_test_access_right_some_obj').id,
|
'model_id': self.browse_ref('test_access_rights.model_test_access_right_some_obj').id,
|
||||||
'domain_force': "[('val', '>', 0)]"
|
'domain_force': "[('val', '>', 0)]"
|
||||||
})
|
})
|
||||||
|
# create a global rule that forbid access to records without
|
||||||
|
# categories, the search is part of the test
|
||||||
|
self.env['ir.rule'].create({
|
||||||
|
'name': 'See all categories',
|
||||||
|
'model_id': self.browse_ref('test_access_rights.model_test_access_right_some_obj').id,
|
||||||
|
'domain_force': "[('categ_id', 'in', user.env['test_access_right.obj_categ'].search([]).ids)]"
|
||||||
|
})
|
||||||
|
|
||||||
def test_basic_access(self):
|
def test_basic_access(self):
|
||||||
env = self.env(user=self.browse_ref('base.public_user'))
|
env = self.env(user=self.browse_ref('base.public_user'))
|
||||||
@@ -82,3 +90,25 @@ class TestRules(TransactionCase):
|
|||||||
Model = env['test_access_right.some_obj']
|
Model = env['test_access_right.some_obj']
|
||||||
with self.assertQueryCount(0):
|
with self.assertQueryCount(0):
|
||||||
Model._filter_access_rules('read')
|
Model._filter_access_rules('read')
|
||||||
|
|
||||||
|
def test_no_context_in_ir_rules(self):
|
||||||
|
""" The context should not impact the ir rules. """
|
||||||
|
env = self.env(user=self.browse_ref('base.public_user'))
|
||||||
|
ObjCateg = self.env['test_access_right.obj_categ']
|
||||||
|
SomeObj = self.env['test_access_right.some_obj']
|
||||||
|
|
||||||
|
# validate the effect of context on category search, there are
|
||||||
|
# no existing media category
|
||||||
|
self.assertTrue(ObjCateg.search([]))
|
||||||
|
self.assertFalse(ObjCateg.with_context(only_media=True).search([]))
|
||||||
|
|
||||||
|
# record1 is food and is accessible with an empy context
|
||||||
|
ObjCateg.clear_caches()
|
||||||
|
records = SomeObj.search([('id', '=', self.id1)])
|
||||||
|
self.assertTrue(records)
|
||||||
|
|
||||||
|
# it should also be accessible as the context is not used when
|
||||||
|
# searching for SomeObjs
|
||||||
|
ObjCateg.clear_caches()
|
||||||
|
records = SomeObj.with_context(only_media=True).search([('id', '=', self.id1)])
|
||||||
|
self.assertTrue(records)
|
||||||
|
|||||||
@@ -706,7 +706,8 @@ class ChromeBrowser():
|
|||||||
|
|
||||||
def set_cookie(self, name, value, path, domain):
|
def set_cookie(self, name, value, path, domain):
|
||||||
params = {'name': name, 'value': value, 'path': path, 'domain': domain}
|
params = {'name': name, 'value': value, 'path': path, 'domain': domain}
|
||||||
self._websocket_send('Network.setCookie', params=params)
|
_id = self._websocket_send('Network.setCookie', params=params)
|
||||||
|
return self._websocket_wait_id(_id)
|
||||||
|
|
||||||
def _wait_ready(self, ready_code, timeout=60):
|
def _wait_ready(self, ready_code, timeout=60):
|
||||||
self._logger.info('Evaluate ready code "%s"', ready_code)
|
self._logger.info('Evaluate ready code "%s"', ready_code)
|
||||||
|
|||||||
@@ -72,7 +72,7 @@ def float_round(value, precision_digits=None, precision_rounding=None, rounding_
|
|||||||
normalized_value = value / rounding_factor # normalize
|
normalized_value = value / rounding_factor # normalize
|
||||||
sign = math.copysign(1.0, normalized_value)
|
sign = math.copysign(1.0, normalized_value)
|
||||||
epsilon_magnitude = math.log(abs(normalized_value), 2)
|
epsilon_magnitude = math.log(abs(normalized_value), 2)
|
||||||
epsilon = 2**(epsilon_magnitude-53)
|
epsilon = 2**(epsilon_magnitude-52)
|
||||||
|
|
||||||
# TIE-BREAKING: UP/DOWN (for ceiling[resp. flooring] operations)
|
# TIE-BREAKING: UP/DOWN (for ceiling[resp. flooring] operations)
|
||||||
# When rounding the value up[resp. down], we instead subtract[resp. add] the epsilon value
|
# When rounding the value up[resp. down], we instead subtract[resp. add] the epsilon value
|
||||||
|
|||||||
Reference in New Issue
Block a user