388 lines
16 KiB
Python
388 lines
16 KiB
Python
from django.db import models
|
||
import logging
|
||
from .ssh_client import SSHManager
|
||
from django.core.validators import URLValidator
|
||
from django.core.exceptions import ValidationError
|
||
from .system_settings import SystemSettings
|
||
|
||
logger = logging.getLogger(__name__)
|
||
|
||
def validate_domain(value):
|
||
"""Domain formatını kontrol et"""
|
||
if ' ' in value: # Boşluk kontrolü
|
||
raise ValidationError('Domain adında boşluk olamaz')
|
||
if not any(char.isalpha() for char in value): # En az bir harf kontrolü
|
||
raise ValidationError('Domain adında en az bir harf olmalıdır')
|
||
if not all(c.isalnum() or c in '-.' for c in value): # Geçerli karakter kontrolü
|
||
raise ValidationError('Domain adında sadece harf, rakam, tire (-) ve nokta (.) olabilir')
|
||
|
||
class Customer(models.Model):
|
||
CUSTOMER_TYPES = (
|
||
('individual', 'Bireysel'),
|
||
('corporate', 'Kurumsal'),
|
||
)
|
||
|
||
customer_type = models.CharField(max_length=20, choices=CUSTOMER_TYPES, verbose_name='Müşteri Tipi')
|
||
|
||
# Ortak alanlar
|
||
name = models.CharField(max_length=200, verbose_name='Ad/Firma Adı')
|
||
email = models.EmailField(verbose_name='E-posta')
|
||
phone = models.CharField(max_length=20, blank=True, null=True, verbose_name='Telefon')
|
||
address = models.TextField(blank=True, null=True, verbose_name='Adres')
|
||
|
||
# Bireysel müşteri alanları
|
||
surname = models.CharField(max_length=100, blank=True, null=True, verbose_name='Soyad')
|
||
birth_date = models.DateField(blank=True, null=True, verbose_name='Doğum Tarihi')
|
||
tc_number = models.CharField(max_length=11, blank=True, null=True, verbose_name='TC Kimlik No')
|
||
|
||
# Kurumsal müşteri alanları
|
||
company_name = models.CharField(max_length=200, blank=True, null=True, verbose_name='Şirket Adı')
|
||
tax_number = models.CharField(max_length=20, blank=True, null=True, verbose_name='Vergi No')
|
||
tax_office = models.CharField(max_length=100, blank=True, null=True, verbose_name='Vergi Dairesi')
|
||
authorized_person = models.CharField(max_length=200, blank=True, null=True, verbose_name='Yetkili Kişi')
|
||
|
||
# Genel bilgiler
|
||
notes = models.TextField(blank=True, null=True, verbose_name='Notlar')
|
||
created_at = models.DateTimeField(auto_now_add=True, verbose_name='Oluşturma Tarihi')
|
||
updated_at = models.DateTimeField(auto_now=True, verbose_name='Güncelleme Tarihi')
|
||
is_active = models.BooleanField(default=True, verbose_name='Aktif')
|
||
|
||
def __str__(self):
|
||
if self.customer_type == 'corporate':
|
||
return f"{self.company_name or self.name} (Kurumsal)"
|
||
else:
|
||
return f"{self.name} {self.surname or ''} (Bireysel)".strip()
|
||
|
||
def get_display_name(self):
|
||
"""Görüntüleme için uygun isim döndür"""
|
||
if self.customer_type == 'corporate':
|
||
return self.company_name or self.name
|
||
else:
|
||
return f"{self.name} {self.surname or ''}".strip()
|
||
|
||
def clean(self):
|
||
"""Model validasyonu"""
|
||
if self.customer_type == 'individual':
|
||
if not self.surname:
|
||
raise ValidationError('Bireysel müşteriler için soyad zorunludur.')
|
||
elif self.customer_type == 'corporate':
|
||
if not self.company_name:
|
||
raise ValidationError('Kurumsal müşteriler için şirket adı zorunludur.')
|
||
|
||
class Meta:
|
||
verbose_name = "Müşteri"
|
||
verbose_name_plural = "Müşteriler"
|
||
ordering = ['-created_at']
|
||
|
||
class SSHCredential(models.Model):
|
||
name = models.CharField(max_length=100, verbose_name='Host Adı', default='Varsayılan Host')
|
||
hostname = models.CharField(max_length=255)
|
||
username = models.CharField(max_length=100)
|
||
password = models.CharField(max_length=100)
|
||
port = models.IntegerField(default=22)
|
||
base_path = models.CharField(max_length=500, help_text="Projelerin oluşturulacağı ana dizin") # Yeni alan
|
||
is_default = models.BooleanField(default=False, verbose_name='Varsayılan Host')
|
||
connection_status = models.CharField(max_length=20, default='unknown', choices=[
|
||
('connected', 'Bağlı'),
|
||
('failed', 'Başarısız'),
|
||
('unknown', 'Bilinmiyor')
|
||
])
|
||
is_online = models.BooleanField(default=False) # Bağlantı durumu
|
||
last_check = models.DateTimeField(auto_now=True) # Son kontrol zamanı
|
||
last_checked = models.DateTimeField(null=True, blank=True, verbose_name='Son Kontrol')
|
||
disk_usage = models.FloatField(null=True, blank=True, verbose_name='Disk Kullanımı (%)')
|
||
created_at = models.DateTimeField(auto_now_add=True)
|
||
|
||
def get_manager(self):
|
||
"""SSHManager instance'ı döndür"""
|
||
return SSHManager(self)
|
||
|
||
def __str__(self):
|
||
return f"{self.username}@{self.hostname}"
|
||
|
||
class Project(models.Model):
|
||
name = models.CharField(max_length=100, verbose_name='Proje Adı')
|
||
folder_name = models.CharField(max_length=100, verbose_name='Klasör Adı')
|
||
ssh_credential = models.ForeignKey(SSHCredential, on_delete=models.CASCADE)
|
||
customer = models.ForeignKey(Customer, on_delete=models.CASCADE, verbose_name='Müşteri', null=True, blank=True)
|
||
url = models.TextField(null=True, blank=True, verbose_name='Site URL')
|
||
disk_usage = models.CharField(max_length=20, null=True, blank=True)
|
||
last_backup = models.DateTimeField(null=True, blank=True, verbose_name='Son Yedekleme')
|
||
host_renewal_date = models.DateField(null=True, blank=True, verbose_name='Host Yenileme Tarihi')
|
||
meta_key = models.CharField(max_length=32, null=True, blank=True, verbose_name='Meta Key', help_text='Site aktiflik kontrolü için benzersiz anahtar')
|
||
is_site_active = models.BooleanField(default=False, verbose_name='Site Aktif')
|
||
last_site_check = models.DateTimeField(null=True, blank=True, verbose_name='Son Site Kontrolü')
|
||
created_at = models.DateTimeField(auto_now_add=True)
|
||
updated_at = models.DateTimeField(auto_now=True)
|
||
|
||
def generate_meta_key(self):
|
||
"""Benzersiz meta key oluştur"""
|
||
import uuid
|
||
self.meta_key = uuid.uuid4().hex[:32]
|
||
return self.meta_key
|
||
|
||
def get_meta_tag(self):
|
||
"""HTML meta tag'ı döndür"""
|
||
if not self.meta_key:
|
||
self.generate_meta_key()
|
||
self.save()
|
||
|
||
# Farklı meta tag formatları
|
||
meta_tags = [
|
||
f'<meta name="site-verification" content="{self.meta_key}">',
|
||
f'<meta name="site-verify" content="{self.meta_key}">',
|
||
f'<meta name="verification" content="{self.meta_key}">'
|
||
]
|
||
|
||
# HTML yorum içinde meta key
|
||
comment_tag = f'<!-- site-verification: {self.meta_key} -->'
|
||
|
||
# Tüm formatları birleştir
|
||
return meta_tags[0], meta_tags, comment_tag
|
||
|
||
|
||
|
||
def save(self, *args, **kwargs):
|
||
self.clean()
|
||
super().save(*args, **kwargs)
|
||
|
||
def get_full_path(self):
|
||
"""Projenin tam yolunu döndür"""
|
||
return f"{self.ssh_credential.base_path}/{self.folder_name}"
|
||
|
||
@property
|
||
def has_requirements(self):
|
||
"""Projenin req.txt dosyası var mı kontrol et"""
|
||
try:
|
||
ssh_manager = self.ssh_credential.get_manager()
|
||
check_cmd = f'test -f "{self.get_full_path()}/req.txt" && echo "exists"'
|
||
stdout, stderr, status = ssh_manager.execute_command(check_cmd)
|
||
|
||
# Debug için log ekle
|
||
logger.info(f"has_requirements check for project {self.id}")
|
||
logger.info(f"Command: {check_cmd}")
|
||
logger.info(f"Status: {status}, Stdout: {stdout}, Stderr: {stderr}")
|
||
|
||
return status and stdout.strip() == "exists"
|
||
except Exception as e:
|
||
logger.exception(f"Error checking requirements for project {self.id}")
|
||
return False
|
||
finally:
|
||
if 'ssh_manager' in locals():
|
||
ssh_manager.close()
|
||
|
||
def __str__(self):
|
||
return self.name
|
||
|
||
class Meta:
|
||
verbose_name = "Proje"
|
||
verbose_name_plural = "Projeler"
|
||
ordering = ['-created_at']
|
||
|
||
class Invoice(models.Model):
|
||
INVOICE_STATUS = (
|
||
('draft', 'Taslak'),
|
||
('sent', 'Gönderildi'),
|
||
('paid', 'Ödendi'),
|
||
('overdue', 'Gecikti'),
|
||
('cancelled', 'İptal Edildi'),
|
||
)
|
||
|
||
PAYMENT_METHODS = (
|
||
('bank_transfer', 'Banka Havalesi'),
|
||
('credit_card', 'Kredi Kartı'),
|
||
('cash', 'Nakit'),
|
||
('other', 'Diğer'),
|
||
)
|
||
|
||
INVOICE_TYPES = (
|
||
('income', 'Gelir'),
|
||
('expense', 'Gider'),
|
||
)
|
||
|
||
customer = models.ForeignKey(Customer, on_delete=models.CASCADE, related_name='invoices', verbose_name='Müşteri')
|
||
invoice_number = models.CharField(max_length=50, unique=True, verbose_name='Fatura No')
|
||
invoice_type = models.CharField(max_length=10, choices=INVOICE_TYPES, default='income', verbose_name='Fatura Tipi')
|
||
issue_date = models.DateField(verbose_name='Düzenleme Tarihi')
|
||
due_date = models.DateField(verbose_name='Son Ödeme Tarihi')
|
||
status = models.CharField(max_length=20, choices=INVOICE_STATUS, default='draft', verbose_name='Durum')
|
||
payment_method = models.CharField(max_length=20, choices=PAYMENT_METHODS, default='bank_transfer', verbose_name='Ödeme Yöntemi')
|
||
total_amount = models.DecimalField(max_digits=10, decimal_places=2, default=0, verbose_name='Toplam Tutar')
|
||
notes = models.TextField(blank=True, null=True, verbose_name='Notlar')
|
||
payment_notes = models.TextField(blank=True, null=True, verbose_name='Ödeme Notları')
|
||
created_at = models.DateTimeField(auto_now_add=True, verbose_name='Oluşturma Tarihi')
|
||
updated_at = models.DateTimeField(auto_now=True, verbose_name='Güncelleme Tarihi')
|
||
|
||
def __str__(self):
|
||
return f"Fatura #{self.invoice_number} - {self.customer.get_display_name()}"
|
||
|
||
def save(self, *args, **kwargs):
|
||
if not self.invoice_number:
|
||
# Otomatik fatura numarası oluştur (F-YIL-AYGUN-XXXX)
|
||
from datetime import datetime
|
||
year = datetime.now().strftime('%Y')
|
||
day_month = datetime.now().strftime('%d%m')
|
||
|
||
# Son fatura numarasını bul
|
||
last_invoice = Invoice.objects.filter(
|
||
invoice_number__startswith=f'F-{year}'
|
||
).order_by('-invoice_number').first()
|
||
|
||
if last_invoice:
|
||
try:
|
||
last_num = int(last_invoice.invoice_number.split('-')[-1])
|
||
new_num = last_num + 1
|
||
except (ValueError, IndexError):
|
||
new_num = 1
|
||
else:
|
||
new_num = 1
|
||
|
||
self.invoice_number = f'F-{year}-{day_month}-{new_num:04d}'
|
||
|
||
super().save(*args, **kwargs)
|
||
|
||
class Meta:
|
||
verbose_name = "Fatura"
|
||
verbose_name_plural = "Faturalar"
|
||
ordering = ['-issue_date', '-id']
|
||
|
||
class InvoiceItem(models.Model):
|
||
invoice = models.ForeignKey(Invoice, related_name='items', on_delete=models.CASCADE, verbose_name='Fatura')
|
||
project = models.ForeignKey(Project, on_delete=models.SET_NULL, null=True, blank=True, verbose_name='İlişkili Proje')
|
||
description = models.CharField(max_length=255, verbose_name='Açıklama')
|
||
amount = models.DecimalField(max_digits=10, decimal_places=2, verbose_name='Tutar')
|
||
|
||
def __str__(self):
|
||
return f"{self.description} - {self.invoice.invoice_number}"
|
||
|
||
def save(self, *args, **kwargs):
|
||
super().save(*args, **kwargs)
|
||
self.update_invoice_total()
|
||
|
||
def delete(self, *args, **kwargs):
|
||
invoice = self.invoice
|
||
super().delete(*args, **kwargs)
|
||
# Fatura silindiğinde tutarı güncelle
|
||
self.update_invoice_total()
|
||
|
||
def update_invoice_total(self):
|
||
"""Fatura toplam tutarını güncelle"""
|
||
from decimal import Decimal, InvalidOperation
|
||
|
||
invoice = self.invoice
|
||
items = invoice.items.all()
|
||
|
||
# Toplam tutar - güvenli dönüşümle hesapla
|
||
total = Decimal('0.00')
|
||
|
||
for item in items:
|
||
try:
|
||
# Eğer item.amount None ise veya geçersizse, 0 olarak kabul et
|
||
if item.amount is None:
|
||
continue
|
||
|
||
# String ise decimal'e çevir
|
||
if isinstance(item.amount, str):
|
||
if ',' in item.amount:
|
||
item.amount = item.amount.replace(',', '.')
|
||
item_amount = Decimal(item.amount)
|
||
else:
|
||
item_amount = Decimal(str(item.amount))
|
||
|
||
total += item_amount
|
||
except (InvalidOperation, ValueError, TypeError) as e:
|
||
print(f"Invoice total hesaplama hatası: {str(e)}")
|
||
# Hatalı öğeyi atla
|
||
continue
|
||
|
||
# Fatura tutarını güncelle
|
||
invoice.total_amount = total
|
||
invoice.save()
|
||
|
||
class Meta:
|
||
verbose_name = "Fatura Kalemi"
|
||
verbose_name_plural = "Fatura Kalemleri"
|
||
ordering = ['id']
|
||
|
||
class SSHLog(models.Model):
|
||
LOG_TYPES = (
|
||
('connection', 'Bağlantı Kontrolü'),
|
||
('command', 'Komut Çalıştırma'),
|
||
('folder', 'Klasör İşlemi'),
|
||
('file', 'Dosya İşlemi'),
|
||
('backup', 'Yedekleme'), # Yeni tip ekleyelim
|
||
)
|
||
|
||
ssh_credential = models.ForeignKey(SSHCredential, on_delete=models.CASCADE)
|
||
log_type = models.CharField(max_length=20, choices=LOG_TYPES)
|
||
command = models.TextField()
|
||
output = models.TextField()
|
||
status = models.BooleanField(default=True)
|
||
created_at = models.DateTimeField(auto_now_add=True)
|
||
|
||
def __str__(self):
|
||
return f"{self.ssh_credential.hostname} - {self.log_type} - {self.created_at}"
|
||
|
||
class Meta:
|
||
ordering = ['-created_at']
|
||
|
||
class Backup(models.Model):
|
||
BACKUP_STATUS = (
|
||
('running', 'Devam Ediyor'),
|
||
('completed', 'Tamamlandı'),
|
||
('failed', 'Başarısız'),
|
||
('cancelled', 'İptal Edildi'),
|
||
)
|
||
|
||
BACKUP_TYPE = (
|
||
('manual', 'Manuel'),
|
||
('auto', 'Otomatik'),
|
||
('scheduled', 'Zamanlanmış'),
|
||
)
|
||
|
||
project = models.ForeignKey(Project, on_delete=models.CASCADE, verbose_name='Proje')
|
||
backup_type = models.CharField(max_length=20, choices=BACKUP_TYPE, default='manual', verbose_name='Yedekleme Tipi')
|
||
status = models.CharField(max_length=20, choices=BACKUP_STATUS, default='running', verbose_name='Durum')
|
||
start_time = models.DateTimeField(auto_now_add=True, verbose_name='Başlangıç Zamanı')
|
||
end_time = models.DateTimeField(null=True, blank=True, verbose_name='Bitiş Zamanı')
|
||
file_path = models.CharField(max_length=500, null=True, blank=True, verbose_name='Dosya Yolu')
|
||
file_size = models.BigIntegerField(null=True, blank=True, verbose_name='Dosya Boyutu') # BigIntegerField kullanarak büyük dosya boyutlarını destekle
|
||
error_message = models.TextField(null=True, blank=True, verbose_name='Hata Mesajı')
|
||
notes = models.TextField(null=True, blank=True, verbose_name='Notlar')
|
||
|
||
def __str__(self):
|
||
return f"{self.project.name} - {self.start_time.strftime('%d.%m.%Y %H:%M')}"
|
||
|
||
@property
|
||
def duration(self):
|
||
"""Yedekleme süresini hesapla"""
|
||
if self.end_time:
|
||
duration = self.end_time - self.start_time
|
||
total_seconds = int(duration.total_seconds())
|
||
hours = total_seconds // 3600
|
||
minutes = (total_seconds % 3600) // 60
|
||
seconds = total_seconds % 60
|
||
|
||
if hours > 0:
|
||
return f"{hours}s {minutes}d {seconds}s"
|
||
elif minutes > 0:
|
||
return f"{minutes}d {seconds}s"
|
||
else:
|
||
return f"{seconds}s"
|
||
return "-"
|
||
|
||
@property
|
||
def status_badge_class(self):
|
||
"""Durum badge CSS sınıfı"""
|
||
status_classes = {
|
||
'running': 'bg-info',
|
||
'completed': 'bg-success',
|
||
'failed': 'bg-danger',
|
||
'cancelled': 'bg-warning'
|
||
}
|
||
return status_classes.get(self.status, 'bg-secondary')
|
||
|
||
class Meta:
|
||
verbose_name = "Yedekleme"
|
||
verbose_name_plural = "Yedeklemeler"
|
||
ordering = ['-start_time'] |