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:
Parthiv Patel 2018-07-13 09:51:33 +00:00
commit d6834ffaca
71 changed files with 2731 additions and 596 deletions

View File

@ -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):

View File

@ -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':

View File

@ -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)

View File

@ -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()

View File

@ -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:

View File

@ -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}'/>

View File

@ -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

View File

@ -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>

View File

@ -177,4 +177,4 @@
groups="group_mrp_routings"
sequence="50"/>
</data>
</flectra>
</flectra>

View File

@ -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.

View File

@ -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 &gt; 0) and (qty_done&gt;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')"/>

View File

@ -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)

View File

@ -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')

View File

@ -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,

View File

@ -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">

View File

@ -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"/>

View File

@ -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()

View File

@ -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,
}

View File

@ -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',

View File

@ -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'

View File

@ -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;

View File

@ -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,

View File

@ -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;

View File

@ -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>

View File

@ -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)"'>

View File

@ -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>

View File

@ -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

View File

@ -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

View File

@ -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'];

View File

@ -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;
}

View File

@ -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)

View File

@ -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)

View File

@ -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

View File

@ -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')

View File

@ -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

View File

@ -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)

View File

@ -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

View File

@ -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)

View File

@ -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

View File

@ -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):

View File

@ -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':

View File

@ -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')

View File

@ -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.

View File

@ -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):

View File

@ -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):

View File

@ -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):

View File

@ -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>

View File

@ -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

View File

@ -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>

View File

@ -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&amp;value=%s&amp;width=%s&amp;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&amp;value=%s&amp;width=%s&amp;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&amp;value=%s&amp;width=%s&amp;height=%s' % ('EAN8', move.product_id.barcode, 600, 100)" style="width:100%;height:50px"/>
<img t-else="" t-att-src="'/report/barcode/?type=%s&amp;value=%s&amp;width=%s&amp;height=%s' % ('Code128', move.product_id.barcode, 600, 100)" style="width:100%;height:50px"/>
</span>
</t>
</td>

View File

@ -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

View File

@ -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)

View File

@ -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

View File

@ -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.

View 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)

View File

@ -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):

View File

@ -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')]"/>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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)

View File

@ -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?"),
}
}

View File

@ -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

View File

@ -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>

View File

@ -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>

View File

@ -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

View 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]

View File

@ -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)

View File

@ -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