Salvando estado atual

Diversas modificações
This commit is contained in:
Eberton committed 2026-06-27 03:02:56 -03:00
1 parent 740547a8ed
commit 8dd945798a
69 files changed
+2498 -278

No files matched your search

+11 -11
View File
@@ -3,20 +3,20 @@ 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
farms = Farm.objects.none()
if request.user.is_authenticated:
farms = request.user.farms.only("id", "name")
farms = request.user.farms.filter(is_active=True).only("id", "name")
farm_id = request.session.get("selected_farm_id")
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)
selected_farm = farms.filter(id=farm_id).first()
if selected_farm is None:
request.session.pop("selected_farm_id", None)
return {
'SELECTED_FARM': selected_farm,
'farms': farms
"SELECTED_FARM": selected_farm,
"farms": farms,
}
@@ -7,35 +7,35 @@
/* BUTTON STYLES */
.btn-simple {
color: var(--color-text-dark);
color: var(--color-plus-btn);
padding: 0 var(--space-xs);
border-radius: var(--border-radius-sm);
}
.btn-simple:hover {
color: var(--color-text-light);
color: var(--color-plus-btn-hover);
background-color: var(--color-bg-dark);
transition: all var(--transition-speed) ease;
}
/* PRIMARY BUTTON (Main Action) */
.btn-primary {
background-color: var(--color-primary);
background-color: var(--color-plus-btn);
color: var(--color-text-light); /* White text */
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) */
background-color: var(--color-plus-btn-hover); /* Darker blue on hover (a specific dark value often works best) */
}
/* SECONDARY BUTTON (Neutral Action) */
.btn-secondary {
background-color: var(--color-secondary);
color: var(--color-text-light);
padding: var(--space-sm);
border-radius: var(--border-radius-sm);
color: var(--color-text-light);
}
.btn-secondary:hover {
@@ -44,12 +44,12 @@
/* SUCCESS/DANGER BUTTONS */
.btn-success {
background-color: var(--color-success);
background-color: var(--color-plus-btn);
color: var(--color-text-light);
}
.btn-danger {
background-color: var(--color-danger);
background-color: var(--color-danger-btn);
color: var(--color-text-light);
}
@@ -9,13 +9,14 @@
/* Title */
.confirmation-container h1 {
font-size: 1.5rem;
font-size: 22px;
text-align: center;
}
/* Description */
.confirmation-description {
text-align: end;
margin: 10px 0 15px 0;
}
/* Actions */
@@ -26,11 +27,13 @@
}
/* Both buttons */
.confirmation-container form .confirm, .negate {
.confirmation-container form .confirm,
.confirmation-container form .negate {
display: flex;
align-items: center;
justify-content: center;
min-width: 120px;
min-width: 100px;
max-height: 30px;
padding: var(--space-sm) var(--space-sm);
border: none;
border-radius: var(--border-radius-sm);
@@ -41,30 +44,22 @@
/* Confirm button */
.confirmation-container form .confirm {
background-color: #dc3545;
background-color: var(--color-danger-btn);
color: white;
}
.confirmation-container form .confirm:hover {
filter: brightness(0.92);
}
.confirmation-container form .confirm:active {
transform: scale(0.98);
background-color: var(--color-danger-btn-hover);
}
/* Negate button */
.confirmation-container form .negate {
background-color: var(--color-secondary);
background-color: var(--color-neutral-btn);
color: var(--color-text-light);
}
.confirmation-container form .negate:hover {
filter: brightness(0.92);
}
.confirmation-container form .negate:active {
transform: scale(0.90);
background-color: var(--color-neutral-btn-hover);
}
@media (max-width: 600px) {
@@ -0,0 +1,169 @@
/* Container do formulário */
.generic-form {
padding: 2rem;
background: var(--color-bg-form);
border-radius: 12px;
}
/* Labels */
.generic-form label {
display: block;
margin-bottom: 0.4rem;
font-weight: 600;
color: var(--color-form-label);
}
/* Campos */
.generic-form input,
.generic-form select,
.generic-form textarea {
width: 100%;
padding: 0.75rem 1rem;
margin-bottom: 1rem;
border: 1px solid var(--color-input-border);
border-radius: 8px;
font-size: 1rem;
box-sizing: border-box;
transition: all 0.2s ease;
}
/* Foco */
.generic-form input:focus,
.generic-form select:focus,
.generic-form textarea:focus {
outline: none;
border-color: var(--color-input-focus-border);
box-shadow: 0 0 0 3px rgba(37, 99, 235, 0.15);
}
/* Checkbox e radio */
.generic-form input[type="checkbox"],
.generic-form input[type="radio"] {
width: auto;
margin-right: 0.5rem;
}
/* Mensagens de erro */
.errorlist {
list-style: none;
padding: 0.75rem;
margin-bottom: 1rem;
background: var(--color-bg-error);
color: var(--color-text-error);
border-radius: 8px;
}
/* Texto de ajuda */
.helptext {
display: block;
margin-top: -0.5rem;
margin-bottom: 1rem;
font-size: 0.85rem;
color: var(--color-text-help);
}
/* Área dos botões */
.form-actions {
display: flex;
gap: 1rem;
margin-top: 1.5rem;
justify-content: flex-end;
}
/* Botão principal */
.btn-primary {
display: inline-block;
padding: 0.8rem 1.4rem;
background-color: var(--color-plus-btn);
color: var(--color-text-light); /* White text */
text-decoration: none;
border: none;
border-radius: 8px;
font-weight: 600;
cursor: pointer;
transition: 0.2s;
}
.btn-primary:hover {
background-color: var(--color-plus-btn-hover);
}
/* Botão cancelar */
.btn-secondary {
display: inline-block;
padding: 0.8rem 1.2rem;
background-color: var(--color-neutral-btn);
color: var(--color-text-light);
text-decoration: none;
border-radius: 8px;
font-weight: 600;
transition: 0.2s;
}
.btn-secondary:hover {
background: var(--color-neutral-btn-hover);
}
/* Tom Select - campo */
.generic-form .ts-wrapper {
width: 100%;
margin-bottom: 1rem;
}
/* Caixa principal */
.generic-form .ts-control {
width: 100%;
min-height: 46px;
padding: 0.65rem 1rem;
border: 1px solid var(--color-input-border);
border-radius: 8px;
background: white;
box-sizing: border-box;
font-size: 1rem;
transition: all 0.2s ease;
}
/* Input interno */
.generic-form .ts-control input {
margin: 0;
padding: 0;
border: none;
box-shadow: none;
}
/* Foco */
.generic-form .ts-wrapper.focus .ts-control {
outline: none;
border-color: var(--color-input-focus-border);
box-shadow: 0 0 0 3px rgba(37, 99, 235, 0.15);
}
/* Tags selecionadas (multi select) */
.generic-form .item {
background: var(--color-plus-btn);
color: var(--color-text-light);
border-radius: 6px;
padding: 0.25rem 0.5rem;
}
/* Botão remover tag */
.generic-form .remove {
color: white;
}
/* Dropdown */
.generic-form .ts-dropdown {
border-radius: 8px;
border: 1px solid var(--color-input-border);
box-shadow: 0 4px 12px rgba(0,0,0,.1);
}
/* Opções */
.generic-form .option {
padding: .6rem 1rem;
}
.generic-form .option.active {
background: var(--color-plus-btn-hover);
color: var(--color-text-light);
}
@@ -0,0 +1,5 @@
.form-modal-title {
font-size: 22px;
margin-top: 10px;
margin-left: 10px;
}
+122 -33
View File
@@ -25,6 +25,7 @@ a {
overflow-x: hidden;
z-index: 150;
box-shadow: 2px 0 5px rgba(0, 0, 0, 0.1);
scrollbar-width: none;
}
.sidebar-brand {
@@ -110,29 +111,106 @@ body.sidebar-collapsed .main-content {
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;
/* Farm Selection */
.ts-wrapper {
width: clamp(200px, 25vw, 300px);
margin: 0 auto;
}
.navbar-farm-selector:hover {
opacity: 1;
.ts-wrapper.single .ts-control {
position: relative;
width: 100%;
padding: 4px 20px 4px 6px;
border: none;
border-radius: 6px;
background: transparent;
box-shadow: none;
font-size: 16px;
justify-content: center;
cursor: pointer;
transition: none;
font-weight: 600;
color: var(--color-primary);
}
.farm-name {
font-size: var(--font-md);
font-weight: 500;
margin: 0;
.ts-wrapper.single .ts-control:hover {
box-shadow: none;
}
.ts-wrapper.focus .ts-control {
box-shadow: none;
width: 100%;
}
.ts-wrapper.single .ts-control::after {
content: "";
position: absolute;
right: 8px;
top: 50%;
transform: translateY(-50%);
border-left: 4px solid transparent;
border-right: 4px solid transparent;
border-top: 5px solid #666;
pointer-events: none;
transition: transform .2s ease;
}
.ts-wrapper.single.dropdown-active .ts-control::after {
transform: translateY(-50%) rotate(180deg);
}
.ts-dropdown {
width: 100%;
margin-top: 2px;
border: none;
border-radius: 6px;
background: #f5f5f5;
box-shadow: 2px 2px 6px rgba(0,0,0,.12);
overflow: hidden;
}
.ts-dropdown .option {
padding: 4px 6px;
font-size: 16px;
text-align: center;
cursor: pointer;
font-weight: 600;
color: var(--color-primary);
}
.ts-dropdown .option:hover {
background: #ececec;
}
.ts-dropdown .active {
background: #e0e0e0;
}
.ts-control input {
display: none;
}
.ts-control .item {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
/* Fim do css do seletor antigo */
.navbar-user-info {
.ts-dropdown .option.see-all {
color: var(--color-primary);
background: transparent;
font-weight: 500;
border-top: 1px solid #ddd;
}
.ts-dropdown .option.see-all:hover,
.ts-dropdown .option.see-all.active {
cursor: pointer;
}
/* NAV BAR RIGHT */
.navbar-user-info,
.navbar-user-info-farm-select {
display: flex;
justify-content: flex-end;
gap: var(--space-sm);
@@ -157,33 +235,44 @@ body.sidebar-collapsed .main-content {
/* BASE MODAL */
.modal-base {
position: fixed;
inset: 0;
z-index: 200;
display: flex;
align-items: center;
justify-content: center;
background-color: rgba(0, 0, 0, 0.6);
position: fixed;
inset: 0;
z-index: 200;
display: flex;
align-items: center;
justify-content: center;
background-color: rgba(0, 0, 0, 0.4);
}
.modal-content {
background-color: var(--color-bg-light);
padding: var(--space-lg);
border-radius: var(--space-sm);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2);
background-color: var(--color-bg-light);
padding: var(--space-md);
border-radius: var(--space-sm);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2);
}
@media screen and (max-width: 768px) {
.navbar-farm-selector,
.navbar-user-info {
transition: opacity var(--transition-speed) ease;
}
@view-transition {
navigation: auto;
}
::view-transition-old(root),
::view-transition-new(root) {
animation-duration: 0.3s;
animation-timing-function: ease;
}
@media screen and (max-width: 768px) {
body:not(.sidebar-collapsed) .navbar-farm-selector,
body:not(.sidebar-collapsed) .navbar-user-info {
display: none;
}
body:not(.sidebar-collapsed) .main-content {
transform: translateX(0);
width: 100%;
overflow: hidden;
}
.navbar {
padding: 0 var(--space-sm);
}
@@ -0,0 +1,11 @@
.modal-content {
min-width: 50vw;
max-width: 85vw;
max-height: 90vh;
overflow-y: scroll;
scrollbar-width: none;
}
.modal-content:has(.confirmation-container) {
min-width: 0;
}
@@ -1,34 +1,45 @@
: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;
--login-background-primary: #244238;
--login-background-secondary: #396a5a;
--color-login-button: #396a5a;
--color-login-button-focus: #76948a;
--color-farm-select-hover: #244238;
--color-farm-select-info: #6d6d6d;
--color-new-farm-button: #343a40;
--color-new-farm-hover: #6b7480;
--color-new-farm-button: #396a5a;
--color-new-farm-hover: #244238;
--color-plus-btn: #396a5a;
--color-plus-btn-hover: #76948a;
--color-danger-btn: #8b3d3d;
--color-danger-btn-hover: #b37a7a;
--color-neutral-btn: #5f6b67;
--color-neutral-btn-hover: #97a39f;
--color-input-border: #5f6b67;
--color-bg-filter: #76948a;
--color-text-filter: #f5faf5;
/* GENERIC COLORS */
--color-simple-light-gray: #ccc;
--color-border-subtle: #ccc;
/* PRIMARY COLORS */
--color-primary: #007bff;
--color-primary: #244238;
--color-secondary: #6c757d;
--color-success: #28a745;
--color-danger: #dc3545;
--color-danger-light: #f8d7da;
--color-danger: #8b3d3d;
--color-danger-light: #b37a7a;
--color-warn: #dca235;
/* BACKGROUND/TEXT COLORS */
--color-bg-light: #f0f0f0;
--color-bg-light: #f5f7f5;
--color-bg-white: #fafafa;
--color-bg-dark: #343a40;
--color-bg-sidebar: #343a40;
--color-bg-form: #fafafa;
--color-bg-dark: #244238;
--color-bg-sidebar: #244238;
--color-text-dark: #151422;
--color-form-label: #151422;
--color-text-light: #f8f9fa;
/* SPACING DIMENSIONS */
--space-xs: 8px;
--space-sm: 12px;
@@ -0,0 +1,33 @@
// Creates TomSelect Selects when loading a page
document.addEventListener("DOMContentLoaded", () => {
new TomSelect("#farm_selection_select", {
create: false,
sortField: {
field: "text",
direction: "asc"
},
controlInput: null,
render: {
option: function(data, escape) {
return `
<div class="option ${data.value == 0 ? 'see-all' : ''}">
${escape(data.text)}
</div>
`;
}
}
});
});
// Creates TomSelect Selects when an htmx swap is detected
document.addEventListener("htmx:afterSwap", function(event) {
const animals = document.querySelector("#id_animals");
if (animals && !animals.tomselect) {
new TomSelect("#id_animals", {
plugins: ['remove_button'],
create: false,
});
}
});
@@ -1,6 +1,30 @@
<form method="POST" action="{{ action_url }}">
<form method="POST" action="{{ action_url }}" class="generic-form">
{% csrf_token %}
{{ form }}
<button class="btn-primary" type="submit">Salvar</button>
<a href="{{ return_url }}" class="btn-primary">Cancelar</a>
{% for field in form %}
<div class="form-group">
{{ field.label_tag }}
{{ field }}
{% if field.help_text %}
<small class="helptext">{{ field.help_text }}</small>
{% endif %}
{{ field.errors }}
</div>
{% endfor %}
<div class="form-actions">
<button class="btn-primary" type="submit">
{% if action_button_text %}
{{ action_button_text }}
{% else %}
Salvar
{% endif %}
</button>
<a href="{{ return_url }}" class="btn-secondary">
Cancelar
</a>
</div>
</form>
@@ -0,0 +1,7 @@
{% extends 'layouts/base_modal.html' %}
{% load static %}
{% block content %}
<h1 class="form-modal-title">{{ modal_title }}</h1>
{% include 'components/form.html' %}
{% endblock %}
@@ -3,18 +3,14 @@
<i class="fa-solid fa-bars" id="sidebar-toggle"></i>
</div>
{% comment %} <div class="navbar-farm-selector w33">
<span class="farm-name"><i class="fa-regular fa-circle-check"></i> {{ SELECTED_FARM.name }}</span>
<a href="{% url 'entities:user:farm_deselect' %}" class="btn-simple">Alterar</a>
</div> {% endcomment %}
<form method="post" action="{% url 'entities:user:farm_set_post' %}" class="navbar-farm-selector w33">
{% csrf_token %}
<input type="hidden" name="next" value="{{ request.path }}">
<select name="farm_id" onchange="this.form.submit()">
<select name="farm_id" id="farm_selection_select" onchange="this.form.submit()">
<option value="0" class="see-all">Ver todas</option>
{% for farm in farms %}
<option value="{{ farm.id }}" {% if farm.id == SELECTED_FARM.id %}selected{% endif %}>
{{ farm.name }}
{{ farm.name|upper }}
</option>
{% endfor %}
</select>
@@ -7,7 +7,7 @@
<li class="sidebar-header">ACOMPANHAMENTO</li>
<li class="{% if request.resolver_match.url_name != 'report' %}highlight-hover{% endif %}">
<a href="{% url 'entities:farm:farm_home' %}" class="sidebar-link {% if request.resolver_match.url_name == 'report' %}active{% endif %}"><i class="fa-solid fa-chart-pie"></i> Relatórios</a>
<a href="{% url 'report:dashboard' %}" class="sidebar-link {% if request.resolver_match.url_name == 'report' %}active{% endif %}"><i class="fa-solid fa-chart-pie"></i> Painel</a>
</li>
<li class="sidebar-header">REGISTROS</li>
@@ -27,12 +27,14 @@
</li>
<li class="{% if request.resolver_match.url_name not in 'paddock_list, paddock_create, paddock_update, paddock_delete' %}highlight-hover{% endif %}">
<a href="{% url 'entities:farm:paddock_list' %}" class="sidebar-link {% if request.resolver_match.url_name in 'paddock_list, paddock_create, paddock_update, paddock_delete' %}active{% endif %}"><i class="fa-solid fa-leaf"></i> Piquetes</a>
<a href="{% url 'entities:farm:paddock_list' %}" class="sidebar-link {% if request.resolver_match.url_name in 'paddock_list, paddock_create, paddock_update, paddock_delete' %}active{% endif %}"><i class="fa-solid fa-seedling"></i> Piquetes</a>
</li>
<!--
<li class="{% if request.resolver_match.url_name not in 'paddock_group_list, paddock_group_create, paddock_group_update, paddock_group_delete' %}highlight-hover{% endif %}">
<a href="{% url 'entities:farm:paddock_group_list' %}" class="sidebar-link {% if request.resolver_match.url_name in 'paddock_group_list, paddock_group_create, paddock_group_update, paddock_group_delete' %}active{% endif %}"><i class="fa-solid fa-layer-group"></i> Blocos</a>
</li>
-->
<li class="sidebar-header">CONFIGURAÇÔES</li>
@@ -41,7 +43,7 @@
</li>
{% if user.is_staff or user.is_superuser %}
<li class="{% if request.resolver_match.url_name not in 'admin' %}highlight-hover{% endif %}">
<a href="{% url 'admin:index' %}" class="sidebar-link"><i class="fa-solid fa-user-tie"></i> Painel Administrativo</a>
<a href="{% url 'admin:index' %}" target="_blank" class="sidebar-link"><i class="fa-solid fa-user-tie"></i> Painel Administrativo</a>
</li>
{% endif %}
</ul>
@@ -15,13 +15,20 @@
<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/layouts/base_modal.css' %}">
<link rel="stylesheet" href="{% static 'core/css/components/confirmation_popup.css' %}">
<link rel="stylesheet" href="{% static 'farm/css/user_authorize.css' %}">
<link rel="stylesheet" href="{% static 'core/css/components/form.css' %}">
<link rel="stylesheet" href="{% static 'core/css/components/form_modal.css' %}">
<link rel="stylesheet" href="{% static 'report/css/request_report.css' %}">
<link rel="stylesheet" href="{% static 'rotation/css/rotation_view_more.css' %}">
{% block extra_css %}{% endblock %}
<!-- Importing Font Awesome -->
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/7.0.1/css/all.min.css" integrity="sha512-2SwdPD6INVrV/lHTZbO2nodKhrnDdJK9/kg2XD1r9uGqPo1cUbujc+IYdlYdEErWNu69gVcYgdxlmVmzTWnetw==" crossorigin="anonymous" referrerpolicy="no-referrer" />
<!-- Importing Tom Select -->
<link href="https://cdn.jsdelivr.net/npm/tom-select@2.6.1/dist/css/tom-select.css" rel="stylesheet">
</head>
<body>
@@ -34,7 +41,11 @@
</body>
<!-- Importing custom JS -->
<script src="{% static 'core/js/main.js' %}"></script>
<script src="{% static 'core/js/tomselect.js' %}"></script>
<!-- Importing HTMX -->
<script src="https://cdn.jsdelivr.net/npm/htmx.org@2.0.8/dist/htmx.min.js" integrity="sha384-/TgkGk7p307TH7EXJDuUlgG3Ce1UVolAOFopFekQkkXihi5u/6OCvVKyz1W+idaz" crossorigin="anonymous"></script>
<!-- Importing Tom Select -->
<script src="https://cdn.jsdelivr.net/npm/tom-select@2.6.1/dist/js/tom-select.complete.min.js"></script>
{% block extra_js %}{% endblock extra_js %}
</html>
@@ -15,16 +15,24 @@
<link rel="stylesheet" href="{% static 'core/css/components/buttons.css' %}">
<link rel="stylesheet" href="{% static 'user/css/farm_select.css' %}">
<!-- Templates based on base_model can't pass their stylesheet on block extra_css -->
<link rel="stylesheet" href="{% static 'core/css/layouts/base_modal.css' %}">
<link rel="stylesheet" href="{% static 'core/css/components/confirmation_popup.css' %}">
<link rel="stylesheet" href="{% static 'farm/css/user_authorize.css' %}">
<link rel="stylesheet" href="{% static 'core/css/components/form.css' %}">
<link rel="stylesheet" href="{% static 'core/css/components/form_modal.css' %}">
<!-- Importing Font Awesome -->
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/7.0.1/css/all.min.css" integrity="sha512-2SwdPD6INVrV/lHTZbO2nodKhrnDdJK9/kg2XD1r9uGqPo1cUbujc+IYdlYdEErWNu69gVcYgdxlmVmzTWnetw==" crossorigin="anonymous" referrerpolicy="no-referrer" />
{% block extra_css %}{% endblock extra_css %}
</head>
<body>
<nav class="farm-select-navbar">
<div class="navbar-brand">
MultiFarm
</div>
<div class="navbar-user-info">
<div class="navbar-user-info-farm-select">
<span class="username">Olá, {{ request.user.short_name }}</span>
<a href="{% url 'entities:user:user_logout' %}"><i class="fa-solid fa-right-from-bracket"></i></a>
</div>
@@ -41,7 +49,6 @@
<div class="farm-item">
<h3 class="farm-name">{{ farm.name }}</h3>
<p class="farm-description">
<span class="total-area">Área total: {{ farm.total_area }}m²</span>
<span class="count-piquetes">Piquetes: {{ farm.total_paddocks }}</span>
<span class="count-animais">Animais: {{ farm.total_animals }}</span>
</p>
@@ -49,13 +56,16 @@
</a>
</li>
{% endfor %}
<p><a hx-get="{% url "entities:farm:farm_create" %}" hx-swap="beforeend" hx-target="body" class="new-farm-btn"><i class="fa-solid fa-plus"></i> Criar nova fazenda</a></p>
</ul>
{% else %}
<div class="no-farms">
<p>Nenhuma fazenda encontrada</p>
<p><a href="#"><i class="fa-solid fa-plus"></i> Criar nova fazenda</a></p>
<p><a hx-get="{% url "entities:farm:farm_create" %}" hx-swap="beforeend" hx-target="body" class="new-farm-btn"><i class="fa-solid fa-plus"></i> Criar nova fazenda</a></p>
</div>
{% endif %}
</div>
</body>
<!-- Importing HTMX -->
<script src="https://cdn.jsdelivr.net/npm/htmx.org@2.0.8/dist/htmx.min.js" integrity="sha384-/TgkGk7p307TH7EXJDuUlgG3Ce1UVolAOFopFekQkkXihi5u/6OCvVKyz1W+idaz" crossorigin="anonymous"></script>
</html>
@@ -16,7 +16,7 @@ def get_attr(obj, attr):
value = value()
# If none, return empty string to avoid displaying "None" in the template
return value if value is not None else ""
return value if value is not None else "-"
except Exception:
return ""
return "- !"
+18 -3
View File
@@ -1,11 +1,12 @@
from django.contrib import admin
from import_export.admin import ImportExportModelAdmin
from .models.user import User
from .models.farm import Farm, PaddockGroup, Paddock
from .models.animal import Animal, BatchType, Batch, BatchAnimal, FeedingRecord
from .models.rotation import Rotation
admin.site.register([
from .models import (
User,
Farm,
PaddockGroup,
@@ -15,5 +16,19 @@ admin.site.register([
BatchAnimal,
FeedingRecord,
Rotation,
BatchType
])
BatchType,
)
class DefaultImportExportAdmin(ImportExportModelAdmin):
pass
admin.site.register(User, DefaultImportExportAdmin)
admin.site.register(Farm, DefaultImportExportAdmin)
admin.site.register(PaddockGroup, DefaultImportExportAdmin)
admin.site.register(Paddock, DefaultImportExportAdmin)
admin.site.register(Animal, DefaultImportExportAdmin)
admin.site.register(Batch, DefaultImportExportAdmin)
admin.site.register(BatchAnimal, DefaultImportExportAdmin)
admin.site.register(FeedingRecord, DefaultImportExportAdmin)
admin.site.register(Rotation, DefaultImportExportAdmin)
admin.site.register(BatchType, DefaultImportExportAdmin)
+5 -6
View File
@@ -4,11 +4,10 @@ from ..models import Farm, PaddockGroup, Paddock
class FarmForm(forms.ModelForm):
class Meta:
model = Farm
fields = ['name', 'total_area']
fields = ['name']
labels = {
'name': 'Nome',
'total_area': 'Área total',
}
FarmCreateForm = FarmEditForm = FarmForm # Alias since its implementations are the same
@@ -26,12 +25,12 @@ PaddockGroupCreateForm = PaddockGroupEditForm = PaddockGroupForm
class PaddockForm(forms.ModelForm):
class Meta:
model = Paddock
fields = ['paddock_number', 'area', 'load_capacity', 'paddock_group']
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',
'area': 'Área (m²)',
# 'load_capacity': 'Lotação máxima',
# 'paddock_group': 'Bloco',
}
PaddockCreateForm = PaddockEditForm = PaddockForm
+10 -10
View File
@@ -4,20 +4,20 @@ from ..models import Rotation
class RotationEntryForm(forms.ModelForm):
class Meta:
model = Rotation
fields = ['batch', 'entry_height', 'paddock']
fields = ['batch', 'paddock', 'entry_date']
labels = {
'batch': 'Lote de animais',
'entry_height': 'Altura do pasto',
'paddock': 'Piquete',
'entry_date': 'Horário da entrada'
}
class RotationExitForm(forms.ModelForm):
class Meta:
model = Rotation
fields = ['batch', 'exit_height']
labels = {
'batch': 'Lote de animais',
'exit_height': 'Altura do pasto',
widgets = {
'entry_date': forms.DateTimeInput(
attrs={
'type': 'datetime-local',
'class': 'form-control'
},
format='%Y-%m-%dT%H:%M'
),
}
@@ -0,0 +1,23 @@
# Generated by Django 6.0.2 on 2026-06-08 16:05
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('entities', '0003_farm_default_consumption_farm_summer_consumption_and_more'),
]
operations = [
migrations.AddField(
model_name='farm',
name='ideal_paddock_occupation',
field=models.IntegerField(default=2),
),
migrations.AddField(
model_name='farm',
name='ideal_paddock_rest',
field=models.IntegerField(default=20),
),
]
@@ -0,0 +1,18 @@
# Generated by Django 6.0.2 on 2026-06-10 15:22
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('entities', '0004_farm_ideal_paddock_occupation_and_more'),
]
operations = [
migrations.AlterField(
model_name='farm',
name='default_consumption',
field=models.DecimalField(decimal_places=2, default=2.5, max_digits=4),
),
]
@@ -0,0 +1,17 @@
# Generated by Django 6.0.2 on 2026-06-21 20:29
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('entities', '0005_alter_farm_default_consumption'),
]
operations = [
migrations.RemoveField(
model_name='farm',
name='total_area',
),
]
@@ -0,0 +1,19 @@
# Generated by Django 6.0.2 on 2026-06-23 00:19
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('entities', '0006_remove_farm_total_area'),
]
operations = [
migrations.AlterField(
model_name='rotation',
name='batch',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='rotations', to='entities.batch'),
),
]
@@ -0,0 +1,18 @@
# Generated by Django 6.0.2 on 2026-06-23 00:23
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('entities', '0007_alter_rotation_batch'),
]
operations = [
migrations.AlterField(
model_name='farm',
name='default_consumption',
field=models.DecimalField(decimal_places=2, default=2.75, max_digits=4),
),
]
+29 -7
View File
@@ -1,6 +1,7 @@
from datetime import date
from django.db import models
from django.db.models import Q
from django.db.models import Q, Sum
from decimal import Decimal
class Animal(models.Model):
is_active = models.BooleanField(default=True)
@@ -10,7 +11,7 @@ class Animal(models.Model):
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}"
return f"{self.identifier}"
@property
def age(self):
@@ -56,12 +57,14 @@ class BatchType(models.Model):
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}"
return f"{self.name} ({self.consumption_percentage}%)"
@property
def consumption_percentage(self):
return int((self.consumption_factor - 1) * 100)
value = int((self.consumption_factor - 1) * 100)
if value >= 0:
return f'+{value}'
return f'{value}'
class Batch(models.Model):
is_active = models.BooleanField(default=True)
@@ -87,6 +90,15 @@ class Batch(models.Model):
self.batchanimal_set.all().update(is_active=True)
self.__original_is_active = self.is_active
@property
def total_ugm(self):
if self.animals:
total_weight = self.animals.aggregate(
total=Sum("weight")
)["total"] or 0
return Decimal(f"{total_weight / 500:.2f}")
def __str__(self):
return self.identifier
@@ -112,20 +124,30 @@ class BatchAnimal(models.Model):
def __str__(self):
return f"{self.animal.identifier} in {self.batch.identifier}"
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.__original_is_active = self.is_active
def save(self, *args, **kwargs):
is_active_changed = self.__original_is_active != self.is_active
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)
self.__original_is_active = self.is_active
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"
return f"{self.animal.identifier} - {self.feed_quantity}kg"
+44 -9
View File
@@ -1,12 +1,16 @@
from django.db import models
from django.db.models import Sum
from django.apps import apps
from django.utils import timezone
class Farm(models.Model):
is_active = models.BooleanField(default=True)
name = models.CharField(max_length=45)
total_area = models.IntegerField(null=True, blank=True)
default_consumption = models.DecimalField(max_digits=4, decimal_places=2, default=1.00)
default_consumption = models.DecimalField(max_digits=4, decimal_places=2, default=2.75)
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)
ideal_paddock_occupation = models.IntegerField(default=2)
ideal_paddock_rest = models.IntegerField(default=20)
def __str__(self):
@@ -14,14 +18,40 @@ class Farm(models.Model):
@property
def animal_count(self):
return self.animals.filter(
is_active=True
).count()
return self.animals.filter(is_active=True).count()
@property
def paddock_count(self):
return self.paddocks.filter(
return self.paddocks.filter(is_active=True).count()
@property
def total_ugm(self):
total_weight = (
self.animals
.filter(is_active=True)
.aggregate(total=Sum('weight'))['total']
or 0
)
return round(total_weight / 500, 2)
@property
def batch_count(self):
Batch = apps.get_model('entities', 'Batch')
return Batch.objects.filter(
animals__farm=self,
is_active=True
).distinct().count()
@property
def paddocks_area(self):
return sum(paddock.area for paddock in self.paddocks.filter(is_active=True))
@property
def total_rotations(self):
Rotation = apps.get_model('entities', 'Rotation')
return Rotation.objects.filter(
is_active=True,
paddock__farm=self
).count()
@@ -31,7 +61,7 @@ class PaddockGroup(models.Model):
description = models.TextField(null=True, blank=True)
def __str__(self):
return f"Bloco {self.id}"
return f"{self.id}"
class Paddock(models.Model):
@@ -44,10 +74,15 @@ class Paddock(models.Model):
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}"
return f"{self.paddock_number}"
@property
def occupied_as_text(self):
if self.occupied:
return "Sim"
return "Não"
return "Não"
# @property
# def is_occupied(self):
# from . import Rotation
# return Rotation.objects.filter(paddock=self, is_active=True).exists()
+37 -14
View File
@@ -1,5 +1,6 @@
from django.db import models
from django.utils import timezone
from decimal import Decimal
class Rotation(models.Model):
is_active = models.BooleanField(default=True)
@@ -9,28 +10,50 @@ class Rotation(models.Model):
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)
batch = models.ForeignKey('entities.Batch', on_delete=models.PROTECT, 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}"
return f"Movimentação {self.id}"
@property
def duration(self):
end = self.exit_date or timezone.now()
return end - self.entry_date
delta = end - self.entry_date
return Decimal(str(round(delta.total_seconds() / 86400, 2)))
@property
def duration_display_day(self):
end = self.exit_date or timezone.now()
delta = end - self.entry_date
days = self.duration
if days < 1:
return "menos de um"
days = delta.days
if days == 0:
return "Hoje"
if days == 1:
return "1"
return f"{days}"
string_day = f"{days}".replace('.', ',')
return string_day
@property
def livestock_unit_day(self):
if self.duration == 0:
return 0
return Decimal(self.livestock_unit * self.duration)
@property
def animal_count_day(self):
if self.duration == 0:
return 0
return Decimal(self.animal_count * self.duration)
@property
def season(self):
month = self.exit_date.month
day = self.exit_date.day
if (month == 12 and day >= 21) or (month in [1, 2]) or (month == 3 and day < 20):
return "Summer"
elif (month == 3 and day >= 20) or (month in [4, 5]) or (month == 6 and day < 21):
return "Fall"
elif (month == 6 and day >= 21) or (month in [7, 8]) or (month == 9 and day < 22):
return "Winter"
else:
return "Spring"
+6 -2
View File
@@ -54,6 +54,7 @@ def get_animal_create_context():
context['form'] = form
context['action_url'] = reverse('entities:animal:animal_create')
context['return_url'] = reverse('entities:animal:animal_list')
context['modal_title'] = 'Cadastrar animal'
return context
@@ -82,6 +83,7 @@ def get_animal_update_context(request, animal_id):
)
context['return_url'] = reverse('entities:animal:animal_list')
context['form'] = form
context['modal_title'] = 'Editar animal'
return context
@@ -120,7 +122,8 @@ def get_batch_create_context(request):
'title': 'Novo Lote',
'form': form,
'action_url': reverse('entities:animal:batch_create'),
'return_url': reverse('entities:animal:batch_list')
'return_url': reverse('entities:animal:batch_list'),
'modal_title': 'Cadastrar lote'
}
return context
@@ -153,7 +156,8 @@ def get_batch_update_context(request, batch_id):
'entities:animal:batch_update',
kwargs={'batch_id': batch_old.pk}
),
'form': form
'form': form,
'modal_title': 'Editar lote'
}
return context
+7 -2
View File
@@ -70,7 +70,7 @@ def get_farm_home_context(request):
batch_types = _get_farms_batch_types(request)
context = {}
context['title'] = selected_farm.name
context['title'] = 'Configurações da Fazenda'
context['selected_farm'] = selected_farm
context['farms'] = farms_with_counts
context['allowed_users'] = allowed_users
@@ -86,6 +86,7 @@ def get_farm_create_context():
context['form'] = form
context['action_url'] = reverse('entities:farm:farm_create')
context['return_url'] = reverse('entities:farm:farm_home')
context['modal_title'] = 'Criar fazenda'
return context
@@ -143,15 +144,19 @@ def delete_farm(request, farm_id):
farm.is_active = False
farm.save()
def update_consumption(request):
def update_metrics(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'))
ideal_paddock_occupation = int(request.POST.get('ideal_paddock_occupation'))
ideal_paddock_rest = int(request.POST.get('ideal_paddock_rest'))
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.ideal_paddock_occupation = ideal_paddock_occupation
selected_farm.ideal_paddock_rest = ideal_paddock_rest
selected_farm.save()
@@ -0,0 +1,73 @@
from django.shortcuts import get_object_or_404
from django.urls import reverse
from django.db.models import Max, Q, F, Exists, OuterRef
from django.utils.safestring import mark_safe
from django.utils import timezone
from entities.models import Farm, Rotation, Paddock, Batch
from entities.forms import RotationEntryForm
from report.services import _get_rotation_consumption
def _get_selected_farm_id(request):
return request.session['selected_farm_id']
def _get_selected_farm(request):
return Farm.objects.get(id=_get_selected_farm_id(request))
def _get_rotation_by_id(request, rotation_id):
farm = _get_selected_farm(request)
return get_object_or_404(Rotation, pk=rotation_id, paddock__farm=farm)
def _get_batches(farm):
return (Batch.objects
.filter(animals__farm=farm, is_active=True)
.annotate(
in_rotation=Exists(
Rotation.objects.filter(
batch=OuterRef("pk"),
exit_date__isnull=True
)
)
)
.distinct()
)
def _get_free_paddocks(farm):
now = timezone.now()
paddocks = (
Paddock.objects
.filter(farm=farm, occupied=False)
.annotate(last_exit=Max("rotations__exit_date"))
.order_by(F("last_exit").asc(nulls_first=True))
)
for paddock in paddocks:
paddock.current_rest = (now - paddock.last_exit).days if paddock.last_exit else None
return paddocks
def get_rotation_create_context(request):
farm = _get_selected_farm(request)
form = RotationEntryForm()
form.fields['batch'].queryset = _get_batches(farm)
form.fields['batch'].empty_label = "Selecione o lote"
form.fields['paddock'].queryset = _get_free_paddocks(farm)
form.fields['paddock'].empty_label = "Selecione o piquete"
return {
'form': form,
'modal_title': 'Iniciar movimentação',
'action_button_text': mark_safe("""<i class="fa-solid fa-arrow-rotate-right"></i> Movimentar""")
}
def get_view_more_context(request, rotation_id):
rotation = _get_rotation_by_id(request, rotation_id)
consume = _get_rotation_consumption(rotation, _get_selected_farm(request))
free_paddocks = _get_free_paddocks(request)
return {
'rotation': rotation,
'consume': consume,
'free_paddocks': free_paddocks
}
+6 -1
View File
@@ -72,6 +72,11 @@ def farm_set(request, farm_id=None):
farm_id = farm_id or request.POST.get("farm_id")
next_url = request.POST.get("next")
print(farm_id)
if farm_id == '0':
return farm_deselect(request)
if not farm_id:
return redirect('entities:user:farm_select')
@@ -79,7 +84,7 @@ def farm_set(request, farm_id=None):
farm = request.user.farms.get(id=farm_id)
request.session['selected_farm_id'] = farm.id
return redirect(next_url or 'entities:farm:farm_home')
return redirect(next_url or 'report:dashboard')
except request.user.farms.model.DoesNotExist:
return redirect('entities:user:farm_select')
@@ -10,31 +10,6 @@
color: #6c757d;
}
.header-right {
display: flex;
gap: 12px;
}
.farm-info {
min-width: 100px;
padding: 14px;
text-align: center;
background: white;
border: 1px solid #e9ecef;
border-radius: 10px;
box-shadow: 0 2px 6px rgba(0,0,0,.04);
}
.farm-info p:first-child {
font-size: .8rem;
color: #6c757d;
}
.farm-info p:last-child {
font-size: 1.2rem;
font-weight: 600;
}
/* Containers */
.farm-config-container,
.batch-type-config-container,
@@ -51,7 +26,8 @@
/* Headers */
.allowed-users-header,
.batch-type-config-header,
.section-header {
.section-header,
.available-farms-header {
display: flex;
justify-content: space-between;
align-items: center;
@@ -66,7 +42,8 @@
}
.allowed-users-header a,
.batch-type-config-header a {
.batch-type-config-header a,
.available-farms-header a {
display: flex;
align-items: center;
gap: 6px;
@@ -77,6 +54,8 @@
background-color: #198754;
border-radius: 5px;
padding: 3px 6px;
width: 120px;
justify-content: center;
}
/* Consumptions */
@@ -93,11 +72,19 @@
}
.config-card-title {
margin-bottom: 8px;
font-size: .85rem;
display: flex;
justify-content: space-between;
align-items: center;
padding: 0 3px;
margin-bottom: 5px;
font-size: 14px;
color: #6c757d;
}
.metric-question {
cursor: pointer;
}
.config-card input {
width: 100%;
padding: 10px;
@@ -112,12 +99,12 @@
}
.config-submit {
flex: 1;
flex: .5;
display: flex;
}
.config-submit button {
width: 50%;
width: 100%;
height: 50%;
margin: auto;
border: none;
@@ -196,7 +183,7 @@
gap: 14px;
}
.available-farms-title {
.available-farms-header {
grid-column: 1 / -1;
}
@@ -40,14 +40,14 @@
padding: 0 var(--space-sm);
border: none;
border-radius: var(--border-radius-sm);
background: var(--color-primary);
background: var(--color-neutral-btn);
color: var(--color-text-light);
cursor: pointer;
transition: background var(--transition-speed);
}
.entity_search form button:hover {
background: var(--color-secondary);
background: var(--color-neutral-btn-hover);
}
/* CLEAR FILTER */
@@ -57,16 +57,15 @@
gap: 4px;
height: 32px;
padding: 0 var(--space-sm);
background: var(--color-danger-light);
color: var(--color-danger);
background: var(--color-bg-filter);
color: var(--color-text-filter);
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);
background: var(--color-danger-btn-hover);
}
/* NEW BUTTON */
@@ -75,7 +74,7 @@
user-select: none;
height: 32px;
padding: 0 var(--space-md);
background: var(--color-success);
background: var(--color-plus-btn);
color: var(--color-text-light);
border-radius: var(--border-radius-sm);
text-decoration: none;
@@ -87,7 +86,7 @@
}
.new-entity-button:hover {
background: var(--color-success);
background: var(--color-plus-btn-hover);
opacity: 0.85;
}
@@ -9,8 +9,7 @@
/* Active rotation cards */
.active-rotation-card {
min-width: 300px;
max-width: 300px;
width: 300px;
background: #ffffff;
color: #212529;
border: 1px solid #e9ecef;
@@ -23,9 +22,15 @@
transition: 0.2s ease;
}
.active-rotation-card {
user-select: none;
}
.active-rotation-card:hover {
transform: translateY(-5px);
box-shadow: 0 4px 10px rgba(0, 0, 0, 0.08);
border-color: var(--color-primary);
cursor: pointer;
}
.active-rotation-card p {
@@ -68,4 +73,8 @@
align-items: center;
}
.active-rotation-card {
width: 100%;
}
}
@@ -0,0 +1,176 @@
.rotation-view-more {
display: flex;
flex-direction: column;
gap: 28px;
}
.rotation-view-more-rotation-status {
text-align: center;
}
.rotation-view-more-rotation-status h2 {
margin-top: 8px;
color: var(--color-primary);
}
.rotation-view-more-status-badge {
display: inline-block;
padding: 6px 13px;
border-radius: 999px;
background: rgba(46, 204, 113, .15);
color: #27ae60;
font-size: 13px;
font-weight: 600;
}
.rotation-view-more-rotation-cards {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 16px;
}
.rotation-view-more-card {
padding: 4px;
border-radius: 12px;
border: 1px solid var(--color-secondary);
text-align: center;
background: white;
}
.rotation-view-more-label {
display: block;
color: #777;
font-size: 13px;
margin-bottom: 6px;
}
.rotation-view-more-card strong {
font-size: 22px;
color: var(--color-primary);
}
.rotation-view-more-rotation-section h3,
.rotation-view-more-rotation-next h3 {
margin-bottom: 16px;
color: var(--color-primary);
}
.rotation-view-more-info-grid {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 16px;
}
.rotation-view-more-info {
display: flex;
flex-direction: column;
padding: 14px;
border: 1px solid #e5e5e5;
border-radius: 10px;
background: #fafafa;
}
.rotation-view-more-info span {
font-size: 13px;
color: #888;
margin-bottom: 5px;
}
.rotation-view-more-info strong {
color: var(--color-primary);
}
.rotation-view-more-rotation-next {
display: flex;
flex-direction: column;
gap: 12px;
padding-top: 8px;
}
.rotation-view-more-rotation-next h3 {
display: flex;
align-items: center;
gap: 8px;
margin: 0;
}
.rotation-view-more-rotation-next .rotation-next-actions {
display: grid;
grid-template-columns: 1fr auto auto;
gap: 12px;
align-items: center;
}
.rotation-view-more-rotation-next select {
height: 44px;
width: 100%;
padding: 0 16px;
border-radius: 8px;
border: 1px solid #ddd;
background: white;
font-size: 15px;
color: #444;
outline: none;
cursor: pointer;
}
.rotation-view-more-rotation-next select:focus {
border-color: var(--color-primary);
}
.rotation-view-more-rotate,
.rotation-view-finish-rotate {
height: 44px;
display: flex;
align-items: center;
justify-content: center;
gap: 8px;
padding: 0 20px;
border-radius: 8px;
font-weight: 600;
cursor: pointer;
transition: .2s;
white-space: nowrap;
}
.rotation-view-more-rotate {
border: none;
background: var(--color-primary);
color: white;
}
.rotation-view-more-rotate:hover {
filter: brightness(.95);
}
.rotation-view-finish-rotate {
border: 1px solid var(--color-secondary);
background: white;
color: var(--color-secondary);
}
.rotation-view-finish-rotate:hover {
background: var(--color-secondary);
color: white;
}
@media (max-width: 700px) {
.rotation-view-more-rotation-cards {
grid-template-columns: 1fr;
}
.rotation-view-more-info-grid {
grid-template-columns: 1fr;
}
.rotation-view-more-rotation-next .rotation-next-actions {
grid-template-columns: 1fr;
}
.rotation-view-more-rotation-next select,
.rotation-view-more-rotate,
.rotation-view-finish-rotate {
width: 100%;
}
}
@@ -39,4 +39,14 @@ document.addEventListener("DOMContentLoaded", () => {
isAnimating = false;
}
}
});
});
document.addEventListener("DOMContentLoaded", function () {
const params = new URLSearchParams(window.location.search);
if (params.has("page")) {
document.querySelector("#table").scrollIntoView({
behavior: "auto"
});
}
});
@@ -19,7 +19,7 @@ body {
justify-content: space-between;
align-items: center;
padding: 0 var(--space-lg);
z-index: 950;
z-index: 20;
}
/* SITE'S TITLE */
@@ -43,7 +43,7 @@ body {
.farm-select-container h1 {
font-size: var(--font-xl);
color: #1f2937;
color: var(--login-background-primary);
margin-bottom: var(--space-xl);
}
@@ -127,7 +127,7 @@ body {
}
/* NEW FARM BUTTON */
.no-farms a {
.new-farm-btn {
display: inline-flex;
align-items: center;
gap: var(--space-xs);
@@ -141,7 +141,7 @@ body {
transition: background-color 0.2s;
}
.no-farms a:hover {
.new-farm-btn:hover {
background-color: var(--color-new-farm-hover);
}
@@ -2,9 +2,11 @@
{% load static %}
{% block extra_css %}
{{ block.super }}
<link rel="stylesheet" href="{% static 'farm/css/home.css' %}">
{% endblock %}
{% block content %}
New - Farm
Nova Fazenda <br>
{% include 'components/form.html' %}
{% endblock %}
@@ -10,40 +10,34 @@
{% block content %}
<div class="header">
<div class="header-left">
<h1 class="title">{{ title }}</h1>
<h1 class="title">{{ selected_farm.name }}</h1>
<p>Configurações da fazenda</p>
</div>
<div class="header-right">
<div class="farm-info">
<p>Animais</p>
<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">
<h2 class="farm-config-title">Métricas da fazenda</h2>
<form action="{% url "entities:farm:metrics_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 class="config-card" title="Percentual do peso do animal correspondente ao seu consumo diário">
<p class="config-card-title">Consumo padrão <i class="fa-solid fa-circle-question metric-question"></i></p>
<input type="number" name="default-consume" min="0.01" step="0.01" value="{{ selected_farm.default_consumption|unlocalize }}" required>
</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 class="config-card" title="Variação do consumo no verão (1,00 = sem variação)">
<p class="config-card-title">Consumo no verão <i class="fa-solid fa-circle-question metric-question"></i></p>
<input type="number" name="summer-consume" min="0.01" step="0.01" value="{{ selected_farm.summer_consumption|unlocalize }}" required>
</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 class="config-card" title="Variação do consumo no inverno (1,00 = sem variação)">
<p class="config-card-title">Consumo no inverno <i class="fa-solid fa-circle-question metric-question"></i></p>
<input type="number" name="winter-consume" min="0.01" step="0.01" value="{{ selected_farm.winter_consumption|unlocalize }}" required>
</div>
<div class="config-card" title="Tempo ideal de ocupação em um piquete em dias">
<p class="config-card-title">Tempo de ocupação <i class="fa-solid fa-circle-question metric-question"></i></p>
<input type="number" name="ideal_paddock_occupation" min="1" step="1" value="{{ selected_farm.ideal_paddock_occupation|unlocalize }}" required>
</div>
<div class="config-card" title="Tempo ideal de descanço para uma piquete em dias">
<p class="config-card-title">Tempo de repouso <i class="fa-solid fa-circle-question metric-question"></i></p>
<input type="number" name="ideal_paddock_rest" min="1" step="1" value="{{ selected_farm.ideal_paddock_rest|unlocalize }}" required>
</div>
<div class="config-submit">
<button type="submit">Salvar</button>
@@ -54,8 +48,11 @@
<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>
<a hx-get="{% url "entities:animal:batchtype_create" %}" hx-swap="beforeend" hx-target="body"><i class="fa-solid fa-plus"></i> Tipo de Lote</a>
</div>
{% if batch_types|length < 1 %}
<p class="type-consumption-factor">Nenhum tipo de lote foi cadastrado ainda</p>
{% endif %}
{% for type in batch_types %}
<div class="batch-type-card">
<div class="batch-type-left">
@@ -87,7 +84,10 @@
</div>
<div class="available-farms-container">
<h2 class="available-farms-title">Suas fazendas</h2>
<div class="available-farms-header">
<h2 class="available-farms-title">Suas fazendas</h2>
<a hx-get="{% url "entities:farm:farm_create" %}" hx-swap="beforeend" hx-target="body"><i class="fa-solid fa-plus"></i> Fazenda</a>
</div>
{% for farm in farms %}
<div class="available-farm-card">
<p class="available-farm-name">{{ farm.name }}</p>
@@ -0,0 +1,11 @@
{% extends 'layouts/base_modal.html' %}
{% load static %}
{% block extra_css %}
<link rel="stylesheet" href="{% static 'layouts/entity_create.css' %}">
{% endblock %}
{% block content %}
<h1 class="entity-modal-title">{{ modal_title }}</h1>
{% include 'components/form.html' %}
{% endblock %}
@@ -27,6 +27,7 @@
<button type="submit">
<i class="fa-solid fa-magnifying-glass"></i>
</button>
<!-- New -->
<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>
</div>
@@ -0,0 +1,11 @@
{% extends 'layouts/base_modal.html' %}
{% load static %}
{% block extra_css %}
<link rel="stylesheet" href="{% static 'layouts/entity_update.css' %}">
{% endblock %}
{% block content %}
<h1 class="entity-modal-title">{{ modal_title }}</h1>
{% include 'components/form.html' %}
{% endblock %}
@@ -0,0 +1 @@
MOVE TEMPLATE
@@ -12,13 +12,13 @@
<p>Movimentações em aberto</p>
<div class="open-rotations-container">
{% for obj in open_rotations %}
<div class="active-rotation-card">
<div class="active-rotation-card" hx-get="{% url 'entities:rotation:view_more' obj.id %}" hx-swap="beforeend" hx-target="body">
<div class="identification">
<p>Lote {{ obj.batch.identifier }} em piquete {{ obj.paddock.paddock_number }}</p>
<p><i class="fa-solid fa-cow"></i> Lote {{ obj.batch.identifier }} em piquete {{ obj.paddock.paddock_number }}</p>
</div>
<div class="duration">
<p>Há {{ obj.duration_display_day }} dias, desde {{ obj.entry_date|date:"d/m/Y" }}</p>
<p><i class="fa-solid fa-arrow-rotate-right"></i> Há {{ obj.duration_display_day }} dias, desde {{ obj.entry_date|date:"d/m/Y" }}</p>
</div>
<div class="animals">
@@ -39,12 +39,12 @@
{% endblock %}
{% block table %}
<table>
<table id="table">
<tr>
{% for col in columns %}
<th>{{ col.label }}</th>
{% endfor %}
<th>Excluir</th>
{% comment %} <th>Excluir</th> {% endcomment %}
</tr>
{% for obj in object_list %}
@@ -56,14 +56,14 @@
{% endfor %}
<!-- Delete -->
<td>
{% comment %} <td>
<form method="POST" action="{% url delete_url obj.id %}">
{% csrf_token %}
<button type="submit" class="btn-icon-trash">
<i class="fa-solid fa-trash"></i>
</button>
</form>
</td>
</td> {% endcomment %}
</tr>
{% endfor %}
</table>
@@ -0,0 +1,101 @@
{% extends 'layouts/base_modal.html' %}
{% load static %}
{% block content %}
<div class="rotation-view-more">
<div class="rotation-view-more-rotation-status">
<span class="rotation-view-more-status-badge">● Em andamento</span>
<h2>Movimentação #{{ rotation.id }}</h2>
</div>
<div class="rotation-view-more-rotation-cards">
<div class="rotation-view-more-card">
<span class="rotation-view-more-label">Tempo no piquete</span>
<strong>{{ rotation.duration_display_day }} dias</strong>
</div>
<div class="rotation-view-more-card">
<span class="rotation-view-more-label">UGM-dia</span>
<strong>{{ rotation.livestock_unit_day }}</strong>
</div>
<div class="rotation-view-more-card">
<span class="rotation-view-more-label">Animais-dia</span>
<strong>{{ rotation.animal_count_day }}</strong>
</div>
</div>
<div class="rotation-view-more-rotation-section">
<h3>Informações</h3>
<div class="rotation-view-more-info-grid">
<div class="rotation-view-more-info">
<span>Lote</span>
<strong>{{ rotation.batch|default:"—" }}</strong>
</div>
<div class="rotation-view-more-info">
<span>Piquete</span>
<strong>{{ rotation.paddock }}</strong>
</div>
<div class="rotation-view-more-info">
<span>Entrada</span>
<strong>{{ rotation.entry_date|date:"d/m/Y H:i" }}</strong>
</div>
<div class="rotation-view-more-info">
<span>Animais</span>
<strong>{{ rotation.animal_count }}</strong>
</div>
<div class="rotation-view-more-info">
<span>UGM</span>
<strong>{{ rotation.livestock_unit }}</strong>
</div>
<div class="rotation-view-more-info">
<span>Consumo</span>
<strong>{{ consume }} Kg</strong>
</div>
</div>
</div>
<div class="rotation-view-more-rotation-next">
<h3>
<i class="fa-solid fa-rotate"></i>
Rotacionar lote
</h3>
<div class="rotation-next-actions">
{% if free_paddocks|length > 0 %}
<select name="next_paddock">
<option selected disabled>Selecione o próximo piquete</option>
{% for paddock in free_paddocks %}
<option value="{{ paddock.id }}">{{ paddock.paddock_number }} | {% if paddock.current_rest %}Descanço: {{ paddock.current_rest }} dias {% else %}nunca utilizado{% endif %}</option>
{% endfor %}
</select>
<button class="rotation-view-more-rotate">
<i class="fa-solid fa-rotate"></i>
Dar andamento
</button>
{% endif %}
<button class="rotation-view-finish-rotate">
<i class="fa-solid fa-clipboard-check"></i>
Encerrar movimentação
</button>
</div>
</div>
</div>
{% endblock %}
+1 -1
View File
@@ -17,7 +17,7 @@ urlpatterns = [
path('create/', views.farm_create, name='farm_create'),
path('update/<int:farm_id>', views.farm_update, name='farm_update'),
path('delete/<int:farm_id>', views.farm_delete, name='farm_delete'),
path('consumption/update', views.consumption_update, name='consumption_update'),
path('metrics/update', views.metrics_update, name='metrics_update'),
path('paddock/create/', views.paddock_create, name='paddock_create'),
path('paddock/update/<int:paddock_id>', views.paddock_update, name='paddock_update'),
+2 -2
View File
@@ -7,7 +7,7 @@ app_name = 'rotation'
urlpatterns = [
path('', RotationListView.as_view(), name='rotation_home'),
path('create/', views.rotation_create, name='rotation_create'),
path('delete/<int:rotation_id>', views.rotation_delete, name='rotation_delete'),
# path('delete/<int:rotation_id>', views.rotation_delete, name='rotation_delete'),
path('more/<int:rotation_id>', views.rotation_view_more, name='view_more'),
]
+12 -12
View File
@@ -9,26 +9,26 @@ from ..services import animal as services
def animal_create(request):
context = services.get_animal_create_context()
if request.method == 'GET':
return render(request, 'animal/animal_create.html', context)
return render(request, 'components/form_modal.html', context)
if request.method == 'POST':
if services.create_animal(request):
return redirect('entities:animal:animal_list')
context['error'] = "Não foi possível registrar o animal"
return render(request, 'animal/animal_create.html', context)
return render(request, 'components/form_modal.html', context)
@farm_required
def animal_update(request, animal_id):
context = services.get_animal_update_context(request, animal_id)
if request.method == 'GET':
return render(request, 'animal/animal_update.html', context)
return render(request, 'components/form_modal.html', context)
if request.method == 'POST':
if services.update_animal(request, animal_id):
return redirect('entities:animal:animal_list')
context['error'] = "Não foi possível editar o animal"
return render(request, 'animal/animal_update.html', context)
return render(request, 'components/form_modal.html', context)
@farm_required
@@ -48,26 +48,26 @@ def animal_delete(request, animal_id):
def batch_create(request):
context = services.get_batch_create_context(request)
if request.method == 'GET':
return render(request, 'batch/batch_create.html', context)
return render(request, 'components/form_modal.html', context)
if request.method == 'POST':
if services.create_batch(request):
return redirect('entities:animal:batch_list')
context['error'] = "Não foi possível criar o lote"
return render(request, 'batch/batch_create.html', context)
return render(request, 'components/form_modal.html', context)
@farm_required
def batch_update(request, batch_id):
context = services.get_batch_update_context(request, batch_id)
if request.method == 'GET':
return render(request, 'batch/batch_update.html', context)
return render(request, 'components/form_modal.html', context)
if request.method == 'POST':
if services.update_batch(request, batch_id):
return redirect('entities:animal:batch_list')
context['error'] = "Não foi possível editar o lote"
return render(request, 'batch/batch_update.html', context)
return render(request, 'components/form_modal.html', context)
@farm_required
@@ -86,25 +86,25 @@ def batch_delete(request, batch_id):
def batchtype_create(request):
context = services.get_batchtype_create_context(request)
if request.method == 'GET':
return render(request, 'batch/batchtype_create.html', context)
return render(request, 'components/form_modal.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)
return render(request, 'components/form_modal.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)
return render(request, 'components/form_modal.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)
return render(request, 'components/form_modal.html', context)
@farm_required
def batchtype_delete(request, batchtype_id):
+6 -6
View File
@@ -9,17 +9,16 @@ def farm_home(request):
return render(request, 'farm/farm_home.html', context)
@farm_required
def farm_create(request):
context = services.get_farm_create_context()
if request.method == 'GET':
return render(request, 'farm/farm_create.html', context)
return render(request, 'components/form_modal.html', context)
if request.method == 'POST':
if services.create_farm(request):
return redirect('entities:farm:farm_home')
context['error'] = "Não foi possível criar a fazenda"
return render(request, 'farm/farm_create.html', context)
return render(request, 'components/form_modal.html', context)
@farm_required
def farm_update(request, farm_id):
@@ -42,11 +41,12 @@ def farm_delete(request, farm_id):
if request.method == 'POST':
services.delete_farm(request, farm_id)
return redirect('entities:farm:farm_create')
return redirect('entities:farm:farm_home')
def consumption_update(request):
@farm_required
def metrics_update(request):
if request.method=='POST':
services.update_consumption(request)
services.update_metrics(request)
return redirect('entities:farm:farm_home')
@@ -9,8 +9,9 @@ class BatchListView(BaseListView):
columns = [
{"field": "identifier", "label": "Identificador"},
{"field": "animal_cathgory", "label": "Categoria"},
{"field": "animal_category", "label": "Categoria"},
{"field": "batch_type", "label": "Tipo"},
{"field": "total_ugm", "label": "UGM"},
]
create_url = "entities:animal:batch_create"
@@ -9,8 +9,8 @@ class PaddockListView(BaseListView):
columns = [
{"field": "paddock_number", "label": "Número"},
{"field": "occupied_as_text", "label": "Ocupado"},
{"field": "area", "label": "Área"},
{"field": "load_capacity", "label": "Capacidade"},
{"field": "area", "label": "Área (m²)"},
# {"field": "load_capacity", "label": "Capacidade"},
]
create_url = "entities:farm:paddock_create"
@@ -24,11 +24,11 @@ class RotationListView(BaseListView):
details_url = "entities:rotation:rotation_home" # Trocar pela url de detalhe
filter_fields = ["id", "batch", "paddock"]
order_by = ["id"] # Orders from oldest to newest
order_by = ["-id"]
def get_queryset(self):
queryset = super().get_queryset()
self.open_rotations = queryset.filter(exit_date__isnull=True, paddock__farm=self.selected_farm)
self.open_rotations = queryset.filter(exit_date__isnull=True, is_active=True, paddock__farm=self.selected_farm)
return super().get_queryset().filter(is_active=True, paddock__farm=self.selected_farm)
+10 -6
View File
@@ -1,14 +1,18 @@
from django.shortcuts import render
from core.decorators import farm_required
@farm_required
def rotation_home(request):
return render(request, 'rotation/rotation_home.html')
from ..services import rotation as services
@farm_required
def rotation_create(request):
return render(request, 'rotation/rotation_create.html')
context = services.get_rotation_create_context(request)
return render(request, 'components/form_modal.html', context)
# @farm_required
# def rotation_delete(request):
# return render(request, 'rotation/rotation_delete.html')
@farm_required
def rotation_delete(request):
return render(request, 'rotation/rotation_delete.html')
def rotation_view_more(request, rotation_id):
context = services.get_view_more_context(request, rotation_id)
return render(request, 'rotation/rotation_view_more.html', context)
+12
View File
@@ -0,0 +1,12 @@
from django import forms
class ReportForm(forms.Form):
start_date = forms.DateField(
label='Data Inicial',
widget=forms.DateInput(attrs={'type': 'date'})
)
end_date = forms.DateField(
label='Data Final',
widget=forms.DateInput(attrs={'type': 'date'})
)
+40
View File
@@ -0,0 +1,40 @@
from django.contrib.staticfiles import finders
from io import BytesIO
from django.http import HttpResponse
from django.template.loader import get_template
from xhtml2pdf import pisa
def render_report_pdf(template_name, context, filename='relatorio.pdf'):
css_path = finders.find('report/css/report.css')
with open(css_path, encoding='utf-8') as f:
context['pdf_css'] = f.read()
template = get_template(template_name)
html = template.render(context)
pdf_buffer = BytesIO()
result = pisa.CreatePDF(
html,
dest=pdf_buffer,
encoding='utf-8'
)
if result.err:
return HttpResponse(
'Erro ao gerar PDF',
status=500
)
response = HttpResponse(
pdf_buffer.getvalue(),
content_type='application/pdf'
)
response['Content-Disposition'] = (
f'attachment; filename="{filename}"'
)
return response
+493
View File
@@ -0,0 +1,493 @@
from django.shortcuts import get_object_or_404
from django.urls import reverse
from entities.models import Farm, Rotation
from .forms import ReportForm
from datetime import timedelta
from django.utils import timezone
from collections import defaultdict
from decimal import Decimal
from datetime import datetime
from .utils import dumps_clean
from collections import defaultdict
def _get_selected_farm_id(request):
return request.session['selected_farm_id']
def _get_selected_farm(request):
return get_object_or_404(
Farm,
pk=_get_selected_farm_id(request),
is_active=True
)
def _get_farm_area(farm):
if farm.paddocks_area > 10000:
return f'{farm.paddocks_area / 10000} ha'
return f'{farm.paddocks_area} m²'
def _get_past_rotations(farm, days):
today = timezone.now()
one_year_ago = today - timedelta(days=days)
return Rotation.objects.filter(
is_active=True,
paddock__farm=farm,
entry_date__gte=one_year_ago,
exit_date__isnull=False,
)
def _group_paddock_rotations(rotations):
paddock_rotations = defaultdict(list)
for rotation in rotations:
if rotation.exit_date:
paddock_rotations[rotation.paddock_id].append(rotation)
for paddock_id, rotations_list in paddock_rotations.items():
rotations_list.sort(
key=lambda r: r.entry_date
)
return paddock_rotations
def _calculate_rest_periods(rotations):
paddock_rotations = _group_paddock_rotations(rotations)
rests = []
for paddock_id, rotations_list in paddock_rotations.items():
for i in range(len(rotations_list) - 1):
current_rotation = rotations_list[i]
next_rotation = rotations_list[i + 1]
rest_days = (
next_rotation.entry_date -
current_rotation.exit_date
).days
rests.append({
'paddock': current_rotation.paddock,
'days': rest_days,
'previous_rotation': current_rotation,
'next_rotation': next_rotation,
})
return rests
def _get_warnings(rotations, selected_farm):
ideal_occupation = selected_farm.ideal_paddock_occupation
ideal_rest = selected_farm.ideal_paddock_rest
rests = _calculate_rest_periods(rotations)
warnings = set()
for rotation in rotations:
occupation_days = (rotation.exit_date - rotation.entry_date).days
if occupation_days > ideal_occupation:
warnings.add(f'Ocupação acima do ideal (Piquete {rotation.paddock.paddock_number}, duração {occupation_days} dias)')
for rest in rests:
if rest['days'] < ideal_rest:
warnings.add(f'Descanso abaixo do ideal (Piquete {rest['paddock']}, duração {rest['days']} dias)')
return warnings
def _get_average_occupations(rotations):
total_count = rotations.count()
if total_count == 0:
return 0
total_sum = 0
for rotation in rotations:
total_sum += rotation.duration
return round(total_sum / total_count, 1)
def _get_average_rest(rotations):
rests = _calculate_rest_periods(rotations)
rests_count = len(rests)
if rests_count == 0:
return 0
rest_sum = 0
for rest in rests:
rest_sum += rest['days']
return round(rest_sum / rests_count, 2)
def _get_total_consume(rotations, selected_farm):
livestock_consume = 500 * (selected_farm.default_consumption / 100)
duration_factor = Decimal(-0.4946)
total_consume = 0
for rotation in rotations:
batchtype_factor = 1
if rotation.batch and rotation.batch.batch_type:
batchtype_factor = (
rotation.batch.batch_type.consumption_factor or 1
)
total_consume += livestock_consume * rotation.livestock_unit_day * batchtype_factor * rotation.duration ** duration_factor
return Decimal(round(total_consume / 1000, 2))
def _get_average_paddock_consumption(rotations, selected_farm):
total_consume = _get_total_consume(rotations, selected_farm)
paddock_count = (rotations.values('paddock').distinct().count())
if paddock_count == 0:
return 0
return Decimal(round(total_consume / paddock_count, 4))
def _get_total_produced(rotations, selected_farm):
total_consume = _get_total_consume(rotations, selected_farm)
production_factor = Decimal(1.35)
return Decimal(round(total_consume * production_factor, 2))
def _get_average_paddock_production(rotations, selected_farm):
total_produced = _get_total_produced(rotations, selected_farm)
paddock_count = (rotations.values('paddock').distinct().count())
if paddock_count == 0:
return 0
return Decimal(round(total_produced / paddock_count, 4))
def _get_average_hectare_production(rotations, selected_farm):
total_production = _get_total_produced(rotations, selected_farm)
total_hectares = Decimal(sum(rotation.paddock.area or 0 for rotation in rotations)) / Decimal('10000')
if total_hectares == 0:
return 0
return Decimal(round(total_production / total_hectares, 4))
def get_last_twelve_months():
MONTHS = [
"Jan", "Fev", "Mar", "Abr",
"Mai", "Jun", "Jul", "Ago",
"Set", "Out", "Nov", "Dez"
]
today = datetime.now()
months = []
for i in range(11, -1, -1):
month = today.month - i
year = today.year
while month <= 0:
month += 12
year -= 1
months.append(
f"{MONTHS[month - 1]}/{str(year)[2:]}"
)
return months
def _get_monthly_metric(farm, metric_function):
data = []
today = timezone.now()
for i in range(11, -1, -1):
month = today.month - i
year = today.year
while month <= 0:
month += 12
year -= 1
month_rotations = Rotation.objects.filter(
is_active=True,
paddock__farm=farm,
exit_date__isnull=False,
entry_date__year=year,
entry_date__month=month
)
data.append(
metric_function(month_rotations)
)
return data
def _get_monthly_average_rest(farm):
today = timezone.now()
rotations = _get_past_rotations(farm, 365)
rests = _calculate_rest_periods(rotations)
monthly_rests = defaultdict(list)
for rest in rests:
rest_month = rest['next_rotation'].entry_date.month
rest_year = rest['next_rotation'].entry_date.year
monthly_rests[(rest_year, rest_month)].append(
rest['days']
)
data = []
for i in range(11, -1, -1):
month = today.month - i
year = today.year
while month <= 0:
month += 12
year -= 1
values = monthly_rests.get(
(year, month),
[]
)
if values:
data.append(
round(sum(values) / len(values), 2)
)
else:
data.append(0)
return data
def get_dashboard_context(request):
farm = _get_selected_farm(request)
context_rotations = _get_past_rotations(farm, 365)
context = {}
context['title'] = 'Dashboard'
context['farm'] = _get_selected_farm(request)
context['farmcards'] = [
{'title': 'Animais', 'info': farm.animal_count},
{'title': 'UGM Total', 'info': farm.total_ugm},
{'title': 'Lotes', 'info': farm.batch_count},
{'title': 'Piquetes', 'info': farm.paddock_count},
{'title': 'Área dos Piquetes', 'info': _get_farm_area(farm)},
{'title': 'Movimentações Totais', 'info': farm.total_rotations},
]
context['warnings'] = _get_warnings(context_rotations, farm)
context['average_occupation'] = _get_average_occupations(context_rotations)
context['average_rest'] = _get_average_rest(context_rotations)
context['rotation_count'] = context_rotations.count()
context['total_consumed'] = _get_total_consume(context_rotations, farm)
context['average_paddock_consumption'] = _get_average_paddock_consumption(context_rotations, farm)
context['total_produced'] = _get_total_produced(context_rotations, farm)
context['average_paddock_production'] = _get_average_paddock_production(context_rotations, farm)
context['average_hectare_production'] = _get_average_hectare_production(context_rotations, farm)
# charts context
context['months'] = dumps_clean(get_last_twelve_months())
context['occupation'] = dumps_clean(_get_monthly_metric(farm, _get_average_occupations))
context['rest'] = dumps_clean(_get_monthly_average_rest(farm))
context['consume'] = dumps_clean(_get_monthly_metric(farm, lambda r: _get_total_consume(r, farm)))
context['production'] = dumps_clean(_get_monthly_metric(farm, lambda r: _get_total_produced(r, farm)))
return context
def get_request_report_context(request):
form = ReportForm()
return {
'form': form,
'action_url': reverse('report:create_report'),
'return_url': reverse('report:dashboard'),
'action_button_text' : 'Gerar',
}
def _get_rotations_between_dates(
farm,
start_date,
end_date
):
return Rotation.objects.filter(
is_active=True,
paddock__farm=farm,
entry_date__date__gte=start_date,
entry_date__date__lte=end_date,
exit_date__isnull=False
).select_related(
'paddock',
'batch',
'batch__batch_type'
)
def _get_rotation_consumption(
rotation,
farm
):
livestock_consume = (
500 * (farm.default_consumption / 100)
)
duration_factor = Decimal('-0.4946')
batchtype_factor = 1
if (
rotation.batch and
rotation.batch.batch_type
):
batchtype_factor = (
rotation.batch.batch_type.consumption_factor or 1
)
total = (
livestock_consume *
rotation.livestock_unit_day *
batchtype_factor *
rotation.duration ** duration_factor
)
return Decimal(
round(total, 4)
)
def _get_rotation_production(
rotation,
farm
):
return Decimal(
round(
_get_rotation_consumption(
rotation,
farm
) * Decimal('1.35'),
4
)
)
def _get_report_summary(
rotations,
farm
):
return {
'rotation_count': rotations.count(),
'average_occupation': _get_average_occupations(rotations),
'average_rest': _get_average_rest(rotations),
'total_consumed': _get_total_consume(rotations, farm),
'total_produced': _get_total_produced(rotations, farm),
'average_paddock_production':
_get_average_paddock_production(
rotations,
farm
),
'average_hectare_production':
_get_average_hectare_production(
rotations,
farm
)
}
def _get_paddock_report(
rotations,
farm
):
report = []
paddocks = (
rotations
.values_list(
'paddock_id',
flat=True
)
.distinct()
)
for paddock_id in paddocks:
paddock_rotations = rotations.filter(
paddock_id=paddock_id
)
paddock = paddock_rotations.first().paddock
report.append({
'paddock': paddock,
'area': paddock.area,
'rotation_count': paddock_rotations.count(),
'average_occupation': _get_average_occupations(paddock_rotations),
'average_rest': _get_average_rest(paddock_rotations),
'consumption': _get_total_consume(paddock_rotations,farm),
'production': _get_total_produced(paddock_rotations,farm),
})
return sorted(
report,
key=lambda x: x['paddock'].paddock_number
)
def _get_batch_report(rotations, farm):
report = []
batch_ids = (rotations.values_list('batch_id', flat=True).distinct())
for batch_id in batch_ids:
if batch_id is None:
continue
batch_rotations = rotations.filter(batch_id=batch_id)
batch = batch_rotations.first().batch
report.append({
'batch': batch,
'identifier': batch.identifier,
'animal_category': batch.animal_category,
'batch_type': (batch.batch_type.name if batch.batch_type else '-'),
'rotation_count': batch_rotations.count(),
'consumption':_get_total_consume( batch_rotations, farm),
'production':_get_total_produced( batch_rotations, farm),
})
return sorted(
report,
key=lambda x: x['identifier']
)
def _get_rotation_report(rotations, farm):
report = []
for rotation in rotations.order_by('entry_date'):
report.append({
'entry_date': rotation.entry_date,
'exit_date': rotation.exit_date,
'paddock': rotation.paddock,
'batch': rotation.batch,
'animal_count': rotation.animal_count,
'occupation': rotation.duration,
'livestock_unit_day': round(rotation.livestock_unit_day, 2),
'consumption': _get_rotation_consumption(rotation, farm),
'production': _get_rotation_production(rotation, farm)
})
return report
def get_report_context(request):
farm = _get_selected_farm(request)
form = ReportForm(request.POST)
if form.is_valid():
start_date = form.cleaned_data['start_date']
end_date = form.cleaned_data['end_date']
rotations = _get_rotations_between_dates(farm, start_date, end_date)
return {
'title': 'Relatório Completo',
'farm': farm,
'start_date': start_date,
'end_date': end_date,
'generated_at': timezone.now(),
'summary': _get_report_summary(rotations, farm),
'warnings': _get_warnings(rotations, farm),
'paddocks': _get_paddock_report(rotations, farm),
'batches': _get_batch_report(rotations, farm),
'rotations': _get_rotation_report(rotations, farm)
}
@@ -0,0 +1,261 @@
/* Header */
.dashboard-header{
display:flex;
justify-content:space-between;
align-items:center;
margin-bottom:30px;
}
.generate-report-button{
display:flex;
align-items:center;
gap:8px;
padding:12px 18px;
background:#2f855a;
color:white;
text-decoration:none;
border-radius:8px;
font-weight:600;
transition:0.2s;
}
.generate-report-button:hover{
background:#276749;
}
/* Sections */
.dashboard-farm,
.dashboard-warnings,
.dashboard-rotation,
.dashboard-consumption-production{
background:white;
border-radius:14px;
padding:25px;
margin-bottom:25px;
box-shadow:0 2px 10px rgba(0,0,0,.05);
}
.dashboard-subtitle{
font-size:24px;
font-weight:600;
}
.subtitle-footer{
margin-top:4px;
margin-bottom:20px;
color:#718096;
font-size:14px;
}
/* Farm */
.dashboard-farm-cards-container{
display:flex;
flex-wrap:nowrap;
gap:10px;
margin-top:20px;
}
.dashboard-farm-infocard{
flex:1 1 180px;
border:1px solid #e5e7eb;
border-radius:12px;
padding:15px;
transition:0.2s;
}
.dashboard-farm-infocard .title{
font-size:13px;
color:#6b7280;
text-wrap: nowrap;
}
.dashboard-farm-infocard .info{
font-size:28px;
font-weight:700;
margin-top:8px;
text-wrap: nowrap;
}
/* Warnings */
.dashboard-warnings .warning{
background:#fff7ed;
border-left:4px solid #f59e0b;
border-radius:8px;
padding:14px;
margin-top:12px;
}
.warning-description{
color:#92400e;
font-size:14px;
}
.dashboard-warnings .warnings-container {
max-height: 0;
overflow: hidden;
transition: max-height 0.25s ease;
}
.dashboard-warnings.show .warnings-toggler {
transform: rotate(-180deg);
}
.warnings-toggler {
margin-left: 20px;
cursor: pointer;
}
.dashboard-warnings .warnings-toggler {
transform: rotate(0deg);
transition: transform 0.25s ease;
}
/* Rotations */
.dashboard-rotation-cards-container{
display:grid;
grid-template-columns:repeat(auto-fit,minmax(220px,1fr));
gap:15px;
margin-bottom:20px;
}
.rotation-info-card{
border:1px solid #e5e7eb;
border-radius:12px;
padding:18px;
}
.rotation-info-card .title{
color:#6b7280;
font-size:13px;
}
.rotation-info-card .info{
margin-top:8px;
font-size:28px;
font-weight:700;
}
.dashboard-rotation-graphs{
display:grid;
grid-template-columns:1fr 1fr;
gap:20px;
}
.average-rotation-month,
.average-rest-month{
border:1px solid #e5e7eb;
border-radius:12px;
padding:20px;
min-height:320px;
}
.graph-title{
font-size:16px;
font-weight:600;
margin-bottom:15px;
}
/* Consumption & Production */
.dashboard-consumption-production{
background:white;
border-radius:14px;
padding:25px;
margin-bottom:25px;
box-shadow:0 2px 10px rgba(0,0,0,.05);
}
.consumption-production-grid{
display:grid;
grid-template-columns:1fr 1fr;
gap:20px;
}
.dashboard-consumption-container,
.dashboard-production-container{
display:flex;
flex-direction:column;
gap:20px;
}
.consumption-card-container,
.production-card-container{
display:flex;
text-wrap: nowrap;
justify-content: space-between;
gap:15px;
}
.consumption-card,
.production-card{
border:1px solid #e5e7eb;
border-radius:12px;
padding:18px;
width: 100%;
}
.consumption-card .title,
.production-card .title{
color:#6b7280;
font-size:13px;
}
.consumption-card .info,
.production-card .info{
margin-top:8px;
font-size:28px;
font-weight:700;
}
.consumption-graph-container,
.production-graph-container{
width:100%;
flex:1;
}
.consumption-graph,
.production-graph{
border:1px solid #e5e7eb;
border-radius:12px;
padding:20px;
min-height:350px;
height:100%;
}
/* Responsive */
@media(max-width:1100px){
.dashboard-farm-cards-container{
flex-wrap:wrap;
}
.dashboard-farm-infocard {
flex: 1 0 calc(50% - 10px);
}
.dashboard-rotation-graphs{
grid-template-columns:1fr;
}
.consumption-production-grid{
grid-template-columns:1fr;
}
.consumption-card-container,
.production-card-container{
flex-wrap: wrap;
}
}
@media(max-width:768px){
.dashboard-header{
flex-direction:column;
align-items:flex-start;
gap:15px;
}
.generate-report-button{
width:100%;
justify-content:center;
}
}
@@ -0,0 +1,64 @@
@page {
size: A4;
margin: 2cm;
}
body {
font-family: Arial, sans-serif;
font-size: 11px;
color: #222;
}
h1, h2, h3 {
margin-bottom: 8px;
}
h1 {
text-align: center;
}
h2 {
border-bottom: 1px solid #ccc;
padding-bottom: 4px;
margin-top: 24px;
}
table {
width: 100%;
border-collapse: collapse;
margin-top: 8px;
margin-bottom: 16px;
}
th, td {
border: 1px solid #ccc;
padding: 3px;
}
th {
background: #f2f2f2;
text-align: left;
}
.text-center {
text-align: center;
}
.cover {
margin-top: 180px;
text-align: center;
}
.cover-title {
font-size: 28px;
font-weight: bold;
}
.cover-subtitle {
font-size: 16px;
margin-top: 20px;
}
ul {
margin-top: 4px;
}
@@ -0,0 +1,5 @@
.request-report-title {
font-size: 22px;
margin-top: 10px;
margin-left: 10px;
}
@@ -0,0 +1,49 @@
document.addEventListener("DOMContentLoaded", function () {
if (!window.months) return;
new Chart(document.getElementById("occupation-chart"), {
type: "line",
data: {
labels: months,
datasets: [{ data: occupation, label: "Ocupação média (dias)", borderWidth: 2, tension: 0.3 }]
},
options: {
scales: {
y: {
beginAtZero: true
}
}
}
});
new Chart(document.getElementById("rest-chart"), {
type: "line",
data: {
labels: months,
datasets: [{ data: rest, label: "Repouso médio (dias)", borderWidth: 2, tension: 0.3 }]
},
options: {
scales: {
y: {
beginAtZero: true
}
}
}
});
new Chart(document.getElementById("consume-chart"), {
type: "bar",
data: {
labels: months,
datasets: [{ data: consume, label: "Consumo (Mg)", borderWidth: 1 }]
}
});
new Chart(document.getElementById("production-chart"), {
type: "bar",
data: {
labels: months,
datasets: [{ data: production, label: "Produção (Mg)", borderWidth: 1 }]
}
});
});
@@ -0,0 +1,18 @@
// Warnings Toggle
document.addEventListener("DOMContentLoaded", () => {
const toggler = document.getElementById("warnings-toggler");
const container = document.getElementById("dashboard-warnings");
const warningsBox = container.querySelector(".warnings-container");
if (!toggler) return;
toggler.addEventListener("click", () => {
container.classList.toggle("show");
if (container.classList.contains("show")) {
warningsBox.style.maxHeight = warningsBox.scrollHeight + "px";
} else {
warningsBox.style.maxHeight = "0px";
}
});
});
@@ -0,0 +1,130 @@
{% extends 'layouts/base.html' %}
{% load static %}
{% block extra_css %}
{{ block.super }}
<link rel="stylesheet" href="{% static 'report/css/dashboard.css' %}">
{% endblock extra_css %}
{% block content %}
<div class="dashboard-header">
<h1 class="page-title">Painel</h1>
<a hx-get="{% url "report:request_report" %}" hx-swap="beforeend" hx-target="body" class="generate-report-button"><i class="fa-solid fa-file-arrow-down"></i> Exportar relatório completo</a>
</div>
<div class="dashboard-farm">
<h2 class="dashboard-subtitle">{{ farm.name }}</h2>
<div class="dashboard-farm-cards-container">
{% for card in farmcards %}
<div class="dashboard-farm-infocard">
<p class="title">{{ card.title }}</p>
<p class="info">{{ card.info }}</p>
</div>
{% endfor %}
</div>
</div>
{% if warnings %}
<div class="dashboard-warnings" id="dashboard-warnings">
<h2 class="dashboard-subtitle"><i class="fa-solid fa-triangle-exclamation"></i> Avisos <i class="fa-solid fa-angle-down warnings-toggler" id="warnings-toggler"></i></h2>
<p class="subtitle-footer">Últimos 12 meses</p>
<div class="warnings-container">
{% for warning in warnings %}
<div class="warning">
<p class="warning-description">{{ warning }}</p>
</div>
{% endfor %}
</div>
</div>
{% endif %}
<div class="dashboard-rotation">
<h2 class="dashboard-subtitle"><i class="fa-solid fa-arrows-spin"></i> Movimentações</h2>
<p class="subtitle-footer">Últimos 12 meses</p>
<div class="dashboard-rotation-cards-container">
<div class="rotation-info-card">
<p class="title">Tempo de Ocupação Médio</p>
<p class="info">{{ average_occupation }} dias</p>
</div>
<div class="rotation-info-card">
<p class="title">Tempo de Repouso Médio</p>
<p class="info">{{ average_rest }} dias</p>
</div>
<div class="rotation-info-card">
<p class="title">Movimentações realizadas</p>
<p class="info">{{ rotation_count }}</p>
</div>
</div>
<div class="dashboard-rotation-graphs">
<div class="average-rotation-month">
<p class="graph-title">Média de ocupação por mês</p>
<canvas id="occupation-chart"></canvas>
</div>
<div class="average-rest-month">
<p class="graph-title">Média de repouso por mês</p>
<canvas id="rest-chart"></canvas>
</div>
</div>
</div>
<div class="dashboard-consumption-production">
<h2 class="dashboard-subtitle"><i class="fa-solid fa-seedling"></i> Consumo e Produção</h2>
<p class="subtitle-footer">Últimos 12 meses</p>
<div class="consumption-production-grid">
<div class="dashboard-consumption-container">
<div class="consumption-card-container">
<div class="consumption-card">
<p class="title">Consumo Total</p>
<p class="info">{{ total_consumed }} Mg</p>
</div>
<div class="consumption-card">
<p class="title">Consumo por piquete</p>
<p class="info">{{ average_paddock_consumption }} Mg</p>
</div>
</div>
<div class="consumption-graph-container">
<div class="consumption-graph">
<p class="graph-title">Média mensal de consumo por piquete</p>
<canvas id="consume-chart"></canvas>
</div>
</div>
</div>
<div class="dashboard-production-container">
<div class="production-card-container">
<div class="production-card">
<p class="title">Produção Total</p>
<p class="info">{{ total_produced }} Mg</p>
</div>
<div class="production-card">
<p class="title">Produção por piquete</p>
<p class="info">{{ average_paddock_production }} Mg</p>
</div>
<div class="production-card">
<p class="title">Produção por hectare</p>
<p class="info">{{ average_hectare_production }} Mg</p>
</div>
</div>
<div class="production-graph-container">
<div class="production-graph">
<p class="graph-title">Média mensal de produção por piquete</p>
<canvas id="production-chart"></canvas>
</div>
</div>
</div>
</div>
</div>
{% endblock content %}
{% block extra_js %}
{{ block.super }}
<script>
window.months = {{ months|safe }};
window.occupation = {{ occupation|safe }};
window.rest = {{ rest|safe }};
window.consume = {{ consume|safe }};
window.production = {{ production|safe }};
</script>
<script src="{% static "report/js/dashboard.js" %}"></script>
<script src="{% static "report/js/chart_creation.js" %}"></script>
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
{% endblock extra_js %}
@@ -0,0 +1,157 @@
<!DOCTYPE html>
</html>
<html lang="pt-br">
<head>
<meta charset="UTF-8">
<title>Relatório Completo</title>
<style>
{{ pdf_css|safe }}
</style>
</head>
<body>
<div class="cover">
<div class="cover-title">Relatório Completo de Pastejo</div>
<div class="cover-subtitle">{{ farm.name }}</div>
<p>Período analisado: {{ start_date }} até {{ end_date }}</p>
<p>Emitido em: {{ generated_at }}</p>
</div>
<h2>Resumo Geral</h2>
<table>
<tr>
<th>Indicador</th>
<th>Valor</th>
</tr>
<tr>
<td>Movimentações</td>
<td>{{ summary.rotation_count }}</td>
</tr>
<tr>
<td>Tempo médio de ocupação</td>
<td>{{ summary.average_occupation }} dias</td>
</tr>
<tr>
<td>Tempo médio de repouso</td>
<td>{{ summary.average_rest }} dias</td>
</tr>
<tr>
<td>Consumo total</td>
<td>{{ summary.total_consumed }} Mg</td>
</tr>
<tr>
<td>Produção total</td>
<td>{{ summary.total_produced }} Mg</td>
</tr>
<tr>
<td>Produção média por piquete</td>
<td>{{ summary.average_paddock_production }} Mg</td>
</tr>
<tr>
<td>Produção média por hectare</td>
<td>{{ summary.average_hectare_production }} Mg/ha</td>
</tr>
</table>
<h2>Avisos</h2>
{% if warnings %}
<ul>
{% for warning in warnings %}
<li>{{ warning }}</li>
{% endfor %}
</ul>
{% else %}
<p>Nenhum aviso encontrado para o período analisado.</p>
{% endif %}
<h2>Indicadores por Piquete</h2>
<table>
<tr>
<th>Piquete</th>
<th>Área (m²)</th>
<th>Mov.</th>
<th>TO Médio</th>
<th>TR Médio</th>
<th>Consumo (Mg)</th>
<th>Produção (Mg)</th>
</tr>
{% for paddock in paddocks %}
<tr>
<td>{{ paddock.paddock.paddock_number }}</td>
<td>{{ paddock.area }}</td>
<td>{{ paddock.rotation_count }}</td>
<td>{{ paddock.average_occupation }}</td>
<td>{{ paddock.average_rest }}</td>
<td>{{ paddock.consumption }}</td>
<td>{{ paddock.production }}</td>
</tr>
{% endfor %}
</table>
<h2>Indicadores por Lote</h2>
<table>
<tr>
<th>Lote</th>
<th>Categoria</th>
<th>Tipo</th>
<th>Movimentações</th>
<th>Consumo (Mg)</th>
<th>Produção (Mg)</th>
</tr>
{% for batch in batches %}
<tr>
<td>{{ batch.identifier }}</td>
<td>{{ batch.animal_category }}</td>
<td>{{ batch.batch_type }}</td>
<td>{{ batch.rotation_count }}</td>
<td>{{ batch.consumption }}</td>
<td>{{ batch.production }}</td>
</tr>
{% endfor %}
</table>
<h2>Movimentações</h2>
<table>
<tr>
<th>Entrada</th>
<th>Saída</th>
<th>Piquete</th>
<th>Lote</th>
<th>Animais</th>
<th>UGM-dia</th>
<th>Ocupação (dias)</th>
<th>Consumo (Kg)</th>
<th>Produção (Kg)</th>
</tr>
{% for rotation in rotations %}
<tr>
<td>{{ rotation.entry_date|date:"d/m/y H:i" }}</td>
<td>{{ rotation.exit_date|date:"d/m/y H:i" }}</td>
<td>{{ rotation.paddock.paddock_number }}</td>
<td>{{ rotation.batch }}</td>
<td>{{ rotation.animal_count }}</td>
<td>{{ rotation.livestock_unit_day }}</td>
<td>{{ rotation.occupation }}</td>
<td>{{ rotation.consumption }}</td>
<td>{{ rotation.production }}</td>
</tr>
{% endfor %}
</table>
<h2>Metodologia</h2>
<p>Os indicadores apresentados neste relatório foram calculados a partir dos registros de movimentação dos lotes entre os piquetes.</p>
<p>O consumo de pastagem foi estimado considerando uma Unidade de Gado Maior equivalente a 500 kg de peso vivo e um consumo diário de matéria seca correspondente a {{ farm.default_consumption }}% do peso vivo.</p>
<p>Os valores de consumo foram corrigidos conforme o tipo de lote e ajustados pelo tempo de permanência utilizando fator exponencial baseado na quarta lei do Pastoreio Racional Voisin.</p>
<p>A produção de forragem foi estimada pela aplicação de um fator de correção de 1,35 sobre o consumo estimado, representando perdas por pisoteio, material não consumido e demais fatores inerentes ao processo de pastejo.</p>
</body>
</html>
@@ -0,0 +1,7 @@
{% extends 'layouts/base_modal.html' %}
{% load static %}
{% block content %}
<h1 class="request-report-title">Gerar relatório</h1>
{% include 'components/form.html' %}
{% endblock %}
+5 -7
View File
@@ -1,13 +1,11 @@
from django.urls import path
from django.http import HttpResponse
from django.contrib.auth.decorators import login_required
from . import views
app_name = 'report'
@login_required(login_url='/user/login/')
def home(request):
return HttpResponse("Home app page")
urlpatterns = [
path('', home),
path('', views.dashboard, name='dashboard'),
path("request/", views.request_report, name="request_report"),
path('report/', views.create_report, name='create_report'),
]
+16
View File
@@ -0,0 +1,16 @@
import json
from decimal import Decimal
from datetime import date, datetime
def json_safe(value):
if isinstance(value, Decimal):
return float(value)
if isinstance(value, (datetime, date)):
return value.isoformat()
return value
def dumps_clean(data):
return json.dumps(
data,
default=json_safe
)
+19 -2
View File
@@ -1,3 +1,20 @@
from django.shortcuts import render
from django.shortcuts import render, redirect
from . import services
from .pdf import render_report_pdf
# Create your views here.
def dashboard(request):
context = services.get_dashboard_context(request)
return render(request, 'report/dashboard.html', context)
def request_report(request):
context = services.get_request_report_context(request)
return render(request, 'report/request_report.html', context)
def create_report(request):
if request.method != 'POST':
return redirect('report:dashboard')
context = services.get_report_context(request)
return render_report_pdf('report/report_pdf.html', context, filename='relatorio.pdf')
# return render(request, 'report/report_pdf.html', context)
+1
View File
@@ -26,6 +26,7 @@ INSTALLED_APPS = [
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
'import_export',
# My apps
'core',
'entities',
BIN
View File
Binary file not shown.