diff --git a/academic/__init__.py b/academic/__init__.py index 4d73c438..6c1e1357 100644 --- a/academic/__init__.py +++ b/academic/__init__.py @@ -4,5 +4,3 @@ ############################################################################## from . import models from . import wizards -from . import controllers -from .hooks import post_init_hook diff --git a/academic/__manifest__.py b/academic/__manifest__.py index 306ed564..ce916526 100644 --- a/academic/__manifest__.py +++ b/academic/__manifest__.py @@ -32,8 +32,6 @@ "hr", "website", "board", - "sale_management", - "account", "contacts", "report_aeroo", "crm", @@ -49,43 +47,36 @@ "views/res_partner_views.xml", "views/academic_group_views.xml", "views/academic_division_views.xml", - "views/academic_level_views.xml", - "views/academic_study_plan_views.xml", "views/academic_promotion_views.xml", "views/academic_section_views.xml", "views/academic_subject_views.xml", + "views/academic_year_views.xml", "views/hr_views.xml", "views/res_users_views.xml", "views/res_company_views.xml", "views/login_page.xml", - "views/sale_order_views.xml", - "views/crm_lead_view.xml", "views/res_partner_link_views.xml", "views/res_partner_relationship_views.xml", - "views/account_move_views.xml", "wizards/portal_wizard_views.xml", "report/ir_actions_report.xml", "views/res_partner_category.xml", - "views/account_portal_templates.xml", - "report/report_invoice.xml", ], "demo": [ "demo/res_partner_relationship_demo.xml", "demo/res_partner_demo.xml", "demo/academic.subject.csv", - "demo/academic.section.csv", "demo/academic.level.csv", + "demo/academic.section.csv", "demo/academic.promotion.csv", "demo/res.partner.csv", "demo/res.partner.link.csv", "demo/academic.division.csv", - "demo/academic.study.plan.csv", "demo/res_company_demo.xml", + "demo/academic_year.xml", "demo/academic_group.xml", "demo/res_users_demo.xml", ], "installable": True, "auto_install": False, "application": False, - "post_init_hook": "post_init_hook", } diff --git a/academic/controllers/__init__.py b/academic/controllers/__init__.py deleted file mode 100644 index 8c3feb6f..00000000 --- a/academic/controllers/__init__.py +++ /dev/null @@ -1 +0,0 @@ -from . import portal diff --git a/academic/demo/academic.level.csv b/academic/demo/academic.level.csv index dde51656..7c0e1d9f 100644 --- a/academic/demo/academic.level.csv +++ b/academic/demo/academic.level.csv @@ -1,14 +1,8 @@ -id,name,section_id/id -academic_course_1,Primer Grado,academic_section_1 -academic_course_2,Segundo Grado,academic_section_1 -academic_course_3,Tercer Grado,academic_section_1 -academic_course_4,Cuarto Grado,academic_section_1 -academic_course_5,Quinto Grado,academic_section_1 -academic_course_6,Sexto Grado,academic_section_1 -academic_course_7,Séptimo Grado,academic_section_1 -academic_course_8,Primer Grado,academic_section_2 -academic_course_9,Segundo Grado,academic_section_2 -academic_course_10,Tercero Grado,academic_section_2 -academic_course_11,Cuarto Grado,academic_section_2 -academic_course_12,Quinto Grado,academic_section_2 -academic_course_13,Sexto Grado,academic_section_2 +id,name +academic_level_1,Primer año +academic_level_2,Segundo año +academic_level_3,Tercer año +academic_level_4,Cuarto año +academic_level_5,Quinto año +academic_level_6,Sexto año +academic_level_7,Séptimo año diff --git a/academic/demo/academic.section.csv b/academic/demo/academic.section.csv index e1d1a78e..dc98412c 100644 --- a/academic/demo/academic.section.csv +++ b/academic/demo/academic.section.csv @@ -1,3 +1,11 @@ -id,name -academic_section_1,Primaria -academic_section_2,Secundaria +id,name,correlative_ids/id,level_ids/id +academic_section_1,Nivel Inicial,,"academic_level_1" +academic_section_2,Primario 1 a 6,academic_section_1,"academic_level_1,academic_level_2,academic_level_3,academic_level_4,academic_level_5,academic_level_6" +academic_section_3,Primario 1 a 7,academic_section_1,"academic_level_1,academic_level_2,academic_level_3,academic_level_4,academic_level_5,academic_level_6,academic_level_7" +academic_section_4,Secundario 1 a 5,academic_section_3,"academic_level_1,academic_level_2,academic_level_3,academic_level_4,academic_level_5" +academic_section_5,Secundario 1 a 6,academic_section_2,"academic_level_1,academic_level_2,academic_level_3,academic_level_4,academic_level_5,academic_level_6" +academic_section_6,Inglés Básico,,"academic_level_1,academic_level_2,academic_level_3" +academic_section_7,Inglés Avanzado,academic_section_6,"academic_level_1,academic_level_2,academic_level_3" +academic_section_8,Inglés Técnico,academic_section_7,"academic_level_1,academic_level_2,academic_level_3" +academic_section_9,Ingeniería Industrial,,"academic_level_1,academic_level_2,academic_level_3,academic_level_4,academic_level_5" +academic_section_10,Maestría en Ciencias de Datos,academic_section_9,"academic_level_1,academic_level_2" diff --git a/academic/demo/academic.study.plan.csv b/academic/demo/academic.study.plan.csv deleted file mode 100644 index c8706d65..00000000 --- a/academic/demo/academic.study.plan.csv +++ /dev/null @@ -1,3 +0,0 @@ -id,name,level_ids/id -academic_study_plan_1,Primario hasta 6,"academic_course_1,academic_course_2,academic_course_3,academic_course_4,academic_course_5,academic_course_6,academic_course_8,academic_course_9,academic_course_10,academic_course_11,academic_course_12,academic_course_13" -academic_study_plan_2,Primario hasta 7,"academic_course_1,academic_course_2,academic_course_3,academic_course_4,academic_course_5,academic_course_6,academic_course_7,academic_course_8,academic_course_9,academic_course_10,academic_course_11,academic_course_12,academic_course_13" diff --git a/academic/demo/academic_group.xml b/academic/demo/academic_group.xml index f3c4eca3..d6c7028b 100644 --- a/academic/demo/academic_group.xml +++ b/academic/demo/academic_group.xml @@ -1,29 +1,29 @@ - + + - - + - + + - - + - + + - - + diff --git a/academic/demo/academic_year.xml b/academic/demo/academic_year.xml new file mode 100644 index 00000000..f3fcd7ec --- /dev/null +++ b/academic/demo/academic_year.xml @@ -0,0 +1,20 @@ + + + + Ciclo Lectivo 2025 + + + + + + Ciclo Lectivo 2024 + + + + + + Ciclo Lectivo 2026 + + + + diff --git a/academic/demo/res.partner.csv b/academic/demo/res.partner.csv index 1b1cb17c..96fc48b3 100644 --- a/academic/demo/res.partner.csv +++ b/academic/demo/res.partner.csv @@ -1,32 +1,32 @@ -id,name,partner_type,is_company,section_id/id,promotion_id/id,sex,parent_id/id,links_by_student,vat -res_partner_flia_mufaza,Flia. Mufaza,family,False,,,,,,10000000 -res_partner_flia_frias,Flia. Frias,family,False,,,,,True,10000001 -res_partner_flia_pampa,Flia. Pampa,family,False,,,,,,10000002 -res_partner_ignacio_rodriguez,Ignacio Rodriguez,teacher,False,,,M,,,10000003 -res_partner_laura_sali,Laura Sali,teacher,False,,,F,,,10000004 -res_partner_alvaro_diaz,Alvaro Diaz,student,False,,academic_promotion_1,M,,,10000005 -res_partner_jose_martin_rodriguez,José Martín Rodriguez,student,False,,academic_promotion_1,M,,,10000006 -res_partner_martin_perez,Martin Perez,student,False,,academic_promotion_2,M,,,10000007 -res_partner_juan_gomez,Juan Gomez,administrator,False,academic_section_1,,M,,,10000008 -res_partner_roberto_martin,Roberto Martin,administrator,False,academic_section_1,,M,,,10000009 -res_partner_malena_apdes,Malena Apdes,,False,,,F,,,10000010 -res_partner_luz,Luz Mufaza,student,False,,academic_promotion_2,F,res_partner_flia_mufaza,,10000011 -res_partner_bautista,Bautista Mufaza,student,False,,academic_promotion_2,M,res_partner_flia_mufaza,,10000012 -res_partner_mateo,Mateo Frias,student,False,,academic_promotion_2,M,res_partner_flia_frias,,10000013 -res_partner_amalia,Amalia Frias,student,False,,academic_promotion_2,F,res_partner_flia_frias,,10000014 -res_partner_ignacio,Ignacio Frias,student,False,,academic_promotion_2,M,res_partner_flia_frias,,10000015 -res_partner_martina,Martina Pampa,student,False,,academic_promotion_2,M,res_partner_flia_pampa,,10000016 -res_partner_gonzalo,Gonzalo Mufaza,relative,False,,,M,,,10000017 -res_partner_teresa,Teresa Ciudadela,relative,False,,,F,,,10000018 -res_partner_marina,Marina Priti,relative,False,,,F,,,10000019 -res_partner_juan,Juan Frias,relative,False,,,M,,,10000020 -res_partner_manuel,Manuel Pampa,relative,False,,,M,,,10000021 -res_partner_cecilia,Cecilia Todi,relative,False,,,F,,,10000022 -res_partner_marcelo,Marcelo Pampa,relative,False,,,M,,,10000023 -res_partner_laura,Laura Perez,relative,False,,,F,,,10000024 -res_partner_cristina,Cristina Garcia,relative,False,,,F,,,10000025 -res_partner_rosa,Rosa Roma,relative,False,,,F,,,10000026 -res_partner_daniel,Daniel Priti,relative,False,,,M,,,10000027 -res_partner_jose_luis,Jose Luis Mufaza,relative,False,,,M,,,10000028 -res_partner_hospital_espanol,Hospital Español,,True,,,,,,10000029 -res_partner_sanatorio_amercano,Sanatorio Americano,,True,,,,,,10000030 +id,name,partner_type,is_company,promotion_id/id,sex,parent_id/id,links_by_student,vat +res_partner_flia_mufaza,Flia. Mufaza,family,False,,,,,10000000 +res_partner_flia_frias,Flia. Frias,family,False,,,,True,10000001 +res_partner_flia_pampa,Flia. Pampa,family,False,,,,,10000002 +res_partner_ignacio_rodriguez,Ignacio Rodriguez,teacher,False,,M,,,10000003 +res_partner_laura_sali,Laura Sali,teacher,False,,F,,,10000004 +res_partner_alvaro_diaz,Alvaro Diaz,student,False,academic_promotion_1,M,,,10000005 +res_partner_jose_martin_rodriguez,José Martín Rodriguez,student,False,academic_promotion_1,M,,,10000006 +res_partner_martin_perez,Martin Perez,student,False,academic_promotion_2,M,,,10000007 +res_partner_juan_gomez,Juan Gomez,administrator,False,,M,,,10000008 +res_partner_roberto_martin,Roberto Martin,administrator,False,,M,,,10000009 +res_partner_malena_apdes,Malena Apdes,,False,,F,,,10000010 +res_partner_luz,Luz Mufaza,student,False,academic_promotion_2,F,res_partner_flia_mufaza,,10000011 +res_partner_bautista,Bautista Mufaza,student,False,academic_promotion_2,M,res_partner_flia_mufaza,,10000012 +res_partner_mateo,Mateo Frias,student,False,academic_promotion_2,M,res_partner_flia_frias,,10000013 +res_partner_amalia,Amalia Frias,student,False,academic_promotion_2,F,res_partner_flia_frias,,10000014 +res_partner_ignacio,Ignacio Frias,student,False,academic_promotion_2,M,res_partner_flia_frias,,10000015 +res_partner_martina,Martina Pampa,student,False,academic_promotion_2,M,res_partner_flia_pampa,,10000016 +res_partner_gonzalo,Gonzalo Mufaza,relative,False,,M,,,10000017 +res_partner_teresa,Teresa Ciudadela,relative,False,,F,,,10000018 +res_partner_marina,Marina Priti,relative,False,,F,,,10000019 +res_partner_juan,Juan Frias,relative,False,,M,,,10000020 +res_partner_manuel,Manuel Pampa,relative,False,,M,,,10000021 +res_partner_cecilia,Cecilia Todi,relative,False,,F,,,10000022 +res_partner_marcelo,Marcelo Pampa,relative,False,,M,,,10000023 +res_partner_laura,Laura Perez,relative,False,,F,,,10000024 +res_partner_cristina,Cristina Garcia,relative,False,,F,,,10000025 +res_partner_rosa,Rosa Roma,relative,False,,F,,,10000026 +res_partner_daniel,Daniel Priti,relative,False,,M,,,10000027 +res_partner_jose_luis,Jose Luis Mufaza,relative,False,,M,,,10000028 +res_partner_hospital_espanol,Hospital Español,,True,,,,,10000029 +res_partner_sanatorio_amercano,Sanatorio Americano,,True,,,,,10000030 diff --git a/academic/demo/res_company_demo.xml b/academic/demo/res_company_demo.xml index 44c4d169..d0051352 100644 --- a/academic/demo/res_company_demo.xml +++ b/academic/demo/res_company_demo.xml @@ -15,7 +15,19 @@ APDES ASOCIACIÓN PARA LA PROMOCIÓN DEPORTIVA, EDUCATIVA Y SOCIAL - + + + + + + + + + + + Colegio Los Senderos + + @@ -25,10 +37,9 @@ - 31 Colegio Los Arroyos - + @@ -37,10 +48,22 @@ - Colegio Los Molinos - + + + + + + + + + + + + Facultad de Ingeniería + + diff --git a/academic/demo/res_partner_demo.xml b/academic/demo/res_partner_demo.xml index 80523f79..385b5b5e 100644 --- a/academic/demo/res_partner_demo.xml +++ b/academic/demo/res_partner_demo.xml @@ -11,6 +11,16 @@ + + Facultad de Ingeniería + + + + + Jardín Los Senderos + + + Ignacio Rodriguez diff --git a/academic/demo/res_users_demo.xml b/academic/demo/res_users_demo.xml index bca85fc3..3ff46474 100644 --- a/academic/demo/res_users_demo.xml +++ b/academic/demo/res_users_demo.xml @@ -3,15 +3,15 @@ - + - + - + diff --git a/academic/migrations/18.0.0.0/__init__.py b/academic/migrations/18.0.0.0/__init__.py new file mode 100644 index 00000000..19cac4ed --- /dev/null +++ b/academic/migrations/18.0.0.0/__init__.py @@ -0,0 +1 @@ +from . import pre_migration diff --git a/academic/migrations/18.0.0.0/pre_migration.py b/academic/migrations/18.0.0.0/pre_migration.py new file mode 100644 index 00000000..3f7ad391 --- /dev/null +++ b/academic/migrations/18.0.0.0/pre_migration.py @@ -0,0 +1,77 @@ +from datetime import datetime + +from odoo import SUPERUSER_ID + + +def migrate(cr, version): + cr.execute(""" + SELECT column_name FROM information_schema.columns + WHERE table_name = 'academic_group' AND column_name = 'year' + """) + if not cr.fetchone(): + return + + cr.execute(""" + SELECT column_name FROM information_schema.columns + WHERE table_name = 'academic_group' AND column_name = 'year_id' + """) + if not cr.fetchone(): + cr.execute("ALTER TABLE academic_group ADD COLUMN year_id INTEGER") + + cr.execute(""" + SELECT table_name FROM information_schema.tables + WHERE table_name = 'academic_year' + """) + if not cr.fetchone(): + cr.execute(""" + CREATE TABLE academic_year ( + id SERIAL PRIMARY KEY, + create_uid INTEGER, + create_date TIMESTAMP, + write_uid INTEGER, + write_date TIMESTAMP, + name VARCHAR, + date_start DATE, + date_end DATE, + active BOOLEAN DEFAULT TRUE + ) + """) + + cr.execute("SELECT DISTINCT year FROM academic_group WHERE year IS NOT NULL") + years = [row[0] for row in cr.fetchall()] + + year_mapping = {} + for year in years: + cr.execute("SELECT id FROM academic_year WHERE name = %s", (f"Ciclo Lectivo {year}",)) + row = cr.fetchone() + if row: + year_id = row[0] + else: + cr.execute( + """ + INSERT INTO academic_year ( + name, date_start, date_end, create_uid, create_date, write_uid, write_date + ) VALUES (%s, %s, %s, %s, %s, %s, %s) RETURNING id + """, + ( + f"Ciclo Lectivo {year}", + f"{year}-03-01", + f"{year}-12-20", + SUPERUSER_ID, + datetime.now(), + SUPERUSER_ID, + datetime.now(), + ), + ) + year_id = cr.fetchone()[0] + year_mapping[year] = year_id + + for year, year_id in year_mapping.items(): + cr.execute( + """ + UPDATE academic_group + SET year_id = %s + WHERE year = %s + """, + (year_id, year), + ) diff --git a/academic/models/__init__.py b/academic/models/__init__.py index 141fabce..139e283c 100644 --- a/academic/models/__init__.py +++ b/academic/models/__init__.py @@ -5,10 +5,10 @@ from . import academic_division from . import academic_group from . import academic_level -from . import academic_study_plan from . import academic_promotion from . import academic_section from . import academic_subject +from . import academic_year from . import hr from . import res_company from . import res_partner_relationship @@ -16,7 +16,4 @@ from . import res_users from . import res_partner_role from . import res_partner_link -from . import sale_order -from . import account_move -from . import account_move_line from . import res_partner_category diff --git a/academic/models/academic_group.py b/academic/models/academic_group.py index 85a5c932..0f24b19e 100644 --- a/academic/models/academic_group.py +++ b/academic/models/academic_group.py @@ -2,9 +2,6 @@ # For copyright and license notices, see __manifest__.py file in module root # directory ############################################################################## -import random -import string -from datetime import date from odoo import api, fields, models @@ -12,13 +9,13 @@ class AcademicGroup(models.Model): _name = "academic.group" _description = "group" - _order = "year desc, name" + _order = "date_start desc, name" _sql_constraints = [ ( "group_unique", - "unique(subject_id, company_id, level_id, year, division_id)", - "Group should be unique per Institution, Subject," " Course-Division and Year", + "unique(subject_id, company_id, level_id, year_id, division_id)", + "Group should be unique per Institution, Subject," " Course-Division and Academic Year", ) ] @@ -31,7 +28,13 @@ class AcademicGroup(models.Model): ("parent", "Relative"), ] ) - year = fields.Integer(required=True, default=date.today().year, index=True) + year_id = fields.Many2one( + "academic.year", + string="Academic Year", + required=True, + index=True, + ) + date_start = fields.Date(related="year_id.date_start", store=True) division_id = fields.Many2one( "academic.division", string="Division", @@ -43,11 +46,19 @@ class AcademicGroup(models.Model): context={"default_is_company": True}, default=lambda self: self.env.company, ) - study_plan_level_ids = fields.Many2many(related="company_id.study_plan_id.level_ids") + section_ids = fields.Many2many("academic.section", related="company_id.section_ids") + section_id = fields.Many2one( + "academic.section", + string="Study Plan", + required=True, + domain="[('id', 'in', section_ids)]", + ) + level_ids = fields.Many2many(related="section_id.level_ids") level_id = fields.Many2one( "academic.level", string="Level", required=True, + domain="[('id', 'in', level_ids)]", ) subject_id = fields.Many2one("academic.subject", string="Subject", required=False, index=True) teacher_id = fields.Many2one( @@ -68,82 +79,26 @@ class AcademicGroup(models.Model): ) name = fields.Char(compute="_compute_name", store=True) active = fields.Boolean(default=True) - student_ids_count = fields.Integer( - string="Student Count", - compute="_compute_student_ids_count", - ) + capacity = fields.Integer() - @api.depends("company_id", "level_id", "division_id", "year") + @api.depends("company_id", "level_id", "division_id", "year_id") def _compute_name(self): for line in self: name_parts = [ line.company_id.name, - line.level_id.name if line.level_id else None, + line.section_id.name, + line.level_id.name, line.division_id.name if line.division_id else None, - line.level_id.section_id.name if line.level_id and line.level_id.section_id else None, - self.env._("Year: %s", line.year), + line.year_id.name if line.year_id else None, ] line.name = " - ".join(filter(None, name_parts)) - def create_students_users(self): - """ - This function create users if they don't exist for students related - to this group. - """ - self.student_ids.quickly_create_portal_user() - # Creamos contrasenas para todos los students que no tengan una - # explicita (no hashed) - for user in self.student_ids.mapped("user_ids").filtered(lambda x: not x.password): - user.password = "".join(random.choice(string.ascii_uppercase + string.digits) for _ in range(6)) - - def print_users(self): - """ - This function prints a report with users login and password. - """ - self.ensure_one() - self.create_students_users() - report = ( - self.env["ir.actions.report"] - .search([("report_name", "=", "academic.template_report_users")], limit=1) - .report_action(self) - ) - return report - - @api.depends("student_ids") - def _compute_student_ids_count(self): - for group in self: - group.student_ids_count = len(group.student_ids) - - def create_next_year_groups(self): - # estamos pasando de un año a otro sin usar study plan por lo siguiente: - # a) hay muchos colegios que no lo tienen bien implmentado - # b) los study plan no pueden reflejar todos los casos todavia (por ) - - for rec in self: - next_group = rec.env["academic.group"].search( - [ - ("year", "=", rec.year + 1), - ("company_id", "=", rec.company_id.id), - ("level_id", "=", rec.level_id.id), - ("division_id", "=", rec.division_id.id), - ], - limit=1, - ) - - if not next_group: - next_group = rec.copy( - default={ - "year": rec.year + 1, - "student_ids": False, - } - ) - - def open_student_view(self): + def open_students(self): action = self.env.ref("academic.action_academic_partner_students").read()[0] action.update( { - "domain": [("current_main_group_id", "=", self.id)], - "views": [(False, "list")], + "domain": [("id", "in", self.student_ids.ids)], + "views": [(False, "list"), (False, "form")], "context": {"from_open_student_view": True}, } ) diff --git a/academic/models/academic_level.py b/academic/models/academic_level.py index 1279ecb1..d5c58876 100644 --- a/academic/models/academic_level.py +++ b/academic/models/academic_level.py @@ -2,32 +2,18 @@ # For copyright and license notices, see __manifest__.py file in module root # directory ############################################################################## -from odoo import api, fields, models +from odoo import fields, models class AcademicLevel(models.Model): _name = "academic.level" _description = "level" - _order = "sequence" - _rec_names_search = ["name", "section_id.name"] + _sql_constraints = [ + ( + "unique_level_name", + "UNIQUE(name)", + "The level name must be unique.", + ) + ] - sequence = fields.Integer() - name = fields.Char( - required=True, - translate=True, - ) - section_id = fields.Many2one( - "academic.section", - string="Section", - required=True, - ) - group_ids = fields.One2many( - "academic.group", - "level_id", - string="Groups", - ) - - @api.depends("name", "section_id.name") - def _compute_display_name(self): - for rec in self: - rec.display_name = rec.name + " - " + rec.section_id.name if rec.name else "" + name = fields.Char(required=True) diff --git a/academic/models/academic_section.py b/academic/models/academic_section.py index fd71fa8d..cb8fc6c5 100644 --- a/academic/models/academic_section.py +++ b/academic/models/academic_section.py @@ -8,7 +8,25 @@ class AcademicSection(models.Model): _name = "academic.section" _description = "section" + _order = "sequence" name = fields.Char( required=True, ) + correlative_ids = fields.Many2many( + "academic.section", + "academic_section_correlative_ids_rel", + "section_id", + "correlative_id", + string="Correlative Study Plans", + ) + level_ids = fields.Many2many( + "academic.level", + "academic_section_level_ids_rel", + "section_id", + "level_id", + string="Levels", + ) + sequence = fields.Integer( + default=10, + ) diff --git a/academic/models/academic_study_plan.py b/academic/models/academic_study_plan.py deleted file mode 100644 index f6fd3463..00000000 --- a/academic/models/academic_study_plan.py +++ /dev/null @@ -1,16 +0,0 @@ -from odoo import fields, models - - -class StudyPlan(models.Model): - _name = "academic.study.plan" - _description = "Study Plan" - - name = fields.Char(required=True) - - level_ids = fields.Many2many( - "academic.level", - "academic_study_plan_ids_level_ids_rel", - "academic_study_plan_id", - "level_id", - string="Levels", - ) diff --git a/academic/models/academic_year.py b/academic/models/academic_year.py new file mode 100644 index 00000000..bb830f3f --- /dev/null +++ b/academic/models/academic_year.py @@ -0,0 +1,25 @@ +############################################################################## +# For copyright and license notices, see __manifest__.py file in module root +# directory +############################################################################## +from odoo import fields, models + + +class AcademicYear(models.Model): + _name = "academic.year" + _description = "Academic Year" + _order = "date_start desc" + + name = fields.Char(required=True) + active = fields.Boolean(default=True) + date_start = fields.Date(required=True) + date_end = fields.Date(required=True) + group_ids = fields.One2many( + "academic.group", + "year_id", + ) + + _sql_constraints = [ + ("name_unique", "unique(name)", "Name must be unique!"), + ("date_check", "check(date_start <= date_end)", "The start date must be before or equal to the end date!"), + ] diff --git a/academic/models/hr.py b/academic/models/hr.py index a69d2a4a..c1999682 100644 --- a/academic/models/hr.py +++ b/academic/models/hr.py @@ -51,5 +51,3 @@ class Employee(models.Model): comodel_name="hr.employee.asignatures", inverse_name="teacher_id", ) - - study_plan_level_ids = fields.Many2many(related="company_id.study_plan_id.level_ids") diff --git a/academic/models/res_company.py b/academic/models/res_company.py index 20370beb..64366558 100644 --- a/academic/models/res_company.py +++ b/academic/models/res_company.py @@ -2,8 +2,7 @@ # For copyright and license notices, see __manifest__.py file in module root # directory ############################################################################## -from odoo import api, fields, models -from odoo.exceptions import UserError +from odoo import fields, models class ResCompany(models.Model): @@ -14,25 +13,5 @@ class ResCompany(models.Model): "company_id", string="Groups", ) - study_plan_id = fields.Many2one(comodel_name="academic.study.plan", string="Plan de Estudio") + section_ids = fields.Many2many("academic.section", string="Study Plans") family_required = fields.Boolean(default=True) # Falta revisar la implementación con ese booleano en False - require_student_on_invoices = fields.Boolean(default=True) - - @api.constrains("require_student_on_invoices") - def _check_invoices_with_student(self): - cia_require_students = self.filtered("require_student_on_invoices") - if cia_require_students: - invoices = self.env["account.move"].search( - [ - ("company_id", "in", cia_require_students.ids), - ("move_type", "in", ["out_invoice", "out_refund"]), - ("student_id", "=", False), - ], - limit=1, - ) - if invoices: - raise UserError( - self.env._( - "You cannot mark require student on invoices if there are already invoices without students." - ) - ) diff --git a/academic/models/res_partner.py b/academic/models/res_partner.py index bb6acab8..affd6456 100644 --- a/academic/models/res_partner.py +++ b/academic/models/res_partner.py @@ -2,6 +2,7 @@ # For copyright and license notices, see __manifest__.py file in module root # directory ############################################################################## +from collections import defaultdict from datetime import date from odoo import _, api, fields, models @@ -30,10 +31,6 @@ class ResPartner(models.Model): readonly=False, store=True, ) - section_id = fields.Many2one( - "academic.section", - string="Section", - ) promotion_id = fields.Many2one( "academic.promotion", string="Promotion", @@ -91,6 +88,7 @@ class ResPartner(models.Model): compute="_compute_payment_responsible", store=True, ) + parent_id = fields.Many2one(context={"default_partner_type": "family"}) @api.depends("parent_links_by_student", "parent_id.student_link_ids") def _compute_student_links(self): @@ -141,8 +139,6 @@ def _compute_student_links(self): # create tantas cosas student_ids = fields.One2many("res.partner", "parent_id") company_id = fields.Many2one(compute="_compute_company_id", store=True, readonly=False) - # company_type = fields.Selection(selection_add=[('family', 'Family')]) - # is_family = fields.Boolean() same_dni_partner_id = fields.Many2one( "res.partner", string="Partner with same DNI", @@ -157,18 +153,6 @@ def _compute_student_links(self): category_id = fields.Many2many(check_company=True) student_count = fields.Integer(compute="_compute_student_count", store=True) - # @api.depends('is_family') - # def _compute_company_type(self): - # families = self.filtered(lambda x: x.is_company and x.is_family) - # families.company_type = 'family' - # return super(ResPartner, self - families)._compute_company_type() - - # def _write_company_type(self): - # families = self.filtered(lambda x: x.company_type == 'family') - # families.is_company = True - # families.is_family = True - # return super(ResPartner, self - families)._write_company_type() - @api.constrains("company_id", "partner_type", "parent_id") def _check_family_configured(self): if self.filtered( @@ -191,14 +175,6 @@ def _compute_related_user_id(self): def _compute_partner_type(self): self.filtered(lambda x: x.is_company and x.partner_type).partner_type = False - def quickly_create_portal_user(self): - """Metodo que crea o activa usuario inactivo en el grupo portal que - se defina - """ - # TODO: el metodo onchange_portal_id no existe. - # Esto dejo de usarse pero queda el codigo por posible implementacion a futuro - raise UserError(_("Esta función se encuentra en desarrollo!")) - @api.depends("parent_id") def _compute_company_id(self): """ @@ -243,11 +219,19 @@ def _check_unique_main_group_per_year(self): porque, al usar un campo many2many, la validación no se activaba al agregar un estudiante directamente desde un grupo. """ - current_year = date.today().year for partner in self: - domain = [("student_ids", "=", partner.id), ("subject_id", "=", False), ("year", ">=", current_year)] - grouped_data = self.env["academic.group"].read_group(domain, ["year"], ["year"]) - duplicate_years = [group["year"] for group in grouped_data if group["year_count"] > 1] + groups = self.env["academic.group"].search( + [ + ("student_ids", "=", partner.id), + ("subject_id", "=", False), + ] + ) + year_map = defaultdict(list) + for group in groups: + year = group.year_id.date_start.year + year_map[year].append(group) + + duplicate_years = [str(y) for y, g in year_map.items() if len(g) > 1] if duplicate_years: raise ValidationError( _( @@ -259,9 +243,17 @@ def _check_unique_main_group_per_year(self): @api.depends("student_group_ids") def _compute_current_main_group(self): + current_year_start = date(date.today().year, 1, 1) + current_year_end = date(date.today().year, 12, 31) for rec in self: - student_group = rec.student_group_ids.filtered(lambda g: g.year == date.today().year and not g.subject_id) - rec.current_main_group_id = student_group[:1] + groups = rec.student_group_ids.filtered_domain( + [ + ("year_id.date_start", ">=", current_year_start), + ("year_id.date_start", "<=", current_year_end), + ("subject_id", "=", False), + ] + ) + rec.current_main_group_id = groups[:1] if groups else False @api.depends("student_link_ids", "student_link_ids.role_ids") def _compute_payment_responsible(self): @@ -296,10 +288,3 @@ def web_search_read(self, domain, specification, offset=0, limit=None, order=Non def _compute_student_count(self): for rec in self.filtered(lambda x: x.partner_type == "family"): rec.student_count = len(rec.student_ids) - - @api.constrains("partner_type") - def _check_groups_student(self): - if self.env.context.get("install_mode"): - return True - if self.filtered(lambda x: x.partner_type == "student" and not x.student_group_ids): - raise UserError(_("The student must belong to at least one academic group.")) diff --git a/academic/models/sale_order.py b/academic/models/sale_order.py deleted file mode 100644 index e27dbb04..00000000 --- a/academic/models/sale_order.py +++ /dev/null @@ -1,68 +0,0 @@ -############################################################################## -# For copyright and license notices, see __manifest__.py file in module root -# directory -############################################################################## -from odoo import api, fields, models - - -class SaleOrder(models.Model): - _inherit = "sale.order" - - partner_invoice_ids = fields.Many2many("res.partner", compute="_compute_partner_invoice") - is_academic_sale = fields.Boolean(compute="_compute_is_academic_sale", readonly=False) - - @api.depends("partner_id") - def _compute_is_academic_sale(self): - for rec in self: - rec.is_academic_sale = True if not rec.partner_id else rec.partner_id.partner_type == "student" - - # dejamos solo depends a partner_id para que si cambia algo de la asignación no se re-calculen todas las ventas existentes - @api.depends("partner_id") - def _compute_partner_invoice(self): - orders = self.filtered("partner_id") - for rec in orders: - rec.partner_invoice_ids = rec.partner_id.payment_responsible_ids - (self - orders).partner_invoice_ids = False - - @api.depends("partner_invoice_ids") - def _compute_partner_invoice_id(self): - # si bien en el dominio solo permitimos estudiantes, para no romper demo data de odoo ni tests, si no es un estudiante - # dejamos compute by super - students_orders = self.filtered("is_academic_sale") - for order in students_orders: - order.partner_invoice_id = order.partner_invoice_ids._origin[:1] - super(SaleOrder, self - students_orders)._compute_partner_invoice_id() - - def _prepare_invoice(self): - res = super()._prepare_invoice() - if self.is_academic_sale: - res["student_id"] = self.partner_id.id - return res - - def action_confirm(self): - for rec in self.filtered("is_academic_sale"): - rec.message_subscribe( - [ - payment_responsible.id - for payment_responsible in rec.partner_invoice_id | rec.partner_invoice_ids - if payment_responsible not in rec.sudo().message_partner_ids - ] - ) - return super().action_confirm() - - def _message_get_default_recipients(self): - """Por defecto las plantillas mandan a partner_id pero para nosotros el partners es el estudiante. - Cambiamos plantillas para que usen el campo "use_default_to" y luego cae en este método de python donde - podemos ir mejorando a medida que nos pidan y modificar la logica de recipients. - Por ahora lo mandamos solo al partner de facturación si está definido - """ - default_recipients = super()._message_get_default_recipients() - for record in self.filtered("is_academic_sale"): - payment_responsible = record.partner_invoice_id | record.partner_invoice_ids - if payment_responsible: - default_recipients[record.id] = { - "email_cc": False, - "email_to": False, - "partner_ids": payment_responsible.ids, - } - return default_recipients diff --git a/academic/security/academic_security.xml b/academic/security/academic_security.xml index 87e2bce7..0c4306c7 100644 --- a/academic/security/academic_security.xml +++ b/academic/security/academic_security.xml @@ -1,10 +1,5 @@ - - - - - Academic Academic @@ -94,13 +89,6 @@ - - portal administrator: only section groups - - [('level_id.section_id','=',user.partner_id.section_id.id)] - - - portal gral administrator: all groups diff --git a/academic/security/ir.model.access.csv b/academic/security/ir.model.access.csv index fe635e06..e44f9ce2 100644 --- a/academic/security/ir.model.access.csv +++ b/academic/security/ir.model.access.csv @@ -7,18 +7,18 @@ access_academic_level_manager,academic.level.manager,model_academic_level,group_ access_academic_level_user,academic.level.user,model_academic_level,group_user,1,0,0,0 access_academic_subject_manager,academic.subject.manager,model_academic_subject,group_manager,1,1,1,1 access_academic_subject_user,academic.subject.user,model_academic_subject,group_user,1,1,1,1 -access_academic_study_plan_manager,academic.study_plan.manager,model_academic_study_plan,group_manager,1,1,1,1 -access_academic_study_plan_user,academic.study_plan.user,model_academic_study_plan,group_user,1,1,1,1 access_academic_section_manager,academic.section.manager,model_academic_section,group_manager,1,1,1,1 access_academic_section_user,academic.section.user,model_academic_section,group_user,1,0,0,0 access_academic_promotion_manager,academic.promotion.manager,model_academic_promotion,group_manager,1,1,1,1 access_academic_promotion_user,academic.promotion.user,model_academic_promotion,group_user,1,1,1,1 access_academic_division_manager,academic.division.manager,model_academic_division,group_manager,1,1,1,1 access_academic_division_user,academic.division.user,model_academic_division,group_user,1,0,0,0 +access_academic_year_manager,academic.year.manager,model_academic_year,group_manager,1,1,1,1 +access_academic_year_user,academic.year.user,model_academic_year,group_user,1,1,1,0 +access_academic_year_global,academic.year.global,model_academic_year,base.group_user,1,0,0,0 access_group_global,academic.group.global,model_academic_group,base.group_user,1,0,0,0 access_subject_global,academic.subject.global,model_academic_subject,base.group_user,1,0,0,0 access_level_global,academic.level.global,model_academic_level,base.group_user,1,0,0,0 -access_study_plan_global,academic.study_plan.global,model_academic_study_plan,base.group_user,1,0,0,0 access_res_partner_category_global,academic.res_partner_category.global,base.model_res_partner_category,base.group_user,1,0,0,0 access_academic_section_global,academic.res_partner_category.global,model_academic_section,base.group_user,1,0,0,0 access_academic_promotion_global,academic.academic_promotion_global,model_academic_promotion,base.group_user,1,0,0,0 diff --git a/academic/views/academic_group_views.xml b/academic/views/academic_group_views.xml index 55bba81a..927a6bd5 100644 --- a/academic/views/academic_group_views.xml +++ b/academic/views/academic_group_views.xml @@ -7,15 +7,12 @@ - + - - - @@ -28,27 +25,35 @@ academic.group + + + + + Students + + + - - - - - - - - - - - - - + + + + + + + + + + + + + - + @@ -66,10 +71,12 @@ + - - + + + @@ -82,7 +89,7 @@ - + @@ -105,20 +112,7 @@ [('company_id','=',id)] - - Move to Next Year - code - - - action - - if records: - action = records.create_next_year_groups() - - - - diff --git a/academic/views/academic_level_views.xml b/academic/views/academic_level_views.xml deleted file mode 100644 index b2d33cde..00000000 --- a/academic/views/academic_level_views.xml +++ /dev/null @@ -1,65 +0,0 @@ - - - - - - academic.level.select - academic.level - - - - - - - - - - - - - - - academic.level.form - academic.level - - - - - - - - - - - - - - - - - - - - academic.level.list - academic.level - - - - - - - - - - - Levels - academic.level - list,form - - - [] - - - - - diff --git a/academic/views/academic_section_views.xml b/academic/views/academic_section_views.xml index 094e5f8c..b218e66a 100644 --- a/academic/views/academic_section_views.xml +++ b/academic/views/academic_section_views.xml @@ -1,48 +1,48 @@ - - academic.section.select academic.section - - + + - academic.section.form academic.section - - + + + + - - academic.section.list academic.section - + + + + - Sections + Study Plans academic.section list,form @@ -50,6 +50,6 @@ [] - + diff --git a/academic/views/academic_study_plan_views.xml b/academic/views/academic_study_plan_views.xml deleted file mode 100644 index 0a9d7f80..00000000 --- a/academic/views/academic_study_plan_views.xml +++ /dev/null @@ -1,42 +0,0 @@ - - - - Study Plan - ir.actions.act_window - academic.study.plan - list,form - - - - academic.study.plan.view.form - academic.study.plan - - - - - - - - - - - - - - - - - - - academic.study.plan.view.list - academic.study.plan - - - - - - - - - - diff --git a/academic/views/academic_year_views.xml b/academic/views/academic_year_views.xml new file mode 100644 index 00000000..685a985d --- /dev/null +++ b/academic/views/academic_year_views.xml @@ -0,0 +1,67 @@ + + + + academic.year.search + academic.year + + + + + + + + + + + + academic.year.form + academic.year + + + + + + + + + + + + + + + + + + + + academic.year.list + academic.year + + + + + + + + + + + Academic Years + ir.actions.act_window + academic.year + list,form + + + Create a new Academic Year + + + + + + + diff --git a/academic/views/crm_lead_view.xml b/academic/views/crm_lead_view.xml deleted file mode 100644 index 78583eb1..00000000 --- a/academic/views/crm_lead_view.xml +++ /dev/null @@ -1,12 +0,0 @@ - - - crm.lead.form.inherit - crm.lead - - - - 1 - - - - diff --git a/academic/views/hr_views.xml b/academic/views/hr_views.xml index fa746d82..6eb55d6d 100644 --- a/academic/views/hr_views.xml +++ b/academic/views/hr_views.xml @@ -6,13 +6,11 @@ - - - + diff --git a/academic/views/res_company_views.xml b/academic/views/res_company_views.xml index b13f98b0..990892ad 100644 --- a/academic/views/res_company_views.xml +++ b/academic/views/res_company_views.xml @@ -32,9 +32,8 @@ - + - diff --git a/academic/views/res_partner_views.xml b/academic/views/res_partner_views.xml index 2e45537c..2531ee17 100644 --- a/academic/views/res_partner_views.xml +++ b/academic/views/res_partner_views.xml @@ -36,18 +36,15 @@ - - - A partner with the same dni already exists ( - Company: ). + + If the partner will be responsible for payment, remember to set the identification number. + - not is_company @@ -85,26 +82,8 @@ {'no_create': True} - - - - - is_company or not parent_id or partner_type in ['student', 'family'] - - - - - - partner_type in ['student', 'family'] or not is_company and parent_id - - - @@ -150,7 +129,7 @@ - + @@ -161,7 +140,7 @@ - + @@ -187,48 +166,6 @@ - - - - - - - - - - - - @@ -248,7 +185,7 @@ diff --git a/academic/wizards/__init__.py b/academic/wizards/__init__.py index 2e688cf9..70209db3 100644 --- a/academic/wizards/__init__.py +++ b/academic/wizards/__init__.py @@ -4,4 +4,3 @@ ############################################################################## from . import portal_wizard_user from . import portal_wizard -from . import account_move_send diff --git a/academic_sale_subscription/__init__.py b/academic_sale_subscription/__init__.py index f1bea77f..abded77c 100644 --- a/academic_sale_subscription/__init__.py +++ b/academic_sale_subscription/__init__.py @@ -3,3 +3,4 @@ from . import models from . import wizard from . import controllers +from .hooks import post_init_hook diff --git a/academic_sale_subscription/__manifest__.py b/academic_sale_subscription/__manifest__.py index 4a6600fb..bab39fa3 100644 --- a/academic_sale_subscription/__manifest__.py +++ b/academic_sale_subscription/__manifest__.py @@ -8,18 +8,35 @@ "author": "ADHOC SA", "website": "www.adhoc.com.ar", "license": "AGPL-3", - "depends": ["academic", "sale_subscription_ux"], + "depends": [ + "academic", + "sale_subscription", + "sale_subscription_ux", + "crm", + "sale_management", + "account", + ], "data": [ "data/ir_actions_server.xml", "security/ir.model.access.csv", + "security/academic_security.xml", "views/sale_order_template_views.xml", "views/sale_subscription_plan_views.xml", + "views/sale_order_views.xml", "wizard/academic_order_wizard_views.xml", "views/res_partner_views.xml", "views/sale_subscription_views.xml", "views/res_config_settings_views.xml", + "views/academic_group_views.xml", + "views/account_move_views.xml", + "views/product_template_views.xml", + "views/account_portal_templates.xml", + "views/crm_lead_views.xml", + "views/res_company_views.xml", + "report/report_invoice.xml", ], "installable": True, - "auto_install": False, + "auto_install": ["sale_subscription", "academic"], "application": False, + "post_init_hook": "post_init_hook", } diff --git a/academic_sale_subscription/controllers/__init__.py b/academic_sale_subscription/controllers/__init__.py index 8c3feb6f..e05b02b3 100644 --- a/academic_sale_subscription/controllers/__init__.py +++ b/academic_sale_subscription/controllers/__init__.py @@ -1 +1,2 @@ -from . import portal +from . import portal_account +from . import customer_portal diff --git a/academic_sale_subscription/controllers/portal.py b/academic_sale_subscription/controllers/customer_portal.py similarity index 100% rename from academic_sale_subscription/controllers/portal.py rename to academic_sale_subscription/controllers/customer_portal.py diff --git a/academic/controllers/portal.py b/academic_sale_subscription/controllers/portal_account.py similarity index 100% rename from academic/controllers/portal.py rename to academic_sale_subscription/controllers/portal_account.py diff --git a/academic/hooks.py b/academic_sale_subscription/hooks.py similarity index 100% rename from academic/hooks.py rename to academic_sale_subscription/hooks.py diff --git a/academic_sale_subscription/models/__init__.py b/academic_sale_subscription/models/__init__.py index 63df8a1f..dc9729f0 100644 --- a/academic_sale_subscription/models/__init__.py +++ b/academic_sale_subscription/models/__init__.py @@ -6,3 +6,8 @@ from . import sale_order_line from . import academic_group from . import res_config_settings +from . import crm_lead +from . import account_move +from . import account_move_line +from . import product_template +from . import res_company diff --git a/academic_sale_subscription/models/academic_group.py b/academic_sale_subscription/models/academic_group.py index e935acce..4f75fa54 100644 --- a/academic_sale_subscription/models/academic_group.py +++ b/academic_sale_subscription/models/academic_group.py @@ -2,13 +2,173 @@ # For copyright and license notices, see __manifest__.py file in module root # directory ############################################################################## -from odoo import models +from odoo import _, api, fields, models +from odoo.exceptions import ValidationError class AcademicGroup(models.Model): _inherit = "academic.group" + fee_so_line_ids = fields.One2many( + "sale.order.line", + "group_id", + string="Main SO Line", + domain=[("product_id.academic_product_type", "=", "fee")], + ) + registration_so_line_ids = fields.One2many( + "sale.order.line", + "group_id", + string="Registration SO Line", + domain=[("product_id.academic_product_type", "=", "registration")], + ) + opportunities_ids = fields.One2many( + "crm.lead", + "group_id", + string="Opportunities", + ) + fee_student_count = fields.Integer(compute="_compute_fee_student_count") + no_fee_student_count = fields.Integer(compute="_compute_no_fee_student_count") + registration_student_count = fields.Integer(compute="_compute_registration_student_count") + opportunities_student_count = fields.Integer(compute="_compute_opportunities_student_count") + vacancies = fields.Integer(compute="_compute_vacancies", store=True) + manage_sale_workflow = fields.Boolean(compute="_compute_manage_sale_workflow", store=True, readonly=False) + student_ids = fields.Many2many( + compute="_compute_student_ids", + store=True, + readonly=False, + ) + + # TODO mejorar todos estos compute, read group? podemos simplificar calculos? query? + def _compute_no_fee_student_count(self): + for group in self: + group.no_fee_student_count = len( + group.registration_so_line_ids.filtered(lambda x: x.state in ["sale"]).mapped("order_id.partner_id") + - group.fee_so_line_ids.mapped("order_id.partner_id") + ) + + def _compute_registration_student_count(self): + # TODO tal vez deberiamos hacer una constraint que no pueda permitir dos lineas de venta "activas" para mismo academic_product_type, student y grupo + # luego el mapped no seria necesario + for group in self: + group.registration_student_count = len( + group.registration_so_line_ids.filtered(lambda x: x.state in ["draft", "sent"]).mapped( + "order_id.partner_id" + ) + ) + + def _compute_opportunities_student_count(self): + for group in self: + group.opportunities_student_count = len( + group.opportunities_ids.filtered( + lambda x: x.active + and x.partner_id + not in group.registration_so_line_ids.mapped("order_id.partner_id") + | group.fee_so_line_ids.mapped("order_id.partner_id") + ) + ) + + def _compute_fee_student_count(self): + for group in self: + group.fee_student_count = len( + group.fee_so_line_ids.filtered( + lambda x: x.order_id.subscription_state in ["3_progress", "4_paused"] + ).mapped("order_id.partner_id") + ) + + @api.depends("fee_so_line_ids.order_id.state", "capacity", "manage_sale_workflow") + def _compute_vacancies(self): + for group in self: + if group.manage_sale_workflow: + group.vacancies = group.capacity - len( + group.registration_so_line_ids.filtered(lambda x: x.order_id.state == "sale").mapped( + "order_id.partner_id" + ) + ) + else: + group.vacancies = group.capacity - len(group.student_ids) + + @api.constrains("vacancies") + def _check_vacancies(self): + if self.filtered(lambda x: x.vacancies < 0): + raise ValidationError(_("There can be no negative vacancies. Increase group capacity.")) + + def open_opportunities(self): + action = self.env["ir.actions.actions"]._for_xml_id("crm.crm_lead_action_pipeline") + action.update( + { + "context": {"search_default_group_id": self.id}, + } + ) + return action + + def open_registration_sales(self): + action = self.env["ir.actions.actions"]._for_xml_id("sale.action_quotations_with_onboarding") + action.update({"domain": [("id", "in", self.registration_so_line_ids.mapped("order_id").ids)], "context": {}}) + return action + + def open_no_fee_students(self): + action = self.env.ref("academic.action_academic_partner_students").read()[0] + action.update( + { + "domain": [ + ( + "id", + "in", + ( + self.registration_so_line_ids.filtered(lambda x: x.state in ["sale"]).mapped( + "order_id.partner_id" + ) + - self.fee_so_line_ids.mapped("order_id.partner_id") + ).ids, + ) + ], + "views": [(False, "list"), (False, "form")], + "context": {"from_open_student_view": True}, + } + ) + return action + + @api.depends("manage_sale_workflow", "registration_so_line_ids.state", "fee_so_line_ids.state") + def _compute_student_ids(self): + for group in self.filtered("manage_sale_workflow"): + group.student_ids = group.fee_so_line_ids.filtered(lambda x: x.order_id.state == "sale").mapped( + "order_id.partner_id" + ) | group.registration_so_line_ids.filtered(lambda x: x.order_id.state == "sale").mapped( + "order_id.partner_id" + ) + + @api.constrains("capacity") + def _check_capacity(self): + if self.filtered(lambda x: x.capacity <= 0): + raise ValidationError(self.env._("The capacity must be greater than 0.")) + + @api.depends("subject_id") + def _compute_manage_sale_workflow(self): + groups_with_subject = self.filtered("subject_id") + groups_with_subject.manage_sale_workflow = False + (self - groups_with_subject).manage_sale_workflow = True + def open_order_wizard(self): action = self.env.ref("academic_sale_subscription.action_view_academic_order_wizard").read()[0] action.update({"context": {"default_student_ids": self.student_ids.ids}}) return action + + def open_fee_sales(self): + action = self.env["ir.actions.actions"]._for_xml_id("sale.action_quotations_with_onboarding") + action.update( + { + "domain": [ + ( + "id", + "in", + self.fee_so_line_ids.filtered( + lambda x: x.order_id.subscription_state in ["3_progress", "4_paused"] + ) + .mapped("order_id") + .ids, + ) + ], + "context": {}, + } + ) + return action diff --git a/academic/models/account_move.py b/academic_sale_subscription/models/account_move.py similarity index 94% rename from academic/models/account_move.py rename to academic_sale_subscription/models/account_move.py index 677d468d..af9394d1 100644 --- a/academic/models/account_move.py +++ b/academic_sale_subscription/models/account_move.py @@ -8,7 +8,10 @@ class AccountMove(models.Model): # Este campo solo lo uso para calcular el dominio del student_id ya que implica una búsqueda por el rol de pago. student_ids = fields.Many2many("res.partner", string="Students List", compute="_compute_student_ids") student_id = fields.Many2one( - "res.partner", domain="[('id', 'in', student_ids), ('partner_type', '=', 'student')]", index=True + "res.partner", + domain="[('id', 'in', student_ids), ('partner_type', '=', 'student')]", + index=True, + context={"default_partner_type": "student"}, ) require_student_on_invoices = fields.Boolean(related="company_id.require_student_on_invoices") is_academic_sale = fields.Boolean(compute="_compute_is_academic_sale") diff --git a/academic/models/account_move_line.py b/academic_sale_subscription/models/account_move_line.py similarity index 100% rename from academic/models/account_move_line.py rename to academic_sale_subscription/models/account_move_line.py diff --git a/academic_sale_subscription/models/crm_lead.py b/academic_sale_subscription/models/crm_lead.py new file mode 100644 index 00000000..c7b77bfc --- /dev/null +++ b/academic_sale_subscription/models/crm_lead.py @@ -0,0 +1,16 @@ +############################################################################## +# For copyright and license notices, see __manifest__.py file in module root +# directory +############################################################################## +from odoo import fields, models + + +class CrmLead(models.Model): + _inherit = "crm.lead" + + group_id = fields.Many2one("academic.group") + + def _create_customer(self): + if self.group_id: + return super(CrmLead, self.with_context(default_partner_type="student"))._create_customer() + return super()._create_customer() diff --git a/academic_sale_subscription/models/product_template.py b/academic_sale_subscription/models/product_template.py new file mode 100644 index 00000000..7398229b --- /dev/null +++ b/academic_sale_subscription/models/product_template.py @@ -0,0 +1,12 @@ +from odoo import fields, models + + +class ProductTemplate(models.Model): + _inherit = "product.template" + + academic_product_type = fields.Selection( + selection=[ + ("fee", "Fee"), + ("registration", "Registration"), + ], + ) diff --git a/academic_sale_subscription/models/res_company.py b/academic_sale_subscription/models/res_company.py new file mode 100644 index 00000000..8725098e --- /dev/null +++ b/academic_sale_subscription/models/res_company.py @@ -0,0 +1,31 @@ +############################################################################## +# For copyright and license notices, see __manifest__.py file in module root +# directory +############################################################################## +from odoo import api, fields, models +from odoo.exceptions import UserError + + +class ResCompany(models.Model): + _inherit = "res.company" + + require_student_on_invoices = fields.Boolean(default=True) + + @api.constrains("require_student_on_invoices") + def _check_invoices_with_student(self): + cia_require_students = self.filtered("require_student_on_invoices") + if cia_require_students: + invoices = self.env["account.move"].search( + [ + ("company_id", "in", cia_require_students.ids), + ("move_type", "in", ["out_invoice", "out_refund"]), + ("student_id", "=", False), + ], + limit=1, + ) + if invoices: + raise UserError( + self.env._( + "You cannot mark require student on invoices if there are already invoices without students." + ) + ) diff --git a/academic_sale_subscription/models/res_partner.py b/academic_sale_subscription/models/res_partner.py index 3d21d087..c2a659ae 100644 --- a/academic_sale_subscription/models/res_partner.py +++ b/academic_sale_subscription/models/res_partner.py @@ -22,3 +22,15 @@ def _compute_current_subscription(self): lambda line: not line.order_id.end_date or line.order_id.next_invoice_date < line.order_id.end_date ) ) + + def open_academic_order_wizard(self): + action = self.env["ir.actions.actions"]._for_xml_id( + "academic_sale_subscription.action_view_academic_order_wizard" + ) + if academic_group := self.env.context.get("academic_group_id"): + action.update( + { + "context": {"academic_group_id": academic_group}, + } + ) + return action diff --git a/academic_sale_subscription/models/sale_order.py b/academic_sale_subscription/models/sale_order.py index f75d58d8..1267206b 100644 --- a/academic_sale_subscription/models/sale_order.py +++ b/academic_sale_subscription/models/sale_order.py @@ -13,6 +13,68 @@ class SaleOrder(models.Model): current_main_group_id = fields.Many2one("academic.group", related="partner_id.current_main_group_id", store=True) show_update_end_date = fields.Boolean(store=False) + partner_id = fields.Many2one( + domain="[('type', '!=', 'private'), ('company_id', 'in', (False, company_id)), ('partner_type', '=', 'student')]" + ) + partner_invoice_ids = fields.Many2many("res.partner", compute="_compute_partner_invoice") + is_academic_sale = fields.Boolean(compute="_compute_is_academic_sale", readonly=False) + + @api.depends("partner_id") + def _compute_is_academic_sale(self): + for rec in self: + rec.is_academic_sale = True if not rec.partner_id else rec.partner_id.partner_type == "student" + + # dejamos solo depends a partner_id para que si cambia algo de la asignación no se re-calculen todas las ventas existentes + @api.depends("partner_id") + def _compute_partner_invoice(self): + orders = self.filtered("partner_id") + for rec in orders: + rec.partner_invoice_ids = rec.partner_id.payment_responsible_ids + (self - orders).partner_invoice_ids = False + + @api.depends("partner_invoice_ids") + def _compute_partner_invoice_id(self): + # si bien en el dominio solo permitimos estudiantes, para no romper demo data de odoo ni tests, si no es un estudiante + # dejamos compute by super + students_orders = self.filtered("is_academic_sale") + for order in students_orders: + order.partner_invoice_id = order.partner_invoice_ids._origin[:1] + super(SaleOrder, self - students_orders)._compute_partner_invoice_id() + + def _prepare_invoice(self): + res = super()._prepare_invoice() + if self.is_academic_sale: + res["student_id"] = self.partner_id.id + return res + + def action_confirm(self): + for rec in self.filtered("is_academic_sale"): + rec.message_subscribe( + [ + payment_responsible.id + for payment_responsible in rec.partner_invoice_id | rec.partner_invoice_ids + if payment_responsible not in rec.sudo().message_partner_ids + ] + ) + return super().action_confirm() + + def _message_get_default_recipients(self): + """Por defecto las plantillas mandan a partner_id pero para nosotros el partners es el estudiante. + Cambiamos plantillas para que usen el campo "use_default_to" y luego cae en este método de python donde + podemos ir mejorando a medida que nos pidan y modificar la logica de recipients. + Por ahora lo mandamos solo al partner de facturación si está definido + """ + default_recipients = super()._message_get_default_recipients() + for record in self.filtered("is_academic_sale"): + payment_responsible = record.partner_invoice_id | record.partner_invoice_ids + if payment_responsible: + default_recipients[record.id] = { + "email_cc": False, + "email_to": False, + "partner_ids": payment_responsible.ids, + } + return default_recipients + def _set_deferred_end_date_from_template(self): self.ensure_one() if ( diff --git a/academic_sale_subscription/models/sale_order_line.py b/academic_sale_subscription/models/sale_order_line.py index 879a8ed7..502f2293 100644 --- a/academic_sale_subscription/models/sale_order_line.py +++ b/academic_sale_subscription/models/sale_order_line.py @@ -2,7 +2,7 @@ # For copyright and license notices, see __manifest__.py file in module root # directory ############################################################################## -from odoo import fields, models +from odoo import api, fields, models class SaleOrderLine(models.Model): @@ -10,6 +10,18 @@ class SaleOrderLine(models.Model): partner_invoice_id = fields.Many2one(related="order_id.partner_invoice_id") payment_term_id = fields.Many2one(related="order_id.payment_term_id") + group_id = fields.Many2one("academic.group", compute="_compute_group_id", store=True, readonly=False) + academic_product_type = fields.Selection( + related="product_id.academic_product_type", + ) + + @api.depends("product_id") + def _compute_group_id(self): + for rec in self: + if rec.product_id.academic_product_type and rec.order_id.opportunity_id.group_id: + rec.group_id = rec.order_id.opportunity_id.group_id + else: + rec.group_id = False def open_subscription_form(self): return { diff --git a/academic/report/report_invoice.xml b/academic_sale_subscription/report/report_invoice.xml similarity index 100% rename from academic/report/report_invoice.xml rename to academic_sale_subscription/report/report_invoice.xml diff --git a/academic_sale_subscription/security/academic_security.xml b/academic_sale_subscription/security/academic_security.xml new file mode 100644 index 00000000..6ebb9e05 --- /dev/null +++ b/academic_sale_subscription/security/academic_security.xml @@ -0,0 +1,7 @@ + + + + + + + diff --git a/academic_sale_subscription/views/academic_group_views.xml b/academic_sale_subscription/views/academic_group_views.xml new file mode 100644 index 00000000..8b1ac8f1 --- /dev/null +++ b/academic_sale_subscription/views/academic_group_views.xml @@ -0,0 +1,44 @@ + + + academic.group.form + academic.group + + + + + + + + + + + + + + + + + + + + + + + + manage_sale_workflow + + + + + + academic.group.list + academic.group + + + + + + + + + diff --git a/academic/views/account_move_views.xml b/academic_sale_subscription/views/account_move_views.xml similarity index 100% rename from academic/views/account_move_views.xml rename to academic_sale_subscription/views/account_move_views.xml diff --git a/academic/views/account_portal_templates.xml b/academic_sale_subscription/views/account_portal_templates.xml similarity index 100% rename from academic/views/account_portal_templates.xml rename to academic_sale_subscription/views/account_portal_templates.xml diff --git a/academic_sale_subscription/views/crm_lead_views.xml b/academic_sale_subscription/views/crm_lead_views.xml new file mode 100644 index 00000000..61ae6e9f --- /dev/null +++ b/academic_sale_subscription/views/crm_lead_views.xml @@ -0,0 +1,27 @@ + + + + crm.lead.form.inherit.group_id + crm.lead + + + + + + + + + + crm.lead.form.inherit.group_id + crm.lead + + + + + + + 1 + + + + diff --git a/academic_sale_subscription/views/product_template_views.xml b/academic_sale_subscription/views/product_template_views.xml new file mode 100644 index 00000000..750db2a8 --- /dev/null +++ b/academic_sale_subscription/views/product_template_views.xml @@ -0,0 +1,12 @@ + + + + product.template + + + + + + + + diff --git a/academic_sale_subscription/views/res_company_views.xml b/academic_sale_subscription/views/res_company_views.xml new file mode 100644 index 00000000..1ec9e1b4 --- /dev/null +++ b/academic_sale_subscription/views/res_company_views.xml @@ -0,0 +1,15 @@ + + + + + res.company.form + res.company + + + + + + + + + diff --git a/academic_sale_subscription/views/res_partner_views.xml b/academic_sale_subscription/views/res_partner_views.xml index bf668d30..62f5f910 100644 --- a/academic_sale_subscription/views/res_partner_views.xml +++ b/academic_sale_subscription/views/res_partner_views.xml @@ -6,8 +6,8 @@ - @@ -51,6 +51,18 @@ + + + is_company or not parent_id or partner_type in ['student', 'family'] + + + + + partner_type in ['student', 'family'] or not is_company and parent_id + + diff --git a/academic/views/sale_order_views.xml b/academic_sale_subscription/views/sale_order_views.xml similarity index 81% rename from academic/views/sale_order_views.xml rename to academic_sale_subscription/views/sale_order_views.xml index d2d4691b..ce11df6b 100644 --- a/academic/views/sale_order_views.xml +++ b/academic_sale_subscription/views/sale_order_views.xml @@ -26,6 +26,10 @@ 1 + + + + diff --git a/academic_sale_subscription/wizard/__init__.py b/academic_sale_subscription/wizard/__init__.py index a0f6f3f5..a3ec7c95 100644 --- a/academic_sale_subscription/wizard/__init__.py +++ b/academic_sale_subscription/wizard/__init__.py @@ -1,3 +1,4 @@ # © 2016 ADHOC SA # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). from . import academic_order_wizard +from . import account_move_send diff --git a/academic_sale_subscription/wizard/academic_order_wizard.py b/academic_sale_subscription/wizard/academic_order_wizard.py index c97a6d9d..c2e4bfdc 100644 --- a/academic_sale_subscription/wizard/academic_order_wizard.py +++ b/academic_sale_subscription/wizard/academic_order_wizard.py @@ -29,9 +29,6 @@ class OrderWizard(models.TransientModel): ) validity_date = fields.Date() payment_term_id = fields.Many2one("account.payment.term") - academic_group_id = fields.Many2one( - "academic.group", help="If you define a group, the selected students will be added to this group." - ) def action_create_mass_subscription(self): if not self.student_ids: @@ -86,6 +83,7 @@ def _create_mass_subscription(self): "product_uom_qty": line.quantity, "price_unit": line.price, **({"name": line.description} if line.description else {}), + **({"group_id": line.academic_group_id.id} if line.academic_group_id else {}), }, ) for line in self.order_wizard_line_ids @@ -93,9 +91,6 @@ def _create_mass_subscription(self): } ) - if self.academic_group_id: - self.academic_group_id.student_ids = [Command.link(student.id)] - if self.status_sale == "confirmed": subscription.action_confirm() @@ -159,7 +154,7 @@ def _check_validity_date(self): raise UserError(self.env._("The date of the next invoice cannot be earlier than the validity date.")) @api.constrains("order_wizard_line_ids") - def _check_price(self): + def _check_wizard_lines(self): for rec in self: if not rec.order_wizard_line_ids: raise ValidationError(self.env._("There must be product lines.")) @@ -188,6 +183,12 @@ class AcademicOrderWizardLine(models.TransientModel): description = fields.Text() quantity = fields.Float(default=1.0) template_id = fields.Many2one("sale.order.template", store=False) + academic_group_id = fields.Many2one( + "academic.group", compute="_compute_academic_group_id", store=True, readonly=False + ) + academic_product_type = fields.Selection( + related="product_id.academic_product_type", + ) @api.depends("product_id") def _compute_price(self): @@ -196,3 +197,10 @@ def _compute_price(self): rec.product_id, rec.academic_order_wizard_id.plan_id, rec.academic_order_wizard_id.pricelist_id ) rec.price = pricing.price if pricing else 0.0 + + @api.depends("academic_product_type") + def _compute_academic_group_id(self): + if academic_group := self.env.context.get("academic_group_id"): + academic_products = self.filtered("academic_product_type") + academic_products.academic_group_id = academic_group + (self - academic_products).academic_group_id = False diff --git a/academic_sale_subscription/wizard/academic_order_wizard_views.xml b/academic_sale_subscription/wizard/academic_order_wizard_views.xml index a4821f87..baaf03d4 100644 --- a/academic_sale_subscription/wizard/academic_order_wizard_views.xml +++ b/academic_sale_subscription/wizard/academic_order_wizard_views.xml @@ -15,7 +15,6 @@ - @@ -23,10 +22,12 @@ + + diff --git a/academic/wizards/account_move_send.py b/academic_sale_subscription/wizard/account_move_send.py similarity index 100% rename from academic/wizards/account_move_send.py rename to academic_sale_subscription/wizard/account_move_send.py diff --git a/demo_academic/__manifest__.py b/demo_academic/__manifest__.py index d9b0959a..472061b8 100644 --- a/demo_academic/__manifest__.py +++ b/demo_academic/__manifest__.py @@ -14,17 +14,17 @@ "sale_loyalty_ux", ], "demo": [ - "demo/res_partner_category.xml", - "demo/res_partner.xml", - "demo/res.partner.link.csv", - "demo/product_template.xml", - "demo/product_pricelist.xml", - "demo/academic.section.csv", - "demo/academic.level.csv", - "demo/academic_group.xml", - "demo/res_users.xml", - "demo/loyalty_program.xml", - "demo/init_demo_py.xml", + # "demo/res_partner_category.xml", + # "demo/res_partner.xml", + # "demo/res.partner.link.csv", + # "demo/product_template.xml", + # "demo/product_pricelist.xml", + # "demo/academic.section.csv", + # "demo/academic.level.csv", + # "demo/academic_group.xml", + # "demo/res_users.xml", + # "demo/loyalty_program.xml", + # "demo/init_demo_py.xml", ], "installable": True, "auto_install": False,
+ Create a new Academic Year +