Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
617 changes: 154 additions & 463 deletions erpnext/accounts/doctype/purchase_invoice/purchase_invoice.json

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -400,6 +400,7 @@ def on_submit(self):
# 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")

Expand Down Expand Up @@ -998,6 +999,7 @@ def on_cancel(self):
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()

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -621,8 +621,10 @@ def test_update_stock_and_purchase_return(self):
self.assertEqual(actual_qty_0, get_qty_after_transaction())

def test_subcontracting_via_purchase_invoice(self):
from erpnext.buying.doctype.purchase_order.test_purchase_order import update_backflush_based_on
from erpnext.stock.doctype.stock_entry.test_stock_entry import make_stock_entry

update_backflush_based_on('BOM')
make_stock_entry(item_code="_Test Item", target="_Test Warehouse 1 - _TC", qty=100, basic_rate=100)
make_stock_entry(item_code="_Test Item Home Desktop 100", target="_Test Warehouse 1 - _TC",
qty=100, basic_rate=100)
Expand Down
39 changes: 37 additions & 2 deletions erpnext/buying/doctype/purchase_order/purchase_order.js
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,39 @@ frappe.ui.form.on("Purchase Order", {
} else {
frm.set_value("tax_withholding_category", frm.supplier_tds);
}
},

refresh: function(frm) {
frm.trigger('get_materials_from_supplier');
},

get_materials_from_supplier: function(frm) {
let po_details = [];

if (frm.doc.supplied_items && (frm.doc.per_received == 100 || frm.doc.status === 'Closed')) {
frm.doc.supplied_items.forEach(d => {
if (d.total_supplied_qty && d.total_supplied_qty != d.consumed_qty) {
po_details.push(d.name)
}
});
}

if (po_details && po_details.length) {
frm.add_custom_button(__('Return of Components'), () => {
frm.call({
method: 'erpnext.buying.doctype.purchase_order.purchase_order.get_materials_from_supplier',
freeze: true,
freeze_message: __('Creating Stock Entry'),
args: { purchase_order: frm.doc.name, po_details: po_details },
callback: function(r) {
if (r && r.message) {
const doc = frappe.model.sync(r.message);
frappe.set_route("Form", doc[0].doctype, doc[0].name);
}
}
});
}, __('Create'));
}
}
});

Expand Down Expand Up @@ -217,7 +250,7 @@ erpnext.buying.PurchaseOrderController = class PurchaseOrderController extends e
}

has_unsupplied_items() {
return this.frm.doc['supplied_items'].some(item => item.required_qty != item.supplied_qty)
return this.frm.doc['supplied_items'].some(item => item.required_qty > item.supplied_qty);
}

make_stock_entry() {
Expand Down Expand Up @@ -513,12 +546,14 @@ erpnext.buying.PurchaseOrderController = class PurchaseOrderController extends e
],
primary_action: function() {
var data = d.get_values();
let reason_for_hold = 'Reason for hold: ' + data.reason_for_hold;

frappe.call({
method: "frappe.desk.form.utils.add_comment",
args: {
reference_doctype: me.frm.doctype,
reference_name: me.frm.docname,
content: __('Reason for hold:') + " " +data.reason_for_hold,
content: __(reason_for_hold),
comment_email: frappe.session.user,
comment_by: frappe.session.user_fullname
},
Expand Down
3 changes: 2 additions & 1 deletion erpnext/buying/doctype/purchase_order/purchase_order.json
Original file line number Diff line number Diff line change
Expand Up @@ -609,6 +609,7 @@
"fieldname": "supplied_items",
"fieldtype": "Table",
"label": "Supplied Items",
"no_copy": 1,
"oldfieldname": "po_raw_material_details",
"oldfieldtype": "Table",
"options": "Purchase Order Item Supplied",
Expand Down Expand Up @@ -1377,7 +1378,7 @@
"idx": 105,
"is_submittable": 1,
"links": [],
"modified": "2021-04-19 00:55:30.781375",
"modified": "2021-05-30 15:17:53.663648",
"modified_by": "Administrator",
"module": "Buying",
"name": "Purchase Order",
Expand Down
68 changes: 63 additions & 5 deletions erpnext/buying/doctype/purchase_order/purchase_order.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,12 +14,11 @@
from erpnext.buying.utils import validate_for_items, check_on_hold_or_closed_status
from erpnext.stock.utils import get_bin
from erpnext.accounts.party import get_party_account_currency
from six import string_types
from erpnext.stock.doctype.item.item import get_item_defaults
from erpnext.setup.doctype.item_group.item_group import get_item_group_defaults
from erpnext.accounts.doctype.tax_withholding_category.tax_withholding_category import get_party_tax_withholding_details
from erpnext.accounts.doctype.sales_invoice.sales_invoice import validate_inter_company_party, update_linked_doc,\
unlink_inter_company_doc
from erpnext.accounts.doctype.sales_invoice.sales_invoice import (validate_inter_company_party,
update_linked_doc, unlink_inter_company_doc)

form_grid_templates = {
"items": "templates/form_grid/item_grid.html"
Expand Down Expand Up @@ -503,9 +502,11 @@ def update_item(obj, target, source_parent):

@frappe.whitelist()
def make_rm_stock_entry(purchase_order, rm_items):
if isinstance(rm_items, string_types):
rm_items_list = rm_items

if isinstance(rm_items, str):
rm_items_list = json.loads(rm_items)
else:
elif not rm_items:
frappe.throw(_("No Items available for transfer"))

if rm_items_list:
Expand Down Expand Up @@ -543,6 +544,8 @@ def make_rm_stock_entry(purchase_order, rm_items):
'qty': rm_item_data["qty"],
'from_warehouse': rm_item_data["warehouse"],
'stock_uom': rm_item_data["stock_uom"],
'serial_no': rm_item_data.get('serial_no'),
'batch_no': rm_item_data.get('batch_no'),
'main_item_code': rm_item_data["item_code"],
'allow_alternative_item': item_wh.get(rm_item_code, {}).get('allow_alternative_item')
}
Expand Down Expand Up @@ -582,3 +585,58 @@ def update_status(status, name):
def make_inter_company_sales_order(source_name, target_doc=None):
from erpnext.accounts.doctype.sales_invoice.sales_invoice import make_inter_company_transaction
return make_inter_company_transaction("Purchase Order", source_name, target_doc)

@frappe.whitelist()
def get_materials_from_supplier(purchase_order, po_details):
if isinstance(po_details, str):
po_details = json.loads(po_details)

doc = frappe.get_cached_doc('Purchase Order', purchase_order)
doc.initialized_fields()
doc.purchase_orders = [doc.name]
doc.get_available_materials()

if not doc.available_materials:
frappe.throw(_('Materials are already received against the purchase order {0}')
.format(purchase_order))

return make_return_stock_entry_for_subcontract(doc.available_materials, doc, po_details)

def make_return_stock_entry_for_subcontract(available_materials, po_doc, po_details):
ste_doc = frappe.new_doc('Stock Entry')
ste_doc.purpose = 'Material Transfer'
ste_doc.purchase_order = po_doc.name
ste_doc.company = po_doc.company
ste_doc.is_return = 1

for key, value in available_materials.items():
if not value.qty:
continue

if value.batch_no:
for batch_no, qty in value.batch_no.items():
if qty > 0:
add_items_in_ste(ste_doc, value, value.qty, po_details, batch_no)
else:
add_items_in_ste(ste_doc, value, value.qty, po_details)

ste_doc.set_stock_entry_type()
ste_doc.calculate_rate_and_amount()

return ste_doc

def add_items_in_ste(ste_doc, row, qty, po_details, batch_no=None):
item = ste_doc.append('items', row.item_details)

po_detail = list(set(row.po_details).intersection(po_details))
item.update({
'qty': qty,
'batch_no': batch_no,
'basic_rate': row.item_details['rate'],
'po_detail': po_detail[0] if po_detail else '',
's_warehouse': row.item_details['t_warehouse'],
't_warehouse': row.item_details['s_warehouse'],
'item_code': row.item_details['rm_item_code'],
'subcontracted_item': row.item_details['main_item_code'],
'serial_no': '\n'.join(row.serial_no) if row.serial_no else ''
})
103 changes: 18 additions & 85 deletions erpnext/buying/doctype/purchase_order/test_purchase_order.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,6 @@
from erpnext.manufacturing.doctype.blanket_order.test_blanket_order import make_blanket_order

from erpnext.stock.doctype.batch.test_batch import make_new_batch
from erpnext.controllers.buying_controller import get_backflushed_subcontracted_raw_materials

class TestPurchaseOrder(unittest.TestCase):
def test_make_purchase_receipt(self):
Expand Down Expand Up @@ -771,7 +770,7 @@ def test_reserved_qty_subcontract_po(self):
self.assertEqual(bin11.reserved_qty_for_sub_contract, bin1.reserved_qty_for_sub_contract)

def test_exploded_items_in_subcontracted(self):
item_code = "_Test Subcontracted FG Item 1"
item_code = "_Test Subcontracted FG Item 11"
make_subcontracted_item(item_code=item_code)

po = create_purchase_order(item_code=item_code, qty=1,
Expand Down Expand Up @@ -848,79 +847,6 @@ def test_backflush_based_on_stock_entry(self):
for item in rm_items:
transferred_rm_map[item.get('rm_item_code')] = item

for item in pr.get('supplied_items'):
self.assertEqual(item.get('required_qty'), (transferred_rm_map[item.get('rm_item_code')].get('qty') / order_qty) * received_qty)

update_backflush_based_on("BOM")

def test_backflushed_based_on_for_multiple_batches(self):
item_code = "_Test Subcontracted FG Item 2"
make_item('Sub Contracted Raw Material 2', {
'is_stock_item': 1,
'is_sub_contracted_item': 1
})

make_subcontracted_item(item_code=item_code, has_batch_no=1, create_new_batch=1,
raw_materials=["Sub Contracted Raw Material 2"])

update_backflush_based_on("Material Transferred for Subcontract")

order_qty = 500
po = create_purchase_order(item_code=item_code, qty=order_qty,
is_subcontracted="Yes", supplier_warehouse="_Test Warehouse 1 - _TC")

make_stock_entry(target="_Test Warehouse - _TC",
item_code = "Sub Contracted Raw Material 2", qty=552, basic_rate=100)

rm_items = [
{"item_code":item_code,"rm_item_code":"Sub Contracted Raw Material 2","item_name":"_Test Item",
"qty":552,"warehouse":"_Test Warehouse - _TC", "stock_uom":"Nos"}]

rm_item_string = json.dumps(rm_items)
se = frappe.get_doc(make_subcontract_transfer_entry(po.name, rm_item_string))
se.submit()

for batch in ["ABCD1", "ABCD2", "ABCD3", "ABCD4"]:
make_new_batch(batch_id=batch, item_code=item_code)

pr = make_purchase_receipt(po.name)

# partial receipt
pr.get('items')[0].qty = 30
pr.get('items')[0].batch_no = "ABCD1"

purchase_order = po.name
purchase_order_item = po.items[0].name

for batch_no, qty in {"ABCD2": 60, "ABCD3": 70, "ABCD4":40}.items():
pr.append("items", {
"item_code": pr.get('items')[0].item_code,
"item_name": pr.get('items')[0].item_name,
"uom": pr.get('items')[0].uom,
"stock_uom": pr.get('items')[0].stock_uom,
"warehouse": pr.get('items')[0].warehouse,
"conversion_factor": pr.get('items')[0].conversion_factor,
"cost_center": pr.get('items')[0].cost_center,
"rate": pr.get('items')[0].rate,
"qty": qty,
"batch_no": batch_no,
"purchase_order": purchase_order,
"purchase_order_item": purchase_order_item
})

pr.submit()

pr1 = make_purchase_receipt(po.name)
pr1.get('items')[0].qty = 300
pr1.get('items')[0].batch_no = "ABCD1"
pr1.save()

pr_key = ("Sub Contracted Raw Material 2", po.name)
consumed_qty = get_backflushed_subcontracted_raw_materials([po.name]).get(pr_key)

self.assertTrue(pr1.supplied_items[0].consumed_qty > 0)
self.assertTrue(pr1.supplied_items[0].consumed_qty, flt(552.0) - flt(consumed_qty))

update_backflush_based_on("BOM")

def test_supplied_qty_against_subcontracted_po(self):
Expand Down Expand Up @@ -1117,22 +1043,29 @@ def create_purchase_order(**args):
po.conversion_factor = args.conversion_factor or 1
po.supplier_warehouse = args.supplier_warehouse or None

po.append("items", {
"item_code": args.item or args.item_code or "_Test Item",
"warehouse": args.warehouse or "_Test Warehouse - _TC",
"qty": args.qty or 10,
"rate": args.rate or 500,
"schedule_date": add_days(nowdate(), 1),
"include_exploded_items": args.get('include_exploded_items', 1),
"against_blanket_order": args.against_blanket_order
})
if args.rm_items:
for row in args.rm_items:
po.append("items", row)
else:
po.append("items", {
"item_code": args.item or args.item_code or "_Test Item",
"warehouse": args.warehouse or "_Test Warehouse - _TC",
"qty": args.qty or 10,
"rate": args.rate or 500,
"schedule_date": add_days(nowdate(), 1),
"include_exploded_items": args.get('include_exploded_items', 1),
"against_blanket_order": args.against_blanket_order
})

po.set_missing_values()
if not args.do_not_save:
po.insert()
if not args.do_not_submit:
if po.is_subcontracted == "Yes":
supp_items = po.get("supplied_items")
for d in supp_items:
d.reserve_warehouse = args.warehouse or "_Test Warehouse - _TC"
if not d.reserve_warehouse:
d.reserve_warehouse = args.warehouse or "_Test Warehouse - _TC"
po.submit()

return po
Expand Down
Loading