[MERGE] forward port branch saas-12.1 up to d785adba6c

This commit is contained in:
Christophe Simonis
2019-05-17 16:32:26 +02:00
60 changed files with 482 additions and 214 deletions
+31 -92
View File
@@ -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'})
+15 -16
View File
@@ -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
+1 -1
View File
@@ -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,
})
+30 -15
View File
@@ -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', '&lt;', 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):
+4 -1
View File
@@ -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
+29 -6
View File
@@ -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
+1 -1
View File
@@ -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"/>
+17 -8
View File
@@ -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%
1 id name
2 tax_group_iva_2 IVA 2%
3 tax_group_iva_4 IVA 4%
4 tax_group_iva_5 IVA 5%
5 tax_group_iva_10 IVA 10%
6 tax_group_iva_12 IVA 12%
7 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
1 id description chart_template_id/id name sequence amount amount_type type_tax_use price_include account_id/id refund_account_id/id tag_ids/id children_tax_ids/id tax_group_id/id
5 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
6 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
7 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
8 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
9 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
10 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
11 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
12 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>
+11 -11
View File
@@ -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>
+1 -1
View File
@@ -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',
+4 -1
View File
@@ -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:
+2 -2
View File
@@ -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>
+4 -1
View File
@@ -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) {
+1 -1
View File
@@ -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"/>
+1 -1
View File
@@ -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(),
+4
View File
@@ -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)]
+1 -1
View File
@@ -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
+1
View File
@@ -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)
+1
View File
@@ -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
+6
View File
@@ -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);
+5 -1
View File
@@ -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);
+6 -3
View File
@@ -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",
+1 -1
View File
@@ -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&amp;website_id=#{website.id if website else ''}&amp;url_return=#{url_return}">
<i class="fa fa-plus-circle"/>
Add a language...
+3
View File
@@ -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>
+3 -2
View File
@@ -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),
+21
View File
@@ -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
+15 -9
View File
@@ -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)
+11
View File
@@ -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
+10 -2
View File
@@ -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):
+4 -4
View File
@@ -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
View File
@@ -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
1 id name model_id:id group_id:id perm_read perm_write perm_create perm_unlink
2 access_test_convert_usered access_test_convert_usered model_test_convert_usered base.group_user 1 1 1 1
3 access_test_convert_test_model access_test_convert_test_model model_test_convert_test_model 1 1 1 1
+25
View File
@@ -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")),
+1 -1
View File
@@ -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)
+5 -3
View File
@@ -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)