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
133 lines
4.2 KiB
Python
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,
|
|
}
|