add odoo erp
This commit is contained in:
@@ -0,0 +1,2 @@
|
||||
from . import controllers
|
||||
from . import models
|
||||
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"name": "Account API (ARG)",
|
||||
"version": "18.0.1.0.3",
|
||||
"author": "Valentin Romero",
|
||||
"website": "",
|
||||
"license": "AGPL-3",
|
||||
"depends": [
|
||||
"base",
|
||||
"l10n_ar",
|
||||
],
|
||||
"category": "Tools",
|
||||
"data": [
|
||||
"data/ir_config_parameter.xml"
|
||||
],
|
||||
"installable": True,
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
from . import main
|
||||
@@ -0,0 +1,548 @@
|
||||
import json
|
||||
import base64
|
||||
import logging
|
||||
import requests
|
||||
import datetime
|
||||
import traceback
|
||||
from odoo import http
|
||||
from odoo.http import request
|
||||
from dateutil.relativedelta import relativedelta
|
||||
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class AccountMoveController(http.Controller):
|
||||
|
||||
@http.route(
|
||||
"/account/move/create", type="json", auth="public", methods=["POST"], csrf=False
|
||||
)
|
||||
def create_account_move(self, **kwargs):
|
||||
try:
|
||||
data = json.loads(request.httprequest.get_data())
|
||||
if not data:
|
||||
return {
|
||||
"success": False,
|
||||
"error": "No se recibieron datos en la petición",
|
||||
}
|
||||
|
||||
company = (
|
||||
request.env["res.company"]
|
||||
.sudo()
|
||||
.search([("active", "=", True)], limit=1)
|
||||
)
|
||||
if not company:
|
||||
return {"success": False, "error": "No hay ninguna compañía activa."}
|
||||
|
||||
journal = (
|
||||
request.env["account.journal"]
|
||||
.sudo()
|
||||
.search(
|
||||
[("type", "=", "sale"), ("company_id", "=", company.id)], limit=1
|
||||
)
|
||||
)
|
||||
if not journal:
|
||||
return {
|
||||
"success": False,
|
||||
"error": "No se encontró un diario de ventas en la compañía activa.",
|
||||
}
|
||||
|
||||
data_user = data.get("user")
|
||||
data_product = data.get("product")
|
||||
if not data_user or not data_product:
|
||||
return {
|
||||
"success": False,
|
||||
"error": "Faltan datos de usuario o producto en la petición",
|
||||
}
|
||||
|
||||
user_external_id = data_user.get("id")
|
||||
user_dni = data_user.get("dni")
|
||||
data_product_description = data_product.get("description")
|
||||
data_product_amount = data_product.get("amount", 0)
|
||||
data_product_session_id = data_product.get("sessionId")
|
||||
data_external_invoice_id = data_product.get("invoiceId")
|
||||
if (
|
||||
not user_external_id
|
||||
or not data_product_description
|
||||
or not data_external_invoice_id
|
||||
):
|
||||
return {
|
||||
"success": False,
|
||||
"error": "Faltan datos necesarios en la petición",
|
||||
}
|
||||
|
||||
partner_id = (
|
||||
request.env["res.partner"]
|
||||
.sudo()
|
||||
.search(
|
||||
[
|
||||
("vat", "=", user_dni),
|
||||
],
|
||||
limit=1,
|
||||
)
|
||||
)
|
||||
|
||||
if not partner_id:
|
||||
partner_vals = {
|
||||
"name": f"Client {user_external_id}",
|
||||
"vat": user_dni,
|
||||
"ref": user_external_id,
|
||||
"company_id": company.id,
|
||||
"l10n_ar_afip_responsibility_type_id": request.env.ref("l10n_ar.res_CF").id,
|
||||
"l10n_latam_identification_type_id": request.env.ref("l10n_ar.it_dni").id
|
||||
}
|
||||
partner_id = request.env["res.partner"].sudo().create(partner_vals)
|
||||
|
||||
product_id = (
|
||||
request.env["product.product"]
|
||||
.sudo()
|
||||
.search(
|
||||
[
|
||||
("default_code", "=", data_product_session_id)
|
||||
],
|
||||
limit=1,
|
||||
)
|
||||
)
|
||||
|
||||
if not product_id:
|
||||
default_tax_id = company.account_sale_tax_id
|
||||
product_vals = {
|
||||
"name": data_product_description,
|
||||
"default_code": data_product_session_id,
|
||||
"list_price": data_product_amount,
|
||||
"company_id": company.id,
|
||||
"type": "service",
|
||||
"taxes_id": [(6, 0, [default_tax_id.id])] if default_tax_id else [],
|
||||
}
|
||||
product_tmpl_id = request.env["product.template"].sudo().create(product_vals)
|
||||
product_id = product_tmpl_id.product_variant_id
|
||||
|
||||
today = datetime.date.today()
|
||||
first_day = today.replace(day=1)
|
||||
next_month_first = first_day + relativedelta(months=1)
|
||||
date_value = datetime.datetime.now().strftime("%Y-%m-%d")
|
||||
vals = {
|
||||
"partner_id": partner_id.id,
|
||||
"company_id": company.id,
|
||||
"journal_id": journal.id,
|
||||
"move_type": "out_invoice",
|
||||
"date": date_value,
|
||||
"invoice_date": date_value,
|
||||
"l10n_ar_afip_service_start": first_day.strftime("%Y-%m-%d"),
|
||||
"l10n_ar_afip_service_end": next_month_first.strftime("%Y-%m-%d"),
|
||||
"ref": data_external_invoice_id,
|
||||
"invoice_line_ids": [
|
||||
(
|
||||
0,
|
||||
0,
|
||||
{
|
||||
"product_id": product_id.id,
|
||||
"quantity": 1.0,
|
||||
"price_unit": float(data_product_amount),
|
||||
"name": data_product_description,
|
||||
},
|
||||
)
|
||||
],
|
||||
}
|
||||
|
||||
account_move = request.env["account.move"].sudo().create(vals)
|
||||
|
||||
account_move.action_post()
|
||||
account_move.sudo()._portal_ensure_token()
|
||||
base_url = request.httprequest.host_url.rstrip("/")
|
||||
share_url = "{}/my/invoices/{}?access_token={}".format(
|
||||
base_url,
|
||||
account_move.id,
|
||||
account_move.access_token,
|
||||
)
|
||||
|
||||
request.env.cr.commit()
|
||||
|
||||
pdf_response = requests.get(f"{share_url}&report_type=pdf&download=true", timeout=30)
|
||||
pdf_base64 = base64.b64encode(pdf_response.content)
|
||||
|
||||
return {
|
||||
"invoiceId": data_external_invoice_id,
|
||||
"base64Invoice": pdf_base64,
|
||||
"linkPayment": "url_de_mercadopago_o_payu",
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
# Log del error
|
||||
_logger.error("Error al crear account.move: %s", str(e))
|
||||
|
||||
# Rollback en caso de error
|
||||
request.env.cr.rollback()
|
||||
|
||||
return {"success": False, "error": str(e)}
|
||||
|
||||
@http.route(
|
||||
"/account/move/status", type="json", auth="public", methods=["GET"], csrf=False
|
||||
)
|
||||
def get_account_move_status(self, **kwargs):
|
||||
try:
|
||||
data = json.loads(request.httprequest.get_data())
|
||||
if not data:
|
||||
return {
|
||||
"success": False,
|
||||
"error": "No se recibieron datos en la petición",
|
||||
}
|
||||
|
||||
data_external_invoice_id = data.get("invoiceId")
|
||||
if not data_external_invoice_id:
|
||||
return {
|
||||
"success": False,
|
||||
"error": "Falta el ID de la factura en la petición",
|
||||
}
|
||||
|
||||
account_move = (
|
||||
request.env["account.move"]
|
||||
.sudo()
|
||||
.search([("ref", "=", data_external_invoice_id)], limit=1)
|
||||
)
|
||||
|
||||
if not account_move:
|
||||
return {
|
||||
"success": False,
|
||||
"error": "No se encontró ninguna factura con el ID proporcionado.",
|
||||
}
|
||||
|
||||
states = {
|
||||
"not_paid": "PENDING",
|
||||
"paid": "PAID",
|
||||
"cancel": "CANCELED",
|
||||
}
|
||||
status = states.get(account_move.payment_state, "UNKNOWN")
|
||||
return {
|
||||
"invoiceId": data_external_invoice_id,
|
||||
"status": status,
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
# Log del error
|
||||
_logger.error("Error al obtener el estado de account.move: %s", str(e))
|
||||
|
||||
return {"success": False, "error": str(e)}
|
||||
|
||||
@http.route(
|
||||
"/account/move/cancel", type="json", auth="public", methods=["POST"], csrf=False
|
||||
)
|
||||
def cancel_account_move(self, **kwargs):
|
||||
try:
|
||||
data = json.loads(request.httprequest.get_data())
|
||||
if not data:
|
||||
return {"success": False, "error": "No se recibieron datos en la petición"}
|
||||
|
||||
data_external_invoice_id = data.get("invoiceId")
|
||||
if not data_external_invoice_id:
|
||||
return {"success": False, "error": "Falta el ID de la factura en la petición"}
|
||||
|
||||
account_move = (
|
||||
request.env["account.move"]
|
||||
.sudo()
|
||||
.search([("ref", "=", data_external_invoice_id)], limit=1)
|
||||
)
|
||||
|
||||
if not account_move:
|
||||
return {"success": False, "error": "No se encontró ninguna factura con el ID proporcionado."}
|
||||
|
||||
# Si la factura está en estado 'posted', crear nota de crédito automáticamente
|
||||
# (esto prevalece sobre la comprobación de AFIP)
|
||||
force_refund = account_move.state == "posted"
|
||||
|
||||
# Determinar si la factura fue publicada en AFIP.
|
||||
# En este entorno hay módulos que usan los campos `afip_auth_code` / `afip_auth_mode`
|
||||
# (por ejemplo integraciones con pyafipws). Si existe un código de autorización,
|
||||
# consideramos que ya fue publicada.
|
||||
published_in_afip = False
|
||||
try:
|
||||
published_in_afip = bool(getattr(account_move, "afip_auth_code", False))
|
||||
except Exception:
|
||||
published_in_afip = False
|
||||
|
||||
# Si no está publicada y no está 'posted', cancelamos directamente
|
||||
if not published_in_afip and not force_refund:
|
||||
# Use el método estándar para cancelar (button_cancel está presente en account.move)
|
||||
account_move.sudo().button_cancel()
|
||||
request.env.cr.commit()
|
||||
return {"invoiceId": data_external_invoice_id, "status": "CANCELED", "success": True}
|
||||
|
||||
# Si está publicada en AFIP, crear nota de crédito (out_refund) con motivo 'Devolucion'
|
||||
# Reutilizamos las líneas de la factura original
|
||||
# Si ya existe una nota de crédito asociada a esta factura, devolverla en vez de crear otra
|
||||
try:
|
||||
existing_refunds = account_move.reversal_move_ids.filtered(
|
||||
lambda m: m.move_type in ("out_refund", "in_refund") and m.state != "cancel"
|
||||
)
|
||||
except Exception:
|
||||
existing_refunds = account_move.reversal_move_ids.filtered(
|
||||
lambda m: m.move_type in ("out_refund", "in_refund")
|
||||
)
|
||||
|
||||
if existing_refunds:
|
||||
# Use the most recent refund (by id)
|
||||
credit_move = existing_refunds.sorted(lambda r: r.id, reverse=True)[0]
|
||||
try:
|
||||
credit_move.sudo()._portal_ensure_token()
|
||||
except Exception:
|
||||
pass
|
||||
request.env.cr.commit()
|
||||
base_url = request.httprequest.host_url.rstrip("/")
|
||||
share_url = "{}/my/invoices/{}?access_token={}".format(
|
||||
base_url, credit_move.id, credit_move.access_token
|
||||
)
|
||||
try:
|
||||
pdf_response = requests.get(f"{share_url}&report_type=pdf&download=true", timeout=30)
|
||||
pdf_base64 = base64.b64encode(pdf_response.content)
|
||||
except Exception:
|
||||
pdf_base64 = False
|
||||
|
||||
return {
|
||||
"invoiceId": data_external_invoice_id,
|
||||
"status": "REFUNDED",
|
||||
"creditInvoiceRef": credit_move.ref or (credit_move.name or f"{data_external_invoice_id}-NC"),
|
||||
"creditInvoiceId": credit_move.id,
|
||||
"base64Invoice": pdf_base64,
|
||||
"success": True,
|
||||
}
|
||||
|
||||
invoice_lines = []
|
||||
for line in account_move.invoice_line_ids:
|
||||
tax_ids = line.tax_ids.ids if hasattr(line, "tax_ids") else []
|
||||
invoice_lines.append(
|
||||
(
|
||||
0,
|
||||
0,
|
||||
{
|
||||
"product_id": line.product_id.id if line.product_id else False,
|
||||
"name": line.name,
|
||||
"quantity": float(line.quantity),
|
||||
"price_unit": float(line.price_unit),
|
||||
"tax_ids": [(6, 0, tax_ids)],
|
||||
"account_id": line.account_id.id if line.account_id else False,
|
||||
},
|
||||
)
|
||||
)
|
||||
|
||||
today = datetime.datetime.now().strftime("%Y-%m-%d")
|
||||
refund_ref = f"{data_external_invoice_id}-NC"
|
||||
|
||||
# Prepare default values for reversal so Odoo sets reversed_entry_id and handles reconciliation
|
||||
default_vals = {
|
||||
"ref": refund_ref,
|
||||
"invoice_origin": account_move.name or account_move.l10n_latam_document_number or account_move.ref,
|
||||
"date": today,
|
||||
"invoice_date": today,
|
||||
# AFIP related fields: repetir periodo de servicio si existían
|
||||
"l10n_ar_afip_service_start": getattr(account_move, "l10n_ar_afip_service_start", False),
|
||||
"l10n_ar_afip_service_end": getattr(account_move, "l10n_ar_afip_service_end", False),
|
||||
}
|
||||
|
||||
# Determine reverse move_type using a safe local map (mirrors core TYPE_REVERSE_MAP)
|
||||
reverse_map = {
|
||||
"entry": "entry",
|
||||
"out_invoice": "out_refund",
|
||||
"out_refund": "out_invoice",
|
||||
"in_invoice": "in_refund",
|
||||
"in_refund": "in_invoice",
|
||||
"out_receipt": "out_refund",
|
||||
"in_receipt": "in_refund",
|
||||
}
|
||||
reverse_move_type = reverse_map.get(account_move.move_type, "out_refund")
|
||||
default_vals["move_type"] = reverse_move_type
|
||||
|
||||
# Prefer a l10n_latam document type from the invoice's available types filtered for credit_note
|
||||
try:
|
||||
# account_move.l10n_latam_available_document_type_ids is computed and should contain valid candidates
|
||||
candidates = account_move.l10n_latam_available_document_type_ids.filtered(
|
||||
lambda d: d.internal_type == "credit_note"
|
||||
)
|
||||
if candidates:
|
||||
default_vals["l10n_latam_document_type_id"] = candidates[0].id
|
||||
else:
|
||||
# Fallback: search globally by country of the company
|
||||
country = account_move.company_id.account_fiscal_country_id and account_move.company_id.account_fiscal_country_id.id or False
|
||||
if country:
|
||||
doc_type = request.env["l10n_latam.document.type"].sudo().search([
|
||||
("internal_type", "=", "credit_note"),
|
||||
("country_id", "=", country),
|
||||
], limit=1)
|
||||
if doc_type:
|
||||
default_vals["l10n_latam_document_type_id"] = doc_type.id
|
||||
except Exception:
|
||||
# If localization not available or any error, continue without setting the document type
|
||||
pass
|
||||
|
||||
# Prefer using the same journal as the original invoice for the reversal
|
||||
try:
|
||||
if account_move.journal_id:
|
||||
default_vals["journal_id"] = account_move.journal_id.id
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# Use the core _reverse_moves to create a proper reversal (sets reversed_entry_id and posts/reconciles if cancel=True)
|
||||
# Run reversal using the invoice's company context to avoid multi-company singleton issues
|
||||
try:
|
||||
credit_moves = account_move.sudo().with_company(account_move.company_id)._reverse_moves(default_values_list=[default_vals], cancel=True)
|
||||
except Exception:
|
||||
# Fallback to calling without explicit company if something goes wrong
|
||||
credit_moves = account_move.sudo()._reverse_moves(default_values_list=[default_vals], cancel=True)
|
||||
# _reverse_moves returns the created reverse moves; we're dealing with a single invoice
|
||||
credit_move = credit_moves and credit_moves[0] or None
|
||||
if credit_move:
|
||||
try:
|
||||
credit_move.sudo().with_company(credit_move.company_id)._portal_ensure_token()
|
||||
except Exception:
|
||||
credit_move.sudo()._portal_ensure_token()
|
||||
# Commit antes de intentar obtener el PDF
|
||||
request.env.cr.commit()
|
||||
|
||||
base_url = request.httprequest.host_url.rstrip("/")
|
||||
share_url = "{}/my/invoices/{}?access_token={}".format(
|
||||
base_url, credit_move.id, credit_move.access_token
|
||||
)
|
||||
|
||||
# Obtener PDF de la nota de crédito
|
||||
try:
|
||||
pdf_response = requests.get(f"{share_url}&report_type=pdf&download=true", timeout=30)
|
||||
pdf_base64 = base64.b64encode(pdf_response.content)
|
||||
except Exception:
|
||||
pdf_base64 = False
|
||||
|
||||
return {
|
||||
"invoiceId": data_external_invoice_id,
|
||||
"status": "REFUNDED",
|
||||
"creditInvoiceRef": refund_ref,
|
||||
"creditInvoiceId": credit_move.id,
|
||||
"base64Invoice": pdf_base64,
|
||||
"success": True,
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
# Log full traceback to help identify the source of 'Expected singleton' errors
|
||||
_logger.error("Error al cancelar account.move: %s", str(e))
|
||||
_logger.error(traceback.format_exc())
|
||||
request.env.cr.rollback()
|
||||
return {"success": False, "error": str(e)}
|
||||
|
||||
@http.route(
|
||||
"/account/move/paid", type="json", auth="public", methods=["POST"], csrf=False
|
||||
)
|
||||
def pay_account_move(self, **kwargs):
|
||||
try:
|
||||
data = json.loads(request.httprequest.get_data())
|
||||
if not data:
|
||||
return {"success": False, "error": "No se recibieron datos en la petición"}
|
||||
|
||||
data_external_invoice_id = data.get("invoiceId")
|
||||
amount = data.get("amount")
|
||||
|
||||
if not data_external_invoice_id:
|
||||
return {"success": False, "error": "Falta el ID de la factura en la petición"}
|
||||
|
||||
account_move = (
|
||||
request.env["account.move"].sudo().search([("ref", "=", data_external_invoice_id)], limit=1)
|
||||
)
|
||||
|
||||
if not account_move:
|
||||
return {"success": False, "error": "No se encontró ninguna factura con el ID proporcionado."}
|
||||
|
||||
# Only allow payments on posted invoices
|
||||
if account_move.state != "posted":
|
||||
return {"success": False, "error": "Solo se pueden pagar facturas en estado 'posted'."}
|
||||
|
||||
# If the invoice was reversed/cancelled, don't allow payment
|
||||
if account_move.reversal_move_ids:
|
||||
return {"success": False, "error": "La factura ya tiene una nota de crédito asociada (revertida). No se puede pagar."}
|
||||
|
||||
# If already fully paid, return status
|
||||
if account_move.payment_state == "paid":
|
||||
return {
|
||||
"invoiceId": data_external_invoice_id,
|
||||
"status": "PAID",
|
||||
"paid": True,
|
||||
"paymentIds": [p.id for p in account_move.payment_id | account_move.reconciled_payment_ids | account_move.matched_payment_ids],
|
||||
"success": True,
|
||||
}
|
||||
|
||||
# Determine amount to pay: use provided amount or remaining residual
|
||||
try:
|
||||
if amount is None:
|
||||
# amount_residual is in company currency
|
||||
amount_to_pay = float(account_move.amount_residual)
|
||||
else:
|
||||
amount_to_pay = float(amount)
|
||||
except Exception:
|
||||
return {"success": False, "error": "El campo amount debe ser numérico."}
|
||||
|
||||
# Use the register payment wizard to create/post/reconcile the payment properly
|
||||
company = account_move.company_id
|
||||
try:
|
||||
PaymentRegisterModel = (
|
||||
request.env["account.payment.register"].sudo().with_company(company).with_context(
|
||||
active_model="account.move",
|
||||
active_ids=[account_move.id],
|
||||
skip_invoice_sync=True,
|
||||
company_id=company.id,
|
||||
)
|
||||
)
|
||||
|
||||
# Create wizard record (default_get will prepare line_ids etc.) in the company's context
|
||||
wizard = PaymentRegisterModel.create({})
|
||||
|
||||
# Set custom amount (wizard.amount is a compute editable field)
|
||||
try:
|
||||
wizard.amount = amount_to_pay
|
||||
except Exception:
|
||||
# If write fails for any reason, try setting custom_user_amount/currency
|
||||
try:
|
||||
wizard.custom_user_amount = amount_to_pay
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# Create and post payments under company context
|
||||
payments = wizard.with_company(company)._create_payments()
|
||||
|
||||
except Exception as e:
|
||||
# If company-scoped approach fails, fallback to plain context (more permissive)
|
||||
_logger.warning("Falling back to non-company-scoped payment register due to: %s", str(e))
|
||||
_logger.warning(traceback.format_exc())
|
||||
try:
|
||||
PaymentRegisterModel = request.env["account.payment.register"].sudo().with_context(
|
||||
active_model="account.move",
|
||||
active_ids=[account_move.id],
|
||||
skip_invoice_sync=True,
|
||||
)
|
||||
wizard = PaymentRegisterModel.create({})
|
||||
try:
|
||||
wizard.amount = amount_to_pay
|
||||
except Exception:
|
||||
try:
|
||||
wizard.custom_user_amount = amount_to_pay
|
||||
except Exception:
|
||||
pass
|
||||
payments = wizard._create_payments()
|
||||
except Exception as e2:
|
||||
_logger.error("Error creando el pago para la factura %s: %s", data_external_invoice_id, str(e2))
|
||||
_logger.error(traceback.format_exc())
|
||||
request.env.cr.rollback()
|
||||
return {"success": False, "error": str(e2)}
|
||||
|
||||
# Commit DB before returning
|
||||
request.env.cr.commit()
|
||||
|
||||
# Refresh invoice payment state
|
||||
account_move_sudo = request.env["account.move"].sudo().browse(account_move.id)
|
||||
|
||||
return {
|
||||
"invoiceId": data_external_invoice_id,
|
||||
"status": account_move_sudo.payment_state.upper() if account_move_sudo.payment_state else "UNKNOWN",
|
||||
"paid": account_move_sudo.payment_state == "paid",
|
||||
"paymentIds": payments.ids if payments else [],
|
||||
"success": True,
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
_logger.error("Error al crear el pago account.move: %s", str(e))
|
||||
_logger.error(traceback.format_exc())
|
||||
request.env.cr.rollback()
|
||||
return {"success": False, "error": str(e)}
|
||||
@@ -0,0 +1,9 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<data noupdate="1">
|
||||
<record id="account_invoice_webhook_url" model="ir.config_parameter">
|
||||
<field name="key">account.api.invoice.state.webhook</field>
|
||||
<field name="value">https://libraries-exclusive-vernon-device.trycloudflare.com/odoo/webhook</field>
|
||||
</record>
|
||||
</data>
|
||||
</odoo>
|
||||
@@ -0,0 +1 @@
|
||||
from . import account_move
|
||||
@@ -0,0 +1,75 @@
|
||||
from odoo import models, _
|
||||
import requests
|
||||
import json
|
||||
import logging
|
||||
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class AccountMove(models.Model):
|
||||
_inherit = "account.move"
|
||||
|
||||
def _send_payment_status_webhook(self):
|
||||
"""Send webhook notification when payment status changes"""
|
||||
for record in self:
|
||||
states = {
|
||||
"not_paid": "PENDING",
|
||||
"paid": "PAID",
|
||||
"cancel": "CANCELED",
|
||||
}
|
||||
if not record.ref or len(record.ref) == 0:
|
||||
_logger.warning(
|
||||
f"Skipping webhook for invoice with empty reference (ID: {record.id})"
|
||||
)
|
||||
continue
|
||||
if not record.payment_state or len(record.payment_state) == 0:
|
||||
_logger.warning(
|
||||
f"Skipping webhook for invoice {record.ref} with empty payment state"
|
||||
)
|
||||
continue
|
||||
if not record.payment_state in states.keys():
|
||||
_logger.warning(
|
||||
f"Skipping webhook for invoice {record.ref} with unsupported payment state {record.payment_state}"
|
||||
)
|
||||
continue
|
||||
|
||||
data = {
|
||||
"invoiceId": record.ref,
|
||||
"status": states.get(record.payment_state, "UNKNOWN"),
|
||||
}
|
||||
|
||||
webhook_url = (
|
||||
self.env["ir.config_parameter"]
|
||||
.sudo()
|
||||
.get_param("account.api.invoice.state.webhook")
|
||||
)
|
||||
|
||||
try:
|
||||
headers = {"Content-Type": "application/json"}
|
||||
|
||||
response = requests.post(
|
||||
webhook_url, data=json.dumps(data), headers=headers, timeout=30
|
||||
)
|
||||
|
||||
if response.status_code == 200:
|
||||
_logger.info(
|
||||
f"Notificación de pago enviada exitosamente para factura {record.ref}"
|
||||
)
|
||||
else:
|
||||
_logger.warning(
|
||||
f"Error al enviar notificación de pago para factura {record.ref}. Status code: {response.status_code}"
|
||||
)
|
||||
|
||||
except requests.exceptions.RequestException as e:
|
||||
_logger.error(
|
||||
f"Error al enviar notificación de pago para factura {record.ref}: {str(e)}"
|
||||
)
|
||||
except Exception as e:
|
||||
_logger.error(
|
||||
f"Error inesperado al procesar notificación de pago para factura {record.ref}: {str(e)}"
|
||||
)
|
||||
|
||||
def _compute_payment_state(self):
|
||||
super()._compute_payment_state()
|
||||
for record in self:
|
||||
record._send_payment_status_webhook()
|
||||
Reference in New Issue
Block a user