diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 00000000000..98eb36db26e --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,40 @@ +# Security Policy + +## Supported Versions + +| Version | Supported | +| ------- | ------------------ | +| 12.0 | :white_check_mark: | +| 11.0 | :white_check_mark: | +| 10.0 | :white_check_mark: | +| <=9.0 | :x: | + +## Reporting a Vulnerability + +Please share privately the details of your security vulnerability by contacting our Security Team: +[Contact Info](https://www.odoo.com/security-report) + +Make sure to include as much information as possible, with the detailed steps to reproduce the problem, +the versions that are affected, the expected results and actual results, and any other information that +might help us react faster and more efficiently. + +We tend to prefer _text-based descriptions_ accompanied with a proof-of-concept script/exploit, rather +than screenshots and videos. + +Our [Responsible Disclosure](https://www.odoo.com/security-report) page gives an overview of the +process, including: + + - Our Incident Response Procedure (what will happen after you report an issue) + - Our Rules (what you can and cannot do while researching security issues) + - Guidelines with **DO REPORT** and **DO NOT REPORT** issues + (what kind of issues will be accepted/rejected) + + +## Important note + +We receive a majority of security reports that have little to no impact on the security of Odoo or +the Odoo Cloud, and we ultimately have to reject them. To avoid a disappointing experience when +contacting us, please try to put together a proof-of-concept attack and take a critical look at +what's really at risk. +If the proposed attack scenario turns out unrealistic, your report will probably be rejected. +Also be sure to review our list of [non-qualifying issues](https://www.odoo.com/security-report#what). diff --git a/addons/account/models/account.py b/addons/account/models/account.py index 0931f8ebd3d..6be56cd4795 100644 --- a/addons/account/models/account.py +++ b/addons/account/models/account.py @@ -1067,18 +1067,20 @@ class AccountTax(models.Model): @api.model def default_get(self, vals): - rslt = super(AccountTax, self).default_get(vals) + # company_id is added so that we are sure to fetch a default value from it to use in repartition lines, below + rslt = super(AccountTax, self).default_get(vals + ['company_id']) + company_id = rslt.get('company_id') if 'refund_repartition_line_ids' in vals: rslt['refund_repartition_line_ids'] = [ - (0, 0, { 'repartition_type': 'base', 'factor_percent': 100.0, 'tag_ids': [], 'company_id': rslt.get('company_id')}), - (0, 0, { 'repartition_type': 'tax', 'factor_percent': 100.0, 'tag_ids': [], 'company_id': rslt.get('company_id')}), + (0, 0, { 'repartition_type': 'base', 'factor_percent': 100.0, 'tag_ids': [], 'company_id': company_id}), + (0, 0, { 'repartition_type': 'tax', 'factor_percent': 100.0, 'tag_ids': [], 'company_id': company_id}), ] if 'invoice_repartition_line_ids' in vals: rslt['invoice_repartition_line_ids'] = [ - (0, 0, { 'repartition_type': 'base', 'factor_percent': 100.0, 'tag_ids': [], 'company_id': rslt.get('company_id')}), - (0, 0, { 'repartition_type': 'tax', 'factor_percent': 100.0, 'tag_ids': [], 'company_id': rslt.get('company_id')}), + (0, 0, { 'repartition_type': 'base', 'factor_percent': 100.0, 'tag_ids': [], 'company_id': company_id}), + (0, 0, { 'repartition_type': 'tax', 'factor_percent': 100.0, 'tag_ids': [], 'company_id': company_id}), ] return rslt @@ -1174,7 +1176,8 @@ class AccountTax(models.Model): str(invoice_tax_val['account_id']) + '-' + \ str(invoice_tax_val['account_analytic_id']) + '-' + \ str(invoice_tax_val.get('analytic_tag_ids', [])) + '-' + \ - str(invoice_tax_val.get('tax_ids') or []) + str(invoice_tax_val.get('tax_ids') or []) + '-' + \ + str(invoice_tax_val.get('tag_ids') or []) def _compute_amount(self, base_amount, price_unit, quantity=1.0, product=None, partner=None): """ Returns the amount of a single tax. base_amount is the actual amount on which the tax is applied, which is @@ -1235,6 +1238,10 @@ class AccountTax(models.Model): all_taxes += tax return all_taxes + def get_tax_tags(self, is_refund, repartition_type): + rep_lines = self.mapped(is_refund and 'refund_repartition_line_ids' or 'invoice_repartition_line_ids') + return rep_lines.filtered(lambda x: x.repartition_type == repartition_type).mapped('tag_ids') + @api.multi def compute_all(self, price_unit, currency=None, quantity=1.0, product=None, partner=None, is_refund=False): """ Returns all information required to apply taxes (in self + their children in case of a tax group). @@ -1392,12 +1399,23 @@ class AccountTax(models.Model): if tax.price_include and not total_included_checkpoints.get(i): cumulated_tax_included_amount += tax_amount + # If the tax affects the base of subsequent taxes, its tax move lines must + # receive the base tags and tag_ids of these taxes, so that the tax report computes + # the right total + subsequent_taxes = self.env['account.tax'] + subsequent_tags = self.env['account.account.tag'] + if tax.include_base_amount: + subsequent_taxes = taxes[i+1:] + subsequent_tags = subsequent_taxes.get_tax_tags(is_refund, 'base') + + # Compute the tax lines tax_repartition_lines = is_refund and tax.refund_repartition_line_ids or tax.invoice_repartition_line_ids repartition_lines_to_treat = len(tax_repartition_lines) total_amount = 0 for repartition_line in tax_repartition_lines.filtered(lambda x: x.repartition_type == 'tax'): # In case some rounding error occurs, we compensate for it on the last line line_amount = round(sign * tax_amount * repartition_line.factor if repartition_lines_to_treat != 1 else tax_amount - total_amount, prec) + taxes_vals.append({ 'id': tax.id, 'name': partner and tax.with_context(lang=partner.lang).name or tax.name, @@ -1409,7 +1427,8 @@ class AccountTax(models.Model): 'price_include': tax.price_include, 'tax_exigibility': tax.tax_exigibility, 'tax_repartition_line_id': repartition_line.id, - 'tag_ids': [(6, False, repartition_line.tag_ids.ids)], + 'tag_ids': [(6, False, (repartition_line.tag_ids + subsequent_tags).ids)], + 'tax_ids': [(6, False, subsequent_taxes.ids)] }) total_amount += line_amount diff --git a/addons/account/models/account_bank_statement.py b/addons/account/models/account_bank_statement.py index 8cea4c9bd92..7537227fdae 100644 --- a/addons/account/models/account_bank_statement.py +++ b/addons/account/models/account_bank_statement.py @@ -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 '' @@ -683,6 +630,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 @@ -705,12 +654,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: @@ -760,36 +729,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_invoice.py b/addons/account/models/account_invoice.py index 47c0cb42b27..8770b232808 100644 --- a/addons/account/models/account_invoice.py +++ b/addons/account/models/account_invoice.py @@ -879,7 +879,7 @@ class AccountInvoice(models.Model): domain = {} company_id = self.company_id.id p = self.partner_id if not company_id else self.partner_id.with_context(force_company=company_id) - type = self.type + type = self.type or self.env.context.get('type', 'out_invoice') if p: rec_account = p.property_account_receivable_id pay_account = p.property_account_payable_id @@ -1081,7 +1081,7 @@ class AccountInvoice(models.Model): else: return self.env.ref('account.invoice_form').id - def _prepare_tax_line_vals(self, line, tax, tax_ids): + def _prepare_tax_line_vals(self, line, tax): ''' Prepare values to create an account.invoice.tax line. :param line: An account.invoice.line record. :param tax: Tax values outputted by compute_all() in account.tax. @@ -1099,8 +1099,9 @@ class AccountInvoice(models.Model): 'account_analytic_id': tax['analytic'] and line.account_analytic_id.id or False, 'account_id': tax['account_id'] or line.account_id.id, 'analytic_tag_ids': tax['analytic'] and line.analytic_tag_ids.ids or False, - 'tax_ids': tax_ids and [(6, None, tax_ids)] or False, 'tax_repartition_line_id': tax.get('tax_repartition_line_id'), # For base amount, we let this field empty + 'tag_ids': tax['tag_ids'], + 'tax_ids': tax['tax_ids'], } # If the taxes generate moves on the same financial account as the invoice line, @@ -1130,7 +1131,6 @@ class AccountInvoice(models.Model): price_unit = line.price_unit * (1 - (line.discount or 0.0) / 100.0) taxes = line.invoice_line_tax_ids._origin.compute_all(price_unit, self.currency_id, line.quantity, line.product_id, self.partner_id, is_refund=self.type in ('in_refund', 'out_refund'))['taxes'] - affecting_base_tax_ids = [] for tax_vals in taxes: # Retrieve the tax record (not in tax_vals when dealing with group of taxes). if tax_map.get(tax_vals['id']): @@ -1138,7 +1138,7 @@ class AccountInvoice(models.Model): else: tax = tax_map[tax_vals['id']] = self.env['account.tax'].browse(tax_vals['id']) - val = self._prepare_tax_line_vals(line, tax_vals, affecting_base_tax_ids) + val = self._prepare_tax_line_vals(line, tax_vals) key = tax.get_grouping_key(val) if key not in tax_grouped: @@ -1148,11 +1148,6 @@ class AccountInvoice(models.Model): for field in default_tax_group_fields: tax_grouped[key][field] += round_curr(val.get(field)) or 0 - if tax.amount_type == "group": - affecting_base_tax_ids += tax.children_tax_ids.filtered(lambda t: is_tax_affecting_base_amount(t)).ids - elif is_tax_affecting_base_amount(tax): - affecting_base_tax_ids.append(tax.id) - return tax_grouped @api.multi @@ -1268,7 +1263,6 @@ class AccountInvoice(models.Model): def tax_line_move_line_get(self): self.ensure_one() - res = [] # loop the invoice.tax.line in reversal sequence for tax_line in sorted(self.tax_line_ids, key=lambda x: -x.sequence): @@ -1288,7 +1282,8 @@ class AccountInvoice(models.Model): 'invoice_id': self.id, 'tax_ids': tax_line.tax_ids and [(6, 0, tax_line.tax_ids.ids)] or False, # We don't pass an empty recordset here, as it would reset the tax_exibility of the line, due to the condition in account.move.line's create 'tax_repartition_line_id': tax_line.tax_repartition_line_id.id, - 'tag_ids': [(6, 0, tax_line.tax_repartition_line_id.tag_ids.ids)], + 'tag_ids': [(6, 0, tax_line.tag_ids.ids)], + 'tax_base_amount': tax_line.base, }) return res @@ -2146,8 +2141,8 @@ class AccountInvoiceTax(models.Model): company_id = fields.Many2one('res.company', string='Company', related='account_id.company_id', store=True, readonly=True) currency_id = fields.Many2one('res.currency', related='invoice_id.currency_id', store=True, readonly=True) base = fields.Monetary(string='Base') - tax_ids = fields.Many2many('account.tax', string='Affecting Base Taxes', - help='Taxes affecting the tax base amount applied before this one.') + tax_ids = fields.Many2many('account.tax', string='Affecting Base Taxes', help='Taxes whose base amount needs has to be affected by this tax line.') + tag_ids = fields.Many2many(string="Tags", comodel_name='account.account.tag', help="The taxes that will be applied on the move line generated for this tax entry") @api.depends('amount', 'amount_rounding') def _compute_amount_total(self): diff --git a/addons/account/models/account_journal_dashboard.py b/addons/account/models/account_journal_dashboard.py index cf1f40f7a94..08b119405b4 100644 --- a/addons/account/models/account_journal_dashboard.py +++ b/addons/account/models/account_journal_dashboard.py @@ -3,6 +3,7 @@ from datetime import datetime, timedelta from babel.dates import format_datetime, format_date from odoo import models, api, _, fields +from odoo.osv import expression from odoo.release import version from odoo.tools import DEFAULT_SERVER_DATE_FORMAT as DF, safe_eval from odoo.tools.misc import formatLang, format_date as odoo_format_date @@ -427,10 +428,16 @@ class account_journal(models.Model): action_name = 'action_view_bank_statement_tree' elif self.type == 'sale': action_name = 'action_invoice_tree1' - self = self.with_context(use_domain=[('journal_id', '=', self.id)]) + use_domain = expression.AND( + [self.env.context.get('use_domain', []), [('journal_id', '=', self.id)]] + ) + self = self.with_context(use_domain=use_domain) elif self.type == 'purchase': action_name = 'action_vendor_bill_template' - self = self.with_context(use_domain=[('journal_id', '=', self.id)]) + use_domain = expression.AND( + [self.env.context.get('use_domain', []), [('journal_id', '=', self.id)]] + ) + self = self.with_context(use_domain=use_domain) else: action_name = 'action_move_journal_line' diff --git a/addons/account/models/account_move.py b/addons/account/models/account_move.py index 8fb43c81d3d..19e3ea5dec4 100644 --- a/addons/account/models/account_move.py +++ b/addons/account/models/account_move.py @@ -87,14 +87,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 @@ -290,6 +288,7 @@ class AccountMove(models.Model): 'company_currency_id': self.company_id.currency_id.id, 'tax_repartition_line_id': line_vals['tax_repartition_line_id'], 'tag_ids': line_vals['tag_ids'], + 'tax_base_amount': line_vals['base'], } # N.B. currency_id/amount_currency are not set because if we have two lines with the same tax # and different currencies, we have no idea which currency set on this line. @@ -580,7 +579,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 @@ -609,15 +608,6 @@ class AccountMoveLine(models.Model): currency = self.env['account.journal'].browse(context['default_journal_id']).currency_id return currency - @api.depends('move_id.line_ids', 'move_id.line_ids.tax_line_id', 'move_id.line_ids.debit', 'move_id.line_ids.credit') - def _compute_tax_base_amount(self): - for move_line in self: - if move_line.tax_line_id: - base_lines = move_line.move_id.line_ids.filtered(lambda line: move_line.tax_line_id in line.tax_ids) - move_line.tax_base_amount = abs(sum(base_lines.mapped('balance'))) - else: - move_line.tax_base_amount = 0 - @api.depends('move_id') def _compute_parent_state(self): for record in self.filtered('move_id'): @@ -641,7 +631,7 @@ class AccountMoveLine(models.Model): help="The residual amount on a journal item expressed in the company currency.") amount_residual_currency = fields.Monetary(compute='_amount_residual', string='Residual Amount in Currency', store=True, help="The residual amount on a journal item expressed in its currency (possibly not the company currency).") - tax_base_amount = fields.Monetary(string="Base Amount", compute='_compute_tax_base_amount', currency_field='company_currency_id', store=True) + tax_base_amount = fields.Monetary(string="Base Amount", currency_field='company_currency_id') account_id = fields.Many2one('account.account', string='Account', required=True, index=True, ondelete="cascade", domain=[('deprecated', '=', False)], default=lambda self: self._context.get('account_id', False)) move_id = fields.Many2one('account.move', string='Journal Entry', ondelete="cascade", @@ -699,21 +689,23 @@ class AccountMoveLine(models.Model): if {'debit', 'credit', 'partner_id', 'account_id'}.isdisjoint(fields): return rec #compute the default credit/debit of the next line in case of a manual entry - AccountMove = self.env['account.move'] + move = self.env['account.move'].new({'line_ids': self._context['line_ids']}) balance = 0 - for line in AccountMove.resolve_2many_commands( - 'line_ids', self._context['line_ids'], fields=['credit', 'debit']): - balance += line.get('debit', 0) - line.get('credit', 0) - if len(self._context['line_ids']) > 1: - lines = AccountMove.resolve_2many_commands('line_ids', self._context['line_ids'][-2:], fields=['partner_id', 'account_id']) - if lines[0].get('partner_id', False) == lines[1].get('partner_id', False): - rec.update({'partner_id': lines[0].get('partner_id', False)}) - if lines[0].get('account_id', False) == lines[1].get('account_id', False): - rec.update({'account_id': lines[0].get('account_id', False)}) + for line in move.line_ids: + balance += line.debit - line.credit if balance < 0: rec.update({'debit': -balance}) if balance > 0: rec.update({'credit': balance}) + + # Set the partner/account to the same value as the two last lines if they are equal + if len(move.line_ids) > 1: + last_lines = move.line_ids[-2:] + if last_lines[0].account_id == last_lines[1].account_id: + rec['account_id'] = last_lines[0].account_id.id + if last_lines[0].partner_id == last_lines[1].partner_id: + rec['partner_id'] = last_lines[0].partner_id.id + return rec @api.multi @@ -821,21 +813,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 @@ -998,7 +991,10 @@ class AccountMoveLine(models.Model): debit_moves = debit_moves.sorted(key=lambda a: (a.date_maturity or a.date, a.currency_id)) credit_moves = credit_moves.sorted(key=lambda a: (a.date_maturity or a.date, a.currency_id)) # Compute on which field reconciliation should be based upon: - field = self[0].account_id.currency_id and 'amount_residual_currency' or 'amount_residual' + if self[0].account_id.currency_id and self[0].account_id.currency_id != self[0].account_id.company_id.currency_id: + field = 'amount_residual_currency' + else: + field = 'amount_residual' #if all lines share the same currency, use amount_residual_currency to avoid currency rounding error if self[0].currency_id and all([x.amount_currency and x.currency_id == self[0].currency_id for x in self]): field = 'amount_residual_currency' @@ -1712,6 +1708,7 @@ class AccountPartialReconcile(models.Model): 'move_id': newly_created_move.id, 'partner_id': line.partner_id.id, 'tax_repartition_line_id': line.tax_repartition_line_id.id, + 'tax_base_amount': line.tax_base_amount, 'tag_ids': [(6, 0, line.tag_ids.ids)], }) if line.account_id.reconcile: @@ -1737,6 +1734,7 @@ class AccountPartialReconcile(models.Model): 'amount_currency': self.amount_currency and line.currency_id.round(line.amount_currency * amount / line.balance) or 0.0, 'partner_id': line.partner_id.id, 'tax_repartition_line_id': line.tax_repartition_line_id.id, + 'tax_base_amount': line.tax_base_amount, 'tag_ids': [(6, 0, line.tag_ids.ids)], }) self.env['account.move.line'].with_context(check_move_validity=False).create({ diff --git a/addons/account/models/chart_template.py b/addons/account/models/chart_template.py index 53b36274de1..6409ae13445 100644 --- a/addons/account/models/chart_template.py +++ b/addons/account/models/chart_template.py @@ -894,7 +894,9 @@ class AccountTaxTemplate(models.Model): 'account_dict': dictionary containing a to-do list with all the accounts to assign on new taxes } """ - ChartTemplate = self.env['account.chart.template'] + # default_company_id is needed in context to allow creation of default + # repartition lines on taxes + ChartTemplate = self.env['account.chart.template'].with_context(default_company_id=company.id) todo_dict = {'account.tax': {}, 'account.tax.repartition.line': {}} tax_template_to_tax = {} @@ -926,7 +928,7 @@ class AccountTaxTemplate(models.Model): # We also have to delay the assignation of accounts to repartition lines all_tax_rep_lines = tax.invoice_repartition_line_ids + tax.refund_repartition_line_ids all_template_rep_lines = template.invoice_repartition_line_ids + template.refund_repartition_line_ids - for i in range(0, len(all_tax_rep_lines)): + for i in range(0, len(all_template_rep_lines)): # We assume template and tax repartition lines are in the same order template_account = all_template_rep_lines[i].account_id if template_account: diff --git a/addons/account/models/product.py b/addons/account/models/product.py index 655b457bbf0..f9b046c652a 100644 --- a/addons/account/models/product.py +++ b/addons/account/models/product.py @@ -81,4 +81,5 @@ class ProductProduct(models.Model): 'analytic_tag_ids': line.get('analytic_tag_ids', False), 'tax_repartition_line_id': line.get('tax_repartition_line_id'), 'tag_ids': line.get('tag_ids'), + 'tax_base_amount': line.get('tax_base_amount'), } diff --git a/addons/account/models/reconciliation_widget.py b/addons/account/models/reconciliation_widget.py index 0d6577fe34c..f10bacfa3f3 100644 --- a/addons/account/models/reconciliation_widget.py +++ b/addons/account/models/reconciliation_widget.py @@ -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) diff --git a/addons/account/report/account_invoice_report.py b/addons/account/report/account_invoice_report.py index 594a49d831e..b94bd711c63 100644 --- a/addons/account/report/account_invoice_report.py +++ b/addons/account/report/account_invoice_report.py @@ -112,13 +112,13 @@ class AccountInvoiceReport(models.Model): 1 AS nbr, ai.id AS invoice_id, ai.type, ai.state, pt.categ_id, ai.date_due, ai.account_id, ail.account_id AS account_line_id, ai.partner_bank_id, - SUM ((invoice_type.sign_qty * ail.quantity) / u.factor * u2.factor) AS product_qty, + SUM ((invoice_type.sign_qty * ail.quantity) / COALESCE(u.factor,1) * COALESCE(u2.factor,1)) AS product_qty, SUM(ail.price_subtotal_signed * invoice_type.sign) AS price_total, - (ai.amount_total * invoice_type.sign) / (SELECT count(*) FROM account_invoice_line l where invoice_id = ai.id) * + ai.amount_total_signed / (SELECT count(*) FROM account_invoice_line l where invoice_id = ai.id) * count(*) * invoice_type.sign AS amount_total, SUM(ABS(ail.price_subtotal_signed)) / CASE - WHEN SUM(ail.quantity / u.factor * u2.factor) <> 0::numeric - THEN SUM(ail.quantity / u.factor * u2.factor) + WHEN SUM(ail.quantity / COALESCE(u.factor,1) * COALESCE(u2.factor,1)) <> 0::numeric + THEN SUM(ail.quantity / COALESCE(u.factor,1) * COALESCE(u2.factor,1)) ELSE 1::numeric END AS price_average, ai.residual_company_signed / (SELECT count(*) FROM account_invoice_line l where invoice_id = ai.id) * diff --git a/addons/account/static/src/js/reconciliation/reconciliation_action.js b/addons/account/static/src/js/reconciliation/reconciliation_action.js index 67d5de9de08..5f27b0970f6 100644 --- a/addons/account/static/src/js/reconciliation/reconciliation_action.js +++ b/addons/account/static/src/js/reconciliation/reconciliation_action.js @@ -346,9 +346,12 @@ var StatementAction = AbstractAction.extend({ 'context': self.model.getContext(), }); _.each(result.handles, function (handle) { - self._getWidget(handle).destroy(); - var index = _.findIndex(self.widgets, function (widget) {return widget.handle===handle;}); - self.widgets.splice(index, 1); + var widget = self._getWidget(handle); + if (widget) { + widget.destroy(); + var index = _.findIndex(self.widgets, function (widget) {return widget.handle===handle;}); + self.widgets.splice(index, 1); + } }); // Get number of widget and if less than constant and if there are more to laod, load until constant if (self.widgets.length < self.model.defaultDisplayQty diff --git a/addons/account/static/src/js/reconciliation/reconciliation_model.js b/addons/account/static/src/js/reconciliation/reconciliation_model.js index c28790796d2..d4dce5581ca 100644 --- a/addons/account/static/src/js/reconciliation/reconciliation_model.js +++ b/addons/account/static/src/js/reconciliation/reconciliation_model.js @@ -623,13 +623,13 @@ var StatementModel = BasicModel.extend({ prop = this._formatQuickCreate(line); line.reconciliation_proposition.push(prop); } - if (!line.reconciliation_proposition.slice(0,-1).every(function(prop) {return prop.to_check == values.to_check;})) { + if ('to_check' in values && !line.reconciliation_proposition.slice(0,-1).every(function(prop) {return prop.to_check == values.to_check;})) { new CrashManager().show_warning({data: { exception_type: _t("Incorrect Operation"), message: _t("You cannot mix items with and without the 'To Check' checkbox ticked.") }}); // FIXME: model should not be tied to the DOM ! - $('.create_to_check input').click(); + $('.create_to_check input:visible').prop('checked', !values.to_check).change(); return Promise.resolve(); } _.each(values, function (value, fieldName) { diff --git a/addons/account/static/src/js/reconciliation/reconciliation_renderer.js b/addons/account/static/src/js/reconciliation/reconciliation_renderer.js index b59b99bc881..53cd0b76598 100644 --- a/addons/account/static/src/js/reconciliation/reconciliation_renderer.js +++ b/addons/account/static/src/js/reconciliation/reconciliation_renderer.js @@ -938,10 +938,12 @@ var ManualLineRenderer = LineRenderer.extend({ * @override */ _renderCreate: function (state) { - var parentPromise = this._super(state); - this.$('.create .create_journal_id').show(); - this.$('.create .create_date').removeClass('d-none'); - this.$('.create .create_journal_id .o_input').addClass('o_required_modifier'); + var self = this; + var parentPromise = this._super(state).then(function() { + self.$('.create .create_journal_id').show(); + self.$('.create .create_date').removeClass('d-none'); + self.$('.create .create_journal_id .o_input').addClass('o_required_modifier'); + }); return parentPromise; }, diff --git a/addons/account/tests/test_account_customer_invoice.py b/addons/account/tests/test_account_customer_invoice.py index 5334151eebb..1a84c82cda2 100644 --- a/addons/account/tests/test_account_customer_invoice.py +++ b/addons/account/tests/test_account_customer_invoice.py @@ -306,30 +306,30 @@ class TestAccountCustomerInvoice(AccountTestUsers): dashboard_data = journal.get_journal_dashboard_datas() self.assertEquals(dashboard_data['number_draft'], 2) - self.assertEquals(dashboard_data['sum_draft'], '$ 68.42') + self.assertIn('68.42', dashboard_data['sum_draft']) self.assertEquals(dashboard_data['number_waiting'], 0) - self.assertEquals(dashboard_data['sum_waiting'], '$ 0.00') + self.assertIn('0.00', dashboard_data['sum_waiting']) # Check Both invoice.action_invoice_open() dashboard_data = journal.get_journal_dashboard_datas() self.assertEquals(dashboard_data['number_draft'], 1) - self.assertEquals(dashboard_data['sum_draft'], '$ -13.30') + self.assertIn('-13.30', dashboard_data['sum_draft']) self.assertEquals(dashboard_data['number_waiting'], 1) - self.assertEquals(dashboard_data['sum_waiting'], '$ 81.72') + self.assertIn('81.72', dashboard_data['sum_waiting']) # Check waiting payment refund.action_invoice_open() dashboard_data = journal.get_journal_dashboard_datas() self.assertEquals(dashboard_data['number_draft'], 0) - self.assertEquals(dashboard_data['sum_draft'], '$ 0.00') + self.assertIn('0.00', dashboard_data['sum_draft']) self.assertEquals(dashboard_data['number_waiting'], 2) - self.assertEquals(dashboard_data['sum_waiting'], '$ 68.42') + self.assertIn('68.42', dashboard_data['sum_waiting']) # Check partial receivable_account = refund.move_id.line_ids.mapped('account_id').filtered(lambda a: a.internal_type == 'receivable') @@ -353,12 +353,12 @@ class TestAccountCustomerInvoice(AccountTestUsers): dashboard_data = journal.get_journal_dashboard_datas() self.assertEquals(dashboard_data['number_draft'], 0) - self.assertEquals(dashboard_data['sum_draft'], '$ 0.00') + self.assertIn('0.00', dashboard_data['sum_draft']) self.assertEquals(dashboard_data['number_waiting'], 2) - self.assertEquals(dashboard_data['sum_waiting'], '$ 78.42') + self.assertIn('78.42', dashboard_data['sum_waiting']) with patch('odoo.fields.Date.today', patched_today): dashboard_data = journal.get_journal_dashboard_datas() self.assertEquals(dashboard_data['number_late'], 2) - self.assertEquals(dashboard_data['sum_late'], '$ 78.42') + self.assertIn('78.42', dashboard_data['sum_late']) diff --git a/addons/account/tests/test_invoice_taxes.py b/addons/account/tests/test_invoice_taxes.py index 4b0d8380f72..b6759271579 100644 --- a/addons/account/tests/test_invoice_taxes.py +++ b/addons/account/tests/test_invoice_taxes.py @@ -100,8 +100,8 @@ class TestInvoiceTaxes(AccountingTestCase): Expected: Tax | Taxes | Base | Amount -------------------------------------------- - 21% incl | / | 100 | 21 - 12% | 21% incl | 121 | 14.52 + 21% incl | 12% | 100 | 21 + 12% | / | 121 | 14.52 12% | / | 100 | 12 ''' invoice = self._create_invoice([ @@ -110,9 +110,8 @@ class TestInvoiceTaxes(AccountingTestCase): ]) invoice.action_invoice_open() self.assertRecordValues(invoice.tax_line_ids.sorted(lambda x: x.amount), [ - {'name': self.percent_tax_2.name, 'base': 100, 'amount': 12, 'tax_ids': []}, - {'name': self.percent_tax_2.name, 'base': 121, 'amount': 14.52, 'tax_ids': [self.percent_tax_1_incl.id]}, - {'name': self.percent_tax_1_incl.name, 'base': 100, 'amount': 21, 'tax_ids': []}, + {'name': self.percent_tax_1_incl.name, 'base': 100, 'amount': 21, 'tax_ids': [self.percent_tax_2.id]}, + {'name': self.percent_tax_2.name, 'base': 221, 'amount': 26.52, 'tax_ids': []}, ]) def test_group_of_taxes(self): @@ -135,9 +134,8 @@ class TestInvoiceTaxes(AccountingTestCase): ]) invoice.action_invoice_open() self.assertRecordValues(invoice.tax_line_ids.sorted(lambda x: x.amount), [ - {'name': self.percent_tax_2.name, 'base': 100, 'amount': 12, 'tax_ids': []}, - {'name': self.percent_tax_2.name, 'base': 121, 'amount': 14.52, 'tax_ids': [self.percent_tax_1_incl.id]}, - {'name': self.percent_tax_1_incl.name, 'base': 100, 'amount': 21, 'tax_ids': []}, + {'name': self.percent_tax_1_incl.name, 'base': 100, 'amount': 21, 'tax_ids': [self.percent_tax_2.id]}, + {'name': self.percent_tax_2.name, 'base': 221, 'amount': 26.52, 'tax_ids': []}, ]) def _create_tax_tag(self, tag_name): diff --git a/addons/account/tests/test_reconciliation.py b/addons/account/tests/test_reconciliation.py index 62d414fdf54..0a35310f8a7 100644 --- a/addons/account/tests/test_reconciliation.py +++ b/addons/account/tests/test_reconciliation.py @@ -91,6 +91,11 @@ class TestReconciliation(AccountingTestCase): 'code': 'CABA', 'type': 'general', }) + self.general_journal = self.env['account.journal'].create({ + 'name': 'general', + 'code': 'GENE', + 'type': 'general', + }) # Tax Cash Basis self.tax_cash_basis = self.env['account.tax'].create({ @@ -1304,6 +1309,7 @@ class TestReconciliationExec(TestReconciliation): 'debit': 16.67, 'move_id': purchase_move.id, 'tax_repartition_line_id': self.tax_cash_basis.invoice_repartition_line_ids.filtered(lambda x: x.repartition_type == 'tax').id, + 'tax_base_amount': 83.33, }) purchase_move.post() @@ -1422,6 +1428,7 @@ class TestReconciliationExec(TestReconciliation): 'debit': 5, 'move_id': purchase_move.id, 'tax_repartition_line_id': tax_cash_basis10percent.invoice_repartition_line_ids.filtered(lambda x: x.repartition_type == 'tax').id, + 'tax_base_amount': 50, }) AccountMoveLine.create({ 'name': 'expenseTaxed 20%', @@ -1436,6 +1443,7 @@ class TestReconciliationExec(TestReconciliation): 'debit': 16.67, 'move_id': purchase_move.id, 'tax_repartition_line_id': self.tax_cash_basis.invoice_repartition_line_ids.filtered(lambda x: x.repartition_type == 'tax').id, + 'tax_base_amount': 83.33, }) purchase_move.post() @@ -1646,6 +1654,7 @@ class TestReconciliationExec(TestReconciliation): 'currency_id': self.currency_usd_id, 'amount_currency': 848.16, 'tax_repartition_line_id': self.tax_cash_basis.invoice_repartition_line_ids.filtered(lambda x: x.repartition_type == 'tax').id, + 'tax_base_amount': 106841.65, }) purchase_payable_line0 = aml_obj.create({ 'name': 'Payable', @@ -1816,6 +1825,7 @@ class TestReconciliationExec(TestReconciliation): 'debit': 17094.66, 'move_id': purchase_move.id, 'tax_repartition_line_id': self.tax_cash_basis.invoice_repartition_line_ids.filtered(lambda x: x.repartition_type == 'tax').id, + 'tax_base_amount': 106841.65, 'currency_id': self.currency_usd_id, 'amount_currency': 848.16, }) @@ -1975,6 +1985,7 @@ class TestReconciliationExec(TestReconciliation): 'debit': 5, 'move_id': purchase_move.id, 'tax_repartition_line_id': tax_cash_basis10percent.invoice_repartition_line_ids.filtered(lambda x: x.repartition_type == 'tax').id, + 'tax_base_amount': 50, }) AccountMoveLine.create({ 'name': 'expenseTaxed 20%', @@ -1989,6 +2000,7 @@ class TestReconciliationExec(TestReconciliation): 'debit': 20, 'move_id': purchase_move.id, 'tax_repartition_line_id': self.tax_cash_basis.invoice_repartition_line_ids.filtered(lambda x: x.repartition_type == 'tax').id, + 'tax_base_amount': 100, }) purchase_move.post() @@ -2056,6 +2068,71 @@ class TestReconciliationExec(TestReconciliation): self.assertTrue(pay_receivable_line1.reconciled) self.assertEqual(pay_receivable_line1.matched_debit_ids, move_caba1.tax_cash_basis_rec_id) + def test_reconciliation_with_currency(self): + #reconciliation on an account having a foreign currency being + #the same as the company one + account_rcv = self.account_rcv + account_rcv.currency_id = self.currency_euro_id + aml_obj = self.env['account.move.line'].with_context( + check_move_validity=False) + general_move1 = self.env['account.move'].create({ + 'name': 'general1', + 'journal_id': self.general_journal.id, + }) + aml_obj.create({ + 'name': 'debit1', + 'account_id': account_rcv.id, + 'debit': 11, + 'move_id': general_move1.id, + }) + aml_obj.create({ + 'name': 'credit1', + 'account_id': self.account_rsa.id, + 'credit': 11, + 'move_id': general_move1.id, + }) + general_move1.post() + general_move2 = self.env['account.move'].create({ + 'name': 'general2', + 'journal_id': self.general_journal.id, + }) + aml_obj.create({ + 'name': 'credit2', + 'account_id': account_rcv.id, + 'credit': 10, + 'move_id': general_move2.id, + }) + aml_obj.create({ + 'name': 'debit2', + 'account_id': self.account_rsa.id, + 'debit': 10, + 'move_id': general_move2.id, + }) + general_move2.post() + general_move3 = self.env['account.move'].create({ + 'name': 'general3', + 'journal_id': self.general_journal.id, + }) + aml_obj.create({ + 'name': 'credit3', + 'account_id': account_rcv.id, + 'credit': 1, + 'move_id': general_move3.id, + }) + aml_obj.create({ + 'name': 'debit3', + 'account_id': self.account_rsa.id, + 'debit': 1, + 'move_id': general_move3.id, + }) + general_move3.post() + to_reconcile = ((general_move1 + general_move2 + general_move3) + .mapped('line_ids') + .filtered(lambda l: l.account_id.id == account_rcv.id)) + to_reconcile.reconcile() + for aml in to_reconcile: + self.assertEqual(aml.amount_residual, 0.0) + def test_reconciliation_process_move_lines_with_mixed_currencies(self): # Delete any old rate - to make sure that we use the ones we need. old_rates = self.env['res.currency.rate'].search( diff --git a/addons/account/views/account_invoice_view.xml b/addons/account/views/account_invoice_view.xml index b965b67f488..a55aafc795e 100644 --- a/addons/account/views/account_invoice_view.xml +++ b/addons/account/views/account_invoice_view.xml @@ -384,6 +384,7 @@ + @@ -655,6 +656,7 @@ + diff --git a/addons/account/views/account_move_views.xml b/addons/account/views/account_move_views.xml index 0eadb5cdcf8..aa2e1a35123 100644 --- a/addons/account/views/account_move_views.xml +++ b/addons/account/views/account_move_views.xml @@ -357,6 +357,7 @@ + diff --git a/addons/account/views/account_payment_view.xml b/addons/account/views/account_payment_view.xml index 772a29affff..a3e175bdf20 100644 --- a/addons/account/views/account_payment_view.xml +++ b/addons/account/views/account_payment_view.xml @@ -99,7 +99,8 @@ - + + - + diff --git a/addons/hr_attendance_presence/views/hr_employee.xml b/addons/hr_attendance_presence/views/hr_employee.xml index c49871522a2..a645e594b20 100644 --- a/addons/hr_attendance_presence/views/hr_employee.xml +++ b/addons/hr_attendance_presence/views/hr_employee.xml @@ -4,7 +4,7 @@ hr.employee hr.employee - + 20 diff --git a/addons/hr_contract/models/hr_contract.py b/addons/hr_contract/models/hr_contract.py index 366624ab8f4..f8662c1f55f 100644 --- a/addons/hr_contract/models/hr_contract.py +++ b/addons/hr_contract/models/hr_contract.py @@ -60,12 +60,12 @@ class Contract(models.Model): @api.constrains('employee_id', 'state', 'date_start', 'date_end') def _check_current_contract(self): - """ Two contracts in state [incoming | pending | open | close] cannot overlap """ - for contract in self.filtered(lambda c: c.state not in ['draft', 'cancel']): + """ Two contracts in state [incoming | pending | open] cannot overlap """ + for contract in self.filtered(lambda c: c.state not in ['draft', 'cancel', 'close']): domain = [ ('id', '!=', contract.id), ('employee_id', '=', contract.employee_id.id), - ('state', 'in', ['incoming', 'pending', 'open', 'close']), + ('state', 'in', ['incoming', 'pending', 'open']), ] if not contract.date_end: diff --git a/addons/hr_expense/models/hr_expense.py b/addons/hr_expense/models/hr_expense.py index 5ece94762a4..5c865fc053e 100644 --- a/addons/hr_expense/models/hr_expense.py +++ b/addons/hr_expense/models/hr_expense.py @@ -151,14 +151,14 @@ class HrExpense(models.Model): @api.model def get_empty_list_help(self, help_message): - if help_message and "oe_view_nocontent_smiling_face" not in help_message: + if help_message and "o_view_nocontent_smiling_face" not in help_message: use_mailgateway = self.env['ir.config_parameter'].sudo().get_param('hr_expense.use_mailgateway') alias_record = use_mailgateway and self.env.ref('hr_expense.mail_alias_expense') or False if alias_record and alias_record.alias_domain and alias_record.alias_name: link = "%(email)s" % { 'email': '%s@%s' % (alias_record.alias_name, alias_record.alias_domain) } - return '

%s

%s

' % ( + return '

%s

%s

' % ( _('Add a new expense,'), _('or send receipts by email to %s.') % (link),) return super(HrExpense, self).get_empty_list_help(help_message) @@ -211,6 +211,25 @@ class HrExpense(models.Model): # Business # ---------------------------------------- + @api.multi + def _prepare_move_values(self): + """ + This function prepares move values related to an expense + """ + self.ensure_one() + journal = self.sheet_id.bank_journal_id if self.payment_mode == 'company_account' else self.sheet_id.journal_id + account_date = self.sheet_id.accounting_date or self.date + move_values = { + 'journal_id': journal.id, + 'company_id': self.env.company.id, + 'date': account_date, + 'ref': self.sheet_id.name, + # force the name to the default value, to avoid an eventual 'default_name' in the context + # to set it to '' which cause no number to be given to the account.move when posted. + 'name': '/', + } + return move_values + @api.multi def _get_account_move_by_sheet(self): """ Return a mapping between the expense sheet of current expense and its account move @@ -219,18 +238,8 @@ class HrExpense(models.Model): move_grouped_by_sheet = {} for expense in self: # create the move that will contain the accounting entries - account_date = expense.sheet_id.accounting_date or expense.date if expense.sheet_id.id not in move_grouped_by_sheet: - journal = expense.sheet_id.bank_journal_id if expense.payment_mode == 'company_account' else expense.sheet_id.journal_id - move = self.env['account.move'].create({ - 'journal_id': journal.id, - 'company_id': self.env.company.id, - 'date': account_date, - 'ref': expense.sheet_id.name, - # force the name to the default value, to avoid an eventual 'default_name' in the context - # to set it to '' which cause no number to be given to the account.move when posted. - 'name': '/', - }) + move = self.env['account.move'].create(expense._prepare_move_values()) move_grouped_by_sheet[expense.sheet_id.id] = move else: move = move_grouped_by_sheet[expense.sheet_id.id] @@ -325,6 +334,7 @@ class HrExpense(models.Model): 'amount_currency': amount_currency if different_currency else 0.0, 'account_id': tax['account_id'] or move_line_src['account_id'], 'tax_repartition_line_id': tax['tax_repartition_line_id'], + 'tax_base_amount': tax['base'], 'expense_id': expense.id, 'partner_id': partner_id, 'currency_id': expense.currency_id.id if different_currency else False, diff --git a/addons/hr_holidays/models/hr_employee.py b/addons/hr_holidays/models/hr_employee.py index 459cf0a642d..3ff7b0ebcf6 100644 --- a/addons/hr_holidays/models/hr_employee.py +++ b/addons/hr_holidays/models/hr_employee.py @@ -153,8 +153,8 @@ class HrEmployeeBase(models.AbstractModel): hr_vals['manager_id'] = values['parent_id'] if values.get('department_id') is not None: hr_vals['department_id'] = values['department_id'] - holidays = self.env['hr.leave'].search(['|', ('state', 'in', ['draft', 'confirm']), ('date_from', '>', today_date), ('employee_id', 'in', self.ids)]) + holidays = self.env['hr.leave'].sudo().search(['|', ('state', 'in', ['draft', 'confirm']), ('date_from', '>', today_date), ('employee_id', 'in', self.ids)]) holidays.write(hr_vals) - allocations = self.env['hr.leave.allocation'].search([('state', 'in', ['draft', 'confirm']), ('employee_id', 'in', self.ids)]) + allocations = self.env['hr.leave.allocation'].sudo().search([('state', 'in', ['draft', 'confirm']), ('employee_id', 'in', self.ids)]) allocations.write(hr_vals) return res diff --git a/addons/hr_holidays/models/hr_leave.py b/addons/hr_holidays/models/hr_leave.py index add502f82fb..ec3ee75f609 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, 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__) @@ -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 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) @@ -355,11 +358,17 @@ class HolidaysRequest(models.Model): self.mode_company_id = False self.department_id = False + @api.multi + def _sync_employee_details(self): + for holiday in self: + holiday.manager_id = holiday.employee_id.parent_id.id + if holiday.employee_id: + holiday.department_id = holiday.employee_id.department_id + @api.onchange('employee_id') def _onchange_employee_id(self): - self.manager_id = self.employee_id.parent_id.id - if self.employee_id: - self.department_id = self.employee_id.department_id + self._sync_employee_details() + self.holiday_status_id = False @api.onchange('date_from', 'date_to', 'employee_id') def _onchange_leave_dates(self): @@ -385,6 +394,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): @@ -413,7 +432,7 @@ class HolidaysRequest(models.Model): def _check_date(self): for holiday in self: domain = [ - ('date_from', '<=', holiday.date_to), + ('date_from', '<', holiday.date_to), ('date_to', '>', holiday.date_from), ('employee_id', '=', holiday.employee_id.id), ('id', '!=', holiday.id), @@ -456,7 +475,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 @@ -466,11 +488,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 @@ -511,7 +540,7 @@ class HolidaysRequest(models.Model): if holiday.validation_type == 'hr': holiday.message_subscribe(partner_ids=(holiday.employee_id.parent_id.user_id.partner_id | holiday.employee_id.leave_manager_id.partner_id).ids) if employee_id: - holiday._onchange_employee_id() + holiday._sync_employee_details() if 'number_of_days' not in values and ('date_from' in values or 'date_to' in values): holiday._onchange_leave_dates() if leave_type.validation_type == 'no_validation': @@ -550,7 +579,7 @@ class HolidaysRequest(models.Model): for holiday in self: if employee_id: holiday.add_follower(employee_id) - holiday._onchange_employee_id() + self._sync_employee_details() if 'number_of_days' not in values and ('date_from' in values or 'date_to' in values): holiday._onchange_leave_dates() return result diff --git a/addons/hr_holidays/models/hr_leave_allocation.py b/addons/hr_holidays/models/hr_leave_allocation.py index 91aa37b2e2d..c400d47aedf 100644 --- a/addons/hr_holidays/models/hr_leave_allocation.py +++ b/addons/hr_holidays/models/hr_leave_allocation.py @@ -218,7 +218,10 @@ class HolidaysAllocation(models.Model): @api.depends('number_of_days', 'employee_id') def _compute_number_of_hours_display(self): for allocation in self: - allocation.number_of_hours_display = allocation.number_of_days * (allocation.employee_id.resource_calendar_id.hours_per_day or HOURS_PER_DAY) + if allocation.parent_id and allocation.parent_id.type_request_unit == "hour": + allocation.number_of_hours_display = allocation.number_of_days * HOURS_PER_DAY + else: + allocation.number_of_hours_display = allocation.number_of_days * (allocation.employee_id.resource_calendar_id.hours_per_day or HOURS_PER_DAY) @api.multi @api.depends('number_of_hours_display', 'number_of_days_display') diff --git a/addons/hr_holidays/tests/test_leave_requests.py b/addons/hr_holidays/tests/test_leave_requests.py index 737e904b438..eacd2a396e0 100644 --- a/addons/hr_holidays/tests/test_leave_requests.py +++ b/addons/hr_holidays/tests/test_leave_requests.py @@ -158,9 +158,9 @@ class TestLeaveRequests(TestHrHolidaysBase): self.employee_hrmanager.write({'department_id': self.hr_dept.id}) self.assertFalse(self.env['hr.leave'].search([('employee_id', 'in', self.hr_dept.member_ids.ids)])) leave_form = Form(self.env['hr.leave'].sudo(self.user_hrmanager), view='hr_holidays.hr_leave_view_form_manager') - leave_form.holiday_status_id = self.holidays_type_1 leave_form.holiday_type = 'department' leave_form.department_id = self.hr_dept + leave_form.holiday_status_id = self.holidays_type_1 leave = leave_form.save() leave.action_approve() member_ids = self.hr_dept.member_ids.ids diff --git a/addons/hr_holidays/views/hr_leave_views.xml b/addons/hr_holidays/views/hr_leave_views.xml index 1208b7f4ab6..8ff0691879e 100644 --- a/addons/hr_holidays/views/hr_leave_views.xml +++ b/addons/hr_holidays/views/hr_leave_views.xml @@ -387,7 +387,7 @@ - + diff --git a/addons/hr_maintenance/views/hr_views.xml b/addons/hr_maintenance/views/hr_views.xml index 070328c9464..826ddcf2d44 100644 --- a/addons/hr_maintenance/views/hr_views.xml +++ b/addons/hr_maintenance/views/hr_views.xml @@ -7,7 +7,7 @@