[MERGE] forward port of branch 8.0 up to ed1c173
[odoo/odoo.git] / addons / account / res_config.py
1 # -*- coding: utf-8 -*-
2 ##############################################################################
3 #
4 #    OpenERP, Open Source Business Applications
5 #    Copyright (C) 2004-2012 OpenERP S.A. (<http://openerp.com>).
6 #
7 #    This program is free software: you can redistribute it and/or modify
8 #    it under the terms of the GNU Affero General Public License as
9 #    published by the Free Software Foundation, either version 3 of the
10 #    License, or (at your option) any later version.
11 #
12 #    This program is distributed in the hope that it will be useful,
13 #    but WITHOUT ANY WARRANTY; without even the implied warranty of
14 #    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
15 #    GNU Affero General Public License for more details.
16 #
17 #    You should have received a copy of the GNU Affero General Public License
18 #    along with this program.  If not, see <http://www.gnu.org/licenses/>.
19 #
20 ##############################################################################
21
22 import time
23 import datetime
24 from dateutil.relativedelta import relativedelta
25
26 import openerp
27 from openerp import SUPERUSER_ID
28 from openerp.tools import DEFAULT_SERVER_DATE_FORMAT as DF
29 from openerp.tools.translate import _
30 from openerp.osv import fields, osv
31
32 class account_config_settings(osv.osv_memory):
33     _name = 'account.config.settings'
34     _inherit = 'res.config.settings'
35
36     _columns = {
37         'company_id': fields.many2one('res.company', 'Company', required=True),
38         'has_default_company': fields.boolean('Has default company', readonly=True),
39         'expects_chart_of_accounts': fields.related('company_id', 'expects_chart_of_accounts', type='boolean',
40             string='This company has its own chart of accounts',
41             help="""Check this box if this company is a legal entity."""),
42         'currency_id': fields.related('company_id', 'currency_id', type='many2one', relation='res.currency', required=True,
43             string='Default company currency', help="Main currency of the company."),
44         'paypal_account': fields.related('company_id', 'paypal_account', type='char', size=128,
45             string='Paypal account', help="Paypal account (email) for receiving online payments (credit card, etc.) If you set a paypal account, the customer  will be able to pay your invoices or quotations with a button \"Pay with  Paypal\" in automated emails or through the Odoo portal."),
46         'company_footer': fields.related('company_id', 'rml_footer', type='text', readonly=True,
47             string='Bank accounts footer preview', help="Bank accounts as printed in the footer of each printed document"),
48
49         'has_chart_of_accounts': fields.boolean('Company has a chart of accounts'),
50         'chart_template_id': fields.many2one('account.chart.template', 'Template', domain="[('visible','=', True)]"),
51         'code_digits': fields.integer('# of Digits', help="No. of digits to use for account code"),
52         'tax_calculation_rounding_method': fields.related('company_id',
53             'tax_calculation_rounding_method', type='selection', selection=[
54             ('round_per_line', 'Round calculation of taxes per line'),
55             ('round_globally', 'Round globally calculation of taxes '),
56             ], string='Tax calculation rounding method',
57             help="If you select 'Round per line' : for each tax, the tax amount will first be computed and rounded for each PO/SO/invoice line and then these rounded amounts will be summed, leading to the total amount for that tax. If you select 'Round globally': for each tax, the tax amount will be computed for each PO/SO/invoice line, then these amounts will be summed and eventually this total tax amount will be rounded. If you sell with tax included, you should choose 'Round per line' because you certainly want the sum of your tax-included line subtotals to be equal to the total amount with taxes."),
58         'sale_tax': fields.many2one("account.tax.template", "Default sale tax"),
59         'purchase_tax': fields.many2one("account.tax.template", "Default purchase tax"),
60         'sale_tax_rate': fields.float('Sales tax (%)'),
61         'purchase_tax_rate': fields.float('Purchase tax (%)'),
62         'complete_tax_set': fields.boolean('Complete set of taxes', help='This boolean helps you to choose if you want to propose to the user to encode the sales and purchase rates or use the usual m2o fields. This last choice assumes that the set of tax defined for the chosen template is complete'),
63
64         'has_fiscal_year': fields.boolean('Company has a fiscal year'),
65         'date_start': fields.date('Start date', required=True),
66         'date_stop': fields.date('End date', required=True),
67         'period': fields.selection([('month', 'Monthly'), ('3months','3 Monthly')], 'Periods', required=True),
68
69         'sale_journal_id': fields.many2one('account.journal', 'Sale journal'),
70         'sale_sequence_prefix': fields.related('sale_journal_id', 'sequence_id', 'prefix', type='char', string='Invoice sequence'),
71         'sale_sequence_next': fields.related('sale_journal_id', 'sequence_id', 'number_next', type='integer', string='Next invoice number'),
72         'sale_refund_journal_id': fields.many2one('account.journal', 'Sale refund journal'),
73         'sale_refund_sequence_prefix': fields.related('sale_refund_journal_id', 'sequence_id', 'prefix', type='char', string='Credit note sequence'),
74         'sale_refund_sequence_next': fields.related('sale_refund_journal_id', 'sequence_id', 'number_next', type='integer', string='Next credit note number'),
75         'purchase_journal_id': fields.many2one('account.journal', 'Purchase journal'),
76         'purchase_sequence_prefix': fields.related('purchase_journal_id', 'sequence_id', 'prefix', type='char', string='Supplier invoice sequence'),
77         'purchase_sequence_next': fields.related('purchase_journal_id', 'sequence_id', 'number_next', type='integer', string='Next supplier invoice number'),
78         'purchase_refund_journal_id': fields.many2one('account.journal', 'Purchase refund journal'),
79         'purchase_refund_sequence_prefix': fields.related('purchase_refund_journal_id', 'sequence_id', 'prefix', type='char', string='Supplier credit note sequence'),
80         'purchase_refund_sequence_next': fields.related('purchase_refund_journal_id', 'sequence_id', 'number_next', type='integer', string='Next supplier credit note number'),
81
82         'module_account_check_writing': fields.boolean('Pay your suppliers by check',
83             help='This allows you to check writing and printing.\n'
84                  '-This installs the module account_check_writing.'),
85         'module_account_accountant': fields.boolean('Full accounting features: journals, legal statements, chart of accounts, etc.',
86             help="""If you do not check this box, you will be able to do invoicing & payments, but not accounting (Journal Items, Chart of  Accounts, ...)"""),
87         'module_account_asset': fields.boolean('Assets management',
88             help='This allows you to manage the assets owned by a company or a person.\n'
89                  'It keeps track of the depreciation occurred on those assets, and creates account move for those depreciation lines.\n'
90                  '-This installs the module account_asset. If you do not check this box, you will be able to do invoicing & payments, '
91                  'but not accounting (Journal Items, Chart of Accounts, ...)'),
92         'module_account_budget': fields.boolean('Budget management',
93             help='This allows accountants to manage analytic and crossovered budgets. '
94                  'Once the master budgets and the budgets are defined, '
95                  'the project managers can set the planned amount on each analytic account.\n'
96                  '-This installs the module account_budget.'),
97         'module_account_payment': fields.boolean('Manage payment orders',
98             help='This allows you to create and manage your payment orders, with purposes to \n'
99                  '* serve as base for an easy plug-in of various automated payment mechanisms, and \n'
100                  '* provide a more efficient way to manage invoice payments.\n'
101                  '-This installs the module account_payment.' ),
102         'module_account_voucher': fields.boolean('Manage customer payments',
103             help='This includes all the basic requirements of voucher entries for bank, cash, sales, purchase, expense, contra, etc.\n'
104                  '-This installs the module account_voucher.'),
105         'module_account_followup': fields.boolean('Manage customer payment follow-ups',
106             help='This allows to automate letters for unpaid invoices, with multi-level recalls.\n'
107                  '-This installs the module account_followup.'),
108         'module_product_email_template': fields.boolean('Send products tools and information at the invoice confirmation',
109             help='With this module, link your products to a template to send complete information and tools to your customer.\n'
110                  'For instance when invoicing a training, the training agenda and materials will automatically be send to your customers.'),
111         'module_account_bank_statement_import_ofx': fields.boolean('Import of Bank Statements in .OFX Format',
112             help='Get your bank statements from you bank and import them in Odoo in .OFX format.\n'
113                 '-that installs the module account_bank_statement_import.'),
114         'module_account_bank_statement_import_qif': fields.boolean('Import of Bank Statements in .QIF Format.',
115             help='Get your bank statements from you bank and import them in Odoo in .QIF format.\n'
116                 '-that installs the module account_bank_statement_import_qif.'),
117         'group_proforma_invoices': fields.boolean('Allow pro-forma invoices',
118             implied_group='account.group_proforma_invoices',
119             help="Allows you to put invoices in pro-forma state."),
120         'default_sale_tax': fields.many2one('account.tax', 'Default sale tax',
121             help="This sale tax will be assigned by default on new products."),
122         'default_purchase_tax': fields.many2one('account.tax', 'Default purchase tax',
123             help="This purchase tax will be assigned by default on new products."),
124         'decimal_precision': fields.integer('Decimal precision on journal entries',
125             help="""As an example, a decimal precision of 2 will allow journal entries  like: 9.99 EUR, whereas a decimal precision of 4 will allow journal  entries like: 0.0231 EUR."""),
126         'group_multi_currency': fields.boolean('Allow multi currencies',
127             implied_group='base.group_multi_currency',
128             help="Allows you multi currency environment"),
129         'group_analytic_accounting': fields.boolean('Analytic accounting',
130             implied_group='analytic.group_analytic_accounting',
131             help="Allows you to use the analytic accounting."),
132         'group_check_supplier_invoice_total': fields.boolean('Check the total of supplier invoices', 
133             implied_group="account.group_supplier_inv_check_total"),
134         'income_currency_exchange_account_id': fields.related(
135             'company_id', 'income_currency_exchange_account_id',
136             type='many2one',
137             relation='account.account',
138             string="Gain Exchange Rate Account", 
139             domain="[('type', '=', 'other')]"),
140         'expense_currency_exchange_account_id': fields.related(
141             'company_id', 'expense_currency_exchange_account_id',
142             type="many2one",
143             relation='account.account',
144             string="Loss Exchange Rate Account",
145             domain="[('type', '=', 'other')]"),
146     }
147     def onchange_company_id(self, cr, uid, ids, company_id, context=None):
148         res = super(account_config_settings, self).onchange_company_id(cr, uid, ids, company_id, context=context)
149         if company_id:
150             company = self.pool.get('res.company').browse(cr, uid, company_id, context=context)
151             res['value'].update({'income_currency_exchange_account_id': company.income_currency_exchange_account_id and company.income_currency_exchange_account_id.id or False, 
152                                  'expense_currency_exchange_account_id': company.expense_currency_exchange_account_id and company.expense_currency_exchange_account_id.id or False})
153         else: 
154             res['value'].update({'income_currency_exchange_account_id': False, 
155                                  'expense_currency_exchange_account_id': False})
156         return res
157
158     def _default_company(self, cr, uid, context=None):
159         user = self.pool.get('res.users').browse(cr, uid, uid, context=context)
160         return user.company_id.id
161
162     def _default_has_default_company(self, cr, uid, context=None):
163         count = self.pool.get('res.company').search_count(cr, uid, [], context=context)
164         return bool(count == 1)
165
166     def _get_default_fiscalyear_data(self, cr, uid, company_id, context=None):
167         """Compute default period, starting and ending date for fiscalyear
168         - if in a fiscal year, use its period, starting and ending date
169         - if past fiscal year, use its period, and new dates [ending date of the latest +1 day ; ending date of the latest +1 year]
170         - if no fiscal year, use monthly, 1st jan, 31th dec of this year
171         :return: (date_start, date_stop, period) at format DEFAULT_SERVER_DATETIME_FORMAT
172         """
173         fiscalyear_ids = self.pool.get('account.fiscalyear').search(cr, uid,
174                 [('date_start', '<=', time.strftime(DF)), ('date_stop', '>=', time.strftime(DF)),
175                  ('company_id', '=', company_id)])
176         if fiscalyear_ids:
177             # is in a current fiscal year, use this one
178             fiscalyear = self.pool.get('account.fiscalyear').browse(cr, uid, fiscalyear_ids[0], context=context)
179             if len(fiscalyear.period_ids) == 5:  # 4 periods of 3 months + opening period
180                 period = '3months'
181             else:
182                 period = 'month'
183             return (fiscalyear.date_start, fiscalyear.date_stop, period)
184         else:
185             past_fiscalyear_ids = self.pool.get('account.fiscalyear').search(cr, uid,
186                 [('date_stop', '<=', time.strftime(DF)), ('company_id', '=', company_id)])
187             if past_fiscalyear_ids:
188                 # use the latest fiscal, sorted by (start_date, id)
189                 latest_year = self.pool.get('account.fiscalyear').browse(cr, uid, past_fiscalyear_ids[-1], context=context)
190                 latest_stop = datetime.datetime.strptime(latest_year.date_stop, DF)
191                 if len(latest_year.period_ids) == 5:
192                     period = '3months'
193                 else:
194                     period = 'month'
195                 return ((latest_stop+datetime.timedelta(days=1)).strftime(DF), latest_stop.replace(year=latest_stop.year+1).strftime(DF), period)
196             else:
197                 return (time.strftime('%Y-01-01'), time.strftime('%Y-12-31'), 'month')
198
199
200     _defaults = {
201         'company_id': _default_company,
202         'has_default_company': _default_has_default_company,
203     }
204
205     def create(self, cr, uid, values, context=None):
206         id = super(account_config_settings, self).create(cr, uid, values, context)
207         # Hack: to avoid some nasty bug, related fields are not written upon record creation.
208         # Hence we write on those fields here.
209         vals = {}
210         for fname, field in self._columns.iteritems():
211             if isinstance(field, fields.related) and fname in values:
212                 vals[fname] = values[fname]
213         self.write(cr, uid, [id], vals, context)
214         return id
215
216     def onchange_company_id(self, cr, uid, ids, company_id, context=None):
217         # update related fields
218         values = {}
219         values['currency_id'] = False
220         if company_id:
221             company = self.pool.get('res.company').browse(cr, uid, company_id, context=context)
222             has_chart_of_accounts = company_id not in self.pool.get('account.installer').get_unconfigured_cmp(cr, uid)
223             fiscalyear_count = self.pool.get('account.fiscalyear').search_count(cr, uid,
224                 [('date_start', '<=', time.strftime('%Y-%m-%d')), ('date_stop', '>=', time.strftime('%Y-%m-%d')),
225                  ('company_id', '=', company_id)])
226             date_start, date_stop, period = self._get_default_fiscalyear_data(cr, uid, company_id, context=context)
227             values = {
228                 'expects_chart_of_accounts': company.expects_chart_of_accounts,
229                 'currency_id': company.currency_id.id,
230                 'paypal_account': company.paypal_account,
231                 'company_footer': company.rml_footer,
232                 'has_chart_of_accounts': has_chart_of_accounts,
233                 'has_fiscal_year': bool(fiscalyear_count),
234                 'chart_template_id': False,
235                 'tax_calculation_rounding_method': company.tax_calculation_rounding_method,
236                 'date_start': date_start,
237                 'date_stop': date_stop,
238                 'period': period,
239             }
240             # update journals and sequences
241             for journal_type in ('sale', 'sale_refund', 'purchase', 'purchase_refund'):
242                 for suffix in ('_journal_id', '_sequence_prefix', '_sequence_next'):
243                     values[journal_type + suffix] = False
244             journal_obj = self.pool.get('account.journal')
245             journal_ids = journal_obj.search(cr, uid, [('company_id', '=', company_id)])
246             for journal in journal_obj.browse(cr, uid, journal_ids):
247                 if journal.type in ('sale', 'sale_refund', 'purchase', 'purchase_refund'):
248                     values.update({
249                         journal.type + '_journal_id': journal.id,
250                         journal.type + '_sequence_prefix': journal.sequence_id.prefix,
251                         journal.type + '_sequence_next': journal.sequence_id.number_next,
252                     })
253             # update taxes
254             ir_values = self.pool.get('ir.values')
255             taxes_id = ir_values.get_default(cr, uid, 'product.product', 'taxes_id', company_id=company_id)
256             supplier_taxes_id = ir_values.get_default(cr, uid, 'product.product', 'supplier_taxes_id', company_id=company_id)
257             values.update({
258                 'default_sale_tax': isinstance(taxes_id, list) and taxes_id[0] or taxes_id,
259                 'default_purchase_tax': isinstance(supplier_taxes_id, list) and supplier_taxes_id[0] or supplier_taxes_id,
260             })
261         return {'value': values}
262
263     def onchange_chart_template_id(self, cr, uid, ids, chart_template_id, context=None):
264         tax_templ_obj = self.pool.get('account.tax.template')
265         res = {'value': {
266             'complete_tax_set': False, 'sale_tax': False, 'purchase_tax': False,
267             'sale_tax_rate': 15, 'purchase_tax_rate': 15,
268         }}
269         if chart_template_id:
270             # update complete_tax_set, sale_tax and purchase_tax
271             chart_template = self.pool.get('account.chart.template').browse(cr, uid, chart_template_id, context=context)
272             res['value'].update({'complete_tax_set': chart_template.complete_tax_set})
273             if chart_template.complete_tax_set:
274                 # default tax is given by the lowest sequence. For same sequence we will take the latest created as it will be the case for tax created while isntalling the generic chart of account
275                 sale_tax_ids = tax_templ_obj.search(cr, uid,
276                     [("chart_template_id", "=", chart_template_id), ('type_tax_use', 'in', ('sale','all'))],
277                     order="sequence, id desc")
278                 purchase_tax_ids = tax_templ_obj.search(cr, uid,
279                     [("chart_template_id", "=", chart_template_id), ('type_tax_use', 'in', ('purchase','all'))],
280                     order="sequence, id desc")
281                 res['value']['sale_tax'] = sale_tax_ids and sale_tax_ids[0] or False
282                 res['value']['purchase_tax'] = purchase_tax_ids and purchase_tax_ids[0] or False
283             if chart_template.code_digits:
284                 res['value']['code_digits'] = chart_template.code_digits
285         return res
286
287     def onchange_tax_rate(self, cr, uid, ids, rate, context=None):
288         return {'value': {'purchase_tax_rate': rate or False}}
289
290     def onchange_multi_currency(self, cr, uid, ids, group_multi_currency, context=None):
291         res = {}
292         if not group_multi_currency:
293             res['value'] = {'income_currency_exchange_account_id': False, 'expense_currency_exchange_account_id': False}
294         return res
295     
296     def onchange_start_date(self, cr, uid, id, start_date):
297         if start_date:
298             start_date = datetime.datetime.strptime(start_date, "%Y-%m-%d")
299             end_date = (start_date + relativedelta(months=12)) - relativedelta(days=1)
300             return {'value': {'date_stop': end_date.strftime('%Y-%m-%d')}}
301         return {}
302
303     def open_company_form(self, cr, uid, ids, context=None):
304         config = self.browse(cr, uid, ids[0], context)
305         return {
306             'type': 'ir.actions.act_window',
307             'name': 'Configure your Company',
308             'res_model': 'res.company',
309             'res_id': config.company_id.id,
310             'view_mode': 'form',
311         }
312
313     def set_default_taxes(self, cr, uid, ids, context=None):
314         """ set default sale and purchase taxes for products """
315         if uid != SUPERUSER_ID and not self.pool['res.users'].has_group(cr, uid, 'base.group_erp_manager'):
316             raise openerp.exceptions.AccessError(_("Only administrators can change the settings"))
317         ir_values = self.pool.get('ir.values')
318         config = self.browse(cr, uid, ids[0], context)
319         ir_values.set_default(cr, SUPERUSER_ID, 'product.product', 'taxes_id',
320             config.default_sale_tax and [config.default_sale_tax.id] or False, company_id=config.company_id.id)
321         ir_values.set_default(cr, SUPERUSER_ID, 'product.product', 'supplier_taxes_id',
322             config.default_purchase_tax and [config.default_purchase_tax.id] or False, company_id=config.company_id.id)
323
324     def set_chart_of_accounts(self, cr, uid, ids, context=None):
325         """ install a chart of accounts for the given company (if required) """
326         config = self.browse(cr, uid, ids[0], context)
327         if config.chart_template_id:
328             assert config.expects_chart_of_accounts and not config.has_chart_of_accounts
329             wizard = self.pool.get('wizard.multi.charts.accounts')
330             wizard_id = wizard.create(cr, uid, {
331                 'company_id': config.company_id.id,
332                 'chart_template_id': config.chart_template_id.id,
333                 'code_digits': config.code_digits or 6,
334                 'sale_tax': config.sale_tax.id,
335                 'purchase_tax': config.purchase_tax.id,
336                 'sale_tax_rate': config.sale_tax_rate,
337                 'purchase_tax_rate': config.purchase_tax_rate,
338                 'complete_tax_set': config.complete_tax_set,
339                 'currency_id': config.currency_id.id,
340             }, context)
341             wizard.execute(cr, uid, [wizard_id], context)
342
343     def set_fiscalyear(self, cr, uid, ids, context=None):
344         """ create a fiscal year for the given company (if necessary) """
345         config = self.browse(cr, uid, ids[0], context)
346         if config.has_chart_of_accounts or config.chart_template_id:
347             fiscalyear = self.pool.get('account.fiscalyear')
348             fiscalyear_count = fiscalyear.search_count(cr, uid,
349                 [('date_start', '<=', config.date_start), ('date_stop', '>=', config.date_stop),
350                  ('company_id', '=', config.company_id.id)],
351                 context=context)
352             if not fiscalyear_count:
353                 name = code = config.date_start[:4]
354                 if int(name) != int(config.date_stop[:4]):
355                     name = config.date_start[:4] +'-'+ config.date_stop[:4]
356                     code = config.date_start[2:4] +'-'+ config.date_stop[2:4]
357                 vals = {
358                     'name': name,
359                     'code': code,
360                     'date_start': config.date_start,
361                     'date_stop': config.date_stop,
362                     'company_id': config.company_id.id,
363                 }
364                 fiscalyear_id = fiscalyear.create(cr, uid, vals, context=context)
365                 if config.period == 'month':
366                     fiscalyear.create_period(cr, uid, [fiscalyear_id])
367                 elif config.period == '3months':
368                     fiscalyear.create_period3(cr, uid, [fiscalyear_id])
369
370     def get_default_dp(self, cr, uid, fields, context=None):
371         dp = self.pool.get('ir.model.data').get_object(cr, uid, 'product','decimal_account')
372         return {'decimal_precision': dp.digits}
373
374     def set_default_dp(self, cr, uid, ids, context=None):
375         config = self.browse(cr, uid, ids[0], context)
376         dp = self.pool.get('ir.model.data').get_object(cr, uid, 'product','decimal_account')
377         dp.write({'digits': config.decimal_precision})
378
379     def onchange_analytic_accounting(self, cr, uid, ids, analytic_accounting, context=None):
380         if analytic_accounting:
381             return {'value': {
382                 'module_account_accountant': True,
383                 }}
384         return {}
385 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: