Merge pull request #26198 from nextchamp-saqib/pos-fixes-9-v13
refactor(pos): use pos invoice item name as unique identifier
diff --git a/erpnext/accounts/custom/address.py b/erpnext/accounts/custom/address.py
index 5e76403..c417a49 100644
--- a/erpnext/accounts/custom/address.py
+++ b/erpnext/accounts/custom/address.py
@@ -33,6 +33,8 @@
if address and frappe.db.get_value('Dynamic Link',
{'parent': address, 'link_name': company}):
filters.append(["Address", "name", "=", address])
+ if not address:
+ filters.append(["Address", "is_shipping_address", "=", 1])
address = frappe.get_all("Address", filters=filters, fields=fields) or {}
diff --git a/erpnext/accounts/deferred_revenue.py b/erpnext/accounts/deferred_revenue.py
index dd346bc..2f86c6c 100644
--- a/erpnext/accounts/deferred_revenue.py
+++ b/erpnext/accounts/deferred_revenue.py
@@ -263,6 +263,9 @@
amount, base_amount = calculate_amount(doc, item, last_gl_entry,
total_days, total_booking_days, account_currency)
+ if not amount:
+ return
+
if via_journal_entry:
book_revenue_via_journal_entry(doc, credit_account, debit_account, against, amount,
base_amount, end_date, project, account_currency, item.cost_center, item, deferred_process, submit_journal_entry)
diff --git a/erpnext/accounts/doctype/accounts_settings/accounts_settings.json b/erpnext/accounts/doctype/accounts_settings/accounts_settings.json
index 0ff7230..703e93c 100644
--- a/erpnext/accounts/doctype/accounts_settings/accounts_settings.json
+++ b/erpnext/accounts/doctype/accounts_settings/accounts_settings.json
@@ -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",
diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js
index f58c8f4..dc9094c 100644
--- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js
+++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js
@@ -27,10 +27,6 @@
});
},
- company: function() {
- erpnext.accounts.dimensions.update_dimension(this.frm, this.frm.doctype);
- },
-
onload: function() {
this._super();
@@ -569,5 +565,9 @@
frm: frm,
freeze_message: __("Creating Purchase Receipt ...")
})
- }
+ },
+
+ company: function(frm) {
+ erpnext.accounts.dimensions.update_dimension(frm, frm.doctype);
+ },
})
diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.json b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.json
index a714ac7..00ef7d5 100644
--- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.json
+++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.json
@@ -175,9 +175,7 @@
"hidden": 1,
"label": "Title",
"no_copy": 1,
- "print_hide": 1,
- "show_days": 1,
- "show_seconds": 1
+ "print_hide": 1
},
{
"fieldname": "naming_series",
@@ -189,9 +187,7 @@
"options": "ACC-PINV-.YYYY.-\nACC-PINV-RET-.YYYY.-",
"print_hide": 1,
"reqd": 1,
- "set_only_once": 1,
- "show_days": 1,
- "show_seconds": 1
+ "set_only_once": 1
},
{
"fieldname": "supplier",
@@ -203,9 +199,7 @@
"options": "Supplier",
"print_hide": 1,
"reqd": 1,
- "search_index": 1,
- "show_days": 1,
- "show_seconds": 1
+ "search_index": 1
},
{
"bold": 1,
@@ -217,9 +211,7 @@
"label": "Supplier Name",
"oldfieldname": "supplier_name",
"oldfieldtype": "Data",
- "read_only": 1,
- "show_days": 1,
- "show_seconds": 1
+ "read_only": 1
},
{
"fetch_from": "supplier.tax_id",
@@ -227,27 +219,21 @@
"fieldtype": "Read Only",
"label": "Tax Id",
"print_hide": 1,
- "read_only": 1,
- "show_days": 1,
- "show_seconds": 1
+ "read_only": 1
},
{
"fieldname": "due_date",
"fieldtype": "Date",
"label": "Due Date",
"oldfieldname": "due_date",
- "oldfieldtype": "Date",
- "show_days": 1,
- "show_seconds": 1
+ "oldfieldtype": "Date"
},
{
"default": "0",
"fieldname": "is_paid",
"fieldtype": "Check",
"label": "Is Paid",
- "print_hide": 1,
- "show_days": 1,
- "show_seconds": 1
+ "print_hide": 1
},
{
"default": "0",
@@ -255,25 +241,19 @@
"fieldtype": "Check",
"label": "Is Return (Debit Note)",
"no_copy": 1,
- "print_hide": 1,
- "show_days": 1,
- "show_seconds": 1
+ "print_hide": 1
},
{
"default": "0",
"fieldname": "apply_tds",
"fieldtype": "Check",
"label": "Apply Tax Withholding Amount",
- "print_hide": 1,
- "show_days": 1,
- "show_seconds": 1
+ "print_hide": 1
},
{
"fieldname": "column_break1",
"fieldtype": "Column Break",
"oldfieldtype": "Column Break",
- "show_days": 1,
- "show_seconds": 1,
"width": "50%"
},
{
@@ -283,17 +263,13 @@
"label": "Company",
"options": "Company",
"print_hide": 1,
- "remember_last_selected_value": 1,
- "show_days": 1,
- "show_seconds": 1
+ "remember_last_selected_value": 1
},
{
"fieldname": "cost_center",
"fieldtype": "Link",
"label": "Cost Center",
- "options": "Cost Center",
- "show_days": 1,
- "show_seconds": 1
+ "options": "Cost Center"
},
{
"default": "Today",
@@ -305,9 +281,7 @@
"oldfieldtype": "Date",
"print_hide": 1,
"reqd": 1,
- "search_index": 1,
- "show_days": 1,
- "show_seconds": 1
+ "search_index": 1
},
{
"fieldname": "posting_time",
@@ -316,8 +290,6 @@
"no_copy": 1,
"print_hide": 1,
"print_width": "100px",
- "show_days": 1,
- "show_seconds": 1,
"width": "100px"
},
{
@@ -326,9 +298,7 @@
"fieldname": "set_posting_time",
"fieldtype": "Check",
"label": "Edit Posting Date and Time",
- "print_hide": 1,
- "show_days": 1,
- "show_seconds": 1
+ "print_hide": 1
},
{
"fieldname": "amended_from",
@@ -340,58 +310,44 @@
"oldfieldtype": "Link",
"options": "Purchase Invoice",
"print_hide": 1,
- "read_only": 1,
- "show_days": 1,
- "show_seconds": 1
+ "read_only": 1
},
{
"collapsible": 1,
"collapsible_depends_on": "eval:doc.on_hold",
"fieldname": "sb_14",
"fieldtype": "Section Break",
- "label": "Hold Invoice",
- "show_days": 1,
- "show_seconds": 1
+ "label": "Hold Invoice"
},
{
"default": "0",
"fieldname": "on_hold",
"fieldtype": "Check",
- "label": "Hold Invoice",
- "show_days": 1,
- "show_seconds": 1
+ "label": "Hold Invoice"
},
{
"depends_on": "eval:doc.on_hold",
"description": "Once set, this invoice will be on hold till the set date",
"fieldname": "release_date",
"fieldtype": "Date",
- "label": "Release Date",
- "show_days": 1,
- "show_seconds": 1
+ "label": "Release Date"
},
{
"fieldname": "cb_17",
- "fieldtype": "Column Break",
- "show_days": 1,
- "show_seconds": 1
+ "fieldtype": "Column Break"
},
{
"depends_on": "eval:doc.on_hold",
"fieldname": "hold_comment",
"fieldtype": "Small Text",
- "label": "Reason For Putting On Hold",
- "show_days": 1,
- "show_seconds": 1
+ "label": "Reason For Putting On Hold"
},
{
"collapsible": 1,
"collapsible_depends_on": "bill_no",
"fieldname": "supplier_invoice_details",
"fieldtype": "Section Break",
- "label": "Supplier Invoice Details",
- "show_days": 1,
- "show_seconds": 1
+ "label": "Supplier Invoice Details"
},
{
"fieldname": "bill_no",
@@ -399,15 +355,11 @@
"label": "Supplier Invoice No",
"oldfieldname": "bill_no",
"oldfieldtype": "Data",
- "print_hide": 1,
- "show_days": 1,
- "show_seconds": 1
+ "print_hide": 1
},
{
"fieldname": "column_break_15",
- "fieldtype": "Column Break",
- "show_days": 1,
- "show_seconds": 1
+ "fieldtype": "Column Break"
},
{
"fieldname": "bill_date",
@@ -416,17 +368,13 @@
"no_copy": 1,
"oldfieldname": "bill_date",
"oldfieldtype": "Date",
- "print_hide": 1,
- "show_days": 1,
- "show_seconds": 1
+ "print_hide": 1
},
{
"depends_on": "return_against",
"fieldname": "returns",
"fieldtype": "Section Break",
- "label": "Returns",
- "show_days": 1,
- "show_seconds": 1
+ "label": "Returns"
},
{
"depends_on": "return_against",
@@ -436,34 +384,26 @@
"no_copy": 1,
"options": "Purchase Invoice",
"print_hide": 1,
- "read_only": 1,
- "show_days": 1,
- "show_seconds": 1
+ "read_only": 1
},
{
"collapsible": 1,
"fieldname": "section_addresses",
"fieldtype": "Section Break",
- "label": "Address and Contact",
- "show_days": 1,
- "show_seconds": 1
+ "label": "Address and Contact"
},
{
"fieldname": "supplier_address",
"fieldtype": "Link",
"label": "Select Supplier Address",
"options": "Address",
- "print_hide": 1,
- "show_days": 1,
- "show_seconds": 1
+ "print_hide": 1
},
{
"fieldname": "address_display",
"fieldtype": "Small Text",
"label": "Address",
- "read_only": 1,
- "show_days": 1,
- "show_seconds": 1
+ "read_only": 1
},
{
"fieldname": "contact_person",
@@ -471,67 +411,51 @@
"in_global_search": 1,
"label": "Contact Person",
"options": "Contact",
- "print_hide": 1,
- "show_days": 1,
- "show_seconds": 1
+ "print_hide": 1
},
{
"fieldname": "contact_display",
"fieldtype": "Small Text",
"label": "Contact",
- "read_only": 1,
- "show_days": 1,
- "show_seconds": 1
+ "read_only": 1
},
{
"fieldname": "contact_mobile",
"fieldtype": "Small Text",
"label": "Mobile No",
- "read_only": 1,
- "show_days": 1,
- "show_seconds": 1
+ "read_only": 1
},
{
"fieldname": "contact_email",
"fieldtype": "Small Text",
"label": "Contact Email",
"print_hide": 1,
- "read_only": 1,
- "show_days": 1,
- "show_seconds": 1
+ "read_only": 1
},
{
"fieldname": "col_break_address",
- "fieldtype": "Column Break",
- "show_days": 1,
- "show_seconds": 1
+ "fieldtype": "Column Break"
},
{
"fieldname": "shipping_address",
"fieldtype": "Link",
"label": "Select Shipping Address",
"options": "Address",
- "print_hide": 1,
- "show_days": 1,
- "show_seconds": 1
+ "print_hide": 1
},
{
"fieldname": "shipping_address_display",
"fieldtype": "Small Text",
"label": "Shipping Address",
"print_hide": 1,
- "read_only": 1,
- "show_days": 1,
- "show_seconds": 1
+ "read_only": 1
},
{
"collapsible": 1,
"fieldname": "currency_and_price_list",
"fieldtype": "Section Break",
"label": "Currency and Price List",
- "options": "fa fa-tag",
- "show_days": 1,
- "show_seconds": 1
+ "options": "fa fa-tag"
},
{
"fieldname": "currency",
@@ -540,9 +464,7 @@
"oldfieldname": "currency",
"oldfieldtype": "Select",
"options": "Currency",
- "print_hide": 1,
- "show_days": 1,
- "show_seconds": 1
+ "print_hide": 1
},
{
"fieldname": "conversion_rate",
@@ -551,24 +473,18 @@
"oldfieldname": "conversion_rate",
"oldfieldtype": "Currency",
"precision": "9",
- "print_hide": 1,
- "show_days": 1,
- "show_seconds": 1
+ "print_hide": 1
},
{
"fieldname": "column_break2",
- "fieldtype": "Column Break",
- "show_days": 1,
- "show_seconds": 1
+ "fieldtype": "Column Break"
},
{
"fieldname": "buying_price_list",
"fieldtype": "Link",
"label": "Price List",
"options": "Price List",
- "print_hide": 1,
- "show_days": 1,
- "show_seconds": 1
+ "print_hide": 1
},
{
"fieldname": "price_list_currency",
@@ -576,18 +492,14 @@
"label": "Price List Currency",
"options": "Currency",
"print_hide": 1,
- "read_only": 1,
- "show_days": 1,
- "show_seconds": 1
+ "read_only": 1
},
{
"fieldname": "plc_conversion_rate",
"fieldtype": "Float",
"label": "Price List Exchange Rate",
"precision": "9",
- "print_hide": 1,
- "show_days": 1,
- "show_seconds": 1
+ "print_hide": 1
},
{
"default": "0",
@@ -596,15 +508,11 @@
"label": "Ignore Pricing Rule",
"no_copy": 1,
"permlevel": 1,
- "print_hide": 1,
- "show_days": 1,
- "show_seconds": 1
+ "print_hide": 1
},
{
"fieldname": "sec_warehouse",
- "fieldtype": "Section Break",
- "show_days": 1,
- "show_seconds": 1
+ "fieldtype": "Section Break"
},
{
"depends_on": "update_stock",
@@ -613,9 +521,7 @@
"fieldtype": "Link",
"label": "Set Accepted Warehouse",
"options": "Warehouse",
- "print_hide": 1,
- "show_days": 1,
- "show_seconds": 1
+ "print_hide": 1
},
{
"depends_on": "update_stock",
@@ -625,15 +531,11 @@
"label": "Rejected Warehouse",
"no_copy": 1,
"options": "Warehouse",
- "print_hide": 1,
- "show_days": 1,
- "show_seconds": 1
+ "print_hide": 1
},
{
"fieldname": "col_break_warehouse",
- "fieldtype": "Column Break",
- "show_days": 1,
- "show_seconds": 1
+ "fieldtype": "Column Break"
},
{
"default": "No",
@@ -641,33 +543,25 @@
"fieldtype": "Select",
"label": "Raw Materials Supplied",
"options": "No\nYes",
- "print_hide": 1,
- "show_days": 1,
- "show_seconds": 1
+ "print_hide": 1
},
{
"fieldname": "items_section",
"fieldtype": "Section Break",
"oldfieldtype": "Section Break",
- "options": "fa fa-shopping-cart",
- "show_days": 1,
- "show_seconds": 1
+ "options": "fa fa-shopping-cart"
},
{
"default": "0",
"fieldname": "update_stock",
"fieldtype": "Check",
"label": "Update Stock",
- "print_hide": 1,
- "show_days": 1,
- "show_seconds": 1
+ "print_hide": 1
},
{
"fieldname": "scan_barcode",
"fieldtype": "Data",
- "label": "Scan Barcode",
- "show_days": 1,
- "show_seconds": 1
+ "label": "Scan Barcode"
},
{
"allow_bulk_edit": 1,
@@ -677,56 +571,43 @@
"oldfieldname": "entries",
"oldfieldtype": "Table",
"options": "Purchase Invoice Item",
- "reqd": 1,
- "show_days": 1,
- "show_seconds": 1
+ "reqd": 1
},
{
"fieldname": "pricing_rule_details",
"fieldtype": "Section Break",
- "label": "Pricing Rules",
- "show_days": 1,
- "show_seconds": 1
+ "label": "Pricing Rules"
},
{
"fieldname": "pricing_rules",
"fieldtype": "Table",
"label": "Pricing Rule Detail",
"options": "Pricing Rule Detail",
- "read_only": 1,
- "show_days": 1,
- "show_seconds": 1
+ "read_only": 1
},
{
"collapsible_depends_on": "supplied_items",
"fieldname": "raw_materials_supplied",
"fieldtype": "Section Break",
- "label": "Raw Materials Supplied",
- "show_days": 1,
- "show_seconds": 1
+ "label": "Raw Materials Supplied"
},
{
+ "depends_on": "update_stock",
"fieldname": "supplied_items",
"fieldtype": "Table",
"label": "Supplied Items",
- "options": "Purchase Receipt Item Supplied",
- "read_only": 1,
- "show_days": 1,
- "show_seconds": 1
+ "no_copy": 1,
+ "options": "Purchase Receipt Item Supplied"
},
{
"fieldname": "section_break_26",
- "fieldtype": "Section Break",
- "show_days": 1,
- "show_seconds": 1
+ "fieldtype": "Section Break"
},
{
"fieldname": "total_qty",
"fieldtype": "Float",
"label": "Total Quantity",
- "read_only": 1,
- "show_days": 1,
- "show_seconds": 1
+ "read_only": 1
},
{
"fieldname": "base_total",
@@ -734,9 +615,7 @@
"label": "Total (Company Currency)",
"options": "Company:company:default_currency",
"print_hide": 1,
- "read_only": 1,
- "show_days": 1,
- "show_seconds": 1
+ "read_only": 1
},
{
"fieldname": "base_net_total",
@@ -746,24 +625,18 @@
"oldfieldtype": "Currency",
"options": "Company:company:default_currency",
"print_hide": 1,
- "read_only": 1,
- "show_days": 1,
- "show_seconds": 1
+ "read_only": 1
},
{
"fieldname": "column_break_28",
- "fieldtype": "Column Break",
- "show_days": 1,
- "show_seconds": 1
+ "fieldtype": "Column Break"
},
{
"fieldname": "total",
"fieldtype": "Currency",
"label": "Total",
"options": "currency",
- "read_only": 1,
- "show_days": 1,
- "show_seconds": 1
+ "read_only": 1
},
{
"fieldname": "net_total",
@@ -773,56 +646,42 @@
"oldfieldtype": "Currency",
"options": "currency",
"print_hide": 1,
- "read_only": 1,
- "show_days": 1,
- "show_seconds": 1
+ "read_only": 1
},
{
"fieldname": "total_net_weight",
"fieldtype": "Float",
"label": "Total Net Weight",
"print_hide": 1,
- "read_only": 1,
- "show_days": 1,
- "show_seconds": 1
+ "read_only": 1
},
{
"fieldname": "taxes_section",
"fieldtype": "Section Break",
"oldfieldtype": "Section Break",
- "options": "fa fa-money",
- "show_days": 1,
- "show_seconds": 1
+ "options": "fa fa-money"
},
{
"fieldname": "tax_category",
"fieldtype": "Link",
"label": "Tax Category",
"options": "Tax Category",
- "print_hide": 1,
- "show_days": 1,
- "show_seconds": 1
+ "print_hide": 1
},
{
"fieldname": "column_break_49",
- "fieldtype": "Column Break",
- "show_days": 1,
- "show_seconds": 1
+ "fieldtype": "Column Break"
},
{
"fieldname": "shipping_rule",
"fieldtype": "Link",
"label": "Shipping Rule",
"options": "Shipping Rule",
- "print_hide": 1,
- "show_days": 1,
- "show_seconds": 1
+ "print_hide": 1
},
{
"fieldname": "section_break_51",
- "fieldtype": "Section Break",
- "show_days": 1,
- "show_seconds": 1
+ "fieldtype": "Section Break"
},
{
"fieldname": "taxes_and_charges",
@@ -831,9 +690,7 @@
"oldfieldname": "purchase_other_charges",
"oldfieldtype": "Link",
"options": "Purchase Taxes and Charges Template",
- "print_hide": 1,
- "show_days": 1,
- "show_seconds": 1
+ "print_hide": 1
},
{
"fieldname": "taxes",
@@ -841,17 +698,13 @@
"label": "Purchase Taxes and Charges",
"oldfieldname": "purchase_tax_details",
"oldfieldtype": "Table",
- "options": "Purchase Taxes and Charges",
- "show_days": 1,
- "show_seconds": 1
+ "options": "Purchase Taxes and Charges"
},
{
"collapsible": 1,
"fieldname": "sec_tax_breakup",
"fieldtype": "Section Break",
- "label": "Tax Breakup",
- "show_days": 1,
- "show_seconds": 1
+ "label": "Tax Breakup"
},
{
"fieldname": "other_charges_calculation",
@@ -860,17 +713,13 @@
"no_copy": 1,
"oldfieldtype": "HTML",
"print_hide": 1,
- "read_only": 1,
- "show_days": 1,
- "show_seconds": 1
+ "read_only": 1
},
{
"fieldname": "totals",
"fieldtype": "Section Break",
"oldfieldtype": "Section Break",
- "options": "fa fa-money",
- "show_days": 1,
- "show_seconds": 1
+ "options": "fa fa-money"
},
{
"fieldname": "base_taxes_and_charges_added",
@@ -880,9 +729,7 @@
"oldfieldtype": "Currency",
"options": "Company:company:default_currency",
"print_hide": 1,
- "read_only": 1,
- "show_days": 1,
- "show_seconds": 1
+ "read_only": 1
},
{
"fieldname": "base_taxes_and_charges_deducted",
@@ -892,9 +739,7 @@
"oldfieldtype": "Currency",
"options": "Company:company:default_currency",
"print_hide": 1,
- "read_only": 1,
- "show_days": 1,
- "show_seconds": 1
+ "read_only": 1
},
{
"fieldname": "base_total_taxes_and_charges",
@@ -904,15 +749,11 @@
"oldfieldtype": "Currency",
"options": "Company:company:default_currency",
"print_hide": 1,
- "read_only": 1,
- "show_days": 1,
- "show_seconds": 1
+ "read_only": 1
},
{
"fieldname": "column_break_40",
- "fieldtype": "Column Break",
- "show_days": 1,
- "show_seconds": 1
+ "fieldtype": "Column Break"
},
{
"fieldname": "taxes_and_charges_added",
@@ -922,9 +763,7 @@
"oldfieldtype": "Currency",
"options": "currency",
"print_hide": 1,
- "read_only": 1,
- "show_days": 1,
- "show_seconds": 1
+ "read_only": 1
},
{
"fieldname": "taxes_and_charges_deducted",
@@ -934,9 +773,7 @@
"oldfieldtype": "Currency",
"options": "currency",
"print_hide": 1,
- "read_only": 1,
- "show_days": 1,
- "show_seconds": 1
+ "read_only": 1
},
{
"fieldname": "total_taxes_and_charges",
@@ -944,18 +781,14 @@
"label": "Total Taxes and Charges",
"options": "currency",
"print_hide": 1,
- "read_only": 1,
- "show_days": 1,
- "show_seconds": 1
+ "read_only": 1
},
{
"collapsible": 1,
"collapsible_depends_on": "discount_amount",
"fieldname": "section_break_44",
"fieldtype": "Section Break",
- "label": "Additional Discount",
- "show_days": 1,
- "show_seconds": 1
+ "label": "Additional Discount"
},
{
"default": "Grand Total",
@@ -963,9 +796,7 @@
"fieldtype": "Select",
"label": "Apply Additional Discount On",
"options": "\nGrand Total\nNet Total",
- "print_hide": 1,
- "show_days": 1,
- "show_seconds": 1
+ "print_hide": 1
},
{
"fieldname": "base_discount_amount",
@@ -973,38 +804,28 @@
"label": "Additional Discount Amount (Company Currency)",
"options": "Company:company:default_currency",
"print_hide": 1,
- "read_only": 1,
- "show_days": 1,
- "show_seconds": 1
+ "read_only": 1
},
{
"fieldname": "column_break_46",
- "fieldtype": "Column Break",
- "show_days": 1,
- "show_seconds": 1
+ "fieldtype": "Column Break"
},
{
"fieldname": "additional_discount_percentage",
"fieldtype": "Float",
"label": "Additional Discount Percentage",
- "print_hide": 1,
- "show_days": 1,
- "show_seconds": 1
+ "print_hide": 1
},
{
"fieldname": "discount_amount",
"fieldtype": "Currency",
"label": "Additional Discount Amount",
"options": "currency",
- "print_hide": 1,
- "show_days": 1,
- "show_seconds": 1
+ "print_hide": 1
},
{
"fieldname": "section_break_49",
- "fieldtype": "Section Break",
- "show_days": 1,
- "show_seconds": 1
+ "fieldtype": "Section Break"
},
{
"fieldname": "base_grand_total",
@@ -1014,9 +835,7 @@
"oldfieldtype": "Currency",
"options": "Company:company:default_currency",
"print_hide": 1,
- "read_only": 1,
- "show_days": 1,
- "show_seconds": 1
+ "read_only": 1
},
{
"depends_on": "eval:!doc.disable_rounded_total",
@@ -1026,9 +845,7 @@
"no_copy": 1,
"options": "Company:company:default_currency",
"print_hide": 1,
- "read_only": 1,
- "show_days": 1,
- "show_seconds": 1
+ "read_only": 1
},
{
"depends_on": "eval:!doc.disable_rounded_total",
@@ -1038,9 +855,7 @@
"no_copy": 1,
"options": "Company:company:default_currency",
"print_hide": 1,
- "read_only": 1,
- "show_days": 1,
- "show_seconds": 1
+ "read_only": 1
},
{
"fieldname": "base_in_words",
@@ -1050,17 +865,13 @@
"oldfieldname": "in_words",
"oldfieldtype": "Data",
"print_hide": 1,
- "read_only": 1,
- "show_days": 1,
- "show_seconds": 1
+ "read_only": 1
},
{
"fieldname": "column_break8",
"fieldtype": "Column Break",
"oldfieldtype": "Column Break",
"print_hide": 1,
- "show_days": 1,
- "show_seconds": 1,
"width": "50%"
},
{
@@ -1071,9 +882,7 @@
"oldfieldname": "grand_total_import",
"oldfieldtype": "Currency",
"options": "currency",
- "read_only": 1,
- "show_days": 1,
- "show_seconds": 1
+ "read_only": 1
},
{
"depends_on": "eval:!doc.disable_rounded_total",
@@ -1083,9 +892,7 @@
"no_copy": 1,
"options": "currency",
"print_hide": 1,
- "read_only": 1,
- "show_days": 1,
- "show_seconds": 1
+ "read_only": 1
},
{
"depends_on": "eval:!doc.disable_rounded_total",
@@ -1095,9 +902,7 @@
"no_copy": 1,
"options": "currency",
"print_hide": 1,
- "read_only": 1,
- "show_days": 1,
- "show_seconds": 1
+ "read_only": 1
},
{
"fieldname": "in_words",
@@ -1107,9 +912,7 @@
"oldfieldname": "in_words_import",
"oldfieldtype": "Data",
"print_hide": 1,
- "read_only": 1,
- "show_days": 1,
- "show_seconds": 1
+ "read_only": 1
},
{
"fieldname": "total_advance",
@@ -1120,9 +923,7 @@
"oldfieldtype": "Currency",
"options": "party_account_currency",
"print_hide": 1,
- "read_only": 1,
- "show_days": 1,
- "show_seconds": 1
+ "read_only": 1
},
{
"fieldname": "outstanding_amount",
@@ -1133,18 +934,14 @@
"oldfieldtype": "Currency",
"options": "party_account_currency",
"print_hide": 1,
- "read_only": 1,
- "show_days": 1,
- "show_seconds": 1
+ "read_only": 1
},
{
"default": "0",
"depends_on": "grand_total",
"fieldname": "disable_rounded_total",
"fieldtype": "Check",
- "label": "Disable Rounded Total",
- "show_days": 1,
- "show_seconds": 1
+ "label": "Disable Rounded Total"
},
{
"collapsible": 1,
@@ -1152,26 +949,20 @@
"depends_on": "eval:doc.is_paid===1||(doc.advances && doc.advances.length>0)",
"fieldname": "payments_section",
"fieldtype": "Section Break",
- "label": "Payments",
- "show_days": 1,
- "show_seconds": 1
+ "label": "Payments"
},
{
"fieldname": "mode_of_payment",
"fieldtype": "Link",
"label": "Mode of Payment",
"options": "Mode of Payment",
- "print_hide": 1,
- "show_days": 1,
- "show_seconds": 1
+ "print_hide": 1
},
{
"fieldname": "cash_bank_account",
"fieldtype": "Link",
"label": "Cash/Bank Account",
- "options": "Account",
- "show_days": 1,
- "show_seconds": 1
+ "options": "Account"
},
{
"fieldname": "clearance_date",
@@ -1179,15 +970,11 @@
"label": "Clearance Date",
"no_copy": 1,
"print_hide": 1,
- "read_only": 1,
- "show_days": 1,
- "show_seconds": 1
+ "read_only": 1
},
{
"fieldname": "col_br_payments",
- "fieldtype": "Column Break",
- "show_days": 1,
- "show_seconds": 1
+ "fieldtype": "Column Break"
},
{
"depends_on": "is_paid",
@@ -1196,9 +983,7 @@
"label": "Paid Amount",
"no_copy": 1,
"options": "currency",
- "print_hide": 1,
- "show_days": 1,
- "show_seconds": 1
+ "print_hide": 1
},
{
"fieldname": "base_paid_amount",
@@ -1207,9 +992,7 @@
"no_copy": 1,
"options": "Company:company:default_currency",
"print_hide": 1,
- "read_only": 1,
- "show_days": 1,
- "show_seconds": 1
+ "read_only": 1
},
{
"collapsible": 1,
@@ -1217,9 +1000,7 @@
"depends_on": "grand_total",
"fieldname": "write_off",
"fieldtype": "Section Break",
- "label": "Write Off",
- "show_days": 1,
- "show_seconds": 1
+ "label": "Write Off"
},
{
"fieldname": "write_off_amount",
@@ -1227,9 +1008,7 @@
"label": "Write Off Amount",
"no_copy": 1,
"options": "currency",
- "print_hide": 1,
- "show_days": 1,
- "show_seconds": 1
+ "print_hide": 1
},
{
"fieldname": "base_write_off_amount",
@@ -1238,15 +1017,11 @@
"no_copy": 1,
"options": "Company:company:default_currency",
"print_hide": 1,
- "read_only": 1,
- "show_days": 1,
- "show_seconds": 1
+ "read_only": 1
},
{
"fieldname": "column_break_61",
- "fieldtype": "Column Break",
- "show_days": 1,
- "show_seconds": 1
+ "fieldtype": "Column Break"
},
{
"depends_on": "eval:flt(doc.write_off_amount)!=0",
@@ -1254,9 +1029,7 @@
"fieldtype": "Link",
"label": "Write Off Account",
"options": "Account",
- "print_hide": 1,
- "show_days": 1,
- "show_seconds": 1
+ "print_hide": 1
},
{
"depends_on": "eval:flt(doc.write_off_amount)!=0",
@@ -1264,9 +1037,7 @@
"fieldtype": "Link",
"label": "Write Off Cost Center",
"options": "Cost Center",
- "print_hide": 1,
- "show_days": 1,
- "show_seconds": 1
+ "print_hide": 1
},
{
"collapsible": 1,
@@ -1276,17 +1047,13 @@
"label": "Advance Payments",
"oldfieldtype": "Section Break",
"options": "fa fa-money",
- "print_hide": 1,
- "show_days": 1,
- "show_seconds": 1
+ "print_hide": 1
},
{
"default": "0",
"fieldname": "allocate_advances_automatically",
"fieldtype": "Check",
- "label": "Set Advances and Allocate (FIFO)",
- "show_days": 1,
- "show_seconds": 1
+ "label": "Set Advances and Allocate (FIFO)"
},
{
"depends_on": "eval:!doc.allocate_advances_automatically",
@@ -1294,9 +1061,7 @@
"fieldtype": "Button",
"label": "Get Advances Paid",
"oldfieldtype": "Button",
- "print_hide": 1,
- "show_days": 1,
- "show_seconds": 1
+ "print_hide": 1
},
{
"fieldname": "advances",
@@ -1306,26 +1071,20 @@
"oldfieldname": "advance_allocation_details",
"oldfieldtype": "Table",
"options": "Purchase Invoice Advance",
- "print_hide": 1,
- "show_days": 1,
- "show_seconds": 1
+ "print_hide": 1
},
{
"collapsible": 1,
"collapsible_depends_on": "eval:(!doc.is_return)",
"fieldname": "payment_schedule_section",
"fieldtype": "Section Break",
- "label": "Payment Terms",
- "show_days": 1,
- "show_seconds": 1
+ "label": "Payment Terms"
},
{
"fieldname": "payment_terms_template",
"fieldtype": "Link",
"label": "Payment Terms Template",
- "options": "Payment Terms Template",
- "show_days": 1,
- "show_seconds": 1
+ "options": "Payment Terms Template"
},
{
"fieldname": "payment_schedule",
@@ -1333,9 +1092,7 @@
"label": "Payment Schedule",
"no_copy": 1,
"options": "Payment Schedule",
- "print_hide": 1,
- "show_days": 1,
- "show_seconds": 1
+ "print_hide": 1
},
{
"collapsible": 1,
@@ -1343,33 +1100,25 @@
"fieldname": "terms_section_break",
"fieldtype": "Section Break",
"label": "Terms and Conditions",
- "options": "fa fa-legal",
- "show_days": 1,
- "show_seconds": 1
+ "options": "fa fa-legal"
},
{
"fieldname": "tc_name",
"fieldtype": "Link",
"label": "Terms",
"options": "Terms and Conditions",
- "print_hide": 1,
- "show_days": 1,
- "show_seconds": 1
+ "print_hide": 1
},
{
"fieldname": "terms",
"fieldtype": "Text Editor",
- "label": "Terms and Conditions1",
- "show_days": 1,
- "show_seconds": 1
+ "label": "Terms and Conditions1"
},
{
"collapsible": 1,
"fieldname": "printing_settings",
"fieldtype": "Section Break",
- "label": "Printing Settings",
- "show_days": 1,
- "show_seconds": 1
+ "label": "Printing Settings"
},
{
"allow_on_submit": 1,
@@ -1377,9 +1126,7 @@
"fieldtype": "Link",
"label": "Letter Head",
"options": "Letter Head",
- "print_hide": 1,
- "show_days": 1,
- "show_seconds": 1
+ "print_hide": 1
},
{
"allow_on_submit": 1,
@@ -1387,15 +1134,11 @@
"fieldname": "group_same_items",
"fieldtype": "Check",
"label": "Group same items",
- "print_hide": 1,
- "show_days": 1,
- "show_seconds": 1
+ "print_hide": 1
},
{
"fieldname": "column_break_112",
- "fieldtype": "Column Break",
- "show_days": 1,
- "show_seconds": 1
+ "fieldtype": "Column Break"
},
{
"allow_on_submit": 1,
@@ -1407,18 +1150,14 @@
"oldfieldtype": "Link",
"options": "Print Heading",
"print_hide": 1,
- "report_hide": 1,
- "show_days": 1,
- "show_seconds": 1
+ "report_hide": 1
},
{
"fieldname": "language",
"fieldtype": "Data",
"label": "Print Language",
"print_hide": 1,
- "read_only": 1,
- "show_days": 1,
- "show_seconds": 1
+ "read_only": 1
},
{
"collapsible": 1,
@@ -1427,9 +1166,7 @@
"label": "More Information",
"oldfieldtype": "Section Break",
"options": "fa fa-file-text",
- "print_hide": 1,
- "show_days": 1,
- "show_seconds": 1
+ "print_hide": 1
},
{
"fieldname": "credit_to",
@@ -1440,9 +1177,7 @@
"options": "Account",
"print_hide": 1,
"reqd": 1,
- "search_index": 1,
- "show_days": 1,
- "show_seconds": 1
+ "search_index": 1
},
{
"fieldname": "party_account_currency",
@@ -1452,9 +1187,7 @@
"no_copy": 1,
"options": "Currency",
"print_hide": 1,
- "read_only": 1,
- "show_days": 1,
- "show_seconds": 1
+ "read_only": 1
},
{
"default": "No",
@@ -1464,9 +1197,7 @@
"oldfieldname": "is_opening",
"oldfieldtype": "Select",
"options": "No\nYes",
- "print_hide": 1,
- "show_days": 1,
- "show_seconds": 1
+ "print_hide": 1
},
{
"fieldname": "against_expense_account",
@@ -1476,15 +1207,11 @@
"no_copy": 1,
"oldfieldname": "against_expense_account",
"oldfieldtype": "Small Text",
- "print_hide": 1,
- "show_days": 1,
- "show_seconds": 1
+ "print_hide": 1
},
{
"fieldname": "column_break_63",
- "fieldtype": "Column Break",
- "show_days": 1,
- "show_seconds": 1
+ "fieldtype": "Column Break"
},
{
"default": "Draft",
@@ -1493,9 +1220,7 @@
"in_standard_filter": 1,
"label": "Status",
"options": "\nDraft\nReturn\nDebit Note Issued\nSubmitted\nPaid\nUnpaid\nOverdue\nCancelled\nInternal Transfer",
- "print_hide": 1,
- "show_days": 1,
- "show_seconds": 1
+ "print_hide": 1
},
{
"fieldname": "inter_company_invoice_reference",
@@ -1504,9 +1229,7 @@
"no_copy": 1,
"options": "Sales Invoice",
"print_hide": 1,
- "read_only": 1,
- "show_days": 1,
- "show_seconds": 1
+ "read_only": 1
},
{
"fieldname": "remarks",
@@ -1515,18 +1238,14 @@
"no_copy": 1,
"oldfieldname": "remarks",
"oldfieldtype": "Text",
- "print_hide": 1,
- "show_days": 1,
- "show_seconds": 1
+ "print_hide": 1
},
{
"collapsible": 1,
"fieldname": "subscription_section",
"fieldtype": "Section Break",
"label": "Subscription Section",
- "print_hide": 1,
- "show_days": 1,
- "show_seconds": 1
+ "print_hide": 1
},
{
"allow_on_submit": 1,
@@ -1535,9 +1254,7 @@
"fieldtype": "Date",
"label": "From Date",
"no_copy": 1,
- "print_hide": 1,
- "show_days": 1,
- "show_seconds": 1
+ "print_hide": 1
},
{
"allow_on_submit": 1,
@@ -1546,15 +1263,11 @@
"fieldtype": "Date",
"label": "To Date",
"no_copy": 1,
- "print_hide": 1,
- "show_days": 1,
- "show_seconds": 1
+ "print_hide": 1
},
{
"fieldname": "column_break_114",
- "fieldtype": "Column Break",
- "show_days": 1,
- "show_seconds": 1
+ "fieldtype": "Column Break"
},
{
"fieldname": "auto_repeat",
@@ -1563,32 +1276,24 @@
"no_copy": 1,
"options": "Auto Repeat",
"print_hide": 1,
- "read_only": 1,
- "show_days": 1,
- "show_seconds": 1
+ "read_only": 1
},
{
"allow_on_submit": 1,
"depends_on": "eval: doc.auto_repeat",
"fieldname": "update_auto_repeat_reference",
"fieldtype": "Button",
- "label": "Update Auto Repeat Reference",
- "show_days": 1,
- "show_seconds": 1
+ "label": "Update Auto Repeat Reference"
},
{
"collapsible": 1,
"fieldname": "accounting_dimensions_section",
"fieldtype": "Section Break",
- "label": "Accounting Dimensions ",
- "show_days": 1,
- "show_seconds": 1
+ "label": "Accounting Dimensions "
},
{
"fieldname": "dimension_col_break",
- "fieldtype": "Column Break",
- "show_days": 1,
- "show_seconds": 1
+ "fieldtype": "Column Break"
},
{
"default": "0",
@@ -1596,9 +1301,7 @@
"fieldname": "is_internal_supplier",
"fieldtype": "Check",
"label": "Is Internal Supplier",
- "read_only": 1,
- "show_days": 1,
- "show_seconds": 1
+ "read_only": 1
},
{
"fieldname": "tax_withholding_category",
@@ -1606,25 +1309,19 @@
"hidden": 1,
"label": "Tax Withholding Category",
"options": "Tax Withholding Category",
- "print_hide": 1,
- "show_days": 1,
- "show_seconds": 1
+ "print_hide": 1
},
{
"fieldname": "billing_address",
"fieldtype": "Link",
"label": "Select Billing Address",
- "options": "Address",
- "show_days": 1,
- "show_seconds": 1
+ "options": "Address"
},
{
"fieldname": "billing_address_display",
"fieldtype": "Small Text",
"label": "Billing Address",
- "read_only": 1,
- "show_days": 1,
- "show_seconds": 1
+ "read_only": 1
},
{
"fieldname": "project",
@@ -1638,9 +1335,7 @@
"fieldname": "unrealized_profit_loss_account",
"fieldtype": "Link",
"label": "Unrealized Profit / Loss Account",
- "options": "Account",
- "show_days": 1,
- "show_seconds": 1
+ "options": "Account"
},
{
"depends_on": "eval:doc.is_internal_supplier",
@@ -1649,9 +1344,7 @@
"fieldname": "represents_company",
"fieldtype": "Link",
"label": "Represents Company",
- "options": "Company",
- "show_days": 1,
- "show_seconds": 1
+ "options": "Company"
},
{
"depends_on": "eval:doc.update_stock && doc.is_internal_supplier",
@@ -1663,8 +1356,6 @@
"options": "Warehouse",
"print_hide": 1,
"print_width": "50px",
- "show_days": 1,
- "show_seconds": 1,
"width": "50px"
},
{
@@ -1692,7 +1383,7 @@
"idx": 204,
"is_submittable": 1,
"links": [],
- "modified": "2021-06-09 12:30:25.632109",
+ "modified": "2021-06-15 18:20:56.806195",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Purchase Invoice",
diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py
index 0ee0bc7..45d89ad 100644
--- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py
+++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py
@@ -400,6 +400,7 @@
# 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 @@
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()
diff --git a/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py b/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py
index 503dda7..ff433b9 100644
--- a/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py
+++ b/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py
@@ -621,8 +621,10 @@
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)
diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py
index e14f305..55a5b99 100644
--- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py
+++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py
@@ -989,7 +989,7 @@
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
diff --git a/erpnext/buying/doctype/buying_settings/buying_settings.json b/erpnext/buying/doctype/buying_settings/buying_settings.json
index 630a1dc..b9c77d5 100644
--- a/erpnext/buying/doctype/buying_settings/buying_settings.json
+++ b/erpnext/buying/doctype/buying_settings/buying_settings.json
@@ -9,13 +9,14 @@
"supp_master_name",
"supplier_group",
"buying_price_list",
+ "maintain_same_rate_action",
+ "role_to_override_stop_action",
"column_break_3",
"po_required",
"pr_required",
"maintain_same_rate",
- "maintain_same_rate_action",
- "role_to_override_stop_action",
"allow_multiple_items",
+ "bill_for_rejected_quantity_in_purchase_invoice",
"subcontract",
"backflush_raw_materials_of_subcontract_based_on",
"column_break_11",
@@ -108,6 +109,13 @@
"fieldtype": "Link",
"label": "Role Allowed to Override Stop Action",
"options": "Role"
+ },
+ {
+ "default": "1",
+ "description": "If checked, Rejected Quantity will be included while making Purchase Invoice from Purchase Receipt.",
+ "fieldname": "bill_for_rejected_quantity_in_purchase_invoice",
+ "fieldtype": "Check",
+ "label": "Bill for Rejected Quantity in Purchase Invoice"
}
],
"icon": "fa fa-cog",
@@ -115,7 +123,7 @@
"index_web_pages_for_search": 1,
"issingle": 1,
"links": [],
- "modified": "2021-04-04 20:01:44.087066",
+ "modified": "2021-06-24 10:38:28.934525",
"modified_by": "Administrator",
"module": "Buying",
"name": "Buying Settings",
diff --git a/erpnext/buying/doctype/purchase_order/purchase_order.js b/erpnext/buying/doctype/purchase_order/purchase_order.js
index 0f6d927..233a9c8 100644
--- a/erpnext/buying/doctype/purchase_order/purchase_order.js
+++ b/erpnext/buying/doctype/purchase_order/purchase_order.js
@@ -53,6 +53,39 @@
} 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 @@
},
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 @@
],
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
},
diff --git a/erpnext/buying/doctype/purchase_order/purchase_order.json b/erpnext/buying/doctype/purchase_order/purchase_order.json
index 41668c6..bb0ad60 100644
--- a/erpnext/buying/doctype/purchase_order/purchase_order.json
+++ b/erpnext/buying/doctype/purchase_order/purchase_order.json
@@ -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",
diff --git a/erpnext/buying/doctype/purchase_order/purchase_order.py b/erpnext/buying/doctype/purchase_order/purchase_order.py
index 2629ba7..eaa502f 100644
--- a/erpnext/buying/doctype/purchase_order/purchase_order.py
+++ b/erpnext/buying/doctype/purchase_order/purchase_order.py
@@ -14,12 +14,11 @@
from erpnext.buying.utils import validate_for_items, check_on_hold_or_closed_status
from erpnext.stock.utils import get_bin
from erpnext.accounts.party import get_party_account_currency
-from six import string_types
from erpnext.stock.doctype.item.item import get_item_defaults
from erpnext.setup.doctype.item_group.item_group import get_item_group_defaults
from erpnext.accounts.doctype.tax_withholding_category.tax_withholding_category import get_party_tax_withholding_details
-from erpnext.accounts.doctype.sales_invoice.sales_invoice import validate_inter_company_party, update_linked_doc,\
- unlink_inter_company_doc
+from erpnext.accounts.doctype.sales_invoice.sales_invoice import (validate_inter_company_party,
+ update_linked_doc, unlink_inter_company_doc)
form_grid_templates = {
"items": "templates/form_grid/item_grid.html"
@@ -503,9 +502,11 @@
@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 @@
'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 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 ''
+ })
\ No newline at end of file
diff --git a/erpnext/buying/doctype/purchase_order/test_purchase_order.py b/erpnext/buying/doctype/purchase_order/test_purchase_order.py
index 3b9f8e9..8563b97 100644
--- a/erpnext/buying/doctype/purchase_order/test_purchase_order.py
+++ b/erpnext/buying/doctype/purchase_order/test_purchase_order.py
@@ -20,7 +20,6 @@
from erpnext.manufacturing.doctype.blanket_order.test_blanket_order import make_blanket_order
from erpnext.stock.doctype.batch.test_batch import make_new_batch
-from erpnext.controllers.buying_controller import get_backflushed_subcontracted_raw_materials
class TestPurchaseOrder(unittest.TestCase):
def test_make_purchase_receipt(self):
@@ -771,7 +770,7 @@
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 @@
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 @@
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
diff --git a/erpnext/buying/doctype/purchase_order_item_supplied/purchase_order_item_supplied.json b/erpnext/buying/doctype/purchase_order_item_supplied/purchase_order_item_supplied.json
index d7ea9c1..60247bd 100644
--- a/erpnext/buying/doctype/purchase_order_item_supplied/purchase_order_item_supplied.json
+++ b/erpnext/buying/doctype/purchase_order_item_supplied/purchase_order_item_supplied.json
@@ -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",
diff --git a/erpnext/buying/doctype/purchase_receipt_item_supplied/purchase_receipt_item_supplied.json b/erpnext/buying/doctype/purchase_receipt_item_supplied/purchase_receipt_item_supplied.json
index dc00bca..f9cd720 100644
--- a/erpnext/buying/doctype/purchase_receipt_item_supplied/purchase_receipt_item_supplied.json
+++ b/erpnext/buying/doctype/purchase_receipt_item_supplied/purchase_receipt_item_supplied.json
@@ -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",
diff --git a/erpnext/buying/doctype/request_for_quotation/request_for_quotation.py b/erpnext/buying/doctype/request_for_quotation/request_for_quotation.py
index 0127eb8..a4ce84e 100644
--- a/erpnext/buying/doctype/request_for_quotation/request_for_quotation.py
+++ b/erpnext/buying/doctype/request_for_quotation/request_for_quotation.py
@@ -317,19 +317,21 @@
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)
diff --git a/erpnext/buying/report/subcontract_order_summary/__init__.py b/erpnext/buying/report/subcontract_order_summary/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/erpnext/buying/report/subcontract_order_summary/__init__.py
diff --git a/erpnext/buying/report/subcontract_order_summary/subcontract_order_summary.js b/erpnext/buying/report/subcontract_order_summary/subcontract_order_summary.js
new file mode 100644
index 0000000..5ba52f1
--- /dev/null
+++ b/erpnext/buying/report/subcontract_order_summary/subcontract_order_summary.js
@@ -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')
+ }
+ }
+ }
+ }
+ ]
+};
diff --git a/erpnext/buying/report/subcontract_order_summary/subcontract_order_summary.json b/erpnext/buying/report/subcontract_order_summary/subcontract_order_summary.json
new file mode 100644
index 0000000..526a8d8
--- /dev/null
+++ b/erpnext/buying/report/subcontract_order_summary/subcontract_order_summary.json
@@ -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"
+ }
+ ]
+}
\ No newline at end of file
diff --git a/erpnext/buying/report/subcontract_order_summary/subcontract_order_summary.py b/erpnext/buying/report/subcontract_order_summary/subcontract_order_summary.py
new file mode 100644
index 0000000..0c0d4f0
--- /dev/null
+++ b/erpnext/buying/report/subcontract_order_summary/subcontract_order_summary.py
@@ -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
+ }
+ ]
\ No newline at end of file
diff --git a/erpnext/controllers/accounts_controller.py b/erpnext/controllers/accounts_controller.py
index 243939b..1c086e9 100644
--- a/erpnext/controllers/accounts_controller.py
+++ b/erpnext/controllers/accounts_controller.py
@@ -828,8 +828,14 @@
role_allowed_to_over_bill = frappe.db.get_single_value('Accounts Settings', 'role_allowed_to_over_bill')
if total_billed_amt - max_allowed_amt > 0.01 and role_allowed_to_over_bill not in frappe.get_roles():
- frappe.throw(_("Cannot overbill for Item {0} in row {1} more than {2}. To allow over-billing, please set allowance in Accounts Settings")
- .format(item.item_code, item.idx, max_allowed_amt))
+ if self.doctype != "Purchase Invoice":
+ self.throw_overbill_exception(item, max_allowed_amt)
+ elif not cint(frappe.db.get_single_value("Buying Settings", "bill_for_rejected_quantity_in_purchase_invoice")):
+ self.throw_overbill_exception(item, max_allowed_amt)
+
+ def throw_overbill_exception(self, item, max_allowed_amt):
+ frappe.throw(_("Cannot overbill for Item {0} in row {1} more than {2}. To allow over-billing, please set allowance in Accounts Settings")
+ .format(item.item_code, item.idx, max_allowed_amt))
def get_company_default(self, fieldname):
from erpnext.accounts.utils import get_company_default
diff --git a/erpnext/controllers/buying_controller.py b/erpnext/controllers/buying_controller.py
index da81911..6a550e0 100644
--- a/erpnext/controllers/buying_controller.py
+++ b/erpnext/controllers/buying_controller.py
@@ -11,16 +11,17 @@
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 @@
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 @@
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 @@
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 @@
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 @@
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 @@
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 @@
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 @@
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 @@
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 @@
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
diff --git a/erpnext/controllers/queries.py b/erpnext/controllers/queries.py
index 7bd739a..2803193 100644
--- a/erpnext/controllers/queries.py
+++ b/erpnext/controllers/queries.py
@@ -19,7 +19,7 @@
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 @@
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,
diff --git a/erpnext/controllers/stock_controller.py b/erpnext/controllers/stock_controller.py
index 6a7c9e3..35097b9 100644
--- a/erpnext/controllers/stock_controller.py
+++ b/erpnext/controllers/stock_controller.py
@@ -501,7 +501,6 @@
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 @@
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 @@
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)
diff --git a/erpnext/controllers/subcontracting.py b/erpnext/controllers/subcontracting.py
new file mode 100644
index 0000000..36ae110
--- /dev/null
+++ b/erpnext/controllers/subcontracting.py
@@ -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"))
\ No newline at end of file
diff --git a/erpnext/controllers/taxes_and_totals.py b/erpnext/controllers/taxes_and_totals.py
index 2bb83ea..56da5b7 100644
--- a/erpnext/controllers/taxes_and_totals.py
+++ b/erpnext/controllers/taxes_and_totals.py
@@ -658,7 +658,13 @@
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)
diff --git a/erpnext/crm/report/first_response_time_for_opportunity/first_response_time_for_opportunity.js b/erpnext/crm/report/first_response_time_for_opportunity/first_response_time_for_opportunity.js
index 3f5c95a..fe5707a 100644
--- a/erpnext/crm/report/first_response_time_for_opportunity/first_response_time_for_opportunity.js
+++ b/erpnext/crm/report/first_response_time_for_opportunity/first_response_time_for_opportunity.js
@@ -22,10 +22,10 @@
get_chart_data: function (_columns, result) {
return {
data: {
- labels: result.map(d => d[0]),
+ labels: result.map(d => d.creation_date),
datasets: [{
name: "First Response Time",
- values: result.map(d => d[1])
+ values: result.map(d => d.first_response_time)
}]
},
type: "line",
@@ -35,8 +35,7 @@
hide_days: 0,
hide_seconds: 0
};
- value = frappe.utils.get_formatted_duration(d, duration_options);
- return value;
+ return frappe.utils.get_formatted_duration(d, duration_options);
}
}
}
diff --git a/erpnext/hr/doctype/employee/employee.json b/erpnext/hr/doctype/employee/employee.json
index 5442ed5..d592a9c 100644
--- a/erpnext/hr/doctype/employee/employee.json
+++ b/erpnext/hr/doctype/employee/employee.json
@@ -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",
diff --git a/erpnext/hr/doctype/employee/employee.py b/erpnext/hr/doctype/employee/employee.py
index bc56942..fa017d9 100755
--- a/erpnext/hr/doctype/employee/employee.py
+++ b/erpnext/hr/doctype/employee/employee.py
@@ -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.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 @@
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()
diff --git a/erpnext/hr/doctype/employee/employee_dashboard.py b/erpnext/hr/doctype/employee/employee_dashboard.py
index 285374d..e853bee 100644
--- a/erpnext/hr/doctype/employee/employee_dashboard.py
+++ b/erpnext/hr/doctype/employee/employee_dashboard.py
@@ -7,7 +7,8 @@
'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 @@
},
{
'label': _('Lifecycle'),
- 'items': ['Employee Transfer', 'Employee Promotion', 'Employee Separation']
+ 'items': ['Employee Transfer', 'Employee Promotion', 'Employee Separation', 'Employee Grievance']
},
{
'label': _('Shift'),
diff --git a/erpnext/hr/doctype/employee/employee_list.js b/erpnext/hr/doctype/employee/employee_list.js
index 6679e31..d37e149 100644
--- a/erpnext/hr/doctype/employee/employee_list.js
+++ b/erpnext/hr/doctype/employee/employee_list.js
@@ -3,7 +3,7 @@
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;
}
};
diff --git a/erpnext/hr/doctype/employee_grievance/__init__.py b/erpnext/hr/doctype/employee_grievance/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/erpnext/hr/doctype/employee_grievance/__init__.py
diff --git a/erpnext/hr/doctype/employee_grievance/employee_grievance.js b/erpnext/hr/doctype/employee_grievance/employee_grievance.js
new file mode 100644
index 0000000..25c5bad
--- /dev/null
+++ b/erpnext/hr/doctype/employee_grievance/employee_grievance.js
@@ -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
+ };
+ });
+ },
+});
diff --git a/erpnext/hr/doctype/employee_grievance/employee_grievance.json b/erpnext/hr/doctype/employee_grievance/employee_grievance.json
new file mode 100644
index 0000000..5a91856
--- /dev/null
+++ b/erpnext/hr/doctype/employee_grievance/employee_grievance.json
@@ -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
+}
\ No newline at end of file
diff --git a/erpnext/hr/doctype/employee_grievance/employee_grievance.py b/erpnext/hr/doctype/employee_grievance/employee_grievance.py
new file mode 100644
index 0000000..503b5ea
--- /dev/null
+++ b/erpnext/hr/doctype/employee_grievance/employee_grievance.py
@@ -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"))
+ )
+
diff --git a/erpnext/hr/doctype/employee_grievance/employee_grievance_list.js b/erpnext/hr/doctype/employee_grievance/employee_grievance_list.js
new file mode 100644
index 0000000..fc08e21
--- /dev/null
+++ b/erpnext/hr/doctype/employee_grievance/employee_grievance_list.js
@@ -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];
+ }
+};
\ No newline at end of file
diff --git a/erpnext/hr/doctype/employee_grievance/test_employee_grievance.py b/erpnext/hr/doctype/employee_grievance/test_employee_grievance.py
new file mode 100644
index 0000000..a615b20
--- /dev/null
+++ b/erpnext/hr/doctype/employee_grievance/test_employee_grievance.py
@@ -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
+
diff --git a/erpnext/hr/doctype/grievance_type/__init__.py b/erpnext/hr/doctype/grievance_type/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/erpnext/hr/doctype/grievance_type/__init__.py
diff --git a/erpnext/hr/doctype/grievance_type/grievance_type.js b/erpnext/hr/doctype/grievance_type/grievance_type.js
new file mode 100644
index 0000000..425f2fd
--- /dev/null
+++ b/erpnext/hr/doctype/grievance_type/grievance_type.js
@@ -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) {
+
+ // }
+});
diff --git a/erpnext/hr/doctype/grievance_type/grievance_type.json b/erpnext/hr/doctype/grievance_type/grievance_type.json
new file mode 100644
index 0000000..1dce00a
--- /dev/null
+++ b/erpnext/hr/doctype/grievance_type/grievance_type.json
@@ -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"
+}
\ No newline at end of file
diff --git a/erpnext/hr/doctype/grievance_type/grievance_type.py b/erpnext/hr/doctype/grievance_type/grievance_type.py
new file mode 100644
index 0000000..618cf0a
--- /dev/null
+++ b/erpnext/hr/doctype/grievance_type/grievance_type.py
@@ -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
diff --git a/erpnext/hr/doctype/grievance_type/test_grievance_type.py b/erpnext/hr/doctype/grievance_type/test_grievance_type.py
new file mode 100644
index 0000000..a02a34d
--- /dev/null
+++ b/erpnext/hr/doctype/grievance_type/test_grievance_type.py
@@ -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
diff --git a/erpnext/hr/doctype/job_applicant/job_applicant_list.js b/erpnext/hr/doctype/job_applicant/job_applicant_list.js
index 3b9141b..2ad0d59 100644
--- a/erpnext/hr/doctype/job_applicant/job_applicant_list.js
+++ b/erpnext/hr/doctype/job_applicant/job_applicant_list.js
@@ -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];
diff --git a/erpnext/hr/doctype/staffing_plan/staffing_plan.py b/erpnext/hr/doctype/staffing_plan/staffing_plan.py
index 533149a..e6c783a 100644
--- a/erpnext/hr/doctype/staffing_plan/staffing_plan.py
+++ b/erpnext/hr/doctype/staffing_plan/staffing_plan.py
@@ -41,7 +41,7 @@
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 @@
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 @@
(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 @@
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 @@
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
\ No newline at end of file
+ return staffing_plan if staffing_plan else None
diff --git a/erpnext/hr/workspace/hr/hr.json b/erpnext/hr/workspace/hr/hr.json
index c5201c2..4500ba4 100644
--- a/erpnext/hr/workspace/hr/hr.json
+++ b/erpnext/hr/workspace/hr/hr.json
@@ -154,6 +154,24 @@
"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,
"is_query_report": 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",
diff --git a/erpnext/loan_management/doctype/loan/loan.py b/erpnext/loan_management/doctype/loan/loan.py
index 69d11a8..ff7fbbd 100644
--- a/erpnext/loan_management/doctype/loan/loan.py
+++ b/erpnext/loan_management/doctype/loan/loan.py
@@ -60,8 +60,9 @@
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 @@
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',
diff --git a/erpnext/loan_management/doctype/loan/test_loan.py b/erpnext/loan_management/doctype/loan/test_loan.py
index fa4707c..314f58d 100644
--- a/erpnext/loan_management/doctype/loan/test_loan.py
+++ b/erpnext/loan_management/doctype/loan/test_loan.py
@@ -49,7 +49,11 @@
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 @@
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 @@
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 @@
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 @@
for pledge in proposed_pledges:
loan_application.append('proposed_pledges', pledge)
+ if do_not_save:
+ return loan_application
+
loan_application.save()
loan_application.submit()
diff --git a/erpnext/loan_management/doctype/loan_application/loan_application.py b/erpnext/loan_management/doctype/loan_application/loan_application.py
index 9c0147e..d8f3577 100644
--- a/erpnext/loan_management/doctype/loan_application/loan_application.py
+++ b/erpnext/loan_management/doctype/loan_application/loan_application.py
@@ -46,9 +46,11 @@
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)))
diff --git a/erpnext/loan_management/doctype/loan_repayment/loan_repayment.py b/erpnext/loan_management/doctype/loan_repayment/loan_repayment.py
index 3d99b1f..b8b1a40 100644
--- a/erpnext/loan_management/doctype/loan_repayment/loan_repayment.py
+++ b/erpnext/loan_management/doctype/loan_repayment/loan_repayment.py
@@ -235,70 +235,71 @@
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):
diff --git a/erpnext/manufacturing/doctype/bom/bom.py b/erpnext/manufacturing/doctype/bom/bom.py
index d1f6385..3f109d9 100644
--- a/erpnext/manufacturing/doctype/bom/bom.py
+++ b/erpnext/manufacturing/doctype/bom/bom.py
@@ -81,7 +81,7 @@
self.validate_operations()
self.calculate_cost()
self.update_stock_qty()
- self.update_cost(update_parent=False, from_child_bom=True, save=False)
+ self.update_cost(update_parent=False, from_child_bom=True, update_hour_rate = False, save=False)
def get_context(self, context):
context.parents = [{'name': 'boms', 'title': _('All BOMs') }]
@@ -213,7 +213,7 @@
return flt(rate) * flt(self.plc_conversion_rate or 1) / (self.conversion_rate or 1)
@frappe.whitelist()
- def update_cost(self, update_parent=True, from_child_bom=False, save=True):
+ def update_cost(self, update_parent=True, from_child_bom=False, update_hour_rate = True, save=True):
if self.docstatus == 2:
return
@@ -242,7 +242,7 @@
if self.docstatus == 1:
self.flags.ignore_validate_update_after_submit = True
- self.calculate_cost()
+ self.calculate_cost(update_hour_rate)
if save:
self.db_update()
@@ -403,32 +403,47 @@
bom_list.reverse()
return bom_list
- def calculate_cost(self):
+ def calculate_cost(self, update_hour_rate = False):
"""Calculate bom totals"""
- self.calculate_op_cost()
+ self.calculate_op_cost(update_hour_rate)
self.calculate_rm_cost()
self.calculate_sm_cost()
self.total_cost = self.operating_cost + self.raw_material_cost - self.scrap_material_cost
self.base_total_cost = self.base_operating_cost + self.base_raw_material_cost - self.base_scrap_material_cost
- def calculate_op_cost(self):
+ def calculate_op_cost(self, update_hour_rate = False):
"""Update workstation rate and calculates totals"""
self.operating_cost = 0
self.base_operating_cost = 0
for d in self.get('operations'):
if d.workstation:
- if not d.hour_rate:
- hour_rate = flt(frappe.db.get_value("Workstation", d.workstation, "hour_rate"))
- d.hour_rate = hour_rate / flt(self.conversion_rate) if self.conversion_rate else hour_rate
-
- if d.hour_rate and d.time_in_mins:
- d.base_hour_rate = flt(d.hour_rate) * flt(self.conversion_rate)
- d.operating_cost = flt(d.hour_rate) * flt(d.time_in_mins) / 60.0
- d.base_operating_cost = flt(d.operating_cost) * flt(self.conversion_rate)
+ self.update_rate_and_time(d, update_hour_rate)
self.operating_cost += flt(d.operating_cost)
self.base_operating_cost += flt(d.base_operating_cost)
+ def update_rate_and_time(self, row, update_hour_rate = False):
+ if not row.hour_rate or update_hour_rate:
+ hour_rate = flt(frappe.get_cached_value("Workstation", row.workstation, "hour_rate"))
+ row.hour_rate = (hour_rate / flt(self.conversion_rate)
+ if self.conversion_rate and hour_rate else hour_rate)
+
+ if self.routing:
+ row.time_in_mins = flt(frappe.db.get_value("BOM Operation", {
+ "workstation": row.workstation,
+ "operation": row.operation,
+ "sequence_id": row.sequence_id,
+ "parent": self.routing
+ }, ["time_in_mins"]))
+
+ if row.hour_rate and row.time_in_mins:
+ row.base_hour_rate = flt(row.hour_rate) * flt(self.conversion_rate)
+ row.operating_cost = flt(row.hour_rate) * flt(row.time_in_mins) / 60.0
+ row.base_operating_cost = flt(row.operating_cost) * flt(self.conversion_rate)
+
+ if update_hour_rate:
+ row.db_update()
+
def calculate_rm_cost(self):
"""Fetch RM rate as per today's valuation rate and calculate totals"""
total_rm_cost = 0
@@ -975,7 +990,7 @@
if filters and filters.get("is_stock_item"):
query_filters["is_stock_item"] = 1
-
+
return frappe.get_all("Item",
fields = fields, filters=query_filters,
or_filters = or_cond_filters, order_by=order_by,
diff --git a/erpnext/manufacturing/doctype/bom/test_bom.py b/erpnext/manufacturing/doctype/bom/test_bom.py
index e1cca9e..1f443fb 100644
--- a/erpnext/manufacturing/doctype/bom/test_bom.py
+++ b/erpnext/manufacturing/doctype/bom/test_bom.py
@@ -12,6 +12,7 @@
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')
@@ -122,7 +123,7 @@
bom.items[0].conversion_factor = 5
bom.insert()
- bom.update_cost()
+ bom.update_cost(update_hour_rate = False)
# test amounts in selected currency
self.assertEqual(bom.items[0].rate, 300)
@@ -160,6 +161,7 @@
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, {
diff --git a/erpnext/manufacturing/doctype/production_plan/production_plan.js b/erpnext/manufacturing/doctype/production_plan/production_plan.js
index 64d5841..450aa04 100644
--- a/erpnext/manufacturing/doctype/production_plan/production_plan.js
+++ b/erpnext/manufacturing/doctype/production_plan/production_plan.js
@@ -306,8 +306,25 @@
},
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) {
diff --git a/erpnext/manufacturing/doctype/production_plan/production_plan.py b/erpnext/manufacturing/doctype/production_plan/production_plan.py
index 46e0476..0ede1bd 100644
--- a/erpnext/manufacturing/doctype/production_plan/production_plan.py
+++ b/erpnext/manufacturing/doctype/production_plan/production_plan.py
@@ -98,7 +98,7 @@
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 @@
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 @@
}
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 @@
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 @@
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 @@
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 @@
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 @@
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)
diff --git a/erpnext/manufacturing/doctype/routing/routing.py b/erpnext/manufacturing/doctype/routing/routing.py
index 8312d74..ece0db7 100644
--- a/erpnext/manufacturing/doctype/routing/routing.py
+++ b/erpnext/manufacturing/doctype/routing/routing.py
@@ -4,14 +4,24 @@
from __future__ import unicode_literals
import frappe
-from frappe.utils import cint
+from frappe.utils import cint, flt
from frappe import _
from frappe.model.document import Document
class Routing(Document):
def validate(self):
+ self.calculate_operating_cost()
self.set_routing_id()
+ def on_update(self):
+ self.calculate_operating_cost()
+
+ def calculate_operating_cost(self):
+ for operation in self.operations:
+ if not operation.hour_rate:
+ operation.hour_rate = frappe.db.get_value("Workstation", operation.workstation, 'hour_rate')
+ operation.operating_cost = flt(flt(operation.hour_rate) * flt(operation.time_in_mins) / 60, 2)
+
def set_routing_id(self):
sequence_id = 0
for row in self.operations:
@@ -21,4 +31,4 @@
frappe.throw(_("At row #{0}: the sequence id {1} cannot be less than previous row sequence id {2}")
.format(row.idx, row.sequence_id, sequence_id))
- sequence_id = row.sequence_id
\ No newline at end of file
+ sequence_id = row.sequence_id
diff --git a/erpnext/manufacturing/doctype/routing/test_routing.py b/erpnext/manufacturing/doctype/routing/test_routing.py
index 6a38dcf..92f2694 100644
--- a/erpnext/manufacturing/doctype/routing/test_routing.py
+++ b/erpnext/manufacturing/doctype/routing/test_routing.py
@@ -7,9 +7,7 @@
import frappe
from frappe.test_runner import make_test_records
from erpnext.stock.doctype.item.test_item import make_item
-from erpnext.manufacturing.doctype.operation.test_operation import make_operation
from erpnext.manufacturing.doctype.job_card.job_card import OperationSequenceError
-from erpnext.manufacturing.doctype.workstation.test_workstation import make_workstation
from erpnext.manufacturing.doctype.work_order.test_work_order import make_wo_order_test_record
class TestRouting(unittest.TestCase):
@@ -48,7 +46,53 @@
wo_doc.cancel()
wo_doc.delete()
+ def test_update_bom_operation_time(self):
+ operations = [
+ {
+ "operation": "Test Operation A",
+ "workstation": "_Test Workstation A",
+ "hour_rate_rent": 300,
+ "hour_rate_labour": 750 ,
+ "time_in_mins": 30
+ },
+ {
+ "operation": "Test Operation B",
+ "workstation": "_Test Workstation B",
+ "hour_rate_labour": 200,
+ "hour_rate_rent": 1000,
+ "time_in_mins": 20
+ }
+ ]
+
+ test_routing_operations = [
+ {
+ "operation": "Test Operation A",
+ "workstation": "_Test Workstation A",
+ "time_in_mins": 30
+ },
+ {
+ "operation": "Test Operation B",
+ "workstation": "_Test Workstation A",
+ "time_in_mins": 20
+ }
+ ]
+ setup_operations(operations)
+ routing_doc = create_routing(routing_name="Routing Test", operations=test_routing_operations)
+ bom_doc = setup_bom(item_code="_Testing Item", routing=routing_doc.name, currency = 'INR')
+ self.assertEqual(routing_doc.operations[0].time_in_mins, 30)
+ self.assertEqual(routing_doc.operations[1].time_in_mins, 20)
+ routing_doc.operations[0].time_in_mins = 90
+ routing_doc.operations[1].time_in_mins = 42.2
+ routing_doc.save()
+ bom_doc.update_cost()
+ bom_doc.reload()
+ self.assertEqual(bom_doc.operations[0].time_in_mins, 90)
+ self.assertEqual(bom_doc.operations[1].time_in_mins, 42.2)
+
+
def setup_operations(rows):
+ from erpnext.manufacturing.doctype.workstation.test_workstation import make_workstation
+ from erpnext.manufacturing.doctype.operation.test_operation import make_operation
for row in rows:
make_workstation(row)
make_operation(row)
@@ -61,12 +105,14 @@
if not args.do_not_save:
try:
- for operation in args.operations:
- doc.append("operations", operation)
-
doc.insert()
except frappe.DuplicateEntryError:
doc = frappe.get_doc("Routing", args.routing_name)
+ doc.delete_key('operations')
+ for operation in args.operations:
+ doc.append("operations", operation)
+
+ doc.save()
return doc
@@ -91,7 +137,7 @@
name = frappe.db.get_value('BOM', {'item': args.item_code}, 'name')
if not name:
bom_doc = make_bom(item = args.item_code, raw_materials = args.get("raw_materials"),
- routing = args.routing, with_operations=1)
+ routing = args.routing, with_operations=1, currency = args.currency)
else:
bom_doc = frappe.get_doc("BOM", name)
diff --git a/erpnext/manufacturing/doctype/work_order/work_order.js b/erpnext/manufacturing/doctype/work_order/work_order.js
index 3e5a72d..8088d93 100644
--- a/erpnext/manufacturing/doctype/work_order/work_order.js
+++ b/erpnext/manufacturing/doctype/work_order/work_order.js
@@ -704,6 +704,8 @@
stop_work_order: function(frm, status) {
frappe.call({
method: "erpnext.manufacturing.doctype.work_order.work_order.stop_unstop",
+ freeze: true,
+ freeze_message: __("Updating Work Order status"),
args: {
work_order: frm.doc.name,
status: status
diff --git a/erpnext/manufacturing/doctype/workstation/test_workstation.py b/erpnext/manufacturing/doctype/workstation/test_workstation.py
index c6699be..9b73aca 100644
--- a/erpnext/manufacturing/doctype/workstation/test_workstation.py
+++ b/erpnext/manufacturing/doctype/workstation/test_workstation.py
@@ -1,16 +1,19 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors and Contributors
# See license.txt
from __future__ import unicode_literals
+from erpnext.manufacturing.doctype.operation.test_operation import make_operation
import frappe
import unittest
from erpnext.manufacturing.doctype.workstation.workstation import check_if_within_operating_hours, NotInWorkingHoursError, WorkstationHolidayError
+from erpnext.manufacturing.doctype.routing.test_routing import setup_bom, create_routing
+from frappe.test_runner import make_test_records
test_dependencies = ["Warehouse"]
test_records = frappe.get_test_records('Workstation')
+make_test_records('Workstation')
class TestWorkstation(unittest.TestCase):
-
def test_validate_timings(self):
check_if_within_operating_hours("_Test Workstation 1", "Operation 1", "2013-02-02 11:00:00", "2013-02-02 19:00:00")
check_if_within_operating_hours("_Test Workstation 1", "Operation 1", "2013-02-02 10:00:00", "2013-02-02 20:00:00")
@@ -21,6 +24,58 @@
self.assertRaises(WorkstationHolidayError, check_if_within_operating_hours,
"_Test Workstation 1", "Operation 1", "2013-02-01 10:00:00", "2013-02-02 20:00:00")
+ def test_update_bom_operation_rate(self):
+ operations = [
+ {
+ "operation": "Test Operation A",
+ "workstation": "_Test Workstation A",
+ "hour_rate_rent": 300,
+ "time_in_mins": 60
+ },
+ {
+ "operation": "Test Operation B",
+ "workstation": "_Test Workstation B",
+ "hour_rate_rent": 1000,
+ "time_in_mins": 60
+ }
+ ]
+
+ for row in operations:
+ make_workstation(row)
+ make_operation(row)
+
+ test_routing_operations = [
+ {
+ "operation": "Test Operation A",
+ "workstation": "_Test Workstation A",
+ "time_in_mins": 60
+ },
+ {
+ "operation": "Test Operation B",
+ "workstation": "_Test Workstation A",
+ "time_in_mins": 60
+ }
+ ]
+ routing_doc = create_routing(routing_name = "Routing Test", operations=test_routing_operations)
+ bom_doc = setup_bom(item_code="_Testing Item", routing=routing_doc.name, currency="INR")
+ w1 = frappe.get_doc("Workstation", "_Test Workstation A")
+ #resets values
+ w1.hour_rate_rent = 300
+ w1.hour_rate_labour = 0
+ w1.save()
+ bom_doc.update_cost()
+ bom_doc.reload()
+ self.assertEqual(w1.hour_rate, 300)
+ self.assertEqual(bom_doc.operations[0].hour_rate, 300)
+ w1.hour_rate_rent = 250
+ w1.save()
+ #updating after setting new rates in workstations
+ bom_doc.update_cost()
+ bom_doc.reload()
+ self.assertEqual(w1.hour_rate, 250)
+ self.assertEqual(bom_doc.operations[0].hour_rate, 250)
+ self.assertEqual(bom_doc.operations[1].hour_rate, 250)
+
def make_workstation(*args, **kwargs):
args = args if args else kwargs
if isinstance(args, tuple):
@@ -34,9 +89,10 @@
"doctype": "Workstation",
"workstation_name": workstation_name
})
-
+ doc.hour_rate_rent = args.get("hour_rate_rent")
+ doc.hour_rate_labour = args.get("hour_rate_labour")
doc.insert()
return doc
except frappe.DuplicateEntryError:
- return frappe.get_doc("Workstation", workstation_name)
\ No newline at end of file
+ return frappe.get_doc("Workstation", workstation_name)
diff --git a/erpnext/manufacturing/doctype/workstation/workstation.py b/erpnext/manufacturing/doctype/workstation/workstation.py
index 3512e59..f4483f7 100644
--- a/erpnext/manufacturing/doctype/workstation/workstation.py
+++ b/erpnext/manufacturing/doctype/workstation/workstation.py
@@ -39,7 +39,8 @@
def update_bom_operation(self):
bom_list = frappe.db.sql("""select DISTINCT parent from `tabBOM Operation`
- where workstation = %s""", self.name)
+ where workstation = %s and parenttype = 'routing' """, self.name)
+
for bom_no in bom_list:
frappe.db.sql("""update `tabBOM Operation` set hour_rate = %s
where parent = %s and workstation = %s""",
@@ -71,7 +72,7 @@
def is_within_operating_hours(workstation, operation, from_datetime, to_datetime):
operation_length = time_diff_in_seconds(to_datetime, from_datetime)
workstation = frappe.get_doc("Workstation", workstation)
-
+
if not workstation.working_hours:
return
diff --git a/erpnext/patches.txt b/erpnext/patches.txt
index ed6fefd..dd0e33b 100644
--- a/erpnext/patches.txt
+++ b/erpnext/patches.txt
@@ -288,3 +288,4 @@
erpnext.patches.v13_0.update_timesheet_changes
erpnext.patches.v13_0.set_training_event_attendance
erpnext.patches.v13_0.rename_issue_status_hold_to_on_hold
+erpnext.patches.v13_0.bill_for_rejected_quantity_in_purchase_invoice
\ No newline at end of file
diff --git a/erpnext/patches/v13_0/bill_for_rejected_quantity_in_purchase_invoice.py b/erpnext/patches/v13_0/bill_for_rejected_quantity_in_purchase_invoice.py
new file mode 100644
index 0000000..be85cfd
--- /dev/null
+++ b/erpnext/patches/v13_0/bill_for_rejected_quantity_in_purchase_invoice.py
@@ -0,0 +1,8 @@
+from __future__ import unicode_literals
+import frappe
+
+def execute():
+ frappe.reload_doctype("Buying Settings")
+ buying_settings = frappe.get_single("Buying Settings")
+ buying_settings.bill_for_rejected_quantity_in_purchase_invoice = 0
+ buying_settings.save()
\ No newline at end of file
diff --git a/erpnext/payroll/doctype/additional_salary/additional_salary.js b/erpnext/payroll/doctype/additional_salary/additional_salary.js
index d1ed91f..24ffce5 100644
--- a/erpnext/payroll/doctype/additional_salary/additional_salary.js
+++ b/erpnext/payroll/doctype/additional_salary/additional_salary.js
@@ -12,8 +12,12 @@
}
};
});
+ },
- frm.trigger('set_earning_component');
+ onload: function(frm) {
+ if (frm.doc.type) {
+ frm.trigger('set_component_query');
+ }
},
employee: function(frm) {
@@ -46,14 +50,19 @@
},
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
};
});
},
diff --git a/erpnext/payroll/doctype/payroll_entry/payroll_entry.py b/erpnext/payroll/doctype/payroll_entry/payroll_entry.py
index 7a70679..e71d81f 100644
--- a/erpnext/payroll/doctype/payroll_entry/payroll_entry.py
+++ b/erpnext/payroll/doctype/payroll_entry/payroll_entry.py
@@ -11,6 +11,7 @@
from erpnext.accounts.utils import get_fiscal_year
from erpnext.hr.doctype.employee.employee import get_holiday_list_for_employee
from frappe.desk.reportview import get_match_cond, get_filters_cond
+from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import get_accounting_dimensions
class PayrollEntry(Document):
def onload(self):
@@ -41,7 +42,7 @@
emp_with_sal_slip.append(employee_details.employee)
if len(emp_with_sal_slip):
- frappe.throw(_("Salary Slip already exists for {0} ").format(comma_and(emp_with_sal_slip)))
+ frappe.throw(_("Salary Slip already exists for {0}").format(comma_and(emp_with_sal_slip)))
def on_cancel(self):
frappe.delete_doc("Salary Slip", frappe.db.sql_list("""select name from `tabSalary Slip`
@@ -211,7 +212,7 @@
return account_dict
def make_accrual_jv_entry(self):
- self.check_permission('write')
+ self.check_permission("write")
earnings = self.get_salary_component_total(component_type = "earnings") or {}
deductions = self.get_salary_component_total(component_type = "deductions") or {}
payroll_payable_account = self.payroll_payable_account
@@ -219,12 +220,13 @@
precision = frappe.get_precision("Journal Entry Account", "debit_in_account_currency")
if earnings or deductions:
- journal_entry = frappe.new_doc('Journal Entry')
- journal_entry.voucher_type = 'Journal Entry'
- journal_entry.user_remark = _('Accrual Journal Entry for salaries from {0} to {1}')\
+ journal_entry = frappe.new_doc("Journal Entry")
+ journal_entry.voucher_type = "Journal Entry"
+ journal_entry.user_remark = _("Accrual Journal Entry for salaries from {0} to {1}")\
.format(self.start_date, self.end_date)
journal_entry.company = self.company
journal_entry.posting_date = self.posting_date
+ accounting_dimensions = get_accounting_dimensions() or []
accounts = []
currencies = []
@@ -236,37 +238,34 @@
for acc_cc, amount in earnings.items():
exchange_rate, amt = self.get_amount_and_exchange_rate_for_journal_entry(acc_cc[0], amount, company_currency, currencies)
payable_amount += flt(amount, precision)
- accounts.append({
+ accounts.append(self.update_accounting_dimensions({
"account": acc_cc[0],
"debit_in_account_currency": flt(amt, precision),
"exchange_rate": flt(exchange_rate),
- "party_type": '',
"cost_center": acc_cc[1] or self.cost_center,
"project": self.project
- })
+ }, accounting_dimensions))
# Deductions
for acc_cc, amount in deductions.items():
exchange_rate, amt = self.get_amount_and_exchange_rate_for_journal_entry(acc_cc[0], amount, company_currency, currencies)
payable_amount -= flt(amount, precision)
- accounts.append({
+ accounts.append(self.update_accounting_dimensions({
"account": acc_cc[0],
"credit_in_account_currency": flt(amt, precision),
"exchange_rate": flt(exchange_rate),
"cost_center": acc_cc[1] or self.cost_center,
- "party_type": '',
"project": self.project
- })
+ }, accounting_dimensions))
# Payable amount
exchange_rate, payable_amt = self.get_amount_and_exchange_rate_for_journal_entry(payroll_payable_account, payable_amount, company_currency, currencies)
- accounts.append({
+ accounts.append(self.update_accounting_dimensions({
"account": payroll_payable_account,
"credit_in_account_currency": flt(payable_amt, precision),
"exchange_rate": flt(exchange_rate),
- "party_type": '',
"cost_center": self.cost_center
- })
+ }, accounting_dimensions))
journal_entry.set("accounts", accounts)
if len(currencies) > 1:
@@ -286,6 +285,12 @@
return jv_name
+ def update_accounting_dimensions(self, row, accounting_dimensions):
+ for dimension in accounting_dimensions:
+ row.update({dimension: self.get(dimension)})
+
+ return row
+
def get_amount_and_exchange_rate_for_journal_entry(self, account, amount, company_currency, currencies):
conversion_rate = 1
exchange_rate = self.exchange_rate
diff --git a/erpnext/payroll/doctype/salary_slip/test_salary_slip.py b/erpnext/payroll/doctype/salary_slip/test_salary_slip.py
index 9e7db97..ce88cc3 100644
--- a/erpnext/payroll/doctype/salary_slip/test_salary_slip.py
+++ b/erpnext/payroll/doctype/salary_slip/test_salary_slip.py
@@ -481,6 +481,7 @@
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})})
diff --git a/erpnext/payroll/doctype/salary_structure/test_salary_structure.py b/erpnext/payroll/doctype/salary_structure/test_salary_structure.py
index dce6b7a..e7d123c 100644
--- a/erpnext/payroll/doctype/salary_structure/test_salary_structure.py
+++ b/erpnext/payroll/doctype/salary_structure/test_salary_structure.py
@@ -124,8 +124,8 @@
"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
diff --git a/erpnext/portal/product_configurator/test_product_configurator.py b/erpnext/portal/product_configurator/test_product_configurator.py
index 3521e7e..daaba67 100644
--- a/erpnext/portal/product_configurator/test_product_configurator.py
+++ b/erpnext/portal/product_configurator/test_product_configurator.py
@@ -43,6 +43,30 @@
"show_variant_in_website": 1
}).insert()
+ def create_regular_web_item(self, name, item_group=None):
+ if not frappe.db.exists('Item', name):
+ doc = frappe.get_doc({
+ "description": name,
+ "item_code": name,
+ "item_name": name,
+ "doctype": "Item",
+ "is_stock_item": 1,
+ "item_group": item_group or "_Test Item Group",
+ "stock_uom": "_Test UOM",
+ "item_defaults": [{
+ "company": "_Test Company",
+ "default_warehouse": "_Test Warehouse - _TC",
+ "expense_account": "_Test Account Cost for Goods Sold - _TC",
+ "buying_cost_center": "_Test Cost Center - _TC",
+ "selling_cost_center": "_Test Cost Center - _TC",
+ "income_account": "Sales - _TC"
+ }],
+ "show_in_website": 1
+ }).insert()
+ else:
+ doc = frappe.get_doc("Item", name)
+ return doc
+
def test_product_list(self):
template_items = frappe.get_all('Item', {'show_in_website': 1})
variant_items = frappe.get_all('Item', {'show_variant_in_website': 1})
@@ -79,3 +103,42 @@
'Test Size': ['2XL']
})
self.assertEqual(len(items), 1)
+
+ def test_products_in_multiple_item_groups(self):
+ """Check if product is visible on multiple item group pages barring its own."""
+ from erpnext.shopping_cart.product_query import ProductQuery
+
+ if not frappe.db.exists("Item Group", {"name": "Tech Items"}):
+ item_group_doc = frappe.get_doc({
+ "doctype": "Item Group",
+ "item_group_name": "Tech Items",
+ "parent_item_group": "All Item Groups",
+ "show_in_website": 1
+ }).insert()
+ else:
+ item_group_doc = frappe.get_doc("Item Group", "Tech Items")
+
+ doc = self.create_regular_web_item("Portal Item", item_group="Tech Items")
+ if not frappe.db.exists("Website Item Group", {"parent": "Portal Item"}):
+ doc.append("website_item_groups", {
+ "item_group": "_Test Item Group Desktops"
+ })
+ doc.save()
+
+ # check if item is visible in its own Item Group's page
+ engine = ProductQuery()
+ items = engine.query({}, {"item_group": "Tech Items"}, None, start=0, item_group="Tech Items")
+ self.assertEqual(len(items), 1)
+ self.assertEqual(items[0].item_code, "Portal Item")
+
+ # check if item is visible in configured foreign Item Group's page
+ engine = ProductQuery()
+ items = engine.query({}, {"item_group": "_Test Item Group Desktops"}, None, start=0, item_group="_Test Item Group Desktops")
+ item_codes = [row.item_code for row in items]
+
+ self.assertIn(len(items), [2, 3])
+ self.assertIn("Portal Item", item_codes)
+
+ # teardown
+ doc.delete()
+ item_group_doc.delete()
\ No newline at end of file
diff --git a/erpnext/projects/report/project_profitability/test_project_profitability.py b/erpnext/projects/report/project_profitability/test_project_profitability.py
index ea6bdb5..180926f 100644
--- a/erpnext/projects/report/project_profitability/test_project_profitability.py
+++ b/erpnext/projects/report/project_profitability/test_project_profitability.py
@@ -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 @@
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()
}
diff --git a/erpnext/public/js/controllers/buying.js b/erpnext/public/js/controllers/buying.js
index e7dcd41..5c9f5d7 100644
--- a/erpnext/public/js/controllers/buying.js
+++ b/erpnext/public/js/controllers/buying.js
@@ -122,9 +122,20 @@
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(){
diff --git a/erpnext/public/js/controllers/transaction.js b/erpnext/public/js/controllers/transaction.js
index 89fed3b..b3af3d6 100644
--- a/erpnext/public/js/controllers/transaction.js
+++ b/erpnext/public/js/controllers/transaction.js
@@ -387,7 +387,7 @@
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 @@
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);
@@ -864,9 +868,6 @@
}
- if (this.frm.doc.posting_date) var date = this.frm.doc.posting_date;
- else var date = this.frm.doc.transaction_date;
-
if (frappe.meta.get_docfield(this.frm.doctype, "shipping_address") &&
in_list(['Purchase Order', 'Purchase Receipt', 'Purchase Invoice'], this.frm.doctype)) {
erpnext.utils.get_shipping_address(this.frm, function(){
diff --git a/erpnext/public/js/utils/party.js b/erpnext/public/js/utils/party.js
index 808dd5a..a79eadc 100644
--- a/erpnext/public/js/utils/party.js
+++ b/erpnext/public/js/utils/party.js
@@ -274,9 +274,9 @@
return true;
}
-erpnext.utils.get_shipping_address = function(frm, callback){
+erpnext.utils.get_shipping_address = function(frm, callback) {
if (frm.doc.company) {
- if (!(frm.doc.inter_com_order_reference || frm.doc.internal_invoice_reference ||
+ if ((frm.doc.inter_company_order_reference || frm.doc.internal_invoice_reference ||
frm.doc.internal_order_reference)) {
if (callback) {
return callback();
diff --git a/erpnext/public/scss/shopping_cart.scss b/erpnext/public/scss/shopping_cart.scss
index 9402cf9..5962859 100644
--- a/erpnext/public/scss/shopping_cart.scss
+++ b/erpnext/public/scss/shopping_cart.scss
@@ -467,11 +467,15 @@
.btn-change-address {
color: var(--blue-500);
- box-shadow: none;
- border: 1px solid var(--blue-500);
}
}
+.btn-new-address:hover, .btn-change-address:hover {
+ box-shadow: none;
+ color: var(--blue-500) !important;
+ border: 1px solid var(--blue-500);
+}
+
.modal .address-card {
.card-body {
padding: var(--padding-sm);
diff --git a/erpnext/regional/report/gstr_1/gstr_1.py b/erpnext/regional/report/gstr_1/gstr_1.py
index 80e2d72..1096159 100644
--- a/erpnext/regional/report/gstr_1/gstr_1.py
+++ b/erpnext/regional/report/gstr_1/gstr_1.py
@@ -201,7 +201,7 @@
elif self.filters.get("type_of_business") == "EXPORT":
conditions += """ AND is_return !=1 and gst_category = 'Overseas' """
- conditions += " AND billing_address_gstin NOT IN %(company_gstins)s"
+ conditions += " AND IFNULL(billing_address_gstin, '') NOT IN %(company_gstins)s"
return conditions
diff --git a/erpnext/selling/doctype/product_bundle/product_bundle.py b/erpnext/selling/doctype/product_bundle/product_bundle.py
index d3281f7..ae3482f 100644
--- a/erpnext/selling/doctype/product_bundle/product_bundle.py
+++ b/erpnext/selling/doctype/product_bundle/product_bundle.py
@@ -4,6 +4,8 @@
from __future__ import unicode_literals
import frappe
+from frappe.utils import get_link_to_form
+
from frappe import _
from frappe.model.document import Document
@@ -18,6 +20,27 @@
from erpnext.utilities.transaction_base import validate_uom_is_integer
validate_uom_is_integer(self, "uom", "qty")
+ def on_trash(self):
+ linked_doctypes = ["Delivery Note", "Sales Invoice", "POS Invoice", "Purchase Receipt", "Purchase Invoice",
+ "Stock Entry", "Stock Reconciliation", "Sales Order", "Purchase Order", "Material Request"]
+
+ invoice_links = []
+ for doctype in linked_doctypes:
+ item_doctype = doctype + " Item"
+
+ if doctype == "Stock Entry":
+ item_doctype = doctype + " Detail"
+
+ invoices = frappe.db.get_all(item_doctype, {"item_code": self.new_item_code, "docstatus": 1}, ["parent"])
+
+ for invoice in invoices:
+ invoice_links.append(get_link_to_form(doctype, invoice['parent']))
+
+ if len(invoice_links):
+ frappe.throw(
+ "This Product Bundle is linked with {0}. You will have to cancel these documents in order to delete this Product Bundle"
+ .format(", ".join(invoice_links)), title=_("Not Allowed"))
+
def validate_main_item(self):
"""Validates, main Item is not a stock item"""
if frappe.db.get_value("Item", self.new_item_code, "is_stock_item"):
diff --git a/erpnext/selling/doctype/sales_order/sales_order.py b/erpnext/selling/doctype/sales_order/sales_order.py
index 551f715..41f57a3 100755
--- a/erpnext/selling/doctype/sales_order/sales_order.py
+++ b/erpnext/selling/doctype/sales_order/sales_order.py
@@ -233,7 +233,7 @@
# 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:
diff --git a/erpnext/selling/doctype/sales_order/test_sales_order.py b/erpnext/selling/doctype/sales_order/test_sales_order.py
index 9873710..974648d 100644
--- a/erpnext/selling/doctype/sales_order/test_sales_order.py
+++ b/erpnext/selling/doctype/sales_order/test_sales_order.py
@@ -1217,6 +1217,19 @@
# 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")
diff --git a/erpnext/selling/report/sales_order_analysis/sales_order_analysis.py b/erpnext/selling/report/sales_order_analysis/sales_order_analysis.py
index f5feb95..8cb2446 100644
--- a/erpnext/selling/report/sales_order_analysis/sales_order_analysis.py
+++ b/erpnext/selling/report/sales_order_analysis/sales_order_analysis.py
@@ -59,7 +59,7 @@
IF(so.status in ('Completed','To Bill'), 0, (SELECT delay_days)) as delay,
soi.qty, soi.delivered_qty,
(soi.qty - soi.delivered_qty) AS pending_qty,
- IFNULL(sii.qty, 0) as billed_qty,
+ IFNULL(SUM(sii.qty), 0) as billed_qty,
soi.base_amount as amount,
(soi.delivered_qty * soi.base_rate) as delivered_qty_amount,
(soi.billed_amt * IFNULL(so.conversion_rate, 1)) as billed_amount,
diff --git a/erpnext/setup/doctype/item_group/item_group.py b/erpnext/setup/doctype/item_group/item_group.py
index db31d6d..1c72ceb 100644
--- a/erpnext/setup/doctype/item_group/item_group.py
+++ b/erpnext/setup/doctype/item_group/item_group.py
@@ -91,7 +91,7 @@
field_filters['item_group'] = self.name
engine = ProductQuery()
- context.items = engine.query(attribute_filters, field_filters, search, start)
+ context.items = engine.query(attribute_filters, field_filters, search, start, item_group=self.name)
filter_engine = ProductFiltersBuilder(self.name)
diff --git a/erpnext/shopping_cart/filters.py b/erpnext/shopping_cart/filters.py
index 6c63d87..7dfa09e 100644
--- a/erpnext/shopping_cart/filters.py
+++ b/erpnext/shopping_cart/filters.py
@@ -22,12 +22,15 @@
filter_data = []
for df in fields:
- filters = {}
+ filters, or_filters = {}, []
if df.fieldtype == "Link":
if self.item_group:
- filters['item_group'] = self.item_group
+ or_filters.extend([
+ ["item_group", "=", self.item_group],
+ ["Website Item Group", "item_group", "=", self.item_group]
+ ])
- values = frappe.get_all("Item", fields=[df.fieldname], filters=filters, distinct="True", pluck=df.fieldname)
+ values = frappe.get_all("Item", fields=[df.fieldname], filters=filters, or_filters=or_filters, distinct="True", pluck=df.fieldname)
else:
doctype = df.get_link_doctype()
@@ -44,7 +47,9 @@
values = [d.name for d in frappe.get_all(doctype, filters)]
# Remove None
- values = values.remove(None) if None in values else values
+ if None in values:
+ values.remove(None)
+
if values:
filter_data.append([df, values])
@@ -61,14 +66,18 @@
for attr_doc in attribute_docs:
selected_attributes = []
for attr in attr_doc.item_attribute_values:
+ or_filters = []
filters= [
["Item Variant Attribute", "attribute", "=", attr.parent],
["Item Variant Attribute", "attribute_value", "=", attr.attribute_value]
]
if self.item_group:
- filters.append(["item_group", "=", self.item_group])
+ or_filters.extend([
+ ["item_group", "=", self.item_group],
+ ["Website Item Group", "item_group", "=", self.item_group]
+ ])
- if frappe.db.get_all("Item", filters, limit=1):
+ if frappe.db.get_all("Item", filters, or_filters=or_filters, limit=1):
selected_attributes.append(attr)
if selected_attributes:
diff --git a/erpnext/shopping_cart/product_query.py b/erpnext/shopping_cart/product_query.py
index 36d446e..d96d803 100644
--- a/erpnext/shopping_cart/product_query.py
+++ b/erpnext/shopping_cart/product_query.py
@@ -22,13 +22,14 @@
self.settings = frappe.get_doc("Products Settings")
self.cart_settings = frappe.get_doc("Shopping Cart Settings")
self.page_length = self.settings.products_per_page or 20
- self.fields = ['name', 'item_name', 'item_code', 'website_image', 'variant_of', 'has_variants', 'item_group', 'image', 'web_long_description', 'description', 'route']
+ self.fields = ['name', 'item_name', 'item_code', 'website_image', 'variant_of', 'has_variants',
+ 'item_group', 'image', 'web_long_description', 'description', 'route', 'weightage']
self.filters = []
self.or_filters = [['show_in_website', '=', 1]]
if not self.settings.get('hide_variants'):
self.or_filters.append(['show_variant_in_website', '=', 1])
- def query(self, attributes=None, fields=None, search_term=None, start=0):
+ def query(self, attributes=None, fields=None, search_term=None, start=0, item_group=None):
"""Summary
Args:
@@ -44,6 +45,15 @@
if search_term: self.build_search_filters(search_term)
result = []
+ website_item_groups = []
+
+ # if from item group page consider website item group table
+ if item_group:
+ website_item_groups = frappe.db.get_all(
+ "Item",
+ fields=self.fields + ["`tabWebsite Item Group`.parent as wig_parent"],
+ filters=[["Website Item Group", "item_group", "=", item_group]]
+ )
if attributes:
all_items = []
@@ -65,18 +75,33 @@
)
items_dict = {item.name: item for item in items}
- # TODO: Replace Variants by their parent templates
all_items.append(set(items_dict.keys()))
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
+ )
+
+ # Combine results having context of website item groups into item results
+ if item_group and website_item_groups:
+ items_list = {row.name for row in result}
+ for row in website_item_groups:
+ if row.wig_parent not in items_list:
+ result.append(row)
+
+ result = sorted(result, key=lambda x: x.get("weightage"), reverse=True)
for item in result:
product_info = get_product_info_for_website(item.item_code, skip_quotation_creation=True).get('product_info')
if product_info:
- item.formatted_price = product_info['price'].get('formatted_price') if product_info['price'] else None
+ item.formatted_price = (product_info.get('price') or {}).get('formatted_price')
return result
@@ -90,7 +115,16 @@
if not values:
continue
- if isinstance(values, list):
+ # handle multiselect fields in filter addition
+ meta = frappe.get_meta('Item', cached=True)
+ df = meta.get_field(field)
+ if df.fieldtype == 'Table MultiSelect':
+ child_doctype = df.options
+ child_meta = frappe.get_meta(child_doctype, cached=True)
+ fields = child_meta.get("fields")
+ if fields:
+ self.filters.append([child_doctype, fields[0].fieldname, 'IN', values])
+ elif isinstance(values, list):
# If value is a list use `IN` query
self.filters.append([field, 'IN', values])
else:
diff --git a/erpnext/stock/doctype/bin/bin.py b/erpnext/stock/doctype/bin/bin.py
index 0514bd2..4364201 100644
--- a/erpnext/stock/doctype/bin/bin.py
+++ b/erpnext/stock/doctype/bin/bin.py
@@ -54,7 +54,7 @@
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 @@
#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
diff --git a/erpnext/stock/doctype/delivery_note/delivery_note_list.js b/erpnext/stock/doctype/delivery_note/delivery_note_list.js
index f08125b..0402898 100644
--- a/erpnext/stock/doctype/delivery_note/delivery_note_list.js
+++ b/erpnext/stock/doctype/delivery_note/delivery_note_list.js
@@ -6,8 +6,8 @@
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) {
diff --git a/erpnext/stock/doctype/item_alternative/test_item_alternative.py b/erpnext/stock/doctype/item_alternative/test_item_alternative.py
index d5700fe..8f76844 100644
--- a/erpnext/stock/doctype/item_alternative/test_item_alternative.py
+++ b/erpnext/stock/doctype/item_alternative/test_item_alternative.py
@@ -18,6 +18,9 @@
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 @@
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',
diff --git a/erpnext/stock/doctype/material_request/material_request.js b/erpnext/stock/doctype/material_request/material_request.js
index 92c8d21..6e66f98 100644
--- a/erpnext/stock/doctype/material_request/material_request.js
+++ b/erpnext/stock/doctype/material_request/material_request.js
@@ -101,7 +101,8 @@
}
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'));
diff --git a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.json b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.json
index ad350d3..44fb736 100755
--- a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.json
+++ b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.json
@@ -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",
diff --git a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py
index 83ba324..e488b69 100644
--- a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py
+++ b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py
@@ -202,6 +202,7 @@
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 @@
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):
@@ -579,7 +581,6 @@
@frappe.whitelist()
def make_purchase_invoice(source_name, target_doc=None):
- from frappe.model.mapper import get_mapped_doc
from erpnext.accounts.party import get_payment_terms_template
doc = frappe.get_doc('Purchase Receipt', source_name)
@@ -599,11 +600,16 @@
def update_item(source_doc, target_doc, source_parent):
target_doc.qty, returned_qty = get_pending_qty(source_doc)
+ if frappe.db.get_single_value("Buying Settings", "bill_for_rejected_quantity_in_purchase_invoice"):
+ target_doc.rejected_qty = 0
target_doc.stock_qty = flt(target_doc.qty) * flt(target_doc.conversion_factor, target_doc.precision("conversion_factor"))
returned_qty_map[source_doc.name] = returned_qty
def get_pending_qty(item_row):
- pending_qty = item_row.qty - invoiced_qty_map.get(item_row.name, 0)
+ qty = item_row.qty
+ if frappe.db.get_single_value("Buying Settings", "bill_for_rejected_quantity_in_purchase_invoice"):
+ qty = item_row.received_qty
+ pending_qty = qty - invoiced_qty_map.get(item_row.name, 0)
returned_qty = flt(returned_qty_map.get(item_row.name, 0))
if returned_qty:
if returned_qty >= pending_qty:
diff --git a/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py b/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py
index 8d9b675..07c5da1 100644
--- a/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py
+++ b/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py
@@ -335,6 +335,10 @@
se2.cancel()
se3.cancel()
po.reload()
+ pr2.load_from_db()
+ pr2.cancel()
+
+ po.load_from_db()
po.cancel()
def test_serial_no_supplier(self):
@@ -417,11 +421,18 @@
self.assertEqual(return_pr_2.items[0].qty, -3)
# Make PI against unreturned amount
+ buying_settings = frappe.get_single("Buying Settings")
+ buying_settings.bill_for_rejected_quantity_in_purchase_invoice = 0
+ buying_settings.save()
+
pi = make_purchase_invoice(pr.name)
pi.submit()
self.assertEqual(pi.items[0].qty, 3)
+ buying_settings.bill_for_rejected_quantity_in_purchase_invoice = 1
+ buying_settings.save()
+
pr.load_from_db()
# PR should be completed on billing all unreturned amount
self.assertEqual(pr.items[0].billed_amt, 150)
diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.js b/erpnext/stock/doctype/stock_entry/stock_entry.js
index 1a25994..6708393 100644
--- a/erpnext/stock/doctype/stock_entry/stock_entry.js
+++ b/erpnext/stock/doctype/stock_entry/stock_entry.js
@@ -1079,6 +1079,10 @@
}
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
diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.json b/erpnext/stock/doctype/stock_entry/stock_entry.json
index a0b5457..523d332 100644
--- a/erpnext/stock/doctype/stock_entry/stock_entry.json
+++ b/erpnext/stock/doctype/stock_entry/stock_entry.json
@@ -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",
diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.py b/erpnext/stock/doctype/stock_entry/stock_entry.py
index 560ceaa..66f8b63 100644
--- a/erpnext/stock/doctype/stock_entry/stock_entry.py
+++ b/erpnext/stock/doctype/stock_entry/stock_entry.py
@@ -97,8 +97,7 @@
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 @@
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 @@
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 @@
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 @@
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 @@
.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 @@
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 @@
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
\ No newline at end of file
diff --git a/erpnext/stock/doctype/stock_ledger_entry/stock_ledger_entry.py b/erpnext/stock/doctype/stock_ledger_entry/stock_ledger_entry.py
index b0e7440..0febcb6 100644
--- a/erpnext/stock/doctype/stock_ledger_entry/stock_ledger_entry.py
+++ b/erpnext/stock/doctype/stock_ledger_entry/stock_ledger_entry.py
@@ -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 @@
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):
diff --git a/erpnext/stock/report/incorrect_balance_qty_after_transaction/__init__.py b/erpnext/stock/report/incorrect_balance_qty_after_transaction/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/erpnext/stock/report/incorrect_balance_qty_after_transaction/__init__.py
diff --git a/erpnext/stock/report/incorrect_balance_qty_after_transaction/incorrect_balance_qty_after_transaction.js b/erpnext/stock/report/incorrect_balance_qty_after_transaction/incorrect_balance_qty_after_transaction.js
new file mode 100644
index 0000000..bf11277
--- /dev/null
+++ b/erpnext/stock/report/incorrect_balance_qty_after_transaction/incorrect_balance_qty_after_transaction.js
@@ -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'
+ }
+ ]
+};
diff --git a/erpnext/stock/report/incorrect_balance_qty_after_transaction/incorrect_balance_qty_after_transaction.json b/erpnext/stock/report/incorrect_balance_qty_after_transaction/incorrect_balance_qty_after_transaction.json
new file mode 100644
index 0000000..a5815bc
--- /dev/null
+++ b/erpnext/stock/report/incorrect_balance_qty_after_transaction/incorrect_balance_qty_after_transaction.json
@@ -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"
+ }
+ ]
+}
\ No newline at end of file
diff --git a/erpnext/stock/report/incorrect_balance_qty_after_transaction/incorrect_balance_qty_after_transaction.py b/erpnext/stock/report/incorrect_balance_qty_after_transaction/incorrect_balance_qty_after_transaction.py
new file mode 100644
index 0000000..cf174c9
--- /dev/null
+++ b/erpnext/stock/report/incorrect_balance_qty_after_transaction/incorrect_balance_qty_after_transaction.py
@@ -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
+ }]
\ No newline at end of file
diff --git a/erpnext/stock/report/incorrect_serial_no_valuation/__init__.py b/erpnext/stock/report/incorrect_serial_no_valuation/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/erpnext/stock/report/incorrect_serial_no_valuation/__init__.py
diff --git a/erpnext/stock/report/incorrect_serial_no_valuation/incorrect_serial_no_valuation.js b/erpnext/stock/report/incorrect_serial_no_valuation/incorrect_serial_no_valuation.js
new file mode 100644
index 0000000..c62d480
--- /dev/null
+++ b/erpnext/stock/report/incorrect_serial_no_valuation/incorrect_serial_no_valuation.js
@@ -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")
+ }
+ ]
+};
diff --git a/erpnext/stock/report/incorrect_serial_no_valuation/incorrect_serial_no_valuation.json b/erpnext/stock/report/incorrect_serial_no_valuation/incorrect_serial_no_valuation.json
new file mode 100644
index 0000000..cc384a5
--- /dev/null
+++ b/erpnext/stock/report/incorrect_serial_no_valuation/incorrect_serial_no_valuation.json
@@ -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"
+ }
+ ]
+}
\ No newline at end of file
diff --git a/erpnext/stock/report/incorrect_serial_no_valuation/incorrect_serial_no_valuation.py b/erpnext/stock/report/incorrect_serial_no_valuation/incorrect_serial_no_valuation.py
new file mode 100644
index 0000000..e54cf4c
--- /dev/null
+++ b/erpnext/stock/report/incorrect_serial_no_valuation/incorrect_serial_no_valuation.py
@@ -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
+ }]
\ No newline at end of file
diff --git a/erpnext/stock/stock_ledger.py b/erpnext/stock/stock_ledger.py
index fc82c78..9fe89c3 100644
--- a/erpnext/stock/stock_ledger.py
+++ b/erpnext/stock/stock_ledger.py
@@ -22,6 +22,7 @@
# _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 @@
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 @@
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 @@
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):
diff --git a/erpnext/stock/utils.py b/erpnext/stock/utils.py
index 034d3eb..8a6a3a3 100644
--- a/erpnext/stock/utils.py
+++ b/erpnext/stock/utils.py
@@ -177,7 +177,7 @@
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)
diff --git a/erpnext/stock/workspace/stock/stock.json b/erpnext/stock/workspace/stock/stock.json
index 3221dc4..529ce8e 100644
--- a/erpnext/stock/workspace/stock/stock.json
+++ b/erpnext/stock/workspace/stock/stock.json
@@ -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",
diff --git a/erpnext/support/report/first_response_time_for_issues/first_response_time_for_issues.js b/erpnext/support/report/first_response_time_for_issues/first_response_time_for_issues.js
index 576e0b7..18691fe 100644
--- a/erpnext/support/report/first_response_time_for_issues/first_response_time_for_issues.js
+++ b/erpnext/support/report/first_response_time_for_issues/first_response_time_for_issues.js
@@ -22,10 +22,10 @@
get_chart_data: function(_columns, result) {
return {
data: {
- labels: result.map(d => d[0]),
+ labels: result.map(d => d.creation_date),
datasets: [{
name: 'First Response Time',
- values: result.map(d => d[1])
+ values: result.map(d => d.first_response_time)
}]
},
type: "line",
@@ -35,8 +35,7 @@
hide_days: 0,
hide_seconds: 0
};
- value = frappe.utils.get_formatted_duration(d, duration_options);
- return value;
+ return frappe.utils.get_formatted_duration(d, duration_options);
}
}
}
diff --git a/erpnext/templates/includes/cart/cart_address.html b/erpnext/templates/includes/cart/cart_address.html
index 84a9430..4482bc1 100644
--- a/erpnext/templates/includes/cart/cart_address.html
+++ b/erpnext/templates/includes/cart/cart_address.html
@@ -99,6 +99,7 @@
fieldname: 'country',
fieldtype: 'Link',
options: 'Country',
+ only_select: true,
reqd: 1
},
{
diff --git a/erpnext/templates/includes/transaction_row.html b/erpnext/templates/includes/transaction_row.html
index 3834131..3cfb8d8 100644
--- a/erpnext/templates/includes/transaction_row.html
+++ b/erpnext/templates/includes/transaction_row.html
@@ -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>
diff --git a/erpnext/tests/test_subcontracting.py b/erpnext/tests/test_subcontracting.py
new file mode 100644
index 0000000..8b0ce09
--- /dev/null
+++ b/erpnext/tests/test_subcontracting.py
@@ -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)
\ No newline at end of file