[IMP] mrp: editable timer

This commit is contained in:
Arnold Moyaux
2020-07-27 15:03:05 +00:00
parent 27d6f88ab2
commit 3355371f34
4 changed files with 102 additions and 52 deletions
+63 -22
View File
@@ -95,8 +95,8 @@ class MrpWorkorder(models.Model):
states={'done': [('readonly', True)], 'cancel': [('readonly', True)]},
help="Expected duration (in minutes)")
duration = fields.Float(
'Real Duration', compute='_compute_duration',
readonly=True, store=True)
'Real Duration', compute='_compute_duration', inverse='_set_duration',
readonly=False, store=True)
duration_unit = fields.Float(
'Duration Per Unit', compute='_compute_duration',
readonly=True, store=True)
@@ -282,6 +282,42 @@ class MrpWorkorder(models.Model):
else:
order.duration_percent = 0
def _set_duration(self):
def _float_duration_to_second(duration):
minutes = duration // 1
seconds = (duration % 1) * 60
return minutes * 60 + seconds
for order in self:
old_order_duation = sum(order.time_ids.mapped('duration'))
new_order_duration = order.duration
if new_order_duration == old_order_duation:
continue
delta_duration = new_order_duration - old_order_duation
if delta_duration > 0:
date_start = datetime.now() - timedelta(seconds=_float_duration_to_second(delta_duration))
self.env['mrp.workcenter.productivity'].create(
order._prepare_timeline_vals(delta_duration, date_start, datetime.now())
)
else:
duration_to_remove = abs(delta_duration)
timelines = order.time_ids.sorted(lambda t: t.date_start)
timelines_to_unlink = self.env['mrp.workcenter.productivity']
for timeline in timelines:
if duration_to_remove <= 0.0:
break
if timeline.duration <= duration_to_remove:
duration_to_remove -= timeline.duration
timelines_to_unlink |= timeline
else:
new_time_line_duration = timeline.duration - duration_to_remove
timeline.date_start = timeline.date_end - timedelta(seconds=_float_duration_to_second(new_time_line_duration))
break
timelines_to_unlink.unlink()
@api.depends('duration', 'duration_expected', 'state')
def _compute_progress(self):
for order in self:
@@ -343,7 +379,7 @@ class MrpWorkorder(models.Model):
if workorder.state in ('progress', 'done', 'cancel'):
raise UserError(_('You cannot change the workcenter of a work order that is in progress or done.'))
workorder.leave_id.resource_id = self.env['mrp.workcenter'].browse(values['workcenter_id']).resource_id
if any(k not in ['time_ids', 'duration_expected', 'next_work_order_id'] for k in values.keys()) and any(workorder.state == 'done' for workorder in self):
if 'next_work_order_id' in values and any(workorder.state == 'done' for workorder in self):
raise UserError(_('You can not change the finished work order.'))
if 'date_planned_start' in values or 'date_planned_finished' in values:
for workorder in self:
@@ -484,29 +520,13 @@ class MrpWorkorder(models.Model):
if self.product_tracking == 'serial':
self.qty_producing = 1.0
# Need a loss in case of the real time exceeding the expected
timeline = self.env['mrp.workcenter.productivity']
if not self.duration_expected or self.duration < self.duration_expected:
loss_id = self.env['mrp.workcenter.productivity.loss'].search([('loss_type','=','productive')], limit=1)
if not len(loss_id):
raise UserError(_("You need to define at least one productivity loss in the category 'Productivity'. Create one from the Manufacturing app, menu: Configuration / Productivity Losses."))
else:
loss_id = self.env['mrp.workcenter.productivity.loss'].search([('loss_type','=','performance')], limit=1)
if not len(loss_id):
raise UserError(_("You need to define at least one productivity loss in the category 'Performance'. Create one from the Manufacturing app, menu: Configuration / Productivity Losses."))
self.env['mrp.workcenter.productivity'].create(
self._prepare_timeline_vals(self.duration, datetime.now())
)
if self.production_id.state != 'progress':
self.production_id.write({
'date_start': datetime.now(),
})
timeline.create({
'workorder_id': self.id,
'workcenter_id': self.workcenter_id.id,
'description': _('Time Tracking: %(user)s', user=self.env.user.name),
'loss_id': loss_id[0].id,
'date_start': datetime.now(),
'user_id': self.env.user.id, # FIXME sle: can be inconsistent with company_id
'company_id': self.company_id.id,
})
if self.state == 'progress':
return True
start_date = datetime.now()
@@ -707,6 +727,27 @@ class MrpWorkorder(models.Model):
round=False
)
def _prepare_timeline_vals(self, duration, date_start, date_end=False):
# Need a loss in case of the real time exceeding the expected
if not self.duration_expected or duration < self.duration_expected:
loss_id = self.env['mrp.workcenter.productivity.loss'].search([('loss_type', '=', 'productive')], limit=1)
if not len(loss_id):
raise UserError(_("You need to define at least one productivity loss in the category 'Productivity'. Create one from the Manufacturing app, menu: Configuration / Productivity Losses."))
else:
loss_id = self.env['mrp.workcenter.productivity.loss'].search([('loss_type', '=', 'performance')], limit=1)
if not len(loss_id):
raise UserError(_("You need to define at least one productivity loss in the category 'Performance'. Create one from the Manufacturing app, menu: Configuration / Productivity Losses."))
return {
'workorder_id': self.id,
'workcenter_id': self.workcenter_id.id,
'description': _('Time Tracking: %(user)s', user=self.env.user.name),
'loss_id': loss_id[0].id,
'date_start': date_start,
'date_end': date_end,
'user_id': self.env.user.id, # FIXME sle: can be inconsistent with company_id
'company_id': self.company_id.id,
}
def _update_finished_move(self):
""" Update the finished move & move lines in order to set the finished
product lot on it as well as the produced quantity. This method get the
+28 -18
View File
@@ -4,6 +4,7 @@ odoo.define('mrp.mrp_state', function (require) {
var AbstractField = require('web.AbstractField');
var core = require('web.core');
var fields = require('web.basic_fields');
var fieldUtils = require('web.field_utils');
var field_registry = require('web.field_registry');
var time = require('web.time');
@@ -44,11 +45,13 @@ var SetBulletStatus = AbstractField.extend({
}
});
var TimeCounter = AbstractField.extend({
supportedFieldTypes: [],
/**
* @override
*/
var TimeCounter = fields.FieldFloatTime.extend({
init: function () {
this._super.apply(this, arguments);
this.duration = this.record.data.duration;
},
willStart: function () {
var self = this;
var def = this._rpc({
@@ -56,16 +59,19 @@ var TimeCounter = AbstractField.extend({
method: 'search_read',
domain: [
['workorder_id', '=', this.record.data.id],
['date_end', '=', false],
],
}).then(function (result) {
if (self.mode === 'readonly') {
var currentDate = new Date();
self.duration = 0;
_.each(result, function (data) {
self.duration += data.date_end ?
self._getDateDifference(data.date_start, data.date_end) :
self._getDateDifference(time.auto_str_to_date(data.date_start), currentDate);
});
var currentDate = new Date();
var duration = 0;
if (result.length > 0) {
duration += self._getDateDifference(time.auto_str_to_date(result[0].date_start), currentDate);
}
var minutes = duration / 60 >> 0;
var seconds = duration % 60;
self.duration += minutes + seconds / 60;
if (self.mode === 'edit') {
self.value = self.duration;
}
});
return Promise.all([this._super.apply(this, arguments), def]);
@@ -100,13 +106,17 @@ var TimeCounter = AbstractField.extend({
* @returns {integer} the difference in millisecond
*/
_getDateDifference: function (dateStart, dateEnd) {
return moment(dateEnd).diff(moment(dateStart));
return moment(dateEnd).diff(moment(dateStart), 'seconds');
},
/**
* @override
*/
_render: function () {
this._startTimeCounter();
_renderReadonly: function () {
if (this.record.data.is_user_working) {
this._startTimeCounter();
} else {
this._super.apply(this, arguments);
}
},
/**
* @private
@@ -116,13 +126,13 @@ var TimeCounter = AbstractField.extend({
clearTimeout(this.timer);
if (this.record.data.is_user_working) {
this.timer = setTimeout(function () {
self.duration += 1000;
self.duration += 1/60;
self._startTimeCounter();
}, 1000);
} else {
clearTimeout(this.timer);
}
this.$el.html($('<span>' + moment.utc(this.duration).format("HH:mm:ss") + '</span>'));
this.$el.text(fieldUtils.format.float_time(this.duration));
},
});
+10 -11
View File
@@ -53,11 +53,16 @@ QUnit.module('mrp', {
QUnit.test("mrp_time_counter: basic rendering", async function (assert) {
assert.expect(2);
var data = {
foo: {
fields: { duration: { string: "Duration", type: "float" } },
records: [{id: 1, duration:150.5}]
},
};
var form = await createView({
View: FormView,
model: 'partner',
data: this.data,
model: 'foo',
data: data,
res_id: 1,
arch:
'<form>' +
@@ -66,19 +71,13 @@ QUnit.module('mrp', {
mockRPC: function (route, args) {
if (args.method === 'search_read' && args.model === 'mrp.workcenter.productivity') {
assert.ok(true, "the widget should fetch the mrp.workcenter.productivity");
return Promise.resolve([{
date_start: '2017-01-01 08:00:00',
date_end: '2017-01-01 10:00:00',
}, {
date_start: '2017-01-01 12:00:00',
date_end: '2017-01-01 12:30:00',
}]);
return Promise.resolve([]);
}
return this._super.apply(this, arguments);
},
});
assert.strictEqual(form.$('.o_field_widget[name="duration"]').text(), "02:30:00",
assert.strictEqual(form.$('.o_field_widget[name="duration"]').text(), "150:30",
"the timer should be correctly set");
form.destroy();
+1 -1
View File
@@ -86,7 +86,7 @@
<field name="date_finished" optional="hide" readonly="1"/>
<field name="duration_expected" widget="float_time"/>
<field name="duration" widget="mrp_time_counter"
attrs="{'invisible': ['|', ('production_state','=', 'draft'), ('time_ids', '=', [])]}"/>
attrs="{'invisible': [('production_state','=', 'draft')], 'readonly': [('is_user_working', '=', True)]}"/>
<field name="state" widget="badge" decoration-success="state == 'done'" decoration-info="state not in ('done', 'cancel')"/>
<button name="button_start" type="object" string="Start" class="btn-success"
attrs="{'invisible': ['|', '|', '|', ('production_state','in', ('draft', 'done')), ('working_state', '=', 'blocked'), ('state', '=', 'done'), ('is_user_working', '!=', False)]}"/>