BatchType | Autorização de usuários | Fatores de Consumo

Com a finalização da estrutura da tela de configuração da fazenda, três recursos foram implementados:
1 - Fatores de consumo: Agora é possível definir três fatores diferentes, o consumo padrão (ainda passará por ajustes) que representará o consumo do animal com base em seu peso, o consumo no verão, valor utilizado para correção de consumo nos meses de verão e consumo de inverno segundo a mesma lógica da informação anterior;
2 - BatchTypes: Tipos de lote, permite ao usuário, na tela de configuração da sua fazenda, criar tipos para os lotes, os quais carregam um multiplicador de consumo, utilizado futuramente na geração de relatório como uma camada de correção a mais nos valores;
3 - Permissão de acesso para outros usuários: A tela de configuração da fazenda agora permite que sejam inclusos outros usuários na fazenda selecionada, carregando as mesmas permissões e capacidades do dono.
This commit is contained in:
Eberton committed 2026-05-31 01:12:30 -03:00
1 parent 8d61dc60a8
commit 740547a8ed
22 files changed
+664 -63

No files matched your search

@@ -42,3 +42,10 @@ document.addEventListener("DOMContentLoaded", () => {
}); });
}); });
}); });
// Blocks scroll form changing the value on inputs type number
document.querySelectorAll('input[type="number"]').forEach(input => {
input.addEventListener('wheel', function(event) {
event.target.blur();
});
});
@@ -11,10 +11,12 @@
<link rel="stylesheet" href="{% static 'core/css/reset.css' %}"> <link rel="stylesheet" href="{% static 'core/css/reset.css' %}">
<link rel="stylesheet" href="{% static 'core/css/variables.css' %}"> <link rel="stylesheet" href="{% static 'core/css/variables.css' %}">
<link rel="stylesheet" href="{% static 'core/css/global.css' %}">
<link rel="stylesheet" href="{% static 'core/css/components/buttons.css' %}"> <link rel="stylesheet" href="{% static 'core/css/components/buttons.css' %}">
<link rel="stylesheet" href="{% static 'core/css/global.css' %}">
<!-- Templates based on base_model can't pass their stylesheet on block extra_css -->
<link rel="stylesheet" href="{% static 'core/css/components/confirmation_popup.css' %}"> <link rel="stylesheet" href="{% static 'core/css/components/confirmation_popup.css' %}">
<link rel="stylesheet" href="{% static 'farm/css/user_authorize.css' %}">
{% block extra_css %}{% endblock %} {% block extra_css %}{% endblock %}
+2 -1
View File
@@ -2,7 +2,7 @@ from django.contrib import admin
from .models.user import User from .models.user import User
from .models.farm import Farm, PaddockGroup, Paddock from .models.farm import Farm, PaddockGroup, Paddock
from .models.animal import Animal, Batch, BatchAnimal, FeedingRecord from .models.animal import Animal, BatchType, Batch, BatchAnimal, FeedingRecord
from .models.rotation import Rotation from .models.rotation import Rotation
admin.site.register([ admin.site.register([
@@ -15,4 +15,5 @@ admin.site.register([
BatchAnimal, BatchAnimal,
FeedingRecord, FeedingRecord,
Rotation, Rotation,
BatchType
]) ])
+12 -1
View File
@@ -1,5 +1,5 @@
from django import forms from django import forms
from ..models.animal import Batch, Animal from ..models.animal import Batch, Animal, BatchType
class AnimalCreateForm(forms.ModelForm): class AnimalCreateForm(forms.ModelForm):
class Meta: class Meta:
@@ -43,3 +43,14 @@ class BatchForm(forms.ModelForm):
'animals': 'Animais', 'animals': 'Animais',
} }
BatchCreateForm = BatchEditForm = BatchForm # Alias to BatchForm since its implementations are the same BatchCreateForm = BatchEditForm = BatchForm # Alias to BatchForm since its implementations are the same
class BatchTypeForm(forms.ModelForm):
class Meta:
model = BatchType
fields = ['name', 'consumption_factor']
labels = {
'name': 'Nome',
'consumption_factor': 'Fator de consumo',
}
BatchTypeCreateForm = BatchTypeEditForm = BatchTypeForm
@@ -0,0 +1,29 @@
# Generated by Django 6.0.2 on 2026-05-30 03:13
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('entities', '0001_initial'),
]
operations = [
migrations.CreateModel(
name='BatchType',
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)),
('consumption_factor', models.DecimalField(decimal_places=2, default=1.0, max_digits=4)),
('farm', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='batch_types', to='entities.farm')),
],
),
migrations.AlterField(
model_name='batch',
name='batch_type',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='batches', to='entities.batchtype'),
),
]
@@ -0,0 +1,28 @@
# Generated by Django 6.0.2 on 2026-05-30 19:18
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('entities', '0002_batchtype_alter_batch_batch_type'),
]
operations = [
migrations.AddField(
model_name='farm',
name='default_consumption',
field=models.DecimalField(decimal_places=2, default=1.0, max_digits=4),
),
migrations.AddField(
model_name='farm',
name='summer_consumption',
field=models.DecimalField(decimal_places=2, default=1.0, max_digits=4),
),
migrations.AddField(
model_name='farm',
name='winter_consumption',
field=models.DecimalField(decimal_places=2, default=1.0, max_digits=4),
),
]
+29 -1
View File
@@ -36,6 +36,10 @@ class Animal(models.Model):
else: else:
return "Menos de um mês" # Less than a month return "Menos de um mês" # Less than a month
@property
def is_in_batch(self):
return self.batchanimal_set.filter(is_active=True).exists()
class Meta: class Meta:
constraints = [ constraints = [
models.UniqueConstraint( models.UniqueConstraint(
@@ -45,11 +49,25 @@ class Animal(models.Model):
) )
] ]
class BatchType(models.Model):
is_active = models.BooleanField(default=True)
farm = models.ForeignKey('entities.Farm', on_delete=models.CASCADE, related_name='batch_types')
name = models.CharField(max_length=45)
consumption_factor = models.DecimalField(max_digits=4, decimal_places=2, default=1.00)
def __str__(self):
state = 'Ativo' if self.is_active else 'Inativo'
return f"{state} - {self.name}, {self.consumption_factor}"
@property
def consumption_percentage(self):
return int((self.consumption_factor - 1) * 100)
class Batch(models.Model): class Batch(models.Model):
is_active = models.BooleanField(default=True) is_active = models.BooleanField(default=True)
identifier = models.CharField(max_length=45) identifier = models.CharField(max_length=45)
animal_category = models.CharField(max_length=45, null=True, blank=True) animal_category = models.CharField(max_length=45, null=True, blank=True)
batch_type = models.CharField(max_length=45, null=True, blank=True) batch_type = models.ForeignKey(BatchType, on_delete=models.SET_NULL, related_name='batches', null=True, blank=True)
animals = models.ManyToManyField(Animal, through='BatchAnimal', related_name='batches') animals = models.ManyToManyField(Animal, through='BatchAnimal', related_name='batches')
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
@@ -94,6 +112,16 @@ class BatchAnimal(models.Model):
def __str__(self): def __str__(self):
return f"{self.animal.identifier} in {self.batch.identifier}" return f"{self.animal.identifier} in {self.batch.identifier}"
def save(self, *args, **kwargs):
is_active_changed = self.__original_is_active != self.is_active
if is_active_changed and self.is_active:
raise ValueError(
"Reactivating a BatchAnimal is not allowed automatically because it may violate "
"the unique active batch constraint for an animal. Review the logic in "
"models/animal.py (BatchAnimal.save) before proceeding."
)
super().save(*args, **kwargs)
class FeedingRecord(models.Model): class FeedingRecord(models.Model):
animal = models.ForeignKey(Animal, on_delete=models.CASCADE, related_name='feeding_record') 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') rotation = models.ForeignKey('entities.Rotation', on_delete=models.CASCADE, related_name='feeding_record')
+17
View File
@@ -4,10 +4,27 @@ class Farm(models.Model):
is_active = models.BooleanField(default=True) is_active = models.BooleanField(default=True)
name = models.CharField(max_length=45) name = models.CharField(max_length=45)
total_area = models.IntegerField(null=True, blank=True) total_area = models.IntegerField(null=True, blank=True)
default_consumption = models.DecimalField(max_digits=4, decimal_places=2, default=1.00)
summer_consumption = models.DecimalField(max_digits=4, decimal_places=2, default=1.00)
winter_consumption = models.DecimalField(max_digits=4, decimal_places=2, default=1.00)
def __str__(self): def __str__(self):
return self.name return self.name
@property
def animal_count(self):
return self.animals.filter(
is_active=True
).count()
@property
def paddock_count(self):
return self.paddocks.filter(
is_active=True
).count()
class PaddockGroup(models.Model): class PaddockGroup(models.Model):
is_active = models.BooleanField(default=True) is_active = models.BooleanField(default=True)
identifier = models.CharField(max_length=45) identifier = models.CharField(max_length=45)
+107 -9
View File
@@ -1,8 +1,8 @@
from django.shortcuts import get_object_or_404 from django.shortcuts import get_object_or_404
from django.urls import reverse from django.urls import reverse
from ..models import Animal, Batch from ..models import Animal, Batch, BatchType
from ..forms import AnimalCreateForm, AnimalEditForm, BatchForm from ..forms import AnimalCreateForm, AnimalEditForm, BatchForm, BatchTypeCreateForm, BatchTypeEditForm
from entities.models import Farm from entities.models import Farm
# Local utility functions # Local utility functions
@@ -15,15 +15,36 @@ def _get_selected_farm(request):
def _get_animal_by_id(request, animal_id): def _get_animal_by_id(request, animal_id):
return get_object_or_404(Animal, pk=animal_id, farm_id=_get_selected_farm_id(request)) return get_object_or_404(Animal, pk=animal_id, farm_id=_get_selected_farm_id(request))
def _get_farms_active_animals(request): # def _get_farms_active_animals(request):
return Animal.objects.filter( # return Animal.objects.filter(
farm=_get_selected_farm_id(request), # farm=_get_selected_farm_id(request),
is_active=True # is_active=True
) # )
def _get_batch_by_id(request, batch_id): def _get_batch_by_id(request, batch_id):
return get_object_or_404(Batch, pk=batch_id, is_active=True) return get_object_or_404(Batch, pk=batch_id, is_active=True)
def _get_farms_batch_types(request):
return BatchType.objects.filter(is_active=True, farm_id=_get_selected_farm_id(request))
def _get_no_batch_farms_animals(request):
return Animal.objects.filter(
farm_id=_get_selected_farm_id(request),
is_active=True
).exclude(
batchanimal__is_active=True
)
def _get_animals_from_batch(batch_id):
return Batch.objects.get(
id=batch_id
).animals.filter(
batchanimal__is_active=True
)
def _get_batchtype_by_id(request, batchtype_id):
return get_object_or_404(BatchType, pk=batchtype_id, is_active=True, farm=_get_selected_farm(request))
# Animal Services # Animal Services
def get_animal_create_context(): def get_animal_create_context():
form = AnimalCreateForm() form = AnimalCreateForm()
@@ -92,7 +113,8 @@ def delete_animal(request, animal_id):
# Batch Services # Batch Services
def get_batch_create_context(request): def get_batch_create_context(request):
form = BatchForm() form = BatchForm()
form.fields['animals'].queryset = _get_farms_active_animals(request) form.fields['animals'].queryset = _get_no_batch_farms_animals(request)
form.fields['batch_type'].queryset = _get_farms_batch_types(request)
context = { context = {
'title': 'Novo Lote', 'title': 'Novo Lote',
@@ -117,8 +139,12 @@ def create_batch(request):
def get_batch_update_context(request, batch_id): def get_batch_update_context(request, batch_id):
batch_old = _get_batch_by_id(request, batch_id) batch_old = _get_batch_by_id(request, batch_id)
animals_querset = _get_no_batch_farms_animals(request) | _get_animals_from_batch(batch_id)
animals_querset = animals_querset.distinct()
form = BatchForm(instance=batch_old) form = BatchForm(instance=batch_old)
form.fields['animals'].queryset = _get_farms_active_animals(request) form.fields['animals'].queryset = animals_querset
form.fields['batch_type'].queryset = _get_farms_batch_types(request)
context = { context = {
'title': f'Editar Lote {batch_old.identifier}', 'title': f'Editar Lote {batch_old.identifier}',
@@ -164,3 +190,75 @@ def delete_batch(request, batch_id):
batch = _get_batch_by_id(request, batch_id) batch = _get_batch_by_id(request, batch_id)
batch.is_active = False batch.is_active = False
batch.save() batch.save()
# Batch Type Services
def get_batchtype_create_context(request):
form = BatchTypeCreateForm()
context = {
'title': 'Novo Tipo de Lote',
'form': form,
'action_url': reverse('entities:animal:batchtype_create'),
'return_url': reverse('entities:farm:farm_home'),
}
return context
def create_batchtype(request):
form = BatchTypeCreateForm(request.POST)
if form.is_valid():
batchtype = form.save(commit=False)
batchtype.farm = _get_selected_farm(request)
batchtype.save()
return True
return False
def get_batchtype_update_context(request, batchtype_id):
batchtype = _get_batchtype_by_id(request, batchtype_id)
form = BatchTypeEditForm(instance=batchtype)
context = {}
context['title'] = 'Editar tipo de lote'
context['action_url'] = reverse(
'entities:animal:batchtype_update',
kwargs={'batchtype_id': batchtype.pk}
)
context['return_url'] = reverse('entities:farm:farm_home')
context['form'] = form
return context
def update_batchtype(request, batchtype_id):
batchtype = _get_batchtype_by_id(request, batchtype_id)
form = BatchTypeEditForm(request.POST, instance=batchtype)
if form.is_valid():
form.save()
return True
return False
def get_batchtype_delete_context(request, batchtype_id):
batchtype = _get_batchtype_by_id(request, batchtype_id)
context = {}
context['description'] = f"Excluir tipo de lote {batchtype.name}?"
context['positive_url'] = reverse('entities:animal:batchtype_delete', kwargs={'batchtype_id': batchtype_id})
context['negative_url'] = reverse('entities:farm:farm_home')
return context
def delete_batchtype(request, batchtype_id):
batchtype = _get_batchtype_by_id(request, batchtype_id)
Batch.objects.filter(
is_active=True,
batch_type=batchtype
).update(
batch_type=None
)
batchtype.is_active = False
batchtype.save()
+78 -12
View File
@@ -1,7 +1,9 @@
from django.shortcuts import get_object_or_404 from django.shortcuts import get_object_or_404
from django.urls import reverse from django.urls import reverse
from django.db.models import Count, Q
from decimal import Decimal
from ..models import Farm, Paddock, PaddockGroup from ..models import Farm, Paddock, PaddockGroup, BatchType, User
from ..forms import FarmCreateForm, FarmEditForm, PaddockCreateForm, PaddockEditForm, PaddockGroupCreateForm, PaddockGroupEditForm from ..forms import FarmCreateForm, FarmEditForm, PaddockCreateForm, PaddockEditForm, PaddockGroupCreateForm, PaddockGroupEditForm
@@ -43,20 +45,36 @@ def _get_paddock_group_by_id(request, paddock_group_id):
is_active=True is_active=True
) )
def _get_farms_batch_types(request):
return BatchType.objects.filter(is_active=True, farm=_get_selected_farm(request))
# Farm Services # Farm Services
def get_farm_home_context(request): def get_farm_home_context(request):
authenticated_user = request.user authenticated_user = request.user
selected_farm = _get_selected_farm(request)
farms_query = authenticated_user.farms.filter(is_active=True)
farms_with_counts = farms_query.annotate(
total_animals=Count(
'animals',
filter=Q(animals__is_active=True),
distinct=True
),
total_paddocks=Count(
'paddocks',
filter=Q(paddocks__is_active=True),
distinct=True
)
)
allowed_users = selected_farm.users.exclude(pk=authenticated_user.pk)
batch_types = _get_farms_batch_types(request)
context = {} context = {}
context['title'] = 'Fazenda' context['title'] = selected_farm.name
context['selected_farm'] = selected_farm
farms = Farm.objects.filter( context['farms'] = farms_with_counts
users=authenticated_user, context['allowed_users'] = allowed_users
is_active=True context['batch_types'] = batch_types
)
context['farms'] = farms
return context return context
@@ -67,7 +85,7 @@ def get_farm_create_context():
context['title'] = 'Nova fazenda' context['title'] = 'Nova fazenda'
context['form'] = form context['form'] = form
context['action_url'] = reverse('entities:farm:farm_create') context['action_url'] = reverse('entities:farm:farm_create')
context['return_url'] = reverse('entities:farm:farm_list') context['return_url'] = reverse('entities:farm:farm_home')
return context return context
@@ -92,7 +110,7 @@ def get_update_farm_context(request, farm_id):
'entities:farm:farm_update', 'entities:farm:farm_update',
kwargs={'farm_id': farm.pk} kwargs={'farm_id': farm.pk}
) )
context['return_url'] = reverse('entities:farm:farm_list') context['return_url'] = reverse('entities:farm:farm_home')
context['form'] = form context['form'] = form
return context return context
@@ -116,7 +134,7 @@ def get_farm_delete_context(request, farm_id):
'entities:farm:farm_delete', 'entities:farm:farm_delete',
kwargs={'farm_id': farm_id} kwargs={'farm_id': farm_id}
) )
context['negative_url'] = reverse('entities:farm:farm_list') context['negative_url'] = reverse('entities:farm:farm_home')
return context return context
@@ -125,6 +143,17 @@ def delete_farm(request, farm_id):
farm.is_active = False farm.is_active = False
farm.save() farm.save()
def update_consumption(request):
default_consumption = Decimal(request.POST.get('default-consume'))
summer_consumption = Decimal(request.POST.get('summer-consume'))
winter_consumption = Decimal(request.POST.get('winter-consume'))
selected_farm = _get_selected_farm(request)
selected_farm.default_consumption = default_consumption
selected_farm.summer_consumption = summer_consumption
selected_farm.winter_consumption = winter_consumption
selected_farm.save()
# Paddock Services # Paddock Services
def get_paddock_create_context(): def get_paddock_create_context():
@@ -271,3 +300,40 @@ def delete_paddock_group(request, paddock_group_id):
paddock_group = _get_paddock_group_by_id(request, paddock_group_id) paddock_group = _get_paddock_group_by_id(request, paddock_group_id)
paddock_group.is_active = False paddock_group.is_active = False
paddock_group.save() paddock_group.save()
# User Authorize Services
def authorize_user(request, user_id):
farm = _get_selected_farm(request)
user = get_object_or_404(User, id=user_id, is_active=True)
user.farms.add(farm)
def get_deauthorize_user_context(request, user_id):
user = get_object_or_404(User, id=user_id, is_active=True)
context = {}
context['description'] = f"Remover acesso de {user.short_name}?"
context['positive_url'] = reverse(
'entities:farm:farm_user_deauthorize',
kwargs={'user_id': user_id}
)
context['negative_url'] = reverse('entities:farm:farm_home')
return context
def deauthorize_user(request, user_id):
farm = _get_selected_farm(request)
user = get_object_or_404(User, id=user_id, is_active=True)
user.farms.remove(farm)
def get_user_card_context(request):
context = {}
informed_username = request.GET.get('username')
found_user = User.objects.filter(login=informed_username).first()
if found_user.farms.contains(_get_selected_farm(request)):
context['already_allowed'] = 'True'
else:
context['found_user'] = found_user
return context
@@ -0,0 +1,120 @@
.user-authorize-title {
margin-bottom: 20px;
font-size: 28px;
font-weight: 600;
}
.user-search {
background: #fff;
border: 1px solid #e9ecef;
border-radius: 12px;
padding: 20px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);
}
.user-search label {
display: block;
margin-bottom: 8px;
font-weight: 500;
}
.user-search input {
width: 100%;
padding: 10px 12px;
border: 1px solid #dee2e6;
border-radius: 8px;
margin-bottom: 12px;
transition: border-color 0.2s ease;
}
.user-search input:focus {
outline: none;
border-color: #198754;
}
.user-search button {
width: 100%;
border: none;
border-radius: 8px;
padding: 10px;
cursor: pointer;
font-weight: 500;
color: white;
background: #198754;
transition: 0.2s ease;
}
.user-search button:hover {
background: #157347;
}
.user-card-container {
margin-top: 20px;
min-height: 150px;
width: 350px;
}
.user-card {
border: 1px solid #e9ecef;
border-radius: 10px;
padding: 16px;
background: #fafafa;
display: flex;
flex-direction: column;
gap: 10px;
}
.user-card .name {
margin: 0;
font-size: 16px;
font-weight: 600;
}
.user-card .name i {
margin-right: 6px;
color: #6c757d;
}
.user-card .alert {
margin: 0;
font-size: 13px;
color: #6c757d;
}
.user-card button {
align-self: flex-end;
text-decoration: none;
background: #198754;
color: white;
padding: 8px 14px;
border-radius: 8px;
font-size: 14px;
font-weight: 500;
transition: 0.2s ease;
}
.user-card button:hover {
background: #157347;
}
.failed-user-find {
text-align: center;
padding: 15px;
color: #dc3545;
font-size: 14px;
}
.failed-user-find p {
margin: 0;
}
.already-allowed-user {
text-align: center;
padding: 15px;
color: #198754;
font-size: 14px;
}
.already-allowed-user p {
margin: 0;
}
@@ -8,6 +8,9 @@ document.addEventListener("DOMContentLoaded", () => {
let isAnimating = false; let isAnimating = false;
container.addEventListener("wheel", (event) => { container.addEventListener("wheel", (event) => {
let isMobile = window.innerWidth <= 768;
if (isMobile) return;
event.preventDefault(); event.preventDefault();
const maxScroll = const maxScroll =
@@ -0,0 +1,11 @@
{% extends 'layouts/base_modal.html' %}
{% load static %}
{% block extra_css %}
<link rel="stylesheet" href="{% static 'animal/css/batch.css' %}">
{% endblock %}
{% block content %}
NOVO TIPO DE LOTE <br>
{% include 'components/form.html' %}
{% endblock %}
@@ -0,0 +1,11 @@
{% extends 'layouts/base_modal.html' %}
{% load static %}
{% block extra_css %}
<link rel="stylesheet" href="{% static 'animal/css/animal.css' %}">
{% endblock %}
{% block content %}
EDITAR TIPO DE LOTE <br>
{% include 'components/form.html' %}
{% endblock %}
@@ -1,46 +1,105 @@
{% extends 'layouts/base.html' %} {% extends 'layouts/base.html' %}
{% load static %} {% load static %}
{% load l10n %}
{% block extra_css %} {% block extra_css %}
<link rel="stylesheet" href="{% static 'farm/css/home.css' %}"> {{ block.super }}
<link rel="stylesheet" href="{% static 'farm/css/farm_home.css' %}">
{% endblock %} {% endblock %}
{% block content %} {% block content %}
<div class="farm_title"> <div class="header">
<h1>Fazendas</h1> <div class="header-left">
<form method="GET" action="{% url 'entities:farm:farm_home' %}" class="form_farm_search"> <h1 class="title">{{ title }}</h1>
{% if filter_query %} <p>Configurações da fazenda</p>
<a href="{% url 'entities:farm:farm_home' %}" class="btn-clear-filter">{{filter_query}}<i class="fa-solid fa-xmark"></i></a> </div>
{% endif %} <div class="header-right">
<label for="farm_search">Buscar</label> <div class="farm-info">
<input type="text" name="farm_search" required> <p>Animais</p>
<button type="submit"><i class="fa-solid fa-magnifying-glass"></i></button> <p>{{ selected_farm.animal_count }}</p>
</div>
<div class="farm-info">
<p>Piquetes</p>
<p>{{ selected_farm.paddock_count }}</p>
</div>
<div class="farm-info">
<p>Área</p>
<p>{{ selected_farm.total_area }}</p>
</div>
</div>
</div>
<div class="farm-config-container">
<h2 class="farm-config-title">Fatores de consumo</h2>
<form action="{% url "entities:farm:consumption_update" %}" method="POST">
{% csrf_token %}
<div class="config-card">
<p class="config-card-title">Consumo padrão</p>
<input type="number" name="default-consume" min="0.01" step="0.01" value="{{ selected_farm.default_consumption|unlocalize }}">
</div>
<div class="config-card">
<p class="config-card-title">Consumo no verão</p>
<input type="number" name="summer-consume" min="0.01" step="0.01" value="{{ selected_farm.summer_consumption|unlocalize }}">
</div>
<div class="config-card">
<p class="config-card-title">Consumo no inverno</p>
<input type="number" name="winter-consume" min="0.01" step="0.01" value="{{ selected_farm.winter_consumption|unlocalize }}">
</div>
<div class="config-submit">
<button type="submit">Salvar</button>
</div>
</form> </form>
<a href="{% url 'entities:farm:farm_create' %}">Novo <i class="fa-solid fa-plus"></i></a> </div>
<div class="batch-type-config-container">
<div class="batch-type-config-header">
<h2 class="batch-type-title">Tipos de lote</h2>
<a hx-get="{% url "entities:animal:batchtype_create" %}" hx-swap="beforeend" hx-target="body"><i class="fa-solid fa-plus"></i> Tipo</a>
</div>
{% for type in batch_types %}
<div class="batch-type-card">
<div class="batch-type-left">
<p class="type-title">{{ type.name }}</p>
<p class="type-consumption-factor">Multiplicativo de consumo: {{ type.consumption_factor }} ({{ type.consumption_percentage }}%)</p>
</div>
<div class="batch-type-right">
<a class="edit-type" hx-get="{% url "entities:animal:batchtype_update" type.id %}" hx-swap="beforeend" hx-target="body">Editar</a>
<a class="delete-type" hx-get="{% url "entities:animal:batchtype_delete" type.id %}" hx-swap="beforeend" hx-target="body">Excluir</a>
</div>
</div>
{% endfor %}
</div>
<div class="allowed-users-container">
<div class="allowed-users-header">
<h2 class="allowed-users-title">Usuários com acesso</h2>
<a hx-get="{% url "entities:farm:new_authorization" %}" hx-swap="beforeend" hx-target="body"><i class="fa-solid fa-plus"></i> Permissão</a>
</div>
{% if allowed_users|length < 1 %}
<p class="type-consumption-factor">Seu usuário é o único vinculado a essa fazenda</p>
{% endif %}
{% for user in allowed_users %}
<div class="allowed-user-card">
<p class="name"><i class="fa-regular fa-user"></i> {{user.name}}</p>
<a hx-get="{% url "entities:farm:farm_user_deauthorize" user.id %}" hx-swap="beforeend" hx-target="body" class="user-remove"><i class="fa-solid fa-user-slash"></i></a>
</div>
{% endfor %}
</div>
<div class="available-farms-container">
<h2 class="available-farms-title">Suas fazendas</h2>
{% for farm in farms %}
<div class="available-farm-card">
<p class="available-farm-name">{{ farm.name }}</p>
<div class="available-farm-ifo">
<p class="animal-count">{{ farm.animal_count }} Animais</p>
<p class="animal-count">{{ farm.paddock_count }} Piquetes</p>
</div>
<div class="available-farm-options">
<a class="available-farm-select" href="{% url "entities:user:farm_set" farm.id %}">Selecionar</a>
<a class="available-farm-delete" hx-get="{% url "entities:farm:farm_delete" farm.id %}" hx-swap="beforeend" hx-target="body">Excluir</a>
</div>
</div>
{% endfor %}
</div> </div>
{% if not farms %}
<p>Nenhuma fazenda encontrado.</p>
{% else %}
<table>
<tr>
<th>Nome</th>
<th>Área total</th>
<th>Editar</th>
<th>Excluir</th>
</tr>
{% for farm in farms %}
<tr>
<td>{{ farm.name }}</td>
<td>{{ farm.total_area }} m2</td>
<td><a href="{% url 'entities:farm:farm_update' farm.id %}"><i class="fa-solid fa-pen-to-square"></i></a></td>
<td>
<form method="POST" action="{% url 'entities:farm:farm_delete' farm.id %}">
{% csrf_token %}
<button type="submit" class="btn-icon-trash"><i class="fa-solid fa-trash"></i></button>
</form>
</td>
</tr>
{% endfor %}
</table>
{% endif %}
{% endblock %} {% endblock %}
@@ -0,0 +1,22 @@
<div class="user-card-container">
{% if found_user %}
<div class="user-card">
<p class="name"><i class="fa-regular fa-user"></i> {{found_user.full_name}}</p>
<p class="alert">Ao liberar esse usuário ele terá as mesmas permissões que você</p>
<form action="{% url "entities:farm:farm_user_authorize" found_user.id %}" method="POST">
{% csrf_token %}
<button type="submit">Permitir</button>
</form>
</div>
{% else %}
{% if already_allowed %}
<div class="already-allowed-user">
<p>O usuário já possui acesso a essa fazenda</p>
</div>
{% else %}
<div class="failed-user-find">
<p>Não foi possível localizar esse usuário</p>
</div>
{% endif %}
{% endif %}
</div>
@@ -0,0 +1,12 @@
{% extends 'layouts/base_modal.html' %}
{% load static %}
{% block content %}
<h1 class="user-authorize-title">Autorizar usuário</h1>
<div class="user-search">
<label for="username">Usuário</label>
<input type="text" id="username" name="username" placeholder="Encontre buscando pelo login do usuário" autocomplete="off">
<button hx-get="{% url "entities:farm:get_user_card" %}" hx-include="#username" hx-target=".user-card-container" hx-swap="outterHTML">Buscar</button>
<div class="user-card-container"></div>
</div>
{% endblock content %}
@@ -27,8 +27,8 @@
<button type="submit"> <button type="submit">
<i class="fa-solid fa-magnifying-glass"></i> <i class="fa-solid fa-magnifying-glass"></i>
</button> </button>
<a class="new-entity-button" hx-get="{% url create_url %}" hx-swap="beforeend" hx-target="body">Novo <i class="fa-solid fa-plus"></i></a>
</form> </form>
<a class="new-entity-button" hx-get="{% url create_url %}" hx-swap="beforeend" hx-target="body">Novo <i class="fa-solid fa-plus"></i></a>
</div> </div>
{% if not object_list %} {% if not object_list %}
+4
View File
@@ -15,4 +15,8 @@ urlpatterns = [
path('batch/create/', views.batch_create, name='batch_create'), path('batch/create/', views.batch_create, name='batch_create'),
path('batch/update/<int:batch_id>', views.batch_update, name='batch_update'), path('batch/update/<int:batch_id>', views.batch_update, name='batch_update'),
path('batch/delete/<int:batch_id>', views.batch_delete, name='batch_delete'), path('batch/delete/<int:batch_id>', views.batch_delete, name='batch_delete'),
path('batch/type/create/', views.batchtype_create, name='batchtype_create'),
path('batch/type/update/<int:batchtype_id>', views.batchtype_update, name='batchtype_update'),
path('batch/type/delete/<int:batchtype_id>', views.batchtype_delete, name='batchtype_delete'),
] ]
+6
View File
@@ -5,6 +5,11 @@ from ..views import PaddockListView, PaddockGroupListView
app_name = 'farm' app_name = 'farm'
urlpatterns = [ urlpatterns = [
path('usercard/', views.get_user_card, name='get_user_card'),
path('authorize/', views.farm_user_authorize, name='new_authorization'),
path('authorize/<int:user_id>', views.farm_user_authorize, name='farm_user_authorize'),
path('deauthorize/<int:user_id>', views.farm_user_deauthorize, name='farm_user_deauthorize'),
path('', views.farm_home, name='farm_home'), path('', views.farm_home, name='farm_home'),
path('paddock/list/', PaddockListView.as_view(), name='paddock_list'), path('paddock/list/', PaddockListView.as_view(), name='paddock_list'),
path('paddock/group/list/', PaddockGroupListView.as_view(), name='paddock_group_list'), path('paddock/group/list/', PaddockGroupListView.as_view(), name='paddock_group_list'),
@@ -12,6 +17,7 @@ urlpatterns = [
path('create/', views.farm_create, name='farm_create'), path('create/', views.farm_create, name='farm_create'),
path('update/<int:farm_id>', views.farm_update, name='farm_update'), path('update/<int:farm_id>', views.farm_update, name='farm_update'),
path('delete/<int:farm_id>', views.farm_delete, name='farm_delete'), path('delete/<int:farm_id>', views.farm_delete, name='farm_delete'),
path('consumption/update', views.consumption_update, name='consumption_update'),
path('paddock/create/', views.paddock_create, name='paddock_create'), path('paddock/create/', views.paddock_create, name='paddock_create'),
path('paddock/update/<int:paddock_id>', views.paddock_update, name='paddock_update'), path('paddock/update/<int:paddock_id>', views.paddock_update, name='paddock_update'),
+36
View File
@@ -80,3 +80,39 @@ def batch_delete(request, batch_id):
services.delete_batch(request, batch_id) services.delete_batch(request, batch_id)
return redirect('entities:animal:batch_list') return redirect('entities:animal:batch_list')
# Batch Type views
@farm_required
def batchtype_create(request):
context = services.get_batchtype_create_context(request)
if request.method == 'GET':
return render(request, 'batch/batchtype_create.html', context)
if request.method == 'POST':
if services.create_batchtype(request):
return redirect('entities:farm:farm_home')
context['error'] = "Não foi possível criar o tipo de lote"
return render(request, 'batch/batchtype_create.html', context)
@farm_required
def batchtype_update(request, batchtype_id):
context = services.get_batchtype_update_context(request, batchtype_id)
if request.method == 'GET':
return render(request, 'batch/batchtype_update.html', context)
if request.method == 'POST':
if services.update_batchtype(request, batchtype_id):
return redirect('entities:farm:farm_home')
context['error'] = "Não foi possível editar o tipo de lote"
return render(request, 'batch/batchtype_update.html', context)
@farm_required
def batchtype_delete(request, batchtype_id):
if request.method == 'GET':
context = services.get_batchtype_delete_context(request, batchtype_id)
return render(request, 'components/confirmation_popup.html', context)
if request.method == 'POST':
services.delete_batchtype(request, batchtype_id)
return redirect('entities:farm:farm_home')
+31 -1
View File
@@ -17,7 +17,7 @@ def farm_create(request):
if request.method == 'POST': if request.method == 'POST':
if services.create_farm(request): if services.create_farm(request):
return redirect('entities:farm:farm_list') return redirect('entities:farm:farm_home')
context['error'] = "Não foi possível criar a fazenda" context['error'] = "Não foi possível criar a fazenda"
return render(request, 'farm/farm_create.html', context) return render(request, 'farm/farm_create.html', context)
@@ -44,6 +44,11 @@ def farm_delete(request, farm_id):
return redirect('entities:farm:farm_create') return redirect('entities:farm:farm_create')
def consumption_update(request):
if request.method=='POST':
services.update_consumption(request)
return redirect('entities:farm:farm_home')
# Paddock Views # Paddock Views
@farm_required @farm_required
@@ -121,3 +126,28 @@ def paddock_group_delete(request, paddock_group_id):
services.delete_paddock_group(request, paddock_group_id) services.delete_paddock_group(request, paddock_group_id)
return redirect('entities:farm:paddock_group_list') return redirect('entities:farm:paddock_group_list')
# User Authorization Views
@farm_required
def farm_user_authorize(request, user_id=None):
if request.method == 'GET':
return render(request, 'farm/user_authorize.html')
if request.method == 'POST':
if not user_id:
return redirect('entities:farm:farm_home')
services.authorize_user(request, user_id)
return redirect('entities:farm:farm_home')
@farm_required
def farm_user_deauthorize(request, user_id):
if request.method == 'GET':
context = services.get_deauthorize_user_context(request, user_id)
return render(request, 'components/confirmation_popup.html', context)
if request.method == 'POST':
services.deauthorize_user(request, user_id)
return redirect('entities:farm:farm_home')
@farm_required
def get_user_card(request):
context = services.get_user_card_context(request)
return render(request, 'farm/user-auth-card.html', context)