2018-01-16 06:58:15 +01:00
# -*- coding: utf-8 -*-
2018-01-16 11:34:37 +01:00
# Part of Odoo, Flectra. See LICENSE file for full copyright and licensing details.
2018-01-16 06:58:15 +01:00
import uuid
from itertools import groupby
from datetime import datetime , timedelta
from werkzeug . urls import url_encode
2018-01-16 11:34:37 +01:00
from flectra import api , fields , models , _
from flectra . exceptions import UserError , AccessError
from flectra . osv import expression
from flectra . tools import float_is_zero , float_compare , DEFAULT_SERVER_DATETIME_FORMAT
2018-01-16 06:58:15 +01:00
2018-01-16 11:34:37 +01:00
from flectra . tools . misc import formatLang
2018-01-17 11:23:19 +01:00
from flectra . exceptions import ValidationError
2018-01-16 11:34:37 +01:00
from flectra . addons import decimal_precision as dp
2018-01-16 06:58:15 +01:00
class SaleOrder ( models . Model ) :
_name = " sale.order "
2018-01-17 11:23:19 +01:00
_inherit = [ ' mail.thread ' , ' mail.activity.mixin ' , ' portal.mixin ' ,
' ir.branch.company.mixin ' ]
2018-01-16 06:58:15 +01:00
_description = " Quotation "
_order = ' date_order desc, id desc '
@api.depends ( ' order_line.price_total ' )
def _amount_all ( self ) :
"""
Compute the total amounts of the SO .
"""
for order in self :
amount_untaxed = amount_tax = 0.0
for line in order . order_line :
amount_untaxed + = line . price_subtotal
amount_tax + = line . price_tax
order . update ( {
' amount_untaxed ' : order . pricelist_id . currency_id . round ( amount_untaxed ) ,
' amount_tax ' : order . pricelist_id . currency_id . round ( amount_tax ) ,
' amount_total ' : amount_untaxed + amount_tax ,
} )
@api.depends ( ' state ' , ' order_line.invoice_status ' )
def _get_invoiced ( self ) :
"""
Compute the invoice status of a SO . 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 : if any SO line is ' to invoice ' , the whole SO is ' to invoice '
- invoiced : if all SO lines are invoiced , the SO is invoiced .
- upselling : if all SO lines are invoiced or upselling , the status is upselling .
The invoice_ids are obtained thanks to the invoice lines of the SO lines , and we also search
for possible refunds created directly from existing invoices . This is necessary since such a
refund is not directly linked to the SO .
"""
for order in self :
invoice_ids = order . order_line . mapped ( ' invoice_lines ' ) . mapped ( ' invoice_id ' ) . filtered ( lambda r : r . type in [ ' out_invoice ' , ' out_refund ' ] )
# Search for invoices which have been 'cancelled' (filter_refund = 'modify' in
# 'account.invoice.refund')
# use like as origin may contains multiple references (e.g. 'SO01, SO02')
2018-07-13 11:26:56 +02:00
refunds = invoice_ids . search ( [ ( ' origin ' , ' like ' , order . name ) , ( ' company_id ' , ' = ' , order . company_id . id ) , ( ' branch_id ' , ' = ' , order . branch_id . id ) ] ) . filtered ( lambda r : r . type in [ ' out_invoice ' , ' out_refund ' ] )
2018-01-16 06:58:15 +01:00
invoice_ids | = refunds . filtered ( lambda r : order . name in [ origin . strip ( ) for origin in r . origin . split ( ' , ' ) ] )
# Search for refunds as well
refund_ids = self . env [ ' account.invoice ' ] . browse ( )
if invoice_ids :
for inv in invoice_ids :
refund_ids + = refund_ids . search ( [ ( ' type ' , ' = ' , ' out_refund ' ) , ( ' origin ' , ' = ' , inv . number ) , ( ' origin ' , ' != ' , False ) , ( ' journal_id ' , ' = ' , inv . journal_id . id ) ] )
# Ignore the status of the deposit product
deposit_product_id = self . env [ ' sale.advance.payment.inv ' ] . _default_product_id ( )
line_invoice_status = [ line . invoice_status for line in order . order_line if line . product_id != deposit_product_id ]
if order . state not in ( ' sale ' , ' done ' ) :
invoice_status = ' no '
elif any ( invoice_status == ' to invoice ' for invoice_status in line_invoice_status ) :
invoice_status = ' to invoice '
elif all ( invoice_status == ' invoiced ' for invoice_status in line_invoice_status ) :
invoice_status = ' invoiced '
elif all ( invoice_status in [ ' invoiced ' , ' upselling ' ] for invoice_status in line_invoice_status ) :
invoice_status = ' upselling '
else :
invoice_status = ' no '
order . update ( {
' invoice_count ' : len ( set ( invoice_ids . ids + refund_ids . ids ) ) ,
' invoice_ids ' : invoice_ids . ids + refund_ids . ids ,
' invoice_status ' : invoice_status
} )
@api.model
def get_empty_list_help ( self , help ) :
if help :
return ' <p class= ' ' oe_view_nocontent_create ' ' " > %s </p> ' % ( help )
return super ( SaleOrder , self ) . get_empty_list_help ( help )
def _get_default_access_token ( self ) :
return str ( uuid . uuid4 ( ) )
@api.model
def _default_note ( self ) :
return self . env [ ' ir.config_parameter ' ] . sudo ( ) . get_param ( ' sale.use_sale_note ' ) and self . env . user . company_id . sale_note or ' '
@api.model
def _get_default_team ( self ) :
return self . env [ ' crm.team ' ] . _get_default_team_id ( )
@api.onchange ( ' fiscal_position_id ' )
def _compute_tax_id ( self ) :
"""
Trigger the recompute of the taxes if the fiscal position is changed on the SO .
"""
for order in self :
order . order_line . _compute_tax_id ( )
name = fields . Char ( string = ' Order Reference ' , required = True , copy = False , readonly = True , states = { ' draft ' : [ ( ' readonly ' , False ) ] } , index = True , default = lambda self : _ ( ' New ' ) )
origin = fields . Char ( string = ' Source Document ' , help = " Reference of the document that generated this sales order request. " )
client_order_ref = fields . Char ( string = ' Customer Reference ' , copy = False )
access_token = fields . Char (
' Security Token ' , copy = False ,
default = _get_default_access_token )
state = fields . Selection ( [
( ' draft ' , ' Quotation ' ) ,
( ' sent ' , ' Quotation Sent ' ) ,
( ' sale ' , ' Sales Order ' ) ,
( ' done ' , ' Locked ' ) ,
( ' cancel ' , ' Cancelled ' ) ,
] , string = ' Status ' , readonly = True , copy = False , index = True , track_visibility = ' onchange ' , default = ' draft ' )
date_order = fields . Datetime ( string = ' Order Date ' , required = True , readonly = True , index = True , states = { ' draft ' : [ ( ' readonly ' , False ) ] , ' sent ' : [ ( ' readonly ' , False ) ] } , copy = False , default = fields . Datetime . now )
validity_date = fields . Date ( string = ' Expiration Date ' , readonly = True , copy = False , states = { ' draft ' : [ ( ' readonly ' , False ) ] , ' sent ' : [ ( ' readonly ' , False ) ] } ,
help = " Manually set the expiration date of your quotation (offer), or it will set the date automatically based on the template if online quotation is installed. " )
is_expired = fields . Boolean ( compute = ' _compute_is_expired ' , string = " Is expired " )
create_date = fields . Datetime ( string = ' Creation Date ' , readonly = True , index = True , help = " Date on which sales order is created. " )
2018-04-05 10:25:40 +02:00
confirmation_date = fields . Datetime ( string = ' Confirmation Date ' , readonly = True , index = True , help = " Date on which the sales order is confirmed. " , oldname = " date_confirm " , copy = False )
2018-01-16 06:58:15 +01:00
user_id = fields . Many2one ( ' res.users ' , string = ' Salesperson ' , index = True , track_visibility = ' onchange ' , default = lambda self : self . env . user )
partner_id = fields . Many2one ( ' res.partner ' , string = ' Customer ' , readonly = True , states = { ' draft ' : [ ( ' readonly ' , False ) ] , ' sent ' : [ ( ' readonly ' , False ) ] } , required = True , change_default = True , index = True , track_visibility = ' always ' )
partner_invoice_id = fields . Many2one ( ' res.partner ' , string = ' Invoice Address ' , readonly = True , required = True , states = { ' draft ' : [ ( ' readonly ' , False ) ] , ' sent ' : [ ( ' readonly ' , False ) ] } , help = " Invoice address for current sales order. " )
partner_shipping_id = fields . Many2one ( ' res.partner ' , string = ' Delivery Address ' , readonly = True , required = True , states = { ' draft ' : [ ( ' readonly ' , False ) ] , ' sent ' : [ ( ' readonly ' , False ) ] } , help = " Delivery address for current sales order. " )
pricelist_id = fields . Many2one ( ' product.pricelist ' , string = ' Pricelist ' , required = True , readonly = True , states = { ' draft ' : [ ( ' readonly ' , False ) ] , ' sent ' : [ ( ' readonly ' , False ) ] } , help = " Pricelist for current sales order. " )
currency_id = fields . Many2one ( " res.currency " , related = ' pricelist_id.currency_id ' , string = " Currency " , readonly = True , required = True )
analytic_account_id = fields . Many2one ( ' account.analytic.account ' , ' Analytic Account ' , readonly = True , states = { ' draft ' : [ ( ' readonly ' , False ) ] , ' sent ' : [ ( ' readonly ' , False ) ] } , help = " The analytic account related to a sales order. " , copy = False , oldname = ' project_id ' )
order_line = fields . One2many ( ' sale.order.line ' , ' order_id ' , string = ' Order Lines ' , states = { ' cancel ' : [ ( ' readonly ' , True ) ] , ' done ' : [ ( ' readonly ' , True ) ] } , copy = True , auto_join = True )
invoice_count = fields . Integer ( string = ' # of Invoices ' , compute = ' _get_invoiced ' , readonly = True )
invoice_ids = fields . Many2many ( " account.invoice " , string = ' Invoices ' , compute = " _get_invoiced " , readonly = True , copy = False )
invoice_status = fields . Selection ( [
( ' upselling ' , ' Upselling Opportunity ' ) ,
( ' invoiced ' , ' Fully Invoiced ' ) ,
( ' to invoice ' , ' To Invoice ' ) ,
( ' no ' , ' Nothing to Invoice ' )
] , string = ' Invoice Status ' , compute = ' _get_invoiced ' , store = True , readonly = True )
note = fields . Text ( ' Terms and conditions ' , default = _default_note )
amount_untaxed = fields . Monetary ( string = ' Untaxed Amount ' , store = True , readonly = True , compute = ' _amount_all ' , track_visibility = ' onchange ' )
amount_tax = fields . Monetary ( string = ' Taxes ' , store = True , readonly = True , compute = ' _amount_all ' )
amount_total = fields . Monetary ( string = ' Total ' , store = True , readonly = True , compute = ' _amount_all ' , track_visibility = ' always ' )
payment_term_id = fields . Many2one ( ' account.payment.term ' , string = ' Payment Terms ' , oldname = ' payment_term ' )
fiscal_position_id = fields . Many2one ( ' account.fiscal.position ' , oldname = ' fiscal_position ' , string = ' Fiscal Position ' )
company_id = fields . Many2one ( ' res.company ' , ' Company ' , default = lambda self : self . env [ ' res.company ' ] . _company_default_get ( ' sale.order ' ) )
team_id = fields . Many2one ( ' crm.team ' , ' Sales Channel ' , change_default = True , default = _get_default_team , oldname = ' section_id ' )
product_id = fields . Many2one ( ' product.product ' , related = ' order_line.product_id ' , string = ' Product ' )
2018-01-17 11:23:19 +01:00
@api.multi
@api.constrains ( ' branch_id ' , ' company_id ' )
def _check_company_branch ( self ) :
for order in self :
if order . branch_id and order . company_id != order . branch_id . company_id :
raise ValidationError ( _ (
' Configuration Error of Company: \n '
' The Sales Order Company ( %s ) and the Company ( %s ) of '
' Branch must be the same! ' ) % (
order . company_id . name , order . branch_id . company_id . name ) )
2018-01-16 06:58:15 +01:00
def _compute_portal_url ( self ) :
super ( SaleOrder , self ) . _compute_portal_url ( )
for order in self :
order . portal_url = ' /my/orders/ %s ' % ( order . id )
def _compute_is_expired ( self ) :
now = datetime . now ( )
for order in self :
if order . validity_date and fields . Datetime . from_string ( order . validity_date ) < now :
order . is_expired = True
else :
order . is_expired = False
@api.model
def _get_customer_lead ( self , product_tmpl_id ) :
return False
@api.multi
def unlink ( self ) :
for order in self :
if order . state not in ( ' draft ' , ' cancel ' ) :
raise UserError ( _ ( ' You can not delete a sent quotation or a sales order! Try to cancel it before. ' ) )
return super ( SaleOrder , self ) . unlink ( )
@api.multi
def _track_subtype ( self , init_values ) :
self . ensure_one ( )
if ' state ' in init_values and self . state == ' sale ' :
return ' sale.mt_order_confirmed '
elif ' state ' in init_values and self . state == ' sent ' :
return ' sale.mt_order_sent '
return super ( SaleOrder , self ) . _track_subtype ( init_values )
@api.multi
@api.onchange ( ' partner_shipping_id ' , ' partner_id ' )
def onchange_partner_shipping_id ( self ) :
"""
Trigger the change of fiscal position when the shipping address is modified .
"""
self . fiscal_position_id = self . env [ ' account.fiscal.position ' ] . get_fiscal_position ( self . partner_id . id , self . partner_shipping_id . id )
return { }
@api.multi
@api.onchange ( ' partner_id ' )
def onchange_partner_id ( self ) :
"""
Update the following fields when the partner is changed :
- Pricelist
- Payment terms
- Invoice address
- Delivery address
"""
if not self . partner_id :
self . update ( {
' partner_invoice_id ' : False ,
' partner_shipping_id ' : False ,
' payment_term_id ' : False ,
' fiscal_position_id ' : False ,
} )
return
addr = self . partner_id . address_get ( [ ' delivery ' , ' invoice ' ] )
values = {
' pricelist_id ' : self . partner_id . property_product_pricelist and self . partner_id . property_product_pricelist . id or False ,
' payment_term_id ' : self . partner_id . property_payment_term_id and self . partner_id . property_payment_term_id . id or False ,
' partner_invoice_id ' : addr [ ' invoice ' ] ,
' partner_shipping_id ' : addr [ ' delivery ' ] ,
' user_id ' : self . partner_id . user_id . id or self . env . uid
}
if self . env [ ' ir.config_parameter ' ] . sudo ( ) . get_param ( ' sale.use_sale_note ' ) and self . env . user . company_id . sale_note :
values [ ' note ' ] = self . with_context ( lang = self . partner_id . lang ) . env . user . company_id . sale_note
if self . partner_id . team_id :
values [ ' team_id ' ] = self . partner_id . team_id . id
self . update ( values )
@api.onchange ( ' partner_id ' )
def onchange_partner_id_warning ( self ) :
if not self . partner_id :
return
warning = { }
title = False
message = False
partner = self . partner_id
# If partner has no warning, check its company
if partner . sale_warn == ' no-message ' and partner . parent_id :
partner = partner . parent_id
if partner . sale_warn != ' no-message ' :
# Block if partner only has warning but parent company is blocked
if partner . sale_warn != ' block ' and partner . parent_id and partner . parent_id . sale_warn == ' block ' :
partner = partner . parent_id
title = ( " Warning for %s " ) % partner . name
message = partner . sale_warn_msg
warning = {
' title ' : title ,
' message ' : message ,
}
if partner . sale_warn == ' block ' :
self . update ( { ' partner_id ' : False , ' partner_invoice_id ' : False , ' partner_shipping_id ' : False , ' pricelist_id ' : False } )
return { ' warning ' : warning }
if warning :
return { ' warning ' : warning }
@api.model
def create ( self , vals ) :
if vals . get ( ' name ' , _ ( ' New ' ) ) == _ ( ' New ' ) :
if ' company_id ' in vals :
vals [ ' name ' ] = self . env [ ' ir.sequence ' ] . with_context ( force_company = vals [ ' company_id ' ] ) . next_by_code ( ' sale.order ' ) or _ ( ' New ' )
else :
vals [ ' name ' ] = self . env [ ' ir.sequence ' ] . next_by_code ( ' sale.order ' ) or _ ( ' New ' )
# Makes sure partner_invoice_id', 'partner_shipping_id' and 'pricelist_id' are defined
if any ( f not in vals for f in [ ' partner_invoice_id ' , ' partner_shipping_id ' , ' pricelist_id ' ] ) :
partner = self . env [ ' res.partner ' ] . browse ( vals . get ( ' partner_id ' ) )
addr = partner . address_get ( [ ' delivery ' , ' invoice ' ] )
vals [ ' partner_invoice_id ' ] = vals . setdefault ( ' partner_invoice_id ' , addr [ ' invoice ' ] )
vals [ ' partner_shipping_id ' ] = vals . setdefault ( ' partner_shipping_id ' , addr [ ' delivery ' ] )
vals [ ' pricelist_id ' ] = vals . setdefault ( ' pricelist_id ' , partner . property_product_pricelist and partner . property_product_pricelist . id )
result = super ( SaleOrder , self ) . create ( vals )
return result
@api.multi
def copy_data ( self , default = None ) :
if default is None :
default = { }
if ' order_line ' not in default :
default [ ' order_line ' ] = [ ( 0 , 0 , line . copy_data ( ) [ 0 ] ) for line in self . order_line . filtered ( lambda l : not l . is_downpayment ) ]
return super ( SaleOrder , self ) . copy_data ( default )
@api.multi
def name_get ( self ) :
if self . _context . get ( ' sale_show_partner_name ' ) :
res = [ ]
for order in self :
name = order . name
if order . partner_id . name :
name = ' %s - %s ' % ( name , order . partner_id . name )
res . append ( ( order . id , name ) )
return res
return super ( SaleOrder , self ) . name_get ( )
@api.model
def name_search ( self , name = ' ' , args = None , operator = ' ilike ' , limit = 100 ) :
if self . _context . get ( ' sale_show_partner_name ' ) :
if operator in ( ' ilike ' , ' like ' , ' = ' , ' =like ' , ' =ilike ' ) :
domain = expression . AND ( [
args or [ ] ,
[ ' | ' , ( ' name ' , operator , name ) , ( ' partner_id.name ' , operator , name ) ]
] )
return self . search ( domain , limit = limit ) . name_get ( )
return super ( SaleOrder , self ) . name_search ( name , args , operator , limit )
@api.model_cr_context
def _init_column ( self , column_name ) :
""" Initialize the value of the given column for existing rows.
Overridden here because we need to generate different access tokens
and by default _init_column calls the default method once and applies
it for every record .
"""
if column_name != ' access_token ' :
super ( SaleOrder , self ) . _init_column ( column_name )
else :
query = """ UPDATE %(table_name)s
SET % ( column_name ) s = md5 ( md5 ( random ( ) : : varchar | | id : : varchar ) | | clock_timestamp ( ) : : varchar ) : : uuid : : varchar
WHERE % ( column_name ) s IS NULL
""" % { ' table_name ' : self._table, ' column_name ' : column_name}
self . env . cr . execute ( query )
def _generate_access_token ( self ) :
for order in self :
order . access_token = self . _get_default_access_token ( )
@api.multi
def _prepare_invoice ( self ) :
"""
Prepare the dict of values to create the new invoice for a sales order . This method may be
overridden to implement custom invoice generation ( making sure to call super ( ) to establish
a clean extension chain ) .
"""
self . ensure_one ( )
journal_id = self . env [ ' account.invoice ' ] . default_get ( [ ' journal_id ' ] ) [ ' journal_id ' ]
if not journal_id :
raise UserError ( _ ( ' Please define an accounting sales journal for this company. ' ) )
invoice_vals = {
' name ' : self . client_order_ref or ' ' ,
' origin ' : self . name ,
2018-01-17 11:23:19 +01:00
' branch_id ' : self . branch_id and self . branch_id . id ,
2018-01-16 06:58:15 +01:00
' type ' : ' out_invoice ' ,
' account_id ' : self . partner_invoice_id . property_account_receivable_id . id ,
' partner_id ' : self . partner_invoice_id . id ,
' partner_shipping_id ' : self . partner_shipping_id . id ,
' journal_id ' : journal_id ,
' currency_id ' : self . pricelist_id . currency_id . id ,
' comment ' : self . note ,
' payment_term_id ' : self . payment_term_id . id ,
' fiscal_position_id ' : self . fiscal_position_id . id or self . partner_invoice_id . property_account_position_id . id ,
' company_id ' : self . company_id . id ,
' user_id ' : self . user_id and self . user_id . id ,
' team_id ' : self . team_id . id
}
return invoice_vals
@api.multi
def print_quotation ( self ) :
self . filtered ( lambda s : s . state == ' draft ' ) . write ( { ' state ' : ' sent ' } )
return self . env . ref ( ' sale.action_report_saleorder ' ) . report_action ( self )
@api.multi
def action_view_invoice ( self ) :
invoices = self . mapped ( ' invoice_ids ' )
action = self . env . ref ( ' account.action_invoice_tree1 ' ) . read ( ) [ 0 ]
if len ( invoices ) > 1 :
action [ ' domain ' ] = [ ( ' id ' , ' in ' , invoices . ids ) ]
elif len ( invoices ) == 1 :
action [ ' views ' ] = [ ( self . env . ref ( ' account.invoice_form ' ) . id , ' form ' ) ]
action [ ' res_id ' ] = invoices . ids [ 0 ]
else :
action = { ' type ' : ' ir.actions.act_window_close ' }
return action
@api.multi
def action_invoice_create ( self , grouped = False , final = False ) :
"""
Create the invoice associated to the SO .
: param grouped : if True , invoices are grouped by SO id . If False , invoices are grouped by
( partner_invoice_id , currency )
: param final : if True , refunds will be generated if necessary
: returns : list of created invoices
"""
inv_obj = self . env [ ' account.invoice ' ]
precision = self . env [ ' decimal.precision ' ] . precision_get ( ' Product Unit of Measure ' )
invoices = { }
references = { }
2018-07-13 11:26:56 +02:00
invoices_origin = { }
invoices_name = { }
2018-01-16 06:58:15 +01:00
for order in self :
group_key = order . id if grouped else ( order . partner_invoice_id . id , order . currency_id . id )
for line in order . order_line . sorted ( key = lambda l : l . qty_to_invoice < 0 ) :
if float_is_zero ( line . qty_to_invoice , precision_digits = precision ) :
continue
if group_key not in invoices :
inv_data = order . _prepare_invoice ( )
invoice = inv_obj . create ( inv_data )
references [ invoice ] = order
invoices [ group_key ] = invoice
2018-07-13 11:26:56 +02:00
invoices_origin [ group_key ] = [ invoice . origin ]
invoices_name [ group_key ] = [ invoice . name ]
2018-01-16 06:58:15 +01:00
elif group_key in invoices :
2018-07-13 11:26:56 +02:00
if order . name not in invoices_origin [ group_key ] :
invoices_origin [ group_key ] . append ( order . name )
if order . client_order_ref and order . client_order_ref not in invoices_name [ group_key ] :
invoices_name [ group_key ] . append ( order . client_order_ref )
2018-01-16 06:58:15 +01:00
if line . qty_to_invoice > 0 :
line . invoice_line_create ( invoices [ group_key ] . id , line . qty_to_invoice )
elif line . qty_to_invoice < 0 and final :
line . invoice_line_create ( invoices [ group_key ] . id , line . qty_to_invoice )
if references . get ( invoices . get ( group_key ) ) :
if order not in references [ invoices [ group_key ] ] :
2018-07-13 11:26:56 +02:00
references [ invoices [ group_key ] ] | = order
for group_key in invoices :
invoices [ group_key ] . write ( { ' name ' : ' , ' . join ( invoices_name [ group_key ] ) ,
' origin ' : ' , ' . join ( invoices_origin [ group_key ] ) } )
2018-01-16 06:58:15 +01:00
if not invoices :
raise UserError ( _ ( ' There is no invoiceable line. ' ) )
for invoice in invoices . values ( ) :
if not invoice . invoice_line_ids :
raise UserError ( _ ( ' There is no invoiceable line. ' ) )
# If invoice is negative, do a refund invoice instead
if invoice . amount_untaxed < 0 :
invoice . type = ' out_refund '
for line in invoice . invoice_line_ids :
line . quantity = - line . quantity
# Use additional field helper function (for account extensions)
for line in invoice . invoice_line_ids :
line . _set_additional_fields ( invoice )
# Necessary to force computation of taxes. In account_invoice, they are triggered
# by onchanges, which are not triggered when doing a create.
invoice . compute_taxes ( )
invoice . message_post_with_view ( ' mail.message_origin_link ' ,
values = { ' self ' : invoice , ' origin ' : references [ invoice ] } ,
subtype_id = self . env . ref ( ' mail.mt_note ' ) . id )
return [ inv . id for inv in invoices . values ( ) ]
@api.multi
def action_draft ( self ) :
orders = self . filtered ( lambda s : s . state in [ ' cancel ' , ' sent ' ] )
return orders . write ( {
' state ' : ' draft ' ,
} )
@api.multi
def action_cancel ( self ) :
return self . write ( { ' state ' : ' cancel ' } )
@api.multi
def action_quotation_send ( self ) :
'''
This function opens a window to compose an email , with the edi sale template message loaded by default
'''
self . ensure_one ( )
ir_model_data = self . env [ ' ir.model.data ' ]
try :
template_id = ir_model_data . get_object_reference ( ' sale ' , ' email_template_edi_sale ' ) [ 1 ]
except ValueError :
template_id = False
try :
compose_form_id = ir_model_data . get_object_reference ( ' mail ' , ' email_compose_message_wizard_form ' ) [ 1 ]
except ValueError :
compose_form_id = False
ctx = {
' default_model ' : ' sale.order ' ,
' default_res_id ' : self . ids [ 0 ] ,
' default_use_template ' : bool ( template_id ) ,
' default_template_id ' : template_id ,
' default_composition_mode ' : ' comment ' ,
' mark_so_as_sent ' : True ,
' custom_layout ' : " sale.mail_template_data_notification_email_sale_order " ,
' proforma ' : self . env . context . get ( ' proforma ' , False ) ,
' force_email ' : True
}
return {
' type ' : ' ir.actions.act_window ' ,
' view_type ' : ' form ' ,
' view_mode ' : ' form ' ,
' res_model ' : ' mail.compose.message ' ,
' views ' : [ ( compose_form_id , ' form ' ) ] ,
' view_id ' : compose_form_id ,
' target ' : ' new ' ,
' context ' : ctx ,
}
@api.multi
def force_quotation_send ( self ) :
for order in self :
email_act = order . action_quotation_send ( )
if email_act and email_act . get ( ' context ' ) :
email_ctx = email_act [ ' context ' ]
email_ctx . update ( default_email_from = order . company_id . email )
order . with_context ( email_ctx ) . message_post_with_template ( email_ctx . get ( ' default_template_id ' ) )
return True
@api.multi
def action_done ( self ) :
return self . write ( { ' state ' : ' done ' } )
@api.multi
def action_unlock ( self ) :
self . write ( { ' state ' : ' sale ' } )
@api.multi
def _action_confirm ( self ) :
for order in self . filtered ( lambda order : order . partner_id not in order . message_partner_ids ) :
order . message_subscribe ( [ order . partner_id . id ] )
self . write ( {
' state ' : ' sale ' ,
' confirmation_date ' : fields . Datetime . now ( )
} )
if self . env . context . get ( ' send_email ' ) :
self . force_quotation_send ( )
# create an analytic account if at least an expense product
if any ( [ expense_policy != ' no ' for expense_policy in self . order_line . mapped ( ' product_id.expense_policy ' ) ] ) :
if not self . analytic_account_id :
self . _create_analytic_account ( )
return True
@api.multi
def action_confirm ( self ) :
self . _action_confirm ( )
if self . env [ ' ir.config_parameter ' ] . sudo ( ) . get_param ( ' sale.auto_done_setting ' ) :
self . action_done ( )
return True
@api.multi
def _create_analytic_account ( self , prefix = None ) :
for order in self :
name = order . name
if prefix :
name = prefix + " : " + order . name
analytic = self . env [ ' account.analytic.account ' ] . create ( {
' name ' : name ,
' code ' : order . client_order_ref ,
2018-01-17 11:23:19 +01:00
' branch_id ' : order . branch_id and order . branch_id . id ,
2018-01-16 06:58:15 +01:00
' company_id ' : order . company_id . id ,
' partner_id ' : order . partner_id . id
} )
order . analytic_account_id = analytic
@api.multi
def order_lines_layouted ( self ) :
"""
Returns this order lines classified by sale_layout_category and separated in
pages according to the category pagebreaks . Used to render the report .
"""
self . ensure_one ( )
report_pages = [ [ ] ]
for category , lines in groupby ( self . order_line , lambda l : l . layout_category_id ) :
# If last added category induced a pagebreak, this one will be on a new page
if report_pages [ - 1 ] and report_pages [ - 1 ] [ - 1 ] [ ' pagebreak ' ] :
report_pages . append ( [ ] )
# Append category to current report page
report_pages [ - 1 ] . append ( {
2018-04-05 10:25:40 +02:00
' name ' : category and category . name or _ ( ' Uncategorized ' ) ,
2018-01-16 06:58:15 +01:00
' subtotal ' : category and category . subtotal ,
' pagebreak ' : category and category . pagebreak ,
' lines ' : list ( lines )
} )
return report_pages
@api.multi
def _get_tax_amount_by_group ( self ) :
self . ensure_one ( )
res = { }
for line in self . order_line :
2018-04-05 10:25:40 +02:00
price_reduce = line . price_unit * ( 1.0 - line . discount / 100.0 )
taxes = line . tax_id . compute_all ( price_reduce , quantity = line . product_uom_qty , product = line . product_id , partner = self . partner_shipping_id ) [ ' taxes ' ]
2018-01-16 06:58:15 +01:00
for tax in line . tax_id :
group = tax . tax_group_id
res . setdefault ( group , { ' amount ' : 0.0 , ' base ' : 0.0 } )
for t in taxes :
2018-04-05 10:25:40 +02:00
if t [ ' id ' ] == tax . id or t [ ' id ' ] in tax . children_tax_ids . ids :
res [ group ] [ ' amount ' ] + = t [ ' amount ' ]
res [ group ] [ ' base ' ] + = t [ ' base ' ]
2018-01-16 06:58:15 +01:00
res = sorted ( res . items ( ) , key = lambda l : l [ 0 ] . sequence )
res = [ ( l [ 0 ] . name , l [ 1 ] [ ' amount ' ] , l [ 1 ] [ ' base ' ] , len ( res ) ) for l in res ]
return res
@api.multi
def get_access_action ( self , access_uid = None ) :
""" Instead of the classic form view, redirect to the online order for
portal users or if force_website = True in the context . """
# TDE note: read access on sales order to portal users granted to followed sales orders
self . ensure_one ( )
if self . state != ' cancel ' and ( self . state != ' draft ' or self . env . context . get ( ' mark_so_as_sent ' ) ) :
user , record = self . env . user , self
if access_uid :
user = self . env [ ' res.users ' ] . sudo ( ) . browse ( access_uid )
record = self . sudo ( user )
if user . share or self . env . context . get ( ' force_website ' ) :
try :
record . check_access_rule ( ' read ' )
except AccessError :
if self . env . context . get ( ' force_website ' ) :
return {
' type ' : ' ir.actions.act_url ' ,
' url ' : ' /my/orders/ %s ' % self . id ,
' target ' : ' self ' ,
' res_id ' : self . id ,
}
else :
pass
else :
return {
' type ' : ' ir.actions.act_url ' ,
' url ' : ' /my/orders/ %s ?access_token= %s ' % ( self . id , self . access_token ) ,
' target ' : ' self ' ,
' res_id ' : self . id ,
}
return super ( SaleOrder , self ) . get_access_action ( access_uid )
def get_mail_url ( self ) :
return self . get_share_url ( )
def get_portal_confirmation_action ( self ) :
return self . env [ ' ir.config_parameter ' ] . sudo ( ) . get_param ( ' sale.sale_portal_confirmation_options ' , default = ' none ' )
@api.multi
def _notification_recipients ( self , message , groups ) :
groups = super ( SaleOrder , self ) . _notification_recipients ( message , groups )
self . ensure_one ( )
if self . state not in ( ' draft ' , ' cancel ' ) :
for group_name , group_method , group_data in groups :
if group_name == ' customer ' :
continue
group_data [ ' has_button_access ' ] = True
return groups
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 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 '
@api.depends ( ' product_uom_qty ' , ' discount ' , ' price_unit ' , ' tax_id ' )
def _compute_amount ( self ) :
"""
Compute the amounts of the SO line .
"""
for line in self :
price = line . price_unit * ( 1 - ( line . discount or 0.0 ) / 100.0 )
taxes = line . tax_id . compute_all ( price , line . order_id . currency_id , line . product_uom_qty , product = line . product_id , partner = line . order_id . partner_shipping_id )
line . update ( {
' price_tax ' : sum ( t . get ( ' amount ' , 0.0 ) for t in taxes . get ( ' taxes ' , [ ] ) ) ,
' price_total ' : taxes [ ' total_included ' ] ,
' price_subtotal ' : taxes [ ' total_excluded ' ] ,
} )
@api.depends ( ' product_id ' , ' order_id.state ' , ' qty_invoiced ' , ' qty_delivered ' )
def _compute_product_updatable ( self ) :
for line in self :
if line . state in [ ' done ' , ' cancel ' ] or ( line . state == ' sale ' and ( line . qty_invoiced > 0 or line . qty_delivered > 0 ) ) :
line . product_updatable = False
else :
line . product_updatable = True
@api.depends ( ' product_id.invoice_policy ' , ' order_id.state ' )
def _compute_qty_delivered_updateable ( self ) :
for line in self :
line . qty_delivered_updateable = ( line . order_id . state == ' sale ' ) and ( line . product_id . service_type == ' manual ' ) and ( line . product_id . expense_policy == ' no ' )
@api.depends ( ' state ' ,
' price_reduce_taxinc ' ,
' qty_delivered ' ,
' invoice_lines ' ,
' invoice_lines.price_total ' ,
' invoice_lines.invoice_id ' ,
' invoice_lines.invoice_id.state ' ,
' invoice_lines.invoice_id.refund_invoice_ids ' ,
' invoice_lines.invoice_id.refund_invoice_ids.state ' ,
' invoice_lines.invoice_id.refund_invoice_ids.amount_total ' )
def _compute_invoice_amount ( self ) :
for line in self :
# Invoice lines referenced by this line
invoice_lines = line . invoice_lines . filtered ( lambda l : l . invoice_id . state in ( ' open ' , ' paid ' ) )
# Refund invoices linked to invoice_lines
refund_invoices = invoice_lines . mapped ( ' invoice_id.refund_invoice_ids ' ) . filtered ( lambda inv : inv . state in ( ' open ' , ' paid ' ) )
# Total invoiced amount
invoiced_amount_total = sum ( invoice_lines . mapped ( ' price_total ' ) )
# Total refunded amount
refund_amount_total = sum ( refund_invoices . mapped ( ' amount_total ' ) )
# Total of remaining amount to invoice on the sale ordered (and draft invoice included) to support upsell (when
# delivered quantity is higher than ordered one). Draft invoice are ignored on purpose, the 'to invoice' should
# come only from the SO lines.
total_sale_line = line . price_total
if line . product_id . invoice_policy == ' delivery ' :
total_sale_line = line . price_reduce_taxinc * line . qty_delivered
line . amt_invoiced = invoiced_amount_total - refund_amount_total
line . amt_to_invoice = ( total_sale_line - invoiced_amount_total ) if line . state in [ ' sale ' , ' done ' ] else 0.0
@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
@api.depends ( ' invoice_lines.invoice_id.state ' , ' invoice_lines.quantity ' )
def _get_invoice_qty ( self ) :
"""
Compute the quantity invoiced . If case of a refund , the quantity invoiced is decreased . Note
that this is the case only if the refund is generated from the SO and that is intentional : if
a refund made would automatically decrease the invoiced quantity , then there is a risk of reinvoicing
it automatically , which may not be wanted at all . That ' s why the refund has to be created from the SO
"""
for line in self :
qty_invoiced = 0.0
for invoice_line in line . invoice_lines :
if invoice_line . invoice_id . state != ' cancel ' :
if invoice_line . invoice_id . type == ' out_invoice ' :
qty_invoiced + = invoice_line . uom_id . _compute_quantity ( invoice_line . quantity , line . product_uom )
elif invoice_line . invoice_id . type == ' out_refund ' :
qty_invoiced - = invoice_line . uom_id . _compute_quantity ( invoice_line . quantity , line . product_uom )
line . qty_invoiced = qty_invoiced
@api.depends ( ' price_unit ' , ' discount ' )
def _get_price_reduce ( self ) :
for line in self :
line . price_reduce = line . price_unit * ( 1.0 - line . discount / 100.0 )
@api.depends ( ' price_total ' , ' product_uom_qty ' )
def _get_price_reduce_tax ( self ) :
for line in self :
line . price_reduce_taxinc = line . price_total / line . product_uom_qty if line . product_uom_qty else 0.0
@api.depends ( ' price_subtotal ' , ' product_uom_qty ' )
def _get_price_reduce_notax ( self ) :
for line in self :
line . price_reduce_taxexcl = line . price_subtotal / line . product_uom_qty if line . product_uom_qty else 0.0
@api.multi
def _compute_tax_id ( self ) :
for line in self :
fpos = line . order_id . fiscal_position_id or line . order_id . partner_id . property_account_position_id
# If company_id is set, always filter taxes by the company
taxes = line . product_id . taxes_id . filtered ( lambda r : not line . company_id or r . company_id == line . company_id )
line . tax_id = fpos . map_tax ( taxes , line . product_id , line . order_id . partner_shipping_id ) if fpos else taxes
@api.model
def _get_purchase_price ( self , pricelist , product , product_uom , date ) :
return { }
@api.model
def _prepare_add_missing_fields ( self , values ) :
""" Deduce missing required fields from the onchange """
res = { }
onchange_fields = [ ' name ' , ' price_unit ' , ' product_uom ' , ' tax_id ' ]
if values . get ( ' order_id ' ) and values . get ( ' product_id ' ) and any ( f not in values for f in onchange_fields ) :
line = self . new ( values )
line . product_id_change ( )
for field in onchange_fields :
if field not in values :
res [ field ] = line . _fields [ field ] . convert_to_write ( line [ field ] , line )
return res
@api.model
def create ( self , values ) :
values . update ( self . _prepare_add_missing_fields ( values ) )
line = super ( SaleOrderLine , self ) . create ( values )
if line . order_id . state == ' sale ' :
msg = _ ( " Extra line with %s " ) % ( line . product_id . display_name , )
line . order_id . message_post ( body = msg )
# create an analytic account if at least an expense product
if line . product_id . expense_policy != ' no ' and not self . order_id . analytic_account_id :
self . order_id . _create_analytic_account ( )
return line
def _update_line_quantity ( self , values ) :
orders = self . mapped ( ' order_id ' )
for order in orders :
order_lines = self . filtered ( lambda x : x . order_id == order )
msg = " <b>The ordered quantity has been updated.</b><ul> "
for line in order_lines :
msg + = " <li> %s : " % ( line . product_id . display_name , )
msg + = " <br/> " + _ ( " Ordered Quantity " ) + " : %s -> %s <br/> " % (
line . product_uom_qty , float ( values [ ' product_uom_qty ' ] ) , )
if line . product_id . type in ( ' consu ' , ' product ' ) :
msg + = _ ( " Delivered Quantity " ) + " : %s <br/> " % ( line . qty_delivered , )
msg + = _ ( " Invoiced Quantity " ) + " : %s <br/> " % ( line . qty_invoiced , )
msg + = " </ul> "
order . message_post ( body = msg )
@api.multi
def write ( self , values ) :
if ' product_uom_qty ' in values :
precision = self . env [ ' decimal.precision ' ] . precision_get ( ' Product Unit of Measure ' )
self . filtered (
lambda r : r . state == ' sale ' and float_compare ( r . product_uom_qty , values [ ' product_uom_qty ' ] , precision_digits = precision ) != 0 ) . _update_line_quantity ( values )
# Prevent writing on a locked SO.
protected_fields = self . _get_protected_fields ( )
if ' done ' in self . mapped ( ' order_id.state ' ) and any ( f in values . keys ( ) for f in protected_fields ) :
2018-04-05 10:25:40 +02:00
protected_fields_modified = list ( set ( protected_fields ) & set ( values . keys ( ) ) )
2018-01-16 06:58:15 +01:00
fields = self . env [ ' ir.model.fields ' ] . search ( [
2018-04-05 10:25:40 +02:00
( ' name ' , ' in ' , protected_fields_modified ) , ( ' model ' , ' = ' , self . _name )
2018-01-16 06:58:15 +01:00
] )
raise UserError (
_ ( ' It is forbidden to modify the following fields in a locked order: \n %s ' )
% ' \n ' . join ( fields . mapped ( ' field_description ' ) )
)
result = super ( SaleOrderLine , self ) . write ( values )
return result
order_id = fields . Many2one ( ' sale.order ' , string = ' Order Reference ' , required = True , ondelete = ' cascade ' , index = True , copy = False )
name = fields . Text ( string = ' Description ' , required = True )
sequence = fields . Integer ( string = ' Sequence ' , default = 10 )
invoice_lines = fields . Many2many ( ' account.invoice.line ' , ' sale_order_line_invoice_rel ' , ' order_line_id ' , ' invoice_line_id ' , string = ' Invoice Lines ' , copy = False )
invoice_status = fields . Selection ( [
( ' upselling ' , ' Upselling Opportunity ' ) ,
( ' invoiced ' , ' Fully Invoiced ' ) ,
( ' to invoice ' , ' To Invoice ' ) ,
( ' no ' , ' Nothing to Invoice ' )
] , string = ' Invoice Status ' , compute = ' _compute_invoice_status ' , store = True , readonly = True , default = ' no ' )
price_unit = fields . Float ( ' Unit Price ' , required = True , digits = dp . get_precision ( ' Product Price ' ) , default = 0.0 )
price_subtotal = fields . Monetary ( compute = ' _compute_amount ' , string = ' Subtotal ' , readonly = True , store = True )
price_tax = fields . Float ( compute = ' _compute_amount ' , string = ' Taxes ' , readonly = True , store = True )
price_total = fields . Monetary ( compute = ' _compute_amount ' , string = ' Total ' , readonly = True , store = True )
price_reduce = fields . Float ( compute = ' _get_price_reduce ' , string = ' Price Reduce ' , digits = dp . get_precision ( ' Product Price ' ) , readonly = True , store = True )
tax_id = fields . Many2many ( ' account.tax ' , string = ' Taxes ' , domain = [ ' | ' , ( ' active ' , ' = ' , False ) , ( ' active ' , ' = ' , True ) ] )
price_reduce_taxinc = fields . Monetary ( compute = ' _get_price_reduce_tax ' , string = ' Price Reduce Tax inc ' , readonly = True , store = True )
price_reduce_taxexcl = fields . Monetary ( compute = ' _get_price_reduce_notax ' , string = ' Price Reduce Tax excl ' , readonly = True , store = True )
discount = fields . Float ( string = ' Discount ( % ) ' , digits = dp . get_precision ( ' Discount ' ) , default = 0.0 )
product_id = fields . Many2one ( ' product.product ' , string = ' Product ' , domain = [ ( ' sale_ok ' , ' = ' , True ) ] , change_default = True , ondelete = ' restrict ' , required = True )
product_updatable = fields . Boolean ( compute = ' _compute_product_updatable ' , string = ' Can Edit Product ' , readonly = True , default = True )
product_uom_qty = fields . Float ( string = ' Quantity ' , digits = dp . get_precision ( ' Product Unit of Measure ' ) , required = True , default = 1.0 )
product_uom = fields . Many2one ( ' product.uom ' , string = ' Unit of Measure ' , required = True )
# Non-stored related field to allow portal user to see the image of the product he has ordered
product_image = fields . Binary ( ' Product Image ' , related = " product_id.image " , store = False )
qty_delivered_updateable = fields . Boolean ( compute = ' _compute_qty_delivered_updateable ' , string = ' Can Edit Delivered ' , readonly = True , default = True )
qty_delivered = fields . Float ( string = ' Delivered ' , copy = False , digits = dp . get_precision ( ' Product Unit of Measure ' ) , default = 0.0 )
qty_to_invoice = fields . Float (
compute = ' _get_to_invoice_qty ' , string = ' To Invoice ' , store = True , readonly = True ,
digits = dp . get_precision ( ' Product Unit of Measure ' ) )
qty_invoiced = fields . Float (
compute = ' _get_invoice_qty ' , string = ' Invoiced ' , store = True , readonly = True ,
digits = dp . get_precision ( ' Product Unit of Measure ' ) )
salesman_id = fields . Many2one ( related = ' order_id.user_id ' , store = True , string = ' Salesperson ' , readonly = True )
currency_id = fields . Many2one ( related = ' order_id.currency_id ' , store = True , string = ' Currency ' , readonly = True )
company_id = fields . Many2one ( related = ' order_id.company_id ' , string = ' Company ' , store = True , readonly = True )
2018-01-17 11:23:19 +01:00
branch_id = fields . Many2one ( related = ' order_id.branch_id ' ,
string = ' Branch ' , store = True , readonly = True )
2018-01-16 06:58:15 +01:00
order_partner_id = fields . Many2one ( related = ' order_id.partner_id ' , store = True , string = ' Customer ' )
analytic_tag_ids = fields . Many2many ( ' account.analytic.tag ' , string = ' Analytic Tags ' )
is_downpayment = fields . Boolean (
string = " Is a down payment " , help = " Down payments are made when creating invoices from a sales order. "
" They are not copied when duplicating a sales order. " )
state = fields . Selection ( [
( ' draft ' , ' Quotation ' ) ,
( ' sent ' , ' Quotation Sent ' ) ,
( ' sale ' , ' Sales Order ' ) ,
( ' done ' , ' Done ' ) ,
( ' cancel ' , ' Cancelled ' ) ,
] , related = ' order_id.state ' , string = ' Order Status ' , readonly = True , copy = False , store = True , default = ' draft ' )
customer_lead = fields . Float (
' Delivery Lead Time ' , required = True , default = 0.0 ,
help = " Number of days between the order confirmation and the shipping of the products to the customer " , oldname = " delay " )
amt_to_invoice = fields . Monetary ( string = ' Amount To Invoice ' , compute = ' _compute_invoice_amount ' , compute_sudo = True , store = True )
amt_invoiced = fields . Monetary ( string = ' Amount Invoiced ' , compute = ' _compute_invoice_amount ' , compute_sudo = True , store = True )
layout_category_id = fields . Many2one ( ' sale.layout_category ' , string = ' Section ' )
layout_category_sequence = fields . Integer ( string = ' Layout Sequence ' )
# TODO: remove layout_category_sequence in master or make it work properly
@api.multi
def _prepare_invoice_line ( self , qty ) :
"""
Prepare the dict of values to create the new invoice line for a sales order line .
: param qty : float quantity to invoice
"""
self . ensure_one ( )
res = { }
account = self . product_id . property_account_income_id or self . product_id . categ_id . property_account_income_categ_id
if not account :
raise UserError ( _ ( ' Please define income account for this product: " %s " (id: %d ) - or for its category: " %s " . ' ) %
( self . product_id . name , self . product_id . id , self . product_id . categ_id . name ) )
fpos = self . order_id . fiscal_position_id or self . order_id . partner_id . property_account_position_id
if fpos :
account = fpos . map_account ( account )
res = {
' name ' : self . name ,
' sequence ' : self . sequence ,
' origin ' : self . order_id . name ,
2018-01-17 11:23:19 +01:00
' branch_id ' : self . branch_id and self . branch_id . id ,
2018-01-16 06:58:15 +01:00
' account_id ' : account . id ,
' price_unit ' : self . price_unit ,
' quantity ' : qty ,
' discount ' : self . discount ,
' uom_id ' : self . product_uom . id ,
' product_id ' : self . product_id . id or False ,
' layout_category_id ' : self . layout_category_id and self . layout_category_id . id or False ,
' invoice_line_tax_ids ' : [ ( 6 , 0 , self . tax_id . ids ) ] ,
' account_analytic_id ' : self . order_id . analytic_account_id . id ,
' analytic_tag_ids ' : [ ( 6 , 0 , self . analytic_tag_ids . ids ) ] ,
}
return res
@api.multi
def invoice_line_create ( self , invoice_id , qty ) :
""" Create an invoice line. The quantity to invoice can be positive (invoice) or negative (refund).
: param invoice_id : integer
: param qty : float quantity to invoice
: returns recordset of account . invoice . line created
"""
invoice_lines = self . env [ ' account.invoice.line ' ]
precision = self . env [ ' decimal.precision ' ] . precision_get ( ' Product Unit of Measure ' )
for line in self :
if not float_is_zero ( qty , precision_digits = precision ) :
vals = line . _prepare_invoice_line ( qty = qty )
vals . update ( { ' invoice_id ' : invoice_id , ' sale_line_ids ' : [ ( 6 , 0 , [ line . id ] ) ] } )
invoice_lines | = self . env [ ' account.invoice.line ' ] . create ( vals )
return invoice_lines
@api.multi
def _prepare_procurement_values ( self , group_id = False ) :
""" Prepare specific key for moves or other components that will be created from a procurement rule
comming from a sale order line . This method could be override in order to add other custom key that could
be used in move / po creation .
"""
return { }
@api.multi
def _get_display_price ( self , product ) :
# TO DO: move me in master/saas-16 on sale.order
if self . order_id . pricelist_id . discount_policy == ' with_discount ' :
return product . with_context ( pricelist = self . order_id . pricelist_id . id ) . price
final_price , rule_id = self . order_id . pricelist_id . get_product_price_rule ( self . product_id , self . product_uom_qty or 1.0 , self . order_id . partner_id )
context_partner = dict ( self . env . context , partner_id = self . order_id . partner_id . id , date = self . order_id . date_order )
base_price , currency_id = self . with_context ( context_partner ) . _get_real_price_currency ( self . product_id , rule_id , self . product_uom_qty , self . product_uom , self . order_id . pricelist_id . id )
if currency_id != self . order_id . pricelist_id . currency_id . id :
base_price = self . env [ ' res.currency ' ] . browse ( currency_id ) . with_context ( context_partner ) . compute ( base_price , self . order_id . pricelist_id . currency_id )
# negative discounts (= surcharge) are included in the display price
return max ( base_price , final_price )
@api.multi
@api.onchange ( ' product_id ' )
def product_id_change ( self ) :
if not self . product_id :
return { ' domain ' : { ' product_uom ' : [ ] } }
vals = { }
domain = { ' product_uom ' : [ ( ' category_id ' , ' = ' , self . product_id . uom_id . category_id . id ) ] }
if not self . product_uom or ( self . product_id . uom_id . id != self . product_uom . id ) :
vals [ ' product_uom ' ] = self . product_id . uom_id
vals [ ' product_uom_qty ' ] = 1.0
product = self . product_id . with_context (
lang = self . order_id . partner_id . lang ,
partner = self . order_id . partner_id . id ,
quantity = vals . get ( ' product_uom_qty ' ) or self . product_uom_qty ,
date = self . order_id . date_order ,
pricelist = self . order_id . pricelist_id . id ,
uom = self . product_uom . id
)
result = { ' domain ' : domain }
title = False
message = False
warning = { }
if product . sale_line_warn != ' no-message ' :
title = _ ( " Warning for %s " ) % product . name
message = product . sale_line_warn_msg
warning [ ' title ' ] = title
warning [ ' message ' ] = message
result = { ' warning ' : warning }
if product . sale_line_warn == ' block ' :
self . product_id = False
return result
name = product . name_get ( ) [ 0 ] [ 1 ]
if product . description_sale :
name + = ' \n ' + product . description_sale
vals [ ' name ' ] = name
self . _compute_tax_id ( )
if self . order_id . pricelist_id and self . order_id . partner_id :
vals [ ' price_unit ' ] = self . env [ ' account.tax ' ] . _fix_tax_included_price_company ( self . _get_display_price ( product ) , product . taxes_id , self . tax_id , self . company_id )
self . update ( vals )
return result
@api.onchange ( ' product_uom ' , ' product_uom_qty ' )
def product_uom_change ( self ) :
if not self . product_uom or not self . product_id :
self . price_unit = 0.0
return
if self . order_id . pricelist_id and self . order_id . partner_id :
product = self . product_id . with_context (
lang = self . order_id . partner_id . lang ,
partner = self . order_id . partner_id . id ,
quantity = self . product_uom_qty ,
date = self . order_id . date_order ,
pricelist = self . order_id . pricelist_id . id ,
uom = self . product_uom . id ,
fiscal_position = self . env . context . get ( ' fiscal_position ' )
)
self . price_unit = self . env [ ' account.tax ' ] . _fix_tax_included_price_company ( self . _get_display_price ( product ) , product . taxes_id , self . tax_id , self . company_id )
@api.multi
def name_get ( self ) :
2018-07-13 11:26:56 +02:00
result = [ ]
for so_line in self :
name = ' %s - %s ' % ( so_line . order_id . name , so_line . name . split ( ' \n ' ) [ 0 ] or so_line . product_id . name )
if so_line . order_partner_id . ref :
name = ' %s ( %s ) ' % ( name , so_line . order_partner_id . ref )
result . append ( ( so_line . id , name ) )
return result
2018-01-16 06:58:15 +01:00
@api.model
def name_search ( self , name = ' ' , args = None , operator = ' ilike ' , limit = 100 ) :
2018-07-13 11:26:56 +02:00
if operator in ( ' ilike ' , ' like ' , ' = ' , ' =like ' , ' =ilike ' ) :
args = expression . AND ( [
args or [ ] ,
[ ' | ' , ( ' order_id.name ' , operator , name ) , ( ' name ' , operator , name ) ]
] )
2018-01-16 06:58:15 +01:00
return super ( SaleOrderLine , self ) . name_search ( name , args , operator , limit )
@api.multi
def unlink ( self ) :
if self . filtered ( lambda x : x . state in ( ' sale ' , ' done ' ) ) :
raise UserError ( _ ( ' You can not remove a sales order line. \n Discard changes and try setting the quantity to 0. ' ) )
return super ( SaleOrderLine , self ) . unlink ( )
@api.multi
def _get_delivered_qty ( self ) :
'''
Intended to be overridden in sale_stock and sale_mrp
: return : the quantity delivered
: rtype : float
'''
return 0.0
def _get_real_price_currency ( self , product , rule_id , qty , uom , pricelist_id ) :
""" Retrieve the price before applying the pricelist
: param obj product : object of current product record
: parem float qty : total quentity of product
: param tuple price_and_rule : tuple ( price , suitable_rule ) coming from pricelist computation
: param obj uom : unit of measure of current order line
: param integer pricelist_id : pricelist id of sales order """
PricelistItem = self . env [ ' product.pricelist.item ' ]
field_name = ' lst_price '
currency_id = None
product_currency = None
if rule_id :
pricelist_item = PricelistItem . browse ( rule_id )
if pricelist_item . pricelist_id . discount_policy == ' without_discount ' :
while pricelist_item . base == ' pricelist ' and pricelist_item . base_pricelist_id and pricelist_item . base_pricelist_id . discount_policy == ' without_discount ' :
price , rule_id = pricelist_item . base_pricelist_id . with_context ( uom = uom . id ) . get_product_price_rule ( product , qty , self . order_id . partner_id )
pricelist_item = PricelistItem . browse ( rule_id )
if pricelist_item . base == ' standard_price ' :
field_name = ' standard_price '
if pricelist_item . base == ' pricelist ' and pricelist_item . base_pricelist_id :
field_name = ' price '
product = product . with_context ( pricelist = pricelist_item . base_pricelist_id . id )
product_currency = pricelist_item . base_pricelist_id . currency_id
currency_id = pricelist_item . pricelist_id . currency_id
product_currency = product_currency or ( product . company_id and product . company_id . currency_id ) or self . env . user . company_id . currency_id
if not currency_id :
currency_id = product_currency
cur_factor = 1.0
else :
if currency_id . id == product_currency . id :
cur_factor = 1.0
else :
cur_factor = currency_id . _get_conversion_rate ( product_currency , currency_id )
product_uom = self . env . context . get ( ' uom ' ) or product . uom_id . id
if uom and uom . id != product_uom :
# the unit price is in a different uom
uom_factor = uom . _compute_price ( 1.0 , product . uom_id )
else :
uom_factor = 1.0
return product [ field_name ] * uom_factor * cur_factor , currency_id . id
def _get_protected_fields ( self ) :
return [
' product_id ' , ' name ' , ' price_unit ' , ' product_uom ' , ' product_uom_qty ' ,
' tax_id ' , ' analytic_tag_ids '
]
@api.onchange ( ' product_id ' , ' price_unit ' , ' product_uom ' , ' product_uom_qty ' , ' tax_id ' )
def _onchange_discount ( self ) :
self . discount = 0.0
if not ( self . product_id and self . product_uom and
self . order_id . partner_id and self . order_id . pricelist_id and
self . order_id . pricelist_id . discount_policy == ' without_discount ' and
self . env . user . has_group ( ' sale.group_discount_per_so_line ' ) ) :
return
context_partner = dict ( self . env . context , partner_id = self . order_id . partner_id . id , date = self . order_id . date_order )
pricelist_context = dict ( context_partner , uom = self . product_uom . id )
price , rule_id = self . order_id . pricelist_id . with_context ( pricelist_context ) . get_product_price_rule ( self . product_id , self . product_uom_qty or 1.0 , self . order_id . partner_id )
new_list_price , currency_id = self . with_context ( context_partner ) . _get_real_price_currency ( self . product_id , rule_id , self . product_uom_qty , self . product_uom , self . order_id . pricelist_id . id )
if new_list_price != 0 :
if self . order_id . pricelist_id . currency_id . id != currency_id :
# we need new_list_price in the same currency as price, which is in the SO's pricelist's currency
new_list_price = self . env [ ' res.currency ' ] . browse ( currency_id ) . with_context ( context_partner ) . compute ( new_list_price , self . order_id . pricelist_id . currency_id )
discount = ( new_list_price - price ) / new_list_price * 100
if discount > 0 :
self . discount = discount
###########################
# Analytic Methods
###########################
@api.multi
def _analytic_compute_delivered_quantity_domain ( self ) :
""" Return the domain of the analytic lines to use to recompute the delivered quantity
on SO lines . This method is a hook : since analytic line are used for timesheet ,
expense , . . . each use case should provide its part of the domain .
"""
2018-07-13 11:26:56 +02:00
return [ ' & ' , ( ' so_line ' , ' in ' , self . ids ) , ( ' amount ' , ' <= ' , 0.0 ) ]
2018-01-16 06:58:15 +01:00
@api.multi
def _analytic_compute_delivered_quantity ( self ) :
""" Compute and write the delivered quantity of current SO lines, based on their related
analytic lines .
"""
# avoid recomputation if no SO lines concerned
if not self :
return False
# group anaytic lines by product uom and so line
domain = self . _analytic_compute_delivered_quantity_domain ( )
data = self . env [ ' account.analytic.line ' ] . read_group (
domain ,
[ ' so_line ' , ' unit_amount ' , ' product_uom_id ' ] , [ ' product_uom_id ' , ' so_line ' ] , lazy = False
)
# Force recompute for the "unlink last line" case: if remove the last AAL link to the SO, the read_group
# will give no value for the qty of the SOL, so we need to reset it to 0.0
value_to_write = { }
if self . _context . get ( ' sale_analytic_force_recompute ' ) :
value_to_write = dict . fromkeys ( [ sol for sol in self ] , 0.0 )
# convert uom and sum all unit_amount of analytic lines to get the delivered qty of SO lines
for item in data :
if not item [ ' product_uom_id ' ] :
continue
so_line = self . browse ( item [ ' so_line ' ] [ 0 ] )
value_to_write . setdefault ( so_line , 0.0 )
uom = self . env [ ' product.uom ' ] . browse ( item [ ' product_uom_id ' ] [ 0 ] )
if so_line . product_uom . category_id == uom . category_id :
qty = uom . _compute_quantity ( item [ ' unit_amount ' ] , so_line . product_uom )
else :
qty = item [ ' unit_amount ' ]
value_to_write [ so_line ] + = qty
# write the delivered quantity
for so_line , qty in value_to_write . items ( ) :
so_line . write ( { ' qty_delivered ' : qty } )
return True
2018-07-13 11:26:56 +02:00
def _is_delivery ( self ) :
self . ensure_one ( )
return False