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.

374 lines
14 KiB

  1. # -*- coding: utf-8 -*-
  2. ##############################################################################
  3. #
  4. # Author: Vincent Renaville, ported by Joel Grand-Guillaume
  5. # Copyright 2010-2012 Camptocamp SA
  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. from openerp.osv import orm, fields
  22. class AccountHoursBlock(orm.Model):
  23. _name = "account.hours.block"
  24. _inherit = ['mail.thread']
  25. def _get_last_action(self, cr, uid, ids, name, arg, context=None):
  26. """ Return the last analytic line date for an invoice"""
  27. res = {}
  28. for block in self.browse(cr, uid, ids, context=context):
  29. cr.execute("SELECT max(al.date) FROM account_analytic_line AS al"
  30. " WHERE al.invoice_id = %s", (block.invoice_id.id,))
  31. fetch_res = cr.fetchone()
  32. res[block.id] = fetch_res[0] if fetch_res else False
  33. return res
  34. def _compute_hours(self, cr, uid, ids, fields, args, context=None):
  35. """Return a dict of [id][fields]"""
  36. if isinstance(ids, (int, long)):
  37. ids = [ids]
  38. result = {}
  39. aal_obj = self.pool.get('account.analytic.line')
  40. for block in self.browse(cr, uid, ids, context=context):
  41. result[block.id] = {'amount_hours_block': 0.0,
  42. 'amount_hours_block_done': 0.0}
  43. # Compute hours bought
  44. for line in block.invoice_id.invoice_line:
  45. hours_bought = 0.0
  46. if line.product_id:
  47. # We will now calculate the product_quantity
  48. factor = line.uos_id.factor
  49. if factor == 0.0:
  50. factor = 1.0
  51. amount = line.quantity
  52. hours_bought += (amount / factor)
  53. result[block.id]['amount_hours_block'] += hours_bought
  54. # Compute hours spent
  55. hours_used = 0.0
  56. # Get ids of analytic line generated from
  57. # timesheet associated to the current block
  58. cr.execute("SELECT al.id "
  59. "FROM account_analytic_line AS al, "
  60. " account_analytic_journal AS aj "
  61. "WHERE aj.id = al.journal_id "
  62. "AND aj.type = 'general' "
  63. "AND al.invoice_id = %s", (block.invoice_id.id,))
  64. res_line_ids = cr.fetchall()
  65. line_ids = [l[0] for l in res_line_ids] if res_line_ids else []
  66. for line in aal_obj.browse(cr, uid, line_ids, context=context):
  67. factor = 1.0
  68. if line.product_uom_id and line.product_uom_id.factor != 0.0:
  69. factor = line.product_uom_id.factor
  70. factor_invoicing = 1.0
  71. if line.to_invoice and line.to_invoice.factor != 0.0:
  72. factor_invoicing = 1.0 - line.to_invoice.factor / 100
  73. hours_used += ((line.unit_amount / factor) * factor_invoicing)
  74. result[block.id]['amount_hours_block_done'] = hours_used
  75. return result
  76. def _compute_amount(self, cr, uid, ids, fields, args, context=None):
  77. if context is None:
  78. context = {}
  79. result = {}
  80. aal_obj = self.pool.get('account.analytic.line')
  81. pricelist_obj = self.pool.get('product.pricelist')
  82. for block in self.browse(cr, uid, ids, context=context):
  83. result[block.id] = {'amount_hours_block': 0.0,
  84. 'amount_hours_block_done': 0.0}
  85. # Compute amount bought
  86. for line in block.invoice_id.invoice_line:
  87. amount_bought = 0.0
  88. if line.product_id:
  89. ## We will now calculate the product_quantity
  90. factor = line.uos_id.factor
  91. if factor == 0.0:
  92. factor = 1.0
  93. amount = line.quantity * line.price_unit
  94. amount_bought += (amount / factor)
  95. result[block.id]['amount_hours_block'] += amount_bought
  96. # Compute total amount
  97. # Get ids of analytic line generated from timesheet associated to current block
  98. cr.execute("SELECT al.id FROM account_analytic_line AS al,"
  99. " account_analytic_journal AS aj"
  100. " WHERE aj.id = al.journal_id"
  101. " AND aj.type='general'"
  102. " AND al.invoice_id = %s", (block.invoice_id.id,))
  103. res_line_ids = cr.fetchall()
  104. line_ids = [l[0] for l in res_line_ids] if res_line_ids else []
  105. total_amount = 0.0
  106. for line in aal_obj.browse(cr, uid, line_ids, context=context):
  107. factor_invoicing = 1.0
  108. if line.to_invoice and line.to_invoice.factor != 0.0:
  109. factor_invoicing = 1.0 - line.to_invoice.factor / 100
  110. ctx = dict(context, uom=line.product_uom_id.id)
  111. amount = pricelist_obj.price_get(
  112. cr, uid,
  113. [line.account_id.pricelist_id.id],
  114. line.product_id.id,
  115. line.unit_amount or 1.0,
  116. line.account_id.partner_id.id or False,
  117. ctx)[line.account_id.pricelist_id.id]
  118. total_amount += amount * line.unit_amount * factor_invoicing
  119. result[block.id]['amount_hours_block_done'] += total_amount
  120. return result
  121. def _compute(self, cr, uid, ids, fields, args, context=None):
  122. result = {}
  123. block_per_types = {}
  124. for block in self.browse(cr, uid, ids, context=context):
  125. block_per_types.setdefault(block.type, []).append(block.id)
  126. for block_type in block_per_types:
  127. if block_type:
  128. func = getattr(self, "_compute_%s" % block_type)
  129. result.update(func(cr, uid, ids, fields, args, context=context))
  130. for block in result:
  131. result[block]['amount_hours_block_delta'] = \
  132. result[block]['amount_hours_block'] - \
  133. result[block]['amount_hours_block_done']
  134. return result
  135. def _get_analytic_line(self, cr, uid, ids, context=None):
  136. invoice_ids = []
  137. an_lines_obj = self.pool.get('account.analytic.line')
  138. block_obj = self.pool.get('account.hours.block')
  139. for line in an_lines_obj.browse(cr, uid, ids, context=context):
  140. if line.invoice_id:
  141. invoice_ids.append(line.invoice_id.id)
  142. return block_obj.search(
  143. cr, uid, [('invoice_id', 'in', invoice_ids)], context=context)
  144. def _get_invoice(self, cr, uid, ids, context=None):
  145. block_ids = set()
  146. inv_obj = self.pool.get('account.invoice')
  147. for invoice in inv_obj.browse(cr, uid, ids, context=context):
  148. block_ids.update([inv.id for inv in invoice.account_hours_block_ids])
  149. return list(block_ids)
  150. def action_send_block(self, cr, uid, ids, context=None):
  151. """Open a form to send by email. Return an action dict."""
  152. assert len(ids) == 1, '''\
  153. This option should only be used for a single ID at a time.'''
  154. ir_model_data = self.pool.get('ir.model.data')
  155. try:
  156. template_id = ir_model_data.get_object_reference(
  157. cr, uid, 'analytic_hours_block', 'email_template_hours_block'
  158. )[1]
  159. except ValueError:
  160. template_id = False
  161. try:
  162. compose_form_id = ir_model_data.get_object_reference(
  163. cr, uid, 'mail', 'email_compose_message_wizard_form'
  164. )[1]
  165. except ValueError:
  166. compose_form_id = False
  167. ctx = {
  168. 'default_model': self._name,
  169. 'default_res_id': ids[0],
  170. 'default_use_template': bool(template_id),
  171. 'default_template_id': template_id,
  172. 'default_composition_mode': 'comment',
  173. }
  174. return {
  175. 'type': 'ir.actions.act_window',
  176. 'view_type': 'form',
  177. 'view_mode': 'form',
  178. 'res_model': 'mail.compose.message',
  179. 'views': [(compose_form_id, 'form')],
  180. 'view_id': compose_form_id,
  181. 'target': 'new',
  182. 'context': ctx,
  183. }
  184. _recompute_triggers = {
  185. 'account.hours.block': (lambda self, cr, uid, ids, c=None:
  186. ids, ['invoice_id', 'type'], 10),
  187. 'account.invoice': (_get_invoice, ['analytic_line_ids'], 10),
  188. 'account.analytic.line': (
  189. _get_analytic_line,
  190. ['product_uom_id', 'unit_amount', 'to_invoice', 'invoice_id'],
  191. 10),
  192. }
  193. _columns = {
  194. 'amount_hours_block': fields.function(
  195. _compute,
  196. type='float',
  197. string='Quantity / Amount bought',
  198. store=_recompute_triggers,
  199. multi='amount_hours_block_delta',
  200. help="Amount bought by the customer. "
  201. "This amount is expressed in the base Unit of Measure "
  202. "(factor=1.0)"),
  203. 'amount_hours_block_done': fields.function(
  204. _compute,
  205. type='float',
  206. string='Quantity / Amount used',
  207. store=_recompute_triggers,
  208. multi='amount_hours_block_delta',
  209. help="Amount done by the staff. "
  210. "This amount is expressed in the base Unit of Measure "
  211. "(factor=1.0)"),
  212. 'amount_hours_block_delta': fields.function(
  213. _compute,
  214. type='float',
  215. string='Difference',
  216. store=_recompute_triggers,
  217. multi='amount_hours_block_delta',
  218. help="Difference between bought and used. "
  219. "This amount is expressed in the base Unit of Measure "
  220. "(factor=1.0)"),
  221. 'last_action_date': fields.function(
  222. _get_last_action,
  223. type='date',
  224. string='Last action date',
  225. help="Date of the last analytic line linked to the invoice "
  226. "related to this block hours."),
  227. 'close_date': fields.date('Closed Date'),
  228. 'invoice_id': fields.many2one(
  229. 'account.invoice',
  230. 'Invoice',
  231. ondelete='cascade',
  232. required=True),
  233. 'type': fields.selection(
  234. [('hours', 'Hours'),
  235. ('amount', 'Amount')],
  236. string='Type of Block',
  237. required=True,
  238. help="The block is based on the quantity of hours "
  239. "or on the amount."),
  240. # Invoices related infos
  241. 'date_invoice': fields.related(
  242. 'invoice_id', 'date_invoice',
  243. type="date",
  244. string="Invoice Date",
  245. store=True,
  246. readonly=True),
  247. 'user_id': fields.related(
  248. 'invoice_id', 'user_id',
  249. type="many2one",
  250. relation="res.users",
  251. string="Salesman",
  252. store=True,
  253. readonly=True),
  254. 'partner_id': fields.related(
  255. 'invoice_id', 'partner_id',
  256. type="many2one",
  257. relation="res.partner",
  258. string="Partner",
  259. store=True,
  260. readonly=True),
  261. 'name': fields.related(
  262. 'invoice_id', 'name',
  263. type="char",
  264. string="Description",
  265. store=True,
  266. readonly=True),
  267. 'number': fields.related(
  268. 'invoice_id', 'number',
  269. type="char",
  270. string="Number",
  271. store=True,
  272. readonly=True),
  273. 'journal_id': fields.related(
  274. 'invoice_id', 'journal_id',
  275. type="many2one",
  276. relation="account.journal",
  277. string="Journal",
  278. store=True,
  279. readonly=True),
  280. 'period_id': fields.related(
  281. 'invoice_id', 'period_id',
  282. type="many2one",
  283. relation="account.period",
  284. string="Period",
  285. store=True,
  286. readonly=True),
  287. 'company_id': fields.related(
  288. 'invoice_id', 'company_id',
  289. type="many2one",
  290. relation="res.company",
  291. string="Company",
  292. store=True,
  293. readonly=True),
  294. 'currency_id': fields.related(
  295. 'invoice_id', 'currency_id',
  296. type="many2one",
  297. relation="res.currency",
  298. string="Currency",
  299. store=True,
  300. readonly=True),
  301. 'residual': fields.related(
  302. 'invoice_id', 'residual',
  303. type="float",
  304. string="Residual",
  305. store=True,
  306. readonly=True),
  307. 'amount_total': fields.related(
  308. 'invoice_id', 'amount_total',
  309. type="float",
  310. string="Total",
  311. store=True,
  312. readonly=True),
  313. 'department_id': fields.related(
  314. 'invoice_id', 'department_id',
  315. type='many2one',
  316. relation='hr.department',
  317. string='Department',
  318. store=True,
  319. readonly=True),
  320. 'state': fields.related(
  321. 'invoice_id', 'state',
  322. type='selection',
  323. selection=[
  324. ('draft', 'Draft'),
  325. ('proforma', 'Pro-forma'),
  326. ('proforma2', 'Pro-forma'),
  327. ('open', 'Open'),
  328. ('paid', 'Paid'),
  329. ('cancel', 'Cancelled'),
  330. ],
  331. string='State',
  332. readonly=True,
  333. store=True),
  334. }
  335. ############################################################################
  336. ## Add hours blocks on invoice
  337. ############################################################################
  338. class AccountInvoice(orm.Model):
  339. _inherit = 'account.invoice'
  340. _columns = {
  341. 'account_hours_block_ids': fields.one2many(
  342. 'account.hours.block',
  343. 'invoice_id',
  344. string='Hours Block')
  345. }