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

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

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

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

Openerp BOM成本计算错误问题的解决。



  • 我所在的公司是一家以制造为主的企业,在成品生产的过程中,会先生产出低级别的半成品,再一级一级组装,生产出最终的成品。其中,有一些半成品是委托其他公司生产的(原材料由我们提供),库房对半成品单独管理,为了应对市场需求,有的半成品会预先生产好。 因此在做BOM定义的时候,半成品的BOM类型只能定义为Normal,不能定义为phantom。

    这样就发现了一个问题(此问题在openerp6.1和openerp7.0均存在),打印产品成本构成时,成本计算到半成品就结束了,有时候原材料的数量计算也有问题。但是打印BOM结构又完全正确,当时百思不得期间,一个偶然的机会,把所有的半成品的BOM类型都改为phantom,一切OK。但这不是我想要的,于是跟踪调测mrp部分的代码,比较部件BOM类型为Normal和phantom的成本计算代码,终于解决了这个问题,需要修改addons/mrp/mrp.py中的_bom_explpde函数,
    (以openerp 7.0为例,见蓝色字体),经过分析此函数仅在打印BOM成本构成时被调用,不会影响其他功能。


      def _bom_explode(self, cr, uid, bom, factor, properties=None, addthis=False, level=0, routing_id=False):
            """ Finds Products and Work Centers for related BoM for manufacturing order.
            @param bom: BoM of particular product.
            @param factor: Factor of product UoM.
            @param properties: A List of properties Ids.
            @param addthis: If BoM found then True else False.
            @param level: Depth level to find BoM lines starts from 10.
            @return: result: List of dictionaries containing product details.
                    result2: List of dictionaries containing Work Center details.
            """
            routing_obj = self.pool.get('mrp.routing')
            factor = factor / (bom.product_efficiency or 1.0)
            factor = rounding(factor, bom.product_rounding)
            if factor < bom.product_rounding:
                factor = bom.product_rounding
            result = []
            result2 = []
            phantom = False
            if bom.type == 'phantom' and not bom.bom_lines:
                newbom = self._bom_find(cr, uid, bom.product_id.id, bom.product_uom.id, properties)

                if newbom:
                    res = self._bom_explode(cr, uid, self.browse(cr, uid, [newbom])[0], factorbom.product_qty, properties, addthis=True, level=level+10)
                    result = result + res[0]
                    result2 = result2 + res[1]
                    phantom = True
                else:
                    phantom = False
            if not phantom:
                #下面两行为新增,修正bom_lines计算错误问题。
    [color=blue]            bom_id = self._bom_find(cr, uid, bom.product_id.id, bom.product_uom.id, properties)
                bom_lines = self.browse(cr, uid, bom_id).bom_lines[/color]
                if addthis and not bom_lines:
                    result.append(
                    {
                        'name': bom.product_id.name,
                        'product_id': bom.product_id.id,
                        'product_qty': bom.product_qty * factor,
                        'product_uom': bom.product_uom.id,
                        'product_uos_qty': bom.product_uos and bom.product_uos_qty * factor or False,
                        'product_uos': bom.product_uos and bom.product_uos.id or False,
                    })
                routing = (routing_id and routing_obj.browse(cr, uid, routing_id)) or bom.routing_id or False
                if routing:
                    for wc_use in routing.workcenter_lines:
                        wc = wc_use.workcenter_id
                        d, m = divmod(factor, wc_use.workcenter_id.capacity_per_cycle)
                        mult = (d + (m and 1.0 or 0.0))
                        cycle = mult * wc_use.cycle_nbr
                        result2.append({
                            'name': tools.ustr(wc_use.name) + ' - '  + tools.ustr(bom.product_id.name),
                            'workcenter_id': wc.id,
                            'sequence': level+(wc_use.sequence or 0),
                            'cycle': cycle,
                            'hour': float(wc_use.hour_nbr
    mult + ((wc.time_start or 0.0)+(wc.time_stop or 0.0)+cycle*(wc.time_cycle or 0.0)) * (wc.time_efficiency or 1.0)),
                        })
                #从正确的bom_lines中取数据       
    [color=blue]            for bom2 in bom_lines:[/color]
    #          for bom2 in bom.bom_lines:
                    #修改RES的计算,解决成本计算中原材料数量不对问题
                    [color=blue]res = self._bom_explode(cr, uid, bom2, factor*bom.product_qty, properties, addthis=True, level=level+10)[/color]
    #              res = self._bom_explode(cr, uid, bom2, factor, properties, addthis=True, level=level+10)
                    result = result + res[0]
                    result2 = result2 + res[1]
            return result, result2



  • 我所在的公司是一家以制造为主的企业,在成品生产的过程中,会先生产出低级别的半成品,再一级一级组装,生产出最终的成品。其中,有一些半成品是委托其他公司生产的(原材料由我们提供),库房对半成品单独管理,为了应对市场需求,有的半成品会预先生产好。 因此在做BOM定义的时候,半成品的BOM类型只能定义为Normal,不能定义为phantom。

    这样就发现了一个问题(此问题在openerp6.1和openerp7.0均存在),打印产品成本构成时,成本计算到半成品就结束了,有时候原材料的数量计算也有问题。但是打印BOM结构又完全正确,当时百思不得期间,一个偶然的机会,把所有的半成品的BOM类型都改为phantom,一切OK。但这不是我想要的,于是跟踪调测mrp部分的代码,比较部件BOM类型为Normal和phantom的成本计算代码,终于解决了这个问题,需要修改addons/mrp/mrp.py中的_bom_explpde函数,
    (以openerp 7.0为例,见蓝色字体),经过分析此函数仅在打印BOM成本构成时被调用,不会影响其他功能。


      def _bom_explode(self, cr, uid, bom, factor, properties=None, addthis=False, level=0, routing_id=False):
            """ Finds Products and Work Centers for related BoM for manufacturing order.
            @param bom: BoM of particular product.
            @param factor: Factor of product UoM.
            @param properties: A List of properties Ids.
            @param addthis: If BoM found then True else False.
            @param level: Depth level to find BoM lines starts from 10.
            @return: result: List of dictionaries containing product details.
                    result2: List of dictionaries containing Work Center details.
            """
            routing_obj = self.pool.get('mrp.routing')
            factor = factor / (bom.product_efficiency or 1.0)
            factor = rounding(factor, bom.product_rounding)
            if factor < bom.product_rounding:
                factor = bom.product_rounding
            result = []
            result2 = []
            phantom = False
            if bom.type == 'phantom' and not bom.bom_lines:
                newbom = self._bom_find(cr, uid, bom.product_id.id, bom.product_uom.id, properties)

                if newbom:
                    res = self._bom_explode(cr, uid, self.browse(cr, uid, [newbom])[0], factorbom.product_qty, properties, addthis=True, level=level+10)
                    result = result + res[0]
                    result2 = result2 + res[1]
                    phantom = True
                else:
                    phantom = False
            if not phantom:
                #下面两行为新增,修正bom_lines计算错误问题。
    [color=blue]            bom_id = self._bom_find(cr, uid, bom.product_id.id, bom.product_uom.id, properties)
                bom_lines = self.browse(cr, uid, bom_id).bom_lines[/color]
                if addthis and not bom_lines:
                    result.append(
                    {
                        'name': bom.product_id.name,
                        'product_id': bom.product_id.id,
                        'product_qty': bom.product_qty * factor,
                        'product_uom': bom.product_uom.id,
                        'product_uos_qty': bom.product_uos and bom.product_uos_qty * factor or False,
                        'product_uos': bom.product_uos and bom.product_uos.id or False,
                    })
                routing = (routing_id and routing_obj.browse(cr, uid, routing_id)) or bom.routing_id or False
                if routing:
                    for wc_use in routing.workcenter_lines:
                        wc = wc_use.workcenter_id
                        d, m = divmod(factor, wc_use.workcenter_id.capacity_per_cycle)
                        mult = (d + (m and 1.0 or 0.0))
                        cycle = mult * wc_use.cycle_nbr
                        result2.append({
                            'name': tools.ustr(wc_use.name) + ' - '  + tools.ustr(bom.product_id.name),
                            'workcenter_id': wc.id,
                            'sequence': level+(wc_use.sequence or 0),
                            'cycle': cycle,
                            'hour': float(wc_use.hour_nbr
    mult + ((wc.time_start or 0.0)+(wc.time_stop or 0.0)+cycle*(wc.time_cycle or 0.0)) * (wc.time_efficiency or 1.0)),
                        })
                #从正确的bom_lines中取数据       
    [color=blue]            for bom2 in bom_lines:[/color]
    #          for bom2 in bom.bom_lines:
                    #修改RES的计算,解决成本计算中原材料数量不对问题
                    [color=blue]res = self._bom_explode(cr, uid, bom2, factor*bom.product_qty, properties, addthis=True, level=level+10)[/color]
    #              res = self._bom_explode(cr, uid, bom2, factor, properties, addthis=True, level=level+10)
                    result = result + res[0]
                    result2 = result2 + res[1]
            return result, result2



  • 关注... 谢谢分享...



  • 上层的数量没传下去啊,这。。。 应该到官方报个bug



  • 已经上报了官方的bug,但没有响应,只好自己解决了。呵呵。


  • 管理员

    @bluesthink 非常感谢您的分享。 我看了一下您的修改,感觉是有问题的,跟你探讨一下。


    首先,你说_bom_explode只在打印成本构成时被调用,你的修改不会影响其他流程。这个说法是错误的,_bom_explode函数在生产单确认时会被调用用以分解BOM来计算生产所需物料,所以_bom_explode在生产流程中有非常大的影响作用。


    你的修改主要基于此
    [code]bom_id = self._bom_find(cr, uid, bom.product_id.id, bom.product_uom.id, properties)
    bom_lines = self.browse(cr, uid, bom_id).bom_lines[/code]
    就是在当前bom为非phantom BOM时您调用bom_find来找到一个与当前BOM有同样产品的一个不同(也可能相同)的BOM并基于这个BOM来做展开运算。


    我们现在把问题简化,不考虑Phantom和子BOM分解的情况。比如我要分解一个一层结构的Normal BOM, 那么程序就直接执行到了你修改的那一行,然后去bom_find,如果找到的bom与当前BOM是一样的,倒也不影响计算结果,但是如果找到的bom与当前的bom不同,则就有问题了。


    我考虑到的可能造成问题的一种场景是这样的:
    比如说我们有一个产品A, 我们为他设置了不同的BOM: BOM-A1, BOM-A2 (比如可能是Bom-A2中有一个与Bom-A1不同的替代料)
    在生产时,由于原材料库存的原因,我们专门为产品A指定了BOM-A2作为其生产BOM,但是经过你的修改,系统会计算后用BOM-A1的物料进行生产,因为bom_find在不考虑property时总是返回最先找到的那个BOM。


    更严重的是,对于有子BOM的多层次BOM,用你的修改,就无法在创建最终产品的生产单后,通过mrp运算自动生成中间产品的生产单。事实上你似乎把Normal Bom完全和phantom的处理方法相同,一张最终产成品的生产单的物料展开中包括了所有子产品的物料和中间物料。  :o


    对于你成本计算错误的说法,你可以举例说明,我们进一步探讨,可能是你理解的问题,你的修改应该是问题。



  • 1、对于一个产品多BOM的情况,应该是用到了properties,
    bom_id = self._bom_find(cr, uid, bom.product_id.id, bom.product_uom.id, properties)
    bom_id是按照propertiers查询的,能够支持一个产品多个属性的情况。若查询不到,那就用最接近的。

    2、为什么要这样修改,可能我没有说清楚。

    这样修改的原因是:原有bom.bom_lines的值有错误,为什么呢,和openerp BOM数据存放方式有关系

    若有产品 X2,bom结构定义如下:
    x2 1.000 Unit(s)
    - a2 0.700 Unit(s)
    - x1 3.000 Unit(s)
    ... - a1 0.200 Unit(s)

    那么在mrp_bom表中的数据如下:(为便于理解,仅列出主要的几个字段)
    id product_uom product_qty product_efficiency name product_id bom_id type
    6 1                 1                 1                         x3 7                 normal
    7 1                 2                 1                         x2 5         6         normal
    1 1                 1                 1                         x1 3                 normal
    2 1                 0.2                 1                         a1 2         1         normal
    3 1                 1                 1                         x2 5                 normal
    5 1                 0.7                 1                         a2 6         3         normal
    8 1                 3                 1                         x1 3         3         normal

    从表中数据可以看到x2,x1有两条记录,一条记录描述了本级别的BOM项,一条记录描述了和上级BOM之间的关系。 a1只被X1引用,没有下级,故只有
    一条记录,a2只被x2引用,没有下级,故只有一条记录。

    x2本身的id是3,那么从X2获取的子bom_lines数据,分别对应id为5和8的记录,其中8和X1相关,注意问题就出在这个地方,如果按照Bom_id为8去
    获取Bom_line子项,是无法的到数据的,这就是为何BOM成本计算时计算到半成品就结束的原因。

    bom_id = self._bom_find(cr, uid, bom.product_id.id, bom.product_uom.id, properties)的目的是找到X1本级别的BOM项,也就是id为1的那条记录
    数据,通过bom_id=1做关联,恰好找到id为2的Bom子项目,也就是说bom_lines = self.browse(cr, uid, bom_id).bom_lines获取的子项才是正确的。

    3、第三个问题,看了一下,确实_bom_explode函数在别的地方也可能有调用。这是我的失误。




  • 管理员

    1. 理解正确


      2. [font=verdana][size=11px]前面的说明都是正确的,但是[/size][/font]
      [quote][font=verdana][font=Verdana][size=78%]x[/size][/font][/font][font=verdana][size=11px]2本身的id是3,那么从X2获取的子bom_lines数据,分别对应id为5和8的记录,其中8和X1相关,注意问题就出在这个地方,如果按照Bom_id为8去[/size][/font][font=verdana][size=11px]获取Bom_line子项,是无法的到数据的,这就是为何BOM成本计算时计算到半成品就结束的原因[/size][/font][/quote]
      这个结论是错误的,bom_explode所做的分解是到bom_line所对应的bom(normal)没有bom_line为止的(这个有点绕 ;) ),也就是说它不会去分解子产品的bom的。这也是为什么你确认产成品的生产单后,在所需原材料中只显示子产品(而非子产品的所有物料)的原因。


      你所做的修改是将bom_explode的设计本意彻底修改了,这样的确能迭代计算所有的子产品的物料,但是, 8) ,mrp的运算也从此限于一片混沌中。


      不相信的话,你可以确认一张成品的生产单,看看算出来的物料是什么样子的。


      我其实理解你需要的结果,你希望能迭代分解子bom以获得象Bom Structure这样的展开结构,然后运算其材料成本,那么你完全可以用mrp.bom中的 _child_compute来实现。


      我理解的是,这不能算一个bug,这是你的一个需求,而实现的方式方式不是很正确。


  • 感谢指导,可是在这种情况下打印BOM成本结构,确实有问题啊,到半成品就结束了,而且半成品的成本显示为0.应该如何修改呢?



  • 昨天想了一下,问题的根本在于_bom_explode函数要同时支持生产和成本计算,对于生产来说,mrp投料是一级一级的计算,不能一下子直接算出所有的原材料,否则库存中已有的半成品就消耗不掉了。而对于成本构成来说,是需要算出产品所有的原材料的。满足了生产的需求,就无法满足成本计算了。

    因此解决的方法:

    1、在addons/mrp/mrp.py中新建一个_bom_explode2函数

    def _bom_explode2(self, cr, uid, bom, factor, properties=None, addthis=False, level=0, routing_id=False):
            """ Finds Products and Work Centers for related BoM for manufacturing order.
            @param bom: BoM of particular product.
            @param factor: Factor of product UoM.
            @param properties: A List of properties Ids.
            @param addthis: If BoM found then True else False.
            @param level: Depth level to find BoM lines starts from 10.
            @return: result: List of dictionaries containing product details.
                    result2: List of dictionaries containing Work Center details.
            """
            routing_obj = self.pool.get('mrp.routing')
            factor = factor / (bom.product_efficiency or 1.0)
            factor = rounding(factor, bom.product_rounding)
            if factor < bom.product_rounding:
                factor = bom.product_rounding
            result = []
            result2 = []
            phantom = False
            if bom.type == 'phantom' and not bom.bom_lines:
                newbom = self._bom_find(cr, uid, bom.product_id.id, bom.product_uom.id, properties)

                if newbom:
    [color=blue]                res = self._bom_explode2(cr, uid, self.browse(cr, uid, [newbom])[0], factorbom.product_qty, properties, addthis=True, level=level+10)[/color]
                    result = result + res[0]
                    result2 = result2 + res[1]
                    phantom = True
                else:
                    phantom = False
            if not phantom:
                #下面三行为新增和修改,修正成本计算时bom_lines计算错误问题。
    [color=blue]            bom_id = self._bom_find(cr, uid, bom.product_id.id, bom.product_uom.id, properties)
                bom_lines = self.browse(cr, uid, bom_id).bom_lines
                if addthis and not bom_lines:[/color]   
                    result.append(
                    {
                        'name': bom.product_id.name,
                        'product_id': bom.product_id.id,
                        'product_qty': bom.product_qty * factor,
                        'product_uom': bom.product_uom.id,
                        'product_uos_qty': bom.product_uos and bom.product_uos_qty * factor or False,
                        'product_uos': bom.product_uos and bom.product_uos.id or False,
                    })
                routing = (routing_id and routing_obj.browse(cr, uid, routing_id)) or bom.routing_id or False
                if routing:
                    for wc_use in routing.workcenter_lines:
                        wc = wc_use.workcenter_id
                        d, m = divmod(factor, wc_use.workcenter_id.capacity_per_cycle)
                        mult = (d + (m and 1.0 or 0.0))
                        cycle = mult * wc_use.cycle_nbr
                        result2.append({
                            'name': tools.ustr(wc_use.name) + ' - '  + tools.ustr(bom.product_id.name),
                            'workcenter_id': wc.id,
                            'sequence': level+(wc_use.sequence or 0),
                            'cycle': cycle,
                            'hour': float(wc_use.hour_nbr
    mult + ((wc.time_start or 0.0)+(wc.time_stop or 0.0)+cycle*(wc.time_cycle or 0.0)) * (wc.time_efficiency or 1.0)),
                        })
                #从正确的bom_lines中取数据       
    [color=blue]            for bom2 in bom_lines:[/color]
    #          for bom2 in bom.bom_lines:
                    #修改RES的计算,解决成本计算中原材料数量不对问题
    [color=blue]                res = self._bom_explode2(cr, uid, bom2, factor*bom.product_qty, properties, addthis=True, level=level+10)[/color]
    #              res = self._bom_explode(cr, uid, bom2, factor, properties, addthis=True, level=level+10)
                    result = result + res[0]
                    result2 = result2 + res[1]
            return result, result2

    2、修改addons/mrp/report/price.py
    [color=blue]                  sub_boms = bom_pool._bom_explode2(cr, uid, bom, factor / bom.product_qty)[/color]
    #                sub_boms = bom_pool._bom_explode(cr, uid, bom, factor / bom.product_qty)



  • 弱弱地问一句...

    BOM中的成本计算, 是不是仅仅 就是下面子项的成本价*需求量之和呢?


  • 管理员

    @bluethink 解决方法很简单啊 ;D 。 如果这符合贵公司的要求也未尝不可。 但是,要知道这种成本的计算是很不准确的,对于一个产品有多个BOM的情况,你无法选择参与成本运算的BOM,你也无法选择或指定BOM property,希望你可以继续改进。

    另外, 你的成本运算只考虑材料成本而没有考虑制造费用,理论上也是不合理的。 这里有一个模块: [url=https://apps.openerp.com/apps/product_cost_incl_bom/]https://apps.openerp.com/apps/product_cost_incl_bom/[/url]
    他定义了一个cost_price的函数字段,如果产品有BOM,则会计算组成该产品的物料的Standard Price之和再加上根据routing计算出的制造费用。

    [b]注:[/b]如果将这个模块中计算子产品的standar_price改成新定义的cost_price, 就可以循环递归运算所有的下级bom的物料成本和相应的制造费用了。


  • 管理员

    [quote author=mrshelly link=topic=10066.msg21808#msg21808 date=1377742115]
    弱弱地问一句...

    BOM中的成本计算, 是不是仅仅 就是下面子项的成本价需求量之和呢?
    [/quote]

    默认就是用子项的标准成本价
    需求量计算的,只要楼主给那些半成品定义了标准成本价,就可以计算出成品的成本。但是,楼主希望那个半成品的标准成本不用手动输入,而是应该迭代动态运算出来。



  • https://apps.openerp.com/apps/product_cost_incl_bom/    如果有工艺线路,只计算了循环成本,没有计算小时成本,不知道哪个逻辑是对的,而打印成本结构 将小时成本计算进去了。[img][/img]



  • [quote author=digitalsatori link=topic=10066.msg21809#msg21809 date=1377742995]
    @bluethink 解决方法很简单啊 ;D 。 如果这符合贵公司的要求也未尝不可。 但是,要知道这种成本的计算是很不准确的,对于一个产品有多个BOM的情况,你无法选择参与成本运算的BOM,你也无法选择或指定BOM property,希望你可以继续改进。

    另外, 你的成本运算只考虑材料成本而没有考虑制造费用,理论上也是不合理的。 这里有一个模块: [url=https://apps.openerp.com/apps/product_cost_incl_bom/]https://apps.openerp.com/apps/product_cost_incl_bom/[/url]
    他定义了一个cost_price的函数字段,如果产品有BOM,则会计算组成该产品的物料的Standard Price之和再加上根据routing计算出的制造费用。

    [b]注:[/b]如果将这个模块中计算子产品的standar_price改成新定义的cost_price, 就可以循环递归运算所有的下级bom的物料成本和相应的制造费用了。
    [/quote]

    1、目前我公司暂时没有用到多属性,而且老板关心的是物料成本,所以这样的改动目前能够满足要求。

    2、对于制造费用分摊,有两种方式,一种是按照工时分摊,一种是按照产品成本(含新产品试制成本)分摊,我理解openerp支持的是前一种。而且对于第一种来说,由于_bom_explode2对于routing和workcenter信息是有返回的,那么report/price.py还是会做这部分的计算的。(不知理解是否正确)

    3、product_cost_incl_bom这个模块我用过,是有一定的改善,但还是存在有的半成品没有展开计算以及数量不对的情况(当设置参数为小数点的时,比如将一个外购的大尺寸PVC板切割加工成小尺寸的PVC板,就需要设置类似A<-0.2B之类的BOM定义。这个时候这个模块的计算会出现错误,可以设置3级以上BOM试验一下,一定会出问题)


登录后回复
 

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