├── .dockerignore ├── .flake8 ├── .gitignore ├── Dockerfile ├── README.md ├── ai ├── __init__.py ├── admin.py ├── agent.py ├── apps.py ├── management │ ├── __init__.py │ └── commands │ │ ├── __init__.py │ │ └── sge_agent_invoke.py ├── migrations │ ├── 0001_initial.py │ └── __init__.py ├── models.py └── prompts.py ├── app ├── __init__.py ├── asgi.py ├── metrics.py ├── settings.py ├── templates │ ├── base.html │ ├── components │ │ ├── _ai_result.html │ │ ├── _footer.html │ │ ├── _header.html │ │ ├── _pagination.html │ │ ├── _product_metrics.html │ │ ├── _sales_metrics.html │ │ └── _sidebar.html │ ├── home.html │ └── registration │ │ └── login.html ├── urls.py ├── views.py └── wsgi.py ├── authentication ├── __init__.py ├── apps.py ├── migrations │ └── __init__.py └── urls.py ├── brands ├── __init__.py ├── admin.py ├── apps.py ├── forms.py ├── migrations │ ├── 0001_initial.py │ └── __init__.py ├── models.py ├── serializers.py ├── templates │ ├── brand_create.html │ ├── brand_delete.html │ ├── brand_detail.html │ ├── brand_list.html │ └── brand_update.html ├── urls.py └── views.py ├── categories ├── __init__.py ├── admin.py ├── apps.py ├── forms.py ├── migrations │ ├── 0001_initial.py │ └── __init__.py ├── models.py ├── serializers.py ├── templates │ ├── category_create.html │ ├── category_delete.html │ ├── category_detail.html │ ├── category_list.html │ └── category_update.html ├── urls.py └── views.py ├── cron ├── docker-compose.yml ├── inflows ├── __init__.py ├── admin.py ├── apps.py ├── forms.py ├── migrations │ ├── 0001_initial.py │ └── __init__.py ├── models.py ├── serializers.py ├── signals.py ├── templates │ ├── inflow_create.html │ ├── inflow_detail.html │ └── inflow_list.html ├── urls.py └── views.py ├── manage.py ├── outflows ├── __init__.py ├── admin.py ├── apps.py ├── forms.py ├── migrations │ ├── 0001_initial.py │ └── __init__.py ├── models.py ├── serializers.py ├── signals.py ├── templates │ ├── outflow_create.html │ ├── outflow_detail.html │ └── outflow_list.html ├── urls.py └── views.py ├── products ├── __init__.py ├── admin.py ├── apps.py ├── forms.py ├── management │ ├── __init__.py │ └── commands │ │ ├── __init__.py │ │ └── fazer_coisas.py ├── migrations │ ├── 0001_initial.py │ └── __init__.py ├── models.py ├── serializers.py ├── templates │ ├── product_create.html │ ├── product_delete.html │ ├── product_detail.html │ ├── product_list.html │ └── product_update.html ├── urls.py └── views.py ├── requirements.txt ├── requirements_dev.txt └── suppliers ├── __init__.py ├── admin.py ├── apps.py ├── forms.py ├── migrations ├── 0001_initial.py └── __init__.py ├── models.py ├── serializers.py ├── templates ├── supplier_create.html ├── supplier_delete.html ├── supplier_detail.html ├── supplier_list.html └── supplier_update.html ├── urls.py └── views.py /.dockerignore: -------------------------------------------------------------------------------- 1 | .venv 2 | .git 3 | .gitignore 4 | venv 5 | __pycache__ 6 | -------------------------------------------------------------------------------- /.flake8: -------------------------------------------------------------------------------- 1 | [flake8] 2 | exclude = venv 3 | ignore = E501 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | venv 2 | db.sqlite3 3 | media 4 | __pycache__/ 5 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:3.11-slim 2 | 3 | WORKDIR /sge 4 | 5 | ENV PYTHONDONTWRITEBYTECODE 1 6 | ENV PYTHONUNBUFFERED 1 7 | 8 | RUN apt update && apt -y install cron && apt -y install nano 9 | 10 | COPY . . 11 | 12 | RUN pip install --upgrade pip 13 | RUN pip install -r requirements.txt 14 | 15 | COPY ./cron /etc/cron.d/cron 16 | RUN chmod 0644 /etc/cron.d/cron 17 | RUN crontab /etc/cron.d/cron 18 | 19 | EXPOSE 8000 20 | 21 | CMD cron ; python manage.py migrate && python manage.py runserver 0.0.0.0:8000 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Sistema de Gestão de Estoque (SGE) 2 | 3 | Bem-vindo ao Sistema de Gestão de Estoque (SGE), um projeto desenvolvido em Django e Bootstrap 5 para facilitar o gerenciamento de estoque. Este README fornece informações essenciais sobre como configurar e executar o projeto em seu ambiente local. 4 | 5 | ## Requisitos 6 | 7 | Certifique-se de que você tenha os seguintes requisitos instalados em seu sistema: 8 | 9 | - Python (versão recomendada: 3.7 ou superior) 10 | - Django (instalado automaticamente ao seguir as instruções abaixo) 11 | - Outras dependências listadas no arquivo `requirements.txt` 12 | 13 | 14 | ## Instalação das Dependências 15 | 16 | Com o ambiente virtual ativado, instale as dependências do projeto usando o comando: 17 | ```bash 18 | pip install -r requirements.txt 19 | ``` 20 | 21 | 22 | ## Rodar o projeto 23 | 24 | Após instalar as dependências, aplique as migrations no banco de dados com o comando: 25 | ```bash 26 | python manage.py migrate 27 | ``` 28 | 29 | Agora o projeto jã pode ser inicializado com o comando: 30 | ```bash 31 | python manage.py runserver 32 | ``` 33 | 34 | Após isso, o sistema estará pronto para ser acessado em: 35 | [http://localhost:8000](http://localhost:8000) 36 | -------------------------------------------------------------------------------- /ai/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pycodebr/sge/18d17f898802a3c67b3fbb0463757f0c182d1717/ai/__init__.py -------------------------------------------------------------------------------- /ai/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | from . import models 3 | 4 | 5 | class AIResultAdmin(admin.ModelAdmin): 6 | list_display = ('created_at', 'result',) 7 | 8 | 9 | admin.site.register(models.AIResult, AIResultAdmin) 10 | -------------------------------------------------------------------------------- /ai/agent.py: -------------------------------------------------------------------------------- 1 | import json 2 | from django.conf import settings 3 | from django.core import serializers 4 | from openai import OpenAI 5 | from ai import prompts, models 6 | from products.models import Product 7 | from outflows.models import Outflow 8 | 9 | 10 | class SGEAgent: 11 | 12 | def __init__(self): 13 | self.__client = OpenAI( 14 | api_key=settings.OPENAI_API_KEY, 15 | ) 16 | 17 | def __get_data(self): 18 | products = Product.objects.all() 19 | outflows = Outflow.objects.all() 20 | return json.dumps({ 21 | 'products': serializers.serialize('json', products), 22 | 'outflows': serializers.serialize('json', outflows), 23 | }) 24 | 25 | def invoke(self): 26 | response = self.__client.chat.completions.create( 27 | model=settings.OPENAI_MODEL, 28 | messages=[ 29 | { 30 | 'role': 'system', 31 | 'content': prompts.SYSTEM_PROMPT, 32 | }, 33 | { 34 | 'role': 'user', 35 | 'content': prompts.USER_PROMPT.replace('{{data}}', self.__get_data()), 36 | }, 37 | ], 38 | ) 39 | result = response.choices[0].message.content 40 | models.AIResult.objects.create(result=result) 41 | -------------------------------------------------------------------------------- /ai/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class AiConfig(AppConfig): 5 | default_auto_field = 'django.db.models.BigAutoField' 6 | name = 'ai' 7 | -------------------------------------------------------------------------------- /ai/management/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pycodebr/sge/18d17f898802a3c67b3fbb0463757f0c182d1717/ai/management/__init__.py -------------------------------------------------------------------------------- /ai/management/commands/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pycodebr/sge/18d17f898802a3c67b3fbb0463757f0c182d1717/ai/management/commands/__init__.py -------------------------------------------------------------------------------- /ai/management/commands/sge_agent_invoke.py: -------------------------------------------------------------------------------- 1 | from django.core.management.base import BaseCommand 2 | from ai.agent import SGEAgent 3 | 4 | 5 | class Command(BaseCommand): 6 | 7 | def handle(self, *args, **options): 8 | agent = SGEAgent() 9 | agent.invoke() 10 | 11 | self.stdout.write( 12 | self.style.SUCCESS('SGE AGENT INVOCADO COM SUCESSO!') 13 | ) 14 | -------------------------------------------------------------------------------- /ai/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 5.0.1 on 2024-08-04 14:21 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | initial = True 9 | 10 | dependencies = [ 11 | ] 12 | 13 | operations = [ 14 | migrations.CreateModel( 15 | name='AIResult', 16 | fields=[ 17 | ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 18 | ('created_at', models.DateTimeField(auto_now_add=True)), 19 | ('result', models.TextField(blank=True, null=True)), 20 | ], 21 | options={ 22 | 'ordering': ['-created_at'], 23 | }, 24 | ), 25 | ] 26 | -------------------------------------------------------------------------------- /ai/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pycodebr/sge/18d17f898802a3c67b3fbb0463757f0c182d1717/ai/migrations/__init__.py -------------------------------------------------------------------------------- /ai/models.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | 3 | 4 | class AIResult(models.Model): 5 | created_at = models.DateTimeField(auto_now_add=True) 6 | result = models.TextField(null=True, blank=True) 7 | 8 | class Meta: 9 | ordering = ['-created_at'] 10 | -------------------------------------------------------------------------------- /ai/prompts.py: -------------------------------------------------------------------------------- 1 | SYSTEM_PROMPT = ''' 2 | Você é um agente virtual especialista em gestão de estoque e vendas. 3 | Você deve gerar relatórios de insights sobre estoque de produtos baseado 4 | nos dados de um sistema de gestão de estoque feito em django que serão passados. 5 | Faça análises de reposição de produtos e também relatórios de saídas do estoque e valores. 6 | Dê respostas curtas, resumidas e diretas. Você irá gerar análises e sugestões diárias para 7 | os usuários do sistema. 8 | ''' 9 | 10 | USER_PROMPT = ''' 11 | Faça uma análise e dê sugestões com base nos dados atuais: 12 | {{data}} 13 | ''' 14 | -------------------------------------------------------------------------------- /app/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pycodebr/sge/18d17f898802a3c67b3fbb0463757f0c182d1717/app/__init__.py -------------------------------------------------------------------------------- /app/asgi.py: -------------------------------------------------------------------------------- 1 | """ 2 | ASGI config for app project. 3 | 4 | It exposes the ASGI callable as a module-level variable named ``application``. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/5.0/howto/deployment/asgi/ 8 | """ 9 | 10 | import os 11 | 12 | from django.core.asgi import get_asgi_application 13 | 14 | os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'app.settings') 15 | 16 | application = get_asgi_application() 17 | -------------------------------------------------------------------------------- /app/metrics.py: -------------------------------------------------------------------------------- 1 | from django.db.models import Sum, F 2 | from django.utils.formats import number_format 3 | from django.utils import timezone 4 | from brands.models import Brand 5 | from categories.models import Category 6 | from products.models import Product 7 | from outflows.models import Outflow 8 | 9 | 10 | def get_product_metrics(): 11 | products = Product.objects.all() 12 | total_cost_price = sum(product.cost_price * product.quantity for product in products) 13 | total_selling_price = sum(product.selling_price * product.quantity for product in products) 14 | total_quantity = sum(product.quantity for product in products) 15 | total_profit = total_selling_price - total_cost_price 16 | 17 | return dict( 18 | total_cost_price=number_format(total_cost_price, decimal_pos=2, force_grouping=True), 19 | total_selling_price=number_format(total_selling_price, decimal_pos=2, force_grouping=True), 20 | total_quantity=total_quantity, 21 | total_profit=number_format(total_profit, decimal_pos=2, force_grouping=True), 22 | ) 23 | 24 | 25 | def get_sales_metrics(): 26 | total_sales = Outflow.objects.count() 27 | total_products_sold = Outflow.objects.aggregate(total_products_sold=Sum('quantity'))['total_products_sold'] or 0 28 | total_sales_value = sum(outflow.quantity * outflow.product.selling_price for outflow in Outflow.objects.all()) 29 | total_sales_cost = sum(outflow.quantity * outflow.product.cost_price for outflow in Outflow.objects.all()) 30 | total_sales_profit = total_sales_value - total_sales_cost 31 | 32 | return dict( 33 | total_sales=total_sales, 34 | total_products_sold=total_products_sold, 35 | total_sales_value=number_format(total_sales_value, decimal_pos=2, force_grouping=True), 36 | total_sales_profit=number_format(total_sales_profit, decimal_pos=2, force_grouping=True), 37 | ) 38 | 39 | 40 | def get_daily_sales_data(): 41 | today = timezone.now().date() 42 | dates = [str(today - timezone.timedelta(days=i)) for i in range(6, -1, -1)] 43 | values = list() 44 | 45 | for date in dates: 46 | sales_total = Outflow.objects.filter( 47 | created_at__date=date 48 | ).aggregate( 49 | total_sales=Sum(F('product__selling_price') * F('quantity')) 50 | )['total_sales'] or 0 51 | values.append(float(sales_total)) 52 | 53 | return dict( 54 | dates=dates, 55 | values=values, 56 | ) 57 | 58 | 59 | def get_daily_sales_quantity_data(): 60 | today = timezone.now().date() 61 | dates = [str(today - timezone.timedelta(days=i)) for i in range(6, -1, -1)] 62 | quantities = list() 63 | 64 | for date in dates: 65 | sales_quantity = Outflow.objects.filter(created_at__date=date).count() 66 | quantities.append(sales_quantity) 67 | 68 | return dict( 69 | dates=dates, 70 | values=quantities, 71 | ) 72 | 73 | 74 | def get_graphic_product_category_metric(): 75 | categories = Category.objects.all() 76 | return {category.name: Product.objects.filter(category=category).count() for category in categories} 77 | 78 | 79 | def get_graphic_product_brand_metric(): 80 | brands = Brand.objects.all() 81 | return {brand.name: Product.objects.filter(brand=brand).count() for brand in brands} 82 | -------------------------------------------------------------------------------- /app/settings.py: -------------------------------------------------------------------------------- 1 | """ 2 | Django settings for app project. 3 | 4 | Generated by 'django-admin startproject' using Django 5.0.1. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/5.0/topics/settings/ 8 | 9 | For the full list of settings and their values, see 10 | https://docs.djangoproject.com/en/5.0/ref/settings/ 11 | """ 12 | 13 | from pathlib import Path 14 | from datetime import timedelta 15 | 16 | # Build paths inside the project like this: BASE_DIR / 'subdir'. 17 | BASE_DIR = Path(__file__).resolve().parent.parent 18 | 19 | 20 | # Quick-start development settings - unsuitable for production 21 | # See https://docs.djangoproject.com/en/5.0/howto/deployment/checklist/ 22 | 23 | # SECURITY WARNING: keep the secret key used in production secret! 24 | SECRET_KEY = 'django-insecure-p%a7njmpp-*sj*-g$k!-(olb3%0(y&bk=y34g!!)e993whp6w*' 25 | 26 | # SECURITY WARNING: don't run with debug turned on in production! 27 | DEBUG = True 28 | 29 | ALLOWED_HOSTS = [] 30 | 31 | 32 | # Application definition 33 | 34 | INSTALLED_APPS = [ 35 | 'django.contrib.admin', 36 | 'django.contrib.auth', 37 | 'django.contrib.contenttypes', 38 | 'django.contrib.sessions', 39 | 'django.contrib.messages', 40 | 'django.contrib.staticfiles', 41 | 42 | 'rest_framework', 43 | 'rest_framework_simplejwt', 44 | 45 | 'authentication', 46 | 'brands', 47 | 'suppliers', 48 | 'categories', 49 | 'products', 50 | 'inflows', 51 | 'outflows', 52 | 'ai', 53 | ] 54 | 55 | LOGIN_URL = 'login' 56 | 57 | LOGIN_REDIRECT_URL = '/' 58 | LOGOUT_REDIRECT_URL = '/login/' 59 | 60 | MIDDLEWARE = [ 61 | 'django.middleware.security.SecurityMiddleware', 62 | 'django.contrib.sessions.middleware.SessionMiddleware', 63 | 'django.middleware.common.CommonMiddleware', 64 | 'django.middleware.csrf.CsrfViewMiddleware', 65 | 'django.contrib.auth.middleware.AuthenticationMiddleware', 66 | 'django.contrib.messages.middleware.MessageMiddleware', 67 | 'django.middleware.clickjacking.XFrameOptionsMiddleware', 68 | ] 69 | 70 | ROOT_URLCONF = 'app.urls' 71 | 72 | TEMPLATES = [ 73 | { 74 | 'BACKEND': 'django.template.backends.django.DjangoTemplates', 75 | 'DIRS': ['app/templates'], 76 | 'APP_DIRS': True, 77 | 'OPTIONS': { 78 | 'context_processors': [ 79 | 'django.template.context_processors.debug', 80 | 'django.template.context_processors.request', 81 | 'django.contrib.auth.context_processors.auth', 82 | 'django.contrib.messages.context_processors.messages', 83 | ], 84 | }, 85 | }, 86 | ] 87 | 88 | WSGI_APPLICATION = 'app.wsgi.application' 89 | 90 | 91 | # Database 92 | # https://docs.djangoproject.com/en/5.0/ref/settings/#databases 93 | 94 | DATABASES = { 95 | 'prod': { 96 | 'ENGINE': 'django.db.backends.postgresql_psycopg2', 97 | 'NAME': 'sge', 98 | 'USER': 'postgres', 99 | 'PASSWORD': 'postgres', 100 | 'HOST': 'sge_db', 101 | 'PORT': '5432', 102 | }, 103 | 'default': { 104 | 'ENGINE': 'django.db.backends.sqlite3', 105 | 'NAME': BASE_DIR / 'db.sqlite3', 106 | } 107 | } 108 | 109 | 110 | # Password validation 111 | # https://docs.djangoproject.com/en/5.0/ref/settings/#auth-password-validators 112 | 113 | AUTH_PASSWORD_VALIDATORS = [ 114 | { 115 | 'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator', 116 | }, 117 | { 118 | 'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator', 119 | }, 120 | { 121 | 'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator', 122 | }, 123 | { 124 | 'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator', 125 | }, 126 | ] 127 | 128 | 129 | # Internationalization 130 | # https://docs.djangoproject.com/en/5.0/topics/i18n/ 131 | 132 | LANGUAGE_CODE = 'pt-BR' 133 | 134 | TIME_ZONE = 'UTC' 135 | 136 | USE_I18N = True 137 | 138 | USE_TZ = False 139 | 140 | 141 | # Static files (CSS, JavaScript, Images) 142 | # https://docs.djangoproject.com/en/5.0/howto/static-files/ 143 | 144 | STATIC_URL = 'static/' 145 | 146 | # Default primary key field type 147 | # https://docs.djangoproject.com/en/5.0/ref/settings/#default-auto-field 148 | 149 | DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField' 150 | 151 | 152 | REST_FRAMEWORK = { 153 | 'DEFAULT_AUTHENTICATION_CLASSES': ( 154 | 'rest_framework_simplejwt.authentication.JWTAuthentication', 155 | ), 156 | 'DEFAULT_PERMISSION_CLASSES': ( 157 | 'rest_framework.permissions.IsAuthenticated', 158 | 'rest_framework.permissions.DjangoModelPermissions', 159 | ), 160 | } 161 | 162 | SIMPLE_JWT = { 163 | "ACCESS_TOKEN_LIFETIME": timedelta(days=1), 164 | "REFRESH_TOKEN_LIFETIME": timedelta(days=7), 165 | } 166 | 167 | OPENAI_MODEL = 'gpt-3.5-turbo' 168 | OPENAI_API_KEY = '' 169 | -------------------------------------------------------------------------------- /app/templates/base.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | {% block title %}{% endblock %} 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | {% include 'components/_header.html' %} 16 | 17 |
18 | {% if user.is_authenticated %} 19 | {% include 'components/_sidebar.html' %} 20 | {% endif %} 21 | 22 |
23 | 24 |
25 |
26 | {% block content %} 27 | {% endblock %} 28 |
29 |
30 | 31 |
32 |
33 | 34 | {% include 'components/_footer.html' %} 35 | 36 | 37 | -------------------------------------------------------------------------------- /app/templates/components/_ai_result.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |

4 | 8 |

9 |
10 |
11 | {{ ai_result|linebreaks }} 12 |
13 |
14 |
15 |
16 | -------------------------------------------------------------------------------- /app/templates/components/_footer.html: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/templates/components/_header.html: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/templates/components/_pagination.html: -------------------------------------------------------------------------------- 1 | {% if page_obj.has_other_pages %} 2 | 49 | {% endif %} 50 | -------------------------------------------------------------------------------- /app/templates/components/_product_metrics.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 |
5 |
6 |
7 |
Quantidade de produtos
8 |

{{ product_metrics.total_quantity }}

9 |
10 |
11 |
12 |
13 |
14 |
15 |
Custo do estoque
16 |

R$ {{ product_metrics.total_cost_price }}

17 |
18 |
19 |
20 |
21 |
22 |
23 |
Valor do estoque
24 |

R$ {{ product_metrics.total_selling_price }}

25 |
26 |
27 |
28 |
29 |
30 |
31 |
Lucro do estoque
32 |

R$ {{ product_metrics.total_profit }}

33 |
34 |
35 |
36 |
37 |
38 |
-------------------------------------------------------------------------------- /app/templates/components/_sales_metrics.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 |
5 |
6 |
7 |
Quantidade de vendas
8 |

{{ sales_metrics.total_sales }}

9 |
10 |
11 |
12 |
13 |
14 |
15 |
Produtos vendidos
16 |

{{ sales_metrics.total_products_sold }}

17 |
18 |
19 |
20 |
21 |
22 |
23 |
Valor das vendas
24 |

R$ {{ sales_metrics.total_sales_value }}

25 |
26 |
27 |
28 |
29 |
30 |
31 |
Lucro das vendas
32 |

R$ {{ sales_metrics.total_sales_profit }}

33 |
34 |
35 |
36 |
37 |
38 |
-------------------------------------------------------------------------------- /app/templates/components/_sidebar.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 | 8 | 77 |
78 |
79 |
-------------------------------------------------------------------------------- /app/templates/home.html: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} 2 | 3 | {% block title %} 4 | SGE - Home 5 | {% endblock %} 6 | 7 | {% block content %} 8 | 9 | {% include 'components/_ai_result.html' %} 10 | 11 | {% if perms.products.view_product and perms.inflows.view_inflow %} 12 | {% include 'components/_product_metrics.html' %} 13 | {% endif %} 14 | 15 | {% if perms.outflows.view_outflow %} 16 | {% include 'components/_sales_metrics.html' %} 17 | {% endif %} 18 | 19 | 20 | 21 | {% if perms.outflows.view_outflow %} 22 |
23 |
24 |
Valor de vendas (Últimos 7 Dias)
25 | 26 |
27 |
28 |
Quantidade de Vendas Diárias
29 | 30 |
31 | 82 |
83 | {% endif %} 84 | 85 | {% if perms.products.view_product %} 86 |
87 |
88 | {% if product_count_by_category != '{}' %} 89 |
Produtos por Categoria
90 |
91 |
92 | 93 |
94 | {% endif %} 95 |
96 |
97 | {% if product_count_by_category != '{}' %} 98 |
Produtos por Marca
99 |
100 |
101 | 102 |
103 | {% endif %} 104 |
105 | 149 |
150 | {% endif %} 151 | {% endblock %} 152 | -------------------------------------------------------------------------------- /app/templates/registration/login.html: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} 2 | {% block title %}Login{% endblock %} 3 | 4 | {% block content %} 5 |
6 |
7 |
8 |
9 |
10 |
Login
11 |
12 | {% csrf_token %} 13 |
14 | 15 | 16 |
17 |
18 | 19 | 20 |
21 |
22 | 23 |
24 |
25 | {% if form.errors %} 26 |
27 |
    28 | {% for field in form %} 29 | {% for error in field.errors %} 30 |
  • {{ error }}
  • 31 | {% endfor %} 32 | {% endfor %} 33 | {% for error in form.non_field_errors %} 34 |
  • {{ error }}
  • 35 | {% endfor %} 36 |
37 |
38 | {% endif %} 39 |
40 |
41 |
42 |
43 |
44 | {% endblock %} 45 | -------------------------------------------------------------------------------- /app/urls.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | from django.urls import path, include 3 | from django.contrib.auth import views as auth_views 4 | from . import views 5 | 6 | 7 | urlpatterns = [ 8 | path('admin/', admin.site.urls), 9 | 10 | path('login/', auth_views.LoginView.as_view(), name='login'), 11 | path('logout/', auth_views.LogoutView.as_view(), name='logout'), 12 | 13 | path('api/v1/', include('authentication.urls')), 14 | 15 | path('', views.home, name='home'), 16 | path('', include('suppliers.urls')), 17 | path('', include('brands.urls')), 18 | path('', include('categories.urls')), 19 | path('', include('products.urls')), 20 | path('', include('inflows.urls')), 21 | path('', include('outflows.urls')), 22 | ] 23 | -------------------------------------------------------------------------------- /app/views.py: -------------------------------------------------------------------------------- 1 | import json 2 | from django.contrib.auth.decorators import login_required 3 | from django.shortcuts import render 4 | from ai.models import AIResult 5 | from . import metrics 6 | 7 | 8 | @login_required(login_url='login') 9 | def home(request): 10 | product_metrics = metrics.get_product_metrics() 11 | sales_metrics = metrics.get_sales_metrics() 12 | graphic_product_category_metric = metrics.get_graphic_product_category_metric() 13 | graphic_product_brand_metric = metrics.get_graphic_product_brand_metric() 14 | daily_sales_data = metrics.get_daily_sales_data() 15 | daily_sales_quantity_data = metrics.get_daily_sales_quantity_data() 16 | ai_result = AIResult.objects.first().result 17 | 18 | context = { 19 | 'product_metrics': product_metrics, 20 | 'sales_metrics': sales_metrics, 21 | 'product_count_by_category': json.dumps(graphic_product_category_metric), 22 | 'product_count_by_brand': json.dumps(graphic_product_brand_metric), 23 | 'daily_sales_data': json.dumps(daily_sales_data), 24 | 'daily_sales_quantity_data': json.dumps(daily_sales_quantity_data), 25 | 'ai_result': ai_result, 26 | } 27 | 28 | return render(request, 'home.html', context) 29 | -------------------------------------------------------------------------------- /app/wsgi.py: -------------------------------------------------------------------------------- 1 | """ 2 | WSGI config for app project. 3 | 4 | It exposes the WSGI callable as a module-level variable named ``application``. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/5.0/howto/deployment/wsgi/ 8 | """ 9 | 10 | import os 11 | 12 | from django.core.wsgi import get_wsgi_application 13 | 14 | os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'app.settings') 15 | 16 | application = get_wsgi_application() 17 | -------------------------------------------------------------------------------- /authentication/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pycodebr/sge/18d17f898802a3c67b3fbb0463757f0c182d1717/authentication/__init__.py -------------------------------------------------------------------------------- /authentication/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class AuthenticationConfig(AppConfig): 5 | default_auto_field = 'django.db.models.BigAutoField' 6 | name = 'authentication' 7 | -------------------------------------------------------------------------------- /authentication/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pycodebr/sge/18d17f898802a3c67b3fbb0463757f0c182d1717/authentication/migrations/__init__.py -------------------------------------------------------------------------------- /authentication/urls.py: -------------------------------------------------------------------------------- 1 | from django.urls import path 2 | from rest_framework_simplejwt.views import TokenObtainPairView, TokenRefreshView, TokenVerifyView 3 | 4 | 5 | urlpatterns = [ 6 | path('authentication/token/', TokenObtainPairView.as_view(), name='token_obtain_pair'), 7 | path('authentication/token/refresh/', TokenRefreshView.as_view(), name='token_refresh'), 8 | path('authentication/token/verify/', TokenVerifyView.as_view(), name='token_verify'), 9 | ] 10 | -------------------------------------------------------------------------------- /brands/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pycodebr/sge/18d17f898802a3c67b3fbb0463757f0c182d1717/brands/__init__.py -------------------------------------------------------------------------------- /brands/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | from . import models 3 | 4 | 5 | class BrandAdmin(admin.ModelAdmin): 6 | list_display = ('name', 'description',) 7 | search_fields = ('name',) 8 | 9 | 10 | admin.site.register(models.Brand, BrandAdmin) 11 | -------------------------------------------------------------------------------- /brands/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class BrandsConfig(AppConfig): 5 | default_auto_field = 'django.db.models.BigAutoField' 6 | name = 'brands' 7 | -------------------------------------------------------------------------------- /brands/forms.py: -------------------------------------------------------------------------------- 1 | from django import forms 2 | from . import models 3 | 4 | 5 | class BrandForm(forms.ModelForm): 6 | 7 | class Meta: 8 | model = models.Brand 9 | fields = ['name', 'description'] 10 | widgets = { 11 | 'name': forms.TextInput(attrs={'class': 'form-control'}), 12 | 'description': forms.Textarea(attrs={'class': 'form-control', 'rows': 3}), 13 | } 14 | labels = { 15 | 'name': 'Nome', 16 | 'description': 'Descrição', 17 | } 18 | -------------------------------------------------------------------------------- /brands/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 5.0.1 on 2024-02-09 21:42 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | initial = True 9 | 10 | dependencies = [ 11 | ] 12 | 13 | operations = [ 14 | migrations.CreateModel( 15 | name='Brand', 16 | fields=[ 17 | ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 18 | ('name', models.CharField(max_length=500)), 19 | ('description', models.TextField(blank=True, null=True)), 20 | ('created_at', models.DateTimeField(auto_now_add=True)), 21 | ('updated_at', models.DateTimeField(auto_now=True)), 22 | ], 23 | options={ 24 | 'ordering': ['name'], 25 | }, 26 | ), 27 | ] 28 | -------------------------------------------------------------------------------- /brands/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pycodebr/sge/18d17f898802a3c67b3fbb0463757f0c182d1717/brands/migrations/__init__.py -------------------------------------------------------------------------------- /brands/models.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | 3 | 4 | class Brand(models.Model): 5 | name = models.CharField(max_length=500) 6 | description = models.TextField(null=True, blank=True) 7 | created_at = models.DateTimeField(auto_now_add=True) 8 | updated_at = models.DateTimeField(auto_now=True) 9 | 10 | class Meta: 11 | ordering = ['name'] 12 | 13 | def __str__(self): 14 | return self.name 15 | -------------------------------------------------------------------------------- /brands/serializers.py: -------------------------------------------------------------------------------- 1 | from rest_framework import serializers 2 | from brands.models import Brand 3 | 4 | 5 | class BrandSerializer(serializers.ModelSerializer): 6 | 7 | class Meta: 8 | model = Brand 9 | fields = '__all__' 10 | -------------------------------------------------------------------------------- /brands/templates/brand_create.html: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} 2 | 3 | {% block content %} 4 |
5 |

Cadastrar Marca

6 | 7 |
8 |
9 |
10 | {% csrf_token %} 11 | {{ form.as_p }} 12 | 13 |
14 |
15 |
16 | Cancelar e Voltar para a Lista de Marcas 17 |
18 | {% endblock %} 19 | -------------------------------------------------------------------------------- /brands/templates/brand_delete.html: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} 2 | 3 | {% block content %} 4 |
5 |

Deletar Marca

6 | 7 |
8 |
9 |

Tem certeza de que deseja excluir a marca {{ object.name }}?

10 | 11 |
12 | {% csrf_token %} 13 | 14 |
15 | 16 | Cancelar e Voltar para a Lista de Marcas 17 |
18 |
19 |
20 | {% endblock %} 21 | -------------------------------------------------------------------------------- /brands/templates/brand_detail.html: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} 2 | 3 | {% block content %} 4 |
5 |

Detalhes da Marca

6 | 7 |
8 |
9 |

{{ object.name }}

10 |

{{ object.description }}

11 |
12 |
13 | 14 | Voltar para a Lista de Marcas 15 |
16 | {% endblock %} 17 | -------------------------------------------------------------------------------- /brands/templates/brand_list.html: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} 2 | 3 | {% block title %} 4 | SGE - Marcas 5 | {% endblock %} 6 | 7 | {% block content %} 8 | 9 |
10 |
11 |
12 |
13 | 14 | 17 |
18 |
19 |
20 | {% if perms.brands.add_brand %} 21 | 26 | {% endif %} 27 |
28 | 29 |
30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | {% for brand in brands %} 41 | 42 | 47 | 48 | 49 | 66 | 67 | {% endfor %} 68 | 69 |
IDNomeDescriçãoAções
43 | 44 | {{ brand.id }} 45 | 46 | {{ brand.name }}{{ brand.description }} 50 | 51 | 52 | 53 | 54 | {% if perms.brands.change_brand %} 55 | 56 | 57 | 58 | {% endif %} 59 | 60 | {% if perms.brands.delete_brand %} 61 | 62 | 63 | 64 | {% endif %} 65 |
70 |
71 | 72 | {% include 'components/_pagination.html' %} 73 | 74 | {% endblock %} -------------------------------------------------------------------------------- /brands/templates/brand_update.html: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} 2 | 3 | {% block content %} 4 |
5 |

Editar Marca

6 | 7 |
8 |
9 |
10 | {% csrf_token %} 11 | {{ form.as_p }} 12 | 13 |
14 |
15 |
16 | 17 | Cancelar e Voltar para a Lista de Marcas 18 |
19 | {% endblock %} 20 | -------------------------------------------------------------------------------- /brands/urls.py: -------------------------------------------------------------------------------- 1 | from django.urls import path 2 | from . import views 3 | 4 | 5 | urlpatterns = [ 6 | path('brands/list/', views.BrandListView.as_view(), name='brand_list'), 7 | path('brands/create/', views.BrandCreateView.as_view(), name='brand_create'), 8 | path('brands//detail/', views.BrandDetailView.as_view(), name='brand_detail'), 9 | path('brands//update/', views.BrandUpdateView.as_view(), name='brand_update'), 10 | path('brands//delete/', views.BrandDeleteView.as_view(), name='brand_delete'), 11 | 12 | path('api/v1/brands/', views.BrandCreateListAPIView.as_view(), name='brand-create-list-api-view'), 13 | path('api/v1/brands//', views.BrandRetrieveUpdateDestroyAPIView.as_view(), name='brand-detail-api-view'), 14 | ] 15 | -------------------------------------------------------------------------------- /brands/views.py: -------------------------------------------------------------------------------- 1 | from rest_framework import generics 2 | from django.contrib.auth.mixins import LoginRequiredMixin, PermissionRequiredMixin 3 | from django.urls import reverse_lazy 4 | from django.views.generic import ListView, CreateView, DetailView, UpdateView, DeleteView 5 | from . import models, forms, serializers 6 | 7 | 8 | class BrandListView(LoginRequiredMixin, PermissionRequiredMixin, ListView): 9 | model = models.Brand 10 | template_name = 'brand_list.html' 11 | context_object_name = 'brands' 12 | paginate_by = 10 13 | permission_required = 'brands.view_brand' 14 | 15 | def get_queryset(self): 16 | queryset = super().get_queryset() 17 | name = self.request.GET.get('name') 18 | 19 | if name: 20 | queryset = queryset.filter(name__icontains=name) 21 | 22 | return queryset 23 | 24 | 25 | class BrandCreateView(LoginRequiredMixin, PermissionRequiredMixin, CreateView): 26 | model = models.Brand 27 | template_name = 'brand_create.html' 28 | form_class = forms.BrandForm 29 | success_url = reverse_lazy('brand_list') 30 | permission_required = 'brands.add_brand' 31 | 32 | 33 | class BrandDetailView(LoginRequiredMixin, PermissionRequiredMixin, DetailView): 34 | model = models.Brand 35 | template_name = 'brand_detail.html' 36 | permission_required = 'brands.view_brand' 37 | 38 | 39 | class BrandUpdateView(LoginRequiredMixin, PermissionRequiredMixin, UpdateView): 40 | model = models.Brand 41 | template_name = 'brand_update.html' 42 | form_class = forms.BrandForm 43 | success_url = reverse_lazy('brand_list') 44 | permission_required = 'brands.change_brand' 45 | 46 | 47 | class BrandDeleteView(LoginRequiredMixin, PermissionRequiredMixin, DeleteView): 48 | model = models.Brand 49 | template_name = 'brand_delete.html' 50 | success_url = reverse_lazy('brand_list') 51 | permission_required = 'brands.delete_brand' 52 | 53 | 54 | class BrandCreateListAPIView(generics.ListCreateAPIView): 55 | queryset = models.Brand.objects.all() 56 | serializer_class = serializers.BrandSerializer 57 | 58 | 59 | class BrandRetrieveUpdateDestroyAPIView(generics.RetrieveUpdateDestroyAPIView): 60 | queryset = models.Brand.objects.all() 61 | serializer_class = serializers.BrandSerializer 62 | -------------------------------------------------------------------------------- /categories/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pycodebr/sge/18d17f898802a3c67b3fbb0463757f0c182d1717/categories/__init__.py -------------------------------------------------------------------------------- /categories/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | from . import models 3 | 4 | 5 | class CategoryAdmin(admin.ModelAdmin): 6 | list_display = ('name', 'description',) 7 | search_fields = ('name',) 8 | 9 | 10 | admin.site.register(models.Category, CategoryAdmin) 11 | -------------------------------------------------------------------------------- /categories/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class CategoriesConfig(AppConfig): 5 | default_auto_field = 'django.db.models.BigAutoField' 6 | name = 'categories' 7 | -------------------------------------------------------------------------------- /categories/forms.py: -------------------------------------------------------------------------------- 1 | from django import forms 2 | from . import models 3 | 4 | 5 | class CategoryForm(forms.ModelForm): 6 | 7 | class Meta: 8 | model = models.Category 9 | fields = ['name', 'description'] 10 | widgets = { 11 | 'name': forms.TextInput(attrs={'class': 'form-control'}), 12 | 'description': forms.Textarea(attrs={'class': 'form-control', 'rows': 3}), 13 | } 14 | labels = { 15 | 'name': 'Nome', 16 | 'description': 'Descrição', 17 | } 18 | -------------------------------------------------------------------------------- /categories/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 5.0.1 on 2024-02-09 21:42 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | initial = True 9 | 10 | dependencies = [ 11 | ] 12 | 13 | operations = [ 14 | migrations.CreateModel( 15 | name='Category', 16 | fields=[ 17 | ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 18 | ('name', models.CharField(max_length=500)), 19 | ('description', models.TextField(blank=True, null=True)), 20 | ('created_at', models.DateTimeField(auto_now_add=True)), 21 | ('updated_at', models.DateTimeField(auto_now=True)), 22 | ], 23 | options={ 24 | 'ordering': ['name'], 25 | }, 26 | ), 27 | ] 28 | -------------------------------------------------------------------------------- /categories/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pycodebr/sge/18d17f898802a3c67b3fbb0463757f0c182d1717/categories/migrations/__init__.py -------------------------------------------------------------------------------- /categories/models.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | 3 | 4 | class Category(models.Model): 5 | name = models.CharField(max_length=500) 6 | description = models.TextField(null=True, blank=True) 7 | created_at = models.DateTimeField(auto_now_add=True) 8 | updated_at = models.DateTimeField(auto_now=True) 9 | 10 | class Meta: 11 | ordering = ['name'] 12 | 13 | def __str__(self): 14 | return self.name 15 | -------------------------------------------------------------------------------- /categories/serializers.py: -------------------------------------------------------------------------------- 1 | from rest_framework import serializers 2 | from categories.models import Category 3 | 4 | 5 | class CategorySerializer(serializers.ModelSerializer): 6 | 7 | class Meta: 8 | model = Category 9 | fields = '__all__' 10 | -------------------------------------------------------------------------------- /categories/templates/category_create.html: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} 2 | 3 | {% block content %} 4 |
5 |

Cadastrar Categoria

6 | 7 |
8 |
9 |
10 | {% csrf_token %} 11 | {{ form.as_p }} 12 | 13 |
14 |
15 |
16 | Cancelar e Voltar para a Lista de Categorias 17 |
18 | {% endblock %} 19 | -------------------------------------------------------------------------------- /categories/templates/category_delete.html: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} 2 | 3 | {% block content %} 4 |
5 |

Deletar Categoria

6 | 7 |
8 |
9 |

Tem certeza de que deseja excluir a categoria {{ object.name }}?

10 | 11 |
12 | {% csrf_token %} 13 | 14 |
15 | 16 | Cancelar e Voltar para a Lista de Categorias 17 |
18 |
19 |
20 | {% endblock %} 21 | -------------------------------------------------------------------------------- /categories/templates/category_detail.html: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} 2 | 3 | {% block content %} 4 |
5 |

Detalhes da Categoria

6 | 7 |
8 |
9 |

{{ object.name }}

10 |

{{ object.description }}

11 |
12 |
13 | 14 | Voltar para a Lista de Categorias 15 |
16 | {% endblock %} 17 | -------------------------------------------------------------------------------- /categories/templates/category_list.html: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} 2 | 3 | {% block title %} 4 | SGE - Categorias 5 | {% endblock %} 6 | 7 | {% block content %} 8 | 9 |
10 |
11 |
12 |
13 | 14 | 17 |
18 |
19 |
20 | {% if perms.categories.add_category %} 21 | 26 | {% endif %} 27 |
28 | 29 |
30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | {% for category in categories %} 41 | 42 | 47 | 48 | 49 | 66 | 67 | {% endfor %} 68 | 69 |
IDNomeDescriçãoAções
43 | 44 | {{ category.id }} 45 | 46 | {{ category.name }}{{ category.description }} 50 | 51 | 52 | 53 | 54 | {% if perms.categories.change_category %} 55 | 56 | 57 | 58 | {% endif %} 59 | 60 | {% if perms.categories.delete_category %} 61 | 62 | 63 | 64 | {% endif %} 65 |
70 |
71 | 72 | {% include 'components/_pagination.html' %} 73 | 74 | {% endblock %} -------------------------------------------------------------------------------- /categories/templates/category_update.html: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} 2 | 3 | {% block content %} 4 |
5 |

Editar Categoria

6 | 7 |
8 |
9 |
10 | {% csrf_token %} 11 | {{ form.as_p }} 12 | 13 |
14 |
15 |
16 | 17 | Cancelar e Voltar para a Lista de Categorias 18 |
19 | {% endblock %} 20 | -------------------------------------------------------------------------------- /categories/urls.py: -------------------------------------------------------------------------------- 1 | from django.urls import path 2 | from . import views 3 | 4 | 5 | urlpatterns = [ 6 | path('categories/list/', views.CategoryListView.as_view(), name='category_list'), 7 | path('categories/create/', views.CategoryCreateView.as_view(), name='category_create'), 8 | path('categories//detail/', views.CategoryDetailView.as_view(), name='category_detail'), 9 | path('categories//update/', views.CategoryUpdateView.as_view(), name='category_update'), 10 | path('categories//delete/', views.CategoryDeleteView.as_view(), name='category_delete'), 11 | 12 | path('api/v1/categories/', views.CategoryCreateListAPIView.as_view(), name='category-create-list-api-view'), 13 | path('api/v1/categories//', views.CategoryRetrieveUpdateDestroyAPIView.as_view(), name='category-detail-api-view'), 14 | ] 15 | -------------------------------------------------------------------------------- /categories/views.py: -------------------------------------------------------------------------------- 1 | from rest_framework import generics 2 | from django.contrib.auth.mixins import LoginRequiredMixin, PermissionRequiredMixin 3 | from django.urls import reverse_lazy 4 | from django.views.generic import ListView, CreateView, DetailView, UpdateView, DeleteView 5 | from . import models, forms, serializers 6 | 7 | 8 | class CategoryListView(LoginRequiredMixin, PermissionRequiredMixin, ListView): 9 | model = models.Category 10 | template_name = 'category_list.html' 11 | context_object_name = 'categories' 12 | paginate_by = 10 13 | permission_required = 'categories.view_category' 14 | 15 | def get_queryset(self): 16 | queryset = super().get_queryset() 17 | name = self.request.GET.get('name') 18 | 19 | if name: 20 | queryset = queryset.filter(name__icontains=name) 21 | 22 | return queryset 23 | 24 | 25 | class CategoryCreateView(LoginRequiredMixin, PermissionRequiredMixin, CreateView): 26 | model = models.Category 27 | template_name = 'category_create.html' 28 | form_class = forms.CategoryForm 29 | success_url = reverse_lazy('category_list') 30 | permission_required = 'categories.add_category' 31 | 32 | 33 | class CategoryDetailView(LoginRequiredMixin, PermissionRequiredMixin, DetailView): 34 | model = models.Category 35 | template_name = 'category_detail.html' 36 | permission_required = 'categories.view_category' 37 | 38 | 39 | class CategoryUpdateView(LoginRequiredMixin, PermissionRequiredMixin, UpdateView): 40 | model = models.Category 41 | template_name = 'category_update.html' 42 | form_class = forms.CategoryForm 43 | success_url = reverse_lazy('category_list') 44 | permission_required = 'categories.change_category' 45 | 46 | 47 | class CategoryDeleteView(LoginRequiredMixin, PermissionRequiredMixin, DeleteView): 48 | model = models.Category 49 | template_name = 'category_delete.html' 50 | success_url = reverse_lazy('category_list') 51 | permission_required = 'categories.delete_category' 52 | 53 | 54 | class CategoryCreateListAPIView(generics.ListCreateAPIView): 55 | queryset = models.Category.objects.all() 56 | serializer_class = serializers.CategorySerializer 57 | 58 | 59 | class CategoryRetrieveUpdateDestroyAPIView(generics.RetrieveUpdateDestroyAPIView): 60 | queryset = models.Category.objects.all() 61 | serializer_class = serializers.CategorySerializer 62 | -------------------------------------------------------------------------------- /cron: -------------------------------------------------------------------------------- 1 | * * * * * cd /sge && /usr/local/bin/python manage.py fazer_coisas >> /var/log/cron.log 2>&1 2 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | services: 2 | 3 | sge_web: 4 | build: . 5 | restart: always 6 | ports: 7 | - 8000:8000 8 | depends_on: 9 | - sge_db 10 | 11 | sge_db: 12 | image: postgres:15 13 | ports: 14 | - 5432:5432 15 | volumes: 16 | - postgres_data:/var/lib/postgresql/data/ 17 | environment: 18 | - POSTGRES_USER=postgres 19 | - POSTGRES_PASSWORD=postgres 20 | - POSTGRES_DB=sge 21 | 22 | volumes: 23 | postgres_data: 24 | -------------------------------------------------------------------------------- /inflows/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pycodebr/sge/18d17f898802a3c67b3fbb0463757f0c182d1717/inflows/__init__.py -------------------------------------------------------------------------------- /inflows/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | from . import models 3 | 4 | 5 | class InflowAdmin(admin.ModelAdmin): 6 | list_display = ('supplier', 'product', 'quantity', 'created_at', 'updated_at',) 7 | search_fields = ('supplier__name', 'product__title',) 8 | 9 | 10 | admin.site.register(models.Inflow, InflowAdmin) 11 | -------------------------------------------------------------------------------- /inflows/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class InflowsConfig(AppConfig): 5 | default_auto_field = 'django.db.models.BigAutoField' 6 | name = 'inflows' 7 | 8 | def ready(self): 9 | import inflows.signals # noqa: F401 10 | -------------------------------------------------------------------------------- /inflows/forms.py: -------------------------------------------------------------------------------- 1 | from django import forms 2 | from . import models 3 | 4 | 5 | class InflowForm(forms.ModelForm): 6 | 7 | class Meta: 8 | model = models.Inflow 9 | fields = ['supplier', 'product', 'quantity', 'description'] 10 | widgets = { 11 | 'supplier': forms.Select(attrs={'class': 'form-control'}), 12 | 'product': forms.Select(attrs={'class': 'form-control'}), 13 | 'quantity': forms.NumberInput(attrs={'class': 'form-control'}), 14 | 'description': forms.Textarea(attrs={'class': 'form-control', 'rows': 3}), 15 | } 16 | labels = { 17 | 'supplier': 'Fornecedor', 18 | 'product': 'Produto', 19 | 'quantity': 'Quantidade', 20 | 'description': 'Descrição', 21 | } 22 | -------------------------------------------------------------------------------- /inflows/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 5.0.1 on 2024-02-09 21:42 2 | 3 | import django.db.models.deletion 4 | from django.db import migrations, models 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | initial = True 10 | 11 | dependencies = [ 12 | ('products', '0001_initial'), 13 | ('suppliers', '0001_initial'), 14 | ] 15 | 16 | operations = [ 17 | migrations.CreateModel( 18 | name='Inflow', 19 | fields=[ 20 | ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 21 | ('quantity', models.IntegerField()), 22 | ('description', models.TextField(blank=True, null=True)), 23 | ('created_at', models.DateTimeField(auto_now_add=True)), 24 | ('updated_at', models.DateTimeField(auto_now=True)), 25 | ('product', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='inflows', to='products.product')), 26 | ('supplier', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='inflows', to='suppliers.supplier')), 27 | ], 28 | options={ 29 | 'ordering': ['-created_at'], 30 | }, 31 | ), 32 | ] 33 | -------------------------------------------------------------------------------- /inflows/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pycodebr/sge/18d17f898802a3c67b3fbb0463757f0c182d1717/inflows/migrations/__init__.py -------------------------------------------------------------------------------- /inflows/models.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | from products.models import Product 3 | from suppliers.models import Supplier 4 | 5 | 6 | class Inflow(models.Model): 7 | supplier = models.ForeignKey(Supplier, on_delete=models.PROTECT, related_name='inflows') 8 | product = models.ForeignKey(Product, on_delete=models.PROTECT, related_name='inflows') 9 | quantity = models.IntegerField() 10 | description = models.TextField(null=True, blank=True) 11 | created_at = models.DateTimeField(auto_now_add=True) 12 | updated_at = models.DateTimeField(auto_now=True) 13 | 14 | class Meta: 15 | ordering = ['-created_at'] 16 | 17 | def __str__(self): 18 | return str(self.product) 19 | -------------------------------------------------------------------------------- /inflows/serializers.py: -------------------------------------------------------------------------------- 1 | from rest_framework import serializers 2 | from inflows.models import Inflow 3 | 4 | 5 | class InflowSerializer(serializers.ModelSerializer): 6 | 7 | class Meta: 8 | model = Inflow 9 | fields = '__all__' 10 | -------------------------------------------------------------------------------- /inflows/signals.py: -------------------------------------------------------------------------------- 1 | from django.db.models.signals import post_save 2 | from django.dispatch import receiver 3 | from .models import Inflow 4 | 5 | 6 | @receiver(post_save, sender=Inflow) 7 | def update_product_quantity(sender, instance, created, **kwargs): 8 | if created: 9 | if instance.quantity > 0: 10 | product = instance.product 11 | product.quantity += instance.quantity 12 | product.save() 13 | -------------------------------------------------------------------------------- /inflows/templates/inflow_create.html: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} 2 | 3 | {% block content %} 4 |
5 |

Cadastrar Entrada

6 | 7 |
8 |
9 |
10 | {% csrf_token %} 11 | {{ form.as_p }} 12 | 13 |
14 |
15 |
16 | Cancelar e Voltar para a Lista de Entradas 17 |
18 | {% endblock %} 19 | -------------------------------------------------------------------------------- /inflows/templates/inflow_detail.html: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} 2 | 3 | {% block content %} 4 |
5 |

Detalhes da Entrada

6 | 7 |
8 |
9 |

{{ object.product }}

10 |

Fornecedor: {{ object.supplier }}

11 |

Quantidade: {{ object.quantity }}

12 |

{{ object.description }}

13 |
14 |
15 | 16 | Voltar para a Lista de Entradas 17 |
18 | {% endblock %} 19 | -------------------------------------------------------------------------------- /inflows/templates/inflow_list.html: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} 2 | 3 | {% block title %} 4 | SGE - Entradas 5 | {% endblock %} 6 | 7 | {% block content %} 8 | 9 |
10 |
11 |
12 |
13 | 14 | 17 |
18 |
19 |
20 | {% if perms.inflows.add_inflow %} 21 | 26 | {% endif %} 27 |
28 | 29 |
30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | {% for inflow in inflows %} 43 | 44 | 49 | 50 | 51 | 52 | 53 | 58 | 59 | {% endfor %} 60 | 61 |
IDProdutoFornecedorQuantidadeData de entradaAções
45 | 46 | {{ inflow.id }} 47 | 48 | {{ inflow.product }}{{ inflow.supplier }}{{ inflow.quantity }}{{ inflow.created_at }} 54 | 55 | 56 | 57 |
62 |
63 | 64 | {% include 'components/_pagination.html' %} 65 | 66 | {% endblock %} -------------------------------------------------------------------------------- /inflows/urls.py: -------------------------------------------------------------------------------- 1 | from django.urls import path 2 | from . import views 3 | 4 | 5 | urlpatterns = [ 6 | path('inflows/list/', views.InflowListView.as_view(), name='inflow_list'), 7 | path('inflows/create/', views.InflowCreateView.as_view(), name='inflow_create'), 8 | path('inflows//detail/', views.InflowDetailView.as_view(), name='inflow_detail'), 9 | 10 | path('api/v1/inflows/', views.InflowCreateListAPIView.as_view(), name='inflow-create-list-api-view'), 11 | path('api/v1/inflows//', views.InflowRetrieveAPIView.as_view(), name='inflow-detail-api-view'), 12 | ] 13 | -------------------------------------------------------------------------------- /inflows/views.py: -------------------------------------------------------------------------------- 1 | from rest_framework import generics 2 | from django.contrib.auth.mixins import LoginRequiredMixin, PermissionRequiredMixin 3 | from django.urls import reverse_lazy 4 | from django.views.generic import ListView, CreateView, DetailView 5 | from . import models, forms, serializers 6 | 7 | 8 | class InflowListView(LoginRequiredMixin, PermissionRequiredMixin, ListView): 9 | model = models.Inflow 10 | template_name = 'inflow_list.html' 11 | context_object_name = 'inflows' 12 | paginate_by = 10 13 | permission_required = 'inflows.view_inflow' 14 | 15 | def get_queryset(self): 16 | queryset = super().get_queryset() 17 | product = self.request.GET.get('product') 18 | 19 | if product: 20 | queryset = queryset.filter(product__title__icontains=product) 21 | 22 | return queryset 23 | 24 | 25 | class InflowCreateView(LoginRequiredMixin, PermissionRequiredMixin, CreateView): 26 | model = models.Inflow 27 | template_name = 'inflow_create.html' 28 | form_class = forms.InflowForm 29 | success_url = reverse_lazy('inflow_list') 30 | permission_required = 'inflows.add_inflow' 31 | 32 | 33 | class InflowDetailView(LoginRequiredMixin, PermissionRequiredMixin, DetailView): 34 | model = models.Inflow 35 | template_name = 'inflow_detail.html' 36 | permission_required = 'inflows.view_inflow' 37 | 38 | 39 | class InflowCreateListAPIView(generics.ListCreateAPIView): 40 | queryset = models.Inflow.objects.all() 41 | serializer_class = serializers.InflowSerializer 42 | 43 | 44 | class InflowRetrieveAPIView(generics.RetrieveAPIView): 45 | queryset = models.Inflow.objects.all() 46 | serializer_class = serializers.InflowSerializer 47 | -------------------------------------------------------------------------------- /manage.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | """Django's command-line utility for administrative tasks.""" 3 | import os 4 | import sys 5 | 6 | 7 | def main(): 8 | """Run administrative tasks.""" 9 | os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'app.settings') 10 | try: 11 | from django.core.management import execute_from_command_line 12 | except ImportError as exc: 13 | raise ImportError( 14 | "Couldn't import Django. Are you sure it's installed and " 15 | "available on your PYTHONPATH environment variable? Did you " 16 | "forget to activate a virtual environment?" 17 | ) from exc 18 | execute_from_command_line(sys.argv) 19 | 20 | 21 | if __name__ == '__main__': 22 | main() 23 | -------------------------------------------------------------------------------- /outflows/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pycodebr/sge/18d17f898802a3c67b3fbb0463757f0c182d1717/outflows/__init__.py -------------------------------------------------------------------------------- /outflows/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | from . import models 3 | 4 | 5 | class OutflowAdmin(admin.ModelAdmin): 6 | list_display = ('product', 'quantity', 'created_at', 'updated_at',) 7 | search_fields = ('product__title',) 8 | 9 | 10 | admin.site.register(models.Outflow, OutflowAdmin) 11 | -------------------------------------------------------------------------------- /outflows/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class OutflowsConfig(AppConfig): 5 | default_auto_field = 'django.db.models.BigAutoField' 6 | name = 'outflows' 7 | 8 | def ready(self): 9 | import outflows.signals # noqa: F401 10 | -------------------------------------------------------------------------------- /outflows/forms.py: -------------------------------------------------------------------------------- 1 | from django import forms 2 | from django.core.exceptions import ValidationError 3 | from . import models 4 | 5 | 6 | class OutflowForm(forms.ModelForm): 7 | class Meta: 8 | model = models.Outflow 9 | fields = ['product', 'quantity', 'description'] 10 | widgets = { 11 | 'product': forms.Select(attrs={'class': 'form-control'}), 12 | 'quantity': forms.NumberInput(attrs={'class': 'form-control'}), 13 | 'description': forms.Textarea(attrs={'class': 'form-control', 'rows': 3}), 14 | } 15 | labels = { 16 | 'product': 'Produto', 17 | 'quantity': 'Quantidade', 18 | 'description': 'Descrição', 19 | } 20 | 21 | def clean_quantity(self): 22 | quantity = self.cleaned_data.get('quantity') 23 | product = self.cleaned_data.get('product') 24 | 25 | if quantity > product.quantity: 26 | raise ValidationError( 27 | f'A quantidade disponível em estoque para o produto {product.title} é de {product.quantity} unidades.' 28 | ) 29 | 30 | return quantity 31 | -------------------------------------------------------------------------------- /outflows/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 5.0.1 on 2024-02-09 21:42 2 | 3 | import django.db.models.deletion 4 | from django.db import migrations, models 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | initial = True 10 | 11 | dependencies = [ 12 | ('products', '0001_initial'), 13 | ] 14 | 15 | operations = [ 16 | migrations.CreateModel( 17 | name='Outflow', 18 | fields=[ 19 | ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 20 | ('quantity', models.IntegerField()), 21 | ('description', models.TextField(blank=True, null=True)), 22 | ('created_at', models.DateTimeField(auto_now_add=True)), 23 | ('updated_at', models.DateTimeField(auto_now=True)), 24 | ('product', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='outflows', to='products.product')), 25 | ], 26 | ), 27 | ] 28 | -------------------------------------------------------------------------------- /outflows/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pycodebr/sge/18d17f898802a3c67b3fbb0463757f0c182d1717/outflows/migrations/__init__.py -------------------------------------------------------------------------------- /outflows/models.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | from products.models import Product 3 | 4 | 5 | class Outflow(models.Model): 6 | product = models.ForeignKey(Product, on_delete=models.PROTECT, related_name='outflows') 7 | quantity = models.IntegerField() 8 | description = models.TextField(null=True, blank=True) 9 | created_at = models.DateTimeField(auto_now_add=True) 10 | updated_at = models.DateTimeField(auto_now=True) 11 | 12 | def __str__(self): 13 | return str(self.product) 14 | -------------------------------------------------------------------------------- /outflows/serializers.py: -------------------------------------------------------------------------------- 1 | from rest_framework import serializers 2 | from outflows.models import Outflow 3 | 4 | 5 | class OutflowSerializer(serializers.ModelSerializer): 6 | 7 | class Meta: 8 | model = Outflow 9 | fields = '__all__' 10 | -------------------------------------------------------------------------------- /outflows/signals.py: -------------------------------------------------------------------------------- 1 | from django.db.models.signals import post_save 2 | from django.dispatch import receiver 3 | from .models import Outflow 4 | 5 | 6 | @receiver(post_save, sender=Outflow) 7 | def update_product_quantity(sender, instance, created, **kwargs): 8 | if created: 9 | if instance.quantity > 0: 10 | product = instance.product 11 | product.quantity -= instance.quantity 12 | product.save() 13 | -------------------------------------------------------------------------------- /outflows/templates/outflow_create.html: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} 2 | 3 | {% block content %} 4 |
5 |

Cadastrar Saída

6 | 7 |
8 |
9 |
10 | {% csrf_token %} 11 | {{ form.as_p }} 12 | 13 |
14 |
15 |
16 | Cancelar e Voltar para a Lista de Saídas 17 |
18 | {% endblock %} 19 | -------------------------------------------------------------------------------- /outflows/templates/outflow_detail.html: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} 2 | 3 | {% block content %} 4 |
5 |

Detalhes da Saída

6 | 7 |
8 |
9 |

{{ object.product }}

10 |

Fornecedor: {{ object.supplier }}

11 |

Quantidade: {{ object.quantity }}

12 |

{{ object.description }}

13 |
14 |
15 | 16 | Voltar para a Lista de Saídas 17 |
18 | {% endblock %} 19 | -------------------------------------------------------------------------------- /outflows/templates/outflow_list.html: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} 2 | 3 | {% block title %} 4 | SGE - Saídas 5 | {% endblock %} 6 | 7 | {% block content %} 8 | 9 | {% if perms.outflows.view_outflow %} 10 | {% include 'components/_sales_metrics.html' %} 11 | {% endif %} 12 | 13 |
14 | 15 |
16 |
17 |
18 |
19 | 20 | 23 |
24 |
25 |
26 | {% if perms.outflows.add_outflow %} 27 | 32 | {% endif %} 33 |
34 | 35 |
36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | {% for outflow in outflows %} 49 | 50 | 55 | 56 | 57 | 58 | 59 | 64 | 65 | {% endfor %} 66 | 67 |
IDProdutoFornecedorQuantidadeData de entradaAções
51 | 52 | {{ outflow.id }} 53 | 54 | {{ outflow.product }}{{ outflow.supplier }}{{ outflow.quantity }}{{ outflow.created_at }} 60 | 61 | 62 | 63 |
68 |
69 | 70 | {% include 'components/_pagination.html' %} 71 | 72 | {% endblock %} -------------------------------------------------------------------------------- /outflows/urls.py: -------------------------------------------------------------------------------- 1 | from django.urls import path 2 | from . import views 3 | 4 | 5 | urlpatterns = [ 6 | path('outflows/list/', views.OutflowListView.as_view(), name='outflow_list'), 7 | path('outflows/create/', views.OutflowCreateView.as_view(), name='outflow_create'), 8 | path('outflows//detail/', views.OutflowDetailView.as_view(), name='outflow_detail'), 9 | 10 | path('api/v1/outflows/', views.OutflowCreateListAPIView.as_view(), name='outflow-create-list-api-view'), 11 | path('api/v1/outflows//', views.OutflowRetrieveAPIView.as_view(), name='outflow-detail-api-view'), 12 | ] 13 | -------------------------------------------------------------------------------- /outflows/views.py: -------------------------------------------------------------------------------- 1 | from rest_framework import generics 2 | from django.contrib.auth.mixins import LoginRequiredMixin, PermissionRequiredMixin 3 | from django.urls import reverse_lazy 4 | from django.views.generic import ListView, CreateView, DetailView 5 | from app import metrics 6 | from . import models, forms, serializers 7 | 8 | 9 | class OutflowListView(LoginRequiredMixin, PermissionRequiredMixin, ListView): 10 | model = models.Outflow 11 | template_name = 'outflow_list.html' 12 | context_object_name = 'outflows' 13 | paginate_by = 10 14 | permission_required = 'outflows.view_outflow' 15 | 16 | def get_queryset(self): 17 | queryset = super().get_queryset() 18 | product = self.request.GET.get('product') 19 | 20 | if product: 21 | queryset = queryset.filter(product__title__icontains=product) 22 | 23 | return queryset 24 | 25 | def get_context_data(self, **kwargs): 26 | context = super().get_context_data(**kwargs) 27 | context['product_metrics'] = metrics.get_product_metrics() 28 | context['sales_metrics'] = metrics.get_sales_metrics() 29 | return context 30 | 31 | 32 | class OutflowCreateView(LoginRequiredMixin, PermissionRequiredMixin, CreateView): 33 | model = models.Outflow 34 | template_name = 'outflow_create.html' 35 | form_class = forms.OutflowForm 36 | success_url = reverse_lazy('outflow_list') 37 | permission_required = 'outflows.add_outflow' 38 | 39 | 40 | class OutflowDetailView(LoginRequiredMixin, PermissionRequiredMixin, DetailView): 41 | model = models.Outflow 42 | template_name = 'outflow_detail.html' 43 | permission_required = 'outflows.view_outflow' 44 | 45 | 46 | class OutflowCreateListAPIView(generics.ListCreateAPIView): 47 | queryset = models.Outflow.objects.all() 48 | serializer_class = serializers.OutflowSerializer 49 | 50 | 51 | class OutflowRetrieveAPIView(generics.RetrieveAPIView): 52 | queryset = models.Outflow.objects.all() 53 | serializer_class = serializers.OutflowSerializer 54 | -------------------------------------------------------------------------------- /products/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pycodebr/sge/18d17f898802a3c67b3fbb0463757f0c182d1717/products/__init__.py -------------------------------------------------------------------------------- /products/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | from . import models 3 | 4 | 5 | class ProductAdmin(admin.ModelAdmin): 6 | list_display = ('title', 'serie_number',) 7 | search_fields = ('title',) 8 | 9 | 10 | admin.site.register(models.Product, ProductAdmin) 11 | -------------------------------------------------------------------------------- /products/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class ProductsConfig(AppConfig): 5 | default_auto_field = 'django.db.models.BigAutoField' 6 | name = 'products' 7 | -------------------------------------------------------------------------------- /products/forms.py: -------------------------------------------------------------------------------- 1 | from django import forms 2 | from . import models 3 | 4 | 5 | class ProductForm(forms.ModelForm): 6 | 7 | class Meta: 8 | model = models.Product 9 | fields = ['title', 'category', 'brand', 'description', 'serie_number', 'cost_price', 'selling_price'] 10 | widgets = { 11 | 'title': forms.TextInput(attrs={'class': 'form-control'}), 12 | 'category': forms.Select(attrs={'class': 'form-control'}), 13 | 'brand': forms.Select(attrs={'class': 'form-control'}), 14 | 'description': forms.Textarea(attrs={'class': 'form-control', 'rows': 3}), 15 | 'serie_number': forms.TextInput(attrs={'class': 'form-control'}), 16 | 'cost_price': forms.NumberInput(attrs={'class': 'form-control'}), 17 | 'selling_price': forms.NumberInput(attrs={'class': 'form-control'}), 18 | } 19 | labels = { 20 | 'title': 'Título', 21 | 'category': 'Categoria', 22 | 'brand': 'Marca', 23 | 'description': 'Descrição', 24 | 'serie_number': 'Número de Série', 25 | 'cost_price': 'Preço de Custo', 26 | 'selling_price': 'Preço de Venda', 27 | } 28 | -------------------------------------------------------------------------------- /products/management/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pycodebr/sge/18d17f898802a3c67b3fbb0463757f0c182d1717/products/management/__init__.py -------------------------------------------------------------------------------- /products/management/commands/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pycodebr/sge/18d17f898802a3c67b3fbb0463757f0c182d1717/products/management/commands/__init__.py -------------------------------------------------------------------------------- /products/management/commands/fazer_coisas.py: -------------------------------------------------------------------------------- 1 | from django.core.management.base import BaseCommand 2 | 3 | 4 | class Command(BaseCommand): 5 | 6 | def handle(self, *args, **options): 7 | self.stdout.write( 8 | self.style.SUCCESS('COISAS FEITAS COM SUCESSO!') 9 | ) 10 | -------------------------------------------------------------------------------- /products/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 5.0.1 on 2024-02-09 21:42 2 | 3 | import django.db.models.deletion 4 | from django.db import migrations, models 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | initial = True 10 | 11 | dependencies = [ 12 | ('brands', '0001_initial'), 13 | ('categories', '0001_initial'), 14 | ] 15 | 16 | operations = [ 17 | migrations.CreateModel( 18 | name='Product', 19 | fields=[ 20 | ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 21 | ('title', models.CharField(max_length=500)), 22 | ('description', models.TextField(blank=True, null=True)), 23 | ('serie_number', models.CharField(blank=True, max_length=200, null=True)), 24 | ('cost_price', models.DecimalField(decimal_places=2, max_digits=20)), 25 | ('selling_price', models.DecimalField(decimal_places=2, max_digits=20)), 26 | ('quantity', models.IntegerField(default=0)), 27 | ('created_at', models.DateTimeField(auto_now_add=True)), 28 | ('updated_at', models.DateTimeField(auto_now=True)), 29 | ('brand', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='products', to='brands.brand')), 30 | ('category', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='products', to='categories.category')), 31 | ], 32 | options={ 33 | 'ordering': ['title'], 34 | }, 35 | ), 36 | ] 37 | -------------------------------------------------------------------------------- /products/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pycodebr/sge/18d17f898802a3c67b3fbb0463757f0c182d1717/products/migrations/__init__.py -------------------------------------------------------------------------------- /products/models.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | from categories.models import Category 3 | from brands.models import Brand 4 | 5 | 6 | class Product(models.Model): 7 | title = models.CharField(max_length=500) 8 | category = models.ForeignKey(Category, on_delete=models.PROTECT, related_name='products') 9 | brand = models.ForeignKey(Brand, on_delete=models.PROTECT, related_name='products') 10 | description = models.TextField(null=True, blank=True) 11 | serie_number = models.CharField(max_length=200, null=True, blank=True) 12 | cost_price = models.DecimalField(max_digits=20, decimal_places=2) 13 | selling_price = models.DecimalField(max_digits=20, decimal_places=2) 14 | quantity = models.IntegerField(default=0) 15 | created_at = models.DateTimeField(auto_now_add=True) 16 | updated_at = models.DateTimeField(auto_now=True) 17 | 18 | class Meta: 19 | ordering = ['title'] 20 | 21 | def __str__(self): 22 | return self.title 23 | -------------------------------------------------------------------------------- /products/serializers.py: -------------------------------------------------------------------------------- 1 | from rest_framework import serializers 2 | from products.models import Product 3 | 4 | 5 | class ProductSerializer(serializers.ModelSerializer): 6 | 7 | class Meta: 8 | model = Product 9 | fields = '__all__' 10 | -------------------------------------------------------------------------------- /products/templates/product_create.html: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} 2 | 3 | {% block content %} 4 |
5 |

Cadastrar Produto

6 | 7 |
8 |
9 |
10 | {% csrf_token %} 11 | {{ form.as_p }} 12 | 13 |
14 |
15 |
16 | Cancelar e Voltar para a Lista de Produtos 17 |
18 | {% endblock %} 19 | -------------------------------------------------------------------------------- /products/templates/product_delete.html: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} 2 | 3 | {% block content %} 4 |
5 |

Deletar Produto

6 | 7 |
8 |
9 |

Tem certeza de que deseja excluir o produto {{ object.title }}?

10 | 11 |
12 | {% csrf_token %} 13 | 14 |
15 | 16 | Cancelar e Voltar para a Lista de Produtos 17 |
18 |
19 |
20 | {% endblock %} 21 | -------------------------------------------------------------------------------- /products/templates/product_detail.html: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} 2 | 3 | {% block content %} 4 |
5 |

Detalhes do Produto

6 | 7 |
8 |
9 |

{{ object.title }}

10 |

Categoria: {{ object.category }}

11 |

Marca: {{ object.brand }}

12 |

{{ object.description }}

13 |

Número de Série: {{ object.serie_number }}

14 |

Preço de Custo: R$ {{ object.cost_price }}

15 |

Preço de Venda: R$ {{ object.selling_price }}

16 |

Quantidade em Estoque: {{ object.quantity }}

17 |
18 |
19 | 20 | Voltar para a Lista de Produtos 21 |
22 | {% endblock %} 23 | -------------------------------------------------------------------------------- /products/templates/product_list.html: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} 2 | 3 | {% block title %} 4 | SGE - Produtos 5 | {% endblock %} 6 | 7 | {% block content %} 8 | 9 | {% if perms.products.view_product and perms.inflows.view_inflow %} 10 | {% include 'components/_product_metrics.html' %} 11 | {% endif %} 12 | 13 |
14 | 15 |
16 |
17 |
18 |
19 | 20 | 21 | 24 |
25 |
26 |
27 | {% if perms.products.add_product %} 28 | 33 | {% endif %} 34 |
35 | 36 |
37 |
38 |
39 |
40 | 46 | 52 | 55 |
56 |
57 |
58 |
59 | 60 |
61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | {% for product in products %} 77 | 78 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 107 | 108 | {% endfor %} 109 | 110 |
IDTítuloCategoriaMarcaPreço de CustoPreço de VendaNúmero de SérieQuantidadeAções
79 | 80 | {{ product.id }} 81 | 82 | {{ product.title }}{{ product.category }}{{ product.brand }}R$ {{ product.cost_price }}R$ {{ product.selling_price }}{{ product.serie_number }}{{ product.quantity }} 91 | 92 | 93 | 94 | 95 | {% if perms.products.change_product %} 96 | 97 | 98 | 99 | {% endif %} 100 | 101 | {% if perms.products.delete_product %} 102 | 103 | 104 | 105 | {% endif %} 106 |
111 |
112 | 113 | {% include 'components/_pagination.html' %} 114 | 115 | {% endblock %} 116 | -------------------------------------------------------------------------------- /products/templates/product_update.html: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} 2 | 3 | {% block content %} 4 |
5 |

Editar Produto

6 | 7 |
8 |
9 |
10 | {% csrf_token %} 11 | {{ form.as_p }} 12 | 13 |
14 |
15 |
16 | 17 | Cancelar e Voltar para a Lista de Produtos 18 |
19 | {% endblock %} 20 | -------------------------------------------------------------------------------- /products/urls.py: -------------------------------------------------------------------------------- 1 | from django.urls import path 2 | from . import views 3 | 4 | 5 | urlpatterns = [ 6 | path('products/list/', views.ProductListView.as_view(), name='product_list'), 7 | path('products/create/', views.ProductCreateView.as_view(), name='product_create'), 8 | path('products//detail/', views.ProductDetailView.as_view(), name='product_detail'), 9 | path('products//update/', views.ProductUpdateView.as_view(), name='product_update'), 10 | path('products//delete/', views.ProductDeleteView.as_view(), name='product_delete'), 11 | 12 | path('api/v1/products/', views.ProductCreateListAPIView.as_view(), name='product-create-list-api-view'), 13 | path('api/v1/products//', views.ProductRetrieveUpdateDestroyAPIView.as_view(), name='product-detail-api-view'), 14 | ] 15 | -------------------------------------------------------------------------------- /products/views.py: -------------------------------------------------------------------------------- 1 | from rest_framework import generics 2 | from django.contrib.auth.mixins import LoginRequiredMixin, PermissionRequiredMixin 3 | from django.urls import reverse_lazy 4 | from django.views.generic import ListView, CreateView, DetailView, UpdateView, DeleteView 5 | from app import metrics 6 | from brands.models import Brand 7 | from categories.models import Category 8 | from . import models, forms, serializers 9 | 10 | 11 | class ProductListView(LoginRequiredMixin, PermissionRequiredMixin, ListView): 12 | model = models.Product 13 | template_name = 'product_list.html' 14 | context_object_name = 'products' 15 | paginate_by = 10 16 | permission_required = 'products.view_product' 17 | 18 | def get_queryset(self): 19 | queryset = super().get_queryset() 20 | title = self.request.GET.get('title') 21 | serie_number = self.request.GET.get('serie_number') 22 | category = self.request.GET.get('category') 23 | brand = self.request.GET.get('brand') 24 | 25 | if title: 26 | queryset = queryset.filter(title__icontains=title) 27 | if serie_number: 28 | queryset = queryset.filter(serie_number__icontains=serie_number) 29 | if category: 30 | queryset = queryset.filter(category_id=category) 31 | if brand: 32 | queryset = queryset.filter(brand__id=brand) 33 | 34 | return queryset 35 | 36 | def get_context_data(self, **kwargs): 37 | context = super().get_context_data(**kwargs) 38 | context['product_metrics'] = metrics.get_product_metrics() 39 | context['sales_metrics'] = metrics.get_sales_metrics() 40 | context['categories'] = Category.objects.all() 41 | context['brands'] = Brand.objects.all() 42 | return context 43 | 44 | 45 | class ProductCreateView(LoginRequiredMixin, PermissionRequiredMixin, CreateView): 46 | model = models.Product 47 | template_name = 'product_create.html' 48 | form_class = forms.ProductForm 49 | success_url = reverse_lazy('product_list') 50 | permission_required = 'products.add_product' 51 | 52 | 53 | class ProductDetailView(LoginRequiredMixin, PermissionRequiredMixin, DetailView): 54 | model = models.Product 55 | template_name = 'product_detail.html' 56 | permission_required = 'products.view_product' 57 | 58 | 59 | class ProductUpdateView(LoginRequiredMixin, PermissionRequiredMixin, UpdateView): 60 | model = models.Product 61 | template_name = 'product_update.html' 62 | form_class = forms.ProductForm 63 | success_url = reverse_lazy('product_list') 64 | permission_required = 'products.change_product' 65 | 66 | 67 | class ProductDeleteView(LoginRequiredMixin, PermissionRequiredMixin, DeleteView): 68 | model = models.Product 69 | template_name = 'product_delete.html' 70 | success_url = reverse_lazy('product_list') 71 | permission_required = 'products.delete_product' 72 | 73 | 74 | class ProductCreateListAPIView(generics.ListCreateAPIView): 75 | queryset = models.Product.objects.all() 76 | serializer_class = serializers.ProductSerializer 77 | 78 | 79 | class ProductRetrieveUpdateDestroyAPIView(generics.RetrieveUpdateDestroyAPIView): 80 | queryset = models.Product.objects.all() 81 | serializer_class = serializers.ProductSerializer 82 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | annotated-types==0.7.0 2 | anyio==4.8.0 3 | asgiref==3.8.1 4 | certifi==2024.12.14 5 | distro==1.9.0 6 | Django==5.0.1 7 | djangorestframework==3.15.1 8 | djangorestframework-simplejwt==5.3.1 9 | h11==0.14.0 10 | httpcore==1.0.7 11 | httpx==0.28.1 12 | idna==3.10 13 | openai==1.38.0 14 | psycopg2-binary==2.9.10 15 | pydantic==2.10.6 16 | pydantic_core==2.27.2 17 | PyJWT==2.10.1 18 | sniffio==1.3.1 19 | sqlparse==0.5.3 20 | tqdm==4.67.1 21 | typing_extensions==4.12.2 22 | -------------------------------------------------------------------------------- /requirements_dev.txt: -------------------------------------------------------------------------------- 1 | flake8==7.0.0 2 | -------------------------------------------------------------------------------- /suppliers/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pycodebr/sge/18d17f898802a3c67b3fbb0463757f0c182d1717/suppliers/__init__.py -------------------------------------------------------------------------------- /suppliers/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | from . import models 3 | 4 | 5 | class SupplierAdmin(admin.ModelAdmin): 6 | list_display = ('name', 'description',) 7 | search_fields = ('name',) 8 | 9 | 10 | admin.site.register(models.Supplier, SupplierAdmin) 11 | -------------------------------------------------------------------------------- /suppliers/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class SuppliersConfig(AppConfig): 5 | default_auto_field = 'django.db.models.BigAutoField' 6 | name = 'suppliers' 7 | -------------------------------------------------------------------------------- /suppliers/forms.py: -------------------------------------------------------------------------------- 1 | from django import forms 2 | from . import models 3 | 4 | 5 | class SupplierForm(forms.ModelForm): 6 | 7 | class Meta: 8 | model = models.Supplier 9 | fields = ['name', 'description'] 10 | widgets = { 11 | 'name': forms.TextInput(attrs={'class': 'form-control'}), 12 | 'description': forms.Textarea(attrs={'class': 'form-control', 'rows': 3}), 13 | } 14 | labels = { 15 | 'name': 'Nome', 16 | 'description': 'Descrição', 17 | } 18 | -------------------------------------------------------------------------------- /suppliers/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 5.0.1 on 2024-02-09 21:42 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | initial = True 9 | 10 | dependencies = [ 11 | ] 12 | 13 | operations = [ 14 | migrations.CreateModel( 15 | name='Supplier', 16 | fields=[ 17 | ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 18 | ('name', models.CharField(max_length=500)), 19 | ('description', models.TextField(blank=True, null=True)), 20 | ('created_at', models.DateTimeField(auto_now_add=True)), 21 | ('updated_at', models.DateTimeField(auto_now=True)), 22 | ], 23 | options={ 24 | 'ordering': ['name'], 25 | }, 26 | ), 27 | ] 28 | -------------------------------------------------------------------------------- /suppliers/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pycodebr/sge/18d17f898802a3c67b3fbb0463757f0c182d1717/suppliers/migrations/__init__.py -------------------------------------------------------------------------------- /suppliers/models.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | 3 | 4 | class Supplier(models.Model): 5 | name = models.CharField(max_length=500) 6 | description = models.TextField(null=True, blank=True) 7 | created_at = models.DateTimeField(auto_now_add=True) 8 | updated_at = models.DateTimeField(auto_now=True) 9 | 10 | class Meta: 11 | ordering = ['name'] 12 | 13 | def __str__(self): 14 | return self.name 15 | -------------------------------------------------------------------------------- /suppliers/serializers.py: -------------------------------------------------------------------------------- 1 | from rest_framework import serializers 2 | from suppliers.models import Supplier 3 | 4 | 5 | class SupplierSerializer(serializers.ModelSerializer): 6 | 7 | class Meta: 8 | model = Supplier 9 | fields = '__all__' 10 | -------------------------------------------------------------------------------- /suppliers/templates/supplier_create.html: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} 2 | 3 | {% block content %} 4 |
5 |

Cadastrar Fornecedor

6 | 7 |
8 |
9 |
10 | {% csrf_token %} 11 | {{ form.as_p }} 12 | 13 |
14 |
15 |
16 | Cancelar e Voltar para a Lista de Fornecedores 17 |
18 | {% endblock %} 19 | -------------------------------------------------------------------------------- /suppliers/templates/supplier_delete.html: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} 2 | 3 | {% block content %} 4 |
5 |

Deletar Fornecedor

6 | 7 |
8 |
9 |

Tem certeza de que deseja excluir o fornecedor {{ object.name }}?

10 | 11 |
12 | {% csrf_token %} 13 | 14 |
15 | 16 | Cancelar e Voltar para a Lista de Fornecedores 17 |
18 |
19 |
20 | {% endblock %} 21 | -------------------------------------------------------------------------------- /suppliers/templates/supplier_detail.html: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} 2 | 3 | {% block content %} 4 |
5 |

Detalhes do Fornecedor

6 | 7 |
8 |
9 |

{{ object.name }}

10 |

{{ object.description }}

11 |
12 |
13 | 14 | Voltar para a Lista de Fornecedores 15 |
16 | {% endblock %} 17 | -------------------------------------------------------------------------------- /suppliers/templates/supplier_list.html: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} 2 | 3 | {% block title %} 4 | SGE - Fornecedores 5 | {% endblock %} 6 | 7 | {% block content %} 8 | 9 |
10 |
11 |
12 |
13 | 14 | 17 |
18 |
19 |
20 | {% if perms.suppliers.add_supplier %} 21 | 26 | {% endif %} 27 |
28 | 29 |
30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | {% for supplier in suppliers %} 41 | 42 | 47 | 48 | 49 | 66 | 67 | {% endfor %} 68 | 69 |
IDNomeDescriçãoAções
43 | 44 | {{ supplier.id }} 45 | 46 | {{ supplier.name }}{{ supplier.description }} 50 | 51 | 52 | 53 | 54 | {% if perms.suppliers.change_supplier %} 55 | 56 | 57 | 58 | {% endif %} 59 | 60 | {% if perms.suppliers.delete_supplier %} 61 | 62 | 63 | 64 | {% endif %} 65 |
70 |
71 | 72 | {% include 'components/_pagination.html' %} 73 | 74 | {% endblock %} -------------------------------------------------------------------------------- /suppliers/templates/supplier_update.html: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} 2 | 3 | {% block content %} 4 |
5 |

Editar Fornecedor

6 | 7 |
8 |
9 |
10 | {% csrf_token %} 11 | {{ form.as_p }} 12 | 13 |
14 |
15 |
16 | 17 | Cancelar e Voltar para a Lista de Fornecedores 18 |
19 | {% endblock %} 20 | -------------------------------------------------------------------------------- /suppliers/urls.py: -------------------------------------------------------------------------------- 1 | from django.urls import path 2 | from . import views 3 | 4 | 5 | urlpatterns = [ 6 | path('suppliers/list/', views.SupplierListView.as_view(), name='supplier_list'), 7 | path('suppliers/create/', views.SupplierCreateView.as_view(), name='supplier_create'), 8 | path('suppliers//detail/', views.SupplierDetailView.as_view(), name='supplier_detail'), 9 | path('suppliers//update/', views.SupplierUpdateView.as_view(), name='supplier_update'), 10 | path('suppliers//delete/', views.SupplierDeleteView.as_view(), name='supplier_delete'), 11 | 12 | path('api/v1/suppliers/', views.SupplierCreateListAPIView.as_view(), name='supplier-create-list-api-view'), 13 | path('api/v1/suppliers//', views.SupplierRetrieveUpdateDestroyAPIView.as_view(), name='supplier-detail-api-view'), 14 | ] 15 | -------------------------------------------------------------------------------- /suppliers/views.py: -------------------------------------------------------------------------------- 1 | from rest_framework import generics 2 | from django.contrib.auth.mixins import LoginRequiredMixin, PermissionRequiredMixin 3 | from django.urls import reverse_lazy 4 | from django.views.generic import ListView, CreateView, DetailView, UpdateView, DeleteView 5 | from . import models, forms, serializers 6 | 7 | 8 | class SupplierListView(LoginRequiredMixin, PermissionRequiredMixin, ListView): 9 | model = models.Supplier 10 | template_name = 'supplier_list.html' 11 | context_object_name = 'suppliers' 12 | paginate_by = 10 13 | permission_required = 'suppliers.view_supplier' 14 | 15 | def get_queryset(self): 16 | queryset = super().get_queryset() 17 | name = self.request.GET.get('name') 18 | 19 | if name: 20 | queryset = queryset.filter(name__icontains=name) 21 | 22 | return queryset 23 | 24 | 25 | class SupplierCreateView(LoginRequiredMixin, PermissionRequiredMixin, CreateView): 26 | model = models.Supplier 27 | template_name = 'supplier_create.html' 28 | form_class = forms.SupplierForm 29 | success_url = reverse_lazy('supplier_list') 30 | permission_required = 'suppliers.add_supplier' 31 | 32 | 33 | class SupplierDetailView(LoginRequiredMixin, PermissionRequiredMixin, DetailView): 34 | model = models.Supplier 35 | template_name = 'supplier_detail.html' 36 | permission_required = 'suppliers.view_supplier' 37 | 38 | 39 | class SupplierUpdateView(LoginRequiredMixin, PermissionRequiredMixin, UpdateView): 40 | model = models.Supplier 41 | template_name = 'supplier_update.html' 42 | form_class = forms.SupplierForm 43 | success_url = reverse_lazy('supplier_list') 44 | permission_required = 'suppliers.change_supplier' 45 | 46 | 47 | class SupplierDeleteView(LoginRequiredMixin, PermissionRequiredMixin, DeleteView): 48 | model = models.Supplier 49 | template_name = 'supplier_delete.html' 50 | success_url = reverse_lazy('supplier_list') 51 | permission_required = 'suppliers.delete_supplier' 52 | 53 | 54 | class SupplierCreateListAPIView(generics.ListCreateAPIView): 55 | queryset = models.Supplier.objects.all() 56 | serializer_class = serializers.SupplierSerializer 57 | 58 | 59 | class SupplierRetrieveUpdateDestroyAPIView(generics.RetrieveUpdateDestroyAPIView): 60 | queryset = models.Supplier.objects.all() 61 | serializer_class = serializers.SupplierSerializer 62 | --------------------------------------------------------------------------------