Odoo18-Base/addons/l10n_jo_edi/models/account_move.py
2025-01-06 10:57:38 +07:00

199 lines
7.9 KiB
Python

import base64
import requests
import uuid
from werkzeug.urls import url_encode
from odoo import _, api, fields, models
JOFOTARA_URL = "https://backend.jofotara.gov.jo/core/invoices/"
class AccountMove(models.Model):
_inherit = 'account.move'
l10n_jo_edi_uuid = fields.Char(string="Invoice UUID", copy=False, compute="_compute_l10n_jo_edi_uuid", store=True)
l10n_jo_edi_qr = fields.Char(string="QR", copy=False)
l10n_jo_edi_is_needed = fields.Boolean(
compute="_compute_l10n_jo_edi_is_needed",
help="Jordan: technical field to determine if this invoice is eligible to be e-invoiced.",
)
l10n_jo_edi_state = fields.Selection(
selection=[('to_send', 'To Send'), ('sent', 'Sent')],
string="JoFotara State",
copy=False)
l10n_jo_edi_error = fields.Text(
string="JoFotara Error",
copy=False,
readonly=True,
help="Jordan: Error details.",
)
l10n_jo_edi_xml_attachment_file = fields.Binary(
string="Jordan E-Invoice XML File",
copy=False,
attachment=True,
help="Jordan: technical field holding the e-invoice XML data.",
)
l10n_jo_edi_xml_attachment_id = fields.Many2one(
comodel_name="ir.attachment",
string="Jordan E-Invoice XML",
compute=lambda self: self._compute_linked_attachment_id(
"l10n_jo_edi_xml_attachment_id", "l10n_jo_edi_xml_attachment_file"
),
depends=["l10n_jo_edi_xml_attachment_file"],
help="Jordan: e-invoice XML.",
)
@api.depends("country_code", "move_type")
def _compute_l10n_jo_edi_is_needed(self):
for move in self:
move.l10n_jo_edi_is_needed = (
move.country_code == "JO"
and move.move_type in ("out_invoice", "out_refund")
)
@api.depends("l10n_jo_edi_state")
def _compute_show_reset_to_draft_button(self):
# EXTENDS 'account'
super()._compute_show_reset_to_draft_button()
self.filtered(lambda move: move.l10n_jo_edi_state == 'sent').show_reset_to_draft_button = False
@api.depends("l10n_jo_edi_is_needed")
def _compute_l10n_jo_edi_uuid(self):
for invoice in self:
if invoice.l10n_jo_edi_is_needed and not invoice.l10n_jo_edi_uuid:
invoice.l10n_jo_edi_uuid = uuid.uuid4()
def _l10n_jo_qr_code_src(self):
self.ensure_one()
encoded_params = url_encode({
'barcode_type': 'QR',
'value': self.l10n_jo_edi_qr,
'width': 200,
'height': 200,
})
return f'/report/barcode/?{encoded_params}'
def _is_sales_refund(self):
self.ensure_one()
return self.company_id.l10n_jo_edi_taxpayer_type == 'sales' and self.move_type == 'out_refund'
def button_draft(self):
# EXTENDS 'account'
self.write(
{
"l10n_jo_edi_error": False,
"l10n_jo_edi_state": False,
}
)
return super().button_draft()
def _post(self, soft=True):
# EXTENDS 'account'
for invoice in self.filtered('l10n_jo_edi_is_needed'):
invoice.l10n_jo_edi_state = 'to_send'
return super()._post(soft)
def _get_name_invoice_report(self):
# EXTENDS account
self.ensure_one()
if self.l10n_jo_edi_state == 'sent' and self.l10n_jo_edi_xml_attachment_id:
return 'l10n_jo_edi.report_invoice_document'
return super()._get_name_invoice_report()
def _l10n_jo_build_jofotara_headers(self):
self.ensure_one()
return {
'Client-Id': self.company_id.l10n_jo_edi_client_identifier,
'Secret-Key': self.company_id.l10n_jo_edi_secret_key,
}
def _submit_to_jofotara(self):
self.ensure_one()
headers = self._l10n_jo_build_jofotara_headers()
xml_invoice = self.env['account.edi.xml.ubl_21.jo']._export_invoice(self)[0]
params = {'invoice': base64.b64encode(xml_invoice).decode()}
try:
response = requests.post(JOFOTARA_URL, json=params, headers=headers, timeout=50)
except requests.exceptions.Timeout:
return _("Request time out! Please try again.")
except requests.exceptions.RequestException as e:
return _("Invalid request: %s", e)
if not response.ok:
return _("Request failed: %s", response.content.decode())
dict_response = response.json()
self.l10n_jo_edi_qr = str(dict_response.get('EINV_QR', ''))
self.env["ir.attachment"].create(
{
"res_model": "account.move",
"res_id": self.id,
"res_field": "l10n_jo_edi_xml_attachment_file",
"name": self._l10n_jo_edi_get_xml_attachment_name(),
"raw": xml_invoice,
}
)
def _l10n_jo_edi_get_xml_attachment_name(self):
return f"{self.name.replace('/', '_')}_edi.xml"
def _l10n_jo_validate_config(self):
error_msg = ''
if not self.company_id.l10n_jo_edi_client_identifier:
error_msg += _("Client ID is missing.\n")
if not self.company_id.l10n_jo_edi_secret_key:
error_msg += _("Secret key is missing.\n")
if not self.company_id.l10n_jo_edi_taxpayer_type:
error_msg += _("Taxpayer type is missing.\n")
if not self.company_id.l10n_jo_edi_sequence_income_source:
error_msg += _("Activity number (Sequence of income source) is missing.\n")
if error_msg:
return _("%s To set: Configuration > Settings > Electronic Invoicing (Jordan)", error_msg)
def _l10n_jo_validate_fields(self):
def has_non_digit_vat(partner, partner_type):
if partner.vat and not partner.vat.isdigit():
return _("JoFotara portal cannot process %s VAT with non-digit characters in it\n", partner_type)
return ""
error_msg = ''
customer = self.partner_id
error_msg += has_non_digit_vat(customer, 'customer')
supplier = self.company_id.partner_id.commercial_partner_id
error_msg += has_non_digit_vat(supplier, 'supplier')
if any(
line.display_type not in ('line_note', 'line_section')
and (line.quantity < 0 or line.price_unit < 0)
for line in self.invoice_line_ids
):
error_msg += _("JoFotara portal cannot process negative quantity nor negative price on invoice lines")
for line in self.invoice_line_ids.filtered(lambda line: line.display_type not in ('line_note', 'line_section')):
if self.company_id.l10n_jo_edi_taxpayer_type == 'income' and len(line.tax_ids) != 0:
error_msg += _("No taxes are allowed on invoice lines for taxpayers unregistered in the sales tax")
elif self.company_id.l10n_jo_edi_taxpayer_type == 'sales' and len(line.tax_ids) != 1:
error_msg += _("One general tax per invoice line is expected for taxpayers registered in the sales tax")
elif self.company_id.l10n_jo_edi_taxpayer_type == 'special' and len(line.tax_ids) != 2:
error_msg += _("One special and one general tax per invoice line is expected for taxpayers registered in the special tax")
return error_msg
def _l10n_jo_edi_send(self):
self.ensure_one()
if not self.env['res.company']._with_locked_records(records=self, allow_raising=False):
return
if error_message := self._l10n_jo_validate_config() or self._l10n_jo_validate_fields() or self._submit_to_jofotara():
self.l10n_jo_edi_error = error_message
return error_message
else:
self.l10n_jo_edi_error = False
self.l10n_jo_edi_state = 'sent'
self.with_context(no_new_invoice=True).message_post(
body=_("E-invoice (JoFotara) submitted successfully."),
attachment_ids=self.l10n_jo_edi_xml_attachment_id.ids,
)