1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
|
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from dateutil.relativedelta import relativedelta
from odoo import api, fields, models, _
from odoo.exceptions import UserError
from odoo.tools import float_compare
class SaleOrder(models.Model):
_inherit = 'sale.order'
purchase_order_count = fields.Integer(
"Number of Purchase Order Generated",
compute='_compute_purchase_order_count',
groups='purchase.group_purchase_user')
@api.depends('order_line.purchase_line_ids.order_id')
def _compute_purchase_order_count(self):
for order in self:
order.purchase_order_count = len(self._get_purchase_orders())
def _action_confirm(self):
result = super(SaleOrder, self)._action_confirm()
for order in self:
order.order_line.sudo()._purchase_service_generation()
return result
def action_cancel(self):
result = super(SaleOrder, self).action_cancel()
# When a sale person cancel a SO, he might not have the rights to write
# on PO. But we need the system to create an activity on the PO (so 'write'
# access), hence the `sudo`.
self.sudo()._activity_cancel_on_purchase()
return result
def action_view_purchase_orders(self):
self.ensure_one()
purchase_order_ids = self._get_purchase_orders().ids
action = {
'res_model': 'purchase.order',
'type': 'ir.actions.act_window',
}
if len(purchase_order_ids) == 1:
action.update({
'view_mode': 'form',
'res_id': purchase_order_ids[0],
})
else:
action.update({
'name': _("Purchase Order generated from %s", self.name),
'domain': [('id', 'in', purchase_order_ids)],
'view_mode': 'tree,form',
})
return action
def _get_purchase_orders(self):
return self.order_line.purchase_line_ids.order_id
def _activity_cancel_on_purchase(self):
""" If some SO are cancelled, we need to put an activity on their generated purchase. If sale lines of
different sale orders impact different purchase, we only want one activity to be attached.
"""
purchase_to_notify_map = {} # map PO -> recordset of SOL as {purchase.order: set(sale.orde.liner)}
purchase_order_lines = self.env['purchase.order.line'].search([('sale_line_id', 'in', self.mapped('order_line').ids), ('state', '!=', 'cancel')])
for purchase_line in purchase_order_lines:
purchase_to_notify_map.setdefault(purchase_line.order_id, self.env['sale.order.line'])
purchase_to_notify_map[purchase_line.order_id] |= purchase_line.sale_line_id
for purchase_order, sale_order_lines in purchase_to_notify_map.items():
purchase_order._activity_schedule_with_view('mail.mail_activity_data_warning',
user_id=purchase_order.user_id.id or self.env.uid,
views_or_xmlid='sale_purchase.exception_purchase_on_sale_cancellation',
render_context={
'sale_orders': sale_order_lines.mapped('order_id'),
'sale_order_lines': sale_order_lines,
})
class SaleOrderLine(models.Model):
_inherit = 'sale.order.line'
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.")
purchase_line_count = fields.Integer("Number of generated purchase items", compute='_compute_purchase_count')
@api.depends('purchase_line_ids')
def _compute_purchase_count(self):
database_data = self.env['purchase.order.line'].sudo().read_group([('sale_line_id', 'in', self.ids)], ['sale_line_id'], ['sale_line_id'])
mapped_data = dict([(db['sale_line_id'][0], db['sale_line_id_count']) for db in database_data])
for line in self:
line.purchase_line_count = mapped_data.get(line.id, 0)
@api.onchange('product_uom_qty')
def _onchange_service_product_uom_qty(self):
if self.state == 'sale' and self.product_id.type == 'service' and self.product_id.service_to_purchase:
if self.product_uom_qty < self._origin.product_uom_qty:
if self.product_uom_qty < self.qty_delivered:
return {}
warning_mess = {
'title': _('Ordered quantity decreased!'),
'message': _('You are decreasing the ordered quantity! Do not forget to manually update the purchase order if needed.'),
}
return {'warning': warning_mess}
return {}
# --------------------------
# CRUD
# --------------------------
@api.model_create_multi
def create(self, values):
lines = super(SaleOrderLine, self).create(values)
# Do not generate purchase when expense SO line since the product is already delivered
lines.filtered(
lambda line: line.state == 'sale' and not line.is_expense
)._purchase_service_generation()
return lines
def write(self, values):
increased_lines = None
decreased_lines = None
increased_values = {}
decreased_values = {}
if 'product_uom_qty' in values:
precision = self.env['decimal.precision'].precision_get('Product Unit of Measure')
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)
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)
increased_values = {line.id: line.product_uom_qty for line in increased_lines}
decreased_values = {line.id: line.product_uom_qty for line in decreased_lines}
result = super(SaleOrderLine, self).write(values)
if increased_lines:
increased_lines._purchase_increase_ordered_qty(values['product_uom_qty'], increased_values)
if decreased_lines:
decreased_lines._purchase_decrease_ordered_qty(values['product_uom_qty'], decreased_values)
return result
# --------------------------
# Business Methods
# --------------------------
def _purchase_decrease_ordered_qty(self, new_qty, origin_values):
""" Decrease the quantity from SO line will add a next acitivities on the related purchase order
:param new_qty: new quantity (lower than the current one on SO line), expressed
in UoM of SO line.
:param origin_values: map from sale line id to old value for the ordered quantity (dict)
"""
purchase_to_notify_map = {} # map PO -> set(SOL)
last_purchase_lines = self.env['purchase.order.line'].search([('sale_line_id', 'in', self.ids)])
for purchase_line in last_purchase_lines:
purchase_to_notify_map.setdefault(purchase_line.order_id, self.env['sale.order.line'])
purchase_to_notify_map[purchase_line.order_id] |= purchase_line.sale_line_id
# create next activity
for purchase_order, sale_lines in purchase_to_notify_map.items():
render_context = {
'sale_lines': sale_lines,
'sale_orders': sale_lines.mapped('order_id'),
'origin_values': origin_values,
}
purchase_order._activity_schedule_with_view('mail.mail_activity_data_warning',
user_id=purchase_order.user_id.id or self.env.uid,
views_or_xmlid='sale_purchase.exception_purchase_on_sale_quantity_decreased',
render_context=render_context)
def _purchase_increase_ordered_qty(self, new_qty, origin_values):
""" Increase the quantity on the related purchase lines
:param new_qty: new quantity (higher than the current one on SO line), expressed
in UoM of SO line.
:param origin_values: map from sale line id to old value for the ordered quantity (dict)
"""
for line in self:
last_purchase_line = self.env['purchase.order.line'].search([('sale_line_id', '=', line.id)], order='create_date DESC', limit=1)
if last_purchase_line.state in ['draft', 'sent', 'to approve']: # update qty for draft PO lines
quantity = line.product_uom._compute_quantity(new_qty, last_purchase_line.product_uom)
last_purchase_line.write({'product_qty': quantity})
elif last_purchase_line.state in ['purchase', 'done', 'cancel']: # create new PO, by forcing the quantity as the difference from SO line
quantity = line.product_uom._compute_quantity(new_qty - origin_values.get(line.id, 0.0), last_purchase_line.product_uom)
line._purchase_service_create(quantity=quantity)
def _purchase_get_date_order(self, supplierinfo):
""" return the ordered date for the purchase order, computed as : SO commitment date - supplier delay """
commitment_date = fields.Datetime.from_string(self.order_id.commitment_date or fields.Datetime.now())
return commitment_date - relativedelta(days=int(supplierinfo.delay))
def _purchase_service_prepare_order_values(self, supplierinfo):
""" Returns the values to create the purchase order from the current SO line.
:param supplierinfo: record of product.supplierinfo
:rtype: dict
"""
self.ensure_one()
partner_supplier = supplierinfo.name
fpos = self.env['account.fiscal.position'].sudo().get_fiscal_position(partner_supplier.id)
date_order = self._purchase_get_date_order(supplierinfo)
return {
'partner_id': partner_supplier.id,
'partner_ref': partner_supplier.ref,
'company_id': self.company_id.id,
'currency_id': partner_supplier.property_purchase_currency_id.id or self.env.company.currency_id.id,
'dest_address_id': False, # False since only supported in stock
'origin': self.order_id.name,
'payment_term_id': partner_supplier.property_supplier_payment_term_id.id,
'date_order': date_order,
'fiscal_position_id': fpos.id,
}
def _purchase_service_prepare_line_values(self, purchase_order, quantity=False):
""" Returns the values to create the purchase order line from the current SO line.
:param purchase_order: record of purchase.order
:rtype: dict
:param quantity: the quantity to force on the PO line, expressed in SO line UoM
"""
self.ensure_one()
# compute quantity from SO line UoM
product_quantity = self.product_uom_qty
if quantity:
product_quantity = quantity
purchase_qty_uom = self.product_uom._compute_quantity(product_quantity, self.product_id.uom_po_id)
# determine vendor (real supplier, sharing the same partner as the one from the PO, but with more accurate informations like validity, quantity, ...)
# Note: one partner can have multiple supplier info for the same product
supplierinfo = self.product_id._select_seller(
partner_id=purchase_order.partner_id,
quantity=purchase_qty_uom,
date=purchase_order.date_order and purchase_order.date_order.date(), # and purchase_order.date_order[:10],
uom_id=self.product_id.uom_po_id
)
fpos = purchase_order.fiscal_position_id
taxes = fpos.map_tax(self.product_id.supplier_taxes_id)
if taxes:
taxes = taxes.filtered(lambda t: t.company_id.id == self.company_id.id)
# compute unit price
price_unit = 0.0
if supplierinfo:
price_unit = self.env['account.tax'].sudo()._fix_tax_included_price_company(supplierinfo.price, self.product_id.supplier_taxes_id, taxes, self.company_id)
if purchase_order.currency_id and supplierinfo.currency_id != purchase_order.currency_id:
price_unit = supplierinfo.currency_id.compute(price_unit, purchase_order.currency_id)
return {
'name': '[%s] %s' % (self.product_id.default_code, self.name) if self.product_id.default_code else self.name,
'product_qty': purchase_qty_uom,
'product_id': self.product_id.id,
'product_uom': self.product_id.uom_po_id.id,
'price_unit': price_unit,
'date_planned': fields.Date.from_string(purchase_order.date_order) + relativedelta(days=int(supplierinfo.delay)),
'taxes_id': [(6, 0, taxes.ids)],
'order_id': purchase_order.id,
'sale_line_id': self.id,
}
def _purchase_service_create(self, quantity=False):
""" On Sales Order confirmation, some lines (services ones) can create a purchase order line and maybe a purchase order.
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
a new PO line. The created purchase order line will be linked to the SO line.
:param quantity: the quantity to force on the PO line, expressed in SO line UoM
"""
PurchaseOrder = self.env['purchase.order']
supplier_po_map = {}
sale_line_purchase_map = {}
for line in self:
line = line.with_company(line.company_id)
# determine vendor of the order (take the first matching company and product)
suppliers = line.product_id._select_seller(quantity=line.product_uom_qty, uom_id=line.product_uom)
if not suppliers:
raise UserError(_("There is no vendor associated to the product %s. Please define a vendor for this product.") % (line.product_id.display_name,))
supplierinfo = suppliers[0]
partner_supplier = supplierinfo.name # yes, this field is not explicit .... it is a res.partner !
# determine (or create) PO
purchase_order = supplier_po_map.get(partner_supplier.id)
if not purchase_order:
purchase_order = PurchaseOrder.search([
('partner_id', '=', partner_supplier.id),
('state', '=', 'draft'),
('company_id', '=', line.company_id.id),
], limit=1)
if not purchase_order:
values = line._purchase_service_prepare_order_values(supplierinfo)
purchase_order = PurchaseOrder.create(values)
else: # update origin of existing PO
so_name = line.order_id.name
origins = []
if purchase_order.origin:
origins = purchase_order.origin.split(', ') + origins
if so_name not in origins:
origins += [so_name]
purchase_order.write({
'origin': ', '.join(origins)
})
supplier_po_map[partner_supplier.id] = purchase_order
# add a PO line to the PO
values = line._purchase_service_prepare_line_values(purchase_order, quantity=quantity)
purchase_line = line.env['purchase.order.line'].create(values)
# link the generated purchase to the SO line
sale_line_purchase_map.setdefault(line, line.env['purchase.order.line'])
sale_line_purchase_map[line] |= purchase_line
return sale_line_purchase_map
def _purchase_service_generation(self):
""" Create a Purchase for the first time from the sale line. If the SO line already created a PO, it
will not create a second one.
"""
sale_line_purchase_map = {}
for line in self:
# Do not regenerate PO line if the SO line has already created one in the past (SO cancel/reconfirmation case)
if line.product_id.service_to_purchase and not line.purchase_line_count:
result = line._purchase_service_create()
sale_line_purchase_map.update(result)
return sale_line_purchase_map
|