fix: purchase invoice qty change not recalculate the consumed qty and added test cases for purchase invoice

This commit is contained in:
Rohit Waghchaure 2021-06-15 17:29:52 +05:30
parent 6bbc8ec3e8
commit 110e152fa3
8 changed files with 463 additions and 498 deletions

View File

@ -400,6 +400,7 @@ class PurchaseInvoice(BuyingController):
# because updating ordered qty in bin depends upon updated ordered qty in PO
if self.update_stock == 1:
self.update_stock_ledger()
self.set_consumed_qty_in_po()
from erpnext.stock.doctype.serial_no.serial_no import update_serial_nos_after_submit
update_serial_nos_after_submit(self, "items")
@ -998,6 +999,7 @@ class PurchaseInvoice(BuyingController):
if self.update_stock == 1:
self.update_stock_ledger()
self.delete_auto_created_batches()
self.set_consumed_qty_in_po()
self.make_gl_entries_on_cancel()

View File

@ -58,6 +58,11 @@ class BuyingController(StockController, Subcontracting):
if self.doctype in ("Purchase Receipt", "Purchase Invoice"):
self.update_valuation_rate()
def onload(self):
super(BuyingController, self).onload()
self.set_onload("backflush_based_on", frappe.db.get_single_value('Buying Settings',
'backflush_raw_materials_of_subcontract_based_on'))
def set_missing_values(self, for_validate=False):
super(BuyingController, self).set_missing_values(for_validate)

View File

@ -40,9 +40,10 @@ class Subcontracting():
self.purchase_orders = [d.purchase_order for d in self.items if d.purchase_order]
def __identify_change_in_item_table(self):
self.changed_name = []
self.__changed_name = []
self.__reference_name = []
if self.doctype == 'Purchase Order' or not self.get(self.raw_material_table):
if self.doctype == 'Purchase Order' or self.is_new():
self.set(self.raw_material_table, [])
return
@ -51,17 +52,18 @@ class Subcontracting():
return True
for n_row in self.items:
self.__reference_name.append(n_row.name)
if (n_row.name not in item_dict) or (n_row.item_code, n_row.qty) != item_dict[n_row.name]:
self.changed_name.append(n_row.name)
self.__changed_name.append(n_row.name)
if item_dict.get(n_row.name):
del item_dict[n_row.name]
self.changed_name.extend(item_dict.keys())
self.__changed_name.extend(item_dict.keys())
def __get_data_before_save(self):
item_dict = {}
if self.doctype == 'Purchase Receipt' and self._doc_before_save:
if self.doctype in ['Purchase Receipt', 'Purchase Invoice'] and self._doc_before_save:
for row in self._doc_before_save.get('items'):
item_dict[row.name] = (row.item_code, row.qty)
@ -149,7 +151,7 @@ class Subcontracting():
def __get_received_items(self, doctype):
fields = []
self.po_field = 'purchase_order' if doctype == 'Purchase Receipt' else 'po_detail'
self.po_field = 'purchase_order'
for field in ['name', self.po_field, 'parent']:
fields.append(f'`tab{doctype} Item`.`{field}`')
@ -161,9 +163,9 @@ class Subcontracting():
return frappe.get_all(f'{doctype}', fields = fields, filters = filters)
def __get_consumed_items(self, doctype, pr_items):
return frappe.get_all(f'{doctype} Item Supplied',
return frappe.get_all('Purchase Receipt Item Supplied',
fields = ['serial_no', 'rm_item_code', 'reference_name', 'batch_no', 'consumed_qty', 'main_item_code'],
filters = {'docstatus': 1, 'reference_name': ('in', list(pr_items))})
filters = {'docstatus': 1, 'reference_name': ('in', list(pr_items)), 'parenttype': doctype})
def __set_alternative_item_details(self, row):
if row.get('original_item'):
@ -196,13 +198,16 @@ class Subcontracting():
return frappe.get_all('BOM', fields = fields, filters=filters, order_by = f'`tab{doctype}`.`idx`') or []
def __remove_changed_rows(self):
if not self.changed_name:
if not self.__changed_name:
return
i=1
self.set(self.raw_material_table, [])
for d in self._doc_before_save.supplied_items:
if d.reference_name in self.changed_name:
if d.reference_name in self.__changed_name:
continue
if (d.reference_name not in self.__reference_name):
continue
d.idx = i
@ -215,8 +220,8 @@ class Subcontracting():
has_supplied_items = True if self.get(self.raw_material_table) else False
for row in self.items:
if (self.doctype != 'Purchase Order' and ((self.changed_name and row.name not in self.changed_name)
or (has_supplied_items and not self.changed_name))):
if (self.doctype != 'Purchase Order' and ((self.__changed_name and row.name not in self.__changed_name)
or (has_supplied_items and not self.__changed_name))):
continue
if self.doctype == 'Purchase Order' or self.backflush_based_on == 'BOM':
@ -305,16 +310,18 @@ class Subcontracting():
self.available_materials[key]['serial_no'].remove(sn)
def set_consumed_qty_in_po(self):
# Update consumed qty back in the purchase order
if self.is_subcontracted != 'Yes':
return
self.__get_purchase_orders()
consumed_items, pr_items = self.__update_consumed_materials(self.doctype, return_consumed_items=True)
itemwise_consumed_qty = defaultdict(float)
for row in consumed_items:
key = (row.rm_item_code, row.main_item_code, pr_items.get(row.reference_name))
itemwise_consumed_qty[key] += row.consumed_qty
for doctype in ['Purchase Receipt', 'Purchase Invoice']:
consumed_items, pr_items = self.__update_consumed_materials(doctype, return_consumed_items=True)
for row in consumed_items:
key = (row.rm_item_code, row.main_item_code, pr_items.get(row.reference_name))
itemwise_consumed_qty[key] += row.consumed_qty
self.__update_consumed_qty_in_po(itemwise_consumed_qty)

View File

@ -122,10 +122,21 @@ erpnext.buying.BuyingController = class BuyingController extends erpnext.Transac
this.set_from_product_bundle();
}
this.toggle_subcontracting_fields();
super.refresh();
}
},
supplier() {
toggle_subcontracting_fields: function() {
if (in_list(['Purchase Receipt', 'Purchase Invoice'], this.frm.doc.doctype)) {
this.frm.fields_dict.supplied_items.grid.update_docfield_property('consumed_qty',
'read_only', this.frm.doc.__onload && this.frm.doc.__onload.backflush_based_on === 'BOM');
this.frm.set_df_property('supplied_items', 'cannot_add_rows', 1);
this.frm.set_df_property('supplied_items', 'cannot_delete_rows', 1);
}
},
supplier: function() {
var me = this;
erpnext.utils.get_party_details(this.frm, null, null, function(){
me.apply_price_list();

View File

@ -75,15 +75,6 @@ frappe.ui.form.on("Purchase Receipt", {
}
frm.events.add_custom_buttons(frm);
frm.trigger('toggle_subcontracting_fields');
},
toggle_subcontracting_fields: function(frm) {
frm.fields_dict.supplied_items.grid.update_docfield_property('consumed_qty',
'read_only', frm.doc.__onload && frm.doc.__onload.backflush_based_on === 'BOM');
frm.set_df_property('supplied_items', 'cannot_add_rows', 1);
frm.set_df_property('supplied_items', 'cannot_delete_rows', 1);
},
add_custom_buttons: function(frm) {

View File

@ -102,11 +102,6 @@ class PurchaseReceipt(BuyingController):
if self.get("items") and self.apply_putaway_rule and not self.get("is_return"):
apply_putaway_rule(self.doctype, self.get("items"), self.company)
def onload(self):
super(PurchaseReceipt, self).onload()
self.set_onload("backflush_based_on", frappe.db.get_single_value('Buying Settings',
'backflush_raw_materials_of_subcontract_based_on'))
def validate(self):
self.validate_posting_time()
super(PurchaseReceipt, self).validate()

View File

@ -10,7 +10,7 @@ from erpnext.stock.doctype.item.test_item import make_item
from erpnext.manufacturing.doctype.production_plan.test_production_plan import make_bom
from erpnext.buying.doctype.purchase_order.test_purchase_order import create_purchase_order
from erpnext.buying.doctype.purchase_order.purchase_order import (make_rm_stock_entry,
make_purchase_receipt, get_materials_from_supplier)
make_purchase_receipt, make_purchase_invoice, get_materials_from_supplier)
class TestSubcontracting(unittest.TestCase):
def setUp(self):
@ -458,10 +458,273 @@ class TestSubcontracting(unittest.TestCase):
self.assertEqual(value.qty, details.qty)
self.assertEqual(value.batch_no, details.batch_no)
def test_item_with_batch_based_on_material_transfer_for_purchase_invoice(self):
'''
- Set backflush based on Material Transferred for Subcontract
- Create subcontracted PO for the item Subcontracted Item SA4 (has batch no).
- Transfer the components from Stores to Supplier warehouse with batch no and serial nos.
- Transfer the components in multiple batches with extra 2 qty for the batched item.
- Create the 3 purchase receipt against the PO and split Subcontracted Items into two batches.
- Keep the qty as 2 for Subcontracted Item in the purchase receipt.
- In the first purchase receipt the batched raw materials will be consumed 2 extra qty.
'''
set_backflush_based_on('Material Transferred for Subcontract')
item_code = 'Subcontracted Item SA4'
items = [{'warehouse': '_Test Warehouse - _TC', 'item_code': item_code, 'qty': 10, 'rate': 100}]
rm_items = [{'item_code': 'Subcontracted SRM Item 1', 'qty': 10},
{'item_code': 'Subcontracted SRM Item 2', 'qty': 10},
{'item_code': 'Subcontracted SRM Item 3', 'qty': 3},
{'item_code': 'Subcontracted SRM Item 3', 'qty': 3},
{'item_code': 'Subcontracted SRM Item 3', 'qty': 3},
{'item_code': 'Subcontracted SRM Item 3', 'qty': 3}
]
itemwise_details = make_stock_in_entry(rm_items=rm_items)
po = create_purchase_order(rm_items = items, is_subcontracted="Yes",
supplier_warehouse="_Test Warehouse 1 - _TC")
for d in rm_items:
d['po_detail'] = po.items[0].name
make_stock_transfer_entry(po_no = po.name, main_item_code=item_code,
rm_items=rm_items, itemwise_details=copy.deepcopy(itemwise_details))
pr1 = make_purchase_invoice(po.name)
pr1.update_stock = 1
pr1.items[0].qty = 2
pr1.items[0].expense_account = 'Stock Adjustment - _TC'
add_second_row_in_pr(pr1)
pr1.save()
pr1.submit()
for key, value in get_supplied_items(pr1).items():
qty = 4 if key != 'Subcontracted SRM Item 3' else 6
self.assertEqual(value.qty, qty)
pr1 = make_purchase_invoice(po.name)
pr1.update_stock = 1
pr1.items[0].expense_account = 'Stock Adjustment - _TC'
pr1.items[0].qty = 2
add_second_row_in_pr(pr1)
pr1.save()
pr1.submit()
for key, value in get_supplied_items(pr1).items():
self.assertEqual(value.qty, 4)
pr1 = make_purchase_invoice(po.name)
pr1.update_stock = 1
pr1.items[0].qty = 2
pr1.items[0].expense_account = 'Stock Adjustment - _TC'
pr1.save()
pr1.submit()
for key, value in get_supplied_items(pr1).items():
self.assertEqual(value.qty, 2)
def test_partial_transfer_serial_no_components_based_on_material_transfer_for_purchase_invoice(self):
'''
- Set backflush based on Material Transferred for Subcontract
- Create subcontracted PO for the item Subcontracted Item SA2.
- Transfer the partial components from Stores to Supplier warehouse with serial nos.
- Create partial purchase receipt against the PO and change the qty manually.
- Transfer the remaining components from Stores to Supplier warehouse with serial nos.
- Create purchase receipt for remaining qty against the PO and change the qty manually.
'''
set_backflush_based_on('Material Transferred for Subcontract')
item_code = 'Subcontracted Item SA2'
items = [{'warehouse': '_Test Warehouse - _TC', 'item_code': item_code, 'qty': 10, 'rate': 100}]
rm_items = [{'item_code': 'Subcontracted SRM Item 2', 'qty': 5}]
itemwise_details = make_stock_in_entry(rm_items=rm_items)
po = create_purchase_order(rm_items = items, is_subcontracted="Yes",
supplier_warehouse="_Test Warehouse 1 - _TC")
for d in rm_items:
d['po_detail'] = po.items[0].name
make_stock_transfer_entry(po_no = po.name, main_item_code=item_code,
rm_items=rm_items, itemwise_details=copy.deepcopy(itemwise_details))
pr1 = make_purchase_invoice(po.name)
pr1.update_stock = 1
pr1.items[0].qty = 5
pr1.items[0].expense_account = 'Stock Adjustment - _TC'
pr1.save()
for key, value in get_supplied_items(pr1).items():
details = itemwise_details.get(key)
self.assertEqual(value.qty, 3)
self.assertEqual(sorted(value.serial_no), sorted(details.serial_no[0:3]))
pr1.load_from_db()
pr1.supplied_items[0].consumed_qty = 5
pr1.supplied_items[0].serial_no = '\n'.join(itemwise_details[pr1.supplied_items[0].rm_item_code]['serial_no'])
pr1.save()
pr1.submit()
for key, value in get_supplied_items(pr1).items():
details = itemwise_details.get(key)
self.assertEqual(value.qty, details.qty)
self.assertEqual(sorted(value.serial_no), sorted(details.serial_no))
itemwise_details = make_stock_in_entry(rm_items=rm_items)
for d in rm_items:
d['po_detail'] = po.items[0].name
make_stock_transfer_entry(po_no = po.name, main_item_code=item_code,
rm_items=rm_items, itemwise_details=copy.deepcopy(itemwise_details))
pr1 = make_purchase_invoice(po.name)
pr1.update_stock = 1
pr1.items[0].expense_account = 'Stock Adjustment - _TC'
pr1.submit()
for key, value in get_supplied_items(pr1).items():
details = itemwise_details.get(key)
self.assertEqual(value.qty, details.qty)
self.assertEqual(sorted(value.serial_no), sorted(details.serial_no))
def test_partial_transfer_batch_based_on_material_transfer_for_purchase_invoice(self):
'''
- Set backflush based on Material Transferred for Subcontract
- Create subcontracted PO for the item Subcontracted Item SA6.
- Transfer the partial components from Stores to Supplier warehouse with batch.
- Create partial purchase receipt against the PO and change the qty manually.
- Transfer the remaining components from Stores to Supplier warehouse with batch.
- Create purchase receipt for remaining qty against the PO and change the qty manually.
'''
set_backflush_based_on('Material Transferred for Subcontract')
item_code = 'Subcontracted Item SA6'
items = [{'warehouse': '_Test Warehouse - _TC', 'item_code': item_code, 'qty': 10, 'rate': 100}]
rm_items = [{'item_code': 'Subcontracted SRM Item 3', 'qty': 5}]
itemwise_details = make_stock_in_entry(rm_items=rm_items)
po = create_purchase_order(rm_items = items, is_subcontracted="Yes",
supplier_warehouse="_Test Warehouse 1 - _TC")
for d in rm_items:
d['po_detail'] = po.items[0].name
make_stock_transfer_entry(po_no = po.name, main_item_code=item_code,
rm_items=rm_items, itemwise_details=copy.deepcopy(itemwise_details))
pr1 = make_purchase_invoice(po.name)
pr1.update_stock = 1
pr1.items[0].qty = 5
pr1.items[0].expense_account = 'Stock Adjustment - _TC'
pr1.save()
transferred_batch_no = ''
for key, value in get_supplied_items(pr1).items():
details = itemwise_details.get(key)
self.assertEqual(value.qty, 3)
transferred_batch_no = details.batch_no
self.assertEqual(value.batch_no, details.batch_no)
pr1.load_from_db()
pr1.supplied_items[0].consumed_qty = 5
pr1.supplied_items[0].batch_no = list(transferred_batch_no.keys())[0]
pr1.save()
pr1.submit()
for key, value in get_supplied_items(pr1).items():
details = itemwise_details.get(key)
self.assertEqual(value.qty, details.qty)
self.assertEqual(value.batch_no, details.batch_no)
itemwise_details = make_stock_in_entry(rm_items=rm_items)
for d in rm_items:
d['po_detail'] = po.items[0].name
make_stock_transfer_entry(po_no = po.name, main_item_code=item_code,
rm_items=rm_items, itemwise_details=copy.deepcopy(itemwise_details))
pr1 = make_purchase_invoice(po.name)
pr1.update_stock = 1
pr1.items[0].expense_account = 'Stock Adjustment - _TC'
pr1.submit()
for key, value in get_supplied_items(pr1).items():
details = itemwise_details.get(key)
self.assertEqual(value.qty, details.qty)
self.assertEqual(value.batch_no, details.batch_no)
def test_item_with_batch_based_on_bom_for_purchase_invoice(self):
'''
- Set backflush based on BOM
- Create subcontracted PO for the item Subcontracted Item SA4 (has batch no).
- Transfer the components from Stores to Supplier warehouse with batch no and serial nos.
- Transfer the components in multiple batches.
- Create the 3 purchase receipt against the PO and split Subcontracted Items into two batches.
- Keep the qty as 2 for Subcontracted Item in the purchase receipt.
'''
set_backflush_based_on('BOM')
item_code = 'Subcontracted Item SA4'
items = [{'warehouse': '_Test Warehouse - _TC', 'item_code': item_code, 'qty': 10, 'rate': 100}]
rm_items = [{'item_code': 'Subcontracted SRM Item 1', 'qty': 10},
{'item_code': 'Subcontracted SRM Item 2', 'qty': 10},
{'item_code': 'Subcontracted SRM Item 3', 'qty': 3},
{'item_code': 'Subcontracted SRM Item 3', 'qty': 3},
{'item_code': 'Subcontracted SRM Item 3', 'qty': 3},
{'item_code': 'Subcontracted SRM Item 3', 'qty': 1}
]
itemwise_details = make_stock_in_entry(rm_items=rm_items)
po = create_purchase_order(rm_items = items, is_subcontracted="Yes",
supplier_warehouse="_Test Warehouse 1 - _TC")
for d in rm_items:
d['po_detail'] = po.items[0].name
make_stock_transfer_entry(po_no = po.name, main_item_code=item_code,
rm_items=rm_items, itemwise_details=copy.deepcopy(itemwise_details))
pr1 = make_purchase_invoice(po.name)
pr1.update_stock = 1
pr1.items[0].qty = 2
pr1.items[0].expense_account = 'Stock Adjustment - _TC'
add_second_row_in_pr(pr1)
pr1.save()
pr1.submit()
for key, value in get_supplied_items(pr1).items():
self.assertEqual(value.qty, 4)
pr1 = make_purchase_invoice(po.name)
pr1.update_stock = 1
pr1.items[0].qty = 2
pr1.items[0].expense_account = 'Stock Adjustment - _TC'
add_second_row_in_pr(pr1)
pr1.save()
pr1.submit()
for key, value in get_supplied_items(pr1).items():
self.assertEqual(value.qty, 4)
pr1 = make_purchase_invoice(po.name)
pr1.update_stock = 1
pr1.items[0].qty = 2
pr1.items[0].expense_account = 'Stock Adjustment - _TC'
pr1.save()
pr1.submit()
for key, value in get_supplied_items(pr1).items():
self.assertEqual(value.qty, 2)
def add_second_row_in_pr(pr):
item_dict = {}
for column in ['item_code', 'item_name', 'qty', 'uom', 'warehouse', 'stock_uom',
'purchase_order', 'purchase_order_item', 'conversion_factor', 'rate']:
'purchase_order', 'purchase_order_item', 'conversion_factor', 'rate', 'expense_account', 'po_detail']:
item_dict[column] = pr.items[0].get(column)
pr.append('items', item_dict)