mirror of
https://gitlab.com/hibou-io/hibou-odoo/suite.git
synced 2025-01-20 12:37:31 +02:00
Initial commit account_payment_disperse for 11.0
This commit is contained in:
2
account_payment_disperse/__init__.py
Normal file
2
account_payment_disperse/__init__.py
Normal file
@@ -0,0 +1,2 @@
|
||||
from . import models
|
||||
from . import wizard
|
||||
19
account_payment_disperse/__manifest__.py
Normal file
19
account_payment_disperse/__manifest__.py
Normal file
@@ -0,0 +1,19 @@
|
||||
{
|
||||
'name': 'Payment Disperse',
|
||||
'version': '11.0.1.0.0',
|
||||
'author': 'Hibou Corp. <hello@hibou.io>',
|
||||
'category': 'Accounting',
|
||||
'summary': 'Pay multiple invoices with one Payment',
|
||||
'description': """
|
||||
Pay multiple invoices with one Payment, and manually disperse the amount per invoice.
|
||||
""",
|
||||
'website': 'https://hibou.io/',
|
||||
'depends': [
|
||||
'account_payment',
|
||||
],
|
||||
'data': [
|
||||
'wizard/register_payment_wizard_views.xml',
|
||||
],
|
||||
'installable': True,
|
||||
'auto_install': False,
|
||||
}
|
||||
1
account_payment_disperse/models/__init__.py
Normal file
1
account_payment_disperse/models/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
from . import account
|
||||
66
account_payment_disperse/models/account.py
Normal file
66
account_payment_disperse/models/account.py
Normal file
@@ -0,0 +1,66 @@
|
||||
from odoo import api, fields, models
|
||||
|
||||
|
||||
class AccountPayment(models.Model):
|
||||
_inherit = 'account.payment'
|
||||
|
||||
def _create_payment_entry(self, amount):
|
||||
wizard_id = self.env.context.get('payment_wizard_id')
|
||||
if wizard_id:
|
||||
wizard = self.env['account.register.payments'].browse(wizard_id)
|
||||
assert wizard
|
||||
if wizard.is_manual_disperse:
|
||||
return self._create_payment_entry_manual_disperse(
|
||||
-sum(wizard.invoice_line_ids.filtered(lambda p: p.partner_id == self.partner_id).mapped('amount')),
|
||||
wizard)
|
||||
|
||||
return super(AccountPayment, self)._create_payment_entry(amount)
|
||||
|
||||
def _create_payment_entry_manual_disperse(self, amount, wizard):
|
||||
self.amount = abs(amount)
|
||||
if hasattr(self, 'check_amount_in_words'):
|
||||
self.check_amount_in_words = self.currency_id.amount_to_text(self.amount)
|
||||
aml_obj = self.env['account.move.line'].with_context(check_move_validity=False)
|
||||
invoice_currency = False
|
||||
if self.invoice_ids and all([x.currency_id == self.invoice_ids[0].currency_id for x in self.invoice_ids]):
|
||||
# if all the invoices selected share the same currency, record the paiement in that currency too
|
||||
invoice_currency = self.invoice_ids[0].currency_id
|
||||
debit, credit, amount_currency, currency_id = aml_obj.with_context(date=self.payment_date)\
|
||||
.compute_amount_fields(amount, self.currency_id, self.company_id.currency_id, invoice_currency)
|
||||
|
||||
move = self.env['account.move'].create(self._get_move_vals())
|
||||
|
||||
inv_lines = []
|
||||
for partial_invoice in wizard.invoice_line_ids.filtered(lambda p: p.amount and p.partner_id == self.partner_id):
|
||||
inv_amount = partial_invoice.amount if amount > 0 else -partial_invoice.amount
|
||||
i_debit, i_credit, i_amount_currency, i_currency_id = aml_obj.with_context(
|
||||
date=self.payment_date).compute_amount_fields(inv_amount, self.currency_id, self.company_id.currency_id,
|
||||
invoice_currency)
|
||||
counterpart_aml_dict = self._get_shared_move_line_vals(i_debit, i_credit, i_amount_currency, move.id, False)
|
||||
counterpart_aml_dict.update(self._get_counterpart_move_line_vals(partial_invoice.invoice_id))
|
||||
counterpart_aml_dict.update({'currency_id': currency_id})
|
||||
counterpart_aml = aml_obj.create(counterpart_aml_dict)
|
||||
# capture writeoff account etc.
|
||||
counterpart_aml |= partial_invoice.invoice_id.move_id.line_ids.filtered(
|
||||
lambda r: not r.reconciled and r.account_id.internal_type in ('payable', 'receivable'))
|
||||
inv_lines.append((counterpart_aml, partial_invoice.writeoff_acc_id))
|
||||
|
||||
# Create Payment side (payment journal default accounts)
|
||||
if not self.currency_id.is_zero(self.amount):
|
||||
if not self.currency_id != self.company_id.currency_id:
|
||||
amount_currency = 0
|
||||
liquidity_aml_dict = self._get_shared_move_line_vals(credit, debit, -amount_currency, move.id, False)
|
||||
liquidity_aml_dict.update(self._get_liquidity_move_line_vals(-amount))
|
||||
aml_obj.create(liquidity_aml_dict)
|
||||
|
||||
# validate the payment
|
||||
move.post()
|
||||
|
||||
# reconcile the invoice receivable/payable line(s) with the payment
|
||||
for inv_lines, writeoff_acc_id in inv_lines:
|
||||
# _logger.warn('pair: ')
|
||||
# for l in inv_lines:
|
||||
# _logger.warn(' ' + str(l) + ' credit: ' + str(l.credit) + ' debit: ' + str(l.debit))
|
||||
inv_lines.reconcile(writeoff_acc_id, wizard.writeoff_journal_id)
|
||||
|
||||
return move
|
||||
1
account_payment_disperse/tests/__init__.py
Normal file
1
account_payment_disperse/tests/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
from . import test_payment_multi
|
||||
124
account_payment_disperse/tests/test_payment_multi.py
Normal file
124
account_payment_disperse/tests/test_payment_multi.py
Normal file
@@ -0,0 +1,124 @@
|
||||
from odoo.addons.account.tests.test_payment import TestPayment
|
||||
from odoo.exceptions import ValidationError
|
||||
import time
|
||||
|
||||
|
||||
class PaymentMultiTest(TestPayment):
|
||||
|
||||
def test_multiple_payments_partial(self):
|
||||
""" Create test to pay several vendor bills/invoices at once """
|
||||
# One payment for inv_1 and inv_2 (same partner)
|
||||
inv_1 = self.create_invoice(amount=100, currency_id=self.currency_eur_id, partner=self.partner_agrolait.id)
|
||||
inv_2 = self.create_invoice(amount=500, currency_id=self.currency_eur_id, partner=self.partner_agrolait.id)
|
||||
|
||||
ids = [inv_1.id, inv_2.id]
|
||||
register_payments = self.register_payments_model.with_context(active_ids=ids).create({
|
||||
'payment_date': time.strftime('%Y') + '-07-15',
|
||||
'journal_id': self.bank_journal_euro.id,
|
||||
'payment_method_id': self.payment_method_manual_in.id,
|
||||
})
|
||||
register_payments.amount = 399.0
|
||||
register_payments.is_manual_disperse = True
|
||||
|
||||
with self.assertRaises(ValidationError):
|
||||
register_payments.create_payments()
|
||||
|
||||
for line in register_payments.invoice_line_ids:
|
||||
if line.invoice_id == inv_1:
|
||||
line.amount = 99.0
|
||||
if line.invoice_id == inv_2:
|
||||
line.amount = 300.0
|
||||
|
||||
register_payments.create_payments()
|
||||
|
||||
payment_ids = self.payment_model.search([('invoice_ids', 'in', ids)], order="id desc")
|
||||
self.assertEqual(len(payment_ids), 1, 'Need only one payment.')
|
||||
self.assertEqual(payment_ids.amount, 399.0)
|
||||
|
||||
self.assertEqual(inv_1.residual_signed, 1.0)
|
||||
self.assertEqual(inv_2.residual_signed, 200.0)
|
||||
|
||||
register_payments = self.register_payments_model.with_context(active_ids=ids).create({
|
||||
'payment_date': time.strftime('%Y') + '-07-15',
|
||||
'journal_id': self.bank_journal_euro.id,
|
||||
'payment_method_id': self.payment_method_manual_in.id,
|
||||
})
|
||||
register_payments.amount = 200.0
|
||||
register_payments.is_manual_disperse = True
|
||||
|
||||
for line in register_payments.invoice_line_ids:
|
||||
if line.invoice_id == inv_2:
|
||||
line.amount = 200.0
|
||||
|
||||
register_payments.create_payments()
|
||||
self.assertEqual(inv_1.residual_signed, 1.0)
|
||||
self.assertEqual(inv_2.residual_signed, 0.0)
|
||||
|
||||
def test_multiple_payments_write_off(self):
|
||||
inv_1 = self.create_invoice(amount=100, currency_id=self.currency_eur_id, partner=self.partner_agrolait.id)
|
||||
inv_2 = self.create_invoice(amount=500, currency_id=self.currency_eur_id, partner=self.partner_agrolait.id)
|
||||
|
||||
ids = [inv_1.id, inv_2.id]
|
||||
register_payments = self.register_payments_model.with_context(active_ids=ids).create({
|
||||
'payment_date': time.strftime('%Y') + '-07-15',
|
||||
'journal_id': self.bank_journal_euro.id,
|
||||
'payment_method_id': self.payment_method_manual_in.id,
|
||||
})
|
||||
register_payments.amount = 400.0
|
||||
register_payments.is_manual_disperse = True
|
||||
register_payments.writeoff_journal_id = inv_1.journal_id
|
||||
|
||||
with self.assertRaises(ValidationError):
|
||||
register_payments.create_payments()
|
||||
|
||||
for line in register_payments.invoice_line_ids:
|
||||
if line.invoice_id == inv_1:
|
||||
line.amount = 100.0
|
||||
if line.invoice_id == inv_2:
|
||||
line.amount = 300.0
|
||||
line.writeoff_acc_id = self.account_revenue
|
||||
|
||||
register_payments.create_payments()
|
||||
|
||||
payment_ids = self.payment_model.search([('invoice_ids', 'in', ids)], order="id desc")
|
||||
self.assertEqual(len(payment_ids), 1, 'Need only one payment.')
|
||||
self.assertEqual(payment_ids.amount, 400.0)
|
||||
|
||||
self.assertEqual(inv_1.residual_signed, 0.0)
|
||||
self.assertEqual(inv_2.residual_signed, 0.0)
|
||||
|
||||
def test_multiple_payments_partial_multi(self):
|
||||
""" Create test to pay several vendor bills/invoices at once """
|
||||
# One payment for inv_1 and inv_2 (same partner)
|
||||
inv_1 = self.create_invoice(amount=100, currency_id=self.currency_eur_id, partner=self.partner_agrolait.id)
|
||||
inv_2 = self.create_invoice(amount=500, currency_id=self.currency_eur_id, partner=self.partner_china_exp.id)
|
||||
|
||||
ids = [inv_1.id, inv_2.id]
|
||||
register_payments = self.register_payments_model.with_context(active_ids=ids).create({
|
||||
'payment_date': time.strftime('%Y') + '-07-15',
|
||||
'journal_id': self.bank_journal_euro.id,
|
||||
'payment_method_id': self.payment_method_manual_in.id,
|
||||
})
|
||||
register_payments.amount = 400.0
|
||||
register_payments.is_manual_disperse = True
|
||||
|
||||
for line in register_payments.invoice_line_ids:
|
||||
if line.invoice_id == inv_1:
|
||||
line.amount = 100.0
|
||||
if line.invoice_id == inv_2:
|
||||
line.amount = 300.0
|
||||
|
||||
register_payments.create_payments()
|
||||
|
||||
payment_ids = self.payment_model.search([('invoice_ids', 'in', ids)], order="id desc")
|
||||
self.assertEqual(len(payment_ids), 2, 'Need two payments.')
|
||||
# for pay in payment_ids:
|
||||
# _logger.warn(str(pay) + ' amount: ' + str(pay.amount))
|
||||
# for line in pay.move_line_ids:
|
||||
# _logger.warn(' ' +
|
||||
# str(line) + ' name: ' + str(line.name) + ' :: credit: ' + str(line.credit) + ' debit: ' +
|
||||
# str(line.debit))
|
||||
self.assertEqual(sum(payment_ids.mapped('amount')), 400.0)
|
||||
|
||||
self.assertEqual(inv_1.residual_signed, 0.0)
|
||||
self.assertEqual(inv_2.residual_signed, 200.0)
|
||||
1
account_payment_disperse/wizard/__init__.py
Normal file
1
account_payment_disperse/wizard/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
from . import register_payment_wizard
|
||||
87
account_payment_disperse/wizard/register_payment_wizard.py
Normal file
87
account_payment_disperse/wizard/register_payment_wizard.py
Normal file
@@ -0,0 +1,87 @@
|
||||
from odoo import api, fields, models
|
||||
from odoo.exceptions import ValidationError
|
||||
|
||||
|
||||
class AccountRegisterPayments(models.TransientModel):
|
||||
_inherit = 'account.register.payments'
|
||||
|
||||
is_manual_disperse = fields.Boolean(string='Disperse Manually')
|
||||
invoice_line_ids = fields.One2many('account.register.payments.invoice.line', 'wizard_id', string='Invoices')
|
||||
writeoff_journal_id = fields.Many2one('account.journal', string='Write-off Journal')
|
||||
due_date_cutoff = fields.Date(string='Due Date Cutoff', default=fields.Date.today)
|
||||
|
||||
@api.model
|
||||
def default_get(self, fields):
|
||||
rec = super(AccountRegisterPayments, self).default_get(fields)
|
||||
invoice_ids = rec['invoice_ids'][0][2]
|
||||
rec['invoice_line_ids'] = [(0, 0, {'invoice_id': i, 'amount': 0.0}) for i in invoice_ids]
|
||||
return rec
|
||||
|
||||
@api.multi
|
||||
def create_payments(self):
|
||||
for payment in self.filtered(lambda p: p.is_manual_disperse):
|
||||
line_amount = sum(payment.invoice_line_ids.mapped('amount'))
|
||||
if abs(line_amount - payment.amount) >= 0.01:
|
||||
raise ValidationError('Cannot pay for %0.2f worth of invoices with %0.2f total.' %
|
||||
(line_amount, payment.amount))
|
||||
if not payment.writeoff_journal_id and payment.invoice_line_ids.filtered(lambda l: l.writeoff_acc_id):
|
||||
raise ValidationError('Cannot write off without a write off journal.')
|
||||
new_self = self.with_context(payment_wizard_id=self.id)
|
||||
return super(AccountRegisterPayments, new_self).create_payments()
|
||||
|
||||
@api.multi
|
||||
def action_fill_residual(self):
|
||||
for payment in self:
|
||||
for line in payment.invoice_line_ids:
|
||||
line.amount = line.residual
|
||||
action = self.env.ref('account.action_account_payment_from_invoices').read()[0]
|
||||
action['res_id'] = payment.id
|
||||
return action
|
||||
|
||||
@api.multi
|
||||
def action_fill_residual_due(self):
|
||||
for payment in self:
|
||||
for line in payment.invoice_line_ids:
|
||||
line.amount = line.residual_due
|
||||
action = self.env.ref('account.action_account_payment_from_invoices').read()[0]
|
||||
action['res_id'] = payment.id
|
||||
return action
|
||||
|
||||
|
||||
class AccountRegisterPaymentsInvoiceLine(models.TransientModel):
|
||||
_name = 'account.register.payments.invoice.line'
|
||||
|
||||
wizard_id = fields.Many2one('account.register.payments')
|
||||
invoice_id = fields.Many2one('account.invoice', string='Invoice', required=True)
|
||||
partner_id = fields.Many2one('res.partner', string='Partner', compute='_compute_balances')
|
||||
residual = fields.Float(string='Remaining', compute='_compute_balances')
|
||||
residual_due = fields.Float(string='Due', compute='_compute_balances')
|
||||
difference = fields.Float(string='Difference', default=0.0)
|
||||
amount = fields.Float(string='Amount')
|
||||
writeoff_acc_id = fields.Many2one('account.account', string='Write-off Account')
|
||||
|
||||
@api.depends('invoice_id.residual', 'wizard_id.due_date_cutoff', 'invoice_id.partner_id')
|
||||
def _compute_balances(self):
|
||||
for line in self:
|
||||
line.residual = line.invoice_id.residual
|
||||
|
||||
cutoff_date = line.wizard_id.due_date_cutoff
|
||||
total_amount = 0.0
|
||||
total_reconciled = 0.0
|
||||
for move_line in line.invoice_id.move_id.line_ids.filtered(lambda r: (
|
||||
not r.reconciled
|
||||
and r.account_id.internal_type in ('payable', 'receivable')
|
||||
and r.date_maturity <= cutoff_date
|
||||
)):
|
||||
amount = abs(move_line.debit - move_line.credit)
|
||||
total_amount += amount
|
||||
for partial_line in (move_line.matched_debit_ids + move_line.matched_credit_ids):
|
||||
total_reconciled += partial_line.amount
|
||||
line.residual_due = total_amount - total_reconciled
|
||||
line.difference = line.residual - line.amount
|
||||
line.partner_id = line.invoice_id.partner_id
|
||||
|
||||
@api.onchange('amount')
|
||||
def _onchange_amount(self):
|
||||
for line in self:
|
||||
line.difference = line.residual - line.amount
|
||||
@@ -0,0 +1,45 @@
|
||||
<?xml version="1.0" encoding="UTF-8" ?>
|
||||
<odoo>
|
||||
<record id="view_account_payment_from_invoices_inherited" model="ir.ui.view">
|
||||
<field name="name">account.register.payments.wizard.inherited</field>
|
||||
<field name="model">account.register.payments</field>
|
||||
<field name="inherit_id" ref="account.view_account_payment_from_invoices"/>
|
||||
<field name="arch" type="xml">
|
||||
<xpath expr="//field[@name='amount']" position="attributes">
|
||||
<attribute name="attrs">{'readonly': [('multi', '=', True), ('is_manual_disperse', '!=', True)]}</attribute>
|
||||
</xpath>
|
||||
<xpath expr="//field[@name='amount']" position="after">
|
||||
<field name="is_manual_disperse"/>
|
||||
</xpath>
|
||||
<xpath expr="//form/group[1]" position="after">
|
||||
<group name="invoice_lines" attrs="{'invisible': [('is_manual_disperse', '=', False)]}">
|
||||
<group>
|
||||
<button type="object" name="action_fill_residual"
|
||||
string="Fill with Remaining" class="btn-default"/>
|
||||
<button type="object" name="action_fill_residual_due"
|
||||
string="Fill with Due" class="btn-default"/>
|
||||
</group>
|
||||
<field name="invoice_line_ids" nolabel="1">
|
||||
<tree editable="bottom" create="false">
|
||||
<field name="wizard_id" invisible="1"/>
|
||||
<field name="partner_id" readonly="1"/>
|
||||
<field name="invoice_id"/>
|
||||
<field name="residual" readonly="1" sum="Total Residual"/>
|
||||
<field name="residual_due" readonly="1" sum="Total Due"/>
|
||||
<field name="amount" sum="Total Amount"/>
|
||||
<field name="difference" readonly="1" sum="Total Difference"/>
|
||||
<field name="writeoff_acc_id"/>
|
||||
</tree>
|
||||
</field>
|
||||
</group>
|
||||
<group name="invoice_totals" attrs="{'invisible': [('is_manual_disperse', '=', False)]}">
|
||||
<group>
|
||||
<field name="writeoff_journal_id"/>
|
||||
<field name="due_date_cutoff"/>
|
||||
</group>
|
||||
<group/>
|
||||
</group>
|
||||
</xpath>
|
||||
</field>
|
||||
</record>
|
||||
</odoo>
|
||||
Reference in New Issue
Block a user