feat(fusion_accounting_documents): Documents app <-> invoice bridge
Replaces Enterprise's documents_account with a Fusion-native bridge. When a PDF/image lands in the Documents app, users can convert it into a draft vendor bill via a wizard that copies the document's binary onto the new account.move and posts a chatter note linking back to the source document. Adds: - documents.document.move_id (Many2one to the linked invoice) - documents.document.is_invoice_candidate (computed; True for unlinked PDF/image binaries) - documents.document.action_create_invoice() opens the wizard - account.move.source_document_ids reverse linkage + statinfo button - fusion.create.invoice.from.document.wizard (TransientModel + form) - ir.actions.server bound to documents.document so the workflow appears in the kanban/list Actions menu (the Documents app has no regular form view to inherit from in v19) The wizard: - defaults to the company's first purchase journal - supports vendor bill or vendor credit note - copies the source attachment onto the new move - posts a chatter note linking back - marks the document linked so it stops appearing as a candidate Auto-installs when documents + fusion_accounting_core are both present. 8 unit tests cover the candidate flag, wizard happy path, attachment copy, reverse linkage, already-linked guard, non-PDF guard, and credit-note creation. Made-with: Cursor
This commit is contained in:
2
fusion_accounting_documents/__init__.py
Normal file
2
fusion_accounting_documents/__init__.py
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
from . import models
|
||||||
|
from . import wizards
|
||||||
48
fusion_accounting_documents/__manifest__.py
Normal file
48
fusion_accounting_documents/__manifest__.py
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
{
|
||||||
|
'name': 'Fusion Accounting — Documents Bridge',
|
||||||
|
'version': '19.0.1.0.0',
|
||||||
|
'category': 'Accounting/Accounting',
|
||||||
|
'summary': 'Bridges the Documents app to Accounting: route scanned bills into vendor invoices.',
|
||||||
|
'description': """
|
||||||
|
Fusion Accounting — Documents Bridge
|
||||||
|
====================================
|
||||||
|
|
||||||
|
A Fusion-native replacement for Enterprise's ``documents_account`` module.
|
||||||
|
|
||||||
|
Adds:
|
||||||
|
|
||||||
|
- ``documents.document.move_id`` — Many2one to the linked accounting move.
|
||||||
|
- ``documents.document.is_invoice_candidate`` — computed flag for PDFs/images
|
||||||
|
not yet linked to a move.
|
||||||
|
- ``documents.document.action_create_invoice()`` — opens a wizard that
|
||||||
|
creates a draft vendor bill and copies the document's binary as an
|
||||||
|
attachment on the new ``account.move``.
|
||||||
|
- ``account.move.source_document_ids`` — reverse linkage with a stat button
|
||||||
|
on the invoice form.
|
||||||
|
- A ``fusion.create.invoice.from.document.wizard`` model + form view.
|
||||||
|
- A server action bound to ``documents.document`` so the workflow is
|
||||||
|
reachable from the Documents Actions menu (the Documents app uses
|
||||||
|
kanban/list views without a regular form view to inherit from).
|
||||||
|
|
||||||
|
Auto-installs when ``documents`` and ``fusion_accounting_core`` are both
|
||||||
|
present.
|
||||||
|
""",
|
||||||
|
'author': 'Nexa Systems Inc.',
|
||||||
|
'license': 'LGPL-3',
|
||||||
|
'depends': [
|
||||||
|
'fusion_accounting_core',
|
||||||
|
'account',
|
||||||
|
'documents',
|
||||||
|
],
|
||||||
|
'data': [
|
||||||
|
'security/ir.model.access.csv',
|
||||||
|
'wizards/create_invoice_from_document_views.xml',
|
||||||
|
'views/documents_document_views.xml',
|
||||||
|
'views/account_move_views.xml',
|
||||||
|
'data/server_actions_data.xml',
|
||||||
|
],
|
||||||
|
'auto_install': ['documents', 'fusion_accounting_core'],
|
||||||
|
'installable': True,
|
||||||
|
'application': False,
|
||||||
|
'icon': '/fusion_accounting_documents/static/description/icon.png',
|
||||||
|
}
|
||||||
25
fusion_accounting_documents/data/server_actions_data.xml
Normal file
25
fusion_accounting_documents/data/server_actions_data.xml
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<odoo>
|
||||||
|
<!--
|
||||||
|
Server action bound to documents.document so the
|
||||||
|
"Create Vendor Invoice" workflow appears in the cog/Actions
|
||||||
|
menu of the Documents kanban + list views.
|
||||||
|
|
||||||
|
We dispatch through ``action_create_invoice`` so the same
|
||||||
|
validation runs whether the user clicks the action or calls
|
||||||
|
the method programmatically.
|
||||||
|
-->
|
||||||
|
<record id="action_create_invoice_from_document" model="ir.actions.server">
|
||||||
|
<field name="name">Create Vendor Invoice (Fusion)</field>
|
||||||
|
<field name="model_id" ref="documents.model_documents_document"/>
|
||||||
|
<field name="binding_model_id" ref="documents.model_documents_document"/>
|
||||||
|
<field name="binding_view_types">list,kanban</field>
|
||||||
|
<field name="state">code</field>
|
||||||
|
<field name="code">
|
||||||
|
if records and len(records) == 1:
|
||||||
|
action = records.action_create_invoice()
|
||||||
|
else:
|
||||||
|
raise UserError(_("Select exactly one document to convert into a vendor invoice."))
|
||||||
|
</field>
|
||||||
|
</record>
|
||||||
|
</odoo>
|
||||||
2
fusion_accounting_documents/models/__init__.py
Normal file
2
fusion_accounting_documents/models/__init__.py
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
from . import documents_document
|
||||||
|
from . import account_move
|
||||||
33
fusion_accounting_documents/models/account_move.py
Normal file
33
fusion_accounting_documents/models/account_move.py
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
"""Reverse linkage from account.move back to source documents."""
|
||||||
|
|
||||||
|
from odoo import _, fields, models
|
||||||
|
|
||||||
|
|
||||||
|
class AccountMove(models.Model):
|
||||||
|
_inherit = 'account.move'
|
||||||
|
|
||||||
|
source_document_ids = fields.One2many(
|
||||||
|
'documents.document',
|
||||||
|
'move_id',
|
||||||
|
string='Source Documents',
|
||||||
|
readonly=True,
|
||||||
|
help="Documents in the Documents app that were used to create this move.",
|
||||||
|
)
|
||||||
|
source_document_count = fields.Integer(
|
||||||
|
string='Source Document Count',
|
||||||
|
compute='_compute_source_document_count',
|
||||||
|
)
|
||||||
|
|
||||||
|
def _compute_source_document_count(self):
|
||||||
|
for m in self:
|
||||||
|
m.source_document_count = len(m.source_document_ids)
|
||||||
|
|
||||||
|
def action_open_source_documents(self):
|
||||||
|
self.ensure_one()
|
||||||
|
return {
|
||||||
|
'type': 'ir.actions.act_window',
|
||||||
|
'name': _('Source Documents'),
|
||||||
|
'res_model': 'documents.document',
|
||||||
|
'view_mode': 'kanban,list',
|
||||||
|
'domain': [('move_id', '=', self.id)],
|
||||||
|
}
|
||||||
71
fusion_accounting_documents/models/documents_document.py
Normal file
71
fusion_accounting_documents/models/documents_document.py
Normal file
@@ -0,0 +1,71 @@
|
|||||||
|
"""Bridge documents.document to accounting moves.
|
||||||
|
|
||||||
|
Adds a Many2one link to the created invoice/move, a computed
|
||||||
|
``is_invoice_candidate`` flag for PDFs/images that have not yet been
|
||||||
|
turned into a vendor bill, and the ``action_create_invoice`` entry
|
||||||
|
point used by both the form button and the server action.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from odoo import _, api, fields, models
|
||||||
|
from odoo.exceptions import UserError
|
||||||
|
|
||||||
|
|
||||||
|
INVOICE_CANDIDATE_MIMETYPES = (
|
||||||
|
'application/pdf',
|
||||||
|
'image/png',
|
||||||
|
'image/jpeg',
|
||||||
|
'image/jpg',
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class DocumentsDocument(models.Model):
|
||||||
|
_inherit = 'documents.document'
|
||||||
|
|
||||||
|
move_id = fields.Many2one(
|
||||||
|
'account.move',
|
||||||
|
string='Linked Invoice/Move',
|
||||||
|
copy=False,
|
||||||
|
ondelete='set null',
|
||||||
|
help="The accounting move this document was used to create.",
|
||||||
|
)
|
||||||
|
is_invoice_candidate = fields.Boolean(
|
||||||
|
string='Is Invoice Candidate',
|
||||||
|
compute='_compute_is_invoice_candidate',
|
||||||
|
store=True,
|
||||||
|
help="True when this document looks like a vendor bill "
|
||||||
|
"(PDF/image binary) and has not yet been linked to a move.",
|
||||||
|
)
|
||||||
|
|
||||||
|
@api.depends('mimetype', 'type', 'move_id')
|
||||||
|
def _compute_is_invoice_candidate(self):
|
||||||
|
for d in self:
|
||||||
|
d.is_invoice_candidate = (
|
||||||
|
d.type == 'binary'
|
||||||
|
and (d.mimetype or '') in INVOICE_CANDIDATE_MIMETYPES
|
||||||
|
and not d.move_id
|
||||||
|
)
|
||||||
|
|
||||||
|
def action_create_invoice(self):
|
||||||
|
"""Open the wizard to create a vendor invoice from this document."""
|
||||||
|
self.ensure_one()
|
||||||
|
if self.move_id:
|
||||||
|
raise UserError(_(
|
||||||
|
"This document is already linked to invoice %s.",
|
||||||
|
self.move_id.display_name,
|
||||||
|
))
|
||||||
|
if self.type == 'folder':
|
||||||
|
raise UserError(_(
|
||||||
|
"Folders cannot be turned into invoices."
|
||||||
|
))
|
||||||
|
if (self.mimetype or '') not in INVOICE_CANDIDATE_MIMETYPES:
|
||||||
|
raise UserError(_(
|
||||||
|
"Only PDF or image documents can be turned into invoices."
|
||||||
|
))
|
||||||
|
return {
|
||||||
|
'type': 'ir.actions.act_window',
|
||||||
|
'name': _('Create Invoice from Document'),
|
||||||
|
'res_model': 'fusion.create.invoice.from.document.wizard',
|
||||||
|
'view_mode': 'form',
|
||||||
|
'target': 'new',
|
||||||
|
'context': {'default_document_id': self.id},
|
||||||
|
}
|
||||||
2
fusion_accounting_documents/security/ir.model.access.csv
Normal file
2
fusion_accounting_documents/security/ir.model.access.csv
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
|
||||||
|
access_fusion_create_invoice_wizard_user,fusion.create.invoice.wizard.user,model_fusion_create_invoice_from_document_wizard,base.group_user,1,1,1,1
|
||||||
|
BIN
fusion_accounting_documents/static/description/icon.png
Normal file
BIN
fusion_accounting_documents/static/description/icon.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 72 KiB |
1
fusion_accounting_documents/tests/__init__.py
Normal file
1
fusion_accounting_documents/tests/__init__.py
Normal file
@@ -0,0 +1 @@
|
|||||||
|
from . import test_document_to_invoice
|
||||||
140
fusion_accounting_documents/tests/test_document_to_invoice.py
Normal file
140
fusion_accounting_documents/tests/test_document_to_invoice.py
Normal file
@@ -0,0 +1,140 @@
|
|||||||
|
"""Tests for the documents.document <-> account.move bridge."""
|
||||||
|
|
||||||
|
import base64
|
||||||
|
|
||||||
|
from odoo.exceptions import UserError
|
||||||
|
from odoo.tests import tagged
|
||||||
|
from odoo.tests.common import TransactionCase
|
||||||
|
|
||||||
|
|
||||||
|
@tagged('post_install', '-at_install', 'fusion_accounting_documents')
|
||||||
|
class TestDocumentToInvoice(TransactionCase):
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def setUpClass(cls):
|
||||||
|
super().setUpClass()
|
||||||
|
cls.vendor = cls.env['res.partner'].create({
|
||||||
|
'name': 'Test Doc Vendor',
|
||||||
|
'supplier_rank': 1,
|
||||||
|
})
|
||||||
|
cls.purchase_journal = cls.env['account.journal'].search(
|
||||||
|
[('type', '=', 'purchase'),
|
||||||
|
('company_id', '=', cls.env.company.id)],
|
||||||
|
limit=1,
|
||||||
|
)
|
||||||
|
|
||||||
|
def _make_document(self, name='Test Bill PDF',
|
||||||
|
mimetype='application/pdf',
|
||||||
|
payload=b'%PDF-fake-bill-content'):
|
||||||
|
attachment = self.env['ir.attachment'].create({
|
||||||
|
'name': name,
|
||||||
|
'datas': base64.b64encode(payload),
|
||||||
|
'mimetype': mimetype,
|
||||||
|
})
|
||||||
|
Document = self.env['documents.document']
|
||||||
|
doc_vals = {
|
||||||
|
'name': name,
|
||||||
|
'attachment_id': attachment.id,
|
||||||
|
'mimetype': mimetype,
|
||||||
|
'type': 'binary',
|
||||||
|
}
|
||||||
|
if 'folder_id' in Document._fields:
|
||||||
|
folder = Document.search(
|
||||||
|
[('type', '=', 'folder')], limit=1,
|
||||||
|
)
|
||||||
|
if folder:
|
||||||
|
doc_vals['folder_id'] = folder.id
|
||||||
|
return Document.create(doc_vals)
|
||||||
|
|
||||||
|
def test_invoice_candidate_flag_pdf(self):
|
||||||
|
doc = self._make_document()
|
||||||
|
self.assertTrue(doc.is_invoice_candidate)
|
||||||
|
|
||||||
|
def test_invoice_candidate_flag_image(self):
|
||||||
|
doc = self._make_document(
|
||||||
|
name='scan.png',
|
||||||
|
mimetype='image/png',
|
||||||
|
payload=b'\x89PNG\r\n\x1a\nfake',
|
||||||
|
)
|
||||||
|
self.assertTrue(doc.is_invoice_candidate)
|
||||||
|
|
||||||
|
def test_invoice_candidate_flag_text_excluded(self):
|
||||||
|
doc = self._make_document(
|
||||||
|
name='note.txt',
|
||||||
|
mimetype='text/plain',
|
||||||
|
payload=b'just a note',
|
||||||
|
)
|
||||||
|
self.assertFalse(doc.is_invoice_candidate)
|
||||||
|
|
||||||
|
def test_action_create_invoice_opens_wizard(self):
|
||||||
|
doc = self._make_document()
|
||||||
|
action = doc.action_create_invoice()
|
||||||
|
self.assertEqual(action['type'], 'ir.actions.act_window')
|
||||||
|
self.assertEqual(
|
||||||
|
action['res_model'],
|
||||||
|
'fusion.create.invoice.from.document.wizard',
|
||||||
|
)
|
||||||
|
self.assertEqual(action['target'], 'new')
|
||||||
|
self.assertEqual(action['context']['default_document_id'], doc.id)
|
||||||
|
|
||||||
|
def test_wizard_creates_invoice_and_links(self):
|
||||||
|
doc = self._make_document()
|
||||||
|
wizard = self.env['fusion.create.invoice.from.document.wizard'].create({
|
||||||
|
'document_id': doc.id,
|
||||||
|
'partner_id': self.vendor.id,
|
||||||
|
'move_type': 'in_invoice',
|
||||||
|
})
|
||||||
|
self.assertTrue(wizard.journal_id, "Default journal should resolve")
|
||||||
|
action = wizard.action_create_invoice()
|
||||||
|
|
||||||
|
self.assertEqual(action['res_model'], 'account.move')
|
||||||
|
move = self.env['account.move'].browse(action['res_id'])
|
||||||
|
self.assertEqual(move.move_type, 'in_invoice')
|
||||||
|
self.assertEqual(move.partner_id, self.vendor)
|
||||||
|
|
||||||
|
self.assertEqual(doc.move_id, move)
|
||||||
|
self.assertFalse(doc.is_invoice_candidate,
|
||||||
|
"Linked docs should no longer be candidates")
|
||||||
|
|
||||||
|
self.assertEqual(move.source_document_count, 1)
|
||||||
|
self.assertIn(doc, move.source_document_ids)
|
||||||
|
|
||||||
|
attachments = self.env['ir.attachment'].search([
|
||||||
|
('res_model', '=', 'account.move'),
|
||||||
|
('res_id', '=', move.id),
|
||||||
|
])
|
||||||
|
self.assertTrue(
|
||||||
|
attachments,
|
||||||
|
"An attachment copy should land on the new move",
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_action_create_invoice_already_linked_raises(self):
|
||||||
|
doc = self._make_document()
|
||||||
|
existing_move = self.env['account.move'].create({
|
||||||
|
'move_type': 'in_invoice',
|
||||||
|
'partner_id': self.vendor.id,
|
||||||
|
})
|
||||||
|
doc.move_id = existing_move.id
|
||||||
|
with self.assertRaises(UserError):
|
||||||
|
doc.action_create_invoice()
|
||||||
|
|
||||||
|
def test_action_create_invoice_non_candidate_raises(self):
|
||||||
|
doc = self._make_document(
|
||||||
|
name='note.txt',
|
||||||
|
mimetype='text/plain',
|
||||||
|
payload=b'hello',
|
||||||
|
)
|
||||||
|
with self.assertRaises(UserError):
|
||||||
|
doc.action_create_invoice()
|
||||||
|
|
||||||
|
def test_wizard_creates_credit_note(self):
|
||||||
|
doc = self._make_document(name='credit-note.pdf')
|
||||||
|
wizard = self.env['fusion.create.invoice.from.document.wizard'].create({
|
||||||
|
'document_id': doc.id,
|
||||||
|
'partner_id': self.vendor.id,
|
||||||
|
'move_type': 'in_refund',
|
||||||
|
})
|
||||||
|
action = wizard.action_create_invoice()
|
||||||
|
move = self.env['account.move'].browse(action['res_id'])
|
||||||
|
self.assertEqual(move.move_type, 'in_refund')
|
||||||
|
self.assertEqual(doc.move_id, move)
|
||||||
21
fusion_accounting_documents/views/account_move_views.xml
Normal file
21
fusion_accounting_documents/views/account_move_views.xml
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<odoo>
|
||||||
|
<record id="view_move_form_inherit_fusion_documents" model="ir.ui.view">
|
||||||
|
<field name="name">account.move.form.inherit.fusion.documents</field>
|
||||||
|
<field name="model">account.move</field>
|
||||||
|
<field name="inherit_id" ref="account.view_move_form"/>
|
||||||
|
<field name="arch" type="xml">
|
||||||
|
<xpath expr="//div[@name='button_box']" position="inside">
|
||||||
|
<button class="oe_stat_button"
|
||||||
|
type="object"
|
||||||
|
name="action_open_source_documents"
|
||||||
|
icon="fa-file-text-o"
|
||||||
|
invisible="source_document_count == 0">
|
||||||
|
<field name="source_document_count"
|
||||||
|
widget="statinfo"
|
||||||
|
string="Source Docs"/>
|
||||||
|
</button>
|
||||||
|
</xpath>
|
||||||
|
</field>
|
||||||
|
</record>
|
||||||
|
</odoo>
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<odoo>
|
||||||
|
<!--
|
||||||
|
The Documents app does not ship a regular form view for
|
||||||
|
documents.document; editing happens in the side panel of the
|
||||||
|
kanban/list views. We therefore add the new fields to the
|
||||||
|
kanban + list views and rely on a server action (defined in
|
||||||
|
data/server_actions_data.xml) to expose the "Create Invoice"
|
||||||
|
workflow from the Actions menu.
|
||||||
|
-->
|
||||||
|
|
||||||
|
<record id="view_documents_document_kanban_inherit_fusion_acc"
|
||||||
|
model="ir.ui.view">
|
||||||
|
<field name="name">documents.document.kanban.inherit.fusion.acc</field>
|
||||||
|
<field name="model">documents.document</field>
|
||||||
|
<field name="inherit_id" ref="documents.document_view_kanban"/>
|
||||||
|
<field name="arch" type="xml">
|
||||||
|
<xpath expr="//field[@name='name']" position="after">
|
||||||
|
<field name="is_invoice_candidate"/>
|
||||||
|
<field name="move_id"/>
|
||||||
|
</xpath>
|
||||||
|
</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<record id="view_documents_document_list_inherit_fusion_acc"
|
||||||
|
model="ir.ui.view">
|
||||||
|
<field name="name">documents.document.list.inherit.fusion.acc</field>
|
||||||
|
<field name="model">documents.document</field>
|
||||||
|
<field name="inherit_id" ref="documents.documents_view_list_main"/>
|
||||||
|
<field name="arch" type="xml">
|
||||||
|
<xpath expr="//field[@name='name']" position="after">
|
||||||
|
<field name="is_invoice_candidate" optional="hide"/>
|
||||||
|
<field name="move_id"
|
||||||
|
string="Linked Invoice"
|
||||||
|
optional="hide"/>
|
||||||
|
</xpath>
|
||||||
|
</field>
|
||||||
|
</record>
|
||||||
|
</odoo>
|
||||||
1
fusion_accounting_documents/wizards/__init__.py
Normal file
1
fusion_accounting_documents/wizards/__init__.py
Normal file
@@ -0,0 +1 @@
|
|||||||
|
from . import create_invoice_from_document
|
||||||
@@ -0,0 +1,132 @@
|
|||||||
|
"""Wizard to create a vendor invoice from a Documents document.
|
||||||
|
|
||||||
|
The wizard creates an empty draft ``account.move`` of the chosen
|
||||||
|
move type, copies the document's binary attachment onto the new
|
||||||
|
move, posts a chatter note linking back to the source document,
|
||||||
|
and finally stores the move on ``documents.document.move_id`` so
|
||||||
|
the source no longer appears as an invoice candidate.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from odoo import _, api, fields, models
|
||||||
|
from odoo.exceptions import UserError
|
||||||
|
|
||||||
|
|
||||||
|
MOVE_TYPE_LABELS = {
|
||||||
|
'in_invoice': _('Vendor Bill'),
|
||||||
|
'in_refund': _('Vendor Credit Note'),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class CreateInvoiceFromDocumentWizard(models.TransientModel):
|
||||||
|
_name = 'fusion.create.invoice.from.document.wizard'
|
||||||
|
_description = 'Create Vendor Invoice from Document'
|
||||||
|
|
||||||
|
document_id = fields.Many2one(
|
||||||
|
'documents.document',
|
||||||
|
string='Source Document',
|
||||||
|
required=True,
|
||||||
|
readonly=True,
|
||||||
|
ondelete='cascade',
|
||||||
|
)
|
||||||
|
document_name = fields.Char(related='document_id.name', readonly=True)
|
||||||
|
document_mimetype = fields.Char(related='document_id.mimetype', readonly=True)
|
||||||
|
|
||||||
|
partner_id = fields.Many2one(
|
||||||
|
'res.partner',
|
||||||
|
string='Vendor',
|
||||||
|
domain="[('supplier_rank', '>', 0)]",
|
||||||
|
)
|
||||||
|
move_type = fields.Selection(
|
||||||
|
[
|
||||||
|
('in_invoice', 'Vendor Bill'),
|
||||||
|
('in_refund', 'Vendor Credit Note'),
|
||||||
|
],
|
||||||
|
string='Type',
|
||||||
|
default='in_invoice',
|
||||||
|
required=True,
|
||||||
|
)
|
||||||
|
company_id = fields.Many2one(
|
||||||
|
'res.company',
|
||||||
|
string='Company',
|
||||||
|
default=lambda self: self.env.company,
|
||||||
|
required=True,
|
||||||
|
)
|
||||||
|
journal_id = fields.Many2one(
|
||||||
|
'account.journal',
|
||||||
|
string='Journal',
|
||||||
|
domain="[('type', '=', 'purchase'), ('company_id', '=', company_id)]",
|
||||||
|
default=lambda self: self._default_journal(),
|
||||||
|
)
|
||||||
|
|
||||||
|
@api.model
|
||||||
|
def _default_journal(self):
|
||||||
|
return self.env['account.journal'].search(
|
||||||
|
[('type', '=', 'purchase'),
|
||||||
|
('company_id', '=', self.env.company.id)],
|
||||||
|
limit=1,
|
||||||
|
)
|
||||||
|
|
||||||
|
@api.onchange('company_id')
|
||||||
|
def _onchange_company_id(self):
|
||||||
|
if self.journal_id and self.journal_id.company_id != self.company_id:
|
||||||
|
self.journal_id = self.env['account.journal'].search(
|
||||||
|
[('type', '=', 'purchase'),
|
||||||
|
('company_id', '=', self.company_id.id)],
|
||||||
|
limit=1,
|
||||||
|
)
|
||||||
|
|
||||||
|
def action_create_invoice(self):
|
||||||
|
self.ensure_one()
|
||||||
|
if not self.document_id:
|
||||||
|
raise UserError(_("No document selected."))
|
||||||
|
if self.document_id.move_id:
|
||||||
|
raise UserError(_(
|
||||||
|
"Document %(doc)s is already linked to invoice %(inv)s.",
|
||||||
|
doc=self.document_id.display_name,
|
||||||
|
inv=self.document_id.move_id.display_name,
|
||||||
|
))
|
||||||
|
if not self.journal_id:
|
||||||
|
raise UserError(_(
|
||||||
|
"No purchase journal configured for company %s.",
|
||||||
|
self.company_id.display_name,
|
||||||
|
))
|
||||||
|
|
||||||
|
move_vals = {
|
||||||
|
'move_type': self.move_type,
|
||||||
|
'journal_id': self.journal_id.id,
|
||||||
|
'company_id': self.company_id.id,
|
||||||
|
}
|
||||||
|
if self.partner_id:
|
||||||
|
move_vals['partner_id'] = self.partner_id.id
|
||||||
|
|
||||||
|
move = self.env['account.move'].create(move_vals)
|
||||||
|
|
||||||
|
attachment = self.document_id.attachment_id
|
||||||
|
if attachment:
|
||||||
|
attachment_copy = attachment.copy({
|
||||||
|
'res_model': 'account.move',
|
||||||
|
'res_id': move.id,
|
||||||
|
})
|
||||||
|
move.message_post(
|
||||||
|
body=_(
|
||||||
|
"Created from Documents source: <strong>%s</strong>",
|
||||||
|
self.document_id.name,
|
||||||
|
),
|
||||||
|
attachment_ids=[attachment_copy.id],
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
move.message_post(body=_(
|
||||||
|
"Created from Documents source: <strong>%s</strong> "
|
||||||
|
"(no attachment to copy).",
|
||||||
|
self.document_id.name,
|
||||||
|
))
|
||||||
|
|
||||||
|
self.document_id.move_id = move.id
|
||||||
|
|
||||||
|
return {
|
||||||
|
'type': 'ir.actions.act_window',
|
||||||
|
'name': MOVE_TYPE_LABELS.get(self.move_type, _('Invoice')),
|
||||||
|
'res_model': 'account.move',
|
||||||
|
'view_mode': 'form',
|
||||||
|
'res_id': move.id,
|
||||||
|
}
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<odoo>
|
||||||
|
<record id="view_create_invoice_from_document_wizard_form" model="ir.ui.view">
|
||||||
|
<field name="name">fusion.create.invoice.from.document.wizard.form</field>
|
||||||
|
<field name="model">fusion.create.invoice.from.document.wizard</field>
|
||||||
|
<field name="arch" type="xml">
|
||||||
|
<form string="Create Invoice from Document">
|
||||||
|
<sheet>
|
||||||
|
<group>
|
||||||
|
<field name="document_id" invisible="1"/>
|
||||||
|
<field name="document_name" readonly="1"/>
|
||||||
|
<field name="document_mimetype" readonly="1"/>
|
||||||
|
</group>
|
||||||
|
<group>
|
||||||
|
<field name="move_type"/>
|
||||||
|
<field name="partner_id" options="{'no_create': True}"/>
|
||||||
|
<field name="company_id"
|
||||||
|
groups="base.group_multi_company"
|
||||||
|
options="{'no_create': True}"/>
|
||||||
|
<field name="journal_id" options="{'no_create': True}"/>
|
||||||
|
</group>
|
||||||
|
</sheet>
|
||||||
|
<footer>
|
||||||
|
<button name="action_create_invoice"
|
||||||
|
string="Create Invoice"
|
||||||
|
type="object"
|
||||||
|
class="btn-primary"
|
||||||
|
data-hotkey="q"/>
|
||||||
|
<button string="Cancel"
|
||||||
|
class="btn-secondary"
|
||||||
|
special="cancel"
|
||||||
|
data-hotkey="x"/>
|
||||||
|
</footer>
|
||||||
|
</form>
|
||||||
|
</field>
|
||||||
|
</record>
|
||||||
|
</odoo>
|
||||||
Reference in New Issue
Block a user