primeira versão
This commit is contained in:
6
models/__init__.py
Normal file
6
models/__init__.py
Normal file
@@ -0,0 +1,6 @@
|
||||
from . import plano
|
||||
from . import diario
|
||||
from . import crianca
|
||||
from . import res_partner
|
||||
from . import financeiro
|
||||
from . import report_parser
|
||||
160
models/crianca.py
Normal file
160
models/crianca.py
Normal file
@@ -0,0 +1,160 @@
|
||||
# ARQUIVO: ./models/crianca.py
|
||||
from odoo import models, fields, api, _
|
||||
from datetime import date
|
||||
|
||||
class RecreacaoCrianca(models.Model):
|
||||
_name = 'recreacao.crianca'
|
||||
_description = 'Aluno / Criança'
|
||||
_inherit = ['mail.thread'] # Permite o log de mensagens e chat no rodapé
|
||||
|
||||
# --- DADOS PESSOAIS ---
|
||||
name = fields.Char(string='Nome da Criança', required=True, tracking=True)
|
||||
foto = fields.Binary(string="Foto")
|
||||
data_nascimento = fields.Date(string='Data de Nascimento')
|
||||
idade = fields.Char(compute='_compute_idade', string='Idade')
|
||||
|
||||
# --- FAMÍLIA ---
|
||||
pai_id = fields.Many2one('res.partner', string='Pai', domain="[('is_company', '=', False)]")
|
||||
mae_id = fields.Many2one('res.partner', string='Mãe', domain="[('is_company', '=', False)]")
|
||||
responsavel_financeiro_id = fields.Many2one('res.partner', string='Responsável Financeiro', required=True)
|
||||
|
||||
# --- SAÚDE ---
|
||||
tem_alergia = fields.Boolean(string="Tem Alergia?", tracking=True)
|
||||
alergias = fields.Text(string='Quais Alergias?')
|
||||
toma_remedio = fields.Boolean(string="Toma Remédio?")
|
||||
medicamentos = fields.Text(string='Quais Medicamentos?')
|
||||
horario_medicacao = fields.Char(string='Horários')
|
||||
observacoes_saude = fields.Text(string='Obs. Médicas')
|
||||
|
||||
# --- PLANO & FINANCEIRO ---
|
||||
plano_id = fields.Many2one('recreacao.plano', string='Plano Contratado', tracking=True)
|
||||
valor_plano = fields.Float(related='plano_id.valor', string='Valor do Plano (R$)', readonly=True)
|
||||
|
||||
# Novo relacionamento com o Financeiro Simplificado
|
||||
financeiro_ids = fields.One2many('recreacao.financeiro', 'crianca_id', string='Histórico de Pagamentos')
|
||||
|
||||
# --- HISTÓRICO DE FREQUÊNCIA ---
|
||||
diario_ids = fields.One2many('recreacao.diario', 'crianca_id', string='Histórico Diário')
|
||||
|
||||
# --- CONTROLE DE STATUS (Lógica Corrigida) ---
|
||||
status_dia = fields.Selection([
|
||||
('ausente', 'Ausente'),
|
||||
('presente', 'Na Creche'),
|
||||
('finalizado', 'Já Saiu')
|
||||
], compute='_compute_status_dia', string='Status Hoje', store=False)
|
||||
|
||||
@api.depends('diario_ids')
|
||||
def _compute_status_dia(self):
|
||||
hoje = fields.Date.today()
|
||||
for rec in self:
|
||||
# Busca o ÚLTIMO movimento de entrada ou saída de hoje
|
||||
ultimo_movimento = self.env['recreacao.diario'].search([
|
||||
('crianca_id', '=', rec.id),
|
||||
('data', '=', hoje),
|
||||
('tipo', 'in', ['entrada', 'saida'])
|
||||
], order='create_date desc', limit=1)
|
||||
|
||||
if not ultimo_movimento:
|
||||
rec.status_dia = 'ausente'
|
||||
elif ultimo_movimento.tipo == 'entrada':
|
||||
rec.status_dia = 'presente'
|
||||
elif ultimo_movimento.tipo == 'saida':
|
||||
rec.status_dia = 'finalizado'
|
||||
else:
|
||||
rec.status_dia = 'ausente'
|
||||
|
||||
# --- AÇÕES DE FREQUÊNCIA ---
|
||||
def action_marcar_entrada(self):
|
||||
"""Botão Verde do Kanban"""
|
||||
for rec in self:
|
||||
self.env['recreacao.diario'].create({
|
||||
'crianca_id': rec.id,
|
||||
'tipo': 'entrada',
|
||||
'descricao': 'Chegou na recreação.'
|
||||
})
|
||||
# Força atualização da interface
|
||||
rec._compute_status_dia()
|
||||
|
||||
def action_marcar_saida(self):
|
||||
"""Botão Vermelho do Kanban"""
|
||||
for rec in self:
|
||||
self.env['recreacao.diario'].create({
|
||||
'crianca_id': rec.id,
|
||||
'tipo': 'saida',
|
||||
'descricao': 'Foi embora.'
|
||||
})
|
||||
rec._compute_status_dia()
|
||||
|
||||
# --- AÇÕES DE UTILIDADE ---
|
||||
def action_abrir_whatsapp(self):
|
||||
self.ensure_one()
|
||||
if not self.responsavel_financeiro_id.phone and not self.responsavel_financeiro_id.mobile:
|
||||
return {
|
||||
'type': 'ir.actions.client',
|
||||
'tag': 'display_notification',
|
||||
'params': {'title': 'Erro', 'message': 'Responsável sem telefone cadastrado!', 'type': 'danger'}
|
||||
}
|
||||
|
||||
numero = self.responsavel_financeiro_id.mobile or self.responsavel_financeiro_id.phone
|
||||
phone = ''.join(filter(str.isdigit, numero))
|
||||
msg = f"Olá, gostaria de falar sobre: {self.name}"
|
||||
return {
|
||||
'type': 'ir.actions.act_url',
|
||||
'url': f"https://api.whatsapp.com/send?phone={phone}&text={msg}",
|
||||
'target': 'new',
|
||||
}
|
||||
|
||||
def action_abrir_historico(self):
|
||||
"""Abre a lista filtrada do diário dessa criança"""
|
||||
return {
|
||||
'name': f'Histórico: {self.name}',
|
||||
'type': 'ir.actions.act_window',
|
||||
'res_model': 'recreacao.diario',
|
||||
'domain': [('crianca_id', '=', self.id)],
|
||||
'view_mode': 'tree,form',
|
||||
}
|
||||
|
||||
# --- NOVA AÇÃO FINANCEIRA (SIMPLIFICADA) ---
|
||||
def action_gerar_cobranca(self):
|
||||
"""Gera um lançamento de Receita no Financeiro Simplificado"""
|
||||
self.ensure_one()
|
||||
if not self.plano_id:
|
||||
return {
|
||||
'type': 'ir.actions.client',
|
||||
'tag': 'display_notification',
|
||||
'params': {'title': 'Atenção', 'message': 'Selecione um Plano antes de gerar cobrança.', 'type': 'warning'}
|
||||
}
|
||||
|
||||
# Cria a cobrança no novo modelo
|
||||
self.env['recreacao.financeiro'].create({
|
||||
'name': f"Mensalidade: {self.name} - {fields.Date.today().strftime('%m/%Y')}",
|
||||
'tipo': 'receita',
|
||||
'valor': self.valor_plano,
|
||||
'partner_id': self.responsavel_financeiro_id.id,
|
||||
'crianca_id': self.id,
|
||||
'data_vencimento': fields.Date.today(),
|
||||
'status': 'pendente'
|
||||
})
|
||||
|
||||
return {
|
||||
'type': 'ir.actions.client',
|
||||
'tag': 'display_notification',
|
||||
'params': {
|
||||
'title': 'Sucesso',
|
||||
'message': f'Cobrança de R$ {self.valor_plano} gerada no Financeiro!',
|
||||
'type': 'success',
|
||||
'sticky': False,
|
||||
}
|
||||
}
|
||||
|
||||
# --- CÁLCULO DE IDADE ---
|
||||
@api.depends('data_nascimento')
|
||||
def _compute_idade(self):
|
||||
for rec in self:
|
||||
if rec.data_nascimento:
|
||||
today = date.today()
|
||||
# Lógica precisa de cálculo de idade (considera dia e mês)
|
||||
years = today.year - rec.data_nascimento.year - ((today.month, today.day) < (rec.data_nascimento.month, rec.data_nascimento.day))
|
||||
rec.idade = f"{years} anos"
|
||||
else:
|
||||
rec.idade = "Sem data"
|
||||
30
models/diario.py
Normal file
30
models/diario.py
Normal file
@@ -0,0 +1,30 @@
|
||||
from odoo import models, fields, api
|
||||
from datetime import datetime
|
||||
import pytz
|
||||
|
||||
class RecreacaoDiario(models.Model):
|
||||
_name = 'recreacao.diario'
|
||||
_description = 'Diário de Classe'
|
||||
_order = 'data desc, create_date desc' # Ordena pelo mais recente
|
||||
|
||||
data = fields.Date(string='Data', required=True, default=fields.Date.context_today)
|
||||
|
||||
# Campo para guardar a hora exata (para o relatório)
|
||||
hora_registro = fields.Char(string='Hora', default=lambda self: self._get_hora_atual())
|
||||
|
||||
crianca_id = fields.Many2one('recreacao.crianca', string='Criança', required=True)
|
||||
|
||||
tipo = fields.Selection([
|
||||
('entrada', '🟢 Entrada / Chegada'),
|
||||
('saida', '🔴 Saída / Foi Embora'),
|
||||
('ocorrencia', '⚠️ Ocorrência / Incidente'),
|
||||
('saude', '💊 Medicamento / Saúde'),
|
||||
('rotina', '📝 Rotina / Anotação')
|
||||
], string='Tipo', required=True, default='rotina')
|
||||
|
||||
descricao = fields.Text(string='Observações')
|
||||
|
||||
def _get_hora_atual(self):
|
||||
# Pega a hora atual no fuso de SP (Hardcoded para facilitar)
|
||||
tz = pytz.timezone('America/Sao_Paulo')
|
||||
return datetime.now(tz).strftime('%H:%M')
|
||||
52
models/financeiro.py
Normal file
52
models/financeiro.py
Normal file
@@ -0,0 +1,52 @@
|
||||
# ARQUIVO: ./models/financeiro.py
|
||||
from odoo import models, fields, api
|
||||
|
||||
class RecreacaoFinanceiro(models.Model):
|
||||
_name = 'recreacao.financeiro'
|
||||
_description = 'Movimentação Financeira'
|
||||
_order = 'data_vencimento desc'
|
||||
|
||||
name = fields.Char(string='Descrição', required=True)
|
||||
|
||||
tipo = fields.Selection([
|
||||
('receita', '🟢 Receita (Entrada)'),
|
||||
('despesa', '🔴 Despesa (Saída)')
|
||||
], string='Tipo', required=True, default='receita')
|
||||
|
||||
valor = fields.Float(string='Valor (R$)', required=True)
|
||||
|
||||
# --- NOVO CAMPO PARA CONTABILIDADE ---
|
||||
forma_pagamento = fields.Selection([
|
||||
('pix', 'PIX'),
|
||||
('dinheiro', 'Dinheiro'),
|
||||
('cartao', 'Cartão'),
|
||||
('boleto', 'Boleto'),
|
||||
('transferencia', 'TED/DOC')
|
||||
], string='Forma de Pagto')
|
||||
# -------------------------------------
|
||||
|
||||
data_vencimento = fields.Date(string='Vencimento', required=True, default=fields.Date.context_today)
|
||||
data_pagamento = fields.Date(string='Data Pagamento')
|
||||
|
||||
status = fields.Selection([
|
||||
('pendente', 'Pendente'),
|
||||
('pago', 'Pago'),
|
||||
('cancelado', 'Cancelado')
|
||||
], string='Status', default='pendente', tracking=True)
|
||||
|
||||
partner_id = fields.Many2one('res.partner', string='Pessoa/Fornecedor')
|
||||
crianca_id = fields.Many2one('recreacao.crianca', string='Referente ao Aluno')
|
||||
|
||||
def action_pagar(self):
|
||||
for rec in self:
|
||||
rec.status = 'pago'
|
||||
rec.data_pagamento = fields.Date.today()
|
||||
|
||||
def action_cancelar(self):
|
||||
for rec in self:
|
||||
rec.status = 'cancelado'
|
||||
|
||||
def action_redefinir(self):
|
||||
for rec in self:
|
||||
rec.status = 'pendente'
|
||||
rec.data_pagamento = False
|
||||
19
models/plano.py
Normal file
19
models/plano.py
Normal file
@@ -0,0 +1,19 @@
|
||||
from odoo import models, fields
|
||||
|
||||
class RecreacaoPlano(models.Model):
|
||||
_name = 'recreacao.plano'
|
||||
_description = 'Planos de Cobrança'
|
||||
|
||||
name = fields.Char(string='Nome do Plano', required=True)
|
||||
|
||||
# NOVO: Tipo de período
|
||||
tipo_cobranca = fields.Selection([
|
||||
('mensal', 'Mensalidade (Recorrente)'),
|
||||
('diaria', 'Diária / Avulso')
|
||||
], string='Tipo de Cobrança', default='mensal', required=True)
|
||||
|
||||
valor = fields.Float(string='Valor (R$)', required=True)
|
||||
produto_id = fields.Many2one('product.product', string='Produto/Serviço Vinculado')
|
||||
|
||||
horario_inicio = fields.Float(string='Entrada')
|
||||
horario_fim = fields.Float(string='Saída')
|
||||
43
models/report_parser.py
Normal file
43
models/report_parser.py
Normal file
@@ -0,0 +1,43 @@
|
||||
from odoo import models, api
|
||||
|
||||
class RelatorioFinanceiroParser(models.AbstractModel):
|
||||
_name = 'report.plugin_recre.template_financeiro_mensal'
|
||||
_description = 'Lógica do Relatório Financeiro'
|
||||
|
||||
@api.model
|
||||
def _get_report_values(self, docids, data=None):
|
||||
# 1. Recupera o Wizard para poder usar "docs" no template
|
||||
# Isso corrige o erro KeyError: 'docs'
|
||||
docs = self.env['recreacao.financeiro.wizard'].browse(docids)
|
||||
|
||||
# Se os dados não vierem no 'data', pega do wizard (docs)
|
||||
start = data.get('data_inicio') or docs.data_inicio
|
||||
end = data.get('data_fim') or docs.data_fim
|
||||
|
||||
# 2. Busca os movimentos no banco de dados
|
||||
movimentos = self.env['recreacao.financeiro'].search([
|
||||
('data_vencimento', '>=', start),
|
||||
('data_vencimento', '<=', end),
|
||||
('status', '!=', 'cancelado') # Ignora cancelados
|
||||
], order='data_vencimento asc')
|
||||
|
||||
# 3. Separa e Calcula
|
||||
entradas = movimentos.filtered(lambda r: r.tipo == 'receita')
|
||||
saidas = movimentos.filtered(lambda r: r.tipo == 'despesa')
|
||||
|
||||
total_entradas = sum(entradas.mapped('valor'))
|
||||
total_saidas = sum(saidas.mapped('valor'))
|
||||
|
||||
# 4. Retorna o dicionário completo para o XML
|
||||
return {
|
||||
'doc_ids': docids,
|
||||
'doc_model': 'recreacao.financeiro.wizard',
|
||||
'docs': docs, # <--- AQUI ESTAVA FALTANDO!
|
||||
'data_inicio': start,
|
||||
'data_fim': end,
|
||||
'entradas': entradas,
|
||||
'saidas': saidas,
|
||||
'total_entradas': total_entradas,
|
||||
'total_saidas': total_saidas,
|
||||
'saldo_final': total_entradas - total_saidas,
|
||||
}
|
||||
11
models/res_partner.py
Normal file
11
models/res_partner.py
Normal file
@@ -0,0 +1,11 @@
|
||||
from odoo import models, fields
|
||||
|
||||
class ResPartner(models.Model):
|
||||
_inherit = 'res.partner'
|
||||
|
||||
# CORREÇÃO AQUI: O segundo parâmetro deve ser igual ao nome do campo na criança
|
||||
crianca_ids = fields.One2many(
|
||||
'recreacao.crianca',
|
||||
'responsavel_financeiro_id',
|
||||
string='Filhos (Responsável Fin.)'
|
||||
)
|
||||
Reference in New Issue
Block a user