diff --git a/medical_queue_management/README.rst b/medical_queue_management/README.rst
new file mode 100644
index 000000000..d79d0756d
--- /dev/null
+++ b/medical_queue_management/README.rst
@@ -0,0 +1 @@
+TO DO
diff --git a/medical_queue_management/__init__.py b/medical_queue_management/__init__.py
new file mode 100644
index 000000000..aee8895e7
--- /dev/null
+++ b/medical_queue_management/__init__.py
@@ -0,0 +1,2 @@
+from . import models
+from . import wizards
diff --git a/medical_queue_management/__manifest__.py b/medical_queue_management/__manifest__.py
new file mode 100644
index 000000000..a7505c566
--- /dev/null
+++ b/medical_queue_management/__manifest__.py
@@ -0,0 +1,39 @@
+# Copyright 2023 CreuBlanca
+# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
+
+{
+ "name": "Medical Queue Management",
+ "summary": """
+ Manage patients with queue""",
+ "version": "16.0.1.0.0",
+ "license": "AGPL-3",
+ "author": "CreuBlanca",
+ "website": "https://github.com/tegin/cb-medical",
+ "depends": [
+ "queue_management_display",
+ "cb_medical_careplan_sale",
+ "web_ir_actions_act_multi",
+ ],
+ "data": [
+ "views/queue_location.xml",
+ "views/queue_location_action.xml",
+ "views/queue_location_group.xml",
+ "wizards/medical_careplan_add_plan_definition.xml",
+ "wizards/queue_token_location_kanban_assign.xml",
+ "views/queue_token_location.xml",
+ "views/queue_token.xml",
+ "views/res_partner_queue_location.xml",
+ "views/res_partner.xml",
+ "security/ir.model.access.csv",
+ "views/queue_area.xml",
+ "views/queue_location_area.xml",
+ "views/workflow_plan_definition.xml",
+ "views/medical_request_group.xml",
+ "views/medical_encounter.xml",
+ ],
+ "demo": [],
+ "qweb": [],
+ "assets": {
+ "web.assets_backend": ["/medical_queue_management/static/src/**/*.scss"],
+ },
+}
diff --git a/medical_queue_management/migrations/14.0.1.1.0/post-migration.py b/medical_queue_management/migrations/14.0.1.1.0/post-migration.py
new file mode 100644
index 000000000..318e27643
--- /dev/null
+++ b/medical_queue_management/migrations/14.0.1.1.0/post-migration.py
@@ -0,0 +1,19 @@
+# Copyright 2024 Creu Blanca
+# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html)
+from openupgradelib import openupgrade
+
+
+@openupgrade.migrate()
+def migrate(env, version):
+ openupgrade.logged_query(
+ env.cr,
+ """
+ UPDATE medical_request_group mrg
+ SET generate_queue_task = wpd.generate_queue_task,
+ queue_area_id = wpd.queue_area_id
+ FROM workflow_plan_definition wpd
+ WHERE wpd.id = mrg.plan_definition_id
+ AND mrg.plan_definition_action_id IS NULL
+ AND mrg.queue_token_location_id IS NOT NULL
+ """,
+ )
diff --git a/medical_queue_management/models/__init__.py b/medical_queue_management/models/__init__.py
new file mode 100644
index 000000000..7a9b4c32b
--- /dev/null
+++ b/medical_queue_management/models/__init__.py
@@ -0,0 +1,12 @@
+from . import medical_encounter
+from . import medical_request_group
+from . import workflow_plan_definition
+from . import queue_location_area
+from . import queue_area
+from . import res_partner
+from . import res_partner_queue_location
+from . import queue_token
+from . import queue_token_location
+from . import queue_location_group
+from . import queue_location_action
+from . import queue_location
diff --git a/medical_queue_management/models/medical_encounter.py b/medical_queue_management/models/medical_encounter.py
new file mode 100644
index 000000000..e94fc0f85
--- /dev/null
+++ b/medical_queue_management/models/medical_encounter.py
@@ -0,0 +1,22 @@
+# Copyright 2023 CreuBlanca
+# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
+
+from odoo import fields, models
+
+
+class MedicalEncounter(models.Model):
+
+ _inherit = "medical.encounter"
+
+ queue_token_id = fields.Many2one("queue.token", readonly=True)
+
+ def _generate_token_vals(self):
+ return {}
+
+ def _get_queue_token(self):
+ self.ensure_one()
+ if not self.queue_token_id:
+ self.queue_token_id = self.env["queue.token"].create(
+ self._generate_token_vals()
+ )
+ return self.queue_token_id
diff --git a/medical_queue_management/models/medical_request_group.py b/medical_queue_management/models/medical_request_group.py
new file mode 100644
index 000000000..42f780bbf
--- /dev/null
+++ b/medical_queue_management/models/medical_request_group.py
@@ -0,0 +1,95 @@
+# Copyright 2023 CreuBlanca
+# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
+
+from odoo import _, api, fields, models
+from odoo.exceptions import ValidationError
+
+
+class MedicalRequestGroup(models.Model):
+
+ _inherit = "medical.request.group"
+
+ queue_token_location_id = fields.Many2one("queue.token.location", readonly=True)
+ generate_queue_task = fields.Selection(
+ selection=lambda r: r.env["workflow.plan.definition"]
+ ._fields["generate_queue_task"]
+ .selection
+ )
+ queue_area_id = fields.Many2one("queue.area")
+
+ @api.constrains("performer_id", "center_id", "encounter_id", "fhir_state")
+ def _check_queue_token(self):
+ for record in self:
+ record._review_queue_token()
+
+ def _clean_queue_token(self):
+ if self.queue_token_location_id:
+ # TODO: Maybe we should cancell in-progress or finished jobs, isn't it :S
+ if self.queue_token_location_id.state == "draft":
+ self.queue_token_location_id.state = "cancelled"
+ self.queue_token_location_id = False
+ return False
+
+ def _review_queue_token(self):
+ if self.fhir_state == "cancelled":
+ return self._clean_queue_token()
+ if not self.generate_queue_task:
+ return self._clean_queue_token()
+ return getattr(self, "_review_queue_token_%s" % self.generate_queue_task)()
+
+ def _review_queue_token_performer(self):
+ location_area = self.performer_id.queue_location_ids.filtered(
+ lambda r: r.center_id == self.center_id
+ )
+ if not location_area:
+ location_area = self.performer_id.queue_location_ids.filtered(
+ lambda r: not r.center_id
+ )
+ if not location_area:
+ return self._clean_queue_token()
+ return self._manage_queue_token(
+ location=location_area.location_id, group=location_area.group_id
+ )
+
+ def _review_queue_token_area(self):
+ area = self.queue_area_id
+ location_area = area.location_ids.filtered(
+ lambda r: r.center_id == self.center_id
+ )
+ if not location_area:
+ return self._clean_queue_token()
+ return self._manage_queue_token(
+ location=location_area.location_id, group=location_area.group_id
+ )
+
+ def _manage_queue_token(self, location=False, group=False):
+ if not location and not group:
+ return self._clean_queue_token()
+ if location and group:
+ raise ValidationError(
+ _("Location and Group cannot be defined at the same time")
+ )
+ if self.queue_token_location_id:
+ if self.queue_token_location_id.state in ["draft"]:
+ self.queue_token_location_id.write(
+ {
+ "location_id": location and location.id,
+ "group_id": group and group.id,
+ }
+ )
+ return self.queue_token_location_id
+ return self._create_queue_token(location=location, group=group)
+
+ def _create_queue_token(self, location=False, group=False):
+ self.queue_token_location_id = self.env["queue.token.location"].create(
+ self._create_queue_token_vals(location=location, group=group)
+ )
+ return self.queue_token_location_id
+
+ def _create_queue_token_vals(self, location=False, group=False):
+ token = self.encounter_id._get_queue_token()
+ return {
+ "group_id": group and group.id,
+ "location_id": location and location.id,
+ "token_id": token.id,
+ }
diff --git a/medical_queue_management/models/queue_area.py b/medical_queue_management/models/queue_area.py
new file mode 100644
index 000000000..d53ba3f4a
--- /dev/null
+++ b/medical_queue_management/models/queue_area.py
@@ -0,0 +1,14 @@
+# Copyright 2023 CreuBlanca
+# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
+
+from odoo import fields, models
+
+
+class QueueArea(models.Model):
+
+ _name = "queue.area"
+ _description = "Queue Area"
+
+ name = fields.Char(required=True)
+ active = fields.Boolean(default=True)
+ location_ids = fields.One2many("queue.location.area", inverse_name="area_id")
diff --git a/medical_queue_management/models/queue_location.py b/medical_queue_management/models/queue_location.py
new file mode 100644
index 000000000..1828dcc0a
--- /dev/null
+++ b/medical_queue_management/models/queue_location.py
@@ -0,0 +1,12 @@
+# Copyright 2024 Dixmit
+# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
+
+from odoo import fields, models
+
+
+class QueueLocation(models.Model):
+
+ _inherit = "queue.location"
+
+ action_ids = fields.Many2many("queue.location.action")
+ allows_flag = fields.Boolean()
diff --git a/medical_queue_management/models/queue_location_action.py b/medical_queue_management/models/queue_location_action.py
new file mode 100644
index 000000000..900b414dd
--- /dev/null
+++ b/medical_queue_management/models/queue_location_action.py
@@ -0,0 +1,17 @@
+# Copyright 2024 Dixmit
+# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
+
+from odoo import fields, models
+
+
+class QueueLocationAction(models.Model):
+
+ _name = "queue.location.action"
+ _description = "Queue Location Action" # TODO
+ _order = "sequence asc"
+ _inherit = ["mail.thread", "mail.activity.mixin"]
+
+ sequence = fields.Integer()
+ name = fields.Char()
+ icon = fields.Char()
+ color = fields.Char()
diff --git a/medical_queue_management/models/queue_location_area.py b/medical_queue_management/models/queue_location_area.py
new file mode 100644
index 000000000..59fa52b46
--- /dev/null
+++ b/medical_queue_management/models/queue_location_area.py
@@ -0,0 +1,24 @@
+# Copyright 2023 CreuBlanca
+# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
+
+from odoo import fields, models
+
+
+class QueueLocationArea(models.Model):
+ _name = "queue.location.area"
+ _description = "Location Area"
+
+ area_id = fields.Many2one("queue.area", required=True)
+ center_id = fields.Many2one(
+ "res.partner", domain=[("is_center", "=", True)], required=True
+ )
+ location_id = fields.Many2one("queue.location")
+ group_id = fields.Many2one("queue.location.group")
+
+ _sql_constraints = [
+ (
+ "center_area_uniq",
+ "UNIQUE(area_id, center_id)",
+ "Center for each area must be unique!",
+ ),
+ ]
diff --git a/medical_queue_management/models/queue_location_group.py b/medical_queue_management/models/queue_location_group.py
new file mode 100644
index 000000000..68e43b445
--- /dev/null
+++ b/medical_queue_management/models/queue_location_group.py
@@ -0,0 +1,11 @@
+# Copyright 2024 Dixmit
+# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
+
+from odoo import fields, models
+
+
+class QueueLocationGroup(models.Model):
+
+ _inherit = "queue.location.group"
+
+ color = fields.Char()
diff --git a/medical_queue_management/models/queue_token.py b/medical_queue_management/models/queue_token.py
new file mode 100644
index 000000000..d507a4e8f
--- /dev/null
+++ b/medical_queue_management/models/queue_token.py
@@ -0,0 +1,33 @@
+# Copyright 2023 Dixmit
+# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
+
+from odoo import api, fields, models
+
+
+class QueueToken(models.Model):
+
+ _inherit = "queue.token"
+
+ encounter_ids = fields.One2many("medical.encounter", inverse_name="queue_token_id")
+ encounter_count = fields.Integer(compute="_compute_encounter_count")
+
+ @api.depends("encounter_ids")
+ def _compute_encounter_count(self):
+ for record in self:
+ record.encounter_count = len(record.encounter_ids)
+
+ def view_encounter(self):
+ self.ensure_one()
+ encounter = self.encounter_ids
+ encounter.ensure_one()
+ action = self.env["ir.actions.act_window"]._for_xml_id(
+ "medical_administration_encounter.action_encounter_medical_his"
+ )
+ action["res_id"] = encounter.id
+ action["view_mode"] = "form"
+ action["views"] = [
+ (view_id, view_mode)
+ for view_id, view_mode in action["views"]
+ if view_mode == "form"
+ ]
+ return action
diff --git a/medical_queue_management/models/queue_token_location.py b/medical_queue_management/models/queue_token_location.py
new file mode 100644
index 000000000..6cb620f71
--- /dev/null
+++ b/medical_queue_management/models/queue_token_location.py
@@ -0,0 +1,147 @@
+# Copyright 2023 Dixmit
+# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
+
+import json
+
+from odoo import _, api, fields, models
+from odoo.exceptions import ValidationError
+
+
+class QueueTokenLocation(models.Model):
+
+ _inherit = "queue.token.location"
+ request_group_ids = fields.One2many(
+ "medical.request.group", inverse_name="queue_token_location_id"
+ )
+ patient_id = fields.Many2one(
+ "medical.patient", store=True, compute="_compute_encounter"
+ )
+ encounter_id = fields.Many2one(
+ "medical.encounter", store=True, compute="_compute_encounter"
+ )
+ encounter_identifier = fields.Char(related="encounter_id.internal_identifier")
+ request_group_count = fields.Integer(compute="_compute_request_group_count")
+ payor_id = fields.Many2one("res.partner", compute="_compute_payor")
+ info = fields.Text()
+ color = fields.Char(related="group_id.color")
+ action_data = fields.Char(compute="_compute_action_data")
+ allows_flag = fields.Boolean(related="location_id.allows_flag")
+ action_id = fields.Many2one("queue.location.action", readonly=True)
+ flagged = fields.Boolean()
+
+ @api.depends("location_id")
+ def _compute_action_data(self):
+ for record in self:
+ actions = record.location_id.action_ids
+ if record.action_id not in actions:
+ actions |= record.action_id
+ record.action_data = json.dumps(actions.read(["name", "icon", "color"]))
+
+ @api.depends("request_group_ids")
+ def _compute_payor(self):
+ for record in self:
+ record.payor_id = record.request_group_ids.payor_id[:1]
+
+ @api.depends("token_id.encounter_ids")
+ def _compute_encounter(self):
+ for record in self:
+ encounter = record.token_id.encounter_ids
+ if not encounter or len(encounter) > 1:
+ record.patient_id = False
+ record.encounter_id = False
+ continue
+ record.patient_id = encounter.patient_id
+ record.encounter_id = encounter
+
+ @api.depends("request_group_ids")
+ def _compute_request_group_count(self):
+ for record in self:
+ record.request_group_count = len(record.request_group_ids)
+
+ def view_encounter(self):
+ self.ensure_one()
+ encounter = self.request_group_ids.encounter_id
+ encounter.ensure_one()
+ action = self.env["ir.actions.act_window"]._for_xml_id(
+ "medical_administration_encounter.action_encounter_medical_his"
+ )
+ action["res_id"] = encounter.id
+ action["view_mode"] = "form"
+ action["views"] = [
+ (view_id, view_mode)
+ for view_id, view_mode in action["views"]
+ if view_mode == "form"
+ ]
+ return action
+
+ def action_kanban_call(self):
+ self.ensure_one()
+ if self.state != "in-progress":
+ raise ValidationError(_("State must be in-progress"))
+ self.with_context(
+ location_id=self.location_id.id, ignore_expected_location=True
+ ).action_call()
+ return {"type": "ir.actions.client", "tag": "soft_reload"}
+
+ def action_kanban_leave(self):
+ self.ensure_one()
+ if self.state != "in-progress":
+ raise ValidationError(_("State must be in-progress"))
+ self.with_context(location_id=self.location_id.id).action_leave()
+ return {"type": "ir.actions.client", "tag": "soft_reload"}
+
+ def action_kanban_back_to_draft(self):
+ self.ensure_one()
+ if self.state != "in-progress":
+ raise ValidationError(_("State must be in-progress"))
+ self.with_context(location_id=self.location_id.id).action_back_to_draft()
+ return {"type": "ir.actions.client", "tag": "soft_reload"}
+
+ def action_kanban_cancel(self):
+ self.ensure_one()
+ self.with_context(location_id=self.location_id.id).action_cancel()
+ return {"type": "ir.actions.client", "tag": "soft_reload"}
+
+ def action_kanban_assign(self):
+ self.ensure_one()
+ if self.location_id and self.state == "draft":
+ self.with_context(location_id=self.location_id.id).action_assign()
+ self.with_context(
+ location_id=self.location_id.id, ignore_expected_location=True
+ ).action_call()
+ return {"type": "ir.actions.client", "tag": "soft_reload"}
+ action = self.env["ir.actions.act_window"]._for_xml_id(
+ "medical_queue_management.queue_token_location_kanban_assign_act_window"
+ )
+ action["context"] = {"default_token_location_id": self.id}
+ return action
+
+ def edit_info_action(self):
+ self.ensure_one()
+ action = self.env["ir.actions.act_window"]._for_xml_id(
+ "medical_queue_management.queue_token_location_edit_info"
+ )
+ action["res_id"] = self.id
+ return action
+
+ def force_save(self):
+ self.ensure_one()
+ return {
+ "type": "ir.actions.act_multi",
+ "actions": [
+ {"type": "ir.actions.act_window_close"},
+ {"type": "ir.actions.client", "tag": "soft_reload"},
+ ],
+ }
+
+ def assign_location_action(self):
+ self.ensure_one()
+ action = self.env["queue.location.action"].browse(
+ self.env.context.get("action_id")
+ )
+ if action and action in self.location_id.action_ids:
+ self.action_id = action
+
+ def toggle_flagged(self):
+ for record in self:
+ record.flagged = not record.flagged
diff --git a/medical_queue_management/models/res_partner.py b/medical_queue_management/models/res_partner.py
new file mode 100644
index 000000000..41b5c0781
--- /dev/null
+++ b/medical_queue_management/models/res_partner.py
@@ -0,0 +1,13 @@
+# Copyright 2023 CreuBlanca
+# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
+
+from odoo import fields, models
+
+
+class ResPartner(models.Model):
+
+ _inherit = "res.partner"
+
+ queue_location_ids = fields.One2many(
+ "res.partner.queue.location", inverse_name="practitioner_id"
+ )
diff --git a/medical_queue_management/models/res_partner_queue_location.py b/medical_queue_management/models/res_partner_queue_location.py
new file mode 100644
index 000000000..126a4a9e7
--- /dev/null
+++ b/medical_queue_management/models/res_partner_queue_location.py
@@ -0,0 +1,23 @@
+# Copyright 2023 CreuBlanca
+# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
+
+from odoo import fields, models
+
+
+class ResPartnerQueueLocation(models.Model):
+
+ _name = "res.partner.queue.location"
+ _description = "Partner Queue Location"
+
+ practitioner_id = fields.Many2one("res.partner", required=True)
+ center_id = fields.Many2one("res.partner", domain=[("is_center", "=", True)])
+ location_id = fields.Many2one("queue.location")
+ group_id = fields.Many2one("queue.location.group")
+
+ _sql_constraints = [
+ (
+ "center_area_uniq",
+ "UNIQUE(practitioner_id, center_id)",
+ "Center for each area must be unique!",
+ ),
+ ]
diff --git a/medical_queue_management/models/workflow_plan_definition.py b/medical_queue_management/models/workflow_plan_definition.py
new file mode 100644
index 000000000..f056dfb8b
--- /dev/null
+++ b/medical_queue_management/models/workflow_plan_definition.py
@@ -0,0 +1,42 @@
+# Copyright 2023 CreuBlanca
+# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
+
+from odoo import _, api, fields, models
+from odoo.exceptions import ValidationError
+
+
+class WorkflowPlanDefinition(models.Model):
+ _inherit = "workflow.plan.definition"
+
+ generate_queue_task = fields.Selection(
+ [("performer", "Performer"), ("area", "Area")]
+ )
+ queue_area_id = fields.Many2one("queue.area")
+
+ @api.constrains("generate_queue_task", "performer_required")
+ def _check_generate_token(self):
+ for record in self:
+ if (
+ record.generate_queue_task == "performer"
+ and not record.performer_required
+ ):
+ raise ValidationError(
+ _(
+ "Performer must be required in order to generate task by performer"
+ )
+ )
+
+ def _get_request_group_vals(self, vals):
+ result = super()._get_request_group_vals(vals)
+ if (
+ self.generate_queue_task
+ and not vals.get("plan_definition_action_id")
+ and not self.env.context.get("do_not_generate_queue_task", False)
+ ):
+ result.update(
+ {
+ "generate_queue_task": self.generate_queue_task,
+ "queue_area_id": self.queue_area_id.id,
+ }
+ )
+ return result
diff --git a/medical_queue_management/security/ir.model.access.csv b/medical_queue_management/security/ir.model.access.csv
new file mode 100644
index 000000000..81d66ec48
--- /dev/null
+++ b/medical_queue_management/security/ir.model.access.csv
@@ -0,0 +1,19 @@
+id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
+
+acl_queue_area_planner,queue.area Planner,model_queue_area,queue_management.group_queue_planner,1,0,0,0
+acl_queue_area_processor,queue.area Processor,model_queue_area,queue_management.group_queue_processor,1,0,0,0
+acl_queue_area_admin,queue.area Administrator,model_queue_area,queue_management.group_queue_admin,1,1,1,0
+
+acl_queue_location_area_planner,queue.location.area Planner,model_queue_location_area,queue_management.group_queue_planner,1,0,0,0
+acl_queue_location_area_processor,queue.location.area Processor,model_queue_location_area,queue_management.group_queue_processor,1,0,0,0
+acl_queue_location_area_admin,queue.location.area Administrator,model_queue_location_area,queue_management.group_queue_admin,1,1,1,1
+
+acl_partner_location_area_planner,partner.location.area Planner,model_res_partner_queue_location,queue_management.group_queue_planner,1,0,0,0
+acl_partner_location_area_processor,partner.location.area Processor,model_res_partner_queue_location,queue_management.group_queue_processor,1,0,0,0
+acl_partner_location_area_admin,partner.location.area Administrator,model_res_partner_queue_location,queue_management.group_queue_admin,1,1,1,1
+
+acl_queue_token_location_kanban_assign,acl_queue_token_location_kanban_assign,model_queue_token_location_kanban_assign,queue_management.group_queue_processor,1,1,1,0
+
+acl_queue_location_action_planner,acl_queue_location_action_planner,model_queue_location_action,queue_management.group_queue_planner,1,0,0,0
+acl_queue_location_action_processor,acl_queue_location_action_planner,model_queue_location_action,queue_management.group_queue_processor,1,0,0,0
+acl_queue_location_action_manage,acl_queue_location_action_manage,model_queue_location_action,queue_management.group_queue_admin,1,1,1,0
diff --git a/medical_queue_management/static/src/scss/medical_queue_management.scss b/medical_queue_management/static/src/scss/medical_queue_management.scss
new file mode 100644
index 000000000..9bb7a7f5b
--- /dev/null
+++ b/medical_queue_management/static/src/scss/medical_queue_management.scss
@@ -0,0 +1,59 @@
+.o_kanban_renderer {
+ &.o_kanban_dashboard {
+ &.o_kanban_queue_token_location {
+ .o_kanban_record {
+ min-width: 450px;
+ }
+ }
+ }
+ .oe_kanban_queue_location {
+ &.oe_kanban_queue_token_location_draft::after {
+ background-color: $yellow;
+ }
+ &.oe_kanban_queue_token_location_in-progress::after {
+ background-color: $cyan;
+ }
+ &.oe_kanban_queue_token_location_done::after {
+ background-color: $green;
+ }
+ &.oe_kanban_queue_token_location_cancelled {
+ background-color: $red;
+ }
+ .oe_kanban_queue_token_location_data {
+ width: calc(100% - 30px);
+ }
+ .oe_kanban_queue_token_location_actions {
+ width: 30px;
+ height: 100%;
+ top: 0px;
+ right: 0px;
+ position: absolute;
+ display: flex;
+ flex-direction: column;
+ height: 100%;
+ text-align: center;
+ align-items: center;
+
+ .oe_kanban_queue_token_location_actions_item {
+ width: 100%;
+ padding: 5px;
+ flex-grow: 1;
+ display: flex;
+ align-items: center;
+ &.btn {
+ border-radius: 0px;
+ min-height: 15px;
+ }
+ .oe_kanban_queue_token_location_actions_item_icon {
+ width: 100%;
+ text-align: center;
+ }
+ }
+ }
+ }
+ .btn {
+ &.btn-fill {
+ width: 100%;
+ }
+ }
+}
diff --git a/medical_queue_management/tests/__init__.py b/medical_queue_management/tests/__init__.py
new file mode 100644
index 000000000..22845dc4e
--- /dev/null
+++ b/medical_queue_management/tests/__init__.py
@@ -0,0 +1 @@
+from . import test_medical_queue
diff --git a/medical_queue_management/tests/test_medical_queue.py b/medical_queue_management/tests/test_medical_queue.py
new file mode 100644
index 000000000..3a8c0b2d0
--- /dev/null
+++ b/medical_queue_management/tests/test_medical_queue.py
@@ -0,0 +1,757 @@
+# Copyright 2023 Dixmit
+# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
+
+from odoo.exceptions import ValidationError
+from odoo.tests.common import TransactionCase
+
+
+class TestMedicalQueue(TransactionCase):
+ @classmethod
+ def setUpClass(cls):
+ super().setUpClass()
+ cls.payor = cls.env["res.partner"].create(
+ {"name": "Payor", "is_payor": True, "is_medical": True}
+ )
+ cls.coverage_template = cls.env["medical.coverage.template"].create(
+ {"payor_id": cls.payor.id, "name": "Coverage"}
+ )
+ cls.company = cls.env.ref("base.main_company")
+ cls.center = cls.env["res.partner"].create(
+ {
+ "name": "Center",
+ "is_medical": True,
+ "is_center": True,
+ "encounter_sequence_prefix": "S",
+ "stock_location_id": cls.env.ref("stock.warehouse0").id,
+ "stock_picking_type_id": cls.env["stock.picking.type"]
+ .search([], limit=1)
+ .id,
+ }
+ )
+ cls.performer = cls.env["res.partner"].create(
+ {
+ "name": "Performer",
+ "is_medical": True,
+ "is_practitioner": True,
+ }
+ )
+ cls.performer_2 = cls.env["res.partner"].create(
+ {
+ "name": "Performer",
+ "is_medical": True,
+ "is_practitioner": True,
+ }
+ )
+ cls.center_2 = cls.env["res.partner"].create(
+ {
+ "name": "Center2",
+ "is_medical": True,
+ "is_center": True,
+ "encounter_sequence_prefix": "X",
+ "stock_location_id": cls.env.ref("stock.warehouse0").id,
+ "stock_picking_type_id": cls.env["stock.picking.type"]
+ .search([], limit=1)
+ .id,
+ }
+ )
+ cls.location = cls.env["res.partner"].create(
+ {
+ "name": "Location",
+ "is_medical": True,
+ "is_location": True,
+ "center_id": cls.center.id,
+ "stock_location_id": cls.env.ref("stock.warehouse0").id,
+ "stock_picking_type_id": cls.env["stock.picking.type"]
+ .search([], limit=1)
+ .id,
+ }
+ )
+ cls.location_2 = cls.env["res.partner"].create(
+ {
+ "name": "Location 2",
+ "is_medical": True,
+ "is_location": True,
+ "center_id": cls.center_2.id,
+ "stock_location_id": cls.env.ref("stock.warehouse0").id,
+ "stock_picking_type_id": cls.env["stock.picking.type"]
+ .search([], limit=1)
+ .id,
+ }
+ )
+ cls.agreement = cls.env["medical.coverage.agreement"].create(
+ {
+ "name": "Agreement",
+ "center_ids": [(4, cls.center.id), (4, cls.center_2.id)],
+ "coverage_template_ids": [(4, cls.coverage_template.id)],
+ "company_id": cls.company.id,
+ "authorization_method_id": cls.env.ref(
+ "medical_financial_coverage_request.without"
+ ).id,
+ "authorization_format_id": cls.env.ref(
+ "medical_financial_coverage_request.format_anything"
+ ).id,
+ }
+ )
+ cls.patient_01 = cls.create_patient("Patient 01")
+ cls.coverage_01 = cls.env["medical.coverage"].create(
+ {
+ "patient_id": cls.patient_01.id,
+ "coverage_template_id": cls.coverage_template.id,
+ }
+ )
+ cls.product_01 = cls.create_product("Medical resonance")
+ cls.product_02 = cls.create_product("Report")
+ cls.product_03 = cls.env["product.product"].create(
+ {
+ "type": "service",
+ "name": "Clinical material",
+ "is_medication": False,
+ "lst_price": 10.0,
+ }
+ )
+
+ cls.product_04 = cls.create_product("MR complex")
+ cls.plan_definition = cls.env["workflow.plan.definition"].create(
+ {"name": "Plan", "is_billable": True}
+ )
+
+ cls.plan_definition2 = cls.env["workflow.plan.definition"].create(
+ {"name": "Plan2", "is_billable": True}
+ )
+
+ cls.activity = cls.env["workflow.activity.definition"].create(
+ {
+ "name": "Activity",
+ "service_id": cls.product_02.id,
+ "model_id": cls.env.ref(
+ "medical_clinical_procedure." "model_medical_procedure_request"
+ ).id,
+ }
+ )
+ cls.activity2 = cls.env["workflow.activity.definition"].create(
+ {
+ "name": "Activity2",
+ "service_id": cls.product_03.id,
+ "model_id": cls.env.ref(
+ "medical_clinical_procedure." "model_medical_procedure_request"
+ ).id,
+ }
+ )
+ cls.env["workflow.plan.definition.action"].create(
+ {
+ "activity_definition_id": cls.activity.id,
+ "direct_plan_definition_id": cls.plan_definition.id,
+ "is_billable": False,
+ "name": "Action",
+ }
+ )
+ cls.env["workflow.plan.definition.action"].create(
+ {
+ "activity_definition_id": cls.activity2.id,
+ "direct_plan_definition_id": cls.plan_definition.id,
+ "is_billable": False,
+ "name": "Action2",
+ }
+ )
+ cls.env["workflow.plan.definition.action"].create(
+ {
+ "activity_definition_id": cls.activity.id,
+ "direct_plan_definition_id": cls.plan_definition2.id,
+ "is_billable": False,
+ "name": "Action",
+ }
+ )
+ cls.env["workflow.plan.definition.action"].create(
+ {
+ "activity_definition_id": cls.activity2.id,
+ "direct_plan_definition_id": cls.plan_definition2.id,
+ "is_billable": False,
+ "name": "Action2",
+ }
+ )
+ cls.env["workflow.plan.definition.action"].create(
+ {
+ "activity_definition_id": cls.activity2.id,
+ "direct_plan_definition_id": cls.plan_definition2.id,
+ "is_billable": False,
+ "name": "Action3",
+ }
+ )
+ cls.agreement_line = cls.env["medical.coverage.agreement.item"].create(
+ {
+ "product_id": cls.product_01.id,
+ "coverage_agreement_id": cls.agreement.id,
+ "plan_definition_id": cls.plan_definition.id,
+ "total_price": 100,
+ "authorization_method_id": cls.env.ref(
+ "medical_financial_coverage_request.without"
+ ).id,
+ "authorization_format_id": cls.env.ref(
+ "medical_financial_coverage_request.format_anything"
+ ).id,
+ }
+ )
+ cls.agreement_line2 = cls.env["medical.coverage.agreement.item"].create(
+ {
+ "product_id": cls.product_03.id,
+ "coverage_agreement_id": cls.agreement.id,
+ "plan_definition_id": cls.plan_definition.id,
+ "total_price": 100.0,
+ "authorization_method_id": cls.env.ref(
+ "medical_financial_coverage_request.without"
+ ).id,
+ "authorization_format_id": cls.env.ref(
+ "medical_financial_coverage_request.format_anything"
+ ).id,
+ }
+ )
+ cls.agreement_line3 = cls.env["medical.coverage.agreement.item"].create(
+ {
+ "product_id": cls.product_04.id,
+ "coverage_agreement_id": cls.agreement.id,
+ "plan_definition_id": cls.plan_definition2.id,
+ "total_price": 100.0,
+ "authorization_method_id": cls.env.ref(
+ "medical_financial_coverage_request.without"
+ ).id,
+ "authorization_format_id": cls.env.ref(
+ "medical_financial_coverage_request.format_anything"
+ ).id,
+ }
+ )
+ cls.queue_location = cls.env["queue.location"].create(
+ {"name": "Queue Location"}
+ )
+ cls.queue_location_2 = cls.env["queue.location"].create(
+ {"name": "Queue Location 2"}
+ )
+ cls.queue_location_group = cls.env["queue.location.group"].create(
+ {"name": "Queue Location Group"}
+ )
+ cls.queue_area = cls.env["queue.area"].create({"name": "Area"})
+
+ @classmethod
+ def create_patient(cls, name):
+ return cls.env["medical.patient"].create({"name": name})
+
+ @classmethod
+ def create_product(cls, name):
+ return cls.env["product.product"].create({"type": "service", "name": name})
+
+ @classmethod
+ def create_practitioner(cls, name):
+ return cls.env["res.partner"].create(
+ {"name": name, "is_practitioner": True, "agent": True}
+ )
+
+ def create_careplan_and_group(self, line=None, extra_vals=None):
+ if line is None:
+ line = self.agreement_line
+ if extra_vals is None:
+ extra_vals = {}
+ encounter = self.env["medical.encounter"].create(
+ {"patient_id": self.patient_01.id, "center_id": self.center.id}
+ )
+ careplan = self.env["medical.careplan"].create(
+ {
+ "patient_id": encounter.patient_id.id,
+ "encounter_id": encounter.id,
+ "center_id": encounter.center_id.id,
+ "coverage_id": self.coverage_01.id,
+ }
+ )
+ wizard_vals = {
+ "careplan_id": careplan.id,
+ "agreement_line_id": line.id,
+ }
+ wizard_vals.update(extra_vals)
+ wizard = self.env["medical.careplan.add.plan.definition"].create(wizard_vals)
+ wizard.run()
+ group = self.env["medical.request.group"].search(
+ [("careplan_id", "=", careplan.id)]
+ )
+ group.ensure_one()
+ self.assertEqual(group.center_id, encounter.center_id)
+ return encounter, careplan, group
+
+ def test_no_queue_token(self):
+ encounter, careplan, group = self.create_careplan_and_group()
+ self.assertFalse(encounter.queue_token_id)
+ self.assertFalse(group.queue_token_location_id)
+
+ def test_queue_token_area_no_matching(self):
+ self.plan_definition.write(
+ {
+ "generate_queue_task": "area",
+ "queue_area_id": self.queue_area.id,
+ }
+ )
+ self.env["queue.location.area"].create(
+ {
+ "area_id": self.queue_area.id,
+ "center_id": self.center_2.id,
+ "location_id": self.queue_location.id,
+ }
+ )
+ encounter, careplan, group = self.create_careplan_and_group()
+ self.assertFalse(encounter.queue_token_id)
+ self.assertFalse(group.queue_token_location_id)
+
+ def test_queue_token_area_location_matching(self):
+ self.plan_definition.write(
+ {
+ "generate_queue_task": "area",
+ "queue_area_id": self.queue_area.id,
+ }
+ )
+
+ self.env["queue.location.area"].create(
+ {
+ "area_id": self.queue_area.id,
+ "center_id": self.center.id,
+ "location_id": self.queue_location.id,
+ }
+ )
+ encounter, careplan, group = self.create_careplan_and_group()
+ self.assertTrue(encounter.queue_token_id)
+ self.assertTrue(group.queue_token_location_id)
+ # Testing some stuff of views here
+ self.assertTrue(encounter.queue_token_id.encounter_count, 1)
+ action = encounter.queue_token_id.view_encounter()
+ self.assertEqual(
+ encounter, self.env[action["res_model"]].browse(action["res_id"])
+ )
+ self.assertTrue(group.queue_token_location_id.request_group_count, 1)
+ action = group.queue_token_location_id.view_encounter()
+ self.assertEqual(
+ encounter, self.env[action["res_model"]].browse(action["res_id"])
+ )
+
+ def test_queue_token_area_group_matching(self):
+ self.plan_definition.write(
+ {
+ "generate_queue_task": "area",
+ "queue_area_id": self.queue_area.id,
+ }
+ )
+ self.env["queue.location.area"].create(
+ {
+ "area_id": self.queue_area.id,
+ "center_id": self.center.id,
+ "group_id": self.queue_location_group.id,
+ }
+ )
+ encounter, careplan, group = self.create_careplan_and_group()
+ self.assertTrue(encounter.queue_token_id)
+ self.assertTrue(group.queue_token_location_id)
+
+ def test_queue_token_area_error_matching(self):
+ self.plan_definition.write(
+ {
+ "generate_queue_task": "area",
+ "queue_area_id": self.queue_area.id,
+ }
+ )
+ self.env["queue.location.area"].create(
+ {
+ "area_id": self.queue_area.id,
+ "center_id": self.center.id,
+ "location_id": self.queue_location.id,
+ "group_id": self.queue_location_group.id,
+ }
+ )
+ with self.assertRaises(ValidationError):
+ encounter, careplan, group = self.create_careplan_and_group()
+
+ def test_queue_token_area_misconfigured_matching(self):
+ self.plan_definition.write(
+ {
+ "generate_queue_task": "area",
+ "queue_area_id": self.queue_area.id,
+ }
+ )
+ self.env["queue.location.area"].create(
+ {
+ "area_id": self.queue_area.id,
+ "center_id": self.center.id,
+ }
+ )
+ encounter, careplan, group = self.create_careplan_and_group()
+ self.assertFalse(encounter.queue_token_id)
+ self.assertFalse(group.queue_token_location_id)
+
+ def test_queue_token_cancelling(self):
+ self.plan_definition.write(
+ {
+ "generate_queue_task": "area",
+ "queue_area_id": self.queue_area.id,
+ }
+ )
+ self.env["queue.location.area"].create(
+ {
+ "area_id": self.queue_area.id,
+ "center_id": self.center.id,
+ "group_id": self.queue_location_group.id,
+ }
+ )
+ encounter, careplan, group = self.create_careplan_and_group()
+ self.assertTrue(encounter.queue_token_id)
+ self.assertTrue(group.queue_token_location_id)
+ self.assertEqual(group.queue_token_location_id.state, "draft")
+ token_location = group.queue_token_location_id
+ group.cancel()
+ self.assertFalse(group.queue_token_location_id)
+ token_location.invalidate_recordset()
+ self.assertEqual(token_location.state, "cancelled")
+
+ def test_queue_token_performer_location_matching(self):
+ self.plan_definition.write(
+ {
+ "generate_queue_task": "performer",
+ "performer_required": True,
+ }
+ )
+ self.env["res.partner.queue.location"].create(
+ {
+ "practitioner_id": self.performer.id,
+ "center_id": self.center.id,
+ "location_id": self.queue_location.id,
+ }
+ )
+ encounter, careplan, group = self.create_careplan_and_group(
+ extra_vals={
+ "performer_id": self.performer.id,
+ }
+ )
+ self.assertTrue(encounter.queue_token_id)
+ self.assertTrue(group.queue_token_location_id)
+
+ def test_queue_token_performer_location_matching_dont_send(self):
+ self.plan_definition.write(
+ {
+ "generate_queue_task": "performer",
+ "performer_required": True,
+ }
+ )
+ self.env["res.partner.queue.location"].create(
+ {
+ "practitioner_id": self.performer.id,
+ "center_id": self.center.id,
+ "location_id": self.queue_location.id,
+ }
+ )
+ encounter, careplan, group = self.create_careplan_and_group(
+ extra_vals={"performer_id": self.performer.id, "send_to_queue": False}
+ )
+ self.assertFalse(encounter.queue_token_id)
+
+ def test_queue_token_change_performer_different_location(self):
+ self.plan_definition.write(
+ {
+ "generate_queue_task": "performer",
+ "performer_required": True,
+ }
+ )
+ self.env["res.partner.queue.location"].create(
+ {
+ "practitioner_id": self.performer.id,
+ "center_id": self.center.id,
+ "location_id": self.queue_location.id,
+ }
+ )
+ self.env["res.partner.queue.location"].create(
+ {
+ "practitioner_id": self.performer_2.id,
+ "center_id": self.center.id,
+ "group_id": self.queue_location_group.id,
+ }
+ )
+ encounter, careplan, group = self.create_careplan_and_group(
+ extra_vals={
+ "performer_id": self.performer.id,
+ }
+ )
+ self.assertTrue(encounter.queue_token_id)
+ self.assertTrue(group.queue_token_location_id)
+ self.assertEqual(group.queue_token_location_id.location_id, self.queue_location)
+ self.assertFalse(group.queue_token_location_id.group_id)
+ group.performer_id = self.performer_2
+ self.assertFalse(group.queue_token_location_id.location_id)
+ self.assertEqual(
+ group.queue_token_location_id.group_id, self.queue_location_group
+ )
+
+ def test_queue_token_change_performer_cancel(self):
+ self.plan_definition.write(
+ {
+ "generate_queue_task": "performer",
+ "performer_required": True,
+ }
+ )
+ self.env["res.partner.queue.location"].create(
+ {
+ "practitioner_id": self.performer.id,
+ "center_id": self.center.id,
+ "location_id": self.queue_location.id,
+ }
+ )
+ encounter, careplan, group = self.create_careplan_and_group(
+ extra_vals={
+ "performer_id": self.performer.id,
+ }
+ )
+ self.assertTrue(encounter.queue_token_id)
+ self.assertTrue(group.queue_token_location_id)
+ self.assertEqual(group.queue_token_location_id.location_id, self.queue_location)
+ self.assertFalse(group.queue_token_location_id.group_id)
+ queue_token_location = group.queue_token_location_id
+ group.performer_id = self.performer_2
+ self.assertFalse(group.queue_token_location_id)
+ self.assertEqual(queue_token_location.state, "cancelled")
+
+ def test_queue_token_performer_all_center(self):
+ self.plan_definition.write(
+ {
+ "generate_queue_task": "performer",
+ "performer_required": True,
+ }
+ )
+ self.env["res.partner.queue.location"].create(
+ {
+ "practitioner_id": self.performer.id,
+ "location_id": self.queue_location.id,
+ }
+ )
+ encounter, careplan, group = self.create_careplan_and_group(
+ extra_vals={
+ "performer_id": self.performer.id,
+ }
+ )
+ self.assertTrue(encounter.queue_token_id)
+ self.assertTrue(group.queue_token_location_id)
+ self.assertEqual(group.queue_token_location_id.location_id, self.queue_location)
+ self.assertFalse(group.queue_token_location_id.group_id)
+
+ def test_queue_token_performer_group_matching(self):
+ self.plan_definition.write(
+ {
+ "generate_queue_task": "performer",
+ "performer_required": True,
+ }
+ )
+ self.env["res.partner.queue.location"].create(
+ {
+ "practitioner_id": self.performer.id,
+ "center_id": self.center.id,
+ "group_id": self.queue_location_group.id,
+ }
+ )
+ encounter, careplan, group = self.create_careplan_and_group(
+ extra_vals={
+ "performer_id": self.performer.id,
+ }
+ )
+ self.assertTrue(encounter.queue_token_id)
+ self.assertTrue(group.queue_token_location_id)
+
+ def test_queue_token_performer_misconfigured_matching(self):
+ self.plan_definition.write(
+ {
+ "generate_queue_task": "performer",
+ "performer_required": True,
+ }
+ )
+ self.env["res.partner.queue.location"].create(
+ {
+ "practitioner_id": self.performer.id,
+ "center_id": self.center.id,
+ }
+ )
+ encounter, careplan, group = self.create_careplan_and_group(
+ extra_vals={
+ "performer_id": self.performer.id,
+ }
+ )
+ self.assertFalse(encounter.queue_token_id)
+ self.assertFalse(group.queue_token_location_id)
+
+ def test_kanban_group(self):
+ self.queue_location.group_ids = self.queue_location_group
+ self.plan_definition.write(
+ {
+ "generate_queue_task": "area",
+ "queue_area_id": self.queue_area.id,
+ }
+ )
+ self.env["queue.location.area"].create(
+ {
+ "area_id": self.queue_area.id,
+ "center_id": self.center.id,
+ "group_id": self.queue_location_group.id,
+ }
+ )
+ encounter, careplan, group = self.create_careplan_and_group()
+ self.assertTrue(encounter.queue_token_id)
+ self.assertTrue(group.queue_token_location_id)
+ self.assertEqual(group.queue_token_location_id.state, "draft")
+ token_location = group.queue_token_location_id
+ action = token_location.action_kanban_assign()
+ self.assertTrue(isinstance(action, dict))
+ self.env[action["res_model"]].with_context(**action["context"]).create(
+ {"location_id": self.queue_location.id}
+ ).assign()
+ self.assertEqual(token_location.state, "in-progress")
+ self.assertTrue(token_location.expected_location_id)
+ token_location.action_kanban_back_to_draft()
+ self.assertEqual(token_location.state, "draft")
+ action = token_location.action_kanban_assign()
+ self.assertTrue(isinstance(action, dict))
+ self.env[action["res_model"]].with_context(**action["context"]).create(
+ {"location_id": self.queue_location.id}
+ ).assign()
+ self.assertEqual(token_location.state, "in-progress")
+ token_location.expected_location_id = False
+ token_location.action_kanban_call()
+ self.assertTrue(token_location.expected_location_id)
+ token_location.action_kanban_leave()
+ self.assertEqual(token_location.state, "done")
+
+ def test_kanban_location(self):
+ self.plan_definition.write(
+ {
+ "generate_queue_task": "area",
+ "queue_area_id": self.queue_area.id,
+ }
+ )
+ self.env["queue.location.area"].create(
+ {
+ "area_id": self.queue_area.id,
+ "center_id": self.center.id,
+ "location_id": self.queue_location.id,
+ }
+ )
+ encounter, careplan, group = self.create_careplan_and_group()
+ self.assertTrue(encounter.queue_token_id)
+ self.assertTrue(group.queue_token_location_id)
+ self.assertEqual(group.queue_token_location_id.state, "draft")
+ token_location = group.queue_token_location_id
+ token_location.action_kanban_assign()
+ self.assertEqual(token_location.state, "in-progress")
+ self.assertTrue(token_location.expected_location_id)
+ token_location.action_kanban_back_to_draft()
+ self.assertEqual(token_location.state, "draft")
+ token_location.action_kanban_assign()
+ self.assertEqual(token_location.state, "in-progress")
+ token_location.expected_location_id = False
+ token_location.action_kanban_call()
+ self.assertTrue(token_location.expected_location_id)
+ token_location.action_kanban_leave()
+ self.assertEqual(token_location.state, "done")
+
+ def test_kanban_call_exception(self):
+ self.plan_definition.write(
+ {
+ "generate_queue_task": "area",
+ "queue_area_id": self.queue_area.id,
+ }
+ )
+ self.env["queue.location.area"].create(
+ {
+ "area_id": self.queue_area.id,
+ "center_id": self.center.id,
+ "location_id": self.queue_location.id,
+ }
+ )
+ encounter, careplan, group = self.create_careplan_and_group()
+ self.assertTrue(encounter.queue_token_id)
+ self.assertTrue(group.queue_token_location_id)
+ self.assertEqual(group.queue_token_location_id.state, "draft")
+ token_location = group.queue_token_location_id
+ with self.assertRaises(ValidationError):
+ token_location.action_kanban_call()
+
+ def test_kanban_leave_exception(self):
+ self.plan_definition.write(
+ {
+ "generate_queue_task": "area",
+ "queue_area_id": self.queue_area.id,
+ }
+ )
+ self.env["queue.location.area"].create(
+ {
+ "area_id": self.queue_area.id,
+ "center_id": self.center.id,
+ "location_id": self.queue_location.id,
+ }
+ )
+ encounter, careplan, group = self.create_careplan_and_group()
+ self.assertTrue(encounter.queue_token_id)
+ self.assertTrue(group.queue_token_location_id)
+ self.assertEqual(group.queue_token_location_id.state, "draft")
+ token_location = group.queue_token_location_id
+ with self.assertRaises(ValidationError):
+ token_location.action_kanban_leave()
+
+ def test_kanban_back_to_draft_exception(self):
+ self.plan_definition.write(
+ {
+ "generate_queue_task": "area",
+ "queue_area_id": self.queue_area.id,
+ }
+ )
+ self.env["queue.location.area"].create(
+ {
+ "area_id": self.queue_area.id,
+ "center_id": self.center.id,
+ "location_id": self.queue_location.id,
+ }
+ )
+ encounter, careplan, group = self.create_careplan_and_group()
+ self.assertTrue(encounter.queue_token_id)
+ self.assertTrue(group.queue_token_location_id)
+ self.assertEqual(group.queue_token_location_id.state, "draft")
+ token_location = group.queue_token_location_id
+ with self.assertRaises(ValidationError):
+ token_location.action_kanban_back_to_draft()
+
+ def test_kanban_reassign(self):
+ self.queue_location.group_ids = self.queue_location_group
+ self.queue_location_2.group_ids = self.queue_location_group
+ self.plan_definition.write(
+ {
+ "generate_queue_task": "area",
+ "queue_area_id": self.queue_area.id,
+ }
+ )
+ self.env["queue.location.area"].create(
+ {
+ "area_id": self.queue_area.id,
+ "center_id": self.center.id,
+ "group_id": self.queue_location_group.id,
+ }
+ )
+ encounter, careplan, group = self.create_careplan_and_group()
+ self.assertTrue(encounter.queue_token_id)
+ self.assertTrue(group.queue_token_location_id)
+ self.assertEqual(group.queue_token_location_id.state, "draft")
+ token_location = group.queue_token_location_id
+ action = token_location.action_kanban_assign()
+ self.assertTrue(isinstance(action, dict))
+ self.env[action["res_model"]].with_context(**action["context"]).create(
+ {"location_id": self.queue_location.id}
+ ).assign()
+ self.assertEqual(token_location.state, "in-progress")
+ self.assertTrue(token_location.expected_location_id)
+ self.assertEqual(self.queue_location, token_location.expected_location_id)
+ action = token_location.with_context(
+ default_do_not_call=True
+ ).action_kanban_assign()
+ self.assertTrue(isinstance(action, dict))
+ with self.assertRaises(ValidationError):
+ self.env[action["res_model"]].with_context(**action["context"]).create(
+ {"location_id": self.queue_location.id}
+ ).assign()
+ self.env[action["res_model"]].with_context(**action["context"]).create(
+ {"location_id": self.queue_location_2.id}
+ ).assign()
+ self.assertEqual(self.queue_location_2, token_location.expected_location_id)
diff --git a/medical_queue_management/views/medical_encounter.xml b/medical_queue_management/views/medical_encounter.xml
new file mode 100644
index 000000000..06ec657e2
--- /dev/null
+++ b/medical_queue_management/views/medical_encounter.xml
@@ -0,0 +1,17 @@
+
+
+
+
+
+
+
diff --git a/medical_queue_management/views/medical_request_group.xml b/medical_queue_management/views/medical_request_group.xml
new file mode 100644
index 000000000..d21aff953
--- /dev/null
+++ b/medical_queue_management/views/medical_request_group.xml
@@ -0,0 +1,16 @@
+
+
+
+
+
+
+
diff --git a/medical_queue_management/views/queue_area.xml b/medical_queue_management/views/queue_area.xml
new file mode 100644
index 000000000..cb0c16dee
--- /dev/null
+++ b/medical_queue_management/views/queue_area.xml
@@ -0,0 +1,61 @@
+
+
+
+
+
+ queue.area.form (in medical_queue_management)
+ queue.area
+
+
+
+
+
+
+ queue.area.search (in medical_queue_management)
+ queue.area
+
+
+
+
+
+
+
+
+ queue.area.tree (in medical_queue_management)
+ queue.area
+
+
+
+
+
+
+
+
+ Area
+ queue.area
+ tree,form
+ []
+ {}
+
+
+
+
+
diff --git a/medical_queue_management/views/queue_location.xml b/medical_queue_management/views/queue_location.xml
new file mode 100644
index 000000000..16ba314d7
--- /dev/null
+++ b/medical_queue_management/views/queue_location.xml
@@ -0,0 +1,22 @@
+
+
+
+
+
+ queue.location
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/medical_queue_management/views/queue_location_action.xml b/medical_queue_management/views/queue_location_action.xml
new file mode 100644
index 000000000..257cc27d2
--- /dev/null
+++ b/medical_queue_management/views/queue_location_action.xml
@@ -0,0 +1,64 @@
+
+
+
+
+
+ queue.location.action
+
+
+
+
+
+
+ queue.location.action
+
+
+
+
+
+
+
+
+ queue.location.action
+
+
+
+
+
+
+
+
+
+
+ Queue Location Action
+ queue.location.action
+ tree,form
+ []
+ {}
+
+
+
+
+
diff --git a/medical_queue_management/views/queue_location_area.xml b/medical_queue_management/views/queue_location_area.xml
new file mode 100644
index 000000000..d8bc83fed
--- /dev/null
+++ b/medical_queue_management/views/queue_location_area.xml
@@ -0,0 +1,49 @@
+
+
+
+
+
+ queue.location.area.form (in medical_queue_management)
+ queue.location.area
+
+
+
+
+
+
+ queue.location.area.tree (in medical_queue_management)
+ queue.location.area
+
+
+
+
+
+
+
+
+
+
diff --git a/medical_queue_management/views/queue_location_group.xml b/medical_queue_management/views/queue_location_group.xml
new file mode 100644
index 000000000..d32f3e70e
--- /dev/null
+++ b/medical_queue_management/views/queue_location_group.xml
@@ -0,0 +1,21 @@
+
+
+
+
+
+ queue.location.group
+
+
+
+
+
+
+
+
+
+
+
diff --git a/medical_queue_management/views/queue_token.xml b/medical_queue_management/views/queue_token.xml
new file mode 100644
index 000000000..37dcec8e1
--- /dev/null
+++ b/medical_queue_management/views/queue_token.xml
@@ -0,0 +1,26 @@
+
+
+
+
+
+ queue.token.form (in medical_queue_management)
+ queue.token
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/medical_queue_management/views/queue_token_location.xml b/medical_queue_management/views/queue_token_location.xml
new file mode 100644
index 000000000..7d9e6e9d0
--- /dev/null
+++ b/medical_queue_management/views/queue_token_location.xml
@@ -0,0 +1,271 @@
+
+
+
+
+
+ queue.token.location.tree (in medical_queue_management)
+ queue.token.location
+
+
+
+
+
+
+
+
+
+
+
+
+ queue.token.location.tree (in medical_queue_management)
+ queue.token.location
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Assign
+
+
+ Call
+
+
+
+ Reassign
+
+
+ Leave
+
+
+ Back to draft
+
+
+ Cancel
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ queue.token.location.tree (in medical_queue_management)
+ queue.token.location
+ 99
+
+
+
+
+
+ Edit Info Locations
+ queue.token.location
+ form
+ new
+ [('encounter_id', '!=', False)]
+ {}
+
+
+
+ form
+
+
+
+
+
+ My Token Locations
+ queue.token.location
+ kanban
+ []
+ {}
+
+
+
+
+
+
diff --git a/medical_queue_management/views/res_partner.xml b/medical_queue_management/views/res_partner.xml
new file mode 100644
index 000000000..176353989
--- /dev/null
+++ b/medical_queue_management/views/res_partner.xml
@@ -0,0 +1,25 @@
+
+
+
+
+
+ res.partner.form (in medical_queue_management)
+ res.partner
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/medical_queue_management/views/res_partner_queue_location.xml b/medical_queue_management/views/res_partner_queue_location.xml
new file mode 100644
index 000000000..fd74e0007
--- /dev/null
+++ b/medical_queue_management/views/res_partner_queue_location.xml
@@ -0,0 +1,47 @@
+
+
+
+
+
+ res.partner.queue.location.form (in medical_queue_management)
+ res.partner.queue.location
+
+
+
+
+
+
+
+ res.partner.queue.location.tree (in medical_queue_management)
+ res.partner.queue.location
+
+
+
+
+
+
+
+
+
+
diff --git a/medical_queue_management/views/workflow_plan_definition.xml b/medical_queue_management/views/workflow_plan_definition.xml
new file mode 100644
index 000000000..94d37baae
--- /dev/null
+++ b/medical_queue_management/views/workflow_plan_definition.xml
@@ -0,0 +1,25 @@
+
+
+
+
+ workflow.plan.definition.form (in medical_queue_management)
+ workflow.plan.definition
+
+
+
+
+
+
+
+
+
+
diff --git a/medical_queue_management/wizards/__init__.py b/medical_queue_management/wizards/__init__.py
new file mode 100644
index 000000000..ff0949112
--- /dev/null
+++ b/medical_queue_management/wizards/__init__.py
@@ -0,0 +1,2 @@
+from . import medical_careplan_add_plan_definition
+from . import queue_token_location_kanban_assign
diff --git a/medical_queue_management/wizards/medical_careplan_add_plan_definition.py b/medical_queue_management/wizards/medical_careplan_add_plan_definition.py
new file mode 100644
index 000000000..0a89426b2
--- /dev/null
+++ b/medical_queue_management/wizards/medical_careplan_add_plan_definition.py
@@ -0,0 +1,16 @@
+# Copyright 2024 CreuBlanca
+# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
+
+from odoo import fields, models
+
+
+class MedicalCareplanAddPlanDefinition(models.TransientModel):
+
+ _inherit = "medical.careplan.add.plan.definition"
+
+ send_to_queue = fields.Boolean(default=True)
+
+ def _get_context(self):
+ result = super()._get_context()
+ result["do_not_generate_queue_task"] = not self.send_to_queue
+ return result
diff --git a/medical_queue_management/wizards/medical_careplan_add_plan_definition.xml b/medical_queue_management/wizards/medical_careplan_add_plan_definition.xml
new file mode 100644
index 000000000..0589eb2cb
--- /dev/null
+++ b/medical_queue_management/wizards/medical_careplan_add_plan_definition.xml
@@ -0,0 +1,22 @@
+
+
+
+
+
+ medical.careplan.add.plan.definition
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/medical_queue_management/wizards/queue_token_location_kanban_assign.py b/medical_queue_management/wizards/queue_token_location_kanban_assign.py
new file mode 100644
index 000000000..0cf882221
--- /dev/null
+++ b/medical_queue_management/wizards/queue_token_location_kanban_assign.py
@@ -0,0 +1,39 @@
+# Copyright 2024 Dixmit
+# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
+
+from odoo import fields, models
+
+
+class QueueTokenLocationKanbanAssign(models.TransientModel):
+
+ _name = "queue.token.location.kanban.assign"
+ _description = "Queue Token Location Kanban Assign"
+
+ token_location_id = fields.Many2one("queue.token.location", required=True)
+ group_id = fields.Many2one(related="token_location_id.group_id")
+ location_id = fields.Many2one("queue.location", required=True)
+ do_not_call = fields.Boolean()
+
+ def assign(self):
+ if (
+ self.token_location_id.location_id != self.location_id
+ and self.token_location_id.state == "in-progress"
+ ):
+ self.token_location_id._action_back_to_draft(
+ self.token_location_id.location_id
+ )
+ self.token_location_id.with_context(
+ location_id=self.location_id.id
+ ).action_assign()
+ if not self.do_not_call:
+ self.token_location_id.with_context(
+ location_id=self.location_id.id,
+ ignore_expected_location=True,
+ ).action_call()
+ return {
+ "type": "ir.actions.act_multi",
+ "actions": [
+ {"type": "ir.actions.act_window_close"},
+ {"type": "ir.actions.client", "tag": "soft_reload"},
+ ],
+ }
diff --git a/medical_queue_management/wizards/queue_token_location_kanban_assign.xml b/medical_queue_management/wizards/queue_token_location_kanban_assign.xml
new file mode 100644
index 000000000..97a0841f8
--- /dev/null
+++ b/medical_queue_management/wizards/queue_token_location_kanban_assign.xml
@@ -0,0 +1,42 @@
+
+
+
+
+
+ queue.token.location.kanban.assign
+
+
+
+
+
+
+ Queue Token Location Kanban Assign
+ queue.token.location.kanban.assign
+ form
+ {}
+ new
+
+
+
+
diff --git a/setup/medical_queue_management/odoo/addons/medical_queue_management b/setup/medical_queue_management/odoo/addons/medical_queue_management
new file mode 120000
index 000000000..6e19933a6
--- /dev/null
+++ b/setup/medical_queue_management/odoo/addons/medical_queue_management
@@ -0,0 +1 @@
+../../../../medical_queue_management
\ No newline at end of file
diff --git a/setup/medical_queue_management/setup.py b/setup/medical_queue_management/setup.py
new file mode 100644
index 000000000..28c57bb64
--- /dev/null
+++ b/setup/medical_queue_management/setup.py
@@ -0,0 +1,6 @@
+import setuptools
+
+setuptools.setup(
+ setup_requires=['setuptools-odoo'],
+ odoo_addon=True,
+)
diff --git a/test-requirements.txt b/test-requirements.txt
index eb856f922..83f713cfe 100644
--- a/test-requirements.txt
+++ b/test-requirements.txt
@@ -27,5 +27,9 @@ odoo-addon-sale-commission-cancel@git+https://github.com/tegin/cb-addons.git@16.
odoo-addon-sale-third-party@git+https://github.com/tegin/cb-addons.git@16.0#subdirectory=setup/sale_third_party
odoo-addon-sequence-parser@git+https://github.com/tegin/cb-addons.git@16.0#subdirectory=setup/sequence_parser
+# kiwi
+odoo-addon-queue-management-display@git+https://github.com/tegin/kiwi.git@16.0#subdirectory=setup/queue_management_display
+odoo-addon-queue-management@git+https://github.com/tegin/kiwi.git@16.0#subdirectory=setup/queue_management
+
# TO REMOVE
odoo-addon-commission-delegated-partner@git+https://github.com/dixmit/commission.git@16.0-mig-sale_commission_delegated_partner#subdirectory=setup/commission_delegated_partner