[MERGE] forward port branch saas-11.4 up to dc35de9d30

This commit is contained in:
Christophe Simonis
2018-11-06 14:12:51 +01:00
10 changed files with 296 additions and 28 deletions
+3 -5
View File
@@ -423,7 +423,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.remove_move_reconcile()
#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 |= 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
@@ -1073,10 +1073,8 @@ class AccountMoveLine(models.Model):
# 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:
vals['currency_id'] = account.currency_id.id
ctx = {}
if 'date' in vals:
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()))
date = vals.get('date') or vals.get('date_maturity') or fields.Date.today()
vals['amount_currency'] = account.company_id.currency_id._convert(amount, account.currency_id, account.company_id, date)
#Toggle the 'tax_exigible' field to False in case it is not yet given and the tax in 'tax_line_id' or one of
#the 'tax_ids' is a cash based tax.
+14 -5
View File
@@ -139,17 +139,18 @@ class ReportBomStructure(models.AbstractModel):
continue
price = line.product_id.uom_id._compute_price(line.product_id.standard_price, line.product_uom_id) * line_quantity
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)
else:
sub_total = price
sub_total = self.env.user.company_id.currency_id.round(sub_total)
components.append({
'prod_id': line.product_id.id,
'prod_name': line.product_id.display_name,
'code': line.child_bom_id and self._get_bom_reference(line.child_bom_id) or '',
'prod_qty': line_quantity,
'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,
'line_id': line.id,
'level': level or 0,
@@ -175,14 +176,21 @@ class ReportBomStructure(models.AbstractModel):
'operation': operation,
'name': operation.name + ' - ' + operation.workcenter_id.name,
'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
def _get_price(self, bom, factor):
price = 0
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])
for line in bom.bom_line_ids:
@@ -192,7 +200,8 @@ class ReportBomStructure(models.AbstractModel):
price += sub_price
else:
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
def _get_pdf_line(self, bom_id, product_id=False, qty=1, child_bom_ids=[], unfolded=False):
+189
View File
@@ -3,6 +3,8 @@
from odoo import exceptions
from odoo.addons.mrp.tests.common import TestMrpCommon
from odoo.tests import Form
from odoo.tools import float_compare, float_round
class TestBoM(TestMrpCommon):
@@ -228,3 +230,190 @@ class TestBoM(TestMrpCommon):
})
# Check consumed materials in production order.
self.assertEqual(mrp_order.move_raw_ids.mapped('product_id').ids, consu_product_ids)
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')
+4 -2
View File
@@ -1,6 +1,7 @@
# -*- coding: utf-8 -*-
# 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.addons import decimal_precision as dp
from odoo.exceptions import UserError, ValidationError
@@ -145,8 +146,9 @@ class ProductTemplateAttributeLine(models.Model):
# search on a m2o and one on a m2m, probably this will quickly become
# difficult to compute - check if performance optimization is required
if name and operator in ('=', 'ilike', '=ilike', 'like', '=like'):
args = expression.AND([['|', ('attribute_id', operator, name), ('value_ids', operator, name)], args])
attribute_ids = self._search(args, limit=limit, access_rights_uid=name_get_uid)
args = args or []
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 super(ProductTemplateAttributeLine, self)._name_search(name=name, args=args, operator=operator, limit=limit, name_get_uid=name_get_uid)
@@ -316,7 +316,7 @@ class PurchaseOrder(models.Model):
self.partner_id = partner.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.currency_id = requisition.currency_id.id
if not self.origin or requisition.name not in self.origin.split(', '):
@@ -1106,6 +1106,13 @@ var FieldX2Many = AbstractField.extend({
}
return def.then(function () {
self.pager.updateState({ size: self.value.count });
var newEval = self._evalColumnInvisibleFields();
if (!_.isEqual(self.currentColInvisibleFields, newEval)) {
self.currentColInvisibleFields = newEval;
self.renderer.updateState(self.value, {
columnInvisibleFields: self.currentColInvisibleFields,
});
}
});
},
/**
@@ -13588,6 +13588,53 @@ QUnit.module('relational_fields', {
form.destroy();
});
QUnit.test('one2many column visiblity depends on onchange of parent field', function (assert) {
assert.expect(3);
this.data.partner.records[0].p = [2];
this.data.partner.records[0].bar = false;
this.data.partner.onchanges.p = function (obj) {
// set bar to true when line is added
if (obj.p.length > 1 && obj.p[1][2].foo === 'New line') {
obj.bar = true;
}
};
var form = createView({
View: FormView,
model: 'partner',
data: this.data,
arch:'<form>' +
'<field name="bar"/>' +
'<field name="p">' +
'<tree editable="bottom">' +
'<field name="foo"/>' +
'<field name="int_field" attrs="{\'column_invisible\': [(\'parent.bar\', \'=\', False)]}"/>' +
'</tree>' +
'</field>' +
'</form>',
res_id: 1,
});
// bar is false so there should be 1 column
assert.strictEqual(form.$('th').length, 1,
"should be only 1 column ('foo') in the one2many");
assert.strictEqual(form.$('.o_list_view .o_data_row').length, 1, "should contain one row");
form.$buttons.find('.o_form_button_edit').click();
// add a new o2m record
form.$('.o_field_x2many_list_row_add a').click();
form.$('.o_field_one2many input:first').focus();
form.$('.o_field_one2many input:first').val('New line').trigger('input');
form.$el.click(); // click outside to save a line
assert.strictEqual(form.$('th').length, 2, "should be 2 columns('foo' + 'int_field')");
form.destroy();
});
QUnit.test('one2many column_invisible on view not inline', function (assert) {
assert.expect(4);
+8 -2
View File
@@ -349,6 +349,9 @@ class WebsiteSale(ProductConfiguratorController):
revive: Revival method when abandoned cart. Can be 'merge' or 'squash'
"""
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 = {}
if access_token:
abandoned_order = request.env['sale.order'].sudo().search([('access_token', '=', access_token)], limit=1)
@@ -392,7 +395,7 @@ class WebsiteSale(ProductConfiguratorController):
@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):
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':
request.session['sale_order_id'] = None
sale_order = request.website.sale_get_order(force_create=True)
@@ -1097,7 +1100,10 @@ class WebsiteSale(ProductConfiguratorController):
if 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)
optional_product_ids = []
for k, v in kw.items():
if "optional-product-" in k and int(kw.get(k.replace("product", "add"))):
@@ -58,9 +58,14 @@ class WebsiteSaleDelivery(WebsiteSale):
def _update_website_sale_delivery(self, **post):
order = request.website.sale_get_order()
carrier_id = int(post['carrier_id'])
currency = order.currency_id
if order:
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,
'error_message': order.delivery_message,
'carrier_id': carrier_id,
+17 -12
View File
@@ -72,6 +72,14 @@ def set_limit_memory_hard():
soft, hard = resource.getrlimit(rlimit)
resource.setrlimit(rlimit, (config['limit_memory_hard'], hard))
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
#----------------------------------------------------------
@@ -672,13 +680,7 @@ class PreforkServer(CommonServer):
for fd in ready[0]:
if fd in fds:
fds[fd].watchdog_time = time.time()
try:
# empty pipe
while os.read(fd, 1):
pass
except OSError as e:
if e.errno not in [errno.EAGAIN]:
raise
empty_pipe(fd)
except select.error as e:
if e.args[0] not in [errno.EINTR]:
raise
@@ -769,6 +771,7 @@ class Worker(object):
self.watchdog_time = time.time()
self.watchdog_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.
self.watchdog_timeout = multi.timeout
self.ppid = os.getpid()
@@ -792,8 +795,9 @@ class Worker(object):
def sleep(self):
try:
wakeup_fd = self.eintr_pipe[0]
select.select([self.multi.socket, wakeup_fd], [], [], self.multi.beat)
select.select([self.multi.socket, self.wakeup_fd_r], [], [], self.multi.beat)
# clear wakeup pipe if we were interrupted
empty_pipe(self.wakeup_fd_r)
except select.error as e:
if e.args[0] not in [errno.EINTR]:
raise
@@ -845,7 +849,7 @@ class Worker(object):
signal.signal(signal.SIGINT, self.signal_handler)
signal.signal(signal.SIGTERM, 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):
pass
@@ -917,8 +921,9 @@ class WorkerCron(Worker):
# simulate interruptible sleep with select(wakeup_fd, timeout)
try:
wakeup_fd = self.eintr_pipe[0]
select.select([wakeup_fd], [], [], interval)
select.select([self.wakeup_fd_r], [], [], interval)
# clear wakeup pipe if we were interrupted
empty_pipe(self.wakeup_fd_r)
except select.error as e:
if e.args[0] != errno.EINTR:
raise