Odoo Commit 每日一读/2 - 08ab28

今天我们来介绍下关于odoo10的提交:08ab28

改进结果:

这个提交是对销售费用中的expense_policy字段的值增加了onchange方法以便动态的更改该字段的值

# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.

from odoo import api, models


class ProductTemplate(models.Model):
    _inherit = "product.template"

    @api.onchange('can_be_expensed')
    def _onchange_can_be_expensed(self):
	if not self.can_be_expensed:
	    self.expense_policy = 'no'
  • 通过代码我们发现,这个提交在sale_expense模块中对project.template进行了继承,新增加了一个onchange方法.当产品的can_be_expense字段为False时,把expense_policy的值设置为no.(即这个选项)

为何要进行这个修改:

我们首先来看下销售模块中定义产品时可以选择的字段

image.png

  • 上图中,我们发现在安装sale_expense模块后创建产品时新增一个bool字段can_be_expensed
  • 进入sale_expense模块.我们在views文件夹中可以看到这个模块对product.template视图做的继承

    <?xml version="1.0" encoding="utf-8"?>
    <odoo>
    <record model="ir.ui.view" id="product_template_form_view_expense_policy">
        <field name="name">product.template.expense.policy</field>
        <field name="model">product.template</field>
        <field name="inherit_id" ref="sale.product_template_form_view_invoice_policy"/>
        <field name="arch" type="xml">
            <field name="invoice_policy" position="after">
                <field name="expense_policy" widget="radio"
                       attrs="{'invisible': [('can_be_expensed','=',False)]}"/>
            </field>
        </field>
    </record>
    </odoo>
  • 上面的代码相信大家很容易看懂,无非就是在我们把该字段设置为True时。就能在开票策略下出现一个Re-Invoice Expense的选择框。这三个选项也很好理解,

    • no :即表示不进行开票.
    • At cost :即使用成本价格进行开票.
    • At sale price :即使用销售价格进行开票.这就需要通过前一个xml文件来结合一起看 由于在xml中定义了’invisible’属性,我们把产品的can_be_expense属性取消时,Re_Invoice Expense这个选项会自动隐藏.但其中的expense_policy字段还保留以前选择的内容. image.png

bug重现

在实际生产过程中.操作者很容易忽视这个问题.

这就会在销售订单完成后的开票操作产生bug.我们来演示下这个bug的产生。

演示版本为中文版:

  • 创建名为iphone的产品。勾选可用于费用. 开票策略中选择已交货数量 . 重新开票费用设置为销售价格.保存 image.png
  • 编辑刚才创建的iphone产品。取消选择可用于费用.点击保存.这时候这个iphone产品的重新开票费用字段依然保存着销售价格这个选择.
  • 创建新的销售订单, 订单行中添加iphone这个产品。

    image.png

保存后点击把该订单状态设为确认销售。

该订单已经确认销售,按照平时的使用,当我们交货后即可进行开票操作。让我们来试下

image.png

点击交货按钮,并进行确认. 这里bug就产生了,我们无法进行对该产品销售交付后的开票操作

在销售订单的发票状态中该订单显示为没有要开票的

image.png

这是为什么呢?让我们通过代码来进行解释。 首先,打开sale模块中的sale.py文件。 我们来看下发票状态的生成策略。

class SaleOrderLine(models.Model):
    _name = 'sale.order.line'
    _description = 'Sales Order Line'
    _order = 'order_id, layout_category_id, sequence, id'

    @api.depends('state', 'product_uom_qty', 'qty_delivered', 'qty_to_invoice', 'qty_invoiced')
    def _compute_invoice_status(self):
        """
        Compute the invoice status of a SO line. Possible statuses:
        - no: if the SO is not in status 'sale' or 'done', we consider that there is nothing to
          invoice. This is also hte default value if the conditions of no other status is met.
        - to invoice: we refer to the quantity to invoice of the line. Refer to method
          `_get_to_invoice_qty()` for more information on how this quantity is calculated.
        - upselling: this is possible only for a product invoiced on ordered quantities for which
          we delivered more than expected. The could arise if, for example, a project took more
          time than expected but we decided not to invoice the extra cost to the client. This
          occurs onyl in state 'sale', so that when a SO is set to done, the upselling opportunity
          is removed from the list.
        - invoiced: the quantity invoiced is larger or equal to the quantity ordered.
        """
        precision = self.env['decimal.precision'].precision_get('Product Unit of Measure')
        for line in self:
            if line.state not in ('sale', 'done'):
                line.invoice_status = 'no'
		# 注意第一个elif判断
            elif not float_is_zero(line.qty_to_invoice, precision_digits=precision):
                line.invoice_status = 'to invoice'
            elif line.state == 'sale' and line.product_id.invoice_policy == 'order' and\
                    float_compare(line.qty_delivered, line.product_uom_qty, precision_digits=precision) == 1:
                line.invoice_status = 'upselling'
            elif float_compare(line.qty_invoiced, line.product_uom_qty, precision_digits=precision) >= 0:
                line.invoice_status = 'invoiced'
            else:
                line.invoice_status = 'no'

可以看到,发票状态首先通过对订单的状态进行判断,然后根据各个条件来决定开票的状态。

那为什么我们的expense_policy字段会影响开票状态呢。继续往里走,来到第一个elif判断,这里的判断条件是当使用odoo的工具函数float_is_zero对销售订单中产品的字段qty_to_invoice判断,不为零时,开票状态处于可开票

我们来看qty_to_invoice这个字段。它是一个计算字段

    @api.depends('qty_invoiced', 'qty_delivered', 'product_uom_qty', 'order_id.state')
    def _get_to_invoice_qty(self):
        """
        Compute the quantity to invoice. If the invoice policy is order, the quantity to invoice is
        calculated from the ordered quantity. Otherwise, the quantity delivered is used.
        """
        for line in self:
            if line.order_id.state in ['sale', 'done']:
                if line.product_id.invoice_policy == 'order':
                    line.qty_to_invoice = line.product_uom_qty - line.qty_invoiced
                else:
                    line.qty_to_invoice = line.qty_delivered - line.qty_invoiced
            else:
                line.qty_to_invoice = 0

通过这个计算方法我们可以发现,由于我们的iphone产品使用的是已交货数量的开票策略,我们就需要qty_delivered这个字段。 这个字段是在sale模块中定义的

qty_delivered = fields.Float(string='Delivered', copy=False, digits=dp.get_precision('Product Unit of Measure'), default=0.0)

但是它的取值是通过sale_stock模块扩展的.进入sale_stock模块models文件夹中的stock.py文件.在13行

image.png

我们看到qty_delivered字段的取值会在这里进行一次更新.

注意这里的代码.首先会对产品的expense_policy字段取值进行过滤。

到这里 相信我们已经可以得出结论了。因为我们的iphone产品的expense_policy的默认值依然存在,即使发了货,在销售订单中该产品却还是被过滤了。销售订单的字段qty_delivered始终是默认值0.0。所以销售订单的开票状态始终为没有要开票的

原始提交信息

commit 08ab28a66cf14707d86e1b3b045b8c1d5e7a3414
Author:     Nicolas Martinelli <[email protected]>
AuthorDate: Fri Nov 3 14:13:39 2017 +0100
Commit:     Nicolas Martinelli <[email protected]>
CommitDate: Mon Nov 6 13:05:37 2017 +0100

    [FIX] sale_expense: fields consistency

    Use case to reproduce:
    - Set a product to be expensed
    - Set the expense_policy to something else than no
    - Do a delivery order with a picking
    - Validate the picking
    -> Delivered quantity to 0 and impossible to create an invoice
    if the invoice_policy is delivered_quantity

    It happens due to this commit 48ea59d
    What does it do:
    - The move could be generated by an expense.
    - If the move has 'no' as expense policy thus we won't add it in the invoice
    Problem we can't guess if the move come from an expense or not (limitation).

    This commit add an onchange on can_be_expense is order to set the expense
    policy back to 'no' when the user uncheck it.

    Courtesy of amoyaux

    opw-777139
---
 addons/sale_expense/__init__.py                |  2 ++
 addons/sale_expense/models/__init__.py         |  4 ++++
 addons/sale_expense/models/product_template.py | 13 +++++++++++++
 3 files changed, 19 insertions(+)
diff --git a/addons/sale_expense/__init__.py b/addons/sale_expense/__init__.py
index 67dee8c..dc5e6b6 100644
--- a/addons/sale_expense/__init__.py
+++ b/addons/sale_expense/__init__.py
@@ -1,2 +1,4 @@
 # -*- coding: utf-8 -*-
 # Part of Odoo. See LICENSE file for full copyright and licensing details.
+
+from . import models
diff --git a/addons/sale_expense/models/__init__.py b/addons/sale_expense/models/__init__.py
new file mode 100644
index 0000000..3a8ca74
--- /dev/null
+++ b/addons/sale_expense/models/__init__.py
@@ -0,0 +1,4 @@
+# -*- coding: utf-8 -*-
+# Part of Odoo. See LICENSE file for full copyright and licensing details.
+
+from . import product_template
diff --git a/addons/sale_expense/models/product_template.py b/addons/sale_expense/models/product_template.py
new file mode 100644
index 0000000..5e6299f
--- /dev/null
+++ b/addons/sale_expense/models/product_template.py
@@ -0,0 +1,13 @@
+# -*- coding: utf-8 -*-
+# Part of Odoo. See LICENSE file for full copyright and licensing details.
+
+from odoo import api, models
+
+
+class ProductTemplate(models.Model):
+    _inherit = "product.template"
+
+    @api.onchange('can_be_expensed')
+    def _onchange_can_be_expensed(self):
+        if not self.can_be_expensed:
+            self.expense_policy = 'no'