diff --git a/addons/account/__manifest__.py b/addons/account/__manifest__.py index 4514659c9d9..3667a01f966 100644 --- a/addons/account/__manifest__.py +++ b/addons/account/__manifest__.py @@ -24,6 +24,7 @@ You could use this simplified accounting in case you work with an (external) acc 'data/digest_data.xml', 'views/account_menuitem.xml', 'views/account_payment_view.xml', + 'wizard/account_invoice_import_view.xml', 'wizard/account_unreconcile_view.xml', 'wizard/account_move_reversal_view.xml', 'views/account_move_views.xml', @@ -49,7 +50,6 @@ You could use this simplified accounting in case you work with an (external) acc 'views/tax_adjustments.xml', 'wizard/wizard_tax_adjustments_view.xml', 'views/res_config_settings_views.xml', - 'views/account_invoice_import_wizard_view.xml', 'views/account_journal_dashboard_view.xml', 'views/account_portal_templates.xml', 'views/report_payment_receipt_templates.xml', diff --git a/addons/account/i18n/account.pot b/addons/account/i18n/account.pot index 036c6a62735..b88b5f39baa 100644 --- a/addons/account/i18n/account.pot +++ b/addons/account/i18n/account.pot @@ -670,6 +670,16 @@ msgstr "" msgid "A tax fiscal position could be defined only one time on same taxes." msgstr "" +#. module: account +#: code:addons/account/models/res_users.py:17 +#, python-format +msgid "A user cannot have both Tax B2B and Tax B2C.\n" +"Problematic user(s): %s\n" +"You should go in General Settings, and choose to display Product Prices\n" +"either in 'Tax-Included' or in 'Tax-Excluded' mode\n" +"(or switch twice the mode if you are already in the desired one)." +msgstr "" + #. module: account #: model:res.groups,name:account.group_warning_account msgid "A warning can be set on a partner (Account)" diff --git a/addons/account/models/__init__.py b/addons/account/models/__init__.py index 95499de5d3d..6f178fd2665 100644 --- a/addons/account/models/__init__.py +++ b/addons/account/models/__init__.py @@ -19,4 +19,4 @@ from . import account_cash_rounding from . import reconciliation_widget from . import account_incoterms from . import digest -from . import account_invoice_import_wizard +from . import res_users diff --git a/addons/account/models/account.py b/addons/account/models/account.py index 3379aa344cc..e9f51aefc74 100644 --- a/addons/account/models/account.py +++ b/addons/account/models/account.py @@ -487,10 +487,10 @@ class AccountJournal(models.Model): bank_id = fields.Many2one('res.bank', related='bank_account_id.bank_id', readonly=False) post_at_bank_rec = fields.Boolean(string="Post At Bank Reconciliation", help="Whether or not the payments made in this journal should be generated in draft state, so that the related journal entries are only posted when performing bank reconciliation.") - # alias configuration for 'purchase' type journals + # alias configuration for journals alias_id = fields.Many2one('mail.alias', string='Alias', copy=False) alias_domain = fields.Char('Alias domain', compute='_compute_alias_domain', default=lambda self: self.env["ir.config_parameter"].sudo().get_param("mail.catchall.domain")) - alias_name = fields.Char('Alias Name for Vendor Bills', related='alias_id.alias_name', help="It creates draft vendor bill by sending an email.", readonly=False) + alias_name = fields.Char('Alias Name', related='alias_id.alias_name', help="It creates draft invoices and bills by sending an email.", readonly=False) journal_group_ids = fields.Many2many('account.journal.group', string="Journal Groups") @@ -583,13 +583,13 @@ class AccountJournal(models.Model): self.default_debit_account_id = self.default_credit_account_id @api.multi - def _get_alias_values(self, alias_name=None): + def _get_alias_values(self, type, alias_name=None): if not alias_name: alias_name = self.name if self.company_id != self.env.ref('base.main_company'): alias_name += '-' + str(self.company_id.name) return { - 'alias_defaults': {'type': 'in_invoice', 'company_id': self.company_id.id}, + 'alias_defaults': {'type': type == 'purchase' and 'in_invoice' or 'out_invoice', 'company_id': self.company_id.id}, 'alias_parent_thread_id': self.id, 'alias_name': re.sub(r'[^\w]+', '-', alias_name) } @@ -617,7 +617,7 @@ class AccountJournal(models.Model): def _update_mail_alias(self, vals): self.ensure_one() - alias_values = self._get_alias_values(alias_name=vals.get('alias_name')) + alias_values = self._get_alias_values(type=vals.get('type') or self.type, alias_name=vals.get('alias_name')) if self.alias_id: self.alias_id.write(alias_values) else: @@ -663,7 +663,7 @@ class AccountJournal(models.Model): bank_account = self.env['res.partner.bank'].browse(vals['bank_account_id']) if bank_account.partner_id != company.partner_id: raise UserError(_("The partners of the journal's company and the related bank account mismatch.")) - if vals.get('type') == 'purchase': + if 'alias_name' in vals: journal._update_mail_alias(vals) result = super(AccountJournal, self).write(vals) @@ -787,8 +787,7 @@ class AccountJournal(models.Model): if vals.get('type') in ('sale', 'purchase') and vals.get('refund_sequence') and not vals.get('refund_sequence_id'): vals.update({'refund_sequence_id': self.sudo()._create_sequence(vals, refund=True).id}) journal = super(AccountJournal, self.with_context(mail_create_nolog=True)).create(vals) - if journal.type == 'purchase': - # create a mail alias for purchase journals (always, deactivated if alias_name isn't set) + if 'alias_name' in vals: journal._update_mail_alias(vals) # Create the bank_account_id if necessary diff --git a/addons/account/models/account_bank_statement.py b/addons/account/models/account_bank_statement.py index b63543a6b0f..487477dee6a 100644 --- a/addons/account/models/account_bank_statement.py +++ b/addons/account/models/account_bank_statement.py @@ -235,14 +235,18 @@ class AccountBankStatement(models.Model): statements = self.filtered(lambda r: r.state == 'open') for statement in statements: moves = self.env['account.move'] + # `line.journal_entry_ids` gets invalidated from the cache during the loop + # because new move lines are being created at each iteration. + # The below dict is to prevent the ORM to permanently refetch `line.journal_entry_ids` + line_journal_entries = {line: line.journal_entry_ids for line in statement.line_ids} for st_line in statement.line_ids: #upon bank statement confirmation, look if some lines have the account_id set. It would trigger a journal entry #creation towards that account, with the wanted side-effect to skip that line in the bank reconciliation widget. + journal_entries = line_journal_entries[st_line] st_line.fast_counterpart_creation() - if not st_line.account_id and not st_line.journal_entry_ids.ids and not st_line.statement_id.currency_id.is_zero(st_line.amount): + if not st_line.account_id and not journal_entries.ids and not st_line.statement_id.currency_id.is_zero(st_line.amount): raise UserError(_('All the account entries lines must be processed in order to close the statement.')) - for aml in st_line.journal_entry_ids: - moves |= aml.move_id + moves = statement.mapped('line_ids.journal_entry_ids.move_id') if moves: moves.filtered(lambda m: m.state != 'posted').post() statement.message_post(body=_('Statement %s confirmed, journal items were created.') % (statement.name,)) diff --git a/addons/account/models/account_invoice.py b/addons/account/models/account_invoice.py index 4d8e5583f08..f2a5e0d785b 100644 --- a/addons/account/models/account_invoice.py +++ b/addons/account/models/account_invoice.py @@ -103,6 +103,11 @@ class AccountInvoice(models.Model): if invoice_type == 'out_invoice' and self.env['ir.config_parameter'].sudo().get_param('account.use_invoice_terms'): return self.env.user.company_id.invoice_terms + def _get_aml_for_amount_residual(self): + """ Get the aml to consider to compute the amount residual of invoices """ + self.ensure_one() + return self.sudo().move_id.line_ids.filtered(lambda l: l.account_id == self.account_id) + @api.one @api.depends( 'state', 'currency_id', 'invoice_line_ids.price_subtotal', @@ -112,14 +117,13 @@ class AccountInvoice(models.Model): residual = 0.0 residual_company_signed = 0.0 sign = self.type in ['in_refund', 'out_refund'] and -1 or 1 - for line in self.sudo().move_id.line_ids: - if line.account_id == self.account_id: - residual_company_signed += line.amount_residual - if line.currency_id == self.currency_id: - residual += line.amount_residual_currency if line.currency_id else line.amount_residual - else: - from_currency = line.currency_id or line.company_id.currency_id - residual += from_currency._convert(line.amount_residual, self.currency_id, line.company_id, line.date or fields.Date.today()) + for line in self._get_aml_for_amount_residual(): + residual_company_signed += line.amount_residual + if line.currency_id == self.currency_id: + residual += line.amount_residual_currency if line.currency_id else line.amount_residual + else: + from_currency = line.currency_id or line.company_id.currency_id + residual += from_currency._convert(line.amount_residual, self.currency_id, line.company_id, line.date or fields.Date.today()) self.residual_company_signed = abs(residual_company_signed) * sign self.residual_signed = abs(residual) * sign self.residual = abs(residual) @@ -1000,7 +1004,7 @@ class AccountInvoice(models.Model): # lots of duplicate calls to action_invoice_open, so we remove those already open to_open_invoices = self.filtered(lambda inv: inv.state != 'open') if to_open_invoices.filtered(lambda inv: not inv.partner_id): - raise UserError(_("The field Vendor is required, please complete it to validate the Vendor Bill.")) + raise UserError(_("The field Vendor/Customer is required, please complete it to validate the Vendor Bill/Customer Invoice.")) if to_open_invoices.filtered(lambda inv: inv.state != 'draft'): raise UserError(_("Invoice must be in draft state in order to validate it.")) if to_open_invoices.filtered(lambda inv: float_compare(inv.amount_total, 0.0, precision_rounding=inv.currency_id.rounding) == -1): @@ -1032,6 +1036,10 @@ class AccountInvoice(models.Model): raise UserError(_('Invoice must be paid in order to set it to register payment.')) return self.write({'state': 'open'}) + @api.multi + def action_register_payment(self): + return self.env['account.payment'].with_context(active_ids=self.ids, active_model='account.invoice', active_id=self.id).action_register_payment() + @api.multi def action_invoice_cancel(self): return self.filtered(lambda inv: inv.state != 'cancel').action_cancel() @@ -1110,12 +1118,18 @@ class AccountInvoice(models.Model): tax_grouped[key][field] += round_curr(val.get(field) or 0) return tax_grouped + @api.multi + def _get_aml_for_register_payment(self): + """ Get the aml to consider to reconcile in register payment """ + self.ensure_one() + return self.move_id.line_ids.filtered(lambda r: not r.reconciled and r.account_id.internal_type in ('payable', 'receivable')) + @api.multi def register_payment(self, payment_line, writeoff_acc_id=False, writeoff_journal_id=False): """ Reconcile payable/receivable lines from the invoice with payment_line """ line_to_reconcile = self.env['account.move.line'] for inv in self: - line_to_reconcile += inv.move_id.line_ids.filtered(lambda r: not r.reconciled and r.account_id.internal_type in ('payable', 'receivable')) + line_to_reconcile += inv._get_aml_for_register_payment() return (line_to_reconcile + payment_line).reconcile(writeoff_acc_id, writeoff_journal_id) @api.multi diff --git a/addons/account/models/account_move.py b/addons/account/models/account_move.py index b9c82492446..eaa18ec5e4d 100644 --- a/addons/account/models/account_move.py +++ b/addons/account/models/account_move.py @@ -816,7 +816,9 @@ class AccountMoveLine(models.Model): total_amount_currency = 0 maxdate = date.min to_balance = {} + cash_basis_partial = self.env['account.partial.reconcile'] for aml in amls: + cash_basis_partial |= aml.move_id.tax_cash_basis_rec_id total_debit += aml.debit total_credit += aml.credit maxdate = max(aml.date, maxdate) @@ -831,15 +833,28 @@ class AccountMoveLine(models.Model): to_balance[aml.currency_id] = [self.env['account.move.line'], 0] to_balance[aml.currency_id][0] += aml to_balance[aml.currency_id][1] += aml.amount_residual != 0 and aml.amount_residual or aml.amount_residual_currency + # Check if reconciliation is total # To check if reconciliation is total we have 3 differents use case: # 1) There are multiple currency different than company currency, in that case we check using debit-credit # 2) We only have one currency which is different than company currency, in that case we check using amount_currency # 3) We have only one currency and some entries that don't have a secundary currency, in that case we check debit-credit # or amount_currency. + # 4) Cash basis full reconciliation + # - either none of the moves are cash basis reconciled, and we proceed + # - or some moves are cash basis reconciled and we make sure they are all fully reconciled + digits_rounding_precision = amls[0].company_id.currency_id.rounding - if (currency and float_is_zero(total_amount_currency, precision_rounding=currency.rounding)) or \ - (multiple_currency and float_compare(total_debit, total_credit, precision_rounding=digits_rounding_precision) == 0): + if ( + ( + not cash_basis_partial or (cash_basis_partial and all([p >= 1.0 for p in amls._get_matched_percentage().values()])) + ) and + ( + currency and float_is_zero(total_amount_currency, precision_rounding=currency.rounding) or + multiple_currency and float_compare(total_debit, total_credit, precision_rounding=digits_rounding_precision) == 0 + ) + ): + exchange_move_id = False # Eventually create a journal entry to book the difference due to foreign currency's exchange rate that fluctuates if to_balance and any([not float_is_zero(residual, precision_rounding=digits_rounding_precision) for aml, residual in to_balance.values()]): @@ -933,7 +948,8 @@ class AccountMoveLine(models.Model): for after_rec_dict in cash_basis_subjected: new_rec = part_rec.create(after_rec_dict) - if cash_basis: + # if the pair belongs to move being reverted, do not create CABA entry + if cash_basis and not (new_rec.debit_move_id + new_rec.credit_move_id).mapped('move_id').mapped('reverse_entry_id'): new_rec.create_tax_cash_basis_entry(cash_basis_percentage_before_rec) self.recompute() diff --git a/addons/account/models/account_payment.py b/addons/account/models/account_payment.py index 72ea7718c08..baca985def8 100644 --- a/addons/account/models/account_payment.py +++ b/addons/account/models/account_payment.py @@ -6,7 +6,6 @@ from odoo.tools import float_compare from itertools import groupby - MAP_INVOICE_TYPE_PARTNER_TYPE = { 'out_invoice': 'customer', 'out_refund': 'customer', @@ -21,6 +20,7 @@ MAP_INVOICE_TYPE_PAYMENT_SIGN = { 'out_refund': 1, } + class account_payment_method(models.Model): _name = "account.payment.method" _description = "Payment Methods" @@ -30,16 +30,36 @@ class account_payment_method(models.Model): payment_type = fields.Selection([('inbound', 'Inbound'), ('outbound', 'Outbound')], required=True) -class account_abstract_payment(models.AbstractModel): - _name = "account.abstract.payment" - _description = "Contains the logic shared between models which allows to register payments" +class account_payment(models.Model): + _name = "account.payment" + _inherit = ['mail.thread', 'mail.activity.mixin'] + _description = "Payments" + _order = "payment_date desc, name desc" - invoice_ids = fields.Many2many('account.invoice', string='Invoices', copy=False) - multi = fields.Boolean(string='Multi', - help='Technical field indicating if the user selected invoices from multiple partners or from different types.') + name = fields.Char(readonly=True, copy=False) # The name is attributed upon post() + payment_reference = fields.Char(copy=False, readonly=True, help="Reference of the document used to issue this payment. Eg. check number, file name, etc.") + move_name = fields.Char(string='Journal Entry Name', readonly=True, + default=False, copy=False, + help="Technical field holding the number given to the journal entry, automatically set when the statement line is reconciled then stored to set the same number again if the line is cancelled, set to draft and re-processed again.") - payment_type = fields.Selection([('outbound', 'Send Money'), ('inbound', 'Receive Money')], string='Payment Type', required=True) - payment_method_id = fields.Many2one('account.payment.method', string='Payment Method Type', required=True, oldname="payment_method", + # Money flows from the journal_id's default_debit_account_id or default_credit_account_id to the destination_account_id + destination_account_id = fields.Many2one('account.account', compute='_compute_destination_account_id', readonly=True) + # For money transfer, money goes from journal_id to a transfer account, then from the transfer account to destination_journal_id + destination_journal_id = fields.Many2one('account.journal', string='Transfer To', domain=[('type', 'in', ('bank', 'cash'))], readonly=True, states={'draft': [('readonly', False)]}) + + invoice_ids = fields.Many2many('account.invoice', 'account_invoice_payment_rel', 'payment_id', 'invoice_id', string="Invoices", copy=False, readonly=True, + help="""Technical field containing the invoice for which the payment has been generated. + This does not especially correspond to the invoices reconciled with the payment, + as it can have been generated first, and reconciled later""") + reconciled_invoice_ids = fields.Many2many('account.invoice', string='Reconciled Invoices', compute='_compute_reconciled_invoice_ids', help="Invoices whose journal items have been reconciled with this payment's.") + has_invoices = fields.Boolean(compute="_compute_reconciled_invoice_ids", help="Technical field used for usability purposes") + + move_line_ids = fields.One2many('account.move.line', 'payment_id', readonly=True, copy=False, ondelete='restrict') + move_reconciled = fields.Boolean(compute="_get_move_reconciled", readonly=True) + + state = fields.Selection([('draft', 'Draft'), ('posted', 'Posted'), ('sent', 'Sent'), ('reconciled', 'Reconciled'), ('cancelled', 'Cancelled')], readonly=True, default='draft', copy=False, string="Status") + payment_type = fields.Selection([('outbound', 'Send Money'), ('inbound', 'Receive Money'), ('transfer', 'Internal Transfer')], string='Payment Type', required=True, readonly=True, states={'draft': [('readonly', False)]}) + payment_method_id = fields.Many2one('account.payment.method', string='Payment Method Type', required=True, readonly=True, states={'draft': [('readonly', False)]}, oldname="payment_method", help="Manual: Get paid by cash, check or any other method outside of Odoo.\n"\ "Electronic: Get paid automatically through a payment acquirer by requesting a transaction on a card saved by the customer when buying or subscribing online (payment token).\n"\ "Check: Pay bill by check and print it from Odoo.\n"\ @@ -48,17 +68,20 @@ class account_abstract_payment(models.AbstractModel): payment_method_code = fields.Char(related='payment_method_id.code', help="Technical field used to adapt the interface to the payment type selected.", readonly=True) - partner_type = fields.Selection([('customer', 'Customer'), ('supplier', 'Vendor')], tracking=True) - partner_id = fields.Many2one('res.partner', string='Partner', tracking=True) + partner_type = fields.Selection([('customer', 'Customer'), ('supplier', 'Vendor')], tracking=True, readonly=True, states={'draft': [('readonly', False)]}) + partner_id = fields.Many2one('res.partner', string='Partner', tracking=True, readonly=True, states={'draft': [('readonly', False)]}) - amount = fields.Monetary(string='Payment Amount', required=True, tracking=True) - currency_id = fields.Many2one('res.currency', string='Currency', required=True, default=lambda self: self.env.user.company_id.currency_id) - payment_date = fields.Date(string='Payment Date', default=fields.Date.context_today, required=True, copy=False, tracking=True) - communication = fields.Char(string='Memo') - journal_id = fields.Many2one('account.journal', string='Payment Journal', required=True, tracking=True, domain=[('type', 'in', ('bank', 'cash'))]) + amount = fields.Monetary(string='Payment Amount', required=True, readonly=True, states={'draft': [('readonly', False)]}, tracking=True) + currency_id = fields.Many2one('res.currency', string='Currency', required=True, readonly=True, states={'draft': [('readonly', False)]}, default=lambda self: self.env.user.company_id.currency_id) + payment_date = fields.Date(string='Payment Date', default=fields.Date.context_today, required=True, readonly=True, states={'draft': [('readonly', False)]}, copy=False, tracking=True) + communication = fields.Char(string='Memo', readonly=True, states={'draft': [('readonly', False)]}) + journal_id = fields.Many2one('account.journal', string='Payment Journal', required=True, readonly=True, states={'draft': [('readonly', False)]}, tracking=True, domain=[('type', 'in', ('bank', 'cash'))]) + company_id = fields.Many2one('res.company', related='journal_id.company_id', string='Company', readonly=True) hide_payment_method = fields.Boolean(compute='_compute_hide_payment_method', - help="Technical field used to hide the payment method if the selected journal has only one available which is 'manual'") + help="Technical field used to hide the payment method if the" + "selected journal has only one available which is 'manual'") + payment_difference = fields.Monetary(compute='_compute_payment_difference', readonly=True) payment_difference_handling = fields.Selection([('open', 'Keep open'), ('reconcile', 'Mark invoice as fully paid')], default='open', string="Payment Difference Handling", copy=False) writeoff_account_id = fields.Many2one('account.account', string="Difference Account", domain=[('deprecated', '=', False)], copy=False) @@ -66,13 +89,14 @@ class account_abstract_payment(models.AbstractModel): string='Journal Item Label', help='Change label of the counterpart that will hold the payment difference', default='Write-Off') - partner_bank_account_id = fields.Many2one('res.partner.bank', string="Recipient Bank Account") + partner_bank_account_id = fields.Many2one('res.partner.bank', string="Recipient Bank Account", readonly=True, states={'draft': [('readonly', False)]}) show_partner_bank_account = fields.Boolean(compute='_compute_show_partner_bank', help='Technical field used to know whether the field `partner_bank_account_id` needs to be displayed or not in the payments form views') + require_partner_bank_account = fields.Boolean(compute='_compute_show_partner_bank', help='Technical field used to know whether the field `partner_bank_account_id` needs to be required or not in the payments form views') @api.model def default_get(self, fields): - rec = super(account_abstract_payment, self).default_get(fields) - active_ids = self._context.get('active_ids') + rec = super(account_payment, self).default_get(fields) + active_ids = self._context.get('active_ids') or self._context.get('active_id') active_model = self._context.get('active_model') # Check for selected invoices ids @@ -84,30 +108,16 @@ class account_abstract_payment(models.AbstractModel): # Check all invoices are open if any(invoice.state != 'open' for invoice in invoices): raise UserError(_("You can only register payments for open invoices")) - # Check all invoices have the same currency - if any(inv.currency_id != invoices[0].currency_id for inv in invoices): - raise UserError(_("In order to pay multiple invoices at once, they must use the same currency.")) - - # Look if we are mixin multiple commercial_partner or customer invoices with vendor bills - multi = any(inv.commercial_partner_id != invoices[0].commercial_partner_id - or MAP_INVOICE_TYPE_PARTNER_TYPE[inv.type] != MAP_INVOICE_TYPE_PARTNER_TYPE[invoices[0].type] - or inv.account_id != invoices[0].account_id - or inv.partner_bank_id != invoices[0].partner_bank_id - for inv in invoices) - - currency = invoices[0].currency_id - - total_amount = self._compute_payment_amount(invoices=invoices, currency=currency) + amount = self._compute_payment_amount(invoices, invoices[0].currency_id) rec.update({ - 'amount': abs(total_amount), - 'currency_id': currency.id, - 'payment_type': total_amount > 0 and 'inbound' or 'outbound', - 'partner_id': False if multi else invoices[0].commercial_partner_id.id, - 'partner_type': False if multi else MAP_INVOICE_TYPE_PARTNER_TYPE[invoices[0].type], - 'communication': ' '.join([ref for ref in invoices.mapped('reference') if ref]), + 'currency_id': invoices[0].currency_id.id, + 'amount': abs(amount), + 'payment_type': 'inbound' if amount > 0 else 'outbound', + 'partner_id': invoices[0].commercial_partner_id.id, + 'partner_type': MAP_INVOICE_TYPE_PARTNER_TYPE[invoices[0].type], + 'communication': invoices[0].reference or invoices[0].number, 'invoice_ids': [(6, 0, invoices.ids)], - 'multi': multi, }) return rec @@ -121,12 +131,17 @@ class account_abstract_payment(models.AbstractModel): def _get_method_codes_using_bank_account(self): return [] + @api.model + def _get_method_codes_needing_bank_account(self): + return [] + @api.depends('payment_method_code') def _compute_show_partner_bank(self): """ Computes if the destination bank account must be displayed in the payment form view. By default, it won't be displayed but some modules might change that, depending on the payment type.""" for payment in self: - payment.show_partner_bank_account = payment.payment_method_code in self._get_method_codes_using_bank_account() and not self.multi + payment.show_partner_bank_account = payment.payment_method_code in self._get_method_codes_using_bank_account() + payment.require_partner_bank_account = payment.payment_method_code in self._get_method_codes_needing_bank_account() @api.multi @api.depends('payment_type', 'journal_id') @@ -140,9 +155,9 @@ class account_abstract_payment(models.AbstractModel): or payment.journal_id.outbound_payment_method_ids payment.hide_payment_method = len(journal_payment_methods) == 1 and journal_payment_methods[0].code == 'manual' - @api.depends('invoice_ids', 'amount', 'payment_date', 'currency_id') + @api.depends('invoice_ids', 'amount', 'payment_date', 'currency_id', 'payment_type') def _compute_payment_difference(self): - for pay in self.filtered(lambda p: p.invoice_ids): + for pay in self.filtered(lambda p: p.invoice_ids and p.state == 'draft'): payment_amount = -pay.amount if pay.payment_type == 'outbound' else pay.amount pay.payment_difference = pay._compute_payment_amount() - payment_amount @@ -162,12 +177,20 @@ class account_abstract_payment(models.AbstractModel): # Set payment method domain (restrict to methods enabled for the journal and to selected payment type) payment_type = self.payment_type in ('outbound', 'transfer') and 'outbound' or 'inbound' - return {'domain': {'payment_method_id': [('payment_type', '=', payment_type), ('id', 'in', payment_methods_list)]}} + + domain = {'payment_method_id': [('payment_type', '=', payment_type), ('id', 'in', payment_methods_list)]} + + if self.env.context.get('active_model') == 'account.invoice': + active_ids = self._context.get('active_ids') + invoices = self.env['account.invoice'].browse(active_ids) + self.amount = abs(self._compute_payment_amount(invoices)) + + return {'domain': domain} return {} @api.onchange('partner_id') def _onchange_partner_id(self): - if not self.multi and self.invoice_ids and self.invoice_ids[0].partner_bank_id: + if self.invoice_ids and self.invoice_ids[0].partner_bank_id: self.partner_bank_account_id = self.invoice_ids[0].partner_bank_id elif self.partner_id != self.partner_bank_account_id.partner_id: # This condition ensures we use the default value provided into @@ -182,9 +205,38 @@ class account_abstract_payment(models.AbstractModel): self.partner_bank_account_id = False return {'domain': {'partner_bank_account_id': [('partner_id', 'in', [self.partner_id.id, self.partner_id.commercial_partner_id.id])]}} + @api.onchange('partner_type') + def _onchange_partner_type(self): + self.ensure_one() + # Set partner_id domain + if self.partner_type: + return {'domain': {'partner_id': [(self.partner_type, '=', True)]}} + + @api.onchange('payment_type') + def _onchange_payment_type(self): + if not self.invoice_ids and not self.partner_type: + # Set default partner type for the payment type + if self.payment_type == 'inbound': + self.partner_type = 'customer' + elif self.payment_type == 'outbound': + self.partner_type = 'supplier' + elif self.payment_type not in ('inbound', 'outbound'): + self.partner_type = False + # Set payment method domain + res = self._onchange_journal() + if not res.get('domain', {}): + res['domain'] = {} + jrnl_filters = self._compute_journal_domain_and_types() + journal_types = jrnl_filters['journal_types'] + journal_types.update(['bank', 'cash']) + res['domain']['journal_id'] = jrnl_filters['domain'] + [('type', 'in', list(journal_types))] + return res + def _compute_journal_domain_and_types(self): journal_type = ['bank', 'cash'] domain = [] + if self.invoice_ids: + domain.append(('company_id', '=', self.invoice_ids[0].company_id.id)) if self.currency_id.is_zero(self.amount) and self.has_invoices: # In case of payment with 0 amount, allow to select a journal of type 'general' like # 'Miscellaneous Operations' and set this journal by default. @@ -202,7 +254,9 @@ class account_abstract_payment(models.AbstractModel): jrnl_filters = self._compute_journal_domain_and_types() journal_types = jrnl_filters['journal_types'] domain_on_types = [('type', 'in', list(journal_types))] - if self.journal_id.type not in journal_types: + if self.invoice_ids: + domain_on_types.append(('company_id', '=', self.invoice_ids[0].company_id.id)) + if self.journal_id.type not in journal_types or (self.invoice_ids and self.journal_id.company_id != self.invoice_ids[0].company_id): self.journal_id = self.env['account.journal'].search(domain_on_types, limit=1) return {'domain': {'journal_id': jrnl_filters['domain'] + domain_on_types}} @@ -210,11 +264,14 @@ class account_abstract_payment(models.AbstractModel): def _onchange_currency(self): self.amount = abs(self._compute_payment_amount()) - # Set by default the first liquidity journal having this currency if exists. - if self.journal_id: + if self.journal_id: # TODO: only return if currency differ? return - journal = self.env['account.journal'].search( - [('type', 'in', ('bank', 'cash')), ('currency_id', '=', self.currency_id.id)], limit=1) + + # Set by default the first liquidity journal having this currency if exists. + domain = [('type', 'in', ('bank', 'cash')), ('currency_id', '=', self.currency_id.id)] + if self.invoice_ids: + domain.append(('company_id', '=', self.invoice_ids[0].company_id.id)) + journal = self.env['account.journal'].search(domain, limit=1) if journal: return {'value': {'journal_id': journal.id}} @@ -233,7 +290,7 @@ class account_abstract_payment(models.AbstractModel): # Get the payment currency if not currency: - currency = self.currency_id or self.journal_id.currency_id or self.journal_id.company_id.currency_id or invoices and invoices[0].currency_id + currency = self.currency_id or self.journal_id.currency_id or self.journal_id.company_id.currency_id # Avoid currency rounding issues by summing the amounts according to the company_currency_id before invoice_datas = invoices.read_group( @@ -250,154 +307,6 @@ class account_abstract_payment(models.AbstractModel): total += payment_currency._convert(amount_total, currency, self.env.user.company_id, self.payment_date or fields.Date.today()) return total - -class account_register_payments(models.TransientModel): - _name = "account.register.payments" - _inherit = 'account.abstract.payment' - _description = "Register Payments" - - group_invoices = fields.Boolean(string="Group Invoices", help="""If enabled, groups invoices by commercial partner, invoice account, - type and recipient bank account in the generated payments. If disabled, - a distinct payment will be generated for each invoice.""") - show_communication_field = fields.Boolean(compute='_compute_show_communication_field') - - @api.depends('invoice_ids.partner_id', 'group_invoices') - def _compute_show_communication_field(self): - """ We allow choosing a common communication for payments if the group - option has been activated, and all the invoices relate to the same - partner. - """ - for record in self: - record.show_communication_field = len(record.invoice_ids) == 1 \ - or record.group_invoices and len(record.mapped('invoice_ids.partner_id.commercial_partner_id')) == 1 - - @api.onchange('journal_id') - def _onchange_journal(self): - res = super(account_register_payments, self)._onchange_journal() - active_ids = self._context.get('active_ids') - invoices = self.env['account.invoice'].browse(active_ids) - self.amount = abs(self._compute_payment_amount(invoices)) - return res - - @api.model - def default_get(self, fields): - rec = super(account_register_payments, self).default_get(fields) - active_ids = self._context.get('active_ids') - - if not active_ids: - raise UserError(_("Programming error: wizard action executed without active_ids in context.")) - - return rec - - @api.multi - def _groupby_invoices(self): - '''Groups the invoices linked to the wizard. - - If the group_invoices option is activated, invoices will be grouped - according to their commercial partner, their account, their type and - the account where the payment they expect should end up. Otherwise, - invoices will be grouped so that each of them belongs to a - distinct group. - - :return: a dictionary mapping, grouping invoices as a recordset under each of its keys. - ''' - if not self.group_invoices: - return {inv.id: inv for inv in self.invoice_ids} - - results = {} - # Create a dict dispatching invoices according to their commercial_partner_id, account_id, invoice_type and partner_bank_id - for inv in self.invoice_ids: - partner_id = inv.commercial_partner_id.id - account_id = inv.account_id.id - invoice_type = MAP_INVOICE_TYPE_PARTNER_TYPE[inv.type] - recipient_account = inv.partner_bank_id - key = (partner_id, account_id, invoice_type, recipient_account) - if not key in results: - results[key] = self.env['account.invoice'] - results[key] += inv - return results - - @api.multi - def _prepare_payment_vals(self, invoices): - '''Create the payment values. - - :param invoices: The invoices that should have the same commercial partner and the same type. - :return: The payment values as a dictionary. - ''' - amount = self._compute_payment_amount(invoices=invoices) if self.multi else self.amount - payment_type = ('inbound' if amount > 0 else 'outbound') if self.multi else self.payment_type - bank_account = self.multi and invoices[0].partner_bank_id or self.partner_bank_account_id - pmt_communication = self.show_communication_field and self.communication \ - or self.group_invoices and ' '.join([inv.reference or inv.number for inv in invoices]) \ - or invoices[0].reference # in this case, invoices contains only one element, since group_invoices is False - values = { - 'journal_id': self.journal_id.id, - 'payment_method_id': self.payment_method_id.id, - 'payment_date': self.payment_date, - 'communication': pmt_communication, - 'invoice_ids': [(6, 0, invoices.ids)], - 'payment_type': payment_type, - 'amount': abs(amount), - 'currency_id': self.currency_id.id, - 'partner_id': invoices[0].commercial_partner_id.id, - 'partner_type': MAP_INVOICE_TYPE_PARTNER_TYPE[invoices[0].type], - 'partner_bank_account_id': bank_account.id, - 'multi': False, - 'payment_difference_handling': self.payment_difference_handling, - 'writeoff_account_id': self.writeoff_account_id.id, - 'writeoff_label': self.writeoff_label, - } - - return values - - @api.multi - def get_payments_vals(self): - '''Compute the values for payments. - - :return: a list of payment values (dictionary). - ''' - if self.multi: - groups = self._groupby_invoices() - return [self._prepare_payment_vals(invoices) for invoices in groups.values()] - return [self._prepare_payment_vals(self.invoice_ids)] - - @api.multi - def create_payments(self): - '''Create payments according to the invoices. - Having invoices with different commercial_partner_id or different type (Vendor bills with customer invoices) - leads to multiple payments. - In case of all the invoices are related to the same commercial_partner_id and have the same type, - only one payment will be created. - - :return: The ir.actions.act_window to show created payments. - ''' - Payment = self.env['account.payment'] - payments = Payment - for payment_vals in self.get_payments_vals(): - payments += Payment.create(payment_vals) - payments.post() - - action_vals = { - 'name': _('Payments'), - 'domain': [('id', 'in', payments.ids), ('state', '=', 'posted')], - 'view_type': 'form', - 'res_model': 'account.payment', - 'view_id': False, - 'type': 'ir.actions.act_window', - } - if len(payments) == 1: - action_vals.update({'res_id': payments[0].id, 'view_mode': 'form'}) - else: - action_vals['view_mode'] = 'tree,form' - return action_vals - - -class account_payment(models.Model): - _name = "account.payment" - _inherit = ['mail.thread', 'mail.activity.mixin', 'account.abstract.payment'] - _description = "Payments" - _order = "payment_date desc, name desc" - @api.multi def name_get(self): return [(payment.id, payment.name or _('Draft Payment')) for payment in self] @@ -410,40 +319,16 @@ class account_payment(models.Model): for aml in payment.move_line_ids.filtered(lambda x: x.account_id.reconcile): if not aml.reconciled: rec = False + break payment.move_reconciled = rec - company_id = fields.Many2one('res.company', related='journal_id.company_id', string='Company', readonly=True) - name = fields.Char(readonly=True, copy=False) # The name is attributed upon post() - state = fields.Selection([('draft', 'Draft'), ('posted', 'Posted'), ('sent', 'Sent'), ('reconciled', 'Reconciled'), ('cancelled', 'Cancelled')], readonly=True, default='draft', copy=False, string="Status") - - payment_type = fields.Selection(selection_add=[('transfer', 'Internal Transfer')]) - payment_reference = fields.Char(copy=False, readonly=True, help="Reference of the document used to issue this payment. Eg. check number, file name, etc.") - move_name = fields.Char(string='Journal Entry Name', readonly=True, - default=False, copy=False, - help="Technical field holding the number given to the journal entry, automatically set when the statement line is reconciled then stored to set the same number again if the line is cancelled, set to draft and re-processed again.") - - # Money flows from the journal_id's default_debit_account_id or default_credit_account_id to the destination_account_id - destination_account_id = fields.Many2one('account.account', compute='_compute_destination_account_id', readonly=True) - # For money transfer, money goes from journal_id to a transfer account, then from the transfer account to destination_journal_id - destination_journal_id = fields.Many2one('account.journal', string='Transfer To', domain=[('type', 'in', ('bank', 'cash'))]) - - invoice_ids = fields.Many2many('account.invoice', 'account_invoice_payment_rel', 'payment_id', 'invoice_id', string="Invoices", copy=False, readonly=True, help="""Technical field containing the invoices for which the payment has been generated. - This does not especially correspond to the invoices reconciled with the payment, - as it can have been generated first, and reconciled later""") - reconciled_invoice_ids = fields.Many2many('account.invoice', string='Reconciled Invoices', compute='_compute_reconciled_invoice_ids', help="Invoices whose journal items have been reconciled with this payment's.") - has_invoices = fields.Boolean(compute="_compute_reconciled_invoice_ids", help="Technical field used for usability purposes") - - # FIXME: ondelete='restrict' not working (eg. cancel a bank statement reconciliation with a payment) - move_line_ids = fields.One2many('account.move.line', 'payment_id', readonly=True, copy=False, ondelete='restrict') - move_reconciled = fields.Boolean(compute="_get_move_reconciled", readonly=True) - def open_payment_matching_screen(self): # Open reconciliation view for customers/suppliers move_line_id = False for move_line in self.move_line_ids: if move_line.account_id.reconcile: move_line_id = move_line.id - break; + break if not self.partner_id: raise UserError(_("Payments without a customer can't be matched")) action_context = {'company_ids': [self.company_id.id], 'partner_ids': [self.partner_id.commercial_partner_id.id]} @@ -484,61 +369,25 @@ class account_payment(models.Model): def _compute_reconciled_invoice_ids(self): for record in self: record.reconciled_invoice_ids = (record.move_line_ids.mapped('matched_debit_ids.debit_move_id.invoice_id') | - record.move_line_ids.mapped('matched_credit_ids.credit_move_id.invoice_id')) + record.move_line_ids.mapped('matched_credit_ids.credit_move_id.invoice_id')) record.has_invoices = bool(record.reconciled_invoice_ids) - @api.onchange('partner_type') - def _onchange_partner_type(self): - self.ensure_one() - # Set partner_id domain - if self.partner_type: - return {'domain': {'partner_id': [(self.partner_type, '=', True)]}} + @api.multi + def action_register_payment(self): + active_ids = self.env.context.get('active_ids') + if not active_ids: + return '' - @api.onchange('payment_type') - def _onchange_payment_type(self): - if not self.invoice_ids and not self.partner_type: - # Set default partner type for the payment type - if self.payment_type == 'inbound': - self.partner_type = 'customer' - elif self.payment_type == 'outbound': - self.partner_type = 'supplier' - elif self.payment_type not in ('inbound', 'outbound'): - self.partner_type = False - # Set payment method domain - res = self._onchange_journal() - if not res.get('domain', {}): - res['domain'] = {} - jrnl_filters = self._compute_journal_domain_and_types() - journal_types = jrnl_filters['journal_types'] - journal_types.update(['bank', 'cash']) - res['domain']['journal_id'] = jrnl_filters['domain'] + [('type', 'in', list(journal_types))] - return res - - @api.model - def default_get(self, fields): - rec = super(account_payment, self).default_get(fields) - invoice_defaults = self.resolve_2many_commands('invoice_ids', rec.get('invoice_ids')) - if invoice_defaults and len(invoice_defaults) == 1: - invoice = invoice_defaults[0] - rec['communication'] = invoice['reference'] or invoice['name'] or invoice['number'] - rec['currency_id'] = invoice['currency_id'][0] - rec['payment_type'] = invoice['type'] in ('out_invoice', 'in_refund') and 'inbound' or 'outbound' - rec['partner_type'] = MAP_INVOICE_TYPE_PARTNER_TYPE[invoice['type']] - rec['partner_id'] = invoice['partner_id'][0] - rec['amount'] = invoice['residual'] - return rec - - @api.model - def create(self, vals): - rslt = super(account_payment, self).create(vals) - # When a payment is created by the multi payments wizard in 'multi' mode, - # its partner_bank_account_id will never be displayed, and hence stay empty, - # even if the payment method requires it. This condition ensures we set - # the first (and thus most prioritary) account of the partner in this field - # in that situation. - if not rslt.partner_bank_account_id and rslt.show_partner_bank_account and rslt.partner_id.bank_ids: - rslt.partner_bank_account_id = rslt.partner_id.bank_ids[0] - return rslt + return { + 'name': _('Register Payment'), + 'res_model': len(active_ids) == 1 and 'account.payment' or 'account.payment.register', + 'view_type': 'form', + 'view_mode': 'form', + 'view_id': len(active_ids) != 1 and self.env.ref('account.view_account_payment_form_multi').id or self.env.ref('account.view_account_payment_invoice_form').id, + 'context': self.env.context, + 'target': 'new', + 'type': 'ir.actions.act_window', + } @api.multi def button_journal_entries(self): @@ -569,10 +418,6 @@ class account_payment(models.Model): 'domain': [('id', 'in', [x.id for x in self.reconciled_invoice_ids])], } - @api.multi - def button_dummy(self): - return True - @api.multi def unreconcile(self): """ Set back the payments in 'posted' or 'sent' state, without deleting the journal entries. @@ -588,7 +433,7 @@ class account_payment(models.Model): def cancel(self): for rec in self: for move in rec.move_line_ids.mapped('move_id'): - if rec.invoice_ids: + if rec.reconciled_invoice_ids: move.line_ids.remove_move_reconcile() move.button_cancel() move.unlink() @@ -656,17 +501,6 @@ class account_payment(models.Model): def action_draft(self): return self.write({'state': 'draft'}) - def action_validate_invoice_payment(self): - """ Posts a payment used to pay an invoice. This function only posts the - payment by default but can be overridden to apply specific post or pre-processing. - It is called by the "validate" button of the popup window - triggered on invoice form by the "Register Payment" button. - """ - if any(len(record.invoice_ids) != 1 for record in self): - # For multiple invoices, there is account.register.payments wizard - raise UserError(_("This method should only be called to process a single invoice's payment.")) - return self.post() - def _create_payment_entry(self, amount): """ Create a journal entry corresponding to a payment, if the payment references invoice(s) they are reconciled. Return the journal entry. @@ -810,7 +644,7 @@ class account_payment(models.Model): name = _('Transfer to %s') % self.destination_journal_id.name vals = { 'name': name, - 'account_id': self.payment_type in ('outbound','transfer') and self.journal_id.default_debit_account_id.id or self.journal_id.default_credit_account_id.id, + 'account_id': self.payment_type in ('outbound', 'transfer') and self.journal_id.default_debit_account_id.id or self.journal_id.default_credit_account_id.id, 'journal_id': self.journal_id.id, 'currency_id': self.currency_id != self.company_id.currency_id and self.currency_id.id or False, } @@ -825,3 +659,118 @@ class account_payment(models.Model): }) return vals + + +class payment_register(models.TransientModel): + _name = 'account.payment.register' + _description = 'Register Payment' + + payment_date = fields.Date(required=True, default=fields.Date.context_today) + journal_id = fields.Many2one('account.journal', required=True, domain=[('type', 'in', ('bank', 'cash'))]) + payment_method_id = fields.Many2one('account.payment.method', string='Payment Method Type', required=True, + help="Manual: Get paid by cash, check or any other method outside of Odoo.\n" + "Electronic: Get paid automatically through a payment acquirer by requesting a transaction on a card saved by the customer when buying or subscribing online (payment token).\n" + "Check: Pay bill by check and print it from Odoo.\n" + "Batch Deposit: Encase several customer checks at once by generating a batch deposit to submit to your bank. When encoding the bank statement in Odoo, you are suggested to reconcile the transaction with the batch deposit.To enable batch deposit, module account_batch_payment must be installed.\n" + "SEPA Credit Transfer: Pay bill from a SEPA Credit Transfer file you submit to your bank. To enable sepa credit transfer, module account_sepa must be installed ") + invoice_ids = fields.Many2many('account.invoice', 'account_invoice_payment_rel_transient', 'payment_id', 'invoice_id', string="Invoices", copy=False, readonly=True) + + @api.model + def default_get(self, fields): + rec = super(payment_register, self).default_get(fields) + active_ids = self._context.get('active_ids') + active_model = self._context.get('active_model') + invoices = self.env['account.invoice'].browse(active_ids) + + # Check all invoices are open + if any(invoice.state != 'open' for invoice in invoices): + raise UserError(_("You can only register payments for open invoices")) + # Check all invoices are inbound or all invoices are outbound + outbound_list = [invoice.type in ('in_invoice', 'out_refund') for invoice in invoices] + first_outbound = invoices[0].type in ('in_invoice', 'out_refund') + if any(x != first_outbound for x in outbound_list): + raise UserError(_("You can only register at the same time for payment that are all inbound or all outbound")) + if 'invoice_ids' not in rec: + rec['invoice_ids'] = [(6, 0, invoices.ids)] + if 'journal_id' not in rec: + rec['journal_id'] = self.env['account.journal'].search([('company_id', '=', self.env.user.company_id.id), ('type', 'in', ('bank', 'cash'))], limit=1).id + if 'payment_method_id' not in rec: + if invoices[0].type in ('out_invoice', 'in_refund'): + domain = [('payment_type', '=', 'inbound')] + else: + domain = [('payment_type', '=', 'outbound')] + rec['payment_method_id'] = self.env['account.payment.method'].search(domain, limit=1).id + return rec + + @api.onchange('journal_id', 'invoice_ids') + def _onchange_journal(self): + active_ids = self._context.get('active_ids') + invoices = self.env['account.invoice'].browse(active_ids) + if self.journal_id and invoices: + if invoices[0].type in ('out_invoice', 'in_refund'): + domain = [('payment_type', '=', 'inbound'), ('id', 'in', self.journal_id.inbound_payment_method_ids.ids)] + else: + domain = [('payment_type', '=', 'outbound'), ('id', 'in', self.journal_id.outbound_payment_method_ids.ids)] + + return {'domain': {'payment_method_id': domain}} + return {} + + @api.multi + def _prepare_payment_vals(self, invoice): + '''Create the payment values. + + :param invoice: A single invoice/bill to pay. + :return: The payment values as a dictionary. + ''' + amount = self.env['account.payment']._compute_payment_amount(invoices=invoice, currency=invoice.currency_id) + values = { + 'journal_id': self.journal_id.id, + 'payment_method_id': self.payment_method_id.id, + 'payment_date': self.payment_date, + 'communication': invoice.reference or invoice.number, + 'invoice_ids': [(6, 0, invoice.ids)], + 'payment_type': ('inbound' if amount > 0 else 'outbound'), + 'amount': abs(amount), + 'currency_id': invoice.currency_id.id, + 'partner_id': invoice.commercial_partner_id.id, + 'partner_type': MAP_INVOICE_TYPE_PARTNER_TYPE[invoice.type], + 'partner_bank_account_id': invoice.partner_bank_id.id, + } + return values + + @api.multi + def get_payments_vals(self): + '''Compute the values for payments. + + :return: a list of payment values (dictionary). + ''' + return [self._prepare_payment_vals(invoice) for invoice in self.invoice_ids] + + @api.multi + def create_payments(self): + '''Create payments according to the invoices. + Having invoices with different commercial_partner_id or different type + (Vendor bills with customer invoices) leads to multiple payments. + In case of all the invoices are related to the same + commercial_partner_id and have the same type, only one payment will be + created. + + :return: The ir.actions.act_window to show created payments. + ''' + Payment = self.env['account.payment'] + payments = Payment.create(self.get_payments_vals()) + payments.post() + + action_vals = { + 'name': _('Payments'), + 'domain': [('id', 'in', payments.ids), ('state', '=', 'posted')], + 'view_type': 'form', + 'res_model': 'account.payment', + 'view_id': False, + 'type': 'ir.actions.act_window', + } + if len(payments) == 1: + action_vals.update({'res_id': payments[0].id, 'view_mode': 'form'}) + else: + action_vals['view_mode'] = 'tree,form' + return action_vals diff --git a/addons/account/models/reconciliation_widget.py b/addons/account/models/reconciliation_widget.py index 875a346dabd..4683afab86f 100644 --- a/addons/account/models/reconciliation_widget.py +++ b/addons/account/models/reconciliation_widget.py @@ -95,6 +95,17 @@ class AccountReconciliation(models.AbstractModel): WHERE st_line.id IN %s ''' params = [tuple(st_lines.ids)] + + # 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 + self._cr.execute(query, params) result = {} diff --git a/addons/account/models/res_users.py b/addons/account/models/res_users.py new file mode 100644 index 00000000000..ab85e359d22 --- /dev/null +++ b/addons/account/models/res_users.py @@ -0,0 +1,26 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from odoo import api, models, _ +from odoo.exceptions import ValidationError + + +class Users(models.Model): + _inherit = "res.users" + + @api.multi + @api.constrains('groups_id') + def _check_one_user_type(self): + super(Users, self)._check_one_user_type() + + users_with_both_groups = self.filtered(lambda user: + user.user_has_groups('account.group_show_line_subtotals_tax_included') and + user.user_has_groups('account.group_show_line_subtotals_tax_excluded') + ) + if users_with_both_groups: + names = ", ".join(users_with_both_groups.mapped('name')) + raise ValidationError(_("A user cannot have both Tax B2B and Tax B2C.\n" + "Problematic user(s): %s\n" + "You should go in General Settings, and choose to display Product Prices\n" + "either in 'Tax-Included' or in 'Tax-Excluded' mode\n" + "(or switch twice the mode if you are already in the desired one).") % names) diff --git a/addons/account/security/account_security.xml b/addons/account/security/account_security.xml index 5e5a63f120f..bad495b9290 100644 --- a/addons/account/security/account_security.xml +++ b/addons/account/security/account_security.xml @@ -25,18 +25,6 @@ - - - - - - - - - - - - Show Full Accounting Features @@ -80,6 +68,16 @@ + + + + + + + + + + diff --git a/addons/account/static/src/js/bills_tree_upload.js b/addons/account/static/src/js/bills_tree_upload.js index edc043a5978..5171051bb97 100644 --- a/addons/account/static/src/js/bills_tree_upload.js +++ b/addons/account/static/src/js/bills_tree_upload.js @@ -22,7 +22,6 @@ odoo.define('account.bills.tree', function (require) { this.$buttons.on('click', '.o_button_upload_bill', function () { var state = self.model.get(self.handle, {raw: true}); var context = state.getContext() - context['type'] = 'in_invoice' self.do_action({ type: 'ir.actions.act_window', res_model: 'account.invoice.import.wizard', @@ -41,5 +40,5 @@ odoo.define('account.bills.tree', function (require) { }), }); - viewRegistry.add('account_bills_tree', BillsListView); + viewRegistry.add('account_tree', BillsListView); }); diff --git a/addons/account/static/src/js/reconciliation/reconciliation_action.js b/addons/account/static/src/js/reconciliation/reconciliation_action.js index 6c4fb9c912a..67d5de9de08 100644 --- a/addons/account/static/src/js/reconciliation/reconciliation_action.js +++ b/addons/account/static/src/js/reconciliation/reconciliation_action.js @@ -259,7 +259,7 @@ var StatementAction = AbstractAction.extend({ } }); }, - + /** * @private * @param {OdooEvent} ev @@ -308,7 +308,7 @@ var StatementAction = AbstractAction.extend({ return this.model.closeStatement().then(function (result) { self.do_action({ name: 'Bank Statements', - res_model: 'account.bank.statement', + res_model: 'account.bank.statement.line', res_id: result, views: [[false, 'form']], type: 'ir.actions.act_window', diff --git a/addons/account/static/src/js/reconciliation/reconciliation_renderer.js b/addons/account/static/src/js/reconciliation/reconciliation_renderer.js index 7e432a0e0a5..33b4430ab2f 100644 --- a/addons/account/static/src/js/reconciliation/reconciliation_renderer.js +++ b/addons/account/static/src/js/reconciliation/reconciliation_renderer.js @@ -573,7 +573,10 @@ var LineRenderer = Widget.extend(FieldManagerMixin, { type: 'boolean', name: 'to_check', }], { - account_id: {string: _t("Account")}, + account_id: { + string: _t("Account"), + domain: [['deprecated', '=', false]], + }, label: {string: _t("Label")}, amount: {string: _t("Account")}, }).then(function (recordID) { diff --git a/addons/account/static/src/xml/account_reconciliation.xml b/addons/account/static/src/xml/account_reconciliation.xml index d76ef05e917..2555b348990 100644 --- a/addons/account/static/src/xml/account_reconciliation.xml +++ b/addons/account/static/src/xml/account_reconciliation.xml @@ -67,7 +67,7 @@ - +

@@ -236,7 +236,7 @@ - + account.invoice.tree
account.invoice - + @@ -233,7 +233,7 @@ account.invoice.supplier.tree account.invoice - + @@ -267,7 +267,7 @@
- diff --git a/addons/account/views/account_move_views.xml b/addons/account/views/account_move_views.xml index 381fa39ae2f..03fba65b684 100644 --- a/addons/account/views/account_move_views.xml +++ b/addons/account/views/account_move_views.xml @@ -36,6 +36,7 @@ + @@ -43,7 +44,7 @@ - +