diff --git a/addons/account_analytic_analysis/__openerp__.py b/addons/account_analytic_analysis/__openerp__.py index 99e4353afa1..61a92490c47 100644 --- a/addons/account_analytic_analysis/__openerp__.py +++ b/addons/account_analytic_analysis/__openerp__.py @@ -45,6 +45,7 @@ Adds menu to show relevant information to each manager.You can also view the rep 'static/src/css/analytic.css' ], 'demo': ['analytic_account_demo.xml'], + 'test': ['test/account_analytic_analysis.yml'], 'installable': True, 'auto_install': False, } diff --git a/addons/account_analytic_analysis/account_analytic_analysis.py b/addons/account_analytic_analysis/account_analytic_analysis.py index 64912213520..0a56c80766b 100644 --- a/addons/account_analytic_analysis/account_analytic_analysis.py +++ b/addons/account_analytic_analysis/account_analytic_analysis.py @@ -18,6 +18,7 @@ # along with this program. If not, see . # ############################################################################## +from dateutil.relativedelta import relativedelta import datetime import logging import time @@ -31,6 +32,58 @@ from openerp.addons.decimal_precision import decimal_precision as dp _logger = logging.getLogger(__name__) +class account_analytic_invoice_line(osv.osv): + _name = "account.analytic.invoice.line" + + def _amount_line(self, cr, uid, ids, prop, unknow_none, unknow_dict, context=None): + res = {} + for line in self.browse(cr, uid, ids, context=context): + res[line.id] = line.quantity * line.price_unit + if line.analytic_account_id.pricelist_id: + cur = line.analytic_account_id.pricelist_id.currency_id + res[line.id] = self.pool.get('res.currency').round(cr, uid, cur, res[line.id]) + return res + + _columns = { + 'product_id': fields.many2one('product.product','Product',required=True), + 'analytic_account_id': fields.many2one('account.analytic.account', 'Analytic Account'), + 'name': fields.text('Description', required=True), + 'quantity': fields.float('Quantity', required=True), + 'uom_id': fields.many2one('product.uom', 'Unit of Measure',required=True), + 'price_unit': fields.float('Unit Price', required=True), + 'price_subtotal': fields.function(_amount_line, string='Sub Total', type="float",digits_compute= dp.get_precision('Account')), + } + _defaults = { + 'quantity' : 1, + } + + def product_id_change(self, cr, uid, ids, product, uom_id, qty=0, name='', partner_id=False, price_unit=False, pricelist_id=False, company_id=None, context=None): + context = context or {} + uom_obj = self.pool.get('product.uom') + company_id = company_id or False + context.update({'company_id': company_id, 'force_company': company_id, 'pricelist_id': pricelist_id}) + + if not product: + return {'value': {'price_unit': 0.0}, 'domain':{'product_uom':[]}} + if partner_id: + part = self.pool.get('res.partner').browse(cr, uid, partner_id, context=context) + if part.lang: + context.update({'lang': part.lang}) + + result = {} + res = self.pool.get('product.product').browse(cr, uid, product, context=context) + result.update({'name':res.partner_ref or False,'uom_id': uom_id or res.uom_id.id or False, 'price_unit': res.list_price or 0.0}) + if res.description: + result['name'] += '\n'+res.description + + res_final = {'value':result} + if result['uom_id'] != res.uom_id.id: + selected_uom = uom_obj.browse(cr, uid, result['uom_id'], context=context) + new_price = uom_obj._compute_price(cr, uid, res.uom_id.id, res_final['value']['price_unit'], result['uom_id']) + res_final['value']['price_unit'] = new_price + return res_final + + class account_analytic_account(osv.osv): _name = "account.analytic.account" _inherit = "account.analytic.account" @@ -458,6 +511,22 @@ class account_analytic_account(osv.osv): 'invoiced_total' : fields.function(_sum_of_fields, type="float",multi="sum_of_all", string="Total Invoiced"), 'remaining_total' : fields.function(_sum_of_fields, type="float",multi="sum_of_all", string="Total Remaining", help="Expectation of remaining income for this contract. Computed as the sum of remaining subtotals which, in turn, are computed as the maximum between '(Estimation - Invoiced)' and 'To Invoice' amounts"), 'toinvoice_total' : fields.function(_sum_of_fields, type="float",multi="sum_of_all", string="Total to Invoice", help=" Sum of everything that could be invoiced for this contract."), + 'recurring_invoice_line_ids': fields.one2many('account.analytic.invoice.line', 'analytic_account_id', 'Invoice Lines'), + 'recurring_invoices' : fields.boolean('Generate recurring invoices automatically'), + 'recurring_rule_type': fields.selection([ + ('daily', 'Day(s)'), + ('weekly', 'Week(s)'), + ('monthly', 'Month(s)'), + ('yearly', 'Year(s)'), + ], 'Recurrency', help="Invoice automatically repeat at specified interval"), + 'recurring_interval': fields.integer('Repeat Every', help="Repeat every (Days/Week/Month/Year)"), + 'recurring_next_date': fields.date('Date of Next Invoice'), + } + + _defaults = { + 'recurring_interval': 1, + 'recurring_next_date': lambda *a: time.strftime('%Y-%m-%d'), + 'recurring_rule_type':'monthly' } def open_sale_order_lines(self,cr,uid,ids,context=None): @@ -480,17 +549,38 @@ class account_analytic_account(osv.osv): def on_change_template(self, cr, uid, ids, template_id, context=None): if not template_id: return {} + obj_analytic_line = self.pool.get('account.analytic.invoice.line') res = super(account_analytic_account, self).on_change_template(cr, uid, ids, template_id, context=context) - if template_id and 'value' in res: - template = self.browse(cr, uid, template_id, context=context) - res['value']['fix_price_invoices'] = template.fix_price_invoices - res['value']['invoice_on_timesheets'] = template.invoice_on_timesheets - res['value']['hours_qtt_est'] = template.hours_qtt_est - res['value']['amount_max'] = template.amount_max - res['value']['to_invoice'] = template.to_invoice.id - res['value']['pricelist_id'] = template.pricelist_id.id + + template = self.browse(cr, uid, template_id, context=context) + invoice_line_ids = [] + for x in template.recurring_invoice_line_ids: + invoice_line_ids.append((0, 0, { + 'product_id': x.product_id.id, + 'uom_id': x.uom_id.id, + 'name': x.name, + 'quantity': x.quantity, + 'price_unit': x.price_unit, + 'analytic_account_id': x.analytic_account_id and x.analytic_account_id.id or False, + })) + res['value']['fix_price_invoices'] = template.fix_price_invoices + res['value']['invoice_on_timesheets'] = template.invoice_on_timesheets + res['value']['hours_qtt_est'] = template.hours_qtt_est + res['value']['amount_max'] = template.amount_max + res['value']['to_invoice'] = template.to_invoice.id + res['value']['pricelist_id'] = template.pricelist_id.id + res['value']['recurring_invoices'] = template.recurring_invoices + res['value']['recurring_interval'] = template.recurring_interval + res['value']['recurring_rule_type'] = template.recurring_rule_type + res['value']['recurring_invoice_line_ids'] = invoice_line_ids return res + def onchange_recurring_invoices(self, cr, uid, ids, recurring_invoices, date_start=False, context=None): + value = {} + if date_start and recurring_invoices: + value = {'value': {'recurring_next_date': date_start}} + return value + def cron_account_analytic_account(self, cr, uid, context=None): if context is None: context = {} @@ -544,6 +634,86 @@ class account_analytic_account(osv.osv): pass return result + def _prepare_invoice(self, cr, uid, contract, context=None): + context = context or {} + + inv_obj = self.pool.get('account.invoice') + journal_obj = self.pool.get('account.journal') + fpos_obj = self.pool.get('account.fiscal.position') + + if not contract.partner_id: + raise osv.except_osv(_('No Customer Defined !'),_("You must first select a Customer for Contract %s!") % contract.name ) + + fpos = contract.partner_id.property_account_position.id or False + journal_ids = journal_obj.search(cr, uid, [('type', '=','sale'),('company_id', '=', contract.company_id.id or False)], limit=1) + if not journal_ids: + raise osv.except_osv(_('Error!'), + _('Please define a sale journal for the company "%s".') % (contract.company_id.name or '', )) + + partner_payment_term = contract.partner_id.property_payment_term and contract.partner_id.property_payment_term.id or False + + + inv_data = { + 'reference': contract.code or False, + 'account_id': contract.partner_id.property_account_receivable.id, + 'type': 'out_invoice', + 'partner_id': contract.partner_id.id, + 'currency_id': contract.partner_id.property_product_pricelist.id or False, + 'journal_id': len(journal_ids) and journal_ids[0] or False, + 'date_invoice': contract.recurring_next_date, + 'origin': contract.name, + 'fiscal_position': fpos, + 'payment_term': partner_payment_term, + 'company_id': contract.company_id.id or False, + } + invoice_id = inv_obj.create(cr, uid, inv_data, context=context) + + for line in contract.recurring_invoice_line_ids: + + res = line.product_id + account_id = res.property_account_income.id + if not account_id: + account_id = res.categ_id.property_account_income_categ.id + account_id = fpos_obj.map_account(cr, uid, fpos, account_id) + + taxes = res.taxes_id and res.taxes_id or False + tax_id = fpos_obj.map_tax(cr, uid, fpos, taxes) + + invoice_line_vals = { + 'name': line.name, + 'account_id': account_id, + 'account_analytic_id': contract.id, + 'price_unit': line.price_unit or 0.0, + 'quantity': line.quantity, + 'uos_id': line.uom_id.id or False, + 'product_id': line.product_id.id or False, + 'invoice_id' : invoice_id, + 'invoice_line_tax_id': [(6, 0, tax_id)], + } + self.pool.get('account.invoice.line').create(cr, uid, invoice_line_vals, context=context) + + inv_obj.button_compute(cr, uid, [invoice_id], context=context) + return invoice_id + + def recurring_create_invoice(self, cr, uid, automatic=False, context=None): + context = context or {} + current_date = time.strftime('%Y-%m-%d') + + contract_ids = self.search(cr, uid, [('recurring_next_date','<=', current_date), ('state','=', 'open'), ('recurring_invoices','=', True)]) + for contract in self.browse(cr, uid, contract_ids, context=context): + invoice_id = self._prepare_invoice(cr, uid, contract, context=context) + + next_date = datetime.datetime.strptime(contract.recurring_next_date or current_date, "%Y-%m-%d") + interval = contract.recurring_interval + if contract.recurring_rule_type == 'daily': + new_date = next_date+relativedelta(days=+interval) + elif contract.recurring_rule_type == 'weekly': + new_date = next_date+relativedelta(weeks=+interval) + else: + new_date = next_date+relativedelta(months=+interval) + self.write(cr, uid, [contract.id], {'recurring_next_date': new_date.strftime('%Y-%m-%d')}, context=context) + return True + class account_analytic_account_summary_user(osv.osv): _name = "account_analytic_analysis.summary.user" _description = "Hours Summary by User" diff --git a/addons/account_analytic_analysis/account_analytic_analysis_cron.xml b/addons/account_analytic_analysis/account_analytic_analysis_cron.xml index 311d8708872..14ca1fd2135 100644 --- a/addons/account_analytic_analysis/account_analytic_analysis_cron.xml +++ b/addons/account_analytic_analysis/account_analytic_analysis_cron.xml @@ -73,5 +73,16 @@ Hello ${object.name}, + + + Generate Recurring Invoices from Contracts + 1 + days + -1 + + + + + diff --git a/addons/account_analytic_analysis/account_analytic_analysis_view.xml b/addons/account_analytic_analysis/account_analytic_analysis_view.xml index 922ecf11a4b..b4746484e7c 100644 --- a/addons/account_analytic_analysis/account_analytic_analysis_view.xml +++ b/addons/account_analytic_analysis/account_analytic_analysis_view.xml @@ -36,7 +36,7 @@ - {'required': [('type','=','contract'),'|',('fix_price_invoices','=',True), ('invoice_on_timesheets', '=', True)]} + {'required': [('type','=','contract'),'|','|',('fix_price_invoices','=',True), ('invoice_on_timesheets', '=', True), ('recurring_invoices', '=', True)]} @@ -129,8 +129,8 @@ - -

+ +

When invoicing on timesheet, OpenERP uses the pricelist of the contract which uses the price defined on the product related to each employee to @@ -143,9 +143,36 @@ + attrs="{'required': [('invoice_on_timesheets', '=', True)], 'invisible': [('invoice_on_timesheets','=',False)]}"/> + +

+ +
+ + +
diff --git a/addons/account_analytic_analysis/analytic_account_demo.xml b/addons/account_analytic_analysis/analytic_account_demo.xml index f721fc1459d..4198b3e425c 100644 --- a/addons/account_analytic_analysis/analytic_account_demo.xml +++ b/addons/account_analytic_analysis/analytic_account_demo.xml @@ -33,9 +33,11 @@ + + True True 100 diff --git a/addons/account_analytic_analysis/security/ir.model.access.csv b/addons/account_analytic_analysis/security/ir.model.access.csv index 74425cea3dd..58998e95427 100644 --- a/addons/account_analytic_analysis/security/ir.model.access.csv +++ b/addons/account_analytic_analysis/security/ir.model.access.csv @@ -3,3 +3,6 @@ access_account_analytic_analysis_summary_user_sale,account_analytic_analysis.sum access_account_analytic_analysis_summary_month_sale,account_analytic_analysis.summary.month sale,model_account_analytic_analysis_summary_month,base.group_sale_salesman,1,0,0,0 access_account_analytic_analysis_summary_user,account_analytic_analysis.summary.user,model_account_analytic_analysis_summary_user,account.group_account_manager,1,0,0,0 access_account_analytic_analysis_summary_month,account_analytic_analysis.summary.month,model_account_analytic_analysis_summary_month,account.group_account_manager,1,0,0,0 +access_edi_edi,access_edi_edi,edi.model_edi_edi,account.group_account_manager,1,1,1,1 +access_account_analytic_invoice_line,account_analytic_invoice_line.user,model_account_analytic_invoice_line,account.group_account_user,1,1,1,1 +access_account_analytic_invoice_line,account_analytic_invoice_line.user,model_account_analytic_invoice_line,account.group_sale_salesman,1,1,1,1 \ No newline at end of file diff --git a/addons/account_analytic_analysis/test/account_analytic_analysis.yml b/addons/account_analytic_analysis/test/account_analytic_analysis.yml new file mode 100644 index 00000000000..49668a2b781 --- /dev/null +++ b/addons/account_analytic_analysis/test/account_analytic_analysis.yml @@ -0,0 +1,44 @@ +- + In order to test Contract Recurrent Invoice I create a new Contract Template +- + !record {model: account.analytic.account, id: contract_template}: + name: Maintenance of Servers + company_id: base.main_company + partner_id: base.main_partner + type: template + recurring_invoices : 1 + recurring_interval : 1 + recurring_invoice_line_ids: + - quantity: 2.0 + price_unit: 75.0 + name: Database Administration + product_id: product.product_product_consultant + uom_id: product.product_uom_hour +- + I create a contract based on this template +- + !record {model: account.analytic.account, id: contract_main}: + partner_id: base.main_partner + template_id: account_analytic_analysis.contract_template +- + I check that the contract inherited from data of the template +- + !python {model: account.analytic.account}: | + contract = self.browse(cr, uid, ref('account_analytic_analysis.contract_main')) + template = self.browse(cr, uid, ref('account_analytic_analysis.contract_template')) + assert template.recurring_interval == contract.recurring_interval, "The recurring interval of the contract does not match with the contract template" + assert template.recurring_invoices == contract.recurring_invoices, "Invoice method of contract does not match with the contract template" +- + I generate all invoices from contracts having recurring invoicing +- + !python {model: account.analytic.account}: | + self.recurring_create_invoice(cr, uid) +- + I test the generated invoice +- + !python {model: account.invoice}: | + aid = ref('account_analytic_analysis.contract_main') + ids = self.search(cr, uid, [('invoice_line.account_analytic_id','=',aid)], context=context) + assert len(ids)>=1, 'No invoice created for the contract' + for invoice in self.browse(cr, uid, ids,context=context): + assert invoice.amount_untaxed == 150.0, "The invoice total is different than 150!"