[IMP] mrp: editable timer
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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));
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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)]}"/>
|
||||
|
||||
Reference in New Issue
Block a user