diff --git a/quality_control_issue/README.rst b/quality_control_issue/README.rst new file mode 100644 index 000000000..f17a29a5a --- /dev/null +++ b/quality_control_issue/README.rst @@ -0,0 +1,69 @@ +.. image:: https://img.shields.io/badge/licence-AGPL--3-blue.svg + :target: http://www.gnu.org/licenses/agpl-3.0-standalone.html + :alt: License: AGPL-3 + +===================== +Quality Control Issue +===================== + +WIP + +This module extends the functionality of quality Control to allow you to +report and manage quality control issues. + +Usage +===== + +To use this module, you need to: + +#. Go to *Quality Control > Issues > QC Issues*. + +.. image:: https://odoo-community.org/website/image/ir.attachment/5784_f2813bd/datas + :alt: Try me on Runbot + :target: https://runbot.odoo-community.org/runbot/129/9.0 + +Known issues / Roadmap +====================== + +Todo: +----- + +* Add Dispositions: RMA, scrap, rework... +* reference to PO, MO, QC... +* Link to Problem tracking... + +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 smash it by providing detailed and welcomed feedback. + +Credits +======= + +Images +------ + +* Odoo Community Association: `Icon `_. + +Contributors +------------ + +* Lois Rilo + +Maintainer +---------- + +.. image:: https://odoo-community.org/logo.png + :alt: Odoo Community Association + :target: https://odoo-community.org + +This module is maintained by the OCA. + +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. + +To contribute to this module, please visit https://odoo-community.org. diff --git a/quality_control_issue/__init__.py b/quality_control_issue/__init__.py new file mode 100644 index 000000000..7201c0805 --- /dev/null +++ b/quality_control_issue/__init__.py @@ -0,0 +1,5 @@ +# -*- coding: utf-8 -*- +# Copyright 2017 Eficent Business and IT Consulting Services S.L. +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +from . import models diff --git a/quality_control_issue/__openerp__.py b/quality_control_issue/__openerp__.py new file mode 100644 index 000000000..823c85570 --- /dev/null +++ b/quality_control_issue/__openerp__.py @@ -0,0 +1,25 @@ +# -*- coding: utf-8 -*- +# Copyright 2017 Eficent Business and IT Consulting Services S.L. +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). +{ + "name": "Quality Control Issue", + "summary": "Allow to manage and report Quality Control Issues.", + "version": "9.0.1.0.0", + "category": "Quality Control", + "website": "https://odoo-community.org/", + "author": "Eficent , Odoo Community Association (OCA)", + "license": "AGPL-3", + "application": False, + "installable": True, + "depends": ["quality_control", "quality_control_team", "stock"], + "data": [ + "security/ir.model.access.csv", + "security/quality_control_issue_security.xml", + "data/qc_issue_sequence.xml", + "data/qc_stage_data.xml", + "views/qc_issue_view.xml", + "views/qc_problem_view.xml", + "views/qc_problem_group_view.xml", + "views/qc_team_dashboard_view.xml", + ], +} diff --git a/quality_control_issue/data/qc_issue_sequence.xml b/quality_control_issue/data/qc_issue_sequence.xml new file mode 100644 index 000000000..d779d8a87 --- /dev/null +++ b/quality_control_issue/data/qc_issue_sequence.xml @@ -0,0 +1,15 @@ + + + + + + + QC Issues + qc.issue + QCI/%(range_year)s/ + 5 + + + + diff --git a/quality_control_issue/data/qc_stage_data.xml b/quality_control_issue/data/qc_stage_data.xml new file mode 100644 index 000000000..25617235c --- /dev/null +++ b/quality_control_issue/data/qc_stage_data.xml @@ -0,0 +1,23 @@ + + + + + + + + New + 1 + False + + + + + + Done + 100 + True + + + + diff --git a/quality_control_issue/models/__init__.py b/quality_control_issue/models/__init__.py new file mode 100644 index 000000000..56785ea53 --- /dev/null +++ b/quality_control_issue/models/__init__.py @@ -0,0 +1,9 @@ +# -*- coding: utf-8 -*- +# Copyright 2017 Eficent Business and IT Consulting Services S.L. +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +from . import qc_issue +from . import qc_problem +from . import qc_problem_group +from . import qc_stage +from . import qc_issue_stage diff --git a/quality_control_issue/models/qc_issue.py b/quality_control_issue/models/qc_issue.py new file mode 100644 index 000000000..66df5eafe --- /dev/null +++ b/quality_control_issue/models/qc_issue.py @@ -0,0 +1,173 @@ +# -*- coding: utf-8 -*- +# Copyright 2017 Eficent Business and IT Consulting Services S.L. +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +from openerp import api, fields, models +import openerp.addons.decimal_precision as dp + + +class QualityControlIssue(models.Model): + _name = "qc.issue" + _description = "Quality Control Issue" + _inherit = "mail.thread" + + @api.model + def create(self, vals): + vals['name'] = self.env['ir.sequence'].next_by_code( + 'qc.issue') or '' + return super(QualityControlIssue, self).create(vals) + + @api.one + def _get_uom(self): + self.product_uom = self.product_id.product_tmpl_id.uom_id + + def _get_default_stage_id(self): + """ Gives default stage_id """ + team_id = self.env['qc.team']._get_default_qc_team_id( + user_id=self.env.uid) + return self.issue_stage_find([], team_id, [('fold', '=', False)]) + + @api.multi + def _read_group_stage_ids(self, domain, read_group_order=None, + access_rights_uid=None): + access_rights_uid = access_rights_uid or self._uid + stage_obj = self.env['qc.issue.stage'] + search_domain = [] + qc_team_id = self.env.context.get('default_qc_team_id') or False + if qc_team_id: + search_domain += ['|', ('id', 'in', self.ids)] + search_domain += ['|', ('qc_team_id', '=', qc_team_id)] + search_domain += [('qc_team_id', '=', False)] + else: + search_domain += ['|', ('id', 'in', self.ids)] + search_domain += [('qc_team_id', '=', False)] + # perform search + stage_ids = stage_obj._search(search_domain, + access_rights_uid=access_rights_uid) + result = [stage.name_get()[0] for stage in + stage_obj.browse(stage_ids)] + # restore order of the search + result.sort( + lambda x, y: cmp(stage_ids.index(x[0]), stage_ids.index(y[0]))) + + fold = {} + for stage in stage_obj.browse(stage_ids): + fold[stage.id] = stage.fold or False + return result, fold + + name = fields.Char(readonly=True) + state = fields.Selection( + selection=[("new", "New"), + ("progress", "In Progress"), + ("done", "Done"), + ("cancel", "Cancel")], default="new", + track_visibility='onchange') + product_id = fields.Many2one( + comodel_name="product.product", string="Product", + readonly=True, states={"new": [("readonly", False)]}, + required=True) + product_tracking = fields.Selection(related="product_id.tracking") + product_qty = fields.Float( + string="Product Quantity", required=True, default=1.0, + readonly=True, states={"new": [("readonly", False)]}, + digits_compute=dp.get_precision("Product Unit of Measure")) + product_uom = fields.Many2one( + comodel_name="product.uom", string="Product Unit of Measure", + required=True, default=_get_uom, + readonly=True, states={"new": [("readonly", False)]},) + lot_id = fields.Many2one( + comodel_name="stock.production.lot", string="Lot/Serial Number", + readonly=True, states={"new": [("readonly", False)]},) + location_id = fields.Many2one( + comodel_name="stock.location", string="Location", + readonly=True, states={"new": [("readonly", False)]},) + inspector_id = fields.Many2one( + comodel_name="res.users", string="Inspector", + track_visibility="onchange", + readonly=True, states={"new": [("readonly", False)]}, + default=lambda self: self.env.user, required=True) + responsible_id = fields.Many2one( + comodel_name="res.users", string="Assigned to", + track_visibility="onchange", + states={"done": [("readonly", True)]},) + description = fields.Text( + states={"done": [("readonly", True)]},) + qc_problem_ids = fields.Many2many( + comodel_name="qc.problem", string="Problems", + relation="qc_issue_problem_rel", column1="qc_issue_id", + column2="qc_problem_id", + states={"done": [("readonly", True)]},) + color = fields.Integer(string='Color Index') + stage_id = fields.Many2one( + comodel_name="qc.issue.stage", string='Stage', + track_visibility='onchange', + select=True, default=_get_default_stage_id, + domain="['|', ('qc_team_id', '=', False), " + "('qc_team_id', '=', qc_team_id)]") + qc_team_id = fields.Many2one( + comodel_name='qc.team', string='QC Team', + default=lambda self: self.env[ + 'qc.team'].sudo()._get_default_qc_team_id(user_id=self.env.uid), + index=True, track_visibility='onchange') + company_id = fields.Many2one( + comodel_name='res.company', string='Company', required=True, + default=lambda self: self.env.user.company_id) + + _group_by_full = { + 'stage_id': _read_group_stage_ids + } + + def issue_stage_find(self, cases, team_id, domain=None, order='sequence'): + """ Override of the base.stage method + Parameter of the stage search taken from the problem: + - team_id: if set, stages must belong to this team or + be a default stage; if not set, stages must be default + stages + """ + team_ids = set() + if team_id: + team_ids.add(team_id) + for problem in cases: + if problem.team_id: + team_ids.add(problem.team_id.id) + search_domain = [] + if team_ids: + search_domain += [('|')] * (len(team_ids) - 1) + for team_id in team_ids: + search_domain.append(('qc_team_id', '=', team_id.id)) + search_domain += list(domain) + # perform search, return the first found + stage_ids = self.env['qc.issue.stage'].search( + search_domain, order=order, limit=1) + if stage_ids: + return stage_ids[0] + return False + + @api.multi + def action_confirm(self): + self.write({'state': 'progress'}) + + @api.multi + def action_done(self): + self.write({'state': 'done'}) + + @api.multi + def action_cancel(self): + self.write({'state': 'cancel'}) + + @api.onchange('product_id') + def _onchange_product_id(self): + self.product_uom = self.product_id.product_tmpl_id.uom_id + if self.lot_id.product_id != self.product_id: + self.lot_id = False + if self.product_id: + return {'domain': { + 'lot_id': [('product_id', '=', self.product_id.id)]}} + return {'domain': {'lot_id': []}} + + @api.onchange("lot_id") + def _onchange_lot_id(self): + product = self.lot_id.product_id + if product: + self.product_id = product + self.product_uom = product.product_tmpl_id.uom_id diff --git a/quality_control_issue/models/qc_issue_stage.py b/quality_control_issue/models/qc_issue_stage.py new file mode 100644 index 000000000..3dfbfbfba --- /dev/null +++ b/quality_control_issue/models/qc_issue_stage.py @@ -0,0 +1,41 @@ +# -*- coding: utf-8 -*- +# Copyright 2017 Eficent Business and IT Consulting Services S.L. +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +from openerp import api, fields, models + +AVAILABLE_PRIORITIES = [ + ('0', 'Normal'), + ('1', 'Low'), + ('2', 'High'), + ('3', 'Very High'), +] + + +class QualityControlIssueStage(models.Model): + _name = "qc.issue.stage" + _rec_name = 'name' + _order = "sequence, name, id" + + @api.model + def default_get(self, fields): + """ Hack : when going from the kanban view, creating a stage with a + qc team in context should not create a stage for the current team only. + """ + ctx = dict(self.env.context) + if ctx.get('default_qc_team_id') and not ctx.get('some_context'): + ctx.pop('default_qc_team_id') + return super(QualityControlIssueStage, + self.with_context(ctx)).default_get(fields) + + name = fields.Char('Stage Name', required=True) + sequence = fields.Integer( + string='Sequence', help="Used to order stages. Lower is better.", + default=1) + qc_team_id = fields.Many2one( + comodel_name='qc.team', string='Quality Control Team', + ondelete='set null') + fold = fields.Boolean( + string='Folded in Pipeline', default=False, + help='This stage is folded in the kanban view when there are no ' + 'records in that stage to display.') diff --git a/quality_control_issue/models/qc_problem.py b/quality_control_issue/models/qc_problem.py new file mode 100644 index 000000000..429154ba3 --- /dev/null +++ b/quality_control_issue/models/qc_problem.py @@ -0,0 +1,113 @@ +# -*- coding: utf-8 -*- +# Copyright 2017 Eficent Business and IT Consulting Services S.L. +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +from openerp import api, fields, models + +AVAILABLE_PRIORITIES = [ + ('0', 'Normal'), + ('1', 'Low'), + ('2', 'High'), + ('3', 'Very High'), +] + + +class QcProblem(models.Model): + _name = "qc.problem" + _description = "Quality Control Problem Tracking" + _inherit = "mail.thread" + + def _get_default_stage_id(self): + """ Gives default stage_id """ + team_id = self.env['qc.team']._get_default_qc_team_id( + user_id=self.env.uid) + return self.stage_find([], team_id, [('fold', '=', False)]) + + @api.multi + def _read_group_stage_ids(self, domain, read_group_order=None, + access_rights_uid=None): + access_rights_uid = access_rights_uid or self._uid + stage_obj = self.env['qc.stage'] + search_domain = [] + qc_team_id = self.env.context.get('default_qc_team_id') or False + if qc_team_id: + search_domain += ['|', ('id', 'in', self.ids)] + search_domain += ['|', ('qc_team_id', '=', qc_team_id)] + search_domain += [('qc_team_id', '=', False)] + else: + search_domain += ['|', ('id', 'in', self.ids)] + search_domain += [('qc_team_id', '=', False)] + # perform search + stage_ids = stage_obj._search(search_domain, + access_rights_uid=access_rights_uid) + result = [stage.name_get()[0] for stage in + stage_obj.browse(stage_ids)] + # restore order of the search + result.sort( + lambda x, y: cmp(stage_ids.index(x[0]), stage_ids.index(y[0]))) + + fold = {} + for stage in stage_obj.browse(stage_ids): + fold[stage.id] = stage.fold or False + return result, fold + + @api.one + @api.depends('issue_ids') + def _compute_count(self): + self.issue_count = len(self.issue_ids) + + name = fields.Char() + notes = fields.Text() + issue_ids = fields.Many2many( + comodel_name="qc.issue", string="QC Issues", + relation="qc_issue_problem_rel", column1="qc_problem_id", + column2="qc_issue_id") + problem_group_id = fields.Many2one( + comodel_name="qc.problem.group", string="Problem Group") + issue_count = fields.Integer( + string="Issues", compute=_compute_count, store=True) + color = fields.Integer(string='Color Index') + priority = fields.Selection( + selection=AVAILABLE_PRIORITIES, string='Rating', index=True) + stage_id = fields.Many2one( + comodel_name="qc.stage", string='Stage', track_visibility='onchange', + select=True, default=_get_default_stage_id, + domain="['|', ('qc_team_id', '=', False), " + "('qc_team_id', '=', qc_team_id)]") + qc_team_id = fields.Many2one( + comodel_name='qc.team', string='QC Team', + default=lambda self: self.env[ + 'qc.team'].sudo()._get_default_qc_team_id(user_id=self.env.uid), + index=True, track_visibility='onchange') + company_id = fields.Many2one( + comodel_name='res.company', string='Company', required=True, + default=lambda self: self.env.user.company_id) + _group_by_full = { + 'stage_id': _read_group_stage_ids + } + + def stage_find(self, cases, team_id, domain=None, order='sequence'): + """ Override of the base.stage method + Parameter of the stage search taken from the problem: + - team_id: if set, stages must belong to this team or + be a default stage; if not set, stages must be default + stages + """ + team_ids = set() + if team_id: + team_ids.add(team_id) + for problem in cases: + if problem.team_id: + team_ids.add(problem.team_id.id) + search_domain = [] + if team_ids: + search_domain += [('|')] * (len(team_ids) - 1) + for team_id in team_ids: + search_domain.append(('qc_team_id', '=', team_id.id)) + search_domain += list(domain) + # perform search, return the first found + stage_ids = self.env['qc.stage'].search( + search_domain, order=order, limit=1) + if stage_ids: + return stage_ids[0] + return False diff --git a/quality_control_issue/models/qc_problem_group.py b/quality_control_issue/models/qc_problem_group.py new file mode 100644 index 000000000..c2468cb74 --- /dev/null +++ b/quality_control_issue/models/qc_problem_group.py @@ -0,0 +1,17 @@ +# -*- coding: utf-8 -*- +# Copyright 2017 Eficent Business and IT Consulting Services S.L. +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +from openerp import fields, models + + +class QcProblemGroup(models.Model): + _name = "qc.problem.group" + _description = "Quality Control Problem Tracking Groups" + + name = fields.Char() + problem_ids = fields.One2many( + comodel_name="qc.problem", inverse_name="problem_group_id") + company_id = fields.Many2one( + comodel_name='res.company', string='Company', required=True, + default=lambda self: self.env.user.company_id) diff --git a/quality_control_issue/models/qc_stage.py b/quality_control_issue/models/qc_stage.py new file mode 100644 index 000000000..1b237e255 --- /dev/null +++ b/quality_control_issue/models/qc_stage.py @@ -0,0 +1,41 @@ +# -*- coding: utf-8 -*- +# Copyright 2017 Eficent Business and IT Consulting Services S.L. +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +from openerp import api, fields, models + +AVAILABLE_PRIORITIES = [ + ('0', 'Normal'), + ('1', 'Low'), + ('2', 'High'), + ('3', 'Very High'), +] + + +class QualityControlStage(models.Model): + _name = "qc.stage" + _rec_name = 'name' + _order = "sequence, name, id" + + @api.model + def default_get(self, fields): + """ Hack : when going from the kanban view, creating a stage with a + qc team in context should not create a stage for the current team only. + """ + ctx = dict(self.env.context) + if ctx.get('default_qc_team_id') and not ctx.get('some_context'): + ctx.pop('default_qc_team_id') + return super(QualityControlStage, + self.with_context(ctx)).default_get(fields) + + name = fields.Char('Stage Name', required=True) + sequence = fields.Integer( + string='Sequence', help="Used to order stages. Lower is better.", + default=1) + qc_team_id = fields.Many2one( + comodel_name='qc.team', string='Quality Control Team', + ondelete='set null') + fold = fields.Boolean( + string='Folded in Pipeline', default=False, + help='This stage is folded in the kanban view when there are no ' + 'records in that stage to display.') diff --git a/quality_control_issue/security/ir.model.access.csv b/quality_control_issue/security/ir.model.access.csv new file mode 100644 index 000000000..eaf2e3511 --- /dev/null +++ b/quality_control_issue/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_user_qc_issue,qc.issue.user,quality_control_issue.model_qc_issue,quality_control.group_quality_control_user,1,1,1,0 +access_manager_qc_issue,qc.issue.manager,quality_control_issue.model_qc_issue,quality_control.group_quality_control_manager,1,1,1,1 +access_user_qc_problem_group,qc.problem.group.user,quality_control_issue.model_qc_problem_group,quality_control.group_quality_control_user,1,0,0,0 +access_manager_qc_problem_group,qc.problem.group.manager,quality_control_issue.model_qc_problem_group,quality_control.group_quality_control_manager,1,1,1,1 +access_user_qc_problem,qc.problem.user,quality_control_issue.model_qc_problem,quality_control.group_quality_control_user,1,1,1,0 +access_manager_qc_team,qc.problem.manager,quality_control_issue.model_qc_problem,quality_control.group_quality_control_manager,1,1,1,1 +access_user_qc_stage,qc.stage.user,quality_control_issue.model_qc_stage,quality_control.group_quality_control_user,1,0,0,0 +access_manager_qc_stage,qc.stage.manager,quality_control_issue.model_qc_stage,quality_control.group_quality_control_manager,1,1,1,1 +access_user_qc_issue_stage,qc.issue.stage.user,quality_control_issue.model_qc_issue_stage,quality_control.group_quality_control_user,1,0,0,0 +access_manager_qc_issue_stage,qc.issue.stage.manager,quality_control_issue.model_qc_issue_stage,quality_control.group_quality_control_manager,1,1,1,1 diff --git a/quality_control_issue/security/quality_control_issue_security.xml b/quality_control_issue/security/quality_control_issue_security.xml new file mode 100644 index 000000000..014abff4e --- /dev/null +++ b/quality_control_issue/security/quality_control_issue_security.xml @@ -0,0 +1,22 @@ + + + + + + qc.issue multi-company + + + ['|',('company_id','=',False),('company_id','child_of',[user.company_id.id])] + + + + qc.issue multi-company + + + ['|',('company_id','=',False),('company_id','child_of',[user.company_id.id])] + + + + diff --git a/quality_control_issue/static/description/icon.png b/quality_control_issue/static/description/icon.png new file mode 100644 index 000000000..e0aa80bce Binary files /dev/null and b/quality_control_issue/static/description/icon.png differ diff --git a/quality_control_issue/static/description/icon.svg b/quality_control_issue/static/description/icon.svg new file mode 100644 index 000000000..1c275d282 --- /dev/null +++ b/quality_control_issue/static/description/icon.svg @@ -0,0 +1,95 @@ + + + + + + + + image/svg+xml + + + + + + + + + + + + + + diff --git a/quality_control_issue/views/qc_issue_view.xml b/quality_control_issue/views/qc_issue_view.xml new file mode 100644 index 000000000..0648932ca --- /dev/null +++ b/quality_control_issue/views/qc_issue_view.xml @@ -0,0 +1,175 @@ + + + + + + + qc.issue.tree + qc.issue + + + + + + + + + + + + + qc.issue.form + qc.issue + +
+
+
+ +
+ +
+
+
+ + + + + + + + + + + + + + + + + + + + + +
+
+ + +
+
+
+
+ + + qc.issue.kanban + qc.issue + + + + + + +
+ +
+
+ +
+ Product:

+ Qty:

+ Location: +

+ Problems: + + ; + + +
+
+
+
+
+
+
+
+ + + qc.issue.serch + qc.issue + + + + + + + + + + + + + + + + + + + + QC Issues + ir.actions.act_window + qc.issue + form + kanban,tree,form + + + + + + +
diff --git a/quality_control_issue/views/qc_problem_group_view.xml b/quality_control_issue/views/qc_problem_group_view.xml new file mode 100644 index 000000000..7a5a0364f --- /dev/null +++ b/quality_control_issue/views/qc_problem_group_view.xml @@ -0,0 +1,48 @@ + + + + + + + qc.problem.group.tree + qc.problem.group + + + + + + + + + qc.problem.group.form + qc.problem.group + +
+ + + + + + + + + + +
+
+
+ + + Problem groups + ir.actions.act_window + qc.problem.group + form + + + + +
diff --git a/quality_control_issue/views/qc_problem_view.xml b/quality_control_issue/views/qc_problem_view.xml new file mode 100644 index 000000000..d86ca46fc --- /dev/null +++ b/quality_control_issue/views/qc_problem_view.xml @@ -0,0 +1,136 @@ + + + + + + + qc.problem.tree + qc.problem + + + + + + + + + + + qc.problem.form + qc.problem + +
+
+ + +
+ + + + + + + + + + + + + + + + + + +
+ + +
+
+
+
+ + + qc.problem.kanban + qc.problem + + + + + + + + +
+ +
+
+ +
+

+ issues + +
+
+
+
+
+
+
+
+ + + qc.problem.serch + qc.problem + + + + + + + + + + + + + + + + + Problem Tracking + ir.actions.act_window + qc.problem + form + kanban,tree,form + + + + + + +
diff --git a/quality_control_issue/views/qc_team_dashboard_view.xml b/quality_control_issue/views/qc_team_dashboard_view.xml new file mode 100644 index 000000000..99bc0376c --- /dev/null +++ b/quality_control_issue/views/qc_team_dashboard_view.xml @@ -0,0 +1,56 @@ + + + + + + Issues by team + qc.issue + kanban,tree,form,pivot + { + 'search_default_qc_team_id': [active_id], + 'default_qc_team_id': active_id, + 'default_user_id': uid, + } + + +

Manage Quality Control Issues through your team flow.

+
+
+ + + Problem tracking + qc.problem + kanban,tree,form,pivot + { + 'search_default_qc_team_id': [active_id], + 'default_qc_team_id': active_id, + 'default_user_id': uid, + } + + +

Track problems through your team flow.

+
+
+ + + qc.team.dashboard - quality_control_issue + qc.team + + + + + + + + + + + + + +