199 lines
7.9 KiB
Python
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,
|
|
)
|