sale_order_line.py 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264
  1. # -*- coding: utf-8 -*-
  2. # Part of Odoo. See LICENSE file for full copyright and licensing details.
  3. from dateutil.relativedelta import relativedelta
  4. from odoo import api, fields, models, _
  5. from odoo.exceptions import UserError
  6. from odoo.tools import float_compare
  7. from odoo.tools.misc import get_lang
  8. class SaleOrderLine(models.Model):
  9. _inherit = 'sale.order.line'
  10. purchase_line_ids = fields.One2many('purchase.order.line', 'sale_line_id', string="Generated Purchase Lines", readonly=True, help="Purchase line generated by this Sales item on order confirmation, or when the quantity was increased.")
  11. purchase_line_count = fields.Integer("Number of generated purchase items", compute='_compute_purchase_count')
  12. @api.depends('purchase_line_ids')
  13. def _compute_purchase_count(self):
  14. database_data = self.env['purchase.order.line'].sudo().read_group([('sale_line_id', 'in', self.ids)], ['sale_line_id'], ['sale_line_id'])
  15. mapped_data = dict([(db['sale_line_id'][0], db['sale_line_id_count']) for db in database_data])
  16. for line in self:
  17. line.purchase_line_count = mapped_data.get(line.id, 0)
  18. @api.onchange('product_uom_qty')
  19. def _onchange_service_product_uom_qty(self):
  20. if self.state == 'sale' and self.product_id.type == 'service' and self.product_id.service_to_purchase:
  21. if self.product_uom_qty < self._origin.product_uom_qty:
  22. if self.product_uom_qty < self.qty_delivered:
  23. return {}
  24. warning_mess = {
  25. 'title': _('Ordered quantity decreased!'),
  26. 'message': _('You are decreasing the ordered quantity! Do not forget to manually update the purchase order if needed.'),
  27. }
  28. return {'warning': warning_mess}
  29. return {}
  30. # --------------------------
  31. # CRUD
  32. # --------------------------
  33. @api.model_create_multi
  34. def create(self, values):
  35. lines = super(SaleOrderLine, self).create(values)
  36. # Do not generate purchase when expense SO line since the product is already delivered
  37. lines.filtered(
  38. lambda line: line.state == 'sale' and not line.is_expense
  39. )._purchase_service_generation()
  40. return lines
  41. def write(self, values):
  42. increased_lines = None
  43. decreased_lines = None
  44. increased_values = {}
  45. decreased_values = {}
  46. if 'product_uom_qty' in values:
  47. precision = self.env['decimal.precision'].precision_get('Product Unit of Measure')
  48. increased_lines = self.sudo().filtered(lambda r: r.product_id.service_to_purchase and r.purchase_line_count and float_compare(r.product_uom_qty, values['product_uom_qty'], precision_digits=precision) == -1)
  49. decreased_lines = self.sudo().filtered(lambda r: r.product_id.service_to_purchase and r.purchase_line_count and float_compare(r.product_uom_qty, values['product_uom_qty'], precision_digits=precision) == 1)
  50. increased_values = {line.id: line.product_uom_qty for line in increased_lines}
  51. decreased_values = {line.id: line.product_uom_qty for line in decreased_lines}
  52. result = super(SaleOrderLine, self).write(values)
  53. if increased_lines:
  54. increased_lines._purchase_increase_ordered_qty(values['product_uom_qty'], increased_values)
  55. if decreased_lines:
  56. decreased_lines._purchase_decrease_ordered_qty(values['product_uom_qty'], decreased_values)
  57. return result
  58. # --------------------------
  59. # Business Methods
  60. # --------------------------
  61. def _purchase_decrease_ordered_qty(self, new_qty, origin_values):
  62. """ Decrease the quantity from SO line will add a next acitivities on the related purchase order
  63. :param new_qty: new quantity (lower than the current one on SO line), expressed
  64. in UoM of SO line.
  65. :param origin_values: map from sale line id to old value for the ordered quantity (dict)
  66. """
  67. purchase_to_notify_map = {} # map PO -> set(SOL)
  68. last_purchase_lines = self.env['purchase.order.line'].search([('sale_line_id', 'in', self.ids)])
  69. for purchase_line in last_purchase_lines:
  70. purchase_to_notify_map.setdefault(purchase_line.order_id, self.env['sale.order.line'])
  71. purchase_to_notify_map[purchase_line.order_id] |= purchase_line.sale_line_id
  72. # create next activity
  73. for purchase_order, sale_lines in purchase_to_notify_map.items():
  74. render_context = {
  75. 'sale_lines': sale_lines,
  76. 'sale_orders': sale_lines.mapped('order_id'),
  77. 'origin_values': origin_values,
  78. }
  79. purchase_order._activity_schedule_with_view('mail.mail_activity_data_warning',
  80. user_id=purchase_order.user_id.id or self.env.uid,
  81. views_or_xmlid='sale_purchase.exception_purchase_on_sale_quantity_decreased',
  82. render_context=render_context)
  83. def _purchase_increase_ordered_qty(self, new_qty, origin_values):
  84. """ Increase the quantity on the related purchase lines
  85. :param new_qty: new quantity (higher than the current one on SO line), expressed
  86. in UoM of SO line.
  87. :param origin_values: map from sale line id to old value for the ordered quantity (dict)
  88. """
  89. for line in self:
  90. last_purchase_line = self.env['purchase.order.line'].search([('sale_line_id', '=', line.id)], order='create_date DESC', limit=1)
  91. if last_purchase_line.state in ['draft', 'sent', 'to approve']: # update qty for draft PO lines
  92. quantity = line.product_uom._compute_quantity(new_qty, last_purchase_line.product_uom)
  93. last_purchase_line.write({'product_qty': quantity})
  94. elif last_purchase_line.state in ['purchase', 'done', 'cancel']: # create new PO, by forcing the quantity as the difference from SO line
  95. quantity = line.product_uom._compute_quantity(new_qty - origin_values.get(line.id, 0.0), last_purchase_line.product_uom)
  96. line._purchase_service_create(quantity=quantity)
  97. def _purchase_get_date_order(self, supplierinfo):
  98. """ return the ordered date for the purchase order, computed as : SO commitment date - supplier delay """
  99. commitment_date = fields.Datetime.from_string(self.order_id.commitment_date or fields.Datetime.now())
  100. return commitment_date - relativedelta(days=int(supplierinfo.delay))
  101. def _purchase_service_prepare_order_values(self, supplierinfo):
  102. """ Returns the values to create the purchase order from the current SO line.
  103. :param supplierinfo: record of product.supplierinfo
  104. :rtype: dict
  105. """
  106. self.ensure_one()
  107. partner_supplier = supplierinfo.partner_id
  108. fpos = self.env['account.fiscal.position'].sudo()._get_fiscal_position(partner_supplier)
  109. date_order = self._purchase_get_date_order(supplierinfo)
  110. return {
  111. 'partner_id': partner_supplier.id,
  112. 'partner_ref': partner_supplier.ref,
  113. 'company_id': self.company_id.id,
  114. 'currency_id': partner_supplier.property_purchase_currency_id.id or self.env.company.currency_id.id,
  115. 'dest_address_id': False, # False since only supported in stock
  116. 'origin': self.order_id.name,
  117. 'payment_term_id': partner_supplier.property_supplier_payment_term_id.id,
  118. 'date_order': date_order,
  119. 'fiscal_position_id': fpos.id,
  120. }
  121. def _purchase_service_prepare_line_values(self, purchase_order, quantity=False):
  122. """ Returns the values to create the purchase order line from the current SO line.
  123. :param purchase_order: record of purchase.order
  124. :rtype: dict
  125. :param quantity: the quantity to force on the PO line, expressed in SO line UoM
  126. """
  127. self.ensure_one()
  128. # compute quantity from SO line UoM
  129. product_quantity = self.product_uom_qty
  130. if quantity:
  131. product_quantity = quantity
  132. purchase_qty_uom = self.product_uom._compute_quantity(product_quantity, self.product_id.uom_po_id)
  133. # determine vendor (real supplier, sharing the same partner as the one from the PO, but with more accurate informations like validity, quantity, ...)
  134. # Note: one partner can have multiple supplier info for the same product
  135. supplierinfo = self.product_id._select_seller(
  136. partner_id=purchase_order.partner_id,
  137. quantity=purchase_qty_uom,
  138. date=purchase_order.date_order and purchase_order.date_order.date(), # and purchase_order.date_order[:10],
  139. uom_id=self.product_id.uom_po_id
  140. )
  141. supplier_taxes = self.product_id.supplier_taxes_id.filtered(lambda t: t.company_id.id == self.company_id.id)
  142. taxes = purchase_order.fiscal_position_id.map_tax(supplier_taxes)
  143. # compute unit price
  144. price_unit = 0.0
  145. product_ctx = {
  146. 'lang': get_lang(self.env, purchase_order.partner_id.lang).code,
  147. 'company_id': purchase_order.company_id,
  148. }
  149. if supplierinfo:
  150. price_unit = self.env['account.tax'].sudo()._fix_tax_included_price_company(
  151. supplierinfo.price, supplier_taxes, taxes, self.company_id)
  152. if purchase_order.currency_id and supplierinfo.currency_id != purchase_order.currency_id:
  153. price_unit = supplierinfo.currency_id._convert(price_unit, purchase_order.currency_id, purchase_order.company_id, fields.Date.context_today(self))
  154. product_ctx.update({'seller_id': supplierinfo.id})
  155. else:
  156. product_ctx.update({'partner_id': purchase_order.partner_id.id})
  157. product = self.product_id.with_context(**product_ctx)
  158. name = product.display_name
  159. if product.description_purchase:
  160. name += '\n' + product.description_purchase
  161. return {
  162. 'name': name,
  163. 'product_qty': purchase_qty_uom,
  164. 'product_id': self.product_id.id,
  165. 'product_uom': self.product_id.uom_po_id.id,
  166. 'price_unit': price_unit,
  167. 'date_planned': fields.Date.from_string(purchase_order.date_order) + relativedelta(days=int(supplierinfo.delay)),
  168. 'taxes_id': [(6, 0, taxes.ids)],
  169. 'order_id': purchase_order.id,
  170. 'sale_line_id': self.id,
  171. }
  172. def _retrieve_purchase_partner(self):
  173. """ In case we want to explicitely name a partner from whom we want to buy or receive products
  174. """
  175. self.ensure_one()
  176. return False
  177. def _purchase_service_create(self, quantity=False):
  178. """ On Sales Order confirmation, some lines (services ones) can create a purchase order line and maybe a purchase order.
  179. If a line should create a RFQ, it will check for existing PO. If no one is find, the SO line will create one, then adds
  180. a new PO line. The created purchase order line will be linked to the SO line.
  181. :param quantity: the quantity to force on the PO line, expressed in SO line UoM
  182. """
  183. PurchaseOrder = self.env['purchase.order']
  184. supplier_po_map = {}
  185. sale_line_purchase_map = {}
  186. for line in self:
  187. line = line.with_company(line.company_id)
  188. # determine vendor of the order (take the first matching company and product)
  189. suppliers = line.product_id._select_seller(partner_id=line._retrieve_purchase_partner(), quantity=line.product_uom_qty, uom_id=line.product_uom)
  190. if not suppliers:
  191. raise UserError(_("There is no vendor associated to the product %s. Please define a vendor for this product.") % (line.product_id.display_name,))
  192. supplierinfo = suppliers[0]
  193. partner_supplier = supplierinfo.partner_id
  194. # determine (or create) PO
  195. purchase_order = supplier_po_map.get(partner_supplier.id)
  196. if not purchase_order:
  197. purchase_order = PurchaseOrder.search([
  198. ('partner_id', '=', partner_supplier.id),
  199. ('state', '=', 'draft'),
  200. ('company_id', '=', line.company_id.id),
  201. ], limit=1)
  202. if not purchase_order:
  203. values = line._purchase_service_prepare_order_values(supplierinfo)
  204. purchase_order = PurchaseOrder.with_context(mail_create_nosubscribe=True).create(values)
  205. else: # update origin of existing PO
  206. so_name = line.order_id.name
  207. origins = []
  208. if purchase_order.origin:
  209. origins = purchase_order.origin.split(', ') + origins
  210. if so_name not in origins:
  211. origins += [so_name]
  212. purchase_order.write({
  213. 'origin': ', '.join(origins)
  214. })
  215. supplier_po_map[partner_supplier.id] = purchase_order
  216. # add a PO line to the PO
  217. values = line._purchase_service_prepare_line_values(purchase_order, quantity=quantity)
  218. purchase_line = line.env['purchase.order.line'].create(values)
  219. # link the generated purchase to the SO line
  220. sale_line_purchase_map.setdefault(line, line.env['purchase.order.line'])
  221. sale_line_purchase_map[line] |= purchase_line
  222. return sale_line_purchase_map
  223. def _purchase_service_generation(self):
  224. """ Create a Purchase for the first time from the sale line. If the SO line already created a PO, it
  225. will not create a second one.
  226. """
  227. sale_line_purchase_map = {}
  228. for line in self:
  229. # Do not regenerate PO line if the SO line has already created one in the past (SO cancel/reconfirmation case)
  230. if line.product_id.service_to_purchase and not line.purchase_line_count:
  231. result = line._purchase_service_create()
  232. sale_line_purchase_map.update(result)
  233. return sale_line_purchase_map