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')的代码:
    [code]def test_state(self, cr, uid, ids, mode, *args):
            assert mode in ('finished', 'canceled'), _("invalid mode for test_state")
            finished = True
            canceled = False
            write_done_ids = []
            write_cancel_ids = []
            for order in self.browse(cr, uid, ids, context={}):
                for line in order.order_line:
                    if (not line.procurement_id) or (line.procurement_id.state=='done'):
                        if line.state != 'done':
                            write_done_ids.append(line.id)
                    else:
                        finished = False
                    if line.procurement_id:
                        if (line.procurement_id.state == 'cancel'):
                            canceled = True
                            if line.state != 'exception':
                                write_cancel_ids.append(line.id)
            if write_done_ids:
                self.pool.get('sale.order.line').write(cr, uid, write_done_ids, {'state': 'done'})
            if write_cancel_ids:
                self.pool.get('sale.order.line').write(cr, uid, write_cancel_ids, {'state': 'exception'})

            if mode == 'finished':
                return finished
            elif mode == 'canceled':
                return canceled[/code]
    经测试,该组合产品会生成一个需求单,并且该需求单的状态一直为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
    [code]def test_state(self, cr, uid, ids, mode, *args):
            assert mode in ('finished', 'canceled'), _("invalid mode for test_state")
            finished = True
            canceled = False
            write_done_ids = []
            write_cancel_ids = []
            for order in self.browse(cr, uid, ids, context={}):
                for line in order.order_line:
                    if (not line.procurement_id) or (line.procurement_id.state=='done'):
                        if line.state != 'done':
                            write_done_ids.append(line.id)
                    else:
        mrp = self.pool.get('mrp.bom').search(cr,uid,[('product_id','=',line.product_id.id),('bom_id','=',False),('type','=','phantom')],context={})
        if len(mrp):
    for move_line in line.move_ids:
        move_line.product_id.id,line.product_id.id,move_line.state
        if move_line.product_id.id==line.product_id.id:
          continue
       
        if move_line.product_id.id != line.product_id.id and move_line.state!='done':
    finished=False
        else:
    finished = False
                    if line.procurement_id:
                        if (line.procurement_id.state == 'cancel'):
                            canceled = True
                            if line.state != 'exception':
                                write_cancel_ids.append(line.id)
            if write_done_ids:
                self.pool.get('sale.order.line').write(cr, uid, write_done_ids, {'state': 'done'})
            if write_cancel_ids:
                self.pool.get('sale.order.line').write(cr, uid, write_cancel_ids, {'state': 'exception'})

            if mode == 'finished':
                return finished
            elif mode == 'canceled':
                return canceled[/code]
    2.在stock.move的action_done()里触发对应的组合产品的需求单工作流往下走。
    [code]
    wf_service = netsvc.LocalService("workflow")
    stock_move = self.browse(cr,uid,ids[0],context=context)
    sale_order_ids = self.pool.get('sale.order').search(cr,uid,[('name','=',stock_move.origin)],context=context)
    if len(sale_order_ids):
        sale_order = self.pool.get('sale.order').browse(cr,uid,sale_order_ids[0],context=context)
        for line in sale_order.order_line:
    mrp = self.pool.get('mrp.bom').search(cr,uid,[('product_id','=',line.product_id.id),('bom_id','=',False),('type','=','phantom')],context={})
    if len(mrp):
        p_order_ids = self.pool.get('procurement.order').search(cr,uid,[('origin','=',stock_move.origin),('product_id','=',line.product_id.id)],context=context)
       
        if len(p_order_ids):
    wf_service.trg_trigger(uid, 'procurement.order', p_order_ids[0],cr)
    [/code]

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



  • 首先说一下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')的代码:
    [code]def test_state(self, cr, uid, ids, mode, *args):
            assert mode in ('finished', 'canceled'), _("invalid mode for test_state")
            finished = True
            canceled = False
            write_done_ids = []
            write_cancel_ids = []
            for order in self.browse(cr, uid, ids, context={}):
                for line in order.order_line:
                    if (not line.procurement_id) or (line.procurement_id.state=='done'):
                        if line.state != 'done':
                            write_done_ids.append(line.id)
                    else:
                        finished = False
                    if line.procurement_id:
                        if (line.procurement_id.state == 'cancel'):
                            canceled = True
                            if line.state != 'exception':
                                write_cancel_ids.append(line.id)
            if write_done_ids:
                self.pool.get('sale.order.line').write(cr, uid, write_done_ids, {'state': 'done'})
            if write_cancel_ids:
                self.pool.get('sale.order.line').write(cr, uid, write_cancel_ids, {'state': 'exception'})

            if mode == 'finished':
                return finished
            elif mode == 'canceled':
                return canceled[/code]
    经测试,该组合产品会生成一个需求单,并且该需求单的状态一直为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
    [code]def test_state(self, cr, uid, ids, mode, *args):
            assert mode in ('finished', 'canceled'), _("invalid mode for test_state")
            finished = True
            canceled = False
            write_done_ids = []
            write_cancel_ids = []
            for order in self.browse(cr, uid, ids, context={}):
                for line in order.order_line:
                    if (not line.procurement_id) or (line.procurement_id.state=='done'):
                        if line.state != 'done':
                            write_done_ids.append(line.id)
                    else:
        mrp = self.pool.get('mrp.bom').search(cr,uid,[('product_id','=',line.product_id.id),('bom_id','=',False),('type','=','phantom')],context={})
        if len(mrp):
    for move_line in line.move_ids:
        move_line.product_id.id,line.product_id.id,move_line.state
        if move_line.product_id.id==line.product_id.id:
          continue
       
        if move_line.product_id.id != line.product_id.id and move_line.state!='done':
    finished=False
        else:
    finished = False
                    if line.procurement_id:
                        if (line.procurement_id.state == 'cancel'):
                            canceled = True
                            if line.state != 'exception':
                                write_cancel_ids.append(line.id)
            if write_done_ids:
                self.pool.get('sale.order.line').write(cr, uid, write_done_ids, {'state': 'done'})
            if write_cancel_ids:
                self.pool.get('sale.order.line').write(cr, uid, write_cancel_ids, {'state': 'exception'})

            if mode == 'finished':
                return finished
            elif mode == 'canceled':
                return canceled[/code]
    2.在stock.move的action_done()里触发对应的组合产品的需求单工作流往下走。
    [code]
    wf_service = netsvc.LocalService("workflow")
    stock_move = self.browse(cr,uid,ids[0],context=context)
    sale_order_ids = self.pool.get('sale.order').search(cr,uid,[('name','=',stock_move.origin)],context=context)
    if len(sale_order_ids):
        sale_order = self.pool.get('sale.order').browse(cr,uid,sale_order_ids[0],context=context)
        for line in sale_order.order_line:
    mrp = self.pool.get('mrp.bom').search(cr,uid,[('product_id','=',line.product_id.id),('bom_id','=',False),('type','=','phantom')],context={})
    if len(mrp):
        p_order_ids = self.pool.get('procurement.order').search(cr,uid,[('origin','=',stock_move.origin),('product_id','=',line.product_id.id)],context=context)
       
        if len(p_order_ids):
    wf_service.trg_trigger(uid, 'procurement.order', p_order_ids[0],cr)
    [/code]

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



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



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


  • 管理员

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

                if move.move_dest_id.id and (move.state != 'done'):
                    # Downstream move should only be triggered if this move is the last pending upstream move
                    other_upstream_move_ids = self.search(cr, uid, [('id','!=',move.id),('state','not in',['done','cancel']),
                                                ('move_dest_id','=',move.move_dest_id.id)], context=context)
                    if not other_upstream_move_ids:
                        self.write(cr, uid, [move.id], {'move_history_ids': [(4, move.move_dest_id.id)]})
                        if move.move_dest_id.state in ('waiting', 'confirmed'):
                            self.force_assign(cr, uid, [move.move_dest_id.id], context=context)
                            if move.move_dest_id.picking_id:
                                wf_service.trg_write(uid, 'stock.picking', move.move_dest_id.picking_id.id, cr)
                            if move.move_dest_id.auto_validate:
                                self.action_done(cr, uid, [move.move_dest_id.id], context=context)
    [/code]
    这部分代码的用意是当一个库存移动有关联的连锁移动时(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就改变它的状态,所以
    [code]
    other_upstream_move_ids = self.search(cr, uid, [('id','!=',move.id),('state','not in',['done','cancel']), ('move_dest_id','=',move.move_dest_id.id)], context=context)
    if not other_upstream_move_ids:
    [/code]
    [font=verdana]所以other_upstream_move_ids始终有值,无法执行'[/font][font=verdana]if not other_upstream_move_ids:'下的代码, 一个解决方法是将当前的move的状态改写为‘done’的write方法放在[/font]
    [code]
    for move in self.browse(cr, uid, ids, context=context):
    [/code][font=verdana]
    [size=1em]for 循环中。这样每个move分别改变其完成状态,这样就能判断是否是最后一个变成done的库存移动了。[/size][/font][size=1em]
    [font=verdana]当然还有一个方法就是,仍然保持最后一次性修改所有move为done状态,但是判断出是否已经处理了所有的上游move:[/font]
    [font=verdana][url=http://bazaar.launchpad.net/~openerp-dev/openobject-addons/7.0-opw-607970-acl/revision/9462#stock/stock.py]http://bazaar.launchpad.net/~openerp-dev/openobject-addons/7.0-opw-607970-acl/revision/9462#stock/stock.py[/url][/font][/size]
    该修改还没有并入7.0分支,在master分支中也已经意识到了该问题,请参见代码中的备注信息:
    [code]

                if move.move_dest_id.state in ('waiting', 'confirmed'):
                    # FIXME is opw 607970 still present with new WMS?
                    # (see commits 1ef2c181033bd200906fb1e5ce35e234bf566ac6
                    # and 41c5ceb8ebb95c1b4e98d8dd1f12b8e547a24b1d)
                    other_upstream_move_ids = self.search(cr, uid, [('id', '!=', move.id), ('state', 'not in', ['done', 'cancel']),
                                                ('move_dest_id', '=', move.move_dest_id.id)], context=context)
    [/code][font=verdana]
    所以,官方应该已经意识到你发现的问题,目前可以暂时使用[size=x-small][url=http://bazaar.launchpad.net/~openerp-dev/openobject-addons/7.0-opw-607970-acl/revision/9462#stock/stock.py]http://bazaar.launchpad.net/~openerp-dev/openobject-addons/7.0-opw-607970-acl/revision/9462#stock/stock.py[/url]的修改[/size][/font]


  • 管理员

    楼主和校长都分析得很好,学习了 :-)



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


登录后回复
 

与 Odoo 中文社区 的连接断开,我们正在尝试重连,请耐心等待