供应商价目表的自动匹配

在上一篇的odoo commit每日一读/10-a9a189中,我们提到了product中的select_seller()函数,这个函数使用在哪里的呢?

odoo中SO(Sale Order)跟PO(Purchase Order)又是如何关联在一起的呢?

带着这些疑问,让我们来对odoo的源码进行深入的探究。

理论基础

在odoo中,根据产品的供应方式(Supply Method)不同(Buy or Produce) ,我们可以通过外购,或者自产来满足需求;

同时根据产品的类型不同,需求可以通过

  • 采购单(Supply Method + Product Type:Buy + Stockable Product/Service)
  • 生产单(Produce + Stockable Product)
  • 任务单(Produce + Service)来满足。

物料需求有两种类型:MTO和MTS。

  • MTO类型的需求将不考虑库存情况,即时根据上诉规则转化为对应的业务单据以平衡需求。
  • MTS类型的需求则会检查现有库存,尽可能通过消耗库存来平衡需求,同时由于库存规则的存在,当库存量低于最小库存时会激发补货需求,该需求为MTO类型,因此可转化为采购单,生产单,任务单等。

我们暂时不考虑补货规则,只从MTO的角度来分析Odoo11中产生销售订单后自动关联采购订单的逻辑。

odoo模拟业务操作

根据上面的理论基础,我们使用第一种,通过采购单这一需求方式来模拟一个贸易公司的销售过程。

我们的公司A的主营业务是把国内制造的男生衬衫进行出口。主要的供应商是【海澜】公司。

首先,需要把销售的产品在odoo中创建。

  • 我们的产品是男士衬衫,产品类型是可库存产品. image.png

  • 库存选项中,既然是贸易商品,那勾选【按订单生成】(make to order) image.png

  • 采购中添加我们的供货商 【海澜】,海澜公司要求最少的采购数为10件。 image.png

进入销售模块,创建销售订单。

  • 我们来销售给中国出口商。假设第一个订单量比较小。只需要5件。注意到此时销售订单编号SO025 image.png 点击【确认销售】后创建这个订单。

  • 然后来到采购模块中。我们会发现有一张采购询价单自动关联了SO025。可是,奇怪的是,为什么金额是0呢? image.png

  • 这里就要带入到我们一开始说的commit-a9a189这个提交中的select_seller方法了。 我们来看看采购模块中purchas.py对应的源代码。

    def _make_po_get_domain(self, values, partner):
        ...
        ...
        # domain中保存了供应商的id等, 用来为search方法搜索同一个供应商.
        domain += (
            ('partner_id', '=', partner.id),
            ('state', '=', 'draft'),
            ('picking_type_id', '=', self.picking_type_id.id),
            ('company_id', '=', values['company_id'].id),
            )
        ...
        return domain
    
    @api.multi
    def _run_buy(self, product_id, product_qty, product_uom, location_id, name, origin, values):
        #定义一个字典,用来存放domain与对应的po映射.
        cache = {}
        ...
        ...
        # 使用内部方法获取domain。(domain是一个数据字段,存放着'partner_id','state','company_id'等字段的键值对)
        domain = self._make_po_get_domain(values, partner)
    
       # 如果cache中存在domain,则直接获取映射.
        if domain in cache:
            po = cache[domain]
        # 如果cache中不存在domain,则在`purchase.order`数据中搜索符合domain条件的po.
        else:
            po = self.env['purchase.order'].search([dom for dom in domain])
            po = po[0] if po else False
            cache[domain] = po
        # 搜索不到po则直接创建一张po询价单
        if not po:
            vals = self._prepare_purchase_order(product_id, product_qty, product_uom, origin, values, partner)
            po = self.env['purchase.order'].create(vals)
            cache[domain] = po
        # 获取关联的SOxxxx订单编号更新为origin字段的值,如果是多个则显示为SOxxxx,SOxxxx。
        elif not po.origin or origin not in po.origin.split(', '):
            if po.origin:
                if origin:
                    po.write({'origin': po.origin + ', ' + origin})
                else:
                    po.write({'origin': po.origin})
            else:
                po.write({'origin': origin})
    
        # 创建采购商品目录
        # Create Line
        po_line = False
    
        #对采购产品进行遍历,如果`select_seller`函数得到了满足条件的seller,则更新价格。
        #注意到每次遍历`po.order_line`,都会调用了产品的`select_seller()`方法。
        #其中的quantity参数使用的是累计的`product_qty`.即每次都会统计产品的总需求量.然后在进行供应商的条件判断.
        for line in po.order_line:
            if line.product_id == product_id and line.product_uom == product_id.uom_po_id:
                if line._merge_in_existing_line(product_id, product_qty, product_uom, location_id, name, origin, values):
                    procurement_uom_po_qty = product_uom._compute_quantity(product_qty, product_id.uom_po_id)
                    seller = product_id._select_seller(
                        partner_id=partner,
                        quantity=line.product_qty + procurement_uom_po_qty,
                        date=po.date_order and po.date_order[:10],
                        uom_id=product_id.uom_po_id)
    
    
                    price_unit = self.env['account.tax']._fix_tax_included_price_company(seller.price, line.product_id.supplier_taxes_id, line.taxes_id, values['company_id']) if seller else 0.0
                    if price_unit and seller and po.currency_id and seller.currency_id != po.currency_id:
                        price_unit = seller.currency_id.compute(price_unit, po.currency_id)
                      # 重新更新价格,需求量
                    po_line = line.write({
                        'product_qty': line.product_qty + procurement_uom_po_qty,
                        'price_unit': price_unit,
                        'move_dest_ids': [(4, x.id) for x in values.get('move_dest_ids', [])]
                    })
                    break
        # 如果没有po_line,则创建一个新的`order_line`.
        if not po_line:
            vals = self._prepare_purchase_order_line(product_id, product_qty, product_uom, values, po, supplier)
            self.env['purchase.order.line'].create(vals)
    
    
    @api.multi
    def _prepare_purchase_order_line(self, product_id, product_qty, product_uom, values, po, supplier):
        # 对多个销售订单中的需求数量进行统计
        procurement_uom_po_qty = product_uom._compute_quantity(product_qty, product_id.uom_po_id)
        # 使用product的select_seller()方法对供应商是否满足条件进行判断,若查找不到,seller为空
        seller = product_id._select_seller(
            partner_id=supplier.name,
            quantity=procurement_uom_po_qty,
            date=po.date_order and po.date_order[:10],
            uom_id=product_id.uom_po_id)
    
        ...
        ...
         # 若seller不存在则为0.0。
        price_unit = self.env['account.tax']._fix_tax_included_price_company(seller.price, product_id.supplier_taxes_id, taxes_id, values['company_id']) if seller else 0.0
        ...
        ...
        return {
            'name': name,
            'product_qty': procurement_uom_po_qty,
            'product_id': product_id.id,
            'product_uom': product_id.uom_po_id.id,
            'price_unit': price_unit,
            'date_planned': date_planned,
            'orderpoint_id': values.get('orderpoint_id', False) and values.get('orderpoint_id').id,
            'taxes_id': [(6, 0, taxes_id.ids)],
            'order_id': po.id,
            'move_dest_ids': [(4, x.id) for x in values.get('move_dest_ids', [])],
        }
    
    

生成po的逻辑:

  1. 首先生成一张对应于供应商的采购询价单,若为同一供应商,则合并在一张采购询价单中

    • 看到purchase.py中首先使用cache字典用来保存domain.而domain是对同一供应商(partner_id)。可以这么理解,这里创建了对应一个供应商的采购询价单,然后把它保存起来.
  2. 每次会对询价单中的采购商品order_line字段遍历,使用select_seller函数对其供货时间,供货数量进行判断,若满足条件,查找到seller,则更新价格.

    • 观察名为_prepare_purchase_order_line的函数。查看它的return值(一个数据字典),可以判断出它是为构造采购产品(purchase.order_line)提供初始化数据的。
  3. 在odoo11中,当销售订单中销售产品的数量低于供应商的最低采购数时,生成的采购订单中,价格一项始终为0.0

验证结果

看了源码,为了验证我们的猜想,那就再创建一张新的销售订单.这次还是销售5件男士衬衫,让2张SO的合计数量达到【海澜】的最小采购数10。

image.png

观察新的采购订单,发现合并2张SO后价格显示已经正常。

再来验证下合并order_line的功能。举个例子,我们的A公司又有一项新业务,出口【海澜】制造的【男士西装】。那正常的情况下我们销售【男士西装】后由于供应商相同,会把新的销售订单合并到一张采购报价单上。来试试看。

  • 创建【男士西装】产品 image.png
  • 销售6件该产品,注意到销售订单为SO027
  • 观察原来的采购询价单PO00009,已经合并SO027 image.png