Merge branch 'version-13-hotfix' of https://github.com/frappe/erpnext into payroll_accounting_dimension
This commit is contained in:
commit
3007c9900b
@ -260,7 +260,7 @@
|
||||
"description": "If enabled, ledger entries will be posted for change amount in POS transactions",
|
||||
"fieldname": "post_change_gl_entries",
|
||||
"fieldtype": "Check",
|
||||
"label": "Change Ledger Entries for Change Amount"
|
||||
"label": "Create Ledger Entries for Change Amount"
|
||||
}
|
||||
],
|
||||
"icon": "icon-cog",
|
||||
@ -268,7 +268,7 @@
|
||||
"index_web_pages_for_search": 1,
|
||||
"issingle": 1,
|
||||
"links": [],
|
||||
"modified": "2021-06-16 13:14:45.739107",
|
||||
"modified": "2021-06-17 20:26:03.721202",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Accounts Settings",
|
||||
|
@ -86,7 +86,7 @@ def resolve_dunning(doc, state):
|
||||
for reference in doc.references:
|
||||
if reference.reference_doctype == 'Sales Invoice' and reference.outstanding_amount <= 0:
|
||||
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:
|
||||
frappe.db.set_value("Dunning", dunning.name, "status", 'Resolved')
|
||||
@ -96,7 +96,7 @@ def calculate_interest_and_amount(posting_date, outstanding_amount, rate_of_inte
|
||||
grand_total = 0
|
||||
if rate_of_interest:
|
||||
interest_per_year = flt(outstanding_amount) * flt(rate_of_interest) / 100
|
||||
interest_amount = (interest_per_year * cint(overdue_days)) / 365
|
||||
interest_amount = (interest_per_year * cint(overdue_days)) / 365
|
||||
grand_total = flt(outstanding_amount) + flt(interest_amount) + flt(dunning_fee)
|
||||
dunning_amount = flt(interest_amount) + flt(dunning_fee)
|
||||
return {
|
||||
|
File diff suppressed because it is too large
Load Diff
@ -400,6 +400,7 @@ class PurchaseInvoice(BuyingController):
|
||||
# because updating ordered qty in bin depends upon updated ordered qty in PO
|
||||
if self.update_stock == 1:
|
||||
self.update_stock_ledger()
|
||||
self.set_consumed_qty_in_po()
|
||||
from erpnext.stock.doctype.serial_no.serial_no import update_serial_nos_after_submit
|
||||
update_serial_nos_after_submit(self, "items")
|
||||
|
||||
@ -998,6 +999,7 @@ class PurchaseInvoice(BuyingController):
|
||||
if self.update_stock == 1:
|
||||
self.update_stock_ledger()
|
||||
self.delete_auto_created_batches()
|
||||
self.set_consumed_qty_in_po()
|
||||
|
||||
self.make_gl_entries_on_cancel()
|
||||
|
||||
|
@ -621,8 +621,10 @@ class TestPurchaseInvoice(unittest.TestCase):
|
||||
self.assertEqual(actual_qty_0, get_qty_after_transaction())
|
||||
|
||||
def test_subcontracting_via_purchase_invoice(self):
|
||||
from erpnext.buying.doctype.purchase_order.test_purchase_order import update_backflush_based_on
|
||||
from erpnext.stock.doctype.stock_entry.test_stock_entry import make_stock_entry
|
||||
|
||||
update_backflush_based_on('BOM')
|
||||
make_stock_entry(item_code="_Test Item", target="_Test Warehouse 1 - _TC", qty=100, basic_rate=100)
|
||||
make_stock_entry(item_code="_Test Item Home Desktop 100", target="_Test Warehouse 1 - _TC",
|
||||
qty=100, basic_rate=100)
|
||||
|
@ -272,7 +272,7 @@
|
||||
"fieldname": "rate",
|
||||
"fieldtype": "Currency",
|
||||
"in_list_view": 1,
|
||||
"label": "Rate ",
|
||||
"label": "Rate",
|
||||
"oldfieldname": "import_rate",
|
||||
"oldfieldtype": "Currency",
|
||||
"options": "currency",
|
||||
@ -854,7 +854,7 @@
|
||||
"idx": 1,
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2021-03-30 09:02:39.256602",
|
||||
"modified": "2021-06-16 19:57:03.101571",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Purchase Invoice Item",
|
||||
|
@ -989,7 +989,7 @@ class SalesInvoice(SellingController):
|
||||
|
||||
for payment_mode in self.payments:
|
||||
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:
|
||||
# POS, make payment entries
|
||||
|
@ -53,6 +53,39 @@ frappe.ui.form.on("Purchase Order", {
|
||||
} else {
|
||||
frm.set_value("tax_withholding_category", frm.supplier_tds);
|
||||
}
|
||||
},
|
||||
|
||||
refresh: function(frm) {
|
||||
frm.trigger('get_materials_from_supplier');
|
||||
},
|
||||
|
||||
get_materials_from_supplier: function(frm) {
|
||||
let po_details = [];
|
||||
|
||||
if (frm.doc.supplied_items && (frm.doc.per_received == 100 || frm.doc.status === 'Closed')) {
|
||||
frm.doc.supplied_items.forEach(d => {
|
||||
if (d.total_supplied_qty && d.total_supplied_qty != d.consumed_qty) {
|
||||
po_details.push(d.name)
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (po_details && po_details.length) {
|
||||
frm.add_custom_button(__('Return of Components'), () => {
|
||||
frm.call({
|
||||
method: 'erpnext.buying.doctype.purchase_order.purchase_order.get_materials_from_supplier',
|
||||
freeze: true,
|
||||
freeze_message: __('Creating Stock Entry'),
|
||||
args: { purchase_order: frm.doc.name, po_details: po_details },
|
||||
callback: function(r) {
|
||||
if (r && r.message) {
|
||||
const doc = frappe.model.sync(r.message);
|
||||
frappe.set_route("Form", doc[0].doctype, doc[0].name);
|
||||
}
|
||||
}
|
||||
});
|
||||
}, __('Create'));
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@ -217,7 +250,7 @@ erpnext.buying.PurchaseOrderController = erpnext.buying.BuyingController.extend(
|
||||
},
|
||||
|
||||
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() {
|
||||
@ -513,12 +546,14 @@ erpnext.buying.PurchaseOrderController = erpnext.buying.BuyingController.extend(
|
||||
],
|
||||
primary_action: function() {
|
||||
var data = d.get_values();
|
||||
let reason_for_hold = 'Reason for hold: ' + data.reason_for_hold;
|
||||
|
||||
frappe.call({
|
||||
method: "frappe.desk.form.utils.add_comment",
|
||||
args: {
|
||||
reference_doctype: me.frm.doctype,
|
||||
reference_name: me.frm.docname,
|
||||
content: __('Reason for hold:') + " " +data.reason_for_hold,
|
||||
content: __(reason_for_hold),
|
||||
comment_email: frappe.session.user,
|
||||
comment_by: frappe.session.user_fullname
|
||||
},
|
||||
|
@ -609,6 +609,7 @@
|
||||
"fieldname": "supplied_items",
|
||||
"fieldtype": "Table",
|
||||
"label": "Supplied Items",
|
||||
"no_copy": 1,
|
||||
"oldfieldname": "po_raw_material_details",
|
||||
"oldfieldtype": "Table",
|
||||
"options": "Purchase Order Item Supplied",
|
||||
@ -1377,7 +1378,7 @@
|
||||
"idx": 105,
|
||||
"is_submittable": 1,
|
||||
"links": [],
|
||||
"modified": "2021-04-19 00:55:30.781375",
|
||||
"modified": "2021-05-30 15:17:53.663648",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Buying",
|
||||
"name": "Purchase Order",
|
||||
|
@ -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.stock.utils import get_bin
|
||||
from erpnext.accounts.party import get_party_account_currency
|
||||
from six import string_types
|
||||
from erpnext.stock.doctype.item.item import get_item_defaults
|
||||
from erpnext.setup.doctype.item_group.item_group import get_item_group_defaults
|
||||
from erpnext.accounts.doctype.tax_withholding_category.tax_withholding_category import get_party_tax_withholding_details
|
||||
from erpnext.accounts.doctype.sales_invoice.sales_invoice import validate_inter_company_party, update_linked_doc,\
|
||||
unlink_inter_company_doc
|
||||
from erpnext.accounts.doctype.sales_invoice.sales_invoice import (validate_inter_company_party,
|
||||
update_linked_doc, unlink_inter_company_doc)
|
||||
|
||||
form_grid_templates = {
|
||||
"items": "templates/form_grid/item_grid.html"
|
||||
@ -503,9 +502,11 @@ def get_mapped_purchase_invoice(source_name, target_doc=None, ignore_permissions
|
||||
|
||||
@frappe.whitelist()
|
||||
def make_rm_stock_entry(purchase_order, rm_items):
|
||||
if isinstance(rm_items, string_types):
|
||||
rm_items_list = rm_items
|
||||
|
||||
if isinstance(rm_items, str):
|
||||
rm_items_list = json.loads(rm_items)
|
||||
else:
|
||||
elif not rm_items:
|
||||
frappe.throw(_("No Items available for transfer"))
|
||||
|
||||
if rm_items_list:
|
||||
@ -543,6 +544,8 @@ def make_rm_stock_entry(purchase_order, rm_items):
|
||||
'qty': rm_item_data["qty"],
|
||||
'from_warehouse': rm_item_data["warehouse"],
|
||||
'stock_uom': rm_item_data["stock_uom"],
|
||||
'serial_no': rm_item_data.get('serial_no'),
|
||||
'batch_no': rm_item_data.get('batch_no'),
|
||||
'main_item_code': rm_item_data["item_code"],
|
||||
'allow_alternative_item': item_wh.get(rm_item_code, {}).get('allow_alternative_item')
|
||||
}
|
||||
@ -582,3 +585,58 @@ def update_status(status, name):
|
||||
def make_inter_company_sales_order(source_name, target_doc=None):
|
||||
from erpnext.accounts.doctype.sales_invoice.sales_invoice import make_inter_company_transaction
|
||||
return make_inter_company_transaction("Purchase Order", source_name, target_doc)
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_materials_from_supplier(purchase_order, po_details):
|
||||
if isinstance(po_details, str):
|
||||
po_details = json.loads(po_details)
|
||||
|
||||
doc = frappe.get_cached_doc('Purchase Order', purchase_order)
|
||||
doc.initialized_fields()
|
||||
doc.purchase_orders = [doc.name]
|
||||
doc.get_available_materials()
|
||||
|
||||
if not doc.available_materials:
|
||||
frappe.throw(_('Materials are already received against the purchase order {0}')
|
||||
.format(purchase_order))
|
||||
|
||||
return make_return_stock_entry_for_subcontract(doc.available_materials, doc, po_details)
|
||||
|
||||
def make_return_stock_entry_for_subcontract(available_materials, po_doc, po_details):
|
||||
ste_doc = frappe.new_doc('Stock Entry')
|
||||
ste_doc.purpose = 'Material Transfer'
|
||||
ste_doc.purchase_order = po_doc.name
|
||||
ste_doc.company = po_doc.company
|
||||
ste_doc.is_return = 1
|
||||
|
||||
for key, value in available_materials.items():
|
||||
if not value.qty:
|
||||
continue
|
||||
|
||||
if value.batch_no:
|
||||
for batch_no, qty in value.batch_no.items():
|
||||
if qty > 0:
|
||||
add_items_in_ste(ste_doc, value, value.qty, po_details, batch_no)
|
||||
else:
|
||||
add_items_in_ste(ste_doc, value, value.qty, po_details)
|
||||
|
||||
ste_doc.set_stock_entry_type()
|
||||
ste_doc.calculate_rate_and_amount()
|
||||
|
||||
return ste_doc
|
||||
|
||||
def add_items_in_ste(ste_doc, row, qty, po_details, batch_no=None):
|
||||
item = ste_doc.append('items', row.item_details)
|
||||
|
||||
po_detail = list(set(row.po_details).intersection(po_details))
|
||||
item.update({
|
||||
'qty': qty,
|
||||
'batch_no': batch_no,
|
||||
'basic_rate': row.item_details['rate'],
|
||||
'po_detail': po_detail[0] if po_detail else '',
|
||||
's_warehouse': row.item_details['t_warehouse'],
|
||||
't_warehouse': row.item_details['s_warehouse'],
|
||||
'item_code': row.item_details['rm_item_code'],
|
||||
'subcontracted_item': row.item_details['main_item_code'],
|
||||
'serial_no': '\n'.join(row.serial_no) if row.serial_no else ''
|
||||
})
|
@ -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.stock.doctype.batch.test_batch import make_new_batch
|
||||
from erpnext.controllers.buying_controller import get_backflushed_subcontracted_raw_materials
|
||||
|
||||
class TestPurchaseOrder(unittest.TestCase):
|
||||
def test_make_purchase_receipt(self):
|
||||
@ -771,7 +770,7 @@ class TestPurchaseOrder(unittest.TestCase):
|
||||
self.assertEqual(bin11.reserved_qty_for_sub_contract, bin1.reserved_qty_for_sub_contract)
|
||||
|
||||
def test_exploded_items_in_subcontracted(self):
|
||||
item_code = "_Test Subcontracted FG Item 1"
|
||||
item_code = "_Test Subcontracted FG Item 11"
|
||||
make_subcontracted_item(item_code=item_code)
|
||||
|
||||
po = create_purchase_order(item_code=item_code, qty=1,
|
||||
@ -848,79 +847,6 @@ class TestPurchaseOrder(unittest.TestCase):
|
||||
for item in rm_items:
|
||||
transferred_rm_map[item.get('rm_item_code')] = item
|
||||
|
||||
for item in pr.get('supplied_items'):
|
||||
self.assertEqual(item.get('required_qty'), (transferred_rm_map[item.get('rm_item_code')].get('qty') / order_qty) * received_qty)
|
||||
|
||||
update_backflush_based_on("BOM")
|
||||
|
||||
def test_backflushed_based_on_for_multiple_batches(self):
|
||||
item_code = "_Test Subcontracted FG Item 2"
|
||||
make_item('Sub Contracted Raw Material 2', {
|
||||
'is_stock_item': 1,
|
||||
'is_sub_contracted_item': 1
|
||||
})
|
||||
|
||||
make_subcontracted_item(item_code=item_code, has_batch_no=1, create_new_batch=1,
|
||||
raw_materials=["Sub Contracted Raw Material 2"])
|
||||
|
||||
update_backflush_based_on("Material Transferred for Subcontract")
|
||||
|
||||
order_qty = 500
|
||||
po = create_purchase_order(item_code=item_code, qty=order_qty,
|
||||
is_subcontracted="Yes", supplier_warehouse="_Test Warehouse 1 - _TC")
|
||||
|
||||
make_stock_entry(target="_Test Warehouse - _TC",
|
||||
item_code = "Sub Contracted Raw Material 2", qty=552, basic_rate=100)
|
||||
|
||||
rm_items = [
|
||||
{"item_code":item_code,"rm_item_code":"Sub Contracted Raw Material 2","item_name":"_Test Item",
|
||||
"qty":552,"warehouse":"_Test Warehouse - _TC", "stock_uom":"Nos"}]
|
||||
|
||||
rm_item_string = json.dumps(rm_items)
|
||||
se = frappe.get_doc(make_subcontract_transfer_entry(po.name, rm_item_string))
|
||||
se.submit()
|
||||
|
||||
for batch in ["ABCD1", "ABCD2", "ABCD3", "ABCD4"]:
|
||||
make_new_batch(batch_id=batch, item_code=item_code)
|
||||
|
||||
pr = make_purchase_receipt(po.name)
|
||||
|
||||
# partial receipt
|
||||
pr.get('items')[0].qty = 30
|
||||
pr.get('items')[0].batch_no = "ABCD1"
|
||||
|
||||
purchase_order = po.name
|
||||
purchase_order_item = po.items[0].name
|
||||
|
||||
for batch_no, qty in {"ABCD2": 60, "ABCD3": 70, "ABCD4":40}.items():
|
||||
pr.append("items", {
|
||||
"item_code": pr.get('items')[0].item_code,
|
||||
"item_name": pr.get('items')[0].item_name,
|
||||
"uom": pr.get('items')[0].uom,
|
||||
"stock_uom": pr.get('items')[0].stock_uom,
|
||||
"warehouse": pr.get('items')[0].warehouse,
|
||||
"conversion_factor": pr.get('items')[0].conversion_factor,
|
||||
"cost_center": pr.get('items')[0].cost_center,
|
||||
"rate": pr.get('items')[0].rate,
|
||||
"qty": qty,
|
||||
"batch_no": batch_no,
|
||||
"purchase_order": purchase_order,
|
||||
"purchase_order_item": purchase_order_item
|
||||
})
|
||||
|
||||
pr.submit()
|
||||
|
||||
pr1 = make_purchase_receipt(po.name)
|
||||
pr1.get('items')[0].qty = 300
|
||||
pr1.get('items')[0].batch_no = "ABCD1"
|
||||
pr1.save()
|
||||
|
||||
pr_key = ("Sub Contracted Raw Material 2", po.name)
|
||||
consumed_qty = get_backflushed_subcontracted_raw_materials([po.name]).get(pr_key)
|
||||
|
||||
self.assertTrue(pr1.supplied_items[0].consumed_qty > 0)
|
||||
self.assertTrue(pr1.supplied_items[0].consumed_qty, flt(552.0) - flt(consumed_qty))
|
||||
|
||||
update_backflush_based_on("BOM")
|
||||
|
||||
def test_supplied_qty_against_subcontracted_po(self):
|
||||
@ -1117,22 +1043,29 @@ def create_purchase_order(**args):
|
||||
po.conversion_factor = args.conversion_factor or 1
|
||||
po.supplier_warehouse = args.supplier_warehouse or None
|
||||
|
||||
po.append("items", {
|
||||
"item_code": args.item or args.item_code or "_Test Item",
|
||||
"warehouse": args.warehouse or "_Test Warehouse - _TC",
|
||||
"qty": args.qty or 10,
|
||||
"rate": args.rate or 500,
|
||||
"schedule_date": add_days(nowdate(), 1),
|
||||
"include_exploded_items": args.get('include_exploded_items', 1),
|
||||
"against_blanket_order": args.against_blanket_order
|
||||
})
|
||||
if args.rm_items:
|
||||
for row in args.rm_items:
|
||||
po.append("items", row)
|
||||
else:
|
||||
po.append("items", {
|
||||
"item_code": args.item or args.item_code or "_Test Item",
|
||||
"warehouse": args.warehouse or "_Test Warehouse - _TC",
|
||||
"qty": args.qty or 10,
|
||||
"rate": args.rate or 500,
|
||||
"schedule_date": add_days(nowdate(), 1),
|
||||
"include_exploded_items": args.get('include_exploded_items', 1),
|
||||
"against_blanket_order": args.against_blanket_order
|
||||
})
|
||||
|
||||
po.set_missing_values()
|
||||
if not args.do_not_save:
|
||||
po.insert()
|
||||
if not args.do_not_submit:
|
||||
if po.is_subcontracted == "Yes":
|
||||
supp_items = po.get("supplied_items")
|
||||
for d in supp_items:
|
||||
d.reserve_warehouse = args.warehouse or "_Test Warehouse - _TC"
|
||||
if not d.reserve_warehouse:
|
||||
d.reserve_warehouse = args.warehouse or "_Test Warehouse - _TC"
|
||||
po.submit()
|
||||
|
||||
return po
|
||||
|
@ -6,21 +6,25 @@
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"main_item_code",
|
||||
"bom_detail_no",
|
||||
"rm_item_code",
|
||||
"column_break_3",
|
||||
"stock_uom",
|
||||
"reserve_warehouse",
|
||||
"conversion_factor",
|
||||
"column_break_6",
|
||||
"rm_item_code",
|
||||
"bom_detail_no",
|
||||
"reference_name",
|
||||
"reserve_warehouse",
|
||||
"section_break2",
|
||||
"rate",
|
||||
"col_break2",
|
||||
"amount",
|
||||
"section_break1",
|
||||
"required_qty",
|
||||
"supplied_qty",
|
||||
"col_break1",
|
||||
"supplied_qty"
|
||||
"consumed_qty",
|
||||
"returned_qty",
|
||||
"total_supplied_qty"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
@ -125,6 +129,8 @@
|
||||
"fieldtype": "Float",
|
||||
"in_list_view": 1,
|
||||
"label": "Supplied Qty",
|
||||
"no_copy": 1,
|
||||
"print_hide": 1,
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
@ -142,13 +148,42 @@
|
||||
{
|
||||
"fieldname": "col_break2",
|
||||
"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,
|
||||
"idx": 1,
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2020-09-18 17:26:09.703215",
|
||||
"modified": "2021-06-09 15:17:58.128242",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Buying",
|
||||
"name": "Purchase Order Item Supplied",
|
||||
|
@ -6,10 +6,11 @@
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"main_item_code",
|
||||
"description",
|
||||
"rm_item_code",
|
||||
"item_name",
|
||||
"bom_detail_no",
|
||||
"col_break1",
|
||||
"rm_item_code",
|
||||
"description",
|
||||
"stock_uom",
|
||||
"conversion_factor",
|
||||
"reference_name",
|
||||
@ -25,7 +26,8 @@
|
||||
"secbreak_3",
|
||||
"batch_no",
|
||||
"col_break4",
|
||||
"serial_no"
|
||||
"serial_no",
|
||||
"purchase_order"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
@ -52,7 +54,6 @@
|
||||
"fieldname": "description",
|
||||
"fieldtype": "Text Editor",
|
||||
"in_global_search": 1,
|
||||
"in_list_view": 1,
|
||||
"label": "Description",
|
||||
"oldfieldname": "description",
|
||||
"oldfieldtype": "Data",
|
||||
@ -81,18 +82,20 @@
|
||||
"fieldname": "required_qty",
|
||||
"fieldtype": "Float",
|
||||
"in_list_view": 1,
|
||||
"label": "Required Qty",
|
||||
"label": "Available Qty For Consumption",
|
||||
"oldfieldname": "required_qty",
|
||||
"oldfieldtype": "Currency",
|
||||
"print_hide": 1,
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"columns": 2,
|
||||
"fieldname": "consumed_qty",
|
||||
"fieldtype": "Float",
|
||||
"label": "Consumed Qty",
|
||||
"in_list_view": 1,
|
||||
"label": "Qty to Be Consumed",
|
||||
"oldfieldname": "consumed_qty",
|
||||
"oldfieldtype": "Currency",
|
||||
"read_only": 1,
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
@ -183,12 +186,28 @@
|
||||
{
|
||||
"fieldname": "col_break4",
|
||||
"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,
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2020-09-18 17:26:09.703215",
|
||||
"modified": "2021-06-19 19:33:04.431213",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Buying",
|
||||
"name": "Purchase Receipt Item Supplied",
|
||||
|
@ -317,19 +317,21 @@ def add_items(sq_doc, supplier, items):
|
||||
create_rfq_items(sq_doc, supplier, data)
|
||||
|
||||
def create_rfq_items(sq_doc, supplier, data):
|
||||
sq_doc.append('items', {
|
||||
"item_code": data.item_code,
|
||||
"item_name": data.item_name,
|
||||
"description": data.description,
|
||||
"qty": data.qty,
|
||||
"rate": data.rate,
|
||||
"conversion_factor": data.conversion_factor if data.conversion_factor else None,
|
||||
"supplier_part_no": frappe.db.get_value("Item Supplier", {'parent': data.item_code, 'supplier': supplier}, "supplier_part_no"),
|
||||
"warehouse": data.warehouse or '',
|
||||
args = {}
|
||||
|
||||
for field in ['item_code', 'item_name', 'description', 'qty', 'rate', 'conversion_factor',
|
||||
'warehouse', 'material_request', 'material_request_item', 'stock_qty']:
|
||||
args[field] = data.get(field)
|
||||
|
||||
args.update({
|
||||
"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()
|
||||
def get_pdf(doctype, name, supplier):
|
||||
doc = get_rfq_doc(doctype, name, supplier)
|
||||
|
@ -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')
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
};
|
@ -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"
|
||||
}
|
||||
]
|
||||
}
|
@ -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
|
||||
}
|
||||
]
|
@ -11,16 +11,17 @@ from erpnext.accounts.party import get_party_details
|
||||
from erpnext.stock.get_item_details import get_conversion_factor
|
||||
from erpnext.buying.utils import validate_for_items, update_last_purchase_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 frappe.contacts.doctype.address.address import get_address_display
|
||||
|
||||
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.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):
|
||||
if self.get("supplier_name"):
|
||||
@ -57,6 +58,11 @@ class BuyingController(StockController):
|
||||
if self.doctype in ("Purchase Receipt", "Purchase Invoice"):
|
||||
self.update_valuation_rate()
|
||||
|
||||
def onload(self):
|
||||
super(BuyingController, self).onload()
|
||||
self.set_onload("backflush_based_on", frappe.db.get_single_value('Buying Settings',
|
||||
'backflush_raw_materials_of_subcontract_based_on'))
|
||||
|
||||
def set_missing_values(self, for_validate=False):
|
||||
super(BuyingController, self).set_missing_values(for_validate)
|
||||
|
||||
@ -171,12 +177,13 @@ class BuyingController(StockController):
|
||||
|
||||
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_qty, stock_and_asset_items_amount = 0, 0
|
||||
last_item_idx = 1
|
||||
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_amount += flt(d.base_net_amount)
|
||||
last_item_idx = d.idx
|
||||
@ -255,7 +262,7 @@ class BuyingController(StockController):
|
||||
supplied_items_cost = 0.0
|
||||
for d in self.get("supplied_items"):
|
||||
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({
|
||||
"item_code": d.rm_item_code,
|
||||
"warehouse": self.supplier_warehouse,
|
||||
@ -285,11 +292,13 @@ class BuyingController(StockController):
|
||||
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))
|
||||
|
||||
if self.doctype == "Purchase Order":
|
||||
for supplied_item in self.get("supplied_items"):
|
||||
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)))
|
||||
if self.doctype != "Purchase Order":
|
||||
return
|
||||
|
||||
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:
|
||||
for item in self.get("items"):
|
||||
if item.bom:
|
||||
@ -297,23 +306,7 @@ class BuyingController(StockController):
|
||||
|
||||
def create_raw_materials_supplied(self, raw_material_table):
|
||||
if self.is_subcontracted=="Yes":
|
||||
parent_items = []
|
||||
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)
|
||||
self.set_materials_for_subcontracted_items(raw_material_table)
|
||||
|
||||
elif self.doctype in ["Purchase Receipt", "Purchase Invoice"]:
|
||||
for item in self.get("items"):
|
||||
@ -322,176 +315,6 @@ class BuyingController(StockController):
|
||||
if self.is_subcontracted == "No" and self.get("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
|
||||
def sub_contracted_items(self):
|
||||
if not hasattr(self, "_sub_contracted_items"):
|
||||
@ -683,7 +506,8 @@ class BuyingController(StockController):
|
||||
self.process_fixed_asset()
|
||||
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):
|
||||
super(BuyingController, self).on_cancel()
|
||||
@ -691,7 +515,9 @@ class BuyingController(StockController):
|
||||
if self.get('is_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']:
|
||||
field = 'purchase_invoice' if self.doctype == 'Purchase Invoice' else 'purchase_receipt'
|
||||
|
||||
@ -863,104 +689,6 @@ class BuyingController(StockController):
|
||||
else:
|
||||
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):
|
||||
asset_items_data = {}
|
||||
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)
|
||||
|
||||
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
|
||||
|
@ -19,7 +19,7 @@ def employee_query(doctype, txt, searchfield, start, page_len, filters):
|
||||
fields = get_fields("Employee", ["name", "employee_name"])
|
||||
|
||||
return frappe.db.sql("""select {fields} from `tabEmployee`
|
||||
where status = 'Active'
|
||||
where status in ('Active', 'Suspended')
|
||||
and docstatus < 2
|
||||
and ({key} 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`
|
||||
where
|
||||
`tabProject`.status not in ("Completed", "Cancelled")
|
||||
and {cond} {match_cond} {scond}
|
||||
and {cond} {scond} {match_cond}
|
||||
order by
|
||||
if(locate(%(_txt)s, name), locate(%(_txt)s, name), 99999),
|
||||
idx desc,
|
||||
|
@ -501,7 +501,6 @@ class StockController(AccountsController):
|
||||
check_if_stock_and_account_balance_synced(self.posting_date,
|
||||
self.company, self.doctype, self.name)
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def make_quality_inspections(doctype, docname, items):
|
||||
if isinstance(items, str):
|
||||
@ -533,21 +532,75 @@ def make_quality_inspections(doctype, docname, items):
|
||||
|
||||
return inspections
|
||||
|
||||
|
||||
def is_reposting_pending():
|
||||
return frappe.db.exists("Repost Item Valuation",
|
||||
{'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):
|
||||
sl_entries = frappe.get_all("Stock Ledger Entry",
|
||||
if validate_future_sle_not_exists(args, key, sl_entries):
|
||||
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},
|
||||
fields=["item_code", "warehouse"],
|
||||
order_by="creation asc")
|
||||
|
||||
if not sl_entries:
|
||||
return
|
||||
|
||||
def get_conditions_to_validate_future_sle(sl_entries):
|
||||
warehouse_items_map = {}
|
||||
for entry in sl_entries:
|
||||
if entry.warehouse not in warehouse_items_map:
|
||||
@ -561,17 +614,7 @@ def future_sle_exists(args):
|
||||
f"""warehouse = {frappe.db.escape(warehouse)}
|
||||
and item_code in ({', '.join(frappe.db.escape(item) for item in items)})""")
|
||||
|
||||
return frappe.db.sql("""
|
||||
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)
|
||||
return or_conditions
|
||||
|
||||
def create_repost_item_valuation_entry(args):
|
||||
args = frappe._dict(args)
|
||||
|
393
erpnext/controllers/subcontracting.py
Normal file
393
erpnext/controllers/subcontracting.py
Normal 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"))
|
@ -658,7 +658,13 @@ class calculate_taxes_and_totals(object):
|
||||
item.margin_type = None
|
||||
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
|
||||
rate_with_margin = flt(item.price_list_rate) + flt(margin_value)
|
||||
base_rate_with_margin = flt(rate_with_margin) * flt(self.doc.conversion_rate)
|
||||
|
@ -207,7 +207,7 @@
|
||||
"label": "Status",
|
||||
"oldfieldname": "status",
|
||||
"oldfieldtype": "Select",
|
||||
"options": "Active\nInactive\nLeft",
|
||||
"options": "Active\nInactive\nSuspended\nLeft",
|
||||
"reqd": 1,
|
||||
"search_index": 1
|
||||
},
|
||||
@ -813,7 +813,7 @@
|
||||
"idx": 24,
|
||||
"image_field": "image",
|
||||
"links": [],
|
||||
"modified": "2021-06-12 11:31:37.730760",
|
||||
"modified": "2021-06-17 11:31:37.730760",
|
||||
"modified_by": "Administrator",
|
||||
"module": "HR",
|
||||
"name": "Employee",
|
||||
|
@ -4,7 +4,7 @@
|
||||
from __future__ import unicode_literals
|
||||
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 import throw, _, scrub
|
||||
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 erpnext.utilities.transaction_base import delete_events
|
||||
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 EmployeeLeftValidationError(frappe.ValidationError): pass
|
||||
@ -37,7 +36,7 @@ class Employee(NestedSet):
|
||||
|
||||
def validate(self):
|
||||
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.set_employee_name()
|
||||
|
@ -7,7 +7,8 @@ def get_data():
|
||||
'heatmap_message': _('This is based on the attendance of this Employee'),
|
||||
'fieldname': 'employee',
|
||||
'non_standard_fieldnames': {
|
||||
'Bank Account': 'party'
|
||||
'Bank Account': 'party',
|
||||
'Employee Grievance': 'raised_by'
|
||||
},
|
||||
'transactions': [
|
||||
{
|
||||
@ -20,7 +21,7 @@ def get_data():
|
||||
},
|
||||
{
|
||||
'label': _('Lifecycle'),
|
||||
'items': ['Employee Transfer', 'Employee Promotion', 'Employee Separation']
|
||||
'items': ['Employee Transfer', 'Employee Promotion', 'Employee Separation', 'Employee Grievance']
|
||||
},
|
||||
{
|
||||
'label': _('Shift'),
|
||||
|
@ -3,7 +3,7 @@ frappe.listview_settings['Employee'] = {
|
||||
filters: [["status","=", "Active"]],
|
||||
get_indicator: function(doc) {
|
||||
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;
|
||||
}
|
||||
};
|
||||
|
0
erpnext/hr/doctype/employee_grievance/__init__.py
Normal file
0
erpnext/hr/doctype/employee_grievance/__init__.py
Normal file
39
erpnext/hr/doctype/employee_grievance/employee_grievance.js
Normal file
39
erpnext/hr/doctype/employee_grievance/employee_grievance.js
Normal 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
|
||||
};
|
||||
});
|
||||
},
|
||||
});
|
261
erpnext/hr/doctype/employee_grievance/employee_grievance.json
Normal file
261
erpnext/hr/doctype/employee_grievance/employee_grievance.json
Normal 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
|
||||
}
|
15
erpnext/hr/doctype/employee_grievance/employee_grievance.py
Normal file
15
erpnext/hr/doctype/employee_grievance/employee_grievance.py
Normal 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"))
|
||||
)
|
||||
|
@ -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];
|
||||
}
|
||||
};
|
@ -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
|
||||
|
0
erpnext/hr/doctype/grievance_type/__init__.py
Normal file
0
erpnext/hr/doctype/grievance_type/__init__.py
Normal file
8
erpnext/hr/doctype/grievance_type/grievance_type.js
Normal file
8
erpnext/hr/doctype/grievance_type/grievance_type.js
Normal 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) {
|
||||
|
||||
// }
|
||||
});
|
70
erpnext/hr/doctype/grievance_type/grievance_type.json
Normal file
70
erpnext/hr/doctype/grievance_type/grievance_type.json
Normal 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"
|
||||
}
|
8
erpnext/hr/doctype/grievance_type/grievance_type.py
Normal file
8
erpnext/hr/doctype/grievance_type/grievance_type.py
Normal 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
|
8
erpnext/hr/doctype/grievance_type/test_grievance_type.py
Normal file
8
erpnext/hr/doctype/grievance_type/test_grievance_type.py
Normal 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
|
@ -2,7 +2,7 @@
|
||||
// MIT License. See license.txt
|
||||
|
||||
frappe.listview_settings['Job Applicant'] = {
|
||||
add_fields: ["company", "designation", "job_applicant", "status"],
|
||||
add_fields: ["status"],
|
||||
get_indicator: function (doc) {
|
||||
if (doc.status == "Accepted") {
|
||||
return [__(doc.status), "green", "status,=," + doc.status];
|
||||
|
@ -41,7 +41,7 @@ class StaffingPlan(Document):
|
||||
|
||||
detail.total_estimated_cost = 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)
|
||||
|
||||
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 \
|
||||
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} \
|
||||
for {2} as per staffing plan {3} for parent company {4}."
|
||||
.format(cint(parent_plan_details[0].vacancies),
|
||||
for {2} as per staffing plan {3} for parent company {4}.").format(
|
||||
cint(parent_plan_details[0].vacancies),
|
||||
parent_plan_details[0].total_estimated_cost,
|
||||
frappe.bold(staffing_plan_detail.designation),
|
||||
parent_plan_details[0].name,
|
||||
parent_company)), ParentCompanyError)
|
||||
parent_company), ParentCompanyError)
|
||||
|
||||
#Get vacanices already planned for all companies down the hierarchy of Parent Company
|
||||
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(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}. \
|
||||
You can only plan for upto {4} vacancies and and budget {5} as per staffing plan {6} for parent company {3}."
|
||||
.format(cint(all_sibling_details.vacancies),
|
||||
You can only plan for upto {4} vacancies and and budget {5} as per staffing plan {6} for parent company {3}.").format(
|
||||
cint(all_sibling_details.vacancies),
|
||||
all_sibling_details.total_estimated_cost,
|
||||
frappe.bold(staffing_plan_detail.designation),
|
||||
parent_company,
|
||||
cint(parent_plan_details[0].vacancies),
|
||||
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):
|
||||
#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 \
|
||||
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}. \
|
||||
Staffing Plan for {0} should allocate more vacancies and budget for {3} than planned for its subsidiary companies"
|
||||
.format(self.company,
|
||||
Staffing Plan for {0} should allocate more vacancies and budget for {3} than planned for its subsidiary companies").format(
|
||||
self.company,
|
||||
cint(children_details.vacancies),
|
||||
children_details.total_estimated_cost,
|
||||
frappe.bold(staffing_plan_detail.designation))), SubsidiaryCompanyError)
|
||||
frappe.bold(staffing_plan_detail.designation)), SubsidiaryCompanyError)
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_designation_counts(designation, company):
|
||||
@ -170,4 +170,4 @@ def get_active_staffing_plan_details(company, designation, from_date=getdate(now
|
||||
designation, from_date, to_date)
|
||||
|
||||
# Only a single staffing plan can be active for a designation on given date
|
||||
return staffing_plan if staffing_plan else None
|
||||
return staffing_plan if staffing_plan else None
|
||||
|
@ -11,8 +11,8 @@
|
||||
"event": "Submit",
|
||||
"idx": 0,
|
||||
"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>",
|
||||
"modified": "2021-05-24 16:29:13.165930",
|
||||
"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-06-16 14:08:12.933367",
|
||||
"modified_by": "Administrator",
|
||||
"module": "HR",
|
||||
"name": "Training Scheduled",
|
||||
|
@ -24,19 +24,19 @@
|
||||
{% set start = frappe.utils.get_datetime(doc.start_time) %}
|
||||
{% set end = frappe.utils.get_datetime(doc.end_time) %}
|
||||
{% if start.date() == end.date() %}
|
||||
<li>{{_("Date")}}: <b>{{ start.strftime("%A, %d %b %Y") }}</b></li>
|
||||
<li>
|
||||
{{_("Timing")}}: <b>{{ start.strftime("%I:%M %p") + ' to ' + end.strftime("%I:%M %p") }}</b>
|
||||
</li>
|
||||
<li>{{_("Date")}}: <b>{{ start.strftime("%A, %d %b %Y") }}</b></li>
|
||||
<li>
|
||||
{{_("Timing")}}: <b>{{ start.strftime("%I:%M %p") + ' to ' + end.strftime("%I:%M %p") }}</b>
|
||||
</li>
|
||||
{% else %}
|
||||
<li>{{_("Start Time")}}: <b>{{ start.strftime("%A, %d %b %Y at %I:%M %p") }}</b>
|
||||
</li>
|
||||
<li>{{_("End Time")}}: <b>{{ end.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>
|
||||
<li>{{_("End Time")}}: <b>{{ end.strftime("%A, %d %b %Y at %I:%M %p") }}</b></li>
|
||||
{% 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 %}
|
||||
<li>Note: This Training Event is mandatory</li>
|
||||
<li>{{ _("Note: This Training Event is mandatory") }}</li>
|
||||
{% endif %}
|
||||
</ul>
|
||||
</div>
|
||||
@ -44,4 +44,4 @@
|
||||
<td width="15"></td>
|
||||
</tr>
|
||||
<tr height="10"></tr>
|
||||
</table>
|
||||
</table>
|
||||
|
@ -153,6 +153,24 @@
|
||||
"onboard": 0,
|
||||
"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",
|
||||
"hidden": 0,
|
||||
@ -823,7 +841,7 @@
|
||||
"type": "Link"
|
||||
}
|
||||
],
|
||||
"modified": "2021-04-26 13:36:15.413819",
|
||||
"modified": "2021-05-13 17:19:40.524444",
|
||||
"modified_by": "Administrator",
|
||||
"module": "HR",
|
||||
"name": "HR",
|
||||
|
@ -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)
|
||||
|
||||
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)
|
||||
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):
|
||||
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)
|
||||
|
||||
def get_total_loan_amount(applicant_type, applicant, company):
|
||||
return frappe.db.get_value('Loan',
|
||||
{'applicant_type': applicant_type, 'company': company, 'applicant': applicant, 'docstatus': 1},
|
||||
'sum(loan_amount)')
|
||||
pending_amount = 0
|
||||
loan_details = frappe.db.get_all("Loan",
|
||||
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):
|
||||
return frappe.db.get_value('Sanctioned Loan Amount',
|
||||
|
@ -49,7 +49,11 @@ class TestLoan(unittest.TestCase):
|
||||
if not frappe.db.exists("Customer", "_Test Loan Customer"):
|
||||
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)
|
||||
|
||||
@ -125,6 +129,38 @@ class TestLoan(unittest.TestCase):
|
||||
self.assertTrue(gl_entries1)
|
||||
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):
|
||||
pledge = [{
|
||||
"loan_security": "Test Security 1",
|
||||
@ -367,7 +403,7 @@ class TestLoan(unittest.TestCase):
|
||||
unpledge_request.load_from_db()
|
||||
self.assertEqual(unpledge_request.docstatus, 1)
|
||||
|
||||
def test_santined_loan_security_unpledge(self):
|
||||
def test_sanctioned_loan_security_unpledge(self):
|
||||
pledge = [{
|
||||
"loan_security": "Test Security 1",
|
||||
"qty": 4000.00
|
||||
@ -858,7 +894,7 @@ def create_repayment_entry(loan, applicant, posting_date, paid_amount):
|
||||
return lr
|
||||
|
||||
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.applicant_type = 'Customer'
|
||||
loan_application.company = company
|
||||
@ -874,6 +910,9 @@ def create_loan_application(company, applicant, loan_type, proposed_pledges, rep
|
||||
for pledge in proposed_pledges:
|
||||
loan_application.append('proposed_pledges', pledge)
|
||||
|
||||
if do_not_save:
|
||||
return loan_application
|
||||
|
||||
loan_application.save()
|
||||
loan_application.submit()
|
||||
|
||||
|
@ -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))
|
||||
|
||||
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)
|
||||
|
||||
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):
|
||||
frappe.throw(_("Sanctioned Amount limit crossed for {0} {1}").format(self.applicant_type, frappe.bold(self.applicant)))
|
||||
|
||||
|
@ -235,70 +235,71 @@ class LoanRepayment(AccountsController):
|
||||
else:
|
||||
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(
|
||||
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,
|
||||
"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": _("Penalty against loan:") + self.against_loan,
|
||||
"remarks": remarks,
|
||||
"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,
|
||||
"account": loan_details.loan_account,
|
||||
"party_type": loan_details.applicant_type,
|
||||
"party": loan_details.applicant,
|
||||
"against": loan_details.payment_account,
|
||||
"credit": self.total_penalty_paid,
|
||||
"credit_in_account_currency": self.total_penalty_paid,
|
||||
"credit": self.amount_paid,
|
||||
"credit_in_account_currency": self.amount_paid,
|
||||
"against_voucher_type": "Loan",
|
||||
"against_voucher": self.against_loan,
|
||||
"remarks": _("Penalty against loan:") + 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.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)
|
||||
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,
|
||||
payment_type, interest_payable, payable_principal_amount, amount_paid, penalty_amount=None):
|
||||
|
@ -12,6 +12,7 @@ from erpnext.manufacturing.doctype.bom_update_tool.bom_update_tool import update
|
||||
from six import string_types
|
||||
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.tests.test_subcontracting import set_backflush_based_on
|
||||
|
||||
test_records = frappe.get_test_records('BOM')
|
||||
|
||||
@ -160,6 +161,7 @@ class TestBOM(unittest.TestCase):
|
||||
|
||||
def test_subcontractor_sourced_item(self):
|
||||
item_code = "_Test Subcontracted FG Item 1"
|
||||
set_backflush_based_on('Material Transferred for Subcontract')
|
||||
|
||||
if not frappe.db.exists('Item', item_code):
|
||||
make_item(item_code, {
|
||||
|
@ -306,8 +306,25 @@ frappe.ui.form.on('Production Plan', {
|
||||
},
|
||||
|
||||
download_materials_required: function(frm) {
|
||||
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 });
|
||||
const fields = [{
|
||||
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) {
|
||||
|
@ -98,7 +98,7 @@ class ProductionPlan(Document):
|
||||
def get_items(self):
|
||||
self.set('po_items', [])
|
||||
if self.get_items_from == "Sales Order":
|
||||
self.get_so_items()
|
||||
self.get_so_items()
|
||||
|
||||
elif self.get_items_from == "Material Request":
|
||||
self.get_mr_items()
|
||||
@ -170,11 +170,11 @@ class ProductionPlan(Document):
|
||||
refs = {}
|
||||
for data in items:
|
||||
item_details = get_item_details(data.item_code)
|
||||
if self.combine_items:
|
||||
if self.combine_items:
|
||||
if item_details.bom_no in refs:
|
||||
refs[item_details.bom_no]['so_details'].append({
|
||||
'sales_order': data.parent,
|
||||
'sales_order_item': data.name,
|
||||
'sales_order_item': data.name,
|
||||
'qty': data.pending_qty
|
||||
})
|
||||
refs[item_details.bom_no]['qty'] += data.pending_qty
|
||||
@ -188,10 +188,10 @@ class ProductionPlan(Document):
|
||||
}
|
||||
refs[item_details.bom_no]['so_details'].append({
|
||||
'sales_order': data.parent,
|
||||
'sales_order_item': data.name,
|
||||
'sales_order_item': data.name,
|
||||
'qty': data.pending_qty
|
||||
})
|
||||
|
||||
|
||||
pi = self.append('po_items', {
|
||||
'include_exploded_items': 1,
|
||||
'warehouse': data.warehouse,
|
||||
@ -209,12 +209,12 @@ class ProductionPlan(Document):
|
||||
pi.sales_order = data.parent
|
||||
pi.sales_order_item = data.name
|
||||
pi.description = data.description
|
||||
|
||||
|
||||
elif self.get_items_from == "Material Request":
|
||||
pi.material_request = data.parent
|
||||
pi.material_request_item = data.name
|
||||
pi.description = data.description
|
||||
|
||||
|
||||
if refs:
|
||||
for po_item in self.po_items:
|
||||
po_item.planned_qty = refs[po_item.bom_no]['qty']
|
||||
@ -477,18 +477,19 @@ class ProductionPlan(Document):
|
||||
msgprint(_("No material request created"))
|
||||
|
||||
@frappe.whitelist()
|
||||
def download_raw_materials(doc):
|
||||
def download_raw_materials(doc, warehouses=None):
|
||||
if isinstance(doc, string_types):
|
||||
doc = frappe._dict(json.loads(doc))
|
||||
|
||||
item_list = [['Item Code', 'Description', 'Stock UOM', 'Warehouse', 'Required Qty as per BOM',
|
||||
'Projected Qty', 'Actual Qty', 'Ordered Qty', 'Reserved Qty for Production',
|
||||
'Safety Stock', 'Required Qty']]
|
||||
'Projected Qty', 'Available Qty In Hand', 'Ordered Qty', 'Planned 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'),
|
||||
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'):
|
||||
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,
|
||||
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.purchase_uom, item_uom.conversion_factor
|
||||
item.purchase_uom, item_uom.conversion_factor, item.safety_stock
|
||||
from
|
||||
`tabBOM Explosion Item` bei
|
||||
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,
|
||||
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`
|
||||
where item_code = %(item_code)s {conditions}
|
||||
ifnull(sum(reserved_qty_for_production),0) as reserved_qty_for_production, warehouse,
|
||||
ifnull(sum(planned_qty),0) as planned_qty
|
||||
from `tabBin` where item_code = %(item_code)s {conditions}
|
||||
group by item_code, warehouse
|
||||
""".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()
|
||||
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):
|
||||
doc = frappe._dict(json.loads(doc))
|
||||
|
||||
warehouse_list = []
|
||||
if warehouses:
|
||||
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"))
|
||||
get_warehouse_list(warehouses, warehouse_list)
|
||||
|
||||
if 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"))
|
||||
|
||||
warehouse_list = None
|
||||
@ -795,7 +800,7 @@ def get_items_for_material_requests(doc, warehouses=None):
|
||||
if 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 = []
|
||||
for item in mr_items:
|
||||
get_materials_from_other_locations(item, warehouses, new_mr_items, company)
|
||||
|
@ -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) {
|
||||
@ -46,14 +50,19 @@ frappe.ui.form.on('Additional Salary', {
|
||||
},
|
||||
|
||||
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;
|
||||
let filters = {company: frm.doc.company};
|
||||
if (frm.doc.type) {
|
||||
filters.type = frm.doc.type;
|
||||
}
|
||||
frm.set_query("salary_component", function() {
|
||||
return {
|
||||
filters: {type: ["in", ["earning", "deduction"]], company: frm.doc.company}
|
||||
filters: filters
|
||||
};
|
||||
});
|
||||
},
|
||||
|
@ -481,6 +481,7 @@ def make_employee_salary_slip(user, payroll_frequency, salary_structure=None):
|
||||
if not salary_structure:
|
||||
salary_structure = payroll_frequency + " Salary Structure Test for Salary Slip"
|
||||
|
||||
|
||||
employee = frappe.db.get_value("Employee", {"user_id": user})
|
||||
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})})
|
||||
|
@ -124,8 +124,8 @@ def make_salary_structure(salary_structure, payroll_frequency, employee=None,
|
||||
"doctype": "Salary Structure",
|
||||
"name": salary_structure,
|
||||
"company": company or erpnext.get_default_company(),
|
||||
"earnings": make_earning_salary_component(test_tax=test_tax, company_list=["_Test Company"]),
|
||||
"deductions": make_deduction_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(setup=True, test_tax=test_tax, company_list=["_Test Company"]),
|
||||
"payroll_frequency": payroll_frequency,
|
||||
"payment_account": get_random("Account", filters={'account_currency': currency}),
|
||||
"currency": currency
|
||||
|
@ -1,7 +1,7 @@
|
||||
from __future__ import unicode_literals
|
||||
import unittest
|
||||
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.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
|
||||
@ -16,17 +16,22 @@ class TestProjectProfitability(unittest.TestCase):
|
||||
make_salary_structure_for_timesheet(emp, company='_Test Company')
|
||||
self.timesheet = make_timesheet(emp, simulate = True, is_billable=1)
|
||||
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.sales_invoice = make_sales_invoice(self.timesheet.name, '_Test Item', '_Test Customer')
|
||||
self.sales_invoice.due_date = nowdate()
|
||||
self.sales_invoice.submit()
|
||||
|
||||
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):
|
||||
filters = {
|
||||
'company': '_Test Company',
|
||||
'start_date': getdate(),
|
||||
'start_date': add_days(getdate(), -3),
|
||||
'end_date': getdate()
|
||||
}
|
||||
|
||||
|
@ -122,9 +122,20 @@ erpnext.buying.BuyingController = erpnext.TransactionController.extend({
|
||||
this.set_from_product_bundle();
|
||||
}
|
||||
|
||||
this.toggle_subcontracting_fields();
|
||||
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() {
|
||||
var me = this;
|
||||
erpnext.utils.get_party_details(this.frm, null, null, function(){
|
||||
|
@ -387,7 +387,7 @@ erpnext.TransactionController = erpnext.taxes_and_totals.extend({
|
||||
|
||||
if(this.frm.doc.scan_barcode) {
|
||||
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 }
|
||||
}).then(r => {
|
||||
const data = r && r.message;
|
||||
@ -723,6 +723,10 @@ erpnext.TransactionController = erpnext.taxes_and_totals.extend({
|
||||
var me = this;
|
||||
var item = frappe.get_doc(cdt, cdn);
|
||||
|
||||
if (item && item.doctype === 'Purchase Receipt Item Supplied') {
|
||||
return;
|
||||
}
|
||||
|
||||
if (item && item.serial_no) {
|
||||
if (!item.item_code) {
|
||||
this.frm.trigger("item_code", cdt, cdn);
|
||||
|
@ -233,7 +233,7 @@ class SalesOrder(SellingController):
|
||||
# Checks Sales Invoice
|
||||
submit_rv = frappe.db.sql_list("""select t1.name
|
||||
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)
|
||||
|
||||
if submit_rv:
|
||||
|
@ -1217,6 +1217,19 @@ class TestSalesOrder(unittest.TestCase):
|
||||
# To test if the SO does NOT have a Blanket Order
|
||||
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):
|
||||
so = frappe.new_doc("Sales Order")
|
||||
|
@ -1867,7 +1867,7 @@
|
||||
"South Africa": {
|
||||
"South Africa Tax": {
|
||||
"account_name": "VAT",
|
||||
"tax_rate": 14.00
|
||||
"tax_rate": 15.00
|
||||
}
|
||||
},
|
||||
|
||||
|
@ -61,7 +61,8 @@ class ProductQuery:
|
||||
],
|
||||
or_filters=self.or_filters,
|
||||
start=start,
|
||||
limit=self.page_length
|
||||
limit=self.page_length,
|
||||
order_by="weightage desc"
|
||||
)
|
||||
|
||||
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))]
|
||||
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:
|
||||
product_info = get_product_info_for_website(item.item_code, skip_quotation_creation=True).get('product_info')
|
||||
|
@ -54,7 +54,7 @@ class Bin(Document):
|
||||
self.reserved_qty = flt(self.reserved_qty) + flt(args.get("reserved_qty"))
|
||||
self.indented_qty = flt(self.indented_qty) + flt(args.get("indented_qty"))
|
||||
self.planned_qty = flt(self.planned_qty) + flt(args.get("planned_qty"))
|
||||
|
||||
|
||||
self.set_projected_qty()
|
||||
self.db_update()
|
||||
|
||||
@ -115,7 +115,7 @@ class Bin(Document):
|
||||
#Get Transferred Entries
|
||||
materials_transferred = frappe.db.sql("""
|
||||
select
|
||||
ifnull(sum(transfer_qty),0)
|
||||
ifnull(sum(CASE WHEN se.is_return = 1 THEN (transfer_qty * -1) ELSE transfer_qty END),0)
|
||||
from
|
||||
`tabStock Entry` se, `tabStock Entry Detail` sed, `tabPurchase Order` po
|
||||
where
|
||||
|
@ -6,8 +6,8 @@ frappe.listview_settings['Delivery Note'] = {
|
||||
return [__("Return"), "gray", "is_return,=,Yes"];
|
||||
} else if (doc.status === "Closed") {
|
||||
return [__("Closed"), "green", "status,=,Closed"];
|
||||
} else if (flt(doc.per_returned, 2) === 100) {
|
||||
return [__("Return Issued"), "grey", "per_returned,=,100"];
|
||||
} else if (doc.status === "Return Issued") {
|
||||
return [__("Return Issued"), "grey", "status,=,Return Issued"];
|
||||
} else if (flt(doc.per_billed, 2) < 100) {
|
||||
return [__("To Bill"), "orange", "per_billed,<,100"];
|
||||
} else if (flt(doc.per_billed, 2) === 100) {
|
||||
|
@ -18,6 +18,9 @@ class TestItemAlternative(unittest.TestCase):
|
||||
make_items()
|
||||
|
||||
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',
|
||||
qty=5, rate=2000)
|
||||
create_stock_reconciliation(item_code='Test FG A RW 2', warehouse='_Test Warehouse - _TC',
|
||||
@ -65,6 +68,8 @@ class TestItemAlternative(unittest.TestCase):
|
||||
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):
|
||||
create_stock_reconciliation(item_code='Alternate Item For A RW 1',
|
||||
|
@ -101,7 +101,8 @@ frappe.ui.form.on('Material Request', {
|
||||
}
|
||||
|
||||
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 = () => {
|
||||
frm.add_custom_button(__('Pick List'),
|
||||
() => frm.events.create_pick_list(frm), __('Create'));
|
||||
|
@ -514,8 +514,7 @@
|
||||
"oldfieldname": "pr_raw_material_details",
|
||||
"oldfieldtype": "Table",
|
||||
"options": "Purchase Receipt Item Supplied",
|
||||
"print_hide": 1,
|
||||
"read_only": 1
|
||||
"print_hide": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "section_break0",
|
||||
@ -1149,7 +1148,7 @@
|
||||
"idx": 261,
|
||||
"is_submittable": 1,
|
||||
"links": [],
|
||||
"modified": "2021-04-19 01:01:00.754119",
|
||||
"modified": "2021-05-25 00:15:12.239017",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Stock",
|
||||
"name": "Purchase Receipt",
|
||||
|
@ -202,6 +202,7 @@ class PurchaseReceipt(BuyingController):
|
||||
|
||||
self.make_gl_entries()
|
||||
self.repost_future_sle_and_gle()
|
||||
self.set_consumed_qty_in_po()
|
||||
|
||||
def check_next_docstatus(self):
|
||||
submit_rv = frappe.db.sql("""select t1.name
|
||||
@ -233,6 +234,7 @@ class PurchaseReceipt(BuyingController):
|
||||
self.repost_future_sle_and_gle()
|
||||
self.ignore_linked_doctypes = ('GL Entry', 'Stock Ledger Entry', 'Repost Item Valuation')
|
||||
self.delete_auto_created_batches()
|
||||
self.set_consumed_qty_in_po()
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_current_stock(self):
|
||||
|
@ -335,6 +335,10 @@ class TestPurchaseReceipt(unittest.TestCase):
|
||||
se2.cancel()
|
||||
se3.cancel()
|
||||
po.reload()
|
||||
pr2.load_from_db()
|
||||
pr2.cancel()
|
||||
|
||||
po.load_from_db()
|
||||
po.cancel()
|
||||
|
||||
def test_serial_no_supplier(self):
|
||||
|
@ -1079,6 +1079,10 @@ erpnext.stock.select_batch_and_serial_no = (frm, item) => {
|
||||
}
|
||||
|
||||
function attach_bom_items(bom_no) {
|
||||
if (!bom_no) {
|
||||
return
|
||||
}
|
||||
|
||||
if (check_should_not_attach_bom_items(bom_no)) return
|
||||
frappe.db.get_doc("BOM",bom_no).then(bom => {
|
||||
const {name, items} = bom
|
||||
|
@ -74,7 +74,8 @@
|
||||
"total_amount",
|
||||
"job_card",
|
||||
"amended_from",
|
||||
"credit_note"
|
||||
"credit_note",
|
||||
"is_return"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
@ -611,6 +612,16 @@
|
||||
"fieldname": "apply_putaway_rule",
|
||||
"fieldtype": "Check",
|
||||
"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",
|
||||
@ -618,7 +629,7 @@
|
||||
"index_web_pages_for_search": 1,
|
||||
"is_submittable": 1,
|
||||
"links": [],
|
||||
"modified": "2021-05-24 11:32:23.904307",
|
||||
"modified": "2021-05-26 17:07:58.015737",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Stock",
|
||||
"name": "Stock Entry",
|
||||
|
@ -97,8 +97,7 @@ class StockEntry(StockController):
|
||||
update_serial_nos_after_submit(self, "items")
|
||||
self.update_work_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()
|
||||
|
||||
@ -117,9 +116,7 @@ class StockEntry(StockController):
|
||||
self.set_material_request_transfer_status('Completed')
|
||||
|
||||
def on_cancel(self):
|
||||
|
||||
if self.purchase_order and self.purpose == "Send to Subcontractor":
|
||||
self.update_purchase_order_supplied_items()
|
||||
self.update_purchase_order_supplied_items()
|
||||
|
||||
if self.work_order and self.purpose == "Material Consumption for Manufacture":
|
||||
self.validate_work_order_status()
|
||||
@ -1008,10 +1005,12 @@ class StockEntry(StockController):
|
||||
if self.purchase_order and self.purpose == "Send to Subcontractor":
|
||||
#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))
|
||||
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))
|
||||
|
||||
for item in itervalues(item_dict):
|
||||
if self.pro_doc and cint(self.pro_doc.from_wip_warehouse):
|
||||
@ -1294,7 +1293,8 @@ class StockEntry(StockController):
|
||||
item_dict[item]["qty"] = 0
|
||||
|
||||
# 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"]:
|
||||
del item_dict[item]
|
||||
|
||||
@ -1347,7 +1347,7 @@ class StockEntry(StockController):
|
||||
se_child.is_scrap_item = item_dict[d].get("is_scrap_item", 0)
|
||||
|
||||
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):
|
||||
se_child.set(field, item_dict[d].get(field))
|
||||
|
||||
@ -1400,33 +1400,26 @@ class StockEntry(StockController):
|
||||
.format(item.batch_no, item.item_code))
|
||||
|
||||
def update_purchase_order_supplied_items(self):
|
||||
#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))
|
||||
if (self.purchase_order and
|
||||
(self.purpose in ['Send to Subcontractor', 'Material Transfer'] or self.is_return)):
|
||||
|
||||
#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
|
||||
SET
|
||||
pos.supplied_qty = IFNULL((SELECT ifnull(sum(transfer_qty), 0)
|
||||
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})
|
||||
supplied_items = get_supplied_items(self.purchase_order)
|
||||
for name, item in supplied_items.items():
|
||||
frappe.db.set_value('Purchase Order Item Supplied', name, item)
|
||||
|
||||
#Update reserved sub contracted quantity in bin based on Supplied Item Details and
|
||||
for d in self.get("items"):
|
||||
item_code = d.get('original_item') or d.get('item_code')
|
||||
reserve_warehouse = item_wh.get(item_code)
|
||||
stock_bin = get_bin(item_code, reserve_warehouse)
|
||||
stock_bin.update_reserved_qty_for_sub_contracting()
|
||||
#Update reserved sub contracted quantity in bin based on Supplied Item Details and
|
||||
for d in self.get("items"):
|
||||
item_code = d.get('original_item') or d.get('item_code')
|
||||
reserve_warehouse = item_wh.get(item_code)
|
||||
stock_bin = get_bin(item_code, reserve_warehouse)
|
||||
stock_bin.update_reserved_qty_for_sub_contracting()
|
||||
|
||||
def update_so_in_serial_number(self):
|
||||
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
|
||||
""" %(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`
|
||||
SET
|
||||
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)
|
||||
sample_quantity = qty_diff
|
||||
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
|
@ -5,7 +5,7 @@
|
||||
from __future__ import unicode_literals
|
||||
import frappe
|
||||
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 datetime import date
|
||||
from erpnext.controllers.item_variant import ItemTemplateCannotHaveStock
|
||||
@ -108,17 +108,18 @@ class StockLedgerEntry(Document):
|
||||
self.stock_uom = item_det.stock_uom
|
||||
|
||||
def check_stock_frozen_date(self):
|
||||
stock_frozen_upto = frappe.db.get_value('Stock Settings', None, 'stock_frozen_upto') or ''
|
||||
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_settings = frappe.get_doc('Stock Settings', 'Stock Settings')
|
||||
|
||||
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:
|
||||
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())
|
||||
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)
|
||||
|
||||
def scrub_posting_time(self):
|
||||
|
@ -473,6 +473,13 @@ class StockReconciliation(StockController):
|
||||
else:
|
||||
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()
|
||||
def get_items(warehouse, posting_date, posting_time, company):
|
||||
lft, rgt = frappe.db.get_value("Warehouse", warehouse, ["lft", "rgt"])
|
||||
|
@ -11,6 +11,7 @@ from frappe.test_runner import make_test_records
|
||||
import erpnext
|
||||
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.stock.doctype.item.test_item import create_item
|
||||
|
||||
test_records = frappe.get_test_records('Warehouse')
|
||||
|
||||
@ -92,6 +93,39 @@ class TestWarehouse(unittest.TestCase):
|
||||
self.assertTrue(frappe.db.get_value("Warehouse",
|
||||
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):
|
||||
if not company:
|
||||
company = "_Test Company"
|
||||
|
@ -54,6 +54,7 @@ class Warehouse(NestedSet):
|
||||
throw(_("Child warehouse exists for this warehouse. You can not delete this warehouse."))
|
||||
|
||||
self.update_nsm_model()
|
||||
self.unlink_from_items()
|
||||
|
||||
def check_if_sle_exists(self):
|
||||
return frappe.db.sql("""select name from `tabStock Ledger Entry`
|
||||
@ -138,6 +139,12 @@ class Warehouse(NestedSet):
|
||||
self.save()
|
||||
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()
|
||||
def get_children(doctype, parent=None, company=None, is_root=False):
|
||||
if is_root:
|
||||
|
@ -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'
|
||||
}
|
||||
]
|
||||
};
|
@ -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"
|
||||
}
|
||||
]
|
||||
}
|
@ -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
|
||||
}]
|
@ -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")
|
||||
}
|
||||
]
|
||||
};
|
@ -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"
|
||||
}
|
||||
]
|
||||
}
|
@ -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
|
||||
}]
|
@ -22,6 +22,7 @@ _exceptions = frappe.local('stockledger_exceptions')
|
||||
# _exceptions = []
|
||||
|
||||
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:
|
||||
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)
|
||||
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:
|
||||
if sle.serial_no:
|
||||
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()
|
||||
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):
|
||||
from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos
|
||||
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)
|
||||
|
||||
# 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.update_valuation_rate(reset_outgoing_rate=False)
|
||||
for d in (doc.items + doc.supplied_items):
|
||||
|
@ -177,7 +177,7 @@ def get_bin(item_code, warehouse):
|
||||
return bin_obj
|
||||
|
||||
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:
|
||||
bin = get_bin(args.get("item_code"), args.get("warehouse"))
|
||||
bin.update_stock(args, allow_negative_stock, via_landed_cost_voucher)
|
||||
|
@ -15,6 +15,7 @@
|
||||
"hide_custom": 0,
|
||||
"icon": "stock",
|
||||
"idx": 0,
|
||||
"is_default": 0,
|
||||
"is_standard": 1,
|
||||
"label": "Stock",
|
||||
"links": [
|
||||
@ -653,9 +654,44 @@
|
||||
"link_type": "Report",
|
||||
"onboard": 0,
|
||||
"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",
|
||||
"module": "Stock",
|
||||
"name": "Stock",
|
||||
|
@ -166,7 +166,7 @@
|
||||
"options": "Service Level Agreement"
|
||||
},
|
||||
{
|
||||
"depends_on": "eval: doc.status != 'Replied';",
|
||||
"depends_on": "eval: doc.status != 'Replied' && doc.service_level_agreement;",
|
||||
"fieldname": "response_by",
|
||||
"fieldtype": "Datetime",
|
||||
"label": "Response By",
|
||||
@ -180,7 +180,7 @@
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"depends_on": "eval: doc.status != 'Replied';",
|
||||
"depends_on": "eval: doc.status != 'Replied' && doc.service_level_agreement;",
|
||||
"fieldname": "resolution_by",
|
||||
"fieldtype": "Datetime",
|
||||
"label": "Resolution By",
|
||||
@ -410,7 +410,7 @@
|
||||
"icon": "fa fa-ticket",
|
||||
"idx": 7,
|
||||
"links": [],
|
||||
"modified": "2021-05-26 10:49:07.574769",
|
||||
"modified": "2021-06-10 03:22:27.098898",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Support",
|
||||
"name": "Issue",
|
||||
|
@ -29,6 +29,9 @@ class Issue(Document):
|
||||
self.update_status()
|
||||
self.set_lead_contact(self.raised_by)
|
||||
|
||||
if not self.service_level_agreement:
|
||||
self.reset_sla_fields()
|
||||
|
||||
def on_update(self):
|
||||
# Add a communication in the issue timeline
|
||||
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 \
|
||||
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):
|
||||
status = frappe.db.get_value("Issue", self.name, "status")
|
||||
if self.status != "Open" and status == "Open" and not self.first_responded_on:
|
||||
@ -511,4 +521,4 @@ def get_time_in_timedelta(time):
|
||||
Converts datetime.time(10, 36, 55, 961454) to datetime.timedelta(seconds=38215)
|
||||
"""
|
||||
import datetime
|
||||
return datetime.timedelta(hours=time.hour, minutes=time.minute, seconds=time.second)
|
||||
return datetime.timedelta(hours=time.hour, minutes=time.minute, seconds=time.second)
|
||||
|
@ -13,9 +13,11 @@
|
||||
{{ doc.items_preview }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-sm-3 text-right bold">
|
||||
{{ doc.get_formatted("grand_total") }}
|
||||
</div>
|
||||
{% if doc.get('grand_total') %}
|
||||
<div class="col-sm-3 text-right bold">
|
||||
{{ doc.get_formatted("grand_total") }}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
<a class="transaction-item-link" href="/{{ pathname }}/{{ doc.name }}">Link</a>
|
||||
</div>
|
||||
|
877
erpnext/tests/test_subcontracting.py
Normal file
877
erpnext/tests/test_subcontracting.py
Normal 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)
|
Loading…
x
Reference in New Issue
Block a user