Odoo中文社区可以通过以下三个域名访问:shine-it.net , odoocn.org,odoo.net.cn

原论坛用户的基本信息和发帖这里都予以保留,请注意:原论坛用户无需重新注册新用户,但是您的密码需要重置

开发人员可以登录gitter讨论组: http://gitter.im/odoo-china/Talk, 需要github账号

如果您登录系统碰到问题,请在微信公众号留言:

(7.0 version)当销售单中包含service或phantom类型的产品时,销售单不能完成的原因分析及解决方案



  • 首先说一下service类型的产品,由于该类型的产品不需要发货,所以当在销售订单确认了后,销售单直接变成了等待开票的状态,但当开票的流程结束后,订单却还是停在销售单的状态上,该问题的解决方案是安装Tasks on SO模块,这个不是本文的重点,本文的重点是当产品类型为stockable,需求方式为Make to Stock且bom type为Sets/Phantom时,销售订单不能完成的问题。

    当销售单中包含phantom类型的组合产品时,在销售订单的order_line中,你能看到move_ids包含了组成该组合产品的若干产品和该组合产品,并且,当销售单发货完成后,再看该销售订单会发现,该组合产品仍未waiting available的状态,其他产品均为done状态。
    [img http://images.cnitblog.com/i/396990/201407/041103124187005.png /img]

    再来看销售订单的流程:
    [img http://images.cnitblog.com/i/396990/201407/041055089492003.png /img]

    在流程图上可以看出,送货的流stuck在ship到ship_end这个结点上,因此test_state('finished')的值肯定是false的,找到test_state('finished')的代码:

    def test_state(self, cr, uid, ids, mode, *args):<br />&nbsp; &nbsp; &nbsp; &nbsp; assert mode in (&#039;finished&#039;, &#039;canceled&#039;), _(&quot;invalid mode for test_state&quot;)<br />&nbsp; &nbsp; &nbsp; &nbsp; finished = True<br />&nbsp; &nbsp; &nbsp; &nbsp; canceled = False<br />&nbsp; &nbsp; &nbsp; &nbsp; write_done_ids = &#91;]<br />&nbsp; &nbsp; &nbsp; &nbsp; write_cancel_ids = &#91;]<br />&nbsp; &nbsp; &nbsp; &nbsp; for order in self.browse(cr, uid, ids, context={}):<br />&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; for line in order.order_line:<br />&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; if (not line.procurement_id) or (line.procurement_id.state==&#039;done&#039;):<br />&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; if line.state != &#039;done&#039;:<br />&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; write_done_ids.append(line.id)<br />&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; else:<br />&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; finished = False<br />&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; if line.procurement_id:<br />&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; if (line.procurement_id.state == &#039;cancel&#039;):<br />&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; canceled = True<br />&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; if line.state != &#039;exception&#039;:<br />&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; write_cancel_ids.append(line.id)<br />&nbsp; &nbsp; &nbsp; &nbsp; if write_done_ids:<br />&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; self.pool.get(&#039;sale.order.line&#039;).write(cr, uid, write_done_ids, {&#039;state&#039;: &#039;done&#039;})<br />&nbsp; &nbsp; &nbsp; &nbsp; if write_cancel_ids:<br />&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; self.pool.get(&#039;sale.order.line&#039;).write(cr, uid, write_cancel_ids, {&#039;state&#039;: &#039;exception&#039;})<br /><br />&nbsp; &nbsp; &nbsp; &nbsp; if mode == &#039;finished&#039;:<br />&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; return finished<br />&nbsp; &nbsp; &nbsp; &nbsp; elif mode == &#039;canceled&#039;:<br />&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; return canceled
    


    经测试,该组合产品会生成一个需求单,并且该需求单的状态一直为waiting.这就是为什么ship这个工作流一直走不下去的原因了。

    [img http://images.cnitblog.com/i/396990/201407/041116002468947.png /img]

    再查看需求单的工作流:
    [img http://images.cnitblog.com/i/396990/201407/041119060438443.png /img]
    问题应该出在check_move_done()这里,但该组合产品在库中的确没有库存,没有发货,那么如何确定该组合产品的状态呢?

    (这个bug在官方论坛上有人提出来过,但不知道为什么到现在还没解决,我试过8.0的版本,依然存在这个问题)

    没有找到官方的解决方案,又看着销售订单里那一排排的“销售订单”状态十分不爽,用自己的方式来解决这个问题:

    1.我尝试过手动改掉组合产品的状态,强制变为done状态,无效。
    2.尝试过程中发现,销售订单确认的时候会调用一边test_state方法,然后到发货完成都没再调用
    3.当手动点order_line中的Process Entirely按钮后,工作流完成了。

    于是,我想着手动触发这个工作流继续往下走。

    1.重写test_state()方法,使之在判断出是phantom类型的产品且所有子产品全为done状态时返回True

    def test_state(self, cr, uid, ids, mode, *args):<br />&nbsp; &nbsp; &nbsp; &nbsp; assert mode in (&#039;finished&#039;, &#039;canceled&#039;), _(&quot;invalid mode for test_state&quot;)<br />&nbsp; &nbsp; &nbsp; &nbsp; finished = True<br />&nbsp; &nbsp; &nbsp; &nbsp; canceled = False<br />&nbsp; &nbsp; &nbsp; &nbsp; write_done_ids = &#91;]<br />&nbsp; &nbsp; &nbsp; &nbsp; write_cancel_ids = &#91;]<br />&nbsp; &nbsp; &nbsp; &nbsp; for order in self.browse(cr, uid, ids, context={}):<br />&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; for line in order.order_line:<br />&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; if (not line.procurement_id) or (line.procurement_id.state==&#039;done&#039;):<br />&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; if line.state != &#039;done&#039;:<br />&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; write_done_ids.append(line.id)<br />&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; else:<br />		&nbsp; &nbsp; mrp = self.pool.get(&#039;mrp.bom&#039;).search(cr,uid,[(&#039;product_id&#039;,&#039;=&#039;,line.product_id.id),(&#039;bom_id&#039;,&#039;=&#039;,False),(&#039;type&#039;,&#039;=&#039;,&#039;phantom&#039;)],context={})<br />		&nbsp; &nbsp; if len(mrp):<br />			for move_line in line.move_ids:<br />			&nbsp; &nbsp; move_line.product_id.id,line.product_id.id,move_line.state<br />			&nbsp; &nbsp; if move_line.product_id.id==line.product_id.id:<br />			&nbsp; &nbsp; &nbsp; continue<br />			&nbsp; &nbsp; <br />			&nbsp; &nbsp; if move_line.product_id.id != line.product_id.id and move_line.state!=&#039;done&#039;:<br />				finished=False<br />		&nbsp; &nbsp; else:<br />			finished = False<br />&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; if line.procurement_id:<br />&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; if (line.procurement_id.state == &#039;cancel&#039;):<br />&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; canceled = True<br />&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; if line.state != &#039;exception&#039;:<br />&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; write_cancel_ids.append(line.id)<br />&nbsp; &nbsp; &nbsp; &nbsp; if write_done_ids:<br />&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; self.pool.get(&#039;sale.order.line&#039;).write(cr, uid, write_done_ids, {&#039;state&#039;: &#039;done&#039;})<br />&nbsp; &nbsp; &nbsp; &nbsp; if write_cancel_ids:<br />&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; self.pool.get(&#039;sale.order.line&#039;).write(cr, uid, write_cancel_ids, {&#039;state&#039;: &#039;exception&#039;})<br /><br />&nbsp; &nbsp; &nbsp; &nbsp; if mode == &#039;finished&#039;:<br />&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; return finished<br />&nbsp; &nbsp; &nbsp; &nbsp; elif mode == &#039;canceled&#039;:<br />&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; return canceled
    


    2.在stock.move的action_done()里触发对应的组合产品的需求单工作流往下走。

    <br />wf_service = netsvc.LocalService(&quot;workflow&quot;)<br />	stock_move = self.browse(cr,uid,ids[0],context=context)<br />	sale_order_ids = self.pool.get(&#039;sale.order&#039;).search(cr,uid,[(&#039;name&#039;,&#039;=&#039;,stock_move.origin)],context=context)<br />	if len(sale_order_ids):<br />	&nbsp; &nbsp; sale_order = self.pool.get(&#039;sale.order&#039;).browse(cr,uid,sale_order_ids[0],context=context)<br />	&nbsp; &nbsp; for line in sale_order.order_line:<br />		mrp = self.pool.get(&#039;mrp.bom&#039;).search(cr,uid,[(&#039;product_id&#039;,&#039;=&#039;,line.product_id.id),(&#039;bom_id&#039;,&#039;=&#039;,False),(&#039;type&#039;,&#039;=&#039;,&#039;phantom&#039;)],context={})<br />		if len(mrp):<br />		&nbsp; &nbsp; p_order_ids = self.pool.get(&#039;procurement.order&#039;).search(cr,uid,[(&#039;origin&#039;,&#039;=&#039;,stock_move.origin),(&#039;product_id&#039;,&#039;=&#039;,line.product_id.id)],context=context)<br />		&nbsp; &nbsp; <br />		&nbsp; &nbsp; if len(p_order_ids):<br />			wf_service.trg_trigger(uid, &#039;procurement.order&#039;, p_order_ids[0],cr)<br />
    



    虽说这样是解决了问题,但感觉方法还不是最好,坐等官方版本,欢迎各位大神提出更好的思路进行分享。



  • 楼主多图做出分享,
    十分感谢。



  • 拜读下!!先评价再仔细的看下


  • 管理员

    感谢Kevin图文并茂,认真仔细的分析。
    看了一下,这个问题产生的原因是在‘stock.move'的‘action_done'方法上,action_done中

    <br /><br />&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; if move.move_dest_id.id and (move.state != &#039;done&#039;):<br />&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; # Downstream move should only be triggered if this move is the last pending upstream move<br />&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; other_upstream_move_ids = self.search(cr, uid, [(&#039;id&#039;,&#039;!=&#039;,move.id),(&#039;state&#039;,&#039;not in&#039;,&#91;&#039;done&#039;,&#039;cancel&#039;]),<br />&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; (&#039;move_dest_id&#039;,&#039;=&#039;,move.move_dest_id.id)], context=context)<br />&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; if not other_upstream_move_ids:<br />&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; self.write(cr, uid, [move.id], {&#039;move_history_ids&#039;: [(4, move.move_dest_id.id)]})<br />&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; if move.move_dest_id.state in (&#039;waiting&#039;, &#039;confirmed&#039;):<br />&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; self.force_assign(cr, uid, [move.move_dest_id.id], context=context)<br />&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; if move.move_dest_id.picking_id:<br />&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; wf_service.trg_write(uid, &#039;stock.picking&#039;, move.move_dest_id.picking_id.id, cr)<br />&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; if move.move_dest_id.auto_validate:<br />&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; self.action_done(cr, uid, [move.move_dest_id.id], context=context)<br />
    


    这部分代码的用意是当一个库存移动有关联的连锁移动时(move_dest_id有值时),检查当前move是否是与move_dest_id关联的所有上游连锁move中最后一个完成(done)的move,如果是则将move_dest_id也改为完成(done)状态。
    <pre>
    M1-------|
    M2-------|(move_dest_id.id)
    Mx-------|------------------------>M[sub]downstream[/sub]
    :            |
    Mn-------|
    </pre>
    即如果有M1~Mn个move都链接到[font=verdana]M[/font][font=verdana][sub]downstream/sub, 如果Mx是最后一个完成的Move(即当Mx完成后,M1~Mn的所有Move都进入done状态),则系统对[/font][size=10px][font=verdana]M[/font][/size][font=verdana][sub]downstream[/sub]调用action_done方法并将其状态改写为'done'[/font]


    问题是,如果M1~Mn同时进入完成状态时,即action_done函数调用时其传入的ids值中包含M1~Mn的id值,因为action_done是在最后一次性改成done状态的,而不是每处理一个move就改变它的状态,所以

    <br />other_upstream_move_ids = self.search(cr, uid, [(&#039;id&#039;,&#039;!=&#039;,move.id),(&#039;state&#039;,&#039;not in&#039;,&#91;&#039;done&#039;,&#039;cancel&#039;]), (&#039;move_dest_id&#039;,&#039;=&#039;,move.move_dest_id.id)], context=context)<br />if not other_upstream_move_ids:<br />
    


    [font=verdana]所以other_upstream_move_ids始终有值,无法执行'[/font][font=verdana]if not other_upstream_move_ids:'下的代码, 一个解决方法是将当前的move的状态改写为‘done’的write方法放在[/font]

    <br />for move in self.browse(cr, uid, ids, context=context): <br />
    

    [font=verdana]
    [size=1em]for 循环中。这样每个move分别改变其完成状态,这样就能判断是否是最后一个变成done的库存移动了。[/size][/font][size=1em]
    [font=verdana]当然还有一个方法就是,仍然保持最后一次性修改所有move为done状态,但是判断出是否已经处理了所有的上游move:[/font]
    [font=verdana] http://bazaar.launchpad.net/~openerp-dev/openobject-addons/7.0-opw-607970-acl/revision/9462#stock/stock.py [/font][/size]
    该修改还没有并入7.0分支,在master分支中也已经意识到了该问题,请参见代码中的备注信息:

    <br /><br />&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; if move.move_dest_id.state in (&#039;waiting&#039;, &#039;confirmed&#039;):<br />&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; # FIXME is opw 607970 still present with new WMS?<br />&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; # (see commits 1ef2c181033bd200906fb1e5ce35e234bf566ac6<br />&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; # and 41c5ceb8ebb95c1b4e98d8dd1f12b8e547a24b1d)<br />&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; other_upstream_move_ids = self.search(cr, uid, [(&#039;id&#039;, &#039;!=&#039;, move.id), (&#039;state&#039;, &#039;not in&#039;, &#91;&#039;done&#039;, &#039;cancel&#039;]),<br />&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; (&#039;move_dest_id&#039;, &#039;=&#039;, move.move_dest_id.id)], context=context)<br />
    

    [font=verdana]
    所以,官方应该已经意识到你发现的问题,目前可以暂时使用[size=x-small] http://bazaar.launchpad.net/~openerp-dev/openobject-addons/7.0-opw-607970-acl/revision/9462#stock/stock.py 的修改[/size][/font]


  • 管理员

    楼主和校长都分析得很好,学习了 🙂



  • 谢谢校长的回复,按照您的方法,完美解决该问题 😉