Salvando estado atual
Diversas modificações
This commit is contained in:
1 parent
740547a8ed
commit
8dd945798a
69 files changed
+2479
-259
No files matched your search
@@ -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.
|
# Injects the selected farm and the list of farms into the template context for authenticated users.
|
||||||
def selected_farm_processor(request):
|
def selected_farm_processor(request):
|
||||||
selected_farm = None
|
selected_farm = None
|
||||||
farms = None
|
farms = Farm.objects.none()
|
||||||
|
|
||||||
if request.user.is_authenticated:
|
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:
|
if farm_id:
|
||||||
try:
|
selected_farm = farms.filter(id=farm_id).first()
|
||||||
selected_farm = farms.get(id=farm_id)
|
|
||||||
except Farm.DoesNotExist:
|
if selected_farm is None:
|
||||||
request.session.pop('selected_farm_id', None)
|
request.session.pop("selected_farm_id", None)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
'SELECTED_FARM': selected_farm,
|
"SELECTED_FARM": selected_farm,
|
||||||
'farms': farms
|
"farms": farms,
|
||||||
}
|
}
|
||||||
@@ -7,35 +7,35 @@
|
|||||||
|
|
||||||
/* BUTTON STYLES */
|
/* BUTTON STYLES */
|
||||||
.btn-simple {
|
.btn-simple {
|
||||||
color: var(--color-text-dark);
|
color: var(--color-plus-btn);
|
||||||
padding: 0 var(--space-xs);
|
padding: 0 var(--space-xs);
|
||||||
border-radius: var(--border-radius-sm);
|
border-radius: var(--border-radius-sm);
|
||||||
}
|
}
|
||||||
|
|
||||||
.btn-simple:hover {
|
.btn-simple:hover {
|
||||||
color: var(--color-text-light);
|
color: var(--color-plus-btn-hover);
|
||||||
background-color: var(--color-bg-dark);
|
background-color: var(--color-bg-dark);
|
||||||
transition: all var(--transition-speed) ease;
|
transition: all var(--transition-speed) ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* PRIMARY BUTTON (Main Action) */
|
/* PRIMARY BUTTON (Main Action) */
|
||||||
.btn-primary {
|
.btn-primary {
|
||||||
background-color: var(--color-primary);
|
background-color: var(--color-plus-btn);
|
||||||
|
color: var(--color-text-light); /* White text */
|
||||||
padding: var(--space-sm);
|
padding: var(--space-sm);
|
||||||
border-radius: var(--border-radius-sm);
|
border-radius: var(--border-radius-sm);
|
||||||
color: var(--color-text-light); /* White text */
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.btn-primary:hover {
|
.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) */
|
/* SECONDARY BUTTON (Neutral Action) */
|
||||||
.btn-secondary {
|
.btn-secondary {
|
||||||
background-color: var(--color-secondary);
|
background-color: var(--color-secondary);
|
||||||
|
color: var(--color-text-light);
|
||||||
padding: var(--space-sm);
|
padding: var(--space-sm);
|
||||||
border-radius: var(--border-radius-sm);
|
border-radius: var(--border-radius-sm);
|
||||||
color: var(--color-text-light);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.btn-secondary:hover {
|
.btn-secondary:hover {
|
||||||
@@ -44,12 +44,12 @@
|
|||||||
|
|
||||||
/* SUCCESS/DANGER BUTTONS */
|
/* SUCCESS/DANGER BUTTONS */
|
||||||
.btn-success {
|
.btn-success {
|
||||||
background-color: var(--color-success);
|
background-color: var(--color-plus-btn);
|
||||||
color: var(--color-text-light);
|
color: var(--color-text-light);
|
||||||
}
|
}
|
||||||
|
|
||||||
.btn-danger {
|
.btn-danger {
|
||||||
background-color: var(--color-danger);
|
background-color: var(--color-danger-btn);
|
||||||
color: var(--color-text-light);
|
color: var(--color-text-light);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -9,13 +9,14 @@
|
|||||||
|
|
||||||
/* Title */
|
/* Title */
|
||||||
.confirmation-container h1 {
|
.confirmation-container h1 {
|
||||||
font-size: 1.5rem;
|
font-size: 22px;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Description */
|
/* Description */
|
||||||
.confirmation-description {
|
.confirmation-description {
|
||||||
text-align: end;
|
text-align: end;
|
||||||
|
margin: 10px 0 15px 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Actions */
|
/* Actions */
|
||||||
@@ -26,11 +27,13 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
/* Both buttons */
|
/* Both buttons */
|
||||||
.confirmation-container form .confirm, .negate {
|
.confirmation-container form .confirm,
|
||||||
|
.confirmation-container form .negate {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
min-width: 120px;
|
min-width: 100px;
|
||||||
|
max-height: 30px;
|
||||||
padding: var(--space-sm) var(--space-sm);
|
padding: var(--space-sm) var(--space-sm);
|
||||||
border: none;
|
border: none;
|
||||||
border-radius: var(--border-radius-sm);
|
border-radius: var(--border-radius-sm);
|
||||||
@@ -41,30 +44,22 @@
|
|||||||
|
|
||||||
/* Confirm button */
|
/* Confirm button */
|
||||||
.confirmation-container form .confirm {
|
.confirmation-container form .confirm {
|
||||||
background-color: #dc3545;
|
background-color: var(--color-danger-btn);
|
||||||
color: white;
|
color: white;
|
||||||
}
|
}
|
||||||
|
|
||||||
.confirmation-container form .confirm:hover {
|
.confirmation-container form .confirm:hover {
|
||||||
filter: brightness(0.92);
|
background-color: var(--color-danger-btn-hover);
|
||||||
}
|
|
||||||
|
|
||||||
.confirmation-container form .confirm:active {
|
|
||||||
transform: scale(0.98);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Negate button */
|
/* Negate button */
|
||||||
.confirmation-container form .negate {
|
.confirmation-container form .negate {
|
||||||
background-color: var(--color-secondary);
|
background-color: var(--color-neutral-btn);
|
||||||
color: var(--color-text-light);
|
color: var(--color-text-light);
|
||||||
}
|
}
|
||||||
|
|
||||||
.confirmation-container form .negate:hover {
|
.confirmation-container form .negate:hover {
|
||||||
filter: brightness(0.92);
|
background-color: var(--color-neutral-btn-hover);
|
||||||
}
|
|
||||||
|
|
||||||
.confirmation-container form .negate:active {
|
|
||||||
transform: scale(0.90);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 600px) {
|
@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;
|
||||||
|
}
|
||||||
@@ -25,6 +25,7 @@ a {
|
|||||||
overflow-x: hidden;
|
overflow-x: hidden;
|
||||||
z-index: 150;
|
z-index: 150;
|
||||||
box-shadow: 2px 0 5px rgba(0, 0, 0, 0.1);
|
box-shadow: 2px 0 5px rgba(0, 0, 0, 0.1);
|
||||||
|
scrollbar-width: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.sidebar-brand {
|
.sidebar-brand {
|
||||||
@@ -110,29 +111,106 @@ body.sidebar-collapsed .main-content {
|
|||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Inicio do css do seletor antigo */
|
/* Farm Selection */
|
||||||
.navbar-farm-selector {
|
.ts-wrapper {
|
||||||
display: flex;
|
width: clamp(200px, 25vw, 300px);
|
||||||
flex-direction: column;
|
margin: 0 auto;
|
||||||
align-items: center;
|
|
||||||
cursor: default;
|
|
||||||
opacity: 0.75;
|
|
||||||
transition: all var(--transition-speed) ease;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.navbar-farm-selector:hover {
|
.ts-wrapper.single .ts-control {
|
||||||
opacity: 1;
|
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 {
|
.ts-wrapper.single .ts-control:hover {
|
||||||
font-size: var(--font-md);
|
box-shadow: none;
|
||||||
font-weight: 500;
|
}
|
||||||
margin: 0;
|
|
||||||
|
.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;
|
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;
|
display: flex;
|
||||||
justify-content: flex-end;
|
justify-content: flex-end;
|
||||||
gap: var(--space-sm);
|
gap: var(--space-sm);
|
||||||
@@ -163,27 +241,38 @@ body.sidebar-collapsed .main-content {
|
|||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
background-color: rgba(0, 0, 0, 0.6);
|
background-color: rgba(0, 0, 0, 0.4);
|
||||||
}
|
}
|
||||||
|
|
||||||
.modal-content {
|
.modal-content {
|
||||||
background-color: var(--color-bg-light);
|
background-color: var(--color-bg-light);
|
||||||
padding: var(--space-lg);
|
padding: var(--space-md);
|
||||||
border-radius: var(--space-sm);
|
border-radius: var(--space-sm);
|
||||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2);
|
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2);
|
||||||
}
|
}
|
||||||
|
|
||||||
@media screen and (max-width: 768px) {
|
@view-transition {
|
||||||
.navbar-farm-selector,
|
navigation: auto;
|
||||||
.navbar-user-info {
|
}
|
||||||
transition: opacity var(--transition-speed) ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
::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-farm-selector,
|
||||||
body:not(.sidebar-collapsed) .navbar-user-info {
|
body:not(.sidebar-collapsed) .navbar-user-info {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
body:not(.sidebar-collapsed) .main-content {
|
||||||
|
transform: translateX(0);
|
||||||
|
width: 100%;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
.navbar {
|
.navbar {
|
||||||
padding: 0 var(--space-sm);
|
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,32 +1,43 @@
|
|||||||
:root {
|
:root {
|
||||||
/* SPECIFIC COLORS */
|
/* SPECIFIC COLORS */
|
||||||
--login-background-primary: #343a40;
|
--login-background-primary: #244238;
|
||||||
--login-background-secondary: #6b7480;
|
--login-background-secondary: #396a5a;
|
||||||
--color-login-button: #343a40;
|
--color-login-button: #396a5a;
|
||||||
--color-login-button-focus: #6b7480;
|
--color-login-button-focus: #76948a;
|
||||||
--color-farm-select-hover: #343a40;
|
--color-farm-select-hover: #244238;
|
||||||
--color-farm-select-info: #6d6d6d;
|
--color-farm-select-info: #6d6d6d;
|
||||||
--color-new-farm-button: #343a40;
|
--color-new-farm-button: #396a5a;
|
||||||
--color-new-farm-hover: #6b7480;
|
--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 */
|
/* GENERIC COLORS */
|
||||||
--color-simple-light-gray: #ccc;
|
--color-simple-light-gray: #ccc;
|
||||||
--color-border-subtle: #ccc;
|
--color-border-subtle: #ccc;
|
||||||
|
|
||||||
/* PRIMARY COLORS */
|
/* PRIMARY COLORS */
|
||||||
--color-primary: #007bff;
|
--color-primary: #244238;
|
||||||
--color-secondary: #6c757d;
|
--color-secondary: #6c757d;
|
||||||
--color-success: #28a745;
|
--color-success: #28a745;
|
||||||
--color-danger: #dc3545;
|
--color-danger: #8b3d3d;
|
||||||
--color-danger-light: #f8d7da;
|
--color-danger-light: #b37a7a;
|
||||||
--color-warn: #dca235;
|
--color-warn: #dca235;
|
||||||
|
|
||||||
/* BACKGROUND/TEXT COLORS */
|
/* BACKGROUND/TEXT COLORS */
|
||||||
--color-bg-light: #f0f0f0;
|
--color-bg-light: #f5f7f5;
|
||||||
--color-bg-white: #fafafa;
|
--color-bg-white: #fafafa;
|
||||||
--color-bg-dark: #343a40;
|
--color-bg-form: #fafafa;
|
||||||
--color-bg-sidebar: #343a40;
|
--color-bg-dark: #244238;
|
||||||
|
--color-bg-sidebar: #244238;
|
||||||
--color-text-dark: #151422;
|
--color-text-dark: #151422;
|
||||||
|
--color-form-label: #151422;
|
||||||
--color-text-light: #f8f9fa;
|
--color-text-light: #f8f9fa;
|
||||||
|
|
||||||
/* SPACING DIMENSIONS */
|
/* SPACING DIMENSIONS */
|
||||||
|
|||||||
@@ -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 %}
|
{% csrf_token %}
|
||||||
{{ form }}
|
|
||||||
<button class="btn-primary" type="submit">Salvar</button>
|
{% for field in form %}
|
||||||
<a href="{{ return_url }}" class="btn-primary">Cancelar</a>
|
<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>
|
</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>
|
<i class="fa-solid fa-bars" id="sidebar-toggle"></i>
|
||||||
</div>
|
</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">
|
<form method="post" action="{% url 'entities:user:farm_set_post' %}" class="navbar-farm-selector w33">
|
||||||
{% csrf_token %}
|
{% csrf_token %}
|
||||||
<input type="hidden" name="next" value="{{ request.path }}">
|
<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 %}
|
{% for farm in farms %}
|
||||||
<option value="{{ farm.id }}" {% if farm.id == SELECTED_FARM.id %}selected{% endif %}>
|
<option value="{{ farm.id }}" {% if farm.id == SELECTED_FARM.id %}selected{% endif %}>
|
||||||
{{ farm.name }}
|
{{ farm.name|upper }}
|
||||||
</option>
|
</option>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</select>
|
</select>
|
||||||
|
|||||||
@@ -7,7 +7,7 @@
|
|||||||
<li class="sidebar-header">ACOMPANHAMENTO</li>
|
<li class="sidebar-header">ACOMPANHAMENTO</li>
|
||||||
|
|
||||||
<li class="{% if request.resolver_match.url_name != 'report' %}highlight-hover{% endif %}">
|
<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>
|
||||||
|
|
||||||
<li class="sidebar-header">REGISTROS</li>
|
<li class="sidebar-header">REGISTROS</li>
|
||||||
@@ -27,12 +27,14 @@
|
|||||||
</li>
|
</li>
|
||||||
|
|
||||||
<li class="{% if request.resolver_match.url_name not in 'paddock_list, paddock_create, paddock_update, paddock_delete' %}highlight-hover{% endif %}">
|
<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>
|
||||||
|
|
||||||
|
<!--
|
||||||
<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 %}">
|
<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>
|
<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>
|
||||||
|
-->
|
||||||
|
|
||||||
<li class="sidebar-header">CONFIGURAÇÔES</li>
|
<li class="sidebar-header">CONFIGURAÇÔES</li>
|
||||||
|
|
||||||
@@ -41,7 +43,7 @@
|
|||||||
</li>
|
</li>
|
||||||
{% if user.is_staff or user.is_superuser %}
|
{% if user.is_staff or user.is_superuser %}
|
||||||
<li class="{% if request.resolver_match.url_name not in 'admin' %}highlight-hover{% endif %}">
|
<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>
|
</li>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</ul>
|
</ul>
|
||||||
|
|||||||
@@ -15,13 +15,20 @@
|
|||||||
<link rel="stylesheet" href="{% static 'core/css/global.css' %}">
|
<link rel="stylesheet" href="{% static 'core/css/global.css' %}">
|
||||||
|
|
||||||
<!-- Templates based on base_model can't pass their stylesheet on block extra_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 'core/css/components/confirmation_popup.css' %}">
|
||||||
<link rel="stylesheet" href="{% static 'farm/css/user_authorize.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 %}
|
{% block extra_css %}{% endblock %}
|
||||||
|
|
||||||
<!-- Importing Font Awesome -->
|
<!-- 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" />
|
<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>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
@@ -34,7 +41,11 @@
|
|||||||
</body>
|
</body>
|
||||||
<!-- Importing custom JS -->
|
<!-- Importing custom JS -->
|
||||||
<script src="{% static 'core/js/main.js' %}"></script>
|
<script src="{% static 'core/js/main.js' %}"></script>
|
||||||
|
<script src="{% static 'core/js/tomselect.js' %}"></script>
|
||||||
<!-- Importing HTMX -->
|
<!-- 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>
|
<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 %}
|
{% block extra_js %}{% endblock extra_js %}
|
||||||
</html>
|
</html>
|
||||||
@@ -15,16 +15,24 @@
|
|||||||
<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 'user/css/farm_select.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 -->
|
<!-- 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" />
|
<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>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<nav class="farm-select-navbar">
|
<nav class="farm-select-navbar">
|
||||||
<div class="navbar-brand">
|
<div class="navbar-brand">
|
||||||
MultiFarm
|
MultiFarm
|
||||||
</div>
|
</div>
|
||||||
<div class="navbar-user-info">
|
<div class="navbar-user-info-farm-select">
|
||||||
<span class="username">Olá, {{ request.user.short_name }}</span>
|
<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>
|
<a href="{% url 'entities:user:user_logout' %}"><i class="fa-solid fa-right-from-bracket"></i></a>
|
||||||
</div>
|
</div>
|
||||||
@@ -41,7 +49,6 @@
|
|||||||
<div class="farm-item">
|
<div class="farm-item">
|
||||||
<h3 class="farm-name">{{ farm.name }}</h3>
|
<h3 class="farm-name">{{ farm.name }}</h3>
|
||||||
<p class="farm-description">
|
<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-piquetes">Piquetes: {{ farm.total_paddocks }}</span>
|
||||||
<span class="count-animais">Animais: {{ farm.total_animals }}</span>
|
<span class="count-animais">Animais: {{ farm.total_animals }}</span>
|
||||||
</p>
|
</p>
|
||||||
@@ -49,13 +56,16 @@
|
|||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
{% endfor %}
|
{% 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>
|
</ul>
|
||||||
{% else %}
|
{% else %}
|
||||||
<div class="no-farms">
|
<div class="no-farms">
|
||||||
<p>Nenhuma fazenda encontrada</p>
|
<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>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
</body>
|
</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>
|
</html>
|
||||||
@@ -16,7 +16,7 @@ def get_attr(obj, attr):
|
|||||||
value = value()
|
value = value()
|
||||||
|
|
||||||
# If none, return empty string to avoid displaying "None" in the template
|
# 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:
|
except Exception:
|
||||||
return ""
|
return "- !"
|
||||||
@@ -1,11 +1,12 @@
|
|||||||
from django.contrib import admin
|
from django.contrib import admin
|
||||||
|
from import_export.admin import ImportExportModelAdmin
|
||||||
|
|
||||||
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, BatchType, 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([
|
from .models import (
|
||||||
User,
|
User,
|
||||||
Farm,
|
Farm,
|
||||||
PaddockGroup,
|
PaddockGroup,
|
||||||
@@ -15,5 +16,19 @@ admin.site.register([
|
|||||||
BatchAnimal,
|
BatchAnimal,
|
||||||
FeedingRecord,
|
FeedingRecord,
|
||||||
Rotation,
|
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)
|
||||||
@@ -4,11 +4,10 @@ from ..models import Farm, PaddockGroup, Paddock
|
|||||||
class FarmForm(forms.ModelForm):
|
class FarmForm(forms.ModelForm):
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Farm
|
model = Farm
|
||||||
fields = ['name', 'total_area']
|
fields = ['name']
|
||||||
|
|
||||||
labels = {
|
labels = {
|
||||||
'name': 'Nome',
|
'name': 'Nome',
|
||||||
'total_area': 'Área total',
|
|
||||||
}
|
}
|
||||||
FarmCreateForm = FarmEditForm = FarmForm # Alias since its implementations are the same
|
FarmCreateForm = FarmEditForm = FarmForm # Alias since its implementations are the same
|
||||||
|
|
||||||
@@ -26,12 +25,12 @@ PaddockGroupCreateForm = PaddockGroupEditForm = PaddockGroupForm
|
|||||||
class PaddockForm(forms.ModelForm):
|
class PaddockForm(forms.ModelForm):
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Paddock
|
model = Paddock
|
||||||
fields = ['paddock_number', 'area', 'load_capacity', 'paddock_group']
|
fields = ['paddock_number', 'area'] #, 'load_capacity', 'paddock_group']
|
||||||
|
|
||||||
labels = {
|
labels = {
|
||||||
'paddock_number': 'Número do piquete',
|
'paddock_number': 'Número do piquete',
|
||||||
'area': 'Área',
|
'area': 'Área (m²)',
|
||||||
'load_capacity': 'Lotação máxima',
|
# 'load_capacity': 'Lotação máxima',
|
||||||
'paddock_group': 'Bloco',
|
# 'paddock_group': 'Bloco',
|
||||||
}
|
}
|
||||||
PaddockCreateForm = PaddockEditForm = PaddockForm
|
PaddockCreateForm = PaddockEditForm = PaddockForm
|
||||||
@@ -4,20 +4,20 @@ from ..models import Rotation
|
|||||||
class RotationEntryForm(forms.ModelForm):
|
class RotationEntryForm(forms.ModelForm):
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Rotation
|
model = Rotation
|
||||||
fields = ['batch', 'entry_height', 'paddock']
|
fields = ['batch', 'paddock', 'entry_date']
|
||||||
|
|
||||||
labels = {
|
labels = {
|
||||||
'batch': 'Lote de animais',
|
'batch': 'Lote de animais',
|
||||||
'entry_height': 'Altura do pasto',
|
|
||||||
'paddock': 'Piquete',
|
'paddock': 'Piquete',
|
||||||
|
'entry_date': 'Horário da entrada'
|
||||||
}
|
}
|
||||||
|
|
||||||
class RotationExitForm(forms.ModelForm):
|
widgets = {
|
||||||
class Meta:
|
'entry_date': forms.DateTimeInput(
|
||||||
model = Rotation
|
attrs={
|
||||||
fields = ['batch', 'exit_height']
|
'type': 'datetime-local',
|
||||||
|
'class': 'form-control'
|
||||||
labels = {
|
},
|
||||||
'batch': 'Lote de animais',
|
format='%Y-%m-%dT%H:%M'
|
||||||
'exit_height': 'Altura do pasto',
|
),
|
||||||
}
|
}
|
||||||
@@ -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),
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
from datetime import date
|
from datetime import date
|
||||||
from django.db import models
|
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):
|
class Animal(models.Model):
|
||||||
is_active = models.BooleanField(default=True)
|
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')
|
farm = models.ForeignKey('entities.Farm', on_delete=models.CASCADE, related_name='animals')
|
||||||
|
|
||||||
def __str__(self):
|
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
|
@property
|
||||||
def age(self):
|
def age(self):
|
||||||
@@ -56,12 +57,14 @@ class BatchType(models.Model):
|
|||||||
consumption_factor = models.DecimalField(max_digits=4, decimal_places=2, default=1.00)
|
consumption_factor = models.DecimalField(max_digits=4, decimal_places=2, default=1.00)
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
state = 'Ativo' if self.is_active else 'Inativo'
|
return f"{self.name} ({self.consumption_percentage}%)"
|
||||||
return f"{state} - {self.name}, {self.consumption_factor}"
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def consumption_percentage(self):
|
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):
|
class Batch(models.Model):
|
||||||
is_active = models.BooleanField(default=True)
|
is_active = models.BooleanField(default=True)
|
||||||
@@ -88,6 +91,15 @@ class Batch(models.Model):
|
|||||||
|
|
||||||
self.__original_is_active = self.is_active
|
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):
|
def __str__(self):
|
||||||
return self.identifier
|
return self.identifier
|
||||||
|
|
||||||
@@ -112,20 +124,30 @@ 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 __init__(self, *args, **kwargs):
|
||||||
|
super().__init__(*args, **kwargs)
|
||||||
|
self.__original_is_active = self.is_active
|
||||||
|
|
||||||
def save(self, *args, **kwargs):
|
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:
|
if is_active_changed and self.is_active:
|
||||||
raise ValueError(
|
raise ValueError(
|
||||||
"Reactivating a BatchAnimal is not allowed automatically because it may violate "
|
"Reactivating a BatchAnimal is not allowed automatically because it may violate "
|
||||||
"the unique active batch constraint for an animal. Review the logic in "
|
"the unique active batch constraint for an animal. Review the logic in "
|
||||||
"models/animal.py (BatchAnimal.save) before proceeding."
|
"models/animal.py (BatchAnimal.save) before proceeding."
|
||||||
)
|
)
|
||||||
|
|
||||||
super().save(*args, **kwargs)
|
super().save(*args, **kwargs)
|
||||||
|
|
||||||
|
self.__original_is_active = self.is_active
|
||||||
|
|
||||||
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')
|
||||||
feed_quantity = models.DecimalField(max_digits=10, decimal_places=2)
|
feed_quantity = models.DecimalField(max_digits=10, decimal_places=2)
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return f"Consumo de {self.animal.identifier} - {self.feed_quantity} kg"
|
return f"{self.animal.identifier} - {self.feed_quantity}kg"
|
||||||
@@ -1,12 +1,16 @@
|
|||||||
from django.db import models
|
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):
|
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)
|
default_consumption = models.DecimalField(max_digits=4, decimal_places=2, default=2.75)
|
||||||
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)
|
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)
|
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):
|
def __str__(self):
|
||||||
@@ -14,14 +18,40 @@ class Farm(models.Model):
|
|||||||
|
|
||||||
@property
|
@property
|
||||||
def animal_count(self):
|
def animal_count(self):
|
||||||
return self.animals.filter(
|
return self.animals.filter(is_active=True).count()
|
||||||
is_active=True
|
|
||||||
).count()
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def paddock_count(self):
|
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
|
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()
|
).count()
|
||||||
|
|
||||||
|
|
||||||
@@ -31,7 +61,7 @@ class PaddockGroup(models.Model):
|
|||||||
description = models.TextField(null=True, blank=True)
|
description = models.TextField(null=True, blank=True)
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return f"Bloco {self.id}"
|
return f"{self.id}"
|
||||||
|
|
||||||
|
|
||||||
class Paddock(models.Model):
|
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)
|
paddock_group = models.ForeignKey(PaddockGroup, on_delete=models.SET_NULL, related_name='paddocks', null=True, blank=True)
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return f"Piquete {self.paddock_number} - Bloco {self.paddock_group.id}"
|
return f"{self.paddock_number}"
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def occupied_as_text(self):
|
def occupied_as_text(self):
|
||||||
if self.occupied:
|
if self.occupied:
|
||||||
return "Sim"
|
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()
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
from django.db import models
|
from django.db import models
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
|
from decimal import Decimal
|
||||||
|
|
||||||
class Rotation(models.Model):
|
class Rotation(models.Model):
|
||||||
is_active = models.BooleanField(default=True)
|
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)
|
exit_height = models.DecimalField(max_digits=10, decimal_places=2, null=True, blank=True)
|
||||||
animal_count = models.IntegerField()
|
animal_count = models.IntegerField()
|
||||||
livestock_unit = models.DecimalField(max_digits=10, decimal_places=2)
|
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')
|
paddock = models.ForeignKey('entities.Paddock', on_delete=models.CASCADE, related_name='rotations')
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return f"Movimentação {self.id} - Lote {self.batch.identifier}"
|
return f"Movimentação {self.id}"
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def duration(self):
|
def duration(self):
|
||||||
end = self.exit_date or timezone.now()
|
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
|
@property
|
||||||
def duration_display_day(self):
|
def duration_display_day(self):
|
||||||
end = self.exit_date or timezone.now()
|
days = self.duration
|
||||||
delta = end - self.entry_date
|
|
||||||
|
|
||||||
days = delta.days
|
if days < 1:
|
||||||
|
return "menos de um"
|
||||||
|
|
||||||
if days == 0:
|
string_day = f"{days}".replace('.', ',')
|
||||||
return "Hoje"
|
return string_day
|
||||||
|
|
||||||
if days == 1:
|
@property
|
||||||
return "1"
|
def livestock_unit_day(self):
|
||||||
|
if self.duration == 0:
|
||||||
|
return 0
|
||||||
|
return Decimal(self.livestock_unit * self.duration)
|
||||||
|
|
||||||
return f"{days}"
|
@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"
|
||||||
@@ -54,6 +54,7 @@ def get_animal_create_context():
|
|||||||
context['form'] = form
|
context['form'] = form
|
||||||
context['action_url'] = reverse('entities:animal:animal_create')
|
context['action_url'] = reverse('entities:animal:animal_create')
|
||||||
context['return_url'] = reverse('entities:animal:animal_list')
|
context['return_url'] = reverse('entities:animal:animal_list')
|
||||||
|
context['modal_title'] = 'Cadastrar animal'
|
||||||
|
|
||||||
return context
|
return context
|
||||||
|
|
||||||
@@ -82,6 +83,7 @@ def get_animal_update_context(request, animal_id):
|
|||||||
)
|
)
|
||||||
context['return_url'] = reverse('entities:animal:animal_list')
|
context['return_url'] = reverse('entities:animal:animal_list')
|
||||||
context['form'] = form
|
context['form'] = form
|
||||||
|
context['modal_title'] = 'Editar animal'
|
||||||
|
|
||||||
return context
|
return context
|
||||||
|
|
||||||
@@ -120,7 +122,8 @@ def get_batch_create_context(request):
|
|||||||
'title': 'Novo Lote',
|
'title': 'Novo Lote',
|
||||||
'form': form,
|
'form': form,
|
||||||
'action_url': reverse('entities:animal:batch_create'),
|
'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
|
return context
|
||||||
@@ -153,7 +156,8 @@ def get_batch_update_context(request, batch_id):
|
|||||||
'entities:animal:batch_update',
|
'entities:animal:batch_update',
|
||||||
kwargs={'batch_id': batch_old.pk}
|
kwargs={'batch_id': batch_old.pk}
|
||||||
),
|
),
|
||||||
'form': form
|
'form': form,
|
||||||
|
'modal_title': 'Editar lote'
|
||||||
}
|
}
|
||||||
|
|
||||||
return context
|
return context
|
||||||
|
|||||||
@@ -70,7 +70,7 @@ def get_farm_home_context(request):
|
|||||||
batch_types = _get_farms_batch_types(request)
|
batch_types = _get_farms_batch_types(request)
|
||||||
|
|
||||||
context = {}
|
context = {}
|
||||||
context['title'] = selected_farm.name
|
context['title'] = 'Configurações da Fazenda'
|
||||||
context['selected_farm'] = selected_farm
|
context['selected_farm'] = selected_farm
|
||||||
context['farms'] = farms_with_counts
|
context['farms'] = farms_with_counts
|
||||||
context['allowed_users'] = allowed_users
|
context['allowed_users'] = allowed_users
|
||||||
@@ -86,6 +86,7 @@ def get_farm_create_context():
|
|||||||
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_home')
|
context['return_url'] = reverse('entities:farm:farm_home')
|
||||||
|
context['modal_title'] = 'Criar fazenda'
|
||||||
|
|
||||||
return context
|
return context
|
||||||
|
|
||||||
@@ -143,15 +144,19 @@ def delete_farm(request, farm_id):
|
|||||||
farm.is_active = False
|
farm.is_active = False
|
||||||
farm.save()
|
farm.save()
|
||||||
|
|
||||||
def update_consumption(request):
|
def update_metrics(request):
|
||||||
default_consumption = Decimal(request.POST.get('default-consume'))
|
default_consumption = Decimal(request.POST.get('default-consume'))
|
||||||
summer_consumption = Decimal(request.POST.get('summer-consume'))
|
summer_consumption = Decimal(request.POST.get('summer-consume'))
|
||||||
winter_consumption = Decimal(request.POST.get('winter-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 = _get_selected_farm(request)
|
||||||
selected_farm.default_consumption = default_consumption
|
selected_farm.default_consumption = default_consumption
|
||||||
selected_farm.summer_consumption = summer_consumption
|
selected_farm.summer_consumption = summer_consumption
|
||||||
selected_farm.winter_consumption = winter_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()
|
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
|
||||||
|
}
|
||||||
@@ -72,6 +72,11 @@ def farm_set(request, farm_id=None):
|
|||||||
farm_id = farm_id or request.POST.get("farm_id")
|
farm_id = farm_id or request.POST.get("farm_id")
|
||||||
next_url = request.POST.get("next")
|
next_url = request.POST.get("next")
|
||||||
|
|
||||||
|
print(farm_id)
|
||||||
|
|
||||||
|
if farm_id == '0':
|
||||||
|
return farm_deselect(request)
|
||||||
|
|
||||||
if not farm_id:
|
if not farm_id:
|
||||||
return redirect('entities:user:farm_select')
|
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)
|
farm = request.user.farms.get(id=farm_id)
|
||||||
request.session['selected_farm_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:
|
except request.user.farms.model.DoesNotExist:
|
||||||
return redirect('entities:user:farm_select')
|
return redirect('entities:user:farm_select')
|
||||||
|
|||||||
@@ -10,31 +10,6 @@
|
|||||||
color: #6c757d;
|
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 */
|
/* Containers */
|
||||||
.farm-config-container,
|
.farm-config-container,
|
||||||
.batch-type-config-container,
|
.batch-type-config-container,
|
||||||
@@ -51,7 +26,8 @@
|
|||||||
/* Headers */
|
/* Headers */
|
||||||
.allowed-users-header,
|
.allowed-users-header,
|
||||||
.batch-type-config-header,
|
.batch-type-config-header,
|
||||||
.section-header {
|
.section-header,
|
||||||
|
.available-farms-header {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
@@ -66,7 +42,8 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.allowed-users-header a,
|
.allowed-users-header a,
|
||||||
.batch-type-config-header a {
|
.batch-type-config-header a,
|
||||||
|
.available-farms-header a {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 6px;
|
gap: 6px;
|
||||||
@@ -77,6 +54,8 @@
|
|||||||
background-color: #198754;
|
background-color: #198754;
|
||||||
border-radius: 5px;
|
border-radius: 5px;
|
||||||
padding: 3px 6px;
|
padding: 3px 6px;
|
||||||
|
width: 120px;
|
||||||
|
justify-content: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Consumptions */
|
/* Consumptions */
|
||||||
@@ -93,11 +72,19 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.config-card-title {
|
.config-card-title {
|
||||||
margin-bottom: 8px;
|
display: flex;
|
||||||
font-size: .85rem;
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
padding: 0 3px;
|
||||||
|
margin-bottom: 5px;
|
||||||
|
font-size: 14px;
|
||||||
color: #6c757d;
|
color: #6c757d;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.metric-question {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
.config-card input {
|
.config-card input {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
padding: 10px;
|
padding: 10px;
|
||||||
@@ -112,12 +99,12 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.config-submit {
|
.config-submit {
|
||||||
flex: 1;
|
flex: .5;
|
||||||
display: flex;
|
display: flex;
|
||||||
}
|
}
|
||||||
|
|
||||||
.config-submit button {
|
.config-submit button {
|
||||||
width: 50%;
|
width: 100%;
|
||||||
height: 50%;
|
height: 50%;
|
||||||
margin: auto;
|
margin: auto;
|
||||||
border: none;
|
border: none;
|
||||||
@@ -196,7 +183,7 @@
|
|||||||
gap: 14px;
|
gap: 14px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.available-farms-title {
|
.available-farms-header {
|
||||||
grid-column: 1 / -1;
|
grid-column: 1 / -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -40,14 +40,14 @@
|
|||||||
padding: 0 var(--space-sm);
|
padding: 0 var(--space-sm);
|
||||||
border: none;
|
border: none;
|
||||||
border-radius: var(--border-radius-sm);
|
border-radius: var(--border-radius-sm);
|
||||||
background: var(--color-primary);
|
background: var(--color-neutral-btn);
|
||||||
color: var(--color-text-light);
|
color: var(--color-text-light);
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
transition: background var(--transition-speed);
|
transition: background var(--transition-speed);
|
||||||
}
|
}
|
||||||
|
|
||||||
.entity_search form button:hover {
|
.entity_search form button:hover {
|
||||||
background: var(--color-secondary);
|
background: var(--color-neutral-btn-hover);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* CLEAR FILTER */
|
/* CLEAR FILTER */
|
||||||
@@ -57,16 +57,15 @@
|
|||||||
gap: 4px;
|
gap: 4px;
|
||||||
height: 32px;
|
height: 32px;
|
||||||
padding: 0 var(--space-sm);
|
padding: 0 var(--space-sm);
|
||||||
background: var(--color-danger-light);
|
background: var(--color-bg-filter);
|
||||||
color: var(--color-danger);
|
color: var(--color-text-filter);
|
||||||
border-radius: var(--border-radius-sm);
|
border-radius: var(--border-radius-sm);
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
font-size: var(--font-sm);
|
font-size: var(--font-sm);
|
||||||
}
|
}
|
||||||
|
|
||||||
.btn-clear-filter:hover {
|
.btn-clear-filter:hover {
|
||||||
background: var(--color-danger);
|
background: var(--color-danger-btn-hover);
|
||||||
color: var(--color-text-light);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* NEW BUTTON */
|
/* NEW BUTTON */
|
||||||
@@ -75,7 +74,7 @@
|
|||||||
user-select: none;
|
user-select: none;
|
||||||
height: 32px;
|
height: 32px;
|
||||||
padding: 0 var(--space-md);
|
padding: 0 var(--space-md);
|
||||||
background: var(--color-success);
|
background: var(--color-plus-btn);
|
||||||
color: var(--color-text-light);
|
color: var(--color-text-light);
|
||||||
border-radius: var(--border-radius-sm);
|
border-radius: var(--border-radius-sm);
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
@@ -87,7 +86,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.new-entity-button:hover {
|
.new-entity-button:hover {
|
||||||
background: var(--color-success);
|
background: var(--color-plus-btn-hover);
|
||||||
opacity: 0.85;
|
opacity: 0.85;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -9,8 +9,7 @@
|
|||||||
|
|
||||||
/* Active rotation cards */
|
/* Active rotation cards */
|
||||||
.active-rotation-card {
|
.active-rotation-card {
|
||||||
min-width: 300px;
|
width: 300px;
|
||||||
max-width: 300px;
|
|
||||||
background: #ffffff;
|
background: #ffffff;
|
||||||
color: #212529;
|
color: #212529;
|
||||||
border: 1px solid #e9ecef;
|
border: 1px solid #e9ecef;
|
||||||
@@ -23,9 +22,15 @@
|
|||||||
transition: 0.2s ease;
|
transition: 0.2s ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.active-rotation-card {
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
|
|
||||||
.active-rotation-card:hover {
|
.active-rotation-card:hover {
|
||||||
transform: translateY(-5px);
|
transform: translateY(-5px);
|
||||||
box-shadow: 0 4px 10px rgba(0, 0, 0, 0.08);
|
box-shadow: 0 4px 10px rgba(0, 0, 0, 0.08);
|
||||||
|
border-color: var(--color-primary);
|
||||||
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
.active-rotation-card p {
|
.active-rotation-card p {
|
||||||
@@ -68,4 +73,8 @@
|
|||||||
align-items: center;
|
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%;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -40,3 +40,13 @@ document.addEventListener("DOMContentLoaded", () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
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;
|
justify-content: space-between;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
padding: 0 var(--space-lg);
|
padding: 0 var(--space-lg);
|
||||||
z-index: 950;
|
z-index: 20;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* SITE'S TITLE */
|
/* SITE'S TITLE */
|
||||||
@@ -43,7 +43,7 @@ body {
|
|||||||
|
|
||||||
.farm-select-container h1 {
|
.farm-select-container h1 {
|
||||||
font-size: var(--font-xl);
|
font-size: var(--font-xl);
|
||||||
color: #1f2937;
|
color: var(--login-background-primary);
|
||||||
margin-bottom: var(--space-xl);
|
margin-bottom: var(--space-xl);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -127,7 +127,7 @@ body {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/* NEW FARM BUTTON */
|
/* NEW FARM BUTTON */
|
||||||
.no-farms a {
|
.new-farm-btn {
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: var(--space-xs);
|
gap: var(--space-xs);
|
||||||
@@ -141,7 +141,7 @@ body {
|
|||||||
transition: background-color 0.2s;
|
transition: background-color 0.2s;
|
||||||
}
|
}
|
||||||
|
|
||||||
.no-farms a:hover {
|
.new-farm-btn:hover {
|
||||||
background-color: var(--color-new-farm-hover);
|
background-color: var(--color-new-farm-hover);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2,9 +2,11 @@
|
|||||||
{% load static %}
|
{% load static %}
|
||||||
|
|
||||||
{% block extra_css %}
|
{% block extra_css %}
|
||||||
|
{{ block.super }}
|
||||||
<link rel="stylesheet" href="{% static 'farm/css/home.css' %}">
|
<link rel="stylesheet" href="{% static 'farm/css/home.css' %}">
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
New - Farm
|
Nova Fazenda <br>
|
||||||
|
{% include 'components/form.html' %}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
@@ -10,40 +10,34 @@
|
|||||||
{% block content %}
|
{% block content %}
|
||||||
<div class="header">
|
<div class="header">
|
||||||
<div class="header-left">
|
<div class="header-left">
|
||||||
<h1 class="title">{{ title }}</h1>
|
<h1 class="title">{{ selected_farm.name }}</h1>
|
||||||
<p>Configurações da fazenda</p>
|
<p>Configurações da fazenda</p>
|
||||||
</div>
|
</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>
|
||||||
|
|
||||||
<div class="farm-config-container">
|
<div class="farm-config-container">
|
||||||
<h2 class="farm-config-title">Fatores de consumo</h2>
|
<h2 class="farm-config-title">Métricas da fazenda</h2>
|
||||||
<form action="{% url "entities:farm:consumption_update" %}" method="POST">
|
<form action="{% url "entities:farm:metrics_update" %}" method="POST">
|
||||||
{% csrf_token %}
|
{% csrf_token %}
|
||||||
<div class="config-card">
|
<div class="config-card" title="Percentual do peso do animal correspondente ao seu consumo diário">
|
||||||
<p class="config-card-title">Consumo padrão</p>
|
<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 }}">
|
<input type="number" name="default-consume" min="0.01" step="0.01" value="{{ selected_farm.default_consumption|unlocalize }}" required>
|
||||||
</div>
|
</div>
|
||||||
<div class="config-card">
|
<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</p>
|
<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 }}">
|
<input type="number" name="summer-consume" min="0.01" step="0.01" value="{{ selected_farm.summer_consumption|unlocalize }}" required>
|
||||||
</div>
|
</div>
|
||||||
<div class="config-card">
|
<div class="config-card" title="Variação do consumo no inverno (1,00 = sem variação)">
|
||||||
<p class="config-card-title">Consumo no inverno</p>
|
<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 }}">
|
<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>
|
||||||
<div class="config-submit">
|
<div class="config-submit">
|
||||||
<button type="submit">Salvar</button>
|
<button type="submit">Salvar</button>
|
||||||
@@ -54,8 +48,11 @@
|
|||||||
<div class="batch-type-config-container">
|
<div class="batch-type-config-container">
|
||||||
<div class="batch-type-config-header">
|
<div class="batch-type-config-header">
|
||||||
<h2 class="batch-type-title">Tipos de lote</h2>
|
<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>
|
</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 %}
|
{% for type in batch_types %}
|
||||||
<div class="batch-type-card">
|
<div class="batch-type-card">
|
||||||
<div class="batch-type-left">
|
<div class="batch-type-left">
|
||||||
@@ -87,7 +84,10 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="available-farms-container">
|
<div class="available-farms-container">
|
||||||
|
<div class="available-farms-header">
|
||||||
<h2 class="available-farms-title">Suas fazendas</h2>
|
<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 %}
|
{% for farm in farms %}
|
||||||
<div class="available-farm-card">
|
<div class="available-farm-card">
|
||||||
<p class="available-farm-name">{{ farm.name }}</p>
|
<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">
|
<button type="submit">
|
||||||
<i class="fa-solid fa-magnifying-glass"></i>
|
<i class="fa-solid fa-magnifying-glass"></i>
|
||||||
</button>
|
</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>
|
<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>
|
||||||
</div>
|
</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>
|
<p>Movimentações em aberto</p>
|
||||||
<div class="open-rotations-container">
|
<div class="open-rotations-container">
|
||||||
{% for obj in open_rotations %}
|
{% 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">
|
<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>
|
||||||
|
|
||||||
<div class="duration">
|
<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>
|
||||||
|
|
||||||
<div class="animals">
|
<div class="animals">
|
||||||
@@ -39,12 +39,12 @@
|
|||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block table %}
|
{% block table %}
|
||||||
<table>
|
<table id="table">
|
||||||
<tr>
|
<tr>
|
||||||
{% for col in columns %}
|
{% for col in columns %}
|
||||||
<th>{{ col.label }}</th>
|
<th>{{ col.label }}</th>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
<th>Excluir</th>
|
{% comment %} <th>Excluir</th> {% endcomment %}
|
||||||
</tr>
|
</tr>
|
||||||
|
|
||||||
{% for obj in object_list %}
|
{% for obj in object_list %}
|
||||||
@@ -56,14 +56,14 @@
|
|||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
|
||||||
<!-- Delete -->
|
<!-- Delete -->
|
||||||
<td>
|
{% comment %} <td>
|
||||||
<form method="POST" action="{% url delete_url obj.id %}">
|
<form method="POST" action="{% url delete_url obj.id %}">
|
||||||
{% csrf_token %}
|
{% csrf_token %}
|
||||||
<button type="submit" class="btn-icon-trash">
|
<button type="submit" class="btn-icon-trash">
|
||||||
<i class="fa-solid fa-trash"></i>
|
<i class="fa-solid fa-trash"></i>
|
||||||
</button>
|
</button>
|
||||||
</form>
|
</form>
|
||||||
</td>
|
</td> {% endcomment %}
|
||||||
</tr>
|
</tr>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</table>
|
</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 %}
|
||||||
@@ -17,7 +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('metrics/update', views.metrics_update, name='metrics_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'),
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ app_name = 'rotation'
|
|||||||
|
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
path('', RotationListView.as_view(), name='rotation_home'),
|
path('', RotationListView.as_view(), name='rotation_home'),
|
||||||
|
|
||||||
path('create/', views.rotation_create, name='rotation_create'),
|
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'),
|
||||||
]
|
]
|
||||||
@@ -9,26 +9,26 @@ from ..services import animal as services
|
|||||||
def animal_create(request):
|
def animal_create(request):
|
||||||
context = services.get_animal_create_context()
|
context = services.get_animal_create_context()
|
||||||
if request.method == 'GET':
|
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 request.method == 'POST':
|
||||||
if services.create_animal(request):
|
if services.create_animal(request):
|
||||||
return redirect('entities:animal:animal_list')
|
return redirect('entities:animal:animal_list')
|
||||||
context['error'] = "Não foi possível registrar o animal"
|
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
|
@farm_required
|
||||||
def animal_update(request, animal_id):
|
def animal_update(request, animal_id):
|
||||||
context = services.get_animal_update_context(request, animal_id)
|
context = services.get_animal_update_context(request, animal_id)
|
||||||
if request.method == 'GET':
|
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 request.method == 'POST':
|
||||||
if services.update_animal(request, animal_id):
|
if services.update_animal(request, animal_id):
|
||||||
return redirect('entities:animal:animal_list')
|
return redirect('entities:animal:animal_list')
|
||||||
context['error'] = "Não foi possível editar o animal"
|
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
|
@farm_required
|
||||||
@@ -48,26 +48,26 @@ def animal_delete(request, animal_id):
|
|||||||
def batch_create(request):
|
def batch_create(request):
|
||||||
context = services.get_batch_create_context(request)
|
context = services.get_batch_create_context(request)
|
||||||
if request.method == 'GET':
|
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 request.method == 'POST':
|
||||||
if services.create_batch(request):
|
if services.create_batch(request):
|
||||||
return redirect('entities:animal:batch_list')
|
return redirect('entities:animal:batch_list')
|
||||||
context['error'] = "Não foi possível criar o lote"
|
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
|
@farm_required
|
||||||
def batch_update(request, batch_id):
|
def batch_update(request, batch_id):
|
||||||
context = services.get_batch_update_context(request, batch_id)
|
context = services.get_batch_update_context(request, batch_id)
|
||||||
if request.method == 'GET':
|
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 request.method == 'POST':
|
||||||
if services.update_batch(request, batch_id):
|
if services.update_batch(request, batch_id):
|
||||||
return redirect('entities:animal:batch_list')
|
return redirect('entities:animal:batch_list')
|
||||||
context['error'] = "Não foi possível editar o lote"
|
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
|
@farm_required
|
||||||
@@ -86,25 +86,25 @@ def batch_delete(request, batch_id):
|
|||||||
def batchtype_create(request):
|
def batchtype_create(request):
|
||||||
context = services.get_batchtype_create_context(request)
|
context = services.get_batchtype_create_context(request)
|
||||||
if request.method == 'GET':
|
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 request.method == 'POST':
|
||||||
if services.create_batchtype(request):
|
if services.create_batchtype(request):
|
||||||
return redirect('entities:farm:farm_home')
|
return redirect('entities:farm:farm_home')
|
||||||
context['error'] = "Não foi possível criar o tipo de lote"
|
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
|
@farm_required
|
||||||
def batchtype_update(request, batchtype_id):
|
def batchtype_update(request, batchtype_id):
|
||||||
context = services.get_batchtype_update_context(request, batchtype_id)
|
context = services.get_batchtype_update_context(request, batchtype_id)
|
||||||
if request.method == 'GET':
|
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 request.method == 'POST':
|
||||||
if services.update_batchtype(request, batchtype_id):
|
if services.update_batchtype(request, batchtype_id):
|
||||||
return redirect('entities:farm:farm_home')
|
return redirect('entities:farm:farm_home')
|
||||||
context['error'] = "Não foi possível editar o tipo de lote"
|
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
|
@farm_required
|
||||||
def batchtype_delete(request, batchtype_id):
|
def batchtype_delete(request, batchtype_id):
|
||||||
|
|||||||
@@ -9,17 +9,16 @@ def farm_home(request):
|
|||||||
return render(request, 'farm/farm_home.html', context)
|
return render(request, 'farm/farm_home.html', context)
|
||||||
|
|
||||||
|
|
||||||
@farm_required
|
|
||||||
def farm_create(request):
|
def farm_create(request):
|
||||||
context = services.get_farm_create_context()
|
context = services.get_farm_create_context()
|
||||||
if request.method == 'GET':
|
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 request.method == 'POST':
|
||||||
if services.create_farm(request):
|
if services.create_farm(request):
|
||||||
return redirect('entities:farm:farm_home')
|
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, 'components/form_modal.html', context)
|
||||||
|
|
||||||
@farm_required
|
@farm_required
|
||||||
def farm_update(request, farm_id):
|
def farm_update(request, farm_id):
|
||||||
@@ -42,11 +41,12 @@ def farm_delete(request, farm_id):
|
|||||||
if request.method == 'POST':
|
if request.method == 'POST':
|
||||||
services.delete_farm(request, farm_id)
|
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':
|
if request.method=='POST':
|
||||||
services.update_consumption(request)
|
services.update_metrics(request)
|
||||||
|
|
||||||
return redirect('entities:farm:farm_home')
|
return redirect('entities:farm:farm_home')
|
||||||
|
|
||||||
|
|||||||
@@ -9,8 +9,9 @@ class BatchListView(BaseListView):
|
|||||||
|
|
||||||
columns = [
|
columns = [
|
||||||
{"field": "identifier", "label": "Identificador"},
|
{"field": "identifier", "label": "Identificador"},
|
||||||
{"field": "animal_cathgory", "label": "Categoria"},
|
{"field": "animal_category", "label": "Categoria"},
|
||||||
{"field": "batch_type", "label": "Tipo"},
|
{"field": "batch_type", "label": "Tipo"},
|
||||||
|
{"field": "total_ugm", "label": "UGM"},
|
||||||
]
|
]
|
||||||
|
|
||||||
create_url = "entities:animal:batch_create"
|
create_url = "entities:animal:batch_create"
|
||||||
|
|||||||
@@ -9,8 +9,8 @@ class PaddockListView(BaseListView):
|
|||||||
columns = [
|
columns = [
|
||||||
{"field": "paddock_number", "label": "Número"},
|
{"field": "paddock_number", "label": "Número"},
|
||||||
{"field": "occupied_as_text", "label": "Ocupado"},
|
{"field": "occupied_as_text", "label": "Ocupado"},
|
||||||
{"field": "area", "label": "Área"},
|
{"field": "area", "label": "Área (m²)"},
|
||||||
{"field": "load_capacity", "label": "Capacidade"},
|
# {"field": "load_capacity", "label": "Capacidade"},
|
||||||
]
|
]
|
||||||
|
|
||||||
create_url = "entities:farm:paddock_create"
|
create_url = "entities:farm:paddock_create"
|
||||||
|
|||||||
@@ -24,11 +24,11 @@ class RotationListView(BaseListView):
|
|||||||
details_url = "entities:rotation:rotation_home" # Trocar pela url de detalhe
|
details_url = "entities:rotation:rotation_home" # Trocar pela url de detalhe
|
||||||
|
|
||||||
filter_fields = ["id", "batch", "paddock"]
|
filter_fields = ["id", "batch", "paddock"]
|
||||||
order_by = ["id"] # Orders from oldest to newest
|
order_by = ["-id"]
|
||||||
|
|
||||||
def get_queryset(self):
|
def get_queryset(self):
|
||||||
queryset = super().get_queryset()
|
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)
|
return super().get_queryset().filter(is_active=True, paddock__farm=self.selected_farm)
|
||||||
|
|
||||||
|
|||||||
@@ -1,14 +1,18 @@
|
|||||||
from django.shortcuts import render
|
from django.shortcuts import render
|
||||||
from core.decorators import farm_required
|
from core.decorators import farm_required
|
||||||
|
|
||||||
@farm_required
|
from ..services import rotation as services
|
||||||
def rotation_home(request):
|
|
||||||
return render(request, 'rotation/rotation_home.html')
|
|
||||||
|
|
||||||
@farm_required
|
@farm_required
|
||||||
def rotation_create(request):
|
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
|
@farm_required
|
||||||
def rotation_delete(request):
|
def rotation_view_more(request, rotation_id):
|
||||||
return render(request, 'rotation/rotation_delete.html')
|
context = services.get_view_more_context(request, rotation_id)
|
||||||
|
return render(request, 'rotation/rotation_view_more.html', context)
|
||||||
@@ -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'})
|
||||||
|
)
|
||||||
@@ -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
|
||||||
@@ -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 %}
|
||||||
@@ -1,13 +1,11 @@
|
|||||||
from django.urls import path
|
from django.urls import path
|
||||||
from django.http import HttpResponse
|
|
||||||
from django.contrib.auth.decorators import login_required
|
from . import views
|
||||||
|
|
||||||
app_name = 'report'
|
app_name = 'report'
|
||||||
|
|
||||||
@login_required(login_url='/user/login/')
|
|
||||||
def home(request):
|
|
||||||
return HttpResponse("Home app page")
|
|
||||||
|
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
path('', home),
|
path('', views.dashboard, name='dashboard'),
|
||||||
|
path("request/", views.request_report, name="request_report"),
|
||||||
|
path('report/', views.create_report, name='create_report'),
|
||||||
]
|
]
|
||||||
@@ -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
|
||||||
|
)
|
||||||
@@ -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)
|
||||||
@@ -26,6 +26,7 @@ INSTALLED_APPS = [
|
|||||||
'django.contrib.sessions',
|
'django.contrib.sessions',
|
||||||
'django.contrib.messages',
|
'django.contrib.messages',
|
||||||
'django.contrib.staticfiles',
|
'django.contrib.staticfiles',
|
||||||
|
'import_export',
|
||||||
# My apps
|
# My apps
|
||||||
'core',
|
'core',
|
||||||
'entities',
|
'entities',
|
||||||
|
|||||||
Binary file not shown.
Reference in new issue
Block a user