|
| 1 | +from odoo import _, api, fields, models |
| 2 | +from odoo.exceptions import ValidationError |
| 3 | + |
| 4 | +class AccountPaymentRegister(models.TransientModel): |
| 5 | + _inherit = "account.payment.register" |
| 6 | + |
| 7 | + communication = fields.Text() |
| 8 | + single_payment = fields.Boolean(compute="_compute_single_payment") |
| 9 | + payment_lines_ids = fields.One2many(comodel_name="account.payment.lines", inverse_name="account_payment_register_id", store=True) |
| 10 | + |
| 11 | + # ------------------------------------------------------------------------- |
| 12 | + # COMPUTE METHODS |
| 13 | + # ------------------------------------------------------------------------- |
| 14 | + |
| 15 | + @api.depends('payment_lines_ids') |
| 16 | + def _compute_single_payment(self): |
| 17 | + self.single_payment = len(self.payment_lines_ids) == 1 |
| 18 | + |
| 19 | + # ------------------------------------------------------------------------- |
| 20 | + # LOW-LEVEL METHODS |
| 21 | + # ------------------------------------------------------------------------- |
| 22 | + |
| 23 | + @api.model |
| 24 | + def default_get(self, fields_list): |
| 25 | + # OVERRIDE |
| 26 | + res = super().default_get(fields_list) |
| 27 | + |
| 28 | + # Retrieve available_line |
| 29 | + if 'line_ids' in fields_list: |
| 30 | + if self._context.get('active_model') == 'account.move': |
| 31 | + lines = self.env['account.move'].browse(self._context.get('active_ids', [])).line_ids |
| 32 | + elif self._context.get('active_model') == 'account.move.line': |
| 33 | + lines = self.env['account.move.line'].browse(self._context.get('active_ids', [])) |
| 34 | + else: |
| 35 | + raise UserError(_( |
| 36 | + "The register payment wizard should only be called on account.move or account.move.line records." |
| 37 | + )) |
| 38 | + |
| 39 | + available_lines = self.env['account.move.line'] |
| 40 | + valid_account_types = self.env['account.payment']._get_valid_payment_account_types() |
| 41 | + for line in lines: |
| 42 | + |
| 43 | + if line.account_type not in valid_account_types: |
| 44 | + continue |
| 45 | + if line.currency_id: |
| 46 | + if line.currency_id.is_zero(line.amount_residual_currency): |
| 47 | + continue |
| 48 | + else: |
| 49 | + if line.company_currency_id.is_zero(line.amount_residual): |
| 50 | + continue |
| 51 | + available_lines |= line |
| 52 | + |
| 53 | + # Store lines |
| 54 | + payment_lines = [] |
| 55 | + if available_lines: |
| 56 | + for line in available_lines: |
| 57 | + payment_lines.append((0, 0, { |
| 58 | + 'partner_id': line.partner_id.id, |
| 59 | + 'name': line.move_name, |
| 60 | + 'memo_id': line.move_name, |
| 61 | + 'invoice_date': line.invoice_date, |
| 62 | + 'amount_residual': line.amount_residual, |
| 63 | + 'balance_amount': line.amount_residual, |
| 64 | + 'payment_amount': line.amount_residual |
| 65 | + })) |
| 66 | + |
| 67 | + res['payment_lines_ids'] = payment_lines |
| 68 | + |
| 69 | + return res |
| 70 | + |
| 71 | + # ------------------------------------------------------------------------- |
| 72 | + # BUSINESS METHODS |
| 73 | + # ------------------------------------------------------------------------- |
| 74 | + |
| 75 | + def _create_payment_vals_from_batch(self, batch_result): |
| 76 | + batch_values = self._get_wizard_values_from_batch(batch_result) |
| 77 | + |
| 78 | + if batch_values['payment_type'] == 'inbound': |
| 79 | + partner_bank_id = self.journal_id.bank_account_id.id |
| 80 | + else: |
| 81 | + partner_bank_id = batch_result['payment_values']['partner_bank_id'] |
| 82 | + |
| 83 | + payment_method_line = self.payment_method_line_id |
| 84 | + |
| 85 | + if batch_values['payment_type'] != payment_method_line.payment_type: |
| 86 | + payment_method_line = self.journal_id._get_available_payment_method_lines(batch_values['payment_type'])[:1] |
| 87 | + |
| 88 | + # Fetch the Partial set amount. |
| 89 | + memo = self._get_communication(batch_result['lines']) |
| 90 | + payment_line = self.payment_lines_ids.search([('memo_id', '=', memo)], order='id desc', limit=1) |
| 91 | + |
| 92 | + payment_vals = { |
| 93 | + 'date': self.payment_date, |
| 94 | + 'amount': abs(payment_line.payment_amount), |
| 95 | + 'payment_type': batch_values['payment_type'], |
| 96 | + 'partner_type': batch_values['partner_type'], |
| 97 | + 'memo': self._get_communication(batch_result['lines']), |
| 98 | + 'journal_id': self.journal_id.id, |
| 99 | + 'company_id': self.company_id.id, |
| 100 | + 'currency_id': batch_values['source_currency_id'], |
| 101 | + 'partner_id': batch_values['partner_id'], |
| 102 | + 'payment_method_line_id': payment_method_line.id, |
| 103 | + 'destination_account_id': batch_result['lines'][0].account_id.id, |
| 104 | + 'write_off_line_vals': [], |
| 105 | + } |
| 106 | + |
| 107 | + if partner_bank_id: |
| 108 | + payment_vals['partner_bank_id'] = partner_bank_id |
| 109 | + |
| 110 | + total_amount_values = self._get_total_amounts_to_pay([batch_result]) |
| 111 | + total_amount = total_amount_values['amount_by_default'] |
| 112 | + currency = self.env['res.currency'].browse(batch_values['source_currency_id']) |
| 113 | + if total_amount_values['epd_applied']: |
| 114 | + payment_vals['amount'] = total_amount |
| 115 | + |
| 116 | + epd_aml_values_list = [] |
| 117 | + for aml in batch_result['lines']: |
| 118 | + if aml.move_id._is_eligible_for_early_payment_discount(currency, self.payment_date): |
| 119 | + epd_aml_values_list.append({ |
| 120 | + 'aml': aml, |
| 121 | + 'amount_currency': -aml.amount_residual_currency, |
| 122 | + 'balance': currency._convert(-aml.amount_residual_currency, aml.company_currency_id, self.company_id, self.payment_date), |
| 123 | + }) |
| 124 | + |
| 125 | + open_amount_currency = (batch_values['source_amount_currency'] - total_amount) * (-1 if batch_values['payment_type'] == 'outbound' else 1) |
| 126 | + open_balance = currency._convert(open_amount_currency, aml.company_currency_id, self.company_id, self.payment_date) |
| 127 | + early_payment_values = self.env['account.move']\ |
| 128 | + ._get_invoice_counterpart_amls_for_early_payment_discount(epd_aml_values_list, open_balance) |
| 129 | + for aml_values_list in early_payment_values.values(): |
| 130 | + payment_vals['write_off_line_vals'] += aml_values_list |
| 131 | + |
| 132 | + return payment_vals |
| 133 | + |
| 134 | + def _create_payment_vals_from_wizard(self, batch_result): |
| 135 | + amount = self.amount |
| 136 | + if not self.single_payment: |
| 137 | + amount = 0 |
| 138 | + for line in self.payment_lines_ids: |
| 139 | + amount += abs(line.payment_amount) |
| 140 | + payment_vals = { |
| 141 | + 'date': self.payment_date, |
| 142 | + 'amount': amount, |
| 143 | + 'payment_type': self.payment_type, |
| 144 | + 'partner_type': self.partner_type, |
| 145 | + 'memo': self.communication, |
| 146 | + 'journal_id': self.journal_id.id, |
| 147 | + 'company_id': self.company_id.id, |
| 148 | + 'currency_id': self.currency_id.id, |
| 149 | + 'partner_id': self.partner_id.id, |
| 150 | + 'partner_bank_id': self.partner_bank_id.id, |
| 151 | + 'payment_method_line_id': self.payment_method_line_id.id, |
| 152 | + 'destination_account_id': self.line_ids[0].account_id.id, |
| 153 | + 'write_off_line_vals': [], |
| 154 | + } |
| 155 | + |
| 156 | + if self.payment_difference_handling == 'reconcile': |
| 157 | + if self.early_payment_discount_mode: |
| 158 | + epd_aml_values_list = [] |
| 159 | + for aml in batch_result['lines']: |
| 160 | + if aml.move_id._is_eligible_for_early_payment_discount(self.currency_id, self.payment_date): |
| 161 | + epd_aml_values_list.append({ |
| 162 | + 'aml': aml, |
| 163 | + 'amount_currency': -aml.amount_residual_currency, |
| 164 | + 'balance': aml.currency_id._convert(-aml.amount_residual_currency, aml.company_currency_id, date=self.payment_date), |
| 165 | + }) |
| 166 | + |
| 167 | + open_amount_currency = self.payment_difference * (-1 if self.payment_type == 'outbound' else 1) |
| 168 | + open_balance = self.currency_id._convert(open_amount_currency, self.company_id.currency_id, self.company_id, self.payment_date) |
| 169 | + early_payment_values = self.env['account.move']._get_invoice_counterpart_amls_for_early_payment_discount(epd_aml_values_list, open_balance) |
| 170 | + for aml_values_list in early_payment_values.values(): |
| 171 | + payment_vals['write_off_line_vals'] += aml_values_list |
| 172 | + |
| 173 | + elif not self.currency_id.is_zero(self.payment_difference): |
| 174 | + |
| 175 | + if self.writeoff_is_exchange_account: |
| 176 | + if self.currency_id != self.company_currency_id: |
| 177 | + payment_vals['force_balance'] = sum(batch_result['lines'].mapped('amount_residual')) |
| 178 | + else: |
| 179 | + if self.payment_type == 'inbound': |
| 180 | + # Receive money. |
| 181 | + write_off_amount_currency = self.payment_difference |
| 182 | + else: # if self.payment_type == 'outbound': |
| 183 | + # Send money. |
| 184 | + write_off_amount_currency = -self.payment_difference |
| 185 | + |
| 186 | + payment_vals['write_off_line_vals'].append({ |
| 187 | + 'name': self.writeoff_label, |
| 188 | + 'account_id': self.writeoff_account_id.id, |
| 189 | + 'partner_id': self.partner_id.id, |
| 190 | + 'currency_id': self.currency_id.id, |
| 191 | + 'amount_currency': write_off_amount_currency, |
| 192 | + 'balance': self.currency_id._convert(write_off_amount_currency, self.company_id.currency_id, self.company_id, self.payment_date), |
| 193 | + }) |
| 194 | + return payment_vals |
| 195 | + |
| 196 | + def _reconcile_payments(self, to_process, edit_mode=False): |
| 197 | + """ Reconcile payments using the specified partial amounts per invoice. """ |
| 198 | + |
| 199 | + domain = [ |
| 200 | + ('parent_state', '=', 'posted'), |
| 201 | + ('account_type', 'in', self.env['account.payment']._get_valid_payment_account_types()), |
| 202 | + ('reconciled', '=', False), |
| 203 | + ] |
| 204 | + |
| 205 | + for vals in to_process: |
| 206 | + payment = vals['payment'] |
| 207 | + payment_lines = payment.move_id.line_ids.filtered_domain(domain) |
| 208 | + invoice_line = vals['to_reconcile'] |
| 209 | + amount_to_reconcile = vals['create_vals']['amount'] # Get the specified partial amount |
| 210 | + extra_context = {'forced_rate_from_register_payment': vals['rate']} if 'rate' in vals else {} |
| 211 | + |
| 212 | + if abs(invoice_line.amount_residual_currency) <= abs(amount_to_reconcile): |
| 213 | + # Fully reconcile this invoice |
| 214 | + (payment_lines + invoice_line).with_context(**extra_context).reconcile() |
| 215 | + else: |
| 216 | + # Partial reconciliation - create a partial reconcile entry |
| 217 | + self.env['account.partial.reconcile'].create({ |
| 218 | + 'debit_move_id': invoice_line.id if invoice_line.balance > 0 else payment_lines.id, |
| 219 | + 'credit_move_id': payment_lines.id if invoice_line.balance > 0 else invoice_line.id, |
| 220 | + 'amount': amount_to_reconcile, |
| 221 | + 'debit_amount_currency': amount_to_reconcile if invoice_line.balance > 0 else 0.0, |
| 222 | + 'credit_amount_currency': amount_to_reconcile if invoice_line.balance < 0 else 0.0, |
| 223 | + 'company_id': payment.company_id.id, |
| 224 | + }) |
| 225 | + |
| 226 | + # Link payment to reconciled journal entries |
| 227 | + invoice_line.move_id.matched_payment_ids += payment |
| 228 | + |
| 229 | + def _create_payments(self): |
| 230 | + self.ensure_one() |
| 231 | + batches = [] |
| 232 | + |
| 233 | + for batch in self.batches: |
| 234 | + batch_account = self._get_batch_account(batch) |
| 235 | + if self.require_partner_bank_account and (not batch_account or not batch_account.allow_out_payment): |
| 236 | + continue |
| 237 | + batches.append(batch) |
| 238 | + |
| 239 | + if not batches: |
| 240 | + raise UserError(_( |
| 241 | + "To record payments with %(payment_method)s, the recipient bank account must be manually validated. You should go on the partner bank account in order to validate it.", |
| 242 | + payment_method=self.payment_method_line_id.name, |
| 243 | + )) |
| 244 | + |
| 245 | + first_batch_result = batches[0] |
| 246 | + edit_mode = self.can_edit_wizard and (len(first_batch_result['lines']) == 1 or self.group_payment) |
| 247 | + to_process_single = [] |
| 248 | + to_process = [] |
| 249 | + |
| 250 | + # single_payment: |
| 251 | + payment_vals = self._create_payment_vals_from_wizard(first_batch_result) |
| 252 | + to_process_values = { |
| 253 | + 'create_vals': payment_vals, |
| 254 | + 'to_reconcile': first_batch_result['lines'], |
| 255 | + 'batch': first_batch_result, |
| 256 | + } |
| 257 | + |
| 258 | + # Force the rate during the reconciliation to put the difference directly on the |
| 259 | + # exchange difference. |
| 260 | + if self.writeoff_is_exchange_account and self.currency_id == self.company_currency_id: |
| 261 | + total_batch_residual = sum(first_batch_result['lines'].mapped('amount_residual_currency')) |
| 262 | + to_process_values['rate'] = abs(total_batch_residual / self.amount) if self.amount else 0.0 |
| 263 | + |
| 264 | + to_process_single.append(to_process_values) |
| 265 | + |
| 266 | + # Don't group payments: Create one batch per move. |
| 267 | + lines_to_pay = self._get_total_amounts_to_pay(batches)['lines'] if self.installments_mode in ('next', 'overdue', 'before_date') else self.line_ids |
| 268 | + new_batches = [] |
| 269 | + for batch_result in batches: |
| 270 | + for line in batch_result['lines']: |
| 271 | + if line not in lines_to_pay: |
| 272 | + continue |
| 273 | + new_batches.append({ |
| 274 | + **batch_result, |
| 275 | + 'payment_values': { |
| 276 | + **batch_result['payment_values'], |
| 277 | + 'payment_type': 'inbound' if line.balance > 0 else 'outbound' |
| 278 | + }, |
| 279 | + 'lines': line, |
| 280 | + }) |
| 281 | + batches = new_batches |
| 282 | + |
| 283 | + for batch_result in batches: |
| 284 | + to_process.append({ |
| 285 | + 'create_vals': self._create_payment_vals_from_batch(batch_result), |
| 286 | + 'to_reconcile': batch_result['lines'], |
| 287 | + 'batch': batch_result, |
| 288 | + }) |
| 289 | + |
| 290 | + if self.single_payment or self.group_payment: |
| 291 | + |
| 292 | + payments = self._init_payments(to_process_single, edit_mode=edit_mode) |
| 293 | + self._post_payments(to_process_single, edit_mode=edit_mode) |
| 294 | + |
| 295 | + if self.group_payment: |
| 296 | + for vals in to_process: |
| 297 | + vals['payment'] = to_process_single[0]['payment'] |
| 298 | + self._reconcile_payments(to_process, edit_mode=edit_mode) |
| 299 | + else: |
| 300 | + self._reconcile_payments(to_process_single, edit_mode=edit_mode) |
| 301 | + else: |
| 302 | + payments = self._init_payments(to_process, edit_mode=edit_mode) |
| 303 | + self._post_payments(to_process, edit_mode=edit_mode) |
| 304 | + self._reconcile_payments(to_process, edit_mode=edit_mode) |
| 305 | + |
| 306 | + return payments |
0 commit comments