# © 2009 EduSense BV () # © 2011-2013 Therp BV () # © 2016 Serv. Tecnol. Avanzados - Pedro M. Baeza # © 2016 Akretion (Alexis de Lattre - alexis.delattre@akretion.com) # License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html). import base64 from flectra import api, fields, models, _ from flectra.exceptions import UserError, ValidationError class AccountPaymentOrder(models.Model): _name = 'account.payment.order' _description = 'Payment Order' _inherit = ['mail.thread'] _order = 'id desc' def domain_journal_id(self): if not self.payment_mode_id: return [('id', '=', False)] if self.payment_mode_id.bank_account_link == 'fixed': return [('id', '=', self.payment_mode_id.fixed_journal_id.id)] elif self.payment_mode_id.bank_account_link == 'variable': jrl_ids = self.payment_mode_id.variable_journal_ids.ids return [('id', 'in', jrl_ids)] name = fields.Char( string='Number', readonly=True, copy=False) # v8 field : name payment_mode_id = fields.Many2one( 'account.payment.mode', 'Payment Mode', required=True, ondelete='restrict', track_visibility='onchange', readonly=True, states={'draft': [('readonly', False)]}) payment_type = fields.Selection([ ('inbound', 'Inbound'), ('outbound', 'Outbound'), ], string='Payment Type', readonly=True, required=True) payment_method_id = fields.Many2one( 'account.payment.method', related='payment_mode_id.payment_method_id', readonly=True, store=True) company_id = fields.Many2one( related='payment_mode_id.company_id', store=True, readonly=True) company_currency_id = fields.Many2one( related='payment_mode_id.company_id.currency_id', store=True, readonly=True) bank_account_link = fields.Selection( related='payment_mode_id.bank_account_link', readonly=True) journal_id = fields.Many2one( 'account.journal', string='Bank Journal', ondelete='restrict', readonly=True, states={'draft': [('readonly', False)]}, domain=domain_journal_id, track_visibility='onchange') # The journal_id field is only required at confirm step, to # allow auto-creation of payment order from invoice company_partner_bank_id = fields.Many2one( related='journal_id.bank_account_id', string='Company Bank Account', readonly=True) state = fields.Selection( [ ('draft', 'Draft'), ('open', 'Confirmed'), ('generated', 'File Generated'), ('uploaded', 'File Uploaded'), ('done', 'Done'), ('cancel', 'Cancel'), ], string='Status', readonly=True, copy=False, default='draft', track_visibility='onchange') date_prefered = fields.Selection([ ('now', 'Immediately'), ('due', 'Due Date'), ('fixed', 'Fixed Date'), ], string='Payment Execution Date Type', required=True, default='due', track_visibility='onchange', readonly=True, states={'draft': [('readonly', False)]}) ignore_past_date = fields.Boolean( string='Ignore Past Date', readonly=True, states={'draft': [('readonly', False)]}, help='the payment date will not be set to today if enabled.', ) date_scheduled = fields.Date( string='Payment Execution Date', readonly=True, states={'draft': [('readonly', False)]}, track_visibility='onchange', help="Select a requested date of execution if you selected 'Due Date' " "as the Payment Execution Date Type.") date_generated = fields.Date(string='File Generation Date', readonly=True) date_uploaded = fields.Date(string='File Upload Date', readonly=True) date_done = fields.Date(string='Done Date', readonly=True) generated_user_id = fields.Many2one( 'res.users', string='Generated by', readonly=True, ondelete='restrict', copy=False) payment_line_ids = fields.One2many( 'account.payment.line', 'order_id', string='Transaction Lines', readonly=True, states={'draft': [('readonly', False)]}) # v8 field : line_ids bank_line_ids = fields.One2many( 'bank.payment.line', 'order_id', string="Bank Payment Lines", readonly=True, help="The bank payment lines are used to generate the payment file. " "They are automatically created from transaction lines upon " "confirmation of the payment order: one bank payment line can " "group several transaction lines if the option " "'Group Transactions in Payment Orders' is active on the payment " "mode.") total_company_currency = fields.Monetary( compute='_compute_total', store=True, readonly=True, currency_field='company_currency_id') bank_line_count = fields.Integer( compute='_compute_bank_line_count', string='Number of Bank Lines', readonly=True) move_ids = fields.One2many( 'account.move', 'payment_order_id', string='Journal Entries', readonly=True) description = fields.Char() @api.multi def unlink(self): for order in self: if order.state == 'uploaded': raise UserError(_( "You cannot delete an uploaded payment order. You can " "cancel it in order to do so.")) return super(AccountPaymentOrder, self).unlink() @api.multi @api.constrains('payment_type', 'payment_mode_id') def payment_order_constraints(self): for order in self: if ( order.payment_mode_id.payment_type and order.payment_mode_id.payment_type != order.payment_type): raise ValidationError(_( "The payment type (%s) is not the same as the payment " "type of the payment mode (%s)") % ( order.payment_type, order.payment_mode_id.payment_type)) @api.multi @api.constrains('date_scheduled') def check_date_scheduled(self): today = fields.Date.context_today(self) for order in self: if order.date_scheduled: if order.date_scheduled < today: raise ValidationError(_( "On payment order %s, the Payment Execution Date " "is in the past (%s).") % (order.name, order.date_scheduled)) @api.multi @api.depends( 'payment_line_ids', 'payment_line_ids.amount_company_currency') def _compute_total(self): for rec in self: rec.total_company_currency = sum( rec.mapped('payment_line_ids.amount_company_currency') or [0.0]) @api.multi @api.depends('bank_line_ids') def _compute_bank_line_count(self): for order in self: order.bank_line_count = len(order.bank_line_ids) @api.model def create(self, vals): if vals.get('name', 'New') == 'New': vals['name'] = self.env['ir.sequence'].next_by_code( 'account.payment.order') or 'New' if vals.get('payment_mode_id'): payment_mode = self.env['account.payment.mode'].browse( vals['payment_mode_id']) vals['payment_type'] = payment_mode.payment_type if payment_mode.bank_account_link == 'fixed': vals['journal_id'] = payment_mode.fixed_journal_id.id if ( not vals.get('date_prefered') and payment_mode.default_date_prefered): vals['date_prefered'] = payment_mode.default_date_prefered return super(AccountPaymentOrder, self).create(vals) @api.onchange('payment_mode_id') def payment_mode_id_change(self): domain = self.domain_journal_id() res = {'domain': { 'journal_id': domain }} journals = self.env['account.journal'].search(domain) if len(journals) == 1: self.journal_id = journals if self.payment_mode_id.default_date_prefered: self.date_prefered = self.payment_mode_id.default_date_prefered return res @api.multi def action_done(self): self.write({ 'date_done': fields.Date.context_today(self), 'state': 'done', }) return True @api.multi def action_done_cancel(self): for move in self.move_ids: move.button_cancel() for move_line in move.line_ids: move_line.remove_move_reconcile() move.unlink() self.action_cancel() return True @api.multi def cancel2draft(self): self.write({'state': 'draft'}) return True @api.multi def action_cancel(self): for order in self: order.write({'state': 'cancel'}) order.bank_line_ids.unlink() return True @api.model def _prepare_bank_payment_line(self, paylines): return { 'order_id': paylines[0].order_id.id, 'payment_line_ids': [(6, 0, paylines.ids)], 'communication': '-'.join( [line.communication for line in paylines]), } @api.multi def draft2open(self): """ Called when you click on the 'Confirm' button Set the 'date' on payment line depending on the 'date_prefered' setting of the payment.order Re-generate the bank payment lines """ bplo = self.env['bank.payment.line'] today = fields.Date.context_today(self) for order in self: if not order.journal_id: raise UserError(_( 'Missing Bank Journal on payment order %s.') % order.name) if ( order.payment_method_id.bank_account_required and not order.journal_id.bank_account_id): raise UserError(_( "Missing bank account on bank journal '%s'.") % order.journal_id.display_name) if not order.payment_line_ids: raise UserError(_( 'There are no transactions on payment order %s.') % order.name) # Delete existing bank payment lines order.bank_line_ids.unlink() # Create the bank payment lines from the payment lines group_paylines = {} # key = hashcode for payline in order.payment_line_ids: payline.draft2open_payment_line_check() # Compute requested payment date if order.date_prefered == 'due': requested_date = payline.ml_maturity_date or today elif order.date_prefered == 'fixed': requested_date = order.date_scheduled or today else: requested_date = today # No payment date in the past if requested_date < today and not order.ignore_past_date: requested_date = today # inbound: check option no_debit_before_maturity if ( order.payment_type == 'inbound' and order.payment_mode_id.no_debit_before_maturity and payline.ml_maturity_date and requested_date < payline.ml_maturity_date): raise UserError(_( "The payment mode '%s' has the option " "'Disallow Debit Before Maturity Date'. The " "payment line %s has a maturity date %s " "which is after the computed payment date %s.") % ( order.payment_mode_id.name, payline.name, payline.ml_maturity_date, requested_date)) # Write requested_date on 'date' field of payment line payline.date = requested_date # Group options if order.payment_mode_id.group_lines: hashcode = payline.payment_line_hashcode() else: # Use line ID as hascode, which actually means no grouping hashcode = payline.id if hashcode in group_paylines: group_paylines[hashcode]['paylines'] += payline group_paylines[hashcode]['total'] +=\ payline.amount_currency else: group_paylines[hashcode] = { 'paylines': payline, 'total': payline.amount_currency, } # Create bank payment lines for paydict in list(group_paylines.values()): # Block if a bank payment line is <= 0 if paydict['total'] <= 0: raise UserError(_( "The amount for Partner '%s' is negative " "or null (%.2f) !") % (paydict['paylines'][0].partner_id.name, paydict['total'])) vals = self._prepare_bank_payment_line(paydict['paylines']) bplo.create(vals) self.write({'state': 'open'}) return True @api.multi def generate_payment_file(self): """Returns (payment file as string, filename)""" self.ensure_one() if self.payment_method_id.code == 'manual': return (False, False) else: raise UserError(_( "No handler for this payment method. Maybe you haven't " "installed the related Flectra module.")) @api.multi def open2generated(self): self.ensure_one() payment_file_str, filename = self.generate_payment_file() action = {} if payment_file_str and filename: attachment = self.env['ir.attachment'].create({ 'res_model': 'account.payment.order', 'res_id': self.id, 'name': filename, 'datas': base64.b64encode(payment_file_str), 'datas_fname': filename, }) simplified_form_view = self.env.ref( 'account_payment_order.view_attachment_simplified_form') action = { 'name': _('Payment File'), 'view_mode': 'form', 'view_id': simplified_form_view.id, 'res_model': 'ir.attachment', 'type': 'ir.actions.act_window', 'target': 'current', 'res_id': attachment.id, } self.write({ 'date_generated': fields.Date.context_today(self), 'state': 'generated', 'generated_user_id': self._uid, }) return action @api.multi def generated2uploaded(self): for order in self: if order.payment_mode_id.generate_move: order.generate_move() self.write({ 'state': 'uploaded', 'date_uploaded': fields.Date.context_today(self), }) return True @api.multi def _prepare_move(self, bank_lines=None): if self.payment_type == 'outbound': ref = _('Payment order %s') % self.name else: ref = _('Debit order %s') % self.name if bank_lines and len(bank_lines) == 1: ref += " - " + bank_lines.name if self.payment_mode_id.offsetting_account == 'bank_account': journal_id = self.journal_id.id elif self.payment_mode_id.offsetting_account == 'transfer_account': journal_id = self.payment_mode_id.transfer_journal_id.id vals = { 'journal_id': journal_id, 'ref': ref, 'payment_order_id': self.id, 'line_ids': [], } return vals @api.multi def _prepare_move_line_offsetting_account( self, amount_company_currency, amount_payment_currency, bank_lines): vals = {} if self.payment_type == 'outbound': name = _('Payment order %s') % self.name else: name = _('Debit order %s') % self.name if self.payment_mode_id.offsetting_account == 'bank_account': vals.update({'date': bank_lines[0].date}) else: vals.update({'date_maturity': bank_lines[0].date}) if self.payment_mode_id.offsetting_account == 'bank_account': account_id = self.journal_id.default_debit_account_id.id elif self.payment_mode_id.offsetting_account == 'transfer_account': account_id = self.payment_mode_id.transfer_account_id.id partner_id = False for index, bank_line in enumerate(bank_lines): if index == 0: partner_id = bank_line.payment_line_ids[0].partner_id.id elif bank_line.payment_line_ids[0].partner_id.id != partner_id: # we have different partners in the grouped move partner_id = False break vals.update({ 'name': name, 'partner_id': partner_id, 'account_id': account_id, 'credit': (self.payment_type == 'outbound' and amount_company_currency or 0.0), 'debit': (self.payment_type == 'inbound' and amount_company_currency or 0.0), }) if bank_lines[0].currency_id != bank_lines[0].company_currency_id: sign = self.payment_type == 'outbound' and -1 or 1 vals.update({ 'currency_id': bank_lines[0].currency_id.id, 'amount_currency': amount_payment_currency * sign, }) return vals @api.multi def _prepare_move_line_partner_account(self, bank_line): if bank_line.payment_line_ids[0].move_line_id: account_id =\ bank_line.payment_line_ids[0].move_line_id.account_id.id else: if self.payment_type == 'inbound': account_id =\ bank_line.partner_id.property_account_receivable_id.id else: account_id =\ bank_line.partner_id.property_account_payable_id.id if self.payment_type == 'outbound': name = _('Payment bank line %s') % bank_line.name else: name = _('Debit bank line %s') % bank_line.name vals = { 'name': name, 'bank_payment_line_id': bank_line.id, 'partner_id': bank_line.partner_id.id, 'account_id': account_id, 'credit': (self.payment_type == 'inbound' and bank_line.amount_company_currency or 0.0), 'debit': (self.payment_type == 'outbound' and bank_line.amount_company_currency or 0.0), } if bank_line.currency_id != bank_line.company_currency_id: sign = self.payment_type == 'inbound' and -1 or 1 vals.update({ 'currency_id': bank_line.currency_id.id, 'amount_currency': bank_line.amount_currency * sign, }) return vals @api.multi def generate_move(self): """ Create the moves that pay off the move lines from the payment/debit order. """ self.ensure_one() am_obj = self.env['account.move'] post_move = self.payment_mode_id.post_move # prepare a dict "trfmoves" that can be used when # self.payment_mode_id.move_option = date or line # key = unique identifier (date or True or line.id) # value = bank_pay_lines (recordset that can have several entries) trfmoves = {} for bline in self.bank_line_ids: hashcode = bline.move_line_offsetting_account_hashcode() if hashcode in trfmoves: trfmoves[hashcode] += bline else: trfmoves[hashcode] = bline for hashcode, blines in trfmoves.items(): mvals = self._prepare_move(blines) total_company_currency = total_payment_currency = 0 for bline in blines: total_company_currency += bline.amount_company_currency total_payment_currency += bline.amount_currency partner_ml_vals = self._prepare_move_line_partner_account( bline) mvals['line_ids'].append((0, 0, partner_ml_vals)) trf_ml_vals = self._prepare_move_line_offsetting_account( total_company_currency, total_payment_currency, blines) mvals['line_ids'].append((0, 0, trf_ml_vals)) move = am_obj.create(mvals) blines.reconcile_payment_lines() if post_move: move.post()