From 60723d10473eff727b60958c3eb168956a668a1a Mon Sep 17 00:00:00 2001
From: Nicolas Martinelli <nim@odoo.com>
Date: Thu, 27 Aug 2015 12:27:33 +0200
Subject: [PATCH] [ADD] sale_expense: adaptation due to the new Sale module

Links expenses and sale modules.

Reason: complete rewrite of the Sale module.

Responsible: fp, dbo, nim
---
 addons/sale_expense/__init__.py               |  3 +
 addons/sale_expense/__openerp__.py            | 27 ++++++
 addons/sale_expense/models/__init__.py        |  4 +
 addons/sale_expense/models/analytic.py        | 19 ++++
 addons/sale_expense/models/product.py         | 15 ++++
 addons/sale_expense/sale_expense_demo.xml     | 21 +++++
 addons/sale_expense/tests/__init__.py         |  1 +
 .../sale_expense/tests/test_sale_expense.py   | 86 +++++++++++++++++++
 addons/sale_expense/views/product_view.xml    | 15 ++++
 9 files changed, 191 insertions(+)
 create mode 100644 addons/sale_expense/__init__.py
 create mode 100644 addons/sale_expense/__openerp__.py
 create mode 100644 addons/sale_expense/models/__init__.py
 create mode 100644 addons/sale_expense/models/analytic.py
 create mode 100644 addons/sale_expense/models/product.py
 create mode 100644 addons/sale_expense/sale_expense_demo.xml
 create mode 100644 addons/sale_expense/tests/__init__.py
 create mode 100644 addons/sale_expense/tests/test_sale_expense.py
 create mode 100644 addons/sale_expense/views/product_view.xml

diff --git a/addons/sale_expense/__init__.py b/addons/sale_expense/__init__.py
new file mode 100644
index 000000000000..48f417bbd932
--- /dev/null
+++ b/addons/sale_expense/__init__.py
@@ -0,0 +1,3 @@
+# -*- coding: utf-8 -*-
+# Part of Odoo. See LICENSE file for full copyright and licensing details.
+import models
diff --git a/addons/sale_expense/__openerp__.py b/addons/sale_expense/__openerp__.py
new file mode 100644
index 000000000000..6907791fab31
--- /dev/null
+++ b/addons/sale_expense/__openerp__.py
@@ -0,0 +1,27 @@
+# -*- coding: utf-8 -*-
+# Part of Odoo. See LICENSE file for full copyright and licensing details.
+
+{
+    'name': 'Sales Expense',
+    'version': '1.0',
+    'category': 'Hidden',
+    'summary': 'Quotation, Sale Orders, Delivery & Invoicing Control',
+    'description': """
+Module used for demo data
+=========================
+
+Create some products for which you can re-invoice the costs.
+This module does not add any feature, despite a few demo data to
+test the features easily.
+""",
+    'author': 'OpenERP SA',
+    'website': 'https://www.odoo.com/page/warehouse',
+    'depends': ['sale', 'hr_expense'],
+    'data': [
+        'views/product_view.xml',
+    ],
+    'demo': ['sale_expense_demo.xml'],
+    'test': [],
+    'installable': True,
+    'auto_install': True,
+}
diff --git a/addons/sale_expense/models/__init__.py b/addons/sale_expense/models/__init__.py
new file mode 100644
index 000000000000..2bf570fe2beb
--- /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.
+import analytic
+import product
diff --git a/addons/sale_expense/models/analytic.py b/addons/sale_expense/models/analytic.py
new file mode 100644
index 000000000000..3baeeaaa4f54
--- /dev/null
+++ b/addons/sale_expense/models/analytic.py
@@ -0,0 +1,19 @@
+# -*- coding: utf-8 -*-
+# Part of Odoo. See LICENSE file for full copyright and licensing details.
+
+from openerp import models
+
+
+class AccountAnalyticLine(models.Model):
+    _inherit = "account.analytic.line"
+
+    def _get_invoice_price(self, order):
+        if self.product_id.invoice_policy == 'cost' and self.product_id.expense_policy == 'sales_price':
+            return self.product_id.with_context(
+                partner=order.partner_id.id,
+                date_order=order.date_order,
+                pricelist=order.pricelist_id.id,
+                uom=self.product_uom_id.id
+            ).price
+        else:
+            return super(AccountAnalyticLine, self)._get_invoice_price(order)
diff --git a/addons/sale_expense/models/product.py b/addons/sale_expense/models/product.py
new file mode 100644
index 000000000000..0d3276b453f7
--- /dev/null
+++ b/addons/sale_expense/models/product.py
@@ -0,0 +1,15 @@
+# -*- coding: utf-8 -*-
+# Part of Odoo. See LICENSE file for full copyright and licensing details.
+
+from openerp import fields, models
+
+
+class ProductProduct(models.Model):
+    _inherit = "product.template"
+
+    expense_policy = fields.Selection(
+        [('cost', 'At Cost'), ('sales_price', 'At Sales Price')],
+        string='Expense Invoice Policy',
+        help="If you invoice at cost, the expense will be invoiced on the sale order at the cost of the analytic line;"
+        "if you invoice at sales price, the price of the product will be used instead.",
+        default='cost')
diff --git a/addons/sale_expense/sale_expense_demo.xml b/addons/sale_expense/sale_expense_demo.xml
new file mode 100644
index 000000000000..ee398004c98a
--- /dev/null
+++ b/addons/sale_expense/sale_expense_demo.xml
@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="utf-8"?>
+<openerp>
+    <data noupdate="1">
+    
+        <record id="hr_expense.car_travel" model="product.product">
+            <field name="invoice_policy">cost</field>
+            <field name="expense_policy">sales_price</field>
+        </record>
+
+        <record id="hr_expense.air_ticket" model="product.product">
+            <field name="invoice_policy">cost</field>
+            <field name="expense_policy">cost</field>
+        </record>
+
+        <record id="hr_expense.hotel_rent" model="product.product">
+            <field name="invoice_policy">cost</field>
+            <field name="expense_policy">cost</field>
+        </record>
+
+    </data>
+</openerp>
diff --git a/addons/sale_expense/tests/__init__.py b/addons/sale_expense/tests/__init__.py
new file mode 100644
index 000000000000..2ceb1c00c655
--- /dev/null
+++ b/addons/sale_expense/tests/__init__.py
@@ -0,0 +1 @@
+import test_sale_expense
diff --git a/addons/sale_expense/tests/test_sale_expense.py b/addons/sale_expense/tests/test_sale_expense.py
new file mode 100644
index 000000000000..837d2612a537
--- /dev/null
+++ b/addons/sale_expense/tests/test_sale_expense.py
@@ -0,0 +1,86 @@
+# -*- coding: utf-8 -*-
+# Part of Odoo. See LICENSE file for full copyright and licensing details.
+from openerp.addons.sale.tests.test_sale_common import TestSale
+from openerp import workflow
+
+
+class TestSaleExpense(TestSale):
+    def test_sale_expense(self):
+        """ Test the behaviour of sales orders when managing expenses """
+        # create a so with a product invoiced on delivery
+        prod = self.env.ref('product.product_product_1')
+        so = self.env['sale.order'].create({
+            'partner_id': self.partner.id,
+            'partner_invoice_id': self.partner.id,
+            'partner_shipping_id': self.partner.id,
+            'order_line': [(0, 0, {'name': prod.name, 'product_id': prod.id, 'product_uom_qty': 2, 'product_uom': prod.uom_id.id, 'price_unit': prod.list_price})],
+            'pricelist_id': self.env.ref('product.list0').id,
+        })
+        so.action_confirm()
+        so._create_analytic_account()  # normally created at so confirmation when you use the right products
+        init_price = so.amount_total
+
+        # create some expense and validate it (expense at cost)
+        prod_exp = self.env.ref('hr_expense.air_ticket')
+        company = self.env.ref('base.main_company')
+        journal = self.env['account.journal'].create({'name': 'Purchase Journal - Test', 'code': 'HRTPJ', 'type': 'purchase', 'company_id': company.id})
+        account_payable = self.env['account.account'].create({'code': 'X1111', 'name': 'HR Expense - Test Payable Account', 'user_type_id': self.env.ref('account.data_account_type_payable').id, 'reconcile': True})
+        exp = self.env['hr.expense.expense'].create({
+            'name': 'Test Expense',
+            'user_id': self.user.id,
+            'line_ids': [(0, 0, {'product_id': prod_exp.id,
+                                 'name': 'Air Travel',
+                                 'analytic_account': so.project_id.id,
+                                 'unit_amount': 700.0,
+                                 })],
+            'journal_id': journal.id,
+            'employee_payable_account_id': account_payable.id,
+        })
+        cr, uid = self.cr, self.uid
+        # Submit to Manager
+        workflow.trg_validate(uid, 'hr.expense.expense', exp.id, 'confirm', cr)
+        # Approve
+        workflow.trg_validate(uid, 'hr.expense.expense', exp.id, 'validate', cr)
+        # Create Expense Entries
+        workflow.trg_validate(uid, 'hr.expense.expense', exp.id, 'done', cr)
+
+        # expense should now be in sales order
+        self.assertTrue(prod_exp in map(lambda so: so.product_id, so.order_line), 'Sale Expense: expense product should be in so')
+        sol = so.order_line.filtered(lambda sol: sol.product_id.id == prod_exp.id)
+        self.assertEqual((sol.price_unit, sol.qty_delivered), (700.0, 1.0), 'Sale Expense: error when invoicing an expense at cost')
+        self.assertEqual(so.amount_total, init_price, 'Sale Expense: price of so not updated after adding expense')
+
+        # create some expense and validate it (expense at sales price)
+        init_price = so.amount_total
+        prod_exp = self.env.ref('hr_expense.car_travel')
+        exp = self.env['hr.expense.expense'].create({
+            'name': 'Test Expense',
+            'user_id': self.user.id,
+            'line_ids': [(0, 0, {'product_id': prod_exp.id,
+                                 'name': 'Car Travel',
+                                 'analytic_account': so.project_id.id,
+                                 'uom_id': self.env.ref('product.product_uom_km').id,
+                                 'unit_amount': 0.15,
+                                 'unit_quantity': 100,
+                                 })],
+            'journal_id': journal.id,
+            'employee_payable_account_id': account_payable.id,
+        })
+        # Submit to Manager
+        workflow.trg_validate(uid, 'hr.expense.expense', exp.id, 'confirm', cr)
+        # Approve
+        workflow.trg_validate(uid, 'hr.expense.expense', exp.id, 'validate', cr)
+        # Create Expense Entries
+        workflow.trg_validate(uid, 'hr.expense.expense', exp.id, 'done', cr)
+
+        # expense should now be in sales order
+        self.assertTrue(prod_exp in map(lambda so: so.product_id, so.order_line), 'Sale Expense: expense product should be in so')
+        sol = so.order_line.filtered(lambda sol: sol.product_id.id == prod_exp.id)
+        self.assertEqual((sol.price_unit, sol.qty_delivered), (0.32, 100.0), 'Sale Expense: error when invoicing an expense at cost')
+        self.assertEqual(so.amount_total, init_price, 'Sale Expense: price of so not updated after adding expense')
+        # self.assertTrue(so.invoice_status, 'no', 'Sale Expense: expenses should not impact the invoice_status of the so')
+
+        # both expenses should be invoiced
+        inv_id = so.action_invoice_create()
+        inv = self.env['account.invoice'].browse(inv_id)
+        self.assertEqual(inv.amount_total, 732.0, 'Sale Expense: invoicing of exepense is wrong')
diff --git a/addons/sale_expense/views/product_view.xml b/addons/sale_expense/views/product_view.xml
new file mode 100644
index 000000000000..5f2dbd81c0f5
--- /dev/null
+++ b/addons/sale_expense/views/product_view.xml
@@ -0,0 +1,15 @@
+<?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': [('invoice_policy','!=','cost')]}"/>
+            </field>
+        </field>
+    </record>
+</odoo>
+    
\ No newline at end of file
-- 
GitLab