From acf73169d52b050d8cb85d675d616b848a5167c1 Mon Sep 17 00:00:00 2001 From: Enric Tobella Date: Fri, 25 Mar 2022 11:12:22 +0100 Subject: [PATCH 1/8] [ADD] lims Co-authored-by: Kevin Luna --- lims/README.rst | 124 ++++++++ lims/__init__.py | 1 + lims/__manifest__.py | 24 ++ lims/data/ir_sequence_data.xml | 19 ++ lims/demo/demo.xml | 12 + lims/models/__init__.py | 5 + lims/models/lims_analysis.py | 186 ++++++++++++ lims/models/lims_sample.py | 139 +++++++++ lims/models/lims_sample_type.py | 14 + lims/models/product_template.py | 18 ++ lims/models/res_users.py | 8 + lims/pyproject.toml | 3 + lims/readme/CONTEXT.md | 5 + lims/readme/CONTRIBUTORS.md | 2 + lims/readme/DESCRIPTION.md | 3 + lims/readme/ROADMAP.md | 9 + lims/readme/USAGE.md | 10 + lims/security/ir.model.access.csv | 9 + lims/security/security.xml | 54 ++++ lims/static/description/icon.png | Bin 0 -> 11109 bytes lims/static/description/icon.svg | 46 +++ lims/static/description/index.html | 473 +++++++++++++++++++++++++++++ lims/tests/__init__.py | 1 + lims/tests/test_lims.py | 150 +++++++++ lims/views/lims_analysis.xml | 89 ++++++ lims/views/lims_sample.xml | 99 ++++++ lims/views/lims_sample_type.xml | 73 +++++ lims/views/menu.xml | 20 ++ lims/views/product_template.xml | 62 ++++ 29 files changed, 1658 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_data.xml create mode 100644 lims/demo/demo.xml create mode 100644 lims/models/__init__.py create mode 100644 lims/models/lims_analysis.py create mode 100644 lims/models/lims_sample.py create mode 100644 lims/models/lims_sample_type.py create mode 100644 lims/models/product_template.py create mode 100644 lims/models/res_users.py create mode 100644 lims/pyproject.toml create mode 100644 lims/readme/CONTEXT.md create mode 100644 lims/readme/CONTRIBUTORS.md create mode 100644 lims/readme/DESCRIPTION.md create mode 100644 lims/readme/ROADMAP.md create mode 100644 lims/readme/USAGE.md create mode 100644 lims/security/ir.model.access.csv create mode 100644 lims/security/security.xml 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/tests/__init__.py create mode 100644 lims/tests/test_lims.py create mode 100644 lims/views/lims_analysis.xml create mode 100644 lims/views/lims_sample.xml create mode 100644 lims/views/lims_sample_type.xml create mode 100644 lims/views/menu.xml create mode 100644 lims/views/product_template.xml diff --git a/lims/README.rst b/lims/README.rst new file mode 100644 index 0000000..42f2bd4 --- /dev/null +++ b/lims/README.rst @@ -0,0 +1,124 @@ +.. image:: https://odoo-community.org/readme-banner-image + :target: https://odoo-community.org/get-involved?utm_source=readme + :alt: Odoo Community Association + +==== +Lims +==== + +.. + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! This file is generated by oca-gen-addon-readme !! + !! changes will be overwritten. !! + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! source digest: sha256:05b7fd50b44f8d195624bc3de3642e125de904aa3b3dd42d9f5cebb7f4917ead + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + +.. |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/license-LGPL--3-blue.png + :target: http://www.gnu.org/licenses/lgpl-3.0-standalone.html + :alt: License: LGPL-3 +.. |badge3| image:: https://img.shields.io/badge/github-OCA%2Fconnector--lims-lightgray.png?logo=github + :target: https://github.com/OCA/connector-lims/tree/19.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-19-0/connector-lims-19-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=19.0 + :alt: Try me on Runboat + +|badge1| |badge2| |badge3| |badge4| |badge5| + +This module allows to manage a Laboratory from an odoo instance. + +It implements a simple LIMS for your company. + +**Table of contents** + +.. contents:: + :local: + +Use Cases / Context +=================== + +This module gives a base for a vertical for laboratory companies. + +By default, it adds Samples and Analysis and some configurations related +to it. + +In order to keep odoo modular, we will split some functionality to make +it more feasible later on. + +Usage +===== + +1. A sampler user will create a sample from LIMS > Sample > Samples + + 1. Add a sample date, customer and sample type + 2. Add the different analysis you will do + 3. Receive the sample + +2. An analyst user will review al the samples to analyze and will set + the value. Once it has been set, he will submit the results + +3. A verifier user will verify all the results. The verifier must be + different than the analyst + +Known issues / Roadmap +====================== + +The following characteristics are still Work In Progress: + +- Usage of worklist +- Integration with devices +- Integration with sales / accounting +- Calculations +- Quality controls +- Batching of samples +- Sample storage + +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 +------- + +* Dixmit +* Creu Blanca + +Contributors +------------ + +- Dixmit + + - Enric Tobella + +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. + +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..0650744 --- /dev/null +++ b/lims/__init__.py @@ -0,0 +1 @@ +from . import models diff --git a/lims/__manifest__.py b/lims/__manifest__.py new file mode 100644 index 0000000..4982e94 --- /dev/null +++ b/lims/__manifest__.py @@ -0,0 +1,24 @@ +# Copyright 2023 Dixmit +# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl). + +{ + "name": "Lims", + "summary": """ + Laboratory Information Management System""", + "version": "19.0.1.0.0", + "license": "LGPL-3", + "author": "Dixmit, Creu Blanca,Odoo Community Association (OCA)", + "website": "https://github.com/OCA/connector-lims", + "depends": ["product"], + "data": [ + "security/security.xml", + "security/ir.model.access.csv", + "data/ir_sequence_data.xml", + "views/menu.xml", + "views/lims_sample_type.xml", + "views/lims_sample.xml", + "views/lims_analysis.xml", + "views/product_template.xml", + ], + "demo": ["demo/demo.xml"], +} diff --git a/lims/data/ir_sequence_data.xml b/lims/data/ir_sequence_data.xml new file mode 100644 index 0000000..6604bb4 --- /dev/null +++ b/lims/data/ir_sequence_data.xml @@ -0,0 +1,19 @@ + + + + + LIMS Sample sequence + lims.sample + SPL + 6 + + + LIMS Analysis sequence + lims.analysis + LA + 6 + + diff --git a/lims/demo/demo.xml b/lims/demo/demo.xml new file mode 100644 index 0000000..74f15f4 --- /dev/null +++ b/lims/demo/demo.xml @@ -0,0 +1,12 @@ + + + + + Blood + + + Hematology + + + diff --git a/lims/models/__init__.py b/lims/models/__init__.py new file mode 100644 index 0000000..bf6feb8 --- /dev/null +++ b/lims/models/__init__.py @@ -0,0 +1,5 @@ +from . import lims_sample +from . import lims_sample_type +from . import lims_analysis +from . import res_users +from . import product_template diff --git a/lims/models/lims_analysis.py b/lims/models/lims_analysis.py new file mode 100644 index 0000000..dcafb9a --- /dev/null +++ b/lims/models/lims_analysis.py @@ -0,0 +1,186 @@ +# Copyright 2023 Dixmit +# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl). + +from odoo import api, fields, models +from odoo.exceptions import AccessDenied + + +class LimsAnalysis(models.Model): + """ + Identifies the specific value provided by the laboratory. + It is linked to a sample and a product (which identifies the type of analysis). + """ + + _name = "lims.analysis" + _description = "Analysis" + _check_company_auto = True + + identifier = fields.Char(required=True, default="/", readonly=True) + sample_id = fields.Many2one("lims.sample", required=True) + company_id = fields.Many2one( + "res.company", related="sample_id.company_id", store=True + ) + state = fields.Selection( + [ + ("registered", "Registered"), + ("to_analyze", "To Analyze"), + ("to_be_verified", "To be verified"), + ("verified", "Verified"), + ("rejected", "Rejected"), + ], + required=True, + default="registered", + readonly=True, + ) + product_id = fields.Many2one( + "product.product", + required=True, + domain=[("service_tracking", "=", "laboratory")], + readonly=True, + ) + name = fields.Char( + required=True, + readonly=True, + compute="_compute_name", + precompute=True, + store=True, + ) + analyst_id = fields.Many2one("res.users", readonly=True) + capture_date = fields.Datetime(readonly=True) + submitted_date = fields.Datetime() + verified_by = fields.Many2one("res.users", readonly=True) + due_date = fields.Datetime(readonly=True) + verification_date = fields.Datetime(readonly=True) + uom_id = fields.Many2one( + "uom.uom", + readonly=True, + compute="_compute_uom_id", + store=True, + ) + progress = fields.Float(compute="_compute_progress", store=True) + can_verify = fields.Boolean(compute="_compute_can_verify") + value = fields.Char( + readonly=True, + ) + _identifier_unique = models.Constraint( + "unique(identifier, company_id)", "Analysis identifier must be unique" + ) + + @api.depends("product_id") + def _compute_name(self): + for record in self: + record.name = record.product_id.name + + @api.depends("product_id") + def _compute_uom_id(self): + for record in self: + record.uom_id = record.product_id.laboratory_uom_id + + @api.model_create_multi + def create(self, mvals): + for vals in mvals: + if vals.get("identifier", "/") == "/": + vals["identifier"] = self._get_identifier(vals) + return super().create(mvals) + + def _get_identifier(self, vals): + return ( + self.env["ir.sequence"] + .with_company(vals.get("company_id", self.env.company.id)) + .next_by_code("lims.analysis") + or "/" + ) + + @api.model + def _add_missing_default_values(self, values): + defaults = super()._add_missing_default_values(values) + product = self.env["product.product"].browse(defaults["product_id"]) + if "uom_id" not in values: + defaults["uom_id"] = product.laboratory_uom_id.id + if values.get("name"): + defaults["name"] = product.name + return defaults + + def _receive_sample(self): + for record in self: + record.write(record._receive_sample_vals()) + + def _receive_sample_vals(self): + return { + "state": "to_analyze", + } + + def analyze_action(self): + if not self.env.user.has_group("lims.group_lims_analyst"): + raise AccessDenied(self.env._("You are not allowed to analyze this")) + for record in self.filtered(lambda r: r.state == "to_analyze"): + record.write(record._analyze_action_vals()) + # We need to use sudo as an analyst shouldn't be able to modify the sample, + # but we want to trigger the check to verify which is based on analyses states + self.mapped("sample_id").sudo().check_to_verify() + + def _analyze_action_vals(self): + return { + "state": "to_be_verified", + "analyst_id": self.env.user.id, + "submitted_date": fields.Datetime.now(), + } + + def verify_action(self): + if not self.env.user.has_group("lims.group_lims_verifier"): + raise AccessDenied(self.env._("You are not allowed to verify an analysis")) + for record in self.filtered(lambda r: r.can_verify): + record.write(record._verify_action_vals()) + # We need to use sudo as a verifier shouldn't be able to modify the sample, + # but we want to trigger the check verify which is based on analyses states + self.mapped("sample_id").sudo().check_verify() + + def _verify_action_vals(self): + return { + "state": "verified", + "verified_by": self.env.user.id, + "verification_date": fields.Datetime.now(), + } + + @api.depends("state") + def _compute_progress(self): + for record in self: + record.progress = record._get_progress() + + def final_states(self): + return ["verified"] + + def _get_progress(self): + if self.state in self.final_states(): + return 100 + if self.state == "to_be_verified": + return 50 + return 0 + + @api.depends_context("uid") + @api.depends("state", "analyst_id") + def _compute_can_verify(self): + verify_param = int( + self.env["ir.config_parameter"] + .sudo() + .get_param("lims.unforce_double_verification_manager"), + ) and self.env.user.has_group("lims.group_lims_manager") + for record in self: + record.can_verify = record.state == "to_be_verified" and ( + verify_param or record.analyst_id != self.env.user + ) + + def retract_action(self): + if not self.env.user.has_group("lims.group_lims_verifier"): + raise AccessDenied( + self.env._("You are not allowed to retract an analysis to be verified") + ) + for record in self.filtered(lambda r: r.state == "to_be_verified"): + record.write(record._retract_action_vals()) + + def _retract_action_vals(self): + return { + "state": "to_analyze", + "analyst_id": False, + "submitted_date": False, + } diff --git a/lims/models/lims_sample.py b/lims/models/lims_sample.py new file mode 100644 index 0000000..5d58d76 --- /dev/null +++ b/lims/models/lims_sample.py @@ -0,0 +1,139 @@ +# Copyright 2023 Dixmit +# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl). + +from odoo import api, fields, models + + +class LimsSample(models.Model): + _name = "lims.sample" + _inherit = ["mail.thread", "mail.activity.mixin"] + _description = "Sample" + _rec_name = "identifier" + _check_company_auto = True + + identifier = fields.Char(required=True, default="/", readonly=True, copy=False) + external_identifier = fields.Char() + state = fields.Selection( + [ + ("registered", "Registered"), + ("scheduled_sampling", "Scheduled Sampling"), + ("due", "Sample due"), + ("received", "Received"), + ("to_be_verified", "To be verified"), + ("verified", "Verified"), + ("published", "Published"), + ("cancelled", "Cancelled"), + ("invalid", "Invalid"), + ], + required=True, + default="due", + readonly=True, + tracking=True, + ) + sample_type_id = fields.Many2one( + "lims.sample.type", + required=True, + tracking=True, + ) + sample_date = fields.Datetime( + default=fields.Datetime.now(), + required=True, + readonly=True, + tracking=True, + ) + received_date = fields.Datetime(readonly=True) + published_date = fields.Datetime(readonly=True) + customer_id = fields.Many2one("res.partner", tracking=True) + company_id = fields.Many2one( + "res.company", default=lambda self: self.env.company.id, tracking=True + ) + priority = fields.Selection( + [ + ("1", "Highest"), + ("2", "High"), + ("3", "Normal"), + ("4", "Low"), + ("5", "Lowest"), + ], + required=True, + default="3", + ) + analysis_ids = fields.One2many("lims.analysis", inverse_name="sample_id") + interpretation = fields.Html() + progress = fields.Float( + compute="_compute_progress", + store=True, + ) + _identifier_unique = models.Constraint( + "unique(identifier, company_id)", "Sample identifier must be unique" + ) + + @api.model_create_multi + def create(self, mvals): + for vals in mvals: + if vals.get("identifier", "/") == "/": + vals["identifier"] = self._get_identifier(vals) + return super().create(mvals) + + def _get_identifier(self, vals): + return ( + self.env["ir.sequence"] + .with_company(vals.get("company_id", self.env.company.id)) + .next_by_code("lims.sample") + or "/" + ) + + def receive_sample_action(self): + for record in self.filtered(lambda r: r.state == "due"): + record.write(record._receive_sample_vals()) + record.analysis_ids._receive_sample() + + def _receive_sample_vals(self): + return { + "state": "received", + "received_date": fields.Datetime.now(), + } + + def check_to_verify(self): + for record in self: + if record._check_to_verify(): + record.write(record._check_to_verify_vals()) + + def _check_to_verify(self): + return not any( + self.analysis_ids.filtered( + lambda r: r.state in ["registered", "to_analyze"] + ) + ) + + def _check_to_verify_vals(self): + return {"state": "to_be_verified"} + + def check_verify(self): + for record in self: + if record._check_verify(): + record.write(record._check_verify_vals()) + + def _check_verify(self): + return not any( + self.analysis_ids.filtered( + lambda r: r.state in ["registered", "to_analyze", "to_be_verified"] + ) + ) + + def _check_verify_vals(self): + return {"state": "verified"} + + @api.depends("analysis_ids", "analysis_ids.progress") + def _compute_progress(self): + for record in self: + record.progress = record._get_progress() + + def get_analysis(self): + return self.analysis_ids.filtered(lambda r: r.state not in ["invalid"]) + + def _get_progress(self): + analysis = self.get_analysis() + if not analysis: + return 0 + return sum(analysis.mapped("progress")) / len(analysis) diff --git a/lims/models/lims_sample_type.py b/lims/models/lims_sample_type.py new file mode 100644 index 0000000..0352f3c --- /dev/null +++ b/lims/models/lims_sample_type.py @@ -0,0 +1,14 @@ +# Copyright 2023 Dixmit +# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl). + +from odoo import fields, models + + +class LimsSampleType(models.Model): + _name = "lims.sample.type" + _description = "Sample Type" + + name = fields.Char(required=True) + description = fields.Text() + hazardous = fields.Boolean() + active = fields.Boolean(default=True) diff --git a/lims/models/product_template.py b/lims/models/product_template.py new file mode 100644 index 0000000..c02fd7c --- /dev/null +++ b/lims/models/product_template.py @@ -0,0 +1,18 @@ +# Copyright 2023 Dixmit +# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl). + +from odoo import fields, models + + +class ProductTemplate(models.Model): + _inherit = "product.template" + + laboratory_uom_id = fields.Many2one("uom.uom") + service_tracking = fields.Selection( + selection_add=[ + ("laboratory", "Laboratory"), + ], + ondelete={ + "laboratory": "set default", + }, + ) diff --git a/lims/models/res_users.py b/lims/models/res_users.py new file mode 100644 index 0000000..183a14f --- /dev/null +++ b/lims/models/res_users.py @@ -0,0 +1,8 @@ +# Copyright 2023 Dixmit +# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl). + +from odoo import models + + +class ResUsers(models.Model): + _inherit = "res.users" 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/CONTEXT.md b/lims/readme/CONTEXT.md new file mode 100644 index 0000000..c886f7f --- /dev/null +++ b/lims/readme/CONTEXT.md @@ -0,0 +1,5 @@ +This module gives a base for a vertical for laboratory companies. + +By default, it adds Samples and Analysis and some configurations related to it. + +In order to keep odoo modular, we will split some functionality to make it more feasible later on. diff --git a/lims/readme/CONTRIBUTORS.md b/lims/readme/CONTRIBUTORS.md new file mode 100644 index 0000000..ac3e05e --- /dev/null +++ b/lims/readme/CONTRIBUTORS.md @@ -0,0 +1,2 @@ +- Dixmit + - Enric Tobella diff --git a/lims/readme/DESCRIPTION.md b/lims/readme/DESCRIPTION.md new file mode 100644 index 0000000..73a3252 --- /dev/null +++ b/lims/readme/DESCRIPTION.md @@ -0,0 +1,3 @@ +This module allows to manage a Laboratory from an odoo instance. + +It implements a simple LIMS for your company. diff --git a/lims/readme/ROADMAP.md b/lims/readme/ROADMAP.md new file mode 100644 index 0000000..1550cd1 --- /dev/null +++ b/lims/readme/ROADMAP.md @@ -0,0 +1,9 @@ +The following characteristics are still Work In Progress: + +- Usage of worklist +- Integration with devices +- Integration with sales / accounting +- Calculations +- Quality controls +- Batching of samples +- Sample storage diff --git a/lims/readme/USAGE.md b/lims/readme/USAGE.md new file mode 100644 index 0000000..53dccf8 --- /dev/null +++ b/lims/readme/USAGE.md @@ -0,0 +1,10 @@ +1. A sampler user will create a sample from LIMS \> Sample \> Samples + 1. Add a sample date, customer and sample type + 2. Add the different analysis you will do + 3. Receive the sample + +2. An analyst user will review al the samples to analyze and will set + the value. Once it has been set, he will submit the results + +3. A verifier user will verify all the results. The verifier must be + different than the analyst diff --git a/lims/security/ir.model.access.csv b/lims/security/ir.model.access.csv new file mode 100644 index 0000000..5aa8cfc --- /dev/null +++ b/lims/security/ir.model.access.csv @@ -0,0 +1,9 @@ +id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink +access_lims_sample_type,access_lims_analysis,model_lims_sample_type,lims.group_lims_base,1,0,0,0 +manage_lims_sample_type,access_lims_analysis,model_lims_sample_type,lims.group_lims_manager,1,1,1,1 +access_lims_sample,access_lims_analysis,model_lims_sample,lims.group_lims_base,1,0,0,0 +manage_lims_sample,access_lims_analysis,model_lims_sample,lims.group_lims_sampler,1,1,1,0 +access_lims_analysis,access_lims_analysis,model_lims_analysis,lims.group_lims_base,1,0,0,0 +manage_lims_analysis_analyst,access_lims_analysis,model_lims_analysis,lims.group_lims_analyst,1,1,1,1 +manage_lims_analysis_verifier,access_lims_analysis,model_lims_analysis,lims.group_lims_verifier,1,1,1,1 +manage_lims_analysis_publisher,access_lims_analysis,model_lims_analysis,lims.group_lims_publisher,1,1,1,1 diff --git a/lims/security/security.xml b/lims/security/security.xml new file mode 100644 index 0000000..5273242 --- /dev/null +++ b/lims/security/security.xml @@ -0,0 +1,54 @@ + + + + + LIMS + + + LIMS + + 7 + + + LIMS Base Root Menu + + + Sampler + + + + + Analyst + + + + + Verifier + + + + + Publisher + + + + + Manager + + + + + + diff --git a/lims/static/description/icon.png b/lims/static/description/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..d947bb6c144c76ddaf520b4565fa5e50b40c202a GIT binary patch literal 11109 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 + + + + + + + diff --git a/lims/static/description/index.html b/lims/static/description/index.html new file mode 100644 index 0000000..8c1565b --- /dev/null +++ b/lims/static/description/index.html @@ -0,0 +1,473 @@ + + + + + +README.rst + + + +
+ + + +Odoo Community Association + +
+

Lims

+ +

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

+

This module allows to manage a Laboratory from an odoo instance.

+

It implements a simple LIMS for your company.

+

Table of contents

+ +
+

Use Cases / Context

+

This module gives a base for a vertical for laboratory companies.

+

By default, it adds Samples and Analysis and some configurations related +to it.

+

In order to keep odoo modular, we will split some functionality to make +it more feasible later on.

+
+
+

Usage

+
    +
  1. A sampler user will create a sample from LIMS > Sample > Samples
      +
    1. Add a sample date, customer and sample type
    2. +
    3. Add the different analysis you will do
    4. +
    5. Receive the sample
    6. +
    +
  2. +
  3. An analyst user will review al the samples to analyze and will set +the value. Once it has been set, he will submit the results
  4. +
  5. A verifier user will verify all the results. The verifier must be +different than the analyst
  6. +
+
+
+

Known issues / Roadmap

+

The following characteristics are still Work In Progress:

+
    +
  • Usage of worklist
  • +
  • Integration with devices
  • +
  • Integration with sales / accounting
  • +
  • Calculations
  • +
  • Quality controls
  • +
  • Batching of samples
  • +
  • Sample storage
  • +
+
+
+

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

+
    +
  • Dixmit
  • +
  • Creu Blanca
  • +
+
+
+

Contributors

+
    +
  • Dixmit
      +
    • Enric Tobella
    • +
    +
  • +
+
+
+

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.

+

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/tests/__init__.py b/lims/tests/__init__.py new file mode 100644 index 0000000..ceb595f --- /dev/null +++ b/lims/tests/__init__.py @@ -0,0 +1 @@ +from . import test_lims diff --git a/lims/tests/test_lims.py b/lims/tests/test_lims.py new file mode 100644 index 0000000..41eefc7 --- /dev/null +++ b/lims/tests/test_lims.py @@ -0,0 +1,150 @@ +# Copyright 2026 Dixmit +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +from odoo.tests import new_test_user +from odoo.tests.common import TransactionCase + + +class TestLims(TransactionCase): + @classmethod + def setUpClass(cls): + super().setUpClass() + cls.analyte_01 = cls.env["product.product"].create( + { + "name": "Analyte 01", + "type": "service", + "service_tracking": "laboratory", + "laboratory_uom_id": cls.env.ref("uom.product_uom_unit").id, + } + ) + cls.analyte_02 = cls.env["product.product"].create( + { + "name": "Analyte 02", + "type": "service", + "service_tracking": "laboratory", + "laboratory_uom_id": cls.env.ref("uom.product_uom_millimeter").id, + } + ) + cls.sample_type = cls.env["lims.sample.type"].create( + { + "name": "Blood Sample", + } + ) + cls.analyst = new_test_user( + cls.env, + name="Because I am an analyst", + login="analyst", + password="analyst", + email="analyst@test.com", + groups="lims.group_lims_analyst", + company_id=cls.env.company.id, + ) + cls.verifier = new_test_user( + cls.env, + name="Because I am a verifier", + login="verifier", + password="verifier", + email="verifier@test.com", + groups="lims.group_lims_verifier", + company_id=cls.env.company.id, + ) + + def test_flow_01(self): + """ + Lims Sample with 2 analytes + - Create sample with 2 analyses + - Receive sample + - Analyze both analyses + - Verify both analyses + """ + sample = self.env["lims.sample"].create( + { + "external_identifier": "Sample 01", + "sample_type_id": self.sample_type.id, + } + ) + analysis_01 = self.env["lims.analysis"].create( + { + "sample_id": sample.id, + "product_id": self.analyte_01.id, + } + ) + analysis_02 = self.env["lims.analysis"].create( + { + "sample_id": sample.id, + "product_id": self.analyte_02.id, + } + ) + self.assertEqual(analysis_01.uom_id, self.analyte_01.laboratory_uom_id) + self.assertEqual(analysis_02.uom_id, self.analyte_02.laboratory_uom_id) + self.assertEqual(analysis_01.state, "registered") + self.assertEqual(analysis_02.state, "registered") + self.assertEqual(sample.state, "due") + sample.receive_sample_action() + self.assertEqual(sample.state, "received") + self.assertEqual(analysis_01.state, "to_analyze") + self.assertEqual(analysis_02.state, "to_analyze") + analysis_01.with_user(self.analyst.id).analyze_action() + self.assertEqual(analysis_01.state, "to_be_verified") + self.assertEqual(analysis_01.analyst_id, self.analyst) + self.assertEqual(analysis_02.state, "to_analyze") + self.assertEqual(sample.state, "received") + analysis_02.with_user(self.analyst.id).analyze_action() + self.assertEqual(analysis_02.state, "to_be_verified") + self.assertEqual(sample.state, "to_be_verified") + analysis_01.with_user(self.verifier.id).verify_action() + self.assertEqual(analysis_01.state, "verified") + self.assertEqual(sample.state, "to_be_verified") + analysis_02.with_user(self.verifier.id).verify_action() + self.assertEqual(analysis_02.state, "verified") + self.assertEqual(sample.state, "verified") + + def test_flow_02(self): + """ + Lims Sample with 2 analytes + - Create sample with 2 analyses + - Receive sample + - Analyze first analysis and verify it + - Analyze second analysis and verify it + """ + sample = self.env["lims.sample"].create( + { + "external_identifier": "Sample 01", + "sample_type_id": self.sample_type.id, + } + ) + analysis_01 = self.env["lims.analysis"].create( + { + "sample_id": sample.id, + "product_id": self.analyte_01.id, + } + ) + analysis_02 = self.env["lims.analysis"].create( + { + "sample_id": sample.id, + "product_id": self.analyte_02.id, + } + ) + self.assertEqual(analysis_01.uom_id, self.analyte_01.laboratory_uom_id) + self.assertEqual(analysis_02.uom_id, self.analyte_02.laboratory_uom_id) + self.assertEqual(analysis_01.state, "registered") + self.assertEqual(analysis_02.state, "registered") + self.assertEqual(sample.state, "due") + sample.receive_sample_action() + self.assertEqual(sample.state, "received") + self.assertEqual(analysis_01.state, "to_analyze") + self.assertEqual(analysis_02.state, "to_analyze") + analysis_01.with_user(self.analyst.id).analyze_action() + self.assertEqual(analysis_01.state, "to_be_verified") + self.assertEqual(analysis_01.analyst_id, self.analyst) + self.assertEqual(analysis_02.state, "to_analyze") + self.assertEqual(sample.state, "received") + analysis_01.with_user(self.verifier.id).verify_action() + self.assertEqual(analysis_01.state, "verified") + self.assertEqual(sample.state, "received") + analysis_02.with_user(self.analyst.id).analyze_action() + self.assertEqual(analysis_02.state, "to_be_verified") + self.assertEqual(sample.state, "to_be_verified") + analysis_02.with_user(self.verifier.id).verify_action() + self.assertEqual(analysis_02.state, "verified") + self.assertEqual(sample.state, "verified") diff --git a/lims/views/lims_analysis.xml b/lims/views/lims_analysis.xml new file mode 100644 index 0000000..f043bfa --- /dev/null +++ b/lims/views/lims_analysis.xml @@ -0,0 +1,89 @@ + + + + + lims.analysis.form (in lims) + lims.analysis + +
+
+ +
+ +
+

+

+ + + + + + + + + + + lims.analysis.search (in lims) + lims.analysis + + + + + + + + lims.analysis.list (in lims) + lims.analysis + + + + + + + + + + +