在上一篇的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中创建。
我们的产品是男士衬衫,产品类型是可库存产品.
库存选项中,既然是贸易商品,那勾选【按订单生成】(make to order)
采购中添加我们的供货商 【海澜】,海澜公司要求最少的采购数为10件。
好了, 现在我们的衬衫已经可以开卖了。
进入销售模块,创建销售订单。
我们来销售给中国出口商。假设第一个订单量比较小。只需要5件。注意到此时销售订单编号SO025 点击【确认销售】后创建这个订单。
然后来到采购模块中。我们会发现有一张采购询价单自动关联了SO025。可是,奇怪的是,为什么金额是0呢?
这里就要带入到我们一开始说的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的逻辑:
首先生成一张对应于供应商的采购询价单,若为同一供应商,则合并在一张采购询价单中
- 看到
purchase.py
中首先使用cache
字典用来保存domain
.而domain
是对同一供应商(partner_id
)。可以这么理解,这里创建了对应一个供应商的采购询价单,然后把它保存起来.
- 看到
每次会对询价单中的采购商品
order_line
字段遍历,使用select_seller
函数对其供货时间,供货数量进行判断,若满足条件,查找到seller,则更新价格.- 观察名为
_prepare_purchase_order_line
的函数。查看它的return值(一个数据字典),可以判断出它是为构造采购产品(purchase.order_line)提供初始化数据的。
- 观察名为
在odoo11中,当销售订单中销售产品的数量低于供应商的最低采购数时,生成的采购订单中,价格一项始终为0.0
验证结果
看了源码,为了验证我们的猜想,那就再创建一张新的销售订单.这次还是销售5件男士衬衫,让2张SO的合计数量达到【海澜】的最小采购数10。
观察新的采购订单,发现合并2张SO后价格显示已经正常。
再来验证下合并order_line
的功能。举个例子,我们的A公司又有一项新业务,出口【海澜】制造的【男士西装】。那正常的情况下我们销售【男士西装】后由于供应商相同,会把新的销售订单合并到一张采购报价单上。来试试看。
- 创建【男士西装】产品
- 销售6件该产品,注意到销售订单为SO027
- 观察原来的采购询价单PO00009,已经合并SO027