[MERGE] forward port branch saas-12.1 up to d785adba6c
This commit is contained in:
@@ -446,7 +446,6 @@ class AccountBankStatementLine(models.Model):
|
||||
""" Prepare the dict of values to balance the move.
|
||||
|
||||
:param recordset move: the account.move to link the move line
|
||||
:param dict move: a dict of vals of a account.move which will be created later
|
||||
:param float amount: the amount of transaction that wasn't already reconciled
|
||||
"""
|
||||
company_currency = self.journal_id.company_id.currency_id
|
||||
@@ -454,23 +453,19 @@ class AccountBankStatementLine(models.Model):
|
||||
st_line_currency = self.currency_id or statement_currency
|
||||
amount_currency = False
|
||||
st_line_currency_rate = self.currency_id and (self.amount_currency / self.amount) or False
|
||||
if isinstance(move, dict):
|
||||
amount_sum = sum(x[2].get('amount_currency', 0) for x in move['line_ids'])
|
||||
else:
|
||||
amount_sum = sum(x.amount_currency for x in move.line_ids)
|
||||
# We have several use case here to compare the currency and amount currency of counterpart line to balance the move:
|
||||
if st_line_currency != company_currency and st_line_currency == statement_currency:
|
||||
# company in currency A, statement in currency B and transaction in currency B
|
||||
# counterpart line must have currency B and correct amount is inverse of already existing lines
|
||||
amount_currency = -amount_sum
|
||||
amount_currency = -sum([x.amount_currency for x in move.line_ids])
|
||||
elif st_line_currency != company_currency and statement_currency == company_currency:
|
||||
# company in currency A, statement in currency A and transaction in currency B
|
||||
# counterpart line must have currency B and correct amount is inverse of already existing lines
|
||||
amount_currency = -amount_sum
|
||||
amount_currency = -sum([x.amount_currency for x in move.line_ids])
|
||||
elif st_line_currency != company_currency and st_line_currency != statement_currency:
|
||||
# company in currency A, statement in currency B and transaction in currency C
|
||||
# counterpart line must have currency B and use rate between B and C to compute correct amount
|
||||
amount_currency = -amount_sum/st_line_currency_rate
|
||||
amount_currency = -sum([x.amount_currency for x in move.line_ids])/st_line_currency_rate
|
||||
elif st_line_currency == company_currency and statement_currency != company_currency:
|
||||
# company in currency A, statement in currency B and transaction in currency A
|
||||
# counterpart line must have currency B and amount is computed using the rate between A and B
|
||||
@@ -479,8 +474,9 @@ class AccountBankStatementLine(models.Model):
|
||||
# last case is company in currency A, statement in currency A and transaction in currency A
|
||||
# and in this case counterpart line does not need any second currency nor amount_currency
|
||||
|
||||
aml_dict = {
|
||||
return {
|
||||
'name': self.name,
|
||||
'move_id': move.id,
|
||||
'partner_id': self.partner_id and self.partner_id.id or False,
|
||||
'account_id': amount >= 0 \
|
||||
and self.statement_id.journal_id.default_credit_account_id.id \
|
||||
@@ -491,71 +487,22 @@ class AccountBankStatementLine(models.Model):
|
||||
'currency_id': statement_currency != company_currency and statement_currency.id or (st_line_currency != company_currency and st_line_currency.id or False),
|
||||
'amount_currency': amount_currency,
|
||||
}
|
||||
if isinstance(move, self.env['account.move'].__class__):
|
||||
aml_dict['move_id'] = move.id
|
||||
return aml_dict
|
||||
|
||||
@api.multi
|
||||
def fast_counterpart_creation(self):
|
||||
"""This function is called when confirming a bank statement and will allow to automatically process lines without
|
||||
going in the bank reconciliation widget. By setting an account_id on bank statement lines, it will create a journal
|
||||
entry using that account to counterpart the bank account
|
||||
"""
|
||||
payment_list = []
|
||||
move_list = []
|
||||
account_type_receivable = self.env.ref('account.data_account_type_receivable')
|
||||
already_done_stmt_line_ids = [a['statement_line_id'][0] for a in self.env['account.move.line'].read_group([('statement_line_id', 'in', self.ids)], ['statement_line_id'], ['statement_line_id'])]
|
||||
managed_st_line = []
|
||||
for st_line in self:
|
||||
# Technical functionality to automatically reconcile by creating a new move line
|
||||
if st_line.account_id and not st_line.id in already_done_stmt_line_ids:
|
||||
managed_st_line.append(st_line.id)
|
||||
# Create payment vals
|
||||
total = st_line.amount
|
||||
payment_methods = (total > 0) and st_line.journal_id.inbound_payment_method_ids or st_line.journal_id.outbound_payment_method_ids
|
||||
currency = st_line.journal_id.currency_id or st_line.company_id.currency_id
|
||||
partner_type = 'customer' if st_line.account_id.user_type_id == account_type_receivable else 'supplier'
|
||||
payment_list.append({
|
||||
'payment_method_id': payment_methods and payment_methods[0].id or False,
|
||||
'payment_type': total > 0 and 'inbound' or 'outbound',
|
||||
'partner_id': st_line.partner_id.id,
|
||||
'partner_type': partner_type,
|
||||
'journal_id': st_line.statement_id.journal_id.id,
|
||||
'payment_date': st_line.date,
|
||||
'state': 'reconciled',
|
||||
'currency_id': currency.id,
|
||||
'amount': abs(total),
|
||||
'communication': st_line._get_communication(payment_methods[0] if payment_methods else False),
|
||||
'name': st_line.statement_id.name or _("Bank Statement %s") % st_line.date,
|
||||
})
|
||||
|
||||
# Create move and move line vals
|
||||
move_vals = st_line._prepare_reconciliation_move(st_line.statement_id.name)
|
||||
aml_dict = {
|
||||
if st_line.account_id and not st_line.journal_entry_ids.ids:
|
||||
vals = {
|
||||
'name': st_line.name,
|
||||
'debit': st_line.amount < 0 and -st_line.amount or 0.0,
|
||||
'credit': st_line.amount > 0 and st_line.amount or 0.0,
|
||||
'account_id': st_line.account_id.id,
|
||||
'partner_id': st_line.partner_id.id,
|
||||
'statement_line_id': st_line.id,
|
||||
}
|
||||
st_line._prepare_move_line_for_currency(aml_dict, st_line.date or fields.Date.context_today())
|
||||
move_vals['line_ids'] = [(0, 0, aml_dict)]
|
||||
balance_line = self._prepare_reconciliation_move_line(move_vals, st_line.amount)
|
||||
move_vals['line_ids'].append((0, 0, balance_line))
|
||||
move_list.append(move_vals)
|
||||
|
||||
# Creates
|
||||
payment_ids = self.env['account.payment'].create(payment_list)
|
||||
for payment_id, move_vals in zip(payment_ids, move_list):
|
||||
for line in move_vals['line_ids']:
|
||||
line[2]['payment_id'] = payment_id.id
|
||||
move_ids = self.env['account.move'].create(move_list)
|
||||
move_ids.post()
|
||||
|
||||
for move, st_line, payment in zip(move_ids, self.browse(managed_st_line), payment_ids):
|
||||
st_line.write({'move_name': move.name})
|
||||
payment.write({'payment_reference': move.name})
|
||||
st_line.process_reconciliation(new_aml_dicts=[vals])
|
||||
|
||||
def _get_communication(self, payment_method_id):
|
||||
return self.name or ''
|
||||
@@ -681,6 +628,8 @@ class AccountBankStatementLine(models.Model):
|
||||
# Create move line(s). Either matching an existing journal entry (eg. invoice), in which
|
||||
# case we reconcile the existing and the new move lines together, or being a write-off.
|
||||
if counterpart_aml_dicts or new_aml_dicts:
|
||||
st_line_currency = self.currency_id or statement_currency
|
||||
st_line_currency_rate = self.currency_id and (self.amount_currency / self.amount) or False
|
||||
|
||||
# Create the move
|
||||
self.sequence = self.statement_id.line_ids.ids.index(self.id) + 1
|
||||
@@ -703,12 +652,32 @@ class AccountBankStatementLine(models.Model):
|
||||
|
||||
# Complete dicts to create both counterpart move lines and write-offs
|
||||
to_create = (counterpart_aml_dicts + new_aml_dicts)
|
||||
company = self.company_id
|
||||
date = self.date or fields.Date.today()
|
||||
for aml_dict in to_create:
|
||||
aml_dict['move_id'] = move.id
|
||||
aml_dict['partner_id'] = self.partner_id.id
|
||||
aml_dict['statement_line_id'] = self.id
|
||||
self._prepare_move_line_for_currency(aml_dict, date)
|
||||
if st_line_currency.id != company_currency.id:
|
||||
aml_dict['amount_currency'] = aml_dict['debit'] - aml_dict['credit']
|
||||
aml_dict['currency_id'] = st_line_currency.id
|
||||
if self.currency_id and statement_currency.id == company_currency.id and st_line_currency_rate:
|
||||
# Statement is in company currency but the transaction is in foreign currency
|
||||
aml_dict['debit'] = company_currency.round(aml_dict['debit'] / st_line_currency_rate)
|
||||
aml_dict['credit'] = company_currency.round(aml_dict['credit'] / st_line_currency_rate)
|
||||
elif self.currency_id and st_line_currency_rate:
|
||||
# Statement is in foreign currency and the transaction is in another one
|
||||
aml_dict['debit'] = statement_currency._convert(aml_dict['debit'] / st_line_currency_rate, company_currency, company, date)
|
||||
aml_dict['credit'] = statement_currency._convert(aml_dict['credit'] / st_line_currency_rate, company_currency, company, date)
|
||||
else:
|
||||
# Statement is in foreign currency and no extra currency is given for the transaction
|
||||
aml_dict['debit'] = st_line_currency._convert(aml_dict['debit'], company_currency, company, date)
|
||||
aml_dict['credit'] = st_line_currency._convert(aml_dict['credit'], company_currency, company, date)
|
||||
elif statement_currency.id != company_currency.id:
|
||||
# Statement is in foreign currency but the transaction is in company currency
|
||||
prorata_factor = (aml_dict['debit'] - aml_dict['credit']) / self.amount_currency
|
||||
aml_dict['amount_currency'] = prorata_factor * self.amount
|
||||
aml_dict['currency_id'] = statement_currency.id
|
||||
|
||||
# Create write-offs
|
||||
for aml_dict in new_aml_dicts:
|
||||
@@ -758,36 +727,6 @@ class AccountBankStatementLine(models.Model):
|
||||
counterpart_moves.assert_balanced()
|
||||
return counterpart_moves
|
||||
|
||||
@api.multi
|
||||
def _prepare_move_line_for_currency(self, aml_dict, date):
|
||||
self.ensure_one()
|
||||
company_currency = self.journal_id.company_id.currency_id
|
||||
statement_currency = self.journal_id.currency_id or company_currency
|
||||
st_line_currency = self.currency_id or statement_currency
|
||||
st_line_currency_rate = self.currency_id and (self.amount_currency / self.amount) or False
|
||||
company = self.company_id
|
||||
|
||||
if st_line_currency.id != company_currency.id:
|
||||
aml_dict['amount_currency'] = aml_dict['debit'] - aml_dict['credit']
|
||||
aml_dict['currency_id'] = st_line_currency.id
|
||||
if self.currency_id and statement_currency.id == company_currency.id and st_line_currency_rate:
|
||||
# Statement is in company currency but the transaction is in foreign currency
|
||||
aml_dict['debit'] = company_currency.round(aml_dict['debit'] / st_line_currency_rate)
|
||||
aml_dict['credit'] = company_currency.round(aml_dict['credit'] / st_line_currency_rate)
|
||||
elif self.currency_id and st_line_currency_rate:
|
||||
# Statement is in foreign currency and the transaction is in another one
|
||||
aml_dict['debit'] = statement_currency._convert(aml_dict['debit'] / st_line_currency_rate, company_currency, company, date)
|
||||
aml_dict['credit'] = statement_currency._convert(aml_dict['credit'] / st_line_currency_rate, company_currency, company, date)
|
||||
else:
|
||||
# Statement is in foreign currency and no extra currency is given for the transaction
|
||||
aml_dict['debit'] = st_line_currency._convert(aml_dict['debit'], company_currency, company, date)
|
||||
aml_dict['credit'] = st_line_currency._convert(aml_dict['credit'], company_currency, company, date)
|
||||
elif statement_currency.id != company_currency.id:
|
||||
# Statement is in foreign currency but the transaction is in company currency
|
||||
prorata_factor = (aml_dict['debit'] - aml_dict['credit']) / self.amount_currency
|
||||
aml_dict['amount_currency'] = prorata_factor * self.amount
|
||||
aml_dict['currency_id'] = statement_currency.id
|
||||
|
||||
def _check_invoice_state(self, invoice):
|
||||
if invoice.state == 'in_payment' and all([payment.state == 'reconciled' for payment in invoice.mapped('payment_move_line_ids.payment_id')]):
|
||||
invoice.write({'state': 'paid'})
|
||||
|
||||
@@ -55,14 +55,12 @@ class AccountMove(models.Model):
|
||||
if line.account_id.user_type_id.type in ('receivable', 'payable'):
|
||||
amount = abs(line.debit - line.credit)
|
||||
total_amount += amount
|
||||
for partial_line in (line.matched_debit_ids + line.matched_credit_ids):
|
||||
total_reconciled += partial_line.amount
|
||||
precision_currency = move.currency_id or move.company_id.currency_id
|
||||
if float_is_zero(total_amount, precision_rounding=precision_currency.rounding):
|
||||
move.matched_percentage = 1.0
|
||||
else:
|
||||
for line in move.line_ids:
|
||||
if line.account_id.user_type_id.type in ('receivable', 'payable'):
|
||||
for partial_line in (line.matched_debit_ids + line.matched_credit_ids):
|
||||
total_reconciled += partial_line.amount
|
||||
move.matched_percentage = total_reconciled / total_amount
|
||||
|
||||
@api.multi
|
||||
@@ -550,7 +548,7 @@ class AccountMoveLine(models.Model):
|
||||
else:
|
||||
date = partial_line.credit_move_id.date if partial_line.debit_move_id == line else partial_line.debit_move_id.date
|
||||
rate = line.currency_id.with_context(date=date).rate
|
||||
amount_residual_currency += sign_partial_line * line.currency_id.round(partial_line.amount * rate)
|
||||
amount_residual_currency += sign_partial_line * partial_line.amount * rate
|
||||
|
||||
#computing the `reconciled` field.
|
||||
reconciled = False
|
||||
@@ -760,21 +758,22 @@ class AccountMoveLine(models.Model):
|
||||
In case of full reconciliation, all moves belonging to the reconciliation will belong to the same account_full_reconcile object.
|
||||
"""
|
||||
# Get first all aml involved
|
||||
todo = self.env['account.partial.reconcile'].search_read(['|', ('debit_move_id', 'in', self.ids), ('credit_move_id', 'in', self.ids)], ['debit_move_id', 'credit_move_id'])
|
||||
amls = set(self.ids)
|
||||
part_recs = self.env['account.partial.reconcile'].search(['|', ('debit_move_id', 'in', self.ids), ('credit_move_id', 'in', self.ids)])
|
||||
amls = self
|
||||
todo = set(part_recs)
|
||||
seen = set()
|
||||
while todo:
|
||||
aml_ids = [rec['debit_move_id'][0] for rec in todo if rec['debit_move_id']] + [rec['credit_move_id'][0] for rec in todo if rec['credit_move_id']]
|
||||
amls |= set(aml_ids)
|
||||
seen |= set([rec['id'] for rec in todo])
|
||||
todo = self.env['account.partial.reconcile'].search_read(['&', '|', ('credit_move_id', 'in', aml_ids), ('debit_move_id', 'in', aml_ids), '!', ('id', 'in', list(seen))], ['debit_move_id', 'credit_move_id'])
|
||||
|
||||
partial_rec_ids = list(seen)
|
||||
partial_rec = todo.pop()
|
||||
seen.add(partial_rec)
|
||||
for aml in [partial_rec.debit_move_id, partial_rec.credit_move_id]:
|
||||
if aml not in amls:
|
||||
amls += aml
|
||||
for x in aml.matched_debit_ids | aml.matched_credit_ids:
|
||||
if x not in seen:
|
||||
todo.add(x)
|
||||
partial_rec_ids = [x.id for x in seen]
|
||||
if not amls:
|
||||
return
|
||||
else:
|
||||
amls = self.browse(list(amls))
|
||||
|
||||
# If we have multiple currency, we can only base ourselve on debit-credit to see if it is fully reconciled
|
||||
currency = set([a.currency_id for a in amls if a.currency_id.id != False])
|
||||
multiple_currency = False
|
||||
|
||||
@@ -115,7 +115,7 @@ class account_abstract_payment(models.AbstractModel):
|
||||
'payment_type': total_amount > 0 and 'inbound' or 'outbound',
|
||||
'partner_id': False if multi else invoices[0].commercial_partner_id.id,
|
||||
'partner_type': False if multi else MAP_INVOICE_TYPE_PARTNER_TYPE[invoices[0].type],
|
||||
'communication': ' '.join([ref for ref in invoices.mapped('reference') if ref]),
|
||||
'communication': ' '.join([ref for ref in invoices.mapped('reference') if ref])[:2000],
|
||||
'invoice_ids': [(6, 0, invoices.ids)],
|
||||
'multi': multi,
|
||||
})
|
||||
|
||||
@@ -83,28 +83,43 @@ class AccountReconciliation(models.AbstractModel):
|
||||
|
||||
@api.model
|
||||
def _get_bank_statement_line_partners(self, st_lines):
|
||||
params = []
|
||||
|
||||
# Add the res.partner.ban's IR rules. In case partners are not shared between companies,
|
||||
# identical bank accounts may exist in a company we don't have access to.
|
||||
ir_rules_query = self.env['res.partner.bank']._where_calc([])
|
||||
self.env['res.partner.bank']._apply_ir_rules(ir_rules_query, 'read')
|
||||
from_clause, where_clause, where_clause_params = ir_rules_query.get_sql()
|
||||
if where_clause:
|
||||
where_bank = ('AND %s' % where_clause).replace('res_partner_bank', 'bank')
|
||||
params += where_clause_params
|
||||
else:
|
||||
where_bank = ''
|
||||
|
||||
# Add the res.partner's IR rules. In case partners are not shared between companies,
|
||||
# identical partners may exist in a company we don't have access to.
|
||||
ir_rules_query = self.env['res.partner']._where_calc([])
|
||||
self.env['res.partner']._apply_ir_rules(ir_rules_query, 'read')
|
||||
from_clause, where_clause, where_clause_params = ir_rules_query.get_sql()
|
||||
if where_clause:
|
||||
where_partner = ('AND %s' % where_clause).replace('res_partner', 'p3')
|
||||
params += where_clause_params
|
||||
else:
|
||||
where_partner = ''
|
||||
|
||||
query = '''
|
||||
SELECT
|
||||
st_line.id AS id,
|
||||
COALESCE(p1.id,p2.id,p3.id) AS partner_id
|
||||
FROM account_bank_statement_line st_line
|
||||
LEFT JOIN res_partner_bank bank ON bank.id = st_line.bank_account_id OR bank.acc_number = st_line.account_number
|
||||
LEFT JOIN res_partner p1 ON st_line.partner_id=p1.id
|
||||
LEFT JOIN res_partner p2 ON bank.partner_id=p2.id
|
||||
LEFT JOIN res_partner p3 ON p3.name ILIKE st_line.partner_name
|
||||
WHERE st_line.id IN %s
|
||||
'''
|
||||
params = [tuple(st_lines.ids)]
|
||||
query += 'LEFT JOIN res_partner_bank bank ON bank.id = st_line.bank_account_id OR bank.acc_number = st_line.account_number %s\n' % (where_bank)
|
||||
query += 'LEFT JOIN res_partner p1 ON st_line.partner_id=p1.id \n'
|
||||
query += 'LEFT JOIN res_partner p2 ON bank.partner_id=p2.id \n'
|
||||
query += 'LEFT JOIN res_partner p3 ON p3.name ILIKE st_line.partner_name %s\n' % (where_partner)
|
||||
query += 'WHERE st_line.id IN %s'
|
||||
|
||||
# Add the res.partner's IR rules to the WHERE clause. In case partners are not shared
|
||||
# between companies, identical partners may exist in a company we don't have access to.
|
||||
ir_rules_query = self.env['res.partner']._where_calc([])
|
||||
self.env['res.partner']._apply_ir_rules(ir_rules_query, 'read')
|
||||
from_clause, where_clause, where_clause_params = ir_rules_query.get_sql()
|
||||
where_p3 = (" AND %s" % where_clause).replace('res_partner', 'p3') if where_clause else ''
|
||||
if where_p3:
|
||||
query += where_p3
|
||||
params += where_clause_params
|
||||
params += [tuple(st_lines.ids)]
|
||||
|
||||
self._cr.execute(query, params)
|
||||
|
||||
|
||||
@@ -106,7 +106,8 @@
|
||||
<filter string="Payment Method" name="paymentmethod" domain="[]" context="{'group_by': 'payment_method_id'}"/>
|
||||
<filter string="State" name="state" domain="[]" context="{'group_by': 'state'}"/>
|
||||
<filter string="Currency" name="currency" domain="[]" context="{'group_by': 'currency_id'}" groups="base.group_multi_currency"/>
|
||||
<filter string="Company" name="company" domain="[]" context="{'group_by': 'company_id'}" groups="base.group_multi_company"/>
|
||||
<!-- company_id is not stored (cdca5d4751a979f6b694f02d88160843f81a77f5). But we can't break structure-->
|
||||
<filter string="Company" name="company" domain="[]" context="{'group_by': 'company_id'}" groups="base.group_multi_company" invisible="1" />
|
||||
<separator/>
|
||||
<filter string="Late Activities" name="activities_overdue"
|
||||
domain="[('activity_ids.date_deadline', '<', context_today().strftime('%Y-%m-%d'))]"
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from werkzeug.urls import url_encode
|
||||
|
||||
from odoo import http, _
|
||||
from odoo.addons.portal.controllers.portal import _build_url_w_params
|
||||
from odoo.addons.payment.controllers.portal import PaymentProcessing
|
||||
@@ -17,8 +19,6 @@ class PaymentPortal(http.Controller):
|
||||
|
||||
:return html: form containing all values related to the acquirer to
|
||||
redirect customers to the acquirer website """
|
||||
success_url = kwargs.get('success_url', '/my')
|
||||
|
||||
invoice_sudo = request.env['account.invoice'].sudo().browse(invoice_id)
|
||||
if not invoice_sudo:
|
||||
return False
|
||||
@@ -30,6 +30,10 @@ class PaymentPortal(http.Controller):
|
||||
|
||||
if request.env.user._is_public():
|
||||
save_token = False # we avoid to create a token for the public user
|
||||
|
||||
success_url = kwargs.get(
|
||||
'success_url', "%s?%s" % (invoice_sudo.access_url, url_encode({'access_token': access_token}) if access_token else '')
|
||||
)
|
||||
vals = {
|
||||
'acquirer_id': acquirer_id,
|
||||
'return_url': success_url,
|
||||
@@ -54,7 +58,6 @@ class PaymentPortal(http.Controller):
|
||||
def invoice_pay_token(self, invoice_id, pm_id=None, **kwargs):
|
||||
""" Use a token to perform a s2s transaction """
|
||||
error_url = kwargs.get('error_url', '/my')
|
||||
success_url = kwargs.get('success_url', '/my')
|
||||
access_token = kwargs.get('access_token')
|
||||
params = {}
|
||||
if access_token:
|
||||
@@ -65,6 +68,9 @@ class PaymentPortal(http.Controller):
|
||||
params['error'] = 'pay_invoice_invalid_doc'
|
||||
return request.redirect(_build_url_w_params(error_url, params))
|
||||
|
||||
success_url = kwargs.get(
|
||||
'success_url', "%s?%s" % (invoice_sudo.access_url, url_encode({'access_token': access_token}) if access_token else '')
|
||||
)
|
||||
try:
|
||||
token = request.env['payment.token'].sudo().browse(int(pm_id))
|
||||
except (ValueError, TypeError):
|
||||
|
||||
@@ -54,7 +54,10 @@ class Lead(models.Model):
|
||||
_primary_email = 'email_from'
|
||||
|
||||
def _default_probability(self):
|
||||
stage_id = self._default_stage_id()
|
||||
if 'default_stage_id' in self._context:
|
||||
stage_id = self._context.get('default_stage_id')
|
||||
else:
|
||||
stage_id = self._default_stage_id()
|
||||
if stage_id:
|
||||
return self.env['crm.stage'].browse(stage_id).probability
|
||||
return 10
|
||||
|
||||
@@ -189,12 +189,12 @@ class GoogleService(models.TransientModel):
|
||||
status = error.response.status_code
|
||||
response = ""
|
||||
else:
|
||||
req = json.loads(error.request.body)
|
||||
req = json.loads(error.request.body or 'null')
|
||||
res = error.response.json()
|
||||
_logger.exception("Error while requesting Google Services\nRequest:\n%s\nResponse:\n%s", pformat(req), pformat(res))
|
||||
if error.response.status_code in (400, 401, 410):
|
||||
raise UserError(_("Error while requesting Google Services: %s" % res['error']['message']))
|
||||
raise self.env['res.config.settings'].get_config_warning(_("Something went wrong with your request to google"))
|
||||
raise UserError(_("Error while requesting Google Services: %s") % res['error']['message'])
|
||||
raise self.env['res.config.settings'].get_config_warning(_("Something went wrong with your request to google: %s") % res['error']['message'])
|
||||
return (status, response, ask_time)
|
||||
|
||||
# TODO : remove me, it is only used in google calendar. Make google_calendar use the constants
|
||||
|
||||
@@ -13,6 +13,7 @@ from odoo import api, fields, models, tools, SUPERUSER_ID
|
||||
from odoo.addons.resource.models.resource import float_to_time, HOURS_PER_DAY
|
||||
from odoo.exceptions import AccessError, UserError, ValidationError
|
||||
from odoo.tools import float_compare
|
||||
from odoo.tools.float_utils import float_round
|
||||
from odoo.tools.translate import _
|
||||
|
||||
_logger = logging.getLogger(__name__)
|
||||
@@ -147,6 +148,8 @@ class HolidaysRequest(models.Model):
|
||||
number_of_hours_display = fields.Float(
|
||||
'Duration in hours', compute='_compute_number_of_hours_display', copy=False, readonly=True,
|
||||
help='Number of hours of the time off request according to your working schedule. Used for interface.')
|
||||
duration_display = fields.Char('Requested (Days/Hours)', compute='_compute_duration_display',
|
||||
help="Field allowing to see the leave request duration in days or hours depending on the leave_type_request_unit") # details
|
||||
# details
|
||||
meeting_id = fields.Many2one('calendar.event', string='Meeting')
|
||||
parent_id = fields.Many2one('hr.leave', string='Parent', copy=False)
|
||||
@@ -386,6 +389,16 @@ class HolidaysRequest(models.Model):
|
||||
else:
|
||||
holiday.number_of_hours_display = 0
|
||||
|
||||
@api.multi
|
||||
@api.depends('number_of_hours_display', 'number_of_days_display')
|
||||
def _compute_duration_display(self):
|
||||
for leave in self:
|
||||
leave.duration_display = '%g %s' % (
|
||||
(float_round(leave.number_of_hours_display, precision_digits=2)
|
||||
if leave.leave_type_request_unit == 'hour'
|
||||
else float_round(leave.number_of_days_display, precision_digits=2)),
|
||||
_('hour(s)') if leave.leave_type_request_unit == 'hour' else _('day(s)'))
|
||||
|
||||
@api.multi
|
||||
@api.depends('state', 'employee_id', 'department_id')
|
||||
def _compute_can_reset(self):
|
||||
@@ -457,7 +470,10 @@ class HolidaysRequest(models.Model):
|
||||
res = []
|
||||
for leave in self:
|
||||
if self.env.context.get('short_name'):
|
||||
res.append((leave.id, _("%s : %.2f day(s)") % (leave.name or leave.holiday_status_id.name, leave.number_of_days)))
|
||||
if leave.leave_type_request_unit == 'hour':
|
||||
res.append((leave.id, _("%s : %.2f hour(s)") % (leave.name or leave.holiday_status_id.name, leave.number_of_hours_display)))
|
||||
else:
|
||||
res.append((leave.id, _("%s : %.2f day(s)") % (leave.name or leave.holiday_status_id.name, leave.number_of_days)))
|
||||
else:
|
||||
if leave.holiday_type == 'company':
|
||||
target = leave.mode_company_id.name
|
||||
@@ -467,11 +483,18 @@ class HolidaysRequest(models.Model):
|
||||
target = leave.category_id.name
|
||||
else:
|
||||
target = leave.employee_id.name
|
||||
res.append(
|
||||
(leave.id,
|
||||
_("%s on %s :%.2f day(s)") %
|
||||
(target, leave.holiday_status_id.name, leave.number_of_days))
|
||||
)
|
||||
if leave.leave_type_request_unit == 'hour':
|
||||
res.append(
|
||||
(leave.id,
|
||||
_("%s on %s : %.2f hour(s)") %
|
||||
(target, leave.holiday_status_id.name, leave.number_of_hours_display))
|
||||
)
|
||||
else:
|
||||
res.append(
|
||||
(leave.id,
|
||||
_("%s on %s : %.2f day(s)") %
|
||||
(target, leave.holiday_status_id.name, leave.number_of_days))
|
||||
)
|
||||
return res
|
||||
|
||||
@api.multi
|
||||
|
||||
@@ -313,7 +313,7 @@
|
||||
<field name="name"/>
|
||||
<field name="date_from"/>
|
||||
<field name="date_to"/>
|
||||
<field name="number_of_days" string="Number of Days" sum="Total time off"/>
|
||||
<field name="duration_display"/>
|
||||
<field name="state"/>
|
||||
<field name="payslip_status" widget="toggle_button" options='{"active": "Reported in last payslips", "inactive": "To Report in Payslip"}' groups="hr_holidays.group_hr_holidays_manager"/>
|
||||
<field name="category_id" invisible="1"/>
|
||||
|
||||
@@ -893,14 +893,23 @@ class Escpos:
|
||||
self._raw(PAPER_FULL_CUT)
|
||||
|
||||
|
||||
def cashdraw(self, pin):
|
||||
""" Send pulse to kick the cash drawer """
|
||||
if pin == 2:
|
||||
self._raw(CD_KICK_2)
|
||||
elif pin == 5:
|
||||
self._raw(CD_KICK_5)
|
||||
else:
|
||||
raise CashDrawerError()
|
||||
def cashdraw(self, pin, tries=5):
|
||||
""" Send pulse to kick the cash drawer
|
||||
|
||||
With some printers the drawer will not open after one pulse, for this reason we will check
|
||||
the drawer status up to 'tries' times and send a new pulse if the drawer is still closed. If the
|
||||
drawer status is open, we will stop sending pulses.
|
||||
"""
|
||||
for i in range(tries):
|
||||
if pin == 2:
|
||||
self._raw(CD_KICK_2)
|
||||
elif pin == 5:
|
||||
self._raw(CD_KICK_5)
|
||||
else:
|
||||
raise CashDrawerError()
|
||||
|
||||
if i != tries - 1 and not self.get_printer_status()['printer']['drawer_pin_high']:
|
||||
break
|
||||
|
||||
|
||||
def hw(self, hw):
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
'views/fleet_views.xml',
|
||||
'views/res_config_settings_views.xml',
|
||||
'views/hr_contract_views.xml',
|
||||
'security/security.xml',
|
||||
],
|
||||
'auto_install': True,
|
||||
}
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<!-- Note for later versions, the group Employee Manager should become contract manager -->
|
||||
<record id="hr.group_hr_manager" model="res.groups">
|
||||
<field name="implied_ids" eval="[(4, ref('fleet.fleet_group_manager'))]"/>
|
||||
</record>
|
||||
</odoo>
|
||||
@@ -1,6 +1,7 @@
|
||||
id,name
|
||||
tax_group_iva_2,IVA 2%
|
||||
tax_group_iva_4,IVA 4%
|
||||
tax_group_iva_5,IVA 5%
|
||||
tax_group_iva_10,IVA 10%
|
||||
tax_group_iva_12,IVA 12%
|
||||
tax_group_iva_21,IVA 21%
|
||||
|
||||
|
@@ -5,6 +5,8 @@ id,description,chart_template_id/id,name,sequence,amount,amount_type,type_tax_us
|
||||
21a,21a,l10n_it_chart_template_generic,Iva al 21% (credito),4,21,percent,purchase,False,1601,1601,tax_tag_10,,tax_group_iva_21
|
||||
20v,20v,l10n_it_chart_template_generic,Iva al 20% (debito),3,20,percent,sale,False,2601,2601,tax_tag_35,,tax_group_iva_20
|
||||
20a,20a,l10n_it_chart_template_generic,Iva al 20% (credito),4,20,percent,purchase,False,1601,1601,tax_tag_05,,tax_group_iva_20
|
||||
5v,5v,l10n_it_chart_template_generic,Iva al 5% (debito),5,5,percent,sale,False,2601,2601,tax_tag_5v,,tax_group_iva_5
|
||||
5a,5a,l10n_it_chart_template_generic,Iva al 5% (credito),6,5,percent,purchase,False,1601,1601,tax_tag_5a,,tax_group_iva_5
|
||||
10v,10v,l10n_it_chart_template_generic,Iva al 10% (debito),5,10,percent,sale,False,2601,2601,tax_tag_29,,tax_group_iva_10
|
||||
10a,10a,l10n_it_chart_template_generic,Iva al 10% (credito),6,10,percent,purchase,False,1601,1601,tax_tag_01,,tax_group_iva_10
|
||||
10AOb,10AOb,l10n_it_chart_template_generic,Iva al 10% indetraibile (D),200,0,percent,none,False,1601,1601,,,tax_group_iva_10
|
||||
|
||||
|
@@ -190,4 +190,12 @@
|
||||
<field name="name">00art15v</field>
|
||||
<field name="applicability">taxes</field>
|
||||
</record>
|
||||
<record id="tax_tag_5a" model="account.account.tag">
|
||||
<field name="name">5a</field>
|
||||
<field name="applicability">taxes</field>
|
||||
</record>
|
||||
<record id="tax_tag_5v" model="account.account.tag">
|
||||
<field name="name">5v</field>
|
||||
<field name="applicability">taxes</field>
|
||||
</record>
|
||||
</odoo>
|
||||
|
||||
@@ -479,25 +479,25 @@
|
||||
<field name='name'>119.01 IVA pendiente de pago</field>
|
||||
<field name='color'>4</field>
|
||||
<field name='applicability'>accounts</field>
|
||||
<field name='nature'>A</field>
|
||||
<field name='nature'>D</field>
|
||||
</record>
|
||||
<record id='account_tag_119_02' model='account.account.tag'>
|
||||
<field name='name'>119.02 IVA de importación pendiente de pago</field>
|
||||
<field name='color'>4</field>
|
||||
<field name='applicability'>accounts</field>
|
||||
<field name='nature'>A</field>
|
||||
<field name='nature'>D</field>
|
||||
</record>
|
||||
<record id='account_tag_119_03' model='account.account.tag'>
|
||||
<field name='name'>119.03 IEPS pendiente de pago</field>
|
||||
<field name='color'>4</field>
|
||||
<field name='applicability'>accounts</field>
|
||||
<field name='nature'>A</field>
|
||||
<field name='nature'>D</field>
|
||||
</record>
|
||||
<record id='account_tag_119_04' model='account.account.tag'>
|
||||
<field name='name'>119.04 IEPS pendiente de pago en importación</field>
|
||||
<field name='color'>4</field>
|
||||
<field name='applicability'>accounts</field>
|
||||
<field name='nature'>A</field>
|
||||
<field name='nature'>D</field>
|
||||
</record>
|
||||
<record id='account_tag_120_01' model='account.account.tag'>
|
||||
<field name='name'>120.01 Anticipo a proveedores nacional</field>
|
||||
@@ -917,7 +917,7 @@
|
||||
<field name='name'>180.01 Crédito mercantil</field>
|
||||
<field name='color'>4</field>
|
||||
<field name='applicability'>accounts</field>
|
||||
<field name='nature'>A</field>
|
||||
<field name='nature'>D</field>
|
||||
</record>
|
||||
<record id='account_tag_181_01' model='account.account.tag'>
|
||||
<field name='name'>181.01 Gastos de instalación</field>
|
||||
@@ -2423,7 +2423,7 @@
|
||||
<field name='name'>503 Devoluciones, descuentos o bonificaciones sobre compras</field>
|
||||
<field name='color'>1</field>
|
||||
<field name='applicability'>accounts</field>
|
||||
<field name='nature'>D</field>
|
||||
<field name='nature'>A</field>
|
||||
</record>
|
||||
<record id='account_tag_503_01' model='account.account.tag'>
|
||||
<field name='name'>503.01 Devoluciones, descuentos o bonificaciones sobre compras</field>
|
||||
@@ -2585,13 +2585,13 @@
|
||||
<field name='name'>505.01 Costo por venta de activo fijo</field>
|
||||
<field name='color'>4</field>
|
||||
<field name='applicability'>accounts</field>
|
||||
<field name='nature'>A</field>
|
||||
<field name='nature'>D</field>
|
||||
</record>
|
||||
<record id='account_tag_505_02' model='account.account.tag'>
|
||||
<field name='name'>505.02 Costo por baja de activo fijo</field>
|
||||
<field name='color'>4</field>
|
||||
<field name='applicability'>accounts</field>
|
||||
<field name='nature'>A</field>
|
||||
<field name='nature'>D</field>
|
||||
</record>
|
||||
<record id='account_tag_601_01' model='account.account.tag'>
|
||||
<field name='name'>601.01 Sueldos y salarios</field>
|
||||
@@ -4781,7 +4781,7 @@
|
||||
<field name='name'>607.01 Participación de los trabajadores en las utilidades</field>
|
||||
<field name='color'>4</field>
|
||||
<field name='applicability'>accounts</field>
|
||||
<field name='nature'>A</field>
|
||||
<field name='nature'>D</field>
|
||||
</record>
|
||||
<record id='account_tag_608_01' model='account.account.tag'>
|
||||
<field name='name'>608.01 Participación en resultados de subsidiarias</field>
|
||||
@@ -4799,7 +4799,7 @@
|
||||
<field name='name'>610.01 Participación de los trabajadores en las utilidades diferida</field>
|
||||
<field name='color'>4</field>
|
||||
<field name='applicability'>accounts</field>
|
||||
<field name='nature'>A</field>
|
||||
<field name='nature'>D</field>
|
||||
</record>
|
||||
<record id='account_tag_611_01' model='account.account.tag'>
|
||||
<field name='name'>611.01 Impuesto Sobre la renta</field>
|
||||
@@ -4817,7 +4817,7 @@
|
||||
<field name='name'>612.01 Gastos no deducibles para CUFIN</field>
|
||||
<field name='color'>4</field>
|
||||
<field name='applicability'>accounts</field>
|
||||
<field name='nature'>A</field>
|
||||
<field name='nature'>D</field>
|
||||
</record>
|
||||
<record id='account_tag_613_01' model='account.account.tag'>
|
||||
<field name='name'>613.01 Depreciación de edificios</field>
|
||||
|
||||
@@ -147,7 +147,7 @@ class MailActivity(models.Model):
|
||||
('today', 'Today'),
|
||||
('planned', 'Planned')], 'State',
|
||||
compute='_compute_state')
|
||||
recommended_activity_type_id = fields.Many2one('mail.activity.type', string="Recommended Activity Type", readonly=True)
|
||||
recommended_activity_type_id = fields.Many2one('mail.activity.type', string="Recommended Activity Type")
|
||||
previous_activity_type_id = fields.Many2one('mail.activity.type', string='Previous Activity Type', readonly=True)
|
||||
has_recommended_activities = fields.Boolean(
|
||||
'Next activities available',
|
||||
|
||||
@@ -372,6 +372,7 @@ class MailComposer(models.TransientModel):
|
||||
- normal mode: return rendered values
|
||||
/!\ for x2many field, this onchange return command instead of ids
|
||||
"""
|
||||
attachment_ids = []
|
||||
if template_id and composition_mode == 'mass_mail':
|
||||
template = self.env['mail.template'].browse(template_id)
|
||||
fields = ['subject', 'body_html', 'email_from', 'reply_to', 'mail_server_id']
|
||||
@@ -397,7 +398,7 @@ class MailComposer(models.TransientModel):
|
||||
'res_id': 0,
|
||||
'type': 'binary', # override default_type from context, possibly meant for another model!
|
||||
}
|
||||
values.setdefault('attachment_ids', list()).append(Attachment.create(data_attach).id)
|
||||
attachment_ids.append(Attachment.create(data_attach).id)
|
||||
else:
|
||||
default_values = self.with_context(default_composition_mode=composition_mode, default_model=model, default_res_id=res_id).default_get(['composition_mode', 'model', 'res_id', 'parent_id', 'partner_ids', 'subject', 'body', 'email_from', 'reply_to', 'attachment_ids', 'mail_server_id'])
|
||||
values = dict((key, default_values[key]) for key in ['subject', 'body', 'partner_ids', 'email_from', 'reply_to', 'attachment_ids', 'mail_server_id'] if key in default_values)
|
||||
@@ -410,6 +411,8 @@ class MailComposer(models.TransientModel):
|
||||
# this force the complete replacement of x2many field with
|
||||
# command and is compatible with onchange api.v7
|
||||
values = self._convert_to_write(values)
|
||||
if attachment_ids:
|
||||
values.update(attachment_ids=[(6, 0, attachment_ids)])
|
||||
|
||||
return {'value': values}
|
||||
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo import api, models
|
||||
from odoo import api, models, _
|
||||
|
||||
from odoo.tools import float_round
|
||||
from odoo.exceptions import UserError
|
||||
|
||||
|
||||
class MrpProduction(models.Model):
|
||||
@@ -42,6 +43,9 @@ class MrpProduction(models.Model):
|
||||
""" Generates moves and work orders
|
||||
@return: Newly generated picking Id.
|
||||
"""
|
||||
for production in self:
|
||||
if production.product_id in production.bom_id.sub_products.mapped('product_id'):
|
||||
raise UserError(_("You cannot have %s as the finished product and in the Byproducts") % production.product_id.name)
|
||||
res = super(MrpProduction, self)._generate_finished_moves()
|
||||
for production in self.filtered(lambda production: production.bom_id):
|
||||
for sub_product in production.bom_id.sub_products:
|
||||
|
||||
@@ -10,12 +10,12 @@
|
||||
<page string="Byproducts" attrs="{'invisible': [('type','=','phantom')]}">
|
||||
<field name="sub_products">
|
||||
<tree string="Byproducts" editable="top">
|
||||
<field name="product_id"/>
|
||||
<field name="product_id" domain="[('id', '!=', parent.product_id)]"/>
|
||||
<field name="product_qty"/>
|
||||
<field name="product_uom_id" groups="uom.group_uom"/>
|
||||
</tree>
|
||||
<form string="Byproducts">
|
||||
<field name="product_id"/>
|
||||
<field name="product_id" domain="[('id', '!=', parent.product_id)]"/>
|
||||
<field name="product_qty"/>
|
||||
<field name="product_uom_id" groups="uom.group_uom"/>
|
||||
</form>
|
||||
|
||||
@@ -404,7 +404,7 @@ var Gui = core.Class.extend({
|
||||
}
|
||||
} else if (input === '-') {
|
||||
if (options.firstinput) {
|
||||
newbuf = '-';
|
||||
newbuf = '-0';
|
||||
} else if ( newbuf[0] === '-' ) {
|
||||
newbuf = newbuf.substring(1,newbuf.length);
|
||||
} else {
|
||||
@@ -419,6 +419,9 @@ var Gui = core.Class.extend({
|
||||
newbuf += input;
|
||||
}
|
||||
}
|
||||
if (newbuf === "-") {
|
||||
newbuf = "";
|
||||
}
|
||||
|
||||
// End of input buffer at 12 characters.
|
||||
if (newbuf.length > buffer.length && newbuf.length > 12) {
|
||||
|
||||
@@ -990,7 +990,7 @@
|
||||
<line t-foreach='taxes' t-as='taxe'>
|
||||
<left><t t-esc='taxe.name' /></left>
|
||||
<right>
|
||||
<t t-esc='widget.format_currency_no_symbol(taxe.total)'/>
|
||||
<t t-esc='widget.format_currency_no_symbol(taxe.tax_amount)'/>
|
||||
</right>
|
||||
</line>
|
||||
|
||||
|
||||
@@ -47,7 +47,6 @@
|
||||
<field name="arch" type="xml">
|
||||
<kanban create="false" class="o_kanban_dashboard o_pos_kanban">
|
||||
<field name="name"/>
|
||||
<field name="session_ids"/>
|
||||
<field name="current_session_id"/>
|
||||
<field name="current_session_state"/>
|
||||
<field name="last_session_closing_date"/>
|
||||
|
||||
@@ -137,7 +137,7 @@ class PortalChatter(http.Controller):
|
||||
raise Forbidden()
|
||||
# Non-employee see only messages with not internal subtype (aka, no internal logs)
|
||||
if not request.env['res.users'].has_group('base.group_user'):
|
||||
domain = expression.AND([['&', '&', ('subtype_id', '!=', False), ('subtype_id.internal', '=', False), ('website_published', '=', True)], domain])
|
||||
domain = expression.AND([Message._non_employee_message_domain(), domain])
|
||||
Message = request.env['mail.message'].sudo()
|
||||
return {
|
||||
'messages': Message.search(domain, limit=limit, offset=offset).portal_message_format(),
|
||||
|
||||
@@ -21,3 +21,7 @@ class MailMessage(models.Model):
|
||||
message_tree = dict((m.id, m) for m in self.sudo())
|
||||
self._message_read_dict_postprocess(message_values, message_tree)
|
||||
return message_values
|
||||
|
||||
@api.model
|
||||
def _non_employee_message_domain(self):
|
||||
return ['&', ('subtype_id', '!=', False), ('subtype_id.internal', '=', False)]
|
||||
|
||||
@@ -1266,7 +1266,7 @@ class SaleOrderLine(models.Model):
|
||||
result.setdefault(so_line_id, 0.0)
|
||||
uom = product_uom_map.get(item['product_uom_id'][0])
|
||||
if so_line.product_uom.category_id == uom.category_id:
|
||||
qty = uom._compute_quantity(item['unit_amount'], so_line.product_uom)
|
||||
qty = uom._compute_quantity(item['unit_amount'], so_line.product_uom, rounding_method='HALF-UP')
|
||||
else:
|
||||
qty = item['unit_amount']
|
||||
result[so_line_id] += qty
|
||||
|
||||
@@ -52,7 +52,7 @@ class PosSaleReport(models.Model):
|
||||
LEFT JOIN product_pricelist pp ON (so.pricelist_id = pp.id)
|
||||
LEFT JOIN uom_uom u on (u.id=sol.product_uom)
|
||||
LEFT JOIN uom_uom u2 on (u2.id=pt.uom_id)
|
||||
WHERE so.state != 'cancel'
|
||||
WHERE so.state in ('sale','done')
|
||||
"""
|
||||
return so_str
|
||||
|
||||
|
||||
@@ -236,6 +236,10 @@ class SaleOrderLine(models.Model):
|
||||
'partner_id': self.order_id.partner_id.id,
|
||||
'email_from': self.order_id.partner_id.email,
|
||||
})
|
||||
# duplicating a project doesn't set the SO on sub-tasks
|
||||
project.tasks.filtered(lambda task: task.parent_id != False).write({
|
||||
'sale_line_id': self.id,
|
||||
})
|
||||
else:
|
||||
project = self.env['project.project'].create(values)
|
||||
# link project as generated by current so line
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
from . import test_default_team
|
||||
@@ -0,0 +1,48 @@
|
||||
from odoo.tests import common
|
||||
|
||||
|
||||
class TestDefaultTeam(common.SavepointCase):
|
||||
"""Tests to check if correct default team is found."""
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
"""Set up data for default team tests."""
|
||||
super(TestDefaultTeam, cls).setUpClass()
|
||||
cls.CrmTeam = cls.env['crm.team']
|
||||
ResUsers = cls.env['res.users'].with_context(
|
||||
{'no_reset_password': True})
|
||||
group_sale_manager = cls.env.ref('sales_team.group_sale_manager')
|
||||
cls.user = ResUsers.create({
|
||||
'name': 'Team User',
|
||||
'login': 'sales_team_user',
|
||||
'email': 'sales.team.user@example.com',
|
||||
'groups_id': [(6, 0, [group_sale_manager.id])]
|
||||
})
|
||||
cls.team_1 = cls.env['crm.team'].create({
|
||||
'name': 'Test Team',
|
||||
'member_ids': [(4, cls.user.id)],
|
||||
'company_id': False
|
||||
})
|
||||
# Europe Team (fall back team)
|
||||
cls.team_2 = cls.env.ref('sales_team.team_sales_department')
|
||||
|
||||
def test_01_user_team(self):
|
||||
"""Get default team, when user belongs to one."""
|
||||
team = self.CrmTeam.sudo(self.user)._get_default_team_id()
|
||||
self.assertEqual(team, self.team_1)
|
||||
|
||||
def test_02_fallback_team(self):
|
||||
"""Get default team when user does not belong to any team.
|
||||
|
||||
Case 1: fall back default team (from XML ref) is active.
|
||||
Case 2: fall back default team is not active.
|
||||
"""
|
||||
# Clear users from team.
|
||||
self.team_1.member_ids = [(5,)]
|
||||
# Case 1.
|
||||
team = self.CrmTeam.sudo(self.user)._get_default_team_id()
|
||||
self.assertEqual(team, self.team_2)
|
||||
# Case 2.
|
||||
self.team_2.active = False
|
||||
team = self.CrmTeam.sudo(self.user)._get_default_team_id()
|
||||
self.assertEqual(team, self.CrmTeam)
|
||||
@@ -4,6 +4,7 @@
|
||||
from . import account_chart_template
|
||||
from . import account_invoice
|
||||
from . import account_move
|
||||
from . import account_reconciliation_widget
|
||||
from . import product
|
||||
from . import stock
|
||||
from . import res_config_settings
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from odoo import api, models
|
||||
from odoo.osv import expression
|
||||
|
||||
|
||||
class AccountReconciliation(models.AbstractModel):
|
||||
_inherit = "account.reconciliation.widget"
|
||||
|
||||
@api.model
|
||||
def _domain_move_lines_for_reconciliation(self, st_line, aml_accounts, partner_id, excluded_ids=None, search_str=False):
|
||||
def to_int(val):
|
||||
try:
|
||||
return int(val)
|
||||
except (ValueError, TypeError):
|
||||
return None
|
||||
|
||||
domain = super()._domain_move_lines_for_reconciliation(
|
||||
st_line, aml_accounts, partner_id, excluded_ids=excluded_ids, search_str=search_str
|
||||
)
|
||||
acc_props = (
|
||||
"property_stock_account_input",
|
||||
"property_stock_account_output",
|
||||
"property_stock_account_input_categ_id",
|
||||
"property_stock_account_output_categ_id",
|
||||
)
|
||||
acc_ids = [
|
||||
(acc["value_reference"] or "").split(",")[-1]
|
||||
for acc in self.env["ir.property"]
|
||||
.sudo()
|
||||
.search([("name", "in", acc_props), ("value_reference", "!=", False)])
|
||||
.read(["value_reference"])
|
||||
if to_int((acc["value_reference"] or "").split(",")[-1])
|
||||
]
|
||||
if acc_ids:
|
||||
domain = expression.AND([domain, [("account_id.id", "not in", acc_ids)]])
|
||||
return domain
|
||||
@@ -40,6 +40,12 @@
|
||||
</xpath>
|
||||
</template>
|
||||
|
||||
<template id="assets_common" name="tour" inherit_id="web.assets_common">
|
||||
<xpath expr="//script[last()]" position="after">
|
||||
<script type="text/javascript" src="/survey/static/src/js/tour_test_survey.js"></script>
|
||||
</xpath>
|
||||
</template>
|
||||
|
||||
<template id="assets_backend" name="survey assets" inherit_id="web.assets_backend">
|
||||
<xpath expr="." position="inside">
|
||||
<link rel="stylesheet" href="/survey/static/src/css/survey_result.css"/>
|
||||
|
||||
@@ -491,7 +491,11 @@ var AbstractWebClient = Widget.extend(ServiceProviderMixin, KeyboardNavigationMi
|
||||
new RainbowMan(data).appendTo(this.$el);
|
||||
} else {
|
||||
// For instance keep title blank, as we don't have title in data
|
||||
this.notification_manager.notify('', data.message, true);
|
||||
this.call('notification', 'notify', {
|
||||
title: "",
|
||||
message: data.message,
|
||||
sticky: false
|
||||
});
|
||||
}
|
||||
} else {
|
||||
throw new Error('Unknown effect type: ' + type);
|
||||
|
||||
@@ -44,8 +44,12 @@
|
||||
}
|
||||
}
|
||||
|
||||
// Main function that starts orchestration of tests
|
||||
function clickEverywhere(){
|
||||
setTimeout(_clickEverywhere, 1000);
|
||||
}
|
||||
|
||||
// Main function that starts orchestration of tests
|
||||
function _clickEverywhere(){
|
||||
console.log("Starting ClickEverywhere test");
|
||||
var startTime = performance.now();
|
||||
createWebClientHooks();
|
||||
|
||||
@@ -13,6 +13,7 @@ var DateWidget = Widget.extend({
|
||||
type_of_date: "date",
|
||||
events: {
|
||||
'change.datetimepicker': 'changeDatetime',
|
||||
'error.datetimepicker': 'errorDatetime',
|
||||
'change .o_datepicker_input': 'changeDatetime',
|
||||
'input input': '_onInput',
|
||||
'keydown': '_onKeydown',
|
||||
@@ -103,6 +104,12 @@ var DateWidget = Widget.extend({
|
||||
}
|
||||
}
|
||||
},
|
||||
/**
|
||||
* Library clears the wrong date format so just ignore error
|
||||
*/
|
||||
errorDatetime: function (e) {
|
||||
return false;
|
||||
},
|
||||
/**
|
||||
* Focuses the datepicker input. This function must be called in order to
|
||||
* prevent 'input' events triggered by the lib to bubble up, and to cause
|
||||
|
||||
@@ -2661,6 +2661,25 @@ QUnit.module('basic_fields', {
|
||||
form.destroy();
|
||||
});
|
||||
|
||||
QUnit.test('date field should remove the date if the date is not valid', function (assert) {
|
||||
assert.expect(1);
|
||||
|
||||
var form = createView({
|
||||
View: FormView,
|
||||
model: 'partner',
|
||||
data: this.data,
|
||||
arch: '<form string="Partners"><field name="date"/></form>',
|
||||
res_id: 4,
|
||||
});
|
||||
// switch to edit mode
|
||||
form.$buttons.find('.o_form_button_edit').click();
|
||||
// set an invalid date
|
||||
var $input = form.$('.o_field_widget[name=date] input');
|
||||
$input.val('mmmh').trigger('change');
|
||||
assert.strictEqual($input.text(), "", "The date field should be empty");
|
||||
form.destroy();
|
||||
});
|
||||
|
||||
QUnit.test('date field value should not set on first click', function (assert) {
|
||||
assert.expect(2);
|
||||
|
||||
|
||||
@@ -193,6 +193,9 @@ return core.Class.extend(mixins.EventDispatcherMixin, ServicesMixin, {
|
||||
* @returns {boolean} true if a tip was found and activated/updated
|
||||
*/
|
||||
_check_for_tooltip: function (tip, tour_name) {
|
||||
if (tip === undefined) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if ($('body').hasClass('o_ui_blocked')) {
|
||||
this._deactivate_tip(tip);
|
||||
|
||||
@@ -138,12 +138,15 @@ class Website(models.Model):
|
||||
|
||||
@api.multi
|
||||
def write(self, values):
|
||||
public_user_to_change_websites = self.env['website']
|
||||
self._get_languages.clear_cache(self)
|
||||
if 'company_id' in values and 'user_id' not in values:
|
||||
company = self.env['res.company'].browse(values['company_id'])
|
||||
values['user_id'] = company._get_public_user().id
|
||||
public_user_to_change_websites = self.filtered(lambda w: w.user_id.company_id.id != values['company_id'])
|
||||
if public_user_to_change_websites:
|
||||
company = self.env['res.company'].browse(values['company_id'])
|
||||
super(Website, public_user_to_change_websites).write(dict(values, user_id=company._get_public_user().id))
|
||||
|
||||
result = super(Website, self).write(values)
|
||||
result = super(Website, self - public_user_to_change_websites).write(values)
|
||||
if 'cdn_activated' in values or 'cdn_url' in values or 'cdn_filters' in values:
|
||||
# invalidate the caches from static node at compile time
|
||||
self.env['ir.qweb'].clear_caches()
|
||||
|
||||
@@ -947,6 +947,10 @@ registry.gallerySlider = Animation.extend({
|
||||
destroy: function () {
|
||||
this._super.apply(this, arguments);
|
||||
|
||||
if (!this.$indicator) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.$prev.prependTo(this.$indicator);
|
||||
this.$next.appendTo(this.$indicator);
|
||||
this.$carousel.off('.gallery_slider');
|
||||
|
||||
@@ -12,6 +12,7 @@ var ready = $.when(domReady, session.is_bound, ajax.loadXML());
|
||||
|
||||
tour.register('rte_translator', {
|
||||
test: true,
|
||||
url: '/',
|
||||
wait_for: ready,
|
||||
}, [{
|
||||
content: "click on Add a language",
|
||||
|
||||
@@ -420,7 +420,7 @@
|
||||
</div>
|
||||
</li>
|
||||
<li groups="website.group_website_publisher" class="list-inline-item">
|
||||
<t t-set="url_return" t-value="url_for('', '[lang]') + '?' + keep_query()"/>
|
||||
<t t-set="url_return" t-value="quote_plus(url_for('', '[lang]') + '?' + keep_query())"/>
|
||||
<a t-attf-href="/web#action=base.action_view_base_language_install&website_id=#{website.id if website else ''}&url_return=#{url_return}">
|
||||
<i class="fa fa-plus-circle"/>
|
||||
Add a language...
|
||||
|
||||
@@ -74,5 +74,8 @@
|
||||
</div>
|
||||
</div>
|
||||
</xpath>
|
||||
<xpath expr="//div[@data-js='layout_column']" position="attributes">
|
||||
<attribute name="data-exclude" add=".s_latest_posts, .s_latest_posts_big_picture" separator=","/>
|
||||
</xpath>
|
||||
</template>
|
||||
</odoo>
|
||||
|
||||
@@ -164,13 +164,9 @@
|
||||
<t t-if="any([p.website_published for p in partner.implemented_partner_ids])">
|
||||
<h3 id="references">References</h3>
|
||||
<div t-foreach="partner.implemented_partner_ids" t-if="reference.website_published" t-as="reference" class="media mt-3">
|
||||
<a t-attf-href="/customers/#{slug(reference)}">
|
||||
<span t-field="reference.image_medium" t-options='{"widget": "image", "class": "d-block mr-3"}'/>
|
||||
</a>
|
||||
<span t-field="reference.image_medium" t-options='{"widget": "image", "class": "d-block mr-3"}'/>
|
||||
<div class="media-body" style="min-height: 64px;">
|
||||
<a t-attf-href="/customers/#{slug(reference)}">
|
||||
<span t-field="reference.self"/>
|
||||
</a>
|
||||
<span t-field="reference.self"/>
|
||||
<div t-field='reference.website_short_description'/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -234,4 +234,13 @@
|
||||
</t>
|
||||
</template>
|
||||
|
||||
<template id="references_block_href" inherit_id="website_crm_partner_assign.references_block" name="Partner References Block">
|
||||
<xpath expr="//div/span" position="replace">
|
||||
<a t-attf-href="/customers/#{slug(reference)}">$0</a>
|
||||
</xpath>
|
||||
<xpath expr="//div[hasclass('media-body')]/span" position="replace">
|
||||
<a t-attf-href="/customers/#{slug(reference)}">$0</a>
|
||||
</xpath>
|
||||
</template>
|
||||
|
||||
</odoo>
|
||||
|
||||
@@ -251,10 +251,11 @@ class Post(models.Model):
|
||||
operator = operator == "=" and '!=' or '='
|
||||
value = True
|
||||
|
||||
if self._uid == SUPERUSER_ID:
|
||||
user = self.env.user
|
||||
# Won't impact sitemap, search() in converter is forced as public user
|
||||
if user._is_admin():
|
||||
return [(1, '=', 1)]
|
||||
|
||||
user = self.env['res.users'].browse(self._uid)
|
||||
req = """
|
||||
SELECT p.id
|
||||
FROM forum_post p
|
||||
|
||||
@@ -26,6 +26,11 @@ class MailMessage(models.Model):
|
||||
description = fields.Char(compute="_compute_description", help='Message description: either the subject, or the beginning of the body')
|
||||
website_published = fields.Boolean(string='Published', help="Visible on the website as a comment", copy=False)
|
||||
|
||||
@api.model
|
||||
def _non_employee_message_domain(self):
|
||||
domain = super(MailMessage, self)._non_employee_message_domain()
|
||||
return expression.AND([domain, [('website_published', '=', True)]])
|
||||
|
||||
@api.multi
|
||||
def _compute_description(self):
|
||||
for message in self:
|
||||
|
||||
@@ -65,12 +65,11 @@ class WebsiteSaleDelivery(WebsiteSale):
|
||||
def _update_website_sale_delivery_return(self, order, **post):
|
||||
carrier_id = int(post['carrier_id'])
|
||||
currency = order.currency_id
|
||||
delivery_price = order.order_line.filtered(lambda line: line.is_delivery).price_unit
|
||||
if order:
|
||||
return {'status': order.delivery_rating_success,
|
||||
'error_message': order.delivery_message,
|
||||
'carrier_id': carrier_id,
|
||||
'new_amount_delivery': self._format_amount(delivery_price, currency),
|
||||
'new_amount_delivery': self._format_amount(order.amount_delivery, currency),
|
||||
'new_amount_untaxed': self._format_amount(order.amount_untaxed, currency),
|
||||
'new_amount_tax': self._format_amount(order.amount_tax, currency),
|
||||
'new_amount_total': self._format_amount(order.amount_total, currency),
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
Germany, 2019-05-14
|
||||
|
||||
ecoservice GbR agrees to the terms of the Odoo Corporate Contributor License
|
||||
Agreement v1.0.
|
||||
|
||||
I declare that I am authorized and able to make this agreement and sign this
|
||||
declaration.
|
||||
|
||||
Signed,
|
||||
|
||||
Falk Neubert neubert@ecoservice.de https://github.com/FNeu75
|
||||
|
||||
List of contributors:
|
||||
|
||||
* Christian Engelhardt c.engelhardt@ecoservice.de https://github.com/c-engelhardt-ecoservice
|
||||
* Christian Schöttke c.schoettke@ecoservice.de https://github.com/cschoettke
|
||||
* Falk Neubert neubert@ecoservice.de https://github.com/FNeu75
|
||||
* Gülhan Celik g.celik@ecoservice.de https://github.com/guelhancelik
|
||||
* Jan Brodersen 4rmitxes@gmail.com https://github.com/Armitxes
|
||||
* Kamal Prajapati k.prajapati@ecoservice.de https://github.com/kamalprajapati
|
||||
* Marcel Much m.much@ecoservice.de https://github.com/mmuch
|
||||
@@ -12,17 +12,24 @@ João Alfredo Gama Batista joao.gama@savoirfairelinux.com https://github.com/joa
|
||||
|
||||
List of contributors:
|
||||
|
||||
Agathe Mollé agathe.molle@savoirfairelinux.com https://github.com/Ehtaga
|
||||
Bruno Joliveau bruno.joliveau@savoirfairelinux.com https://github.com/bjoliveau
|
||||
David Dufresne david.dufresne@savoirfairelinux.com https://github.com/dufresnedavid
|
||||
Istvan SZALAÏ istvan.szalai@savoirfairelinux.com https://github.com/ventilooo
|
||||
João Alfredo Gama Batista joao.gama@savoirfairelinux.com https://github.com/joaoalf
|
||||
Julien Jezequel-Breard julien.jezequel-breard@savoirfairelinux.com https://github.com/jjbreard
|
||||
Quentin Lavallée-Bourdeau quentin.lavallee@savoirfairelinux.com https://github.com/qtiplb
|
||||
|
||||
Rim Ben Dhaou rim.bendhaou@savoirfairelinux.com https://github.com/rimbendhaou
|
||||
Yasmine El Mrini yasmine.elmrini@savoirfairelinux.com https://github.com/yasmineelmrini
|
||||
Luis Garcia Ruiz luis.garcia@savoirfairelinux.com https://github.com/eilst
|
||||
Dimitar Apostolov dimitar.apostolov@savoirfairelinux.com https://github.com/dapostolov-sfl
|
||||
Pierre Gault pierre.gault@savoirfairelinux.com https://github.com/gaultp
|
||||
Maroua Romdhane maroua.romdhane@savoirfairelinux.com https://github.com/mromdhane
|
||||
Jean-François Bolduc jean-francois.bolduc@savoirfairelinux.com https://github.com/fjdrake
|
||||
Blaise Frison blaise.frison@savoirfairelinux.com https://github.com/bfrison-sfl
|
||||
Larbi Gharib larbi.gharib@savoirfairelinux.com https://github.com/lgharib
|
||||
Jananjoy Rajkumar jananjoy.rajkumar@savoirfairelinux.com https://github.com/jananjoy
|
||||
Yasmine El Mrini yasmine.elmrini@savoirfairelinux.com https://github.com/yasmineelmrini (up to 2019-05-13)
|
||||
Agathe Mollé agathe.molle@savoirfairelinux.com https://github.com/Ehtaga (up to 2019-05-13)
|
||||
Bruno Joliveau bruno.joliveau@savoirfairelinux.com https://github.com/bjoliveau (up to 2019-05-13)
|
||||
David Dufresne david.dufresne@savoirfairelinux.com https://github.com/dufresnedavid (up to 2019-05-13)
|
||||
Istvan SZALAÏ istvan.szalai@savoirfairelinux.com https://github.com/ventilooo (up to 2019-05-13)
|
||||
João Alfredo Gama Batista joao.gama@savoirfairelinux.com https://github.com/joaoalf (up to 2019-05-13)
|
||||
Julien Jezequel-Breard julien.jezequel-breard@savoirfairelinux.com https://github.com/jjbreard (up to 2019-05-13)
|
||||
Quentin Lavallée-Bourdeau quentin.lavallee@savoirfairelinux.com https://github.com/qtiplb (up to 2019-05-13)
|
||||
Jérome Boisvert-Chouinard jerome.boisvertchouinard@savoirfairelinux.com https://github.com/jbchouinard (up to 2017-10-17)
|
||||
Julie Moussu julie.mousse@savoirfairelinux.com https://github.com/JulieSFL (up to 2017-10-17)
|
||||
Pierre Lamarche pierre.lamarche@savoirfairelinux.com https://github.com/plamarche (up to 2017-10-17)
|
||||
@@ -34,6 +41,5 @@ Jordi Riera jordi.riera@savoirfairelinux.com https://github.com/foutoucour (up t
|
||||
Julien Roux julien.roux@savoirfairelinux.com https://github.com/jrouxsfl (up to 2017-05-12)
|
||||
Loïc Faure-Lacroix loic.lacroix@savoirfairelinux.com https://github.com/llacroix (up to 2017-05-12)
|
||||
Maxime Chambreuil maxime.chambreuil@savoirfairelinux.com https://github.com/max3903 (up to 2017-05-12)
|
||||
Pierre Gault pierre.gault@savoirfairelinux.com https://github.com/gaultp (up to 2017-05-12)
|
||||
Sandy Carter sandy.carter@savoirfairelinux.com https://github.com/bwrsandman (up to 2017-05-12)
|
||||
Vincent Vinet vincent.vinet@savoirfairelinux.com https://github.com/veloutin (up to 2017-05-12)
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
Belgium, 2019-05-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,
|
||||
|
||||
Geert Janssens geert@kobaltwit.be https://github.com/gjanssens
|
||||
@@ -119,10 +119,18 @@ def encode_rfc2822_address_header(header_text):
|
||||
# Header as a string, using an unlimited line length.", the old one
|
||||
# was "A synonym for Header.encode()." so call encode() directly?
|
||||
name = Header(pycompat.to_text(name)).encode()
|
||||
return formataddr((name, email))
|
||||
# if the from does not follow the (name <addr>),* convention, we might
|
||||
# try to encode meaningless strings as address, as getaddresses is naive
|
||||
# note it would also fail on real addresses with non-ascii characters
|
||||
try:
|
||||
return formataddr((name, email))
|
||||
except UnicodeEncodeError:
|
||||
_logger.warning(_('Failed to encode the address %s\n'
|
||||
'from mail header:\n%s') % addr, header_text)
|
||||
return ""
|
||||
|
||||
addresses = getaddresses([pycompat.to_text(ustr(header_text))])
|
||||
return COMMASPACE.join(encode_addr(a) for a in addresses)
|
||||
return COMMASPACE.join(a for a in (encode_addr(addr) for addr in addresses) if a)
|
||||
|
||||
|
||||
class IrMailServer(models.Model):
|
||||
|
||||
@@ -1166,9 +1166,9 @@ class UsersView(models.Model):
|
||||
group_multi_company = self.env.ref('base.group_multi_company', False)
|
||||
if group_multi_company and 'company_ids' in values:
|
||||
if len(user.company_ids) <= 1 and user.id in group_multi_company.users.ids:
|
||||
group_multi_company.write({'users': [(3, user.id)]})
|
||||
user.write({'groups_id': [(3, group_multi_company.id)]})
|
||||
elif len(user.company_ids) > 1 and user.id not in group_multi_company.users.ids:
|
||||
group_multi_company.write({'users': [(4, user.id)]})
|
||||
user.write({'groups_id': [(4, group_multi_company.id)]})
|
||||
return user
|
||||
|
||||
@api.multi
|
||||
@@ -1179,9 +1179,9 @@ class UsersView(models.Model):
|
||||
if group_multi_company and 'company_ids' in values:
|
||||
for user in self:
|
||||
if len(user.company_ids) <= 1 and user.id in group_multi_company.users.ids:
|
||||
group_multi_company.write({'users': [(3, user.id)]})
|
||||
user.write({'groups_id': [(3, group_multi_company.id)]})
|
||||
elif len(user.company_ids) > 1 and user.id not in group_multi_company.users.ids:
|
||||
group_multi_company.write({'users': [(4, user.id)]})
|
||||
user.write({'groups_id': [(4, group_multi_company.id)]})
|
||||
return res
|
||||
|
||||
def _remove_reified_groups(self, values):
|
||||
|
||||
@@ -1,10 +1 @@
|
||||
from odoo import fields, models
|
||||
|
||||
|
||||
class Usered(models.Model):
|
||||
_name = 'test_convert.usered'
|
||||
_description = "z test model ignore"
|
||||
|
||||
name = fields.Char()
|
||||
user_id = fields.Many2one('res.users', default=lambda self: self.env.user)
|
||||
tz = fields.Char(default=lambda self: self.env.context.get('tz') or self.env.user.tz)
|
||||
from . import models # noqa
|
||||
|
||||
@@ -1,2 +1,3 @@
|
||||
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
|
||||
access_test_convert_usered,access_test_convert_usered,model_test_convert_usered,base.group_user,1,1,1,1
|
||||
access_test_convert_test_model,access_test_convert_test_model,model_test_convert_test_model,,1,1,1,1
|
||||
|
@@ -0,0 +1,25 @@
|
||||
from odoo import api, fields, models
|
||||
|
||||
class TestModel(models.Model):
|
||||
_name = 'test_convert.test_model'
|
||||
_description = "Test Convert Model"
|
||||
|
||||
@api.model
|
||||
def action_test_date(self, today_date):
|
||||
return True
|
||||
|
||||
@api.model
|
||||
def action_test_time(self, cur_time):
|
||||
return True
|
||||
|
||||
@api.model
|
||||
def action_test_timezone(self, timezone):
|
||||
return True
|
||||
|
||||
class Usered(models.Model):
|
||||
_name = 'test_convert.usered'
|
||||
_description = "z test model ignore"
|
||||
|
||||
name = fields.Char()
|
||||
user_id = fields.Many2one('res.users', default=lambda self: self.env.user)
|
||||
tz = fields.Char(default=lambda self: self.env.context.get('tz') or self.env.user.tz)
|
||||
@@ -4,8 +4,10 @@
|
||||
import collections
|
||||
import unittest
|
||||
|
||||
from lxml import etree as ET
|
||||
from lxml.builder import E
|
||||
|
||||
import odoo
|
||||
from odoo.tests import common
|
||||
from odoo.tools.convert import _eval_xml
|
||||
|
||||
@@ -16,6 +18,25 @@ class TestEvalXML(common.TransactionCase):
|
||||
def eval_xml(self, node, obj=None):
|
||||
return _eval_xml(obj, node, self.env)
|
||||
|
||||
def test_function_eval(self):
|
||||
def id_get(): pass
|
||||
Obj = collections.namedtuple('Obj', ['module', 'idref', 'id_get'])
|
||||
obj = Obj('test_convert', {}, id_get)
|
||||
|
||||
try:
|
||||
test_datetime = ET.XML("<function name='action_test_date' model='test_convert.test_model' eval='[datetime.now().strftime(\"%Y-%m-%d %H:%M:%S\")]'/>")
|
||||
self.eval_xml(node=test_datetime, obj=obj)
|
||||
test_time = ET.XML("<function name='action_test_time' model='test_convert.test_model' eval='[time.strftime(\"%Y-%m-%d %H:%M:%S\")]'/>")
|
||||
self.eval_xml(node=test_time, obj=obj)
|
||||
test_timedelta = ET.XML("<function name='action_test_date' model='test_convert.test_model' eval='[(datetime.today()-timedelta(days=365)).strftime(\"%Y-%m-%d %H:%M:%S\")]'/>")
|
||||
self.eval_xml(node=test_timedelta, obj=obj)
|
||||
test_relativedelta = ET.XML("<function name='action_test_date' model='test_convert.test_model' eval='[(datetime.today()+relativedelta(months=3)).strftime(\"%Y-%m-%d %H:%M:%S\")]'/>")
|
||||
self.eval_xml(node=test_relativedelta, obj=obj)
|
||||
test_timezone = ET.XML("<function name='action_test_timezone' model='test_convert.test_model' eval='[pytz.timezone(\"Asia/Calcutta\")]'/>")
|
||||
self.eval_xml(node=test_timezone, obj=obj)
|
||||
except ValueError as e:
|
||||
self.fail(e.message)
|
||||
|
||||
def test_char(self):
|
||||
self.assertEqual(
|
||||
self.eval_xml(Field("foo")),
|
||||
|
||||
@@ -329,7 +329,7 @@ class ThreadedServer(CommonServer):
|
||||
os._exit(0)
|
||||
# interrupt run() to start shutdown
|
||||
raise KeyboardInterrupt()
|
||||
elif sig == signal.SIGXCPU:
|
||||
elif hasattr(signal, 'SIGXCPU') and sig == signal.SIGXCPU:
|
||||
sys.stderr.write("CPU time limit exceeded! Shutting down immediately\n")
|
||||
sys.stderr.flush()
|
||||
os._exit(0)
|
||||
|
||||
@@ -178,18 +178,20 @@ def _eval_xml(self, node, env):
|
||||
return res
|
||||
elif node.tag == "function":
|
||||
a_eval = node.get('eval')
|
||||
model_str = node.get('model')
|
||||
if a_eval:
|
||||
self.idref['ref'] = self.id_get
|
||||
idref2 = _get_idref(self, env, model_str, self.idref)
|
||||
args = safe_eval(a_eval, idref2)
|
||||
# ensure the args are a list (sometimes folks eval a tuple which
|
||||
# is inconvenient when trying to concatenate w/ a list)
|
||||
args = list(safe_eval(a_eval, self.idref))
|
||||
args = list(safe_eval(a_eval, idref2))
|
||||
else:
|
||||
args = [
|
||||
r for r in (_eval_xml(self, n, env) for n in node)
|
||||
if r is not None
|
||||
]
|
||||
|
||||
model = env[node.get('model')]
|
||||
model = env[model_str]
|
||||
method_name = node.get('name')
|
||||
method = getattr(model, method_name)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user