Merge branch 'version-13-hotfix' of https://github.com/frappe/erpnext into payroll_accounting_dimension

This commit is contained in:
Deepesh Garg 2021-06-23 23:06:15 +05:30
commit 3007c9900b
89 changed files with 3324 additions and 1229 deletions

View File

@ -260,7 +260,7 @@
"description": "If enabled, ledger entries will be posted for change amount in POS transactions", "description": "If enabled, ledger entries will be posted for change amount in POS transactions",
"fieldname": "post_change_gl_entries", "fieldname": "post_change_gl_entries",
"fieldtype": "Check", "fieldtype": "Check",
"label": "Change Ledger Entries for Change Amount" "label": "Create Ledger Entries for Change Amount"
} }
], ],
"icon": "icon-cog", "icon": "icon-cog",
@ -268,7 +268,7 @@
"index_web_pages_for_search": 1, "index_web_pages_for_search": 1,
"issingle": 1, "issingle": 1,
"links": [], "links": [],
"modified": "2021-06-16 13:14:45.739107", "modified": "2021-06-17 20:26:03.721202",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Accounts", "module": "Accounts",
"name": "Accounts Settings", "name": "Accounts Settings",

View File

@ -86,7 +86,7 @@ def resolve_dunning(doc, state):
for reference in doc.references: for reference in doc.references:
if reference.reference_doctype == 'Sales Invoice' and reference.outstanding_amount <= 0: if reference.reference_doctype == 'Sales Invoice' and reference.outstanding_amount <= 0:
dunnings = frappe.get_list('Dunning', filters={ dunnings = frappe.get_list('Dunning', filters={
'sales_invoice': reference.reference_name, 'status': ('!=', 'Resolved')}) 'sales_invoice': reference.reference_name, 'status': ('!=', 'Resolved')}, ignore_permissions=True)
for dunning in dunnings: for dunning in dunnings:
frappe.db.set_value("Dunning", dunning.name, "status", 'Resolved') frappe.db.set_value("Dunning", dunning.name, "status", 'Resolved')

View File

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

View File

@ -621,8 +621,10 @@ class TestPurchaseInvoice(unittest.TestCase):
self.assertEqual(actual_qty_0, get_qty_after_transaction()) self.assertEqual(actual_qty_0, get_qty_after_transaction())
def test_subcontracting_via_purchase_invoice(self): 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 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", 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", make_stock_entry(item_code="_Test Item Home Desktop 100", target="_Test Warehouse 1 - _TC",
qty=100, basic_rate=100) qty=100, basic_rate=100)

View File

@ -272,7 +272,7 @@
"fieldname": "rate", "fieldname": "rate",
"fieldtype": "Currency", "fieldtype": "Currency",
"in_list_view": 1, "in_list_view": 1,
"label": "Rate ", "label": "Rate",
"oldfieldname": "import_rate", "oldfieldname": "import_rate",
"oldfieldtype": "Currency", "oldfieldtype": "Currency",
"options": "currency", "options": "currency",
@ -854,7 +854,7 @@
"idx": 1, "idx": 1,
"istable": 1, "istable": 1,
"links": [], "links": [],
"modified": "2021-03-30 09:02:39.256602", "modified": "2021-06-16 19:57:03.101571",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Accounts", "module": "Accounts",
"name": "Purchase Invoice Item", "name": "Purchase Invoice Item",

View File

@ -989,7 +989,7 @@ class SalesInvoice(SellingController):
for payment_mode in self.payments: for payment_mode in self.payments:
if skip_change_gl_entries and payment_mode.account == self.account_for_change_amount: if skip_change_gl_entries and payment_mode.account == self.account_for_change_amount:
payment_mode.base_amount -= self.change_amount payment_mode.base_amount -= flt(self.change_amount)
if payment_mode.amount: if payment_mode.amount:
# POS, make payment entries # POS, make payment entries

View File

@ -53,6 +53,39 @@ frappe.ui.form.on("Purchase Order", {
} else { } else {
frm.set_value("tax_withholding_category", frm.supplier_tds); 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'));
}
} }
}); });
@ -217,7 +250,7 @@ erpnext.buying.PurchaseOrderController = erpnext.buying.BuyingController.extend(
}, },
has_unsupplied_items: function() { has_unsupplied_items: function() {
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: function() { make_stock_entry: function() {
@ -513,12 +546,14 @@ erpnext.buying.PurchaseOrderController = erpnext.buying.BuyingController.extend(
], ],
primary_action: function() { primary_action: function() {
var data = d.get_values(); var data = d.get_values();
let reason_for_hold = 'Reason for hold: ' + data.reason_for_hold;
frappe.call({ frappe.call({
method: "frappe.desk.form.utils.add_comment", method: "frappe.desk.form.utils.add_comment",
args: { args: {
reference_doctype: me.frm.doctype, reference_doctype: me.frm.doctype,
reference_name: me.frm.docname, reference_name: me.frm.docname,
content: __('Reason for hold:') + " " +data.reason_for_hold, content: __(reason_for_hold),
comment_email: frappe.session.user, comment_email: frappe.session.user,
comment_by: frappe.session.user_fullname comment_by: frappe.session.user_fullname
}, },

View File

@ -609,6 +609,7 @@
"fieldname": "supplied_items", "fieldname": "supplied_items",
"fieldtype": "Table", "fieldtype": "Table",
"label": "Supplied Items", "label": "Supplied Items",
"no_copy": 1,
"oldfieldname": "po_raw_material_details", "oldfieldname": "po_raw_material_details",
"oldfieldtype": "Table", "oldfieldtype": "Table",
"options": "Purchase Order Item Supplied", "options": "Purchase Order Item Supplied",
@ -1377,7 +1378,7 @@
"idx": 105, "idx": 105,
"is_submittable": 1, "is_submittable": 1,
"links": [], "links": [],
"modified": "2021-04-19 00:55:30.781375", "modified": "2021-05-30 15:17:53.663648",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Buying", "module": "Buying",
"name": "Purchase Order", "name": "Purchase Order",

View File

@ -14,12 +14,11 @@ from frappe.desk.notifications import clear_doctype_notifications
from erpnext.buying.utils import validate_for_items, check_on_hold_or_closed_status from erpnext.buying.utils import validate_for_items, check_on_hold_or_closed_status
from erpnext.stock.utils import get_bin from erpnext.stock.utils import get_bin
from erpnext.accounts.party import get_party_account_currency 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.stock.doctype.item.item import get_item_defaults
from erpnext.setup.doctype.item_group.item_group import get_item_group_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.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,\ from erpnext.accounts.doctype.sales_invoice.sales_invoice import (validate_inter_company_party,
unlink_inter_company_doc update_linked_doc, unlink_inter_company_doc)
form_grid_templates = { form_grid_templates = {
"items": "templates/form_grid/item_grid.html" "items": "templates/form_grid/item_grid.html"
@ -503,9 +502,11 @@ def get_mapped_purchase_invoice(source_name, target_doc=None, ignore_permissions
@frappe.whitelist() @frappe.whitelist()
def make_rm_stock_entry(purchase_order, rm_items): 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) rm_items_list = json.loads(rm_items)
else: elif not rm_items:
frappe.throw(_("No Items available for transfer")) frappe.throw(_("No Items available for transfer"))
if rm_items_list: if rm_items_list:
@ -543,6 +544,8 @@ def make_rm_stock_entry(purchase_order, rm_items):
'qty': rm_item_data["qty"], 'qty': rm_item_data["qty"],
'from_warehouse': rm_item_data["warehouse"], 'from_warehouse': rm_item_data["warehouse"],
'stock_uom': rm_item_data["stock_uom"], '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"], 'main_item_code': rm_item_data["item_code"],
'allow_alternative_item': item_wh.get(rm_item_code, {}).get('allow_alternative_item') 'allow_alternative_item': item_wh.get(rm_item_code, {}).get('allow_alternative_item')
} }
@ -582,3 +585,58 @@ def update_status(status, name):
def make_inter_company_sales_order(source_name, target_doc=None): def make_inter_company_sales_order(source_name, target_doc=None):
from erpnext.accounts.doctype.sales_invoice.sales_invoice import make_inter_company_transaction from erpnext.accounts.doctype.sales_invoice.sales_invoice import make_inter_company_transaction
return make_inter_company_transaction("Purchase Order", source_name, target_doc) 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 ''
})

View File

@ -20,7 +20,6 @@ from erpnext.controllers.status_updater import OverAllowanceError
from erpnext.manufacturing.doctype.blanket_order.test_blanket_order import make_blanket_order 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.stock.doctype.batch.test_batch import make_new_batch
from erpnext.controllers.buying_controller import get_backflushed_subcontracted_raw_materials
class TestPurchaseOrder(unittest.TestCase): class TestPurchaseOrder(unittest.TestCase):
def test_make_purchase_receipt(self): def test_make_purchase_receipt(self):
@ -771,7 +770,7 @@ class TestPurchaseOrder(unittest.TestCase):
self.assertEqual(bin11.reserved_qty_for_sub_contract, bin1.reserved_qty_for_sub_contract) self.assertEqual(bin11.reserved_qty_for_sub_contract, bin1.reserved_qty_for_sub_contract)
def test_exploded_items_in_subcontracted(self): 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) make_subcontracted_item(item_code=item_code)
po = create_purchase_order(item_code=item_code, qty=1, po = create_purchase_order(item_code=item_code, qty=1,
@ -848,79 +847,6 @@ class TestPurchaseOrder(unittest.TestCase):
for item in rm_items: for item in rm_items:
transferred_rm_map[item.get('rm_item_code')] = item 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") update_backflush_based_on("BOM")
def test_supplied_qty_against_subcontracted_po(self): def test_supplied_qty_against_subcontracted_po(self):
@ -1117,22 +1043,29 @@ def create_purchase_order(**args):
po.conversion_factor = args.conversion_factor or 1 po.conversion_factor = args.conversion_factor or 1
po.supplier_warehouse = args.supplier_warehouse or None po.supplier_warehouse = args.supplier_warehouse or None
po.append("items", { if args.rm_items:
"item_code": args.item or args.item_code or "_Test Item", for row in args.rm_items:
"warehouse": args.warehouse or "_Test Warehouse - _TC", po.append("items", row)
"qty": args.qty or 10, else:
"rate": args.rate or 500, po.append("items", {
"schedule_date": add_days(nowdate(), 1), "item_code": args.item or args.item_code or "_Test Item",
"include_exploded_items": args.get('include_exploded_items', 1), "warehouse": args.warehouse or "_Test Warehouse - _TC",
"against_blanket_order": args.against_blanket_order "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: if not args.do_not_save:
po.insert() po.insert()
if not args.do_not_submit: if not args.do_not_submit:
if po.is_subcontracted == "Yes": if po.is_subcontracted == "Yes":
supp_items = po.get("supplied_items") supp_items = po.get("supplied_items")
for d in supp_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() po.submit()
return po return po

View File

@ -6,21 +6,25 @@
"engine": "InnoDB", "engine": "InnoDB",
"field_order": [ "field_order": [
"main_item_code", "main_item_code",
"bom_detail_no", "rm_item_code",
"column_break_3",
"stock_uom", "stock_uom",
"reserve_warehouse",
"conversion_factor", "conversion_factor",
"column_break_6", "column_break_6",
"rm_item_code", "bom_detail_no",
"reference_name", "reference_name",
"reserve_warehouse",
"section_break2", "section_break2",
"rate", "rate",
"col_break2", "col_break2",
"amount", "amount",
"section_break1", "section_break1",
"required_qty", "required_qty",
"supplied_qty",
"col_break1", "col_break1",
"supplied_qty" "consumed_qty",
"returned_qty",
"total_supplied_qty"
], ],
"fields": [ "fields": [
{ {
@ -125,6 +129,8 @@
"fieldtype": "Float", "fieldtype": "Float",
"in_list_view": 1, "in_list_view": 1,
"label": "Supplied Qty", "label": "Supplied Qty",
"no_copy": 1,
"print_hide": 1,
"read_only": 1 "read_only": 1
}, },
{ {
@ -142,13 +148,42 @@
{ {
"fieldname": "col_break2", "fieldname": "col_break2",
"fieldtype": "Column Break" "fieldtype": "Column Break"
},
{
"fieldname": "consumed_qty",
"fieldtype": "Float",
"label": "Consumed Qty",
"no_copy": 1,
"print_hide": 1,
"read_only": 1
},
{
"fieldname": "returned_qty",
"fieldtype": "Float",
"label": "Returned Qty",
"no_copy": 1,
"print_hide": 1,
"read_only": 1
},
{
"fieldname": "total_supplied_qty",
"fieldtype": "Float",
"hidden": 1,
"label": "Total Supplied Qty",
"no_copy": 1,
"print_hide": 1,
"read_only": 1
},
{
"fieldname": "column_break_3",
"fieldtype": "Column Break"
} }
], ],
"hide_toolbar": 1, "hide_toolbar": 1,
"idx": 1, "idx": 1,
"istable": 1, "istable": 1,
"links": [], "links": [],
"modified": "2020-09-18 17:26:09.703215", "modified": "2021-06-09 15:17:58.128242",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Buying", "module": "Buying",
"name": "Purchase Order Item Supplied", "name": "Purchase Order Item Supplied",

View File

@ -6,10 +6,11 @@
"engine": "InnoDB", "engine": "InnoDB",
"field_order": [ "field_order": [
"main_item_code", "main_item_code",
"description", "rm_item_code",
"item_name",
"bom_detail_no", "bom_detail_no",
"col_break1", "col_break1",
"rm_item_code", "description",
"stock_uom", "stock_uom",
"conversion_factor", "conversion_factor",
"reference_name", "reference_name",
@ -25,7 +26,8 @@
"secbreak_3", "secbreak_3",
"batch_no", "batch_no",
"col_break4", "col_break4",
"serial_no" "serial_no",
"purchase_order"
], ],
"fields": [ "fields": [
{ {
@ -52,7 +54,6 @@
"fieldname": "description", "fieldname": "description",
"fieldtype": "Text Editor", "fieldtype": "Text Editor",
"in_global_search": 1, "in_global_search": 1,
"in_list_view": 1,
"label": "Description", "label": "Description",
"oldfieldname": "description", "oldfieldname": "description",
"oldfieldtype": "Data", "oldfieldtype": "Data",
@ -81,18 +82,20 @@
"fieldname": "required_qty", "fieldname": "required_qty",
"fieldtype": "Float", "fieldtype": "Float",
"in_list_view": 1, "in_list_view": 1,
"label": "Required Qty", "label": "Available Qty For Consumption",
"oldfieldname": "required_qty", "oldfieldname": "required_qty",
"oldfieldtype": "Currency", "oldfieldtype": "Currency",
"print_hide": 1,
"read_only": 1 "read_only": 1
}, },
{ {
"columns": 2,
"fieldname": "consumed_qty", "fieldname": "consumed_qty",
"fieldtype": "Float", "fieldtype": "Float",
"label": "Consumed Qty", "in_list_view": 1,
"label": "Qty to Be Consumed",
"oldfieldname": "consumed_qty", "oldfieldname": "consumed_qty",
"oldfieldtype": "Currency", "oldfieldtype": "Currency",
"read_only": 1,
"reqd": 1 "reqd": 1
}, },
{ {
@ -183,12 +186,28 @@
{ {
"fieldname": "col_break4", "fieldname": "col_break4",
"fieldtype": "Column Break" "fieldtype": "Column Break"
},
{
"fieldname": "item_name",
"fieldtype": "Data",
"label": "Item Name",
"read_only": 1
},
{
"fieldname": "purchase_order",
"fieldtype": "Link",
"hidden": 1,
"label": "Purchase Order",
"no_copy": 1,
"options": "Purchase Order",
"print_hide": 1,
"read_only": 1
} }
], ],
"idx": 1, "idx": 1,
"istable": 1, "istable": 1,
"links": [], "links": [],
"modified": "2020-09-18 17:26:09.703215", "modified": "2021-06-19 19:33:04.431213",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Buying", "module": "Buying",
"name": "Purchase Receipt Item Supplied", "name": "Purchase Receipt Item Supplied",

View File

@ -317,19 +317,21 @@ def add_items(sq_doc, supplier, items):
create_rfq_items(sq_doc, supplier, data) create_rfq_items(sq_doc, supplier, data)
def create_rfq_items(sq_doc, supplier, data): def create_rfq_items(sq_doc, supplier, data):
sq_doc.append('items', { args = {}
"item_code": data.item_code,
"item_name": data.item_name, for field in ['item_code', 'item_name', 'description', 'qty', 'rate', 'conversion_factor',
"description": data.description, 'warehouse', 'material_request', 'material_request_item', 'stock_qty']:
"qty": data.qty, args[field] = data.get(field)
"rate": data.rate,
"conversion_factor": data.conversion_factor if data.conversion_factor else None, args.update({
"supplier_part_no": frappe.db.get_value("Item Supplier", {'parent': data.item_code, 'supplier': supplier}, "supplier_part_no"),
"warehouse": data.warehouse or '',
"request_for_quotation_item": data.name, "request_for_quotation_item": data.name,
"request_for_quotation": data.parent "request_for_quotation": data.parent,
"supplier_part_no": frappe.db.get_value("Item Supplier",
{'parent': data.item_code, 'supplier': supplier}, "supplier_part_no")
}) })
sq_doc.append('items', args)
@frappe.whitelist() @frappe.whitelist()
def get_pdf(doctype, name, supplier): def get_pdf(doctype, name, supplier):
doc = get_rfq_doc(doctype, name, supplier) doc = get_rfq_doc(doctype, name, supplier)

View File

@ -0,0 +1,45 @@
// Copyright (c) 2016, Frappe Technologies Pvt. Ltd. and contributors
// For license information, please see license.txt
/* eslint-disable */
frappe.query_reports["Subcontract Order Summary"] = {
"filters": [
{
label: __("Company"),
fieldname: "company",
fieldtype: "Link",
options: "Company",
default: frappe.defaults.get_user_default("Company"),
reqd: 1
},
{
label: __("From Date"),
fieldname:"from_date",
fieldtype: "Date",
default: frappe.datetime.add_months(frappe.datetime.get_today(), -1),
reqd: 1
},
{
label: __("To Date"),
fieldname:"to_date",
fieldtype: "Date",
default: frappe.datetime.get_today(),
reqd: 1
},
{
label: __("Purchase Order"),
fieldname: "name",
fieldtype: "Link",
options: "Purchase Order",
get_query: function() {
return {
filters: {
docstatus: 1,
is_subcontracted: 'Yes',
company: frappe.query_report.get_filter_value('company')
}
}
}
}
]
};

View File

@ -0,0 +1,32 @@
{
"add_total_row": 0,
"columns": [],
"creation": "2021-05-31 14:43:32.417694",
"disable_prepared_report": 0,
"disabled": 0,
"docstatus": 0,
"doctype": "Report",
"filters": [],
"idx": 0,
"is_standard": "Yes",
"modified": "2021-05-31 14:43:32.417694",
"modified_by": "Administrator",
"module": "Buying",
"name": "Subcontract Order Summary",
"owner": "Administrator",
"prepared_report": 0,
"ref_doctype": "Purchase Order",
"report_name": "Subcontract Order Summary",
"report_type": "Script Report",
"roles": [
{
"role": "Stock User"
},
{
"role": "Purchase Manager"
},
{
"role": "Purchase User"
}
]
}

View File

@ -0,0 +1,152 @@
# Copyright (c) 2013, Frappe Technologies Pvt. Ltd. and contributors
# For license information, please see license.txt
from __future__ import unicode_literals
import frappe
from frappe import _
def execute(filters=None):
columns, data = [], []
columns = get_columns()
data = get_data(filters)
return columns, data
def get_data(report_filters):
data = []
orders = get_subcontracted_orders(report_filters)
if orders:
supplied_items = get_supplied_items(orders, report_filters)
po_details = prepare_subcontracted_data(orders, supplied_items)
get_subcontracted_data(po_details, data)
return data
def get_subcontracted_orders(report_filters):
fields = ['`tabPurchase Order Item`.`parent` as po_id', '`tabPurchase Order Item`.`item_code`',
'`tabPurchase Order Item`.`item_name`', '`tabPurchase Order Item`.`qty`', '`tabPurchase Order Item`.`name`',
'`tabPurchase Order Item`.`received_qty`', '`tabPurchase Order`.`status`']
filters = get_filters(report_filters)
return frappe.get_all('Purchase Order', fields = fields, filters=filters) or []
def get_filters(report_filters):
filters = [['Purchase Order', 'docstatus', '=', 1], ['Purchase Order', 'is_subcontracted', '=', 'Yes'],
['Purchase Order', 'transaction_date', 'between', (report_filters.from_date, report_filters.to_date)]]
for field in ['name', 'company']:
if report_filters.get(field):
filters.append(['Purchase Order', field, '=', report_filters.get(field)])
return filters
def get_supplied_items(orders, report_filters):
if not orders:
return []
fields = ['parent', 'main_item_code', 'rm_item_code', 'required_qty',
'supplied_qty', 'returned_qty', 'total_supplied_qty', 'consumed_qty', 'reference_name']
filters = {'parent': ('in', [d.po_id for d in orders]), 'docstatus': 1}
supplied_items = {}
for row in frappe.get_all('Purchase Order Item Supplied', fields = fields, filters=filters):
new_key = (row.parent, row.reference_name, row.main_item_code)
supplied_items.setdefault(new_key, []).append(row)
return supplied_items
def prepare_subcontracted_data(orders, supplied_items):
po_details = {}
for row in orders:
key = (row.po_id, row.name, row.item_code)
if key not in po_details:
po_details.setdefault(key, frappe._dict({'po_item': row, 'supplied_items': []}))
details = po_details[key]
if supplied_items.get(key):
for supplied_item in supplied_items[key]:
details['supplied_items'].append(supplied_item)
return po_details
def get_subcontracted_data(po_details, data):
for key, details in po_details.items():
res = details.po_item
for index, row in enumerate(details.supplied_items):
if index != 0:
res = {}
res.update(row)
data.append(res)
def get_columns():
return [
{
"label": _("Purchase Order"),
"fieldname": "po_id",
"fieldtype": "Link",
"options": "Purchase Order",
"width": 100
},
{
"label": _("Status"),
"fieldname": "status",
"fieldtype": "Data",
"width": 80
},
{
"label": _("Subcontracted Item"),
"fieldname": "item_code",
"fieldtype": "Link",
"options": "Item",
"width": 160
},
{
"label": _("Order Qty"),
"fieldname": "qty",
"fieldtype": "Float",
"width": 90
},
{
"label": _("Received Qty"),
"fieldname": "received_qty",
"fieldtype": "Float",
"width": 110
},
{
"label": _("Supplied Item"),
"fieldname": "rm_item_code",
"fieldtype": "Link",
"options": "Item",
"width": 160
},
{
"label": _("Required Qty"),
"fieldname": "required_qty",
"fieldtype": "Float",
"width": 110
},
{
"label": _("Supplied Qty"),
"fieldname": "supplied_qty",
"fieldtype": "Float",
"width": 110
},
{
"label": _("Consumed Qty"),
"fieldname": "consumed_qty",
"fieldtype": "Float",
"width": 120
},
{
"label": _("Returned Qty"),
"fieldname": "returned_qty",
"fieldtype": "Float",
"width": 110
}
]

View File

@ -11,16 +11,17 @@ from erpnext.accounts.party import get_party_details
from erpnext.stock.get_item_details import get_conversion_factor from erpnext.stock.get_item_details import get_conversion_factor
from erpnext.buying.utils import validate_for_items, update_last_purchase_rate from erpnext.buying.utils import validate_for_items, update_last_purchase_rate
from erpnext.stock.stock_ledger import get_valuation_rate from erpnext.stock.stock_ledger import get_valuation_rate
from erpnext.stock.doctype.stock_entry.stock_entry import get_used_alternative_items
from erpnext.stock.doctype.serial_no.serial_no import get_auto_serial_nos, auto_make_serial_nos, get_serial_nos from erpnext.stock.doctype.serial_no.serial_no import get_auto_serial_nos, auto_make_serial_nos, get_serial_nos
from frappe.contacts.doctype.address.address import get_address_display from frappe.contacts.doctype.address.address import get_address_display
from erpnext.accounts.doctype.budget.budget import validate_expense_against_budget from erpnext.accounts.doctype.budget.budget import validate_expense_against_budget
from erpnext.controllers.stock_controller import StockController
from erpnext.controllers.sales_and_purchase_return import get_rate_for_return from erpnext.controllers.sales_and_purchase_return import get_rate_for_return
from erpnext.stock.utils import get_incoming_rate from erpnext.stock.utils import get_incoming_rate
class BuyingController(StockController): from erpnext.controllers.stock_controller import StockController
from erpnext.controllers.subcontracting import Subcontracting
class BuyingController(StockController, Subcontracting):
def get_feed(self): def get_feed(self):
if self.get("supplier_name"): if self.get("supplier_name"):
@ -57,6 +58,11 @@ class BuyingController(StockController):
if self.doctype in ("Purchase Receipt", "Purchase Invoice"): if self.doctype in ("Purchase Receipt", "Purchase Invoice"):
self.update_valuation_rate() 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): def set_missing_values(self, for_validate=False):
super(BuyingController, self).set_missing_values(for_validate) super(BuyingController, self).set_missing_values(for_validate)
@ -171,12 +177,13 @@ class BuyingController(StockController):
TODO: rename item_tax_amount to valuation_tax_amount TODO: rename item_tax_amount to valuation_tax_amount
""" """
stock_and_asset_items = []
stock_and_asset_items = self.get_stock_items() + self.get_asset_items() stock_and_asset_items = self.get_stock_items() + self.get_asset_items()
stock_and_asset_items_qty, stock_and_asset_items_amount = 0, 0 stock_and_asset_items_qty, stock_and_asset_items_amount = 0, 0
last_item_idx = 1 last_item_idx = 1
for d in self.get("items"): for d in self.get("items"):
if d.item_code and d.item_code in stock_and_asset_items: if (d.item_code and d.item_code in stock_and_asset_items):
stock_and_asset_items_qty += flt(d.qty) stock_and_asset_items_qty += flt(d.qty)
stock_and_asset_items_amount += flt(d.base_net_amount) stock_and_asset_items_amount += flt(d.base_net_amount)
last_item_idx = d.idx last_item_idx = d.idx
@ -255,7 +262,7 @@ class BuyingController(StockController):
supplied_items_cost = 0.0 supplied_items_cost = 0.0
for d in self.get("supplied_items"): for d in self.get("supplied_items"):
if d.reference_name == item_row_id: if d.reference_name == item_row_id:
if reset_outgoing_rate and frappe.db.get_value('Item', d.rm_item_code, 'is_stock_item'): if reset_outgoing_rate and frappe.get_cached_value('Item', d.rm_item_code, 'is_stock_item'):
rate = get_incoming_rate({ rate = get_incoming_rate({
"item_code": d.rm_item_code, "item_code": d.rm_item_code,
"warehouse": self.supplier_warehouse, "warehouse": self.supplier_warehouse,
@ -285,11 +292,13 @@ class BuyingController(StockController):
if item in self.sub_contracted_items and not item.bom: if item in self.sub_contracted_items and not item.bom:
frappe.throw(_("Please select BOM in BOM field for Item {0}").format(item.item_code)) frappe.throw(_("Please select BOM in BOM field for Item {0}").format(item.item_code))
if self.doctype == "Purchase Order": if self.doctype != "Purchase Order":
for supplied_item in self.get("supplied_items"): return
if not supplied_item.reserve_warehouse:
frappe.throw(_("Reserved Warehouse is mandatory for Item {0} in Raw Materials supplied").format(frappe.bold(supplied_item.rm_item_code)))
for row in self.get("supplied_items"):
if not row.reserve_warehouse:
msg = f"Reserved Warehouse is mandatory for the Item {frappe.bold(row.rm_item_code)} in Raw Materials supplied"
frappe.throw(_(msg))
else: else:
for item in self.get("items"): for item in self.get("items"):
if item.bom: if item.bom:
@ -297,23 +306,7 @@ class BuyingController(StockController):
def create_raw_materials_supplied(self, raw_material_table): def create_raw_materials_supplied(self, raw_material_table):
if self.is_subcontracted=="Yes": if self.is_subcontracted=="Yes":
parent_items = [] self.set_materials_for_subcontracted_items(raw_material_table)
backflush_raw_materials_based_on = frappe.db.get_single_value("Buying Settings",
"backflush_raw_materials_of_subcontract_based_on")
if (self.doctype == 'Purchase Receipt' and
backflush_raw_materials_based_on != 'BOM'):
self.update_raw_materials_supplied_based_on_stock_entries()
else:
for item in self.get("items"):
if self.doctype in ["Purchase Receipt", "Purchase Invoice"]:
item.rm_supp_cost = 0.0
if item.bom and item.item_code in self.sub_contracted_items:
self.update_raw_materials_supplied_based_on_bom(item, raw_material_table)
if [item.item_code, item.name] not in parent_items:
parent_items.append([item.item_code, item.name])
self.cleanup_raw_materials_supplied(parent_items, raw_material_table)
elif self.doctype in ["Purchase Receipt", "Purchase Invoice"]: elif self.doctype in ["Purchase Receipt", "Purchase Invoice"]:
for item in self.get("items"): for item in self.get("items"):
@ -322,176 +315,6 @@ class BuyingController(StockController):
if self.is_subcontracted == "No" and self.get("supplied_items"): if self.is_subcontracted == "No" and self.get("supplied_items"):
self.set('supplied_items', []) self.set('supplied_items', [])
def update_raw_materials_supplied_based_on_stock_entries(self):
self.set('supplied_items', [])
purchase_orders = set(d.purchase_order for d in self.items)
# qty of raw materials backflushed (for each item per purchase order)
backflushed_raw_materials_map = get_backflushed_subcontracted_raw_materials(purchase_orders)
# qty of "finished good" item yet to be received
qty_to_be_received_map = get_qty_to_be_received(purchase_orders)
for item in self.get('items'):
if not item.purchase_order:
continue
# reset raw_material cost
item.rm_supp_cost = 0
# qty of raw materials transferred to the supplier
transferred_raw_materials = get_subcontracted_raw_materials_from_se(item.purchase_order, item.item_code)
non_stock_items = get_non_stock_items(item.purchase_order, item.item_code)
item_key = '{}{}'.format(item.item_code, item.purchase_order)
fg_yet_to_be_received = qty_to_be_received_map.get(item_key)
if not fg_yet_to_be_received:
frappe.throw(_("Row #{0}: Item {1} is already fully received in Purchase Order {2}")
.format(item.idx, frappe.bold(item.item_code),
frappe.utils.get_link_to_form("Purchase Order", item.purchase_order)),
title=_("Limit Crossed"))
transferred_batch_qty_map = get_transferred_batch_qty_map(item.purchase_order, item.item_code)
# backflushed_batch_qty_map = get_backflushed_batch_qty_map(item.purchase_order, item.item_code)
for raw_material in transferred_raw_materials + non_stock_items:
rm_item_key = (raw_material.rm_item_code, item.item_code, item.purchase_order)
raw_material_data = backflushed_raw_materials_map.get(rm_item_key, {})
consumed_qty = raw_material_data.get('qty', 0)
consumed_serial_nos = raw_material_data.get('serial_no', '')
consumed_batch_nos = raw_material_data.get('batch_nos', '')
transferred_qty = raw_material.qty
rm_qty_to_be_consumed = transferred_qty - consumed_qty
# backflush all remaining transferred qty in the last Purchase Receipt
if fg_yet_to_be_received == item.qty:
qty = rm_qty_to_be_consumed
else:
qty = (rm_qty_to_be_consumed / fg_yet_to_be_received) * item.qty
if frappe.get_cached_value('UOM', raw_material.stock_uom, 'must_be_whole_number'):
qty = frappe.utils.ceil(qty)
if qty > rm_qty_to_be_consumed:
qty = rm_qty_to_be_consumed
if not qty: continue
if raw_material.serial_nos:
set_serial_nos(raw_material, consumed_serial_nos, qty)
if raw_material.batch_nos:
backflushed_batch_qty_map = raw_material_data.get('consumed_batch', {})
batches_qty = get_batches_with_qty(raw_material.rm_item_code, raw_material.main_item_code,
qty, transferred_batch_qty_map, backflushed_batch_qty_map, item.purchase_order)
for batch_data in batches_qty:
qty = batch_data['qty']
raw_material.batch_no = batch_data['batch']
if qty > 0:
self.append_raw_material_to_be_backflushed(item, raw_material, qty)
else:
self.append_raw_material_to_be_backflushed(item, raw_material, qty)
def append_raw_material_to_be_backflushed(self, fg_item_row, raw_material_data, qty):
rm = self.append('supplied_items', {})
rm.update(raw_material_data)
if not rm.main_item_code:
rm.main_item_code = fg_item_row.item_code
rm.reference_name = fg_item_row.name
rm.required_qty = qty
rm.consumed_qty = qty
def update_raw_materials_supplied_based_on_bom(self, item, raw_material_table):
exploded_item = 1
if hasattr(item, 'include_exploded_items'):
exploded_item = item.get('include_exploded_items')
bom_items = get_items_from_bom(item.item_code, item.bom, exploded_item)
used_alternative_items = []
if self.doctype in ["Purchase Receipt", "Purchase Invoice"] and item.purchase_order:
used_alternative_items = get_used_alternative_items(purchase_order = item.purchase_order)
raw_materials_cost = 0
items = list(set([d.item_code for d in bom_items]))
item_wh = frappe._dict(frappe.db.sql("""select i.item_code, id.default_warehouse
from `tabItem` i, `tabItem Default` id
where id.parent=i.name and id.company=%s and i.name in ({0})"""
.format(", ".join(["%s"] * len(items))), [self.company] + items))
for bom_item in bom_items:
if self.doctype == "Purchase Order":
reserve_warehouse = bom_item.source_warehouse or item_wh.get(bom_item.item_code)
if frappe.db.get_value("Warehouse", reserve_warehouse, "company") != self.company:
reserve_warehouse = None
conversion_factor = item.conversion_factor
if (self.doctype in ["Purchase Receipt", "Purchase Invoice"] and item.purchase_order and
bom_item.item_code in used_alternative_items):
alternative_item_data = used_alternative_items.get(bom_item.item_code)
bom_item.item_code = alternative_item_data.item_code
bom_item.item_name = alternative_item_data.item_name
bom_item.stock_uom = alternative_item_data.stock_uom
conversion_factor = alternative_item_data.conversion_factor
bom_item.description = alternative_item_data.description
# check if exists
exists = 0
for d in self.get(raw_material_table):
if d.main_item_code == item.item_code and d.rm_item_code == bom_item.item_code \
and d.reference_name == item.name:
rm, exists = d, 1
break
if not exists:
rm = self.append(raw_material_table, {})
required_qty = flt(flt(bom_item.qty_consumed_per_unit) * (flt(item.qty) + getattr(item, 'rejected_qty', 0)) *
flt(conversion_factor), rm.precision("required_qty"))
rm.reference_name = item.name
rm.bom_detail_no = bom_item.name
rm.main_item_code = item.item_code
rm.rm_item_code = bom_item.item_code
rm.stock_uom = bom_item.stock_uom
rm.required_qty = required_qty
rm.rate = bom_item.rate
rm.conversion_factor = conversion_factor
if self.doctype in ["Purchase Receipt", "Purchase Invoice"]:
rm.consumed_qty = required_qty
rm.description = bom_item.description
if item.batch_no and frappe.db.get_value("Item", rm.rm_item_code, "has_batch_no") and not rm.batch_no:
rm.batch_no = item.batch_no
elif not rm.reserve_warehouse:
rm.reserve_warehouse = reserve_warehouse
def cleanup_raw_materials_supplied(self, parent_items, raw_material_table):
"""Remove all those child items which are no longer present in main item table"""
delete_list = []
for d in self.get(raw_material_table):
if [d.main_item_code, d.reference_name] not in parent_items:
# mark for deletion from doclist
delete_list.append(d)
# delete from doclist
if delete_list:
rm_supplied_details = self.get(raw_material_table)
self.set(raw_material_table, [])
for d in rm_supplied_details:
if d not in delete_list:
self.append(raw_material_table, d)
@property @property
def sub_contracted_items(self): def sub_contracted_items(self):
if not hasattr(self, "_sub_contracted_items"): if not hasattr(self, "_sub_contracted_items"):
@ -683,7 +506,8 @@ class BuyingController(StockController):
self.process_fixed_asset() self.process_fixed_asset()
self.update_fixed_asset(field) self.update_fixed_asset(field)
update_last_purchase_rate(self, is_submit = 1) if self.doctype in ['Purchase Order', 'Purchase Receipt']:
update_last_purchase_rate(self, is_submit = 1)
def on_cancel(self): def on_cancel(self):
super(BuyingController, self).on_cancel() super(BuyingController, self).on_cancel()
@ -691,7 +515,9 @@ class BuyingController(StockController):
if self.get('is_return'): if self.get('is_return'):
return return
update_last_purchase_rate(self, is_submit = 0) if self.doctype in ['Purchase Order', 'Purchase Receipt']:
update_last_purchase_rate(self, is_submit = 0)
if self.doctype in ['Purchase Receipt', 'Purchase Invoice']: if self.doctype in ['Purchase Receipt', 'Purchase Invoice']:
field = 'purchase_invoice' if self.doctype == 'Purchase Invoice' else 'purchase_receipt' field = 'purchase_invoice' if self.doctype == 'Purchase Invoice' else 'purchase_receipt'
@ -863,104 +689,6 @@ class BuyingController(StockController):
else: else:
validate_item_type(self, "is_purchase_item", "purchase") validate_item_type(self, "is_purchase_item", "purchase")
def get_items_from_bom(item_code, bom, exploded_item=1):
doctype = "BOM Item" if not exploded_item else "BOM Explosion Item"
bom_items = frappe.db.sql("""select t2.item_code, t2.name,
t2.rate, t2.stock_uom, t2.source_warehouse, t2.description,
t2.stock_qty / ifnull(t1.quantity, 1) as qty_consumed_per_unit
from
`tabBOM` t1, `tab{0}` t2, tabItem t3
where
t2.parent = t1.name and t1.item = %s
and t1.docstatus = 1 and t1.is_active = 1 and t1.name = %s
and t2.sourced_by_supplier = 0
and t2.item_code = t3.name""".format(doctype),
(item_code, bom), as_dict=1)
if not bom_items:
msgprint(_("Specified BOM {0} does not exist for Item {1}").format(bom, item_code), raise_exception=1)
return bom_items
def get_subcontracted_raw_materials_from_se(purchase_order, fg_item):
common_query = """
SELECT
sed.item_code AS rm_item_code,
SUM(sed.qty) AS qty,
sed.description,
sed.stock_uom,
sed.subcontracted_item AS main_item_code,
{serial_no_concat_syntax} AS serial_nos,
{batch_no_concat_syntax} AS batch_nos
FROM `tabStock Entry` se,`tabStock Entry Detail` sed
WHERE
se.name = sed.parent
AND se.docstatus=1
AND se.purpose='Send to Subcontractor'
AND se.purchase_order = %s
AND IFNULL(sed.t_warehouse, '') != ''
AND IFNULL(sed.subcontracted_item, '') in ('', %s)
GROUP BY sed.item_code, sed.subcontracted_item
"""
raw_materials = frappe.db.multisql({
'mariadb': common_query.format(
serial_no_concat_syntax="GROUP_CONCAT(sed.serial_no)",
batch_no_concat_syntax="GROUP_CONCAT(sed.batch_no)"
),
'postgres': common_query.format(
serial_no_concat_syntax="STRING_AGG(sed.serial_no, ',')",
batch_no_concat_syntax="STRING_AGG(sed.batch_no, ',')"
)
}, (purchase_order, fg_item), as_dict=1)
return raw_materials
def get_backflushed_subcontracted_raw_materials(purchase_orders):
purchase_receipts = frappe.get_all("Purchase Receipt Item",
fields = ["purchase_order", "item_code", "name", "parent"],
filters={"docstatus": 1, "purchase_order": ("in", list(purchase_orders))})
distinct_purchase_receipts = {}
for pr in purchase_receipts:
key = (pr.purchase_order, pr.item_code, pr.parent)
distinct_purchase_receipts.setdefault(key, []).append(pr.name)
backflushed_raw_materials_map = frappe._dict()
for args, references in iteritems(distinct_purchase_receipts):
purchase_receipt_supplied_items = get_supplied_items(args[1], args[2], references)
for data in purchase_receipt_supplied_items:
pr_key = (data.rm_item_code, data.main_item_code, args[0])
if pr_key not in backflushed_raw_materials_map:
backflushed_raw_materials_map.setdefault(pr_key, frappe._dict({
"qty": 0.0,
"serial_no": [],
"batch_no": [],
"consumed_batch": {}
}))
row = backflushed_raw_materials_map.get(pr_key)
row.qty += data.consumed_qty
for field in ["serial_no", "batch_no"]:
if data.get(field):
row[field].append(data.get(field))
if data.get("batch_no"):
if data.get("batch_no") in row.consumed_batch:
row.consumed_batch[data.get("batch_no")] += data.consumed_qty
else:
row.consumed_batch[data.get("batch_no")] = data.consumed_qty
return backflushed_raw_materials_map
def get_supplied_items(item_code, purchase_receipt, references):
return frappe.get_all("Purchase Receipt Item Supplied",
fields=["rm_item_code", "main_item_code", "consumed_qty", "serial_no", "batch_no"],
filters={"main_item_code": item_code, "parent": purchase_receipt, "reference_name": ("in", references)})
def get_asset_item_details(asset_items): def get_asset_item_details(asset_items):
asset_items_data = {} asset_items_data = {}
for d in frappe.get_all('Item', fields = ["name", "auto_create_assets", "asset_naming_series"], for d in frappe.get_all('Item', fields = ["name", "auto_create_assets", "asset_naming_series"],
@ -992,135 +720,3 @@ def validate_item_type(doc, fieldname, message):
error_message = _("Following item {0} is not marked as {1} item. You can enable them as {1} item from its Item master").format(items, message) error_message = _("Following item {0} is not marked as {1} item. You can enable them as {1} item from its Item master").format(items, message)
frappe.throw(error_message) frappe.throw(error_message)
def get_qty_to_be_received(purchase_orders):
return frappe._dict(frappe.db.sql("""
SELECT CONCAT(poi.`item_code`, poi.`parent`) AS item_key,
SUM(poi.`qty`) - SUM(poi.`received_qty`) AS qty_to_be_received
FROM `tabPurchase Order Item` poi
WHERE
poi.`parent` in %s
GROUP BY poi.`item_code`, poi.`parent`
HAVING SUM(poi.`qty`) > SUM(poi.`received_qty`)
""", (purchase_orders)))
def get_non_stock_items(purchase_order, fg_item_code):
return frappe.db.sql("""
SELECT
pois.main_item_code,
pois.rm_item_code,
item.description,
pois.required_qty AS qty,
pois.rate,
1 as non_stock_item,
pois.stock_uom
FROM `tabPurchase Order Item Supplied` pois, `tabItem` item
WHERE
pois.`rm_item_code` = item.`name`
AND item.is_stock_item = 0
AND pois.`parent` = %s
AND pois.`main_item_code` = %s
""", (purchase_order, fg_item_code), as_dict=1)
def set_serial_nos(raw_material, consumed_serial_nos, qty):
serial_nos = set(get_serial_nos(raw_material.serial_nos)) - \
set(get_serial_nos(consumed_serial_nos))
if serial_nos and qty <= len(serial_nos):
raw_material.serial_no = '\n'.join(list(serial_nos)[0:frappe.utils.cint(qty)])
def get_transferred_batch_qty_map(purchase_order, fg_item):
# returns
# {
# (item_code, fg_code): {
# batch1: 10, # qty
# batch2: 16
# },
# }
transferred_batch_qty_map = {}
transferred_batches = frappe.db.sql("""
SELECT
sed.batch_no,
SUM(sed.qty) AS qty,
sed.item_code,
sed.subcontracted_item
FROM `tabStock Entry` se,`tabStock Entry Detail` sed
WHERE
se.name = sed.parent
AND se.docstatus=1
AND se.purpose='Send to Subcontractor'
AND se.purchase_order = %s
AND ifnull(sed.subcontracted_item, '') in ('', %s)
AND sed.batch_no IS NOT NULL
GROUP BY
sed.batch_no,
sed.item_code
""", (purchase_order, fg_item), as_dict=1)
for batch_data in transferred_batches:
key = ((batch_data.item_code, fg_item)
if batch_data.subcontracted_item else (batch_data.item_code, purchase_order))
transferred_batch_qty_map.setdefault(key, OrderedDict())
transferred_batch_qty_map[key][batch_data.batch_no] = batch_data.qty
return transferred_batch_qty_map
def get_backflushed_batch_qty_map(purchase_order, fg_item):
# returns
# {
# (item_code, fg_code): {
# batch1: 10, # qty
# batch2: 16
# },
# }
backflushed_batch_qty_map = {}
backflushed_batches = frappe.db.sql("""
SELECT
pris.batch_no,
SUM(pris.consumed_qty) AS qty,
pris.rm_item_code AS item_code
FROM `tabPurchase Receipt` pr, `tabPurchase Receipt Item` pri, `tabPurchase Receipt Item Supplied` pris
WHERE
pr.name = pri.parent
AND pri.parent = pris.parent
AND pri.purchase_order = %s
AND pri.item_code = pris.main_item_code
AND pr.docstatus = 1
AND pris.main_item_code = %s
AND pris.batch_no IS NOT NULL
GROUP BY
pris.rm_item_code, pris.batch_no
""", (purchase_order, fg_item), as_dict=1)
for batch_data in backflushed_batches:
backflushed_batch_qty_map.setdefault((batch_data.item_code, fg_item), {})
backflushed_batch_qty_map[(batch_data.item_code, fg_item)][batch_data.batch_no] = batch_data.qty
return backflushed_batch_qty_map
def get_batches_with_qty(item_code, fg_item, required_qty, transferred_batch_qty_map, backflushed_batches, po):
# Returns available batches to be backflushed based on requirements
transferred_batches = transferred_batch_qty_map.get((item_code, fg_item), {})
if not transferred_batches:
transferred_batches = transferred_batch_qty_map.get((item_code, po), {})
available_batches = []
for (batch, transferred_qty) in transferred_batches.items():
backflushed_qty = backflushed_batches.get(batch, 0)
available_qty = transferred_qty - backflushed_qty
if available_qty >= required_qty:
available_batches.append({'batch': batch, 'qty': required_qty})
break
elif available_qty != 0:
available_batches.append({'batch': batch, 'qty': available_qty})
required_qty -= available_qty
for row in available_batches:
if backflushed_batches.get(row.get('batch'), 0) > 0:
backflushed_batches[row.get('batch')] += row.get('qty')
else:
backflushed_batches[row.get('batch')] = row.get('qty')
return available_batches

View File

@ -19,7 +19,7 @@ def employee_query(doctype, txt, searchfield, start, page_len, filters):
fields = get_fields("Employee", ["name", "employee_name"]) fields = get_fields("Employee", ["name", "employee_name"])
return frappe.db.sql("""select {fields} from `tabEmployee` return frappe.db.sql("""select {fields} from `tabEmployee`
where status = 'Active' where status in ('Active', 'Suspended')
and docstatus < 2 and docstatus < 2
and ({key} like %(txt)s and ({key} like %(txt)s
or employee_name like %(txt)s) or employee_name like %(txt)s)
@ -315,7 +315,7 @@ def get_project_name(doctype, txt, searchfield, start, page_len, filters):
return frappe.db.sql("""select {fields} from `tabProject` return frappe.db.sql("""select {fields} from `tabProject`
where where
`tabProject`.status not in ("Completed", "Cancelled") `tabProject`.status not in ("Completed", "Cancelled")
and {cond} {match_cond} {scond} and {cond} {scond} {match_cond}
order by order by
if(locate(%(_txt)s, name), locate(%(_txt)s, name), 99999), if(locate(%(_txt)s, name), locate(%(_txt)s, name), 99999),
idx desc, idx desc,

View File

@ -501,7 +501,6 @@ class StockController(AccountsController):
check_if_stock_and_account_balance_synced(self.posting_date, check_if_stock_and_account_balance_synced(self.posting_date,
self.company, self.doctype, self.name) self.company, self.doctype, self.name)
@frappe.whitelist() @frappe.whitelist()
def make_quality_inspections(doctype, docname, items): def make_quality_inspections(doctype, docname, items):
if isinstance(items, str): if isinstance(items, str):
@ -533,21 +532,75 @@ def make_quality_inspections(doctype, docname, items):
return inspections return inspections
def is_reposting_pending(): def is_reposting_pending():
return frappe.db.exists("Repost Item Valuation", return frappe.db.exists("Repost Item Valuation",
{'docstatus': 1, 'status': ['in', ['Queued','In Progress']]}) {'docstatus': 1, 'status': ['in', ['Queued','In Progress']]})
def future_sle_exists(args, sl_entries=None):
key = (args.voucher_type, args.voucher_no)
def future_sle_exists(args): if validate_future_sle_not_exists(args, key, sl_entries):
sl_entries = frappe.get_all("Stock Ledger Entry", return False
elif get_cached_data(args, key):
return True
if not sl_entries:
sl_entries = get_sle_entries_against_voucher(args)
if not sl_entries:
return
or_conditions = get_conditions_to_validate_future_sle(sl_entries)
data = frappe.db.sql("""
select item_code, warehouse, count(name) as total_row
from `tabStock Ledger Entry`
where
({})
and timestamp(posting_date, posting_time)
>= timestamp(%(posting_date)s, %(posting_time)s)
and voucher_no != %(voucher_no)s
and is_cancelled = 0
GROUP BY
item_code, warehouse
""".format(" or ".join(or_conditions)), args, as_dict=1)
for d in data:
frappe.local.future_sle[key][(d.item_code, d.warehouse)] = d.total_row
return len(data)
def validate_future_sle_not_exists(args, key, sl_entries=None):
item_key = ''
if args.get('item_code'):
item_key = (args.get('item_code'), args.get('warehouse'))
if not sl_entries and hasattr(frappe.local, 'future_sle'):
if (not frappe.local.future_sle.get(key) or
(item_key and item_key not in frappe.local.future_sle.get(key))):
return True
def get_cached_data(args, key):
if not hasattr(frappe.local, 'future_sle'):
frappe.local.future_sle = {}
if key not in frappe.local.future_sle:
frappe.local.future_sle[key] = frappe._dict({})
if args.get('item_code'):
item_key = (args.get('item_code'), args.get('warehouse'))
count = frappe.local.future_sle[key].get(item_key)
return True if (count or count == 0) else False
else:
return frappe.local.future_sle[key]
def get_sle_entries_against_voucher(args):
return frappe.get_all("Stock Ledger Entry",
filters={"voucher_type": args.voucher_type, "voucher_no": args.voucher_no}, filters={"voucher_type": args.voucher_type, "voucher_no": args.voucher_no},
fields=["item_code", "warehouse"], fields=["item_code", "warehouse"],
order_by="creation asc") order_by="creation asc")
if not sl_entries: def get_conditions_to_validate_future_sle(sl_entries):
return
warehouse_items_map = {} warehouse_items_map = {}
for entry in sl_entries: for entry in sl_entries:
if entry.warehouse not in warehouse_items_map: if entry.warehouse not in warehouse_items_map:
@ -561,17 +614,7 @@ def future_sle_exists(args):
f"""warehouse = {frappe.db.escape(warehouse)} f"""warehouse = {frappe.db.escape(warehouse)}
and item_code in ({', '.join(frappe.db.escape(item) for item in items)})""") and item_code in ({', '.join(frappe.db.escape(item) for item in items)})""")
return frappe.db.sql(""" return or_conditions
select name
from `tabStock Ledger Entry`
where
({})
and timestamp(posting_date, posting_time)
>= timestamp(%(posting_date)s, %(posting_time)s)
and voucher_no != %(voucher_no)s
and is_cancelled = 0
limit 1
""".format(" or ".join(or_conditions)), args)
def create_repost_item_valuation_entry(args): def create_repost_item_valuation_entry(args):
args = frappe._dict(args) args = frappe._dict(args)

View File

@ -0,0 +1,393 @@
import frappe
import copy
from frappe import _
from frappe.utils import flt, cint, get_link_to_form
from collections import defaultdict
from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos
class Subcontracting():
def set_materials_for_subcontracted_items(self, raw_material_table):
if self.doctype == 'Purchase Invoice' and not self.update_stock:
return
self.raw_material_table = raw_material_table
self.__identify_change_in_item_table()
self.__prepare_supplied_items()
self.__validate_supplied_items()
def __prepare_supplied_items(self):
self.initialized_fields()
self.__get_purchase_orders()
self.__get_pending_qty_to_receive()
self.get_available_materials()
self.__remove_changed_rows()
self.__set_supplied_items()
def initialized_fields(self):
self.available_materials = frappe._dict()
self.__transferred_items = frappe._dict()
self.alternative_item_details = frappe._dict()
self.__get_backflush_based_on()
def __get_backflush_based_on(self):
self.backflush_based_on = frappe.db.get_single_value("Buying Settings",
"backflush_raw_materials_of_subcontract_based_on")
def __get_purchase_orders(self):
self.purchase_orders = []
if self.doctype == 'Purchase Order':
return
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.__reference_name = []
if self.doctype == 'Purchase Order' or self.is_new():
self.set(self.raw_material_table, [])
return
item_dict = self.__get_data_before_save()
if not item_dict:
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)
if item_dict.get(n_row.name):
del item_dict[n_row.name]
self.__changed_name.extend(item_dict.keys())
def __get_data_before_save(self):
item_dict = {}
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)
return item_dict
def get_available_materials(self):
''' Get the available raw materials which has been transferred to the supplier.
available_materials = {
(item_code, subcontracted_item, purchase_order): {
'qty': 1, 'serial_no': [ABC], 'batch_no': {'batch1': 1}, 'data': item_details
}
}
'''
if not self.purchase_orders:
return
for row in self.__get_transferred_items():
key = (row.rm_item_code, row.main_item_code, row.purchase_order)
if key not in self.available_materials:
self.available_materials.setdefault(key, frappe._dict({'qty': 0, 'serial_no': [],
'batch_no': defaultdict(float), 'item_details': row, 'po_details': []})
)
details = self.available_materials[key]
details.qty += row.qty
details.po_details.append(row.po_detail)
if row.serial_no:
details.serial_no.extend(get_serial_nos(row.serial_no))
if row.batch_no:
details.batch_no[row.batch_no] += row.qty
self.__set_alternative_item_details(row)
self.__transferred_items = copy.deepcopy(self.available_materials)
for doctype in ['Purchase Receipt', 'Purchase Invoice']:
self.__update_consumed_materials(doctype)
def __update_consumed_materials(self, doctype, return_consumed_items=False):
'''Deduct the consumed materials from the available materials.'''
pr_items = self.__get_received_items(doctype)
if not pr_items:
return ([], {}) if return_consumed_items else None
pr_items = {d.name: d.get(self.get('po_field') or 'purchase_order') for d in pr_items}
consumed_materials = self.__get_consumed_items(doctype, pr_items.keys())
if return_consumed_items:
return (consumed_materials, pr_items)
for row in consumed_materials:
key = (row.rm_item_code, row.main_item_code, pr_items.get(row.reference_name))
if not self.available_materials.get(key):
continue
self.available_materials[key]['qty'] -= row.consumed_qty
if row.serial_no:
self.available_materials[key]['serial_no'] = list(
set(self.available_materials[key]['serial_no']) - set(get_serial_nos(row.serial_no))
)
if row.batch_no:
self.available_materials[key]['batch_no'][row.batch_no] -= row.consumed_qty
def __get_transferred_items(self):
fields = ['`tabStock Entry`.`purchase_order`']
alias_dict = {'item_code': 'rm_item_code', 'subcontracted_item': 'main_item_code', 'basic_rate': 'rate'}
child_table_fields = ['item_code', 'item_name', 'description', 'qty', 'basic_rate', 'amount',
'serial_no', 'uom', 'subcontracted_item', 'stock_uom', 'batch_no', 'conversion_factor',
's_warehouse', 't_warehouse', 'item_group', 'po_detail']
if self.backflush_based_on == 'BOM':
child_table_fields.append('original_item')
for field in child_table_fields:
fields.append(f'`tabStock Entry Detail`.`{field}` As {alias_dict.get(field, field)}')
filters = [['Stock Entry', 'docstatus', '=', 1], ['Stock Entry', 'purpose', '=', 'Send to Subcontractor'],
['Stock Entry', 'purchase_order', 'in', self.purchase_orders]]
return frappe.get_all('Stock Entry', fields = fields, filters=filters)
def __get_received_items(self, doctype):
fields = []
self.po_field = 'purchase_order'
for field in ['name', self.po_field, 'parent']:
fields.append(f'`tab{doctype} Item`.`{field}`')
filters = [[doctype, 'docstatus', '=', 1], [f'{doctype} Item', self.po_field, 'in', self.purchase_orders]]
if doctype == 'Purchase Invoice':
filters.append(['Purchase Invoice', 'update_stock', "=", 1])
return frappe.get_all(f'{doctype}', fields = fields, filters = filters)
def __get_consumed_items(self, doctype, pr_items):
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)), 'parenttype': doctype})
def __set_alternative_item_details(self, row):
if row.get('original_item'):
self.alternative_item_details[row.get('original_item')] = row
def __get_pending_qty_to_receive(self):
'''Get qty to be received against the purchase order.'''
self.qty_to_be_received = defaultdict(float)
if self.doctype != 'Purchase Order' and self.backflush_based_on != 'BOM' and self.purchase_orders:
for row in frappe.get_all('Purchase Order Item',
fields = ['item_code', '(qty - received_qty) as qty', 'parent', 'name'],
filters = {'docstatus': 1, 'parent': ('in', self.purchase_orders)}):
self.qty_to_be_received[(row.item_code, row.parent)] += row.qty
def __get_materials_from_bom(self, item_code, bom_no, exploded_item=0):
doctype = 'BOM Item' if not exploded_item else 'BOM Explosion Item'
fields = [f'`tab{doctype}`.`stock_qty` / `tabBOM`.`quantity` as qty_consumed_per_unit']
alias_dict = {'item_code': 'rm_item_code', 'name': 'bom_detail_no', 'source_warehouse': 'reserve_warehouse'}
for field in ['item_code', 'name', 'rate', 'stock_uom',
'source_warehouse', 'description', 'item_name', 'stock_uom']:
fields.append(f'`tab{doctype}`.`{field}` As {alias_dict.get(field, field)}')
filters = [[doctype, 'parent', '=', bom_no], [doctype, 'docstatus', '=', 1],
['BOM', 'item', '=', item_code], [doctype, 'sourced_by_supplier', '=', 0]]
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:
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:
continue
if (d.reference_name not in self.__reference_name):
continue
d.idx = i
self.append('supplied_items', d)
i += 1
def __set_supplied_items(self):
self.bom_items = {}
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))):
continue
if self.doctype == 'Purchase Order' or self.backflush_based_on == 'BOM':
for bom_item in self.__get_materials_from_bom(row.item_code, row.bom, row.get('include_exploded_items')):
qty = (flt(bom_item.qty_consumed_per_unit) * flt(row.qty) * row.conversion_factor)
bom_item.main_item_code = row.item_code
self.__update_reserve_warehouse(bom_item, row)
self.__set_alternative_item(bom_item)
self.__add_supplied_item(row, bom_item, qty)
elif self.backflush_based_on != 'BOM':
for key, transfer_item in self.available_materials.items():
if (key[1], key[2]) == (row.item_code, row.purchase_order) and transfer_item.qty > 0:
qty = self.__get_qty_based_on_material_transfer(row, transfer_item) or 0
transfer_item.qty -= qty
self.__add_supplied_item(row, transfer_item.get('item_details'), qty)
if self.qty_to_be_received:
self.qty_to_be_received[(row.item_code, row.purchase_order)] -= row.qty
def __update_reserve_warehouse(self, row, item):
if self.doctype == 'Purchase Order':
row.reserve_warehouse = (self.set_reserve_warehouse or item.warehouse)
def __get_qty_based_on_material_transfer(self, item_row, transfer_item):
key = (item_row.item_code, item_row.purchase_order)
if self.qty_to_be_received == item_row.qty:
return transfer_item.qty
if self.qty_to_be_received:
qty = (flt(item_row.qty) * flt(transfer_item.qty)) / flt(self.qty_to_be_received.get(key, 0))
transfer_item.item_details.required_qty = transfer_item.qty
if (transfer_item.serial_no or frappe.get_cached_value('UOM',
transfer_item.item_details.stock_uom, 'must_be_whole_number')):
return frappe.utils.ceil(qty)
return qty
def __set_alternative_item(self, bom_item):
if self.alternative_item_details.get(bom_item.rm_item_code):
bom_item.update(self.alternative_item_details[bom_item.rm_item_code])
def __add_supplied_item(self, item_row, bom_item, qty):
bom_item.conversion_factor = item_row.conversion_factor
rm_obj = self.append(self.raw_material_table, bom_item)
rm_obj.reference_name = item_row.name
if self.doctype == 'Purchase Order':
rm_obj.required_qty = qty
else:
rm_obj.consumed_qty = 0
rm_obj.purchase_order = item_row.purchase_order
self.__set_batch_nos(bom_item, item_row, rm_obj, qty)
def __set_batch_nos(self, bom_item, item_row, rm_obj, qty):
key = (rm_obj.rm_item_code, item_row.item_code, item_row.purchase_order)
if (self.available_materials.get(key) and self.available_materials[key]['batch_no']):
new_rm_obj = None
for batch_no, batch_qty in self.available_materials[key]['batch_no'].items():
if batch_qty >= qty:
self.__set_batch_no_as_per_qty(item_row, rm_obj, batch_no, qty)
self.available_materials[key]['batch_no'][batch_no] -= qty
return
elif qty > 0 and batch_qty > 0:
qty -= batch_qty
new_rm_obj = self.append(self.raw_material_table, bom_item)
new_rm_obj.reference_name = item_row.name
self.__set_batch_no_as_per_qty(item_row, new_rm_obj, batch_no, batch_qty)
self.available_materials[key]['batch_no'][batch_no] = 0
if abs(qty) > 0 and not new_rm_obj:
self.__set_consumed_qty(rm_obj, qty)
else:
self.__set_consumed_qty(rm_obj, qty, bom_item.required_qty or qty)
self.__set_serial_nos(item_row, rm_obj)
def __set_consumed_qty(self, rm_obj, consumed_qty, required_qty=0):
rm_obj.required_qty = required_qty
rm_obj.consumed_qty = consumed_qty
def __set_batch_no_as_per_qty(self, item_row, rm_obj, batch_no, qty):
rm_obj.update({'consumed_qty': qty, 'batch_no': batch_no,
'required_qty': qty, 'purchase_order': item_row.purchase_order})
self.__set_serial_nos(item_row, rm_obj)
def __set_serial_nos(self, item_row, rm_obj):
key = (rm_obj.rm_item_code, item_row.item_code, item_row.purchase_order)
if (self.available_materials.get(key) and self.available_materials[key]['serial_no']):
used_serial_nos = self.available_materials[key]['serial_no'][0: cint(rm_obj.consumed_qty)]
rm_obj.serial_no = '\n'.join(used_serial_nos)
# Removed the used serial nos from the list
for sn in used_serial_nos:
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()
itemwise_consumed_qty = defaultdict(float)
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)
def __update_consumed_qty_in_po(self, itemwise_consumed_qty):
fields = ['main_item_code', 'rm_item_code', 'parent', 'supplied_qty', 'name']
filters = {'docstatus': 1, 'parent': ('in', self.purchase_orders)}
for row in frappe.get_all('Purchase Order Item Supplied', fields = fields, filters=filters, order_by='idx'):
key = (row.rm_item_code, row.main_item_code, row.parent)
consumed_qty = itemwise_consumed_qty.get(key, 0)
if row.supplied_qty < consumed_qty:
consumed_qty = row.supplied_qty
itemwise_consumed_qty[key] -= consumed_qty
frappe.db.set_value('Purchase Order Item Supplied', row.name, 'consumed_qty', consumed_qty)
def __validate_supplied_items(self):
if self.doctype not in ['Purchase Invoice', 'Purchase Receipt']:
return
for row in self.get(self.raw_material_table):
self.__validate_consumed_qty(row)
key = (row.rm_item_code, row.main_item_code, row.purchase_order)
if not self.__transferred_items or not self.__transferred_items.get(key):
return
self.__validate_batch_no(row, key)
self.__validate_serial_no(row, key)
def __validate_consumed_qty(self, row):
if self.backflush_based_on != 'BOM' and flt(row.consumed_qty) == 0.0:
msg = f'Row {row.idx}: the consumed qty cannot be zero for the item {frappe.bold(row.rm_item_code)}'
frappe.throw(_(msg),title=_('Consumed Items Qty Check'))
def __validate_batch_no(self, row, key):
if row.get('batch_no') and row.get('batch_no') not in self.__transferred_items.get(key).get('batch_no'):
link = get_link_to_form('Purchase Order', row.purchase_order)
msg = f'The Batch No {frappe.bold(row.get("batch_no"))} has not supplied against the Purchase Order {link}'
frappe.throw(_(msg), title=_("Incorrect Batch Consumed"))
def __validate_serial_no(self, row, key):
if row.get('serial_no'):
serial_nos = get_serial_nos(row.get('serial_no'))
incorrect_sn = set(serial_nos).difference(self.__transferred_items.get(key).get('serial_no'))
if incorrect_sn:
incorrect_sn = "\n".join(incorrect_sn)
link = get_link_to_form('Purchase Order', row.purchase_order)
msg = f'The Serial Nos {incorrect_sn} has not supplied against the Purchase Order {link}'
frappe.throw(_(msg), title=_("Incorrect Serial Number Consumed"))

View File

@ -658,7 +658,13 @@ class calculate_taxes_and_totals(object):
item.margin_type = None item.margin_type = None
item.margin_rate_or_amount = 0.0 item.margin_rate_or_amount = 0.0
if item.margin_type and item.margin_rate_or_amount: if not item.pricing_rules and flt(item.rate) > flt(item.price_list_rate):
item.margin_type = "Amount"
item.margin_rate_or_amount = flt(item.rate - item.price_list_rate,
item.precision("margin_rate_or_amount"))
item.rate_with_margin = item.rate
elif item.margin_type and item.margin_rate_or_amount:
margin_value = item.margin_rate_or_amount if item.margin_type == 'Amount' else flt(item.price_list_rate) * flt(item.margin_rate_or_amount) / 100 margin_value = item.margin_rate_or_amount if item.margin_type == 'Amount' else flt(item.price_list_rate) * flt(item.margin_rate_or_amount) / 100
rate_with_margin = flt(item.price_list_rate) + flt(margin_value) rate_with_margin = flt(item.price_list_rate) + flt(margin_value)
base_rate_with_margin = flt(rate_with_margin) * flt(self.doc.conversion_rate) base_rate_with_margin = flt(rate_with_margin) * flt(self.doc.conversion_rate)

View File

@ -207,7 +207,7 @@
"label": "Status", "label": "Status",
"oldfieldname": "status", "oldfieldname": "status",
"oldfieldtype": "Select", "oldfieldtype": "Select",
"options": "Active\nInactive\nLeft", "options": "Active\nInactive\nSuspended\nLeft",
"reqd": 1, "reqd": 1,
"search_index": 1 "search_index": 1
}, },
@ -813,7 +813,7 @@
"idx": 24, "idx": 24,
"image_field": "image", "image_field": "image",
"links": [], "links": [],
"modified": "2021-06-12 11:31:37.730760", "modified": "2021-06-17 11:31:37.730760",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "HR", "module": "HR",
"name": "Employee", "name": "Employee",

View File

@ -4,7 +4,7 @@
from __future__ import unicode_literals from __future__ import unicode_literals
import frappe import frappe
from frappe.utils import getdate, validate_email_address, today, add_years, format_datetime, cstr from frappe.utils import getdate, validate_email_address, today, add_years, cstr
from frappe.model.naming import set_name_by_naming_series from frappe.model.naming import set_name_by_naming_series
from frappe import throw, _, scrub from frappe import throw, _, scrub
from frappe.permissions import add_user_permission, remove_user_permission, \ from frappe.permissions import add_user_permission, remove_user_permission, \
@ -12,7 +12,6 @@ from frappe.permissions import add_user_permission, remove_user_permission, \
from frappe.model.document import Document from frappe.model.document import Document
from erpnext.utilities.transaction_base import delete_events from erpnext.utilities.transaction_base import delete_events
from frappe.utils.nestedset import NestedSet from frappe.utils.nestedset import NestedSet
from erpnext.hr.doctype.job_offer.job_offer import get_staffing_plan_detail
class EmployeeUserDisabledError(frappe.ValidationError): pass class EmployeeUserDisabledError(frappe.ValidationError): pass
class EmployeeLeftValidationError(frappe.ValidationError): pass class EmployeeLeftValidationError(frappe.ValidationError): pass
@ -37,7 +36,7 @@ class Employee(NestedSet):
def validate(self): def validate(self):
from erpnext.controllers.status_updater import validate_status from erpnext.controllers.status_updater import validate_status
validate_status(self.status, ["Active", "Inactive", "Left"]) validate_status(self.status, ["Active", "Inactive", "Suspended", "Left"])
self.employee = self.name self.employee = self.name
self.set_employee_name() self.set_employee_name()

View File

@ -7,7 +7,8 @@ def get_data():
'heatmap_message': _('This is based on the attendance of this Employee'), 'heatmap_message': _('This is based on the attendance of this Employee'),
'fieldname': 'employee', 'fieldname': 'employee',
'non_standard_fieldnames': { 'non_standard_fieldnames': {
'Bank Account': 'party' 'Bank Account': 'party',
'Employee Grievance': 'raised_by'
}, },
'transactions': [ 'transactions': [
{ {
@ -20,7 +21,7 @@ def get_data():
}, },
{ {
'label': _('Lifecycle'), 'label': _('Lifecycle'),
'items': ['Employee Transfer', 'Employee Promotion', 'Employee Separation'] 'items': ['Employee Transfer', 'Employee Promotion', 'Employee Separation', 'Employee Grievance']
}, },
{ {
'label': _('Shift'), 'label': _('Shift'),

View File

@ -3,7 +3,7 @@ frappe.listview_settings['Employee'] = {
filters: [["status","=", "Active"]], filters: [["status","=", "Active"]],
get_indicator: function(doc) { get_indicator: function(doc) {
var indicator = [__(doc.status), frappe.utils.guess_colour(doc.status), "status,=," + doc.status]; var indicator = [__(doc.status), frappe.utils.guess_colour(doc.status), "status,=," + doc.status];
indicator[1] = {"Active": "green", "Inactive": "red", "Left": "gray"}[doc.status]; indicator[1] = {"Active": "green", "Inactive": "red", "Left": "gray", "Suspended": "orange"}[doc.status];
return indicator; return indicator;
} }
}; };

View File

@ -0,0 +1,39 @@
// Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and contributors
// For license information, please see license.txt
frappe.ui.form.on('Employee Grievance', {
setup: function(frm) {
frm.set_query('grievance_against_party', function() {
return {
filters: {
name: ['in', [
'Company', 'Department', 'Employee Group', 'Employee Grade', 'Employee']
]
}
};
});
frm.set_query('associated_document_type', function() {
let ignore_modules = ["Setup", "Core", "Integrations", "Automation", "Website",
"Utilities", "Event Streaming", "Social", "Chat", "Data Migration", "Printing", "Desk", "Custom"];
return {
filters: {
istable: 0,
issingle: 0,
module: ["Not In", ignore_modules]
}
};
});
},
grievance_against_party: function(frm) {
let filters = {};
if (frm.doc.grievance_against_party == 'Employee' && frm.doc.raised_by) {
filters.name = ["!=", frm.doc.raised_by];
}
frm.set_query('grievance_against', function() {
return {
filters: filters
};
});
},
});

View File

@ -0,0 +1,261 @@
{
"actions": [],
"autoname": "HR-GRIEV-.YYYY.-.#####",
"creation": "2021-05-11 13:41:51.485295",
"doctype": "DocType",
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
"subject",
"raised_by",
"employee_name",
"designation",
"column_break_3",
"date",
"status",
"reports_to",
"grievance_details_section",
"grievance_against_party",
"grievance_against",
"grievance_type",
"column_break_11",
"associated_document_type",
"associated_document",
"section_break_14",
"description",
"investigation_details_section",
"cause_of_grievance",
"resolution_details_section",
"resolved_by",
"resolution_date",
"employee_responsible",
"column_break_16",
"resolution_detail",
"amended_from"
],
"fields": [
{
"fieldname": "grievance_type",
"fieldtype": "Link",
"in_list_view": 1,
"label": "Grievance Type",
"options": "Grievance Type",
"reqd": 1
},
{
"fieldname": "column_break_3",
"fieldtype": "Column Break"
},
{
"fieldname": "date",
"fieldtype": "Date",
"in_list_view": 1,
"label": "Date ",
"reqd": 1
},
{
"default": "Open",
"fieldname": "status",
"fieldtype": "Select",
"in_list_view": 1,
"label": "Status",
"options": "Open\nInvestigated\nResolved\nInvalid",
"reqd": 1
},
{
"fieldname": "description",
"fieldtype": "Text",
"label": "Description",
"reqd": 1
},
{
"fieldname": "cause_of_grievance",
"fieldtype": "Text",
"label": "Cause of Grievance",
"mandatory_depends_on": "eval: doc.status == \"Investigated\" || doc.status == \"Resolved\""
},
{
"fieldname": "resolution_details_section",
"fieldtype": "Section Break",
"label": "Resolution Details"
},
{
"fieldname": "resolved_by",
"fieldtype": "Link",
"label": "Resolved By",
"mandatory_depends_on": "eval: doc.status == \"Resolved\"",
"options": "User"
},
{
"fieldname": "employee_responsible",
"fieldtype": "Link",
"label": "Employee Responsible ",
"options": "Employee"
},
{
"fieldname": "resolution_detail",
"fieldtype": "Small Text",
"label": "Resolution Details",
"mandatory_depends_on": "eval: doc.status == \"Resolved\""
},
{
"fieldname": "column_break_16",
"fieldtype": "Column Break"
},
{
"fieldname": "resolution_date",
"fieldtype": "Date",
"label": "Resolution Date",
"mandatory_depends_on": "eval: doc.status == \"Resolved\""
},
{
"fieldname": "grievance_against",
"fieldtype": "Dynamic Link",
"label": "Grievance Against",
"options": "grievance_against_party",
"reqd": 1
},
{
"fieldname": "raised_by",
"fieldtype": "Link",
"label": "Raised By",
"options": "Employee",
"reqd": 1
},
{
"fieldname": "amended_from",
"fieldtype": "Link",
"label": "Amended From",
"no_copy": 1,
"options": "Employee Grievance",
"print_hide": 1,
"read_only": 1
},
{
"fetch_from": "raised_by.designation",
"fieldname": "designation",
"fieldtype": "Link",
"label": "Designation",
"options": "Designation",
"read_only": 1
},
{
"fetch_from": "raised_by.reports_to",
"fieldname": "reports_to",
"fieldtype": "Link",
"label": "Reports To",
"options": "Employee",
"read_only": 1
},
{
"fieldname": "grievance_details_section",
"fieldtype": "Section Break",
"label": "Grievance Details"
},
{
"fieldname": "column_break_11",
"fieldtype": "Column Break"
},
{
"fieldname": "section_break_14",
"fieldtype": "Section Break"
},
{
"fieldname": "grievance_against_party",
"fieldtype": "Link",
"in_list_view": 1,
"label": "Grievance Against Party",
"options": "DocType",
"reqd": 1
},
{
"fieldname": "associated_document_type",
"fieldtype": "Link",
"label": "Associated Document Type",
"options": "DocType"
},
{
"fieldname": "associated_document",
"fieldtype": "Dynamic Link",
"label": "Associated Document",
"options": "associated_document_type"
},
{
"fieldname": "investigation_details_section",
"fieldtype": "Section Break",
"label": "Investigation Details"
},
{
"fetch_from": "raised_by.employee_name",
"fieldname": "employee_name",
"fieldtype": "Data",
"label": "Employee Name",
"read_only": 1
},
{
"fieldname": "subject",
"fieldtype": "Data",
"label": "Subject",
"reqd": 1
}
],
"index_web_pages_for_search": 1,
"is_submittable": 1,
"links": [],
"modified": "2021-06-21 12:51:01.499486",
"modified_by": "Administrator",
"module": "HR",
"name": "Employee Grievance",
"owner": "Administrator",
"permissions": [
{
"amend": 1,
"cancel": 1,
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "System Manager",
"select": 1,
"share": 1,
"submit": 1,
"write": 1
},
{
"amend": 1,
"cancel": 1,
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "HR Manager",
"select": 1,
"share": 1,
"submit": 1,
"write": 1
},
{
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "HR User",
"share": 1,
"write": 1
}
],
"search_fields": "subject,raised_by,grievance_against_party",
"sort_field": "modified",
"sort_order": "DESC",
"title_field": "subject",
"track_changes": 1
}

View File

@ -0,0 +1,15 @@
# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and contributors
# For license information, please see license.txt
import frappe
from frappe import _, bold
from frappe.model.document import Document
class EmployeeGrievance(Document):
def on_submit(self):
if self.status not in ["Invalid", "Resolved"]:
frappe.throw(_("Only Employee Grievance with status {0} or {1} can be submitted").format(
bold("Invalid"),
bold("Resolved"))
)

View File

@ -0,0 +1,12 @@
frappe.listview_settings["Employee Grievance"] = {
has_indicator_for_draft: 1,
get_indicator: function(doc) {
var colors = {
"Open": "red",
"Investigated": "orange",
"Resolved": "green",
"Invalid": "grey"
};
return [__(doc.status), colors[doc.status], "status,=," + doc.status];
}
};

View File

@ -0,0 +1,51 @@
# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and Contributors
# See license.txt
import frappe
import unittest
from frappe.utils import today
from erpnext.hr.doctype.employee.test_employee import make_employee
class TestEmployeeGrievance(unittest.TestCase):
def test_create_employee_grievance(self):
create_employee_grievance()
def create_employee_grievance():
grievance_type = create_grievance_type()
emp_1 = make_employee("test_emp_grievance_@example.com", company="_Test Company")
emp_2 = make_employee("testculprit@example.com", company="_Test Company")
grievance = frappe.new_doc("Employee Grievance")
grievance.subject = "Test Employee Grievance"
grievance.raised_by = emp_1
grievance.date = today()
grievance.grievance_type = grievance_type
grievance.grievance_against_party = "Employee"
grievance.grievance_against = emp_2
grievance.description = "test descrip"
#set cause
grievance.cause_of_grievance = "test cause"
#resolution details
grievance.resolution_date = today()
grievance.resolution_detail = "test resolution detail"
grievance.resolved_by = "test_emp_grievance_@example.com"
grievance.employee_responsible = emp_2
grievance.status = "Resolved"
grievance.save()
grievance.submit()
return grievance
def create_grievance_type():
if frappe.db.exists("Grievance Type", "Employee Abuse"):
return frappe.get_doc("Grievance Type", "Employee Abuse")
grievance_type = frappe.new_doc("Grievance Type")
grievance_type.name = "Employee Abuse"
grievance_type.description = "Test"
grievance_type.save()
return grievance_type.name

View File

@ -0,0 +1,8 @@
// Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and contributors
// For license information, please see license.txt
frappe.ui.form.on('Grievance Type', {
// refresh: function(frm) {
// }
});

View File

@ -0,0 +1,70 @@
{
"actions": [],
"autoname": "Prompt",
"creation": "2021-05-11 12:41:50.256071",
"doctype": "DocType",
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
"section_break_5",
"description"
],
"fields": [
{
"fieldname": "section_break_5",
"fieldtype": "Section Break"
},
{
"fieldname": "description",
"fieldtype": "Text",
"label": "Description"
}
],
"index_web_pages_for_search": 1,
"links": [],
"modified": "2021-06-21 12:54:37.764712",
"modified_by": "Administrator",
"module": "HR",
"name": "Grievance Type",
"owner": "Administrator",
"permissions": [
{
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "System Manager",
"share": 1,
"write": 1
},
{
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "HR Manager",
"share": 1,
"write": 1
},
{
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "HR User",
"share": 1,
"write": 1
}
],
"sort_field": "modified",
"sort_order": "DESC"
}

View File

@ -0,0 +1,8 @@
# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and contributors
# For license information, please see license.txt
# import frappe
from frappe.model.document import Document
class GrievanceType(Document):
pass

View File

@ -0,0 +1,8 @@
# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and Contributors
# See license.txt
# import frappe
import unittest
class TestGrievanceType(unittest.TestCase):
pass

View File

@ -2,7 +2,7 @@
// MIT License. See license.txt // MIT License. See license.txt
frappe.listview_settings['Job Applicant'] = { frappe.listview_settings['Job Applicant'] = {
add_fields: ["company", "designation", "job_applicant", "status"], add_fields: ["status"],
get_indicator: function (doc) { get_indicator: function (doc) {
if (doc.status == "Accepted") { if (doc.status == "Accepted") {
return [__(doc.status), "green", "status,=," + doc.status]; return [__(doc.status), "green", "status,=," + doc.status];

View File

@ -41,7 +41,7 @@ class StaffingPlan(Document):
detail.total_estimated_cost = 0 detail.total_estimated_cost = 0
if detail.number_of_positions > 0: if detail.number_of_positions > 0:
if detail.vacancies > 0 and detail.estimated_cost_per_position: if detail.vacancies and detail.estimated_cost_per_position:
detail.total_estimated_cost = cint(detail.vacancies) * flt(detail.estimated_cost_per_position) detail.total_estimated_cost = cint(detail.vacancies) * flt(detail.estimated_cost_per_position)
self.total_estimated_budget += detail.total_estimated_cost self.total_estimated_budget += detail.total_estimated_cost
@ -76,12 +76,12 @@ class StaffingPlan(Document):
if cint(staffing_plan_detail.vacancies) > cint(parent_plan_details[0].vacancies) or \ if cint(staffing_plan_detail.vacancies) > cint(parent_plan_details[0].vacancies) or \
flt(staffing_plan_detail.total_estimated_cost) > flt(parent_plan_details[0].total_estimated_cost): flt(staffing_plan_detail.total_estimated_cost) > flt(parent_plan_details[0].total_estimated_cost):
frappe.throw(_("You can only plan for upto {0} vacancies and budget {1} \ frappe.throw(_("You can only plan for upto {0} vacancies and budget {1} \
for {2} as per staffing plan {3} for parent company {4}." for {2} as per staffing plan {3} for parent company {4}.").format(
.format(cint(parent_plan_details[0].vacancies), cint(parent_plan_details[0].vacancies),
parent_plan_details[0].total_estimated_cost, parent_plan_details[0].total_estimated_cost,
frappe.bold(staffing_plan_detail.designation), frappe.bold(staffing_plan_detail.designation),
parent_plan_details[0].name, parent_plan_details[0].name,
parent_company)), ParentCompanyError) parent_company), ParentCompanyError)
#Get vacanices already planned for all companies down the hierarchy of Parent Company #Get vacanices already planned for all companies down the hierarchy of Parent Company
lft, rgt = frappe.get_cached_value('Company', parent_company, ["lft", "rgt"]) lft, rgt = frappe.get_cached_value('Company', parent_company, ["lft", "rgt"])
@ -98,14 +98,14 @@ class StaffingPlan(Document):
(flt(parent_plan_details[0].total_estimated_cost) < \ (flt(parent_plan_details[0].total_estimated_cost) < \
(flt(staffing_plan_detail.total_estimated_cost) + flt(all_sibling_details.total_estimated_cost))): (flt(staffing_plan_detail.total_estimated_cost) + flt(all_sibling_details.total_estimated_cost))):
frappe.throw(_("{0} vacancies and {1} budget for {2} already planned for subsidiary companies of {3}. \ frappe.throw(_("{0} vacancies and {1} budget for {2} already planned for subsidiary companies of {3}. \
You can only plan for upto {4} vacancies and and budget {5} as per staffing plan {6} for parent company {3}." You can only plan for upto {4} vacancies and and budget {5} as per staffing plan {6} for parent company {3}.").format(
.format(cint(all_sibling_details.vacancies), cint(all_sibling_details.vacancies),
all_sibling_details.total_estimated_cost, all_sibling_details.total_estimated_cost,
frappe.bold(staffing_plan_detail.designation), frappe.bold(staffing_plan_detail.designation),
parent_company, parent_company,
cint(parent_plan_details[0].vacancies), cint(parent_plan_details[0].vacancies),
parent_plan_details[0].total_estimated_cost, parent_plan_details[0].total_estimated_cost,
parent_plan_details[0].name))) parent_plan_details[0].name))
def validate_with_subsidiary_plans(self, staffing_plan_detail): def validate_with_subsidiary_plans(self, staffing_plan_detail):
#Valdate this plan with all child company plan #Valdate this plan with all child company plan
@ -121,11 +121,11 @@ class StaffingPlan(Document):
cint(staffing_plan_detail.vacancies) < cint(children_details.vacancies) or \ cint(staffing_plan_detail.vacancies) < cint(children_details.vacancies) or \
flt(staffing_plan_detail.total_estimated_cost) < flt(children_details.total_estimated_cost): flt(staffing_plan_detail.total_estimated_cost) < flt(children_details.total_estimated_cost):
frappe.throw(_("Subsidiary companies have already planned for {1} vacancies at a budget of {2}. \ frappe.throw(_("Subsidiary companies have already planned for {1} vacancies at a budget of {2}. \
Staffing Plan for {0} should allocate more vacancies and budget for {3} than planned for its subsidiary companies" Staffing Plan for {0} should allocate more vacancies and budget for {3} than planned for its subsidiary companies").format(
.format(self.company, self.company,
cint(children_details.vacancies), cint(children_details.vacancies),
children_details.total_estimated_cost, children_details.total_estimated_cost,
frappe.bold(staffing_plan_detail.designation))), SubsidiaryCompanyError) frappe.bold(staffing_plan_detail.designation)), SubsidiaryCompanyError)
@frappe.whitelist() @frappe.whitelist()
def get_designation_counts(designation, company): def get_designation_counts(designation, company):

View File

@ -11,8 +11,8 @@
"event": "Submit", "event": "Submit",
"idx": 0, "idx": 0,
"is_standard": 1, "is_standard": 1,
"message": "<table class=\"panel-header\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" width=\"100%\">\n <tr height=\"10\"></tr>\n <tr>\n <td width=\"15\"></td>\n <td>\n <div class=\"text-medium text-muted\">\n <span>{{_(\"Training Event:\")}} {{ doc.event_name }}</span>\n </div>\n </td>\n <td width=\"15\"></td>\n </tr>\n <tr height=\"10\"></tr>\n</table>\n\n<table class=\"panel-body\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" width=\"100%\">\n <tr height=\"10\"></tr>\n <tr>\n <td width=\"15\"></td>\n <td>\n <div>\n {{ doc.introduction }}\n <ul class=\"list-unstyled\" style=\"line-height: 1.7\">\n <li>{{_(\"Event Location\")}}: <b>{{ doc.location }}</b></li>\n {% set start = frappe.utils.get_datetime(doc.start_time) %}\n {% set end = frappe.utils.get_datetime(doc.end_time) %}\n {% if start.date() == end.date() %}\n <li>{{_(\"Date\")}}: <b>{{ start.strftime(\"%A, %d %b %Y\") }}</b></li>\n <li>\n {{_(\"Timing\")}}: <b>{{ start.strftime(\"%I:%M %p\") + ' to ' + end.strftime(\"%I:%M %p\") }}</b>\n </li>\n {% else %}\n <li>{{_(\"Start Time\")}}: <b>{{ start.strftime(\"%A, %d %b %Y at %I:%M %p\") }}</b>\n </li>\n <li>{{_(\"End Time\")}}: <b>{{ end.strftime(\"%A, %d %b %Y at %I:%M %p\") }}</b>\n </li>\n {% endif %}\n <li>{{ _('Event Link') }}: {{ frappe.utils.get_link_to_form(doc.doctype, doc.name) }}</li>\n {% if doc.is_mandatory %}\n <li>Note: This Training Event is mandatory</li>\n {% endif %}\n </ul>\n </div>\n </td>\n <td width=\"15\"></td>\n </tr>\n <tr height=\"10\"></tr>\n</table>", "message": "<table class=\"panel-header\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" width=\"100%\">\n <tr height=\"10\"></tr>\n <tr>\n <td width=\"15\"></td>\n <td>\n <div class=\"text-medium text-muted\">\n <span>{{_(\"Training Event:\")}} {{ doc.event_name }}</span>\n </div>\n </td>\n <td width=\"15\"></td>\n </tr>\n <tr height=\"10\"></tr>\n</table>\n\n<table class=\"panel-body\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" width=\"100%\">\n <tr height=\"10\"></tr>\n <tr>\n <td width=\"15\"></td>\n <td>\n <div>\n {{ doc.introduction }}\n <ul class=\"list-unstyled\" style=\"line-height: 1.7\">\n <li>{{_(\"Event Location\")}}: <b>{{ doc.location }}</b></li>\n {% set start = frappe.utils.get_datetime(doc.start_time) %}\n {% set end = frappe.utils.get_datetime(doc.end_time) %}\n {% if start.date() == end.date() %}\n <li>{{_(\"Date\")}}: <b>{{ start.strftime(\"%A, %d %b %Y\") }}</b></li>\n <li>\n {{_(\"Timing\")}}: <b>{{ start.strftime(\"%I:%M %p\") + ' to ' + end.strftime(\"%I:%M %p\") }}</b>\n </li>\n {% else %}\n <li>\n {{_(\"Start Time\")}}: <b>{{ start.strftime(\"%A, %d %b %Y at %I:%M %p\") }}</b>\n </li>\n <li>{{_(\"End Time\")}}: <b>{{ end.strftime(\"%A, %d %b %Y at %I:%M %p\") }}</b></li>\n {% endif %}\n <li>{{ _(\"Event Link\") }}: {{ frappe.utils.get_link_to_form(doc.doctype, doc.name) }}</li>\n {% if doc.is_mandatory %}\n <li>{{ _(\"Note: This Training Event is mandatory\") }}</li>\n {% endif %}\n </ul>\n </div>\n </td>\n <td width=\"15\"></td>\n </tr>\n <tr height=\"10\"></tr>\n</table>",
"modified": "2021-05-24 16:29:13.165930", "modified": "2021-06-16 14:08:12.933367",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "HR", "module": "HR",
"name": "Training Scheduled", "name": "Training Scheduled",

View File

@ -24,19 +24,19 @@
{% set start = frappe.utils.get_datetime(doc.start_time) %} {% set start = frappe.utils.get_datetime(doc.start_time) %}
{% set end = frappe.utils.get_datetime(doc.end_time) %} {% set end = frappe.utils.get_datetime(doc.end_time) %}
{% if start.date() == end.date() %} {% if start.date() == end.date() %}
<li>{{_("Date")}}: <b>{{ start.strftime("%A, %d %b %Y") }}</b></li> <li>{{_("Date")}}: <b>{{ start.strftime("%A, %d %b %Y") }}</b></li>
<li> <li>
{{_("Timing")}}: <b>{{ start.strftime("%I:%M %p") + ' to ' + end.strftime("%I:%M %p") }}</b> {{_("Timing")}}: <b>{{ start.strftime("%I:%M %p") + ' to ' + end.strftime("%I:%M %p") }}</b>
</li> </li>
{% else %} {% else %}
<li>{{_("Start Time")}}: <b>{{ start.strftime("%A, %d %b %Y at %I:%M %p") }}</b> <li>
</li> {{_("Start Time")}}: <b>{{ start.strftime("%A, %d %b %Y at %I:%M %p") }}</b>
<li>{{_("End Time")}}: <b>{{ end.strftime("%A, %d %b %Y at %I:%M %p") }}</b> </li>
</li> <li>{{_("End Time")}}: <b>{{ end.strftime("%A, %d %b %Y at %I:%M %p") }}</b></li>
{% endif %} {% endif %}
<li>{{ _('Event Link') }}: {{ frappe.utils.get_link_to_form(doc.doctype, doc.name) }}</li> <li>{{ _("Event Link") }}: {{ frappe.utils.get_link_to_form(doc.doctype, doc.name) }}</li>
{% if doc.is_mandatory %} {% if doc.is_mandatory %}
<li>Note: This Training Event is mandatory</li> <li>{{ _("Note: This Training Event is mandatory") }}</li>
{% endif %} {% endif %}
</ul> </ul>
</div> </div>

View File

@ -153,6 +153,24 @@
"onboard": 0, "onboard": 0,
"type": "Link" "type": "Link"
}, },
{
"hidden": 0,
"is_query_report": 0,
"label": "Grievance Type",
"link_to": "Grievance Type",
"link_type": "DocType",
"onboard": 0,
"type": "Link"
},
{
"hidden": 0,
"is_query_report": 0,
"label": "Employee Grievance",
"link_to": "Employee Grievance",
"link_type": "DocType",
"onboard": 0,
"type": "Link"
},
{ {
"dependencies": "Employee", "dependencies": "Employee",
"hidden": 0, "hidden": 0,
@ -823,7 +841,7 @@
"type": "Link" "type": "Link"
} }
], ],
"modified": "2021-04-26 13:36:15.413819", "modified": "2021-05-13 17:19:40.524444",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "HR", "module": "HR",
"name": "HR", "name": "HR",

View File

@ -60,8 +60,9 @@ class Loan(AccountsController):
self.monthly_repayment_amount = get_monthly_repayment_amount(self.repayment_method, self.loan_amount, self.rate_of_interest, self.repayment_periods) self.monthly_repayment_amount = get_monthly_repayment_amount(self.repayment_method, self.loan_amount, self.rate_of_interest, self.repayment_periods)
def check_sanctioned_amount_limit(self): def check_sanctioned_amount_limit(self):
total_loan_amount = get_total_loan_amount(self.applicant_type, self.applicant, self.company)
sanctioned_amount_limit = get_sanctioned_amount_limit(self.applicant_type, self.applicant, self.company) sanctioned_amount_limit = get_sanctioned_amount_limit(self.applicant_type, self.applicant, self.company)
if sanctioned_amount_limit:
total_loan_amount = get_total_loan_amount(self.applicant_type, self.applicant, self.company)
if sanctioned_amount_limit and flt(self.loan_amount) + flt(total_loan_amount) > flt(sanctioned_amount_limit): if sanctioned_amount_limit and flt(self.loan_amount) + flt(total_loan_amount) > flt(sanctioned_amount_limit):
frappe.throw(_("Sanctioned Amount limit crossed for {0} {1}").format(self.applicant_type, frappe.bold(self.applicant))) frappe.throw(_("Sanctioned Amount limit crossed for {0} {1}").format(self.applicant_type, frappe.bold(self.applicant)))
@ -155,9 +156,29 @@ def update_total_amount_paid(doc):
frappe.db.set_value("Loan", doc.name, "total_amount_paid", total_amount_paid) frappe.db.set_value("Loan", doc.name, "total_amount_paid", total_amount_paid)
def get_total_loan_amount(applicant_type, applicant, company): def get_total_loan_amount(applicant_type, applicant, company):
return frappe.db.get_value('Loan', pending_amount = 0
{'applicant_type': applicant_type, 'company': company, 'applicant': applicant, 'docstatus': 1}, loan_details = frappe.db.get_all("Loan",
'sum(loan_amount)') filters={"applicant_type": applicant_type, "company": company, "applicant": applicant, "docstatus": 1,
"status": ("!=", "Closed")},
fields=["status", "total_payment", "disbursed_amount", "total_interest_payable", "total_principal_paid",
"written_off_amount"])
interest_amount = flt(frappe.db.get_value("Loan Interest Accrual", {"applicant_type": applicant_type,
"company": company, "applicant": applicant, "docstatus": 1}, "sum(interest_amount - paid_interest_amount)"))
for loan in loan_details:
if loan.status in ("Disbursed", "Loan Closure Requested"):
pending_amount += flt(loan.total_payment) - flt(loan.total_interest_payable) \
- flt(loan.total_principal_paid) - flt(loan.written_off_amount)
elif loan.status == "Partially Disbursed":
pending_amount += flt(loan.disbursed_amount) - flt(loan.total_interest_payable) \
- flt(loan.total_principal_paid) - flt(loan.written_off_amount)
elif loan.status == "Sanctioned":
pending_amount += flt(loan.total_payment)
pending_amount += interest_amount
return pending_amount
def get_sanctioned_amount_limit(applicant_type, applicant, company): def get_sanctioned_amount_limit(applicant_type, applicant, company):
return frappe.db.get_value('Sanctioned Loan Amount', return frappe.db.get_value('Sanctioned Loan Amount',

View File

@ -49,7 +49,11 @@ class TestLoan(unittest.TestCase):
if not frappe.db.exists("Customer", "_Test Loan Customer"): if not frappe.db.exists("Customer", "_Test Loan Customer"):
frappe.get_doc(get_customer_dict('_Test Loan Customer')).insert(ignore_permissions=True) frappe.get_doc(get_customer_dict('_Test Loan Customer')).insert(ignore_permissions=True)
self.applicant2 = frappe.db.get_value("Customer", {'name': '_Test Loan Customer'}, 'name') if not frappe.db.exists("Customer", "_Test Loan Customer 1"):
frappe.get_doc(get_customer_dict("_Test Loan Customer 1")).insert(ignore_permissions=True)
self.applicant2 = frappe.db.get_value("Customer", {"name": "_Test Loan Customer"}, "name")
self.applicant3 = frappe.db.get_value("Customer", {"name": "_Test Loan Customer 1"}, "name")
create_loan(self.applicant1, "Personal Loan", 280000, "Repay Over Number of Periods", 20) create_loan(self.applicant1, "Personal Loan", 280000, "Repay Over Number of Periods", 20)
@ -125,6 +129,38 @@ class TestLoan(unittest.TestCase):
self.assertTrue(gl_entries1) self.assertTrue(gl_entries1)
self.assertTrue(gl_entries2) self.assertTrue(gl_entries2)
def test_sanctioned_amount_limit(self):
# Clear loan docs before checking
frappe.db.sql("DELETE FROM `tabLoan` where applicant = '_Test Loan Customer 1'")
frappe.db.sql("DELETE FROM `tabLoan Application` where applicant = '_Test Loan Customer 1'")
frappe.db.sql("DELETE FROM `tabLoan Security Pledge` where applicant = '_Test Loan Customer 1'")
if not frappe.db.get_value("Sanctioned Loan Amount", filters={"applicant_type": "Customer",
"applicant": "_Test Loan Customer 1", "company": "_Test Company"}):
frappe.get_doc({
"doctype": "Sanctioned Loan Amount",
"applicant_type": "Customer",
"applicant": "_Test Loan Customer 1",
"sanctioned_amount_limit": 1500000,
"company": "_Test Company"
}).insert(ignore_permissions=True)
# Make First Loan
pledge = [{
"loan_security": "Test Security 1",
"qty": 4000.00
}]
loan_application = create_loan_application('_Test Company', self.applicant3, 'Demand Loan', pledge)
create_pledge(loan_application)
loan = create_demand_loan(self.applicant3, "Demand Loan", loan_application, posting_date='2019-10-01')
loan.submit()
# Make second loan greater than the sanctioned amount
loan_application = create_loan_application('_Test Company', self.applicant3, 'Demand Loan', pledge,
do_not_save=True)
self.assertRaises(frappe.ValidationError, loan_application.save)
def test_regular_loan_repayment(self): def test_regular_loan_repayment(self):
pledge = [{ pledge = [{
"loan_security": "Test Security 1", "loan_security": "Test Security 1",
@ -367,7 +403,7 @@ class TestLoan(unittest.TestCase):
unpledge_request.load_from_db() unpledge_request.load_from_db()
self.assertEqual(unpledge_request.docstatus, 1) self.assertEqual(unpledge_request.docstatus, 1)
def test_santined_loan_security_unpledge(self): def test_sanctioned_loan_security_unpledge(self):
pledge = [{ pledge = [{
"loan_security": "Test Security 1", "loan_security": "Test Security 1",
"qty": 4000.00 "qty": 4000.00
@ -858,7 +894,7 @@ def create_repayment_entry(loan, applicant, posting_date, paid_amount):
return lr return lr
def create_loan_application(company, applicant, loan_type, proposed_pledges, repayment_method=None, def create_loan_application(company, applicant, loan_type, proposed_pledges, repayment_method=None,
repayment_periods=None, posting_date=None): repayment_periods=None, posting_date=None, do_not_save=False):
loan_application = frappe.new_doc('Loan Application') loan_application = frappe.new_doc('Loan Application')
loan_application.applicant_type = 'Customer' loan_application.applicant_type = 'Customer'
loan_application.company = company loan_application.company = company
@ -874,6 +910,9 @@ def create_loan_application(company, applicant, loan_type, proposed_pledges, rep
for pledge in proposed_pledges: for pledge in proposed_pledges:
loan_application.append('proposed_pledges', pledge) loan_application.append('proposed_pledges', pledge)
if do_not_save:
return loan_application
loan_application.save() loan_application.save()
loan_application.submit() loan_application.submit()

View File

@ -46,9 +46,11 @@ class LoanApplication(Document):
frappe.throw(_("Loan Amount exceeds maximum loan amount of {0} as per proposed securities").format(self.maximum_loan_amount)) frappe.throw(_("Loan Amount exceeds maximum loan amount of {0} as per proposed securities").format(self.maximum_loan_amount))
def check_sanctioned_amount_limit(self): def check_sanctioned_amount_limit(self):
total_loan_amount = get_total_loan_amount(self.applicant_type, self.applicant, self.company)
sanctioned_amount_limit = get_sanctioned_amount_limit(self.applicant_type, self.applicant, self.company) sanctioned_amount_limit = get_sanctioned_amount_limit(self.applicant_type, self.applicant, self.company)
if sanctioned_amount_limit:
total_loan_amount = get_total_loan_amount(self.applicant_type, self.applicant, self.company)
if sanctioned_amount_limit and flt(self.loan_amount) + flt(total_loan_amount) > flt(sanctioned_amount_limit): if sanctioned_amount_limit and flt(self.loan_amount) + flt(total_loan_amount) > flt(sanctioned_amount_limit):
frappe.throw(_("Sanctioned Amount limit crossed for {0} {1}").format(self.applicant_type, frappe.bold(self.applicant))) frappe.throw(_("Sanctioned Amount limit crossed for {0} {1}").format(self.applicant_type, frappe.bold(self.applicant)))

View File

@ -235,70 +235,71 @@ class LoanRepayment(AccountsController):
else: else:
remarks = _("Repayment against Loan: ") + self.against_loan remarks = _("Repayment against Loan: ") + self.against_loan
if self.total_penalty_paid: if not loan_details.repay_from_salary:
if self.total_penalty_paid:
gle_map.append(
self.get_gl_dict({
"account": loan_details.loan_account,
"against": loan_details.payment_account,
"debit": self.total_penalty_paid,
"debit_in_account_currency": self.total_penalty_paid,
"against_voucher_type": "Loan",
"against_voucher": self.against_loan,
"remarks": _("Penalty against loan:") + self.against_loan,
"cost_center": self.cost_center,
"party_type": self.applicant_type,
"party": self.applicant,
"posting_date": getdate(self.posting_date)
})
)
gle_map.append(
self.get_gl_dict({
"account": loan_details.penalty_income_account,
"against": loan_details.payment_account,
"credit": self.total_penalty_paid,
"credit_in_account_currency": self.total_penalty_paid,
"against_voucher_type": "Loan",
"against_voucher": self.against_loan,
"remarks": _("Penalty against loan:") + self.against_loan,
"cost_center": self.cost_center,
"posting_date": getdate(self.posting_date)
})
)
gle_map.append( gle_map.append(
self.get_gl_dict({ self.get_gl_dict({
"account": loan_details.loan_account, "account": loan_details.payment_account,
"against": loan_details.payment_account, "against": loan_details.loan_account + ", " + loan_details.interest_income_account
"debit": self.total_penalty_paid, + ", " + loan_details.penalty_income_account,
"debit_in_account_currency": self.total_penalty_paid, "debit": self.amount_paid,
"debit_in_account_currency": self.amount_paid,
"against_voucher_type": "Loan", "against_voucher_type": "Loan",
"against_voucher": self.against_loan, "against_voucher": self.against_loan,
"remarks": _("Penalty against loan:") + self.against_loan, "remarks": remarks,
"cost_center": self.cost_center, "cost_center": self.cost_center,
"party_type": self.applicant_type,
"party": self.applicant,
"posting_date": getdate(self.posting_date) "posting_date": getdate(self.posting_date)
}) })
) )
gle_map.append( gle_map.append(
self.get_gl_dict({ self.get_gl_dict({
"account": loan_details.penalty_income_account, "account": loan_details.loan_account,
"party_type": loan_details.applicant_type,
"party": loan_details.applicant,
"against": loan_details.payment_account, "against": loan_details.payment_account,
"credit": self.total_penalty_paid, "credit": self.amount_paid,
"credit_in_account_currency": self.total_penalty_paid, "credit_in_account_currency": self.amount_paid,
"against_voucher_type": "Loan", "against_voucher_type": "Loan",
"against_voucher": self.against_loan, "against_voucher": self.against_loan,
"remarks": _("Penalty against loan:") + self.against_loan, "remarks": remarks,
"cost_center": self.cost_center, "cost_center": self.cost_center,
"posting_date": getdate(self.posting_date) "posting_date": getdate(self.posting_date)
}) })
) )
gle_map.append( if gle_map:
self.get_gl_dict({ make_gl_entries(gle_map, cancel=cancel, adv_adj=adv_adj, merge_entries=False)
"account": loan_details.payment_account,
"against": loan_details.loan_account + ", " + loan_details.interest_income_account
+ ", " + loan_details.penalty_income_account,
"debit": self.amount_paid,
"debit_in_account_currency": self.amount_paid,
"against_voucher_type": "Loan",
"against_voucher": self.against_loan,
"remarks": remarks,
"cost_center": self.cost_center,
"posting_date": getdate(self.posting_date)
})
)
gle_map.append(
self.get_gl_dict({
"account": loan_details.loan_account,
"party_type": loan_details.applicant_type,
"party": loan_details.applicant,
"against": loan_details.payment_account,
"credit": self.amount_paid,
"credit_in_account_currency": self.amount_paid,
"against_voucher_type": "Loan",
"against_voucher": self.against_loan,
"remarks": remarks,
"cost_center": self.cost_center,
"posting_date": getdate(self.posting_date)
})
)
if gle_map:
make_gl_entries(gle_map, cancel=cancel, adv_adj=adv_adj, merge_entries=False)
def create_repayment_entry(loan, applicant, company, posting_date, loan_type, def create_repayment_entry(loan, applicant, company, posting_date, loan_type,
payment_type, interest_payable, payable_principal_amount, amount_paid, penalty_amount=None): payment_type, interest_payable, payable_principal_amount, amount_paid, penalty_amount=None):

View File

@ -12,6 +12,7 @@ from erpnext.manufacturing.doctype.bom_update_tool.bom_update_tool import update
from six import string_types from six import string_types
from erpnext.stock.doctype.item.test_item import make_item from erpnext.stock.doctype.item.test_item import make_item
from erpnext.buying.doctype.purchase_order.test_purchase_order import create_purchase_order from erpnext.buying.doctype.purchase_order.test_purchase_order import create_purchase_order
from erpnext.tests.test_subcontracting import set_backflush_based_on
test_records = frappe.get_test_records('BOM') test_records = frappe.get_test_records('BOM')
@ -160,6 +161,7 @@ class TestBOM(unittest.TestCase):
def test_subcontractor_sourced_item(self): def test_subcontractor_sourced_item(self):
item_code = "_Test Subcontracted FG Item 1" item_code = "_Test Subcontracted FG Item 1"
set_backflush_based_on('Material Transferred for Subcontract')
if not frappe.db.exists('Item', item_code): if not frappe.db.exists('Item', item_code):
make_item(item_code, { make_item(item_code, {

View File

@ -306,8 +306,25 @@ frappe.ui.form.on('Production Plan', {
}, },
download_materials_required: function(frm) { download_materials_required: function(frm) {
let get_template_url = 'erpnext.manufacturing.doctype.production_plan.production_plan.download_raw_materials'; const fields = [{
open_url_post(frappe.request.url, { cmd: get_template_url, doc: frm.doc }); fieldname: 'warehouses',
fieldtype: 'Table MultiSelect',
label: __('Warehouses'),
default: frm.doc.from_warehouse,
options: "Production Plan Material Request Warehouse",
get_query: function () {
return {
filters: {
company: frm.doc.company
}
};
},
}];
frappe.prompt(fields, (row) => {
let get_template_url = 'erpnext.manufacturing.doctype.production_plan.production_plan.download_raw_materials';
open_url_post(frappe.request.url, { cmd: get_template_url, doc: frm.doc, warehouses: row.warehouses });
}, __('Select Warehouses to get Stock for Materials Planning'), __('Get Stock'));
}, },
show_progress: function(frm) { show_progress: function(frm) {

View File

@ -477,18 +477,19 @@ class ProductionPlan(Document):
msgprint(_("No material request created")) msgprint(_("No material request created"))
@frappe.whitelist() @frappe.whitelist()
def download_raw_materials(doc): def download_raw_materials(doc, warehouses=None):
if isinstance(doc, string_types): if isinstance(doc, string_types):
doc = frappe._dict(json.loads(doc)) doc = frappe._dict(json.loads(doc))
item_list = [['Item Code', 'Description', 'Stock UOM', 'Warehouse', 'Required Qty as per BOM', item_list = [['Item Code', 'Description', 'Stock UOM', 'Warehouse', 'Required Qty as per BOM',
'Projected Qty', 'Actual Qty', 'Ordered Qty', 'Reserved Qty for Production', 'Projected Qty', 'Available Qty In Hand', 'Ordered Qty', 'Planned Qty',
'Safety Stock', 'Required Qty']] 'Reserved Qty for Production', 'Safety Stock', 'Required Qty']]
for d in get_items_for_material_requests(doc): doc.warehouse = None
for d in get_items_for_material_requests(doc, warehouses=warehouses, get_parent_warehouse_data=True):
item_list.append([d.get('item_code'), d.get('description'), d.get('stock_uom'), d.get('warehouse'), item_list.append([d.get('item_code'), d.get('description'), d.get('stock_uom'), d.get('warehouse'),
d.get('required_bom_qty'), d.get('projected_qty'), d.get('actual_qty'), d.get('ordered_qty'), d.get('required_bom_qty'), d.get('projected_qty'), d.get('actual_qty'), d.get('ordered_qty'),
d.get('reserved_qty_for_production'), d.get('safety_stock'), d.get('quantity')]) d.get('planned_qty'), d.get('reserved_qty_for_production'), d.get('safety_stock'), d.get('quantity')])
if not doc.get('for_warehouse'): if not doc.get('for_warehouse'):
row = {'item_code': d.get('item_code')} row = {'item_code': d.get('item_code')}
@ -507,7 +508,7 @@ def get_exploded_items(item_details, company, bom_no, include_non_stock_items, p
ifnull(sum(bei.stock_qty/ifnull(bom.quantity, 1)), 0)*%s as qty, item.item_name, ifnull(sum(bei.stock_qty/ifnull(bom.quantity, 1)), 0)*%s as qty, item.item_name,
bei.description, bei.stock_uom, item.min_order_qty, bei.source_warehouse, bei.description, bei.stock_uom, item.min_order_qty, bei.source_warehouse,
item.default_material_request_type, item.min_order_qty, item_default.default_warehouse, item.default_material_request_type, item.min_order_qty, item_default.default_warehouse,
item.purchase_uom, item_uom.conversion_factor item.purchase_uom, item_uom.conversion_factor, item.safety_stock
from from
`tabBOM Explosion Item` bei `tabBOM Explosion Item` bei
JOIN `tabBOM` bom ON bom.name = bei.parent JOIN `tabBOM` bom ON bom.name = bei.parent
@ -677,32 +678,36 @@ def get_bin_details(row, company, for_warehouse=None, all_warehouse=False):
return frappe.db.sql(""" select ifnull(sum(projected_qty),0) as projected_qty, return frappe.db.sql(""" select ifnull(sum(projected_qty),0) as projected_qty,
ifnull(sum(actual_qty),0) as actual_qty, ifnull(sum(ordered_qty),0) as ordered_qty, ifnull(sum(actual_qty),0) as actual_qty, ifnull(sum(ordered_qty),0) as ordered_qty,
ifnull(sum(reserved_qty_for_production),0) as reserved_qty_for_production, warehouse from `tabBin` ifnull(sum(reserved_qty_for_production),0) as reserved_qty_for_production, warehouse,
where item_code = %(item_code)s {conditions} ifnull(sum(planned_qty),0) as planned_qty
from `tabBin` where item_code = %(item_code)s {conditions}
group by item_code, warehouse group by item_code, warehouse
""".format(conditions=conditions), { "item_code": row['item_code'] }, as_dict=1) """.format(conditions=conditions), { "item_code": row['item_code'] }, as_dict=1)
def get_warehouse_list(warehouses, warehouse_list=[]):
if isinstance(warehouses, string_types):
warehouses = json.loads(warehouses)
for row in warehouses:
child_warehouses = frappe.db.get_descendants('Warehouse', row.get("warehouse"))
if child_warehouses:
warehouse_list.extend(child_warehouses)
else:
warehouse_list.append(row.get("warehouse"))
@frappe.whitelist() @frappe.whitelist()
def get_items_for_material_requests(doc, warehouses=None): def get_items_for_material_requests(doc, warehouses=None, get_parent_warehouse_data=None):
if isinstance(doc, string_types): if isinstance(doc, string_types):
doc = frappe._dict(json.loads(doc)) doc = frappe._dict(json.loads(doc))
warehouse_list = [] warehouse_list = []
if warehouses: if warehouses:
if isinstance(warehouses, string_types): get_warehouse_list(warehouses, warehouse_list)
warehouses = json.loads(warehouses)
for row in warehouses:
child_warehouses = frappe.db.get_descendants('Warehouse', row.get("warehouse"))
if child_warehouses:
warehouse_list.extend(child_warehouses)
else:
warehouse_list.append(row.get("warehouse"))
if warehouse_list: if warehouse_list:
warehouses = list(set(warehouse_list)) warehouses = list(set(warehouse_list))
if doc.get("for_warehouse") and doc.get("for_warehouse") in warehouses: if doc.get("for_warehouse") and not get_parent_warehouse_data and doc.get("for_warehouse") in warehouses:
warehouses.remove(doc.get("for_warehouse")) warehouses.remove(doc.get("for_warehouse"))
warehouse_list = None warehouse_list = None
@ -795,7 +800,7 @@ def get_items_for_material_requests(doc, warehouses=None):
if items: if items:
mr_items.append(items) mr_items.append(items)
if not ignore_existing_ordered_qty and warehouses: if (not ignore_existing_ordered_qty or get_parent_warehouse_data) and warehouses:
new_mr_items = [] new_mr_items = []
for item in mr_items: for item in mr_items:
get_materials_from_other_locations(item, warehouses, new_mr_items, company) get_materials_from_other_locations(item, warehouses, new_mr_items, company)

View File

@ -12,8 +12,12 @@ frappe.ui.form.on('Additional Salary', {
} }
}; };
}); });
},
frm.trigger('set_earning_component'); onload: function(frm) {
if (frm.doc.type) {
frm.trigger('set_component_query');
}
}, },
employee: function(frm) { employee: function(frm) {
@ -46,14 +50,19 @@ frappe.ui.form.on('Additional Salary', {
}, },
company: function(frm) { company: function(frm) {
frm.trigger('set_earning_component'); frm.set_value("type", "");
frm.trigger('set_component_query');
}, },
set_earning_component: function(frm) { set_component_query: function(frm) {
if (!frm.doc.company) return; if (!frm.doc.company) return;
let filters = {company: frm.doc.company};
if (frm.doc.type) {
filters.type = frm.doc.type;
}
frm.set_query("salary_component", function() { frm.set_query("salary_component", function() {
return { return {
filters: {type: ["in", ["earning", "deduction"]], company: frm.doc.company} filters: filters
}; };
}); });
}, },

View File

@ -481,6 +481,7 @@ def make_employee_salary_slip(user, payroll_frequency, salary_structure=None):
if not salary_structure: if not salary_structure:
salary_structure = payroll_frequency + " Salary Structure Test for Salary Slip" salary_structure = payroll_frequency + " Salary Structure Test for Salary Slip"
employee = frappe.db.get_value("Employee", {"user_id": user}) employee = frappe.db.get_value("Employee", {"user_id": user})
salary_structure_doc = make_salary_structure(salary_structure, payroll_frequency, employee=employee) salary_structure_doc = make_salary_structure(salary_structure, payroll_frequency, employee=employee)
salary_slip_name = frappe.db.get_value("Salary Slip", {"employee": frappe.db.get_value("Employee", {"user_id": user})}) salary_slip_name = frappe.db.get_value("Salary Slip", {"employee": frappe.db.get_value("Employee", {"user_id": user})})

View File

@ -124,8 +124,8 @@ def make_salary_structure(salary_structure, payroll_frequency, employee=None,
"doctype": "Salary Structure", "doctype": "Salary Structure",
"name": salary_structure, "name": salary_structure,
"company": company or erpnext.get_default_company(), "company": company or erpnext.get_default_company(),
"earnings": make_earning_salary_component(test_tax=test_tax, company_list=["_Test Company"]), "earnings": make_earning_salary_component(setup=True, test_tax=test_tax, company_list=["_Test Company"]),
"deductions": make_deduction_salary_component(test_tax=test_tax, company_list=["_Test Company"]), "deductions": make_deduction_salary_component(setup=True, test_tax=test_tax, company_list=["_Test Company"]),
"payroll_frequency": payroll_frequency, "payroll_frequency": payroll_frequency,
"payment_account": get_random("Account", filters={'account_currency': currency}), "payment_account": get_random("Account", filters={'account_currency': currency}),
"currency": currency "currency": currency

View File

@ -1,7 +1,7 @@
from __future__ import unicode_literals from __future__ import unicode_literals
import unittest import unittest
import frappe import frappe
from frappe.utils import getdate, nowdate from frappe.utils import getdate, nowdate, add_days
from erpnext.hr.doctype.employee.test_employee import make_employee from erpnext.hr.doctype.employee.test_employee import make_employee
from erpnext.projects.doctype.timesheet.test_timesheet import make_salary_structure_for_timesheet, make_timesheet from erpnext.projects.doctype.timesheet.test_timesheet import make_salary_structure_for_timesheet, make_timesheet
from erpnext.projects.doctype.timesheet.timesheet import make_salary_slip, make_sales_invoice from erpnext.projects.doctype.timesheet.timesheet import make_salary_slip, make_sales_invoice
@ -16,17 +16,22 @@ class TestProjectProfitability(unittest.TestCase):
make_salary_structure_for_timesheet(emp, company='_Test Company') make_salary_structure_for_timesheet(emp, company='_Test Company')
self.timesheet = make_timesheet(emp, simulate = True, is_billable=1) self.timesheet = make_timesheet(emp, simulate = True, is_billable=1)
self.salary_slip = make_salary_slip(self.timesheet.name) self.salary_slip = make_salary_slip(self.timesheet.name)
holidays = self.salary_slip.get_holidays_for_employee(nowdate(), nowdate())
if holidays:
frappe.db.set_value('Payroll Settings', None, 'include_holidays_in_total_working_days', 1)
self.salary_slip.submit() self.salary_slip.submit()
self.sales_invoice = make_sales_invoice(self.timesheet.name, '_Test Item', '_Test Customer') self.sales_invoice = make_sales_invoice(self.timesheet.name, '_Test Item', '_Test Customer')
self.sales_invoice.due_date = nowdate() self.sales_invoice.due_date = nowdate()
self.sales_invoice.submit() self.sales_invoice.submit()
frappe.db.set_value('HR Settings', None, 'standard_working_hours', 8) frappe.db.set_value('HR Settings', None, 'standard_working_hours', 8)
frappe.db.set_value('Payroll Settings', None, 'include_holidays_in_total_working_days', 0)
def test_project_profitability(self): def test_project_profitability(self):
filters = { filters = {
'company': '_Test Company', 'company': '_Test Company',
'start_date': getdate(), 'start_date': add_days(getdate(), -3),
'end_date': getdate() 'end_date': getdate()
} }

View File

@ -122,9 +122,20 @@ erpnext.buying.BuyingController = erpnext.TransactionController.extend({
this.set_from_product_bundle(); this.set_from_product_bundle();
} }
this.toggle_subcontracting_fields();
this._super(); this._super();
}, },
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() { supplier: function() {
var me = this; var me = this;
erpnext.utils.get_party_details(this.frm, null, null, function(){ erpnext.utils.get_party_details(this.frm, null, null, function(){

View File

@ -387,7 +387,7 @@ erpnext.TransactionController = erpnext.taxes_and_totals.extend({
if(this.frm.doc.scan_barcode) { if(this.frm.doc.scan_barcode) {
frappe.call({ frappe.call({
method: "erpnext.selling.page.point_of_sale.point_of_sale.search_serial_or_batch_or_barcode_number", method: "erpnext.selling.page.point_of_sale.point_of_sale.search_for_serial_or_batch_or_barcode_number",
args: { search_value: this.frm.doc.scan_barcode } args: { search_value: this.frm.doc.scan_barcode }
}).then(r => { }).then(r => {
const data = r && r.message; const data = r && r.message;
@ -723,6 +723,10 @@ erpnext.TransactionController = erpnext.taxes_and_totals.extend({
var me = this; var me = this;
var item = frappe.get_doc(cdt, cdn); var item = frappe.get_doc(cdt, cdn);
if (item && item.doctype === 'Purchase Receipt Item Supplied') {
return;
}
if (item && item.serial_no) { if (item && item.serial_no) {
if (!item.item_code) { if (!item.item_code) {
this.frm.trigger("item_code", cdt, cdn); this.frm.trigger("item_code", cdt, cdn);

View File

@ -233,7 +233,7 @@ class SalesOrder(SellingController):
# Checks Sales Invoice # Checks Sales Invoice
submit_rv = frappe.db.sql_list("""select t1.name submit_rv = frappe.db.sql_list("""select t1.name
from `tabSales Invoice` t1,`tabSales Invoice Item` t2 from `tabSales Invoice` t1,`tabSales Invoice Item` t2
where t1.name = t2.parent and t2.sales_order = %s and t1.docstatus = 1""", where t1.name = t2.parent and t2.sales_order = %s and t1.docstatus < 2""",
self.name) self.name)
if submit_rv: if submit_rv:

View File

@ -1217,6 +1217,19 @@ class TestSalesOrder(unittest.TestCase):
# To test if the SO does NOT have a Blanket Order # To test if the SO does NOT have a Blanket Order
self.assertEqual(so_doc.items[0].blanket_order, None) self.assertEqual(so_doc.items[0].blanket_order, None)
def test_so_cancellation_when_si_drafted(self):
"""
Test to check if Sales Order gets cancelled if Sales Invoice is in Draft state
Expected result: sales order should not get cancelled
"""
so = make_sales_order()
so.submit()
si = make_sales_invoice(so.name)
si.save()
self.assertRaises(frappe.ValidationError, so.cancel)
def make_sales_order(**args): def make_sales_order(**args):
so = frappe.new_doc("Sales Order") so = frappe.new_doc("Sales Order")

View File

@ -1867,7 +1867,7 @@
"South Africa": { "South Africa": {
"South Africa Tax": { "South Africa Tax": {
"account_name": "VAT", "account_name": "VAT",
"tax_rate": 14.00 "tax_rate": 15.00
} }
}, },

View File

@ -61,7 +61,8 @@ class ProductQuery:
], ],
or_filters=self.or_filters, or_filters=self.or_filters,
start=start, start=start,
limit=self.page_length limit=self.page_length,
order_by="weightage desc"
) )
items_dict = {item.name: item for item in items} items_dict = {item.name: item for item in items}
@ -71,7 +72,15 @@ class ProductQuery:
result = [items_dict.get(item) for item in list(set.intersection(*all_items))] result = [items_dict.get(item) for item in list(set.intersection(*all_items))]
else: else:
result = frappe.get_all("Item", fields=self.fields, filters=self.filters, or_filters=self.or_filters, start=start, limit=self.page_length) result = frappe.get_all(
"Item",
fields=self.fields,
filters=self.filters,
or_filters=self.or_filters,
start=start,
limit=self.page_length,
order_by="weightage desc"
)
for item in result: for item in result:
product_info = get_product_info_for_website(item.item_code, skip_quotation_creation=True).get('product_info') product_info = get_product_info_for_website(item.item_code, skip_quotation_creation=True).get('product_info')

View File

@ -115,7 +115,7 @@ class Bin(Document):
#Get Transferred Entries #Get Transferred Entries
materials_transferred = frappe.db.sql(""" materials_transferred = frappe.db.sql("""
select select
ifnull(sum(transfer_qty),0) ifnull(sum(CASE WHEN se.is_return = 1 THEN (transfer_qty * -1) ELSE transfer_qty END),0)
from from
`tabStock Entry` se, `tabStock Entry Detail` sed, `tabPurchase Order` po `tabStock Entry` se, `tabStock Entry Detail` sed, `tabPurchase Order` po
where where

View File

@ -6,8 +6,8 @@ frappe.listview_settings['Delivery Note'] = {
return [__("Return"), "gray", "is_return,=,Yes"]; return [__("Return"), "gray", "is_return,=,Yes"];
} else if (doc.status === "Closed") { } else if (doc.status === "Closed") {
return [__("Closed"), "green", "status,=,Closed"]; return [__("Closed"), "green", "status,=,Closed"];
} else if (flt(doc.per_returned, 2) === 100) { } else if (doc.status === "Return Issued") {
return [__("Return Issued"), "grey", "per_returned,=,100"]; return [__("Return Issued"), "grey", "status,=,Return Issued"];
} else if (flt(doc.per_billed, 2) < 100) { } else if (flt(doc.per_billed, 2) < 100) {
return [__("To Bill"), "orange", "per_billed,<,100"]; return [__("To Bill"), "orange", "per_billed,<,100"];
} else if (flt(doc.per_billed, 2) === 100) { } else if (flt(doc.per_billed, 2) === 100) {

View File

@ -18,6 +18,9 @@ class TestItemAlternative(unittest.TestCase):
make_items() make_items()
def test_alternative_item_for_subcontract_rm(self): def test_alternative_item_for_subcontract_rm(self):
frappe.db.set_value('Buying Settings', None,
'backflush_raw_materials_of_subcontract_based_on', 'BOM')
create_stock_reconciliation(item_code='Alternate Item For A RW 1', warehouse='_Test Warehouse - _TC', create_stock_reconciliation(item_code='Alternate Item For A RW 1', warehouse='_Test Warehouse - _TC',
qty=5, rate=2000) qty=5, rate=2000)
create_stock_reconciliation(item_code='Test FG A RW 2', warehouse='_Test Warehouse - _TC', create_stock_reconciliation(item_code='Test FG A RW 2', warehouse='_Test Warehouse - _TC',
@ -65,6 +68,8 @@ class TestItemAlternative(unittest.TestCase):
status = True status = True
self.assertEqual(status, True) self.assertEqual(status, True)
frappe.db.set_value('Buying Settings', None,
'backflush_raw_materials_of_subcontract_based_on', 'Material Transferred for Subcontract')
def test_alternative_item_for_production_rm(self): def test_alternative_item_for_production_rm(self):
create_stock_reconciliation(item_code='Alternate Item For A RW 1', create_stock_reconciliation(item_code='Alternate Item For A RW 1',

View File

@ -101,7 +101,8 @@ frappe.ui.form.on('Material Request', {
} }
if (frm.doc.docstatus == 1 && frm.doc.status != 'Stopped') { if (frm.doc.docstatus == 1 && frm.doc.status != 'Stopped') {
if (flt(frm.doc.per_ordered, 2) < 100) { let precision = frappe.defaults.get_default("float_precision");
if (flt(frm.doc.per_ordered, precision) < 100) {
let add_create_pick_list_button = () => { let add_create_pick_list_button = () => {
frm.add_custom_button(__('Pick List'), frm.add_custom_button(__('Pick List'),
() => frm.events.create_pick_list(frm), __('Create')); () => frm.events.create_pick_list(frm), __('Create'));

View File

@ -514,8 +514,7 @@
"oldfieldname": "pr_raw_material_details", "oldfieldname": "pr_raw_material_details",
"oldfieldtype": "Table", "oldfieldtype": "Table",
"options": "Purchase Receipt Item Supplied", "options": "Purchase Receipt Item Supplied",
"print_hide": 1, "print_hide": 1
"read_only": 1
}, },
{ {
"fieldname": "section_break0", "fieldname": "section_break0",
@ -1149,7 +1148,7 @@
"idx": 261, "idx": 261,
"is_submittable": 1, "is_submittable": 1,
"links": [], "links": [],
"modified": "2021-04-19 01:01:00.754119", "modified": "2021-05-25 00:15:12.239017",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Stock", "module": "Stock",
"name": "Purchase Receipt", "name": "Purchase Receipt",

View File

@ -202,6 +202,7 @@ class PurchaseReceipt(BuyingController):
self.make_gl_entries() self.make_gl_entries()
self.repost_future_sle_and_gle() self.repost_future_sle_and_gle()
self.set_consumed_qty_in_po()
def check_next_docstatus(self): def check_next_docstatus(self):
submit_rv = frappe.db.sql("""select t1.name submit_rv = frappe.db.sql("""select t1.name
@ -233,6 +234,7 @@ class PurchaseReceipt(BuyingController):
self.repost_future_sle_and_gle() self.repost_future_sle_and_gle()
self.ignore_linked_doctypes = ('GL Entry', 'Stock Ledger Entry', 'Repost Item Valuation') self.ignore_linked_doctypes = ('GL Entry', 'Stock Ledger Entry', 'Repost Item Valuation')
self.delete_auto_created_batches() self.delete_auto_created_batches()
self.set_consumed_qty_in_po()
@frappe.whitelist() @frappe.whitelist()
def get_current_stock(self): def get_current_stock(self):

View File

@ -335,6 +335,10 @@ class TestPurchaseReceipt(unittest.TestCase):
se2.cancel() se2.cancel()
se3.cancel() se3.cancel()
po.reload() po.reload()
pr2.load_from_db()
pr2.cancel()
po.load_from_db()
po.cancel() po.cancel()
def test_serial_no_supplier(self): def test_serial_no_supplier(self):

View File

@ -1079,6 +1079,10 @@ erpnext.stock.select_batch_and_serial_no = (frm, item) => {
} }
function attach_bom_items(bom_no) { function attach_bom_items(bom_no) {
if (!bom_no) {
return
}
if (check_should_not_attach_bom_items(bom_no)) return if (check_should_not_attach_bom_items(bom_no)) return
frappe.db.get_doc("BOM",bom_no).then(bom => { frappe.db.get_doc("BOM",bom_no).then(bom => {
const {name, items} = bom const {name, items} = bom

View File

@ -74,7 +74,8 @@
"total_amount", "total_amount",
"job_card", "job_card",
"amended_from", "amended_from",
"credit_note" "credit_note",
"is_return"
], ],
"fields": [ "fields": [
{ {
@ -611,6 +612,16 @@
"fieldname": "apply_putaway_rule", "fieldname": "apply_putaway_rule",
"fieldtype": "Check", "fieldtype": "Check",
"label": "Apply Putaway Rule" "label": "Apply Putaway Rule"
},
{
"default": "0",
"fieldname": "is_return",
"fieldtype": "Check",
"hidden": 1,
"label": "Is Return",
"no_copy": 1,
"print_hide": 1,
"read_only": 1
} }
], ],
"icon": "fa fa-file-text", "icon": "fa fa-file-text",
@ -618,7 +629,7 @@
"index_web_pages_for_search": 1, "index_web_pages_for_search": 1,
"is_submittable": 1, "is_submittable": 1,
"links": [], "links": [],
"modified": "2021-05-24 11:32:23.904307", "modified": "2021-05-26 17:07:58.015737",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Stock", "module": "Stock",
"name": "Stock Entry", "name": "Stock Entry",

View File

@ -97,8 +97,7 @@ class StockEntry(StockController):
update_serial_nos_after_submit(self, "items") update_serial_nos_after_submit(self, "items")
self.update_work_order() self.update_work_order()
self.validate_purchase_order() self.validate_purchase_order()
if self.purchase_order and self.purpose == "Send to Subcontractor": self.update_purchase_order_supplied_items()
self.update_purchase_order_supplied_items()
self.make_gl_entries() self.make_gl_entries()
@ -117,9 +116,7 @@ class StockEntry(StockController):
self.set_material_request_transfer_status('Completed') self.set_material_request_transfer_status('Completed')
def on_cancel(self): def on_cancel(self):
self.update_purchase_order_supplied_items()
if self.purchase_order and self.purpose == "Send to Subcontractor":
self.update_purchase_order_supplied_items()
if self.work_order and self.purpose == "Material Consumption for Manufacture": if self.work_order and self.purpose == "Material Consumption for Manufacture":
self.validate_work_order_status() self.validate_work_order_status()
@ -1008,10 +1005,12 @@ class StockEntry(StockController):
if self.purchase_order and self.purpose == "Send to Subcontractor": if self.purchase_order and self.purpose == "Send to Subcontractor":
#Get PO Supplied Items Details #Get PO Supplied Items Details
item_wh = frappe._dict(frappe.db.sql(""" item_wh = frappe._dict(frappe.db.sql("""
select rm_item_code, reserve_warehouse SELECT
from `tabPurchase Order` po, `tabPurchase Order Item Supplied` poitemsup rm_item_code, reserve_warehouse
where po.name = poitemsup.parent FROM
and po.name = %s""",self.purchase_order)) `tabPurchase Order` po, `tabPurchase Order Item Supplied` poitemsup
WHERE
po.name = poitemsup.parent and po.name = %s """,self.purchase_order))
for item in itervalues(item_dict): for item in itervalues(item_dict):
if self.pro_doc and cint(self.pro_doc.from_wip_warehouse): if self.pro_doc and cint(self.pro_doc.from_wip_warehouse):
@ -1294,7 +1293,8 @@ class StockEntry(StockController):
item_dict[item]["qty"] = 0 item_dict[item]["qty"] = 0
# delete items with 0 qty # delete items with 0 qty
for item in item_dict.keys(): list_of_items = item_dict.keys()
for item in list_of_items:
if not item_dict[item]["qty"]: if not item_dict[item]["qty"]:
del item_dict[item] del item_dict[item]
@ -1347,7 +1347,7 @@ class StockEntry(StockController):
se_child.is_scrap_item = item_dict[d].get("is_scrap_item", 0) se_child.is_scrap_item = item_dict[d].get("is_scrap_item", 0)
for field in ["idx", "po_detail", "original_item", for field in ["idx", "po_detail", "original_item",
"expense_account", "description", "item_name"]: "expense_account", "description", "item_name", "serial_no", "batch_no"]:
if item_dict[d].get(field): if item_dict[d].get(field):
se_child.set(field, item_dict[d].get(field)) se_child.set(field, item_dict[d].get(field))
@ -1400,33 +1400,26 @@ class StockEntry(StockController):
.format(item.batch_no, item.item_code)) .format(item.batch_no, item.item_code))
def update_purchase_order_supplied_items(self): def update_purchase_order_supplied_items(self):
#Get PO Supplied Items Details if (self.purchase_order and
item_wh = frappe._dict(frappe.db.sql(""" (self.purpose in ['Send to Subcontractor', 'Material Transfer'] or self.is_return)):
select rm_item_code, reserve_warehouse
from `tabPurchase Order` po, `tabPurchase Order Item Supplied` poitemsup
where po.name = poitemsup.parent
and po.name = %s""", self.purchase_order))
#Update Supplied Qty in PO Supplied Items #Get PO Supplied Items Details
item_wh = frappe._dict(frappe.db.sql("""
select rm_item_code, reserve_warehouse
from `tabPurchase Order` po, `tabPurchase Order Item Supplied` poitemsup
where po.name = poitemsup.parent
and po.name = %s""", self.purchase_order))
frappe.db.sql("""UPDATE `tabPurchase Order Item Supplied` pos supplied_items = get_supplied_items(self.purchase_order)
SET for name, item in supplied_items.items():
pos.supplied_qty = IFNULL((SELECT ifnull(sum(transfer_qty), 0) frappe.db.set_value('Purchase Order Item Supplied', name, item)
FROM
`tabStock Entry Detail` sed, `tabStock Entry` se
WHERE
pos.name = sed.po_detail AND pos.rm_item_code = sed.item_code
AND pos.parent = se.purchase_order AND sed.docstatus = 1
AND se.name = sed.parent and se.purchase_order = %(po)s
), 0)
WHERE pos.docstatus = 1 and pos.parent = %(po)s""", {"po": self.purchase_order})
#Update reserved sub contracted quantity in bin based on Supplied Item Details and #Update reserved sub contracted quantity in bin based on Supplied Item Details and
for d in self.get("items"): for d in self.get("items"):
item_code = d.get('original_item') or d.get('item_code') item_code = d.get('original_item') or d.get('item_code')
reserve_warehouse = item_wh.get(item_code) reserve_warehouse = item_wh.get(item_code)
stock_bin = get_bin(item_code, reserve_warehouse) stock_bin = get_bin(item_code, reserve_warehouse)
stock_bin.update_reserved_qty_for_sub_contracting() stock_bin.update_reserved_qty_for_sub_contracting()
def update_so_in_serial_number(self): def update_so_in_serial_number(self):
so_name, item_code = frappe.db.get_value("Work Order", self.work_order, ["sales_order", "production_item"]) so_name, item_code = frappe.db.get_value("Work Order", self.work_order, ["sales_order", "production_item"])
@ -1480,7 +1473,7 @@ class StockEntry(StockController):
cond += """ WHEN (parent = %s and name = %s) THEN %s cond += """ WHEN (parent = %s and name = %s) THEN %s
""" %(frappe.db.escape(data[0]), frappe.db.escape(data[1]), transferred_qty) """ %(frappe.db.escape(data[0]), frappe.db.escape(data[1]), transferred_qty)
if cond and stock_entries_child_list: if stock_entries_child_list:
frappe.db.sql(""" UPDATE `tabStock Entry Detail` frappe.db.sql(""" UPDATE `tabStock Entry Detail`
SET SET
transferred_qty = CASE {cond} END transferred_qty = CASE {cond} END
@ -1751,3 +1744,30 @@ def validate_sample_quantity(item_code, sample_quantity, qty, batch_no = None):
format(max_retain_qty, batch_no, item_code), alert=True) format(max_retain_qty, batch_no, item_code), alert=True)
sample_quantity = qty_diff sample_quantity = qty_diff
return sample_quantity return sample_quantity
def get_supplied_items(purchase_order):
fields = ['`tabStock Entry Detail`.`transfer_qty`', '`tabStock Entry`.`is_return`',
'`tabStock Entry Detail`.`po_detail`', '`tabStock Entry Detail`.`item_code`']
filters = [['Stock Entry', 'docstatus', '=', 1], ['Stock Entry', 'purchase_order', '=', purchase_order]]
supplied_item_details = {}
for row in frappe.get_all('Stock Entry', fields = fields, filters = filters):
if not row.po_detail:
continue
key = row.po_detail
if key not in supplied_item_details:
supplied_item_details.setdefault(key,
frappe._dict({'supplied_qty': 0, 'returned_qty':0, 'total_supplied_qty':0}))
supplied_item = supplied_item_details[key]
if row.is_return:
supplied_item.returned_qty += row.transfer_qty
else:
supplied_item.supplied_qty += row.transfer_qty
supplied_item.total_supplied_qty = flt(supplied_item.supplied_qty) - flt(supplied_item.returned_qty)
return supplied_item_details

View File

@ -5,7 +5,7 @@
from __future__ import unicode_literals from __future__ import unicode_literals
import frappe import frappe
from frappe import _ from frappe import _
from frappe.utils import flt, getdate, add_days, formatdate, get_datetime, date_diff from frappe.utils import flt, getdate, add_days, formatdate, get_datetime, cint
from frappe.model.document import Document from frappe.model.document import Document
from datetime import date from datetime import date
from erpnext.controllers.item_variant import ItemTemplateCannotHaveStock from erpnext.controllers.item_variant import ItemTemplateCannotHaveStock
@ -108,17 +108,18 @@ class StockLedgerEntry(Document):
self.stock_uom = item_det.stock_uom self.stock_uom = item_det.stock_uom
def check_stock_frozen_date(self): def check_stock_frozen_date(self):
stock_frozen_upto = frappe.db.get_value('Stock Settings', None, 'stock_frozen_upto') or '' stock_settings = frappe.get_doc('Stock Settings', 'Stock Settings')
if stock_frozen_upto:
stock_auth_role = frappe.db.get_value('Stock Settings', None,'stock_auth_role')
if getdate(self.posting_date) <= getdate(stock_frozen_upto) and not stock_auth_role in frappe.get_roles():
frappe.throw(_("Stock transactions before {0} are frozen").format(formatdate(stock_frozen_upto)), StockFreezeError)
stock_frozen_upto_days = int(frappe.db.get_value('Stock Settings', None, 'stock_frozen_upto_days') or 0) if stock_settings.stock_frozen_upto:
if (getdate(self.posting_date) <= getdate(stock_settings.stock_frozen_upto)
and stock_settings.stock_auth_role not in frappe.get_roles()):
frappe.throw(_("Stock transactions before {0} are frozen")
.format(formatdate(stock_settings.stock_frozen_upto)), StockFreezeError)
stock_frozen_upto_days = cint(stock_settings.stock_frozen_upto_days)
if stock_frozen_upto_days: if stock_frozen_upto_days:
stock_auth_role = frappe.db.get_value('Stock Settings', None,'stock_auth_role')
older_than_x_days_ago = (add_days(getdate(self.posting_date), stock_frozen_upto_days) <= date.today()) older_than_x_days_ago = (add_days(getdate(self.posting_date), stock_frozen_upto_days) <= date.today())
if older_than_x_days_ago and not stock_auth_role in frappe.get_roles(): if older_than_x_days_ago and stock_settings.stock_auth_role not in frappe.get_roles():
frappe.throw(_("Not allowed to update stock transactions older than {0}").format(stock_frozen_upto_days), StockFreezeError) frappe.throw(_("Not allowed to update stock transactions older than {0}").format(stock_frozen_upto_days), StockFreezeError)
def scrub_posting_time(self): def scrub_posting_time(self):

View File

@ -473,6 +473,13 @@ class StockReconciliation(StockController):
else: else:
self._submit() self._submit()
def cancel(self):
if len(self.items) > 100:
msgprint(_("The task has been enqueued as a background job. In case there is any issue on processing in background, the system will add a comment about the error on this Stock Reconciliation and revert to the Submitted stage"))
self.queue_action('cancel', timeout=2000)
else:
self._cancel()
@frappe.whitelist() @frappe.whitelist()
def get_items(warehouse, posting_date, posting_time, company): def get_items(warehouse, posting_date, posting_time, company):
lft, rgt = frappe.db.get_value("Warehouse", warehouse, ["lft", "rgt"]) lft, rgt = frappe.db.get_value("Warehouse", warehouse, ["lft", "rgt"])

View File

@ -11,6 +11,7 @@ from frappe.test_runner import make_test_records
import erpnext import erpnext
from erpnext.stock.doctype.stock_entry.stock_entry_utils import make_stock_entry from erpnext.stock.doctype.stock_entry.stock_entry_utils import make_stock_entry
from erpnext.accounts.doctype.account.test_account import get_inventory_account, create_account from erpnext.accounts.doctype.account.test_account import get_inventory_account, create_account
from erpnext.stock.doctype.item.test_item import create_item
test_records = frappe.get_test_records('Warehouse') test_records = frappe.get_test_records('Warehouse')
@ -92,6 +93,39 @@ class TestWarehouse(unittest.TestCase):
self.assertTrue(frappe.db.get_value("Warehouse", self.assertTrue(frappe.db.get_value("Warehouse",
filters={"account": "Test Warehouse for Merging 2 - TCP1"})) filters={"account": "Test Warehouse for Merging 2 - TCP1"}))
def test_unlinking_warehouse_from_item_defaults(self):
company = "_Test Company"
warehouse_names = [f'_Test Warehouse {i} for Unlinking' for i in range(2)]
warehouse_ids = []
for warehouse in warehouse_names:
warehouse_id = create_warehouse(warehouse, company=company)
warehouse_ids.append(warehouse_id)
item_names = [f'_Test Item {i} for Unlinking' for i in range(2)]
for item, warehouse in zip(item_names, warehouse_ids):
create_item(item, warehouse=warehouse, company=company)
# Delete warehouses
for warehouse in warehouse_ids:
frappe.delete_doc("Warehouse", warehouse)
# Check Item existance
for item in item_names:
self.assertTrue(
bool(frappe.db.exists("Item", item)),
f"{item} doesn't exist"
)
item_doc = frappe.get_doc("Item", item)
for item_default in item_doc.item_defaults:
self.assertNotIn(
item_default.default_warehouse,
warehouse_ids,
f"{item} linked to {item_default.default_warehouse} in {warehouse_ids}."
)
def create_warehouse(warehouse_name, properties=None, company=None): def create_warehouse(warehouse_name, properties=None, company=None):
if not company: if not company:
company = "_Test Company" company = "_Test Company"

View File

@ -54,6 +54,7 @@ class Warehouse(NestedSet):
throw(_("Child warehouse exists for this warehouse. You can not delete this warehouse.")) throw(_("Child warehouse exists for this warehouse. You can not delete this warehouse."))
self.update_nsm_model() self.update_nsm_model()
self.unlink_from_items()
def check_if_sle_exists(self): def check_if_sle_exists(self):
return frappe.db.sql("""select name from `tabStock Ledger Entry` return frappe.db.sql("""select name from `tabStock Ledger Entry`
@ -138,6 +139,12 @@ class Warehouse(NestedSet):
self.save() self.save()
return 1 return 1
def unlink_from_items(self):
frappe.db.sql("""
update `tabItem Default`
set default_warehouse=NULL
where default_warehouse=%s""", self.name)
@frappe.whitelist() @frappe.whitelist()
def get_children(doctype, parent=None, company=None, is_root=False): def get_children(doctype, parent=None, company=None, is_root=False):
if is_root: if is_root:

View File

@ -0,0 +1,27 @@
// Copyright (c) 2016, Frappe Technologies Pvt. Ltd. and contributors
// For license information, please see license.txt
/* eslint-disable */
frappe.query_reports["Incorrect Balance Qty After Transaction"] = {
"filters": [
{
label: __("Company"),
fieldtype: "Link",
fieldname: "company",
options: "Company",
default: frappe.defaults.get_user_default("Company"),
reqd: 1
},
{
label: __('Item Code'),
fieldtype: 'Link',
fieldname: 'item_code',
options: 'Item'
},
{
label: __('Warehouse'),
fieldtype: 'Link',
fieldname: 'warehouse'
}
]
};

View File

@ -0,0 +1,32 @@
{
"add_total_row": 0,
"columns": [],
"creation": "2021-05-12 16:47:58.717853",
"disable_prepared_report": 0,
"disabled": 0,
"docstatus": 0,
"doctype": "Report",
"filters": [],
"idx": 0,
"is_standard": "Yes",
"modified": "2021-05-12 16:48:28.347575",
"modified_by": "Administrator",
"module": "Stock",
"name": "Incorrect Balance Qty After Transaction",
"owner": "Administrator",
"prepared_report": 0,
"ref_doctype": "Stock Ledger Entry",
"report_name": "Incorrect Balance Qty After Transaction",
"report_type": "Script Report",
"roles": [
{
"role": "Stock User"
},
{
"role": "Stock Manager"
},
{
"role": "Purchase User"
}
]
}

View File

@ -0,0 +1,111 @@
# Copyright (c) 2013, Frappe Technologies Pvt. Ltd. and contributors
# For license information, please see license.txt
import frappe
from frappe import _
from six import iteritems
from frappe.utils import flt
def execute(filters=None):
columns, data = [], []
columns = get_columns()
data = get_data(filters)
return columns, data
def get_data(filters):
data = get_stock_ledger_entries(filters)
itewise_balance_qty = {}
for row in data:
key = (row.item_code, row.warehouse)
itewise_balance_qty.setdefault(key, []).append(row)
res = validate_data(itewise_balance_qty)
return res
def validate_data(itewise_balance_qty):
res = []
for key, data in iteritems(itewise_balance_qty):
row = get_incorrect_data(data)
if row:
res.append(row)
res.append({})
return res
def get_incorrect_data(data):
balance_qty = 0.0
for row in data:
balance_qty += row.actual_qty
if row.voucher_type == "Stock Reconciliation" and not row.batch_no:
balance_qty = flt(row.qty_after_transaction)
row.expected_balance_qty = balance_qty
if abs(flt(row.expected_balance_qty) - flt(row.qty_after_transaction)) > 0.5:
row.differnce = abs(flt(row.expected_balance_qty) - flt(row.qty_after_transaction))
return row
def get_stock_ledger_entries(report_filters):
filters = {}
fields = ['name', 'voucher_type', 'voucher_no', 'item_code', 'actual_qty',
'posting_date', 'posting_time', 'company', 'warehouse', 'qty_after_transaction', 'batch_no']
for field in ['warehouse', 'item_code', 'company']:
if report_filters.get(field):
filters[field] = report_filters.get(field)
return frappe.get_all('Stock Ledger Entry', fields = fields, filters = filters,
order_by = 'timestamp(posting_date, posting_time) asc, creation asc')
def get_columns():
return [{
'label': _('Id'),
'fieldtype': 'Link',
'fieldname': 'name',
'options': 'Stock Ledger Entry',
'width': 120
}, {
'label': _('Posting Date'),
'fieldtype': 'Date',
'fieldname': 'posting_date',
'width': 110
}, {
'label': _('Voucher Type'),
'fieldtype': 'Link',
'fieldname': 'voucher_type',
'options': 'DocType',
'width': 120
}, {
'label': _('Voucher No'),
'fieldtype': 'Dynamic Link',
'fieldname': 'voucher_no',
'options': 'voucher_type',
'width': 120
}, {
'label': _('Item Code'),
'fieldtype': 'Link',
'fieldname': 'item_code',
'options': 'Item',
'width': 120
}, {
'label': _('Warehouse'),
'fieldtype': 'Link',
'fieldname': 'warehouse',
'options': 'Warehouse',
'width': 120
}, {
'label': _('Expected Balance Qty'),
'fieldtype': 'Float',
'fieldname': 'expected_balance_qty',
'width': 170
}, {
'label': _('Actual Balance Qty'),
'fieldtype': 'Float',
'fieldname': 'qty_after_transaction',
'width': 150
}, {
'label': _('Difference'),
'fieldtype': 'Float',
'fieldname': 'differnce',
'width': 110
}]

View File

@ -0,0 +1,35 @@
// Copyright (c) 2016, Frappe Technologies Pvt. Ltd. and contributors
// For license information, please see license.txt
/* eslint-disable */
frappe.query_reports["Incorrect Serial No Valuation"] = {
"filters": [
{
label: __('Item Code'),
fieldtype: 'Link',
fieldname: 'item_code',
options: 'Item',
get_query: function() {
return {
filters: {
'has_serial_no': 1
}
}
}
},
{
label: __('From Date'),
fieldtype: 'Date',
fieldname: 'from_date',
reqd: 1,
default: frappe.defaults.get_user_default("year_start_date")
},
{
label: __('To Date'),
fieldtype: 'Date',
fieldname: 'to_date',
reqd: 1,
default: frappe.defaults.get_user_default("year_end_date")
}
]
};

View File

@ -0,0 +1,36 @@
{
"add_total_row": 0,
"columns": [],
"creation": "2021-05-13 13:07:00.767845",
"disable_prepared_report": 0,
"disabled": 0,
"docstatus": 0,
"doctype": "Report",
"filters": [],
"idx": 0,
"is_standard": "Yes",
"json": "{}",
"modified": "2021-05-13 13:07:00.767845",
"modified_by": "Administrator",
"module": "Stock",
"name": "Incorrect Serial No Valuation",
"owner": "Administrator",
"prepared_report": 0,
"ref_doctype": "Stock Ledger Entry",
"report_name": "Incorrect Serial No Valuation",
"report_type": "Script Report",
"roles": [
{
"role": "Stock User"
},
{
"role": "Accounts Manager"
},
{
"role": "Accounts User"
},
{
"role": "Stock Manager"
}
]
}

View File

@ -0,0 +1,148 @@
# Copyright (c) 2013, Frappe Technologies Pvt. Ltd. and contributors
# For license information, please see license.txt
import frappe
import copy
from frappe import _
from six import iteritems
from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos
def execute(filters=None):
columns, data = [], []
columns = get_columns()
data = get_data(filters)
return columns, data
def get_data(filters):
data = get_stock_ledger_entries(filters)
serial_nos_data = prepare_serial_nos(data)
data = get_incorrect_serial_nos(serial_nos_data)
return data
def prepare_serial_nos(data):
serial_no_wise_data = {}
for row in data:
if not row.serial_nos:
continue
for serial_no in get_serial_nos(row.serial_nos):
sle = copy.deepcopy(row)
sle.serial_no = serial_no
sle.qty = 1 if sle.actual_qty > 0 else -1
sle.valuation_rate = sle.valuation_rate if sle.actual_qty > 0 else sle.valuation_rate * -1
serial_no_wise_data.setdefault(serial_no, []).append(sle)
return serial_no_wise_data
def get_incorrect_serial_nos(serial_nos_data):
result = []
total_value = frappe._dict({'qty': 0, 'valuation_rate': 0, 'serial_no': frappe.bold(_('Balance'))})
for serial_no, data in iteritems(serial_nos_data):
total_dict = frappe._dict({'qty': 0, 'valuation_rate': 0, 'serial_no': frappe.bold(_('Total'))})
if check_incorrect_serial_data(data, total_dict):
result.extend(data)
total_value.qty += total_dict.qty
total_value.valuation_rate += total_dict.valuation_rate
result.append(total_dict)
result.append({})
result.append(total_value)
return result
def check_incorrect_serial_data(data, total_dict):
incorrect_data = False
for row in data:
total_dict.qty += row.qty
total_dict.valuation_rate += row.valuation_rate
if ((total_dict.qty == 0 and abs(total_dict.valuation_rate) > 0) or total_dict.qty < 0):
incorrect_data = True
return incorrect_data
def get_stock_ledger_entries(report_filters):
fields = ['name', 'voucher_type', 'voucher_no', 'item_code', 'serial_no as serial_nos', 'actual_qty',
'posting_date', 'posting_time', 'company', 'warehouse', '(stock_value_difference / actual_qty) as valuation_rate']
filters = {'serial_no': ("is", "set")}
if report_filters.get('item_code'):
filters['item_code'] = report_filters.get('item_code')
if report_filters.get('from_date') and report_filters.get('to_date'):
filters['posting_date'] = ('between', [report_filters.get('from_date'), report_filters.get('to_date')])
return frappe.get_all('Stock Ledger Entry', fields = fields, filters = filters,
order_by = 'timestamp(posting_date, posting_time) asc, creation asc')
def get_columns():
return [{
'label': _('Company'),
'fieldtype': 'Link',
'fieldname': 'company',
'options': 'Company',
'width': 120
}, {
'label': _('Id'),
'fieldtype': 'Link',
'fieldname': 'name',
'options': 'Stock Ledger Entry',
'width': 120
}, {
'label': _('Posting Date'),
'fieldtype': 'Date',
'fieldname': 'posting_date',
'width': 90
}, {
'label': _('Posting Time'),
'fieldtype': 'Time',
'fieldname': 'posting_time',
'width': 90
}, {
'label': _('Voucher Type'),
'fieldtype': 'Link',
'fieldname': 'voucher_type',
'options': 'DocType',
'width': 100
}, {
'label': _('Voucher No'),
'fieldtype': 'Dynamic Link',
'fieldname': 'voucher_no',
'options': 'voucher_type',
'width': 110
}, {
'label': _('Item Code'),
'fieldtype': 'Link',
'fieldname': 'item_code',
'options': 'Item',
'width': 120
}, {
'label': _('Warehouse'),
'fieldtype': 'Link',
'fieldname': 'warehouse',
'options': 'Warehouse',
'width': 120
}, {
'label': _('Serial No'),
'fieldtype': 'Link',
'fieldname': 'serial_no',
'options': 'Serial No',
'width': 100
}, {
'label': _('Qty'),
'fieldtype': 'Float',
'fieldname': 'qty',
'width': 80
}, {
'label': _('Valuation Rate (In / Out)'),
'fieldtype': 'Currency',
'fieldname': 'valuation_rate',
'width': 110
}]

View File

@ -22,6 +22,7 @@ _exceptions = frappe.local('stockledger_exceptions')
# _exceptions = [] # _exceptions = []
def make_sl_entries(sl_entries, allow_negative_stock=False, via_landed_cost_voucher=False): def make_sl_entries(sl_entries, allow_negative_stock=False, via_landed_cost_voucher=False):
from erpnext.controllers.stock_controller import future_sle_exists
if sl_entries: if sl_entries:
from erpnext.stock.utils import update_bin from erpnext.stock.utils import update_bin
@ -30,6 +31,9 @@ def make_sl_entries(sl_entries, allow_negative_stock=False, via_landed_cost_vouc
validate_cancellation(sl_entries) validate_cancellation(sl_entries)
set_as_cancel(sl_entries[0].get('voucher_type'), sl_entries[0].get('voucher_no')) set_as_cancel(sl_entries[0].get('voucher_type'), sl_entries[0].get('voucher_no'))
args = get_args_for_future_sle(sl_entries[0])
future_sle_exists(args, sl_entries)
for sle in sl_entries: for sle in sl_entries:
if sle.serial_no: if sle.serial_no:
validate_serial_no(sle) validate_serial_no(sle)
@ -53,6 +57,14 @@ def make_sl_entries(sl_entries, allow_negative_stock=False, via_landed_cost_vouc
args = sle_doc.as_dict() args = sle_doc.as_dict()
update_bin(args, allow_negative_stock, via_landed_cost_voucher) update_bin(args, allow_negative_stock, via_landed_cost_voucher)
def get_args_for_future_sle(row):
return frappe._dict({
'voucher_type': row.get('voucher_type'),
'voucher_no': row.get('voucher_no'),
'posting_date': row.get('posting_date'),
'posting_time': row.get('posting_time')
})
def validate_serial_no(sle): def validate_serial_no(sle):
from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos
for sn in get_serial_nos(sle.serial_no): for sn in get_serial_nos(sle.serial_no):
@ -472,7 +484,7 @@ class update_entries_after(object):
frappe.db.set_value("Purchase Receipt Item Supplied", sle.voucher_detail_no, "rate", outgoing_rate) frappe.db.set_value("Purchase Receipt Item Supplied", sle.voucher_detail_no, "rate", outgoing_rate)
# Recalculate subcontracted item's rate in case of subcontracted purchase receipt/invoice # Recalculate subcontracted item's rate in case of subcontracted purchase receipt/invoice
if frappe.db.get_value(sle.voucher_type, sle.voucher_no, "is_subcontracted"): if frappe.get_cached_value(sle.voucher_type, sle.voucher_no, "is_subcontracted") == 'Yes':
doc = frappe.get_doc(sle.voucher_type, sle.voucher_no) doc = frappe.get_doc(sle.voucher_type, sle.voucher_no)
doc.update_valuation_rate(reset_outgoing_rate=False) doc.update_valuation_rate(reset_outgoing_rate=False)
for d in (doc.items + doc.supplied_items): for d in (doc.items + doc.supplied_items):

View File

@ -177,7 +177,7 @@ def get_bin(item_code, warehouse):
return bin_obj return bin_obj
def update_bin(args, allow_negative_stock=False, via_landed_cost_voucher=False): def update_bin(args, allow_negative_stock=False, via_landed_cost_voucher=False):
is_stock_item = frappe.db.get_value('Item', args.get("item_code"), 'is_stock_item') is_stock_item = frappe.get_cached_value('Item', args.get("item_code"), 'is_stock_item')
if is_stock_item: if is_stock_item:
bin = get_bin(args.get("item_code"), args.get("warehouse")) bin = get_bin(args.get("item_code"), args.get("warehouse"))
bin.update_stock(args, allow_negative_stock, via_landed_cost_voucher) bin.update_stock(args, allow_negative_stock, via_landed_cost_voucher)

View File

@ -15,6 +15,7 @@
"hide_custom": 0, "hide_custom": 0,
"icon": "stock", "icon": "stock",
"idx": 0, "idx": 0,
"is_default": 0,
"is_standard": 1, "is_standard": 1,
"label": "Stock", "label": "Stock",
"links": [ "links": [
@ -653,9 +654,44 @@
"link_type": "Report", "link_type": "Report",
"onboard": 0, "onboard": 0,
"type": "Link" "type": "Link"
},
{
"hidden": 0,
"is_query_report": 0,
"label": "Incorrect Data Report",
"link_type": "DocType",
"onboard": 0,
"type": "Card Break"
},
{
"hidden": 0,
"is_query_report": 0,
"label": "Incorrect Serial No Qty and Valuation",
"link_to": "Incorrect Serial No Valuation",
"link_type": "Report",
"onboard": 0,
"type": "Link"
},
{
"hidden": 0,
"is_query_report": 0,
"label": "Incorrect Balance Qty After Transaction",
"link_to": "Incorrect Balance Qty After Transaction",
"link_type": "Report",
"onboard": 0,
"type": "Link"
},
{
"hidden": 0,
"is_query_report": 0,
"label": "Stock and Account Value Comparison",
"link_to": "Stock and Account Value Comparison",
"link_type": "Report",
"onboard": 0,
"type": "Link"
} }
], ],
"modified": "2020-12-01 13:38:36.282890", "modified": "2021-05-13 13:10:24.914983",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Stock", "module": "Stock",
"name": "Stock", "name": "Stock",

View File

@ -166,7 +166,7 @@
"options": "Service Level Agreement" "options": "Service Level Agreement"
}, },
{ {
"depends_on": "eval: doc.status != 'Replied';", "depends_on": "eval: doc.status != 'Replied' && doc.service_level_agreement;",
"fieldname": "response_by", "fieldname": "response_by",
"fieldtype": "Datetime", "fieldtype": "Datetime",
"label": "Response By", "label": "Response By",
@ -180,7 +180,7 @@
"read_only": 1 "read_only": 1
}, },
{ {
"depends_on": "eval: doc.status != 'Replied';", "depends_on": "eval: doc.status != 'Replied' && doc.service_level_agreement;",
"fieldname": "resolution_by", "fieldname": "resolution_by",
"fieldtype": "Datetime", "fieldtype": "Datetime",
"label": "Resolution By", "label": "Resolution By",
@ -410,7 +410,7 @@
"icon": "fa fa-ticket", "icon": "fa fa-ticket",
"idx": 7, "idx": 7,
"links": [], "links": [],
"modified": "2021-05-26 10:49:07.574769", "modified": "2021-06-10 03:22:27.098898",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Support", "module": "Support",
"name": "Issue", "name": "Issue",

View File

@ -29,6 +29,9 @@ class Issue(Document):
self.update_status() self.update_status()
self.set_lead_contact(self.raised_by) self.set_lead_contact(self.raised_by)
if not self.service_level_agreement:
self.reset_sla_fields()
def on_update(self): def on_update(self):
# Add a communication in the issue timeline # Add a communication in the issue timeline
if self.flags.create_communication and self.via_customer_portal: if self.flags.create_communication and self.via_customer_portal:
@ -54,6 +57,13 @@ class Issue(Document):
self.company = frappe.db.get_value("Lead", self.lead, "company") or \ self.company = frappe.db.get_value("Lead", self.lead, "company") or \
frappe.db.get_default("Company") frappe.db.get_default("Company")
def reset_sla_fields(self):
self.agreement_status = ""
self.response_by = ""
self.resolution_by = ""
self.response_by_variance = 0
self.resolution_by_variance = 0
def update_status(self): def update_status(self):
status = frappe.db.get_value("Issue", self.name, "status") status = frappe.db.get_value("Issue", self.name, "status")
if self.status != "Open" and status == "Open" and not self.first_responded_on: if self.status != "Open" and status == "Open" and not self.first_responded_on:

View File

@ -13,9 +13,11 @@
{{ doc.items_preview }} {{ doc.items_preview }}
</div> </div>
</div> </div>
<div class="col-sm-3 text-right bold"> {% if doc.get('grand_total') %}
{{ doc.get_formatted("grand_total") }} <div class="col-sm-3 text-right bold">
</div> {{ doc.get_formatted("grand_total") }}
</div>
{% endif %}
</div> </div>
<a class="transaction-item-link" href="/{{ pathname }}/{{ doc.name }}">Link</a> <a class="transaction-item-link" href="/{{ pathname }}/{{ doc.name }}">Link</a>
</div> </div>

View File

@ -0,0 +1,877 @@
from __future__ import unicode_literals
import frappe
import unittest
import copy
from frappe.utils import cint
from collections import defaultdict
from erpnext.stock.doctype.stock_entry.test_stock_entry import make_stock_entry
from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos
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, make_purchase_invoice, get_materials_from_supplier)
class TestSubcontracting(unittest.TestCase):
def setUp(self):
make_subcontract_items()
make_raw_materials()
make_bom_for_subcontracted_items()
def test_po_with_bom(self):
'''
- Set backflush based on BOM
- Create subcontracted PO for the item Subcontracted Item SA1 and add same item two times.
- Transfer the components from Stores to Supplier warehouse with batch no and serial nos.
- Create purchase receipt against the PO and check serial nos and batch no.
'''
set_backflush_based_on('BOM')
item_code = 'Subcontracted Item SA1'
items = [{'warehouse': '_Test Warehouse - _TC', 'item_code': item_code, 'qty': 5, 'rate': 100},
{'warehouse': '_Test Warehouse - _TC', 'item_code': item_code, 'qty': 6, 'rate': 100}]
rm_items = [{'item_code': 'Subcontracted SRM Item 1', 'qty': 5},
{'item_code': 'Subcontracted SRM Item 2', 'qty': 5},
{'item_code': 'Subcontracted SRM Item 3', 'qty': 5},
{'item_code': 'Subcontracted SRM Item 1', 'qty': 6},
{'item_code': 'Subcontracted SRM Item 2', 'qty': 6},
{'item_code': 'Subcontracted SRM Item 3', 'qty': 6}
]
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 if d.get('qty') == 5 else po.items[1].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_receipt(po.name)
pr1.submit()
for key, value in get_supplied_items(pr1).items():
transferred_detais = itemwise_details.get(key)
for field in ['qty', 'serial_no', 'batch_no']:
if value.get(field):
transfer, consumed = (transferred_detais.get(field), value.get(field))
if field == 'serial_no':
transfer, consumed = (sorted(transfer), sorted(consumed))
self.assertEqual(transfer, consumed)
def test_po_with_material_transfer(self):
'''
- Set backflush based on Material Transfer
- Create subcontracted PO for the item Subcontracted Item SA1 and Subcontracted Item SA5.
- Transfer the components from Stores to Supplier warehouse with batch no and serial nos.
- Transfer extra item Subcontracted SRM Item 4 for the subcontract item Subcontracted Item SA5.
- Create partial purchase receipt against the PO and check serial nos and batch no.
'''
set_backflush_based_on('Material Transferred for Subcontract')
items = [{'warehouse': '_Test Warehouse - _TC', 'item_code': 'Subcontracted Item SA1', 'qty': 5, 'rate': 100},
{'warehouse': '_Test Warehouse - _TC', 'item_code': 'Subcontracted Item SA5', 'qty': 6, 'rate': 100}]
rm_items = [{'item_code': 'Subcontracted SRM Item 1', 'qty': 5, 'main_item_code': 'Subcontracted Item SA1'},
{'item_code': 'Subcontracted SRM Item 2', 'qty': 5, 'main_item_code': 'Subcontracted Item SA1'},
{'item_code': 'Subcontracted SRM Item 3', 'qty': 5, 'main_item_code': 'Subcontracted Item SA1'},
{'item_code': 'Subcontracted SRM Item 5', 'qty': 6, 'main_item_code': 'Subcontracted Item SA5'},
{'item_code': 'Subcontracted SRM Item 4', 'qty': 6, 'main_item_code': 'Subcontracted Item SA5'}
]
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 if d.get('qty') == 5 else po.items[1].name
make_stock_transfer_entry(po_no = po.name,
rm_items=rm_items, itemwise_details=copy.deepcopy(itemwise_details))
pr1 = make_purchase_receipt(po.name)
pr1.remove(pr1.items[1])
pr1.submit()
for key, value in get_supplied_items(pr1).items():
transferred_detais = itemwise_details.get(key)
for field in ['qty', 'serial_no', 'batch_no']:
if value.get(field):
self.assertEqual(value.get(field), transferred_detais.get(field))
pr2 = make_purchase_receipt(po.name)
pr2.submit()
for key, value in get_supplied_items(pr2).items():
transferred_detais = itemwise_details.get(key)
for field in ['qty', 'serial_no', 'batch_no']:
if value.get(field):
self.assertEqual(value.get(field), transferred_detais.get(field))
def test_subcontract_with_same_components_different_fg(self):
'''
- Set backflush based on Material Transfer
- Create subcontracted PO for the item Subcontracted Item SA2 and Subcontracted Item SA3.
- Transfer the components from Stores to Supplier warehouse with serial nos.
- Transfer extra qty of components for the item Subcontracted Item SA2.
- Create partial purchase receipt against the PO and check serial nos and batch no.
'''
set_backflush_based_on('Material Transferred for Subcontract')
items = [{'warehouse': '_Test Warehouse - _TC', 'item_code': 'Subcontracted Item SA2', 'qty': 5, 'rate': 100},
{'warehouse': '_Test Warehouse - _TC', 'item_code': 'Subcontracted Item SA3', 'qty': 6, 'rate': 100}]
rm_items = [{'item_code': 'Subcontracted SRM Item 2', 'qty': 6, 'main_item_code': 'Subcontracted Item SA2'},
{'item_code': 'Subcontracted SRM Item 2', 'qty': 6, 'main_item_code': 'Subcontracted Item SA3'}
]
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 if d.get('qty') == 5 else po.items[1].name
make_stock_transfer_entry(po_no = po.name,
rm_items=rm_items, itemwise_details=copy.deepcopy(itemwise_details))
pr1 = make_purchase_receipt(po.name)
pr1.items[0].qty = 3
pr1.remove(pr1.items[1])
pr1.submit()
for key, value in get_supplied_items(pr1).items():
transferred_detais = itemwise_details.get(key)
self.assertEqual(value.qty, 4)
self.assertEqual(sorted(value.serial_no), sorted(transferred_detais.get('serial_no')[0:4]))
pr2 = make_purchase_receipt(po.name)
pr2.items[0].qty = 2
pr2.remove(pr2.items[1])
pr2.submit()
for key, value in get_supplied_items(pr2).items():
transferred_detais = itemwise_details.get(key)
self.assertEqual(value.qty, 2)
self.assertEqual(sorted(value.serial_no), sorted(transferred_detais.get('serial_no')[4:6]))
pr3 = make_purchase_receipt(po.name)
pr3.submit()
for key, value in get_supplied_items(pr3).items():
transferred_detais = itemwise_details.get(key)
self.assertEqual(value.qty, 6)
self.assertEqual(sorted(value.serial_no), sorted(transferred_detais.get('serial_no')[6:12]))
def test_return_non_consumed_materials(self):
'''
- Set backflush based on Material Transfer
- Create subcontracted PO for the item Subcontracted Item SA2.
- Transfer the components from Stores to Supplier warehouse with serial nos.
- Transfer extra qty of component for the subcontracted item Subcontracted Item SA2.
- Create purchase receipt for full qty against the PO and change the qty of raw material.
- After that return the non consumed material back to the store from supplier's warehouse.
'''
set_backflush_based_on('Material Transferred for Subcontract')
items = [{'warehouse': '_Test Warehouse - _TC', 'item_code': 'Subcontracted Item SA2', 'qty': 5, 'rate': 100}]
rm_items = [{'item_code': 'Subcontracted SRM Item 2', 'qty': 6, 'main_item_code': 'Subcontracted Item SA2'}]
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,
rm_items=rm_items, itemwise_details=copy.deepcopy(itemwise_details))
pr1 = make_purchase_receipt(po.name)
pr1.save()
pr1.supplied_items[0].consumed_qty = 5
pr1.supplied_items[0].serial_no = '\n'.join(sorted(
itemwise_details.get('Subcontracted SRM Item 2').get('serial_no')[0:5]
))
pr1.submit()
for key, value in get_supplied_items(pr1).items():
transferred_detais = itemwise_details.get(key)
self.assertEqual(value.qty, 5)
self.assertEqual(sorted(value.serial_no), sorted(transferred_detais.get('serial_no')[0:5]))
po.load_from_db()
self.assertEqual(po.supplied_items[0].consumed_qty, 5)
doc = get_materials_from_supplier(po.name, [d.name for d in po.supplied_items])
self.assertEqual(doc.items[0].qty, 1)
self.assertEqual(doc.items[0].s_warehouse, '_Test Warehouse 1 - _TC')
self.assertEqual(doc.items[0].t_warehouse, '_Test Warehouse - _TC')
self.assertEqual(get_serial_nos(doc.items[0].serial_no),
itemwise_details.get(doc.items[0].item_code)['serial_no'][5:6])
def test_item_with_batch_based_on_bom(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_receipt(po.name)
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_receipt(po.name)
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_receipt(po.name)
pr1.items[0].qty = 2
pr1.save()
pr1.submit()
for key, value in get_supplied_items(pr1).items():
self.assertEqual(value.qty, 2)
def test_item_with_batch_based_on_material_transfer(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_receipt(po.name)
pr1.items[0].qty = 2
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_receipt(po.name)
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_receipt(po.name)
pr1.items[0].qty = 2
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(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_receipt(po.name)
pr1.items[0].qty = 5
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_receipt(po.name)
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_incorrect_serial_no_components_based_on_material_transfer(self):
'''
- Set backflush based on Material Transferred for Subcontract
- Create subcontracted PO for the item Subcontracted Item SA2.
- Transfer the serialized componenets to the supplier.
- Create purchase receipt and change the serial no which is not transferred.
- System should throw the error and not allowed to save the purchase receipt.
'''
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': 10}]
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_receipt(po.name)
pr1.save()
pr1.supplied_items[0].serial_no = 'ABCD'
self.assertRaises(frappe.ValidationError, pr1.save)
pr1.delete()
def test_partial_transfer_batch_based_on_material_transfer(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_receipt(po.name)
pr1.items[0].qty = 5
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_receipt(po.name)
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_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', 'expense_account', 'po_detail']:
item_dict[column] = pr.items[0].get(column)
pr.append('items', item_dict)
pr.set_missing_values()
def get_supplied_items(pr_doc):
supplied_items = {}
for row in pr_doc.get('supplied_items'):
if row.rm_item_code not in supplied_items:
supplied_items.setdefault(row.rm_item_code,
frappe._dict({'qty': 0, 'serial_no': [], 'batch_no': defaultdict(float)}))
details = supplied_items[row.rm_item_code]
update_item_details(row, details)
return supplied_items
def make_stock_in_entry(**args):
args = frappe._dict(args)
items = {}
for row in args.rm_items:
row = frappe._dict(row)
doc = make_stock_entry(target=row.warehouse or '_Test Warehouse - _TC',
item_code=row.item_code, qty=row.qty or 1, basic_rate=row.rate or 100)
if row.item_code not in items:
items.setdefault(row.item_code, frappe._dict({'qty': 0, 'serial_no': [], 'batch_no': defaultdict(float)}))
child_row = doc.items[0]
details = items[child_row.item_code]
update_item_details(child_row, details)
return items
def update_item_details(child_row, details):
details.qty += (child_row.get('qty') if child_row.doctype == 'Stock Entry Detail'
else child_row.get('consumed_qty'))
if child_row.serial_no:
details.serial_no.extend(get_serial_nos(child_row.serial_no))
if child_row.batch_no:
details.batch_no[child_row.batch_no] += (child_row.get('qty') or child_row.get('consumed_qty'))
def make_stock_transfer_entry(**args):
args = frappe._dict(args)
items = []
for row in args.rm_items:
row = frappe._dict(row)
item = {'item_code': row.main_item_code or args.main_item_code, 'rm_item_code': row.item_code,
'qty': row.qty or 1, 'item_name': row.item_code, 'rate': row.rate or 100,
'stock_uom': row.stock_uom or 'Nos', 'warehouse': row.warehuose or '_Test Warehouse - _TC'}
item_details = args.itemwise_details.get(row.item_code)
if item_details and item_details.serial_no:
serial_nos = item_details.serial_no[0:cint(row.qty)]
item['serial_no'] = '\n'.join(serial_nos)
item_details.serial_no = list(set(item_details.serial_no) - set(serial_nos))
if item_details and item_details.batch_no:
for batch_no, batch_qty in item_details.batch_no.items():
if batch_qty >= row.qty:
item['batch_no'] = batch_no
item_details.batch_no[batch_no] -= row.qty
break
items.append(item)
ste_dict = make_rm_stock_entry(args.po_no, items)
doc = frappe.get_doc(ste_dict)
doc.insert()
doc.submit()
return doc
def make_subcontract_items():
sub_contracted_items = {'Subcontracted Item SA1': {}, 'Subcontracted Item SA2': {}, 'Subcontracted Item SA3': {},
'Subcontracted Item SA4': {'has_batch_no': 1, 'create_new_batch': 1, 'batch_number_series': 'SBAT.####'},
'Subcontracted Item SA5': {}, 'Subcontracted Item SA6': {}}
for item, properties in sub_contracted_items.items():
if not frappe.db.exists('Item', item):
properties.update({'is_stock_item': 1, 'is_sub_contracted_item': 1})
make_item(item, properties)
def make_raw_materials():
raw_materials = {'Subcontracted SRM Item 1': {},
'Subcontracted SRM Item 2': {'has_serial_no': 1, 'serial_no_series': 'SRI.####'},
'Subcontracted SRM Item 3': {'has_batch_no': 1, 'create_new_batch': 1, 'batch_number_series': 'BAT.####'},
'Subcontracted SRM Item 4': {'has_serial_no': 1, 'serial_no_series': 'SRII.####'},
'Subcontracted SRM Item 5': {'has_serial_no': 1, 'serial_no_series': 'SRII.####'}}
for item, properties in raw_materials.items():
if not frappe.db.exists('Item', item):
properties.update({'is_stock_item': 1})
make_item(item, properties)
def make_bom_for_subcontracted_items():
boms = {
'Subcontracted Item SA1': ['Subcontracted SRM Item 1', 'Subcontracted SRM Item 2', 'Subcontracted SRM Item 3'],
'Subcontracted Item SA2': ['Subcontracted SRM Item 2'],
'Subcontracted Item SA3': ['Subcontracted SRM Item 2'],
'Subcontracted Item SA4': ['Subcontracted SRM Item 1', 'Subcontracted SRM Item 2', 'Subcontracted SRM Item 3'],
'Subcontracted Item SA5': ['Subcontracted SRM Item 5'],
'Subcontracted Item SA6': ['Subcontracted SRM Item 3']
}
for item_code, raw_materials in boms.items():
if not frappe.db.exists('BOM', {'item': item_code}):
make_bom(item=item_code, raw_materials=raw_materials, rate=100)
def set_backflush_based_on(based_on):
frappe.db.set_value('Buying Settings', None,
'backflush_raw_materials_of_subcontract_based_on', based_on)