You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

375 lines
18 KiB

  1. # -*- coding: utf-8 -*-
  2. ##############################################################################
  3. #
  4. # OpenERP, Open Source Management Solution
  5. # Copyright (C) 2004-2010 Tiny SPRL (<http://tiny.be>).
  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. import time
  22. from openerp.osv import fields, osv
  23. from openerp.tools.translate import _
  24. class hr_timesheet_invoice_factor(osv.osv):
  25. _name = "hr_timesheet_invoice.factor"
  26. _description = "Invoice Rate"
  27. _order = 'factor'
  28. _columns = {
  29. 'name': fields.char('Internal Name', required=True, translate=True),
  30. 'customer_name': fields.char('Name', help="Label for the customer"),
  31. 'factor': fields.float('Discount (%)', required=True, help="Discount in percentage"),
  32. }
  33. _defaults = {
  34. 'factor': lambda *a: 0.0,
  35. }
  36. class account_analytic_account(osv.osv):
  37. def _invoiced_calc(self, cr, uid, ids, name, arg, context=None):
  38. obj_invoice = self.pool.get('account.invoice')
  39. res = {}
  40. cr.execute('SELECT account_id as account_id, l.invoice_id '
  41. 'FROM hr_analytic_timesheet h LEFT JOIN account_analytic_line l '
  42. 'ON (h.line_id=l.id) '
  43. 'WHERE l.account_id = ANY(%s)', (ids,))
  44. account_to_invoice_map = {}
  45. for rec in cr.dictfetchall():
  46. account_to_invoice_map.setdefault(rec['account_id'], []).append(rec['invoice_id'])
  47. for account in self.browse(cr, uid, ids, context=context):
  48. invoice_ids = filter(None, list(set(account_to_invoice_map.get(account.id, []))))
  49. for invoice in obj_invoice.browse(cr, uid, invoice_ids, context=context):
  50. res.setdefault(account.id, 0.0)
  51. res[account.id] += invoice.amount_untaxed
  52. for id in ids:
  53. res[id] = round(res.get(id, 0.0),2)
  54. return res
  55. _inherit = "account.analytic.account"
  56. _columns = {
  57. 'pricelist_id': fields.many2one('product.pricelist', 'Pricelist',
  58. help="The product to invoice is defined on the employee form, the price will be deducted by this pricelist on the product."),
  59. 'amount_max': fields.float('Max. Invoice Price',
  60. help="Keep empty if this contract is not limited to a total fixed price."),
  61. 'amount_invoiced': fields.function(_invoiced_calc, string='Invoiced Amount',
  62. help="Total invoiced"),
  63. 'to_invoice': fields.many2one('hr_timesheet_invoice.factor', 'Timesheet Invoicing Ratio',
  64. help="You usually invoice 100% of the timesheets. But if you mix fixed price and timesheet invoicing, you may use another ratio. For instance, if you do a 20% advance invoice (fixed price, based on a sales order), you should invoice the rest on timesheet with a 80% ratio."),
  65. }
  66. def on_change_partner_id(self, cr, uid, ids, partner_id, name, context=None):
  67. res = super(account_analytic_account, self).on_change_partner_id(cr, uid, ids, partner_id, name, context=context)
  68. if partner_id:
  69. part = self.pool.get('res.partner').browse(cr, uid, partner_id, context=context)
  70. pricelist = part.property_product_pricelist and part.property_product_pricelist.id or False
  71. if pricelist:
  72. res['value']['pricelist_id'] = pricelist
  73. return res
  74. def set_close(self, cr, uid, ids, context=None):
  75. return self.write(cr, uid, ids, {'state': 'close'}, context=context)
  76. def set_cancel(self, cr, uid, ids, context=None):
  77. return self.write(cr, uid, ids, {'state': 'cancelled'}, context=context)
  78. def set_open(self, cr, uid, ids, context=None):
  79. return self.write(cr, uid, ids, {'state': 'open'}, context=context)
  80. def set_pending(self, cr, uid, ids, context=None):
  81. return self.write(cr, uid, ids, {'state': 'pending'}, context=context)
  82. class account_analytic_line(osv.osv):
  83. _inherit = 'account.analytic.line'
  84. _columns = {
  85. 'invoice_id': fields.many2one('account.invoice', 'Invoice', ondelete="set null", copy=False),
  86. 'to_invoice': fields.many2one('hr_timesheet_invoice.factor', 'Invoiceable', help="It allows to set the discount while making invoice, keep empty if the activities should not be invoiced."),
  87. }
  88. def _default_journal(self, cr, uid, context=None):
  89. proxy = self.pool.get('hr.employee')
  90. record_ids = proxy.search(cr, uid, [('user_id', '=', uid)], context=context)
  91. if record_ids:
  92. employee = proxy.browse(cr, uid, record_ids[0], context=context)
  93. return employee.journal_id and employee.journal_id.id or False
  94. return False
  95. def _default_general_account(self, cr, uid, context=None):
  96. proxy = self.pool.get('hr.employee')
  97. record_ids = proxy.search(cr, uid, [('user_id', '=', uid)], context=context)
  98. if record_ids:
  99. employee = proxy.browse(cr, uid, record_ids[0], context=context)
  100. if employee.product_id and employee.product_id.property_account_income:
  101. return employee.product_id.property_account_income.id
  102. return False
  103. _defaults = {
  104. 'journal_id' : _default_journal,
  105. 'general_account_id' : _default_general_account,
  106. }
  107. def write(self, cr, uid, ids, vals, context=None):
  108. #self._check_inv(cr, uid, ids, vals)
  109. return super(account_analytic_line,self).write(cr, uid, ids, vals,
  110. context=context)
  111. def _check_inv(self, cr, uid, ids, vals):
  112. select = ids
  113. if isinstance(select, (int, long)):
  114. select = [ids]
  115. if ( not vals.has_key('invoice_id')) or vals['invoice_id' ] == False:
  116. for line in self.browse(cr, uid, select):
  117. if line.invoice_id:
  118. raise osv.except_osv(_('Error!'),
  119. _('You cannot modify an invoiced analytic line!'))
  120. return True
  121. def _get_invoice_price(self, cr, uid, account, product_id, user_id, qty, context = {}):
  122. pro_price_obj = self.pool.get('product.pricelist')
  123. if account.pricelist_id:
  124. pl = account.pricelist_id.id
  125. price = pro_price_obj.price_get(cr,uid,[pl], product_id, qty or 1.0, account.partner_id.id, context=context)[pl]
  126. else:
  127. price = 0.0
  128. return price
  129. def _prepare_cost_invoice(self, cr, uid, partner, company_id, currency_id, analytic_lines, context=None):
  130. """ returns values used to create main invoice from analytic lines"""
  131. account_payment_term_obj = self.pool['account.payment.term']
  132. invoice_name = analytic_lines[0].account_id.name
  133. date_due = False
  134. if partner.property_payment_term:
  135. pterm_list = account_payment_term_obj.compute(cr, uid,
  136. partner.property_payment_term.id, value=1,
  137. date_ref=time.strftime('%Y-%m-%d'))
  138. if pterm_list:
  139. pterm_list = [line[0] for line in pterm_list]
  140. pterm_list.sort()
  141. date_due = pterm_list[-1]
  142. return {
  143. 'name': "%s - %s" % (time.strftime('%d/%m/%Y'), invoice_name),
  144. 'partner_id': partner.id,
  145. 'company_id': company_id,
  146. 'payment_term': partner.property_payment_term.id or False,
  147. 'account_id': partner.property_account_receivable.id,
  148. 'currency_id': currency_id,
  149. 'date_due': date_due,
  150. 'fiscal_position': partner.property_account_position.id
  151. }
  152. def _prepare_cost_invoice_line(self, cr, uid, invoice_id, product_id, uom, user_id,
  153. factor_id, account, analytic_lines, journal_type, data, context=None):
  154. product_obj = self.pool['product.product']
  155. uom_context = dict(context or {}, uom=uom)
  156. total_price = sum(l.amount for l in analytic_lines)
  157. total_qty = sum(l.unit_amount for l in analytic_lines)
  158. if data.get('product'):
  159. # force product, use its public price
  160. if isinstance(data['product'], (tuple, list)):
  161. product_id = data['product'][0]
  162. else:
  163. product_id = data['product']
  164. unit_price = self._get_invoice_price(cr, uid, account, product_id, user_id, total_qty, uom_context)
  165. elif journal_type == 'general' and product_id:
  166. # timesheets, use sale price
  167. unit_price = self._get_invoice_price(cr, uid, account, product_id, user_id, total_qty, uom_context)
  168. else:
  169. # expenses, using price from amount field
  170. unit_price = total_price*-1.0 / total_qty
  171. factor = self.pool['hr_timesheet_invoice.factor'].browse(cr, uid, factor_id, context=uom_context)
  172. factor_name = factor.customer_name or ''
  173. curr_invoice_line = {
  174. 'price_unit': unit_price,
  175. 'quantity': total_qty,
  176. 'product_id': product_id,
  177. 'discount': factor.factor,
  178. 'invoice_id': invoice_id,
  179. 'name': factor_name,
  180. 'uos_id': uom,
  181. 'account_analytic_id': account.id,
  182. }
  183. if product_id:
  184. product = product_obj.browse(cr, uid, product_id, context=uom_context)
  185. factor_name = product_obj.name_get(cr, uid, [product_id], context=uom_context)[0][1]
  186. if factor.customer_name:
  187. factor_name += ' - ' + factor.customer_name
  188. general_account = product.property_account_income or product.categ_id.property_account_income_categ
  189. if not general_account:
  190. raise osv.except_osv(_('Error!'), _("Configuration Error!") + '\n' + _("Please define income account for product '%s'.") % product.name)
  191. general_account = account.partner_id.property_account_position.map_account(general_account)
  192. taxes = product.taxes_id or general_account.tax_ids
  193. tax = self.pool['account.fiscal.position'].map_tax(cr, uid, account.partner_id.property_account_position, taxes, context=context)
  194. curr_invoice_line.update({
  195. 'invoice_line_tax_id': [(6, 0, tax)],
  196. 'name': factor_name,
  197. 'invoice_line_tax_id': [(6, 0, tax)],
  198. 'account_id': general_account.id,
  199. })
  200. note = []
  201. for line in analytic_lines:
  202. # set invoice_line_note
  203. details = []
  204. if data.get('date', False):
  205. details.append(line['date'])
  206. if data.get('time', False):
  207. if line['product_uom_id']:
  208. details.append("%s %s" % (line.unit_amount, line.product_uom_id.name))
  209. else:
  210. details.append("%s" % (line['unit_amount'], ))
  211. if data.get('name', False):
  212. details.append(line['name'])
  213. if details:
  214. note.append(u' - '.join(map(lambda x: unicode(x) or '', details)))
  215. if note:
  216. curr_invoice_line['name'] += "\n" + ("\n".join(map(lambda x: unicode(x) or '', note)))
  217. return curr_invoice_line
  218. def invoice_cost_create(self, cr, uid, ids, data=None, context=None):
  219. invoice_obj = self.pool.get('account.invoice')
  220. invoice_line_obj = self.pool.get('account.invoice.line')
  221. analytic_line_obj = self.pool.get('account.analytic.line')
  222. invoices = []
  223. if context is None:
  224. context = {}
  225. if data is None:
  226. data = {}
  227. # use key (partner/account, company, currency)
  228. # creates one invoice per key
  229. invoice_grouping = {}
  230. currency_id = False
  231. # prepare for iteration on journal and accounts
  232. for line in self.browse(cr, uid, ids, context=context):
  233. key = (line.account_id.id,
  234. line.account_id.company_id.id,
  235. line.account_id.pricelist_id.currency_id.id)
  236. invoice_grouping.setdefault(key, []).append(line)
  237. for (key_id, company_id, currency_id), analytic_lines in invoice_grouping.items():
  238. # key_id is an account.analytic.account
  239. account = analytic_lines[0].account_id
  240. partner = account.partner_id # will be the same for every line
  241. if (not partner) or not (currency_id):
  242. raise osv.except_osv(_('Error!'), _('Contract incomplete. Please fill in the Customer and Pricelist fields for %s.') % (account.name))
  243. curr_invoice = self._prepare_cost_invoice(cr, uid, partner, company_id, currency_id, analytic_lines, context=context)
  244. invoice_context = dict(context,
  245. lang=partner.lang,
  246. force_company=company_id, # set force_company in context so the correct product properties are selected (eg. income account)
  247. company_id=company_id) # set company_id in context, so the correct default journal will be selected
  248. last_invoice = invoice_obj.create(cr, uid, curr_invoice, context=invoice_context)
  249. invoices.append(last_invoice)
  250. # use key (product, uom, user, invoiceable, analytic account, journal type)
  251. # creates one invoice line per key
  252. invoice_lines_grouping = {}
  253. for analytic_line in analytic_lines:
  254. account = analytic_line.account_id
  255. if not analytic_line.to_invoice:
  256. raise osv.except_osv(_('Error!'), _('Trying to invoice non invoiceable line for %s.') % (analytic_line.product_id.name))
  257. key = (analytic_line.product_id.id,
  258. analytic_line.product_uom_id.id,
  259. analytic_line.user_id.id,
  260. analytic_line.to_invoice.id,
  261. analytic_line.account_id,
  262. analytic_line.journal_id.type)
  263. # We want to retrieve the data in the partner language for the invoice creation
  264. analytic_line = analytic_line_obj.browse(cr, uid , [line.id for line in analytic_line], context=invoice_context)
  265. invoice_lines_grouping.setdefault(key, []).append(analytic_line)
  266. # finally creates the invoice line
  267. for (product_id, uom, user_id, factor_id, account, journal_type), lines_to_invoice in invoice_lines_grouping.items():
  268. curr_invoice_line = self._prepare_cost_invoice_line(cr, uid, last_invoice,
  269. product_id, uom, user_id, factor_id, account, lines_to_invoice,
  270. journal_type, data, context=invoice_context)
  271. invoice_line_obj.create(cr, uid, curr_invoice_line, context=context)
  272. self.write(cr, uid, [l.id for l in analytic_lines], {'invoice_id': last_invoice}, context=context)
  273. invoice_obj.button_reset_taxes(cr, uid, [last_invoice], context)
  274. return invoices
  275. class hr_analytic_timesheet(osv.osv):
  276. _inherit = "hr.analytic.timesheet"
  277. def on_change_account_id(self, cr, uid, ids, account_id, user_id=False):
  278. res = super(hr_analytic_timesheet, self).on_change_account_id(
  279. cr, uid, ids, account_id, context=user_id)
  280. if not account_id:
  281. return res
  282. res.setdefault('value',{})
  283. acc = self.pool.get('account.analytic.account').browse(cr, uid, account_id)
  284. st = acc.to_invoice.id
  285. res['value']['to_invoice'] = st or False
  286. if acc.state=='pending':
  287. res['warning'] = {
  288. 'title': _('Warning'),
  289. 'message': _('The analytic account is in pending state.\nYou should not work on this account !')
  290. }
  291. return res
  292. class account_invoice(osv.osv):
  293. _inherit = "account.invoice"
  294. def _get_analytic_lines(self, cr, uid, ids, context=None):
  295. iml = super(account_invoice, self)._get_analytic_lines(cr, uid, ids, context=context)
  296. inv = self.browse(cr, uid, ids, context=context)[0]
  297. if inv.type == 'in_invoice':
  298. obj_analytic_account = self.pool.get('account.analytic.account')
  299. for il in iml:
  300. if il['account_analytic_id']:
  301. # *-* browse (or refactor to avoid read inside the loop)
  302. to_invoice = obj_analytic_account.read(cr, uid, [il['account_analytic_id']], ['to_invoice'], context=context)[0]['to_invoice']
  303. if to_invoice:
  304. il['analytic_lines'][0][2]['to_invoice'] = to_invoice[0]
  305. return iml
  306. class account_move_line(osv.osv):
  307. _inherit = "account.move.line"
  308. def create_analytic_lines(self, cr, uid, ids, context=None):
  309. res = super(account_move_line, self).create_analytic_lines(cr, uid, ids,context=context)
  310. analytic_line_obj = self.pool.get('account.analytic.line')
  311. for move_line in self.browse(cr, uid, ids, context=context):
  312. #For customer invoice, link analytic line to the invoice so it is not proposed for invoicing in Bill Tasks Work
  313. invoice_id = move_line.invoice and move_line.invoice.type in ('out_invoice','out_refund') and move_line.invoice.id or False
  314. for line in move_line.analytic_lines:
  315. analytic_line_obj.write(cr, uid, line.id, {
  316. 'invoice_id': invoice_id,
  317. 'to_invoice': line.account_id.to_invoice and line.account_id.to_invoice.id or False
  318. }, context=context)
  319. return res
  320. # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: