[MERGE] forward port branch saas-11.3 up to f66a9cdbcf
This commit is contained in:
@@ -406,7 +406,7 @@ class AccountMove(models.Model):
|
|||||||
aml = ac_move.line_ids.filtered(lambda x: x.account_id.reconcile or x.account_id.internal_type == 'liquidity')
|
aml = ac_move.line_ids.filtered(lambda x: x.account_id.reconcile or x.account_id.internal_type == 'liquidity')
|
||||||
aml.remove_move_reconcile()
|
aml.remove_move_reconcile()
|
||||||
#reconcile together the reconcilable (or the liquidity aml) and their newly created counterpart
|
#reconcile together the reconcilable (or the liquidity aml) and their newly created counterpart
|
||||||
for account in list(set([x.account_id for x in aml])):
|
for account in set([x.account_id for x in aml]):
|
||||||
to_rec = aml.filtered(lambda y: y.account_id == account)
|
to_rec = aml.filtered(lambda y: y.account_id == account)
|
||||||
to_rec |= reversed_move.line_ids.filtered(lambda y: y.account_id == account)
|
to_rec |= reversed_move.line_ids.filtered(lambda y: y.account_id == account)
|
||||||
#reconciliation will be full, so speed up the computation by using skip_full_reconcile_check in the context
|
#reconciliation will be full, so speed up the computation by using skip_full_reconcile_check in the context
|
||||||
@@ -1061,10 +1061,8 @@ class AccountMoveLine(models.Model):
|
|||||||
# the provided values were not already multi-currency
|
# the provided values were not already multi-currency
|
||||||
if account.currency_id and 'amount_currency' not in vals and account.currency_id.id != account.company_id.currency_id.id:
|
if account.currency_id and 'amount_currency' not in vals and account.currency_id.id != account.company_id.currency_id.id:
|
||||||
vals['currency_id'] = account.currency_id.id
|
vals['currency_id'] = account.currency_id.id
|
||||||
ctx = {}
|
date = vals.get('date') or vals.get('date_maturity') or fields.Date.today()
|
||||||
if 'date' in vals:
|
vals['amount_currency'] = account.company_id.currency_id._convert(amount, account.currency_id, account.company_id, date)
|
||||||
ctx['date'] = vals['date']
|
|
||||||
vals['amount_currency'] = account.company_id.currency_id._convert(amount, account.currency_id, account.company_id, vals.get('date', fields.Date.today()))
|
|
||||||
|
|
||||||
if not ok:
|
if not ok:
|
||||||
raise UserError(_('You cannot use this general account in this journal, check the tab \'Entry Controls\' on the related journal.'))
|
raise UserError(_('You cannot use this general account in this journal, check the tab \'Entry Controls\' on the related journal.'))
|
||||||
|
|||||||
@@ -90,6 +90,7 @@ class HrEmployee(models.Model):
|
|||||||
action_message = self.env.ref('hr_attendance.hr_attendance_action_greeting_message').read()[0]
|
action_message = self.env.ref('hr_attendance.hr_attendance_action_greeting_message').read()[0]
|
||||||
action_message['previous_attendance_change_date'] = self.last_attendance_id and (self.last_attendance_id.check_out or self.last_attendance_id.check_in) or False
|
action_message['previous_attendance_change_date'] = self.last_attendance_id and (self.last_attendance_id.check_out or self.last_attendance_id.check_in) or False
|
||||||
action_message['employee_name'] = self.name
|
action_message['employee_name'] = self.name
|
||||||
|
action_message['barcode'] = self.barcode
|
||||||
action_message['next_action'] = next_action
|
action_message['next_action'] = next_action
|
||||||
|
|
||||||
if self.user_id:
|
if self.user_id:
|
||||||
|
|||||||
@@ -51,6 +51,7 @@ var GreetingMessage = AbstractAction.extend({
|
|||||||
this.attendance.check_in_time = this.attendance.check_in && this.attendance.check_in.format(this.format_time);
|
this.attendance.check_in_time = this.attendance.check_in && this.attendance.check_in.format(this.format_time);
|
||||||
this.attendance.check_out_time = this.attendance.check_out && this.attendance.check_out.format(this.format_time);
|
this.attendance.check_out_time = this.attendance.check_out && this.attendance.check_out.format(this.format_time);
|
||||||
this.employee_name = action.employee_name;
|
this.employee_name = action.employee_name;
|
||||||
|
this.attendanceBarcode = action.barcode;
|
||||||
},
|
},
|
||||||
|
|
||||||
start: function() {
|
start: function() {
|
||||||
@@ -136,21 +137,27 @@ var GreetingMessage = AbstractAction.extend({
|
|||||||
|
|
||||||
_onBarcodeScanned: function(barcode) {
|
_onBarcodeScanned: function(barcode) {
|
||||||
var self = this;
|
var self = this;
|
||||||
if (this.return_to_main_menu) { // in case of multiple scans in the greeting message view, delete the timer, a new one will be created.
|
if (this.attendanceBarcode !== barcode){
|
||||||
clearTimeout(this.return_to_main_menu);
|
if (this.return_to_main_menu) { // in case of multiple scans in the greeting message view, delete the timer, a new one will be created.
|
||||||
|
clearTimeout(this.return_to_main_menu);
|
||||||
|
}
|
||||||
|
core.bus.off('barcode_scanned', this, this._onBarcodeScanned);
|
||||||
|
this._rpc({
|
||||||
|
model: 'hr.employee',
|
||||||
|
method: 'attendance_scan',
|
||||||
|
args: [barcode, ],
|
||||||
|
})
|
||||||
|
.then(function (result) {
|
||||||
|
if (result.action) {
|
||||||
|
self.do_action(result.action);
|
||||||
|
} else if (result.warning) {
|
||||||
|
self.do_warn(result.warning);
|
||||||
|
setTimeout( function() { self.do_action(self.next_action, {clear_breadcrumbs: true}); }, 5000);
|
||||||
|
}
|
||||||
|
}, function () {
|
||||||
|
setTimeout( function() { self.do_action(self.next_action, {clear_breadcrumbs: true}); }, 5000);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
this._rpc({
|
|
||||||
model: 'hr.employee',
|
|
||||||
method: 'attendance_scan',
|
|
||||||
args: [barcode, ],
|
|
||||||
})
|
|
||||||
.then(function (result) {
|
|
||||||
if (result.action) {
|
|
||||||
self.do_action(result.action);
|
|
||||||
} else if (result.warning) {
|
|
||||||
self.do_warn(result.warning);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
},
|
},
|
||||||
|
|
||||||
destroy: function () {
|
destroy: function () {
|
||||||
|
|||||||
@@ -36,6 +36,7 @@ var KioskMode = AbstractAction.extend({
|
|||||||
|
|
||||||
_onBarcodeScanned: function(barcode) {
|
_onBarcodeScanned: function(barcode) {
|
||||||
var self = this;
|
var self = this;
|
||||||
|
core.bus.off('barcode_scanned', this, this._onBarcodeScanned);
|
||||||
this._rpc({
|
this._rpc({
|
||||||
model: 'hr.employee',
|
model: 'hr.employee',
|
||||||
method: 'attendance_scan',
|
method: 'attendance_scan',
|
||||||
@@ -46,7 +47,10 @@ var KioskMode = AbstractAction.extend({
|
|||||||
self.do_action(result.action);
|
self.do_action(result.action);
|
||||||
} else if (result.warning) {
|
} else if (result.warning) {
|
||||||
self.do_warn(result.warning);
|
self.do_warn(result.warning);
|
||||||
|
core.bus.on('barcode_scanned', self, self._onBarcodeScanned);
|
||||||
}
|
}
|
||||||
|
}, function () {
|
||||||
|
core.bus.on('barcode_scanned', self, self._onBarcodeScanned);
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|||||||
@@ -2,8 +2,11 @@ odoo.define('hr_attendance.tests', function (require) {
|
|||||||
"use strict";
|
"use strict";
|
||||||
|
|
||||||
var testUtils = require('web.test_utils');
|
var testUtils = require('web.test_utils');
|
||||||
|
var core = require('web.core');
|
||||||
|
|
||||||
var MyAttendances = require('hr_attendance.my_attendances');
|
var MyAttendances = require('hr_attendance.my_attendances');
|
||||||
|
var KioskMode = require('hr_attendance.kiosk_mode');
|
||||||
|
var GreetingMessage = require('hr_attendance.greeting_message');
|
||||||
|
|
||||||
|
|
||||||
QUnit.module('HR Attendance', {
|
QUnit.module('HR Attendance', {
|
||||||
@@ -19,12 +22,30 @@ QUnit.module('HR Attendance', {
|
|||||||
default: 1,
|
default: 1,
|
||||||
},
|
},
|
||||||
user_id: {string: 'user ID', type: 'integer'},
|
user_id: {string: 'user ID', type: 'integer'},
|
||||||
|
barcode: {string:'barcode', type: 'integer'},
|
||||||
},
|
},
|
||||||
records: [{
|
records: [{
|
||||||
id: 1,
|
id: 1,
|
||||||
name: "Employee A",
|
name: "Employee A",
|
||||||
attendance_state: 'checked_out',
|
attendance_state: 'checked_out',
|
||||||
user_id: 1,
|
user_id: 1,
|
||||||
|
barcode: 1,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 2,
|
||||||
|
name: "Employee B",
|
||||||
|
attendance_state: 'checked_out',
|
||||||
|
user_id: 2,
|
||||||
|
barcode: 2,
|
||||||
|
}],
|
||||||
|
},
|
||||||
|
'res.company': {
|
||||||
|
fields: {
|
||||||
|
name: {string: 'Name', type: 'char'},
|
||||||
|
},
|
||||||
|
records: [{
|
||||||
|
id: 1,
|
||||||
|
name: "Company A",
|
||||||
}],
|
}],
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
@@ -51,6 +72,112 @@ QUnit.module('HR Attendance', {
|
|||||||
clientAction.destroy();
|
clientAction.destroy();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
QUnit.test('Attendance Kiosk Mode Test', function (assert) {
|
||||||
|
assert.expect(2);
|
||||||
|
|
||||||
|
var $target = $('#qunit-fixture');
|
||||||
|
var self = this;
|
||||||
|
var rpcCount = 0;
|
||||||
|
var clientAction = new KioskMode(null);
|
||||||
|
testUtils.addMockEnvironment(clientAction, {
|
||||||
|
data: this.data,
|
||||||
|
session: {
|
||||||
|
uid: 1,
|
||||||
|
company_id: 1,
|
||||||
|
},
|
||||||
|
mockRPC: function(route, args) {
|
||||||
|
if (args.method === 'attendance_scan' && args.model === 'hr.employee') {
|
||||||
|
|
||||||
|
rpcCount++;
|
||||||
|
return $.when(self.data['hr.employee'].records[0]);
|
||||||
|
}
|
||||||
|
return this._super(route, args);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
clientAction.appendTo($target);
|
||||||
|
core.bus.trigger('barcode_scanned', 1);
|
||||||
|
core.bus.trigger('barcode_scanned', 1);
|
||||||
|
assert.strictEqual(rpcCount, 1, 'RPC call should have been done only once.');
|
||||||
|
|
||||||
|
core.bus.trigger('barcode_scanned', 2);
|
||||||
|
assert.strictEqual(rpcCount, 1, 'RPC call should have been done only once.');
|
||||||
|
|
||||||
|
clientAction.destroy();
|
||||||
|
});
|
||||||
|
|
||||||
|
QUnit.test('Attendance Greeting Message Test', function (assert) {
|
||||||
|
assert.expect(10);
|
||||||
|
|
||||||
|
var $target = $('#qunit-fixture');
|
||||||
|
var self = this;
|
||||||
|
var rpcCount = 0;
|
||||||
|
|
||||||
|
var clientActions = [];
|
||||||
|
function createGreetingMessage (target, barcode){
|
||||||
|
var action = {
|
||||||
|
attendance: {
|
||||||
|
check_in: "2018-09-20 13:41:13",
|
||||||
|
employee_id: [barcode],
|
||||||
|
},
|
||||||
|
next_action: "hr_attendance.hr_attendance_action_kiosk_mode",
|
||||||
|
barcode: barcode,
|
||||||
|
}
|
||||||
|
var clientAction = new GreetingMessage(null, action);
|
||||||
|
testUtils.addMockEnvironment(clientAction, {
|
||||||
|
data: self.data,
|
||||||
|
session: {
|
||||||
|
uid: 1,
|
||||||
|
company_id: 1,
|
||||||
|
},
|
||||||
|
mockRPC: function(route, args) {
|
||||||
|
if (args.method === 'attendance_scan' && args.model === 'hr.employee') {
|
||||||
|
rpcCount++;
|
||||||
|
action.attendance.employee_id = [args.args[0], 'Employee'];
|
||||||
|
/*
|
||||||
|
if rpc have been made, a new instance is created to simulate the same behaviour
|
||||||
|
as functional flow.
|
||||||
|
*/
|
||||||
|
createGreetingMessage (target, args.args[0]);
|
||||||
|
return $.when({action: action});
|
||||||
|
}
|
||||||
|
return this._super(route, args);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
clientAction.appendTo(target);
|
||||||
|
|
||||||
|
clientActions.push(clientAction);
|
||||||
|
};
|
||||||
|
|
||||||
|
// init - mock coming from kiosk
|
||||||
|
createGreetingMessage ($target, 1);
|
||||||
|
assert.strictEqual(clientActions.length, 1, 'Number of clientAction must = 1.');
|
||||||
|
|
||||||
|
core.bus.trigger('barcode_scanned', 1);
|
||||||
|
/*
|
||||||
|
As action is given when instantiate GreetingMessage, we simulate that we come from the KioskMode
|
||||||
|
So rescanning the same barcode won't lead to another RPC.
|
||||||
|
*/
|
||||||
|
assert.strictEqual(clientActions.length, 1, 'Number of clientActions must = 1.');
|
||||||
|
assert.strictEqual(rpcCount, 0, 'RPC call should not have been done.');
|
||||||
|
|
||||||
|
core.bus.trigger('barcode_scanned', 2);
|
||||||
|
assert.strictEqual(clientActions.length, 2, 'Number of clientActions must = 2.');
|
||||||
|
assert.strictEqual(rpcCount, 1, 'RPC call should have been done only once.');
|
||||||
|
core.bus.trigger('barcode_scanned', 2);
|
||||||
|
assert.strictEqual(clientActions.length, 2, 'Number of clientActions must = 2.');
|
||||||
|
assert.strictEqual(rpcCount, 1, 'RPC call should have been done only once.');
|
||||||
|
|
||||||
|
core.bus.trigger('barcode_scanned', 1);
|
||||||
|
assert.strictEqual(clientActions.length, 3, 'Number of clientActions must = 3.');
|
||||||
|
core.bus.trigger('barcode_scanned', 1);
|
||||||
|
assert.strictEqual(clientActions.length, 3, 'Number of clientActions must = 3.');
|
||||||
|
assert.strictEqual(rpcCount, 2, 'RPC call should have been done only twice.');
|
||||||
|
|
||||||
|
_.each(clientActions, function(clientAction) {
|
||||||
|
clientAction.destroy();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -119,16 +119,17 @@ class ReportBomStructure(models.AbstractModel):
|
|||||||
continue
|
continue
|
||||||
price = line.product_id.uom_id._compute_price(line.product_id.standard_price, line.product_uom_id) * line_quantity
|
price = line.product_id.uom_id._compute_price(line.product_id.standard_price, line.product_uom_id) * line_quantity
|
||||||
if line.child_bom_id:
|
if line.child_bom_id:
|
||||||
factor = float_round(line.product_uom_id._compute_quantity(line_quantity, line.child_bom_id.product_uom_id) / line.child_bom_id.product_qty, precision_rounding=1, rounding_method='UP')
|
factor = line.product_uom_id._compute_quantity(line_quantity, line.child_bom_id.product_uom_id) / line.child_bom_id.product_qty
|
||||||
sub_total = self._get_price(line.child_bom_id, factor)
|
sub_total = self._get_price(line.child_bom_id, factor)
|
||||||
else:
|
else:
|
||||||
sub_total = price
|
sub_total = price
|
||||||
|
sub_total = self.env.user.company_id.currency_id.round(sub_total)
|
||||||
components.append({
|
components.append({
|
||||||
'prod_id': line.product_id.id,
|
'prod_id': line.product_id.id,
|
||||||
'prod_name': line.product_id.display_name,
|
'prod_name': line.product_id.display_name,
|
||||||
'prod_qty': line_quantity,
|
'prod_qty': line_quantity,
|
||||||
'prod_uom': line.product_uom_id.name,
|
'prod_uom': line.product_uom_id.name,
|
||||||
'prod_cost': price,
|
'prod_cost': self.env.user.company_id.currency_id.round(price),
|
||||||
'parent_id': bom.id,
|
'parent_id': bom.id,
|
||||||
'line_id': line.id,
|
'line_id': line.id,
|
||||||
'level': level or 0,
|
'level': level or 0,
|
||||||
@@ -151,14 +152,21 @@ class ReportBomStructure(models.AbstractModel):
|
|||||||
'operation': operation,
|
'operation': operation,
|
||||||
'name': operation.name + ' - ' + operation.workcenter_id.name,
|
'name': operation.name + ' - ' + operation.workcenter_id.name,
|
||||||
'duration_expected': duration_expected,
|
'duration_expected': duration_expected,
|
||||||
'total': float_round(total, precision_rounding=self.env.user.company_id.currency_id.rounding),
|
'total': self.env.user.company_id.currency_id.round(total),
|
||||||
})
|
})
|
||||||
return operations
|
return operations
|
||||||
|
|
||||||
def _get_price(self, bom, factor):
|
def _get_price(self, bom, factor):
|
||||||
price = 0
|
price = 0
|
||||||
if bom.routing_id:
|
if bom.routing_id:
|
||||||
operations = self._get_operation_line(bom.routing_id, factor, 0)
|
# routing are defined on a BoM and don't have a concept of quantity.
|
||||||
|
# It means that the operation time are defined for the quantity on
|
||||||
|
# the BoM (the user produces a batch of products). E.g the user
|
||||||
|
# product a batch of 10 units with a 5 minutes operation, the time
|
||||||
|
# will be the 5 for a quantity between 1-10, then doubled for
|
||||||
|
# 11-20,...
|
||||||
|
operation_cycle = float_round(factor, precision_rounding=1, rounding_method='UP')
|
||||||
|
operations = self._get_operation_line(bom.routing_id, operation_cycle, 0)
|
||||||
price += sum([op['total'] for op in operations])
|
price += sum([op['total'] for op in operations])
|
||||||
|
|
||||||
for line in bom.bom_line_ids:
|
for line in bom.bom_line_ids:
|
||||||
@@ -168,7 +176,8 @@ class ReportBomStructure(models.AbstractModel):
|
|||||||
price += sub_price
|
price += sub_price
|
||||||
else:
|
else:
|
||||||
prod_qty = line.product_qty * factor
|
prod_qty = line.product_qty * factor
|
||||||
price += (line.product_id.uom_id._compute_price(line.product_id.standard_price, line.product_uom_id) * prod_qty)
|
not_rounded_price = line.product_id.uom_id._compute_price(line.product_id.standard_price, line.product_uom_id) * prod_qty
|
||||||
|
price += self.env.user.company_id.currency_id.round(not_rounded_price)
|
||||||
return price
|
return price
|
||||||
|
|
||||||
def _get_pdf_line(self, bom_id, product_id=False, qty=1, child_bom_ids=[], unfolded=False):
|
def _get_pdf_line(self, bom_id, product_id=False, qty=1, child_bom_ids=[], unfolded=False):
|
||||||
|
|||||||
@@ -3,6 +3,8 @@
|
|||||||
|
|
||||||
from odoo import exceptions
|
from odoo import exceptions
|
||||||
from odoo.addons.mrp.tests.common import TestMrpCommon
|
from odoo.addons.mrp.tests.common import TestMrpCommon
|
||||||
|
from odoo.tests import Form
|
||||||
|
from odoo.tools import float_compare, float_round
|
||||||
|
|
||||||
|
|
||||||
class TestBoM(TestMrpCommon):
|
class TestBoM(TestMrpCommon):
|
||||||
@@ -158,3 +160,190 @@ class TestBoM(TestMrpCommon):
|
|||||||
})
|
})
|
||||||
with self.assertRaises(exceptions.UserError):
|
with self.assertRaises(exceptions.UserError):
|
||||||
test_bom_3.explode(self.product_9, 1)
|
test_bom_3.explode(self.product_9, 1)
|
||||||
|
|
||||||
|
def test_bom_report(self):
|
||||||
|
""" Simulate a crumble receipt with mrp and open the bom structure
|
||||||
|
report and check that data insde are correct.
|
||||||
|
"""
|
||||||
|
uom_kg = self.env.ref('uom.product_uom_kgm')
|
||||||
|
uom_litre = self.env.ref('uom.product_uom_litre')
|
||||||
|
crumble = self.env['product.product'].create({
|
||||||
|
'name': 'Crumble',
|
||||||
|
'type': 'product',
|
||||||
|
'uom_id': uom_kg.id,
|
||||||
|
'uom_po_id': uom_kg.id,
|
||||||
|
})
|
||||||
|
butter = self.env['product.product'].create({
|
||||||
|
'name': 'Butter',
|
||||||
|
'type': 'product',
|
||||||
|
'uom_id': uom_kg.id,
|
||||||
|
'uom_po_id': uom_kg.id,
|
||||||
|
'standard_price': 7.01
|
||||||
|
})
|
||||||
|
biscuit = self.env['product.product'].create({
|
||||||
|
'name': 'Biscuit',
|
||||||
|
'type': 'product',
|
||||||
|
'uom_id': uom_kg.id,
|
||||||
|
'uom_po_id': uom_kg.id,
|
||||||
|
'standard_price': 1.5
|
||||||
|
})
|
||||||
|
bom_form_crumble = Form(self.env['mrp.bom'])
|
||||||
|
bom_form_crumble.product_tmpl_id = crumble.product_tmpl_id
|
||||||
|
bom_form_crumble.product_qty = 11
|
||||||
|
bom_form_crumble.product_uom_id = uom_kg
|
||||||
|
bom_crumble = bom_form_crumble.save()
|
||||||
|
|
||||||
|
with Form(bom_crumble) as bom:
|
||||||
|
with bom.bom_line_ids.new() as line:
|
||||||
|
line.product_id = butter
|
||||||
|
line.product_uom_id = uom_kg
|
||||||
|
line.product_qty = 5
|
||||||
|
with bom.bom_line_ids.new() as line:
|
||||||
|
line.product_id = biscuit
|
||||||
|
line.product_uom_id = uom_kg
|
||||||
|
line.product_qty = 6
|
||||||
|
|
||||||
|
workcenter = self.env['mrp.workcenter'].create({
|
||||||
|
'costs_hour': 10,
|
||||||
|
'name': 'Deserts Table'
|
||||||
|
})
|
||||||
|
|
||||||
|
routing_form = Form(self.env['mrp.routing'])
|
||||||
|
routing_form.name = "Crumble process"
|
||||||
|
routing_crumble = routing_form.save()
|
||||||
|
|
||||||
|
with Form(routing_crumble) as routing:
|
||||||
|
with routing.operation_ids.new() as operation:
|
||||||
|
operation.workcenter_id = workcenter
|
||||||
|
operation.name = 'Prepare biscuits'
|
||||||
|
operation.time_cycle_manual = 5
|
||||||
|
with routing.operation_ids.new() as operation:
|
||||||
|
operation.workcenter_id = workcenter
|
||||||
|
operation.name = 'Prepare butter'
|
||||||
|
operation.time_cycle_manual = 3
|
||||||
|
with routing.operation_ids.new() as operation:
|
||||||
|
operation.workcenter_id = workcenter
|
||||||
|
operation.name = 'Mix manually'
|
||||||
|
operation.time_cycle_manual = 5
|
||||||
|
|
||||||
|
bom_crumble.routing_id = routing_crumble.id
|
||||||
|
|
||||||
|
# TEST BOM STRUCTURE VALUE WITH BOM QUANTITY
|
||||||
|
report_values = self.env['report.mrp.report_bom_structure']._get_report_data(bom_id=bom_crumble.id, searchQty=11, searchVariant=False)
|
||||||
|
# 5 min 'Prepare biscuits' + 3 min 'Prepare butter' + 5 min 'Mix manually' = 13 minutes
|
||||||
|
self.assertEqual(report_values['lines']['operations_time'], 13.0, 'Operation time should be the same for 1 unit or for the batch')
|
||||||
|
# Operation cost is the sum of operation line.
|
||||||
|
operation_cost = float_round(5 / 60 * 10, precision_digits=2) * 2 + float_round(3 / 60 * 10, precision_digits=2)
|
||||||
|
self.assertEqual(float_compare(report_values['lines']['operations_cost'], operation_cost, precision_digits=2), 0, '13 minute for 10$/hours -> 2.16')
|
||||||
|
|
||||||
|
for component_line in report_values['lines']['components']:
|
||||||
|
# standard price * bom line quantity * current quantity / bom finished product quantity
|
||||||
|
if component_line['prod_id'] == butter.id:
|
||||||
|
# 5 kg of butter at 7.01$ for 11kg of crumble -> 35.05$
|
||||||
|
self.assertEqual(float_compare(component_line['total'], (7.01 * 5), precision_digits=2), 0)
|
||||||
|
if component_line['prod_id'] == biscuit.id:
|
||||||
|
# 6 kg of biscuits at 1.50$ for 11kg of crumble -> 9$
|
||||||
|
self.assertEqual(float_compare(component_line['total'], (1.5 * 6), precision_digits=2), 0)
|
||||||
|
# total price = 35.05 + 9 + operation_cost(0.83 + 0.83 + 0.5 = 2.16) = 46,21
|
||||||
|
self.assertEqual(float_compare(report_values['lines']['total'], 46.21, precision_digits=2), 0, 'Product Bom Price is not correct')
|
||||||
|
self.assertEqual(float_compare(report_values['lines']['total'] / 11.0, 4.20, precision_digits=2), 0, 'Product Unit Bom Price is not correct')
|
||||||
|
|
||||||
|
# TEST BOM STRUCTURE VALUE BY UNIT
|
||||||
|
report_values = self.env['report.mrp.report_bom_structure']._get_report_data(bom_id=bom_crumble.id, searchQty=1, searchVariant=False)
|
||||||
|
# 5 min 'Prepare biscuits' + 3 min 'Prepare butter' + 5 min 'Mix manually' = 13 minutes
|
||||||
|
self.assertEqual(report_values['lines']['operations_time'], 13.0, 'Operation time should be the same for 1 unit or for the batch')
|
||||||
|
# Operation cost is the sum of operation line.
|
||||||
|
operation_cost = float_round(5 / 60 * 10, precision_digits=2) * 2 + float_round(3 / 60 * 10, precision_digits=2)
|
||||||
|
self.assertEqual(float_compare(report_values['lines']['operations_cost'], operation_cost, precision_digits=2), 0, '13 minute for 10$/hours -> 2.16')
|
||||||
|
|
||||||
|
for component_line in report_values['lines']['components']:
|
||||||
|
# standard price * bom line quantity * current quantity / bom finished product quantity
|
||||||
|
if component_line['prod_id'] == butter.id:
|
||||||
|
# 5 kg of butter at 7.01$ for 11kg of crumble -> / 11 for price per unit (3.19)
|
||||||
|
self.assertEqual(float_compare(component_line['total'], (7.01 * 5) * (1 / 11), precision_digits=2), 0)
|
||||||
|
if component_line['prod_id'] == biscuit.id:
|
||||||
|
# 6 kg of biscuits at 1.50$ for 11kg of crumble -> / 11 for price per unit (0.82)
|
||||||
|
self.assertEqual(float_compare(component_line['total'], (1.5 * 6) * (1 / 11), precision_digits=2), 0)
|
||||||
|
# total price = 3.19 + 0.82 + operation_cost(0.83 + 0.83 + 0.5 = 2.16) = 6,17
|
||||||
|
self.assertEqual(float_compare(report_values['lines']['total'], 6.17, precision_digits=2), 0, 'Product Unit Bom Price is not correct')
|
||||||
|
|
||||||
|
# TEST OPERATION COST WHEN PRODUCED QTY > BOM QUANTITY
|
||||||
|
report_values_12 = self.env['report.mrp.report_bom_structure']._get_report_data(bom_id=bom_crumble.id, searchQty=12, searchVariant=False)
|
||||||
|
report_values_22 = self.env['report.mrp.report_bom_structure']._get_report_data(bom_id=bom_crumble.id, searchQty=22, searchVariant=False)
|
||||||
|
operation_cost = float_round(10 / 60 * 10, precision_digits=2) * 2 + float_round(6 / 60 * 10, precision_digits=2)
|
||||||
|
# Both needs 2 operation cycle
|
||||||
|
self.assertEqual(report_values_12['lines']['operations_cost'], report_values_22['lines']['operations_cost'])
|
||||||
|
self.assertEqual(report_values_22['lines']['operations_cost'], operation_cost)
|
||||||
|
report_values_23 = self.env['report.mrp.report_bom_structure']._get_report_data(bom_id=bom_crumble.id, searchQty=23, searchVariant=False)
|
||||||
|
operation_cost = float_round(15 / 60 * 10, precision_digits=2) * 2 + float_round(9 / 60 * 10, precision_digits=2)
|
||||||
|
self.assertEqual(report_values_23['lines']['operations_cost'], operation_cost)
|
||||||
|
|
||||||
|
# Create a more complex BoM with a sub product
|
||||||
|
cheese_cake = self.env['product.product'].create({
|
||||||
|
'name': 'Cheese Cake 300g',
|
||||||
|
'type': 'product',
|
||||||
|
})
|
||||||
|
cream = self.env['product.product'].create({
|
||||||
|
'name': 'cream',
|
||||||
|
'type': 'product',
|
||||||
|
'uom_id': uom_litre.id,
|
||||||
|
'uom_po_id': uom_litre.id,
|
||||||
|
'standard_price': 5.17,
|
||||||
|
})
|
||||||
|
bom_form_cheese_cake = Form(self.env['mrp.bom'])
|
||||||
|
bom_form_cheese_cake.product_tmpl_id = cheese_cake.product_tmpl_id
|
||||||
|
bom_form_cheese_cake.product_qty = 60
|
||||||
|
bom_form_cheese_cake.product_uom_id = self.uom_unit
|
||||||
|
bom_cheese_cake = bom_form_cheese_cake.save()
|
||||||
|
|
||||||
|
with Form(bom_cheese_cake) as bom:
|
||||||
|
with bom.bom_line_ids.new() as line:
|
||||||
|
line.product_id = cream
|
||||||
|
line.product_uom_id = uom_litre
|
||||||
|
line.product_qty = 3
|
||||||
|
with bom.bom_line_ids.new() as line:
|
||||||
|
line.product_id = crumble
|
||||||
|
line.product_uom_id = uom_kg
|
||||||
|
line.product_qty = 5.4
|
||||||
|
|
||||||
|
workcenter_2 = self.env['mrp.workcenter'].create({
|
||||||
|
'name': 'cake mounting',
|
||||||
|
'costs_hour': 20,
|
||||||
|
'time_start': 10,
|
||||||
|
'time_stop': 15
|
||||||
|
})
|
||||||
|
|
||||||
|
routing_form = Form(self.env['mrp.routing'])
|
||||||
|
routing_form.name = "Cheese cake process"
|
||||||
|
routing_cheese = routing_form.save()
|
||||||
|
|
||||||
|
with Form(routing_cheese) as routing:
|
||||||
|
with routing.operation_ids.new() as operation:
|
||||||
|
operation.workcenter_id = workcenter
|
||||||
|
operation.name = 'Mix cheese and crumble'
|
||||||
|
operation.time_cycle_manual = 10
|
||||||
|
with routing.operation_ids.new() as operation:
|
||||||
|
operation.workcenter_id = workcenter_2
|
||||||
|
operation.name = 'Cake mounting'
|
||||||
|
operation.time_cycle_manual = 5
|
||||||
|
|
||||||
|
bom_cheese_cake.routing_id = routing_cheese.id
|
||||||
|
|
||||||
|
# TEST CHEESE BOM STRUCTURE VALUE WITH BOM QUANTITY
|
||||||
|
report_values = self.env['report.mrp.report_bom_structure']._get_report_data(bom_id=bom_cheese_cake.id, searchQty=60, searchVariant=False)
|
||||||
|
self.assertEqual(report_values['lines']['operations_time'], 40.0, 'Operation time should be the same for 1 unit or for the batch')
|
||||||
|
# Operation cost is the sum of operation line.
|
||||||
|
operation_cost = float_round(10 / 60 * 10, precision_digits=2) + float_round(30 / 60 * 20, precision_digits=2)
|
||||||
|
self.assertEqual(float_compare(report_values['lines']['operations_cost'], operation_cost, precision_digits=2), 0)
|
||||||
|
|
||||||
|
for component_line in report_values['lines']['components']:
|
||||||
|
# standard price * bom line quantity * current quantity / bom finished product quantity
|
||||||
|
if component_line['prod_id'] == cream.id:
|
||||||
|
# 3 liter of cream at 5.17$ for 60 unit of cheese cake -> 15.51$
|
||||||
|
self.assertEqual(float_compare(component_line['total'], (3 * 5.17), precision_digits=2), 0)
|
||||||
|
if component_line['prod_id'] == crumble.id:
|
||||||
|
# 5.4 kg of crumble at the cost of a batch.
|
||||||
|
crumble_cost = self.env['report.mrp.report_bom_structure']._get_report_data(bom_id=bom_crumble.id, searchQty=5.4, searchVariant=False)['lines']['total']
|
||||||
|
self.assertEqual(float_compare(component_line['total'], crumble_cost, precision_digits=2), 0)
|
||||||
|
# total price = 15.51 + crumble_cost + operation_cost(10 + 1.67 = 11.67) = 27.18 + crumble_cost
|
||||||
|
self.assertEqual(float_compare(report_values['lines']['total'], 27.18 + crumble_cost, precision_digits=2), 0, 'Product Bom Price is not correct')
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||||
|
|
||||||
|
from odoo.osv import expression
|
||||||
from odoo import api, fields, models, _
|
from odoo import api, fields, models, _
|
||||||
from odoo.addons import decimal_precision as dp
|
from odoo.addons import decimal_precision as dp
|
||||||
from odoo.exceptions import UserError, ValidationError
|
from odoo.exceptions import UserError, ValidationError
|
||||||
@@ -108,7 +109,8 @@ class ProductAttributeLine(models.Model):
|
|||||||
# search on a m2o and one on a m2m, probably this will quickly become
|
# search on a m2o and one on a m2m, probably this will quickly become
|
||||||
# difficult to compute - check if performance optimization is required
|
# difficult to compute - check if performance optimization is required
|
||||||
if name and operator in ('=', 'ilike', '=ilike', 'like', '=like'):
|
if name and operator in ('=', 'ilike', '=ilike', 'like', '=like'):
|
||||||
args = expression.AND([['|', ('attribute_id', operator, name), ('value_ids', operator, name)], args])
|
args = args or []
|
||||||
attribute_ids = self._search(args, limit=limit, access_rights_uid=name_get_uid)
|
domain = ['|', ('attribute_id', operator, name), ('value_ids', operator, name)]
|
||||||
|
attribute_ids = self._search(expression.AND([domain, args]), limit=limit, access_rights_uid=name_get_uid)
|
||||||
return self.browse(attribute_ids).name_get()
|
return self.browse(attribute_ids).name_get()
|
||||||
return super(ProductAttributeLine, self)._name_search(name=name, args=args, operator=operator, limit=limit, name_get_uid=name_get_uid)
|
return super(ProductAttributeLine, self)._name_search(name=name, args=args, operator=operator, limit=limit, name_get_uid=name_get_uid)
|
||||||
|
|||||||
@@ -310,7 +310,7 @@ class PurchaseOrder(models.Model):
|
|||||||
|
|
||||||
self.partner_id = partner.id
|
self.partner_id = partner.id
|
||||||
self.fiscal_position_id = fpos.id
|
self.fiscal_position_id = fpos.id
|
||||||
self.payment_term_id = payment_term.id,
|
self.payment_term_id = payment_term.id
|
||||||
self.company_id = requisition.company_id.id
|
self.company_id = requisition.company_id.id
|
||||||
self.currency_id = requisition.currency_id.id
|
self.currency_id = requisition.currency_id.id
|
||||||
if not self.origin or requisition.name not in self.origin.split(', '):
|
if not self.origin or requisition.name not in self.origin.split(', '):
|
||||||
|
|||||||
@@ -359,6 +359,9 @@ class WebsiteSale(http.Controller):
|
|||||||
revive: Revival method when abandoned cart. Can be 'merge' or 'squash'
|
revive: Revival method when abandoned cart. Can be 'merge' or 'squash'
|
||||||
"""
|
"""
|
||||||
order = request.website.sale_get_order()
|
order = request.website.sale_get_order()
|
||||||
|
if order and order.state != 'draft':
|
||||||
|
request.session['sale_order_id'] = None
|
||||||
|
order = request.website.sale_get_order()
|
||||||
values = {}
|
values = {}
|
||||||
if access_token:
|
if access_token:
|
||||||
abandoned_order = request.env['sale.order'].sudo().search([('access_token', '=', access_token)], limit=1)
|
abandoned_order = request.env['sale.order'].sudo().search([('access_token', '=', access_token)], limit=1)
|
||||||
@@ -402,7 +405,7 @@ class WebsiteSale(http.Controller):
|
|||||||
|
|
||||||
@http.route(['/shop/cart/update'], type='http', auth="public", methods=['POST'], website=True, csrf=False)
|
@http.route(['/shop/cart/update'], type='http', auth="public", methods=['POST'], website=True, csrf=False)
|
||||||
def cart_update(self, product_id, add_qty=1, set_qty=0, **kw):
|
def cart_update(self, product_id, add_qty=1, set_qty=0, **kw):
|
||||||
sale_order = request.website.sale_get_order(force_create=1)
|
sale_order = request.website.sale_get_order(force_create=True)
|
||||||
if sale_order.state != 'draft':
|
if sale_order.state != 'draft':
|
||||||
request.session['sale_order_id'] = None
|
request.session['sale_order_id'] = None
|
||||||
sale_order = request.website.sale_get_order(force_create=True)
|
sale_order = request.website.sale_get_order(force_create=True)
|
||||||
|
|||||||
@@ -58,9 +58,14 @@ class WebsiteSaleDelivery(WebsiteSale):
|
|||||||
def _update_website_sale_delivery(self, **post):
|
def _update_website_sale_delivery(self, **post):
|
||||||
order = request.website.sale_get_order()
|
order = request.website.sale_get_order()
|
||||||
carrier_id = int(post['carrier_id'])
|
carrier_id = int(post['carrier_id'])
|
||||||
currency = order.currency_id
|
|
||||||
if order:
|
if order:
|
||||||
order._check_carrier_quotation(force_carrier_id=carrier_id)
|
order._check_carrier_quotation(force_carrier_id=carrier_id)
|
||||||
|
return self._update_website_sale_delivery_return(order, **post)
|
||||||
|
|
||||||
|
def _update_website_sale_delivery_return(self, order, **post):
|
||||||
|
carrier_id = int(post['carrier_id'])
|
||||||
|
currency = order.currency_id
|
||||||
|
if order:
|
||||||
return {'status': order.delivery_rating_success,
|
return {'status': order.delivery_rating_success,
|
||||||
'error_message': order.delivery_message,
|
'error_message': order.delivery_message,
|
||||||
'carrier_id': carrier_id,
|
'carrier_id': carrier_id,
|
||||||
|
|||||||
@@ -19,7 +19,10 @@ class WebsiteSaleOptions(WebsiteSale):
|
|||||||
if lang:
|
if lang:
|
||||||
request.website = request.website.with_context(lang=lang)
|
request.website = request.website.with_context(lang=lang)
|
||||||
|
|
||||||
order = request.website.sale_get_order(force_create=1)
|
order = request.website.sale_get_order(force_create=True)
|
||||||
|
if order.state != 'draft':
|
||||||
|
request.session['sale_order_id'] = None
|
||||||
|
order = request.website.sale_get_order(force_create=True)
|
||||||
product = request.env['product.product'].browse(int(product_id))
|
product = request.env['product.product'].browse(int(product_id))
|
||||||
|
|
||||||
option_ids = product.optional_product_ids.mapped('product_variant_ids').ids
|
option_ids = product.optional_product_ids.mapped('product_variant_ids').ids
|
||||||
|
|||||||
+17
-12
@@ -59,6 +59,14 @@ def memory_info(process):
|
|||||||
pmem = (getattr(process, 'memory_info', None) or process.get_memory_info)()
|
pmem = (getattr(process, 'memory_info', None) or process.get_memory_info)()
|
||||||
return (pmem.rss, pmem.vms)
|
return (pmem.rss, pmem.vms)
|
||||||
|
|
||||||
|
def empty_pipe(fd):
|
||||||
|
try:
|
||||||
|
while os.read(fd, 1):
|
||||||
|
pass
|
||||||
|
except OSError as e:
|
||||||
|
if e.errno not in [errno.EAGAIN]:
|
||||||
|
raise
|
||||||
|
|
||||||
#----------------------------------------------------------
|
#----------------------------------------------------------
|
||||||
# Werkzeug WSGI servers patched
|
# Werkzeug WSGI servers patched
|
||||||
#----------------------------------------------------------
|
#----------------------------------------------------------
|
||||||
@@ -562,13 +570,7 @@ class PreforkServer(CommonServer):
|
|||||||
for fd in ready[0]:
|
for fd in ready[0]:
|
||||||
if fd in fds:
|
if fd in fds:
|
||||||
fds[fd].watchdog_time = time.time()
|
fds[fd].watchdog_time = time.time()
|
||||||
try:
|
empty_pipe(fd)
|
||||||
# empty pipe
|
|
||||||
while os.read(fd, 1):
|
|
||||||
pass
|
|
||||||
except OSError as e:
|
|
||||||
if e.errno not in [errno.EAGAIN]:
|
|
||||||
raise
|
|
||||||
except select.error as e:
|
except select.error as e:
|
||||||
if e.args[0] not in [errno.EINTR]:
|
if e.args[0] not in [errno.EINTR]:
|
||||||
raise
|
raise
|
||||||
@@ -659,6 +661,7 @@ class Worker(object):
|
|||||||
self.watchdog_time = time.time()
|
self.watchdog_time = time.time()
|
||||||
self.watchdog_pipe = multi.pipe_new()
|
self.watchdog_pipe = multi.pipe_new()
|
||||||
self.eintr_pipe = multi.pipe_new()
|
self.eintr_pipe = multi.pipe_new()
|
||||||
|
self.wakeup_fd_r, self.wakeup_fd_w = self.eintr_pipe
|
||||||
# Can be set to None if no watchdog is desired.
|
# Can be set to None if no watchdog is desired.
|
||||||
self.watchdog_timeout = multi.timeout
|
self.watchdog_timeout = multi.timeout
|
||||||
self.ppid = os.getpid()
|
self.ppid = os.getpid()
|
||||||
@@ -682,8 +685,9 @@ class Worker(object):
|
|||||||
|
|
||||||
def sleep(self):
|
def sleep(self):
|
||||||
try:
|
try:
|
||||||
wakeup_fd = self.eintr_pipe[0]
|
select.select([self.multi.socket, self.wakeup_fd_r], [], [], self.multi.beat)
|
||||||
select.select([self.multi.socket, wakeup_fd], [], [], self.multi.beat)
|
# clear wakeup pipe if we were interrupted
|
||||||
|
empty_pipe(self.wakeup_fd_r)
|
||||||
except select.error as e:
|
except select.error as e:
|
||||||
if e.args[0] not in [errno.EINTR]:
|
if e.args[0] not in [errno.EINTR]:
|
||||||
raise
|
raise
|
||||||
@@ -737,7 +741,7 @@ class Worker(object):
|
|||||||
signal.signal(signal.SIGINT, self.signal_handler)
|
signal.signal(signal.SIGINT, self.signal_handler)
|
||||||
signal.signal(signal.SIGTERM, signal.SIG_DFL)
|
signal.signal(signal.SIGTERM, signal.SIG_DFL)
|
||||||
signal.signal(signal.SIGCHLD, signal.SIG_DFL)
|
signal.signal(signal.SIGCHLD, signal.SIG_DFL)
|
||||||
signal.set_wakeup_fd(self.eintr_pipe[1])
|
signal.set_wakeup_fd(self.wakeup_fd_w)
|
||||||
|
|
||||||
def stop(self):
|
def stop(self):
|
||||||
pass
|
pass
|
||||||
@@ -809,8 +813,9 @@ class WorkerCron(Worker):
|
|||||||
|
|
||||||
# simulate interruptible sleep with select(wakeup_fd, timeout)
|
# simulate interruptible sleep with select(wakeup_fd, timeout)
|
||||||
try:
|
try:
|
||||||
wakeup_fd = self.eintr_pipe[0]
|
select.select([self.wakeup_fd_r], [], [], interval)
|
||||||
select.select([wakeup_fd], [], [], interval)
|
# clear wakeup pipe if we were interrupted
|
||||||
|
empty_pipe(self.wakeup_fd_r)
|
||||||
except select.error as e:
|
except select.error as e:
|
||||||
if e.args[0] != errno.EINTR:
|
if e.args[0] != errno.EINTR:
|
||||||
raise
|
raise
|
||||||
|
|||||||
Reference in New Issue
Block a user