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.

467 lines
17 KiB

  1. # Copyright 2004-2010 OpenERP SA
  2. # Copyright 2014 Angel Moya <angel.moya@domatix.com>
  3. # Copyright 2015 Pedro M. Baeza <pedro.baeza@tecnativa.com>
  4. # Copyright 2016-2018 Carlos Dauden <carlos.dauden@tecnativa.com>
  5. # Copyright 2016-2017 LasLabs Inc.
  6. # Copyright 2018 ACSONE SA/NV
  7. # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
  8. from odoo import api, fields, models
  9. from odoo.exceptions import ValidationError
  10. from odoo.tools.translate import _
  11. class ContractContract(models.Model):
  12. _name = 'contract.contract'
  13. _description = "Contract"
  14. _order = 'code, name asc'
  15. _inherit = [
  16. 'mail.thread',
  17. 'mail.activity.mixin',
  18. 'contract.abstract.contract',
  19. ]
  20. active = fields.Boolean(
  21. default=True,
  22. )
  23. code = fields.Char(
  24. string="Reference",
  25. )
  26. group_id = fields.Many2one(
  27. string="Group",
  28. comodel_name='account.analytic.account',
  29. ondelete='restrict',
  30. )
  31. currency_id = fields.Many2one(
  32. related="company_id.currency_id",
  33. string="Currency",
  34. readonly=True,
  35. )
  36. contract_template_id = fields.Many2one(
  37. string='Contract Template', comodel_name='contract.template'
  38. )
  39. contract_line_ids = fields.One2many(
  40. string='Contract lines',
  41. comodel_name='contract.line',
  42. inverse_name='contract_id',
  43. copy=True,
  44. )
  45. user_id = fields.Many2one(
  46. comodel_name='res.users',
  47. string='Responsible',
  48. index=True,
  49. default=lambda self: self.env.user,
  50. )
  51. create_invoice_visibility = fields.Boolean(
  52. compute='_compute_create_invoice_visibility'
  53. )
  54. recurring_next_date = fields.Date(
  55. compute='_compute_recurring_next_date',
  56. string='Date of Next Invoice',
  57. store=True,
  58. )
  59. date_end = fields.Date(
  60. compute='_compute_date_end', string='Date End', store=True
  61. )
  62. payment_term_id = fields.Many2one(
  63. comodel_name='account.payment.term', string='Payment Terms', index=True
  64. )
  65. invoice_count = fields.Integer(compute="_compute_invoice_count")
  66. fiscal_position_id = fields.Many2one(
  67. comodel_name='account.fiscal.position',
  68. string='Fiscal Position',
  69. ondelete='restrict',
  70. )
  71. invoice_partner_id = fields.Many2one(
  72. string="Invoicing contact",
  73. comodel_name='res.partner',
  74. ondelete='restrict',
  75. )
  76. partner_id = fields.Many2one(
  77. comodel_name='res.partner',
  78. inverse='_inverse_partner_id',
  79. required=True
  80. )
  81. commercial_partner_id = fields.Many2one(
  82. 'res.partner',
  83. related='partner_id.commercial_partner_id',
  84. store=True,
  85. string='Commercial Entity',
  86. index=True
  87. )
  88. tag_ids = fields.Many2many(comodel_name="contract.tag", string="Tags")
  89. @api.multi
  90. def _inverse_partner_id(self):
  91. for rec in self:
  92. if not rec.invoice_partner_id:
  93. rec.invoice_partner_id = rec.partner_id.address_get(
  94. ['invoice']
  95. )['invoice']
  96. @api.multi
  97. def _get_related_invoices(self):
  98. self.ensure_one()
  99. invoices = (
  100. self.env['account.invoice.line']
  101. .search(
  102. [
  103. (
  104. 'contract_line_id',
  105. 'in',
  106. self.contract_line_ids.ids,
  107. )
  108. ]
  109. )
  110. .mapped('invoice_id')
  111. )
  112. invoices |= self.env['account.invoice'].search(
  113. [('old_contract_id', '=', self.id)]
  114. )
  115. return invoices
  116. @api.multi
  117. def _compute_invoice_count(self):
  118. for rec in self:
  119. rec.invoice_count = len(rec._get_related_invoices())
  120. @api.multi
  121. def action_show_invoices(self):
  122. self.ensure_one()
  123. tree_view_ref = (
  124. 'account.invoice_supplier_tree'
  125. if self.contract_type == 'purchase'
  126. else 'account.invoice_tree_with_onboarding'
  127. )
  128. form_view_ref = (
  129. 'account.invoice_supplier_form'
  130. if self.contract_type == 'purchase'
  131. else 'account.invoice_form'
  132. )
  133. tree_view = self.env.ref(tree_view_ref, raise_if_not_found=False)
  134. form_view = self.env.ref(form_view_ref, raise_if_not_found=False)
  135. action = {
  136. 'type': 'ir.actions.act_window',
  137. 'name': 'Invoices',
  138. 'res_model': 'account.invoice',
  139. 'view_type': 'form',
  140. 'view_mode': 'tree,kanban,form,calendar,pivot,graph,activity',
  141. 'domain': [('id', 'in', self._get_related_invoices().ids)],
  142. }
  143. if tree_view and form_view:
  144. action['views'] = [(tree_view.id, 'tree'), (form_view.id, 'form')]
  145. return action
  146. @api.depends('contract_line_ids.date_end')
  147. def _compute_date_end(self):
  148. for contract in self:
  149. contract.date_end = False
  150. date_end = contract.contract_line_ids.mapped('date_end')
  151. if date_end and all(date_end):
  152. contract.date_end = max(date_end)
  153. @api.depends(
  154. 'contract_line_ids.recurring_next_date',
  155. 'contract_line_ids.is_canceled',
  156. )
  157. def _compute_recurring_next_date(self):
  158. for contract in self:
  159. recurring_next_date = contract.contract_line_ids.filtered(
  160. lambda l: l.recurring_next_date and not l.is_canceled
  161. ).mapped('recurring_next_date')
  162. if recurring_next_date:
  163. contract.recurring_next_date = min(recurring_next_date)
  164. @api.depends('contract_line_ids.create_invoice_visibility')
  165. def _compute_create_invoice_visibility(self):
  166. for contract in self:
  167. contract.create_invoice_visibility = any(
  168. contract.contract_line_ids.mapped(
  169. 'create_invoice_visibility'
  170. )
  171. )
  172. @api.onchange('contract_template_id')
  173. def _onchange_contract_template_id(self):
  174. """Update the contract fields with that of the template.
  175. Take special consideration with the `contract_line_ids`,
  176. which must be created using the data from the contract lines. Cascade
  177. deletion ensures that any errant lines that are created are also
  178. deleted.
  179. """
  180. contract_template_id = self.contract_template_id
  181. if not contract_template_id:
  182. return
  183. for field_name, field in contract_template_id._fields.items():
  184. if field.name == 'contract_line_ids':
  185. lines = self._convert_contract_lines(contract_template_id)
  186. self.contract_line_ids += lines
  187. elif not any(
  188. (
  189. field.compute,
  190. field.related,
  191. field.automatic,
  192. field.readonly,
  193. field.company_dependent,
  194. field.name in self.NO_SYNC,
  195. )
  196. ):
  197. self[field_name] = self.contract_template_id[field_name]
  198. @api.onchange('partner_id')
  199. def _onchange_partner_id(self):
  200. self.pricelist_id = self.partner_id.property_product_pricelist.id
  201. self.fiscal_position_id = self.partner_id.property_account_position_id
  202. if self.contract_type == 'purchase':
  203. self.payment_term_id = \
  204. self.partner_id.property_supplier_payment_term_id
  205. else:
  206. self.payment_term_id = \
  207. self.partner_id.property_payment_term_id
  208. self.invoice_partner_id = self.partner_id.address_get(['invoice'])[
  209. 'invoice'
  210. ]
  211. return {
  212. 'domain': {
  213. 'invoice_partner_id': [
  214. '|',
  215. ('id', 'parent_of', self.partner_id.id),
  216. ('id', 'child_of', self.partner_id.id),
  217. ]
  218. }
  219. }
  220. @api.multi
  221. def _convert_contract_lines(self, contract):
  222. self.ensure_one()
  223. new_lines = self.env['contract.line']
  224. contract_line_model = self.env['contract.line']
  225. for contract_line in contract.contract_line_ids:
  226. vals = contract_line._convert_to_write(contract_line.read()[0])
  227. # Remove template link field
  228. vals.pop('contract_template_id', False)
  229. vals['date_start'] = fields.Date.context_today(contract_line)
  230. vals['recurring_next_date'] = fields.Date.context_today(
  231. contract_line
  232. )
  233. new_lines += contract_line_model.new(vals)
  234. new_lines._onchange_date_start()
  235. new_lines._onchange_is_auto_renew()
  236. return new_lines
  237. @api.multi
  238. def _prepare_invoice(self, date_invoice, journal=None):
  239. self.ensure_one()
  240. if not journal:
  241. journal = (
  242. self.journal_id
  243. if self.journal_id.type == self.contract_type
  244. else self.env['account.journal'].search(
  245. [
  246. ('type', '=', self.contract_type),
  247. ('company_id', '=', self.company_id.id),
  248. ],
  249. limit=1,
  250. )
  251. )
  252. if not journal:
  253. raise ValidationError(
  254. _("Please define a %s journal for the company '%s'.")
  255. % (self.contract_type, self.company_id.name or '')
  256. )
  257. currency = (
  258. self.pricelist_id.currency_id
  259. or self.partner_id.property_product_pricelist.currency_id
  260. or self.company_id.currency_id
  261. )
  262. invoice_type = 'out_invoice'
  263. if self.contract_type == 'purchase':
  264. invoice_type = 'in_invoice'
  265. vinvoice = self.env['account.invoice'].new({
  266. 'partner_id': self.invoice_partner_id.id,
  267. 'type': invoice_type,
  268. })
  269. vinvoice._onchange_partner_id()
  270. invoice_vals = vinvoice._convert_to_write(vinvoice._cache)
  271. invoice_vals.update({
  272. 'name': self.code,
  273. 'currency_id': currency.id,
  274. 'date_invoice': date_invoice,
  275. 'journal_id': journal.id,
  276. 'origin': self.name,
  277. 'company_id': self.company_id.id,
  278. 'user_id': self.user_id.id,
  279. })
  280. if self.payment_term_id:
  281. invoice_vals['payment_term_id'] = self.payment_term_id.id
  282. if self.fiscal_position_id:
  283. invoice_vals['fiscal_position_id'] = self.fiscal_position_id.id
  284. return invoice_vals
  285. @api.multi
  286. def action_contract_send(self):
  287. self.ensure_one()
  288. template = self.env.ref('contract.email_contract_template', False)
  289. compose_form = self.env.ref('mail.email_compose_message_wizard_form')
  290. ctx = dict(
  291. default_model='contract.contract',
  292. default_res_id=self.id,
  293. default_use_template=bool(template),
  294. default_template_id=template and template.id or False,
  295. default_composition_mode='comment',
  296. )
  297. return {
  298. 'name': _('Compose Email'),
  299. 'type': 'ir.actions.act_window',
  300. 'view_type': 'form',
  301. 'view_mode': 'form',
  302. 'res_model': 'mail.compose.message',
  303. 'views': [(compose_form.id, 'form')],
  304. 'view_id': compose_form.id,
  305. 'target': 'new',
  306. 'context': ctx,
  307. }
  308. @api.model
  309. def _finalize_invoice_values(self, invoice_values):
  310. """
  311. This method adds the missing values in the invoice lines dictionaries.
  312. If no account on the product, the invoice lines account is
  313. taken from the invoice's journal in _onchange_product_id
  314. This code is not in finalize_creation_from_contract because it's
  315. not possible to create an invoice line with no account
  316. :param invoice_values: dictionary (invoice values)
  317. :return: updated dictionary (invoice values)
  318. """
  319. # If no account on the product, the invoice lines account is
  320. # taken from the invoice's journal in _onchange_product_id
  321. # This code is not in finalize_creation_from_contract because it's
  322. # not possible to create an invoice line with no account
  323. new_invoice = self.env['account.invoice'].new(invoice_values)
  324. for invoice_line in new_invoice.invoice_line_ids:
  325. name = invoice_line.name
  326. account_analytic_id = invoice_line.account_analytic_id
  327. price_unit = invoice_line.price_unit
  328. invoice_line.invoice_id = new_invoice
  329. invoice_line._onchange_product_id()
  330. invoice_line.update(
  331. {
  332. 'name': name,
  333. 'account_analytic_id': account_analytic_id,
  334. 'price_unit': price_unit,
  335. }
  336. )
  337. return new_invoice._convert_to_write(new_invoice._cache)
  338. @api.model
  339. def _finalize_invoice_creation(self, invoices):
  340. invoices.compute_taxes()
  341. @api.model
  342. def _finalize_and_create_invoices(self, invoices_values):
  343. """
  344. This method:
  345. - finalizes the invoices values (onchange's...)
  346. - creates the invoices
  347. - finalizes the created invoices (onchange's, tax computation...)
  348. :param invoices_values: list of dictionaries (invoices values)
  349. :return: created invoices (account.invoice)
  350. """
  351. if isinstance(invoices_values, dict):
  352. invoices_values = [invoices_values]
  353. final_invoices_values = []
  354. for invoice_values in invoices_values:
  355. final_invoices_values.append(
  356. self._finalize_invoice_values(invoice_values)
  357. )
  358. invoices = self.env['account.invoice'].create(final_invoices_values)
  359. self._finalize_invoice_creation(invoices)
  360. return invoices
  361. @api.model
  362. def _get_contracts_to_invoice_domain(self, date_ref=None):
  363. """
  364. This method builds the domain to use to find all
  365. contracts (contract.contract) to invoice.
  366. :param date_ref: optional reference date to use instead of today
  367. :return: list (domain) usable on contract.contract
  368. """
  369. domain = []
  370. if not date_ref:
  371. date_ref = fields.Date.context_today(self)
  372. domain.extend([('recurring_next_date', '<=', date_ref)])
  373. return domain
  374. @api.multi
  375. def _get_lines_to_invoice(self, date_ref):
  376. """
  377. This method fetches and returns the lines to invoice on the contract
  378. (self), based on the given date.
  379. :param date_ref: date used as reference date to find lines to invoice
  380. :return: contract lines (contract.line recordset)
  381. """
  382. self.ensure_one()
  383. return self.contract_line_ids.filtered(
  384. lambda l: not l.is_canceled
  385. and l.recurring_next_date
  386. and l.recurring_next_date <= date_ref
  387. )
  388. @api.multi
  389. def _prepare_recurring_invoices_values(self, date_ref=False):
  390. """
  391. This method builds the list of invoices values to create, based on
  392. the lines to invoice of the contracts in self.
  393. !!! The date of next invoice (recurring_next_date) is updated here !!!
  394. :return: list of dictionaries (invoices values)
  395. """
  396. invoices_values = []
  397. for contract in self:
  398. if not date_ref:
  399. date_ref = contract.recurring_next_date
  400. if not date_ref:
  401. # this use case is possible when recurring_create_invoice is
  402. # called for a finished contract
  403. continue
  404. contract_lines = contract._get_lines_to_invoice(date_ref)
  405. if not contract_lines:
  406. continue
  407. invoice_values = contract._prepare_invoice(date_ref)
  408. for line in contract_lines:
  409. invoice_values.setdefault('invoice_line_ids', [])
  410. invoice_line_values = line._prepare_invoice_line(
  411. invoice_id=False
  412. )
  413. if invoice_line_values:
  414. invoice_values['invoice_line_ids'].append(
  415. (0, 0, invoice_line_values)
  416. )
  417. invoices_values.append(invoice_values)
  418. contract_lines._update_recurring_next_date()
  419. return invoices_values
  420. @api.multi
  421. def recurring_create_invoice(self):
  422. """
  423. This method triggers the creation of the next invoices of the contracts
  424. even if their next invoicing date is in the future.
  425. """
  426. return self._recurring_create_invoice()
  427. @api.multi
  428. def _recurring_create_invoice(self, date_ref=False):
  429. invoices_values = self._prepare_recurring_invoices_values(date_ref)
  430. return self._finalize_and_create_invoices(invoices_values)
  431. @api.model
  432. def cron_recurring_create_invoice(self):
  433. domain = self._get_contracts_to_invoice_domain()
  434. contracts_to_invoice = self.search(domain)
  435. date_ref = fields.Date.context_today(contracts_to_invoice)
  436. contracts_to_invoice._recurring_create_invoice(date_ref)