# -*- coding: utf-8 -*- # © 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 (http://www.gnu.org/licenses/agpl.html). from openerp import models, fields, api, _ from openerp.exceptions import UserError, ValidationError class AccountPaymentOrder(models.Model): _name = 'account.payment.order' _description = 'Payment Order' _inherit = ['mail.thread'] _order = 'id desc' name = fields.Char( string='Number', readonly=True, copy=False, track_visibility='onchange') # v8 field : name payment_mode_id = fields.Many2one( 'account.payment.mode', 'Payment Method', 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) 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', readonly=True, states={'draft': [('readonly', False)]}) # 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)]}) 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_done = fields.Date(string='Date Done', readonly=True) date_generated = fields.Date(string='Date Generated', 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) total_company_currency = fields.Monetary( compute='_compute_total', store=True, readonly=True, currency_field='company_currency_id') bank_line_count = fields.Integer( compute='_bank_line_count', string='Number of Bank Lines', readonly=True) @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.one @api.depends( 'payment_line_ids', 'payment_line_ids.amount_company_currency') def _compute_total(self): self.total_company_currency = sum( self.mapped('payment_line_ids.amount_company_currency') or [0.0]) @api.multi @api.depends('bank_line_ids') def _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' return super(AccountPaymentOrder, self).create(vals) @api.onchange('payment_mode_id') def payment_mode_id_change(self): res = {'domain': {}} if self.payment_mode_id: if self.payment_mode_id.bank_account_link == 'fixed': self.journal_id = self.payment_mode_id.fixed_journal_id # journal_id is a required field, so I can't set it readonly # so I restrict the domain so that the user cannot change # the journal res['domain']['journal_id'] = [( 'id', '=', self.payment_mode_id.fixed_journal_id.id)] else: res['domain']['journal_id'] = [( 'id', 'in', self.payment_mode_id.variable_journal_ids.ids)] else: self.journal_id = False return res @api.multi def action_done(self): self.write({ 'date_done': fields.Date.context_today(self), 'state': 'done', }) 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) # 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.check_payment_line() # 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 # 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 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() raise UserError(_( "No handler for this payment method. Maybe you haven't " "installed the related Odoo module.")) @api.multi def open2generated(self): self.ensure_one() payment_file_str, filename = self.generate_payment_file() attachment = self.env['ir.attachment'].create({ 'res_model': 'account.payment.order', 'res_id': self.id, 'name': filename, 'datas': payment_file_str.encode('base64'), 'datas_fname': filename, }) self.write({ 'date_generated': fields.Date.context_today(self), 'state': 'generated', 'generated_user_id': self._uid, }) 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, } return action @api.multi def generated2uploaded(self): for order in self: if order.payment_mode_id.transfer_move: order.generate_transfer_move() self.write({'state': 'uploaded'}) return True # Generation of transfer move @api.multi def _prepare_transfer_move(self): vals = { 'journal_id': self.mode.transfer_journal_id.id, 'ref': '%s %s' % ( self.payment_order_type[:3].upper(), self.reference) } return vals @api.multi def _prepare_move_line_transfer_account( self, amount, move, bank_payment_lines, labels): if len(bank_payment_lines) == 1: partner_id = bank_payment_lines[0].partner_id.id name = _('%s bank line %s') % (labels[self.payment_order_type], bank_payment_lines[0].name) else: partner_id = False name = '%s %s' % ( labels[self.payment_order_type], self.reference) date_maturity = bank_payment_lines[0].date vals = { 'name': name, 'move_id': move.id, 'partner_id': partner_id, 'account_id': self.mode.transfer_account_id.id, 'credit': (self.payment_order_type == 'payment' and amount or 0.0), 'debit': (self.payment_order_type == 'debit' and amount or 0.0), 'date_maturity': date_maturity, } return vals @api.multi def _prepare_move_line_partner_account(self, bank_line, move, labels): # TODO : ALEXIS check don't group if move_line_id.account_id # is not the same 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_order_type == 'debit': account_id =\ bank_line.partner_id.property_account_receivable.id else: account_id = bank_line.partner_id.property_account_payable.id vals = { 'name': _('%s line %s') % ( labels[self.payment_order_type], bank_line.name), 'move_id': move.id, 'partner_id': bank_line.partner_id.id, 'account_id': account_id, 'credit': (self.payment_order_type == 'debit' and bank_line.amount_currency or 0.0), 'debit': (self.payment_order_type == 'payment' and bank_line.amount_currency or 0.0), } return vals @api.multi def action_sent_no_move_line_hook(self, pay_line): """This function is designed to be inherited""" return @api.multi def _create_move_line_partner_account(self, bank_line, move, labels): """This method is designed to be inherited in a custom module""" # TODO: take multicurrency into account company_currency = self.env.user.company_id.currency_id if bank_line.currency != company_currency: raise UserError(_( "Cannot generate the transfer move when " "the currency of the payment (%s) is not the " "same as the currency of the company (%s). This " "is not supported for the moment.") % (bank_line.currency.name, company_currency.name)) aml_obj = self.env['account.move.line'] # create the payment/debit counterpart move line # on the partner account partner_ml_vals = self._prepare_move_line_partner_account( bank_line, move, labels) partner_move_line = aml_obj.create(partner_ml_vals) # register the payment/debit move line # on the payment line and call reconciliation on it bank_line.write({'transit_move_line_id': partner_move_line.id}) @api.multi def _reconcile_payment_lines(self, bank_payment_lines): for bline in bank_payment_lines: if all([pline.move_line_id for pline in bline.payment_line_ids]): bline.debit_reconcile() else: self.action_sent_no_move_line_hook(bline) @api.multi def generate_transfer_move(self): """ Create the moves that pay off the move lines from the debit order. """ self.ensure_one() am_obj = self.env['account.move'] aml_obj = self.env['account.move.line'] labels = { 'outbound': _('Payment'), 'inbound': _('Direct debit'), } # prepare a dict "trfmoves" that can be used when # self.mode.transfer_move_option = date or line # key = unique identifier (date or True or line.id) # value = [pay_line1, pay_line2, ...] trfmoves = {} for bline in self.bank_line_ids: hashcode = bline.move_line_transfer_account_hashcode() if hashcode in trfmoves: trfmoves[hashcode].append(bline) else: trfmoves[hashcode] = [bline] for hashcode, blines in trfmoves.iteritems(): mvals = self._prepare_transfer_move() move = am_obj.create(mvals) total_amount = 0 for bline in blines: total_amount += bline.amount_currency self._create_move_line_partner_account(bline, move, labels) # create the payment/debit move line on the transfer account trf_ml_vals = self._prepare_move_line_transfer_account( total_amount, move, blines, labels) aml_obj.create(trf_ml_vals) self._reconcile_payment_lines(blines) move.post()