Files
Odoo-Modules/fusion_accounting_documents/wizards/create_invoice_from_document.py
gsinghpal 71f39c8d33 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
2026-04-20 00:34:50 -04:00

133 lines
4.2 KiB
Python

"""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,
}