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 "Debit Notes" +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 @@ + + + + account.move.form.debit + account.move + + + +
+ +
+ + + + +
+
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:") + " %s" % ( + 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 @@ + + + + + account.debit.note.form + account.debit.note + +
+ + + + + + + + + + + + + +
+
+ +
+
+ + + Create Debit Note + account.debit.note + tree,form + + new + + list + +
+