即使一个人,也要活得像军队一样!

Odoo12-- 销售订单确认后生成销售出库单的代码跟踪

如题:销售订单在报价单状态点击确认按钮,状态变更为销售订单,同时生成一个销售出库单

报价单

销售订单


———————————————–分割线—————————————–
####代码过程
#####1.点击 确认按钮
主要执行两个逻辑:
一是将单据状态改变,由报价单改为销售订单
二是通过推拉规则生成调拨单

以下为sale模块的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@api.multi
def action_confirm(self):
if self._get_forbidden_state_confirm() & set(self.mapped('state')):
raise UserError(_(
'It is not allowed to confirm an order in the following states: %s'
) % (', '.join(self._get_forbidden_state_confirm())))

for order in self.filtered(lambda order: order.partner_id not in order.message_partner_ids):
order.message_subscribe([order.partner_id.id])
self.write({
'state': 'sale',
'confirmation_date': fields.Datetime.now()
})
self._action_confirm()
if self.env['ir.config_parameter'].sudo().get_param('sale.auto_done_setting'):
self.action_done()
return True

重点代码是self._action_confirm(),

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@api.multi
def _action_confirm(self):
""" Implementation of additionnal mecanism of Sales Order confirmation.
This method should be extended when the confirmation should generated
other documents. In this method, the SO are in 'sale' state (not yet 'done').
"""
if self.env.context.get('send_email'):
self.force_quotation_send()

# create an analytic account if at least an expense product
for order in self:
if any([expense_policy not in [False, 'no'] for expense_policy in order.order_line.mapped('product_id.expense_policy')]):
if not order.analytic_account_id:
order._create_analytic_account()

return True

通过代码注释,我们可以确定这是确认订单的附加机制,生成其他单据时候应拓展此方法。

####2.生成调拨单
继续跟踪代码
以下为sale_stock模块的代码:

1
2
3
4
5
@api.multi
def _action_confirm(self):
for order in self:
order.order_line._action_launch_stock_rule()
super(SaleOrder, self)._action_confirm()

不难发现,再次拓展了_action_confirm方法。
重点代码:order.order_line._action_launch_stock_rule()
继续跟踪sale.order.line的_action_launch_stock_rule的方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53

@api.multi
def _action_launch_stock_rule(self):
"""
Launch procurement group run method with required/custom fields genrated by a
sale order line. procurement group will launch '_run_pull', '_run_buy' or '_run_manufacture'
depending on the sale order line product rule.
"""
precision = self.env['decimal.precision'].precision_get('Product Unit of Measure')
errors = []
for line in self:
if line.state != 'sale' or not line.product_id.type in ('consu','product'):
continue
qty = line._get_qty_procurement()
if float_compare(qty, line.product_uom_qty, precision_digits=precision) >= 0:
continue

group_id = line.order_id.procurement_group_id
if not group_id:
group_id = self.env['procurement.group'].create({
'name': line.order_id.name, 'move_type': line.order_id.picking_policy,
'sale_id': line.order_id.id,
'partner_id': line.order_id.partner_shipping_id.id,
})
line.order_id.procurement_group_id = group_id
else:
# In case the procurement group is already created and the order was
# cancelled, we need to update certain values of the group.
updated_vals = {}
if group_id.partner_id != line.order_id.partner_shipping_id:
updated_vals.update({'partner_id': line.order_id.partner_shipping_id.id})
if group_id.move_type != line.order_id.picking_policy:
updated_vals.update({'move_type': line.order_id.picking_policy})
if updated_vals:
group_id.write(updated_vals)

values = line._prepare_procurement_values(group_id=group_id)
product_qty = line.product_uom_qty - qty

procurement_uom = line.product_uom
quant_uom = line.product_id.uom_id
get_param = self.env['ir.config_parameter'].sudo().get_param
if procurement_uom.id != quant_uom.id and get_param('stock.propagate_uom') != '1':
product_qty = line.product_uom._compute_quantity(product_qty, quant_uom, rounding_method='HALF-UP')
procurement_uom = quant_uom

try:
self.env['procurement.group'].run(line.product_id, product_qty, procurement_uom, line.order_id.partner_shipping_id.property_stock_customer, line.name, line.order_id.name, values)
except UserError as error:
errors.append(error.name)
if errors:
raise UserError('\n'.join(errors))
return True

主要代码self.env['procurement.group'].run(line.product_id, product_qty, procurement_uom, line.order_id.partner_shipping_id.property_stock_customer, line.name, line.order_id.name, values)
在此之前,主要是计算生成调拨单的数据。

继续跟踪run方法。

以下为stock模块的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
@api.model
def run(self, product_id, product_qty, product_uom, location_id, name, origin, values):
""" Method used in a procurement case. The purpose is to supply the
product passed as argument in the location also given as an argument.
In order to be able to find a suitable location that provide the product
it will search among stock.rule.
"""
values.setdefault('company_id', self.env['res.company']._company_default_get('procurement.group'))
values.setdefault('priority', '1')
values.setdefault('date_planned', fields.Datetime.now())
rule = self._get_rule(product_id, location_id, values)
if not rule:
raise UserError(_('No procurement rule found in location "%s" for product "%s".\n Check routes configuration.') % (location_id.display_name, product_id.display_name))
action = 'pull' if rule.action == 'pull_push' else rule.action
if hasattr(rule, '_run_%s' % action):
getattr(rule, '_run_%s' % action)(product_id, product_qty, product_uom, location_id, name, origin, values)
else:
_logger.error("The method _run_%s doesn't exist on the procument rules" % action)
return True

重点代码:getattr(rule, '_run_%s' % action)(product_id, product_qty, product_uom, location_id, name, origin, values)
发现是根据推拉规则,通过反射的方式执行的代码。
执行_run_pull或者_run_push….

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
def _run_pull(self, product_id, product_qty, product_uom, location_id, name, origin, values):
if not self.location_src_id:
msg = _('No source location defined on stock rule: %s!') % (self.name, )
raise UserError(msg)

# create the move as SUPERUSER because the current user may not have the rights to do it (mto product launched by a sale for example)
# Search if picking with move for it exists already:
group_id = False
if self.group_propagation_option == 'propagate':
group_id = values.get('group_id', False) and values['group_id'].id
elif self.group_propagation_option == 'fixed':
group_id = self.group_id.id

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().with_context(force_company=data.get('company_id', False)).create(data)
move._action_confirm()
return True

重点代码move._action_confirm()
此时是先生成库存移动move单,通过_action_confirm()生成picking单

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
def _action_confirm(self, merge=True, merge_into=False):
""" Confirms stock move or put it in waiting if it's linked to another move.
:param: merge: According to this boolean, a newly confirmed move will be merged
in another move of the same picking sharing its characteristics.
"""
move_create_proc = self.env['stock.move']
move_to_confirm = self.env['stock.move']
move_waiting = self.env['stock.move']

to_assign = {}
for move in self:
# if the move is preceeded, then it's waiting (if preceeding move is done, then action_assign has been called already and its state is already available)
if move.move_orig_ids:
move_waiting |= move
else:
if move.procure_method == 'make_to_order':
move_create_proc |= move
else:
move_to_confirm |= move
if move._should_be_assigned():
key = (move.group_id.id, move.location_id.id, move.location_dest_id.id)
if key not in to_assign:
to_assign[key] = self.env['stock.move']
to_assign[key] |= move

# create procurements for make to order moves
for move in move_create_proc:
values = move._prepare_procurement_values()
origin = (move.group_id and move.group_id.name or (move.origin or move.picking_id.name or "/"))
self.env['procurement.group'].run(move.product_id, move.product_uom_qty, move.product_uom, move.location_id, move.rule_id and move.rule_id.name or "/", origin,
values)

move_to_confirm.write({'state': 'confirmed'})
(move_waiting | move_create_proc).write({'state': 'waiting'})

# assign picking in batch for all confirmed move that share the same details
for moves in to_assign.values():
moves._assign_picking()
self._push_apply()
if merge:
return self._merge_moves(merge_into=merge_into)
return self

重点代码:moves._assign_picking()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
def _assign_picking(self):
""" Try to assign the moves to an existing picking that has not been
reserved yet and has the same procurement group, locations and picking
type (moves should already have them identical). Otherwise, create a new
picking to assign them to. """
Picking = self.env['stock.picking']
for move in self:
recompute = False
picking = move._search_picking_for_assignation()
if picking:
if picking.partner_id.id != move.partner_id.id or picking.origin != move.origin:
# If a picking is found, we'll append `move` to its move list and thus its
# `partner_id` and `ref` field will refer to multiple records. In this
# case, we chose to wipe them.
picking.write({
'partner_id': False,
'origin': False,
})
else:
recompute = True
picking = Picking.create(move._get_new_picking_values())
move.write({'picking_id': picking.id})
move._assign_picking_post_process(new=recompute)
# If this method is called in batch by a write on a one2many and
# at some point had to create a picking, some next iterations could
# try to find back the created picking. As we look for it by searching
# on some computed fields, we have to force a recompute, else the
# record won't be found.
if recompute:
move.recompute()
return True

在此处生成的是确认状态的picking单以及move单。
但是为什么我们在交货单中看到的是就绪状态的单据呢?
别急,我们继续查找代码。
在此,先插一句,必须了解什么是jit即时调度。(我不知道,别问我了!)
以下代码为procurement_jit模块

1
2
3
4
5
6
7
8
9
@api.multi
def _action_launch_stock_rule(self):
res = super(SaleOrderLine, self)._action_launch_stock_rule()
orders = list(set(x.order_id for x in self))
for order in orders:
reassign = order.picking_ids.filtered(lambda x: x.state=='confirmed' or (x.state in ['waiting', 'assigned'] and not x.printed))
if reassign:
reassign.action_assign()
return res

聪明的你,已经发现玄机所在了,不错,就是执行了一个关键代码reassign.action_assign()

以上。

备注:
花费了一天时间,研究逻辑,可能是本人也不太熟悉这部分业务的过程。时间耽误很长。建议各位研究业务过程,业务了解,代码也就好找了。

-------------本文结束感谢您的阅读-------------