diff options
| author | Christophe Simonis <chs@odoo.com> | 2019-05-17 15:50:59 +0200 |
|---|---|---|
| committer | Christophe Simonis <chs@odoo.com> | 2019-05-17 15:50:59 +0200 |
| commit | d785adba6c09b0773bdd874182f450ba2ebe7c6d (patch) | |
| tree | 5189f5dc30c84515eda62c4d6a512f552ae8f900 | |
| parent | 8968a620105e2e79f2aa9d8db36b5408b89e92ee (diff) | |
| parent | c545783b95b15f65a9798003b614f75685dc434e (diff) | |
[MERGE] forward port branch 12.0 up to c545783b95b
63 files changed, 490 insertions, 245 deletions
diff --git a/addons/account/models/account_bank_statement.py b/addons/account/models/account_bank_statement.py index 9ab3bf91e69..aff0ef405e1 100644 --- a/addons/account/models/account_bank_statement.py +++ b/addons/account/models/account_bank_statement.py @@ -435,7 +435,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 @@ -443,23 +442,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 @@ -468,8 +463,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 \ @@ -480,71 +476,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 '' @@ -664,6 +611,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 @@ -684,12 +633,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: @@ -739,36 +708,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'}) diff --git a/addons/account/models/account_move.py b/addons/account/models/account_move.py index a2f95a4624d..3e984a3337c 100644 --- a/addons/account/models/account_move.py +++ b/addons/account/models/account_move.py @@ -54,14 +54,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.one @@ -524,7 +522,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 @@ -733,21 +731,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 diff --git a/addons/account/models/account_payment.py b/addons/account/models/account_payment.py index 2248020d5e7..9de2dbb05ec 100644 --- a/addons/account/models/account_payment.py +++ b/addons/account/models/account_payment.py @@ -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, }) diff --git a/addons/account/models/reconciliation_widget.py b/addons/account/models/reconciliation_widget.py index ad20ae8c653..70f53fbb102 100644 --- a/addons/account/models/reconciliation_widget.py +++ b/addons/account/models/reconciliation_widget.py @@ -78,28 +78,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) diff --git a/addons/account/views/account_payment_view.xml b/addons/account/views/account_payment_view.xml index 70d7c869d8f..58a673c23c3 100644 --- a/addons/account/views/account_payment_view.xml +++ b/addons/account/views/account_payment_view.xml @@ -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'))]" diff --git a/addons/account_payment/controllers/payment.py b/addons/account_payment/controllers/payment.py index 8ac01bd398d..c3922258c6f 100644 --- a/addons/account_payment/controllers/payment.py +++ b/addons/account_payment/controllers/payment.py @@ -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): diff --git a/addons/crm/models/crm_lead.py b/addons/crm/models/crm_lead.py index 36710856502..f9efd9f6250 100644 --- a/addons/crm/models/crm_lead.py +++ b/addons/crm/models/crm_lead.py @@ -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 diff --git a/addons/google_account/models/google_service.py b/addons/google_account/models/google_service.py index 5292a490515..fb9213f96b2 100644 --- a/addons/google_account/models/google_service.py +++ b/addons/google_account/models/google_service.py @@ -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 diff --git a/addons/hr_holidays/models/hr_leave.py b/addons/hr_holidays/models/hr_leave.py index 7aaf0a7097c..c77b8cb4671 100644 --- a/addons/hr_holidays/models/hr_leave.py +++ b/addons/hr_holidays/models/hr_leave.py @@ -13,6 +13,7 @@ from odoo import api, fields, models, 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__) @@ -146,6 +147,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 leave 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) @@ -375,6 +378,16 @@ class HolidaysRequest(models.Model): 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): for holiday in self: @@ -445,7 +458,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 @@ -455,11 +471,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 diff --git a/addons/hr_holidays/views/hr_leave_views.xml b/addons/hr_holidays/views/hr_leave_views.xml index 8614cb5eecf..2918b593701 100644 --- a/addons/hr_holidays/views/hr_leave_views.xml +++ b/addons/hr_holidays/views/hr_leave_views.xml @@ -295,7 +295,7 @@ <field name="name"/> <field name="date_from"/> <field name="date_to"/> - <field name="number_of_days" string="Number of Days" sum="Total leaves"/> + <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"/> diff --git a/addons/hw_escpos/escpos/escpos.py b/addons/hw_escpos/escpos/escpos.py index a92b0d44eb1..d9f40d24816 100644 --- a/addons/hw_escpos/escpos/escpos.py +++ b/addons/hw_escpos/escpos/escpos.py @@ -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): diff --git a/addons/l10n_be_hr_payroll_fleet/__manifest__.py b/addons/l10n_be_hr_payroll_fleet/__manifest__.py index d31865d69b5..3f840a44b74 100644 --- a/addons/l10n_be_hr_payroll_fleet/__manifest__.py +++ b/addons/l10n_be_hr_payroll_fleet/__manifest__.py @@ -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, } diff --git a/addons/l10n_be_hr_payroll_fleet/security/security.xml b/addons/l10n_be_hr_payroll_fleet/security/security.xml new file mode 100644 index 00000000000..3fc21715a4c --- /dev/null +++ b/addons/l10n_be_hr_payroll_fleet/security/security.xml @@ -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> diff --git a/addons/l10n_it/data/account.tax.group.csv b/addons/l10n_it/data/account.tax.group.csv index ec8fffb16bc..c8035a8f072 100644 --- a/addons/l10n_it/data/account.tax.group.csv +++ b/addons/l10n_it/data/account.tax.group.csv @@ -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% diff --git a/addons/l10n_it/data/account.tax.template.csv b/addons/l10n_it/data/account.tax.template.csv index fefd1aa22f8..032b5aff32f 100644 --- a/addons/l10n_it/data/account.tax.template.csv +++ b/addons/l10n_it/data/account.tax.template.csv @@ -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 diff --git a/addons/l10n_it/data/l10n_it_chart_data.xml b/addons/l10n_it/data/l10n_it_chart_data.xml index d05183c2215..bcc8636d527 100644 --- a/addons/l10n_it/data/l10n_it_chart_data.xml +++ b/addons/l10n_it/data/l10n_it_chart_data.xml @@ -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> diff --git a/addons/l10n_mx/data/account_tag_data.xml b/addons/l10n_mx/data/account_tag_data.xml index 244c9b65ff3..1e8b23dacda 100644 --- a/addons/l10n_mx/data/account_tag_data.xml +++ b/addons/l10n_mx/data/account_tag_data.xml @@ -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> diff --git a/addons/mail/models/mail_activity.py b/addons/mail/models/mail_activity.py index b31b906d23f..716d481b898 100644 --- a/addons/mail/models/mail_activity.py +++ b/addons/mail/models/mail_activity.py @@ -145,7 +145,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', diff --git a/addons/mail/wizard/mail_compose_message.py b/addons/mail/wizard/mail_compose_message.py index 46ec87060cf..75b867d3ad5 100644 --- a/addons/mail/wizard/mail_compose_message.py +++ b/addons/mail/wizard/mail_compose_message.py @@ -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} diff --git a/addons/mrp_byproduct/models/mrp_production.py b/addons/mrp_byproduct/models/mrp_production.py index 6903ff8bd7d..a7d0e6a1ffd 100644 --- a/addons/mrp_byproduct/models/mrp_production.py +++ b/addons/mrp_byproduct/models/mrp_production.py @@ -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: diff --git a/addons/mrp_byproduct/views/mrp_bom_views.xml b/addons/mrp_byproduct/views/mrp_bom_views.xml index 9132209d75c..72599f994c2 100644 --- a/addons/mrp_byproduct/views/mrp_bom_views.xml +++ b/addons/mrp_byproduct/views/mrp_bom_views.xml @@ -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> diff --git a/addons/point_of_sale/static/src/js/gui.js b/addons/point_of_sale/static/src/js/gui.js index b1d99e2e199..0439c11cb7d 100644 --- a/addons/point_of_sale/static/src/js/gui.js +++ b/addons/point_of_sale/static/src/js/gui.js @@ -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) { diff --git a/addons/point_of_sale/static/src/xml/pos.xml b/addons/point_of_sale/static/src/xml/pos.xml index 26d315631a8..17c47b154ad 100644 --- a/addons/point_of_sale/static/src/xml/pos.xml +++ b/addons/point_of_sale/static/src/xml/pos.xml @@ -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> diff --git a/addons/point_of_sale/views/point_of_sale_dashboard.xml b/addons/point_of_sale/views/point_of_sale_dashboard.xml index c45bc2e19cf..b630f2e4e12 100644 --- a/addons/point_of_sale/views/point_of_sale_dashboard.xml +++ b/addons/point_of_sale/views/point_of_sale_dashboard.xml @@ -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"/> diff --git a/addons/portal/controllers/mail.py b/addons/portal/controllers/mail.py index dc83f05a3d2..22e861063d8 100644 --- a/addons/portal/controllers/mail.py +++ b/addons/portal/controllers/mail.py @@ -112,7 +112,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(), diff --git a/addons/portal/models/mail_message.py b/addons/portal/models/mail_message.py index 4a5d1f804d9..afe97f1b8ca 100644 --- a/addons/portal/models/mail_message.py +++ b/addons/portal/models/mail_message.py @@ -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)] diff --git a/addons/sale/models/sale.py b/addons/sale/models/sale.py index e980aba5fcf..313f2608277 100644 --- a/addons/sale/models/sale.py +++ b/addons/sale/models/sale.py @@ -1267,7 +1267,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 diff --git a/addons/sale/report/report_all_channels_sales.py b/addons/sale/report/report_all_channels_sales.py index 1638c087d05..473ab1f1362 100644 --- a/addons/sale/report/report_all_channels_sales.py +++ b/addons/sale/report/report_all_channels_sales.py @@ -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 diff --git a/addons/sale_timesheet/models/sale_order.py b/addons/sale_timesheet/models/sale_order.py index c76798bce10..1e1df8677a7 100644 --- a/addons/sale_timesheet/models/sale_order.py +++ b/addons/sale_timesheet/models/sale_order.py @@ -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 diff --git a/addons/sales_team/tests/__init__.py b/addons/sales_team/tests/__init__.py new file mode 100644 index 00000000000..7bb8a11fc5e --- /dev/null +++ b/addons/sales_team/tests/__init__.py @@ -0,0 +1 @@ +from . import test_default_team diff --git a/addons/sales_team/tests/test_default_team.py b/addons/sales_team/tests/test_default_team.py new file mode 100644 index 00000000000..9905272b3d9 --- /dev/null +++ b/addons/sales_team/tests/test_default_team.py @@ -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) diff --git a/addons/snailmail/models/snailmail_letter.py b/addons/snailmail/models/snailmail_letter.py index c2e3a241b1f..7ee847589c9 100644 --- a/addons/snailmail/models/snailmail_letter.py +++ b/addons/snailmail/models/snailmail_letter.py @@ -312,36 +312,10 @@ class SnailmailLetter(models.Model): @api.multi def _snailmail_estimate(self): """ - Send a request to estimate the cost of sending all the documents with - the differents options. - - The JSON object sent is the one generated from self._snailmail_create() - - arguments sent: - { - "documents":[{ - pages: int, - res_id: int (client_side, optional), - res_model: int (client_side, optional), - address: { - country_code: char (country name) - } - }], - 'color': # color on the letter, - 'duplex': # one/two side printing, - } - - The answer of the server is the same JSON object with some additionnal fields: - { - "total_cost": integer, //The cost of sending ALL the documents - body: JSON object (same as body param except new param 'cost' in each documents) - } + Return the numbers of stamps needed to send a letter. + As 1 letter = 1 stamp, we just need to return the number of letters. """ - endpoint = self.env['ir.config_parameter'].sudo().get_param('snailmail.endpoint', DEFAULT_ENDPOINT) - params = self._snailmail_create('estimate') - req = jsonrpc(endpoint + '/iap/snailmail/1/estimate', params=params) - - return req['total_cost'] + return len(self) @api.model def _snailmail_cron(self): diff --git a/addons/stock_account/models/__init__.py b/addons/stock_account/models/__init__.py index 707d4618bda..a11616c850a 100644 --- a/addons/stock_account/models/__init__.py +++ b/addons/stock_account/models/__init__.py @@ -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 diff --git a/addons/stock_account/models/account_reconciliation_widget.py b/addons/stock_account/models/account_reconciliation_widget.py new file mode 100644 index 00000000000..44110053899 --- /dev/null +++ b/addons/stock_account/models/account_reconciliation_widget.py @@ -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 diff --git a/addons/survey/views/assets.xml b/addons/survey/views/assets.xml index 1443e12814f..39b4bdf1215 100644 --- a/addons/survey/views/assets.xml +++ b/addons/survey/views/assets.xml @@ -29,6 +29,12 @@ <link rel="stylesheet" type="text/less" href="/web_editor/static/src/scss/web_editor.common.scss"/> </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"/> diff --git a/addons/web/static/src/js/chrome/abstract_web_client.js b/addons/web/static/src/js/chrome/abstract_web_client.js index 2516b0a7230..2a8e8151560 100644 --- a/addons/web/static/src/js/chrome/abstract_web_client.js +++ b/addons/web/static/src/js/chrome/abstract_web_client.js @@ -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); diff --git a/addons/web/static/src/js/tools/test_menus.js b/addons/web/static/src/js/tools/test_menus.js index c7b1846b1e9..7f69c65a13e 100644 --- a/addons/web/static/src/js/tools/test_menus.js +++ b/addons/web/static/src/js/tools/test_menus.js @@ -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(); diff --git a/addons/web/static/src/js/widgets/date_picker.js b/addons/web/static/src/js/widgets/date_picker.js index 57c935905e5..df9601b3a04 100644 --- a/addons/web/static/src/js/widgets/date_picker.js +++ b/addons/web/static/src/js/widgets/date_picker.js @@ -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', @@ -104,6 +105,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 * unwanted effects (like triggering 'field_changed' events) diff --git a/addons/web/static/tests/fields/basic_fields_tests.js b/addons/web/static/tests/fields/basic_fields_tests.js index 3e69a66b308..0b3e124f9fa 100644 --- a/addons/web/static/tests/fields/basic_fields_tests.js +++ b/addons/web/static/tests/fields/basic_fields_tests.js @@ -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); diff --git a/addons/web_editor/static/src/js/widgets/widgets.js b/addons/web_editor/static/src/js/widgets/widgets.js index dceb9e25412..8c22de39090 100644 --- a/addons/web_editor/static/src/js/widgets/widgets.js +++ b/addons/web_editor/static/src/js/widgets/widgets.js @@ -1399,6 +1399,7 @@ var LinkDialog = Dialog.extend({ } this.data.range = range.create(sc, so, ec, eo); + $(editable).data("range", this.data.range); this.data.range.select(); } else { nodes = dom.ancestor(sc, dom.isAnchor).childNodes; diff --git a/addons/web_tour/static/src/js/tour_manager.js b/addons/web_tour/static/src/js/tour_manager.js index f0521441a9e..f1e6c15933a 100644 --- a/addons/web_tour/static/src/js/tour_manager.js +++ b/addons/web_tour/static/src/js/tour_manager.js @@ -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); diff --git a/addons/website/models/website.py b/addons/website/models/website.py index 122ed2c4dfd..8dea824beb7 100644 --- a/addons/website/models/website.py +++ b/addons/website/models/website.py @@ -139,12 +139,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() diff --git a/addons/website/static/src/js/content/snippets.animation.js b/addons/website/static/src/js/content/snippets.animation.js index 948aae75c60..c02885d38f4 100644 --- a/addons/website/static/src/js/content/snippets.animation.js +++ b/addons/website/static/src/js/content/snippets.animation.js @@ -930,6 +930,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'); diff --git a/addons/website/static/src/js/tours/rte.js b/addons/website/static/src/js/tours/rte.js index 67426a64303..58ddca69424 100644 --- a/addons/website/static/src/js/tours/rte.js +++ b/addons/website/static/src/js/tours/rte.js @@ -6,6 +6,7 @@ var base = require('web_editor.base'); tour.register('rte_translator', { test: true, + url: '/', wait_for: base.ready(), }, [{ content: "click on Add a language", diff --git a/addons/website/views/website_templates.xml b/addons/website/views/website_templates.xml index 1d2a3822c4c..4ecdee3e720 100644 --- a/addons/website/views/website_templates.xml +++ b/addons/website/views/website_templates.xml @@ -401,7 +401,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... diff --git a/addons/website_blog/views/snippets.xml b/addons/website_blog/views/snippets.xml index f8564f6b254..8702928d12d 100644 --- a/addons/website_blog/views/snippets.xml +++ b/addons/website_blog/views/snippets.xml @@ -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> diff --git a/addons/website_crm_partner_assign/views/website_crm_partner_assign_templates.xml b/addons/website_crm_partner_assign/views/website_crm_partner_assign_templates.xml index e8863b571e3..b1e0e863959 100644 --- a/addons/website_crm_partner_assign/views/website_crm_partner_assign_templates.xml +++ b/addons/website_crm_partner_assign/views/website_crm_partner_assign_templates.xml @@ -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> diff --git a/addons/website_customer/views/website_customer_templates.xml b/addons/website_customer/views/website_customer_templates.xml index da6e9aa8a33..9cd668d901d 100644 --- a/addons/website_customer/views/website_customer_templates.xml +++ b/addons/website_customer/views/website_customer_templates.xml @@ -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> diff --git a/addons/website_forum/models/forum.py b/addons/website_forum/models/forum.py index 26cf278bb80..2b2b734249d 100644 --- a/addons/website_forum/models/forum.py +++ b/addons/website_forum/models/forum.py @@ -266,10 +266,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 diff --git a/addons/website_mail/models/mail_message.py b/addons/website_mail/models/mail_message.py index 34609160f1d..683990e1e67 100644 --- a/addons/website_mail/models/mail_message.py +++ b/addons/website_mail/models/mail_message.py @@ -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: diff --git a/addons/website_sale_delivery/controllers/main.py b/addons/website_sale_delivery/controllers/main.py index 28c36ff6d51..05b37c6c4cf 100644 --- a/addons/website_sale_delivery/controllers/main.py +++ b/addons/website_sale_delivery/controllers/main.py @@ -69,7 +69,7 @@ class WebsiteSaleDelivery(WebsiteSale): return {'status': order.delivery_rating_success, 'error_message': order.delivery_message, 'carrier_id': carrier_id, - 'new_amount_delivery': self._format_amount(order.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), diff --git a/addons/website_slides/models/slide_channel.py b/addons/website_slides/models/slide_channel.py index fb56ca97b25..cc47e2b5ac4 100644 --- a/addons/website_slides/models/slide_channel.py +++ b/addons/website_slides/models/slide_channel.py @@ -1,7 +1,7 @@ # -*- coding: utf-8 -*- # Part of Odoo. See LICENSE file for full copyright and licensing details. -from odoo import api, fields, models, SUPERUSER_ID, _ +from odoo import api, fields, models, _ from odoo.addons.http_routing.models.ir_http import slug from odoo.tools.translate import html_translate @@ -107,7 +107,8 @@ class Channel(models.Model): if not value: operator = operator == "=" and '!=' or '=' - if self._uid == SUPERUSER_ID: + # Won't impact sitemap, search() in converter is forced as public user + if self.env.user._is_admin(): return [(1, '=', 1)] # Better perfs to split request and use inner join that left join @@ -204,4 +205,4 @@ class Category(models.Model): record.nbr_documents = result[record.id].get('document', 0) record.nbr_videos = result[record.id].get('video', 0) record.nbr_infographics = result[record.id].get('infographic', 0) - record.total = record.nbr_presentations + record.nbr_documents + record.nbr_videos + record.nbr_infographics
\ No newline at end of file + record.total = record.nbr_presentations + record.nbr_documents + record.nbr_videos + record.nbr_infographics diff --git a/doc/cla/corporate/ecoservice.md b/doc/cla/corporate/ecoservice.md new file mode 100644 index 00000000000..b1b1cbe8ff0 --- /dev/null +++ b/doc/cla/corporate/ecoservice.md @@ -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 diff --git a/doc/cla/corporate/savoirfairelinux.md b/doc/cla/corporate/savoirfairelinux.md index 913e317cb07..04f4563d4b7 100644 --- a/doc/cla/corporate/savoirfairelinux.md +++ b/doc/cla/corporate/savoirfairelinux.md @@ -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) diff --git a/doc/cla/individual/gjanssens.md b/doc/cla/individual/gjanssens.md new file mode 100644 index 00000000000..d3cda29d978 --- /dev/null +++ b/doc/cla/individual/gjanssens.md @@ -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 diff --git a/odoo/addons/base/models/ir_mail_server.py b/odoo/addons/base/models/ir_mail_server.py index 6773e1f61cc..2422b9cf4a9 100644 --- a/odoo/addons/base/models/ir_mail_server.py +++ b/odoo/addons/base/models/ir_mail_server.py @@ -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): diff --git a/odoo/addons/base/models/res_users.py b/odoo/addons/base/models/res_users.py index ca1c20d8cf2..4e5fc90cf2c 100644 --- a/odoo/addons/base/models/res_users.py +++ b/odoo/addons/base/models/res_users.py @@ -1167,9 +1167,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 @@ -1180,9 +1180,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): diff --git a/odoo/addons/test_convert/__init__.py b/odoo/addons/test_convert/__init__.py index a301ab24b52..f1192956fd9 100644 --- a/odoo/addons/test_convert/__init__.py +++ b/odoo/addons/test_convert/__init__.py @@ -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 diff --git a/odoo/addons/test_convert/ir.model.access.csv b/odoo/addons/test_convert/ir.model.access.csv index 0282a269c41..0ba260e485e 100644 --- a/odoo/addons/test_convert/ir.model.access.csv +++ b/odoo/addons/test_convert/ir.model.access.csv @@ -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
\ No newline at end of file diff --git a/odoo/addons/test_convert/models.py b/odoo/addons/test_convert/models.py new file mode 100644 index 00000000000..dbf0429b8ed --- /dev/null +++ b/odoo/addons/test_convert/models.py @@ -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) diff --git a/odoo/addons/test_convert/tests/test_convert.py b/odoo/addons/test_convert/tests/test_convert.py index e81526dd43c..1bb79619c21 100644 --- a/odoo/addons/test_convert/tests/test_convert.py +++ b/odoo/addons/test_convert/tests/test_convert.py @@ -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")), diff --git a/odoo/service/server.py b/odoo/service/server.py index 4b05d197a09..b99d292cb9e 100644 --- a/odoo/service/server.py +++ b/odoo/service/server.py @@ -324,7 +324,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) diff --git a/odoo/tools/convert.py b/odoo/tools/convert.py index e677560979e..0b612f7b9ff 100644 --- a/odoo/tools/convert.py +++ b/odoo/tools/convert.py @@ -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) |
