summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorJosse Colpaert <jco@odoo.com>2019-12-12 14:25:34 +0000
committerJosse Colpaert <jco@odoo.com>2020-02-17 16:33:22 +0000
commit47eff65afdba3a2a8283342d8b636657f1fac317 (patch)
tree3ee74b04a78fa7fd051f9659d7c96008c4fe4e6d
parent8de07a8aa569cf6e21388d67c02344c8fdf0751e (diff)
[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>
-rw-r--r--addons/account_debit_note/__init__.py5
-rw-r--r--addons/account_debit_note/__manifest__.py23
-rw-r--r--addons/account_debit_note/i18n/account_debit_note.pot165
-rw-r--r--addons/account_debit_note/models/__init__.py4
-rw-r--r--addons/account_debit_note/models/account_move.py30
-rw-r--r--addons/account_debit_note/security/ir.model.access.csv2
-rw-r--r--addons/account_debit_note/tests/__init__.py3
-rw-r--r--addons/account_debit_note/tests/test_out_debit_note.py46
-rw-r--r--addons/account_debit_note/views/account_move_view.xml28
-rw-r--r--addons/account_debit_note/wizard/__init__.py4
-rw-r--r--addons/account_debit_note/wizard/account_debit_note.py90
-rw-r--r--addons/account_debit_note/wizard/account_debit_note_view.xml40
12 files changed, 440 insertions, 0 deletions
diff --git a/addons/account_debit_note/__init__.py b/addons/account_debit_note/__init__.py
new file mode 100644
index 00000000000..2ae6446f9dc
--- /dev/null
+++ b/addons/account_debit_note/__init__.py
@@ -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
diff --git a/addons/account_debit_note/__manifest__.py b/addons/account_debit_note/__manifest__.py
new file mode 100644
index 00000000000..07a739af121
--- /dev/null
+++ b/addons/account_debit_note/__manifest__.py
@@ -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,
+}
diff --git a/addons/account_debit_note/i18n/account_debit_note.pot b/addons/account_debit_note/i18n/account_debit_note.pot
new file mode 100644
index 00000000000..f07668e4af9
--- /dev/null
+++ b/addons/account_debit_note/i18n/account_debit_note.pot
@@ -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 ""
diff --git a/addons/account_debit_note/models/__init__.py b/addons/account_debit_note/models/__init__.py
new file mode 100644
index 00000000000..2184e77ab4a
--- /dev/null
+++ b/addons/account_debit_note/models/__init__.py
@@ -0,0 +1,4 @@
+# -*- coding: utf-8 -*-
+# Part of Odoo. See LICENSE file for full copyright and licensing details.
+
+from . import account_move \ No newline at end of file
diff --git a/addons/account_debit_note/models/account_move.py b/addons/account_debit_note/models/account_move.py
new file mode 100644
index 00000000000..75b5768da14
--- /dev/null
+++ b/addons/account_debit_note/models/account_move.py
@@ -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)],
+ }
diff --git a/addons/account_debit_note/security/ir.model.access.csv b/addons/account_debit_note/security/ir.model.access.csv
new file mode 100644
index 00000000000..2e015287121
--- /dev/null
+++ b/addons/account_debit_note/security/ir.model.access.csv
@@ -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
diff --git a/addons/account_debit_note/tests/__init__.py b/addons/account_debit_note/tests/__init__.py
new file mode 100644
index 00000000000..2e857acab4d
--- /dev/null
+++ b/addons/account_debit_note/tests/__init__.py
@@ -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 \ No newline at end of file
diff --git a/addons/account_debit_note/tests/test_out_debit_note.py b/addons/account_debit_note/tests/test_out_debit_note.py
new file mode 100644
index 00000000000..a184e3f65a2
--- /dev/null
+++ b/addons/account_debit_note/tests/test_out_debit_note.py
@@ -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') \ No newline at end of file
diff --git a/addons/account_debit_note/views/account_move_view.xml b/addons/account_debit_note/views/account_move_view.xml
new file mode 100644
index 00000000000..6deffec1c07
--- /dev/null
+++ b/addons/account_debit_note/views/account_move_view.xml
@@ -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>
diff --git a/addons/account_debit_note/wizard/__init__.py b/addons/account_debit_note/wizard/__init__.py
new file mode 100644
index 00000000000..2048c309b0f
--- /dev/null
+++ b/addons/account_debit_note/wizard/__init__.py
@@ -0,0 +1,4 @@
+# -*- coding: utf-8 -*-
+# Part of Odoo. See LICENSE file for full copyright and licensing details.
+
+from . import account_debit_note \ No newline at end of file
diff --git a/addons/account_debit_note/wizard/account_debit_note.py b/addons/account_debit_note/wizard/account_debit_note.py
new file mode 100644
index 00000000000..af553426c12
--- /dev/null
+++ b/addons/account_debit_note/wizard/account_debit_note.py
@@ -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
diff --git a/addons/account_debit_note/wizard/account_debit_note_view.xml b/addons/account_debit_note/wizard/account_debit_note_view.xml
new file mode 100644
index 00000000000..be21ca6a987
--- /dev/null
+++ b/addons/account_debit_note/wizard/account_debit_note_view.xml
@@ -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>