Merge branch 'master-fatemi-05072018' into 'master-patch-july-2018'
Master fatemi 05072018 See merge request flectra-hq/flectra!91
This commit is contained in:
commit
d6834ffaca
@ -80,6 +80,8 @@ class MrpBom(models.Model):
|
||||
def onchange_product_tmpl_id(self):
|
||||
if self.product_tmpl_id:
|
||||
self.product_uom_id = self.product_tmpl_id.uom_id.id
|
||||
if self.product_id.product_tmpl_id != self.product_tmpl_id:
|
||||
self.product_id = False
|
||||
|
||||
@api.onchange('routing_id')
|
||||
def onchange_routing_id(self):
|
||||
|
@ -389,7 +389,7 @@ class MrpProduction(models.Model):
|
||||
source_location = routing.location_id
|
||||
else:
|
||||
source_location = self.location_src_id
|
||||
original_quantity = self.product_qty - self.qty_produced
|
||||
original_quantity = (self.product_qty - self.qty_produced) or 1.0
|
||||
data = {
|
||||
'sequence': bom_line.sequence,
|
||||
'name': self.name,
|
||||
@ -474,10 +474,16 @@ class MrpProduction(models.Model):
|
||||
@api.multi
|
||||
def _generate_workorders(self, exploded_boms):
|
||||
workorders = self.env['mrp.workorder']
|
||||
original_one = False
|
||||
for bom, bom_data in exploded_boms:
|
||||
# If the routing of the parent BoM and phantom BoM are the same, don't recreate work orders, but use one master routing
|
||||
if bom.routing_id.id and (not bom_data['parent_line'] or bom_data['parent_line'].bom_id.routing_id.id != bom.routing_id.id):
|
||||
workorders += self._workorders_create(bom, bom_data)
|
||||
temp_workorders = self._workorders_create(bom, bom_data)
|
||||
workorders += temp_workorders
|
||||
if temp_workorders: # In order to avoid two "ending work orders"
|
||||
if original_one:
|
||||
temp_workorders[-1].next_work_order_id = original_one
|
||||
original_one = temp_workorders[0]
|
||||
return workorders
|
||||
|
||||
def _workorders_create(self, bom, bom_data):
|
||||
@ -558,7 +564,7 @@ class MrpProduction(models.Model):
|
||||
order._cal_price(moves_to_do)
|
||||
moves_to_finish = order.move_finished_ids.filtered(lambda x: x.state not in ('done','cancel'))
|
||||
moves_to_finish._action_done()
|
||||
#order.action_assign()
|
||||
order.action_assign()
|
||||
consume_move_lines = moves_to_do.mapped('active_move_line_ids')
|
||||
for moveline in moves_to_finish.mapped('active_move_line_ids'):
|
||||
if moveline.product_id == order.product_id and moveline.move_id.has_tracking != 'none':
|
||||
|
@ -219,6 +219,7 @@ class MrpWorkorder(models.Model):
|
||||
'done_wo': False,
|
||||
'location_id': move.location_id.id,
|
||||
'location_dest_id': move.location_dest_id.id,
|
||||
'date': move.date,
|
||||
})
|
||||
qty_todo -= 1
|
||||
elif float_compare(qty_todo, 0.0, precision_rounding=rounding) < 0:
|
||||
@ -284,6 +285,18 @@ class MrpWorkorder(models.Model):
|
||||
self.final_lot_id = self.env['stock.production.lot'].search([('use_next_on_work_order_id', '=', self.id)],
|
||||
order='create_date, id', limit=1)
|
||||
|
||||
def _get_byproduct_move_line(self, by_product_move, quantity):
|
||||
return {
|
||||
'move_id': by_product_move.id,
|
||||
'product_id': by_product_move.product_id.id,
|
||||
'product_uom_qty': quantity,
|
||||
'product_uom_id': by_product_move.product_uom.id,
|
||||
'qty_done': quantity,
|
||||
'workorder_id': self.id,
|
||||
'location_id': by_product_move.location_id.id,
|
||||
'location_dest_id': by_product_move.location_dest_id.id,
|
||||
}
|
||||
|
||||
@api.multi
|
||||
def record_production(self):
|
||||
self.ensure_one()
|
||||
@ -339,33 +352,37 @@ class MrpWorkorder(models.Model):
|
||||
# If last work order, then post lots used
|
||||
# TODO: should be same as checking if for every workorder something has been done?
|
||||
if not self.next_work_order_id:
|
||||
production_moves = self.production_id.move_finished_ids.filtered(lambda x: (x.state not in ('done', 'cancel')))
|
||||
for production_move in production_moves:
|
||||
if production_move.product_id.id == self.production_id.product_id.id and production_move.has_tracking != 'none':
|
||||
move_line = production_move.move_line_ids.filtered(lambda x: x.lot_id.id == self.final_lot_id.id)
|
||||
if move_line:
|
||||
move_line.product_uom_qty += self.qty_producing
|
||||
else:
|
||||
move_line.create({'move_id': production_move.id,
|
||||
'product_id': production_move.product_id.id,
|
||||
'lot_id': self.final_lot_id.id,
|
||||
'product_uom_qty': self.qty_producing,
|
||||
'product_uom_id': production_move.product_uom.id,
|
||||
'qty_done': self.qty_producing,
|
||||
'workorder_id': self.id,
|
||||
'location_id': production_move.location_id.id,
|
||||
'location_dest_id': production_move.location_dest_id.id,
|
||||
})
|
||||
elif production_move.unit_factor:
|
||||
rounding = production_move.product_uom.rounding
|
||||
production_move.quantity_done += float_round(self.qty_producing * production_move.unit_factor, precision_rounding=rounding)
|
||||
production_move = self.production_id.move_finished_ids.filtered(
|
||||
lambda x: (x.product_id.id == self.production_id.product_id.id) and (x.state not in ('done', 'cancel')))
|
||||
if production_move.product_id.tracking != 'none':
|
||||
move_line = production_move.move_line_ids.filtered(lambda x: x.lot_id.id == self.final_lot_id.id)
|
||||
if move_line:
|
||||
move_line.product_uom_qty += self.qty_producing
|
||||
move_line.qty_done += self.qty_producing
|
||||
else:
|
||||
production_move.quantity_done += self.qty_producing
|
||||
move_line.create({'move_id': production_move.id,
|
||||
'product_id': production_move.product_id.id,
|
||||
'lot_id': self.final_lot_id.id,
|
||||
'product_uom_qty': self.qty_producing,
|
||||
'product_uom_id': production_move.product_uom.id,
|
||||
'qty_done': self.qty_producing,
|
||||
'workorder_id': self.id,
|
||||
'location_id': production_move.location_id.id,
|
||||
'location_dest_id': production_move.location_dest_id.id,
|
||||
})
|
||||
else:
|
||||
production_move.quantity_done += self.qty_producing
|
||||
|
||||
if not self.next_work_order_id:
|
||||
for by_product_move in self.production_id.move_finished_ids.filtered(lambda x: (x.product_id.id != self.production_id.product_id.id) and (x.state not in ('done', 'cancel'))):
|
||||
if by_product_move.has_tracking == 'none':
|
||||
by_product_move.quantity_done += self.qty_producing * by_product_move.unit_factor
|
||||
if by_product_move.has_tracking != 'serial':
|
||||
values = self._get_byproduct_move_line(by_product_move, self.qty_producing * by_product_move.unit_factor)
|
||||
self.env['stock.move.line'].create(values)
|
||||
elif by_product_move.has_tracking == 'serial':
|
||||
qty_todo = by_product_move.product_uom._compute_quantity(self.qty_producing * by_product_move.unit_factor, by_product_move.product_id.uom_id)
|
||||
for i in range(0, int(float_round(qty_todo, precision_digits=0))):
|
||||
values = self._get_byproduct_move_line(by_product_move, 1)
|
||||
self.env['stock.move.line'].create(values)
|
||||
|
||||
# Update workorder quantity produced
|
||||
self.qty_produced += self.qty_producing
|
||||
@ -395,7 +412,12 @@ class MrpWorkorder(models.Model):
|
||||
|
||||
@api.multi
|
||||
def button_start(self):
|
||||
# TDE CLEANME
|
||||
self.ensure_one()
|
||||
# As button_start is automatically called in the new view
|
||||
if self.state in ('done', 'cancel'):
|
||||
return True
|
||||
|
||||
# Need a loss in case of the real time exceeding the expected
|
||||
timeline = self.env['mrp.workcenter.productivity']
|
||||
if self.duration < self.duration_expected:
|
||||
loss_id = self.env['mrp.workcenter.productivity.loss'].search([('loss_type','=','productive')], limit=1)
|
||||
|
@ -73,6 +73,13 @@ class StockMove(models.Model):
|
||||
order_finished_lot_ids = fields.Many2many('stock.production.lot', compute='_compute_order_finished_lot_ids')
|
||||
finished_lots_exist = fields.Boolean('Finished Lots Exist', compute='_compute_order_finished_lot_ids')
|
||||
|
||||
def _unreserve_initial_demand(self, new_move):
|
||||
# If you were already putting stock.move.lots on the next one in the work order, transfer those to the new move
|
||||
self.filtered(lambda m: m.production_id or m.raw_material_production_id)\
|
||||
.mapped('move_line_ids')\
|
||||
.filtered(lambda ml: ml.qty_done == 0.0)\
|
||||
.write({'move_id': new_move, 'product_uom_qty': 0})
|
||||
|
||||
@api.depends('active_move_line_ids.qty_done', 'active_move_line_ids.product_uom_id')
|
||||
def _compute_done_quantity(self):
|
||||
super(StockMove, self)._compute_done_quantity()
|
||||
|
@ -29,12 +29,12 @@ class StockWarehouse(models.Model):
|
||||
return result
|
||||
|
||||
def _get_manufacture_route_id(self):
|
||||
manufacture_route_id = self.env.ref('mrp.route_warehouse0_manufacture').id
|
||||
if not manufacture_route_id:
|
||||
manufacture_route_id = self.env['stock.location.route'].search([('name', 'like', _('Manufacture'))], limit=1).id
|
||||
if not manufacture_route_id:
|
||||
manufacture_route = self.env.ref('mrp.route_warehouse0_manufacture', raise_if_not_found=False)
|
||||
if not manufacture_route:
|
||||
manufacture_route = self.env['stock.location.route'].search([('name', 'like', _('Manufacture'))], limit=1)
|
||||
if not manufacture_route:
|
||||
raise exceptions.UserError(_('Can\'t find any generic Manufacture route.'))
|
||||
return manufacture_route_id
|
||||
return manufacture_route.id
|
||||
|
||||
def _get_manufacture_pull_rules_values(self, route_values):
|
||||
if not self.manu_type_id:
|
||||
|
@ -36,7 +36,7 @@
|
||||
<span t-att-res-id="bom_line['product_id'].id" res-model="product.product" view-type="form" t-esc="bom_line['product_id'].name"/>
|
||||
</td>
|
||||
<td class="text-right">
|
||||
<span t-esc="bom_line['product_uom_qty']"/> <span t-esc="bom_line['product_uom'].name" groups="product.group_uom"/>
|
||||
<span t-esc="bom_line['product_uom_qty']" t-esc-options='{"widget": "float", "decimal_precision": "Product Unit of Measure"}'/> <span t-esc="bom_line['product_uom'].name" groups="product.group_uom"/>
|
||||
</td>
|
||||
<td class="text-right">
|
||||
<span t-esc="bom_line['price_unit']" t-options='{"widget": "monetary", "display_currency": currency}'/>
|
||||
|
@ -7,36 +7,49 @@ from flectra import api, models
|
||||
class BomStructureReport(models.AbstractModel):
|
||||
_name = 'report.mrp.mrp_bom_structure_report'
|
||||
|
||||
def get_children(self, object, level=0):
|
||||
@api.model
|
||||
def _get_child_vals(self, record, level, qty, uom):
|
||||
"""Get bom.line values.
|
||||
|
||||
:param record: mrp.bom.line record
|
||||
:param level: level of recursion
|
||||
:param qty: quantity of the product
|
||||
:param uom: unit of measurement of a product
|
||||
"""
|
||||
child = {
|
||||
'pname': record.product_id.name_get()[0][1],
|
||||
'pcode': record.product_id.default_code,
|
||||
'puom': record.product_uom_id,
|
||||
'uname': record.product_uom_id.name,
|
||||
'level': level,
|
||||
'code': record.bom_id.code,
|
||||
}
|
||||
qty_per_bom = record.bom_id.product_qty
|
||||
if uom:
|
||||
if uom != record.bom_id.product_uom_id:
|
||||
qty = uom._compute_quantity(qty, record.bom_id.product_uom_id)
|
||||
child['pqty'] = (record.product_qty * qty) / qty_per_bom
|
||||
else:
|
||||
# for the first case, the ponderation is right
|
||||
child['pqty'] = (record.product_qty * qty)
|
||||
return child
|
||||
|
||||
def get_children(self, records, level=0):
|
||||
result = []
|
||||
|
||||
def _get_rec(object, level, qty=1.0, uom=False):
|
||||
for l in object:
|
||||
res = {}
|
||||
res['pname'] = l.product_id.name_get()[0][1]
|
||||
res['pcode'] = l.product_id.default_code
|
||||
qty_per_bom = l.bom_id.product_qty
|
||||
if uom:
|
||||
if uom != l.bom_id.product_uom_id:
|
||||
qty = uom._compute_quantity(qty, l.bom_id.product_uom_id)
|
||||
res['pqty'] = (l.product_qty *qty)/ qty_per_bom
|
||||
else:
|
||||
#for the first case, the ponderation is right
|
||||
res['pqty'] = (l.product_qty *qty)
|
||||
res['puom'] = l.product_uom_id
|
||||
res['uname'] = l.product_uom_id.name
|
||||
res['level'] = level
|
||||
res['code'] = l.bom_id.code
|
||||
result.append(res)
|
||||
def _get_rec(records, level, qty=1.0, uom=False):
|
||||
for l in records:
|
||||
child = self._get_child_vals(l, level, qty, uom)
|
||||
result.append(child)
|
||||
if l.child_line_ids:
|
||||
if level < 6:
|
||||
level += 1
|
||||
_get_rec(l.child_line_ids, level, qty=res['pqty'], uom=res['puom'])
|
||||
_get_rec(l.child_line_ids, level, qty=child['pqty'], uom=child['puom'])
|
||||
if level > 0 and level < 6:
|
||||
level -= 1
|
||||
return result
|
||||
|
||||
children = _get_rec(object, level)
|
||||
children = _get_rec(records, level)
|
||||
|
||||
return children
|
||||
|
||||
|
@ -126,7 +126,7 @@
|
||||
<field name="state" invisible="1" force_save="1"/>
|
||||
<field name="product_uom_qty" string="To Consume"/>
|
||||
<field name="reserved_availability" attrs="{'invisible': [('is_done', '=', True)]}" string="Reserved"/>
|
||||
<field name="quantity_done" string="Consumed"/>
|
||||
<field name="quantity_done" string="Consumed" readonly="1"/>
|
||||
</tree>
|
||||
</field>
|
||||
</page>
|
||||
|
@ -177,4 +177,4 @@
|
||||
groups="group_mrp_routings"
|
||||
sequence="50"/>
|
||||
</data>
|
||||
</flectra>
|
||||
</flectra>
|
@ -190,18 +190,19 @@
|
||||
<label for="duration"/>
|
||||
<div>
|
||||
<button style="pointer-events: none;" class="oe_inline label label-default">
|
||||
<field name="duration" widget="mrp_time_counter"/>
|
||||
<field name="duration" widget="mrp_time_counter" help="Time the currently logged user spent on this workorder."/>
|
||||
</button>
|
||||
</div>
|
||||
</group>
|
||||
</group>
|
||||
<group>
|
||||
<field name="time_ids" nolabel="1">
|
||||
<field name="time_ids" nolabel="1" context="{'default_workcenter_id': workcenter_id}">
|
||||
<tree>
|
||||
<field name="date_start"/>
|
||||
<field name="date_end"/>
|
||||
<field name="duration" widget="float_time" sum="Total duration"/>
|
||||
<field name="user_id"/>
|
||||
<field name="workcenter_id" invisible="1"/>
|
||||
<field name="loss_id" string="Efficiency"/>
|
||||
</tree>
|
||||
<form>
|
||||
@ -213,6 +214,7 @@
|
||||
</group>
|
||||
<group>
|
||||
<field name="user_id"/>
|
||||
<field name="workcenter_id"/>
|
||||
<field name="loss_id"/>
|
||||
</group>
|
||||
</group>
|
||||
@ -404,6 +406,8 @@
|
||||
<field name="context">{'search_default_ready': True, 'search_default_progress': True}</field>
|
||||
<field name="help" type="html">
|
||||
<p class="oe_view_nocontent_create">
|
||||
Click to start a new work order.
|
||||
</p><p>
|
||||
Work Orders are operations to be processed at a Work Center to realize a
|
||||
Manufacturing Order. Work Orders are trigerred by Manufacturing Orders,
|
||||
they are based on the Routing defined on these ones.
|
||||
|
@ -22,7 +22,7 @@
|
||||
<label for="product_uom_qty"/>
|
||||
<div class="o_row">
|
||||
<span><field name="product_uom_qty" readonly="1" nolabel="1"/></span>
|
||||
<span><field name="product_uom" attrs="{'readonly': [('id', '!=', False)]}" nolabel="1"/></span>
|
||||
<span><field name="product_uom" readonly="1" force_save="1" nolabel="1"/></span>
|
||||
</div>
|
||||
<label for="quantity_done"/>
|
||||
<div class="o_row">
|
||||
@ -44,7 +44,7 @@
|
||||
<field name="finished_lots_exist" invisible="1"/>
|
||||
</group>
|
||||
</group>
|
||||
<field name="active_move_line_ids" attrs="{'readonly': [('is_locked', '=', True)], 'invisible': [('has_tracking', '=', 'none'), ('finished_lots_exist', '=', False)]}" context="{'default_workorder_id': workorder_id, 'default_product_uom_id': product_uom, 'default_product_id': product_id, 'default_location_id': location_id, 'default_location_dest_id': location_dest_id, 'default_production_id': production_id or raw_material_production_id}">
|
||||
<field name="active_move_line_ids" attrs="{'readonly': [('is_locked', '=', True)]}" context="{'default_workorder_id': workorder_id, 'default_product_uom_id': product_uom, 'default_product_id': product_id, 'default_location_id': location_id, 'default_location_dest_id': location_dest_id, 'default_production_id': production_id or raw_material_production_id}">
|
||||
<tree editable="bottom" decoration-success="product_qty==qty_done" decoration-danger="(product_qty > 0) and (qty_done>product_qty)">
|
||||
<field name="lot_id" attrs="{'column_invisible': [('parent.has_tracking', '=', 'none')]}" domain="[('product_id', '=', parent.product_id)]" context="{'default_product_id': parent.product_id}"/>
|
||||
<field name="lot_produced_id" options="{'no_open': True, 'no_create': True}" domain="[('id', 'in', parent.order_finished_lot_ids)]" invisible="not context.get('final_lots')"/>
|
||||
|
@ -43,7 +43,8 @@ class ChangeProductionQty(models.TransientModel):
|
||||
production = wizard.mo_id
|
||||
produced = sum(production.move_finished_ids.filtered(lambda m: m.product_id == production.product_id).mapped('quantity_done'))
|
||||
if wizard.product_qty < produced:
|
||||
raise UserError(_("You have already processed %d. Please input a quantity higher than %d ")%(produced, produced))
|
||||
format_qty = '%.{precision}f'.format(precision=precision)
|
||||
raise UserError(_("You have already processed %s. Please input a quantity higher than %s ") % (format_qty % produced, format_qty % produced))
|
||||
production.write({'product_qty': wizard.product_qty})
|
||||
done_moves = production.move_finished_ids.filtered(lambda x: x.state == 'done' and x.product_id == production.product_id)
|
||||
qty_produced = production.product_id.uom_id._compute_quantity(sum(done_moves.mapped('product_qty')), production.product_uom_id)
|
||||
|
@ -38,7 +38,8 @@ class MrpProductProduce(models.TransientModel):
|
||||
if 'produce_line_ids' in fields:
|
||||
lines = []
|
||||
for move in production.move_raw_ids.filtered(lambda x: (x.product_id.tracking != 'none') and x.state not in ('done', 'cancel') and x.bom_line_id):
|
||||
qty_to_consume = todo_quantity / move.bom_line_id.bom_id.product_qty * move.bom_line_id.product_qty
|
||||
qty_to_consume = float_round(todo_quantity / move.bom_line_id.bom_id.product_qty * move.bom_line_id.product_qty,
|
||||
precision_rounding=move.product_uom.rounding, rounding_method="UP")
|
||||
for move_line in move.move_line_ids:
|
||||
if float_compare(qty_to_consume, 0.0, precision_rounding=move.product_uom.rounding) <= 0:
|
||||
break
|
||||
@ -176,9 +177,9 @@ class MrpProductProduceLine(models.TransientModel):
|
||||
product_produce_id = fields.Many2one('mrp.product.produce')
|
||||
product_id = fields.Many2one('product.product', 'Product')
|
||||
lot_id = fields.Many2one('stock.production.lot', 'Lot')
|
||||
qty_to_consume = fields.Float('To Consume')
|
||||
qty_to_consume = fields.Float('To Consume', digits=dp.get_precision('Product Unit of Measure'))
|
||||
product_uom_id = fields.Many2one('product.uom', 'Unit of Measure')
|
||||
qty_done = fields.Float('Done')
|
||||
qty_done = fields.Float('Done', digits=dp.get_precision('Product Unit of Measure'))
|
||||
move_id = fields.Many2one('stock.move')
|
||||
|
||||
@api.onchange('lot_id')
|
||||
|
@ -207,9 +207,11 @@ class Repair(models.Model):
|
||||
def action_validate(self):
|
||||
self.ensure_one()
|
||||
precision = self.env['decimal.precision'].precision_get('Product Unit of Measure')
|
||||
available_qty = self.env['stock.quant']._get_available_quantity(self.product_id, self.location_id, self.lot_id, strict=True)
|
||||
if float_compare(available_qty, self.product_qty, precision_digits=precision) >= 0:
|
||||
return self.action_repair_confirm()
|
||||
available_qty_owner = self.env['stock.quant']._get_available_quantity(self.product_id, self.location_id, self.lot_id, owner_id=self.partner_id, strict=True)
|
||||
available_qty_noown = self.env['stock.quant']._get_available_quantity(self.product_id, self.location_id, self.lot_id, strict=True)
|
||||
for available_qty in [available_qty_owner, available_qty_noown]:
|
||||
if float_compare(available_qty, self.product_qty, precision_digits=precision) >= 0:
|
||||
return self.action_repair_confirm()
|
||||
else:
|
||||
return {
|
||||
'name': _('Insufficient Quantity'),
|
||||
@ -435,8 +437,15 @@ class Repair(models.Model):
|
||||
if self.filtered(lambda repair: not repair.repaired):
|
||||
raise UserError(_("Repair must be repaired in order to make the product moves."))
|
||||
res = {}
|
||||
precision = self.env['decimal.precision'].precision_get('Product Unit of Measure')
|
||||
Move = self.env['stock.move']
|
||||
for repair in self:
|
||||
# Try to create move with the appropriate owner
|
||||
owner_id = False
|
||||
available_qty_owner = self.env['stock.quant']._get_available_quantity(repair.product_id, repair.location_id, repair.lot_id, owner_id=repair.partner_id, strict=True)
|
||||
if float_compare(available_qty_owner, repair.product_qty, precision_digits=precision) >= 0:
|
||||
owner_id = repair.partner_id.id
|
||||
|
||||
moves = self.env['stock.move']
|
||||
for operation in repair.operations:
|
||||
move = Move.create({
|
||||
@ -454,6 +463,7 @@ class Repair(models.Model):
|
||||
'qty_done': operation.product_uom_qty,
|
||||
'package_id': False,
|
||||
'result_package_id': False,
|
||||
'owner_id': owner_id,
|
||||
'location_id': operation.location_id.id, #TODO: owner stuff
|
||||
'location_dest_id': operation.location_dest_id.id,})],
|
||||
'repair_id': repair.id,
|
||||
@ -476,6 +486,7 @@ class Repair(models.Model):
|
||||
'qty_done': repair.product_qty,
|
||||
'package_id': False,
|
||||
'result_package_id': False,
|
||||
'owner_id': owner_id,
|
||||
'location_id': repair.location_id.id, #TODO: owner stuff
|
||||
'location_dest_id': repair.location_dest_id.id,})],
|
||||
'repair_id': repair.id,
|
||||
|
@ -2,7 +2,7 @@
|
||||
<flectra>
|
||||
<data>
|
||||
<template id="report_mrprepairorder">
|
||||
<t t-foreach="docs" t-as="o">
|
||||
<t t-set="o" t-value="doc"/>
|
||||
<t t-call="web.external_layout">
|
||||
<t t-set="o" t-value="o.with_context({'lang': o.partner_id.lang})" />
|
||||
<div class="page">
|
||||
@ -155,7 +155,6 @@
|
||||
<div class="oe_structure"/>
|
||||
</div>
|
||||
</t>
|
||||
</t>
|
||||
</template>
|
||||
|
||||
<template id="report_mrprepairorder2">
|
||||
|
@ -150,7 +150,7 @@
|
||||
</group>
|
||||
</form>
|
||||
<tree string="Fees" editable="bottom">
|
||||
<field name="product_id" domain="[('type','=','service')]"/>
|
||||
<field name="product_id" domain="[('type','=','service')]" required="True"/>
|
||||
<field name='name'/>
|
||||
<field name="product_uom_qty" string="Quantity"/>
|
||||
<field name="product_uom" string="Unit of Measure" groups="product.group_uom"/>
|
||||
|
@ -318,10 +318,11 @@ class PosConfig(models.Model):
|
||||
def name_get(self):
|
||||
result = []
|
||||
for config in self:
|
||||
if (not config.session_ids) or (config.session_ids[0].state == 'closed'):
|
||||
last_session = self.env['pos.session'].search([('config_id', '=', config.id)], limit=1)
|
||||
if (not last_session) or (last_session.state == 'closed'):
|
||||
result.append((config.id, config.name + ' (' + _('not used') + ')'))
|
||||
continue
|
||||
result.append((config.id, config.name + ' (' + config.session_ids[0].user_id.name + ')'))
|
||||
result.append((config.id, config.name + ' (' + last_session.user_id.name + ')'))
|
||||
return result
|
||||
|
||||
@api.model
|
||||
@ -350,11 +351,12 @@ class PosConfig(models.Model):
|
||||
|
||||
@api.multi
|
||||
def write(self, vals):
|
||||
if (self.is_posbox or vals.get('is_posbox')) and (self.iface_customer_facing_display or vals.get('iface_customer_facing_display')):
|
||||
facing_display = (self.customer_facing_display_html or vals.get('customer_facing_display_html') or '').strip()
|
||||
if not facing_display:
|
||||
vals['customer_facing_display_html'] = self._compute_default_customer_html()
|
||||
result = super(PosConfig, self).write(vals)
|
||||
|
||||
config_display = self.filtered(lambda c: c.is_posbox and c.iface_customer_facing_display and not (c.customer_facing_display_html or '').strip())
|
||||
if config_display:
|
||||
super(PosConfig, config_display).write({'customer_facing_display_html': self._compute_default_customer_html()})
|
||||
|
||||
self.sudo()._set_fiscal_position()
|
||||
self.sudo()._check_modules_to_install()
|
||||
self.sudo()._check_groups_implied()
|
||||
|
@ -46,9 +46,11 @@ class PosOrder(models.Model):
|
||||
}
|
||||
|
||||
def _payment_fields(self, ui_paymentline):
|
||||
payment_date = ui_paymentline['name']
|
||||
payment_date = fields.Date.context_today(self, fields.Datetime.from_string(payment_date))
|
||||
return {
|
||||
'amount': ui_paymentline['amount'] or 0.0,
|
||||
'payment_date': ui_paymentline['name'],
|
||||
'payment_date': payment_date,
|
||||
'statement_id': ui_paymentline['statement_id'],
|
||||
'payment_name': ui_paymentline.get('note', False),
|
||||
'journal': ui_paymentline['journal_id'],
|
||||
@ -173,7 +175,7 @@ class PosOrder(models.Model):
|
||||
'comment': self.note or '',
|
||||
# considering partner's sale pricelist's currency
|
||||
'currency_id': self.pricelist_id.currency_id.id,
|
||||
'user_id': self.env.uid,
|
||||
'user_id': self.user_id.id,
|
||||
}
|
||||
|
||||
@api.model
|
||||
@ -258,7 +260,7 @@ class PosOrder(models.Model):
|
||||
line = grouped_data[product_key][0]
|
||||
product = Product.browse(line['product_id'])
|
||||
# In the SO part, the entries will be inverted by function compute_invoice_totals
|
||||
price_unit = - product._get_anglo_saxon_price_unit()
|
||||
price_unit = self._get_pos_anglo_saxon_price_unit(product, line['partner_id'], line['quantity'])
|
||||
account_analytic = Analytic.browse(line.get('analytic_account_id'))
|
||||
res = Product._anglo_saxon_sale_move_lines(
|
||||
line['name'], product, product.uom_id, line['quantity'], price_unit,
|
||||
@ -412,6 +414,18 @@ class PosOrder(models.Model):
|
||||
move.sudo().post()
|
||||
return True
|
||||
|
||||
def _get_pos_anglo_saxon_price_unit(self, product, partner_id, quantity):
|
||||
price_unit = product._get_anglo_saxon_price_unit()
|
||||
if product._get_invoice_policy() == "delivery":
|
||||
moves = self.filtered(lambda o: o.partner_id.id == partner_id)\
|
||||
.mapped('picking_id.move_lines')\
|
||||
.filtered(lambda m: m.product_id.id == product.id)\
|
||||
.sorted(lambda x: x.date)
|
||||
average_price_unit = product._compute_average_price(0, quantity, moves)
|
||||
price_unit = average_price_unit or price_unit
|
||||
# In the SO part, the entries will be inverted by function compute_invoice_totals
|
||||
return - price_unit
|
||||
|
||||
def _reconcile_payments(self):
|
||||
for order in self:
|
||||
aml = order.statement_ids.mapped('journal_entry_ids') | order.account_move.line_ids | order.invoice_id.move_id.line_ids
|
||||
@ -801,7 +815,7 @@ class PosOrder(models.Model):
|
||||
"""Create a new payment for the order"""
|
||||
args = {
|
||||
'amount': data['amount'],
|
||||
'date': data.get('payment_date', fields.Date.today()),
|
||||
'date': data.get('payment_date', fields.Date.context_today(self)),
|
||||
'name': self.name + ': ' + (data.get('payment_name', '') or ''),
|
||||
'partner_id': self.env["res.partner"]._find_accounting_partner(self.partner_id).id or False,
|
||||
}
|
||||
|
@ -38,8 +38,8 @@ class PosSession(models.Model):
|
||||
paid=order.amount_paid,
|
||||
))
|
||||
order.action_pos_order_done()
|
||||
orders = session.order_ids.filtered(lambda order: order.state in ['invoiced', 'done'])
|
||||
orders.sudo()._reconcile_payments()
|
||||
orders_to_reconcile = session.order_ids.filtered(lambda order: order.state in ['invoiced', 'done'] and order.partner_id)
|
||||
orders_to_reconcile.sudo()._reconcile_payments()
|
||||
|
||||
config_id = fields.Many2one(
|
||||
'pos.config', string='Point of Sale',
|
||||
|
@ -21,6 +21,23 @@ class ProductTemplate(models.Model):
|
||||
raise UserError(_('You cannot delete a product saleable in point of sale while a session is still opened.'))
|
||||
return super(ProductTemplate, self).unlink()
|
||||
|
||||
@api.onchange('sale_ok')
|
||||
def _onchange_sale_ok(self):
|
||||
if not self.sale_ok:
|
||||
self.available_in_pos = False
|
||||
|
||||
|
||||
class ProductProduct(models.Model):
|
||||
_inherit = 'product.product'
|
||||
|
||||
@api.multi
|
||||
def unlink(self):
|
||||
product_ctx = dict(self.env.context or {}, active_test=False)
|
||||
if self.env['pos.session'].search_count([('state', '!=', 'closed')]):
|
||||
if self.with_context(product_ctx).search_count([('id', 'in', self.ids), ('product_tmpl_id.available_in_pos', '=', True)]):
|
||||
raise UserError(_('You cannot delete a product saleable in point of sale while a session is still opened.'))
|
||||
return super(ProductProduct, self).unlink()
|
||||
|
||||
|
||||
class ProductUomCateg(models.Model):
|
||||
_inherit = 'product.uom.categ'
|
||||
|
@ -215,7 +215,7 @@ var ProxyDevice = core.Class.extend(mixins.PropertiesMixin,{
|
||||
var self = this;
|
||||
|
||||
function status(){
|
||||
self.connection.rpc('/hw_proxy/status_json',{},{timeout:2500})
|
||||
self.connection.rpc('/hw_proxy/status_json',{},{shadow: true, timeout:2500})
|
||||
.then(function(driver_status){
|
||||
self.set_connection_status('connected',driver_status);
|
||||
},function(){
|
||||
@ -239,7 +239,7 @@ var ProxyDevice = core.Class.extend(mixins.PropertiesMixin,{
|
||||
callbacks[i](params);
|
||||
}
|
||||
if(this.get('status').status !== 'disconnected'){
|
||||
return this.connection.rpc('/hw_proxy/' + name, params || {});
|
||||
return this.connection.rpc('/hw_proxy/' + name, params || {}, {shadow: true});
|
||||
}else{
|
||||
return (new $.Deferred()).reject();
|
||||
}
|
||||
@ -588,7 +588,7 @@ var BarcodeReader = core.Class.extend({
|
||||
this.remote_active = 1;
|
||||
|
||||
function waitforbarcode(){
|
||||
return self.proxy.connection.rpc('/hw_proxy/scanner',{},{timeout:7500})
|
||||
return self.proxy.connection.rpc('/hw_proxy/scanner',{},{shadow: true, timeout:7500})
|
||||
.then(function(barcode){
|
||||
if(!self.remote_scanning){
|
||||
self.remote_active = 0;
|
||||
|
@ -259,6 +259,7 @@ exports.PosModel = Backbone.Model.extend({
|
||||
// we attribute a role to the user, 'cashier' or 'manager', depending
|
||||
// on the group the user belongs.
|
||||
var pos_users = [];
|
||||
var current_cashier = self.get_cashier();
|
||||
for (var i = 0; i < users.length; i++) {
|
||||
var user = users[i];
|
||||
for (var j = 0; j < user.groups_id.length; j++) {
|
||||
@ -276,6 +277,8 @@ exports.PosModel = Backbone.Model.extend({
|
||||
// replace the current user with its updated version
|
||||
if (user.id === self.user.id) {
|
||||
self.user = user;
|
||||
}
|
||||
if (user.id === current_cashier.id) {
|
||||
self.set_cashier(user);
|
||||
}
|
||||
}
|
||||
@ -629,7 +632,7 @@ exports.PosModel = Backbone.Model.extend({
|
||||
// changes the current cashier
|
||||
set_cashier: function(user){
|
||||
this.set('cashier', user);
|
||||
this.db.set_cashier(this.cashier);
|
||||
this.db.set_cashier(this.get('cashier'));
|
||||
},
|
||||
//creates a new empty order and sets it as the current order
|
||||
add_new_order: function(){
|
||||
@ -940,6 +943,7 @@ exports.PosModel = Backbone.Model.extend({
|
||||
model: 'pos.order',
|
||||
method: 'create_from_ui',
|
||||
args: args,
|
||||
kwargs: {context: session.user_context},
|
||||
}, {
|
||||
timeout: timeout,
|
||||
shadow: !options.to_invoice
|
||||
@ -1384,7 +1388,8 @@ exports.Orderline = Backbone.Model.extend({
|
||||
if (unit.rounding) {
|
||||
this.quantity = round_pr(quant, unit.rounding);
|
||||
var decimals = this.pos.dp['Product Unit of Measure'];
|
||||
this.quantityStr = field_utils.format.float(round_di(this.quantity, decimals), {digits: [69, decimals]});
|
||||
this.quantity = round_di(this.quantity, decimals)
|
||||
this.quantityStr = field_utils.format.float(this.quantity, {digits: [69, decimals]});
|
||||
} else {
|
||||
this.quantity = round_pr(quant, 1);
|
||||
this.quantityStr = this.quantity.toFixed(0);
|
||||
@ -1400,6 +1405,7 @@ exports.Orderline = Backbone.Model.extend({
|
||||
this.set_unit_price(this.product.get_price(this.order.pricelist, this.get_quantity()));
|
||||
this.order.fix_tax_included_price(this);
|
||||
}
|
||||
this.trigger('change', this);
|
||||
},
|
||||
// return the quantity of product
|
||||
get_quantity: function(){
|
||||
@ -2118,7 +2124,7 @@ exports.Order = Backbone.Model.extend({
|
||||
company_registry: company.company_registry,
|
||||
contact_address: company.partner_id[1],
|
||||
vat: company.vat,
|
||||
vat_label: company.country.vat_label,
|
||||
vat_label: company.country && company.country.vat_label || '',
|
||||
name: company.name,
|
||||
phone: company.phone,
|
||||
logo: this.pos.company_logo_base64,
|
||||
|
@ -1172,9 +1172,11 @@ var ClientListScreenWidget = ScreenWidget.extend({
|
||||
var order = this.pos.get_order();
|
||||
if( this.has_client_changed() ){
|
||||
var default_fiscal_position_id = _.findWhere(this.pos.fiscal_positions, {'id': this.pos.config.default_fiscal_position_id[0]});
|
||||
if ( this.new_client && this.new_client.property_account_position_id ) {
|
||||
var client_fiscal_position_id = _.findWhere(this.pos.fiscal_positions, {'id': this.new_client.property_account_position_id[0]});
|
||||
order.fiscal_position = client_fiscal_position_id || default_fiscal_position_id;
|
||||
if ( this.new_client ) {
|
||||
if (this.new_client.property_account_position_id ){
|
||||
var client_fiscal_position_id = _.findWhere(this.pos.fiscal_positions, {'id': this.new_client.property_account_position_id[0]});
|
||||
order.fiscal_position = client_fiscal_position_id || default_fiscal_position_id;
|
||||
}
|
||||
order.set_pricelist(_.findWhere(this.pos.pricelists, {'id': this.new_client.property_product_pricelist[0]}) || this.pos.default_pricelist);
|
||||
} else {
|
||||
order.fiscal_position = default_fiscal_position_id;
|
||||
|
@ -134,7 +134,7 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div t-if="widget.editable" class="o_kanban_card_manage_settings row">
|
||||
<div t-if="widget.editable" class="o_kanban_card_manage_settings row" groups="point_of_sale.group_pos_manager">
|
||||
<div class="col-xs-12 text-right">
|
||||
<a type="edit">Settings</a>
|
||||
</div>
|
||||
|
@ -22,7 +22,8 @@
|
||||
</tr></thead>
|
||||
<tbody>
|
||||
<tr t-foreach='products' t-as='line'>
|
||||
<td><t t-esc="line['product_name']" /></td>
|
||||
<t t-set="internal_reference" t-value="line['code'] and '[%s] ' % line['code'] or ''" />
|
||||
<td><t t-esc="internal_reference" /><t t-esc="line['product_name']" /></td>
|
||||
<td>
|
||||
<t t-esc="line['quantity']" />
|
||||
<t t-if='line["uom"] != "Unit(s)"'>
|
||||
|
@ -5,8 +5,8 @@
|
||||
<!-- This is a test account for testing with test cards and cannot be used in a live environment -->
|
||||
<record id="pos_mercury_configuration" model="pos_mercury.configuration">
|
||||
<field name="name">Mercury Demo</field>
|
||||
<field name="merchant_id">334160</field>
|
||||
<field name="merchant_pwd">81303DUR</field>
|
||||
<field name="merchant_id">755847002</field>
|
||||
<field name="merchant_pwd">xyz</field>
|
||||
</record>
|
||||
</data>
|
||||
</flectra>
|
||||
|
@ -4,6 +4,7 @@
|
||||
import logging
|
||||
|
||||
from flectra import models, fields, api, _
|
||||
from flectra.tools.float_utils import float_compare
|
||||
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
@ -71,8 +72,9 @@ class PosOrder(models.Model):
|
||||
statement_id = super(PosOrder, self).add_payment(data)
|
||||
statement_lines = self.env['account.bank.statement.line'].search([('statement_id', '=', statement_id),
|
||||
('pos_statement_id', '=', self.id),
|
||||
('journal_id', '=', data['journal']),
|
||||
('amount', '=', data['amount'])])
|
||||
('journal_id', '=', data['journal'])])
|
||||
statement_lines = statement_lines.filtered(lambda line: float_compare(line.amount, data['amount'],
|
||||
precision_rounding=line.journal_currency_id.rounding) == 0)
|
||||
|
||||
# we can get multiple statement_lines when there are >1 credit
|
||||
# card payments with the same amount. In that case it doesn't
|
||||
|
@ -61,11 +61,15 @@ class MercuryTransaction(models.Model):
|
||||
'SOAPAction': 'http://www.mercurypay.com/CreditTransaction',
|
||||
}
|
||||
|
||||
url = 'https://w1.mercurypay.com/ws/ws.asmx'
|
||||
if self.env['ir.config_parameter'].sudo().get_param('pos_mercury.enable_test_env'):
|
||||
url = 'https://w1.mercurycert.net/ws/ws.asmx'
|
||||
|
||||
try:
|
||||
r = requests.post('https://w1.mercurypay.com/ws/ws.asmx', data=xml_transaction, headers=headers, timeout=65)
|
||||
r = requests.post(url, data=xml_transaction, headers=headers, timeout=65)
|
||||
r.raise_for_status()
|
||||
response = werkzeug.utils.unescape(r.content.decode())
|
||||
except:
|
||||
except Exception:
|
||||
response = "timeout"
|
||||
|
||||
return response
|
||||
|
@ -449,6 +449,7 @@ PaymentScreenWidget.include({
|
||||
order.selected_paymentline.paid = true;
|
||||
order.selected_paymentline.mercury_swipe_pending = false;
|
||||
order.selected_paymentline.mercury_amount = response.authorize;
|
||||
order.selected_paymentline.set_amount(response.authorize);
|
||||
order.selected_paymentline.mercury_card_number = decodedMagtek['number'];
|
||||
order.selected_paymentline.mercury_card_brand = response.card_type;
|
||||
order.selected_paymentline.mercury_card_owner_name = decodedMagtek['name'];
|
||||
|
@ -27,11 +27,14 @@ var Printer = core.Class.extend(mixins.PropertiesMixin,{
|
||||
function send_printing_job(){
|
||||
if(self.receipt_queue.length > 0){
|
||||
var r = self.receipt_queue.shift();
|
||||
self.connection.rpc('/hw_proxy/print_xml_receipt',{receipt: r},{timeout: 5000})
|
||||
var options = {shadow: true, timeout: 5000};
|
||||
self.connection.rpc('/hw_proxy/print_xml_receipt', {receipt: r}, options)
|
||||
.then(function(){
|
||||
send_printing_job();
|
||||
},function(){
|
||||
},function(error, event){
|
||||
self.receipt_queue.unshift(r);
|
||||
console.log('There was an error while trying to print the order:');
|
||||
console.log(error);
|
||||
});
|
||||
}
|
||||
}
|
||||
@ -55,7 +58,7 @@ models.load_models({
|
||||
|
||||
for(var i = 0; i < printers.length; i++){
|
||||
if(active_printers[printers[i].id]){
|
||||
var url = printers[i].proxy_ip;
|
||||
var url = printers[i].proxy_ip || '';
|
||||
if(url.indexOf('//') < 0){
|
||||
url = 'http://'+url;
|
||||
}
|
||||
|
@ -25,9 +25,7 @@ class AccountInvoiceLine(models.Model):
|
||||
qty_done = sum([x.uom_id._compute_quantity(x.quantity, x.product_id.uom_id) for x in s_line.invoice_lines if x.invoice_id.state in ('open', 'paid')])
|
||||
quantity = self.uom_id._compute_quantity(self.quantity, self.product_id.uom_id)
|
||||
# Put moves in fixed order by date executed
|
||||
moves = self.env['stock.move']
|
||||
moves |= s_line.move_ids
|
||||
moves.sorted(lambda x: x.date)
|
||||
moves = s_line.move_ids.sorted(lambda x: x.date)
|
||||
# Go through all the moves and do nothing until you get to qty_done
|
||||
# Beyond qty_done we need to calculate the average of the price_unit
|
||||
# on the moves we encounter.
|
||||
@ -37,23 +35,4 @@ class AccountInvoiceLine(models.Model):
|
||||
return price_unit
|
||||
|
||||
def _compute_average_price(self, qty_done, quantity, moves):
|
||||
average_price_unit = 0
|
||||
qty_delivered = 0
|
||||
invoiced_qty = 0
|
||||
for move in moves:
|
||||
if move.state != 'done':
|
||||
continue
|
||||
invoiced_qty += move.product_qty
|
||||
if invoiced_qty <= qty_done:
|
||||
continue
|
||||
qty_to_consider = move.product_qty
|
||||
if invoiced_qty - move.product_qty < qty_done:
|
||||
qty_to_consider = invoiced_qty - qty_done
|
||||
qty_to_consider = min(qty_to_consider, quantity - qty_delivered)
|
||||
qty_delivered += qty_to_consider
|
||||
# `move.price_unit` is negative if the move is out and positive if the move is
|
||||
# dropshipped. Use its absolute value to compute the average price unit.
|
||||
average_price_unit = (average_price_unit * (qty_delivered - qty_to_consider) + abs(move.price_unit) * qty_to_consider) / qty_delivered
|
||||
if qty_delivered == quantity:
|
||||
break
|
||||
return average_price_unit
|
||||
return self.env['product.product']._compute_average_price(qty_done, quantity, moves)
|
||||
|
@ -5,7 +5,7 @@ from datetime import datetime, timedelta
|
||||
|
||||
from flectra import api, fields, models, _
|
||||
from flectra.tools import DEFAULT_SERVER_DATETIME_FORMAT, float_compare
|
||||
from flectra.exceptions import UserError, ValidationError
|
||||
from flectra.exceptions import UserError
|
||||
|
||||
|
||||
class SaleOrder(models.Model):
|
||||
@ -137,13 +137,9 @@ class SaleOrderLine(models.Model):
|
||||
@api.multi
|
||||
@api.depends('product_id')
|
||||
def _compute_qty_delivered_updateable(self):
|
||||
# prefetch field before filtering
|
||||
self.mapped('product_id')
|
||||
# on consumable or stockable products, qty_delivered_updateable defaults
|
||||
# to False; on other lines use the original computation
|
||||
lines = self.filtered(lambda line: line.product_id.type not in ('consu', 'product'))
|
||||
lines = lines.with_prefetch(self._prefetch)
|
||||
super(SaleOrderLine, lines)._compute_qty_delivered_updateable()
|
||||
for line in self:
|
||||
if line.product_id.type not in ('consu', 'product'):
|
||||
super(SaleOrderLine, line)._compute_qty_delivered_updateable()
|
||||
|
||||
@api.onchange('product_id')
|
||||
def _onchange_product_id_set_customer_lead(self):
|
||||
@ -343,8 +339,8 @@ class SaleOrderLine(models.Model):
|
||||
raise UserError('You cannot decrease the ordered quantity below the delivered quantity.\n'
|
||||
'Create a return first.')
|
||||
for line in self:
|
||||
pickings = self.order_id.picking_ids.filtered(lambda p: p.state not in ('done', 'cancel'))
|
||||
pickings = line.order_id.picking_ids.filtered(lambda p: p.state not in ('done', 'cancel'))
|
||||
for picking in pickings:
|
||||
picking.message_post("The quantity of %s has been updated from %d to %d in %s" %
|
||||
(line.product_id.name, line.product_uom_qty, values['product_uom_qty'], self.order_id.name))
|
||||
(line.product_id.display_name, line.product_uom_qty, values['product_uom_qty'], line.order_id.name))
|
||||
super(SaleOrderLine, self)._update_line_quantity(values)
|
||||
|
@ -28,7 +28,7 @@ class StockMove(models.Model):
|
||||
|
||||
def _action_done(self):
|
||||
result = super(StockMove, self)._action_done()
|
||||
for line in result.mapped('sale_line_id'):
|
||||
for line in result.mapped('sale_line_id').sudo():
|
||||
line.qty_delivered = line._get_delivered_qty()
|
||||
return result
|
||||
|
||||
@ -39,7 +39,7 @@ class StockMove(models.Model):
|
||||
for move in self:
|
||||
if move.state == 'done':
|
||||
sale_order_lines = self.filtered(lambda move: move.sale_line_id and move.product_id.expense_policy == 'no').mapped('sale_line_id')
|
||||
for line in sale_order_lines:
|
||||
for line in sale_order_lines.sudo():
|
||||
line.qty_delivered = line._get_delivered_qty()
|
||||
return res
|
||||
|
||||
@ -64,15 +64,3 @@ class StockPicking(models.Model):
|
||||
_inherit = 'stock.picking'
|
||||
|
||||
sale_id = fields.Many2one(related="group_id.sale_id", string="Sales Order", store=True)
|
||||
|
||||
@api.multi
|
||||
def _create_backorder(self, backorder_moves=[]):
|
||||
res = super(StockPicking, self)._create_backorder(backorder_moves)
|
||||
for picking in self.filtered(lambda pick: pick.picking_type_id.code == 'outgoing'):
|
||||
backorder = picking.search([('backorder_id', '=', picking.id)])
|
||||
if backorder.sale_id:
|
||||
backorder.message_post_with_view(
|
||||
'mail.message_origin_link',
|
||||
values={'self': backorder, 'origin': backorder.sale_id},
|
||||
subtype_id=self.env.ref('mail.mt_note').id)
|
||||
return res
|
||||
|
@ -52,7 +52,7 @@ class TestSaleStock(TestSale):
|
||||
pick_2 = self.so.picking_ids[0]
|
||||
pick_2.force_assign()
|
||||
pick_2.move_lines.write({'quantity_done': 1})
|
||||
self.assertIsNone(pick_2.button_validate(), 'Sale Stock: second picking should be final without need for a backorder')
|
||||
self.assertTrue(pick_2.button_validate(), 'Sale Stock: second picking should be final without need for a backorder')
|
||||
self.assertEqual(self.so.invoice_status, 'to invoice', 'Sale Stock: so invoice_status should be "to invoice" after complete delivery')
|
||||
del_qties = [sol.qty_delivered for sol in self.so.order_line]
|
||||
del_qties_truth = [2.0 if sol.product_id.type in ['product', 'consu'] else 0.0 for sol in self.so.order_line]
|
||||
@ -104,7 +104,7 @@ class TestSaleStock(TestSale):
|
||||
pick = self.so.picking_ids
|
||||
pick.force_assign()
|
||||
pick.move_lines.write({'quantity_done': 2})
|
||||
self.assertIsNone(pick.button_validate(), 'Sale Stock: complete delivery should not need a backorder')
|
||||
self.assertTrue(pick.button_validate(), 'Sale Stock: complete delivery should not need a backorder')
|
||||
del_qties = [sol.qty_delivered for sol in self.so.order_line]
|
||||
del_qties_truth = [2.0 if sol.product_id.type in ['product', 'consu'] else 0.0 for sol in self.so.order_line]
|
||||
self.assertEqual(del_qties, del_qties_truth, 'Sale Stock: delivered quantities are wrong after partial delivery')
|
||||
|
@ -77,7 +77,7 @@ class ProcurementRule(models.Model):
|
||||
|
||||
data = self._get_stock_move_values(product_id, product_qty, product_uom, location_id, name, origin, values, group_id)
|
||||
# Since action_confirm launch following procurement_group we should activate it.
|
||||
move = self.env['stock.move'].sudo().create(data)
|
||||
move = self.env['stock.move'].sudo().with_context(force_company=data.get('company_id', False)).create(data)
|
||||
move._action_confirm()
|
||||
return True
|
||||
|
||||
|
@ -98,13 +98,13 @@ class Product(models.Model):
|
||||
|
||||
domain_move_in = [('product_id', 'in', self.ids)] + domain_move_in_loc
|
||||
domain_move_out = [('product_id', 'in', self.ids)] + domain_move_out_loc
|
||||
if lot_id:
|
||||
if lot_id is not None:
|
||||
domain_quant += [('lot_id', '=', lot_id)]
|
||||
if owner_id:
|
||||
if owner_id is not None:
|
||||
domain_quant += [('owner_id', '=', owner_id)]
|
||||
domain_move_in += [('restrict_partner_id', '=', owner_id)]
|
||||
domain_move_out += [('restrict_partner_id', '=', owner_id)]
|
||||
if package_id:
|
||||
if package_id is not None:
|
||||
domain_quant += [('package_id', '=', package_id)]
|
||||
if dates_in_the_past:
|
||||
domain_move_in_done = list(domain_move_in)
|
||||
@ -221,6 +221,19 @@ class Product(models.Model):
|
||||
domain + loc_domain + ['!'] + dest_loc_domain if dest_loc_domain else domain + loc_domain
|
||||
)
|
||||
|
||||
def _search_qty_available(self, operator, value):
|
||||
# In the very specific case we want to retrieve products with stock available, we only need
|
||||
# to use the quants, not the stock moves. Therefore, we bypass the usual
|
||||
# '_search_product_quantity' method and call '_search_qty_available_new' instead. This
|
||||
# allows better performances.
|
||||
if value == 0.0 and operator == '>' and not ({'from_date', 'to_date'} & set(self.env.context.keys())):
|
||||
product_ids = self._search_qty_available_new(
|
||||
operator, value, self.env.context.get('lot_id'), self.env.context.get('owner_id'),
|
||||
self.env.context.get('package_id')
|
||||
)
|
||||
return [('id', 'in', product_ids)]
|
||||
return self._search_product_quantity(operator, value, 'qty_available')
|
||||
|
||||
def _search_virtual_available(self, operator, value):
|
||||
# TDE FIXME: should probably clean the search methods
|
||||
return self._search_product_quantity(operator, value, 'virtual_available')
|
||||
@ -245,24 +258,13 @@ class Product(models.Model):
|
||||
|
||||
# TODO: Still optimization possible when searching virtual quantities
|
||||
ids = []
|
||||
for product in self.search([]):
|
||||
for product in self.with_context(prefetch_fields=False).search([]):
|
||||
if OPERATORS[operator](product[field], value):
|
||||
ids.append(product.id)
|
||||
return [('id', 'in', ids)]
|
||||
|
||||
def _search_qty_available(self, operator, value):
|
||||
# TDE FIXME: should probably clean the search methods
|
||||
if value == 0.0 and operator in ('=', '>=', '<='):
|
||||
return self._search_product_quantity(operator, value, 'qty_available')
|
||||
product_ids = self._search_qty_available_new(operator, value, self._context.get('lot_id'), self._context.get('owner_id'), self._context.get('package_id'))
|
||||
if (value > 0 and operator in ('<=', '<')) or (value < 0 and operator in ('>=', '>')):
|
||||
# include also unavailable products
|
||||
domain = self._search_product_quantity(operator, value, 'qty_available')
|
||||
product_ids += domain[0][2]
|
||||
return [('id', 'in', product_ids)]
|
||||
|
||||
def _search_qty_available_new(self, operator, value, lot_id=False, owner_id=False, package_id=False):
|
||||
# TDE FIXME: should probably clean the search methods
|
||||
''' Optimized method which doesn't search on stock.moves, only on stock.quants. '''
|
||||
product_ids = set()
|
||||
domain_quant = self._get_domain_locations()[0]
|
||||
if lot_id:
|
||||
@ -410,8 +412,11 @@ class ProductTemplate(models.Model):
|
||||
outgoing_qty = fields.Float(
|
||||
'Outgoing', compute='_compute_quantities', search='_search_outgoing_qty',
|
||||
digits=dp.get_precision('Product Unit of Measure'))
|
||||
location_id = fields.Many2one('stock.location', 'Location')
|
||||
warehouse_id = fields.Many2one('stock.warehouse', 'Warehouse')
|
||||
# The goal of these fields is not to be able to search a location_id/warehouse_id but
|
||||
# to properly make these fields "dummy": only used to put some keys in context from
|
||||
# the search view in order to influence computed field
|
||||
location_id = fields.Many2one('stock.location', 'Location', store=False, search=lambda operator, operand, vals: [])
|
||||
warehouse_id = fields.Many2one('stock.warehouse', 'Warehouse', store=False, search=lambda operator, operand, vals: [])
|
||||
route_ids = fields.Many2many(
|
||||
'stock.location.route', 'stock_route_product', 'product_id', 'route_id', 'Routes',
|
||||
domain=[('product_selectable', '=', True)],
|
||||
@ -503,7 +508,7 @@ class ProductTemplate(models.Model):
|
||||
if 'uom_id' in vals:
|
||||
new_uom = self.env['product.uom'].browse(vals['uom_id'])
|
||||
updated = self.filtered(lambda template: template.uom_id != new_uom)
|
||||
done_moves = self.env['stock.move'].search([('product_id', 'in', updated.mapped('product_variant_ids').ids)], limit=1)
|
||||
done_moves = self.env['stock.move'].search([('product_id', 'in', updated.with_context(active_test=False).mapped('product_variant_ids').ids)], limit=1)
|
||||
if done_moves:
|
||||
raise UserError(_("You can not change the unit of measure of a product that has already been used in a done stock move. If you need to change the unit of measure, you may deactivate this product."))
|
||||
if 'type' in vals and vals['type'] != 'product' and sum(self.mapped('nbr_reordering_rules')) != 0:
|
||||
@ -578,3 +583,27 @@ class ProductCategory(models.Model):
|
||||
category = category.parent_id
|
||||
routes |= category.route_ids
|
||||
self.total_route_ids = routes
|
||||
|
||||
|
||||
class ProductUoM(models.Model):
|
||||
_inherit = 'product.uom'
|
||||
|
||||
def write(self, values):
|
||||
# Users can not update the factor if open stock moves are based on it
|
||||
if 'factor' in values or 'factor_inv' in values or 'category_id' in values:
|
||||
changed = self.filtered(
|
||||
lambda u: any(u[f] != values[f] if f in values else False
|
||||
for f in {'factor', 'factor_inv', 'category_id'}))
|
||||
if changed:
|
||||
stock_move_lines = self.env['stock.move.line'].search_count([
|
||||
('product_uom_id.category_id', 'in', changed.mapped('category_id.id')),
|
||||
('state', '!=', 'cancel'),
|
||||
])
|
||||
|
||||
if stock_move_lines:
|
||||
raise UserError(_(
|
||||
"You cannot change the ratio of this unit of mesure as some"
|
||||
" products with this UoM have already been moved or are "
|
||||
"currently reserved."
|
||||
))
|
||||
return super(ProductUoM, self).write(values)
|
||||
|
@ -28,12 +28,18 @@ class Company(models.Model):
|
||||
location.sudo().write({'company_id': self.id})
|
||||
self.write({'internal_transit_location_id': location.id})
|
||||
|
||||
warehouses = self.env['stock.warehouse'].search([('partner_id', '=', self.partner_id.id)])
|
||||
warehouses.mapped('partner_id').with_context(force_company=self.id).write({
|
||||
'property_stock_customer': location.id,
|
||||
'property_stock_supplier': location.id,
|
||||
})
|
||||
|
||||
@api.model
|
||||
def create(self, vals):
|
||||
company = super(Company, self).create(vals)
|
||||
|
||||
# multi-company rules prevents creating warehouse and sub-locations
|
||||
company.create_transit_location()
|
||||
# mutli-company rules prevents creating warehouse and sub-locations
|
||||
self.env['stock.warehouse'].check_access_rights('create')
|
||||
self.env['stock.warehouse'].sudo().create({'name': company.name, 'code': company.name[:5], 'company_id': company.id, 'partner_id': company.partner_id.id})
|
||||
company.create_transit_location()
|
||||
return company
|
||||
|
@ -339,7 +339,7 @@ class InventoryLine(models.Model):
|
||||
# TDE FIXME: necessary ? -> replace by location_id
|
||||
prodlot_name = fields.Char(
|
||||
'Serial Number Name',
|
||||
related='prod_lot_id.name', store=True)
|
||||
related='prod_lot_id.name', store=True, readonly=True)
|
||||
company_id = fields.Many2one(
|
||||
'res.company', 'Company', related='inventory_id.company_id',
|
||||
index=True, readonly=True, store=True)
|
||||
|
@ -91,6 +91,21 @@ class Location(models.Model):
|
||||
if 'usage' in values and values['usage'] == 'view':
|
||||
if self.mapped('quant_ids'):
|
||||
raise UserError(_("This location's usage cannot be changed to view as it contains products."))
|
||||
if 'usage' in values or 'scrap_location' in values:
|
||||
|
||||
modified_locations = self.filtered(
|
||||
lambda l: any(l[f] != values[f] if f in values else False
|
||||
for f in {'usage', 'scrap_location'}))
|
||||
reserved_quantities = self.env['stock.move.line'].search_count([
|
||||
('location_id', 'in', modified_locations.ids),
|
||||
('product_qty', '>', 0),
|
||||
])
|
||||
if reserved_quantities:
|
||||
raise UserError(_(
|
||||
"You cannot change the location type or its use as a scrap"
|
||||
" location as there are products reserved in this location."
|
||||
" Please unreserve the products first."
|
||||
))
|
||||
return super(Location, self).write(values)
|
||||
|
||||
@api.multi
|
||||
|
@ -275,14 +275,23 @@ class StockMove(models.Model):
|
||||
""" This will return the move lines to consider when applying _quantity_done_compute on a stock.move.
|
||||
In some context, such as MRP, it is necessary to compute quantity_done on filtered sock.move.line."""
|
||||
self.ensure_one()
|
||||
return self.move_line_ids
|
||||
return self.move_line_ids or self.move_line_nosuggest_ids
|
||||
|
||||
@api.depends('move_line_ids.qty_done', 'move_line_ids.product_uom_id')
|
||||
@api.depends('move_line_ids.qty_done', 'move_line_ids.product_uom_id', 'move_line_nosuggest_ids.qty_done')
|
||||
def _quantity_done_compute(self):
|
||||
""" This field represents the sum of the move lines `qty_done`. It allows the user to know
|
||||
if there is still work to do.
|
||||
|
||||
We take care of rounding this value at the general decimal precision and not the rounding
|
||||
of the move's UOM to make sure this value is really close to the real sum, because this
|
||||
field will be used in `_action_done` in order to know if the move will need a backorder or
|
||||
an extra move.
|
||||
"""
|
||||
for move in self:
|
||||
quantity_done = 0
|
||||
for move_line in move._get_move_lines():
|
||||
# Transform the move_line quantity_done into the move uom.
|
||||
move.quantity_done += move_line.product_uom_id._compute_quantity(move_line.qty_done, move.product_uom)
|
||||
quantity_done += move_line.product_uom_id._compute_quantity(move_line.qty_done, move.product_uom, round=False)
|
||||
move.quantity_done = quantity_done
|
||||
|
||||
def _quantity_done_set(self):
|
||||
quantity_done = self[0].quantity_done # any call to create will invalidate `move.quantity_done`
|
||||
@ -459,10 +468,10 @@ class StockMove(models.Model):
|
||||
if propagated_date_field:
|
||||
current_date = datetime.strptime(move.date_expected, DEFAULT_SERVER_DATETIME_FORMAT)
|
||||
new_date = datetime.strptime(vals.get(propagated_date_field), DEFAULT_SERVER_DATETIME_FORMAT)
|
||||
delta = relativedelta.relativedelta(new_date, current_date)
|
||||
if abs(delta.days) >= move.company_id.propagation_minimum_delta:
|
||||
delta_days = (new_date - current_date).total_seconds() / 86400
|
||||
if abs(delta_days) >= move.company_id.propagation_minimum_delta:
|
||||
old_move_date = datetime.strptime(move.move_dest_ids[0].date_expected, DEFAULT_SERVER_DATETIME_FORMAT)
|
||||
new_move_date = (old_move_date + relativedelta.relativedelta(days=delta.days or 0)).strftime(DEFAULT_SERVER_DATETIME_FORMAT)
|
||||
new_move_date = (old_move_date + relativedelta.relativedelta(days=delta_days or 0)).strftime(DEFAULT_SERVER_DATETIME_FORMAT)
|
||||
propagated_changes_dict['date_expected'] = new_move_date
|
||||
#For pushed moves as well as for pulled moves, propagate by recursive call of write().
|
||||
#Note that, for pulled moves we intentionally don't propagate on the procurement.
|
||||
@ -521,9 +530,19 @@ class StockMove(models.Model):
|
||||
}
|
||||
|
||||
def _do_unreserve(self):
|
||||
if any(move.state in ('done', 'cancel') for move in self):
|
||||
raise UserError(_('Cannot unreserve a done move'))
|
||||
self.mapped('move_line_ids').unlink()
|
||||
moves_to_unreserve = self.env['stock.move']
|
||||
for move in self:
|
||||
if move.state == 'cancel':
|
||||
# We may have cancelled move in an open picking in a "propagate_cancel" scenario.
|
||||
continue
|
||||
if move.state == 'done':
|
||||
if move.scrapped:
|
||||
# We may have done move in an open picking in a scrap scenario.
|
||||
continue
|
||||
else:
|
||||
raise UserError(_('Cannot unreserve a done move'))
|
||||
moves_to_unreserve |= move
|
||||
moves_to_unreserve.mapped('move_line_ids').unlink()
|
||||
return True
|
||||
|
||||
def _push_apply(self):
|
||||
@ -618,7 +637,7 @@ class StockMove(models.Model):
|
||||
# We are using propagate to False in order to not cancel destination moves merged in moves[0]
|
||||
moves_to_unlink.write({'propagate': False})
|
||||
moves_to_unlink._action_cancel()
|
||||
moves_to_unlink.unlink()
|
||||
moves_to_unlink.sudo().unlink()
|
||||
return (self | self.env['stock.move'].concat(*moves_to_merge)) - moves_to_unlink
|
||||
|
||||
def _get_relevant_state_among_moves(self):
|
||||
@ -679,7 +698,7 @@ class StockMove(models.Model):
|
||||
self.product_uom = product.uom_id.id
|
||||
return {'domain': {'product_uom': [('category_id', '=', product.uom_id.category_id.id)]}}
|
||||
|
||||
@api.onchange('date')
|
||||
@api.onchange('date_expected')
|
||||
def onchange_date(self):
|
||||
if self.date_expected:
|
||||
self.date = self.date_expected
|
||||
@ -805,7 +824,7 @@ class StockMove(models.Model):
|
||||
group_id = False
|
||||
return {
|
||||
'company_id': self.company_id,
|
||||
'date_planned': self.date,
|
||||
'date_planned': self.date_expected,
|
||||
'move_dest_ids': self,
|
||||
'group_id': group_id,
|
||||
'route_ids': self.route_ids,
|
||||
@ -834,7 +853,12 @@ class StockMove(models.Model):
|
||||
}
|
||||
if quantity:
|
||||
uom_quantity = self.product_id.uom_id._compute_quantity(quantity, self.product_uom, rounding_method='HALF-UP')
|
||||
vals = dict(vals, product_uom_qty=uom_quantity)
|
||||
uom_quantity_back_to_product_uom = self.product_uom._compute_quantity(uom_quantity, self.product_id.uom_id, rounding_method='HALF-UP')
|
||||
rounding = self.env['decimal.precision'].precision_get('Product Unit of Measure')
|
||||
if float_compare(quantity, uom_quantity_back_to_product_uom, precision_digits=rounding) == 0:
|
||||
vals = dict(vals, product_uom_qty=uom_quantity)
|
||||
else:
|
||||
vals = dict(vals, product_uom_qty=quantity, product_uom_id=self.product_id.uom_id.id)
|
||||
if reserved_quant:
|
||||
vals = dict(
|
||||
vals,
|
||||
@ -859,18 +883,32 @@ class StockMove(models.Model):
|
||||
|
||||
taken_quantity = min(available_quantity, need)
|
||||
|
||||
# `taken_quantity` is in the quants unit of measure. There's a possibility that the move's
|
||||
# unit of measure won't be respected if we blindly reserve this quantity, a common usecase
|
||||
# is if the move's unit of measure's rounding does not allow fractional reservation. We chose
|
||||
# to convert `taken_quantity` to the move's unit of measure with a down rounding method and
|
||||
# then get it back in the quants unit of measure with an half-up rounding_method. This
|
||||
# way, we'll never reserve more than allowed. We do not apply this logic if
|
||||
# `available_quantity` is brought by a chained move line. In this case, `_prepare_move_line_vals`
|
||||
# will take care of changing the UOM to the UOM of the product.
|
||||
if not strict:
|
||||
taken_quantity_move_uom = self.product_id.uom_id._compute_quantity(taken_quantity, self.product_uom, rounding_method='DOWN')
|
||||
taken_quantity = self.product_uom._compute_quantity(taken_quantity_move_uom, self.product_id.uom_id, rounding_method='HALF-UP')
|
||||
|
||||
quants = []
|
||||
|
||||
if self.product_id.tracking == 'serial':
|
||||
rounding = self.env['decimal.precision'].precision_get('Product Unit of Measure')
|
||||
if float_compare(taken_quantity, int(taken_quantity), precision_digits=rounding) != 0:
|
||||
taken_quantity = 0
|
||||
|
||||
try:
|
||||
quants = self.env['stock.quant']._update_reserved_quantity(
|
||||
self.product_id, location_id, taken_quantity, lot_id=lot_id,
|
||||
package_id=package_id, owner_id=owner_id, strict=strict
|
||||
)
|
||||
if not float_is_zero(taken_quantity, precision_rounding=self.product_id.uom_id.rounding):
|
||||
quants = self.env['stock.quant']._update_reserved_quantity(
|
||||
self.product_id, location_id, taken_quantity, lot_id=lot_id,
|
||||
package_id=package_id, owner_id=owner_id, strict=strict
|
||||
)
|
||||
except UserError:
|
||||
# If it raises here, it means that the `available_quantity` brought by a done move line
|
||||
# is not available on the quants itself. This could be the result of an inventory
|
||||
# adjustment that removed totally of partially `available_quantity`. When this happens, we
|
||||
# chose to do nothing. This situation could not happen on MTS move, because in this case
|
||||
# `available_quantity` is directly the quantity on the quants themselves.
|
||||
taken_quantity = 0
|
||||
|
||||
# Find a candidate move line to update or create a new one.
|
||||
@ -878,7 +916,7 @@ class StockMove(models.Model):
|
||||
to_update = self.move_line_ids.filtered(lambda m: m.product_id.tracking != 'serial' and
|
||||
m.location_id.id == reserved_quant.location_id.id and m.lot_id.id == reserved_quant.lot_id.id and m.package_id.id == reserved_quant.package_id.id and m.owner_id.id == reserved_quant.owner_id.id)
|
||||
if to_update:
|
||||
to_update[0].with_context(bypass_reservation_update=True).product_uom_qty += self.product_id.uom_id._compute_quantity(quantity, self.product_uom, rounding_method='HALF-UP')
|
||||
to_update[0].with_context(bypass_reservation_update=True).product_uom_qty += self.product_id.uom_id._compute_quantity(quantity, to_update[0].product_uom_id, rounding_method='HALF-UP')
|
||||
else:
|
||||
if self.product_id.tracking == 'serial':
|
||||
for i in range(0, int(quantity)):
|
||||
@ -896,7 +934,7 @@ class StockMove(models.Model):
|
||||
assigned_moves = self.env['stock.move']
|
||||
partially_available_moves = self.env['stock.move']
|
||||
for move in self.filtered(lambda m: m.state in ['confirmed', 'waiting', 'partially_available']):
|
||||
if move.location_id.usage in ('supplier', 'inventory', 'production', 'customer')\
|
||||
if move.location_id.should_bypass_reservation()\
|
||||
or move.product_id.type == 'consu':
|
||||
# create the move line(s) but do not impact quants
|
||||
if move.product_id.tracking == 'serial' and (move.picking_type_id.use_create_lots or move.picking_type_id.use_existing_lots):
|
||||
@ -919,12 +957,18 @@ class StockMove(models.Model):
|
||||
if not move.move_orig_ids:
|
||||
if move.procure_method == 'make_to_order':
|
||||
continue
|
||||
# If we don't need any quantity, consider the move assigned.
|
||||
need = move.product_qty - move.reserved_availability
|
||||
if float_is_zero(need, precision_rounding=move.product_id.uom_id.rounding):
|
||||
assigned_moves |= move
|
||||
continue
|
||||
# Reserve new quants and create move lines accordingly.
|
||||
available_quantity = self.env['stock.quant']._get_available_quantity(move.product_id, move.location_id)
|
||||
if available_quantity <= 0:
|
||||
continue
|
||||
need = move.product_qty - move.reserved_availability
|
||||
taken_quantity = move._update_reserved_quantity(need, available_quantity, move.location_id, strict=False)
|
||||
if float_is_zero(taken_quantity, precision_rounding=move.product_id.uom_id.rounding):
|
||||
continue
|
||||
if need == taken_quantity:
|
||||
assigned_moves |= move
|
||||
else:
|
||||
@ -980,7 +1024,19 @@ class StockMove(models.Model):
|
||||
available_move_lines[(move_line.location_id, move_line.lot_id, move_line.result_package_id, move_line.owner_id)] -= move_line.product_qty
|
||||
for (location_id, lot_id, package_id, owner_id), quantity in available_move_lines.items():
|
||||
need = move.product_qty - sum(move.move_line_ids.mapped('product_qty'))
|
||||
taken_quantity = move._update_reserved_quantity(need, quantity, location_id, lot_id, package_id, owner_id)
|
||||
# `quantity` is what is brought by chained done move lines. We double check
|
||||
# here this quantity is available on the quants themselves. If not, this
|
||||
# could be the result of an inventory adjustment that removed totally of
|
||||
# partially `quantity`. When this happens, we chose to reserve the maximum
|
||||
# still available. This situation could not happen on MTS move, because in
|
||||
# this case `quantity` is directly the quantity on the quants themselves.
|
||||
available_quantity = self.env['stock.quant']._get_available_quantity(
|
||||
move.product_id, location_id, lot_id=lot_id, package_id=package_id, owner_id=owner_id, strict=True)
|
||||
if float_is_zero(available_quantity, precision_rounding=move.product_id.uom_id.rounding):
|
||||
continue
|
||||
taken_quantity = move._update_reserved_quantity(need, min(quantity, available_quantity), location_id, lot_id, package_id, owner_id)
|
||||
if float_is_zero(taken_quantity, precision_rounding=move.product_id.uom_id.rounding):
|
||||
continue
|
||||
if need - taken_quantity == 0.0:
|
||||
assigned_moves |= move
|
||||
break
|
||||
@ -1031,8 +1087,8 @@ class StockMove(models.Model):
|
||||
# create the extra moves
|
||||
extra_move_quantity = float_round(
|
||||
self.quantity_done - self.product_uom_qty,
|
||||
precision_rounding=self.product_uom.rounding,
|
||||
rounding_method ='UP')
|
||||
precision_rounding=rounding,
|
||||
rounding_method='HALF-UP')
|
||||
extra_move_vals = self._prepare_extra_move_vals(extra_move_quantity)
|
||||
extra_move = self.copy(default=extra_move_vals)
|
||||
if extra_move.picking_id:
|
||||
@ -1060,6 +1116,9 @@ class StockMove(models.Model):
|
||||
break
|
||||
return extra_move
|
||||
|
||||
def _unreserve_initial_demand(self, new_move):
|
||||
pass
|
||||
|
||||
def check_move_bal_qty(self):
|
||||
for move in self:
|
||||
if move.move_type == 'in':
|
||||
@ -1106,8 +1165,10 @@ class StockMove(models.Model):
|
||||
|
||||
# Split moves where necessary and move quants
|
||||
for move in moves_todo:
|
||||
rounding = move.product_uom.rounding
|
||||
if float_compare(move.quantity_done, move.product_uom_qty, precision_rounding=rounding) < 0:
|
||||
# To know whether we need to create a backorder or not, round to the general product's
|
||||
# decimal precision and not the product's UOM.
|
||||
rounding = self.env['decimal.precision'].precision_get('Product Unit of Measure')
|
||||
if float_compare(move.quantity_done, move.product_uom_qty, precision_digits=rounding) < 0:
|
||||
# Need to do some kind of conversion here
|
||||
qty_split = move.product_uom._compute_quantity(move.product_uom_qty - move.quantity_done, move.product_id.uom_id, rounding_method='HALF-UP')
|
||||
new_move = move._split(qty_split)
|
||||
@ -1120,9 +1181,7 @@ class StockMove(models.Model):
|
||||
move_line.write({'product_uom_qty': move_line.qty_done})
|
||||
except UserError:
|
||||
pass
|
||||
|
||||
# If you were already putting stock.move.lots on the next one in the work order, transfer those to the new move
|
||||
move.move_line_ids.filtered(lambda x: x.qty_done == 0.0).write({'move_id': new_move})
|
||||
move._unreserve_initial_demand(new_move)
|
||||
move.move_line_ids._action_done()
|
||||
# Check the consistency of the result packages; there should be an unique location across
|
||||
# the contained quants.
|
||||
@ -1154,15 +1213,17 @@ class StockMove(models.Model):
|
||||
self.mapped('move_line_ids').unlink()
|
||||
return super(StockMove, self).unlink()
|
||||
|
||||
def _prepare_move_split_vals(self, uom_qty):
|
||||
def _prepare_move_split_vals(self, qty):
|
||||
vals = {
|
||||
'product_uom_qty': uom_qty,
|
||||
'product_uom_qty': qty,
|
||||
'procure_method': 'make_to_stock',
|
||||
'move_dest_ids': [(4, x.id) for x in self.move_dest_ids if x.state not in ('done', 'cancel')],
|
||||
'move_orig_ids': [(4, x.id) for x in self.move_orig_ids],
|
||||
'origin_returned_move_id': self.origin_returned_move_id.id,
|
||||
'price_unit': self.price_unit,
|
||||
}
|
||||
if self.env.context.get('force_split_uom_id'):
|
||||
vals['product_uom'] = self.env.context['force_split_uom_id']
|
||||
return vals
|
||||
|
||||
def _split(self, qty, restrict_partner_id=False):
|
||||
@ -1181,9 +1242,18 @@ class StockMove(models.Model):
|
||||
raise UserError(_('You cannot split a draft move. It needs to be confirmed first.'))
|
||||
if float_is_zero(qty, precision_rounding=self.product_id.uom_id.rounding) or self.product_qty <= qty:
|
||||
return self.id
|
||||
# HALF-UP rounding as only rounding errors will be because of propagation of error from default UoM
|
||||
|
||||
decimal_precision = self.env['decimal.precision'].precision_get('Product Unit of Measure')
|
||||
|
||||
# `qty` passed as argument is the quantity to backorder and is always expressed in the
|
||||
# quants UOM. If we're able to convert back and forth this quantity in the move's and the
|
||||
# quants UOM, the backordered move can keep the UOM of the move. Else, we'll create is in
|
||||
# the UOM of the quants.
|
||||
uom_qty = self.product_id.uom_id._compute_quantity(qty, self.product_uom, rounding_method='HALF-UP')
|
||||
defaults = self._prepare_move_split_vals(uom_qty)
|
||||
if float_compare(qty, self.product_uom._compute_quantity(uom_qty, self.product_id.uom_id, rounding_method='HALF-UP'), precision_digits=decimal_precision) == 0:
|
||||
defaults = self._prepare_move_split_vals(uom_qty)
|
||||
else:
|
||||
defaults = self.with_context(force_split_uom_id=self.product_id.uom_id.id)._prepare_move_split_vals(qty)
|
||||
|
||||
if restrict_partner_id:
|
||||
defaults['restrict_partner_id'] = restrict_partner_id
|
||||
@ -1192,21 +1262,15 @@ class StockMove(models.Model):
|
||||
if self.env.context.get('source_location_id'):
|
||||
defaults['location_id'] = self.env.context['source_location_id']
|
||||
new_move = self.with_context(rounding_method='HALF-UP').copy(defaults)
|
||||
# ctx = context.copy()
|
||||
# TDE CLEANME: used only in write in this file, to clean
|
||||
# ctx['do_not_propagate'] = True
|
||||
|
||||
# FIXME: pim fix your crap
|
||||
self.with_context(do_not_propagate=True, do_not_unreserve=True, rounding_method='HALF-UP').write({'product_uom_qty': self.product_uom_qty - uom_qty})
|
||||
|
||||
# if self.move_dest_id and self.propagate and self.move_dest_id.state not in ('done', 'cancel'):
|
||||
# new_move_prop = self.move_dest_id.split(qty)
|
||||
# new_move.write({'move_dest_id': new_move_prop})
|
||||
# returning the first element of list returned by action_confirm is ok because we checked it wouldn't be exploded (and
|
||||
# thus the result of action_confirm should always be a list of 1 element length)
|
||||
# In this case we don't merge move since the new move with 0 quantity done will be used for the backorder.
|
||||
# Update the original `product_qty` of the move. Use the general product's decimal
|
||||
# precision and not the move's UOM to handle case where the `quantity_done` is not
|
||||
# compatible with the move's UOM.
|
||||
new_product_qty = self.product_id.uom_id._compute_quantity(self.product_qty - qty, self.product_uom, round=False)
|
||||
new_product_qty = float_round(new_product_qty, precision_digits=self.env['decimal.precision'].precision_get('Product Unit of Measure'))
|
||||
self.with_context(do_not_propagate=True, do_not_unreserve=True, rounding_method='HALF-UP').write({'product_uom_qty': new_product_qty})
|
||||
new_move = new_move._action_confirm(merge=False)
|
||||
# TDE FIXME: due to action confirm change
|
||||
return new_move.id
|
||||
|
||||
def _recompute_state(self):
|
||||
|
@ -52,7 +52,6 @@ class StockMoveLine(models.Model):
|
||||
reference = fields.Char(related='move_id.reference', store=True)
|
||||
in_entire_package = fields.Boolean(compute='_compute_in_entire_package')
|
||||
|
||||
@api.one
|
||||
def _compute_location_description(self):
|
||||
for operation, operation_sudo in izip(self, self.sudo()):
|
||||
operation.from_loc = '%s%s' % (operation_sudo.location_id.name, operation.product_id and operation_sudo.package_id.name or '')
|
||||
@ -141,7 +140,7 @@ class StockMoveLine(models.Model):
|
||||
help him. This onchange will warn him if he set `qty_done` to a non-supported value.
|
||||
"""
|
||||
res = {}
|
||||
if self.product_id.tracking == 'serial':
|
||||
if self.qty_done and self.product_id.tracking == 'serial':
|
||||
if float_compare(self.qty_done, 1.0, precision_rounding=self.move_id.product_id.uom_id.rounding) != 0:
|
||||
message = _('You can only process 1.0 %s for products with unique serial number.') % self.product_id.uom_id.name
|
||||
res['warning'] = {'title': _('Warning'), 'message': message}
|
||||
@ -269,7 +268,7 @@ class StockMoveLine(models.Model):
|
||||
except UserError:
|
||||
pass
|
||||
if new_product_qty != ml.product_qty:
|
||||
new_product_uom_qty = self.product_id.uom_id._compute_quantity(new_product_qty, self.product_uom_id, rounding_method='HALF-UP')
|
||||
new_product_uom_qty = ml.product_id.uom_id._compute_quantity(new_product_qty, ml.product_uom_id, rounding_method='HALF-UP')
|
||||
ml.with_context(bypass_reservation_update=True).product_uom_qty = new_product_uom_qty
|
||||
|
||||
# When editing a done move line, the reserved availability of a potential chained move is impacted. Take care of running again `_action_assign` on the concerned moves.
|
||||
@ -370,6 +369,15 @@ class StockMoveLine(models.Model):
|
||||
# `action_done` on the next move lines.
|
||||
ml_to_delete = self.env['stock.move.line']
|
||||
for ml in self:
|
||||
# Check here if `ml.qty_done` respects the rounding of `ml.product_uom_id`.
|
||||
uom_qty = float_round(ml.qty_done, precision_rounding=ml.product_uom_id.rounding, rounding_method='HALF-UP')
|
||||
precision_digits = self.env['decimal.precision'].precision_get('Product Unit of Measure')
|
||||
qty_done = float_round(ml.qty_done, precision_digits=precision_digits, rounding_method='HALF-UP')
|
||||
if float_compare(uom_qty, qty_done, precision_digits=precision_digits) != 0:
|
||||
raise UserError(_('The quantity done for the product "%s" doesn\'t respect the rounding precision \
|
||||
defined on the unit of measure "%s". Please change the quantity done or the \
|
||||
rounding precision of your unit of measure.') % (ml.product_id.display_name, ml.product_uom_id.name))
|
||||
|
||||
qty_done_float_compared = float_compare(ml.qty_done, 0, precision_rounding=ml.product_uom_id.rounding)
|
||||
if qty_done_float_compared > 0:
|
||||
if ml.product_id.tracking != 'none':
|
||||
|
@ -320,7 +320,7 @@ class Picking(models.Model):
|
||||
for picking in self:
|
||||
if self.env.context.get('force_detailed_view'):
|
||||
picking.show_operations = True
|
||||
break
|
||||
continue
|
||||
if picking.picking_type_id.show_operations:
|
||||
if (picking.state == 'draft' and not self.env.context.get('planned_picking')) or picking.state != 'draft':
|
||||
picking.show_operations = True
|
||||
@ -669,6 +669,7 @@ class Picking(models.Model):
|
||||
all_in = True
|
||||
pack_move_lines = self.move_line_ids.filtered(lambda ml: ml.package_id == package)
|
||||
keys = ['product_id', 'lot_id']
|
||||
precision_digits = self.env['decimal.precision'].precision_get('Product Unit of Measure')
|
||||
|
||||
grouped_quants = {}
|
||||
for k, g in groupby(sorted(package.quant_ids, key=itemgetter(*keys)), key=itemgetter(*keys)):
|
||||
@ -677,8 +678,8 @@ class Picking(models.Model):
|
||||
grouped_ops = {}
|
||||
for k, g in groupby(sorted(pack_move_lines, key=itemgetter(*keys)), key=itemgetter(*keys)):
|
||||
grouped_ops[k] = sum(self.env['stock.move.line'].concat(*list(g)).mapped('product_qty'))
|
||||
if any(grouped_quants.get(key, 0) - grouped_ops.get(key, 0) != 0 for key in grouped_quants) \
|
||||
or any(grouped_ops.get(key, 0) - grouped_quants.get(key, 0) != 0 for key in grouped_ops):
|
||||
if any(not float_is_zero(grouped_quants.get(key, 0) - grouped_ops.get(key, 0), precision_digits=precision_digits) for key in grouped_quants) \
|
||||
or any(not float_is_zero(grouped_ops.get(key, 0) - grouped_quants.get(key, 0), precision_digits=precision_digits) for key in grouped_ops):
|
||||
all_in = False
|
||||
return all_in
|
||||
|
||||
@ -704,7 +705,8 @@ class Picking(models.Model):
|
||||
|
||||
# If no lots when needed, raise error
|
||||
picking_type = self.picking_type_id
|
||||
no_quantities_done = all(float_is_zero(move_line.qty_done, precision_rounding=move_line.product_uom_id.rounding) for move_line in self.move_line_ids)
|
||||
precision_digits = self.env['decimal.precision'].precision_get('Product Unit of Measure')
|
||||
no_quantities_done = all(float_is_zero(move_line.qty_done, precision_digits=precision_digits) for move_line in self.move_line_ids)
|
||||
no_reserved_quantities = all(float_is_zero(move_line.product_qty, precision_rounding=move_line.product_uom_id.rounding) for move_line in self.move_line_ids)
|
||||
if no_reserved_quantities and no_quantities_done:
|
||||
raise UserError(_('You cannot validate a transfer if you have not processed any quantity. You should rather cancel the transfer.'))
|
||||
@ -760,7 +762,7 @@ class Picking(models.Model):
|
||||
if self._check_backorder():
|
||||
return self.action_generate_backorder_wizard()
|
||||
self.action_done()
|
||||
return
|
||||
return True
|
||||
|
||||
def action_generate_backorder_wizard(self):
|
||||
view = self.env.ref('stock.view_backorder_confirmation')
|
||||
|
@ -37,6 +37,18 @@ class ProductionLot(models.Model):
|
||||
raise UserError(_("You are not allowed to create a lot for this picking type"))
|
||||
return super(ProductionLot, self).create(vals)
|
||||
|
||||
@api.multi
|
||||
def write(self, vals):
|
||||
if 'product_id' in vals:
|
||||
move_lines = self.env['stock.move.line'].search([('lot_id', 'in', self.ids)])
|
||||
if move_lines:
|
||||
raise UserError(_(
|
||||
'You are not allowed to change the product linked to a serial or lot number ' +
|
||||
'if some stock moves have already been created with that number. ' +
|
||||
'This would lead to inconsistencies in your stock.'
|
||||
))
|
||||
return super(ProductionLot, self).write(vals)
|
||||
|
||||
@api.one
|
||||
def _product_qty(self):
|
||||
# We only care for the quants in internal or transit locations.
|
||||
|
@ -81,12 +81,6 @@ class StockQuant(models.Model):
|
||||
if float_compare(quant.quantity, 1, precision_rounding=quant.product_uom_id.rounding) > 0 and quant.lot_id and quant.product_id.tracking == 'serial':
|
||||
raise ValidationError(_('A serial number should only be linked to a single product.'))
|
||||
|
||||
@api.constrains('in_date', 'lot_id')
|
||||
def check_in_date(self):
|
||||
for quant in self:
|
||||
if quant.in_date and not quant.lot_id:
|
||||
raise ValidationError(_('An incoming date cannot be set to an untracked product.'))
|
||||
|
||||
@api.constrains('location_id')
|
||||
def check_location_id(self):
|
||||
for quant in self:
|
||||
@ -111,9 +105,9 @@ class StockQuant(models.Model):
|
||||
@api.model
|
||||
def _get_removal_strategy_order(self, removal_strategy):
|
||||
if removal_strategy == 'fifo':
|
||||
return 'in_date, id'
|
||||
return 'in_date ASC NULLS FIRST, id'
|
||||
elif removal_strategy == 'lifo':
|
||||
return 'in_date desc, id desc'
|
||||
return 'in_date DESC NULLS LAST, id desc'
|
||||
raise UserError(_('Removal strategy %s not implemented.') % (removal_strategy,))
|
||||
|
||||
def _gather(self, product_id, location_id, lot_id=None, package_id=None, owner_id=None, strict=False):
|
||||
@ -136,7 +130,17 @@ class StockQuant(models.Model):
|
||||
domain = expression.AND([[('owner_id', '=', owner_id and owner_id.id or False)], domain])
|
||||
domain = expression.AND([[('location_id', '=', location_id.id)], domain])
|
||||
|
||||
return self.search(domain, order=removal_strategy_order)
|
||||
# Copy code of _search for special NULLS FIRST/LAST order
|
||||
self.sudo(self._uid).check_access_rights('read')
|
||||
query = self._where_calc(domain)
|
||||
self._apply_ir_rules(query, 'read')
|
||||
from_clause, where_clause, where_clause_params = query.get_sql()
|
||||
where_str = where_clause and (" WHERE %s" % where_clause) or ''
|
||||
query_str = 'SELECT "%s".id FROM ' % self._table + from_clause + where_str + " ORDER BY "+ removal_strategy_order
|
||||
self._cr.execute(query_str, where_clause_params)
|
||||
res = self._cr.fetchall()
|
||||
# No uniquify list necessary as auto_join is not applied anyways...
|
||||
return self.browse([x[0] for x in res])
|
||||
|
||||
@api.model
|
||||
def _get_available_quantity(self, product_id, location_id, lot_id=None, package_id=None, owner_id=None, strict=False, allow_negative=False):
|
||||
@ -198,17 +202,16 @@ class StockQuant(models.Model):
|
||||
quants = self._gather(product_id, location_id, lot_id=lot_id, package_id=package_id, owner_id=owner_id, strict=True)
|
||||
rounding = product_id.uom_id.rounding
|
||||
|
||||
if lot_id:
|
||||
incoming_dates = quants.mapped('in_date') # `mapped` already filtered out falsy items
|
||||
incoming_dates = [fields.Datetime.from_string(incoming_date) for incoming_date in incoming_dates]
|
||||
if in_date:
|
||||
incoming_dates += [in_date]
|
||||
# If multiple incoming dates are available for a given lot_id/package_id/owner_id, we
|
||||
# consider only the oldest one as being relevant.
|
||||
if incoming_dates:
|
||||
in_date = fields.Datetime.to_string(min(incoming_dates))
|
||||
else:
|
||||
in_date = fields.Datetime.now()
|
||||
incoming_dates = [d for d in quants.mapped('in_date') if d]
|
||||
incoming_dates = [fields.Datetime.from_string(incoming_date) for incoming_date in incoming_dates]
|
||||
if in_date:
|
||||
incoming_dates += [in_date]
|
||||
# If multiple incoming dates are available for a given lot_id/package_id/owner_id, we
|
||||
# consider only the oldest one as being relevant.
|
||||
if incoming_dates:
|
||||
in_date = fields.Datetime.to_string(min(incoming_dates))
|
||||
else:
|
||||
in_date = fields.Datetime.now()
|
||||
|
||||
for quant in quants:
|
||||
try:
|
||||
@ -255,13 +258,21 @@ class StockQuant(models.Model):
|
||||
self = self.sudo()
|
||||
rounding = product_id.uom_id.rounding
|
||||
quants = self._gather(product_id, location_id, lot_id=lot_id, package_id=package_id, owner_id=owner_id, strict=strict)
|
||||
available_quantity = self._get_available_quantity(product_id, location_id, lot_id=lot_id, package_id=package_id, owner_id=owner_id, strict=strict)
|
||||
if float_compare(quantity, 0, precision_rounding=rounding) > 0 and float_compare(quantity, available_quantity, precision_rounding=rounding) > 0:
|
||||
raise UserError(_('It is not possible to reserve more products of %s than you have in stock.') % (', '.join(quants.mapped('product_id').mapped('display_name'))))
|
||||
elif float_compare(quantity, 0, precision_rounding=rounding) < 0 and float_compare(abs(quantity), sum(quants.mapped('reserved_quantity')), precision_rounding=rounding) > 0:
|
||||
raise UserError(_('It is not possible to unreserve more products of %s than you have in stock.') % (', '.join(quants.mapped('product_id').mapped('display_name'))))
|
||||
|
||||
reserved_quants = []
|
||||
|
||||
if float_compare(quantity, 0, precision_rounding=rounding) > 0:
|
||||
# if we want to reserve
|
||||
available_quantity = self._get_available_quantity(product_id, location_id, lot_id=lot_id, package_id=package_id, owner_id=owner_id, strict=strict)
|
||||
if float_compare(quantity, available_quantity, precision_rounding=rounding) > 0:
|
||||
raise UserError(_('It is not possible to reserve more products of %s than you have in stock.') % (', '.join(quants.mapped('product_id').mapped('display_name'))))
|
||||
elif float_compare(quantity, 0, precision_rounding=rounding) < 0:
|
||||
# if we want to unreserve
|
||||
available_quantity = sum(quants.mapped('reserved_quantity'))
|
||||
if float_compare(abs(quantity), available_quantity, precision_rounding=rounding) > 0:
|
||||
raise UserError(_('It is not possible to unreserve more products of %s than you have in stock.') % (', '.join(quants.mapped('product_id').mapped('display_name'))))
|
||||
else:
|
||||
return reserved_quants
|
||||
|
||||
for quant in quants:
|
||||
if float_compare(quantity, 0, precision_rounding=rounding) > 0:
|
||||
max_quantity_on_quant = quant.quantity - quant.reserved_quantity
|
||||
@ -450,6 +461,12 @@ class QuantPackage(models.Model):
|
||||
if move_lines_to_remove:
|
||||
move_lines_to_remove.write({'result_package_id': False})
|
||||
else:
|
||||
move_line_to_modify = self.env['stock.move.line'].search([
|
||||
('package_id', '=', package.id),
|
||||
('state', 'in', ('assigned', 'partially_available')),
|
||||
('product_qty', '!=', 0),
|
||||
])
|
||||
move_line_to_modify.write({'package_id': False})
|
||||
package.mapped('quant_ids').write({'package_id': False})
|
||||
|
||||
def action_view_picking(self):
|
||||
|
@ -21,47 +21,55 @@ class MrpStockReport(models.TransientModel):
|
||||
|
||||
@api.model
|
||||
def get_move_lines_upstream(self, move_lines):
|
||||
res = self.env['stock.move.line']
|
||||
for move_line in move_lines:
|
||||
lines_seen = move_lines
|
||||
lines_todo = list(move_lines)
|
||||
while lines_todo:
|
||||
move_line = lines_todo.pop(0)
|
||||
# if MTO
|
||||
if move_line.move_id.move_orig_ids:
|
||||
res |= move_line.move_id.move_orig_ids.mapped('move_line_ids').filtered(
|
||||
lambda m: m.lot_id.id == move_line.lot_id.id)
|
||||
lines = move_line.move_id.move_orig_ids.mapped('move_line_ids').filtered(
|
||||
lambda m: m.lot_id == move_line.lot_id
|
||||
) - lines_seen
|
||||
# if MTS
|
||||
elif move_line.location_id.usage == 'internal':
|
||||
lines = self.env['stock.move.line'].search([
|
||||
('product_id', '=', move_line.product_id.id),
|
||||
('lot_id', '=', move_line.lot_id.id),
|
||||
('location_dest_id', '=', move_line.location_id.id),
|
||||
('id', 'not in', lines_seen.ids),
|
||||
('date', '<', move_line.date),
|
||||
])
|
||||
else:
|
||||
if move_line.location_id.usage == 'internal':
|
||||
res |= self.env['stock.move.line'].search([
|
||||
('product_id', '=', move_line.product_id.id),
|
||||
('lot_id', '=', move_line.lot_id.id),
|
||||
('location_dest_id', '=', move_line.location_id.id),
|
||||
('id', '!=', move_line.id),
|
||||
('date', '<', move_line.date),
|
||||
])
|
||||
if res:
|
||||
res |= self.get_move_lines_upstream(res)
|
||||
return res
|
||||
continue
|
||||
lines_todo += list(lines)
|
||||
lines_seen |= lines
|
||||
return lines_seen - move_lines
|
||||
|
||||
@api.model
|
||||
def get_move_lines_downstream(self, move_lines):
|
||||
res = self.env['stock.move.line']
|
||||
for move_line in move_lines:
|
||||
lines_seen = move_lines
|
||||
lines_todo = list(move_lines)
|
||||
while lines_todo:
|
||||
move_line = lines_todo.pop(0)
|
||||
# if MTO
|
||||
if move_line.move_id.move_dest_ids:
|
||||
res |= move_line.move_id.move_dest_ids.mapped('move_line_ids').filtered(
|
||||
lambda m: m.lot_id.id == move_line.lot_id.id)
|
||||
lines = move_line.move_id.move_dest_ids.mapped('move_line_ids').filtered(
|
||||
lambda m: m.lot_id == move_line.lot_id
|
||||
) - lines_seen
|
||||
# if MTS
|
||||
elif move_line.location_dest_id.usage == 'internal':
|
||||
lines = self.env['stock.move.line'].search([
|
||||
('product_id', '=', move_line.product_id.id),
|
||||
('lot_id', '=', move_line.lot_id.id),
|
||||
('location_id', '=', move_line.location_dest_id.id),
|
||||
('id', 'not in', lines_seen.ids),
|
||||
('date', '>', move_line.date),
|
||||
])
|
||||
else:
|
||||
if move_line.location_dest_id.usage == 'internal':
|
||||
res |= self.env['stock.move.line'].search([
|
||||
('product_id', '=', move_line.product_id.id),
|
||||
('lot_id', '=', move_line.lot_id.id),
|
||||
('location_id', '=', move_line.location_dest_id.id),
|
||||
('id', '!=', move_line.id),
|
||||
('date', '>', move_line.date),
|
||||
])
|
||||
if res:
|
||||
res |= self.get_move_lines_downstream(res)
|
||||
return res
|
||||
continue
|
||||
lines_todo += list(lines)
|
||||
lines_seen |= lines
|
||||
return lines_seen - move_lines
|
||||
|
||||
@api.model
|
||||
def get_lines(self, line_id=None, **kw):
|
||||
|
@ -163,11 +163,14 @@ class Warehouse(models.Model):
|
||||
# If another partner assigned
|
||||
if vals.get('partner_id'):
|
||||
warehouses._update_partner_data(vals['partner_id'], vals.get('company_id'))
|
||||
|
||||
res = super(Warehouse, self).write(vals)
|
||||
|
||||
# check if we need to delete and recreate route
|
||||
if vals.get('reception_steps') or vals.get('delivery_steps'):
|
||||
warehouses._update_routes()
|
||||
route_vals = warehouses._update_routes()
|
||||
if route_vals:
|
||||
self.write(route_vals)
|
||||
|
||||
if vals.get('resupply_wh_ids') and not vals.get('resupply_route_ids'):
|
||||
for warehouse in warehouses:
|
||||
@ -188,9 +191,10 @@ class Warehouse(models.Model):
|
||||
ResCompany = self.env['res.company']
|
||||
if company_id:
|
||||
transit_loc = ResCompany.browse(company_id).internal_transit_location_id.id
|
||||
self.env['res.partner'].browse(partner_id).with_context(force_company=company_id).write({'property_stock_customer': transit_loc, 'property_stock_supplier': transit_loc})
|
||||
else:
|
||||
transit_loc = ResCompany._company_default_get('stock.warehouse').internal_transit_location_id.id
|
||||
self.env['res.partner'].browse(partner_id).write({'property_stock_customer': transit_loc, 'property_stock_supplier': transit_loc})
|
||||
self.env['res.partner'].browse(partner_id).write({'property_stock_customer': transit_loc, 'property_stock_supplier': transit_loc})
|
||||
|
||||
def create_sequences_and_picking_types(self):
|
||||
IrSequenceSudo = self.env['ir.sequence'].sudo()
|
||||
@ -293,7 +297,7 @@ class Warehouse(models.Model):
|
||||
reception_route.pull_ids.write({'active': False})
|
||||
reception_route.push_ids.write({'active': False})
|
||||
else:
|
||||
reception_route = self.env['stock.location.route'].create(warehouse._get_reception_delivery_route_values(warehouse.reception_steps))
|
||||
warehouse.reception_route_id = reception_route = self.env['stock.location.route'].create(warehouse._get_reception_delivery_route_values(warehouse.reception_steps))
|
||||
# push / procurement (pull) rules for reception
|
||||
routings = routes_data[warehouse.id][warehouse.reception_steps]
|
||||
push_rules_list, pull_rules_list = warehouse._get_push_pull_rules_values(
|
||||
@ -406,7 +410,7 @@ class Warehouse(models.Model):
|
||||
|
||||
pull_rules_list = supplier_wh._get_supply_pull_rules_values(
|
||||
[self.Routing(output_location, transit_location, supplier_wh.out_type_id)],
|
||||
values={'route_id': inter_wh_route.id, 'propagate_warehouse_id': self.id})
|
||||
values={'route_id': inter_wh_route.id})
|
||||
pull_rules_list += self._get_supply_pull_rules_values(
|
||||
[self.Routing(transit_location, input_location, self.in_type_id)],
|
||||
values={'route_id': inter_wh_route.id, 'propagate_warehouse_id': supplier_wh.id})
|
||||
@ -606,11 +610,18 @@ class Warehouse(models.Model):
|
||||
routes_data = self.get_routes_dict()
|
||||
# change the default source and destination location and (de)activate operation types
|
||||
self._update_picking_type()
|
||||
self._create_or_update_delivery_route(routes_data)
|
||||
self._create_or_update_reception_route(routes_data)
|
||||
self._create_or_update_crossdock_route(routes_data)
|
||||
self._create_or_update_mto_pull(routes_data)
|
||||
return True
|
||||
delivery_route = self._create_or_update_delivery_route(routes_data)
|
||||
reception_route = self._create_or_update_reception_route(routes_data)
|
||||
crossdock_route = self._create_or_update_crossdock_route(routes_data)
|
||||
mto_pull = self._create_or_update_mto_pull(routes_data)
|
||||
|
||||
return {
|
||||
'route_ids': [(4, route.id) for route in reception_route | delivery_route | crossdock_route],
|
||||
'mto_pull_id': mto_pull.id,
|
||||
'reception_route_id': reception_route.id,
|
||||
'delivery_route_id': delivery_route.id,
|
||||
'crossdock_route_id': crossdock_route.id,
|
||||
}
|
||||
|
||||
@api.one
|
||||
def _update_picking_type(self):
|
||||
|
@ -9,8 +9,8 @@
|
||||
</template>
|
||||
|
||||
<template id="report_location_barcode">
|
||||
<t t-call="web.basic_layout">
|
||||
<div t-foreach="[docs[x:x+4] for x in xrange(0, len(docs), 4)]" t-as="page_docs" class="page page_stock_location_barcodes">
|
||||
<t t-call="web.html_container">
|
||||
<div t-foreach="[docs[x:x+4] for x in xrange(0, len(docs), 4)]" t-as="page_docs" class="page article page_stock_location_barcodes">
|
||||
<t t-foreach="page_docs" t-as="o">
|
||||
<t t-if="o.barcode"><t t-set="content" t-value="o.barcode"/></t>
|
||||
<t t-if="not o.barcode"><t t-set="content" t-value="o.name"/></t>
|
||||
|
@ -61,7 +61,7 @@ class ReportStockForecat(models.Model):
|
||||
LEFT JOIN
|
||||
stock_location source_location ON sm.location_id = source_location.id
|
||||
WHERE
|
||||
sm.state IN ('confirmed','assigned','waiting') and
|
||||
sm.state IN ('confirmed','partially_available','assigned','waiting') and
|
||||
source_location.usage != 'internal' and dest_location.usage = 'internal'
|
||||
GROUP BY sm.date_expected,sm.product_id
|
||||
UNION ALL
|
||||
@ -82,7 +82,7 @@ class ReportStockForecat(models.Model):
|
||||
LEFT JOIN
|
||||
stock_location dest_location ON sm.location_dest_id = dest_location.id
|
||||
WHERE
|
||||
sm.state IN ('confirmed','assigned','waiting') and
|
||||
sm.state IN ('confirmed','partially_available','assigned','waiting') and
|
||||
source_location.usage = 'internal' and dest_location.usage != 'internal'
|
||||
GROUP BY sm.date_expected,sm.product_id)
|
||||
as MAIN
|
||||
|
@ -45,7 +45,7 @@
|
||||
<field name="view_type">form</field>
|
||||
<field name="view_mode">graph,pivot</field>
|
||||
<field name="search_view_id" ref="view_stock_level_forecast_filter"/>
|
||||
<field name="context">{'search_default_pivot_by':1, 'search_default_graph_by':1, 'search_default_product_tmpl_id': active_id}</field>
|
||||
<field name="context">{'search_default_product_tmpl_id': active_id}</field>
|
||||
<field name="view_id" ref="view_stock_level_forecast_pivot"/>
|
||||
</record>
|
||||
|
||||
@ -55,7 +55,7 @@
|
||||
<field name="view_type">form</field>
|
||||
<field name="view_mode">graph,pivot</field>
|
||||
<field name="search_view_id" ref="view_stock_level_forecast_filter"/>
|
||||
<field name="context">{'search_default_pivot_by':1, 'search_default_graph_by':1, 'search_default_product_id': active_id}</field>
|
||||
<field name="context">{'search_default_product_id': active_id}</field>
|
||||
<field name="view_id" ref="view_stock_level_forecast_pivot"/>
|
||||
</record>
|
||||
</flectra>
|
||||
|
@ -102,14 +102,16 @@
|
||||
<span t-field="move.product_id.description_picking"/>
|
||||
</td>
|
||||
<td>
|
||||
<span t-if="move.product_qty" t-esc="move.product_qty"/>
|
||||
<span t-if="move.product_qty" t-field="move.product_qty"/>
|
||||
<span t-if="not move.product_qty" t-esc="move.product_uom._compute_quantity(move.quantity_done, move.product_id.uom_id, rounding_method='HALF-UP')"/>
|
||||
<span t-field="move.product_id.uom_id" groups="product.group_uom"/>
|
||||
</td>
|
||||
<td>
|
||||
<t t-if="has_barcode">
|
||||
<span t-if="move.product_id and move.product_id.barcode">
|
||||
<img t-att-src="'/report/barcode/?type=%s&value=%s&width=%s&height=%s' % ('Code128', move.product_id.barcode, 600, 100)" style="width:100%;height:50px"/>
|
||||
<img t-if="move.product_id.barcode and len(move.product_id.barcode) == 13" t-att-src="'/report/barcode/?type=%s&value=%s&width=%s&height=%s' % ('EAN13', move.product_id.barcode, 600, 100)" style="width:100%;height:50px"/>
|
||||
<img t-elif="move.product_id.barcode and len(move.product_id.barcode) == 8" t-att-src="'/report/barcode/?type=%s&value=%s&width=%s&height=%s' % ('EAN8', move.product_id.barcode, 600, 100)" style="width:100%;height:50px"/>
|
||||
<img t-else="" t-att-src="'/report/barcode/?type=%s&value=%s&width=%s&height=%s' % ('Code128', move.product_id.barcode, 600, 100)" style="width:100%;height:50px"/>
|
||||
</span>
|
||||
</t>
|
||||
</td>
|
||||
|
@ -9,4 +9,5 @@ from . import test_quant
|
||||
from . import test_inventory
|
||||
from . import test_move
|
||||
from . import test_move2
|
||||
from . import test_robustness
|
||||
from . import test_stock_branch
|
||||
|
@ -827,6 +827,129 @@ class StockMove(TransactionCase):
|
||||
|
||||
self.assertEqual(len(move.move_line_ids), 4.0)
|
||||
|
||||
def test_availability_6(self):
|
||||
""" Check that, in the scenario where a move is in a bigger uom than the uom of the quants
|
||||
and this uom only allows entire numbers, we don't make a partial reservation when the
|
||||
quantity available is not enough to reserve the move. Check also that it is not possible
|
||||
to set `quantity_done` with a value not honouring the UOM's rounding.
|
||||
"""
|
||||
# on the dozen uom, set the rounding set 1.0
|
||||
self.uom_dozen.rounding = 1
|
||||
|
||||
# 6 units are available in stock
|
||||
self.env['stock.quant']._update_available_quantity(self.product1, self.stock_location, 6.0)
|
||||
|
||||
# the move should not be reserved
|
||||
move = self.env['stock.move'].create({
|
||||
'name': 'test_availability_6',
|
||||
'location_id': self.stock_location.id,
|
||||
'location_dest_id': self.customer_location.id,
|
||||
'product_id': self.product1.id,
|
||||
'product_uom': self.uom_dozen.id,
|
||||
'product_uom_qty': 1,
|
||||
})
|
||||
move._action_confirm()
|
||||
move._action_assign()
|
||||
self.assertEqual(move.state, 'confirmed')
|
||||
|
||||
# the quants should be left untouched
|
||||
self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product1, self.stock_location), 6.0)
|
||||
|
||||
# make 8 units available, the move should again not be reservabale
|
||||
self.env['stock.quant']._update_available_quantity(self.product1, self.stock_location, 2.0)
|
||||
move._action_assign()
|
||||
self.assertEqual(move.state, 'confirmed')
|
||||
self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product1, self.stock_location), 8.0)
|
||||
|
||||
# make 12 units available, this time the move should be reservable
|
||||
self.env['stock.quant']._update_available_quantity(self.product1, self.stock_location, 4.0)
|
||||
move._action_assign()
|
||||
self.assertEqual(move.state, 'assigned')
|
||||
self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product1, self.stock_location), 0.0)
|
||||
|
||||
# Check it isn't possible to set any value to quantity_done
|
||||
with self.assertRaises(UserError):
|
||||
move.quantity_done = 0.1
|
||||
move._action_done()
|
||||
|
||||
with self.assertRaises(UserError):
|
||||
move.quantity_done = 1.1
|
||||
move._action_done()
|
||||
|
||||
with self.assertRaises(UserError):
|
||||
move.quantity_done = 0.9
|
||||
move._action_done()
|
||||
|
||||
move.quantity_done = 1
|
||||
move._action_done()
|
||||
self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product1, self.customer_location), 12.0)
|
||||
|
||||
def test_availability_7(self):
|
||||
""" Check that, in the scenario where a move is in a bigger uom than the uom of the quants
|
||||
and this uom only allows entire numbers, we only reserve quantity honouring the uom's
|
||||
rounding even if the quantity is set across multiple quants.
|
||||
"""
|
||||
# on the dozen uom, set the rounding set 1.0
|
||||
self.uom_dozen.rounding = 1
|
||||
|
||||
# make 12 quants of 1
|
||||
for i in range(1, 13):
|
||||
lot_id = self.env['stock.production.lot'].create({
|
||||
'name': 'lot%s' % str(i),
|
||||
'product_id': self.product2.id,
|
||||
})
|
||||
self.env['stock.quant']._update_available_quantity(self.product2, self.stock_location, 1.0, lot_id=lot_id)
|
||||
|
||||
# the move should be reserved
|
||||
move = self.env['stock.move'].create({
|
||||
'name': 'test_availability_7',
|
||||
'location_id': self.stock_location.id,
|
||||
'location_dest_id': self.customer_location.id,
|
||||
'product_id': self.product2.id,
|
||||
'product_uom': self.uom_dozen.id,
|
||||
'product_uom_qty': 1,
|
||||
})
|
||||
move._action_confirm()
|
||||
move._action_assign()
|
||||
self.assertEqual(move.state, 'assigned')
|
||||
self.assertEqual(len(move.move_line_ids.mapped('product_uom_id')), 1)
|
||||
self.assertEqual(move.move_line_ids.mapped('product_uom_id'), self.uom_unit)
|
||||
|
||||
for move_line in move.move_line_ids:
|
||||
move_line.qty_done = 1
|
||||
move._action_done()
|
||||
|
||||
self.assertEqual(move.product_uom_qty, 1)
|
||||
self.assertEqual(move.product_uom.id, self.uom_dozen.id)
|
||||
self.assertEqual(move.state, 'done')
|
||||
self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product2, self.customer_location), 12.0)
|
||||
self.assertEqual(len(self.env['stock.quant']._gather(self.product2, self.customer_location)), 12)
|
||||
|
||||
def test_availability_8(self):
|
||||
""" Test the assignment mechanism when the product quantity is decreased on a partially
|
||||
reserved stock move.
|
||||
"""
|
||||
# make some stock
|
||||
self.env['stock.quant']._update_available_quantity(self.product1, self.stock_location, 3.0)
|
||||
self.assertAlmostEqual(self.product1.qty_available, 3.0)
|
||||
|
||||
move_partial = self.env['stock.move'].create({
|
||||
'name': 'test_partial',
|
||||
'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': 5.0,
|
||||
})
|
||||
|
||||
move_partial._action_confirm()
|
||||
move_partial._action_assign()
|
||||
self.assertAlmostEqual(self.product1.virtual_available, -2.0)
|
||||
self.assertEqual(move_partial.state, 'partially_available')
|
||||
move_partial.product_uom_qty = 3.0
|
||||
move_partial._action_assign()
|
||||
self.assertEqual(move_partial.state, 'assigned')
|
||||
|
||||
def test_unreserve_1(self):
|
||||
""" Check that unreserving a stock move sets the products reserved as available and
|
||||
set the state back to confirmed.
|
||||
@ -1022,6 +1145,47 @@ class StockMove(TransactionCase):
|
||||
for quant in quants:
|
||||
self.assertEqual(quant.reserved_quantity, 0)
|
||||
|
||||
def test_unreserve_6(self):
|
||||
""" In a situation with a negative and a positive quant, reserve and unreserve.
|
||||
"""
|
||||
q1 = self.env['stock.quant'].create({
|
||||
'product_id': self.product1.id,
|
||||
'location_id': self.stock_location.id,
|
||||
'quantity': -10,
|
||||
'reserved_quantity': 0,
|
||||
})
|
||||
|
||||
q2 = self.env['stock.quant'].create({
|
||||
'product_id': self.product1.id,
|
||||
'location_id': self.stock_location.id,
|
||||
'quantity': 30.0,
|
||||
'reserved_quantity': 10.0,
|
||||
})
|
||||
|
||||
self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product1, self.stock_location), 10.0)
|
||||
|
||||
move1 = self.env['stock.move'].create({
|
||||
'name': 'test_unreserve_6',
|
||||
'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': 10.0,
|
||||
})
|
||||
move1._action_confirm()
|
||||
move1._action_assign()
|
||||
self.assertEqual(move1.state, 'assigned')
|
||||
self.assertEqual(len(move1.move_line_ids), 1)
|
||||
self.assertEqual(move1.move_line_ids.product_qty, 10)
|
||||
self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product1, self.stock_location), 0.0)
|
||||
self.assertEqual(q2.reserved_quantity, 20)
|
||||
|
||||
move1._do_unreserve()
|
||||
self.assertEqual(move1.state, 'confirmed')
|
||||
self.assertEqual(len(move1.move_line_ids), 0)
|
||||
self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product1, self.stock_location), 10.0)
|
||||
self.assertEqual(q2.reserved_quantity, 10)
|
||||
|
||||
def test_link_assign_1(self):
|
||||
""" Test the assignment mechanism when two chained stock moves try to move one unit of an
|
||||
untracked product.
|
||||
@ -1356,6 +1520,319 @@ class StockMove(TransactionCase):
|
||||
self.assertEqual(move_stock_stock_1.state, 'assigned')
|
||||
self.assertEqual(move_stock_stock_2.state, 'waiting')
|
||||
|
||||
def test_link_assign_7(self):
|
||||
# on the dozen uom, set the rounding set 1.0
|
||||
self.uom_dozen.rounding = 1
|
||||
|
||||
# 6 units are available in stock
|
||||
self.env['stock.quant']._update_available_quantity(self.product1, self.stock_location, 6.0)
|
||||
|
||||
# create pickings and moves for a pick -> pack mto scenario
|
||||
picking_stock_pack = self.env['stock.picking'].create({
|
||||
'location_id': self.stock_location.id,
|
||||
'location_dest_id': self.pack_location.id,
|
||||
'picking_type_id': self.env.ref('stock.picking_type_internal').id,
|
||||
})
|
||||
move_stock_pack = self.env['stock.move'].create({
|
||||
'name': 'test_link_assign_7',
|
||||
'location_id': self.stock_location.id,
|
||||
'location_dest_id': self.pack_location.id,
|
||||
'product_id': self.product1.id,
|
||||
'product_uom': self.uom_dozen.id,
|
||||
'product_uom_qty': 1.0,
|
||||
'picking_id': picking_stock_pack.id,
|
||||
})
|
||||
picking_pack_cust = self.env['stock.picking'].create({
|
||||
'location_id': self.pack_location.id,
|
||||
'location_dest_id': self.customer_location.id,
|
||||
'picking_type_id': self.env.ref('stock.picking_type_out').id,
|
||||
})
|
||||
move_pack_cust = self.env['stock.move'].create({
|
||||
'name': 'test_link_assign_7',
|
||||
'location_id': self.pack_location.id,
|
||||
'location_dest_id': self.customer_location.id,
|
||||
'product_id': self.product1.id,
|
||||
'product_uom': self.uom_dozen.id,
|
||||
'product_uom_qty': 1.0,
|
||||
'picking_id': picking_pack_cust.id,
|
||||
})
|
||||
move_stock_pack.write({'move_dest_ids': [(4, move_pack_cust.id, 0)]})
|
||||
move_pack_cust.write({'move_orig_ids': [(4, move_stock_pack.id, 0)]})
|
||||
(move_stock_pack + move_pack_cust)._action_confirm()
|
||||
|
||||
# the pick should not be reservable because of the rounding of the dozen
|
||||
move_stock_pack._action_assign()
|
||||
self.assertEqual(move_stock_pack.state, 'confirmed')
|
||||
move_pack_cust._action_assign()
|
||||
self.assertEqual(move_pack_cust.state, 'waiting')
|
||||
|
||||
# move the 6 units by adding an unreserved move line
|
||||
move_stock_pack.write({'move_line_ids': [(0, 0, {
|
||||
'product_id': self.product1.id,
|
||||
'product_uom_id': self.uom_unit.id,
|
||||
'qty_done': 6,
|
||||
'product_uom_qty': 0,
|
||||
'lot_id': False,
|
||||
'package_id': False,
|
||||
'result_package_id': False,
|
||||
'location_id': move_stock_pack.location_id.id,
|
||||
'location_dest_id': move_stock_pack.location_dest_id.id,
|
||||
'picking_id': picking_stock_pack.id,
|
||||
})]})
|
||||
|
||||
# the quantity done on the move should not respect the rounding of the move line
|
||||
self.assertEqual(move_stock_pack.quantity_done, 0.5)
|
||||
|
||||
# create the backorder in the uom of the quants
|
||||
backorder_wizard_dict = picking_stock_pack.button_validate()
|
||||
backorder_wizard = self.env[backorder_wizard_dict['res_model']].browse(backorder_wizard_dict['res_id'])
|
||||
backorder_wizard.process()
|
||||
self.assertEqual(move_stock_pack.state, 'done')
|
||||
self.assertEqual(move_stock_pack.quantity_done, 0.5)
|
||||
self.assertEqual(move_stock_pack.product_uom_qty, 0.5)
|
||||
|
||||
# the second move should not be reservable because of the rounding on the dozen
|
||||
move_pack_cust._action_assign()
|
||||
self.assertEqual(move_pack_cust.state, 'partially_available')
|
||||
move_line_pack_cust = move_pack_cust.move_line_ids
|
||||
self.assertEqual(move_line_pack_cust.product_uom_qty, 6)
|
||||
self.assertEqual(move_line_pack_cust.product_uom_id.id, self.uom_unit.id)
|
||||
|
||||
# move a dozen on the backorder to see how we handle the extra move
|
||||
backorder = self.env['stock.picking'].search([('backorder_id', '=', picking_stock_pack.id)])
|
||||
backorder.move_lines.write({'move_line_ids': [(0, 0, {
|
||||
'product_id': self.product1.id,
|
||||
'product_uom_id': self.uom_dozen.id,
|
||||
'qty_done': 1,
|
||||
'product_uom_qty': 0,
|
||||
'lot_id': False,
|
||||
'package_id': False,
|
||||
'result_package_id': False,
|
||||
'location_id': backorder.location_id.id,
|
||||
'location_dest_id': backorder.location_dest_id.id,
|
||||
'picking_id': backorder.id,
|
||||
})]})
|
||||
overprocessed_wizard_dict = backorder.button_validate()
|
||||
overprocessed_wizard = self.env[overprocessed_wizard_dict['res_model']].browse(overprocessed_wizard_dict['res_id'])
|
||||
overprocessed_wizard.action_confirm()
|
||||
backorder_move = backorder.move_lines
|
||||
self.assertEqual(backorder_move.state, 'done')
|
||||
self.assertEqual(backorder_move.quantity_done, 12.0)
|
||||
self.assertEqual(backorder_move.product_uom_qty, 12.0)
|
||||
self.assertEqual(backorder_move.product_uom, self.uom_unit)
|
||||
|
||||
# the second move should now be reservable
|
||||
move_pack_cust._action_assign()
|
||||
self.assertEqual(move_pack_cust.state, 'assigned')
|
||||
self.assertEqual(move_line_pack_cust.product_uom_qty, 12)
|
||||
self.assertEqual(move_line_pack_cust.product_uom_id.id, self.uom_unit.id)
|
||||
self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product1, move_stock_pack.location_dest_id), 6)
|
||||
|
||||
def test_link_assign_8(self):
|
||||
""" Set the rounding of the dozen to 1.0, create a chain of two move for a dozen, the product
|
||||
concerned is tracked by serial number. Check that the flow is ok.
|
||||
"""
|
||||
# on the dozen uom, set the rounding set 1.0
|
||||
self.uom_dozen.rounding = 1
|
||||
|
||||
# 6 units are available in stock
|
||||
for i in range(1, 13):
|
||||
lot_id = self.env['stock.production.lot'].create({
|
||||
'name': 'lot%s' % str(i),
|
||||
'product_id': self.product2.id,
|
||||
})
|
||||
self.env['stock.quant']._update_available_quantity(self.product2, self.stock_location, 1.0, lot_id=lot_id)
|
||||
|
||||
# create pickings and moves for a pick -> pack mto scenario
|
||||
picking_stock_pack = self.env['stock.picking'].create({
|
||||
'location_id': self.stock_location.id,
|
||||
'location_dest_id': self.pack_location.id,
|
||||
'picking_type_id': self.env.ref('stock.picking_type_internal').id,
|
||||
})
|
||||
move_stock_pack = self.env['stock.move'].create({
|
||||
'name': 'test_link_assign_7',
|
||||
'location_id': self.stock_location.id,
|
||||
'location_dest_id': self.pack_location.id,
|
||||
'product_id': self.product2.id,
|
||||
'product_uom': self.uom_dozen.id,
|
||||
'product_uom_qty': 1.0,
|
||||
'picking_id': picking_stock_pack.id,
|
||||
})
|
||||
picking_pack_cust = self.env['stock.picking'].create({
|
||||
'location_id': self.pack_location.id,
|
||||
'location_dest_id': self.customer_location.id,
|
||||
'picking_type_id': self.env.ref('stock.picking_type_out').id,
|
||||
})
|
||||
move_pack_cust = self.env['stock.move'].create({
|
||||
'name': 'test_link_assign_7',
|
||||
'location_id': self.pack_location.id,
|
||||
'location_dest_id': self.customer_location.id,
|
||||
'product_id': self.product2.id,
|
||||
'product_uom': self.uom_dozen.id,
|
||||
'product_uom_qty': 1.0,
|
||||
'picking_id': picking_pack_cust.id,
|
||||
})
|
||||
move_stock_pack.write({'move_dest_ids': [(4, move_pack_cust.id, 0)]})
|
||||
move_pack_cust.write({'move_orig_ids': [(4, move_stock_pack.id, 0)]})
|
||||
(move_stock_pack + move_pack_cust)._action_confirm()
|
||||
|
||||
move_stock_pack._action_assign()
|
||||
self.assertEqual(move_stock_pack.state, 'assigned')
|
||||
move_pack_cust._action_assign()
|
||||
self.assertEqual(move_pack_cust.state, 'waiting')
|
||||
|
||||
for ml in move_stock_pack.move_line_ids:
|
||||
ml.qty_done = 1
|
||||
picking_stock_pack.button_validate()
|
||||
self.assertEqual(move_pack_cust.state, 'assigned')
|
||||
for ml in move_pack_cust.move_line_ids:
|
||||
self.assertEqual(ml.product_uom_qty, 1)
|
||||
self.assertEqual(ml.product_uom_id.id, self.uom_unit.id)
|
||||
self.assertTrue(bool(ml.lot_id.id))
|
||||
|
||||
def test_link_assign_9(self):
|
||||
""" Create an uom "3 units" which is 3 times the units but without rounding. Create 3
|
||||
quants in stock and two chained moves. The first move will bring the 3 quants but the
|
||||
second only validate 2 and create a backorder for the last one. Check that the reservation
|
||||
is correctly cleared up for the last one.
|
||||
"""
|
||||
uom_3units = self.env['product.uom'].create({
|
||||
'name': '3 units',
|
||||
'category_id': self.uom_unit.category_id.id,
|
||||
'factor_inv': 3,
|
||||
'rounding': 1,
|
||||
})
|
||||
for i in range(1, 4):
|
||||
lot_id = self.env['stock.production.lot'].create({
|
||||
'name': 'lot%s' % str(i),
|
||||
'product_id': self.product2.id,
|
||||
})
|
||||
self.env['stock.quant']._update_available_quantity(self.product2, self.stock_location, 1.0, lot_id=lot_id)
|
||||
|
||||
picking_stock_pack = self.env['stock.picking'].create({
|
||||
'location_id': self.stock_location.id,
|
||||
'location_dest_id': self.pack_location.id,
|
||||
'picking_type_id': self.env.ref('stock.picking_type_internal').id,
|
||||
})
|
||||
move_stock_pack = self.env['stock.move'].create({
|
||||
'name': 'test_link_assign_9',
|
||||
'location_id': self.stock_location.id,
|
||||
'location_dest_id': self.pack_location.id,
|
||||
'product_id': self.product2.id,
|
||||
'product_uom': uom_3units.id,
|
||||
'product_uom_qty': 1.0,
|
||||
'picking_id': picking_stock_pack.id,
|
||||
})
|
||||
picking_pack_cust = self.env['stock.picking'].create({
|
||||
'location_id': self.pack_location.id,
|
||||
'location_dest_id': self.customer_location.id,
|
||||
'picking_type_id': self.env.ref('stock.picking_type_out').id,
|
||||
})
|
||||
move_pack_cust = self.env['stock.move'].create({
|
||||
'name': 'test_link_assign_0',
|
||||
'location_id': self.pack_location.id,
|
||||
'location_dest_id': self.customer_location.id,
|
||||
'product_id': self.product2.id,
|
||||
'product_uom': uom_3units.id,
|
||||
'product_uom_qty': 1.0,
|
||||
'picking_id': picking_pack_cust.id,
|
||||
})
|
||||
move_stock_pack.write({'move_dest_ids': [(4, move_pack_cust.id, 0)]})
|
||||
move_pack_cust.write({'move_orig_ids': [(4, move_stock_pack.id, 0)]})
|
||||
(move_stock_pack + move_pack_cust)._action_confirm()
|
||||
|
||||
picking_stock_pack.action_assign()
|
||||
for ml in picking_stock_pack.move_lines.move_line_ids:
|
||||
ml.qty_done = 1
|
||||
picking_stock_pack.button_validate()
|
||||
self.assertEqual(picking_pack_cust.state, 'assigned')
|
||||
for ml in picking_pack_cust.move_lines.move_line_ids:
|
||||
if ml.lot_id.name != 'lot3':
|
||||
ml.qty_done = 1
|
||||
res_dict_for_back_order = picking_pack_cust.button_validate()
|
||||
backorder_wizard = self.env[(res_dict_for_back_order.get('res_model'))].browse(res_dict_for_back_order.get('res_id'))
|
||||
backorder_wizard.process()
|
||||
backorder = self.env['stock.picking'].search([('backorder_id', '=', picking_pack_cust.id)])
|
||||
backordered_move = backorder.move_lines
|
||||
|
||||
# due to the rounding, the backordered quantity is 0.999 ; we shoudln't be able to reserve
|
||||
# 0.999 on a tracked by serial number quant
|
||||
backordered_move._action_assign()
|
||||
self.assertEqual(backordered_move.reserved_availability, 0)
|
||||
|
||||
# force the serial number and validate
|
||||
lot3 = self.env['stock.production.lot'].search([('name', '=', "lot3")])
|
||||
backorder.write({'move_line_ids': [(0, 0, {
|
||||
'product_id': self.product2.id,
|
||||
'product_uom_id': self.uom_unit.id,
|
||||
'qty_done': 1,
|
||||
'product_uom_qty': 0,
|
||||
'lot_id': lot3.id,
|
||||
'package_id': False,
|
||||
'result_package_id': False,
|
||||
'location_id': backordered_move.location_id.id,
|
||||
'location_dest_id': backordered_move.location_dest_id.id,
|
||||
'move_id': backordered_move.id,
|
||||
})]})
|
||||
|
||||
overprocessed_wizard = backorder.button_validate()
|
||||
overprocessed_wizard = self.env['stock.overprocessed.transfer'].browse(overprocessed_wizard['res_id'])
|
||||
overprocessed_wizard.action_confirm()
|
||||
|
||||
self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product2, self.customer_location), 3)
|
||||
self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product2, self.pack_location), 0)
|
||||
|
||||
def test_link_assign_10(self):
|
||||
""" Test the assignment mechanism with partial availability.
|
||||
"""
|
||||
# make some stock:
|
||||
# stock location: 2.0
|
||||
# pack location: -1.0
|
||||
self.env['stock.quant']._update_available_quantity(self.product1, self.stock_location, 2.0)
|
||||
self.assertEqual(len(self.env['stock.quant']._gather(self.product1, self.stock_location)), 1.0)
|
||||
|
||||
move_out = self.env['stock.move'].create({
|
||||
'name': 'test_link_assign_out',
|
||||
'location_id': self.pack_location.id,
|
||||
'location_dest_id': self.customer_location.id,
|
||||
'product_id': self.product1.id,
|
||||
'product_uom': self.uom_unit.id,
|
||||
'product_uom_qty': 1.0,
|
||||
})
|
||||
move_out._action_confirm()
|
||||
move_out._action_assign()
|
||||
move_out.quantity_done = 1.0
|
||||
move_out._action_done()
|
||||
self.assertEqual(len(self.env['stock.quant']._gather(self.product1, self.pack_location)), 1.0)
|
||||
|
||||
move_stock_pack = self.env['stock.move'].create({
|
||||
'name': 'test_link_assign_1_1',
|
||||
'location_id': self.stock_location.id,
|
||||
'location_dest_id': self.pack_location.id,
|
||||
'product_id': self.product1.id,
|
||||
'product_uom': self.uom_unit.id,
|
||||
'product_uom_qty': 2.0,
|
||||
})
|
||||
move_pack_cust = self.env['stock.move'].create({
|
||||
'name': 'test_link_assign_1_2',
|
||||
'location_id': self.pack_location.id,
|
||||
'location_dest_id': self.customer_location.id,
|
||||
'product_id': self.product1.id,
|
||||
'product_uom': self.uom_unit.id,
|
||||
'product_uom_qty': 2.0,
|
||||
})
|
||||
move_stock_pack.write({'move_dest_ids': [(4, move_pack_cust.id, 0)]})
|
||||
move_pack_cust.write({'move_orig_ids': [(4, move_stock_pack.id, 0)]})
|
||||
|
||||
(move_stock_pack + move_pack_cust)._action_confirm()
|
||||
move_stock_pack._action_assign()
|
||||
move_stock_pack.quantity_done = 2.0
|
||||
move_stock_pack._action_done()
|
||||
self.assertEqual(len(move_pack_cust.move_line_ids), 1)
|
||||
|
||||
self.assertAlmostEqual(move_pack_cust.reserved_availability, 1.0)
|
||||
self.assertEqual(move_pack_cust.state, 'partially_available')
|
||||
|
||||
def test_use_unreserved_move_line_1(self):
|
||||
""" Test that validating a stock move linked to an untracked product reserved by another one
|
||||
correctly unreserves the other one.
|
||||
@ -1784,7 +2261,7 @@ class StockMove(TransactionCase):
|
||||
self.assertEqual(move_line.product_qty, 5)
|
||||
move_line.qty_done = 5.0
|
||||
self.assertEqual(move_line.product_qty, 5) # don't change reservation
|
||||
move_line.with_context(debug=True).lot_id = lot1
|
||||
move_line.lot_id = lot1
|
||||
self.assertEqual(move_line.product_qty, 5) # don't change reservation when assgning a lot now
|
||||
|
||||
move1._action_done()
|
||||
@ -2598,9 +3075,9 @@ class StockMove(TransactionCase):
|
||||
return picking
|
||||
|
||||
def test_immediate_validate_5(self):
|
||||
""" Create a picking and simulates validate button effect.
|
||||
Test that tracked products can be received without specifying a serial
|
||||
number when the picking type is configured that way.
|
||||
""" In a receipt with a single tracked by serial numbers move, clicking on validate without
|
||||
filling any quantities nor lot should open an UserError except if the picking type is
|
||||
configured to allow otherwise.
|
||||
"""
|
||||
picking_type_id = self.env.ref('stock.picking_type_in')
|
||||
product_id = self.product2
|
||||
@ -3166,39 +3643,6 @@ class StockMove(TransactionCase):
|
||||
self.assertEqual(self.product1.qty_available, 5.0)
|
||||
self.assertEqual(self.product1.with_context(company_owned=True).qty_available, 10.0)
|
||||
|
||||
def test_split_1(self):
|
||||
""" When we split a move line and having one without quantity done, we want to keep reservation
|
||||
on the new one as it has not been unreserved during the copy.
|
||||
"""
|
||||
move1 = self.env['stock.move'].create({
|
||||
'name': 'test_split_1',
|
||||
'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': 10.0,
|
||||
'picking_type_id': self.env.ref('stock.picking_type_in').id,
|
||||
})
|
||||
|
||||
self.env['stock.quant']._update_available_quantity(self.product1, self.stock_location, 10)
|
||||
|
||||
move1._action_confirm()
|
||||
move1._action_assign()
|
||||
move_line = move1.move_line_ids
|
||||
|
||||
default = {'product_uom_qty': 3,
|
||||
'qty_done': 3}
|
||||
move_line.copy(default=default)
|
||||
move_line.with_context(bypass_reservation_update=True).write({'product_uom_qty': 7, 'qty_done': 0})
|
||||
move1._action_done()
|
||||
|
||||
new_move = self.env['stock.move'].search([('name', '=', 'test_split_1'), ('state', '=', 'confirmed')])
|
||||
|
||||
self.assertEqual(move1.move_line_ids.product_uom_qty, 0.0)
|
||||
self.assertEqual(move1.move_line_ids.qty_done, 3.0)
|
||||
self.assertEqual(new_move.move_line_ids.product_uom_qty, 7.0)
|
||||
self.assertEqual(new_move.move_line_ids.qty_done, 0.0)
|
||||
|
||||
def test_edit_initial_demand_1(self):
|
||||
""" Increase initial demand once everything is reserved and check if
|
||||
the existing move_line is updated.
|
||||
@ -3410,4 +3854,3 @@ class StockMove(TransactionCase):
|
||||
picking.button_validate()
|
||||
self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product1, self.stock_location), 0)
|
||||
self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product1, self.customer_location), 2)
|
||||
|
||||
|
@ -185,6 +185,73 @@ class TestPickShip(TestStockCommon):
|
||||
# the client picking should not be assigned anymore, as we returned partially what we took
|
||||
self.assertEqual(picking_client.state, 'confirmed')
|
||||
|
||||
def test_mto_moves_return_return(self):
|
||||
picking_pick, picking_client = self.create_pick_ship()
|
||||
stock_location = self.env['stock.location'].browse(self.stock_location)
|
||||
lot = self.env['stock.production.lot'].create({
|
||||
'product_id': self.productA.id,
|
||||
'name': '123456789'
|
||||
})
|
||||
self.env['stock.quant']._update_available_quantity(self.productA, stock_location, 10.0, lot_id=lot)
|
||||
|
||||
picking_pick.action_assign()
|
||||
picking_pick.move_lines[0].move_line_ids[0].qty_done = 10.0
|
||||
picking_pick.button_validate()
|
||||
self.assertEqual(picking_pick.state, 'done')
|
||||
self.assertEqual(picking_client.state, 'assigned')
|
||||
|
||||
# return this picking
|
||||
stock_return_picking = self.env['stock.return.picking']\
|
||||
.with_context(active_ids=picking_pick.ids, active_id=picking_pick.ids[0])\
|
||||
.create({})
|
||||
stock_return_picking.product_return_moves.quantity = 10.0
|
||||
stock_return_picking_action = stock_return_picking.create_returns()
|
||||
return_pick = self.env['stock.picking'].browse(stock_return_picking_action['res_id'])
|
||||
return_pick.move_lines[0].move_line_ids[0].write({
|
||||
'qty_done': 10.0,
|
||||
'lot_id': lot.id,
|
||||
})
|
||||
return_pick.button_validate()
|
||||
# return this return of this picking
|
||||
stock_return_picking = self.env['stock.return.picking']\
|
||||
.with_context(active_id=return_pick.id)\
|
||||
.create({})
|
||||
stock_return_picking.product_return_moves.quantity = 10.0
|
||||
stock_return_picking_action = stock_return_picking.create_returns()
|
||||
return_return_pick = self.env['stock.picking'].browse(stock_return_picking_action['res_id'])
|
||||
return_return_pick.move_lines[0].move_line_ids[0].write({
|
||||
'qty_done': 10.0,
|
||||
'lot_id': lot.id,
|
||||
})
|
||||
return_return_pick.button_validate()
|
||||
# test computation of traceability
|
||||
vals = {
|
||||
'line_id': 1,
|
||||
'model_name': 'stock.move.line',
|
||||
'level': 11,
|
||||
'parent_quant': False,
|
||||
}
|
||||
lines = self.env['stock.traceability.report'].get_lines(
|
||||
model_id=return_return_pick.move_line_ids[0].id,
|
||||
stream='upstream',
|
||||
**vals
|
||||
)
|
||||
self.assertEqual(
|
||||
[l.get('res_id') for l in lines],
|
||||
[return_return_pick.id, return_pick.id, picking_pick.id],
|
||||
"Upstream computation from return of return worked"
|
||||
)
|
||||
lines = self.env['stock.traceability.report'].get_lines(
|
||||
model_id=picking_pick.move_line_ids[0].id,
|
||||
stream='downstream',
|
||||
**vals
|
||||
)
|
||||
self.assertEqual(
|
||||
[l.get('res_id') for l in lines],
|
||||
[picking_pick.id, return_pick.id, return_return_pick.id],
|
||||
"Downstream computation from original picking worked"
|
||||
)
|
||||
|
||||
def test_mto_resupply_cancel_ship(self):
|
||||
""" This test simulates a pick pack ship with a resupply route
|
||||
set. Pick and pack are validated, ship is cancelled. This test
|
||||
@ -963,6 +1030,82 @@ 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_extra_move_5(self):
|
||||
""" Create a picking a move that is problematic with
|
||||
rounding (5.95 - 5.5 = 0.4500000000000002). Ensure that
|
||||
initial demand is corrct afer action_done and backoder
|
||||
are not created.
|
||||
"""
|
||||
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.5,
|
||||
'quantity_done': 5.95,
|
||||
'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.5)
|
||||
delivery.action_confirm()
|
||||
delivery.action_assign()
|
||||
delivery.action_done()
|
||||
self.assertEqual(delivery.move_lines.product_uom_qty, 5.95, 'Move initial demand should be 5.95')
|
||||
|
||||
back_order = self.env['stock.picking'].search([('backorder_id', '=', delivery.id)])
|
||||
self.assertFalse(back_order, 'There should be no back order')
|
||||
|
||||
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
|
||||
|
@ -629,7 +629,24 @@ class StockQuant(TransactionCase):
|
||||
})
|
||||
quantity, in_date = self.env['stock.quant']._update_available_quantity(product1, stock_location, 1.0)
|
||||
self.assertEqual(quantity, 1)
|
||||
self.assertEqual(in_date, None)
|
||||
self.assertNotEqual(in_date, None)
|
||||
|
||||
|
||||
def test_in_date_1b(self):
|
||||
stock_location = self.env.ref('stock.stock_location_stock')
|
||||
product1 = self.env['product.product'].create({
|
||||
'name': 'Product A',
|
||||
'type': 'product',
|
||||
})
|
||||
self.env['stock.quant'].create({
|
||||
'product_id': product1.id,
|
||||
'location_id': stock_location.id,
|
||||
'quantity': 1.0,
|
||||
})
|
||||
quantity, in_date = self.env['stock.quant']._update_available_quantity(product1, stock_location, 2.0)
|
||||
self.assertEqual(quantity, 3)
|
||||
self.assertNotEqual(in_date, None)
|
||||
|
||||
|
||||
def test_in_date_2(self):
|
||||
""" Check that an incoming date is correctly set when updating the quantity of a tracked
|
||||
@ -707,6 +724,34 @@ class StockQuant(TransactionCase):
|
||||
# Removal strategy is LIFO, so lot1 should be received as it was received later.
|
||||
self.assertEqual(quants[0][0].lot_id.id, lot1.id)
|
||||
|
||||
def test_in_date_4b(self):
|
||||
""" Check for LIFO and max with/without in_date that it handles the LIFO NULLS LAST well
|
||||
"""
|
||||
stock_location = self.env.ref('stock.stock_location_stock')
|
||||
stock_location1 = self.env.ref('stock.stock_location_components')
|
||||
stock_location2 = self.env.ref('stock.stock_location_14')
|
||||
lifo_strategy = self.env['product.removal'].search([('method', '=', 'lifo')])
|
||||
stock_location.removal_strategy_id = lifo_strategy
|
||||
product1 = self.env['product.product'].create({
|
||||
'name': 'Product A',
|
||||
'type': 'product',
|
||||
'tracking': 'serial',
|
||||
})
|
||||
|
||||
self.env['stock.quant'].create({
|
||||
'product_id': product1.id,
|
||||
'location_id': stock_location1.id,
|
||||
'quantity': 1.0,
|
||||
})
|
||||
|
||||
in_date_location2 = datetime.now()
|
||||
self.env['stock.quant']._update_available_quantity(product1, stock_location2, 1.0, in_date=in_date_location2)
|
||||
|
||||
quants = self.env['stock.quant']._update_reserved_quantity(product1, stock_location, 1)
|
||||
|
||||
# Removal strategy is LIFO, so the one with date is the most recent one and should be selected
|
||||
self.assertEqual(quants[0][0].location_id.id, stock_location2.id)
|
||||
|
||||
def test_in_date_5(self):
|
||||
""" Receive the same lot at different times, once they're in the same location, the quants
|
||||
are merged and only the earliest incoming date is kept.
|
||||
|
147
addons/stock/tests/test_robustness.py
Normal file
147
addons/stock/tests/test_robustness.py
Normal file
@ -0,0 +1,147 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo, Flectra. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from flectra.exceptions import UserError
|
||||
from flectra.tests.common import TransactionCase
|
||||
|
||||
|
||||
class TestRobustness(TransactionCase):
|
||||
def setUp(self):
|
||||
super(TestRobustness, self).setUp()
|
||||
self.stock_location = self.env.ref('stock.stock_location_stock')
|
||||
self.customer_location = self.env.ref('stock.stock_location_customers')
|
||||
self.uom_unit = self.env.ref('product.product_uom_unit')
|
||||
self.uom_dozen = self.env.ref('product.product_uom_dozen')
|
||||
self.product1 = self.env['product.product'].create({
|
||||
'name': 'Product A',
|
||||
'type': 'product',
|
||||
'categ_id': self.env.ref('product.product_category_all').id,
|
||||
})
|
||||
|
||||
def test_uom_factor(self):
|
||||
""" Changing the factor of a unit of measure shouldn't be allowed while
|
||||
quantities are reserved, else the existing move lines won't be consistent
|
||||
with the `reserved_quantity` on quants.
|
||||
"""
|
||||
# make some stock
|
||||
self.env['stock.quant']._update_available_quantity(
|
||||
self.product1,
|
||||
self.stock_location,
|
||||
12,
|
||||
)
|
||||
|
||||
# reserve a dozen
|
||||
move1 = self.env['stock.move'].create({
|
||||
'name': 'test_uom_rounding',
|
||||
'location_id': self.stock_location.id,
|
||||
'location_dest_id': self.customer_location.id,
|
||||
'product_id': self.product1.id,
|
||||
'product_uom': self.uom_dozen.id,
|
||||
'product_uom_qty': 1,
|
||||
})
|
||||
move1._action_confirm()
|
||||
move1._action_assign()
|
||||
self.assertEqual(move1.state, 'assigned')
|
||||
quant = self.env['stock.quant']._gather(
|
||||
self.product1,
|
||||
self.stock_location,
|
||||
)
|
||||
|
||||
# assert the reservation
|
||||
self.assertEqual(quant.reserved_quantity, 12)
|
||||
self.assertEqual(move1.product_qty, 12)
|
||||
|
||||
# change the factor
|
||||
with self.assertRaises(UserError):
|
||||
with self.cr.savepoint():
|
||||
move1.product_uom.factor = 0.05
|
||||
|
||||
# assert the reservation
|
||||
self.assertEqual(quant.reserved_quantity, 12)
|
||||
self.assertEqual(move1.state, 'assigned')
|
||||
self.assertEqual(move1.product_qty, 12)
|
||||
|
||||
# unreserve
|
||||
move1._do_unreserve()
|
||||
|
||||
def test_location_usage(self):
|
||||
""" Changing the usage of a location shouldn't be allowed while
|
||||
quantities are reserved, else the existing move lines won't be
|
||||
consistent with the `reserved_quantity` on the quants.
|
||||
"""
|
||||
# change stock usage
|
||||
self.stock_location.scrap_location = True
|
||||
|
||||
# make some stock
|
||||
self.env['stock.quant']._update_available_quantity(
|
||||
self.product1,
|
||||
self.stock_location,
|
||||
1,
|
||||
)
|
||||
|
||||
# reserve a unit
|
||||
move1 = self.env['stock.move'].create({
|
||||
'name': 'test_location_archive',
|
||||
'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': 1,
|
||||
})
|
||||
move1._action_confirm()
|
||||
move1._action_assign()
|
||||
self.assertEqual(move1.state, 'assigned')
|
||||
quant = self.env['stock.quant']._gather(
|
||||
self.product1,
|
||||
self.stock_location,
|
||||
)
|
||||
|
||||
# assert the reservation
|
||||
self.assertEqual(quant.reserved_quantity, 0) # reservation is bypassed in scrap location
|
||||
self.assertEqual(move1.product_qty, 1)
|
||||
|
||||
# change the stock usage
|
||||
with self.assertRaises(UserError):
|
||||
with self.cr.savepoint():
|
||||
self.stock_location.scrap_location = False
|
||||
|
||||
# unreserve
|
||||
move1._do_unreserve()
|
||||
|
||||
def test_package_unpack(self):
|
||||
""" Unpack a package that contains quants with a reservation
|
||||
should also remove the package on the reserved move lines.
|
||||
"""
|
||||
package = self.env['stock.quant.package'].create({
|
||||
'name': 'Shell Helix HX7 10W30',
|
||||
})
|
||||
|
||||
self.env['stock.quant']._update_available_quantity(
|
||||
self.product1,
|
||||
self.stock_location,
|
||||
10,
|
||||
package_id=package
|
||||
)
|
||||
|
||||
# reserve a dozen
|
||||
move1 = self.env['stock.move'].create({
|
||||
'name': 'test_uom_rounding',
|
||||
'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': 10,
|
||||
})
|
||||
move1._action_confirm()
|
||||
move1._action_assign()
|
||||
|
||||
move1.result_package_id = False
|
||||
|
||||
package.unpack()
|
||||
|
||||
# unreserve
|
||||
move1._do_unreserve()
|
||||
self.assertEqual(len(self.env['stock.quant']._gather(self.product1, self.stock_location)), 1)
|
||||
self.assertEqual(len(self.env['stock.quant']._gather(self.product1, self.stock_location, package_id=package)), 0)
|
||||
|
||||
self.assertEqual(self.env['stock.quant']._gather(self.product1, self.stock_location).reserved_quantity, 0)
|
@ -229,6 +229,215 @@ class TestWarehouse(TestStockCommon):
|
||||
quant = self.env['stock.quant'].search([('product_id', '=', productA.id), ('location_id', '=', location_loss.id)])
|
||||
self.assertEqual(len(quant), 1)
|
||||
|
||||
def test_resupply_route(self):
|
||||
""" Simulate a resupply chain between warehouses.
|
||||
Stock -> transit -> Dist. -> transit -> Shop -> Customer
|
||||
Create the move from Shop to Customer and ensure that all the pull
|
||||
rules are triggered in order to complete the move chain to Stock.
|
||||
"""
|
||||
warehouse_stock = self.env['stock.warehouse'].create({
|
||||
'name': 'Stock.',
|
||||
'code': 'STK',
|
||||
})
|
||||
|
||||
warehouse_distribution = self.env['stock.warehouse'].create({
|
||||
'name': 'Dist.',
|
||||
'code': 'DIST',
|
||||
'default_resupply_wh_id': warehouse_stock.id,
|
||||
'resupply_wh_ids': [(6, 0, [warehouse_stock.id])]
|
||||
})
|
||||
|
||||
warehouse_shop = self.env['stock.warehouse'].create({
|
||||
'name': 'Shop',
|
||||
'code': 'SHOP',
|
||||
'default_resupply_wh_id': warehouse_distribution.id,
|
||||
'resupply_wh_ids': [(6, 0, [warehouse_distribution.id])]
|
||||
})
|
||||
|
||||
route_stock_to_dist = warehouse_distribution.resupply_route_ids
|
||||
route_dist_to_shop = warehouse_shop.resupply_route_ids
|
||||
|
||||
# Change the procure_method on the pull rules between dist and shop
|
||||
# warehouses. Since mto and resupply routes are both on product it will
|
||||
# select one randomly between them and if it select the resupply it is
|
||||
# 'make to stock' and it will not create the picking between stock and
|
||||
# dist warehouses.
|
||||
route_dist_to_shop.pull_ids.write({'procure_method': 'make_to_order'})
|
||||
|
||||
product = self.env['product.product'].create({
|
||||
'name': 'Fakir',
|
||||
'type': 'product',
|
||||
'route_ids': [(4, route_id) for route_id in [route_stock_to_dist.id, route_dist_to_shop.id, self.env.ref('stock.route_warehouse0_mto').id]],
|
||||
})
|
||||
|
||||
picking_out = self.env['stock.picking'].create({
|
||||
'partner_id': self.env.ref('base.res_partner_2').id,
|
||||
'picking_type_id': self.env.ref('stock.picking_type_out').id,
|
||||
'location_id': warehouse_shop.lot_stock_id.id,
|
||||
'location_dest_id': self.env.ref('stock.stock_location_customers').id,
|
||||
})
|
||||
self.env['stock.move'].create({
|
||||
'name': product.name,
|
||||
'product_id': product.id,
|
||||
'product_uom_qty': 1,
|
||||
'product_uom': product.uom_id.id,
|
||||
'picking_id': picking_out.id,
|
||||
'location_id': warehouse_shop.lot_stock_id.id,
|
||||
'location_dest_id': self.env.ref('stock.stock_location_customers').id,
|
||||
'warehouse_id': warehouse_shop.id,
|
||||
'procure_method': 'make_to_order',
|
||||
})
|
||||
picking_out.action_confirm()
|
||||
|
||||
moves = self.env['stock.move'].search([('product_id', '=', product.id)])
|
||||
# Shop/Stock -> Customer
|
||||
# Transit -> Shop/Stock
|
||||
# Dist/Stock -> Transit
|
||||
# Transit -> Dist/Stock
|
||||
# Stock/Stock -> Transit
|
||||
self.assertEqual(len(moves), 5, 'Invalid moves number.')
|
||||
self.assertTrue(self.env['stock.move'].search([('location_id', '=', warehouse_stock.lot_stock_id.id)]))
|
||||
self.assertTrue(self.env['stock.move'].search([('location_dest_id', '=', warehouse_distribution.lot_stock_id.id)]))
|
||||
self.assertTrue(self.env['stock.move'].search([('location_id', '=', warehouse_distribution.lot_stock_id.id)]))
|
||||
self.assertTrue(self.env['stock.move'].search([('location_dest_id', '=', warehouse_shop.lot_stock_id.id)]))
|
||||
self.assertTrue(self.env['stock.move'].search([('location_id', '=', warehouse_shop.lot_stock_id.id)]))
|
||||
|
||||
def test_mutiple_resupply_warehouse(self):
|
||||
""" Simulate the following situation:
|
||||
- 2 shops with stock are resupply by 2 distinct warehouses
|
||||
- Shop Namur is resupply by the warehouse stock Namur
|
||||
- Shop Wavre is resupply by the warehouse stock Wavre
|
||||
- Simulate 2 moves for the same product but in different shop.
|
||||
This test ensure that the move are supplied by the correct distribution
|
||||
warehouse.
|
||||
"""
|
||||
customer_location = self.env.ref('stock.stock_location_customers')
|
||||
|
||||
warehouse_distribution_wavre = self.env['stock.warehouse'].create({
|
||||
'name': 'Stock Wavre.',
|
||||
'code': 'WV',
|
||||
})
|
||||
|
||||
warehouse_shop_wavre = self.env['stock.warehouse'].create({
|
||||
'name': 'Shop Wavre',
|
||||
'code': 'SHWV',
|
||||
'default_resupply_wh_id': warehouse_distribution_wavre.id,
|
||||
'resupply_wh_ids': [(6, 0, [warehouse_distribution_wavre.id])]
|
||||
})
|
||||
|
||||
warehouse_distribution_namur = self.env['stock.warehouse'].create({
|
||||
'name': 'Stock Namur.',
|
||||
'code': 'NM',
|
||||
})
|
||||
|
||||
warehouse_shop_namur = self.env['stock.warehouse'].create({
|
||||
'name': 'Shop Namur',
|
||||
'code': 'SHNM',
|
||||
'default_resupply_wh_id': warehouse_distribution_namur.id,
|
||||
'resupply_wh_ids': [(6, 0, [warehouse_distribution_namur.id])]
|
||||
})
|
||||
|
||||
route_shop_namur = warehouse_shop_namur.resupply_route_ids
|
||||
route_shop_wavre = warehouse_shop_wavre.resupply_route_ids
|
||||
|
||||
# The product contains the 2 resupply routes.
|
||||
product = self.env['product.product'].create({
|
||||
'name': 'Fakir',
|
||||
'type': 'product',
|
||||
'route_ids': [(4, route_id) for route_id in [route_shop_namur.id, route_shop_wavre.id, self.env.ref('stock.route_warehouse0_mto').id]],
|
||||
})
|
||||
|
||||
# Add 1 quant in each distribution warehouse.
|
||||
self.env['stock.quant']._update_available_quantity(product, warehouse_distribution_wavre.lot_stock_id, 1.0)
|
||||
self.env['stock.quant']._update_available_quantity(product, warehouse_distribution_namur.lot_stock_id, 1.0)
|
||||
|
||||
# Create the move for the shop Namur. Should create a resupply from
|
||||
# distribution warehouse Namur.
|
||||
picking_out_namur = self.env['stock.picking'].create({
|
||||
'partner_id': self.env.ref('base.res_partner_2').id,
|
||||
'picking_type_id': self.env.ref('stock.picking_type_out').id,
|
||||
'location_id': warehouse_shop_namur.lot_stock_id.id,
|
||||
'location_dest_id': customer_location.id,
|
||||
})
|
||||
self.env['stock.move'].create({
|
||||
'name': product.name,
|
||||
'product_id': product.id,
|
||||
'product_uom_qty': 1,
|
||||
'product_uom': product.uom_id.id,
|
||||
'picking_id': picking_out_namur.id,
|
||||
'location_id': warehouse_shop_namur.lot_stock_id.id,
|
||||
'location_dest_id': customer_location.id,
|
||||
'warehouse_id': warehouse_shop_namur.id,
|
||||
'procure_method': 'make_to_order',
|
||||
})
|
||||
picking_out_namur.action_confirm()
|
||||
|
||||
# Validate the picking
|
||||
# Dist. warehouse Namur -> transit Location -> Shop Namur
|
||||
picking_stock_transit = self.env['stock.picking'].search([('location_id', '=', warehouse_distribution_namur.lot_stock_id.id)])
|
||||
self.assertTrue(picking_stock_transit)
|
||||
picking_stock_transit.action_assign()
|
||||
picking_stock_transit.move_lines[0].quantity_done = 1.0
|
||||
picking_stock_transit.action_done()
|
||||
|
||||
picking_transit_shop_namur = self.env['stock.picking'].search([('location_dest_id', '=', warehouse_shop_namur.lot_stock_id.id)])
|
||||
self.assertTrue(picking_transit_shop_namur)
|
||||
picking_transit_shop_namur.action_assign()
|
||||
picking_transit_shop_namur.move_lines[0].quantity_done = 1.0
|
||||
picking_transit_shop_namur.action_done()
|
||||
|
||||
picking_out_namur.action_assign()
|
||||
picking_out_namur.move_lines[0].quantity_done = 1.0
|
||||
picking_out_namur.action_done()
|
||||
|
||||
# Check that the correct quantity has been provided to customer
|
||||
self.assertEqual(self.env['stock.quant']._gather(product, customer_location).quantity, 1)
|
||||
# Ensure there still no quants in distribution warehouse
|
||||
self.assertEqual(len(self.env['stock.quant']._gather(product, warehouse_distribution_namur.lot_stock_id)), 0)
|
||||
|
||||
# Create the move for the shop Wavre. Should create a resupply from
|
||||
# distribution warehouse Wavre.
|
||||
picking_out_wavre = self.env['stock.picking'].create({
|
||||
'partner_id': self.env.ref('base.res_partner_2').id,
|
||||
'picking_type_id': self.env.ref('stock.picking_type_out').id,
|
||||
'location_id': warehouse_shop_wavre.lot_stock_id.id,
|
||||
'location_dest_id': customer_location.id,
|
||||
})
|
||||
self.env['stock.move'].create({
|
||||
'name': product.name,
|
||||
'product_id': product.id,
|
||||
'product_uom_qty': 1,
|
||||
'product_uom': product.uom_id.id,
|
||||
'picking_id': picking_out_wavre.id,
|
||||
'location_id': warehouse_shop_wavre.lot_stock_id.id,
|
||||
'location_dest_id': customer_location.id,
|
||||
'warehouse_id': warehouse_shop_wavre.id,
|
||||
'procure_method': 'make_to_order',
|
||||
})
|
||||
picking_out_wavre.action_confirm()
|
||||
|
||||
# Validate the picking
|
||||
# Dist. warehouse Wavre -> transit Location -> Shop Wavre
|
||||
picking_stock_transit = self.env['stock.picking'].search([('location_id', '=', warehouse_distribution_wavre.lot_stock_id.id)])
|
||||
self.assertTrue(picking_stock_transit)
|
||||
picking_stock_transit.action_assign()
|
||||
picking_stock_transit.move_lines[0].quantity_done = 1.0
|
||||
picking_stock_transit.action_done()
|
||||
|
||||
picking_transit_shop_wavre = self.env['stock.picking'].search([('location_dest_id', '=', warehouse_shop_wavre.lot_stock_id.id)])
|
||||
self.assertTrue(picking_transit_shop_wavre)
|
||||
picking_transit_shop_wavre.action_assign()
|
||||
picking_transit_shop_wavre.move_lines[0].quantity_done = 1.0
|
||||
picking_transit_shop_wavre.action_done()
|
||||
|
||||
picking_out_wavre.action_assign()
|
||||
picking_out_wavre.move_lines[0].quantity_done = 1.0
|
||||
picking_out_wavre.action_done()
|
||||
|
||||
# Check that the correct quantity has been provided to customer
|
||||
self.assertEqual(self.env['stock.quant']._gather(product, customer_location).quantity, 2)
|
||||
# Ensure there still no quants in distribution warehouse
|
||||
self.assertEqual(len(self.env['stock.quant']._gather(product, warehouse_distribution_wavre.lot_stock_id)), 0)
|
||||
|
||||
class TestResupply(TestStockCommon):
|
||||
def setUp(self):
|
||||
|
@ -78,6 +78,7 @@
|
||||
<group string="Applied On">
|
||||
<field name="location_id"/>
|
||||
<field name="warehouse_id" groups="base.group_no_one"/>
|
||||
<field name="company_id" options="{'no_create': True}" groups="base.group_multi_company"/>
|
||||
</group>
|
||||
<group string="Creates">
|
||||
<field name="location_src_id" attrs="{'required': [('action', '=', 'move')], 'invisible':[('action', '!=', 'move')]}" domain="[('usage','!=','view')]"/>
|
||||
|
@ -273,7 +273,7 @@
|
||||
<field name="push_ids" colspan="4" nolabel="1"/>
|
||||
</group>
|
||||
<group string="Procurement Rules" colspan="4" >
|
||||
<field name="pull_ids" colspan="4" nolabel="1"/>
|
||||
<field name="pull_ids" colspan="4" nolabel="1" context="{'default_company_id': company_id}"/>
|
||||
</group>
|
||||
</sheet>
|
||||
</form>
|
||||
@ -285,6 +285,7 @@
|
||||
<field name="model">stock.location.route</field>
|
||||
<field name="arch" type="xml">
|
||||
<search string="Route">
|
||||
<field name="name"/>
|
||||
<filter string="Archived" name="inactive" domain="[('active','=',False)]"/>
|
||||
</search>
|
||||
</field>
|
||||
|
@ -25,6 +25,8 @@
|
||||
<field name="state" widget="statusbar"/>
|
||||
</header>
|
||||
<sheet>
|
||||
<field name="in_entire_package" invisible="1"/>
|
||||
<field name="picking_id" invisible="1"/>
|
||||
<group>
|
||||
<group>
|
||||
<field name="date"/>
|
||||
@ -44,7 +46,8 @@
|
||||
<field name="qty_done"/>
|
||||
<field name="product_uom_id" options="{'no_create': True}" string="Unit of Measure" groups="product.group_uom"/>
|
||||
</div>
|
||||
<field name="lot_id" string="Lot/Serial Number" groups="stock.group_production_lot"/>
|
||||
<field name="lot_id" attrs="{'readonly': [('in_entire_package', '=', True)]}" domain="[('product_id', '=', product_id)]" groups="stock.group_production_lot" context="{'default_product_id': product_id, 'active_picking_id': picking_id}"/>
|
||||
<field name="lot_name" attrs="{'readonly': [('in_entire_package', '=', True)]}" groups="stock.group_production_lot"/>
|
||||
<field name="package_id" string="Source Package" groups="product.group_stock_packaging"/>
|
||||
<field name="result_package_id" string="Destination Package" groups="stock.group_tracking_lot"/>
|
||||
<field name="owner_id" string="Owner" groups="stock.group_tracking_owner"/>
|
||||
@ -86,6 +89,7 @@
|
||||
<field name="model">stock.move.line</field>
|
||||
<field name="arch" type="xml">
|
||||
<kanban class="o_kanban_mobile">
|
||||
<field name="in_entire_package"/>
|
||||
<templates>
|
||||
<t t-name="kanban-box">
|
||||
<div t-attf-class="oe_kanban_card oe_kanban_global_click">
|
||||
@ -93,6 +97,8 @@
|
||||
<field name="picking_id"/>
|
||||
<div class="row">
|
||||
<div class="col-xs-6">
|
||||
<field name="lot_id" invisible="not context.get('show_lots_m2o')" domain="[('product_id', '=', product_id)]" groups="stock.group_production_lot" context="{'default_product_id': product_id, 'active_picking_id': picking_id}"/>
|
||||
<field name="lot_name" invisible="not context.get('show_lots_text')" groups="stock.group_production_lot"/>
|
||||
<field name="qty_done" string="Quantity Done"/>
|
||||
<field name="product_uom_id" string="Unit of Measure" groups="product.group_uom"/>
|
||||
</div>
|
||||
|
@ -77,9 +77,9 @@
|
||||
<kanban class="o_kanban_mobile">
|
||||
<field name="name"/>
|
||||
<field name="product_id"/>
|
||||
<field name="date"/>
|
||||
<field name="priority"/>
|
||||
<field name="state"/>
|
||||
<field name="show_details_visible"/>
|
||||
<templates>
|
||||
<t t-name="kanban-box">
|
||||
<div t-attf-class="oe_kanban_global_click">
|
||||
@ -93,7 +93,11 @@
|
||||
<field name="product_id"/>
|
||||
</div>
|
||||
<div class="o_kanban_record_bottom">
|
||||
<div class="oe_kanban_bottom_left"/>
|
||||
<div class="oe_kanban_bottom_left">
|
||||
<button name="action_show_details" string="Register lots, packs, location"
|
||||
class="o_icon_button fa fa-list" type="object"
|
||||
attrs="{'invisible': [('show_details_visible', '=', False)]}" options='{"warn": true}'/>
|
||||
</div>
|
||||
<div class="oe_kanban_bottom_right">
|
||||
<span><field name="product_uom_qty"/></span>
|
||||
</div>
|
||||
|
@ -26,17 +26,6 @@
|
||||
<field name="value">manual_periodic</field>
|
||||
<field name="type">selection</field>
|
||||
</record>
|
||||
<record id="action_stock_account_valuation_report" model="ir.actions.server">
|
||||
<field name="name">Stock Valuation Report</field>
|
||||
<field name="type">ir.actions.server</field>
|
||||
<field name="state">code</field>
|
||||
<field name="model_id" ref="product.model_product_product"></field>
|
||||
<field name="binding_model_id" ref="product.model_product_product"></field>
|
||||
<field name="code">
|
||||
env['stock.move']._run_fifo_vacuum()
|
||||
action = env.ref('stock_account.product_valuation_action').read()[0]
|
||||
</field>
|
||||
</record>
|
||||
</data>
|
||||
</flectra>
|
||||
|
||||
|
@ -55,10 +55,10 @@
|
||||
'fields_id': field_id,
|
||||
'value': value,
|
||||
}
|
||||
properties = PropertyObj.search([('name', '=', record), ('company_id', '=', company.id)])
|
||||
if properties:
|
||||
# the property exist: modify it
|
||||
properties.write(vals)
|
||||
else:
|
||||
properties = PropertyObj.search([('name', '=', record), ('company_id', '=', company.id)], limit=1)
|
||||
if not properties:
|
||||
# create the property
|
||||
PropertyObj.create(vals)
|
||||
elif not properties.value_reference:
|
||||
# update the property if False
|
||||
properties.write(vals)
|
||||
|
@ -63,7 +63,7 @@ class ProductTemplate(models.Model):
|
||||
if self.property_cost_method == 'fifo' and self.cost_method in ['average', 'standard']:
|
||||
# Cannot use the `stock_value` computed field as it's already invalidated when
|
||||
# entering this method.
|
||||
valuation = sum([variant._sum_remaining_values() for variant in self.product_variant_ids])
|
||||
valuation = sum([variant._sum_remaining_values()[0] for variant in self.product_variant_ids])
|
||||
qty_available = self.with_context(company_owned=True).qty_available
|
||||
if qty_available:
|
||||
self.standard_price = valuation / qty_available
|
||||
@ -85,16 +85,7 @@ class ProductTemplate(models.Model):
|
||||
|
||||
@api.multi
|
||||
def action_open_product_moves(self):
|
||||
self.ensure_one()
|
||||
action = self.env.ref('stock_account.stock_move_valuation_action').read()[0]
|
||||
action['domain'] = [('product_tmpl_id', '=', self.id)]
|
||||
action['context'] = {
|
||||
'search_default_outgoing': True,
|
||||
'search_default_incoming': True,
|
||||
'search_default_done': True,
|
||||
'is_avg': self.cost_method == 'average',
|
||||
}
|
||||
return action
|
||||
pass
|
||||
|
||||
@api.multi
|
||||
def get_product_accounts(self, fiscal_pos=None):
|
||||
@ -111,6 +102,12 @@ class ProductProduct(models.Model):
|
||||
|
||||
stock_value = fields.Float(
|
||||
'Value', compute='_compute_stock_value')
|
||||
qty_at_date = fields.Float(
|
||||
'Quantity', compute='_compute_stock_value')
|
||||
stock_fifo_real_time_aml_ids = fields.Many2many(
|
||||
'account.move.line', compute='_compute_stock_value')
|
||||
stock_fifo_manual_move_ids = fields.Many2many(
|
||||
'stock.move', compute='_compute_stock_value')
|
||||
|
||||
@api.multi
|
||||
def do_change_standard_price(self, new_price, account_id):
|
||||
@ -148,11 +145,13 @@ class ProductProduct(models.Model):
|
||||
'account_id': debit_account_id,
|
||||
'debit': abs(diff * qty_available),
|
||||
'credit': 0,
|
||||
'product_id': product.id,
|
||||
}), (0, 0, {
|
||||
'name': _('Standard Price changed - %s') % (product.display_name),
|
||||
'account_id': credit_account_id,
|
||||
'debit': 0,
|
||||
'credit': abs(diff * qty_available),
|
||||
'product_id': product.id,
|
||||
})],
|
||||
}
|
||||
move = AccountMove.create(move_vals)
|
||||
@ -173,29 +172,98 @@ class ProductProduct(models.Model):
|
||||
StockMove = self.env['stock.move']
|
||||
domain = [('product_id', '=', self.id)] + StockMove._get_all_base_domain()
|
||||
moves = StockMove.search(domain)
|
||||
return sum(moves.mapped('remaining_value'))
|
||||
return sum(moves.mapped('remaining_value')), moves
|
||||
|
||||
@api.multi
|
||||
@api.depends('stock_move_ids.product_qty', 'stock_move_ids.state', 'product_tmpl_id.cost_method')
|
||||
@api.depends('stock_move_ids.product_qty', 'stock_move_ids.state', 'stock_move_ids.remaining_value', 'product_tmpl_id.cost_method', 'product_tmpl_id.standard_price', 'product_tmpl_id.property_valuation', 'product_tmpl_id.categ_id.property_valuation')
|
||||
def _compute_stock_value(self):
|
||||
StockMove = self.env['stock.move']
|
||||
to_date = self.env.context.get('to_date')
|
||||
|
||||
self.env['account.move.line'].check_access_rights('read')
|
||||
fifo_automated_values = {}
|
||||
query = """SELECT aml.product_id, aml.account_id, sum(aml.debit) - sum(aml.credit), sum(quantity), array_agg(aml.id)
|
||||
FROM account_move_line AS aml
|
||||
WHERE aml.product_id IS NOT NULL AND aml.company_id=%%s %s
|
||||
GROUP BY aml.product_id, aml.account_id"""
|
||||
params = (self.env.user.company_id.id,)
|
||||
if to_date:
|
||||
query = query % ('AND aml.date <= %s',)
|
||||
params = params + (to_date,)
|
||||
else:
|
||||
query = query % ('',)
|
||||
self.env.cr.execute(query, params=params)
|
||||
|
||||
res = self.env.cr.fetchall()
|
||||
for row in res:
|
||||
fifo_automated_values[(row[0], row[1])] = (row[2], row[3], list(row[4]))
|
||||
|
||||
for product in self:
|
||||
if product.cost_method in ['standard', 'average']:
|
||||
product.stock_value = product.standard_price * product.with_context(company_owned=True).qty_available
|
||||
qty_available = product.with_context(company_owned=True, owner_id=False).qty_available
|
||||
price_used = product.standard_price
|
||||
if to_date:
|
||||
price_used = product.get_history_price(
|
||||
self.env.user.company_id.id,
|
||||
date=to_date,
|
||||
)
|
||||
product.stock_value = price_used * qty_available
|
||||
product.qty_at_date = qty_available
|
||||
elif product.cost_method == 'fifo':
|
||||
product.stock_value = product._sum_remaining_values()
|
||||
if to_date:
|
||||
if product.product_tmpl_id.valuation == 'manual_periodic':
|
||||
domain = [('product_id', '=', product.id), ('date', '<=', to_date)] + StockMove._get_all_base_domain()
|
||||
moves = StockMove.search(domain)
|
||||
product.stock_value = sum(moves.mapped('value'))
|
||||
product.qty_at_date = product.with_context(company_owned=True, owner_id=False).qty_available
|
||||
product.stock_fifo_manual_move_ids = StockMove.browse(moves.ids)
|
||||
elif product.product_tmpl_id.valuation == 'real_time':
|
||||
valuation_account_id = product.categ_id.property_stock_valuation_account_id.id
|
||||
value, quantity, aml_ids = fifo_automated_values.get((product.id, valuation_account_id)) or (0, 0, [])
|
||||
product.stock_value = value
|
||||
product.qty_at_date = quantity
|
||||
product.stock_fifo_real_time_aml_ids = self.env['account.move.line'].browse(aml_ids)
|
||||
else:
|
||||
product.stock_value, moves = product._sum_remaining_values()
|
||||
product.qty_at_date = product.with_context(company_owned=True, owner_id=False).qty_available
|
||||
if product.product_tmpl_id.valuation == 'manual_periodic':
|
||||
product.stock_fifo_manual_move_ids = moves
|
||||
elif product.product_tmpl_id.valuation == 'real_time':
|
||||
valuation_account_id = product.categ_id.property_stock_valuation_account_id.id
|
||||
value, quantity, aml_ids = fifo_automated_values.get((product.id, valuation_account_id)) or (0, 0, [])
|
||||
product.stock_fifo_real_time_aml_ids = self.env['account.move.line'].browse(aml_ids)
|
||||
|
||||
def action_valuation_at_date_details(self):
|
||||
""" Returns an action with either a list view of all the valued stock moves of `self` if the
|
||||
valuation is set as manual or a list view of all the account move lines if the valuation is
|
||||
set as automated.
|
||||
"""
|
||||
self.ensure_one()
|
||||
to_date = self.env.context.get('to_date')
|
||||
action = {
|
||||
'name': _('Valuation at date'),
|
||||
'type': 'ir.actions.act_window',
|
||||
'view_type': 'form',
|
||||
'view_mode': 'tree,form',
|
||||
'context': self.env.context,
|
||||
}
|
||||
if self.valuation == 'real_time':
|
||||
action['res_model'] = 'account.move.line'
|
||||
action['domain'] = [('id', 'in', self.with_context(to_date=to_date).stock_fifo_real_time_aml_ids.ids)]
|
||||
tree_view_ref = self.env.ref('stock_account.view_stock_account_aml')
|
||||
form_view_ref = self.env.ref('account.view_move_line_form')
|
||||
action['views'] = [(tree_view_ref.id, 'tree'), (form_view_ref.id, 'form')]
|
||||
else:
|
||||
action['res_model'] = 'stock.move'
|
||||
action['domain'] = [('id', 'in', self.with_context(to_date=to_date).stock_fifo_manual_move_ids.ids)]
|
||||
tree_view_ref = self.env.ref('stock_account.view_move_tree_valuation_at_date')
|
||||
form_view_ref = self.env.ref('stock.view_move_form')
|
||||
action['views'] = [(tree_view_ref.id, 'tree'), (form_view_ref.id, 'form')]
|
||||
return action
|
||||
|
||||
@api.multi
|
||||
def action_open_product_moves(self):
|
||||
self.ensure_one()
|
||||
action = self.env.ref('stock_account.stock_move_valuation_action').read()[0]
|
||||
action['domain'] = [('product_id', '=', self.id)]
|
||||
action['context'] = {
|
||||
'search_default_outgoing': True,
|
||||
'search_default_incoming': True,
|
||||
'search_default_done': True,
|
||||
'is_avg': self.cost_method == 'average',
|
||||
}
|
||||
return action
|
||||
pass
|
||||
|
||||
@api.model
|
||||
def _anglo_saxon_sale_move_lines(self, name, product, uom, qty, price_unit, currency=False, amount_currency=False, fiscal_position=False, account_analytic=False, analytic_tags=False):
|
||||
@ -260,6 +328,28 @@ class ProductProduct(models.Model):
|
||||
return price or 0.0
|
||||
return self.uom_id._compute_price(price, uom)
|
||||
|
||||
def _compute_average_price(self, qty_done, quantity, moves):
|
||||
average_price_unit = 0
|
||||
qty_delivered = 0
|
||||
invoiced_qty = 0
|
||||
for move in moves:
|
||||
if move.state != 'done':
|
||||
continue
|
||||
invoiced_qty += move.product_qty
|
||||
if invoiced_qty <= qty_done:
|
||||
continue
|
||||
qty_to_consider = move.product_qty
|
||||
if invoiced_qty - move.product_qty < qty_done:
|
||||
qty_to_consider = invoiced_qty - qty_done
|
||||
qty_to_consider = min(qty_to_consider, quantity - qty_delivered)
|
||||
qty_delivered += qty_to_consider
|
||||
# `move.price_unit` is negative if the move is out and positive if the move is
|
||||
# dropshipped. Use its absolute value to compute the average price unit.
|
||||
average_price_unit = (average_price_unit * (qty_delivered - qty_to_consider) + abs(move.price_unit) * qty_to_consider) / qty_delivered
|
||||
if qty_delivered == quantity:
|
||||
break
|
||||
return average_price_unit
|
||||
|
||||
|
||||
class ProductCategory(models.Model):
|
||||
_inherit = 'product.category'
|
||||
@ -311,4 +401,3 @@ class ProductCategory(models.Model):
|
||||
'message': _("Changing your cost method is an important change that will impact your inventory valuation. Are you sure you want to make that change?"),
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -71,37 +71,45 @@ class StockMoveLine(models.Model):
|
||||
if move.product_id.valuation == 'real_time' and (move._is_in() or move._is_out()):
|
||||
move.with_context(force_valuation_amount=correction_value)._account_entry_move()
|
||||
return res
|
||||
|
||||
|
||||
@api.multi
|
||||
def write(self, vals):
|
||||
""" When editing a done stock.move.line, we impact the valuation. Users may increase or
|
||||
decrease the `qty_done` field. There are three cost method available: standard, average
|
||||
and fifo. We implement the logic in a similar way for standard and average: increase
|
||||
or decrease the original value with the standard or average price of today. In fifo, we
|
||||
have a different logic wheter the move is incoming or outgoing. If the move is incoming, we
|
||||
update the value and remaining_value/qty with the unit price of the move. If the move is
|
||||
outgoing and the user increases qty_done, we call _run_fifo and it'll consume layer(s) in
|
||||
the stack the same way a new outgoing move would have done. If the move is outoing and the
|
||||
user decreases qty_done, we either increase the last receipt candidate if one is found or
|
||||
we decrease the value with the last fifo price.
|
||||
"""
|
||||
if 'qty_done' in vals:
|
||||
# We need to update the `value`, `remaining_value` and `remaining_qty` on the linked
|
||||
# stock move.
|
||||
moves_to_update = {}
|
||||
for move_line in self.filtered(lambda ml: ml.state == 'done' and (ml.move_id._is_in() or ml.move_id._is_out())):
|
||||
moves_to_update[move_line.move_id] = vals['qty_done'] - move_line.qty_done
|
||||
|
||||
for move_id, qty_difference in moves_to_update.items():
|
||||
# more/less units are available, update `remaining_value` and
|
||||
# `remaining_qty` on the linked stock move.
|
||||
move_vals = {'remaining_qty': move_id.remaining_qty + qty_difference}
|
||||
new_remaining_value = 0
|
||||
move_vals = {}
|
||||
if move_id.product_id.cost_method in ['standard', 'average']:
|
||||
correction_value = qty_difference * move_id.product_id.standard_price
|
||||
move_vals['value'] = move_id.value - correction_value
|
||||
move_vals.pop('remaining_qty')
|
||||
if move_id._is_in():
|
||||
move_vals['value'] = move_id.value + correction_value
|
||||
elif move_id._is_out():
|
||||
move_vals['value'] = move_id.value - correction_value
|
||||
else:
|
||||
# FIFO handling
|
||||
if move_id._is_in():
|
||||
correction_value = qty_difference * move_id.price_unit
|
||||
new_remaining_value = move_id.remaining_value + correction_value
|
||||
move_vals['value'] = move_id.value + correction_value
|
||||
move_vals['remaining_qty'] = move_id.remaining_qty + qty_difference
|
||||
move_vals['remaining_value'] = move_id.remaining_value + correction_value
|
||||
elif move_id._is_out() and qty_difference > 0:
|
||||
# send more, run fifo again
|
||||
correction_value = self.env['stock.move']._run_fifo(move_id, quantity=qty_difference)
|
||||
new_remaining_value = move_id.remaining_value + correction_value
|
||||
move_vals.pop('remaining_qty')
|
||||
# no need to adapt `remaining_qty` and `remaining_value` as `_run_fifo` took care of it
|
||||
move_vals['value'] = move_id.value - correction_value
|
||||
elif move_id._is_out() and qty_difference < 0:
|
||||
# fake return, find the last receipt and augment its qties
|
||||
candidates_receipt = self.env['stock.move'].search(move_id._get_in_domain(), order='date, id desc', limit=1)
|
||||
if candidates_receipt:
|
||||
candidates_receipt.write({
|
||||
@ -111,15 +119,11 @@ class StockMoveLine(models.Model):
|
||||
correction_value = qty_difference * candidates_receipt.price_unit
|
||||
else:
|
||||
correction_value = qty_difference * move_id.product_id.standard_price
|
||||
move_vals.pop('remaining_qty')
|
||||
if move_id._is_out():
|
||||
move_vals['remaining_value'] = new_remaining_value if new_remaining_value < 0 else 0
|
||||
else:
|
||||
move_vals['remaining_value'] = new_remaining_value
|
||||
move_vals['value'] = move_id.value - correction_value
|
||||
move_id.write(move_vals)
|
||||
|
||||
if move_id.product_id.valuation == 'real_time':
|
||||
move_id.with_context(force_valuation_amount=correction_value)._account_entry_move()
|
||||
move_id.with_context(force_valuation_amount=correction_value, forced_quantity=qty_difference)._account_entry_move()
|
||||
if qty_difference > 0:
|
||||
move_id.product_price_update_before_done(forced_qty=qty_difference)
|
||||
return super(StockMoveLine, self).write(vals)
|
||||
@ -210,13 +214,23 @@ class StockMove(models.Model):
|
||||
|
||||
@api.model
|
||||
def _run_fifo(self, move, quantity=None):
|
||||
""" Value `move` according to the FIFO rule, meaning we consume the
|
||||
oldest receipt first. Candidates receipts are marked consumed or free
|
||||
thanks to their `remaining_qty` and `remaining_value` fields.
|
||||
By definition, `move` should be an outgoing stock move.
|
||||
|
||||
:param quantity: quantity to value instead of `move.product_qty`
|
||||
:returns: valued amount in absolute
|
||||
"""
|
||||
move.ensure_one()
|
||||
# Find back incoming stock moves (called candidates here) to value this move.
|
||||
|
||||
# Deal with possible move lines that do not impact the valuation.
|
||||
valued_move_lines = move.move_line_ids.filtered(lambda ml: ml.location_id._should_be_valued() and not ml.location_dest_id._should_be_valued() and not ml.owner_id)
|
||||
valued_quantity = 0
|
||||
for valued_move_line in valued_move_lines:
|
||||
valued_quantity += valued_move_line.product_uom_id._compute_quantity(valued_move_line.qty_done, move.product_id.uom_id)
|
||||
|
||||
# Find back incoming stock moves (called candidates here) to value this move.
|
||||
qty_to_take_on_candidates = quantity or valued_quantity
|
||||
candidates = move.product_id._get_fifo_candidates_in_move()
|
||||
new_standard_price = 0
|
||||
@ -246,7 +260,7 @@ class StockMove(models.Model):
|
||||
|
||||
# Update the standard price with the price of the last used candidate, if any.
|
||||
if new_standard_price and move.product_id.cost_method == 'fifo':
|
||||
move.product_id.standard_price = new_standard_price
|
||||
move.product_id.sudo().standard_price = new_standard_price
|
||||
|
||||
# If there's still quantity to value but we're out of candidates, we fall in the
|
||||
# negative stock use case. We chose to value the out move at the price of the
|
||||
@ -259,11 +273,12 @@ class StockMove(models.Model):
|
||||
elif qty_to_take_on_candidates > 0:
|
||||
last_fifo_price = new_standard_price or move.product_id.standard_price
|
||||
negative_stock_value = last_fifo_price * -qty_to_take_on_candidates
|
||||
tmp_value += abs(negative_stock_value)
|
||||
vals = {
|
||||
'remaining_qty': move.remaining_qty + -qty_to_take_on_candidates,
|
||||
'remaining_value': move.remaining_value + negative_stock_value,
|
||||
'value': -tmp_value + negative_stock_value,
|
||||
'price_unit': (-tmp_value + negative_stock_value) / (move.product_qty or quantity),
|
||||
'value': -tmp_value,
|
||||
'price_unit': -1 * last_fifo_price,
|
||||
}
|
||||
move.write(vals)
|
||||
return tmp_value
|
||||
@ -297,7 +312,9 @@ class StockMove(models.Model):
|
||||
self.write(vals)
|
||||
elif self._is_out():
|
||||
valued_move_lines = self.move_line_ids.filtered(lambda ml: ml.location_id._should_be_valued() and not ml.location_dest_id._should_be_valued() and not ml.owner_id)
|
||||
valued_quantity = sum(valued_move_lines.mapped('qty_done'))
|
||||
valued_quantity = 0
|
||||
for valued_move_line in valued_move_lines:
|
||||
valued_quantity += valued_move_line.product_uom_id._compute_quantity(valued_move_line.qty_done, self.product_id.uom_id)
|
||||
self.env['stock.move']._run_fifo(self, quantity=quantity)
|
||||
if self.product_id.cost_method in ['standard', 'average']:
|
||||
curr_rounding = self.company_id.currency_id.rounding
|
||||
@ -355,6 +372,7 @@ class StockMove(models.Model):
|
||||
product_tot_qty_available = move.product_id.qty_available + tmpl_dict[move.product_id.id]
|
||||
rounding = move.product_id.uom_id.rounding
|
||||
|
||||
qty_done = 0.0
|
||||
if float_is_zero(product_tot_qty_available, precision_rounding=rounding):
|
||||
new_std_price = move._get_price_unit()
|
||||
elif float_is_zero(product_tot_qty_available + move.product_qty, precision_rounding=rounding):
|
||||
@ -362,10 +380,11 @@ class StockMove(models.Model):
|
||||
else:
|
||||
# Get the standard price
|
||||
amount_unit = std_price_update.get((move.company_id.id, move.product_id.id)) or move.product_id.standard_price
|
||||
qty = forced_qty or move.product_qty
|
||||
new_std_price = ((amount_unit * product_tot_qty_available) + (move._get_price_unit() * qty)) / (product_tot_qty_available + move.product_qty)
|
||||
qty_done = move.product_uom._compute_quantity(move.quantity_done, move.product_id.uom_id)
|
||||
qty = forced_qty or qty_done
|
||||
new_std_price = ((amount_unit * product_tot_qty_available) + (move._get_price_unit() * qty)) / (product_tot_qty_available + qty_done)
|
||||
|
||||
tmpl_dict[move.product_id.id] += move.product_qty
|
||||
tmpl_dict[move.product_id.id] += qty_done
|
||||
# Write the standard price, as SUPERUSER_ID because a warehouse manager may not have the right to write on products
|
||||
move.product_id.with_context(force_company=move.company_id.id).sudo().write({'standard_price': new_std_price})
|
||||
std_price_update[move.company_id.id, move.product_id.id] = new_std_price
|
||||
@ -388,12 +407,14 @@ class StockMove(models.Model):
|
||||
if not candidates:
|
||||
continue
|
||||
qty_to_take_on_candidates = abs(move.remaining_qty)
|
||||
qty_taken_on_candidates = 0
|
||||
tmp_value = 0
|
||||
for candidate in candidates:
|
||||
if candidate.remaining_qty <= qty_to_take_on_candidates:
|
||||
qty_taken_on_candidate = candidate.remaining_qty
|
||||
else:
|
||||
qty_taken_on_candidate = qty_to_take_on_candidates
|
||||
qty_taken_on_candidates += qty_taken_on_candidate
|
||||
|
||||
value_taken_on_candidate = qty_taken_on_candidate * candidate.price_unit
|
||||
candidate_vals = {
|
||||
@ -407,27 +428,22 @@ class StockMove(models.Model):
|
||||
if qty_to_take_on_candidates == 0:
|
||||
break
|
||||
|
||||
remaining_value_before_vacuum = move.remaining_value
|
||||
|
||||
# If `remaining_qty` should be updated to 0, we wipe `remaining_value`. If it was set
|
||||
# it was only used to infer the correction entry anyway.
|
||||
new_remaining_qty = -qty_to_take_on_candidates
|
||||
new_remaining_value = 0 if not new_remaining_qty else move.remaining_value + tmp_value
|
||||
# When working with `price_unit`, beware that out move are negative.
|
||||
move_price_unit = move.price_unit if move._is_out() else -1 * move.price_unit
|
||||
# Get the estimated value we will correct.
|
||||
remaining_value_before_vacuum = qty_taken_on_candidates * move_price_unit
|
||||
new_remaining_qty = move.remaining_qty + qty_taken_on_candidates
|
||||
new_remaining_value = new_remaining_qty * abs(move.price_unit)
|
||||
|
||||
corrected_value = remaining_value_before_vacuum + tmp_value
|
||||
move.write({
|
||||
'remaining_value': new_remaining_value,
|
||||
'remaining_qty': new_remaining_qty,
|
||||
'value': move.value - corrected_value,
|
||||
})
|
||||
|
||||
if move.product_id.valuation == 'real_time':
|
||||
# If `move.remaining_value` is negative, it means that we initially valued this move at
|
||||
# an estimated price *and* posted an entry. `tmp_value` is the real value we took to
|
||||
# compensate and should always be positive, but if the remaining value is still negative
|
||||
# we have to take care to not overvalue by decreasing the correction entry by what's
|
||||
# already been posted.
|
||||
corrected_value = tmp_value
|
||||
if remaining_value_before_vacuum < 0:
|
||||
corrected_value += remaining_value_before_vacuum
|
||||
|
||||
# If `corrected_value` is 0, absolutely do *not* call `_account_entry_move`. We
|
||||
# force the amount in the context, but in the case it is 0 it'll create an entry
|
||||
# for the entire cost of the move. This case happens when the candidates moves
|
||||
@ -441,9 +457,9 @@ class StockMove(models.Model):
|
||||
# The correction should behave as a return too. As `_account_entry_move`
|
||||
# will post the natural values for an IN move (credit IN account, debit
|
||||
# OUT one), we inverse the sign to create the correct entries.
|
||||
move.with_context(force_valuation_amount=-corrected_value)._account_entry_move()
|
||||
move.with_context(force_valuation_amount=-corrected_value, forced_quantity=0)._account_entry_move()
|
||||
else:
|
||||
move.with_context(force_valuation_amount=corrected_value)._account_entry_move()
|
||||
move.with_context(force_valuation_amount=corrected_value, forced_quantity=0)._account_entry_move()
|
||||
|
||||
@api.model
|
||||
def _run_fifo_vacuum(self):
|
||||
@ -502,6 +518,11 @@ class StockMove(models.Model):
|
||||
else:
|
||||
valuation_amount = cost
|
||||
|
||||
if self._context.get('forced_ref'):
|
||||
ref = self._context['forced_ref']
|
||||
else:
|
||||
ref = self.picking_id.name
|
||||
|
||||
# the standard_price of the product may be in another decimal precision, or not compatible with the coinage of
|
||||
# the company currency... so we need to use round() before creating the accounting entries.
|
||||
debit_value = self.company_id.currency_id.round(valuation_amount)
|
||||
@ -511,24 +532,13 @@ class StockMove(models.Model):
|
||||
raise UserError(_("The cost of %s is currently equal to 0. Change the cost or the configuration of your product to avoid an incorrect valuation.") % (self.product_id.name,))
|
||||
credit_value = debit_value
|
||||
|
||||
if self.product_id.cost_method == 'average' and self.company_id.anglo_saxon_accounting:
|
||||
# in case of a supplier return in anglo saxon mode, for products in average costing method, the stock_input
|
||||
# account books the real purchase price, while the stock account books the average price. The difference is
|
||||
# booked in the dedicated price difference account.
|
||||
if self.location_dest_id.usage == 'supplier' and self.origin_returned_move_id and self.origin_returned_move_id.purchase_line_id:
|
||||
debit_value = self.origin_returned_move_id.price_unit * qty
|
||||
# in case of a customer return in anglo saxon mode, for products in average costing method, the stock valuation
|
||||
# is made using the original average price to negate the delivery effect.
|
||||
if self.location_id.usage == 'customer' and self.origin_returned_move_id:
|
||||
debit_value = self.origin_returned_move_id.price_unit * qty
|
||||
credit_value = debit_value
|
||||
partner_id = (self.picking_id.partner_id and self.env['res.partner']._find_accounting_partner(self.picking_id.partner_id).id) or False
|
||||
debit_line_vals = {
|
||||
'name': self.name,
|
||||
'product_id': self.product_id.id,
|
||||
'quantity': qty,
|
||||
'product_uom_id': self.product_id.uom_id.id,
|
||||
'ref': self.picking_id.name,
|
||||
'ref': ref,
|
||||
'partner_id': partner_id,
|
||||
'debit': debit_value if debit_value > 0 else 0,
|
||||
'credit': -debit_value if debit_value < 0 else 0,
|
||||
@ -539,7 +549,7 @@ class StockMove(models.Model):
|
||||
'product_id': self.product_id.id,
|
||||
'quantity': qty,
|
||||
'product_uom_id': self.product_id.uom_id.id,
|
||||
'ref': self.picking_id.name,
|
||||
'ref': ref,
|
||||
'partner_id': partner_id,
|
||||
'credit': credit_value if credit_value > 0 else 0,
|
||||
'debit': -credit_value if credit_value < 0 else 0,
|
||||
@ -559,7 +569,7 @@ class StockMove(models.Model):
|
||||
'product_id': self.product_id.id,
|
||||
'quantity': qty,
|
||||
'product_uom_id': self.product_id.uom_id.id,
|
||||
'ref': self.picking_id.name,
|
||||
'ref': ref,
|
||||
'partner_id': partner_id,
|
||||
'credit': diff_amount > 0 and diff_amount or 0,
|
||||
'debit': diff_amount < 0 and -diff_amount or 0,
|
||||
@ -571,14 +581,26 @@ class StockMove(models.Model):
|
||||
def _create_account_move_line(self, credit_account_id, debit_account_id, journal_id):
|
||||
self.ensure_one()
|
||||
AccountMove = self.env['account.move']
|
||||
move_lines = self._prepare_account_move_line(self.product_qty, abs(self.value), credit_account_id, debit_account_id)
|
||||
quantity = self.env.context.get('forced_quantity', self.product_qty)
|
||||
quantity = quantity if self._is_in() else -1 * quantity
|
||||
|
||||
# Make an informative `ref` on the created account move to differentiate between classic
|
||||
# movements, vacuum and edition of past moves.
|
||||
ref = self.picking_id.name
|
||||
if self.env.context.get('force_valuation_amount'):
|
||||
if self.env.context.get('forced_quantity') == 0:
|
||||
ref = 'Revaluation of %s (negative inventory)' % ref
|
||||
elif self.env.context.get('forced_quantity') is not None:
|
||||
ref = 'Correction of %s (modification of past move)' % ref
|
||||
|
||||
move_lines = self.with_context(forced_ref=ref)._prepare_account_move_line(quantity, abs(self.value), credit_account_id, debit_account_id)
|
||||
if move_lines:
|
||||
date = self._context.get('force_period_date', fields.Date.context_today(self))
|
||||
new_account_move = AccountMove.create({
|
||||
new_account_move = AccountMove.sudo().create({
|
||||
'journal_id': journal_id,
|
||||
'line_ids': move_lines,
|
||||
'date': date,
|
||||
'ref': self.picking_id.name,
|
||||
'ref': ref,
|
||||
'stock_move_id': self.id,
|
||||
})
|
||||
new_account_move.post()
|
||||
|
File diff suppressed because it is too large
Load Diff
@ -86,97 +86,21 @@
|
||||
</data>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="product_valuation_tree" model="ir.ui.view">
|
||||
<field name="name">product.valuation.tree</field>
|
||||
<field name="model">product.product</field>
|
||||
<field name="arch" type="xml">
|
||||
<tree>
|
||||
<field name="display_name" string="Product"/>
|
||||
<field name="qty_available" context="{'company_owned': True}" string="Quantity on Hand"/>
|
||||
<field name="uom_id" groups="product.group_uom"/>
|
||||
<field name="currency_id" invisible="1"/>
|
||||
<field name="stock_value" sum="Stock Valuation" widget="monetary" string="Total Value"/>
|
||||
</tree>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="product_valuation_action" model="ir.actions.act_window">
|
||||
<field name="name">Product Valuation</field>
|
||||
<field name="type">ir.actions.act_window</field>
|
||||
<field name="res_model">product.product</field>
|
||||
<field name="view_mode">tree,form</field>
|
||||
<field name="view_type">form</field>
|
||||
<field name="view_id" ref="product_valuation_tree"/>
|
||||
<field name="view_id" ref="view_stock_product_tree2"/>
|
||||
<field name="domain">[('type', '=', 'product'), ('qty_available', '!=', 0)]</field>
|
||||
<field name="context">{}</field>
|
||||
<field name="context">{'company_owned': True}</field>
|
||||
<field name="help" type="html">
|
||||
<p class="oe_view_nocontent_create">
|
||||
If there are products, you will see its name and valuation.
|
||||
</p>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="product_valuation_form_view" model="ir.ui.view">
|
||||
<field name="name">product.product</field>
|
||||
<field name="model">product.product</field>
|
||||
<field name="inherit_id" ref="stock.product_form_view_procurement_button"/>
|
||||
<field name="arch" type="xml">
|
||||
<xpath expr="//button[@name='action_view_stock_move_lines']" position="after">
|
||||
<button string="Inventory Valuation" type="object" name="action_open_product_moves" class="oe_stat_button" icon="fa-cubes" attrs="{'invisible': ['|', ('cost_method', '=', 'standard'), ('id', '=', False)]}"/>
|
||||
</xpath>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="product_template_valuation_form_view" model="ir.ui.view">
|
||||
<field name="name">product.template</field>
|
||||
<field name="model">product.template</field>
|
||||
<field name="inherit_id" ref="stock.product_template_form_view_procurement_button"/>
|
||||
<field name="arch" type="xml">
|
||||
<xpath expr="//button[@name='action_view_stock_move_lines']" position="after">
|
||||
<button string="Inventory Valuation" type="object" name="action_open_product_moves" class="oe_stat_button" icon="fa-cubes" attrs="{'invisible': [('cost_method', '=', 'standard')]}"/>
|
||||
</xpath>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<!-- stock move valuation view -->
|
||||
<record id="view_move_tree_valuation" model="ir.ui.view">
|
||||
<field name="name">stock.move.tree.valuation</field>
|
||||
<field name="model">stock.move</field>
|
||||
<field eval="8" name="priority"/>
|
||||
<field name="arch" type="xml">
|
||||
<tree decoration-muted="state == 'cancel'" decoration-danger="(state not in ('cancel','done')) and date > current_date" string="Moves" create="0">
|
||||
<field name="name"/>
|
||||
<field name="picking_id" string="Reference"/>
|
||||
<field name="origin"/>
|
||||
<field name="picking_type_id" invisible="1"/>
|
||||
<field name="create_date" invisible="1" groups="base.group_no_one"/>
|
||||
<field name="product_id"/>
|
||||
<field name="location_id" groups="stock.group_stock_multi_locations"/>
|
||||
<field name="location_dest_id" groups="stock.group_stock_multi_locations"/>
|
||||
<field name="date" groups="base.group_no_one"/>
|
||||
<field name="state" invisible="1"/>
|
||||
<field name="product_uom_qty" string="Qty"/>
|
||||
<field name="product_uom" options="{'no_open': True, 'no_create': True}" string="Unit of Measure" groups="product.group_uom"/>
|
||||
<field name="price_unit"/>
|
||||
<field name="value" sum="Stock Valuation"/>
|
||||
<field name="remaining_qty" invisible="context.get('is_avg')"/>
|
||||
<field name="remaining_value" sum="Stock Valuation" invisible="context.get('is_avg')"/>
|
||||
</tree>
|
||||
</field>
|
||||
</record>
|
||||
<record id="stock_move_valuation_action" model="ir.actions.act_window">
|
||||
<field name="name">Stock Moves</field>
|
||||
<field name="res_model">stock.move</field>
|
||||
<field name="type">ir.actions.act_window</field>
|
||||
<field name="view_type">form</field>
|
||||
<field name="view_id" ref="view_move_tree_valuation"/>
|
||||
<field name="search_view_id" ref="stock.view_move_search"/>
|
||||
<field name="context">{'search_default_outgoing': 1, 'search_default_incoming': 1, 'search_default_done': 1, 'search_default_group_by_product': 1}</field>
|
||||
<field name="help" type="html">
|
||||
<p class="oe_view_nocontent_create">
|
||||
Click to create a stock movement.
|
||||
</p>
|
||||
</field>
|
||||
</record>
|
||||
</data>
|
||||
</data>
|
||||
</flectra>
|
||||
|
@ -49,6 +49,82 @@
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<menuitem id="menu_valuation" name="Inventory Valuation" parent="stock.menu_warehouse_report" sequence="110" action="stock_account.action_stock_account_valuation_report"/>
|
||||
<!-- valuation wizard: current or at date -->
|
||||
<record id="view_stock_quantity_history" model="ir.ui.view">
|
||||
<field name="name">Valuation Report</field>
|
||||
<field name="model">stock.quantity.history</field>
|
||||
<field name="arch" type="xml">
|
||||
<form string="Choose your date">
|
||||
<group>
|
||||
<group>
|
||||
<field name="compute_at_date" widget="radio"/>
|
||||
<field name="date" attrs="{'invisible': [('compute_at_date', '=', 0)]}"/>
|
||||
</group>
|
||||
</group>
|
||||
<footer>
|
||||
<button name="open_table" string="Retrieve the inventory valuation" type="object" class="btn-primary"/>
|
||||
<button string="Cancel" class="btn-default" special="cancel" />
|
||||
</footer>
|
||||
</form>
|
||||
</field>
|
||||
</record>
|
||||
<record id="view_stock_product_tree2" model="ir.ui.view">
|
||||
<field name="name">product.stock.tree.2</field>
|
||||
<field name="model">product.product</field>
|
||||
<field name="arch" type="xml">
|
||||
<tree>
|
||||
<field name="name"/>
|
||||
<field name="qty_at_date"/>
|
||||
<field name="uom_id" groups="product.group_uom"/>
|
||||
<field name="currency_id" invisible="1"/>
|
||||
<field name="stock_value" sum="Stock Valuation" widget="monetary"/>
|
||||
<field name="cost_method" invisible="1"/>
|
||||
<button name="action_valuation_at_date_details" type="object" icon="fa-info-circle" attrs="{'invisible': [('cost_method', '!=', 'fifo')]}" />
|
||||
</tree>
|
||||
</field>
|
||||
</record>
|
||||
<record id="action_stock_inventory_valuation" model="ir.actions.act_window">
|
||||
<field name="name">Valuation Report</field>
|
||||
<field name="res_model">stock.quantity.history</field>
|
||||
<field name="view_type">form</field>
|
||||
<field name="view_mode">form</field>
|
||||
<field name="view_id" ref="stock_account.view_stock_quantity_history"/>
|
||||
<field name="target">new</field>
|
||||
<field name="context">{'default_compute_at_date': 0, 'valuation': True}</field>
|
||||
</record>
|
||||
<menuitem id="menu_valuation" name="Inventory Valuation" parent="stock.menu_warehouse_report" sequence="110" action="action_stock_inventory_valuation"/>
|
||||
|
||||
<!-- AML specialized tree view -->
|
||||
<record id="view_stock_account_aml" model="ir.ui.view">
|
||||
<field name="name">stock.account.aml</field>
|
||||
<field name="model">account.move.line</field>
|
||||
<field name="arch" type="xml">
|
||||
<tree>
|
||||
<field name="date" />
|
||||
<field name="ref" />
|
||||
<field name="product_id" />
|
||||
<field name="quantity" />
|
||||
<field name="product_uom_id" groups="product.group_uom" />
|
||||
<field name="balance" string="Value"/>
|
||||
</tree>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<!-- stock move specialized tree view -->
|
||||
<record id="view_move_tree_valuation_at_date" model="ir.ui.view">
|
||||
<field name="name">stock.move.tree.valuation.at.date</field>
|
||||
<field name="model">stock.move</field>
|
||||
<field name="arch" type="xml">
|
||||
<tree>
|
||||
<field name="date" />
|
||||
<field name="picking_id" string="Reference"/>
|
||||
<field name="state" invisible="1"/>
|
||||
<field name="product_id"/>
|
||||
<field name="product_uom_qty" string="Quantity"/>
|
||||
<field name="product_uom" options="{'no_open': True, 'no_create': True}" string="Unit of Measure" groups="product.group_uom"/>
|
||||
<field name="value" sum="Stock Valuation"/>
|
||||
</tree>
|
||||
</field>
|
||||
</record>
|
||||
</data>
|
||||
</flectra>
|
||||
|
@ -2,4 +2,5 @@
|
||||
# Part of Odoo, Flectra. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from . import stock_change_standard_price
|
||||
from . import stock_quantity_history
|
||||
|
||||
|
32
addons/stock_account/wizard/stock_quantity_history.py
Normal file
32
addons/stock_account/wizard/stock_quantity_history.py
Normal file
@ -0,0 +1,32 @@
|
||||
# Part of Odoo, Flectra. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from flectra import models, _
|
||||
|
||||
|
||||
class StockQuantityHistory(models.TransientModel):
|
||||
_inherit = 'stock.quantity.history'
|
||||
|
||||
def open_table(self):
|
||||
if not self.env.context.get('valuation'):
|
||||
return super(StockQuantityHistory, self).open_table()
|
||||
|
||||
self.env['stock.move']._run_fifo_vacuum()
|
||||
|
||||
if self.compute_at_date:
|
||||
tree_view_id = self.env.ref('stock_account.view_stock_product_tree2').id
|
||||
form_view_id = self.env.ref('stock.product_form_view_procurement_button').id
|
||||
# We pass `to_date` in the context so that `qty_available` will be computed across
|
||||
# moves until date.
|
||||
action = {
|
||||
'type': 'ir.actions.act_window',
|
||||
'views': [(tree_view_id, 'tree'), (form_view_id, 'form')],
|
||||
'view_mode': 'tree,form',
|
||||
'name': _('Products'),
|
||||
'res_model': 'product.product',
|
||||
'domain': "[('type', '=', 'product'), ('qty_available', '!=', 0)]",
|
||||
'context': dict(self.env.context, to_date=self.date, company_owned=True),
|
||||
}
|
||||
return action
|
||||
else:
|
||||
return self.env.ref('stock_account.product_valuation_action').read()[0]
|
||||
|
@ -11,13 +11,14 @@ class TestStockValuation(AccountingTestCase):
|
||||
self.stock_location = self.env.ref('stock.stock_location_stock')
|
||||
self.partner_id = self.env.ref('base.res_partner_1')
|
||||
self.product1 = self.env.ref('product.product_product_8')
|
||||
self.categ_id = self.product1.categ_id
|
||||
|
||||
self.acc_payable = self.env['account.account'].search([('name', '=', 'Account Payable')]).id
|
||||
self.acc_expense = self.env['account.account'].search([('name', '=', 'Expenses')]).id
|
||||
self.acc_receivable = self.env['account.account'].search([('name', '=', 'Account Receivable')]).id
|
||||
self.acc_sale = self.env['account.account'].search([('name', '=', 'Product Sales')]).id
|
||||
self.acc_stock_in = self.env['account.account'].search([('name', '=', 'Stock Interim Account (Received)')]).id
|
||||
self.acc_stock_out = self.env['account.account'].search([('name', '=', 'Stock Interim Account (Delivered)')]).id
|
||||
self.acc_payable = self.partner_id.property_account_payable_id.id
|
||||
self.acc_expense = self.categ_id.property_account_expense_categ_id.id
|
||||
self.acc_receivable = self.partner_id.property_account_receivable_id.id
|
||||
self.acc_sale = self.categ_id.property_account_income_categ_id.id
|
||||
self.acc_stock_in = self.categ_id.property_stock_account_input_categ_id.id
|
||||
self.acc_stock_out = self.categ_id.property_stock_account_output_categ_id.id
|
||||
|
||||
def _dropship_product1(self):
|
||||
# enable the dropship and MTO route on the product
|
||||
@ -84,8 +85,7 @@ class TestStockValuation(AccountingTestCase):
|
||||
return all_amls
|
||||
|
||||
def _check_results(self, expected_aml, expected_aml_count, all_amls):
|
||||
# Construct a dict similar to `expected_aml` with `all_amls` in
|
||||
# order to
|
||||
# Construct a dict similar to `expected_aml` with `all_amls` in order to
|
||||
# compare them.
|
||||
result_aml = {}
|
||||
for aml in all_amls:
|
||||
@ -93,12 +93,12 @@ class TestStockValuation(AccountingTestCase):
|
||||
if result_aml.get(account_id):
|
||||
debit = result_aml[account_id][0]
|
||||
credit = result_aml[account_id][1]
|
||||
result_aml[account_id] = (
|
||||
debit + aml.debit, credit + aml.credit)
|
||||
result_aml[account_id] = (debit + aml.debit, credit + aml.credit)
|
||||
else:
|
||||
result_aml[account_id] = (aml.debit, aml.credit)
|
||||
|
||||
self.assertEqual(len(all_amls), expected_aml_count)
|
||||
|
||||
for k, v in expected_aml.items():
|
||||
self.assertEqual(result_aml[k], v)
|
||||
|
||||
|
@ -91,11 +91,13 @@ class LandedCost(models.Model):
|
||||
raise UserError(_('Cost and adjustments lines do not match. You should maybe recompute the landed costs.'))
|
||||
|
||||
for cost in self:
|
||||
move = self.env['account.move'].create({
|
||||
move = self.env['account.move']
|
||||
move_vals = {
|
||||
'journal_id': cost.account_journal_id.id,
|
||||
'date': cost.date,
|
||||
'ref': cost.name
|
||||
})
|
||||
'ref': cost.name,
|
||||
'line_ids': [],
|
||||
}
|
||||
for line in cost.valuation_adjustment_lines.filtered(lambda line: line.move_id):
|
||||
# Prorate the value at what's still in stock
|
||||
cost_to_add = (line.move_id.remaining_qty / line.move_id.product_qty) * line.additional_landed_cost
|
||||
@ -103,6 +105,7 @@ class LandedCost(models.Model):
|
||||
new_landed_cost_value = line.move_id.landed_cost_value + line.additional_landed_cost
|
||||
line.move_id.write({
|
||||
'landed_cost_value': new_landed_cost_value,
|
||||
'value': line.move_id.value + cost_to_add,
|
||||
'remaining_value': line.move_id.remaining_value + cost_to_add,
|
||||
'price_unit': (line.move_id.value + new_landed_cost_value) / line.move_id.product_qty,
|
||||
})
|
||||
@ -113,9 +116,9 @@ class LandedCost(models.Model):
|
||||
qty_out = line.move_id.product_qty - line.move_id.remaining_qty
|
||||
elif line.move_id._is_out():
|
||||
qty_out = line.move_id.product_qty
|
||||
line._create_accounting_entries(move, qty_out)
|
||||
|
||||
move.assert_balanced()
|
||||
move_vals['line_ids'] += line._create_accounting_entries(move, qty_out)
|
||||
|
||||
move = move.create(move_vals)
|
||||
cost.write({'state': 'done', 'account_move_id': move.id})
|
||||
move.post()
|
||||
return True
|
||||
@ -315,13 +318,12 @@ class AdjustmentLines(models.Model):
|
||||
Generate the account.move.line values to track the landed cost.
|
||||
Afterwards, for the goods that are already out of stock, we should create the out moves
|
||||
"""
|
||||
AccountMoveLine = self.env['account.move.line'].with_context(check_move_validity=False, recompute=False)
|
||||
AccountMoveLine = []
|
||||
|
||||
base_line = {
|
||||
'name': self.name,
|
||||
'move_id': move.id,
|
||||
'product_id': self.product_id.id,
|
||||
'quantity': self.quantity,
|
||||
'quantity': 0,
|
||||
}
|
||||
debit_line = dict(base_line, account_id=debit_account_id)
|
||||
credit_line = dict(base_line, account_id=credit_account_id)
|
||||
@ -333,18 +335,18 @@ class AdjustmentLines(models.Model):
|
||||
# negative cost, reverse the entry
|
||||
debit_line['credit'] = -diff
|
||||
credit_line['debit'] = -diff
|
||||
AccountMoveLine.create(debit_line)
|
||||
AccountMoveLine.create(credit_line)
|
||||
AccountMoveLine.append([0, 0, debit_line])
|
||||
AccountMoveLine.append([0, 0, credit_line])
|
||||
|
||||
# Create account move lines for quants already out of stock
|
||||
if qty_out > 0:
|
||||
debit_line = dict(base_line,
|
||||
name=(self.name + ": " + str(qty_out) + _(' already out')),
|
||||
quantity=qty_out,
|
||||
quantity=0,
|
||||
account_id=already_out_account_id)
|
||||
credit_line = dict(base_line,
|
||||
name=(self.name + ": " + str(qty_out) + _(' already out')),
|
||||
quantity=qty_out,
|
||||
quantity=0,
|
||||
account_id=debit_account_id)
|
||||
diff = diff * qty_out / self.quantity
|
||||
if diff > 0:
|
||||
@ -354,18 +356,18 @@ class AdjustmentLines(models.Model):
|
||||
# negative cost, reverse the entry
|
||||
debit_line['credit'] = -diff
|
||||
credit_line['debit'] = -diff
|
||||
AccountMoveLine.create(debit_line)
|
||||
AccountMoveLine.create(credit_line)
|
||||
AccountMoveLine.append([0, 0, debit_line])
|
||||
AccountMoveLine.append([0, 0, credit_line])
|
||||
|
||||
# TDE FIXME: oh dear
|
||||
if self.env.user.company_id.anglo_saxon_accounting:
|
||||
debit_line = dict(base_line,
|
||||
name=(self.name + ": " + str(qty_out) + _(' already out')),
|
||||
quantity=qty_out,
|
||||
quantity=0,
|
||||
account_id=credit_account_id)
|
||||
credit_line = dict(base_line,
|
||||
name=(self.name + ": " + str(qty_out) + _(' already out')),
|
||||
quantity=qty_out,
|
||||
quantity=0,
|
||||
account_id=already_out_account_id)
|
||||
|
||||
if diff > 0:
|
||||
@ -375,7 +377,7 @@ class AdjustmentLines(models.Model):
|
||||
# negative cost, reverse the entry
|
||||
debit_line['credit'] = -diff
|
||||
credit_line['debit'] = -diff
|
||||
AccountMoveLine.create(debit_line)
|
||||
AccountMoveLine.create(credit_line)
|
||||
AccountMoveLine.append([0, 0, debit_line])
|
||||
AccountMoveLine.append([0, 0, credit_line])
|
||||
|
||||
return True
|
||||
return AccountMoveLine
|
||||
|
Loading…
Reference in New Issue
Block a user