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 logging
from datetime import timedelta
from functools import partial
import psycopg2
import pytz
2018-01-16 11:34:37 +01:00
from flectra import api , fields , models , tools , _
from flectra . tools import float_is_zero
from flectra . exceptions import UserError
from flectra . http import request
from flectra . addons import decimal_precision as dp
2018-01-16 06:58:15 +01:00
_logger = logging . getLogger ( __name__ )
class PosOrder ( models . Model ) :
_name = " pos.order "
_description = " Point of Sale Orders "
_order = " id desc "
@api.model
def _amount_line_tax ( self , line , fiscal_position_id ) :
taxes = line . tax_ids . filtered ( lambda t : t . company_id . id == line . order_id . company_id . id )
if fiscal_position_id :
taxes = fiscal_position_id . map_tax ( taxes , line . product_id , line . order_id . partner_id )
price = line . price_unit * ( 1 - ( line . discount or 0.0 ) / 100.0 )
taxes = taxes . compute_all ( price , line . order_id . pricelist_id . currency_id , line . qty , product = line . product_id , partner = line . order_id . partner_id or False ) [ ' taxes ' ]
return sum ( tax . get ( ' amount ' , 0.0 ) for tax in taxes )
@api.model
def _order_fields ( self , ui_order ) :
process_line = partial ( self . env [ ' pos.order.line ' ] . _order_line_fields , session_id = ui_order [ ' pos_session_id ' ] )
return {
' name ' : ui_order [ ' name ' ] ,
' user_id ' : ui_order [ ' user_id ' ] or False ,
' session_id ' : ui_order [ ' pos_session_id ' ] ,
' lines ' : [ process_line ( l ) for l in ui_order [ ' lines ' ] ] if ui_order [ ' lines ' ] else False ,
' pos_reference ' : ui_order [ ' name ' ] ,
' partner_id ' : ui_order [ ' partner_id ' ] or False ,
' date_order ' : ui_order [ ' creation_date ' ] ,
' fiscal_position_id ' : ui_order [ ' fiscal_position_id ' ] ,
' pricelist_id ' : ui_order [ ' pricelist_id ' ] ,
}
def _payment_fields ( self , ui_paymentline ) :
return {
' amount ' : ui_paymentline [ ' amount ' ] or 0.0 ,
' payment_date ' : ui_paymentline [ ' name ' ] ,
' statement_id ' : ui_paymentline [ ' statement_id ' ] ,
' payment_name ' : ui_paymentline . get ( ' note ' , False ) ,
' journal ' : ui_paymentline [ ' journal_id ' ] ,
}
# This deals with orders that belong to a closed session. In order
# to recover from this situation we create a new rescue session,
# making it obvious that something went wrong.
# A new, separate, rescue session is preferred for every such recovery,
# to avoid adding unrelated orders to live sessions.
def _get_valid_session ( self , order ) :
PosSession = self . env [ ' pos.session ' ]
closed_session = PosSession . browse ( order [ ' pos_session_id ' ] )
_logger . warning ( ' session %s (ID: %s ) was closed but received order %s (total: %s ) belonging to it ' ,
closed_session . name ,
closed_session . id ,
order [ ' name ' ] ,
order [ ' amount_total ' ] )
rescue_session = PosSession . search ( [
( ' state ' , ' not in ' , ( ' closed ' , ' closing_control ' ) ) ,
( ' rescue ' , ' = ' , True ) ,
( ' config_id ' , ' = ' , closed_session . config_id . id ) ,
] , limit = 1 )
if rescue_session :
_logger . warning ( ' reusing recovery session %s for saving order %s ' , rescue_session . name , order [ ' name ' ] )
return rescue_session
_logger . warning ( ' attempting to create recovery session for saving order %s ' , order [ ' name ' ] )
new_session = PosSession . create ( {
' config_id ' : closed_session . config_id . id ,
' name ' : _ ( ' (RESCUE FOR %(session)s ) ' ) % { ' session ' : closed_session . name } ,
' rescue ' : True , # avoid conflict with live sessions
} )
# bypass opening_control (necessary when using cash control)
new_session . action_pos_session_open ( )
return new_session
def _match_payment_to_invoice ( self , order ) :
account_precision = self . env [ ' decimal.precision ' ] . precision_get ( ' Account ' )
# ignore orders with an amount_paid of 0 because those are returns through the POS
if not float_is_zero ( order [ ' amount_return ' ] , account_precision ) and not float_is_zero ( order [ ' amount_paid ' ] , account_precision ) :
cur_amount_paid = 0
payments_to_keep = [ ]
for payment in order . get ( ' statement_ids ' ) :
if cur_amount_paid + payment [ 2 ] [ ' amount ' ] > order [ ' amount_total ' ] :
payment [ 2 ] [ ' amount ' ] = order [ ' amount_total ' ] - cur_amount_paid
payments_to_keep . append ( payment )
break
cur_amount_paid + = payment [ 2 ] [ ' amount ' ]
payments_to_keep . append ( payment )
order [ ' statement_ids ' ] = payments_to_keep
order [ ' amount_return ' ] = 0
@api.model
def _process_order ( self , pos_order ) :
prec_acc = self . env [ ' decimal.precision ' ] . precision_get ( ' Account ' )
pos_session = self . env [ ' pos.session ' ] . browse ( pos_order [ ' pos_session_id ' ] )
if pos_session . state == ' closing_control ' or pos_session . state == ' closed ' :
pos_order [ ' pos_session_id ' ] = self . _get_valid_session ( pos_order ) . id
order = self . create ( self . _order_fields ( pos_order ) )
journal_ids = set ( )
for payments in pos_order [ ' statement_ids ' ] :
if not float_is_zero ( payments [ 2 ] [ ' amount ' ] , precision_digits = prec_acc ) :
order . add_payment ( self . _payment_fields ( payments [ 2 ] ) )
journal_ids . add ( payments [ 2 ] [ ' journal_id ' ] )
if pos_session . sequence_number < = pos_order [ ' sequence_number ' ] :
pos_session . write ( { ' sequence_number ' : pos_order [ ' sequence_number ' ] + 1 } )
pos_session . refresh ( )
if not float_is_zero ( pos_order [ ' amount_return ' ] , prec_acc ) :
cash_journal_id = pos_session . cash_journal_id . id
if not cash_journal_id :
# Select for change one of the cash journals used in this
# payment
cash_journal = self . env [ ' account.journal ' ] . search ( [
( ' type ' , ' = ' , ' cash ' ) ,
( ' id ' , ' in ' , list ( journal_ids ) ) ,
] , limit = 1 )
if not cash_journal :
# If none, select for change one of the cash journals of the POS
# This is used for example when a customer pays by credit card
# an amount higher than total amount of the order and gets cash back
cash_journal = [ statement . journal_id for statement in pos_session . statement_ids if statement . journal_id . type == ' cash ' ]
if not cash_journal :
raise UserError ( _ ( " No cash statement found for this session. Unable to record returned cash. " ) )
cash_journal_id = cash_journal [ 0 ] . id
order . add_payment ( {
' amount ' : - pos_order [ ' amount_return ' ] ,
' payment_date ' : fields . Datetime . now ( ) ,
' payment_name ' : _ ( ' return ' ) ,
' journal ' : cash_journal_id ,
} )
return order
def _prepare_analytic_account ( self , line ) :
''' This method is designed to be inherited in a custom module '''
return False
def _create_account_move ( self , dt , ref , journal_id , company_id ) :
date_tz_user = fields . Datetime . context_timestamp ( self , fields . Datetime . from_string ( dt ) )
date_tz_user = fields . Date . to_string ( date_tz_user )
return self . env [ ' account.move ' ] . sudo ( ) . create ( { ' ref ' : ref , ' journal_id ' : journal_id , ' date ' : date_tz_user } )
def _prepare_invoice ( self ) :
"""
Prepare the dict of values to create the new invoice for a pos order .
"""
return {
' name ' : self . name ,
' origin ' : self . name ,
' account_id ' : self . partner_id . property_account_receivable_id . id ,
' journal_id ' : self . session_id . config_id . invoice_journal_id . id ,
' company_id ' : self . company_id . id ,
' type ' : ' out_invoice ' ,
' reference ' : self . name ,
' partner_id ' : self . partner_id . id ,
' comment ' : self . note or ' ' ,
# considering partner's sale pricelist's currency
' currency_id ' : self . pricelist_id . currency_id . id ,
' user_id ' : self . env . uid ,
}
@api.model
def _get_account_move_line_group_data_type_key ( self , data_type , values ) :
"""
Return a tuple which will be used as a key for grouping account
move lines in _create_account_move_line method .
: param data_type : ' product ' , ' tax ' , . . . .
: param values : account move line values
: return : tuple ( ) representing the data_type key
"""
if data_type == ' product ' :
return ( ' product ' ,
values [ ' partner_id ' ] ,
( values [ ' product_id ' ] , tuple ( values [ ' tax_ids ' ] [ 0 ] [ 2 ] ) , values [ ' name ' ] ) ,
values [ ' analytic_account_id ' ] ,
values [ ' debit ' ] > 0 )
elif data_type == ' tax ' :
return ( ' tax ' ,
values [ ' partner_id ' ] ,
values [ ' tax_line_id ' ] ,
values [ ' debit ' ] > 0 )
elif data_type == ' counter_part ' :
return ( ' counter_part ' ,
values [ ' partner_id ' ] ,
values [ ' account_id ' ] ,
values [ ' debit ' ] > 0 )
return False
def _action_create_invoice_line ( self , line = False , invoice_id = False ) :
InvoiceLine = self . env [ ' account.invoice.line ' ]
inv_name = line . product_id . name_get ( ) [ 0 ] [ 1 ]
inv_line = {
' invoice_id ' : invoice_id ,
' product_id ' : line . product_id . id ,
' quantity ' : line . qty ,
' account_analytic_id ' : self . _prepare_analytic_account ( line ) ,
' name ' : inv_name ,
}
# Oldlin trick
invoice_line = InvoiceLine . sudo ( ) . new ( inv_line )
invoice_line . _onchange_product_id ( )
invoice_line . invoice_line_tax_ids = invoice_line . invoice_line_tax_ids . filtered ( lambda t : t . company_id . id == line . order_id . company_id . id ) . ids
fiscal_position_id = line . order_id . fiscal_position_id
if fiscal_position_id :
invoice_line . invoice_line_tax_ids = fiscal_position_id . map_tax ( invoice_line . invoice_line_tax_ids , line . product_id , line . order_id . partner_id )
invoice_line . invoice_line_tax_ids = invoice_line . invoice_line_tax_ids . ids
# We convert a new id object back to a dictionary to write to
# bridge between old and new api
inv_line = invoice_line . _convert_to_write ( { name : invoice_line [ name ] for name in invoice_line . _cache } )
inv_line . update ( price_unit = line . price_unit , discount = line . discount , name = inv_name )
return InvoiceLine . sudo ( ) . create ( inv_line )
def _create_account_move_line ( self , session = None , move = None ) :
# Tricky, via the workflow, we only have one id in the ids variable
""" Create a account move line of order grouped by products or not. """
IrProperty = self . env [ ' ir.property ' ]
ResPartner = self . env [ ' res.partner ' ]
if session and not all ( session . id == order . session_id . id for order in self ) :
raise UserError ( _ ( ' Selected orders do not have the same session! ' ) )
grouped_data = { }
have_to_group_by = session and session . config_id . group_by or False
rounding_method = session and session . config_id . company_id . tax_calculation_rounding_method
for order in self . filtered ( lambda o : not o . account_move or o . state == ' paid ' ) :
current_company = order . sale_journal . company_id
account_def = IrProperty . get (
' property_account_receivable_id ' , ' res.partner ' )
order_account = order . partner_id . property_account_receivable_id . id or account_def and account_def . id
partner_id = ResPartner . _find_accounting_partner ( order . partner_id ) . id or False
if move is None :
# Create an entry for the sale
journal_id = self . env [ ' ir.config_parameter ' ] . sudo ( ) . get_param (
' pos.closing.journal_id_ %s ' % current_company . id , default = order . sale_journal . id )
move = self . _create_account_move (
order . session_id . start_at , order . name , int ( journal_id ) , order . company_id . id )
def insert_data ( data_type , values ) :
# if have_to_group_by:
values . update ( {
' partner_id ' : partner_id ,
' move_id ' : move . id ,
} )
key = self . _get_account_move_line_group_data_type_key ( data_type , values )
if not key :
return
grouped_data . setdefault ( key , [ ] )
if have_to_group_by :
if not grouped_data [ key ] :
grouped_data [ key ] . append ( values )
else :
current_value = grouped_data [ key ] [ 0 ]
current_value [ ' quantity ' ] = current_value . get ( ' quantity ' , 0.0 ) + values . get ( ' quantity ' , 0.0 )
current_value [ ' credit ' ] = current_value . get ( ' credit ' , 0.0 ) + values . get ( ' credit ' , 0.0 )
current_value [ ' debit ' ] = current_value . get ( ' debit ' , 0.0 ) + values . get ( ' debit ' , 0.0 )
else :
grouped_data [ key ] . append ( values )
# because of the weird way the pos order is written, we need to make sure there is at least one line,
# because just after the 'for' loop there are references to 'line' and 'income_account' variables (that
# are set inside the for loop)
# TOFIX: a deep refactoring of this method (and class!) is needed
# in order to get rid of this stupid hack
assert order . lines , _ ( ' The POS order must have lines when calling this method ' )
# Create an move for each order line
cur = order . pricelist_id . currency_id
for line in order . lines :
amount = line . price_subtotal
# Search for the income account
if line . product_id . property_account_income_id . id :
income_account = line . product_id . property_account_income_id . id
elif line . product_id . categ_id . property_account_income_categ_id . id :
income_account = line . product_id . categ_id . property_account_income_categ_id . id
else :
raise UserError ( _ ( ' Please define income '
' account for this product: " %s " (id: %d ). ' )
% ( line . product_id . name , line . product_id . id ) )
name = line . product_id . name
if line . notice :
# add discount reason in move
name = name + ' ( ' + line . notice + ' ) '
# Create a move for the line for the order line
insert_data ( ' product ' , {
' name ' : name ,
' quantity ' : line . qty ,
' product_id ' : line . product_id . id ,
' account_id ' : income_account ,
' analytic_account_id ' : self . _prepare_analytic_account ( line ) ,
' credit ' : ( ( amount > 0 ) and amount ) or 0.0 ,
' debit ' : ( ( amount < 0 ) and - amount ) or 0.0 ,
' tax_ids ' : [ ( 6 , 0 , line . tax_ids_after_fiscal_position . ids ) ] ,
' partner_id ' : partner_id
} )
# Create the tax lines
taxes = line . tax_ids_after_fiscal_position . filtered ( lambda t : t . company_id . id == current_company . id )
if not taxes :
continue
price = line . price_unit * ( 1 - ( line . discount or 0.0 ) / 100.0 )
for tax in taxes . compute_all ( price , cur , line . qty ) [ ' taxes ' ] :
insert_data ( ' tax ' , {
' name ' : _ ( ' Tax ' ) + ' ' + tax [ ' name ' ] ,
' product_id ' : line . product_id . id ,
' quantity ' : line . qty ,
' account_id ' : tax [ ' account_id ' ] or income_account ,
' credit ' : ( ( tax [ ' amount ' ] > 0 ) and tax [ ' amount ' ] ) or 0.0 ,
' debit ' : ( ( tax [ ' amount ' ] < 0 ) and - tax [ ' amount ' ] ) or 0.0 ,
' tax_line_id ' : tax [ ' id ' ] ,
' partner_id ' : partner_id
} )
# round tax lines per order
if rounding_method == ' round_globally ' :
for group_key , group_value in grouped_data . items ( ) :
if group_key [ 0 ] == ' tax ' :
for line in group_value :
line [ ' credit ' ] = cur . round ( line [ ' credit ' ] )
line [ ' debit ' ] = cur . round ( line [ ' debit ' ] )
# counterpart
insert_data ( ' counter_part ' , {
' name ' : _ ( " Trade Receivables " ) , # order.name,
' account_id ' : order_account ,
' credit ' : ( ( order . amount_total < 0 ) and - order . amount_total ) or 0.0 ,
' debit ' : ( ( order . amount_total > 0 ) and order . amount_total ) or 0.0 ,
' partner_id ' : partner_id
} )
order . write ( { ' state ' : ' done ' , ' account_move ' : move . id } )
all_lines = [ ]
for group_key , group_data in grouped_data . items ( ) :
for value in group_data :
all_lines . append ( ( 0 , 0 , value ) , )
if move : # In case no order was changed
move . sudo ( ) . write ( { ' line_ids ' : all_lines } )
move . sudo ( ) . post ( )
return True
def _reconcile_payments ( self ) :
for order in self :
aml = order . statement_ids . mapped ( ' journal_entry_ids ' ) | order . account_move . line_ids | order . invoice_id . move_id . line_ids
aml = aml . filtered ( lambda r : not r . reconciled and r . account_id . internal_type == ' receivable ' and r . partner_id == order . partner_id . commercial_partner_id )
try :
aml . reconcile ( )
except Exception :
# There might be unexpected situations where the automatic reconciliation won't
# work. We don't want the user to be blocked because of this, since the automatic
# reconciliation is introduced for convenience, not for mandatory accounting
# reasons.
_logger . error ( ' Reconciliation did not work for order %s ' , order . name )
continue
def _default_session ( self ) :
return self . env [ ' pos.session ' ] . search ( [ ( ' state ' , ' = ' , ' opened ' ) , ( ' user_id ' , ' = ' , self . env . uid ) ] , limit = 1 )
def _default_pricelist ( self ) :
return self . _default_session ( ) . config_id . pricelist_id
name = fields . Char ( string = ' Order Ref ' , required = True , readonly = True , copy = False , default = ' / ' )
company_id = fields . Many2one ( ' res.company ' , string = ' Company ' , required = True , readonly = True , default = lambda self : self . env . user . company_id )
date_order = fields . Datetime ( string = ' Order Date ' , readonly = True , index = True , default = fields . Datetime . now )
user_id = fields . Many2one (
comodel_name = ' res.users ' , string = ' Salesman ' ,
help = " Person who uses the cash register. It can be a reliever, a student or an interim employee. " ,
default = lambda self : self . env . uid ,
states = { ' done ' : [ ( ' readonly ' , True ) ] , ' invoiced ' : [ ( ' readonly ' , True ) ] } ,
)
amount_tax = fields . Float ( compute = ' _compute_amount_all ' , string = ' Taxes ' , digits = 0 )
amount_total = fields . Float ( compute = ' _compute_amount_all ' , string = ' Total ' , digits = 0 )
amount_paid = fields . Float ( compute = ' _compute_amount_all ' , string = ' Paid ' , states = { ' draft ' : [ ( ' readonly ' , False ) ] } , readonly = True , digits = 0 )
amount_return = fields . Float ( compute = ' _compute_amount_all ' , string = ' Returned ' , digits = 0 )
lines = fields . One2many ( ' pos.order.line ' , ' order_id ' , string = ' Order Lines ' , states = { ' draft ' : [ ( ' readonly ' , False ) ] } , readonly = True , copy = True )
statement_ids = fields . One2many ( ' account.bank.statement.line ' , ' pos_statement_id ' , string = ' Payments ' , states = { ' draft ' : [ ( ' readonly ' , False ) ] } , readonly = True )
pricelist_id = fields . Many2one ( ' product.pricelist ' , string = ' Pricelist ' , required = True , states = {
' draft ' : [ ( ' readonly ' , False ) ] } , readonly = True , default = _default_pricelist )
partner_id = fields . Many2one ( ' res.partner ' , string = ' Customer ' , change_default = True , index = True , states = { ' draft ' : [ ( ' readonly ' , False ) ] , ' paid ' : [ ( ' readonly ' , False ) ] } )
sequence_number = fields . Integer ( string = ' Sequence Number ' , help = ' A session-unique sequence number for the order ' , default = 1 )
session_id = fields . Many2one (
' pos.session ' , string = ' Session ' , required = True , index = True ,
domain = " [( ' state ' , ' = ' , ' opened ' )] " , states = { ' draft ' : [ ( ' readonly ' , False ) ] } ,
readonly = True , default = _default_session )
config_id = fields . Many2one ( ' pos.config ' , related = ' session_id.config_id ' , string = " Point of Sale " )
state = fields . Selection (
[ ( ' draft ' , ' New ' ) , ( ' cancel ' , ' Cancelled ' ) , ( ' paid ' , ' Paid ' ) , ( ' done ' , ' Posted ' ) , ( ' invoiced ' , ' Invoiced ' ) ] ,
' Status ' , readonly = True , copy = False , default = ' draft ' )
invoice_id = fields . Many2one ( ' account.invoice ' , string = ' Invoice ' , copy = False )
account_move = fields . Many2one ( ' account.move ' , string = ' Journal Entry ' , readonly = True , copy = False )
picking_id = fields . Many2one ( ' stock.picking ' , string = ' Picking ' , readonly = True , copy = False )
picking_type_id = fields . Many2one ( ' stock.picking.type ' , related = ' session_id.config_id.picking_type_id ' , string = " Operation Type " )
location_id = fields . Many2one (
comodel_name = ' stock.location ' ,
related = ' session_id.config_id.stock_location_id ' ,
string = " Location " , store = True ,
readonly = True ,
)
note = fields . Text ( string = ' Internal Notes ' )
nb_print = fields . Integer ( string = ' Number of Print ' , readonly = True , copy = False , default = 0 )
pos_reference = fields . Char ( string = ' Receipt Ref ' , readonly = True , copy = False )
sale_journal = fields . Many2one ( ' account.journal ' , related = ' session_id.config_id.journal_id ' , string = ' Sales Journal ' , store = True , readonly = True )
fiscal_position_id = fields . Many2one (
comodel_name = ' account.fiscal.position ' , string = ' Fiscal Position ' ,
default = lambda self : self . _default_session ( ) . config_id . default_fiscal_position_id ,
readonly = True ,
states = { ' draft ' : [ ( ' readonly ' , False ) ] } ,
)
@api.depends ( ' statement_ids ' , ' lines.price_subtotal_incl ' , ' lines.discount ' )
def _compute_amount_all ( self ) :
for order in self :
order . amount_paid = order . amount_return = order . amount_tax = 0.0
currency = order . pricelist_id . currency_id
order . amount_paid = sum ( payment . amount for payment in order . statement_ids )
order . amount_return = sum ( payment . amount < 0 and payment . amount or 0 for payment in order . statement_ids )
order . amount_tax = currency . round ( sum ( self . _amount_line_tax ( line , order . fiscal_position_id ) for line in order . lines ) )
amount_untaxed = currency . round ( sum ( line . price_subtotal for line in order . lines ) )
order . amount_total = order . amount_tax + amount_untaxed
@api.onchange ( ' partner_id ' )
def _onchange_partner_id ( self ) :
if self . partner_id :
self . pricelist = self . partner_id . property_product_pricelist . id
@api.multi
def write ( self , vals ) :
res = super ( PosOrder , self ) . write ( vals )
Partner = self . env [ ' res.partner ' ]
# If you change the partner of the PoS order, change also the partner of the associated bank statement lines
if ' partner_id ' in vals :
for order in self :
partner_id = False
if order . invoice_id :
raise UserError ( _ ( " You cannot change the partner of a POS order for which an invoice has already been issued. " ) )
if vals [ ' partner_id ' ] :
partner = Partner . browse ( vals [ ' partner_id ' ] )
partner_id = Partner . _find_accounting_partner ( partner ) . id
order . statement_ids . write ( { ' partner_id ' : partner_id } )
return res
@api.multi
def unlink ( self ) :
for pos_order in self . filtered ( lambda pos_order : pos_order . state not in [ ' draft ' , ' cancel ' ] ) :
raise UserError ( _ ( ' In order to delete a sale, it must be new or cancelled. ' ) )
return super ( PosOrder , self ) . unlink ( )
@api.model
def create ( self , values ) :
if values . get ( ' session_id ' ) :
# set name based on the sequence specified on the config
session = self . env [ ' pos.session ' ] . browse ( values [ ' session_id ' ] )
values [ ' name ' ] = session . config_id . sequence_id . _next ( )
values . setdefault ( ' pricelist_id ' , session . config_id . pricelist_id . id )
else :
# fallback on any pos.order sequence
values [ ' name ' ] = self . env [ ' ir.sequence ' ] . next_by_code ( ' pos.order ' )
return super ( PosOrder , self ) . create ( values )
@api.multi
def action_view_invoice ( self ) :
return {
' name ' : _ ( ' Customer Invoice ' ) ,
' view_mode ' : ' form ' ,
' view_id ' : self . env . ref ( ' account.invoice_form ' ) . id ,
' res_model ' : ' account.invoice ' ,
' context ' : " { ' type ' : ' out_invoice ' } " ,
' type ' : ' ir.actions.act_window ' ,
' res_id ' : self . invoice_id . id ,
}
@api.multi
def action_pos_order_paid ( self ) :
if not self . test_paid ( ) :
raise UserError ( _ ( " Order is not paid. " ) )
self . write ( { ' state ' : ' paid ' } )
return self . create_picking ( )
@api.multi
def action_pos_order_invoice ( self ) :
Invoice = self . env [ ' account.invoice ' ]
for order in self :
# Force company for all SUPERUSER_ID action
local_context = dict ( self . env . context , force_company = order . company_id . id , company_id = order . company_id . id )
if order . invoice_id :
Invoice + = order . invoice_id
continue
if not order . partner_id :
raise UserError ( _ ( ' Please provide a partner for the sale. ' ) )
invoice = Invoice . new ( order . _prepare_invoice ( ) )
invoice . _onchange_partner_id ( )
invoice . fiscal_position_id = order . fiscal_position_id
inv = invoice . _convert_to_write ( { name : invoice [ name ] for name in invoice . _cache } )
new_invoice = Invoice . with_context ( local_context ) . sudo ( ) . create ( inv )
message = _ ( " This invoice has been created from the point of sale session: <a href=# data-oe-model=pos.order data-oe-id= %d > %s </a> " ) % ( order . id , order . name )
new_invoice . message_post ( body = message )
order . write ( { ' invoice_id ' : new_invoice . id , ' state ' : ' invoiced ' } )
Invoice + = new_invoice
for line in order . lines :
self . with_context ( local_context ) . _action_create_invoice_line ( line , new_invoice . id )
new_invoice . with_context ( local_context ) . sudo ( ) . compute_taxes ( )
order . sudo ( ) . write ( { ' state ' : ' invoiced ' } )
if not Invoice :
return { }
return {
' name ' : _ ( ' Customer Invoice ' ) ,
' view_type ' : ' form ' ,
' view_mode ' : ' form ' ,
' view_id ' : self . env . ref ( ' account.invoice_form ' ) . id ,
' res_model ' : ' account.invoice ' ,
' context ' : " { ' type ' : ' out_invoice ' } " ,
' type ' : ' ir.actions.act_window ' ,
' nodestroy ' : True ,
' target ' : ' current ' ,
' res_id ' : Invoice and Invoice . ids [ 0 ] or False ,
}
# this method is unused, and so is the state 'cancel'
@api.multi
def action_pos_order_cancel ( self ) :
return self . write ( { ' state ' : ' cancel ' } )
@api.multi
def action_pos_order_done ( self ) :
return self . _create_account_move_line ( )
@api.model
def create_from_ui ( self , orders ) :
# Keep only new orders
submitted_references = [ o [ ' data ' ] [ ' name ' ] for o in orders ]
pos_order = self . search ( [ ( ' pos_reference ' , ' in ' , submitted_references ) ] )
existing_orders = pos_order . read ( [ ' pos_reference ' ] )
existing_references = set ( [ o [ ' pos_reference ' ] for o in existing_orders ] )
orders_to_save = [ o for o in orders if o [ ' data ' ] [ ' name ' ] not in existing_references ]
order_ids = [ ]
for tmp_order in orders_to_save :
to_invoice = tmp_order [ ' to_invoice ' ]
order = tmp_order [ ' data ' ]
if to_invoice :
self . _match_payment_to_invoice ( order )
pos_order = self . _process_order ( order )
order_ids . append ( pos_order . id )
try :
pos_order . action_pos_order_paid ( )
except psycopg2 . OperationalError :
# do not hide transactional errors, the order(s) won't be saved!
raise
except Exception as e :
_logger . error ( ' Could not fully process the POS Order: %s ' , tools . ustr ( e ) )
if to_invoice :
pos_order . action_pos_order_invoice ( )
pos_order . invoice_id . sudo ( ) . action_invoice_open ( )
pos_order . account_move = pos_order . invoice_id . move_id
return order_ids
def test_paid ( self ) :
""" A Point of Sale is paid when the sum
@return : True
"""
for order in self :
if order . lines and not order . amount_total :
continue
if ( not order . lines ) or ( not order . statement_ids ) or ( abs ( order . amount_total - order . amount_paid ) > 0.00001 ) :
return False
return True
def create_picking ( self ) :
""" Create a picking for each order and validate it. """
Picking = self . env [ ' stock.picking ' ]
Move = self . env [ ' stock.move ' ]
StockWarehouse = self . env [ ' stock.warehouse ' ]
for order in self :
if not order . lines . filtered ( lambda l : l . product_id . type in [ ' product ' , ' consu ' ] ) :
continue
address = order . partner_id . address_get ( [ ' delivery ' ] ) or { }
picking_type = order . picking_type_id
return_pick_type = order . picking_type_id . return_picking_type_id or order . picking_type_id
order_picking = Picking
return_picking = Picking
moves = Move
location_id = order . location_id . id
if order . partner_id :
destination_id = order . partner_id . property_stock_customer . id
else :
if ( not picking_type ) or ( not picking_type . default_location_dest_id ) :
customerloc , supplierloc = StockWarehouse . _get_partner_locations ( )
destination_id = customerloc . id
else :
destination_id = picking_type . default_location_dest_id . id
if picking_type :
message = _ ( " This transfer has been created from the point of sale session: <a href=# data-oe-model=pos.order data-oe-id= %d > %s </a> " ) % ( order . id , order . name )
picking_vals = {
' origin ' : order . name ,
' partner_id ' : address . get ( ' delivery ' , False ) ,
' date_done ' : order . date_order ,
' picking_type_id ' : picking_type . id ,
' company_id ' : order . company_id . id ,
' move_type ' : ' direct ' ,
' note ' : order . note or " " ,
' location_id ' : location_id ,
' location_dest_id ' : destination_id ,
}
pos_qty = any ( [ x . qty > 0 for x in order . lines if x . product_id . type in [ ' product ' , ' consu ' ] ] )
if pos_qty :
order_picking = Picking . create ( picking_vals . copy ( ) )
order_picking . message_post ( body = message )
neg_qty = any ( [ x . qty < 0 for x in order . lines if x . product_id . type in [ ' product ' , ' consu ' ] ] )
if neg_qty :
return_vals = picking_vals . copy ( )
return_vals . update ( {
' location_id ' : destination_id ,
' location_dest_id ' : return_pick_type != picking_type and return_pick_type . default_location_dest_id . id or location_id ,
' picking_type_id ' : return_pick_type . id
} )
return_picking = Picking . create ( return_vals )
return_picking . message_post ( body = message )
for line in order . lines . filtered ( lambda l : l . product_id . type in [ ' product ' , ' consu ' ] and not float_is_zero ( l . qty , precision_rounding = l . product_id . uom_id . rounding ) ) :
moves | = Move . create ( {
' name ' : line . name ,
' product_uom ' : line . product_id . uom_id . id ,
' picking_id ' : order_picking . id if line . qty > = 0 else return_picking . id ,
' picking_type_id ' : picking_type . id if line . qty > = 0 else return_pick_type . id ,
' product_id ' : line . product_id . id ,
' product_uom_qty ' : abs ( line . qty ) ,
' state ' : ' draft ' ,
' location_id ' : location_id if line . qty > = 0 else destination_id ,
' location_dest_id ' : destination_id if line . qty > = 0 else return_pick_type != picking_type and return_pick_type . default_location_dest_id . id or location_id ,
} )
# prefer associating the regular order picking, not the return
order . write ( { ' picking_id ' : order_picking . id or return_picking . id } )
if return_picking :
order . _force_picking_done ( return_picking )
if order_picking :
order . _force_picking_done ( order_picking )
# when the pos.config has no picking_type_id set only the moves will be created
if moves and not return_picking and not order_picking :
moves . _action_assign ( )
moves . filtered ( lambda m : m . state in [ ' confirmed ' , ' waiting ' ] ) . force_assign ( )
moves . filtered ( lambda m : m . product_id . tracking == ' none ' ) . _action_done ( )
return True
def _force_picking_done ( self , picking ) :
""" Force picking in order to be set as done. """
self . ensure_one ( )
picking . action_assign ( )
picking . force_assign ( )
wrong_lots = self . set_pack_operation_lot ( picking )
if not wrong_lots :
picking . action_done ( )
def set_pack_operation_lot ( self , picking = None ) :
""" Set Serial/Lot number in pack operations to mark the pack operation done. """
StockProductionLot = self . env [ ' stock.production.lot ' ]
PosPackOperationLot = self . env [ ' pos.pack.operation.lot ' ]
has_wrong_lots = False
for order in self :
for move in ( picking or self . picking_id ) . move_lines :
picking_type = ( picking or self . picking_id ) . picking_type_id
lots_necessary = True
if picking_type :
lots_necessary = picking_type and picking_type . use_existing_lots
qty = 0
qty_done = 0
pack_lots = [ ]
pos_pack_lots = PosPackOperationLot . search ( [ ( ' order_id ' , ' = ' , order . id ) , ( ' product_id ' , ' = ' , move . product_id . id ) ] )
pack_lot_names = [ pos_pack . lot_name for pos_pack in pos_pack_lots ]
if pack_lot_names and lots_necessary :
for lot_name in list ( set ( pack_lot_names ) ) :
stock_production_lot = StockProductionLot . search ( [ ( ' name ' , ' = ' , lot_name ) , ( ' product_id ' , ' = ' , move . product_id . id ) ] )
if stock_production_lot :
if stock_production_lot . product_id . tracking == ' lot ' :
# if a lot nr is set through the frontend it will refer to the full quantity
qty = move . product_uom_qty
else : # serial numbers
qty = 1.0
qty_done + = qty
pack_lots . append ( { ' lot_id ' : stock_production_lot . id , ' qty ' : qty } )
else :
has_wrong_lots = True
elif move . product_id . tracking == ' none ' or not lots_necessary :
qty_done = move . product_uom_qty
else :
has_wrong_lots = True
for pack_lot in pack_lots :
lot_id , qty = pack_lot [ ' lot_id ' ] , pack_lot [ ' qty ' ]
self . env [ ' stock.move.line ' ] . create ( {
' move_id ' : move . id ,
' product_id ' : move . product_id . id ,
' product_uom_id ' : move . product_uom . id ,
' qty_done ' : qty ,
' location_id ' : move . location_id . id ,
' location_dest_id ' : move . location_dest_id . id ,
' lot_id ' : lot_id ,
} )
if not pack_lots :
move . quantity_done = qty_done
return has_wrong_lots
def _prepare_bank_statement_line_payment_values ( self , data ) :
""" Create a new payment for the order """
args = {
' amount ' : data [ ' amount ' ] ,
' date ' : data . get ( ' payment_date ' , fields . Date . today ( ) ) ,
' name ' : self . name + ' : ' + ( data . get ( ' payment_name ' , ' ' ) or ' ' ) ,
' partner_id ' : self . env [ " res.partner " ] . _find_accounting_partner ( self . partner_id ) . id or False ,
}
journal_id = data . get ( ' journal ' , False )
statement_id = data . get ( ' statement_id ' , False )
assert journal_id or statement_id , " No statement_id or journal_id passed to the method! "
journal = self . env [ ' account.journal ' ] . browse ( journal_id )
# use the company of the journal and not of the current user
company_cxt = dict ( self . env . context , force_company = journal . company_id . id )
account_def = self . env [ ' ir.property ' ] . with_context ( company_cxt ) . get ( ' property_account_receivable_id ' , ' res.partner ' )
args [ ' account_id ' ] = ( self . partner_id . property_account_receivable_id . id ) or ( account_def and account_def . id ) or False
if not args [ ' account_id ' ] :
if not args [ ' partner_id ' ] :
msg = _ ( ' There is no receivable account defined to make payment. ' )
else :
msg = _ ( ' There is no receivable account defined to make payment for the partner: " %s " (id: %d ). ' ) % (
self . partner_id . name , self . partner_id . id , )
raise UserError ( msg )
context = dict ( self . env . context )
context . pop ( ' pos_session_id ' , False )
for statement in self . session_id . statement_ids :
if statement . id == statement_id :
journal_id = statement . journal_id . id
break
elif statement . journal_id . id == journal_id :
statement_id = statement . id
break
if not statement_id :
raise UserError ( _ ( ' You have to open at least one cashbox. ' ) )
args . update ( {
' statement_id ' : statement_id ,
' pos_statement_id ' : self . id ,
' journal_id ' : journal_id ,
' ref ' : self . session_id . name ,
} )
return args
def add_payment ( self , data ) :
""" Create a new payment for the order """
args = self . _prepare_bank_statement_line_payment_values ( data )
context = dict ( self . env . context )
context . pop ( ' pos_session_id ' , False )
self . env [ ' account.bank.statement.line ' ] . with_context ( context ) . create ( args )
return args . get ( ' statement_id ' , False )
@api.multi
def refund ( self ) :
""" Create a copy of order for refund order """
PosOrder = self . env [ ' pos.order ' ]
current_session = self . env [ ' pos.session ' ] . search ( [ ( ' state ' , ' != ' , ' closed ' ) , ( ' user_id ' , ' = ' , self . env . uid ) ] , limit = 1 )
if not current_session :
raise UserError ( _ ( ' To return product(s), you need to open a session that will be used to register the refund. ' ) )
for order in self :
clone = order . copy ( {
# ot used, name forced by create
' name ' : order . name + _ ( ' REFUND ' ) ,
' session_id ' : current_session . id ,
' date_order ' : fields . Datetime . now ( ) ,
' pos_reference ' : order . pos_reference ,
' lines ' : False ,
} )
for line in order . lines :
clone_line = line . copy ( {
# required=True, copy=False
' name ' : line . name + _ ( ' REFUND ' ) ,
' order_id ' : clone . id ,
' qty ' : - line . qty ,
} )
PosOrder + = clone
return {
' name ' : _ ( ' Return Products ' ) ,
' view_type ' : ' form ' ,
' view_mode ' : ' form ' ,
' res_model ' : ' pos.order ' ,
' res_id ' : PosOrder . ids [ 0 ] ,
' view_id ' : False ,
' context ' : self . env . context ,
' type ' : ' ir.actions.act_window ' ,
' target ' : ' current ' ,
}
class PosOrderLine ( models . Model ) :
_name = " pos.order.line "
_description = " Lines of Point of Sale Orders "
_rec_name = " product_id "
def _order_line_fields ( self , line , session_id = None ) :
if line and ' name ' not in line [ 2 ] :
session = self . env [ ' pos.session ' ] . browse ( session_id ) . exists ( ) if session_id else None
if session and session . config_id . sequence_line_id :
# set name based on the sequence specified on the config
line [ 2 ] [ ' name ' ] = session . config_id . sequence_line_id . _next ( )
else :
# fallback on any pos.order.line sequence
line [ 2 ] [ ' name ' ] = self . env [ ' ir.sequence ' ] . next_by_code ( ' pos.order.line ' )
if line and ' tax_ids ' not in line [ 2 ] :
product = self . env [ ' product.product ' ] . browse ( line [ 2 ] [ ' product_id ' ] )
line [ 2 ] [ ' tax_ids ' ] = [ ( 6 , 0 , [ x . id for x in product . taxes_id ] ) ]
return line
company_id = fields . Many2one ( ' res.company ' , string = ' Company ' , required = True , default = lambda self : self . env . user . company_id )
name = fields . Char ( string = ' Line No ' , required = True , copy = False )
notice = fields . Char ( string = ' Discount Notice ' )
product_id = fields . Many2one ( ' product.product ' , string = ' Product ' , domain = [ ( ' sale_ok ' , ' = ' , True ) ] , required = True , change_default = True )
price_unit = fields . Float ( string = ' Unit Price ' , digits = 0 )
qty = fields . Float ( ' Quantity ' , digits = dp . get_precision ( ' Product Unit of Measure ' ) , default = 1 )
price_subtotal = fields . Float ( compute = ' _compute_amount_line_all ' , digits = 0 , string = ' Subtotal w/o Tax ' )
price_subtotal_incl = fields . Float ( compute = ' _compute_amount_line_all ' , digits = 0 , string = ' Subtotal ' )
discount = fields . Float ( string = ' Discount ( % ) ' , digits = 0 , default = 0.0 )
order_id = fields . Many2one ( ' pos.order ' , string = ' Order Ref ' , ondelete = ' cascade ' )
create_date = fields . Datetime ( string = ' Creation Date ' , readonly = True )
tax_ids = fields . Many2many ( ' account.tax ' , string = ' Taxes ' , readonly = True )
tax_ids_after_fiscal_position = fields . Many2many ( ' account.tax ' , compute = ' _get_tax_ids_after_fiscal_position ' , string = ' Taxes ' )
pack_lot_ids = fields . One2many ( ' pos.pack.operation.lot ' , ' pos_order_line_id ' , string = ' Lot/serial Number ' )
@api.model
def create ( self , values ) :
if values . get ( ' order_id ' ) and not values . get ( ' name ' ) :
# set name based on the sequence specified on the config
config_id = self . order_id . browse ( values [ ' order_id ' ] ) . session_id . config_id . id
# HACK: sequence created in the same transaction as the config
# cf TODO master is pos.config create
# remove me saas-15
self . env . cr . execute ( """
SELECT s . id
FROM ir_sequence s
JOIN pos_config c
ON s . create_date = c . create_date
WHERE c . id = % s
AND s . code = ' pos.order.line '
LIMIT 1
""" , (config_id,))
sequence = self . env . cr . fetchone ( )
if sequence :
values [ ' name ' ] = self . env [ ' ir.sequence ' ] . browse ( sequence [ 0 ] ) . _next ( )
if not values . get ( ' name ' ) :
# fallback on any pos.order sequence
values [ ' name ' ] = self . env [ ' ir.sequence ' ] . next_by_code ( ' pos.order.line ' )
return super ( PosOrderLine , self ) . create ( values )
@api.depends ( ' price_unit ' , ' tax_ids ' , ' qty ' , ' discount ' , ' product_id ' )
def _compute_amount_line_all ( self ) :
for line in self :
fpos = line . order_id . fiscal_position_id
tax_ids_after_fiscal_position = fpos . map_tax ( line . tax_ids , line . product_id , line . order_id . partner_id ) if fpos else line . tax_ids
price = line . price_unit * ( 1 - ( line . discount or 0.0 ) / 100.0 )
taxes = tax_ids_after_fiscal_position . compute_all ( price , line . order_id . pricelist_id . currency_id , line . qty , product = line . product_id , partner = line . order_id . partner_id )
line . update ( {
' price_subtotal_incl ' : taxes [ ' total_included ' ] ,
' price_subtotal ' : taxes [ ' total_excluded ' ] ,
} )
@api.onchange ( ' product_id ' )
def _onchange_product_id ( self ) :
if self . product_id :
if not self . order_id . pricelist_id :
raise UserError (
_ ( ' You have to select a pricelist in the sale form ! \n '
' Please set one before choosing a product. ' ) )
price = self . order_id . pricelist_id . get_product_price (
self . product_id , self . qty or 1.0 , self . order_id . partner_id )
self . _onchange_qty ( )
self . tax_ids = self . product_id . taxes_id . filtered ( lambda r : not self . company_id or r . company_id == self . company_id )
fpos = self . order_id . fiscal_position_id
tax_ids_after_fiscal_position = fpos . map_tax ( self . tax_ids , self . product_id , self . order_id . partner_id ) if fpos else self . tax_ids
self . price_unit = self . env [ ' account.tax ' ] . _fix_tax_included_price_company ( price , self . product_id . taxes_id , tax_ids_after_fiscal_position , self . company_id )
@api.onchange ( ' qty ' , ' discount ' , ' price_unit ' , ' tax_ids ' )
def _onchange_qty ( self ) :
if self . product_id :
if not self . order_id . pricelist_id :
raise UserError ( _ ( ' You have to select a pricelist in the sale form ! ' ) )
price = self . price_unit * ( 1 - ( self . discount or 0.0 ) / 100.0 )
self . price_subtotal = self . price_subtotal_incl = price * self . qty
if ( self . product_id . taxes_id ) :
taxes = self . product_id . taxes_id . compute_all ( price , self . order_id . pricelist_id . currency_id , self . qty , product = self . product_id , partner = False )
self . price_subtotal = taxes [ ' total_excluded ' ]
self . price_subtotal_incl = taxes [ ' total_included ' ]
@api.multi
def _get_tax_ids_after_fiscal_position ( self ) :
for line in self :
line . tax_ids_after_fiscal_position = line . order_id . fiscal_position_id . map_tax ( line . tax_ids , line . product_id , line . order_id . partner_id )
class PosOrderLineLot ( models . Model ) :
_name = " pos.pack.operation.lot "
_description = " Specify product lot/serial number in pos order line "
pos_order_line_id = fields . Many2one ( ' pos.order.line ' )
order_id = fields . Many2one ( ' pos.order ' , related = " pos_order_line_id.order_id " )
lot_name = fields . Char ( ' Lot Name ' )
product_id = fields . Many2one ( ' product.product ' , related = ' pos_order_line_id.product_id ' )
class ReportSaleDetails ( models . AbstractModel ) :
_name = ' report.point_of_sale.report_saledetails '
@api.model
def get_sale_details ( self , date_start = False , date_stop = False , configs = False ) :
""" Serialise the orders of the day information
params : date_start , date_stop string representing the datetime of order
"""
if not configs :
configs = self . env [ ' pos.config ' ] . search ( [ ] )
user_tz = pytz . timezone ( self . env . context . get ( ' tz ' ) or self . env . user . tz or ' UTC ' )
today = user_tz . localize ( fields . Datetime . from_string ( fields . Date . context_today ( self ) ) )
today = today . astimezone ( pytz . timezone ( ' UTC ' ) )
if date_start :
date_start = fields . Datetime . from_string ( date_start )
else :
# start by default today 00:00:00
date_start = today
if date_stop :
# set time to 23:59:59
date_stop = fields . Datetime . from_string ( date_stop )
else :
# stop by default today 23:59:59
date_stop = today + timedelta ( days = 1 , seconds = - 1 )
# avoid a date_stop smaller than date_start
date_stop = max ( date_stop , date_start )
date_start = fields . Datetime . to_string ( date_start )
date_stop = fields . Datetime . to_string ( date_stop )
orders = self . env [ ' pos.order ' ] . search ( [
( ' date_order ' , ' >= ' , date_start ) ,
( ' date_order ' , ' <= ' , date_stop ) ,
( ' state ' , ' in ' , [ ' paid ' , ' invoiced ' , ' done ' ] ) ,
( ' config_id ' , ' in ' , configs . ids ) ] )
user_currency = self . env . user . company_id . currency_id
total = 0.0
products_sold = { }
taxes = { }
for order in orders :
if user_currency != order . pricelist_id . currency_id :
total + = order . pricelist_id . currency_id . compute ( order . amount_total , user_currency )
else :
total + = order . amount_total
currency = order . session_id . currency_id
for line in order . lines :
key = ( line . product_id , line . price_unit , line . discount )
products_sold . setdefault ( key , 0.0 )
products_sold [ key ] + = line . qty
if line . tax_ids_after_fiscal_position :
line_taxes = line . tax_ids_after_fiscal_position . compute_all ( line . price_unit * ( 1 - ( line . discount or 0.0 ) / 100.0 ) , currency , line . qty , product = line . product_id , partner = line . order_id . partner_id or False )
for tax in line_taxes [ ' taxes ' ] :
taxes . setdefault ( tax [ ' id ' ] , { ' name ' : tax [ ' name ' ] , ' tax_amount ' : 0.0 , ' base_amount ' : 0.0 } )
taxes [ tax [ ' id ' ] ] [ ' tax_amount ' ] + = tax [ ' amount ' ]
taxes [ tax [ ' id ' ] ] [ ' base_amount ' ] + = line . price_subtotal
st_line_ids = self . env [ " account.bank.statement.line " ] . search ( [ ( ' pos_statement_id ' , ' in ' , orders . ids ) ] ) . ids
if st_line_ids :
self . env . cr . execute ( """
SELECT aj . name , sum ( amount ) total
FROM account_bank_statement_line AS absl ,
account_bank_statement AS abs ,
account_journal AS aj
WHERE absl . statement_id = abs . id
AND abs . journal_id = aj . id
AND absl . id IN % s
GROUP BY aj . name
""" , (tuple(st_line_ids),))
payments = self . env . cr . dictfetchall ( )
else :
payments = [ ]
return {
' currency_precision ' : user_currency . decimal_places ,
' total_paid ' : user_currency . round ( total ) ,
' payments ' : payments ,
' company_name ' : self . env . user . company_id . name ,
' taxes ' : list ( taxes . values ( ) ) ,
' products ' : sorted ( [ {
' product_id ' : product . id ,
' product_name ' : product . name ,
' code ' : product . default_code ,
' quantity ' : qty ,
' price_unit ' : price_unit ,
' discount ' : discount ,
' uom ' : product . uom_id . name
} for ( product , price_unit , discount ) , qty in products_sold . items ( ) ] , key = lambda l : l [ ' product_name ' ] )
}
@api.multi
def get_report_values ( self , docids , data = None ) :
data = dict ( data or { } )
configs = self . env [ ' pos.config ' ] . browse ( data [ ' config_ids ' ] )
data . update ( self . get_sale_details ( data [ ' date_start ' ] , data [ ' date_stop ' ] , configs ) )
return data