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