From 6be70466e53daf8b5abdcca51168ffa0229b9d2b Mon Sep 17 00:00:00 2001 From: Maxime Chambreuil Date: Tue, 16 Sep 2025 18:02:05 -0600 Subject: [PATCH 1/4] [ADD] lims --- lims/README.rst | 205 +++++++ lims/__init__.py | 4 + lims/__manifest__.py | 48 ++ lims/data/ir_sequence.xml | 28 + lims/data/lims_laboratory.xml | 5 + lims/data/lims_stage.xml | 26 + lims/data/lims_team.xml | 5 + lims/data/mail_message_subtype.xml | 59 ++ lims/data/module_category.xml | 6 + lims/models/__init__.py | 22 + lims/models/lims_batch.py | 150 +++++ lims/models/lims_category.py | 32 ++ lims/models/lims_equipment.py | 29 + lims/models/lims_laboratory.py | 24 + lims/models/lims_model_mixin.py | 56 ++ lims/models/lims_operator.py | 39 ++ lims/models/lims_operator_calendar_filter.py | 29 + lims/models/lims_order.py | 354 ++++++++++++ lims/models/lims_order_line.py | 200 +++++++ lims/models/lims_order_type.py | 16 + lims/models/lims_sample.py | 49 ++ lims/models/lims_stage.py | 108 ++++ lims/models/lims_tag.py | 32 ++ lims/models/lims_team.py | 89 +++ lims/models/lims_template.py | 26 + lims/models/res_company.py | 24 + lims/models/res_config_settings.py | 64 +++ lims/models/res_partner.py | 11 + lims/pyproject.toml | 3 + lims/readme/CONFIGURE.md | 72 +++ lims/readme/CONTRIBUTORS.md | 3 + lims/readme/CREDITS.md | 3 + lims/readme/DESCRIPTION.md | 1 + lims/readme/USAGE.md | 19 + lims/security/ir.model.access.csv | 26 + lims/security/ir_rule.xml | 61 ++ lims/security/res_groups.xml | 53 ++ lims/static/description/icon.jpg | Bin 0 -> 54988 bytes lims/static/description/icon.png | Bin 0 -> 14809 bytes lims/static/description/icon.svg | 54 ++ lims/static/description/index.html | 558 +++++++++++++++++++ lims/static/src/scss/team_dashboard.scss | 5 + lims/views/lims_batch.xml | 257 +++++++++ lims/views/lims_category.xml | 53 ++ lims/views/lims_equipment.xml | 199 +++++++ lims/views/lims_laboratory.xml | 147 +++++ lims/views/lims_operator.xml | 170 ++++++ lims/views/lims_order.xml | 395 +++++++++++++ lims/views/lims_order_line.xml | 378 +++++++++++++ lims/views/lims_order_type.xml | 37 ++ lims/views/lims_sample.xml | 69 +++ lims/views/lims_stage.xml | 97 ++++ lims/views/lims_tag.xml | 51 ++ lims/views/lims_team.xml | 247 ++++++++ lims/views/lims_template.xml | 72 +++ lims/views/menu.xml | 204 +++++++ lims/views/res_config_settings.xml | 105 ++++ lims/views/res_partner.xml | 47 ++ lims/wizard/__init__.py | 3 + lims/wizard/lims_wizard.py | 45 ++ lims/wizard/lims_wizard.xml | 40 ++ 61 files changed, 5214 insertions(+) create mode 100644 lims/README.rst create mode 100644 lims/__init__.py create mode 100644 lims/__manifest__.py create mode 100644 lims/data/ir_sequence.xml create mode 100644 lims/data/lims_laboratory.xml create mode 100644 lims/data/lims_stage.xml create mode 100644 lims/data/lims_team.xml create mode 100644 lims/data/mail_message_subtype.xml create mode 100644 lims/data/module_category.xml create mode 100644 lims/models/__init__.py create mode 100644 lims/models/lims_batch.py create mode 100644 lims/models/lims_category.py create mode 100644 lims/models/lims_equipment.py create mode 100644 lims/models/lims_laboratory.py create mode 100644 lims/models/lims_model_mixin.py create mode 100644 lims/models/lims_operator.py create mode 100644 lims/models/lims_operator_calendar_filter.py create mode 100644 lims/models/lims_order.py create mode 100644 lims/models/lims_order_line.py create mode 100644 lims/models/lims_order_type.py create mode 100644 lims/models/lims_sample.py create mode 100644 lims/models/lims_stage.py create mode 100644 lims/models/lims_tag.py create mode 100644 lims/models/lims_team.py create mode 100644 lims/models/lims_template.py create mode 100644 lims/models/res_company.py create mode 100644 lims/models/res_config_settings.py create mode 100644 lims/models/res_partner.py create mode 100644 lims/pyproject.toml create mode 100644 lims/readme/CONFIGURE.md create mode 100644 lims/readme/CONTRIBUTORS.md create mode 100644 lims/readme/CREDITS.md create mode 100644 lims/readme/DESCRIPTION.md create mode 100644 lims/readme/USAGE.md create mode 100644 lims/security/ir.model.access.csv create mode 100644 lims/security/ir_rule.xml create mode 100644 lims/security/res_groups.xml create mode 100644 lims/static/description/icon.jpg create mode 100644 lims/static/description/icon.png create mode 100644 lims/static/description/icon.svg create mode 100644 lims/static/description/index.html create mode 100644 lims/static/src/scss/team_dashboard.scss create mode 100644 lims/views/lims_batch.xml create mode 100644 lims/views/lims_category.xml create mode 100644 lims/views/lims_equipment.xml create mode 100644 lims/views/lims_laboratory.xml create mode 100644 lims/views/lims_operator.xml create mode 100644 lims/views/lims_order.xml create mode 100644 lims/views/lims_order_line.xml create mode 100644 lims/views/lims_order_type.xml create mode 100644 lims/views/lims_sample.xml create mode 100644 lims/views/lims_stage.xml create mode 100644 lims/views/lims_tag.xml create mode 100644 lims/views/lims_team.xml create mode 100644 lims/views/lims_template.xml create mode 100644 lims/views/menu.xml create mode 100644 lims/views/res_config_settings.xml create mode 100644 lims/views/res_partner.xml create mode 100644 lims/wizard/__init__.py create mode 100644 lims/wizard/lims_wizard.py create mode 100644 lims/wizard/lims_wizard.xml diff --git a/lims/README.rst b/lims/README.rst new file mode 100644 index 0000000..78831d5 --- /dev/null +++ b/lims/README.rst @@ -0,0 +1,205 @@ +=============================================== +Laboratory Information Management System (LIMS) +=============================================== + +.. + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! This file is generated by oca-gen-addon-readme !! + !! changes will be overwritten. !! + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! source digest: sha256:ffab1ec59149ad1f3ae402cbbc3dcf330af32e4d971e498b3b2fcf487fcb200a + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + +.. |badge1| image:: https://img.shields.io/badge/maturity-Beta-yellow.png + :target: https://odoo-community.org/page/development-status + :alt: Beta +.. |badge2| image:: https://img.shields.io/badge/licence-AGPL--3-blue.png + :target: http://www.gnu.org/licenses/agpl-3.0-standalone.html + :alt: License: AGPL-3 +.. |badge3| image:: https://img.shields.io/badge/github-OCA%2Fconnector--lims-lightgray.png?logo=github + :target: https://github.com/OCA/connector-lims/tree/18.0/lims + :alt: OCA/connector-lims +.. |badge4| image:: https://img.shields.io/badge/weblate-Translate%20me-F47D42.png + :target: https://translation.odoo-community.org/projects/connector-lims-18-0/connector-lims-18-0-lims + :alt: Translate me on Weblate +.. |badge5| image:: https://img.shields.io/badge/runboat-Try%20me-875A7B.png + :target: https://runboat.odoo-community.org/builds?repo=OCA/connector-lims&target_branch=18.0 + :alt: Try me on Runboat + +|badge1| |badge2| |badge3| |badge4| |badge5| + +This module is the base of the LIMS application in Odoo. + +**Table of contents** + +.. contents:: + :local: + +Configuration +============= + +The base LIMS module can be used with minimal initial configuration. It +also allows for many advanced features, which require a more in-depth +configuration. + +Analysis Stages +--------------- + +The stage of an analysis is used to monitor its progress. Stages can be +configured based on your company's specific business needs. A basic set +of analysis stages comes pre-configured for use. + +1. Go to *LIMS > Configuration > Stages* +2. Create or edit a stage +3. Set the name for the stage. +4. Set the sequence order for the stage. +5. Select *Order* type to apply this stage to your analysis. +6. Additionally, you can set a color for the stage. + +Laboratories +------------ + +You can manage different laboratories. + +Advanced Configurations +----------------------- + +Additional features can be enabled in the General Settings panel for +LIMS. + +1. Go to *LIMS > Configuration > Settings* +2. Enable additional options +3. Configure new options + +Manage Teams +~~~~~~~~~~~~ + +Teams can be used to organize the processing of analysis into groups. +Different teams may have different workflows that an analysis needs to +follow. + +1. Go to *LIMS > Configuration > Operators > Teams* +2. Create or select a team +3. Set the team name, description, and sequence + +You can now define custom stages for each team processing analysis. + +1. Go to *LIMS > Configuration > Stages* +2. Create or edit a stage +3. Select the teams for which this stage should be used + +Manage Categories +~~~~~~~~~~~~~~~~~ + +Categories are used to group operators and the type of analysis an +operator can do. + +1. Go to *LIMS > Configuration > Operators > Categories* +2. Create or select a category +3. Set the name and description of category +4. Additionally, you can select a parent category if required + +Manage Tags +~~~~~~~~~~~ + +Tags can be used to filter and report on analysis + +1. Go to *LIMS > Configuration > Analysis > Tags* +2. Create or select a tag +3. Set the tag name +4. Set a color index for the tag + +Manage Templates +~~~~~~~~~~~~~~~~ + +Templates allow you to create standard templates for your analysis. + +1. Go to *LIMS > Master Data > Templates* +2. Create or select a template +3. Set the name +4. Set the standard order instructions + +Usage +===== + +To use this module, you need to: + +Add LIMS Operators +------------------ + +Operators are the people responsible for performing an analysis. These +operators may be subcontractors or the company's own employees. + +1. Go to *LIMS > Master Data > Operators* +2. Create an operator + +Process Analysis +---------------- + +Once you have established your data, you can begin processing analysis. + +1. Go to *LIMS > Dashboard > Analysis* +2. Create or select an analysis +3. Enter relevant details for the analysis +4. Process the analysis through each stage as defined by your business + requirements + +Bug Tracker +=========== + +Bugs are tracked on `GitHub Issues `_. +In case of trouble, please check there if your issue has already been reported. +If you spotted it first, help us to smash it by providing a detailed and welcomed +`feedback `_. + +Do not contact contributors directly about support or help with technical issues. + +Credits +======= + +Authors +------- + +* Open Source Integrators + +Contributors +------------ + +- Rodrigo Madrid Carmona +- Johannan Jasiel Luna García +- Maxime Chambreuil + +Other credits +------------- + +The development of this module has been financially supported by: + +- Ganaderos Asociados de Querétaro + +Maintainers +----------- + +This module is maintained by the OCA. + +.. image:: https://odoo-community.org/logo.png + :alt: Odoo Community Association + :target: https://odoo-community.org + +OCA, or the Odoo Community Association, is a nonprofit organization whose +mission is to support the collaborative development of Odoo features and +promote its widespread use. + +.. |maintainer-max3903| image:: https://github.com/max3903.png?size=40px + :target: https://github.com/max3903 + :alt: max3903 +.. |maintainer-jasiel-osi| image:: https://github.com/jasiel-osi.png?size=40px + :target: https://github.com/jasiel-osi + :alt: jasiel-osi + +Current `maintainers `__: + +|maintainer-max3903| |maintainer-jasiel-osi| + +This module is part of the `OCA/connector-lims `_ project on GitHub. + +You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute. diff --git a/lims/__init__.py b/lims/__init__.py new file mode 100644 index 0000000..4d7a49b --- /dev/null +++ b/lims/__init__.py @@ -0,0 +1,4 @@ +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +from . import models +from . import wizard diff --git a/lims/__manifest__.py b/lims/__manifest__.py new file mode 100644 index 0000000..462cfa4 --- /dev/null +++ b/lims/__manifest__.py @@ -0,0 +1,48 @@ +# Copyright (C) 2025 Open Source Integrators +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). +{ + "name": "Laboratory Information Management System (LIMS)", + "summary": "Manage LIMS Equipments, Analysis and Work Orders", + "version": "18.0.1.0.0", + "license": "AGPL-3", + "category": "LIMS", + "author": "Open Source Integrators, Odoo Community Association (OCA)", + "website": "https://github.com/OCA/connector-lims", + "depends": ["mail", "resource"], + "data": [ + "data/ir_sequence.xml", + "data/mail_message_subtype.xml", + "data/module_category.xml", + "data/lims_stage.xml", + "data/lims_team.xml", + "data/lims_laboratory.xml", + "security/res_groups.xml", + "security/ir.model.access.csv", + "security/ir_rule.xml", + "views/res_config_settings.xml", + "views/lims_stage.xml", + "views/lims_tag.xml", + "views/res_partner.xml", + "views/lims_laboratory.xml", + "views/lims_operator.xml", + "views/lims_sample.xml", + "views/lims_order_line.xml", + "views/lims_order.xml", + "views/lims_order_type.xml", + "views/lims_batch.xml", + "views/lims_category.xml", + "views/lims_equipment.xml", + "views/lims_template.xml", + "views/lims_team.xml", + "views/menu.xml", + "wizard/lims_wizard.xml", + ], + "application": True, + "development_status": "Beta", + "maintainers": ["max3903", "jasiel-osi"], + "assets": { + "web.assets_backend": [ + "lims/static/src/scss/team_dashboard.scss", + ] + }, +} diff --git a/lims/data/ir_sequence.xml b/lims/data/ir_sequence.xml new file mode 100644 index 0000000..36c8ef8 --- /dev/null +++ b/lims/data/ir_sequence.xml @@ -0,0 +1,28 @@ + + + + LIMS Analysis + lims.order + LA + 3 + + + + + + LIMS Work Orders + lims.order.line + LWO + 3 + + + + + + LIMS Batch + lims.batch + LB + 3 + + + diff --git a/lims/data/lims_laboratory.xml b/lims/data/lims_laboratory.xml new file mode 100644 index 0000000..d2449c9 --- /dev/null +++ b/lims/data/lims_laboratory.xml @@ -0,0 +1,5 @@ + + + Default Laboratory + + diff --git a/lims/data/lims_stage.xml b/lims/data/lims_stage.xml new file mode 100644 index 0000000..a0f9d4e --- /dev/null +++ b/lims/data/lims_stage.xml @@ -0,0 +1,26 @@ + + + New + 10 + True + order + #ECF0F1 + + + Completed + 80 + order + True + True + #7F8C8D + + + Cancelled + 100 + True + order + True + True + #1C2833 + + diff --git a/lims/data/lims_team.xml b/lims/data/lims_team.xml new file mode 100644 index 0000000..2499987 --- /dev/null +++ b/lims/data/lims_team.xml @@ -0,0 +1,5 @@ + + + Default Team + + diff --git a/lims/data/mail_message_subtype.xml b/lims/data/mail_message_subtype.xml new file mode 100644 index 0000000..4d129eb --- /dev/null +++ b/lims/data/mail_message_subtype.xml @@ -0,0 +1,59 @@ + + + + Analysis Created + 0 + lims.order + + + Analysis created + + + Analysis Confirmed + 10 + lims.order + + + Analysis confirmed + + + Analysis Scheduled + 20 + lims.order + + + Analysis scheduled + + + Analysis Assigned + 30 + lims.order + + + Analysis assigned + + + Analysis Started + 40 + lims.order + + + Analysis started + + + Analysis Completed + 50 + lims.order + + + Analysis completed + + + Analysis Cancelled + 100 + lims.order + + + Analysis cancelled + + diff --git a/lims/data/module_category.xml b/lims/data/module_category.xml new file mode 100644 index 0000000..1b9f8f2 --- /dev/null +++ b/lims/data/module_category.xml @@ -0,0 +1,6 @@ + + + LIMS + 20 + + diff --git a/lims/models/__init__.py b/lims/models/__init__.py new file mode 100644 index 0000000..893de72 --- /dev/null +++ b/lims/models/__init__.py @@ -0,0 +1,22 @@ +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +from . import ( + res_company, + res_config_settings, + lims_model_mixin, + lims_category, + lims_template, + lims_tag, + lims_stage, + lims_team, + lims_laboratory, + lims_equipment, + lims_sample, + lims_operator, + lims_operator_calendar_filter, + lims_order_line, + lims_order, + lims_order_type, + lims_batch, + res_partner, +) diff --git a/lims/models/lims_batch.py b/lims/models/lims_batch.py new file mode 100644 index 0000000..a3426ca --- /dev/null +++ b/lims/models/lims_batch.py @@ -0,0 +1,150 @@ +# Copyright (C) 2025 Open Source Integrators +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + + +from odoo import _, api, fields, models +from odoo.exceptions import ValidationError + +from . import lims_stage + + +class LIMSBatch(models.Model): + _name = "lims.batch" + _description = "LIMS Batch" + _inherit = ["mail.thread", "mail.activity.mixin", "lims.model.mixin"] + + def _default_stage_id(self): + stage = self.env["lims.stage"].search( + [ + ("stage_type", "=", "batch"), + ("is_default", "=", True), + ("company_id", "in", (self.env.company.id, False)), + ], + order="sequence asc", + limit=1, + ) + if stage: + return stage + raise ValidationError(_("You must create a LIMS order stage first.")) + + def _default_team_id(self): + team = self.env["lims.team"].search( + [("company_id", "in", (self.env.company.id, False))], + order="sequence asc", + limit=1, + ) + if team: + return team + raise ValidationError(_("You must create an LIMS team first.")) + + def _default_laboratory_id(self): + rec = self.env["lims.laboratory"].search( + [("company_id", "in", (self.env.company.id, False))], + order="sequence asc", + limit=1, + ) + if rec: + return rec + raise ValidationError(_("You must create a laboratory first.")) + + stage_id = fields.Many2one( + "lims.stage", + string="Stage", + tracking=True, + index=True, + copy=False, + group_expand="_read_group_stage_ids", + default=lambda self: self._default_stage_id(), + ) + is_closed = fields.Boolean( + "Is closed", + related="stage_id.is_closed", + ) + priority = fields.Selection( + lims_stage.AVAILABLE_PRIORITIES, + index=True, + default=lims_stage.AVAILABLE_PRIORITIES[0][0], + ) + laboratory_id = fields.Many2one( + "lims.laboratory", + string="Laboratory", + default=lambda self: self._default_laboratory_id(), + index=True, + required=True, + tracking=True, + ) + team_id = fields.Many2one( + "lims.team", + string="Team", + default=lambda self: self._default_team_id(), + index=True, + required=True, + tracking=True, + ) + + # Request + name = fields.Char( + required=True, + index=True, + copy=False, + default=lambda self: _("New"), + ) + type = fields.Many2one("lims.order.type") + internal_type = fields.Selection(related="type.internal_type") + line_ids = fields.One2many("lims.order.line", "batch_id", string="Work Orders") + company_id = fields.Many2one( + "res.company", + string="Company", + required=True, + index=True, + default=lambda self: self.env.company, + help="Company related to this order", + ) + + description = fields.Text() + + # Planning + operator_id = fields.Many2one("lims.operator", string="Assigned To", index=True) + sequence = fields.Integer(default=10) + equipment_id = fields.Many2one("lims.equipment", string="Equipment") + scheduled_date_start = fields.Datetime(string="Scheduled Start (ETA)") + scheduled_duration = fields.Float(help="Scheduled duration in hours") + scheduled_date_end = fields.Datetime(string="Scheduled End") + + @api.model + def _read_group_stage_ids(self, stages, domain, order=None): + search_domain = [("stage_type", "=", "batch")] + if self.env.context.get("default_team_id"): + search_domain = [ + "&", + ("team_ids", "in", self.env.context["default_team_id"]), + ] + search_domain + return stages.search(search_domain, order=order) + + @api.model_create_multi + def create(self, vals_list): + for vals in vals_list: + if vals.get("name", _("New")) == _("New"): + vals["name"] = self.env["ir.sequence"].next_by_code("lims.batch") or _( + "New" + ) + return super().create(vals_list) + + def can_unlink(self): + """:return True if the order can be deleted, False otherwise""" + return self.stage_id == self._default_stage_id() + + def unlink(self): + if all(rec.can_unlink() for rec in self): + return super().unlink() + raise ValidationError(_("You cannot delete this batch.")) + + def action_complete(self): + return self.write( + { + "stage_id": self.env.ref("lims.lims_stage_completed").id, + } + ) + + def action_cancel(self): + return self.write({"stage_id": self.env.ref("lims.lims_stage_cancelled").id}) diff --git a/lims/models/lims_category.py b/lims/models/lims_category.py new file mode 100644 index 0000000..c78499f --- /dev/null +++ b/lims/models/lims_category.py @@ -0,0 +1,32 @@ +# Copyright (C) 2025 Open Source Integrators +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +from odoo import fields, models + + +class LIMSCategory(models.Model): + _name = "lims.category" + _description = "LIMS Category" + + name = fields.Char(required=True) + parent_id = fields.Many2one("lims.category", string="Parent") + color = fields.Integer("Color Index", default=10) + full_name = fields.Char(compute="_compute_full_name") + description = fields.Char() + company_id = fields.Many2one( + "res.company", + string="Company", + required=False, + index=True, + help="Company related to this category", + ) + + _sql_constraints = [("name_uniq", "unique (name)", "Category name already exists!")] + + def _compute_full_name(self): + for record in self: + record.full_name = ( + record.parent_id.full_name + "/" + record.name + if record.parent_id + else record.name + ) diff --git a/lims/models/lims_equipment.py b/lims/models/lims_equipment.py new file mode 100644 index 0000000..ed9c315 --- /dev/null +++ b/lims/models/lims_equipment.py @@ -0,0 +1,29 @@ +# Copyright (C) 2025 - TODAY, Open Source Integrators +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +from odoo import fields, models + + +class LIMSEquipment(models.Model): + _name = "lims.equipment" + _description = "LIMS Equipment" + _inherit = ["mail.thread", "mail.activity.mixin", "lims.model.mixin"] + _stage_type = "equipment" + + name = fields.Char(required=True) + operator_id = fields.Many2one("lims.operator", string="Assigned Operator") + notes = fields.Text() + color = fields.Integer("Color Index") + laboratory_id = fields.Many2one("lims.laboratory", string="Laboratory") + company_id = fields.Many2one( + "res.company", + string="Company", + required=True, + index=True, + default=lambda self: self.env.company, + help="Company related to this equipment", + ) + + _sql_constraints = [ + ("name_uniq", "unique (name)", "Equipment name already exists!") + ] diff --git a/lims/models/lims_laboratory.py b/lims/models/lims_laboratory.py new file mode 100644 index 0000000..965b999 --- /dev/null +++ b/lims/models/lims_laboratory.py @@ -0,0 +1,24 @@ +# Copyright (C) 2025 Open Source Integrators +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +from odoo import fields, models + + +class LIMSLaboratory(models.Model): + _name = "lims.laboratory" + _description = "Laboratories" + _inherits = {"res.partner": "partner_id"} + _inherit = ["mail.thread", "mail.activity.mixin"] + + partner_id = fields.Many2one( + "res.partner", + string="Related Partner", + required=True, + ondelete="restrict", + delegate=True, + auto_join=True, + ) + + sequence = fields.Integer(default=1, help="Used to order laboratories.") + description = fields.Text(translate=True) + color = fields.Integer("Color Index") diff --git a/lims/models/lims_model_mixin.py b/lims/models/lims_model_mixin.py new file mode 100644 index 0000000..81126e9 --- /dev/null +++ b/lims/models/lims_model_mixin.py @@ -0,0 +1,56 @@ +# Copyright (C) 2025 Open Source Integrators +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +from odoo import api, fields, models + + +class LIMSModelMixin(models.AbstractModel): + _name = "lims.model.mixin" + _description = "LIMS Model Mixin" + _stage_type = "" + + stage_id = fields.Many2one( + "lims.stage", + string="Stage", + tracking=True, + index=True, + copy=False, + group_expand="_read_group_stage_ids", + default=lambda self: self._default_stage_id(), + ) + hide = fields.Boolean() + + @api.model + def _read_group_stage_ids(self, stages, domain): + return self.env["lims.stage"].search([("stage_type", "=", self._stage_type)]) + + def _default_stage_id(self): + return self.env["lims.stage"].search( + [("stage_type", "=", self._stage_type)], limit=1 + ) + + def new_stage(self, operator): + seq = self.stage_id.sequence + order_by = "asc" if operator == ">" else "desc" + new_stage = self.env["lims.stage"].search( + [("stage_type", "=", self._stage_type), ("sequence", operator, seq)], + order=f"sequence {order_by}", + limit=1, + ) + if new_stage: + self.stage_id = new_stage + self._onchange_stage_id() + + def next_stage(self): + self.new_stage(">") + + def previous_stage(self): + self.new_stage("<") + + @api.onchange("stage_id") + def _onchange_stage_id(self): + # get last stage + highest_stage = self.env["lims.stage"].search( + [("stage_type", "=", self._stage_type)], order="sequence desc", limit=1 + ) + self.hide = self.stage_id.name == highest_stage.name diff --git a/lims/models/lims_operator.py b/lims/models/lims_operator.py new file mode 100644 index 0000000..ffc620d --- /dev/null +++ b/lims/models/lims_operator.py @@ -0,0 +1,39 @@ +# Copyright (C) 2025 Open Source Integrators +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +from odoo import api, fields, models + + +class LIMSOperator(models.Model): + _name = "lims.operator" + _inherits = {"res.partner": "partner_id"} + _inherit = ["mail.thread.blacklist", "lims.model.mixin"] + _description = "LIMS Operator" + _stage_type = "operator" + + partner_id = fields.Many2one( + "res.partner", + string="Related Partner", + required=True, + ondelete="restrict", + delegate=True, + auto_join=True, + ) + category_ids = fields.Many2many("lims.category", string="Categories") + calendar_id = fields.Many2one("resource.calendar", string="Working Schedule") + active = fields.Boolean(default=True) + active_partner = fields.Boolean( + related="partner_id.active", readonly=True, string="Partner is Active" + ) + + def toggle_active(self): + for person in self: + if not person.active and not person.partner_id.active: + person.partner_id.toggle_active() + return super().toggle_active() + + @api.model_create_multi + def create(self, vals_list): + for vals in vals_list: + vals.update({"lims_operator": True}) + return super().create(vals_list) diff --git a/lims/models/lims_operator_calendar_filter.py b/lims/models/lims_operator_calendar_filter.py new file mode 100644 index 0000000..b5a322b --- /dev/null +++ b/lims/models/lims_operator_calendar_filter.py @@ -0,0 +1,29 @@ +# Copyright (C) 2025 Open Source Integrators +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). +from odoo import fields, models + + +class LIMSOperatorCalendarFilter(models.Model): + """Assigned Operator Calendar Filter""" + + _name = "lims.operator.calendar.filter" + _description = "LIMS Operator Calendar Filter" + + user_id = fields.Many2one( + "res.users", + "Me", + required=True, + default=lambda self: self.env.user, + ondelete="cascade", + ) + operator_id = fields.Many2one("lims.operator", "LIMS Operator", required=True) + active = fields.Boolean(default=True) + person_checked = fields.Boolean(default=True) + + _sql_constraints = [ + ( + "user_id_lims_operator_id_unique", + "UNIQUE(user_id,operator_id)", + "You cannot have the same operator twice.", + ) + ] diff --git a/lims/models/lims_order.py b/lims/models/lims_order.py new file mode 100644 index 0000000..1a2c795 --- /dev/null +++ b/lims/models/lims_order.py @@ -0,0 +1,354 @@ +# Copyright (C) 2025 Open Source Integrators +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +from datetime import datetime, timedelta + +from odoo import _, api, fields, models +from odoo.exceptions import UserError, ValidationError + +from . import lims_stage + + +class LIMSOrder(models.Model): + _name = "lims.order" + _description = "LIMS Order" + _inherit = ["mail.thread", "mail.activity.mixin"] + + def _default_stage_id(self): + stage = self.env["lims.stage"].search( + [ + ("stage_type", "=", "order"), + ("is_default", "=", True), + ("company_id", "in", (self.env.company.id, False)), + ], + order="sequence asc", + limit=1, + ) + if stage: + return stage + raise ValidationError(_("You must create a LIMS order stage first.")) + + def _default_team_id(self): + team = self.env["lims.team"].search( + [("company_id", "in", (self.env.company.id, False))], + order="sequence asc", + limit=1, + ) + if team: + return team + raise ValidationError(_("You must create an LIMS team first.")) + + def _default_laboratory_id(self): + rec = self.env["lims.laboratory"].search( + [("company_id", "in", (self.env.company.id, False))], + order="sequence asc", + limit=1, + ) + if rec: + return rec + raise ValidationError(_("You must create a laboratory first.")) + + @api.depends("date_start", "date_end") + def _compute_duration(self): + for rec in self: + duration = 0.0 + if rec.date_start and rec.date_end: + start = fields.Datetime.from_string(rec.date_start) + end = fields.Datetime.from_string(rec.date_end) + delta = end - start + duration = delta.total_seconds() / 3600 + rec.duration = duration + + @api.depends("stage_id") + def _get_stage_color(self): + """Get stage color""" + self.custom_color = self.stage_id.custom_color or "#FFFFFF" + + def _track_subtype(self, init_values): + self.ensure_one() + if "stage_id" in init_values: + if self.stage_id.id == self.env.ref("lims.lims_stage_completed").id: + return self.env.ref("lims.mt_order_completed") + elif self.stage_id.id == self.env.ref("lims.lims_stage_cancelled").id: + return self.env.ref("lims.mt_order_cancelled") + return super()._track_subtype(init_values) + + def _calc_request_late(self, vals): + if vals.get("request_early", False): + early = fields.Datetime.from_string(vals.get("request_early")) + else: + early = datetime.now() + + if vals.get("priority") == "0": + vals["request_late"] = early + timedelta( + hours=self.env.company.lims_order_request_late_lowest + ) + elif vals.get("priority") == "1": + vals["request_late"] = early + timedelta( + hours=self.env.company.lims_order_request_late_low + ) + elif vals.get("priority") == "2": + vals["request_late"] = early + timedelta( + hours=self.env.company.lims_order_request_late_medium + ) + elif vals.get("priority") == "3": + vals["request_late"] = early + timedelta( + hours=self.env.company.lims_order_request_late_high + ) + return vals + + stage_id = fields.Many2one( + "lims.stage", + string="Stage", + tracking=True, + index=True, + copy=False, + group_expand="_read_group_stage_ids", + default=lambda self: self._default_stage_id(), + ) + is_closed = fields.Boolean( + "Is closed", + related="stage_id.is_closed", + ) + priority = fields.Selection( + lims_stage.AVAILABLE_PRIORITIES, + index=True, + default=lims_stage.AVAILABLE_PRIORITIES[0][0], + ) + tag_ids = fields.Many2many( + "lims.tag", + "lims_order_tag_rel", + "lims_order_id", + "tag_id", + string="Tags", + help="Classify and analyze your analysis", + ) + color = fields.Integer("Color Index", default=0) + laboratory_id = fields.Many2one( + "lims.laboratory", + string="Laboratory", + default=lambda self: self._default_laboratory_id(), + index=True, + required=True, + tracking=True, + ) + team_id = fields.Many2one( + "lims.team", + string="Team", + default=lambda self: self._default_team_id(), + index=True, + required=True, + tracking=True, + ) + + # Request + name = fields.Char( + required=True, + index=True, + copy=False, + default=lambda self: _("New"), + ) + line_ids = fields.One2many("lims.order.line", "order_id", string="Work Orders") + partner_id = fields.Many2one( + "res.partner", + string="Partner", + tracking=True, + index=True, + copy=False, + ) + request_early = fields.Datetime( + string="Earliest Request Date", default=datetime.now() + ) + request_late = fields.Datetime(string="Latest Request Date") + color = fields.Integer("Color Index") + company_id = fields.Many2one( + "res.company", + string="Company", + required=True, + index=True, + default=lambda self: self.env.company, + help="Company related to this order", + ) + + description = fields.Text() + + # Planning + operator_id = fields.Many2one("lims.operator", string="Assigned To", index=True) + sample_id = fields.Many2one("lims.sample", string="Sample", index=True) + scheduled_date_start = fields.Datetime(string="Scheduled Start (ETA)") + scheduled_duration = fields.Float(help="Scheduled duration of the work in hours") + scheduled_date_end = fields.Datetime(string="Scheduled End") + sequence = fields.Integer(default=10) + + # Execution + date_start = fields.Datetime(string="Actual Start") + date_end = fields.Datetime(string="Actual End") + duration = fields.Float( + string="Actual duration", + compute=_compute_duration, + help="Actual duration in hours", + ) + current_date = fields.Datetime(default=fields.Datetime.now, store=True) + + # Field for Stage Color + custom_color = fields.Char(related="stage_id.custom_color", string="Stage Color") + + # Template + template_id = fields.Many2one("lims.template", string="Template") + category_ids = fields.Many2many("lims.category", string="Categories") + + # Equipment used for Maintenance + equipment_id = fields.Many2one("lims.equipment", string="Equipment") + + type = fields.Many2one("lims.order.type") + internal_type = fields.Selection(related="type.internal_type") + + @api.model + def _read_group_stage_ids(self, stages, domain, order=None): + search_domain = [("stage_type", "=", "order")] + if self.env.context.get("default_team_id"): + search_domain = [ + "&", + ("team_ids", "in", self.env.context["default_team_id"]), + ] + search_domain + return stages.search(search_domain, order=order) + + @api.model_create_multi + def create(self, vals_list): + for vals in vals_list: + if vals.get("name", _("New")) == _("New"): + vals["name"] = self.env["ir.sequence"].next_by_code("lims.order") or _( + "New" + ) + self._calc_scheduled_dates(vals) + if not vals.get("request_late"): + vals = self._calc_request_late(vals) + return super().create(vals_list) + + is_button = fields.Boolean(default=False) + + def write(self, vals): + if vals.get("stage_id", False) and vals.get("is_button", False): + vals["is_button"] = False + else: + stage_id = self.env["lims.stage"].browse(vals.get("stage_id")) + if stage_id == self.env.ref("lims.lims_stage_completed"): + raise UserError(_("Cannot move to completed from Kanban")) + self._calc_scheduled_dates(vals) + res = super().write(vals) + return res + + def can_unlink(self): + """:return True if the order can be deleted, False otherwise""" + return self.stage_id == self._default_stage_id() + + def unlink(self): + if all(order.can_unlink() for order in self): + return super().unlink() + raise ValidationError(_("You cannot delete this order.")) + + def _calc_scheduled_dates(self, vals): + """Calculate scheduled dates and duration""" + + if ( + vals.get("scheduled_duration") is not None + or vals.get("scheduled_date_start") + or vals.get("scheduled_date_end") + ): + if vals.get("scheduled_date_start") and vals.get("scheduled_date_end"): + new_date_start = fields.Datetime.from_string( + vals.get("scheduled_date_start", False) + ) + new_date_end = fields.Datetime.from_string( + vals.get("scheduled_date_end", False) + ) + hours = new_date_end.replace(second=0) - new_date_start.replace( + second=0 + ) + hrs = hours.total_seconds() / 3600 + vals["scheduled_duration"] = float(hrs) + + elif vals.get("scheduled_date_end"): + hrs = ( + vals.get("scheduled_duration", False) + or self.scheduled_duration + or 0 + ) + date_to_with_delta = fields.Datetime.from_string( + vals.get("scheduled_date_end", False) + ) - timedelta(hours=hrs) + vals["scheduled_date_start"] = str(date_to_with_delta) + + elif ( + vals.get("scheduled_duration", False) is not None + and vals.get("scheduled_date_start", self.scheduled_date_start) + and ( + self.scheduled_date_start != vals.get("scheduled_date_start", False) + ) + ): + hours = vals.get("scheduled_duration", False) + start_date_val = vals.get( + "scheduled_date_start", self.scheduled_date_start + ) + start_date = fields.Datetime.from_string(start_date_val) + date_to_with_delta = start_date + timedelta(hours=hours) + vals["scheduled_date_end"] = str(date_to_with_delta) + elif vals.get("scheduled_date_start") is not None: + vals["scheduled_date_end"] = False + + def action_complete(self): + return self.write( + { + "stage_id": self.env.ref("lims.lims_stage_completed").id, + "is_button": True, + } + ) + + def action_cancel(self): + return self.write({"stage_id": self.env.ref("lims.lims_stage_cancelled").id}) + + @api.onchange("scheduled_date_end") + def onchange_scheduled_date_end(self): + if self.scheduled_date_end: + date_to_with_delta = fields.Datetime.from_string( + self.scheduled_date_end + ) - timedelta(hours=self.scheduled_duration) + self.date_start = str(date_to_with_delta) + + @api.onchange("scheduled_date_start", "scheduled_duration") + def onchange_scheduled_duration(self): + if self.scheduled_duration and self.scheduled_date_start: + date_to_with_delta = fields.Datetime.from_string( + self.scheduled_date_start + ) + timedelta(hours=self.scheduled_duration) + self.scheduled_date_end = str(date_to_with_delta) + else: + self.scheduled_date_end = self.scheduled_date_start + + @api.onchange("template_id") + def _onchange_template_id(self): + if self.template_id: + self.category_ids = self.template_id.category_ids + self.scheduled_duration = self.template_id.duration + if self.template_id.type_id: + self.type = self.template_id.type_id + if self.template_id.team_id: + self.team_id = self.template_id.team_id + + @api.constrains("scheduled_date_start") + def check_day(self): + for rec in self: + if not rec.scheduled_date_start: + continue + + holidays = self.env["resource.calendar.leaves"].search( + [ + ("date_from", ">=", rec.scheduled_date_start), + ("date_to", "<=", rec.scheduled_date_end), + ] + ) + if holidays: + msg = ( + f"{rec.scheduled_date_start.date()} is a holiday {holidays[0].name}" + ) + raise ValidationError(_(msg)) diff --git a/lims/models/lims_order_line.py b/lims/models/lims_order_line.py new file mode 100644 index 0000000..5a64abe --- /dev/null +++ b/lims/models/lims_order_line.py @@ -0,0 +1,200 @@ +# Copyright (C) 2025 Open Source Integrators +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +from datetime import datetime + +from odoo import _, api, fields, models +from odoo.exceptions import ValidationError + +from . import lims_stage + + +class LIMSOrderLine(models.Model): + _name = "lims.order.line" + _description = "LIMS Order Line" + _inherit = ["mail.thread", "mail.activity.mixin"] + + def _default_stage_id(self): + stage = self.env["lims.stage"].search( + [ + ("stage_type", "=", "order"), + ("is_default", "=", True), + ("company_id", "in", (self.env.company.id, False)), + ], + order="sequence asc", + limit=1, + ) + if stage: + return stage + raise ValidationError(_("You must create a LIMS order stage first.")) + + @api.depends("date_start", "date_end") + def _compute_duration(self): + for rec in self: + duration = 0.0 + if rec.date_start and rec.date_end: + start = fields.Datetime.from_string(rec.date_start) + end = fields.Datetime.from_string(rec.date_end) + delta = end - start + duration = delta.total_seconds() / 3600 + rec.duration = duration + + def _track_subtype(self, init_values): + self.ensure_one() + if "stage_id" in init_values: + if self.stage_id.id == self.env.ref("lims.lims_stage_completed").id: + return self.env.ref("lims.mt_order_completed") + elif self.stage_id.id == self.env.ref("lims.lims_stage_cancelled").id: + return self.env.ref("lims.mt_order_cancelled") + return super()._track_subtype(init_values) + + stage_id = fields.Many2one( + "lims.stage", + string="Stage", + tracking=True, + index=True, + copy=False, + group_expand="_read_group_stage_ids", + default=lambda self: self._default_stage_id(), + ) + is_closed = fields.Boolean( + "Is closed", + related="stage_id.is_closed", + ) + priority = fields.Selection( + lims_stage.AVAILABLE_PRIORITIES, + index=True, + default=lims_stage.AVAILABLE_PRIORITIES[0][0], + ) + category_ids = fields.Many2many("lims.category", string="Categories") + tag_ids = fields.Many2many( + "lims.tag", + "lims_order_line_tag_rel", + "lims_order_line_id", + "tag_id", + string="Tags", + help="Classify and analyze your work orders", + ) + color = fields.Integer("Color Index", default=0) + batch_id = fields.Many2one( + "lims.batch", + string="Batch", + index=True, + ) + laboratory_id = fields.Many2one( + "lims.laboratory", + string="Laboratory", + related="order_id.laboratory_id", + index=True, + required=True, + tracking=True, + ) + team_id = fields.Many2one( + "lims.team", + string="Team", + related="order_id.team_id", + index=True, + required=True, + tracking=True, + ) + + # Request + name = fields.Char( + required=True, + index=True, + copy=False, + default=lambda self: _("New"), + ) + order_id = fields.Many2one( + "lims.order", + string="Analysis", + required=True, + index=True, + ) + request_early = fields.Datetime( + string="Earliest Request Date", default=datetime.now() + ) + request_late = fields.Datetime(string="Latest Request Date") + company_id = fields.Many2one( + "res.company", + string="Company", + required=True, + index=True, + default=lambda self: self.env.company, + help="Company related to this order", + ) + + # Planning + sample_id = fields.Many2one( + "lims.sample", string="Sample", related="order_id.sample_id", index=True + ) + operator_id = fields.Many2one("lims.operator", string="Assigned To", index=True) + scheduled_date_start = fields.Datetime(string="Scheduled Start (ETA)") + scheduled_duration = fields.Float(help="Scheduled duration of the work in hours") + scheduled_date_end = fields.Datetime(string="Scheduled End") + + # Execution + date_start = fields.Datetime(string="Actual Start") + date_end = fields.Datetime(string="Actual End") + duration = fields.Float( + string="Actual duration", + compute=_compute_duration, + help="Actual duration in hours", + ) + todo = fields.Text(string="Instructions") + current_date = fields.Datetime(default=fields.Datetime.now, store=True) + + # Equipment used for Maintenance + equipment_id = fields.Many2one("lims.equipment", string="Equipment") + + type = fields.Many2one("lims.order.type") + internal_type = fields.Selection(related="type.internal_type") + + # Result + result_type = fields.Selection( + [ + ("manual", "Manual"), + ("calculated", "Calculated"), + ], + required=True, + default="manual", + ) + result_formula = fields.Text() + result_min = fields.Float(string="Minimum") + result_max = fields.Float(string="Maximum") + result_value = fields.Float(string="Value") + success = fields.Boolean() + + @api.model + def _read_group_stage_ids(self, stages, domain, order=None): + search_domain = [("stage_type", "=", "order")] + if self.env.context.get("default_team_id"): + search_domain = [ + "&", + ("team_ids", "in", self.env.context["default_team_id"]), + ] + search_domain + return stages.search(search_domain, order=order) + + @api.model_create_multi + def create(self, vals_list): + for vals in vals_list: + if vals.get("name", _("New")) == _("New"): + vals["name"] = self.env["ir.sequence"].next_by_code( + "lims.order.line" + ) or _("New") + return super().create(vals_list) + + def can_unlink(self): + """:return True if the order can be deleted, False otherwise""" + return self.stage_id == self._default_stage_id() + + def unlink(self): + if all(order.can_unlink() for order in self): + return super().unlink() + raise ValidationError(_("You cannot delete this order.")) + + def action_complete(self): + return self.write({"stage_id": self.env.ref("lims.lims_stage_completed").id}) + + def action_cancel(self): + return self.write({"stage_id": self.env.ref("lims.lims_stage_cancelled").id}) diff --git a/lims/models/lims_order_type.py b/lims/models/lims_order_type.py new file mode 100644 index 0000000..d910be7 --- /dev/null +++ b/lims/models/lims_order_type.py @@ -0,0 +1,16 @@ +# Copyright (C) 2025 Open Source Integrators +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +from odoo import fields, models + + +class LIMSOrderType(models.Model): + _name = "lims.order.type" + _description = "LIMS Order Type" + + name = fields.Char(required=True) + + internal_type = fields.Selection( + selection=[("lims", "LIMS")], + default="lims", + ) diff --git a/lims/models/lims_sample.py b/lims/models/lims_sample.py new file mode 100644 index 0000000..7049bfe --- /dev/null +++ b/lims/models/lims_sample.py @@ -0,0 +1,49 @@ +# Copyright (C) 2025 Open Source Integrators +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +from odoo import _, fields, models +from odoo.exceptions import ValidationError + + +class LIMSSample(models.Model): + _name = "lims.sample" + _inherit = ["mail.thread", "mail.activity.mixin", "lims.model.mixin"] + _description = "LIMS Sample" + _stage_type = "sample" + + def _default_stage_id(self): + stage = self.env["lims.stage"].search( + [ + ("stage_type", "=", "sample"), + ("is_default", "=", True), + ("company_id", "in", (self.env.company.id, False)), + ], + order="sequence asc", + limit=1, + ) + if stage: + return stage + raise ValidationError(_("You must create a LIMS sample stage first.")) + + name = fields.Char( + required=True, + index=True, + copy=False, + default=lambda self: _("New"), + ) + stage_id = fields.Many2one( + "lims.stage", + string="Stage", + tracking=True, + index=True, + copy=False, + group_expand="_read_group_stage_ids", + default=lambda self: self._default_stage_id(), + ) + owner_id = fields.Many2one( + "res.partner", + string="Owner", + tracking=True, + index=True, + copy=False, + ) diff --git a/lims/models/lims_stage.py b/lims/models/lims_stage.py new file mode 100644 index 0000000..51f4bc6 --- /dev/null +++ b/lims/models/lims_stage.py @@ -0,0 +1,108 @@ +# Copyright (C) 2025 Open Source Integrators +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +from odoo import _, api, fields, models +from odoo.exceptions import ValidationError + +AVAILABLE_PRIORITIES = [("0", "Normal"), ("1", "Low"), ("2", "High"), ("3", "Urgent")] + + +class FSMStage(models.Model): + _name = "lims.stage" + _description = "LIMS Stage" + _order = "sequence, name, id" + + def _default_team_ids(self): + default_team_id = self.env.context.get("default_team_id") + return [default_team_id] if default_team_id else None + + active = fields.Boolean(default=True) + name = fields.Char(required=True) + sequence = fields.Integer(default=1, help="Used to order stages. Lower is better.") + legend_priority = fields.Text( + "Priority Management Explanation", + translate=True, + help="Explanation text to help users using" + " the star and priority mechanism on" + " stages or orders that are in this" + " stage.", + ) + fold = fields.Boolean( + "Folded in Kanban", + help="This stage is folded in the kanban view when " + "there are no record in that stage to display.", + ) + is_closed = fields.Boolean( + "Is a close stage", help="Services in this stage are considered as closed." + ) + is_default = fields.Boolean("Is a default stage", help="Used a default stage") + custom_color = fields.Char( + "Color Code", default="#FFFFFF", help="Use Hex Code only Ex:-#FFFFFF" + ) + description = fields.Text(translate=True) + stage_type = fields.Selection( + [ + ("order", "Order"), + ("batch", "Batch"), + ("sample", "Sample"), + ("equipment", "Equipment"), + ("operator", "Operator"), + ], + "Type", + required=True, + default="order", + ) + company_id = fields.Many2one( + "res.company", + string="Company", + default=lambda self: self.env.user.company_id.id, + ) + team_ids = fields.Many2many( + "lims.team", + "order_team_stage_rel", + "stage_id", + "team_id", + string="Teams", + default=lambda self: self._default_team_ids(), + ) + + def get_color_information(self): + # get stage ids + stage_ids = self.search([]) + color_information_dict = [] + for stage in stage_ids: + color_information_dict.append( + { + "color": stage.custom_color, + "field": "stage_id", + "opt": "==", + "value": stage.name, + } + ) + return color_information_dict + + @api.model_create_multi + def create(self, vals_list): + stages = self.search([]) + for vals in vals_list: + for stage in stages: + if stage.stage_type == vals.get( + "stage_type" + ) and stage.sequence == vals.get("sequence"): + raise ValidationError( + _( + "Cannot create LIMS Stage because " + "it has the same Type and Sequence " + "of an existing one." + ) + ) + return super().create(vals_list) + + @api.constrains("custom_color") + def _check_custom_color_hex_code(self): + if ( + self.custom_color + and not self.custom_color.startswith("#") + or len(self.custom_color) != 7 + ): + raise ValidationError(_("Color code should be Hex Code. Ex:-#FFFFFF")) diff --git a/lims/models/lims_tag.py b/lims/models/lims_tag.py new file mode 100644 index 0000000..a0d2c9d --- /dev/null +++ b/lims/models/lims_tag.py @@ -0,0 +1,32 @@ +# Copyright (C) 2025 Open Source Integrators +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +from odoo import fields, models + + +class LIMSTag(models.Model): + _name = "lims.tag" + _description = "LIMS Tag" + + name = fields.Char(required=True) + parent_id = fields.Many2one("lims.tag", string="Parent") + color = fields.Integer("Color Index", default=10) + full_name = fields.Char(compute="_compute_full_name") + company_id = fields.Many2one( + "res.company", + string="Company", + required=True, + index=True, + default=lambda self: self.env.user.company_id, + help="Company related to this tag", + ) + + _sql_constraints = [("name_uniq", "unique (name)", "Tag name already exists!")] + + def _compute_full_name(self): + for record in self: + record.full_name = ( + record.parent_id.name + "/" + record.name + if record.parent_id + else record.name + ) diff --git a/lims/models/lims_team.py b/lims/models/lims_team.py new file mode 100644 index 0000000..bc4e1d5 --- /dev/null +++ b/lims/models/lims_team.py @@ -0,0 +1,89 @@ +# Copyright (C) 2025 Open Source Integrators +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +from odoo import fields, models + + +class LIMSTeam(models.Model): + _name = "lims.team" + _description = "LIMS Team" + _inherit = ["mail.thread", "mail.activity.mixin"] + + def _default_stages(self): + return self.env["lims.stage"].search([("is_default", "=", True)]) + + def _compute_order_count(self): + order_data = self.env["lims.order"].read_group( + [("team_id", "in", self.ids), ("stage_id.is_closed", "=", False)], + ["team_id"], + ["team_id"], + ) + result = {data["team_id"][0]: int(data["team_id_count"]) for data in order_data} + for team in self: + team.order_count = result.get(team.id, 0) + + def _compute_order_need_assign_count(self): + order_data = self.env["lims.order"].read_group( + [ + ("team_id", "in", self.ids), + ("operator_id", "=", False), + ("stage_id.is_closed", "=", False), + ], + ["team_id"], + ["team_id"], + ) + result = {data["team_id"][0]: int(data["team_id_count"]) for data in order_data} + for team in self: + team.order_need_assign_count = result.get(team.id, 0) + + def _compute_order_need_schedule_count(self): + order_data = self.env["lims.order"].read_group( + [ + ("team_id", "in", self.ids), + ("scheduled_date_start", "=", False), + ("stage_id.is_closed", "=", False), + ], + ["team_id"], + ["team_id"], + ) + result = {data["team_id"][0]: int(data["team_id_count"]) for data in order_data} + for team in self: + team.order_need_schedule_count = result.get(team.id, 0) + + name = fields.Char(required=True, translate=True) + description = fields.Text(translate=True) + color = fields.Integer("Color Index") + stage_ids = fields.Many2many( + "lims.stage", + "lims_order_team_stage_rel", + "team_id", + "stage_id", + string="Stages", + default=_default_stages, + ) + order_ids = fields.One2many( + "lims.order", + "team_id", + string="Analysis", + domain=[("stage_id.is_closed", "=", False)], + ) + order_count = fields.Integer( + compute="_compute_order_count", string="Analysis Count" + ) + order_need_assign_count = fields.Integer( + compute="_compute_order_need_assign_count", string="Analysis to Assign" + ) + order_need_schedule_count = fields.Integer( + compute="_compute_order_need_schedule_count", string="Analysis to Schedule" + ) + sequence = fields.Integer(default=1, help="Used to sort teams. Lower is better.") + company_id = fields.Many2one( + "res.company", + string="Company", + required=True, + index=True, + default=lambda self: self.env.company, + help="Company related to this team", + ) + + _sql_constraints = [("name_uniq", "unique (name)", "Team name already exists!")] diff --git a/lims/models/lims_template.py b/lims/models/lims_template.py new file mode 100644 index 0000000..3e14872 --- /dev/null +++ b/lims/models/lims_template.py @@ -0,0 +1,26 @@ +# Copyright (C) 2025 Open Source Integrators +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +from odoo import fields, models + + +class LIMSTemplate(models.Model): + _name = "lims.template" + _description = "LIMS Order Template" + + name = fields.Char(required=True) + instructions = fields.Text() + category_ids = fields.Many2many("lims.category", string="Categories") + duration = fields.Float(help="Default duration in hours") + company_id = fields.Many2one( + "res.company", + string="Company", + index=True, + help="Company related to this template", + ) + type_id = fields.Many2one("lims.order.type", string="Type") + team_id = fields.Many2one( + "lims.team", + string="Team", + help="Choose a team to be set on orders of this template", + ) diff --git a/lims/models/res_company.py b/lims/models/res_company.py new file mode 100644 index 0000000..9889493 --- /dev/null +++ b/lims/models/res_company.py @@ -0,0 +1,24 @@ +# Copyright (C) 2025 Open Source Integrators +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +from odoo import fields, models + + +class ResCompany(models.Model): + _inherit = "res.company" + + lims_order_request_late_lowest = fields.Float( + string="Hours of Buffer for Lowest Priority LIMS Orders", + default=72, + ) + lims_order_request_late_low = fields.Float( + string="Hours of Buffer for Low Priority LIMS Orders", + default=48, + ) + lims_order_request_late_medium = fields.Float( + string="Hours of Buffer for Medium Priority LIMS Orders", + default=24, + ) + lims_order_request_late_high = fields.Float( + string="Hours of Buffer for High Priority LIMS Orders", default=8 + ) diff --git a/lims/models/res_config_settings.py b/lims/models/res_config_settings.py new file mode 100644 index 0000000..d9b7711 --- /dev/null +++ b/lims/models/res_config_settings.py @@ -0,0 +1,64 @@ +# Copyright (C) 2025 Open Source Integrators +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +from odoo import fields, models + + +class ResConfigSettings(models.TransientModel): + _inherit = "res.config.settings" + + # Groups + group_lims_laboratory = fields.Boolean( + string="Manage Laboratories", implied_group="lims.group_lims_laboratory" + ) + group_lims_team = fields.Boolean( + string="Manage Teams", implied_group="lims.group_lims_team" + ) + group_lims_category = fields.Boolean( + string="Manage Categories", implied_group="lims.group_lims_category" + ) + group_lims_tag = fields.Boolean( + string="Manage Tags", implied_group="lims.group_lims_tag" + ) + group_lims_equipment = fields.Boolean( + string="Manage Equipments", implied_group="lims.group_lims_equipment" + ) + group_lims_template = fields.Boolean( + string="Manage Templates", implied_group="lims.group_lims_template" + ) + group_lims_batch = fields.Boolean( + string="Manage Batches", implied_group="lims.group_lims_batch" + ) + + # Modules + module_lims_account = fields.Boolean(string="Invoice your analysis") + module_lims_maintenance = fields.Boolean( + string="Manage maintenance of your equipments" + ) + module_lims_purchase = fields.Boolean( + string="Manage subcontractors and their pricelists" + ) + module_lims_sale = fields.Boolean(string="Sell LIMS services") + module_lims_stock = fields.Boolean(string="Use Odoo Logistics") + + # Priorities + lims_order_request_late_lowest = fields.Float( + string="Hours of Buffer for Lowest Priority LIMS Orders", + related="company_id.lims_order_request_late_lowest", + readonly=False, + ) + lims_order_request_late_low = fields.Float( + string="Hours of Buffer for Low Priority LIMS Orders", + related="company_id.lims_order_request_late_low", + readonly=False, + ) + lims_order_request_late_medium = fields.Float( + string="Hours of Buffer for Medium Priority LIMS Orders", + related="company_id.lims_order_request_late_medium", + readonly=False, + ) + lims_order_request_late_high = fields.Float( + string="Hours of Buffer for High Priority LIMS Orders", + related="company_id.lims_order_request_late_high", + readonly=False, + ) diff --git a/lims/models/res_partner.py b/lims/models/res_partner.py new file mode 100644 index 0000000..7171e74 --- /dev/null +++ b/lims/models/res_partner.py @@ -0,0 +1,11 @@ +# Copyright (C) 2025 Open Source Integrators +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +from odoo import fields, models + + +class ResPartner(models.Model): + _inherit = "res.partner" + + lims_laboratory = fields.Boolean("Is a laboratory") + lims_operator = fields.Boolean("Is an operator") diff --git a/lims/pyproject.toml b/lims/pyproject.toml new file mode 100644 index 0000000..4231d0c --- /dev/null +++ b/lims/pyproject.toml @@ -0,0 +1,3 @@ +[build-system] +requires = ["whool"] +build-backend = "whool.buildapi" diff --git a/lims/readme/CONFIGURE.md b/lims/readme/CONFIGURE.md new file mode 100644 index 0000000..88a4e41 --- /dev/null +++ b/lims/readme/CONFIGURE.md @@ -0,0 +1,72 @@ +The base LIMS module can be used with minimal initial configuration. +It also allows for many advanced features, which require +a more in-depth configuration. + +## Analysis Stages + +The stage of an analysis is used to monitor its progress. Stages can be +configured based on your company's specific business needs. A basic set +of analysis stages comes pre-configured for use. + +1. Go to *LIMS \> Configuration \> Stages* +2. Create or edit a stage +3. Set the name for the stage. +4. Set the sequence order for the stage. +5. Select *Order* type to apply this stage to your analysis. +6. Additionally, you can set a color for the stage. + +## Laboratories + +You can manage different laboratories. + +## Advanced Configurations + +Additional features can be enabled in the General Settings panel for LIMS. + +1. Go to *LIMS \> Configuration \> Settings* +2. Enable additional options +3. Configure new options + +### Manage Teams + +Teams can be used to organize the processing of analysis +into groups. Different teams may have different workflows that an analysis +needs to follow. + +1. Go to *LIMS \> Configuration \> Operators \> Teams* +2. Create or select a team +3. Set the team name, description, and sequence + +You can now define custom stages for each team processing analysis. + +1. Go to *LIMS \> Configuration \> Stages* +2. Create or edit a stage +3. Select the teams for which this stage should be used + +### Manage Categories + +Categories are used to group operators and the type of analysis an operator can +do. + +1. Go to *LIMS \> Configuration \> Operators \> Categories* +2. Create or select a category +3. Set the name and description of category +4. Additionally, you can select a parent category if required + +### Manage Tags + +Tags can be used to filter and report on analysis + +1. Go to *LIMS \> Configuration \> Analysis \> Tags* +2. Create or select a tag +3. Set the tag name +4. Set a color index for the tag + +### Manage Templates + +Templates allow you to create standard templates for your analysis. + +1. Go to *LIMS \> Master Data \> Templates* +2. Create or select a template +3. Set the name +4. Set the standard order instructions diff --git a/lims/readme/CONTRIBUTORS.md b/lims/readme/CONTRIBUTORS.md new file mode 100644 index 0000000..b634127 --- /dev/null +++ b/lims/readme/CONTRIBUTORS.md @@ -0,0 +1,3 @@ +- Rodrigo Madrid Carmona \<\> +- Johannan Jasiel Luna García\<\> +- Maxime Chambreuil \<\> diff --git a/lims/readme/CREDITS.md b/lims/readme/CREDITS.md new file mode 100644 index 0000000..9646c07 --- /dev/null +++ b/lims/readme/CREDITS.md @@ -0,0 +1,3 @@ +The development of this module has been financially supported by: + +- Ganaderos Asociados de Querétaro \<\> diff --git a/lims/readme/DESCRIPTION.md b/lims/readme/DESCRIPTION.md new file mode 100644 index 0000000..9dbd771 --- /dev/null +++ b/lims/readme/DESCRIPTION.md @@ -0,0 +1 @@ +This module is the base of the LIMS application in Odoo. diff --git a/lims/readme/USAGE.md b/lims/readme/USAGE.md new file mode 100644 index 0000000..49c1df2 --- /dev/null +++ b/lims/readme/USAGE.md @@ -0,0 +1,19 @@ +To use this module, you need to: + +## Add LIMS Operators + +Operators are the people responsible for performing an analysis. +These operators may be subcontractors or the company's own employees. + +1. Go to *LIMS \> Master Data \> Operators* +2. Create an operator + +## Process Analysis + +Once you have established your data, you can begin processing analysis. + +1. Go to *LIMS \> Dashboard \> Analysis* +2. Create or select an analysis +3. Enter relevant details for the analysis +4. Process the analysis through each stage as defined by your business + requirements diff --git a/lims/security/ir.model.access.csv b/lims/security/ir.model.access.csv new file mode 100644 index 0000000..beae1e8 --- /dev/null +++ b/lims/security/ir.model.access.csv @@ -0,0 +1,26 @@ +id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink +access_lims_stage_lims_user,lims.stage.user,model_lims_stage,lims.group_lims_user_own,1,0,0,0 +access_lims_stage_lims_manager,lims.stage.manager,model_lims_stage,lims.group_lims_manager,1,1,1,1 +access_lims_tag_lims_user,lims.tag.user,model_lims_tag,lims.group_lims_user_own,1,0,0,0 +access_lims_tag_lims_manager,lims.tag.manager,model_lims_tag,lims.group_lims_manager,1,1,1,1 +access_lims_operator_user,lims.operator.user,model_lims_operator,lims.group_lims_user_own,1,1,0,0 +access_lims_operator_manager,lims.operator.manager,model_lims_operator,lims.group_lims_manager,1,1,1,1 +access_lims_sample_user,lims.sample.user,model_lims_sample,lims.group_lims_user_own,1,1,0,0 +access_lims_sample_manager,lims.sample.manager,model_lims_sample,lims.group_lims_manager,1,1,1,1 +access_lims_order_user,lims.order.user,model_lims_order,lims.group_lims_user_own,1,1,1,0 +access_lims_order_line_user,lims.order.line.user,model_lims_order_line,lims.group_lims_user_own,1,1,1,0 +access_lims_batch_user,lims.batch.user,model_lims_batch,lims.group_lims_user_own,1,1,1,0 +access_lims_equipment_lims_user,lims.equipment.user,model_lims_equipment,lims.group_lims_user_own,1,0,0,0 +access_lims_equipment_lims_manager,lims.equipment.manager,model_lims_equipment,lims.group_lims_manager,1,1,1,1 +access_lims_category_user,lims.category.user,model_lims_category,lims.group_lims_user_own,1,0,0,0 +access_lims_category_manager,lims.category.manager,model_lims_category,lims.group_lims_manager,1,1,1,1 +access_lims_template_user,lims.template.user,model_lims_template,lims.group_lims_user_own,1,0,0,0 +access_lims_template_manager,lims.template.manager,model_lims_template,lims.group_lims_manager,1,1,1,1 +access_lims_team_user,lims.team.user,model_lims_team,lims.group_lims_user_own,1,0,0,0 +access_lims_team_manager,lims.team.manager,model_lims_team,lims.group_lims_manager,1,1,1,1 +access_lims_laboratory_user,lims.laboratory.user,model_lims_laboratory,lims.group_lims_user_own,1,0,0,0 +access_lims_laboratory_manager,lims.laboratory.manager,model_lims_laboratory,lims.group_lims_manager,1,1,1,1 +access_lims_order_type_user,lims.order.type.user,model_lims_order_type,lims.group_lims_user_own,1,0,0,0 +access_lims_order_type_manager,lims.order.type.manager,model_lims_order_type,lims.group_lims_manager,1,1,1,1 +access_lims_calendar_filter,lims.calendar.filter.user,model_lims_operator_calendar_filter,lims.group_lims_user_own,1,1,1,1 +access_lims_wizard,access_lims_wizard,model_lims_wizard,lims.group_lims_user_own,1,0,0,0 diff --git a/lims/security/ir_rule.xml b/lims/security/ir_rule.xml new file mode 100644 index 0000000..ffc6c16 --- /dev/null +++ b/lims/security/ir_rule.xml @@ -0,0 +1,61 @@ + + + + LIMS Orders Entry + + + [('company_id', 'in', company_ids + [False])] + + + + LIMS Orders Entry (only own) + + [('company_id', 'in', company_ids + [False])] + + + + + LIMS Orders Entry + + [(1, '=', 1)] + + + + + LIMS Templates Entry + + + [('company_id', 'in', company_ids + [False])] + + + LIMS Teams Entry + + + [('company_id', 'in', company_ids + [False])] + + + LIMS Tags Entry + + + [('company_id', 'in', company_ids + [False])] + + + LIMS Categories Entry + + + [('company_id', 'in', company_ids + [False])] + + + LIMS Equipments Entry + + + [('company_id', 'in', company_ids + [False])] + + + LIMS Stage Entry + + + [('company_id', 'in', company_ids + [False])] + + diff --git a/lims/security/res_groups.xml b/lims/security/res_groups.xml new file mode 100644 index 0000000..b2d6da5 --- /dev/null +++ b/lims/security/res_groups.xml @@ -0,0 +1,53 @@ + + + + User (only own documents) + + + + + + User + + + + + Manager + + + + + + + + Manage Laboratories + + + + Manage LIMS Teams + + + + Manage LIMS Operators Categories + + + + Manage LIMS Tags + + + + Manage LIMS Equipments + + + + Manage LIMS Templates + + + + Manage LIMS Batches + + + diff --git a/lims/static/description/icon.jpg b/lims/static/description/icon.jpg new file mode 100644 index 0000000000000000000000000000000000000000..f71a53897e8456688da1da84f818ce0fe4f07bb2 GIT binary patch literal 54988 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 literal 0 HcmV?d00001 diff --git a/lims/static/description/icon.png b/lims/static/description/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..3fd26c65954b2ada1f94804c9dca65cd0cbc4ed5 GIT binary patch literal 14809 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 Opl8y`QsolHZ~qrx1 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/lims/static/description/index.html b/lims/static/description/index.html new file mode 100644 index 0000000..fc3265f --- /dev/null +++ b/lims/static/description/index.html @@ -0,0 +1,558 @@ + + + + + +Laboratory Information Management System (LIMS) + + + +
+

Laboratory Information Management System (LIMS)

+ + +

Beta License: AGPL-3 OCA/connector-lims Translate me on Weblate Try me on Runboat

+

This module is the base of the LIMS application in Odoo.

+

Table of contents

+ +
+

Configuration

+

The base LIMS module can be used with minimal initial configuration. It +also allows for many advanced features, which require a more in-depth +configuration.

+
+

Analysis Stages

+

The stage of an analysis is used to monitor its progress. Stages can be +configured based on your company’s specific business needs. A basic set +of analysis stages comes pre-configured for use.

+
    +
  1. Go to LIMS > Configuration > Stages
  2. +
  3. Create or edit a stage
  4. +
  5. Set the name for the stage.
  6. +
  7. Set the sequence order for the stage.
  8. +
  9. Select Order type to apply this stage to your analysis.
  10. +
  11. Additionally, you can set a color for the stage.
  12. +
+
+
+

Laboratories

+

You can manage different laboratories.

+
+
+

Advanced Configurations

+

Additional features can be enabled in the General Settings panel for +LIMS.

+
    +
  1. Go to LIMS > Configuration > Settings
  2. +
  3. Enable additional options
  4. +
  5. Configure new options
  6. +
+
+

Manage Teams

+

Teams can be used to organize the processing of analysis into groups. +Different teams may have different workflows that an analysis needs to +follow.

+
    +
  1. Go to LIMS > Configuration > Operators > Teams
  2. +
  3. Create or select a team
  4. +
  5. Set the team name, description, and sequence
  6. +
+

You can now define custom stages for each team processing analysis.

+
    +
  1. Go to LIMS > Configuration > Stages
  2. +
  3. Create or edit a stage
  4. +
  5. Select the teams for which this stage should be used
  6. +
+
+
+

Manage Categories

+

Categories are used to group operators and the type of analysis an +operator can do.

+
    +
  1. Go to LIMS > Configuration > Operators > Categories
  2. +
  3. Create or select a category
  4. +
  5. Set the name and description of category
  6. +
  7. Additionally, you can select a parent category if required
  8. +
+
+
+

Manage Tags

+

Tags can be used to filter and report on analysis

+
    +
  1. Go to LIMS > Configuration > Analysis > Tags
  2. +
  3. Create or select a tag
  4. +
  5. Set the tag name
  6. +
  7. Set a color index for the tag
  8. +
+
+
+

Manage Templates

+

Templates allow you to create standard templates for your analysis.

+
    +
  1. Go to LIMS > Master Data > Templates
  2. +
  3. Create or select a template
  4. +
  5. Set the name
  6. +
  7. Set the standard order instructions
  8. +
+
+
+
+
+

Usage

+

To use this module, you need to:

+
+

Add LIMS Operators

+

Operators are the people responsible for performing an analysis. These +operators may be subcontractors or the company’s own employees.

+
    +
  1. Go to LIMS > Master Data > Operators
  2. +
  3. Create an operator
  4. +
+
+
+

Process Analysis

+

Once you have established your data, you can begin processing analysis.

+
    +
  1. Go to LIMS > Dashboard > Analysis
  2. +
  3. Create or select an analysis
  4. +
  5. Enter relevant details for the analysis
  6. +
  7. Process the analysis through each stage as defined by your business +requirements
  8. +
+
+
+
+

Bug Tracker

+

Bugs are tracked on GitHub Issues. +In case of trouble, please check there if your issue has already been reported. +If you spotted it first, help us to smash it by providing a detailed and welcomed +feedback.

+

Do not contact contributors directly about support or help with technical issues.

+
+
+

Credits

+
+

Authors

+
    +
  • Open Source Integrators
  • +
+
+
+

Contributors

+ +
+
+

Other credits

+

The development of this module has been financially supported by:

+ +
+
+

Maintainers

+

This module is maintained by the OCA.

+ +Odoo Community Association + +

OCA, or the Odoo Community Association, is a nonprofit organization whose +mission is to support the collaborative development of Odoo features and +promote its widespread use.

+

Current maintainers:

+

max3903 jasiel-osi

+

This module is part of the OCA/connector-lims project on GitHub.

+

You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute.

+
+
+
+ + diff --git a/lims/static/src/scss/team_dashboard.scss b/lims/static/src/scss/team_dashboard.scss new file mode 100644 index 0000000..dd4c0ea --- /dev/null +++ b/lims/static/src/scss/team_dashboard.scss @@ -0,0 +1,5 @@ +.o_kanban_view.o_kanban_ungrouped.o_kanban_dashboard.o_lims_team_kanban { + .o_kanban_record { + width: 308px; + } +} diff --git a/lims/views/lims_batch.xml b/lims/views/lims_batch.xml new file mode 100644 index 0000000..7f10d86 --- /dev/null +++ b/lims/views/lims_batch.xml @@ -0,0 +1,257 @@ + + + + lims.batch.form + lims.batch + +
+
+ +
+ +
+
+

+ +

+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + lims.batch.list + lims.batch + + + + + + + + + + + + Orders + lims.batch + list,form,calendar + +

+ Create an Order. +

+
+
+ + + + lims.batch.search + lims.batch + + + + + + + + + + + + + + + + + + + + + + + + + + + Batches + lims.batch + calendar + + + + + + + + + + + + Batches + lims.batch + kanban,list,form,calendar + + +

+ Create a batch. +

+
+
+ + + + + lims.batch.graph + lims.batch + + + + + + + + + lims.batch.pivot + lims.batch + + + + + + + diff --git a/lims/views/lims_category.xml b/lims/views/lims_category.xml new file mode 100644 index 0000000..a53432d --- /dev/null +++ b/lims/views/lims_category.xml @@ -0,0 +1,53 @@ + + + + lims.category.list + lims.category + + + + + + + + + + lims.category.form + lims.category + +
+ + +
+
+
+ + + LIMS Category + lims.category + list,form + +

+ Add a LIMS Operator Category here. +

+
+
+
diff --git a/lims/views/lims_equipment.xml b/lims/views/lims_equipment.xml new file mode 100644 index 0000000..d8729f1 --- /dev/null +++ b/lims/views/lims_equipment.xml @@ -0,0 +1,199 @@ + + + + lims.equipment.search + lims.equipment + + + + + + + + + + + + + lims.equipment.list + lims.equipment + + + + + + + + + + + lims.equipment.form + lims.equipment + +
+
+
+ + + + +
+
+ + + + LIMS Equipments + lims.equipment + list,form + + {'default_laboratory_id': + context.get('laboratory_id', False)} + + +

+ Add an equipment here. +

+
+
+ + + lims.equipment.kanban + lims.equipment + + + + + + + + +
+
+
+ + + +
+
+ +
+
+ +
+
+
+
+ + +
+
+ + +
+
+
+ + + + + + LIMS Equipment + lims.equipment + list,kanban,form + +

+ Add a LIMS Equipment here. +

+
+
+ + + lims.equipment.graph + lims.equipment + + + + + + + + lims.equipment.pivot + lims.equipment + + + + + + + + Equipments + lims.equipment + graph,pivot + +

+ Equipments Report +

+
+
+ diff --git a/lims/views/lims_laboratory.xml b/lims/views/lims_laboratory.xml new file mode 100644 index 0000000..eedbc97 --- /dev/null +++ b/lims/views/lims_laboratory.xml @@ -0,0 +1,147 @@ + + + + lims.laboratory.list + lims.laboratory + + + + + + + + + + lims.laboratory.form + lims.laboratory + +
+ +
+