diff --git a/lims/README.rst b/lims/README.rst new file mode 100644 index 0000000..8f91aa1 --- /dev/null +++ b/lims/README.rst @@ -0,0 +1,109 @@ +==== +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/licence-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/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 allows to manage a Laboratory from an odoo instance. + +It implements a simple LIMS for your company. + +**Table of contents** + +.. contents:: + :local: + +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..ddd760b --- /dev/null +++ b/lims/__manifest__.py @@ -0,0 +1,25 @@ +# Copyright 2023 Dixmit +# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl). + +{ + "name": "Lims", + "summary": """ + Laboratory Information Management System""", + "version": "18.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_department.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..c2755a1 --- /dev/null +++ b/lims/models/__init__.py @@ -0,0 +1,6 @@ +from . import lims_sample +from . import lims_sample_type +from . import lims_department +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..4d7c827 --- /dev/null +++ b/lims/models/lims_analysis.py @@ -0,0 +1,176 @@ +# 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): + _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=[("laboratory_ok", "=", True)], + readonly=True, + ) + name = fields.Char( + required=True, + readonly=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, + ) + progress = fields.Float(compute="_compute_progress", store=True) + can_verify = fields.Boolean(compute="_compute_can_verify") + value = fields.Char( + readonly=True, + ) + # TODO: Replace this for something better. isn't it? + + _sql_constraints = [ + ( + "identifier_unique", + "unique(identifier, company_id)", + "Sample identifier must be unique", + ) + ] + + @api.onchange("product_id") + def _onchange_product(self): + for record in self: + if not record.product_id: + continue + record.name = record.product_id.name + 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_context(force_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(_("You are not allowed to analyze this")) + for record in self.filtered(lambda r: r.state == "to_analyze"): + record.write(record._analyze_action_vals()) + self.mapped("sample_id").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(_("You are not allowed to verify an analysis")) + for record in self.filtered(lambda r: r.can_verify): + record.write(record._verify_action_vals()) + self.mapped("sample_id").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( + _("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_department.py b/lims/models/lims_department.py new file mode 100644 index 0000000..be2918a --- /dev/null +++ b/lims/models/lims_department.py @@ -0,0 +1,13 @@ +# Copyright 2023 Dixmit +# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl). + +from odoo import fields, models + + +class LimsDepartment(models.Model): + _name = "lims.department" + _description = "Department" + + name = fields.Char(required=True) + active = fields.Boolean(default=True) + user_ids = fields.Many2many("res.users") diff --git a/lims/models/lims_sample.py b/lims/models/lims_sample.py new file mode 100644 index 0000000..7a308c6 --- /dev/null +++ b/lims/models/lims_sample.py @@ -0,0 +1,146 @@ +# 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, + ) + + _sql_constraints = [ + ( + "identifier_unique", + "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_context(force_company=vals.get("company_id", self.env.company.id)) + .next_by_code("lims.sample") + or "/" + ) + + def receive_sample_action(self): + for record in self: + record.filtered(lambda r: r.state == "due").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..cf3237f --- /dev/null +++ b/lims/models/product_template.py @@ -0,0 +1,12 @@ +# 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_ok = fields.Boolean(string="Can be used on Lab") + is_lab_template = fields.Boolean() + laboratory_uom_id = fields.Many2one("uom.uom") diff --git a/lims/models/res_users.py b/lims/models/res_users.py new file mode 100644 index 0000000..cab7b59 --- /dev/null +++ b/lims/models/res_users.py @@ -0,0 +1,10 @@ +# Copyright 2023 Dixmit +# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl). + +from odoo import fields, models + + +class ResUsers(models.Model): + _inherit = "res.users" + + lims_department_ids = fields.Many2many("lims.department") 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/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..210e66a --- /dev/null +++ b/lims/security/ir.model.access.csv @@ -0,0 +1,11 @@ +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_department,access_lims_analysis,model_lims_department,lims.group_lims_base,1,0,0,0 +manage_lims_department,access_lims_analysis,model_lims_department,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..470baae --- /dev/null +++ b/lims/security/security.xml @@ -0,0 +1,50 @@ + + + + + LIMS + + + 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 0000000..af9bd35 Binary files /dev/null and b/lims/static/description/icon.png differ diff --git a/lims/static/description/index.html b/lims/static/description/index.html new file mode 100644 index 0000000..b663d07 --- /dev/null +++ b/lims/static/description/index.html @@ -0,0 +1,458 @@ + + + + + +Lims + + + +
+

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

+ +
+

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/views/lims_analysis.xml b/lims/views/lims_analysis.xml new file mode 100644 index 0000000..9d73343 --- /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 + + + + + + + + + + +