142 lines
5.0 KiB
Python
142 lines
5.0 KiB
Python
from django.db import transaction
|
||
from django.utils import timezone
|
||
|
||
from commerce.integrations.moysklad import MoySkladError
|
||
from commerce.models import ExternalProject, ProjectPayment, Sale, SourceRecord
|
||
from commerce.services.ingestion import ingest_source_record
|
||
from commerce.services.moysklad_sync import (
|
||
_document_currency,
|
||
_external_id,
|
||
_money,
|
||
_reference_external_id,
|
||
_source_updated_at,
|
||
)
|
||
|
||
|
||
def project_reference(source, row):
|
||
if not isinstance(row, dict):
|
||
return None
|
||
external_id = _reference_external_id(row)
|
||
if not external_id:
|
||
return None
|
||
project, _ = ExternalProject.objects.get_or_create(
|
||
source=source,
|
||
external_id=external_id,
|
||
defaults={"name": str(row.get("name") or external_id)[:255]},
|
||
)
|
||
# A reference must not erase information obtained from the full catalogue.
|
||
if row.get("name") and project.name != row["name"]:
|
||
project.name = str(row["name"])[:255]
|
||
project.save(update_fields=["name"])
|
||
return project
|
||
|
||
|
||
@transaction.atomic
|
||
def normalize_project(source, row):
|
||
external_id = _external_id(row)
|
||
if not external_id or not row.get("name"):
|
||
raise MoySkladError("В проекте отсутствует ID или название.")
|
||
ingest_source_record(
|
||
source=source,
|
||
entity_type="project",
|
||
external_id=external_id,
|
||
payload=row,
|
||
source_updated_at=_source_updated_at(row),
|
||
)
|
||
project, _ = ExternalProject.objects.update_or_create(
|
||
source=source,
|
||
external_id=external_id,
|
||
defaults={
|
||
"name": str(row["name"])[:255],
|
||
"code": str(row.get("code") or "")[:255],
|
||
"description": str(row.get("description") or ""),
|
||
"archived": bool(row.get("archived")),
|
||
"synced_at": timezone.now(),
|
||
},
|
||
)
|
||
return project
|
||
|
||
|
||
@transaction.atomic
|
||
def normalize_project_payment(source, row, document_type, seen_at):
|
||
if document_type not in {"paymentout", "cashout"}:
|
||
raise MoySkladError("Неподдерживаемый платёж проекта.")
|
||
external_id = _external_id(row)
|
||
occurred_at = _source_updated_at({"updated": row.get("moment")})
|
||
if not external_id or not occurred_at or row.get("sum") is None:
|
||
raise MoySkladError("В платеже отсутствует ID, дата или сумма.")
|
||
amount = _money(row["sum"])
|
||
if amount < 0:
|
||
raise MoySkladError("Отрицательная сумма исходящего платежа.")
|
||
record = ingest_source_record(
|
||
source=source,
|
||
entity_type=document_type,
|
||
external_id=external_id,
|
||
payload=row,
|
||
source_updated_at=_source_updated_at(row),
|
||
).record
|
||
payment, _ = ProjectPayment.objects.update_or_create(
|
||
source=source,
|
||
external_id=external_id,
|
||
document_type=document_type,
|
||
defaults={
|
||
"source_record": record,
|
||
"project": project_reference(source, row.get("project")),
|
||
"number": str(row.get("name") or "")[:255],
|
||
"description": str(
|
||
row.get("paymentPurpose") or row.get("description") or ""
|
||
),
|
||
"expense_category": str((row.get("expenseItem") or {}).get("name") or "")[
|
||
:255
|
||
],
|
||
"payee": str((row.get("agent") or {}).get("name") or "")[:255],
|
||
"amount": amount,
|
||
"currency": _document_currency(row),
|
||
"occurred_at": occurred_at,
|
||
"applicable": bool(row.get("applicable", False)) and not row.get("deleted"),
|
||
"seen_at": seen_at,
|
||
"missing_from_source": False,
|
||
},
|
||
)
|
||
return payment
|
||
|
||
|
||
def restore_sale_projects(source):
|
||
count = 0
|
||
records = SourceRecord.objects.filter(
|
||
source=source, entity_type__in=["demand", "salesreturn"]
|
||
)
|
||
for record in records.iterator(chunk_size=500):
|
||
project = project_reference(source, record.payload.get("project"))
|
||
if project:
|
||
count += Sale.objects.filter(
|
||
source_record=record, project__isnull=True
|
||
).update(project=project)
|
||
return count
|
||
|
||
|
||
def sync_project_catalog(source, client):
|
||
count = 0
|
||
for archived in [False, True]:
|
||
for row in client.iter_project_data("project", archived=archived):
|
||
normalize_project(source, row)
|
||
count += 1
|
||
restore_sale_projects(source)
|
||
return count
|
||
|
||
|
||
def sync_project_payments(source, client, progress=None):
|
||
count = 0
|
||
for document_type in ["paymentout", "cashout"]:
|
||
seen_at = timezone.now()
|
||
for row in client.iter_project_data(document_type):
|
||
normalize_project_payment(source, row, document_type, seen_at)
|
||
count += 1
|
||
if progress and count % 25 == 0:
|
||
progress(count)
|
||
# Only a fully successful scan may retire missing documents. Keep history.
|
||
ProjectPayment.objects.filter(
|
||
source=source, document_type=document_type, seen_at__lt=seen_at
|
||
).update(missing_from_source=True)
|
||
return count
|