[MERGE] recurring invoices on contracts
bzr revid: fp@openerp.com-20130312154529-1bu8lwfobrvk7zqa
This commit is contained in:
commit
e519da72a0
|
@ -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,
|
||||
}
|
||||
|
|
|
@ -18,6 +18,7 @@
|
|||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
##############################################################################
|
||||
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"
|
||||
|
|
|
@ -73,5 +73,16 @@ Hello ${object.name},
|
|||
<field name="function" eval="'cron_account_analytic_account'"/>
|
||||
<field name="args" eval="'()'" />
|
||||
</record>
|
||||
|
||||
<record model="ir.cron" id="account_analytic_cron_for_invoice">
|
||||
<field name="name">Generate Recurring Invoices from Contracts</field>
|
||||
<field name="interval_number">1</field>
|
||||
<field name="interval_type">days</field>
|
||||
<field name="numbercall">-1</field>
|
||||
<field name="model" eval="'account.analytic.account'"/>
|
||||
<field name="function" eval="'recurring_create_invoice'"/>
|
||||
<field name="args" eval="'()'"/>
|
||||
</record>
|
||||
|
||||
</data>
|
||||
</openerp>
|
||||
|
|
|
@ -36,7 +36,7 @@
|
|||
</div>
|
||||
</xpath>
|
||||
<field name="partner_id" position="attributes">
|
||||
<attribute name="attrs">{'required': [('type','=','contract'),'|',('fix_price_invoices','=',True), ('invoice_on_timesheets', '=', True)]}</attribute>
|
||||
<attribute name="attrs">{'required': [('type','=','contract'),'|','|',('fix_price_invoices','=',True), ('invoice_on_timesheets', '=', True), ('recurring_invoices', '=', True)]}</attribute>
|
||||
</field>
|
||||
<xpath expr='//group[@name="invoice_on_timesheets"]' position="replace">
|
||||
</xpath>
|
||||
|
@ -129,8 +129,8 @@
|
|||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<group name='invoice_on_timesheets' attrs="{'invisible': [('invoice_on_timesheets','=',False)]}">
|
||||
<p class="oe_grey oe_edit_only" colspan="2">
|
||||
<group name='invoice_on_timesheets'>
|
||||
<p class="oe_grey oe_edit_only" colspan="2" attrs="{'invisible': [('invoice_on_timesheets','=',False)]}">
|
||||
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 @@
|
|||
<field name="to_invoice"
|
||||
class="oe_inline"
|
||||
widget="selection"
|
||||
attrs="{'required': [('invoice_on_timesheets', '=', True)]}"/>
|
||||
attrs="{'required': [('invoice_on_timesheets', '=', True)], 'invisible': [('invoice_on_timesheets','=',False)]}"/>
|
||||
</group>
|
||||
</group>
|
||||
<separator string="Recurring Invoices" attrs="{'invisible': [('recurring_invoices','!=',True)]}"/>
|
||||
<div>
|
||||
<field name="recurring_invoices" on_change="onchange_recurring_invoices(recurring_invoices, date_start)" class="oe_inline"/>
|
||||
<label for="recurring_invoices" />
|
||||
<button class="oe_link" name="recurring_create_invoice" attrs="{'invisible': [('recurring_invoices','!=',True)]}" string="⇒ create invoices" type="object" groups="base.group_no_one"/>
|
||||
</div>
|
||||
<group attrs="{'invisible': [('recurring_invoices','!=',True)]}">
|
||||
<label for="recurring_interval"/>
|
||||
<div>
|
||||
<field name="recurring_interval" class="oe_inline" attrs="{'required': [('recurring_invoices', '=', True)]}"/>
|
||||
<field name="recurring_rule_type" class="oe_inline" attrs="{'required': [('recurring_invoices', '=', True)]}"/>
|
||||
</div>
|
||||
<field name="recurring_next_date"/>
|
||||
</group>
|
||||
<label for="recurring_invoice_line_ids" attrs="{'invisible': [('recurring_invoices','=',False)]}"/>
|
||||
<div attrs="{'invisible': [('recurring_invoices','=',False)]}">
|
||||
<field name="recurring_invoice_line_ids">
|
||||
<tree string="Account Analytic Lines" editable="bottom">
|
||||
<field name="product_id" on_change="product_id_change(product_id, uom_id, quantity, name, parent.partner_id, price_unit, parent.pricelist_id, parent.company_id)"/>
|
||||
<field name="name"/>
|
||||
<field name="quantity"/>
|
||||
<field name="uom_id"/>
|
||||
<field name="price_unit"/>
|
||||
<field name="price_subtotal"/>
|
||||
</tree>
|
||||
</field>
|
||||
</div>
|
||||
</xpath>
|
||||
</field>
|
||||
</record>
|
||||
|
|
|
@ -33,9 +33,11 @@
|
|||
|
||||
<record id="sale.sale_order_6" model="sale.order">
|
||||
<field name="project_id" ref="account.analytic_super_product_trainings"/>
|
||||
<field name="partner_id" ref="base.res_partner_1"/>
|
||||
</record>
|
||||
|
||||
<record id="account.analytic_support_internal" model="account.analytic.account">
|
||||
<field name="partner_id" ref="base.res_partner_1"/>
|
||||
<field name="use_timesheets">True</field>
|
||||
<field name="invoice_on_timesheets">True</field>
|
||||
<field name="hours_qtt_est">100</field>
|
||||
|
|
|
@ -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
|
|
|
@ -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!"
|
Loading…
Reference in New Issue