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 @@
+
+ TrueTrue100
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!"