[ADD] account_debit_note: Debit Note functionality

You might think that if the debit note is like the inverse
of a credit note, that it is just a regular invoice.

Well, it is, except that we had several localizations
adding a link with the original invoice for the debit note.
This way, we can share that many2one and add a wizard that is
similar as the one for the credit once and for all.

In order to keep things simple, we always create a
draft debit note.  The only option we give, is to copy
the original invoice lines or not.

You can also create a debit note from a credit note,
in which case the type needs to change, but we don't
copy the lines in that case (for now).

opw-2181586

closes odoo/odoo#45542

X-original-commit: f1af011d0966fa6e436168879b7f18fabb955934
Signed-off-by: Josse Colpaert <jco@openerp.com>
This commit is contained in:
Josse Colpaert
2020-02-17 16:33:22 +00:00
parent 8de07a8aa5
commit 47eff65afd
12 changed files with 440 additions and 0 deletions
+5
View File
@@ -0,0 +1,5 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from . import models
from . import wizard
+23
View File
@@ -0,0 +1,23 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
{
'name': 'Debit Notes',
'version': '1.0',
'category': 'Accounting/Accounting',
'summary': 'Debit Notes',
'description': """
In a lot of countries, a debit note is used as an increase of the amounts of an existing invoice
or in some specific cases to cancel a credit note.
It is like a regular invoice, but we need to keep track of the link with the original invoice.
The wizard used is similar as the one for the credit note.
""",
'depends': ['account'],
'data': [
'wizard/account_debit_note_view.xml',
'views/account_move_view.xml',
'security/ir.model.access.csv',
],
'installable': True,
'auto_install': False,
}
@@ -0,0 +1,165 @@
# Translation of Odoo Server.
# This file contains the translation of the following modules:
# * account_debit_note
#
msgid ""
msgstr ""
"Project-Id-Version: Odoo Server 13.0+e\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2020-02-14 09:23+0000\n"
"PO-Revision-Date: 2020-02-14 09:23+0000\n"
"Last-Translator: \n"
"Language-Team: \n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: \n"
"Plural-Forms: \n"
#. module: account_debit_note
#: model_terms:ir.ui.view,arch_db:account_debit_note.view_move_form_debit
msgid "<span class=\"o_stat_text\">Debit Notes</span>"
msgstr ""
#. module: account_debit_note
#: model_terms:ir.ui.view,arch_db:account_debit_note.view_move_form_debit
msgid "Add Debit Note"
msgstr ""
#. module: account_debit_note
#: model:ir.model,name:account_debit_note.model_account_debit_note
msgid "Add Debit Note wizard"
msgstr ""
#. module: account_debit_note
#: model_terms:ir.ui.view,arch_db:account_debit_note.view_account_debit_note
msgid "Cancel"
msgstr ""
#. module: account_debit_note
#: model:ir.model.fields,field_description:account_debit_note.field_account_debit_note__copy_lines
msgid "Copy Lines"
msgstr ""
#. module: account_debit_note
#: model:ir.actions.act_window,name:account_debit_note.action_view_account_move_debit
#: model_terms:ir.ui.view,arch_db:account_debit_note.view_account_debit_note
msgid "Create Debit Note"
msgstr ""
#. module: account_debit_note
#: model:ir.model.fields,field_description:account_debit_note.field_account_debit_note__create_uid
msgid "Created by"
msgstr ""
#. module: account_debit_note
#: model:ir.model.fields,field_description:account_debit_note.field_account_debit_note__create_date
msgid "Created on"
msgstr ""
#. module: account_debit_note
#: model:ir.model.fields,field_description:account_debit_note.field_account_debit_note__date
#: model_terms:ir.ui.view,arch_db:account_debit_note.view_account_debit_note
msgid "Debit Note Date"
msgstr ""
#. module: account_debit_note
#: code:addons/account_debit_note/models/account_move.py:0
#: code:addons/account_debit_note/wizard/account_debit_note.py:0
#: model:ir.model.fields,field_description:account_debit_note.field_account_move__debit_note_ids
#, python-format
msgid "Debit Notes"
msgstr ""
#. module: account_debit_note
#: model:ir.model.fields,field_description:account_debit_note.field_account_debit_note__display_name
msgid "Display Name"
msgstr ""
#. module: account_debit_note
#: model:ir.model.fields,field_description:account_debit_note.field_account_debit_note__id
msgid "ID"
msgstr ""
#. module: account_debit_note
#: model:ir.model.fields,help:account_debit_note.field_account_debit_note__journal_id
msgid "If empty, uses the journal of the journal entry to be debited."
msgstr ""
#. module: account_debit_note
#: model:ir.model.fields,help:account_debit_note.field_account_debit_note__copy_lines
msgid ""
"In case you need to do corrections for every line, it can be in handy to "
"copy them. We won't copy them for debit notes from credit notes. "
msgstr ""
#. module: account_debit_note
#: model:ir.model,name:account_debit_note.model_account_move
msgid "Journal Entries"
msgstr ""
#. module: account_debit_note
#: model:ir.model.fields,field_description:account_debit_note.field_account_debit_note__journal_type
msgid "Journal Type"
msgstr ""
#. module: account_debit_note
#: model:ir.model.fields,field_description:account_debit_note.field_account_debit_note____last_update
msgid "Last Modified on"
msgstr ""
#. module: account_debit_note
#: model:ir.model.fields,field_description:account_debit_note.field_account_debit_note__write_uid
msgid "Last Updated by"
msgstr ""
#. module: account_debit_note
#: model:ir.model.fields,field_description:account_debit_note.field_account_debit_note__write_date
msgid "Last Updated on"
msgstr ""
#. module: account_debit_note
#: model:ir.model.fields,field_description:account_debit_note.field_account_debit_note__move_ids
msgid "Move"
msgstr ""
#. module: account_debit_note
#: model:ir.model.fields,field_description:account_debit_note.field_account_debit_note__move_type
msgid "Move Type"
msgstr ""
#. module: account_debit_note
#: model:ir.model.fields,field_description:account_debit_note.field_account_move__debit_note_count
msgid "Number of Debit Notes"
msgstr ""
#. module: account_debit_note
#: model:ir.model.fields,field_description:account_debit_note.field_account_move__debit_origin_id
msgid "Original Invoice Debited"
msgstr ""
#. module: account_debit_note
#: model:ir.model.fields,field_description:account_debit_note.field_account_debit_note__reason
msgid "Reason"
msgstr ""
#. module: account_debit_note
#: model:ir.model.fields,help:account_debit_note.field_account_move__debit_note_ids
msgid "The debit notes created for this invoice"
msgstr ""
#. module: account_debit_note
#: code:addons/account_debit_note/wizard/account_debit_note.py:0
#, python-format
msgid "This debit note was created from:"
msgstr ""
#. module: account_debit_note
#: model:ir.model.fields,field_description:account_debit_note.field_account_debit_note__journal_id
msgid "Use Specific Journal"
msgstr ""
#. module: account_debit_note
#: code:addons/account_debit_note/wizard/account_debit_note.py:0
#, python-format
msgid "You can only debit posted moves."
msgstr ""
@@ -0,0 +1,4 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from . import account_move
@@ -0,0 +1,30 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import models, fields, api, _
class AccountMove(models.Model):
_inherit = "account.move"
debit_origin_id = fields.Many2one('account.move', 'Original Invoice Debited', readonly=True, copy=False)
debit_note_ids = fields.One2many('account.move', 'debit_origin_id', 'Debit Notes',
help="The debit notes created for this invoice")
debit_note_count = fields.Integer('Number of Debit Notes', compute='_compute_debit_count')
@api.depends('debit_note_ids')
def _compute_debit_count(self):
debit_data = self.env['account.move'].read_group([('debit_origin_id', 'in', self.ids)],
['debit_origin_id'], ['debit_origin_id'])
data_map = {datum['debit_origin_id'][0]: datum['debit_origin_id_count'] for datum in debit_data}
for inv in self:
inv.debit_note_count = data_map.get(inv.id, 0.0)
def action_view_debit_notes(self):
self.ensure_one()
return {
'type': 'ir.actions.act_window',
'name': _('Debit Notes'),
'res_model': 'account.move',
'view_mode': 'tree,form',
'domain': [('debit_origin_id', '=', self.id)],
}
@@ -0,0 +1,2 @@
"id","name","model_id:id","group_id:id","perm_read","perm_write","perm_create","perm_unlink"
"access_account_debit_note_user","account_debit_note_group_invoice","model_account_debit_note","account.group_account_invoice",1,1,1,0
1 id name model_id:id group_id:id perm_read perm_write perm_create perm_unlink
2 access_account_debit_note_user account_debit_note_group_invoice model_account_debit_note account.group_account_invoice 1 1 1 0
@@ -0,0 +1,3 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from . import test_out_debit_note
@@ -0,0 +1,46 @@
# -*- coding: utf-8 -*-
from odoo.addons.account.tests.common import AccountTestInvoicingCommon
from odoo.tests import tagged
from odoo import fields
@tagged('post_install', '-at_install')
class TestAccountDebitNote(AccountTestInvoicingCommon):
def test_00_debit_note_out_invoice(self):
""" Debit Note of a regular Customer Invoice"""
invoice = self.init_invoice('out_invoice')
invoice.post()
move_debit_note_wiz = self.env['account.debit.note'].with_context(active_model="account.move",
active_ids=invoice.ids).create({
'date': fields.Date.from_string('2019-02-01'),
'reason': 'no reason',
'copy_lines': True,
})
move_debit_note_wiz.create_debit()
# Search for the original invoice
debit_note = self.env['account.move'].search([('debit_origin_id', '=', invoice.id)])
debit_note.ensure_one()
self.assertEqual(len(debit_note.invoice_line_ids), 2, "Should have copied the invoice lines")
self.assertEqual(debit_note.type, 'out_invoice', 'Type of debit note should be the same as the original invoice')
self.assertEqual(debit_note.state, 'draft', 'We should create debit notes in draft state')
def test_10_debit_note_in_refund(self):
""" Debit Note of a vendor refund (is a regular vendor bill) """
invoice = self.init_invoice('in_refund')
invoice.post()
move_debit_note_wiz = self.env['account.debit.note'].with_context(active_model="account.move",
active_ids=invoice.ids).create({
'date': fields.Date.from_string('2019-02-01'),
'reason': 'in order to cancel refund',
})
move_debit_note_wiz.create_debit()
# Search for the original invoice
debit_note = self.env['account.move'].search([('debit_origin_id', '=', invoice.id)])
debit_note.ensure_one()
self.assertFalse(debit_note.invoice_line_ids, 'We should not copy lines by default on debit notes')
self.assertEqual(debit_note.type, 'in_invoice', 'Type of debit note should not be refund anymore')
self.assertEqual(debit_note.state, 'draft', 'We should create debit notes in draft state')
@@ -0,0 +1,28 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<record id="view_move_form_debit" model="ir.ui.view">
<field name="name">account.move.form.debit</field>
<field name="model">account.move</field>
<field name="inherit_id" ref="account.view_move_form"/>
<field name="arch" type="xml">
<button name="action_reverse" position="after">
<field name="debit_origin_id" invisible="1"/>
<button name="%(action_view_account_move_debit)d" string='Add Debit Note'
type='action' groups="account.group_account_invoice"
attrs="{'invisible': ['|', '|', ('debit_origin_id', '!=', False),
('type', 'not in', ('out_invoice', 'in_invoice', 'out_refund', 'in_refund')), ('state', '!=', 'posted')]}"/>
</button>
<div class="oe_button_box" position="inside">
<button type="object" class="oe_stat_button" name="action_view_debit_notes" icon="fa-plus" attrs="{'invisible': [('debit_note_count', '=', 0)]}">
<div class="o_field_widget o_stat_info">
<span class="o_stat_value"><field name="debit_note_count"/></span>
<span class="o_stat_text">Debit Notes</span>
</div>
</button>
</div>
<field name="invoice_origin" position="after">
<field name="debit_origin_id" attrs="{'invisible': [('debit_origin_id', '=', False)]}"/>
</field>
</field>
</record>
</odoo>
@@ -0,0 +1,4 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from . import account_debit_note
@@ -0,0 +1,90 @@
# -*- coding: utf-8 -*-
from odoo import models, fields, api
from odoo.tools.translate import _
from odoo.exceptions import UserError
class AccountDebitNote(models.TransientModel):
"""
Add Debit Note wizard: when you want to correct an invoice with a positive amount.
Opposite of a Credit Note, but different from a regular invoice as you need the link to the original invoice.
In some cases, also used to cancel Credit Notes
"""
_name = 'account.debit.note'
_description = 'Add Debit Note wizard'
move_ids = fields.Many2many('account.move', 'account_move_debit_move', 'debit_id', 'move_id',
domain=[('state', '=', 'posted')])
date = fields.Date(string='Debit Note Date', default=fields.Date.context_today, required=True)
reason = fields.Char(string='Reason')
journal_id = fields.Many2one('account.journal', string='Use Specific Journal',
help='If empty, uses the journal of the journal entry to be debited.')
copy_lines = fields.Boolean("Copy Lines",
help="In case you need to do corrections for every line, it can be in handy to copy them. "
"We won't copy them for debit notes from credit notes. ")
# computed fields
move_type = fields.Char(compute="_compute_from_moves")
journal_type = fields.Char(compute="_compute_from_moves")
@api.model
def default_get(self, fields):
res = super(AccountDebitNote, self).default_get(fields)
move_ids = self.env['account.move'].browse(self.env.context['active_ids']) if self.env.context.get('active_model') == 'account.move' else self.env['account.move']
if any(move.state != "posted" for move in move_ids):
raise UserError(_('You can only debit posted moves.'))
res['move_ids'] = [(6, 0, move_ids.ids)]
return res
@api.depends('move_ids')
def _compute_from_moves(self):
for record in self:
move_ids = record.move_ids
record.move_type = move_ids[0].type if len(move_ids) == 1 or not any(m.type != move_ids[0].type for m in move_ids) else False
record.journal_type = record.move_type in ['in_refund', 'in_invoice'] and 'purchase' or 'sale'
def _prepare_default_values(self, move):
if move.type in ('in_refund', 'out_refund'):
type = 'in_invoice' if move.type == 'in_refund' else 'out_invoice'
else:
type = move.type
default_values = {
'ref': '%s, %s' % (move.name, self.reason) if self.reason else move.name,
'date': self.date or move.date,
'invoice_date': move.is_invoice(include_receipts=True) and (self.date or move.date) or False,
'journal_id': self.journal_id and self.journal_id.id or move.journal_id.id,
'invoice_payment_term_id': None,
'debit_origin_id': move.id,
'type': type,
}
if not self.copy_lines or move.type in [('in_refund', 'out_refund')]:
default_values['line_ids'] = [(5, 0, 0)]
return default_values
def create_debit(self):
self.ensure_one()
new_moves = self.env['account.move']
for move in self.move_ids.with_context(include_business_fields=True): #copy sale/purchase links
default_values = self._prepare_default_values(move)
new_move = move.with_context(internal_type='debit_note').copy(default=default_values) # Context key is used for l10n_latam_invoice_document for ar/cl/pe
move_msg = _(
"This debit note was created from:") + " <a href=# data-oe-model=account.move data-oe-id=%d>%s</a>" % (
move.id, move.name)
new_move.message_post(body=move_msg)
new_moves |= new_move
action = {
'name': _('Debit Notes'),
'type': 'ir.actions.act_window',
'res_model': 'account.move',
}
if len(new_moves) == 1:
action.update({
'view_mode': 'form',
'res_id': new_moves.id,
})
else:
action.update({
'view_mode': 'tree,form',
'domain': [('id', 'in', new_moves.ids)],
})
return action
@@ -0,0 +1,40 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<data>
<record id="view_account_debit_note" model="ir.ui.view">
<field name="name">account.debit.note.form</field>
<field name="model">account.debit.note</field>
<field name="arch" type="xml">
<form string="Create Debit Note">
<field name="move_type" invisible="1"/>
<field name="journal_type" invisible="1"/>
<field name="move_ids" invisible="1"/>
<group>
<group>
<field name="reason"/>
<field name="date" string="Debit Note Date"/>
<field name="copy_lines" attrs="{'invisible': [('move_type', 'in', ['in_refund', 'out_refund'])]}"/>
</group>
<group>
<field name="journal_id" domain="[('type', '=', journal_type)]"/>
</group>
</group>
<footer>
<button string='Create Debit Note' name="create_debit" type="object" class="btn-primary"/>
<button string="Cancel" class="btn-secondary" special="cancel"/>
</footer>
</form>
</field>
</record>
<record id="action_view_account_move_debit" model="ir.actions.act_window">
<field name="name">Create Debit Note</field>
<field name="res_model">account.debit.note</field>
<field name="view_mode">tree,form</field>
<field name="view_id" ref="view_account_debit_note"/>
<field name="target">new</field>
<field name="binding_model_id" ref="account.model_account_move" />
<field name="binding_view_types">list</field>
</record>
</data>
</odoo>