[IMP] sale,hr_expense: simplify invoice policy, 2 options instead of 3
The invoice_policy 'time & material' has been replaced by an
expense_policy!='no', with invoice_policy='delivery'.
This commit is contained in:
@@ -7,4 +7,5 @@ from openerp import fields, models
|
|||||||
class ProductTemplate(models.Model):
|
class ProductTemplate(models.Model):
|
||||||
_inherit = "product.template"
|
_inherit = "product.template"
|
||||||
|
|
||||||
can_be_expensed = fields.Boolean(help="Specify whether the product can be selected in an HR expense.")
|
can_be_expensed = fields.Boolean(help="Specify whether the product can be selected in an HR expense.", string="Can be Expensed")
|
||||||
|
|
||||||
|
|||||||
@@ -71,8 +71,7 @@ class sale_configuration(osv.TransientModel):
|
|||||||
'Advanced pricing based on formula: You can have all the rights on pricelist'),
|
'Advanced pricing based on formula: You can have all the rights on pricelist'),
|
||||||
'default_invoice_policy': fields.selection([
|
'default_invoice_policy': fields.selection([
|
||||||
('order', 'Invoice ordered quantities'),
|
('order', 'Invoice ordered quantities'),
|
||||||
('delivery', 'Invoice delivered quantities'),
|
('delivery', 'Invoice delivered quantities')
|
||||||
('cost', 'Invoice based on costs (time and material, expenses)')
|
|
||||||
], 'Default Invoicing', default_model='product.template'),
|
], 'Default Invoicing', default_model='product.template'),
|
||||||
'deposit_product_id_setting': fields.many2one('product.product', 'Deposit Product',\
|
'deposit_product_id_setting': fields.many2one('product.product', 'Deposit Product',\
|
||||||
domain="[('type', '=', 'service')]",\
|
domain="[('type', '=', 'service')]",\
|
||||||
|
|||||||
+8
-17
@@ -449,11 +449,6 @@ class SaleOrder(models.Model):
|
|||||||
if self.env.context.get('send_email'):
|
if self.env.context.get('send_email'):
|
||||||
self.force_quotation_send()
|
self.force_quotation_send()
|
||||||
order.order_line._action_procurement_create()
|
order.order_line._action_procurement_create()
|
||||||
if not order.project_id:
|
|
||||||
for line in order.order_line:
|
|
||||||
if line.product_id.invoice_policy == 'cost':
|
|
||||||
order._create_analytic_account()
|
|
||||||
break
|
|
||||||
if self.env['ir.values'].get_default('sale.config.settings', 'auto_done_setting'):
|
if self.env['ir.values'].get_default('sale.config.settings', 'auto_done_setting'):
|
||||||
self.action_done()
|
self.action_done()
|
||||||
return True
|
return True
|
||||||
@@ -572,7 +567,7 @@ class SaleOrderLine(models.Model):
|
|||||||
@api.depends('product_id.invoice_policy', 'order_id.state')
|
@api.depends('product_id.invoice_policy', 'order_id.state')
|
||||||
def _compute_qty_delivered_updateable(self):
|
def _compute_qty_delivered_updateable(self):
|
||||||
for line in self:
|
for line in self:
|
||||||
line.qty_delivered_updateable = line.product_id.invoice_policy in ('order', 'delivery') and line.order_id.state == 'sale' and line.product_id.track_service == 'manual'
|
line.qty_delivered_updateable = line.order_id.state == 'sale' and line.product_id.track_service == 'manual'
|
||||||
|
|
||||||
@api.depends('qty_invoiced', 'qty_delivered', 'product_uom_qty', 'order_id.state')
|
@api.depends('qty_invoiced', 'qty_delivered', 'product_uom_qty', 'order_id.state')
|
||||||
def _get_to_invoice_qty(self):
|
def _get_to_invoice_qty(self):
|
||||||
@@ -671,10 +666,6 @@ class SaleOrderLine(models.Model):
|
|||||||
new_procs.run()
|
new_procs.run()
|
||||||
return new_procs
|
return new_procs
|
||||||
|
|
||||||
@api.model
|
|
||||||
def _get_analytic_invoice_policy(self):
|
|
||||||
return ['cost']
|
|
||||||
|
|
||||||
@api.model
|
@api.model
|
||||||
def _get_analytic_track_service(self):
|
def _get_analytic_track_service(self):
|
||||||
return []
|
return []
|
||||||
@@ -691,8 +682,7 @@ class SaleOrderLine(models.Model):
|
|||||||
line = super(SaleOrderLine, self).create(values)
|
line = super(SaleOrderLine, self).create(values)
|
||||||
if line.state == 'sale':
|
if line.state == 'sale':
|
||||||
if (not line.order_id.project_id and
|
if (not line.order_id.project_id and
|
||||||
(line.product_id.track_service in self._get_analytic_track_service() or
|
(line.product_id.track_service in self._get_analytic_track_service())):
|
||||||
line.product_id.invoice_policy in self._get_analytic_invoice_policy())):
|
|
||||||
line.order_id._create_analytic_account()
|
line.order_id._create_analytic_account()
|
||||||
line._action_procurement_create()
|
line._action_procurement_create()
|
||||||
|
|
||||||
@@ -1004,7 +994,10 @@ class ProductTemplate(models.Model):
|
|||||||
default='manual')
|
default='manual')
|
||||||
sale_line_warn = fields.Selection(WARNING_MESSAGE, 'Sales Order Line', help=WARNING_HELP, required=True, default="no-message")
|
sale_line_warn = fields.Selection(WARNING_MESSAGE, 'Sales Order Line', help=WARNING_HELP, required=True, default="no-message")
|
||||||
sale_line_warn_msg = fields.Text('Message for Sales Order Line')
|
sale_line_warn_msg = fields.Text('Message for Sales Order Line')
|
||||||
|
expense_policy = fields.Selection(
|
||||||
|
[('no', 'No'), ('cost', 'At cost'), ('sales_price', 'At sale price')],
|
||||||
|
string='Re-Invoice Expenses',
|
||||||
|
default='no')
|
||||||
@api.multi
|
@api.multi
|
||||||
@api.depends('product_variant_ids.sales_count')
|
@api.depends('product_variant_ids.sales_count')
|
||||||
def _sales_count(self):
|
def _sales_count(self):
|
||||||
@@ -1033,8 +1026,6 @@ class ProductTemplate(models.Model):
|
|||||||
invoice_policy = fields.Selection(
|
invoice_policy = fields.Selection(
|
||||||
[('order', 'Ordered quantities'),
|
[('order', 'Ordered quantities'),
|
||||||
('delivery', 'Delivered quantities'),
|
('delivery', 'Delivered quantities'),
|
||||||
('cost', 'Reinvoice Costs')],
|
], string='Invoicing Policy', help='Ordered Quantity: Invoice based on the quantity the customer ordered.\n'
|
||||||
string='Invoicing Policy', help='Ordered Quantity: Invoice based on the quantity the customer ordered.\n'
|
'Delivered Quantity: Invoiced based on the quantity the vendor delivered (time or deliveries).',
|
||||||
'Delivered Quantity: Invoiced based on the quantity the vendor delivered.\n'
|
|
||||||
'Reinvoice Costs: Invoice with some additional charges (product transfer, labour charges,...)',
|
|
||||||
default='order')
|
default='order')
|
||||||
|
|||||||
@@ -40,6 +40,13 @@ class AccountAnalyticLine(models.Model):
|
|||||||
so_line = fields.Many2one('sale.order.line', string='Sale Order Line')
|
so_line = fields.Many2one('sale.order.line', string='Sale Order Line')
|
||||||
|
|
||||||
def _get_invoice_price(self, order):
|
def _get_invoice_price(self, order):
|
||||||
|
if 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
|
||||||
if self.unit_amount == 0.0:
|
if self.unit_amount == 0.0:
|
||||||
return 0.0
|
return 0.0
|
||||||
price_unit = abs(self.amount / self.unit_amount)
|
price_unit = abs(self.amount / self.unit_amount)
|
||||||
@@ -47,20 +54,12 @@ class AccountAnalyticLine(models.Model):
|
|||||||
price_unit = self.currency_id.compute(price_unit, order.currency_id)
|
price_unit = self.currency_id.compute(price_unit, order.currency_id)
|
||||||
return price_unit
|
return price_unit
|
||||||
|
|
||||||
def _get_sale_order_line_vals(self):
|
def _get_sale_order_line_vals(self, order, price):
|
||||||
order = self.env['sale.order'].search([('project_id', '=', self.account_id.id)], limit=1)
|
|
||||||
if not order:
|
|
||||||
return False
|
|
||||||
if order.state != 'sale':
|
|
||||||
raise UserError(_('The Sale Order %s linked to the Analytic Account must be validated before registering expenses.') % order.name)
|
|
||||||
|
|
||||||
last_so_line = self.env['sale.order.line'].search([('order_id', '=', order.id)], order='sequence desc', limit=1)
|
last_so_line = self.env['sale.order.line'].search([('order_id', '=', order.id)], order='sequence desc', limit=1)
|
||||||
last_sequence = last_so_line.sequence + 1 if last_so_line else 100
|
last_sequence = last_so_line.sequence + 1 if last_so_line else 100
|
||||||
|
|
||||||
fpos = order.fiscal_position_id or order.partner_id.property_account_position_id
|
fpos = order.fiscal_position_id or order.partner_id.property_account_position_id
|
||||||
taxes = fpos.map_tax(self.product_id.taxes_id)
|
taxes = fpos.map_tax(self.product_id.taxes_id)
|
||||||
price = self._get_invoice_price(order)
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
'order_id': order.id,
|
'order_id': order.id,
|
||||||
'name': self.name,
|
'name': self.name,
|
||||||
@@ -77,30 +76,24 @@ class AccountAnalyticLine(models.Model):
|
|||||||
def _get_sale_order_line(self, vals=None):
|
def _get_sale_order_line(self, vals=None):
|
||||||
result = dict(vals or {})
|
result = dict(vals or {})
|
||||||
so_line = result.get('so_line', False) or self.so_line
|
so_line = result.get('so_line', False) or self.so_line
|
||||||
if not so_line and self.account_id and self.product_id and self.product_id.invoice_policy in ('cost', 'order'):
|
if not so_line and self.account_id and self.product_id and (self.product_id.expense_policy!='no'):
|
||||||
|
order = self.env['sale.order'].search([('project_id', '=', self.account_id.id), ('state','=','sale')], limit=1)
|
||||||
|
if not order:
|
||||||
|
return result
|
||||||
|
price = self._get_invoice_price(order)
|
||||||
so_lines = self.env['sale.order.line'].search([
|
so_lines = self.env['sale.order.line'].search([
|
||||||
('order_id.project_id', '=', self.account_id.id),
|
('order_id', '=', order.id),
|
||||||
('state', '=', 'sale'),
|
('price_unit', '=', price),
|
||||||
('product_id', '=', self.product_id.id)])
|
('product_id', '=', self.product_id.id)])
|
||||||
# Use the existing SO line only if the unit prices are the same, otherwise we create
|
|
||||||
# a new line
|
|
||||||
for line in so_lines:
|
|
||||||
if line.product_id.invoice_policy != 'cost' or (line.product_id.invoice_policy == 'cost' and line.price_unit == self._get_invoice_price(line.order_id)):
|
|
||||||
result.update({'so_line': line.id})
|
|
||||||
so_line = line
|
|
||||||
break
|
|
||||||
|
|
||||||
|
if so_lines:
|
||||||
|
result.update({'so_line': so_lines[0].id})
|
||||||
else:
|
else:
|
||||||
# This will trigger the creation of a new SO line
|
order_line_vals = self._get_sale_order_line_vals(order, price)
|
||||||
so_line = False
|
if order_line_vals:
|
||||||
|
so_line = self.env['sale.order.line'].create(order_line_vals)
|
||||||
if not so_line and self.account_id and self.product_id and self.product_id.invoice_policy == 'cost':
|
so_line._compute_tax_id()
|
||||||
order_line_vals = self._get_sale_order_line_vals()
|
result.update({'so_line': so_line.id})
|
||||||
if order_line_vals:
|
|
||||||
so_line = self.env['sale.order.line'].create(order_line_vals)
|
|
||||||
so_line._compute_tax_id()
|
|
||||||
result.update({'so_line': so_line.id})
|
|
||||||
|
|
||||||
return result
|
return result
|
||||||
|
|
||||||
@api.multi
|
@api.multi
|
||||||
|
|||||||
@@ -146,7 +146,7 @@
|
|||||||
</record>
|
</record>
|
||||||
|
|
||||||
<record id="product.service_cost_01" model="product.product">
|
<record id="product.service_cost_01" model="product.product">
|
||||||
<field name="invoice_policy">cost</field>
|
<field name="expense_policy">cost</field>
|
||||||
</record>
|
</record>
|
||||||
|
|
||||||
<record id="product.service_order_01" model="product.product">
|
<record id="product.service_order_01" model="product.product">
|
||||||
|
|||||||
@@ -40,7 +40,7 @@ class TestSaleOrder(TestSale):
|
|||||||
|
|
||||||
# deliver lines except 'time and material' then invoice again
|
# deliver lines except 'time and material' then invoice again
|
||||||
for line in so.order_line:
|
for line in so.order_line:
|
||||||
line.qty_delivered = 2 if line.product_id.invoice_policy in ['order', 'delivery'] else 0
|
line.qty_delivered = 2 if line.product_id.expense_policy=='no' else 0
|
||||||
self.assertTrue(so.invoice_status == 'to invoice', 'Sale: SO status after delivery should be "to invoice"')
|
self.assertTrue(so.invoice_status == 'to invoice', 'Sale: SO status after delivery should be "to invoice"')
|
||||||
inv_id = so.action_invoice_create()
|
inv_id = so.action_invoice_create()
|
||||||
inv = inv_obj.browse(inv_id)
|
inv = inv_obj.browse(inv_id)
|
||||||
|
|||||||
@@ -1,3 +1,2 @@
|
|||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||||
import models
|
|
||||||
|
|||||||
@@ -1,4 +0,0 @@
|
|||||||
# -*- coding: utf-8 -*-
|
|
||||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
|
||||||
import analytic
|
|
||||||
import product
|
|
||||||
@@ -1,19 +0,0 @@
|
|||||||
# -*- 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)
|
|
||||||
@@ -1,15 +0,0 @@
|
|||||||
# -*- 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')
|
|
||||||
@@ -1,19 +1,19 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<openerp>
|
<openerp>
|
||||||
<data noupdate="1">
|
<data noupdate="1">
|
||||||
|
|
||||||
<record id="hr_expense.car_travel" model="product.product">
|
<record id="hr_expense.car_travel" model="product.product">
|
||||||
<field name="invoice_policy">cost</field>
|
<field name="invoice_policy">delivery</field>
|
||||||
<field name="expense_policy">sales_price</field>
|
<field name="expense_policy">sales_price</field>
|
||||||
</record>
|
</record>
|
||||||
|
|
||||||
<record id="hr_expense.air_ticket" model="product.product">
|
<record id="hr_expense.air_ticket" model="product.product">
|
||||||
<field name="invoice_policy">cost</field>
|
<field name="invoice_policy">delivery</field>
|
||||||
<field name="expense_policy">cost</field>
|
<field name="expense_policy">cost</field>
|
||||||
</record>
|
</record>
|
||||||
|
|
||||||
<record id="hr_expense.hotel_rent" model="product.product">
|
<record id="hr_expense.hotel_rent" model="product.product">
|
||||||
<field name="invoice_policy">cost</field>
|
<field name="invoice_policy">delivery</field>
|
||||||
<field name="expense_policy">cost</field>
|
<field name="expense_policy">cost</field>
|
||||||
</record>
|
</record>
|
||||||
|
|
||||||
|
|||||||
@@ -7,9 +7,8 @@
|
|||||||
<field name="arch" type="xml">
|
<field name="arch" type="xml">
|
||||||
<field name="invoice_policy" position="after">
|
<field name="invoice_policy" position="after">
|
||||||
<field name="expense_policy" widget="radio"
|
<field name="expense_policy" widget="radio"
|
||||||
attrs="{'invisible': [('invoice_policy','!=','cost')]}"/>
|
attrs="{'invisible': [('can_be_expensed','=',False)]}"/>
|
||||||
</field>
|
</field>
|
||||||
</field>
|
</field>
|
||||||
</record>
|
</record>
|
||||||
</odoo>
|
</odoo>
|
||||||
|
|
||||||
@@ -285,7 +285,7 @@ class StockMove(models.Model):
|
|||||||
# Update delivered quantities on sale order lines
|
# Update delivered quantities on sale order lines
|
||||||
todo = self.env['sale.order.line']
|
todo = self.env['sale.order.line']
|
||||||
for move in self:
|
for move in self:
|
||||||
if (move.procurement_id.sale_line_id) and (move.product_id.invoice_policy in ('order', 'delivery')):
|
if (move.procurement_id.sale_line_id) and (move.product_id.expense_policy=='no'):
|
||||||
todo |= move.procurement_id.sale_line_id
|
todo |= move.procurement_id.sale_line_id
|
||||||
for line in todo:
|
for line in todo:
|
||||||
line.qty_delivered = line._get_delivered_qty()
|
line.qty_delivered = line._get_delivered_qty()
|
||||||
|
|||||||
@@ -33,7 +33,7 @@ class ProductTemplate(models.Model):
|
|||||||
|
|
||||||
@api.onchange('type', 'invoice_policy')
|
@api.onchange('type', 'invoice_policy')
|
||||||
def onchange_type_timesheet(self):
|
def onchange_type_timesheet(self):
|
||||||
if self.type == 'service' and self.invoice_policy != 'cost':
|
if self.type == 'service':
|
||||||
self.track_service = 'timesheet'
|
self.track_service = 'timesheet'
|
||||||
else:
|
else:
|
||||||
self.track_service = 'manual'
|
self.track_service = 'manual'
|
||||||
|
|||||||
Reference in New Issue
Block a user