diff --git a/multigranja/apps/core/__init__.py b/multigranja/apps/core/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/multigranja/apps/core/admin.py b/multigranja/apps/core/admin.py
new file mode 100644
index 0000000..8c38f3f
--- /dev/null
+++ b/multigranja/apps/core/admin.py
@@ -0,0 +1,3 @@
+from django.contrib import admin
+
+# Register your models here.
diff --git a/multigranja/apps/core/apps.py b/multigranja/apps/core/apps.py
new file mode 100644
index 0000000..26f78a8
--- /dev/null
+++ b/multigranja/apps/core/apps.py
@@ -0,0 +1,5 @@
+from django.apps import AppConfig
+
+
+class CoreConfig(AppConfig):
+ name = 'core'
diff --git a/multigranja/apps/core/context_processors.py b/multigranja/apps/core/context_processors.py
new file mode 100644
index 0000000..ba35f29
--- /dev/null
+++ b/multigranja/apps/core/context_processors.py
@@ -0,0 +1,22 @@
+from entities.models import Farm
+
+# Injects the selected farm and the list of farms into the template context for authenticated users.
+def selected_farm_processor(request):
+ selected_farm = None
+ farms = None
+
+ if request.user.is_authenticated:
+ farms = request.user.farms.only("id", "name")
+
+ farm_id = request.session.get('selected_farm_id')
+
+ if farm_id:
+ try:
+ selected_farm = farms.get(id=farm_id)
+ except Farm.DoesNotExist:
+ request.session.pop('selected_farm_id', None)
+
+ return {
+ 'SELECTED_FARM': selected_farm,
+ 'farms': farms
+ }
\ No newline at end of file
diff --git a/multigranja/apps/core/decorators.py b/multigranja/apps/core/decorators.py
new file mode 100644
index 0000000..0f21990
--- /dev/null
+++ b/multigranja/apps/core/decorators.py
@@ -0,0 +1,18 @@
+from django.shortcuts import redirect
+from django.urls import reverse
+from functools import wraps
+
+FARM_SELECT_URL = 'entities:user:farm_select'
+
+def farm_required(view_func):
+ """
+ Decorator that ensures the user has a farm selected in the session.
+ If there isn't one, it redirects to the farm selection screen.
+ """
+ @wraps(view_func)
+ def wrapper(request, *args, **kwargs):
+ if 'selected_farm_id' not in request.session:
+ return redirect(reverse(FARM_SELECT_URL))
+ return view_func(request, *args, **kwargs)
+
+ return wrapper
\ No newline at end of file
diff --git a/multigranja/apps/core/middlewares.py b/multigranja/apps/core/middlewares.py
new file mode 100644
index 0000000..1d1d652
--- /dev/null
+++ b/multigranja/apps/core/middlewares.py
@@ -0,0 +1,24 @@
+from django.shortcuts import redirect
+from django.conf import settings
+
+# Forces the user to be logged in the system
+# A URL whitelist can be found in the settings.py file
+class LoginRequiredMiddleware:
+ def __init__(self, get_response):
+ self.get_response = get_response
+ self.exempt_paths = getattr(settings, 'LOGIN_EXEMPT_PATHS', [])
+ self.exempt_prefixes = getattr(settings, 'LOGIN_EXEMPT_PREFIX', [])
+
+ def __call__(self, request):
+ path = request.path_info
+
+ if any(path.startswith(prefix) for prefix in self.exempt_prefixes):
+ return self.get_response(request)
+
+ if path in self.exempt_paths:
+ return self.get_response(request)
+
+ if not request.user.is_authenticated:
+ return redirect(settings.LOGIN_URL)
+
+ return self.get_response(request)
\ No newline at end of file
diff --git a/multigranja/apps/core/models.py b/multigranja/apps/core/models.py
new file mode 100644
index 0000000..71a8362
--- /dev/null
+++ b/multigranja/apps/core/models.py
@@ -0,0 +1,3 @@
+from django.db import models
+
+# Create your models here.
diff --git a/multigranja/apps/core/templates/404.html b/multigranja/apps/core/templates/404.html
new file mode 100644
index 0000000..e69de29
diff --git a/multigranja/apps/core/templates/base.html b/multigranja/apps/core/templates/base.html
new file mode 100644
index 0000000..7161b8b
--- /dev/null
+++ b/multigranja/apps/core/templates/base.html
@@ -0,0 +1,29 @@
+{% load static %}
+
+
+
+
+ {{ title }}
+
+
+
+
+
+
+
+
+
+
+
+ {% block extra_css %}{% endblock %}
+
+
+ {% include 'includes/_sidebar.html' %}
+ {% include 'includes/_navbar.html' %}
+
+
+ {% block content %}{% endblock %}
+
+
+
+
\ No newline at end of file
diff --git a/multigranja/apps/core/templates/entity_home.html b/multigranja/apps/core/templates/entity_home.html
new file mode 100644
index 0000000..8e6960f
--- /dev/null
+++ b/multigranja/apps/core/templates/entity_home.html
@@ -0,0 +1,48 @@
+{% extends 'base.html' %}
+{% load static %}
+
+{% block extra_css %}
+
+{% endblock %}
+
+{% block content %}
+
+
{{entity_name}}
+
+
Novo
+
+ {% if not entities %}
+ Nenhum registro encontrado.
+ {% else %}
+
+
+ | Identificador |
+ Idade |
+ Peso |
+ Editar |
+ Excluir |
+
+ {% for entity in entities %}
+
+ | {{ animal.identifier }} |
+ {{ animal.age }} |
+ {{ animal.weight }} Kg |
+ |
+
+
+ |
+
+ {% endfor %}
+
+ {% endif %}
+{% endblock %}
\ No newline at end of file
diff --git a/multigranja/apps/core/templates/farm_select.html b/multigranja/apps/core/templates/farm_select.html
new file mode 100644
index 0000000..f6efea4
--- /dev/null
+++ b/multigranja/apps/core/templates/farm_select.html
@@ -0,0 +1,59 @@
+{% load static %}
+
+
+
+
+ Seleção de Fazenda
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Selecione sua fazenda
+
+ {% if farms %}
+
+ {% else %}
+
+ {% endif %}
+
+
+
\ No newline at end of file
diff --git a/multigranja/apps/core/templates/includes/_form.html b/multigranja/apps/core/templates/includes/_form.html
new file mode 100644
index 0000000..8bd2c53
--- /dev/null
+++ b/multigranja/apps/core/templates/includes/_form.html
@@ -0,0 +1,6 @@
+
\ No newline at end of file
diff --git a/multigranja/apps/core/templates/includes/_navbar.html b/multigranja/apps/core/templates/includes/_navbar.html
new file mode 100644
index 0000000..5c740e1
--- /dev/null
+++ b/multigranja/apps/core/templates/includes/_navbar.html
@@ -0,0 +1,28 @@
+
diff --git a/multigranja/apps/core/templates/includes/_sidebar.html b/multigranja/apps/core/templates/includes/_sidebar.html
new file mode 100644
index 0000000..962f36a
--- /dev/null
+++ b/multigranja/apps/core/templates/includes/_sidebar.html
@@ -0,0 +1,48 @@
+
diff --git a/multigranja/apps/core/templates/login.html b/multigranja/apps/core/templates/login.html
new file mode 100644
index 0000000..75f6e39
--- /dev/null
+++ b/multigranja/apps/core/templates/login.html
@@ -0,0 +1,39 @@
+{% load static %}
+
+
+
+
+ Login
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Multifarm pastoreio
+
+
+
+
+
\ No newline at end of file
diff --git a/multigranja/apps/core/tests.py b/multigranja/apps/core/tests.py
new file mode 100644
index 0000000..7ce503c
--- /dev/null
+++ b/multigranja/apps/core/tests.py
@@ -0,0 +1,3 @@
+from django.test import TestCase
+
+# Create your tests here.
diff --git a/multigranja/apps/core/views.py b/multigranja/apps/core/views.py
new file mode 100644
index 0000000..91ea44a
--- /dev/null
+++ b/multigranja/apps/core/views.py
@@ -0,0 +1,3 @@
+from django.shortcuts import render
+
+# Create your views here.
diff --git a/multigranja/apps/entities/__init__.py b/multigranja/apps/entities/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/multigranja/apps/entities/admin.py b/multigranja/apps/entities/admin.py
new file mode 100644
index 0000000..6c53895
--- /dev/null
+++ b/multigranja/apps/entities/admin.py
@@ -0,0 +1,18 @@
+from django.contrib import admin
+
+from .models.user import User
+from .models.farm import Farm, PaddockGroup, Paddock
+from .models.animal import Animal, Batch, BatchAnimal, FeedingRecord
+from .models.rotation import Rotation
+
+admin.site.register([
+ User,
+ Farm,
+ PaddockGroup,
+ Paddock,
+ Animal,
+ Batch,
+ BatchAnimal,
+ FeedingRecord,
+ Rotation,
+])
\ No newline at end of file
diff --git a/multigranja/apps/entities/apps.py b/multigranja/apps/entities/apps.py
new file mode 100644
index 0000000..20a0283
--- /dev/null
+++ b/multigranja/apps/entities/apps.py
@@ -0,0 +1,5 @@
+from django.apps import AppConfig
+
+
+class EntitiesConfig(AppConfig):
+ name = 'entities'
\ No newline at end of file
diff --git a/multigranja/apps/entities/forms/__init__.py b/multigranja/apps/entities/forms/__init__.py
new file mode 100644
index 0000000..dbd3f91
--- /dev/null
+++ b/multigranja/apps/entities/forms/__init__.py
@@ -0,0 +1,4 @@
+from .animal import *
+from .farm import *
+from .rotation import *
+from .user import *
\ No newline at end of file
diff --git a/multigranja/apps/entities/forms/animal.py b/multigranja/apps/entities/forms/animal.py
new file mode 100644
index 0000000..f7c9e93
--- /dev/null
+++ b/multigranja/apps/entities/forms/animal.py
@@ -0,0 +1,45 @@
+from django import forms
+from ..models.animal import Batch, Animal
+
+class AnimalCreateForm(forms.ModelForm):
+ class Meta:
+ model = Animal
+ fields = ['identifier', 'birth', 'weight']
+
+ labels = {
+ 'identifier': 'Identificador',
+ 'birth': 'Data de Nascimento',
+ 'weight': 'Peso (kg)',
+ }
+
+ widgets = {
+ 'birth': forms.DateInput(attrs={'type': 'date'}, format='%Y-%m-%d'),
+ 'weight': forms.NumberInput(attrs={'step': '0.01'})
+ }
+
+class AnimalEditForm(forms.ModelForm):
+ class Meta:
+ model = Animal
+ fields = ['identifier', 'weight']
+
+ labels = {
+ 'identifier': 'Identificador',
+ 'weight': 'Peso (kg)',
+ }
+
+ widgets = {
+ 'weight': forms.NumberInput(attrs={'step': '0.01'})
+ }
+
+class BatchForm(forms.ModelForm):
+ class Meta:
+ model = Batch
+ fields = ['identifier', 'animal_category', 'batch_type', 'animals']
+
+ labels = {
+ 'identifier': 'Identificador',
+ 'animal_category': 'Categoria animal',
+ 'batch_type': 'Tipo',
+ 'animals': 'Animais',
+ }
+BatchCreateForm = BatchEditForm = BatchForm # Alias to BatchForm since its implementations are the same
\ No newline at end of file
diff --git a/multigranja/apps/entities/forms/farm.py b/multigranja/apps/entities/forms/farm.py
new file mode 100644
index 0000000..fda5d3a
--- /dev/null
+++ b/multigranja/apps/entities/forms/farm.py
@@ -0,0 +1,37 @@
+from django import forms
+from ..models import Farm, PaddockGroup, Paddock
+
+class FarmForm(forms.ModelForm):
+ class Meta:
+ model = Farm
+ fields = ['name', 'total_area']
+
+ labels = {
+ 'name': 'Nome',
+ 'total_area': 'Área total',
+ }
+FarmCreateForm = FarmEditForm = FarmForm # Alias since its implementations are the same
+
+class PaddockGroupForm(forms.ModelForm):
+ class Meta:
+ model = PaddockGroup
+ fields = ['identifier', 'description']
+
+ labels = {
+ 'identifier': 'Identificador',
+ 'description': 'Descrição',
+ }
+PaddockGroupCreateForm = PaddockGroupEditForm = PaddockGroupForm
+
+class PaddockForm(forms.ModelForm):
+ class Meta:
+ model = Paddock
+ fields = ['paddock_number', 'area', 'load_capacity', 'paddock_group']
+
+ labels = {
+ 'paddock_number': 'Número do piquete',
+ 'area': 'Área',
+ 'load_capacity': 'Lotação máxima',
+ 'paddock_group': 'Bloco',
+ }
+PaddockCreateForm = PaddockEditForm = PaddockForm
\ No newline at end of file
diff --git a/multigranja/apps/entities/forms/rotation.py b/multigranja/apps/entities/forms/rotation.py
new file mode 100644
index 0000000..c15706b
--- /dev/null
+++ b/multigranja/apps/entities/forms/rotation.py
@@ -0,0 +1,23 @@
+from django import forms
+from ..models import Rotation
+
+class RotationEntryForm(forms.ModelForm):
+ class Meta:
+ model = Rotation
+ fields = ['batch', 'entry_height', 'paddock']
+
+ labels = {
+ 'batch': 'Lote de animais',
+ 'entry_height': 'Altura do pasto',
+ 'paddock': 'Piquete',
+ }
+
+class RotationExitForm(forms.ModelForm):
+ class Meta:
+ model = Rotation
+ fields = ['batch', 'exit_height']
+
+ labels = {
+ 'batch': 'Lote de animais',
+ 'exit_height': 'Altura do pasto',
+ }
\ No newline at end of file
diff --git a/multigranja/apps/entities/forms/user.py b/multigranja/apps/entities/forms/user.py
new file mode 100644
index 0000000..46ca0e3
--- /dev/null
+++ b/multigranja/apps/entities/forms/user.py
@@ -0,0 +1,35 @@
+from django import forms
+from ..models import User
+
+class UserCreateForm(forms.ModelForm):
+ class Meta:
+ model = User
+ fields = ['name', 'login', 'email',]
+
+ labels = {
+ 'name': 'Nome',
+ 'login': 'Login',
+ 'email': 'E-mail',
+ }
+
+ widgets = {
+ 'email': forms.EmailInput(),
+ }
+
+class UserEditForm(forms.ModelForm):
+ class Meta:
+ model = User
+ fields = ['name', 'email',]
+
+ labels = {
+ 'name': 'Nome',
+ 'email': 'E-mail',
+ }
+
+ widgets = {
+ 'email': forms.EmailInput(),
+ }
+
+class UserLoginForm(forms.Form):
+ login = forms.CharField(max_length=45, label='Usuário')
+ password = forms.CharField(widget=forms.PasswordInput, label='Senha')
\ No newline at end of file
diff --git a/multigranja/apps/entities/migrations/0001_initial.py b/multigranja/apps/entities/migrations/0001_initial.py
new file mode 100644
index 0000000..c45bb0b
--- /dev/null
+++ b/multigranja/apps/entities/migrations/0001_initial.py
@@ -0,0 +1,145 @@
+# Generated by Django 6.0.2 on 2026-02-28 19:45
+
+import django.db.models.deletion
+import django.utils.timezone
+import entities.models.user
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ initial = True
+
+ dependencies = [
+ ('auth', '0012_alter_user_first_name_max_length'),
+ ]
+
+ operations = [
+ migrations.CreateModel(
+ name='Animal',
+ fields=[
+ ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+ ('is_active', models.BooleanField(default=True)),
+ ('identifier', models.CharField(max_length=45)),
+ ('birth', models.DateField()),
+ ('weight', models.DecimalField(decimal_places=2, max_digits=10)),
+ ],
+ ),
+ migrations.CreateModel(
+ name='Batch',
+ fields=[
+ ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+ ('is_active', models.BooleanField(default=True)),
+ ('identifier', models.CharField(max_length=45)),
+ ('animal_category', models.CharField(blank=True, max_length=45, null=True)),
+ ('batch_type', models.CharField(blank=True, max_length=45, null=True)),
+ ],
+ ),
+ migrations.CreateModel(
+ name='Farm',
+ fields=[
+ ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+ ('is_active', models.BooleanField(default=True)),
+ ('name', models.CharField(max_length=45)),
+ ('total_area', models.IntegerField(blank=True, null=True)),
+ ],
+ ),
+ migrations.CreateModel(
+ name='PaddockGroup',
+ fields=[
+ ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+ ('is_active', models.BooleanField(default=True)),
+ ('identifier', models.CharField(max_length=45)),
+ ('description', models.TextField(blank=True, null=True)),
+ ],
+ ),
+ migrations.CreateModel(
+ name='BatchAnimal',
+ fields=[
+ ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+ ('is_active', models.BooleanField(default=True)),
+ ('animal', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='entities.animal')),
+ ('batch', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='entities.batch')),
+ ],
+ ),
+ migrations.AddField(
+ model_name='batch',
+ name='animals',
+ field=models.ManyToManyField(related_name='batches', through='entities.BatchAnimal', to='entities.animal'),
+ ),
+ migrations.AddField(
+ model_name='animal',
+ name='farm',
+ field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='animals', to='entities.farm'),
+ ),
+ migrations.CreateModel(
+ name='User',
+ fields=[
+ ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+ ('password', models.CharField(max_length=128, verbose_name='password')),
+ ('name', models.CharField(max_length=45)),
+ ('login', models.CharField(max_length=45, unique=True)),
+ ('email', models.EmailField(max_length=254, unique=True)),
+ ('is_active', models.BooleanField(default=True)),
+ ('is_superuser', models.BooleanField(default=False)),
+ ('is_staff', models.BooleanField(default=False)),
+ ('date_joined', models.DateTimeField(default=django.utils.timezone.now)),
+ ('last_login', models.DateTimeField(blank=True, null=True)),
+ ('groups', models.ManyToManyField(blank=True, help_text='The groups this user belongs to. A user will get all permissions granted to each of their groups.', related_name='user_set', related_query_name='user', to='auth.group', verbose_name='groups')),
+ ('user_permissions', models.ManyToManyField(blank=True, help_text='Specific permissions for this user.', related_name='user_set', related_query_name='user', to='auth.permission', verbose_name='user permissions')),
+ ('farms', models.ManyToManyField(related_name='users', to='entities.farm')),
+ ],
+ options={
+ 'verbose_name': 'User',
+ 'verbose_name_plural': 'Users',
+ },
+ managers=[
+ ('objects', entities.models.user.CustomUserManager()),
+ ],
+ ),
+ migrations.CreateModel(
+ name='Paddock',
+ fields=[
+ ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+ ('is_active', models.BooleanField(default=True)),
+ ('paddock_number', models.IntegerField()),
+ ('area', models.DecimalField(blank=True, decimal_places=2, max_digits=10, null=True)),
+ ('load_capacity', models.IntegerField(blank=True, null=True)),
+ ('occupied', models.BooleanField(default=False)),
+ ('farm', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='paddocks', to='entities.farm')),
+ ('paddock_group', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='paddocks', to='entities.paddockgroup')),
+ ],
+ ),
+ migrations.CreateModel(
+ name='Rotation',
+ fields=[
+ ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+ ('is_active', models.BooleanField(default=True)),
+ ('entry_date', models.DateTimeField()),
+ ('entry_height', models.DecimalField(decimal_places=2, max_digits=10)),
+ ('exit_date', models.DateTimeField(blank=True, null=True)),
+ ('exit_height', models.DecimalField(blank=True, decimal_places=2, max_digits=10, null=True)),
+ ('animal_count', models.IntegerField()),
+ ('livestock_unit', models.DecimalField(decimal_places=2, max_digits=10)),
+ ('batch', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='rotations', to='entities.batch')),
+ ('paddock', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='rotations', to='entities.paddock')),
+ ],
+ ),
+ migrations.CreateModel(
+ name='FeedingRecord',
+ fields=[
+ ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+ ('feed_quantity', models.DecimalField(decimal_places=2, max_digits=10)),
+ ('animal', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='feeding_record', to='entities.animal')),
+ ('rotation', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='feeding_record', to='entities.rotation')),
+ ],
+ ),
+ migrations.AddConstraint(
+ model_name='batchanimal',
+ constraint=models.UniqueConstraint(condition=models.Q(('is_active', True)), fields=('animal',), name='unique_active_animal_batch_link'),
+ ),
+ migrations.AddConstraint(
+ model_name='animal',
+ constraint=models.UniqueConstraint(condition=models.Q(('is_active', True)), fields=('identifier', 'farm'), name='unique_active_farm_animal'),
+ ),
+ ]
diff --git a/multigranja/apps/entities/migrations/__init__.py b/multigranja/apps/entities/migrations/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/multigranja/apps/entities/models/__init__.py b/multigranja/apps/entities/models/__init__.py
new file mode 100644
index 0000000..b0285f3
--- /dev/null
+++ b/multigranja/apps/entities/models/__init__.py
@@ -0,0 +1,4 @@
+from .user import *
+from .farm import *
+from .animal import *
+from .rotation import *
\ No newline at end of file
diff --git a/multigranja/apps/entities/models/animal.py b/multigranja/apps/entities/models/animal.py
new file mode 100644
index 0000000..995fa4c
--- /dev/null
+++ b/multigranja/apps/entities/models/animal.py
@@ -0,0 +1,103 @@
+from datetime import date
+from django.db import models
+from django.db.models import Q
+
+class Animal(models.Model):
+ is_active = models.BooleanField(default=True)
+ identifier = models.CharField(max_length=45)
+ birth = models.DateField()
+ weight = models.DecimalField(max_digits=10, decimal_places=2)
+ farm = models.ForeignKey('entities.Farm', on_delete=models.CASCADE, related_name='animals')
+
+ def __str__(self):
+ return f"Animal {self.identifier} - birth {self.birth} - weight {self.weight} - farm {self.farm} - is_active {self.is_active}"
+
+ @property
+ def age(self):
+ today = date.today()
+ birth_date = self.birth
+
+ years = today.year - birth_date.year
+ months = today.month - birth_date.month
+
+ if today.day < birth_date.day:
+ months -= 1
+
+ if months < 0:
+ months += 12
+ years -= 1
+
+ if years > 0 and months > 0:
+ return f"{years} anos e {months} meses" # YYYY years and MM months
+ elif years > 0:
+ return f"{years} anos" # YYYY years
+ elif months > 0:
+ return f"{months} meses" # MM months
+ else:
+ return "Menos de um mês" # Less than a month
+
+ class Meta:
+ constraints = [
+ models.UniqueConstraint(
+ fields=['identifier', 'farm'],
+ condition=Q(is_active=True),
+ name='unique_active_farm_animal'
+ )
+ ]
+
+class Batch(models.Model):
+ is_active = models.BooleanField(default=True)
+ identifier = models.CharField(max_length=45)
+ animal_category = models.CharField(max_length=45, null=True, blank=True)
+ batch_type = models.CharField(max_length=45, null=True, blank=True)
+ animals = models.ManyToManyField(Animal, through='BatchAnimal', related_name='batches')
+
+ def __init__(self, *args, **kwargs):
+ super().__init__(*args, **kwargs)
+ self.__original_is_active = self.is_active
+
+ def save(self, *args, **kwargs):
+ super().save(*args, **kwargs)
+
+ is_active_changed = self.__original_is_active != self.is_active
+
+ if is_active_changed:
+ if self.is_active is False:
+ self.batchanimal_set.all().update(is_active=False)
+
+ elif self.is_active is True:
+ self.batchanimal_set.all().update(is_active=True)
+
+ self.__original_is_active = self.is_active
+
+ def __str__(self):
+ return self.identifier
+
+class BatchAnimal(models.Model):
+ animal = models.ForeignKey(Animal, on_delete=models.CASCADE)
+ batch = models.ForeignKey(Batch, on_delete=models.CASCADE)
+
+ is_active = models.BooleanField(default=True)
+
+ # When reactivating a batch, all its animals must not belong to any other active batch.
+ # Otherwise, the database will raise an IntegrityError due to the unique constraint on active animals.
+ # This is a rare use case, but any future code regarding it must implement error treatment
+ class Meta:
+ constraints = [
+ models.UniqueConstraint(
+ fields=['animal'],
+ condition=Q(is_active=True),
+ name='unique_active_animal_batch_link'
+ )
+ ]
+
+ def __str__(self):
+ return f"{self.animal.identifier} in {self.batch.identifier}"
+
+class FeedingRecord(models.Model):
+ animal = models.ForeignKey(Animal, on_delete=models.CASCADE, related_name='feeding_record')
+ rotation = models.ForeignKey('entities.Rotation', on_delete=models.CASCADE, related_name='feeding_record')
+ feed_quantity = models.DecimalField(max_digits=10, decimal_places=2)
+
+ def __str__(self):
+ return f"Consumo de {self.animal.identifier} - {self.feed_quantity} kg"
\ No newline at end of file
diff --git a/multigranja/apps/entities/models/farm.py b/multigranja/apps/entities/models/farm.py
new file mode 100644
index 0000000..fac3974
--- /dev/null
+++ b/multigranja/apps/entities/models/farm.py
@@ -0,0 +1,30 @@
+from django.db import models
+
+class Farm(models.Model):
+ is_active = models.BooleanField(default=True)
+ name = models.CharField(max_length=45)
+ total_area = models.IntegerField(null=True, blank=True)
+
+ def __str__(self):
+ return self.name
+
+class PaddockGroup(models.Model):
+ is_active = models.BooleanField(default=True)
+ identifier = models.CharField(max_length=45)
+ description = models.TextField(null=True, blank=True)
+
+ def __str__(self):
+ return f"Bloco {self.id}"
+
+
+class Paddock(models.Model):
+ is_active = models.BooleanField(default=True)
+ paddock_number = models.IntegerField()
+ area = models.DecimalField(max_digits=10, decimal_places=2, null=True, blank=True)
+ load_capacity = models.IntegerField(null=True, blank=True)
+ occupied = models.BooleanField(default=False)
+ farm = models.ForeignKey(Farm, on_delete=models.CASCADE, related_name='paddocks')
+ paddock_group = models.ForeignKey(PaddockGroup, on_delete=models.SET_NULL, related_name='paddocks', null=True, blank=True)
+
+ def __str__(self):
+ return f"Piquete {self.paddock_number} - Bloco {self.paddock_group.id}"
\ No newline at end of file
diff --git a/multigranja/apps/entities/models/rotation.py b/multigranja/apps/entities/models/rotation.py
new file mode 100644
index 0000000..7395261
--- /dev/null
+++ b/multigranja/apps/entities/models/rotation.py
@@ -0,0 +1,15 @@
+from django.db import models
+
+class Rotation(models.Model):
+ is_active = models.BooleanField(default=True)
+ entry_date = models.DateTimeField()
+ entry_height = models.DecimalField(max_digits=10, decimal_places=2)
+ exit_date = models.DateTimeField(null=True, blank=True)
+ exit_height = models.DecimalField(max_digits=10, decimal_places=2, null=True, blank=True)
+ animal_count = models.IntegerField()
+ livestock_unit = models.DecimalField(max_digits=10, decimal_places=2)
+ batch = models.ForeignKey('entities.Batch', on_delete=models.SET_NULL, related_name='rotations', null=True, blank=True)
+ paddock = models.ForeignKey('entities.Paddock', on_delete=models.CASCADE, related_name='rotations')
+
+ def __str__(self):
+ return f"Movimentação {self.id} - Lote {self.batch.identifier}"
\ No newline at end of file
diff --git a/multigranja/apps/entities/models/user.py b/multigranja/apps/entities/models/user.py
new file mode 100644
index 0000000..be2782c
--- /dev/null
+++ b/multigranja/apps/entities/models/user.py
@@ -0,0 +1,63 @@
+from django.db import models
+from django.contrib.auth.models import UserManager, AbstractBaseUser, PermissionsMixin
+from django.utils import timezone
+
+class CustomUserManager(UserManager):
+ def _create_user(self, name, login, email, password, **extra_fields):
+ if not login:
+ raise ValueError("Information provided is not a valid login")
+ if not email:
+ raise ValueError("You must inform an e-mail adress")
+
+ email = self.normalize_email(email)
+ user = self.model(
+ name = name,
+ login = login,
+ email = email,
+ **extra_fields)
+ user.set_password(password)
+ user.save(using=self.db)
+
+ return user
+
+ def create_user(self, name=None, login=None, email=None, password=None, **extra_fields):
+ extra_fields.setdefault('is_staff', False)
+ extra_fields.setdefault('is_superuser', False)
+ return self._create_user(name, login, email, password, **extra_fields)
+
+ def create_superuser(self, name=None, login=None, email=None, password=None, **extra_fields):
+ extra_fields.setdefault('is_staff', True)
+ extra_fields.setdefault('is_superuser', True)
+ return self._create_user(name, login, email, password, **extra_fields)
+
+class User(AbstractBaseUser, PermissionsMixin):
+ name = models.CharField(max_length=45)
+ login = models.CharField(max_length=45, unique=True)
+ email = models.EmailField(unique=True)
+
+ farms = models.ManyToManyField('entities.Farm', related_name='users')
+
+ is_active = models.BooleanField(default=True)
+ is_superuser = models.BooleanField(default=False)
+ is_staff = models.BooleanField(default=False)
+
+ date_joined = models.DateTimeField(default=timezone.now)
+ last_login = models.DateTimeField(blank=True, null=True)
+
+ objects = CustomUserManager()
+
+ USERNAME_FIELD = 'login'
+ EMAIL_FIELD = 'email'
+ REQUIRED_FIELDS = ['name', 'email']
+
+ class Meta:
+ verbose_name = 'User'
+ verbose_name_plural = 'Users'
+
+ @property
+ def full_name(self):
+ return self.name
+
+ @property
+ def short_name(self):
+ return self.name.split(' ')[0] or self.email.split('@')[0]
\ No newline at end of file
diff --git a/multigranja/apps/entities/services/__init__.py b/multigranja/apps/entities/services/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/multigranja/apps/entities/services/animal.py b/multigranja/apps/entities/services/animal.py
new file mode 100644
index 0000000..c3b709c
--- /dev/null
+++ b/multigranja/apps/entities/services/animal.py
@@ -0,0 +1,227 @@
+from django.shortcuts import get_object_or_404
+from django.urls import reverse
+from django.db.models import Prefetch
+
+from ..models import Animal, Batch
+from ..forms import AnimalCreateForm, AnimalEditForm, BatchForm
+from entities.models import Farm
+
+
+def get_selected_farm(request):
+ return Farm.objects.get(id=request.session['selected_farm_id'])
+
+
+# Animal Services
+def get_animal_home_context(request):
+ selected_farm = get_selected_farm(request)
+
+ context = {}
+ context['title'] = 'Animais'
+ context['farm'] = selected_farm
+
+ animals = Animal.objects.filter(
+ farm=selected_farm,
+ is_active=True
+ )
+
+ filter_query = request.GET.get('animal_search')
+
+ if filter_query:
+ context['filter_query'] = filter_query
+ animals = animals.filter(
+ identifier__icontains=filter_query
+ )
+
+ context['animals'] = animals
+
+ return context
+
+
+def get_animal_new_context():
+ form = AnimalCreateForm()
+
+ context = {}
+ context['title'] = 'Novo animal'
+ context['form'] = form
+ context['action_url'] = reverse('entities:animal:animal_new')
+ context['return_url'] = reverse('entities:animal:animal_home')
+
+ return context
+
+
+def create_animal(request):
+ form = AnimalCreateForm(request.POST)
+ selected_farm = get_selected_farm(request)
+
+ if form.is_valid():
+ animal = form.save(commit=False)
+ animal.farm = selected_farm
+ animal.save()
+ return True
+
+ return False
+
+
+def update_animal(request, animal_id):
+ farm_id = request.session['selected_farm_id']
+ animal = get_object_or_404(Animal, pk=animal_id, farm_id=farm_id)
+
+ context = {}
+ context['title'] = 'Editar animal'
+ context['action_url'] = reverse(
+ 'entities:animal:animal_update',
+ kwargs={'animal_id': animal.pk}
+ )
+ context['return_url'] = reverse('entities:animal:animal_home')
+
+ if request.method == 'GET':
+ form = AnimalEditForm(instance=animal)
+
+ elif request.method == 'POST':
+ form = AnimalEditForm(request.POST, instance=animal)
+
+ if form.is_valid():
+ form.save()
+ return {'redirect': 'entities:animal:animal_home'}
+
+ context['form'] = form
+
+ return {'context': context}
+
+
+def delete_animal(request, animal_id):
+ farm_id = request.session['selected_farm_id']
+ animal = get_object_or_404(Animal, pk=animal_id, farm_id=farm_id)
+ animal.is_active = False
+ animal.save()
+
+
+# Batch Services
+
+def get_batch_home_context(request):
+ selected_farm = get_selected_farm(request)
+
+ context = {}
+ context['title'] = 'Lotes'
+ context['farm'] = selected_farm
+
+ all_batches = Batch.objects.filter(
+ batchanimal__animal__farm=selected_farm,
+ is_active=True
+ ).distinct().prefetch_related(
+ Prefetch('animals', queryset=Animal.objects.filter(is_active=True))
+ )
+
+ filter_query = request.GET.get('batch_search')
+
+ if filter_query:
+ context['filter_query'] = filter_query
+ all_batches = all_batches.filter(
+ identifier__icontains=filter_query
+ )
+
+ context['batches'] = all_batches
+
+ return context
+
+
+def create_batch(request):
+ selected_farm = get_selected_farm(request)
+
+ if request.method == 'GET':
+ form = BatchForm()
+ form.fields['animals'].queryset = Animal.objects.filter(
+ farm=selected_farm,
+ is_active=True
+ )
+
+ context = {
+ 'title': 'Novo Lote',
+ 'form': form,
+ 'action_url': reverse('entities:animal:batch_new'),
+ 'return_url': reverse('entities:animal:batch_home')
+ }
+
+ return {'context': context}
+
+ elif request.method == 'POST':
+ form = BatchForm(request.POST)
+
+ if form.is_valid():
+ batch = form.save(commit=False)
+ batch.save()
+ form.save_m2m()
+
+ return {'redirect': 'entities:animal:batch_home'}
+
+ form.fields['animals'].queryset = Animal.objects.filter(
+ farm=selected_farm,
+ is_active=True
+ )
+
+ context = {
+ 'title': 'Novo Lote',
+ 'form': form,
+ 'action_url': reverse('entities:animal:batch_new'),
+ 'return_url': reverse('entities:animal:batch_home')
+ }
+
+ return {'context': context}
+
+
+def update_batch(request, batch_id):
+ batch_old = get_object_or_404(Batch, pk=batch_id, is_active=True)
+ selected_farm = get_selected_farm(request)
+
+ context = {
+ 'title': f'Editar Lote {batch_old.identifier}',
+ 'return_url': reverse('entities:animal:batch_home')
+ }
+
+ if request.method == 'GET':
+ form = BatchForm(instance=batch_old)
+
+ form.fields['animals'].queryset = Animal.objects.filter(
+ farm=selected_farm,
+ is_active=True
+ )
+
+ context['action_url'] = reverse(
+ 'entities:animal:batch_update',
+ kwargs={'batch_id': batch_old.pk}
+ )
+
+ elif request.method == 'POST':
+ form = BatchForm(request.POST)
+
+ if form.is_valid():
+ batch_new = form.save(commit=False)
+ batch_new.is_active = True
+
+ batch_old.is_active = False
+ batch_old.save()
+
+ batch_new.save()
+ form.save_m2m()
+
+ return {'redirect': 'entities:animal:batch_home'}
+
+ form.fields['animals'].queryset = Animal.objects.filter(
+ farm=selected_farm,
+ is_active=True
+ )
+
+ context['action_url'] = reverse(
+ 'entities:animal:batch_update',
+ kwargs={'batch_id': batch_old.pk}
+ )
+
+ context['form'] = form
+
+ return {'context': context}
+
+
+def delete_batch(batch_id):
+ batch = get_object_or_404(Batch, pk=batch_id, is_active=True)
+ batch.is_active = False
+ batch.save()
\ No newline at end of file
diff --git a/multigranja/apps/entities/services/farm.py b/multigranja/apps/entities/services/farm.py
new file mode 100644
index 0000000..a685e5d
--- /dev/null
+++ b/multigranja/apps/entities/services/farm.py
@@ -0,0 +1,293 @@
+from django.shortcuts import get_object_or_404
+from django.urls import reverse
+from django.db.models import Prefetch
+
+from ..models import Farm, Paddock, PaddockGroup
+from ..forms import FarmCreateForm, FarmEditForm, PaddockCreateForm, PaddockEditForm, PaddockGroupCreateForm, PaddockGroupEditForm
+
+
+def get_selected_farm(request):
+ return Farm.objects.get(id=request.session['selected_farm_id'])
+
+
+# Farm Services
+def get_farm_home_context(request):
+ authenticated_user = request.user
+
+ context = {}
+ context['title'] = 'Fazenda'
+
+ farms = Farm.objects.filter(
+ users=authenticated_user, # Verificar se da para deixar apenas = ou deve usar um is in ou algo do tipo
+ is_active=True
+ )
+
+ context['farms'] = farms
+
+ return context
+
+
+def get_farm_new_context():
+ form = FarmCreateForm()
+
+ context = {}
+ context['title'] = 'Nova fazenda'
+ context['form'] = form
+ context['action_url'] = reverse('entities:farm:farm_new')
+ context['return_url'] = reverse('entities:farm:farm_home')
+
+ return context
+
+
+def create_farm(request):
+ form = FarmCreateForm(request.POST)
+ authenticated_user = request.user
+
+ if form.is_valid():
+ farm = form.save()
+ farm.users.add(authenticated_user)
+ return True
+
+ return False
+
+
+def update_farm(request, farm_id):
+ farm = get_object_or_404(Farm, pk=farm_id, is_active=True)
+
+ context = {}
+ context['title'] = 'Editar fazenda'
+ context['action_url'] = reverse(
+ 'entities:farm:farm_update',
+ kwargs={'farm_id': farm.pk}
+ )
+ context['return_url'] = reverse('entities:farm:farm_home')
+
+ if request.method == 'GET':
+ form = FarmEditForm(instance=farm)
+
+ elif request.method == 'POST':
+ form = FarmEditForm(request.POST, instance=farm)
+
+ if form.is_valid():
+ form.save()
+ return {'redirect': 'entities:farm:farm_home'}
+
+ context['form'] = form
+
+ return {'context': context}
+
+
+def delete_farm(farm_id):
+ farm = get_object_or_404(Farm, pk=farm_id, is_active=True)
+ farm.is_active = False
+ farm.save()
+
+
+# Paddock Services
+
+def get_paddock_home_context(request):
+ selected_farm = get_selected_farm(request)
+
+ context = {}
+ context['title'] = 'Piquete'
+ context['farm'] = selected_farm
+
+ all_paddocks = Paddock.objects.filter(
+ farm=selected_farm,
+ is_active=True
+ )
+
+ filter_query = request.GET.get('paddock_search')
+
+ if filter_query:
+ context['filter_query'] = filter_query
+ all_paddocks = all_paddocks.filter(
+ identifier__icontains=filter_query
+ )
+
+ context['paddocks'] = all_paddocks
+
+ return context
+
+
+def create_paddock(request):
+ selected_farm = get_selected_farm(request)
+
+ if request.method == 'GET':
+ form = PaddockCreateForm()
+
+ context = {
+ 'title': 'Novo Piquete',
+ 'form': form,
+ 'action_url': reverse('entities:farm:paddock_new'),
+ 'return_url': reverse('entities:farm:paddock_home')
+ }
+
+ return {'context': context}
+
+ elif request.method == 'POST':
+ form = PaddockCreateForm(request.POST)
+
+ if form.is_valid():
+ paddock = form.save(commit=False)
+ paddock.farm = selected_farm
+ paddock.save()
+
+ return {'redirect': 'entities:farm:paddock_home'}
+
+ context = {
+ 'title': 'Novo Piquete',
+ 'form': form,
+ 'action_url': reverse('entities:farm:paddock_new'),
+ 'return_url': reverse('entities:farm:paddock_home')
+ }
+
+ return {'context': context}
+
+
+def update_paddock(request, paddock_id):
+ farm_id = request.session['selected_farm_id']
+ paddock = get_object_or_404(Paddock, pk=paddock_id, farm_id=farm_id, is_active=True)
+
+ context = {}
+ context['title'] = 'Editar piquete'
+ context['action_url'] = reverse(
+ 'entities:farm:paddock_update',
+ kwargs={'paddock_id': paddock.pk}
+ )
+ context['return_url'] = reverse('entities:farm:paddock_home')
+
+ if request.method == 'GET':
+ form = PaddockEditForm(instance=paddock)
+
+ elif request.method == 'POST':
+ form = PaddockEditForm(request.POST, instance=paddock)
+
+ if form.is_valid():
+ form.save()
+ return {'redirect': 'entities:farm:paddock_home'}
+
+ context['form'] = form
+
+ return {'context': context}
+
+
+def delete_paddock(paddock_id):
+ paddock = get_object_or_404(Paddock, pk=paddock_id, is_active=True)
+ paddock.is_active = False
+ paddock.save()
+
+
+# Paddock Group Services
+
+def get_paddock_group_home_context(request):
+ selected_farm = get_selected_farm(request)
+
+ context = {}
+ context['title'] = 'Blocos'
+ context['farm'] = selected_farm
+
+ all_paddock_groups = PaddockGroup.objects.filter(
+ paddocks__farm=selected_farm,
+ is_active=True
+ ).distinct().prefetch_related(
+ Prefetch('paddocks', queryset=Paddock.objects.filter(is_active=True))
+ )
+
+ filter_query = request.GET.get('paddock_group_search')
+
+ if filter_query:
+ context['filter_query'] = filter_query
+ all_paddock_groups = all_paddock_groups.filter(
+ identifier__icontains=filter_query
+ )
+
+ context['paddock_groups'] = all_paddock_groups
+
+ return context
+
+
+def create_paddock_group(request):
+ selected_farm = get_selected_farm(request)
+
+ if request.method == 'GET':
+ form = PaddockGroupCreateForm()
+ form.fields['paddocks'].queryset = Paddock.objects.filter(
+ farm=selected_farm,
+ is_active=True
+ )
+
+ if request.method == 'POST':
+ form = PaddockGroupCreateForm(request.POST)
+
+ if form.is_valid():
+ paddock_group = form.save(commit=False)
+ paddock_group.save()
+
+ return {'redirect': 'entities:farm:paddock_group_home'}
+
+ form.fields['paddocks'].queryset = Paddock.objects.filter(
+ farm=selected_farm,
+ is_active=True
+ )
+
+ context = {
+ 'title': 'Novo Bloco',
+ 'form': form,
+ 'action_url': reverse('entities:farm:paddock_group_new'),
+ 'return_url': reverse('entities:farm:paddock_group_home')
+ }
+
+ return {'context': context}
+
+
+def update_paddock_group(request, paddock_group_id):
+ selected_farm = get_selected_farm(request)
+ paddock_group = get_object_or_404(PaddockGroup, pk=paddock_group_id, is_active=True)
+
+ context = {
+ 'title': f'Editar Lote {paddock_group.identifier}',
+ 'return_url': reverse('entities:farm:paddock_group_home')
+ }
+
+ if request.method == 'GET':
+ form = PaddockGroupEditForm(instance=paddock_group)
+
+ form.fields['paddocks'].queryset = Paddock.objects.filter(
+ farm=selected_farm,
+ is_active=True
+ )
+
+ context['action_url'] = reverse(
+ 'entities:farm:paddock_group_update',
+ kwargs={'paddock_group_id': paddock_group.pk}
+ )
+
+ elif request.method == 'POST':
+ form = PaddockGroupEditForm(request.POST, instance=paddock_group)
+
+ if form.is_valid():
+ paddock_group = form.save()
+ paddock_group.save()
+
+ return {'redirect': 'entities:farm:paddock_group_home'}
+
+ form.fields['paddocks'].queryset = Paddock.objects.filter(
+ farm=selected_farm,
+ is_active=True
+ )
+
+ context['action_url'] = reverse(
+ 'entities:farm:paddock_group_update',
+ kwargs={'paddock_group_id': paddock_group.pk}
+ )
+
+ context['form'] = form
+
+ return {'context': context}
+
+
+def delete_paddock_group(paddock_group_id):
+ paddock_group = get_object_or_404(PaddockGroup, pk=paddock_group_id, is_active=True)
+ paddock_group.is_active = False
+ paddock_group.save()
\ No newline at end of file
diff --git a/multigranja/apps/entities/services/user.py b/multigranja/apps/entities/services/user.py
new file mode 100644
index 0000000..7cd1123
--- /dev/null
+++ b/multigranja/apps/entities/services/user.py
@@ -0,0 +1,88 @@
+from django.shortcuts import redirect
+from django.contrib.auth import authenticate, login, logout
+from django.db.models import Count
+
+from ..forms import UserLoginForm
+
+
+def user_profile(request): # TO DO
+ ...
+
+
+def user_login(request):
+ if request.user.is_authenticated:
+ return {'redirect': 'entities:user:farm_select'}
+
+ context = {}
+ context['fail_login'] = False
+ form = UserLoginForm()
+
+ if request.method == "POST":
+ form = UserLoginForm(request.POST)
+
+ if form.is_valid():
+ username = form.cleaned_data['login']
+ password = form.cleaned_data['password']
+ user = authenticate(request, username=username, password=password)
+
+ if user is not None:
+ login(request, user)
+ context['title'] = 'Fazendas'
+ return {'redirect': 'entities:user:farm_select'}
+ else:
+ context['fail_login'] = True
+ else:
+ context['fail_login'] = True
+
+ context['form'] = form
+ return {'context': context}
+
+
+def user_logout(request):
+ logout(request)
+ return redirect('entities:user:user_login')
+
+
+def user_signin(request): # TO DO
+ ...
+
+
+def farm_select(request):
+ if 'selected_farm_id' in request.session:
+ return {'redirect': 'entities:farm:farm_home'}
+
+ farms_query = request.user.farms.filter(is_active=True)
+ farms_with_counts = farms_query.annotate(
+ total_animals=Count('animals'),
+ total_paddocks=Count('paddocks')
+ )
+
+ context = {
+ 'farms': farms_with_counts
+ }
+
+ return {'context': context}
+
+
+def farm_set(request, farm_id=None):
+ # Allows setting the selected farm either via URL parameter or POST data
+ farm_id = farm_id or request.POST.get("farm_id")
+ next_url = request.POST.get("next")
+
+ if not farm_id:
+ return redirect('entities:user:farm_select')
+
+ try:
+ farm = request.user.farms.get(id=farm_id)
+ request.session['selected_farm_id'] = farm.id
+
+ return redirect(next_url or 'entities:farm:farm_home')
+
+ except request.user.farms.model.DoesNotExist:
+ return redirect('entities:user:farm_select')
+
+def farm_deselect(request):
+ if 'selected_farm_id' in request.session:
+ del request.session['selected_farm_id']
+
+ return redirect('entities:user:farm_select')
\ No newline at end of file
diff --git a/multigranja/apps/entities/static/animal/css/animal.css b/multigranja/apps/entities/static/animal/css/animal.css
new file mode 100644
index 0000000..5118db5
--- /dev/null
+++ b/multigranja/apps/entities/static/animal/css/animal.css
@@ -0,0 +1,152 @@
+/* ===== PAGE HEADER ===== */
+.animal_title {
+ display: flex;
+ align-items: center;
+ gap: var(--space-md);
+ margin-bottom: var(--space-lg);
+}
+
+.animal_title h1 {
+ font-size: var(--font-xl);
+ color: var(--color-text-dark);
+ margin: 0;
+ margin-right: var(--space-lg);
+}
+
+/* ===== SEARCH FORM ===== */
+.form_animal_search {
+ display: flex;
+ align-items: center;
+ gap: var(--space-xs);
+ margin-left: auto;
+}
+
+.form_animal_search label {
+ font-size: var(--font-sm);
+ color: var(--color-text-dark);
+}
+
+/* Input com altura igual ao botão */
+.form_animal_search input {
+ height: 32px;
+ padding: 0 var(--space-sm);
+ font-size: var(--font-sm);
+ border: 1px solid var(--color-border-subtle);
+ border-radius: var(--border-radius-sm);
+ outline: none;
+}
+
+.form_animal_search input:focus {
+ border-color: var(--color-primary);
+}
+
+/* Botão buscar */
+.form_animal_search button {
+ height: 32px;
+ padding: 0 var(--space-sm);
+ border: none;
+ border-radius: var(--border-radius-sm);
+ background: var(--color-primary);
+ color: var(--color-text-light);
+ cursor: pointer;
+ transition: background var(--transition-speed);
+}
+
+.form_animal_search button:hover {
+ background: var(--color-secondary);
+}
+
+/* ===== CLEAR FILTER ===== */
+.btn-clear-filter {
+ display: flex;
+ align-items: center;
+ gap: 4px;
+ height: 32px;
+ padding: 0 var(--space-sm);
+ background: var(--color-danger-light);
+ color: var(--color-danger);
+ border-radius: var(--border-radius-sm);
+ text-decoration: none;
+ font-size: var(--font-sm);
+}
+
+.btn-clear-filter:hover {
+ background: var(--color-danger);
+ color: var(--color-text-light);
+}
+
+/* ===== NEW BUTTON ===== */
+.animal_title > a {
+ height: 32px;
+ padding: 0 var(--space-md);
+ background: var(--color-success);
+ color: var(--color-text-light);
+ border-radius: var(--border-radius-sm);
+ text-decoration: none;
+ font-size: var(--font-sm);
+ display: flex;
+ align-items: center;
+ gap: var(--space-xs);
+ transition: background var(--transition-speed);
+}
+
+.animal_title > a:hover {
+ background: var(--color-success);
+ opacity: 0.85;
+}
+
+/* ===== TABLE ===== */
+table {
+ width: 100%;
+ border-collapse: collapse;
+ background: var(--color-bg-white);
+ border-radius: var(--border-radius-sm);
+ overflow: hidden;
+}
+
+table th {
+ text-align: left;
+ padding: var(--space-sm);
+ font-size: var(--font-sm);
+ background: var(--color-bg-dark);
+ color: var(--color-text-light);
+ font-weight: 500;
+}
+
+table td {
+ padding: var(--space-sm);
+ font-size: var(--font-md);
+ border-bottom: 1px solid var(--color-border-subtle);
+}
+
+table tr:last-child td {
+ border-bottom: none;
+}
+
+table tbody tr:hover {
+ background: var(--color-bg-light);
+}
+
+/* ===== ACTION ICONS ===== */
+table a {
+ color: var(--color-primary);
+ transition: color var(--transition-speed);
+}
+
+table a:hover {
+ color: var(--color-secondary);
+}
+
+/* DELETE BUTTON */
+.btn-icon-trash {
+ background: transparent;
+ border: none;
+ color: var(--color-danger);
+ cursor: pointer;
+ transition: color var(--transition-speed);
+}
+
+.btn-icon-trash:hover {
+ color: var(--color-danger);
+ opacity: 0.7;
+}
\ No newline at end of file
diff --git a/multigranja/apps/entities/static/animal/css/batch.css b/multigranja/apps/entities/static/animal/css/batch.css
new file mode 100644
index 0000000..d341a1f
--- /dev/null
+++ b/multigranja/apps/entities/static/animal/css/batch.css
@@ -0,0 +1,150 @@
+/* ===== PAGE HEADER ===== */
+.batch_title {
+ display: flex;
+ align-items: center;
+ gap: var(--space-md);
+ margin-bottom: var(--space-lg);
+}
+
+.batch_title h1 {
+ font-size: var(--font-xl);
+ color: var(--color-text-dark);
+ margin: 0;
+ margin-right: var(--space-lg);
+}
+
+/* ===== SEARCH FORM ===== */
+.form_batch_search {
+ display: flex;
+ align-items: center;
+ gap: var(--space-xs);
+ margin-left: auto;
+}
+
+.form_batch_search label {
+ font-size: var(--font-sm);
+ color: var(--color-text-dark);
+}
+
+.form_batch_search input {
+ height: 32px;
+ padding: 0 var(--space-sm);
+ font-size: var(--font-sm);
+ border: 1px solid var(--color-border-subtle);
+ border-radius: var(--border-radius-sm);
+ outline: none;
+}
+
+.form_batch_search input:focus {
+ border-color: var(--color-primary);
+}
+
+.form_batch_search button {
+ height: 32px;
+ padding: 0 var(--space-sm);
+ border: none;
+ border-radius: var(--border-radius-sm);
+ background: var(--color-primary);
+ color: var(--color-text-light);
+ cursor: pointer;
+ transition: background var(--transition-speed);
+}
+
+.form_batch_search button:hover {
+ background: var(--color-secondary);
+}
+
+/* ===== CLEAR FILTER ===== */
+.btn-clear-filter {
+ display: flex;
+ align-items: center;
+ gap: 4px;
+ height: 32px;
+ padding: 0 var(--space-sm);
+ background: var(--color-danger-light);
+ color: var(--color-danger);
+ border-radius: var(--border-radius-sm);
+ text-decoration: none;
+ font-size: var(--font-sm);
+}
+
+.btn-clear-filter:hover {
+ background: var(--color-danger);
+ color: var(--color-text-light);
+}
+
+/* ===== NEW BUTTON ===== */
+.batch_title > a {
+ height: 32px;
+ padding: 0 var(--space-md);
+ background: var(--color-success);
+ color: var(--color-text-light);
+ border-radius: var(--border-radius-sm);
+ text-decoration: none;
+ font-size: var(--font-sm);
+ display: flex;
+ align-items: center;
+ gap: var(--space-xs);
+ transition: background var(--transition-speed);
+}
+
+.batch_title > a:hover {
+ background: var(--color-success);
+ opacity: 0.85;
+}
+
+/* ===== TABLE ===== */
+table {
+ width: 100%;
+ border-collapse: collapse;
+ background: var(--color-bg-white);
+ border-radius: var(--border-radius-sm);
+ overflow: hidden;
+}
+
+table th {
+ text-align: left;
+ padding: var(--space-sm);
+ font-size: var(--font-sm);
+ background: var(--color-bg-dark);
+ color: var(--color-text-light);
+ font-weight: 500;
+}
+
+table td {
+ padding: var(--space-sm);
+ font-size: var(--font-md);
+ border-bottom: 1px solid var(--color-border-subtle);
+}
+
+table tr:last-child td {
+ border-bottom: none;
+}
+
+table tbody tr:hover {
+ background: var(--color-bg-light);
+}
+
+/* ===== ACTION ICONS ===== */
+table a {
+ color: var(--color-primary);
+ transition: color var(--transition-speed);
+}
+
+table a:hover {
+ color: var(--color-secondary);
+}
+
+/* DELETE BUTTON */
+.btn-icon-trash {
+ background: transparent;
+ border: none;
+ color: var(--color-danger);
+ cursor: pointer;
+ transition: color var(--transition-speed);
+}
+
+.btn-icon-trash:hover {
+ color: var(--color-danger);
+ opacity: 0.7;
+}
\ No newline at end of file
diff --git a/multigranja/apps/entities/static/farm/css/home.css b/multigranja/apps/entities/static/farm/css/home.css
new file mode 100644
index 0000000..e69de29
diff --git a/multigranja/apps/entities/static/farm/css/paddock.css b/multigranja/apps/entities/static/farm/css/paddock.css
new file mode 100644
index 0000000..c8310cd
--- /dev/null
+++ b/multigranja/apps/entities/static/farm/css/paddock.css
@@ -0,0 +1,94 @@
+/* ===== PAGE HEADER ===== */
+.paddock_title {
+ display: flex;
+ align-items: center;
+ gap: var(--space-md);
+ margin-bottom: var(--space-lg);
+}
+
+.paddock_title h1 {
+ font-size: var(--font-xl);
+ color: var(--color-text-dark);
+ margin: 0;
+ margin-right: var(--space-lg);
+}
+
+/* ===== SEARCH FORM ===== */
+.form_paddock_search {
+ display: flex;
+ align-items: center;
+ gap: var(--space-xs);
+ margin-left: auto;
+}
+
+.form_paddock_search label {
+ font-size: var(--font-sm);
+ color: var(--color-text-dark);
+}
+
+.form_paddock_search input {
+ height: 32px;
+ padding: 0 var(--space-sm);
+ font-size: var(--font-sm);
+ border: 1px solid var(--color-border-subtle);
+ border-radius: var(--border-radius-sm);
+ outline: none;
+}
+
+.form_paddock_search input:focus {
+ border-color: var(--color-primary);
+}
+
+.form_paddock_search button {
+ height: 32px;
+ padding: 0 var(--space-sm);
+ border: none;
+ border-radius: var(--border-radius-sm);
+ background: var(--color-primary);
+ color: var(--color-text-light);
+ cursor: pointer;
+ transition: background var(--transition-speed);
+}
+
+.form_paddock_search button:hover {
+ background: var(--color-secondary);
+}
+
+/* ===== CLEAR FILTER ===== */
+.btn-clear-filter {
+ display: flex;
+ align-items: center;
+ gap: 4px;
+ height: 32px;
+ padding: 0 var(--space-sm);
+ background: var(--color-danger-light);
+ color: var(--color-danger);
+ border-radius: var(--border-radius-sm);
+ text-decoration: none;
+ font-size: var(--font-sm);
+}
+
+.btn-clear-filter:hover {
+ background: var(--color-danger);
+ color: var(--color-text-light);
+}
+
+/* ===== NEW BUTTON ===== */
+.paddock_title > a {
+ height: 32px;
+ padding: 0 var(--space-md);
+ background: var(--color-success);
+ color: var(--color-text-light);
+ border-radius: var(--border-radius-sm);
+ text-decoration: none;
+ font-size: var(--font-sm);
+ display: flex;
+ align-items: center;
+ gap: var(--space-xs);
+ transition: background var(--transition-speed);
+}
+
+.paddock_title > a:hover {
+ background: var(--color-success);
+ opacity: 0.85;
+}
\ No newline at end of file
diff --git a/multigranja/apps/entities/static/farm/css/paddock_group.css b/multigranja/apps/entities/static/farm/css/paddock_group.css
new file mode 100644
index 0000000..f6e6cbe
--- /dev/null
+++ b/multigranja/apps/entities/static/farm/css/paddock_group.css
@@ -0,0 +1,75 @@
+/* ===== PAGE HEADER ===== */
+.paddock_group_title {
+ display: flex;
+ align-items: center;
+ gap: var(--space-md);
+ margin-bottom: var(--space-lg);
+}
+
+.paddock_group_title h1 {
+ font-size: var(--font-xl);
+ color: var(--color-text-dark);
+ margin: 0;
+ margin-right: var(--space-lg);
+}
+
+/* ===== SEARCH FORM ===== */
+.form_paddock_group_search {
+ display: flex;
+ align-items: center;
+ gap: var(--space-xs);
+ margin-left: auto;
+}
+
+.form_paddock_group_search label {
+ font-size: var(--font-sm);
+ color: var(--color-text-dark);
+}
+
+.form_paddock_group_search input {
+ height: 32px;
+ padding: 0 var(--space-sm);
+ font-size: var(--font-sm);
+ border: 1px solid var(--color-border-subtle);
+ border-radius: var(--border-radius-sm);
+ outline: none;
+}
+
+.form_paddock_group_search input:focus {
+ border-color: var(--color-primary);
+}
+
+.form_paddock_group_search button {
+ height: 32px;
+ padding: 0 var(--space-sm);
+ border: none;
+ border-radius: var(--border-radius-sm);
+ background: var(--color-primary);
+ color: var(--color-text-light);
+ cursor: pointer;
+ transition: background var(--transition-speed);
+}
+
+.form_paddock_group_search button:hover {
+ background: var(--color-secondary);
+}
+
+/* ===== NEW BUTTON ===== */
+.paddock_group_title > a {
+ height: 32px;
+ padding: 0 var(--space-md);
+ background: var(--color-success);
+ color: var(--color-text-light);
+ border-radius: var(--border-radius-sm);
+ text-decoration: none;
+ font-size: var(--font-sm);
+ display: flex;
+ align-items: center;
+ gap: var(--space-xs);
+ transition: background var(--transition-speed);
+}
+
+.paddock_group_title > a:hover {
+ background: var(--color-success);
+ opacity: 0.85;
+}
\ No newline at end of file
diff --git a/multigranja/apps/entities/static/rotation/css/home.css b/multigranja/apps/entities/static/rotation/css/home.css
new file mode 100644
index 0000000..e69de29
diff --git a/multigranja/apps/entities/static/user/css/farm_select.css b/multigranja/apps/entities/static/user/css/farm_select.css
new file mode 100644
index 0000000..327d59d
--- /dev/null
+++ b/multigranja/apps/entities/static/user/css/farm_select.css
@@ -0,0 +1,162 @@
+/* CONTENT */
+body {
+ display: block;
+ min-height: 100vh;
+ margin: 0;
+ padding-top: var(--header-height);
+}
+
+/* NAVBAR */
+.farm-select-navbar {
+ position: fixed;
+ top: 0;
+ left: 0;
+ width: 100%;
+ height: var(--header-height);
+ background-color: var(--color-bg-light);
+ box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ padding: 0 var(--space-lg);
+ z-index: 950;
+}
+
+/* SITE'S TITLE */
+.farm-select-navbar .navbar-brand {
+ font-weight: bold;
+ font-size: 1.5rem;
+ color: var(--color-bg-sidebar, #444);
+ cursor: default;
+}
+
+/* .navbar-user-info and .username uses gap and align style from global.css */
+
+/* SELECT CONTAINER */
+.farm-select-container {
+ width: 100%;
+ max-width: 700px;
+ margin: var(--space-xl) auto;
+ text-align: center;
+ padding: 0;
+}
+
+.farm-select-container h1 {
+ font-size: var(--font-xl);
+ color: #1f2937;
+ margin-bottom: var(--space-xl);
+}
+
+/* LIST OF FARMS */
+.farm-list {
+ list-style: none;
+ padding: 0;
+ margin: 0;
+ display: flex;
+ flex-direction: column;
+ gap: var(--space-sm);
+}
+
+.farm-button {
+ text-decoration: none;
+ display: block;
+ width: 100%;
+}
+
+.farm-item {
+ padding: var(--space-md);
+ text-align: left;
+ background-color: #ffffff;
+ border: 1px solid #e0e0e0;
+ border-left: 5px solid transparent;
+ border-radius: 6px;
+
+ transition: all 0.3s ease;
+}
+
+.farm-item:hover {
+ box-shadow: 0 4px 10px rgba(0, 0, 0, 0.08);
+ border-color: var(--color-farm-select-hover);
+ border-left-color: var(--color-farm-select-hover); /* Cor primária */
+}
+
+.farm-name {
+ font-size: 1.25rem;
+ color: var(--color-text-dark);
+ margin: 0 0 var(--space-xs);
+ font-weight: 700;
+}
+
+.farm-description {
+ display: flex;
+ justify-content: flex-start;
+ gap: var(--space-sm);
+ align-items: center;
+ margin-top: var(--space-sm);
+ font-size: var(--font-sm);
+ flex-wrap: wrap;
+}
+
+/* DATA SEPARATOR */
+.total-area, .count-piquetes, .count-animais {
+ padding: 0;
+ background-color: transparent;
+ color: var(--color-farm-select-info);
+ font-weight: 500;
+}
+
+.total-area:after, .count-piquetes:after {
+ content: "|";
+ margin-left: var(--space-xs);
+ color: var(--color-simple-light-gray);
+}
+
+/* WHEN NO FARMS */
+.no-farms {
+ padding: var(--space-lg);
+ margin-top: var(--space-lg);
+ border: 2px dashed #ccc;
+ border-radius: var(--border-radius-sm);
+ background-color: var(--color-bg-white);
+}
+
+.no-farms p {
+ color: var(--color-farm-select-info);
+ font-size: var(--font-md);
+ margin: var(--space-xs) 0;
+}
+
+/* NEW FARM BUTTON */
+.no-farms a {
+ display: inline-flex;
+ align-items: center;
+ gap: var(--space-xs);
+ margin-top: var(--space-md);
+ padding: var(--space-xs) var(--space-md);
+ background-color: var(--color-new-farm-button);
+ color: white;
+ text-decoration: none;
+ font-weight: 500;
+ border-radius: 4px;
+ transition: background-color 0.2s;
+}
+
+.no-farms a:hover {
+ background-color: var(--color-new-farm-hover);
+}
+
+@media (max-width: 768px) {
+ body {
+ padding: 0;
+ }
+ .farm-select-navbar {
+ padding: 0 var(--space-md);
+ }
+ .farm-select-container {
+ padding: var(--space-md) var(--space-sm);
+ margin: var(--header-height) 0 0 0;
+ max-width: 100%;
+ box-shadow: none;
+ border-radius: 0;
+ }
+}
\ No newline at end of file
diff --git a/multigranja/apps/entities/static/user/css/login.css b/multigranja/apps/entities/static/user/css/login.css
new file mode 100644
index 0000000..b483708
--- /dev/null
+++ b/multigranja/apps/entities/static/user/css/login.css
@@ -0,0 +1,145 @@
+body {
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ min-height: 100vh;
+ margin: 0;
+ background-color: var(--color-bg-light);
+ color: var(--color-text-dark);
+}
+
+.login-container {
+ display: flex;
+ width: 850px;
+ max-width: 90%;
+ min-height: 450px;
+ border-radius: var(--border-radius-lg, 10px);
+ overflow: hidden;
+ box-shadow: 0 5px 20px rgba(0, 0, 0, 0.15);
+ background-color: #ffffff;
+}
+
+.ilustration-container {
+ flex: 1;
+ background: linear-gradient(135deg, var(--login-background-primary), var(--login-background-secondary));
+ display: flex;
+ flex-direction: column;
+ justify-content: center;
+ align-items: center;
+ padding: var(--space-lg);
+ color: var(--color-text-light);
+ text-align: center;
+ font-size: var(--font-lg, 1.5rem);
+ font-weight: bold;
+}
+
+.ilustration-container h1 {
+ margin: 0;
+ line-height: 1.2;
+}
+
+.login-form-container {
+ flex: 1;
+ display: flex;
+ flex-direction: column;
+ justify-content: center;
+ padding: var(--space-xl);
+}
+
+.login-form-container h1 {
+ font-size: var(--font-xl);
+ margin-bottom: var(--space-md);
+ color: var(--color-text-dark);
+ text-align: center;
+}
+
+.login-form-container form {
+ display: flex;
+ flex-direction: column;
+ gap: var(--space-sm);
+}
+
+.login-form-container input[type="text"],
+.login-form-container input[type="password"] {
+ padding: var(--space-sm);
+ font-size: var(--font-md);
+ border-radius: var(--border-radius-sm);
+ border: 1px solid var(--color-border-subtle);
+ width: 100%;
+ transition: border-color var(--transition-speed) ease;
+}
+
+.login-form-container input:focus {
+ border-color: var(--color-primary);
+ outline: none;
+ box-shadow: 0 0 0 1px var(--color-primary-light, rgba(0, 123, 255, 0.2));
+}
+
+.login-form-container button[type="submit"] {
+ background-color: var(--color-login-button);
+ color: white;
+ padding: var(--space-xs);
+ font-size: var(--font-lg);
+ font-weight: bold;
+ border: none;
+ border-radius: var(--border-radius-sm);
+ cursor: pointer;
+ margin-top: var(--space-sm);
+ transition: background-color var(--transition-speed) ease;
+}
+
+.login-form-container button[type="submit"]:hover {
+ background-color: var(--color-login-button-focus);
+}
+
+.login-fail {
+ color: var(--color-danger);
+ background-color: var(--color-danger-light);
+ border: 1px solid var(--color-danger);
+ padding: var(--space-xs);
+ border-radius: var(--border-radius-sm);
+ font-size: var(--font-sm);
+ text-align: center;
+ margin-top: var(--space-sm);
+}
+
+.login-fail i {
+ margin-right: var(--space-xs);
+}
+
+.login-form-container p {
+ text-align: center;
+ font-size: var(--font-sm);
+ margin-top: var(--space-md);
+}
+
+.login-form-container a {
+ color: var(--color-primary);
+ text-decoration: none;
+}
+
+.login-form-container a:hover {
+ text-decoration: underline;
+}
+
+@media (max-width: 800px) {
+ .login-container {
+ flex-direction: column;
+ height: auto;
+ max-width: 95%;
+ box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
+ }
+ .ilustration-container, .login-form-container {
+ flex: none;
+ width: 100%;
+ }
+ .ilustration-container {
+ padding: var(--space-md);
+ }
+ .ilustration-container h1 {
+ font-size: var(--font-md);
+ }
+ .login-form-container {
+ padding: var(--space-lg) var(--space-md);
+ }
+}
\ No newline at end of file
diff --git a/multigranja/apps/entities/templates/animal/animal_home.html b/multigranja/apps/entities/templates/animal/animal_home.html
new file mode 100644
index 0000000..e961db2
--- /dev/null
+++ b/multigranja/apps/entities/templates/animal/animal_home.html
@@ -0,0 +1,48 @@
+{% extends 'base.html' %}
+{% load static %}
+
+{% block extra_css %}
+
+{% endblock %}
+
+{% block content %}
+
+ {% if not animals %}
+ Nenhum animal encontrado.
+ {% else %}
+
+
+ | Identificador |
+ Idade |
+ Peso |
+ Editar |
+ Excluir |
+
+ {% for animal in animals %}
+
+ | {{ animal.identifier }} |
+ {{ animal.age }} |
+ {{ animal.weight }} Kg |
+ |
+
+
+ |
+
+ {% endfor %}
+
+ {% endif %}
+{% endblock %}
\ No newline at end of file
diff --git a/multigranja/apps/entities/templates/animal/animal_new.html b/multigranja/apps/entities/templates/animal/animal_new.html
new file mode 100644
index 0000000..01ab5fc
--- /dev/null
+++ b/multigranja/apps/entities/templates/animal/animal_new.html
@@ -0,0 +1,11 @@
+{% extends 'base.html' %}
+{% load static %}
+
+{% block extra_css %}
+
+{% endblock %}
+
+{% block content %}
+ NOVO ANIMAL
+ {% include 'includes/_form.html' %}
+{% endblock %}
\ No newline at end of file
diff --git a/multigranja/apps/entities/templates/animal/animal_update.html b/multigranja/apps/entities/templates/animal/animal_update.html
new file mode 100644
index 0000000..91a13f3
--- /dev/null
+++ b/multigranja/apps/entities/templates/animal/animal_update.html
@@ -0,0 +1,11 @@
+{% extends 'base.html' %}
+{% load static %}
+
+{% block extra_css %}
+
+{% endblock %}
+
+{% block content %}
+ EDITAR ANIMAL
+ {% include 'includes/_form.html' %}
+{% endblock %}
\ No newline at end of file
diff --git a/multigranja/apps/entities/templates/batch/batch_home.html b/multigranja/apps/entities/templates/batch/batch_home.html
new file mode 100644
index 0000000..f36b8ca
--- /dev/null
+++ b/multigranja/apps/entities/templates/batch/batch_home.html
@@ -0,0 +1,48 @@
+{% extends 'base.html' %}
+{% load static %}
+
+{% block extra_css %}
+
+{% endblock %}
+
+{% block content %}
+
+ {% if not batches %}
+ Nenhum lote encontrado.
+ {% else %}
+
+
+ | Identificador |
+ Categoria animal |
+ Tipo de lote |
+ Editar |
+ Excluir |
+
+ {% for batch in batches %}
+
+ | {{ batch.identifier }} |
+ {{ batch.animal_category }} |
+ {{ batch.batch_type }} Kg |
+ |
+
+
+ |
+
+ {% endfor %}
+
+ {% endif %}
+{% endblock %}
\ No newline at end of file
diff --git a/multigranja/apps/entities/templates/batch/batch_new.html b/multigranja/apps/entities/templates/batch/batch_new.html
new file mode 100644
index 0000000..72f0361
--- /dev/null
+++ b/multigranja/apps/entities/templates/batch/batch_new.html
@@ -0,0 +1,11 @@
+{% extends 'base.html' %}
+{% load static %}
+
+{% block extra_css %}
+
+{% endblock %}
+
+{% block content %}
+ NOVO LOTE
+ {% include 'includes/_form.html' %}
+{% endblock %}
\ No newline at end of file
diff --git a/multigranja/apps/entities/templates/batch/batch_update.html b/multigranja/apps/entities/templates/batch/batch_update.html
new file mode 100644
index 0000000..09b7aed
--- /dev/null
+++ b/multigranja/apps/entities/templates/batch/batch_update.html
@@ -0,0 +1,11 @@
+{% extends 'base.html' %}
+{% load static %}
+
+{% block extra_css %}
+
+{% endblock %}
+
+{% block content %}
+ EDITAR LOTE
+ {% include 'includes/_form.html' %}
+{% endblock %}
\ No newline at end of file
diff --git a/multigranja/apps/entities/templates/farm/farm_home.html b/multigranja/apps/entities/templates/farm/farm_home.html
new file mode 100644
index 0000000..376fe09
--- /dev/null
+++ b/multigranja/apps/entities/templates/farm/farm_home.html
@@ -0,0 +1,46 @@
+{% extends 'base.html' %}
+{% load static %}
+
+{% block extra_css %}
+
+{% endblock %}
+
+{% block content %}
+
+ {% if not farms %}
+ Nenhuma fazenda encontrado.
+ {% else %}
+
+
+ | Nome |
+ Área total |
+ Editar |
+ Excluir |
+
+ {% for farm in farms %}
+
+ | {{ farm.name }} |
+ {{ farm.total_area }} m2 |
+ |
+
+
+ |
+
+ {% endfor %}
+
+ {% endif %}
+{% endblock %}
\ No newline at end of file
diff --git a/multigranja/apps/entities/templates/farm/farm_new.html b/multigranja/apps/entities/templates/farm/farm_new.html
new file mode 100644
index 0000000..1a6a2a6
--- /dev/null
+++ b/multigranja/apps/entities/templates/farm/farm_new.html
@@ -0,0 +1,10 @@
+{% extends 'base.html' %}
+{% load static %}
+
+{% block extra_css %}
+
+{% endblock %}
+
+{% block content %}
+ New - Farm
+{% endblock %}
\ No newline at end of file
diff --git a/multigranja/apps/entities/templates/farm/farm_update.html b/multigranja/apps/entities/templates/farm/farm_update.html
new file mode 100644
index 0000000..dc68d98
--- /dev/null
+++ b/multigranja/apps/entities/templates/farm/farm_update.html
@@ -0,0 +1,10 @@
+{% extends 'base.html' %}
+{% load static %}
+
+{% block extra_css %}
+
+{% endblock %}
+
+{% block content %}
+ Update - Farm
+{% endblock %}
\ No newline at end of file
diff --git a/multigranja/apps/entities/templates/paddock/paddock_home.html b/multigranja/apps/entities/templates/paddock/paddock_home.html
new file mode 100644
index 0000000..cbbcf94
--- /dev/null
+++ b/multigranja/apps/entities/templates/paddock/paddock_home.html
@@ -0,0 +1,85 @@
+{% extends 'base.html' %}
+{% load static %}
+
+{% block extra_css %}
+
+{% endblock %}
+
+{% block content %}
+
+
+ {% if not paddocks %}
+ Nenhum piquete encontrado.
+ {% else %}
+
+
+ | Número |
+ Área |
+ Capacidade |
+ Ocupado |
+ Bloco |
+ Editar |
+ Excluir |
+
+
+ {% for paddock in paddocks %}
+
+ | {{ paddock.paddock_number }} |
+ {{ paddock.area }} ha |
+ {{ paddock.load_capacity }} |
+
+ {% if paddock.occupied %}
+ Sim
+ {% else %}
+ Não
+ {% endif %}
+ |
+
+ {% if paddock.paddock_group %}
+ {{ paddock.paddock_group.identifier }}
+ {% else %}
+ -
+ {% endif %}
+ |
+
+
+
+
+
+ |
+
+
+
+ |
+
+ {% endfor %}
+
+
+ {% endif %}
+{% endblock %}
\ No newline at end of file
diff --git a/multigranja/apps/entities/templates/paddock/paddock_new.html b/multigranja/apps/entities/templates/paddock/paddock_new.html
new file mode 100644
index 0000000..ae3602e
--- /dev/null
+++ b/multigranja/apps/entities/templates/paddock/paddock_new.html
@@ -0,0 +1,10 @@
+{% extends 'base.html' %}
+{% load static %}
+
+{% block extra_css %}
+
+{% endblock %}
+
+{% block content %}
+ New - Paddock
+{% endblock %}
\ No newline at end of file
diff --git a/multigranja/apps/entities/templates/paddock/paddock_update.html b/multigranja/apps/entities/templates/paddock/paddock_update.html
new file mode 100644
index 0000000..ea1f89e
--- /dev/null
+++ b/multigranja/apps/entities/templates/paddock/paddock_update.html
@@ -0,0 +1,10 @@
+{% extends 'base.html' %}
+{% load static %}
+
+{% block extra_css %}
+
+{% endblock %}
+
+{% block content %}
+ Delete - Paddock
+{% endblock %}
\ No newline at end of file
diff --git a/multigranja/apps/entities/templates/paddock_group/paddock_group_home.html b/multigranja/apps/entities/templates/paddock_group/paddock_group_home.html
new file mode 100644
index 0000000..9a35fb1
--- /dev/null
+++ b/multigranja/apps/entities/templates/paddock_group/paddock_group_home.html
@@ -0,0 +1,76 @@
+{% extends 'base.html' %}
+{% load static %}
+
+{% block extra_css %}
+
+{% endblock %}
+
+{% block content %}
+
+
+ {% if not paddock_groups %}
+ Nenhum bloco encontrado.
+ {% else %}
+
+
+ | Identificador |
+ Descrição |
+ Piquetes |
+ Editar |
+ Excluir |
+
+
+ {% for group in paddock_groups %}
+
+ | {{ group.identifier }} |
+ {{ group.description }} |
+
+
+ {% for paddock in group.paddocks.all %}
+ {{ paddock.paddock_number }}{% if not forloop.last %}, {% endif %}
+ {% empty %}
+ -
+ {% endfor %}
+ |
+
+
+
+
+
+ |
+
+
+
+ |
+
+ {% endfor %}
+
+
+ {% endif %}
+{% endblock %}
\ No newline at end of file
diff --git a/multigranja/apps/entities/templates/paddock_group/paddock_group_new.html b/multigranja/apps/entities/templates/paddock_group/paddock_group_new.html
new file mode 100644
index 0000000..206e97e
--- /dev/null
+++ b/multigranja/apps/entities/templates/paddock_group/paddock_group_new.html
@@ -0,0 +1,10 @@
+{% extends 'base.html' %}
+{% load static %}
+
+{% block extra_css %}
+
+{% endblock %}
+
+{% block content %}
+ New - Paddock Group
+{% endblock %}
\ No newline at end of file
diff --git a/multigranja/apps/entities/templates/paddock_group/paddock_group_update.html b/multigranja/apps/entities/templates/paddock_group/paddock_group_update.html
new file mode 100644
index 0000000..ea70fdc
--- /dev/null
+++ b/multigranja/apps/entities/templates/paddock_group/paddock_group_update.html
@@ -0,0 +1,10 @@
+{% extends 'base.html' %}
+{% load static %}
+
+{% block extra_css %}
+
+{% endblock %}
+
+{% block content %}
+ Delete - Paddock Group
+{% endblock %}
\ No newline at end of file
diff --git a/multigranja/apps/entities/templates/rotation/rotation_home.html b/multigranja/apps/entities/templates/rotation/rotation_home.html
new file mode 100644
index 0000000..8044d70
--- /dev/null
+++ b/multigranja/apps/entities/templates/rotation/rotation_home.html
@@ -0,0 +1,10 @@
+{% extends 'base.html' %}
+{% load static %}
+
+{% block extra_css %}
+
+{% endblock %}
+
+{% block content %}
+ Home - Rotation
+{% endblock %}
\ No newline at end of file
diff --git a/multigranja/apps/entities/templates/rotation/rotation_new.html b/multigranja/apps/entities/templates/rotation/rotation_new.html
new file mode 100644
index 0000000..81aaf58
--- /dev/null
+++ b/multigranja/apps/entities/templates/rotation/rotation_new.html
@@ -0,0 +1,10 @@
+{% extends 'base.html' %}
+{% load static %}
+
+{% block extra_css %}
+
+{% endblock %}
+
+{% block content %}
+ New - Rotation
+{% endblock %}
\ No newline at end of file
diff --git a/multigranja/apps/entities/templates/rotation/rotation_update.html b/multigranja/apps/entities/templates/rotation/rotation_update.html
new file mode 100644
index 0000000..af0450d
--- /dev/null
+++ b/multigranja/apps/entities/templates/rotation/rotation_update.html
@@ -0,0 +1,10 @@
+{% extends 'base.html' %}
+{% load static %}
+
+{% block extra_css %}
+
+{% endblock %}
+
+{% block content %}
+ Delete - Rotation
+{% endblock %}
\ No newline at end of file
diff --git a/multigranja/apps/entities/tests.py b/multigranja/apps/entities/tests.py
new file mode 100644
index 0000000..7ce503c
--- /dev/null
+++ b/multigranja/apps/entities/tests.py
@@ -0,0 +1,3 @@
+from django.test import TestCase
+
+# Create your tests here.
diff --git a/multigranja/apps/entities/urls/__init__.py b/multigranja/apps/entities/urls/__init__.py
new file mode 100644
index 0000000..efbff3c
--- /dev/null
+++ b/multigranja/apps/entities/urls/__init__.py
@@ -0,0 +1,10 @@
+from django.urls import include, path
+
+app_name = "entities"
+
+urlpatterns = [
+ path("animal/", include("entities.urls.animal", namespace='animal')),
+ path("farm/", include("entities.urls.farm", namespace='farm')),
+ path("rotation/", include("entities.urls.rotation", namespace='rotation')),
+ path("user/", include("entities.urls.user", namespace='user')),
+]
\ No newline at end of file
diff --git a/multigranja/apps/entities/urls/animal.py b/multigranja/apps/entities/urls/animal.py
new file mode 100644
index 0000000..11ba6a6
--- /dev/null
+++ b/multigranja/apps/entities/urls/animal.py
@@ -0,0 +1,17 @@
+from django.urls import path
+from ..views import animal as views
+
+app_name = 'animal'
+
+urlpatterns = [
+ path('', views.animal_home, name='animal_home'),
+ path('batch/', views.batch_home, name='batch_home'),
+
+ path('new/', views.animal_new, name='animal_new'),
+ path('update/', views.animal_update, name='animal_update'),
+ path('delete/', views.animal_delete, name='animal_delete'),
+
+ path('batch/new/', views.batch_new, name='batch_new'),
+ path('batch/update/', views.batch_update, name='batch_update'),
+ path('batch/delete/', views.batch_delete, name='batch_delete'),
+]
\ No newline at end of file
diff --git a/multigranja/apps/entities/urls/farm.py b/multigranja/apps/entities/urls/farm.py
new file mode 100644
index 0000000..d85df9b
--- /dev/null
+++ b/multigranja/apps/entities/urls/farm.py
@@ -0,0 +1,22 @@
+from django.urls import path
+from ..views import farm as views
+
+app_name = 'farm'
+
+urlpatterns = [
+ path('', views.farm_home, name='farm_home'),
+ path('paddock/', views.paddock_home, name='paddock_home'),
+ path("paddock/group/", views.paddock_group_home, name="paddock_group_home"),
+
+ path('new/', views.farm_new, name='farm_new'),
+ path('update/', views.farm_update, name='farm_update'),
+ path('delete/', views.farm_delete, name='farm_delete'),
+
+ path('paddock/new/', views.paddock_new, name='paddock_new'),
+ path('paddock/update/', views.paddock_update, name='paddock_update'),
+ path('paddock/delete/', views.paddock_delete, name='paddock_delete'),
+
+ path('paddock_group/new/', views.paddock_group_new, name='paddock_group_new'),
+ path('paddock_group/update/', views.paddock_group_update, name='paddock_group_update'),
+ path('paddock_group/delete/', views.paddock_group_delete, name='paddock_group_delete'),
+]
\ No newline at end of file
diff --git a/multigranja/apps/entities/urls/rotation.py b/multigranja/apps/entities/urls/rotation.py
new file mode 100644
index 0000000..f676530
--- /dev/null
+++ b/multigranja/apps/entities/urls/rotation.py
@@ -0,0 +1,14 @@
+from django.urls import path
+from django.http import HttpResponse
+from django.contrib.auth.decorators import login_required
+
+from ..views import rotation as views
+
+app_name = 'rotation'
+
+urlpatterns = [
+ path('', views.rotation_home, name='rotation_home'),
+
+ path('new/', views.rotation_new, name='rotation_new'),
+ path('delete/', views.rotation_delete, name='rotation_delete'),
+]
\ No newline at end of file
diff --git a/multigranja/apps/entities/urls/user.py b/multigranja/apps/entities/urls/user.py
new file mode 100644
index 0000000..4a537d5
--- /dev/null
+++ b/multigranja/apps/entities/urls/user.py
@@ -0,0 +1,18 @@
+from django.urls import path
+from django.http import HttpResponse
+from django.contrib.auth.decorators import login_required
+from ..views import user as views
+
+app_name = 'user'
+
+urlpatterns = [
+ path('', views.user_profile, name='user_profile'),
+ path('login/', views.user_login, name='user_login'),
+ path('logout/', views.user_logout, name='user_logout'),
+ path('signin/', views.user_signin, name='user_signin'),
+
+ path('farm-select/', views.farm_select, name='farm_select'),
+ path('farm-set//', views.farm_set, name='farm_set'),
+ path('farm-set/', views.farm_set, name='farm_set_post'),
+ path('farm-deselect/', views.farm_deselect, name='farm_deselect'),
+]
\ No newline at end of file
diff --git a/multigranja/apps/entities/views/__init__.py b/multigranja/apps/entities/views/__init__.py
new file mode 100644
index 0000000..9f69dcb
--- /dev/null
+++ b/multigranja/apps/entities/views/__init__.py
@@ -0,0 +1,3 @@
+from .animal import *
+from .farm import *
+from .rotation import *
\ No newline at end of file
diff --git a/multigranja/apps/entities/views/animal.py b/multigranja/apps/entities/views/animal.py
new file mode 100644
index 0000000..bc01734
--- /dev/null
+++ b/multigranja/apps/entities/views/animal.py
@@ -0,0 +1,76 @@
+from django.views.decorators.http import require_http_methods
+from django.shortcuts import redirect, render
+from core.decorators import farm_required
+
+from ..services import animal as services
+
+# Animal Views
+@farm_required
+def animal_home(request):
+ context = services.get_animal_home_context(request)
+ return render(request, 'animal/animal_home.html', context)
+
+
+@farm_required
+def animal_new(request):
+ if request.method == 'GET':
+ context = services.get_animal_new_context()
+ return render(request, 'animal/animal_new.html', context)
+
+ elif request.method == 'POST':
+ success = services.create_animal(request)
+ if success:
+ return redirect('entities:animal:animal_home')
+
+ return redirect('entities:animal:animal_new')
+
+
+@farm_required
+def animal_update(request, animal_id):
+ result = services.update_animal(request, animal_id)
+
+ if result.get('redirect'):
+ return redirect(result['redirect'])
+
+ return render(request, 'animal/animal_update.html', result['context'])
+
+
+@farm_required
+@require_http_methods(["POST"])
+def animal_delete(request, animal_id):
+ services.delete_animal(request, animal_id)
+ return redirect('entities:animal:animal_home')
+
+
+# Batch Views
+@farm_required
+def batch_home(request):
+ context = services.get_batch_home_context(request)
+ return render(request, 'batch/batch_home.html', context)
+
+
+@farm_required
+def batch_new(request):
+ result = services.create_batch(request)
+
+ if result.get('redirect'):
+ return redirect(result['redirect'])
+
+ return render(request, 'batch/batch_new.html', result['context'])
+
+
+@farm_required
+def batch_update(request, batch_id):
+ result = services.update_batch(request, batch_id)
+
+ if result.get('redirect'):
+ return redirect(result['redirect'])
+
+ return render(request, 'batch/batch_update.html', result['context'])
+
+
+@farm_required
+@require_http_methods(["POST"])
+def batch_delete(request, batch_id):
+ services.delete_batch(batch_id)
+ return redirect('entities:animal:batch_home')
\ No newline at end of file
diff --git a/multigranja/apps/entities/views/farm.py b/multigranja/apps/entities/views/farm.py
new file mode 100644
index 0000000..e70a65c
--- /dev/null
+++ b/multigranja/apps/entities/views/farm.py
@@ -0,0 +1,122 @@
+from django.views.decorators.http import require_http_methods
+from django.shortcuts import redirect, render
+from core.decorators import farm_required
+from ..services import farm as services
+
+# Farm Views
+@farm_required
+def farm_home(request):
+ context = services.get_farm_home_context(request)
+ return render(request, 'farm/farm_home.html', context)
+
+
+@farm_required
+def farm_new(request):
+ if request.method == 'GET':
+ context = services.get_farm_new_context()
+ return render(request, 'farm/farm_new.html', context)
+
+ elif request.method == 'POST':
+ success = services.create_farm(request)
+ if success:
+ return redirect('entities:farm:farm_home')
+
+ return redirect('entities:farm:farm_new')
+
+
+
+@farm_required
+def farm_update(request, farm_id):
+ result = services.update_farm(request, farm_id)
+
+ if result.get('redirect'):
+ return redirect(result['redirect'])
+
+ return render(request, 'farm/farm_update.html', result['context'])
+
+
+@farm_required
+@require_http_methods(["POST"])
+def farm_delete(request, farm_id):
+ services.delete_farm(request, farm_id)
+ return redirect('entities:farm:farm_home')
+
+
+# Paddock Views
+@farm_required
+def paddock_home(request):
+ context = {}
+ context['title'] = 'Piquete'
+ return render(request, 'paddock/paddock_home.html', context)
+
+
+@farm_required
+def paddock_new(request):
+ if request.method == 'GET':
+ context = services.get_paddock_new_context()
+ return render(request, 'paddock/paddock_new.html', context)
+
+ elif request.method == 'POST':
+ success = services.create_paddock(request)
+ if success:
+ return redirect('entities:farm:paddock_home')
+
+ return redirect('entities:farm:paddock_new')
+
+
+
+@farm_required
+def paddock_update(request, paddock_id):
+ result = services.update_paddock(request, paddock_id)
+
+ if result.get('redirect'):
+ return redirect(result['redirect'])
+
+ return render(request, 'paddock/paddock_update.html', result['context'])
+
+
+@farm_required
+@require_http_methods(["POST"])
+def paddock_delete(request, paddock_id):
+ services.delete_paddock(request, paddock_id)
+ return redirect('entities:farm:paddock_home')
+
+
+# Paddock Group Views
+@farm_required
+def paddock_group_home(request):
+ context = {}
+ context['title'] = 'Bloco'
+ return render(request, 'paddock_group/paddock_group_home.html', context)
+
+
+@farm_required
+def paddock_group_new(request):
+ if request.method == 'GET':
+ context = services.get_paddock_group_new_context()
+ return render(request, 'paddock_group/paddock_group_new.html', context)
+
+ elif request.method == 'POST':
+ success = services.create_paddock_group(request)
+ if success:
+ return redirect('entities:farm:paddock_group_home')
+
+ return redirect('entities:farm:paddock_group_new')
+
+
+
+@farm_required
+def paddock_group_update(request, paddock_group_id):
+ result = services.update_paddock_group(request, paddock_group_id)
+
+ if result.get('redirect'):
+ return redirect(result['redirect'])
+
+ return render(request, 'paddock_group/paddock_group_update.html', result['context'])
+
+
+@farm_required
+@require_http_methods(["POST"])
+def paddock_group_delete(request, paddock_group_id):
+ services.delete_paddock_group(request, paddock_group_id)
+ return redirect('entities:farm:paddock_group_home')
\ No newline at end of file
diff --git a/multigranja/apps/entities/views/rotation.py b/multigranja/apps/entities/views/rotation.py
new file mode 100644
index 0000000..2cca8b7
--- /dev/null
+++ b/multigranja/apps/entities/views/rotation.py
@@ -0,0 +1,10 @@
+from django.shortcuts import render
+
+def rotation_home(request):
+ return render(request, 'rotation/rotation_home.html')
+
+def rotation_new(request):
+ return render(request, 'rotation/rotation_new.html')
+
+def rotation_delete(request):
+ return render(request, 'rotation/rotation_delete.html')
\ No newline at end of file
diff --git a/multigranja/apps/entities/views/user.py b/multigranja/apps/entities/views/user.py
new file mode 100644
index 0000000..86e2a15
--- /dev/null
+++ b/multigranja/apps/entities/views/user.py
@@ -0,0 +1,40 @@
+from django.shortcuts import redirect, render
+from ..services import user as services
+
+
+def user_profile(request): # TO DO
+ return services.user_profile(request)
+
+
+def user_login(request):
+ result = services.user_login(request)
+
+ if result.get('redirect'):
+ return redirect(result['redirect'])
+
+ return render(request, 'login.html', result['context'])
+
+
+def user_logout(request):
+ return services.user_logout(request)
+
+
+def user_signin(request): # TO DO
+ return services.user_signin(request)
+
+
+def farm_select(request):
+ result = services.farm_select(request)
+
+ if result.get('redirect'):
+ return redirect(result['redirect'])
+
+ return render(request, 'farm_select.html', result['context'])
+
+
+def farm_set(request, farm_id=None):
+ return services.farm_set(request, farm_id)
+
+
+def farm_deselect(request):
+ return services.farm_deselect(request)
\ No newline at end of file
diff --git a/multigranja/apps/report/__init__.py b/multigranja/apps/report/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/multigranja/apps/report/admin.py b/multigranja/apps/report/admin.py
new file mode 100644
index 0000000..8c38f3f
--- /dev/null
+++ b/multigranja/apps/report/admin.py
@@ -0,0 +1,3 @@
+from django.contrib import admin
+
+# Register your models here.
diff --git a/multigranja/apps/report/apps.py b/multigranja/apps/report/apps.py
new file mode 100644
index 0000000..138ba20
--- /dev/null
+++ b/multigranja/apps/report/apps.py
@@ -0,0 +1,5 @@
+from django.apps import AppConfig
+
+
+class ReportConfig(AppConfig):
+ name = 'report'
diff --git a/multigranja/apps/report/models.py b/multigranja/apps/report/models.py
new file mode 100644
index 0000000..71a8362
--- /dev/null
+++ b/multigranja/apps/report/models.py
@@ -0,0 +1,3 @@
+from django.db import models
+
+# Create your models here.
diff --git a/multigranja/apps/report/tests.py b/multigranja/apps/report/tests.py
new file mode 100644
index 0000000..7ce503c
--- /dev/null
+++ b/multigranja/apps/report/tests.py
@@ -0,0 +1,3 @@
+from django.test import TestCase
+
+# Create your tests here.
diff --git a/multigranja/apps/report/urls.py b/multigranja/apps/report/urls.py
new file mode 100644
index 0000000..13aab57
--- /dev/null
+++ b/multigranja/apps/report/urls.py
@@ -0,0 +1,13 @@
+from django.urls import path
+from django.http import HttpResponse
+from django.contrib.auth.decorators import login_required
+
+app_name = 'report'
+
+@login_required(login_url='/user/login/')
+def home(request):
+ return HttpResponse("Home app page")
+
+urlpatterns = [
+ path('', home),
+]
\ No newline at end of file
diff --git a/multigranja/apps/report/views.py b/multigranja/apps/report/views.py
new file mode 100644
index 0000000..91ea44a
--- /dev/null
+++ b/multigranja/apps/report/views.py
@@ -0,0 +1,3 @@
+from django.shortcuts import render
+
+# Create your views here.
diff --git a/multigranja/manage.py b/multigranja/manage.py
new file mode 100644
index 0000000..2a3194a
--- /dev/null
+++ b/multigranja/manage.py
@@ -0,0 +1,22 @@
+#!/usr/bin/env python
+"""Django's command-line utility for administrative tasks."""
+import os
+import sys
+
+
+def main():
+ """Run administrative tasks."""
+ os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'multigranja.settings')
+ try:
+ from django.core.management import execute_from_command_line
+ except ImportError as exc:
+ raise ImportError(
+ "Couldn't import Django. Are you sure it's installed and "
+ "available on your PYTHONPATH environment variable? Did you "
+ "forget to activate a virtual environment?"
+ ) from exc
+ execute_from_command_line(sys.argv)
+
+
+if __name__ == '__main__':
+ main()
diff --git a/multigranja/multigranja/__init__.py b/multigranja/multigranja/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/multigranja/multigranja/asgi.py b/multigranja/multigranja/asgi.py
new file mode 100644
index 0000000..5a481aa
--- /dev/null
+++ b/multigranja/multigranja/asgi.py
@@ -0,0 +1,7 @@
+import os
+
+from django.core.asgi import get_asgi_application
+
+os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'multigranja.settings')
+
+application = get_asgi_application()
diff --git a/multigranja/multigranja/settings.py b/multigranja/multigranja/settings.py
new file mode 100644
index 0000000..98bd4d7
--- /dev/null
+++ b/multigranja/multigranja/settings.py
@@ -0,0 +1,148 @@
+from pathlib import Path
+import sys
+
+# Build paths inside the project like this: BASE_DIR / 'subdir'.
+BASE_DIR = Path(__file__).resolve().parent.parent
+sys.path.append(str(BASE_DIR / "apps"))
+
+# Quick-start development settings - unsuitable for production
+# See https://docs.djangoproject.com/en/6.0/howto/deployment/checklist/
+
+# SECURITY WARNING: keep the secret key used in production secret!
+SECRET_KEY = 'django-insecure-aziq*utn^c^-nha$#r4veem)7iew70x47zo!9wfsb(y)pav_6v'
+
+# SECURITY WARNING: don't run with debug turned on in production!
+DEBUG = True
+
+ALLOWED_HOSTS = ['*']
+
+
+# Application definition
+
+INSTALLED_APPS = [
+ 'django.contrib.admin',
+ 'django.contrib.auth',
+ 'django.contrib.contenttypes',
+ 'django.contrib.sessions',
+ 'django.contrib.messages',
+ 'django.contrib.staticfiles',
+ # My apps
+ 'core',
+ 'entities',
+ 'report',
+]
+
+MIDDLEWARE = [
+ 'django.middleware.security.SecurityMiddleware',
+ 'django.contrib.sessions.middleware.SessionMiddleware',
+ 'django.middleware.common.CommonMiddleware',
+ 'django.middleware.csrf.CsrfViewMiddleware',
+ 'django.contrib.auth.middleware.AuthenticationMiddleware',
+ 'django.contrib.messages.middleware.MessageMiddleware',
+ 'django.middleware.clickjacking.XFrameOptionsMiddleware',
+ # My middlewares
+ 'core.middlewares.LoginRequiredMiddleware',
+]
+
+# Defines the system login URL
+LOGIN_URL = '/user/login/'
+# Defines URL paths that do not require login
+LOGIN_EXEMPT_PATHS = [
+ '/user/login/',
+ '/user/signin/',
+]
+# Defines prefixes for URL paths that do not require login
+LOGIN_EXEMPT_PREFIX = [
+ '/admin/',
+]
+
+ROOT_URLCONF = 'multigranja.urls'
+
+TEMPLATES = [
+ {
+ 'BACKEND': 'django.template.backends.django.DjangoTemplates',
+ 'DIRS': [BASE_DIR/'templates'],
+ 'APP_DIRS': True,
+ 'OPTIONS': {
+ 'context_processors': [
+ 'django.template.context_processors.request',
+ 'django.contrib.auth.context_processors.auth',
+ 'django.contrib.messages.context_processors.messages',
+ # My processors
+ 'core.context_processors.selected_farm_processor',
+ ],
+ },
+ },
+]
+
+WSGI_APPLICATION = 'multigranja.wsgi.application'
+
+
+# Database
+# https://docs.djangoproject.com/en/5.2/ref/settings/#databases
+
+# DATABASES = {
+# 'default': {
+# 'ENGINE': 'django.db.backends.postgresql',
+# 'NAME': 'multifarmdb',
+# 'USER': 'postgres',
+# 'PASSWORD': 'root',
+# 'HOST': 'localhost',
+# 'PORT': '5432',
+# }
+# }
+
+DATABASES = {
+ 'default': {
+ 'ENGINE': 'django.db.backends.sqlite3',
+ 'NAME': BASE_DIR / 'db.sqlite3',
+ }
+}
+
+
+# Password validation
+# https://docs.djangoproject.com/en/6.0/ref/settings/#auth-password-validators
+
+AUTH_PASSWORD_VALIDATORS = [
+ {
+ 'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator',
+ },
+ {
+ 'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator',
+ },
+ {
+ 'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator',
+ },
+ {
+ 'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator',
+ },
+]
+
+AUTH_USER_MODEL = 'entities.User'
+
+
+# Internationalization
+# https://docs.djangoproject.com/en/6.0/topics/i18n/
+
+LANGUAGE_CODE = 'pt-br'
+
+TIME_ZONE = 'America/Sao_Paulo'
+
+USE_I18N = True
+
+USE_TZ = True
+
+
+# Static files (CSS, JavaScript, Images)
+# https://docs.djangoproject.com/en/5.2/howto/static-files/
+
+STATIC_URL = 'static/'
+STATIC_DIR = [BASE_DIR / "static"]
+STATICFILES_DIRS = [
+ BASE_DIR / "static",
+]
+
+# Default primary key field type
+# https://docs.djangoproject.com/en/5.2/ref/settings/#default-auto-field
+
+DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'
diff --git a/multigranja/multigranja/urls.py b/multigranja/multigranja/urls.py
new file mode 100644
index 0000000..7453de5
--- /dev/null
+++ b/multigranja/multigranja/urls.py
@@ -0,0 +1,13 @@
+from django.contrib import admin
+from django.shortcuts import redirect
+from django.urls import path, include
+
+def home(request):
+ return redirect('entities:user:user_login')
+
+urlpatterns = [
+ path('admin/', admin.site.urls),
+ path('', home),
+ path('', include('entities.urls')),
+ path('report/', include('report.urls')),
+]
\ No newline at end of file
diff --git a/multigranja/multigranja/wsgi.py b/multigranja/multigranja/wsgi.py
new file mode 100644
index 0000000..38a3a92
--- /dev/null
+++ b/multigranja/multigranja/wsgi.py
@@ -0,0 +1,16 @@
+"""
+WSGI config for multigranja project.
+
+It exposes the WSGI callable as a module-level variable named ``application``.
+
+For more information on this file, see
+https://docs.djangoproject.com/en/6.0/howto/deployment/wsgi/
+"""
+
+import os
+
+from django.core.wsgi import get_wsgi_application
+
+os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'multigranja.settings')
+
+application = get_wsgi_application()
diff --git a/multigranja/static/css/base/reset.css b/multigranja/static/css/base/reset.css
new file mode 100644
index 0000000..0f3f1cf
--- /dev/null
+++ b/multigranja/static/css/base/reset.css
@@ -0,0 +1,42 @@
+/*
+ * File: static/css/base/reset.css
+ * Purpose: Normalize and reset default browser styles.
+ */
+*,
+*::before,
+*::after {
+ box-sizing: border-box;
+ margin: 0;
+ padding: 0;
+}
+
+html {
+ font-size: 16px;
+ font-family: Arial, sans-serif;
+ line-height: 1.5;
+}
+
+ul, ol {
+ list-style: none;
+}
+
+a {
+ color: inherit;
+ text-decoration: none;
+}
+
+img,
+picture,
+video,
+canvas,
+svg {
+ display: block;
+ max-width: 100%;
+}
+
+input,
+button,
+textarea,
+select {
+ font: inherit;
+}
\ No newline at end of file
diff --git a/multigranja/static/css/base/variables.css b/multigranja/static/css/base/variables.css
new file mode 100644
index 0000000..ec510f0
--- /dev/null
+++ b/multigranja/static/css/base/variables.css
@@ -0,0 +1,50 @@
+:root {
+ /* SPECIFIC COLORS */
+ --login-background-primary: #343a40;
+ --login-background-secondary: #6b7480;
+ --color-login-button: #343a40;
+ --color-login-button-focus: #6b7480;
+ --color-farm-select-hover: #343a40;
+ --color-farm-select-info: #6d6d6d;
+ --color-new-farm-button: #343a40;
+ --color-new-farm-hover: #6b7480;
+
+ /* GENERIC COLORS */
+ --color-simple-light-gray: #ccc;
+ --color-border-subtle: #ccc;
+
+ /* PRIMARY COLORS */
+ --color-primary: #007bff;
+ --color-secondary: #6c757d;
+ --color-success: #28a745;
+ --color-danger: #dc3545;
+ --color-danger-light: #f8d7da;
+ --color-warn: #dca235;
+
+ /* BACKGROUND/TEXT COLORS */
+ --color-bg-light: #f0f0f0;
+ --color-bg-white: #fafafa;
+ --color-bg-dark: #343a40;
+ --color-bg-sidebar: #343a40;
+ --color-text-dark: #151422;
+ --color-text-light: #f8f9fa;
+
+ /* SPACING DIMENSIONS */
+ --space-xs: 8px;
+ --space-sm: 12px;
+ --space-md: 16px;
+ --space-lg: 24px;
+ --space-xl: 32px;
+ --header-height: 60px;
+
+ /* FONT DIMENSIONS */
+ --font-xs: 12px;
+ --font-sm: 14px;
+ --font-md: 16px;
+ --font-lg: 24px;
+ --font-xl: 32px;
+
+ --border-radius-sm: 4px;
+ --transition-speed: 0.4s;
+ --transition-speed-long: 1.0s;
+}
\ No newline at end of file
diff --git a/multigranja/static/css/layout/buttons.css b/multigranja/static/css/layout/buttons.css
new file mode 100644
index 0000000..9cda81b
--- /dev/null
+++ b/multigranja/static/css/layout/buttons.css
@@ -0,0 +1,54 @@
+/* BUTTON STYLES */
+.btn-simple {
+ color: var(--color-text-dark);
+ padding: 0 var(--space-xs);
+ border-radius: var(--border-radius-sm);
+}
+
+.btn-simple:hover {
+ color: var(--color-text-light);
+ background-color: var(--color-bg-dark);
+ transition: all var(--transition-speed) ease;
+}
+
+/* PRIMARY BUTTON (Main Action) */
+.btn-primary {
+ background-color: var(--color-primary);
+ padding: var(--space-sm);
+ border-radius: var(--border-radius-sm);
+ color: var(--color-text-light); /* White text */
+}
+
+.btn-primary:hover {
+ background-color: #0056b3; /* Darker blue on hover (a specific dark value often works best) */
+}
+
+/* SECONDARY BUTTON (Neutral Action) */
+.btn-secondary {
+ background-color: var(--color-secondary);
+ padding: var(--space-sm);
+ border-radius: var(--border-radius-sm);
+ color: var(--color-text-light);
+}
+
+.btn-secondary:hover {
+ background-color: #5a6268;
+}
+
+/* SUCCESS/DANGER BUTTONS */
+.btn-success {
+ background-color: var(--color-success);
+ color: var(--color-text-light);
+}
+
+.btn-danger {
+ background-color: var(--color-danger);
+ color: var(--color-text-light);
+}
+
+/* HOVER EFFECTS */
+.highlight-hover:hover {
+ background-color: rgba(255, 255, 255, 0.15);
+ backdrop-filter: blur(8px);
+ transition: all var(--transition-speed) ease;
+}
\ No newline at end of file
diff --git a/multigranja/static/css/layout/entity_home.css b/multigranja/static/css/layout/entity_home.css
new file mode 100644
index 0000000..e69de29
diff --git a/multigranja/static/css/layout/global.css b/multigranja/static/css/layout/global.css
new file mode 100644
index 0000000..505f479
--- /dev/null
+++ b/multigranja/static/css/layout/global.css
@@ -0,0 +1,151 @@
+/* DEFAULTS */
+body {
+ background-color: var(--color-bg-light);
+}
+
+.w33 {
+ width: 33%;
+}
+
+/* SIDE BAR */
+.sidebar {
+ position: fixed;
+ top: 0;
+ left: 0;
+ height: 100vh;
+ width: 250px;
+ background-color: var(--color-bg-sidebar);
+ padding: 0 var(--space-md);
+ overflow-y: auto;
+ overflow-x: hidden;
+ z-index: 1000;
+ box-shadow: 2px 0 5px rgba(0, 0, 0, 0.1);
+}
+
+.sidebar-brand {
+ text-align: center;
+ padding: var(--space-md);
+ color: var(--color-text-light);
+ font-weight: bold;
+ font-size: var(--font-lg);
+ display: block;
+ margin-bottom: var(--space-sm);
+}
+
+.sidebar-menu {
+ margin: var(--space-sm) 0;
+}
+
+.sidebar-header {
+ color: var(--color-text-light);
+ font-size: var(--font-sm);
+ margin-top: var(--space-xs);
+ padding: var(--space-xs) 0;
+ text-transform: uppercase;
+ opacity: 0.6;
+}
+
+.sidebar-link {
+ color: var(--color-text-light);
+ padding: var(--space-xs) var(--space-xs);
+ border-radius: var(--border-radius-sm);
+ width: 100%;
+ display: block;
+ opacity: 1;
+ transition: opacity var(--transition-speed-long) cubic-bezier(0.2, 0.85, 0.75, 1.7), transform var(--transition-speed) ease;
+}
+
+.active {
+ background-color: rgba(255, 255, 255, 0.15);
+ backdrop-filter: blur(8px);
+ box-shadow: inset -1px 0 0 0 rgba(255, 255, 255, 0.2), inset 0 -1px 0 0 rgba(255, 255, 255, 0.2);
+}
+
+/* SIDEBAR TOGGLING */
+
+body.sidebar-collapsed .sidebar {
+ transform: translateX(-250px);
+}
+
+body.sidebar-collapsed .sidebar-link {
+ opacity: -20;
+ transform: translateX(260px);
+}
+
+body.sidebar-collapsed .navbar,
+body.sidebar-collapsed .main-content {
+ transform: translateX(0);
+ width: 100%;
+}
+
+.sidebar,
+.navbar,
+.main-content {
+ transition: all var(--transition-speed) ease;
+}
+
+/* NAV BAR */
+.navbar {
+ position: fixed;
+ top: 0;
+ left: 0;
+ right: 0;
+ transform: translateX(250px);
+ height: var(--header-height);
+ width: calc(100% - 250px);
+ box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ padding: 0 var(--space-md);
+ z-index: 900;
+}
+
+#sidebar-toggle {
+ cursor: pointer;
+}
+
+/* Inicio do css do seletor antigo */
+.navbar-farm-selector {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ cursor: default;
+ opacity: 0.75;
+ transition: all var(--transition-speed) ease;
+}
+
+.navbar-farm-selector:hover {
+ opacity: 1;
+}
+
+.farm-name {
+ font-size: var(--font-md);
+ font-weight: 500;
+ margin: 0;
+ white-space: nowrap;
+}
+/* Fim do css do seletor antigo */
+
+.navbar-user-info {
+ display: flex;
+ justify-content: flex-end;
+ gap: var(--space-md);
+}
+
+.username {
+ cursor: default;
+}
+
+/* MAIN CONTENT */
+.main-content {
+ position: absolute;
+ top: var(--header-height);
+ left: 0;
+ bottom: 0;
+ right: 0;
+ transform: translateX(250px);
+ padding: var(--space-lg);
+ width: calc(100% - 250px);
+ overflow-y: auto;
+}
\ No newline at end of file
diff --git a/multigranja/static/img/favicon/favicon.ico b/multigranja/static/img/favicon/favicon.ico
new file mode 100644
index 0000000..7a739ff
Binary files /dev/null and b/multigranja/static/img/favicon/favicon.ico differ
diff --git a/multigranja/static/js/main.js b/multigranja/static/js/main.js
new file mode 100644
index 0000000..a3313c7
--- /dev/null
+++ b/multigranja/static/js/main.js
@@ -0,0 +1,19 @@
+// SIDEBAR TOGGLER
+document.addEventListener("DOMContentLoaded", () => {
+ const button = document.getElementById("sidebar-toggle");
+ const body = document.body;
+
+ if (!button) return;
+
+ const savedState = localStorage.getItem("sidebarCollapsed");
+ if (savedState === "true") {
+ body.classList.add("sidebar-collapsed");
+ }
+
+ button.addEventListener("click", () => {
+ body.classList.toggle("sidebar-collapsed");
+
+ const isCollapsed = body.classList.contains("sidebar-collapsed");
+ localStorage.setItem("sidebarCollapsed", isCollapsed);
+ });
+});
\ No newline at end of file
diff --git a/requirements.txt b/requirements.txt
new file mode 100644
index 0000000..c9d8643
Binary files /dev/null and b/requirements.txt differ