From d76d6109559bf00ff7bd792ec216d5aa612f463d Mon Sep 17 00:00:00 2001 From: creidsu Date: Tue, 17 Feb 2026 23:47:16 +0100 Subject: [PATCH] =?UTF-8?q?primeira=20vers=C3=A3o?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 1 + __init__.py | 2 + __manifest__.py | 23 + __pycache__/__init__.cpython-310.pyc | Bin 0 -> 195 bytes gerar_projeto.sh | 42 + models/__init__.py | 6 + models/crianca.py | 160 ++++ models/diario.py | 30 + models/financeiro.py | 52 ++ models/plano.py | 19 + models/report_parser.py | 43 + models/res_partner.py | 11 + projeto_completo.txt | 1177 ++++++++++++++++++++++++++ reports/financeiro_report.xml | 0 reports/financeiro_template.xml | 167 ++++ reports/historico_template.xml | 47 + reports/report.xml | 12 + security/ir.model.access.csv | 6 + static/img/favicon.ico | Bin 0 -> 36194 bytes static/img/favicon.png | Bin 0 -> 120425 bytes static/img/icon_192.png | Bin 0 -> 23334 bytes static/img/icon_512.png | Bin 0 -> 120425 bytes static/manifest.json | 21 + views/crianca_view.xml | 200 +++++ views/diario_view.xml | 50 ++ views/financeiro_view.xml | 118 +++ views/financeiro_wizard_view.xml | 40 + views/plano_view.xml | 25 + views/res_partner_view.xml | 20 + views/web_layout.xml | 15 + wizard/__init__.py | 1 + wizard/financeiro_wizard.py | 16 + 32 files changed, 2304 insertions(+) create mode 100644 README.md create mode 100644 __init__.py create mode 100644 __manifest__.py create mode 100644 __pycache__/__init__.cpython-310.pyc create mode 100755 gerar_projeto.sh create mode 100644 models/__init__.py create mode 100644 models/crianca.py create mode 100644 models/diario.py create mode 100644 models/financeiro.py create mode 100644 models/plano.py create mode 100644 models/report_parser.py create mode 100644 models/res_partner.py create mode 100644 projeto_completo.txt create mode 100644 reports/financeiro_report.xml create mode 100644 reports/financeiro_template.xml create mode 100644 reports/historico_template.xml create mode 100644 reports/report.xml create mode 100644 security/ir.model.access.csv create mode 100644 static/img/favicon.ico create mode 100644 static/img/favicon.png create mode 100644 static/img/icon_192.png create mode 100644 static/img/icon_512.png create mode 100644 static/manifest.json create mode 100644 views/crianca_view.xml create mode 100644 views/diario_view.xml create mode 100644 views/financeiro_view.xml create mode 100644 views/financeiro_wizard_view.xml create mode 100644 views/plano_view.xml create mode 100644 views/res_partner_view.xml create mode 100644 views/web_layout.xml create mode 100644 wizard/__init__.py create mode 100644 wizard/financeiro_wizard.py diff --git a/README.md b/README.md new file mode 100644 index 0000000..ac7a001 --- /dev/null +++ b/README.md @@ -0,0 +1 @@ +Plugin pessoal da recreação \ No newline at end of file diff --git a/__init__.py b/__init__.py new file mode 100644 index 0000000..c536983 --- /dev/null +++ b/__init__.py @@ -0,0 +1,2 @@ +from . import models +from . import wizard \ No newline at end of file diff --git a/__manifest__.py b/__manifest__.py new file mode 100644 index 0000000..e7a4092 --- /dev/null +++ b/__manifest__.py @@ -0,0 +1,23 @@ +# 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', +} \ No newline at end of file diff --git a/__pycache__/__init__.cpython-310.pyc b/__pycache__/__init__.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..7f474ed88118da7d07d56265309ef66438569a0d GIT binary patch literal 195 zcmd1j<>g`kf_*2aWNHHG#~=WHoCYR=<+JP)8W&sjB H3<8V*6%Q+V literal 0 HcmV?d00001 diff --git a/gerar_projeto.sh b/gerar_projeto.sh new file mode 100755 index 0000000..0dd1cc7 --- /dev/null +++ b/gerar_projeto.sh @@ -0,0 +1,42 @@ +#!/bin/bash + +# Nome do arquivo final +OUTPUT="projeto_completo.txt" + +# Pastas ou arquivos para ignorar (separados por |) +# Ajuste conforme sua necessidade. Ex: node_modules, .git, venv, arquivos de log, etc. +IGNORE="\.git|node_modules|vendor|__pycache__|\.idea|\.vscode|dist|build|target|\.lock|yarn-error.log" + +# Remove o arquivo anterior se existir +rm -f "$OUTPUT" + +echo "=== INICIANDO GERAÇÃO DO ARQUIVO ===" + +# 1. Grava a Estrutura de Diretórios +echo "Gerando estrutura de pastas..." +echo "============== ESTRUTURA DO PROJETO ==============" >> "$OUTPUT" +# Usa find para listar, remove o ./ do inicio e filtra os ignorados +find . -maxdepth 4 -not -path '*/.*' | grep -vE "$IGNORE" | grep -v "$OUTPUT" | grep -v "$0" >> "$OUTPUT" +echo -e "\n\n" >> "$OUTPUT" + +# 2. Grava o Conteúdo dos Arquivos +echo "Lendo arquivos..." +# Busca arquivos (-type f), ignora ocultos, filtra a lista de ignorados, o output e o próprio script +find . -type f -not -path '*/.*' | grep -vE "$IGNORE" | grep -v "$OUTPUT" | grep -v "$0" | while read -r file; do + + # Verifica se o arquivo é binário (ex: imagens, executáveis) usando grep + # Se for texto (-I), ele processa. + if grep -qI . "$file"; then + echo "Adicionando: $file" + echo "==================================================================" >> "$OUTPUT" + echo "ARQUIVO: $file" >> "$OUTPUT" + echo "==================================================================" >> "$OUTPUT" + cat "$file" >> "$OUTPUT" + echo -e "\n\n" >> "$OUTPUT" + else + echo "Pulenado arquivo binário: $file" + fi +done + +echo "=== CONCLUÍDO ===" +echo "O arquivo '$OUTPUT' foi gerado com sucesso." \ No newline at end of file diff --git a/models/__init__.py b/models/__init__.py new file mode 100644 index 0000000..880d02a --- /dev/null +++ b/models/__init__.py @@ -0,0 +1,6 @@ +from . import plano +from . import diario +from . import crianca +from . import res_partner +from . import financeiro +from . import report_parser \ No newline at end of file diff --git a/models/crianca.py b/models/crianca.py new file mode 100644 index 0000000..565ae13 --- /dev/null +++ b/models/crianca.py @@ -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" \ No newline at end of file diff --git a/models/diario.py b/models/diario.py new file mode 100644 index 0000000..5bf75e8 --- /dev/null +++ b/models/diario.py @@ -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') \ No newline at end of file diff --git a/models/financeiro.py b/models/financeiro.py new file mode 100644 index 0000000..6c99c44 --- /dev/null +++ b/models/financeiro.py @@ -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 \ No newline at end of file diff --git a/models/plano.py b/models/plano.py new file mode 100644 index 0000000..426f967 --- /dev/null +++ b/models/plano.py @@ -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') \ No newline at end of file diff --git a/models/report_parser.py b/models/report_parser.py new file mode 100644 index 0000000..64da7ad --- /dev/null +++ b/models/report_parser.py @@ -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, + } \ No newline at end of file diff --git a/models/res_partner.py b/models/res_partner.py new file mode 100644 index 0000000..50cc74f --- /dev/null +++ b/models/res_partner.py @@ -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.)' + ) \ No newline at end of file diff --git a/projeto_completo.txt b/projeto_completo.txt new file mode 100644 index 0000000..f1b06a0 --- /dev/null +++ b/projeto_completo.txt @@ -0,0 +1,1177 @@ +============== 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 +
  • +
+
+
+ +
+
+ +