[MERGE] forward port branch saas-11.2 up to 55a14f9ce8

This commit is contained in:
Christophe Simonis
2018-03-27 19:41:30 +02:00
19 changed files with 185 additions and 37 deletions
@@ -219,7 +219,7 @@ class account_journal(models.Model):
data as its first element, and the arguments dictionary to use to run
it as its second.
"""
return ("""SELECT state, residual_signed as amount_total, currency_id AS currency
return ("""SELECT state, residual_signed as amount_total, currency_id AS currency, type
FROM account_invoice
WHERE journal_id = %(journal_id)s AND state = 'open';""", {'journal_id':self.id})
@@ -229,7 +229,7 @@ class account_journal(models.Model):
gather the bills in draft state data, and the arguments
dictionary to use to run it as its second.
"""
return ("""SELECT state, amount_total, currency_id AS currency
return ("""SELECT state, amount_total, currency_id AS currency, type
FROM account_invoice
WHERE journal_id = %(journal_id)s AND state = 'draft';""", {'journal_id':self.id})
@@ -242,7 +242,9 @@ class account_journal(models.Model):
for result in results_dict:
cur = self.env['res.currency'].browse(result.get('currency'))
rslt_count += 1
rslt_sum += cur.compute(result.get('amount_total'), target_currency)
type_factor = result.get('type') in ('in_refund', 'out_refund') and -1 or 1
rslt_sum += type_factor * cur.compute(result.get('amount_total'), target_currency)
return (rslt_count, rslt_sum)
@api.multi
+11
View File
@@ -299,6 +299,17 @@ class MrpProduction(models.Model):
self.location_src_id = self.picking_type_id.default_location_src_id.id or location.id
self.location_dest_id = self.picking_type_id.default_location_dest_id.id or location.id
@api.multi
def write (self, vals):
res = super(MrpProduction, self).write(vals)
if 'date_planned_start' in vals:
moves = (self.mapped('move_raw_ids') + self.mapped('move_finished_ids')).filtered(
lambda r: r.state not in ['done', 'cancel'])
moves.write({
'date_expected': vals['date_planned_start'],
})
return res
@api.model
def create(self, values):
if not values.get('name', False) or values['name'] == _('New'):
+2 -2
View File
@@ -137,12 +137,12 @@ class StockMove(models.Model):
If you want to cancel this MO, please change the consumed quantities to 0.'))
return super(StockMove, self)._action_cancel()
def _action_confirm(self, merge=True):
def _action_confirm(self, merge=True, merge_into=False):
moves = self.env['stock.move']
for move in self:
moves |= move.action_explode()
# we go further with the list of ids potentially changed by action_explode
return super(StockMove, moves)._action_confirm(merge=merge)
return super(StockMove, moves)._action_confirm(merge=merge, merge_into=merge_into)
def action_explode(self):
""" Explodes pickings """
@@ -155,13 +155,13 @@ class AuthorizeAPI():
billTo = etree.SubElement(payment_profile, "billTo")
etree.SubElement(billTo, "address").text = (partner.street or '' + (partner.street2 if partner.street2 else '')) or None
missing_fields = [partner._fields[field].string for field in ['city', 'country_id', 'zip'] if not partner[field]]
missing_fields = [partner._fields[field].string for field in ['city', 'country_id'] if not partner[field]]
if missing_fields:
raise ValidationError({'missing_fields': missing_fields})
etree.SubElement(billTo, "city").text = partner.city
etree.SubElement(billTo, "state").text = partner.state_id.name or None
etree.SubElement(billTo, "zip").text = partner.zip
etree.SubElement(billTo, "zip").text = partner.zip or ''
etree.SubElement(billTo, "country").text = partner.country_id.name or None
payment = etree.SubElement(payment_profile, "payment")
creditCard = etree.SubElement(payment, "creditCard")
@@ -2,7 +2,7 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import api, fields, models, _
from odoo.tools.float_utils import float_round
from odoo.tools.float_utils import float_round, float_is_zero
class AccountInvoice(models.Model):
@@ -95,8 +95,11 @@ class AccountInvoice(models.Model):
if index+1 != len(no_zero_timesheet_revenue):
price_subtotal_inv = invoice_line.currency_id.compute(invoice_line.price_subtotal, timesheet_line.currency_id)
price_subtotal_sol = timesheet_line.so_line.currency_id.compute(timesheet_line.so_line.price_subtotal, timesheet_line.currency_id)
line_revenue = timesheet_line.timesheet_revenue * price_subtotal_inv / price_subtotal_sol
total_revenue_per_currency[timesheet_line.currency_id.id] += line_revenue
if not float_is_zero(price_subtotal_sol, precision_rounding=timesheet_line.currency_id.rounding):
line_revenue = timesheet_line.timesheet_revenue * price_subtotal_inv / price_subtotal_sol
total_revenue_per_currency[timesheet_line.currency_id.id] += line_revenue
else:
total_revenue_per_currency[timesheet_line.currency_id.id] += timesheet_line.timesheet_revenue
else: # last line: add the difference to avoid rounding problem
last_price_subtotal_inv = invoice_line.currency_id.compute(invoice_line.price_subtotal, timesheet_line.currency_id)
total_revenue = sum([self.env['res.currency'].browse(currency_id).compute(amount, timesheet_line.currency_id) for currency_id, amount in total_revenue_per_currency.items()])
+22 -9
View File
@@ -542,7 +542,7 @@ class StockMove(models.Model):
move.product_uom.id, move.restrict_partner_id.id, move.scrapped, move.origin_returned_move_id.id
]
def _merge_moves(self):
def _merge_moves(self, merge_into=False):
""" This method will, for each move in `self`, go up in their linked picking and try to
find in their existing moves a candidate into which we can merge the move.
:return: Recordset of moves passed to this method. If some of the passed moves were merged
@@ -550,12 +550,19 @@ class StockMove(models.Model):
"""
distinct_fields = self._prepare_merge_moves_distinct_fields()
candidate_moves_list = []
if not merge_into:
for picking in self.mapped('picking_id'):
candidate_moves_list.append(picking.move_lines)
else:
candidate_moves_list.append(merge_into | self)
# Move removed after merge
moves_to_unlink = self.env['stock.move']
moves_to_merge = []
for picking in self.mapped('picking_id'):
for candidate_moves in candidate_moves_list:
# First step find move to merge.
for k, g in groupby(sorted(picking.move_lines, key=self._prepare_merge_move_sort_method), key=itemgetter(*distinct_fields)):
for k, g in groupby(sorted(candidate_moves, key=self._prepare_merge_move_sort_method), key=itemgetter(*distinct_fields)):
moves = self.env['stock.move'].concat(*g).filtered(lambda m: m.state not in ('done', 'cancel', 'draft'))
# If we have multiple records we will merge then in a single one.
if len(moves) > 1:
@@ -709,7 +716,7 @@ class StockMove(models.Model):
'location_dest_id': self.location_dest_id.id,
}
def _action_confirm(self, merge=True):
def _action_confirm(self, merge=True, merge_into=False):
""" Confirms stock move or put it in waiting if it's linked to another move.
:param: merge: According to this boolean, a newly confirmed move will be merged
in another move of the same picking sharing its characteristics.
@@ -749,7 +756,7 @@ class StockMove(models.Model):
moves._assign_picking()
self._push_apply()
if merge:
return self._merge_moves()
return self._merge_moves(merge_into=merge_into)
return self
def _prepare_procurement_values(self):
@@ -985,7 +992,7 @@ class StockMove(models.Model):
The rationale for the creation of an extra move is the application of a potential push
rule that will handle the extra quantities.
"""
extra_move = self.env['stock.move']
extra_move = self
rounding = self.product_uom.rounding
# moves created after the picking is assigned do not have `product_uom_qty`, but we shouldn't create extra moves for them
if float_compare(self.quantity_done, self.product_uom_qty, precision_rounding=rounding) > 0:
@@ -995,7 +1002,11 @@ class StockMove(models.Model):
precision_rounding=self.product_uom.rounding,
rounding_method ='UP')
extra_move_vals = self._prepare_extra_move_vals(extra_move_quantity)
extra_move = self.copy(default=extra_move_vals)._action_confirm()
extra_move = self.copy(default=extra_move_vals)
if extra_move.picking_id:
extra_move = extra_move._action_confirm(merge_into=self)
else:
extra_move = extra_move._action_confirm()
# link it to some move lines. We don't need to do it for move since they should be merged.
if self.exists() and not self.picking_id:
@@ -1034,7 +1045,9 @@ class StockMove(models.Model):
for move in moves:
if move.state == 'cancel' or move.quantity_done <= 0:
continue
moves_todo |= move
# extra move will not be merged in mrp
if not move.picking_id:
moves_todo |= move
moves_todo |= move._create_extra_move()
# Split moves where necessary and move quants
@@ -1064,7 +1077,7 @@ class StockMove(models.Model):
.filtered(lambda p: p.quant_ids and len(p.quant_ids) > 1):
if len(result_package.quant_ids.mapped('location_id')) > 1:
raise UserError(_('You should not put the contents of a package in different locations.'))
picking = self and self[0].picking_id or False
picking = moves_todo and moves_todo[0].picking_id or False
moves_todo.write({'state': 'done', 'date': fields.Datetime.now()})
moves_todo.mapped('move_dest_ids')._action_assign()
+15 -4
View File
@@ -403,6 +403,7 @@ class StockMoveLine(models.Model):
ml_to_delete.unlink()
# Now, we can actually move the quant.
done_ml = self.env['stock.move.line']
for ml in self - ml_to_delete:
if ml.product_id.type == 'product':
Quant = self.env['stock.quant']
@@ -411,7 +412,7 @@ class StockMoveLine(models.Model):
# if this move line is force assigned, unreserve elsewhere if needed
if not ml.location_id.should_bypass_reservation() and float_compare(ml.qty_done, ml.product_qty, precision_rounding=rounding) > 0:
extra_qty = ml.qty_done - ml.product_qty
ml._free_reservation(ml.product_id, ml.location_id, extra_qty, lot_id=ml.lot_id, package_id=ml.package_id, owner_id=ml.owner_id)
ml._free_reservation(ml.product_id, ml.location_id, extra_qty, lot_id=ml.lot_id, package_id=ml.package_id, owner_id=ml.owner_id, ml_to_ignore=done_ml)
# unreserve what's been reserved
if not ml.location_id.should_bypass_reservation() and ml.product_id.type == 'product' and ml.product_qty:
try:
@@ -430,8 +431,12 @@ class StockMoveLine(models.Model):
Quant._update_available_quantity(ml.product_id, ml.location_id, -taken_from_untracked_qty, lot_id=False, package_id=ml.package_id, owner_id=ml.owner_id)
Quant._update_available_quantity(ml.product_id, ml.location_id, taken_from_untracked_qty, lot_id=ml.lot_id, package_id=ml.package_id, owner_id=ml.owner_id)
Quant._update_available_quantity(ml.product_id, ml.location_dest_id, quantity, lot_id=ml.lot_id, package_id=ml.result_package_id, owner_id=ml.owner_id, in_date=in_date)
done_ml |= ml
# Reset the reserved quantity as we just moved it to the destination location.
(self - ml_to_delete).with_context(bypass_reservation_update=True).write({'product_uom_qty': 0.00})
(self - ml_to_delete).with_context(bypass_reservation_update=True).write({
'product_uom_qty': 0.00,
'date': fields.Datetime.now(),
})
def _log_message(self, record, move, template, vals):
data = vals.copy()
@@ -449,13 +454,19 @@ class StockMoveLine(models.Model):
data['owner_name'] = self.env['res.partner'].browse(vals.get('owner_id')).name
record.message_post_with_view(template, values={'move': move, 'vals': dict(vals, **data)}, subtype_id=self.env.ref('mail.mt_note').id)
def _free_reservation(self, product_id, location_id, quantity, lot_id=None, package_id=None, owner_id=None):
def _free_reservation(self, product_id, location_id, quantity, lot_id=None, package_id=None, owner_id=None, ml_to_ignore=None):
""" When editing a done move line or validating one with some forced quantities, it is
possible to impact quants that were not reserved. It is therefore necessary to edit or
unlink the move lines that reserved a quantity now unavailable.
:param ml_to_ignore: recordset of `stock.move.line` that should NOT be unreserved
"""
self.ensure_one()
if ml_to_ignore is None:
ml_to_ignore = self.env['stock.move.line']
ml_to_ignore |= self
# Check the available quantity, with the `strict` kw set to `True`. If the available
# quantity is greather than the quantity now unavailable, there is nothing to do.
available_quantity = self.env['stock.quant']._get_available_quantity(
@@ -472,7 +483,7 @@ class StockMoveLine(models.Model):
('owner_id', '=', owner_id.id if owner_id else False),
('package_id', '=', package_id.id if package_id else False),
('product_qty', '>', 0.0),
('id', '!=', self.id),
('id', 'not in', ml_to_ignore.ids),
]
oudated_candidates = self.env['stock.move.line'].search(oudated_move_lines_domain)
+5
View File
@@ -300,11 +300,16 @@ class Picking(models.Model):
product_id = fields.Many2one('product.product', 'Product', related='move_lines.product_id')
show_operations = fields.Boolean(compute='_compute_show_operations')
show_lots_text = fields.Boolean(compute='_compute_show_lots_text')
has_tracking = fields.Boolean(compute='_compute_has_tracking')
_sql_constraints = [
('name_uniq', 'unique(name, company_id)', 'Reference must be unique per company!'),
]
def _compute_has_tracking(self):
for picking in self:
picking.has_tracking = any(m.has_tracking != 'none' for m in picking.move_lines)
@api.depends('picking_type_id.show_operations')
def _compute_show_operations(self):
for picking in self:
+7 -1
View File
@@ -90,6 +90,12 @@ class MrpStockReport(models.TransientModel):
ref = move_line.move_id.scrap_ids[0].name
return res_model, res_id, ref
@api.model
def _quantity_to_str(self, from_uom, to_uom, qty):
""" workaround to apply the float rounding logic of t-esc on data prepared server side """
qty = from_uom._compute_quantity(qty, to_uom, rounding_method='HALF-UP')
return self.env['ir.qweb.field.float'].value_to_html(qty, {'decimal_precision': 'Product Unit of Measure'})
def _get_usage(self, move_line):
usage = ''
if (move_line.location_id.usage == 'internal') and (move_line.location_dest_id.usage == 'internal'):
@@ -114,9 +120,9 @@ class MrpStockReport(models.TransientModel):
'model_id': move_line.id,
'model': 'stock.move.line',
'product_id': move_line.product_id.display_name,
'product_qty_uom': "%s %s" % (self._quantity_to_str(move_line.product_uom_id, move_line.product_id.uom_id, move_line.qty_done), move_line.product_id.uom_id.name),
'lot_name': move_line.lot_id.name,
'lot_id': move_line.lot_id.id,
'product_qty_uom': str(move_line.product_uom_id._compute_quantity(move_line.qty_done, move_line.product_id.uom_id, rounding_method='HALF-UP')) + ' ' + move_line.product_id.uom_id.name,
'location_source': move_line.location_id.name if not unfoldable or level == 1 else False,
'location_destination': move_line.location_dest_id.name if not unfoldable or level == 1 else False,
'reference_id': ref,
+35
View File
@@ -1474,6 +1474,41 @@ class StockMove(TransactionCase):
self.assertEqual(move1.state, 'confirmed')
self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product1, self.stock_location, lot_id=lot1), 0.0)
def test_use_unreserved_move_line_3(self):
""" Test the behavior of `_free_reservation` when ran on a recordset of move lines where
some are assigned and some are force assigned. `_free_reservation` should not use an
already processed move line when looking for a move line candidate to unreserve.
"""
self.env['stock.quant']._update_available_quantity(self.product1, self.stock_location, 1.0)
move1 = self.env['stock.move'].create({
'name': 'test_use_unreserved_move_line_3',
'location_id': self.stock_location.id,
'location_dest_id': self.customer_location.id,
'product_id': self.product1.id,
'product_uom': self.uom_unit.id,
'product_uom_qty': 3.0,
})
move1._action_confirm()
move1._action_assign()
move1.quantity_done = 1
# add a forced move line in `move1`
move1.write({'move_line_ids': [(0, 0, {
'product_id': self.product1.id,
'product_uom_id': self.uom_unit.id,
'qty_done': 2,
'product_uom_qty': 0,
'lot_id': False,
'package_id': False,
'result_package_id': False,
'location_id': move1.location_id.id,
'location_dest_id': move1.location_dest_id.id,
})]})
move1._action_done()
self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product1, self.customer_location), 3.0)
def test_edit_reserved_move_line_1(self):
""" Test that editing a stock move line linked to an untracked product correctly and
directly adapts the reservation. In this case, we edit the sublocation where we take the
+44
View File
@@ -1015,6 +1015,50 @@ class TestSinglePicking(TestStockCommon):
self.assertEqual(sum(move1.move_line_ids.mapped('qty_done')), 2.0)
self.assertEqual(move1.state, 'done')
def test_extra_move_4(self):
""" Create a picking with similar moves (created after
confirmation). Action done should propagate all the extra
quantity and only merge extra moves in their original moves.
"""
delivery = self.env['stock.picking'].create({
'location_id': self.stock_location,
'location_dest_id': self.customer_location,
'partner_id': self.partner_delta_id,
'picking_type_id': self.picking_type_out,
})
self.MoveObj.create({
'name': self.productA.name,
'product_id': self.productA.id,
'product_uom_qty': 5,
'quantity_done': 10,
'product_uom': self.productA.uom_id.id,
'picking_id': delivery.id,
'location_id': self.stock_location,
'location_dest_id': self.customer_location,
})
stock_location = self.env['stock.location'].browse(self.stock_location)
self.env['stock.quant']._update_available_quantity(self.productA, stock_location, 5)
delivery.action_confirm()
delivery.action_assign()
delivery.write({
'move_lines': [(0, 0, {
'name': self.productA.name,
'product_id': self.productA.id,
'product_uom_qty': 0,
'quantity_done': 10,
'state': 'assigned',
'product_uom': self.productA.uom_id.id,
'picking_id': delivery.id,
'location_id': self.stock_location,
'location_dest_id': self.customer_location,
})]
})
delivery.action_done()
self.assertEqual(len(delivery.move_lines), 2, 'Move should not be merged together')
for move in delivery.move_lines:
self.assertEqual(move.quantity_done, move.product_uom_qty, 'Initial demand should be equals to quantity done')
def test_recheck_availability_1(self):
""" Check the good behavior of check availability. I create a DO for 2 unit with
only one in stock. After the first check availability, I should have 1 reserved
+2 -1
View File
@@ -225,13 +225,14 @@
<sheet>
<div class="oe_button_box" name="button_box">
<field name="has_scrap_move" invisible="True"/>
<field name="has_tracking" invisible="True"/>
<button name="action_see_move_scrap" string="Scraps" type="object"
class="oe_stat_button" icon="fa-arrows-v"
attrs="{'invisible': [('has_scrap_move', '=', False)]}"/>
<button name="action_see_packages" string="Packages" type="object"
class="oe_stat_button" icon="fa-cubes"
attrs="{'invisible': [('has_packages', '=', False)]}"/>
<button name="%(action_stock_report)d" icon="fa-arrow-up" class="oe_stat_button" string="Traceability Report" type="action" states="done" groups="stock.group_production_lot"/>
<button name="%(action_stock_report)d" icon="fa-arrow-up" class="oe_stat_button" string="Traceability Report" type="action" attrs="{'invisible': ['|', ('state', '!=', 'done'), ('has_tracking', '=', False)]}" groups="stock.group_production_lot"/>
</div>
<h1 class="hidden-xs">
<field name="name" attrs="{'invisible': [('name','=','/')]}" readonly="1"/>
@@ -632,7 +632,8 @@ return AbstractModel.extend({
var date_start;
var date_stop;
var date_delay = evt[this.mapping.date_delay] || 1.0,
all_day = this.mapping.all_day ? evt[this.mapping.all_day] : false,
all_day = this.fields[this.mapping.date_start].type === 'date' ||
this.mapping.all_day && evt[this.mapping.all_day] || false,
the_title = '',
attendees = [];
@@ -656,8 +657,6 @@ return AbstractModel.extend({
if (this.mapping.all_day && evt[this.mapping.all_day]) {
date_stop.add(1, 'days');
}
var isAllDay = this.fields[this.mapping.date_start].type === 'date' ||
this.mapping.all_day && evt[this.mapping.all_day] || false;
var r = {
'record': evt,
'start': date_start,
@@ -665,7 +664,7 @@ return AbstractModel.extend({
'r_start': date_start,
'r_end': date_stop,
'title': the_title,
'allDay': isAllDay,
'allDay': all_day,
'id': evt.id,
'attendees':attendees,
};
@@ -1595,7 +1595,7 @@ QUnit.module('Views', {
});
QUnit.test('set event as all day when field is date', function (assert) {
assert.expect(1);
assert.expect(2);
this.data.event.records[0].start_date = "2016-12-14";
@@ -1619,9 +1619,16 @@ QUnit.module('Views', {
viewOptions: {
initialDate: initialDate,
},
session: {
getTZOffset: function () {
return -480;
}
},
});
assert.strictEqual(calendar.$('.fc-day-grid .fc-event-container').length, 1,
"should be one event in the all day row");
assert.strictEqual(calendar.model.data.data[0].r_start.date(), 14,
"the date should be 14");
calendar.destroy();
});
+5 -5
View File
@@ -70,15 +70,15 @@ class Http(models.AbstractModel):
@classmethod
def _add_dispatch_parameters(cls, func):
context = dict(request.context)
if not context.get('tz'):
context = {}
if not request.context.get('tz'):
context['tz'] = request.session.get('geoip', {}).get('time_zone')
request.website = request.env['website'].get_current_website() # can use `request.env` since auth methods are called
context['website_id'] = request.website.id
# bind modified context
request.context = context
# modify bound context
request.context = dict(request.context, **context)
super(Http, cls)._add_dispatch_parameters(func)
@@ -116,7 +116,7 @@ class Http(models.AbstractModel):
mypage = pages[0] if pages else False
_, ext = os.path.splitext(req_page)
if mypage:
return request.render(mypage.view_id.id, {
return request.render(mypage.get_view_identifier(), {
# 'path': req_page[1:],
'deletable': True,
'main_object': mypage,
+5
View File
@@ -658,6 +658,11 @@ class Page(models.Model):
item = self.search_read(domain, fields=['id', 'name', 'url', 'website_published', 'website_indexed', 'date_publish', 'menu_ids', 'is_homepage'], limit=1)
return item
@api.multi
def get_view_identifier(self):
""" Get identifier of this page view that may be used to render it """
return self.view_id.id
@api.model
def save_page_info(self, website_id, data):
website = self.env['website'].browse(website_id)
+1 -1
View File
@@ -176,7 +176,7 @@
<t t-set="additional_title">Shop</t>
<div id="wrap" class="js_sale">
<div class="oe_structure" />
<div t-attf-class="container oe_website_sale #{'oe_website_sale_search' if search else None}">
<div class="container oe_website_sale">
<div class="products_pager">
<div class="row" style="width: 100%;">
<t t-call="website_sale.search">
@@ -422,6 +422,7 @@ class IrMailServer(models.Model):
email_to = message['To']
email_cc = message['Cc']
email_bcc = message['Bcc']
del message['Bcc']
smtp_to_list = [
address
+6 -1
View File
@@ -458,13 +458,18 @@ class IrTranslation(models.Model):
continue
# remap existing translations on terms when possible
trans_src = record_trans.mapped('src')
for trans in record_trans:
if trans.src == trans.value:
discarded += trans
elif trans.src not in terms:
matches = get_close_matches(trans.src, terms, 1, 0.9)
if matches:
trans.write({'src': matches[0], 'state': trans.state})
if matches[0] in trans_src:
# there is already a translation for this term; discard this one
discarded += trans
else:
trans.write({'src': matches[0], 'state': trans.state})
else:
outdated += trans