fix error in gitignore

This commit is contained in:
2026-09-24 12:32:59 +03:30
commit 86c117ee2a
165 changed files with 5963 additions and 0 deletions
+1
View File
@@ -0,0 +1 @@
# invoicing app
+6
View File
@@ -0,0 +1,6 @@
from django.apps import AppConfig
class InvoicingConfig(AppConfig):
default_auto_field = "django.db.models.BigAutoField"
name = "apps.invoicing"
+33
View File
@@ -0,0 +1,33 @@
# Generated by Django 5.2.17 on 2026-09-15 09:29
import uuid
from django.db import migrations, models
class Migration(migrations.Migration):
initial = True
dependencies = [
]
operations = [
migrations.CreateModel(
name='Invoice',
fields=[
('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
('created_at', models.DateTimeField(auto_now_add=True, db_index=True)),
('updated_at', models.DateTimeField(auto_now=True)),
('number', models.CharField(max_length=40, unique=True)),
('amount', models.DecimalField(decimal_places=2, max_digits=18)),
('currency', models.CharField(default='IRR', max_length=8)),
('sync_state', models.CharField(choices=[('pending', 'Pending'), ('processing', 'Processing'), ('synchronized', 'Synchronized'), ('failed', 'Failed'), ('retrying', 'Retrying'), ('cancelled', 'Cancelled')], default='pending', max_length=16)),
('odoo_invoice_id', models.CharField(blank=True, max_length=120)),
('last_error', models.TextField(blank=True)),
('synced_at', models.DateTimeField(blank=True, null=True)),
],
options={
'ordering': ['-created_at'],
},
),
]
+22
View File
@@ -0,0 +1,22 @@
# Generated by Django 5.2.17 on 2026-09-15 09:29
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
initial = True
dependencies = [
('invoicing', '0001_initial'),
('orders', '0001_initial'),
]
operations = [
migrations.AddField(
model_name='invoice',
name='order',
field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='invoices', to='orders.processingorder'),
),
]
+28
View File
@@ -0,0 +1,28 @@
from django.db import models
from apps.core.models import TimeStampedModel, UUIDModel
class Invoice(UUIDModel, TimeStampedModel):
class SyncState(models.TextChoices):
PENDING = "pending", "Pending"
PROCESSING = "processing", "Processing"
SYNCHRONIZED = "synchronized", "Synchronized"
FAILED = "failed", "Failed"
RETRYING = "retrying", "Retrying"
CANCELLED = "cancelled", "Cancelled"
order = models.ForeignKey("orders.ProcessingOrder", on_delete=models.PROTECT, related_name="invoices")
number = models.CharField(max_length=40, unique=True)
amount = models.DecimalField(max_digits=18, decimal_places=2)
currency = models.CharField(max_length=8, default="IRR")
sync_state = models.CharField(max_length=16, choices=SyncState.choices, default=SyncState.PENDING)
odoo_invoice_id = models.CharField(max_length=120, blank=True)
last_error = models.TextField(blank=True)
synced_at = models.DateTimeField(null=True, blank=True)
class Meta:
ordering = ["-created_at"]
def __str__(self) -> str:
return self.number
+13
View File
@@ -0,0 +1,13 @@
from rest_framework import serializers
from apps.invoicing.models import Invoice
class InvoiceSerializer(serializers.ModelSerializer):
class Meta:
model = Invoice
fields = "__all__"
read_only_fields = [
"id", "number", "amount", "currency", "sync_state",
"odoo_invoice_id", "last_error", "synced_at",
]
+30
View File
@@ -0,0 +1,30 @@
from celery import shared_task
from django.utils import timezone
from apps.integrations.odoo.services import OdooInvoiceService
from apps.invoicing.models import Invoice
@shared_task(bind=True, max_retries=5, default_retry_delay=60)
def sync_invoice_to_odoo(self, invoice_id):
"""Synchronize an invoice to Odoo with automatic retry (idempotent)."""
try:
invoice = Invoice.objects.get(pk=invoice_id)
except Invoice.DoesNotExist:
return
if invoice.sync_state == Invoice.SyncState.SYNCHRONIZED:
return # already synchronized; retries must be idempotent
invoice.sync_state = Invoice.SyncState.PROCESSING
invoice.save(update_fields=["sync_state"])
try:
result = OdooInvoiceService().sync_invoice(invoice)
invoice.odoo_invoice_id = result.get("id", "")
invoice.sync_state = Invoice.SyncState.SYNCHRONIZED
invoice.synced_at = timezone.now()
invoice.save(update_fields=["odoo_invoice_id", "sync_state", "synced_at"])
except Exception as exc:
invoice.sync_state = Invoice.SyncState.RETRYING
invoice.last_error = str(exc)
invoice.save(update_fields=["sync_state", "last_error"])
raise self.retry(exc=exc)
+8
View File
@@ -0,0 +1,8 @@
from rest_framework.routers import DefaultRouter
from apps.invoicing.views import InvoiceViewSet
router = DefaultRouter()
router.register("", InvoiceViewSet, basename="invoice")
urlpatterns = router.urls
+52
View File
@@ -0,0 +1,52 @@
from django.utils import timezone
from rest_framework import decorators, mixins, response, viewsets
from apps.core.errors import ApiError
from apps.integrations.odoo.services import OdooInvoiceService
from apps.invoicing.models import Invoice
from apps.invoicing.serializers import InvoiceSerializer
from apps.rbac.permissions import ActionPermission
class InvoiceViewSet(
mixins.ListModelMixin, mixins.RetrieveModelMixin, mixins.CreateModelMixin, viewsets.GenericViewSet
):
serializer_class = InvoiceSerializer
permission_classes = [ActionPermission]
code_map = {
"list": "invoice.view",
"retrieve": "invoice.view",
"create": "invoice.create",
"sync": "invoice.create",
}
def get_queryset(self):
user = self.request.user
qs = Invoice.objects.select_related("order").all()
if user.is_superuser:
return qs
return qs.filter(order__customer__user=user)
def perform_create(self, serializer):
order = serializer.validated_data["order"]
total = (order.price_snapshot or {}).get("total")
if not total:
raise ApiError("NO_PRICE_SNAPSHOT", "Order has no price snapshot.")
serializer.save(number=f"INV-{order.order_number}", amount=total, currency=order.currency)
@decorators.action(detail=True, methods=["post"])
def sync(self, request, pk=None):
invoice = self.get_object()
invoice.sync_state = Invoice.SyncState.PROCESSING
invoice.save(update_fields=["sync_state"])
try:
result = OdooInvoiceService().sync_invoice(invoice)
invoice.odoo_invoice_id = result.get("id", "")
invoice.sync_state = Invoice.SyncState.SYNCHRONIZED
invoice.synced_at = timezone.now()
invoice.save(update_fields=["odoo_invoice_id", "sync_state", "synced_at"])
except Exception as exc:
invoice.sync_state = Invoice.SyncState.FAILED
invoice.last_error = str(exc)
invoice.save(update_fields=["sync_state", "last_error"])
return response.Response(InvoiceSerializer(invoice).data)