diff --git a/maintenance_repair/README.rst b/maintenance_repair/README.rst
new file mode 100644
index 00000000..1b2a6ab9
--- /dev/null
+++ b/maintenance_repair/README.rst
@@ -0,0 +1,33 @@
+**************************
+Hibou - Maintenance Repair
+**************************
+
+Keep track of parts required to repair equipment.
+
+For more information and add-ons, visit `Hibou.io `_.
+
+
+=============
+Main Features
+=============
+
+* Consume products on Maintenance Requests.
+* New Model: Maintenance Request Repair Line
+* New 'Parts' notebook tab on Maintenance Request form.
+* New Equipment Repair filter 'To Repair' to view maintenance requests with part line items that have not yet been consumed.
+* Tally for total cost of Parts.
+* Includes Employee permissions for managing maintenance request repair line items
+
+.. image:: https://user-images.githubusercontent.com/15882954/41262389-6665024a-6d95-11e8-9d94-236c635e1cf2.png
+ :alt: 'Equipment Request Detail'
+ :width: 988
+ :align: left
+
+
+=======
+License
+=======
+
+Please see `LICENSE `_.
+
+Copyright Hibou Corp. 2018
diff --git a/maintenance_repair/__init__.py b/maintenance_repair/__init__.py
new file mode 100644
index 00000000..0650744f
--- /dev/null
+++ b/maintenance_repair/__init__.py
@@ -0,0 +1 @@
+from . import models
diff --git a/maintenance_repair/__manifest__.py b/maintenance_repair/__manifest__.py
new file mode 100644
index 00000000..58faf7a4
--- /dev/null
+++ b/maintenance_repair/__manifest__.py
@@ -0,0 +1,25 @@
+{
+ 'name': 'Equipment Repair',
+ 'version': '17.0.1.0.0',
+ 'author': 'Hibou Corp. ',
+ 'category': 'Human Resources',
+ 'summary': 'Consume products on Maintenance Requests',
+ 'description': """
+Equipment Repair
+================
+
+Keep track of parts required to repair equipment.
+""",
+ 'website': 'https://hibou.io/',
+ 'depends': [
+ 'stock',
+ 'maintenance_notebook',
+ 'hr_department_project',
+ ],
+ 'data': [
+ 'security/ir.model.access.csv',
+ 'views/maintenance_views.xml',
+ ],
+ 'installable': True,
+ 'auto_install': False,
+}
diff --git a/maintenance_repair/models/__init__.py b/maintenance_repair/models/__init__.py
new file mode 100644
index 00000000..12bf298f
--- /dev/null
+++ b/maintenance_repair/models/__init__.py
@@ -0,0 +1 @@
+from . import maintenance
diff --git a/maintenance_repair/models/maintenance.py b/maintenance_repair/models/maintenance.py
new file mode 100644
index 00000000..fb2bebae
--- /dev/null
+++ b/maintenance_repair/models/maintenance.py
@@ -0,0 +1,144 @@
+from odoo import api, fields, models, _
+from odoo.addons import decimal_precision as dp
+from odoo.exceptions import UserError, ValidationError
+
+
+class StockMove(models.Model):
+ _inherit = 'stock.move'
+
+ maintenance_request_id = fields.Many2one('maintenance.request')
+
+
+class MaintenanceTeam(models.Model):
+ _inherit = 'maintenance.team'
+
+ repair_location_id = fields.Many2one('stock.location', string='Default Repair Parts Source')
+ repair_location_dest_id = fields.Many2one('stock.location', string='Default Repair Parts Destination')
+
+
+class MaintenanceRequest(models.Model):
+ _inherit = 'maintenance.request'
+
+ maintenance_type = fields.Selection(selection_add=[('negligence', 'Negligence')])
+ repair_line_ids = fields.One2many('maintenance.request.repair.line', 'request_id', 'Parts', copy=True)
+ repair_status = fields.Selection([
+ ('repaired', 'Repaired'),
+ ('to repair', 'To Repair'),
+ ('no', 'Nothing to Repair')
+ ], string='Repair Status', compute='_get_repaired', store=True, readonly=True)
+ repair_location_id = fields.Many2one('stock.location', string='Source Location')
+ repair_location_dest_id = fields.Many2one('stock.location', string='Destination Location')
+ total_lst_price = fields.Float(string='Total Price', compute='_compute_repair_totals', store=True)
+ total_standard_price = fields.Float(string='Total Est. Cost', compute='_compute_repair_totals', store=True)
+ total_cost = fields.Float(string='Total Cost', compute='_compute_repair_totals', store=True)
+
+ @api.model_create_multi
+ def create(self, vals_list):
+ res = super().create(vals_list)
+ for request in res:
+ request.repair_location_id = request.repair_location_id or request.maintenance_team_id.repair_location_id
+ request.repair_location_dest_id = request.repair_location_id or request.maintenance_team_id.repair_location_dest_id
+ return res
+
+ @api.depends('repair_line_ids.lst_price', 'repair_line_ids.standard_price', 'repair_line_ids.cost')
+ def _compute_repair_totals(self):
+ for repair in self:
+ repair.total_lst_price = sum(l.lst_price for l in repair.repair_line_ids)
+ repair.total_standard_price = sum(l.standard_price for l in repair.repair_line_ids)
+ repair.total_cost = sum(l.cost for l in repair.repair_line_ids)
+
+ @api.depends('repair_line_ids.state')
+ def _get_repaired(self):
+ for request in self:
+ if not request.repair_line_ids:
+ request.repair_status = 'no'
+ elif request.repair_line_ids.filtered(lambda l: l.state != 'done'):
+ request.repair_status = 'to repair'
+ else:
+ request.repair_status = 'repaired'
+
+ @api.onchange('maintenance_team_id')
+ def _onchange_maintenance_team(self):
+ for request in self:
+ if request.maintenance_team_id:
+ request.repair_location_id = request.maintenance_team_id.repair_location_id
+ request.repair_location_dest_id = request.maintenance_team_id.repair_location_dest_id
+
+
+ def action_complete_repair(self):
+ for request in self.filtered(lambda r: r.repair_status == 'to repair'):
+ request.repair_line_ids.action_complete()
+ return True
+
+class MaintenanceRequestRepairLine(models.Model):
+ _name = 'maintenance.request.repair.line'
+ _description = 'Maintenance Request Repair Lines'
+
+ request_id = fields.Many2one('maintenance.request', copy=False)
+ product_id = fields.Many2one('product.product', 'Product', required=True)
+ product_uom_qty = fields.Float('Quantity', default=1.0, digits='Product Unit of Measure', required=True)
+ product_uom_id = fields.Many2one('uom.uom', 'Product Unit of Measure', required=True)
+ state = fields.Selection([
+ ('draft', 'Draft'),
+ ('done', 'Done'),
+ ], string='State', copy=False, default='draft')
+ move_id = fields.Many2one('stock.move', string='Stock Move')
+ lst_price = fields.Float(string='Sale Price')
+ standard_price = fields.Float(string='Est. Cost')
+ cost = fields.Float(string='Cost', compute='_compute_actual_cost', store=True)
+
+ def unlink(self):
+ if self.filtered(lambda l: l.state == 'done'):
+ raise UserError(_('Only draft lines can be deleted.'))
+ return super(MaintenanceRequestRepairLine, self).unlink()
+
+ @api.onchange('product_id', 'product_uom_qty')
+ def onchange_product_id(self):
+ if self.product_id:
+ self.product_uom_id = self.product_id.uom_id
+ self.lst_price = self.product_id.lst_price * self.product_uom_qty
+ self.standard_price = self.product_id.standard_price * self.product_uom_qty
+
+ @api.depends('product_id', 'move_id')
+ def _compute_actual_cost(self):
+ for line in self:
+ if line.move_id:
+ line.cost = sum(abs(m.amount) for m in line.move_id.account_move_ids)
+ else:
+ line.cost = 0.0
+
+ def _repair_complete_stock_move_values(self):
+ values = {
+ 'name': self.request_id.name,
+ 'product_id': self.product_id.id,
+ 'product_uom_qty': self.product_uom_qty,
+ 'product_uom': self.product_uom_id.id,
+ 'location_id': self.request_id.repair_location_id.id,
+ 'location_dest_id': self.request_id.repair_location_dest_id.id,
+ 'maintenance_request_id': self.request_id.id,
+ 'origin': self.request_id.name,
+ }
+ # Optional modules for linking maintenance requests to projects, and stock moves to analytic accounts
+ if hasattr(self.request_id, 'project_id') and hasattr(self.env['stock.move'], 'analytic_account_id'):
+ values['analytic_account_id'] = self.request_id.project_id.analytic_account_id.id
+ return values
+
+ def action_complete(self):
+ # Create stock movements. - Inspired by mrp_repair
+ stock_move_model = self.env['stock.move']
+ for line in self.filtered(lambda l: not l.state == 'done'):
+ move = stock_move_model.create(self._repair_complete_stock_move_values())
+ move = move.sudo()
+ move._action_confirm()
+ move._action_assign()
+ if move.state != 'assigned':
+ raise ValidationError(_('Unable to reserve inventory.'))
+
+ move.quantity = line.product_uom_qty
+ move.picked = True
+ move._action_done()
+ if move.state != 'done':
+ raise ValidationError(_('Unable to move inventory.'))
+
+ line.write({'move_id': move.id, 'state': 'done'})
+ return True
diff --git a/maintenance_repair/security/ir.model.access.csv b/maintenance_repair/security/ir.model.access.csv
new file mode 100644
index 00000000..26e989ac
--- /dev/null
+++ b/maintenance_repair/security/ir.model.access.csv
@@ -0,0 +1,2 @@
+"id","name","model_id:id","group_id:id","perm_read","perm_write","perm_create","perm_unlink"
+"access_maintenance_request_repair_line","access maintenance.request.repair.line","model_maintenance_request_repair_line","base.group_user",1,1,1,1
\ No newline at end of file
diff --git a/maintenance_repair/tests/__init__.py b/maintenance_repair/tests/__init__.py
new file mode 100644
index 00000000..3820c3df
--- /dev/null
+++ b/maintenance_repair/tests/__init__.py
@@ -0,0 +1 @@
+from . import test_maintenance_repair
diff --git a/maintenance_repair/tests/test_maintenance_repair.py b/maintenance_repair/tests/test_maintenance_repair.py
new file mode 100644
index 00000000..aa358fa7
--- /dev/null
+++ b/maintenance_repair/tests/test_maintenance_repair.py
@@ -0,0 +1,37 @@
+from odoo.tests import common
+
+
+class TestMaintenanceRepair(common.TransactionCase):
+ """Tests for repairs
+ """
+
+ def test_create(self):
+ equipment = self.env['maintenance.equipment'].create({
+ 'name': 'Monitor',
+ })
+
+ loc_from = self.env.ref('stock.stock_location_stock')
+ loc_to = self.env.ref('stock.stock_location_output')
+ request = self.env['maintenance.request'].create({
+ 'name': 'Repair Monitor',
+ 'equipment_id': equipment.id,
+ 'repair_location_id': loc_from.id,
+ 'repair_location_dest_id': loc_to.id,
+ })
+ self.assertEqual(request.repair_status, 'no')
+
+ product_to_repair = self.env.ref('product.product_product_24_product_template').product_variant_id
+ line = self.env['maintenance.request.repair.line'].create({
+ 'request_id': request.id,
+ 'product_id': product_to_repair.id,
+ 'product_uom_id': product_to_repair.uom_id.id,
+ })
+
+ self.assertEqual(request.repair_status, 'to repair')
+ line.action_complete()
+ self.assertEqual(request.repair_status, 'repaired')
+ self.assertEqual(line.state, 'done')
+ self.assertTrue(line.move_id, 'Expect a stock move to be done.')
+
+
+
diff --git a/maintenance_repair/views/maintenance_views.xml b/maintenance_repair/views/maintenance_views.xml
new file mode 100644
index 00000000..adb8332e
--- /dev/null
+++ b/maintenance_repair/views/maintenance_views.xml
@@ -0,0 +1,81 @@
+
+
+
+ maintenance.team.form.inherited
+ maintenance.team
+
+
+
+
+
+
+
+
+
+
+ equipment.request.form.inherited
+ maintenance.request
+
+
+
+
+
+
+
+
+
+
+ equipment.request.form.notebook.inherited
+ maintenance.request
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ equipment.request.search.inherited
+ maintenance.request
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/maintenance_timesheet/models/maintenance.py b/maintenance_timesheet/models/maintenance.py
index 70c7ebad..c9433733 100644
--- a/maintenance_timesheet/models/maintenance.py
+++ b/maintenance_timesheet/models/maintenance.py
@@ -34,13 +34,14 @@ class MaintenanceRequest(models.Model):
department_id = fields.Many2one('hr.department')
- @api.model
- def create(self, values):
- if not values.get('project_id') and values.get('department_id'):
- department = self.env['hr.department'].browse(values.get('department_id'))
- if department and department.project_ids:
- values.update({'project_id': department.project_ids.ids[0]})
- return super(MaintenanceRequest, self).create(values)
+ @api.model_create_multi
+ def create(self, vals_list):
+ for vals in vals_list:
+ if not vals.get('project_id') and vals.get('department_id'):
+ department = self.env['hr.department'].browse(vals.get('department_id'))
+ if department.project_ids:
+ vals['project_id'] = department.project_ids.ids[0]
+ return super(MaintenanceRequest, self).create(vals_list)
@api.depends('duration', 'timesheet_ids.unit_amount')
def _hours_get(self):
diff --git a/maintenance_usage/models/maintenance.py b/maintenance_usage/models/maintenance.py
index 6547dc43..73e3d7fc 100644
--- a/maintenance_usage/models/maintenance.py
+++ b/maintenance_usage/models/maintenance.py
@@ -16,7 +16,7 @@ class MaintenanceEquipment(models.Model):
department_id = fields.Many2one(tracking=False)
usage_qty = fields.Float(string='Usage', default=0.0)
usage_uom_id = fields.Many2one('uom.uom', related='category_id.usage_uom_id')
- usage_log_ids = fields.One2many('maintenance.usage.log', 'equipment_id', string='Usage')
+ usage_log_ids = fields.One2many('maintenance.usage.log', 'equipment_id', string='Usage Logs')
usage_count = fields.Integer(string='Usage Count', compute='_compute_usage_count')
maintenance_usage = fields.Float(string='Preventative Usage')
period = fields.Integer(compute='_compute_period', string='Days betweeen each preventive maintenance',
@@ -31,12 +31,13 @@ class MaintenanceEquipment(models.Model):
recurring = equipment.maintenance_ids.filtered('repeat_unit')
equipment.period = min([get_timedelta(r.repeat_interval, r.repeat_unit).days for r in recurring], default=0)
- @api.model
- def create(self, values):
- record = super(MaintenanceEquipment, self).create(values)
- # create first usage record
- record._log_usage()
- return record
+ @api.model_create_multi
+ def create(self, vals_list):
+ res = super(MaintenanceEquipment, self).create(vals_list)
+ for equipment in res:
+ # create first usage record
+ equipment._log_usage()
+ return res
def write(self, values):
usage_qty = values.get('usage_qty')
@@ -117,6 +118,7 @@ class MaintenanceUsageLog(models.Model):
_name = 'maintenance.usage.log'
_order = 'date DESC'
_log_access = False
+ _description = 'Maintenance Usage Log'
date = fields.Datetime(string='Date', default=fields.Datetime.now)
equipment_id = fields.Many2one('maintenance.equipment', string='Equipment', required=True)
@@ -125,13 +127,14 @@ class MaintenanceUsageLog(models.Model):
qty = fields.Float(string='Quantity')
uom_id = fields.Many2one(string='Unit of Measure', related='equipment_id.category_id.usage_uom_id')
- @api.model
- def create(self, values):
- equipment = self.env['maintenance.equipment'].browse(values.get('equipment_id'))
- if not values.get('employee_id'):
- values['employee_id'] = equipment.employee_id.id
- if not values.get('department_id'):
- values['department_id'] = equipment.department_id.id
- if not values.get('qty'):
- values['qty'] = equipment.usage_qty
- return super(MaintenanceUsageLog, self).create(values)
+ @api.model_create_multi
+ def create(self, vals_list):
+ for vals in vals_list:
+ equipment = self.env['maintenance.equipment'].browse(vals.get('equipment_id'))
+ if not vals.get('employee_id'):
+ vals['employee_id'] = equipment.employee_id.id
+ if not vals.get('department_id'):
+ vals['department_id'] = equipment.department_id.id
+ if not vals.get('qty'):
+ vals['qty'] = equipment.usage_qty
+ return super(MaintenanceUsageLog, self).create(vals_list)