2018-01-16 06:58:15 +01:00
# -*- coding: utf-8 -*-
from ast import literal_eval
from operator import itemgetter
import time
2018-01-16 11:34:37 +01:00
from flectra import api , fields , models , _
from flectra . tools import DEFAULT_SERVER_DATETIME_FORMAT
from flectra . exceptions import ValidationError
from flectra . addons . base . res . res_partner import WARNING_MESSAGE , WARNING_HELP
2018-01-16 06:58:15 +01:00
class AccountFiscalPosition ( models . Model ) :
_name = ' account.fiscal.position '
2018-01-17 11:23:19 +01:00
_inherit = [ ' ir.branch.company.mixin ' ]
2018-01-16 06:58:15 +01:00
_description = ' Fiscal Position '
_order = ' sequence '
sequence = fields . Integer ( )
name = fields . Char ( string = ' Fiscal Position ' , required = True )
active = fields . Boolean ( default = True ,
help = " By unchecking the active field, you may hide a fiscal position without deleting it. " )
company_id = fields . Many2one ( ' res.company ' , string = ' Company ' )
account_ids = fields . One2many ( ' account.fiscal.position.account ' , ' position_id ' , string = ' Account Mapping ' , copy = True )
tax_ids = fields . One2many ( ' account.fiscal.position.tax ' , ' position_id ' , string = ' Tax Mapping ' , copy = True )
note = fields . Text ( ' Notes ' , translate = True , help = " Legal mentions that have to be printed on the invoices. " )
auto_apply = fields . Boolean ( string = ' Detect Automatically ' , help = " Apply automatically this fiscal position. " )
vat_required = fields . Boolean ( string = ' VAT required ' , help = " Apply only if partner has a VAT number. " )
country_id = fields . Many2one ( ' res.country ' , string = ' Country ' ,
help = " Apply only if delivery or invoicing country match. " )
country_group_id = fields . Many2one ( ' res.country.group ' , string = ' Country Group ' ,
help = " Apply only if delivery or invocing country match the group. " )
state_ids = fields . Many2many ( ' res.country.state ' , string = ' Federal States ' )
zip_from = fields . Integer ( string = ' Zip Range From ' , default = 0 )
zip_to = fields . Integer ( string = ' Zip Range To ' , default = 0 )
# To be used in hiding the 'Federal States' field('attrs' in view side) when selected 'Country' has 0 states.
states_count = fields . Integer ( compute = ' _compute_states_count ' )
@api.one
def _compute_states_count ( self ) :
self . states_count = len ( self . country_id . state_ids )
@api.one
@api.constrains ( ' zip_from ' , ' zip_to ' )
def _check_zip ( self ) :
if self . zip_from > self . zip_to :
raise ValidationError ( _ ( ' Invalid " Zip Range " , please configure it properly. ' ) )
return True
@api.model # noqa
def map_tax ( self , taxes , product = None , partner = None ) :
result = self . env [ ' account.tax ' ] . browse ( )
for tax in taxes :
tax_count = 0
for t in self . tax_ids :
if t . tax_src_id == tax :
tax_count + = 1
if t . tax_dest_id :
result | = t . tax_dest_id
if not tax_count :
result | = tax
return result
@api.model
def map_account ( self , account ) :
for pos in self . account_ids :
if pos . account_src_id == account :
return pos . account_dest_id
return account
@api.model
def map_accounts ( self , accounts ) :
""" Receive a dictionary having accounts in values and try to replace those accounts accordingly to the fiscal position.
"""
ref_dict = { }
for line in self . account_ids :
ref_dict [ line . account_src_id ] = line . account_dest_id
for key , acc in accounts . items ( ) :
if acc in ref_dict :
accounts [ key ] = ref_dict [ acc ]
return accounts
@api.onchange ( ' country_id ' )
def _onchange_country_id ( self ) :
if self . country_id :
self . zip_from = self . zip_to = self . country_group_id = False
self . state_ids = [ ( 5 , ) ]
self . states_count = len ( self . country_id . state_ids )
@api.onchange ( ' country_group_id ' )
def _onchange_country_group_id ( self ) :
if self . country_group_id :
self . zip_from = self . zip_to = self . country_id = False
self . state_ids = [ ( 5 , ) ]
@api.model
def _get_fpos_by_region ( self , country_id = False , state_id = False , zipcode = False , vat_required = False ) :
if not country_id :
return False
base_domain = [ ( ' auto_apply ' , ' = ' , True ) , ( ' vat_required ' , ' = ' , vat_required ) ]
if self . env . context . get ( ' force_company ' ) :
base_domain . append ( ( ' company_id ' , ' = ' , self . env . context . get ( ' force_company ' ) ) )
null_state_dom = state_domain = [ ( ' state_ids ' , ' = ' , False ) ]
null_zip_dom = zip_domain = [ ( ' zip_from ' , ' = ' , 0 ) , ( ' zip_to ' , ' = ' , 0 ) ]
null_country_dom = [ ( ' country_id ' , ' = ' , False ) , ( ' country_group_id ' , ' = ' , False ) ]
if zipcode and zipcode . isdigit ( ) :
zipcode = int ( zipcode )
zip_domain = [ ( ' zip_from ' , ' <= ' , zipcode ) , ( ' zip_to ' , ' >= ' , zipcode ) ]
else :
zipcode = 0
if state_id :
state_domain = [ ( ' state_ids ' , ' = ' , state_id ) ]
domain_country = base_domain + [ ( ' country_id ' , ' = ' , country_id ) ]
domain_group = base_domain + [ ( ' country_group_id.country_ids ' , ' = ' , country_id ) ]
# Build domain to search records with exact matching criteria
fpos = self . search ( domain_country + state_domain + zip_domain , limit = 1 )
# return records that fit the most the criteria, and fallback on less specific fiscal positions if any can be found
if not fpos and state_id :
fpos = self . search ( domain_country + null_state_dom + zip_domain , limit = 1 )
if not fpos and zipcode :
fpos = self . search ( domain_country + state_domain + null_zip_dom , limit = 1 )
if not fpos and state_id and zipcode :
fpos = self . search ( domain_country + null_state_dom + null_zip_dom , limit = 1 )
# fallback: country group with no state/zip range
if not fpos :
fpos = self . search ( domain_group + null_state_dom + null_zip_dom , limit = 1 )
if not fpos :
# Fallback on catchall (no country, no group)
fpos = self . search ( base_domain + null_country_dom , limit = 1 )
return fpos or False
@api.model
def get_fiscal_position ( self , partner_id , delivery_id = None ) :
if not partner_id :
return False
# This can be easily overriden to apply more complex fiscal rules
PartnerObj = self . env [ ' res.partner ' ]
partner = PartnerObj . browse ( partner_id )
# if no delivery use invoicing
if delivery_id :
delivery = PartnerObj . browse ( delivery_id )
else :
delivery = partner
# partner manually set fiscal position always win
if delivery . property_account_position_id or partner . property_account_position_id :
return delivery . property_account_position_id . id or partner . property_account_position_id . id
# First search only matching VAT positions
vat_required = bool ( partner . vat )
fp = self . _get_fpos_by_region ( delivery . country_id . id , delivery . state_id . id , delivery . zip , vat_required )
# Then if VAT required found no match, try positions that do not require it
if not fp and vat_required :
fp = self . _get_fpos_by_region ( delivery . country_id . id , delivery . state_id . id , delivery . zip , False )
return fp . id if fp else False
class AccountFiscalPositionTax ( models . Model ) :
_name = ' account.fiscal.position.tax '
2018-01-17 11:23:19 +01:00
_inherit = [ ' ir.branch.company.mixin ' ]
2018-01-16 06:58:15 +01:00
_description = ' Taxes Fiscal Position '
_rec_name = ' position_id '
position_id = fields . Many2one ( ' account.fiscal.position ' , string = ' Fiscal Position ' ,
required = True , ondelete = ' cascade ' )
tax_src_id = fields . Many2one ( ' account.tax ' , string = ' Tax on Product ' , required = True )
tax_dest_id = fields . Many2one ( ' account.tax ' , string = ' Tax to Apply ' )
_sql_constraints = [
( ' tax_src_dest_uniq ' ,
' unique (position_id,tax_src_id,tax_dest_id) ' ,
' A tax fiscal position could be defined only once time on same taxes. ' )
]
class AccountFiscalPositionAccount ( models . Model ) :
_name = ' account.fiscal.position.account '
_description = ' Accounts Fiscal Position '
2018-01-17 11:23:19 +01:00
_inherit = [ ' ir.branch.company.mixin ' ]
2018-01-16 06:58:15 +01:00
_rec_name = ' position_id '
position_id = fields . Many2one ( ' account.fiscal.position ' , string = ' Fiscal Position ' ,
required = True , ondelete = ' cascade ' )
account_src_id = fields . Many2one ( ' account.account ' , string = ' Account on Product ' ,
domain = [ ( ' deprecated ' , ' = ' , False ) ] , required = True )
account_dest_id = fields . Many2one ( ' account.account ' , string = ' Account to Use Instead ' ,
domain = [ ( ' deprecated ' , ' = ' , False ) ] , required = True )
_sql_constraints = [
( ' account_src_dest_uniq ' ,
' unique (position_id,account_src_id,account_dest_id) ' ,
' An account fiscal position could be defined only once time on same accounts. ' )
]
class ResPartner ( models . Model ) :
_name = ' res.partner '
_inherit = ' res.partner '
@api.multi
def _credit_debit_get ( self ) :
tables , where_clause , where_params = self . env [ ' account.move.line ' ] . _query_get ( )
where_params = [ tuple ( self . ids ) ] + where_params
if where_clause :
where_clause = ' AND ' + where_clause
self . _cr . execute ( """ SELECT account_move_line.partner_id, act.type, SUM(account_move_line.amount_residual)
FROM account_move_line
LEFT JOIN account_account a ON ( account_move_line . account_id = a . id )
LEFT JOIN account_account_type act ON ( a . user_type_id = act . id )
WHERE act . type IN ( ' receivable ' , ' payable ' )
AND account_move_line . partner_id IN % s
AND account_move_line . reconciled IS FALSE
""" + where_clause + """
GROUP BY account_move_line . partner_id , act . type
""" , where_params)
for pid , type , val in self . _cr . fetchall ( ) :
partner = self . browse ( pid )
if type == ' receivable ' :
partner . credit = val
elif type == ' payable ' :
partner . debit = - val
@api.multi
def _asset_difference_search ( self , account_type , operator , operand ) :
if operator not in ( ' < ' , ' = ' , ' > ' , ' >= ' , ' <= ' ) :
return [ ]
if type ( operand ) not in ( float , int ) :
return [ ]
sign = 1
if account_type == ' payable ' :
sign = - 1
res = self . _cr . execute ( '''
SELECT partner . id
FROM res_partner partner
LEFT JOIN account_move_line aml ON aml . partner_id = partner . id
RIGHT JOIN account_account acc ON aml . account_id = acc . id
WHERE acc . internal_type = % s
AND NOT acc . deprecated
GROUP BY partner . id
HAVING % s * COALESCE ( SUM ( aml . amount_residual ) , 0 ) ''' + operator + ''' % s ''' , (account_type, sign, operand))
res = self . _cr . fetchall ( )
if not res :
return [ ( ' id ' , ' = ' , ' 0 ' ) ]
return [ ( ' id ' , ' in ' , [ r [ 0 ] for r in res ] ) ]
@api.model
def _credit_search ( self , operator , operand ) :
return self . _asset_difference_search ( ' receivable ' , operator , operand )
@api.model
def _debit_search ( self , operator , operand ) :
return self . _asset_difference_search ( ' payable ' , operator , operand )
@api.multi
def _invoice_total ( self ) :
account_invoice_report = self . env [ ' account.invoice.report ' ]
if not self . ids :
self . total_invoiced = 0.0
return True
user_currency_id = self . env . user . company_id . currency_id . id
all_partners_and_children = { }
all_partner_ids = [ ]
for partner in self :
# price_total is in the company currency
all_partners_and_children [ partner ] = self . with_context ( active_test = False ) . search ( [ ( ' id ' , ' child_of ' , partner . id ) ] ) . ids
all_partner_ids + = all_partners_and_children [ partner ]
# searching account.invoice.report via the orm is comparatively expensive
# (generates queries "id in []" forcing to build the full table).
# In simple cases where all invoices are in the same currency than the user's company
# access directly these elements
# generate where clause to include multicompany rules
where_query = account_invoice_report . _where_calc ( [
( ' partner_id ' , ' in ' , all_partner_ids ) , ( ' state ' , ' not in ' , [ ' draft ' , ' cancel ' ] ) ,
( ' type ' , ' in ' , ( ' out_invoice ' , ' out_refund ' ) )
] )
account_invoice_report . _apply_ir_rules ( where_query , ' read ' )
from_clause , where_clause , where_clause_params = where_query . get_sql ( )
# price_total is in the company currency
query = """
SELECT SUM ( price_total ) as total , partner_id
FROM account_invoice_report account_invoice_report
WHERE % s
GROUP BY partner_id
""" % where_clause
self . env . cr . execute ( query , where_clause_params )
price_totals = self . env . cr . dictfetchall ( )
for partner , child_ids in all_partners_and_children . items ( ) :
partner . total_invoiced = sum ( price [ ' total ' ] for price in price_totals if price [ ' partner_id ' ] in child_ids )
@api.multi
def _compute_journal_item_count ( self ) :
AccountMoveLine = self . env [ ' account.move.line ' ]
for partner in self :
partner . journal_item_count = AccountMoveLine . search_count ( [ ( ' partner_id ' , ' = ' , partner . id ) ] )
@api.multi
def _compute_contracts_count ( self ) :
AccountAnalyticAccount = self . env [ ' account.analytic.account ' ]
for partner in self :
partner . contracts_count = AccountAnalyticAccount . search_count ( [ ( ' partner_id ' , ' = ' , partner . id ) ] )
def get_followup_lines_domain ( self , date , overdue_only = False , only_unblocked = False ) :
domain = [ ( ' reconciled ' , ' = ' , False ) , ( ' account_id.deprecated ' , ' = ' , False ) , ( ' account_id.internal_type ' , ' = ' , ' receivable ' ) , ' | ' , ( ' debit ' , ' != ' , 0 ) , ( ' credit ' , ' != ' , 0 ) , ( ' company_id ' , ' = ' , self . env . user . company_id . id ) ]
if only_unblocked :
domain + = [ ( ' blocked ' , ' = ' , False ) ]
if self . ids :
if ' exclude_given_ids ' in self . _context :
domain + = [ ( ' partner_id ' , ' not in ' , self . ids ) ]
else :
domain + = [ ( ' partner_id ' , ' in ' , self . ids ) ]
#adding the overdue lines
overdue_domain = [ ' | ' , ' & ' , ( ' date_maturity ' , ' != ' , False ) , ( ' date_maturity ' , ' < ' , date ) , ' & ' , ( ' date_maturity ' , ' = ' , False ) , ( ' date ' , ' < ' , date ) ]
if overdue_only :
domain + = overdue_domain
return domain
@api.one
def _compute_has_unreconciled_entries ( self ) :
# Avoid useless work if has_unreconciled_entries is not relevant for this partner
if not self . active or not self . is_company and self . parent_id :
return
self . env . cr . execute (
""" SELECT 1 FROM(
SELECT
p . last_time_entries_checked AS last_time_entries_checked ,
MAX ( l . write_date ) AS max_date
FROM
account_move_line l
RIGHT JOIN account_account a ON ( a . id = l . account_id )
RIGHT JOIN res_partner p ON ( l . partner_id = p . id )
WHERE
p . id = % s
AND EXISTS (
SELECT 1
FROM account_move_line l
WHERE l . account_id = a . id
AND l . partner_id = p . id
AND l . amount_residual > 0
)
AND EXISTS (
SELECT 1
FROM account_move_line l
WHERE l . account_id = a . id
AND l . partner_id = p . id
AND l . amount_residual < 0
)
GROUP BY p . last_time_entries_checked
) as s
WHERE ( last_time_entries_checked IS NULL OR max_date > last_time_entries_checked )
""" , (self.id,))
self . has_unreconciled_entries = self . env . cr . rowcount == 1
@api.multi
def mark_as_reconciled ( self ) :
self . env [ ' account.partial.reconcile ' ] . check_access_rights ( ' write ' )
return self . sudo ( ) . with_context ( company_id = self . env . user . company_id . id ) . write ( { ' last_time_entries_checked ' : time . strftime ( DEFAULT_SERVER_DATETIME_FORMAT ) } )
@api.one
def _get_company_currency ( self ) :
if self . company_id :
self . currency_id = self . sudo ( ) . company_id . currency_id
else :
self . currency_id = self . env . user . company_id . currency_id
credit = fields . Monetary ( compute = ' _credit_debit_get ' , search = _credit_search ,
string = ' Total Receivable ' , help = " Total amount this customer owes you. " )
debit = fields . Monetary ( compute = ' _credit_debit_get ' , search = _debit_search , string = ' Total Payable ' ,
help = " Total amount you have to pay to this vendor. " )
debit_limit = fields . Monetary ( ' Payable Limit ' )
total_invoiced = fields . Monetary ( compute = ' _invoice_total ' , string = " Total Invoiced " ,
groups = ' account.group_account_invoice ' )
currency_id = fields . Many2one ( ' res.currency ' , compute = ' _get_company_currency ' , readonly = True ,
string = " Currency " , help = ' Utility field to express amount currency ' )
contracts_count = fields . Integer ( compute = ' _compute_contracts_count ' , string = " Contracts " , type = ' integer ' )
journal_item_count = fields . Integer ( compute = ' _compute_journal_item_count ' , string = " Journal Items " , type = " integer " )
property_account_payable_id = fields . Many2one ( ' account.account ' , company_dependent = True ,
string = " Account Payable " , oldname = " property_account_payable " ,
domain = " [( ' internal_type ' , ' = ' , ' payable ' ), ( ' deprecated ' , ' = ' , False)] " ,
help = " This account will be used instead of the default one as the payable account for the current partner " ,
required = True )
property_account_receivable_id = fields . Many2one ( ' account.account ' , company_dependent = True ,
string = " Account Receivable " , oldname = " property_account_receivable " ,
domain = " [( ' internal_type ' , ' = ' , ' receivable ' ), ( ' deprecated ' , ' = ' , False)] " ,
help = " This account will be used instead of the default one as the receivable account for the current partner " ,
required = True )
property_account_position_id = fields . Many2one ( ' account.fiscal.position ' , company_dependent = True ,
string = " Fiscal Position " ,
help = " The fiscal position will determine taxes and accounts used for the partner. " , oldname = " property_account_position " )
property_payment_term_id = fields . Many2one ( ' account.payment.term ' , company_dependent = True ,
string = ' Customer Payment Terms ' ,
help = " This payment term will be used instead of the default one for sales orders and customer invoices " , oldname = " property_payment_term " )
property_supplier_payment_term_id = fields . Many2one ( ' account.payment.term ' , company_dependent = True ,
string = ' Vendor Payment Terms ' ,
help = " This payment term will be used instead of the default one for purchase orders and vendor bills " , oldname = " property_supplier_payment_term " )
ref_company_ids = fields . One2many ( ' res.company ' , ' partner_id ' ,
string = ' Companies that refers to partner ' , oldname = " ref_companies " )
has_unreconciled_entries = fields . Boolean ( compute = ' _compute_has_unreconciled_entries ' ,
help = " The partner has at least one unreconciled debit and credit since last time the invoices & payments matching was performed. " )
last_time_entries_checked = fields . Datetime ( oldname = ' last_reconciliation_date ' ,
string = ' Latest Invoices & Payments Matching Date ' , readonly = True , copy = False ,
help = ' Last time the invoices & payments matching was performed for this partner. '
' It is set either if there \' s not at least an unreconciled debit and an unreconciled credit '
' or if you click the " Done " button. ' )
invoice_ids = fields . One2many ( ' account.invoice ' , ' partner_id ' , string = ' Invoices ' , readonly = True , copy = False )
contract_ids = fields . One2many ( ' account.analytic.account ' , ' partner_id ' , string = ' Contracts ' , readonly = True )
bank_account_count = fields . Integer ( compute = ' _compute_bank_count ' , string = " Bank " )
trust = fields . Selection ( [ ( ' good ' , ' Good Debtor ' ) , ( ' normal ' , ' Normal Debtor ' ) , ( ' bad ' , ' Bad Debtor ' ) ] , string = ' Degree of trust you have in this debtor ' , default = ' normal ' , company_dependent = True )
invoice_warn = fields . Selection ( WARNING_MESSAGE , ' Invoice ' , help = WARNING_HELP , required = True , default = " no-message " )
invoice_warn_msg = fields . Text ( ' Message for Invoice ' )
@api.multi
def _compute_bank_count ( self ) :
bank_data = self . env [ ' res.partner.bank ' ] . read_group ( [ ( ' partner_id ' , ' in ' , self . ids ) ] , [ ' partner_id ' ] , [ ' partner_id ' ] )
mapped_data = dict ( [ ( bank [ ' partner_id ' ] [ 0 ] , bank [ ' partner_id_count ' ] ) for bank in bank_data ] )
for partner in self :
partner . bank_account_count = mapped_data . get ( partner . id , 0 )
def _find_accounting_partner ( self , partner ) :
''' Find the partner for which the accounting entries will be created '''
return partner . commercial_partner_id
@api.model
def _commercial_fields ( self ) :
return super ( ResPartner , self ) . _commercial_fields ( ) + \
[ ' debit_limit ' , ' property_account_payable_id ' , ' property_account_receivable_id ' , ' property_account_position_id ' ,
' property_payment_term_id ' , ' property_supplier_payment_term_id ' , ' last_time_entries_checked ' ]
@api.multi
def action_view_partner_invoices ( self ) :
self . ensure_one ( )
action = self . env . ref ( ' account.action_invoice_refund_out_tree ' ) . read ( ) [ 0 ]
action [ ' domain ' ] = literal_eval ( action [ ' domain ' ] )
action [ ' domain ' ] . append ( ( ' partner_id ' , ' child_of ' , self . id ) )
return action