Compare commits

...

3 Commits

Author SHA1 Message Date
Flatlogic Bot
f52acbf5c3 Auto commit: 2026-02-06T00:30:42.683Z 2026-02-06 00:30:42 +00:00
Flatlogic Bot
cddb69516f Auto commit: 2026-02-06T00:08:49.034Z 2026-02-06 00:08:49 +00:00
Flatlogic Bot
e6fa05f94e Auto commit: 2026-02-05T22:10:08.709Z 2026-02-05 22:10:08 +00:00
32 changed files with 1469 additions and 211 deletions

Binary file not shown.

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 109 KiB

View File

@ -43,14 +43,9 @@ CSRF_COOKIE_SECURE = True
SESSION_COOKIE_SAMESITE = "None" SESSION_COOKIE_SAMESITE = "None"
CSRF_COOKIE_SAMESITE = "None" CSRF_COOKIE_SAMESITE = "None"
# Quick-start development settings - unsuitable for production
# See https://docs.djangoproject.com/en/5.2/howto/deployment/checklist/
# Application definition # Application definition
INSTALLED_APPS = [ INSTALLED_APPS = [
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes', 'django.contrib.contenttypes',
'django.contrib.sessions', 'django.contrib.sessions',
'django.contrib.messages', 'django.contrib.messages',
@ -63,10 +58,7 @@ MIDDLEWARE = [
'django.contrib.sessions.middleware.SessionMiddleware', 'django.contrib.sessions.middleware.SessionMiddleware',
'django.middleware.common.CommonMiddleware', 'django.middleware.common.CommonMiddleware',
'django.middleware.csrf.CsrfViewMiddleware', 'django.middleware.csrf.CsrfViewMiddleware',
'django.contrib.auth.middleware.AuthenticationMiddleware',
'django.contrib.messages.middleware.MessageMiddleware', 'django.contrib.messages.middleware.MessageMiddleware',
# Disable X-Frame-Options middleware to allow Flatlogic preview iframes.
# 'django.middleware.clickjacking.XFrameOptionsMiddleware',
] ]
X_FRAME_OPTIONS = 'ALLOWALL' X_FRAME_OPTIONS = 'ALLOWALL'
@ -81,7 +73,6 @@ TEMPLATES = [
'OPTIONS': { 'OPTIONS': {
'context_processors': [ 'context_processors': [
'django.template.context_processors.request', 'django.template.context_processors.request',
'django.contrib.auth.context_processors.auth',
'django.contrib.messages.context_processors.messages', 'django.contrib.messages.context_processors.messages',
# IMPORTANT: do not remove injects PROJECT_DESCRIPTION/PROJECT_IMAGE_URL and cache-busting timestamp # IMPORTANT: do not remove injects PROJECT_DESCRIPTION/PROJECT_IMAGE_URL and cache-busting timestamp
'core.context_processors.project_context', 'core.context_processors.project_context',
@ -110,26 +101,6 @@ DATABASES = {
}, },
} }
# Password validation
# https://docs.djangoproject.com/en/5.2/ref/settings/#auth-password-validators
AUTH_PASSWORD_VALIDATORS = [
{
'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator',
},
{
'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator',
},
{
'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator',
},
{
'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator',
},
]
# Internationalization # Internationalization
# https://docs.djangoproject.com/en/5.2/topics/i18n/ # https://docs.djangoproject.com/en/5.2/topics/i18n/

View File

@ -14,13 +14,11 @@ Including another URLconf
1. Import the include() function: from django.urls import include, path 1. Import the include() function: from django.urls import include, path
2. Add a URL to urlpatterns: path('blog/', include('blog.urls')) 2. Add a URL to urlpatterns: path('blog/', include('blog.urls'))
""" """
from django.contrib import admin
from django.urls import include, path from django.urls import include, path
from django.conf import settings from django.conf import settings
from django.conf.urls.static import static from django.conf.urls.static import static
urlpatterns = [ urlpatterns = [
path("admin/", admin.site.urls),
path("", include("core.urls")), path("", include("core.urls")),
] ]

Binary file not shown.

Binary file not shown.

View File

@ -1,3 +1,24 @@
from django.contrib import admin from django.contrib import admin
from .models import Firma, Fatura, FaturaKalemi
# Register your models here. @admin.register(Firma)
class FirmaAdmin(admin.ModelAdmin):
list_display = ('ad', 'vergi_no', 'olusturulma_tarihi')
search_fields = ('ad', 'vergi_no')
class FaturaKalemiInline(admin.TabularInline):
model = FaturaKalemi
extra = 1
@admin.register(Fatura)
class FaturaAdmin(admin.ModelAdmin):
list_display = ('fatura_no', 'firma', 'tarih', 'genel_toplam', 'islenmis')
list_filter = ('islenmis', 'tarih', 'firma')
search_fields = ('fatura_no', 'firma__ad')
inlines = [FaturaKalemiInline]
@admin.register(FaturaKalemi)
class FaturaKalemiAdmin(admin.ModelAdmin):
list_display = ('urun_adi', 'fatura', 'adet', 'toplam_tutar')
list_filter = ('fatura__firma',)
search_fields = ('urun_adi', 'fatura__fatura_no')

27
core/forms.py Normal file
View File

@ -0,0 +1,27 @@
from django import forms
from .models import Firma, Fatura, FaturaKalemi
class FirmaForm(forms.ModelForm):
class Meta:
model = Firma
fields = ['ad', 'vergi_no', 'mersis_no', 'adres']
widgets = {
'ad': forms.TextInput(attrs={'class': 'form-control', 'placeholder': 'Firma Tam Adı'}),
'vergi_no': forms.TextInput(attrs={'class': 'form-control', 'placeholder': 'Vergi No / TC Kimlik No'}),
'mersis_no': forms.TextInput(attrs={'class': 'form-control', 'placeholder': 'MERSİS No (Opsiyonel)'}),
'adres': forms.Textarea(attrs={'class': 'form-control', 'rows': 3, 'placeholder': 'Firma Adresi'}),
}
class FaturaForm(forms.ModelForm):
class Meta:
model = Fatura
fields = ['firma', 'fatura_no', 'tarih', 'ara_toplam', 'kdv_toplam', 'genel_toplam', 'pdf_dosyasi']
widgets = {
'firma': forms.Select(attrs={'class': 'form-select'}),
'fatura_no': forms.TextInput(attrs={'class': 'form-control', 'placeholder': 'Fatura No'}),
'tarih': forms.DateInput(attrs={'class': 'form-control', 'type': 'date'}),
'ara_toplam': forms.NumberInput(attrs={'class': 'form-control', 'step': '0.01'}),
'kdv_toplam': forms.NumberInput(attrs={'class': 'form-control', 'step': '0.01'}),
'genel_toplam': forms.NumberInput(attrs={'class': 'form-control', 'step': '0.01'}),
'pdf_dosyasi': forms.FileInput(attrs={'class': 'form-control'}),
}

View File

@ -0,0 +1,71 @@
# Generated by Django 5.2.7 on 2026-02-05 22:03
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
initial = True
dependencies = [
]
operations = [
migrations.CreateModel(
name='Fatura',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('fatura_no', models.CharField(max_length=50, unique=True, verbose_name='Fatura No')),
('tarih', models.DateField(verbose_name='Fatura Tarihi')),
('ara_toplam', models.DecimalField(decimal_places=2, default=0, max_digits=12, verbose_name='Ara Toplam')),
('kdv_toplam', models.DecimalField(decimal_places=2, default=0, max_digits=12, verbose_name='KDV Toplam')),
('genel_toplam', models.DecimalField(decimal_places=2, default=0, max_digits=12, verbose_name='Genel Toplam')),
('pdf_dosyasi', models.FileField(upload_to='faturalar/%Y/%m/', verbose_name='PDF Dosyası')),
('islenmis', models.BooleanField(default=False, verbose_name='İşlendi mi?')),
('olusturulma_tarihi', models.DateTimeField(auto_now_add=True)),
],
options={
'verbose_name': 'Fatura',
'verbose_name_plural': 'Faturalar',
'ordering': ['-tarih'],
},
),
migrations.CreateModel(
name='Firma',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('ad', models.CharField(max_length=255, verbose_name='Firma Adı')),
('vergi_no', models.CharField(max_length=20, unique=True, verbose_name='Vergi No / TC Kimlik No')),
('mersis_no', models.CharField(blank=True, max_length=30, null=True, verbose_name='MERSİS No')),
('adres', models.TextField(blank=True, null=True, verbose_name='Adres')),
('olusturulma_tarihi', models.DateTimeField(auto_now_add=True)),
],
options={
'verbose_name': 'Firma',
'verbose_name_plural': 'Firmalar',
},
),
migrations.CreateModel(
name='FaturaKalemi',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('urun_adi', models.CharField(max_length=255, verbose_name='Ürün Adı')),
('adet', models.DecimalField(decimal_places=2, default=1, max_digits=10, verbose_name='Adet')),
('birim_fiyat', models.DecimalField(decimal_places=2, max_digits=12, verbose_name='Birim Fiyat')),
('kdv_orani', models.DecimalField(decimal_places=2, default=20, max_digits=5, verbose_name='KDV Oranı (%)')),
('kdv_tutari', models.DecimalField(decimal_places=2, max_digits=12, verbose_name='KDV Tutarı')),
('toplam_tutar', models.DecimalField(decimal_places=2, max_digits=12, verbose_name='Toplam Tutar')),
('fatura', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='kalemler', to='core.fatura', verbose_name='Fatura')),
],
options={
'verbose_name': 'Fatura Kalemi',
'verbose_name_plural': 'Fatura Kalemleri',
},
),
migrations.AddField(
model_name='fatura',
name='firma',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='faturalar', to='core.firma', verbose_name='Firma'),
),
]

View File

@ -1,3 +1,50 @@
from django.db import models from django.db import models
# Create your models here. class Firma(models.Model):
ad = models.CharField(max_length=255, verbose_name="Firma Adı")
vergi_no = models.CharField(max_length=20, unique=True, verbose_name="Vergi No / TC Kimlik No")
mersis_no = models.CharField(max_length=30, blank=True, null=True, verbose_name="MERSİS No")
adres = models.TextField(blank=True, null=True, verbose_name="Adres")
olusturulma_tarihi = models.DateTimeField(auto_now_add=True)
def __str__(self):
return self.ad
class Meta:
verbose_name = "Firma"
verbose_name_plural = "Firmalar"
class Fatura(models.Model):
firma = models.ForeignKey(Firma, on_delete=models.CASCADE, related_name="faturalar", verbose_name="Firma")
fatura_no = models.CharField(max_length=50, unique=True, verbose_name="Fatura No")
tarih = models.DateField(verbose_name="Fatura Tarihi")
ara_toplam = models.DecimalField(max_digits=12, decimal_places=2, default=0, verbose_name="Ara Toplam")
kdv_toplam = models.DecimalField(max_digits=12, decimal_places=2, default=0, verbose_name="KDV Toplam")
genel_toplam = models.DecimalField(max_digits=12, decimal_places=2, default=0, verbose_name="Genel Toplam")
pdf_dosyasi = models.FileField(upload_to="faturalar/%Y/%m/", verbose_name="PDF Dosyası")
islenmis = models.BooleanField(default=False, verbose_name="İşlendi mi?")
olusturulma_tarihi = models.DateTimeField(auto_now_add=True)
def __str__(self):
return f"{self.fatura_no} - {self.firma.ad}"
class Meta:
verbose_name = "Fatura"
verbose_name_plural = "Faturalar"
ordering = ['-tarih']
class FaturaKalemi(models.Model):
fatura = models.ForeignKey(Fatura, on_delete=models.CASCADE, related_name="kalemler", verbose_name="Fatura")
urun_adi = models.CharField(max_length=255, verbose_name="Ürün Adı")
adet = models.DecimalField(max_digits=10, decimal_places=2, default=1, verbose_name="Adet")
birim_fiyat = models.DecimalField(max_digits=12, decimal_places=2, verbose_name="Birim Fiyat")
kdv_orani = models.DecimalField(max_digits=5, decimal_places=2, default=20, verbose_name="KDV Oranı (%)")
kdv_tutari = models.DecimalField(max_digits=12, decimal_places=2, verbose_name="KDV Tutarı")
toplam_tutar = models.DecimalField(max_digits=12, decimal_places=2, verbose_name="Toplam Tutar")
def __str__(self):
return f"{self.urun_adi} ({self.fatura.fatura_no})"
class Meta:
verbose_name = "Fatura Kalemi"
verbose_name_plural = "Fatura Kalemleri"

View File

@ -1,25 +1,210 @@
<!DOCTYPE html> <!DOCTYPE html>
<html lang="en"> <html lang="tr">
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<title>{% block title %}Knowledge Base{% endblock %}</title> <meta name="viewport" content="width=device-width, initial-scale=1.0">
{% if project_description %} <title>{% block title %}FaturaYol - Akıllı Fatura Yönetimi{% endblock %}</title>
<meta name="description" content="{{ project_description }}"> <link rel="preconnect" href="https://fonts.googleapis.com">
<meta property="og:description" content="{{ project_description }}"> <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<meta property="twitter:description" content="{{ project_description }}"> <link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap" rel="stylesheet">
{% endif %} <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
{% if project_image_url %} <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.1/font/bootstrap-icons.css">
<meta property="og:image" content="{{ project_image_url }}"> {% load static %}
<meta property="twitter:image" content="{{ project_image_url }}"> <style>
{% endif %} :root {
{% load static %} --primary-color: #2563eb;
<link rel="stylesheet" href="{% static 'css/custom.css' %}?v={{ deployment_timestamp }}"> --sidebar-bg: #0f172a;
{% block head %}{% endblock %} --sidebar-hover: #1e293b;
--sidebar-active: #334155;
--bg-body: #f1f5f9;
--text-main: #1e293b;
--text-muted: #64748b;
}
body {
font-family: 'Inter', sans-serif;
background-color: var(--bg-body);
color: var(--text-main);
overflow-x: hidden;
margin: 0;
padding: 0;
}
#wrapper {
display: flex;
width: 100%;
min-height: 100vh;
}
#sidebar {
width: 280px;
min-width: 280px;
background: var(--sidebar-bg);
color: #fff;
transition: all 0.3s;
z-index: 1000;
box-shadow: 4px 0 10px rgba(0,0,0,0.1);
display: flex;
flex-direction: column;
}
#sidebar .sidebar-header {
padding: 30px 25px;
background: rgba(0,0,0,0.2);
border-bottom: 1px solid rgba(255,255,255,0.05);
}
#sidebar ul.components {
padding: 20px 15px;
}
#sidebar ul li {
margin-bottom: 5px;
}
#sidebar ul li a {
padding: 12px 20px;
display: flex;
align-items: center;
color: #94a3b8;
text-decoration: none;
border-radius: 10px;
font-weight: 500;
transition: all 0.2s;
}
#sidebar ul li a:hover {
color: #fff;
background: var(--sidebar-hover);
}
#sidebar ul li.active > a {
color: #fff;
background: var(--sidebar-active);
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.2);
}
#sidebar ul li a i {
margin-right: 15px;
font-size: 1.2rem;
}
#content {
flex: 1;
min-width: 0;
padding: 40px;
transition: all 0.3s;
}
.top-navbar {
background: #fff;
border-bottom: 1px solid #e2e8f0;
padding: 15px 25px;
margin-bottom: 35px;
border-radius: 15px;
box-shadow: 0 1px 2px rgba(0,0,0,0.05);
display: flex;
align-items: center;
justify-content: space-between;
}
.card {
border: none;
border-radius: 16px;
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.05), 0 2px 4px -1px rgba(0, 0, 0, 0.03);
transition: transform 0.2s;
}
.card:hover {
transform: translateY(-2px);
}
.btn-primary {
background-color: var(--primary-color);
border: none;
padding: 10px 20px;
border-radius: 10px;
font-weight: 600;
}
.badge-soft-success { background-color: #d1fae5; color: #065f46; }
.badge-soft-primary { background-color: #dbeafe; color: #1e40af; }
@media (max-width: 992px) {
#sidebar {
margin-left: -280px;
}
#sidebar.active {
margin-left: 0;
}
#content {
padding: 20px;
}
}
</style>
{% block extra_css %}{% endblock %}
</head> </head>
<body> <body>
{% block content %}{% endblock %} <div id="wrapper">
</body> <!-- Sidebar -->
<nav id="sidebar">
<div class="sidebar-header">
<a href="{% url 'home' %}" class="text-white text-decoration-none">
<h3 class="mb-0 fw-bold d-flex align-items-center">
<i class="bi bi-lightning-charge-fill text-primary me-2"></i>
FaturaYol
</h3>
</a>
<small class="text-muted">Akıllı Arşivleme Sistemi</small>
</div>
<ul class="list-unstyled components">
<li class="{% if active_menu == 'dashboard' %}active{% endif %}">
<a href="{% url 'home' %}">
<i class="bi bi-grid-1x2-fill"></i>
<span>Gösterge Paneli</span>
</a>
</li>
<li>
<a href="{% url 'fatura_otomatik_yukle' %}">
<i class="bi bi-magic"></i>
<span>Otomatik Yükle</span>
</a>
</li>
<li class="{% if active_menu == 'archive' %}active{% endif %}">
<a href="{% url 'fatura_arsivi' %}">
<i class="bi bi-collection-fill"></i>
<span>Fatura Arşivi</span>
</a>
</li>
<li class="{% if active_menu == 'reports' %}active{% endif %}">
<a href="{% url 'raporlar' %}">
<i class="bi bi-bar-chart-line-fill"></i>
<span>İstatistik & Raporlar</span>
</a>
</li>
</ul>
<div class="mt-auto p-4">
<div class="card bg-primary text-white border-0 p-3" style="background: linear-gradient(45deg, #2563eb, #4f46e5);">
<small class="opacity-75 mb-1">Kullanım Durumu</small>
<div class="progress mb-2" style="height: 6px; background: rgba(255,255,255,0.2);">
<div class="progress-bar bg-white" style="width: 45%;"></div>
</div>
<small class="small">Sürüm 1.0.4</small>
</div>
</div>
</nav>
<!-- Page Content -->
<div id="content">
<div class="container-fluid p-0">
<div class="top-navbar">
<div class="d-flex align-items-center">
<h5 class="mb-0 fw-bold">{% block page_title %}Genel Bakış{% endblock %}</h5>
</div>
<div class="d-flex align-items-center">
<div class="me-4 d-none d-md-block">
<form action="{% url 'search' %}" method="get">
<div class="input-group">
<span class="input-group-text bg-light border-0"><i class="bi bi-search"></i></span>
<input type="text" name="q" class="form-control bg-light border-0" placeholder="Fatura veya firma ara..." style="width: 300px;" value="{{ query|default:'' }}">
</div>
</form>
</div>
</div>
</div>
{% block content %}{% endblock %}
</div>
</div>
</div>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
{% block extra_js %}{% endblock %}
</body>
</html> </html>

View File

@ -0,0 +1,30 @@
{% extends "base.html" %}
{% block title %}Silmeyi Onayla - FaturaYol{% endblock %}
{% block page_title %}{{ type }} Silme İşlemi{% endblock %}
{% block content %}
<div class="row justify-content-center">
<div class="col-md-6">
<div class="card border-0 shadow-sm">
<div class="card-body p-5 text-center">
<div class="bg-danger bg-opacity-10 text-danger mx-auto mb-4 rounded-circle d-flex align-items-center justify-content-center" style="width: 80px; height: 80px;">
<i class="bi bi-exclamation-triangle-fill fs-1"></i>
</div>
<h4 class="fw-bold mb-3">Emin misiniz?</h4>
<p class="text-muted mb-4">
<strong>{{ object }}</strong> isimli {{ type|lower }} kaydını silmek üzeresiniz.
Bu işlem geri alınamaz ve bu kayda bağlı tüm veriler silinecektir.
</p>
<form method="post">
{% csrf_token %}
<div class="d-grid gap-2">
<button type="submit" class="btn btn-danger rounded-pill py-2 fw-bold">Evet, Sil</button>
<a href="{{ request.META.HTTP_REFERER|default:'/' }}" class="btn btn-light rounded-pill py-2">İptal</a>
</div>
</form>
</div>
</div>
</div>
</div>
{% endblock %}

View File

@ -0,0 +1,57 @@
{% extends "base.html" %}
{% block title %}Fatura Arşivi - FaturaYol{% endblock %}
{% block page_title %}Fatura Arşivi{% endblock %}
{% block content %}
<div class="row mb-4">
<div class="col-md-8">
<p class="text-muted">Firmalara göre gruplanmış fatura listesi. Detaylı görüntülemek için bir firmaya tıklayın.</p>
</div>
<div class="col-md-4 text-end">
<a href="{% url 'firma_ekle' %}" class="btn btn-primary rounded-pill px-4">
<i class="bi bi-folder-plus me-2"></i> Yeni Firma Ekle
</a>
</div>
</div>
<div class="row">
{% for firma in firmalar %}
<div class="col-md-4 col-xl-3 mb-4">
<div class="card h-100 shadow-sm border-0">
<div class="card-body p-4 text-center">
<div class="position-absolute top-0 end-0 p-2">
<a href="{% url 'firma_sil' firma.pk %}" class="btn btn-link text-danger p-0" title="Firmayı Sil"><i class="bi bi-trash"></i></a>
</div>
<div class="bg-primary-soft stat-icon mx-auto mb-3" style="width: 60px; height: 60px; border-radius: 50%; display: flex; align-items: center; justify-content: center; background: #e0e7ff; color: #4338ca;">
<i class="bi bi-building" style="font-size: 1.5rem;"></i>
</div>
<h5 class="fw-bold mb-1 text-truncate">{{ firma.ad }}</h5>
<p class="text-muted small mb-3">VN: {{ firma.vergi_no }}</p>
<div class="d-flex justify-content-between align-items-center bg-light p-2 rounded-3 mb-3">
<div class="text-start ps-2">
<small class="text-muted d-block" style="font-size: 0.65rem; text-transform: uppercase;">Fatura</small>
<span class="fw-bold">{{ firma.fatura_sayisi }} Adet</span>
</div>
<div class="text-end pe-2">
<small class="text-muted d-block" style="font-size: 0.65rem; text-transform: uppercase;">Toplam</small>
<span class="fw-bold text-primary">{{ firma.toplam_tutar|default:0|floatformat:2 }} ₺</span>
</div>
</div>
<a href="{% url 'firma_detay' firma.pk %}" class="btn btn-outline-primary btn-sm w-100 rounded-pill">
Dosyaları Görüntüle <i class="bi bi-chevron-right ms-1"></i>
</a>
</div>
</div>
</div>
{% empty %}
<div class="col-12 text-center py-5">
<i class="bi bi-folder2-open text-muted" style="font-size: 4rem;"></i>
<p class="mt-3 text-muted">Henüz kayıtlı bir firma veya fatura bulunamadı.</p>
<a href="{% url 'firma_ekle' %}" class="btn btn-primary mt-2">İlk Firmayı Ekle</a>
</div>
{% endfor %}
</div>
{% endblock %}

View File

@ -0,0 +1,105 @@
{% extends "base.html" %}
{% block title %}Fatura Detayı - {{ fatura.fatura_no }}{% endblock %}
{% block page_title %}Fatura Detayı{% endblock %}
{% block content %}
<nav aria-label="breadcrumb" class="mb-4">
<ol class="breadcrumb">
<li class="breadcrumb-item"><a href="{% url 'fatura_arsivi' %}" class="text-decoration-none">Arşiv</a></li>
<li class="breadcrumb-item"><a href="{% url 'firma_detay' fatura.firma.pk %}" class="text-decoration-none">{{ fatura.firma.ad }}</a></li>
<li class="breadcrumb-item active">{{ fatura.fatura_no }}</li>
</ol>
</nav>
<div class="row">
<div class="col-lg-8">
<!-- PDF Önizleme Kartı (Simüle edilmiş, PDF.js veya iframe kullanılabilir) -->
<div class="card border-0 shadow-sm mb-4">
<div class="card-header bg-white py-3 d-flex justify-content-between align-items-center">
<h5 class="mb-0 fw-bold">PDF Önizleme</h5>
<a href="{{ fatura.pdf_dosyasi.url }}" target="_blank" class="btn btn-sm btn-outline-primary">
<i class="bi bi-box-arrow-up-right me-1"></i> Tam Ekran
</a>
</div>
<div class="card-body p-0" style="height: 600px; background: #525659;">
{% if fatura.pdf_dosyasi %}
<iframe src="{{ fatura.pdf_dosyasi.url }}" width="100%" height="100%" frameborder="0"></iframe>
{% else %}
<div class="h-100 d-flex align-items-center justify-content-center text-white flex-column">
<i class="bi bi-file-earmark-pdf mb-3" style="font-size: 3rem;"></i>
<p>PDF dosyası bulunamadı.</p>
</div>
{% endif %}
</div>
</div>
</div>
<div class="col-lg-4">
<!-- Fatura Bilgileri Yan Panel -->
<div class="card border-0 shadow-sm mb-4">
<div class="card-body">
<h5 class="fw-bold mb-4">Fatura Bilgileri</h5>
<div class="mb-3">
<label class="text-muted small text-uppercase fw-bold">Firma</label>
<p class="fw-bold mb-0 text-primary">{{ fatura.firma.ad }}</p>
</div>
<div class="row mb-3">
<div class="col-6">
<label class="text-muted small text-uppercase fw-bold">Tarih</label>
<p class="fw-bold mb-0">{{ fatura.tarih|date:"d.m.Y" }}</p>
</div>
<div class="col-6">
<label class="text-muted small text-uppercase fw-bold">Fatura No</label>
<p class="fw-bold mb-0">{{ fatura.fatura_no }}</p>
</div>
</div>
<hr class="my-4">
<div class="d-flex justify-content-between mb-2">
<span class="text-muted">Ara Toplam</span>
<span class="fw-bold">{{ fatura.ara_toplam }} ₺</span>
</div>
<div class="d-flex justify-content-between mb-2">
<span class="text-muted">KDV Toplam</span>
<span class="fw-bold">{{ fatura.kdv_toplam }} ₺</span>
</div>
<div class="d-flex justify-content-between mt-3 pt-3 border-top">
<span class="h5 fw-bold mb-0">Genel Toplam</span>
<span class="h5 fw-bold mb-0 text-primary">{{ fatura.genel_toplam }} ₺</span>
</div>
</div>
</div>
<!-- Kalem Detayları -->
<div class="card border-0 shadow-sm">
<div class="card-body p-0">
<div class="p-3 border-bottom">
<h5 class="fw-bold mb-0">Ürün / Hizmet Kalemleri</h5>
</div>
<div class="list-group list-group-flush">
{% for kalem in kalemler %}
<div class="list-group-item p-3">
<div class="d-flex justify-content-between align-items-start mb-1">
<h6 class="fw-bold mb-0">{{ kalem.urun_adi }}</h6>
<span class="badge bg-light text-dark">{{ kalem.adet }} Adet</span>
</div>
<div class="d-flex justify-content-between small text-muted">
<span>Birim: {{ kalem.birim_fiyat }} ₺</span>
<span class="fw-bold text-dark">Toplam: {{ kalem.toplam_tutar }} ₺</span>
</div>
</div>
{% empty %}
<div class="p-4 text-center text-muted">
Kalem verisi bulunamadı.
</div>
{% endfor %}
</div>
</div>
</div>
</div>
</div>
{% endblock %}

View File

@ -0,0 +1,37 @@
{% extends "base.html" %}
{% block title %}{{ title }} - FaturaYol{% endblock %}
{% block page_title %}{{ title }}{% endblock %}
{% block content %}
<div class="row justify-content-center">
<div class="col-md-8">
<div class="card border-0 shadow-sm">
<div class="card-body p-4">
<form method="post" enctype="multipart/form-data">
{% csrf_token %}
<div class="mb-4">
<h5 class="fw-bold mb-3">Fatura Detayları</h5>
{% for field in form %}
<div class="mb-3">
<label class="form-label fw-medium">{{ field.label }}</label>
{{ field }}
{% if field.help_text %}
<div class="form-text">{{ field.help_text }}</div>
{% endif %}
{% for error in field.errors %}
<div class="text-danger small">{{ error }}</div>
{% endfor %}
</div>
{% endfor %}
</div>
<div class="d-flex justify-content-between align-items-center mt-4">
<a href="{% url 'home' %}" class="btn btn-light rounded-pill px-4">İptal</a>
<button type="submit" class="btn btn-primary rounded-pill px-5">Faturayı Yükle</button>
</div>
</form>
</div>
</div>
</div>
</div>
{% endblock %}

View File

@ -0,0 +1,148 @@
{% extends "base.html" %}
{% block title %}Otomatik Fatura Yükle - FaturaYol{% endblock %}
{% block page_title %}Otomatik Fatura Yükle{% endblock %}
{% block content %}
<div class="row justify-content-center">
<div class="col-md-8">
{% if messages %}
{% for message in messages %}
<div class="alert alert-{{ message.tags }} alert-dismissible fade show rounded-4 border-0 shadow-sm mb-4" role="alert">
{{ message }}
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
</div>
{% endfor %}
{% endif %}
<div class="card border-0 shadow-sm">
<div class="card-body p-5 text-center">
<div class="mb-4">
<div class="bg-primary bg-opacity-10 rounded-circle d-inline-flex align-items-center justify-content-center mb-3" style="width: 100px; height: 100px;">
<i class="bi bi-file-earmark-pdf text-primary" style="font-size: 3rem;"></i>
</div>
<h3 class="fw-bold mt-2">Yapay Zeka ile Otomatik Analiz</h3>
<p class="text-muted">Fatura PDF dosyasını seçin, sistemimiz bilgileri otomatik olarak çıkarsın.</p>
</div>
<form method="post" enctype="multipart/form-data" id="auto-upload-form">
{% csrf_token %}
<div class="upload-zone p-5 border border-2 border-dashed rounded-4 mb-4" id="drop-zone">
<input type="file" name="pdf_dosyasi" id="pdf_file" class="d-none" accept=".pdf">
<div id="upload-prompt">
<i class="bi bi-cloud-arrow-up fs-1 text-primary mb-3 d-block"></i>
<label for="pdf_file" class="btn btn-primary rounded-pill px-4 py-2 mb-2 cursor-pointer">
Dosya Seçin
</label>
<p class="mb-0 text-secondary">Veya faturayı buraya sürükleyip bırakın</p>
<small class="text-muted d-block mt-2">Sadece PDF dosyaları desteklenmektedir.</small>
</div>
</div>
<div id="loading-spinner" class="d-none mb-4 py-4">
<div class="spinner-grow text-primary" style="width: 3rem; height: 3rem;" role="status">
<span class="visually-hidden">Analiz ediliyor...</span>
</div>
<h5 class="mt-4 fw-bold text-primary">Fatura Analiz Ediliyor</h5>
<p class="text-muted">Yapay zeka verileri ayrıştırıyor, bu işlem yaklaşık 10-15 saniye sürebilir...</p>
<div class="progress mt-4 mx-auto" style="height: 6px; max-width: 300px;">
<div class="progress-bar progress-bar-striped progress-bar-animated" role="progressbar" style="width: 100%"></div>
</div>
</div>
<div class="d-flex justify-content-center gap-3" id="action-buttons">
<a href="{% url 'home' %}" class="btn btn-light rounded-pill px-4 border">Vazgeç</a>
</div>
</form>
</div>
</div>
<div class="row mt-4">
<div class="col-md-6">
<div class="card border-0 shadow-sm h-100 rounded-4">
<div class="card-body p-4">
<h6 class="fw-bold text-primary mb-3"><i class="bi bi-search me-2"></i>Akıllı Eşleştirme</h6>
<p class="small text-muted mb-0">Sistemimiz Mersis, VKN ve TCKN bilgilerini otomatik tanıyarak firmaları eşleştirir. Firma kayıtlı değilse otomatik oluşturulur.</p>
</div>
</div>
</div>
<div class="col-md-6">
<div class="card border-0 shadow-sm h-100 rounded-4">
<div class="card-body p-4">
<h6 class="fw-bold text-primary mb-3"><i class="bi bi-list-check me-2"></i>Kalem Detayları</h6>
<p class="small text-muted mb-0">Fatura içerisindeki tüm ürün ve hizmet kalemleri, adetleri ve KDV oranları ile birlikte otomatik olarak listelenir.</p>
</div>
</div>
</div>
</div>
</div>
</div>
<style>
.upload-zone {
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
background-color: #f8fafc;
cursor: pointer;
border-color: #e2e8f0 !important;
}
.upload-zone:hover, .upload-zone.dragover {
border-color: var(--primary-color) !important;
background-color: #f0f7ff;
transform: scale(1.01);
}
.cursor-pointer {
cursor: pointer;
}
</style>
<script>
const fileInput = document.getElementById('pdf_file');
const dropZone = document.getElementById('drop-zone');
const form = document.getElementById('auto-upload-form');
const loadingSpinner = document.getElementById('loading-spinner');
const uploadPrompt = document.getElementById('upload-prompt');
const actionButtons = document.getElementById('action-buttons');
function startUpload() {
if (fileInput.files.length > 0) {
loadingSpinner.classList.remove('d-none');
uploadPrompt.classList.add('d-none');
dropZone.classList.add('border-0', 'bg-white');
dropZone.style.pointerEvents = 'none';
actionButtons.classList.add('d-none');
form.submit();
}
}
fileInput.onchange = startUpload;
dropZone.onclick = (e) => {
if (e.target.tagName !== 'INPUT') {
fileInput.click();
}
};
dropZone.ondragover = (e) => {
e.preventDefault();
dropZone.classList.add('dragover');
};
dropZone.ondragleave = () => {
dropZone.classList.remove('dragover');
};
dropZone.ondrop = (e) => {
e.preventDefault();
dropZone.classList.remove('dragover');
if (e.dataTransfer.files.length > 0) {
const files = e.dataTransfer.files;
if (files[0].type === 'application/pdf') {
fileInput.files = files;
startUpload();
} else {
alert('Lütfen sadece PDF dosyası yükleyin.');
}
}
};
</script>
{% endblock %}

View File

@ -0,0 +1,76 @@
{% extends "base.html" %}
{% block title %}Fatura Arşivi - FaturaYol{% endblock %}
{% block page_title %}{{ firma.ad }}{% endblock %}
{% block content %}
<nav aria-label="breadcrumb" class="mb-4">
<ol class="breadcrumb">
<li class="breadcrumb-item"><a href="{% url 'fatura_arsivi' %}" class="text-decoration-none">Arşiv</a></li>
<li class="breadcrumb-item active" aria-current="page">{{ firma.ad }}</li>
</ol>
</nav>
<div class="card border-0 shadow-sm mb-4">
<div class="card-header bg-white py-3">
<div class="d-flex justify-content-between align-items-center">
<h5 class="mb-0 fw-bold"><i class="bi bi-files me-2 text-primary"></i>Tüm Faturalar</h5>
<div class="btn-group">
<a href="{% url 'fatura_otomatik_yukle' %}" class="btn btn-sm btn-primary rounded-pill px-3"><i class="bi bi-magic me-1"></i> Yeni PDF Yükle (Otomatik)</a>
</div>
</div>
</div>
<div class="table-responsive">
<table class="table table-hover align-middle mb-0">
<thead>
<tr>
<th class="ps-4">Fatura No</th>
<th>Tarih</th>
<th>Ara Toplam</th>
<th>KDV</th>
<th>Genel Toplam</th>
<th>Durum</th>
<th class="text-end pe-4">İşlemler</th>
</tr>
</thead>
<tbody>
{% for fatura in faturalar %}
<tr>
<td class="ps-4 fw-medium">{{ fatura.fatura_no }}</td>
<td>{{ fatura.tarih|date:"d.m.Y" }}</td>
<td>{{ fatura.ara_toplam }} ₺</td>
<td>{{ fatura.kdv_toplam }} ₺</td>
<td class="fw-bold text-primary">{{ fatura.genel_toplam }} ₺</td>
<td>
{% if fatura.islenmis %}
<span class="badge badge-soft-success px-2 py-1 rounded-pill">İşlendi</span>
{% else %}
<span class="badge bg-light text-dark px-2 py-1 rounded-pill border">Bekliyor</span>
{% endif %}
</td>
<td class="text-end pe-4">
<a href="{% url 'fatura_detay' fatura.pk %}" class="btn btn-sm btn-light rounded-circle me-1" title="Görüntüle">
<i class="bi bi-eye"></i>
</a>
{% if fatura.pdf_dosyasi %}
<a href="{{ fatura.pdf_dosyasi.url }}" target="_blank" class="btn btn-sm btn-light rounded-circle me-1" title="PDF İndir">
<i class="bi bi-download"></i>
</a>
{% endif %}
<a href="{% url 'fatura_sil' fatura.pk %}" class="btn btn-sm btn-light text-danger rounded-circle" title="Faturayı Sil">
<i class="bi bi-trash"></i>
</a>
</td>
</tr>
{% empty %}
<tr>
<td colspan="7" class="text-center py-5 text-muted">
<p class="mb-0">Bu firmaya ait henüz fatura bulunamadı.</p>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
{% endblock %}

View File

@ -0,0 +1,37 @@
{% extends "base.html" %}
{% block title %}{{ title }} - FaturaYol{% endblock %}
{% block page_title %}{{ title }}{% endblock %}
{% block content %}
<div class="row justify-content-center">
<div class="col-md-8">
<div class="card border-0 shadow-sm">
<div class="card-body p-4">
<form method="post">
{% csrf_token %}
<div class="mb-4">
<h5 class="fw-bold mb-3">Firma Bilgileri</h5>
{% for field in form %}
<div class="mb-3">
<label class="form-label fw-medium">{{ field.label }}</label>
{{ field }}
{% if field.help_text %}
<div class="form-text">{{ field.help_text }}</div>
{% endif %}
{% for error in field.errors %}
<div class="text-danger small">{{ error }}</div>
{% endfor %}
</div>
{% endfor %}
</div>
<div class="d-flex justify-content-between align-items-center mt-4">
<a href="{% url 'fatura_arsivi' %}" class="btn btn-light rounded-pill px-4">İptal</a>
<button type="submit" class="btn btn-primary rounded-pill px-5">Kaydet</button>
</div>
</form>
</div>
</div>
</div>
</div>
{% endblock %}

View File

@ -1,145 +1,149 @@
{% extends "base.html" %} {% extends "base.html" %}
{% block title %}{{ project_name }}{% endblock %} {% block title %}Gösterge Paneli | FaturaYol{% endblock %}
{% block head %}
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;700&display=swap" rel="stylesheet">
<style>
:root {
--bg-color-start: #6a11cb;
--bg-color-end: #2575fc;
--text-color: #ffffff;
--card-bg-color: rgba(255, 255, 255, 0.01);
--card-border-color: rgba(255, 255, 255, 0.1);
}
* {
box-sizing: border-box;
}
body {
margin: 0;
font-family: 'Inter', sans-serif;
background: linear-gradient(45deg, var(--bg-color-start), var(--bg-color-end));
color: var(--text-color);
display: flex;
justify-content: center;
align-items: center;
min-height: 100vh;
text-align: center;
overflow: hidden;
position: relative;
}
body::before {
content: '';
position: absolute;
inset: 0;
background-image: url("data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' width='100' height='100' viewBox='0 0 100 100'><path d='M-10 10L110 10M10 -10L10 110' stroke-width='1' stroke='rgba(255,255,255,0.05)'/></svg>");
animation: bg-pan 20s linear infinite;
z-index: -1;
}
@keyframes bg-pan {
0% {
background-position: 0% 0%;
}
100% {
background-position: 100% 100%;
}
}
main {
padding: 2rem;
}
.card {
background: var(--card-bg-color);
border: 1px solid var(--card-border-color);
border-radius: 16px;
padding: 2.5rem 2rem;
backdrop-filter: blur(20px);
-webkit-backdrop-filter: blur(20px);
box-shadow: 0 12px 36px rgba(0, 0, 0, 0.25);
}
h1 {
font-size: clamp(2.2rem, 3vw + 1.2rem, 3.2rem);
font-weight: 700;
margin: 0 0 1.2rem;
letter-spacing: -0.02em;
}
p {
margin: 0.5rem 0;
font-size: 1.1rem;
opacity: 0.92;
}
.loader {
margin: 1.5rem auto;
width: 56px;
height: 56px;
border: 4px solid rgba(255, 255, 255, 0.25);
border-top-color: #fff;
border-radius: 50%;
animation: spin 1s linear infinite;
}
@keyframes spin {
to {
transform: rotate(360deg);
}
}
.runtime code {
background: rgba(0, 0, 0, 0.25);
padding: 0.15rem 0.45rem;
border-radius: 4px;
font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
}
.sr-only {
position: absolute;
width: 1px;
height: 1px;
padding: 0;
margin: -1px;
overflow: hidden;
clip: rect(0, 0, 0, 0);
border: 0;
}
footer {
position: absolute;
bottom: 1rem;
width: 100%;
text-align: center;
font-size: 0.85rem;
opacity: 0.75;
}
</style>
{% endblock %}
{% block content %} {% block content %}
<main> <div class="row">
<div class="card"> <div class="col-12 mb-4">
<h1>Analyzing your requirements and generating your app…</h1> <h2 class="fw-bold">Gösterge Paneli</h2>
<div class="loader" role="status" aria-live="polite" aria-label="Applying initial changes"> <p class="text-muted">Fatura yönetim sistemine genel bakış ve hızlı istatistikler.</p>
<span class="sr-only">Loading…</span>
</div> </div>
<p class="hint">AppWizzy AI is collecting your requirements and applying the first changes.</p> </div>
<p class="hint">This page will refresh automatically as the plan is implemented.</p>
<p class="runtime"> <!-- Stats Cards -->
Runtime: Django <code>{{ django_version }}</code> · Python <code>{{ python_version }}</code> <div class="row mb-4">
— UTC <code>{{ current_time|date:"Y-m-d H:i:s" }}</code> <div class="col-md-4">
</p> <div class="card stat-card border-0 shadow-sm">
</div> <div class="card-body d-flex align-items-center p-4">
</main> <div class="bg-primary-soft stat-icon me-3" style="width: 50px; height: 50px; border-radius: 12px; display: flex; align-items: center; justify-content: center; background: #dbeafe; color: #2563eb;">
<footer> <i class="bi bi-building" style="font-size: 1.5rem;"></i>
Page updated: {{ current_time|date:"Y-m-d H:i:s" }} (UTC) </div>
</footer> <div>
<h6 class="text-muted mb-1 small text-uppercase fw-bold">Toplam Firma</h6>
<h3 class="fw-bold mb-0 text-primary">{{ toplam_firma }}</h3>
</div>
</div>
</div>
</div>
<div class="col-md-4">
<div class="card stat-card border-0 shadow-sm">
<div class="card-body d-flex align-items-center p-4">
<div class="bg-success-soft stat-icon me-3" style="width: 50px; height: 50px; border-radius: 12px; display: flex; align-items: center; justify-content: center; background: #dcfce7; color: #15803d;">
<i class="bi bi-file-earmark-text" style="font-size: 1.5rem;"></i>
</div>
<div>
<h6 class="text-muted mb-1 small text-uppercase fw-bold">Toplam Fatura</h6>
<h3 class="fw-bold mb-0 text-success">{{ toplam_fatura }}</h3>
</div>
</div>
</div>
</div>
<div class="col-md-4">
<div class="card stat-card border-0 shadow-sm">
<div class="card-body d-flex align-items-center p-4">
<div class="bg-warning-soft stat-icon me-3" style="width: 50px; height: 50px; border-radius: 12px; display: flex; align-items: center; justify-content: center; background: #fef9c3; color: #a16207;">
<i class="bi bi-cash-stack" style="font-size: 1.5rem;"></i>
</div>
<div>
<h6 class="text-muted mb-1 small text-uppercase fw-bold">Toplam Harcama</h6>
<h3 class="fw-bold mb-0 text-warning">₺{{ toplam_harcama|floatformat:2 }}</h3>
</div>
</div>
</div>
</div>
</div>
<div class="row mt-2">
<!-- Recent Invoices Table -->
<div class="col-lg-8">
<div class="card border-0 shadow-sm h-100">
<div class="card-header bg-white py-3 d-flex align-items-center justify-content-between border-0">
<h5 class="mb-0 fw-bold"><i class="bi bi-clock-history me-2 text-primary"></i>Son Eklenen Faturalar</h5>
<a href="{% url 'fatura_arsivi' %}" class="btn btn-sm btn-link text-decoration-none">Tümünü Gör <i class="bi bi-arrow-right"></i></a>
</div>
<div class="card-body p-0">
<div class="table-responsive">
<table class="table table-hover align-middle mb-0">
<thead>
<tr class="bg-light">
<th class="ps-4 border-0">Fatura No</th>
<th class="border-0">Firma</th>
<th class="border-0">Tarih</th>
<th class="border-0">Toplam</th>
<th class="text-end pe-4 border-0">İşlem</th>
</tr>
</thead>
<tbody>
{% for fatura in son_faturalar %}
<tr>
<td class="ps-4 fw-medium text-dark">{{ fatura.fatura_no }}</td>
<td>
<span class="badge bg-light text-dark fw-medium border px-2 py-1">{{ fatura.firma.ad }}</span>
</td>
<td class="text-muted">{{ fatura.tarih|date:"d.m.Y" }}</td>
<td class="fw-bold text-primary">₺{{ fatura.genel_toplam }}</td>
<td class="text-end pe-4">
<a href="{% url 'fatura_detay' fatura.pk %}" class="btn btn-sm btn-light rounded-pill">
<i class="bi bi-eye"></i> İncele
</a>
</td>
</tr>
{% empty %}
<tr>
<td colspan="5" class="text-center py-5">
<div class="opacity-50 mb-3">
<i class="bi bi-file-earmark-plus" style="font-size: 3rem;"></i>
</div>
<p class="text-muted mb-0">Henüz fatura eklenmemiş.</p>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
</div>
</div>
<!-- Quick Actions -->
<div class="col-lg-4">
<div class="card border-0 shadow-sm h-100">
<div class="card-header bg-white py-3 border-0">
<h5 class="mb-0 fw-bold"><i class="bi bi-lightning-fill me-2 text-warning"></i>Hızlı İşlemler</h5>
</div>
<div class="card-body">
<div class="d-grid gap-3">
<a href="{% url 'fatura_otomatik_yukle' %}" class="btn btn-primary text-start p-3 rounded-4 d-flex align-items-center shadow-sm border-0 text-decoration-none" style="background: linear-gradient(135deg, #2563eb 0%, #1d4ed8 100%);">
<div class="bg-white bg-opacity-25 rounded-3 p-2 me-3">
<i class="bi bi-magic fs-4"></i>
</div>
<div>
<div class="fw-bold text-white">Fatura Yükle</div>
<div class="small text-white opacity-75">Yapay zeka ile otomatik analiz</div>
</div>
</a>
<a href="{% url 'firma_ekle' %}" class="btn btn-outline-light text-dark text-start p-3 rounded-4 d-flex align-items-center border bg-light bg-opacity-50 text-decoration-none">
<div class="bg-white rounded-3 p-2 me-3 shadow-sm text-primary">
<i class="bi bi-folder-plus fs-4"></i>
</div>
<div>
<div class="fw-bold text-dark">Yeni Firma Ekle</div>
<div class="small text-muted">Sisteme yeni tedarikçi kaydet</div>
</div>
</a>
<a href="{% url 'fatura_ekle' %}" class="btn btn-link text-muted text-center small mt-2">
Manuel fatura yükle (Eski yöntem)
</a>
</div>
<div class="mt-4 p-4 rounded-4 bg-primary bg-opacity-10 border border-primary border-opacity-10">
<h6 class="fw-bold text-primary mb-2"><i class="bi bi-info-circle-fill me-2"></i>Akıllı Analiz</h6>
<p class="small text-muted mb-0">Sistemimiz Mersis, VKN ve TCKN bilgilerini otomatik tanıyarak firmaları eşleştirir.</p>
</div>
</div>
</div>
</div>
</div>
{% endblock %} {% endblock %}

View File

@ -0,0 +1,68 @@
{% extends "base.html" %}
{% block title %}İstatistik & Raporlar - FaturaYol{% endblock %}
{% block page_title %}İstatistik & Raporlar{% endblock %}
{% block content %}
<div class="row mb-4">
<div class="col-md-6">
<div class="card h-100 border-0 shadow-sm">
<div class="card-body">
<h5 class="fw-bold mb-4">Aylık Harcama Trendi</h5>
<div class="table-responsive">
<table class="table table-sm">
<thead>
<tr>
<th>Ay</th>
<th>Fatura Adedi</th>
<th class="text-end">Toplam Tutar</th>
</tr>
</thead>
<tbody>
{% for harcama in aylik_harcama %}
<tr>
<td>{{ harcama.tarih__month }}. Ay</td>
<td>{{ harcama.adet }}</td>
<td class="text-end fw-bold">{{ harcama.toplam }} ₺</td>
</tr>
{% empty %}
<tr>
<td colspan="3" class="text-center py-4">Veri bulunamadı</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
</div>
</div>
<div class="col-md-6">
<div class="card h-100 border-0 shadow-sm">
<div class="card-body">
<h5 class="fw-bold mb-4">KDV Özeti</h5>
<div class="p-4 bg-light rounded-4 text-center mb-4">
<small class="text-muted d-block mb-1">Toplam Ödenen KDV</small>
<h2 class="fw-bold text-success mb-0">{{ kdv_ozet.toplam_kdv|default:0 }} ₺</h2>
</div>
<div class="p-4 bg-light rounded-4 text-center">
<small class="text-muted d-block mb-1">Fatura Başı Ortalama KDV</small>
<h2 class="fw-bold text-primary mb-0">{{ kdv_ozet.ortalama_kdv|default:0|floatformat:2 }} ₺</h2>
</div>
</div>
</div>
</div>
</div>
<div class="row">
<div class="col-12">
<div class="card border-0 shadow-sm">
<div class="card-body py-5 text-center">
<i class="bi bi-graph-up-arrow text-primary opacity-25" style="font-size: 4rem;"></i>
<h4 class="mt-3 fw-bold">Gelişmiş Raporlama Yakında</h4>
<p class="text-muted">Grafikler, Excel dışa aktarma ve kategori bazlı analizler bir sonraki güncellemede eklenecektir.</p>
</div>
</div>
</div>
</div>
{% endblock %}

View File

@ -0,0 +1,61 @@
{% extends "base.html" %}
{% block title %}Arama Sonuçları - FaturaYol{% endblock %}
{% block page_title %}"{{ query }}" için Arama Sonuçları{% endblock %}
{% block content %}
<div class="mb-5">
<h5 class="fw-bold mb-3"><i class="bi bi-building me-2 text-primary"></i>Firmalar ({{ firmalar|length }})</h5>
<div class="row">
{% for firma in firmalar %}
<div class="col-md-4 mb-3">
<div class="card border-0 shadow-sm">
<div class="card-body">
<h6 class="fw-bold mb-1">{{ firma.ad }}</h6>
<p class="small text-muted mb-2">VN: {{ firma.vergi_no }}</p>
<a href="{% url 'firma_detay' firma.pk %}" class="btn btn-sm btn-outline-primary rounded-pill">Detaya Git</a>
</div>
</div>
</div>
{% empty %}
<div class="col-12 text-muted">Eşleşen firma bulunamadı.</div>
{% endfor %}
</div>
</div>
<div>
<h5 class="fw-bold mb-3"><i class="bi bi-file-earmark-text me-2 text-primary"></i>Faturalar ({{ faturalar|length }})</h5>
<div class="card border-0 shadow-sm">
<div class="table-responsive">
<table class="table table-hover align-middle mb-0">
<thead>
<tr>
<th class="ps-4">Fatura No</th>
<th>Firma</th>
<th>Tarih</th>
<th>Genel Toplam</th>
<th class="text-end pe-4">İşlem</th>
</tr>
</thead>
<tbody>
{% for fatura in faturalar %}
<tr>
<td class="ps-4 fw-medium">{{ fatura.fatura_no }}</td>
<td>{{ fatura.firma.ad }}</td>
<td>{{ fatura.tarih|date:"d.m.Y" }}</td>
<td class="fw-bold">₺{{ fatura.genel_toplam }}</td>
<td class="text-end pe-4">
<a href="{% url 'fatura_detay' fatura.pk %}" class="btn btn-sm btn-light rounded-pill">İncele</a>
</td>
</tr>
{% empty %}
<tr>
<td colspan="5" class="text-center py-4 text-muted">Eşleşen fatura bulunamadı.</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
</div>
{% endblock %}

View File

@ -1,7 +1,19 @@
from django.urls import path from django.urls import path
from .views import (
from .views import home home, fatura_arsivi, firma_detay, fatura_detay, raporlar,
firma_ekle, fatura_ekle, fatura_otomatik_yukle, search, firma_sil, fatura_sil
)
urlpatterns = [ urlpatterns = [
path("", home, name="home"), path("", home, name="home"),
path("arsiv/", fatura_arsivi, name="fatura_arsivi"),
path("firma/<int:pk>/", firma_detay, name="firma_detay"),
path("fatura/<int:pk>/", fatura_detay, name="fatura_detay"),
path("raporlar/", raporlar, name="raporlar"),
path("firma/ekle/", firma_ekle, name="firma_ekle"),
path("fatura/ekle/", fatura_ekle, name="fatura_ekle"),
path("fatura/otomatik-yukle/", fatura_otomatik_yukle, name="fatura_otomatik_yukle"),
path("arama/", search, name="search"),
path("firma/<int:pk>/sil/", firma_sil, name="firma_sil"),
path("fatura/<int:pk>/sil/", fatura_sil, name="fatura_sil"),
] ]

75
core/utils.py Normal file
View File

@ -0,0 +1,75 @@
import os
from pypdf import PdfReader
from ai.local_ai_api import LocalAIApi
import json
def extract_text_from_pdf(pdf_path):
try:
reader = PdfReader(pdf_path)
text = ""
for page in reader.pages:
text += page.extract_text() + "\n"
if not text.strip():
# If no text extracted, maybe it's an image-based PDF
# In a real environment we would use OCR here
return None
return text
except Exception as e:
print(f"Error extracting text from PDF: {e}")
return None
def analyze_invoice_text(text):
prompt = f"""
Sen profesyonel bir fatura veri ayıklama sistemisin. Aşağıdaki fatura metnini analiz et ve bilgileri kesinlikle JSON formatında döndür.
Özellikle şu bilgileri bulmaya çalış:
- Satıcı Firma Bilgileri: Adı, Vergi Kimlik Numarası (VKN), MERSİS Numarası, Adresi.
- Fatura Bilgileri: Fatura No, Tarih, Ara Toplam, KDV Tutarı, Genel Toplam.
- Satır Kalemleri: Her bir ürün veya hizmetin adı, adedi, birim fiyatı, KDV oranı, KDV tutarı ve toplam tutarı.
Dikkat:
- Vergi numarası 10 haneli (VKN) veya 11 haneli (TCKN) olabilir.
- MERSİS no genellikle 16 hanelidir.
- Sayısal değerleri (tutarlar, adetler) sadece rakam ve nokta (ondalık için) olarak döndür.
- Tarihi YYYY-MM-DD formatına çevir.
- Eğer bir veri bulunamazsa null döndür.
İstenen JSON formatı:
{{
"firma_adi": "...",
"vergi_no": "...",
"mersis_no": "...",
"adres": "...",
"fatura_no": "...",
"tarih": "YYYY-MM-DD",
"ara_toplam": 0.00,
"kdv_toplam": 0.00,
"genel_toplam": 0.00,
"kalemler": [
{{
"urun_adi": "...",
"adet": 1,
"birim_fiyat": 0.00,
"kdv_orani": 20,
"kdv_tutari": 0.00,
"toplam_tutar": 0.00
}}
]
}}
Fatura Metni:
{text}
"""
response = LocalAIApi.create_response({
"input": [
{"role": "system", "content": "Sen sadece JSON döndüren, hata payı düşük bir fatura analiz uzmanısın. Yanıtında asla JSON dışında metin bulundurma."},
{"role": "user", "content": prompt},
],
"text": {"format": {"type": "json_object"}},
})
if response.get("success"):
return LocalAIApi.decode_json_from_response(response)
return None

View File

@ -1,25 +1,253 @@
from django.shortcuts import render, get_object_or_404, redirect
from django.db.models import Sum, Count, Avg, Q
from django.db import transaction
from django.core.files.storage import default_storage
from django.contrib import messages
from .models import Firma, Fatura, FaturaKalemi
from .forms import FirmaForm, FaturaForm
from .utils import extract_text_from_pdf, analyze_invoice_text
import os import os
import platform
from django import get_version as django_version
from django.shortcuts import render
from django.utils import timezone
def home(request): def home(request):
"""Render the landing screen with loader and environment details.""" """Fatura Yönetimi Gösterge Paneli."""
host_name = request.get_host().lower() toplam_firma = Firma.objects.count()
agent_brand = "AppWizzy" if host_name == "appwizzy.com" else "Flatlogic" toplam_fatura = Fatura.objects.count()
now = timezone.now() toplam_harcama = Fatura.objects.aggregate(Sum('genel_toplam'))['genel_toplam__sum'] or 0
son_faturalar = Fatura.objects.select_related('firma').all().order_by('-olusturulma_tarihi')[:10]
context = { context = {
"project_name": "New Style", "toplam_firma": toplam_firma,
"agent_brand": agent_brand, "toplam_fatura": toplam_fatura,
"django_version": django_version(), "toplam_harcama": toplam_harcama,
"python_version": platform.python_version(), "son_faturalar": son_faturalar,
"current_time": now, "active_menu": "dashboard"
"host_name": host_name,
"project_description": os.getenv("PROJECT_DESCRIPTION", ""),
"project_image_url": os.getenv("PROJECT_IMAGE_URL", ""),
} }
return render(request, "core/index.html", context) return render(request, "core/index.html", context)
def fatura_arsivi(request):
"""Firmalara göre gruplanmış fatura arşivi."""
firmalar = Firma.objects.annotate(
fatura_sayisi=Count('faturalar'),
toplam_tutar=Sum('faturalar__genel_toplam')
).order_by('ad')
context = {
"firmalar": firmalar,
"active_menu": "archive"
}
return render(request, "core/fatura_arsivi.html", context)
def firma_detay(request, pk):
"""Belirli bir firmanın fatura listesi."""
firma = get_object_or_404(Firma, pk=pk)
faturalar = firma.faturalar.all().order_by('-tarih')
context = {
"firma": firma,
"faturalar": faturalar,
"active_menu": "archive"
}
return render(request, "core/firma_detay.html", context)
def fatura_detay(request, pk):
"""Fatura detay önizleme ve kalemleri."""
fatura = get_object_or_404(Fatura.objects.select_related('firma'), pk=pk)
kalemler = fatura.kalemler.all()
context = {
"fatura": fatura,
"kalemler": kalemler,
"active_menu": "archive"
}
return render(request, "core/fatura_detay.html", context)
def raporlar(request):
"""İstatistik ve Raporlar."""
# Aylık harcama trendi
aylik_harcama = Fatura.objects.values('tarih__month').annotate(
toplam=Sum('genel_toplam'),
adet=Count('id')
).order_by('tarih__month')
# KDV dağılımı
kdv_ozet = Fatura.objects.aggregate(
toplam_kdv=Sum('kdv_toplam'),
ortalama_kdv=Avg('kdv_toplam')
)
context = {
"aylik_harcama": aylik_harcama,
"kdv_ozet": kdv_ozet,
"active_menu": "reports"
}
return render(request, "core/raporlar.html", context)
def firma_ekle(request):
if request.method == 'POST':
form = FirmaForm(request.POST)
if form.is_valid():
form.save()
return redirect('fatura_arsivi')
else:
form = FirmaForm()
return render(request, 'core/firma_form.html', {'form': form, 'title': 'Yeni Firma Ekle'})
def fatura_ekle(request):
firma_id = request.GET.get('firma')
initial = {}
if firma_id:
initial['firma'] = firma_id
if request.method == 'POST':
form = FaturaForm(request.POST, request.FILES)
if form.is_valid():
fatura = form.save()
return redirect('fatura_detay', pk=fatura.pk)
else:
form = FaturaForm(initial=initial)
return render(request, 'core/fatura_form.html', {'form': form, 'title': 'Yeni Fatura Yükle'})
def fatura_otomatik_yukle(request):
if request.method == 'POST' and request.FILES.get('pdf_dosyasi'):
pdf_file = request.FILES['pdf_dosyasi']
# Save temporary file to extract text
temp_name = 'temp_' + pdf_file.name
path = default_storage.save('temp/' + temp_name, pdf_file)
full_path = default_storage.path(path)
try:
text = extract_text_from_pdf(full_path)
if not text:
messages.error(request, "PDF dosyasından metin okunamadı. Dosya taranmış bir resim olabilir veya şifreli olabilir.")
else:
data = analyze_invoice_text(text)
if not data:
messages.error(request, "Yapay zeka faturayı analiz edemedi. Lütfen dosyanın geçerli bir fatura olduğundan emin olun.")
else:
with transaction.atomic():
# Smarter Firma matching
vergi_no = str(data.get('vergi_no', '')).strip()
mersis_no = str(data.get('mersis_no', '')).strip()
firma_adi = data.get('firma_adi', '').strip() or 'Bilinmeyen Firma'
firma = None
# 1. Try by Vergi No / TCKN
if vergi_no and vergi_no != 'None' and vergi_no != 'null':
firma = Firma.objects.filter(vergi_no=vergi_no).first()
# 2. Try by Mersis No
if not firma and mersis_no and mersis_no != 'None' and mersis_no != 'null':
firma = Firma.objects.filter(mersis_no=mersis_no).first()
# 3. Try by Name (Exact)
if not firma and firma_adi != 'Bilinmeyen Firma':
firma = Firma.objects.filter(ad__iexact=firma_adi).first()
# 4. Create if not found
if not firma:
# Ensure we have a unique vergi_no even if AI failed
if not vergi_no or vergi_no == 'None' or vergi_no == 'null':
vergi_no = f"AUTO-{os.urandom(4).hex()}"
firma = Firma.objects.create(
ad=firma_adi,
vergi_no=vergi_no,
mersis_no=mersis_no if (mersis_no != 'None' and mersis_no != 'null') else None,
adres=data.get('adres', '')
)
else:
# Update missing info if found existing firma
updated = False
if not firma.mersis_no and mersis_no and mersis_no != 'None' and mersis_no != 'null':
firma.mersis_no = mersis_no
updated = True
if not firma.adres and data.get('adres'):
firma.adres = data.get('adres')
updated = True
if updated:
firma.save()
# Create Fatura
fatura_no = data.get('fatura_no') or f"AUTO-{os.urandom(4).hex()}"
# Check if this invoice already exists for this firm
fatura = Fatura.objects.filter(fatura_no=fatura_no, firma=firma).first()
if not fatura:
fatura = Fatura.objects.create(
firma=firma,
fatura_no=fatura_no,
tarih=data.get('tarih') or '2026-01-01',
ara_toplam=data.get('ara_toplam') or 0,
kdv_toplam=data.get('kdv_toplam') or 0,
genel_toplam=data.get('genel_toplam') or 0,
pdf_dosyasi=pdf_file,
islenmis=True
)
# Add Items
kalemler = data.get('kalemler', [])
if isinstance(kalemler, list):
for k in kalemler:
FaturaKalemi.objects.create(
fatura=fatura,
urun_adi=k.get('urun_adi') or 'Ürün',
adet=k.get('adet') or 1,
birim_fiyat=k.get('birim_fiyat') or 0,
kdv_orani=k.get('kdv_orani') or 20,
kdv_tutari=k.get('kdv_tutari') or 0,
toplam_tutar=k.get('toplam_tutar') or 0
)
# Clean up temp file
if default_storage.exists(path):
default_storage.delete(path)
messages.success(request, f"Fatura başarıyla analiz edildi ve {firma.ad} firmasına eklendi.")
return redirect('fatura_detay', pk=fatura.pk)
except Exception as e:
messages.error(request, f"Fatura işlenirken bir hata oluştu: {str(e)}")
print(f"Error processing automatic invoice: {e}")
finally:
# Clean up temp file
if default_storage.exists(path):
default_storage.delete(path)
return render(request, 'core/fatura_form_otomatik.html', {'title': 'Otomatik Fatura Yükle', 'active_menu': 'dashboard'})
def search(request):
query = request.GET.get('q', '')
faturalar = []
firmalar = []
if query:
faturalar = Fatura.objects.filter(
Q(fatura_no__icontains=query) |
Q(firma__ad__icontains=query)
).select_related('firma')
firmalar = Firma.objects.filter(
Q(ad__icontains=query) |
Q(vergi_no__icontains=query)
)
context = {
'query': query,
'faturalar': faturalar,
'firmalar': firmalar,
}
return render(request, 'core/search_results.html', context)
def firma_sil(request, pk):
firma = get_object_or_404(Firma, pk=pk)
if request.method == 'POST':
firma.delete()
return redirect('fatura_arsivi')
return render(request, 'core/confirm_delete.html', {'object': firma, 'type': 'Firma'})
def fatura_sil(request, pk):
fatura = get_object_or_404(Fatura, pk=pk)
firma_pk = fatura.firma.pk
if request.method == 'POST':
fatura.delete()
return redirect('firma_detay', pk=firma_pk)
return render(request, 'core/confirm_delete.html', {'object': fatura, 'type': 'Fatura'})