blob: e45f098f65b9de7294dbf3a909a7d92848180d01 [file] [log] [blame]
Rohit Waghchaure7b7ceaa2021-05-24 20:11:15 +05301from __future__ import unicode_literals
Chillar Anand915b3432021-09-02 16:44:59 +05302
Rohit Waghchaure7b7ceaa2021-05-24 20:11:15 +05303import copy
Chillar Anand915b3432021-09-02 16:44:59 +05304import unittest
Rohit Waghchaure7b7ceaa2021-05-24 20:11:15 +05305from collections import defaultdict
Chillar Anand915b3432021-09-02 16:44:59 +05306
7import frappe
8from frappe.utils import cint
9
10from erpnext.buying.doctype.purchase_order.purchase_order import (
11 get_materials_from_supplier,
12 make_purchase_invoice,
13 make_purchase_receipt,
14 make_rm_stock_entry,
15)
Rohit Waghchaure7b7ceaa2021-05-24 20:11:15 +053016from erpnext.buying.doctype.purchase_order.test_purchase_order import create_purchase_order
Chillar Anand915b3432021-09-02 16:44:59 +053017from erpnext.manufacturing.doctype.production_plan.test_production_plan import make_bom
18from erpnext.stock.doctype.item.test_item import make_item
19from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos
20from erpnext.stock.doctype.stock_entry.test_stock_entry import make_stock_entry
21
Rohit Waghchaure7b7ceaa2021-05-24 20:11:15 +053022
23class TestSubcontracting(unittest.TestCase):
24 def setUp(self):
25 make_subcontract_items()
26 make_raw_materials()
27 make_bom_for_subcontracted_items()
28
29 def test_po_with_bom(self):
30 '''
31 - Set backflush based on BOM
32 - Create subcontracted PO for the item Subcontracted Item SA1 and add same item two times.
33 - Transfer the components from Stores to Supplier warehouse with batch no and serial nos.
34 - Create purchase receipt against the PO and check serial nos and batch no.
35 '''
36
37 set_backflush_based_on('BOM')
38 item_code = 'Subcontracted Item SA1'
39 items = [{'warehouse': '_Test Warehouse - _TC', 'item_code': item_code, 'qty': 5, 'rate': 100},
40 {'warehouse': '_Test Warehouse - _TC', 'item_code': item_code, 'qty': 6, 'rate': 100}]
41
42 rm_items = [{'item_code': 'Subcontracted SRM Item 1', 'qty': 5},
43 {'item_code': 'Subcontracted SRM Item 2', 'qty': 5},
44 {'item_code': 'Subcontracted SRM Item 3', 'qty': 5},
45 {'item_code': 'Subcontracted SRM Item 1', 'qty': 6},
46 {'item_code': 'Subcontracted SRM Item 2', 'qty': 6},
47 {'item_code': 'Subcontracted SRM Item 3', 'qty': 6}
48 ]
49
50 itemwise_details = make_stock_in_entry(rm_items=rm_items)
51 po = create_purchase_order(rm_items = items, is_subcontracted="Yes",
52 supplier_warehouse="_Test Warehouse 1 - _TC")
53
54 for d in rm_items:
55 d['po_detail'] = po.items[0].name if d.get('qty') == 5 else po.items[1].name
56
57 make_stock_transfer_entry(po_no = po.name, main_item_code=item_code,
58 rm_items=rm_items, itemwise_details=copy.deepcopy(itemwise_details))
59
60 pr1 = make_purchase_receipt(po.name)
61 pr1.submit()
62
63 for key, value in get_supplied_items(pr1).items():
64 transferred_detais = itemwise_details.get(key)
65
66 for field in ['qty', 'serial_no', 'batch_no']:
67 if value.get(field):
68 transfer, consumed = (transferred_detais.get(field), value.get(field))
69 if field == 'serial_no':
70 transfer, consumed = (sorted(transfer), sorted(consumed))
71
72 self.assertEqual(transfer, consumed)
73
74 def test_po_with_material_transfer(self):
75 '''
76 - Set backflush based on Material Transfer
77 - Create subcontracted PO for the item Subcontracted Item SA1 and Subcontracted Item SA5.
78 - Transfer the components from Stores to Supplier warehouse with batch no and serial nos.
79 - Transfer extra item Subcontracted SRM Item 4 for the subcontract item Subcontracted Item SA5.
80 - Create partial purchase receipt against the PO and check serial nos and batch no.
81 '''
82
83 set_backflush_based_on('Material Transferred for Subcontract')
84 items = [{'warehouse': '_Test Warehouse - _TC', 'item_code': 'Subcontracted Item SA1', 'qty': 5, 'rate': 100},
85 {'warehouse': '_Test Warehouse - _TC', 'item_code': 'Subcontracted Item SA5', 'qty': 6, 'rate': 100}]
86
87 rm_items = [{'item_code': 'Subcontracted SRM Item 1', 'qty': 5, 'main_item_code': 'Subcontracted Item SA1'},
88 {'item_code': 'Subcontracted SRM Item 2', 'qty': 5, 'main_item_code': 'Subcontracted Item SA1'},
89 {'item_code': 'Subcontracted SRM Item 3', 'qty': 5, 'main_item_code': 'Subcontracted Item SA1'},
90 {'item_code': 'Subcontracted SRM Item 5', 'qty': 6, 'main_item_code': 'Subcontracted Item SA5'},
91 {'item_code': 'Subcontracted SRM Item 4', 'qty': 6, 'main_item_code': 'Subcontracted Item SA5'}
92 ]
93
94 itemwise_details = make_stock_in_entry(rm_items=rm_items)
95 po = create_purchase_order(rm_items = items, is_subcontracted="Yes",
96 supplier_warehouse="_Test Warehouse 1 - _TC")
97
98 for d in rm_items:
99 d['po_detail'] = po.items[0].name if d.get('qty') == 5 else po.items[1].name
100
101 make_stock_transfer_entry(po_no = po.name,
102 rm_items=rm_items, itemwise_details=copy.deepcopy(itemwise_details))
103
104 pr1 = make_purchase_receipt(po.name)
105 pr1.remove(pr1.items[1])
106 pr1.submit()
107
108 for key, value in get_supplied_items(pr1).items():
109 transferred_detais = itemwise_details.get(key)
110
111 for field in ['qty', 'serial_no', 'batch_no']:
112 if value.get(field):
113 self.assertEqual(value.get(field), transferred_detais.get(field))
114
115 pr2 = make_purchase_receipt(po.name)
116 pr2.submit()
117
118 for key, value in get_supplied_items(pr2).items():
119 transferred_detais = itemwise_details.get(key)
120
121 for field in ['qty', 'serial_no', 'batch_no']:
122 if value.get(field):
123 self.assertEqual(value.get(field), transferred_detais.get(field))
124
125 def test_subcontract_with_same_components_different_fg(self):
126 '''
127 - Set backflush based on Material Transfer
128 - Create subcontracted PO for the item Subcontracted Item SA2 and Subcontracted Item SA3.
129 - Transfer the components from Stores to Supplier warehouse with serial nos.
130 - Transfer extra qty of components for the item Subcontracted Item SA2.
131 - Create partial purchase receipt against the PO and check serial nos and batch no.
132 '''
133
134 set_backflush_based_on('Material Transferred for Subcontract')
135 items = [{'warehouse': '_Test Warehouse - _TC', 'item_code': 'Subcontracted Item SA2', 'qty': 5, 'rate': 100},
136 {'warehouse': '_Test Warehouse - _TC', 'item_code': 'Subcontracted Item SA3', 'qty': 6, 'rate': 100}]
137
138 rm_items = [{'item_code': 'Subcontracted SRM Item 2', 'qty': 6, 'main_item_code': 'Subcontracted Item SA2'},
139 {'item_code': 'Subcontracted SRM Item 2', 'qty': 6, 'main_item_code': 'Subcontracted Item SA3'}
140 ]
141
142 itemwise_details = make_stock_in_entry(rm_items=rm_items)
143 po = create_purchase_order(rm_items = items, is_subcontracted="Yes",
144 supplier_warehouse="_Test Warehouse 1 - _TC")
145
146 for d in rm_items:
147 d['po_detail'] = po.items[0].name if d.get('qty') == 5 else po.items[1].name
148
149 make_stock_transfer_entry(po_no = po.name,
150 rm_items=rm_items, itemwise_details=copy.deepcopy(itemwise_details))
151
152 pr1 = make_purchase_receipt(po.name)
153 pr1.items[0].qty = 3
154 pr1.remove(pr1.items[1])
155 pr1.submit()
156
157 for key, value in get_supplied_items(pr1).items():
158 transferred_detais = itemwise_details.get(key)
159 self.assertEqual(value.qty, 4)
160 self.assertEqual(sorted(value.serial_no), sorted(transferred_detais.get('serial_no')[0:4]))
161
162 pr2 = make_purchase_receipt(po.name)
163 pr2.items[0].qty = 2
164 pr2.remove(pr2.items[1])
165 pr2.submit()
166
167 for key, value in get_supplied_items(pr2).items():
168 transferred_detais = itemwise_details.get(key)
169
170 self.assertEqual(value.qty, 2)
171 self.assertEqual(sorted(value.serial_no), sorted(transferred_detais.get('serial_no')[4:6]))
172
173 pr3 = make_purchase_receipt(po.name)
174 pr3.submit()
175 for key, value in get_supplied_items(pr3).items():
176 transferred_detais = itemwise_details.get(key)
177
178 self.assertEqual(value.qty, 6)
179 self.assertEqual(sorted(value.serial_no), sorted(transferred_detais.get('serial_no')[6:12]))
180
181 def test_return_non_consumed_materials(self):
182 '''
183 - Set backflush based on Material Transfer
184 - Create subcontracted PO for the item Subcontracted Item SA2.
185 - Transfer the components from Stores to Supplier warehouse with serial nos.
186 - Transfer extra qty of component for the subcontracted item Subcontracted Item SA2.
187 - Create purchase receipt for full qty against the PO and change the qty of raw material.
188 - After that return the non consumed material back to the store from supplier's warehouse.
189 '''
190
191 set_backflush_based_on('Material Transferred for Subcontract')
192 items = [{'warehouse': '_Test Warehouse - _TC', 'item_code': 'Subcontracted Item SA2', 'qty': 5, 'rate': 100}]
193 rm_items = [{'item_code': 'Subcontracted SRM Item 2', 'qty': 6, 'main_item_code': 'Subcontracted Item SA2'}]
194
195 itemwise_details = make_stock_in_entry(rm_items=rm_items)
196 po = create_purchase_order(rm_items = items, is_subcontracted="Yes",
197 supplier_warehouse="_Test Warehouse 1 - _TC")
198
199 for d in rm_items:
200 d['po_detail'] = po.items[0].name
201
202 make_stock_transfer_entry(po_no = po.name,
203 rm_items=rm_items, itemwise_details=copy.deepcopy(itemwise_details))
204
205 pr1 = make_purchase_receipt(po.name)
206 pr1.save()
207 pr1.supplied_items[0].consumed_qty = 5
208 pr1.supplied_items[0].serial_no = '\n'.join(sorted(
209 itemwise_details.get('Subcontracted SRM Item 2').get('serial_no')[0:5]
210 ))
211 pr1.submit()
212
213 for key, value in get_supplied_items(pr1).items():
214 transferred_detais = itemwise_details.get(key)
215 self.assertEqual(value.qty, 5)
216 self.assertEqual(sorted(value.serial_no), sorted(transferred_detais.get('serial_no')[0:5]))
217
218 po.load_from_db()
219 self.assertEqual(po.supplied_items[0].consumed_qty, 5)
220 doc = get_materials_from_supplier(po.name, [d.name for d in po.supplied_items])
221 self.assertEqual(doc.items[0].qty, 1)
222 self.assertEqual(doc.items[0].s_warehouse, '_Test Warehouse 1 - _TC')
223 self.assertEqual(doc.items[0].t_warehouse, '_Test Warehouse - _TC')
224 self.assertEqual(get_serial_nos(doc.items[0].serial_no),
225 itemwise_details.get(doc.items[0].item_code)['serial_no'][5:6])
226
227 def test_item_with_batch_based_on_bom(self):
228 '''
229 - Set backflush based on BOM
230 - Create subcontracted PO for the item Subcontracted Item SA4 (has batch no).
231 - Transfer the components from Stores to Supplier warehouse with batch no and serial nos.
232 - Transfer the components in multiple batches.
233 - Create the 3 purchase receipt against the PO and split Subcontracted Items into two batches.
234 - Keep the qty as 2 for Subcontracted Item in the purchase receipt.
235 '''
236
237 set_backflush_based_on('BOM')
238 item_code = 'Subcontracted Item SA4'
239 items = [{'warehouse': '_Test Warehouse - _TC', 'item_code': item_code, 'qty': 10, 'rate': 100}]
240
241 rm_items = [{'item_code': 'Subcontracted SRM Item 1', 'qty': 10},
242 {'item_code': 'Subcontracted SRM Item 2', 'qty': 10},
243 {'item_code': 'Subcontracted SRM Item 3', 'qty': 3},
244 {'item_code': 'Subcontracted SRM Item 3', 'qty': 3},
245 {'item_code': 'Subcontracted SRM Item 3', 'qty': 3},
246 {'item_code': 'Subcontracted SRM Item 3', 'qty': 1}
247 ]
248
249 itemwise_details = make_stock_in_entry(rm_items=rm_items)
250 po = create_purchase_order(rm_items = items, is_subcontracted="Yes",
251 supplier_warehouse="_Test Warehouse 1 - _TC")
252
253 for d in rm_items:
254 d['po_detail'] = po.items[0].name
255
256 make_stock_transfer_entry(po_no = po.name, main_item_code=item_code,
257 rm_items=rm_items, itemwise_details=copy.deepcopy(itemwise_details))
258
259 pr1 = make_purchase_receipt(po.name)
260 pr1.items[0].qty = 2
261 add_second_row_in_pr(pr1)
262 pr1.save()
263 pr1.submit()
264
265 for key, value in get_supplied_items(pr1).items():
266 self.assertEqual(value.qty, 4)
267
268 pr1 = make_purchase_receipt(po.name)
269 pr1.items[0].qty = 2
270 add_second_row_in_pr(pr1)
271 pr1.save()
272 pr1.submit()
273
274 for key, value in get_supplied_items(pr1).items():
275 self.assertEqual(value.qty, 4)
276
277 pr1 = make_purchase_receipt(po.name)
278 pr1.items[0].qty = 2
279 pr1.save()
280 pr1.submit()
281
282 for key, value in get_supplied_items(pr1).items():
283 self.assertEqual(value.qty, 2)
284
285 def test_item_with_batch_based_on_material_transfer(self):
286 '''
287 - Set backflush based on Material Transferred for Subcontract
288 - Create subcontracted PO for the item Subcontracted Item SA4 (has batch no).
289 - Transfer the components from Stores to Supplier warehouse with batch no and serial nos.
290 - Transfer the components in multiple batches with extra 2 qty for the batched item.
291 - Create the 3 purchase receipt against the PO and split Subcontracted Items into two batches.
292 - Keep the qty as 2 for Subcontracted Item in the purchase receipt.
293 - In the first purchase receipt the batched raw materials will be consumed 2 extra qty.
294 '''
295
296 set_backflush_based_on('Material Transferred for Subcontract')
297 item_code = 'Subcontracted Item SA4'
298 items = [{'warehouse': '_Test Warehouse - _TC', 'item_code': item_code, 'qty': 10, 'rate': 100}]
299
300 rm_items = [{'item_code': 'Subcontracted SRM Item 1', 'qty': 10},
301 {'item_code': 'Subcontracted SRM Item 2', 'qty': 10},
302 {'item_code': 'Subcontracted SRM Item 3', 'qty': 3},
303 {'item_code': 'Subcontracted SRM Item 3', 'qty': 3},
304 {'item_code': 'Subcontracted SRM Item 3', 'qty': 3},
305 {'item_code': 'Subcontracted SRM Item 3', 'qty': 3}
306 ]
307
308 itemwise_details = make_stock_in_entry(rm_items=rm_items)
309 po = create_purchase_order(rm_items = items, is_subcontracted="Yes",
310 supplier_warehouse="_Test Warehouse 1 - _TC")
311
312 for d in rm_items:
313 d['po_detail'] = po.items[0].name
314
315 make_stock_transfer_entry(po_no = po.name, main_item_code=item_code,
316 rm_items=rm_items, itemwise_details=copy.deepcopy(itemwise_details))
317
318 pr1 = make_purchase_receipt(po.name)
319 pr1.items[0].qty = 2
320 add_second_row_in_pr(pr1)
321 pr1.save()
322 pr1.submit()
323
324 for key, value in get_supplied_items(pr1).items():
325 qty = 4 if key != 'Subcontracted SRM Item 3' else 6
326 self.assertEqual(value.qty, qty)
327
328 pr1 = make_purchase_receipt(po.name)
329 pr1.items[0].qty = 2
330 add_second_row_in_pr(pr1)
331 pr1.save()
332 pr1.submit()
333
334 for key, value in get_supplied_items(pr1).items():
335 self.assertEqual(value.qty, 4)
336
337 pr1 = make_purchase_receipt(po.name)
338 pr1.items[0].qty = 2
339 pr1.save()
340 pr1.submit()
341
342 for key, value in get_supplied_items(pr1).items():
343 self.assertEqual(value.qty, 2)
344
345 def test_partial_transfer_serial_no_components_based_on_material_transfer(self):
346 '''
347 - Set backflush based on Material Transferred for Subcontract
348 - Create subcontracted PO for the item Subcontracted Item SA2.
349 - Transfer the partial components from Stores to Supplier warehouse with serial nos.
350 - Create partial purchase receipt against the PO and change the qty manually.
351 - Transfer the remaining components from Stores to Supplier warehouse with serial nos.
352 - Create purchase receipt for remaining qty against the PO and change the qty manually.
353 '''
354
355 set_backflush_based_on('Material Transferred for Subcontract')
356 item_code = 'Subcontracted Item SA2'
357 items = [{'warehouse': '_Test Warehouse - _TC', 'item_code': item_code, 'qty': 10, 'rate': 100}]
358
359 rm_items = [{'item_code': 'Subcontracted SRM Item 2', 'qty': 5}]
360
361 itemwise_details = make_stock_in_entry(rm_items=rm_items)
362 po = create_purchase_order(rm_items = items, is_subcontracted="Yes",
363 supplier_warehouse="_Test Warehouse 1 - _TC")
364
365 for d in rm_items:
366 d['po_detail'] = po.items[0].name
367
368 make_stock_transfer_entry(po_no = po.name, main_item_code=item_code,
369 rm_items=rm_items, itemwise_details=copy.deepcopy(itemwise_details))
370
371 pr1 = make_purchase_receipt(po.name)
372 pr1.items[0].qty = 5
373 pr1.save()
374
375 for key, value in get_supplied_items(pr1).items():
376 details = itemwise_details.get(key)
377 self.assertEqual(value.qty, 3)
378 self.assertEqual(sorted(value.serial_no), sorted(details.serial_no[0:3]))
379
380 pr1.load_from_db()
381 pr1.supplied_items[0].consumed_qty = 5
382 pr1.supplied_items[0].serial_no = '\n'.join(itemwise_details[pr1.supplied_items[0].rm_item_code]['serial_no'])
383 pr1.save()
384 pr1.submit()
385
386 for key, value in get_supplied_items(pr1).items():
387 details = itemwise_details.get(key)
388 self.assertEqual(value.qty, details.qty)
389 self.assertEqual(sorted(value.serial_no), sorted(details.serial_no))
390
391 itemwise_details = make_stock_in_entry(rm_items=rm_items)
392 for d in rm_items:
393 d['po_detail'] = po.items[0].name
394
395 make_stock_transfer_entry(po_no = po.name, main_item_code=item_code,
396 rm_items=rm_items, itemwise_details=copy.deepcopy(itemwise_details))
397
398 pr1 = make_purchase_receipt(po.name)
399 pr1.submit()
400
401 for key, value in get_supplied_items(pr1).items():
402 details = itemwise_details.get(key)
403 self.assertEqual(value.qty, details.qty)
404 self.assertEqual(sorted(value.serial_no), sorted(details.serial_no))
405
Rohit Waghchauree5fb2392021-06-18 20:37:42 +0530406 def test_incorrect_serial_no_components_based_on_material_transfer(self):
407 '''
408 - Set backflush based on Material Transferred for Subcontract
409 - Create subcontracted PO for the item Subcontracted Item SA2.
410 - Transfer the serialized componenets to the supplier.
411 - Create purchase receipt and change the serial no which is not transferred.
412 - System should throw the error and not allowed to save the purchase receipt.
413 '''
414
415 set_backflush_based_on('Material Transferred for Subcontract')
416 item_code = 'Subcontracted Item SA2'
417 items = [{'warehouse': '_Test Warehouse - _TC', 'item_code': item_code, 'qty': 10, 'rate': 100}]
418
419 rm_items = [{'item_code': 'Subcontracted SRM Item 2', 'qty': 10}]
420
421 itemwise_details = make_stock_in_entry(rm_items=rm_items)
422 po = create_purchase_order(rm_items = items, is_subcontracted="Yes",
423 supplier_warehouse="_Test Warehouse 1 - _TC")
424
425 for d in rm_items:
426 d['po_detail'] = po.items[0].name
427
428 make_stock_transfer_entry(po_no = po.name, main_item_code=item_code,
429 rm_items=rm_items, itemwise_details=copy.deepcopy(itemwise_details))
430
431 pr1 = make_purchase_receipt(po.name)
432 pr1.save()
433 pr1.supplied_items[0].serial_no = 'ABCD'
434 self.assertRaises(frappe.ValidationError, pr1.save)
435 pr1.delete()
436
Rohit Waghchaure7b7ceaa2021-05-24 20:11:15 +0530437 def test_partial_transfer_batch_based_on_material_transfer(self):
438 '''
439 - Set backflush based on Material Transferred for Subcontract
440 - Create subcontracted PO for the item Subcontracted Item SA6.
441 - Transfer the partial components from Stores to Supplier warehouse with batch.
442 - Create partial purchase receipt against the PO and change the qty manually.
443 - Transfer the remaining components from Stores to Supplier warehouse with batch.
444 - Create purchase receipt for remaining qty against the PO and change the qty manually.
445 '''
446
447 set_backflush_based_on('Material Transferred for Subcontract')
448 item_code = 'Subcontracted Item SA6'
449 items = [{'warehouse': '_Test Warehouse - _TC', 'item_code': item_code, 'qty': 10, 'rate': 100}]
450
451 rm_items = [{'item_code': 'Subcontracted SRM Item 3', 'qty': 5}]
452
453 itemwise_details = make_stock_in_entry(rm_items=rm_items)
454 po = create_purchase_order(rm_items = items, is_subcontracted="Yes",
455 supplier_warehouse="_Test Warehouse 1 - _TC")
456
457 for d in rm_items:
458 d['po_detail'] = po.items[0].name
459
460 make_stock_transfer_entry(po_no = po.name, main_item_code=item_code,
461 rm_items=rm_items, itemwise_details=copy.deepcopy(itemwise_details))
462
463 pr1 = make_purchase_receipt(po.name)
464 pr1.items[0].qty = 5
465 pr1.save()
466
467 transferred_batch_no = ''
468 for key, value in get_supplied_items(pr1).items():
469 details = itemwise_details.get(key)
470 self.assertEqual(value.qty, 3)
471 transferred_batch_no = details.batch_no
472 self.assertEqual(value.batch_no, details.batch_no)
473
474 pr1.load_from_db()
475 pr1.supplied_items[0].consumed_qty = 5
476 pr1.supplied_items[0].batch_no = list(transferred_batch_no.keys())[0]
477 pr1.save()
478 pr1.submit()
479
480 for key, value in get_supplied_items(pr1).items():
481 details = itemwise_details.get(key)
482 self.assertEqual(value.qty, details.qty)
483 self.assertEqual(value.batch_no, details.batch_no)
484
485 itemwise_details = make_stock_in_entry(rm_items=rm_items)
486 for d in rm_items:
487 d['po_detail'] = po.items[0].name
488
489 make_stock_transfer_entry(po_no = po.name, main_item_code=item_code,
490 rm_items=rm_items, itemwise_details=copy.deepcopy(itemwise_details))
491
492 pr1 = make_purchase_receipt(po.name)
493 pr1.submit()
494
495 for key, value in get_supplied_items(pr1).items():
496 details = itemwise_details.get(key)
497 self.assertEqual(value.qty, details.qty)
498 self.assertEqual(value.batch_no, details.batch_no)
499
Rohit Waghchaure110e1522021-06-15 17:29:52 +0530500
501 def test_item_with_batch_based_on_material_transfer_for_purchase_invoice(self):
502 '''
503 - Set backflush based on Material Transferred for Subcontract
504 - Create subcontracted PO for the item Subcontracted Item SA4 (has batch no).
505 - Transfer the components from Stores to Supplier warehouse with batch no and serial nos.
506 - Transfer the components in multiple batches with extra 2 qty for the batched item.
507 - Create the 3 purchase receipt against the PO and split Subcontracted Items into two batches.
508 - Keep the qty as 2 for Subcontracted Item in the purchase receipt.
509 - In the first purchase receipt the batched raw materials will be consumed 2 extra qty.
510 '''
511
512 set_backflush_based_on('Material Transferred for Subcontract')
513 item_code = 'Subcontracted Item SA4'
514 items = [{'warehouse': '_Test Warehouse - _TC', 'item_code': item_code, 'qty': 10, 'rate': 100}]
515
516 rm_items = [{'item_code': 'Subcontracted SRM Item 1', 'qty': 10},
517 {'item_code': 'Subcontracted SRM Item 2', 'qty': 10},
518 {'item_code': 'Subcontracted SRM Item 3', 'qty': 3},
519 {'item_code': 'Subcontracted SRM Item 3', 'qty': 3},
520 {'item_code': 'Subcontracted SRM Item 3', 'qty': 3},
521 {'item_code': 'Subcontracted SRM Item 3', 'qty': 3}
522 ]
523
524 itemwise_details = make_stock_in_entry(rm_items=rm_items)
525 po = create_purchase_order(rm_items = items, is_subcontracted="Yes",
526 supplier_warehouse="_Test Warehouse 1 - _TC")
527
528 for d in rm_items:
529 d['po_detail'] = po.items[0].name
530
531 make_stock_transfer_entry(po_no = po.name, main_item_code=item_code,
532 rm_items=rm_items, itemwise_details=copy.deepcopy(itemwise_details))
533
534 pr1 = make_purchase_invoice(po.name)
535 pr1.update_stock = 1
536 pr1.items[0].qty = 2
537 pr1.items[0].expense_account = 'Stock Adjustment - _TC'
538 add_second_row_in_pr(pr1)
539 pr1.save()
540 pr1.submit()
541
542 for key, value in get_supplied_items(pr1).items():
543 qty = 4 if key != 'Subcontracted SRM Item 3' else 6
544 self.assertEqual(value.qty, qty)
545
546 pr1 = make_purchase_invoice(po.name)
547 pr1.update_stock = 1
548 pr1.items[0].expense_account = 'Stock Adjustment - _TC'
549 pr1.items[0].qty = 2
550 add_second_row_in_pr(pr1)
551 pr1.save()
552 pr1.submit()
553
554 for key, value in get_supplied_items(pr1).items():
555 self.assertEqual(value.qty, 4)
556
557 pr1 = make_purchase_invoice(po.name)
558 pr1.update_stock = 1
559 pr1.items[0].qty = 2
560 pr1.items[0].expense_account = 'Stock Adjustment - _TC'
561 pr1.save()
562 pr1.submit()
563
564 for key, value in get_supplied_items(pr1).items():
565 self.assertEqual(value.qty, 2)
566
567 def test_partial_transfer_serial_no_components_based_on_material_transfer_for_purchase_invoice(self):
568 '''
569 - Set backflush based on Material Transferred for Subcontract
570 - Create subcontracted PO for the item Subcontracted Item SA2.
571 - Transfer the partial components from Stores to Supplier warehouse with serial nos.
572 - Create partial purchase receipt against the PO and change the qty manually.
573 - Transfer the remaining components from Stores to Supplier warehouse with serial nos.
574 - Create purchase receipt for remaining qty against the PO and change the qty manually.
575 '''
576
577 set_backflush_based_on('Material Transferred for Subcontract')
578 item_code = 'Subcontracted Item SA2'
579 items = [{'warehouse': '_Test Warehouse - _TC', 'item_code': item_code, 'qty': 10, 'rate': 100}]
580
581 rm_items = [{'item_code': 'Subcontracted SRM Item 2', 'qty': 5}]
582
583 itemwise_details = make_stock_in_entry(rm_items=rm_items)
584 po = create_purchase_order(rm_items = items, is_subcontracted="Yes",
585 supplier_warehouse="_Test Warehouse 1 - _TC")
586
587 for d in rm_items:
588 d['po_detail'] = po.items[0].name
589
590 make_stock_transfer_entry(po_no = po.name, main_item_code=item_code,
591 rm_items=rm_items, itemwise_details=copy.deepcopy(itemwise_details))
592
593 pr1 = make_purchase_invoice(po.name)
594 pr1.update_stock = 1
595 pr1.items[0].qty = 5
596 pr1.items[0].expense_account = 'Stock Adjustment - _TC'
597 pr1.save()
598
599 for key, value in get_supplied_items(pr1).items():
600 details = itemwise_details.get(key)
601 self.assertEqual(value.qty, 3)
602 self.assertEqual(sorted(value.serial_no), sorted(details.serial_no[0:3]))
603
604 pr1.load_from_db()
605 pr1.supplied_items[0].consumed_qty = 5
606 pr1.supplied_items[0].serial_no = '\n'.join(itemwise_details[pr1.supplied_items[0].rm_item_code]['serial_no'])
607 pr1.save()
608 pr1.submit()
609
610 for key, value in get_supplied_items(pr1).items():
611 details = itemwise_details.get(key)
612 self.assertEqual(value.qty, details.qty)
613 self.assertEqual(sorted(value.serial_no), sorted(details.serial_no))
614
615 itemwise_details = make_stock_in_entry(rm_items=rm_items)
616 for d in rm_items:
617 d['po_detail'] = po.items[0].name
618
619 make_stock_transfer_entry(po_no = po.name, main_item_code=item_code,
620 rm_items=rm_items, itemwise_details=copy.deepcopy(itemwise_details))
621
622 pr1 = make_purchase_invoice(po.name)
623 pr1.update_stock = 1
624 pr1.items[0].expense_account = 'Stock Adjustment - _TC'
625 pr1.submit()
626
627 for key, value in get_supplied_items(pr1).items():
628 details = itemwise_details.get(key)
629 self.assertEqual(value.qty, details.qty)
630 self.assertEqual(sorted(value.serial_no), sorted(details.serial_no))
631
632 def test_partial_transfer_batch_based_on_material_transfer_for_purchase_invoice(self):
633 '''
634 - Set backflush based on Material Transferred for Subcontract
635 - Create subcontracted PO for the item Subcontracted Item SA6.
636 - Transfer the partial components from Stores to Supplier warehouse with batch.
637 - Create partial purchase receipt against the PO and change the qty manually.
638 - Transfer the remaining components from Stores to Supplier warehouse with batch.
639 - Create purchase receipt for remaining qty against the PO and change the qty manually.
640 '''
641
642 set_backflush_based_on('Material Transferred for Subcontract')
643 item_code = 'Subcontracted Item SA6'
644 items = [{'warehouse': '_Test Warehouse - _TC', 'item_code': item_code, 'qty': 10, 'rate': 100}]
645
646 rm_items = [{'item_code': 'Subcontracted SRM Item 3', 'qty': 5}]
647
648 itemwise_details = make_stock_in_entry(rm_items=rm_items)
649 po = create_purchase_order(rm_items = items, is_subcontracted="Yes",
650 supplier_warehouse="_Test Warehouse 1 - _TC")
651
652 for d in rm_items:
653 d['po_detail'] = po.items[0].name
654
655 make_stock_transfer_entry(po_no = po.name, main_item_code=item_code,
656 rm_items=rm_items, itemwise_details=copy.deepcopy(itemwise_details))
657
658 pr1 = make_purchase_invoice(po.name)
659 pr1.update_stock = 1
660 pr1.items[0].qty = 5
661 pr1.items[0].expense_account = 'Stock Adjustment - _TC'
662 pr1.save()
663
664 transferred_batch_no = ''
665 for key, value in get_supplied_items(pr1).items():
666 details = itemwise_details.get(key)
667 self.assertEqual(value.qty, 3)
668 transferred_batch_no = details.batch_no
669 self.assertEqual(value.batch_no, details.batch_no)
670
671 pr1.load_from_db()
672 pr1.supplied_items[0].consumed_qty = 5
673 pr1.supplied_items[0].batch_no = list(transferred_batch_no.keys())[0]
674 pr1.save()
675 pr1.submit()
676
677 for key, value in get_supplied_items(pr1).items():
678 details = itemwise_details.get(key)
679 self.assertEqual(value.qty, details.qty)
680 self.assertEqual(value.batch_no, details.batch_no)
681
682 itemwise_details = make_stock_in_entry(rm_items=rm_items)
683 for d in rm_items:
684 d['po_detail'] = po.items[0].name
685
686 make_stock_transfer_entry(po_no = po.name, main_item_code=item_code,
687 rm_items=rm_items, itemwise_details=copy.deepcopy(itemwise_details))
688
689 pr1 = make_purchase_invoice(po.name)
690 pr1.update_stock = 1
691 pr1.items[0].expense_account = 'Stock Adjustment - _TC'
692 pr1.submit()
693
694 for key, value in get_supplied_items(pr1).items():
695 details = itemwise_details.get(key)
696 self.assertEqual(value.qty, details.qty)
697 self.assertEqual(value.batch_no, details.batch_no)
698
699 def test_item_with_batch_based_on_bom_for_purchase_invoice(self):
700 '''
701 - Set backflush based on BOM
702 - Create subcontracted PO for the item Subcontracted Item SA4 (has batch no).
703 - Transfer the components from Stores to Supplier warehouse with batch no and serial nos.
704 - Transfer the components in multiple batches.
705 - Create the 3 purchase receipt against the PO and split Subcontracted Items into two batches.
706 - Keep the qty as 2 for Subcontracted Item in the purchase receipt.
707 '''
708
709 set_backflush_based_on('BOM')
710 item_code = 'Subcontracted Item SA4'
711 items = [{'warehouse': '_Test Warehouse - _TC', 'item_code': item_code, 'qty': 10, 'rate': 100}]
712
713 rm_items = [{'item_code': 'Subcontracted SRM Item 1', 'qty': 10},
714 {'item_code': 'Subcontracted SRM Item 2', 'qty': 10},
715 {'item_code': 'Subcontracted SRM Item 3', 'qty': 3},
716 {'item_code': 'Subcontracted SRM Item 3', 'qty': 3},
717 {'item_code': 'Subcontracted SRM Item 3', 'qty': 3},
718 {'item_code': 'Subcontracted SRM Item 3', 'qty': 1}
719 ]
720
721 itemwise_details = make_stock_in_entry(rm_items=rm_items)
722 po = create_purchase_order(rm_items = items, is_subcontracted="Yes",
723 supplier_warehouse="_Test Warehouse 1 - _TC")
724
725 for d in rm_items:
726 d['po_detail'] = po.items[0].name
727
728 make_stock_transfer_entry(po_no = po.name, main_item_code=item_code,
729 rm_items=rm_items, itemwise_details=copy.deepcopy(itemwise_details))
730
731 pr1 = make_purchase_invoice(po.name)
732 pr1.update_stock = 1
733 pr1.items[0].qty = 2
734 pr1.items[0].expense_account = 'Stock Adjustment - _TC'
735 add_second_row_in_pr(pr1)
736 pr1.save()
737 pr1.submit()
738
739 for key, value in get_supplied_items(pr1).items():
740 self.assertEqual(value.qty, 4)
741
742 pr1 = make_purchase_invoice(po.name)
743 pr1.update_stock = 1
744 pr1.items[0].qty = 2
745 pr1.items[0].expense_account = 'Stock Adjustment - _TC'
746 add_second_row_in_pr(pr1)
747 pr1.save()
748 pr1.submit()
749
750 for key, value in get_supplied_items(pr1).items():
751 self.assertEqual(value.qty, 4)
752
753 pr1 = make_purchase_invoice(po.name)
754 pr1.update_stock = 1
755 pr1.items[0].qty = 2
756 pr1.items[0].expense_account = 'Stock Adjustment - _TC'
757 pr1.save()
758 pr1.submit()
759
760 for key, value in get_supplied_items(pr1).items():
761 self.assertEqual(value.qty, 2)
762
Rohit Waghchaure7b7ceaa2021-05-24 20:11:15 +0530763def add_second_row_in_pr(pr):
764 item_dict = {}
765 for column in ['item_code', 'item_name', 'qty', 'uom', 'warehouse', 'stock_uom',
Rohit Waghchaure110e1522021-06-15 17:29:52 +0530766 'purchase_order', 'purchase_order_item', 'conversion_factor', 'rate', 'expense_account', 'po_detail']:
Rohit Waghchaure7b7ceaa2021-05-24 20:11:15 +0530767 item_dict[column] = pr.items[0].get(column)
768
769 pr.append('items', item_dict)
770 pr.set_missing_values()
771
772def get_supplied_items(pr_doc):
773 supplied_items = {}
774 for row in pr_doc.get('supplied_items'):
775 if row.rm_item_code not in supplied_items:
776 supplied_items.setdefault(row.rm_item_code,
777 frappe._dict({'qty': 0, 'serial_no': [], 'batch_no': defaultdict(float)}))
778
779 details = supplied_items[row.rm_item_code]
780 update_item_details(row, details)
781
782 return supplied_items
783
784def make_stock_in_entry(**args):
785 args = frappe._dict(args)
786
787 items = {}
788 for row in args.rm_items:
789 row = frappe._dict(row)
790
791 doc = make_stock_entry(target=row.warehouse or '_Test Warehouse - _TC',
792 item_code=row.item_code, qty=row.qty or 1, basic_rate=row.rate or 100)
793
794 if row.item_code not in items:
795 items.setdefault(row.item_code, frappe._dict({'qty': 0, 'serial_no': [], 'batch_no': defaultdict(float)}))
796
797 child_row = doc.items[0]
798 details = items[child_row.item_code]
799 update_item_details(child_row, details)
800
801 return items
802
803def update_item_details(child_row, details):
804 details.qty += (child_row.get('qty') if child_row.doctype == 'Stock Entry Detail'
805 else child_row.get('consumed_qty'))
806
807 if child_row.serial_no:
808 details.serial_no.extend(get_serial_nos(child_row.serial_no))
809
810 if child_row.batch_no:
811 details.batch_no[child_row.batch_no] += (child_row.get('qty') or child_row.get('consumed_qty'))
812
813def make_stock_transfer_entry(**args):
814 args = frappe._dict(args)
815
816 items = []
817 for row in args.rm_items:
818 row = frappe._dict(row)
819
820 item = {'item_code': row.main_item_code or args.main_item_code, 'rm_item_code': row.item_code,
821 'qty': row.qty or 1, 'item_name': row.item_code, 'rate': row.rate or 100,
822 'stock_uom': row.stock_uom or 'Nos', 'warehouse': row.warehuose or '_Test Warehouse - _TC'}
823
824 item_details = args.itemwise_details.get(row.item_code)
825
826 if item_details and item_details.serial_no:
827 serial_nos = item_details.serial_no[0:cint(row.qty)]
828 item['serial_no'] = '\n'.join(serial_nos)
829 item_details.serial_no = list(set(item_details.serial_no) - set(serial_nos))
830
831 if item_details and item_details.batch_no:
832 for batch_no, batch_qty in item_details.batch_no.items():
833 if batch_qty >= row.qty:
834 item['batch_no'] = batch_no
835 item_details.batch_no[batch_no] -= row.qty
836 break
837
838 items.append(item)
839
840 ste_dict = make_rm_stock_entry(args.po_no, items)
841 doc = frappe.get_doc(ste_dict)
842 doc.insert()
843 doc.submit()
844
845 return doc
846
847def make_subcontract_items():
848 sub_contracted_items = {'Subcontracted Item SA1': {}, 'Subcontracted Item SA2': {}, 'Subcontracted Item SA3': {},
849 'Subcontracted Item SA4': {'has_batch_no': 1, 'create_new_batch': 1, 'batch_number_series': 'SBAT.####'},
850 'Subcontracted Item SA5': {}, 'Subcontracted Item SA6': {}}
851
852 for item, properties in sub_contracted_items.items():
853 if not frappe.db.exists('Item', item):
854 properties.update({'is_stock_item': 1, 'is_sub_contracted_item': 1})
855 make_item(item, properties)
856
857def make_raw_materials():
858 raw_materials = {'Subcontracted SRM Item 1': {},
859 'Subcontracted SRM Item 2': {'has_serial_no': 1, 'serial_no_series': 'SRI.####'},
860 'Subcontracted SRM Item 3': {'has_batch_no': 1, 'create_new_batch': 1, 'batch_number_series': 'BAT.####'},
861 'Subcontracted SRM Item 4': {'has_serial_no': 1, 'serial_no_series': 'SRII.####'},
862 'Subcontracted SRM Item 5': {'has_serial_no': 1, 'serial_no_series': 'SRII.####'}}
863
864 for item, properties in raw_materials.items():
865 if not frappe.db.exists('Item', item):
866 properties.update({'is_stock_item': 1})
867 make_item(item, properties)
868
869def make_bom_for_subcontracted_items():
870 boms = {
871 'Subcontracted Item SA1': ['Subcontracted SRM Item 1', 'Subcontracted SRM Item 2', 'Subcontracted SRM Item 3'],
872 'Subcontracted Item SA2': ['Subcontracted SRM Item 2'],
873 'Subcontracted Item SA3': ['Subcontracted SRM Item 2'],
874 'Subcontracted Item SA4': ['Subcontracted SRM Item 1', 'Subcontracted SRM Item 2', 'Subcontracted SRM Item 3'],
875 'Subcontracted Item SA5': ['Subcontracted SRM Item 5'],
876 'Subcontracted Item SA6': ['Subcontracted SRM Item 3']
877 }
878
879 for item_code, raw_materials in boms.items():
880 if not frappe.db.exists('BOM', {'item': item_code}):
881 make_bom(item=item_code, raw_materials=raw_materials, rate=100)
882
883def set_backflush_based_on(based_on):
884 frappe.db.set_value('Buying Settings', None,
Ankush Menat4551d7d2021-08-19 13:41:10 +0530885 'backflush_raw_materials_of_subcontract_based_on', based_on)