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_bank_statement.py b/addons/account/models/account_bank_statement.py index 155064820bc..487477dee6a 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 14fe69ffcb5..925c43e2ce0 100644 --- a/addons/account/models/account_invoice.py +++ b/addons/account/models/account_invoice.py @@ -875,7 +875,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 diff --git a/addons/account/models/account_journal_dashboard.py b/addons/account/models/account_journal_dashboard.py index f127893c4f0..853e5ab9e1c 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 873733528d6..e5f4920cc3c 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 @@ -580,7 +578,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 @@ -829,21 +827,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 @@ -1006,7 +1005,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' diff --git a/addons/account/models/reconciliation_widget.py b/addons/account/models/reconciliation_widget.py index db3fb9437a1..05edf105763 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 7861644525a..abc17743e61 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/tests/test_account_customer_invoice.py b/addons/account/tests/test_account_customer_invoice.py index b3821a989c2..5c45aca69fe 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_reconciliation.py b/addons/account/tests/test_reconciliation.py index 01eb2c80c9c..dcc8f0e470c 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({ @@ -2062,3 +2067,68 @@ class TestReconciliationExec(TestReconciliation): pay_receivable_line1 = payment1.move_line_ids.filtered(lambda l: l.account_id == self.account_rcv) 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) 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 cf7ec0d6549..29e9896ac7e 100644 --- a/addons/hr_contract/models/hr_contract.py +++ b/addons/hr_contract/models/hr_contract.py @@ -96,12 +96,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 9ea4c4753b0..4e5207a385a 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.user.company_id.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.user.company_id.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] diff --git a/addons/hr_holidays/models/hr.py b/addons/hr_holidays/models/hr.py index 6a0dae5d5a2..2a8a5506217 100644 --- a/addons/hr_holidays/models/hr.py +++ b/addons/hr_holidays/models/hr.py @@ -206,8 +206,8 @@ class Employee(models.Model): 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 3ae7d554205..01c19561b34 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__) @@ -147,6 +148,8 @@ class HolidaysRequest(models.Model): number_of_hours_display = fields.Float( 'Duration in hours', compute='_compute_number_of_hours_display', copy=False, readonly=True, help='Number of hours of the time off request according to your working schedule. Used for interface.') + duration_display = fields.Char('Requested (Days/Hours)', compute='_compute_duration_display', + help="Field allowing to see the leave request duration in days or hours depending on the leave_type_request_unit") # details # details meeting_id = fields.Many2one('calendar.event', string='Meeting') parent_id = fields.Many2one('hr.leave', string='Parent', copy=False) @@ -356,11 +359,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): @@ -386,6 +395,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): @@ -414,7 +433,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), @@ -457,7 +476,10 @@ class HolidaysRequest(models.Model): res = [] for leave in self: if self.env.context.get('short_name'): - res.append((leave.id, _("%s : %.2f day(s)") % (leave.name or leave.holiday_status_id.name, leave.number_of_days))) + if leave.leave_type_request_unit == 'hour': + res.append((leave.id, _("%s : %.2f hour(s)") % (leave.name or leave.holiday_status_id.name, leave.number_of_hours_display))) + else: + res.append((leave.id, _("%s : %.2f day(s)") % (leave.name or leave.holiday_status_id.name, leave.number_of_days))) else: if leave.holiday_type == 'company': target = leave.mode_company_id.name @@ -467,11 +489,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 @@ -512,7 +541,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': @@ -551,7 +580,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 1a1bacbb83b..50ab5127021 100644 --- a/addons/hr_holidays/models/hr_leave_allocation.py +++ b/addons/hr_holidays/models/hr_leave_allocation.py @@ -207,7 +207,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 9f32d96e6c6..f15f9cfb9e5 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)) - 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 5ff1739f107..e137648e758 100644 --- a/addons/hr_holidays/views/hr_leave_views.xml +++ b/addons/hr_holidays/views/hr_leave_views.xml @@ -313,7 +313,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 @@