============== ESTRUTURA DO PROJETO ============== . ./__manifest__.py ./models ./models/financeiro.py ./models/plano.py ./models/__init__.py ./models/crianca.py ./models/diario.py ./models/res_partner.py ./models/report_parser.py ./__init__.py ./reports ./reports/report.xml ./reports/financeiro_report.xml ./reports/historico_template.xml ./reports/financeiro_template.xml ./static ./static/manifest.json ./static/img ./static/img/favicon.png ./static/img/icon_192.png ./static/img/icon_512.png ./static/img/favicon.ico ./wizard ./wizard/financeiro_wizard.py ./wizard/__init__.py ./security ./security/ir.model.access.csv ./views ./views/financeiro_wizard_view.xml ./views/web_layout.xml ./views/crianca_view.xml ./views/plano_view.xml ./views/res_partner_view.xml ./views/diario_view.xml ./views/financeiro_view.xml ================================================================== ARQUIVO: ./__manifest__.py ================================================================== # ARQUIVO: ./__manifest__.py { 'name': 'Gestão de Recreação (Lite)', 'version': '3.0', 'category': 'Services', 'summary': 'Alunos, Pais e Financeiro Simples', 'depends': ['base', 'contacts'], # REMOVIDO 'account' 'data': [ 'security/ir.model.access.csv', 'views/plano_view.xml', 'views/diario_view.xml', 'views/crianca_view.xml', 'views/financeiro_view.xml', 'views/res_partner_view.xml', 'views/web_layout.xml', 'views/financeiro_wizard_view.xml', # NOVO 'reports/report.xml', 'reports/historico_template.xml', 'reports/financeiro_template.xml', # NOVO ], 'application': True, 'license': 'LGPL-3', } ================================================================== ARQUIVO: ./models/financeiro.py ================================================================== # 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) data_vencimento = fields.Date(string='Vencimento', required=True, default=fields.Date.context_today) data_pagamento = fields.Date(string='Data Pagamento') # Status do pagamento status = fields.Selection([ ('pendente', 'Pendente'), ('pago', 'Pago'), ('cancelado', 'Cancelado') ], string='Status', default='pendente', tracking=True) # Relacionamentos 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 ================================================================== ARQUIVO: ./models/plano.py ================================================================== 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') ================================================================== ARQUIVO: ./models/__init__.py ================================================================== from . import plano from . import diario from . import crianca from . import res_partner from . import financeiro from . import report_parser ================================================================== ARQUIVO: ./models/crianca.py ================================================================== # 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" ================================================================== ARQUIVO: ./models/diario.py ================================================================== 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') ================================================================== ARQUIVO: ./models/res_partner.py ================================================================== 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.)' ) ================================================================== ARQUIVO: ./models/report_parser.py ================================================================== 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): start = data.get('data_inicio') end = data.get('data_fim') # Busca todos os movimentos no período movimentos = self.env['recreacao.financeiro'].search([ ('data_vencimento', '>=', start), ('data_vencimento', '<=', end), # Opcional: filtrar só os pagos? Para fluxo de caixa real, sim. # ('status', '=', 'pago') ], order='data_vencimento asc') 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')) return { 'doc_ids': docids, 'doc_model': 'recreacao.financeiro', 'data_inicio': start, 'data_fim': end, 'entradas': entradas, 'saidas': saidas, 'total_entradas': total_entradas, 'total_saidas': total_saidas, 'saldo_final': total_entradas - total_saidas, } ================================================================== ARQUIVO: ./__init__.py ================================================================== from . import models from . import wizard ================================================================== ARQUIVO: ./reports/report.xml ================================================================== Histórico Mensal recreacao.crianca qweb-pdf plugin_recre.report_historico_aluno plugin_recre.report_historico_aluno report ================================================================== ARQUIVO: ./reports/historico_template.xml ================================================================== ================================================================== ARQUIVO: ./reports/financeiro_template.xml ================================================================== Fluxo de Caixa Mensal recreacao.financeiro.wizard qweb-pdf plugin_recre.template_financeiro_mensal plugin_recre.template_financeiro_mensal ================================================================== ARQUIVO: ./static/manifest.json ================================================================== { "name": "CRM Dente de leão", "short_name": "Recreação", "start_url": "/web", "display": "standalone", "background_color": "#ffffff", "theme_color": "#714B67", "scope": "/", "icons": [ { "src": "/plugin_recre/static/img/icon_192.png", "sizes": "192x192", "type": "image/png" }, { "src": "/plugin_recre/static/img/icon_512.png", "sizes": "512x512", "type": "image/png" } ] } ================================================================== ARQUIVO: ./wizard/financeiro_wizard.py ================================================================== from odoo import models, fields, api class FinanceiroRelatorioWizard(models.TransientModel): _name = 'recreacao.financeiro.wizard' _description = 'Wizard de Relatório Financeiro' data_inicio = fields.Date(string='Data Inicial', required=True, default=lambda self: fields.Date.today().replace(day=1)) data_fim = fields.Date(string='Data Final', required=True, default=fields.Date.today()) def action_gerar_pdf(self): # Passa as datas para o relatório data = { 'data_inicio': self.data_inicio, 'data_fim': self.data_fim, } return self.env.ref('plugin_recre.action_report_financeiro_mensal').report_action(self, data=data) ================================================================== ARQUIVO: ./wizard/__init__.py ================================================================== from . import financeiro_wizard ================================================================== ARQUIVO: ./security/ir.model.access.csv ================================================================== id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink access_recreacao_plano,recreacao.plano,model_recreacao_plano,base.group_user,1,1,1,1 access_recreacao_diario,recreacao.diario,model_recreacao_diario,base.group_user,1,1,1,1 access_recreacao_crianca,recreacao.crianca,model_recreacao_crianca,base.group_user,1,1,1,1 access_recreacao_financeiro,recreacao.financeiro,model_recreacao_financeiro,base.group_user,1,1,1,1 access_recreacao_financeiro_wizard,recreacao.financeiro.wizard,model_recreacao_financeiro_wizard,base.group_user,1,1,1,1 ================================================================== ARQUIVO: ./views/financeiro_wizard_view.xml ================================================================== recreacao.financeiro.wizard.form recreacao.financeiro.wizard
Relatório Mensal recreacao.financeiro.wizard form new
================================================================== ARQUIVO: ./views/web_layout.xml ================================================================== ================================================================== ARQUIVO: ./views/crianca_view.xml ================================================================== recreacao.crianca.kanban recreacao.crianca
Foto
Sem Foto
  • ⚠️ ALÉRGICO
================================================================== ARQUIVO: ./views/plano_view.xml ================================================================== recreacao.plano.tree recreacao.plano Planos e Preços recreacao.plano tree,form ================================================================== ARQUIVO: ./views/res_partner_view.xml ================================================================== res.partner.form.recreacao res.partner ================================================================== ARQUIVO: ./views/diario_view.xml ================================================================== recreacao.diario.tree recreacao.diario recreacao.diario.form recreacao.diario
Diário Geral recreacao.diario tree,form
================================================================== ARQUIVO: ./views/financeiro_view.xml ================================================================== recreacao.financeiro.tree recreacao.financeiro